@verbatra/sdk 0.10.0 → 0.11.1
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 +55 -111
- package/dist/config-schema.json +175 -14
- package/dist/index.cjs +11993 -3690
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3231 -781
- package/dist/index.d.ts +3231 -781
- package/dist/index.js +11954 -3670
- package/dist/index.js.map +1 -1
- package/package.json +14 -13
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ import OpenAI from 'openai';
|
|
|
17
17
|
* (an i18next plural key suffix, a vue-i18n pipe-separated value, or an ICU plural), which the adapter
|
|
18
18
|
* decides and which feeds the content hash.
|
|
19
19
|
*/
|
|
20
|
-
declare const translationEntrySchema$
|
|
20
|
+
declare const translationEntrySchema$2: z.ZodObject<{
|
|
21
21
|
key: z.ZodString;
|
|
22
22
|
namespace: z.ZodString;
|
|
23
23
|
value: z.ZodString;
|
|
@@ -30,14 +30,14 @@ declare const translationEntrySchema$1: z.ZodObject<{
|
|
|
30
30
|
* The validated shape of one translation unit; the inferred type of {@link translationEntrySchema},
|
|
31
31
|
* made deeply readonly. See that schema for what each field carries.
|
|
32
32
|
*/
|
|
33
|
-
type TranslationEntry$
|
|
33
|
+
type TranslationEntry$2 = Readonly<z.infer<typeof translationEntrySchema$2>>;
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Result of comparing source placeholders against translated-output placeholders as MULTISETS
|
|
37
37
|
* (counts matter, so dropping one of two occurrences of the same token is still a miss). Always
|
|
38
38
|
* returned, never thrown: a mismatch is data the caller decides what to do about, not a failure.
|
|
39
39
|
*/
|
|
40
|
-
interface PlaceholderIntegrityResult$
|
|
40
|
+
interface PlaceholderIntegrityResult$2 {
|
|
41
41
|
/** True when nothing is missing or extra: the multisets match, regardless of order. */
|
|
42
42
|
readonly matches: boolean;
|
|
43
43
|
/** Occurrences present in source but absent from the translation, one per dropped occurrence. Sorted. */
|
|
@@ -74,7 +74,7 @@ type PlaceholderExtractor = (value: string) => readonly string[];
|
|
|
74
74
|
* defines one) so it matches the entries' format; ai-providers never derives it itself and never parses
|
|
75
75
|
* a specific format's message syntax.
|
|
76
76
|
*/
|
|
77
|
-
type PlaceholderComparator = (source: string, translated: string) => PlaceholderIntegrityResult$
|
|
77
|
+
type PlaceholderComparator = (source: string, translated: string) => PlaceholderIntegrityResult$2;
|
|
78
78
|
/**
|
|
79
79
|
* A batch translation request. Format- and provider-neutral: it carries no prompt,
|
|
80
80
|
* model, key, or other provider-specific field. The placeholder extractor is
|
|
@@ -86,11 +86,18 @@ interface TranslateRequest {
|
|
|
86
86
|
/** BCP-47 target locale to translate into (for example, "de"). */
|
|
87
87
|
readonly targetLocale: string;
|
|
88
88
|
/** The entries to translate; at least one is required. */
|
|
89
|
-
readonly entries: readonly TranslationEntry$
|
|
89
|
+
readonly entries: readonly TranslationEntry$2[];
|
|
90
90
|
/** Optional source-term to target-term map applied by glossary-capable providers. */
|
|
91
91
|
readonly glossary?: Readonly<Record<string, string>>;
|
|
92
92
|
/** Optional target {@link Tone}; machine-translation providers map it to formality. */
|
|
93
93
|
readonly tone?: Tone;
|
|
94
|
+
/**
|
|
95
|
+
* Optional per-key maximum length budget, measured in grapheme clusters. A key present in the map
|
|
96
|
+
* whose translation comes back longer than its budget is flagged `MAX_LENGTH_EXCEEDED`; a key
|
|
97
|
+
* absent from the map is never flagged. Advisory only: it never changes what is sent to the
|
|
98
|
+
* provider and never withholds a value.
|
|
99
|
+
*/
|
|
100
|
+
readonly maxLength?: ReadonlyMap<string, number>;
|
|
94
101
|
/** Mandatory placeholder extractor; the output integrity check runs against it. */
|
|
95
102
|
readonly extractPlaceholders: PlaceholderExtractor;
|
|
96
103
|
/**
|
|
@@ -108,7 +115,10 @@ interface TranslateRequest {
|
|
|
108
115
|
*/
|
|
109
116
|
readonly signal?: AbortSignal;
|
|
110
117
|
}
|
|
111
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Token usage, when the provider reports it. Absent for the providers that do not bill by token
|
|
120
|
+
* (DeepL and Google Cloud Translation).
|
|
121
|
+
*/
|
|
112
122
|
interface Usage {
|
|
113
123
|
/** Tokens consumed by the request, summed across the initial call and any repair round. */
|
|
114
124
|
readonly inputTokens: number;
|
|
@@ -119,10 +129,10 @@ interface Usage {
|
|
|
119
129
|
* Stable codes for a provider's graceful-degradation notices. These are returned DATA on a
|
|
120
130
|
* successful result, NOT thrown:
|
|
121
131
|
*
|
|
122
|
-
* - `FORMALITY_DOWNGRADED`: a requested
|
|
123
|
-
* support
|
|
132
|
+
* - `FORMALITY_DOWNGRADED`: a requested `formal` or `informal` tone was not applied (DeepL's free
|
|
133
|
+
* tier does not support formality, and Google Cloud Translation Basic has no formality control).
|
|
124
134
|
* - `GLOSSARY_IGNORED`: a supplied generic glossary term map was not applied (DeepL only applies a
|
|
125
|
-
* native glossary id, never a term map).
|
|
135
|
+
* native glossary id, never a term map, and Google Cloud Translation Basic supports no glossary).
|
|
126
136
|
* - `PLACEHOLDER_UNSUPPORTED`: at least one placeholder- or ICU-bearing entry was left untranslated
|
|
127
137
|
* because the provider cannot preserve those tokens; such entries are withheld (absent from the
|
|
128
138
|
* result maps) rather than sent to the provider and mangled.
|
|
@@ -141,24 +151,65 @@ interface ProviderNotice {
|
|
|
141
151
|
}
|
|
142
152
|
/**
|
|
143
153
|
* Stable codes for a derived, per-key "needs review" signal. This is verbatra's own computed
|
|
144
|
-
* assessment, never a raw model self-score:
|
|
145
|
-
* values, and `PROVIDER_DEGRADED`
|
|
154
|
+
* assessment, never a raw model self-score: five are recomputable from plain source and translated
|
|
155
|
+
* values, and `FUZZY_CACHE_REUSE` and `PROVIDER_DEGRADED` are layered on afterwards, the first by
|
|
156
|
+
* the SDK from where the value came, the second from the batch's notices.
|
|
146
157
|
*
|
|
147
158
|
* - `LENGTH_RATIO_OUTLIER`: the translated value's length is far shorter or longer than the source's.
|
|
148
|
-
* Only considered once the trimmed source is long enough for the ratio to mean anything.
|
|
159
|
+
* Only considered once the trimmed source is long enough for the ratio to mean anything. Relative
|
|
160
|
+
* to the source, and measured in UTF-16 code units; contrast `MAX_LENGTH_EXCEEDED`.
|
|
161
|
+
* - `MAX_LENGTH_EXCEEDED`: the translated value is longer than the absolute budget configured for
|
|
162
|
+
* its key. Measured in grapheme clusters (user-perceived characters), on the value exactly as
|
|
163
|
+
* written with no trimming, and the comparison is inclusive, so a value of exactly the budget is
|
|
164
|
+
* not flagged. A key with no configured budget is never flagged. Advisory only: an over-budget
|
|
165
|
+
* value is still written to the locale file and still recorded in the lock file, because a
|
|
166
|
+
* correct translation that overruns a layout budget is more useful than no translation at all.
|
|
149
167
|
* - `EQUALS_SOURCE`: the translated value equals the source value once both are trimmed, so a
|
|
150
168
|
* difference in leading or trailing whitespace alone still counts as equal. Also requires that
|
|
151
169
|
* the locales differ and that the value contains at least one letter (so a bare symbol or number
|
|
152
170
|
* is not flagged).
|
|
153
171
|
* - `GLOSSARY_TERM_MISSED`: a configured glossary source term appeared in the source but its target
|
|
154
|
-
* term did not appear in the translation.
|
|
172
|
+
* term did not appear in the translation. Matching is case-insensitive, and the two sides are
|
|
173
|
+
* held to deliberately different standards. The source side requires a whole-word occurrence, so
|
|
174
|
+
* the term "AI" is not found inside "Airport" and cannot raise an expectation the translator was
|
|
175
|
+
* never given; a source term whose edge character belongs to a script written without word
|
|
176
|
+
* separators (Han, kana, Thai and similar) has no boundary to anchor to and falls back to
|
|
177
|
+
* containment. The target side requires containment only, because a translated term legitimately
|
|
178
|
+
* fuses with the text around it: German compounds ("Benutzerkonto"), Korean particles ("계정을")
|
|
179
|
+
* and Japanese loanwords all carry the term with no boundary around it, and demanding one there
|
|
180
|
+
* would flag correct translations in most languages a glossary is used for.
|
|
155
181
|
* - `INTEGRITY_REORDERED`: the placeholder set matched but landed in a different order.
|
|
156
182
|
* - `PROVIDER_DEGRADED`: the batch this key came from carried a `FORMALITY_DOWNGRADED` or
|
|
157
183
|
* `GLOSSARY_IGNORED` notice, either of which can silently change wording. A
|
|
158
184
|
* `PLACEHOLDER_UNSUPPORTED` notice does not raise it, since the affected entries are withheld
|
|
159
185
|
* rather than degraded.
|
|
186
|
+
* - `FUZZY_CACHE_REUSE`: the value was not translated for this source text at all. It was reused
|
|
187
|
+
* from the translation memory for an earlier, near-identical source that has since been edited,
|
|
188
|
+
* so it is a translation of text that is no longer the source text. Similarity is measured in
|
|
189
|
+
* characters and meaning is not a function of character distance: a dropped negation, an
|
|
190
|
+
* inverted modal or a swapped proper noun are all small edits with large consequences, and no
|
|
191
|
+
* threshold separates them from a typo fix. Every such reuse therefore carries this reason, on
|
|
192
|
+
* every run, regardless of score.
|
|
193
|
+
*
|
|
194
|
+
* This tuple is the single source of truth for the set. {@link ReviewReasonCode} is derived from
|
|
195
|
+
* it, so build any runtime validator or exhaustive lookup from this value rather than retyping the
|
|
196
|
+
* members; a hand-copied list silently falls behind the next addition.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```ts
|
|
200
|
+
* import { REVIEW_REASON_CODES } from "@verbatra/sdk";
|
|
201
|
+
* import { z } from "zod";
|
|
202
|
+
*
|
|
203
|
+
* const reasonSchema = z.enum(REVIEW_REASON_CODES);
|
|
204
|
+
* ```
|
|
160
205
|
*/
|
|
161
|
-
|
|
206
|
+
declare const REVIEW_REASON_CODES: readonly ["LENGTH_RATIO_OUTLIER", "MAX_LENGTH_EXCEEDED", "EQUALS_SOURCE", "GLOSSARY_TERM_MISSED", "INTEGRITY_REORDERED", "PROVIDER_DEGRADED", "FUZZY_CACHE_REUSE"];
|
|
207
|
+
/**
|
|
208
|
+
* One of {@link REVIEW_REASON_CODES}. The union is derived from that tuple rather than written out
|
|
209
|
+
* again, so a code can only be added in one place and every consumer that builds a runtime schema
|
|
210
|
+
* or an exhaustive map from the tuple stays in step automatically.
|
|
211
|
+
*/
|
|
212
|
+
type ReviewReasonCode = (typeof REVIEW_REASON_CODES)[number];
|
|
162
213
|
/** A key flagged for human review, carrying every reason code that applies. */
|
|
163
214
|
interface ReviewFlag {
|
|
164
215
|
/** Always "review": the flag exists only for flagged keys, so there is no "ok" member. */
|
|
@@ -175,13 +226,13 @@ interface TranslateResult {
|
|
|
175
226
|
*/
|
|
176
227
|
readonly values: ReadonlyMap<string, string>;
|
|
177
228
|
/** The placeholder-integrity outcome for each key (source vs translated placeholder sets). */
|
|
178
|
-
readonly integrity: ReadonlyMap<string, PlaceholderIntegrityResult$
|
|
229
|
+
readonly integrity: ReadonlyMap<string, PlaceholderIntegrityResult$2>;
|
|
179
230
|
/** Token usage when the provider reports it; absent for token-less providers. */
|
|
180
231
|
readonly usage?: Usage;
|
|
181
232
|
/**
|
|
182
233
|
* Graceful-degradation notices for this batch. Every provider populates this as a present array:
|
|
183
|
-
* DeepL
|
|
184
|
-
* report returns an empty array rather than omitting the field.
|
|
234
|
+
* DeepL and Google Cloud Translation report real notices (for example `GLOSSARY_IGNORED`); an LLM
|
|
235
|
+
* provider with nothing to report returns an empty array rather than omitting the field.
|
|
185
236
|
*/
|
|
186
237
|
readonly notices?: readonly ProviderNotice[];
|
|
187
238
|
/**
|
|
@@ -260,21 +311,26 @@ type GeminiModel = "gemini-2.5-flash" | "gemini-2.5-pro" | "gemma-4-26b-a4b-it"
|
|
|
260
311
|
type OpenAiModel = OpenAI.ChatModel;
|
|
261
312
|
|
|
262
313
|
/**
|
|
263
|
-
* Zod schema accepting exactly one of the
|
|
314
|
+
* Zod schema accepting exactly one of the fourteen source formats verbatra can read and write. Each
|
|
264
315
|
* member names the ecosystem it round-trips, not just a file extension, because several of them share
|
|
265
316
|
* one: `i18next-json` is i18next's nested JSON with its plural key suffixes, `vue-i18n-json` is Vue
|
|
266
317
|
* I18n's JSON with its pipe-separated plural values, `next-intl-json` is next-intl's ICU-message JSON,
|
|
267
|
-
* and `ngx-translate-json` is ngx-translate's nested JSON. The remaining
|
|
318
|
+
* and `ngx-translate-json` is ngx-translate's nested JSON. The remaining ten are `xliff` (the XLIFF
|
|
268
319
|
* interchange XML), `yaml` (plain nested YAML), `arb` (Flutter's Application Resource Bundle),
|
|
269
320
|
* `properties` (Java and Spring `.properties` files), `apple-strings` (Apple's flat `.strings`
|
|
270
321
|
* localization format for iOS and macOS), `apple-xcstrings` (Apple's Xcode String Catalog, a single
|
|
271
322
|
* JSON document holding every locale of a `.xcstrings` catalogue together), `android-xml` (Android's
|
|
272
323
|
* `res/values/strings.xml` resource format, including `<plurals>` and `translatable="false"` entries),
|
|
273
|
-
*
|
|
274
|
-
* `msgid_plural`/`msgstr[n]` plural forms keyed by the file's own `Plural-Forms` index count)
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
324
|
+
* `gettext-po` (GNU gettext's `.po` and `.pot` catalogs, including `msgctxt` disambiguation and
|
|
325
|
+
* `msgid_plural`/`msgstr[n]` plural forms keyed by the file's own `Plural-Forms` index count),
|
|
326
|
+
* `ini` (classic INI configuration files, one level of `[section]` headers over `key=value` lines,
|
|
327
|
+
* whose entries are addressed as `section.key`), and `resx` (.NET's XML resource format; its `<data>`
|
|
328
|
+
* elements carrying a `type` or `mimetype` attribute, and its designer metadata names, are preserved
|
|
329
|
+
* untouched rather than translated).
|
|
330
|
+
*
|
|
331
|
+
* The set is closed: adding a built-in format means adding a member here and an adapter that claims
|
|
332
|
+
* it. A format shipped by a package outside verbatra is named by a `custom:` identifier instead and
|
|
333
|
+
* never joins this set; see `FormatId`.
|
|
278
334
|
*/
|
|
279
335
|
declare const supportedFormatSchema$1: z.ZodEnum<{
|
|
280
336
|
"i18next-json": "i18next-json";
|
|
@@ -289,206 +345,1307 @@ declare const supportedFormatSchema$1: z.ZodEnum<{
|
|
|
289
345
|
"apple-xcstrings": "apple-xcstrings";
|
|
290
346
|
"android-xml": "android-xml";
|
|
291
347
|
"gettext-po": "gettext-po";
|
|
348
|
+
ini: "ini";
|
|
349
|
+
resx: "resx";
|
|
292
350
|
}>;
|
|
293
|
-
/** One of the
|
|
351
|
+
/** One of the fourteen supported source formats; the inferred type of {@link supportedFormatSchema}. */
|
|
294
352
|
type SupportedFormat$1 = z.infer<typeof supportedFormatSchema$1>;
|
|
295
|
-
|
|
296
353
|
/**
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
354
|
+
* A format identifier claimed by an adapter that ships outside verbatra: the reserved
|
|
355
|
+
* `custom:` prefix followed by a name of lowercase ASCII letters and digits in hyphen-separated
|
|
356
|
+
* segments, for example `custom:my-format`. The type itself accepts any text after the prefix;
|
|
357
|
+
* {@link isCustomFormatId} and adapter registration enforce the name's shape at runtime.
|
|
358
|
+
*
|
|
359
|
+
* No built-in format name contains a colon, so a third-party identifier can never shadow a
|
|
360
|
+
* built-in one.
|
|
300
361
|
*/
|
|
301
|
-
type
|
|
302
|
-
/** The file was read in full. */
|
|
303
|
-
readonly kind: "ok";
|
|
304
|
-
/** The file's content, decoded as UTF-8. */
|
|
305
|
-
readonly content: string;
|
|
306
|
-
} | {
|
|
307
|
-
/** No readable file exists at the path. */
|
|
308
|
-
readonly kind: "missing";
|
|
309
|
-
} | {
|
|
310
|
-
/** The file exists but exceeds the requested byte limit, so nothing was read. */
|
|
311
|
-
readonly kind: "too-large";
|
|
312
|
-
};
|
|
362
|
+
type CustomFormatId$1 = `custom:${string}`;
|
|
313
363
|
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
364
|
+
* Narrow a format identifier to a third-party one.
|
|
365
|
+
*
|
|
366
|
+
* @param format - The identifier to classify.
|
|
367
|
+
* @returns True when the identifier is a well-formed `custom:` identifier. A built-in format
|
|
368
|
+
* name always returns false, and so does a `custom:` identifier whose name is empty, holds any
|
|
369
|
+
* character other than a lowercase ASCII letter, a digit, or a hyphen, or has a leading,
|
|
370
|
+
* trailing, or doubled hyphen.
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```ts
|
|
374
|
+
* isCustomFormatId("custom:toml"); // true
|
|
375
|
+
* isCustomFormatId("i18next-json"); // false
|
|
376
|
+
* ```
|
|
316
377
|
*/
|
|
317
|
-
|
|
318
|
-
/** The file was read in full. */
|
|
319
|
-
readonly kind: "ok";
|
|
320
|
-
/** The file's raw bytes. */
|
|
321
|
-
readonly bytes: Uint8Array;
|
|
322
|
-
} | {
|
|
323
|
-
/** No readable file exists at the path. */
|
|
324
|
-
readonly kind: "missing";
|
|
325
|
-
} | {
|
|
326
|
-
/** The file exists but exceeds the requested byte limit, so nothing was read. */
|
|
327
|
-
readonly kind: "too-large";
|
|
328
|
-
};
|
|
378
|
+
declare function isCustomFormatId(format: string): format is CustomFormatId$1;
|
|
329
379
|
/**
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
* embedding application can back part of a project with something other than a local disk.
|
|
380
|
+
* Any format verbatra can be asked to handle: one of the built-in {@link SupportedFormat}
|
|
381
|
+
* members, or a third-party {@link CustomFormatId}.
|
|
333
382
|
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
|
|
338
|
-
|
|
383
|
+
* A built-in name is checked against the closed set at compile time. A `custom:` identifier is
|
|
384
|
+
* only checked for shape, because the adapter behind it lives outside this package; if no
|
|
385
|
+
* registered adapter claims it, the run fails with a structured error naming the format.
|
|
386
|
+
*/
|
|
387
|
+
type FormatId$1 = SupportedFormat$1 | CustomFormatId$1;
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* A single, format-neutral translation unit. Placeholders are supplied already extracted; core never
|
|
391
|
+
* derives them from the value, so an adapter that reads a file is the one authority on what counts as
|
|
392
|
+
* a placeholder in that format.
|
|
339
393
|
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
394
|
+
* The fields are: `key`, the entry's identifier within its namespace, non-empty and already flattened
|
|
395
|
+
* to the dotted path an adapter produced; `namespace`, the namespace the entry belongs to, empty when
|
|
396
|
+
* the format has no namespacing; `value`, the translatable string itself, which is untrusted input and
|
|
397
|
+
* may be empty; `description` and `meaning`, both optional free-form context that a provider is told to
|
|
398
|
+
* use only for disambiguation and never to translate or echo back; `placeholders`, the format's
|
|
399
|
+
* placeholder tokens in document order, used for the output integrity check and to decide which entries
|
|
400
|
+
* a placeholder-blind provider must withhold; and `isPlural`, whether the entry carries plural forms
|
|
401
|
+
* (an i18next plural key suffix, a vue-i18n pipe-separated value, or an ICU plural), which the adapter
|
|
402
|
+
* decides and which feeds the content hash.
|
|
345
403
|
*/
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
404
|
+
declare const translationEntrySchema$1: z.ZodObject<{
|
|
405
|
+
key: z.ZodString;
|
|
406
|
+
namespace: z.ZodString;
|
|
407
|
+
value: z.ZodString;
|
|
408
|
+
description: z.ZodOptional<z.ZodString>;
|
|
409
|
+
meaning: z.ZodOptional<z.ZodString>;
|
|
410
|
+
placeholders: z.ZodReadonly<z.ZodArray<z.ZodString>>;
|
|
411
|
+
isPlural: z.ZodBoolean;
|
|
412
|
+
}, z.core.$strip>;
|
|
413
|
+
/**
|
|
414
|
+
* The validated shape of one translation unit; the inferred type of {@link translationEntrySchema},
|
|
415
|
+
* made deeply readonly. See that schema for what each field carries.
|
|
416
|
+
*/
|
|
417
|
+
type TranslationEntry$1 = Readonly<z.infer<typeof translationEntrySchema$1>>;
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* All translation entries for one locale and namespace, keyed by entry key. This is the neutral
|
|
421
|
+
* intermediate representation every adapter reads into and writes back out of, so nothing downstream
|
|
422
|
+
* of an adapter parses a format's own syntax.
|
|
423
|
+
*/
|
|
424
|
+
interface LocaleResource$1 {
|
|
425
|
+
/** The locale these entries belong to (for example, "en" or "de"). */
|
|
426
|
+
readonly locale: string;
|
|
427
|
+
/** The namespace these entries belong to; empty when the format has no namespacing. */
|
|
428
|
+
readonly namespace: string;
|
|
429
|
+
/** The source format the resource came from, retained for round-trip fidelity on write. */
|
|
430
|
+
readonly format: FormatId$1;
|
|
431
|
+
/** Entries addressable by key, in the order the source file defined them. */
|
|
432
|
+
readonly entries: ReadonlyMap<string, TranslationEntry$1>;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** One distinct translated value inside an {@link InconsistencyGroup}, with every key holding it. */
|
|
436
|
+
interface InconsistentTranslation {
|
|
357
437
|
/**
|
|
358
|
-
*
|
|
359
|
-
*
|
|
438
|
+
* The translated value in its compared form: Unicode NFC, line endings folded to `\n`, and
|
|
439
|
+
* leading and trailing whitespace trimmed. Internal whitespace and letter case are kept.
|
|
360
440
|
*/
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
441
|
+
readonly value: string;
|
|
442
|
+
/** Every key in this locale holding this value, sorted by UTF-16 code unit. */
|
|
443
|
+
readonly keys: readonly string[];
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* One source string that is translated more than one way within a single target locale. Keys are
|
|
447
|
+
* grouped only when their source value, description, meaning, plural flag, plural form, and context
|
|
448
|
+
* all agree, so a string that deliberately carries different disambiguation metadata is never
|
|
449
|
+
* reported.
|
|
450
|
+
*/
|
|
451
|
+
interface InconsistencyGroup {
|
|
452
|
+
/** The shared source value in its compared form (see {@link InconsistentTranslation.value}). */
|
|
453
|
+
readonly source: string;
|
|
454
|
+
/** The disambiguation context every key in the group shares, when the format has one. */
|
|
455
|
+
readonly context?: string;
|
|
456
|
+
/** The description every key in the group shares, when there is one. */
|
|
457
|
+
readonly description?: string;
|
|
458
|
+
/** The meaning every key in the group shares, when there is one. */
|
|
459
|
+
readonly meaning?: string;
|
|
460
|
+
/** Whether the grouped source entries carry plural forms. */
|
|
461
|
+
readonly isPlural: boolean;
|
|
462
|
+
/**
|
|
463
|
+
* The plural form every key in the group shares (a CLDR category such as `one`, or a format's own
|
|
464
|
+
* plural index), when the format encodes one per key. Different forms of one key are never grouped.
|
|
465
|
+
*/
|
|
466
|
+
readonly pluralForm?: string;
|
|
467
|
+
/** Every distinct translation, at least two, sorted by value in UTF-16 code unit order. */
|
|
468
|
+
readonly translations: readonly InconsistentTranslation[];
|
|
366
469
|
}
|
|
367
470
|
|
|
368
471
|
/**
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
* Entries are keyed by configuration fingerprint, then locale, then source-content hash. The
|
|
374
|
-
* fingerprint layer means that changing the provider, model, tone, or glossary does not silently
|
|
375
|
-
* reuse translations produced under the old settings; those entries simply stop matching.
|
|
472
|
+
* Result of comparing source placeholders against translated-output placeholders as MULTISETS
|
|
473
|
+
* (counts matter, so dropping one of two occurrences of the same token is still a miss). Always
|
|
474
|
+
* returned, never thrown: a mismatch is data the caller decides what to do about, not a failure.
|
|
376
475
|
*/
|
|
377
|
-
interface
|
|
378
|
-
/**
|
|
379
|
-
readonly
|
|
380
|
-
/**
|
|
381
|
-
readonly
|
|
476
|
+
interface PlaceholderIntegrityResult$1 {
|
|
477
|
+
/** True when nothing is missing or extra: the multisets match, regardless of order. */
|
|
478
|
+
readonly matches: boolean;
|
|
479
|
+
/** Occurrences present in source but absent from the translation, one per dropped occurrence. Sorted. */
|
|
480
|
+
readonly missing: readonly string[];
|
|
481
|
+
/** Occurrences present in the translation but absent from source, one per surplus occurrence. Sorted. */
|
|
482
|
+
readonly extra: readonly string[];
|
|
483
|
+
/** Same multiset present in both, but in a different order. Never true alongside a mismatch. */
|
|
484
|
+
readonly reordered: boolean;
|
|
382
485
|
}
|
|
383
486
|
|
|
384
487
|
/**
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
* the cache as purely local. Deleting it is always safe: the next run simply repays for the strings
|
|
388
|
-
* it would have reused.
|
|
488
|
+
* Every translation framework a source scan can target, as a tuple; {@link SourceFramework} is
|
|
489
|
+
* derived from it. It holds `i18next` alone today, the one framework an extractor ships for.
|
|
389
490
|
*/
|
|
390
|
-
declare const
|
|
491
|
+
declare const SOURCE_FRAMEWORKS: readonly ["i18next"];
|
|
492
|
+
/**
|
|
493
|
+
* The translation framework whose call sites a source scan looks for. The set is closed: a
|
|
494
|
+
* framework is only nameable here once an extractor ships for it, so a configured value that
|
|
495
|
+
* resolves to nothing cannot exist.
|
|
496
|
+
*
|
|
497
|
+
* `i18next` covers the `t(...)`, `$t(...)`, and `<object>.t(...)` shapes, each also through an
|
|
498
|
+
* optional call (`t?.(...)`) and an explicit type-argument list (`t<string>(...)`), with the key as
|
|
499
|
+
* the first argument and an optional default as either the second argument or a `defaultValue`
|
|
500
|
+
* field on the options object. A namespace-qualified key such as `t("common:nav.home")` is
|
|
501
|
+
* reported as a dynamic call site rather than extracted, because one configuration addresses one
|
|
502
|
+
* catalog file.
|
|
503
|
+
*/
|
|
504
|
+
type SourceFramework = (typeof SOURCE_FRAMEWORKS)[number];
|
|
391
505
|
|
|
506
|
+
/** One application source file, already read, as it is handed to a {@link SourceExtractor}. */
|
|
507
|
+
interface SourceFile {
|
|
508
|
+
/** The file's absolute path, used only for reporting. */
|
|
509
|
+
readonly path: string;
|
|
510
|
+
/** The file's full text. It is untrusted input and is never echoed into a result. */
|
|
511
|
+
readonly content: string;
|
|
512
|
+
}
|
|
513
|
+
/** One translation call site whose key argument was a static string. */
|
|
514
|
+
interface ExtractedCallSite {
|
|
515
|
+
/** The key exactly as the call site spelled it. */
|
|
516
|
+
readonly key: string;
|
|
517
|
+
/** The default value the call site supplied, if any. Absent when the call named only a key. */
|
|
518
|
+
readonly defaultValue?: string;
|
|
519
|
+
/** The one-based line the call site sits on. */
|
|
520
|
+
readonly line: number;
|
|
521
|
+
}
|
|
392
522
|
/**
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
* {@link ProviderConfig}.
|
|
523
|
+
* One translation call site whose key argument could not be resolved statically: a variable, a
|
|
524
|
+
* member expression, or a template literal carrying an expression. It is reported rather than
|
|
525
|
+
* guessed at, so no invented key ever reaches a catalog.
|
|
397
526
|
*/
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
maxOutputTokens: z.ZodNumber;
|
|
410
|
-
requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
411
|
-
}, z.core.$strict>;
|
|
412
|
-
}, z.core.$strip>, z.ZodObject<{
|
|
413
|
-
id: z.ZodLiteral<"gemini">;
|
|
414
|
-
options: z.ZodObject<{
|
|
415
|
-
model: z.ZodString;
|
|
416
|
-
maxOutputTokens: z.ZodNumber;
|
|
417
|
-
requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
418
|
-
}, z.core.$strict>;
|
|
419
|
-
}, z.core.$strip>, z.ZodObject<{
|
|
420
|
-
id: z.ZodLiteral<"deepl">;
|
|
421
|
-
options: z.ZodObject<{
|
|
422
|
-
glossaryId: z.ZodOptional<z.ZodString>;
|
|
423
|
-
requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
424
|
-
}, z.core.$strict>;
|
|
425
|
-
}, z.core.$strip>, z.ZodObject<{
|
|
426
|
-
id: z.ZodLiteral<"google-translate">;
|
|
427
|
-
options: z.ZodObject<{
|
|
428
|
-
requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
429
|
-
}, z.core.$strict>;
|
|
430
|
-
}, z.core.$strip>, z.ZodObject<{
|
|
431
|
-
id: z.ZodLiteral<"openai-compatible">;
|
|
432
|
-
options: z.ZodObject<{
|
|
433
|
-
baseUrl: z.ZodURL;
|
|
434
|
-
model: z.ZodString;
|
|
435
|
-
maxOutputTokens: z.ZodNumber;
|
|
436
|
-
apiKeyEnvVar: z.ZodOptional<z.ZodString>;
|
|
437
|
-
requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
438
|
-
}, z.core.$strict>;
|
|
439
|
-
}, z.core.$strip>], "id">;
|
|
527
|
+
interface DynamicCallSite {
|
|
528
|
+
/** The one-based line the call site sits on. */
|
|
529
|
+
readonly line: number;
|
|
530
|
+
}
|
|
531
|
+
/** A catalog key a file names statically, wherever it names it. */
|
|
532
|
+
interface ReferencedKeySite {
|
|
533
|
+
/** The key as the source can reach it. */
|
|
534
|
+
readonly key: string;
|
|
535
|
+
/** The one-based line it is named on. */
|
|
536
|
+
readonly line: number;
|
|
537
|
+
}
|
|
440
538
|
/**
|
|
441
|
-
*
|
|
442
|
-
* carries exactly the options it supports and nothing else.
|
|
443
|
-
*
|
|
444
|
-
* No variant has a field for an API key. Keys are read from the environment by the provider itself
|
|
445
|
-
* (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `DEEPL_API_KEY`, or
|
|
446
|
-
* `GOOGLE_TRANSLATE_API_KEY`), which is what keeps them out of config files and out of version
|
|
447
|
-
* control. The `openai-compatible` variant may name a different environment variable through
|
|
448
|
-
* `apiKeyEnvVar`, but still never holds the key itself.
|
|
539
|
+
* A key argument with a static head and a dynamic rest, such as the template literal `nav.${page}`.
|
|
449
540
|
*/
|
|
450
|
-
|
|
541
|
+
interface KeyPrefixSite {
|
|
542
|
+
/** The static head every key this site can reach starts with. */
|
|
543
|
+
readonly prefix: string;
|
|
544
|
+
/** The one-based line the site sits on. */
|
|
545
|
+
readonly line: number;
|
|
546
|
+
}
|
|
451
547
|
/**
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
548
|
+
* Why a place in the source reaches keys a static reading cannot bound.
|
|
549
|
+
*
|
|
550
|
+
* - `dynamic-key-prefix`: a key prefix (a `keyPrefix` option or a `getFixedT` prefix argument) is
|
|
551
|
+
* not a static string.
|
|
552
|
+
* - `trans-without-key`: a `Trans` element carries no static `i18nKey`, so its key is its children.
|
|
553
|
+
* - `aliased-translate-function`: the translate function is assigned somewhere its calls cannot be
|
|
554
|
+
* followed, such as an object property.
|
|
555
|
+
* - `translate-function-escapes`: a translate function bound from a recognised source appears
|
|
556
|
+
* somewhere other than a direct call, its own binding, a local alias, or the `t` attribute of a
|
|
557
|
+
* `Trans` or `Translation` element (for example as an argument, a property, an export, or a
|
|
558
|
+
* ternary operand), so other code can call it with keys this file never names.
|
|
559
|
+
* - `unrecognised-translate-source`: a translate source (`useTranslation`, `withTranslation`,
|
|
560
|
+
* `getFixedT`, a `Translation` render prop, a member `t`, or a `t` import) is used in a shape the
|
|
561
|
+
* scan does not recognise, so the keys and prefix the resulting function uses are unknown.
|
|
455
562
|
*/
|
|
456
|
-
type
|
|
457
|
-
|
|
563
|
+
type UnresolvedKeySiteReason = "dynamic-key-prefix" | "trans-without-key" | "aliased-translate-function" | "translate-function-escapes" | "unrecognised-translate-source";
|
|
564
|
+
/** A place whose reachable keys cannot be bounded statically. */
|
|
565
|
+
interface UnresolvedKeySite {
|
|
566
|
+
/** Why the keys it reaches are unknown. */
|
|
567
|
+
readonly reason: UnresolvedKeySiteReason;
|
|
568
|
+
/** The one-based line it sits on. */
|
|
569
|
+
readonly line: number;
|
|
570
|
+
}
|
|
571
|
+
/** Everything one file tells a caller about which catalog keys it can reach. */
|
|
572
|
+
interface KeyUsage {
|
|
573
|
+
/** Every static key the file names, including the extracted calls, in line order. */
|
|
574
|
+
readonly references: readonly ReferencedKeySite[];
|
|
575
|
+
/** Key arguments with no static part at all. */
|
|
576
|
+
readonly dynamic: readonly DynamicCallSite[];
|
|
577
|
+
/** Key arguments with a static head. */
|
|
578
|
+
readonly prefixes: readonly KeyPrefixSite[];
|
|
579
|
+
/** Places whose reachable keys cannot be bounded. */
|
|
580
|
+
readonly unresolved: readonly UnresolvedKeySite[];
|
|
581
|
+
}
|
|
582
|
+
/** Everything one {@link SourceExtractor} found in one file. */
|
|
583
|
+
interface FileExtraction {
|
|
584
|
+
/** Call sites whose key was a static string. */
|
|
585
|
+
readonly calls: readonly ExtractedCallSite[];
|
|
586
|
+
/** Call sites whose key could not be resolved statically. */
|
|
587
|
+
readonly dynamic: readonly DynamicCallSite[];
|
|
588
|
+
/**
|
|
589
|
+
* What the file tells a caller asking which catalog keys the source can reach, beyond the calls
|
|
590
|
+
* above: static keys named some other way, template keys with a static head, and constructs
|
|
591
|
+
* whose keys cannot be bounded. Absent for an extractor that does not report it, in which case
|
|
592
|
+
* the calls and dynamic sites above are all that is known.
|
|
593
|
+
*/
|
|
594
|
+
readonly usage?: KeyUsage;
|
|
595
|
+
/**
|
|
596
|
+
* Whether the file could not be read to its end, so everything above it is partial. An
|
|
597
|
+
* unterminated block comment or template literal abandons the rest of the file. In a file read as
|
|
598
|
+
* markup (`.tsx`, `.jsx`, `.js`), so does a JSX element that never closes or is closed by an
|
|
599
|
+
* enclosing element's tag, and a quoted string in code that runs into the end of its line. The
|
|
600
|
+
* scan records it as an `unparseable` diagnostic and carries on, rather than reporting a short
|
|
601
|
+
* result as if it were the whole file.
|
|
602
|
+
*/
|
|
603
|
+
readonly truncated?: boolean;
|
|
604
|
+
}
|
|
458
605
|
/**
|
|
459
|
-
* The
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
* own tooling.
|
|
463
|
-
*
|
|
464
|
-
* The object is strict, so an unrecognized key is an error rather than being silently ignored: a
|
|
465
|
-
* typo in a config file is reported instead of quietly doing nothing. The one exception is the
|
|
466
|
-
* optional `$schema` key, accepted so a JSON or YAML config can point an editor at the JSON Schema
|
|
467
|
-
* document the package ships as `@verbatra/sdk/config-schema.json`.
|
|
606
|
+
* The contract every framework's source extractor implements: given one file's text, report the
|
|
607
|
+
* translation call sites it contains. One implementation per framework, selected by the configured
|
|
608
|
+
* `extract.framework`.
|
|
468
609
|
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
* per-field check on `files.pattern`.
|
|
610
|
+
* It is pure: it reads no file, makes no network call, and holds no state between files. Supplying
|
|
611
|
+
* your own as `deps.createExtractor` is how you teach `extract` a call shape verbatra does not
|
|
612
|
+
* ship yet.
|
|
473
613
|
*/
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
614
|
+
interface SourceExtractor {
|
|
615
|
+
/** The framework whose call shapes this extractor knows. */
|
|
616
|
+
readonly framework: SourceFramework;
|
|
617
|
+
/** The file extensions this extractor can read, each including its leading dot. */
|
|
618
|
+
readonly extensions: readonly string[];
|
|
619
|
+
/**
|
|
620
|
+
* Find every translation call site in one file.
|
|
621
|
+
*
|
|
622
|
+
* @param file - The file's path and full text.
|
|
623
|
+
* @returns The static and dynamic call sites found, in document order.
|
|
624
|
+
*/
|
|
625
|
+
extract(file: SourceFile): FileExtraction;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** Where something was found: a source file and a line inside it. Never a file's contents. */
|
|
629
|
+
interface SourceLocation {
|
|
630
|
+
/** The source file, relative to the run's working directory, with forward slashes. */
|
|
631
|
+
readonly file: string;
|
|
632
|
+
/** The one-based line number. */
|
|
633
|
+
readonly line: number;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* One key found at two or more call sites that disagree on its default value. Neither value is
|
|
637
|
+
* written: resolving it last-write-wins would make the catalog depend on directory traversal order,
|
|
638
|
+
* so the disagreement is reported for a person to settle at the call sites.
|
|
639
|
+
*/
|
|
640
|
+
interface KeyConflict {
|
|
641
|
+
/** The key the call sites disagree about. */
|
|
642
|
+
readonly key: string;
|
|
643
|
+
/** Every distinct default value found, in the order the scan met them. */
|
|
644
|
+
readonly values: readonly string[];
|
|
645
|
+
/** Where each of those values was found, index-aligned with `values`. */
|
|
646
|
+
readonly locations: readonly SourceLocation[];
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Why a {@link ScanDiagnostic} was reported.
|
|
650
|
+
*
|
|
651
|
+
* - `unreadable`: the file could not be opened, or the path is not a regular file.
|
|
652
|
+
* - `too-large`: the file is larger than 2,000,000 bytes, so it was skipped without being read.
|
|
653
|
+
* - `unparseable`: the file was read, but the scan either failed on it outright, in which case
|
|
654
|
+
* nothing from it is reported, or could not follow it to its end, in which case what was found
|
|
655
|
+
* before that point is still reported.
|
|
656
|
+
* - `unreadable-directory`: a directory under a scan root could not be listed, so nothing beneath
|
|
657
|
+
* it was scanned.
|
|
658
|
+
*/
|
|
659
|
+
type ScanDiagnosticReason = "unreadable" | "too-large" | "unparseable" | "unreadable-directory";
|
|
660
|
+
/**
|
|
661
|
+
* One file or directory the scan could not read, or could not read to its end, reported as data so
|
|
662
|
+
* one bad file never aborts a run.
|
|
663
|
+
*/
|
|
664
|
+
interface ScanDiagnostic {
|
|
665
|
+
/** The file or directory, relative to the run's working directory, with forward slashes. */
|
|
666
|
+
readonly file: string;
|
|
667
|
+
/** Why it was skipped or only partly scanned. */
|
|
668
|
+
readonly reason: ScanDiagnosticReason;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* One hardcoded string literal that reads as user-facing text and never went through a translation
|
|
673
|
+
* call. It names where the literal sits and a bounded excerpt of it, never the surrounding source.
|
|
674
|
+
*/
|
|
675
|
+
interface LiteralFinding {
|
|
676
|
+
/** The source file, relative to the run's working directory, with forward slashes. */
|
|
677
|
+
readonly file: string;
|
|
678
|
+
/** The one-based line the literal starts on. */
|
|
679
|
+
readonly line: number;
|
|
680
|
+
/** The one-based column the literal starts at: its opening quote, or its first character of JSX text. */
|
|
681
|
+
readonly column: number;
|
|
682
|
+
/**
|
|
683
|
+
* The literal's text with whitespace collapsed, cut to at most 80 characters. In JSX text and a
|
|
684
|
+
* quoted JSX attribute value, character references such as `&` are decoded.
|
|
685
|
+
*/
|
|
686
|
+
readonly text: string;
|
|
687
|
+
/** Whether `text` was cut short. A cut excerpt ends in `...`. */
|
|
688
|
+
readonly truncated: boolean;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Why a literal that would have been reported was held back.
|
|
692
|
+
*
|
|
693
|
+
* - `directive`: a `verbatra-ignore-next-line` comment above it, or a `verbatra-ignore-line`
|
|
694
|
+
* comment on the same line. A next-line directive targets the next line that holds code, skipping
|
|
695
|
+
* blank and comment-only lines, and covers every literal that starts on that line plus every
|
|
696
|
+
* attribute of a JSX opening tag that starts there, even when the tag spans several lines. Text and
|
|
697
|
+
* nested elements on later lines are not covered.
|
|
698
|
+
* - `ignore-list`: its text is listed in `extract.literals.ignore`.
|
|
699
|
+
*/
|
|
700
|
+
type LiteralSuppressionReason = "directive" | "ignore-list";
|
|
701
|
+
/** A literal the scan found and did not report, kept so a suppression is visible rather than silent. */
|
|
702
|
+
interface SuppressedLiteral extends LiteralFinding {
|
|
703
|
+
/** Which suppression mechanism held it back. */
|
|
704
|
+
readonly reason: LiteralSuppressionReason;
|
|
705
|
+
}
|
|
706
|
+
/** Everything one untranslated-literal scan found across a project's source. */
|
|
707
|
+
interface LiteralScan {
|
|
708
|
+
/** How many source files were read and scanned. */
|
|
709
|
+
readonly scannedFiles: number;
|
|
710
|
+
/** Literals that look user-facing and were not suppressed, in file then document order. */
|
|
711
|
+
readonly findings: readonly LiteralFinding[];
|
|
712
|
+
/** Literals that were found but suppressed, each with the reason. */
|
|
713
|
+
readonly suppressed: readonly SuppressedLiteral[];
|
|
714
|
+
/**
|
|
715
|
+
* Files and directories the scan could not read to the end. A file listed here was not fully
|
|
716
|
+
* checked, so a scan with any diagnostic is never a clean one.
|
|
717
|
+
*/
|
|
718
|
+
readonly diagnostics: readonly ScanDiagnostic[];
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Zod schema accepting exactly one of the fourteen source formats verbatra can read and write. Each
|
|
723
|
+
* member names the ecosystem it round-trips, not just a file extension, because several of them share
|
|
724
|
+
* one: `i18next-json` is i18next's nested JSON with its plural key suffixes, `vue-i18n-json` is Vue
|
|
725
|
+
* I18n's JSON with its pipe-separated plural values, `next-intl-json` is next-intl's ICU-message JSON,
|
|
726
|
+
* and `ngx-translate-json` is ngx-translate's nested JSON. The remaining ten are `xliff` (the XLIFF
|
|
727
|
+
* interchange XML), `yaml` (plain nested YAML), `arb` (Flutter's Application Resource Bundle),
|
|
728
|
+
* `properties` (Java and Spring `.properties` files), `apple-strings` (Apple's flat `.strings`
|
|
729
|
+
* localization format for iOS and macOS), `apple-xcstrings` (Apple's Xcode String Catalog, a single
|
|
730
|
+
* JSON document holding every locale of a `.xcstrings` catalogue together), `android-xml` (Android's
|
|
731
|
+
* `res/values/strings.xml` resource format, including `<plurals>` and `translatable="false"` entries),
|
|
732
|
+
* `gettext-po` (GNU gettext's `.po` and `.pot` catalogs, including `msgctxt` disambiguation and
|
|
733
|
+
* `msgid_plural`/`msgstr[n]` plural forms keyed by the file's own `Plural-Forms` index count),
|
|
734
|
+
* `ini` (classic INI configuration files, one level of `[section]` headers over `key=value` lines,
|
|
735
|
+
* whose entries are addressed as `section.key`), and `resx` (.NET's XML resource format; its `<data>`
|
|
736
|
+
* elements carrying a `type` or `mimetype` attribute, and its designer metadata names, are preserved
|
|
737
|
+
* untouched rather than translated).
|
|
738
|
+
*
|
|
739
|
+
* The set is closed: adding a built-in format means adding a member here and an adapter that claims
|
|
740
|
+
* it. A format shipped by a package outside verbatra is named by a `custom:` identifier instead and
|
|
741
|
+
* never joins this set; see `FormatId`.
|
|
742
|
+
*/
|
|
743
|
+
declare const supportedFormatSchema: z.ZodEnum<{
|
|
744
|
+
"i18next-json": "i18next-json";
|
|
745
|
+
"vue-i18n-json": "vue-i18n-json";
|
|
746
|
+
"next-intl-json": "next-intl-json";
|
|
747
|
+
"ngx-translate-json": "ngx-translate-json";
|
|
748
|
+
xliff: "xliff";
|
|
749
|
+
yaml: "yaml";
|
|
750
|
+
arb: "arb";
|
|
751
|
+
properties: "properties";
|
|
752
|
+
"apple-strings": "apple-strings";
|
|
753
|
+
"apple-xcstrings": "apple-xcstrings";
|
|
754
|
+
"android-xml": "android-xml";
|
|
755
|
+
"gettext-po": "gettext-po";
|
|
756
|
+
ini: "ini";
|
|
757
|
+
resx: "resx";
|
|
758
|
+
}>;
|
|
759
|
+
/** One of the fourteen supported source formats; the inferred type of {@link supportedFormatSchema}. */
|
|
760
|
+
type SupportedFormat = z.infer<typeof supportedFormatSchema>;
|
|
761
|
+
/**
|
|
762
|
+
* A format identifier claimed by an adapter that ships outside verbatra: the reserved
|
|
763
|
+
* `custom:` prefix followed by a name of lowercase ASCII letters and digits in hyphen-separated
|
|
764
|
+
* segments, for example `custom:my-format`. The type itself accepts any text after the prefix;
|
|
765
|
+
* {@link isCustomFormatId} and adapter registration enforce the name's shape at runtime.
|
|
766
|
+
*
|
|
767
|
+
* No built-in format name contains a colon, so a third-party identifier can never shadow a
|
|
768
|
+
* built-in one.
|
|
769
|
+
*/
|
|
770
|
+
type CustomFormatId = `custom:${string}`;
|
|
771
|
+
/**
|
|
772
|
+
* Any format verbatra can be asked to handle: one of the built-in {@link SupportedFormat}
|
|
773
|
+
* members, or a third-party {@link CustomFormatId}.
|
|
774
|
+
*
|
|
775
|
+
* A built-in name is checked against the closed set at compile time. A `custom:` identifier is
|
|
776
|
+
* only checked for shape, because the adapter behind it lives outside this package; if no
|
|
777
|
+
* registered adapter claims it, the run fails with a structured error naming the format.
|
|
778
|
+
*/
|
|
779
|
+
type FormatId = SupportedFormat | CustomFormatId;
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* A single, format-neutral translation unit. Placeholders are supplied already extracted; core never
|
|
783
|
+
* derives them from the value, so an adapter that reads a file is the one authority on what counts as
|
|
784
|
+
* a placeholder in that format.
|
|
785
|
+
*
|
|
786
|
+
* The fields are: `key`, the entry's identifier within its namespace, non-empty and already flattened
|
|
787
|
+
* to the dotted path an adapter produced; `namespace`, the namespace the entry belongs to, empty when
|
|
788
|
+
* the format has no namespacing; `value`, the translatable string itself, which is untrusted input and
|
|
789
|
+
* may be empty; `description` and `meaning`, both optional free-form context that a provider is told to
|
|
790
|
+
* use only for disambiguation and never to translate or echo back; `placeholders`, the format's
|
|
791
|
+
* placeholder tokens in document order, used for the output integrity check and to decide which entries
|
|
792
|
+
* a placeholder-blind provider must withhold; and `isPlural`, whether the entry carries plural forms
|
|
793
|
+
* (an i18next plural key suffix, a vue-i18n pipe-separated value, or an ICU plural), which the adapter
|
|
794
|
+
* decides and which feeds the content hash.
|
|
795
|
+
*/
|
|
796
|
+
declare const translationEntrySchema: z.ZodObject<{
|
|
797
|
+
key: z.ZodString;
|
|
798
|
+
namespace: z.ZodString;
|
|
799
|
+
value: z.ZodString;
|
|
800
|
+
description: z.ZodOptional<z.ZodString>;
|
|
801
|
+
meaning: z.ZodOptional<z.ZodString>;
|
|
802
|
+
placeholders: z.ZodReadonly<z.ZodArray<z.ZodString>>;
|
|
803
|
+
isPlural: z.ZodBoolean;
|
|
804
|
+
}, z.core.$strip>;
|
|
805
|
+
/**
|
|
806
|
+
* The validated shape of one translation unit; the inferred type of {@link translationEntrySchema},
|
|
807
|
+
* made deeply readonly. See that schema for what each field carries.
|
|
808
|
+
*/
|
|
809
|
+
type TranslationEntry = Readonly<z.infer<typeof translationEntrySchema>>;
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* All translation entries for one locale and namespace, keyed by entry key. This is the neutral
|
|
813
|
+
* intermediate representation every adapter reads into and writes back out of, so nothing downstream
|
|
814
|
+
* of an adapter parses a format's own syntax.
|
|
815
|
+
*/
|
|
816
|
+
interface LocaleResource {
|
|
817
|
+
/** The locale these entries belong to (for example, "en" or "de"). */
|
|
818
|
+
readonly locale: string;
|
|
819
|
+
/** The namespace these entries belong to; empty when the format has no namespacing. */
|
|
820
|
+
readonly namespace: string;
|
|
821
|
+
/** The source format the resource came from, retained for round-trip fidelity on write. */
|
|
822
|
+
readonly format: FormatId;
|
|
823
|
+
/** Entries addressable by key, in the order the source file defined them. */
|
|
824
|
+
readonly entries: ReadonlyMap<string, TranslationEntry>;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Result of comparing source placeholders against translated-output placeholders as MULTISETS
|
|
829
|
+
* (counts matter, so dropping one of two occurrences of the same token is still a miss). Always
|
|
830
|
+
* returned, never thrown: a mismatch is data the caller decides what to do about, not a failure.
|
|
831
|
+
*/
|
|
832
|
+
interface PlaceholderIntegrityResult {
|
|
833
|
+
/** True when nothing is missing or extra: the multisets match, regardless of order. */
|
|
834
|
+
readonly matches: boolean;
|
|
835
|
+
/** Occurrences present in source but absent from the translation, one per dropped occurrence. Sorted. */
|
|
836
|
+
readonly missing: readonly string[];
|
|
837
|
+
/** Occurrences present in the translation but absent from source, one per surplus occurrence. Sorted. */
|
|
838
|
+
readonly extra: readonly string[];
|
|
839
|
+
/** Same multiset present in both, but in a different order. Never true alongside a mismatch. */
|
|
840
|
+
readonly reordered: boolean;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* The result of reading a file into core's intermediate representation. The two diagnostic lists are
|
|
845
|
+
* reported as data rather than thrown, so one bad leaf never fails a whole read.
|
|
846
|
+
*/
|
|
847
|
+
interface ReadResult {
|
|
848
|
+
/** The parsed locale resource. */
|
|
849
|
+
readonly resource: LocaleResource;
|
|
850
|
+
/**
|
|
851
|
+
* Keys whose values are invalid for the format's message syntax. Empty for a format with no
|
|
852
|
+
* message-syntax check, which treats every value as valid.
|
|
853
|
+
*/
|
|
854
|
+
readonly invalidIcuKeys: readonly string[];
|
|
855
|
+
/**
|
|
856
|
+
* Keys or dotted paths that were present in the source but excluded from translation: a stray
|
|
857
|
+
* non-string leaf (a number, boolean, or null) for a tree-based format, or a key the format's own
|
|
858
|
+
* metadata marks as not translatable, such as an Android `translatable="false"` resource. These
|
|
859
|
+
* are never translated, hashed, diffed, or checked for placeholder or ICU integrity.
|
|
860
|
+
*
|
|
861
|
+
* Reporting them is the adapter's choice, so an empty list means this adapter reported none, not
|
|
862
|
+
* necessarily that the file carried none. A tree-based adapter reports its non-string leaves
|
|
863
|
+
* automatically; a flat adapter reports by returning `{ entries, excludedLeafPaths }` from
|
|
864
|
+
* `parseEntries` instead of a bare map.
|
|
865
|
+
*
|
|
866
|
+
* Whether one survives a later rewrite of the same file depends on the adapter: an adapter that
|
|
867
|
+
* reconstructs the whole file from `resource.entries` alone drops it, while an adapter that
|
|
868
|
+
* patches only the entries it touches into the existing document (for example `apple-xcstrings`)
|
|
869
|
+
* preserves it verbatim.
|
|
870
|
+
*/
|
|
871
|
+
readonly excludedLeafPaths: readonly string[];
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* The contract every format adapter implements. A new format attaches by implementing this interface
|
|
875
|
+
* and registering it in an {@link AdapterRegistry}.
|
|
876
|
+
*
|
|
877
|
+
* Implement it through one of the two shared factories rather than from scratch:
|
|
878
|
+
* `createTreeFileAdapter` for a nested-tree format and `createFlatFileAdapter` for a flat
|
|
879
|
+
* key/value format. A format that fits neither shape implements this interface directly.
|
|
880
|
+
*
|
|
881
|
+
* An adapter shipped outside verbatra names itself with a `custom:` identifier and is attached by
|
|
882
|
+
* registering it in a registry the caller hands to the SDK. It is ordinary trusted code: verbatra
|
|
883
|
+
* does not sandbox it.
|
|
884
|
+
*/
|
|
885
|
+
interface FormatAdapter {
|
|
886
|
+
/**
|
|
887
|
+
* The single format this adapter handles: one of core's built-in `SupportedFormat` members, or a
|
|
888
|
+
* third-party `custom:` identifier for an adapter that ships outside verbatra.
|
|
889
|
+
*/
|
|
890
|
+
readonly format: FormatId;
|
|
891
|
+
/**
|
|
892
|
+
* Detect whether this adapter can handle a file, by path extension and an optional content sample.
|
|
893
|
+
* Best-effort and side-effect-free; it reads nothing from disk.
|
|
894
|
+
*
|
|
895
|
+
* @param filePath - The path of the file under consideration.
|
|
896
|
+
* @param sample - An optional leading content sample to aid detection.
|
|
897
|
+
* @returns True if this adapter could handle the file. Several adapters may return true for the same
|
|
898
|
+
* file (all JSON adapters claim `.json`); the caller disambiguates by explicit format.
|
|
899
|
+
*/
|
|
900
|
+
canHandle(filePath: string, sample?: string): boolean;
|
|
901
|
+
/**
|
|
902
|
+
* Read a file into a {@link LocaleResource} plus its message-validity result.
|
|
903
|
+
*
|
|
904
|
+
* @param filePath - The file to read.
|
|
905
|
+
* @param locale - The locale to tag the resource with.
|
|
906
|
+
* @returns The parsed resource and the keys whose values are invalid for the format.
|
|
907
|
+
* @throws `AdapterError` when the content is malformed, oversized, or structurally invalid
|
|
908
|
+
* (the implementation names the specific codes). A missing or unopenable path instead rejects with
|
|
909
|
+
* the underlying filesystem error.
|
|
910
|
+
*/
|
|
911
|
+
read(filePath: string, locale: string): Promise<ReadResult>;
|
|
912
|
+
/**
|
|
913
|
+
* Write a {@link LocaleResource} back to a file, preserving key order and structure, atomically.
|
|
914
|
+
*
|
|
915
|
+
* @param resource - The resource to serialize.
|
|
916
|
+
* @param filePath - The destination file.
|
|
917
|
+
* @throws `AdapterError` if the resource cannot be represented in the format; rejects with the
|
|
918
|
+
* underlying filesystem error on a write failure.
|
|
919
|
+
*/
|
|
920
|
+
write(resource: LocaleResource, filePath: string): Promise<void>;
|
|
921
|
+
/**
|
|
922
|
+
* Extract the format's placeholder tokens from a single value, resolving nothing.
|
|
923
|
+
*
|
|
924
|
+
* @param value - The translatable string to scan.
|
|
925
|
+
* @returns The placeholder tokens found, in document order. Does not throw.
|
|
926
|
+
*/
|
|
927
|
+
extractPlaceholders(value: string): readonly string[];
|
|
928
|
+
/**
|
|
929
|
+
* Validate a single value against the format's message syntax before it is written. Only the
|
|
930
|
+
* ICU-message formats carry a real check today (next-intl and ARB); every other adapter has no
|
|
931
|
+
* message syntax to violate and returns true for any value.
|
|
932
|
+
*
|
|
933
|
+
* @param value - The candidate translated value to validate.
|
|
934
|
+
* @returns True when the value is valid for the format's message syntax. Does not throw; an
|
|
935
|
+
* unparseable value returns false.
|
|
936
|
+
*/
|
|
937
|
+
validateMessage(value: string): boolean;
|
|
938
|
+
/**
|
|
939
|
+
* Optional whole-value placeholder comparison, used by callers instead of independently extracting
|
|
940
|
+
* each side's placeholders with {@link extractPlaceholders} and diffing the flat lists. Both
|
|
941
|
+
* adapter factories accept one, under the `comparePlaceholders` option. Among the shipped
|
|
942
|
+
* adapters two families define one, for different reasons:
|
|
943
|
+
*
|
|
944
|
+
* - The ICU formats (next-intl and ARB) compare branch by branch, because flattening a
|
|
945
|
+
* plural/select value loses which branch a placeholder came from.
|
|
946
|
+
* - The double-brace formats (i18next, ngx-translate, and YAML) add a one-directional
|
|
947
|
+
* single-brace check on top of their flat multiset comparison: a `{name}`-shaped token present
|
|
948
|
+
* in the translated value but absent from the source is reported as `extra`. Single-brace text
|
|
949
|
+
* is ordinary literal content under these formats' default delimiters, so it is never required
|
|
950
|
+
* to survive a translation, but inventing one is never a legitimate translation either.
|
|
951
|
+
*
|
|
952
|
+
* Absent for the remaining adapters, which are compared via `extractPlaceholders` plus a flat
|
|
953
|
+
* multiset check.
|
|
954
|
+
*
|
|
955
|
+
* @param sourceValue - The source value.
|
|
956
|
+
* @param targetValue - The translated value to check against it.
|
|
957
|
+
* @returns The merged placeholder-integrity result. Does not throw.
|
|
958
|
+
*/
|
|
959
|
+
comparePlaceholders?(sourceValue: string, targetValue: string): PlaceholderIntegrityResult;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* The outcome of a size-bounded read through the {@link AdapterFs} port. A path that exists but is
|
|
964
|
+
* not a regular file, or a file larger than the caller's limit, is reported as a state rather than
|
|
965
|
+
* thrown, so an adapter can treat "not a file" and "too large to trust" as ordinary branches. A
|
|
966
|
+
* missing path is not one of these states: the port rejects instead (see
|
|
967
|
+
* {@link AdapterFs.readBounded}).
|
|
968
|
+
*/
|
|
969
|
+
type BoundedReadOutcome = {
|
|
970
|
+
/** The file was read in full. */
|
|
971
|
+
readonly kind: "ok";
|
|
972
|
+
/** The file's content, decoded as UTF-8. */
|
|
973
|
+
readonly content: string;
|
|
974
|
+
} | {
|
|
975
|
+
/** The path exists but is not a regular file, a directory for example, so nothing was read. */
|
|
976
|
+
readonly kind: "not-a-file";
|
|
977
|
+
} | {
|
|
978
|
+
/** The file is larger than the requested byte limit, so nothing was read. */
|
|
979
|
+
readonly kind: "too-large";
|
|
980
|
+
};
|
|
981
|
+
/**
|
|
982
|
+
* The file-system port every format adapter reads and writes through. An adapter that takes this
|
|
983
|
+
* port instead of importing `node:fs` can be tested against an in-memory implementation and stays
|
|
984
|
+
* honest about every path it touches.
|
|
985
|
+
*
|
|
986
|
+
* This is the supported path for an adapter shipped outside verbatra, and the one verbatra hands it.
|
|
987
|
+
* It is a convention, not a sandbox: nothing stops third-party code from reaching the file system
|
|
988
|
+
* another way.
|
|
989
|
+
*/
|
|
990
|
+
interface AdapterFs {
|
|
991
|
+
/**
|
|
992
|
+
* Read a UTF-8 text file, refusing anything larger than the caller's limit.
|
|
993
|
+
*
|
|
994
|
+
* @param path - The file to read.
|
|
995
|
+
* @param maxBytes - The largest file size to read; a larger file yields `too-large` and is not read.
|
|
996
|
+
* @returns The file's content, or the state that prevented reading it.
|
|
997
|
+
* @throws An `Error` whose `code` is `"ENOENT"` when nothing exists at `path`, as `node:fs` does.
|
|
998
|
+
* Several adapters re-read an existing destination before writing so they can preserve its
|
|
999
|
+
* layout, and they rely on exactly that code to mean "no file yet, write a fresh one"; a port
|
|
1000
|
+
* that reports a missing file any other way fails those writes.
|
|
1001
|
+
*/
|
|
1002
|
+
readBounded(path: string, maxBytes: number): Promise<BoundedReadOutcome>;
|
|
1003
|
+
/**
|
|
1004
|
+
* Write a UTF-8 text file atomically, creating missing parent directories.
|
|
1005
|
+
*
|
|
1006
|
+
* @param path - The destination file.
|
|
1007
|
+
* @param data - The content to write.
|
|
1008
|
+
* @returns Nothing, once the content is durably in place under `path`.
|
|
1009
|
+
*/
|
|
1010
|
+
writeFileAtomic(path: string, data: string): Promise<void>;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* The {@link AdapterFs} implementation backed by the real file system, and the default every adapter
|
|
1014
|
+
* factory uses when no port is supplied. It writes through a temporary file and a rename, so an
|
|
1015
|
+
* interrupted write never leaves a half-finished locale file.
|
|
1016
|
+
*
|
|
1017
|
+
* @example
|
|
1018
|
+
* ```ts
|
|
1019
|
+
* const adapter = createFlatFileAdapter({ ...options, fs: nodeAdapterFs });
|
|
1020
|
+
* ```
|
|
1021
|
+
*/
|
|
1022
|
+
declare const nodeAdapterFs: AdapterFs;
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Outcome of resolving an adapter for a file. Structured, never thrown: an unresolvable file is a
|
|
1026
|
+
* status the caller branches on, not an exception.
|
|
1027
|
+
*
|
|
1028
|
+
* `resolved` carries the single {@link FormatAdapter} to use. `no-match` means nothing claimed the
|
|
1029
|
+
* file, and reports the formats that were tried (every registered format for a detection attempt, or
|
|
1030
|
+
* just the one requested when an explicit format was given). `ambiguous` means more than one adapter
|
|
1031
|
+
* claimed the file, which is routine since every JSON adapter claims `.json`; it reports the competing
|
|
1032
|
+
* candidates instead of guessing, and the caller resolves it by naming a format explicitly.
|
|
1033
|
+
*/
|
|
1034
|
+
type AdapterResolution = {
|
|
1035
|
+
/** Exactly one adapter claimed the file. */
|
|
1036
|
+
readonly status: "resolved";
|
|
1037
|
+
/** The adapter to read and write the file with. */
|
|
1038
|
+
readonly adapter: FormatAdapter;
|
|
1039
|
+
} | {
|
|
1040
|
+
/** No registered adapter claimed the file. */
|
|
1041
|
+
readonly status: "no-match";
|
|
1042
|
+
/** The file that could not be matched. */
|
|
1043
|
+
readonly filePath: string;
|
|
1044
|
+
/**
|
|
1045
|
+
* The formats that were tried: every registered format for a detection attempt, or just the
|
|
1046
|
+
* one requested when an explicit format was given.
|
|
1047
|
+
*/
|
|
1048
|
+
readonly triedFormats: readonly FormatId[];
|
|
1049
|
+
} | {
|
|
1050
|
+
/** More than one adapter claimed the file, so detection cannot choose between them. */
|
|
1051
|
+
readonly status: "ambiguous";
|
|
1052
|
+
/** The file that several adapters claimed. */
|
|
1053
|
+
readonly filePath: string;
|
|
1054
|
+
/** The competing formats. Resolve the ambiguity by naming one of them explicitly. */
|
|
1055
|
+
readonly candidates: readonly FormatId[];
|
|
1056
|
+
};
|
|
1057
|
+
/** Options for {@link AdapterRegistry.resolve}: select a format explicitly, or aid detection. */
|
|
1058
|
+
interface ResolveOptions {
|
|
1059
|
+
/** A leading content sample to aid detection. Ignored when `format` is given. */
|
|
1060
|
+
readonly sample?: string;
|
|
1061
|
+
/** Bypass detection and select this format explicitly. */
|
|
1062
|
+
readonly format?: FormatId;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Holds the registered adapters and resolves one for a file. Open for extension: adapters attach
|
|
1066
|
+
* through {@link AdapterRegistry.register} without changing resolution logic, and an unresolvable
|
|
1067
|
+
* file is a structured status rather than an exception.
|
|
1068
|
+
*
|
|
1069
|
+
* @example
|
|
1070
|
+
* ```ts
|
|
1071
|
+
* const registry = createDefaultRegistry().register(createTomlAdapter());
|
|
1072
|
+
* const resolution = registry.resolve("locales/de.toml");
|
|
1073
|
+
* if (resolution.status === "resolved") {
|
|
1074
|
+
* const { resource } = await resolution.adapter.read("locales/de.toml", "de");
|
|
1075
|
+
* }
|
|
1076
|
+
* ```
|
|
1077
|
+
*/
|
|
1078
|
+
declare class AdapterRegistry {
|
|
1079
|
+
private readonly adapters;
|
|
1080
|
+
/**
|
|
1081
|
+
* Register an adapter. Registration order is the order detection consults them.
|
|
1082
|
+
*
|
|
1083
|
+
* A format identifier may be registered only once: a second adapter claiming an identifier the
|
|
1084
|
+
* registry already holds is rejected, so neither a third-party adapter nor a second copy of one
|
|
1085
|
+
* can silently shadow what is already there.
|
|
1086
|
+
*
|
|
1087
|
+
* An adapter whose format is a third-party `custom:` identifier is wrapped so that an unexpected
|
|
1088
|
+
* throw from any of its contract methods surfaces as an {@link AdapterError} with code
|
|
1089
|
+
* `ADAPTER_FAILED` naming the format. An `AdapterError` the adapter raises itself, and a Node.js
|
|
1090
|
+
* system error carrying an errno-style code such as `ENOENT`, pass through unchanged. Built-in
|
|
1091
|
+
* adapters are registered as they are.
|
|
1092
|
+
*
|
|
1093
|
+
* @param adapter - The adapter to add.
|
|
1094
|
+
* @returns This registry, for chaining.
|
|
1095
|
+
* @throws `AdapterError` with code `INVALID_FORMAT_ID` when the format carries the `custom:`
|
|
1096
|
+
* prefix but is not a well-formed identifier. Such an adapter is refused rather than registered
|
|
1097
|
+
* without the containment wrapper, which would silently lose its failure attribution.
|
|
1098
|
+
* @throws `AdapterError` with code `DUPLICATE_FORMAT` when the registry already holds an adapter
|
|
1099
|
+
* for this format.
|
|
1100
|
+
*
|
|
1101
|
+
* @example
|
|
1102
|
+
* ```ts
|
|
1103
|
+
* const registry = createDefaultRegistry().register(createTomlAdapter());
|
|
1104
|
+
* await translate({ config }, { adapterRegistry: registry });
|
|
1105
|
+
* ```
|
|
1106
|
+
*/
|
|
1107
|
+
register(adapter: FormatAdapter): this;
|
|
1108
|
+
private formats;
|
|
1109
|
+
private resolveByFormat;
|
|
1110
|
+
private resolveByDetection;
|
|
1111
|
+
/**
|
|
1112
|
+
* Resolve the adapter for a file, by explicit format when given, otherwise by detection.
|
|
1113
|
+
*
|
|
1114
|
+
* @param filePath - The file to resolve an adapter for.
|
|
1115
|
+
* @param options - `format` selects explicitly and skips detection; `sample` aids detection.
|
|
1116
|
+
* @returns A structured {@link AdapterResolution}: `resolved`, `no-match`, or `ambiguous`. An
|
|
1117
|
+
* unresolvable file is a status, not an exception.
|
|
1118
|
+
* @throws `AdapterError` with code `ADAPTER_FAILED` when a registered third-party adapter throws
|
|
1119
|
+
* from its own `canHandle` during detection. Resolution itself raises nothing.
|
|
1120
|
+
*/
|
|
1121
|
+
resolve(filePath: string, options?: ResolveOptions): AdapterResolution;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Build a registry holding every format adapter verbatra ships, in detection order. This is the
|
|
1126
|
+
* starting point for adding an adapter of your own: register it on the returned registry and hand
|
|
1127
|
+
* that registry to an SDK flow as its `adapterRegistry` dependency.
|
|
1128
|
+
*
|
|
1129
|
+
* @param fs - The file-system port every built-in adapter reads and writes through. Defaults to
|
|
1130
|
+
* {@link nodeAdapterFs}.
|
|
1131
|
+
* @returns A registry of the built-in adapters, ready for further registrations.
|
|
1132
|
+
*
|
|
1133
|
+
* @example
|
|
1134
|
+
* ```ts
|
|
1135
|
+
* const registry = createDefaultRegistry().register(createTomlAdapter());
|
|
1136
|
+
* await translate({ config }, { adapterRegistry: registry });
|
|
1137
|
+
* ```
|
|
1138
|
+
*/
|
|
1139
|
+
declare function createDefaultRegistry(fs?: AdapterFs): AdapterRegistry;
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* The stable, secret-free codes an {@link AdapterError} carries.
|
|
1143
|
+
*
|
|
1144
|
+
* The first seven describe a file verbatra could not handle: `INVALID_JSON`, `INVALID_YAML` and
|
|
1145
|
+
* `INVALID_XML` for malformed syntax, `INVALID_STRUCTURE` for a parseable file of the wrong shape,
|
|
1146
|
+
* `MAX_DEPTH_EXCEEDED` and `INPUT_TOO_LARGE` for the read caps, and `MIXED_STRUCTURE` for a file
|
|
1147
|
+
* that mixes flat and nested keys where the format forbids it.
|
|
1148
|
+
*
|
|
1149
|
+
* The last three describe an adapter rather than a file. `ADAPTER_FAILED` attributes an unexpected
|
|
1150
|
+
* throw to the third-party adapter it came from, so a plugin defect never surfaces as an
|
|
1151
|
+
* unattributed verbatra stack trace. `DUPLICATE_FORMAT` rejects registering a second adapter for a
|
|
1152
|
+
* format identifier a registry already holds. `INVALID_FORMAT_ID` rejects registering an adapter
|
|
1153
|
+
* whose `custom:` identifier is malformed, which would otherwise be registered without the
|
|
1154
|
+
* containment wrapper and silently lose its failure attribution.
|
|
1155
|
+
*
|
|
1156
|
+
* The set is closed. A third-party adapter raises `INVALID_STRUCTURE` (or another member that fits)
|
|
1157
|
+
* rather than minting its own code.
|
|
1158
|
+
*/
|
|
1159
|
+
type AdapterErrorCode = "INVALID_JSON" | "INVALID_YAML" | "INVALID_XML" | "INVALID_STRUCTURE" | "MAX_DEPTH_EXCEEDED" | "INPUT_TOO_LARGE" | "MIXED_STRUCTURE" | "ADAPTER_FAILED" | "DUPLICATE_FORMAT" | "INVALID_FORMAT_ID";
|
|
1160
|
+
/**
|
|
1161
|
+
* A structured, secret-free failure raised by a format adapter or by the adapter registry. Catch it
|
|
1162
|
+
* to branch on {@link AdapterErrorCode} instead of matching on message text.
|
|
1163
|
+
*
|
|
1164
|
+
* @example
|
|
1165
|
+
* ```ts
|
|
1166
|
+
* try {
|
|
1167
|
+
* await adapter.read("locales/de.json", "de");
|
|
1168
|
+
* } catch (error) {
|
|
1169
|
+
* if (error instanceof AdapterError && error.code === "INVALID_JSON") {
|
|
1170
|
+
* console.error(error.message);
|
|
1171
|
+
* }
|
|
1172
|
+
* }
|
|
1173
|
+
* ```
|
|
1174
|
+
*/
|
|
1175
|
+
declare class AdapterError extends Error {
|
|
1176
|
+
/** The stable code for this failure. */
|
|
1177
|
+
readonly code: AdapterErrorCode;
|
|
1178
|
+
/**
|
|
1179
|
+
* @param code - The stable code for this failure.
|
|
1180
|
+
* @param message - A human-readable description. Never include a secret or a file's contents.
|
|
1181
|
+
*/
|
|
1182
|
+
constructor(code: AdapterErrorCode, message: string);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/** A value in a locale tree about to be serialized, with key order preserved at every level. */
|
|
1186
|
+
type OrderedValue = string | number | boolean | null | OrderedRecord | readonly OrderedValue[];
|
|
1187
|
+
/** A locale tree about to be serialized, keyed in the order it will be written out. */
|
|
1188
|
+
type OrderedRecord = ReadonlyMap<string, OrderedValue>;
|
|
1189
|
+
|
|
1190
|
+
/** A terminal value in a parsed locale tree: everything that is not a nested object. */
|
|
1191
|
+
type JsonLeaf = string | number | boolean | null;
|
|
1192
|
+
/** Any node in a parsed locale tree: a leaf, or a nested record of further nodes. */
|
|
1193
|
+
type JsonTree = JsonLeaf | JsonRecord;
|
|
1194
|
+
/** A parsed locale tree node, keyed in the order the source document defined its keys. */
|
|
1195
|
+
type JsonRecord = ReadonlyMap<string, JsonTree>;
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Finds a format's placeholder tokens in one translatable value, in document order. Resolves
|
|
1199
|
+
* nothing and never throws; a value with no placeholders yields an empty list.
|
|
1200
|
+
*/
|
|
1201
|
+
type ExtractPlaceholders = (value: string) => readonly string[];
|
|
1202
|
+
/**
|
|
1203
|
+
* Reports the keys whose values are invalid for a format's message syntax, given every entry read
|
|
1204
|
+
* from one file. Only the ICU-message formats need one; a format with no message syntax omits it.
|
|
1205
|
+
*/
|
|
1206
|
+
type ComputeInvalidIcuKeys = (entries: ReadonlyMap<string, TranslationEntry>) => readonly string[];
|
|
1207
|
+
/**
|
|
1208
|
+
* Decides whether one candidate value is valid for a format's message syntax before it is written.
|
|
1209
|
+
* Never throws: an unparseable value returns false.
|
|
1210
|
+
*/
|
|
1211
|
+
type ValidateMessage = (value: string) => boolean;
|
|
1212
|
+
/**
|
|
1213
|
+
* Compares a source value against its translation as whole values rather than as two flat token
|
|
1214
|
+
* lists, for a format whose structure (an ICU plural or select branch, for example) would be lost by
|
|
1215
|
+
* flattening. Never throws: a mismatch is data.
|
|
1216
|
+
*/
|
|
1217
|
+
type ComparePlaceholders = (sourceValue: string, targetValue: string) => PlaceholderIntegrityResult;
|
|
1218
|
+
/**
|
|
1219
|
+
* Rejects a parsed tree that is structurally wrong for a format, by throwing an `AdapterError`.
|
|
1220
|
+
* Returns nothing when the tree is acceptable.
|
|
1221
|
+
*/
|
|
1222
|
+
type ValidateTree = (tree: JsonRecord) => void;
|
|
1223
|
+
/**
|
|
1224
|
+
* Decides whether a leading content sample looks like this format, so two adapters claiming the same
|
|
1225
|
+
* file extension can still be told apart. Consulted only when a sample is available.
|
|
1226
|
+
*/
|
|
1227
|
+
type Sniff = (sample: string) => boolean;
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* What a flat format's `parseEntries` returns when it has content to report as skipped. Returning a
|
|
1231
|
+
* bare map instead is equivalent to returning one of these with no skipped paths.
|
|
1232
|
+
*/
|
|
1233
|
+
interface FlatParseResult {
|
|
1234
|
+
/** The entries read from the file, keyed by entry key, in document order. */
|
|
1235
|
+
readonly entries: Map<string, TranslationEntry>;
|
|
1236
|
+
/**
|
|
1237
|
+
* Keys the file carried but that are excluded from translation, for example one the format's own
|
|
1238
|
+
* metadata marks as not translatable. Omit it when the format has no such concept.
|
|
1239
|
+
*/
|
|
1240
|
+
readonly excludedLeafPaths?: readonly string[];
|
|
1241
|
+
}
|
|
1242
|
+
/** What a flat format's `parseEntries` may return: the entries alone, or entries plus diagnostics. */
|
|
1243
|
+
type FlatParseOutcome = Map<string, TranslationEntry> | FlatParseResult;
|
|
1244
|
+
/** Everything {@link createFlatFileAdapter} needs to turn a flat key/value format into an adapter. */
|
|
1245
|
+
interface FlatFileAdapterOptions {
|
|
1246
|
+
/** The format this adapter claims: a built-in name, or a `custom:` identifier of your own. */
|
|
1247
|
+
readonly format: FormatId;
|
|
1248
|
+
/**
|
|
1249
|
+
* The file extensions this format owns, each including the leading dot. Matched against a path's
|
|
1250
|
+
* extension case-insensitively.
|
|
1251
|
+
*/
|
|
1252
|
+
readonly extensions: readonly string[];
|
|
1253
|
+
/**
|
|
1254
|
+
* Optional content check, consulted when a sample is available, so this adapter does not claim
|
|
1255
|
+
* every file that merely shares one of its extensions.
|
|
1256
|
+
*/
|
|
1257
|
+
readonly sniff?: Sniff;
|
|
1258
|
+
/**
|
|
1259
|
+
* Parse one file's text into entries keyed by entry key, in document order. Throw an
|
|
1260
|
+
* `AdapterError` for content this format cannot represent; anything else thrown is reported as a
|
|
1261
|
+
* structural failure.
|
|
1262
|
+
*/
|
|
1263
|
+
readonly parseEntries: (content: string, namespace: string, filePath: string, fs: AdapterFs) => FlatParseOutcome | Promise<FlatParseOutcome>;
|
|
1264
|
+
/**
|
|
1265
|
+
* Render entries back to the format's text, preserving key order. Receives the destination path
|
|
1266
|
+
* and the port, for a format that has to consult the existing file to write in place.
|
|
1267
|
+
*/
|
|
1268
|
+
readonly serializeEntries: (entries: ReadonlyMap<string, TranslationEntry>, filePath: string, fs: AdapterFs) => Promise<string> | string;
|
|
1269
|
+
/** Find this format's placeholder tokens in one value. */
|
|
1270
|
+
readonly extractPlaceholders: ExtractPlaceholders;
|
|
1271
|
+
/** Optional message-syntax check applied to a candidate value before it is written. */
|
|
1272
|
+
readonly validateMessage?: ValidateMessage;
|
|
1273
|
+
/** Optional per-file report of the keys whose values are invalid for this format's message syntax. */
|
|
1274
|
+
readonly computeInvalidIcuKeys?: ComputeInvalidIcuKeys;
|
|
1275
|
+
/**
|
|
1276
|
+
* Optional whole-value placeholder comparison, for a format whose structure a flat token list
|
|
1277
|
+
* would lose. Omitted adapters are compared by `extractPlaceholders` plus a flat multiset check.
|
|
1278
|
+
*/
|
|
1279
|
+
readonly comparePlaceholders?: ComparePlaceholders;
|
|
1280
|
+
/** The file-system port to read and write through. Defaults to `nodeAdapterFs`. */
|
|
1281
|
+
readonly fs?: AdapterFs;
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Build a {@link FormatAdapter} for a flat key/value format: one where every entry is addressed by a
|
|
1285
|
+
* single key with no nesting, such as Java `.properties` or Android `strings.xml`. The factory
|
|
1286
|
+
* supplies the bounded read, the atomic write, extension detection and the structured error
|
|
1287
|
+
* handling; you supply only the format's own parsing and serialization.
|
|
1288
|
+
*
|
|
1289
|
+
* The returned adapter's `read` refuses a file larger than 16 MiB with `INPUT_TOO_LARGE` and a path
|
|
1290
|
+
* that is not a regular file with `INVALID_STRUCTURE`, strips a leading byte order mark before
|
|
1291
|
+
* parsing, and uses the file's base name without its extension as the namespace.
|
|
1292
|
+
*
|
|
1293
|
+
* @param options - The format's parsing, serialization and detection behaviour.
|
|
1294
|
+
* @returns A complete adapter, ready to register on an `AdapterRegistry`.
|
|
1295
|
+
* @throws Nothing itself. The returned adapter's `read` and `write` raise `AdapterError` for content
|
|
1296
|
+
* this format cannot represent, and reject with the underlying filesystem error otherwise.
|
|
1297
|
+
*
|
|
1298
|
+
* @example
|
|
1299
|
+
* ```ts
|
|
1300
|
+
* const tomlAdapter = createFlatFileAdapter({
|
|
1301
|
+
* format: "custom:toml",
|
|
1302
|
+
* extensions: [".toml"],
|
|
1303
|
+
* parseEntries: (content, namespace) => parseToml(content, namespace),
|
|
1304
|
+
* serializeEntries: (entries) => serializeToml(entries),
|
|
1305
|
+
* extractPlaceholders: (value) => [...value.matchAll(/\{\w+\}/g)].map((m) => m[0]),
|
|
1306
|
+
* });
|
|
1307
|
+
* ```
|
|
1308
|
+
*/
|
|
1309
|
+
declare function createFlatFileAdapter(options: FlatFileAdapterOptions): FormatAdapter;
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Derives the per-entry facts a format decides for itself from one key and its string value: the
|
|
1313
|
+
* placeholder tokens the value carries, and whether the key is one plural form of a set.
|
|
1314
|
+
*/
|
|
1315
|
+
type DeriveEntry = (key: string, value: string) => {
|
|
1316
|
+
readonly placeholders: readonly string[];
|
|
1317
|
+
readonly isPlural: boolean;
|
|
1318
|
+
};
|
|
1319
|
+
/**
|
|
1320
|
+
* How a dotted key in the source document is addressed. `literal-leaf` keeps `"a.b"` as one key
|
|
1321
|
+
* whose name contains a dot; `path-notation` reads it as the nested path `a` then `b`.
|
|
1322
|
+
*/
|
|
1323
|
+
type KeyMode = "literal-leaf" | "path-notation";
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* Builds the tree to serialize from the entries about to be written, for a format that patches an
|
|
1327
|
+
* existing document rather than rebuilding it from the entries alone.
|
|
1328
|
+
*/
|
|
1329
|
+
type BuildWriteTree = (entries: ReadonlyMap<string, TranslationEntry>, filePath: string, fs: AdapterFs) => OrderedRecord | Promise<OrderedRecord>;
|
|
1330
|
+
/**
|
|
1331
|
+
* Pulls per-key translator descriptions out of the raw file text, for a format that carries them as
|
|
1332
|
+
* metadata alongside the values.
|
|
1333
|
+
*/
|
|
1334
|
+
type DeriveDescriptions = (content: string) => ReadonlyMap<string, string>;
|
|
1335
|
+
/** Everything {@link createTreeFileAdapter} needs to turn a nested-tree format into an adapter. */
|
|
1336
|
+
interface TreeFileAdapterOptions {
|
|
1337
|
+
/** The format this adapter claims: a built-in name, or a `custom:` identifier of your own. */
|
|
1338
|
+
readonly format: FormatId;
|
|
1339
|
+
/**
|
|
1340
|
+
* The file extensions this format owns, each including the leading dot. Matched against a path's
|
|
1341
|
+
* extension case-insensitively.
|
|
1342
|
+
*/
|
|
1343
|
+
readonly extensions: readonly string[];
|
|
1344
|
+
/**
|
|
1345
|
+
* Optional content check, consulted when a sample is available, so this adapter does not claim
|
|
1346
|
+
* every file that merely shares one of its extensions.
|
|
1347
|
+
*/
|
|
1348
|
+
readonly sniff?: Sniff;
|
|
1349
|
+
/**
|
|
1350
|
+
* Parse one file's text into a nested tree, preserving key order at every level. Throw an
|
|
1351
|
+
* `AdapterError` for content this format cannot represent; anything else thrown is reported as a
|
|
1352
|
+
* structural failure.
|
|
1353
|
+
*/
|
|
1354
|
+
readonly parse: (content: string) => JsonRecord;
|
|
1355
|
+
/** Render a nested tree back to the format's text, preserving key order at every level. */
|
|
1356
|
+
readonly serialize: (tree: OrderedRecord) => string;
|
|
1357
|
+
/** Derive the placeholder tokens and the plural flag for one leaf. */
|
|
1358
|
+
readonly deriveEntry: DeriveEntry;
|
|
1359
|
+
/** Find this format's placeholder tokens in one value. */
|
|
1360
|
+
readonly extractPlaceholders: ExtractPlaceholders;
|
|
1361
|
+
/** Optional per-file report of the keys whose values are invalid for this format's message syntax. */
|
|
1362
|
+
readonly computeInvalidIcuKeys?: ComputeInvalidIcuKeys;
|
|
1363
|
+
/** Optional message-syntax check applied to a candidate value before it is written. */
|
|
1364
|
+
readonly validateMessage?: ValidateMessage;
|
|
1365
|
+
/** Optional structural check on the parsed tree, throwing an `AdapterError` for a shape this format forbids. */
|
|
1366
|
+
readonly validateTree?: ValidateTree;
|
|
1367
|
+
/** Optional override for how the tree to write is assembled, for a format that patches the existing document. */
|
|
1368
|
+
readonly buildWriteTree?: BuildWriteTree;
|
|
1369
|
+
/** How a dotted key is addressed. Defaults to `"literal-leaf"`. */
|
|
1370
|
+
readonly keyMode?: KeyMode;
|
|
1371
|
+
/** Optional whole-value placeholder comparison, for a format whose structure flattening would lose. */
|
|
1372
|
+
readonly comparePlaceholders?: ComparePlaceholders;
|
|
1373
|
+
/** Optional extraction of per-key translator descriptions from the raw file text. */
|
|
1374
|
+
readonly deriveDescriptions?: DeriveDescriptions;
|
|
1375
|
+
/** The file-system port to read and write through. Defaults to `nodeAdapterFs`. */
|
|
1376
|
+
readonly fs?: AdapterFs;
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Build a {@link FormatAdapter} for a nested-tree format: one whose entries live at paths through
|
|
1380
|
+
* nested objects, such as i18next JSON or YAML. The factory supplies the bounded read, the atomic
|
|
1381
|
+
* write, tree flattening and unflattening, extension detection and the structured error handling;
|
|
1382
|
+
* you supply only the format's own parsing, serialization and per-leaf facts.
|
|
1383
|
+
*
|
|
1384
|
+
* The returned adapter's `read` refuses a file larger than 16 MiB with `INPUT_TOO_LARGE` and a path
|
|
1385
|
+
* that is not a regular file with `INVALID_STRUCTURE`, strips a leading byte order mark before
|
|
1386
|
+
* parsing, and uses the file's base name without its extension as the namespace.
|
|
1387
|
+
*
|
|
1388
|
+
* @param options - The format's parsing, serialization, detection and per-entry behaviour.
|
|
1389
|
+
* @returns A complete adapter, ready to register on an `AdapterRegistry`.
|
|
1390
|
+
* @throws Nothing itself. The returned adapter's `read` and `write` raise `AdapterError` for content
|
|
1391
|
+
* this format cannot represent, and reject with the underlying filesystem error otherwise.
|
|
1392
|
+
*
|
|
1393
|
+
* @example
|
|
1394
|
+
* ```ts
|
|
1395
|
+
* const hoconAdapter = createTreeFileAdapter({
|
|
1396
|
+
* format: "custom:hocon",
|
|
1397
|
+
* extensions: [".conf"],
|
|
1398
|
+
* parse: (content) => parseHocon(content),
|
|
1399
|
+
* serialize: (tree) => serializeHocon(tree),
|
|
1400
|
+
* deriveEntry: (_key, value) => ({ placeholders: tokensIn(value), isPlural: false }),
|
|
1401
|
+
* extractPlaceholders: (value) => tokensIn(value),
|
|
1402
|
+
* });
|
|
1403
|
+
* ```
|
|
1404
|
+
*/
|
|
1405
|
+
declare function createTreeFileAdapter(options: TreeFileAdapterOptions): FormatAdapter;
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* The outcome of a size-bounded text read. A file that is absent or larger than the caller's limit
|
|
1409
|
+
* is reported as a state rather than thrown, so a caller can treat "no file yet" as normal and an
|
|
1410
|
+
* oversized file as a distinct, actionable condition.
|
|
1411
|
+
*/
|
|
1412
|
+
type BoundedFileRead = {
|
|
1413
|
+
/** The file was read in full. */
|
|
1414
|
+
readonly kind: "ok";
|
|
1415
|
+
/** The file's content, decoded as UTF-8. */
|
|
1416
|
+
readonly content: string;
|
|
1417
|
+
} | {
|
|
1418
|
+
/** No readable file exists at the path. */
|
|
1419
|
+
readonly kind: "missing";
|
|
1420
|
+
} | {
|
|
1421
|
+
/** The file exists but exceeds the requested byte limit, so nothing was read. */
|
|
1422
|
+
readonly kind: "too-large";
|
|
1423
|
+
};
|
|
1424
|
+
/**
|
|
1425
|
+
* The outcome of a size-bounded binary read, used for interchange workbooks. Mirrors
|
|
1426
|
+
* {@link BoundedFileRead} but yields raw bytes rather than decoded text.
|
|
1427
|
+
*/
|
|
1428
|
+
type BoundedBytesRead = {
|
|
1429
|
+
/** The file was read in full. */
|
|
1430
|
+
readonly kind: "ok";
|
|
1431
|
+
/** The file's raw bytes. */
|
|
1432
|
+
readonly bytes: Uint8Array;
|
|
1433
|
+
} | {
|
|
1434
|
+
/** No readable file exists at the path. */
|
|
1435
|
+
readonly kind: "missing";
|
|
1436
|
+
} | {
|
|
1437
|
+
/** The file exists but exceeds the requested byte limit, so nothing was read. */
|
|
1438
|
+
readonly kind: "too-large";
|
|
1439
|
+
};
|
|
1440
|
+
/**
|
|
1441
|
+
* What one entry in a directory listing is: a regular file, a directory, or anything else. A
|
|
1442
|
+
* symbolic link is reported as `other`, never followed, which is what keeps a source scan inside
|
|
1443
|
+
* the roots it was given.
|
|
1444
|
+
*/
|
|
1445
|
+
interface DirectoryEntry {
|
|
1446
|
+
/** The entry's own name, with no directory part. */
|
|
1447
|
+
readonly name: string;
|
|
1448
|
+
/** What the entry is. Anything that is not a regular file or a directory is `other`. */
|
|
1449
|
+
readonly kind: "file" | "directory" | "other";
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* The file-system port the SDK's own I/O goes through. Supplying your own implementation as a
|
|
1453
|
+
* `deps.fs` option redirects that I/O, which is how the SDK's tests avoid touching disk and how an
|
|
1454
|
+
* embedding application can back part of a project with something other than a local disk.
|
|
1455
|
+
*
|
|
1456
|
+
* The seam carries every file the SDK touches once a config is loaded: the run-status file, the
|
|
1457
|
+
* lock-file and the write locks, the translation-memory cache, the config glossary, workbook and TMX
|
|
1458
|
+
* I/O, generated declaration and pseudolocale files, the source scan, and the locale files
|
|
1459
|
+
* themselves, which the adapters read and write through a port built from this one. Two exceptions
|
|
1460
|
+
* remain. {@link loadConfig} finds and loads the config file itself directly from disk; only its
|
|
1461
|
+
* glossary goes through the port. And a caller-supplied `deps.adapterRegistry` holds adapters the
|
|
1462
|
+
* caller constructed, so their file access is whatever the caller wired into them, and supplying
|
|
1463
|
+
* both means the caller owns that wiring.
|
|
1464
|
+
*
|
|
1465
|
+
* Reads are size-bounded by contract so that a hostile or accidentally huge file cannot exhaust
|
|
1466
|
+
* memory. Writes are expected to be atomic: the default implementation writes to a temporary file
|
|
1467
|
+
* and renames it into place, so a crash mid-write never leaves a half-written file behind.
|
|
1468
|
+
* Directory creation is the caller's job, so an implementation whose `writeFile` targets a real
|
|
1469
|
+
* directory tree must also implement `mkdir`.
|
|
1470
|
+
*/
|
|
1471
|
+
interface SdkFs {
|
|
1472
|
+
/** Reports whether a file exists at the path. The default implementation checks existence only. */
|
|
1473
|
+
fileExists(path: string): Promise<boolean>;
|
|
1474
|
+
/** Reads a file as UTF-8 text, refusing to read more than `maxBytes`. */
|
|
1475
|
+
readFileBounded(path: string, maxBytes: number): Promise<BoundedFileRead>;
|
|
1476
|
+
/** Reads a file as raw bytes, refusing to read more than `maxBytes`. */
|
|
1477
|
+
readBytesBounded(path: string, maxBytes: number): Promise<BoundedBytesRead>;
|
|
1478
|
+
/** Writes UTF-8 text to the path, replacing any existing file atomically. */
|
|
1479
|
+
writeFile(path: string, data: string): Promise<void>;
|
|
1480
|
+
/** Writes raw bytes to the path, replacing any existing file atomically. */
|
|
1481
|
+
writeBytes(path: string, data: Uint8Array): Promise<void>;
|
|
1482
|
+
/**
|
|
1483
|
+
* Creates a file only if it does not already exist, returning `false` when it does. This is the
|
|
1484
|
+
* primitive behind the per-locale write lock, so it must be atomic against other processes.
|
|
1485
|
+
*/
|
|
1486
|
+
createExclusive(path: string, data: string): Promise<boolean>;
|
|
1487
|
+
/** Removes the file at the path, succeeding even when it is already absent. */
|
|
1488
|
+
deleteFile(path: string): Promise<void>;
|
|
1489
|
+
/** Creates a directory and any missing parents. Optional; omit it if the backing store has no directories. */
|
|
1490
|
+
mkdir?(path: string): Promise<void>;
|
|
1491
|
+
/**
|
|
1492
|
+
* Lists one directory's immediate entries, without recursing and without following symbolic
|
|
1493
|
+
* links. Optional, so an implementation written before source extraction existed keeps working;
|
|
1494
|
+
* {@link extract} is the only entry point that needs it and reports its absence as
|
|
1495
|
+
* `EXTRACT_FS_UNSUPPORTED`.
|
|
1496
|
+
*
|
|
1497
|
+
* @param path - The directory to list.
|
|
1498
|
+
* @returns Every immediate entry with its kind. Rejects when the path is not a readable directory.
|
|
1499
|
+
*/
|
|
1500
|
+
readDirectory?(path: string): Promise<readonly DirectoryEntry[]>;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
/**
|
|
1504
|
+
* The on-disk translation memory, read from and written to `verbatra.cache.json` (see
|
|
1505
|
+
* {@link CACHE_FILE_NAME}). A run consults it before calling the provider, so a string that was
|
|
1506
|
+
* already translated under the same configuration is reused instead of paid for again.
|
|
1507
|
+
*
|
|
1508
|
+
* Entries are keyed by configuration fingerprint, then locale, then source-content hash. The
|
|
1509
|
+
* fingerprint layer means that changing the provider, model, tone, or glossary does not silently
|
|
1510
|
+
* reuse translations produced under the old settings; those entries simply stop matching.
|
|
1511
|
+
*
|
|
1512
|
+
* `sources` sits beside them as a flat hash-to-text index. A hash cannot be compared for
|
|
1513
|
+
* similarity, so the source text itself has to be on file for a fuzzy match to have anything to
|
|
1514
|
+
* measure a changed string against. It is deliberately outside the fingerprint layer: the source
|
|
1515
|
+
* text a hash stands for does not depend on which provider translated it.
|
|
1516
|
+
*/
|
|
1517
|
+
interface TranslationMemory {
|
|
1518
|
+
/** The cache schema version. A file from a newer version is ignored rather than trusted. */
|
|
1519
|
+
readonly version: number;
|
|
1520
|
+
/** Cached values, nested as configuration fingerprint, then locale, then source-content hash. */
|
|
1521
|
+
readonly entries: Readonly<Record<string, Readonly<Record<string, Readonly<Record<string, string>>>>>>;
|
|
1522
|
+
/**
|
|
1523
|
+
* The source text each content hash stands for. Empty for a cache carried forward from schema
|
|
1524
|
+
* version 1, which stored no source text; it fills in as later runs touch those hashes again.
|
|
1525
|
+
*/
|
|
1526
|
+
readonly sources: Readonly<Record<string, string>>;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* The file name of the project's translation memory, resolved against the run's working directory.
|
|
1531
|
+
* Commit it to reuse translations across machines and CI runs, or add it to `.gitignore` to treat
|
|
1532
|
+
* the cache as purely local. Deleting it is always safe: the next run simply repays for the strings
|
|
1533
|
+
* it would have reused.
|
|
1534
|
+
*/
|
|
1535
|
+
declare const CACHE_FILE_NAME = "verbatra.cache.json";
|
|
1536
|
+
|
|
1537
|
+
/**
|
|
1538
|
+
* The zod schema for the `provider` block, discriminated on `id`. Each variant's options are
|
|
1539
|
+
* validated strictly, so an option that belongs to a different provider is reported as an error
|
|
1540
|
+
* rather than ignored. It is embedded in {@link verbatraConfigSchema} and produces
|
|
1541
|
+
* {@link ProviderConfig}.
|
|
1542
|
+
*/
|
|
1543
|
+
declare const providerConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
1544
|
+
id: z.ZodLiteral<"anthropic">;
|
|
1545
|
+
options: z.ZodObject<{
|
|
1546
|
+
model: z.ZodString;
|
|
1547
|
+
maxTokens: z.ZodNumber;
|
|
1548
|
+
requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
1549
|
+
}, z.core.$strict>;
|
|
1550
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1551
|
+
id: z.ZodLiteral<"openai">;
|
|
1552
|
+
options: z.ZodObject<{
|
|
1553
|
+
model: z.ZodString;
|
|
1554
|
+
maxOutputTokens: z.ZodNumber;
|
|
1555
|
+
requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
1556
|
+
}, z.core.$strict>;
|
|
1557
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1558
|
+
id: z.ZodLiteral<"gemini">;
|
|
1559
|
+
options: z.ZodObject<{
|
|
1560
|
+
model: z.ZodString;
|
|
1561
|
+
maxOutputTokens: z.ZodNumber;
|
|
1562
|
+
requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
1563
|
+
}, z.core.$strict>;
|
|
1564
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1565
|
+
id: z.ZodLiteral<"deepl">;
|
|
1566
|
+
options: z.ZodObject<{
|
|
1567
|
+
glossaryId: z.ZodOptional<z.ZodString>;
|
|
1568
|
+
requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
1569
|
+
}, z.core.$strict>;
|
|
1570
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1571
|
+
id: z.ZodLiteral<"google-translate">;
|
|
1572
|
+
options: z.ZodObject<{
|
|
1573
|
+
requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
1574
|
+
}, z.core.$strict>;
|
|
1575
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1576
|
+
id: z.ZodLiteral<"openai-compatible">;
|
|
1577
|
+
options: z.ZodObject<{
|
|
1578
|
+
baseUrl: z.ZodURL;
|
|
1579
|
+
model: z.ZodString;
|
|
1580
|
+
maxOutputTokens: z.ZodNumber;
|
|
1581
|
+
apiKeyEnvVar: z.ZodOptional<z.ZodString>;
|
|
1582
|
+
requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
1583
|
+
}, z.core.$strict>;
|
|
1584
|
+
}, z.core.$strip>], "id">;
|
|
1585
|
+
/**
|
|
1586
|
+
* The `provider` block of a verbatra config: a discriminated union on `id`, so each provider
|
|
1587
|
+
* carries exactly the options it supports and nothing else.
|
|
1588
|
+
*
|
|
1589
|
+
* No variant has a field for an API key. Keys are read from the environment by the provider itself
|
|
1590
|
+
* (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `DEEPL_API_KEY`, or
|
|
1591
|
+
* `GOOGLE_TRANSLATE_API_KEY`), which is what keeps them out of config files and out of version
|
|
1592
|
+
* control. The `openai-compatible` variant reads `OPENAI_COMPATIBLE_API_KEY` when it is set and
|
|
1593
|
+
* sends no real key otherwise, since a local server usually needs none; naming a different variable
|
|
1594
|
+
* through `apiKeyEnvVar` makes that variable required. It still never holds the key itself.
|
|
1595
|
+
*/
|
|
1596
|
+
type ProviderConfig = z.infer<typeof providerConfigSchema>;
|
|
1597
|
+
/**
|
|
1598
|
+
* The identifier of a supported translation provider: `anthropic`, `openai`, `gemini`, `deepl`,
|
|
1599
|
+
* `google-translate`, or `openai-compatible`. The last one targets a local or self-hosted server
|
|
1600
|
+
* that speaks the OpenAI chat-completions API.
|
|
1601
|
+
*/
|
|
1602
|
+
type ProviderId = ProviderConfig["id"];
|
|
1603
|
+
|
|
1604
|
+
/**
|
|
1605
|
+
* The zod schema every verbatra config is validated against. {@link loadConfig} applies it, so most
|
|
1606
|
+
* consumers never call it directly; reach for it when you build a config programmatically and want
|
|
1607
|
+
* to validate it before handing it to {@link translate}, or when you surface config errors in your
|
|
1608
|
+
* own tooling.
|
|
1609
|
+
*
|
|
1610
|
+
* The object is strict, so an unrecognized key is an error rather than being silently ignored: a
|
|
1611
|
+
* typo in a config file is reported instead of quietly doing nothing. The one exception is the
|
|
1612
|
+
* optional `$schema` key, accepted so a JSON or YAML config can point an editor at the JSON Schema
|
|
1613
|
+
* document the package ships as `@verbatra/sdk/config-schema.json`.
|
|
1614
|
+
*
|
|
1615
|
+
* `format` accepts a built-in format name or a `custom:` identifier such as `custom:toml`. A
|
|
1616
|
+
* `custom:` identifier is checked for shape only; the adapter behind it is supplied through a
|
|
1617
|
+
* flow's `adapterRegistry` dependency, and a run whose registry holds none fails with
|
|
1618
|
+
* `UNKNOWN_FORMAT`.
|
|
1619
|
+
*
|
|
1620
|
+
* `fuzzyCache` is off unless `enabled` is set. With it on, a source string whose earlier form is in
|
|
1621
|
+
* the translation memory close enough to clear `threshold` reuses that translation instead of
|
|
1622
|
+
* paying the provider for it. `threshold` is a similarity ratio from `0.5` to `1` and defaults to `0.9`.
|
|
1623
|
+
*
|
|
1624
|
+
* Beyond the per-field checks, two whole-config rules are enforced: `targetLocales` must not
|
|
1625
|
+
* contain the source locale, and it must not contain two locales that differ only in case (they
|
|
1626
|
+
* would collide on a case-insensitive file system). The `{locale}` token requirement is a
|
|
1627
|
+
* per-field check on `files.pattern`.
|
|
1628
|
+
*/
|
|
1629
|
+
declare const verbatraConfigSchema: z.ZodObject<{
|
|
1630
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
1631
|
+
sourceLocale: z.ZodString;
|
|
1632
|
+
targetLocales: z.ZodArray<z.ZodString>;
|
|
1633
|
+
format: z.ZodUnion<readonly [z.ZodEnum<{
|
|
1634
|
+
"i18next-json": "i18next-json";
|
|
1635
|
+
"vue-i18n-json": "vue-i18n-json";
|
|
1636
|
+
"next-intl-json": "next-intl-json";
|
|
1637
|
+
"ngx-translate-json": "ngx-translate-json";
|
|
1638
|
+
xliff: "xliff";
|
|
1639
|
+
yaml: "yaml";
|
|
1640
|
+
arb: "arb";
|
|
1641
|
+
properties: "properties";
|
|
1642
|
+
"apple-strings": "apple-strings";
|
|
488
1643
|
"apple-xcstrings": "apple-xcstrings";
|
|
489
1644
|
"android-xml": "android-xml";
|
|
490
1645
|
"gettext-po": "gettext-po";
|
|
491
|
-
|
|
1646
|
+
ini: "ini";
|
|
1647
|
+
resx: "resx";
|
|
1648
|
+
}>, z.ZodTemplateLiteral<`custom:${string}`>]>;
|
|
492
1649
|
files: z.ZodObject<{
|
|
493
1650
|
pattern: z.ZodString;
|
|
494
1651
|
localeStyle: z.ZodOptional<z.ZodEnum<{
|
|
@@ -547,12 +1704,40 @@ declare const verbatraConfigSchema: z.ZodObject<{
|
|
|
547
1704
|
}>>;
|
|
548
1705
|
prune: z.ZodOptional<z.ZodBoolean>;
|
|
549
1706
|
generatePlurals: z.ZodOptional<z.ZodBoolean>;
|
|
1707
|
+
fuzzyCache: z.ZodOptional<z.ZodObject<{
|
|
1708
|
+
enabled: z.ZodBoolean;
|
|
1709
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
1710
|
+
}, z.core.$strict>>;
|
|
550
1711
|
maxBatchSize: z.ZodOptional<z.ZodNumber>;
|
|
1712
|
+
maxLength: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
551
1713
|
maxTokens: z.ZodOptional<z.ZodNumber>;
|
|
552
1714
|
budgetBehavior: z.ZodOptional<z.ZodEnum<{
|
|
553
1715
|
warn: "warn";
|
|
554
1716
|
stop: "stop";
|
|
555
1717
|
}>>;
|
|
1718
|
+
rates: z.ZodOptional<z.ZodObject<{
|
|
1719
|
+
asOf: z.ZodISODate;
|
|
1720
|
+
currency: z.ZodString;
|
|
1721
|
+
table: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
|
|
1722
|
+
inputPerMillionTokens: z.ZodNumber;
|
|
1723
|
+
outputPerMillionTokens: z.ZodNumber;
|
|
1724
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
1725
|
+
perMillionCharacters: z.ZodNumber;
|
|
1726
|
+
}, z.core.$strict>]>>;
|
|
1727
|
+
}, z.core.$strict>>;
|
|
1728
|
+
extract: z.ZodOptional<z.ZodObject<{
|
|
1729
|
+
framework: z.ZodEnum<{
|
|
1730
|
+
i18next: "i18next";
|
|
1731
|
+
}>;
|
|
1732
|
+
roots: z.ZodArray<z.ZodString>;
|
|
1733
|
+
exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1734
|
+
literals: z.ZodOptional<z.ZodObject<{
|
|
1735
|
+
ignore: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1736
|
+
}, z.core.$strict>>;
|
|
1737
|
+
unused: z.ZodOptional<z.ZodObject<{
|
|
1738
|
+
ignore: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1739
|
+
}, z.core.$strict>>;
|
|
1740
|
+
}, z.core.$strict>>;
|
|
556
1741
|
}, z.core.$strict>;
|
|
557
1742
|
/**
|
|
558
1743
|
* A config exactly as it is written in a `verbatra.config.ts` file, before the SDK resolves
|
|
@@ -720,6 +1905,46 @@ declare function defineConfig(config: AuthoringConfigFor<"google-translate">): V
|
|
|
720
1905
|
*/
|
|
721
1906
|
declare function defineConfig(config: AuthoringConfig): VerbatraConfigInput;
|
|
722
1907
|
|
|
1908
|
+
/**
|
|
1909
|
+
* The zod schema for the optional `extract` block, validated strictly so a misspelled field is
|
|
1910
|
+
* reported rather than silently ignored. It is embedded in {@link verbatraConfigSchema} and
|
|
1911
|
+
* produces {@link ExtractionConfig}.
|
|
1912
|
+
*/
|
|
1913
|
+
declare const extractionConfigSchema: z.ZodObject<{
|
|
1914
|
+
framework: z.ZodEnum<{
|
|
1915
|
+
i18next: "i18next";
|
|
1916
|
+
}>;
|
|
1917
|
+
roots: z.ZodArray<z.ZodString>;
|
|
1918
|
+
exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1919
|
+
literals: z.ZodOptional<z.ZodObject<{
|
|
1920
|
+
ignore: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1921
|
+
}, z.core.$strict>>;
|
|
1922
|
+
unused: z.ZodOptional<z.ZodObject<{
|
|
1923
|
+
ignore: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1924
|
+
}, z.core.$strict>>;
|
|
1925
|
+
}, z.core.$strict>;
|
|
1926
|
+
/**
|
|
1927
|
+
* The `extract` block of a verbatra config: which translation framework's call sites the source
|
|
1928
|
+
* scan looks for, which directories it walks, and which directory names it skips on the way.
|
|
1929
|
+
*
|
|
1930
|
+
* `roots` are resolved against the run's working directory, and the scan never reads outside them.
|
|
1931
|
+
* `exclude` adds directory names to the set that is always skipped, which already covers
|
|
1932
|
+
* `node_modules`, `.git`, and the usual build output directories.
|
|
1933
|
+
*
|
|
1934
|
+
* `literals.ignore` lists string literals the untranslated-literal scan (`verbatra doctor
|
|
1935
|
+
* --literals`) holds back project-wide, matched against the whole literal text as it is reported:
|
|
1936
|
+
* whitespace collapsed and, in JSX text or a quoted JSX attribute value, character references such
|
|
1937
|
+
* as `&` decoded, so an entry is written `Tom & Jerry`, not `Tom & Jerry`. A held-back
|
|
1938
|
+
* literal is still reported, under `suppressed`, so nothing vanishes silently.
|
|
1939
|
+
*
|
|
1940
|
+
* `unused.ignore` names catalog keys the unused-key report must not count as unused, for a key
|
|
1941
|
+
* referenced only from outside the scanned source (a server template, a CMS, a test fixture). Each
|
|
1942
|
+
* entry is an exact key, or a pattern in which `*` matches any run of characters, such as
|
|
1943
|
+
* `emails.*`. It matches the decoded key as the report shows it (`Welcome. Enjoy`, not the escaped
|
|
1944
|
+
* catalog spelling). A matched key is reported as ignored, never dropped from the report.
|
|
1945
|
+
*/
|
|
1946
|
+
type ExtractionConfig = z.infer<typeof extractionConfigSchema>;
|
|
1947
|
+
|
|
723
1948
|
/**
|
|
724
1949
|
* Where a loaded config's glossary came from, reported by {@link loadConfigWithMeta}. The resolved
|
|
725
1950
|
* {@link VerbatraConfig} holds only the final term map, so this is the one place a tool can tell an
|
|
@@ -844,7 +2069,9 @@ interface LoadConfigOptions {
|
|
|
844
2069
|
type ConfigSource = {
|
|
845
2070
|
/**
|
|
846
2071
|
* `search` means cosmiconfig found the file by walking up from the working directory, up to
|
|
847
|
-
* the nearest `.git` directory
|
|
2072
|
+
* the nearest `.git` directory or, failing that, the home directory or the working directory
|
|
2073
|
+
* itself (see {@link loadConfigWithMeta}); `explicit` means the caller named it through
|
|
2074
|
+
* `configPath`.
|
|
848
2075
|
*/
|
|
849
2076
|
readonly kind: "search" | "explicit";
|
|
850
2077
|
/** The absolute path of the config file that was loaded. */
|
|
@@ -868,8 +2095,8 @@ interface LoadedConfig {
|
|
|
868
2095
|
* config file is in effect, or to distinguish an inline glossary from a glossary file.
|
|
869
2096
|
*
|
|
870
2097
|
* Resolution order is: an explicit `configOverride`, then an explicit `configPath`, then a
|
|
871
|
-
* cosmiconfig search upward from `cwd` across `verbatra.config.ts`,
|
|
872
|
-
* `verbatra` property in `package.json`. The upward search stops at the nearest ancestor directory
|
|
2098
|
+
* cosmiconfig search upward from `cwd` across `verbatra.config.ts`, `.js` and `.cjs`, the
|
|
2099
|
+
* `.verbatrarc` family, and a `verbatra` property in `package.json`. The upward search stops at the nearest ancestor directory
|
|
873
2100
|
* containing a `.git` entry; if none is found, it stops at the user's home directory when that is an
|
|
874
2101
|
* ancestor of `cwd`, and otherwise it does not search above `cwd` at all, so a nested workspace
|
|
875
2102
|
* package finds a config at its monorepo root without wandering above it, and a project outside the
|
|
@@ -929,6 +2156,84 @@ declare function loadConfigWithMeta(options?: LoadConfigOptions): Promise<Loaded
|
|
|
929
2156
|
*/
|
|
930
2157
|
declare function loadConfig(options?: LoadConfigOptions): Promise<VerbatraConfig>;
|
|
931
2158
|
|
|
2159
|
+
/**
|
|
2160
|
+
* What a provider charges for: `tokens` for the prompt-driven LLMs, which bill the prompt and the
|
|
2161
|
+
* completion separately, and `characters` for the machine-translation APIs, which bill the source
|
|
2162
|
+
* text they are handed and report no token usage at all.
|
|
2163
|
+
*/
|
|
2164
|
+
type BillingUnit = "tokens" | "characters";
|
|
2165
|
+
/**
|
|
2166
|
+
* How one provider bills. `billedByApi` is false for a self-hosted endpoint, which still consumes
|
|
2167
|
+
* tokens but sends no invoice, so an estimate reports its quantity and no currency figure.
|
|
2168
|
+
*/
|
|
2169
|
+
interface ProviderBilling {
|
|
2170
|
+
/** The quantity this provider charges for. */
|
|
2171
|
+
readonly unit: BillingUnit;
|
|
2172
|
+
/** Whether a hosted API bills for the work at all. */
|
|
2173
|
+
readonly billedByApi: boolean;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
/**
|
|
2177
|
+
* The zod schema for one token-billed rate entry: an input and an output price per million
|
|
2178
|
+
* tokens, each at most `MAX_RATE` currency units.
|
|
2179
|
+
*/
|
|
2180
|
+
declare const tokenRateSchema: z.ZodObject<{
|
|
2181
|
+
inputPerMillionTokens: z.ZodNumber;
|
|
2182
|
+
outputPerMillionTokens: z.ZodNumber;
|
|
2183
|
+
}, z.core.$strict>;
|
|
2184
|
+
/**
|
|
2185
|
+
* The zod schema for one character-billed rate entry (DeepL, Google Cloud Translation): a
|
|
2186
|
+
* price per million source characters.
|
|
2187
|
+
*/
|
|
2188
|
+
declare const characterRateSchema: z.ZodObject<{
|
|
2189
|
+
perMillionCharacters: z.ZodNumber;
|
|
2190
|
+
}, z.core.$strict>;
|
|
2191
|
+
/**
|
|
2192
|
+
* The zod schema for one `rates.table` entry: either {@link tokenRateSchema} or
|
|
2193
|
+
* {@link characterRateSchema}, never a mix of the two shapes.
|
|
2194
|
+
*/
|
|
2195
|
+
declare const modelRateSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
2196
|
+
inputPerMillionTokens: z.ZodNumber;
|
|
2197
|
+
outputPerMillionTokens: z.ZodNumber;
|
|
2198
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
2199
|
+
perMillionCharacters: z.ZodNumber;
|
|
2200
|
+
}, z.core.$strict>]>;
|
|
2201
|
+
/**
|
|
2202
|
+
* The zod schema for the optional `rates` block: the `asOf` date and `currency` printed beside
|
|
2203
|
+
* every priced estimate, plus a `table` of {@link modelRateSchema} entries keyed by rate key
|
|
2204
|
+
* (`provider/model`, or the bare provider id for a provider without a model).
|
|
2205
|
+
*/
|
|
2206
|
+
declare const rateCardSchema: z.ZodObject<{
|
|
2207
|
+
asOf: z.ZodISODate;
|
|
2208
|
+
currency: z.ZodString;
|
|
2209
|
+
table: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
|
|
2210
|
+
inputPerMillionTokens: z.ZodNumber;
|
|
2211
|
+
outputPerMillionTokens: z.ZodNumber;
|
|
2212
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
2213
|
+
perMillionCharacters: z.ZodNumber;
|
|
2214
|
+
}, z.core.$strict>]>>;
|
|
2215
|
+
}, z.core.$strict>;
|
|
2216
|
+
/**
|
|
2217
|
+
* A rate charging by tokens, with the prompt and the completion priced separately. Every hosted LLM
|
|
2218
|
+
* provider bills this way.
|
|
2219
|
+
*/
|
|
2220
|
+
type TokenRate = z.infer<typeof tokenRateSchema>;
|
|
2221
|
+
/** A rate charging by source characters, which is how the machine-translation APIs bill. */
|
|
2222
|
+
type CharacterRate = z.infer<typeof characterRateSchema>;
|
|
2223
|
+
/** One entry of a {@link RateCard}: either a token rate or a character rate, never both. */
|
|
2224
|
+
type ModelRate = z.infer<typeof modelRateSchema>;
|
|
2225
|
+
/**
|
|
2226
|
+
* The rates a project supplies so an estimate can be expressed in money. verbatra ships no prices of
|
|
2227
|
+
* its own: they are a third party's, they change without notice, and a number baked into a release
|
|
2228
|
+
* would quietly go stale. The card carries the date it was read and the currency it is written in,
|
|
2229
|
+
* so any figure derived from it is visibly as old as `asOf` says it is.
|
|
2230
|
+
*
|
|
2231
|
+
* `table` is keyed by rate key: `provider/model` for a provider configured with a model (for
|
|
2232
|
+
* example `gemini/gemini-2.5-flash`), and the bare provider id for one without (`deepl`,
|
|
2233
|
+
* `google-translate`).
|
|
2234
|
+
*/
|
|
2235
|
+
type RateCard = z.infer<typeof rateCardSchema>;
|
|
2236
|
+
|
|
932
2237
|
/**
|
|
933
2238
|
* The stable, structured failure codes the SDK throws. Branch on {@link SdkError.code} rather than
|
|
934
2239
|
* on a message; the messages are written for humans and are not part of the contract.
|
|
@@ -945,17 +2250,20 @@ declare function loadConfig(options?: LoadConfigOptions): Promise<VerbatraConfig
|
|
|
945
2250
|
* file could not be resolved or parsed. Thrown by {@link loadConfig} and
|
|
946
2251
|
* {@link loadConfigWithMeta}, by {@link readGlossaryFile}, and by {@link updateGlossaryTerm},
|
|
947
2252
|
* which additionally throws it for a blank term or translation and for an edit whose result would
|
|
948
|
-
* exceed the glossary file size limit. {@link
|
|
949
|
-
*
|
|
950
|
-
*
|
|
2253
|
+
* exceed the glossary file size limit. {@link importTmx} and {@link exportTmx} throw it when the
|
|
2254
|
+
* source locale and a target locale are the same language tag once case and separators are
|
|
2255
|
+
* normalized, since a TMX segment could not be attributed to either. {@link importWorkbook} does
|
|
2256
|
+
* not throw it: when a handoff sheet or file names a locale that is not a configured target
|
|
2257
|
+
* locale, it records this code on that locale's {@link LocaleSummary} instead.
|
|
951
2258
|
* - `UNKNOWN_FORMAT`: no adapter is registered for the configured format. Thrown by every entry
|
|
952
2259
|
* point that selects an adapter, before any file is read. {@link doctor} is the exception: it
|
|
953
2260
|
* reports an unresolvable format as a failed `format-adapter` check instead, since reporting that
|
|
954
2261
|
* is the command's job.
|
|
955
2262
|
* - `UNKNOWN_LOCALE`: a requested locale is not among the configured target locales. Thrown through
|
|
956
2263
|
* the shared locale selection by {@link translate}, {@link watch}, {@link check}, {@link diff},
|
|
957
|
-
* {@link keyIntegrity}, {@link lockState}, {@link
|
|
958
|
-
* {@link
|
|
2264
|
+
* {@link keyIntegrity}, {@link lockState}, {@link localeValues}, {@link exportWorkbook},
|
|
2265
|
+
* {@link exportTmx}, {@link importTmx}, {@link keyValue}, {@link editEntry}, and
|
|
2266
|
+
* {@link retranslateEntry}. {@link translate} throws it before anything is
|
|
959
2267
|
* read or spent, and {@link watch} once at startup, before any watching begins.
|
|
960
2268
|
* - `UNKNOWN_KEY`: the requested key is not present in the source resource. Thrown by
|
|
961
2269
|
* {@link keyValue}, {@link editEntry}, and {@link retranslateEntry}.
|
|
@@ -964,9 +2272,12 @@ declare function loadConfig(options?: LoadConfigOptions): Promise<VerbatraConfig
|
|
|
964
2272
|
* {@link translate} and by {@link retranslateEntry}.
|
|
965
2273
|
* - `SOURCE_UNREADABLE`: the source locale file is absent. Thrown by every entry point that reads
|
|
966
2274
|
* the source, including {@link importWorkbook}, and by {@link watch} at startup.
|
|
967
|
-
* {@link importWorkbook} additionally
|
|
2275
|
+
* {@link importWorkbook} and {@link importTmx} additionally throw it when the handoff or TMX file
|
|
2276
|
+
* itself is missing.
|
|
968
2277
|
* - `SOURCE_INVALID`: the source locale file, or an interchange file, could not be parsed. Wraps
|
|
969
|
-
* the adapter or reader error.
|
|
2278
|
+
* the adapter or reader error. {@link pseudolocalize} also throws it when, for a format whose
|
|
2279
|
+
* writer only patches an existing document, the source file could not be copied to seed the
|
|
2280
|
+
* output.
|
|
970
2281
|
* - `LOCK_FILE_INVALID`: the lock-file exists but is corrupt, oversized, or at an unsupported
|
|
971
2282
|
* version. Thrown wherever the lock-file is read or updated: {@link translate}, {@link check},
|
|
972
2283
|
* {@link diff}, {@link keyIntegrity}, {@link lockState}, {@link loadLockFile},
|
|
@@ -997,306 +2308,573 @@ declare function loadConfig(options?: LoadConfigOptions): Promise<VerbatraConfig
|
|
|
997
2308
|
* {@link translate} before any locale runs, and by {@link watch} once at startup, before any
|
|
998
2309
|
* watching begins, since the value is fixed for the session rather than re-read per run.
|
|
999
2310
|
* - `CONCURRENCY_BUDGET_CONFLICT`: a live run requested a `concurrency` above 1 while a token
|
|
1000
|
-
* budget is configured. The
|
|
1001
|
-
*
|
|
2311
|
+
* budget is configured. The ceiling itself would still hold, but which locale loses its
|
|
2312
|
+
* remaining work would depend on the order the locales interleave, so the same project would not
|
|
2313
|
+
* produce the same run twice. A dry run is exempt, since it never consults the budget.
|
|
1002
2314
|
* - `TARGET_UNWRITABLE`: a target locale file could not be written, because its directory is not
|
|
1003
2315
|
* writable, does not exist, is read-only, or is out of space. The message names the target file
|
|
1004
2316
|
* relative to `cwd` and the underlying file-system code, never the internal temporary file the
|
|
1005
2317
|
* atomic write uses. Thrown by {@link editEntry} and {@link retranslateEntry}, which act on one
|
|
1006
|
-
* locale. {@link translate} and {@link importWorkbook} do not throw it: they record it on that
|
|
2318
|
+
* locale, and by {@link pseudolocalize} for the pseudolocale file. {@link translate} and {@link importWorkbook} do not throw it: they record it on that
|
|
1007
2319
|
* locale's {@link LocaleSummary} and carry on with the other locales.
|
|
2320
|
+
* - `PSEUDO_OUTPUT_CONFLICT`: {@link pseudolocalize} was asked to generate a pseudolocale that
|
|
2321
|
+
* names a configured locale, or to write one onto a configured locale file. Refused before
|
|
2322
|
+
* anything is read or written, so a generated pseudolocale can never overwrite a real
|
|
2323
|
+
* translation or stand in for one.
|
|
2324
|
+
* - `SOURCE_UNWRITABLE`: the source locale file could not be written. Thrown by {@link extract}
|
|
2325
|
+
* alone, since it is the only entry point that writes the source locale. The `xliff` and
|
|
2326
|
+
* `apple-xcstrings` formats reach it when no catalog exists yet, because neither is created from
|
|
2327
|
+
* nothing.
|
|
2328
|
+
* - `EXTRACT_NOT_CONFIGURED`: {@link extract} was called with a config that carries no `extract`
|
|
2329
|
+
* block, so there is no framework to look for and no source root to walk.
|
|
2330
|
+
* - `EXTRACT_FS_UNSUPPORTED`: {@link extract} was given a `deps.fs` that implements no
|
|
2331
|
+
* `readDirectory`, so no source file can be discovered. The member is optional on {@link SdkFs}
|
|
2332
|
+
* precisely so an implementation written before extraction existed keeps compiling; this is the
|
|
2333
|
+
* error it gets if it is then handed to {@link extract}.
|
|
2334
|
+
* - `TYPES_OUTPUT_CONFLICT`: {@link generateTypes} refused its output path. Before anything is
|
|
2335
|
+
* read or written, it refuses a path that names no file, is absolute, climbs out of the working
|
|
2336
|
+
* directory, or does not end in `.ts`, `.mts` or `.cts`, and one naming a configured locale
|
|
2337
|
+
* file, the lock file, the translation-memory cache, a file verbatra searches for its
|
|
2338
|
+
* configuration, or the configuration file the run loaded, compared case-insensitively. A
|
|
2339
|
+
* generating run, never a `check` run, also refuses to replace an existing file there that does
|
|
2340
|
+
* not begin with the header line verbatra writes, and leaves that file untouched.
|
|
2341
|
+
* - `TYPES_UNWRITABLE`: the declaration file {@link generateTypes} produces could not be written,
|
|
2342
|
+
* because its directory is not writable, does not exist, or the disk is out of space.
|
|
1008
2343
|
* - `LOCALE_FAILED`: never thrown. It is the fallback code recorded on a failed
|
|
1009
2344
|
* {@link LocaleSummary} when a per-locale failure carries no code of its own.
|
|
1010
2345
|
*/
|
|
1011
|
-
type SdkErrorCode = "CONFIG_NOT_FOUND" | "CONFIG_INVALID" | "UNKNOWN_FORMAT" | "UNKNOWN_LOCALE" | "UNKNOWN_KEY" | "PROVIDER_CONSTRUCTION_FAILED" | "SOURCE_UNREADABLE" | "SOURCE_INVALID" | "LOCK_FILE_INVALID" | "LOCK_CONTENDED" | "GLOSSARY_NOT_FILE_BACKED" | "GLOSSARY_UNWRITABLE" | "LOCALE_LAYOUT_INVALID" | "LOCALE_PATH_COLLISION" | "CONCURRENCY_INVALID" | "CONCURRENCY_BUDGET_CONFLICT" | "TARGET_UNWRITABLE" | "LOCALE_FAILED";
|
|
2346
|
+
type SdkErrorCode = "CONFIG_NOT_FOUND" | "CONFIG_INVALID" | "UNKNOWN_FORMAT" | "UNKNOWN_LOCALE" | "UNKNOWN_KEY" | "PROVIDER_CONSTRUCTION_FAILED" | "SOURCE_UNREADABLE" | "SOURCE_INVALID" | "LOCK_FILE_INVALID" | "LOCK_CONTENDED" | "GLOSSARY_NOT_FILE_BACKED" | "GLOSSARY_UNWRITABLE" | "LOCALE_LAYOUT_INVALID" | "LOCALE_PATH_COLLISION" | "CONCURRENCY_INVALID" | "CONCURRENCY_BUDGET_CONFLICT" | "TARGET_UNWRITABLE" | "PSEUDO_OUTPUT_CONFLICT" | "SOURCE_UNWRITABLE" | "EXTRACT_NOT_CONFIGURED" | "EXTRACT_FS_UNSUPPORTED" | "TYPES_OUTPUT_CONFLICT" | "TYPES_UNWRITABLE" | "LOCALE_FAILED";
|
|
2347
|
+
/**
|
|
2348
|
+
* The single structured error the SDK throws. Every whole-run failure surfaces as an `SdkError`
|
|
2349
|
+
* carrying a stable {@link SdkErrorCode}, except the few a flow's `@throws` names as passed through
|
|
2350
|
+
* unwrapped (a failed workbook write, a watcher factory that throws); per-locale failures, provider notices, and integrity
|
|
2351
|
+
* findings are reported as data on the {@link RunSummary} instead of being thrown.
|
|
2352
|
+
*
|
|
2353
|
+
* An `SdkError` never carries a secret in its message.
|
|
2354
|
+
*/
|
|
2355
|
+
declare class SdkError extends Error {
|
|
2356
|
+
/** The stable {@link SdkErrorCode} for this failure. Branch on this, not on the message. */
|
|
2357
|
+
readonly code: SdkErrorCode;
|
|
2358
|
+
/**
|
|
2359
|
+
* @param code - The stable failure code.
|
|
2360
|
+
* @param message - A human-readable description of the failure. Never contains a secret.
|
|
2361
|
+
* @param options - `cause` carries the error this one wraps, such as the interchange reader's
|
|
2362
|
+
* error for a `SOURCE_INVALID` TMX file. Read that file's line, column and unit with
|
|
2363
|
+
* `tmxErrorLocation` rather than from the cause directly.
|
|
2364
|
+
*/
|
|
2365
|
+
constructor(code: SdkErrorCode, message: string, options?: {
|
|
2366
|
+
readonly cause?: unknown;
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
/**
|
|
2371
|
+
* Conditions the SDK itself reports on a locale, as opposed to the {@link ProviderNotice} codes a
|
|
2372
|
+
* provider raises. Each marks a run that completed but did something a caller may want to know
|
|
2373
|
+
* about.
|
|
2374
|
+
*
|
|
2375
|
+
* - `PLURAL_CATEGORIES_INCOMPLETE`: `i18next-json` only. The target language needs CLDR plural
|
|
2376
|
+
* categories the source does not supply and that plural generation did not produce (it is off,
|
|
2377
|
+
* the provider is not an LLM, or a generated form was withheld), so the missing forms have to be
|
|
2378
|
+
* added by hand.
|
|
2379
|
+
* - `SUB_BATCH_FAILED`: one provider sub-batch of a locale failed. Its keys are withheld and retried
|
|
2380
|
+
* on the next run, while the results of the other sub-batches are kept.
|
|
2381
|
+
* - `BLANK_ROW_BASELINE_RETAINED`: an imported handoff row was blank, so the existing translation
|
|
2382
|
+
* and its lock-file baseline were kept rather than being erased.
|
|
2383
|
+
* - `BUDGET_TOKENS_EXCEEDED`: the configured token budget was reached. Under `warn` the run
|
|
2384
|
+
* continues and the overrun is reported; under `stop` the request that would have crossed the
|
|
2385
|
+
* ceiling is withheld before it is sent, and so is every request after it.
|
|
2386
|
+
* - `CACHE_VERSION_UNRECOGNIZED`: the translation memory is at a version this release does not
|
|
2387
|
+
* understand, so the run used no cache and left the file untouched.
|
|
2388
|
+
*/
|
|
2389
|
+
type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED" | "BUDGET_TOKENS_EXCEEDED" | "CACHE_VERSION_UNRECOGNIZED";
|
|
2390
|
+
/**
|
|
2391
|
+
* Token usage as reported by the provider. Absent when the provider does not report usage, which is
|
|
2392
|
+
* the case for machine-translation APIs such as DeepL that do not bill in tokens.
|
|
2393
|
+
*/
|
|
2394
|
+
interface UsageSummary {
|
|
2395
|
+
/** Tokens consumed by the prompts sent to the provider. */
|
|
2396
|
+
readonly inputTokens: number;
|
|
2397
|
+
/** Tokens consumed by the provider's responses. */
|
|
2398
|
+
readonly outputTokens: number;
|
|
2399
|
+
}
|
|
2400
|
+
/**
|
|
2401
|
+
* What a run does about its token budget. `warn`, the default, counts the spend and reports the
|
|
2402
|
+
* overrun without withholding anything. `stop` turns the figure into a ceiling: before each
|
|
2403
|
+
* provider request the run projects what that request will cost, and withholds it, and everything
|
|
2404
|
+
* after it, rather than send a request that would take the run past `maxTokens`.
|
|
2405
|
+
*/
|
|
2406
|
+
type BudgetBehavior = "warn" | "stop";
|
|
1012
2407
|
/**
|
|
1013
|
-
* The
|
|
1014
|
-
*
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1017
|
-
*
|
|
2408
|
+
* The token budget in force for one run, and how much of it that run consumed.
|
|
2409
|
+
*
|
|
2410
|
+
* It is enforced for every provider, including the machine-translation APIs that report no usage of
|
|
2411
|
+
* their own: those are counted from the same projection the pre-run estimate uses, so a ceiling set
|
|
2412
|
+
* against DeepL or Google Cloud Translation withholds work rather than sitting inert.
|
|
2413
|
+
* {@link supported} says which of the two the figure came from.
|
|
2414
|
+
*
|
|
2415
|
+
* It bounds one run, not a session. {@link watch} starts a fresh budget for every run it triggers,
|
|
2416
|
+
* so a watch session can spend up to `maxTokens` again on each source edit. It also covers
|
|
2417
|
+
* {@link translate} and nothing else: `retranslateEntry` constructs its own provider on a separate
|
|
2418
|
+
* path, reached from the Studio dashboard and the agent tools, and no budget here caps that spend.
|
|
2419
|
+
*
|
|
2420
|
+
* Under `stop` the ceiling is checked before every request the run sends, each half of a re-split
|
|
2421
|
+
* request included, and a refused request is never sent. What a reservation cannot bound is what
|
|
2422
|
+
* happens inside a request it already admitted. The provider layer sends one repair call of its own
|
|
2423
|
+
* when keys come back missing, so one admitted batch can cost up to about twice its projection; and
|
|
2424
|
+
* the count is only as good as what the provider reports, so a provider under-reporting its usage
|
|
2425
|
+
* would leave the ceiling further away than it looks. Both land at reconciliation, and nothing
|
|
2426
|
+
* further is admitted once they do. A request that fails or comes back truncated keeps its whole
|
|
2427
|
+
* projection charged rather than being refunded, because such a call has usually already billed for
|
|
2428
|
+
* its prompt.
|
|
1018
2429
|
*/
|
|
1019
|
-
|
|
1020
|
-
/** The
|
|
1021
|
-
readonly
|
|
2430
|
+
interface RunBudget {
|
|
2431
|
+
/** The configured ceiling, in tokens. */
|
|
2432
|
+
readonly maxTokens: number;
|
|
2433
|
+
/** Whether passing the ceiling only warns, or withholds work to stay under it. */
|
|
2434
|
+
readonly behavior: BudgetBehavior;
|
|
1022
2435
|
/**
|
|
1023
|
-
* @
|
|
1024
|
-
*
|
|
2436
|
+
* Whether {@link tokensUsed} is entirely the provider's own reported usage. False as soon as one
|
|
2437
|
+
* counted request came back without a usable figure, so the total is partly verbatra's own
|
|
2438
|
+
* projection: a machine-translation API reports none at all, and a failed or truncated request
|
|
2439
|
+
* reports none either. Also false when the run sent no request at all, a dry run included, in
|
|
2440
|
+
* which case nothing was counted. The budget is enforced either way.
|
|
2441
|
+
*/
|
|
2442
|
+
readonly supported: boolean;
|
|
2443
|
+
/**
|
|
2444
|
+
* Tokens counted across the run: reported usage for every request that gave one, and the
|
|
2445
|
+
* projection for every request that did not.
|
|
1025
2446
|
*/
|
|
1026
|
-
|
|
2447
|
+
readonly tokensUsed: number;
|
|
2448
|
+
/**
|
|
2449
|
+
* True once the run reached its ceiling: either `tokensUsed` reached or passed `maxTokens`, or,
|
|
2450
|
+
* under `stop`, a request was withheld because sending it would have passed it.
|
|
2451
|
+
*/
|
|
2452
|
+
readonly exceeded: boolean;
|
|
1027
2453
|
}
|
|
1028
|
-
|
|
1029
2454
|
/**
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1037
|
-
*
|
|
1038
|
-
* JSON document holding every locale of a `.xcstrings` catalogue together), `android-xml` (Android's
|
|
1039
|
-
* `res/values/strings.xml` resource format, including `<plurals>` and `translatable="false"` entries),
|
|
1040
|
-
* and `gettext-po` (GNU gettext's `.po` and `.pot` catalogs, including `msgctxt` disambiguation and
|
|
1041
|
-
* `msgid_plural`/`msgstr[n]` plural forms keyed by the file's own `Plural-Forms` index count).
|
|
1042
|
-
*
|
|
1043
|
-
* The set is closed: a format outside it cannot be represented, so adding one means adding a member
|
|
1044
|
-
* here and an adapter that claims it.
|
|
2455
|
+
* Why a {@link RunEstimate} does or does not carry a currency figure.
|
|
2456
|
+
*
|
|
2457
|
+
* - `priced`: a rate for this provider and model was found on the configured rate card and applied.
|
|
2458
|
+
* - `no-rate-on-file`: no rate was configured for {@link RunEstimate.rateKey}. The quantity is still
|
|
2459
|
+
* reported; the money is not, because verbatra ships no prices of its own and will not guess one.
|
|
2460
|
+
* - `rate-unit-mismatch`: a rate exists but is written in the wrong unit for this provider, for
|
|
2461
|
+
* instance a per-character price against a token-billed model. It is refused rather than applied.
|
|
2462
|
+
* - `not-billed`: the provider is a self-hosted endpoint, so no API bills for the run at all.
|
|
1045
2463
|
*/
|
|
1046
|
-
|
|
1047
|
-
"i18next-json": "i18next-json";
|
|
1048
|
-
"vue-i18n-json": "vue-i18n-json";
|
|
1049
|
-
"next-intl-json": "next-intl-json";
|
|
1050
|
-
"ngx-translate-json": "ngx-translate-json";
|
|
1051
|
-
xliff: "xliff";
|
|
1052
|
-
yaml: "yaml";
|
|
1053
|
-
arb: "arb";
|
|
1054
|
-
properties: "properties";
|
|
1055
|
-
"apple-strings": "apple-strings";
|
|
1056
|
-
"apple-xcstrings": "apple-xcstrings";
|
|
1057
|
-
"android-xml": "android-xml";
|
|
1058
|
-
"gettext-po": "gettext-po";
|
|
1059
|
-
}>;
|
|
1060
|
-
/** One of the twelve supported source formats; the inferred type of {@link supportedFormatSchema}. */
|
|
1061
|
-
type SupportedFormat = z.infer<typeof supportedFormatSchema>;
|
|
1062
|
-
|
|
2464
|
+
type EstimatePricing = "priced" | "no-rate-on-file" | "rate-unit-mismatch" | "not-billed";
|
|
1063
2465
|
/**
|
|
1064
|
-
*
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1076
|
-
*
|
|
2466
|
+
* What a pre-run estimate deliberately leaves out. Each code names a way the real run can land
|
|
2467
|
+
* below the figure, above it, or away from it, so an estimate is never mistaken for an invoice.
|
|
2468
|
+
* The first three apply to every estimate; the last three only to a token-billed provider.
|
|
2469
|
+
*
|
|
2470
|
+
* - `CACHE_NOT_CONSULTED`: the estimate does not read the translation memory, so keys a live run
|
|
2471
|
+
* would serve from cache are still counted.
|
|
2472
|
+
* - `SOURCE_DUPLICATES_NOT_DEDUPLICATED`: a live run sends one representative per identical source
|
|
2473
|
+
* string; the estimate counts every key.
|
|
2474
|
+
* - `TRANSPORT_RETRIES_NOT_COUNTED`: the estimate counts one billable call per planned request. The
|
|
2475
|
+
* provider SDKs retry a failed call underneath that, so one planned request can become several
|
|
2476
|
+
* attempts on the wire, and a retried attempt that reached the model is still billed.
|
|
2477
|
+
* - `TRANSLATION_LENGTH_IS_ESTIMATED`: the translation does not exist yet, so the response is sized
|
|
2478
|
+
* from the source value plus a fixed expansion allowance. A target language that runs longer than
|
|
2479
|
+
* the allowance reports more completion tokens than this figure predicts.
|
|
2480
|
+
* - `TOKEN_COUNT_IS_HEURISTIC`: tokens are derived from character counts, not from the provider's
|
|
2481
|
+
* own tokenizer.
|
|
2482
|
+
* - `REPAIR_REQUESTS_NOT_COUNTED`: the extra requests an incomplete response can trigger are not
|
|
2483
|
+
* counted: one bounded repair round when keys came back missing, and a retry in halves, which
|
|
2484
|
+
* recurses, when the output was cut off. A repair round re-sends the system rules, the glossary
|
|
2485
|
+
* and the tone in full, so repairing one key in a large-glossary batch costs close to a whole
|
|
2486
|
+
* extra request.
|
|
1077
2487
|
*/
|
|
1078
|
-
|
|
1079
|
-
key: z.ZodString;
|
|
1080
|
-
namespace: z.ZodString;
|
|
1081
|
-
value: z.ZodString;
|
|
1082
|
-
description: z.ZodOptional<z.ZodString>;
|
|
1083
|
-
meaning: z.ZodOptional<z.ZodString>;
|
|
1084
|
-
placeholders: z.ZodReadonly<z.ZodArray<z.ZodString>>;
|
|
1085
|
-
isPlural: z.ZodBoolean;
|
|
1086
|
-
}, z.core.$strip>;
|
|
2488
|
+
type EstimateCaveatCode = "CACHE_NOT_CONSULTED" | "SOURCE_DUPLICATES_NOT_DEDUPLICATED" | "TRANSPORT_RETRIES_NOT_COUNTED" | "TRANSLATION_LENGTH_IS_ESTIMATED" | "TOKEN_COUNT_IS_HEURISTIC" | "REPAIR_REQUESTS_NOT_COUNTED";
|
|
1087
2489
|
/**
|
|
1088
|
-
* The
|
|
1089
|
-
*
|
|
2490
|
+
* The estimated billing quantity for one target locale, without the money. `inputTokens` and
|
|
2491
|
+
* `outputTokens` are present only for a token-billed provider, and `sourceCharacters` only for a
|
|
2492
|
+
* character-billed one, so a machine-translation API is never handed a token figure it does not
|
|
2493
|
+
* bill by.
|
|
1090
2494
|
*/
|
|
1091
|
-
|
|
1092
|
-
|
|
2495
|
+
interface LocaleEstimateQuantity {
|
|
2496
|
+
/** The target locale this estimate covers. */
|
|
2497
|
+
readonly locale: string;
|
|
2498
|
+
/** How many keys would be sent for this locale, translation and plural generation together. */
|
|
2499
|
+
readonly keys: number;
|
|
2500
|
+
/** How many provider requests those keys would be split into. */
|
|
2501
|
+
readonly requests: number;
|
|
2502
|
+
/** Estimated prompt tokens. Present only when the provider bills by tokens. */
|
|
2503
|
+
readonly inputTokens?: number;
|
|
2504
|
+
/** Estimated completion tokens. Present only when the provider bills by tokens. */
|
|
2505
|
+
readonly outputTokens?: number;
|
|
2506
|
+
/** Estimated source characters. Present only when the provider bills by characters. */
|
|
2507
|
+
readonly sourceCharacters?: number;
|
|
2508
|
+
}
|
|
2509
|
+
/** One locale of a {@link PricedRunEstimate}: the quantity plus the money it comes to. */
|
|
2510
|
+
interface PricedLocaleEstimate extends LocaleEstimateQuantity {
|
|
2511
|
+
/** The estimated cost for this locale, in {@link PricedRunEstimate.currency}. */
|
|
2512
|
+
readonly cost: number;
|
|
2513
|
+
}
|
|
2514
|
+
/** One locale of an {@link UnpricedRunEstimate}: the quantity, and deliberately no money. */
|
|
2515
|
+
interface UnpricedLocaleEstimate extends LocaleEstimateQuantity {
|
|
2516
|
+
/** Never present: the run carries no rate, so no locale of it carries a figure. */
|
|
2517
|
+
readonly cost?: undefined;
|
|
2518
|
+
}
|
|
1093
2519
|
/**
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
* of an adapter parses a format's own syntax.
|
|
2520
|
+
* One target locale of a {@link RunEstimate}. It carries a `cost` exactly when the run it belongs
|
|
2521
|
+
* to was priced, so there is no such thing as a priced estimate with a costless locale.
|
|
1097
2522
|
*/
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
2523
|
+
type LocaleEstimate = PricedLocaleEstimate | UnpricedLocaleEstimate;
|
|
2524
|
+
/**
|
|
2525
|
+
* Everything a {@link RunEstimate} reports regardless of whether it could be priced: which provider
|
|
2526
|
+
* the figure was computed for, how much would be sent, and what the figure leaves out.
|
|
2527
|
+
*/
|
|
2528
|
+
interface EstimateIdentity {
|
|
2529
|
+
/** The provider the estimate was computed for. */
|
|
2530
|
+
readonly provider: ProviderId;
|
|
2531
|
+
/** The configured model, absent for a provider that takes none (`deepl`, `google-translate`). */
|
|
2532
|
+
readonly model?: string;
|
|
2533
|
+
/**
|
|
2534
|
+
* The key a rate is filed under in the config's `rates.table`: `provider/model` for a provider
|
|
2535
|
+
* configured with a model, and the bare provider id otherwise.
|
|
2536
|
+
*/
|
|
2537
|
+
readonly rateKey: string;
|
|
2538
|
+
/**
|
|
2539
|
+
* Keys that would be sent across every locale, counting both the keys that would be translated
|
|
2540
|
+
* and the plural forms that would be generated.
|
|
2541
|
+
*/
|
|
2542
|
+
readonly keys: number;
|
|
2543
|
+
/** Provider requests across every locale, generation batches included. */
|
|
2544
|
+
readonly requests: number;
|
|
2545
|
+
/** What this figure leaves out. Always present, never empty. See {@link EstimateCaveatCode}. */
|
|
2546
|
+
readonly caveats: readonly EstimateCaveatCode[];
|
|
2547
|
+
}
|
|
2548
|
+
/** The quantity a token-billed provider charges for: the prompt and the completion, separately. */
|
|
2549
|
+
interface TokenRunQuantity extends EstimateIdentity {
|
|
2550
|
+
/** Always `tokens` for a prompt-driven LLM. */
|
|
2551
|
+
readonly unit: "tokens";
|
|
2552
|
+
/** Estimated prompt tokens across every locale. */
|
|
2553
|
+
readonly inputTokens: number;
|
|
2554
|
+
/** Estimated completion tokens across every locale. */
|
|
2555
|
+
readonly outputTokens: number;
|
|
2556
|
+
/** Never present: a token-billed provider does not bill source characters. */
|
|
2557
|
+
readonly sourceCharacters?: undefined;
|
|
2558
|
+
}
|
|
2559
|
+
/** The quantity a character-billed provider charges for: the source text handed to it. */
|
|
2560
|
+
interface CharacterRunQuantity extends EstimateIdentity {
|
|
2561
|
+
/** Always `characters` for a machine-translation API. */
|
|
2562
|
+
readonly unit: "characters";
|
|
2563
|
+
/** Estimated source characters across every locale. */
|
|
2564
|
+
readonly sourceCharacters: number;
|
|
2565
|
+
/** Never present: a machine-translation API reports no token usage at all. */
|
|
2566
|
+
readonly inputTokens?: undefined;
|
|
2567
|
+
/** Never present: a machine-translation API reports no token usage at all. */
|
|
2568
|
+
readonly outputTokens?: undefined;
|
|
1107
2569
|
}
|
|
1108
|
-
|
|
1109
2570
|
/**
|
|
1110
|
-
*
|
|
1111
|
-
*
|
|
1112
|
-
* returned, never thrown: a mismatch is data the caller decides what to do about, not a failure.
|
|
2571
|
+
* What a run would send, discriminated on {@link unit} so the billed dimension is always present
|
|
2572
|
+
* and the other one is never readable as a zero.
|
|
1113
2573
|
*/
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
/**
|
|
1118
|
-
readonly
|
|
1119
|
-
/**
|
|
1120
|
-
readonly
|
|
1121
|
-
/**
|
|
1122
|
-
|
|
2574
|
+
type RunEstimateQuantity = TokenRunQuantity | CharacterRunQuantity;
|
|
2575
|
+
/** The money half of a {@link PricedRunEstimate}: the figure, its currency, and its date. */
|
|
2576
|
+
interface PricedEstimateMoney {
|
|
2577
|
+
/** Always `priced`: a rate was found and applied. */
|
|
2578
|
+
readonly pricing: "priced";
|
|
2579
|
+
/** The rate card's currency code. */
|
|
2580
|
+
readonly currency: string;
|
|
2581
|
+
/**
|
|
2582
|
+
* The date the rates were read, as the config declared it. Print it beside any figure: a rate
|
|
2583
|
+
* card is exactly as current as this date.
|
|
2584
|
+
*/
|
|
2585
|
+
readonly asOf: string;
|
|
2586
|
+
/** The per-locale breakdown, in the same order as {@link RunSummary.locales}. */
|
|
2587
|
+
readonly locales: readonly PricedLocaleEstimate[];
|
|
2588
|
+
/** The estimated total, in {@link currency}. */
|
|
2589
|
+
readonly cost: number;
|
|
2590
|
+
}
|
|
2591
|
+
/** The money half of an {@link UnpricedRunEstimate}: why there is no figure, and no figure. */
|
|
2592
|
+
interface UnpricedEstimateMoney {
|
|
2593
|
+
/** Why no currency figure is present. See {@link EstimatePricing}. */
|
|
2594
|
+
readonly pricing: Exclude<EstimatePricing, "priced">;
|
|
2595
|
+
/** The per-locale breakdown, in the same order as {@link RunSummary.locales}. */
|
|
2596
|
+
readonly locales: readonly UnpricedLocaleEstimate[];
|
|
2597
|
+
/** Never present: there is no rate to express a figure in. */
|
|
2598
|
+
readonly currency?: undefined;
|
|
2599
|
+
/** Never present: there is no rate, so there is no date it was read on. */
|
|
2600
|
+
readonly asOf?: undefined;
|
|
2601
|
+
/** Never present: an absent rate is reported as absent, never as a cost of zero. */
|
|
2602
|
+
readonly cost?: undefined;
|
|
2603
|
+
}
|
|
2604
|
+
/** A priced estimate for a token-billed provider: prompt and completion tokens, and their cost. */
|
|
2605
|
+
interface TokenPricedRunEstimate extends TokenRunQuantity, PricedEstimateMoney {
|
|
2606
|
+
}
|
|
2607
|
+
/** A priced estimate for a character-billed provider: source characters, and their cost. */
|
|
2608
|
+
interface CharacterPricedRunEstimate extends CharacterRunQuantity, PricedEstimateMoney {
|
|
2609
|
+
}
|
|
2610
|
+
/** An unpriced estimate for a token-billed provider: prompt and completion tokens, no cost. */
|
|
2611
|
+
interface TokenUnpricedRunEstimate extends TokenRunQuantity, UnpricedEstimateMoney {
|
|
2612
|
+
}
|
|
2613
|
+
/** An unpriced estimate for a character-billed provider: source characters, no cost. */
|
|
2614
|
+
interface CharacterUnpricedRunEstimate extends CharacterRunQuantity, UnpricedEstimateMoney {
|
|
1123
2615
|
}
|
|
1124
|
-
|
|
1125
2616
|
/**
|
|
1126
|
-
*
|
|
1127
|
-
*
|
|
2617
|
+
* An estimate a rate could be applied to: the config supplied a rate for `rateKey` in the right
|
|
2618
|
+
* unit, so the run carries a currency figure and the date that rate was read.
|
|
1128
2619
|
*/
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
2620
|
+
type PricedRunEstimate = TokenPricedRunEstimate | CharacterPricedRunEstimate;
|
|
2621
|
+
/**
|
|
2622
|
+
* An estimate carrying quantity and no money, with `pricing` saying why. verbatra ships no prices
|
|
2623
|
+
* of its own and will not guess one, so the absence is reported rather than rendered as a cost of
|
|
2624
|
+
* zero.
|
|
2625
|
+
*/
|
|
2626
|
+
type UnpricedRunEstimate = TokenUnpricedRunEstimate | CharacterUnpricedRunEstimate;
|
|
2627
|
+
/**
|
|
2628
|
+
* A pre-run estimate: what a run would send, and what that would cost at the rates the project
|
|
2629
|
+
* supplied. It is computed without constructing a provider, reading an API key, or making a network
|
|
2630
|
+
* call.
|
|
2631
|
+
*
|
|
2632
|
+
* It bounds the plan, not the invoice. Every provider call a live run schedules is counted,
|
|
2633
|
+
* including the plural-generation batches; the prompt is measured from the request payload that
|
|
2634
|
+
* would actually be sent rather than modelled; and the response is sized from the source value plus
|
|
2635
|
+
* an expansion allowance. Against that plan a live run usually spends less, because it consults the
|
|
2636
|
+
* translation memory and collapses identical source strings.
|
|
2637
|
+
*
|
|
2638
|
+
* It can also spend more, and {@link EstimateCaveatCode} names every way: the provider SDKs retry a
|
|
2639
|
+
* failed call underneath each planned request, an incomplete response triggers a repair round or a
|
|
2640
|
+
* recursive split, a target language can expand past the allowance, and the token figure is a
|
|
2641
|
+
* character heuristic rather than the provider's own tokenizer. Treat it as a planning figure, not
|
|
2642
|
+
* as a ceiling that cannot be crossed.
|
|
2643
|
+
*
|
|
2644
|
+
* It covers {@link translate} and nothing else. `retranslateEntry` constructs a provider and calls
|
|
2645
|
+
* it on its own path, reached from the Studio dashboard and the agent tools, and no estimate here
|
|
2646
|
+
* sees that spend.
|
|
2647
|
+
*
|
|
2648
|
+
* Branch on {@link pricing}: a {@link PricedRunEstimate} carries `cost`, `currency` and `asOf`, and
|
|
2649
|
+
* an {@link UnpricedRunEstimate} carries none of them and says why.
|
|
2650
|
+
*/
|
|
2651
|
+
type RunEstimate = PricedRunEstimate | UnpricedRunEstimate;
|
|
2652
|
+
/** A condition the SDK reported on a locale. See {@link SdkNoticeCode}. */
|
|
2653
|
+
interface SdkNotice {
|
|
2654
|
+
/** The stable notice code. Branch on this, not on the message. */
|
|
2655
|
+
readonly code: SdkNoticeCode;
|
|
2656
|
+
/** A human-readable description of the condition. */
|
|
2657
|
+
readonly message: string;
|
|
2658
|
+
}
|
|
2659
|
+
/**
|
|
2660
|
+
* Any notice attached to a locale: either one the provider raised, such as a downgraded formality
|
|
2661
|
+
* or an ignored glossary, or one the SDK raised. Discriminate on `code`.
|
|
2662
|
+
*/
|
|
2663
|
+
type LocaleNotice = ProviderNotice | SdkNotice;
|
|
2664
|
+
/**
|
|
2665
|
+
* A key written in this run that was flagged as worth a human look, whether by the provider layer's
|
|
2666
|
+
* quality heuristics, a configured `maxLength` budget, or a fuzzy translation-memory reuse. The
|
|
2667
|
+
* value was still written; these are advisory quality signals, not rejections.
|
|
2668
|
+
*/
|
|
2669
|
+
interface NeedsReviewEntry {
|
|
2670
|
+
/** The key that was flagged. */
|
|
2671
|
+
readonly key: string;
|
|
2672
|
+
/** Why it was flagged. A key can carry more than one reason. */
|
|
2673
|
+
readonly reasons: readonly ReviewReasonCode[];
|
|
2674
|
+
}
|
|
2675
|
+
/**
|
|
2676
|
+
* A key whose translation was reused from the translation memory even though its source string had
|
|
2677
|
+
* changed, because the earlier source it was translated from is close enough to the current one.
|
|
2678
|
+
*
|
|
2679
|
+
* It is reported apart from {@link LocaleSummary.cacheHits} on purpose: an exact cache hit is the
|
|
2680
|
+
* same string it always was, while this is a translation of text that is no longer the source text.
|
|
2681
|
+
* {@link similarity} and {@link previousSource} are the evidence for the reuse, so a reader can
|
|
2682
|
+
* judge it rather than take it on trust.
|
|
2683
|
+
*
|
|
2684
|
+
* A reuse is written to the locale file but deliberately not locked: the key keeps the lock-file
|
|
2685
|
+
* baseline it already had, so it stays changed, is offered and flagged again on every later run at
|
|
2686
|
+
* no provider cost, and settles only once a real translation lands. Every key listed here also
|
|
2687
|
+
* carries `FUZZY_CACHE_REUSE` in {@link LocaleSummary.needsReview}.
|
|
2688
|
+
*/
|
|
2689
|
+
interface FuzzyCacheHit {
|
|
2690
|
+
/** The key whose translation was reused. */
|
|
2691
|
+
readonly key: string;
|
|
2692
|
+
/** The key's source text at the time this translation was produced. */
|
|
2693
|
+
readonly previousSource: string;
|
|
1132
2694
|
/**
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
2695
|
+
* How alike {@link previousSource} and the current source are, from `0` to `1`: one minus the
|
|
2696
|
+
* character edit distance divided by the longer string's length, after Unicode normalization.
|
|
2697
|
+
* Always at or above the configured `fuzzyCache.threshold`.
|
|
1135
2698
|
*/
|
|
1136
|
-
readonly
|
|
2699
|
+
readonly similarity: number;
|
|
2700
|
+
}
|
|
2701
|
+
/** A row of an imported handoff that could not be read. Reported rather than aborting the import. */
|
|
2702
|
+
interface MalformedRowReport {
|
|
2703
|
+
/** The row's 1-based index within its sheet or file. */
|
|
2704
|
+
readonly row: number;
|
|
1137
2705
|
/**
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1140
|
-
* metadata marks as not translatable. These are never translated, hashed, diffed, or checked for
|
|
1141
|
-
* placeholder or ICU integrity. Whether one survives a later rewrite of the same file depends on
|
|
1142
|
-
* the adapter: an adapter that reconstructs the whole file from `resource.entries` alone drops it,
|
|
1143
|
-
* while an adapter that patches only the entries it touches into the existing document (for
|
|
1144
|
-
* example `apple-xcstrings`) preserves it verbatim. Empty for a file with no such leaves.
|
|
2706
|
+
* The 1-based physical line in the source file, for delimited formats where a quoted value can
|
|
2707
|
+
* span several lines. Absent for `.xlsx`, which has rows but no lines.
|
|
1145
2708
|
*/
|
|
1146
|
-
readonly
|
|
2709
|
+
readonly line?: number;
|
|
2710
|
+
/** The column whose value was missing or unusable. */
|
|
2711
|
+
readonly column: string;
|
|
1147
2712
|
}
|
|
1148
2713
|
/**
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
2714
|
+
* A key that appeared more than once in an imported handoff. The first occurrence wins and the rest
|
|
2715
|
+
* are reported here, so a translator who duplicated a row learns which value was actually used.
|
|
2716
|
+
*/
|
|
2717
|
+
interface DuplicateKeyReport {
|
|
2718
|
+
/** The key that appeared more than once. */
|
|
2719
|
+
readonly key: string;
|
|
2720
|
+
/** The 1-based row index of the ignored occurrence. */
|
|
2721
|
+
readonly row: number;
|
|
2722
|
+
/** The 1-based physical line of the ignored occurrence, for delimited formats. Absent for `.xlsx`. */
|
|
2723
|
+
readonly line?: number;
|
|
2724
|
+
}
|
|
2725
|
+
/**
|
|
2726
|
+
* Everything that happened for one locale during a run. The key lists are disjoint accounts of what
|
|
2727
|
+
* became of each key, so a caller can reconstruct the whole run without re-reading any file.
|
|
1151
2728
|
*
|
|
1152
|
-
*
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
1155
|
-
* neither shape implements this interface directly, and either way it first needs its member added
|
|
1156
|
-
* to core's {@link SupportedFormat}.
|
|
2729
|
+
* A locale that failed outright still appears here with `status: "failed"` and an `error`, rather
|
|
2730
|
+
* than the whole run throwing. That is the central contract of {@link translate}: one unreachable
|
|
2731
|
+
* provider or one unwritable file does not discard the locales that succeeded.
|
|
1157
2732
|
*/
|
|
1158
|
-
interface
|
|
1159
|
-
/** The
|
|
1160
|
-
readonly
|
|
2733
|
+
interface LocaleSummary {
|
|
2734
|
+
/** The target locale this summary describes. */
|
|
2735
|
+
readonly locale: string;
|
|
1161
2736
|
/**
|
|
1162
|
-
*
|
|
1163
|
-
*
|
|
1164
|
-
*
|
|
1165
|
-
*
|
|
1166
|
-
* @param sample - An optional leading content sample to aid detection.
|
|
1167
|
-
* @returns True if this adapter could handle the file. Several adapters may return true for the same
|
|
1168
|
-
* file (all JSON adapters claim `.json`); the caller disambiguates by explicit format.
|
|
2737
|
+
* `succeeded` when no key was withheld by the integrity gate, a provider failure, or the token
|
|
2738
|
+
* budget; `partial` when some keys were withheld and others landed; `failed` when keys were
|
|
2739
|
+
* withheld and none landed, or when the locale threw. Keys skipped for invalid ICU source and
|
|
2740
|
+
* handoff rows left blank do not change the status.
|
|
1169
2741
|
*/
|
|
1170
|
-
|
|
2742
|
+
readonly status: "succeeded" | "partial" | "failed";
|
|
1171
2743
|
/**
|
|
1172
|
-
*
|
|
1173
|
-
*
|
|
1174
|
-
* @
|
|
1175
|
-
*
|
|
1176
|
-
* @returns The parsed resource and the keys whose values are invalid for the format.
|
|
1177
|
-
* @throws `AdapterError` when the content is malformed, oversized, or structurally invalid
|
|
1178
|
-
* (the implementation names the specific codes). A missing or unopenable path instead rejects with
|
|
1179
|
-
* the underlying filesystem error.
|
|
2744
|
+
* Keys that received a new value in this run: from the provider for {@link translate}, keys
|
|
2745
|
+
* sharing a translated key's source text included, or from the handoff for
|
|
2746
|
+
* {@link importWorkbook}. On a dry run, the keys a live run would send; cache hits are not
|
|
2747
|
+
* separated out, because a dry run does not read the translation memory.
|
|
1180
2748
|
*/
|
|
1181
|
-
|
|
2749
|
+
readonly translated: readonly string[];
|
|
2750
|
+
/** Keys already up to date against the lock-file baseline, so no provider call was made. */
|
|
2751
|
+
readonly unchanged: readonly string[];
|
|
2752
|
+
/** Keys present in this locale but no longer in the source. Reported, and removed only when pruning. */
|
|
2753
|
+
readonly orphaned: readonly string[];
|
|
2754
|
+
/** Orphaned keys that were actually removed, which happens only when the run was asked to prune. */
|
|
2755
|
+
readonly pruned: readonly string[];
|
|
1182
2756
|
/**
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1185
|
-
* @param resource - The resource to serialize.
|
|
1186
|
-
* @param filePath - The destination file.
|
|
1187
|
-
* @throws `AdapterError` if the resource cannot be represented in the format; rejects with the
|
|
1188
|
-
* underlying filesystem error on a write failure.
|
|
2757
|
+
* Keys whose source text is not valid ICU. They are skipped rather than sent to the provider,
|
|
2758
|
+
* because a broken source message cannot yield a sound translation.
|
|
1189
2759
|
*/
|
|
1190
|
-
|
|
2760
|
+
readonly invalidIcuSource: readonly string[];
|
|
1191
2761
|
/**
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
1194
|
-
* @
|
|
1195
|
-
* @returns The placeholder tokens found, in document order. Does not throw.
|
|
2762
|
+
* Keys served from the translation memory instead of the provider, and so not paid for. Only
|
|
2763
|
+
* exact matches: a reuse for a source string that has since changed is reported in
|
|
2764
|
+
* {@link fuzzyHits} instead, never here.
|
|
1196
2765
|
*/
|
|
1197
|
-
|
|
2766
|
+
readonly cacheHits: readonly string[];
|
|
2767
|
+
/**
|
|
2768
|
+
* Keys whose translation was reused for a source string that had changed, with the score and the
|
|
2769
|
+
* earlier source behind each reuse. Always empty unless `fuzzyCache.enabled` was set. These keys
|
|
2770
|
+
* are written but not locked, so they reappear here on every run until they are really
|
|
2771
|
+
* translated. See {@link FuzzyCacheHit}.
|
|
2772
|
+
*/
|
|
2773
|
+
readonly fuzzyHits: readonly FuzzyCacheHit[];
|
|
2774
|
+
/**
|
|
2775
|
+
* Keys whose translation was refused by the integrity gate, for instance because it dropped a
|
|
2776
|
+
* placeholder. The previous translation, if any, is left untouched.
|
|
2777
|
+
*/
|
|
2778
|
+
readonly integrityMismatches: readonly string[];
|
|
2779
|
+
/** Keys the provider failed to translate, for instance because their sub-batch errored. */
|
|
2780
|
+
readonly providerFailures: readonly string[];
|
|
2781
|
+
/**
|
|
2782
|
+
* Keys whose plural categories were generated for the target language rather than translated one
|
|
2783
|
+
* by one. On a dry run, the forms a live run would generate: generation is a provider call of
|
|
2784
|
+
* its own, so a dry run reports it rather than reporting nothing.
|
|
2785
|
+
*/
|
|
2786
|
+
readonly generated: readonly string[];
|
|
2787
|
+
/** Keys not translated because the token budget was exhausted under `stop` behavior. */
|
|
2788
|
+
readonly budgetWithheld: readonly string[];
|
|
2789
|
+
/** Token usage for this locale. Absent when the provider does not report usage. */
|
|
2790
|
+
readonly usage?: UsageSummary;
|
|
2791
|
+
/** Provider and SDK notices raised while running this locale. Always present, possibly empty. */
|
|
2792
|
+
readonly notices: readonly LocaleNotice[];
|
|
2793
|
+
/** Translated keys flagged as worth a human look. The translations were still written. */
|
|
2794
|
+
readonly needsReview: readonly NeedsReviewEntry[];
|
|
1198
2795
|
/**
|
|
1199
|
-
*
|
|
1200
|
-
*
|
|
1201
|
-
* message syntax to violate and returns true for any value.
|
|
1202
|
-
*
|
|
1203
|
-
* @param value - The candidate translated value to validate.
|
|
1204
|
-
* @returns True when the value is valid for the format's message syntax. Does not throw; an
|
|
1205
|
-
* unparseable value returns false.
|
|
2796
|
+
* Keys an imported handoff left blank while they were missing or out of date in this locale.
|
|
2797
|
+
* Always empty for a {@link translate} run.
|
|
1206
2798
|
*/
|
|
1207
|
-
|
|
2799
|
+
readonly unfilled: readonly string[];
|
|
2800
|
+
/** Unreadable rows from an imported handoff. Always empty for a {@link translate} run. */
|
|
2801
|
+
readonly malformedRows: readonly MalformedRowReport[];
|
|
2802
|
+
/** Repeated keys from an imported handoff. Always empty for a {@link translate} run. */
|
|
2803
|
+
readonly duplicateKeys: readonly DuplicateKeyReport[];
|
|
1208
2804
|
/**
|
|
1209
|
-
*
|
|
1210
|
-
*
|
|
1211
|
-
* families define one, for different reasons:
|
|
1212
|
-
*
|
|
1213
|
-
* - The ICU formats (next-intl and ARB) compare branch by branch, because flattening a
|
|
1214
|
-
* plural/select value loses which branch a placeholder came from.
|
|
1215
|
-
* - The double-brace formats (i18next, ngx-translate, and YAML) add a one-directional
|
|
1216
|
-
* single-brace check on top of their flat multiset comparison: a `{name}`-shaped token present
|
|
1217
|
-
* in the translated value but absent from the source is reported as `extra`. Single-brace text
|
|
1218
|
-
* is ordinary literal content under these formats' default delimiters, so it is never required
|
|
1219
|
-
* to survive a translation, but inventing one is never a legitimate translation either.
|
|
1220
|
-
*
|
|
1221
|
-
* Absent for the remaining adapters, which are compared via `extractPlaceholders` plus a flat
|
|
1222
|
-
* multiset check.
|
|
2805
|
+
* Why this locale failed, when the failure came from a thrown error. The `code` is the failure's
|
|
2806
|
+
* own code where it has one, and `LOCALE_FAILED` otherwise. Never carries a secret.
|
|
1223
2807
|
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
2808
|
+
* Absent unless `status` is `failed`, but a `failed` locale does not always carry it: a locale
|
|
2809
|
+
* whose every key was withheld by the integrity gate, a provider failure, or the token budget is
|
|
2810
|
+
* `failed` with nothing thrown, so this stays undefined and the withheld keys are the account of
|
|
2811
|
+
* what went wrong. Treat it as an optional detail on a failure, never as the failure test.
|
|
1227
2812
|
*/
|
|
1228
|
-
|
|
2813
|
+
readonly error?: {
|
|
2814
|
+
/** The failure's own code where it had one, and `LOCALE_FAILED` otherwise. */
|
|
2815
|
+
readonly code: string;
|
|
2816
|
+
/** A human-readable description of the failure. Never contains a secret. */
|
|
2817
|
+
readonly message: string;
|
|
2818
|
+
};
|
|
1229
2819
|
}
|
|
1230
|
-
|
|
1231
2820
|
/**
|
|
1232
|
-
*
|
|
1233
|
-
*
|
|
2821
|
+
* The result of a whole run, returned by {@link translate}, {@link watch}, and
|
|
2822
|
+
* {@link importWorkbook}.
|
|
1234
2823
|
*
|
|
1235
|
-
*
|
|
1236
|
-
*
|
|
1237
|
-
*
|
|
1238
|
-
* claimed the file, which is routine since every JSON adapter claims `.json`; it reports the competing
|
|
1239
|
-
* candidates instead of guessing, and the caller resolves it by naming a format explicitly.
|
|
2824
|
+
* Per-locale outcomes are data, not exceptions: inspect `failed` and `partial` to decide an exit
|
|
2825
|
+
* code rather than relying on the call to throw. Only whole-run failures, such as an invalid config
|
|
2826
|
+
* or an unreadable source file, throw an {@link SdkError}.
|
|
1240
2827
|
*/
|
|
1241
|
-
|
|
1242
|
-
/**
|
|
1243
|
-
readonly
|
|
1244
|
-
/** The adapter to read and write the file with. */
|
|
1245
|
-
readonly adapter: FormatAdapter;
|
|
1246
|
-
} | {
|
|
1247
|
-
/** No registered adapter claimed the file. */
|
|
1248
|
-
readonly status: "no-match";
|
|
1249
|
-
/** The file that could not be matched. */
|
|
1250
|
-
readonly filePath: string;
|
|
2828
|
+
interface RunSummary {
|
|
2829
|
+
/** True when the run computed everything but wrote nothing and called no provider. */
|
|
2830
|
+
readonly dryRun: boolean;
|
|
1251
2831
|
/**
|
|
1252
|
-
* The
|
|
1253
|
-
*
|
|
2832
|
+
* The full per-locale account. A {@link translate} or {@link watch} run reports them in
|
|
2833
|
+
* configured target order, whatever order a `locales` subset was given in and whatever order
|
|
2834
|
+
* concurrent locales finished in. An {@link importWorkbook} run does not: it reports them in the
|
|
2835
|
+
* order the handoff yielded them, then appends the configured locales the handoff had nothing
|
|
2836
|
+
* for. Match an entry on its `locale` rather than on its position.
|
|
1254
2837
|
*/
|
|
1255
|
-
readonly
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
readonly status: "ambiguous";
|
|
1259
|
-
/** The file that several adapters claimed. */
|
|
1260
|
-
readonly filePath: string;
|
|
1261
|
-
/** The competing formats. Resolve the ambiguity by naming one of them explicitly. */
|
|
1262
|
-
readonly candidates: readonly SupportedFormat[];
|
|
1263
|
-
};
|
|
1264
|
-
/** Options for {@link AdapterRegistry.resolve}: select a format explicitly, or aid detection. */
|
|
1265
|
-
interface ResolveOptions {
|
|
1266
|
-
/** A leading content sample to aid detection. Ignored when `format` is given. */
|
|
1267
|
-
readonly sample?: string;
|
|
1268
|
-
/** Bypass detection and select this format explicitly. */
|
|
1269
|
-
readonly format?: SupportedFormat;
|
|
1270
|
-
}
|
|
1271
|
-
/**
|
|
1272
|
-
* Holds the registered adapters and resolves one for a file. Open for extension: adapters attach
|
|
1273
|
-
* through {@link AdapterRegistry.register} without changing resolution logic, and resolution never
|
|
1274
|
-
* throws.
|
|
1275
|
-
*/
|
|
1276
|
-
declare class AdapterRegistry {
|
|
1277
|
-
private readonly adapters;
|
|
2838
|
+
readonly locales: readonly LocaleSummary[];
|
|
2839
|
+
/** Names of the locales whose status is `succeeded`. */
|
|
2840
|
+
readonly succeeded: readonly string[];
|
|
1278
2841
|
/**
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1281
|
-
*
|
|
1282
|
-
* @param adapter - The adapter to add.
|
|
1283
|
-
* @returns This registry, for chaining.
|
|
2842
|
+
* Names of the locales whose status is `partial`: written to disk, but with keys still missing.
|
|
2843
|
+
* Check this alongside `failed` before treating a run as clean. The CLI exits `1` on a partial
|
|
2844
|
+
* locale for that reason.
|
|
1284
2845
|
*/
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
2846
|
+
readonly partial: readonly string[];
|
|
2847
|
+
/** Names of the locales whose status is `failed`. Check this before treating a run as clean. */
|
|
2848
|
+
readonly failed: readonly string[];
|
|
2849
|
+
/** Token usage summed across every locale. Absent when the provider does not report usage. */
|
|
2850
|
+
readonly usage?: UsageSummary;
|
|
2851
|
+
/** The token budget in force, present only when the config set one. */
|
|
2852
|
+
readonly budget?: RunBudget;
|
|
1289
2853
|
/**
|
|
1290
|
-
*
|
|
1291
|
-
*
|
|
1292
|
-
* @param filePath - The file to resolve an adapter for.
|
|
1293
|
-
* @param options - `format` selects explicitly and skips detection; `sample` aids detection.
|
|
1294
|
-
* @returns A structured {@link AdapterResolution}: `resolved`, `no-match`, or `ambiguous`. Never
|
|
1295
|
-
* throws; an unresolvable file is a status, not an exception.
|
|
2854
|
+
* What the run would have cost, present only when it was asked for a pre-run estimate. Always a
|
|
2855
|
+
* dry run: an estimate constructs no provider and spends nothing.
|
|
1296
2856
|
*/
|
|
1297
|
-
|
|
2857
|
+
readonly estimate?: RunEstimate;
|
|
1298
2858
|
}
|
|
1299
2859
|
|
|
2860
|
+
/**
|
|
2861
|
+
* Where a finished run stands against its token budget.
|
|
2862
|
+
*
|
|
2863
|
+
* - `within`: the run never reached its ceiling.
|
|
2864
|
+
* - `stopped-before-ceiling`: under `stop`, a request was withheld because sending it would have
|
|
2865
|
+
* passed the ceiling, while the counted total was still below it.
|
|
2866
|
+
* - `reached`: the counted total reached or passed the ceiling.
|
|
2867
|
+
*/
|
|
2868
|
+
type BudgetStanding = "within" | "stopped-before-ceiling" | "reached";
|
|
2869
|
+
/**
|
|
2870
|
+
* Classifies a {@link RunBudget} into its {@link BudgetStanding}, so every surface that reports a
|
|
2871
|
+
* budget tells a `stop` run halted short of its ceiling apart from one that actually reached it.
|
|
2872
|
+
*
|
|
2873
|
+
* @param budget - The budget from {@link RunSummary.budget} or {@link RunStatusFile.budget}.
|
|
2874
|
+
* @returns The standing. Pure; throws nothing.
|
|
2875
|
+
*/
|
|
2876
|
+
declare function budgetStanding(budget: RunBudget): BudgetStanding;
|
|
2877
|
+
|
|
1300
2878
|
/** One locale's counts in a {@link CheckSummary}. */
|
|
1301
2879
|
interface LocaleCheckSummary {
|
|
1302
2880
|
/** The target locale these counts describe. */
|
|
@@ -1309,6 +2887,12 @@ interface LocaleCheckSummary {
|
|
|
1309
2887
|
readonly upToDate: number;
|
|
1310
2888
|
/** True when this locale has nothing missing and nothing stale. */
|
|
1311
2889
|
readonly inSync: boolean;
|
|
2890
|
+
/**
|
|
2891
|
+
* Every source string this locale translates more than one way under different keys, present only
|
|
2892
|
+
* when {@link CheckInput.consistency} is true (an empty array then means the locale is
|
|
2893
|
+
* consistent). This is a report and nothing more: it never changes `inSync` or any count.
|
|
2894
|
+
*/
|
|
2895
|
+
readonly inconsistencies?: readonly InconsistencyGroup[];
|
|
1312
2896
|
}
|
|
1313
2897
|
/** The result of {@link check}: per-locale counts plus one project-wide verdict. */
|
|
1314
2898
|
interface CheckSummary {
|
|
@@ -1325,6 +2909,11 @@ interface CheckInput {
|
|
|
1325
2909
|
readonly cwd?: string;
|
|
1326
2910
|
/** Restrict the report to these target locales. Defaults to every configured target locale. */
|
|
1327
2911
|
readonly locales?: readonly string[];
|
|
2912
|
+
/**
|
|
2913
|
+
* Also report, per locale, every source string translated more than one way under different keys
|
|
2914
|
+
* (see {@link LocaleCheckSummary.inconsistencies}). Defaults to false.
|
|
2915
|
+
*/
|
|
2916
|
+
readonly consistency?: boolean;
|
|
1328
2917
|
}
|
|
1329
2918
|
/** Injectable dependencies for {@link check}. Every field has a working default. */
|
|
1330
2919
|
interface CheckDeps {
|
|
@@ -1345,6 +2934,16 @@ interface CheckDeps {
|
|
|
1345
2934
|
*
|
|
1346
2935
|
* Use {@link diff} instead when you need the key names rather than the counts.
|
|
1347
2936
|
*
|
|
2937
|
+
* With {@link CheckInput.consistency} set, each locale also lists the source strings it translates
|
|
2938
|
+
* more than one way under different keys. Only keys that are up to date are compared, since a stale
|
|
2939
|
+
* translation belongs to an older source text. Values are compared after Unicode NFC normalization,
|
|
2940
|
+
* folding line endings, and trimming leading and trailing whitespace; internal whitespace and letter
|
|
2941
|
+
* case count. Keys whose description, meaning, plural flag, or gettext `msgctxt` differ are never
|
|
2942
|
+
* grouped, and neither are the different plural forms of one key: a format that stores each form
|
|
2943
|
+
* under its own key (i18next, Apple `.stringsdict` and `.xcstrings`, Android, gettext) is compared
|
|
2944
|
+
* per plural category or gettext `msgstr` index. The report never affects `inSync`, a count, or any
|
|
2945
|
+
* file, and a translation identical to its own source is not a finding here.
|
|
2946
|
+
*
|
|
1348
2947
|
* Note that a malformed target locale file surfaces the adapter's own error and code rather than a
|
|
1349
2948
|
* wrapped {@link SdkError}, because only source reads are wrapped. Its message names the offending
|
|
1350
2949
|
* locale and the resolved path. A caller that maps SDK codes should be ready for an unrecognized
|
|
@@ -1366,6 +2965,121 @@ interface CheckDeps {
|
|
|
1366
2965
|
*/
|
|
1367
2966
|
declare function check(input: CheckInput, deps?: CheckDeps): Promise<CheckSummary>;
|
|
1368
2967
|
|
|
2968
|
+
/**
|
|
2969
|
+
* Why an unused-key report could not be produced at all.
|
|
2970
|
+
*
|
|
2971
|
+
* - `EXTRACT_NOT_CONFIGURED`: the config carries no `extract` block, so there is no source to scan.
|
|
2972
|
+
* - `EXTRACT_FS_UNSUPPORTED`: the supplied file system implements no `readDirectory`.
|
|
2973
|
+
* - `FRAMEWORK_NOT_MODELED`: the configured format is loaded by a runtime (next-intl, Vue I18n, or
|
|
2974
|
+
* ngx-translate) whose key lookups the i18next source scan does not model.
|
|
2975
|
+
* - `NO_SOURCE_FILES`: there is no source file under the configured roots at all.
|
|
2976
|
+
* - `NO_REFERENCES_FOUND`: the scanned files name no translation key and hold no dynamic key site,
|
|
2977
|
+
* while the catalog is not empty, so the scan most likely looked at the wrong code.
|
|
2978
|
+
*/
|
|
2979
|
+
type UnusedKeysNotRunReason = "EXTRACT_NOT_CONFIGURED" | "EXTRACT_FS_UNSUPPORTED" | "FRAMEWORK_NOT_MODELED" | "NO_SOURCE_FILES" | "NO_REFERENCES_FOUND";
|
|
2980
|
+
/**
|
|
2981
|
+
* Why a completed scan cannot rule a key unused with confidence.
|
|
2982
|
+
*
|
|
2983
|
+
* - `dynamic-keys`: a key argument has no static part (`t(key)`, a template literal with no static
|
|
2984
|
+
* head, a concatenation), so any catalog key could be the one it reaches.
|
|
2985
|
+
* - `dynamic-key-prefix`: a `keyPrefix` option or a `getFixedT` prefix argument is not a static
|
|
2986
|
+
* string, so every key called under it is unknown.
|
|
2987
|
+
* - `trans-without-key`: a `Trans` element carries no static `i18nKey`.
|
|
2988
|
+
* - `aliased-translate-function`: the translate function is assigned somewhere its calls cannot be
|
|
2989
|
+
* followed, such as an object property.
|
|
2990
|
+
* - `translate-function-escapes`: a translate function bound from a recognised source appears
|
|
2991
|
+
* somewhere other than a direct call, its own binding, a local alias, or the `t` attribute of a
|
|
2992
|
+
* `Trans` or `Translation` element, so code the scan cannot follow may call it with any key.
|
|
2993
|
+
* - `unrecognised-translate-source`: `useTranslation`, `withTranslation`, `getFixedT`, a
|
|
2994
|
+
* `Translation` render prop, a member `t`, or a `t` import is used in a shape the scan does not
|
|
2995
|
+
* recognise, so the prefix and keys of the function it yields are unknown.
|
|
2996
|
+
* - `template-files-not-scanned`: the roots hold template files (`.vue`, `.svelte`, `.html`, and
|
|
2997
|
+
* similar) the scan does not read.
|
|
2998
|
+
* - `incomplete-scan`: a file or directory could not be read to its end.
|
|
2999
|
+
*/
|
|
3000
|
+
type UnusedKeysUnreliableReason = "dynamic-keys" | "dynamic-key-prefix" | "trans-without-key" | "aliased-translate-function" | "translate-function-escapes" | "unrecognised-translate-source" | "template-files-not-scanned" | "incomplete-scan";
|
|
3001
|
+
/** One place that makes an unused-key report unreliable. */
|
|
3002
|
+
interface UnusedKeysSite {
|
|
3003
|
+
/** The file or directory, relative to the run's working directory, with forward slashes. */
|
|
3004
|
+
readonly file: string;
|
|
3005
|
+
/** The one-based line, when the reason points at a line. */
|
|
3006
|
+
readonly line?: number;
|
|
3007
|
+
/** For `incomplete-scan`, why the file was skipped (for example `too-large`). */
|
|
3008
|
+
readonly detail?: string;
|
|
3009
|
+
}
|
|
3010
|
+
/** One reason an unused-key report is unreliable, with the places that caused it. */
|
|
3011
|
+
interface UnusedKeysUnreliability {
|
|
3012
|
+
/** The stable reason code. */
|
|
3013
|
+
readonly reason: UnusedKeysUnreliableReason;
|
|
3014
|
+
/** How many places caused it. For template files this can exceed the listed `sites`. */
|
|
3015
|
+
readonly count: number;
|
|
3016
|
+
/**
|
|
3017
|
+
* The places, deduplicated on file and line. Every place is listed, except template files, which
|
|
3018
|
+
* are capped at the first 20.
|
|
3019
|
+
*/
|
|
3020
|
+
readonly sites: readonly UnusedKeysSite[];
|
|
3021
|
+
}
|
|
3022
|
+
/**
|
|
3023
|
+
* A source-catalog key in an unused-key report. `key` is the key as a person and the source spell
|
|
3024
|
+
* it, with the format's internal encoding undone (`Welcome. Enjoy` rather than `Welcome\. Enjoy`,
|
|
3025
|
+
* a gettext `msgctxt` shown as `context|msgid`); `extract.unused.ignore` patterns match it.
|
|
3026
|
+
* `catalogKey` is the key exactly as the catalog adapter and the per-locale `orphaned` lists spell
|
|
3027
|
+
* it, for a tool that needs to address the entry.
|
|
3028
|
+
*/
|
|
3029
|
+
interface UnusedKey {
|
|
3030
|
+
/** The decoded key, as shown to people and matched by `extract.unused.ignore`. */
|
|
3031
|
+
readonly key: string;
|
|
3032
|
+
/** The key as the catalog adapter encodes it. */
|
|
3033
|
+
readonly catalogKey: string;
|
|
3034
|
+
}
|
|
3035
|
+
/** A source-catalog key no static reference names, but a template-literal key could still reach. */
|
|
3036
|
+
interface PossiblyDynamicKey extends UnusedKey {
|
|
3037
|
+
/** The static head of the template-literal key that could reach it. */
|
|
3038
|
+
readonly prefix: string;
|
|
3039
|
+
}
|
|
3040
|
+
/** A template-literal key argument with a static head, located in the source. */
|
|
3041
|
+
interface UnusedKeysPrefixSite extends SourceLocation {
|
|
3042
|
+
/** The static head every key the site can reach starts with. */
|
|
3043
|
+
readonly prefix: string;
|
|
3044
|
+
}
|
|
3045
|
+
/** An unused-key report produced from a source scan. */
|
|
3046
|
+
interface UnusedKeysScan {
|
|
3047
|
+
/**
|
|
3048
|
+
* `complete` when nothing in the scan casts doubt on the verdict; `unreliable` when
|
|
3049
|
+
* {@link UnusedKeysScan.unreliableBecause} is not empty, so a listed key may still be in use.
|
|
3050
|
+
*/
|
|
3051
|
+
readonly status: "complete" | "unreliable";
|
|
3052
|
+
/** Every reason the verdict cannot be trusted. Empty exactly when `status` is `complete`. */
|
|
3053
|
+
readonly unreliableBecause: readonly UnusedKeysUnreliability[];
|
|
3054
|
+
/** How many source files were read and scanned. */
|
|
3055
|
+
readonly scannedFiles: number;
|
|
3056
|
+
/** Source-catalog keys no scanned reference names, in catalog order. */
|
|
3057
|
+
readonly unused: readonly UnusedKey[];
|
|
3058
|
+
/**
|
|
3059
|
+
* Unreferenced source-catalog keys a template-literal key with a static head could reach, in
|
|
3060
|
+
* catalog order. They are listed apart from `unused` and never fail a run.
|
|
3061
|
+
*/
|
|
3062
|
+
readonly possiblyDynamic: readonly PossiblyDynamicKey[];
|
|
3063
|
+
/** Unreferenced source-catalog keys matched by `extract.unused.ignore`, in catalog order. */
|
|
3064
|
+
readonly ignored: readonly UnusedKey[];
|
|
3065
|
+
/** The template-literal key sites behind `possiblyDynamic`. */
|
|
3066
|
+
readonly dynamicPrefixes: readonly UnusedKeysPrefixSite[];
|
|
3067
|
+
}
|
|
3068
|
+
/**
|
|
3069
|
+
* The source-locale catalog keys that nothing in the scanned source references. Distinct from the
|
|
3070
|
+
* per-locale `orphaned` lists, which compare a target locale against the source catalog.
|
|
3071
|
+
*/
|
|
3072
|
+
type UnusedKeysReport = UnusedKeysNotRun | UnusedKeysScan;
|
|
3073
|
+
/** An unused-key report that could not run. It carries no key list, so nothing is misreported. */
|
|
3074
|
+
interface UnusedKeysNotRun {
|
|
3075
|
+
/** Always `not-run`. */
|
|
3076
|
+
readonly status: "not-run";
|
|
3077
|
+
/** The stable reason code. Branch on this, not on the message. */
|
|
3078
|
+
readonly reason: UnusedKeysNotRunReason;
|
|
3079
|
+
/** A human-readable explanation. */
|
|
3080
|
+
readonly message: string;
|
|
3081
|
+
}
|
|
3082
|
+
|
|
1369
3083
|
/** One locale's pending work in a {@link DiffSummary}, as key names rather than counts. */
|
|
1370
3084
|
interface LocaleDiff {
|
|
1371
3085
|
/** The target locale this entry describes. */
|
|
@@ -1391,22 +3105,45 @@ interface DiffSummary {
|
|
|
1391
3105
|
readonly hasPendingChanges: boolean;
|
|
1392
3106
|
/** Per-locale key lists, in configured target order. */
|
|
1393
3107
|
readonly locales: readonly LocaleDiff[];
|
|
3108
|
+
/**
|
|
3109
|
+
* Source-catalog keys nothing in the scanned application source references. Present only when
|
|
3110
|
+
* {@link DiffInput.unused} was set. It is a separate axis from each locale's `orphaned` list and
|
|
3111
|
+
* never flips {@link DiffSummary.hasPendingChanges}.
|
|
3112
|
+
*/
|
|
3113
|
+
readonly unused?: UnusedKeysReport;
|
|
1394
3114
|
}
|
|
1395
3115
|
/** Input for {@link diff}. */
|
|
1396
3116
|
interface DiffInput {
|
|
1397
3117
|
/** The resolved project config, normally from {@link loadConfig}. */
|
|
1398
3118
|
readonly config: VerbatraConfig;
|
|
1399
|
-
/**
|
|
3119
|
+
/**
|
|
3120
|
+
* Directory the `files.pattern` is resolved against. Defaults to the process working directory.
|
|
3121
|
+
*/
|
|
1400
3122
|
readonly cwd?: string;
|
|
1401
3123
|
/** Restrict the report to these target locales. Defaults to every configured target locale. */
|
|
1402
3124
|
readonly locales?: readonly string[];
|
|
3125
|
+
/**
|
|
3126
|
+
* Also scan the application source configured in the `extract` block and report the
|
|
3127
|
+
* source-catalog keys nothing references, as {@link DiffSummary.unused}. Off by default.
|
|
3128
|
+
*/
|
|
3129
|
+
readonly unused?: boolean;
|
|
1403
3130
|
}
|
|
1404
3131
|
/** Injectable dependencies for {@link diff}. Every field has a working default. */
|
|
1405
3132
|
interface DiffDeps {
|
|
1406
|
-
/**
|
|
3133
|
+
/**
|
|
3134
|
+
* Format-adapter registry to resolve the configured format. Defaults to the built-in registry.
|
|
3135
|
+
*/
|
|
1407
3136
|
readonly adapterRegistry?: AdapterRegistry;
|
|
1408
|
-
/**
|
|
3137
|
+
/**
|
|
3138
|
+
* File-system port. Defaults to the real file system. The unused-key scan needs its
|
|
3139
|
+
* `readDirectory` member and reports its absence as `EXTRACT_FS_UNSUPPORTED`.
|
|
3140
|
+
*/
|
|
1409
3141
|
readonly fs?: SdkFs;
|
|
3142
|
+
/**
|
|
3143
|
+
* Extractor factory for the unused-key scan. Defaults to the built-in table keyed by the
|
|
3144
|
+
* configured framework.
|
|
3145
|
+
*/
|
|
3146
|
+
readonly createExtractor?: (framework: SourceFramework) => SourceExtractor;
|
|
1410
3147
|
}
|
|
1411
3148
|
/**
|
|
1412
3149
|
* Reports the per-locale drift between the source and each target locale as key names, without
|
|
@@ -1417,19 +3154,34 @@ interface DiffDeps {
|
|
|
1417
3154
|
* since the key was last translated rather than merely that the two strings differ. Orphaned keys
|
|
1418
3155
|
* are reported but never removed here; pruning happens only in {@link translate}.
|
|
1419
3156
|
*
|
|
3157
|
+
* With `unused` set, it also scans the application source named by the config's `extract` block
|
|
3158
|
+
* and reports, in {@link DiffSummary.unused}, the source-catalog keys no static reference names.
|
|
3159
|
+
* The scan models the i18next runtime: `t` calls and their aliases, `keyPrefix` and `getFixedT`
|
|
3160
|
+
* prefixes, `Trans` keys, namespace-qualified keys, and the plural and context variants of a
|
|
3161
|
+
* referenced key. It is read-only like the rest of `diff`: it never removes, rewrites, or reorders
|
|
3162
|
+
* a catalog, and it constructs no provider and reads no API key. Keys matched by
|
|
3163
|
+
* `extract.unused.ignore` are listed as `ignored`, and keys a template-literal key with a static
|
|
3164
|
+
* head could reach as `possiblyDynamic`, never as `unused`. The report is `complete` only when the
|
|
3165
|
+
* scan can bound every key the source reaches; otherwise it is `unreliable` and names each reason
|
|
3166
|
+
* with its sites, or `not-run` with a reason code and no key list at all when there is nothing
|
|
3167
|
+
* trustworthy to judge by (no `extract` block, a file system with no `readDirectory`, a format
|
|
3168
|
+
* whose runtime is not modeled, no source file, or no reference anywhere in the scanned files).
|
|
3169
|
+
*
|
|
1420
3170
|
* Note that a malformed target locale file surfaces the adapter's own error and code rather than a
|
|
1421
3171
|
* wrapped {@link SdkError}, because only source reads are wrapped. Its message names the offending
|
|
1422
3172
|
* locale and the resolved path. A caller that maps SDK codes should be ready for an unrecognized
|
|
1423
3173
|
* error from a target file.
|
|
1424
3174
|
*
|
|
1425
|
-
* @param input - The config
|
|
1426
|
-
* @param deps - Optional adapter registry
|
|
1427
|
-
* @returns Per-locale missing, changed, and orphaned key lists
|
|
3175
|
+
* @param input - The config, the optional locale filter, and whether to report unused keys.
|
|
3176
|
+
* @param deps - Optional adapter registry, file-system, and extractor-factory overrides.
|
|
3177
|
+
* @returns Per-locale missing, changed, and orphaned key lists, plus the unused-key report when
|
|
3178
|
+
* asked for.
|
|
1428
3179
|
*
|
|
1429
3180
|
* @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
|
|
1430
3181
|
* @throws {@link SdkError} `LOCALE_LAYOUT_INVALID`: the `files.pattern` and `files.localeStyle`
|
|
1431
3182
|
* cannot be combined, or a configured locale has no valid path spelling under that style.
|
|
1432
|
-
* @throws {@link SdkError} `LOCALE_PATH_COLLISION`: two configured locales resolve to the same
|
|
3183
|
+
* @throws {@link SdkError} `LOCALE_PATH_COLLISION`: two configured locales resolve to the same
|
|
3184
|
+
* path.
|
|
1433
3185
|
* @throws {@link SdkError} `SOURCE_UNREADABLE`: the source locale file does not exist.
|
|
1434
3186
|
* @throws {@link SdkError} `SOURCE_INVALID`: the source locale file could not be parsed.
|
|
1435
3187
|
* @throws {@link SdkError} `LOCK_FILE_INVALID`: the lock-file is corrupt, oversized, or at an
|
|
@@ -1447,8 +3199,11 @@ declare function diff(input: DiffInput, deps?: DiffDeps): Promise<DiffSummary>;
|
|
|
1447
3199
|
* - `api-key`: the environment variable the configured provider reads its key from is set.
|
|
1448
3200
|
* - `source-file`: the source locale file exists at its resolved path, is a regular file, and
|
|
1449
3201
|
* parses under the configured format.
|
|
3202
|
+
* - `untranslated-literals`: the application source configured in the `extract` block holds no
|
|
3203
|
+
* hardcoded user-facing string literal and no file the scan could not read. It runs only when
|
|
3204
|
+
* {@link DoctorInput.literals} is set.
|
|
1450
3205
|
*/
|
|
1451
|
-
type DoctorCheckId = "config" | "format-adapter" | "provider" | "api-key" | "source-file";
|
|
3206
|
+
type DoctorCheckId = "config" | "format-adapter" | "provider" | "api-key" | "source-file" | "untranslated-literals";
|
|
1452
3207
|
/**
|
|
1453
3208
|
* The verdict on one {@link DoctorCheck}. `skipped` is reported only for the checks that need a
|
|
1454
3209
|
* loaded config when the `config` check itself failed, so a skipped check is never a problem of its
|
|
@@ -1474,8 +3229,19 @@ interface DoctorCheck {
|
|
|
1474
3229
|
interface DoctorResult {
|
|
1475
3230
|
/** True only when no check failed. This is the value a script should branch on. */
|
|
1476
3231
|
readonly ok: boolean;
|
|
1477
|
-
/**
|
|
3232
|
+
/**
|
|
3233
|
+
* Every check that ran, always in the same order. A setup run has one entry per setup check:
|
|
3234
|
+
* `config`, `format-adapter`, `provider`, `api-key`, and `source-file`. A literal run
|
|
3235
|
+
* ({@link DoctorInput.literals}) has exactly two: `config` and `untranslated-literals`.
|
|
3236
|
+
*/
|
|
1478
3237
|
readonly checks: readonly DoctorCheck[];
|
|
3238
|
+
/**
|
|
3239
|
+
* What the untranslated-literal scan found. Present only on a literal run whose scan actually
|
|
3240
|
+
* ran; absent when the config could not be loaded, no `extract` block is configured, the file
|
|
3241
|
+
* system cannot list directories, or the scan failed to start, each of which fails the
|
|
3242
|
+
* `untranslated-literals` check instead.
|
|
3243
|
+
*/
|
|
3244
|
+
readonly literals?: LiteralScan;
|
|
1479
3245
|
}
|
|
1480
3246
|
/** Input for {@link doctor}. */
|
|
1481
3247
|
interface DoctorInput {
|
|
@@ -1483,6 +3249,13 @@ interface DoctorInput {
|
|
|
1483
3249
|
readonly cwd?: string;
|
|
1484
3250
|
/** An explicit config file to validate, bypassing the search. A missing file is an error rather than a failed check. */
|
|
1485
3251
|
readonly configPath?: string;
|
|
3252
|
+
/**
|
|
3253
|
+
* Run the untranslated-literal scan instead of the setup checks: the config is loaded, then the
|
|
3254
|
+
* source roots of its `extract` block are scanned for hardcoded user-facing string literals. The
|
|
3255
|
+
* provider, API-key, format, and source-file checks do not run, so no API key environment
|
|
3256
|
+
* variable is looked at and a run with no key set can pass.
|
|
3257
|
+
*/
|
|
3258
|
+
readonly literals?: boolean;
|
|
1486
3259
|
}
|
|
1487
3260
|
/** Injectable dependencies for {@link doctor}. Every field has a working default. */
|
|
1488
3261
|
interface DoctorDeps {
|
|
@@ -1490,8 +3263,8 @@ interface DoctorDeps {
|
|
|
1490
3263
|
readonly adapterRegistry?: AdapterRegistry;
|
|
1491
3264
|
/**
|
|
1492
3265
|
* File-system port. Threaded into the config loader, so it backs the glossary-file read the
|
|
1493
|
-
* `config` check performs, and used to read and parse the source locale file.
|
|
1494
|
-
* real file system.
|
|
3266
|
+
* `config` check performs, and used to read and parse the source locale file. On a literal run it
|
|
3267
|
+
* backs the source scan, which needs its `readDirectory` member. Defaults to the real file system.
|
|
1495
3268
|
*/
|
|
1496
3269
|
readonly fs?: SdkFs;
|
|
1497
3270
|
/** Config loader. Defaults to {@link loadConfigWithMeta}. */
|
|
@@ -1527,7 +3300,18 @@ interface DoctorDeps {
|
|
|
1527
3300
|
* verdict they could not reach, and {@link DoctorResult.ok} is false because the config check
|
|
1528
3301
|
* itself failed.
|
|
1529
3302
|
*
|
|
1530
|
-
*
|
|
3303
|
+
* With `literals: true` it runs the untranslated-literal scan instead of the setup checks. The
|
|
3304
|
+
* source roots of the config's `extract` block are read, never written, and every string literal
|
|
3305
|
+
* or piece of JSX text that reads as user-facing text without going through a recognised
|
|
3306
|
+
* translation call is returned in {@link DoctorResult.literals} with its file, line, column, and
|
|
3307
|
+
* an excerpt of at most 80 characters. Literals held back by a `verbatra-ignore-next-line` or
|
|
3308
|
+
* `verbatra-ignore-line` comment, or by `extract.literals.ignore`, are returned under `suppressed`
|
|
3309
|
+
* rather than dropped. The check fails when anything was found or when a file could not be
|
|
3310
|
+
* scanned, so a partial scan is never reported as clean. No provider is constructed and no API key
|
|
3311
|
+
* environment variable is read.
|
|
3312
|
+
*
|
|
3313
|
+
* @param input - The working directory, an optional explicit config path, and whether to run the
|
|
3314
|
+
* untranslated-literal scan instead of the setup checks.
|
|
1531
3315
|
* @param deps - Optional adapter registry, file-system, and config-loader overrides.
|
|
1532
3316
|
* @returns Every check with its verdict, and the project-wide `ok` verdict.
|
|
1533
3317
|
*
|
|
@@ -1548,9 +3332,11 @@ interface DoctorDeps {
|
|
|
1548
3332
|
declare function doctor(input?: DoctorInput, deps?: DoctorDeps): Promise<DoctorResult>;
|
|
1549
3333
|
|
|
1550
3334
|
/**
|
|
1551
|
-
*
|
|
1552
|
-
*
|
|
1553
|
-
*
|
|
3335
|
+
* Every reason a candidate translation can be refused before it is written. The same gate guards
|
|
3336
|
+
* every write path: a provider translation, a cache or duplicate-content reuse, a generated plural
|
|
3337
|
+
* form, a pseudolocalized value, a workbook or TMX import, {@link editEntry}, and
|
|
3338
|
+
* {@link retranslateEntry}, so a hand-typed value and a provider-produced one are held to identical
|
|
3339
|
+
* standards.
|
|
1554
3340
|
*
|
|
1555
3341
|
* A rejected value is never written to the locale file and never recorded in the lock-file, so the
|
|
1556
3342
|
* previous translation stays intact.
|
|
@@ -1559,6 +3345,48 @@ declare function doctor(input?: DoctorInput, deps?: DoctorDeps): Promise<DoctorR
|
|
|
1559
3345
|
* interpolation would break at runtime. For the double-brace formats (i18next, ngx-translate, and
|
|
1560
3346
|
* YAML) this also covers a single-brace `{name}`-shaped token the candidate invented and the
|
|
1561
3347
|
* source never had, which is a fabrication whichever interpolation delimiters the project uses.
|
|
3348
|
+
* - `markup`: the candidate does not carry the same inline HTML or XML tags as the source, or it
|
|
3349
|
+
* carries them unbalanced, mis-nested, or newly nested inside another tag of the same name, any
|
|
3350
|
+
* of which breaks the rendering of the string the way a dropped placeholder breaks its
|
|
3351
|
+
* interpolation, or it would make an HTML parser create markup the source does not have. Tags
|
|
3352
|
+
* are read the way an HTML parser reads them (quoted attribute values may contain `>`, and the
|
|
3353
|
+
* content of a raw text element such as `script`, `style`, `title` or `textarea` is text up to
|
|
3354
|
+
* its closing tag) and compared as a multiset of names plus attribute names, so a different word
|
|
3355
|
+
* order and a translated attribute value are both accepted, and the two spellings of a void
|
|
3356
|
+
* element (`<br>` and `<br/>`) are one tag. Tag names are compared exactly, while the HTML void
|
|
3357
|
+
* elements are recognised as needing no closing tag in any case spelling. An unclosed bracketed
|
|
3358
|
+
* word that is only a name of letters, digits, hyphens, and underscores, such as `<Enter>`, is
|
|
3359
|
+
* set aside on both sides and counted like a tag once the source carries tags; where the source
|
|
3360
|
+
* has no tags, the candidate is refused for a closing tag with no opening tag, an unclosed tag
|
|
3361
|
+
* that carries anything beyond its name, any closed pair, void, or self-closing tag, and a
|
|
3362
|
+
* bracketed word the source does not carry that names a standard HTML element, `image`, or a
|
|
3363
|
+
* custom element (any name with a hyphen). Comments, CDATA sections, declarations, processing
|
|
3364
|
+
* instructions, and a `</` not followed by a letter end where an HTML parser ends them and are
|
|
3365
|
+
* compared as a multiset of their text, so one the candidate adds, drops, or rewrites is
|
|
3366
|
+
* refused. A source whose own tags are not well formed is still compared as a multiset, and no
|
|
3367
|
+
* source is too large to compare; a candidate is refused once it carries more than twice its
|
|
3368
|
+
* source's tags and constructs, or more than 256 for a smaller source. Also refused: a tag the
|
|
3369
|
+
* candidate never finishes when the source finishes all of its own; a `javascript:`,
|
|
3370
|
+
* `vbscript:`, or `data:` scheme in a URL attribute value, read after decoding character
|
|
3371
|
+
* references, unless the source carries that exact value on the same tag and attribute; a URL
|
|
3372
|
+
* in such a value whose scheme and authority no source value of the same tag and attribute has,
|
|
3373
|
+
* or a relative URL made absolute; a changed value of `meta` `http-equiv` or `content`, `script`
|
|
3374
|
+
* `type`, `nomodule`, `integrity` or `crossorigin`, `link` `rel`, `as`, `integrity`,
|
|
3375
|
+
* `crossorigin` or `type`, `base` `target`, SVG animation `attributeName` or `attributeType`,
|
|
3376
|
+
* `iframe` `sandbox`, `allow` or `allowfullscreen`, `form` `method`, `enctype` or `target`, or
|
|
3377
|
+
* any `style`, `srcdoc` or event handler; any change to the content of a `script`, `style`,
|
|
3378
|
+
* `iframe`, `noembed`, `noframes`, `noscript`, `xmp`, or `plaintext` element (`title` and
|
|
3379
|
+
* `textarea` content may be translated); and, unless the candidate is the source unchanged,
|
|
3380
|
+
* raw text an HTML parser can read two ways (a `script` whose content opens `<!--`, a `noscript`
|
|
3381
|
+
* whose content holds a `<`, a raw text element whose content holds a `<` after an `svg`,
|
|
3382
|
+
* `math`, or `select` tag) or a CDATA section or `image` tag in a value that opens `svg`, `math`,
|
|
3383
|
+
* or `select`. The check stands down per tag: a tag the format already reports as a placeholder
|
|
3384
|
+
* (an XLIFF inline element, a next-intl or ARB ICU rich-text tag) is left to the `placeholder`
|
|
3385
|
+
* reason together with as many closing tags as it has openings, while one the candidate leaves
|
|
3386
|
+
* unclosed, one that no longer nests with the other tags as in the source, and one the source
|
|
3387
|
+
* carries only as text are still refused, and any other tag in the same value is still
|
|
3388
|
+
* compared, including a second spelling of the same name. The refusal's `details` names the
|
|
3389
|
+
* offending tags.
|
|
1562
3390
|
* - `icu`: the candidate is not a valid ICU message under the configured format's adapter.
|
|
1563
3391
|
* - `degenerate`: the candidate collapsed into runaway output rather than a translation. Two shapes
|
|
1564
3392
|
* are detected: the candidate is at least twelve times the length of a source of meaningful
|
|
@@ -1566,8 +3394,25 @@ declare function doctor(input?: DoctorInput, deps?: DoctorDeps): Promise<DoctorR
|
|
|
1566
3394
|
* echo of the source is not degenerate by this rule; it surfaces as the `EQUALS_SOURCE` review
|
|
1567
3395
|
* reason instead, which flags rather than refuses.
|
|
1568
3396
|
* - `empty`: the source has text but the candidate is blank, which would silently erase a string.
|
|
3397
|
+
*
|
|
3398
|
+
* This tuple is the single source of truth for the set. {@link IntegrityGateReason} is derived from
|
|
3399
|
+
* it, so build any runtime validator or exhaustive lookup from this value rather than retyping the
|
|
3400
|
+
* members; a hand-copied list silently falls behind the next addition.
|
|
3401
|
+
*
|
|
3402
|
+
* @example
|
|
3403
|
+
* ```ts
|
|
3404
|
+
* import { INTEGRITY_GATE_REASONS } from "@verbatra/sdk";
|
|
3405
|
+
* import { z } from "zod";
|
|
3406
|
+
*
|
|
3407
|
+
* const reasonSchema = z.enum(INTEGRITY_GATE_REASONS);
|
|
3408
|
+
* ```
|
|
1569
3409
|
*/
|
|
1570
|
-
|
|
3410
|
+
declare const INTEGRITY_GATE_REASONS: readonly ["placeholder", "markup", "icu", "degenerate", "empty"];
|
|
3411
|
+
/**
|
|
3412
|
+
* One of {@link INTEGRITY_GATE_REASONS}. The union is derived from that tuple rather than written
|
|
3413
|
+
* out again, so a reason can only be added in one place.
|
|
3414
|
+
*/
|
|
3415
|
+
type IntegrityGateReason = (typeof INTEGRITY_GATE_REASONS)[number];
|
|
1571
3416
|
|
|
1572
3417
|
/** Input for {@link editEntry}. */
|
|
1573
3418
|
interface EditEntryInput {
|
|
@@ -1604,6 +3449,14 @@ type EditEntryResult = {
|
|
|
1604
3449
|
readonly accepted: false;
|
|
1605
3450
|
/** Which integrity rule the value broke. */
|
|
1606
3451
|
readonly reason: IntegrityGateReason;
|
|
3452
|
+
/**
|
|
3453
|
+
* The specific tags behind a `markup` refusal, each prefixed with `-` for one the source had
|
|
3454
|
+
* and the candidate dropped or `+` for one the candidate invented. A candidate carrying more
|
|
3455
|
+
* than twice its source's inline tags and constructs is named as `+more than N inline tags`
|
|
3456
|
+
* instead, where N is at least 256. Absent when no single tag is at fault, such as markup
|
|
3457
|
+
* that came back mis-nested, and absent for every other reason.
|
|
3458
|
+
*/
|
|
3459
|
+
readonly details?: readonly string[];
|
|
1607
3460
|
/** The rejected value, echoed back so a UI can show what was refused. */
|
|
1608
3461
|
readonly value: string;
|
|
1609
3462
|
};
|
|
@@ -1612,8 +3465,8 @@ type EditEntryResult = {
|
|
|
1612
3465
|
* a human corrects one string and it is persisted without re-running the whole project.
|
|
1613
3466
|
*
|
|
1614
3467
|
* The edit is held to the same integrity gate as a provider translation, so a value that drops a
|
|
1615
|
-
* placeholder, breaks ICU syntax, runs away into a hugely
|
|
1616
|
-
* or blanks a non-empty string is refused. A refusal comes back as `accepted: false` rather than as
|
|
3468
|
+
* placeholder, breaks the source's inline markup, breaks ICU syntax, runs away into a hugely
|
|
3469
|
+
* oversized or repetition-dominated value, or blanks a non-empty string is refused. A refusal comes back as `accepted: false` rather than as
|
|
1617
3470
|
* a thrown error, and nothing is written.
|
|
1618
3471
|
*
|
|
1619
3472
|
* The locale's write lock is taken for the whole read-modify-write, so an edit cannot interleave
|
|
@@ -1621,7 +3474,8 @@ type EditEntryResult = {
|
|
|
1621
3474
|
* refused edit takes and releases the lock as well and can therefore fail with `LOCK_CONTENDED`
|
|
1622
3475
|
* even though it would have written nothing. An accepted edit then updates the lock-file baseline
|
|
1623
3476
|
* and feeds the translation memory, which means a later run treats the key as up to date and
|
|
1624
|
-
* reuses the edited text rather than paying the provider to translate it again.
|
|
3477
|
+
* reuses the edited text rather than paying the provider to translate it again. No review reason
|
|
3478
|
+
* is computed for a hand-edited value, so a configured `maxLength` budget is not checked here.
|
|
1625
3479
|
*
|
|
1626
3480
|
* Note that the target locale file surfaces the adapter's own error and code rather than a wrapped
|
|
1627
3481
|
* {@link SdkError}, on the write as well as on the read, because only the source read is wrapped.
|
|
@@ -1655,9 +3509,259 @@ type EditEntryResult = {
|
|
|
1655
3509
|
* is malformed or on the write because the entries cannot be represented in the configured format.
|
|
1656
3510
|
* Its own code is preserved rather than remapped onto an {@link SdkErrorCode}.
|
|
1657
3511
|
*/
|
|
1658
|
-
declare function editEntry(input: EditEntryInput, deps?: EditEntryDeps): Promise<EditEntryResult>;
|
|
3512
|
+
declare function editEntry(input: EditEntryInput, deps?: EditEntryDeps): Promise<EditEntryResult>;
|
|
3513
|
+
|
|
3514
|
+
/** One key the run added to the source catalog, and the call site it was found at. */
|
|
3515
|
+
interface AddedKey {
|
|
3516
|
+
/** The key as written at the call site. */
|
|
3517
|
+
readonly key: string;
|
|
3518
|
+
/** The value written into the catalog: the call site's default, or the empty string when it had none. */
|
|
3519
|
+
readonly value: string;
|
|
3520
|
+
/** The source file of the key's first call site, relative to the run's working directory. */
|
|
3521
|
+
readonly file: string;
|
|
3522
|
+
/** The one-based line of the key's first call site. */
|
|
3523
|
+
readonly line: number;
|
|
3524
|
+
}
|
|
3525
|
+
/**
|
|
3526
|
+
* The result of {@link extract}. Everything it reports is a key, a value, or a file and line: a
|
|
3527
|
+
* source file's contents never travel in it.
|
|
3528
|
+
*/
|
|
3529
|
+
interface ExtractResult {
|
|
3530
|
+
/** The source locale file the run targeted, relative to the run's working directory. */
|
|
3531
|
+
readonly sourcePath: string;
|
|
3532
|
+
/** How many source files were read and scanned. */
|
|
3533
|
+
readonly scannedFiles: number;
|
|
3534
|
+
/** Keys that were not in the catalog and were added, in the order they were found. */
|
|
3535
|
+
readonly added: readonly AddedKey[];
|
|
3536
|
+
/** How many of the keys found were already in the catalog and were left untouched. */
|
|
3537
|
+
readonly existingKeys: number;
|
|
3538
|
+
/** Added keys whose call site carried no default, so they were written with an empty value. */
|
|
3539
|
+
readonly withoutDefault: readonly string[];
|
|
3540
|
+
/** Call sites whose key argument was not a static string, reported rather than guessed at. */
|
|
3541
|
+
readonly dynamic: readonly SourceLocation[];
|
|
3542
|
+
/** Keys found twice with two different default values. Neither value is written. */
|
|
3543
|
+
readonly conflicts: readonly KeyConflict[];
|
|
3544
|
+
/** Files and directories the scan could not read, which never abort the run. */
|
|
3545
|
+
readonly diagnostics: readonly ScanDiagnostic[];
|
|
3546
|
+
/** Whether the source catalog was actually written. False for a dry run and for a run that added nothing. */
|
|
3547
|
+
readonly written: boolean;
|
|
3548
|
+
/** Whether this was a dry run. */
|
|
3549
|
+
readonly dryRun: boolean;
|
|
3550
|
+
}
|
|
3551
|
+
/** Input for {@link extract}. */
|
|
3552
|
+
interface ExtractInput {
|
|
3553
|
+
/** A resolved config whose `extract` block names the framework and the source roots. */
|
|
3554
|
+
readonly config: VerbatraConfig;
|
|
3555
|
+
/** Directory the roots and locale paths are resolved against. Defaults to the process working directory. */
|
|
3556
|
+
readonly cwd?: string;
|
|
3557
|
+
/** Report what would be added without writing anything. */
|
|
3558
|
+
readonly dryRun?: boolean;
|
|
3559
|
+
}
|
|
3560
|
+
/** Injectable dependencies for {@link extract}. Every field has a working default. */
|
|
3561
|
+
interface ExtractDeps {
|
|
3562
|
+
/** Format-adapter registry to resolve the configured format against. Defaults to the built-in registry. */
|
|
3563
|
+
readonly adapterRegistry?: AdapterRegistry;
|
|
3564
|
+
/**
|
|
3565
|
+
* File-system port. It backs both the source scan and the catalog write, so its `readDirectory`
|
|
3566
|
+
* member must be implemented. Defaults to the real file system.
|
|
3567
|
+
*/
|
|
3568
|
+
readonly fs?: SdkFs;
|
|
3569
|
+
/** Extractor factory. Defaults to the built-in table keyed by the configured framework. */
|
|
3570
|
+
readonly createExtractor?: (framework: SourceFramework) => SourceExtractor;
|
|
3571
|
+
}
|
|
3572
|
+
/**
|
|
3573
|
+
* Scans a project's own source for translation call sites and merges what it finds into the source
|
|
3574
|
+
* locale catalog. It is the one entry point that reads application code rather than a locale file,
|
|
3575
|
+
* which is what makes verbatra usable on a project that has no catalog yet.
|
|
3576
|
+
*
|
|
3577
|
+
* It spends nothing: no provider is constructed, no API key environment variable is read, and no
|
|
3578
|
+
* network request is made. A run with no key set succeeds.
|
|
3579
|
+
*
|
|
3580
|
+
* Only the source locale file is ever written, and only keys that are genuinely new are added: a
|
|
3581
|
+
* key already in the catalog keeps its value exactly as it stands, so an editorial fix is never
|
|
3582
|
+
* reverted by a stale default left at a call site. A key in the catalog that no call site mentions
|
|
3583
|
+
* is left alone. A run that finds nothing new writes nothing at all, so it leaves the file
|
|
3584
|
+
* byte-identical and its modification time untouched.
|
|
3585
|
+
*
|
|
3586
|
+
* Everything the scan cannot resolve is reported as data rather than thrown. A call site whose key
|
|
3587
|
+
* argument is not a static string is reported in `dynamic` and never written with a guessed key; a
|
|
3588
|
+
* key found twice with two different defaults is reported in `conflicts` and neither value is
|
|
3589
|
+
* written; a file or directory that cannot be read is reported in `diagnostics` and the scan
|
|
3590
|
+
* carries on. The result never carries a source file's contents, only keys, values, and locations.
|
|
3591
|
+
*
|
|
3592
|
+
* @param input - The config (whose `extract` block names the framework and roots), the working
|
|
3593
|
+
* directory, and whether this is a dry run.
|
|
3594
|
+
* @param deps - Optional adapter registry, file-system, and extractor-factory overrides.
|
|
3595
|
+
* @returns What was added, what was already there, and everything the scan could not resolve.
|
|
3596
|
+
*
|
|
3597
|
+
* @throws {@link SdkError} `EXTRACT_NOT_CONFIGURED`: the config carries no `extract` block.
|
|
3598
|
+
* @throws {@link SdkError} `EXTRACT_FS_UNSUPPORTED`: the supplied `deps.fs` implements no
|
|
3599
|
+
* `readDirectory`, so no source file can be discovered.
|
|
3600
|
+
* @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
|
|
3601
|
+
* @throws {@link SdkError} `SOURCE_INVALID`: a source catalog exists but could not be parsed.
|
|
3602
|
+
* @throws {@link SdkError} `SOURCE_UNWRITABLE`: the source catalog could not be written. The
|
|
3603
|
+
* `xliff` and `apple-xcstrings` formats reach this when no catalog exists yet: neither is created
|
|
3604
|
+
* from nothing, here or anywhere else in verbatra.
|
|
3605
|
+
*
|
|
3606
|
+
* @example
|
|
3607
|
+
* ```ts
|
|
3608
|
+
* import { extract, loadConfig } from "@verbatra/sdk";
|
|
3609
|
+
*
|
|
3610
|
+
* const config = await loadConfig();
|
|
3611
|
+
* const result = await extract({ config, dryRun: true });
|
|
3612
|
+
* console.log(`${result.added.length} new keys, ${result.dynamic.length} dynamic call sites`);
|
|
3613
|
+
* ```
|
|
3614
|
+
*/
|
|
3615
|
+
declare function extract(input: ExtractInput, deps?: ExtractDeps): Promise<ExtractResult>;
|
|
3616
|
+
|
|
3617
|
+
/**
|
|
3618
|
+
* Why verbatra declined to describe a message's arguments rather than guessing at them.
|
|
3619
|
+
*
|
|
3620
|
+
* - `invalid-message-syntax`: the message does not parse as ICU under a format that reads its
|
|
3621
|
+
* arguments that way, or the adapter reports it as invalid.
|
|
3622
|
+
* - `mixed-argument-styles`: the message combines named arguments with numbered or unnumbered
|
|
3623
|
+
* ones, or numbered with unnumbered ones, so no single call signature fits it.
|
|
3624
|
+
* - `argument-index-out-of-range`: a numbered argument addresses a zero-based position below 0 or
|
|
3625
|
+
* of 64 or more (such as `%0$s` or `{64}`).
|
|
3626
|
+
*/
|
|
3627
|
+
type UnresolvedArgumentReason = "invalid-message-syntax" | "mixed-argument-styles" | "argument-index-out-of-range";
|
|
3628
|
+
|
|
3629
|
+
/**
|
|
3630
|
+
* Where {@link generateTypes} writes its declaration when the caller names no path: a `.d.ts` at
|
|
3631
|
+
* the root of the working directory. The file is a checked-in artifact, not a local scratch file:
|
|
3632
|
+
* it belongs in version control so a consumer type-checks against it without running verbatra
|
|
3633
|
+
* first, and so `check` mode has a committed file to compare against.
|
|
3634
|
+
*/
|
|
3635
|
+
declare const DEFAULT_TYPES_PATH = "verbatra-types.d.ts";
|
|
3636
|
+
/** One key whose arguments verbatra declined to describe, and why. */
|
|
3637
|
+
interface UnresolvedMessage {
|
|
3638
|
+
/** The key, which is still declared, with arguments nothing is claimed about. */
|
|
3639
|
+
readonly key: string;
|
|
3640
|
+
/** Why the arguments could not be determined. */
|
|
3641
|
+
readonly reason: UnresolvedArgumentReason;
|
|
3642
|
+
}
|
|
3643
|
+
/** Input for {@link generateTypes}. */
|
|
3644
|
+
interface GenerateTypesInput {
|
|
3645
|
+
/** The resolved project config, normally from {@link loadConfig}. */
|
|
3646
|
+
readonly config: VerbatraConfig;
|
|
3647
|
+
/** Directory the `files.pattern` and the output path are resolved against. Defaults to the process working directory. */
|
|
3648
|
+
readonly cwd?: string;
|
|
3649
|
+
/**
|
|
3650
|
+
* Where to write the declaration, relative to `cwd`. Defaults to {@link DEFAULT_TYPES_PATH}.
|
|
3651
|
+
* Refused with `TYPES_OUTPUT_CONFLICT`, before anything is read or written, when it names no
|
|
3652
|
+
* file, is absolute, climbs out of `cwd`, does not end in `.ts`, `.mts` or `.cts`, or names a
|
|
3653
|
+
* configured locale file, the lock file, the translation-memory cache, a file verbatra searches
|
|
3654
|
+
* for its configuration, or the {@link GenerateTypesInput.configPath} file. Names are compared
|
|
3655
|
+
* case-insensitively. A generating run also refuses to replace an existing file there unless
|
|
3656
|
+
* that file begins with the header line verbatra writes, after any leading byte order mark and
|
|
3657
|
+
* blank lines, and refuses one too large to verify.
|
|
3658
|
+
*/
|
|
3659
|
+
readonly out?: string;
|
|
3660
|
+
/**
|
|
3661
|
+
* The configuration file `config` was loaded from, absolute or relative to `cwd`. It is refused
|
|
3662
|
+
* as the output path even when its name is not one verbatra searches for.
|
|
3663
|
+
*/
|
|
3664
|
+
readonly configPath?: string;
|
|
3665
|
+
/**
|
|
3666
|
+
* Compare instead of writing. The run reports whether the file on disk matches what a fresh
|
|
3667
|
+
* generation would produce and leaves every file untouched.
|
|
3668
|
+
*/
|
|
3669
|
+
readonly check?: boolean;
|
|
3670
|
+
}
|
|
3671
|
+
/** Injectable dependencies for {@link generateTypes}. Every field has a working default. */
|
|
3672
|
+
interface GenerateTypesDeps {
|
|
3673
|
+
/** Format-adapter registry to resolve the configured format. Defaults to the built-in registry. */
|
|
3674
|
+
readonly adapterRegistry?: AdapterRegistry;
|
|
3675
|
+
/** File-system port. Defaults to the real file system. */
|
|
3676
|
+
readonly fs?: SdkFs;
|
|
3677
|
+
}
|
|
3678
|
+
/** The result of {@link generateTypes}: what was declared, and whether the file on disk matched. */
|
|
3679
|
+
interface GenerateTypesResult {
|
|
3680
|
+
/** Absolute path of the declaration file. */
|
|
3681
|
+
readonly path: string;
|
|
3682
|
+
/** The source catalog the declaration was built from, relative to the working directory. */
|
|
3683
|
+
readonly sourcePath: string;
|
|
3684
|
+
/** How many keys the declaration carries. */
|
|
3685
|
+
readonly keys: number;
|
|
3686
|
+
/**
|
|
3687
|
+
* How many of those keys take at least one argument verbatra could determine. A key listed in
|
|
3688
|
+
* {@link GenerateTypesResult.unresolved} is not counted here.
|
|
3689
|
+
*/
|
|
3690
|
+
readonly withArguments: number;
|
|
3691
|
+
/** Keys that are declared but whose arguments could not be determined, and why. */
|
|
3692
|
+
readonly unresolved: readonly UnresolvedMessage[];
|
|
3693
|
+
/** Keys the adapter reported as excluded from translation, which are never declared. */
|
|
3694
|
+
readonly excluded: readonly string[];
|
|
3695
|
+
/** Keys the adapter marked as carrying plural forms. Each sibling is declared on its own. */
|
|
3696
|
+
readonly plural: readonly string[];
|
|
3697
|
+
/**
|
|
3698
|
+
* Whether the declaration file was written. False in `check` mode, and false when the file on
|
|
3699
|
+
* disk already matched, so an unchanged catalog rewrites nothing.
|
|
3700
|
+
*/
|
|
3701
|
+
readonly written: boolean;
|
|
3702
|
+
/** Whether the file on disk differed from the freshly generated declaration when the run started. */
|
|
3703
|
+
readonly stale: boolean;
|
|
3704
|
+
/** Whether this was a `check` run. */
|
|
3705
|
+
readonly check: boolean;
|
|
3706
|
+
}
|
|
3707
|
+
/**
|
|
3708
|
+
* Generates a TypeScript declaration for a project's source catalog: a union of every key it holds
|
|
3709
|
+
* and, per key, the arguments its message interpolates. A consumer that types its translation
|
|
3710
|
+
* function against it turns a misspelled key and a missing interpolation argument into compile
|
|
3711
|
+
* errors instead of runtime lookup failures.
|
|
3712
|
+
*
|
|
3713
|
+
* It reads one file (the source locale catalog) and writes one file (the declaration). It
|
|
3714
|
+
* constructs no provider, reads no API key and makes no network request, so it runs on a fresh
|
|
3715
|
+
* checkout before any key exists.
|
|
3716
|
+
*
|
|
3717
|
+
* The keys are exactly what the format adapter produced when reading the catalog, in document
|
|
3718
|
+
* order, so two runs over an unchanged catalog write byte-identical bytes. Arguments come from the
|
|
3719
|
+
* placeholder tokens the adapter extracted, except for the ICU message formats (`next-intl-json`
|
|
3720
|
+
* and `arb`): there each message is analysed with the same ICU parser the adapter uses, so an
|
|
3721
|
+
* argument that only some `select` or `plural` branches use is still declared, as optional. Keys
|
|
3722
|
+
* are emitted as quoted string literals, so a key carrying a dot, a reserved word, a leading digit,
|
|
3723
|
+
* a quote, or nothing at all is declared verbatim rather than dropped or re-split.
|
|
3724
|
+
*
|
|
3725
|
+
* What it will not claim is as important as what it will. A message whose placeholders name their
|
|
3726
|
+
* arguments gets an object shape; one whose placeholders are numbered or anonymous gets a readonly
|
|
3727
|
+
* tuple; one that takes nothing gets a shape that makes passing an argument a type error. A
|
|
3728
|
+
* message whose syntax the adapter reported as invalid, one that mixes named, numbered and
|
|
3729
|
+
* anonymous arguments, and one that numbers an argument past the 64th are declared with
|
|
3730
|
+
* {@link GenerateTypesResult.unresolved} recording why, rather than being silently declared as
|
|
3731
|
+
* taking nothing. Argument types come only from what the catalog actually records: `number` for a
|
|
3732
|
+
* numeric annotation, printf conversion or ICU `plural`, `string` for a text conversion or ICU
|
|
3733
|
+
* `select`, `Date | number` for a date or time argument, and a `string | number` alias everywhere
|
|
3734
|
+
* else, never a permissive `any`. A name used with several types is declared as the union of what
|
|
3735
|
+
* each use accepts.
|
|
3736
|
+
*
|
|
3737
|
+
* With `check` set, nothing is written: the run reports whether the committed file still matches
|
|
3738
|
+
* what a fresh generation would produce, which is the shape a CI gate wants.
|
|
3739
|
+
*
|
|
3740
|
+
* @param input - The config, the output path, and whether to check rather than write.
|
|
3741
|
+
* @param deps - Optional adapter registry and file-system overrides.
|
|
3742
|
+
* @returns What was declared, and whether the file on disk matched.
|
|
3743
|
+
*
|
|
3744
|
+
* @example
|
|
3745
|
+
* ```ts
|
|
3746
|
+
* const result = await generateTypes({ config });
|
|
3747
|
+
* console.log(`${result.keys} keys declared in ${result.path}`);
|
|
3748
|
+
* ```
|
|
3749
|
+
*
|
|
3750
|
+
* @throws {@link SdkError} `TYPES_OUTPUT_CONFLICT`: the output path is refused (see
|
|
3751
|
+
* {@link GenerateTypesInput.out} for the full set), or a generating run found a file there that
|
|
3752
|
+
* does not begin with the header verbatra writes.
|
|
3753
|
+
* @throws {@link SdkError} `TYPES_UNWRITABLE`: the declaration file could not be written.
|
|
3754
|
+
* @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
|
|
3755
|
+
* @throws {@link SdkError} `LOCALE_LAYOUT_INVALID`: the `files.pattern` and `files.localeStyle`
|
|
3756
|
+
* cannot be combined.
|
|
3757
|
+
* @throws {@link SdkError} `LOCALE_PATH_COLLISION`: two configured locales resolve to the same
|
|
3758
|
+
* path.
|
|
3759
|
+
* @throws {@link SdkError} `SOURCE_UNREADABLE`: the source locale file does not exist.
|
|
3760
|
+
* @throws {@link SdkError} `SOURCE_INVALID`: the source locale file could not be parsed.
|
|
3761
|
+
*/
|
|
3762
|
+
declare function generateTypes(input: GenerateTypesInput, deps?: GenerateTypesDeps): Promise<GenerateTypesResult>;
|
|
1659
3763
|
|
|
1660
|
-
/** One key's placeholder and
|
|
3764
|
+
/** One key's placeholder, ICU, and inline-markup verdict in a {@link LocaleKeyIntegrity} report. */
|
|
1661
3765
|
interface KeyIntegrityEntry {
|
|
1662
3766
|
/** The key this verdict describes. */
|
|
1663
3767
|
readonly key: string;
|
|
@@ -1671,6 +3775,20 @@ interface KeyIntegrityEntry {
|
|
|
1671
3775
|
readonly extra: readonly string[];
|
|
1672
3776
|
/** True when the translation parses as a valid ICU message under the configured format. */
|
|
1673
3777
|
readonly icuValid: boolean;
|
|
3778
|
+
/**
|
|
3779
|
+
* True when the translation carries the source's inline HTML or XML tags, well formed. Compared
|
|
3780
|
+
* exactly as the write-time gate compares them, so a value already on disk is judged by the same
|
|
3781
|
+
* rule as one about to be written.
|
|
3782
|
+
*/
|
|
3783
|
+
readonly markupMatches: boolean;
|
|
3784
|
+
/**
|
|
3785
|
+
* The tags behind a `markupMatches: false` verdict, each prefixed with `-` for one the source has
|
|
3786
|
+
* and the translation lacks or `+` for one the translation added, or `+more than N inline tags`
|
|
3787
|
+
* for a translation carrying more than twice its source's inline tags and constructs (N is at
|
|
3788
|
+
* least 256). Empty when the markup matches, and empty when no single tag is at fault because
|
|
3789
|
+
* the translation's markup is structurally broken.
|
|
3790
|
+
*/
|
|
3791
|
+
readonly markupDetails: readonly string[];
|
|
1674
3792
|
}
|
|
1675
3793
|
/** One locale's per-key integrity verdicts. */
|
|
1676
3794
|
interface LocaleKeyIntegrity {
|
|
@@ -1687,7 +3805,11 @@ interface KeyIntegrityInput {
|
|
|
1687
3805
|
readonly cwd?: string;
|
|
1688
3806
|
/** Restrict the report to these target locales. Defaults to every configured target locale. */
|
|
1689
3807
|
readonly locales?: readonly string[];
|
|
1690
|
-
/**
|
|
3808
|
+
/**
|
|
3809
|
+
* Restrict the report to these keys. Only keys the diff reports as changed are ever judged, so a
|
|
3810
|
+
* requested key that is missing, orphaned, or up to date is left out rather than reported.
|
|
3811
|
+
* Defaults to every changed key.
|
|
3812
|
+
*/
|
|
1691
3813
|
readonly keys?: readonly string[];
|
|
1692
3814
|
}
|
|
1693
3815
|
/** Injectable dependencies for {@link keyIntegrity}. Every field has a working default. */
|
|
@@ -1699,16 +3821,22 @@ interface KeyIntegrityDeps {
|
|
|
1699
3821
|
}
|
|
1700
3822
|
/**
|
|
1701
3823
|
* Reports, per changed key, whether the existing translation still carries the source's
|
|
1702
|
-
* placeholders and still parses as valid ICU. It writes nothing and calls no
|
|
3824
|
+
* placeholders and inline markup and still parses as valid ICU. It writes nothing and calls no
|
|
3825
|
+
* provider.
|
|
1703
3826
|
*
|
|
1704
3827
|
* The scope is deliberately the changed keys rather than every key: a key whose source text has not
|
|
1705
3828
|
* moved was already gated when it was written, so re-reporting it would bury the keys that a source
|
|
1706
3829
|
* edit may have just invalidated. Only keys present in both the source and the target are judged,
|
|
1707
3830
|
* since a missing translation has no placeholders to compare.
|
|
1708
3831
|
*
|
|
1709
|
-
* This is the read-only counterpart to the
|
|
1710
|
-
* {@link
|
|
1711
|
-
*
|
|
3832
|
+
* This is the read-only counterpart to the placeholder, ICU and inline-markup checks of the
|
|
3833
|
+
* write-time integrity gate that {@link translate}, {@link editEntry}, {@link retranslateEntry},
|
|
3834
|
+
* and the workbook and TMX imports enforce, and the data behind a
|
|
3835
|
+
* review dashboard's per-key integrity indicator. Reporting markup here is what makes drift that
|
|
3836
|
+
* predates the gate visible at all: a translation written before the check existed, edited outside
|
|
3837
|
+
* verbatra, or produced by a path that never crossed the gate is judged here by the same rule.
|
|
3838
|
+
* Emptiness and degeneracy are deliberately not reported: both describe a candidate that was
|
|
3839
|
+
* refused rather than a state a stored translation can usefully be in.
|
|
1712
3840
|
*
|
|
1713
3841
|
* Note that a malformed target locale file surfaces the adapter's own error and code rather than a
|
|
1714
3842
|
* wrapped {@link SdkError}, because only source reads are wrapped. Its message names the offending
|
|
@@ -2022,313 +4150,233 @@ interface LockStateDeps {
|
|
|
2022
4150
|
*/
|
|
2023
4151
|
declare function lockState(input: LockStateInput, deps?: LockStateDeps): Promise<LockStateResult>;
|
|
2024
4152
|
|
|
2025
|
-
/**
|
|
2026
|
-
|
|
2027
|
-
* `deps.createProvider` to {@link translate}, {@link watch}, and {@link retranslateEntry}, it is the
|
|
2028
|
-
* seam for injecting a stub in a test or a provider the SDK does not ship.
|
|
2029
|
-
*
|
|
2030
|
-
* The default implementation dispatches on the provider ID and reads the API key from the
|
|
2031
|
-
* environment. A factory that throws is reported as `PROVIDER_CONSTRUCTION_FAILED`.
|
|
2032
|
-
*/
|
|
2033
|
-
type CreateProvider = (config: ProviderConfig) => TranslationProvider;
|
|
2034
|
-
|
|
2035
|
-
/** Input for {@link retranslateEntry}. */
|
|
2036
|
-
interface RetranslateEntryInput {
|
|
4153
|
+
/** Input for {@link pseudolocalize}. */
|
|
4154
|
+
interface PseudolocalizeInput {
|
|
2037
4155
|
/** The resolved project config, normally from {@link loadConfig}. */
|
|
2038
4156
|
readonly config: VerbatraConfig;
|
|
2039
4157
|
/** Directory the `files.pattern` is resolved against. Defaults to the process working directory. */
|
|
2040
4158
|
readonly cwd?: string;
|
|
2041
|
-
/**
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
4159
|
+
/**
|
|
4160
|
+
* The pseudolocale's BCP-47 code. Defaults to `en-XA`. It must not be the source locale or any
|
|
4161
|
+
* configured target locale, compared case-insensitively, so a pseudolocale can never stand in for
|
|
4162
|
+
* a real translation.
|
|
4163
|
+
*/
|
|
4164
|
+
readonly locale?: string;
|
|
4165
|
+
/**
|
|
4166
|
+
* Directory the pseudolocale file is written under, relative to `cwd`. Defaults to
|
|
4167
|
+
* `.verbatra-local/pseudo`, which `verbatra init` already adds to `.gitignore`. The configured
|
|
4168
|
+
* `files.pattern` is expanded inside it, so the file keeps the layout the application expects.
|
|
4169
|
+
*
|
|
4170
|
+
* It must name a directory inside `cwd`: an absolute path, one that climbs out with `..`, and
|
|
4171
|
+
* `cwd` itself are all refused, as is a directory under which the expanded pattern would place
|
|
4172
|
+
* the pseudolocale file beside a configured locale file, so a generated pseudolocale never lands
|
|
4173
|
+
* outside the project or beside the real translations where nothing ignores it.
|
|
4174
|
+
*/
|
|
4175
|
+
readonly out?: string;
|
|
2045
4176
|
}
|
|
2046
|
-
/** Injectable dependencies for {@link
|
|
2047
|
-
interface
|
|
4177
|
+
/** Injectable dependencies for {@link pseudolocalize}. Every field has a working default. */
|
|
4178
|
+
interface PseudolocalizeDeps {
|
|
2048
4179
|
/** Format-adapter registry to resolve the configured format. Defaults to the built-in registry. */
|
|
2049
4180
|
readonly adapterRegistry?: AdapterRegistry;
|
|
2050
|
-
/** Provider factory. Defaults to constructing the provider named in the config. */
|
|
2051
|
-
readonly createProvider?: CreateProvider;
|
|
2052
4181
|
/** File-system port. Defaults to the real file system. */
|
|
2053
4182
|
readonly fs?: SdkFs;
|
|
2054
4183
|
}
|
|
2055
|
-
/**
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
*/
|
|
2060
|
-
|
|
2061
|
-
/**
|
|
2062
|
-
readonly
|
|
2063
|
-
/**
|
|
2064
|
-
readonly
|
|
4184
|
+
/** The result of {@link pseudolocalize}: what was generated and where it landed. */
|
|
4185
|
+
interface PseudolocalizeResult {
|
|
4186
|
+
/** The pseudolocale that was generated. */
|
|
4187
|
+
readonly locale: string;
|
|
4188
|
+
/** Absolute path of the file that was written, or would have been written on a changed run. */
|
|
4189
|
+
readonly path: string;
|
|
4190
|
+
/** How many source entries were processed. */
|
|
4191
|
+
readonly entries: number;
|
|
4192
|
+
/** How many entries carry a pseudolocalized value. */
|
|
4193
|
+
readonly transformed: number;
|
|
2065
4194
|
/**
|
|
2066
|
-
*
|
|
2067
|
-
*
|
|
2068
|
-
*
|
|
4195
|
+
* Keys whose source value was copied verbatim because a pseudolocalized value would not have
|
|
4196
|
+
* passed the same integrity gate a translation must pass. Never a failure: the entry is still
|
|
4197
|
+
* present and still renders.
|
|
2069
4198
|
*/
|
|
2070
|
-
readonly
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
/** Which integrity rule the provider's value broke. */
|
|
2075
|
-
readonly reason: IntegrityGateReason;
|
|
2076
|
-
/** The rejected value, echoed back so a UI can show what was refused. */
|
|
2077
|
-
readonly value: string;
|
|
2078
|
-
};
|
|
4199
|
+
readonly copied: readonly string[];
|
|
4200
|
+
/** False when the file on disk already matched, so nothing was rewritten. */
|
|
4201
|
+
readonly written: boolean;
|
|
4202
|
+
}
|
|
2079
4203
|
/**
|
|
2080
|
-
*
|
|
2081
|
-
*
|
|
2082
|
-
*
|
|
2083
|
-
*
|
|
2084
|
-
*
|
|
2085
|
-
*
|
|
2086
|
-
*
|
|
2087
|
-
*
|
|
2088
|
-
*
|
|
2089
|
-
*
|
|
2090
|
-
*
|
|
2091
|
-
*
|
|
2092
|
-
*
|
|
2093
|
-
*
|
|
2094
|
-
*
|
|
2095
|
-
*
|
|
2096
|
-
*
|
|
2097
|
-
*
|
|
2098
|
-
*
|
|
2099
|
-
*
|
|
2100
|
-
*
|
|
2101
|
-
*
|
|
2102
|
-
*
|
|
4204
|
+
* Generates a pseudolocale from the source strings alone: every value is accented, expanded by
|
|
4205
|
+
* roughly a third of its translatable length, and wrapped in `[` and `]` boundary markers, so a
|
|
4206
|
+
* truncated or concatenated string is obvious on screen and an untranslated hardcoded string stands
|
|
4207
|
+
* out for having escaped the transform.
|
|
4208
|
+
*
|
|
4209
|
+
* It constructs no provider, reads no API key and makes no network request, so it runs on a fresh
|
|
4210
|
+
* checkout before any key exists or any budget is approved.
|
|
4211
|
+
*
|
|
4212
|
+
* Placeholders survive untouched in every syntax the shipped adapters recognise, and every
|
|
4213
|
+
* generated value is held to the same integrity gate a provider's translation must pass. A value
|
|
4214
|
+
* that would not pass it is copied from the source verbatim and reported in
|
|
4215
|
+
* {@link PseudolocalizeResult.copied} rather than written in a broken form.
|
|
4216
|
+
*
|
|
4217
|
+
* The output is deliberately kept out of the project's real locale files: it is written under
|
|
4218
|
+
* `.verbatra-local/pseudo` by default, which `verbatra init` already adds to `.gitignore`, and the
|
|
4219
|
+
* pseudolocale is refused when it names a configured locale, when its file would land in the same
|
|
4220
|
+
* directory as a configured locale file, and when it is directed at the project root or outside the
|
|
4221
|
+
* working directory. Because it lives outside `files.pattern`, {@link translate} never spends on it and
|
|
4222
|
+
* {@link check} and {@link diff} never report it as drifted.
|
|
4223
|
+
*
|
|
4224
|
+
* The transform is deterministic, so a second run over unchanged source rewrites nothing and
|
|
4225
|
+
* reports {@link PseudolocalizeResult.written} as false. For `xliff` and `apple-xcstrings`, whose
|
|
4226
|
+
* writers only patch units already present in the destination, the output is re-copied from the
|
|
4227
|
+
* source on every write, so a key added to the source after the first run still reaches the
|
|
4228
|
+
* pseudolocale; a copied XLIFF has its target-language attribute rewritten to the pseudolocale, so
|
|
4229
|
+
* the file never misdescribes what it holds.
|
|
4230
|
+
*
|
|
4231
|
+
* @param input - The config, the pseudolocale code, and the output directory.
|
|
4232
|
+
* @param deps - Optional adapter registry and file-system overrides.
|
|
4233
|
+
* @returns Where the pseudolocale landed, how many entries it carries, and whether it changed.
|
|
2103
4234
|
*
|
|
2104
|
-
* @
|
|
2105
|
-
*
|
|
2106
|
-
*
|
|
4235
|
+
* @example
|
|
4236
|
+
* ```ts
|
|
4237
|
+
* const result = await pseudolocalize({ config });
|
|
4238
|
+
* console.log(`${result.transformed} of ${result.entries} entries in ${result.path}`);
|
|
4239
|
+
* ```
|
|
2107
4240
|
*
|
|
4241
|
+
* @throws {@link SdkError} `PSEUDO_OUTPUT_CONFLICT`: the pseudolocale names a configured locale, its
|
|
4242
|
+
* file would land in the same directory as a configured locale file, or the output directory is not
|
|
4243
|
+
* a relative path naming a directory inside `cwd`.
|
|
2108
4244
|
* @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
|
|
2109
|
-
* @throws {@link SdkError} `UNKNOWN_LOCALE`: the requested locale is not a configured target locale.
|
|
2110
4245
|
* @throws {@link SdkError} `LOCALE_LAYOUT_INVALID`: the `files.pattern` and `files.localeStyle`
|
|
2111
|
-
* cannot be combined, or the
|
|
4246
|
+
* cannot be combined, or the pseudolocale has no valid path spelling under that style.
|
|
2112
4247
|
* @throws {@link SdkError} `LOCALE_PATH_COLLISION`: two configured locales resolve to the same path.
|
|
2113
4248
|
* @throws {@link SdkError} `SOURCE_UNREADABLE`: the source locale file does not exist.
|
|
2114
|
-
* @throws {@link SdkError} `SOURCE_INVALID`: the source locale file could not be parsed
|
|
2115
|
-
*
|
|
2116
|
-
*
|
|
2117
|
-
*
|
|
2118
|
-
* @throws {@link SdkError} `LOCK_CONTENDED`: the locale's write lock could not be acquired before
|
|
2119
|
-
* the timeout elapsed.
|
|
2120
|
-
* @throws {@link SdkError} `TARGET_UNWRITABLE`: the target locale file could not be written because
|
|
2121
|
-
* of a file-system failure. The message names the target file and the file-system code, never the
|
|
2122
|
-
* internal temporary file.
|
|
2123
|
-
* @throws {@link SdkError} `LOCK_FILE_INVALID`: the lock-file is corrupt, oversized, or at an
|
|
2124
|
-
* unsupported version.
|
|
2125
|
-
* @throws `AdapterError`: the adapter itself refused the target locale file, on the read because it
|
|
2126
|
-
* is malformed or on the write because the entries cannot be represented in the configured format.
|
|
2127
|
-
* Its own code is preserved rather than remapped onto an {@link SdkErrorCode}.
|
|
2128
|
-
* @throws `ProviderError` `INVALID_RESPONSE`: the provider returned no value for the key. Provider
|
|
2129
|
-
* transport and rate-limit failures propagate as `ProviderError` too, since a single-key call has
|
|
2130
|
-
* no per-locale summary to record them on.
|
|
4249
|
+
* @throws {@link SdkError} `SOURCE_INVALID`: the source locale file could not be parsed, or, for a
|
|
4250
|
+
* format whose writer only patches an existing document (`xliff` and `apple-xcstrings`), the
|
|
4251
|
+
* source file could not be copied to seed the output.
|
|
4252
|
+
* @throws {@link SdkError} `TARGET_UNWRITABLE`: the pseudolocale file could not be written.
|
|
2131
4253
|
*/
|
|
2132
|
-
declare function
|
|
4254
|
+
declare function pseudolocalize(input: PseudolocalizeInput, deps?: PseudolocalizeDeps): Promise<PseudolocalizeResult>;
|
|
2133
4255
|
|
|
2134
4256
|
/**
|
|
2135
|
-
*
|
|
2136
|
-
*
|
|
2137
|
-
*
|
|
2138
|
-
*
|
|
2139
|
-
* - `PLURAL_CATEGORIES_INCOMPLETE`: plural generation could not produce every category the target
|
|
2140
|
-
* language requires, so the entry is written with the categories that were produced.
|
|
2141
|
-
* - `SUB_BATCH_FAILED`: one sub-batch of a locale failed while others succeeded. The locale is
|
|
2142
|
-
* reported as `partial` rather than failed.
|
|
2143
|
-
* - `BLANK_ROW_BASELINE_RETAINED`: an imported handoff row was blank, so the existing translation
|
|
2144
|
-
* and its lock-file baseline were kept rather than being erased.
|
|
2145
|
-
* - `BUDGET_TOKENS_EXCEEDED`: the configured token budget was passed. Under `warn` the run
|
|
2146
|
-
* continues; under `stop` the remaining keys are withheld.
|
|
2147
|
-
* - `CACHE_VERSION_UNRECOGNIZED`: the translation memory is at a version this release does not
|
|
2148
|
-
* understand, so it was ignored rather than trusted.
|
|
2149
|
-
*/
|
|
2150
|
-
type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED" | "BUDGET_TOKENS_EXCEEDED" | "CACHE_VERSION_UNRECOGNIZED";
|
|
2151
|
-
/**
|
|
2152
|
-
* Token usage as reported by the provider. Absent when the provider does not report usage, which is
|
|
2153
|
-
* the case for machine-translation APIs such as DeepL that do not bill in tokens.
|
|
2154
|
-
*/
|
|
2155
|
-
interface UsageSummary {
|
|
2156
|
-
/** Tokens consumed by the prompts sent to the provider. */
|
|
2157
|
-
readonly inputTokens: number;
|
|
2158
|
-
/** Tokens consumed by the provider's responses. */
|
|
2159
|
-
readonly outputTokens: number;
|
|
2160
|
-
}
|
|
2161
|
-
/**
|
|
2162
|
-
* What a run does when it passes its token budget: `warn` finishes the work and reports the
|
|
2163
|
-
* overrun, `stop` withholds the remaining keys.
|
|
2164
|
-
*/
|
|
2165
|
-
type BudgetBehavior = "warn" | "stop";
|
|
2166
|
-
/** The token budget in force for a run, and how much of it was actually consumed. */
|
|
2167
|
-
interface RunBudget {
|
|
2168
|
-
/** The configured ceiling, in tokens. */
|
|
2169
|
-
readonly maxTokens: number;
|
|
2170
|
-
/** Whether passing the ceiling warns or stops the run. */
|
|
2171
|
-
readonly behavior: BudgetBehavior;
|
|
2172
|
-
/**
|
|
2173
|
-
* Whether the configured provider reports usage at all. When false the budget cannot be enforced,
|
|
2174
|
-
* because there is nothing to count.
|
|
2175
|
-
*/
|
|
2176
|
-
readonly supported: boolean;
|
|
2177
|
-
/** Tokens consumed across the whole run so far. */
|
|
2178
|
-
readonly tokensUsed: number;
|
|
2179
|
-
/** True once `tokensUsed` passed `maxTokens`. */
|
|
2180
|
-
readonly exceeded: boolean;
|
|
2181
|
-
}
|
|
2182
|
-
/** A condition the SDK reported on a locale. See {@link SdkNoticeCode}. */
|
|
2183
|
-
interface SdkNotice {
|
|
2184
|
-
/** The stable notice code. Branch on this, not on the message. */
|
|
2185
|
-
readonly code: SdkNoticeCode;
|
|
2186
|
-
/** A human-readable description of the condition. */
|
|
2187
|
-
readonly message: string;
|
|
2188
|
-
}
|
|
2189
|
-
/**
|
|
2190
|
-
* Any notice attached to a locale: either one the provider raised, such as a downgraded formality
|
|
2191
|
-
* or an ignored glossary, or one the SDK raised. Discriminate on `code`.
|
|
2192
|
-
*/
|
|
2193
|
-
type LocaleNotice = ProviderNotice | SdkNotice;
|
|
2194
|
-
/**
|
|
2195
|
-
* A translated key the provider layer flagged as worth a human look. The translation was still
|
|
2196
|
-
* written; these are advisory quality signals, not rejections.
|
|
2197
|
-
*/
|
|
2198
|
-
interface NeedsReviewEntry {
|
|
2199
|
-
/** The key that was flagged. */
|
|
2200
|
-
readonly key: string;
|
|
2201
|
-
/** Why it was flagged. A key can carry more than one reason. */
|
|
2202
|
-
readonly reasons: readonly ReviewReasonCode[];
|
|
2203
|
-
}
|
|
2204
|
-
/** A row of an imported handoff that could not be read. Reported rather than aborting the import. */
|
|
2205
|
-
interface MalformedRowReport {
|
|
2206
|
-
/** The row's 1-based index within its sheet or file. */
|
|
2207
|
-
readonly row: number;
|
|
2208
|
-
/**
|
|
2209
|
-
* The 1-based physical line in the source file, for delimited formats where a quoted value can
|
|
2210
|
-
* span several lines. Absent for `.xlsx`, which has rows but no lines.
|
|
2211
|
-
*/
|
|
2212
|
-
readonly line?: number;
|
|
2213
|
-
/** The column whose value was missing or unusable. */
|
|
2214
|
-
readonly column: string;
|
|
2215
|
-
}
|
|
2216
|
-
/**
|
|
2217
|
-
* A key that appeared more than once in an imported handoff. The first occurrence wins and the rest
|
|
2218
|
-
* are reported here, so a translator who duplicated a row learns which value was actually used.
|
|
2219
|
-
*/
|
|
2220
|
-
interface DuplicateKeyReport {
|
|
2221
|
-
/** The key that appeared more than once. */
|
|
2222
|
-
readonly key: string;
|
|
2223
|
-
/** The 1-based row index of the ignored occurrence. */
|
|
2224
|
-
readonly row: number;
|
|
2225
|
-
/** The 1-based physical line of the ignored occurrence, for delimited formats. Absent for `.xlsx`. */
|
|
2226
|
-
readonly line?: number;
|
|
2227
|
-
}
|
|
2228
|
-
/**
|
|
2229
|
-
* Everything that happened for one locale during a run. The key lists are disjoint accounts of what
|
|
2230
|
-
* became of each key, so a caller can reconstruct the whole run without re-reading any file.
|
|
4257
|
+
* Builds a {@link TranslationProvider} from the config's `provider` block. Passed as
|
|
4258
|
+
* `deps.createProvider` to {@link translate}, {@link watch}, and {@link retranslateEntry}, it is the
|
|
4259
|
+
* seam for injecting a stub in a test or a provider the SDK does not ship.
|
|
2231
4260
|
*
|
|
2232
|
-
*
|
|
2233
|
-
*
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
/**
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
/**
|
|
2251
|
-
readonly
|
|
2252
|
-
/**
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
readonly invalidIcuSource: readonly string[];
|
|
2257
|
-
/** Keys served from the translation memory instead of the provider, and so not paid for. */
|
|
2258
|
-
readonly cacheHits: readonly string[];
|
|
2259
|
-
/**
|
|
2260
|
-
* Keys whose translation was refused by the integrity gate, for instance because it dropped a
|
|
2261
|
-
* placeholder. The previous translation, if any, is left untouched.
|
|
2262
|
-
*/
|
|
2263
|
-
readonly integrityMismatches: readonly string[];
|
|
2264
|
-
/** Keys the provider failed to translate, for instance because their sub-batch errored. */
|
|
2265
|
-
readonly providerFailures: readonly string[];
|
|
2266
|
-
/** Keys whose plural categories were generated for the target language rather than translated one by one. */
|
|
2267
|
-
readonly generated: readonly string[];
|
|
2268
|
-
/** Keys not translated because the token budget was exhausted under `stop` behavior. */
|
|
2269
|
-
readonly budgetWithheld: readonly string[];
|
|
2270
|
-
/** Token usage for this locale. Absent when the provider does not report usage. */
|
|
2271
|
-
readonly usage?: UsageSummary;
|
|
2272
|
-
/** Provider and SDK notices raised while running this locale. Always present, possibly empty. */
|
|
2273
|
-
readonly notices: readonly LocaleNotice[];
|
|
2274
|
-
/** Translated keys flagged as worth a human look. The translations were still written. */
|
|
2275
|
-
readonly needsReview: readonly NeedsReviewEntry[];
|
|
2276
|
-
/** Keys left with no translation after the run, whatever the cause. */
|
|
2277
|
-
readonly unfilled: readonly string[];
|
|
2278
|
-
/** Unreadable rows from an imported handoff. Always empty for a {@link translate} run. */
|
|
2279
|
-
readonly malformedRows: readonly MalformedRowReport[];
|
|
2280
|
-
/** Repeated keys from an imported handoff. Always empty for a {@link translate} run. */
|
|
2281
|
-
readonly duplicateKeys: readonly DuplicateKeyReport[];
|
|
2282
|
-
/**
|
|
2283
|
-
* Why this locale failed, when the failure came from a thrown error. The `code` is the failure's
|
|
2284
|
-
* own code where it has one, and `LOCALE_FAILED` otherwise. Never carries a secret.
|
|
2285
|
-
*
|
|
2286
|
-
* Absent unless `status` is `failed`, but a `failed` locale does not always carry it: a locale
|
|
2287
|
-
* whose every key was withheld by the integrity gate, a provider failure, or the token budget is
|
|
2288
|
-
* `failed` with nothing thrown, so this stays undefined and the withheld keys are the account of
|
|
2289
|
-
* what went wrong. Treat it as an optional detail on a failure, never as the failure test.
|
|
2290
|
-
*/
|
|
2291
|
-
readonly error?: {
|
|
2292
|
-
/** The failure's own code where it had one, and `LOCALE_FAILED` otherwise. */
|
|
2293
|
-
readonly code: string;
|
|
2294
|
-
/** A human-readable description of the failure. Never contains a secret. */
|
|
2295
|
-
readonly message: string;
|
|
2296
|
-
};
|
|
4261
|
+
* The default implementation dispatches on the provider ID and reads the API key from the
|
|
4262
|
+
* environment. A factory that throws is reported as `PROVIDER_CONSTRUCTION_FAILED`.
|
|
4263
|
+
*/
|
|
4264
|
+
type CreateProvider = (config: ProviderConfig) => TranslationProvider;
|
|
4265
|
+
|
|
4266
|
+
/** Input for {@link retranslateEntry}. */
|
|
4267
|
+
interface RetranslateEntryInput {
|
|
4268
|
+
/** The resolved project config, normally from {@link loadConfig}. */
|
|
4269
|
+
readonly config: VerbatraConfig;
|
|
4270
|
+
/** Directory the `files.pattern` is resolved against. Defaults to the process working directory. */
|
|
4271
|
+
readonly cwd?: string;
|
|
4272
|
+
/** The target locale to retranslate into. Must be a configured target locale. */
|
|
4273
|
+
readonly locale: string;
|
|
4274
|
+
/** The key to retranslate. Must exist in the source resource. */
|
|
4275
|
+
readonly key: string;
|
|
4276
|
+
}
|
|
4277
|
+
/** Injectable dependencies for {@link retranslateEntry}. Every field has a working default. */
|
|
4278
|
+
interface RetranslateEntryDeps {
|
|
4279
|
+
/** Format-adapter registry to resolve the configured format. Defaults to the built-in registry. */
|
|
4280
|
+
readonly adapterRegistry?: AdapterRegistry;
|
|
4281
|
+
/** Provider factory. Defaults to constructing the provider named in the config. */
|
|
4282
|
+
readonly createProvider?: CreateProvider;
|
|
4283
|
+
/** File-system port. Defaults to the real file system. */
|
|
4284
|
+
readonly fs?: SdkFs;
|
|
2297
4285
|
}
|
|
2298
4286
|
/**
|
|
2299
|
-
* The
|
|
2300
|
-
*
|
|
2301
|
-
*
|
|
2302
|
-
* Per-locale outcomes are data, not exceptions: inspect `failed` and `partial` to decide an exit
|
|
2303
|
-
* code rather than relying on the call to throw. Only whole-run failures, such as an invalid config
|
|
2304
|
-
* or an unreadable source file, throw an {@link SdkError}.
|
|
4287
|
+
* The outcome of {@link retranslateEntry}. As with {@link EditEntryResult}, a value refused by the
|
|
4288
|
+
* integrity gate is reported as data rather than thrown, because it is an expected outcome of
|
|
4289
|
+
* asking a provider for a fresh translation.
|
|
2305
4290
|
*/
|
|
2306
|
-
|
|
2307
|
-
/**
|
|
2308
|
-
readonly
|
|
4291
|
+
type RetranslateEntryResult = {
|
|
4292
|
+
/** The provider's value passed the integrity gate and was written. */
|
|
4293
|
+
readonly accepted: true;
|
|
4294
|
+
/** The newly translated value now stored for the key. */
|
|
4295
|
+
readonly value: string;
|
|
2309
4296
|
/**
|
|
2310
|
-
*
|
|
2311
|
-
*
|
|
2312
|
-
*
|
|
2313
|
-
*
|
|
2314
|
-
*
|
|
4297
|
+
* Quality signals the provider layer raised for this value, such as a length-ratio outlier,
|
|
4298
|
+
* a value identical to the source, or `MAX_LENGTH_EXCEEDED` for a value over the key's
|
|
4299
|
+
* configured `maxLength` budget. Never `FUZZY_CACHE_REUSE`, since this path always calls the
|
|
4300
|
+
* provider and never consults the translation memory. Empty when nothing was flagged. The
|
|
4301
|
+
* value is written either way; these are advisory.
|
|
2315
4302
|
*/
|
|
2316
|
-
readonly
|
|
2317
|
-
|
|
2318
|
-
|
|
4303
|
+
readonly reviewReasons: readonly ReviewReasonCode[];
|
|
4304
|
+
} | {
|
|
4305
|
+
/** The value was refused; nothing was written and the previous translation is intact. */
|
|
4306
|
+
readonly accepted: false;
|
|
4307
|
+
/** Which integrity rule the provider's value broke. */
|
|
4308
|
+
readonly reason: IntegrityGateReason;
|
|
2319
4309
|
/**
|
|
2320
|
-
*
|
|
2321
|
-
*
|
|
2322
|
-
*
|
|
4310
|
+
* The specific tags behind a `markup` refusal, each prefixed with `-` for one the source had
|
|
4311
|
+
* and the candidate dropped or `+` for one the candidate invented. A candidate carrying more
|
|
4312
|
+
* than twice its source's inline tags and constructs is named as `+more than N inline tags`
|
|
4313
|
+
* instead, where N is at least 256. Absent when no single tag is at fault, such as markup
|
|
4314
|
+
* that came back mis-nested, and absent for every other reason.
|
|
2323
4315
|
*/
|
|
2324
|
-
readonly
|
|
2325
|
-
/**
|
|
2326
|
-
readonly
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
4316
|
+
readonly details?: readonly string[];
|
|
4317
|
+
/** The rejected value, echoed back so a UI can show what was refused. */
|
|
4318
|
+
readonly value: string;
|
|
4319
|
+
};
|
|
4320
|
+
/**
|
|
4321
|
+
* Re-runs the configured provider for a single key and saves the result. This is the paid
|
|
4322
|
+
* counterpart to {@link editEntry}: it calls the provider and therefore spends tokens, which is why
|
|
4323
|
+
* a UI should gate it behind an explicit user action.
|
|
4324
|
+
*
|
|
4325
|
+
* The returned value goes through the same integrity gate as a full run, so a translation that
|
|
4326
|
+
* loses a placeholder, breaks the source's inline markup, breaks ICU syntax, degenerates into
|
|
4327
|
+
* runaway output, or comes back blank is refused and nothing is written. Provider quality
|
|
4328
|
+
* signals are surfaced on an accepted result as `reviewReasons` rather than blocking the write.
|
|
4329
|
+
*
|
|
4330
|
+
* The locale's write lock is taken before the provider is called and held across it, covering the
|
|
4331
|
+
* whole read-translate-write cycle, so a slow provider keeps the lock for the length of its call
|
|
4332
|
+
* and a concurrent {@link translate} run on that locale waits. The lock is held for a refused
|
|
4333
|
+
* translation too, since the gate runs inside it. An accepted value then updates the lock-file
|
|
4334
|
+
* baseline and feeds the translation memory, so a later {@link translate} run sees the key as up
|
|
4335
|
+
* to date.
|
|
4336
|
+
*
|
|
4337
|
+
* Note that the target locale file surfaces the adapter's own error and code rather than a wrapped
|
|
4338
|
+
* {@link SdkError}, on the write as well as on the read, because only the source read is wrapped.
|
|
4339
|
+
* A malformed target file fails the read with a message naming the offending locale and the
|
|
4340
|
+
* resolved path. The write raises the adapter's error too, when the entries cannot be represented
|
|
4341
|
+
* in the configured format or the existing destination file cannot be read back to be updated in
|
|
4342
|
+
* place, and that error is re-thrown unchanged so its own code survives for a caller that maps
|
|
4343
|
+
* adapter codes to its own copy. A caller that maps SDK codes should be ready for an unrecognized
|
|
4344
|
+
* error from a target file on either path.
|
|
4345
|
+
*
|
|
4346
|
+
* It spends outside the token budget. `maxTokens` and `budgetBehavior` bound a {@link translate}
|
|
4347
|
+
* run, and this is its own single-key path with its own provider, so a configured ceiling neither
|
|
4348
|
+
* withholds this call nor counts it. A caller that exposes retranslation to users, as the Studio
|
|
4349
|
+
* dashboard and the agent tools do, has to bound that spend itself.
|
|
4350
|
+
*
|
|
4351
|
+
* @param input - The config, locale, and key to retranslate.
|
|
4352
|
+
* @param deps - Optional adapter registry, provider factory, and file-system overrides.
|
|
4353
|
+
* @returns Whether the new value was accepted, with review reasons or the rejection reason.
|
|
4354
|
+
*
|
|
4355
|
+
* @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
|
|
4356
|
+
* @throws {@link SdkError} `UNKNOWN_LOCALE`: the requested locale is not a configured target locale.
|
|
4357
|
+
* @throws {@link SdkError} `LOCALE_LAYOUT_INVALID`: the `files.pattern` and `files.localeStyle`
|
|
4358
|
+
* cannot be combined, or the locale has no valid path spelling under that style.
|
|
4359
|
+
* @throws {@link SdkError} `LOCALE_PATH_COLLISION`: two configured locales resolve to the same path.
|
|
4360
|
+
* @throws {@link SdkError} `SOURCE_UNREADABLE`: the source locale file does not exist.
|
|
4361
|
+
* @throws {@link SdkError} `SOURCE_INVALID`: the source locale file could not be parsed.
|
|
4362
|
+
* @throws {@link SdkError} `UNKNOWN_KEY`: the key is not present in the source resource.
|
|
4363
|
+
* @throws {@link SdkError} `PROVIDER_CONSTRUCTION_FAILED`: the provider could not be constructed,
|
|
4364
|
+
* most often because its API key environment variable is unset.
|
|
4365
|
+
* @throws {@link SdkError} `LOCK_CONTENDED`: the locale's write lock could not be acquired before
|
|
4366
|
+
* the timeout elapsed.
|
|
4367
|
+
* @throws {@link SdkError} `TARGET_UNWRITABLE`: the target locale file could not be written because
|
|
4368
|
+
* of a file-system failure. The message names the target file and the file-system code, never the
|
|
4369
|
+
* internal temporary file.
|
|
4370
|
+
* @throws {@link SdkError} `LOCK_FILE_INVALID`: the lock-file is corrupt, oversized, or at an
|
|
4371
|
+
* unsupported version.
|
|
4372
|
+
* @throws `AdapterError`: the adapter itself refused the target locale file, on the read because it
|
|
4373
|
+
* is malformed or on the write because the entries cannot be represented in the configured format.
|
|
4374
|
+
* Its own code is preserved rather than remapped onto an {@link SdkErrorCode}.
|
|
4375
|
+
* @throws `ProviderError` `INVALID_RESPONSE`: the provider returned no value for the key. Provider
|
|
4376
|
+
* transport and rate-limit failures propagate as `ProviderError` too, since a single-key call has
|
|
4377
|
+
* no per-locale summary to record them on.
|
|
4378
|
+
*/
|
|
4379
|
+
declare function retranslateEntry(input: RetranslateEntryInput, deps?: RetranslateEntryDeps): Promise<RetranslateEntryResult>;
|
|
2332
4380
|
|
|
2333
4381
|
/**
|
|
2334
4382
|
* One locale's entry in a persisted {@link RunStatusFile}. It is a deliberately narrow projection
|
|
@@ -2342,6 +4390,14 @@ interface RunStatusLocale {
|
|
|
2342
4390
|
readonly status: "succeeded" | "partial" | "failed";
|
|
2343
4391
|
/** Keys the run flagged as worth a human look. */
|
|
2344
4392
|
readonly needsReview: readonly NeedsReviewEntry[];
|
|
4393
|
+
/**
|
|
4394
|
+
* Translations reused for a source string that had changed, with the score and the earlier
|
|
4395
|
+
* source behind each one. Every key listed here also carries `FUZZY_CACHE_REUSE` in
|
|
4396
|
+
* {@link needsReview}; this is the evidence a reviewer needs to judge the reuse without
|
|
4397
|
+
* re-running. Absent for a locale that reused nothing, and for a file written before the field
|
|
4398
|
+
* existed.
|
|
4399
|
+
*/
|
|
4400
|
+
readonly fuzzyHits?: readonly FuzzyCacheHit[];
|
|
2345
4401
|
/** Token usage for this locale. Absent when the provider does not report usage. */
|
|
2346
4402
|
readonly usage?: UsageSummary;
|
|
2347
4403
|
}
|
|
@@ -2360,7 +4416,11 @@ interface RunStatusFile {
|
|
|
2360
4416
|
readonly generatedAt: string;
|
|
2361
4417
|
/** Token usage summed across every locale. Absent when the provider does not report usage. */
|
|
2362
4418
|
readonly usage?: UsageSummary;
|
|
2363
|
-
/**
|
|
4419
|
+
/**
|
|
4420
|
+
* The token budget in force during the run, present only when one was configured. A snapshot
|
|
4421
|
+
* written before the budget was enforced, when `supported: false` meant nothing was counted at
|
|
4422
|
+
* all, has such a budget left out rather than read as a count of zero.
|
|
4423
|
+
*/
|
|
2364
4424
|
readonly budget?: RunBudget;
|
|
2365
4425
|
/** Per-locale outcomes from the recorded run. */
|
|
2366
4426
|
readonly locales: readonly RunStatusLocale[];
|
|
@@ -2407,6 +4467,319 @@ type RunStatusResult = {
|
|
|
2407
4467
|
*/
|
|
2408
4468
|
declare function runStatus(input?: RunStatusInput, deps?: RunStatusDeps): Promise<RunStatusResult>;
|
|
2409
4469
|
|
|
4470
|
+
/** Default output path for a TMX export, used when {@link ExportTmxInput.out} is omitted. */
|
|
4471
|
+
declare const DEFAULT_TMX_PATH = "verbatra-memory.tmx";
|
|
4472
|
+
/** How many translation units one locale contributed to the exported file. */
|
|
4473
|
+
interface ExportTmxLocaleCount {
|
|
4474
|
+
/** The configured target locale. */
|
|
4475
|
+
readonly locale: string;
|
|
4476
|
+
/** How many of its memory entries were written. */
|
|
4477
|
+
readonly units: number;
|
|
4478
|
+
}
|
|
4479
|
+
/** What {@link exportTmx} wrote. */
|
|
4480
|
+
interface ExportTmxResult {
|
|
4481
|
+
/** The resolved path the file was written to. */
|
|
4482
|
+
readonly path: string;
|
|
4483
|
+
/** How many `tu` elements the file holds, one per distinct source string. */
|
|
4484
|
+
readonly units: number;
|
|
4485
|
+
/** Per-locale contribution to that total. */
|
|
4486
|
+
readonly locales: readonly ExportTmxLocaleCount[];
|
|
4487
|
+
/**
|
|
4488
|
+
* Translations left out because the memory holds no source text for their content hash, counted
|
|
4489
|
+
* once per locale and hash rather than once per distinct source string. A cache carried forward
|
|
4490
|
+
* from an older schema stores translations without their source, and TMX cannot represent a unit
|
|
4491
|
+
* with no source segment. Those entries fill in as later runs touch the same strings.
|
|
4492
|
+
*/
|
|
4493
|
+
readonly withoutSource: number;
|
|
4494
|
+
/**
|
|
4495
|
+
* Characters removed from segment text because XML 1.0 cannot represent them at all, such as a
|
|
4496
|
+
* control character, a lone surrogate or a noncharacter, counted across every written segment.
|
|
4497
|
+
* Writing them would produce a file no conformant parser accepts, so they are left out and counted
|
|
4498
|
+
* rather than dropped silently.
|
|
4499
|
+
*/
|
|
4500
|
+
readonly illegalCharactersRemoved: number;
|
|
4501
|
+
}
|
|
4502
|
+
/** Input for {@link exportTmx}. */
|
|
4503
|
+
interface ExportTmxInput {
|
|
4504
|
+
/** The resolved project config, normally from {@link loadConfig}. */
|
|
4505
|
+
readonly config: VerbatraConfig;
|
|
4506
|
+
/** Where to write the file. Defaults to {@link DEFAULT_TMX_PATH}, resolved against `cwd`. */
|
|
4507
|
+
readonly out?: string;
|
|
4508
|
+
/** Directory the output path and the memory are resolved against. Defaults to the process working directory. */
|
|
4509
|
+
readonly cwd?: string;
|
|
4510
|
+
/** Subset of configured target locales to export. Defaults to all of them. */
|
|
4511
|
+
readonly locales?: readonly string[];
|
|
4512
|
+
/** Value for the TMX header's `creationtoolversion`. Defaults to `unknown`. */
|
|
4513
|
+
readonly toolVersion?: string;
|
|
4514
|
+
}
|
|
4515
|
+
/** Injectable dependencies for {@link exportTmx}. Every field has a working default. */
|
|
4516
|
+
interface ExportTmxDeps {
|
|
4517
|
+
/** File-system port. Defaults to the real file system. */
|
|
4518
|
+
readonly fs?: SdkFs;
|
|
4519
|
+
}
|
|
4520
|
+
/**
|
|
4521
|
+
* Writes the project's translation memory out as a TMX 1.4b file, the format every mainstream
|
|
4522
|
+
* translation platform can read. It calls no provider and needs no API key: every value comes from
|
|
4523
|
+
* the memory already on disk.
|
|
4524
|
+
*
|
|
4525
|
+
* Only entries stored under the project's current configuration fingerprint are written, which is
|
|
4526
|
+
* the same set a run would reuse, so the file describes the memory as it is actually being used
|
|
4527
|
+
* rather than every translation the project has ever produced. One `tu` element is written per
|
|
4528
|
+
* distinct source string, carrying the source segment and one target segment per exported locale.
|
|
4529
|
+
* An empty memory, including a missing, unparsable or newer-version cache file, produces a valid,
|
|
4530
|
+
* empty TMX file rather than an error. An existing file at the output path is replaced.
|
|
4531
|
+
*
|
|
4532
|
+
* It reads the memory and writes a file; it never changes the memory, which is why the CLI refuses
|
|
4533
|
+
* `--dry-run` and `--overwrite` on this direction rather than accepting and ignoring them.
|
|
4534
|
+
*
|
|
4535
|
+
* @param input - The config, the output path, the locale subset, and the tool version to stamp.
|
|
4536
|
+
* @param deps - Optional file-system override.
|
|
4537
|
+
* @returns The path written, the unit count, and what was left out.
|
|
4538
|
+
*
|
|
4539
|
+
* @throws {@link SdkError} `UNKNOWN_LOCALE`: a requested locale is not a configured target locale.
|
|
4540
|
+
* @throws {@link SdkError} `CONFIG_INVALID`: the source locale and one of the target locales are the
|
|
4541
|
+
* same language tag once case and separators are normalized. Writing that file would produce two
|
|
4542
|
+
* language attributes {@link importTmx} could not tell apart, so the file this project exported
|
|
4543
|
+
* would be one it refuses to read back.
|
|
4544
|
+
* @throws The underlying file-system error, unwrapped, when the output file could not be written.
|
|
4545
|
+
*
|
|
4546
|
+
* @example
|
|
4547
|
+
* ```ts
|
|
4548
|
+
* const result = await exportTmx({ config, out: "memory.tmx" });
|
|
4549
|
+
* console.log(`${result.units} units written to ${result.path}`);
|
|
4550
|
+
* ```
|
|
4551
|
+
*/
|
|
4552
|
+
declare function exportTmx(input: ExportTmxInput, deps?: ExportTmxDeps): Promise<ExportTmxResult>;
|
|
4553
|
+
|
|
4554
|
+
/**
|
|
4555
|
+
* The two delimiter-separated interchange formats. They are one implementation with one parameter, the
|
|
4556
|
+
* field delimiter: `tsv` is not a second format, it is `csv` with a tab. The member is also the file
|
|
4557
|
+
* extension an export writes and an import reads back.
|
|
4558
|
+
*/
|
|
4559
|
+
type DelimitedFormat = "csv" | "tsv";
|
|
4560
|
+
/** Where in an interchange file a refusal happened. */
|
|
4561
|
+
interface ExchangeErrorLocation {
|
|
4562
|
+
/** The 1-based line. */
|
|
4563
|
+
readonly line: number;
|
|
4564
|
+
/** The 1-based column on that line. */
|
|
4565
|
+
readonly column: number;
|
|
4566
|
+
/** The 1-based ordinal of the translation unit the problem sits in, when it sits in one. */
|
|
4567
|
+
readonly unit?: number;
|
|
4568
|
+
}
|
|
4569
|
+
|
|
4570
|
+
/**
|
|
4571
|
+
* Why one translation unit in an imported TMX file was refused. The first five are the shared
|
|
4572
|
+
* integrity gate's own reasons, so an imported unit is held to exactly the standard a provider's
|
|
4573
|
+
* output and a filled translator handoff already face. `sourceBlank` is the one reason particular
|
|
4574
|
+
* to interchange: a unit whose source segment is blank identifies no string, so it could never be
|
|
4575
|
+
* matched and is never stored.
|
|
4576
|
+
*/
|
|
4577
|
+
type TmxRejectionReason = IntegrityGateReason | "sourceBlank";
|
|
4578
|
+
/**
|
|
4579
|
+
* Where in a TMX file a `SOURCE_INVALID` refusal from {@link importTmx} happened: the 1-based `line`
|
|
4580
|
+
* and `column`, and the 1-based `unit` ordinal when the problem sits inside a translation unit. Read
|
|
4581
|
+
* it from a caught error with {@link tmxErrorLocation}; the same place is named in the message.
|
|
4582
|
+
*/
|
|
4583
|
+
type TmxErrorLocation = ExchangeErrorLocation;
|
|
4584
|
+
/**
|
|
4585
|
+
* Reads where in a TMX file a refusal from {@link importTmx} happened, without casting the error or
|
|
4586
|
+
* its `cause`.
|
|
4587
|
+
*
|
|
4588
|
+
* @param error - Whatever was caught from `importTmx`.
|
|
4589
|
+
* @returns The {@link TmxErrorLocation} when `error` is an {@link SdkError} wrapping a TMX parse
|
|
4590
|
+
* failure that has a place in the file; `undefined` for any other error, including a refusal with
|
|
4591
|
+
* no place in the file such as a missing or oversized file.
|
|
4592
|
+
*
|
|
4593
|
+
* @example
|
|
4594
|
+
* ```ts
|
|
4595
|
+
* try {
|
|
4596
|
+
* await importTmx({ config, file: "legacy.tmx" });
|
|
4597
|
+
* } catch (error) {
|
|
4598
|
+
* const at = tmxErrorLocation(error);
|
|
4599
|
+
* if (at !== undefined) console.error(`line ${at.line}, column ${at.column}`);
|
|
4600
|
+
* }
|
|
4601
|
+
* ```
|
|
4602
|
+
*/
|
|
4603
|
+
declare function tmxErrorLocation(error: unknown): TmxErrorLocation | undefined;
|
|
4604
|
+
/** How many units were refused, by reason. See {@link TmxRejectionReason}. */
|
|
4605
|
+
type TmxRejectionCounts = Readonly<Record<TmxRejectionReason, number>>;
|
|
4606
|
+
/** What an import did to the memory for one target locale. */
|
|
4607
|
+
interface ImportTmxLocaleResult {
|
|
4608
|
+
/** The configured target locale, spelled as the config spells it. */
|
|
4609
|
+
readonly locale: string;
|
|
4610
|
+
/** Units stored for the first time. */
|
|
4611
|
+
readonly added: number;
|
|
4612
|
+
/** Units the memory already held with the same translation, so nothing changed. */
|
|
4613
|
+
readonly unchanged: number;
|
|
4614
|
+
/** Units that replaced a different existing translation, which needs `overwrite`. */
|
|
4615
|
+
readonly overwritten: number;
|
|
4616
|
+
/** Units refused because the memory already held a different translation and `overwrite` was off. */
|
|
4617
|
+
readonly kept: number;
|
|
4618
|
+
/** Later units in the same file repeating a source the file had already translated. */
|
|
4619
|
+
readonly duplicates: number;
|
|
4620
|
+
/**
|
|
4621
|
+
* Units carrying two segments of equal standing for this locale with different values, such as
|
|
4622
|
+
* `de-CH` and `de-AT` in a project configured for `de`. Which one is right cannot be known, so
|
|
4623
|
+
* neither is stored. An exact tag always outranks one that only reaches the locale by subtag
|
|
4624
|
+
* prefix, and two segments carrying the same value are not a conflict.
|
|
4625
|
+
*/
|
|
4626
|
+
readonly conflicting: number;
|
|
4627
|
+
/** Units refused before they could be stored, by reason. */
|
|
4628
|
+
readonly rejected: TmxRejectionCounts;
|
|
4629
|
+
}
|
|
4630
|
+
/** A language tag in the file that no configured locale could be resolved to. */
|
|
4631
|
+
interface TmxLanguageReport {
|
|
4632
|
+
/** The tag exactly as the file spelled it. */
|
|
4633
|
+
readonly language: string;
|
|
4634
|
+
/** How many units carried a segment in it. */
|
|
4635
|
+
readonly units: number;
|
|
4636
|
+
}
|
|
4637
|
+
/** What {@link importTmx} read and what it wrote. */
|
|
4638
|
+
interface ImportTmxResult {
|
|
4639
|
+
/** Whether the run was a dry run, in which case the memory file was not written. */
|
|
4640
|
+
readonly dryRun: boolean;
|
|
4641
|
+
/** The resolved path the file was read from. */
|
|
4642
|
+
readonly file: string;
|
|
4643
|
+
/** The `srclang` the file's header declared, or undefined if it declared none. */
|
|
4644
|
+
readonly sourceLanguage: string | undefined;
|
|
4645
|
+
/** How many translation units the file held. */
|
|
4646
|
+
readonly units: number;
|
|
4647
|
+
/** Per-target-locale account of what was stored. */
|
|
4648
|
+
readonly locales: readonly ImportTmxLocaleResult[];
|
|
4649
|
+
/** Units the reader could not use at all, such as a unit with no segment. */
|
|
4650
|
+
readonly skippedUnits: number;
|
|
4651
|
+
/** `tu` elements outside the file's first `body`, which the reader never walked. */
|
|
4652
|
+
readonly unreachableUnits: number;
|
|
4653
|
+
/**
|
|
4654
|
+
* The header's `srclang` when it does not resolve to the configured source locale, so a file
|
|
4655
|
+
* exported from a project with a different source language is visible rather than silently
|
|
4656
|
+
* yielding nothing. Undefined when it matches, when the header declares none, or when it is the
|
|
4657
|
+
* TMX `*all*` wildcard, which declares that the source varies per unit.
|
|
4658
|
+
*/
|
|
4659
|
+
readonly sourceLanguageMismatch: string | undefined;
|
|
4660
|
+
/** Units carrying no segment in the configured source locale, so nothing could be keyed. */
|
|
4661
|
+
readonly unmatchedSourceUnits: number;
|
|
4662
|
+
/**
|
|
4663
|
+
* Units carrying two segments of equal standing that resolve to the configured source locale with
|
|
4664
|
+
* different values, such as an `en-US` "Color" and an `en-GB` "Colour" segment in a project
|
|
4665
|
+
* configured for `en`. An exact tag outranks a prefix match and identical values agree, so neither
|
|
4666
|
+
* counts here. Which value the translations belong to cannot be known, so the unit is refused
|
|
4667
|
+
* rather than attributed to whichever came last.
|
|
4668
|
+
*/
|
|
4669
|
+
readonly conflictingSourceUnits: number;
|
|
4670
|
+
/** Units whose segments carried inline markup, which is flattened to its text. */
|
|
4671
|
+
readonly markupStrippedUnits: number;
|
|
4672
|
+
/**
|
|
4673
|
+
* Units whose inline markup carried a `sub` element, a sub-flow such as a tooltip or alternative
|
|
4674
|
+
* text embedded in the markup. Its text is not part of the segment's own string, so it is left out
|
|
4675
|
+
* of the segment rather than flattened into it.
|
|
4676
|
+
*/
|
|
4677
|
+
readonly subflowDroppedUnits: number;
|
|
4678
|
+
/** Language tags that resolved to no configured locale, with how many units carried each. */
|
|
4679
|
+
readonly unmatchedLanguages: readonly TmxLanguageReport[];
|
|
4680
|
+
/** Language tags that two or more configured locales could claim, so none was chosen. */
|
|
4681
|
+
readonly ambiguousLanguages: readonly TmxLanguageReport[];
|
|
4682
|
+
/**
|
|
4683
|
+
* Segments that resolved cleanly to a configured target locale this run left out, because
|
|
4684
|
+
* {@link ImportTmxInput.locales} narrowed it. Nothing is wrong with them; they are reported so a
|
|
4685
|
+
* narrowed run does not look like a file that held less than it did.
|
|
4686
|
+
*/
|
|
4687
|
+
readonly notImported: readonly TmxLanguageReport[];
|
|
4688
|
+
/**
|
|
4689
|
+
* Whether the memory file could be written. False when the project's cache was written by a newer
|
|
4690
|
+
* build, which is left untouched rather than downgraded.
|
|
4691
|
+
*/
|
|
4692
|
+
readonly memoryWritable: boolean;
|
|
4693
|
+
}
|
|
4694
|
+
/** Input for {@link importTmx}. */
|
|
4695
|
+
interface ImportTmxInput {
|
|
4696
|
+
/** The resolved project config, normally from {@link loadConfig}. */
|
|
4697
|
+
readonly config: VerbatraConfig;
|
|
4698
|
+
/** Path to the TMX file, resolved against `cwd`. */
|
|
4699
|
+
readonly file: string;
|
|
4700
|
+
/** Directory the file and the memory are resolved against. Defaults to the process working directory. */
|
|
4701
|
+
readonly cwd?: string;
|
|
4702
|
+
/** Read and validate the file but write nothing. Defaults to false. */
|
|
4703
|
+
readonly dryRun?: boolean;
|
|
4704
|
+
/**
|
|
4705
|
+
* Let an imported unit replace a translation the memory already holds for the same source and
|
|
4706
|
+
* locale. Off by default, so the project's own memory wins a collision.
|
|
4707
|
+
*/
|
|
4708
|
+
readonly overwrite?: boolean;
|
|
4709
|
+
/** Subset of configured target locales to import. Defaults to all of them. */
|
|
4710
|
+
readonly locales?: readonly string[];
|
|
4711
|
+
}
|
|
4712
|
+
/** Injectable dependencies for {@link importTmx}. Every field has a working default. */
|
|
4713
|
+
interface ImportTmxDeps {
|
|
4714
|
+
/** Format-adapter registry, used for placeholder extraction and ICU validation. Defaults to the built-in registry. */
|
|
4715
|
+
readonly adapterRegistry?: AdapterRegistry;
|
|
4716
|
+
/** File-system port. Defaults to the real file system. */
|
|
4717
|
+
readonly fs?: SdkFs;
|
|
4718
|
+
}
|
|
4719
|
+
/**
|
|
4720
|
+
* Reads a TMX translation memory into the project's own memory, so a team arriving with years of
|
|
4721
|
+
* accumulated translations does not start cold. It calls no provider and needs no API key: every
|
|
4722
|
+
* value comes from the file.
|
|
4723
|
+
*
|
|
4724
|
+
* Nothing from the file is trusted. The XML is parsed under the interchange package's hardening
|
|
4725
|
+
* (entity declarations refused, size and unit bounds enforced), a language tag is resolved to a
|
|
4726
|
+
* configured locale by an explicit rule rather than guessed at, and every candidate translation
|
|
4727
|
+
* must pass the same placeholder, inline markup, ICU, degeneracy and emptiness gate that a
|
|
4728
|
+
* provider's output and a filled translator handoff already face before it is stored. A unit that
|
|
4729
|
+
* fails is counted, never written, so it can never later be served as a cache hit that was never
|
|
4730
|
+
* validated.
|
|
4731
|
+
*
|
|
4732
|
+
* Units are stored under the project's current configuration fingerprint, the same key a real run
|
|
4733
|
+
* writes. That is deliberate: an imported memory is reused exactly when the configuration that
|
|
4734
|
+
* would consume it matches, and stops matching when the provider, model, tone or glossary changes,
|
|
4735
|
+
* which is the protection the fingerprint layer exists to give. Re-import after such a change.
|
|
4736
|
+
*
|
|
4737
|
+
* A collision is decided in favour of what the project already has: if the memory already holds a
|
|
4738
|
+
* different translation for the same source and locale, the imported one is refused and counted as
|
|
4739
|
+
* `kept` unless {@link ImportTmxInput.overwrite} is set. Importing the same file twice therefore
|
|
4740
|
+
* changes nothing the second time, and writes no file at all.
|
|
4741
|
+
*
|
|
4742
|
+
* One consequence is worth stating plainly, because no import-time check can police it. An accepted
|
|
4743
|
+
* unit's source text is written into the memory's source index, which is the same index fuzzy reuse
|
|
4744
|
+
* scores a changed string against. An imported source that DIFFERS from a string in the project can
|
|
4745
|
+
* therefore be served for that string by fuzzy reuse, which is what resemblance means.
|
|
4746
|
+
*
|
|
4747
|
+
* An imported source that is identical to a project string but hashes differently is not reachable
|
|
4748
|
+
* at all. Fuzzy reuse discards any candidate whose normalized source equals the query, so a project
|
|
4749
|
+
* entry that carries a description, a meaning, or a plural flag the unit cannot carry falls through
|
|
4750
|
+
* to the provider rather than being served: the hashes differ, and the identical text disqualifies
|
|
4751
|
+
* the fuzzy candidate.
|
|
4752
|
+
*
|
|
4753
|
+
* A fuzzy reuse is still held to the integrity gate against the real entry and is reported as a
|
|
4754
|
+
* `FUZZY_CACHE_REUSE` review flag on the run summary, so it is visible rather than silent. A project
|
|
4755
|
+
* that does not want an imported memory reachable that way should leave `fuzzyCache` out of its
|
|
4756
|
+
* config.
|
|
4757
|
+
*
|
|
4758
|
+
* @param input - The config, the file path, and the dry-run, overwrite and locale-subset switches.
|
|
4759
|
+
* @param deps - Optional adapter registry and file-system overrides.
|
|
4760
|
+
* @returns What was read, what was stored, and everything that was skipped or refused.
|
|
4761
|
+
*
|
|
4762
|
+
* @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
|
|
4763
|
+
* @throws {@link SdkError} `CONFIG_INVALID`: the source locale and one of the target locales are the
|
|
4764
|
+
* same language tag once case and separators are normalized, so a segment could not be attributed
|
|
4765
|
+
* to either.
|
|
4766
|
+
* @throws {@link SdkError} `UNKNOWN_LOCALE`: a requested locale is not a configured target locale.
|
|
4767
|
+
* @throws {@link SdkError} `SOURCE_UNREADABLE`: no file exists at the given path.
|
|
4768
|
+
* @throws {@link SdkError} `SOURCE_INVALID`: the file is oversized, malformed, not a TMX document,
|
|
4769
|
+
* or declares an XML entity. When the problem has a place in the file, the message names its line,
|
|
4770
|
+
* column and, inside a translation unit, the unit's 1-based ordinal, and `cause` is the interchange
|
|
4771
|
+
* reader's error carrying the same as a structured `location`.
|
|
4772
|
+
*
|
|
4773
|
+
* @example
|
|
4774
|
+
* ```ts
|
|
4775
|
+
* const result = await importTmx({ config, file: "legacy-memory.tmx" });
|
|
4776
|
+
* for (const locale of result.locales) {
|
|
4777
|
+
* console.log(`${locale.locale}: ${locale.added} added, ${locale.kept} kept`);
|
|
4778
|
+
* }
|
|
4779
|
+
* ```
|
|
4780
|
+
*/
|
|
4781
|
+
declare function importTmx(input: ImportTmxInput, deps?: ImportTmxDeps): Promise<ImportTmxResult>;
|
|
4782
|
+
|
|
2410
4783
|
/**
|
|
2411
4784
|
* Whatever could be read about the process currently holding a write lock. Both fields are optional
|
|
2412
4785
|
* because a lock file left behind by a killed process may be truncated or unreadable, and reporting
|
|
@@ -2465,6 +4838,10 @@ interface LocaleStartedEvent {
|
|
|
2465
4838
|
* has already stopped the run still emits, and no provider call follows it. That is deliberate, so
|
|
2466
4839
|
* that `batchIndex` always advances to `totalBatches` and a progress bar reaches its end instead of
|
|
2467
4840
|
* stalling at the point the budget tripped.
|
|
4841
|
+
*
|
|
4842
|
+
* Only the translation batches emit it. A dry run sends nothing and emits none, and neither do the
|
|
4843
|
+
* plural-generation requests, so `totalBatches` counts translation batches alone. Keys served from
|
|
4844
|
+
* the translation memory are not batched, so a fully cached locale emits none either.
|
|
2468
4845
|
*/
|
|
2469
4846
|
interface SubBatchProgressEvent {
|
|
2470
4847
|
/** Discriminant for {@link ProgressEvent}. */
|
|
@@ -2482,14 +4859,17 @@ interface LocaleFinishedEvent {
|
|
|
2482
4859
|
readonly type: "locale-finished";
|
|
2483
4860
|
/** The locale that finished. */
|
|
2484
4861
|
readonly locale: string;
|
|
2485
|
-
/**
|
|
4862
|
+
/** The length of the locale's {@link LocaleSummary.translated} list. */
|
|
2486
4863
|
readonly translated: number;
|
|
2487
4864
|
/** This locale's 0-based position among the run's locales. */
|
|
2488
4865
|
readonly localeIndex: number;
|
|
2489
4866
|
/** How many locales the run covers in total. */
|
|
2490
4867
|
readonly totalLocales: number;
|
|
2491
4868
|
}
|
|
2492
|
-
/**
|
|
4869
|
+
/**
|
|
4870
|
+
* Emitted once, after every locale has finished. Not emitted when the run rejects part way, as it
|
|
4871
|
+
* does on a corrupt lock-file.
|
|
4872
|
+
*/
|
|
2493
4873
|
interface RunFinishedEvent {
|
|
2494
4874
|
/** Discriminant for {@link ProgressEvent}. */
|
|
2495
4875
|
readonly type: "run-finished";
|
|
@@ -2506,7 +4886,10 @@ type ProgressListener = (event: ProgressEvent) => void;
|
|
|
2506
4886
|
interface TranslateInput {
|
|
2507
4887
|
/** The resolved project config, normally from {@link loadConfig}. */
|
|
2508
4888
|
readonly config: VerbatraConfig;
|
|
2509
|
-
/**
|
|
4889
|
+
/**
|
|
4890
|
+
* Directory the `files.pattern` is resolved against, and where the lock-file, the translation
|
|
4891
|
+
* memory, and the run-status file live. Defaults to the process working directory.
|
|
4892
|
+
*/
|
|
2510
4893
|
readonly cwd?: string;
|
|
2511
4894
|
/**
|
|
2512
4895
|
* Restrict the run to a subset of the configured target locales, which is how you translate one
|
|
@@ -2526,6 +4909,20 @@ interface TranslateInput {
|
|
|
2526
4909
|
* preview work without spending anything. Defaults to false.
|
|
2527
4910
|
*/
|
|
2528
4911
|
readonly dryRun?: boolean;
|
|
4912
|
+
/**
|
|
4913
|
+
* Compute a pre-run cost estimate and return it on {@link RunSummary.estimate}. Implies
|
|
4914
|
+
* `dryRun`: an estimate constructs no provider, reads no API key, makes no network call, and
|
|
4915
|
+
* writes no file, so it is safe to run anywhere. Defaults to false.
|
|
4916
|
+
*
|
|
4917
|
+
* The figure bounds the plan rather than the invoice: every provider call a live run schedules is
|
|
4918
|
+
* counted, plural generation included, and the prompt is measured from the request payload that
|
|
4919
|
+
* would be sent. Provider-side retries, a repair round, and a language that expands past the
|
|
4920
|
+
* allowance can each push a live run above it; {@link EstimateCaveatCode} names them all. It
|
|
4921
|
+
* carries a currency amount only when the config supplies a `rates` block covering the configured
|
|
4922
|
+
* provider and model; otherwise it reports the quantity and says why the money is missing. See
|
|
4923
|
+
* {@link RunEstimate}.
|
|
4924
|
+
*/
|
|
4925
|
+
readonly estimate?: boolean;
|
|
2529
4926
|
/**
|
|
2530
4927
|
* Remove keys that no longer exist in the source. Defaults to the config's `prune`, then to
|
|
2531
4928
|
* false, so orphaned keys are reported but kept unless removal is asked for explicitly.
|
|
@@ -2533,7 +4930,8 @@ interface TranslateInput {
|
|
|
2533
4930
|
readonly prune?: boolean;
|
|
2534
4931
|
/**
|
|
2535
4932
|
* Generate the plural categories a target language requires rather than translating each
|
|
2536
|
-
* category separately. Defaults to the config's `generatePlurals`, then to false.
|
|
4933
|
+
* category separately. Defaults to the config's `generatePlurals`, then to false. Takes effect
|
|
4934
|
+
* only with an LLM provider; a machine-translation provider generates nothing.
|
|
2537
4935
|
*/
|
|
2538
4936
|
readonly generatePlurals?: boolean;
|
|
2539
4937
|
/**
|
|
@@ -2543,17 +4941,22 @@ interface TranslateInput {
|
|
|
2543
4941
|
readonly onLockWait?: LockWaitListener;
|
|
2544
4942
|
/** Called as locales and sub-batches start and finish, for progress reporting. */
|
|
2545
4943
|
readonly onProgress?: ProgressListener;
|
|
2546
|
-
/**
|
|
4944
|
+
/**
|
|
4945
|
+
* How long, in milliseconds, to wait for a locale's write lock before that locale fails with
|
|
4946
|
+
* `LOCK_CONTENDED`. Defaults to ten minutes. Not used on a dry run, which takes no lock.
|
|
4947
|
+
*/
|
|
2547
4948
|
readonly lockAcquireTimeoutMs?: number;
|
|
2548
4949
|
/**
|
|
2549
4950
|
* How many locales to run at once. Must be an integer of at least 1; defaults to 1. On a live
|
|
2550
|
-
* run it cannot be combined with a configured token budget
|
|
2551
|
-
*
|
|
4951
|
+
* run it cannot be combined with a configured token budget: the ceiling would still hold, but
|
|
4952
|
+
* which locale loses its remaining work would depend on the order the locales interleave, so the
|
|
4953
|
+
* run would not be reproducible.
|
|
2552
4954
|
*/
|
|
2553
4955
|
readonly concurrency?: number;
|
|
2554
4956
|
/**
|
|
2555
|
-
* Consult and update the translation memory. Defaults to true. Turning it
|
|
2556
|
-
* through the provider, which is what to do when you want to re-pay for a
|
|
4957
|
+
* Consult and update the translation memory, fuzzy reuse included. Defaults to true. Turning it
|
|
4958
|
+
* off forces every key through the provider, which is what to do when you want to re-pay for a
|
|
4959
|
+
* fresh translation. A dry run never reads the memory, whatever this says.
|
|
2557
4960
|
*/
|
|
2558
4961
|
readonly cache?: boolean;
|
|
2559
4962
|
}
|
|
@@ -2566,11 +4969,38 @@ interface TranslateDeps {
|
|
|
2566
4969
|
/** File-system port. Defaults to the real file system. */
|
|
2567
4970
|
readonly fs?: SdkFs;
|
|
2568
4971
|
}
|
|
4972
|
+
/**
|
|
4973
|
+
* Whether a set of run options resolves to a dry run. `estimate` implies `dryRun`, and this is the
|
|
4974
|
+
* one place that implication is decided: {@link translate} calls it, and so should any caller that
|
|
4975
|
+
* has to know before the run starts whether anything will be written or spent, rather than
|
|
4976
|
+
* re-deriving the rule and drifting from it.
|
|
4977
|
+
*
|
|
4978
|
+
* @param input - The `dryRun` and `estimate` options as the caller received them.
|
|
4979
|
+
* @returns True when the run will construct no provider, write no file, and spend nothing.
|
|
4980
|
+
*
|
|
4981
|
+
* @example
|
|
4982
|
+
* ```ts
|
|
4983
|
+
* import { resolveDryRun, translate } from "@verbatra/sdk";
|
|
4984
|
+
*
|
|
4985
|
+
* if (!resolveDryRun(options)) {
|
|
4986
|
+
* await prepareWorkspaceForWrites();
|
|
4987
|
+
* }
|
|
4988
|
+
* const summary = await translate({ config, ...options });
|
|
4989
|
+
* ```
|
|
4990
|
+
*/
|
|
4991
|
+
declare function resolveDryRun(input: {
|
|
4992
|
+
/** The caller's {@link TranslateInput.dryRun}. */
|
|
4993
|
+
readonly dryRun?: boolean | undefined;
|
|
4994
|
+
/** The caller's {@link TranslateInput.estimate}, which implies a dry run. */
|
|
4995
|
+
readonly estimate?: boolean | undefined;
|
|
4996
|
+
}): boolean;
|
|
2569
4997
|
/**
|
|
2570
4998
|
* Runs the one-shot translation flow over every configured target locale, or over the subset named
|
|
2571
4999
|
* by `locales`: read the source, diff
|
|
2572
5000
|
* each locale against the lock-file baseline, translate what is missing or stale, verify placeholder
|
|
2573
|
-
* and ICU integrity, write the locale files, and update the lock-file and translation memory.
|
|
5001
|
+
* and ICU integrity, write the locale files, and update the lock-file and translation memory. A
|
|
5002
|
+
* live run also records its summary in the run-status file that {@link runStatus} reads; a failure
|
|
5003
|
+
* to write that file or the translation memory is swallowed rather than failing the run.
|
|
2574
5004
|
*
|
|
2575
5005
|
* Failure handling is the contract worth understanding. Whole-run problems, such as an unreadable
|
|
2576
5006
|
* source file or a provider that cannot be constructed, throw an {@link SdkError} before any locale
|
|
@@ -2640,13 +5070,6 @@ interface TranslateDeps {
|
|
|
2640
5070
|
*/
|
|
2641
5071
|
declare function translate(input: TranslateInput, deps?: TranslateDeps): Promise<RunSummary>;
|
|
2642
5072
|
|
|
2643
|
-
/**
|
|
2644
|
-
* The two delimiter-separated interchange formats. They are one implementation with one parameter, the
|
|
2645
|
-
* field delimiter: `tsv` is not a second format, it is `csv` with a tab. The member is also the file
|
|
2646
|
-
* extension an export writes and an import reads back.
|
|
2647
|
-
*/
|
|
2648
|
-
type DelimitedFormat = "csv" | "tsv";
|
|
2649
|
-
|
|
2650
5073
|
/**
|
|
2651
5074
|
* The file shape of a translator handoff: a single styled `.xlsx` workbook with one sheet per
|
|
2652
5075
|
* locale, or one plain `.csv` or `.tsv` file per locale.
|
|
@@ -2707,7 +5130,10 @@ interface ExportWorkbookDeps {
|
|
|
2707
5130
|
}
|
|
2708
5131
|
/** What {@link exportWorkbook} wrote. */
|
|
2709
5132
|
interface ExportWorkbookResult {
|
|
2710
|
-
/**
|
|
5133
|
+
/**
|
|
5134
|
+
* The absolute path written: the workbook file for `xlsx`, or the directory the per-locale files
|
|
5135
|
+
* were written into for a delimited format.
|
|
5136
|
+
*/
|
|
2711
5137
|
readonly path: string;
|
|
2712
5138
|
/** Row counts per exported locale. */
|
|
2713
5139
|
readonly locales: readonly {
|
|
@@ -2719,7 +5145,10 @@ interface ExportWorkbookResult {
|
|
|
2719
5145
|
}
|
|
2720
5146
|
/**
|
|
2721
5147
|
* Writes the strings awaiting translation to a handoff a human translator can work in: a styled
|
|
2722
|
-
* `.xlsx` workbook with one sheet per locale, or one `.csv` or `.tsv` file per locale.
|
|
5148
|
+
* `.xlsx` workbook with one sheet per locale, or one `.csv` or `.tsv` file per locale. A delimited
|
|
5149
|
+
* export also writes a `.verbatra-export-<format>.json` manifest into the output directory naming
|
|
5150
|
+
* the locales it exported, which {@link importWorkbook} uses to tell a leftover file from an earlier
|
|
5151
|
+
* export apart from a current one.
|
|
2723
5152
|
*
|
|
2724
5153
|
* By default only missing and stale keys are exported, which is what makes the handoff a work list
|
|
2725
5154
|
* rather than a dump of the whole project. Each row carries the source text alongside any existing
|
|
@@ -2795,9 +5224,11 @@ interface ImportWorkbookDeps {
|
|
|
2795
5224
|
* handoff.
|
|
2796
5225
|
*
|
|
2797
5226
|
* Imported values are held to the same integrity gate as provider output, so a translator who drops
|
|
2798
|
-
* a placeholder or breaks ICU syntax has that row refused rather than
|
|
2799
|
-
*
|
|
2800
|
-
*
|
|
5227
|
+
* a placeholder, breaks inline markup or breaks ICU syntax has that row refused rather than
|
|
5228
|
+
* written, and a row whose source text changed since the export is refused the same way. A row
|
|
5229
|
+
* holding exactly `[[CLEAR]]` empties that key's translation. Each locale takes its write lock, and
|
|
5230
|
+
* the lock-file and translation memory are updated exactly as in a {@link translate} run, so an
|
|
5231
|
+
* imported translation counts as up to date afterwards.
|
|
2801
5232
|
*
|
|
2802
5233
|
* Damage is contained rather than fatal: a blank row keeps the existing translation and its
|
|
2803
5234
|
* baseline, an unreadable row is reported as a {@link MalformedRowReport}, and a repeated key is
|
|
@@ -2805,9 +5236,12 @@ interface ImportWorkbookDeps {
|
|
|
2805
5236
|
* the returned {@link RunSummary} rather than aborting the import. A sheet or file naming a locale
|
|
2806
5237
|
* that is not configured is contained the same way: that locale fails with `CONFIG_INVALID` on its
|
|
2807
5238
|
* own {@link LocaleSummary}, so nothing is written to an unmanaged path and the configured locales
|
|
2808
|
-
* still import.
|
|
2809
|
-
*
|
|
2810
|
-
*
|
|
5239
|
+
* still import. A configured target locale the handoff carries no sheet or file for fails with
|
|
5240
|
+
* `WORKBOOK_SHEET_MISSING`, a delimited file the directory's export manifest does not list fails
|
|
5241
|
+
* with `HANDOFF_FILE_STALE` without being applied, and a row naming a key neither the source nor
|
|
5242
|
+
* the target holds fails its locale. Once the handoff has been read, every per-locale failure is
|
|
5243
|
+
* isolated this way, so callers should inspect {@link RunSummary.failed} and
|
|
5244
|
+
* {@link RunSummary.partial} rather than relying on a thrown error. A corrupt lock-file is the one exception, because it is a single
|
|
2811
5245
|
* shared file rather than a per-locale one: it aborts the whole run even when it is discovered
|
|
2812
5246
|
* after a locale has been applied, so the locales still to come are not written at all.
|
|
2813
5247
|
*
|
|
@@ -2860,9 +5294,10 @@ interface LocalePathResolverConfig {
|
|
|
2860
5294
|
/**
|
|
2861
5295
|
* The configured format. Every format maps each locale to a distinct path except a
|
|
2862
5296
|
* shared-catalogue format (one holding every locale in a single file, such as `apple-xcstrings`),
|
|
2863
|
-
* which maps every locale to the same path.
|
|
5297
|
+
* which maps every locale to the same path. A `custom:` format is always treated as one file per
|
|
5298
|
+
* locale.
|
|
2864
5299
|
*/
|
|
2865
|
-
readonly format:
|
|
5300
|
+
readonly format: FormatId$1;
|
|
2866
5301
|
/** The file layout. */
|
|
2867
5302
|
readonly files: {
|
|
2868
5303
|
/** The path pattern, which must contain the `{locale}` token. */
|
|
@@ -2876,12 +5311,15 @@ interface LocalePathResolver {
|
|
|
2876
5311
|
/**
|
|
2877
5312
|
* The absolute path of a locale's file. Accepts any locale string, including ones outside the
|
|
2878
5313
|
* configured set, so it can be used for exploratory reads.
|
|
5314
|
+
*
|
|
5315
|
+
* @throws {@link SdkError} `LOCALE_LAYOUT_INVALID`: the locale is outside the configured set and
|
|
5316
|
+
* has no valid single-segment spelling under the configured style.
|
|
2879
5317
|
*/
|
|
2880
5318
|
pathFor(locale: string): string;
|
|
2881
5319
|
/**
|
|
2882
5320
|
* The configured locale owning an absolute path, or `undefined` when the path belongs to no
|
|
2883
|
-
* configured locale. The path is
|
|
2884
|
-
* still resolves.
|
|
5321
|
+
* configured locale. The path is resolved against the resolver's `cwd` and normalized before
|
|
5322
|
+
* lookup, so a relative or non-canonical path still resolves.
|
|
2885
5323
|
*
|
|
2886
5324
|
* Always `undefined` for a shared-catalogue format: every configured locale resolves to the same
|
|
2887
5325
|
* path there, so no single locale owns it.
|
|
@@ -3041,8 +5479,11 @@ declare const scaffoldingMetadata: {
|
|
|
3041
5479
|
readonly openai: "maxOutputTokens";
|
|
3042
5480
|
readonly gemini: "maxOutputTokens";
|
|
3043
5481
|
};
|
|
3044
|
-
/**
|
|
3045
|
-
|
|
5482
|
+
/**
|
|
5483
|
+
* Every built-in i18n file format the SDK can read and write. A `custom:` format is never listed,
|
|
5484
|
+
* since its adapter ships outside verbatra.
|
|
5485
|
+
*/
|
|
5486
|
+
readonly supportedFormats: readonly ["i18next-json", "vue-i18n-json", "next-intl-json", "ngx-translate-json", "xliff", "yaml", "arb", "properties", "apple-strings", "apple-xcstrings", "android-xml", "gettext-po", "ini", "resx"];
|
|
3046
5487
|
};
|
|
3047
5488
|
|
|
3048
5489
|
/**
|
|
@@ -3084,7 +5525,10 @@ type WatchRunResult = {
|
|
|
3084
5525
|
interface WatchInput {
|
|
3085
5526
|
/** The resolved project config, normally from {@link loadConfig}. */
|
|
3086
5527
|
readonly config: VerbatraConfig;
|
|
3087
|
-
/**
|
|
5528
|
+
/**
|
|
5529
|
+
* Directory the `files.pattern` is resolved against, and where each run's lock-file, translation
|
|
5530
|
+
* memory, and run-status file live. Defaults to the process working directory.
|
|
5531
|
+
*/
|
|
3088
5532
|
readonly cwd?: string;
|
|
3089
5533
|
/**
|
|
3090
5534
|
* Restrict every run of the session to a subset of the configured target locales. Validated once
|
|
@@ -3103,11 +5547,17 @@ interface WatchInput {
|
|
|
3103
5547
|
readonly onLockWait?: LockWaitListener;
|
|
3104
5548
|
/** Called as locales and sub-batches start and finish, for progress reporting. */
|
|
3105
5549
|
readonly onProgress?: ProgressListener;
|
|
3106
|
-
/**
|
|
5550
|
+
/**
|
|
5551
|
+
* How long, in milliseconds, to wait for a locale's write lock before that locale fails with
|
|
5552
|
+
* `LOCK_CONTENDED` on the run's summary. Defaults to ten minutes.
|
|
5553
|
+
*/
|
|
3107
5554
|
readonly lockAcquireTimeoutMs?: number;
|
|
3108
|
-
/**
|
|
5555
|
+
/**
|
|
5556
|
+
* How many locales to run at once. Defaults to 1. Validated once when watching starts, and
|
|
5557
|
+
* applied to every run.
|
|
5558
|
+
*/
|
|
3109
5559
|
readonly concurrency?: number;
|
|
3110
|
-
/** Consult and update the translation memory. Defaults to true. */
|
|
5560
|
+
/** Consult and update the translation memory, fuzzy reuse included. Defaults to true. */
|
|
3111
5561
|
readonly cache?: boolean;
|
|
3112
5562
|
}
|
|
3113
5563
|
/** Injectable dependencies for {@link watch}. Every field has a working default. */
|
|
@@ -3187,4 +5637,4 @@ interface WatchController {
|
|
|
3187
5637
|
*/
|
|
3188
5638
|
declare function watch(input: WatchInput, deps?: WatchDeps): Promise<WatchController>;
|
|
3189
5639
|
|
|
3190
|
-
export { type BudgetBehavior, CACHE_FILE_NAME, type CheckDeps, type CheckInput, type CheckSummary, type ConfigSource, type CreateProvider, type CreateWatcher, DEFAULT_DELIMITED_PATH, DEFAULT_EXCHANGE_FORMAT, DEFAULT_WORKBOOK_PATH, type DiffDeps, type DiffInput, type DiffSummary, type DoctorCheck, type DoctorCheckId, type DoctorCheckStatus, type DoctorDeps, type DoctorInput, type DoctorResult, type DuplicateKeyReport, EXCHANGE_FORMATS, type EditEntryDeps, type EditEntryInput, type EditEntryResult, type ExchangeFormat, type ExportWorkbookDeps, type ExportWorkbookInput, type ExportWorkbookResult, type GlossaryFileDeps, type GlossaryFileInput, type GlossaryProvenance, type ImportWorkbookDeps, type ImportWorkbookInput, type IntegrityGateReason, type KeyIntegrityDeps, type KeyIntegrityEntry, type KeyIntegrityInput, type KeyValueDeps, type KeyValueInput, type KeyValuePair, type KeyValueResult, LOCK_FILE_NAME, type LoadConfigOptions, type LoadLockFileDeps, type LoadLockFileInput, type LoadedConfig, type LocaleCheckSummary, type LocaleDiff, type LocaleFileSnapshot, type LocaleFinishedEvent, type LocaleKeyIntegrity, type LocaleNotice, type LocalePathResolver, type LocalePathResolverConfig, type LocaleSnapshotDelta, type LocaleStartedEvent, type LocaleStyle, type LocaleSummary, type LocaleValues, type LocaleValuesDeps, type LocaleValuesInput, type LockFile, type LockHolder, type LockLocaleState, type LockStateDeps, type LockStateInput, type LockStateResult, type LockWaitEvent, type LockWaitListener, type MalformedRowReport, type NeedsReviewEntry, type ProgressEvent, type ProgressListener, type ProviderConfig, type ProviderId, type ReadLocaleFileSnapshotDeps, type ReadLocaleFileSnapshotInput, type RetranslateEntryDeps, type RetranslateEntryInput, type RetranslateEntryResult, type ReviewReasonCode, type RunBudget, type RunFinishedEvent, type RunStatusDeps, type RunStatusFile, type RunStatusInput, type RunStatusLocale, type RunStatusResult, type RunSummary, type RunTranslate, type ScaffoldableProviderId, SdkError, type SdkErrorCode, type SdkFs, type SdkNotice, type SdkNoticeCode, type SubBatchProgressEvent, type SupportedFormat$1 as SupportedFormat, type TranslateDeps, type TranslateInput, type TranslationMemory, type UpdateGlossaryTermInput, type UsageSummary, type VerbatraConfig, type VerbatraConfigInput, type WatchController, type WatchDeps, type WatchInput, type WatchRunResult, type Watcher, check, createLocalePathResolver, defineConfig, diff, diffLocaleSnapshots, doctor, editEntry, exportWorkbook, importWorkbook, keyIntegrity, keyValue, loadConfig, loadConfigWithMeta, loadLockFile, localeValues, lockState, readGlossaryFile, readLocaleFileSnapshot, redact, retranslateEntry, runStatus, scaffoldingMetadata, translate, updateGlossaryTerm, verbatraConfigSchema, watch };
|
|
5640
|
+
export { AdapterError, type AdapterErrorCode, type AdapterFs, AdapterRegistry, type AdapterResolution, type AddedKey, type BillingUnit, type BoundedReadOutcome, type BudgetBehavior, type BudgetStanding, type BuildWriteTree, CACHE_FILE_NAME, type CharacterRate, type CharacterRunQuantity, type CheckDeps, type CheckInput, type CheckSummary, type ComparePlaceholders, type ComputeInvalidIcuKeys, type ConfigSource, type CreateProvider, type CreateWatcher, type CustomFormatId$1 as CustomFormatId, DEFAULT_DELIMITED_PATH, DEFAULT_EXCHANGE_FORMAT, DEFAULT_TMX_PATH, DEFAULT_TYPES_PATH, DEFAULT_WORKBOOK_PATH, type DeriveDescriptions, type DeriveEntry, type DiffDeps, type DiffInput, type DiffSummary, type DirectoryEntry, type DoctorCheck, type DoctorCheckId, type DoctorCheckStatus, type DoctorDeps, type DoctorInput, type DoctorResult, type DuplicateKeyReport, EXCHANGE_FORMATS, type EditEntryDeps, type EditEntryInput, type EditEntryResult, type EstimateCaveatCode, type EstimateIdentity, type EstimatePricing, type ExchangeFormat, type ExportTmxDeps, type ExportTmxInput, type ExportTmxLocaleCount, type ExportTmxResult, type ExportWorkbookDeps, type ExportWorkbookInput, type ExportWorkbookResult, type ExtractDeps, type ExtractInput, type ExtractPlaceholders, type ExtractResult, type ExtractionConfig, type FlatFileAdapterOptions, type FlatParseOutcome, type FlatParseResult, type FormatAdapter, type FormatId$1 as FormatId, type FuzzyCacheHit, type GenerateTypesDeps, type GenerateTypesInput, type GenerateTypesResult, type GlossaryFileDeps, type GlossaryFileInput, type GlossaryProvenance, INTEGRITY_GATE_REASONS, type ImportTmxDeps, type ImportTmxInput, type ImportTmxLocaleResult, type ImportTmxResult, type ImportWorkbookDeps, type ImportWorkbookInput, type InconsistencyGroup, type InconsistentTranslation, type IntegrityGateReason, type JsonLeaf, type JsonRecord, type JsonTree, type KeyConflict, type KeyIntegrityDeps, type KeyIntegrityEntry, type KeyIntegrityInput, type KeyMode, type KeyValueDeps, type KeyValueInput, type KeyValuePair, type KeyValueResult, LOCK_FILE_NAME, type LiteralFinding, type LiteralScan, type LiteralSuppressionReason, type LoadConfigOptions, type LoadLockFileDeps, type LoadLockFileInput, type LoadedConfig, type LocaleCheckSummary, type LocaleDiff, type LocaleEstimate, type LocaleEstimateQuantity, type LocaleFileSnapshot, type LocaleFinishedEvent, type LocaleKeyIntegrity, type LocaleNotice, type LocalePathResolver, type LocalePathResolverConfig, type LocaleResource$1 as LocaleResource, type LocaleSnapshotDelta, type LocaleStartedEvent, type LocaleStyle, type LocaleSummary, type LocaleValues, type LocaleValuesDeps, type LocaleValuesInput, type LockFile, type LockHolder, type LockLocaleState, type LockStateDeps, type LockStateInput, type LockStateResult, type LockWaitEvent, type LockWaitListener, type MalformedRowReport, type ModelRate, type NeedsReviewEntry, type OrderedRecord, type OrderedValue, type PlaceholderIntegrityResult$1 as PlaceholderIntegrityResult, type PossiblyDynamicKey, type PricedLocaleEstimate, type PricedRunEstimate, type ProgressEvent, type ProgressListener, type ProviderBilling, type ProviderConfig, type ProviderId, type PseudolocalizeDeps, type PseudolocalizeInput, type PseudolocalizeResult, REVIEW_REASON_CODES, type RateCard, type ReadLocaleFileSnapshotDeps, type ReadLocaleFileSnapshotInput, type ReadResult, type ResolveOptions, type RetranslateEntryDeps, type RetranslateEntryInput, type RetranslateEntryResult, type ReviewReasonCode, type RunBudget, type RunEstimate, type RunEstimateQuantity, type RunFinishedEvent, type RunStatusDeps, type RunStatusFile, type RunStatusInput, type RunStatusLocale, type RunStatusResult, type RunSummary, type RunTranslate, type ScaffoldableProviderId, type ScanDiagnostic, type ScanDiagnosticReason, SdkError, type SdkErrorCode, type SdkFs, type SdkNotice, type SdkNoticeCode, type Sniff, type SourceFramework, type SourceLocation, type SubBatchProgressEvent, type SupportedFormat$1 as SupportedFormat, type SuppressedLiteral, type TmxErrorLocation, type TmxLanguageReport, type TmxRejectionCounts, type TmxRejectionReason, type TokenRate, type TokenRunQuantity, type TranslateDeps, type TranslateInput, type TranslationEntry$1 as TranslationEntry, type TranslationMemory, type TreeFileAdapterOptions, type UnpricedLocaleEstimate, type UnpricedRunEstimate, type UnresolvedArgumentReason, type UnresolvedMessage, type UnusedKey, type UnusedKeysNotRun, type UnusedKeysNotRunReason, type UnusedKeysPrefixSite, type UnusedKeysReport, type UnusedKeysScan, type UnusedKeysSite, type UnusedKeysUnreliability, type UnusedKeysUnreliableReason, type UpdateGlossaryTermInput, type UsageSummary, type ValidateMessage, type ValidateTree, type VerbatraConfig, type VerbatraConfigInput, type WatchController, type WatchDeps, type WatchInput, type WatchRunResult, type Watcher, budgetStanding, check, createDefaultRegistry, createFlatFileAdapter, createLocalePathResolver, createTreeFileAdapter, defineConfig, diff, diffLocaleSnapshots, doctor, editEntry, exportTmx, exportWorkbook, extract, generateTypes, importTmx, importWorkbook, isCustomFormatId, keyIntegrity, keyValue, loadConfig, loadConfigWithMeta, loadLockFile, localeValues, lockState, nodeAdapterFs, pseudolocalize, readGlossaryFile, readLocaleFileSnapshot, redact, resolveDryRun, retranslateEntry, runStatus, scaffoldingMetadata, tmxErrorLocation, translate, updateGlossaryTerm, verbatraConfigSchema, watch };
|