@ai-translate/cli 0.3.0 → 0.4.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/README.md +20 -9
- package/dist/bin.mjs +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-Bb6vfpFm.mjs → src-SwBEpdIU.mjs} +542 -55
- package/dist/src-SwBEpdIU.mjs.map +1 -0
- package/package.json +5 -5
- package/dist/src-Bb6vfpFm.mjs.map +0 -1
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
1
2
|
import { auditCatalogs, defineConfig, defineConfig as defineConfig$1, resolveStateScope, syncCatalogs, usesGeneratorSelfCheck, validateCatalogs, withTranslationIssueCache } from "@ai-translate/core";
|
|
2
3
|
import { adoptExistingTranslations } from "@ai-translate/fs-json";
|
|
3
4
|
import { existsSync, promises } from "node:fs";
|
|
4
5
|
import * as path from "node:path";
|
|
5
6
|
import { config, parse } from "dotenv";
|
|
6
7
|
import { createJiti } from "jiti";
|
|
7
|
-
import { appleIntegration } from "@ai-translate/apple";
|
|
8
|
-
import { detectProject, renderConfig, requiredConfigPackages } from "@ai-translate/integrations";
|
|
8
|
+
import { appleIntegration, expoIntegration } from "@ai-translate/apple";
|
|
9
|
+
import { detectProject, isLocaleTag, renderConfig, requiredConfigPackages } from "@ai-translate/integrations";
|
|
9
10
|
import { builtinIntegrations } from "@ai-translate/next";
|
|
11
|
+
import { execFile, spawn } from "node:child_process";
|
|
12
|
+
import { promisify } from "node:util";
|
|
10
13
|
import { randomUUID } from "node:crypto";
|
|
11
14
|
import * as os from "node:os";
|
|
12
15
|
import { supportsScopedSave } from "@ai-translate/core/types";
|
|
@@ -67,28 +70,350 @@ async function loadConfig(cwd, explicitPath) {
|
|
|
67
70
|
};
|
|
68
71
|
}
|
|
69
72
|
//#endregion
|
|
70
|
-
//#region src/init.ts
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
]
|
|
73
|
+
//#region src/init-project.ts
|
|
74
|
+
const MANAGERS = [
|
|
75
|
+
"npm",
|
|
76
|
+
"pnpm",
|
|
77
|
+
"yarn",
|
|
78
|
+
"bun"
|
|
79
|
+
];
|
|
80
|
+
const LOCKFILES = {
|
|
81
|
+
npm: ["package-lock.json", "npm-shrinkwrap.json"],
|
|
82
|
+
pnpm: ["pnpm-lock.yaml"],
|
|
83
|
+
yarn: ["yarn.lock"],
|
|
84
|
+
bun: ["bun.lock", "bun.lockb"]
|
|
85
|
+
};
|
|
86
|
+
const SCRIPTS = {
|
|
87
|
+
translate: "ai-translate sync",
|
|
88
|
+
"translate:check": "ai-translate check",
|
|
89
|
+
"translate:validate": "ai-translate validate"
|
|
90
|
+
};
|
|
91
|
+
const DEPENDENCY_FIELDS = [
|
|
92
|
+
"dependencies",
|
|
93
|
+
"devDependencies",
|
|
94
|
+
"optionalDependencies"
|
|
95
|
+
];
|
|
96
|
+
function isFirstPartyPackage(name) {
|
|
97
|
+
return name === "ai-translate" || name.startsWith("@ai-translate/");
|
|
98
|
+
}
|
|
99
|
+
/** Registry versions, ranges, and tags are refreshable. Paths, URLs, workspace
|
|
100
|
+
* protocols, and aliases are intentional user choices and remain untouched. */
|
|
101
|
+
function isRegistryVersion(version) {
|
|
102
|
+
return /^[a-z\d.*+~^<>=| _-]+$/iu.test(version.trim());
|
|
103
|
+
}
|
|
104
|
+
function installCommand(manager, packages, cwd, isWorkspaceRoot, field) {
|
|
105
|
+
const adding = packages.length > 0;
|
|
106
|
+
const saveFlag = field === void 0 ? void 0 : manager === "npm" || manager === "pnpm" ? {
|
|
107
|
+
dependencies: "--save-prod",
|
|
108
|
+
devDependencies: "--save-dev",
|
|
109
|
+
optionalDependencies: "--save-optional"
|
|
110
|
+
}[field] : {
|
|
111
|
+
dependencies: void 0,
|
|
112
|
+
devDependencies: "--dev",
|
|
113
|
+
optionalDependencies: "--optional"
|
|
114
|
+
}[field];
|
|
115
|
+
return {
|
|
116
|
+
command: manager,
|
|
117
|
+
args: [
|
|
118
|
+
manager === "npm" || !adding ? "install" : "add",
|
|
119
|
+
...saveFlag === void 0 ? [] : [saveFlag],
|
|
120
|
+
...manager === "pnpm" && adding && isWorkspaceRoot ? ["--workspace-root"] : [],
|
|
121
|
+
...manager === "yarn" ? [] : ["--ignore-scripts"],
|
|
122
|
+
...packages
|
|
123
|
+
],
|
|
124
|
+
cwd,
|
|
125
|
+
...manager === "yarn" ? { env: { YARN_ENABLE_SCRIPTS: "false" } } : {}
|
|
126
|
+
};
|
|
81
127
|
}
|
|
82
|
-
|
|
83
|
-
|
|
128
|
+
function isRecord(value) {
|
|
129
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
130
|
+
}
|
|
131
|
+
function parseManifest(contents, manifestPath) {
|
|
132
|
+
let value;
|
|
84
133
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
134
|
+
value = JSON.parse(contents);
|
|
135
|
+
} catch {
|
|
136
|
+
throw new Error(`Invalid JSON in ${manifestPath}. Fix it before running init.`);
|
|
137
|
+
}
|
|
138
|
+
if (!isRecord(value)) throw new Error(`${manifestPath} must contain a JSON object.`);
|
|
139
|
+
for (const field of [
|
|
140
|
+
"dependencies",
|
|
141
|
+
"devDependencies",
|
|
142
|
+
"optionalDependencies",
|
|
143
|
+
"peerDependencies",
|
|
144
|
+
"scripts"
|
|
145
|
+
]) {
|
|
146
|
+
const entries = value[field];
|
|
147
|
+
if (entries !== void 0 && (!isRecord(entries) || Object.values(entries).some((entry) => typeof entry !== "string" || field !== "scripts" && entry.trim() === ""))) throw new Error(`${manifestPath}: ${field} must be an object of strings.`);
|
|
148
|
+
}
|
|
149
|
+
if (value.packageManager !== void 0 && typeof value.packageManager !== "string") throw new Error(`${manifestPath}: packageManager must be a string.`);
|
|
150
|
+
if (value.workspaces !== void 0) {
|
|
151
|
+
const patterns = isRecord(value.workspaces) ? value.workspaces.packages : value.workspaces;
|
|
152
|
+
if (!Array.isArray(patterns) || patterns.some((pattern) => typeof pattern !== "string" || pattern.trim() === "")) throw new Error(`${manifestPath}: workspaces must contain an array of package patterns.`);
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
async function readOptional(filePath) {
|
|
157
|
+
try {
|
|
158
|
+
return await promises.readFile(filePath, "utf8");
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error.code === "ENOENT") return;
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function exists(filePath) {
|
|
165
|
+
try {
|
|
166
|
+
await promises.lstat(filePath);
|
|
167
|
+
return true;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error.code === "ENOENT") return false;
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** Only literal lists establish membership; YAML aliases and expressions do not. */
|
|
174
|
+
function pnpmWorkspacePatterns(contents) {
|
|
175
|
+
const lines = contents.split(/\r?\n/u);
|
|
176
|
+
const start = lines.findIndex((line) => /^packages\s*:/u.test(line));
|
|
177
|
+
if (start === -1) return;
|
|
178
|
+
const inline = lines[start]?.replace(/^packages\s*:\s*/u, "").trim() ?? "";
|
|
179
|
+
if (inline !== "" && !inline.startsWith("#")) try {
|
|
180
|
+
const parsed = JSON.parse(inline);
|
|
181
|
+
return Array.isArray(parsed) && parsed.every((value) => typeof value === "string") ? parsed : void 0;
|
|
89
182
|
} catch {
|
|
90
|
-
return
|
|
183
|
+
return;
|
|
91
184
|
}
|
|
185
|
+
const patterns = [];
|
|
186
|
+
for (const line of lines.slice(start + 1)) {
|
|
187
|
+
if (line.trim() === "" || line.trimStart().startsWith("#")) continue;
|
|
188
|
+
if (!/^(?:\s|-)/u.test(line)) break;
|
|
189
|
+
const item = line.match(/^\s*-\s+(.+?)\s*$/u)?.[1];
|
|
190
|
+
if (item === void 0) return;
|
|
191
|
+
const doubleQuoted = item.match(/^("(?:[^"\\]|\\.)*")\s*(?:#.*)?$/u)?.[1];
|
|
192
|
+
const singleQuoted = item.match(/^'((?:[^']|'')*)'\s*(?:#.*)?$/u)?.[1];
|
|
193
|
+
if (doubleQuoted !== void 0) try {
|
|
194
|
+
patterns.push(JSON.parse(doubleQuoted));
|
|
195
|
+
} catch {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
else if (singleQuoted !== void 0) patterns.push(singleQuoted.replaceAll("''", "'"));
|
|
199
|
+
else {
|
|
200
|
+
const plain = item.replace(/\s+#.*$/u, "");
|
|
201
|
+
if (/^[!*&@[{?]|[\s"'#:>|]/u.test(plain)) return;
|
|
202
|
+
patterns.push(plain);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return patterns;
|
|
206
|
+
}
|
|
207
|
+
function matchesWorkspace(relative, patterns) {
|
|
208
|
+
const normalized = relative.split(path.sep).join("/");
|
|
209
|
+
const matches = (pattern) => path.matchesGlob(normalized, pattern.replace(/^\.\//u, "").replace(/\/$/u, ""));
|
|
210
|
+
return patterns.some((pattern) => !pattern.startsWith("!") && matches(pattern)) && !patterns.some((pattern) => pattern.startsWith("!") && matches(pattern.slice(1)));
|
|
211
|
+
}
|
|
212
|
+
async function workspaceRoot(cwd, manifest) {
|
|
213
|
+
for (let directory = cwd;; directory = path.dirname(directory)) {
|
|
214
|
+
const text = directory === cwd ? void 0 : await readOptional(path.join(directory, "package.json"));
|
|
215
|
+
const candidate = directory === cwd ? manifest : text === void 0 ? {} : parseManifest(text, path.join(directory, "package.json"));
|
|
216
|
+
const pnpmWorkspace = await readOptional(path.join(directory, "pnpm-workspace.yaml"));
|
|
217
|
+
if (candidate.workspaces !== void 0 || pnpmWorkspace !== void 0) {
|
|
218
|
+
const declared = isRecord(candidate.workspaces) ? candidate.workspaces.packages : candidate.workspaces;
|
|
219
|
+
const patterns = pnpmWorkspace === void 0 ? declared : pnpmWorkspacePatterns(pnpmWorkspace);
|
|
220
|
+
return directory === cwd || patterns !== void 0 && matchesWorkspace(path.relative(directory, cwd), patterns) ? {
|
|
221
|
+
root: directory,
|
|
222
|
+
manifest: candidate
|
|
223
|
+
} : void 0;
|
|
224
|
+
}
|
|
225
|
+
if (path.dirname(directory) === directory || await exists(path.join(directory, ".git"))) return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function managerFromDeclaration(value) {
|
|
229
|
+
if (value === void 0) return;
|
|
230
|
+
if (typeof value !== "string") throw new Error("The package manager must be npm, pnpm, yarn, or bun.");
|
|
231
|
+
const name = value.split("@")[0];
|
|
232
|
+
if (!MANAGERS.includes(name)) throw new Error(`Unsupported package manager ${value}. Use --package-manager npm, pnpm, yarn, or bun.`);
|
|
233
|
+
return name;
|
|
234
|
+
}
|
|
235
|
+
/** Reads project metadata only. Preview never executes a package manager or writes files. */
|
|
236
|
+
async function planProjectSetup(cwd, packages, options = {}) {
|
|
237
|
+
for (const name of packages) if (name.length > 214 || !/^(?:@[a-z\d][a-z\d._-]*\/)?[a-z\d][a-z\d._-]*$/iu.test(name)) throw new Error(`Invalid dependency package name ${JSON.stringify(name)}.`);
|
|
238
|
+
const root = await promises.realpath(cwd);
|
|
239
|
+
const manifestPath = path.join(root, "package.json");
|
|
240
|
+
if (await exists(manifestPath)) {
|
|
241
|
+
if (!(await promises.lstat(manifestPath)).isFile()) throw new Error(`${manifestPath} must be a regular file, not a symlink or directory.`);
|
|
242
|
+
}
|
|
243
|
+
const originalManifest = await readOptional(manifestPath);
|
|
244
|
+
const manifest = originalManifest === void 0 ? { private: true } : parseManifest(originalManifest, manifestPath);
|
|
245
|
+
const workspace = await workspaceRoot(root, manifest);
|
|
246
|
+
let manager = managerFromDeclaration(options.packageManager ?? manifest.packageManager ?? workspace?.manifest.packageManager);
|
|
247
|
+
if (manager === void 0) {
|
|
248
|
+
const detected = /* @__PURE__ */ new Set();
|
|
249
|
+
for (let directory = root;; directory = path.dirname(directory)) {
|
|
250
|
+
for (const candidate of MANAGERS) for (const fileName of LOCKFILES[candidate]) if (await exists(path.join(directory, fileName))) detected.add(candidate);
|
|
251
|
+
if (directory === (workspace?.root ?? root)) break;
|
|
252
|
+
}
|
|
253
|
+
if (detected.size > 1) throw new Error(`Ambiguous package managers (${[...detected].join(", ")}). Pass --package-manager to choose.`);
|
|
254
|
+
manager = [...detected][0] ?? "npm";
|
|
255
|
+
}
|
|
256
|
+
const declared = new Set(DEPENDENCY_FIELDS.flatMap((field) => Object.keys(manifest[field] ?? {})));
|
|
257
|
+
const missing = [...new Set(packages)].filter((name) => !declared.has(name));
|
|
258
|
+
const grouped = {
|
|
259
|
+
dependencies: [],
|
|
260
|
+
devDependencies: [],
|
|
261
|
+
optionalDependencies: []
|
|
262
|
+
};
|
|
263
|
+
const notices = [];
|
|
264
|
+
for (const name of new Set(packages)) {
|
|
265
|
+
const fields = DEPENDENCY_FIELDS.filter((field) => Object.hasOwn(manifest[field] ?? {}, name));
|
|
266
|
+
if (!isFirstPartyPackage(name)) {
|
|
267
|
+
if (fields.length === 0) grouped.devDependencies.push(name);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (fields.length > 1) throw new Error(`${name} is declared in multiple dependency sections (${fields.join(", ")}). Keep it in one section before running init so its dependency category can be preserved.`);
|
|
271
|
+
const field = fields[0] ?? "devDependencies";
|
|
272
|
+
const version = manifest[field]?.[name];
|
|
273
|
+
if (version !== void 0 && !isRegistryVersion(version)) {
|
|
274
|
+
notices.push(`Kept ${name}'s custom source in ${field}. Init cannot refresh it automatically; ensure it supports the generated configuration.`);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
grouped[field].push(`${name}@latest`);
|
|
278
|
+
if (version !== void 0) notices.push(`Refresh ${name} to the latest release, keeping it in ${field}.`);
|
|
279
|
+
}
|
|
280
|
+
const scripts = { ...manifest.scripts };
|
|
281
|
+
let changed = originalManifest === void 0;
|
|
282
|
+
for (const [name, command] of Object.entries(SCRIPTS)) if (!Object.hasOwn(scripts, name)) {
|
|
283
|
+
scripts[name] = command;
|
|
284
|
+
changed = true;
|
|
285
|
+
}
|
|
286
|
+
const indent = originalManifest?.match(/\r?\n([\t ]+)"/u)?.[1] ?? " ";
|
|
287
|
+
const newline = originalManifest?.includes("\r\n") === true ? "\r\n" : "\n";
|
|
288
|
+
const manifestContents = changed ? `${JSON.stringify({
|
|
289
|
+
...manifest,
|
|
290
|
+
scripts
|
|
291
|
+
}, null, indent).replaceAll("\n", newline)}${newline}` : void 0;
|
|
292
|
+
const installCommands = DEPENDENCY_FIELDS.filter((field) => grouped[field].length > 0).map((field) => installCommand(manager, grouped[field], root, workspace?.root === root, field));
|
|
293
|
+
if (installCommands.length === 0) installCommands.push(installCommand(manager, [], root, workspace?.root === root));
|
|
294
|
+
return {
|
|
295
|
+
packageManager: manager,
|
|
296
|
+
manifestPath,
|
|
297
|
+
manifestContents,
|
|
298
|
+
originalManifest,
|
|
299
|
+
packages: missing,
|
|
300
|
+
installCommands,
|
|
301
|
+
notices
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const executeFile = promisify(execFile);
|
|
305
|
+
/** Windows package-manager shims need cmd.exe. Only fixed manager names and
|
|
306
|
+
* argument tokens generated by this module may enter its command string. */
|
|
307
|
+
function packageManagerProcess(command, args, platform = process.platform) {
|
|
308
|
+
if (platform !== "win32" || command === "bun") return {
|
|
309
|
+
command,
|
|
310
|
+
args: [...args]
|
|
311
|
+
};
|
|
312
|
+
if (!MANAGERS.includes(command) || args.some((arg) => !/^[-@a-z\d._/]+$/iu.test(arg))) throw new Error("Unsafe Windows package-manager command or argument.");
|
|
313
|
+
return {
|
|
314
|
+
command: process.env.ComSpec ?? process.env.COMSPEC ?? "cmd.exe",
|
|
315
|
+
args: [
|
|
316
|
+
"/d",
|
|
317
|
+
"/s",
|
|
318
|
+
"/c",
|
|
319
|
+
`"${command} ${args.join(" ")}"`
|
|
320
|
+
],
|
|
321
|
+
windowsVerbatimArguments: true
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
const installProject = async (command, args, cwd, env) => {
|
|
325
|
+
let actualArgs = [...args];
|
|
326
|
+
let actualEnv = {
|
|
327
|
+
...process.env,
|
|
328
|
+
...env
|
|
329
|
+
};
|
|
330
|
+
if (command === "yarn") {
|
|
331
|
+
const probe = packageManagerProcess(command, ["--version"]);
|
|
332
|
+
const { stdout } = await executeFile(probe.command, probe.args, {
|
|
333
|
+
cwd,
|
|
334
|
+
timeout: 15e3,
|
|
335
|
+
...probe.windowsVerbatimArguments === void 0 ? {} : { windowsVerbatimArguments: probe.windowsVerbatimArguments }
|
|
336
|
+
});
|
|
337
|
+
const major = Number.parseInt(stdout.trim().split(".")[0] ?? "", 10);
|
|
338
|
+
if (!Number.isFinite(major) || major < 1) throw new Error("Unable to determine the installed Yarn version.");
|
|
339
|
+
if (major === 1) {
|
|
340
|
+
actualArgs = [
|
|
341
|
+
...actualArgs,
|
|
342
|
+
"--ignore-scripts",
|
|
343
|
+
...actualArgs[0] === "add" ? ["--ignore-workspace-root-check"] : []
|
|
344
|
+
];
|
|
345
|
+
actualEnv = { ...process.env };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
await new Promise((resolve, reject) => {
|
|
349
|
+
const invocation = packageManagerProcess(command, actualArgs);
|
|
350
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
351
|
+
cwd,
|
|
352
|
+
env: actualEnv,
|
|
353
|
+
shell: false,
|
|
354
|
+
stdio: "inherit",
|
|
355
|
+
...invocation.windowsVerbatimArguments === void 0 ? {} : { windowsVerbatimArguments: invocation.windowsVerbatimArguments }
|
|
356
|
+
});
|
|
357
|
+
child.once("error", reject);
|
|
358
|
+
child.once("exit", (code, signal) => {
|
|
359
|
+
if (code === 0) resolve();
|
|
360
|
+
else reject(/* @__PURE__ */ new Error(`${command} exited ${signal === null ? `with code ${String(code)}` : `after signal ${signal}`}.`));
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
/** Leaves successful setup edits in place if installing fails, so init can be retried. */
|
|
365
|
+
async function applyProjectSetup(plan, options = {}) {
|
|
366
|
+
if (await readOptional(plan.manifestPath) !== plan.originalManifest) throw new Error(`${plan.manifestPath} changed during init. Run init again.`);
|
|
367
|
+
if (await exists(plan.manifestPath) && !(await promises.lstat(plan.manifestPath)).isFile()) throw new Error(`${plan.manifestPath} must be a regular file, not a symlink or directory.`);
|
|
368
|
+
const lines = [...plan.notices];
|
|
369
|
+
if (plan.manifestContents !== void 0) {
|
|
370
|
+
await promises.writeFile(plan.manifestPath, plan.manifestContents, {
|
|
371
|
+
encoding: "utf8",
|
|
372
|
+
flag: plan.originalManifest === void 0 ? "wx" : "w"
|
|
373
|
+
});
|
|
374
|
+
lines.push(`${plan.originalManifest === void 0 ? "Created" : "Updated"} package.json with translation scripts.`);
|
|
375
|
+
}
|
|
376
|
+
for (const { command, args, cwd, env } of plan.installCommands) {
|
|
377
|
+
if (options.install === false) {
|
|
378
|
+
if (command === "yarn") lines.push(`Install dependencies (Yarn 2+): YARN_ENABLE_SCRIPTS=false yarn ${args.join(" ")}`, `Install dependencies (Yarn 1): yarn ${args.join(" ")} --ignore-scripts${args[0] === "add" ? " --ignore-workspace-root-check" : ""}`);
|
|
379
|
+
else lines.push(`Install dependencies: ${command} ${args.join(" ")}`);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
await (options.installer ?? installProject)(command, args, cwd, env);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
386
|
+
throw new Error(`Dependency installation failed: ${detail} Setup files were kept. Retry init to finish installing all required dependencies.`, { cause: error });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (options.install !== false) lines.push(`Dependencies installed with ${plan.packageManager}; lifecycle scripts were disabled.`);
|
|
390
|
+
return lines;
|
|
391
|
+
}
|
|
392
|
+
//#endregion
|
|
393
|
+
//#region src/init.ts
|
|
394
|
+
const CONFIG_FILENAME = "ai-translate.config.ts";
|
|
395
|
+
/** Detection and config plans stay independent of the installation workflow. */
|
|
396
|
+
const builtinInitIntegrations = [
|
|
397
|
+
...builtinIntegrations,
|
|
398
|
+
appleIntegration,
|
|
399
|
+
expoIntegration
|
|
400
|
+
];
|
|
401
|
+
function catalogs(setup) {
|
|
402
|
+
return [setup.plan.catalog, ...setup.plan.additionalCatalogs ?? []];
|
|
403
|
+
}
|
|
404
|
+
function hasResources(setup) {
|
|
405
|
+
return catalogs(setup).some((catalog) => catalog.kind !== "adapter" || !Array.isArray(catalog.options.include) || catalog.options.include.length > 0);
|
|
406
|
+
}
|
|
407
|
+
function overlaps(left, right) {
|
|
408
|
+
if (left.kind === "adapter" || right.kind === "adapter") return left.kind === "adapter" && right.kind === "adapter" && left.factory.from === right.factory.from && left.factory.name === right.factory.name && left.options.rootDir === right.options.rootDir;
|
|
409
|
+
const a = path.resolve(left.rootDir);
|
|
410
|
+
const b = path.resolve(right.rootDir);
|
|
411
|
+
const leftFiles = left.kind === "document-json" && left.localeFiles !== void 0 ? Object.values(left.localeFiles).map((file) => path.resolve(a, file)) : void 0;
|
|
412
|
+
const rightFiles = right.kind === "document-json" && right.localeFiles !== void 0 ? Object.values(right.localeFiles).map((file) => path.resolve(b, file)) : void 0;
|
|
413
|
+
if (leftFiles !== void 0 && rightFiles !== void 0) return leftFiles.some((file) => rightFiles.includes(file));
|
|
414
|
+
if (leftFiles !== void 0) return leftFiles.some((file) => right.kind === "document-json" ? path.dirname(file) === b : file.startsWith(`${b}${path.sep}`));
|
|
415
|
+
if (rightFiles !== void 0) return rightFiles.some((file) => left.kind === "document-json" ? path.dirname(file) === a : file.startsWith(`${a}${path.sep}`));
|
|
416
|
+
return a === b || a.startsWith(`${b}${path.sep}`) || b.startsWith(`${a}${path.sep}`);
|
|
92
417
|
}
|
|
93
418
|
function chooseSetup(setups, requested) {
|
|
94
419
|
if (requested !== void 0) {
|
|
@@ -96,49 +421,186 @@ function chooseSetup(setups, requested) {
|
|
|
96
421
|
if (match === void 0) throw new Error(`No ${requested} setup was detected. Detected: ${setups.map((setup) => setup.integrationId).join(", ") || "none"}.`);
|
|
97
422
|
return match;
|
|
98
423
|
}
|
|
99
|
-
const [best
|
|
100
|
-
if (best === void 0) throw new Error("No supported localization setup was found. ai-translate init recognises next-intl and i18next, Apple String Catalogs, localized .strings tables, and Xcode or Apple Swift package projects. For
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
424
|
+
const [best] = setups;
|
|
425
|
+
if (best === void 0) throw new Error("No supported localization setup was found. ai-translate init recognises next-intl and i18next, Expo locale mappings, Apple String Catalogs, localized .strings tables, and Xcode or Apple Swift package projects. For apps with hardcoded text, first externalize strings into localization resources. Run init from the project root; extraction and runtime wiring are described in docs/native-apps.md.");
|
|
426
|
+
const selected = [];
|
|
427
|
+
for (const candidate of setups.filter(hasResources)) {
|
|
428
|
+
const conflict = selected.find((other) => catalogs(other).some((left) => catalogs(candidate).some((right) => overlaps(left, right))));
|
|
429
|
+
if (conflict !== void 0) {
|
|
430
|
+
if (conflict.confidence === candidate.confidence) throw new Error(`Found overlapping localization setups (${conflict.integrationId}, ${candidate.integrationId}). Re-run with --integration <id> to choose.`);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
selected.push(candidate);
|
|
434
|
+
}
|
|
435
|
+
if (selected.length < 2) return selected[0] ?? best;
|
|
436
|
+
if (selected.some((setup) => setup.plan.sourceLocale !== best.plan.sourceLocale)) throw new Error("Detected setups use different source locales. Run init with --integration <id> and use separate configs for each source locale.");
|
|
437
|
+
const targetLocales = resolveTargets(selected.flatMap((setup) => setup.plan.targetLocales), best.plan.sourceLocale);
|
|
438
|
+
const combined = selected.flatMap((setup) => catalogs(setup).map((catalog, index) => {
|
|
439
|
+
const id = `${setup.integrationId}-${String(index + 1)}`;
|
|
440
|
+
return catalog.kind === "adapter" ? {
|
|
441
|
+
...catalog,
|
|
442
|
+
options: {
|
|
443
|
+
...catalog.options,
|
|
444
|
+
id
|
|
445
|
+
}
|
|
446
|
+
} : {
|
|
447
|
+
...catalog,
|
|
448
|
+
id,
|
|
449
|
+
messageFormat: catalog.messageFormat ?? setup.plan.messageFormat
|
|
450
|
+
};
|
|
451
|
+
}));
|
|
452
|
+
return {
|
|
453
|
+
confidence: best.confidence,
|
|
454
|
+
displayName: selected.map((setup) => setup.displayName).join(" + "),
|
|
455
|
+
evidence: selected.flatMap((setup) => setup.evidence),
|
|
456
|
+
integrationId: selected.map((setup) => setup.integrationId).join("+"),
|
|
457
|
+
plan: {
|
|
458
|
+
...best.plan,
|
|
459
|
+
catalog: combined[0] ?? best.plan.catalog,
|
|
460
|
+
additionalCatalogs: combined.slice(1),
|
|
461
|
+
targetLocales,
|
|
462
|
+
warnings: selected.flatMap((setup) => setup.plan.warnings)
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function resolveTargets(locales, source) {
|
|
467
|
+
const names = /* @__PURE__ */ new Map();
|
|
468
|
+
const sourceTag = Intl.getCanonicalLocales(source)[0];
|
|
469
|
+
for (const locale of locales) {
|
|
470
|
+
if (!isLocaleTag(locale)) throw new Error(`Invalid target locale ${JSON.stringify(locale)}.`);
|
|
471
|
+
const canonical = Intl.getCanonicalLocales(locale)[0] ?? locale;
|
|
472
|
+
if (canonical === sourceTag) throw new Error(`Target locale ${locale} is also the source locale.`);
|
|
473
|
+
const previous = names.get(canonical);
|
|
474
|
+
if (previous !== void 0 && previous !== locale) throw new Error(`Locale aliases ${previous} and ${locale} refer to the same language. Normalize the resource names before combining setups.`);
|
|
475
|
+
names.set(canonical, locale);
|
|
476
|
+
}
|
|
477
|
+
return [...names.values()];
|
|
478
|
+
}
|
|
479
|
+
async function readSetupFile(cwd, name) {
|
|
480
|
+
try {
|
|
481
|
+
const file = path.join(cwd, name);
|
|
482
|
+
const stat = await promises.lstat(file);
|
|
483
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`${name} must be a regular file; init will not follow symbolic links.`);
|
|
484
|
+
return await promises.readFile(file, "utf8");
|
|
485
|
+
} catch (error) {
|
|
486
|
+
if (error.code === "ENOENT") return null;
|
|
487
|
+
throw error;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function appendLines(original, additions) {
|
|
491
|
+
const existing = original ?? "";
|
|
492
|
+
const missing = additions.filter((line) => !existing.split(/\r?\n/u).includes(line));
|
|
493
|
+
if (missing.length === 0) return existing;
|
|
494
|
+
const newline = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
495
|
+
return `${existing}${existing.length > 0 && !existing.endsWith("\n") ? newline : ""}${missing.join(newline)}${newline}`;
|
|
496
|
+
}
|
|
497
|
+
function apiKeyVariable(options) {
|
|
498
|
+
if (options.provider !== "ai-sdk") return "OPENAI_API_KEY";
|
|
499
|
+
return {
|
|
500
|
+
"@ai-sdk/anthropic": "ANTHROPIC_API_KEY",
|
|
501
|
+
"@ai-sdk/google": "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
502
|
+
"@ai-sdk/groq": "GROQ_API_KEY",
|
|
503
|
+
"@ai-sdk/mistral": "MISTRAL_API_KEY",
|
|
504
|
+
"@ai-sdk/openai": "OPENAI_API_KEY",
|
|
505
|
+
"@ai-sdk/xai": "XAI_API_KEY"
|
|
506
|
+
}[options.providerPackage ?? "@ai-sdk/openai"];
|
|
507
|
+
}
|
|
508
|
+
/** Detect, plan all changes, then install the generated config's direct dependencies. */
|
|
111
509
|
async function runInit(cwd, options = {}) {
|
|
510
|
+
await readSetupFile(cwd, "package.json");
|
|
112
511
|
const setups = await detectProject(cwd, { integrations: options.integrations ?? builtinInitIntegrations });
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
...
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
512
|
+
const detected = chooseSetup(setups, options.integration);
|
|
513
|
+
const requestedLocales = options.locales === void 0 ? void 0 : resolveTargets(options.locales, detected.plan.sourceLocale).map((locale) => detected.plan.targetLocales.find((existing) => Intl.getCanonicalLocales(existing)[0] === Intl.getCanonicalLocales(locale)[0]) ?? locale);
|
|
514
|
+
const setup = requestedLocales === void 0 ? detected : {
|
|
515
|
+
...detected,
|
|
516
|
+
plan: {
|
|
517
|
+
...detected.plan,
|
|
518
|
+
targetLocales: requestedLocales
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
for (const catalog of catalogs(setup)) {
|
|
522
|
+
if (catalog.kind !== "document-json" || catalog.localeFiles === void 0) continue;
|
|
523
|
+
const missing = setup.plan.targetLocales.filter((locale) => !Object.hasOwn(catalog.localeFiles ?? {}, locale));
|
|
524
|
+
if (missing.length > 0) throw new Error(`Missing locale file mappings for ${missing.join(", ")}. Add them to the project's locale configuration (expo.locales for Expo), or use --locale to select only mapped languages.`);
|
|
525
|
+
}
|
|
526
|
+
const contents = renderConfig(setup.plan, options);
|
|
527
|
+
const lines = [
|
|
528
|
+
`Detected ${setup.displayName}:`,
|
|
529
|
+
...setup.evidence.map((item) => ` - ${item.detail} (${item.source})`),
|
|
530
|
+
` - Source locale ${setup.plan.sourceLocale}, ${String(setup.plan.targetLocales.length)} target locale(s): ${setup.plan.targetLocales.join(", ")}`,
|
|
531
|
+
...setup.plan.warnings.map((warning) => ` ! ${warning}`)
|
|
532
|
+
];
|
|
533
|
+
const selectedIds = setup.integrationId.split("+");
|
|
534
|
+
const others = setups.filter((candidate) => !selectedIds.includes(candidate.integrationId));
|
|
123
535
|
if (others.length > 0) lines.push(`Also detected, not used: ${others.map((candidate) => candidate.displayName).join(", ")}.`);
|
|
536
|
+
const existing = [];
|
|
537
|
+
for (const name of CONFIG_CANDIDATES) {
|
|
538
|
+
const original = await readSetupFile(cwd, name);
|
|
539
|
+
if (original !== null) existing.push({
|
|
540
|
+
contents,
|
|
541
|
+
name,
|
|
542
|
+
original
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
if (existing.length > 1) throw new Error(`Multiple ai-translate configs exist: ${existing.map((file) => file.name).join(", ")}. Keep one config before running init.`);
|
|
546
|
+
const config = existing[0] ?? {
|
|
547
|
+
contents,
|
|
548
|
+
name: CONFIG_FILENAME,
|
|
549
|
+
original: null
|
|
550
|
+
};
|
|
551
|
+
if (config.original !== null && config.original !== contents && options.force !== true && options.preview !== true) throw new Error(`${config.name} already exists with different contents. Use --preview to review changes or --force to overwrite it.`);
|
|
552
|
+
const project = await planProjectSetup(cwd, requiredConfigPackages(setup.plan, options), options.packageManager === void 0 ? {} : { packageManager: options.packageManager });
|
|
553
|
+
const files = [config];
|
|
554
|
+
const ignore = await readSetupFile(cwd, ".gitignore");
|
|
555
|
+
files.push({
|
|
556
|
+
name: ".gitignore",
|
|
557
|
+
original: ignore,
|
|
558
|
+
contents: appendLines(ignore, [
|
|
559
|
+
"node_modules/",
|
|
560
|
+
".env.local",
|
|
561
|
+
".env.*.local"
|
|
562
|
+
])
|
|
563
|
+
});
|
|
564
|
+
const key = apiKeyVariable(options);
|
|
565
|
+
if (key !== void 0) {
|
|
566
|
+
const example = await readSetupFile(cwd, ".env.example");
|
|
567
|
+
const hasKey = example?.split(/\r?\n/u).some((line) => line.trimStart().startsWith(`${key}=`)) === true;
|
|
568
|
+
files.push({
|
|
569
|
+
name: ".env.example",
|
|
570
|
+
original: example,
|
|
571
|
+
contents: hasKey ? example : appendLines(example, [`${key}=`])
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
lines.push("", `Package manager: ${project.packageManager}.`);
|
|
124
575
|
if (options.preview === true) {
|
|
125
|
-
lines.push(
|
|
576
|
+
lines.push(...project.notices);
|
|
577
|
+
for (const file of files) if (file.contents !== file.original || file === config) lines.push("", `Would write ${file.name}:`, "", file.name === ".env.example" ? `${key ?? "PROVIDER_API_KEY"}= (existing entries preserved)` : file.contents);
|
|
578
|
+
if (project.manifestContents !== void 0) lines.push("", "Would write package.json:", "", project.manifestContents);
|
|
579
|
+
for (const command of project.installCommands) lines.push("", `Would ${options.install === false ? "skip installation; run" : "install with"}: ${command.command} ${command.args.join(" ")}`);
|
|
126
580
|
return {
|
|
127
581
|
configPath: null,
|
|
128
582
|
lines,
|
|
129
583
|
setup
|
|
130
584
|
};
|
|
131
585
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
586
|
+
for (const file of files) {
|
|
587
|
+
if (file.contents === file.original) continue;
|
|
588
|
+
if (await readSetupFile(cwd, file.name) !== file.original) throw new Error(`${file.name} changed during init; rerun to review the current project.`);
|
|
589
|
+
await promises.writeFile(path.join(cwd, file.name), file.contents, {
|
|
590
|
+
encoding: "utf8",
|
|
591
|
+
flag: file.original === null ? "wx" : "w"
|
|
592
|
+
});
|
|
593
|
+
lines.push(`Wrote ${file.name}.`);
|
|
594
|
+
}
|
|
595
|
+
lines.push(...await applyProjectSetup(project, { install: options.install !== false }));
|
|
596
|
+
lines.push("", "Setup complete. Before translating:");
|
|
597
|
+
if (key !== void 0) lines.push(` Set ${key} in your shell or .env.local; .env.example documents the variable.`);
|
|
598
|
+
else lines.push(` Configure the credentials your ${options.providerPackage ?? "AI SDK"} provider reads.`);
|
|
599
|
+
if (setup.plan.targetLocales.length === 0) lines.push(` Choose target languages in ${config.name}, or rerun init --locale fr --locale de --force.`);
|
|
600
|
+
if (!hasResources(setup)) lines.push(" Extract localization resources using the instructions above, then rerun init --force.");
|
|
601
|
+
lines.push(" Run npx ai-translate sync --dry-run to review the work, then npx ai-translate sync to translate.");
|
|
140
602
|
return {
|
|
141
|
-
configPath,
|
|
603
|
+
configPath: path.join(cwd, config.name),
|
|
142
604
|
lines,
|
|
143
605
|
setup
|
|
144
606
|
};
|
|
@@ -491,6 +953,10 @@ function requireProviderChoice(value) {
|
|
|
491
953
|
if (value !== "ai-sdk" && value !== "openai") throw new Error(`Option "--provider" accepts "openai" or "ai-sdk", not "${value}".`);
|
|
492
954
|
return value;
|
|
493
955
|
}
|
|
956
|
+
function requirePackageManager(value) {
|
|
957
|
+
if (value !== "npm" && value !== "pnpm" && value !== "yarn" && value !== "bun") throw new Error("Option \"--package-manager\" accepts npm, pnpm, yarn, or bun.");
|
|
958
|
+
return value;
|
|
959
|
+
}
|
|
494
960
|
function requireIdenticalToSourcePolicy(value) {
|
|
495
961
|
if (value !== "adopt" && value !== "skip") throw new Error(`Option "--identical-to-source" accepts "adopt" or "skip", not "${value}".`);
|
|
496
962
|
return value;
|
|
@@ -644,7 +1110,7 @@ function printHelp() {
|
|
|
644
1110
|
console.log(`ai-translate
|
|
645
1111
|
|
|
646
1112
|
Usage:
|
|
647
|
-
ai-translate init [--integration <id>] [--provider <openai|ai-sdk>] [--provider-package <@ai-sdk/...>] [--model <id>] [--preview] [--force]
|
|
1113
|
+
ai-translate init [--locale <locale>] [--integration <id>] [--provider <openai|ai-sdk>] [--provider-package <@ai-sdk/...>] [--model <id>] [--package-manager <npm|pnpm|yarn|bun>] [--no-install] [--preview] [--force]
|
|
648
1114
|
ai-translate validate [--config <path>]
|
|
649
1115
|
ai-translate check [--config <path>] [--locale <locale>] [--catalog <id>] [--unit <id>] [--include-path <json-pointer>] [--max-pending-translations <count>]
|
|
650
1116
|
ai-translate audit [--check] [--refresh] [--config <path>] [--locale <locale>] [--catalog <id>] [--unit <id>] [--include-path <json-pointer>]
|
|
@@ -677,6 +1143,13 @@ function parseCommand(argv) {
|
|
|
677
1143
|
if (flag.length === 0) throw new Error("Encountered an empty option flag.");
|
|
678
1144
|
const nextValue = inlineValue ?? argv[index + 1];
|
|
679
1145
|
switch (flag) {
|
|
1146
|
+
case "no-install":
|
|
1147
|
+
options.install = false;
|
|
1148
|
+
break;
|
|
1149
|
+
case "package-manager":
|
|
1150
|
+
options.packageManager = requireOptionValue(flag, nextValue);
|
|
1151
|
+
if (inlineValue === void 0) index += 1;
|
|
1152
|
+
break;
|
|
680
1153
|
case "check":
|
|
681
1154
|
options.auditCheck = true;
|
|
682
1155
|
break;
|
|
@@ -776,20 +1249,34 @@ async function runCli(argv = process.argv.slice(2), cwd = process.cwd()) {
|
|
|
776
1249
|
return 0;
|
|
777
1250
|
}
|
|
778
1251
|
if (parsed.command === "version") {
|
|
779
|
-
|
|
1252
|
+
const manifest = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
1253
|
+
console.log(manifest.version);
|
|
780
1254
|
return 0;
|
|
781
1255
|
}
|
|
782
1256
|
switch (parsed.command) {
|
|
783
1257
|
case "init": {
|
|
784
1258
|
const result = await runInit(cwd, {
|
|
785
1259
|
force: parsed.options.force === true,
|
|
1260
|
+
install: parsed.options.install !== false,
|
|
786
1261
|
...parsed.options.integration === void 0 ? {} : { integration: parsed.options.integration },
|
|
787
1262
|
...parsed.options.model === void 0 ? {} : { model: parsed.options.model },
|
|
1263
|
+
...parsed.options.locales === void 0 ? {} : { locales: parsed.options.locales },
|
|
1264
|
+
...parsed.options.packageManager === void 0 ? {} : { packageManager: requirePackageManager(parsed.options.packageManager) },
|
|
788
1265
|
preview: parsed.options.preview === true || parsed.options.dryRun === true,
|
|
789
1266
|
...parsed.options.provider === void 0 ? {} : { provider: requireProviderChoice(parsed.options.provider) },
|
|
790
1267
|
...parsed.options.providerPackage === void 0 ? {} : { providerPackage: parsed.options.providerPackage }
|
|
791
1268
|
});
|
|
792
1269
|
console.log(result.lines.join("\n"));
|
|
1270
|
+
if (result.configPath !== null && parsed.options.install !== false) {
|
|
1271
|
+
await loadEnvFiles(cwd);
|
|
1272
|
+
const { config, configPath } = await loadConfig(cwd, result.configPath);
|
|
1273
|
+
const validation = await validateConfig(config, configPath, { locales: [] });
|
|
1274
|
+
if (validation.issues.filter((issue) => issue.severity === "error").length > 0) {
|
|
1275
|
+
console.error(JSON.stringify(validation, null, 2));
|
|
1276
|
+
throw new Error("Setup files were created, but localization validation failed. Fix the reported resources and run npx ai-translate validate.");
|
|
1277
|
+
}
|
|
1278
|
+
console.log("Generated configuration and source resources validated.");
|
|
1279
|
+
}
|
|
793
1280
|
return 0;
|
|
794
1281
|
}
|
|
795
1282
|
case "validate": {
|
|
@@ -962,4 +1449,4 @@ async function runCli(argv = process.argv.slice(2), cwd = process.cwd()) {
|
|
|
962
1449
|
//#endregion
|
|
963
1450
|
export { loadEnvFiles as a, loadConfig as i, runCli as n, findConfigPath as r, defineConfig$1 as t };
|
|
964
1451
|
|
|
965
|
-
//# sourceMappingURL=src-
|
|
1452
|
+
//# sourceMappingURL=src-SwBEpdIU.mjs.map
|