From 21bb538428dc844509af8d134601d7873b9d77c5 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Mon, 12 Jan 2026 11:10:26 +0100 Subject: [PATCH] add a test case so that a substantative placeholder changes are apparent to our contributors --- test/backend-test/check-translations.test.js | 74 +++++++++++++++++--- 1 file changed, 66 insertions(+), 8 deletions(-) diff --git a/test/backend-test/check-translations.test.js b/test/backend-test/check-translations.test.js index f5cf9593e..6e8f88166 100644 --- a/test/backend-test/check-translations.test.js +++ b/test/backend-test/check-translations.test.js @@ -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} A generator that yields file paths. + * @returns {AsyncGenerator} 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,30 @@ 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} 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 +59,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 +74,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 +136,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( + upstreamParams, + localParams, + [ + `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") + ); + } + }); });