@quaso-i18n/cli 1.0.0-rc.2 → 1.0.0-rc.2.main.g0b25541
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/package.json +1 -1
- package/quaso.mjs +151 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quaso-i18n/cli",
|
|
3
|
-
"version": "1.0.0-rc.2",
|
|
3
|
+
"version": "1.0.0-rc.2.main.g0b25541",
|
|
4
4
|
"description": "The command line of Quaso, localization for i18next JSON files: upload, download, status and import, from a laptop, from CI and for AI agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/quaso.mjs
CHANGED
|
@@ -1073,6 +1073,16 @@ var CHECKS = {
|
|
|
1073
1073
|
identical: "warning",
|
|
1074
1074
|
/** The numbers differ from the English. */
|
|
1075
1075
|
numbers_differ: "warning",
|
|
1076
|
+
/** An HTML or i18next Trans tag, such as `<b>` or `<1>`, is missing or extra. */
|
|
1077
|
+
tags_differ: "warning",
|
|
1078
|
+
/** Spaces or line breaks at the start or the end differ from the English. */
|
|
1079
|
+
edge_whitespace: "warning",
|
|
1080
|
+
/** The number of line breaks inside the text differs from the English. */
|
|
1081
|
+
line_breaks_differ: "warning",
|
|
1082
|
+
/** Two spaces in a row, which the English doesn't have. */
|
|
1083
|
+
double_space: "warning",
|
|
1084
|
+
/** Ends with a different question mark, exclamation mark, ellipsis or colon. */
|
|
1085
|
+
end_punctuation: "warning",
|
|
1076
1086
|
/** A glossary translation or a term marked never translate is missing. */
|
|
1077
1087
|
glossary: "warning"
|
|
1078
1088
|
};
|
|
@@ -1208,7 +1218,11 @@ var QuasoConfig = s.object({
|
|
|
1208
1218
|
minItems: 1
|
|
1209
1219
|
}),
|
|
1210
1220
|
limits: s.array(LengthLimit).optional().describe("Maximum lengths, such as for app store fields (FMT-4). They win over limits set on the website."),
|
|
1211
|
-
pluralExclusions: s.array(PluralExclusion).optional()
|
|
1221
|
+
pluralExclusions: s.array(PluralExclusion).optional(),
|
|
1222
|
+
untranslated: s.enum([
|
|
1223
|
+
"source",
|
|
1224
|
+
"omit"
|
|
1225
|
+
]).optional().describe("What download writes for untranslated strings: the source text (default) or nothing, for apps that fall back to the source language themselves")
|
|
1212
1226
|
});
|
|
1213
1227
|
|
|
1214
1228
|
// packages/core/src/api.ts
|
|
@@ -1310,7 +1324,11 @@ var FileVersionQuery = s.object({
|
|
|
1310
1324
|
var ExportQuery = s.object({
|
|
1311
1325
|
languages: s.array(LanguageTag).optional(),
|
|
1312
1326
|
files: s.array(FilePath).optional(),
|
|
1313
|
-
at: PublicationTime.optional()
|
|
1327
|
+
at: PublicationTime.optional(),
|
|
1328
|
+
untranslated: s.enum([
|
|
1329
|
+
"source",
|
|
1330
|
+
"omit"
|
|
1331
|
+
]).optional().describe("Untranslated entries as the English (default) or left out")
|
|
1314
1332
|
});
|
|
1315
1333
|
var ImportRequest = s.object({
|
|
1316
1334
|
language: LanguageTag,
|
|
@@ -1323,6 +1341,7 @@ var ImportRequest = s.object({
|
|
|
1323
1341
|
]),
|
|
1324
1342
|
overwrite: s.boolean().optional().describe("Also replace blue (proofread) translations"),
|
|
1325
1343
|
keepIdentical: s.boolean().optional().describe("Keep values identical to the English"),
|
|
1344
|
+
allowQaErrors: s.boolean().optional().describe("Import values that fail the quality checks, flagged, instead of refusing them"),
|
|
1326
1345
|
dryRun: s.boolean().optional()
|
|
1327
1346
|
});
|
|
1328
1347
|
var JobScope = s.object({
|
|
@@ -1332,6 +1351,7 @@ var JobScope = s.object({
|
|
|
1332
1351
|
maxItems: 5e3
|
|
1333
1352
|
}).optional().describe("Only these strings"),
|
|
1334
1353
|
retranslate: s.boolean().optional().describe("Also re-translate green strings (never blue ones, LLM-4)"),
|
|
1354
|
+
qa: s.boolean().optional().describe("Also re-translate the green translations that fail the quality checks"),
|
|
1335
1355
|
outdated: s.boolean().optional().describe("Update outdated green translations and propose updates for outdated blue ones (default true)"),
|
|
1336
1356
|
instruction: s.string({
|
|
1337
1357
|
maxLength: 4e3
|
|
@@ -1602,7 +1622,31 @@ var ProjectSettings = s.object({
|
|
|
1602
1622
|
suffix: s.string({
|
|
1603
1623
|
minLength: 1,
|
|
1604
1624
|
maxLength: 10
|
|
1605
|
-
})
|
|
1625
|
+
}),
|
|
1626
|
+
extra: s.array(s.object({
|
|
1627
|
+
prefix: s.string({
|
|
1628
|
+
minLength: 1,
|
|
1629
|
+
maxLength: 10
|
|
1630
|
+
}),
|
|
1631
|
+
suffix: s.string({
|
|
1632
|
+
minLength: 1,
|
|
1633
|
+
maxLength: 10
|
|
1634
|
+
})
|
|
1635
|
+
}), {
|
|
1636
|
+
maxItems: 10
|
|
1637
|
+
}).optional().describe("Delimiters of placeholders the app fills in itself, such as { and } for {name}"),
|
|
1638
|
+
optional: s.array(s.object({
|
|
1639
|
+
placeholder: s.string({
|
|
1640
|
+
minLength: 1,
|
|
1641
|
+
maxLength: 100
|
|
1642
|
+
}),
|
|
1643
|
+
languages: s.array(LanguageTag, {
|
|
1644
|
+
minItems: 1,
|
|
1645
|
+
maxItems: 200
|
|
1646
|
+
})
|
|
1647
|
+
}), {
|
|
1648
|
+
maxItems: 50
|
|
1649
|
+
}).optional().describe("Placeholders some languages may leave out, such as {article}")
|
|
1606
1650
|
}),
|
|
1607
1651
|
logoUrl: s.string({
|
|
1608
1652
|
maxLength: 2e3
|
|
@@ -1767,7 +1811,7 @@ var ReviewLanguageRequestRequest = s.object({
|
|
|
1767
1811
|
// package.json
|
|
1768
1812
|
var package_default = {
|
|
1769
1813
|
name: "quaso-workspace",
|
|
1770
|
-
version: "1.0.0-rc.2",
|
|
1814
|
+
version: "1.0.0-rc.2.main.g0b25541",
|
|
1771
1815
|
private: true,
|
|
1772
1816
|
type: "module",
|
|
1773
1817
|
license: "MIT",
|
|
@@ -2431,6 +2475,8 @@ var LONG_TIMEOUT_MS = 5 * 6e4;
|
|
|
2431
2475
|
var MAX_RETRIES = 3;
|
|
2432
2476
|
var BASE_DELAY_MS = 500;
|
|
2433
2477
|
var MAX_RETRY_AFTER_MS = 6e4;
|
|
2478
|
+
var WAKE_POLL_MS = 2e3;
|
|
2479
|
+
var MAX_WAKE_MS = 3 * 6e4;
|
|
2434
2480
|
var ApiClient = class {
|
|
2435
2481
|
baseUrl;
|
|
2436
2482
|
#apiKey;
|
|
@@ -2439,6 +2485,7 @@ var ApiClient = class {
|
|
|
2439
2485
|
#random;
|
|
2440
2486
|
#now;
|
|
2441
2487
|
#log;
|
|
2488
|
+
#awake;
|
|
2442
2489
|
constructor(options) {
|
|
2443
2490
|
this.baseUrl = options.baseUrl;
|
|
2444
2491
|
this.#apiKey = options.apiKey;
|
|
@@ -2448,6 +2495,7 @@ var ApiClient = class {
|
|
|
2448
2495
|
this.#now = options.now ?? Date.now;
|
|
2449
2496
|
this.#log = options.log ?? (() => {
|
|
2450
2497
|
});
|
|
2498
|
+
this.#awake = options.waitWhileAsleep ? null : Promise.resolve();
|
|
2451
2499
|
}
|
|
2452
2500
|
get(path, options = {}) {
|
|
2453
2501
|
return this.request("GET", path, {
|
|
@@ -2472,12 +2520,61 @@ var ApiClient = class {
|
|
|
2472
2520
|
}
|
|
2473
2521
|
return url.toString();
|
|
2474
2522
|
}
|
|
2523
|
+
/**
|
|
2524
|
+
* Asks `/wake` until the server is up, saying so every 15 seconds. Any other answer than
|
|
2525
|
+
* a start in progress (a server without `/wake`, an error) ends the wait: the requests
|
|
2526
|
+
* that follow report their own problems.
|
|
2527
|
+
*/
|
|
2528
|
+
async #waitWhileAsleep() {
|
|
2529
|
+
const started = this.#now();
|
|
2530
|
+
let reported = -Infinity;
|
|
2531
|
+
while (this.#now() - started < MAX_WAKE_MS) {
|
|
2532
|
+
const start = await this.#askWake();
|
|
2533
|
+
if (start === null) return;
|
|
2534
|
+
if (this.#now() - reported >= 15e3) {
|
|
2535
|
+
const elapsed = Math.round(start.elapsedMs / 1e3);
|
|
2536
|
+
const expected = Math.round(start.expectedMs / 1e3);
|
|
2537
|
+
this.#log(`Waking up the instance: ${elapsed} s of about ${expected} s.`);
|
|
2538
|
+
reported = this.#now();
|
|
2539
|
+
}
|
|
2540
|
+
await this.#sleep(WAKE_POLL_MS);
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
/** The start in progress, or null when the server is up or `/wake` says nothing. */
|
|
2544
|
+
async #askWake() {
|
|
2545
|
+
try {
|
|
2546
|
+
const response = await this.#fetch(`${this.baseUrl}/wake`, {
|
|
2547
|
+
headers: {
|
|
2548
|
+
Accept: "application/json",
|
|
2549
|
+
"User-Agent": USER_AGENT
|
|
2550
|
+
},
|
|
2551
|
+
redirect: "manual",
|
|
2552
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
2553
|
+
});
|
|
2554
|
+
const json2 = response.headers.get("Content-Type")?.includes("application/json");
|
|
2555
|
+
if (!response.ok || !json2) {
|
|
2556
|
+
await response.body?.cancel().catch(() => {
|
|
2557
|
+
});
|
|
2558
|
+
return null;
|
|
2559
|
+
}
|
|
2560
|
+
const answer = await response.json();
|
|
2561
|
+
const starting = answer?.state === "starting" && typeof answer.elapsedMs === "number" && typeof answer.expectedMs === "number";
|
|
2562
|
+
return starting ? {
|
|
2563
|
+
elapsedMs: answer.elapsedMs,
|
|
2564
|
+
expectedMs: answer.expectedMs
|
|
2565
|
+
} : null;
|
|
2566
|
+
} catch {
|
|
2567
|
+
return null;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2475
2570
|
async request(method, path, options) {
|
|
2476
2571
|
const url = this.url(path, options.query);
|
|
2477
2572
|
const policy = options.retry ?? (method === "GET" ? "safe" : "once");
|
|
2478
2573
|
const timeoutMs = options.timeoutMs ?? TIMEOUT_MS;
|
|
2479
2574
|
const headers = requestHeaders(this.#apiKey, options.body !== void 0);
|
|
2480
2575
|
const body = options.body === void 0 ? void 0 : JSON.stringify(options.body);
|
|
2576
|
+
this.#awake ??= this.#waitWhileAsleep();
|
|
2577
|
+
await this.#awake;
|
|
2481
2578
|
for (let attempt = 0; ; attempt++) {
|
|
2482
2579
|
let response;
|
|
2483
2580
|
let text;
|
|
@@ -2610,9 +2707,9 @@ function hintFor(code, status2) {
|
|
|
2610
2707
|
case "payload_too_large":
|
|
2611
2708
|
return "Send fewer files at a time with --file.";
|
|
2612
2709
|
case "llm_unavailable":
|
|
2613
|
-
return "
|
|
2710
|
+
return "An administrator enters the Gemini API key in the instance's Settings \u2192 LLM translation.";
|
|
2614
2711
|
case "budget_exceeded":
|
|
2615
|
-
return "LLM translation resumes next month, or when the
|
|
2712
|
+
return "LLM translation resumes next month, or when an administrator raises the monthly token budget in Settings \u2192 LLM translation.";
|
|
2616
2713
|
}
|
|
2617
2714
|
return status2 >= 500 ? "Try again in a moment; if it persists, tell the instance's operator." : void 0;
|
|
2618
2715
|
}
|
|
@@ -3273,6 +3370,7 @@ var Context = class {
|
|
|
3273
3370
|
out;
|
|
3274
3371
|
#fetch;
|
|
3275
3372
|
#sleep;
|
|
3373
|
+
#waitWhileAsleep;
|
|
3276
3374
|
#project;
|
|
3277
3375
|
#sources;
|
|
3278
3376
|
constructor(options) {
|
|
@@ -3282,6 +3380,7 @@ var Context = class {
|
|
|
3282
3380
|
this.out = options.out;
|
|
3283
3381
|
this.#fetch = options.fetch;
|
|
3284
3382
|
this.#sleep = options.sleep;
|
|
3383
|
+
this.#waitWhileAsleep = options.waitWhileAsleep ?? true;
|
|
3285
3384
|
}
|
|
3286
3385
|
/** The project: `--config`, or the nearest `quaso.config.json`. */
|
|
3287
3386
|
project() {
|
|
@@ -3311,7 +3410,8 @@ var Context = class {
|
|
|
3311
3410
|
apiKey: apiKey(this.env),
|
|
3312
3411
|
fetch: this.#fetch,
|
|
3313
3412
|
sleep: this.#sleep,
|
|
3314
|
-
log: (message) => this.out.info(message)
|
|
3413
|
+
log: (message) => this.out.info(message),
|
|
3414
|
+
waitWhileAsleep: this.#waitWhileAsleep
|
|
3315
3415
|
});
|
|
3316
3416
|
}
|
|
3317
3417
|
};
|
|
@@ -7608,7 +7708,8 @@ var download = {
|
|
|
7608
7708
|
query: {
|
|
7609
7709
|
languages,
|
|
7610
7710
|
files: askFiles ? wanted : void 0,
|
|
7611
|
-
at
|
|
7711
|
+
at,
|
|
7712
|
+
untranslated: project.config.untranslated
|
|
7612
7713
|
},
|
|
7613
7714
|
timeoutMs: LONG_TIMEOUT_MS
|
|
7614
7715
|
});
|
|
@@ -8006,6 +8107,11 @@ var importCommand = {
|
|
|
8006
8107
|
type: "boolean",
|
|
8007
8108
|
description: "Keep values identical to the English instead of skipping them"
|
|
8008
8109
|
},
|
|
8110
|
+
{
|
|
8111
|
+
name: "allow-qa-errors",
|
|
8112
|
+
type: "boolean",
|
|
8113
|
+
description: "Import values that fail the quality checks, with their QA errors, instead of refusing them"
|
|
8114
|
+
},
|
|
8009
8115
|
{
|
|
8010
8116
|
name: "dry-run",
|
|
8011
8117
|
type: "boolean",
|
|
@@ -8062,6 +8168,7 @@ var importCommand = {
|
|
|
8062
8168
|
languages: [],
|
|
8063
8169
|
withoutFiles: [],
|
|
8064
8170
|
refused: [],
|
|
8171
|
+
flagged: [],
|
|
8065
8172
|
unknownKeys: [],
|
|
8066
8173
|
invalid: [],
|
|
8067
8174
|
failed: []
|
|
@@ -8108,6 +8215,7 @@ var importCommand = {
|
|
|
8108
8215
|
};
|
|
8109
8216
|
if (flag(ctx.args, "overwrite")) request.overwrite = true;
|
|
8110
8217
|
if (flag(ctx.args, "keep-identical")) request.keepIdentical = true;
|
|
8218
|
+
if (flag(ctx.args, "allow-qa-errors")) request.allowQaErrors = true;
|
|
8111
8219
|
if (dryRun) request.dryRun = true;
|
|
8112
8220
|
let result;
|
|
8113
8221
|
try {
|
|
@@ -8149,6 +8257,17 @@ var importCommand = {
|
|
|
8149
8257
|
});
|
|
8150
8258
|
}
|
|
8151
8259
|
}
|
|
8260
|
+
for (const flagged of result.flagged ?? []) {
|
|
8261
|
+
for (const check of errorChecks(flagged.checks)) {
|
|
8262
|
+
summary.flagged.push({
|
|
8263
|
+
file: pathOf(flagged.file),
|
|
8264
|
+
key: flagged.key,
|
|
8265
|
+
language: flagged.language,
|
|
8266
|
+
check: check.check,
|
|
8267
|
+
message: checkMessage(check)
|
|
8268
|
+
});
|
|
8269
|
+
}
|
|
8270
|
+
}
|
|
8152
8271
|
for (const unknown of result.unknownKeys) {
|
|
8153
8272
|
summary.unknownKeys.push({
|
|
8154
8273
|
file: pathOf(unknown.file),
|
|
@@ -8220,8 +8339,14 @@ function renderImport(out, summary) {
|
|
|
8220
8339
|
if (result.skippedIdentical > 0) {
|
|
8221
8340
|
parts.push(`${result.skippedIdentical} identical to the English (skipped)`);
|
|
8222
8341
|
}
|
|
8342
|
+
if (result.skippedEmpty > 0) parts.push(`${result.skippedEmpty} empty (skipped)`);
|
|
8343
|
+
if (result.droppedForms > 0) {
|
|
8344
|
+
parts.push(`${result.droppedForms} unused plural forms dropped`);
|
|
8345
|
+
}
|
|
8223
8346
|
if (result.skippedBlue > 0) parts.push(`${result.skippedBlue} proofread kept`);
|
|
8224
8347
|
if (result.refused.length > 0) parts.push(red(`${result.refused.length} refused`));
|
|
8348
|
+
const flagged = result.flagged ?? [];
|
|
8349
|
+
if (flagged.length > 0) parts.push(yellow(`${flagged.length} with QA errors`));
|
|
8225
8350
|
if (result.unknownKeys.length > 0) parts.push(`${result.unknownKeys.length} unknown keys`);
|
|
8226
8351
|
out.print(`${bold(result.language.padEnd(6))} ${parts.join(", ")}`);
|
|
8227
8352
|
for (const file of result.unknownFiles) {
|
|
@@ -8246,6 +8371,7 @@ function renderImport(out, summary) {
|
|
|
8246
8371
|
out.print(dim("The other languages were imported; import these again with --language."));
|
|
8247
8372
|
}
|
|
8248
8373
|
section("Refused by the quality checks", summary.refused);
|
|
8374
|
+
section("Imported with QA errors (fix them on the instance)", summary.flagged);
|
|
8249
8375
|
section("Keys the English doesn't have", summary.unknownKeys);
|
|
8250
8376
|
section("Files that can't be read (fix them and import again)", summary.invalid);
|
|
8251
8377
|
if (summary.refused.length > 0 && !summary.dryRun) {
|
|
@@ -8279,6 +8405,11 @@ var translate = {
|
|
|
8279
8405
|
type: "boolean",
|
|
8280
8406
|
description: "Also translate green (not proofread) strings again; blue ones never change"
|
|
8281
8407
|
},
|
|
8408
|
+
{
|
|
8409
|
+
name: "qa",
|
|
8410
|
+
type: "boolean",
|
|
8411
|
+
description: "Also translate again the green strings that fail the quality checks, such as imported ones"
|
|
8412
|
+
},
|
|
8282
8413
|
{
|
|
8283
8414
|
name: "instruction",
|
|
8284
8415
|
type: "string",
|
|
@@ -8341,6 +8472,7 @@ var translate = {
|
|
|
8341
8472
|
};
|
|
8342
8473
|
if (files) request.files = files;
|
|
8343
8474
|
if (flag(ctx.args, "retranslate")) request.retranslate = true;
|
|
8475
|
+
if (flag(ctx.args, "qa")) request.qa = true;
|
|
8344
8476
|
const instruction = option(ctx.args, "instruction");
|
|
8345
8477
|
if (instruction !== void 0 && instruction.trim() !== "") request.instruction = instruction;
|
|
8346
8478
|
const model = option(ctx.args, "model");
|
|
@@ -8427,8 +8559,17 @@ function renderEstimate(out, estimate) {
|
|
|
8427
8559
|
return;
|
|
8428
8560
|
}
|
|
8429
8561
|
out.print(`${count(estimate.strings, "string")} (${count(estimate.words, "word")}) in ${count(estimate.requests, "request")}: about ${estimate.estimatedTokens.input.toLocaleString("en")} input and ${estimate.estimatedTokens.output.toLocaleString("en")} output tokens.`);
|
|
8562
|
+
out.print(workSummary(estimate.work));
|
|
8430
8563
|
out.print(dim("Thinking models use more tokens than this; retries add some too."));
|
|
8431
8564
|
}
|
|
8565
|
+
function workSummary(work) {
|
|
8566
|
+
const parts = [];
|
|
8567
|
+
if (work.translate > 0) parts.push(`${work.translate} untranslated`);
|
|
8568
|
+
if (work.retranslate > 0) parts.push(`${work.retranslate} green to translate again`);
|
|
8569
|
+
if (work.update > 0) parts.push(`${work.update} outdated green to update`);
|
|
8570
|
+
if (work.propose > 0) parts.push(`${work.propose} outdated proofread to get a proposal for review`);
|
|
8571
|
+
return `Of these: ${parts.join(", ")}.`;
|
|
8572
|
+
}
|
|
8432
8573
|
function renderQueued(out, job) {
|
|
8433
8574
|
if (job.status === "done") {
|
|
8434
8575
|
out.print("Nothing to translate: every string in scope is translated.");
|
|
@@ -8505,7 +8646,8 @@ async function run(argv, options = {}) {
|
|
|
8505
8646
|
env,
|
|
8506
8647
|
out,
|
|
8507
8648
|
fetch: options.fetch,
|
|
8508
|
-
sleep: options.sleep
|
|
8649
|
+
sleep: options.sleep,
|
|
8650
|
+
waitWhileAsleep: options.waitWhileAsleep
|
|
8509
8651
|
});
|
|
8510
8652
|
const result = await command.run(ctx);
|
|
8511
8653
|
if (json2) out.document(resultDocument(name, result.exitCode, result.json));
|