HookStackGitHub
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);
}