Skip to content

Commit

Permalink
Merge pull request #16 from matrix-org/t3chguy/i18n-lint-element
Browse files Browse the repository at this point in the history
  • Loading branch information
t3chguy committed Feb 29, 2024
2 parents 0d67a8c + 1d2c4e6 commit 868b141
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 8 deletions.
14 changes: 13 additions & 1 deletion .github/workflows/i18n_check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
60 changes: 53 additions & 7 deletions scripts/lint-i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,78 @@ 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)
*
* Usage: node scripts/lint-i18n.js
*/

import { getTranslations, isPluralisedTranslation } from "./common";
import { KEY_SEPARATOR, Translation, Translations } from "../src";

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();

const filtered = Object.keys(input).filter(key => {
const value = input[key];
function nonNullable<T>(value: T): value is NonNullable<T> {
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 fullKey = 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`);
if (!!key.replace(/[a-z0-9@_.]+/gi, "")) {
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(`"${key}": 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(fullKey) && hardcodedWords.some(word => words.includes(word))) {
console.log(`"${fullKey}": contains forbidden hardcoded word`);
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`);
Expand Down

0 comments on commit 868b141

Please sign in to comment.