@babelize/engine 0.0.0-stage → 0.1.2
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/LICENSE +202 -0
- package/dist/BabelizeEngine.d.ts +12 -0
- package/dist/BabelizeEngine.js +47 -0
- package/dist/analysis/analyze.d.ts +106 -0
- package/dist/analysis/analyze.js +323 -0
- package/dist/config/env.d.ts +9 -0
- package/dist/config/env.js +16 -0
- package/dist/config.d.ts +74 -0
- package/dist/config.js +47 -0
- package/dist/detection/stack.d.ts +42 -0
- package/dist/detection/stack.js +277 -0
- package/dist/django/catalog.d.ts +92 -0
- package/dist/django/catalog.js +677 -0
- package/dist/django/extract.d.ts +20 -0
- package/dist/django/extract.js +79 -0
- package/dist/django/inject-file.d.ts +6 -0
- package/dist/django/inject-file.js +52 -0
- package/dist/django/inject.d.ts +11 -0
- package/dist/django/inject.js +205 -0
- package/dist/django/keys.d.ts +21 -0
- package/dist/django/keys.js +63 -0
- package/dist/django/locale-names.d.ts +13 -0
- package/dist/django/locale-names.js +60 -0
- package/dist/django/mo-file.d.ts +6 -0
- package/dist/django/mo-file.js +60 -0
- package/dist/django/plural-forms.d.ts +34 -0
- package/dist/django/plural-forms.js +216 -0
- package/dist/django/po-file.d.ts +38 -0
- package/dist/django/po-file.js +273 -0
- package/dist/django/python-extract.d.ts +34 -0
- package/dist/django/python-extract.js +619 -0
- package/dist/django/python-lexer.d.ts +65 -0
- package/dist/django/python-lexer.js +398 -0
- package/dist/django/template-extract.d.ts +32 -0
- package/dist/django/template-extract.js +680 -0
- package/dist/django/types.d.ts +49 -0
- package/dist/django/types.js +15 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +140 -0
- package/dist/injection/InjectionStrategy.d.ts +62 -0
- package/dist/injection/InjectionStrategy.js +2 -0
- package/dist/injection/StrategyFactory.d.ts +4 -0
- package/dist/injection/StrategyFactory.js +29 -0
- package/dist/injection/html/html-injector.d.ts +11 -0
- package/dist/injection/html/html-injector.js +173 -0
- package/dist/injection/strategies/NextIntlStrategy.d.ts +25 -0
- package/dist/injection/strategies/NextIntlStrategy.js +141 -0
- package/dist/injection/strategies/ReactI18nextStrategy.d.ts +19 -0
- package/dist/injection/strategies/ReactI18nextStrategy.js +116 -0
- package/dist/injection/strategies/VanillaStrategy.d.ts +11 -0
- package/dist/injection/strategies/VanillaStrategy.js +73 -0
- package/dist/injection/strategies/VueI18nStrategy.d.ts +11 -0
- package/dist/injection/strategies/VueI18nStrategy.js +84 -0
- package/dist/local/formatter.d.ts +26 -0
- package/dist/local/formatter.js +105 -0
- package/dist/local/git.d.ts +23 -0
- package/dist/local/git.js +136 -0
- package/dist/local/hosted-provider.d.ts +68 -0
- package/dist/local/hosted-provider.js +215 -0
- package/dist/local/index.d.ts +23 -0
- package/dist/local/index.js +75 -0
- package/dist/local/provider.d.ts +56 -0
- package/dist/local/provider.js +119 -0
- package/dist/local/pseudo-provider.d.ts +18 -0
- package/dist/local/pseudo-provider.js +208 -0
- package/dist/local/report.d.ts +177 -0
- package/dist/local/report.js +186 -0
- package/dist/local/translate-core.d.ts +218 -0
- package/dist/local/translate-core.js +499 -0
- package/dist/local/translate-messages.d.ts +49 -0
- package/dist/local/translate-messages.js +89 -0
- package/dist/local/workspace-copy.d.ts +91 -0
- package/dist/local/workspace-copy.js +355 -0
- package/dist/modules/extractor/extract.d.ts +20 -0
- package/dist/modules/extractor/extract.js +709 -0
- package/dist/modules/extractor/index.d.ts +2 -0
- package/dist/modules/extractor/index.js +63 -0
- package/dist/modules/finalization/django-finalize.d.ts +28 -0
- package/dist/modules/finalization/django-finalize.js +412 -0
- package/dist/modules/finalization/finalize.d.ts +38 -0
- package/dist/modules/finalization/finalize.js +745 -0
- package/dist/modules/finalization/index.d.ts +1 -0
- package/dist/modules/finalization/index.js +5 -0
- package/dist/modules/finalization/nextjs-finalize.d.ts +40 -0
- package/dist/modules/finalization/nextjs-finalize.js +328 -0
- package/dist/modules/finalization/static-runtime.d.ts +9 -0
- package/dist/modules/finalization/static-runtime.js +261 -0
- package/dist/modules/finalization/writes.d.ts +29 -0
- package/dist/modules/finalization/writes.js +78 -0
- package/dist/modules/format-injection/index.d.ts +19 -0
- package/dist/modules/format-injection/index.js +273 -0
- package/dist/modules/format-injection/inject-formats.d.ts +31 -0
- package/dist/modules/format-injection/inject-formats.js +208 -0
- package/dist/modules/glossary/index.d.ts +2 -0
- package/dist/modules/glossary/index.js +16 -0
- package/dist/modules/glossary/rules.d.ts +81 -0
- package/dist/modules/glossary/rules.js +244 -0
- package/dist/modules/glossary/types.d.ts +22 -0
- package/dist/modules/glossary/types.js +2 -0
- package/dist/modules/injection/index.d.ts +1 -0
- package/dist/modules/injection/index.js +6 -0
- package/dist/modules/injection/inject.d.ts +29 -0
- package/dist/modules/injection/inject.js +1050 -0
- package/dist/modules/keygen/index.d.ts +1 -0
- package/dist/modules/keygen/index.js +11 -0
- package/dist/modules/keygen/keygen.d.ts +4 -0
- package/dist/modules/keygen/keygen.js +123 -0
- package/dist/modules/keygen/locale-data.d.ts +7 -0
- package/dist/modules/keygen/locale-data.js +73 -0
- package/dist/modules/keygen/public.d.ts +2 -0
- package/dist/modules/keygen/public.js +12 -0
- package/dist/modules/locale-builder/index.d.ts +1 -0
- package/dist/modules/locale-builder/index.js +7 -0
- package/dist/modules/locale-builder/locales.d.ts +13 -0
- package/dist/modules/locale-builder/locales.js +130 -0
- package/dist/modules/lockfile/hasher.d.ts +7 -0
- package/dist/modules/lockfile/hasher.js +13 -0
- package/dist/modules/lockfile/index.d.ts +3 -0
- package/dist/modules/lockfile/index.js +22 -0
- package/dist/modules/lockfile/lockfile-manager.d.ts +99 -0
- package/dist/modules/lockfile/lockfile-manager.js +497 -0
- package/dist/modules/lockfile/schema.d.ts +196 -0
- package/dist/modules/lockfile/schema.js +56 -0
- package/dist/modules/parser/index.d.ts +2 -0
- package/dist/modules/parser/index.js +9 -0
- package/dist/modules/summary/index.d.ts +1 -0
- package/dist/modules/summary/index.js +7 -0
- package/dist/modules/summary/summary.d.ts +9 -0
- package/dist/modules/summary/summary.js +195 -0
- package/dist/modules/sync-detector/index.d.ts +2 -0
- package/dist/modules/sync-detector/index.js +7 -0
- package/dist/modules/sync-detector/reporter.d.ts +3 -0
- package/dist/modules/sync-detector/reporter.js +16 -0
- package/dist/modules/sync-detector/sync-detector.d.ts +39 -0
- package/dist/modules/sync-detector/sync-detector.js +374 -0
- package/dist/modules/translator/batch-optimizer.d.ts +12 -0
- package/dist/modules/translator/batch-optimizer.js +30 -0
- package/dist/modules/translator/client/byok.provider.d.ts +64 -0
- package/dist/modules/translator/client/byok.provider.js +319 -0
- package/dist/modules/translator/client/direct-translator.client.d.ts +12 -0
- package/dist/modules/translator/client/direct-translator.client.js +71 -0
- package/dist/modules/translator/client/gemini.provider.d.ts +1 -0
- package/dist/modules/translator/client/gemini.provider.js +110 -0
- package/dist/modules/translator/client/groq.provider.d.ts +1 -0
- package/dist/modules/translator/client/groq.provider.js +111 -0
- package/dist/modules/translator/client/request.types.d.ts +1 -0
- package/dist/modules/translator/client/request.types.js +2 -0
- package/dist/modules/translator/client/retry.d.ts +29 -0
- package/dist/modules/translator/client/retry.js +94 -0
- package/dist/modules/translator/client/test-utils/mock.client.d.ts +31 -0
- package/dist/modules/translator/client/test-utils/mock.client.js +59 -0
- package/dist/modules/translator/client/translator.interface.d.ts +1 -0
- package/dist/modules/translator/client/translator.interface.js +2 -0
- package/dist/modules/translator/icu-branches.d.ts +138 -0
- package/dist/modules/translator/icu-branches.js +393 -0
- package/dist/modules/translator/language-codes/errors.d.ts +10 -0
- package/dist/modules/translator/language-codes/errors.js +18 -0
- package/dist/modules/translator/language-codes/flores-map.d.ts +1 -0
- package/dist/modules/translator/language-codes/flores-map.js +107 -0
- package/dist/modules/translator/language-codes/index.d.ts +11 -0
- package/dist/modules/translator/language-codes/index.js +24 -0
- package/dist/modules/translator/language-codes/language-mapper.d.ts +25 -0
- package/dist/modules/translator/language-codes/language-mapper.js +132 -0
- package/dist/modules/translator/language-codes/rtl-languages.d.ts +2 -0
- package/dist/modules/translator/language-codes/rtl-languages.js +9 -0
- package/dist/modules/translator/memory/memory-matcher.d.ts +8 -0
- package/dist/modules/translator/memory/memory-matcher.js +218 -0
- package/dist/modules/translator/memory/types.d.ts +22 -0
- package/dist/modules/translator/memory/types.js +2 -0
- package/dist/modules/translator/translate.d.ts +74 -0
- package/dist/modules/translator/translate.js +1047 -0
- package/dist/modules/vue/migrate.d.ts +13 -0
- package/dist/modules/vue/migrate.js +40 -0
- package/dist/modules/vue/script-inject.d.ts +16 -0
- package/dist/modules/vue/script-inject.js +115 -0
- package/dist/modules/vue/sfc.d.ts +11 -0
- package/dist/modules/vue/sfc.js +51 -0
- package/dist/modules/vue/template-rewrite.d.ts +25 -0
- package/dist/modules/vue/template-rewrite.js +76 -0
- package/dist/modules/vue/template-scan.d.ts +29 -0
- package/dist/modules/vue/template-scan.js +264 -0
- package/dist/modules/workspace/index.d.ts +1 -0
- package/dist/modules/workspace/index.js +5 -0
- package/dist/modules/workspace/validate.d.ts +13 -0
- package/dist/modules/workspace/validate.js +333 -0
- package/dist/orchestrator.d.ts +13 -0
- package/dist/orchestrator.js +727 -0
- package/dist/providers/default-translation-provider.d.ts +20 -0
- package/dist/providers/default-translation-provider.js +30 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +21 -0
- package/dist/providers/types.d.ts +153 -0
- package/dist/providers/types.js +2 -0
- package/dist/qa/check-project.d.ts +50 -0
- package/dist/qa/check-project.js +193 -0
- package/dist/qa/gettext-files.d.ts +15 -0
- package/dist/qa/gettext-files.js +115 -0
- package/dist/qa/locale-files.d.ts +44 -0
- package/dist/qa/locale-files.js +159 -0
- package/dist/qa/locale-qa.d.ts +61 -0
- package/dist/qa/locale-qa.js +342 -0
- package/dist/qa/review.d.ts +43 -0
- package/dist/qa/review.js +175 -0
- package/dist/runtime/config.d.ts +8 -0
- package/dist/runtime/config.js +37 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/types.d.ts +427 -0
- package/dist/runtime/types.js +38 -0
- package/dist/types.d.ts +134 -0
- package/dist/types.js +12 -0
- package/dist/utils/ast-helpers.d.ts +57 -0
- package/dist/utils/ast-helpers.js +608 -0
- package/dist/utils/cache-keys.d.ts +8 -0
- package/dist/utils/cache-keys.js +18 -0
- package/dist/utils/circuit-breaker.d.ts +43 -0
- package/dist/utils/circuit-breaker.js +119 -0
- package/dist/utils/concurrent-map.d.ts +4 -0
- package/dist/utils/concurrent-map.js +21 -0
- package/dist/utils/detect-format.d.ts +5 -0
- package/dist/utils/detect-format.js +125 -0
- package/dist/utils/extraction-options.d.ts +29 -0
- package/dist/utils/extraction-options.js +33 -0
- package/dist/utils/file-utils.d.ts +33 -0
- package/dist/utils/file-utils.js +267 -0
- package/dist/utils/glossary-match.d.ts +9 -0
- package/dist/utils/glossary-match.js +25 -0
- package/dist/utils/html-extractor.d.ts +8 -0
- package/dist/utils/html-extractor.js +317 -0
- package/dist/utils/i18next-icu.d.ts +38 -0
- package/dist/utils/i18next-icu.js +159 -0
- package/dist/utils/icu-message.d.ts +65 -0
- package/dist/utils/icu-message.js +346 -0
- package/dist/utils/icu-spans.d.ts +10 -0
- package/dist/utils/icu-spans.js +126 -0
- package/dist/utils/jsx-message.d.ts +93 -0
- package/dist/utils/jsx-message.js +628 -0
- package/dist/utils/key-usage.d.ts +35 -0
- package/dist/utils/key-usage.js +247 -0
- package/dist/utils/logger.d.ts +24 -0
- package/dist/utils/logger.js +33 -0
- package/dist/utils/metadata.d.ts +82 -0
- package/dist/utils/metadata.js +261 -0
- package/dist/utils/module-constants.d.ts +30 -0
- package/dist/utils/module-constants.js +265 -0
- package/dist/utils/not-injected.d.ts +6 -0
- package/dist/utils/not-injected.js +38 -0
- package/dist/utils/placeholder-protection.d.ts +74 -0
- package/dist/utils/placeholder-protection.js +181 -0
- package/dist/utils/pseudo-markers.d.ts +9 -0
- package/dist/utils/pseudo-markers.js +16 -0
- package/dist/utils/string-utils.d.ts +52 -0
- package/dist/utils/string-utils.js +270 -0
- package/dist/utils/translation-quality.d.ts +34 -0
- package/dist/utils/translation-quality.js +156 -0
- package/dist/utils/translation-sanitizer.d.ts +11 -0
- package/dist/utils/translation-sanitizer.js +71 -0
- package/dist/utils/translator-caller.d.ts +32 -0
- package/dist/utils/translator-caller.js +217 -0
- package/dist/utils/translator-load-balancer.d.ts +73 -0
- package/dist/utils/translator-load-balancer.js +297 -0
- package/dist/utils/ui-copy.d.ts +32 -0
- package/dist/utils/ui-copy.js +126 -0
- package/dist/utils/untranslated.d.ts +15 -0
- package/dist/utils/untranslated.js +34 -0
- package/dist/utils/workspace.d.ts +14 -0
- package/dist/utils/workspace.js +89 -0
- package/package.json +64 -4
- package/schemas/translate-report.schema.json +297 -0
- package/README.md +0 -3
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.DEFAULT_LOCALE_IGNORE_DIRS = void 0;
|
|
40
|
+
exports.isLocaleCode = isLocaleCode;
|
|
41
|
+
exports.isIgnoredLocalePath = isIgnoredLocalePath;
|
|
42
|
+
exports.groupLocaleFiles = groupLocaleFiles;
|
|
43
|
+
exports.findLocaleFileSets = findLocaleFileSets;
|
|
44
|
+
exports.loadLocale = loadLocale;
|
|
45
|
+
/**
|
|
46
|
+
* Find a project's JSON locale files without running the pipeline (read-only).
|
|
47
|
+
*
|
|
48
|
+
* Recognised layouts, under any directory named locales/messages/lang(s)/
|
|
49
|
+
* i18n/translations (the engine itself writes `<publicDir>/locales/<locale>.json`):
|
|
50
|
+
*
|
|
51
|
+
* locales/en.json, locales/fr.json flat: one file per locale
|
|
52
|
+
* locales/en/common.json, locales/fr/common.json namespaced (i18next): keys become `common:key`
|
|
53
|
+
*/
|
|
54
|
+
const path = __importStar(require("path"));
|
|
55
|
+
const fs_1 = require("fs");
|
|
56
|
+
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
57
|
+
const locale_qa_1 = require("./locale-qa");
|
|
58
|
+
const LOCALE_DIR_NAMES = ['locales', 'locale', 'messages', 'lang', 'langs', 'i18n', 'translations'];
|
|
59
|
+
/**
|
|
60
|
+
* Directories never searched for locale files: dependencies, build output,
|
|
61
|
+
* and test/fixture trees (whose locale files are test data, not the app's).
|
|
62
|
+
*/
|
|
63
|
+
exports.DEFAULT_LOCALE_IGNORE_DIRS = [
|
|
64
|
+
'node_modules', '.git', '.next', 'dist', 'build', 'coverage', '.babelize', 'out',
|
|
65
|
+
'__fixtures__', 'fixtures', '__tests__', '__mocks__', 'test', 'tests', 'testing', 'e2e',
|
|
66
|
+
];
|
|
67
|
+
/** en, fr, pt-BR, zh_Hant, sr-Latn-RS — a locale code, not a namespace like "common". */
|
|
68
|
+
const LOCALE_CODE = /^[a-z]{2,3}(?:[-_](?:[A-Z][a-z]{3}|[A-Za-z]{2}|\d{3}))*$/;
|
|
69
|
+
function isLocaleCode(name) {
|
|
70
|
+
return LOCALE_CODE.test(name);
|
|
71
|
+
}
|
|
72
|
+
function toPosix(p) {
|
|
73
|
+
return p.split(path.sep).join('/');
|
|
74
|
+
}
|
|
75
|
+
/** True when a relative path runs through an ignored directory. */
|
|
76
|
+
function isIgnoredLocalePath(rel, ignoreDirs = exports.DEFAULT_LOCALE_IGNORE_DIRS) {
|
|
77
|
+
const dirs = rel.split('/').slice(0, -1);
|
|
78
|
+
return dirs.some((d) => ignoreDirs.includes(d));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Group relative `.json` paths into locale file sets. Paths that aren't
|
|
82
|
+
* `<dir>/<locale>.json` or `<dir>/<locale>/<ns>.json` under a locale folder
|
|
83
|
+
* are ignored.
|
|
84
|
+
*/
|
|
85
|
+
function groupLocaleFiles(paths, options = {}) {
|
|
86
|
+
var _a;
|
|
87
|
+
const ignoreDirs = options.ignoreDirs ?? exports.DEFAULT_LOCALE_IGNORE_DIRS;
|
|
88
|
+
const sets = new Map();
|
|
89
|
+
for (const rel of [...new Set(paths.map(toPosix))].sort()) {
|
|
90
|
+
if (!rel.endsWith('.json') || isIgnoredLocalePath(rel, ignoreDirs))
|
|
91
|
+
continue;
|
|
92
|
+
const parts = rel.split('/');
|
|
93
|
+
const file = parts[parts.length - 1];
|
|
94
|
+
const parent = parts[parts.length - 2];
|
|
95
|
+
let dir;
|
|
96
|
+
let locale;
|
|
97
|
+
let layout;
|
|
98
|
+
if (parent !== undefined && LOCALE_DIR_NAMES.includes(parent) && isLocaleCode(file.slice(0, -5))) {
|
|
99
|
+
dir = parts.slice(0, -1).join('/');
|
|
100
|
+
locale = file.slice(0, -5);
|
|
101
|
+
layout = 'flat';
|
|
102
|
+
}
|
|
103
|
+
else if (parts.length >= 3 && LOCALE_DIR_NAMES.includes(parts[parts.length - 3]) && isLocaleCode(parent)) {
|
|
104
|
+
dir = parts.slice(0, -2).join('/');
|
|
105
|
+
locale = parent;
|
|
106
|
+
layout = 'namespaced';
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const id = `${dir}\u0000${layout}`;
|
|
112
|
+
const set = sets.get(id) ?? { dir, layout, files: {} };
|
|
113
|
+
((_a = set.files)[locale] ?? (_a[locale] = [])).push(rel);
|
|
114
|
+
sets.set(id, set);
|
|
115
|
+
}
|
|
116
|
+
return [...sets.values()];
|
|
117
|
+
}
|
|
118
|
+
async function findLocaleFileSets(projectDir, options = {}) {
|
|
119
|
+
const names = `{${LOCALE_DIR_NAMES.join(',')}}`;
|
|
120
|
+
const ignoreDirs = options.ignoreDirs ?? exports.DEFAULT_LOCALE_IGNORE_DIRS;
|
|
121
|
+
const found = await (0, fast_glob_1.default)([`**/${names}/*.json`, `**/${names}/*/*.json`], {
|
|
122
|
+
cwd: projectDir,
|
|
123
|
+
ignore: ignoreDirs.map((d) => `**/${d}/**`),
|
|
124
|
+
onlyFiles: true,
|
|
125
|
+
deep: 8,
|
|
126
|
+
followSymbolicLinks: false,
|
|
127
|
+
});
|
|
128
|
+
return groupLocaleFiles(found, options);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Read and flatten one locale of a set. Namespaced files are keyed `ns:key`.
|
|
132
|
+
* `read` reads another version of the files (e.g. from git); by default the
|
|
133
|
+
* working tree is read.
|
|
134
|
+
*/
|
|
135
|
+
async function loadLocale(projectDir, set, locale, read = (rel) => fs_1.promises.readFile(path.join(projectDir, rel), 'utf8').catch((err) => {
|
|
136
|
+
if (err.code === 'ENOENT')
|
|
137
|
+
return null;
|
|
138
|
+
throw err;
|
|
139
|
+
})) {
|
|
140
|
+
const files = set.files[locale] ?? [];
|
|
141
|
+
const messages = {};
|
|
142
|
+
const errors = [];
|
|
143
|
+
for (const rel of files) {
|
|
144
|
+
try {
|
|
145
|
+
const content = await read(rel);
|
|
146
|
+
if (content === null)
|
|
147
|
+
continue;
|
|
148
|
+
const data = JSON.parse(content);
|
|
149
|
+
const flat = (0, locale_qa_1.flattenLocaleMessages)(data);
|
|
150
|
+
const ns = set.layout === 'namespaced' ? path.basename(rel, '.json') : null;
|
|
151
|
+
for (const [k, v] of Object.entries(flat))
|
|
152
|
+
messages[ns ? `${ns}:${k}` : k] = v;
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
errors.push({ file: toPosix(rel), message: err instanceof SyntaxError ? `invalid JSON: ${err.message}` : err.message });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { locale, files, messages, errors };
|
|
159
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type LocaleIssueCode = 'missing_key' | 'extra_key' | 'empty_value' | 'untranslated' | 'placeholder_mismatch' | 'icu_invalid' | 'icu_mismatch' | 'plural_category_invalid' | 'plural_category_missing' | 'glossary_violation'
|
|
2
|
+
/** The source text changed but the translation didn't (review of a diff). */
|
|
3
|
+
| 'stale_translation'
|
|
4
|
+
/** A translation present at the base of a diff was deleted while the source still has it. */
|
|
5
|
+
| 'translation_removed'
|
|
6
|
+
/** Pseudo-localized test output (`--provider pseudo`, ⟦…⟧), not a translation. */
|
|
7
|
+
| 'pseudo_translation';
|
|
8
|
+
export type LocaleIssueSeverity = 'error' | 'warning' | 'info';
|
|
9
|
+
export interface LocaleIssue {
|
|
10
|
+
locale: string;
|
|
11
|
+
key: string;
|
|
12
|
+
code: LocaleIssueCode;
|
|
13
|
+
severity: LocaleIssueSeverity;
|
|
14
|
+
/** What is wrong, in plain words. */
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
17
|
+
export interface LocaleGlossary {
|
|
18
|
+
/** Terms that must appear unchanged in every translation (brand names, …). */
|
|
19
|
+
neverTranslate?: string[];
|
|
20
|
+
/** Terms with a fixed translation per locale. */
|
|
21
|
+
alwaysTranslateAs?: Array<{
|
|
22
|
+
term: string;
|
|
23
|
+
translations: Record<string, string>;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
export interface LocalePairOptions {
|
|
27
|
+
/** Source locale code; values identical to the source aren't flagged for a locale of the same language. */
|
|
28
|
+
sourceLocale?: string;
|
|
29
|
+
glossary?: LocaleGlossary;
|
|
30
|
+
/**
|
|
31
|
+
* Only check these keys (e.g. the keys changed in a diff). Missing/extra
|
|
32
|
+
* detection is limited to them too.
|
|
33
|
+
*/
|
|
34
|
+
keys?: string[];
|
|
35
|
+
}
|
|
36
|
+
export interface LocalePairReport {
|
|
37
|
+
locale: string;
|
|
38
|
+
/** Keys in the source locale (plural groups count once). */
|
|
39
|
+
total: number;
|
|
40
|
+
/** Source keys (or plural groups) with a non-empty value in the target. */
|
|
41
|
+
translated: number;
|
|
42
|
+
/** Issue count by code. */
|
|
43
|
+
counts: Partial<Record<LocaleIssueCode, number>>;
|
|
44
|
+
errors: number;
|
|
45
|
+
warnings: number;
|
|
46
|
+
issues: LocaleIssue[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Flatten nested locale JSON into dotted keys with string values. The
|
|
50
|
+
* engine's `_meta` block is dropped; non-string leaves (numbers, arrays) are
|
|
51
|
+
* not messages and are skipped.
|
|
52
|
+
*/
|
|
53
|
+
export declare function flattenLocaleMessages(data: unknown, prefix?: string, out?: Record<string, string>): Record<string, string>;
|
|
54
|
+
/**
|
|
55
|
+
* Check one target locale against the source locale. Both maps are flat
|
|
56
|
+
* (see {@link flattenLocaleMessages}). Issues come in key order, errors and
|
|
57
|
+
* warnings mixed; callers cap and sort as they need.
|
|
58
|
+
*/
|
|
59
|
+
export declare function checkLocalePair(source: Record<string, string>, target: Record<string, string>, locale: string, options?: LocalePairOptions): LocalePairReport;
|
|
60
|
+
/** Recompute `counts`/`errors`/`warnings` from `issues` (after adding issues to a report). */
|
|
61
|
+
export declare function tallyLocaleIssues(report: LocalePairReport): LocalePairReport;
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.flattenLocaleMessages = flattenLocaleMessages;
|
|
4
|
+
exports.checkLocalePair = checkLocalePair;
|
|
5
|
+
exports.tallyLocaleIssues = tallyLocaleIssues;
|
|
6
|
+
/**
|
|
7
|
+
* QA of an existing target locale against its source locale, without
|
|
8
|
+
* translating anything: missing/extra keys, empty and untranslated values,
|
|
9
|
+
* lost or invented placeholders, broken ICU messages and wrong plural forms
|
|
10
|
+
* (ICU and i18next `_one`/`_other` suffix keys), and glossary violations.
|
|
11
|
+
*
|
|
12
|
+
* Pure: takes flat key → text maps, returns issues in plain words. Used by the
|
|
13
|
+
* MCP `babelize_check` / `babelize_review` tools; the checks are the same ones
|
|
14
|
+
* the translate phase applies to fresh translations (placeholder protection,
|
|
15
|
+
* ICU comparison).
|
|
16
|
+
*/
|
|
17
|
+
const icu_message_1 = require("../utils/icu-message");
|
|
18
|
+
const glossary_match_1 = require("../utils/glossary-match");
|
|
19
|
+
const rules_1 = require("../modules/glossary/rules");
|
|
20
|
+
const placeholder_protection_1 = require("../utils/placeholder-protection");
|
|
21
|
+
const pseudo_markers_1 = require("../utils/pseudo-markers");
|
|
22
|
+
const icu_spans_1 = require("../utils/icu-spans");
|
|
23
|
+
const PLURAL_CATEGORIES = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
|
24
|
+
const PLURAL_SUFFIX = /^(.+)_(zero|one|two|few|many|other)$/;
|
|
25
|
+
/**
|
|
26
|
+
* Flatten nested locale JSON into dotted keys with string values. The
|
|
27
|
+
* engine's `_meta` block is dropped; non-string leaves (numbers, arrays) are
|
|
28
|
+
* not messages and are skipped.
|
|
29
|
+
*/
|
|
30
|
+
function flattenLocaleMessages(data, prefix = '', out = {}) {
|
|
31
|
+
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
32
|
+
return out;
|
|
33
|
+
for (const [k, v] of Object.entries(data)) {
|
|
34
|
+
if (!prefix && k === '_meta')
|
|
35
|
+
continue;
|
|
36
|
+
const key = prefix ? `${prefix}.${k}` : k;
|
|
37
|
+
if (typeof v === 'string')
|
|
38
|
+
out[key] = v;
|
|
39
|
+
else if (v && typeof v === 'object' && !Array.isArray(v))
|
|
40
|
+
flattenLocaleMessages(v, key, out);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
function hasLetters(text) {
|
|
45
|
+
return /\p{L}/u.test(text);
|
|
46
|
+
}
|
|
47
|
+
function language(locale) {
|
|
48
|
+
return locale.toLowerCase().split(/[-_]/)[0];
|
|
49
|
+
}
|
|
50
|
+
function escapeRegex(s) {
|
|
51
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
52
|
+
}
|
|
53
|
+
/** The glossary as the translate phase applies it for `locale`. */
|
|
54
|
+
function resolveGlossary(glossary, locale) {
|
|
55
|
+
const rules = (0, rules_1.normalizeGlossary)([
|
|
56
|
+
...(glossary.neverTranslate ?? []).map((term) => ({ term, type: 'never_translate' })),
|
|
57
|
+
...(glossary.alwaysTranslateAs ?? []).map((e) => ({ term: e.term, type: 'always_translate_as', translations: e.translations })),
|
|
58
|
+
]);
|
|
59
|
+
return {
|
|
60
|
+
neverTranslate: rules.neverTranslate,
|
|
61
|
+
alwaysTranslateAs: (0, rules_1.fixedTermsFor)(rules, locale).map((g) => ({ term: g.term, translations: { [locale]: g.translation } })),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function containsTerm(text, term, ignoreCase) {
|
|
65
|
+
return new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRegex(term)}(?![\\p{L}\\p{N}_])`, ignoreCase ? 'iu' : 'u').test(text);
|
|
66
|
+
}
|
|
67
|
+
/** Protected spans (variables, tags, URLs, …) with how often each occurs. */
|
|
68
|
+
function spanCounts(text, icu) {
|
|
69
|
+
const counts = new Map();
|
|
70
|
+
for (const span of (0, placeholder_protection_1.protectText)(text, [], { icu }).tokens) {
|
|
71
|
+
// Whitespace inside a variable is not meaningful ({ name } == {name}).
|
|
72
|
+
const norm = /^[{%$]/.test(span) ? span.replace(/\s+/g, '') : span;
|
|
73
|
+
counts.set(norm, (counts.get(norm) ?? 0) + 1);
|
|
74
|
+
}
|
|
75
|
+
return counts;
|
|
76
|
+
}
|
|
77
|
+
/** Variables and tags break the UI when lost; a changed URL or email may be deliberate. */
|
|
78
|
+
function isStructural(span) {
|
|
79
|
+
return /^[{%$<]/.test(span);
|
|
80
|
+
}
|
|
81
|
+
function quoteList(spans) {
|
|
82
|
+
return spans.map((s) => (s.length > 40 ? `${s.slice(0, 37)}…` : s)).join(', ');
|
|
83
|
+
}
|
|
84
|
+
function icuIssueCode(issue) {
|
|
85
|
+
switch (issue.code) {
|
|
86
|
+
case 'icu_invalid':
|
|
87
|
+
return 'icu_invalid';
|
|
88
|
+
case 'icu_plural_category_invalid':
|
|
89
|
+
return 'plural_category_invalid';
|
|
90
|
+
case 'icu_plural_category_missing':
|
|
91
|
+
return 'plural_category_missing';
|
|
92
|
+
default:
|
|
93
|
+
return 'icu_mismatch';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const COUNT_SPAN = /^\{\{?-?\s*count\s*\}?\}$/;
|
|
97
|
+
function pluralSpans(forms) {
|
|
98
|
+
const counts = forms.map((f) => spanCounts(f, false));
|
|
99
|
+
const required = new Map();
|
|
100
|
+
const allowed = new Set();
|
|
101
|
+
for (const c of counts)
|
|
102
|
+
for (const span of c.keys())
|
|
103
|
+
allowed.add(span);
|
|
104
|
+
for (const span of allowed) {
|
|
105
|
+
if (COUNT_SPAN.test(span))
|
|
106
|
+
continue;
|
|
107
|
+
const min = Math.min(...counts.map((c) => c.get(span) ?? 0));
|
|
108
|
+
if (min > 0)
|
|
109
|
+
required.set(span, min);
|
|
110
|
+
}
|
|
111
|
+
return { required, allowed };
|
|
112
|
+
}
|
|
113
|
+
function checkValue(key, source, target, locale, options, push, plural) {
|
|
114
|
+
if (target.trim() === '') {
|
|
115
|
+
if (source.trim() !== '')
|
|
116
|
+
push({ key, code: 'empty_value', severity: 'error', message: 'Value is empty; the app shows a blank string.' });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if ((0, pseudo_markers_1.isPseudoLocalized)(target) && !(0, pseudo_markers_1.isPseudoLocalized)(source)) {
|
|
120
|
+
push({
|
|
121
|
+
key,
|
|
122
|
+
code: 'pseudo_translation',
|
|
123
|
+
severity: 'error',
|
|
124
|
+
message: 'Pseudo-localized test text (⟦…⟧ from --provider pseudo), not a translation. Run babelize translate with a real provider to replace it.',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const icu = (0, icu_message_1.isIcuMessage)(source) || (0, icu_message_1.isIcuMessage)(target);
|
|
128
|
+
if (icu) {
|
|
129
|
+
// The translate phase lets a translation add a plural over a count-like
|
|
130
|
+
// argument of a plain source ("{unreadCount} notes" → German one/other);
|
|
131
|
+
// QA accepts the same, or a correct run would fail `babelize check`.
|
|
132
|
+
const pluralizable = (0, icu_message_1.isIcuMessage)(source) ? [] : (0, icu_message_1.countArguments)(source);
|
|
133
|
+
for (const issue of (0, icu_message_1.compareIcuMessages)(source, target, locale, { pluralizable })) {
|
|
134
|
+
push({ key, code: icuIssueCode(issue), severity: issue.severity, message: `${issue.message.charAt(0).toUpperCase()}${issue.message.slice(1)}.` });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Placeholders, tags, URLs: every span in the source must be in the
|
|
138
|
+
// translation exactly as often (in ICU messages braces are structure and
|
|
139
|
+
// were compared above, so only tags/URLs/code are counted).
|
|
140
|
+
const tgt = spanCounts(target, icu);
|
|
141
|
+
const lost = [];
|
|
142
|
+
const added = [];
|
|
143
|
+
if (plural && !icu) {
|
|
144
|
+
// A plural form is checked against all of the key's source forms.
|
|
145
|
+
for (const [span, n] of plural.required)
|
|
146
|
+
if ((tgt.get(span) ?? 0) < n)
|
|
147
|
+
lost.push(span);
|
|
148
|
+
for (const span of tgt.keys())
|
|
149
|
+
if (!plural.allowed.has(span) && !COUNT_SPAN.test(span))
|
|
150
|
+
added.push(span);
|
|
151
|
+
}
|
|
152
|
+
else if (icu) {
|
|
153
|
+
// An ICU message repeats a tag once per branch, and a plural added over a
|
|
154
|
+
// plain source does too: compare which spans appear, not how often (as
|
|
155
|
+
// the translate phase does).
|
|
156
|
+
const src = spanCounts(source, icu);
|
|
157
|
+
for (const span of src.keys())
|
|
158
|
+
if (!tgt.has(span))
|
|
159
|
+
lost.push(span);
|
|
160
|
+
for (const span of tgt.keys())
|
|
161
|
+
if (!src.has(span))
|
|
162
|
+
added.push(span);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const src = spanCounts(source, icu);
|
|
166
|
+
for (const [span, n] of src)
|
|
167
|
+
if ((tgt.get(span) ?? 0) < n)
|
|
168
|
+
lost.push(span);
|
|
169
|
+
for (const [span, n] of tgt)
|
|
170
|
+
if ((src.get(span) ?? 0) < n)
|
|
171
|
+
added.push(span);
|
|
172
|
+
}
|
|
173
|
+
if (lost.length || added.length) {
|
|
174
|
+
const structural = [...lost, ...added].some(isStructural);
|
|
175
|
+
const parts = [
|
|
176
|
+
lost.length ? `lost ${quoteList(lost)}` : '',
|
|
177
|
+
added.length ? `added ${quoteList(added)}` : '',
|
|
178
|
+
].filter(Boolean);
|
|
179
|
+
push({
|
|
180
|
+
key,
|
|
181
|
+
code: 'placeholder_mismatch',
|
|
182
|
+
severity: structural ? 'error' : 'warning',
|
|
183
|
+
message: `Placeholders differ from the source: ${parts.join('; ')}.`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
else if (icu) {
|
|
187
|
+
// Present somewhere is not enough: each branch must carry its source
|
|
188
|
+
// branch's spans (or the whole plain source, for an added plural).
|
|
189
|
+
const branch = (0, icu_spans_1.branchSpanProblems)(source, target);
|
|
190
|
+
if (branch.length) {
|
|
191
|
+
const all = branch.flatMap((p) => [...p.lost, ...p.added]);
|
|
192
|
+
push({
|
|
193
|
+
key,
|
|
194
|
+
code: 'placeholder_mismatch',
|
|
195
|
+
severity: all.some(isStructural) ? 'error' : 'warning',
|
|
196
|
+
message: `Placeholders differ from the source in ${branch.map(icu_spans_1.describeBranchSpanProblem).join('; ')}.`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const neverTranslate = options.glossary?.neverTranslate ?? [];
|
|
201
|
+
if (target === source &&
|
|
202
|
+
hasLetters(source) &&
|
|
203
|
+
!(options.sourceLocale && language(options.sourceLocale) === language(locale)) &&
|
|
204
|
+
hasLetters((0, placeholder_protection_1.protectText)(source, neverTranslate, { icu }).text.replace(/\[#VAR_\d+#\]/g, ''))) {
|
|
205
|
+
const words = source.trim().split(/\s+/).filter(hasLetters).length;
|
|
206
|
+
push({
|
|
207
|
+
key,
|
|
208
|
+
code: 'untranslated',
|
|
209
|
+
severity: words >= 2 ? 'warning' : 'info',
|
|
210
|
+
message: words >= 2
|
|
211
|
+
? 'Same text as the source; looks untranslated.'
|
|
212
|
+
: 'Same text as the source (fine for names or codes, otherwise untranslated).',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
for (const term of neverTranslate) {
|
|
216
|
+
if (term.trim() && (0, rules_1.sourceHasTerm)(source, term) && !containsTerm(target, term, true)) {
|
|
217
|
+
push({ key, code: 'glossary_violation', severity: 'warning', message: `"${term}" must stay untranslated but is missing from the translation.` });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const entry of options.glossary?.alwaysTranslateAs ?? []) {
|
|
221
|
+
// resolveGlossary left only this locale's translation (exact, else base language).
|
|
222
|
+
const expected = (0, rules_1.localeValue)(entry.translations, locale);
|
|
223
|
+
if (!expected || !entry.term.trim())
|
|
224
|
+
continue;
|
|
225
|
+
// Same matcher as the translate phase: inflected forms count (Polish "obszaru roboczego").
|
|
226
|
+
if ((0, rules_1.sourceHasTerm)(source, entry.term, neverTranslate) && !(0, glossary_match_1.containsGlossaryTranslation)(target, expected)) {
|
|
227
|
+
push({ key, code: 'glossary_violation', severity: 'warning', message: `"${entry.term}" should be translated as "${expected}".` });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** Plural groups (i18next `key_one` / `key_other`) in a flat map: base → category → key. */
|
|
232
|
+
function pluralGroups(flat) {
|
|
233
|
+
const groups = new Map();
|
|
234
|
+
for (const key of Object.keys(flat)) {
|
|
235
|
+
const m = PLURAL_SUFFIX.exec(key);
|
|
236
|
+
if (!m)
|
|
237
|
+
continue;
|
|
238
|
+
const group = groups.get(m[1]) ?? new Map();
|
|
239
|
+
group.set(m[2], key);
|
|
240
|
+
groups.set(m[1], group);
|
|
241
|
+
}
|
|
242
|
+
return groups;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Check one target locale against the source locale. Both maps are flat
|
|
246
|
+
* (see {@link flattenLocaleMessages}). Issues come in key order, errors and
|
|
247
|
+
* warnings mixed; callers cap and sort as they need.
|
|
248
|
+
*/
|
|
249
|
+
function checkLocalePair(source, target, locale, options = {}) {
|
|
250
|
+
// Resolved the way the translate phase resolves it (case-insensitive
|
|
251
|
+
// terms, never-translate wins a conflict), so QA expects what translation does.
|
|
252
|
+
if (options.glossary)
|
|
253
|
+
options = { ...options, glossary: resolveGlossary(options.glossary, locale) };
|
|
254
|
+
const issues = [];
|
|
255
|
+
const push = (issue) => issues.push({ locale, ...issue });
|
|
256
|
+
const only = options.keys ? new Set(options.keys) : null;
|
|
257
|
+
const inScope = (key) => !only || only.has(key);
|
|
258
|
+
// A suffix group counts as plural only when the source has its `_other`
|
|
259
|
+
// form; `step_one` alone is just a key name.
|
|
260
|
+
const sourceGroups = new Map([...pluralGroups(source)].filter(([, g]) => g.has('other')));
|
|
261
|
+
const targetGroups = pluralGroups(target);
|
|
262
|
+
const pluralKeys = new Set();
|
|
263
|
+
for (const [base, group] of sourceGroups) {
|
|
264
|
+
for (const key of group.values())
|
|
265
|
+
pluralKeys.add(key);
|
|
266
|
+
for (const key of targetGroups.get(base)?.values() ?? [])
|
|
267
|
+
pluralKeys.add(key);
|
|
268
|
+
}
|
|
269
|
+
let total = 0;
|
|
270
|
+
let translated = 0;
|
|
271
|
+
for (const key of Object.keys(source)) {
|
|
272
|
+
if (pluralKeys.has(key) || !inScope(key))
|
|
273
|
+
continue;
|
|
274
|
+
total++;
|
|
275
|
+
const value = target[key];
|
|
276
|
+
if (value === undefined) {
|
|
277
|
+
push({ key, code: 'missing_key', severity: 'error', message: 'Missing in this locale; the app falls back to the source text.' });
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (value.trim() !== '')
|
|
281
|
+
translated++;
|
|
282
|
+
checkValue(key, source[key], value, locale, options, push);
|
|
283
|
+
}
|
|
284
|
+
const required = (0, icu_message_1.requiredPluralCategories)(locale).cardinal;
|
|
285
|
+
for (const [base, group] of sourceGroups) {
|
|
286
|
+
const groupKeys = [...group.values(), ...(targetGroups.get(base)?.values() ?? [])];
|
|
287
|
+
if (only && !groupKeys.some((k) => only.has(k)))
|
|
288
|
+
continue;
|
|
289
|
+
total++;
|
|
290
|
+
const label = `${base}_*`;
|
|
291
|
+
const present = targetGroups.get(base) ?? new Map();
|
|
292
|
+
if (present.size === 0) {
|
|
293
|
+
push({ key: label, code: 'missing_key', severity: 'error', message: 'Plural forms are missing in this locale; the app falls back to the source text.' });
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
translated++;
|
|
297
|
+
const missing = required.filter((c) => !present.has(c));
|
|
298
|
+
if (missing.length) {
|
|
299
|
+
push({
|
|
300
|
+
key: label,
|
|
301
|
+
code: 'plural_category_missing',
|
|
302
|
+
severity: 'error',
|
|
303
|
+
message: `Missing plural form${missing.length > 1 ? 's' : ''} ${missing.map((c) => `_${c}`).join(', ')} that ${locale} needs; those counts fall back to the source text.`,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
// i18next resolves `_zero` for count 0 in every language: never unused.
|
|
307
|
+
const unused = [...present.keys()].filter((c) => c !== 'zero' && !required.includes(c) && PLURAL_CATEGORIES.includes(c));
|
|
308
|
+
if (unused.length) {
|
|
309
|
+
push({
|
|
310
|
+
key: label,
|
|
311
|
+
code: 'plural_category_invalid',
|
|
312
|
+
severity: 'warning',
|
|
313
|
+
message: `Has plural form${unused.length > 1 ? 's' : ''} ${unused.map((c) => `_${c}`).join(', ')}, which ${locale} doesn't use (valid: ${required.join(', ')}).`,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
const spans = pluralSpans([...group.values()].map((k) => source[k]));
|
|
317
|
+
for (const [category, key] of present) {
|
|
318
|
+
const src = source[group.get(category) ?? group.get('other')];
|
|
319
|
+
checkValue(key, src, target[key], locale, options, push, spans);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
for (const key of Object.keys(target)) {
|
|
323
|
+
if (key in source || pluralKeys.has(key) || !inScope(key))
|
|
324
|
+
continue;
|
|
325
|
+
push({ key, code: 'extra_key', severity: 'info', message: 'Not in the source locale (stale or renamed key).' });
|
|
326
|
+
}
|
|
327
|
+
return tallyLocaleIssues({ locale, total, translated, counts: {}, errors: 0, warnings: 0, issues });
|
|
328
|
+
}
|
|
329
|
+
/** Recompute `counts`/`errors`/`warnings` from `issues` (after adding issues to a report). */
|
|
330
|
+
function tallyLocaleIssues(report) {
|
|
331
|
+
const counts = {};
|
|
332
|
+
let errors = 0;
|
|
333
|
+
let warnings = 0;
|
|
334
|
+
for (const issue of report.issues) {
|
|
335
|
+
counts[issue.code] = (counts[issue.code] ?? 0) + 1;
|
|
336
|
+
if (issue.severity === 'error')
|
|
337
|
+
errors++;
|
|
338
|
+
else if (issue.severity === 'warning')
|
|
339
|
+
warnings++;
|
|
340
|
+
}
|
|
341
|
+
return { ...report, counts, errors, warnings };
|
|
342
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type FindLocaleOptions, type LocaleFileSet } from './locale-files';
|
|
2
|
+
import { type LocaleGlossary, type LocalePairReport } from './locale-qa';
|
|
3
|
+
export interface ReviewOptions {
|
|
4
|
+
/** Ref to compare against (branch, tag, commit). Default HEAD (uncommitted changes). */
|
|
5
|
+
base?: string;
|
|
6
|
+
sourceLocale?: string;
|
|
7
|
+
/** Only these target locales. */
|
|
8
|
+
locales?: string[];
|
|
9
|
+
glossary?: LocaleGlossary;
|
|
10
|
+
/** Directory names skipped when looking for locale files. */
|
|
11
|
+
ignoreDirs?: FindLocaleOptions['ignoreDirs'];
|
|
12
|
+
}
|
|
13
|
+
export interface LocaleSetChangeReview {
|
|
14
|
+
dir: string;
|
|
15
|
+
layout: LocaleFileSet['layout'];
|
|
16
|
+
sourceLocale: string;
|
|
17
|
+
/** Source-locale keys added, changed and removed by the diff. */
|
|
18
|
+
source: {
|
|
19
|
+
added: string[];
|
|
20
|
+
changed: string[];
|
|
21
|
+
removed: string[];
|
|
22
|
+
};
|
|
23
|
+
/** Target locales touched by the diff (or affected by source changes). */
|
|
24
|
+
locales: Array<LocalePairReport & {
|
|
25
|
+
changedKeys: number;
|
|
26
|
+
removedKeys: number;
|
|
27
|
+
fileDeleted: boolean;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
export interface LocaleChangeReview {
|
|
31
|
+
/** What the working tree was compared with. */
|
|
32
|
+
base: string;
|
|
33
|
+
/** Changed locale files, relative to the project. */
|
|
34
|
+
changedLocaleFiles: string[];
|
|
35
|
+
/** Other changed files (code), relative to the project. */
|
|
36
|
+
changedOtherFiles: number;
|
|
37
|
+
sets: LocaleSetChangeReview[];
|
|
38
|
+
}
|
|
39
|
+
export declare class ReviewError extends Error {
|
|
40
|
+
readonly code: 'not_a_repo' | 'git_missing' | 'bad_base';
|
|
41
|
+
constructor(message: string, code: 'not_a_repo' | 'git_missing' | 'bad_base');
|
|
42
|
+
}
|
|
43
|
+
export declare function reviewLocaleChanges(projectDir: string, options?: ReviewOptions): Promise<LocaleChangeReview>;
|