From 63c8137f4edd3dc3d1a86e37787b878db8b0a52c Mon Sep 17 00:00:00 2001 From: magdev Date: Tue, 30 Dec 2025 05:08:09 +0100 Subject: [PATCH] Document v1.2.9 learnings in CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added three new critical sections documenting lessons learned during v1.2.9 development: 1. WordPress Translation Functions with printf - Correct pattern: printf(esc_html__('Text (%s)', 'domain'), value) - Wrong pattern: printf(__('Text (%s)', 'domain'), value) - missing text domain - Explains why text domain must be in translation function - Shows proper output escaping with esc_html 2. Twig Translation Filters and HTML Entity Encoding - Explains why translation filters encode special characters in concatenated strings - Correct pattern: {{ 'text ' ~ currency_symbol }} (no translation filter) - Wrong pattern: {{ ('text ' ~ currency_symbol)|__() }} causes HTML entity encoding - Rule: Only translate static text, not concatenated dynamic values 3. Defensive Programming for POST Data Processing - Compares v1.2.8 (branching) vs v1.2.9 (defensive) patterns - Shows why single decision point is better than multiple branches - Key principles: initialize early, minimize branching, guaranteed execution - Pattern: initialize → conditionally populate → unconditionally act Also corrected v1.2.8 examples that had wrong patterns, noting they were fixed in v1.2.9. These patterns prevent future bugs and ensure consistent, secure implementation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- CLAUDE.md | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b3bc3a5..f274206 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -821,15 +821,15 @@ WooCommerce has different hooks for different product types in the admin product **Lesson:** When adding admin UI for variable product parents, use `woocommerce_product_options_general_product_data` and check `$product->is_type('variable')` to conditionally display. Using `woocommerce_product_options_pricing` will cause forms to never appear for variable products (as discovered in v1.2.6 → v1.2.7 fix). -#### CRITICAL: Currency Symbol Display (Learned in v1.2.8) +#### CRITICAL: Currency Symbol Display (Learned in v1.2.8, Corrected in v1.2.9) When displaying currency symbols in admin interface table headers and input placeholders: **Table Headers:** ```php -// ✅ Correct - Use printf with translation and WooCommerce currency function - +// ✅ Correct - Use printf with esc_html__ for translation (CORRECTED in v1.2.9) + // ❌ Wrong - Hard-coded or missing currency @@ -839,8 +839,8 @@ When displaying currency symbols in admin interface table headers and input plac **Twig Template Placeholders:** ```twig -{# ✅ Correct - Pass currency_symbol from PHP and concatenate in template #} -placeholder="{{ ('e.g., 9.99 ' ~ currency_symbol)|__('wc-tier-package-prices') }}" +{# ✅ Correct - Pass currency_symbol from PHP and concatenate (CORRECTED in v1.2.9 - no translation filter) #} +placeholder="{{ 'e.g., 9.99 ' ~ currency_symbol }}" {# ❌ Wrong - Hard-coded or missing currency #} placeholder="{{ 'e.g., 9.99'|__('wc-tier-package-prices') }}" @@ -917,6 +917,133 @@ if (isset($_POST['_wc_tpp_tiers'])) { **Rule:** Always check `if (!empty($array))` before calling `update_post_meta()` for array data. If empty, call `delete_post_meta()` instead. +#### CRITICAL: WordPress Translation Functions with printf (Learned in v1.2.9) + +When using `printf()` with WordPress translation functions, the text domain must be passed to the translation function, NOT to printf: + +**Wrong Pattern:** + +```php +// ❌ WRONG - Text domain not passed to translation function +printf(__('Price (%s)', 'wc-tier-package-prices'), get_woocommerce_currency_symbol()); +``` + +**Problem:** The `__()` function receives the text domain as a second parameter, but in this pattern it's missing. This causes the string "Price (%s)" to not be found in translation files, so it won't be translated. + +**Correct Pattern:** + +```php +// ✅ CORRECT - Text domain in translation function, with esc_html for output escaping +printf(esc_html__('Price (%s)', 'wc-tier-package-prices'), get_woocommerce_currency_symbol()); +``` + +**Why This Works:** + +- `esc_html__('Price (%s)', 'wc-tier-package-prices')` translates the string and returns it +- `printf()` then substitutes the `%s` placeholder with the currency symbol +- The translated string is used in the final output +- `esc_html` ensures proper output escaping + +**Applied in v1.2.9:** All 6 table headers in `includes/class-wc-tpp-product-meta.php` + +#### CRITICAL: Twig Translation Filters and HTML Entity Encoding (Learned in v1.2.9) + +When concatenating dynamic values in Twig templates, applying the translation filter can cause HTML entity encoding issues: + +**Wrong Pattern:** + +```twig +{# ❌ WRONG - Translation filter encodes special characters in concatenated string #} +placeholder="{{ ('e.g., 9.99 ' ~ currency_symbol)|__('wc-tier-package-prices') }}" +``` + +**Problem:** When `currency_symbol` contains special characters (€, £, ¥, etc.), the concatenated string is passed through the translation function which treats it as a translatable string and encodes special characters as HTML entities (`€`, `£`, etc.). + +**Correct Pattern:** + +```twig +{# ✅ CORRECT - No translation filter, just concatenation #} +placeholder="{{ 'e.g., 9.99 ' ~ currency_symbol }}" +``` + +**Why This Works:** + +- Placeholder examples don't need translation - they're illustrative values +- Direct concatenation preserves special characters +- Currency symbol displays correctly (€ instead of €) + +**Rule:** Only apply translation filters to static text that needs translation, not to concatenated strings with dynamic values that contain special characters. + +**Applied in v1.2.9:** + +- `templates/admin/tier-row.twig` - Price input placeholder +- `templates/admin/package-row.twig` - Price input placeholder + +#### CRITICAL: Defensive Programming for POST Data Processing (Learned in v1.2.9) + +The v1.2.8 fix for variation pricing deletion had the right logic but used a branching structure that could miss edge cases. The v1.2.9 refactor demonstrates a more defensive pattern: + +**Less Defensive Pattern (v1.2.8):** + +```php +// ❌ BRITTLE - Multiple branches, easy to miss edge cases +if (isset($_POST['wc_tpp_tiers'][$loop])) { + $tiers = array(); + foreach ($_POST['wc_tpp_tiers'][$loop] as $tier) { + // ... populate tiers ... + } + if (!empty($tiers)) { + update_post_meta($variation_id, '_wc_tpp_tiers', $tiers); + } else { + delete_post_meta($variation_id, '_wc_tpp_tiers'); + } +} else { + delete_post_meta($variation_id, '_wc_tpp_tiers'); +} +``` + +**Problem:** Two separate code paths to `delete_post_meta()`. If logic changes, easy to update one path but forget the other. + +**More Defensive Pattern (v1.2.9):** + +```php +// ✅ DEFENSIVE - Single decision point, guaranteed cleanup +$tiers = array(); +if (isset($_POST['wc_tpp_tiers'][$loop]) && is_array($_POST['wc_tpp_tiers'][$loop])) { + foreach ($_POST['wc_tpp_tiers'][$loop] as $tier) { + // ... populate tiers ... + } +} +// Always execute one of these based on final state +if (!empty($tiers)) { + update_post_meta($variation_id, '_wc_tpp_tiers', $tiers); +} else { + delete_post_meta($variation_id, '_wc_tpp_tiers'); +} +``` + +**Why This Is Better:** + +- Initialize array at the start (guaranteed initial state) +- Single conditional for populating (with extra `is_array()` safety check) +- Single decision point for save/delete (one place to maintain) +- Impossible to have a code path that doesn't call either update or delete +- Much easier to reason about and modify + +**Key Principles:** + +1. **Initialize variables early** - Establish known initial state +2. **Minimize branching** - Fewer code paths = fewer bugs +3. **Single decision point** - One place determines final action +4. **Add safety checks** - Validate assumptions (`is_array()`) +5. **Guaranteed execution** - Always perform one of update/delete, never neither + +**Applied in v1.2.9:** + +- `save_variation_pricing_fields()` - Both tier and package pricing logic refactored + +**Rule:** When processing user input to decide between update and delete, prefer the pattern: initialize → conditionally populate → unconditionally act based on final state. + ### When Adding Features - Follow the existing pattern: add setting → add UI → add logic → add template