Skip to content

Commit

Permalink
add translation key ci check
Browse files Browse the repository at this point in the history
  • Loading branch information
benalleng committed Feb 19, 2024
1 parent 9db53cc commit 98f7d3a
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 4 deletions.
14 changes: 10 additions & 4 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module.exports = {
"plugin:@typescript-eslint/recommended",
"plugin:solid/typescript",
"plugin:import/typescript",
"plugin:import/recommended"
"plugin:import/recommended",
],
overrides: [
{
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 20 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions tools/internal-rules/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -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 },
},
],
};
18 changes: 18 additions & 0 deletions tools/internal-rules/lib/index.js
Original file line number Diff line number Diff line change
@@ -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");
152 changes: 152 additions & 0 deletions tools/internal-rules/lib/rules/check-i18n-keys.js
Original file line number Diff line number Diff line change
@@ -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}'`
});
}
});
}
}
};
}
};
38 changes: 38 additions & 0 deletions tools/internal-rules/package.json
Original file line number Diff line number Diff line change
@@ -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"
}

0 comments on commit 98f7d3a

Please sign in to comment.