From 0738f0760cc38b446a5b9bf083d29acc6a07a5b7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Feb 2024 13:20:56 +0000 Subject: [PATCH 1/6] Fix linting nested translations --- scripts/lint-i18n.ts | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/scripts/lint-i18n.ts b/scripts/lint-i18n.ts index 50fa5ae..8dc4c98 100644 --- a/scripts/lint-i18n.ts +++ b/scripts/lint-i18n.ts @@ -23,26 +23,49 @@ limitations under the License. */ import { getTranslations, isPluralisedTranslation } from "./common"; +import { KEY_SEPARATOR, Translation, Translations } from "../src"; const input = getTranslations(); const filtered = Object.keys(input).filter(key => { const value = input[key]; +function lintTranslation(keys: string[], value: Translation): boolean { + const key = keys[keys.length - 1]; + const printableKey = keys.join(KEY_SEPARATOR); // Check for invalid characters in the translation key if (!!key.replace(/[a-z0-9_]+/g, "")) { - console.log(`"${key}": key contains invalid characters`); + console.log(`"${printableKey}": key contains invalid characters`); return true; } // Check that the translated string does not match the key. if (key === input[key] || (isPluralisedTranslation(value) && (key === value.other || key === value.one))) { - console.log(`"${key}": key matches value`); + console.log(`"${printableKey}": key matches value`); return true; } return false; -}); +} + +function traverseTranslations(translations: Translations, keys: string[] = []): string[] { + const filtered: string[] = []; + Object.keys(translations).forEach(key => { + const value = translations[key]; + + if (typeof value === "object" && !isPluralisedTranslation(value)) { + filtered.push(...traverseTranslations(value, [...keys, key])); + return; + } + + if (lintTranslation([...keys, key], value)) { + filtered.push(key); + } + }); + return filtered; +} + +const filtered = traverseTranslations(input); if (filtered.length > 0) { console.log(`${filtered.length} invalid translation keys`); From 6d72198f7ea63f154cd9b2810eefed6e8e53495b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Feb 2024 13:21:57 +0000 Subject: [PATCH 2/6] Loosen key validity policy to not fail existing translations --- scripts/lint-i18n.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/lint-i18n.ts b/scripts/lint-i18n.ts index 8dc4c98..1e3877c 100644 --- a/scripts/lint-i18n.ts +++ b/scripts/lint-i18n.ts @@ -34,7 +34,7 @@ function lintTranslation(keys: string[], value: Translation): boolean { const printableKey = keys.join(KEY_SEPARATOR); // Check for invalid characters in the translation key - if (!!key.replace(/[a-z0-9_]+/g, "")) { + if (!!key.replace(/[a-z0-9@_.]+/gi, "")) { console.log(`"${printableKey}": key contains invalid characters`); return true; } From e78002311131814e9f269c5169144157f919b109 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Feb 2024 13:22:27 +0000 Subject: [PATCH 3/6] Add ability to lint against hardcoded strings --- .github/workflows/i18n_check.yml | 14 +++++++++++++- scripts/lint-i18n.ts | 27 +++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/.github/workflows/i18n_check.yml b/.github/workflows/i18n_check.yml index e02a094..2700d5c 100644 --- a/.github/workflows/i18n_check.yml +++ b/.github/workflows/i18n_check.yml @@ -2,7 +2,16 @@ # Additionally prevents changes to files other than en_EN.json apart from RiotRobot automations name: i18n Check on: - workflow_call: {} + workflow_call: + inputs: + hardcoded-words: + type: string + required: false + description: "Hardcoded words to disallow, e.g. 'Element'." + allowed-hardcoded-keys: + type: string + required: false + description: "i18n keys which ignore the forbidden word check." jobs: check: runs-on: ubuntu-latest @@ -37,5 +46,8 @@ jobs: run: "yarn install --frozen-lockfile" - run: yarn run i18n + env: + HARDCODED_WORDS: ${{ inputs.hardcoded-words }} + ALLOWED_HARDCODED_KEYS: ${{ inputs.allowed-hardcoded-keys }} - run: git diff --exit-code diff --git a/scripts/lint-i18n.ts b/scripts/lint-i18n.ts index 1e3877c..1be0bc8 100644 --- a/scripts/lint-i18n.ts +++ b/scripts/lint-i18n.ts @@ -18,6 +18,8 @@ limitations under the License. * Applies the following lint rules to the src/i18n/strings/en_EN.json file: * + ensures the translation key is not equal to its value * + ensures the translation key contains only alphanumerics and underscores + * + ensures no forbidden hardcoded words are found (specified comma separated in environment variable HARDCODED_WORDS) + * unless they are explicitly allowed (keys specified comma separated in environment variable ALLOWED_HARDCODED_KEYS) * * Usage: node scripts/lint-i18n.js */ @@ -25,10 +27,23 @@ limitations under the License. import { getTranslations, isPluralisedTranslation } from "./common"; import { KEY_SEPARATOR, Translation, Translations } from "../src"; +const hardcodedWords = process.env.HARDCODED_WORDS?.toLowerCase().split(",") ?? []; +const allowedHardcodedKeys = process.env.ALLOWED_HARDCODED_KEYS?.split(",") ?? []; + const input = getTranslations(); -const filtered = Object.keys(input).filter(key => { - const value = input[key]; +function nonNullable(value: T): value is NonNullable { + return value !== null && value !== undefined; +} + +function expandTranslations(translation: Translation): string[] { + if (isPluralisedTranslation(translation)) { + return [translation.one, translation.other].filter(nonNullable) + } else { + return [translation]; + } +} + function lintTranslation(keys: string[], value: Translation): boolean { const key = keys[keys.length - 1]; const printableKey = keys.join(KEY_SEPARATOR); @@ -45,6 +60,14 @@ function lintTranslation(keys: string[], value: Translation): boolean { return true; } + if (hardcodedWords.length > 0) { + const words = expandTranslations(value).join(" ").toLowerCase().split(" "); + if (!allowedHardcodedKeys.includes(key) && hardcodedWords.some(word => words.includes(word))) { + console.log(`"${printableKey}": contains forbidden hardcoded word`); + return true; + } + } + return false; } From 5841e6980ce621929e3d8415eb854844e8195d3c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Feb 2024 13:25:12 +0000 Subject: [PATCH 4/6] Fix hardcoded key check --- scripts/lint-i18n.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/lint-i18n.ts b/scripts/lint-i18n.ts index 1be0bc8..2daa977 100644 --- a/scripts/lint-i18n.ts +++ b/scripts/lint-i18n.ts @@ -46,24 +46,24 @@ function expandTranslations(translation: Translation): string[] { function lintTranslation(keys: string[], value: Translation): boolean { const key = keys[keys.length - 1]; - const printableKey = keys.join(KEY_SEPARATOR); + const fullKey = keys.join(KEY_SEPARATOR); // Check for invalid characters in the translation key if (!!key.replace(/[a-z0-9@_.]+/gi, "")) { - console.log(`"${printableKey}": key contains invalid characters`); + console.log(`"${fullKey}": key contains invalid characters`); return true; } // Check that the translated string does not match the key. if (key === input[key] || (isPluralisedTranslation(value) && (key === value.other || key === value.one))) { - console.log(`"${printableKey}": key matches value`); + console.log(`"${fullKey}": key matches value`); return true; } if (hardcodedWords.length > 0) { const words = expandTranslations(value).join(" ").toLowerCase().split(" "); - if (!allowedHardcodedKeys.includes(key) && hardcodedWords.some(word => words.includes(word))) { - console.log(`"${printableKey}": contains forbidden hardcoded word`); + if (!allowedHardcodedKeys.includes(fullKey) && hardcodedWords.some(word => words.includes(word))) { + console.log(`"${fullKey}": contains forbidden hardcoded word`); return true; } } From 78804fc4c595bc8134c82493d1814bc68db15cbe Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Feb 2024 13:26:50 +0000 Subject: [PATCH 5/6] Tweak env var usage --- scripts/lint-i18n.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/lint-i18n.ts b/scripts/lint-i18n.ts index 2daa977..ab355c9 100644 --- a/scripts/lint-i18n.ts +++ b/scripts/lint-i18n.ts @@ -18,8 +18,8 @@ limitations under the License. * Applies the following lint rules to the src/i18n/strings/en_EN.json file: * + ensures the translation key is not equal to its value * + ensures the translation key contains only alphanumerics and underscores - * + ensures no forbidden hardcoded words are found (specified comma separated in environment variable HARDCODED_WORDS) - * unless they are explicitly allowed (keys specified comma separated in environment variable ALLOWED_HARDCODED_KEYS) + * + ensures no forbidden hardcoded words are found (specified new line delimited in environment variable HARDCODED_WORDS) + * unless they are explicitly allowed (keys specified new line delimited in environment variable ALLOWED_HARDCODED_KEYS) * * Usage: node scripts/lint-i18n.js */ @@ -27,8 +27,8 @@ limitations under the License. import { getTranslations, isPluralisedTranslation } from "./common"; import { KEY_SEPARATOR, Translation, Translations } from "../src"; -const hardcodedWords = process.env.HARDCODED_WORDS?.toLowerCase().split(",") ?? []; -const allowedHardcodedKeys = process.env.ALLOWED_HARDCODED_KEYS?.split(",") ?? []; +const hardcodedWords = process.env.HARDCODED_WORDS?.toLowerCase().split("\n").map(k => k.trim()) ?? []; +const allowedHardcodedKeys = process.env.ALLOWED_HARDCODED_KEYS?.split("\n").map(k => k.trim()) ?? []; const input = getTranslations(); From 1d2c4e65eb831d1c39562aa4d38a55d12fdb3310 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Feb 2024 14:35:19 +0000 Subject: [PATCH 6/6] Improve comment --- scripts/lint-i18n.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/lint-i18n.ts b/scripts/lint-i18n.ts index ab355c9..49f8766 100644 --- a/scripts/lint-i18n.ts +++ b/scripts/lint-i18n.ts @@ -17,7 +17,7 @@ limitations under the License. /** * Applies the following lint rules to the src/i18n/strings/en_EN.json file: * + ensures the translation key is not equal to its value - * + ensures the translation key contains only alphanumerics and underscores + * + ensures the translation key contains only alphanumerics and underscores (temporarily allows @ and . for compatibility) * + ensures no forbidden hardcoded words are found (specified new line delimited in environment variable HARDCODED_WORDS) * unless they are explicitly allowed (keys specified new line delimited in environment variable ALLOWED_HARDCODED_KEYS) *