sync locales

This commit is contained in:
Alvin Unreal 2026-06-11 21:29:11 +02:00
parent b1b1f08306
commit e502100cd9
2 changed files with 98 additions and 1 deletions

View file

@ -21,7 +21,8 @@
"package:desktop": "pnpm build && pnpm --filter @open-pets/desktop package",
"release:desktop": "node apps/desktop/scripts/release-local.mjs",
"release:npm": "node scripts/release-npm.mjs",
"plugins:test": "node scripts/test-plugins.mjs",
"plugins:locales": "node scripts/check-plugin-locales.mjs",
"plugins:test": "pnpm plugins:locales && node scripts/test-plugins.mjs",
"plugins:check": "node web/scripts/sync-plugins.js --dry-run --skip-r2",
"plugins:package": "node web/scripts/sync-plugins.js --skip-r2",
"plugins:publish": "node web/scripts/sync-plugins.js",

View file

@ -0,0 +1,96 @@
#!/usr/bin/env node
import { readdir, readFile } from "node:fs/promises";
import { join, relative } from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = fileURLToPath(new URL("..", import.meta.url));
const officialDir = join(repoRoot, "plugins", "official");
const errors = [];
async function pathExists(path) {
try {
await readdir(path);
return true;
} catch (error) {
if (error.code === "ENOENT") return false;
throw error;
}
}
async function readLocaleJson(path) {
try {
const parsed = JSON.parse(await readFile(path, "utf8"));
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
errors.push(`${relative(repoRoot, path)} must be a JSON object.`);
return {};
}
return flattenLocale(parsed);
} catch (error) {
errors.push(`${relative(repoRoot, path)} is not valid JSON: ${error.message}`);
return {};
}
}
function flattenLocale(value, prefix = "") {
const out = {};
for (const [key, entry] of Object.entries(value)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
Object.assign(out, flattenLocale(entry, fullKey));
} else if (typeof entry === "string") {
out[fullKey] = entry;
} else {
errors.push(`Locale key ${fullKey} must be a string.`);
}
}
return out;
}
function compareKeys(pluginName, localeName, englishKeys, localeKeys) {
const localeKeySet = new Set(localeKeys);
const englishKeySet = new Set(englishKeys);
const missing = englishKeys.filter((key) => !localeKeySet.has(key));
const extra = localeKeys.filter((key) => !englishKeySet.has(key));
for (const key of missing) errors.push(`${pluginName}/${localeName} is missing key: ${key}`);
for (const key of extra) errors.push(`${pluginName}/${localeName} has extra key not in en: ${key}`);
}
async function checkPlugin(plugin) {
const localesDir = join(officialDir, plugin.name, "locales");
if (!(await pathExists(localesDir))) return;
const entries = (await readdir(localesDir, { withFileTypes: true }))
.filter((entry) => entry.isFile() && entry.name.endsWith(".json") && !entry.name.startsWith("."))
.sort((a, b) => a.name.localeCompare(b.name));
if (entries.length === 0) return;
if (!entries.some((entry) => entry.name === "en.json")) {
errors.push(`${plugin.name}/locales has locale files but no en.json source locale.`);
return;
}
const english = await readLocaleJson(join(localesDir, "en.json"));
const englishKeys = Object.keys(english).sort();
for (const entry of entries) {
if (entry.name === "en.json") continue;
const locale = await readLocaleJson(join(localesDir, entry.name));
compareKeys(plugin.name, entry.name, englishKeys, Object.keys(locale).sort());
}
}
if (await pathExists(officialDir)) {
const plugins = (await readdir(officialDir, { withFileTypes: true }))
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
.sort((a, b) => a.name.localeCompare(b.name));
for (const plugin of plugins) await checkPlugin(plugin);
}
if (errors.length > 0) {
console.error(`Plugin locale key check failed with ${errors.length} issue${errors.length === 1 ? "" : "s"}:`);
for (const error of errors) console.error(`- ${error}`);
process.exit(1);
}
console.log("Plugin locale key check passed.");