Add translation validation git hook

- Create validate-translations.js script to check all translation keys exist in all locales
- Add translation validation to pre-commit hook
- Validates that es, en, ca translation files have matching keys
- Blocks commits if translations are missing or inconsistent
- Prevents incomplete translations from being committed
This commit is contained in:
counterweight 2025-12-26 11:58:09 +01:00
parent 2fdcbbdfe8
commit 63a4b0f8a2
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
2 changed files with 164 additions and 0 deletions

View file

@ -36,6 +36,19 @@ else
fi fi
fi fi
echo ""
echo "🔍 Validating translation files..."
# Check if any translation files are staged
if git diff --cached --name-only | grep -q "frontend/locales/"; then
# Run translation validation
if ! node scripts/validate-translations.js; then
exit 1
fi
else
echo "✓ No translation files staged, skipping validation"
fi
echo "" echo ""
echo "✅ All pre-commit checks passed" echo "✅ All pre-commit checks passed"

151
scripts/validate-translations.js Executable file
View file

@ -0,0 +1,151 @@
#!/usr/bin/env node
/**
* Translation validation script
* Ensures all translation keys exist in all three language files (es, en, ca)
*/
const fs = require("fs");
const path = require("path");
const LOCALES_DIR = path.join(__dirname, "..", "frontend", "locales");
const LOCALES = ["es", "en", "ca"];
const NAMESPACES = [
"admin",
"auth",
"common",
"exchange",
"invites",
"navigation",
"profile",
"trades",
];
/**
* Get all keys from a nested object recursively
*/
function getAllKeys(obj, prefix = "") {
const keys = [];
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
keys.push(...getAllKeys(value, fullKey));
} else {
keys.push(fullKey);
}
}
return keys;
}
/**
* Check if a key exists in an object (supports nested keys like "a.b.c")
*/
function hasKey(obj, keyPath) {
const parts = keyPath.split(".");
let current = obj;
for (const part of parts) {
if (current === null || current === undefined || typeof current !== "object") {
return false;
}
if (!(part in current)) {
return false;
}
current = current[part];
}
return true;
}
/**
* Validate a single namespace across all locales
*/
function validateNamespace(namespace) {
const translations = {};
const missingKeys = {};
// Load all locale files for this namespace
for (const locale of LOCALES) {
const filePath = path.join(LOCALES_DIR, locale, `${namespace}.json`);
if (!fs.existsSync(filePath)) {
console.error(`❌ Missing file: ${filePath}`);
return false;
}
try {
const content = fs.readFileSync(filePath, "utf8");
translations[locale] = JSON.parse(content);
} catch (error) {
console.error(`❌ Error parsing ${filePath}: ${error.message}`);
return false;
}
}
// Get all keys from Spanish (default) as the reference
const referenceKeys = getAllKeys(translations.es);
missingKeys.es = [];
// Check each locale has all keys
for (const locale of LOCALES) {
missingKeys[locale] = [];
for (const key of referenceKeys) {
if (!hasKey(translations[locale], key)) {
missingKeys[locale].push(key);
}
}
}
// Check if any locale is missing keys
let hasErrors = false;
for (const locale of LOCALES) {
if (missingKeys[locale].length > 0) {
hasErrors = true;
console.error(`\n${namespace}.json - Missing keys in ${locale}:`);
for (const key of missingKeys[locale]) {
console.error(` - ${key}`);
}
}
}
// Check for extra keys in other locales (keys that don't exist in Spanish)
for (const locale of LOCALES) {
if (locale === "es") continue;
const localeKeys = getAllKeys(translations[locale]);
const extraKeys = localeKeys.filter((key) => !hasKey(translations.es, key));
if (extraKeys.length > 0) {
hasErrors = true;
console.error(`\n${namespace}.json - Extra keys in ${locale} (not in Spanish):`);
for (const key of extraKeys) {
console.error(` - ${key}`);
}
}
}
return !hasErrors;
}
/**
* Main validation function
*/
function validateTranslations() {
console.log("🔍 Validating translation files...\n");
let allValid = true;
for (const namespace of NAMESPACES) {
if (!validateNamespace(namespace)) {
allValid = false;
}
}
if (!allValid) {
console.error("\n❌ Translation validation failed!");
console.error(" All translation keys must exist in all three language files (es, en, ca)");
console.error(" To skip this check (not recommended): git commit --no-verify");
process.exit(1);
}
console.log("✅ All translation files are valid");
return true;
}
// Run validation
validateTranslations();