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