@better-i18n/cli 0.1.7 → 0.2.0
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/dist/analyzer/dynamic-matcher.d.ts +53 -0
- package/dist/analyzer/dynamic-matcher.d.ts.map +1 -0
- package/dist/analyzer/dynamic-matcher.js +93 -0
- package/dist/analyzer/dynamic-matcher.js.map +1 -0
- package/dist/analyzer/index.d.ts +13 -4
- package/dist/analyzer/index.d.ts.map +1 -1
- package/dist/analyzer/index.js +165 -22
- package/dist/analyzer/index.js.map +1 -1
- package/dist/analyzer/rules/data-structure.d.ts +13 -0
- package/dist/analyzer/rules/data-structure.d.ts.map +1 -0
- package/dist/analyzer/rules/data-structure.js +264 -0
- package/dist/analyzer/rules/data-structure.js.map +1 -0
- package/dist/analyzer/rules/index.d.ts +10 -7
- package/dist/analyzer/rules/index.d.ts.map +1 -1
- package/dist/analyzer/rules/index.js +10 -7
- package/dist/analyzer/rules/index.js.map +1 -1
- package/dist/analyzer/rules/jsx-text.js +1 -1
- package/dist/analyzer/rules/jsx-text.js.map +1 -1
- package/dist/analyzer/rules/translation-function.d.ts.map +1 -1
- package/dist/analyzer/rules/translation-function.js +94 -6
- package/dist/analyzer/rules/translation-function.js.map +1 -1
- package/dist/analyzer/types.d.ts +28 -3
- package/dist/analyzer/types.d.ts.map +1 -1
- package/dist/commands/check.d.ts +14 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +70 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/doctor.d.ts +20 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +77 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +158 -272
- package/dist/commands/sync.js.map +1 -1
- package/dist/context/detector.js +1 -1
- package/dist/context/detector.js.map +1 -1
- package/dist/context/oidc.d.ts +33 -0
- package/dist/context/oidc.d.ts.map +1 -0
- package/dist/context/oidc.js +63 -0
- package/dist/context/oidc.js.map +1 -0
- package/dist/doctor/index.d.ts +65 -0
- package/dist/doctor/index.d.ts.map +1 -0
- package/dist/doctor/index.js +252 -0
- package/dist/doctor/index.js.map +1 -0
- package/dist/doctor/project-discovery.d.ts +37 -0
- package/dist/doctor/project-discovery.d.ts.map +1 -0
- package/dist/doctor/project-discovery.js +140 -0
- package/dist/doctor/project-discovery.js.map +1 -0
- package/dist/doctor/score.d.ts +41 -0
- package/dist/doctor/score.d.ts.map +1 -0
- package/dist/doctor/score.js +107 -0
- package/dist/doctor/score.js.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -1
- package/dist/reporters/doctor-eslint.d.ts +17 -0
- package/dist/reporters/doctor-eslint.d.ts.map +1 -0
- package/dist/reporters/doctor-eslint.js +139 -0
- package/dist/reporters/doctor-eslint.js.map +1 -0
- package/dist/reporters/doctor-json.d.ts +12 -0
- package/dist/reporters/doctor-json.d.ts.map +1 -0
- package/dist/reporters/doctor-json.js +21 -0
- package/dist/reporters/doctor-json.js.map +1 -0
- package/dist/reporters/doctor-report.d.ts +23 -0
- package/dist/reporters/doctor-report.d.ts.map +1 -0
- package/dist/reporters/doctor-report.js +62 -0
- package/dist/reporters/doctor-report.js.map +1 -0
- package/dist/reporters/eslint-style.js +4 -4
- package/dist/reporters/eslint-style.js.map +1 -1
- package/dist/rules/categories.d.ts +36 -0
- package/dist/rules/categories.d.ts.map +1 -0
- package/dist/rules/categories.js +80 -0
- package/dist/rules/categories.js.map +1 -0
- package/dist/rules/code/index.d.ts +9 -0
- package/dist/rules/code/index.d.ts.map +1 -0
- package/dist/rules/code/index.js +9 -0
- package/dist/rules/code/index.js.map +1 -0
- package/dist/rules/code/jsx-attribute.d.ts +12 -0
- package/dist/rules/code/jsx-attribute.d.ts.map +1 -0
- package/dist/rules/code/jsx-attribute.js +78 -0
- package/dist/rules/code/jsx-attribute.js.map +1 -0
- package/dist/rules/code/jsx-text.d.ts +12 -0
- package/dist/rules/code/jsx-text.d.ts.map +1 -0
- package/dist/rules/code/jsx-text.js +47 -0
- package/dist/rules/code/jsx-text.js.map +1 -0
- package/dist/rules/code/string-variable.d.ts +13 -0
- package/dist/rules/code/string-variable.d.ts.map +1 -0
- package/dist/rules/code/string-variable.js +185 -0
- package/dist/rules/code/string-variable.js.map +1 -0
- package/dist/rules/code/ternary-locale.d.ts +12 -0
- package/dist/rules/code/ternary-locale.d.ts.map +1 -0
- package/dist/rules/code/ternary-locale.js +53 -0
- package/dist/rules/code/ternary-locale.js.map +1 -0
- package/dist/rules/code/toast-message.d.ts +13 -0
- package/dist/rules/code/toast-message.d.ts.map +1 -0
- package/dist/rules/code/toast-message.js +101 -0
- package/dist/rules/code/toast-message.js.map +1 -0
- package/dist/rules/extraction/data-structure.d.ts +13 -0
- package/dist/rules/extraction/data-structure.d.ts.map +1 -0
- package/dist/rules/extraction/data-structure.js +212 -0
- package/dist/rules/extraction/data-structure.js.map +1 -0
- package/dist/rules/extraction/index.d.ts +6 -0
- package/dist/rules/extraction/index.d.ts.map +1 -0
- package/dist/rules/extraction/index.js +6 -0
- package/dist/rules/extraction/index.js.map +1 -0
- package/dist/rules/extraction/translation-function.d.ts +12 -0
- package/dist/rules/extraction/translation-function.d.ts.map +1 -0
- package/dist/rules/extraction/translation-function.js +153 -0
- package/dist/rules/extraction/translation-function.js.map +1 -0
- package/dist/rules/health/index.d.ts +16 -0
- package/dist/rules/health/index.d.ts.map +1 -0
- package/dist/rules/health/index.js +22 -0
- package/dist/rules/health/index.js.map +1 -0
- package/dist/rules/health/missing-translations.d.ts +13 -0
- package/dist/rules/health/missing-translations.d.ts.map +1 -0
- package/dist/rules/health/missing-translations.js +48 -0
- package/dist/rules/health/missing-translations.js.map +1 -0
- package/dist/rules/health/orphan-keys.d.ts +16 -0
- package/dist/rules/health/orphan-keys.d.ts.map +1 -0
- package/dist/rules/health/orphan-keys.js +50 -0
- package/dist/rules/health/orphan-keys.js.map +1 -0
- package/dist/rules/health/placeholder-mismatch.d.ts +32 -0
- package/dist/rules/health/placeholder-mismatch.d.ts.map +1 -0
- package/dist/rules/health/placeholder-mismatch.js +120 -0
- package/dist/rules/health/placeholder-mismatch.js.map +1 -0
- package/dist/rules/registry.d.ts +84 -0
- package/dist/rules/registry.d.ts.map +1 -0
- package/dist/rules/registry.js +11 -0
- package/dist/rules/registry.js.map +1 -0
- package/dist/utils/cdn-client.d.ts +30 -0
- package/dist/utils/cdn-client.d.ts.map +1 -0
- package/dist/utils/cdn-client.js +59 -0
- package/dist/utils/cdn-client.js.map +1 -0
- package/dist/utils/json-keys.d.ts +60 -0
- package/dist/utils/json-keys.d.ts.map +1 -0
- package/dist/utils/json-keys.js +103 -0
- package/dist/utils/json-keys.js.map +1 -0
- package/dist/utils/key-comparison.d.ts +67 -0
- package/dist/utils/key-comparison.d.ts.map +1 -0
- package/dist/utils/key-comparison.js +299 -0
- package/dist/utils/key-comparison.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Missing Translations Rule
|
|
3
|
+
*
|
|
4
|
+
* Detects translation keys that exist in the source locale but are
|
|
5
|
+
* missing in one or more target locales. This is the most fundamental
|
|
6
|
+
* coverage check — if a key is missing, users see fallback or broken UI.
|
|
7
|
+
*
|
|
8
|
+
* Category: Coverage (highest weight in health score)
|
|
9
|
+
* Default Severity: error
|
|
10
|
+
*/
|
|
11
|
+
import { RULE_CATEGORY_MAP, RULE_HELP_MAP } from "../categories.js";
|
|
12
|
+
const RULE_ID = "missing-translations";
|
|
13
|
+
export const missingTranslationsRule = {
|
|
14
|
+
id: RULE_ID,
|
|
15
|
+
defaultSeverity: "error",
|
|
16
|
+
description: "Detect keys present in source locale but missing in target locales",
|
|
17
|
+
run(context) {
|
|
18
|
+
const { sourceLocale, targetLocales, translations } = context;
|
|
19
|
+
const sourceKeys = translations[sourceLocale];
|
|
20
|
+
if (!sourceKeys)
|
|
21
|
+
return [];
|
|
22
|
+
const diagnostics = [];
|
|
23
|
+
const sourceKeySet = Object.keys(sourceKeys);
|
|
24
|
+
for (const targetLocale of targetLocales) {
|
|
25
|
+
const targetKeys = translations[targetLocale] || {};
|
|
26
|
+
for (const key of sourceKeySet) {
|
|
27
|
+
if (!(key in targetKeys)) {
|
|
28
|
+
const namespace = key.split(".")[0] || "default";
|
|
29
|
+
diagnostics.push({
|
|
30
|
+
filePath: `${targetLocale}.json`,
|
|
31
|
+
line: 0,
|
|
32
|
+
column: 0,
|
|
33
|
+
rule: RULE_ID,
|
|
34
|
+
category: RULE_CATEGORY_MAP[RULE_ID] || "Coverage",
|
|
35
|
+
severity: this.defaultSeverity,
|
|
36
|
+
message: `Key "${key}" is missing in locale "${targetLocale}"`,
|
|
37
|
+
help: RULE_HELP_MAP[RULE_ID] || "",
|
|
38
|
+
key,
|
|
39
|
+
namespace,
|
|
40
|
+
language: targetLocale,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return diagnostics;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=missing-translations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"missing-translations.js","sourceRoot":"","sources":["../../../src/rules/health/missing-translations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAOpE,MAAM,OAAO,GAAG,sBAAsB,CAAC;AAEvC,MAAM,CAAC,MAAM,uBAAuB,GAAe;IACjD,EAAE,EAAE,OAAO;IACX,eAAe,EAAE,OAAO;IACxB,WAAW,EACT,oEAAoE;IAEtE,GAAG,CAAC,OAA0B;QAC5B,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC;QAE9D,MAAM,UAAU,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;QAC9C,IAAI,CAAC,UAAU;YAAE,OAAO,EAAE,CAAC;QAE3B,MAAM,WAAW,GAAqB,EAAE,CAAC;QACzC,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAE7C,KAAK,MAAM,YAAY,IAAI,aAAa,EAAE,CAAC;YACzC,MAAM,UAAU,GAAG,YAAY,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;YAEpD,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;gBAC/B,IAAI,CAAC,CAAC,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC;oBACzB,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC;oBAEjD,WAAW,CAAC,IAAI,CAAC;wBACf,QAAQ,EAAE,GAAG,YAAY,OAAO;wBAChC,IAAI,EAAE,CAAC;wBACP,MAAM,EAAE,CAAC;wBACT,IAAI,EAAE,OAAO;wBACb,QAAQ,EAAE,iBAAiB,CAAC,OAAO,CAAC,IAAI,UAAU;wBAClD,QAAQ,EAAE,IAAI,CAAC,eAAe;wBAC9B,OAAO,EAAE,QAAQ,GAAG,2BAA2B,YAAY,GAAG;wBAC9D,IAAI,EAAE,aAAa,CAAC,OAAO,CAAC,IAAI,EAAE;wBAClC,GAAG;wBACH,SAAS;wBACT,QAAQ,EAAE,YAAY;qBACvB,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,WAAW,CAAC;IACrB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orphan Keys Rule
|
|
3
|
+
*
|
|
4
|
+
* Detects translation keys that exist in remote translation files but
|
|
5
|
+
* are never referenced in code. These "orphan" keys increase bundle size
|
|
6
|
+
* and maintenance burden without providing value.
|
|
7
|
+
*
|
|
8
|
+
* Note: This rule requires code analysis results (codeKeys set).
|
|
9
|
+
* If codeKeys is empty, it returns no diagnostics to avoid false positives.
|
|
10
|
+
*
|
|
11
|
+
* Category: Performance
|
|
12
|
+
* Default Severity: warning
|
|
13
|
+
*/
|
|
14
|
+
import type { HealthRule } from "../registry.js";
|
|
15
|
+
export declare const orphanKeysRule: HealthRule;
|
|
16
|
+
//# sourceMappingURL=orphan-keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orphan-keys.d.ts","sourceRoot":"","sources":["../../../src/rules/health/orphan-keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EACV,UAAU,EAGX,MAAM,gBAAgB,CAAC;AAIxB,eAAO,MAAM,cAAc,EAAE,UAuC5B,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orphan Keys Rule
|
|
3
|
+
*
|
|
4
|
+
* Detects translation keys that exist in remote translation files but
|
|
5
|
+
* are never referenced in code. These "orphan" keys increase bundle size
|
|
6
|
+
* and maintenance burden without providing value.
|
|
7
|
+
*
|
|
8
|
+
* Note: This rule requires code analysis results (codeKeys set).
|
|
9
|
+
* If codeKeys is empty, it returns no diagnostics to avoid false positives.
|
|
10
|
+
*
|
|
11
|
+
* Category: Performance
|
|
12
|
+
* Default Severity: warning
|
|
13
|
+
*/
|
|
14
|
+
import { RULE_CATEGORY_MAP, RULE_HELP_MAP } from "../categories.js";
|
|
15
|
+
const RULE_ID = "orphan-keys";
|
|
16
|
+
export const orphanKeysRule = {
|
|
17
|
+
id: RULE_ID,
|
|
18
|
+
defaultSeverity: "warning",
|
|
19
|
+
description: "Detect translation keys not referenced in code (dead translations)",
|
|
20
|
+
run(context) {
|
|
21
|
+
const { sourceLocale, translations, codeKeys } = context;
|
|
22
|
+
// If no code keys were collected, skip — we can't determine orphans
|
|
23
|
+
// without knowing what code actually uses.
|
|
24
|
+
if (codeKeys.size === 0)
|
|
25
|
+
return [];
|
|
26
|
+
const sourceKeys = translations[sourceLocale];
|
|
27
|
+
if (!sourceKeys)
|
|
28
|
+
return [];
|
|
29
|
+
const diagnostics = [];
|
|
30
|
+
for (const key of Object.keys(sourceKeys)) {
|
|
31
|
+
if (!codeKeys.has(key)) {
|
|
32
|
+
const namespace = key.split(".")[0] || "default";
|
|
33
|
+
diagnostics.push({
|
|
34
|
+
filePath: `${sourceLocale}.json`,
|
|
35
|
+
line: 0,
|
|
36
|
+
column: 0,
|
|
37
|
+
rule: RULE_ID,
|
|
38
|
+
category: RULE_CATEGORY_MAP[RULE_ID] || "Performance",
|
|
39
|
+
severity: this.defaultSeverity,
|
|
40
|
+
message: `Key "${key}" exists in translations but is not used in code`,
|
|
41
|
+
help: RULE_HELP_MAP[RULE_ID] || "",
|
|
42
|
+
key,
|
|
43
|
+
namespace,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return diagnostics;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=orphan-keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orphan-keys.js","sourceRoot":"","sources":["../../../src/rules/health/orphan-keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAOpE,MAAM,OAAO,GAAG,aAAa,CAAC;AAE9B,MAAM,CAAC,MAAM,cAAc,GAAe;IACxC,EAAE,EAAE,OAAO;IACX,eAAe,EAAE,SAAS;IAC1B,WAAW,EACT,oEAAoE;IAEtE,GAAG,CAAC,OAA0B;QAC5B,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;QAEzD,oEAAoE;QACpE,2CAA2C;QAC3C,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAEnC,MAAM,UAAU,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;QAC9C,IAAI,CAAC,UAAU;YAAE,OAAO,EAAE,CAAC;QAE3B,MAAM,WAAW,GAAqB,EAAE,CAAC;QAEzC,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YAC1C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBACvB,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC;gBAEjD,WAAW,CAAC,IAAI,CAAC;oBACf,QAAQ,EAAE,GAAG,YAAY,OAAO;oBAChC,IAAI,EAAE,CAAC;oBACP,MAAM,EAAE,CAAC;oBACT,IAAI,EAAE,OAAO;oBACb,QAAQ,EAAE,iBAAiB,CAAC,OAAO,CAAC,IAAI,aAAa;oBACrD,QAAQ,EAAE,IAAI,CAAC,eAAe;oBAC9B,OAAO,EAAE,QAAQ,GAAG,kDAAkD;oBACtE,IAAI,EAAE,aAAa,CAAC,OAAO,CAAC,IAAI,EAAE;oBAClC,GAAG;oBACH,SAAS;iBACV,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,WAAW,CAAC;IACrB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placeholder Mismatch Rule
|
|
3
|
+
*
|
|
4
|
+
* Detects placeholder inconsistencies between source and target translations.
|
|
5
|
+
* Missing placeholders cause runtime errors (blank slots, crashes).
|
|
6
|
+
* Extra placeholders indicate likely copy-paste mistakes.
|
|
7
|
+
*
|
|
8
|
+
* Supports formats:
|
|
9
|
+
* {{name}} - Handlebars / Mustache
|
|
10
|
+
* {count} - ICU MessageFormat / i18next
|
|
11
|
+
* {0}, {1} - Indexed positional
|
|
12
|
+
* ${var} - Template literal style
|
|
13
|
+
* %s, %d, %@ - printf-style
|
|
14
|
+
* %1$s - Positional printf-style
|
|
15
|
+
*
|
|
16
|
+
* Category: Quality
|
|
17
|
+
* Default Severity: error
|
|
18
|
+
*
|
|
19
|
+
* Note: extractPlaceholders() is adapted from
|
|
20
|
+
* apps/api/domains/ai/quality/checks/placeholder-check.ts
|
|
21
|
+
* (cross-repo import not possible, logic kept in sync manually)
|
|
22
|
+
*/
|
|
23
|
+
import type { HealthRule } from "../registry.js";
|
|
24
|
+
/**
|
|
25
|
+
* Extract placeholder tokens from a translation string.
|
|
26
|
+
*
|
|
27
|
+
* Uses ordered pattern matching with overlap detection to handle
|
|
28
|
+
* nested formats like {{name}} vs {name}.
|
|
29
|
+
*/
|
|
30
|
+
export declare function extractPlaceholders(text: string): string[];
|
|
31
|
+
export declare const placeholderMismatchRule: HealthRule;
|
|
32
|
+
//# sourceMappingURL=placeholder-mismatch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placeholder-mismatch.d.ts","sourceRoot":"","sources":["../../../src/rules/health/placeholder-mismatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EACV,UAAU,EAGX,MAAM,gBAAgB,CAAC;AAIxB;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAiC1D;AAED,eAAO,MAAM,uBAAuB,EAAE,UAsErC,CAAC"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placeholder Mismatch Rule
|
|
3
|
+
*
|
|
4
|
+
* Detects placeholder inconsistencies between source and target translations.
|
|
5
|
+
* Missing placeholders cause runtime errors (blank slots, crashes).
|
|
6
|
+
* Extra placeholders indicate likely copy-paste mistakes.
|
|
7
|
+
*
|
|
8
|
+
* Supports formats:
|
|
9
|
+
* {{name}} - Handlebars / Mustache
|
|
10
|
+
* {count} - ICU MessageFormat / i18next
|
|
11
|
+
* {0}, {1} - Indexed positional
|
|
12
|
+
* ${var} - Template literal style
|
|
13
|
+
* %s, %d, %@ - printf-style
|
|
14
|
+
* %1$s - Positional printf-style
|
|
15
|
+
*
|
|
16
|
+
* Category: Quality
|
|
17
|
+
* Default Severity: error
|
|
18
|
+
*
|
|
19
|
+
* Note: extractPlaceholders() is adapted from
|
|
20
|
+
* apps/api/domains/ai/quality/checks/placeholder-check.ts
|
|
21
|
+
* (cross-repo import not possible, logic kept in sync manually)
|
|
22
|
+
*/
|
|
23
|
+
import { RULE_CATEGORY_MAP, RULE_HELP_MAP } from "../categories.js";
|
|
24
|
+
const RULE_ID = "placeholder-mismatch";
|
|
25
|
+
/**
|
|
26
|
+
* Extract placeholder tokens from a translation string.
|
|
27
|
+
*
|
|
28
|
+
* Uses ordered pattern matching with overlap detection to handle
|
|
29
|
+
* nested formats like {{name}} vs {name}.
|
|
30
|
+
*/
|
|
31
|
+
export function extractPlaceholders(text) {
|
|
32
|
+
// Order matters: more specific patterns first to avoid sub-match overlaps.
|
|
33
|
+
const patterns = [
|
|
34
|
+
/\{\{[^}]+\}\}/g, // {{name}} — Handlebars / Mustache
|
|
35
|
+
/\$\{[^}]+\}/g, // ${var} — Template literal
|
|
36
|
+
/\{[a-zA-Z_]\w*\}/g, // {count} — ICU / i18next
|
|
37
|
+
/\{\d+\}/g, // {0}, {1} — Indexed positional
|
|
38
|
+
/%\d*\$?[sd@f]/g, // %s, %d, %1$s — printf-style
|
|
39
|
+
];
|
|
40
|
+
const placeholders = [];
|
|
41
|
+
const claimed = [];
|
|
42
|
+
function isOverlapping(start, end) {
|
|
43
|
+
return claimed.some(([cs, ce]) => (start >= cs && start < ce) || (end > cs && end <= ce));
|
|
44
|
+
}
|
|
45
|
+
for (const pattern of patterns) {
|
|
46
|
+
let match;
|
|
47
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
48
|
+
const start = match.index;
|
|
49
|
+
const end = start + match[0].length;
|
|
50
|
+
if (!isOverlapping(start, end)) {
|
|
51
|
+
placeholders.push(match[0]);
|
|
52
|
+
claimed.push([start, end]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return placeholders;
|
|
57
|
+
}
|
|
58
|
+
export const placeholderMismatchRule = {
|
|
59
|
+
id: RULE_ID,
|
|
60
|
+
defaultSeverity: "error",
|
|
61
|
+
description: "Detect placeholder mismatches between source and target translations",
|
|
62
|
+
run(context) {
|
|
63
|
+
const { sourceLocale, targetLocales, translations } = context;
|
|
64
|
+
const sourceKeys = translations[sourceLocale];
|
|
65
|
+
if (!sourceKeys)
|
|
66
|
+
return [];
|
|
67
|
+
const diagnostics = [];
|
|
68
|
+
for (const targetLocale of targetLocales) {
|
|
69
|
+
const targetKeys = translations[targetLocale] || {};
|
|
70
|
+
for (const [key, sourceValue] of Object.entries(sourceKeys)) {
|
|
71
|
+
const targetValue = targetKeys[key];
|
|
72
|
+
if (targetValue === undefined)
|
|
73
|
+
continue; // missing-translations handles this
|
|
74
|
+
const sourcePh = extractPlaceholders(sourceValue);
|
|
75
|
+
const targetPh = extractPlaceholders(targetValue);
|
|
76
|
+
if (sourcePh.length === 0 && targetPh.length === 0)
|
|
77
|
+
continue;
|
|
78
|
+
const namespace = key.split(".")[0] || "default";
|
|
79
|
+
// Check for missing placeholders in target
|
|
80
|
+
for (const ph of sourcePh) {
|
|
81
|
+
if (!targetPh.includes(ph)) {
|
|
82
|
+
diagnostics.push({
|
|
83
|
+
filePath: `${targetLocale}.json`,
|
|
84
|
+
line: 0,
|
|
85
|
+
column: 0,
|
|
86
|
+
rule: RULE_ID,
|
|
87
|
+
category: RULE_CATEGORY_MAP[RULE_ID] || "Quality",
|
|
88
|
+
severity: this.defaultSeverity,
|
|
89
|
+
message: `Placeholder ${ph} in source is missing in "${targetLocale}" for key "${key}"`,
|
|
90
|
+
help: RULE_HELP_MAP[RULE_ID] || "",
|
|
91
|
+
key,
|
|
92
|
+
namespace,
|
|
93
|
+
language: targetLocale,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Check for extra placeholders in target (lower severity)
|
|
98
|
+
for (const ph of targetPh) {
|
|
99
|
+
if (!sourcePh.includes(ph)) {
|
|
100
|
+
diagnostics.push({
|
|
101
|
+
filePath: `${targetLocale}.json`,
|
|
102
|
+
line: 0,
|
|
103
|
+
column: 0,
|
|
104
|
+
rule: RULE_ID,
|
|
105
|
+
category: RULE_CATEGORY_MAP[RULE_ID] || "Quality",
|
|
106
|
+
severity: "warning",
|
|
107
|
+
message: `Extra placeholder ${ph} in "${targetLocale}" not found in source for key "${key}"`,
|
|
108
|
+
help: RULE_HELP_MAP[RULE_ID] || "",
|
|
109
|
+
key,
|
|
110
|
+
namespace,
|
|
111
|
+
language: targetLocale,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return diagnostics;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=placeholder-mismatch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placeholder-mismatch.js","sourceRoot":"","sources":["../../../src/rules/health/placeholder-mismatch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule Registry
|
|
3
|
+
*
|
|
4
|
+
* Core interfaces for the i18n Doctor rule system.
|
|
5
|
+
* Health rules implement HealthRule interface and produce I18nDiagnostic[].
|
|
6
|
+
*
|
|
7
|
+
* Design: Rules only produce message text. Category and help text are
|
|
8
|
+
* externalized in categories.ts (react-doctor pattern) for central management.
|
|
9
|
+
*/
|
|
10
|
+
import type { ProjectContext } from "../analyzer/types.js";
|
|
11
|
+
/**
|
|
12
|
+
* Universal diagnostic type — the single "currency" of i18n Doctor.
|
|
13
|
+
*
|
|
14
|
+
* All sources (code analysis, health rules, file checks) produce this same type.
|
|
15
|
+
* Inspired by react-doctor's Diagnostic type which unifies oxlint + knip + motion check.
|
|
16
|
+
*/
|
|
17
|
+
export interface I18nDiagnostic {
|
|
18
|
+
/** Relative file path from project root */
|
|
19
|
+
filePath: string;
|
|
20
|
+
/** Line number (1-indexed, 0 if not applicable) */
|
|
21
|
+
line: number;
|
|
22
|
+
/** Column number (1-indexed, 0 if not applicable) */
|
|
23
|
+
column: number;
|
|
24
|
+
/** Rule identifier e.g. "missing-translations", "jsx-text" */
|
|
25
|
+
rule: string;
|
|
26
|
+
/** Diagnostic category */
|
|
27
|
+
category: "Coverage" | "Quality" | "Structure" | "Code" | "Performance";
|
|
28
|
+
/** Severity level */
|
|
29
|
+
severity: "error" | "warning" | "info";
|
|
30
|
+
/** Human-readable description of the issue */
|
|
31
|
+
message: string;
|
|
32
|
+
/** Actionable fix suggestion (populated from RULE_HELP_MAP) */
|
|
33
|
+
help: string;
|
|
34
|
+
/** Translation key involved (if applicable) */
|
|
35
|
+
key?: string;
|
|
36
|
+
/** Namespace involved (if applicable) */
|
|
37
|
+
namespace?: string;
|
|
38
|
+
/** Target language involved (if applicable) */
|
|
39
|
+
language?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Health rule interface.
|
|
43
|
+
*
|
|
44
|
+
* Health rules analyze translation files at the project level (not per-file AST).
|
|
45
|
+
* Each rule receives a context with all translations and code keys,
|
|
46
|
+
* and returns diagnostics for any issues found.
|
|
47
|
+
*
|
|
48
|
+
* Design notes:
|
|
49
|
+
* - defaultSeverity lives on the rule (unlike react-doctor where oxlint sets it)
|
|
50
|
+
* because health rules are standalone functions, not ESLint visitors
|
|
51
|
+
* - Category and help are NOT on the rule — they come from categories.ts
|
|
52
|
+
* - User config can override defaultSeverity
|
|
53
|
+
*/
|
|
54
|
+
export interface HealthRule {
|
|
55
|
+
/** Unique rule identifier e.g. "missing-translations" */
|
|
56
|
+
id: string;
|
|
57
|
+
/** Default severity (can be overridden by user config) */
|
|
58
|
+
defaultSeverity: I18nDiagnostic["severity"];
|
|
59
|
+
/** Short description of what the rule checks */
|
|
60
|
+
description: string;
|
|
61
|
+
/** Execute the rule against the health context */
|
|
62
|
+
run(context: HealthRuleContext): I18nDiagnostic[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Context provided to health rules for analysis.
|
|
66
|
+
*
|
|
67
|
+
* Contains all the data a health rule needs: source translations,
|
|
68
|
+
* target translations, code keys, and project configuration.
|
|
69
|
+
*/
|
|
70
|
+
export interface HealthRuleContext {
|
|
71
|
+
/** Source locale code e.g. "en" */
|
|
72
|
+
sourceLocale: string;
|
|
73
|
+
/** Target locale codes e.g. ["tr", "de", "fr"] */
|
|
74
|
+
targetLocales: string[];
|
|
75
|
+
/** All translations: locale → flat key→value map */
|
|
76
|
+
translations: Record<string, Record<string, string>>;
|
|
77
|
+
/** Translation keys detected in code (from AST analysis) */
|
|
78
|
+
codeKeys: Set<string>;
|
|
79
|
+
/** Project root directory */
|
|
80
|
+
rootDir: string;
|
|
81
|
+
/** Project configuration (from i18n.config.ts) */
|
|
82
|
+
projectContext: ProjectContext | null;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/rules/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,0BAA0B;IAC1B,QAAQ,EAAE,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,MAAM,GAAG,aAAa,CAAC;IACxE,qBAAqB;IACrB,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IACvC,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,UAAU;IACzB,yDAAyD;IACzD,EAAE,EAAE,MAAM,CAAC;IACX,0DAA0D;IAC1D,eAAe,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IAC5C,gDAAgD;IAChD,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,GAAG,CAAC,OAAO,EAAE,iBAAiB,GAAG,cAAc,EAAE,CAAC;CACnD;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,mCAAmC;IACnC,YAAY,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,oDAAoD;IACpD,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACrD,4DAA4D;IAC5D,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACtB,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,cAAc,EAAE,cAAc,GAAG,IAAI,CAAC;CACvC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule Registry
|
|
3
|
+
*
|
|
4
|
+
* Core interfaces for the i18n Doctor rule system.
|
|
5
|
+
* Health rules implement HealthRule interface and produce I18nDiagnostic[].
|
|
6
|
+
*
|
|
7
|
+
* Design: Rules only produce message text. Category and help text are
|
|
8
|
+
* externalized in categories.ts (react-doctor pattern) for central management.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/rules/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDN client utilities
|
|
3
|
+
*
|
|
4
|
+
* Functions for fetching translation data from Better i18n CDN.
|
|
5
|
+
* Extracted from sync.ts for reuse in doctor and sync commands.
|
|
6
|
+
*/
|
|
7
|
+
import type { CdnManifest } from "../analyzer/types.js";
|
|
8
|
+
import type { RemoteTranslations } from "./json-keys.js";
|
|
9
|
+
/**
|
|
10
|
+
* Fetch project manifest from CDN.
|
|
11
|
+
*
|
|
12
|
+
* The manifest contains language metadata, file URLs, and last-modified timestamps.
|
|
13
|
+
* Used as the entry point for all CDN operations.
|
|
14
|
+
*/
|
|
15
|
+
export declare function fetchManifest(cdnBaseUrl: string, workspaceId: string, projectSlug: string): Promise<CdnManifest>;
|
|
16
|
+
/**
|
|
17
|
+
* Fetch remote translation keys for a specific locale.
|
|
18
|
+
*
|
|
19
|
+
* Uses manifest URL if available (preferred, as it may include CDN cache-busting),
|
|
20
|
+
* otherwise constructs a fallback URL from convention.
|
|
21
|
+
*/
|
|
22
|
+
export declare function fetchRemoteKeys(cdnBaseUrl: string, workspaceId: string, projectSlug: string, locale: string, manifest: CdnManifest | null): Promise<RemoteTranslations>;
|
|
23
|
+
/**
|
|
24
|
+
* Fetch a single locale file from CDN.
|
|
25
|
+
*
|
|
26
|
+
* Convenience wrapper that combines manifest lookup + locale fetch.
|
|
27
|
+
* Returns null if the locale is not available.
|
|
28
|
+
*/
|
|
29
|
+
export declare function fetchLocaleFile(cdnBaseUrl: string, workspaceId: string, projectSlug: string, locale: string): Promise<RemoteTranslations | null>;
|
|
30
|
+
//# sourceMappingURL=cdn-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdn-client.d.ts","sourceRoot":"","sources":["../../src/utils/cdn-client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEzD;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,WAAW,CAAC,CAStB;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,WAAW,GAAG,IAAI,GAC3B,OAAO,CAAC,kBAAkB,CAAC,CAe7B;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAkBpC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDN client utilities
|
|
3
|
+
*
|
|
4
|
+
* Functions for fetching translation data from Better i18n CDN.
|
|
5
|
+
* Extracted from sync.ts for reuse in doctor and sync commands.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Fetch project manifest from CDN.
|
|
9
|
+
*
|
|
10
|
+
* The manifest contains language metadata, file URLs, and last-modified timestamps.
|
|
11
|
+
* Used as the entry point for all CDN operations.
|
|
12
|
+
*/
|
|
13
|
+
export async function fetchManifest(cdnBaseUrl, workspaceId, projectSlug) {
|
|
14
|
+
const url = `${cdnBaseUrl}/${workspaceId}/${projectSlug}/manifest.json`;
|
|
15
|
+
const response = await fetch(url);
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
throw new Error(`Manifest fetch failed (${response.status})`);
|
|
18
|
+
}
|
|
19
|
+
return response.json();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Fetch remote translation keys for a specific locale.
|
|
23
|
+
*
|
|
24
|
+
* Uses manifest URL if available (preferred, as it may include CDN cache-busting),
|
|
25
|
+
* otherwise constructs a fallback URL from convention.
|
|
26
|
+
*/
|
|
27
|
+
export async function fetchRemoteKeys(cdnBaseUrl, workspaceId, projectSlug, locale, manifest) {
|
|
28
|
+
let url;
|
|
29
|
+
if (manifest?.files[locale]?.url) {
|
|
30
|
+
url = manifest.files[locale].url;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
url = `${cdnBaseUrl}/${workspaceId}/${projectSlug}/translations/${locale}.json`;
|
|
34
|
+
}
|
|
35
|
+
const response = await fetch(url);
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(`CDN fetch failed (${response.status})`);
|
|
38
|
+
}
|
|
39
|
+
return response.json();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Fetch a single locale file from CDN.
|
|
43
|
+
*
|
|
44
|
+
* Convenience wrapper that combines manifest lookup + locale fetch.
|
|
45
|
+
* Returns null if the locale is not available.
|
|
46
|
+
*/
|
|
47
|
+
export async function fetchLocaleFile(cdnBaseUrl, workspaceId, projectSlug, locale) {
|
|
48
|
+
try {
|
|
49
|
+
const manifest = await fetchManifest(cdnBaseUrl, workspaceId, projectSlug);
|
|
50
|
+
if (!manifest.files[locale]) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return await fetchRemoteKeys(cdnBaseUrl, workspaceId, projectSlug, locale, manifest);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=cdn-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdn-client.js","sourceRoot":"","sources":["../../src/utils/cdn-client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,UAAkB,EAClB,WAAmB,EACnB,WAAmB;IAEnB,MAAM,GAAG,GAAG,GAAG,UAAU,IAAI,WAAW,IAAI,WAAW,gBAAgB,CAAC;IACxE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;IAElC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,0BAA0B,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IAChE,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,EAA0B,CAAC;AACjD,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,UAAkB,EAClB,WAAmB,EACnB,WAAmB,EACnB,MAAc,EACd,QAA4B;IAE5B,IAAI,GAAW,CAAC;IAChB,IAAI,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;QACjC,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC;IACnC,CAAC;SAAM,CAAC;QACN,GAAG,GAAG,GAAG,UAAU,IAAI,WAAW,IAAI,WAAW,iBAAiB,MAAM,OAAO,CAAC;IAClF,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;IAElC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,qBAAqB,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IAC3D,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,EAAiC,CAAC;AACxD,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,UAAkB,EAClB,WAAmB,EACnB,WAAmB,EACnB,MAAc;IAEd,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,UAAU,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;QAE3E,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5B,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,MAAM,eAAe,CAC1B,UAAU,EACV,WAAW,EACX,WAAW,EACX,MAAM,EACN,QAAQ,CACT,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON key utilities
|
|
3
|
+
*
|
|
4
|
+
* Functions for flattening, counting, and working with nested translation JSON files.
|
|
5
|
+
* Extracted from sync.ts for reuse in doctor, check, and sync commands.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Recursive type representing any valid JSON value in a translation file.
|
|
9
|
+
* Leaves are strings (most common), numbers, booleans, null, or arrays.
|
|
10
|
+
* Objects represent nested namespaces.
|
|
11
|
+
*/
|
|
12
|
+
export type TranslationValue = string | number | boolean | null | TranslationValue[] | {
|
|
13
|
+
[key: string]: TranslationValue;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Top-level structure of a remote translation file (locale JSON).
|
|
17
|
+
*/
|
|
18
|
+
export type RemoteTranslations = Record<string, TranslationValue>;
|
|
19
|
+
/**
|
|
20
|
+
* Flatten nested JSON translation file into a namespace-grouped key map.
|
|
21
|
+
*
|
|
22
|
+
* Only first-level key becomes the namespace (Better i18n convention).
|
|
23
|
+
* Returns Record<namespace, fullKey[]>.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```
|
|
27
|
+
* flattenKeys({ common: { title: "Hello", actions: { save: "Save" } } })
|
|
28
|
+
* // => { common: ["common.title", "common.actions.save"] }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function flattenKeys(remoteKeys: RemoteTranslations): Record<string, string[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Flatten nested JSON to a flat key→value map (all namespaces merged).
|
|
34
|
+
*
|
|
35
|
+
* Unlike flattenKeys() which groups by namespace, this returns a single
|
|
36
|
+
* flat map of dotted-path → leaf-value. Useful for health rules that
|
|
37
|
+
* need to compare source and target translation values.
|
|
38
|
+
*/
|
|
39
|
+
export declare function flattenToRecord(translations: RemoteTranslations): Record<string, string>;
|
|
40
|
+
/**
|
|
41
|
+
* Count total leaf keys in nested translation structure.
|
|
42
|
+
* Only counts string values (not numbers, arrays, booleans).
|
|
43
|
+
*/
|
|
44
|
+
export declare function countTotalKeys(remoteKeys: RemoteTranslations): number;
|
|
45
|
+
/**
|
|
46
|
+
* Count total keys from a namespace-grouped string[] map.
|
|
47
|
+
*/
|
|
48
|
+
export declare function countMissingKeys(keys: Record<string, string[]>): number;
|
|
49
|
+
/**
|
|
50
|
+
* Count total keys from a namespace-grouped MissingKeyEntry[] map.
|
|
51
|
+
*/
|
|
52
|
+
export declare function countMissingKeysFromEntries(keys: Record<string, MissingKeyEntry[]>): number;
|
|
53
|
+
/**
|
|
54
|
+
* A key detected in code that doesn't exist in remote translations.
|
|
55
|
+
*/
|
|
56
|
+
export interface MissingKeyEntry {
|
|
57
|
+
key: string;
|
|
58
|
+
value: string;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=json-keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-keys.d.ts","sourceRoot":"","sources":["../../src/utils/json-keys.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GACxB,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,gBAAgB,EAAE,GAClB;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAAA;CAAE,CAAC;AAExC;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;AAElE;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CACzB,UAAU,EAAE,kBAAkB,GAC7B,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CA0B1B;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,YAAY,EAAE,kBAAkB,GAC/B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAuBxB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,kBAAkB,GAAG,MAAM,CAgBrE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,MAAM,CAEvE;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,GACtC,MAAM,CAER;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf"}
|