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'
This commit is contained in:
@@ -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<string, string> {
|
||||
function getBindings(content: string): Map<string, string> {
|
||||
const bindings = new Map<string, string>();
|
||||
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<string, string>
|
||||
): 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<string, unknown>, path: string): unknown {
|
||||
if (typeof obj[path] !== "undefined") return obj[path];
|
||||
function keyExists(key: string, ns: string, trans: Record<string, unknown>): 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<string, unknown>)[part];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function keyExists(key: string, namespace: string, translations: Record<string, unknown>): 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<string, unknown>)[part];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof targetObj !== "object" || targetObj === null) return false;
|
||||
if (typeof obj !== "object" || obj === null) return false;
|
||||
|
||||
const target = targetObj as Record<string, unknown>;
|
||||
|
||||
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<string, unknown>)[part];
|
||||
if (typeof obj !== "object" || obj === null) return false;
|
||||
obj = (obj as Record<string, unknown>)[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<string, TranslationUsage>();
|
||||
for (const usage of allUsages) {
|
||||
const key = `${usage.file}:${usage.line}:${usage.namespace}:${usage.key}`;
|
||||
uniqueUsages.set(key, usage);
|
||||
const unique = new Map<string, { file: string; line: number; ns: string; key: string }>();
|
||||
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 locales) {
|
||||
console.log(`\n${"=".repeat(50)}\nLocale: ${locale}\n${"=".repeat(50)}`);
|
||||
|
||||
for (const locale of localesToCheck) {
|
||||
console.log(`\n${"=".repeat(60)}`);
|
||||
console.log(`Locale: ${locale}`);
|
||||
console.log("=".repeat(60));
|
||||
|
||||
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<string, unknown>;
|
||||
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<string, TranslationUsage[]>();
|
||||
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<string, typeof missing>();
|
||||
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<string, TranslationUsage[]>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, string> {
|
||||
function getBindings(content: string): Map<string, string> {
|
||||
const bindings = new Map<string, string>();
|
||||
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<string, string>): Map<string, Set<string>> {
|
||||
const usedKeys = new Map<string, Set<string>>();
|
||||
|
||||
for (const [varName, namespace] of bindings) {
|
||||
const callPattern = new RegExp(
|
||||
`\\b${varName}\\s*\\(\\s*("[^"]*"|'[^']*'|\`[^\`]*\`)(?:\\s*,|\\s*\\))`,
|
||||
"g"
|
||||
);
|
||||
function getUsedKeys(content: string): Map<string, Set<string>> {
|
||||
const used = new Map<string, Set<string>>();
|
||||
const bindings = getBindings(content);
|
||||
|
||||
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<string, unknown>,
|
||||
prefix = ""
|
||||
): string[] {
|
||||
function flattenKeys(obj: Record<string, unknown>, 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<string, unknown>, fullKey));
|
||||
} else if (typeof value === "string") {
|
||||
if (typeof obj[key] === "object" && obj[key] !== null) {
|
||||
keys.push(...flattenKeys(obj[key] as Record<string, unknown>, fullKey));
|
||||
} else if (typeof obj[key] === "string") {
|
||||
keys.push(fullKey);
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
function isKeyUsed(
|
||||
fullKey: string,
|
||||
usedKeys: Map<string, Set<string>>
|
||||
): boolean {
|
||||
function isUsed(fullKey: string, used: Map<string, Set<string>>): 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<string, Set<string>>();
|
||||
|
||||
const sourceFiles = getAllFiles(SRC_DIR, [".tsx", ".ts"]);
|
||||
const allUsedKeys = new Map<string, Set<string>>();
|
||||
|
||||
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<string, unknown>;
|
||||
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<string, string[]>();
|
||||
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<string, string[]>();
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<HTMLTextAreaElement>(null);
|
||||
@@ -359,7 +377,7 @@ export default function TextSpeakerPage() {
|
||||
}}
|
||||
size="sm"
|
||||
>
|
||||
{t(`languages.${lang.labelKey}`)}
|
||||
{getLanguageLabel(t, lang.label)}
|
||||
</LightButton>
|
||||
))}
|
||||
<Input
|
||||
|
||||
@@ -20,32 +20,50 @@ import { Plus } from "lucide-react";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
|
||||
const SOURCE_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;
|
||||
|
||||
const TARGET_LANGUAGES = [
|
||||
{ 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: "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 LangLabel = typeof SOURCE_LANGUAGES[number]["label"];
|
||||
|
||||
function getLangLabel(t: (key: string) => 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)}
|
||||
</LightButton>
|
||||
))}
|
||||
<Input
|
||||
@@ -353,7 +371,7 @@ export default function TranslatorPage() {
|
||||
}}
|
||||
className="shrink-0"
|
||||
>
|
||||
{t(lang.labelKey)}
|
||||
{getLangLabel(t, lang.label)}
|
||||
</LightButton>
|
||||
))}
|
||||
<Input
|
||||
|
||||
@@ -13,11 +13,18 @@ import { actionCreateCard } from "@/modules/card/card-action";
|
||||
import type { CardType, CardMeaning } from "@/modules/card/card-action-dto";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const QUERY_LANGUAGE_LABELS = {
|
||||
english: "english",
|
||||
chinese: "chinese",
|
||||
japanese: "japanese",
|
||||
korean: "korean",
|
||||
} as const;
|
||||
|
||||
const QUERY_LANGUAGES = [
|
||||
{ value: "en", labelKey: "english" },
|
||||
{ value: "zh", labelKey: "chinese" },
|
||||
{ value: "ja", labelKey: "japanese" },
|
||||
{ value: "ko", labelKey: "korean" },
|
||||
{ value: "en", label: "english" as const },
|
||||
{ value: "zh", label: "chinese" as const },
|
||||
{ value: "ja", label: "japanese" as const },
|
||||
{ value: "ko", label: "korean" as const },
|
||||
] as const;
|
||||
|
||||
interface AddCardModalProps {
|
||||
@@ -169,7 +176,7 @@ export function AddCardModal({
|
||||
}}
|
||||
size="sm"
|
||||
>
|
||||
{t(lang.labelKey)}
|
||||
{t(lang.label)}
|
||||
</LightButton>
|
||||
))}
|
||||
<Input
|
||||
|
||||
@@ -21,7 +21,25 @@ const COMMON_LANGUAGES = [
|
||||
{ label: "portuguese", value: "portuguese" },
|
||||
{ label: "russian", value: "russian" },
|
||||
{ label: "other", value: "other" },
|
||||
];
|
||||
] as const;
|
||||
|
||||
type LocaleLabel = typeof COMMON_LANGUAGES[number]["label"];
|
||||
|
||||
function getLocaleLabel(t: (key: string) => 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) => (
|
||||
<option key={lang.value} value={lang.value}>
|
||||
{t(`translator.${lang.label}`)}
|
||||
{getLocaleLabel(t, lang.label)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
Reference in New Issue
Block a user