chore: add a test case so that a substantative placeholder changes are appant to contributors (#6681)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Frank Elsinga
2026-01-12 11:37:09 +01:00
committed by GitHub
parent 0981fee9b2
commit e9b7ac82b7

View File

@@ -1,16 +1,16 @@
const { describe, it } = require("node:test");
const assert = require("node:assert");
const fs = require("fs");
const fs = require("fs/promises");
const path = require("path");
/**
* Recursively walks a directory and yields file paths.
* @param {string} dir The directory to walk.
* @yields {string} The path to a file.
* @returns {Generator<string>} A generator that yields file paths.
* @returns {AsyncGenerator<string>} A generator that yields file paths.
*/
function* walk(dir) {
const files = fs.readdirSync(dir, { withFileTypes: true });
async function* walk(dir) {
const files = await fs.readdir(dir, { withFileTypes: true });
for (const file of files) {
if (file.isDirectory()) {
yield* walk(path.join(dir, file.name));
@@ -20,6 +20,29 @@ function* walk(dir) {
}
}
const UPSTREAM_EN_JSON = "https://raw.githubusercontent.com/louislam/uptime-kuma/refs/heads/master/src/lang/en.json";
/**
* Extract `{placeholders}` from a translation string.
* @param {string} value The translation string to extract placeholders from.
* @returns {Set<string>} A set of placeholder names.
*/
function extractParams(value) {
if (typeof value !== "string") {
return new Set();
}
const regex = /\{([^}]+)\}/g;
const params = new Set();
let match;
while ((match = regex.exec(value)) !== null) {
params.add(match[1]);
}
return params;
}
/**
* Fallback to get start/end indices of a key within a line.
* @param {string} line - Line of text to search in.
@@ -35,8 +58,8 @@ function getStartEnd(line, key) {
}
describe("Check Translations", () => {
it("should not have missing translation keys", () => {
const enTranslations = JSON.parse(fs.readFileSync("src/lang/en.json", "utf-8"));
it("should not have missing translation keys", async () => {
const enTranslations = JSON.parse(await fs.readFile("src/lang/en.json", "utf-8"));
// this is a resonably crude check, you can get around this trivially
/// this check is just to save on maintainer energy to explain this on every review ^^
@@ -50,9 +73,9 @@ describe("Check Translations", () => {
const roots = ["src", "server"];
for (const root of roots) {
for (const filePath of walk(root)) {
for await (const filePath of walk(root)) {
if (filePath.endsWith(".vue") || filePath.endsWith(".js")) {
const lines = fs.readFileSync(filePath, "utf-8").split("\n");
const lines = (await fs.readFile(filePath, "utf-8")).split("\n");
lines.forEach((line, lineNum) => {
let match;
// front-end style keys ($t / i18n-t)
@@ -112,4 +135,38 @@ describe("Check Translations", () => {
assert.fail(report);
}
});
it("en.json translations must not change placeholder parameters", async () => {
// Load local reference (the one translators are synced against)
const enTranslations = JSON.parse(await fs.readFile("src/lang/en.json", "utf-8"));
// Fetch upstream version
const res = await fetch(UPSTREAM_EN_JSON);
assert.equal(res.ok, true, "Failed to fetch upstream en.json");
const upstreamEn = await res.json();
for (const [key, upstreamValue] of Object.entries(upstreamEn)) {
if (!(key in enTranslations)) {
// deleted keys are fine
continue;
}
const localParams = extractParams(enTranslations[key]);
const upstreamParams = extractParams(upstreamValue);
assert.deepEqual(
localParams,
upstreamParams,
[
`Translation key "${key}" changed placeholder parameters.`,
`This is a breaking change for existing translations.`,
`Please rename the translation key instead of changing placeholders.`,
``,
`your version: ${[...localParams].join(", ")}`,
`on master: ${[...upstreamParams].join(", ")}`,
].join("\n")
);
}
});
});