--- name: cm-safe-i18n description: Use when translating, extracting, or mass-converting hardcoded strings to i18n t() calls. Enforces multi-pass batching, parallel-per-language dispatch, 8 audit gates, and HTML integrity checks. Battle-tested through 21+ batches and 12 bug categories from the March 2026 incidents. --- # Safe i18n Translation v2.0 ## Overview Mass i18n conversion is the most dangerous code transformation in a frontend monolith. A single-pass conversion of 600+ strings corrupted `app.js` beyond repair while 572 backend tests passed green. Additional incidents include HTML tag corruption, variable shadowing, and placeholder translation errors. **Core principle:** Every batch of i18n changes MUST pass ALL 8 audit gates before proceeding. No exceptions. **Violating the letter of this rule is violating the spirit of this rule.** ## The Iron Law ``` NO BATCH WITHOUT PASSING ALL 8 AUDIT GATES. NO LANGUAGE FILE WITHOUT KEY PARITY. NO DEPLOY WITHOUT FULL SYNTAX VALIDATION. NO HTML TAG MODIFICATION — TEXT CONTENT ONLY. NO REGEX TO FIX REGEX ERRORS — USE LEXICAL SCANNER. ``` ## When to Use **ALWAYS** when any of these happen: - Extracting hardcoded strings to `t()` calls - Adding new language file (e.g., `ph.json`) - Mass-converting strings across >10 lines - Updating translation keys or namespaces - Migrating i18n library or pattern **Don't use for:** - Adding 1-3 translation keys (just add manually + test) - Fixing a single typo in a JSON file ## The Protocol ```dot digraph i18n_flow { rankdir=TB; "0. Pre-flight" [shape=box]; "1. Scan ALL files" [shape=box]; ">10 strings?" [shape=diamond]; "Manual add + test" [shape=box]; "2. Plan passes" [shape=box]; "3. Extract batch (max 30)" [shape=box]; "4. 8-Gate Audit" [shape=box, style=filled, fillcolor="#ffffcc"]; "All 8 pass?" [shape=diamond]; "FIX or ROLLBACK" [shape=box, style=filled, fillcolor="#ffcccc"]; "More batches?" [shape=diamond]; "5. Parallel language sync" [shape=box]; "6. Final validation" [shape=box]; "0. Pre-flight" -> "1. Scan ALL files"; "1. Scan ALL files" -> ">10 strings?"; ">10 strings?" -> "Manual add + test" [label="no"]; ">10 strings?" -> "2. Plan passes" [label="yes"]; "2. Plan passes" -> "3. Extract batch (max 30)"; "3. Extract batch (max 30)" -> "4. 8-Gate Audit"; "4. 8-Gate Audit" -> "All 8 pass?"; "All 8 pass?" -> "FIX or ROLLBACK" [label="no"]; "FIX or ROLLBACK" -> "4. 8-Gate Audit"; "All 8 pass?" -> "More batches?" [label="yes"]; "More batches?" -> "3. Extract batch (max 30)" [label="yes"]; "More batches?" -> "5. Parallel language sync" [label="no"]; "5. Parallel language sync" -> "6. Final validation"; } ``` --- ### Phase 0: Pre-Flight Checks (NEW) Before ANY i18n work: ```bash # NEVER work on main git checkout -b i18n/$(date +%Y%m%d)-target-description # Verify baseline is clean node -c public/static/app.js npm run test:gate ``` If either fails, DO NOT PROCEED. Fix the baseline first. --- ### Phase 1: Scan ALL Frontend Files (IMPROVED) > [!CAUTION] > **Lesson #11:** `import-adapters.js` and `import-engine.js` had 60+ hardcoded strings that were initially missed because only `app.js` was scanned. Scan EVERY file that produces user-visible UI text: ```bash # Scan ALL .js files for Vietnamese strings node scripts/i18n-lint.js # Also check non-app.js files grep -rnP '[àáạảãâầấậẩẫăằắặẳẵ]' public/static/*.js --include="*.js" | grep -v "\.backup" | grep -v "i18n" ``` Group strings by **functional domain** — never by file position: | Pass | Domain | Example Keys | |------|--------|-------------| | 1 | Core UI | `sidebar.*`, `common.*`, `login.*` | | 2 | Primary Feature | `vio.*`, `emp.*`, `scores.*` | | 3 | Config & Settings | `config.*`, `benconf.*` | | 4 | Reports & Export | `report.*`, `export.*` | | 5 | Secondary Files | `import-adapters.js`, `import-engine.js` | | 6 | Edge cases | Tooltips, error messages, dynamic labels | **Output:** A numbered list of passes with estimated string count per pass per FILE. --- ### Phase 2: Extract Batch (MAX 30 strings per batch) > [!CAUTION] > **MAX 30 strings per batch. Not 31. Not "about 30". Exactly 30 or fewer.** > The i18n crash happened because 600+ strings were done in one pass. For each batch: 1. **Identify** up to 30 hardcoded strings in the current pass domain 2. **Generate** namespace-compliant keys: `domain.descriptive_key` 3. **Replace** strings with `t('domain.key')` calls 4. **Add** keys to the **primary** language JSON (usually `vi.json`) #### String Replacement Rules (12 Bug Categories Encoded) ```javascript // ✅ CORRECT — backtick template with t() inside `