import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';
const require = createRequire(import.meta.url);
const root = process.cwd();
const contractPath = path.join(root, 'src', 'shared', 'i18n', 'contract', 'locales.json');
const hardcodedBaselinePath = path.join(root, 'scripts', 'i18n-hardcoded-baseline.json');
const literalFallbackBaselinePath = path.join(root, 'scripts', 'i18n-literal-fallback-baseline.json');
const localeFormatBaselinePath = path.join(root, 'scripts', 'i18n-locale-format-baseline.json');
const dynamicKeyAllowlistPath = path.join(root, 'scripts', 'i18n-dynamic-key-allowlist.json');
const l10nIdenticalAllowlistPath = path.join(root, 'scripts', 'i18n-l10n-identical-allowlist.json');
const governanceBaselinePath = path.join(root, 'scripts', 'i18n-governance-baseline.json');
const sharedTermsDir = path.join(root, 'src', 'shared', 'i18n', 'resources', 'shared');
const webLocalesDir = path.join(root, 'src', 'web-ui', 'src', 'locales');
const namespaceRegistryPath = path.join(
root,
'src',
'web-ui',
'src',
'infrastructure',
'i18n',
'presets',
'namespaceRegistry.ts',
);
const webSourceDir = path.join(root, 'src', 'web-ui', 'src');
const mobileWebSourceDir = path.join(root, 'src', 'mobile-web', 'src');
const mobileWebMessagesPath = path.join(mobileWebSourceDir, 'i18n', 'messages.ts');
const installerSourceDir = path.join(root, 'BitFun-Installer', 'src');
const installerLocalesDir = path.join(installerSourceDir, 'i18n', 'locales');
const coreLocalesDir = path.join(root, 'src', 'crates', 'assembly', 'core', 'locales');
const relayHomepageDir = path.join(root, 'src', 'apps', 'relay-server', 'static', 'homepage');
const relayHomepageI18nPath = path.join(relayHomepageDir, 'i18n.json');
const supportedLocales = fs
.readdirSync(webLocalesDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort();
const baselineLocale = supportedLocales.includes('en-US') ? 'en-US' : supportedLocales[0];
const localeContract = readJsonFile(contractPath);
let errorCount = 0;
let warningCount = 0;
let auditTypeScript = null;
let cliOptions = { reportJsonPath: null };
let governanceBaselineCache;
const reportCategories = [
'confirmedUnusedKeys',
'dynamicKeyCandidates',
'sharedTermDuplicates',
'sameTextLocaleInventory',
'l10nQualityCandidates',
'literalDefaultValueFallbacks',
'localeFormatCandidates',
];
const governanceReport = {
version: 1,
generatedBy: 'scripts/i18n-audit.mjs',
summary: {
baseline: {
path: 'scripts/i18n-governance-baseline.json',
enforced: false,
},
byCategory: {},
counts: {},
},
confirmedUnusedKeys: [],
dynamicKeyCandidates: [],
sharedTermDuplicates: [],
sameTextLocaleInventory: [],
l10nQualityCandidates: [],
literalDefaultValueFallbacks: [],
localeFormatCandidates: [],
};
function reportError(message) {
errorCount += 1;
console.error(`[i18n:audit] ERROR ${message}`);
}
function reportWarning(message) {
warningCount += 1;
console.warn(`[i18n:audit] WARN ${message}`);
}
function parseCliOptions(args) {
const options = { reportJsonPath: null };
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === '--report-json') {
const value = args[index + 1];
if (!value || value.startsWith('--')) {
reportError('--report-json requires an output path');
} else {
options.reportJsonPath = value;
index += 1;
}
continue;
}
if (arg.startsWith('--report-json=')) {
const value = arg.slice('--report-json='.length);
if (!value) {
reportError('--report-json requires an output path');
} else {
options.reportJsonPath = value;
}
continue;
}
reportError(`Unknown i18n audit option "${arg}"`);
}
return options;
}
cliOptions = parseCliOptions(process.argv.slice(2));
function loadTypeScriptForAudit() {
try {
return require('typescript');
} catch (error) {
reportError(
`Failed to load TypeScript for i18n audit: ${error.message}. Run pnpm install before running pnpm run i18n:audit.`,
);
return null;
}
}
function toPosixPath(value) {
return value.split(path.sep).join('/');
}
function listFiles(dir, predicate) {
const output = [];
if (!fs.existsSync(dir)) return output;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
output.push(...listFiles(fullPath, predicate));
} else if (!predicate || predicate(fullPath)) {
output.push(fullPath);
}
}
return output;
}
function readJsonFile(file) {
return JSON.parse(fs.readFileSync(file, 'utf8'));
}
function listLocaleNamespaces(locale) {
const localeDir = path.join(webLocalesDir, locale);
const namespaces = listFiles(localeDir, (file) => file.endsWith('.json'))
.map((file) => toPosixPath(path.relative(localeDir, file)).replace(/\.json$/, ''))
.sort();
if (fs.existsSync(path.join(sharedTermsDir, locale, 'terms.json'))) {
namespaces.push('shared');
}
return namespaces.sort();
}
function readRegistryNamespaces() {
const source = fs.readFileSync(namespaceRegistryPath, 'utf8');
const match = source.match(/ALL_NAMESPACES\s*=\s*\[([\s\S]*?)\]\s*as const/);
if (!match) {
reportError(`Could not parse ALL_NAMESPACES from ${namespaceRegistryPath}`);
return [];
}
return Array.from(match[1].matchAll(/['"]([^'"]+)['"]/g))
.map((item) => item[1])
.sort();
}
function readRegistryLocales() {
return [...localeContract.surfaceOrders['web-ui']].sort();
}
function flattenKeys(value, prefix = '') {
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
return prefix ? [prefix] : [];
}
const keys = [];
for (const [key, child] of Object.entries(value)) {
const nextPrefix = prefix ? `${prefix}.${key}` : key;
if (child != null && typeof child === 'object' && !Array.isArray(child)) {
keys.push(...flattenKeys(child, nextPrefix));
} else {
keys.push(nextPrefix);
}
}
return keys.sort();
}
function flattenStringEntries(value, prefix = '') {
if (typeof value === 'string') {
return prefix ? [[prefix, value]] : [];
}
if (Array.isArray(value)) {
const text = value.filter((item) => typeof item === 'string').join('\n');
return prefix ? [[prefix, text]] : [];
}
if (value == null || typeof value !== 'object') {
return prefix ? [[prefix, '']] : [];
}
return Object.entries(value)
.flatMap(([key, child]) => flattenStringEntries(child, prefix ? `${prefix}.${key}` : key))
.sort(([left], [right]) => left.localeCompare(right));
}
function sortedUnique(values) {
return Array.from(new Set(values)).sort();
}
function isPlainObject(value) {
return value != null && typeof value === 'object' && !Array.isArray(value);
}
function extractI18nextPlaceholders(value) {
const matches = String(value).matchAll(/\{\{\s*-?\s*([A-Za-z_][\w]*)\s*\}\}/g);
return sortedUnique(Array.from(matches, (match) => match[1]));
}
function extractMobilePlaceholders(value) {
const matches = String(value).matchAll(/\{\s*([A-Za-z_][\w]*)\s*\}/g);
return sortedUnique(Array.from(matches, (match) => match[1]));
}
function extractFluentPlaceholders(value) {
const matches = String(value).matchAll(/\$\s*([A-Za-z_][\w-]*)/g);
return sortedUnique(Array.from(matches, (match) => match[1]));
}
function sameSet(left, right) {
if (left.length !== right.length) return false;
return left.every((item, index) => item === right[index]);
}
function hasHanText(value) {
return /\p{Script=Han}/u.test(String(value));
}
const zhTwSameTextTerminologySignals = ['了解'];
const zhTwSameTextScriptSignals = new Set([
'这',
'个',
'们',
'为',
'与',
'开',
'关',
'发',
'复',
'后',
'过',
'还',
'进',
'选',
'连',
'远',
'时',
'设',
'数',
'据',
'页',
'项',
'态',
'错',
'误',
'删',
'编',
'导',
'启',
'闭',
'间',
'类',
'显',
'隐',
'径',
'档',
'检',
'测',
'权',
'务',
'动',
'应',
'统',
'议',
'读',
'写',
'话',
'请',
'询',
'详',
'语',
'认',
'让',
'讲',
'试',
'论',
'证',
'评',
'识',
'访',
'调',
'变',
'图',
'标',
'链',
'节',
'览',
'获',
]);
function getZhTwSameTextSignal(value) {
const text = String(value);
for (const phrase of zhTwSameTextTerminologySignals) {
if (text.includes(phrase)) {
return { type: 'terminology', match: phrase };
}
}
for (const character of text) {
if (zhTwSameTextScriptSignals.has(character)) {
return { type: 'script-variant', match: character };
}
}
return null;
}
function sortByReportIdentity(left, right) {
return JSON.stringify(left).localeCompare(JSON.stringify(right));
}
function countEntriesBy(entries, field, options = {}) {
const emptyLabel = options.emptyLabel ?? '';
const includeKeys = options.includeKeys ?? [];
const counts = entries.reduce((acc, entry) => {
const value = entry[field] ?? emptyLabel;
const key = value === '' ? emptyLabel : value;
acc.set(key, (acc.get(key) ?? 0) + 1);
return acc;
}, new Map(includeKeys.map((key) => [key, 0])));
return Object.fromEntries(
Array.from(counts.entries())
.sort(([left], [right]) => String(left).localeCompare(String(right))),
);
}
function finalizeGovernanceReport() {
for (const category of reportCategories) {
governanceReport[category].sort(sortByReportIdentity);
governanceReport.summary.counts[category] = governanceReport[category].length;
}
const governanceSurfaceIds = collectGovernanceBudgetSurfaceIds();
const localeFormatSurfaceIds = collectLocaleFormatSurfaceIds();
governanceReport.summary.byCategory = {
confirmedUnusedKeys: {
bySurface: countEntriesBy(governanceReport.confirmedUnusedKeys, 'surface'),
},
dynamicKeyCandidates: {
byAllowlistId: countEntriesBy(governanceReport.dynamicKeyCandidates, 'allowlistId'),
byProofState: countEntriesBy(governanceReport.dynamicKeyCandidates, 'proofState'),
bySurface: countEntriesBy(governanceReport.dynamicKeyCandidates, 'surface'),
},
sharedTermDuplicates: {
byNamespace: countEntriesBy(governanceReport.sharedTermDuplicates, 'namespace', { emptyLabel: '<none>' }),
bySharedKey: countEntriesBy(governanceReport.sharedTermDuplicates, 'sharedKey'),
bySurface: countEntriesBy(governanceReport.sharedTermDuplicates, 'surface', { includeKeys: governanceSurfaceIds }),
},
sameTextLocaleInventory: {
byAllowlistState: countEntriesBy(governanceReport.sameTextLocaleInventory, 'allowlistState'),
byNamespace: countEntriesBy(governanceReport.sameTextLocaleInventory, 'namespace', { emptyLabel: '<none>' }),
bySignalType: countEntriesBy(governanceReport.sameTextLocaleInventory, 'signalType'),
bySurface: countEntriesBy(governanceReport.sameTextLocaleInventory, 'surface', { includeKeys: governanceSurfaceIds }),
},
l10nQualityCandidates: {
byComparisonLocale: countEntriesBy(governanceReport.l10nQualityCandidates, 'comparisonLocale'),
byNamespace: countEntriesBy(governanceReport.l10nQualityCandidates, 'namespace', { emptyLabel: '<none>' }),
bySurface: countEntriesBy(governanceReport.l10nQualityCandidates, 'surface', { includeKeys: governanceSurfaceIds }),
},
literalDefaultValueFallbacks: {
byFile: countEntriesBy(governanceReport.literalDefaultValueFallbacks, 'file'),
byNamespace: countEntriesBy(governanceReport.literalDefaultValueFallbacks, 'namespace', { emptyLabel: '<none>' }),
},
localeFormatCandidates: {
byFile: countEntriesBy(governanceReport.localeFormatCandidates, 'file'),
bySurface: countEntriesBy(governanceReport.localeFormatCandidates, 'surface', { includeKeys: localeFormatSurfaceIds }),
},
};
}
function writeGovernanceReport() {
finalizeGovernanceReport();
if (!cliOptions.reportJsonPath) return;
const outputPath = path.isAbsolute(cliOptions.reportJsonPath)
? cliOptions.reportJsonPath
: path.join(root, cliOptions.reportJsonPath);
try {
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, `${JSON.stringify(governanceReport, null, 2)}\n`, 'utf8');
} catch (error) {
reportError(`Failed to write i18n governance report to ${toPosixPath(path.relative(root, outputPath))}: ${error.message}`);
}
}
function reportPlaceholderParity(surface, locale, key, expected, actual) {
if (sameSet(expected, actual)) return;
reportError(
`${surface} ${locale} key "${key}" placeholder mismatch: expected [${expected.join(', ')}], got [${actual.join(', ')}]`,
);
}
function readJsonKeys(locale, namespace) {
const file = namespace === 'shared'
? path.join(sharedTermsDir, locale, 'terms.json')
: path.join(webLocalesDir, locale, `${namespace}.json`);
try {
return flattenKeys(readJsonFile(file));
} catch (error) {
reportError(`Failed to parse ${toPosixPath(path.relative(root, file))}: ${error.message}`);
return [];
}
}
function readJsonEntries(locale, namespace) {
const file = namespace === 'shared'
? path.join(sharedTermsDir, locale, 'terms.json')
: path.join(webLocalesDir, locale, `${namespace}.json`);
try {
return new Map(flattenStringEntries(readJsonFile(file)));
} catch (error) {
reportError(`Failed to parse ${toPosixPath(path.relative(root, file))}: ${error.message}`);
return new Map();
}
}
const sharedTermEntryCache = new Map();
function readSharedTermMap(locale) {
if (sharedTermEntryCache.has(locale)) {
return sharedTermEntryCache.get(locale);
}
const file = path.join(sharedTermsDir, locale, 'terms.json');
try {
const entries = new Map(flattenStringEntries(readJsonFile(file)));
sharedTermEntryCache.set(locale, entries);
return entries;
} catch (error) {
reportError(`Failed to parse ${toPosixPath(path.relative(root, file))}: ${error.message}`);
const entries = new Map();
sharedTermEntryCache.set(locale, entries);
return entries;
}
}
function readInstallerJsonKeys(uiLocale) {
const file = path.join(installerLocalesDir, `${uiLocale}.json`);
try {
return flattenKeys(readJsonFile(file));
} catch (error) {
reportError(`Failed to parse ${toPosixPath(path.relative(root, file))}: ${error.message}`);
return [];
}
}
function readInstallerJsonEntries(uiLocale) {
const file = path.join(installerLocalesDir, `${uiLocale}.json`);
try {
return new Map(flattenStringEntries(readJsonFile(file)));
} catch (error) {
reportError(`Failed to parse ${toPosixPath(path.relative(root, file))}: ${error.message}`);
return new Map();
}
}
function propertyNameToString(ts, name) {
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
return name.text;
}
return null;
}
function unwrapTsExpression(ts, expression) {
let current = expression;
while (current && (ts.isAsExpression(current) || ts.isSatisfiesExpression(current))) {
current = current.expression;
}
return current;
}
function flattenTsObjectKeys(ts, objectLiteral, prefix = '') {
const keys = [];
for (const property of objectLiteral.properties) {
if (!ts.isPropertyAssignment(property)) continue;
const key = propertyNameToString(ts, property.name);
if (!key) continue;
if (!prefix && key === 'shared') continue;
const nextPrefix = prefix ? `${prefix}.${key}` : key;
const initializer = unwrapTsExpression(ts, property.initializer);
if (ts.isObjectLiteralExpression(initializer)) {
keys.push(...flattenTsObjectKeys(ts, initializer, nextPrefix));
} else {
keys.push(nextPrefix);
}
}
return keys.sort();
}
function flattenTsObjectEntries(ts, objectLiteral, prefix = '') {
const entries = [];
for (const property of objectLiteral.properties) {
if (!ts.isPropertyAssignment(property)) continue;
const key = propertyNameToString(ts, property.name);
if (!key) continue;
if (!prefix && key === 'shared') continue;
const nextPrefix = prefix ? `${prefix}.${key}` : key;
const initializer = unwrapTsExpression(ts, property.initializer);
if (ts.isObjectLiteralExpression(initializer)) {
entries.push(...flattenTsObjectEntries(ts, initializer, nextPrefix));
} else if (
ts.isStringLiteral(initializer) ||
ts.isNoSubstitutionTemplateLiteral(initializer)
) {
entries.push([nextPrefix, initializer.text]);
} else {
entries.push([nextPrefix, '']);
}
}
return entries.sort(([left], [right]) => left.localeCompare(right));
}
function readMobileMessagesByLocale() {
const ts = auditTypeScript;
if (!ts) {
return new Map();
}
const source = fs.readFileSync(mobileWebMessagesPath, 'utf8');
const sourceFile = ts.createSourceFile(mobileWebMessagesPath, source, ts.ScriptTarget.Latest, true);
const output = new Map();
function visit(node) {
if (
ts.isVariableDeclaration(node) &&
ts.isIdentifier(node.name) &&
node.name.text === 'messages'
) {
const initializer = unwrapTsExpression(ts, node.initializer);
if (!initializer || !ts.isObjectLiteralExpression(initializer)) {
reportError('mobile-web messages export is not an object literal');
return;
}
for (const property of initializer.properties) {
if (!ts.isPropertyAssignment(property)) continue;
const locale = propertyNameToString(ts, property.name);
if (!locale) continue;
const value = unwrapTsExpression(ts, property.initializer);
if (!ts.isObjectLiteralExpression(value)) {
reportError(`mobile-web messages.${locale} is not an object literal`);
continue;
}
output.set(locale, new Map(flattenTsObjectEntries(ts, value)));
}
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return output;
}
function readMobileMessageKeysByLocale() {
return new Map(
Array.from(readMobileMessagesByLocale().entries())
.map(([locale, entries]) => [locale, Array.from(entries.keys()).sort()]),
);
}
function diffSets(left, right) {
const rightSet = new Set(right);
return left.filter((item) => !rightSet.has(item));
}
function auditNamespaceCoverage() {
const registryLocales = readRegistryLocales();
for (const locale of supportedLocales.filter((item) => !registryLocales.includes(item))) {
reportError(`${locale} locale directory exists but is not in the web-ui i18n contract surface order`);
}
for (const locale of registryLocales.filter((item) => !supportedLocales.includes(item))) {
reportError(`web-ui i18n contract surface order includes ${locale} but no matching locale directory exists`);
}
const registryNamespaces = readRegistryNamespaces();
const registrySet = new Set(registryNamespaces);
for (const locale of supportedLocales) {
const localeNamespaces = listLocaleNamespaces(locale);
const missingFromRegistry = localeNamespaces.filter((item) => !registrySet.has(item));
const missingFromLocale = registryNamespaces.filter((item) => !localeNamespaces.includes(item));
for (const namespace of missingFromRegistry) {
reportError(`${locale} namespace "${namespace}" exists on disk but is not in ALL_NAMESPACES`);
}
for (const namespace of missingFromLocale) {
reportError(`ALL_NAMESPACES includes "${namespace}" but ${locale} has no matching JSON file`);
}
}
const baselineNamespaces = listLocaleNamespaces(baselineLocale);
for (const locale of supportedLocales.filter((item) => item !== baselineLocale)) {
const localeNamespaces = listLocaleNamespaces(locale);
for (const namespace of diffSets(baselineNamespaces, localeNamespaces)) {
reportError(`${locale} is missing namespace "${namespace}"`);
}
for (const namespace of diffSets(localeNamespaces, baselineNamespaces)) {
reportError(`${locale} has extra namespace "${namespace}"`);
}
}
return registryNamespaces;
}
function auditSurfaceResourceRoots() {
const localeById = new Map(localeContract.locales.map((locale) => [locale.id, locale]));
for (const [surface, config] of Object.entries(localeContract.surfaces ?? {})) {
const resourceRoot = path.join(root, config.resourceRoot);
if (!fs.existsSync(resourceRoot)) {
reportError(`${surface} resourceRoot does not exist: ${config.resourceRoot}`);
continue;
}
for (const localeId of localeContract.surfaceOrders?.[surface] ?? []) {
if (surface === 'web-ui') {
const localeDir = path.join(resourceRoot, localeId);
if (!fs.existsSync(localeDir)) {
reportError(`${surface} is missing ${localeId} locale directory`);
}
} else if (surface === 'installer') {
const installerLocale = localeById.get(localeId)?.installer?.uiCode;
if (!installerLocale || !fs.existsSync(path.join(resourceRoot, `${installerLocale}.json`))) {
reportError(`${surface} is missing ${localeId} resource JSON`);
}
} else if (surface === 'core') {
if (!fs.existsSync(path.join(resourceRoot, `${localeId}.ftl`))) {
reportError(`${surface} is missing ${localeId} Fluent resource`);
}
} else if (surface === 'mobile-web') {
if (!fs.existsSync(path.join(resourceRoot, 'messages.ts'))) {
reportError(`${surface} is missing messages.ts`);
}
}
}
}
}
function auditGeneratedContract() {
try {
execFileSync(process.execPath, ['scripts/generate-i18n-contract.mjs', '--check'], {
cwd: root,
stdio: 'pipe',
});
} catch (error) {
const stderr = error.stderr?.toString?.().trim();
reportError(`Generated i18n contract files are out of date. Run pnpm run i18n:generate.${stderr ? ` ${stderr}` : ''}`);
}
}
function auditSharedTermsCoverage() {
const expectedLocaleIds = localeContract.locales.map((locale) => locale.id);
if (!fs.existsSync(sharedTermsDir)) {
reportError(`Missing shared i18n terms directory: ${toPosixPath(path.relative(root, sharedTermsDir))}`);
return;
}
const baselineTermsPath = path.join(sharedTermsDir, expectedLocaleIds[0], 'terms.json');
let baselineKeys = [];
try {
baselineKeys = flattenKeys(readJsonFile(baselineTermsPath));
} catch (error) {
reportError(`Failed to parse ${toPosixPath(path.relative(root, baselineTermsPath))}: ${error.message}`);
return;
}
for (const localeId of expectedLocaleIds) {
const termsPath = path.join(sharedTermsDir, localeId, 'terms.json');
if (!fs.existsSync(termsPath)) {
reportError(`${localeId} is missing shared terms.json`);
continue;
}
let keys = [];
try {
keys = flattenKeys(readJsonFile(termsPath));
} catch (error) {
reportError(`Failed to parse ${toPosixPath(path.relative(root, termsPath))}: ${error.message}`);
continue;
}
for (const key of diffSets(baselineKeys, keys)) {
reportError(`${localeId} shared terms.json is missing key "${key}"`);
}
for (const key of diffSets(keys, baselineKeys)) {
reportError(`${localeId} shared terms.json has extra key "${key}"`);
}
}
}
function auditMobileWebBoundary() {
const sourceFiles = listFiles(
mobileWebSourceDir,
(file) => file.endsWith('.ts') || file.endsWith('.tsx'),
);
const forbiddenPatterns = [
/src[/\\]web-ui[/\\]src[/\\]locales/,
/src[/\\]web-ui[/\\]src[/\\]infrastructure[/\\]i18n/,
/\.\.[/\\]\.\.[/\\]web-ui[/\\]/,
];
for (const file of sourceFiles) {
const text = fs.readFileSync(file, 'utf8');
if (forbiddenPatterns.some((pattern) => pattern.test(text))) {
reportError(`${toPosixPath(path.relative(root, file))} imports or references web-ui i18n resources`);
}
}
}
function auditKeyParity(namespaces) {
for (const namespace of namespaces) {
const baselineKeys = readJsonKeys(baselineLocale, namespace);
for (const locale of supportedLocales.filter((item) => item !== baselineLocale)) {
const localeKeys = readJsonKeys(locale, namespace);
const missing = diffSets(baselineKeys, localeKeys);
const extra = diffSets(localeKeys, baselineKeys);
if (missing.length > 0) {
reportError(`${locale}/${namespace}.json is missing ${missing.length} key(s): ${missing.slice(0, 8).join(', ')}`);
}
if (extra.length > 0) {
reportError(`${locale}/${namespace}.json has ${extra.length} extra key(s): ${extra.slice(0, 8).join(', ')}`);
}
}
}
}
function auditWebI18nextPlaceholderParity(namespaces) {
for (const namespace of namespaces) {
const baselineEntries = readJsonEntries(baselineLocale, namespace);
const baselinePlaceholders = new Map(
Array.from(baselineEntries.entries()).map(([key, value]) => [
key,
extractI18nextPlaceholders(value),
]),
);
for (const locale of supportedLocales.filter((item) => item !== baselineLocale)) {
const localeEntries = readJsonEntries(locale, namespace);
for (const [key, expected] of baselinePlaceholders.entries()) {
if (!localeEntries.has(key)) continue;
const actual = extractI18nextPlaceholders(localeEntries.get(key));
reportPlaceholderParity(`web-ui ${namespace}`, locale, key, expected, actual);
}
}
}
}
function auditMobileWebMessageParity() {
const messagesByLocale = readMobileMessageKeysByLocale();
const baselineKeys = messagesByLocale.get('en-US');
if (!baselineKeys) {
reportError('mobile-web messages are missing the en-US baseline locale');
return;
}
for (const [locale, keys] of messagesByLocale.entries()) {
if (locale === 'en-US') continue;
const missing = diffSets(baselineKeys, keys);
const extra = diffSets(keys, baselineKeys);
if (missing.length > 0) {
reportError(`mobile-web ${locale} messages are missing ${missing.length} key(s): ${missing.slice(0, 8).join(', ')}`);
}
if (extra.length > 0) {
reportError(`mobile-web ${locale} messages have ${extra.length} extra key(s): ${extra.slice(0, 8).join(', ')}`);
}
}
}
function auditMobileWebPlaceholderParity() {
const messagesByLocale = readMobileMessagesByLocale();
const baselineEntries = messagesByLocale.get('en-US');
if (!baselineEntries) {
reportError('mobile-web messages are missing the en-US baseline locale');
return;
}
const baselinePlaceholders = new Map(
Array.from(baselineEntries.entries()).map(([key, value]) => [
key,
extractMobilePlaceholders(value),
]),
);
for (const [locale, entries] of messagesByLocale.entries()) {
if (locale === 'en-US') continue;
for (const [key, expected] of baselinePlaceholders.entries()) {
if (!entries.has(key)) continue;
const actual = extractMobilePlaceholders(entries.get(key));
reportPlaceholderParity('mobile-web', locale, key, expected, actual);
}
}
}
function auditInstallerKeyParity() {
const baselineKeys = readInstallerJsonKeys('en');
for (const uiLocale of ['zh', 'zh-TW']) {
const keys = readInstallerJsonKeys(uiLocale);
const missing = diffSets(baselineKeys, keys);
const extra = diffSets(keys, baselineKeys);
if (missing.length > 0) {
reportError(`installer ${uiLocale}.json is missing ${missing.length} key(s): ${missing.slice(0, 8).join(', ')}`);
}
if (extra.length > 0) {
reportError(`installer ${uiLocale}.json has ${extra.length} extra key(s): ${extra.slice(0, 8).join(', ')}`);
}
}
}
function auditInstallerPlaceholderParity() {
const baselineEntries = readInstallerJsonEntries('en');
const baselinePlaceholders = new Map(
Array.from(baselineEntries.entries()).map(([key, value]) => [
key,
extractI18nextPlaceholders(value),
]),
);
for (const uiLocale of ['zh', 'zh-TW']) {
const entries = readInstallerJsonEntries(uiLocale);
for (const [key, expected] of baselinePlaceholders.entries()) {
if (!entries.has(key)) continue;
const actual = extractI18nextPlaceholders(entries.get(key));
reportPlaceholderParity('installer', uiLocale, key, expected, actual);
}
}
}
function readFluentMessages(localeId) {
const file = path.join(coreLocalesDir, `${localeId}.ftl`);
const messages = new Map();
let currentKey = null;
let currentLines = [];
function flushCurrent() {
if (currentKey) {
messages.set(currentKey, currentLines.join('\n'));
}
currentKey = null;
currentLines = [];
}
try {
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
for (const line of lines) {
const match = line.match(/^([A-Za-z][\w-]*)\s*=\s*(.*)$/);
if (match) {
flushCurrent();
currentKey = match[1];
currentLines = [match[2]];
continue;
}
if (currentKey && (/^\s+/.test(line) || line.trim().startsWith('*[') || line.trim().startsWith('['))) {
currentLines.push(line);
}
}
flushCurrent();
} catch (error) {
reportError(`Failed to parse ${toPosixPath(path.relative(root, file))}: ${error.message}`);
}
return messages;
}
function auditCoreFluentParity() {
const coreLocales = localeContract.surfaceOrders?.core ?? [];
const baselineCoreLocale = coreLocales.includes('en-US') ? 'en-US' : coreLocales[0];
const baselineEntries = readFluentMessages(baselineCoreLocale);
const baselineKeys = Array.from(baselineEntries.keys()).sort();
const baselinePlaceholders = new Map(
Array.from(baselineEntries.entries()).map(([key, value]) => [
key,
extractFluentPlaceholders(value),
]),
);
for (const locale of coreLocales.filter((item) => item !== baselineCoreLocale)) {
const entries = readFluentMessages(locale);
const keys = Array.from(entries.keys()).sort();
for (const key of diffSets(baselineKeys, keys)) {
reportError(`core ${locale}.ftl is missing key "${key}"`);
}
for (const key of diffSets(keys, baselineKeys)) {
reportError(`core ${locale}.ftl has extra key "${key}"`);
}
for (const [key, expected] of baselinePlaceholders.entries()) {
if (!entries.has(key)) continue;
const actual = extractFluentPlaceholders(entries.get(key));
reportPlaceholderParity('core Fluent', locale, key, expected, actual);
}
}
}
function readRelayHomepageMessages() {
let resource;
try {
resource = readJsonFile(relayHomepageI18nPath);
} catch (error) {
reportError(`Failed to parse ${toPosixPath(path.relative(root, relayHomepageI18nPath))}: ${error.message}`);
return { localeIds: [], entriesByLocale: new Map() };
}
const entriesByLocale = new Map();
for (const [locale, messages] of Object.entries(resource)) {
entriesByLocale.set(locale, new Map(flattenRelayHomepageEntries(messages, locale)));
}
return {
localeIds: Object.keys(resource).sort(),
entriesByLocale,
};
}
function flattenRelayHomepageEntries(value, locale, prefix = '') {
if (isPlainObject(value) && Object.hasOwn(value, '$shared')) {
const keys = Object.keys(value);
if (keys.length !== 1) {
reportError(`relay static homepage ${locale} key "${prefix}" mixes $shared with local fields`);
}
const sharedKey = value.$shared;
if (!isNonEmptyString(sharedKey)) {
reportError(`relay static homepage ${locale} key "${prefix}" has an invalid $shared reference`);
return prefix ? [[prefix, '']] : [];
}
if (!readSharedTermMap(locale).has(sharedKey)) {
reportError(`relay static homepage ${locale} key "${prefix}" references missing shared term "${sharedKey}"`);
}
return prefix ? [[prefix, `shared:${sharedKey}`]] : [];
}
if (typeof value === 'string') {
return prefix ? [[prefix, value]] : [];
}
if (Array.isArray(value)) {
const text = value.filter((item) => typeof item === 'string').join('\n');
return prefix ? [[prefix, text]] : [];
}
if (value == null || typeof value !== 'object') {
return prefix ? [[prefix, '']] : [];
}
return Object.entries(value)
.flatMap(([key, child]) => flattenRelayHomepageEntries(child, locale, prefix ? `${prefix}.${key}` : key))
.sort(([left], [right]) => left.localeCompare(right));
}
function collectRelayHomepageDataKeys() {
const htmlPath = path.join(relayHomepageDir, 'index.html');
const html = fs.readFileSync(htmlPath, 'utf8');
return sortedUnique(Array.from(html.matchAll(/\bdata-i18n="([^"]+)"/g), (match) => match[1]));
}
function auditRelayStaticHomepageResources() {
const expectedLocaleIds = (localeContract.locales ?? []).map((locale) => locale.id).sort();
const { localeIds, entriesByLocale } = readRelayHomepageMessages();
const baselineLocaleId = expectedLocaleIds.includes('en-US') ? 'en-US' : expectedLocaleIds[0];
const baselineEntries = entriesByLocale.get(baselineLocaleId) ?? new Map();
const baselineKeys = Array.from(baselineEntries.keys()).sort();
const dataKeys = collectRelayHomepageDataKeys();
for (const locale of diffSets(expectedLocaleIds, localeIds)) {
reportError(`relay static homepage i18n.json is missing locale "${locale}"`);
}
for (const locale of diffSets(localeIds, expectedLocaleIds)) {
reportError(`relay static homepage i18n.json has non-canonical locale "${locale}"`);
}
for (const key of diffSets(dataKeys, baselineKeys)) {
reportError(`relay static homepage index.html references missing i18n key "${key}"`);
}
for (const key of diffSets(baselineKeys, dataKeys)) {
reportError(`relay static homepage i18n.json has unused baseline key "${key}"`);
}
const baselinePlaceholders = new Map(
Array.from(baselineEntries.entries()).map(([key, value]) => [
key,
extractI18nextPlaceholders(value),
]),
);
for (const locale of expectedLocaleIds.filter((item) => item !== baselineLocaleId)) {
const entries = entriesByLocale.get(locale);
if (!entries) continue;
const keys = Array.from(entries.keys()).sort();
for (const key of diffSets(baselineKeys, keys)) {
reportError(`relay static homepage ${locale} messages are missing key "${key}"`);
}
for (const key of diffSets(keys, baselineKeys)) {
reportError(`relay static homepage ${locale} messages have extra key "${key}"`);
}
for (const [key, expected] of baselinePlaceholders.entries()) {
if (!entries.has(key)) continue;
const actual = extractI18nextPlaceholders(entries.get(key));
reportPlaceholderParity('relay static homepage', locale, key, expected, actual);
}
}
}
function maybeNamespaceResourceKey(namespace, key) {
return namespace ? `${namespace}:${key}` : key;
}
function pushResourceEntry(entries, { surface, locale, namespace = null, key, value, file }) {
entries.push({
surface,
locale,
namespace,
key,
resourceKey: maybeNamespaceResourceKey(namespace, key),
value: String(value ?? ''),
file,
});
}
function collectI18nResourceEntries(namespaces) {
const entries = [];
const localeById = new Map(localeContract.locales.map((locale) => [locale.id, locale]));
for (const locale of supportedLocales) {
for (const namespace of namespaces) {
const surface = namespace === 'shared' ? 'shared' : 'web-ui';
const file = namespace === 'shared'
? `src/shared/i18n/resources/shared/${locale}/terms.json`
: `src/web-ui/src/locales/${locale}/${namespace}.json`;
for (const [key, value] of readJsonEntries(locale, namespace)) {
pushResourceEntry(entries, {
surface,
locale,
namespace: namespace === 'shared' ? 'shared' : namespace,
key,
value,
file,
});
}
}
}
if (auditTypeScript) {
for (const [locale, messageEntries] of readMobileMessagesByLocale().entries()) {
for (const [key, value] of messageEntries.entries()) {
pushResourceEntry(entries, {
surface: 'mobile-web',
locale,
key,
value,
file: 'src/mobile-web/src/i18n/messages.ts',
});
}
}
}
for (const localeId of localeContract.surfaceOrders?.installer ?? []) {
const uiLocale = localeById.get(localeId)?.installer?.uiCode;
if (!uiLocale) continue;
for (const [key, value] of readInstallerJsonEntries(uiLocale).entries()) {
pushResourceEntry(entries, {
surface: 'installer',
locale: localeId,
key,
value,
file: `BitFun-Installer/src/i18n/locales/${uiLocale}.json`,
});
}
}
for (const locale of localeContract.surfaceOrders?.core ?? []) {
for (const [key, value] of readFluentMessages(locale).entries()) {
pushResourceEntry(entries, {
surface: 'core',
locale,
key,
value,
file: `src/crates/assembly/core/locales/${locale}.ftl`,
});
}
}
const relayMessages = readRelayHomepageMessages();
for (const [locale, relayEntries] of relayMessages.entriesByLocale.entries()) {
for (const [key, value] of relayEntries.entries()) {
pushResourceEntry(entries, {
surface: 'relay-static-homepage',
locale,
key,
value,
file: 'src/apps/relay-server/static/homepage/i18n.json',
});
}
}
return entries;
}
function resourceGroupId(entry) {
return [entry.surface, entry.namespace ?? '', entry.key].join('\u0000');
}
function buildResourceGroups(entries) {
const groups = new Map();
for (const entry of entries) {
const id = resourceGroupId(entry);
const group = groups.get(id) ?? {
id,
surface: entry.surface,
namespace: entry.namespace,
key: entry.key,
resourceKey: entry.resourceKey,
locales: [],
files: [],
valueByLocale: new Map(),
};
group.locales.push(entry.locale);
group.files.push(entry.file);
group.valueByLocale.set(entry.locale, entry.value);
groups.set(id, group);
}
return Array.from(groups.values()).map((group) => ({
...group,
locales: sortedUnique(group.locales),
files: sortedUnique(group.files),
}));
}
function isNonEmptyString(value) {
return typeof value === 'string' && value.trim().length > 0;
}
function optionalStringArray(entry, field) {
return Array.isArray(entry[field]) ? entry[field] : [];
}
function validateOptionalStringArray(entry, field, label) {
if (Object.hasOwn(entry, field) && !Array.isArray(entry[field])) {
reportError(`${label} "${entry.id}" ${field} must be an array`);
return [];
}
return optionalStringArray(entry, field);
}
function readDynamicKeyOwnerSource(entry) {
if (!isNonEmptyString(entry.owner)) {
return '';
}
const ownerPath = path.join(root, entry.owner);
if (!fs.existsSync(ownerPath)) {
reportError(`Dynamic key allowlist "${entry.id}" owner path does not exist: ${entry.owner}`);
return '';
}
const stat = fs.statSync(ownerPath);
if (stat.isFile()) {
return fs.readFileSync(ownerPath, 'utf8');
}
if (!stat.isDirectory()) {
reportError(`Dynamic key allowlist "${entry.id}" owner path is not a file or directory: ${entry.owner}`);
return '';
}
return listFiles(ownerPath, (file) => /\.(?:cjs|js|jsx|mjs|rs|ts|tsx)$/.test(file))
.map((file) => fs.readFileSync(file, 'utf8'))
.join('\n');
}
function readDynamicKeyAllowlist() {
if (!fs.existsSync(dynamicKeyAllowlistPath)) {
reportError('Missing scripts/i18n-dynamic-key-allowlist.json');
return { entries: [] };
}
let allowlist;
try {
allowlist = readJsonFile(dynamicKeyAllowlistPath);
} catch (error) {
reportError(`Failed to parse scripts/i18n-dynamic-key-allowlist.json: ${error.message}`);
return { entries: [] };
}
if (allowlist.version !== 1) {
reportError('scripts/i18n-dynamic-key-allowlist.json must use version 1');
}
if (!Array.isArray(allowlist.entries)) {
reportError('scripts/i18n-dynamic-key-allowlist.json must define an entries array');
return { entries: [] };
}
const seenIds = new Set();
for (const entry of allowlist.entries) {
if (!isNonEmptyString(entry.id)) {
reportError('Dynamic key allowlist entries require a non-empty id');
continue;
}
if (seenIds.has(entry.id)) {
reportError(`Dynamic key allowlist id "${entry.id}" is duplicated`);
}
seenIds.add(entry.id);
for (const field of ['surface', 'owner', 'description']) {
if (!isNonEmptyString(entry[field])) {
reportError(`Dynamic key allowlist "${entry.id}" requires a non-empty ${field}`);
}
}
const keys = validateOptionalStringArray(entry, 'keys', 'Dynamic key allowlist');
const prefixes = validateOptionalStringArray(entry, 'keyPrefixes', 'Dynamic key allowlist');
const sourceReferences = validateOptionalStringArray(entry, 'sourceReferences', 'Dynamic key allowlist');
if (keys.length === 0 && prefixes.length === 0) {
reportError(`Dynamic key allowlist "${entry.id}" must define keys or keyPrefixes`);
}
if (sourceReferences.length === 0) {
reportError(`Dynamic key allowlist "${entry.id}" must define sourceReferences`);
}
for (const key of keys) {
if (!isNonEmptyString(key)) {
reportError(`Dynamic key allowlist "${entry.id}" has an invalid key entry`);
}
}
for (const prefix of prefixes) {
if (!isNonEmptyString(prefix)) {
reportError(`Dynamic key allowlist "${entry.id}" has an invalid keyPrefixes entry`);
}
}
for (const sourceReference of sourceReferences) {
if (!isNonEmptyString(sourceReference)) {
reportError(`Dynamic key allowlist "${entry.id}" has an invalid sourceReferences entry`);
}
}
const ownerSource = sourceReferences.length > 0 ? readDynamicKeyOwnerSource(entry) : '';
let proofState = sourceReferences.length > 0 ? 'source-proven' : 'source-unproven';
for (const sourceReference of sourceReferences.filter(isNonEmptyString)) {
if (!ownerSource.includes(sourceReference)) {
reportError(`Dynamic key allowlist "${entry.id}" source reference "${sourceReference}" was not found under ${entry.owner}`);
proofState = 'source-unproven';
}
}
entry.proofState = proofState;
}
return allowlist;
}
function readL10nIdenticalAllowlist() {
if (!fs.existsSync(l10nIdenticalAllowlistPath)) {
reportError('Missing scripts/i18n-l10n-identical-allowlist.json');
return { entries: [] };
}
let allowlist;
try {
allowlist = readJsonFile(l10nIdenticalAllowlistPath);
} catch (error) {
reportError(`Failed to parse scripts/i18n-l10n-identical-allowlist.json: ${error.message}`);
return { entries: [] };
}
if (allowlist.version !== 1) {
reportError('scripts/i18n-l10n-identical-allowlist.json must use version 1');
}
if (!Array.isArray(allowlist.entries)) {
reportError('scripts/i18n-l10n-identical-allowlist.json must define an entries array');
return { entries: [] };
}
const seenIds = new Set();
for (const entry of allowlist.entries) {
if (!isNonEmptyString(entry.id)) {
reportError('L10n identical allowlist entries require a non-empty id');
continue;
}
if (seenIds.has(entry.id)) {
reportError(`L10n identical allowlist id "${entry.id}" is duplicated`);
}
seenIds.add(entry.id);
for (const field of ['surface', 'locale', 'comparisonLocale', 'owner', 'reason']) {
if (!isNonEmptyString(entry[field])) {
reportError(`L10n identical allowlist "${entry.id}" requires a non-empty ${field}`);
}
}
const keys = validateOptionalStringArray(entry, 'keys', 'L10n identical allowlist');
const prefixes = validateOptionalStringArray(entry, 'keyPrefixes', 'L10n identical allowlist');
if (keys.length === 0 && prefixes.length === 0) {
reportError(`L10n identical allowlist "${entry.id}" must define keys or keyPrefixes`);
}
for (const key of keys) {
if (!isNonEmptyString(key)) {
reportError(`L10n identical allowlist "${entry.id}" has an invalid key entry`);
}
}
for (const prefix of prefixes) {
if (!isNonEmptyString(prefix)) {
reportError(`L10n identical allowlist "${entry.id}" has an invalid keyPrefixes entry`);
}
}
}
return allowlist;
}
function readGovernanceBaseline() {
if (governanceBaselineCache !== undefined) {
return governanceBaselineCache;
}
if (!fs.existsSync(governanceBaselinePath)) {
reportError('Missing scripts/i18n-governance-baseline.json');
governanceBaselineCache = null;
return null;
}
let baseline;
try {
baseline = readJsonFile(governanceBaselinePath);
} catch (error) {
reportError(`Failed to parse scripts/i18n-governance-baseline.json: ${error.message}`);
governanceBaselineCache = null;
return null;
}
if (baseline.version !== 1) {
reportError('scripts/i18n-governance-baseline.json must use version 1');
}
if (!isPlainObject(baseline.budgets)) {
reportError('scripts/i18n-governance-baseline.json must define a budgets object');
governanceBaselineCache = null;
return null;
}
governanceBaselineCache = baseline;
return baseline;
}
function collectGovernanceBudgetSurfaceIds() {
const baseline = readGovernanceBaseline();
if (!baseline) return [];
const surfaces = new Set();
for (const budget of Object.values(baseline.budgets)) {
if (!isPlainObject(budget?.bySurface)) continue;
for (const surface of Object.keys(budget.bySurface)) {
surfaces.add(surface);
}
}
return Array.from(surfaces).sort();
}
function allowlistTargetForGroup(entry, group) {
if (entry.surface !== group.surface) return null;
if (entry.namespace && entry.namespace !== group.namespace) return null;
if (entry.locale && !group.locales.includes(entry.locale)) return null;
return entry.namespace ? group.key : group.resourceKey;
}
function collectDynamicKeyCandidates(resourceGroups) {
const allowlist = readDynamicKeyAllowlist();
const seen = new Set();
function addCandidate(entry, group) {
const id = [entry.id, group.id].join('\u0000');
if (seen.has(id)) return;
seen.add(id);
governanceReport.dynamicKeyCandidates.push({
allowlistId: entry.id,
surface: group.surface,
namespace: group.namespace,
key: entry.namespace ? group.key : group.resourceKey,
resourceKey: group.resourceKey,
owner: entry.owner,
reason: entry.description,
proofState: entry.proofState ?? 'source-unproven',
sourceReferences: entry.sourceReferences ?? [],
locales: group.locales,
files: group.files,
});
}
for (const entry of allowlist.entries ?? []) {
const eligibleGroups = resourceGroups.filter((group) => allowlistTargetForGroup(entry, group) != null);
for (const key of optionalStringArray(entry, 'keys')) {
const matches = eligibleGroups.filter((group) => allowlistTargetForGroup(entry, group) === key);
if (matches.length === 0) {
reportError(`Dynamic key allowlist "${entry.id}" references "${key}" but no ${entry.surface} resource matches`);
}
for (const group of matches) {
addCandidate(entry, group);
}
}
for (const prefix of optionalStringArray(entry, 'keyPrefixes')) {
const matches = eligibleGroups.filter((group) => allowlistTargetForGroup(entry, group).startsWith(prefix));
if (matches.length === 0) {
reportError(`Dynamic key allowlist "${entry.id}" references prefix "${prefix}" but no ${entry.surface} resource matches`);
}
for (const group of matches) {
addCandidate(entry, group);
}
}
}
}
function l10nAllowlistTargetForGroup(entry, group) {
if (entry.surface !== group.surface) return null;
if (entry.namespace && entry.namespace !== group.namespace) return null;
if (!group.valueByLocale.has(entry.locale) || !group.valueByLocale.has(entry.comparisonLocale)) return null;
return entry.namespace ? group.key : group.resourceKey;
}
function l10nIdenticalMatchId(group, locale, comparisonLocale) {
return [group.id, locale, comparisonLocale].join('\u0000');
}
function collectAllowedL10nIdenticalMatches(resourceGroups) {
const allowlist = readL10nIdenticalAllowlist();
const allowed = new Set();
function isIdenticalCandidate(group, entry) {
const value = group.valueByLocale.get(entry.locale);
const comparisonValue = group.valueByLocale.get(entry.comparisonLocale);
return Boolean(value && comparisonValue && value === comparisonValue && hasHanText(value));
}
for (const entry of allowlist.entries ?? []) {
const eligibleGroups = resourceGroups.filter((group) => (
l10nAllowlistTargetForGroup(entry, group) != null && isIdenticalCandidate(group, entry)
));
for (const key of optionalStringArray(entry, 'keys')) {
const matches = eligibleGroups.filter((group) => l10nAllowlistTargetForGroup(entry, group) === key);
if (matches.length === 0) {
reportError(`L10n identical allowlist "${entry.id}" references "${key}" but no ${entry.surface} identical resource matches`);
}
for (const group of matches) {
allowed.add(l10nIdenticalMatchId(group, entry.locale, entry.comparisonLocale));
}
}
for (const prefix of optionalStringArray(entry, 'keyPrefixes')) {
const matches = eligibleGroups.filter((group) => l10nAllowlistTargetForGroup(entry, group).startsWith(prefix));
if (matches.length === 0) {
reportError(`L10n identical allowlist "${entry.id}" references prefix "${prefix}" but no ${entry.surface} identical resource matches`);
}
for (const group of matches) {
allowed.add(l10nIdenticalMatchId(group, entry.locale, entry.comparisonLocale));
}
}
}
return allowed;
}
function collectSharedTermDuplicates(resourceEntries) {
const sharedByLocaleAndValue = new Map();
for (const entry of resourceEntries.filter((item) => item.surface === 'shared')) {
if (!entry.value) continue;
const localeMap = sharedByLocaleAndValue.get(entry.locale) ?? new Map();
localeMap.set(entry.value, [...(localeMap.get(entry.value) ?? []), entry]);
sharedByLocaleAndValue.set(entry.locale, localeMap);
}
for (const entry of resourceEntries.filter((item) => item.surface !== 'shared')) {
if (!entry.value) continue;
const matches = sharedByLocaleAndValue.get(entry.locale)?.get(entry.value) ?? [];
for (const shared of matches) {
governanceReport.sharedTermDuplicates.push({
surface: entry.surface,
locale: entry.locale,
namespace: entry.namespace,
key: entry.key,
resourceKey: entry.resourceKey,
sharedKey: shared.key,
sharedResourceKey: shared.resourceKey,
value: entry.value,
file: entry.file,
reason: 'matches-shared-term-value',
});
}
}
}
function collectSameTextLocaleInventory(resourceGroups, allowedIdenticalMatches) {
for (const group of resourceGroups) {
const simplified = group.valueByLocale.get('zh-CN');
const traditional = group.valueByLocale.get('zh-TW');
if (!simplified || !traditional || simplified !== traditional || !hasHanText(traditional)) {
continue;
}
const signal = getZhTwSameTextSignal(traditional);
const allowlisted = allowedIdenticalMatches.has(l10nIdenticalMatchId(group, 'zh-TW', 'zh-CN'));
governanceReport.sameTextLocaleInventory.push({
surface: group.surface,
namespace: group.namespace,
key: group.key,
resourceKey: group.resourceKey,
locale: 'zh-TW',
comparisonLocale: 'zh-CN',
value: traditional,
files: group.files,
reason: 'same-text-locale-pair',
signal,
signalType: signal?.type ?? 'none',
allowlisted,
allowlistState: allowlisted ? 'allowlisted' : 'unreviewed',
});
}
}
function collectL10nQualityCandidates(resourceGroups, allowedIdenticalMatches) {
for (const group of resourceGroups) {
const simplified = group.valueByLocale.get('zh-CN');
const traditional = group.valueByLocale.get('zh-TW');
if (!simplified || !traditional || simplified !== traditional || !hasHanText(traditional)) {
continue;
}
const signal = getZhTwSameTextSignal(traditional);
if (!signal) {
continue;
}
if (allowedIdenticalMatches.has(l10nIdenticalMatchId(group, 'zh-TW', 'zh-CN'))) {
continue;
}
governanceReport.l10nQualityCandidates.push({
surface: group.surface,
namespace: group.namespace,
key: group.key,
resourceKey: group.resourceKey,
locale: 'zh-TW',
comparisonLocale: 'zh-CN',
value: traditional,
files: group.files,
reason: 'matches-comparison-locale',
signal,
});
}
}
function collectConfirmedUnusedKeys() {
const expectedLocaleIds = (localeContract.locales ?? []).map((locale) => locale.id).sort();
const baselineLocaleId = expectedLocaleIds.includes('en-US') ? 'en-US' : expectedLocaleIds[0];
const { entriesByLocale } = readRelayHomepageMessages();
const baselineEntries = entriesByLocale.get(baselineLocaleId) ?? new Map();
const dataKeys = collectRelayHomepageDataKeys();
for (const key of diffSets(Array.from(baselineEntries.keys()).sort(), dataKeys)) {
governanceReport.confirmedUnusedKeys.push({
surface: 'relay-static-homepage',
key,
resourceKey: key,
file: 'src/apps/relay-server/static/homepage/i18n.json',
reason: 'not-referenced-by-static-data-i18n-attribute',
});
}
}
function auditGovernanceCategoryBudget(category, budget) {
if (!isPlainObject(budget)) {
reportError(`scripts/i18n-governance-baseline.json ${category} budget must be an object`);
return;
}
const entries = governanceReport[category] ?? [];
if (typeof budget.maxTotal !== 'number') {
reportError(`scripts/i18n-governance-baseline.json ${category}.maxTotal must be a number`);
} else if (entries.length > budget.maxTotal) {
reportError(`${category} has ${entries.length} candidate(s), baseline is ${budget.maxTotal}`);
} else if (entries.length < budget.maxTotal) {
reportError(`${category} has ${entries.length} candidate(s), below baseline ${budget.maxTotal}; lower scripts/i18n-governance-baseline.json.`);
}
if (budget.bySurface) {
if (!isPlainObject(budget.bySurface)) {
reportError(`scripts/i18n-governance-baseline.json ${category}.bySurface must be an object`);
return;
}
const actualBySurface = countEntriesBy(entries, 'surface');
const surfaces = sortedUnique([
...Object.keys(actualBySurface),
...Object.keys(budget.bySurface),
]);
for (const surface of surfaces) {
const actual = actualBySurface[surface] ?? 0;
const expected = budget.bySurface[surface];
if (typeof expected !== 'number') {
reportError(`scripts/i18n-governance-baseline.json ${category}.bySurface.${surface} must be a number`);
} else if (actual > expected) {
reportError(`${category} ${surface} has ${actual} candidate(s), baseline is ${expected}`);
} else if (actual < expected) {
reportError(`${category} ${surface} has ${actual} candidate(s), below baseline ${expected}; lower scripts/i18n-governance-baseline.json.`);
}
}
}
auditGovernanceDimensionBudget(category, budget, 'byNamespace', 'namespace', 'namespace');
auditGovernanceDimensionBudget(category, budget, 'bySharedKey', 'sharedKey', 'sharedKey');
auditGovernanceDimensionBudget(category, budget, 'byComparisonLocale', 'comparisonLocale', 'comparisonLocale');
}
function auditGovernanceDimensionBudget(category, budget, budgetField, entryField, label) {
if (!Object.hasOwn(budget, budgetField)) return;
if (!isPlainObject(budget[budgetField])) {
reportError(`scripts/i18n-governance-baseline.json ${category}.${budgetField} must be an object`);
return;
}
const entries = governanceReport[category] ?? [];
const actualByValue = countEntriesBy(entries, entryField, { emptyLabel: '<none>' });
const values = sortedUnique([
...Object.keys(actualByValue),
...Object.keys(budget[budgetField]),
]);
for (const value of values) {
const actual = actualByValue[value] ?? 0;
const expected = budget[budgetField][value];
if (typeof expected !== 'number') {
reportError(`scripts/i18n-governance-baseline.json ${category}.${budgetField}.${value} must be a number`);
} else if (actual > expected) {
reportError(`${category} ${label} ${value} has ${actual} candidate(s), baseline is ${expected}`);
} else if (actual < expected) {
reportError(`${category} ${label} ${value} has ${actual} candidate(s), below baseline ${expected}; lower scripts/i18n-governance-baseline.json.`);
}
}
}
function auditGovernanceBaseline() {
const baseline = readGovernanceBaseline();
if (!baseline) return;
governanceReport.summary.baseline.enforced = true;
for (const category of ['confirmedUnusedKeys', 'sharedTermDuplicates', 'l10nQualityCandidates']) {
auditGovernanceCategoryBudget(category, baseline.budgets[category]);
}
}
function auditI18nGovernanceReport(namespaces) {
const resourceEntries = collectI18nResourceEntries(namespaces);
const resourceGroups = buildResourceGroups(resourceEntries);
const allowedIdenticalMatches = collectAllowedL10nIdenticalMatches(resourceGroups);
collectConfirmedUnusedKeys();
collectDynamicKeyCandidates(resourceGroups);
collectSharedTermDuplicates(resourceEntries);
collectSameTextLocaleInventory(resourceGroups, allowedIdenticalMatches);
collectL10nQualityCandidates(resourceGroups, allowedIdenticalMatches);
auditGovernanceBaseline();
}
function shouldSkipSourceScan(file) {
const normalized = toPosixPath(path.relative(root, file));
return (
normalized.includes('/locales/') ||
normalized.endsWith('/generatedLocaleContract.ts') ||
normalized.endsWith('.test.ts') ||
normalized.endsWith('.test.tsx') ||
normalized.endsWith('.spec.ts') ||
normalized.endsWith('.spec.tsx') ||
normalized.includes('/component-library/components/registry.tsx')
);
}
function shouldSkipMobileWebSourceScan(file) {
const normalized = toPosixPath(path.relative(root, file));
return (
normalized.endsWith('/i18n/messages.ts') ||
normalized.endsWith('/i18n/generatedLocaleContract.ts') ||
normalized.endsWith('.test.ts') ||
normalized.endsWith('.test.tsx') ||
normalized.endsWith('.spec.ts') ||
normalized.endsWith('.spec.tsx')
);
}
function shouldSkipInstallerSourceScan(file) {
const normalized = toPosixPath(path.relative(root, file));
return (
normalized.includes('/i18n/locales/') ||
normalized.endsWith('/i18n/generatedLocaleContract.ts') ||
normalized.endsWith('.test.ts') ||
normalized.endsWith('.test.tsx') ||
normalized.endsWith('.spec.ts') ||
normalized.endsWith('.spec.tsx')
);
}
function auditSourceText() {
const sourceFiles = listFiles(
webSourceDir,
(file) => (file.endsWith('.ts') || file.endsWith('.tsx')) && !shouldSkipSourceScan(file),
);
const fallbackFindings = [];
const fallbackPattern = /\bt\s*\(\s*(['"`])(?:\\.|(?!\1).)+\1\s*,\s*(['"`])/g;
for (const file of sourceFiles) {
const text = fs.readFileSync(file, 'utf8');
const lines = text.split(/\r?\n/);
lines.forEach((line, index) => {
if (fallbackPattern.test(line)) {
fallbackFindings.push(`${toPosixPath(path.relative(root, file))}:${index + 1}`);
}
fallbackPattern.lastIndex = 0;
});
}
if (fallbackFindings.length > 0) {
reportError(`Found ${fallbackFindings.length} t(key, "literal fallback") candidate(s). First entries: ${fallbackFindings.slice(0, 12).join(', ')}`);
}
}
function lineNumberAt(text, index) {
return text.slice(0, index).split(/\r?\n/).length;
}
function createAuditSourceFile(file, text) {
const ts = auditTypeScript;
return ts.createSourceFile(
file,
text,
ts.ScriptTarget.Latest,
true,
file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
);
}
function staticStringLiteral(ts, expression) {
const value = unwrapTsExpression(ts, expression);
if (!value) return null;
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) {
return value.text;
}
return null;
}
function staticStringArrayLiteral(ts, expression) {
const value = unwrapTsExpression(ts, expression);
if (!value || !ts.isArrayLiteralExpression(value)) return null;
const values = [];
for (const element of value.elements) {
const literal = staticStringLiteral(ts, element);
if (!literal) return null;
values.push(literal);
}
return values.length > 0 ? values : null;
}
function isI18nServiceGetTCall(ts, expression) {
return (
ts.isCallExpression(expression) &&
ts.isPropertyAccessExpression(expression.expression) &&
expression.expression.name.text === 'getT' &&
ts.isIdentifier(expression.expression.expression) &&
expression.expression.expression.text === 'i18nService'
);
}
function isI18nServiceTCall(ts, expression) {
return (
ts.isPropertyAccessExpression(expression) &&
expression.name.text === 't' &&
ts.isIdentifier(expression.expression) &&
expression.expression.text === 'i18nService'
);
}
function isI18nHookCall(ts, expression) {
return (
ts.isCallExpression(expression) &&
ts.isIdentifier(expression.expression) &&
(expression.expression.text === 'useI18n' || expression.expression.text === 'useTranslation')
);
}
function collectWebUiTranslationCallFacts() {
const ts = auditTypeScript;
const sourceFiles = listFiles(
webSourceDir,
(file) => (file.endsWith('.ts') || file.endsWith('.tsx')) && !shouldSkipSourceScan(file),
);
const output = [];
for (const file of sourceFiles) {
const text = fs.readFileSync(file, 'utf8');
const sourceFile = createAuditSourceFile(file, text);
const hookNamespaceByTranslator = new Map();
const hookNamespaceListByTranslator = new Map();
const fullKeyTranslatorNames = new Set();
function rememberHookTranslator(node) {
if (!ts.isVariableDeclaration(node) || !node.initializer || !isI18nHookCall(ts, node.initializer)) {
return;
}
const namespace = staticStringLiteral(ts, node.initializer.arguments[0]);
const namespaces = namespace ? null : staticStringArrayLiteral(ts, node.initializer.arguments[0]);
if (!namespace && !namespaces) return;
function rememberLocalName(localName) {
if (!localName) return;
if (namespace) {
hookNamespaceByTranslator.set(localName, namespace);
} else {
hookNamespaceListByTranslator.set(localName, namespaces);
}
}
if (ts.isObjectBindingPattern(node.name)) {
for (const element of node.name.elements) {
const propertyName = element.propertyName ? propertyNameToString(ts, element.propertyName) : propertyNameToString(ts, element.name);
const localName = propertyNameToString(ts, element.name);
if (propertyName === 't' && localName) {
rememberLocalName(localName);
}
}
} else if (ts.isArrayBindingPattern(node.name)) {
const first = node.name.elements[0];
if (first && ts.isBindingElement(first) && ts.isIdentifier(first.name)) {
rememberLocalName(first.name.text);
}
}
}
function rememberGetTTranslator(node) {
if (
ts.isVariableDeclaration(node) &&
ts.isIdentifier(node.name) &&
node.initializer &&
isI18nServiceGetTCall(ts, node.initializer)
) {
fullKeyTranslatorNames.add(node.name.text);
}
}
function recordTranslationCall(node) {
if (!ts.isCallExpression(node)) return;
const key = staticStringLiteral(ts, node.arguments[0]);
if (!key) return;
let fullKey = null;
let kind = 'full';
if (isI18nServiceTCall(ts, node.expression) || isI18nServiceGetTCall(ts, node.expression)) {
if (!key.includes(':')) return;
fullKey = key;
} else if (ts.isIdentifier(node.expression) && fullKeyTranslatorNames.has(node.expression.text)) {
if (!key.includes(':')) return;
fullKey = key;
} else if (ts.isIdentifier(node.expression) && hookNamespaceByTranslator.has(node.expression.text)) {
const namespace = hookNamespaceByTranslator.get(node.expression.text);
fullKey = key.includes(':') ? key : `${namespace}:${key}`;
kind = key.includes(':') ? 'hook-full' : 'hook-relative';
} else if (ts.isIdentifier(node.expression) && hookNamespaceListByTranslator.has(node.expression.text)) {
const namespaces = hookNamespaceListByTranslator.get(node.expression.text);
fullKey = key.includes(':') ? key : `${namespaces[0]}:${key}`;
kind = key.includes(':') ? 'hook-full-array' : 'hook-relative-array';
}
if (!fullKey) return;
output.push({
key: fullKey,
kind,
options: node.arguments[1],
location: `${toPosixPath(path.relative(root, file))}:${lineNumberAt(text, node.getStart(sourceFile))}`,
file: toPosixPath(path.relative(root, file)),
sourceFile,
});
}
function visit(node) {
rememberHookTranslator(node);
rememberGetTTranslator(node);
recordTranslationCall(node);
ts.forEachChild(node, visit);
}
visit(sourceFile);
}
return output;
}
function collectWebUiStaticTranslationKeys() {
return collectWebUiTranslationCallFacts().map(({ key, kind, location }) => ({ key, kind, location }));
}
function buildWebUiKeySet(namespaces) {
const keys = new Set();
for (const namespace of namespaces) {
for (const key of readJsonKeys(baselineLocale, namespace)) {
keys.add(`${namespace}:${key}`);
}
}
return keys;
}
function auditWebUiStaticTranslationKeys(namespaces) {
const knownKeys = buildWebUiKeySet(namespaces);
const missing = collectWebUiStaticTranslationKeys()
.filter(({ key }) => !knownKeys.has(key));
if (missing.length > 0) {
const relativeCount = missing.filter(({ kind }) => kind === 'hook-relative').length;
reportError(
`Found ${missing.length} unknown static Web UI i18n key(s), including ${relativeCount} relative static Web UI i18n key(s). First entries: ${
missing.slice(0, 12).map(({ key, location }) => `${location} ${key}`).join(', ')
}`,
);
}
}
function isLiteralFallbackInitializer(ts, initializer) {
const value = unwrapTsExpression(ts, initializer);
if (!value) return false;
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value) || ts.isTemplateExpression(value)) {
return true;
}
if (ts.isArrayLiteralExpression(value)) {
return value.elements.some((element) => (
ts.isStringLiteral(element) ||
ts.isNoSubstitutionTemplateLiteral(element) ||
ts.isTemplateExpression(element)
));
}
if (ts.isBinaryExpression(value)) {
return (
isLiteralFallbackInitializer(ts, value.left) ||
isLiteralFallbackInitializer(ts, value.right)
);
}
return false;
}
function collectWebUiLiteralFallbackFindings() {
const ts = auditTypeScript;
const findings = [];
for (const call of collectWebUiTranslationCallFacts()) {
const options = unwrapTsExpression(ts, call.options);
if (!options || !ts.isObjectLiteralExpression(options)) continue;
for (const property of options.properties) {
if (!ts.isPropertyAssignment(property)) continue;
if (propertyNameToString(ts, property.name) !== 'defaultValue') continue;
if (!isLiteralFallbackInitializer(ts, property.initializer)) continue;
const [namespace, ...keyParts] = call.key.split(':');
findings.push({
file: call.file,
location: call.location,
namespace: keyParts.length > 0 ? namespace : null,
key: call.key,
});
}
}
return findings;
}
function auditWebUiLiteralFallbackBudget() {
if (!fs.existsSync(literalFallbackBaselinePath)) {
reportError('Missing scripts/i18n-literal-fallback-baseline.json');
return;
}
const baseline = readJsonFile(literalFallbackBaselinePath);
const budgetByFile = new Map(
(baseline.budgets ?? []).map((entry) => [entry.path, entry]),
);
const findingsByFile = new Map();
for (const finding of collectWebUiLiteralFallbackFindings()) {
governanceReport.literalDefaultValueFallbacks.push({
surface: 'web-ui',
namespace: finding.namespace,
key: finding.key,
file: finding.file,
location: finding.location,
reason: 'literal-i18next-defaultValue',
});
findingsByFile.set(finding.file, [...(findingsByFile.get(finding.file) ?? []), finding]);
}
for (const [file, findings] of findingsByFile.entries()) {
const budget = budgetByFile.get(file);
if (!budget) {
reportError(
`${file} has ${findings.length} literal i18next defaultValue fallback(s) but is missing from scripts/i18n-literal-fallback-baseline.json. First entries: ${
findings.slice(0, 8).map((finding) => `${finding.location} ${finding.key}`).join(', ')
}`,
);
continue;
}
const actualCountByKey = new Map();
for (const finding of findings) {
actualCountByKey.set(finding.key, (actualCountByKey.get(finding.key) ?? 0) + 1);
}
if (Array.isArray(budget.literalDefaultValues) || isPlainObject(budget.literalDefaultValues)) {
const allowedCountByKey = new Map(
Array.isArray(budget.literalDefaultValues)
? budget.literalDefaultValues.map((entry) => [entry.key, entry.count])
: Object.entries(budget.literalDefaultValues),
);
for (const [key, count] of actualCountByKey.entries()) {
const allowed = allowedCountByKey.get(key);
if (typeof allowed !== 'number') {
reportError(`${file} has unbudgeted literal i18next defaultValue fallback for "${key}"`);
} else if (count > allowed) {
reportError(`${file} has ${count} literal i18next defaultValue fallback(s) for "${key}", budget is ${allowed}`);
} else if (count < allowed) {
reportError(`${file} has ${count} literal i18next defaultValue fallback(s) for "${key}", below baseline ${allowed}; lower scripts/i18n-literal-fallback-baseline.json.`);
}
}
for (const [key, allowed] of allowedCountByKey.entries()) {
if (allowed > 0 && !actualCountByKey.has(key)) {
reportError(`${file} no longer has a literal i18next defaultValue fallback for "${key}"; lower scripts/i18n-literal-fallback-baseline.json.`);
}
}
} else if (typeof budget.maxLiteralDefaultValues === 'number') {
if (findings.length > budget.maxLiteralDefaultValues) {
reportError(
`${file} has ${findings.length} literal i18next defaultValue fallback(s), budget is ${budget.maxLiteralDefaultValues}. First entries: ${
findings.slice(0, 8).map((finding) => `${finding.location} ${finding.key}`).join(', ')
}`,
);
} else if (findings.length < budget.maxLiteralDefaultValues) {
reportError(
`${file} has ${findings.length} literal i18next defaultValue fallback(s), below baseline ${budget.maxLiteralDefaultValues}; lower scripts/i18n-literal-fallback-baseline.json.`,
);
}
} else {
reportError(`${file} has an invalid literal fallback baseline entry`);
}
}
for (const [file, budget] of budgetByFile.entries()) {
const hasBudgetedFindings = Array.isArray(budget.literalDefaultValues) || isPlainObject(budget.literalDefaultValues)
? Object.keys(budget.literalDefaultValues).length > 0
: budget.maxLiteralDefaultValues > 0;
if (hasBudgetedFindings && !findingsByFile.has(file)) {
reportError(
`${file} no longer has literal i18next defaultValue fallback(s); remove it from scripts/i18n-literal-fallback-baseline.json.`,
);
}
}
}
function countCjkSourceLines(scanRoot, predicate) {
const cjkPattern = /\p{Script=Han}/u;
const findings = [];
const sourceFiles = listFiles(scanRoot, predicate);
for (const file of sourceFiles) {
const text = fs.readFileSync(file, 'utf8');
const lines = text.split(/\r?\n/);
lines.forEach((line, index) => {
if (cjkPattern.test(line)) {
findings.push(`${toPosixPath(path.relative(root, file))}:${index + 1}`);
}
});
}
return findings;
}
function shouldSkipLocaleFormatSourceScan(file) {
const normalized = toPosixPath(path.relative(root, file));
return (
normalized === 'src/web-ui/src/infrastructure/i18n/core/I18nService.ts' ||
normalized === 'src/mobile-web/src/i18n/I18nProvider.tsx' ||
normalized.endsWith('/generatedLocaleContract.ts') ||
normalized.endsWith('.test.ts') ||
normalized.endsWith('.test.tsx') ||
normalized.endsWith('.spec.ts') ||
normalized.endsWith('.spec.tsx')
);
}
function createLocaleFormatScanSpecs() {
return [
{
surface: 'web-ui',
root: webSourceDir,
predicate: (file) => (
(file.endsWith('.ts') || file.endsWith('.tsx')) &&
!shouldSkipSourceScan(file) &&
!shouldSkipLocaleFormatSourceScan(file)
),
},
{
surface: 'mobile-web',
root: mobileWebSourceDir,
predicate: (file) => (
(file.endsWith('.ts') || file.endsWith('.tsx')) &&
!shouldSkipMobileWebSourceScan(file) &&
!shouldSkipLocaleFormatSourceScan(file)
),
},
{
surface: 'installer',
root: installerSourceDir,
predicate: (file) => (
(file.endsWith('.ts') || file.endsWith('.tsx')) &&
!shouldSkipInstallerSourceScan(file) &&
!shouldSkipLocaleFormatSourceScan(file)
),
},
{
surface: 'core-miniapp',
root: path.join(root, 'src', 'crates', 'contracts', 'product-domains', 'src', 'miniapp', 'builtin', 'assets'),
predicate: (file) => file.endsWith('.js'),
},
];
}
function collectLocaleFormatSurfaceIds() {
return sortedUnique(createLocaleFormatScanSpecs().map((spec) => spec.surface));
}
function collectLocaleFormatUsageFindings() {
const formatPattern = /\b(?:new\s+)?Intl\.(?:DateTimeFormat|NumberFormat|RelativeTimeFormat)\s*\(|\.\s*toLocale(?:String|DateString|TimeString)\s*\(/g;
const specs = createLocaleFormatScanSpecs();
const findings = [];
for (const spec of specs) {
for (const file of listFiles(spec.root, spec.predicate)) {
const relativeFile = toPosixPath(path.relative(root, file));
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
lines.forEach((line, index) => {
formatPattern.lastIndex = 0;
let match;
while ((match = formatPattern.exec(line)) != null) {
findings.push({
surface: spec.surface,
file: relativeFile,
location: `${relativeFile}:${index + 1}`,
expression: match[0].trim(),
snippet: line.trim().slice(0, 200),
reason: 'direct-locale-format-call',
});
}
});
}
}
return findings.sort(sortByReportIdentity);
}
function auditLocaleFormatUsageBudget() {
if (!fs.existsSync(localeFormatBaselinePath)) {
reportError('Missing scripts/i18n-locale-format-baseline.json');
return;
}
const baseline = readJsonFile(localeFormatBaselinePath);
if (baseline.version !== 1) {
reportError('scripts/i18n-locale-format-baseline.json must use version 1');
}
if (!Array.isArray(baseline.budgets)) {
reportError('scripts/i18n-locale-format-baseline.json must define a budgets array');
return;
}
const budgetByFile = new Map((baseline.budgets ?? []).map((budget) => [budget.path, budget]));
const findingsByFile = new Map();
for (const finding of collectLocaleFormatUsageFindings()) {
governanceReport.localeFormatCandidates.push(finding);
findingsByFile.set(finding.file, [...(findingsByFile.get(finding.file) ?? []), finding]);
}
for (const [file, findings] of findingsByFile.entries()) {
const budget = budgetByFile.get(file);
if (!budget) {
reportError(
`${file} has ${findings.length} direct locale formatting call(s) but is missing from scripts/i18n-locale-format-baseline.json. First entries: ${
findings.slice(0, 8).map((finding) => `${finding.location} ${finding.expression}`).join(', ')
}`,
);
continue;
}
if (typeof budget.maxLocaleFormatCalls !== 'number') {
reportError(`${file} has an invalid locale format baseline entry`);
} else if (findings.length > budget.maxLocaleFormatCalls) {
reportError(`${file} has ${findings.length} direct locale formatting call(s), budget is ${budget.maxLocaleFormatCalls}`);
} else if (findings.length < budget.maxLocaleFormatCalls) {
reportError(`${file} has ${findings.length} direct locale formatting call(s), below baseline ${budget.maxLocaleFormatCalls}; lower scripts/i18n-locale-format-baseline.json.`);
}
}
for (const [file, budget] of budgetByFile.entries()) {
if (budget.maxLocaleFormatCalls > 0 && !findingsByFile.has(file)) {
reportError(`${file} no longer has direct locale formatting call(s); remove it from scripts/i18n-locale-format-baseline.json.`);
}
}
}
function auditHardcodedSourceBudgets() {
const baseline = readJsonFile(hardcodedBaselinePath);
const budgetById = new Map((baseline.budgets ?? []).map((budget) => [budget.id, budget.maxCjkLines]));
const specs = [
{
id: 'web-ui-source',
root: webSourceDir,
predicate: (file) => (file.endsWith('.ts') || file.endsWith('.tsx')) && !shouldSkipSourceScan(file),
},
{
id: 'mobile-web-source',
root: mobileWebSourceDir,
predicate: (file) => (file.endsWith('.ts') || file.endsWith('.tsx')) && !shouldSkipMobileWebSourceScan(file),
},
{
id: 'installer-source',
root: installerSourceDir,
predicate: (file) => (file.endsWith('.ts') || file.endsWith('.tsx')) && !shouldSkipInstallerSourceScan(file),
},
{
id: 'relay-static-homepage',
root: relayHomepageDir,
predicate: (file) => file.endsWith('.html') || file.endsWith('.js') || file.endsWith('.css'),
},
];
for (const spec of specs) {
const maxCjkLines = budgetById.get(spec.id);
if (typeof maxCjkLines !== 'number') {
reportError(`Missing hardcoded CJK budget for ${spec.id}`);
continue;
}
const findings = countCjkSourceLines(spec.root, spec.predicate);
if (findings.length > maxCjkLines) {
reportError(`${spec.id} has ${findings.length} CJK source candidate line(s), budget is ${maxCjkLines}. First entries: ${findings.slice(0, 12).join(', ')}`);
} else if (findings.length > 0) {
reportWarning(`${spec.id} has ${findings.length} grandfathered CJK source candidate line(s). First entries: ${findings.slice(0, 12).join(', ')}`);
}
}
}
auditGeneratedContract();
auditSharedTermsCoverage();
auditSurfaceResourceRoots();
auditMobileWebBoundary();
const namespaces = auditNamespaceCoverage();
auditKeyParity(namespaces);
auditWebI18nextPlaceholderParity(namespaces);
auditTypeScript = loadTypeScriptForAudit();
if (auditTypeScript) {
auditWebUiStaticTranslationKeys(namespaces);
auditWebUiLiteralFallbackBudget();
auditMobileWebMessageParity();
auditMobileWebPlaceholderParity();
}
auditInstallerKeyParity();
auditInstallerPlaceholderParity();
auditCoreFluentParity();
auditRelayStaticHomepageResources();
auditSourceText();
auditLocaleFormatUsageBudget();
auditHardcodedSourceBudgets();
auditI18nGovernanceReport(namespaces);
writeGovernanceReport();
if (errorCount > 0) {
console.error(`[i18n:audit] Failed with ${errorCount} error(s) and ${warningCount} warning(s).`);
process.exit(1);
}
console.log(`[i18n:audit] Passed with ${warningCount} warning(s).`);