Back to catalogue
ValidationStopStopWhen the agent finishes its task· non-blocking
Full i18n validation (Stop)
Ship no untranslated string, no orphan key
Validates the consistency of translation files on every session stop: every key used in the code exists in the i18n files, and no orphan key lingers. Runs only if frontend files were modified.
What does the Full i18n validation (Stop) hook do?
Full i18n validation (Stop) is a Claude Code Stop hook. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Ship no untranslated string, no orphan key.
Use cases
- Prevent shipping untranslated strings or orphan keys
- Validate translations automatically without waiting for CI
Tags
#i18n#translations#validation#frontend#definition-of-done
settings.json fragment
{
"hooks": {
"Stop": [
{
"hooks": [
{
"command": "node .claude/hooks/i18n-validation.mjs",
"type": "command"
}
]
}
]
}
}Script · .claude/hooks/i18n-validation.mjs
#!/usr/bin/env node
// Valide la cohérence des fichiers de traduction (Stop)
import { readFileSync, existsSync } from 'fs';
import { execSync } from 'child_process';
import { join } from 'path';
import { fileURLToPath } from 'url';
export function run({
exec,
readFile = readFileSync,
projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd(),
} = {}) {
const doExec =
exec ?? ((cmd) => execSync(cmd, { encoding: 'utf8', timeout: 5_000, cwd: projectDir }).trim());
// Cherche les fichiers de traduction JSON (ex: locales/fr.json, messages/en.json)
const i18nFiles = doExec('find . -path ./node_modules -prune -o -name "*.json" -print')
.split('\n')
.filter((f) => /\/(locales?|messages?|i18n)\//i.test(f) && f.endsWith('.json'));
if (i18nFiles.length < 2) return null;
// Groupe par répertoire et vérifie la cohérence des clés
const byDir = {};
for (const f of i18nFiles) {
const dir = f.split('/').slice(0, -1).join('/');
byDir[dir] ??= [];
byDir[dir].push(f);
}
const issues = [];
for (const [, files] of Object.entries(byDir)) {
if (files.length < 2) continue;
const parsed = files
.map((f) => {
try { return { f, keys: new Set(Object.keys(JSON.parse(readFile(join(projectDir, f), 'utf8')))) }; } catch { return null; }
})
.filter(Boolean);
const allKeys = new Set(parsed.flatMap((p) => [...p.keys]));
for (const { f, keys } of parsed) {
const missing = [...allKeys].filter((k) => !keys.has(k));
if (missing.length > 0)
issues.push(`${f} manque ${missing.length} clé(s) : ${missing.slice(0, 5).join(', ')}${missing.length > 5 ? '…' : ''}`);
}
}
const message =
issues.length > 0
? `[i18n-validation] Incohérences détectées :\n${issues.map((i) => ` - ${i}`).join('\n')}\n`
: '[i18n-validation] ✓ Fichiers de traduction cohérents.\n';
return { issues, message };
}
/* v8 ignore next 4 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const result = run();
if (result) process.stderr.write(result.message);
}