Back to catalogue
ValidationStopStopWhen the agent finishes its task· non-blocking
Full i18n validation (Stop)
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.
Use cases
- Prevent shipping untranslated strings or orphan keys
- Validate translations automatically without waiting for CI
Providers & tags
Claude Code
#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);
}