ai-localize-validators 2.0.1 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # ai-localize-validators
2
2
 
3
+ ## 2.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+ - ai-localize-shared@2.0.3
9
+ - ai-localize-config@2.0.3
10
+
11
+ ## 2.0.2
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies
16
+ - ai-localize-shared@2.0.2
17
+
3
18
  ## 2.0.1
4
19
 
5
20
  ### Patch Changes
package/package.json CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "name": "ai-localize-validators",
3
- "version": "2.0.1",
4
- "description": "Locale file validation engine",
3
+ "version": "2.0.4",
4
+ "description": "Locale file validation engine — missing, duplicate, placeholder and unused key validators",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
7
7
  "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "README.md",
11
+ "CHANGELOG.md"
12
+ ],
8
13
  "exports": {
9
14
  ".": {
10
15
  "types": "./dist/index.d.ts",
@@ -12,8 +17,23 @@
12
17
  "require": "./dist/index.js"
13
18
  }
14
19
  },
20
+ "keywords": [
21
+ "i18n",
22
+ "localization",
23
+ "l10n",
24
+ "internationalization",
25
+ "ai-localize",
26
+ "validation",
27
+ "locale",
28
+ "missing-keys",
29
+ "duplicate-keys",
30
+ "translation"
31
+ ],
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ },
15
35
  "dependencies": {
16
- "ai-localize-shared": "2.0.1"
36
+ "ai-localize-shared": "2.0.4"
17
37
  },
