152 lines
3.8 KiB
JavaScript
152 lines
3.8 KiB
JavaScript
|
|
#!/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();
|
||
|
|
|