diff --git a/.eslintrc.cjs b/.eslintrc.cjs index f5d33038..547db19c 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -9,7 +9,7 @@ module.exports = { "plugin:@typescript-eslint/recommended", "plugin:solid/typescript", "plugin:import/typescript", - "plugin:import/recommended" + "plugin:import/recommended", ], overrides: [ { @@ -20,19 +20,25 @@ module.exports = { rules: { "@typescript-eslint/no-var-requires": "off" // Disable this specific rule for CJS files } - } + }, + { + files: ['src/i18n/**/translations.ts'], // Specify the path pattern for the files you want to apply the rule to + rules: { + "internal-rules/check-i18n-keys": "warn" + } + }, ], parser: "@typescript-eslint/parser", parserOptions: { tsconfigRootDir: "./", - project: "tsconfig.json", + //project: "tsconfig.json", ecmaVersion: "latest", sourceType: "module", ecmaFeatures: { jsx: true } }, - plugins: ["@typescript-eslint", "solid", "import"], + plugins: ["@typescript-eslint", "solid", "import", "internal-rules"], rules: { "@typescript-eslint/no-unused-vars": [ "warn", diff --git a/package.json b/package.json index 3f9510c8..dba51b2d 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "eslint-plugin-import": "2.27.5", "eslint-plugin-prettier": "4.2.1", "eslint-plugin-solid": "0.13.0", + "eslint-plugin-internal-rules": "file:tools/internal-rules", "postcss": "^8.4.31", "prettier": "^3.0.3", "prettier-plugin-tailwindcss": "^0.5.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14598a6b..227739b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,6 +120,9 @@ importers: eslint-plugin-import: specifier: 2.27.5 version: 2.27.5(@typescript-eslint/parser@6.9.1)(eslint-import-resolver-typescript@2.7.1)(eslint@8.52.0) + eslint-plugin-internal-rules: + specifier: file:tools/internal-rules + version: file:tools/internal-rules(eslint@8.52.0) eslint-plugin-prettier: specifier: 4.2.1 version: 4.2.1(eslint@8.52.0)(prettier@3.0.3) @@ -11565,6 +11568,11 @@ packages: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} dev: true + /requireindex@1.2.0: + resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==} + engines: {node: '>=0.10.5'} + dev: true + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -13934,3 +13942,15 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + + file:tools/internal-rules(eslint@8.52.0): + resolution: {directory: tools/internal-rules, type: directory} + id: file:tools/internal-rules + name: eslint-plugin-internal-rules + engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0} + peerDependencies: + eslint: '>=7' + dependencies: + eslint: 8.52.0 + requireindex: 1.2.0 + dev: true diff --git a/tools/internal-rules/.eslintrc.js b/tools/internal-rules/.eslintrc.js new file mode 100644 index 00000000..2f33f7a7 --- /dev/null +++ b/tools/internal-rules/.eslintrc.js @@ -0,0 +1,19 @@ +"use strict"; + +module.exports = { + root: true, + extends: [ + "eslint:recommended", + "plugin:eslint-plugin/recommended", + "plugin:node/recommended", + ], + env: { + node: true, + }, + overrides: [ + { + files: ["tests/**/*.js"], + env: { mocha: true }, + }, + ], +}; diff --git a/tools/internal-rules/lib/index.js b/tools/internal-rules/lib/index.js new file mode 100644 index 00000000..3bd8fc7c --- /dev/null +++ b/tools/internal-rules/lib/index.js @@ -0,0 +1,18 @@ +/** + * @fileoverview internal eslint rules + * @author + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const requireIndex = require("requireindex"); + +//------------------------------------------------------------------------------ +// Plugin Definition +//------------------------------------------------------------------------------ + +// import all rules in lib/rules +module.exports.rules = requireIndex(__dirname + "/rules"); diff --git a/tools/internal-rules/lib/rules/check-i18n-keys.js b/tools/internal-rules/lib/rules/check-i18n-keys.js new file mode 100644 index 00000000..75629b8a --- /dev/null +++ b/tools/internal-rules/lib/rules/check-i18n-keys.js @@ -0,0 +1,152 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const { parse } = require("@babel/parser"); + +module.exports = { + meta: { + name: "check-i18n-keys", + type: "suggestion", + docs: { + description: + "Ensure translation keys in other language files match the keys in the English translation file.", + category: "Best Practices", + recommended: true + }, + fixable: null, + schema: [] + }, + create: function (context) { + function extractKeysFromObjectExpression(node) { + const keys = []; + + function traverseProperties(properties) { + if (!properties) return; // Handle undefined or null properties + + for (const property of properties) { + if ( + property.type === "Property" && + property.value.type === "ObjectExpression" + ) { + traverseProperties(property.value.properties); + } else if ( + property.type === "Property" && + property.value.type === "ArrayExpression" + ) { + traverseArrayElements(property.value.elements); + } else if ( + property.type === "Property" && + property.key.type === "Identifier" + ) { + keys.push(property.key.name); + } + } + } + + function traverseArrayElements(elements) { + if (!elements) return; // Handle undefined or null elements + + for (const element of elements) { + if (element.type === "ObjectExpression") { + traverseProperties(element.properties); + } + } + } + + traverseProperties(node.properties); + + return keys; + } + + function extractKeysFromFile(filePath) { + const fileContent = fs.readFileSync(filePath, "utf8"); + const ast = parse(fileContent, { + sourceType: "module", // or 'script' depending on your file + plugins: ["typescript", "jsx"] + }); + const keys = []; + + const properties = ast.program.body[0].declaration.properties; + + function traverseProperties(properties) { + for (const property of properties) { + if ( + property.type === "ObjectProperty" && + property.value.type === "ObjectExpression" + ) { + traverseProperties(property.value.properties); + } else if ( + property.type === "ObjectProperty" && + property.value.type === "ArrayExpression" + ) { + traverseArrayElements(property.value.elements); + } else if ( + property.type === "ObjectProperty" && + property.key.type === "Identifier" + ) { + keys.push(property.key.name); + } + } + } + + function traverseArrayElements(elements) { + if (!elements) return; // Handle undefined or null elements + + for (const element of elements) { + if (element.type === "ObjectExpression") { + traverseProperties(element.properties); + } + } + } + + traverseProperties(properties); + + return keys; + } + + return { + Program(node) { + for (const statement of node.body) { + const relativePath = path.relative(process.cwd(), context.getFilename()) + const fallbackFilePath = path + .relative(process.cwd(), context.getFilename()) + .replace( + /\/i18n\/\w+\/translations\.ts$/, + "/i18n/en/translations.ts" + ); + + const keys = extractKeysFromObjectExpression( + statement.declaration + ); + + const enKeys = extractKeysFromFile(fallbackFilePath); + // Report missing keys and incorrect order + enKeys.forEach((enKey, index) => { + if (!keys.includes(enKey)) { + context.report({ + node: node, + message: `missing key '${enKey}'` + }); + } else if (keys.indexOf(enKey) !== index) { + context.report({ + node: node, + message: `incorrect key location '${enKey}'` + }); + } + }); + + // Report extra keys + keys.forEach(key => { + if (!enKeys.includes(key)) { + context.report({ + node: node, + message: `extra key '${key}'` + }); + } + }); + } + } + }; + } +}; diff --git a/tools/internal-rules/package.json b/tools/internal-rules/package.json new file mode 100644 index 00000000..c2dd173f --- /dev/null +++ b/tools/internal-rules/package.json @@ -0,0 +1,38 @@ +{ + "name": "eslint-plugin-internal-rules", + "version": "0.0.0", + "description": "internal eslint rules", + "keywords": [ + "eslint", + "eslintplugin", + "eslint-plugin" + ], + "author": "", + "main": "./lib/index.js", + "exports": "./lib/index.js", + "scripts": { + "lint": "npm-run-all \"lint:*\"", + "lint:eslint-docs": "npm-run-all \"update:eslint-docs -- --check\"", + "lint:js": "eslint .", + "test": "mocha tests --recursive", + "update:eslint-docs": "eslint-doc-generator" + }, + "dependencies": { + "requireindex": "^1.2.0" + }, + "devDependencies": { + "eslint": "^8.19.0", + "eslint-doc-generator": "^1.0.0", + "eslint-plugin-eslint-plugin": "^5.0.0", + "eslint-plugin-node": "^11.1.0", + "mocha": "^10.0.0", + "npm-run-all": "^4.1.5" + }, + "engines": { + "node": "^14.17.0 || ^16.0.0 || >= 18.0.0" + }, + "peerDependencies": { + "eslint": ">=7" + }, + "license": "ISC" +}