@ai-translate/cli 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 +21 -0
- package/README.md +199 -0
- package/dist/bin.d.mts +1 -0
- package/dist/bin.mjs +9 -0
- package/dist/bin.mjs.map +1 -0
- package/dist/index.d.mts +15 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/src-Cnnq284f.mjs +947 -0
- package/dist/src-Cnnq284f.mjs.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
import { auditCatalogs, defineConfig, defineConfig as defineConfig$1, resolveStateScope, syncCatalogs, usesGeneratorSelfCheck, validateCatalogs, withTranslationIssueCache } from "@ai-translate/core";
|
|
2
|
+
import { adoptExistingTranslations } from "@ai-translate/fs-json";
|
|
3
|
+
import { existsSync, promises } from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { config, parse } from "dotenv";
|
|
6
|
+
import { createJiti } from "jiti";
|
|
7
|
+
import { detectProject, renderConfig } from "@ai-translate/next";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import { supportsScopedSave } from "@ai-translate/core/types";
|
|
11
|
+
//#region src/config.ts
|
|
12
|
+
const CONFIG_CANDIDATES = [
|
|
13
|
+
"ai-translate.config.ts",
|
|
14
|
+
"ai-translate.config.mts",
|
|
15
|
+
"ai-translate.config.js",
|
|
16
|
+
"ai-translate.config.mjs"
|
|
17
|
+
];
|
|
18
|
+
function isAiTranslateConfig(value) {
|
|
19
|
+
return typeof value === "object" && value !== null && "catalogs" in value && "provider" in value && "sourceLocale" in value && "state" in value && "targetLocales" in value;
|
|
20
|
+
}
|
|
21
|
+
function findConfigPath(cwd, explicitPath) {
|
|
22
|
+
if (explicitPath) return path.resolve(cwd, explicitPath);
|
|
23
|
+
for (const candidate of CONFIG_CANDIDATES) {
|
|
24
|
+
const fullPath = path.join(cwd, candidate);
|
|
25
|
+
if (existsSync(fullPath)) return fullPath;
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Unable to find ai-translate config in ${cwd}. Expected one of: ${CONFIG_CANDIDATES.join(", ")}`);
|
|
28
|
+
}
|
|
29
|
+
async function loadEnvFiles(cwd) {
|
|
30
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
31
|
+
const candidates = [
|
|
32
|
+
".env",
|
|
33
|
+
".env.local",
|
|
34
|
+
nodeEnv ? `.env.${nodeEnv}` : void 0,
|
|
35
|
+
nodeEnv ? `.env.${nodeEnv}.local` : void 0
|
|
36
|
+
].filter((value) => value !== void 0);
|
|
37
|
+
const merged = {};
|
|
38
|
+
for (const fileName of candidates) {
|
|
39
|
+
const filePath = path.join(cwd, fileName);
|
|
40
|
+
try {
|
|
41
|
+
const raw = await promises.readFile(filePath, "utf8");
|
|
42
|
+
Object.assign(merged, parse(raw));
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error.code !== "ENOENT") throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
config({
|
|
48
|
+
override: false,
|
|
49
|
+
path: candidates.map((candidate) => path.join(cwd, candidate)),
|
|
50
|
+
processEnv: process.env
|
|
51
|
+
});
|
|
52
|
+
return merged;
|
|
53
|
+
}
|
|
54
|
+
async function loadConfig(cwd, explicitPath) {
|
|
55
|
+
const configPath = findConfigPath(cwd, explicitPath);
|
|
56
|
+
const loaded = await createJiti(import.meta.url, {
|
|
57
|
+
interopDefault: true,
|
|
58
|
+
moduleCache: false
|
|
59
|
+
}).import(configPath);
|
|
60
|
+
const resolved = typeof loaded === "object" && loaded !== null && "default" in loaded ? loaded.default ?? loaded : loaded;
|
|
61
|
+
if (!isAiTranslateConfig(resolved)) throw new Error(`Config file ${configPath} did not export an ai-translate config object.`);
|
|
62
|
+
return {
|
|
63
|
+
config: defineConfig(resolved),
|
|
64
|
+
configPath
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/init.ts
|
|
69
|
+
const CONFIG_FILENAME = "ai-translate.config.ts";
|
|
70
|
+
const DEFAULT_AI_SDK_PACKAGE = "@ai-sdk/openai";
|
|
71
|
+
/** Packages a generated config imports from, whichever provider it wires up. */
|
|
72
|
+
const REQUIRED_PACKAGES = ["@ai-translate/cli", "@ai-translate/fs-json"];
|
|
73
|
+
function describe(setup) {
|
|
74
|
+
return [
|
|
75
|
+
`Detected ${setup.displayName}:`,
|
|
76
|
+
...setup.evidence.map((item) => ` - ${item.detail} (${item.source})`),
|
|
77
|
+
` - Source locale ${setup.plan.sourceLocale}, ${String(setup.plan.targetLocales.length)} target locale(s): ${setup.plan.targetLocales.join(", ")}`
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
async function missingPackages(cwd, plan, options) {
|
|
81
|
+
const expected = [
|
|
82
|
+
...REQUIRED_PACKAGES,
|
|
83
|
+
...plan.messageFormat === "plain" ? [] : ["@ai-translate/message-formats"],
|
|
84
|
+
...options.provider === "ai-sdk" ? [
|
|
85
|
+
"@ai-translate/provider-ai-sdk",
|
|
86
|
+
"ai",
|
|
87
|
+
options.providerPackage ?? DEFAULT_AI_SDK_PACKAGE
|
|
88
|
+
] : ["@ai-translate/provider-openai"]
|
|
89
|
+
];
|
|
90
|
+
try {
|
|
91
|
+
const raw = await promises.readFile(path.join(cwd, "package.json"), "utf8");
|
|
92
|
+
const manifest = JSON.parse(raw);
|
|
93
|
+
const declared = /* @__PURE__ */ new Set([...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.devDependencies ?? {})]);
|
|
94
|
+
return expected.filter((name) => !declared.has(name));
|
|
95
|
+
} catch {
|
|
96
|
+
return expected;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function chooseSetup(setups, requested) {
|
|
100
|
+
if (requested !== void 0) {
|
|
101
|
+
const match = setups.find((setup) => setup.integrationId === requested);
|
|
102
|
+
if (match === void 0) throw new Error(`No ${requested} setup was detected. Detected: ${setups.map((setup) => setup.integrationId).join(", ") || "none"}.`);
|
|
103
|
+
return match;
|
|
104
|
+
}
|
|
105
|
+
const [best, ...rest] = setups;
|
|
106
|
+
if (best === void 0) throw new Error("No supported Next.js localization setup was found. ai-translate init currently recognises next-intl and i18next. Write ai-translate.config.ts by hand, or run init from the directory holding package.json and your locale files.");
|
|
107
|
+
if (rest.length > 0 && rest[0]?.confidence === best.confidence) throw new Error(`Found more than one localization setup (${setups.map((setup) => setup.integrationId).join(", ")}). Re-run with --integration <id> to choose.`);
|
|
108
|
+
return best;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Detects the project's localization setup and writes a config for it.
|
|
112
|
+
*
|
|
113
|
+
* Nothing else is touched. Installing packages, wiring scripts, and editing the
|
|
114
|
+
* Next.js config stay in the user's hands, so `init` on an unfamiliar repository
|
|
115
|
+
* produces exactly one new file and a list of instructions.
|
|
116
|
+
*/
|
|
117
|
+
async function runInit(cwd, options = {}) {
|
|
118
|
+
const setups = await detectProject(cwd, options.integrations === void 0 ? {} : { integrations: options.integrations });
|
|
119
|
+
const setup = chooseSetup(setups, options.integration);
|
|
120
|
+
const contents = renderConfig(setup.plan, {
|
|
121
|
+
...options.model === void 0 ? {} : { model: options.model },
|
|
122
|
+
...options.provider === void 0 ? {} : { provider: options.provider },
|
|
123
|
+
...options.providerPackage === void 0 ? {} : { providerPackage: options.providerPackage }
|
|
124
|
+
});
|
|
125
|
+
const configPath = path.join(cwd, CONFIG_FILENAME);
|
|
126
|
+
const lines = describe(setup);
|
|
127
|
+
for (const warning of setup.plan.warnings) lines.push(` ! ${warning}`);
|
|
128
|
+
const others = setups.filter((candidate) => candidate !== setup);
|
|
129
|
+
if (others.length > 0) lines.push(`Also detected, not used: ${others.map((candidate) => candidate.displayName).join(", ")}.`);
|
|
130
|
+
if (options.preview === true) {
|
|
131
|
+
lines.push("", `Would write ${CONFIG_FILENAME}:`, "", contents);
|
|
132
|
+
return {
|
|
133
|
+
configPath: null,
|
|
134
|
+
lines,
|
|
135
|
+
setup
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (await promises.access(configPath).then(() => true).catch(() => false) && options.force !== true) throw new Error(`${CONFIG_FILENAME} already exists. Pass --force to overwrite it.`);
|
|
139
|
+
await promises.writeFile(configPath, contents, "utf8");
|
|
140
|
+
lines.push("", `Wrote ${CONFIG_FILENAME}.`, "", "Next steps:");
|
|
141
|
+
const install = await missingPackages(cwd, setup.plan, options);
|
|
142
|
+
let step = 1;
|
|
143
|
+
if (install.length > 0) lines.push(` ${String(step++)}. Install: ${install.join(" ")}`);
|
|
144
|
+
const apiKeyVariable = options.provider === "ai-sdk" ? `the API key your ${options.providerPackage ?? DEFAULT_AI_SDK_PACKAGE} provider reads` : "OPENAI_API_KEY";
|
|
145
|
+
lines.push(` ${String(step++)}. Set ${apiKeyVariable}, in your shell or in .env.local.`, ` ${String(step++)}. Review the model and locale list in ${CONFIG_FILENAME}.`, ` ${String(step++)}. Run "ai-translate validate" to confirm the config loads.`, ` ${String(step++)}. Run "ai-translate check" to see what a sync would do.`, ` ${String(step)}. Run "ai-translate sync" to translate.`);
|
|
146
|
+
return {
|
|
147
|
+
configPath,
|
|
148
|
+
lines,
|
|
149
|
+
setup
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/transaction.ts
|
|
154
|
+
const DURABLE_TRANSACTION_STATE_STORE = Symbol.for("@ai-translate/fs-json/durable-transaction-state-store");
|
|
155
|
+
function cloneState(state) {
|
|
156
|
+
return structuredClone(state);
|
|
157
|
+
}
|
|
158
|
+
function cloneEntry(entry) {
|
|
159
|
+
return {
|
|
160
|
+
...entry,
|
|
161
|
+
address: entry.address.map((segment) => ({ ...segment })),
|
|
162
|
+
...entry.meta === void 0 ? {} : { meta: { ...entry.meta } },
|
|
163
|
+
...entry.tokens === void 0 ? {} : { tokens: entry.tokens.map((token) => ({ ...token })) }
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Adapter state may contain parser-owned class instances, so retain it while
|
|
167
|
+
* isolating the mutable translation entries. Staged writes are subsequently
|
|
168
|
+
* serialized and reloaded by the real adapter to obtain isolated live state. */
|
|
169
|
+
function cloneDocument(document) {
|
|
170
|
+
return {
|
|
171
|
+
entries: document.entries.map(cloneEntry),
|
|
172
|
+
...document.reconciliation === void 0 ? {} : { reconciliation: {
|
|
173
|
+
...document.reconciliation.previousPointers === void 0 ? {} : { previousPointers: { ...document.reconciliation.previousPointers } },
|
|
174
|
+
...document.reconciliation.retiredStateKeys === void 0 ? {} : { retiredStateKeys: [...document.reconciliation.retiredStateKeys] }
|
|
175
|
+
} },
|
|
176
|
+
ref: { ...document.ref },
|
|
177
|
+
state: document.state,
|
|
178
|
+
...document.structureDigest === void 0 ? {} : { structureDigest: document.structureDigest }
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function restoreDocumentRef(document, ref) {
|
|
182
|
+
return cloneDocument({
|
|
183
|
+
...document,
|
|
184
|
+
ref: { ...ref }
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function documentKey(ref) {
|
|
188
|
+
return [
|
|
189
|
+
ref.catalogId,
|
|
190
|
+
ref.format,
|
|
191
|
+
ref.locale,
|
|
192
|
+
ref.path,
|
|
193
|
+
ref.unitId
|
|
194
|
+
].join("\0");
|
|
195
|
+
}
|
|
196
|
+
var StagedStateStore = class {
|
|
197
|
+
dirty = false;
|
|
198
|
+
snapshot;
|
|
199
|
+
constructor(initial) {
|
|
200
|
+
this.snapshot = cloneState(initial);
|
|
201
|
+
}
|
|
202
|
+
hasChanges() {
|
|
203
|
+
return this.dirty;
|
|
204
|
+
}
|
|
205
|
+
load() {
|
|
206
|
+
return Promise.resolve(cloneState(this.snapshot));
|
|
207
|
+
}
|
|
208
|
+
save(state) {
|
|
209
|
+
this.snapshot = cloneState(state);
|
|
210
|
+
this.dirty = true;
|
|
211
|
+
return Promise.resolve();
|
|
212
|
+
}
|
|
213
|
+
stagedSnapshot() {
|
|
214
|
+
return cloneState(this.snapshot);
|
|
215
|
+
}
|
|
216
|
+
withLock(operation) {
|
|
217
|
+
return operation();
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
async function readOriginal(filePath) {
|
|
221
|
+
try {
|
|
222
|
+
const [original, stats] = await Promise.all([promises.readFile(filePath), promises.stat(filePath)]);
|
|
223
|
+
return {
|
|
224
|
+
mode: stats.mode,
|
|
225
|
+
original
|
|
226
|
+
};
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error.code === "ENOENT") return {
|
|
229
|
+
mode: void 0,
|
|
230
|
+
original: null
|
|
231
|
+
};
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function writeFileAtomic(filePath, contents, mode) {
|
|
236
|
+
await promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
237
|
+
const temporaryPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.ai-translate-${randomUUID()}`);
|
|
238
|
+
try {
|
|
239
|
+
await promises.writeFile(temporaryPath, contents);
|
|
240
|
+
if (mode !== void 0) await promises.chmod(temporaryPath, mode);
|
|
241
|
+
await promises.rename(temporaryPath, filePath);
|
|
242
|
+
} finally {
|
|
243
|
+
await promises.rm(temporaryPath, { force: true });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
var StagedCatalogs = class {
|
|
247
|
+
catalogs;
|
|
248
|
+
sourceLocale;
|
|
249
|
+
files = /* @__PURE__ */ new Map();
|
|
250
|
+
pendingRefs = /* @__PURE__ */ new Map();
|
|
251
|
+
tempRoot;
|
|
252
|
+
constructor(catalogs, sourceLocale) {
|
|
253
|
+
this.catalogs = catalogs;
|
|
254
|
+
this.sourceLocale = sourceLocale;
|
|
255
|
+
}
|
|
256
|
+
adapters() {
|
|
257
|
+
return this.catalogs.map((catalog) => this.createAdapter(catalog));
|
|
258
|
+
}
|
|
259
|
+
async cleanup() {
|
|
260
|
+
if (this.tempRoot !== void 0) await promises.rm(this.tempRoot, {
|
|
261
|
+
force: true,
|
|
262
|
+
recursive: true
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
async promote() {
|
|
266
|
+
for (const staged of this.files.values()) await writeFileAtomic(staged.realPath, await promises.readFile(staged.tempPath), staged.mode);
|
|
267
|
+
}
|
|
268
|
+
async durableChanges() {
|
|
269
|
+
return Promise.all([...this.files.values()].map(async (staged) => ({
|
|
270
|
+
...staged.mode === void 0 ? {} : { mode: staged.mode },
|
|
271
|
+
next: await promises.readFile(staged.tempPath),
|
|
272
|
+
original: staged.original,
|
|
273
|
+
path: staged.realPath
|
|
274
|
+
})));
|
|
275
|
+
}
|
|
276
|
+
async rollback() {
|
|
277
|
+
const failures = [];
|
|
278
|
+
for (const staged of this.files.values()) try {
|
|
279
|
+
if (staged.original === null) await promises.rm(staged.realPath, { force: true });
|
|
280
|
+
else await writeFileAtomic(staged.realPath, staged.original, staged.mode);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
failures.push(error);
|
|
283
|
+
}
|
|
284
|
+
if (failures.length > 0) throw new AggregateError(failures, "Failed to restore localized documents after commit.");
|
|
285
|
+
}
|
|
286
|
+
async stageFile(realPath) {
|
|
287
|
+
const existing = this.files.get(realPath);
|
|
288
|
+
if (existing) return existing;
|
|
289
|
+
this.tempRoot ??= await promises.mkdtemp(path.join(os.tmpdir(), "ai-translate-stage-"));
|
|
290
|
+
const { mode, original } = await readOriginal(realPath);
|
|
291
|
+
const tempPath = path.join(this.tempRoot, `${String(this.files.size)}${path.extname(realPath) || ".document"}`);
|
|
292
|
+
if (original !== null) await promises.writeFile(tempPath, original);
|
|
293
|
+
const staged = {
|
|
294
|
+
mode,
|
|
295
|
+
original,
|
|
296
|
+
realPath,
|
|
297
|
+
tempPath
|
|
298
|
+
};
|
|
299
|
+
this.files.set(realPath, staged);
|
|
300
|
+
return staged;
|
|
301
|
+
}
|
|
302
|
+
async loadStaged(catalog, ref) {
|
|
303
|
+
const staged = this.files.get(ref.path);
|
|
304
|
+
if (!staged) {
|
|
305
|
+
const document = await catalog.loadDocument(ref);
|
|
306
|
+
return document === null ? null : cloneDocument(document);
|
|
307
|
+
}
|
|
308
|
+
const tempRef = {
|
|
309
|
+
...ref,
|
|
310
|
+
path: staged.tempPath
|
|
311
|
+
};
|
|
312
|
+
const document = await catalog.loadDocument(tempRef);
|
|
313
|
+
return document === null ? null : restoreDocumentRef(document, ref);
|
|
314
|
+
}
|
|
315
|
+
async writeStaged(catalog, document) {
|
|
316
|
+
const staged = await this.stageFile(document.ref.path);
|
|
317
|
+
const tempRef = {
|
|
318
|
+
...document.ref,
|
|
319
|
+
path: staged.tempPath
|
|
320
|
+
};
|
|
321
|
+
const stagedDocument = await catalog.loadDocument(tempRef);
|
|
322
|
+
await catalog.writeDocument({
|
|
323
|
+
...cloneDocument(document),
|
|
324
|
+
ref: tempRef,
|
|
325
|
+
state: stagedDocument === null || catalog.mergeStagedState === void 0 ? document.state : catalog.mergeStagedState({
|
|
326
|
+
document,
|
|
327
|
+
staged: stagedDocument
|
|
328
|
+
})
|
|
329
|
+
});
|
|
330
|
+
if (await catalog.loadDocument(tempRef) === null) throw new Error(`Catalog "${catalog.id}" did not persist staged document ${document.ref.path}.`);
|
|
331
|
+
this.pendingRefs.set(documentKey(document.ref), {
|
|
332
|
+
catalog,
|
|
333
|
+
ref: { ...document.ref }
|
|
334
|
+
});
|
|
335
|
+
for (const pending of this.pendingRefs.values()) if (pending.ref.path === document.ref.path) {
|
|
336
|
+
const pendingTempRef = {
|
|
337
|
+
...pending.ref,
|
|
338
|
+
path: staged.tempPath
|
|
339
|
+
};
|
|
340
|
+
if (await pending.catalog.loadDocument(pendingTempRef) === null) throw new Error(`Catalog "${pending.catalog.id}" could not reload staged document ${pending.ref.path}.`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
createAdapter(catalog) {
|
|
344
|
+
const mergeStagedState = catalog.mergeStagedState?.bind(catalog);
|
|
345
|
+
return {
|
|
346
|
+
createDocumentRef: (sourceRef, locale) => catalog.createDocumentRef(sourceRef, locale),
|
|
347
|
+
id: catalog.id,
|
|
348
|
+
listDocumentRefs: (sourceLocale) => catalog.listDocumentRefs(sourceLocale),
|
|
349
|
+
loadDocument: (ref) => this.loadStaged(catalog, ref),
|
|
350
|
+
...mergeStagedState === void 0 ? {} : { mergeStagedState },
|
|
351
|
+
reconcileDocument: async (args) => {
|
|
352
|
+
return cloneDocument(await catalog.reconcileDocument({
|
|
353
|
+
...args.history === void 0 ? {} : { history: args.history.map((entry) => structuredClone(entry)) },
|
|
354
|
+
ref: { ...args.ref },
|
|
355
|
+
source: cloneDocument(args.source),
|
|
356
|
+
target: args.target === null ? null : cloneDocument(args.target)
|
|
357
|
+
}));
|
|
358
|
+
},
|
|
359
|
+
...catalog.scaffoldLocale === void 0 ? {} : { scaffoldLocale: (options) => this.scaffoldCatalog(catalog, options) },
|
|
360
|
+
writeDocument: (document) => this.writeStaged(catalog, document)
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
async scaffoldCatalog(catalog, options) {
|
|
364
|
+
const strategy = options.strategy ?? "copy-source";
|
|
365
|
+
const fromLocale = strategy === "copy-source" ? this.sourceLocale : options.fromLocale ?? this.sourceLocale;
|
|
366
|
+
const refs = await catalog.listDocumentRefs(fromLocale);
|
|
367
|
+
if (strategy === "empty") return {
|
|
368
|
+
catalogId: catalog.id,
|
|
369
|
+
createdDocuments: 0,
|
|
370
|
+
locale: options.locale,
|
|
371
|
+
skippedDocuments: refs.length,
|
|
372
|
+
strategy
|
|
373
|
+
};
|
|
374
|
+
let createdDocuments = 0;
|
|
375
|
+
let skippedDocuments = 0;
|
|
376
|
+
const adapter = this.createAdapter(catalog);
|
|
377
|
+
for (const sourceRef of refs) {
|
|
378
|
+
const targetRef = adapter.createDocumentRef(sourceRef, options.locale);
|
|
379
|
+
if (await adapter.loadDocument(targetRef) !== null) {
|
|
380
|
+
skippedDocuments += 1;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const source = await adapter.loadDocument(sourceRef);
|
|
384
|
+
if (source === null) {
|
|
385
|
+
skippedDocuments += 1;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
await adapter.writeDocument(await adapter.reconcileDocument({
|
|
389
|
+
ref: targetRef,
|
|
390
|
+
source,
|
|
391
|
+
target: null
|
|
392
|
+
}));
|
|
393
|
+
createdDocuments += 1;
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
catalogId: catalog.id,
|
|
397
|
+
createdDocuments,
|
|
398
|
+
locale: options.locale,
|
|
399
|
+
skippedDocuments,
|
|
400
|
+
strategy
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
function commitFailure(commitError, rollbackErrors) {
|
|
405
|
+
return rollbackErrors.length === 0 ? commitError instanceof Error ? commitError : new Error(String(commitError)) : new AggregateError([commitError, ...rollbackErrors], "Translation commit failed and could not be completely rolled back.");
|
|
406
|
+
}
|
|
407
|
+
function durableStateStore(state) {
|
|
408
|
+
if (typeof state === "object" && state !== null) {
|
|
409
|
+
const candidate = state[DURABLE_TRANSACTION_STATE_STORE];
|
|
410
|
+
if (typeof candidate === "object" && candidate !== null && "commit" in candidate && typeof candidate.commit === "function") return candidate;
|
|
411
|
+
}
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
async function runStagedCatalogTransaction(config, operation, shouldCommit = () => true, scope) {
|
|
415
|
+
const saveScope = supportsScopedSave(config.state) ? scope : void 0;
|
|
416
|
+
return config.state.withLock(async () => {
|
|
417
|
+
const initialState = await config.state.load(saveScope);
|
|
418
|
+
const stagedState = new StagedStateStore(initialState);
|
|
419
|
+
const stagedCatalogs = new StagedCatalogs(config.catalogs, config.sourceLocale);
|
|
420
|
+
const stagedConfig = {
|
|
421
|
+
...config,
|
|
422
|
+
catalogs: stagedCatalogs.adapters(),
|
|
423
|
+
state: stagedState
|
|
424
|
+
};
|
|
425
|
+
try {
|
|
426
|
+
const result = await operation(stagedConfig);
|
|
427
|
+
if (!shouldCommit(result)) return result;
|
|
428
|
+
const durableStore = durableStateStore(config.state);
|
|
429
|
+
if (durableStore !== null) {
|
|
430
|
+
const documents = await stagedCatalogs.durableChanges();
|
|
431
|
+
if (documents.length > 0 || stagedState.hasChanges()) await durableStore.commit({
|
|
432
|
+
documents,
|
|
433
|
+
initialState: cloneState(initialState),
|
|
434
|
+
nextState: stagedState.stagedSnapshot(),
|
|
435
|
+
...saveScope === void 0 ? {} : { scope: saveScope }
|
|
436
|
+
});
|
|
437
|
+
} else {
|
|
438
|
+
let stateSaveAttempted = false;
|
|
439
|
+
try {
|
|
440
|
+
await stagedCatalogs.promote();
|
|
441
|
+
if (stagedState.hasChanges()) {
|
|
442
|
+
stateSaveAttempted = true;
|
|
443
|
+
await config.state.save(stagedState.stagedSnapshot(), saveScope);
|
|
444
|
+
}
|
|
445
|
+
} catch (error) {
|
|
446
|
+
const rollbackErrors = [];
|
|
447
|
+
try {
|
|
448
|
+
await stagedCatalogs.rollback();
|
|
449
|
+
} catch (rollbackError) {
|
|
450
|
+
rollbackErrors.push(rollbackError);
|
|
451
|
+
}
|
|
452
|
+
if (stateSaveAttempted) try {
|
|
453
|
+
await config.state.save(cloneState(initialState), saveScope);
|
|
454
|
+
} catch (rollbackError) {
|
|
455
|
+
rollbackErrors.push(rollbackError);
|
|
456
|
+
}
|
|
457
|
+
throw commitFailure(error, rollbackErrors);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return result;
|
|
461
|
+
} finally {
|
|
462
|
+
await stagedCatalogs.cleanup();
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/index.ts
|
|
468
|
+
function requireOptionValue(optionName, value) {
|
|
469
|
+
if (value === void 0) throw new Error(`Option "--${optionName}" requires a value.`);
|
|
470
|
+
return value;
|
|
471
|
+
}
|
|
472
|
+
function requireProviderChoice(value) {
|
|
473
|
+
if (value !== "ai-sdk" && value !== "openai") throw new Error(`Option "--provider" accepts "openai" or "ai-sdk", not "${value}".`);
|
|
474
|
+
return value;
|
|
475
|
+
}
|
|
476
|
+
function requireIdenticalToSourcePolicy(value) {
|
|
477
|
+
if (value !== "adopt" && value !== "skip") throw new Error(`Option "--identical-to-source" accepts "adopt" or "skip", not "${value}".`);
|
|
478
|
+
return value;
|
|
479
|
+
}
|
|
480
|
+
function requireNonNegativeIntegerOption(optionName, value) {
|
|
481
|
+
const rawValue = requireOptionValue(optionName, value);
|
|
482
|
+
const parsedValue = Number(rawValue);
|
|
483
|
+
if (!Number.isSafeInteger(parsedValue) || parsedValue < 0) throw new Error(`Option "--${optionName}" requires a non-negative integer.`);
|
|
484
|
+
return parsedValue;
|
|
485
|
+
}
|
|
486
|
+
async function validateConfig(config, configPath, options) {
|
|
487
|
+
return {
|
|
488
|
+
...await validateCatalogs(config, options),
|
|
489
|
+
configPath
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
const DEFAULT_SEMANTIC_REPAIR_ROUNDS = 0;
|
|
493
|
+
function printSyncSummary(result, semanticAudit, semanticRepairRounds = 0) {
|
|
494
|
+
const pendingTranslationReasons = summarizePendingTranslationReasons(result);
|
|
495
|
+
const failedTranslationIssues = summarizeFailedTranslationIssues(result);
|
|
496
|
+
console.log(JSON.stringify({
|
|
497
|
+
dryRun: result.dryRun,
|
|
498
|
+
metrics: result.metrics,
|
|
499
|
+
...result.dryRun && Object.keys(pendingTranslationReasons).length > 0 ? { pendingTranslationReasons } : {},
|
|
500
|
+
...semanticAudit === void 0 ? {} : {
|
|
501
|
+
semanticAudit,
|
|
502
|
+
semanticRepairRounds
|
|
503
|
+
},
|
|
504
|
+
...failedTranslationIssues === void 0 ? {} : { failedTranslationIssues }
|
|
505
|
+
}, null, 2));
|
|
506
|
+
}
|
|
507
|
+
function summarizeFailedTranslationIssues(result) {
|
|
508
|
+
if (result.metrics.failedEntries === 0) return;
|
|
509
|
+
const errors = result.documents.flatMap((document) => document.issues.filter((issue) => issue.severity === "error").map((issue) => ({
|
|
510
|
+
catalogId: document.catalogId,
|
|
511
|
+
code: issue.code,
|
|
512
|
+
locale: document.locale,
|
|
513
|
+
message: issue.message,
|
|
514
|
+
path: document.path,
|
|
515
|
+
unitId: document.unitId
|
|
516
|
+
})));
|
|
517
|
+
const counts = {};
|
|
518
|
+
for (const issue of errors) counts[issue.code] = (counts[issue.code] ?? 0) + 1;
|
|
519
|
+
return {
|
|
520
|
+
counts: Object.fromEntries(Object.entries(counts).toSorted(([leftCode, leftCount], [rightCode, rightCount]) => rightCount - leftCount || leftCode.localeCompare(rightCode))),
|
|
521
|
+
examples: errors.slice(0, 25)
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function summarizePendingTranslationReasons(result) {
|
|
525
|
+
const summary = {};
|
|
526
|
+
for (const document of result.documents) for (const [reason, count] of Object.entries(document.pendingTranslationReasons ?? {})) summary[reason] = (summary[reason] ?? 0) + count;
|
|
527
|
+
return Object.fromEntries(Object.entries(summary).toSorted(([leftReason, leftCount], [rightReason, rightCount]) => rightCount - leftCount || leftReason.localeCompare(rightReason)));
|
|
528
|
+
}
|
|
529
|
+
function dryRunBudgetError(config, result) {
|
|
530
|
+
if (!result.dryRun || config.validation?.dryRunBudget === void 0) return;
|
|
531
|
+
const budget = config.validation.dryRunBudget;
|
|
532
|
+
if (budget.maxPendingTranslations !== void 0 && result.metrics.translatedEntries > budget.maxPendingTranslations) return `Translation dry-run planned ${String(result.metrics.translatedEntries)} provider translations, exceeding the configured budget of ${String(budget.maxPendingTranslations)}.`;
|
|
533
|
+
const reasons = summarizePendingTranslationReasons(result);
|
|
534
|
+
const forbidden = (budget.forbiddenPendingTranslationReasons ?? []).filter((reason) => (reasons[reason] ?? 0) > 0);
|
|
535
|
+
return forbidden.length === 0 ? void 0 : `Translation dry-run included forbidden selection reasons: ${forbidden.map((reason) => `${reason} (${String(reasons[reason] ?? 0)})`).join(", ")}.`;
|
|
536
|
+
}
|
|
537
|
+
function assertDryRunBudget(config, result) {
|
|
538
|
+
const error = dryRunBudgetError(config, result);
|
|
539
|
+
if (error !== void 0) throw new Error(error);
|
|
540
|
+
}
|
|
541
|
+
async function convergeSemanticAudits(config, options) {
|
|
542
|
+
let sync = await syncCatalogs(config, options);
|
|
543
|
+
if (options.dryRun === true || sync.metrics.failedEntries > 0 || (config.semanticAudits?.length ?? 0) === 0 || usesGeneratorSelfCheck(config)) return {
|
|
544
|
+
audit: void 0,
|
|
545
|
+
repairRounds: 0,
|
|
546
|
+
sync
|
|
547
|
+
};
|
|
548
|
+
let audit = await auditCatalogs(config, options);
|
|
549
|
+
let repairRounds = 0;
|
|
550
|
+
const maxSemanticRepairRounds = config.validation?.semanticRepairAttempts ?? DEFAULT_SEMANTIC_REPAIR_ROUNDS;
|
|
551
|
+
const repairOptions = { ...options };
|
|
552
|
+
delete repairOptions.forceRetranslate;
|
|
553
|
+
delete repairOptions.forceRetranslatePaths;
|
|
554
|
+
while (audit.retranslate > 0 && repairRounds < maxSemanticRepairRounds) {
|
|
555
|
+
repairRounds += 1;
|
|
556
|
+
sync = await syncCatalogs(config, repairOptions);
|
|
557
|
+
if (sync.metrics.failedEntries > 0) break;
|
|
558
|
+
audit = await auditCatalogs(config, options);
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
audit,
|
|
562
|
+
repairRounds,
|
|
563
|
+
sync
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
async function syncWithSemanticAuditConvergence(config, options) {
|
|
567
|
+
if (options.dryRun === true) return convergeSemanticAudits(config, options);
|
|
568
|
+
return runStagedCatalogTransaction(config, (stagedConfig) => convergeSemanticAudits(stagedConfig, options), (result) => semanticAuditConvergenceError(result) === void 0, resolveStateScope(config, options));
|
|
569
|
+
}
|
|
570
|
+
function semanticAuditConvergenceError(result) {
|
|
571
|
+
if (result.sync.metrics.failedEntries > 0) return "Sync completed with failed translation entries.";
|
|
572
|
+
if ((result.audit?.unresolved ?? 0) > 0) return "Sync completed with unresolved semantic audits. Review or refresh the audit findings.";
|
|
573
|
+
if ((result.audit?.retranslate ?? 0) > 0) return `Semantic audit rejected translations after ${String(result.repairRounds)} configured repair round(s).`;
|
|
574
|
+
if (result.audit?.issues.some((issue) => issue.severity === "error")) return "Semantic audit completed with unresolved or unsafe translations.";
|
|
575
|
+
}
|
|
576
|
+
function assertSemanticAuditConvergence(result) {
|
|
577
|
+
const error = semanticAuditConvergenceError(result);
|
|
578
|
+
if (error !== void 0) throw new Error(error);
|
|
579
|
+
}
|
|
580
|
+
function buildSyncOptions(options) {
|
|
581
|
+
const syncOptions = {};
|
|
582
|
+
if (options.catalogIds && options.catalogIds.length > 0) syncOptions.catalogIds = options.catalogIds;
|
|
583
|
+
if (options.dryRun !== void 0) syncOptions.dryRun = options.dryRun;
|
|
584
|
+
if (options.forceRetranslate !== void 0) syncOptions.forceRetranslate = options.forceRetranslate;
|
|
585
|
+
if (options.forceRetranslatePaths && options.forceRetranslatePaths.length > 0) {
|
|
586
|
+
syncOptions.forceRetranslate = true;
|
|
587
|
+
syncOptions.forceRetranslatePaths = options.forceRetranslatePaths;
|
|
588
|
+
}
|
|
589
|
+
if (options.includePaths && options.includePaths.length > 0) syncOptions.includePaths = options.includePaths;
|
|
590
|
+
if (options.locales && options.locales.length > 0) syncOptions.locales = options.locales;
|
|
591
|
+
if (options.maxPendingTranslations !== void 0) syncOptions.maxPendingTranslations = options.maxPendingTranslations;
|
|
592
|
+
if (options.unitIds && options.unitIds.length > 0) syncOptions.unitIds = options.unitIds;
|
|
593
|
+
return syncOptions;
|
|
594
|
+
}
|
|
595
|
+
function projectStateLocales(state, locales) {
|
|
596
|
+
if (locales === void 0 || locales.length === 0) return state;
|
|
597
|
+
const included = new Set(locales);
|
|
598
|
+
return {
|
|
599
|
+
entries: Object.fromEntries(Object.entries(state.entries).filter(([, entry]) => included.has(entry.locale))),
|
|
600
|
+
version: state.version
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
function hasStoredSemanticAudits(state) {
|
|
604
|
+
return Object.values(state.entries).some((entry) => Object.keys(entry.validationAudits ?? {}).length > 0);
|
|
605
|
+
}
|
|
606
|
+
const EMPTY_SEMANTIC_AUDIT = {
|
|
607
|
+
accepted: 0,
|
|
608
|
+
audited: 0,
|
|
609
|
+
cached: 0,
|
|
610
|
+
checked: 0,
|
|
611
|
+
issues: [],
|
|
612
|
+
retranslate: 0,
|
|
613
|
+
unresolved: 0
|
|
614
|
+
};
|
|
615
|
+
async function scaffoldLocale(config, locale, options = {}) {
|
|
616
|
+
return Promise.all(config.catalogs.map(async (catalog) => {
|
|
617
|
+
if (!catalog.scaffoldLocale) return null;
|
|
618
|
+
return catalog.scaffoldLocale({
|
|
619
|
+
...options.fromLocale === void 0 ? {} : { fromLocale: options.fromLocale },
|
|
620
|
+
locale,
|
|
621
|
+
...options.strategy === void 0 ? {} : { strategy: options.strategy }
|
|
622
|
+
});
|
|
623
|
+
}));
|
|
624
|
+
}
|
|
625
|
+
function printHelp() {
|
|
626
|
+
console.log(`ai-translate
|
|
627
|
+
|
|
628
|
+
Usage:
|
|
629
|
+
ai-translate init [--integration <next-intl|i18next>] [--provider <openai|ai-sdk>] [--provider-package <@ai-sdk/...>] [--model <id>] [--preview] [--force]
|
|
630
|
+
ai-translate validate [--config <path>]
|
|
631
|
+
ai-translate check [--config <path>] [--locale <locale>] [--catalog <id>] [--unit <id>] [--include-path <json-pointer>] [--max-pending-translations <count>]
|
|
632
|
+
ai-translate audit [--check] [--refresh] [--config <path>] [--locale <locale>] [--catalog <id>] [--unit <id>] [--include-path <json-pointer>]
|
|
633
|
+
ai-translate sync [--config <path>] [--dry-run] [--force-retranslate] [--force-retranslate-path <json-pointer>] [--include-path <json-pointer>] [--locale <locale>] [--catalog <id>] [--unit <id>] [--max-pending-translations <count>]
|
|
634
|
+
ai-translate new-locale <locale> [--from <locale>] [--strategy <strategy>] [--config <path>]
|
|
635
|
+
ai-translate scaffold-locale <locale> --from <locale> [--strategy <strategy>] [--config <path>]
|
|
636
|
+
ai-translate adopt [--identical-to-source <adopt|skip>] [--dry-run] [--config <path>]
|
|
637
|
+
ai-translate --help
|
|
638
|
+
ai-translate --version`);
|
|
639
|
+
}
|
|
640
|
+
function parseCommand(argv) {
|
|
641
|
+
const options = {};
|
|
642
|
+
const positionals = [];
|
|
643
|
+
let command;
|
|
644
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
645
|
+
const arg = argv[index];
|
|
646
|
+
if (arg === void 0) continue;
|
|
647
|
+
if (arg === "--help" || arg === "-h") return {
|
|
648
|
+
command: "help",
|
|
649
|
+
options,
|
|
650
|
+
positionals
|
|
651
|
+
};
|
|
652
|
+
if (arg === "--version" || arg === "-v") return {
|
|
653
|
+
command: "version",
|
|
654
|
+
options,
|
|
655
|
+
positionals
|
|
656
|
+
};
|
|
657
|
+
if (arg.startsWith("--")) {
|
|
658
|
+
const [flag = "", inlineValue] = arg.slice(2).split("=", 2);
|
|
659
|
+
if (flag.length === 0) throw new Error("Encountered an empty option flag.");
|
|
660
|
+
const nextValue = inlineValue ?? argv[index + 1];
|
|
661
|
+
switch (flag) {
|
|
662
|
+
case "check":
|
|
663
|
+
options.auditCheck = true;
|
|
664
|
+
break;
|
|
665
|
+
case "config":
|
|
666
|
+
options.config = requireOptionValue(flag, nextValue);
|
|
667
|
+
if (inlineValue === void 0) index += 1;
|
|
668
|
+
break;
|
|
669
|
+
case "catalog":
|
|
670
|
+
(options.catalogIds ??= []).push(requireOptionValue(flag, nextValue));
|
|
671
|
+
if (inlineValue === void 0) index += 1;
|
|
672
|
+
break;
|
|
673
|
+
case "dry-run":
|
|
674
|
+
options.dryRun = true;
|
|
675
|
+
break;
|
|
676
|
+
case "force":
|
|
677
|
+
options.force = true;
|
|
678
|
+
break;
|
|
679
|
+
case "force-retranslate":
|
|
680
|
+
options.forceRetranslate = true;
|
|
681
|
+
break;
|
|
682
|
+
case "force-retranslate-path":
|
|
683
|
+
(options.forceRetranslatePaths ??= []).push(requireOptionValue(flag, nextValue));
|
|
684
|
+
if (inlineValue === void 0) index += 1;
|
|
685
|
+
break;
|
|
686
|
+
case "from":
|
|
687
|
+
options.from = requireOptionValue(flag, nextValue);
|
|
688
|
+
if (inlineValue === void 0) index += 1;
|
|
689
|
+
break;
|
|
690
|
+
case "include-path":
|
|
691
|
+
(options.includePaths ??= []).push(requireOptionValue(flag, nextValue));
|
|
692
|
+
if (inlineValue === void 0) index += 1;
|
|
693
|
+
break;
|
|
694
|
+
case "integration":
|
|
695
|
+
options.integration = requireOptionValue(flag, nextValue);
|
|
696
|
+
if (inlineValue === void 0) index += 1;
|
|
697
|
+
break;
|
|
698
|
+
case "locale":
|
|
699
|
+
(options.locales ??= []).push(requireOptionValue(flag, nextValue));
|
|
700
|
+
if (inlineValue === void 0) index += 1;
|
|
701
|
+
break;
|
|
702
|
+
case "model":
|
|
703
|
+
options.model = requireOptionValue(flag, nextValue);
|
|
704
|
+
if (inlineValue === void 0) index += 1;
|
|
705
|
+
break;
|
|
706
|
+
case "preview":
|
|
707
|
+
options.preview = true;
|
|
708
|
+
break;
|
|
709
|
+
case "provider":
|
|
710
|
+
options.provider = requireOptionValue(flag, nextValue);
|
|
711
|
+
if (inlineValue === void 0) index += 1;
|
|
712
|
+
break;
|
|
713
|
+
case "provider-package":
|
|
714
|
+
options.providerPackage = requireOptionValue(flag, nextValue);
|
|
715
|
+
if (inlineValue === void 0) index += 1;
|
|
716
|
+
break;
|
|
717
|
+
case "max-pending-translations":
|
|
718
|
+
options.maxPendingTranslations = requireNonNegativeIntegerOption(flag, nextValue);
|
|
719
|
+
if (inlineValue === void 0) index += 1;
|
|
720
|
+
break;
|
|
721
|
+
case "identical-to-source":
|
|
722
|
+
options.identicalToSource = requireIdenticalToSourcePolicy(requireOptionValue(flag, nextValue));
|
|
723
|
+
if (inlineValue === void 0) index += 1;
|
|
724
|
+
break;
|
|
725
|
+
case "refresh":
|
|
726
|
+
options.refresh = true;
|
|
727
|
+
break;
|
|
728
|
+
case "strategy":
|
|
729
|
+
options.strategy = requireOptionValue(flag, nextValue);
|
|
730
|
+
if (inlineValue === void 0) index += 1;
|
|
731
|
+
break;
|
|
732
|
+
case "unit":
|
|
733
|
+
(options.unitIds ??= []).push(requireOptionValue(flag, nextValue));
|
|
734
|
+
if (inlineValue === void 0) index += 1;
|
|
735
|
+
break;
|
|
736
|
+
default: throw new Error(`Unknown option "--${flag}".`);
|
|
737
|
+
}
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
if (!command) {
|
|
741
|
+
command = arg;
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
positionals.push(arg);
|
|
745
|
+
}
|
|
746
|
+
const parsedCommand = {
|
|
747
|
+
options,
|
|
748
|
+
positionals
|
|
749
|
+
};
|
|
750
|
+
if (command !== void 0) parsedCommand.command = command;
|
|
751
|
+
return parsedCommand;
|
|
752
|
+
}
|
|
753
|
+
async function runCli(argv = process.argv.slice(2), cwd = process.cwd()) {
|
|
754
|
+
try {
|
|
755
|
+
const parsed = parseCommand(argv);
|
|
756
|
+
if (!parsed.command || parsed.command === "help") {
|
|
757
|
+
printHelp();
|
|
758
|
+
return 0;
|
|
759
|
+
}
|
|
760
|
+
if (parsed.command === "version") {
|
|
761
|
+
console.log("0.0.0");
|
|
762
|
+
return 0;
|
|
763
|
+
}
|
|
764
|
+
switch (parsed.command) {
|
|
765
|
+
case "init": {
|
|
766
|
+
const result = await runInit(cwd, {
|
|
767
|
+
force: parsed.options.force === true,
|
|
768
|
+
...parsed.options.integration === void 0 ? {} : { integration: parsed.options.integration },
|
|
769
|
+
...parsed.options.model === void 0 ? {} : { model: parsed.options.model },
|
|
770
|
+
preview: parsed.options.preview === true || parsed.options.dryRun === true,
|
|
771
|
+
...parsed.options.provider === void 0 ? {} : { provider: requireProviderChoice(parsed.options.provider) },
|
|
772
|
+
...parsed.options.providerPackage === void 0 ? {} : { providerPackage: parsed.options.providerPackage }
|
|
773
|
+
});
|
|
774
|
+
console.log(result.lines.join("\n"));
|
|
775
|
+
return 0;
|
|
776
|
+
}
|
|
777
|
+
case "validate": {
|
|
778
|
+
await loadEnvFiles(cwd);
|
|
779
|
+
const { config, configPath } = await loadConfig(cwd, parsed.options.config);
|
|
780
|
+
const summary = await validateConfig(config, configPath, buildSyncOptions(parsed.options));
|
|
781
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
782
|
+
if (summary.issues.some((issue) => issue.severity === "error")) throw new Error("Validation failed.");
|
|
783
|
+
return 0;
|
|
784
|
+
}
|
|
785
|
+
case "check": {
|
|
786
|
+
await loadEnvFiles(cwd);
|
|
787
|
+
const { config, configPath } = await loadConfig(cwd, parsed.options.config);
|
|
788
|
+
const checkOptions = {
|
|
789
|
+
...buildSyncOptions(parsed.options),
|
|
790
|
+
...process.env.AI_TRANSLATE_CHECK_SNAPSHOT_LOCK === "1" ? { assumeStateLock: true } : {}
|
|
791
|
+
};
|
|
792
|
+
const stateSnapshot = projectStateLocales(await config.state.load(checkOptions.locales === void 0 ? void 0 : { locales: checkOptions.locales }), checkOptions.locales);
|
|
793
|
+
const checkConfig = {
|
|
794
|
+
...config,
|
|
795
|
+
state: {
|
|
796
|
+
load: () => Promise.resolve(stateSnapshot),
|
|
797
|
+
save: () => Promise.reject(/* @__PURE__ */ new Error("Translation check cannot persist translation state.")),
|
|
798
|
+
withLock: (operation) => config.state.withLock(operation)
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
const needsSemanticAudit = (checkConfig.semanticAudits?.length ?? 0) > 0 || hasStoredSemanticAudits(stateSnapshot);
|
|
802
|
+
const { auditResult, dryRunResult, validationResult } = await withTranslationIssueCache(async () => {
|
|
803
|
+
const checkedValidation = await validateConfig(checkConfig, configPath, {
|
|
804
|
+
...checkOptions,
|
|
805
|
+
acceptedProvenanceFastPath: true
|
|
806
|
+
});
|
|
807
|
+
const checkedDryRun = await syncCatalogs(checkConfig, {
|
|
808
|
+
...checkOptions,
|
|
809
|
+
dryRun: true
|
|
810
|
+
});
|
|
811
|
+
return {
|
|
812
|
+
auditResult: needsSemanticAudit ? await auditCatalogs(checkConfig, {
|
|
813
|
+
...checkOptions,
|
|
814
|
+
checkOnly: true
|
|
815
|
+
}) : EMPTY_SEMANTIC_AUDIT,
|
|
816
|
+
dryRunResult: checkedDryRun,
|
|
817
|
+
validationResult: checkedValidation
|
|
818
|
+
};
|
|
819
|
+
});
|
|
820
|
+
const hasValidationErrors = validationResult.issues.some((issue) => issue.severity === "error");
|
|
821
|
+
const hasPendingSync = dryRunResult.metrics.changedDocuments > 0 || dryRunResult.metrics.failedEntries > 0 || dryRunResult.metrics.staleManualEntries > 0 || dryRunResult.metrics.translatedEntries > 0;
|
|
822
|
+
const hasAuditErrors = auditResult.issues.some((issue) => issue.severity === "error");
|
|
823
|
+
console.log(JSON.stringify({
|
|
824
|
+
validation: validationResult,
|
|
825
|
+
audit: auditResult,
|
|
826
|
+
dryRun: dryRunResult.metrics
|
|
827
|
+
}, null, 2));
|
|
828
|
+
if (hasValidationErrors || hasPendingSync || hasAuditErrors) {
|
|
829
|
+
if (hasAuditErrors && !hasValidationErrors && !hasPendingSync) throw new Error("Translation check failed because semantic audit provenance is missing, stale, or unresolved. Run ai-translate audit --refresh.");
|
|
830
|
+
throw new Error("Translation check failed. Run ai-translate sync to reconcile localized content.");
|
|
831
|
+
}
|
|
832
|
+
return 0;
|
|
833
|
+
}
|
|
834
|
+
case "audit": {
|
|
835
|
+
await loadEnvFiles(cwd);
|
|
836
|
+
const { config } = await loadConfig(cwd, parsed.options.config);
|
|
837
|
+
const result = await auditCatalogs(config, {
|
|
838
|
+
...buildSyncOptions(parsed.options),
|
|
839
|
+
checkOnly: parsed.options.auditCheck ?? false,
|
|
840
|
+
refresh: parsed.options.refresh ?? false
|
|
841
|
+
});
|
|
842
|
+
console.log(JSON.stringify(result, null, 2));
|
|
843
|
+
if (result.issues.some((issue) => issue.severity === "error")) throw new Error(parsed.options.auditCheck ? "Semantic audit check failed. Run ai-translate audit --refresh." : "Semantic audit completed with unresolved or unsafe translations.");
|
|
844
|
+
return 0;
|
|
845
|
+
}
|
|
846
|
+
case "sync": {
|
|
847
|
+
await loadEnvFiles(cwd);
|
|
848
|
+
const { config } = await loadConfig(cwd, parsed.options.config);
|
|
849
|
+
const result = await withTranslationIssueCache(() => syncWithSemanticAuditConvergence(config, buildSyncOptions(parsed.options)));
|
|
850
|
+
printSyncSummary(result.sync, result.audit, result.repairRounds);
|
|
851
|
+
assertDryRunBudget(config, result.sync);
|
|
852
|
+
assertSemanticAuditConvergence(result);
|
|
853
|
+
return 0;
|
|
854
|
+
}
|
|
855
|
+
case "new-locale": {
|
|
856
|
+
const locale = parsed.positionals[0];
|
|
857
|
+
if (!locale) throw new Error("The \"new-locale\" command requires a <locale> argument.");
|
|
858
|
+
await loadEnvFiles(cwd);
|
|
859
|
+
const { config } = await loadConfig(cwd, parsed.options.config);
|
|
860
|
+
const fromLocale = parsed.options.from ?? config.sourceLocale;
|
|
861
|
+
const strategy = parsed.options.strategy ?? "copy-source";
|
|
862
|
+
if (parsed.options.dryRun && fromLocale !== config.sourceLocale) throw new Error("The \"new-locale\" command only supports --from <sourceLocale> when used with --dry-run.");
|
|
863
|
+
if (parsed.options.dryRun && strategy !== "copy-source") throw new Error("The \"new-locale\" command only supports --strategy copy-source when used with --dry-run.");
|
|
864
|
+
if (strategy !== "copy-source" && fromLocale === config.sourceLocale) throw new Error(`The "${strategy}" strategy requires --from <locale> to be a translated locale.`);
|
|
865
|
+
const syncOptions = {
|
|
866
|
+
...buildSyncOptions(parsed.options),
|
|
867
|
+
forceRetranslate: strategy === "copy-locale-and-retranslate",
|
|
868
|
+
locales: [locale]
|
|
869
|
+
};
|
|
870
|
+
const { result, scaffoldResults } = parsed.options.dryRun ? {
|
|
871
|
+
result: await convergeSemanticAudits(config, syncOptions),
|
|
872
|
+
scaffoldResults: []
|
|
873
|
+
} : await runStagedCatalogTransaction(config, async (stagedConfig) => {
|
|
874
|
+
const scaffoldResults = await scaffoldLocale(stagedConfig, locale, {
|
|
875
|
+
fromLocale,
|
|
876
|
+
strategy
|
|
877
|
+
});
|
|
878
|
+
return {
|
|
879
|
+
result: await convergeSemanticAudits(stagedConfig, syncOptions),
|
|
880
|
+
scaffoldResults
|
|
881
|
+
};
|
|
882
|
+
}, ({ result }) => semanticAuditConvergenceError(result) === void 0);
|
|
883
|
+
console.log(JSON.stringify({
|
|
884
|
+
dryRun: result.sync.dryRun,
|
|
885
|
+
fromLocale,
|
|
886
|
+
locale,
|
|
887
|
+
metrics: result.sync.metrics,
|
|
888
|
+
scaffoldResults,
|
|
889
|
+
...result.audit === void 0 ? {} : {
|
|
890
|
+
semanticAudit: result.audit,
|
|
891
|
+
semanticRepairRounds: result.repairRounds
|
|
892
|
+
},
|
|
893
|
+
strategy,
|
|
894
|
+
status: semanticAuditConvergenceError(result) === void 0 ? "ok" : "failed"
|
|
895
|
+
}, null, 2));
|
|
896
|
+
assertSemanticAuditConvergence(result);
|
|
897
|
+
return 0;
|
|
898
|
+
}
|
|
899
|
+
case "scaffold-locale": {
|
|
900
|
+
const locale = parsed.positionals[0];
|
|
901
|
+
if (!locale) throw new Error("The \"scaffold-locale\" command requires a <locale> argument.");
|
|
902
|
+
if (!parsed.options.from) throw new Error("The \"scaffold-locale\" command requires --from <locale>.");
|
|
903
|
+
await loadEnvFiles(cwd);
|
|
904
|
+
const { config } = await loadConfig(cwd, parsed.options.config);
|
|
905
|
+
const scaffoldResults = await scaffoldLocale(config, locale, {
|
|
906
|
+
fromLocale: parsed.options.from,
|
|
907
|
+
strategy: parsed.options.strategy ?? "copy-locale"
|
|
908
|
+
});
|
|
909
|
+
console.log(JSON.stringify({
|
|
910
|
+
fromLocale: parsed.options.from,
|
|
911
|
+
locale,
|
|
912
|
+
scaffoldResults,
|
|
913
|
+
strategy: parsed.options.strategy ?? "copy-locale",
|
|
914
|
+
status: "ok"
|
|
915
|
+
}, null, 2));
|
|
916
|
+
return 0;
|
|
917
|
+
}
|
|
918
|
+
case "adopt": {
|
|
919
|
+
await loadEnvFiles(cwd);
|
|
920
|
+
const { config } = await loadConfig(cwd, parsed.options.config);
|
|
921
|
+
const result = await adoptExistingTranslations({
|
|
922
|
+
catalogs: config.catalogs,
|
|
923
|
+
identicalToSource: parsed.options.identicalToSource ?? "adopt",
|
|
924
|
+
sourceLocale: config.sourceLocale,
|
|
925
|
+
targetLocales: config.targetLocales
|
|
926
|
+
});
|
|
927
|
+
if (!parsed.options.dryRun) await config.state.save(result.state);
|
|
928
|
+
console.log(JSON.stringify({
|
|
929
|
+
adopted: result.adopted,
|
|
930
|
+
dryRun: parsed.options.dryRun === true,
|
|
931
|
+
identicalToSource: result.identicalToSource,
|
|
932
|
+
status: "ok",
|
|
933
|
+
untranslated: result.untranslated
|
|
934
|
+
}, null, 2));
|
|
935
|
+
return 0;
|
|
936
|
+
}
|
|
937
|
+
default: throw new Error(`Unknown command "${parsed.command}".`);
|
|
938
|
+
}
|
|
939
|
+
} catch (error) {
|
|
940
|
+
console.error(error instanceof Error ? error.message : `Unexpected CLI failure: ${String(error)}`);
|
|
941
|
+
return 1;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
//#endregion
|
|
945
|
+
export { loadEnvFiles as a, loadConfig as i, runCli as n, findConfigPath as r, defineConfig$1 as t };
|
|
946
|
+
|
|
947
|
+
//# sourceMappingURL=src-Cnnq284f.mjs.map
|