@babelize/engine 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/analyze.js +1 -1
- package/dist/detection/i18n.d.ts +19 -0
- package/dist/detection/i18n.js +144 -0
- package/dist/detection/stack.d.ts +2 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +9 -2
- package/dist/injection/html/html-injector.d.ts +2 -0
- package/dist/injection/html/html-injector.js +55 -6
- package/dist/local/hosted-provider.d.ts +30 -2
- package/dist/local/hosted-provider.js +50 -3
- package/dist/local/translate-core.d.ts +2 -2
- package/dist/local/translate-core.js +6 -2
- package/dist/modules/finalization/finalize.js +6 -4
- package/dist/modules/finalization/static-runtime.d.ts +2 -2
- package/dist/modules/finalization/static-runtime.js +10 -5
- package/dist/modules/lockfile/index.d.ts +1 -1
- package/dist/modules/lockfile/index.js +2 -1
- package/dist/modules/lockfile/lockfile-manager.d.ts +9 -0
- package/dist/modules/lockfile/lockfile-manager.js +43 -1
- package/dist/modules/lockfile/schema.d.ts +14 -0
- package/dist/modules/lockfile/schema.js +2 -0
- package/dist/modules/overrides/index.d.ts +41 -0
- package/dist/modules/overrides/index.js +50 -0
- package/dist/modules/translator/budget.d.ts +25 -0
- package/dist/modules/translator/budget.js +46 -0
- package/dist/modules/translator/translate.d.ts +11 -0
- package/dist/modules/translator/translate.js +50 -10
- package/dist/orchestrator.js +54 -6
- package/dist/providers/types.d.ts +6 -0
- package/dist/runtime/config.js +1 -0
- package/dist/runtime/types.d.ts +21 -0
- package/dist/utils/translator-caller.js +6 -3
- package/package.json +1 -1
package/dist/analysis/analyze.js
CHANGED
|
@@ -139,7 +139,7 @@ async function analyzeWorkspace(workspacePath, options = {}) {
|
|
|
139
139
|
const framework = workspace.framework;
|
|
140
140
|
const support = frameworkSupport(framework);
|
|
141
141
|
if (support === 'unsupported') {
|
|
142
|
-
return empty(framework, `Detected ${framework}, which local translation does not support. Supported: React, Next.js and
|
|
142
|
+
return empty(framework, `Detected ${framework}, which local translation does not support. Supported: React, Next.js, Django and static HTML (Vue and React Native in beta).`);
|
|
143
143
|
}
|
|
144
144
|
const phases = (types_1.FRAMEWORK_CONFIGS[framework] ?? types_1.FRAMEWORK_CONFIGS.unknown).phases;
|
|
145
145
|
// Same ICU decision as a local translate run (whose translator is an LLM).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RepoFiles } from './stack';
|
|
2
|
+
export type I18nLibraryId = 'next-intl' | 'next-i18next' | 'react-i18next' | 'i18next' | 'vue-i18n' | 'react-intl' | 'lingui' | 'django';
|
|
3
|
+
export declare const I18N_LIBRARY_NAMES: Record<I18nLibraryId, string>;
|
|
4
|
+
export type SourceLanguageBasis = 'config' | 'files' | 'default';
|
|
5
|
+
export interface DetectedI18n {
|
|
6
|
+
library: {
|
|
7
|
+
id: I18nLibraryId;
|
|
8
|
+
name: string;
|
|
9
|
+
} | null;
|
|
10
|
+
/** Locale codes the repo already has files for, as written (en, pt-BR, zh_Hant). */
|
|
11
|
+
locales: string[];
|
|
12
|
+
/** Where the locale files live, e.g. "messages" or "public/locales". */
|
|
13
|
+
localeDir: string | null;
|
|
14
|
+
sourceLanguage: {
|
|
15
|
+
code: string;
|
|
16
|
+
basis: SourceLanguageBasis;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export declare function detectI18n(files: RepoFiles, framework?: string): Promise<DetectedI18n>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.I18N_LIBRARY_NAMES = void 0;
|
|
4
|
+
exports.detectI18n = detectI18n;
|
|
5
|
+
/**
|
|
6
|
+
* What a repository already has for i18n, read from a file listing and a few
|
|
7
|
+
* small files (no clone): the i18n library, existing locale files, and the
|
|
8
|
+
* source language. Runs on the same `RepoFiles` as `detectStack`, so the
|
|
9
|
+
* new-project flow can say "Next.js · English · next-intl detected" and
|
|
10
|
+
* pre-select the languages the repo already ships.
|
|
11
|
+
*/
|
|
12
|
+
const locale_files_1 = require("../qa/locale-files");
|
|
13
|
+
exports.I18N_LIBRARY_NAMES = {
|
|
14
|
+
'next-intl': 'next-intl',
|
|
15
|
+
'next-i18next': 'next-i18next',
|
|
16
|
+
'react-i18next': 'react-i18next',
|
|
17
|
+
i18next: 'i18next',
|
|
18
|
+
'vue-i18n': 'vue-i18n',
|
|
19
|
+
'react-intl': 'react-intl',
|
|
20
|
+
lingui: 'Lingui',
|
|
21
|
+
django: 'Django i18n',
|
|
22
|
+
};
|
|
23
|
+
/** Most specific first: next-i18next depends on react-i18next, which depends on i18next. */
|
|
24
|
+
const LIBRARY_DEPS = [
|
|
25
|
+
['next-intl', ['next-intl']],
|
|
26
|
+
['next-i18next', ['next-i18next']],
|
|
27
|
+
['vue-i18n', ['vue-i18n', '@nuxtjs/i18n']],
|
|
28
|
+
['react-i18next', ['react-i18next']],
|
|
29
|
+
['react-intl', ['react-intl']],
|
|
30
|
+
['lingui', ['@lingui/core', '@lingui/react']],
|
|
31
|
+
['i18next', ['i18next']],
|
|
32
|
+
];
|
|
33
|
+
const IGNORED = /(?:^|\/)(?:node_modules|\.git|vendor|dist|build|\.next|out|coverage|__tests__|__fixtures__|fixtures|tests?|e2e)\//;
|
|
34
|
+
/** Files that commonly declare the default locale, checked in this order. */
|
|
35
|
+
const CONFIG_FILE = /(?:^|\/)(?:i18n(?:\/(?:routing|request|config|index))?|i18n\.config|next\.config|next-i18next\.config|nuxt\.config|src\/i18n(?:\/index)?|src\/plugins\/i18n|lingui\.config)\.(?:[cm]?[jt]s|json)$/;
|
|
36
|
+
/** defaultLocale: 'de', fallbackLng: "fr", sourceLocale: 'en', locale: 'en' (vue-i18n createI18n). */
|
|
37
|
+
const DEFAULT_LOCALE_PATTERNS = [
|
|
38
|
+
/\bdefaultLocale\s*[:=]\s*['"`]([A-Za-z_-]{2,12})['"`]/,
|
|
39
|
+
/\bsourceLocale\s*[:=]\s*['"`]([A-Za-z_-]{2,12})['"`]/,
|
|
40
|
+
/\bfallbackLng\s*[:=]\s*['"`]([A-Za-z_-]{2,12})['"`]/,
|
|
41
|
+
/\bfallbackLocale\s*[:=]\s*['"`]([A-Za-z_-]{2,12})['"`]/,
|
|
42
|
+
];
|
|
43
|
+
const DJANGO_LANGUAGE_CODE = /^\s*LANGUAGE_CODE\s*=\s*['"]([A-Za-z_-]{2,12})['"]/m;
|
|
44
|
+
/** Django: locale/<code>/LC_MESSAGES/django.po */
|
|
45
|
+
const DJANGO_PO = /(?:^|\/)locale\/([A-Za-z_-]{2,12})\/LC_MESSAGES\/django\.po$/;
|
|
46
|
+
function depth(p) {
|
|
47
|
+
return p.split('/').length - 1;
|
|
48
|
+
}
|
|
49
|
+
async function readDeps(files, paths) {
|
|
50
|
+
const pkgs = paths
|
|
51
|
+
.filter((p) => (p === 'package.json' || p.endsWith('/package.json')) && depth(p) <= 2)
|
|
52
|
+
.sort((a, b) => depth(a) - depth(b))
|
|
53
|
+
.slice(0, 6);
|
|
54
|
+
const deps = new Set();
|
|
55
|
+
for (const p of pkgs) {
|
|
56
|
+
const raw = await files.read(p);
|
|
57
|
+
if (!raw)
|
|
58
|
+
continue;
|
|
59
|
+
try {
|
|
60
|
+
const pkg = JSON.parse(raw);
|
|
61
|
+
for (const name of Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }))
|
|
62
|
+
deps.add(name);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// not JSON: skip
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return deps;
|
|
69
|
+
}
|
|
70
|
+
/** "en-us" (Django) → "en-US"; "pt_BR" stays as written. */
|
|
71
|
+
function tidyCode(code) {
|
|
72
|
+
const m = /^([a-z]{2,3})-([a-z]{2})$/.exec(code);
|
|
73
|
+
return m ? `${m[1]}-${m[2].toUpperCase()}` : code;
|
|
74
|
+
}
|
|
75
|
+
async function detectI18n(files, framework) {
|
|
76
|
+
const paths = files.paths.filter((p) => !IGNORED.test(p));
|
|
77
|
+
// Library
|
|
78
|
+
let library = null;
|
|
79
|
+
if (framework === 'django') {
|
|
80
|
+
library = { id: 'django', name: exports.I18N_LIBRARY_NAMES.django };
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
const deps = await readDeps(files, paths);
|
|
84
|
+
const found = LIBRARY_DEPS.find(([, names]) => names.some((n) => deps.has(n)));
|
|
85
|
+
if (found)
|
|
86
|
+
library = { id: found[0], name: exports.I18N_LIBRARY_NAMES[found[0]] };
|
|
87
|
+
}
|
|
88
|
+
// Existing locale files: the set with the most locales wins.
|
|
89
|
+
let locales = [];
|
|
90
|
+
let localeDir = null;
|
|
91
|
+
let largestByLocale = new Map();
|
|
92
|
+
const sets = (0, locale_files_1.groupLocaleFiles)(paths);
|
|
93
|
+
if (sets.length > 0) {
|
|
94
|
+
const best = sets.reduce((a, b) => (Object.keys(b.files).length > Object.keys(a.files).length ? b : a));
|
|
95
|
+
locales = Object.keys(best.files).sort();
|
|
96
|
+
localeDir = best.dir;
|
|
97
|
+
if (files.size) {
|
|
98
|
+
largestByLocale = new Map(locales.map((l) => [l, best.files[l].reduce((sum, f) => sum + (files.size(f) ?? 0), 0)]));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const po = paths.map((p) => DJANGO_PO.exec(p)).filter((m) => !!m);
|
|
103
|
+
if (po.length > 0) {
|
|
104
|
+
locales = [...new Set(po.map((m) => m[1]))].sort();
|
|
105
|
+
localeDir = po[0][0].split('/').slice(0, -3).join('/') || '.';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Source language: declared in config, else inferred from the locale files.
|
|
109
|
+
let sourceLanguage = { code: 'en', basis: 'default' };
|
|
110
|
+
const configCandidates = paths
|
|
111
|
+
.filter((p) => CONFIG_FILE.test(p) && depth(p) <= 3)
|
|
112
|
+
.sort((a, b) => depth(a) - depth(b))
|
|
113
|
+
.slice(0, 6);
|
|
114
|
+
if (framework === 'django') {
|
|
115
|
+
configCandidates.unshift(...paths.filter((p) => /(?:^|\/)settings(?:\/base)?\.py$/.test(p) && depth(p) <= 3).slice(0, 2));
|
|
116
|
+
}
|
|
117
|
+
for (const p of configCandidates) {
|
|
118
|
+
const content = await files.read(p);
|
|
119
|
+
if (!content)
|
|
120
|
+
continue;
|
|
121
|
+
const m = p.endsWith('.py')
|
|
122
|
+
? DJANGO_LANGUAGE_CODE.exec(content)
|
|
123
|
+
: DEFAULT_LOCALE_PATTERNS.map((re) => re.exec(content)).find(Boolean);
|
|
124
|
+
const code = m?.[1] ? tidyCode(m[1]) : null;
|
|
125
|
+
if (code && (0, locale_files_1.isLocaleCode)(code)) {
|
|
126
|
+
sourceLanguage = { code, basis: 'config' };
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (sourceLanguage.basis === 'default' && locales.length > 0) {
|
|
131
|
+
if (locales.length === 1) {
|
|
132
|
+
sourceLanguage = { code: locales[0], basis: 'files' };
|
|
133
|
+
}
|
|
134
|
+
else if (locales.some((l) => l === 'en' || l.startsWith('en-') || l.startsWith('en_'))) {
|
|
135
|
+
sourceLanguage = { code: locales.find((l) => l === 'en') ?? locales.find((l) => l.startsWith('en')), basis: 'files' };
|
|
136
|
+
}
|
|
137
|
+
else if (largestByLocale.size > 0) {
|
|
138
|
+
// The source usually has every key: the biggest file set.
|
|
139
|
+
const [code] = [...largestByLocale.entries()].reduce((a, b) => (b[1] > a[1] ? b : a));
|
|
140
|
+
sourceLanguage = { code, basis: 'files' };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { library, locales, localeDir, sourceLanguage };
|
|
144
|
+
}
|
|
@@ -32,6 +32,8 @@ export interface RepoFiles {
|
|
|
32
32
|
read(relPath: string): Promise<string | null>;
|
|
33
33
|
/** The listing is incomplete (very large repository). */
|
|
34
34
|
truncated?: boolean;
|
|
35
|
+
/** File size in bytes, when the listing knows it. */
|
|
36
|
+
size?(relPath: string): number | undefined;
|
|
35
37
|
}
|
|
36
38
|
export declare const FRAMEWORK_DISPLAY_NAMES: Record<string, string>;
|
|
37
39
|
/** Does a Python dependency file list Django itself (not just django-* add-ons)? */
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export * from './providers';
|
|
|
18
18
|
export { runWithEngineLogger, setEngineLogger } from './utils/logger';
|
|
19
19
|
export type { EngineLogger, LogContext } from './utils/logger';
|
|
20
20
|
export * from './modules/lockfile';
|
|
21
|
+
export { applyTranslationOverrides, setLocaleValue, type OverrideReport, type TranslationOverride } from './modules/overrides';
|
|
21
22
|
export * from './modules/translator/language-codes';
|
|
22
23
|
export type { MemoryEntry, FuzzyMatch, MemoryLookupResult } from './modules/translator/memory/types';
|
|
23
24
|
export { matchFromMemory } from './modules/translator/memory/memory-matcher';
|
|
@@ -26,8 +27,10 @@ export type { SyncDetectionResult, SyncDetectedString, SyncMissingLocaleKey } fr
|
|
|
26
27
|
export type { TranslationEntry, WorkspaceInfo } from './runtime/types';
|
|
27
28
|
export { addBabelToHistory, detectProjectInfo, getBabelizeSummary, getProjectInfo, saveCachedStrings, saveProjectConfig, } from './utils/metadata';
|
|
28
29
|
export type { BabelHistoryEntry, ExtractedStringEntry, ProjectInfo } from './utils/metadata';
|
|
29
|
-
export { detectStack, localRepoFiles, FRAMEWORK_DISPLAY_NAMES } from './detection/stack';
|
|
30
|
+
export { detectStack, localRepoFiles, FRAMEWORK_DISPLAY_NAMES, SUPPORTED_SUMMARY } from './detection/stack';
|
|
30
31
|
export type { DetectedStack, RepoFiles, StackFramework, PackageManagerId } from './detection/stack';
|
|
32
|
+
export { detectI18n, I18N_LIBRARY_NAMES } from './detection/i18n';
|
|
33
|
+
export type { DetectedI18n, I18nLibraryId, SourceLanguageBasis } from './detection/i18n';
|
|
31
34
|
export { parseCode } from './utils/ast-helpers';
|
|
32
35
|
export { extractPlaceholders, hasICUSyntax, hashString, protectPlaceholders, restorePlaceholders, } from './utils/string-utils';
|
|
33
36
|
export { sanitizeTranslation, validateRawTokens } from './utils/translation-sanitizer';
|
package/dist/index.js
CHANGED
|
@@ -21,8 +21,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
21
21
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
22
22
|
};
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
-
exports.
|
|
25
|
-
exports.TRANSLATION_CACHE_TTL_SECONDS = exports.translationCacheKey = exports.hashText = exports.readProjectLocaleConfig = exports.localeIssueFile = exports.checkProjectLocales = exports.ReviewError = exports.reviewLocaleChanges = exports.loadLocale = exports.isLocaleCode = exports.isIgnoredLocalePath = exports.groupLocaleFiles = exports.findLocaleFileSets = exports.DEFAULT_LOCALE_IGNORE_DIRS = exports.glossaryFingerprint = exports.normalizeGlossary = exports.localeValue = exports.containsTerm = exports.canonicalizeLocaleKeys = exports.canonicalLocale = exports.tallyLocaleIssues = exports.flattenLocaleMessages = exports.checkLocalePair = exports.frameworkSupport = void 0;
|
|
24
|
+
exports.CircuitBreaker = exports.validateBatchQuality = exports.validateTranslationQuality = exports.translatorLoadBalancer = exports.warmupTranslator = exports.checkTranslatorHealth = exports.callTranslator = exports.requiredPluralCategories = exports.parseIcu = exports.isIcuMessage = exports.compareIcuMessages = exports.sameProtectedSpans = exports.restoreText = exports.protectText = exports.hasNothingToTranslate = exports.checkTokens = exports.validateRawTokens = exports.sanitizeTranslation = exports.restorePlaceholders = exports.protectPlaceholders = exports.hashString = exports.hasICUSyntax = exports.extractPlaceholders = exports.parseCode = exports.I18N_LIBRARY_NAMES = exports.detectI18n = exports.SUPPORTED_SUMMARY = exports.FRAMEWORK_DISPLAY_NAMES = exports.localRepoFiles = exports.detectStack = exports.saveProjectConfig = exports.saveCachedStrings = exports.getProjectInfo = exports.getBabelizeSummary = exports.detectProjectInfo = exports.addBabelToHistory = exports.formatSyncNotificationMessage = exports.detectSyncIssues = exports.matchFromMemory = exports.setLocaleValue = exports.applyTranslationOverrides = exports.setEngineLogger = exports.runWithEngineLogger = exports.validateWorkspace = exports.scanKeyReferences = exports.RunAbortedError = exports.validateEngineConfigV2 = exports.runEngineV2 = exports.loadBabelizeConfig = exports.BabelizeEngine = void 0;
|
|
25
|
+
exports.TRANSLATION_CACHE_TTL_SECONDS = exports.translationCacheKey = exports.hashText = exports.readProjectLocaleConfig = exports.localeIssueFile = exports.checkProjectLocales = exports.ReviewError = exports.reviewLocaleChanges = exports.loadLocale = exports.isLocaleCode = exports.isIgnoredLocalePath = exports.groupLocaleFiles = exports.findLocaleFileSets = exports.DEFAULT_LOCALE_IGNORE_DIRS = exports.glossaryFingerprint = exports.normalizeGlossary = exports.localeValue = exports.containsTerm = exports.canonicalizeLocaleKeys = exports.canonicalLocale = exports.tallyLocaleIssues = exports.flattenLocaleMessages = exports.checkLocalePair = exports.frameworkSupport = exports.analyzeWorkspace = exports.analyzeProject = exports.localeExtension = exports.getCircuitBreaker = exports.CircuitState = void 0;
|
|
26
26
|
var BabelizeEngine_1 = require("./BabelizeEngine");
|
|
27
27
|
Object.defineProperty(exports, "BabelizeEngine", { enumerable: true, get: function () { return BabelizeEngine_1.BabelizeEngine; } });
|
|
28
28
|
var config_1 = require("./config");
|
|
@@ -46,6 +46,9 @@ Object.defineProperty(exports, "runWithEngineLogger", { enumerable: true, get: f
|
|
|
46
46
|
Object.defineProperty(exports, "setEngineLogger", { enumerable: true, get: function () { return logger_1.setEngineLogger; } });
|
|
47
47
|
// ── Pipeline pieces the backend uses directly ────────────────
|
|
48
48
|
__exportStar(require("./modules/lockfile"), exports);
|
|
49
|
+
var overrides_1 = require("./modules/overrides");
|
|
50
|
+
Object.defineProperty(exports, "applyTranslationOverrides", { enumerable: true, get: function () { return overrides_1.applyTranslationOverrides; } });
|
|
51
|
+
Object.defineProperty(exports, "setLocaleValue", { enumerable: true, get: function () { return overrides_1.setLocaleValue; } });
|
|
49
52
|
__exportStar(require("./modules/translator/language-codes"), exports);
|
|
50
53
|
var memory_matcher_1 = require("./modules/translator/memory/memory-matcher");
|
|
51
54
|
Object.defineProperty(exports, "matchFromMemory", { enumerable: true, get: function () { return memory_matcher_1.matchFromMemory; } });
|
|
@@ -64,6 +67,10 @@ var stack_1 = require("./detection/stack");
|
|
|
64
67
|
Object.defineProperty(exports, "detectStack", { enumerable: true, get: function () { return stack_1.detectStack; } });
|
|
65
68
|
Object.defineProperty(exports, "localRepoFiles", { enumerable: true, get: function () { return stack_1.localRepoFiles; } });
|
|
66
69
|
Object.defineProperty(exports, "FRAMEWORK_DISPLAY_NAMES", { enumerable: true, get: function () { return stack_1.FRAMEWORK_DISPLAY_NAMES; } });
|
|
70
|
+
Object.defineProperty(exports, "SUPPORTED_SUMMARY", { enumerable: true, get: function () { return stack_1.SUPPORTED_SUMMARY; } });
|
|
71
|
+
var i18n_1 = require("./detection/i18n");
|
|
72
|
+
Object.defineProperty(exports, "detectI18n", { enumerable: true, get: function () { return i18n_1.detectI18n; } });
|
|
73
|
+
Object.defineProperty(exports, "I18N_LIBRARY_NAMES", { enumerable: true, get: function () { return i18n_1.I18N_LIBRARY_NAMES; } });
|
|
67
74
|
var ast_helpers_1 = require("./utils/ast-helpers");
|
|
68
75
|
Object.defineProperty(exports, "parseCode", { enumerable: true, get: function () { return ast_helpers_1.parseCode; } });
|
|
69
76
|
var string_utils_1 = require("./utils/string-utils");
|
|
@@ -7,5 +7,7 @@ interface HtmlInjectOptions {
|
|
|
7
7
|
targetLocales?: string[];
|
|
8
8
|
sourceLocale?: string;
|
|
9
9
|
}
|
|
10
|
+
/** `<script src>` for the root i18n.js as seen from this page: `i18n.js`, `../i18n.js`, ... */
|
|
11
|
+
export declare function runtimeScriptSrc(filePath: string): string;
|
|
10
12
|
export declare function injectHtml(options: HtmlInjectOptions): Promise<FileInjection>;
|
|
11
13
|
export {};
|
|
@@ -32,10 +32,36 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.runtimeScriptSrc = runtimeScriptSrc;
|
|
36
40
|
exports.injectHtml = injectHtml;
|
|
41
|
+
const path_1 = __importDefault(require("path"));
|
|
37
42
|
const string_utils_1 = require("../../utils/string-utils");
|
|
38
43
|
const html_extractor_1 = require("../../utils/html-extractor");
|
|
44
|
+
// <meta> tags whose content the extractor collects (html-extractor extractHeadStrings).
|
|
45
|
+
const SEO_META = ['description', 'og:title', 'og:description', 'og:site_name', 'twitter:title', 'twitter:description'];
|
|
46
|
+
/**
|
|
47
|
+
* Key for a string in this file found by its text alone. Head strings are extracted
|
|
48
|
+
* with ids like `file:meta:<hash>`, and a string already seen in the body is not
|
|
49
|
+
* extracted twice, so a line-based id can't find them.
|
|
50
|
+
*/
|
|
51
|
+
function keyByText(filePath, text, keyMapping) {
|
|
52
|
+
const prefix = `${filePath}:`;
|
|
53
|
+
const suffix = `:${(0, string_utils_1.hashString)(text)}`;
|
|
54
|
+
for (const [id, key] of keyMapping) {
|
|
55
|
+
if (id.startsWith(prefix) && id.endsWith(suffix))
|
|
56
|
+
return key;
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
/** `<script src>` for the root i18n.js as seen from this page: `i18n.js`, `../i18n.js`, ... */
|
|
61
|
+
function runtimeScriptSrc(filePath) {
|
|
62
|
+
const dir = path_1.default.posix.dirname(filePath.split(path_1.default.sep).join('/'));
|
|
63
|
+
return path_1.default.posix.relative(dir, 'i18n.js') || 'i18n.js';
|
|
64
|
+
}
|
|
39
65
|
function asElement(node) {
|
|
40
66
|
return node.nodeType === 1 ? node : null;
|
|
41
67
|
}
|
|
@@ -60,13 +86,26 @@ function walkHtmlNode(node, filePath, content, keyMapping, matchedIds, injection
|
|
|
60
86
|
}
|
|
61
87
|
}
|
|
62
88
|
const element = asElement(node);
|
|
89
|
+
if (element && tagName === 'meta') {
|
|
90
|
+
const name = element.getAttribute('name') || element.getAttribute('property');
|
|
91
|
+
const value = element.getAttribute('content');
|
|
92
|
+
if (name && SEO_META.includes(name) && value?.trim() && !element.getAttribute('data-i18n-content')) {
|
|
93
|
+
const key = keyByText(filePath, value.trim(), keyMapping);
|
|
94
|
+
if (key) {
|
|
95
|
+
element.setAttribute('data-i18n-content', key);
|
|
96
|
+
injections.push({ file: filePath, line: 0, column: 0, original: value, replacement: `data-i18n-content="${key}"`, key, confidence: 100 });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
63
101
|
if (textChild && !nonTextChild && element && !element.getAttribute('data-i18n')) {
|
|
64
102
|
const text = textChild.text.trim();
|
|
65
103
|
if (node.range) {
|
|
66
104
|
const line = (content.slice(0, node.range[0]).match(/\n/g) || []).length + 1;
|
|
67
105
|
const id = `${filePath}:${line}:${(0, string_utils_1.hashString)(text)}`;
|
|
68
|
-
|
|
69
|
-
|
|
106
|
+
// <title> may only exist under a head id (`file:title:<hash>`) or share a body string's key.
|
|
107
|
+
if (matchedIds.has(id) || tagName === 'title') {
|
|
108
|
+
const key = keyMapping.get(id) ?? (tagName === 'title' ? keyByText(filePath, text, keyMapping) : undefined);
|
|
70
109
|
if (key) {
|
|
71
110
|
element.setAttribute('data-i18n', key);
|
|
72
111
|
injections.push({
|
|
@@ -159,13 +198,23 @@ async function injectHtml(options) {
|
|
|
159
198
|
const matchedIds = new Set(keyMapping.keys());
|
|
160
199
|
walkHtmlNode(htmlEl, filePath, content, keyMapping, matchedIds, injections);
|
|
161
200
|
let modifiedContent = root.toString();
|
|
162
|
-
|
|
163
|
-
|
|
201
|
+
// i18n.js lives at the site root, so pages in subfolders need ../i18n.js. Older runs
|
|
202
|
+
// wrote a bare src="i18n.js" everywhere; repoint those instead of adding a second tag.
|
|
203
|
+
const src = runtimeScriptSrc(filePath);
|
|
204
|
+
const existingScript = /<script([^>]*)src\s*=\s*["']((?:\.\.\/)*i18n\.js)["']/.exec(modifiedContent);
|
|
205
|
+
if (existingScript) {
|
|
206
|
+
if (existingScript[2] !== src) {
|
|
207
|
+
modifiedContent = modifiedContent.replace(existingScript[0], `<script${existingScript[1]}src="${src}"`);
|
|
208
|
+
importAdded = true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
const tag = ` <script src="${src}"></script>\n`;
|
|
164
213
|
if (modifiedContent.includes('</body>')) {
|
|
165
|
-
modifiedContent = modifiedContent.replace('</body>',
|
|
214
|
+
modifiedContent = modifiedContent.replace('</body>', `${tag}</body>`);
|
|
166
215
|
}
|
|
167
216
|
else {
|
|
168
|
-
modifiedContent = modifiedContent.replace('</head>',
|
|
217
|
+
modifiedContent = modifiedContent.replace('</head>', `${tag}</head>`);
|
|
169
218
|
}
|
|
170
219
|
importAdded = true;
|
|
171
220
|
}
|
|
@@ -2,7 +2,8 @@ import type { KeyResult, TranslationBatchRequest, TranslationProvider } from '..
|
|
|
2
2
|
/**
|
|
3
3
|
* Babelize-hosted translation for `babelize translate`: a TranslationProvider
|
|
4
4
|
* that sends the engine's already placeholder-protected strings (never code)
|
|
5
|
-
* to POST {apiUrl}/v1/translate/batch with a Babelize API key
|
|
5
|
+
* to POST {apiUrl}/v1/translate/batch with a Babelize API key or a signed-in
|
|
6
|
+
* CLI session (the endpoint accepts both and bills the same account). The engine
|
|
6
7
|
* still validates every result and runs its repair retry locally; model,
|
|
7
8
|
* quota and credits are decided by the server.
|
|
8
9
|
*/
|
|
@@ -36,8 +37,19 @@ export declare class HostedTranslationError extends Error {
|
|
|
36
37
|
get fatal(): boolean;
|
|
37
38
|
}
|
|
38
39
|
export interface HostedProviderOptions {
|
|
39
|
-
/** Babelize API key (bblz_sk_…). Never printed. */
|
|
40
|
+
/** Babelize API key (bblz_sk_…) or a session access token. Never printed. */
|
|
40
41
|
apiKey: string;
|
|
42
|
+
/**
|
|
43
|
+
* What `apiKey` is, for the message when it's rejected: an API key from
|
|
44
|
+
* BABELIZE_API_KEY (default) or the CLI's signed-in session.
|
|
45
|
+
*/
|
|
46
|
+
credential?: 'api_key' | 'session';
|
|
47
|
+
/**
|
|
48
|
+
* Called once when a request is rejected with 401: return a fresh token
|
|
49
|
+
* (a session refresh) to retry with it, or null to give up. Sessions expire
|
|
50
|
+
* during long runs; API keys don't, so API-key callers leave this out.
|
|
51
|
+
*/
|
|
52
|
+
refreshToken?: () => Promise<string | null>;
|
|
41
53
|
/** e.g. https://api.babelize.co/api */
|
|
42
54
|
apiUrl: string;
|
|
43
55
|
fetch?: typeof fetch;
|
|
@@ -57,10 +69,26 @@ export declare function parseRetryAfterMs(value: string | null): number | undefi
|
|
|
57
69
|
export declare class HostedTranslationProvider implements TranslationProvider {
|
|
58
70
|
/** Model the server reported (X-Babelize-Model), once a request succeeded. */
|
|
59
71
|
model?: string;
|
|
72
|
+
/**
|
|
73
|
+
* True when the server translates with NLLB rather than an LLM: the engine
|
|
74
|
+
* then splits ICU messages and multi-line text and protects glossary terms
|
|
75
|
+
* before sending, as it does for cloud runs. Set by detectCapabilities().
|
|
76
|
+
*/
|
|
77
|
+
plainText: boolean;
|
|
60
78
|
/** Set once a request fails in a way no retry can fix (bad key, no credits). */
|
|
61
79
|
fatalError?: HostedTranslationError;
|
|
62
80
|
private readonly opts;
|
|
81
|
+
/** The token in use; replaced when a session is refreshed. */
|
|
82
|
+
private token;
|
|
83
|
+
/** In-flight refresh, shared by concurrent batches that all got a 401. */
|
|
84
|
+
private refreshing?;
|
|
63
85
|
constructor(options: HostedProviderOptions);
|
|
86
|
+
/**
|
|
87
|
+
* Ask the server what serves translation (GET /v1/translate/capabilities)
|
|
88
|
+
* before a run, so requests are shaped for it. A server without the endpoint
|
|
89
|
+
* (older) or any failure keeps the LLM shape; the run itself reports errors.
|
|
90
|
+
*/
|
|
91
|
+
detectCapabilities(): Promise<void>;
|
|
64
92
|
translateBatch(request: TranslationBatchRequest): Promise<KeyResult[]>;
|
|
65
93
|
private post;
|
|
66
94
|
private backoff;
|
|
@@ -8,7 +8,8 @@ const types_1 = require("@babelize/types");
|
|
|
8
8
|
/**
|
|
9
9
|
* Babelize-hosted translation for `babelize translate`: a TranslationProvider
|
|
10
10
|
* that sends the engine's already placeholder-protected strings (never code)
|
|
11
|
-
* to POST {apiUrl}/v1/translate/batch with a Babelize API key
|
|
11
|
+
* to POST {apiUrl}/v1/translate/batch with a Babelize API key or a signed-in
|
|
12
|
+
* CLI session (the endpoint accepts both and bills the same account). The engine
|
|
12
13
|
* still validates every result and runs its repair retry locally; model,
|
|
13
14
|
* quota and credits are decided by the server.
|
|
14
15
|
*/
|
|
@@ -48,6 +49,13 @@ function parseRetryAfterMs(value) {
|
|
|
48
49
|
}
|
|
49
50
|
class HostedTranslationProvider {
|
|
50
51
|
constructor(options) {
|
|
52
|
+
/**
|
|
53
|
+
* True when the server translates with NLLB rather than an LLM: the engine
|
|
54
|
+
* then splits ICU messages and multi-line text and protects glossary terms
|
|
55
|
+
* before sending, as it does for cloud runs. Set by detectCapabilities().
|
|
56
|
+
*/
|
|
57
|
+
this.plainText = false;
|
|
58
|
+
this.token = options.apiKey;
|
|
51
59
|
this.opts = {
|
|
52
60
|
fetch: globalThis.fetch.bind(globalThis),
|
|
53
61
|
maxRetries: 3,
|
|
@@ -59,6 +67,31 @@ class HostedTranslationProvider {
|
|
|
59
67
|
apiUrl: options.apiUrl.replace(/\/+$/, ''),
|
|
60
68
|
};
|
|
61
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Ask the server what serves translation (GET /v1/translate/capabilities)
|
|
72
|
+
* before a run, so requests are shaped for it. A server without the endpoint
|
|
73
|
+
* (older) or any failure keeps the LLM shape; the run itself reports errors.
|
|
74
|
+
*/
|
|
75
|
+
async detectCapabilities() {
|
|
76
|
+
try {
|
|
77
|
+
const res = await this.opts.fetch(`${this.opts.apiUrl}/v1/translate/capabilities`, {
|
|
78
|
+
headers: {
|
|
79
|
+
Authorization: `Bearer ${this.token}`,
|
|
80
|
+
...(this.opts.userAgent ? { 'User-Agent': this.opts.userAgent } : {}),
|
|
81
|
+
},
|
|
82
|
+
signal: AbortSignal.timeout(15000),
|
|
83
|
+
});
|
|
84
|
+
if (!res.ok)
|
|
85
|
+
return;
|
|
86
|
+
const body = (await res.json());
|
|
87
|
+
this.plainText = body.plainText === true;
|
|
88
|
+
if (body.model)
|
|
89
|
+
this.model ?? (this.model = body.model);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// keep the defaults
|
|
93
|
+
}
|
|
94
|
+
}
|
|
62
95
|
async translateBatch(request) {
|
|
63
96
|
// Strings over the server's limits never leave the machine: they fail
|
|
64
97
|
// locally, and the rest of the batch is still sent.
|
|
@@ -122,6 +155,7 @@ class HostedTranslationProvider {
|
|
|
122
155
|
// replays it instead of translating and charging again.
|
|
123
156
|
const idempotencyKey = (0, node_crypto_1.randomUUID)();
|
|
124
157
|
let attempt = 0;
|
|
158
|
+
let refreshed = false;
|
|
125
159
|
for (;;) {
|
|
126
160
|
let res;
|
|
127
161
|
try {
|
|
@@ -129,7 +163,7 @@ class HostedTranslationProvider {
|
|
|
129
163
|
method: 'POST',
|
|
130
164
|
headers: {
|
|
131
165
|
'Content-Type': 'application/json',
|
|
132
|
-
Authorization: `Bearer ${this.
|
|
166
|
+
Authorization: `Bearer ${this.token}`,
|
|
133
167
|
'Idempotency-Key': idempotencyKey,
|
|
134
168
|
...(this.opts.userAgent ? { 'User-Agent': this.opts.userAgent } : {}),
|
|
135
169
|
},
|
|
@@ -162,6 +196,17 @@ class HostedTranslationProvider {
|
|
|
162
196
|
this.model = res.headers.get(exports.HOSTED_MODEL_HEADER) ?? this.model;
|
|
163
197
|
return Array.isArray(data.results) ? data.results : [];
|
|
164
198
|
}
|
|
199
|
+
// An expired session: refresh it once and try again with the same key.
|
|
200
|
+
if (res.status === 401 && this.opts.refreshToken && !refreshed) {
|
|
201
|
+
refreshed = true;
|
|
202
|
+
const sent = this.token;
|
|
203
|
+
this.refreshing ?? (this.refreshing = this.opts.refreshToken().finally(() => { this.refreshing = undefined; }));
|
|
204
|
+
const fresh = this.token !== sent ? this.token : await this.refreshing;
|
|
205
|
+
if (fresh) {
|
|
206
|
+
this.token = fresh;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
165
210
|
if (RETRYABLE_STATUS.has(res.status) && attempt < this.opts.maxRetries && !(await isHostedOff(res))) {
|
|
166
211
|
const retryAfter = parseRetryAfterMs(res.headers.get('retry-after'));
|
|
167
212
|
await this.opts.sleep(Math.min(retryAfter ?? this.backoff(attempt), this.opts.maxRetryAfterMs));
|
|
@@ -183,7 +228,9 @@ class HostedTranslationProvider {
|
|
|
183
228
|
switch (res.status) {
|
|
184
229
|
case 401:
|
|
185
230
|
case 403:
|
|
186
|
-
return new HostedTranslationError(
|
|
231
|
+
return new HostedTranslationError(this.opts.credential === 'session'
|
|
232
|
+
? `Your Babelize sign-in was rejected (${res.status}). Sign in again with \`babelize auth login\`, then re-run.`
|
|
233
|
+
: `BABELIZE_API_KEY was rejected (${res.status}). Create a new key at ${exports.API_KEYS_URL} and set BABELIZE_API_KEY.`, 'hosted_auth', res.status);
|
|
187
234
|
case 402:
|
|
188
235
|
return new HostedTranslationError(payload?.message ??
|
|
189
236
|
`Your Babelize account is out of AI credits. Top up or upgrade at ${exports.BILLING_URL}, or use your own LLM key (e.g. set ANTHROPIC_API_KEY).`, 'hosted_credits', 402);
|
|
@@ -10,8 +10,8 @@ import { type FileDiffStat, type ApplyIo, type WorkspaceChanges } from './worksp
|
|
|
10
10
|
/** Lockfile scope for local runs: `.babelize/local/babelize-lock.json` in the project root. */
|
|
11
11
|
export declare const LOCAL_PROJECT_ID = "local";
|
|
12
12
|
export declare const LOCAL_LOCKFILE = ".babelize/local/babelize-lock.json";
|
|
13
|
-
export declare const SUPPORTED_LOCAL_FRAMEWORKS: readonly ["react", "nextjs", "django"];
|
|
14
|
-
export declare const BETA_LOCAL_FRAMEWORKS: readonly ["vue", "
|
|
13
|
+
export declare const SUPPORTED_LOCAL_FRAMEWORKS: readonly ["react", "nextjs", "django", "static"];
|
|
14
|
+
export declare const BETA_LOCAL_FRAMEWORKS: readonly ["vue", "react-native"];
|
|
15
15
|
export interface LocalTranslateOptions {
|
|
16
16
|
projectDir: string;
|
|
17
17
|
sourceLocale: string;
|
|
@@ -72,8 +72,8 @@ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
|
72
72
|
/** Lockfile scope for local runs: `.babelize/local/babelize-lock.json` in the project root. */
|
|
73
73
|
exports.LOCAL_PROJECT_ID = 'local';
|
|
74
74
|
exports.LOCAL_LOCKFILE = `.babelize/${exports.LOCAL_PROJECT_ID}/babelize-lock.json`;
|
|
75
|
-
exports.SUPPORTED_LOCAL_FRAMEWORKS = ['react', 'nextjs', 'django'];
|
|
76
|
-
exports.BETA_LOCAL_FRAMEWORKS = ['vue', '
|
|
75
|
+
exports.SUPPORTED_LOCAL_FRAMEWORKS = ['react', 'nextjs', 'django', 'static'];
|
|
76
|
+
exports.BETA_LOCAL_FRAMEWORKS = ['vue', 'react-native'];
|
|
77
77
|
class LocalTranslateError extends Error {
|
|
78
78
|
constructor(message, code,
|
|
79
79
|
/** Files left changed in the project (only for a failed, un-rolled-back apply). */
|
|
@@ -283,6 +283,10 @@ async function runScoped(opts, deps, engineLog, state) {
|
|
|
283
283
|
if (!opts.byok && !translator) {
|
|
284
284
|
throw new LocalTranslateError('No translation provider configured.', 'no_translator');
|
|
285
285
|
}
|
|
286
|
+
// Shape requests for what the server translates with (NLLB needs ICU and
|
|
287
|
+
// multi-line text split and glossary terms protected; an LLM doesn't).
|
|
288
|
+
if (deps.hosted)
|
|
289
|
+
await deps.hosted.detectCapabilities();
|
|
286
290
|
const files = await listProjectFiles(projectDir);
|
|
287
291
|
const temp = await (0, workspace_copy_1.createTempCopy)(projectDir, { files, tmpRoot: deps.tmpRoot });
|
|
288
292
|
try {
|
|
@@ -334,10 +334,12 @@ async function finalizeProject(workspace, dryRun, options = {}) {
|
|
|
334
334
|
}
|
|
335
335
|
else {
|
|
336
336
|
// 'missing' or 'ours' — write/overwrite
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
337
|
+
// sourceDirs[0] is the scan root ("."), not a locale. Targets: this run's plus any
|
|
338
|
+
// locale files already on disk, so the switcher lists every language.
|
|
339
|
+
const sourceLocale = options.sourceLocale || 'en';
|
|
340
|
+
const targetLocales = [...new Set([...(options.targetLocales ?? []), ...(workspace.existingLocales ?? [])])]
|
|
341
|
+
.filter((l) => l && l !== sourceLocale && /^[A-Za-z]{2,3}([-_][A-Za-z0-9]+)*$/.test(l));
|
|
342
|
+
const runtimeContent = (0, static_runtime_1.buildStaticRuntime)({ sourceLocale, targetLocales });
|
|
341
343
|
await writer.write(i18nJsPath, runtimeContent);
|
|
342
344
|
configFileCreated = true;
|
|
343
345
|
if (regenerationStatus === 'ours') {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export declare const STATIC_RUNTIME_VERSION = "1.
|
|
2
|
-
export declare const RUNTIME_HEADER = "/* babelize:static-runtime v1.
|
|
1
|
+
export declare const STATIC_RUNTIME_VERSION = "1.1.0";
|
|
2
|
+
export declare const RUNTIME_HEADER = "/* babelize:static-runtime v1.1.0 */";
|
|
3
3
|
export declare function buildStaticRuntime(config: {
|
|
4
4
|
sourceLocale: string;
|
|
5
5
|
targetLocales: string[];
|
|
@@ -4,8 +4,9 @@ exports.RUNTIME_HEADER = exports.STATIC_RUNTIME_VERSION = void 0;
|
|
|
4
4
|
exports.buildStaticRuntime = buildStaticRuntime;
|
|
5
5
|
exports.buildSwitcherPlaceholder = buildSwitcherPlaceholder;
|
|
6
6
|
exports.checkRuntimeNeedsRegeneration = checkRuntimeNeedsRegeneration;
|
|
7
|
-
exports.STATIC_RUNTIME_VERSION = '1.
|
|
8
|
-
|
|
7
|
+
exports.STATIC_RUNTIME_VERSION = '1.1.0';
|
|
8
|
+
const RUNTIME_MARKER = '/* babelize:static-runtime v';
|
|
9
|
+
exports.RUNTIME_HEADER = `${RUNTIME_MARKER}${exports.STATIC_RUNTIME_VERSION} */`;
|
|
9
10
|
function buildStaticRuntime(config) {
|
|
10
11
|
const locales = JSON.stringify([config.sourceLocale, ...config.targetLocales]);
|
|
11
12
|
const baseDir = config.baseDir ? JSON.stringify(config.baseDir) : 'null';
|
|
@@ -113,8 +114,9 @@ function buildStaticRuntime(config) {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
function applyTranslations() {
|
|
117
|
+
// The whole document, so <title> and SEO <meta> tags in <head> are translated too.
|
|
116
118
|
var walk = document.createTreeWalker(
|
|
117
|
-
document.
|
|
119
|
+
document.documentElement,
|
|
118
120
|
NodeFilter.SHOW_ELEMENT,
|
|
119
121
|
null,
|
|
120
122
|
false
|
|
@@ -150,7 +152,7 @@ function buildStaticRuntime(config) {
|
|
|
150
152
|
}
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
|
-
var attrNames = ['alt', 'title', 'placeholder', 'aria-label', 'aria-description'];
|
|
155
|
+
var attrNames = ['alt', 'title', 'placeholder', 'aria-label', 'aria-description', 'content'];
|
|
154
156
|
for (var a = 0; a < attrNames.length; a++) {
|
|
155
157
|
var attrKey = node.getAttribute('data-i18n-' + attrNames[a]);
|
|
156
158
|
if (attrKey) {
|
|
@@ -162,6 +164,8 @@ function buildStaticRuntime(config) {
|
|
|
162
164
|
}
|
|
163
165
|
|
|
164
166
|
function updateHtmlAttributes() {
|
|
167
|
+
var select = document.querySelector('[data-babelize-switcher-select]');
|
|
168
|
+
if (select) select.value = lang;
|
|
165
169
|
var meta = (loadedLocales[lang] || {})._meta;
|
|
166
170
|
if (!meta) return;
|
|
167
171
|
var html = document.documentElement;
|
|
@@ -254,7 +258,8 @@ function checkRuntimeNeedsRegeneration(existingContent) {
|
|
|
254
258
|
if (!existingContent || existingContent.trim().length === 0) {
|
|
255
259
|
return 'missing';
|
|
256
260
|
}
|
|
257
|
-
|
|
261
|
+
// Any version we generated, so older runtimes get upgraded rather than treated as hand-written.
|
|
262
|
+
if (existingContent.includes(RUNTIME_MARKER)) {
|
|
258
263
|
return 'ours';
|
|
259
264
|
}
|
|
260
265
|
return 'user';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { hashSourceString } from './hasher';
|
|
2
2
|
export { type LockfileData, type LockfileEntry, type LockfilePartition, type LockfileStats, type PartitionedKey, createEmptyLockfile, } from './schema';
|
|
3
|
-
export { buildKeysToTranslateByTargetLocale, clearLockfile, removeLocaleFromLockfile, collectCachedTranslations, computeLockfileStats, getLockfilePath, loadLockfile, partitionKeys, saveLockfile, updateLockfile, lockfileChanged, dropFailedTranslations, isStalePseudoEntry, glossaryChanged, type LockfileReadOptions, } from './lockfile-manager';
|
|
3
|
+
export { buildKeysToTranslateByTargetLocale, clearLockfile, removeLocaleFromLockfile, removeLockfileEntries, collectCachedTranslations, computeLockfileStats, getLockfilePath, loadLockfile, partitionKeys, saveLockfile, updateLockfile, lockfileChanged, dropFailedTranslations, isStalePseudoEntry, glossaryChanged, type LockfileReadOptions, } from './lockfile-manager';
|