langsys-js-typescript 0.6.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,933 @@
1
+ /**
2
+ * Server messages (spec MSG family): the entries a server attaches to its
3
+ * framework's own error response, and how a client finds and renders them.
4
+ *
5
+ * What translation needs is small: the `template` — the framework's own
6
+ * sentence, unfilled, with the field's label written in — and the `params`
7
+ * that fill its `{name}` markers. `message` is the template already filled, the
8
+ * fallback a client shows when it cannot look the template up. Everything else
9
+ * is the framework's and passes through unchanged: `field` in the framework's
10
+ * own path format (a dotted string, Pydantic's `loc` array), and `code`, the
11
+ * framework's own identifier for the failure (Laravel's rule name, Pydantic's
12
+ * error `type`, Django's `code`), or none. The error body is the framework's
13
+ * too, so where the entries sit and what their pieces are called are the
14
+ * app's configuration.
15
+ *
16
+ * Everything here is pure except `renderServerMessage`, which renders through
17
+ * `t()`; that one is exported from the main entry only.
18
+ */
19
+ /** One server message entry (MSG-1), in the SDK's own piece names. */
20
+ interface ServerMessage {
21
+ template?: string;
22
+ params?: Record<string, unknown>;
23
+ message?: string;
24
+ /** The field, in the framework's own path format, unchanged. */
25
+ field?: unknown;
26
+ /** The framework's own identifier for the failure, unchanged. Absent where the framework has none. */
27
+ code?: unknown;
28
+ }
29
+ /** The piece names an app's entries use, where they differ from the SDK's. */
30
+ type ServerMessagePieces = Partial<Record<'template' | 'params' | 'message' | 'field' | 'code', string>>;
31
+ /** Where the entries sit in a response body (MSG-1). One of `key` or `resolver` is required. */
32
+ interface ResolveServerMessagesOptions {
33
+ /**
34
+ * The dotted path the server attached the entries under, beside the
35
+ * framework's own error body — Laravel's package default is `langsys_errors`.
36
+ */
37
+ key?: string;
38
+ /** Maps a body to entries itself, for an app whose failures are carried some other way. */
39
+ resolver?: (body: unknown) => unknown;
40
+ /** The entries' piece names, when the server was configured with names of its own. */
41
+ pieces?: ServerMessagePieces;
42
+ }
43
+ /** The category templates are registered and rendered under unless configured otherwise (MSG-6). */
44
+ declare const DEFAULT_SERVER_MESSAGE_CATEGORY = "Errors";
45
+ /** The marker names in a template, once each, in the order they first appear. */
46
+ declare function templateMarkers(template: string): string[];
47
+ /**
48
+ * Fill a template's markers from params (MSG-4). A marker with no param stays
49
+ * as its literal marker rather than going blank, so a missing value is visible
50
+ * instead of producing a sentence that reads as complete. A structure is never
51
+ * printed into the sentence; its marker stays too.
52
+ */
53
+ declare function fillTemplate(template: string, params?: Record<string, unknown>): string;
54
+ /**
55
+ * An entry from its wire form, or null when it is not one. An entry needs a
56
+ * `template` to look up or a `message` to show; either is a string. `params`
57
+ * is kept when it is an object; `field` and `code` pass through as they are.
58
+ */
59
+ declare function toServerMessage(value: unknown, pieces?: ServerMessagePieces): ServerMessage | null;
60
+ /**
61
+ * The entries a response carries, found where the app's configuration says
62
+ * they are (MSG-1). The body is the framework's own and is never searched by
63
+ * shape: `key` names the dotted path the server attached the entries under,
64
+ * and `resolver` maps the body to entries itself. Throws when neither is given,
65
+ * since there is nowhere to look. `pieces` renames the entries' pieces.
66
+ *
67
+ * Accepts the decoded body or its JSON text, and never changes the body.
68
+ * Unreadable JSON, or a key the body does not carry, resolves to no entries:
69
+ * this runs in an error path already.
70
+ */
71
+ declare function resolveServerMessages(body: unknown, options: ResolveServerMessagesOptions): ServerMessage[];
72
+
73
+ /**
74
+ * Catalog snapshots (spec SNAP family): one file format every Langsys SDK
75
+ * writes and reads, so a snapshot exported by any core loads in any other.
76
+ *
77
+ * A snapshot is the flat catalog (`GET /translations`) for chosen locales and
78
+ * categories, carried as a file so a first render, a mobile bundle or an
79
+ * offline session has translations with no network call. It is a cache, never
80
+ * a source: Langsys produces it, a checksum over its canonical serialisation
81
+ * catches an edited one, and the refresh path is a new export.
82
+ *
83
+ * Everything here is pure. Seeding a loaded snapshot into the SDK is
84
+ * `LangsysApp.loadSnapshot`, on the main entry.
85
+ */
86
+ /** The `format` member every snapshot carries. */
87
+ declare const SNAPSHOT_FORMAT = "langsys-catalog-snapshot";
88
+ /** The only `version` this SDK reads and writes. */
89
+ declare const SNAPSHOT_VERSION = 1;
90
+ /** A category's flat entries as `GET /translations` returns them: phrase → translation or null, or a block id → its phrase map. */
91
+ type SnapshotCategory = Record<string, string | null | Record<string, string | null>>;
92
+ /** locale → category → entries. A category the locale does not hold is absent; one it holds empty is `{}`. */
93
+ type SnapshotCatalog = Record<string, Record<string, SnapshotCategory>>;
94
+ /** The members the checksum covers. */
95
+ interface SnapshotPayload {
96
+ project_id: string;
97
+ /** UTC, `YYYY-MM-DDTHH:MM:SSZ`. */
98
+ generated_at: string;
99
+ base_locale: string;
100
+ /** Lowercase `xx-yy`, sorted ascending. */
101
+ locales: string[];
102
+ /** Sorted ascending. */
103
+ categories: string[];
104
+ catalog: SnapshotCatalog;
105
+ }
106
+ /** A whole snapshot document. */
107
+ interface CatalogSnapshot extends SnapshotPayload {
108
+ format: typeof SNAPSHOT_FORMAT;
109
+ version: typeof SNAPSHOT_VERSION;
110
+ /** `sha256:` and the lowercase hex digest of the canonical serialisation of the payload. */
111
+ checksum: string;
112
+ }
113
+ /** Why a snapshot was refused. */
114
+ type SnapshotRefusal = 'not-json' | 'format' | 'version' | 'missing-member' | 'checksum';
115
+ /** Thrown when a snapshot cannot be loaded. `reason` names why; the message says what to do. */
116
+ declare class SnapshotError extends Error {
117
+ readonly reason: SnapshotRefusal;
118
+ constructor(message: string, reason: SnapshotRefusal);
119
+ }
120
+ /**
121
+ * The canonical serialisation the checksum is taken over, byte for byte the
122
+ * same in every SDK:
123
+ *
124
+ * - strings escaped as CID-1 serialises them — `"`, `\` and U+0000–U+001F only,
125
+ * everything else (`/`, non-ASCII, U+2028) raw — which is `JSON.stringify`'s
126
+ * own string escaping;
127
+ * - no whitespace;
128
+ * - object members sorted by key in Unicode code point order. Neither
129
+ * `Object.keys` order (integer-like keys first) nor the default `sort()`
130
+ * (UTF-16 code units, which puts a key above U+FFFF before one in
131
+ * U+E000–U+FFFF) is that order, so both are avoided;
132
+ * - a map is `{}` even when empty; `null` is `null`.
133
+ */
134
+ declare function canonicalSnapshotJson(value: unknown): string;
135
+ /** `sha256:` and the lowercase hex SHA-256 of the canonical serialisation of the payload members. */
136
+ declare function snapshotChecksum(payload: SnapshotPayload): string;
137
+ /**
138
+ * Build a snapshot document from catalogs already fetched — the writing half,
139
+ * for an export tool. `catalogs` is locale → the catalog `GET /translations`
140
+ * returned; only the chosen categories are kept, and a category a locale does
141
+ * not hold stays absent for that locale. Locales are lowercased; both lists are
142
+ * sorted.
143
+ */
144
+ declare function buildSnapshot(options: {
145
+ projectId: string;
146
+ baseLocale: string;
147
+ catalogs: Record<string, Record<string, SnapshotCategory>>;
148
+ categories: string[];
149
+ generatedAt?: Date;
150
+ }): CatalogSnapshot;
151
+ /**
152
+ * Read a snapshot from its JSON text or parsed document, refusing — with an
153
+ * error naming the reason — a different `format`, an unsupported `version`, a
154
+ * missing member, or a checksum the contents no longer match. The file may be
155
+ * any JSON encoding of the document; the checksum is recomputed from the
156
+ * canonical serialisation, never compared against the file's bytes.
157
+ */
158
+ declare function parseSnapshot(input: string | unknown): CatalogSnapshot;
159
+
160
+ /**
161
+ * Translation function type machinery.
162
+ *
163
+ * The big trick here is `ExtractParamKeys` — a recursive template-literal
164
+ * type that pulls placeholder names out of a phrase at compile time, so
165
+ * `t('Hello, {name}!', { name: 'Sarah' })` is type-checked: passing
166
+ * an object without `name`, or with a key not in the phrase, is a TS error.
167
+ */
168
+ /** Allowed value types for interpolation. Mirrors what ICU MessageFormat will accept. */
169
+ type ParamPrimitive = string | number | Date | boolean;
170
+ /**
171
+ * Pull placeholder names out of a phrase.
172
+ * @example
173
+ * ExtractParamKeys<"Hello, {name}!"> // "name"
174
+ * ExtractParamKeys<"{first} and {last}"> // "first" | "last"
175
+ * ExtractParamKeys<"no placeholders here"> // never
176
+ */
177
+ type ExtractParamKeys<S extends string> = S extends `${string}{${infer K}}${infer Rest}` ? K | ExtractParamKeys<Rest> : never;
178
+ /**
179
+ * Optional `<name>_gender` companions, one per placeholder. Langsys promotes a plain
180
+ * `{username}` to `{username_gender, select, …}` in gendered target locales, so the
181
+ * argument exists in the translation but never in the phrase you wrote. Optional because
182
+ * most apps don't know the gender; without it the neutral `other` branch renders.
183
+ * @example
184
+ * GenderParamsFor<"{username} has been invited"> // { username_gender?: string }
185
+ */
186
+ type GenderParamsFor<S extends string> = {
187
+ [K in ExtractParamKeys<S> as `${K}_gender`]?: string;
188
+ };
189
+ /** Build the params object type required for a given phrase. */
190
+ type ParamsFor<S extends string> = {
191
+ [K in ExtractParamKeys<S>]: ParamPrimitive;
192
+ } & GenderParamsFor<S>;
193
+ /**
194
+ * Whether a phrase can be a legacy key (spec MIG-2): no whitespace and no
195
+ * placeholder, like `checkout.greet`. In the legacy-key mode such an argument
196
+ * resolves to a source value whose placeholders the call site cannot see, so its
197
+ * params are accepted rather than checked. A sentence is never key-shaped, so its
198
+ * placeholders stay checked. A phrase that is not a literal (`string`) is
199
+ * key-shaped too: nothing about its placeholders is known.
200
+ */
201
+ type KeyShaped<P extends string> = P extends `${string}${' ' | '\n' | '\t'}${string}` ? false : P extends `${string}{${string}` ? false : true;
202
+ /**
203
+ * Rest-tuple for the optional params argument.
204
+ * Placeholders in the phrase → the params they name must be passed.
205
+ * No placeholders, and the phrase is a sentence → no params.
206
+ * No placeholders, and the phrase is key-shaped → params are optional and free.
207
+ */
208
+ type TArgs<P extends string> = [ExtractParamKeys<P>] extends [never] ? KeyShaped<P> extends true ? [params?: TranslationParams] : [] : [params: ParamsFor<P>];
209
+ /** Loose params type — used inside generic stores where the phrase isn't a known literal. */
210
+ type TranslationParams = Record<string, ParamPrimitive>;
211
+ /**
212
+ * The translation function signature.
213
+ *
214
+ * t('Save') // no category, no params
215
+ * t('Save', 'UI') // categorized
216
+ * t('Hello, {name}!', { name: 'X' }) // no category, with params
217
+ * t('Hello, {name}!', 'Greetings', { name: 'X' }) // category + params
218
+ * t('checkout.greet', { name: 'X' }) // a legacy key: params accepted
219
+ * t`Hello, ${{ name }}!` // a template tag: `{name}`, with its value
220
+ *
221
+ * The two phrase overloads discriminate on the type at position 2 (string →
222
+ * category, object → params). `category` is required for the second overload —
223
+ * pass any non-empty string. There is no "null category" wire concept:
224
+ * absent category means absent.
225
+ *
226
+ * As a template tag, each value written as a one-key object is a placeholder
227
+ * named by its key in snake_case (`${{ userName }}` is `{user_name}`), with the
228
+ * value as its param. With any other value, the tag returns the rendered text
229
+ * as written: nothing is looked up, translated or registered. A tag takes no
230
+ * category.
231
+ */
232
+ interface TFunction {
233
+ <P extends string>(phrase: P, ...args: TArgs<P>): string;
234
+ <P extends string>(phrase: P, category: string, ...args: TArgs<P>): string;
235
+ (strings: TemplateStringsArray, ...values: unknown[]): string;
236
+ }
237
+
238
+ /**
239
+ * Legacy i18n values to Langsys/ICU source text (spec MIG-4).
240
+ *
241
+ * A migrated app keeps its source-language file and its keys; a key's value
242
+ * becomes the registered phrase after this conversion. The conversion decides
243
+ * the phrase, so it is identity: every JS entry point — the browser `t()`
244
+ * mode, a server `t()`, a vue-i18n or i18next bridge — converts through these
245
+ * functions, never a copy of them.
246
+ *
247
+ * Placeholders convert the same way whatever the file's format. Plurals
248
+ * convert by the format the file declares, because frameworks read the same
249
+ * characters differently: vue-i18n reserves `|`, i18next pairs suffixed keys.
250
+ * The conversion preserves the source framework's own selection. Anything a
251
+ * format does not recognise comes back exactly as written, with `recognised`
252
+ * false and the reason in `issue`, so it registers verbatim and is reported
253
+ * rather than silently mangled.
254
+ */
255
+ /** The formats a file can declare anywhere in the fleet (MIG-7). */
256
+ declare const LEGACY_FORMATS: readonly ["laravel", "vue-i18n", "i18next", "gettext", "rails-i18n", "plain"];
257
+ /** The formats this core reads (MIG-7). Any other configured format is refused at load. */
258
+ declare const SUPPORTED_LEGACY_FORMATS: readonly ["i18next", "vue-i18n", "plain"];
259
+ type LegacyFormat = (typeof SUPPORTED_LEGACY_FORMATS)[number];
260
+ /** A converted value. `text` is the phrase to register; when `recognised` is false it is the value as written. */
261
+ interface LegacyConversion {
262
+ text: string;
263
+ recognised: boolean;
264
+ issue: string | null;
265
+ }
266
+ /** CLDR plural categories, in ICU's customary order. */
267
+ declare const PLURAL_CATEGORIES: readonly ["zero", "one", "two", "few", "many", "other"];
268
+ /**
269
+ * Convert a value under a format.
270
+ *
271
+ * `plain` converts placeholders and nothing else; a `|` in it is text, so such
272
+ * a value is returned verbatim and reported. `vue-i18n` reads any `|` as a
273
+ * plural selected by count. `i18next` has no in-string plural — its plurals are
274
+ * suffix-paired keys, see `convertLegacyPluralForms` — so a `|` is reported.
275
+ */
276
+ declare function convertLegacyValue(value: string, format?: LegacyFormat): LegacyConversion;
277
+ /**
278
+ * One ICU plural from forms keyed by CLDR category — i18next's `key_one` /
279
+ * `key_other` (and its older `key` / `key_plural` pair, as `one` / `other`).
280
+ * i18next selects by locale, so the forms keep their CLDR categories.
281
+ */
282
+ declare function convertLegacyPluralForms(forms: Partial<Record<(typeof PLURAL_CATEGORIES)[number], string>>): LegacyConversion;
283
+ /** A JS entry point that can receive legacy text (MIG-2). */
284
+ type LegacyEntryPoint = 't' | 'vue-i18n' | 'i18next';
285
+ /**
286
+ * Convert the text a call passed when it is not a key — a literal miss
287
+ * (MIG-2) — by the entry point that received it, not by any file: the text is
288
+ * written in the calling framework's syntax.
289
+ *
290
+ * Langsys `t()` converts nothing; its argument is already Langsys syntax. A
291
+ * vue-i18n `$t` or i18next `t` bridge converts under its own format's row.
292
+ */
293
+ declare function convertLegacyCall(text: string, entryPoint: LegacyEntryPoint): LegacyConversion;
294
+
295
+ /**
296
+ * Legacy i18n keys resolved against an app's kept source-language files
297
+ * (spec MIG-2, MIG-5, MIG-7).
298
+ *
299
+ * Pure and fs-free: each file is passed already parsed, as `{ name, format?,
300
+ * namespace?, data }`, so the browser `t()` mode and a server `t()` read one
301
+ * configuration through one resolver and register one phrase per key.
302
+ *
303
+ * - **Formats.** This core reads `i18next`, `vue-i18n` and `plain`, all JSON. A
304
+ * file with no declared format is `plain`. A file declaring any other format,
305
+ * or whose name marks it as another ecosystem's (`.php`, `.yml`, `.po`,
306
+ * `.mo`), is refused when the resolver is built, with an error naming the
307
+ * format and the file.
308
+ * - **Lookup.** A key is read as written first, then by dotted path. The first
309
+ * configured file that holds it answers, and a key held by more than one file
310
+ * is reported by `duplicates()`.
311
+ * - **Category.** A key shaped like a path (`checkout.submit`) contributes its
312
+ * first segment; a file with a `namespace` answers only keys under it
313
+ * (`checkout.submit` in a file with namespace `checkout`) and contributes the
314
+ * namespace. A sentence used as its own key has none.
315
+ * - **Plurals.** In an `i18next` file, suffix-paired keys (`items_one`,
316
+ * `items_other`, or the older `items` / `items_plural`) resolve together as
317
+ * one plural under their base key.
318
+ */
319
+ /** One configured source file. `data` is its parsed JSON object. */
320
+ interface LegacyKeyFile {
321
+ /** The file's path or name, as errors and reports should name it. */
322
+ name: string;
323
+ /** `i18next`, `vue-i18n` or `plain`. Defaults to `plain`. */
324
+ format?: string;
325
+ /** When set, the file answers only keys prefixed `namespace.`, and the namespace is their category. */
326
+ namespace?: string;
327
+ data: unknown;
328
+ }
329
+ /** A key the files hold, converted. */
330
+ interface LegacyKeyHit extends LegacyConversion {
331
+ /** The phrase to register: the converted value, or the value as written when not `recognised`. */
332
+ phrase: string;
333
+ /** The category the key contributes, or null for none (MIG-5). */
334
+ category: string | null;
335
+ key: string;
336
+ /** The file that answered. */
337
+ file: string;
338
+ }
339
+ /** What cannot be migrated as it stands, for a listing command (MIG-4, MIG-7). */
340
+ interface LegacyKeyProblem {
341
+ file: string;
342
+ key: string | null;
343
+ issue: string;
344
+ fix: string;
345
+ }
346
+ interface LegacyKeys {
347
+ /** The key's converted source value, or null when no file holds it (the MIG-2 miss). */
348
+ resolve(key: string): LegacyKeyHit | null;
349
+ /** Keys held by more than one file, with those files in configured order; the first answers. */
350
+ duplicates(): Record<string, string[]>;
351
+ /** Every value that converts to nothing recognised, and every file that is not a JSON object. */
352
+ problems(): LegacyKeyProblem[];
353
+ }
354
+ /** Thrown when the configuration names a file this core cannot read (MIG-7). */
355
+ declare class LegacyFormatError extends Error {
356
+ readonly file: string;
357
+ readonly format: string;
358
+ constructor(message: string, file: string, format: string);
359
+ }
360
+ /**
361
+ * Build a resolver over configured files. Throws `LegacyFormatError` for a file
362
+ * this core does not read — never silently, since a skipped file would turn
363
+ * every one of its keys into a literal phrase named after the key.
364
+ */
365
+ declare function createLegacyKeys(files: LegacyKeyFile[]): LegacyKeys;
366
+
367
+ interface iContentBlock {
368
+ custom_id: string;
369
+ category: string;
370
+ content: string;
371
+ label?: string;
372
+ tokens: string[];
373
+ }
374
+
375
+ /**
376
+ * The IDENTITY CONTRACT — everything that decides a `custom_id`, with no DOM
377
+ * and no module-scope side effects, so it can be imported from a server, a
378
+ * worker or a build script.
379
+ *
380
+ * It lives apart from `content-block.ts` because that module reaches the API
381
+ * client, the discovery lane and the persisted stores; importing it to hash a
382
+ * string pulled a network client and a storage probe along. `langsys-js-server`
383
+ * was sed-extracting these functions out of a published tarball under
384
+ * `@ts-nocheck` for exactly that reason — a copy that silently predates every
385
+ * id rule changed since. Re-exported by `content-block.ts`, so nothing that
386
+ * already imports from there needs to move.
387
+ *
388
+ * Reachable as the `langsys-js-typescript/pure` subpath. Nothing here may touch
389
+ * `window`, `document`, `navigator` or storage, at import time or on any call
390
+ * path — `tests/pure-subpath.test.ts` enforces that under a trapping global.
391
+ */
392
+ /**
393
+ * HTML attributes whose values should be harvested for translation.
394
+ *
395
+ * THE LIST IS NOW IDENTICAL TO `langsys-php`'s, all twenty-seven, in its order.
396
+ *
397
+ * It used to be a subset of fifteen, and this docstring used to argue that the
398
+ * framework-convention attributes (Bootstrap `data-bs-*`, Rails `data-confirm`)
399
+ * were deliberately not mirrored because a JS app renders those strings through
400
+ * its own components. That reasoning is superseded and was left standing beside
401
+ * the list that contradicted it — the twelve below are exactly the attributes it
402
+ * said would never appear here.
403
+ *
404
+ * The reason it was wrong: on an SSR handoff the two SDKs tokenize the SAME
405
+ * markup, so an attribute only one side harvests produces a different token
406
+ * array and therefore a different `custom_id` for identical content. That is not
407
+ * a coverage hole to be argued about, it is a split identity.
408
+ *
409
+ * ORDER IS IDENTITY. `custom_id` hashes the token array, and where an element
410
+ * carries several of these the list order decides the sequence. The same set in
411
+ * a different order agrees on every single-attribute element and diverges on
412
+ * exactly the ones nobody notices, so the twelve are APPENDED — inserting would
413
+ * have re-keyed every block using one of the original fifteen.
414
+ */
415
+ declare const TRANSLATABLE_ATTRIBUTES: string[];
416
+ /**
417
+ * Elements holding code, inert content, or content no implementation can agree
418
+ * on. Measured before this existed: `<style>.plan{color:#fff}</style>`
419
+ * registered `.plan{color:#fff}` as a translatable phrase, and
420
+ * `<script>window.dataLayer.push(1)</script>` registered the statement. Both
421
+ * were then sent for machine translation.
422
+ *
423
+ * `<noscript>` IS HERE ON A REVERSAL, and the reasoning is worth keeping because
424
+ * the obvious answer is the wrong one. An earlier reading excluded it from this
425
+ * list: its text renders to a real visitor whenever scripting is off, so
426
+ * skipping it would leave a visitor-visible sentence permanently untranslated.
427
+ * That premise is true, and it does not survive two questions.
428
+ *
429
+ * - WHO COULD TRANSLATE IT. With scripting off, a browser SDK is not running,
430
+ * so it can translate nothing on that page — including this.
431
+ * - WHAT THE TOKEN ACTUALLY IS. With scripting ENABLED, the spec's default, a
432
+ * parser makes a `noscript` body RAW TEXT rather than markup. Chromium and
433
+ * parse5 both yield the single token `<p>Enable JavaScript</p>`. Registering
434
+ * that sends MARKUP to machine translation, which is this family's own
435
+ * failure mode arriving through the rule meant to prevent it.
436
+ *
437
+ * And the implementations could not be made to agree cheaply. The axis is the
438
+ * parser's scripting flag, not browser-versus-server: Chromium and parse5 agree
439
+ * because parse5 defaults to scripting enabled, while PHP's libxml2 has no such
440
+ * flag and parses the children as elements. `happy-dom` and `jsdom` behave like
441
+ * libxml2, so a lane measuring in a test environment reproduces a
442
+ * browser-versus-server split that does not exist.
443
+ *
444
+ * `<template>` is named as intent and is NOT a vector HERE: its content lives on
445
+ * `HTMLTemplateElement.content`, so a walker over `childNodes` never reaches it
446
+ * and omitting it from this list changes nothing in a DOM or in parse5 — two
447
+ * lanes measured that independently. It is load-bearing elsewhere, which is why
448
+ * it stays named: libxml2 puts `<template>` children in the ordinary tree, so a
449
+ * PHP or Ruby walker that omitted it registers the template's text, and Ruby
450
+ * measured exactly that leak. 8.0.1 states the split rather than calling the
451
+ * exclusion free.
452
+ *
453
+ * `<math>` is notation, not prose. Translating a variable name or an operator
454
+ * corrupts the expression instead of localising it, and before this exclusion
455
+ * `<p>Area <math><mi>x</mi><mo>+</mo><mn>2</mn></math> units</p>` tokenized to
456
+ * `['Area','x','+','2','units']` — the operator and the bare variable registered
457
+ * as translatable phrases and sent for machine translation. Added by spec 8.0.1
458
+ * after v8 shipped without it.
459
+ *
460
+ * `<svg>` IS DELIBERATELY ABSENT and must stay absent. It is the exclusion people
461
+ * expect that is wrong: an `<svg><text>` renders visible words to a reader, so its
462
+ * text is translated like any other. 8.0.1 states that behaviourally because the
463
+ * structural reading — "walk `<svg>` as a block" — regressed the commonest markup
464
+ * there is when PHP implemented it: treating svg as a block dropped the PARENT's
465
+ * direct text, so `<p>Click <svg><path/></svg> to continue</p>` registered nothing
466
+ * and every icon-bearing heading, link and list item lost its words. Adding `svg`
467
+ * here would reproduce that, which is what `tokenizer-convergence` now pins.
468
+ */
469
+ declare const NON_TRANSLATABLE_ELEMENTS: string[];
470
+ /**
471
+ * True when an element opts out of translation entirely — it and its subtree
472
+ * are skipped by the tokenizer.
473
+ *
474
+ * Mirrors `langsys-php`'s `HtmlParser::isTranslationExcluded()` exactly:
475
+ * - `translate="no"` — the HTML standard attribute, matched case-insensitively.
476
+ * - `data-notrans` — PHP's author-facing alias, for hosts whose templating
477
+ * strips unknown bare attributes or where `translate` collides with another
478
+ * tool. Presence is intent; an explicit `"false"`/`"0"` opts out of the
479
+ * opt-out, compared after trimming.
480
+ *
481
+ * Honoring PHP's alias matters for the same reason as the phrase marker: on SSR
482
+ * handoff there is ONE DOM, and content an author marked "do not translate"
483
+ * must not be extracted and registered by whichever SDK happens to walk it.
484
+ *
485
+ * Both this and `isPhraseMarked` trim before comparing. They briefly differed
486
+ * — PHP's phrase check predated the trim its exclusion check gained — and the
487
+ * gap was resolved by asking rather than tidying: mirror exactly, flag the
488
+ * wart, let the owner decide. Divergence-by-tidying is the same failure class
489
+ * as divergence-by-oversight, just better intentioned.
490
+ */
491
+ declare function isTranslationExcluded(element: AttributeReader): boolean;
492
+ /** The element shape the exclusion reads: the DOM's, or a tree view's. */
493
+ interface AttributeReader {
494
+ readonly tagName: string;
495
+ getAttribute(name: string): string | null;
496
+ hasAttribute(name: string): boolean;
497
+ }
498
+ /**
499
+ * THE definition. `phrase.ts` re-exports this rather than restating it — the
500
+ * literal used to appear in both files, so renaming one side left the other
501
+ * silently stale: the tokenizer would stop recognising the marker `Phrase`
502
+ * emits and would re-tokenize a subtree that manages itself.
503
+ */
504
+ declare const PHRASE_MARKER_ATTR = "data-ls-phrase";
505
+ /** PHP's spelling. Recognised so a catalog shared with langsys-php round-trips. */
506
+ declare const PHRASE_MARKER_ATTR_LEGACY = "data-langsys-phrase";
507
+ declare const PHRASE_MARKER_ATTRS: readonly ["data-ls-phrase", "data-langsys-phrase"];
508
+ /**
509
+ * Marker the `Translate` host element carries, holding the resolved
510
+ * `custom_id` of the block rendered inside it.
511
+ *
512
+ * Mirrors how a `Phrase` host carries `PHRASE_MARKER_ATTR`, and exists so a
513
+ * server-rendered page can be read back: the id is otherwise derivable only by
514
+ * re-running the tokenizer over the same subtree, which a reader that only has
515
+ * the HTML cannot do identically.
516
+ */
517
+ declare const CONTENT_BLOCK_MARKER_ATTR = "data-ls-contentblock";
518
+ /** PHP's spelling, accepted on read for the same reason the phrase marker's is. */
519
+ declare const CONTENT_BLOCK_MARKER_ATTR_LEGACY = "data-langsys-contentblock";
520
+ declare const CONTENT_BLOCK_MARKER_ATTRS: readonly ["data-ls-contentblock", "data-langsys-contentblock"];
521
+ /**
522
+ * A producer's statement that text inside this element is ALREADY RESOLVED: rendered by
523
+ * a server SDK in the visitor's language, not authored as source. A reader records no
524
+ * miss for it — neither a registration nor a discovery hint — because the catalog is
525
+ * keyed by source text and this is a translation.
526
+ *
527
+ * Distinct from the identity markers on purpose, and agreed that way with the PHP and
528
+ * Laravel lanes. A MARK stamp answers "which block is this", and a stamped block inside a
529
+ * resolved scope keeps its id and still translates on a later render; this attribute says
530
+ * only "never treated as source". It is also not `translate="no"`, which tells a browser
531
+ * not to machine-translate and would block a legitimate later render.
532
+ *
533
+ * The value is the locale it was resolved into, canonical lowercase, and is informational:
534
+ * presence decides. A bare attribute means the same, because a binding writing it into a
535
+ * layout often does not know the locale at that point. `="false"` and `="0"` opt a subtree
536
+ * back out, the convention `isPhraseMarked` already uses in both SDKs; `="no"` and
537
+ * `="off"` are NOT opt-outs under that shared rule.
538
+ */
539
+ declare const RESOLVED_MARKER_ATTR = "data-ls-resolved";
540
+ /** PHP's spelling, accepted on read for the same reason the other markers' is (MARK-2). */
541
+ declare const RESOLVED_MARKER_ATTR_LEGACY = "data-langsys-resolved";
542
+ declare const RESOLVED_MARKER_ATTRS: readonly ["data-ls-resolved", "data-langsys-resolved"];
543
+ /**
544
+ * Canonical form of one token's text, applied identically to a TEXT NODE and to
545
+ * an ATTRIBUTE VALUE.
546
+ *
547
+ * It used to differ: text nodes collapsed internal whitespace while attributes
548
+ * were only trimmed. So the same authored sentence produced two different ids
549
+ * depending on where it sat —
550
+ *
551
+ * <p>A long\n description</p> -> "A long description"
552
+ * <img alt="A long\n description"> -> "A long\n description"
553
+ *
554
+ * — which also put this SDK and `langsys-php` on different ids for the same
555
+ * markup. One function now, used by both, so "the same content yields the same
556
+ * id" is true by construction rather than by two call sites agreeing.
557
+ *
558
+ * `\s` in JavaScript already covers U+00A0, so a non-breaking space collapses
559
+ * like any other whitespace without special handling. The C0 controls are
560
+ * removed first (`stripC0Controls`), so VT and FF never reach the collapse.
561
+ */
562
+ declare function normalizeTokenText(value: string): string;
563
+ /**
564
+ * Remove the 28 C0 control characters TOK-2 deletes before anything collapses:
565
+ * U+0001–U+0008, U+000B, U+000C and U+000E–U+001F. Applied to every string that becomes an
566
+ * id input or a catalog key — text nodes, translatable attributes, `t()` keys — on register
567
+ * and on lookup alike, and first: strip, then collapse, then trim.
568
+ *
569
+ * Removed rather than mapped to a space, because that is the only treatment every parser can
570
+ * agree with: libxml2 before 2.14 deletes these from DOM text before any SDK code runs, while
571
+ * libxml2 2.14+, parse5 and happy-dom keep them. So VT and FF, which are also in the collapse
572
+ * set, are deleted here before the collapse can see them: `a` VT `b` is `ab`, not `a b`.
573
+ *
574
+ * TAB, LF and CR are not in the set; they reach the collapse and become one space. NUL, U+007F
575
+ * and the C1 range, U+0085 included, are kept.
576
+ */
577
+ declare function stripC0Controls(value: string): string;
578
+ /**
579
+ * Normalize `%name%` markup placeholders to canonical `{name}`.
580
+ *
581
+ * Framework compilers consume bare `{name}` written in markup before the DOM
582
+ * walker ever sees it (Svelte compiles it to an expression; JSX evaluates it),
583
+ * so DOM content accepts `%name%` as a collision-free authoring escape.
584
+ * Normalization runs at every markup capture boundary (content-block
585
+ * tokenizer, `Translate` original-value snapshots, `Phrase` encoding), so the
586
+ * catalog, the wire, and translators only ever see the canonical `{name}`
587
+ * form — and plain `{name}` keeps working for vanilla-HTML authors.
588
+ *
589
+ * Keys must be identifiers (`[A-Za-z_][A-Za-z0-9_]*`), so literal `%` in
590
+ * prose ("20% off", "50% to 60%") can't match. `t()` phrases are JS strings
591
+ * with no compiler collision and stay `{name}`-only.
592
+ *
593
+ * It lives HERE, beside `normalizeTokenText`, because it decides a stored key:
594
+ * every capture boundary runs it, so the same authored content must pass through
595
+ * the same rewrite in every SDK or the key splits. It was in `interpolate.ts`,
596
+ * which is where the RENDER-time counterpart belongs, and where its docstring
597
+ * had drifted onto `adoptPercentPlaceholders` — two functions with one doc
598
+ * between them. `interpolate.ts` re-exports it, so no importer moves.
599
+ */
600
+ declare function normalizeMarkupPlaceholders(text: string): string;
601
+ /**
602
+ * Compute the deterministic content-block id from a `(category, tokens)` pair.
603
+ *
604
+ * JSON-stringifies the tuple so each token is unambiguously delimited
605
+ * (the previous `tokens.join('-')` was collision-prone for tokens
606
+ * containing a hyphen, e.g. `'e-mail'`).
607
+ *
608
+ * Pure function — same inputs always produce the same id. Exported so
609
+ * framework wrappers can compute the id themselves before deciding
610
+ * whether to call `registerContentBlock`.
611
+ */
612
+ declare function generateCustomId(category: string, tokens: string[]): string;
613
+ declare function canonicalContentBlockJson(category: string, tokens: string[]): string;
614
+ /**
615
+ * The id this block would have had before the 0.6.0 MD5 fix.
616
+ *
617
+ * **Lookup only — never register under this.** Blocks registered by an older
618
+ * SDK are keyed by it, so `Translate` falls back to it when the corrected id
619
+ * misses, which keeps existing translations resolving instead of orphaning
620
+ * them. Registering under it would keep minting ids from a hash that both
621
+ * diverges across SDKs and can collide.
622
+ *
623
+ * Note the collision is a property of the FINAL hashed string, not the phrase:
624
+ * `JSON.stringify` shifts every character's offset, so the same phrase pair can
625
+ * collide standalone and not collide here — and changing `category` moves every
626
+ * character into different lanes. Always reason at this level, not at `md5()`.
627
+ *
628
+ * @deprecated Migration aid; will be removed once catalogs have been rebased.
629
+ */
630
+ declare function generateLegacyCustomId(category: string, tokens: string[]): string;
631
+ /**
632
+ * One node of a rich-phrase tree, in the host-neutral shape `encodeRichPhrase`
633
+ * walks.
634
+ *
635
+ * `{ text }` carries a text node's value VERBATIM — uncollapsed, untrimmed,
636
+ * because collapse runs once over the assembled string and not per node (see
637
+ * `encodeRichPhrase`). An element contributes its `children` plus an opaque
638
+ * `payload` handed straight back in `slots`, so a host keeps its own node handle
639
+ * without this module knowing what one is.
640
+ *
641
+ * Anything that is NEITHER text nor element — a comment, a CDATA section, a
642
+ * processing instruction — must contribute NO text and NO slot. The DOM adapter
643
+ * drops them outright.
644
+ *
645
+ * Measured, because the obvious wording here was wrong: mapping one to
646
+ * `{ text: '' }` is HARMLESS (`'a' + '' + 'b'` is still `'ab'`), so an adapter may
647
+ * do that freely. What splits the key is mapping it to its own DATA
648
+ * (`a<!-- note -->b` becoming `'a note b'`) or giving it a slot (`'a{m0o}{m0c}b'`,
649
+ * which also shifts every later slot index). Those two are the mistakes worth
650
+ * naming; "must be omitted" overstates the rule.
651
+ */
652
+ type RichTextNode<T> = {
653
+ readonly text: string;
654
+ }
655
+ /** A marked value (VAR-3): its placeholder, `{NAME}`, never its text. */
656
+ | {
657
+ readonly param: string;
658
+ } | {
659
+ readonly children: readonly RichTextNode<T>[];
660
+ readonly payload: T;
661
+ };
662
+ interface EncodedRichPhrase<T> {
663
+ /** The phrase string: the lookup key, and therefore the identity. */
664
+ phrase: string;
665
+ /** Each element's `payload`, in the order its markup token was assigned. */
666
+ slots: T[];
667
+ }
668
+ /**
669
+ * The `<Phrase>` identity rule, with no DOM in it.
670
+ *
671
+ * `<Phrase>` keys by a STRING, not by a token array and not by a `custom_id`:
672
+ * inline elements become neutral `{mNo}`/`{mNc}` markup-token pairs and the
673
+ * whole thing collapses to one sentence, which is then the catalog key. So this
674
+ * string is as load-bearing as `generateCustomId`'s output, and a second
675
+ * implementation of it re-keys every rich phrase the moment the two drift —
676
+ * silently, as a cache miss and a re-registration rather than an error.
677
+ *
678
+ * That is why it is here and generic over the host's node type: `langsys-js-server`
679
+ * renders `<Phrase>` from parse5 nodes and was otherwise going to write its own
680
+ * encoder. The DOM adapter is `encodeRichText` in `richtext.ts`, which is now
681
+ * nothing but a node-shape mapping over this function.
682
+ *
683
+ * Three details are identity and every adapter has to match them:
684
+ *
685
+ * 1. **Slot indices are assigned in pre-order**, parent before its children,
686
+ * which is why `slots` comes back from here rather than being built by the
687
+ * caller's walk. An adapter that numbered its own slots would have to
688
+ * independently reproduce this order, and a nested phrase is where it would
689
+ * not.
690
+ * 2. **Whitespace collapses ONCE, over the assembled string**, markup tokens
691
+ * included — never per text node. `<p>a <em> b</em></p>` keeps the space
692
+ * before `b` inside the markers; a per-node trim moves it outside them and
693
+ * changes the key. This is the same mistake the attribute path made against
694
+ * the text path, which is what split `custom_id` between this SDK and PHP.
695
+ * 3. **`%name%` normalizes after the collapse**, not before. The two orders
696
+ * happen to agree today (an identifier cannot contain whitespace, so no
697
+ * collapse creates or destroys a match) — stated because "it doesn't matter"
698
+ * is not something a second implementation can verify, while "this order" is.
699
+ *
700
+ * The whitespace collapse is `normalizeTokenText`, deliberately SHARED with the
701
+ * token path so the disputed set (U+FEFF, U+0085, U+180E — see CONFORMANCE) has
702
+ * one definition to change when the spec rules. That sharing is about the
703
+ * whitespace set only: COALESCING adjacent text nodes stays opposite between the
704
+ * two paths, on purpose, and `content-block.ts` says so from its side.
705
+ */
706
+ declare function encodeRichPhrase<T>(nodes: readonly RichTextNode<T>[]): EncodedRichPhrase<T>;
707
+
708
+ /**
709
+ * Value markers (spec VAR-3): how an emitter tells a reader which printed text
710
+ * was a variable, and what it was called, so the reader registers a placeholder
711
+ * instead of one phrase per value (VAR-1).
712
+ *
713
+ * <p>Hello <!--ls:name-->Ana<!--/ls-->, welcome back</p>
714
+ * <p>Hello <span data-ls-param="name">Ana</span>, welcome back</p>
715
+ * [{ text: 'Hello ' }, { comment: 'ls:name' }, { text: 'Ana' }, { comment: '/ls' }, { text: ', welcome back' }]
716
+ *
717
+ * all read as the one token `Hello {name}, welcome back`, with param `name` =
718
+ * `Ana`. The value is text only: an element inside the pair voids the marker,
719
+ * and its comments then split text as any comment does.
720
+ *
721
+ * Within one parent, the text on both sides of a marker, and every marker in
722
+ * between, merge into one run. Everything without a marker splits exactly as
723
+ * before, so no id derived from unmarked markup changes.
724
+ */
725
+ /** The attribute form's attribute, for an emitter that cannot write comments. */
726
+ declare const VAR_PARAM_ATTR = "data-ls-param";
727
+
728
+ /**
729
+ * Placeholder names from source expressions (spec VAR-2). A name matches
730
+ * `[a-z][a-z0-9_]*`, so it is a valid ICU argument, and it is derived the same way
731
+ * by every SDK, because the name is part of the phrase: two SDKs naming the same
732
+ * expression differently register two phrases.
733
+ *
734
+ * Expressions are given as a language-neutral shape, the one the shared naming
735
+ * vectors (`var-naming-vectors.json`) use, so a binding maps its own AST onto it.
736
+ */
737
+ /** An expression that produced a value, as the naming rules see it. */
738
+ type ExpressionShape = {
739
+ identifier: string;
740
+ } | {
741
+ member: string[];
742
+ } | {
743
+ call: {
744
+ callee: string;
745
+ args: ExpressionShape[];
746
+ };
747
+ } | {
748
+ other: 'binary' | 'conditional' | 'template' | 'computed' | 'call-multi' | string;
749
+ };
750
+ /** One value in a phrase: its expression, and the name the developer wrote, if any. */
751
+ interface NamedExpression {
752
+ shape: ExpressionShape;
753
+ /** An explicit name always wins. */
754
+ explicit?: string;
755
+ }
756
+ /** A segment in snake_case, within the grammar, or null when nothing of it survives. */
757
+ declare function snakeCase(segment: string): string | null;
758
+ /**
759
+ * The placeholder name of each value in one phrase, in order (VAR-2):
760
+ *
761
+ * - an explicit name wins;
762
+ * - an identifier is itself in snake_case; a member chain its last segment; a
763
+ * chain ending in `length`, `size` or `count` the segment before plus `_count`;
764
+ * one ending in `value` or `current` the segment before; a call with one
765
+ * argument the argument's name;
766
+ * - the same expression twice is the same placeholder, where the shape identifies
767
+ * it: two shapes containing `other` are two placeholders, however equal;
768
+ * - names that collide are prefixed with their previous segment, then suffixed
769
+ * `_2`, `_3`, and a reserved name (`m0o`) counts as taken;
770
+ * - anything else is `value`, `value_2`, ….
771
+ */
772
+ declare function derivePlaceholderNames(expressions: readonly NamedExpression[]): string[];
773
+
774
+ declare const isEmpty: (data: unknown) => boolean;
775
+ /**
776
+ * MD5 of a string, matching a standard UTF-8 MD5 for all input.
777
+ *
778
+ * Content-block `custom_id`s are derived from this, so it's a wire identity —
779
+ * see `generateCustomId`. Before 0.6.0 this packed UTF-16 code units directly
780
+ * into byte lanes, which agreed with a real MD5 only for ASCII: non-ASCII
781
+ * input both diverged from every other Langsys SDK and could collide (a
782
+ * character's high byte was OR-ed into the neighbouring lane, vanishing
783
+ * whenever that lane already carried those bits — so `éa` and `ǩa` hashed
784
+ * alike). `md5Legacy` preserves the old behavior for migration lookups only.
785
+ */
786
+ declare function md5(inputString: string): string;
787
+ /**
788
+ * The pre-0.6.0 MD5, retained ONLY so existing content blocks stay resolvable:
789
+ * ids minted by an older SDK are keyed by this. Lookup-only — never register
790
+ * new content under it, or the colliding-id population keeps growing.
791
+ *
792
+ * @deprecated Migration aid; will be removed once catalogs have been rebased.
793
+ */
794
+ declare function md5Legacy(inputString: string): string;
795
+
796
+ /**
797
+ * Detects ICU MessageFormat syntax in a translation string.
798
+ *
799
+ * - Matches `{var, plural, …}`, `{var, select, …}`, `{var, selectordinal, …}`,
800
+ * `{var, number, …}`, `{var, date, …}`, `{var, time, …}`.
801
+ * - Also matches the style-less forms `{var, number}`, `{var, date}`,
802
+ * `{var, time}` — valid ICU with the locale-default style.
803
+ * - Doesn't match plain interpolation slots like `{name}` or `{count}`.
804
+ *
805
+ * Exported because the Translation Manager UI needs the same detection logic
806
+ * to decide whether to render a plural-slot editor vs. a single text field.
807
+ */
808
+ declare function isICU(template: string): boolean;
809
+ /**
810
+ * Params keys that have NO matching placeholder in `texts`.
811
+ *
812
+ * A key counts as used when it appears as `{key}`, as the argument of an ICU
813
+ * slot (`{key, plural, …}`), or as `%key%`. Surrounding whitespace is tolerated
814
+ * in the brace forms.
815
+ *
816
+ * BOTH SPELLINGS, because `interpolate` resolves both. It used to accept the
817
+ * brace forms only, on the stated assumption that texts arrive canonical
818
+ * (post-`normalizeMarkupPlaceholders`). That held for the two internal callers
819
+ * and still does — `<Translate>` normalizes its tokens at capture and
820
+ * `<Phrase>`'s string comes out of `encodeRichText` already rewritten — but it
821
+ * stopped being true of the function itself once `interpolate` began adopting
822
+ * `%name%` at render time. A caller passing a raw stored translation then got
823
+ * `name` reported unused for a param that renders perfectly, and
824
+ * `warnUnmatchedParams` told them to "write %name% instead" — which is what they
825
+ * had written. This predicate and `adoptPercentPlaceholders` have to agree about
826
+ * what a placeholder is; they are two halves of one rule.
827
+ *
828
+ * The percent spelling is accepted only for keys that are IDENTIFIERS, matching
829
+ * `adoptPercentPlaceholders` exactly. A key like `a.b` never resolves from
830
+ * `%a.b%` — accepting it here would suppress a warning that is correct.
831
+ *
832
+ * A `<name>_gender` key counts as used wherever `name` is a placeholder. Langsys
833
+ * promotes a plain `{name}` to `{name_gender, select, …}` in gendered target
834
+ * locales, so an app passes the gender beside the value (`GenderParamsFor` types
835
+ * it), and the source text a caller checks never names it. Only a placeholder's
836
+ * companion: `team_gender` with no `{team}` in the text is still reported.
837
+ */
838
+ declare function findUnusedParamKeys(texts: string[], params: Record<string, unknown> | undefined): string[];
839
+ /**
840
+ * Substitute placeholders in a translated string with values from `params`.
841
+ *
842
+ * Two paths:
843
+ * - ICU MessageFormat (target string contains `{var, plural|select|…, …}`):
844
+ * parsed and formatted via `intl-messageformat`, which knows every
845
+ * target locale's plural rules (Arabic's 6 categories, Russian's 4,
846
+ * etc.) and gender-select branches.
847
+ * - Simple `{name}` interpolation: cheap regex replacement. Unknown
848
+ * placeholders are left untouched so missing data is visible to the
849
+ * developer rather than silently rendering empty strings. Number and
850
+ * Date values are formatted per the target locale's CLDR rules
851
+ * (`Intl.NumberFormat` / `Intl.DateTimeFormat`) — pass numbers as
852
+ * strings to opt out (e.g. IDs and codes that must not get grouping
853
+ * separators).
854
+ *
855
+ * Malformed ICU falls through to simple interpolation rather than throwing,
856
+ * so the developer sees a broken-but-visible string instead of a runtime crash.
857
+ * Backend-side validation in langsys-ai is supposed to prevent this — the
858
+ * fall-through is defense in depth.
859
+ *
860
+ * `locale` is required for ICU formatting (drives plural-rule selection).
861
+ * Falls back to `'en'` if absent — simple-interpolation path ignores it.
862
+ *
863
+ * `params` may be omitted, and behaves as an empty map: a select or plural renders
864
+ * its `other` branch (ICU-1). Omitting it used to throw a TypeError inside recovery.
865
+ *
866
+ * `options` routes the two notices to the caller instead of this module's logger,
867
+ * for a host with a logger of its own — a server with a per-instance debug
868
+ * setting, where this module's process-wide logger is the wrong switch. See
869
+ * `InterpolateOptions`.
870
+ */
871
+ declare function interpolate(template: string, params?: Record<string, unknown>, locale?: string, options?: InterpolateOptions): string;
872
+ /**
873
+ * The argument names `interpolate` would default for these params (ICU-4): each
874
+ * `select`, `plural` or plain argument the template uses and `params` does not
875
+ * supply (`null` counts as not supplied), once each, in the order the recovery
876
+ * meets them. Empty for a plain template or one that does not parse.
877
+ *
878
+ * The same walk `interpolate` recovers with, so a host that emits the notice
879
+ * through its own logger names exactly the arguments the render defaulted.
880
+ */
881
+ declare function defaultedArguments(template: string, params?: Record<string, unknown>, locale?: string): string[];
882
+ /**
883
+ * Where `interpolate` reports what it recovered from. Each handler, when given,
884
+ * replaces this module's logger for that notice and is called on EVERY
885
+ * occurrence: gating and deduplication are then the caller's, so a host applies
886
+ * its own debug switch and its own `(template, locale)` dedupe. What the core
887
+ * logger does, and what a handler should reproduce:
888
+ *
889
+ * - `onDefaulted` (ICU-4): a missing argument was recovered with its neutral
890
+ * branch. Debug level only; names every defaulted argument, once each, and the
891
+ * locale.
892
+ * - `onFormatterFailure` (ICU-6): the formatter failed and the SDK's own branch
893
+ * selection rendered the phrase. At every log level, naming the phrase, the
894
+ * locale and the error.
895
+ */
896
+ interface InterpolateOptions {
897
+ onDefaulted?: (names: string[], template: string, locale: string) => void;
898
+ onFormatterFailure?: (error: unknown, template: string, locale: string) => void;
899
+ }
900
+
901
+ /**
902
+ * Normalise a locale identifier to the SDK's single internal form: lowercase
903
+ * `xx-yy` — `en-US` → `en-us`, `zh-Hant-TW` → `zh-hant-tw`. Underscores are
904
+ * accepted and converted (`en_US` → `en-us`). Legacy aliases in the language,
905
+ * script and region are resolved through `Intl.getCanonicalLocales` (`iw` → `he`,
906
+ * `in-ID` → `id-id`, `sh-BA` → `sr-latn-ba`) BEFORE casing is flattened —
907
+ * lowercasing first would lose them, since the alias table is keyed on canonical
908
+ * tags.
909
+ *
910
+ * Variants and extensions are kept as written, lowercased: `en-US-POSIX` is
911
+ * `en-us-posix`. The API's locale codes are ICU locale ids (`en_US_POSIX`), and
912
+ * BCP 47 canonicalisation rewrites some of their variants into a code the API
913
+ * does not hold (`POSIX` becomes the extension `-u-va-posix`).
914
+ *
915
+ * Structurally invalid tags fall back to lowercasing rather than throwing, so a
916
+ * bad config value degrades to a failed catalog lookup instead of a crash — and
917
+ * warns, because that failure is otherwise indistinguishable from a legitimately
918
+ * untranslated locale.
919
+ */
920
+ declare function canonicalizeLocale(locale: string): string;
921
+ /**
922
+ * The `language-script` key of a locale after CLDR likely-subtags expansion
923
+ * (`Intl.Locale.maximize()`): `zh` → `zh-Hans`, `zh-TW` → `zh-Hant`,
924
+ * `sr` → `sr-Cyrl`. Two locales with the same key are mutually intelligible
925
+ * at the script level; two with different keys must not fall back to each
926
+ * other (a `zh-Hant` reader must never receive a `zh-Hans` catalog).
927
+ *
928
+ * Returns `null` when the tag can't be parsed — callers should treat that
929
+ * as "no match" rather than guessing.
930
+ */
931
+ declare function maximizedLangScript(locale: string): string | null;
932
+
933
+ export { createLegacyKeys as $, type AttributeReader as A, type SnapshotCatalog as B, CONTENT_BLOCK_MARKER_ATTR as C, DEFAULT_SERVER_MESSAGE_CATEGORY as D, type EncodedRichPhrase as E, type SnapshotCategory as F, SnapshotError as G, type SnapshotPayload as H, type InterpolateOptions as I, type SnapshotRefusal as J, type TFunction as K, LEGACY_FORMATS as L, TRANSLATABLE_ATTRIBUTES as M, NON_TRANSLATABLE_ELEMENTS as N, type TranslationParams as O, PHRASE_MARKER_ATTR as P, buildSnapshot as Q, RESOLVED_MARKER_ATTR as R, SNAPSHOT_FORMAT as S, type TArgs as T, canonicalContentBlockJson as U, VAR_PARAM_ATTR as V, canonicalSnapshotJson as W, canonicalizeLocale as X, convertLegacyCall as Y, convertLegacyPluralForms as Z, convertLegacyValue as _, CONTENT_BLOCK_MARKER_ATTRS as a, defaultedArguments as a0, derivePlaceholderNames as a1, encodeRichPhrase as a2, fillTemplate as a3, findUnusedParamKeys as a4, generateCustomId as a5, generateLegacyCustomId as a6, type iContentBlock as a7, interpolate as a8, isEmpty as a9, isICU as aa, isTranslationExcluded as ab, maximizedLangScript as ac, md5 as ad, md5Legacy as ae, normalizeMarkupPlaceholders as af, normalizeTokenText as ag, parseSnapshot as ah, resolveServerMessages as ai, snakeCase as aj, snapshotChecksum as ak, stripC0Controls as al, templateMarkers as am, toServerMessage as an, CONTENT_BLOCK_MARKER_ATTR_LEGACY as b, type CatalogSnapshot as c, type ExpressionShape as d, type ExtractParamKeys as e, type LegacyConversion as f, type LegacyEntryPoint as g, type LegacyFormat as h, LegacyFormatError as i, type LegacyKeyFile as j, type LegacyKeyHit as k, type LegacyKeyProblem as l, type LegacyKeys as m, type NamedExpression as n, PHRASE_MARKER_ATTRS as o, PHRASE_MARKER_ATTR_LEGACY as p, type ParamPrimitive as q, type ParamsFor as r, RESOLVED_MARKER_ATTRS as s, RESOLVED_MARKER_ATTR_LEGACY as t, type ResolveServerMessagesOptions as u, type RichTextNode as v, SNAPSHOT_VERSION as w, SUPPORTED_LEGACY_FORMATS as x, type ServerMessage as y, type ServerMessagePieces as z };