404
- The page you are looking for could not be found. + {t("description")}
- Go back home + {t("home")}- The page you are looking for could not be found. + {t("description")}
- Go back home + {t("home")}Hello world
but allows: + * - Strings inside t() calls + * - className attributes + * - data-testid attributes + * - aria-label with t() wrapper + * - Short technical strings (< 3 chars) + */ + +import { readFileSync, readdirSync, statSync } from "fs"; +import { resolve, dirname, relative } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const frontendDir = resolve(__dirname, ".."); + +function getTsxFiles(dir) { + const files = []; + for (const entry of readdirSync(dir)) { + const full = resolve(dir, entry); + if (entry === "node_modules" || entry === ".next" || entry === "public") continue; + if (statSync(full).isDirectory()) { + files.push(...getTsxFiles(full)); + } else if (/\.(tsx|jsx)$/.test(entry) && !entry.endsWith(".d.ts")) { + files.push(full); + } + } + return files; +} + +// Pattern: text content between > and < that looks like user-facing text +const HARDCODED_PATTERN = />([A-Z][a-zA-Z\s]{4,}) p.test(line))) continue; + if (line.includes("t(") || line.includes("getTranslations")) continue; + + const matches = [...line.matchAll(HARDCODED_PATTERN)]; + for (const match of matches) { + const text = match[1].trim(); + // Skip very short or obviously non-user-facing strings + if (text.length < 5) continue; + if (/^[A-Z_]+$/.test(text)) continue; // constants like "PDF", "URL" + if (/^\d/.test(text)) continue; // starts with number + + console.warn(`⚠ ${relPath}:${i + 1} — Possible hardcoded string: "${text}"`); + hasWarnings = true; + } + } +} + +if (hasWarnings) { + console.log("\n💡 Consider wrapping hardcoded strings with translation functions (t())."); + console.log(" These warnings don't fail CI but help maintain i18n coverage."); + // Don't fail CI yet — this is advisory + process.exit(0); +} else { + console.log("✓ No obvious hardcoded user-facing strings found."); +} diff --git a/frontend/scripts/check-locales.mjs b/frontend/scripts/check-locales.mjs new file mode 100644 index 0000000..a7cafe4 --- /dev/null +++ b/frontend/scripts/check-locales.mjs @@ -0,0 +1,67 @@ +#!/usr/bin/env node + +/** + * CI script: Verify all locale files have identical key sets. + * Exits with code 1 if key sets diverge. + */ + +import { readFileSync } from "fs"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const messagesDir = resolve(__dirname, "..", "messages"); + +const locales = ["en", "es", "fr"]; + +function flattenKeys(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(...flattenKeys(value, fullKey)); + } else { + keys.push(fullKey); + } + } + return keys.sort(); +} + +let allKeys = {}; +let hasError = false; + +for (const locale of locales) { + const filePath = resolve(messagesDir, `${locale}.json`); + try { + const content = JSON.parse(readFileSync(filePath, "utf-8")); + allKeys[locale] = flattenKeys(content); + } catch (err) { + console.error(`Failed to read ${filePath}: ${err.message}`); + process.exit(1); + } +} + +const referenceLocale = "en"; +const referenceKeys = allKeys[referenceLocale]; + +for (const locale of locales.filter((l) => l !== referenceLocale)) { + const localeKeys = allKeys[locale]; + const missingInLocale = referenceKeys.filter((k) => !localeKeys.includes(k)); + const extraInLocale = localeKeys.filter((k) => !referenceKeys.includes(k)); + + if (missingInLocale.length > 0) { + console.error(`[${locale}] Missing keys: ${missingInLocale.join(", ")}`); + hasError = true; + } + if (extraInLocale.length > 0) { + console.error(`[${locale}] Extra keys: ${extraInLocale.join(", ")}`); + hasError = true; + } +} + +if (hasError) { + console.error("\nLocale key sets are not in sync. Please update all locale files."); + process.exit(1); +} else { + console.log(`✓ All locale files (${locales.join(", ")}) have identical key sets (${referenceKeys.length} keys).`); +}