diff --git a/.eslintrc.js b/.eslintrc.js index 848ffd3e3..c946b7f16 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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', diff --git a/eslint-rules/validate-translation-keys.js b/eslint-rules/validate-translation-keys.js new file mode 100644 index 000000000..782613d28 --- /dev/null +++ b/eslint-rules/validate-translation-keys.js @@ -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 }); + } + } + }, + }; + }, +}; diff --git a/package.json b/package.json index d1c57f563..9b6270781 100644 --- a/package.json +++ b/package.json @@ -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",