Files
umami/scripts/check-missing-messages.js

138 lines
3.9 KiB
JavaScript

import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const messagesDir = join(__dirname, '..', 'public', 'intl', 'messages');
const sourcePath = join(messagesDir, 'en-US.json');
const removeExtraKeys = process.argv.includes('--remove-extra-keys');
const help = process.argv.includes('--help') || process.argv.includes('-h');
if (help) {
console.log('Usage: node scripts/check-missing-messages.js [--remove-extra-keys]');
process.exit(0);
}
const enRaw = readFileSync(sourcePath, 'utf8');
const newline = enRaw.includes('\r\n') ? '\r\n' : '\n';
const en = JSON.parse(enRaw);
// Flatten nested structure: { label: { foo: 'bar' } } -> { 'label.foo': 'bar' }
function flatten(obj, prefix = '') {
const result = {};
for (const [k, v] of Object.entries(obj)) {
const key = prefix ? `${prefix}.${k}` : k;
if (typeof v === 'object' && v !== null) {
Object.assign(result, flatten(v, key));
} else {
result[key] = v;
}
}
return result;
}
function setNestedValue(target, flatKey, value) {
const parts = flatKey.split('.');
let current = target;
for (let index = 0; index < parts.length - 1; index++) {
current[parts[index]] ??= {};
current = current[parts[index]];
}
current[parts.at(-1)] = value;
}
function buildNormalizedObject(flat) {
const ordered = {};
for (const key of enKeys) {
if (key in flat) {
setNestedValue(ordered, key, flat[key]);
}
}
return ordered;
}
const enFlat = flatten(en);
const enKeys = Object.keys(enFlat);
const enKeySet = new Set(enKeys);
console.log(`en-US.json has ${enKeys.length} keys`);
const files = readdirSync(messagesDir)
.filter(f => f.endsWith('.json') && f !== 'en-US.json')
.sort();
const allMissing = {};
const allExtra = {};
let totalMissing = 0;
let totalExtra = 0;
let updatedFiles = 0;
for (const fname of files) {
const filePath = join(messagesDir, fname);
const raw = readFileSync(filePath, 'utf8');
const data = JSON.parse(raw);
const flat = flatten(data);
const missing = enKeys.filter(k => !(k in flat));
const extra = Object.keys(flat).filter(k => !enKeySet.has(k)).sort();
if (missing.length) {
allMissing[fname] = missing;
console.log(`${fname}: ${missing.length} missing`);
totalMissing += missing.length;
}
if (extra.length) {
allExtra[fname] = extra;
console.log(`${fname}: ${extra.length} extra`);
totalExtra += extra.length;
}
if (removeExtraKeys && extra.length) {
const normalized = buildNormalizedObject(flat);
const nextRaw = `${JSON.stringify(normalized, null, 2).replace(/\n/g, newline)}${newline}`;
if (nextRaw !== raw) {
writeFileSync(filePath, nextRaw, 'utf8');
updatedFiles++;
}
}
}
console.log(`\nTotal missing across all locales: ${totalMissing}`);
console.log(`Total extra across all locales: ${totalExtra}`);
if (removeExtraKeys) {
console.log(`Files updated to remove extra keys: ${updatedFiles}`);
}
const keyCounts = {};
for (const missing of Object.values(allMissing)) {
for (const k of missing) {
keyCounts[k] = (keyCounts[k] || 0) + 1;
}
}
const sorted = Object.entries(keyCounts).sort((a, b) => b[1] - a[1]);
if (sorted.length) {
console.log('\nMost commonly missing keys:');
for (const [k, count] of sorted) {
console.log(` "${k}": missing from ${count} files (en value: "${enFlat[k]}")`);
}
}
const extraKeyCounts = {};
for (const extra of Object.values(allExtra)) {
for (const k of extra) {
extraKeyCounts[k] = (extraKeyCounts[k] || 0) + 1;
}
}
const sortedExtra = Object.entries(extraKeyCounts).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
if (sortedExtra.length) {
console.log('\nMost commonly extra keys:');
for (const [k, count] of sortedExtra) {
console.log(` "${k}": extra in ${count} files`);
}
}