@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,727 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.RunAbortedError = void 0;
|
|
37
|
+
exports.runEngineV2 = runEngineV2;
|
|
38
|
+
const fs = __importStar(require("fs-extra"));
|
|
39
|
+
const logger_1 = require("./utils/logger");
|
|
40
|
+
const types_1 = require("@babelize/types");
|
|
41
|
+
const workspace_1 = require("./modules/workspace");
|
|
42
|
+
const workspace_2 = require("./utils/workspace");
|
|
43
|
+
const extract_1 = require("./modules/extractor/extract");
|
|
44
|
+
const keygen_1 = require("./modules/keygen");
|
|
45
|
+
const locale_builder_1 = require("./modules/locale-builder");
|
|
46
|
+
const translate_1 = require("./modules/translator/translate");
|
|
47
|
+
const glossary_1 = require("./modules/glossary");
|
|
48
|
+
const translator_caller_1 = require("./utils/translator-caller");
|
|
49
|
+
const injection_1 = require("./modules/injection");
|
|
50
|
+
const summary_1 = require("./modules/summary");
|
|
51
|
+
const finalization_1 = require("./modules/finalization");
|
|
52
|
+
const lockfile_1 = require("./modules/lockfile");
|
|
53
|
+
const file_utils_1 = require("./utils/file-utils");
|
|
54
|
+
const key_usage_1 = require("./utils/key-usage");
|
|
55
|
+
const not_injected_1 = require("./utils/not-injected");
|
|
56
|
+
const language_codes_1 = require("./modules/translator/language-codes");
|
|
57
|
+
const format_injection_1 = require("./modules/format-injection");
|
|
58
|
+
const extraction_options_1 = require("./utils/extraction-options");
|
|
59
|
+
const catalog_1 = require("./django/catalog");
|
|
60
|
+
/**
|
|
61
|
+
* Whether the run's translator is an LLM (BYOK or an injected provider that
|
|
62
|
+
* isn't plain-text MT). Only then do JSX sentences get ICU plurals in the
|
|
63
|
+
* source; NLLB translates existing ICU messages branch by branch.
|
|
64
|
+
* `capabilities.icu` overrides the guess.
|
|
65
|
+
*/
|
|
66
|
+
function llmTranslator(config) {
|
|
67
|
+
return config.capabilities?.icu ?? (!!config.byok || (!!config.providers?.translator && !config.providers.translator.plainText));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Engine v2 Orchestrator (backend runtime)
|
|
71
|
+
*
|
|
72
|
+
* This is the single, clean wiring layer for the pipeline.
|
|
73
|
+
* Workers/controllers call `runEngineV2()`.
|
|
74
|
+
*/
|
|
75
|
+
/** Thrown at a progress checkpoint once the run's AbortSignal has fired. */
|
|
76
|
+
class RunAbortedError extends Error {
|
|
77
|
+
constructor(reason) {
|
|
78
|
+
super(reason === 'timeout' ? 'Run timed out' : reason === 'cancelled' ? 'Run cancelled' : `Run aborted: ${reason}`);
|
|
79
|
+
this.reason = reason;
|
|
80
|
+
this.name = 'RunAbortedError';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
exports.RunAbortedError = RunAbortedError;
|
|
84
|
+
function abortReason(signal) {
|
|
85
|
+
const reason = signal.reason;
|
|
86
|
+
if (typeof reason === 'string')
|
|
87
|
+
return reason;
|
|
88
|
+
if (reason instanceof Error)
|
|
89
|
+
return reason.message;
|
|
90
|
+
return 'aborted';
|
|
91
|
+
}
|
|
92
|
+
async function runEngineV2(config) {
|
|
93
|
+
if (config.logger) {
|
|
94
|
+
const { logger, ...rest } = config;
|
|
95
|
+
return (0, logger_1.runWithEngineLogger)(logger, () => runEngineV2(rest));
|
|
96
|
+
}
|
|
97
|
+
const startTime = Date.now();
|
|
98
|
+
// Fresh translations so far, for billing a run that is stopped mid-way.
|
|
99
|
+
const translationProgress = { freshTranslations: 0 };
|
|
100
|
+
try {
|
|
101
|
+
logger_1.engineLogger.info('Starting Babelize engine v2 pipeline', {
|
|
102
|
+
workspacePath: config.workspacePath,
|
|
103
|
+
targetLocales: config.targetLocales,
|
|
104
|
+
dryRun: config.dryRun,
|
|
105
|
+
});
|
|
106
|
+
// Cooperative cancellation: every phase (and every translation batch)
|
|
107
|
+
// reports progress, so the run stops at the next report after the signal
|
|
108
|
+
// fires. The engine has no other way to be stopped mid-run.
|
|
109
|
+
if (config.signal) {
|
|
110
|
+
const signal = config.signal;
|
|
111
|
+
const report = config.onProgress;
|
|
112
|
+
if (signal.aborted)
|
|
113
|
+
throw new RunAbortedError(abortReason(signal));
|
|
114
|
+
config = {
|
|
115
|
+
...config,
|
|
116
|
+
onProgress: (message, step, status) => {
|
|
117
|
+
if (signal.aborted)
|
|
118
|
+
throw new RunAbortedError(abortReason(signal));
|
|
119
|
+
report?.(message, step, status);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// The file-list cache is per run: a long-lived worker (or a CLI rerun in
|
|
124
|
+
// the same process) must see files added since the previous run.
|
|
125
|
+
(0, file_utils_1.clearFindFilesCache)();
|
|
126
|
+
config.onProgress?.('Validating workspace...', 'validation', 'running');
|
|
127
|
+
const { workspace } = await (0, workspace_1.validateWorkspace)(config);
|
|
128
|
+
const frameworkConfig = types_1.FRAMEWORK_CONFIGS[workspace.framework] || types_1.FRAMEWORK_CONFIGS['unknown'];
|
|
129
|
+
config.targetLocales = config.targetLocales.filter((l) => l !== config.sourceLocale);
|
|
130
|
+
// Remove stale locale files from previous runs that are no longer in targetLocales.
|
|
131
|
+
// git reset --hard only resets tracked files; generated locale files are untracked
|
|
132
|
+
// and persist across runs, causing stale languages to appear in outputs.
|
|
133
|
+
await (0, workspace_2.removeStaleLocales)(config.workspacePath, config.targetLocales, config.sourceLocale, workspace.localeFormat, workspace.publicDir);
|
|
134
|
+
config.onProgress?.('Extracting strings from codebase...', 'extraction', 'running');
|
|
135
|
+
// One resolved ICU decision for extraction, translation and injection (#248).
|
|
136
|
+
const icuOptions = await (0, extraction_options_1.resolveIcuOptions)(workspace, llmTranslator(config));
|
|
137
|
+
if (!icuOptions.sentences && workspace.framework !== 'django') {
|
|
138
|
+
logger_1.engineLogger.warn('i18next-icu not loaded by the project i18n config; keeping JSX sentences as fragments');
|
|
139
|
+
}
|
|
140
|
+
const { extraction } = await (0, extract_1.extractStrings)(workspace, config.filesToProcess, icuOptions);
|
|
141
|
+
if (extraction.warnings && extraction.warnings.length > 0) {
|
|
142
|
+
logger_1.engineLogger.warn('Extraction warnings', { count: extraction.warnings.length, warnings: extraction.warnings });
|
|
143
|
+
}
|
|
144
|
+
if (extraction.skippedFiles && extraction.skippedFiles.length > 0) {
|
|
145
|
+
logger_1.engineLogger.warn('Files skipped during extraction', {
|
|
146
|
+
count: extraction.skippedFiles.length,
|
|
147
|
+
files: extraction.skippedFiles.map((f) => `${f.file}: ${f.reason}${f.details ? ' — ' + f.details : ''}`),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
// Partition format strings (currency/date/number) from translatable strings.
|
|
151
|
+
// Gate on format-inject phase being in the framework's phases AND formatConfig being enabled (D5).
|
|
152
|
+
// This means static HTML gets format strings as translatable (month names, currency as text)
|
|
153
|
+
// instead of silently dropping them.
|
|
154
|
+
const formatPartitionEnabled = frameworkConfig.phases.includes('format-inject') && config.formatConfig?.enabled;
|
|
155
|
+
const [translatableStrings, formatStrings] = formatPartitionEnabled
|
|
156
|
+
? (0, format_injection_1.partitionFormatStrings)(extraction.strings)
|
|
157
|
+
: [extraction.strings, []];
|
|
158
|
+
const existingLocale = await (0, locale_builder_1.readLocaleFile)(workspace, config.sourceLocale);
|
|
159
|
+
const existingData = existingLocale || {};
|
|
160
|
+
// Strings injected by earlier runs are no longer extracted, but their keys
|
|
161
|
+
// are still in the source locale file. With a lockfile those keys must
|
|
162
|
+
// still be carried into every target locale (and translated for newly
|
|
163
|
+
// added locales), so "nothing new to extract" is not "nothing to do".
|
|
164
|
+
const hasExistingKeys = !!config.projectId && Object.keys(sourceLocaleKeys(existingData)).length > 0;
|
|
165
|
+
if (translatableStrings.length === 0 && formatStrings.length === 0 && !hasExistingKeys) {
|
|
166
|
+
logger_1.engineLogger.info('No strings found, finishing early');
|
|
167
|
+
return createEmptyResult(startTime, extraction.warnings, extraction.skippedFiles, extraction.notInjected);
|
|
168
|
+
}
|
|
169
|
+
// Use only translatable strings for the rest of the pipeline
|
|
170
|
+
config.onProgress?.('Generating translation keys...', 'keygen', 'running');
|
|
171
|
+
const { keyGeneration } = (0, keygen_1.generateKeys)(translatableStrings, existingData);
|
|
172
|
+
// gettext projects (Django): the catalogue of messages and the project's
|
|
173
|
+
// existing .po translations, which are kept and never re-translated.
|
|
174
|
+
const gettext = workspace.localeFormat === 'po'
|
|
175
|
+
? await catalog_1.GettextCatalog.create(workspace, translatableStrings, keyGeneration.keys, config.targetLocales)
|
|
176
|
+
: null;
|
|
177
|
+
// ── Lockfile: partition keys ─────────────────────────────
|
|
178
|
+
let lockfileStats = { reused: 0, translated: 0, changed: 0 };
|
|
179
|
+
let tmHits = 0;
|
|
180
|
+
// (key, locale) translations the translator produced this run — the
|
|
181
|
+
// billable work. Lockfile/TM/cache reuse and failures are excluded.
|
|
182
|
+
let translatedThisRun = 0;
|
|
183
|
+
let keyUsage;
|
|
184
|
+
let translationMetadata = {};
|
|
185
|
+
if (config.projectId) {
|
|
186
|
+
config.onProgress?.('Checking lockfile for cached translations...', 'lockfile', 'running');
|
|
187
|
+
// Force re-translate: clear lockfile first
|
|
188
|
+
if (config.forceTranslation) {
|
|
189
|
+
await (0, lockfile_1.clearLockfile)(config.projectId, config.dataDir);
|
|
190
|
+
logger_1.engineLogger.info('Force translation: cleared lockfile', { projectId: config.projectId });
|
|
191
|
+
}
|
|
192
|
+
let lockfile = await (0, lockfile_1.loadLockfile)(config.projectId, config.dataDir);
|
|
193
|
+
config.onProgress?.('Processing source locale file...', 'locale', 'running');
|
|
194
|
+
const { localeHandling } = await (0, locale_builder_1.handleBaseLocale)(workspace, keyGeneration.keys, config.sourceLocale, config.dryRun);
|
|
195
|
+
// Build flat key→value map for partitioning: every key in the source
|
|
196
|
+
// locale, not just the ones extracted this run. Keys injected by earlier
|
|
197
|
+
// runs aren't re-extracted, and partitioning only this run's keys used to
|
|
198
|
+
// drop their translations from every target file (and left newly added
|
|
199
|
+
// locales with nothing to translate).
|
|
200
|
+
const flatKeys = sourceLocaleKeys(localeHandling.sourceLocale.data);
|
|
201
|
+
const contextMap = {};
|
|
202
|
+
const extractionContextMap = {};
|
|
203
|
+
for (const k of keyGeneration.keys) {
|
|
204
|
+
flatKeys[k.key] = k.value;
|
|
205
|
+
if (k.parentContext) {
|
|
206
|
+
contextMap[k.key] = k.parentContext;
|
|
207
|
+
}
|
|
208
|
+
if (k.extractionContext) {
|
|
209
|
+
extractionContextMap[k.key] = k.extractionContext;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Advisory only: which source keys no code seems to reference. The scan
|
|
213
|
+
// is a heuristic (renamed translators, other file types, templates), so
|
|
214
|
+
// it never changes what is translated, kept or written.
|
|
215
|
+
keyUsage = await (0, key_usage_1.reportKeyUsage)(workspace, flatKeys, new Set(keyGeneration.keys.map((k) => k.key)));
|
|
216
|
+
// Translations already in the project's .po files win over the lockfile.
|
|
217
|
+
if (gettext)
|
|
218
|
+
lockfile = gettext.seedLockfile(lockfile, flatKeys);
|
|
219
|
+
// Pseudo-localization (--provider pseudo) is not a translation: a real
|
|
220
|
+
// run treats pseudo values in the lockfile as missing and translates them.
|
|
221
|
+
// The glossary is loaded once, here: keys whose glossary rules changed
|
|
222
|
+
// since their stored translation are re-translated (and only those).
|
|
223
|
+
// If the rules failed to load, stored translations are kept as they are
|
|
224
|
+
// rather than redone without the glossary.
|
|
225
|
+
const rules = await (0, translate_1.loadProjectTranslationRules)(config.projectId, config.providers?.rules);
|
|
226
|
+
const lockfileRead = {
|
|
227
|
+
pseudo: config.providers?.translator?.pseudo === true,
|
|
228
|
+
glossaryHash: rules.complete ? (source, locale) => (0, glossary_1.glossaryFingerprint)(source, locale, rules.glossary) : undefined,
|
|
229
|
+
};
|
|
230
|
+
const partition = (0, lockfile_1.partitionKeys)(lockfile, flatKeys, config.targetLocales, lockfileRead);
|
|
231
|
+
lockfileStats = (0, lockfile_1.computeLockfileStats)(partition);
|
|
232
|
+
logger_1.engineLogger.info('Lockfile partition result', {
|
|
233
|
+
unchanged: partition.unchanged.length,
|
|
234
|
+
newKeys: partition.newKeys.length,
|
|
235
|
+
changed: partition.changed.length,
|
|
236
|
+
missingLocales: partition.missingLocales.length,
|
|
237
|
+
});
|
|
238
|
+
// Collect cached translations for unchanged keys, and for keys that only
|
|
239
|
+
// lack some locales (a newly added language): their existing locales are
|
|
240
|
+
// still valid and must not drop out of the other locale files.
|
|
241
|
+
const cachedTranslations = (0, lockfile_1.collectCachedTranslations)(lockfile, [...partition.unchanged, ...partition.missingLocales], config.targetLocales, lockfileRead);
|
|
242
|
+
config.onProgress?.('Translating to target locales...', 'translation', 'running');
|
|
243
|
+
const translatorHealthy = await isTranslatorHealthy(config);
|
|
244
|
+
if (!translatorHealthy) {
|
|
245
|
+
const msg = 'Translator service is not responding — only source locale (en.json) was generated. Target locale files will be missing. Please ensure the translator service is running and try again.';
|
|
246
|
+
logger_1.engineLogger.warn(msg);
|
|
247
|
+
config.onProgress?.(msg, 'translation', 'warning');
|
|
248
|
+
}
|
|
249
|
+
// Only translate keys that need it (new + changed + missing locales)
|
|
250
|
+
const keysNeedingTranslation = [
|
|
251
|
+
...partition.newKeys,
|
|
252
|
+
...partition.changed,
|
|
253
|
+
...partition.missingLocales,
|
|
254
|
+
];
|
|
255
|
+
let translation;
|
|
256
|
+
if (keysNeedingTranslation.length === 0) {
|
|
257
|
+
logger_1.engineLogger.info('All keys reused from lockfile, skipping translation');
|
|
258
|
+
translation = { locales: [], errors: [], failures: [] };
|
|
259
|
+
}
|
|
260
|
+
else if (translatorHealthy) {
|
|
261
|
+
const keysToTranslateByLocale = (0, lockfile_1.buildKeysToTranslateByTargetLocale)(partition, config.targetLocales);
|
|
262
|
+
const result = await (0, translate_1.translateLocales)(workspace, localeHandling.sourceLocale, config.targetLocales, config.forceTranslation, config.dryRun, config.projectId, (msg) => config.onProgress?.(msg, 'translation', 'running'), keysToTranslateByLocale, config.providers?.cache, config.byok, contextMap, extractionContextMap, config.providers, { targetPlurals: icuOptions.targetPlurals, pluralValuePlaceholders: gettext?.pluralValuePlaceholders(), rules, progress: translationProgress });
|
|
263
|
+
translation = result.translation;
|
|
264
|
+
// A translation that wouldn't compile into the .po file is a failure, not output.
|
|
265
|
+
gettext?.validate(translation, flatKeys);
|
|
266
|
+
tmHits = result.stats.tmHits;
|
|
267
|
+
translationMetadata = result.metadata;
|
|
268
|
+
translatedThisRun = freshTranslations(translation, translationMetadata);
|
|
269
|
+
// Strings reused from the lockfile still make a good run: only fail
|
|
270
|
+
// when nothing at all is usable. Otherwise one string that keeps
|
|
271
|
+
// failing (and is retried every run) failed the whole project.
|
|
272
|
+
if (!hasCachedTranslations(cachedTranslations))
|
|
273
|
+
assertSomethingTranslated(translation);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
const errorMsg = 'Translator service unavailable — target locale files (hi.json, fr.json, etc.) were NOT generated. Only source locale (en.json) was created. Check that TRANSLATOR_URL or BYOK provider is configured and the service is running.';
|
|
277
|
+
logger_1.engineLogger.error(errorMsg);
|
|
278
|
+
config.onProgress?.(errorMsg, 'translation', 'error');
|
|
279
|
+
throw new Error(errorMsg);
|
|
280
|
+
}
|
|
281
|
+
// Merge cached translations into translation output locale data
|
|
282
|
+
for (const locale of translation.locales) {
|
|
283
|
+
const cached = cachedTranslations[locale.locale];
|
|
284
|
+
if (cached) {
|
|
285
|
+
const existingFlat = (0, keygen_1.flattenLocaleData)(locale.data);
|
|
286
|
+
const merged = { ...cached, ...existingFlat };
|
|
287
|
+
// Rebuild the locale data with cached + new
|
|
288
|
+
for (const [k, v] of Object.entries(merged)) {
|
|
289
|
+
setNestedKey(locale.data, k, v);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Also create locale files for locales that were fully cached (no translator call)
|
|
294
|
+
for (const targetLocale of config.targetLocales) {
|
|
295
|
+
const existsInTranslation = translation.locales.some((l) => l.locale === targetLocale);
|
|
296
|
+
if (!existsInTranslation && cachedTranslations[targetLocale] && Object.keys(cachedTranslations[targetLocale]).length > 0) {
|
|
297
|
+
// Filter out cached translations that are identical to source text
|
|
298
|
+
// (identity translations mean the previous run's translator was unavailable)
|
|
299
|
+
// A locale where *every* cached value equals the source is what an
|
|
300
|
+
// unavailable translator used to leave behind: skip it. Otherwise
|
|
301
|
+
// keep all values — names and codes are legitimately identical, and
|
|
302
|
+
// dropping them would delete real translations.
|
|
303
|
+
const realTranslations = { ...cachedTranslations[targetLocale] };
|
|
304
|
+
const hasRealTranslations = Object.entries(realTranslations).some(([k, v]) => flatKeys[k] !== v);
|
|
305
|
+
if (!hasRealTranslations) {
|
|
306
|
+
logger_1.engineLogger.warn(`Skipping cached locale "${targetLocale}" — all cached entries are identity translations (same as source). Translator may be unavailable.`);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const cachedData = {};
|
|
310
|
+
for (const [k, v] of Object.entries(realTranslations)) {
|
|
311
|
+
setNestedKey(cachedData, k, v);
|
|
312
|
+
}
|
|
313
|
+
const localePath = (0, file_utils_1.localeFilePath)(workspace.path, workspace.publicDir, workspace.localeFormat, targetLocale);
|
|
314
|
+
translation.locales.push({
|
|
315
|
+
locale: targetLocale,
|
|
316
|
+
path: localePath.relativeFile,
|
|
317
|
+
data: cachedData,
|
|
318
|
+
isNew: false,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Locales rebuilt from the lockfile carry no _meta block; give them the
|
|
323
|
+
// same one a freshly translated locale gets.
|
|
324
|
+
for (const locale of translation.locales) {
|
|
325
|
+
if (!locale.data._meta) {
|
|
326
|
+
locale.data._meta = { dir: (0, language_codes_1.isRTL)(locale.locale) ? 'rtl' : 'ltr', language: locale.locale };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// The translator wrote only this run's strings to disk; write the merged
|
|
330
|
+
// locale (lockfile + new) so the workspace — which the download ZIP is
|
|
331
|
+
// built from — has every translation, including fully cached locales.
|
|
332
|
+
if (!config.dryRun && workspace.localeFormat !== 'po') {
|
|
333
|
+
for (const locale of translation.locales) {
|
|
334
|
+
const { file } = (0, file_utils_1.localeFilePath)(workspace.path, workspace.publicDir, workspace.localeFormat, locale.locale);
|
|
335
|
+
await (0, file_utils_1.writeJsonFile)(file, locale.data, true);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// ── Lockfile: update after translation ─────────────────
|
|
339
|
+
config.onProgress?.('Updating lockfile...', 'lockfile', 'running');
|
|
340
|
+
const allTranslatedPerLocale = {};
|
|
341
|
+
for (const locale of translation.locales) {
|
|
342
|
+
allTranslatedPerLocale[locale.locale] = (0, keygen_1.flattenLocaleData)(locale.data);
|
|
343
|
+
}
|
|
344
|
+
// Only persist lockfile if we actually generated translation data,
|
|
345
|
+
// to prevent corrupting the lockfile when the translator was unavailable.
|
|
346
|
+
const hasRealTranslations = translation.locales.length > 0;
|
|
347
|
+
if (hasRealTranslations) {
|
|
348
|
+
// Lockfile-reused keys skip the translator, so they would otherwise
|
|
349
|
+
// never reach TM. Backfill them without overwriting existing entries —
|
|
350
|
+
// once: keys already marked inTm in the lockfile are skipped, so an
|
|
351
|
+
// unchanged project doesn't re-upsert every key on every run.
|
|
352
|
+
const lockfileMetadata = { ...translationMetadata };
|
|
353
|
+
if (lockfileRead.pseudo) {
|
|
354
|
+
// Record what this run produced as pseudo, so a later real run replaces it.
|
|
355
|
+
for (const [locale, keys] of Object.entries(translationMetadata)) {
|
|
356
|
+
lockfileMetadata[locale] = Object.fromEntries(Object.entries(keys).map(([k, m]) => [k, { ...(m ?? {}), pseudo: true }]));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (!config.dryRun) {
|
|
360
|
+
const pending = {};
|
|
361
|
+
for (const [locale, keys] of Object.entries(cachedTranslations)) {
|
|
362
|
+
for (const [key, value] of Object.entries(keys)) {
|
|
363
|
+
if (lockfile.keys[key]?.metadata?.[locale]?.inTm)
|
|
364
|
+
continue;
|
|
365
|
+
(pending[locale] ?? (pending[locale] = {}))[key] = value;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
const settled = await (0, translate_1.backfillTranslationMemory)(config.projectId, config.sourceLocale, flatKeys, pending, new Set(Object.keys(contextMap)), config.providers);
|
|
370
|
+
for (const [locale, keys] of Object.entries(settled)) {
|
|
371
|
+
const forLocale = (lockfileMetadata[locale] = { ...(lockfileMetadata[locale] ?? {}) });
|
|
372
|
+
for (const key of keys) {
|
|
373
|
+
forLocale[key] = { ...(lockfile.keys[key]?.metadata?.[locale] ?? {}), ...(forLocale[key] ?? {}), inTm: true };
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
logger_1.engineLogger.error('TM backfill from lockfile failed', {
|
|
379
|
+
projectId: config.projectId,
|
|
380
|
+
error: err instanceof Error ? err.message : String(err),
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const previousLockfile = lockfile;
|
|
385
|
+
lockfile = (0, lockfile_1.updateLockfile)(lockfile, flatKeys, allTranslatedPerLocale, config.projectId, lockfileMetadata);
|
|
386
|
+
lockfile = (0, lockfile_1.dropFailedTranslations)(lockfile, translation.failures ?? []);
|
|
387
|
+
// Backup-translator (NLLB) output is shipped in the locale file but not
|
|
388
|
+
// recorded as done, so the next run retries it with the AI translator.
|
|
389
|
+
lockfile = (0, lockfile_1.dropFailedTranslations)(lockfile, fallbackTranslations(translationMetadata));
|
|
390
|
+
// Users commit the lockfile: an unchanged one is not rewritten (no
|
|
391
|
+
// new timestamp, no diff).
|
|
392
|
+
const lockfileOnDisk = await fs.pathExists((0, lockfile_1.getLockfilePath)(config.projectId, config.dataDir));
|
|
393
|
+
if (!config.dryRun && (!lockfileOnDisk || (0, lockfile_1.lockfileChanged)(previousLockfile, lockfile))) {
|
|
394
|
+
await (0, lockfile_1.saveLockfile)(config.projectId, lockfile, config.dataDir);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
logger_1.engineLogger.warn('Skipping lockfile update: no translation data generated (translator may be unavailable)', {
|
|
399
|
+
projectId: config.projectId,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
const result = await finalizePipeline({
|
|
403
|
+
extraction, keyGeneration, localeHandling, translation,
|
|
404
|
+
config, startTime, workspace, allTranslatedPerLocale,
|
|
405
|
+
lockfileStats, tmHits, translationMetadata,
|
|
406
|
+
translatedThisRun, keyUsage,
|
|
407
|
+
formatStrings, frameworkConfig, gettext,
|
|
408
|
+
});
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
// ── No projectId: run without lockfile (legacy path) ─────
|
|
412
|
+
const contextMap = {};
|
|
413
|
+
const extractionContextMap = {};
|
|
414
|
+
for (const k of keyGeneration.keys) {
|
|
415
|
+
if (k.parentContext) {
|
|
416
|
+
contextMap[k.key] = k.parentContext;
|
|
417
|
+
}
|
|
418
|
+
if (k.extractionContext) {
|
|
419
|
+
extractionContextMap[k.key] = k.extractionContext;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
config.onProgress?.('Processing source locale file...', 'locale', 'running');
|
|
423
|
+
const { localeHandling } = await (0, locale_builder_1.handleBaseLocale)(workspace, keyGeneration.keys, config.sourceLocale, config.dryRun);
|
|
424
|
+
config.onProgress?.('Translating to target locales...', 'translation', 'running');
|
|
425
|
+
const translatorHealthy = await isTranslatorHealthy(config);
|
|
426
|
+
if (!translatorHealthy) {
|
|
427
|
+
const msg = 'Translator service is not responding — only source locale (en.json) was generated. Target locale files will be missing. Please ensure the translator service is running and try again.';
|
|
428
|
+
logger_1.engineLogger.warn(msg);
|
|
429
|
+
config.onProgress?.(msg, 'translation', 'warning');
|
|
430
|
+
}
|
|
431
|
+
const translationResult = translatorHealthy
|
|
432
|
+
? await (0, translate_1.translateLocales)(workspace, localeHandling.sourceLocale, config.targetLocales, config.forceTranslation, config.dryRun, config.projectId, (msg) => config.onProgress?.(msg, 'translation', 'running'), undefined, config.providers?.cache, config.byok, contextMap, extractionContextMap, config.providers, { targetPlurals: icuOptions.targetPlurals, pluralValuePlaceholders: gettext?.pluralValuePlaceholders(), progress: translationProgress })
|
|
433
|
+
: {
|
|
434
|
+
translation: {
|
|
435
|
+
locales: [],
|
|
436
|
+
errors: config.targetLocales.map((l) => ({ locale: l, error: 'Translator service unavailable — target locale files were not generated. Only the source locale file was created.' })),
|
|
437
|
+
},
|
|
438
|
+
stats: { tmHits: 0 },
|
|
439
|
+
metadata: {}
|
|
440
|
+
};
|
|
441
|
+
const translation = translationResult.translation;
|
|
442
|
+
gettext?.validate(translation, (0, keygen_1.flattenLocaleData)(localeHandling.sourceLocale.data));
|
|
443
|
+
tmHits = translationResult.stats.tmHits;
|
|
444
|
+
translationMetadata = translationResult.metadata;
|
|
445
|
+
translatedThisRun = freshTranslations(translation, translationMetadata);
|
|
446
|
+
assertSomethingTranslated(translation);
|
|
447
|
+
// Fail immediately when translation errors occurred — before injection/finalization
|
|
448
|
+
if (translation.errors && translation.errors.length > 0) {
|
|
449
|
+
const errorMsg = translation.errors.map((e) => `${e.locale}: ${e.error}`).join('; ');
|
|
450
|
+
logger_1.engineLogger.error('Translation failed', { errors: errorMsg });
|
|
451
|
+
throw new Error(`Translation failed: ${errorMsg}`);
|
|
452
|
+
}
|
|
453
|
+
const allTranslatedPerLocale = {};
|
|
454
|
+
for (const locale of translation.locales) {
|
|
455
|
+
allTranslatedPerLocale[locale.locale] = (0, keygen_1.flattenLocaleData)(locale.data);
|
|
456
|
+
}
|
|
457
|
+
const result = await finalizePipeline({
|
|
458
|
+
extraction, keyGeneration, localeHandling, translation,
|
|
459
|
+
config, startTime, workspace, allTranslatedPerLocale,
|
|
460
|
+
lockfileStats: undefined, tmHits, translationMetadata,
|
|
461
|
+
translatedThisRun, keyUsage,
|
|
462
|
+
formatStrings, frameworkConfig, gettext,
|
|
463
|
+
});
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
logger_1.engineLogger.error('Engine v2 error', { error: error?.message ?? String(error) });
|
|
468
|
+
return {
|
|
469
|
+
success: false,
|
|
470
|
+
error: error?.message ?? String(error),
|
|
471
|
+
...(error instanceof RunAbortedError
|
|
472
|
+
? { aborted: error.reason, partialTranslations: translationProgress.freshTranslations }
|
|
473
|
+
: {}),
|
|
474
|
+
stats: {
|
|
475
|
+
filesScanned: 0,
|
|
476
|
+
filesWithStrings: 0,
|
|
477
|
+
stringsExtracted: 0,
|
|
478
|
+
keysGenerated: 0,
|
|
479
|
+
filesModified: 0,
|
|
480
|
+
injectionsApplied: 0,
|
|
481
|
+
localeFilesCreated: 0,
|
|
482
|
+
processingTimeMs: Date.now() - startTime,
|
|
483
|
+
},
|
|
484
|
+
locales: [],
|
|
485
|
+
injections: { modifiedFiles: [], skippedFiles: [], totalInjections: 0 },
|
|
486
|
+
warnings: [],
|
|
487
|
+
skipped: [],
|
|
488
|
+
diffs: [],
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* (key, locale) translations the translator produced this run, in locales
|
|
494
|
+
* that made it into the output: the billable work. Values reused from
|
|
495
|
+
* translation memory or the cache, and source text copied through untouched
|
|
496
|
+
* (placeholders, codes), are not counted; failed keys are not in the output.
|
|
497
|
+
*/
|
|
498
|
+
function fallbackTranslations(metadata) {
|
|
499
|
+
const out = [];
|
|
500
|
+
for (const [locale, keys] of Object.entries(metadata ?? {})) {
|
|
501
|
+
for (const [key, m] of Object.entries(keys ?? {}))
|
|
502
|
+
if (m?.fallback)
|
|
503
|
+
out.push({ key, locale });
|
|
504
|
+
}
|
|
505
|
+
return out;
|
|
506
|
+
}
|
|
507
|
+
function freshTranslations(translation, metadata) {
|
|
508
|
+
let n = 0;
|
|
509
|
+
for (const locale of translation.locales) {
|
|
510
|
+
const meta = metadata[locale.locale] ?? {};
|
|
511
|
+
for (const [key, value] of Object.entries((0, keygen_1.flattenLocaleData)(locale.data))) {
|
|
512
|
+
if (key === '_meta' || key.startsWith('_meta.') || !value)
|
|
513
|
+
continue;
|
|
514
|
+
const m = meta[key];
|
|
515
|
+
// Backup-translator output isn't billed: the next run redoes it with the AI translator.
|
|
516
|
+
if (m?.fromMemory || m?.fromCache || m?.passthrough || m?.fallback)
|
|
517
|
+
continue;
|
|
518
|
+
n++;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return n;
|
|
522
|
+
}
|
|
523
|
+
/** Flat key → source text for a source locale file, without the `_meta` block. */
|
|
524
|
+
function sourceLocaleKeys(data) {
|
|
525
|
+
const flat = {};
|
|
526
|
+
for (const [key, value] of Object.entries((0, keygen_1.flattenLocaleData)(data))) {
|
|
527
|
+
if (key === '_meta' || key.startsWith('_meta.'))
|
|
528
|
+
continue;
|
|
529
|
+
flat[key] = value;
|
|
530
|
+
}
|
|
531
|
+
return flat;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* An injected TranslationProvider is the host's responsibility (it reports
|
|
535
|
+
* per-key failures itself), so only probe the built-in translator/BYOK
|
|
536
|
+
* endpoint when the engine is going to use it.
|
|
537
|
+
*/
|
|
538
|
+
async function isTranslatorHealthy(config) {
|
|
539
|
+
if (config.providers?.translator)
|
|
540
|
+
return true;
|
|
541
|
+
return (0, translator_caller_1.checkTranslatorHealth)(config.byok);
|
|
542
|
+
}
|
|
543
|
+
function createEmptyResult(startTime, warnings = [], skipped = [], notInjected = []) {
|
|
544
|
+
return {
|
|
545
|
+
success: true,
|
|
546
|
+
notInjected,
|
|
547
|
+
stats: {
|
|
548
|
+
filesScanned: 0,
|
|
549
|
+
filesWithStrings: 0,
|
|
550
|
+
stringsExtracted: 0,
|
|
551
|
+
keysGenerated: 0,
|
|
552
|
+
filesModified: 0,
|
|
553
|
+
injectionsApplied: 0,
|
|
554
|
+
localeFilesCreated: 0,
|
|
555
|
+
processingTimeMs: Date.now() - startTime,
|
|
556
|
+
},
|
|
557
|
+
locales: [],
|
|
558
|
+
injections: { modifiedFiles: [], skippedFiles: [], totalInjections: 0 },
|
|
559
|
+
warnings,
|
|
560
|
+
skipped,
|
|
561
|
+
diffs: [],
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Set a value in a nested object using a dot-separated key path.
|
|
566
|
+
* e.g. setNestedKey(obj, 'homepage.hero.title', 'Hello') →
|
|
567
|
+
* { homepage: { hero: { title: 'Hello' } } }
|
|
568
|
+
*/
|
|
569
|
+
function setNestedKey(obj, dottedKey, value) {
|
|
570
|
+
const parts = dottedKey.split('.');
|
|
571
|
+
let current = obj;
|
|
572
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
573
|
+
const part = parts[i];
|
|
574
|
+
if (!(part in current)) {
|
|
575
|
+
current[part] = {};
|
|
576
|
+
}
|
|
577
|
+
else if (typeof current[part] === 'string') {
|
|
578
|
+
logger_1.engineLogger.warn('Overwriting string value with object for nested key', {
|
|
579
|
+
key: dottedKey,
|
|
580
|
+
part,
|
|
581
|
+
});
|
|
582
|
+
current[part] = {};
|
|
583
|
+
}
|
|
584
|
+
current = current[part];
|
|
585
|
+
}
|
|
586
|
+
current[parts[parts.length - 1]] = value;
|
|
587
|
+
}
|
|
588
|
+
function hasCachedTranslations(cached) {
|
|
589
|
+
return Object.values(cached).some((locale) => Object.keys(locale).length > 0);
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* A run where translation was needed but no string in any locale could be
|
|
593
|
+
* translated is a failed run, not an empty success: fail it with the reasons
|
|
594
|
+
* so the job errors (and is not billed) instead of reporting success.
|
|
595
|
+
*/
|
|
596
|
+
function assertSomethingTranslated(translation) {
|
|
597
|
+
const failures = translation.failures ?? [];
|
|
598
|
+
if (failures.length === 0 || translation.locales.length > 0)
|
|
599
|
+
return;
|
|
600
|
+
const byReason = new Map();
|
|
601
|
+
for (const f of failures)
|
|
602
|
+
byReason.set(f.reason, (byReason.get(f.reason) ?? 0) + 1);
|
|
603
|
+
const summary = [...byReason].map(([reason, n]) => `${n} ${reason}`).join(', ');
|
|
604
|
+
const sample = failures.find((f) => f.message)?.message;
|
|
605
|
+
throw new Error(`No strings could be translated (${summary})${sample ? `: ${sample}` : ''}`);
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Add a changed file to the run's list, folding a later write to a file that is
|
|
609
|
+
* already listed (an entry point that was injected, then wired up) into one
|
|
610
|
+
* entry: the original from the first write, the content from the last.
|
|
611
|
+
*/
|
|
612
|
+
function mergeChangedFile(list, changed) {
|
|
613
|
+
const existing = list.find((f) => f.file === changed.file);
|
|
614
|
+
if (!existing) {
|
|
615
|
+
list.push(changed);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
existing.modifiedContent = changed.modifiedContent;
|
|
619
|
+
existing.injections.push(...changed.injections);
|
|
620
|
+
existing.importAdded = existing.importAdded || changed.importAdded;
|
|
621
|
+
existing.hookAdded = existing.hookAdded || changed.hookAdded;
|
|
622
|
+
}
|
|
623
|
+
async function finalizePipeline(params) {
|
|
624
|
+
const { extraction, keyGeneration, localeHandling, translation, config, startTime, workspace, allTranslatedPerLocale, lockfileStats, tmHits, translationMetadata, formatStrings, frameworkConfig, translatedThisRun, keyUsage, gettext, } = params;
|
|
625
|
+
const keyMapping = (0, keygen_1.createKeyMapping)(keyGeneration.keys);
|
|
626
|
+
// Injection: AST-based i18n hook injection (skipped for frameworks like Django)
|
|
627
|
+
let injection;
|
|
628
|
+
if (frameworkConfig.phases.includes('inject')) {
|
|
629
|
+
config.onProgress?.('Injecting translations into code...', 'injection', 'running');
|
|
630
|
+
// Inject exactly the nodes extraction selected (by source offset).
|
|
631
|
+
const positions = new Map();
|
|
632
|
+
for (const str of extraction.strings)
|
|
633
|
+
if (str.offsets)
|
|
634
|
+
positions.set(str.id, new Set(str.offsets));
|
|
635
|
+
injection = await (0, injection_1.injectTranslations)(workspace, keyMapping, config.confidenceThreshold, config.dryRun, positions);
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
injection = { modifiedFiles: [], skippedFiles: [], totalInjections: 0 };
|
|
639
|
+
}
|
|
640
|
+
const notInjected = frameworkConfig.phases.includes('inject')
|
|
641
|
+
? [...(0, not_injected_1.findNotInjected)(keyGeneration, injection), ...(extraction.notInjected ?? [])]
|
|
642
|
+
: [];
|
|
643
|
+
if (notInjected.length > 0) {
|
|
644
|
+
logger_1.engineLogger.warn('Extracted strings not wired into code', { count: notInjected.length });
|
|
645
|
+
}
|
|
646
|
+
// Format injection: replace hardcoded dates/currencies with Intl.* API calls
|
|
647
|
+
if (frameworkConfig.phases.includes('format-inject') && config.formatConfig?.enabled && formatStrings && formatStrings.length > 0) {
|
|
648
|
+
config.onProgress?.('Injecting Intl.* format replacements...', 'injection', 'running');
|
|
649
|
+
const formatInjection = await (0, format_injection_1.injectFormats)(workspace, formatStrings, config.formatConfig, config.dryRun);
|
|
650
|
+
if (formatInjection.totalInjections > 0) {
|
|
651
|
+
for (const changed of formatInjection.modifiedFiles)
|
|
652
|
+
mergeChangedFile(injection.modifiedFiles, changed);
|
|
653
|
+
injection.totalInjections += formatInjection.totalInjections;
|
|
654
|
+
injection.skippedFiles.push(...formatInjection.skippedFiles);
|
|
655
|
+
}
|
|
656
|
+
logger_1.engineLogger.info('Format injection complete', {
|
|
657
|
+
totalInjections: formatInjection.totalInjections,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
// gettext: merge this run's translations into the project's .po files.
|
|
661
|
+
const finalizationNotes = [];
|
|
662
|
+
if (gettext) {
|
|
663
|
+
gettext.refreshReferences(injection);
|
|
664
|
+
const written = await gettext.write(config.targetLocales, allTranslatedPerLocale, {
|
|
665
|
+
dryRun: config.dryRun,
|
|
666
|
+
pseudo: config.providers?.translator?.pseudo === true,
|
|
667
|
+
});
|
|
668
|
+
translation.locales = written.locales;
|
|
669
|
+
finalizationNotes.push(...written.notes);
|
|
670
|
+
}
|
|
671
|
+
// Finalization: install i18n deps, create config files, modify entry points
|
|
672
|
+
if (frameworkConfig.phases.includes('finalize')) {
|
|
673
|
+
config.onProgress?.('Finalizing project...', 'finalization', 'running');
|
|
674
|
+
const { finalization } = await (0, finalization_1.finalizeProject)(workspace, config.dryRun, {
|
|
675
|
+
targetLocales: config.targetLocales,
|
|
676
|
+
sourceLocale: config.sourceLocale,
|
|
677
|
+
installDependencies: config.installDependencies,
|
|
678
|
+
});
|
|
679
|
+
finalizationNotes.push(...(finalization.warnings ?? []));
|
|
680
|
+
// Every file finalization wrote (package.json, i18n config, entry point,
|
|
681
|
+
// next.config, layout, i18n.js, settings.py, a lockfile it updated) ships
|
|
682
|
+
// with the run like injected files — hosts stage modifiedFiles for the PR.
|
|
683
|
+
for (const changed of finalization.changedFiles ?? []) {
|
|
684
|
+
mergeChangedFile(injection.modifiedFiles, { ...changed, injections: [], importAdded: false, hookAdded: false });
|
|
685
|
+
}
|
|
686
|
+
if (finalization.dependenciesInvalid) {
|
|
687
|
+
if (finalization.packagesInstalled) {
|
|
688
|
+
logger_1.engineLogger.info('New dependencies installed successfully');
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
logger_1.engineLogger.warn(finalization.warnings?.find((w) => w.includes('auto-install')) || 'New dependencies added. Please run your package manager install.');
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (finalization.configFileCreated) {
|
|
695
|
+
logger_1.engineLogger.info('Created i18n configuration file');
|
|
696
|
+
}
|
|
697
|
+
if (finalization.entryPointModified) {
|
|
698
|
+
logger_1.engineLogger.info('Injected i18n import into entry point');
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
config.onProgress?.('Generating summary...', 'summary', 'running');
|
|
702
|
+
const { stats, warnings, diffs } = (0, summary_1.generateSummary)(extraction, keyGeneration, localeHandling, translation, injection, startTime, lockfileStats, tmHits);
|
|
703
|
+
stats.translatedThisRun = Math.max(0, translatedThisRun);
|
|
704
|
+
logger_1.engineLogger.info('Processing complete', stats);
|
|
705
|
+
logger_1.engineLogger.debug('Readable summary', { summary: (0, summary_1.generateReadableSummary)(stats) });
|
|
706
|
+
const allLocales = [localeHandling.sourceLocale, ...translation.locales];
|
|
707
|
+
config.onProgress?.('Complete', 'complete', 'success');
|
|
708
|
+
const allSkipped = [...extraction.skippedFiles, ...injection.skippedFiles];
|
|
709
|
+
return {
|
|
710
|
+
success: true,
|
|
711
|
+
translationFailures: translation.failures ?? [],
|
|
712
|
+
stats,
|
|
713
|
+
locales: allLocales,
|
|
714
|
+
injections: injection,
|
|
715
|
+
warnings: [...warnings],
|
|
716
|
+
skipped: allSkipped,
|
|
717
|
+
diffs,
|
|
718
|
+
notes: finalizationNotes,
|
|
719
|
+
keyUsage,
|
|
720
|
+
notInjected,
|
|
721
|
+
results: {
|
|
722
|
+
source: (0, keygen_1.flattenLocaleData)(localeHandling.sourceLocale.data),
|
|
723
|
+
targets: allTranslatedPerLocale,
|
|
724
|
+
metadata: translationMetadata,
|
|
725
|
+
},
|
|
726
|
+
};
|
|
727
|
+
}
|