From e502100cd965c5e4004f5608594aa55012cc7b50 Mon Sep 17 00:00:00 2001 From: Alvin Unreal Date: Thu, 11 Jun 2026 21:29:11 +0200 Subject: [PATCH] sync locales --- package.json | 3 +- scripts/check-plugin-locales.mjs | 96 ++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 scripts/check-plugin-locales.mjs diff --git a/package.json b/package.json index 6254f59e..3eaad96f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/check-plugin-locales.mjs b/scripts/check-plugin-locales.mjs new file mode 100644 index 00000000..98e969d4 --- /dev/null +++ b/scripts/check-plugin-locales.mjs @@ -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.");