18
38
  "devDependencies": {
19
39
  "tsup": "^8.0.1",
@@ -1,45 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
- import * as fs from 'fs';
3
- import * as path from 'path';
4
- import * as os from 'os';
5
- import { MissingKeyValidator } from '../missing-key-validator.js';
6
-
7
- let tmpDir: string;
8
-
9
- beforeAll(() => {
10
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-localize-test-'));
11
- fs.mkdirSync(path.join(tmpDir, 'en'), { recursive: true });
12
- fs.mkdirSync(path.join(tmpDir, 'fr'), { recursive: true });
13
- fs.writeFileSync(
14
- path.join(tmpDir, 'en', 'translation.json'),
15
- JSON.stringify({ greeting: 'Hello', farewell: 'Goodbye' })
16
- );
17
- fs.writeFileSync(
18
- path.join(tmpDir, 'fr', 'translation.json'),
19
- JSON.stringify({ greeting: 'Bonjour' })
20
- );
21
- });
22
-
23
- afterAll(() => {
24
- fs.rmSync(tmpDir, { recursive: true, force: true });
25
- });
26
-
27
- describe('MissingKeyValidator', () => {
28
- it('detects missing keys in target language', () => {
29
- const validator = new MissingKeyValidator(tmpDir, 'en', ['fr']);
30
- const { errors } = validator.validate();
31
- expect(errors.length).toBeGreaterThan(0);
32
- const missingFarewellInFr = errors.find((e) => e.key.includes('farewell') && e.language === 'fr');
33
- expect(missingFarewellInFr).toBeDefined();
34
- });
35
-
36
- it('returns no errors when all keys present', () => {
37
- fs.writeFileSync(
38
- path.join(tmpDir, 'fr', 'translation.json'),
39
- JSON.stringify({ greeting: 'Bonjour', farewell: 'Au revoir' })
40
- );
41
- const validator = new MissingKeyValidator(tmpDir, 'en', ['fr']);
42
- const { errors } = validator.validate();
43
- expect(errors.length).toBe(0);
44
- });
45
- });
@@ -1,37 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import type { ValidationError } from "ai-localize-shared";
4
- import { readJsonSafe } from "ai-localize-shared";
5
-
6
- export class DuplicateKeyValidator {
7
- constructor(private localesDir: string, private defaultLanguage = "en") {}
8
-
9
- validate(): { errors: ValidationError[]; duplicates: string[] } {
10
- const errors: ValidationError[] = [];
11
- const duplicates: string[] = [];
12
- const defaultDir = path.join(this.localesDir, this.defaultLanguage);
13
- if (!fs.existsSync(defaultDir)) return { errors, duplicates };
14
-
15
- const nsFiles = fs.readdirSync(defaultDir).filter((f) => f.endsWith(".json"));
16
- for (const nsFile of nsFiles) {
17
- const entries = readJsonSafe<Record<string, string>>(path.join(defaultDir, nsFile));
18
- if (!entries) continue;
19
- const namespace = nsFile.replace(".json", "");
20
- const valueMap = new Map<string, string[]>();
21
- for (const [key, value] of Object.entries(entries)) {
22
- if (!value) continue;
23
- const existing = valueMap.get(value) || [];
24
- existing.push(key);
25
- valueMap.set(value, existing);
26
- }
27
- for (const [value, keys] of valueMap) {
28
- if (keys.length > 1) {
29
- const fullKeys = keys.map((k) => `${namespace}.${k}`);
30
- duplicates.push(...fullKeys);
31
- errors.push({ type: "duplicate-key", key: fullKeys.join(", "), message: `Duplicate value "${value}" for: ${fullKeys.join(", ")}` });
32
- }
33
- }
34
- }
35
- return { errors, duplicates };
36
- }
37
- }
package/src/index.ts DELETED
@@ -1,5 +0,0 @@
1
- export * from "./missing-key-validator.js";
2
- export * from "./duplicate-key-validator.js";
3
- export * from "./placeholder-validator.js";
4
- export * from "./unused-key-validator.js";
5
- export * from "./locale-validator.js";
@@ -1,44 +0,0 @@
1
- import type { ValidationResult } from "ai-localize-shared";
2
- import { MissingKeyValidator } from "./missing-key-validator.js";
3
- import { DuplicateKeyValidator } from "./duplicate-key-validator.js";
4
- import { PlaceholderValidator } from "./placeholder-validator.js";
5
- import { UnusedKeyValidator } from "./unused-key-validator.js";
6
-
7
- export interface ValidatorOptions {
8
- localesDir: string;
9
- sourceDir: string;
10
- defaultLanguage?: string;
11
- targetLanguages?: string[];
12
- checkUnused?: boolean;
13
- checkDuplicates?: boolean;
14
- checkPlaceholders?: boolean;
15
- }
16
-
17
- export class LocaleValidator {
18
- constructor(private options: ValidatorOptions) {}
19
-
20
- validate(): ValidationResult {
21
- const errors: ValidationResult["errors"] = [];
22
- const warnings: ValidationResult["warnings"] = [];
23
- const lang = this.options.defaultLanguage || "en";
24
-
25
- const { errors: missingErrors } = new MissingKeyValidator(this.options.localesDir, lang, this.options.targetLanguages || []).validate();
26
- errors.push(...missingErrors);
27
-
28
- if (this.options.checkDuplicates !== false) {
29
- const { errors: dupErrors } = new DuplicateKeyValidator(this.options.localesDir, lang).validate();
30
- errors.push(...dupErrors);
31
- }
32
-
33
- if (this.options.checkPlaceholders !== false) {
34
- errors.push(...new PlaceholderValidator(this.options.localesDir, lang).validate());
35
- }
36
-
37
- if (this.options.checkUnused !== false) {
38
- const { warnings: unusedWarnings } = new UnusedKeyValidator(this.options.localesDir, this.options.sourceDir, lang).validate();
39
- warnings.push(...unusedWarnings);
40
- }
41
-
42
- return { valid: errors.length === 0, errors, warnings };
43
- }
44
- }
@@ -1,46 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import type { ValidationError } from "ai-localize-shared";
4
- import { readJsonSafe } from "ai-localize-shared";
5
-
6
- export class MissingKeyValidator {
7
- constructor(
8
- private localesDir: string,
9
- private defaultLanguage = "en",
10
- private targetLanguages: string[] = []
11
- ) {}
12
-
13
- validate(): { errors: ValidationError[]; missingByLanguage: Record<string, string[]> } {
14
- const errors: ValidationError[] = [];
15
- const missingByLanguage: Record<string, string[]> = {};
16
-
17
- const defaultDir = path.join(this.localesDir, this.defaultLanguage);
18
- if (!fs.existsSync(defaultDir)) return { errors, missingByLanguage };
19
-
20
- const nsFiles = fs.readdirSync(defaultDir).filter((f) => f.endsWith(".json"));
21
- const langs = this.targetLanguages.length > 0
22
- ? this.targetLanguages
23
- : fs.readdirSync(this.localesDir, { withFileTypes: true })
24
- .filter((e) => e.isDirectory() && e.name !== this.defaultLanguage)
25
- .map((e) => e.name);
26
-
27
- for (const nsFile of nsFiles) {
28
- const defaultEntries = readJsonSafe<Record<string, string>>(path.join(defaultDir, nsFile));
29
- if (!defaultEntries) continue;
30
- const namespace = nsFile.replace(".json", "");
31
- for (const lang of langs) {
32
- const targetPath = path.join(this.localesDir, lang, nsFile);
33
- const targetEntries = readJsonSafe<Record<string, string>>(targetPath) || {};
34
- for (const key of Object.keys(defaultEntries)) {
35
- if (!(key in targetEntries) || targetEntries[key] === "") {
36
- const fullKey = `${namespace}.${key}`;
37
- errors.push({ type: "missing-key", key: fullKey, language: lang, message: `Missing key "${fullKey}" in "${lang}"`, filePath: targetPath });
38
- if (!missingByLanguage[lang]) missingByLanguage[lang] = [];
39
- missingByLanguage[lang].push(fullKey);
40
- }
41
- }
42
- }
43
- }
44
- return { errors, missingByLanguage };
45
- }
46
- }
@@ -1,48 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import type { ValidationError } from "ai-localize-shared";
4
- import { readJsonSafe } from "ai-localize-shared";
5
-
6
- const PATTERNS = [/\{\{([^}]+)\}\}/g, /\{([^}]+)\}/g, /%\(([^)]+)\)s/g, /%(\w+)/g];
7
-
8
- export class PlaceholderValidator {
9
- constructor(private localesDir: string, private defaultLanguage = "en") {}
10
-
11
- validate(): ValidationError[] {
12
- const errors: ValidationError[] = [];
13
- const defaultDir = path.join(this.localesDir, this.defaultLanguage);
14
- if (!fs.existsSync(defaultDir)) return errors;
15
-
16
- const nsFiles = fs.readdirSync(defaultDir).filter((f) => f.endsWith(".json"));
17
- const langs = fs.readdirSync(this.localesDir, { withFileTypes: true })
18
- .filter((e) => e.isDirectory() && e.name !== this.defaultLanguage)
19
- .map((e) => e.name);
20
-
21
- for (const nsFile of nsFiles) {
22
- const defaultEntries = readJsonSafe<Record<string, string>>(path.join(defaultDir, nsFile));
23
- if (!defaultEntries) continue;
24
- const namespace = nsFile.replace(".json", "");
25
- for (const lang of langs) {
26
- const targetEntries = readJsonSafe<Record<string, string>>(path.join(this.localesDir, lang, nsFile)) || {};
27
- for (const [key, defaultVal] of Object.entries(defaultEntries)) {
28
- const targetVal = targetEntries[key];
29
- if (!targetVal) continue;
30
- const defPh = this.extract(defaultVal);
31
- const tgtPh = this.extract(targetVal);
32
- const missing = defPh.filter((p) => !tgtPh.includes(p));
33
- const extra = tgtPh.filter((p) => !defPh.includes(p));
34
- if (missing.length > 0 || extra.length > 0) {
35
- errors.push({ type: "placeholder-mismatch", key: `${namespace}.${key}`, language: lang, message: `Placeholder mismatch in "${lang}": missing [${missing.join(", ")}], extra [${extra.join(", ")}]` });
36
- }
37
- }
38
- }
39
- }
40
- return errors;
41
- }
42
-
43
- private extract(value: string): string[] {
44
- const result: string[] = [];
45
- for (const p of PATTERNS) { p.lastIndex = 0; let m; while ((m = p.exec(value)) !== null) result.push(m[0]); }
46
- return [...new Set(result)];
47
- }
48
- }
@@ -1,50 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import type { ValidationWarning } from "ai-localize-shared";
4
- import { readJsonSafe, collectFiles } from "ai-localize-shared";
5
-
6
- export class UnusedKeyValidator {
7
- constructor(
8
- private localesDir: string,
9
- private sourceDir: string,
10
- private defaultLanguage = "en"
11
- ) {}
12
-
13
- validate(): { warnings: ValidationWarning[]; unusedKeys: string[] } {
14
- const warnings: ValidationWarning[] = [];
15
- const unusedKeys: string[] = [];
16
- const allKeys = this.collectLocaleKeys();
17
- const sourceContent = this.collectSourceContent();
18
-
19
- for (const key of allKeys) {
20
- const shortKey = key.includes(".") ? key.split(".").slice(1).join(".") : key;
21
- const referenced =
22
- sourceContent.includes(`'${key}'`) || sourceContent.includes(`"${key}"`) ||
23
- sourceContent.includes(`'${shortKey}'`) || sourceContent.includes(`"${shortKey}"`);
24
- if (!referenced) {
25
- unusedKeys.push(key);
26
- warnings.push({ type: "unused-key", key, message: `Key "${key}" not referenced in source` });
27
- }
28
- }
29
- return { warnings, unusedKeys };
30
- }
31
-
32
- private collectLocaleKeys(): string[] {
33
- const keys: string[] = [];
34
- const defaultDir = path.join(this.localesDir, this.defaultLanguage);
35
- if (!fs.existsSync(defaultDir)) return keys;
36
- for (const file of fs.readdirSync(defaultDir).filter((f) => f.endsWith(".json"))) {
37
- const ns = file.replace(".json", "");
38
- const entries = readJsonSafe<Record<string, string>>(path.join(defaultDir, file)) || {};
39
- for (const key of Object.keys(entries)) keys.push(`${ns}.${key}`);
40
- }
41
- return keys;
42
- }
43
-
44
- private collectSourceContent(): string {
45
- const files = collectFiles(this.sourceDir, ["ts", "tsx", "js", "jsx", "vue", "html"], ["node_modules", "dist"]);
46
- let content = "";
47
- for (const f of files) { try { content += fs.readFileSync(f, 'utf-8') + '\n'; } catch { /**/ } }
48
- return content;
49
- }
50
- }
package/tsconfig.json DELETED
@@ -1,6 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": { "outDir": "./dist", "rootDir": "./src" },
4
- "include": ["src/**/*"],
5
- "exclude": ["node_modules", "dist"]
6
- }