From 399df8e302ae6d96d435e367f3221a1568ca9597 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sat, 24 Sep 2022 11:48:46 +0300 Subject: [PATCH] Add check-translation-jsons script --- README.md | 1 + package.json | 1 + scripts/check-translation-jsons.ts | 195 +++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 scripts/check-translation-jsons.ts diff --git a/README.md b/README.md index 156bd08cf..158ee8e0d 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Things to note: - Some translated `.json` files can also have some lines in English as new lines get added to the site. Those can then be translated. - Some lines have a dynamic part like this one: `"articleBy": "by {{author}}"` in this case `{{author}}` should appear in the translated version unchanged. So in other words don't translate the part inside `{{}}`. - There is one more special syntax to keep in mind. When you translate this line `"project": "Sendou.ink is a project by <2>Sendou with help from contributors:",` the `<2>` should appear in the translated version. The text inside these tags can change. +- To update a translation file copy the existing file, do any modifications needed and send the updated one. Any questions please ask Sendou! diff --git a/package.json b/package.json index 8220d7f31..d4ab2c928 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "create-weapon-json": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/create-weapon-json.ts", "create-gear-json": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/create-gear-json.ts", "create-analyzer-json": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/create-analyzer-json.ts", + "check-translation-jsons": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/check-translation-jsons.ts", "replace-img-names": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/replace-img-names.ts", "lint:ts": "eslint . --ext .ts,.tsx", "lint:styles": "stylelint \"app/styles/**/*.css\"", diff --git a/scripts/check-translation-jsons.ts b/scripts/check-translation-jsons.ts new file mode 100644 index 000000000..559b68dee --- /dev/null +++ b/scripts/check-translation-jsons.ts @@ -0,0 +1,195 @@ +// 1) create status.json + +import fs from "fs"; +import path from "path"; + +const otherLanguageTranslationPath = (code?: string, fileName?: string) => + path.join( + ...[__dirname, "..", "public", "locales", code, fileName].filter( + (val): val is string => !!val + ) + ); + +const allOtherLanguages = fs + .readdirSync(otherLanguageTranslationPath()) + .filter((lang) => lang !== "en"); + +const missingTranslations: Record< + string, + Record> +> = Object.fromEntries(allOtherLanguages.map((lang) => [lang, {}])); + +const totalTranslationCounts: Record = {}; + +for (const file of fs.readdirSync(otherLanguageTranslationPath("en"))) { + const englishContent = JSON.parse( + fs.readFileSync(otherLanguageTranslationPath("en", file), "utf8").trim() + ) as Record; + const key = file.replace(".json", ""); + + if (file !== "gear.json" && file !== "weapons.json") { + totalTranslationCounts[key] = Object.keys(englishContent).length; + } + + for (const lang of allOtherLanguages) { + try { + const otherLanguageContent = JSON.parse( + fs.readFileSync(otherLanguageTranslationPath(lang, file), "utf8").trim() + ) as Record; + + validateNoExtraKeysInOther({ + english: englishContent, + other: otherLanguageContent, + lang, + file, + }); + validateVariables({ + english: englishContent, + other: otherLanguageContent, + lang, + file, + }); + + const missingKeys = Object.keys(englishContent).filter( + (key) => !Object.keys(otherLanguageContent).includes(key) + ); + + if (key === "weapons" || key === "gear") { + if (missingKeys.length > 0) { + console.error(`missing keys in ${lang}/${file}`); + } + } else { + missingTranslations[lang]![key] = missingKeys; + } + } catch (e) { + if ((e as { code: string }).code !== "ENOENT") throw e; + + missingTranslations[lang]![key] = Object.keys(englishContent); + } + } +} + +const markdown = createTranslationProgessMarkdown({ + missingTranslations, + totalTranslationCounts, +}); + +fs.writeFileSync( + path.join(__dirname, "..", "translation-progress.md"), + markdown +); + +function validateNoExtraKeysInOther({ + english, + other, + lang, + file, +}: { + english: Record; + other: Record; + lang: string; + file: string; +}) { + const validKeys = Object.keys(english); + + for (const key of Object.keys(other)) { + if (validKeys.includes(key)) continue; + + console.error(`unknown key in ${lang}/${file}: ${key}`); + } +} + +function validateVariables({ + english, + other, + lang, + file, +}: { + english: Record; + other: Record; + lang: string; + file: string; +}) { + for (const [key, value] of Object.entries(english)) { + const otherValue = other[key]; + if (!otherValue) continue; + + // find all matches inside {{}} + const englishMatches = value.match(/{{(.*?)}}/g); + const otherMatches = otherValue.match(/{{(.*?)}}/g); + + if (!englishMatches && !otherMatches) continue; + + for (const englishVar of englishMatches ?? []) { + if (!otherMatches?.includes(englishVar)) { + console.error( + `variable mismatch in ${lang}/${file}: ${englishVar} is missing in ${otherValue}` + ); + } + } + } +} + +function createTranslationProgessMarkdown({ + missingTranslations, + totalTranslationCounts, +}: { + missingTranslations: Record>>; + totalTranslationCounts: Record; +}) { + const totalKeysCount = Object.values(totalTranslationCounts).reduce( + (a, b) => a + b, + 0 + ); + + return `# Translation Progress +${Object.entries(missingTranslations) + .map(([lang, missingKeysObj]) => { + const totalAmountOfMissingKeys = Object.values(missingKeysObj).reduce( + (a, b) => a + b.length, + 0 + ); + const status = + totalAmountOfMissingKeys === 0 + ? "🟢 Done" + : totalAmountOfMissingKeys === totalKeysCount + ? "🔴 Not started" + : "🟡 In progress"; + + const headers = () => { + if (status !== "🟡 In progress") return ""; + + return Object.entries(missingKeysObj) + .map(([file, missingKeys]) => { + const statusDot = + missingKeys.length === 0 + ? "🟢" + : missingKeys.length === totalTranslationCounts[file]! + ? "🔴" + : "🟡"; + + return `### ${statusDot} ${file}.json +**${ + totalTranslationCounts[file]! - missingKeys.length + }/${totalTranslationCounts[file]!}** +${ + missingKeys.length === 0 || + missingKeys.length === totalTranslationCounts[file]! + ? "" + : `
+Missing + +${missingKeys.map((key) => `- ${key}`).join("\n")} + +
` +}`; + }) + .join("\n\n"); + }; + + return `## /${lang} (${status}) + +${headers()}`; + }) + .join("\n\n---\n\n")}`; +}