Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module.exports = {
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'validate-translation-keys': 'warn',
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
Expand Down
75 changes: 75 additions & 0 deletions eslint-rules/validate-translation-keys.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
const fs = require('fs');
const path = require('path');

function loadKeys(filePath) {
try {
const data = fs.readFileSync(filePath, 'utf8');
const json = JSON.parse(data);
const keys = new Set();

function recurse(obj, prefix = '') {
for (const [k, v] of Object.entries(obj)) {
const full = prefix ? `${prefix}.${k}` : k;
if (v && typeof v === 'object') {
recurse(v, full);
} else {
keys.add(full);
}
}
}
recurse(json);
return keys;
} catch (error) {
console.warn(`Could not load translation file: ${filePath}`);
return new Set();
}
}

const langs = {
German: loadKeys(path.join(process.cwd(), 'src/translations/languages/de.json')),
French: loadKeys(path.join(process.cwd(), 'src/translations/languages/fr.json')),
Italian: loadKeys(path.join(process.cwd(), 'src/translations/languages/it.json')),
};

module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Ensure translation keys exist in DE/FR/IT',
category: 'Possible Errors',
},
schema: [],
messages: {
missingTranslation: 'Translation key "{{key}}" missing in: {{languages}}',
invalidKey: 'Translation key "{{key}}" not found in any translation file',
},
},

create(context) {
return {
CallExpression(node) {
if (
node.callee.name === 'translate' &&
node.arguments.length >= 2 &&
node.arguments[0].type === 'Literal' &&
node.arguments[1].type === 'Literal'
) {
const section = node.arguments[0].value;
const key = node.arguments[1].value;
const fullKey = `${section}.${key}`;

const missing = Object.entries(langs)
.filter(([, set]) => !set.has(fullKey))
.map(([name]) => name);

const reportData = { key: fullKey, languages: missing.join(', ') };
if (missing.length === 3) {
context.report({ node: node.arguments[1], messageId: 'invalidKey', data: reportData });
} else if (missing.length > 0) {
context.report({ node: node.arguments[1], messageId: 'missingTranslation', data: reportData });
}
}
},
};
},
};
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@
"widget": "cp src/index.tsx src/index.bak.tsx && cp src/index-widget.tsx src/index.tsx && env-cmd -f .env.prd env-cmd -f .env.widget react-app-rewired build && mv src/index.bak.tsx src/index.tsx",
"widget:dev": "cp src/index.tsx src/index.bak.tsx && cp src/index-widget.tsx src/index.tsx && env-cmd -f .env.dev env-cmd -f .env.widget react-app-rewired build && mv src/index.bak.tsx src/index.tsx",
"widget:loc": "cp src/index.tsx src/index.bak.tsx && cp src/index-widget.tsx src/index.tsx && env-cmd -f .env.loc env-cmd -f .env.widget react-app-rewired build && mv src/index.bak.tsx src/index.tsx",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --no-fix",
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"lint": "eslint \"{src,apps,libs,test}/**/*.{ts,tsx}\" --rulesdir eslint-rules --no-fix",
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.{ts,tsx}\" --rulesdir eslint-rules --fix",
"test": "react-app-rewired test --watchAll=false --passWithNoTests",
"eject": "react-scripts eject",
"serve": "serve build -l 4000",
Expand Down
Loading