arbret/scripts/validate-translations.js

152 lines
3.8 KiB
JavaScript
Raw Normal View History

#!/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();