@i18n-micro/cli 0.0.0-stage → 2.0.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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -2
  3. package/bin/i18n-micro.mjs +3 -0
  4. package/dist/chunks/check-BaP_H-mS.mjs +175 -0
  5. package/dist/chunks/clean-Do9z6D5j.mjs +68 -0
  6. package/dist/chunks/clean-bQVjG_2M.mjs +1 -0
  7. package/dist/chunks/export-C2J-pagB.mjs +219 -0
  8. package/dist/chunks/export-usBaNIpp.mjs +1 -0
  9. package/dist/chunks/extract-CJ8UfrVJ.mjs +30 -0
  10. package/dist/chunks/format-6CtMgO-M.mjs +26 -0
  11. package/dist/chunks/http-whB7a5ye.mjs +100 -0
  12. package/dist/chunks/import-CnL8U6Za.mjs +99 -0
  13. package/dist/chunks/info-rYas4Yw3.mjs +32 -0
  14. package/dist/chunks/json-BV_apCMu.mjs +439 -0
  15. package/dist/chunks/lint-BaEIQGMl.mjs +125 -0
  16. package/dist/chunks/nuxt-dKxG087V.mjs +366 -0
  17. package/dist/chunks/package-f9OTcDxl.mjs +3 -0
  18. package/dist/chunks/project-DgWgr7pF.mjs +206 -0
  19. package/dist/chunks/protect-D1HIuxmJ.mjs +36 -0
  20. package/dist/chunks/pseudo-oXBaONgF.mjs +32 -0
  21. package/dist/chunks/rename-uJnBX86A.mjs +117 -0
  22. package/dist/chunks/runner-wHXdhRdA.mjs +200 -0
  23. package/dist/chunks/search-Dj_Ynq_x.mjs +67 -0
  24. package/dist/chunks/source-Bx2lHSW7.mjs +744 -0
  25. package/dist/chunks/spec-BwZlT28L.mjs +507 -0
  26. package/dist/chunks/stats-BU-SmrVR.mjs +156 -0
  27. package/dist/chunks/sync-BY2EFEda.mjs +1 -0
  28. package/dist/chunks/sync-C0NVDabK.mjs +56 -0
  29. package/dist/chunks/sync-remote-y3ZATbF2.mjs +148 -0
  30. package/dist/chunks/text-to-i18n-DweavFLm.mjs +404 -0
  31. package/dist/chunks/translate-CmnmgAHX.mjs +543 -0
  32. package/dist/cli.mjs +4 -0
  33. package/dist/index.mjs +3 -0
  34. package/dist/src/cli.d.ts +1 -0
  35. package/dist/src/commands/check.d.ts +28 -0
  36. package/dist/src/commands/clean.d.ts +26 -0
  37. package/dist/src/commands/export.d.ts +10 -0
  38. package/dist/src/commands/extract.d.ts +6 -0
  39. package/dist/src/commands/format.d.ts +8 -0
  40. package/dist/src/commands/import.d.ts +8 -0
  41. package/dist/src/commands/info.d.ts +5 -0
  42. package/dist/src/commands/lint.d.ts +16 -0
  43. package/dist/src/commands/pseudo.d.ts +9 -0
  44. package/dist/src/commands/rename.d.ts +9 -0
  45. package/dist/src/commands/search.d.ts +20 -0
  46. package/dist/src/commands/stats.d.ts +20 -0
  47. package/dist/src/commands/sync-remote.d.ts +30 -0
  48. package/dist/src/commands/sync.d.ts +20 -0
  49. package/dist/src/commands/text-to-i18n.d.ts +10 -0
  50. package/dist/src/commands/translate.d.ts +14 -0
  51. package/dist/src/csv.d.ts +3 -0
  52. package/dist/src/errors.d.ts +22 -0
  53. package/dist/src/fs.d.ts +45 -0
  54. package/dist/src/glossary.d.ts +12 -0
  55. package/dist/src/help.d.ts +3 -0
  56. package/dist/src/http.d.ts +10 -0
  57. package/dist/src/index.d.ts +3 -0
  58. package/dist/src/json.d.ts +34 -0
  59. package/dist/src/lint-rules.d.ts +9 -0
  60. package/dist/src/log.d.ts +88 -0
  61. package/dist/src/nuxt.d.ts +66 -0
  62. package/dist/src/po.d.ts +14 -0
  63. package/dist/src/project.d.ts +64 -0
  64. package/dist/src/runner.d.ts +6 -0
  65. package/dist/src/source.d.ts +109 -0
  66. package/dist/src/spec.d.ts +57 -0
  67. package/dist/src/text-to-i18n.d.ts +46 -0
  68. package/dist/src/translate/ai.d.ts +2 -0
  69. package/dist/src/translate/drivers.d.ts +26 -0
  70. package/dist/src/translate/engine.d.ts +33 -0
  71. package/dist/src/translate/protect.d.ts +11 -0
  72. package/package.json +71 -3
