96 lines
3.3 KiB
JavaScript
96 lines
3.3 KiB
JavaScript
#!/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.");
|