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:
parent
2fdcbbdfe8
commit
63a4b0f8a2
2 changed files with 164 additions and 0 deletions
|
|
@ -36,6 +36,19 @@ else
|
|||
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 "✅ All pre-commit checks passed"
|
||||
|
||||
|
|
|
|||
151
scripts/validate-translations.js
Executable file
151
scripts/validate-translations.js
Executable 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();
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue