From 63a4b0f8a29966012587a4936a63974c82e07c9e Mon Sep 17 00:00:00 2001 From: counterweight Date: Fri, 26 Dec 2025 11:58:09 +0100 Subject: [PATCH] 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 --- .githooks/pre-commit | 13 +++ scripts/validate-translations.js | 151 +++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100755 scripts/validate-translations.js diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 5464890..d362f9e 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -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" diff --git a/scripts/validate-translations.js b/scripts/validate-translations.js new file mode 100755 index 0000000..b1f8f1a --- /dev/null +++ b/scripts/validate-translations.js @@ -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(); +