Skip to content

Commit

Permalink
Add rule
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathan-waarneming-nl committed Oct 11, 2024
1 parent 2e69a03 commit b78f3ad
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import noFunctionWithoutLogging from "./rules/no-function-without-logging"
import noMissingTranslations from "./rules/no-missing-translations"

const rules = {
"no-function-without-logging": noFunctionWithoutLogging,
'no-missing-translations': noMissingTranslations,
}

export { rules }
82 changes: 82 additions & 0 deletions src/rules/__tests__/no-missing-translations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { ESLintUtils } from "@typescript-eslint/utils"
import noMissingTranslations from "../no-missing-translations"
import { jest } from "@jest/globals"

const ruleTester = new ESLintUtils.RuleTester({
parser: "@typescript-eslint/parser",
})
jest.mock("fs", () => {
const actualFs = jest.requireActual<typeof import("fs")>("fs")
const newFs = {
...actualFs,
readFileSync: jest.fn((file: string) => {
if (file === "en.json") {
return JSON.stringify({
"Existing key": "Existing value",
"Key that only exists in en.json": "Value",
})
}
if (file === "nl.json") {
return JSON.stringify({
"Existing key": "Existing value",
})
}
}),
}
return {
__esModule: true,
...newFs,
}
})

ruleTester.run("no-missing-translations", noMissingTranslations, {
valid: [
{
name: "Function declaration",
code: "i18n.t('Existing key')",
options: [
{
translationFiles: ["en.json", "nl.json"],
},
],
},
],
invalid: [
{
name: "Missing translation key in multiple files",
code: 'i18n.t("Missing key")',
errors: [
{
messageId: "missingTranslationKey",
data: {
translationKey: "Missing key",
invalidFiles: "'en.json', 'nl.json'",
},
},
],
options: [
{
translationFiles: ["en.json", "nl.json"],
},
],
},
{
name: "Missing translation key in one file",
code: 'i18n.t("Key that only exists in en.json")',
errors: [
{
messageId: "missingTranslationKey",
data: {
translationKey: "Key that only exists in en.json",
invalidFiles: "'nl.json'",
},
},
],
options: [
{
translationFiles: ["en.json", "nl.json"],
},
],
},
],
})
107 changes: 107 additions & 0 deletions src/rules/no-missing-translations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { readFileSync } from "fs"
import { RuleContext } from "@typescript-eslint/utils/dist/ts-eslint"
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils"
import { isIdentifier, isLiteral, isMemberExpression } from "../utils"

const createRule = ESLintUtils.RuleCreator(
() => "https://github.com/observation/eslint-rules"
)

type MessageIds = "missingTranslationKey";
type Options = [
{
translationFiles: string[];
}
];

const checkTranslationFileForKey = (
translationFile: string,
translationKey: string
) => {
const fileContent = readFileSync(translationFile, "utf8")
const jsonData = JSON.parse(fileContent)
return !(translationKey in jsonData)
}

const checkCallExpression = (
context: Readonly<RuleContext<MessageIds, unknown[]>>,
node: TSESTree.CallExpression,
translationFiles: string[]
) => {
if (isMemberExpression(node.callee)) {
const { object, property } = node.callee

if (isIdentifier(object) && isIdentifier(property)) {
const [argument] = node.arguments
if (
object.name === "i18n" &&
property.name === "t" &&
isLiteral(argument)
) {
const translationKey = argument.value

if (typeof translationKey === "string") {
const invalidTranslationFiles = translationFiles.filter(
(translationFile) =>
checkTranslationFileForKey(translationFile, translationKey)
)

if (invalidTranslationFiles.length > 0) {
context.report({
node,
messageId: "missingTranslationKey",
data: {
translationKey,
invalidFiles: invalidTranslationFiles
.map((file) => `'${file}'`)
.join(", "),
},
})
}
}
}
}
}
}

const noMissingTranslations = createRule<Options, MessageIds>({
create(context, options) {
const [{ translationFiles }] = options
return {
CallExpression: (node) =>
checkCallExpression(context, node, translationFiles),
}
},
name: "no-missing-translations",
meta: {
docs: {
description:
"All translation keys used in the codebase should have a corresponding translation in the translation files",
recommended: "error",
},
messages: {
missingTranslationKey:
"Translation key '{{ translationKey }}' is missing in: {{ invalidFiles }}",
},
type: "problem",
schema: [
{
type: "object",
properties: {
translationFiles: {
type: "array",
items: { type: "string" },
},
},
additionalProperties: false,
},
],
},
defaultOptions: [
{
translationFiles: [],
},
],
})

export default noMissingTranslations

0 comments on commit b78f3ad

Please sign in to comment.