@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/README.md
CHANGED
|
@@ -44,7 +44,9 @@ const config = await loadConfig();
|
|
|
44
44
|
// The provider reads its API key from the environment (e.g. ANTHROPIC_API_KEY). No key is passed.
|
|
45
45
|
const summary = await translate({ config });
|
|
46
46
|
|
|
47
|
-
console.log(
|
|
47
|
+
console.log(
|
|
48
|
+
`${summary.succeeded.length} locale(s) done, ${summary.partial.length} partial, ${summary.failed.length} failed`,
|
|
49
|
+
);
|
|
48
50
|
```
|
|
49
51
|
|
|
50
52
|
## Defining config
|
|
@@ -71,21 +73,29 @@ export default defineConfig({
|
|
|
71
73
|
});
|
|
72
74
|
```
|
|
73
75
|
|
|
74
|
-
`files.pattern` must contain the `{locale}` token,
|
|
76
|
+
`files.pattern` must contain the `{locale}` token, `targetLocales` must not include `sourceLocale`, and `targetLocales` must not list the same locale twice, compared case-insensitively (two such entries would collide as one Excel worksheet on export); all three are enforced when the config is validated. The supported `format` values are `i18next-json`, `vue-i18n-json`, `next-intl-json`, `ngx-translate-json`, `xliff`, `yaml`, `arb`, and `properties`. JSON-family, YAML, and ARB files round-trip in exact document key order: integer-like keys keep their position, new keys append in source-document order, and a YAML composite key (a map or sequence used as a mapping key) fails with a structured error; a `.properties` write preserves the destination file's existing line endings. The optional `glossary` (a term map, inline or a path to a JSON file of the same shape) and `tone` (`"formal"`, `"informal"`, or `"neutral"`) refine the output. The optional `prune` boolean (off by default) opts in to removing orphaned keys (present in a target file but absent from the source) from the written target files and the lock; the `translate --prune` flag overrides it per run. The optional `generatePlurals` boolean (off by default) opts in to synthesizing the CLDR plural forms a richer target language requires but the source lacks (i18next-JSON projects translated by an LLM provider only; DeepL, non-i18next formats, and unknown languages fall back to the per-locale plural warning and never fail); a per-run `generatePlurals` override on `translate` takes precedence, and generated keys are reported separately from translated keys on the summary. The optional `maxBatchSize` (a positive integer, 50 when absent) caps how many entries go into a single provider request, so a large locale is split into sequential sub-batches and one oversized request cannot sink the whole locale. The optional `maxTokens` sets a whole-run ceiling on input plus output tokens across every provider call, and `budgetBehavior` decides what happens once it is reached: `"warn"` (the default) flags it and lets the run continue, `"stop"` withholds every not-yet-attempted key for the rest of the run so it retries next time. Both are config-only, have no CLI flag, and never change an exit code; against a token-less provider such as DeepL, which reports no usage, the budget stays inert rather than tripping falsely.
|
|
77
|
+
|
|
78
|
+
Anthropic takes `{ model, maxTokens }`; OpenAI and Gemini take `{ model, maxOutputTokens }`; `openai-compatible` takes the same pair plus a `baseUrl` (for a local or self-hosted server such as LM Studio, Ollama, or vLLM) and an optional `apiKeyEnvVar`; DeepL takes `{}` (with an optional `glossaryId`). Every provider additionally accepts an optional `requestTimeoutMs`, a positive-integer per-request timeout in milliseconds that bounds each outbound call. API keys are never part of the config. Each provider reads its own environment variable (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `DEEPL_API_KEY`; `openai-compatible` resolves its key from `apiKeyEnvVar`, then `OPENAI_COMPATIBLE_API_KEY`, then falls back to a keyless placeholder).
|
|
75
79
|
|
|
76
80
|
## API reference
|
|
77
81
|
|
|
78
82
|
### `defineConfig(config)`
|
|
79
83
|
|
|
80
|
-
Returns the config unchanged. It exists purely for type inference and editor autocomplete when authoring a code-defined config.
|
|
84
|
+
Returns the config unchanged. It exists purely for type inference and editor autocomplete when authoring a code-defined config. For Anthropic, OpenAI, and Gemini the `model` field is restricted to that provider's known model IDs (sourced from its own SDK), so a model from another provider is a type error at authoring time; the runtime still validates `model` only as a non-empty string, so an unlisted model runs even though the editor flags it. DeepL has no `model` field, and `openai-compatible`'s model is whatever the local server exposes, so neither is restricted.
|
|
81
85
|
|
|
82
86
|
### `loadConfig(options?): Promise<VerbatraConfig>`
|
|
83
87
|
|
|
84
|
-
Discovers and validates the configuration. With no arguments it searches upward from the current working directory; `options` accepts `cwd`, an explicit `configPath`,
|
|
88
|
+
Discovers and validates the configuration. With no arguments it searches upward from the current working directory; `options` accepts `cwd`, an explicit `configPath`, an in-memory `configOverride`, and an `fs` seam. Precedence is `configOverride`, then `configPath`, then the search. Resolves to the validated `VerbatraConfig`, and throws an `SdkError` if no config is found (`CONFIG_NOT_FOUND`) or it fails validation (`CONFIG_INVALID`). A `glossary` given as a file path is read and validated here, so every downstream call receives a plain term map.
|
|
85
89
|
|
|
86
90
|
### `translate(input): Promise<RunSummary>`
|
|
87
91
|
|
|
88
|
-
Runs the one-shot read, diff, translate, write flow over every target locale. `input` is `{ config, cwd?, dryRun?, prune?, generatePlurals? }`. With `dryRun: true` it reads, diffs, and reports without calling the provider or writing anything. `prune` and `generatePlurals` each override the matching config option for this run.
|
|
92
|
+
Runs the one-shot read, diff, translate, write flow over every target locale. `input` is a `TranslateInput`: `{ config, cwd?, dryRun?, prune?, generatePlurals?, cache?, concurrency?, lockAcquireTimeoutMs?, onProgress?, onLockWait? }`. With `dryRun: true` it reads, diffs, and reports without calling the provider or writing anything. `prune` and `generatePlurals` each override the matching config option for this run.
|
|
93
|
+
|
|
94
|
+
`cache` (on by default, and ignored on a dry run) toggles the local content-addressed translation-memory cache in `verbatra.cache.json`: a key whose source content is unchanged, including under a renamed key or shared with another key, is served from the cache instead of being re-sent to the provider. A cached value is re-checked against that key's own current source first, and one that fails the gate falls through to the provider rather than being written. `concurrency` (defaults to 1, strictly serial) is how many target locales may run at once; it must be an integer of at least 1, and on a live run a value above 1 is refused when the config sets `maxTokens`, because concurrent locales would overshoot the budget nondeterministically. `lockAcquireTimeoutMs` overrides how long a locale's write lock keeps retrying before it fails. `onProgress` receives a structured event once per locale before and after it runs, once per provider sub-batch, and once when the locale loop ends; `onLockWait` fires while a locale's write lock is blocked on another process holding it. The SDK writes to no stream, so these callbacks are the only progress signal.
|
|
95
|
+
|
|
96
|
+
Resolves to a `RunSummary`: `dryRun`, `locales`, `succeeded`, `partial`, `failed`, plus `usage` when any provider call reported tokens and `budget` when `maxTokens` is configured. A locale's `status` is `"succeeded"` when nothing was withheld (a no-op with no candidate keys included), `"partial"` when it accepted at least one key and withheld at least one, and `"failed"` when it withheld keys and accepted none, or threw. Each `LocaleSummary` carries `locale`, `status`, `translated`, `unchanged`, `orphaned`, `pruned`, `invalidIcuSource`, `cacheHits`, `integrityMismatches`, `providerFailures`, `budgetWithheld`, `generated`, `notices`, `needsReview`, `unfilled`, `malformedRows`, `duplicateKeys`, an optional `usage`, and an optional `error` on a locale that threw. `cacheHits` are keys served from the translation-memory cache rather than the provider. `integrityMismatches` is a translation that came back and was withheld by verbatra's integrity gate, whose rejection reasons are the exported `IntegrityGateReason` union. `providerFailures` is a key withheld because nothing was translated for it (the provider call failed, or the response was still missing that key), with any secret-free failure code and message reported in `notices`. `budgetWithheld` is a candidate never sent because a `maxTokens` budget in `"stop"` mode had already tripped. `needsReview` flags accepted keys the review heuristics want a human to look at, and never withholds anything. `unfilled`, `malformedRows`, and `duplicateKeys` are populated only by `importWorkbook`, which returns this same shape. Every withheld key keeps its prior lock hash and is retried next run.
|
|
97
|
+
|
|
98
|
+
Whole-run failures throw an `SdkError`: an unknown format, provider construction (including a missing API key), an unreadable or invalid source file, a corrupt lock file, an invalid `concurrency`, or the concurrency-and-budget conflict above. A per-locale failure never throws; it is isolated on that locale's summary. On a non-dry run the flow also writes `.verbatra-local/run-status.json` (best-effort, read back through `runStatus`).
|
|
89
99
|
|
|
90
100
|
```ts
|
|
91
101
|
const preview = await translate({ config, dryRun: true });
|
|
@@ -93,7 +103,9 @@ const preview = await translate({ config, dryRun: true });
|
|
|
93
103
|
|
|
94
104
|
### `watch(input): Promise<WatchController>`
|
|
95
105
|
|
|
96
|
-
Watches the source file and re-runs the translate flow on each debounced change. `input` is `{ config, cwd?, debounceMs?, onRun
|
|
106
|
+
Watches the source file and re-runs the translate flow on each debounced change. `input` is `{ config, cwd?, debounceMs?, onRun, cache?, concurrency?, lockAcquireTimeoutMs?, onLockWait?, onProgress? }`; `debounceMs` defaults to 300, and the last five are passed straight through to every run. One run starts immediately at startup, before any change arrives. Runs are serialized, so changes during a run collapse into a single follow-up.
|
|
107
|
+
|
|
108
|
+
`onRun` receives a `WatchRunResult` per run: `{ status: "succeeded", summary }` or `{ status: "failed", error }` with a secret-free `{ code, message }`, so a failing run is reported and watching continues. `watch` itself throws only at startup: `CONCURRENCY_INVALID` or `CONCURRENCY_BUDGET_CONFLICT` for a `concurrency` no cycle could honor (resolved once, before the watcher exists, rather than failing every cycle), and `SOURCE_UNREADABLE` when the source locale file is absent. Resolves to a `WatchController` whose `stop()` closes the watcher and awaits the in-flight run.
|
|
97
109
|
|
|
98
110
|
```ts
|
|
99
111
|
import { loadConfig, watch } from "@verbatra/sdk";
|
|
@@ -125,7 +137,7 @@ if (!summary.inSync) {
|
|
|
125
137
|
|
|
126
138
|
### `diff(input): Promise<DiffSummary>`
|
|
127
139
|
|
|
128
|
-
Lists the keys a run would touch, without writing anything. `input` is the same `{ config, cwd?, locales? }` shape as `check`. Resolves to a `DiffSummary` whose `locales` lists one `LocaleDiff` each, with the key arrays `missing` (would be added), `changed` (would be re-translated), and `orphaned` (present in the target but absent from the source).
|
|
140
|
+
Lists the keys a run would touch, without writing anything. `input` is the same `{ config, cwd?, locales? }` shape as `check`. Resolves to a `DiffSummary` whose `locales` lists one `LocaleDiff` each, with the key arrays `missing` (would be added), `changed` (would be re-translated), and `orphaned` (present in the target but absent from the source), plus a per-locale `hasPendingChanges` driven by `missing` and `changed` only, since a default run does not prune. The top-level `hasPendingChanges` is true when any checked locale has some.
|
|
129
141
|
|
|
130
142
|
```ts
|
|
131
143
|
import { diff, loadConfig } from "@verbatra/sdk";
|
|
@@ -136,11 +148,11 @@ const summary = await diff({ config });
|
|
|
136
148
|
|
|
137
149
|
### `exportWorkbook(input): Promise<ExportWorkbookResult>`
|
|
138
150
|
|
|
139
|
-
Exports the strings that need translating into a styled Excel workbook for a human translator. `input` is `{ config, cwd?, out?, locales?, includeUnchanged? }`. By default it writes the missing and changed strings for every target locale to `verbatra-translations.xlsx`; `out` overrides the path, `locales` narrows which target locales are exported, and `includeUnchanged: true` also exports already up-to-date strings. Resolves to an `ExportWorkbookResult` with the
|
|
151
|
+
Exports the strings that need translating into a styled Excel workbook for a human translator. `input` is `{ config, cwd?, out?, locales?, includeUnchanged? }`. By default it writes the missing and changed strings for every target locale to `verbatra-translations.xlsx`; `out` overrides the path, `locales` narrows which target locales are exported, and `includeUnchanged: true` also exports already up-to-date strings. No provider is called and no lock file is written. Resolves to an `ExportWorkbookResult` with the absolute `path` written and a per-locale row count.
|
|
140
152
|
|
|
141
153
|
### `importWorkbook(input): Promise<RunSummary>`
|
|
142
154
|
|
|
143
|
-
Imports a filled workbook back into the locale files,
|
|
155
|
+
Imports a filled workbook back into the locale files, gating every row through the same integrity gate as `translate` on top of a fresh source-drift check. `input` is `{ config, workbook, cwd?, dryRun? }`. With `dryRun: true` it validates and reports without writing locale files or updating the lock. Resolves to a `RunSummary`, the same shape `translate` returns: a row the translator left blank whose key still needs a translation is reported in that locale's `unfilled` (nothing is written and the prior lock baseline is kept), an unreadable row in `malformedRows`, a repeated key in `duplicateKeys` (the first occurrence wins), and a configured target locale whose sheet is missing from the workbook is reported as that locale's failed summary rather than silently dropped. `unfilled`, `malformedRows`, and `duplicateKeys` do not feed a locale's `status`, so a partly filled sheet still imports the rows it has. A key is cleared by filling its Translation cell with the `[[CLEAR]]` sentinel; an ordinary blank never clears a value.
|
|
144
156
|
|
|
145
157
|
```ts
|
|
146
158
|
import { exportWorkbook, importWorkbook, loadConfig } from "@verbatra/sdk";
|
|
@@ -160,17 +172,19 @@ See [Manual translation](https://verbatra.kreitz-webdev.de/docs/manual-translati
|
|
|
160
172
|
|
|
161
173
|
Beyond the flows above, the SDK exports the building blocks Verbatra Studio and other tooling sit on, each a one-call read or a locked single-key write:
|
|
162
174
|
|
|
163
|
-
- `keyIntegrity` reports, per changed key, whether its placeholders and
|
|
175
|
+
- `keyIntegrity` reports, per changed key, whether its placeholders still match the source (with the missing and extra tokens on a mismatch) and whether the current target value is still valid ICU.
|
|
164
176
|
- `lockState` reports the lock file's existence, version, and per-locale drift; `loadLockFile` reads the lock file itself.
|
|
165
|
-
- `runStatus` reads the persisted review-flag and token-usage snapshot the last `translate` or `watch` run left behind.
|
|
177
|
+
- `runStatus` reads the persisted review-flag and token-usage snapshot the last non-dry `translate` or `watch` run left behind; it never throws, and a missing, corrupt, or unrecognized file simply reports as unavailable.
|
|
166
178
|
- `keyValue` reads one key's current source and target values.
|
|
167
|
-
- `editEntry` saves a manually edited translation for one key, and `retranslateEntry` re-runs the provider for one key; both run the
|
|
179
|
+
- `editEntry` saves a manually edited translation for one key, and `retranslateEntry` re-runs the provider for one key; both run the candidate through the same integrity gate as a full run (a rejection names an `IntegrityGateReason` and writes nothing) and hold the same per-locale write lock.
|
|
168
180
|
- `readLocaleFileSnapshot` and `diffLocaleSnapshots` snapshot one locale file as per-key content hashes and compare two snapshots, the primitives behind live-refresh watching.
|
|
169
181
|
- `loadConfigWithMeta` is `loadConfig` plus config-source and glossary provenance.
|
|
170
182
|
|
|
171
183
|
## Errors and results
|
|
172
184
|
|
|
173
|
-
|
|
185
|
+
`SdkError` is the SDK's own structured error type, thrown for whole-run failures such as a missing or invalid config or an unreadable source file. It carries a stable `code` from the exported `SdkErrorCode` union and never contains an API key. It is not the only error a caller can see: `retranslateEntry` propagates the provider's own `ProviderError` when the provider call fails or returns nothing for the key, and a target locale file that exists but is malformed rejects with the adapter's own error.
|
|
186
|
+
|
|
187
|
+
Per-locale failures do not throw: they are recorded on the `RunSummary` so one failing locale never aborts the others, and that includes a locale whose write lock could not be acquired. A locale's `error.code` is a preserved string from the underlying provider or adapter failure (`"LOCALE_FAILED"` is only the fallback), deliberately wider than `SdkErrorCode`, so do not treat it as a closed set.
|
|
174
188
|
|
|
175
189
|
## Documentation
|
|
176
190
|
|
package/dist/index.cjs
CHANGED
|
@@ -61,27 +61,31 @@ var translationMemorySchema = zod.z.object({
|
|
|
61
61
|
function cacheFilePath(cwd) {
|
|
62
62
|
return path.resolve(cwd, CACHE_FILE_NAME);
|
|
63
63
|
}
|
|
64
|
+
var UNUSABLE = { memory: EMPTY_MEMORY, writable: true };
|
|
64
65
|
async function readTranslationMemory(path, fs) {
|
|
65
66
|
let read;
|
|
66
67
|
try {
|
|
67
68
|
read = await fs.readFileBounded(path, MAX_CACHE_FILE_BYTES);
|
|
68
69
|
} catch {
|
|
69
|
-
return
|
|
70
|
+
return UNUSABLE;
|
|
70
71
|
}
|
|
71
72
|
if (read.kind !== "ok") {
|
|
72
|
-
return
|
|
73
|
+
return UNUSABLE;
|
|
73
74
|
}
|
|
74
75
|
let parsed;
|
|
75
76
|
try {
|
|
76
77
|
parsed = JSON.parse(read.content);
|
|
77
78
|
} catch {
|
|
78
|
-
return
|
|
79
|
+
return UNUSABLE;
|
|
79
80
|
}
|
|
80
81
|
const result = translationMemorySchema.safeParse(parsed);
|
|
81
|
-
if (!result.success
|
|
82
|
-
return
|
|
82
|
+
if (!result.success) {
|
|
83
|
+
return UNUSABLE;
|
|
83
84
|
}
|
|
84
|
-
|
|
85
|
+
if (result.data.version !== CURRENT_VERSION) {
|
|
86
|
+
return { memory: EMPTY_MEMORY, writable: false };
|
|
87
|
+
}
|
|
88
|
+
return { memory: result.data, writable: true };
|
|
85
89
|
}
|
|
86
90
|
function lookupMemory(memory, fingerprint, locale, contentHash2) {
|
|
87
91
|
return memory.entries[fingerprint]?.[locale]?.[contentHash2];
|
|
@@ -133,7 +137,10 @@ async function feedTranslationMemory(cwd, fs, fingerprint, additionsByLocale) {
|
|
|
133
137
|
}
|
|
134
138
|
try {
|
|
135
139
|
const path = cacheFilePath(cwd);
|
|
136
|
-
const memory = await readTranslationMemory(path, fs);
|
|
140
|
+
const { memory, writable } = await readTranslationMemory(path, fs);
|
|
141
|
+
if (!writable) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
137
144
|
await writeTranslationMemory(path, applyAdditions(memory, fingerprint, additionsByLocale), fs);
|
|
138
145
|
} catch {
|
|
139
146
|
}
|
|
@@ -3339,6 +3346,12 @@ function extractPropertiesPlaceholders(value) {
|
|
|
3339
3346
|
var UNICODE_ESCAPE = /^[0-9a-fA-F]{4}$/;
|
|
3340
3347
|
var LEADING_WHITESPACE = /^[ \t\f]+/;
|
|
3341
3348
|
var TRAILING_TERMINATOR = /(?:\r\n|\r|\n)$/;
|
|
3349
|
+
function detectLineTerminator(content) {
|
|
3350
|
+
if (content.includes("\r\n")) {
|
|
3351
|
+
return "\r\n";
|
|
3352
|
+
}
|
|
3353
|
+
return content.includes("\r") ? "\r" : "\n";
|
|
3354
|
+
}
|
|
3342
3355
|
function isPropertiesWhitespace(char) {
|
|
3343
3356
|
return char === " " || char === " " || char === "\f";
|
|
3344
3357
|
}
|
|
@@ -3544,7 +3557,7 @@ async function readStructure(filePath) {
|
|
|
3544
3557
|
outcome = await readBounded2(filePath);
|
|
3545
3558
|
} catch (error) {
|
|
3546
3559
|
if (isFileNotFound(error)) {
|
|
3547
|
-
return [];
|
|
3560
|
+
return { items: [], terminator: "\n" };
|
|
3548
3561
|
}
|
|
3549
3562
|
throw new AdapterError("INVALID_STRUCTURE", "The destination file could not be read.");
|
|
3550
3563
|
}
|
|
@@ -3554,13 +3567,16 @@ async function readStructure(filePath) {
|
|
|
3554
3567
|
if (outcome.kind === "too-large") {
|
|
3555
3568
|
throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
|
|
3556
3569
|
}
|
|
3557
|
-
return
|
|
3570
|
+
return {
|
|
3571
|
+
items: parseItems(outcome.content),
|
|
3572
|
+
terminator: detectLineTerminator(outcome.content)
|
|
3573
|
+
};
|
|
3558
3574
|
}
|
|
3559
3575
|
async function serializePropertiesEntries(entries, filePath) {
|
|
3560
|
-
const
|
|
3576
|
+
const { items, terminator } = await readStructure(filePath);
|
|
3561
3577
|
const emitted = /* @__PURE__ */ new Set();
|
|
3562
3578
|
const lines = [];
|
|
3563
|
-
for (const item of
|
|
3579
|
+
for (const item of items) {
|
|
3564
3580
|
if (item.kind === "raw") {
|
|
3565
3581
|
lines.push(item.text);
|
|
3566
3582
|
continue;
|
|
@@ -3576,8 +3592,7 @@ async function serializePropertiesEntries(entries, filePath) {
|
|
|
3576
3592
|
lines.push(formatEntry(key, entry.value));
|
|
3577
3593
|
}
|
|
3578
3594
|
}
|
|
3579
|
-
return lines.length === 0 ? "" : `${lines.join(
|
|
3580
|
-
`;
|
|
3595
|
+
return lines.length === 0 ? "" : `${lines.join(terminator)}${terminator}`;
|
|
3581
3596
|
}
|
|
3582
3597
|
function createPropertiesAdapter() {
|
|
3583
3598
|
return createFlatFileAdapter({
|
|
@@ -4130,6 +4145,9 @@ function gateCandidateValue(sourceEntry, candidateValue, adapter) {
|
|
|
4130
4145
|
if (assessValueDegeneracy(sourceEntry.value, candidateValue).degenerate) {
|
|
4131
4146
|
return { accepted: false, reason: "degenerate" };
|
|
4132
4147
|
}
|
|
4148
|
+
if (sourceEntry.value.trim() !== "" && candidateValue.trim() === "") {
|
|
4149
|
+
return { accepted: false, reason: "empty" };
|
|
4150
|
+
}
|
|
4133
4151
|
return { accepted: true };
|
|
4134
4152
|
}
|
|
4135
4153
|
|
|
@@ -4979,6 +4997,10 @@ function fanOutAccepted(params, group, acceptedRepresentative, outcome) {
|
|
|
4979
4997
|
if (source === void 0) {
|
|
4980
4998
|
continue;
|
|
4981
4999
|
}
|
|
5000
|
+
if (!gateCandidateValue(source, acceptedRepresentative.value, params.adapter).accepted) {
|
|
5001
|
+
outcome.integrityMismatches.push(key);
|
|
5002
|
+
continue;
|
|
5003
|
+
}
|
|
4982
5004
|
outcome.accepted.set(key, { value: acceptedRepresentative.value, source });
|
|
4983
5005
|
if (flag !== void 0) {
|
|
4984
5006
|
outcome.reviewFlags.set(key, flag);
|
|
@@ -5370,14 +5392,21 @@ async function createRunCacheState(input, config, cwd, dryRun, fs) {
|
|
|
5370
5392
|
if (dryRun || input.cache === false) {
|
|
5371
5393
|
return void 0;
|
|
5372
5394
|
}
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5395
|
+
const { memory, writable } = await readTranslationMemory(cacheFilePath(cwd), fs);
|
|
5396
|
+
return { memory, writable, fingerprint: computeFingerprint(config), additions: /* @__PURE__ */ new Map() };
|
|
5397
|
+
}
|
|
5398
|
+
function withCacheNotices(summaries, cache) {
|
|
5399
|
+
if (cache === void 0 || cache.writable) {
|
|
5400
|
+
return [...summaries];
|
|
5401
|
+
}
|
|
5402
|
+
const notice = {
|
|
5403
|
+
code: "CACHE_VERSION_UNRECOGNIZED",
|
|
5404
|
+
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.`
|
|
5377
5405
|
};
|
|
5406
|
+
return summaries.map((summary) => ({ ...summary, notices: [...summary.notices, notice] }));
|
|
5378
5407
|
}
|
|
5379
5408
|
async function recordCacheAdditions(cwd, cache, fs) {
|
|
5380
|
-
if (cache === void 0 || cache.additions.size === 0) {
|
|
5409
|
+
if (cache === void 0 || cache.additions.size === 0 || !cache.writable) {
|
|
5381
5410
|
return;
|
|
5382
5411
|
}
|
|
5383
5412
|
try {
|
|
@@ -5464,18 +5493,25 @@ async function runLocaleAt(context, targetLocales, localeIndex, runOne, results)
|
|
|
5464
5493
|
context.onProgress?.({
|
|
5465
5494
|
type: "locale-finished",
|
|
5466
5495
|
locale: targetLocale,
|
|
5467
|
-
translated: summary.translated.length
|
|
5496
|
+
translated: summary.translated.length,
|
|
5497
|
+
localeIndex,
|
|
5498
|
+
totalLocales: targetLocales.length
|
|
5468
5499
|
});
|
|
5469
5500
|
}
|
|
5470
5501
|
async function runLocalesWithProgress(context, targetLocales, runOne, concurrency) {
|
|
5471
5502
|
const totalLocales = targetLocales.length;
|
|
5472
5503
|
const results = new Array(totalLocales);
|
|
5473
5504
|
let nextIndex = 0;
|
|
5505
|
+
let abort;
|
|
5474
5506
|
async function worker() {
|
|
5475
|
-
while (nextIndex < totalLocales) {
|
|
5507
|
+
while (abort === void 0 && nextIndex < totalLocales) {
|
|
5476
5508
|
const localeIndex = nextIndex;
|
|
5477
5509
|
nextIndex += 1;
|
|
5478
|
-
|
|
5510
|
+
try {
|
|
5511
|
+
await runLocaleAt(context, targetLocales, localeIndex, runOne, results);
|
|
5512
|
+
} catch (error) {
|
|
5513
|
+
abort ??= { reason: error };
|
|
5514
|
+
}
|
|
5479
5515
|
}
|
|
5480
5516
|
}
|
|
5481
5517
|
const workerCount = Math.min(concurrency, totalLocales);
|
|
@@ -5484,6 +5520,9 @@ async function runLocalesWithProgress(context, targetLocales, runOne, concurrenc
|
|
|
5484
5520
|
workers.push(worker());
|
|
5485
5521
|
}
|
|
5486
5522
|
await Promise.all(workers);
|
|
5523
|
+
if (abort !== void 0) {
|
|
5524
|
+
throw abort.reason;
|
|
5525
|
+
}
|
|
5487
5526
|
return results.filter((summary) => summary !== void 0);
|
|
5488
5527
|
}
|
|
5489
5528
|
async function runAllLocalesDry(context, targetLocales, concurrency) {
|
|
@@ -5560,7 +5599,8 @@ async function translate2(input, deps = {}) {
|
|
|
5560
5599
|
};
|
|
5561
5600
|
const summaries = dryRun ? await runAllLocalesDry(context, config.targetLocales, concurrency) : await runAllLocalesLive(context, config.targetLocales, concurrency);
|
|
5562
5601
|
input.onProgress?.({ type: "run-finished", localesCompleted: summaries.length });
|
|
5563
|
-
const
|
|
5602
|
+
const locales = withCacheNotices(summaries, cache);
|
|
5603
|
+
const { succeeded, partial, failed } = partition(locales);
|
|
5564
5604
|
const usage = summaries.reduce(
|
|
5565
5605
|
(total, summary2) => combineUsage(total, summary2.usage),
|
|
5566
5606
|
void 0
|
|
@@ -5568,7 +5608,7 @@ async function translate2(input, deps = {}) {
|
|
|
5568
5608
|
const budgetSummary = toBudgetSummary(budget);
|
|
5569
5609
|
const summary = {
|
|
5570
5610
|
dryRun,
|
|
5571
|
-
locales
|
|
5611
|
+
locales,
|
|
5572
5612
|
succeeded,
|
|
5573
5613
|
partial,
|
|
5574
5614
|
failed,
|
|
@@ -6140,12 +6180,12 @@ function classifyClear(row, sourceEntry, buckets) {
|
|
|
6140
6180
|
buckets.withheld.add(row.key);
|
|
6141
6181
|
return;
|
|
6142
6182
|
}
|
|
6143
|
-
buckets.accepted.set(row.key, { value: "", source: sourceEntry });
|
|
6183
|
+
buckets.accepted.set(row.key, { value: "", source: sourceEntry, cleared: true });
|
|
6144
6184
|
}
|
|
6145
|
-
function classifyRows(params, buckets) {
|
|
6185
|
+
function classifyRows(params, buckets, liveCandidates) {
|
|
6146
6186
|
for (const row of params.sheet.rows) {
|
|
6147
6187
|
if (row.translation === "") {
|
|
6148
|
-
if (row.
|
|
6188
|
+
if (liveCandidates.has(row.key)) {
|
|
6149
6189
|
buckets.unfilled.push(row.key);
|
|
6150
6190
|
}
|
|
6151
6191
|
trackBlankDrift(row, params, buckets);
|
|
@@ -6164,7 +6204,11 @@ function classifyRows(params, buckets) {
|
|
|
6164
6204
|
}
|
|
6165
6205
|
const reason = judge(row, sourceEntry, params.adapter);
|
|
6166
6206
|
if (reason === void 0) {
|
|
6167
|
-
buckets.accepted.set(row.key, {
|
|
6207
|
+
buckets.accepted.set(row.key, {
|
|
6208
|
+
value: row.translation,
|
|
6209
|
+
source: sourceEntry,
|
|
6210
|
+
cleared: false
|
|
6211
|
+
});
|
|
6168
6212
|
} else {
|
|
6169
6213
|
buckets.mismatches.push(row.key);
|
|
6170
6214
|
buckets.withheld.add(row.key);
|
|
@@ -6186,7 +6230,7 @@ function importLocale(params) {
|
|
|
6186
6230
|
blankDrifted: /* @__PURE__ */ new Set(),
|
|
6187
6231
|
unfilled: []
|
|
6188
6232
|
};
|
|
6189
|
-
classifyRows(params, buckets);
|
|
6233
|
+
classifyRows(params, buckets, /* @__PURE__ */ new Set([...diff2.missing, ...diff2.changed]));
|
|
6190
6234
|
const rowKeys = new Set(params.sheet.rows.map((row) => row.key));
|
|
6191
6235
|
const invalidIcuSource = [...new Set(params.sourceInvalidIcuKeys)].filter((key) => rowKeys.has(key)).sort();
|
|
6192
6236
|
const translated = [...buckets.accepted.keys()].sort();
|
|
@@ -6244,8 +6288,10 @@ function mergeAccepted(target, accepted) {
|
|
|
6244
6288
|
}
|
|
6245
6289
|
function sheetCacheAdditions(accepted) {
|
|
6246
6290
|
const record = {};
|
|
6247
|
-
for (const [, { value, source }] of accepted) {
|
|
6248
|
-
|
|
6291
|
+
for (const [, { value, source, cleared }] of accepted) {
|
|
6292
|
+
if (!cleared) {
|
|
6293
|
+
record[contentHash(source)] = value;
|
|
6294
|
+
}
|
|
6249
6295
|
}
|
|
6250
6296
|
return record;
|
|
6251
6297
|
}
|
|
@@ -6431,6 +6477,7 @@ async function watch(input, deps = {}) {
|
|
|
6431
6477
|
const cwd = input.cwd ?? process.cwd();
|
|
6432
6478
|
const debounceMs = input.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
6433
6479
|
const fs = deps.fs ?? defaultFs;
|
|
6480
|
+
resolveRunConcurrency(input.concurrency, false, input.config);
|
|
6434
6481
|
const sourcePath = localeFilePath(cwd, input.config.files.pattern, input.config.sourceLocale);
|
|
6435
6482
|
if (!await fs.fileExists(sourcePath)) {
|
|
6436
6483
|
throw new SdkError(
|