import { readFileSync, readdirSync, statSync } from "node:fs"; import { dirname, join, relative } from "node:path"; import { fileURLToPath } from "node:url"; const mobileDir = dirname(dirname(fileURLToPath(import.meta.url))); const sourceDir = join(mobileDir, "lib"); const arbFiles = ["app_en.arb", "app_zh_CN.arb"]; const catalogs = arbFiles.map((file) => ({ file, data: JSON.parse(readFileSync(join(sourceDir, "l10n", file), "utf8")), })); const messageKeys = (catalog) => Object.keys(catalog.data).filter((key) => !key.startsWith("@")); const referenceKeys = messageKeys(catalogs[0]); for (const catalog of catalogs.slice(1)) { const keys = new Set(messageKeys(catalog)); const missing = referenceKeys.filter((key) => !keys.has(key)); const extra = [...keys].filter((key) => !(key in catalogs[0].data)); if (missing.length || extra.length) { throw new Error( `${catalog.file} ARB mismatch\nmissing: ${missing.join(", ")}\nextra: ${extra.join(", ")}`, ); } } for (const key of referenceKeys) { const metadataKey = `@${key}`; const expected = Object.keys( catalogs[0].data[metadataKey]?.placeholders ?? {}, ).sort(); for (const catalog of catalogs.slice(1)) { const actual = Object.keys( catalog.data[metadataKey]?.placeholders ?? {}, ).sort(); if (JSON.stringify(actual) !== JSON.stringify(expected)) { throw new Error(`${catalog.file} placeholder mismatch for ${key}`); } } } const references = new Map(); const directUiLiterals = []; const directUiPatterns = [ /\bText\(\s*(?:const\s+)?(["'])[A-Za-z][^\r\n]*?\1/g, /\bTextSpan\(\s*text\s*:\s*(["'])[A-Za-z][^\r\n]*?\1/g, /\b(?:tooltip|hintText|labelText|helperText|errorText|semanticLabel)\s*:\s*(["'])[A-Za-z][^\r\n]*?\1/g, ]; function scanDirectory(directory) { for (const name of readdirSync(directory)) { const path = join(directory, name); if (statSync(path).isDirectory()) { scanDirectory(path); continue; } if (!name.endsWith(".dart")) continue; const source = readFileSync(path, "utf8"); for (const match of source.matchAll(/(?:context\.)?l10n\.([A-Za-z_]\w*)/g)) { const key = match[1]; if (["dart", "localeName"].includes(key)) continue; if (!references.has(key)) references.set(key, relative(mobileDir, path)); } for (const pattern of directUiPatterns) { for (const match of source.matchAll(pattern)) { if (/^Text\(\s*(["'])v\$/.test(match[0])) continue; const line = source.slice(0, match.index).split(/\r?\n/).length; directUiLiterals.push(`${relative(mobileDir, path)}:${line}`); } } } } scanDirectory(sourceDir); const missingReferences = [...references].filter( ([key]) => !(key in catalogs[0].data), ); if (missingReferences.length) { throw new Error( `Missing ARB messages:\n${missingReferences .map(([key, file]) => `${key} (${file})`) .join("\n")}`, ); } if (directUiLiterals.length) { throw new Error( `Direct English UI literals found:\n${directUiLiterals.join("\n")}`, ); } console.log( `l10n check passed: ${referenceKeys.length} messages, ${references.size} Dart references`, );