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
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:
# 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.jsandimport-engine.jshad 60+ hardcoded strings that were initially missed because onlyapp.jswas scanned.
Scan EVERY file that produces user-visible UI text:
# 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:
- Identify up to 30 hardcoded strings in the current pass domain
- Generate namespace-compliant keys:
domain.descriptive_key - Replace strings with
t('domain.key')calls - Add keys to the primary language JSON (usually
vi.json)
String Replacement Rules (12 Bug Categories Encoded)
// ✅ CORRECT — backtick template with t() inside
`<div>${t('login.welcome')}</div>`
// ✅ CORRECT — concatenation
'<div>' + t('login.welcome') + '</div>'
// ❌ BUG #1 (FATAL) — single-quote wrapping template expression
'${t("login.welcome")}' // ← THIS DESTROYED APP.JS
// ❌ BUG #4 — mismatched delimiters
t('login.welcome`) // ← quote/backtick mismatch
t(`login.welcome') // ← backtick/quote mismatch
Ternary Inside Template Literals (Bug #5)
// ❌ BROKEN — single-quote ternary result with template expression
${ canDo ? '...${t('key')}...' : '' }
// ✅ CORRECT — backtick ternary result
${ canDo ? `...${t('key')}...` : '' }
Variable Shadowing (Bug #3)
// ❌ BROKEN — shadows global t() translation function
items.map((t, i) => `<div>${t('key')}</div>`)
// ✅ CORRECT — use different variable name
items.map((item, i) => `<div>${t('key')}</div>`)
HTML Tag Protection (Bug #2)
// ❌ NEVER modify content inside HTML tags
`< div class="card" >` // spaces inside tags = broken rendering
`style = "color: red"` // space around = breaks attributes
`<!-- text-- >` // broken comment closers
// ✅ ONLY replace text content between tags
`<div class="card">${t('card.title')}</div>`
Static Keys Only (Bug #8)
// ❌ FORBIDDEN — dynamic keys can't be statically validated
t('nav.' + pageName)
t(`messages.${type}`)
// ✅ REQUIRED — static keys only
t('nav.dashboard')
t('nav.employees')
Phase 3: 8-Gate Audit (MANDATORY after every batch)
[!IMPORTANT] All 8 gates must pass. Any failure = STOP and FIX before continuing.
# Gate 1: JavaScript syntax (fast, <1s)
node -c public/static/app.js
# Must output: "public/static/app.js: No syntax errors"
# Gate 2: Syntax check on ALL modified .js files
node -c public/static/import-adapters.js 2>/dev/null
node -c public/static/import-engine.js 2>/dev/null
# Gate 3: Corruption pattern check (catches what node -c misses)
grep -nP "=\s*'[^']*\$\{t\(" public/static/app.js
# Must return 0 matches
# Gate 4: Mismatched delimiter check
grep -nP "t\('[^']*\`\)" public/static/app.js
grep -nP "t\(\`[^']*'\)" public/static/app.js
# Must return 0 matches each
# Gate 5: HTML tag integrity (NEW — Bug #2)
grep -nP "<\s+\w" public/static/app.js | head -5
grep -nP "</\s+\w" public/static/app.js | head -5
grep -nP "--\s+>" public/static/app.js | head -5
grep -nP '\w+\s+=\s+"' public/static/app.js | grep -v "==\|!=\|<=\|>=" | head -5
# Must return 0 matches (excluding legitimate JS operators)
# Gate 6: Variable shadowing check
grep -nP "\.\s*(map|filter|forEach|reduce)\s*\(\s*\(\s*t\s*[,)]" public/static/app.js
# Must return 0 matches
# Gate 7: JSON validity
node -e "JSON.parse(require('fs').readFileSync('public/static/i18n/vi.json'))"
# Gate 8: Full test suite
npm run test:gate
# Must output: 0 failures
Audit Summary Table:
| Gate | Check | Command | Pass Criteria | Bug # Prevented |
|---|---|---|---|---|
| 1 | JS syntax (main) | node -c app.js | No syntax errors | #1, #4 |
| 2 | JS syntax (all files) | node -c *.js | No syntax errors | #11 |
| 3 | Corruption pattern | grep = '..${t( | 0 matches | #1 |
| 4 | Delimiter mismatch | grep mixed delims | 0 matches | #4 |
| 5 | HTML tag integrity | grep < div, </ div | 0 matches | #2 |
| 6 | Variable shadowing | grep .map((t, | 0 matches | #3 |
| 7 | JSON valid | JSON.parse() | No parse errors | #6 |
| 8 | Full test suite | npm run test:gate | 0 failures | #9 |
If ALL 8 gates pass → commit:
git add -A && git commit -