@babelize/engine 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/index.d.ts +54 -0
- package/dist/index.js +126 -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/django/django-template-injector.d.ts +29 -0
- package/dist/injection/django/django-template-injector.js +216 -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 +207 -0
- package/dist/local/report.d.ts +177 -0
- package/dist/local/report.js +186 -0
- package/dist/local/translate-core.d.ts +215 -0
- package/dist/local/translate-core.js +461 -0
- package/dist/local/translate-messages.d.ts +49 -0
- package/dist/local/translate-messages.js +91 -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 +701 -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 +13 -0
- package/dist/modules/finalization/django-finalize.js +275 -0
- package/dist/modules/finalization/finalize.d.ts +36 -0
- package/dist/modules/finalization/finalize.js +717 -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 +37 -0
- package/dist/modules/finalization/nextjs-finalize.js +329 -0
- package/dist/modules/finalization/static-runtime.d.ts +9 -0
- package/dist/modules/finalization/static-runtime.js +261 -0
- package/dist/modules/format-injection/index.d.ts +19 -0
- package/dist/modules/format-injection/index.js +272 -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/glossary.d.ts +9 -0
- package/dist/modules/glossary/glossary.js +27 -0
- package/dist/modules/glossary/index.d.ts +3 -0
- package/dist/modules/glossary/index.js +8 -0
- package/dist/modules/glossary/masker.d.ts +23 -0
- package/dist/modules/glossary/masker.js +56 -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 +1066 -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 +116 -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 +107 -0
- package/dist/modules/locale-builder/po.d.ts +33 -0
- package/dist/modules/locale-builder/po.js +131 -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 +21 -0
- package/dist/modules/lockfile/lockfile-manager.d.ts +90 -0
- package/dist/modules/lockfile/lockfile-manager.js +482 -0
- package/dist/modules/lockfile/schema.d.ts +154 -0
- package/dist/modules/lockfile/schema.js +45 -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/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 +38 -0
- package/dist/modules/translator/translate.js +932 -0
- package/dist/modules/translator/translator.d.ts +5 -0
- package/dist/modules/translator/translator.js +46 -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 +298 -0
- package/dist/orchestrator.d.ts +8 -0
- package/dist/orchestrator.js +616 -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 +145 -0
- package/dist/providers/types.js +2 -0
- package/dist/qa/check-project.d.ts +49 -0
- package/dist/qa/check-project.js +150 -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 +325 -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 +36 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/types.d.ts +363 -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/dtl-mask.d.ts +25 -0
- package/dist/utils/dtl-mask.js +58 -0
- package/dist/utils/extraction-options.d.ts +23 -0
- package/dist/utils/extraction-options.js +23 -0
- package/dist/utils/file-utils.d.ts +33 -0
- package/dist/utils/file-utils.js +262 -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 +621 -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 +80 -0
- package/dist/utils/metadata.js +255 -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 +36 -0
- package/dist/utils/placeholder-protection.d.ts +57 -0
- package/dist/utils/placeholder-protection.js +136 -0
- package/dist/utils/pseudo-markers.d.ts +9 -0
- package/dist/utils/pseudo-markers.js +16 -0
- package/dist/utils/py-extractor.d.ts +14 -0
- package/dist/utils/py-extractor.js +285 -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 +10 -0
- package/dist/utils/translation-sanitizer.js +57 -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 +293 -0
- package/dist/utils/ui-copy.d.ts +32 -0
- package/dist/utils/ui-copy.js +126 -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,90 @@
|
|
|
1
|
+
import type { LockfileEntry } from './schema';
|
|
2
|
+
import { type LockfileData, type LockfilePartition, type LockfileStats, type PartitionedKey } from './schema';
|
|
3
|
+
/**
|
|
4
|
+
* For each target locale, list of translation keys that must go through the translator
|
|
5
|
+
* (new keys, source-changed keys, or keys missing that locale in the lockfile).
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildKeysToTranslateByTargetLocale(partition: LockfilePartition, targetLocales: string[]): Record<string, string[]>;
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the lockfile path for a project:
|
|
10
|
+
* `<dataDir>/.babelize/{projectId}/babelize-lock.json`. `dataDir` defaults to
|
|
11
|
+
* $BABELIZE_DATA_DIR, else `process.cwd()` (the cloud backend). Local runs
|
|
12
|
+
* pass it explicitly so concurrent runs in one process never share it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getLockfilePath(projectId: string, dataDir?: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Load and validate the lockfile from disk.
|
|
17
|
+
* Returns an empty lockfile if the file doesn't exist or is malformed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadLockfile(projectId: string, dataDir?: string): Promise<LockfileData>;
|
|
20
|
+
/**
|
|
21
|
+
* Persist lockfile to disk atomically (write→tmp→rename).
|
|
22
|
+
* Uses advisory file locking via atomic mkdir to prevent concurrent writes
|
|
23
|
+
* from overwriting each other (two simultaneous runs for the same project).
|
|
24
|
+
*/
|
|
25
|
+
export declare function saveLockfile(projectId: string, data: LockfileData, dataDir?: string): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Delete the lockfile for a project (used by "Force full re-translate").
|
|
28
|
+
*/
|
|
29
|
+
export declare function clearLockfile(projectId: string, dataDir?: string): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Purge a specific locale's translations from the lockfile.
|
|
32
|
+
* Used when a language is removed from a project so re-adding it
|
|
33
|
+
* triggers fresh translation instead of resurrecting stale cache.
|
|
34
|
+
*/
|
|
35
|
+
export declare function removeLocaleFromLockfile(projectId: string, localeCode: string, dataDir?: string): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Partition generated keys into unchanged / new / changed / missingLocales
|
|
38
|
+
* by comparing source hashes against the lockfile.
|
|
39
|
+
*
|
|
40
|
+
* @param lockfile - Current lockfile data
|
|
41
|
+
* @param keys - Flat map of translation key → source string value
|
|
42
|
+
* @param targetLocales - Locales the user is targeting this run
|
|
43
|
+
*/
|
|
44
|
+
/** Options for reading the lockfile during a run. */
|
|
45
|
+
export interface LockfileReadOptions {
|
|
46
|
+
/** This run is pseudo-localization: pseudo entries count as done. Otherwise they count as missing. */
|
|
47
|
+
pseudo?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* True when the stored value for `locale` is pseudo-localization and this run
|
|
51
|
+
* is a real one: it must be translated, never reused. Recognised by the
|
|
52
|
+
* `pseudo` metadata flag, or by the pseudo markers for lockfiles written
|
|
53
|
+
* before the flag existed.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isStalePseudoEntry(entry: LockfileEntry, locale: string, options?: LockfileReadOptions): boolean;
|
|
56
|
+
export declare function partitionKeys(lockfile: LockfileData, keys: Record<string, string>, targetLocales: string[], options?: LockfileReadOptions): LockfilePartition;
|
|
57
|
+
/**
|
|
58
|
+
* Merge newly translated values and metadata into the lockfile.
|
|
59
|
+
*
|
|
60
|
+
* @param lockfile - Current lockfile (will be mutated)
|
|
61
|
+
* @param keys - Flat map of key → source value (for hashing)
|
|
62
|
+
* @param translations - Map of locale → { key → translated string }
|
|
63
|
+
* @param projectId - Project identifier
|
|
64
|
+
* @param metadata - Map of locale → { key → metadata object }
|
|
65
|
+
*/
|
|
66
|
+
/**
|
|
67
|
+
* Remove the stored translation of every key that failed this run.
|
|
68
|
+
* updateLockfile records the new source fingerprint for every key and keeps
|
|
69
|
+
* the previous translation where a locale produced none — for a key whose
|
|
70
|
+
* source text changed and then failed, that previous value translates the
|
|
71
|
+
* *old* text. Dropping it makes the next run treat the locale as missing and
|
|
72
|
+
* retry it, instead of silently reusing a stale translation.
|
|
73
|
+
*/
|
|
74
|
+
export declare function dropFailedTranslations(lockfile: LockfileData, failures: Array<{
|
|
75
|
+
key: string;
|
|
76
|
+
locale: string;
|
|
77
|
+
}>): LockfileData;
|
|
78
|
+
/** True when `next` differs from `previous` in anything but timestamps worth saving. */
|
|
79
|
+
export declare function lockfileChanged(previous: LockfileData, next: LockfileData): boolean;
|
|
80
|
+
export declare function updateLockfile(lockfile: LockfileData, keys: Record<string, string>, translations: Record<string, Record<string, string>>, projectId: string, metadata?: Record<string, Record<string, any>>): LockfileData;
|
|
81
|
+
/**
|
|
82
|
+
* Extract cached translations for unchanged keys from the lockfile.
|
|
83
|
+
*
|
|
84
|
+
* @returns Map of locale → { key → translated string }
|
|
85
|
+
*/
|
|
86
|
+
export declare function collectCachedTranslations(lockfile: LockfileData, unchangedKeys: PartitionedKey[], targetLocales: string[], options?: LockfileReadOptions): Record<string, Record<string, string>>;
|
|
87
|
+
/**
|
|
88
|
+
* Compute stats for the UI: X translated, Y reused, Z changed.
|
|
89
|
+
*/
|
|
90
|
+
export declare function computeLockfileStats(partition: LockfilePartition): LockfileStats;
|
|
@@ -0,0 +1,482 @@
|
|
|
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.buildKeysToTranslateByTargetLocale = buildKeysToTranslateByTargetLocale;
|
|
37
|
+
exports.getLockfilePath = getLockfilePath;
|
|
38
|
+
exports.loadLockfile = loadLockfile;
|
|
39
|
+
exports.saveLockfile = saveLockfile;
|
|
40
|
+
exports.clearLockfile = clearLockfile;
|
|
41
|
+
exports.removeLocaleFromLockfile = removeLocaleFromLockfile;
|
|
42
|
+
exports.isStalePseudoEntry = isStalePseudoEntry;
|
|
43
|
+
exports.partitionKeys = partitionKeys;
|
|
44
|
+
exports.dropFailedTranslations = dropFailedTranslations;
|
|
45
|
+
exports.lockfileChanged = lockfileChanged;
|
|
46
|
+
exports.updateLockfile = updateLockfile;
|
|
47
|
+
exports.collectCachedTranslations = collectCachedTranslations;
|
|
48
|
+
exports.computeLockfileStats = computeLockfileStats;
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const pseudo_markers_1 = require("../../utils/pseudo-markers");
|
|
51
|
+
const fs = __importStar(require("fs-extra"));
|
|
52
|
+
const logger_1 = require("../../utils/logger");
|
|
53
|
+
const hasher_1 = require("./hasher");
|
|
54
|
+
const schema_1 = require("./schema");
|
|
55
|
+
const LOCK_RETRIES = 20;
|
|
56
|
+
const LOCK_RETRY_DELAY_MS = 50;
|
|
57
|
+
async function acquireLock(lockDir) {
|
|
58
|
+
let lastErr = null;
|
|
59
|
+
for (let i = 0; i < LOCK_RETRIES; i++) {
|
|
60
|
+
try {
|
|
61
|
+
await fs.mkdir(lockDir, { recursive: false });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
lastErr = err;
|
|
66
|
+
if (err.code !== 'EEXIST')
|
|
67
|
+
throw err;
|
|
68
|
+
await new Promise((r) => setTimeout(r, LOCK_RETRY_DELAY_MS));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`Could not acquire lockfile lock after ${LOCK_RETRIES} retries: ${lastErr?.message}`);
|
|
72
|
+
}
|
|
73
|
+
async function releaseLock(lockDir) {
|
|
74
|
+
try {
|
|
75
|
+
await fs.rmdir(lockDir);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Best-effort cleanup
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* For each target locale, list of translation keys that must go through the translator
|
|
83
|
+
* (new keys, source-changed keys, or keys missing that locale in the lockfile).
|
|
84
|
+
*/
|
|
85
|
+
function buildKeysToTranslateByTargetLocale(partition, targetLocales) {
|
|
86
|
+
const byLocale = {};
|
|
87
|
+
for (const loc of targetLocales) {
|
|
88
|
+
byLocale[loc] = new Set();
|
|
89
|
+
}
|
|
90
|
+
for (const { key } of partition.newKeys) {
|
|
91
|
+
for (const loc of targetLocales) {
|
|
92
|
+
byLocale[loc].add(key);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const { key } of partition.changed) {
|
|
96
|
+
for (const loc of targetLocales) {
|
|
97
|
+
byLocale[loc].add(key);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const { key, missingLocales } of partition.missingLocales) {
|
|
101
|
+
for (const loc of missingLocales) {
|
|
102
|
+
if (byLocale[loc])
|
|
103
|
+
byLocale[loc].add(key);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const result = {};
|
|
107
|
+
for (const loc of targetLocales) {
|
|
108
|
+
result[loc] = Array.from(byLocale[loc]);
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
// ─────────────────────────────────────────────────────────────
|
|
113
|
+
// Path helpers
|
|
114
|
+
// ─────────────────────────────────────────────────────────────
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the lockfile path for a project:
|
|
117
|
+
* `<dataDir>/.babelize/{projectId}/babelize-lock.json`. `dataDir` defaults to
|
|
118
|
+
* $BABELIZE_DATA_DIR, else `process.cwd()` (the cloud backend). Local runs
|
|
119
|
+
* pass it explicitly so concurrent runs in one process never share it.
|
|
120
|
+
*/
|
|
121
|
+
function getLockfilePath(projectId, dataDir) {
|
|
122
|
+
const baseDir = dataDir || process.env.BABELIZE_DATA_DIR || process.cwd();
|
|
123
|
+
return path.join(baseDir, '.babelize', projectId, 'babelize-lock.json');
|
|
124
|
+
}
|
|
125
|
+
// ─────────────────────────────────────────────────────────────
|
|
126
|
+
// Load
|
|
127
|
+
// ─────────────────────────────────────────────────────────────
|
|
128
|
+
/**
|
|
129
|
+
* Load and validate the lockfile from disk.
|
|
130
|
+
* Returns an empty lockfile if the file doesn't exist or is malformed.
|
|
131
|
+
*/
|
|
132
|
+
async function loadLockfile(projectId, dataDir) {
|
|
133
|
+
const lockfilePath = getLockfilePath(projectId, dataDir);
|
|
134
|
+
try {
|
|
135
|
+
if (!(await fs.pathExists(lockfilePath))) {
|
|
136
|
+
logger_1.engineLogger.info('No existing lockfile found, starting fresh', { projectId });
|
|
137
|
+
return (0, schema_1.createEmptyLockfile)(projectId);
|
|
138
|
+
}
|
|
139
|
+
const raw = await fs.readJson(lockfilePath);
|
|
140
|
+
const parsed = schema_1.lockfileDataSchema.safeParse(raw);
|
|
141
|
+
if (!parsed.success) {
|
|
142
|
+
logger_1.engineLogger.warn('Lockfile validation failed, starting fresh', {
|
|
143
|
+
projectId,
|
|
144
|
+
errors: parsed.error.issues,
|
|
145
|
+
});
|
|
146
|
+
return (0, schema_1.createEmptyLockfile)(projectId);
|
|
147
|
+
}
|
|
148
|
+
logger_1.engineLogger.info('Loaded lockfile', {
|
|
149
|
+
projectId,
|
|
150
|
+
keyCount: Object.keys(parsed.data.keys).length,
|
|
151
|
+
});
|
|
152
|
+
return parsed.data;
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
logger_1.engineLogger.warn('Failed to read lockfile, starting fresh', {
|
|
156
|
+
projectId,
|
|
157
|
+
error: err instanceof Error ? err.message : String(err),
|
|
158
|
+
});
|
|
159
|
+
return (0, schema_1.createEmptyLockfile)(projectId);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// ─────────────────────────────────────────────────────────────
|
|
163
|
+
// Save
|
|
164
|
+
// ─────────────────────────────────────────────────────────────
|
|
165
|
+
/**
|
|
166
|
+
* Persist lockfile to disk atomically (write→tmp→rename).
|
|
167
|
+
* Uses advisory file locking via atomic mkdir to prevent concurrent writes
|
|
168
|
+
* from overwriting each other (two simultaneous runs for the same project).
|
|
169
|
+
*/
|
|
170
|
+
async function saveLockfile(projectId, data, dataDir) {
|
|
171
|
+
const lockfilePath = getLockfilePath(projectId, dataDir);
|
|
172
|
+
const lockDir = lockfilePath + '.lock';
|
|
173
|
+
const tmpPath = lockfilePath + '.tmp';
|
|
174
|
+
await fs.ensureDir(path.dirname(lockfilePath));
|
|
175
|
+
await acquireLock(lockDir);
|
|
176
|
+
try {
|
|
177
|
+
// Re-read the current lockfile and merge any keys written by a concurrent run
|
|
178
|
+
// between when we loaded data and now.
|
|
179
|
+
const merged = data;
|
|
180
|
+
try {
|
|
181
|
+
if (await fs.pathExists(lockfilePath)) {
|
|
182
|
+
const current = await fs.readJson(lockfilePath);
|
|
183
|
+
for (const [key, entry] of Object.entries(current.keys || {})) {
|
|
184
|
+
if (!merged.keys[key]) {
|
|
185
|
+
merged.keys[key] = entry;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (readErr) {
|
|
191
|
+
logger_1.engineLogger.warn('Failed to re-read lockfile for merge, proceeding with current data', {
|
|
192
|
+
projectId,
|
|
193
|
+
error: readErr instanceof Error ? readErr.message : String(readErr),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
merged.updatedAt = new Date().toISOString();
|
|
197
|
+
await fs.writeJson(tmpPath, merged, { spaces: 2 });
|
|
198
|
+
await fs.rename(tmpPath, lockfilePath);
|
|
199
|
+
logger_1.engineLogger.info('Saved lockfile', {
|
|
200
|
+
projectId,
|
|
201
|
+
keyCount: Object.keys(merged.keys).length,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
logger_1.engineLogger.error('Failed to save lockfile', {
|
|
206
|
+
projectId,
|
|
207
|
+
error: err instanceof Error ? err.message : String(err),
|
|
208
|
+
});
|
|
209
|
+
await fs.remove(tmpPath).catch((cleanupErr) => {
|
|
210
|
+
logger_1.engineLogger.warn('Failed to clean up temp lockfile', { tmpPath, error: cleanupErr });
|
|
211
|
+
});
|
|
212
|
+
throw err;
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
await releaseLock(lockDir);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// ─────────────────────────────────────────────────────────────
|
|
219
|
+
// Clear (force re-translate)
|
|
220
|
+
// ─────────────────────────────────────────────────────────────
|
|
221
|
+
/**
|
|
222
|
+
* Delete the lockfile for a project (used by "Force full re-translate").
|
|
223
|
+
*/
|
|
224
|
+
async function clearLockfile(projectId, dataDir) {
|
|
225
|
+
const lockfilePath = getLockfilePath(projectId, dataDir);
|
|
226
|
+
try {
|
|
227
|
+
if (await fs.pathExists(lockfilePath)) {
|
|
228
|
+
await fs.remove(lockfilePath);
|
|
229
|
+
logger_1.engineLogger.info('Cleared lockfile', { projectId });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
logger_1.engineLogger.warn('Failed to clear lockfile', {
|
|
234
|
+
projectId,
|
|
235
|
+
error: err instanceof Error ? err.message : String(err),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Purge a specific locale's translations from the lockfile.
|
|
241
|
+
* Used when a language is removed from a project so re-adding it
|
|
242
|
+
* triggers fresh translation instead of resurrecting stale cache.
|
|
243
|
+
*/
|
|
244
|
+
async function removeLocaleFromLockfile(projectId, localeCode, dataDir) {
|
|
245
|
+
const lockfilePath = getLockfilePath(projectId, dataDir);
|
|
246
|
+
try {
|
|
247
|
+
if (!(await fs.pathExists(lockfilePath))) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const lockfile = await loadLockfile(projectId, dataDir);
|
|
251
|
+
let removedCount = 0;
|
|
252
|
+
for (const [key, entry] of Object.entries(lockfile.keys)) {
|
|
253
|
+
if (localeCode in entry.languages) {
|
|
254
|
+
delete entry.languages[localeCode];
|
|
255
|
+
removedCount++;
|
|
256
|
+
}
|
|
257
|
+
if (entry.metadata && localeCode in entry.metadata) {
|
|
258
|
+
delete entry.metadata[localeCode];
|
|
259
|
+
}
|
|
260
|
+
if (Object.keys(entry.languages).length === 0) {
|
|
261
|
+
delete lockfile.keys[key];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (removedCount > 0) {
|
|
265
|
+
lockfile.updatedAt = new Date().toISOString();
|
|
266
|
+
// Write directly (bypass saveLockfile's merge logic which would
|
|
267
|
+
// re-add deleted keys from the on-disk file)
|
|
268
|
+
const lockDir = lockfilePath + '.lock';
|
|
269
|
+
const tmpPath = lockfilePath + '.tmp';
|
|
270
|
+
await fs.ensureDir(path.dirname(lockfilePath));
|
|
271
|
+
await acquireLock(lockDir);
|
|
272
|
+
try {
|
|
273
|
+
await fs.writeJson(tmpPath, lockfile, { spaces: 2 });
|
|
274
|
+
await fs.rename(tmpPath, lockfilePath);
|
|
275
|
+
}
|
|
276
|
+
finally {
|
|
277
|
+
await releaseLock(lockDir);
|
|
278
|
+
}
|
|
279
|
+
logger_1.engineLogger.info('Purged locale from lockfile', {
|
|
280
|
+
projectId,
|
|
281
|
+
localeCode,
|
|
282
|
+
keysAffected: removedCount,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
logger_1.engineLogger.warn('Failed to purge locale from lockfile', {
|
|
288
|
+
projectId,
|
|
289
|
+
localeCode,
|
|
290
|
+
error: err instanceof Error ? err.message : String(err),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* True when the stored value for `locale` is pseudo-localization and this run
|
|
296
|
+
* is a real one: it must be translated, never reused. Recognised by the
|
|
297
|
+
* `pseudo` metadata flag, or by the pseudo markers for lockfiles written
|
|
298
|
+
* before the flag existed.
|
|
299
|
+
*/
|
|
300
|
+
function isStalePseudoEntry(entry, locale, options = {}) {
|
|
301
|
+
if (options.pseudo)
|
|
302
|
+
return false;
|
|
303
|
+
return entry.metadata?.[locale]?.pseudo === true || (0, pseudo_markers_1.isPseudoLocalized)(entry.languages[locale] ?? '');
|
|
304
|
+
}
|
|
305
|
+
function partitionKeys(lockfile, keys, targetLocales, options = {}) {
|
|
306
|
+
const result = {
|
|
307
|
+
unchanged: [],
|
|
308
|
+
newKeys: [],
|
|
309
|
+
changed: [],
|
|
310
|
+
missingLocales: [],
|
|
311
|
+
};
|
|
312
|
+
for (const [key, value] of Object.entries(keys)) {
|
|
313
|
+
const currentHash = (0, hasher_1.hashSourceString)(value);
|
|
314
|
+
const entry = lockfile.keys[key];
|
|
315
|
+
if (!entry) {
|
|
316
|
+
// Key not seen before
|
|
317
|
+
result.newKeys.push({ key, value });
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (entry.sourceHash !== currentHash) {
|
|
321
|
+
// Source string changed
|
|
322
|
+
result.changed.push({ key, value });
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
// Hash matches — check if all target locales are present
|
|
326
|
+
const missing = targetLocales.filter((locale) => !(locale in entry.languages) || isStalePseudoEntry(entry, locale, options));
|
|
327
|
+
if (missing.length === 0) {
|
|
328
|
+
// Fully translated and unchanged
|
|
329
|
+
result.unchanged.push({ key, value });
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
// Some locales missing
|
|
333
|
+
result.missingLocales.push({ key, value, missingLocales: missing });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
338
|
+
// ─────────────────────────────────────────────────────────────
|
|
339
|
+
// Update lockfile with new translations
|
|
340
|
+
// ─────────────────────────────────────────────────────────────
|
|
341
|
+
/**
|
|
342
|
+
* Merge newly translated values and metadata into the lockfile.
|
|
343
|
+
*
|
|
344
|
+
* @param lockfile - Current lockfile (will be mutated)
|
|
345
|
+
* @param keys - Flat map of key → source value (for hashing)
|
|
346
|
+
* @param translations - Map of locale → { key → translated string }
|
|
347
|
+
* @param projectId - Project identifier
|
|
348
|
+
* @param metadata - Map of locale → { key → metadata object }
|
|
349
|
+
*/
|
|
350
|
+
/**
|
|
351
|
+
* Remove the stored translation of every key that failed this run.
|
|
352
|
+
* updateLockfile records the new source fingerprint for every key and keeps
|
|
353
|
+
* the previous translation where a locale produced none — for a key whose
|
|
354
|
+
* source text changed and then failed, that previous value translates the
|
|
355
|
+
* *old* text. Dropping it makes the next run treat the locale as missing and
|
|
356
|
+
* retry it, instead of silently reusing a stale translation.
|
|
357
|
+
*/
|
|
358
|
+
function dropFailedTranslations(lockfile, failures) {
|
|
359
|
+
if (failures.length === 0)
|
|
360
|
+
return lockfile;
|
|
361
|
+
const cloned = JSON.parse(JSON.stringify(lockfile));
|
|
362
|
+
for (const { key, locale } of failures) {
|
|
363
|
+
const entry = cloned.keys[key];
|
|
364
|
+
if (entry?.languages)
|
|
365
|
+
delete entry.languages[locale];
|
|
366
|
+
if (entry?.metadata)
|
|
367
|
+
delete entry.metadata[locale];
|
|
368
|
+
}
|
|
369
|
+
return cloned;
|
|
370
|
+
}
|
|
371
|
+
/** The metadata fields the lockfile schema keeps (anything else is dropped on load anyway). */
|
|
372
|
+
function pickLockfileMetadata(meta) {
|
|
373
|
+
const out = {};
|
|
374
|
+
if (meta.pseudo === true)
|
|
375
|
+
out.pseudo = true;
|
|
376
|
+
if (typeof meta.fromMemory === 'boolean')
|
|
377
|
+
out.fromMemory = meta.fromMemory;
|
|
378
|
+
if (typeof meta.sourceHash === 'string')
|
|
379
|
+
out.sourceHash = meta.sourceHash;
|
|
380
|
+
if (typeof meta.inTm === 'boolean')
|
|
381
|
+
out.inTm = meta.inTm;
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
384
|
+
function sameJson(a, b) {
|
|
385
|
+
return JSON.stringify(a ?? {}) === JSON.stringify(b ?? {});
|
|
386
|
+
}
|
|
387
|
+
/** True when `next` differs from `previous` in anything but timestamps worth saving. */
|
|
388
|
+
function lockfileChanged(previous, next) {
|
|
389
|
+
return previous.projectId !== next.projectId || JSON.stringify(previous.keys) !== JSON.stringify(next.keys);
|
|
390
|
+
}
|
|
391
|
+
function updateLockfile(lockfile, keys, translations, projectId, metadata) {
|
|
392
|
+
// Deep-clone to prevent in-place mutation of the caller's reference.
|
|
393
|
+
// Prevents races when multiple concurrent runs share the same lockfile object.
|
|
394
|
+
const cloned = JSON.parse(JSON.stringify(lockfile));
|
|
395
|
+
const now = new Date().toISOString();
|
|
396
|
+
cloned.projectId = projectId;
|
|
397
|
+
for (const [key, value] of Object.entries(keys)) {
|
|
398
|
+
const sourceHash = (0, hasher_1.hashSourceString)(value);
|
|
399
|
+
const existing = cloned.keys[key];
|
|
400
|
+
// An entry is only touched (and re-stamped) when something in it
|
|
401
|
+
// actually changes, so a no-change rerun leaves the committed
|
|
402
|
+
// lockfile byte-identical.
|
|
403
|
+
const entry = existing ?? { sourceHash, translatedAt: now, languages: {}, metadata: {} };
|
|
404
|
+
let changed = !existing || existing.sourceHash !== sourceHash;
|
|
405
|
+
entry.sourceHash = sourceHash;
|
|
406
|
+
// Merge per-locale translations and metadata
|
|
407
|
+
for (const [locale, translatedKeys] of Object.entries(translations)) {
|
|
408
|
+
if (translatedKeys[key]) {
|
|
409
|
+
// Skip caching identity translations (same as source text) for non-trivial strings.
|
|
410
|
+
// Identity translations mean the translator fell back to source text
|
|
411
|
+
// (e.g., because the service was unavailable). Caching them would cause
|
|
412
|
+
// subsequent runs to skip re-translation and produce fake locale files.
|
|
413
|
+
const translated = translatedKeys[key];
|
|
414
|
+
if (translated === value && value.length > 4) {
|
|
415
|
+
logger_1.engineLogger.warn(`Skipping cache for "${key}" → "${locale}": identity translation (same as source). Translator may be unavailable.`);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (entry.languages[locale] !== translated) {
|
|
419
|
+
entry.languages[locale] = translated;
|
|
420
|
+
changed = true;
|
|
421
|
+
}
|
|
422
|
+
// Metadata from this run replaces the previous run's rather than
|
|
423
|
+
// merging into it, so a stale `fromMemory: true` doesn't survive
|
|
424
|
+
// the key being re-translated. Keys reused from the lockfile
|
|
425
|
+
// carry no new metadata and keep what they had.
|
|
426
|
+
if (metadata?.[locale]?.[key]) {
|
|
427
|
+
const next = pickLockfileMetadata(metadata[locale][key]);
|
|
428
|
+
if (!sameJson(entry.metadata?.[locale], next)) {
|
|
429
|
+
if (!entry.metadata)
|
|
430
|
+
entry.metadata = {};
|
|
431
|
+
entry.metadata[locale] = next;
|
|
432
|
+
changed = true;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (changed) {
|
|
438
|
+
entry.translatedAt = now;
|
|
439
|
+
if (!entry.metadata)
|
|
440
|
+
entry.metadata = {};
|
|
441
|
+
cloned.keys[key] = entry;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return cloned;
|
|
445
|
+
}
|
|
446
|
+
// ─────────────────────────────────────────────────────────────
|
|
447
|
+
// Collect cached translations from lockfile
|
|
448
|
+
// ─────────────────────────────────────────────────────────────
|
|
449
|
+
/**
|
|
450
|
+
* Extract cached translations for unchanged keys from the lockfile.
|
|
451
|
+
*
|
|
452
|
+
* @returns Map of locale → { key → translated string }
|
|
453
|
+
*/
|
|
454
|
+
function collectCachedTranslations(lockfile, unchangedKeys, targetLocales, options = {}) {
|
|
455
|
+
const cached = {};
|
|
456
|
+
for (const locale of targetLocales) {
|
|
457
|
+
cached[locale] = {};
|
|
458
|
+
}
|
|
459
|
+
for (const { key } of unchangedKeys) {
|
|
460
|
+
const entry = lockfile.keys[key];
|
|
461
|
+
if (!entry)
|
|
462
|
+
continue;
|
|
463
|
+
for (const locale of targetLocales) {
|
|
464
|
+
if (entry.languages[locale] && !isStalePseudoEntry(entry, locale, options)) {
|
|
465
|
+
cached[locale][key] = entry.languages[locale];
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return cached;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Compute stats for the UI: X translated, Y reused, Z changed.
|
|
473
|
+
*/
|
|
474
|
+
function computeLockfileStats(partition) {
|
|
475
|
+
return {
|
|
476
|
+
reused: partition.unchanged.length,
|
|
477
|
+
translated: partition.newKeys.length +
|
|
478
|
+
partition.changed.length +
|
|
479
|
+
partition.missingLocales.length,
|
|
480
|
+
changed: partition.changed.length,
|
|
481
|
+
};
|
|
482
|
+
}
|