Skip to content

πŸ“‹ Full Skill Source β€” This is the complete, unedited SKILL.md file. Nothing is hidden or summarized.

← Back to Skills Library

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:

PassDomainExample Keys
1Core UIsidebar.*, common.*, login.*
2Primary Featurevio.*, emp.*, scores.*
3Config & Settingsconfig.*, benconf.*
4Reports & Exportreport.*, export.*
5Secondary Filesimport-adapters.js, import-engine.js
6Edge casesTooltips, 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
`<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) ​

javascript
// ❌ BROKEN β€” single-quote ternary result with template expression
${ canDo ? '...${t('key')}...' : '' }

// βœ… CORRECT β€” backtick ternary result
${ canDo ? `...${t('key')}...` : '' }

Variable Shadowing (Bug #3) ​

javascript
// ❌ 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) ​

javascript
// ❌ 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) ​

javascript
// ❌ 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.

bash
# 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:

GateCheckCommandPass CriteriaBug # Prevented
1JS syntax (main)node -c app.jsNo syntax errors#1, #4
2JS syntax (all files)node -c *.jsNo syntax errors#11
3Corruption patterngrep = '..${t(0 matches#1
4Delimiter mismatchgrep mixed delims0 matches#4
5HTML tag integritygrep < div, </ div0 matches#2
6Variable shadowinggrep .map((t,0 matches#3
7JSON validJSON.parse()No parse errors#6
8Full test suitenpm run test:gate0 failures#9

If ALL 8 gates pass β†’ commit:

bash
git add -A && git commit -m "i18n pass N batch M/T: domain description (X strings)"

If ANY gate fails β†’ FIX immediately. Do NOT proceed to next batch.

If fix attempt uses regex β†’ STOP. Use the lexical scanner instead (Bug #10).


Phase 4: Parallel Language Sync ​

REQUIRED SUB-SKILL: Use cm-execution (Parallel mode).

After ALL strings are extracted to the primary language, sync remaining languages in parallel:

Agent 1 β†’ Translate all keys to en.json (English)
Agent 2 β†’ Translate all keys to th.json (Thai)  
Agent 3 β†’ Translate all keys to ph.json (Filipino)

Each agent prompt MUST include:

markdown
Translate the following i18n keys from vi.json to [LANGUAGE]:

Source file: public/static/lang/vi.json
Target file: public/static/lang/[LANG].json

Rules:
1. Translate ALL keys β€” missing keys will break the app
2. Keep key names EXACTLY the same (only values change)
3. Keep {{param}} interpolation placeholders intact β€” NEVER translate them
4. Do NOT translate technical terms (e.g., PPH, KPI, CSV)
5. Preserve HTML entities if present in values
6. Do NOT produce empty string "" values β€” every key must have content
7. Preserve the exact same JSON structure/nesting

After translation:
1. Validate JSON: node -e "JSON.parse(require('fs').readFileSync('[LANG].json'))"
2. Count keys must EQUAL vi.json key count
3. No null or empty string values
4. All {{param}} placeholders preserved identically

Return: Key count + any untranslatable terms flagged.

After agents return β€” 3-Point Parity Check:

bash
# Check 1: Key count parity
node -e "
const fs = require('fs');
const langs = ['vi','en','th','ph'];
const counts = langs.map(l => {
  const keys = Object.keys(JSON.parse(fs.readFileSync('public/static/i18n/'+l+'.json')));
  console.log(l + ': ' + keys.length + ' keys');
  return keys.length;
});
if (new Set(counts).size !== 1) {
  console.error('❌ KEY PARITY FAILURE! Counts differ across languages.');
  process.exit(1);
} else {
  console.log('βœ… Key parity: all languages have ' + counts[0] + ' keys');
}
"

# Check 2: Empty value detection (NEW β€” prevents blank UI)
node -e "
const fs = require('fs');
const langs = ['vi','en','th','ph'];
let hasEmpty = false;
for (const lang of langs) {
  const data = JSON.parse(fs.readFileSync('public/static/i18n/' + lang + '.json'));
  const check = (obj, prefix) => {
    for (const [k, v] of Object.entries(obj)) {
      const key = prefix ? prefix + '.' + k : k;
      if (v === '' || v === null) { console.error('❌ Empty value: ' + lang + ':' + key); hasEmpty = true; }
      if (typeof v === 'object' && v !== null) check(v, key);
    }
  };
  check(data, '');
}
if (hasEmpty) process.exit(1);
console.log('βœ… No empty values');
"

# Check 3: Placeholder preservation (NEW β€” Bug #7)
node -e "
const fs = require('fs');
const vi = JSON.parse(fs.readFileSync('public/static/i18n/vi.json'));
const flatten = (obj, pre='') => Object.entries(obj).reduce((a, [k,v]) => {
  const key = pre ? pre+'.'+k : k;
  if (typeof v === 'object' && v !== null && !Array.isArray(v)) return [...a, ...flatten(v, key)];
  return [...a, [key, v]];
}, []);
const viFlat = Object.fromEntries(flatten(vi));
let errors = 0;
for (const lang of ['en','th','ph']) {
  const other = Object.fromEntries(flatten(JSON.parse(fs.readFileSync('public/static/i18n/'+lang+'.json'))));
  for (const [key, viVal] of Object.entries(viFlat)) {
    if (typeof viVal !== 'string') continue;
    const viParams = (viVal.match(/\{\{[^}]+\}\}/g) || []).sort().join(',');
    const otherVal = other[key] || '';
    const otherParams = (otherVal.match(/\{\{[^}]+\}\}/g) || []).sort().join(',');
    if (viParams && viParams !== otherParams) {
      console.error('❌ ' + lang + ':' + key + ' placeholder mismatch: vi=' + viParams + ' ' + lang + '=' + otherParams);
      errors++;
    }
  }
}
if (errors) process.exit(1);
console.log('βœ… All placeholders preserved');
"

Phase 5: Final Validation ​

bash
# 1. Full syntax check on ALL frontend files
for f in public/static/app.js public/static/import-adapters.js public/static/import-engine.js; do
  [ -f "$f" ] && node -c "$f"
done

# 2. Full test gate (includes frontend-safety + i18n-sync tests)
npm run test:gate

# 3. Build
npm run build

# 4. Remaining hardcoded scan (should be ~0)
node scripts/i18n-lint.js

# 5. Manual smoke test β€” switch languages in browser

Commit and merge:

bash
git add -A && git commit -m "i18n: complete [scope] - N strings across M languages"
git checkout main
git merge i18n/...

Quick Reference: Key Naming Convention ​

NamespaceUsageExample
commonShared UI (buttons, statuses)common.save, common.cancel
loginAuthentication flowslogin.welcome, login.forgot_pw
sidebarNavigation menusidebar.dashboard, sidebar.logout
vioViolations modulevio.confirm_title, vio.level_high

The 13 Bug Categories β€” Quick Reference ​

#BugPatternDetection Gate
1Single-quote wrapping ${t()}= '..${t(..'Gate 3
2HTML tag corruption< div, </ div, -- >Gate 5
3Variable shadowing.map((t,Gate 6
4Mismatched delimiterst('key\), t(`key')`Gate 4
5Ternary nesting trapSingle-quote branch with ${t()}Gate 1 + 3
6Key parity failuresMissing keys in some languagesPhase 4 parity
7Placeholder translation β†’ Phase 4 placeholder check
8Dynamic key concatenationt('nav.' + var)Static analysis
9Backend pass, frontend broken572 tests green, white screenGate 1 + 8
10Regex fixing regexInfinite fix-break loopUse lexical scanner
11Missed filesOnly scanned app.jsPhase 1 scan ALL
12Line number driftStale line refs across batchesTarget by function name
13Flat vs nested key count mismatchTest vs Gate metricsi18n-sync test design

Red Flags β€” STOP Immediately ​

  • ❌ Converting >30 strings without running ALL 8 audit gates
  • ❌ Using find-replace without verifying backtick vs single-quote context
  • ❌ Committing all translations in a single commit
  • ❌ Skipping key parity check across language files
  • ❌ "It's just a string replacement, it'll be fine"

Rationalization Table ​

ExcuseReality
"It's just find-replace"Find-replace destroyed app.js. MAX 30 strings per batch.
"The regex handles everything"Regex false-positives crashed the app. 8 audit gates after each batch.
"I'll test at the end"You won't find which of 600 changes broke it. Test after each 30.
"One commit is cleaner"One commit = one rollback point for 600 changes. Granular commits.

Integration with Other Skills ​

SkillWhen
cm-quality-gateFinal test gate before deploy
cm-executionPhase 4: Parallel language translation
cm-terminalWhile running audit commands

The Bottom Line ​

30 strings per batch. 8 audit gates after each. No exceptions.

The i18n incidents of March 2026 produced 12 distinct bug categories. This skill encodes protections against every single one. Follow the protocol exactly.

Open Source AI Agent Skills Framework