Skip to content

Commit

Permalink
feat: add no-empty-messages rule
Browse files Browse the repository at this point in the history
  • Loading branch information
radiovisual committed Jun 1, 2024
1 parent 7b73d5c commit a46e230
Show file tree
Hide file tree
Showing 14 changed files with 283 additions and 8 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ For each project where you want to run the i18n-validator, you will need to have
*
**/
"rules": {
"no-untranslated-files": "error"
"no-untranslated-messages": "error",
"no-empty-messages": "error"
},
/**
* Set this dryRun setting to true to get all the same logging and reporting you would
Expand Down
3 changes: 2 additions & 1 deletion i18n-validator.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
},
"pathToTranslatedFiles": "src/tests/fixtures/i18n",
"rules": {
"no-untranslated-messages": "error"
"no-untranslated-messages": "error",
"no-empty-messages": "error"
},
"dryRun": false,
"enabled": true
Expand Down
3 changes: 2 additions & 1 deletion src/config/default-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const config: Config = {
translationFiles: {},
pathToTranslatedFiles: "i18n",
rules: {
"no-untranslated-files": "error",
"no-untranslated-messages": "error",
"no-empty-messages": "error",
},
dryRun: false,
enabled: true,
Expand Down
3 changes: 2 additions & 1 deletion src/rules/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { noEmptyMessages } from "./no-empty-messages/no-empty-messages.js";
import { noUntranslatedMessages } from "./no-untranslated-messages/index.js";

export { noUntranslatedMessages };
export { noUntranslatedMessages, noEmptyMessages };
48 changes: 48 additions & 0 deletions src/rules/no-empty-messages/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# no-empty-messages

The no-empty-messages rule ensures that all messages in the translation files are not empty strings. It also verifies that messages in the base locale are not empty.

The rule runs through all the messages in the translation files and checks for:

1. Empty messages in the base locale file.
2. Empty messages in the translation files that correspond to a non-empty message in the base locale file.

Assuming `en.json` is where your default language is (the source file):

❌ Example of **incorrect** setup for this rule (all messages are empty):

```js
en.json: { 'hello': '' }
fr.json: { 'hello': '' }
de.json: { 'hello': '' }
```

❌ Example of an **incorrect** setup for this rule (some locales have empty messages):

```js
en.json: { 'hello': '' }
fr.json: { 'hello': 'Salut!' }
de.json: { 'hello': 'Hi!' }
```

✅ Examples of a **correct** setup for this rule (all messages are non-empty):

```js
en.json: { 'hello': 'Hi!' }
fr.json: { 'hello': 'Salut!' }
de.json: { 'hello': 'Hallo!' }
```

## Example Configuration

```json
{
"rules": {
"no-empty-messages": "error"
}
}
```

## Version

This rule was introduced in i18n-validator v1.0.0.
1 change: 1 addition & 0 deletions src/rules/no-empty-messages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { noEmptyMessages } from "./no-empty-messages.ts";
108 changes: 108 additions & 0 deletions src/rules/no-empty-messages/no-empty-messages.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { createMockProblemReporter } from "../../tests/utils/test-helpers.ts";
import {
Config,
RuleContext,
RuleSeverity,
TranslationFiles,
} from "../../types.ts";
import { noEmptyMessages } from "./no-empty-messages.ts";
import {
getEmptySourceMessageProblem,
getEmptyTranslatedMessageProblem,
} from "./problems.ts";

const ruleMeta = noEmptyMessages.meta;
const rule = noEmptyMessages;

const defaultLocale = "en";

const baseConfig: Config = {
defaultLocale,
sourceFile: "en.json",
translationFiles: { fr: "fr.json" },
pathToTranslatedFiles: "i18n",
rules: {
"no-empty-messages": "error",
},
dryRun: false,
enabled: true,
};

describe.each([["error"], ["warning"]])(`${rule.meta.name}`, (severity) => {
const context: RuleContext = {
severity: severity as RuleSeverity,
};

it(`should report empty messages in the source file with severity: ${severity}`, () => {
const problemReporter = createMockProblemReporter();

const translationFiles: TranslationFiles = {
en: { greeting: "Hello", empty: "" },
fr: { greeting: "Bonjour", empty: "not empty" },
};

rule.run(translationFiles, baseConfig, problemReporter, context);

const expectedProblem = getEmptySourceMessageProblem({
key: "empty",
locale: "en",
severity: severity as RuleSeverity,
ruleMeta,
});

expect(problemReporter.report).toHaveBeenCalledWith(expectedProblem);
expect(problemReporter.report).toHaveBeenCalledTimes(1);
});

it(`should report empty messages in translation file with severity: ${severity}`, () => {
const problemReporter = createMockProblemReporter();

const translationFiles: TranslationFiles = {
en: { greeting: "Hello", empty: "not empty" },
fr: { greeting: "Bonjour", empty: "" },
};

rule.run(translationFiles, baseConfig, problemReporter, context);

const expectedProblem = getEmptyTranslatedMessageProblem({
key: "empty",
locale: "fr",
severity: severity as RuleSeverity,
ruleMeta,
});

expect(problemReporter.report).toHaveBeenCalledWith(expectedProblem);
expect(problemReporter.report).toHaveBeenCalledTimes(1);
});

it("should not report non-empty messages", () => {
const problemReporter = createMockProblemReporter();

const translationFiles: TranslationFiles = {
en: { greeting: "Hello", farewell: "Goodbye" },
fr: { greeting: "Bonjour", farewell: "Au revoir" },
};

rule.run(translationFiles, baseConfig, problemReporter, context);

expect(problemReporter.report).not.toHaveBeenCalled();
});
});

describe(`${rule.meta.name}: off`, () => {
it("should not report problems when severity = off", () => {
const problemReporter = createMockProblemReporter();

const context: RuleContext = {
severity: "off",
};

const translationFiles: TranslationFiles = {
en: { greeting: "Hello", empty: "" },
fr: { greeting: "Bonjour", empty: "" },
};

rule.run(translationFiles, baseConfig, problemReporter, context);
expect(problemReporter.report).not.toHaveBeenCalled();
});
});
65 changes: 65 additions & 0 deletions src/rules/no-empty-messages/no-empty-messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { SEVERITY_LEVEL } from "../../constants.ts";
import {
Config,
Rule,
RuleContext,
RuleMeta,
TranslationFiles,
} from "../../types.ts";
import {
getEmptySourceMessageProblem,
getEmptyTranslatedMessageProblem,
} from "./problems.ts";

const ruleMeta: RuleMeta = {
name: "no-empty-messages",
description: `Checks for empty messages in translations.`,
url: "TBD",
type: "validation",
defaultSeverity: "error",
};

const noEmptyMessages: Rule = {
meta: ruleMeta,
run: (
translationFiles: TranslationFiles,
config: Config,
problemReporter,
context: RuleContext
) => {
const { defaultLocale } = config;
const { severity } = context;
const baseLocale = translationFiles[defaultLocale];

if (severity === SEVERITY_LEVEL.off) {
return;
}

for (const [locale, data] of Object.entries(translationFiles)) {
for (const [key, message] of Object.entries(data)) {
const baseMessage = baseLocale[key].trim();

const hasEmptyBaseMessage = locale === defaultLocale && !baseMessage;
const hasEmptyTranslation =
locale !== defaultLocale && message.trim() === "";

if (hasEmptyBaseMessage) {
problemReporter.report(
getEmptySourceMessageProblem({ key, locale, severity, ruleMeta })
);
} else if (hasEmptyTranslation) {
problemReporter.report(
getEmptyTranslatedMessageProblem({
key,
locale,
severity,
ruleMeta,
})
);
}
}
}
},
};

export { noEmptyMessages };
33 changes: 33 additions & 0 deletions src/rules/no-empty-messages/problems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Problem } from "../../classes/problem.class.ts";
import { RuleMeta, RuleSeverity } from "../../types.ts";

type ProblemContext = {
key: string;
locale: string;
severity: RuleSeverity;
ruleMeta: RuleMeta;
};

export function getEmptySourceMessageProblem(
problemContext: ProblemContext
): Problem {
const { key, locale, severity, ruleMeta } = problemContext;

return Problem.Builder.withRuleMeta(ruleMeta)
.withSeverity(severity)
.withLocale(locale)
.withMessage(`Empty message found in source key: ${key}`)
.build();
}

export function getEmptyTranslatedMessageProblem(
problemContext: ProblemContext
): Problem {
const { key, locale, severity, ruleMeta } = problemContext;

return Problem.Builder.withRuleMeta(ruleMeta)
.withSeverity(severity)
.withLocale(locale)
.withMessage(`Empty message found in translated key: ${key}`)
.build();
}
10 changes: 10 additions & 0 deletions src/rules/no-untranslated-messages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ fr.json: { 'hello': 'Salut!' }
de.json: { 'hello': 'Hallo!' }
```

## Example Configuration

```json
{
"rules": {
"no-untranslated-messages": "error"
}
}
```

## Version

This rule was introduced in i18n-validator v1.0.0.
4 changes: 3 additions & 1 deletion src/tests/fixtures/i18n/de.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{
"hi": "Hi!"
"untranslated-message": "Hi!",
"empty-string": "",
"only-empty-in-en": "[DE] not empty"
}
4 changes: 3 additions & 1 deletion src/tests/fixtures/i18n/en.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{
"hi": "Hi!"
"untranslated-message": "Hi!",
"empty-string": "",
"only-empty-in-en": ""
}
4 changes: 3 additions & 1 deletion src/tests/fixtures/i18n/fr.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{
"hi": "Hi!"
"untranslated-message": "Hi!",
"empty-string": "",
"only-empty-in-en": "[FR] not empty"
}
2 changes: 1 addition & 1 deletion src/utils/config-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Config } from "../types.ts";
import type { Config } from "../types";

export function getAllSupportedLocales(config: Config): string[] {
const allSupportedLocales = new Set([
Expand Down

0 comments on commit a46e230

Please sign in to comment.