import { readFileSync } from "node:fs"; import type { Messages } from "./index.ts"; export interface ExtractedTranslationKey { key: string; file?: string; offset: number; } export function extractTranslationKeys(source: string, file?: string): ExtractedTranslationKey[] { const found = new Map(); const patterns = [ /(?:\b(?:t|\$t)|\bi18n\.t)\s*\(\s*(["'])([^"']+)\1/g, /\bdata-i18n\s*=\s*(["'])([^"']+)\1/g, /\{t:([A-Za-z0-9_.:-]+)\}/g, ]; for (const pattern of patterns) { for (const match of source.matchAll(pattern)) { const key = (match[2] ?? match[1])!.trim(); if (key && !found.has(key)) found.set(key, { key, file, offset: match.index }); } } return [...found.values()].sort((left, right) => left.key.localeCompare(right.key)); } export function extractTranslationKeysFromFiles( files: Iterable, ): ExtractedTranslationKey[] { const found = new Map(); for (const file of files) { for (const item of extractTranslationKeys(readFileSync(file, "utf8"), file)) { found.set(`${item.file}:${item.key}`, item); } } return [...found.values()].sort( (left, right) => String(left.file).localeCompare(String(right.file)) || left.key.localeCompare(right.key), ); } export function flattenMessageKeys(messages: Messages, prefix = ""): string[] { const keys: string[] = []; for (const [name, value] of Object.entries(messages)) { const key = prefix ? `${prefix}.${name}` : name; if (value && typeof value === "object" && !Array.isArray(value)) { keys.push(...flattenMessageKeys(value as Messages, key)); } else keys.push(key); } return keys.sort(); } export function auditLocaleKeys( messages: Record, referenceLocale: string, ): Record { const reference = new Set(flattenMessageKeys(messages[referenceLocale] ?? {})); const result: Record = {}; for (const [locale, value] of Object.entries(messages)) { const keys = new Set(flattenMessageKeys(value)); result[locale] = { missing: [...reference].filter((key) => !keys.has(key)).sort(), extra: [...keys].filter((key) => !reference.has(key)).sort(), }; } return result; } const ACCENTS: Record = { a: "à", e: "ë", i: "ï", o: "ô", u: "ü", A: "À", E: "Ë", I: "Ï", O: "Ô", U: "Ü", }; export function pseudoLocalize(value: string, options: { rtl?: boolean } = {}): string { const parts = value.split(/(\{[^{}]+\}|<[^>]+>)/g); const transformed = parts .map((part) => /^\{[^{}]+\}$|^<[^>]+>$/.test(part) ? part : part.replace(/[aeiouAEIOU]/g, (character) => ACCENTS[character] ?? character), ) .join(""); return options.rtl ? `\u202e[${transformed}]\u202c` : `[${transformed}~~~]`; } export function createPseudoLocale(messages: Messages, options: { rtl?: boolean } = {}): Messages { const output: Messages = Object.create(null) as Messages; for (const [key, value] of Object.entries(messages)) { output[key] = typeof value === "string" ? pseudoLocalize(value, options) : value && typeof value === "object" && !Array.isArray(value) ? createPseudoLocale(value as Messages, options) : value; } return output; }