@@ -0,0 +1,543 @@
1
+ import { A as log, C as addSecret, E as choose, M as plural, N as spinner, T as ask, _ as findingsError, d as ownValue, g as commandFailed, j as pick, v as notFoundError, w as addUrlSecrets, x as usageError } from "./json-BV_apCMu.mjs";
2
+ import { d as splitList, o as readJsonObject } from "./nuxt-dKxG087V.mjs";
3
+ import { t as Project } from "./project-DgWgr7pF.mjs";
4
+ import { n as requestJson, t as jsonBody } from "./http-whB7a5ye.mjs";
5
+ import { i as restore, n as isIntact, r as protect, t as decodeEntities } from "./protect-D1HIuxmJ.mjs";
6
+ import { trimTrailingSlashes } from "@i18n-micro/utils/app-path";
7
+ import { existsSync } from "node:fs";
8
+ import { resolve } from "node:path";
9
+ // src/glossary.ts
10
+ var optionalString = (value) => typeof value === "string" && value !== "" ? value : void 0;
11
+ function loadGlossary(path) {
12
+ const raw = readJsonObject(path).entries;
13
+ if (!Array.isArray(raw)) return { entries: [] };
14
+ return { entries: raw.flatMap((item) => {
15
+ const entry = item;
16
+ if (typeof entry?.source !== "string" || typeof entry.target !== "string") return [];
17
+ if (entry.source.trim() === "" || entry.target.trim() === "") return [];
18
+ return [{
19
+ source: entry.source,
20
+ target: entry.target,
21
+ from: optionalString(entry.from),
22
+ to: optionalString(entry.to)
23
+ }];
24
+ }) };
25
+ }
26
+ var matches = (bound, locale) => !bound || bound.toLowerCase() === locale.toLowerCase();
27
+ /** Terms for one language pair, as the lines a translation prompt quotes. At most 100: a prompt has a budget. */
28
+ function glossaryContext(glossary, from, to) {
29
+ const terms = glossary.entries.filter((entry) => matches(entry.from, from) && matches(entry.to, to)).slice(0, 100);
30
+ return terms.length === 0 ? void 0 : terms.map((entry) => `${entry.source} => ${entry.target}`).join("\n");
31
+ }
32
+ // src/translate/ai.ts
33
+ var GATEWAY_URL = "https://ai-gateway.vercel.sh/v1";
34
+ var OPENAI_URL = "https://api.openai.com/v1";
35
+ var ANTHROPIC_URL = "https://api.anthropic.com/v1";
36
+ var DEFAULT_GATEWAY_MODEL = "anthropic/claude-sonnet-4.5";
37
+ var OUTPUT_SCHEMA = {
38
+ type: "object",
39
+ properties: { translations: {
40
+ type: "array",
41
+ items: { type: "string" }
42
+ } },
43
+ required: ["translations"],
44
+ additionalProperties: false
45
+ };
46
+ function systemPrompt({ from, to, options }) {
47
+ const lines = [
48
+ `You are a professional localizer translating software UI text from ${from} to ${to}.`,
49
+ "Rules:",
50
+ "1. Tags such as <v0/> are placeholders: keep each one exactly as it is, in the place the grammar needs.",
51
+ "2. Preserve all other HTML/XML tags.",
52
+ "3. Text separated by \"|\" holds plural forms: keep the separators and translate each form.",
53
+ "4. Answer with JSON: {\"translations\": [...]}, one string per input string, in the same order. No explanations."
54
+ ];
55
+ if (options.translationContext) lines.push(`Context: ${options.translationContext}`);
56
+ if (options.glossaryContext) lines.push("Glossary (must be followed when a term appears):", options.glossaryContext);
57
+ return lines.join("\n");
58
+ }
59
+ var userPrompt = (texts) => JSON.stringify(texts);
60
+ function parseTranslations(content, reason, count) {
61
+ if (reason === "length" || reason === "max_tokens") throw usageError("AI: the answer was cut off.", ["Lower --chunkSize or raise --options maxTokens:<n>."]);
62
+ if (!content) throw usageError("AI: empty answer.");
63
+ const translations = JSON.parse(content).translations;
64
+ if (!Array.isArray(translations) || translations.length !== count) throw usageError(`AI: expected ${plural(count, "translation")}.`, ["Lower --chunkSize."]);
65
+ return translations;
66
+ }
67
+ var envKey = (name) => process.env[name]?.trim() || void 0;
68
+ function resolveKey(provider, token) {
69
+ const name = `${provider === "gateway" ? "AI_GATEWAY" : provider.toUpperCase()}_API_KEY`;
70
+ const key = token.trim() || envKey(name);
71
+ if (!key) throw usageError(`AI provider "${provider}" needs a key.`, [`Set I18N_TRANSLATE_TOKEN or ${name}.`]);
72
+ addSecret(key);
73
+ return key;
74
+ }
75
+ async function viaChatCompletions(request, url, model, key, strict) {
76
+ const { texts, options, http } = request;
77
+ const { headers, ...payload } = jsonBody({
78
+ model,
79
+ max_tokens: options.maxTokens ? Number(options.maxTokens) : void 0,
80
+ messages: [{
81
+ role: "system",
82
+ content: systemPrompt(request)
83
+ }, {
84
+ role: "user",
85
+ content: userPrompt(texts)
86
+ }],
87
+ response_format: {
88
+ type: "json_schema",
89
+ json_schema: {
90
+ name: "translations",
91
+ schema: OUTPUT_SCHEMA,
92
+ ...strict ? { strict: true } : {}
93
+ }
94
+ }
95
+ });
96
+ const init = {
97
+ method: "POST",
98
+ ...http,
99
+ ...payload,
100
+ headers: key ? {
101
+ ...headers,
102
+ Authorization: `Bearer ${key}`
103
+ } : headers
104
+ };
105
+ const choice = (await requestJson(`${url}/chat/completions`, init)).choices?.[0];
106
+ if (choice?.message?.refusal) throw usageError(`AI refused: ${choice.message.refusal}`);
107
+ return parseTranslations(choice?.message?.content, choice?.finish_reason, texts.length);
108
+ }
109
+ /** Anthropic's OpenAI layer ignores `response_format`, so structured output needs the native API. */
110
+ async function viaAnthropic(request, url, model, key) {
111
+ const { texts, options, http } = request;
112
+ const { headers, ...payload } = jsonBody({
113
+ model,
114
+ max_tokens: Number(options.maxTokens ?? 8192),
115
+ system: systemPrompt(request),
116
+ messages: [{
117
+ role: "user",
118
+ content: userPrompt(texts)
119
+ }],
120
+ output_config: { format: {
121
+ type: "json_schema",
122
+ schema: OUTPUT_SCHEMA
123
+ } }
124
+ });
125
+ const data = await requestJson(`${url}/messages`, {
126
+ method: "POST",
127
+ ...http,
128
+ ...payload,
129
+ headers: {
130
+ ...headers,
131
+ "x-api-key": key,
132
+ "anthropic-version": "2023-06-01"
133
+ }
134
+ });
135
+ return parseTranslations(data.content?.find((block) => block.type === "text")?.text, data.stop_reason, texts.length);
136
+ }
137
+ var aiDriver = {
138
+ label: "AI",
139
+ maxBatch: 20,
140
+ keyOptional: true,
141
+ translate(request) {
142
+ const { options, token } = request;
143
+ const baseUrl = options.baseUrl && trimTrailingSlashes(options.baseUrl);
144
+ const provider = (options.provider ?? (baseUrl ? "custom" : "gateway")).toLowerCase();
145
+ const model = options.model ?? (provider === "gateway" ? DEFAULT_GATEWAY_MODEL : void 0);
146
+ if (!model) throw usageError("AI translation needs a model.", ["Example: --options provider:openai,model:gpt-4o-mini"]);
147
+ if (provider === "gateway" && !model.includes("/")) throw usageError("Gateway models are written provider/model.", [`Example: --options model:${DEFAULT_GATEWAY_MODEL}`]);
148
+ const named = provider === "gateway" || provider === "openai" || provider === "anthropic";
149
+ if (!named && !baseUrl) throw usageError(`Unknown AI provider "${provider}".`, ["Use gateway, openai or anthropic, or set baseUrl to an OpenAI-compatible server."]);
150
+ const key = named ? resolveKey(provider, token) : token.trim();
151
+ if (provider === "anthropic") return viaAnthropic(request, baseUrl ?? ANTHROPIC_URL, model, key);
152
+ if (provider === "openai") return viaChatCompletions(request, baseUrl ?? OPENAI_URL, model, key, true);
153
+ return viaChatCompletions(request, baseUrl ?? GATEWAY_URL, model, key, false);
154
+ }
155
+ };
156
+ // src/translate/drivers.ts
157
+ var base = (options, fallback) => trimTrailingSlashes(options.baseUrl ?? fallback);
158
+ var bareLanguage = (code) => code.split(/[-_]/)[0].toLowerCase();
159
+ var fromTable = (table) => (code) => table[code.toLowerCase()] ?? table[bareLanguage(code)] ?? bareLanguage(code);
160
+ var google = {
161
+ label: "Google Translate",
162
+ maxBatch: 128,
163
+ async translate({ texts, from, to, token, options, http }) {
164
+ const url = `${base(options, "https://translation.googleapis.com")}/language/translate/v2?key=${encodeURIComponent(token)}`;
165
+ const body = {
166
+ q: texts,
167
+ target: to,
168
+ source: from,
169
+ format: options.format ?? "html",
170
+ model: options.model
171
+ };
172
+ return ((await requestJson(url, {
173
+ method: "POST",
174
+ ...http,
175
+ ...jsonBody(body)
176
+ })).data?.translations ?? []).map((item) => decodeEntities(item.translatedText));
177
+ }
178
+ };
179
+ /** Spellings of Chinese in Traditional script. The bare `zh` a lookup falls back to means Simplified. */
180
+ var TRADITIONAL = [
181
+ "zh-tw",
182
+ "zh-hk",
183
+ "zh-mo",
184
+ "zh-hant",
185
+ "zh-hant-tw",
186
+ "zh-hant-hk",
187
+ "zh-hant-mo"
188
+ ];
189
+ var traditionalAs = (name) => Object.fromEntries(TRADITIONAL.map((code) => [code, name]));
190
+ var DEEPL_TARGETS = {
191
+ en: "EN-US",
192
+ "en-us": "EN-US",
193
+ "en-gb": "EN-GB",
194
+ es: "ES",
195
+ "es-419": "ES-419",
196
+ pt: "PT-PT",
197
+ "pt-pt": "PT-PT",
198
+ "pt-br": "PT-BR",
199
+ zh: "ZH-HANS",
200
+ "zh-cn": "ZH-HANS",
201
+ "zh-hans": "ZH-HANS",
202
+ ...traditionalAs("ZH-HANT")
203
+ };
204
+ var deeplHost = (token) => token.endsWith(":fx") ? "https://api-free.deepl.com" : "https://api.deepl.com";
205
+ var deepl = {
206
+ label: "DeepL",
207
+ maxBatch: 50,
208
+ maxChars: 1e5,
209
+ sourceLang: (code) => bareLanguage(code).toUpperCase(),
210
+ targetLang: (code) => DEEPL_TARGETS[code.toLowerCase()] ?? bareLanguage(code).toUpperCase(),
211
+ async translate({ texts, from, to, token, options, http }) {
212
+ const body = {
213
+ text: texts,
214
+ source_lang: from,
215
+ target_lang: to,
216
+ tag_handling: options.tagHandling ?? "xml",
217
+ formality: options.formality,
218
+ glossary_id: options.glossaryId,
219
+ context: options.context,
220
+ model_type: options.modelType
221
+ };
222
+ const { headers, ...payload } = jsonBody(body);
223
+ return (await requestJson(`${base(options, deeplHost(token))}/v2/translate`, {
224
+ method: "POST",
225
+ ...http,
226
+ ...payload,
227
+ headers: {
228
+ ...headers,
229
+ Authorization: `DeepL-Auth-Key ${token}`
230
+ }
231
+ })).translations.map((item) => item.text);
232
+ }
233
+ };
234
+ var yandexcloud = {
235
+ label: "Yandex Cloud Translate",
236
+ maxBatch: 100,
237
+ maxChars: 1e4,
238
+ sourceLang: bareLanguage,
239
+ targetLang: bareLanguage,
240
+ async translate({ texts, from, to, token, options, http }) {
241
+ const scheme = options.authScheme ?? "Api-Key";
242
+ const { headers, ...payload } = jsonBody({
243
+ texts,
244
+ sourceLanguageCode: from,
245
+ targetLanguageCode: to,
246
+ format: "HTML",
247
+ folderId: options.folderId,
248
+ model: options.model
249
+ });
250
+ return ((await requestJson(`${base(options, "https://translate.api.cloud.yandex.net")}/translate/v2/translate`, {
251
+ method: "POST",
252
+ ...http,
253
+ ...payload,
254
+ headers: {
255
+ ...headers,
256
+ Authorization: `${scheme} ${token}`
257
+ }
258
+ })).translations ?? []).map((item) => item.text ?? "");
259
+ }
260
+ };
261
+ var azureLang = fromTable({
262
+ zh: "zh-Hans",
263
+ "zh-cn": "zh-Hans",
264
+ ...traditionalAs("zh-Hant"),
265
+ "pt-br": "pt",
266
+ "pt-pt": "pt-pt"
267
+ });
268
+ var drivers = {
269
+ ai: aiDriver,
270
+ azure: {
271
+ label: "Azure Translator",
272
+ maxBatch: 1e3,
273
+ maxChars: 5e4,
274
+ sourceLang: azureLang,
275
+ targetLang: azureLang,
276
+ async translate({ texts, from, to, token, options, http }) {
277
+ const query = new URLSearchParams({
278
+ "api-version": "3.0",
279
+ from,
280
+ to,
281
+ textType: "html"
282
+ });
283
+ const { headers, ...payload } = jsonBody(texts.map((text) => ({ Text: text })));
284
+ const auth = { "Ocp-Apim-Subscription-Key": token };
285
+ if (options.region) auth["Ocp-Apim-Subscription-Region"] = options.region;
286
+ return (await requestJson(`${base(options, "https://api.cognitive.microsofttranslator.com")}/translate?${query}`, {
287
+ method: "POST",
288
+ ...http,
289
+ ...payload,
290
+ headers: {
291
+ ...headers,
292
+ ...auth
293
+ }
294
+ })).map((item) => item.translations[0]?.text ?? "");
295
+ }
296
+ },
297
+ deepl,
298
+ google,
299
+ libretranslate: {
300
+ label: "LibreTranslate",
301
+ maxBatch: 100,
302
+ keyOptional: true,
303
+ async translate({ texts, from, to, token, options, http }) {
304
+ const body = {
305
+ q: texts,
306
+ source: from,
307
+ target: to,
308
+ format: "html",
309
+ api_key: token || void 0
310
+ };
311
+ return [(await requestJson(`${base(options, "https://libretranslate.com")}/translate`, {
312
+ method: "POST",
313
+ ...http,
314
+ ...jsonBody(body)
315
+ })).translatedText].flat();
316
+ }
317
+ },
318
+ yandexcloud
319
+ };
320
+ /** Services the old `nuxt-i18n-micro-cli` had: each name fails with the reason it is gone. */
321
+ var removedServices = {
322
+ googlefree: "it used an undocumented endpoint. Use libretranslate for free translation.",
323
+ yandex: "Yandex closed the v1.5 API. Use yandexcloud.",
324
+ ibm: "IBM withdrew Watson Language Translator in 2024.",
325
+ lingvatranslate: "public instances return the source text unchanged. Use libretranslate.",
326
+ modernmt: "ModernMT sunsets on 2026-12-31.",
327
+ lilt: "its API needs a translation memory per language pair, which this CLI has no model for.",
328
+ unbabel: "its public API no longer answers.",
329
+ reverso: "the unofficial endpoint is blocked.",
330
+ baidu: "it was dropped from the CLI. Use ai, deepl, google or azure.",
331
+ papago: "it was dropped from the CLI. Use ai, deepl, google or azure.",
332
+ tencent: "it was dropped from the CLI. Use ai, deepl, google or azure.",
333
+ systran: "it was dropped from the CLI. Use ai, deepl, google or azure.",
334
+ mymemory: "its 500-character limit and missing markup mode break placeholders. Use libretranslate for free translation."
335
+ };
336
+ // src/translate/engine.ts
337
+ function resolveDriver(service) {
338
+ const name = service.toLowerCase();
339
+ const driver = ownValue(drivers, name);
340
+ if (driver) return driver;
341
+ const reason = ownValue(removedServices, name);
342
+ if (reason) throw usageError(`Translation service "${service}" was removed: ${reason}`);
343
+ throw usageError(`Unsupported translation service: ${service}`, [`Supported: ${Object.keys(drivers).join(", ")}`]);
344
+ }
345
+ /** Parses `key:value,key:value`. Only the first colon splits: values are often URLs. */
346
+ function parseOptions(text) {
347
+ const options = {};
348
+ for (const pair of (text ?? "").split(",")) {
349
+ const separator = pair.indexOf(":");
350
+ if (separator <= 0) continue;
351
+ options[pair.slice(0, separator).trim()] = pair.slice(separator + 1).trim();
352
+ }
353
+ return options;
354
+ }
355
+ /** Batches within what the service accepts, by count and by characters. */
356
+ function buildBatches(items, size, maxCount, maxChars = Number.POSITIVE_INFINITY) {
357
+ const batches = [];
358
+ let current = [];
359
+ let chars = 0;
360
+ for (const item of items) {
361
+ const itemChars = size(item);
362
+ if (current.length >= maxCount || current.length > 0 && chars + itemChars > maxChars) {
363
+ batches.push(current);
364
+ current = [];
365
+ chars = 0;
366
+ }
367
+ current.push(item);
368
+ chars += itemChars;
369
+ }
370
+ if (current.length > 0) batches.push(current);
371
+ return batches;
372
+ }
373
+ var httpOptions = (options) => ({
374
+ retries: options.retries ? Number(options.retries) : void 0,
375
+ retryDelayMs: options.retryDelayMs ? Number(options.retryDelayMs) : void 0,
376
+ timeoutMs: options.timeoutMs ? Number(options.timeoutMs) : void 0
377
+ });
378
+ function splitForms(text, separator) {
379
+ return separator && text.includes(separator) ? text.split(separator).map((part) => part.trim()) : [text];
380
+ }
381
+ /** The finished value of an item, or `null` when a placeholder got lost on the way. */
382
+ function assemble(segments, joiner) {
383
+ const values = segments.map((segment) => restore(segment.translated, segment.tokens));
384
+ return values.every((value, position) => value !== "" && isIntact(value, segments[position].tokens)) ? values.join(joiner) : null;
385
+ }
386
+ /**
387
+ * Translate one list of messages, plural forms and placeholders handled, into `to`.
388
+ *
389
+ * `onBatch` hears about every item as soon as its last form is back, so what was paid for can
390
+ * be saved before the next request. A failed request throws, and costs only what it carried.
391
+ */
392
+ async function translateItems(items, from, to, config, extra = {}, onBatch = () => {}) {
393
+ const driver = resolveDriver(config.service);
394
+ const toSegment = (index, form) => {
395
+ const { masked, tokens } = protect(form);
396
+ return {
397
+ index,
398
+ masked,
399
+ tokens
400
+ };
401
+ };
402
+ const formsOf = items.map((item, index) => splitForms(item.text, config.pluralSeparator).map((form) => toSegment(index, form)));
403
+ const segments = formsOf.flat();
404
+ const options = {
405
+ ...config.options,
406
+ ...extra
407
+ };
408
+ const base = {
409
+ from: driver.sourceLang?.(from) ?? from,
410
+ to: driver.targetLang?.(to) ?? to,
411
+ token: config.token,
412
+ options,
413
+ http: httpOptions(options)
414
+ };
415
+ const total = {
416
+ translated: [],
417
+ rejected: []
418
+ };
419
+ const joiner = ` ${config.pluralSeparator} `;
420
+ const maxCount = Math.min(config.chunkSize ?? driver.maxBatch, driver.maxBatch);
421
+ for (const batch of buildBatches(segments, (segment) => segment.masked.length, maxCount, driver.maxChars)) {
422
+ const translated = await driver.translate({
423
+ ...base,
424
+ texts: batch.map((segment) => segment.masked)
425
+ });
426
+ if (translated.length !== batch.length) throw commandFailed(`${driver.label} returned ${plural(translated.length, "translation")} for ${plural(batch.length, "string")}.`, ["Nothing from this batch was written."]);
427
+ for (const [position, segment] of batch.entries()) {
428
+ const text = translated[position];
429
+ segment.translated = typeof text === "string" ? text : "";
430
+ }
431
+ const done = {
432
+ translated: [],
433
+ rejected: []
434
+ };
435
+ for (const index of new Set(batch.map((segment) => segment.index))) {
436
+ if (formsOf[index].some((form) => form.translated === void 0)) continue;
437
+ const value = assemble(formsOf[index], joiner);
438
+ if (value === null) done.rejected.push(items[index].key);
439
+ else done.translated.push({
440
+ key: items[index].key,
441
+ value
442
+ });
443
+ }
444
+ onBatch(done);
445
+ total.translated.push(...done.translated);
446
+ total.rejected.push(...done.rejected);
447
+ }
448
+ return total;
449
+ }
450
+ // src/commands/translate.ts
451
+ var DEFAULT_GLOSSARY = ".i18n-glossary.json";
452
+ function jobsFor(reference, locale, replace) {
453
+ return reference.scopes.flatMap((scope) => {
454
+ const items = Object.entries(reference.strings(scope)).filter(([key, text]) => text !== "" && (replace || (locale.resolve(key, scope) ?? "") === "")).map(([key, text]) => ({
455
+ key,
456
+ text
457
+ }));
458
+ return items.length > 0 ? [{
459
+ scope,
460
+ items
461
+ }] : [];
462
+ });
463
+ }
464
+ async function resolveService(args) {
465
+ const service = args.service ?? await choose("Choose a translation service", Object.keys(drivers));
466
+ if (!service) throw usageError("Translation service is required.", ["Pass --service <name>."]);
467
+ return service;
468
+ }
469
+ async function resolveToken(args, keyOptional) {
470
+ const token = args.token ?? process.env.I18N_TRANSLATE_TOKEN;
471
+ if (token || keyOptional) return token ?? "";
472
+ const answer = await ask("API key:");
473
+ if (!answer) throw usageError("This service needs an API key.", ["Set I18N_TRANSLATE_TOKEN or pass --token."]);
474
+ return answer;
475
+ }
476
+ function resolveGlossary(args, rootDir) {
477
+ const path = resolve(rootDir, args.glossaryFile ?? DEFAULT_GLOSSARY);
478
+ if (existsSync(path)) return loadGlossary(path);
479
+ if (args.glossaryFile) throw notFoundError(`Glossary not found: ${path}`);
480
+ }
481
+ var contextOf = (scope, keys) => {
482
+ const shown = keys.slice(0, 10).join(", ");
483
+ return `scope=${scope === "" ? "global" : scope}; keys=${shown}${keys.length > 10 ? ` (+${keys.length - 10} more)` : ""}`;
484
+ };
485
+ function printPlan(project, codes, replace) {
486
+ const rows = codes.map((code) => {
487
+ const items = jobsFor(project.reference, project.locale(code), replace).flatMap((job) => job.items);
488
+ const chars = items.reduce((sum, item) => sum + item.text.length, 0);
489
+ return [code, `${plural(items.length, "key")}, ${plural(chars, "character")}`];
490
+ });
491
+ log.section("To translate");
492
+ log.rows(rows);
493
+ log.note("Dry run: nothing was sent.");
494
+ }
495
+ async function run(args) {
496
+ addSecret(args.token);
497
+ const project = await Project.load(args);
498
+ const { reference } = project;
499
+ const { defaultLocale, rootDir } = project.config;
500
+ const codes = project.selectLocales(splitList(args.locale));
501
+ if (args.dryRun) {
502
+ printPlan(project, codes, Boolean(args.replace));
503
+ return;
504
+ }
505
+ const service = await resolveService(args);
506
+ const driver = resolveDriver(service);
507
+ const token = await resolveToken(args, Boolean(driver.keyOptional));
508
+ addSecret(token);
509
+ const glossary = resolveGlossary(args, rootDir);
510
+ const options = parseOptions(args.options);
511
+ addUrlSecrets(options.baseUrl);
512
+ const config = {
513
+ service,
514
+ token,
515
+ options,
516
+ chunkSize: args.chunkSize,
517
+ pluralSeparator: args.pluralSeparator
518
+ };
519
+ const rejected = [];
520
+ let translated = 0;
521
+ for (const code of codes) {
522
+ const locale = project.locale(code);
523
+ log.note(`Translating locale: ${code}`);
524
+ spinner.update(`Translating ${code}`);
525
+ const glossaryText = glossary && glossaryContext(glossary, defaultLocale, code);
526
+ for (const { scope, items } of jobsFor(reference, locale, Boolean(args.replace))) await translateItems(items, defaultLocale, code, config, {
527
+ translationContext: contextOf(scope, items.map((item) => item.key)),
528
+ ...glossaryText ? { glossaryContext: glossaryText } : {}
529
+ }, (result) => {
530
+ for (const { key, value } of result.translated) {
531
+ const conflict = locale.set(key, value, scope);
532
+ if (conflict) rejected.push(`${code}: ${key} (${conflict} already holds a value)`);
533
+ else translated += 1;
534
+ }
535
+ rejected.push(...result.rejected.map((key) => `${code}: ${key} (a placeholder was lost, or the answer held no text)`));
536
+ project.save();
537
+ });
538
+ }
539
+ log.list("Not written", rejected);
540
+ log.success(`Translated ${plural(translated, "string")}.`);
541
+ if (rejected.length > 0) throw findingsError(`${plural(rejected.length, "string")} ${pick(rejected.length, "was", "were")} not written.`, ["Fill them in by hand."]);
542
+ }
543
+ export { run as default };
package/dist/cli.mjs ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { t as runCli } from "./chunks/runner-wHXdhRdA.mjs";
3
+ // src/cli.ts
4
+ await runCli(process.argv.slice(2));
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import { t as runCli } from "./chunks/runner-wHXdhRdA.mjs";
2
+ import { a as commands, n as COMMAND_GROUPS } from "./chunks/spec-BwZlT28L.mjs";
3
+ export { COMMAND_GROUPS, commands, runCli };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { Project } from '../project.js';
2
+ import { ProjectScan } from '../source.js';
3
+ import { CheckType } from '../spec.js';
4
+ export interface CheckArgs {
5
+ cwd?: string;
6
+ translationDir?: string;
7
+ only?: string;
8
+ ignore?: string;
9
+ duplicates?: boolean;
10
+ json?: boolean;
11
+ }
12
+ export interface Finding {
13
+ type: CheckType;
14
+ /** Absent for what is not about one locale. */
15
+ locale?: string;
16
+ file: string;
17
+ key: string;
18
+ detail?: string;
19
+ }
20
+ export interface CheckReport {
21
+ ok: boolean;
22
+ summary: Record<string, number>;
23
+ findings: Finding[];
24
+ /** Why `unused` was not checked: nothing can be called unused while some keys are computed at runtime. */
25
+ unusedSkipped?: string[];
26
+ }
27
+ export declare function runChecks(project: Project, scan: ProjectScan, types: Set<CheckType>, askedForUnused?: boolean): CheckReport;
28
+ export default function run(args: CheckArgs): Promise<void>;
@@ -0,0 +1,26 @@
1
+ import { Project } from '../project.js';
2
+ import { ProjectScan } from '../source.js';
3
+ export interface CleanArgs {
4
+ cwd?: string;
5
+ translationDir?: string;
6
+ include?: string;
7
+ exclude?: string;
8
+ ignore?: string;
9
+ dryRun?: boolean;
10
+ force?: boolean;
11
+ }
12
+ export interface UnusedKey {
13
+ code: string;
14
+ scope: string;
15
+ key: string;
16
+ }
17
+ /**
18
+ * Keys of this project's own files that nothing in the project can read. A key used anywhere
19
+ * stays in every scope: a page's route name is not always knowable from its file.
20
+ */
21
+ export declare function findUnused(project: Project, scan: ProjectScan): UnusedKey[];
22
+ /** Why removing keys cannot be trusted here. Empty when it can. */
23
+ export declare function deletionDoubts(project: Project, scan: ProjectScan): string[];
24
+ /** Nothing scanned proves nothing: no flag changes that. */
25
+ export declare function assertScanned(scan: ProjectScan): void;
26
+ export default function run(args: CleanArgs): Promise<void>;
@@ -0,0 +1,10 @@
1
+ export interface ExportArgs {
2
+ cwd?: string;
3
+ translationDir?: string;
4
+ format: 'po' | 'csv';
5
+ dir?: string;
6
+ force?: boolean;
7
+ }
8
+ export declare const defaultExchangeDir: (format: string) => string;
9
+ export declare function readManifest(dir: string): Record<string, unknown>;
10
+ export default function run(args: ExportArgs): Promise<void>;
@@ -0,0 +1,6 @@
1
+ export interface ExtractArgs {
2
+ cwd?: string;
3
+ translationDir?: string;
4
+ ignore?: string;
5
+ }
6
+ export default function run(args: ExtractArgs): Promise<void>;
@@ -0,0 +1,8 @@
1
+ export interface FormatArgs {
2
+ cwd?: string;
3
+ translationDir?: string;
4
+ indent: number;
5
+ sortKeys?: boolean;
6
+ check?: boolean;
7
+ }
8
+ export default function run(args: FormatArgs): Promise<void>;
@@ -0,0 +1,8 @@
1
+ export interface ImportArgs {
2
+ cwd?: string;
3
+ translationDir?: string;
4
+ format: 'po' | 'csv';
5
+ dir?: string;
6
+ dryRun?: boolean;
7
+ }
8
+ export default function run(args: ImportArgs): Promise<void>;
@@ -0,0 +1,5 @@
1
+ export interface InfoArgs {
2
+ cwd?: string;
3
+ json?: boolean;
4
+ }
5
+ export default function run(args: InfoArgs): Promise<void>;
@@ -0,0 +1,16 @@
1
+ export interface LintArgs {
2
+ cwd?: string;
3
+ translationDir?: string;
4
+ rules?: string;
5
+ fix?: boolean;
6
+ json?: boolean;
7
+ }
8
+ export interface LintResult {
9
+ file: string;
10
+ locale: string;
11
+ scope: string;
12
+ key: string;
13
+ value: string;
14
+ rule: string;
15
+ }
16
+ export default function run(args: LintArgs): Promise<void>;
@@ -0,0 +1,9 @@
1
+ export interface PseudoArgs {
2
+ cwd?: string;
3
+ translationDir?: string;
4
+ sourceLocale?: string;
5
+ targetLocale: string;
6
+ replace?: boolean;
7
+ }
8
+ export declare function pseudoLocalize(text: string): string;
9
+ export default function run(args: PseudoArgs): Promise<void>;