@verbatra/sdk 0.6.2 → 0.6.3
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 +27 -13
- package/dist/index.cjs +77 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -10
- package/dist/index.d.ts +63 -10
- package/dist/index.js +77 -30
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.d.cts
CHANGED
|
@@ -972,7 +972,7 @@ interface DiffDeps {
|
|
|
972
972
|
declare function diff(input: DiffInput, deps?: DiffDeps): Promise<DiffSummary>;
|
|
973
973
|
|
|
974
974
|
/** Why {@link gateCandidateValue} rejected a candidate value. */
|
|
975
|
-
type IntegrityGateReason = "placeholder" | "icu" | "degenerate";
|
|
975
|
+
type IntegrityGateReason = "placeholder" | "icu" | "degenerate" | "empty";
|
|
976
976
|
|
|
977
977
|
/** Input for {@link editEntry}: the validated config, exactly one target locale/key pair, and the human-typed replacement value. */
|
|
978
978
|
interface EditEntryInput {
|
|
@@ -1348,7 +1348,7 @@ type RetranslateEntryResult = {
|
|
|
1348
1348
|
declare function retranslateEntry(input: RetranslateEntryInput, deps?: RetranslateEntryDeps): Promise<RetranslateEntryResult>;
|
|
1349
1349
|
|
|
1350
1350
|
/** A stable code for an SDK-originated notice (not a provider notice). */
|
|
1351
|
-
type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED" | "BUDGET_TOKENS_EXCEEDED";
|
|
1351
|
+
type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED" | "BUDGET_TOKENS_EXCEEDED" | "CACHE_VERSION_UNRECOGNIZED";
|
|
1352
1352
|
/** Summed token usage across every provider call in a scope (one locale, or the whole run). */
|
|
1353
1353
|
interface UsageSummary {
|
|
1354
1354
|
readonly inputTokens: number;
|
|
@@ -1432,7 +1432,27 @@ interface LocaleSummary {
|
|
|
1432
1432
|
* {@link LocaleSummary.malformedRows}, and {@link LocaleSummary.duplicateKeys}, deliberately do NOT
|
|
1433
1433
|
* feed this status: they are surfaced in their own lists (and the CLI) but are not withholdings of
|
|
1434
1434
|
* an attempted translation, so a locale that dropped a malformed row, collapsed a duplicate key, or
|
|
1435
|
-
* left a
|
|
1435
|
+
* left a row unfilled while accepting its other rows can still be `"succeeded"`.
|
|
1436
|
+
*
|
|
1437
|
+
* This is a settled decision, not an omission. Recording the reasoning here so the question stops
|
|
1438
|
+
* recurring:
|
|
1439
|
+
*
|
|
1440
|
+
* 1. Two questions, two commands. `import`'s exit code answers "did the import apply what the
|
|
1441
|
+
* translator gave me". `check` and `diff` answer "is this project fully translated", and both
|
|
1442
|
+
* already exit non-zero or report pending in every one of these cases. Folding pending human
|
|
1443
|
+
* work into the import exit code duplicates an existing gate rather than adding one.
|
|
1444
|
+
* 2. It would break the staged handoff. With `de` and `fr` configured and only `de` filled in,
|
|
1445
|
+
* failing on unfilled work makes `fr` fail and takes the whole import with it, even though `de`
|
|
1446
|
+
* imported perfectly. Locale-at-a-time handoff is this feature's most common workflow.
|
|
1447
|
+
* 3. {@link LocaleSummary.malformedRows} structurally cannot signal lost work. The Status column is
|
|
1448
|
+
* the only reachable rejection in the workbook reader; every other field is an unconstrained
|
|
1449
|
+
* string or a tolerant parse. So a row is malformed on its Status cell alone, entirely
|
|
1450
|
+
* independently of whether its Translation cell was filled. An untouched sheet with a mangled
|
|
1451
|
+
* Status column (spreadsheet autocorrect, a CAT-tool round trip) is all-malformed with zero lost
|
|
1452
|
+
* work. A bucket that cannot distinguish dropped work from absent work cannot honestly drive a
|
|
1453
|
+
* failure status.
|
|
1454
|
+
* 4. {@link LocaleSummary.duplicateKeys} never could: the first occurrence wins and the intent is
|
|
1455
|
+
* honoured.
|
|
1436
1456
|
*/
|
|
1437
1457
|
readonly status: "succeeded" | "partial" | "failed";
|
|
1438
1458
|
/**
|
|
@@ -1513,11 +1533,16 @@ interface LocaleSummary {
|
|
|
1513
1533
|
*/
|
|
1514
1534
|
readonly needsReview: readonly NeedsReviewEntry[];
|
|
1515
1535
|
/**
|
|
1516
|
-
* Keys the
|
|
1517
|
-
*
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
1520
|
-
*
|
|
1536
|
+
* Keys the translator left blank that still need a translation, drifted or not, sorted by key.
|
|
1537
|
+
* Pending work made visible: the row is skipped (nothing is written and the prior lock baseline is
|
|
1538
|
+
* kept), but the key is surfaced so an unfinished import is never silently reported as done. Only a
|
|
1539
|
+
* workbook import populates this; the provider path, which never leaves a candidate key unfilled by
|
|
1540
|
+
* a person, always leaves it empty.
|
|
1541
|
+
*
|
|
1542
|
+
* Membership is decided by the import-time diff, not by the `status` the row was exported with: a
|
|
1543
|
+
* never-translated key exports as `new`, and a first handoff is all-new, which is the most common
|
|
1544
|
+
* unfilled case there is. A row exported as `changed` whose key no longer needs work by import time
|
|
1545
|
+
* is correspondingly excluded.
|
|
1521
1546
|
*/
|
|
1522
1547
|
readonly unfilled: readonly string[];
|
|
1523
1548
|
/**
|
|
@@ -1697,6 +1722,19 @@ interface LocaleFinishedEvent {
|
|
|
1697
1722
|
readonly locale: string;
|
|
1698
1723
|
/** Number of keys accepted (translated) for this locale. In a dry-run, the keys that would be translated. */
|
|
1699
1724
|
readonly translated: number;
|
|
1725
|
+
/**
|
|
1726
|
+
* Zero-based index of this locale in the run's target-locale order, carrying the same value as the
|
|
1727
|
+
* matching {@link LocaleStartedEvent}. This is a correlation key, so a consumer can pair a finish
|
|
1728
|
+
* with its start without matching on the locale name.
|
|
1729
|
+
*
|
|
1730
|
+
* It is deliberately NOT a progress counter. The worker pool claims indices up front, so this is a
|
|
1731
|
+
* claim ordinal rather than a completion ordinal: under concurrency the locales finish in a
|
|
1732
|
+
* different order, and rendering `localeIndex + 1` of `totalLocales` on a finish produces a
|
|
1733
|
+
* counter that jumps around and never reaches its own total in order. Do not reintroduce one.
|
|
1734
|
+
*/
|
|
1735
|
+
readonly localeIndex: number;
|
|
1736
|
+
/** Total number of target locales in this run, mirroring {@link LocaleStartedEvent}. */
|
|
1737
|
+
readonly totalLocales: number;
|
|
1700
1738
|
}
|
|
1701
1739
|
/** The run reached the end of its locale loop: how many locales it processed. */
|
|
1702
1740
|
interface RunFinishedEvent {
|
|
@@ -2060,8 +2098,9 @@ interface WatchInput {
|
|
|
2060
2098
|
readonly lockAcquireTimeoutMs?: number;
|
|
2061
2099
|
/**
|
|
2062
2100
|
* Passed through to every run's {@link TranslateInput.concurrency}: how many locales each cycle may
|
|
2063
|
-
* run at once. Defaults to 1 (strictly serial).
|
|
2064
|
-
*
|
|
2101
|
+
* run at once. Defaults to 1 (strictly serial). Because the per-cycle budget tracker is fresh, a
|
|
2102
|
+
* value greater than 1 cannot be honored alongside a `maxTokens` budget; {@link watch} refuses that
|
|
2103
|
+
* combination at startup rather than letting every cycle fail on it.
|
|
2065
2104
|
*/
|
|
2066
2105
|
readonly concurrency?: number;
|
|
2067
2106
|
/**
|
|
@@ -2095,9 +2134,23 @@ interface WatchController {
|
|
|
2095
2134
|
* `onRun` as a {@link WatchRunResult} and watching continues. Returns a controller whose `stop()`
|
|
2096
2135
|
* closes the watcher and awaits the in-flight run.
|
|
2097
2136
|
*
|
|
2137
|
+
* Both startup refusals are ordered deliberately: `concurrency` is resolved first because it is
|
|
2138
|
+
* decidable from the arguments alone, while the missing-source check is an I/O probe of project
|
|
2139
|
+
* state, so argument validation runs before any I/O and before the watcher exists. The concurrency
|
|
2140
|
+
* resolve passes `dryRun: false` unconditionally, which is exact rather than a simplification: there
|
|
2141
|
+
* is no dry-run watch, so the budget conflict always applies to a session.
|
|
2142
|
+
*
|
|
2143
|
+
* Resolving it here rather than leaving it to each cycle is the point, not an optimization: a watch
|
|
2144
|
+
* session is long-lived, so a per-cycle refusal would fail the initial run and every run after it,
|
|
2145
|
+
* indefinitely, while the session stayed alive and looked healthy.
|
|
2146
|
+
*
|
|
2098
2147
|
* @param input - The config, optional cwd/debounce, and the `onRun` callback that receives each result.
|
|
2099
2148
|
* @param deps - Optional composition seams (watcher, run, registry, provider builder, file system) for tests.
|
|
2100
2149
|
* @returns A {@link WatchController}; call `stop()` to close the watcher and await the in-flight run.
|
|
2150
|
+
* @throws {@link SdkError} `CONCURRENCY_INVALID`: at startup only, when `concurrency` is defined but
|
|
2151
|
+
* not an integer of at least 1.
|
|
2152
|
+
* @throws {@link SdkError} `CONCURRENCY_BUDGET_CONFLICT`: at startup only, when `concurrency` is
|
|
2153
|
+
* greater than 1 while the config sets a `maxTokens` budget.
|
|
2101
2154
|
* @throws {@link SdkError} `SOURCE_UNREADABLE`: at startup only, when the source locale file is absent.
|
|
2102
2155
|
* @example
|
|
2103
2156
|
* ```ts
|
package/dist/index.d.ts
CHANGED
|
@@ -972,7 +972,7 @@ interface DiffDeps {
|
|
|
972
972
|
declare function diff(input: DiffInput, deps?: DiffDeps): Promise<DiffSummary>;
|
|
973
973
|
|
|
974
974
|
/** Why {@link gateCandidateValue} rejected a candidate value. */
|
|
975
|
-
type IntegrityGateReason = "placeholder" | "icu" | "degenerate";
|
|
975
|
+
type IntegrityGateReason = "placeholder" | "icu" | "degenerate" | "empty";
|
|
976
976
|
|
|
977
977
|
/** Input for {@link editEntry}: the validated config, exactly one target locale/key pair, and the human-typed replacement value. */
|
|
978
978
|
interface EditEntryInput {
|
|
@@ -1348,7 +1348,7 @@ type RetranslateEntryResult = {
|
|
|
1348
1348
|
declare function retranslateEntry(input: RetranslateEntryInput, deps?: RetranslateEntryDeps): Promise<RetranslateEntryResult>;
|
|
1349
1349
|
|
|
1350
1350
|
/** A stable code for an SDK-originated notice (not a provider notice). */
|
|
1351
|
-
type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED" | "BUDGET_TOKENS_EXCEEDED";
|
|
1351
|
+
type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED" | "BUDGET_TOKENS_EXCEEDED" | "CACHE_VERSION_UNRECOGNIZED";
|
|
1352
1352
|
/** Summed token usage across every provider call in a scope (one locale, or the whole run). */
|
|
1353
1353
|
interface UsageSummary {
|
|
1354
1354
|
readonly inputTokens: number;
|
|
@@ -1432,7 +1432,27 @@ interface LocaleSummary {
|
|
|
1432
1432
|
* {@link LocaleSummary.malformedRows}, and {@link LocaleSummary.duplicateKeys}, deliberately do NOT
|
|
1433
1433
|
* feed this status: they are surfaced in their own lists (and the CLI) but are not withholdings of
|
|
1434
1434
|
* an attempted translation, so a locale that dropped a malformed row, collapsed a duplicate key, or
|
|
1435
|
-
* left a
|
|
1435
|
+
* left a row unfilled while accepting its other rows can still be `"succeeded"`.
|
|
1436
|
+
*
|
|
1437
|
+
* This is a settled decision, not an omission. Recording the reasoning here so the question stops
|
|
1438
|
+
* recurring:
|
|
1439
|
+
*
|
|
1440
|
+
* 1. Two questions, two commands. `import`'s exit code answers "did the import apply what the
|
|
1441
|
+
* translator gave me". `check` and `diff` answer "is this project fully translated", and both
|
|
1442
|
+
* already exit non-zero or report pending in every one of these cases. Folding pending human
|
|
1443
|
+
* work into the import exit code duplicates an existing gate rather than adding one.
|
|
1444
|
+
* 2. It would break the staged handoff. With `de` and `fr` configured and only `de` filled in,
|
|
1445
|
+
* failing on unfilled work makes `fr` fail and takes the whole import with it, even though `de`
|
|
1446
|
+
* imported perfectly. Locale-at-a-time handoff is this feature's most common workflow.
|
|
1447
|
+
* 3. {@link LocaleSummary.malformedRows} structurally cannot signal lost work. The Status column is
|
|
1448
|
+
* the only reachable rejection in the workbook reader; every other field is an unconstrained
|
|
1449
|
+
* string or a tolerant parse. So a row is malformed on its Status cell alone, entirely
|
|
1450
|
+
* independently of whether its Translation cell was filled. An untouched sheet with a mangled
|
|
1451
|
+
* Status column (spreadsheet autocorrect, a CAT-tool round trip) is all-malformed with zero lost
|
|
1452
|
+
* work. A bucket that cannot distinguish dropped work from absent work cannot honestly drive a
|
|
1453
|
+
* failure status.
|
|
1454
|
+
* 4. {@link LocaleSummary.duplicateKeys} never could: the first occurrence wins and the intent is
|
|
1455
|
+
* honoured.
|
|
1436
1456
|
*/
|
|
1437
1457
|
readonly status: "succeeded" | "partial" | "failed";
|
|
1438
1458
|
/**
|
|
@@ -1513,11 +1533,16 @@ interface LocaleSummary {
|
|
|
1513
1533
|
*/
|
|
1514
1534
|
readonly needsReview: readonly NeedsReviewEntry[];
|
|
1515
1535
|
/**
|
|
1516
|
-
* Keys the
|
|
1517
|
-
*
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
1520
|
-
*
|
|
1536
|
+
* Keys the translator left blank that still need a translation, drifted or not, sorted by key.
|
|
1537
|
+
* Pending work made visible: the row is skipped (nothing is written and the prior lock baseline is
|
|
1538
|
+
* kept), but the key is surfaced so an unfinished import is never silently reported as done. Only a
|
|
1539
|
+
* workbook import populates this; the provider path, which never leaves a candidate key unfilled by
|
|
1540
|
+
* a person, always leaves it empty.
|
|
1541
|
+
*
|
|
1542
|
+
* Membership is decided by the import-time diff, not by the `status` the row was exported with: a
|
|
1543
|
+
* never-translated key exports as `new`, and a first handoff is all-new, which is the most common
|
|
1544
|
+
* unfilled case there is. A row exported as `changed` whose key no longer needs work by import time
|
|
1545
|
+
* is correspondingly excluded.
|
|
1521
1546
|
*/
|
|
1522
1547
|
readonly unfilled: readonly string[];
|
|
1523
1548
|
/**
|
|
@@ -1697,6 +1722,19 @@ interface LocaleFinishedEvent {
|
|
|
1697
1722
|
readonly locale: string;
|
|
1698
1723
|
/** Number of keys accepted (translated) for this locale. In a dry-run, the keys that would be translated. */
|
|
1699
1724
|
readonly translated: number;
|
|
1725
|
+
/**
|
|
1726
|
+
* Zero-based index of this locale in the run's target-locale order, carrying the same value as the
|
|
1727
|
+
* matching {@link LocaleStartedEvent}. This is a correlation key, so a consumer can pair a finish
|
|
1728
|
+
* with its start without matching on the locale name.
|
|
1729
|
+
*
|
|
1730
|
+
* It is deliberately NOT a progress counter. The worker pool claims indices up front, so this is a
|
|
1731
|
+
* claim ordinal rather than a completion ordinal: under concurrency the locales finish in a
|
|
1732
|
+
* different order, and rendering `localeIndex + 1` of `totalLocales` on a finish produces a
|
|
1733
|
+
* counter that jumps around and never reaches its own total in order. Do not reintroduce one.
|
|
1734
|
+
*/
|
|
1735
|
+
readonly localeIndex: number;
|
|
1736
|
+
/** Total number of target locales in this run, mirroring {@link LocaleStartedEvent}. */
|
|
1737
|
+
readonly totalLocales: number;
|
|
1700
1738
|
}
|
|
1701
1739
|
/** The run reached the end of its locale loop: how many locales it processed. */
|
|
1702
1740
|
interface RunFinishedEvent {
|
|
@@ -2060,8 +2098,9 @@ interface WatchInput {
|
|
|
2060
2098
|
readonly lockAcquireTimeoutMs?: number;
|
|
2061
2099
|
/**
|
|
2062
2100
|
* Passed through to every run's {@link TranslateInput.concurrency}: how many locales each cycle may
|
|
2063
|
-
* run at once. Defaults to 1 (strictly serial).
|
|
2064
|
-
*
|
|
2101
|
+
* run at once. Defaults to 1 (strictly serial). Because the per-cycle budget tracker is fresh, a
|
|
2102
|
+
* value greater than 1 cannot be honored alongside a `maxTokens` budget; {@link watch} refuses that
|
|
2103
|
+
* combination at startup rather than letting every cycle fail on it.
|
|
2065
2104
|
*/
|
|
2066
2105
|
readonly concurrency?: number;
|
|
2067
2106
|
/**
|
|
@@ -2095,9 +2134,23 @@ interface WatchController {
|
|
|
2095
2134
|
* `onRun` as a {@link WatchRunResult} and watching continues. Returns a controller whose `stop()`
|
|
2096
2135
|
* closes the watcher and awaits the in-flight run.
|
|
2097
2136
|
*
|
|
2137
|
+
* Both startup refusals are ordered deliberately: `concurrency` is resolved first because it is
|
|
2138
|
+
* decidable from the arguments alone, while the missing-source check is an I/O probe of project
|
|
2139
|
+
* state, so argument validation runs before any I/O and before the watcher exists. The concurrency
|
|
2140
|
+
* resolve passes `dryRun: false` unconditionally, which is exact rather than a simplification: there
|
|
2141
|
+
* is no dry-run watch, so the budget conflict always applies to a session.
|
|
2142
|
+
*
|
|
2143
|
+
* Resolving it here rather than leaving it to each cycle is the point, not an optimization: a watch
|
|
2144
|
+
* session is long-lived, so a per-cycle refusal would fail the initial run and every run after it,
|
|
2145
|
+
* indefinitely, while the session stayed alive and looked healthy.
|
|
2146
|
+
*
|
|
2098
2147
|
* @param input - The config, optional cwd/debounce, and the `onRun` callback that receives each result.
|
|
2099
2148
|
* @param deps - Optional composition seams (watcher, run, registry, provider builder, file system) for tests.
|
|
2100
2149
|
* @returns A {@link WatchController}; call `stop()` to close the watcher and await the in-flight run.
|
|
2150
|
+
* @throws {@link SdkError} `CONCURRENCY_INVALID`: at startup only, when `concurrency` is defined but
|
|
2151
|
+
* not an integer of at least 1.
|
|
2152
|
+
* @throws {@link SdkError} `CONCURRENCY_BUDGET_CONFLICT`: at startup only, when `concurrency` is
|
|
2153
|
+
* greater than 1 while the config sets a `maxTokens` budget.
|
|
2101
2154
|
* @throws {@link SdkError} `SOURCE_UNREADABLE`: at startup only, when the source locale file is absent.
|
|
2102
2155
|
* @example
|
|
2103
2156
|
* ```ts
|
package/dist/index.js
CHANGED
|
@@ -31,27 +31,31 @@ var translationMemorySchema = z.object({
|
|
|
31
31
|
function cacheFilePath(cwd) {
|
|
32
32
|
return resolve(cwd, CACHE_FILE_NAME);
|
|
33
33
|
}
|
|
34
|
+
var UNUSABLE = { memory: EMPTY_MEMORY, writable: true };
|
|
34
35
|
async function readTranslationMemory(path, fs) {
|
|
35
36
|
let read;
|
|
36
37
|
try {
|
|
37
38
|
read = await fs.readFileBounded(path, MAX_CACHE_FILE_BYTES);
|
|
38
39
|
} catch {
|
|
39
|
-
return
|
|
40
|
+
return UNUSABLE;
|
|
40
41
|
}
|
|
41
42
|
if (read.kind !== "ok") {
|
|
42
|
-
return
|
|
43
|
+
return UNUSABLE;
|
|
43
44
|
}
|
|
44
45
|
let parsed;
|
|
45
46
|
try {
|
|
46
47
|
parsed = JSON.parse(read.content);
|
|
47
48
|
} catch {
|
|
48
|
-
return
|
|
49
|
+
return UNUSABLE;
|
|
49
50
|
}
|
|
50
51
|
const result = translationMemorySchema.safeParse(parsed);
|
|
51
|
-
if (!result.success
|
|
52
|
-
return
|
|
52
|
+
if (!result.success) {
|
|
53
|
+
return UNUSABLE;
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
+
if (result.data.version !== CURRENT_VERSION) {
|
|
56
|
+
return { memory: EMPTY_MEMORY, writable: false };
|
|
57
|
+
}
|
|
58
|
+
return { memory: result.data, writable: true };
|
|
55
59
|
}
|
|
56
60
|
function lookupMemory(memory, fingerprint, locale, contentHash2) {
|
|
57
61
|
return memory.entries[fingerprint]?.[locale]?.[contentHash2];
|
|
@@ -103,7 +107,10 @@ async function feedTranslationMemory(cwd, fs, fingerprint, additionsByLocale) {
|
|
|
103
107
|
}
|
|
104
108
|
try {
|
|
105
109
|
const path = cacheFilePath(cwd);
|
|
106
|
-
const memory = await readTranslationMemory(path, fs);
|
|
110
|
+
const { memory, writable } = await readTranslationMemory(path, fs);
|
|
111
|
+
if (!writable) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
107
114
|
await writeTranslationMemory(path, applyAdditions(memory, fingerprint, additionsByLocale), fs);
|
|
108
115
|
} catch {
|
|
109
116
|
}
|
|
@@ -3309,6 +3316,12 @@ function extractPropertiesPlaceholders(value) {
|
|
|
3309
3316
|
var UNICODE_ESCAPE = /^[0-9a-fA-F]{4}$/;
|
|
3310
3317
|
var LEADING_WHITESPACE = /^[ \t\f]+/;
|
|
3311
3318
|
var TRAILING_TERMINATOR = /(?:\r\n|\r|\n)$/;
|
|
3319
|
+
function detectLineTerminator(content) {
|
|
3320
|
+
if (content.includes("\r\n")) {
|
|
3321
|
+
return "\r\n";
|
|
3322
|
+
}
|
|
3323
|
+
return content.includes("\r") ? "\r" : "\n";
|
|
3324
|
+
}
|
|
3312
3325
|
function isPropertiesWhitespace(char) {
|
|
3313
3326
|
return char === " " || char === " " || char === "\f";
|
|
3314
3327
|
}
|
|
@@ -3514,7 +3527,7 @@ async function readStructure(filePath) {
|
|
|
3514
3527
|
outcome = await readBounded2(filePath);
|
|
3515
3528
|
} catch (error) {
|
|
3516
3529
|
if (isFileNotFound(error)) {
|
|
3517
|
-
return [];
|
|
3530
|
+
return { items: [], terminator: "\n" };
|
|
3518
3531
|
}
|
|
3519
3532
|
throw new AdapterError("INVALID_STRUCTURE", "The destination file could not be read.");
|
|
3520
3533
|
}
|
|
@@ -3524,13 +3537,16 @@ async function readStructure(filePath) {
|
|
|
3524
3537
|
if (outcome.kind === "too-large") {
|
|
3525
3538
|
throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
|
|
3526
3539
|
}
|
|
3527
|
-
return
|
|
3540
|
+
return {
|
|
3541
|
+
items: parseItems(outcome.content),
|
|
3542
|
+
terminator: detectLineTerminator(outcome.content)
|
|
3543
|
+
};
|
|
3528
3544
|
}
|
|
3529
3545
|
async function serializePropertiesEntries(entries, filePath) {
|
|
3530
|
-
const
|
|
3546
|
+
const { items, terminator } = await readStructure(filePath);
|
|
3531
3547
|
const emitted = /* @__PURE__ */ new Set();
|
|
3532
3548
|
const lines = [];
|
|
3533
|
-
for (const item of
|
|
3549
|
+
for (const item of items) {
|
|
3534
3550
|
if (item.kind === "raw") {
|
|
3535
3551
|
lines.push(item.text);
|
|
3536
3552
|
continue;
|
|
@@ -3546,8 +3562,7 @@ async function serializePropertiesEntries(entries, filePath) {
|
|
|
3546
3562
|
lines.push(formatEntry(key, entry.value));
|
|
3547
3563
|
}
|
|
3548
3564
|
}
|
|
3549
|
-
return lines.length === 0 ? "" : `${lines.join(
|
|
3550
|
-
`;
|
|
3565
|
+
return lines.length === 0 ? "" : `${lines.join(terminator)}${terminator}`;
|
|
3551
3566
|
}
|
|
3552
3567
|
function createPropertiesAdapter() {
|
|
3553
3568
|
return createFlatFileAdapter({
|
|
@@ -4100,6 +4115,9 @@ function gateCandidateValue(sourceEntry, candidateValue, adapter) {
|
|
|
4100
4115
|
if (assessValueDegeneracy(sourceEntry.value, candidateValue).degenerate) {
|
|
4101
4116
|
return { accepted: false, reason: "degenerate" };
|
|
4102
4117
|
}
|
|
4118
|
+
if (sourceEntry.value.trim() !== "" && candidateValue.trim() === "") {
|
|
4119
|
+
return { accepted: false, reason: "empty" };
|
|
4120
|
+
}
|
|
4103
4121
|
return { accepted: true };
|
|
4104
4122
|
}
|
|
4105
4123
|
|
|
@@ -4949,6 +4967,10 @@ function fanOutAccepted(params, group, acceptedRepresentative, outcome) {
|
|
|
4949
4967
|
if (source === void 0) {
|
|
4950
4968
|
continue;
|
|
4951
4969
|
}
|
|
4970
|
+
if (!gateCandidateValue(source, acceptedRepresentative.value, params.adapter).accepted) {
|
|
4971
|
+
outcome.integrityMismatches.push(key);
|
|
4972
|
+
continue;
|
|
4973
|
+
}
|
|
4952
4974
|
outcome.accepted.set(key, { value: acceptedRepresentative.value, source });
|
|
4953
4975
|
if (flag !== void 0) {
|
|
4954
4976
|
outcome.reviewFlags.set(key, flag);
|
|
@@ -5340,14 +5362,21 @@ async function createRunCacheState(input, config, cwd, dryRun, fs) {
|
|
|
5340
5362
|
if (dryRun || input.cache === false) {
|
|
5341
5363
|
return void 0;
|
|
5342
5364
|
}
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5365
|
+
const { memory, writable } = await readTranslationMemory(cacheFilePath(cwd), fs);
|
|
5366
|
+
return { memory, writable, fingerprint: computeFingerprint(config), additions: /* @__PURE__ */ new Map() };
|
|
5367
|
+
}
|
|
5368
|
+
function withCacheNotices(summaries, cache) {
|
|
5369
|
+
if (cache === void 0 || cache.writable) {
|
|
5370
|
+
return [...summaries];
|
|
5371
|
+
}
|
|
5372
|
+
const notice = {
|
|
5373
|
+
code: "CACHE_VERSION_UNRECOGNIZED",
|
|
5374
|
+
message: `${CACHE_FILE_NAME} carries a version this build does not recognize, so it was written by a newer verbatra. It was left untouched and this run used no cache. Upgrade verbatra, or delete the file to rebuild it in this build's format.`
|
|
5347
5375
|
};
|
|
5376
|
+
return summaries.map((summary) => ({ ...summary, notices: [...summary.notices, notice] }));
|
|
5348
5377
|
}
|
|
5349
5378
|
async function recordCacheAdditions(cwd, cache, fs) {
|
|
5350
|
-
if (cache === void 0 || cache.additions.size === 0) {
|
|
5379
|
+
if (cache === void 0 || cache.additions.size === 0 || !cache.writable) {
|
|
5351
5380
|
return;
|
|
5352
5381
|
}
|
|
5353
5382
|
try {
|
|
@@ -5434,18 +5463,25 @@ async function runLocaleAt(context, targetLocales, localeIndex, runOne, results)
|
|
|
5434
5463
|
context.onProgress?.({
|
|
5435
5464
|
type: "locale-finished",
|
|
5436
5465
|
locale: targetLocale,
|
|
5437
|
-
translated: summary.translated.length
|
|
5466
|
+
translated: summary.translated.length,
|
|
5467
|
+
localeIndex,
|
|
5468
|
+
totalLocales: targetLocales.length
|
|
5438
5469
|
});
|
|
5439
5470
|
}
|
|
5440
5471
|
async function runLocalesWithProgress(context, targetLocales, runOne, concurrency) {
|
|
5441
5472
|
const totalLocales = targetLocales.length;
|
|
5442
5473
|
const results = new Array(totalLocales);
|
|
5443
5474
|
let nextIndex = 0;
|
|
5475
|
+
let abort;
|
|
5444
5476
|
async function worker() {
|
|
5445
|
-
while (nextIndex < totalLocales) {
|
|
5477
|
+
while (abort === void 0 && nextIndex < totalLocales) {
|
|
5446
5478
|
const localeIndex = nextIndex;
|
|
5447
5479
|
nextIndex += 1;
|
|
5448
|
-
|
|
5480
|
+
try {
|
|
5481
|
+
await runLocaleAt(context, targetLocales, localeIndex, runOne, results);
|
|
5482
|
+
} catch (error) {
|
|
5483
|
+
abort ??= { reason: error };
|
|
5484
|
+
}
|
|
5449
5485
|
}
|
|
5450
5486
|
}
|
|
5451
5487
|
const workerCount = Math.min(concurrency, totalLocales);
|
|
@@ -5454,6 +5490,9 @@ async function runLocalesWithProgress(context, targetLocales, runOne, concurrenc
|
|
|
5454
5490
|
workers.push(worker());
|
|
5455
5491
|
}
|
|
5456
5492
|
await Promise.all(workers);
|
|
5493
|
+
if (abort !== void 0) {
|
|
5494
|
+
throw abort.reason;
|
|
5495
|
+
}
|
|
5457
5496
|
return results.filter((summary) => summary !== void 0);
|
|
5458
5497
|
}
|
|
5459
5498
|
async function runAllLocalesDry(context, targetLocales, concurrency) {
|
|
@@ -5530,7 +5569,8 @@ async function translate2(input, deps = {}) {
|
|
|
5530
5569
|
};
|
|
5531
5570
|
const summaries = dryRun ? await runAllLocalesDry(context, config.targetLocales, concurrency) : await runAllLocalesLive(context, config.targetLocales, concurrency);
|
|
5532
5571
|
input.onProgress?.({ type: "run-finished", localesCompleted: summaries.length });
|
|
5533
|
-
const
|
|
5572
|
+
const locales = withCacheNotices(summaries, cache);
|
|
5573
|
+
const { succeeded, partial, failed } = partition(locales);
|
|
5534
5574
|
const usage = summaries.reduce(
|
|
5535
5575
|
(total, summary2) => combineUsage(total, summary2.usage),
|
|
5536
5576
|
void 0
|
|
@@ -5538,7 +5578,7 @@ async function translate2(input, deps = {}) {
|
|
|
5538
5578
|
const budgetSummary = toBudgetSummary(budget);
|
|
5539
5579
|
const summary = {
|
|
5540
5580
|
dryRun,
|
|
5541
|
-
locales
|
|
5581
|
+
locales,
|
|
5542
5582
|
succeeded,
|
|
5543
5583
|
partial,
|
|
5544
5584
|
failed,
|
|
@@ -6110,12 +6150,12 @@ function classifyClear(row, sourceEntry, buckets) {
|
|
|
6110
6150
|
buckets.withheld.add(row.key);
|
|
6111
6151
|
return;
|
|
6112
6152
|
}
|
|
6113
|
-
buckets.accepted.set(row.key, { value: "", source: sourceEntry });
|
|
6153
|
+
buckets.accepted.set(row.key, { value: "", source: sourceEntry, cleared: true });
|
|
6114
6154
|
}
|
|
6115
|
-
function classifyRows(params, buckets) {
|
|
6155
|
+
function classifyRows(params, buckets, liveCandidates) {
|
|
6116
6156
|
for (const row of params.sheet.rows) {
|
|
6117
6157
|
if (row.translation === "") {
|
|
6118
|
-
if (row.
|
|
6158
|
+
if (liveCandidates.has(row.key)) {
|
|
6119
6159
|
buckets.unfilled.push(row.key);
|
|
6120
6160
|
}
|
|
6121
6161
|
trackBlankDrift(row, params, buckets);
|
|
@@ -6134,7 +6174,11 @@ function classifyRows(params, buckets) {
|
|
|
6134
6174
|
}
|
|
6135
6175
|
const reason = judge(row, sourceEntry, params.adapter);
|
|
6136
6176
|
if (reason === void 0) {
|
|
6137
|
-
buckets.accepted.set(row.key, {
|
|
6177
|
+
buckets.accepted.set(row.key, {
|
|
6178
|
+
value: row.translation,
|
|
6179
|
+
source: sourceEntry,
|
|
6180
|
+
cleared: false
|
|
6181
|
+
});
|
|
6138
6182
|
} else {
|
|
6139
6183
|
buckets.mismatches.push(row.key);
|
|
6140
6184
|
buckets.withheld.add(row.key);
|
|
@@ -6156,7 +6200,7 @@ function importLocale(params) {
|
|
|
6156
6200
|
blankDrifted: /* @__PURE__ */ new Set(),
|
|
6157
6201
|
unfilled: []
|
|
6158
6202
|
};
|
|
6159
|
-
classifyRows(params, buckets);
|
|
6203
|
+
classifyRows(params, buckets, /* @__PURE__ */ new Set([...diff2.missing, ...diff2.changed]));
|
|
6160
6204
|
const rowKeys = new Set(params.sheet.rows.map((row) => row.key));
|
|
6161
6205
|
const invalidIcuSource = [...new Set(params.sourceInvalidIcuKeys)].filter((key) => rowKeys.has(key)).sort();
|
|
6162
6206
|
const translated = [...buckets.accepted.keys()].sort();
|
|
@@ -6214,8 +6258,10 @@ function mergeAccepted(target, accepted) {
|
|
|
6214
6258
|
}
|
|
6215
6259
|
function sheetCacheAdditions(accepted) {
|
|
6216
6260
|
const record = {};
|
|
6217
|
-
for (const [, { value, source }] of accepted) {
|
|
6218
|
-
|
|
6261
|
+
for (const [, { value, source, cleared }] of accepted) {
|
|
6262
|
+
if (!cleared) {
|
|
6263
|
+
record[contentHash(source)] = value;
|
|
6264
|
+
}
|
|
6219
6265
|
}
|
|
6220
6266
|
return record;
|
|
6221
6267
|
}
|
|
@@ -6401,6 +6447,7 @@ async function watch(input, deps = {}) {
|
|
|
6401
6447
|
const cwd = input.cwd ?? process.cwd();
|
|
6402
6448
|
const debounceMs = input.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
6403
6449
|
const fs = deps.fs ?? defaultFs;
|
|
6450
|
+
resolveRunConcurrency(input.concurrency, false, input.config);
|
|
6404
6451
|
const sourcePath = localeFilePath(cwd, input.config.files.pattern, input.config.sourceLocale);
|
|
6405
6452
|
if (!await fs.fileExists(sourcePath)) {
|
|
6406
6453
|
throw new SdkError(
|