From 1ef337801d3634c9d25c749e092ecdd67d352464 Mon Sep 17 00:00:00 2001 From: goddonebianu Date: Wed, 18 Mar 2026 08:13:58 +0800 Subject: [PATCH] refactor: unify i18n function calls and simplify scripts - Replace dynamic t(lang.labelKey) with static t(lang.label) using helper functions - Add getLanguageLabel/getLangLabel/getLocaleLabel helper functions for switch-based label lookup - Simplify translation check scripts to only detect literal string calls - Fix namespace lookup for dotted namespaces like 'memorize.review' --- scripts/find-missing-translations.ts | 234 +++++++---------------- scripts/find-unused-translations.ts | 202 +++++++------------ src/app/(features)/text-speaker/page.tsx | 42 ++-- src/app/(features)/translator/page.tsx | 64 ++++--- src/app/decks/[deck_id]/AddCardModal.tsx | 17 +- src/components/ui/LocaleSelector.tsx | 22 ++- 6 files changed, 239 insertions(+), 342 deletions(-) diff --git a/scripts/find-missing-translations.ts b/scripts/find-missing-translations.ts index 04cdf6e..20b6bd7 100644 --- a/scripts/find-missing-translations.ts +++ b/scripts/find-missing-translations.ts @@ -10,77 +10,44 @@ const SRC_DIR = "./src"; const MESSAGES_DIR = "./messages"; const ALL_LOCALES = ["en-US", "zh-CN", "ja-JP", "ko-KR", "de-DE", "fr-FR", "it-IT", "ug-CN"]; -interface TranslationUsage { - file: string; - line: number; - namespace: string; - key: string; - isDynamic: boolean; -} - -function parseStringLiteral(s: string): string | null { +function parseString(s: string): string | null { s = s.trim(); - if (s.startsWith('"') && s.endsWith('"')) return s.slice(1, -1); - if (s.startsWith("'") && s.endsWith("'")) return s.slice(1, -1); - if (s.startsWith("`") && s.endsWith("`")) { - return s.includes("${") || s.includes("+") ? null : s.slice(1, -1); + if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + return s.slice(1, -1); + } + if (s.startsWith("`") && s.endsWith("`") && !s.includes("${")) { + return s.slice(1, -1); } return null; } -function extractTranslationBindings(content: string): Map { +function getBindings(content: string): Map { const bindings = new Map(); + const pattern = /(?:const|let|var)\s+(\w+)\s*=\s*(?:await\s+)?(?:useTranslations|getTranslations)\s*\(\s*([^)]*)\s*\)/g; - const patterns = [ - /(?:const|let|var)\s+(\w+)\s*=\s*(?:await\s+)?(?:useTranslations|getTranslations)\s*\(\s*([^)]*)\s*\)/g, - /(?:const|let|var)\s*\{\s*(\w+)\s*\}\s*=\s*await\s+getTranslations\s*\(\s*([^)]*)\s*\)/g, - ]; - - for (const pattern of patterns) { - let match; - while ((match = pattern.exec(content)) !== null) { - const varName = match[1]; - const arg = match[2].trim(); - if (arg === "") { - bindings.set(varName, "__ROOT__"); - } else { - const ns = parseStringLiteral(arg); - if (ns !== null) bindings.set(varName, ns); - } - } + let match; + while ((match = pattern.exec(content)) !== null) { + const varName = match[1]; + const arg = match[2].trim(); + bindings.set(varName, arg ? parseString(arg) || "" : "__ROOT__"); } return bindings; } -function extractTranslationCalls( - content: string, - filePath: string, - bindings: Map -): TranslationUsage[] { - const usages: TranslationUsage[] = []; +function getUsages(content: string, file: string): { file: string; line: number; ns: string; key: string }[] { + const usages: { file: string; line: number; ns: string; key: string }[] = []; + const bindings = getBindings(content); const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; - const lineNum = i + 1; - - for (const [varName, namespace] of bindings) { - const callPattern = new RegExp( - `\\b${varName}\\s*\\(\\s*("[^"]*"|'[^']*'|\`[^\`]*\`)(?:\\s*,|\\s*\\))`, - "g" - ); - + for (const [varName, ns] of bindings) { + const pattern = new RegExp(`\\b${varName}\\s*\\(\\s*("[^"]*"|'[^']*'|\`[^\`]*\`)(?:\\s*,|\\s*\\))`, "g"); let match; - while ((match = callPattern.exec(line)) !== null) { - const arg = match[1]; - const key = parseStringLiteral(arg); - - if (key !== null) { - usages.push({ file: filePath, line: lineNum, namespace, key, isDynamic: false }); - } else { - usages.push({ file: filePath, line: lineNum, namespace, key: arg, isDynamic: true }); - } + while ((match = pattern.exec(line)) !== null) { + const key = parseString(match[1]); + if (key) usages.push({ file, line: i + 1, ns, key }); } } } @@ -88,153 +55,88 @@ function extractTranslationCalls( return usages; } -function getAllFiles(dir: string, extensions: string[]): string[] { +function getFiles(dir: string): string[] { const files: string[] = []; if (!fs.existsSync(dir)) return files; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...getAllFiles(fullPath, extensions)); - } else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) { - files.push(fullPath); - } + const p = path.join(dir, entry.name); + if (entry.isDirectory()) files.push(...getFiles(p)); + else if (entry.isFile() && /\.(tsx?|ts)$/.test(entry.name)) files.push(p); } - return files; } -function navigateToObject(obj: Record, path: string): unknown { - if (typeof obj[path] !== "undefined") return obj[path]; +function keyExists(key: string, ns: string, trans: Record): boolean { + let obj: unknown; - let current: unknown = obj; - for (const part of path.split(".")) { - if (typeof current !== "object" || current === null) return undefined; - current = (current as Record)[part]; - } - return current; -} - -function keyExists(key: string, namespace: string, translations: Record): boolean { - let targetObj: unknown; - - if (namespace === "__ROOT__") { - targetObj = translations; + if (ns === "__ROOT__") { + obj = trans; } else { - targetObj = navigateToObject(translations, namespace); + obj = trans[ns]; + if (typeof obj !== "object" || obj === null) { + obj = trans; + for (const part of ns.split(".")) { + if (typeof obj !== "object" || obj === null) return false; + obj = (obj as Record)[part]; + } + } } - if (typeof targetObj !== "object" || targetObj === null) return false; + if (typeof obj !== "object" || obj === null) return false; - const target = targetObj as Record; - - if (typeof target[key] === "string") return true; - - let current: unknown = target; for (const part of key.split(".")) { - if (typeof current !== "object" || current === null) return false; - current = (current as Record)[part]; + if (typeof obj !== "object" || obj === null) return false; + obj = (obj as Record)[part]; } - return typeof current === "string"; + return typeof obj === "string"; } function main() { - const targetLocale = process.argv[2]; - const localesToCheck = targetLocale ? [targetLocale] : ALL_LOCALES; + const locales = process.argv[2] ? [process.argv[2]] : ALL_LOCALES; - console.log("Scanning source files...\n"); + const files = getFiles(SRC_DIR); + const usages: { file: string; line: number; ns: string; key: string }[] = []; - const sourceFiles = getAllFiles(SRC_DIR, [".tsx", ".ts"]); - const allUsages: TranslationUsage[] = []; - - for (const filePath of sourceFiles) { - const content = fs.readFileSync(filePath, "utf-8"); - const bindings = extractTranslationBindings(content); - const usages = extractTranslationCalls(content, filePath, bindings); - allUsages.push(...usages); + for (const f of files) { + usages.push(...getUsages(fs.readFileSync(f, "utf-8"), f)); } - const uniqueUsages = new Map(); - for (const usage of allUsages) { - const key = `${usage.file}:${usage.line}:${usage.namespace}:${usage.key}`; - uniqueUsages.set(key, usage); + const unique = new Map(); + for (const u of usages) { + unique.set(`${u.file}:${u.line}:${u.ns}:${u.key}`, u); } - const dedupedUsages = Array.from(uniqueUsages.values()); + console.log(`Scanned ${files.length} files, ${unique.size} usages\n`); - console.log(`Found ${sourceFiles.length} files, ${dedupedUsages.length} translation usages\n`); - - for (const locale of localesToCheck) { - console.log(`\n${"=".repeat(60)}`); - console.log(`Locale: ${locale}`); - console.log("=".repeat(60)); + for (const locale of locales) { + console.log(`\n${"=".repeat(50)}\nLocale: ${locale}\n${"=".repeat(50)}`); - const translationPath = path.join(MESSAGES_DIR, `${locale}.json`); - - if (!fs.existsSync(translationPath)) { - console.log(`File not found: ${translationPath}`); + const filePath = path.join(MESSAGES_DIR, `${locale}.json`); + if (!fs.existsSync(filePath)) { + console.log(`File not found: ${filePath}`); continue; } - let translations: Record; - try { - translations = JSON.parse(fs.readFileSync(translationPath, "utf-8")); - } catch (e) { - console.log(`Failed to parse: ${translationPath}`); - console.log(e); - continue; - } + const trans = JSON.parse(fs.readFileSync(filePath, "utf-8")); + const missing = Array.from(unique.values()).filter(u => !keyExists(u.key, u.ns, trans)); - const missing: TranslationUsage[] = []; - const dynamic: TranslationUsage[] = []; - - for (const usage of dedupedUsages) { - if (usage.isDynamic) { - dynamic.push(usage); - } else if (!keyExists(usage.key, usage.namespace, translations)) { - missing.push(usage); - } - } - - if (missing.length === 0 && dynamic.length === 0) { + if (missing.length === 0) { console.log("All translations exist!"); } else { - if (missing.length > 0) { - console.log(`\nMissing ${missing.length} translations:\n`); - - const byFile = new Map(); - for (const usage of missing) { - if (!byFile.has(usage.file)) byFile.set(usage.file, []); - byFile.get(usage.file)!.push(usage); - } - - for (const [file, usages] of byFile) { - console.log(file); - for (const usage of usages) { - const ns = usage.namespace === "__ROOT__" ? "(root)" : usage.namespace; - console.log(` L${usage.line} [${ns}] ${usage.key}`); - } - console.log(); - } + console.log(`\nMissing ${missing.length} translations:\n`); + const byFile = new Map(); + for (const u of missing) { + if (!byFile.has(u.file)) byFile.set(u.file, []); + byFile.get(u.file)!.push(u); } - - if (dynamic.length > 0) { - console.log(`\n${dynamic.length} dynamic keys (manual review):\n`); - - const byFile = new Map(); - for (const usage of dynamic) { - if (!byFile.has(usage.file)) byFile.set(usage.file, []); - byFile.get(usage.file)!.push(usage); - } - - for (const [file, usages] of byFile) { - console.log(file); - for (const usage of usages) { - const ns = usage.namespace === "__ROOT__" ? "(root)" : usage.namespace; - console.log(` L${usage.line} [${ns}] ${usage.key}`); - } + for (const [file, list] of byFile) { + console.log(file); + for (const u of list) { + console.log(` L${u.line} [${u.ns === "__ROOT__" ? "root" : u.ns}] ${u.key}`); } + console.log(); } } } diff --git a/scripts/find-unused-translations.ts b/scripts/find-unused-translations.ts index 93fa270..d9380e8 100644 --- a/scripts/find-unused-translations.ts +++ b/scripts/find-unused-translations.ts @@ -10,207 +10,141 @@ const SRC_DIR = "./src"; const MESSAGES_DIR = "./messages"; const ALL_LOCALES = ["en-US", "zh-CN", "ja-JP", "ko-KR", "de-DE", "fr-FR", "it-IT", "ug-CN"]; -function parseStringLiteral(s: string): string | null { +function parseString(s: string): string | null { s = s.trim(); - if (s.startsWith('"') && s.endsWith('"')) return s.slice(1, -1); - if (s.startsWith("'") && s.endsWith("'")) return s.slice(1, -1); - if (s.startsWith("`") && s.endsWith("`")) { - return s.includes("${") || s.includes("+") ? null : s.slice(1, -1); + if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + return s.slice(1, -1); + } + if (s.startsWith("`") && s.endsWith("`") && !s.includes("${")) { + return s.slice(1, -1); } return null; } -function extractTranslationBindings(content: string): Map { +function getBindings(content: string): Map { const bindings = new Map(); + const pattern = /(?:const|let|var)\s+(\w+)\s*=\s*(?:await\s+)?(?:useTranslations|getTranslations)\s*\(\s*([^)]*)\s*\)/g; - const patterns = [ - /(?:const|let|var)\s+(\w+)\s*=\s*(?:await\s+)?(?:useTranslations|getTranslations)\s*\(\s*([^)]*)\s*\)/g, - /(?:const|let|var)\s*\{\s*(\w+)\s*\}\s*=\s*await\s+getTranslations\s*\(\s*([^)]*)\s*\)/g, - ]; - - for (const pattern of patterns) { - let match; - while ((match = pattern.exec(content)) !== null) { - const varName = match[1]; - const arg = match[2].trim(); - if (arg === "") { - bindings.set(varName, "__ROOT__"); - } else { - const ns = parseStringLiteral(arg); - if (ns !== null) bindings.set(varName, ns); - } - } + let match; + while ((match = pattern.exec(content)) !== null) { + const varName = match[1]; + const arg = match[2].trim(); + bindings.set(varName, arg ? parseString(arg) || "" : "__ROOT__"); } return bindings; } -function extractUsedKeys(content: string, bindings: Map): Map> { - const usedKeys = new Map>(); +function getUsedKeys(content: string): Map> { + const used = new Map>(); + const bindings = getBindings(content); - for (const [varName, namespace] of bindings) { - const callPattern = new RegExp( - `\\b${varName}\\s*\\(\\s*("[^"]*"|'[^']*'|\`[^\`]*\`)(?:\\s*,|\\s*\\))`, - "g" - ); - + for (const [varName, ns] of bindings) { + const pattern = new RegExp(`\\b${varName}\\s*\\(\\s*("[^"]*"|'[^']*'|\`[^\`]*\`)(?:\\s*,|\\s*\\))`, "g"); let match; - while ((match = callPattern.exec(content)) !== null) { - const arg = match[1]; - const key = parseStringLiteral(arg); - - if (key !== null) { - if (!usedKeys.has(namespace)) { - usedKeys.set(namespace, new Set()); - } - usedKeys.get(namespace)!.add(key); + while ((match = pattern.exec(content)) !== null) { + const key = parseString(match[1]); + if (key) { + if (!used.has(ns)) used.set(ns, new Set()); + used.get(ns)!.add(key); } } } - return usedKeys; + return used; } -function getAllFiles(dir: string, extensions: string[]): string[] { +function getFiles(dir: string): string[] { const files: string[] = []; if (!fs.existsSync(dir)) return files; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...getAllFiles(fullPath, extensions)); - } else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) { - files.push(fullPath); - } + const p = path.join(dir, entry.name); + if (entry.isDirectory()) files.push(...getFiles(p)); + else if (entry.isFile() && /\.(tsx?|ts)$/.test(entry.name)) files.push(p); } - return files; } -function flattenTranslations( - obj: Record, - prefix = "" -): string[] { +function flattenKeys(obj: Record, prefix = ""): string[] { const keys: string[] = []; - for (const key of Object.keys(obj)) { const fullKey = prefix ? `${prefix}.${key}` : key; - const value = obj[key]; - - if (typeof value === "object" && value !== null) { - keys.push(...flattenTranslations(value as Record, fullKey)); - } else if (typeof value === "string") { + if (typeof obj[key] === "object" && obj[key] !== null) { + keys.push(...flattenKeys(obj[key] as Record, fullKey)); + } else if (typeof obj[key] === "string") { keys.push(fullKey); } } - return keys; } -function isKeyUsed( - fullKey: string, - usedKeys: Map> -): boolean { +function isUsed(fullKey: string, used: Map>): boolean { const parts = fullKey.split("."); for (let i = 1; i < parts.length; i++) { - const namespace = parts.slice(0, i).join("."); - const keyInNamespace = parts.slice(i).join("."); + const ns = parts.slice(0, i).join("."); + const key = parts.slice(i).join("."); - const nsKeys = usedKeys.get(namespace); + const nsKeys = used.get(ns); if (nsKeys) { - if (nsKeys.has(keyInNamespace)) return true; - - for (const usedKey of nsKeys) { - if (keyInNamespace.startsWith(usedKey + ".")) return true; + if (nsKeys.has(key)) return true; + for (const k of nsKeys) { + if (key.startsWith(k + ".")) return true; } } } - const rootKeys = usedKeys.get("__ROOT__"); - if (rootKeys && rootKeys.has(fullKey)) return true; - - return false; + const rootKeys = used.get("__ROOT__"); + return rootKeys?.has(fullKey) ?? false; } function main() { - const targetLocale = process.argv[2]; - const localesToCheck = targetLocale ? [targetLocale] : ALL_LOCALES; + const locales = process.argv[2] ? [process.argv[2]] : ALL_LOCALES; - console.log("Scanning source files...\n"); + const files = getFiles(SRC_DIR); + const allUsed = new Map>(); - const sourceFiles = getAllFiles(SRC_DIR, [".tsx", ".ts"]); - const allUsedKeys = new Map>(); - - for (const filePath of sourceFiles) { - const content = fs.readFileSync(filePath, "utf-8"); - const bindings = extractTranslationBindings(content); - const usedKeys = extractUsedKeys(content, bindings); - - for (const [ns, keys] of usedKeys) { - if (!allUsedKeys.has(ns)) { - allUsedKeys.set(ns, new Set()); - } - for (const key of keys) { - allUsedKeys.get(ns)!.add(key); - } + for (const f of files) { + const used = getUsedKeys(fs.readFileSync(f, "utf-8")); + for (const [ns, keys] of used) { + if (!allUsed.has(ns)) allUsed.set(ns, new Set()); + for (const k of keys) allUsed.get(ns)!.add(k); } } - console.log(`Scanned ${sourceFiles.length} files`); - console.log(`Found ${allUsedKeys.size} namespaces used\n`); + console.log(`Scanned ${files.length} files, ${allUsed.size} namespaces\n`); - for (const locale of localesToCheck) { - console.log(`\n${"=".repeat(60)}`); - console.log(`Locale: ${locale}`); - console.log("=".repeat(60)); + for (const locale of locales) { + console.log(`\n${"=".repeat(50)}\nLocale: ${locale}\n${"=".repeat(50)}`); - const translationPath = path.join(MESSAGES_DIR, `${locale}.json`); - - if (!fs.existsSync(translationPath)) { - console.log(`File not found: ${translationPath}`); + const filePath = path.join(MESSAGES_DIR, `${locale}.json`); + if (!fs.existsSync(filePath)) { + console.log(`File not found: ${filePath}`); continue; } - let translations: Record; - try { - translations = JSON.parse(fs.readFileSync(translationPath, "utf-8")); - } catch (e) { - console.log(`Failed to parse: ${translationPath}`); - continue; - } + const trans = JSON.parse(fs.readFileSync(filePath, "utf-8")); + const allKeys = flattenKeys(trans); + const unused = allKeys.filter(k => !isUsed(k, allUsed)); - const allKeys = flattenTranslations(translations); - console.log(`Total ${allKeys.length} translation keys`); + console.log(`Total: ${allKeys.length} keys`); - const unusedKeys = allKeys.filter(key => !isKeyUsed(key, allUsedKeys)); - - if (unusedKeys.length === 0) { + if (unused.length === 0) { console.log("No unused translations!"); } else { - console.log(`\n${unusedKeys.length} potentially unused translations:\n`); - - const groupedByNs = new Map(); - for (const key of unusedKeys) { - const firstDot = key.indexOf("."); - const ns = firstDot > 0 ? key.substring(0, firstDot) : key; - const subKey = firstDot > 0 ? key.substring(firstDot + 1) : ""; - - if (!groupedByNs.has(ns)) { - groupedByNs.set(ns, []); - } - groupedByNs.get(ns)!.push(subKey || "(root)"); + console.log(`\n${unused.length} potentially unused:\n`); + const grouped = new Map(); + for (const k of unused) { + const [ns, ...rest] = k.split("."); + if (!grouped.has(ns)) grouped.set(ns, []); + grouped.get(ns)!.push(rest.join(".")); } - - for (const [ns, keys] of groupedByNs) { + for (const [ns, keys] of grouped) { console.log(`${ns}`); - for (const key of keys) { - console.log(` ${key}`); - } + for (const k of keys) console.log(` ${k}`); console.log(); } - - console.log("Note: These may be used dynamically (e.g., t(`prefix.${var}`)). Review before deleting."); } } diff --git a/src/app/(features)/text-speaker/page.tsx b/src/app/(features)/text-speaker/page.tsx index 0e8a794..f4839db 100644 --- a/src/app/(features)/text-speaker/page.tsx +++ b/src/app/(features)/text-speaker/page.tsx @@ -20,19 +20,37 @@ import { PageLayout } from "@/components/ui/PageLayout"; import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts"; const TTS_LANGUAGES = [ - { value: "Auto", labelKey: "auto" }, - { value: "Chinese", labelKey: "chinese" }, - { value: "English", labelKey: "english" }, - { value: "Japanese", labelKey: "japanese" }, - { value: "Korean", labelKey: "korean" }, - { value: "French", labelKey: "french" }, - { value: "German", labelKey: "german" }, - { value: "Italian", labelKey: "italian" }, - { value: "Spanish", labelKey: "spanish" }, - { value: "Portuguese", labelKey: "portuguese" }, - { value: "Russian", labelKey: "russian" }, + { value: "Auto", label: "auto" }, + { value: "Chinese", label: "chinese" }, + { value: "English", label: "english" }, + { value: "Japanese", label: "japanese" }, + { value: "Korean", label: "korean" }, + { value: "French", label: "french" }, + { value: "German", label: "german" }, + { value: "Italian", label: "italian" }, + { value: "Spanish", label: "spanish" }, + { value: "Portuguese", label: "portuguese" }, + { value: "Russian", label: "russian" }, ] as const; +type TTSLabel = typeof TTS_LANGUAGES[number]["label"]; + +function getLanguageLabel(t: (key: string) => string, label: TTSLabel): string { + switch (label) { + case "auto": return t("languages.auto"); + case "chinese": return t("languages.chinese"); + case "english": return t("languages.english"); + case "japanese": return t("languages.japanese"); + case "korean": return t("languages.korean"); + case "french": return t("languages.french"); + case "german": return t("languages.german"); + case "italian": return t("languages.italian"); + case "spanish": return t("languages.spanish"); + case "portuguese": return t("languages.portuguese"); + case "russian": return t("languages.russian"); + } +} + export default function TextSpeakerPage() { const t = useTranslations("text_speaker"); const textareaRef = useRef(null); @@ -359,7 +377,7 @@ export default function TextSpeakerPage() { }} size="sm" > - {t(`languages.${lang.labelKey}`)} + {getLanguageLabel(t, lang.label)} ))} string, label: LangLabel): string { + switch (label) { + case "auto": return t("auto"); + case "chinese": return t("chinese"); + case "english": return t("english"); + case "japanese": return t("japanese"); + case "korean": return t("korean"); + case "french": return t("french"); + case "german": return t("german"); + case "italian": return t("italian"); + case "spanish": return t("spanish"); + case "portuguese": return t("portuguese"); + case "russian": return t("russian"); + } +} + // Estimated button width in pixels (including gap) const BUTTON_WIDTH = 80; const LABEL_WIDTH = 100; @@ -290,7 +308,7 @@ export default function TranslatorPage() { }} className="shrink-0" > - {t(lang.labelKey)} + {getLangLabel(t, lang.label)} ))} - {t(lang.labelKey)} + {getLangLabel(t, lang.label)} ))} - {t(lang.labelKey)} + {t(lang.label)} ))} string, label: LocaleLabel): string { + switch (label) { + case "chinese": return t("translator.chinese"); + case "english": return t("translator.english"); + case "italian": return t("translator.italian"); + case "japanese": return t("translator.japanese"); + case "korean": return t("translator.korean"); + case "french": return t("translator.french"); + case "german": return t("translator.german"); + case "spanish": return t("translator.spanish"); + case "portuguese": return t("translator.portuguese"); + case "russian": return t("translator.russian"); + case "other": return t("translator.other"); + } +} interface LocaleSelectorProps { value: string; @@ -62,7 +80,7 @@ export function LocaleSelector({ value, onChange }: LocaleSelectorProps) { > {COMMON_LANGUAGES.map((lang) => ( ))}