@babelize/engine 0.1.0 → 0.1.3
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/analysis/analyze.js +1 -1
- 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 +5 -0
- package/dist/index.js +16 -2
- package/dist/local/pseudo-provider.js +5 -4
- package/dist/local/translate-core.d.ts +5 -2
- package/dist/local/translate-core.js +43 -5
- package/dist/local/translate-messages.js +7 -9
- package/dist/modules/extractor/extract.js +22 -14
- package/dist/modules/finalization/django-finalize.d.ts +15 -0
- package/dist/modules/finalization/django-finalize.js +156 -19
- package/dist/modules/finalization/finalize.d.ts +2 -0
- package/dist/modules/finalization/finalize.js +58 -30
- package/dist/modules/finalization/nextjs-finalize.d.ts +4 -1
- package/dist/modules/finalization/nextjs-finalize.js +8 -9
- package/dist/modules/finalization/writes.d.ts +29 -0
- package/dist/modules/finalization/writes.js +78 -0
- package/dist/modules/format-injection/index.js +3 -2
- package/dist/modules/glossary/index.d.ts +1 -2
- package/dist/modules/glossary/index.js +14 -6
- package/dist/modules/glossary/rules.d.ts +81 -0
- package/dist/modules/glossary/rules.js +244 -0
- package/dist/modules/injection/inject.js +14 -30
- package/dist/modules/keygen/keygen.js +7 -0
- package/dist/modules/locale-builder/locales.js +23 -0
- package/dist/modules/lockfile/index.d.ts +1 -1
- package/dist/modules/lockfile/index.js +2 -1
- package/dist/modules/lockfile/lockfile-manager.d.ts +9 -0
- package/dist/modules/lockfile/lockfile-manager.js +16 -1
- package/dist/modules/lockfile/schema.d.ts +42 -0
- package/dist/modules/lockfile/schema.js +11 -0
- package/dist/modules/translator/icu-branches.d.ts +138 -0
- package/dist/modules/translator/icu-branches.js +393 -0
- package/dist/modules/translator/translate.d.ts +40 -4
- package/dist/modules/translator/translate.js +297 -173
- package/dist/modules/workspace/validate.js +67 -32
- package/dist/orchestrator.d.ts +5 -0
- package/dist/orchestrator.js +125 -14
- package/dist/providers/types.d.ts +10 -2
- package/dist/qa/check-project.d.ts +2 -1
- package/dist/qa/check-project.js +50 -7
- package/dist/qa/gettext-files.d.ts +15 -0
- package/dist/qa/gettext-files.js +115 -0
- package/dist/qa/locale-qa.js +20 -3
- package/dist/runtime/config.js +1 -0
- package/dist/runtime/types.d.ts +64 -0
- package/dist/utils/extraction-options.d.ts +9 -3
- package/dist/utils/extraction-options.js +13 -3
- package/dist/utils/file-utils.js +7 -2
- package/dist/utils/icu-message.d.ts +2 -2
- package/dist/utils/icu-message.js +43 -7
- package/dist/utils/jsx-message.js +7 -0
- package/dist/utils/metadata.d.ts +4 -2
- package/dist/utils/metadata.js +16 -10
- package/dist/utils/not-injected.js +2 -0
- package/dist/utils/placeholder-protection.d.ts +18 -1
- package/dist/utils/placeholder-protection.js +58 -13
- package/dist/utils/translation-sanitizer.d.ts +1 -0
- package/dist/utils/translation-sanitizer.js +23 -9
- package/dist/utils/translator-load-balancer.js +5 -1
- package/dist/utils/untranslated.d.ts +15 -0
- package/dist/utils/untranslated.js +34 -0
- package/package.json +2 -6
- package/dist/injection/django/django-template-injector.d.ts +0 -29
- package/dist/injection/django/django-template-injector.js +0 -216
- package/dist/modules/glossary/glossary.d.ts +0 -9
- package/dist/modules/glossary/glossary.js +0 -27
- package/dist/modules/glossary/masker.d.ts +0 -23
- package/dist/modules/glossary/masker.js +0 -56
- package/dist/modules/locale-builder/po.d.ts +0 -33
- package/dist/modules/locale-builder/po.js +0 -131
- package/dist/modules/translator/translator.d.ts +0 -5
- package/dist/modules/translator/translator.js +0 -46
- package/dist/utils/dtl-mask.d.ts +0 -25
- package/dist/utils/dtl-mask.js +0 -58
- package/dist/utils/py-extractor.d.ts +0 -14
- package/dist/utils/py-extractor.js +0 -285
|
@@ -0,0 +1,115 @@
|
|
|
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.findGettextSets = findGettextSets;
|
|
40
|
+
exports.loadGettextSet = loadGettextSet;
|
|
41
|
+
/**
|
|
42
|
+
* Django/gettext catalogues for the locale checks: every
|
|
43
|
+
* `<locale dir>/<lang>/LC_MESSAGES/django.po` set. gettext has no source
|
|
44
|
+
* file — the msgids are the source text — so the source side of a check is
|
|
45
|
+
* the set's msgids. Fuzzy and empty entries count as missing (Django ignores
|
|
46
|
+
* them at runtime).
|
|
47
|
+
*/
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const fs_1 = require("fs");
|
|
50
|
+
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
51
|
+
const po_file_1 = require("../django/po-file");
|
|
52
|
+
const locale_names_1 = require("../django/locale-names");
|
|
53
|
+
const locale_files_1 = require("./locale-files");
|
|
54
|
+
async function findGettextSets(projectDir, ignoreDirs = locale_files_1.DEFAULT_LOCALE_IGNORE_DIRS) {
|
|
55
|
+
const found = await (0, fast_glob_1.default)('**/*/LC_MESSAGES/django.po', {
|
|
56
|
+
cwd: projectDir,
|
|
57
|
+
ignore: [...ignoreDirs, 'venv', '.venv', 'site-packages'].map((d) => `**/${d}/**`),
|
|
58
|
+
onlyFiles: true,
|
|
59
|
+
deep: 8,
|
|
60
|
+
followSymbolicLinks: false,
|
|
61
|
+
});
|
|
62
|
+
const sets = new Map();
|
|
63
|
+
for (const rel of found.sort()) {
|
|
64
|
+
const parts = rel.split('/');
|
|
65
|
+
const langDir = parts[parts.length - 3];
|
|
66
|
+
const locale = (0, locale_names_1.localeFromDjangoName)(langDir);
|
|
67
|
+
if (!(0, locale_files_1.isLocaleCode)(locale))
|
|
68
|
+
continue;
|
|
69
|
+
const dir = parts.slice(0, -3).join('/') || '.';
|
|
70
|
+
const set = sets.get(dir) ?? { dir, files: {} };
|
|
71
|
+
set.files[locale] = rel;
|
|
72
|
+
sets.set(dir, set);
|
|
73
|
+
}
|
|
74
|
+
return [...sets.values()];
|
|
75
|
+
}
|
|
76
|
+
/** Message key for a catalogue entry in check reports. */
|
|
77
|
+
function messageKey(msgctxt, msgid) {
|
|
78
|
+
return msgctxt !== undefined ? `${msgctxt}|${msgid}` : msgid;
|
|
79
|
+
}
|
|
80
|
+
async function loadGettextSet(projectDir, set) {
|
|
81
|
+
const source = {};
|
|
82
|
+
const byLocale = {};
|
|
83
|
+
const errors = [];
|
|
84
|
+
for (const [locale, rel] of Object.entries(set.files)) {
|
|
85
|
+
const messages = {};
|
|
86
|
+
byLocale[locale] = messages;
|
|
87
|
+
let content;
|
|
88
|
+
try {
|
|
89
|
+
content = await fs_1.promises.readFile(path.join(projectDir, rel), 'utf8');
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
errors.push({ file: rel, message: err.message });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
let po;
|
|
96
|
+
try {
|
|
97
|
+
po = (0, po_file_1.parsePo)(content);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
errors.push({ file: rel, message: `invalid PO file: ${err.message}` });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
for (const e of po.entries) {
|
|
104
|
+
if (e.obsolete)
|
|
105
|
+
continue;
|
|
106
|
+
const key = messageKey(e.msgctxt, e.msgid);
|
|
107
|
+
// Plural entries are checked through their plural form (it carries the count placeholder).
|
|
108
|
+
source[key] = e.msgidPlural ?? e.msgid;
|
|
109
|
+
if (e.flags.includes('fuzzy') || e.msgstr.length === 0 || e.msgstr.some((s) => !s))
|
|
110
|
+
continue;
|
|
111
|
+
messages[key] = e.msgstr[e.msgstr.length - 1];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { source, byLocale, errors };
|
|
115
|
+
}
|
package/dist/qa/locale-qa.js
CHANGED
|
@@ -16,6 +16,7 @@ exports.tallyLocaleIssues = tallyLocaleIssues;
|
|
|
16
16
|
*/
|
|
17
17
|
const icu_message_1 = require("../utils/icu-message");
|
|
18
18
|
const glossary_match_1 = require("../utils/glossary-match");
|
|
19
|
+
const rules_1 = require("../modules/glossary/rules");
|
|
19
20
|
const placeholder_protection_1 = require("../utils/placeholder-protection");
|
|
20
21
|
const pseudo_markers_1 = require("../utils/pseudo-markers");
|
|
21
22
|
const icu_spans_1 = require("../utils/icu-spans");
|
|
@@ -49,6 +50,17 @@ function language(locale) {
|
|
|
49
50
|
function escapeRegex(s) {
|
|
50
51
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
51
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
|
+
}
|
|
52
64
|
function containsTerm(text, term, ignoreCase) {
|
|
53
65
|
return new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRegex(term)}(?![\\p{L}\\p{N}_])`, ignoreCase ? 'iu' : 'u').test(text);
|
|
54
66
|
}
|
|
@@ -201,16 +213,17 @@ function checkValue(key, source, target, locale, options, push, plural) {
|
|
|
201
213
|
});
|
|
202
214
|
}
|
|
203
215
|
for (const term of neverTranslate) {
|
|
204
|
-
if (term.trim() &&
|
|
216
|
+
if (term.trim() && (0, rules_1.sourceHasTerm)(source, term) && !containsTerm(target, term, true)) {
|
|
205
217
|
push({ key, code: 'glossary_violation', severity: 'warning', message: `"${term}" must stay untranslated but is missing from the translation.` });
|
|
206
218
|
}
|
|
207
219
|
}
|
|
208
220
|
for (const entry of options.glossary?.alwaysTranslateAs ?? []) {
|
|
209
|
-
|
|
221
|
+
// resolveGlossary left only this locale's translation (exact, else base language).
|
|
222
|
+
const expected = (0, rules_1.localeValue)(entry.translations, locale);
|
|
210
223
|
if (!expected || !entry.term.trim())
|
|
211
224
|
continue;
|
|
212
225
|
// Same matcher as the translate phase: inflected forms count (Polish "obszaru roboczego").
|
|
213
|
-
if (
|
|
226
|
+
if ((0, rules_1.sourceHasTerm)(source, entry.term, neverTranslate) && !(0, glossary_match_1.containsGlossaryTranslation)(target, expected)) {
|
|
214
227
|
push({ key, code: 'glossary_violation', severity: 'warning', message: `"${entry.term}" should be translated as "${expected}".` });
|
|
215
228
|
}
|
|
216
229
|
}
|
|
@@ -234,6 +247,10 @@ function pluralGroups(flat) {
|
|
|
234
247
|
* warnings mixed; callers cap and sort as they need.
|
|
235
248
|
*/
|
|
236
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) };
|
|
237
254
|
const issues = [];
|
|
238
255
|
const push = (issue) => issues.push({ locale, ...issue });
|
|
239
256
|
const only = options.keys ? new Set(options.keys) : null;
|
package/dist/runtime/config.js
CHANGED
|
@@ -25,6 +25,7 @@ function validateEngineConfigV2(config) {
|
|
|
25
25
|
dryRun: config.dryRun ?? false,
|
|
26
26
|
forceTranslation: config.forceTranslation ?? false,
|
|
27
27
|
onProgress: config.onProgress,
|
|
28
|
+
signal: config.signal,
|
|
28
29
|
projectId: config.projectId,
|
|
29
30
|
filesToProcess: config.filesToProcess,
|
|
30
31
|
byok: config.byok,
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -35,6 +35,13 @@ export interface EngineConfigV2 {
|
|
|
35
35
|
dataDir?: string;
|
|
36
36
|
/** Logger for this run only (default: the one set with setEngineLogger). */
|
|
37
37
|
logger?: EngineLogger;
|
|
38
|
+
/**
|
|
39
|
+
* Stops the run cooperatively: checked at every progress report, so the
|
|
40
|
+
* pipeline ends at the next phase or translation batch after it fires. The
|
|
41
|
+
* result then has `aborted` set to the signal's reason (e.g. 'cancelled',
|
|
42
|
+
* 'timeout').
|
|
43
|
+
*/
|
|
44
|
+
signal?: AbortSignal;
|
|
38
45
|
/** Callback for progress updates */
|
|
39
46
|
onProgress?: (message: string, step?: string, status?: 'running' | 'success' | 'error' | 'warning') => void;
|
|
40
47
|
/**
|
|
@@ -83,6 +90,12 @@ export interface WorkspaceInfo {
|
|
|
83
90
|
packageJson?: Record<string, any>;
|
|
84
91
|
/** Output locale file format (e.g., 'json' for React, 'po' for Django) */
|
|
85
92
|
localeFormat: 'json' | 'po' | 'arb';
|
|
93
|
+
/**
|
|
94
|
+
* Django: the project root (the directory holding manage.py, i.e. BASE_DIR),
|
|
95
|
+
* relative to the workspace. For gettext projects `publicDir` is the locale
|
|
96
|
+
* directory (LOCALE_PATHS), relative to the workspace.
|
|
97
|
+
*/
|
|
98
|
+
djangoRoot?: string;
|
|
86
99
|
/** Format localization config propagated from EngineConfigV2 */
|
|
87
100
|
formatConfig?: FormatLocaleConfig;
|
|
88
101
|
/** Static HTML runtime options propagated from EngineConfigV2 */
|
|
@@ -132,6 +145,27 @@ export interface ExtractedString {
|
|
|
132
145
|
* exactly these nodes (not others with the same text on the same line).
|
|
133
146
|
*/
|
|
134
147
|
offsets?: number[];
|
|
148
|
+
/**
|
|
149
|
+
* gettext message (Django): `text` is the msgid. Keys are derived from the
|
|
150
|
+
* message itself, and the PO catalogue — not a key — is what code looks up.
|
|
151
|
+
*/
|
|
152
|
+
gettext?: GettextInfo;
|
|
153
|
+
}
|
|
154
|
+
export interface GettextInfo {
|
|
155
|
+
msgid: string;
|
|
156
|
+
msgidPlural?: string;
|
|
157
|
+
msgctxt?: string;
|
|
158
|
+
/** PO flags (python-format, python-brace-format). */
|
|
159
|
+
flags: string[];
|
|
160
|
+
/** Translator comments from the code (`# Translators: …`). */
|
|
161
|
+
comments: string[];
|
|
162
|
+
/** Already marked in the code (gettext call / trans tag); otherwise the injector wraps it. */
|
|
163
|
+
wrapped: boolean;
|
|
164
|
+
/**
|
|
165
|
+
* Translate a plural message as one ICU plural (an LLM can produce every
|
|
166
|
+
* CLDR category); otherwise singular and plural are translated separately.
|
|
167
|
+
*/
|
|
168
|
+
icu?: boolean;
|
|
135
169
|
}
|
|
136
170
|
export interface ExtractionResult {
|
|
137
171
|
strings: ExtractedString[];
|
|
@@ -150,6 +184,10 @@ export interface GeneratedKey {
|
|
|
150
184
|
pluralCategory?: 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
|
|
151
185
|
parentContext?: string;
|
|
152
186
|
extractionContext?: string;
|
|
187
|
+
/** gettext message this key translates, and which part of it (Django). */
|
|
188
|
+
gettext?: GettextInfo & {
|
|
189
|
+
form: 'single' | 'icu' | 'singular' | 'plural';
|
|
190
|
+
};
|
|
153
191
|
}
|
|
154
192
|
export interface KeyGenerationResult {
|
|
155
193
|
keys: GeneratedKey[];
|
|
@@ -167,6 +205,22 @@ export interface LocaleFile {
|
|
|
167
205
|
data: LocaleData;
|
|
168
206
|
isNew: boolean;
|
|
169
207
|
isRTL?: boolean;
|
|
208
|
+
/**
|
|
209
|
+
* Exact file content to write at `path` (e.g. a merged .po catalogue).
|
|
210
|
+
* When set, hosts write this instead of serializing `data` as JSON.
|
|
211
|
+
*/
|
|
212
|
+
content?: string;
|
|
213
|
+
/**
|
|
214
|
+
* There is no file for this locale (the source language of a gettext
|
|
215
|
+
* project: the code itself is the catalogue). Hosts write nothing.
|
|
216
|
+
*/
|
|
217
|
+
noFile?: boolean;
|
|
218
|
+
/** Further files generated for this locale (e.g. a compiled .mo next to the .po). */
|
|
219
|
+
extraFiles?: Array<{
|
|
220
|
+
path: string;
|
|
221
|
+
content: string;
|
|
222
|
+
encoding: 'utf8' | 'base64';
|
|
223
|
+
}>;
|
|
170
224
|
}
|
|
171
225
|
export interface LocaleHandlingResult {
|
|
172
226
|
sourceLocale: LocaleFile;
|
|
@@ -222,6 +276,8 @@ export interface FileInjection {
|
|
|
222
276
|
injections: InjectionLocation[];
|
|
223
277
|
importAdded: boolean;
|
|
224
278
|
hookAdded: boolean;
|
|
279
|
+
/** The run created this file (it was not in the project before), e.g. a generated i18n config. */
|
|
280
|
+
created?: boolean;
|
|
225
281
|
}
|
|
226
282
|
export interface InjectionResult {
|
|
227
283
|
modifiedFiles: FileInjection[];
|
|
@@ -297,6 +353,14 @@ export interface KeyUsageReport {
|
|
|
297
353
|
export interface EngineResultV2 {
|
|
298
354
|
success: boolean;
|
|
299
355
|
error?: string;
|
|
356
|
+
/** Set when the run was stopped through `signal`: the abort reason. */
|
|
357
|
+
aborted?: string;
|
|
358
|
+
/**
|
|
359
|
+
* Only with `aborted`: strings freshly translated (and written to the
|
|
360
|
+
* translation cache and TM) before the stop. A later run reuses them for
|
|
361
|
+
* free, so the caller bills them rather than refunding the whole run.
|
|
362
|
+
*/
|
|
363
|
+
partialTranslations?: number;
|
|
300
364
|
/**
|
|
301
365
|
* Keys that could not be translated, per locale, with the reason. They are
|
|
302
366
|
* absent from the locale files and the lockfile, so the next run retries
|
|
@@ -13,11 +13,17 @@ export interface ResolvedIcuOptions {
|
|
|
13
13
|
* with i18next-icu. Not vue-i18n, the vanilla runtime or static HTML.
|
|
14
14
|
*/
|
|
15
15
|
icuRuntime: boolean;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* A translation may add an ICU plural over a count (`{files} files`): ICU
|
|
18
|
+
* runtime. An LLM writes the plural; for NLLB it is built branch by branch.
|
|
19
|
+
*/
|
|
17
20
|
targetPlurals: boolean;
|
|
18
21
|
}
|
|
19
22
|
/**
|
|
20
|
-
* @param llm the run's translator is an LLM (BYOK or an injected provider)
|
|
21
|
-
*
|
|
23
|
+
* @param llm the run's translator is an LLM (BYOK or an injected provider).
|
|
24
|
+
* Only then do JSX sentences get ICU plurals in the source (that
|
|
25
|
+
* changes the source messages, and the sync check must agree).
|
|
26
|
+
* Translating ICU works with every translator: NLLB gets it
|
|
27
|
+
* branch by branch.
|
|
22
28
|
*/
|
|
23
29
|
export declare function resolveIcuOptions(workspace: WorkspaceInfo, llm: boolean): Promise<ResolvedIcuOptions>;
|
|
@@ -4,10 +4,20 @@ exports.resolveIcuOptions = resolveIcuOptions;
|
|
|
4
4
|
const StrategyFactory_1 = require("../injection/StrategyFactory");
|
|
5
5
|
const i18next_icu_1 = require("./i18next-icu");
|
|
6
6
|
/**
|
|
7
|
-
* @param llm the run's translator is an LLM (BYOK or an injected provider)
|
|
8
|
-
*
|
|
7
|
+
* @param llm the run's translator is an LLM (BYOK or an injected provider).
|
|
8
|
+
* Only then do JSX sentences get ICU plurals in the source (that
|
|
9
|
+
* changes the source messages, and the sync check must agree).
|
|
10
|
+
* Translating ICU works with every translator: NLLB gets it
|
|
11
|
+
* branch by branch.
|
|
9
12
|
*/
|
|
10
13
|
async function resolveIcuOptions(workspace, llm) {
|
|
14
|
+
if (workspace.framework === 'django') {
|
|
15
|
+
// gettext has its own plural forms (msgstr[n]). A plural message is
|
|
16
|
+
// translated as one ICU plural (an LLM whole, NLLB branch by branch with
|
|
17
|
+
// a number of each category) and mapped onto them. The keys are internal:
|
|
18
|
+
// the project's source never gains ICU.
|
|
19
|
+
return { sentences: false, plurals: true, icuRuntime: false, targetPlurals: false };
|
|
20
|
+
}
|
|
11
21
|
const deps = { ...workspace.packageJson?.dependencies, ...workspace.packageJson?.devDependencies };
|
|
12
22
|
const strategy = ['react', 'nextjs'].includes(workspace.framework) ? StrategyFactory_1.StrategyFactory.getStrategy(workspace.framework, deps).id : 'other';
|
|
13
23
|
let icuRuntime = false;
|
|
@@ -19,5 +29,5 @@ async function resolveIcuOptions(workspace, llm) {
|
|
|
19
29
|
icuRuntime = (await (0, i18next_icu_1.detectI18nextIcu)(workspace)).available;
|
|
20
30
|
sentences = icuRuntime;
|
|
21
31
|
}
|
|
22
|
-
return { sentences, plurals: icuRuntime && llm, icuRuntime, targetPlurals: icuRuntime
|
|
32
|
+
return { sentences, plurals: icuRuntime && llm, icuRuntime, targetPlurals: icuRuntime };
|
|
23
33
|
}
|
package/dist/utils/file-utils.js
CHANGED
|
@@ -52,6 +52,7 @@ exports.concurrentMap = concurrentMap;
|
|
|
52
52
|
const fs = __importStar(require("fs"));
|
|
53
53
|
const path = __importStar(require("path"));
|
|
54
54
|
const string_utils_1 = require("./string-utils");
|
|
55
|
+
const locale_names_1 = require("../django/locale-names");
|
|
55
56
|
/**
|
|
56
57
|
* Engine v2 file utils.
|
|
57
58
|
*
|
|
@@ -203,9 +204,13 @@ function localeExtension(format) {
|
|
|
203
204
|
*/
|
|
204
205
|
function localeFilePath(workspacePath, publicDir, localeFormat, targetLocale) {
|
|
205
206
|
if (localeFormat === 'po') {
|
|
206
|
-
|
|
207
|
+
// publicDir is the locale directory (LOCALE_PATHS) for gettext projects;
|
|
208
|
+
// Django names locale directories pt_BR, zh_Hans, …
|
|
209
|
+
const localeDir = publicDir && publicDir !== 'public' ? publicDir : 'locale';
|
|
210
|
+
const name = (0, locale_names_1.djangoLocaleName)(targetLocale);
|
|
211
|
+
const dir = path.join(workspacePath, localeDir, name, 'LC_MESSAGES');
|
|
207
212
|
const file = path.join(dir, 'django.po');
|
|
208
|
-
const relativeFile = path.join(
|
|
213
|
+
const relativeFile = path.join(localeDir, name, 'LC_MESSAGES', 'django.po');
|
|
209
214
|
return { dir, file, relativeFile };
|
|
210
215
|
}
|
|
211
216
|
const ext = localeExtension(localeFormat);
|
|
@@ -38,8 +38,8 @@ export interface IcuIssue {
|
|
|
38
38
|
/**
|
|
39
39
|
* Compare a translated ICU message to its source. Errors mean the message is
|
|
40
40
|
* broken at runtime (unparseable, arguments renamed, select branches changed,
|
|
41
|
-
* no `other
|
|
42
|
-
* falls back to `other` for
|
|
41
|
+
* no `other`, a target plural category whole numbers use). A missing category
|
|
42
|
+
* only fractions use is a warning: ICU falls back to `other` for it.
|
|
43
43
|
*/
|
|
44
44
|
export declare function compareIcuMessages(source: string, translated: string, targetLocale: string, options?: {
|
|
45
45
|
/**
|
|
@@ -190,11 +190,29 @@ function pluralCategories(locale, type) {
|
|
|
190
190
|
return [...CLDR_ORDER];
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Whole numbers (0–999) a plural category is used for, e.g. Czech few → 2, 3, 4,
|
|
195
|
+
* Czech many → none (only fractions like 1.5). CLDR integer rules repeat
|
|
196
|
+
* within every hundred, so this range covers them.
|
|
197
|
+
*/
|
|
198
|
+
function integersOf(locale, type, category) {
|
|
199
|
+
try {
|
|
200
|
+
const rules = new Intl.PluralRules(locale, { type });
|
|
201
|
+
const out = [];
|
|
202
|
+
for (let n = 0; n < 1000; n++)
|
|
203
|
+
if (rules.select(n) === category)
|
|
204
|
+
out.push(n);
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
193
211
|
/**
|
|
194
212
|
* Compare a translated ICU message to its source. Errors mean the message is
|
|
195
213
|
* broken at runtime (unparseable, arguments renamed, select branches changed,
|
|
196
|
-
* no `other
|
|
197
|
-
* falls back to `other` for
|
|
214
|
+
* no `other`, a target plural category whole numbers use). A missing category
|
|
215
|
+
* only fractions use is a warning: ICU falls back to `other` for it.
|
|
198
216
|
*/
|
|
199
217
|
function compareIcuMessages(source, translated, targetLocale, options = {}) {
|
|
200
218
|
let sourceAst;
|
|
@@ -291,11 +309,29 @@ function compareIcuMessages(source, translated, targetLocale, options = {}) {
|
|
|
291
309
|
}
|
|
292
310
|
const missing = allowed.filter((c) => !target.keys.includes(c));
|
|
293
311
|
if (missing.length) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
312
|
+
// A missing category whole numbers reach (Czech few: 2–4) shows the
|
|
313
|
+
// wrong form for common counts ("2 souborů"): an error. One only
|
|
314
|
+
// fractions reach (Czech many: 1.5), or whose numbers all have `=n`
|
|
315
|
+
// branches, falls back to `other` rarely: a warning.
|
|
316
|
+
const type = target.kind === 'selectordinal' ? 'ordinal' : 'cardinal';
|
|
317
|
+
const reached = missing
|
|
318
|
+
.map((c) => ({ c, ns: integersOf(targetLocale, type, c).filter((n) => !target.keys.includes(`=${n}`)) }))
|
|
319
|
+
.filter((m) => m.ns.length > 0);
|
|
320
|
+
if (reached.length) {
|
|
321
|
+
const examples = (ns) => `${ns.slice(0, 3).join(', ')}${ns.length > 3 ? ', …' : ''}`;
|
|
322
|
+
issues.push({
|
|
323
|
+
severity: 'error',
|
|
324
|
+
code: 'icu_plural_category_missing',
|
|
325
|
+
message: `{${target.name}, ${target.kind}} has no ${reached.map((m) => `${m.c} branch (${examples(m.ns)})`).join(', ')} for ${targetLocale}; those counts would get the "other" form`,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
issues.push({
|
|
330
|
+
severity: 'warning',
|
|
331
|
+
code: 'icu_plural_category_missing',
|
|
332
|
+
message: `{${target.name}, ${target.kind}} has no [${missing.join(', ')}] branch for ${targetLocale}; "other" will be used`,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
299
335
|
}
|
|
300
336
|
}
|
|
301
337
|
return issues;
|
|
@@ -432,6 +432,13 @@ function buildJsxMessage(node, options = {}) {
|
|
|
432
432
|
if (!(0, string_utils_1.isTranslatableText)(literal) || (0, string_utils_1.containsPII)(literal))
|
|
433
433
|
return { kind: 'none' };
|
|
434
434
|
const meaningful = pieces.filter((p) => p.type !== 'text' || p.value.trim() !== '');
|
|
435
|
+
// Sibling elements with nothing between them (nav links, a row of buttons)
|
|
436
|
+
// are separate strings, not one sentence: grouping them made
|
|
437
|
+
// "<link>Shop</link><link2>Your cart</link2>…", which can't be split for
|
|
438
|
+
// translators and loses its tags in machine translation. A sentence needs
|
|
439
|
+
// words or a value outside the inline elements.
|
|
440
|
+
if (!meaningful.some((p) => p.type !== 'tag'))
|
|
441
|
+
return { kind: 'none' };
|
|
435
442
|
const isSentence = placeholderCount + tagCount > 0 && (meaningful.length >= 2 || placeholderCount > 0);
|
|
436
443
|
if (!isSentence)
|
|
437
444
|
return { kind: 'none' };
|
package/dist/utils/metadata.d.ts
CHANGED
|
@@ -33,14 +33,16 @@ export interface BabelizeConfig {
|
|
|
33
33
|
excludePatterns: string[];
|
|
34
34
|
customTranslations: Record<string, Record<string, string>>;
|
|
35
35
|
}
|
|
36
|
-
export type FrameworkType = 'react' | 'nextjs' | 'vue' | 'angular' | 'svelte' | 'astro' | 'remix' | 'gatsby' | 'nuxt' | 'static' | 'unknown';
|
|
37
|
-
export type PackageManagerType = 'npm' | 'yarn' | 'pnpm' | 'bun';
|
|
36
|
+
export type FrameworkType = 'react' | 'nextjs' | 'vue' | 'angular' | 'svelte' | 'astro' | 'remix' | 'gatsby' | 'nuxt' | 'static' | 'django' | 'unknown';
|
|
37
|
+
export type PackageManagerType = 'npm' | 'yarn' | 'pnpm' | 'bun' | 'pip' | 'poetry' | 'pipenv' | 'uv' | 'pdm';
|
|
38
38
|
export interface ProjectInfo {
|
|
39
39
|
framework: FrameworkType;
|
|
40
40
|
frameworks?: FrameworkType[];
|
|
41
41
|
frameworkVersion?: string;
|
|
42
42
|
packageManager: PackageManagerType | 'unknown';
|
|
43
43
|
packageManagers: PackageManagerType[];
|
|
44
|
+
/** Set when the project is built with something Babelize doesn't support yet, e.g. "Laravel". */
|
|
45
|
+
unsupportedFramework?: string;
|
|
44
46
|
nodeVersion?: string;
|
|
45
47
|
projectVersion?: string;
|
|
46
48
|
projectName?: string;
|
package/dist/utils/metadata.js
CHANGED
|
@@ -38,9 +38,6 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
return result;
|
|
39
39
|
};
|
|
40
40
|
})();
|
|
41
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
42
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
43
|
-
};
|
|
44
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
42
|
exports.getBabelizeDir = getBabelizeDir;
|
|
46
43
|
exports.ensureBabelizeDir = ensureBabelizeDir;
|
|
@@ -57,8 +54,8 @@ exports.detectProjectInfo = detectProjectInfo;
|
|
|
57
54
|
exports.getProjectInfo = getProjectInfo;
|
|
58
55
|
const fs = __importStar(require("fs-extra"));
|
|
59
56
|
const path = __importStar(require("path"));
|
|
60
|
-
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
61
57
|
const logger_1 = require("./logger");
|
|
58
|
+
const stack_1 = require("../detection/stack");
|
|
62
59
|
const DEFAULT_CONFIG = {
|
|
63
60
|
sourceLocale: 'en',
|
|
64
61
|
targetLocales: [],
|
|
@@ -205,15 +202,23 @@ async function detectProjectInfo(workspacePath) {
|
|
|
205
202
|
}
|
|
206
203
|
}
|
|
207
204
|
}
|
|
205
|
+
let packageManagers = detectPackageManagers(workspacePath);
|
|
206
|
+
let unsupportedFramework;
|
|
208
207
|
if (primaryFramework === 'unknown' && frameworks.length === 0) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
208
|
+
// No JS framework: Django, a static site, or a stack Babelize doesn't support.
|
|
209
|
+
const stack = await (0, stack_1.detectStack)(await (0, stack_1.localRepoFiles)(workspacePath)).catch(() => null);
|
|
210
|
+
if (stack?.framework === 'django' || stack?.framework === 'static') {
|
|
211
|
+
primaryFramework = stack.framework;
|
|
212
|
+
frameworks.push(stack.framework);
|
|
213
|
+
packageManagers = stack.packageManagers;
|
|
214
|
+
logger_1.engineLogger.info('Detected project framework', { workspacePath, framework: stack.framework });
|
|
215
|
+
}
|
|
216
|
+
else if (stack) {
|
|
217
|
+
unsupportedFramework = stack.unsupported?.name;
|
|
218
|
+
if (stack.packageManagers.length)
|
|
219
|
+
packageManagers = stack.packageManagers;
|
|
214
220
|
}
|
|
215
221
|
}
|
|
216
|
-
const packageManagers = detectPackageManagers(workspacePath);
|
|
217
222
|
const primaryPackageManager = packageManagers.length > 0 ? packageManagers[0] : 'unknown';
|
|
218
223
|
const projectInfo = {
|
|
219
224
|
framework: primaryFramework,
|
|
@@ -221,6 +226,7 @@ async function detectProjectInfo(workspacePath) {
|
|
|
221
226
|
frameworkVersion,
|
|
222
227
|
packageManager: primaryPackageManager,
|
|
223
228
|
packageManagers,
|
|
229
|
+
...(unsupportedFramework ? { unsupportedFramework } : {}),
|
|
224
230
|
projectVersion: packageJson.version,
|
|
225
231
|
projectName: packageJson.name,
|
|
226
232
|
dependencies: {
|
|
@@ -17,6 +17,8 @@ function findNotInjected(keyGeneration, injection) {
|
|
|
17
17
|
for (const k of keyGeneration.keys) {
|
|
18
18
|
if (k.isPlural)
|
|
19
19
|
continue; // the base key stands for its plural forms
|
|
20
|
+
if (k.gettext?.wrapped)
|
|
21
|
+
continue; // already marked in the code (gettext call / trans tag)
|
|
20
22
|
const file = k.extractionId.split(':')[0];
|
|
21
23
|
const id = `${file}\u0000${k.key}`;
|
|
22
24
|
if (injected.has(id) || seen.has(id))
|
|
@@ -17,6 +17,16 @@ export interface ProtectedText {
|
|
|
17
17
|
text: string;
|
|
18
18
|
/** Original text of each token, by index: tokens[i] is what `[#VAR_i#]` stands for. */
|
|
19
19
|
tokens: string[];
|
|
20
|
+
/**
|
|
21
|
+
* What each token is restored to: the original text, except for a glossary
|
|
22
|
+
* "always translate as" term, which is restored to its fixed translation.
|
|
23
|
+
*/
|
|
24
|
+
restore: string[];
|
|
25
|
+
}
|
|
26
|
+
/** A term protected as a token and restored to a fixed translation ("always translate as"). */
|
|
27
|
+
export interface FixedTerm {
|
|
28
|
+
term: string;
|
|
29
|
+
translation: string;
|
|
20
30
|
}
|
|
21
31
|
export interface TokenCheck {
|
|
22
32
|
ok: boolean;
|
|
@@ -31,15 +41,22 @@ export interface TokenCheck {
|
|
|
31
41
|
* Replace every protected span (and every never-translate term) with a token.
|
|
32
42
|
* With `icu: true`, braces are left alone (they are ICU structure) and only
|
|
33
43
|
* URLs, emails, code, tags and terms are tokenized.
|
|
44
|
+
* `fixedTerms` ("always translate as") are tokenized too, and `restore` maps
|
|
45
|
+
* their tokens to the fixed translation: a plain-text translator never sees
|
|
46
|
+
* the term, so it can't ignore the rule.
|
|
34
47
|
* Literal `[#VAR_n#]` text already present in the source is protected too, so
|
|
35
48
|
* it can never be confused with an issued token.
|
|
36
49
|
*/
|
|
37
50
|
export declare function protectText(text: string, neverTranslateTerms?: string[], options?: {
|
|
38
51
|
icu?: boolean;
|
|
52
|
+
fixedTerms?: FixedTerm[];
|
|
39
53
|
}): ProtectedText;
|
|
40
54
|
/** Every issued token must appear exactly once, and no other token may appear. */
|
|
41
55
|
export declare function checkTokens(translated: string, tokenCount: number): TokenCheck;
|
|
42
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Swap tokens back. Pass protectText's `restore` (fixed glossary terms become
|
|
58
|
+
* their translation) or `tokens` (original text). Call only after checkTokens passed.
|
|
59
|
+
*/
|
|
43
60
|
export declare function restoreText(translated: string, tokens: string[]): string;
|
|
44
61
|
/**
|
|
45
62
|
* True when nothing translatable is left once protected spans are removed —
|