langsys-js-typescript 0.6.5 → 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.
package/dist/index.d.mts CHANGED
@@ -1,3 +1,6 @@
1
+ import { q as ParamPrimitive, j as LegacyKeyFile, K as TFunction, c as CatalogSnapshot, y as ServerMessage, a7 as iContentBlock } from './pure-DkFXdgVa.mjs';
2
+ export { C as CONTENT_BLOCK_MARKER_ATTR, a as CONTENT_BLOCK_MARKER_ATTRS, b as CONTENT_BLOCK_MARKER_ATTR_LEGACY, D as DEFAULT_SERVER_MESSAGE_CATEGORY, d as ExpressionShape, e as ExtractParamKeys, I as InterpolateOptions, L as LEGACY_FORMATS, f as LegacyConversion, g as LegacyEntryPoint, h as LegacyFormat, i as LegacyFormatError, k as LegacyKeyHit, l as LegacyKeyProblem, m as LegacyKeys, n as NamedExpression, P as PHRASE_MARKER_ATTR, o as PHRASE_MARKER_ATTRS, p as PHRASE_MARKER_ATTR_LEGACY, r as ParamsFor, R as RESOLVED_MARKER_ATTR, s as RESOLVED_MARKER_ATTRS, t as RESOLVED_MARKER_ATTR_LEGACY, u as ResolveServerMessagesOptions, S as SNAPSHOT_FORMAT, w as SNAPSHOT_VERSION, x as SUPPORTED_LEGACY_FORMATS, z as ServerMessagePieces, B as SnapshotCatalog, F as SnapshotCategory, G as SnapshotError, H as SnapshotPayload, J as SnapshotRefusal, T as TArgs, O as TranslationParams, V as VAR_PARAM_ATTR, Q as buildSnapshot, W as canonicalSnapshotJson, X as canonicalizeLocale, Y as convertLegacyCall, Z as convertLegacyPluralForms, _ as convertLegacyValue, $ as createLegacyKeys, a0 as defaultedArguments, a1 as derivePlaceholderNames, a3 as fillTemplate, a4 as findUnusedParamKeys, a5 as generateCustomId, a6 as generateLegacyCustomId, a8 as interpolate, a9 as isEmpty, aa as isICU, ab as isTranslationExcluded, ad as md5, ae as md5Legacy, af as normalizeMarkupPlaceholders, ah as parseSnapshot, ai as resolveServerMessages, aj as snakeCase, ak as snapshotChecksum, am as templateMarkers, an as toServerMessage } from './pure-DkFXdgVa.mjs';
3
+
1
4
  /**
2
5
  * Tiny framework-agnostic reactive value holder — our replacement for a
3
6
  * Svelte `Writable<T>`. The shape is deliberately similar so the mental
@@ -60,6 +63,289 @@ type iCategories = {
60
63
  __uncategorized__: iTranslations;
61
64
  };
62
65
 
66
+ /**
67
+ * Content blocks without a DOM (spec SRV-1, MARK-1, TOK-6): identify, translate
68
+ * and register a `<Translate>` block from a tree, so a framework rendering on a
69
+ * server, or rendering on the client before mount, emits the translated block
70
+ * and its stamped id instead of source text.
71
+ *
72
+ * A tree is the block's content, the children of the host element, as plain
73
+ * nodes. It is walked by the same code as the DOM (`tokenizeNodes`,
74
+ * `findSingleTextNode`, `isExcisedFromUnit`) through a DOM-shaped view of it, so
75
+ * a tree and the DOM it mirrors yield the same tokens, shape and id.
76
+ *
77
+ * `renderBlock` is synchronous and pure over the active catalog: it registers
78
+ * nothing and its output is the same on a server and in the browser for the
79
+ * same catalog, so a client's first render matches what the server sent. A
80
+ * binding registers with `registerBlock`, which touches no DOM.
81
+ */
82
+ /**
83
+ * A node of a block's content. Attribute values are strings; `true` is an attribute with no value.
84
+ *
85
+ * `params` on a nested host's element (a phrase host or a content-block declaration) are
86
+ * that unit's own: it renders with them in place of the block's, so a binding names each
87
+ * unit's values on their own (VAR-2) and a nested phrase keys as it does standing alone.
88
+ * A nested host without them renders with the block's.
89
+ */
90
+ type BlockNode = {
91
+ text: string;
92
+ } | {
93
+ comment: string;
94
+ } | {
95
+ tag: string;
96
+ attrs?: Readonly<Record<string, string | true>>;
97
+ children?: readonly BlockNode[];
98
+ params?: Readonly<Record<string, ParamPrimitive>>;
99
+ };
100
+ /**
101
+ * A node of a rendered block. Every element names, as `source`, the pre-order
102
+ * index of the input element it renders (elements only, counted across the
103
+ * whole input from 0), so a binding can re-attach what the tree cannot carry —
104
+ * handlers, refs, keys — even where a translation reorders inline markup.
105
+ */
106
+ type RenderedNode = {
107
+ text: string;
108
+ } | {
109
+ comment: string;
110
+ } | {
111
+ tag: string;
112
+ attrs: Record<string, string | true>;
113
+ children: RenderedNode[];
114
+ source: number;
115
+ };
116
+ type BlockShape = 'phrase' | 'block' | 'empty';
117
+ interface BlockOptions {
118
+ category?: string;
119
+ params?: Record<string, ParamPrimitive>;
120
+ /** A label for the block in the Translation Manager, as `Translate`'s option. */
121
+ label?: string;
122
+ /**
123
+ * The app's own `custom_id` for the block: rendered under it, and registered
124
+ * under it with its content when the catalog does not hold it, as
125
+ * `Translate`'s `custom_id` option does.
126
+ */
127
+ id?: string;
128
+ /**
129
+ * A `custom_id` to ADOPT: a renderer already rendered this block from the
130
+ * catalog under it (a stamp carried with the resolved marker, MARK-3). Rendered
131
+ * under it, never registered.
132
+ */
133
+ customId?: string;
134
+ /**
135
+ * `registerBlock` only: the host element on the client, whose ancestors decide
136
+ * GATE-10. The walk starts at its parent, since the host itself carries the
137
+ * resolved marker when it was rendered from the catalog.
138
+ */
139
+ host?: Element;
140
+ /** `false` registers nothing, nested hosts included (VAR-7); `renderBlock` ignores it. Default `true`. */
141
+ register?: boolean;
142
+ }
143
+ interface RenderedBlock {
144
+ shape: BlockShape;
145
+ /** The id the block renders under: derived, a historical id the catalog holds (CID-3), or adopted. Null when empty. */
146
+ customId: string | null;
147
+ nodes: RenderedNode[];
148
+ /**
149
+ * The attributes to set on the host: the block's id (MARK-1), and, when the
150
+ * rendered text is a translation (a locale other than the base), the resolved
151
+ * marker with that locale (GATE-10), so a DOM reader on the client never files
152
+ * the served translation as source. Empty for an empty block.
153
+ */
154
+ hostAttrs: Record<string, string>;
155
+ }
156
+ /**
157
+ * A block as a request scope's seed carries it (SRV-4), for a client that cannot
158
+ * recover the block's source from its DOM: `registerBlock` accepts it in place
159
+ * of the source nodes.
160
+ */
161
+ interface SeededBlock {
162
+ customId: string;
163
+ category: string;
164
+ tokens: string[];
165
+ shape: 'phrase' | 'block';
166
+ /**
167
+ * The block's content as `registerBlock` over its nodes would send it, for a
168
+ * client that registers it from the seed. Absent wherever no client does: a
169
+ * phrase-shaped entry, a block the scope's catalog holds, a `collected` one and
170
+ * one marked `register: false`. A seeded block without it registers nothing.
171
+ */
172
+ content?: string;
173
+ /**
174
+ * The server's scope sends this block itself at `close()` (the `server`
175
+ * strategy, or `auto` for a short list), so a client never registers it:
176
+ * not from the seed, and not from the `Translate` over its host.
177
+ */
178
+ collected?: boolean;
179
+ /**
180
+ * `false` when a registration over the block's nodes sends nothing, so
181
+ * `registerBlock(seededBlock)` never registers it: a block rendered with
182
+ * `register: false` (VAR-7), at any depth, whose tokens hold values from
183
+ * variables, carried only so a client can re-render the served text; one under
184
+ * an adopted id; one inside a resolved scope, and the blocks nested in either;
185
+ * and a unit of value markers only.
186
+ */
187
+ register?: false;
188
+ }
189
+ /**
190
+ * A phrase a request scope's render missed, as its seed carries it: `t()`, a
191
+ * `<Phrase>` host or a single-token block. `collected` as on `SeededBlock`: the
192
+ * scope sends it itself at `close()`, and the client never records it again.
193
+ */
194
+ interface SeededPhrase {
195
+ category: string;
196
+ phrase: string;
197
+ collected?: boolean;
198
+ }
199
+ /** A request scope's hydration seed (SRV-4): `LangsysApp.seedCatalog(seed.catalog, seed.locale, seed)` on the client. */
200
+ interface RequestSeed {
201
+ locale: string;
202
+ catalog: iCategories;
203
+ blocks: Record<string, SeededBlock>;
204
+ phrases: SeededPhrase[];
205
+ }
206
+ /** The tokens and shape of a block's content, exactly as `tokenizeElement` and `Translate` decide them for the same markup. */
207
+ declare function tokenizeTree(nodes: readonly BlockNode[]): {
208
+ tokens: string[];
209
+ shape: BlockShape;
210
+ };
211
+ /**
212
+ * Render a block from its content: the translated tree, the id it renders
213
+ * under and the marker to stamp. Synchronous, registers nothing, and reads only
214
+ * the active catalog (a request scope's, or the page's), so the same input and
215
+ * catalog render the same bytes on a server and in the browser.
216
+ *
217
+ * Nested marked hosts are units of their own (MARK-4), rendered in place: a
218
+ * phrase host as its rich phrase, a declared block as its own block (stamped
219
+ * with its id), a stamped one under its id. `translate="no"`, `data-notrans`
220
+ * and code elements are returned as given.
221
+ */
222
+ declare function renderBlock(nodes: readonly BlockNode[], options?: BlockOptions): RenderedBlock;
223
+ /** The notices already given, one per reason. */
224
+ /**
225
+ * Report, as a debug notice once per process per reason, that a binding could
226
+ * not render a block through `renderBlock`. What happens next depends on the
227
+ * reason, and the notice says which:
228
+ *
229
+ * - a framework component in its subtree, a raw-HTML prop, a raw-text element,
230
+ * a suspense placeholder: SRV-1's one sanctioned fallback, not a fault. The
231
+ * block is served as source with no resolved marker and its app-supplied id
232
+ * stamped where it has one, and the client renders and registers it after
233
+ * hydration.
234
+ * - `variable`: the block holds a value from a variable the binding cannot name
235
+ * without its build-time transform, so it renders from the catalog and
236
+ * registers nothing, on the server or the client (VAR-7).
237
+ *
238
+ * The notice is the core's (bindings write no console output of their own). One
239
+ * raised before `init()` is held until debug is known (see `debugNotice`).
240
+ */
241
+ declare function warnUnrenderedBlock(reason: string): void;
242
+ /**
243
+ * Register a block for discovery, with no DOM: what `Translate` registers when
244
+ * it mounts, for a framework that owns the host's text nodes and renders
245
+ * through `renderBlock`. A single-token unit records its phrase miss as `t()`
246
+ * does; a block the catalog lacks, under no historical id, registers with its
247
+ * content; an adopted id (`customId`) registers nothing; nested phrase hosts and
248
+ * declared or unresolved-stamped blocks register as their own units. Inside a
249
+ * request scope it is held for `close()`. Fire-and-forget, and never throws
250
+ * synchronously: the returned promise settles once the registration has run.
251
+ *
252
+ * On a server outside every request scope it resolves at once and registers
253
+ * nothing: there is no scope to hold the block for `close()`, and no `init()` to
254
+ * wait for when the client is the one that initialises, so waiting would keep the
255
+ * tree alive for the life of the process. A debug notice says so, once.
256
+ *
257
+ * Under `requireCredentials` an unconfigured client fails loudly here as on the
258
+ * DOM classes (WIRE-4): on the page the promise rejects with
259
+ * `LangsysConfigurationError`, checked again once `init()` has answered, so a
260
+ * block registered before `init()` refuses rejects too. A `register: false` block
261
+ * is a read and never rejects. A binding that leaves the
262
+ * promise unhandled surfaces it. On a server it never rejects, since an unhandled
263
+ * rejection would end a Node process rather than one request, and the request's
264
+ * `t()` throws already.
265
+ *
266
+ * With `host`, the host is the binding's from this call on: an enclosing
267
+ * `Translate` leaves it alone rather than taking it over as an unmanaged marked host.
268
+ *
269
+ * One-shot: it records what the block is now. A binding calls it again on each
270
+ * change `t` goes through — a locale switch, a new catalog, a navigation — as
271
+ * `Translate` re-enters its lookup (HINT-13).
272
+ *
273
+ * The flows it serves: on a server the outermost host renders and registers,
274
+ * and nested declared hosts are covered through its tree. On the client after
275
+ * hydration, the blocks a scope rendered are registered from its seed, one
276
+ * `registerBlock(seededBlock)` per `seed().blocks` entry after `init()`, since
277
+ * the DOM already holds translated text. That loop is for bindings that mount
278
+ * the DOM classes; a binding that renders trees registers each block from its
279
+ * nodes as it renders. Either way a block or phrase the seed marks `collected`
280
+ * is never registered, since the server's scope sends it itself, and neither is a
281
+ * block the seed marks `register: false`, whose tokens hold per-user values.
282
+ */
283
+ declare function registerBlock(input: readonly BlockNode[] | SeededBlock, options?: BlockOptions): Promise<void>;
284
+ /**
285
+ * A host element's content as a tree: its child nodes as `BlockNode`s, a
286
+ * `<template>`'s content included. For a binding that holds a DOM, on a server
287
+ * or on the client, so no binding walks a DOM of its own to tokenize.
288
+ */
289
+ declare function blockNodesOf(element: {
290
+ childNodes: ArrayLike<Node>;
291
+ }): BlockNode[];
292
+ /**
293
+ * Write a rendered block into the host's EXISTING nodes: each text node's value,
294
+ * each translatable attribute and each nested host's unit markers (its id and
295
+ * resolved marker, MARK-1, GATE-10), in place, never replacing a node, so a
296
+ * framework's references to them stay live; and, given a whole `RenderedBlock`,
297
+ * its `hostAttrs` on the host. Only when the DOM has the rendered tree's
298
+ * structure: the same nodes in the same places, each element where its `source`
299
+ * says. Otherwise nothing is touched and it returns
300
+ * `{ applied: false, reason: 'structure' }`: a translation that reorders inline
301
+ * markup cannot be applied to nodes a framework owns.
302
+ *
303
+ * With `self`, `element` is the tree's root rather than its parent: the tree was
304
+ * rendered from `[{ tag, attrs, children: blockNodesOf(element) }]`, as a
305
+ * standalone phrase host is (`data-ls-phrase` on the root, at `source` 0), and
306
+ * `element` is matched against its one root node.
307
+ */
308
+ declare function applyRendered(element: Element, rendered: RenderedBlock | readonly RenderedNode[], options?: {
309
+ self?: boolean;
310
+ }): {
311
+ applied: boolean;
312
+ reason?: 'structure';
313
+ };
314
+ /** Serialise a block's content as HTML, for the content snapshot a block registers with. Not identity. */
315
+ declare function serializeTree(nodes: readonly BlockNode[]): string;
316
+
317
+ interface HttpResponse {
318
+ status: number;
319
+ statusText: string;
320
+ url: string;
321
+ data: string;
322
+ }
323
+ interface ResponseObject {
324
+ status: boolean;
325
+ page?: number;
326
+ page_count?: number;
327
+ records_per_page?: number;
328
+ data?: object[] | object;
329
+ errors?: string[];
330
+ http?: HttpResponse;
331
+ /**
332
+ * Server-computed write capability for this session, returned by
333
+ * `authorize-project` and by every catalog fetch. Envelope-level on
334
+ * `/translations`: the `data` member there IS the category map, so a stray
335
+ * non-category key inside it would be read as a category.
336
+ */
337
+ write_enabled?: boolean;
338
+ /**
339
+ * Project setting: record a miss only while the loaded locale is the base locale.
340
+ * A top-level sibling of `data` on both catalog routes, and inside `data` on
341
+ * `authorize-project`. Absent means off.
342
+ */
343
+ discovery_base_locale_only?: boolean;
344
+ /** Catalog size counters returned alongside `data` on `/translations`. */
345
+ words?: number;
346
+ untranslated_words?: number;
347
+ }
348
+
63
349
  /**
64
350
  * Minimum interface for a user-locale source.
65
351
  *
@@ -80,6 +366,33 @@ interface LocaleSource {
80
366
  /** Register a callback for locale changes. Returns an unsubscribe function. */
81
367
  subscribe(run: (locale: string) => void): () => void;
82
368
  }
369
+ /**
370
+ * A short-lived write grant (HS256 JWT) minted by the host app's own backend,
371
+ * sent as `X-Write-Grant`. A valid grant makes the session write-enabled
372
+ * server-side, so `write_enabled` comes back true and phrase discovery
373
+ * registers normally — used by login-walled apps our discovery renderer
374
+ * cannot reach.
375
+ *
376
+ * If the token only exists after login, still configure a PROVIDER at `init`
377
+ * that returns `null` until then, rather than leaving `writeGrant` unset and
378
+ * calling `setWriteGrant()` later. An unset grant tells the SDK that no grant
379
+ * can ever arrive, so when authorization comes back read-only it releases the
380
+ * queued misses — which for a login-walled app means they are only reported to
381
+ * a renderer that cannot log in, which is the situation grants exist to solve.
382
+ * A provider returning `null` keeps the queue held until the grant appears.
383
+ *
384
+ * Prefer the FUNCTION form. Grants have a ~5 minute TTL while `init()` runs
385
+ * once and an app session can last hours, so a bare string is expired minutes
386
+ * in and every later write silently degrades the session to read-only. The
387
+ * provider is called immediately before each request, so the host app refreshes
388
+ * by simply returning a fresh token. `null`/`undefined` means "no grant yet"
389
+ * (e.g. before login) and is sent as no header at all.
390
+ *
391
+ * The resolved token is never cached — not on the config singleton, not on the
392
+ * API client — which is what keeps it safe in a long-lived SSR process shared
393
+ * across users.
394
+ */
395
+ type WriteGrant = string | (() => string | null | undefined | Promise<string | null | undefined>);
83
396
  interface iLangsysInitConfig {
84
397
  /** The ID (UUID) of the project created in Langsys */
85
398
  projectid: string;
@@ -96,6 +409,31 @@ interface iLangsysInitConfig {
96
409
  * @default 'en'
97
410
  */
98
411
  baseLocale?: string;
412
+ /**
413
+ * Point the SDK at a different API host — a local instance, a staging
414
+ * host, or a stateful test double.
415
+ *
416
+ * Prefer this over `LangsysAppAPI.setBaseUrl()`. The setter has to run
417
+ * BEFORE `init()`, and running it after leaves the SDK permanently inert:
418
+ * `init()` has already authorized against the default host and failed, and
419
+ * the locale subscription is installed closed over that failed result, so
420
+ * it no-ops for the life of the app. Nothing throws, and no later locale
421
+ * change or URL fix recovers it. This option is applied inside `init()`
422
+ * before authorization, in the same call that consumes it, so there is no
423
+ * ordering left to get wrong.
424
+ *
425
+ * @default 'https://api.langsys.dev/api'
426
+ */
427
+ apiUrl?: string;
428
+ /**
429
+ * Fail loudly when credentials are missing (WIRE-4). Off by default: `init()` without a
430
+ * project id, API key or locale store leaves the client unconfigured, and `t()` renders
431
+ * source text with its params filled. On, `t()` throws `LangsysConfigurationError`
432
+ * instead, so a build or test run with missing credentials stops at the first render.
433
+ *
434
+ * @default false
435
+ */
436
+ requireCredentials?: boolean;
99
437
  /**
100
438
  * Enable debug console messages.
101
439
  * @default false
@@ -111,6 +449,21 @@ interface iLangsysInitConfig {
111
449
  * @default 'client'
112
450
  */
113
451
  ssrTokenStrategy?: 'client' | 'server' | 'auto';
452
+ /**
453
+ * Turn on the legacy-key mode (spec MIG): the app's kept source-language
454
+ * files, each parsed, as `{ name, format?, namespace?, data }`. `t()` then
455
+ * resolves its argument as a key first; a key's value is the phrase, never
456
+ * the key. Formats are `i18next`, `vue-i18n` and `plain` (the default); any
457
+ * other makes `init` throw, naming the file. Unset, `t()` does no key lookup.
458
+ */
459
+ legacyKeys?: LegacyKeyFile[];
460
+ /**
461
+ * The category server messages are registered and rendered under (spec
462
+ * MSG-6). It must be the same category the server registers templates
463
+ * under, or every lookup misses and falls back to the entry's `message`.
464
+ * @default 'Errors'
465
+ */
466
+ messagesCategory?: string;
114
467
  /**
115
468
  * Pre-fetched translation data to bypass the initial API call.
116
469
  * Useful for SSR scenarios to avoid duplicate fetches.
@@ -118,6 +471,17 @@ interface iLangsysInitConfig {
118
471
  initialTranslations?: iCategories;
119
472
  /** Locale that `initialTranslations` corresponds to. */
120
473
  initialTranslationsLocale?: string;
474
+ /**
475
+ * Short-lived write grant for login-walled apps. See `WriteGrant` — prefer
476
+ * the function form; the bare string is quickstart-only. Can also be
477
+ * supplied after `init()` via `LangsysApp.setWriteGrant()`.
478
+ *
479
+ * Note: configuring a grant makes write capability per-user, so a
480
+ * process-level SSR write decision would be unsound. `ssrTokenStrategy`
481
+ * therefore degrades to `'client'` while a grant is configured (logged at
482
+ * init when `debug` is on).
483
+ */
484
+ writeGrant?: WriteGrant;
121
485
  }
122
486
  interface iLangsysConfig {
123
487
  projectid: string;
@@ -125,8 +489,22 @@ interface iLangsysConfig {
125
489
  sUserLocale: LocaleSource;
126
490
  baseLocale: string;
127
491
  debug?: boolean;
128
- /** Permission level of the API key, set automatically after successful authorization. */
129
- key_type?: 'read' | 'write';
492
+ /**
493
+ * Permission level of the API key, set automatically after successful
494
+ * authorization. Display/debug only — NEVER branch on this.
495
+ *
496
+ * A public key shipped in browser JS is extractable, so write capability
497
+ * is decided per-session by the server, not by the key alone: `ip_write`
498
+ * keys are read-only everywhere except from allow-listed IPs. Use the
499
+ * server-computed `writeEnabled` signal for every write decision. The
500
+ * `(string & {})` arm keeps unknown future key types from breaking
501
+ * downstream typechecks.
502
+ */
503
+ key_type?: 'read' | 'write' | 'ip_write' | (string & {});
504
+ /** Short-lived write grant sent as `X-Write-Grant`. See `WriteGrant`. */
505
+ writeGrant?: WriteGrant;
506
+ /** The category server messages render under (MSG-6). */
507
+ messagesCategory?: string;
130
508
  /**
131
509
  * Token creation behavior during SSR.
132
510
  * - 'client': queue tokens for client-side sending (default)
@@ -138,71 +516,156 @@ interface iLangsysConfig {
138
516
  }
139
517
 
140
518
  /**
141
- * Translation function type machinery.
142
- *
143
- * The big trick here is `ExtractParamKeys` — a recursive template-literal
144
- * type that pulls placeholder names out of a phrase at compile time, so
145
- * `t('Hello, {name}!', { name: 'Sarah' })` is type-checked: passing
146
- * an object without `name`, or with a key not in the phrase, is a TS error.
147
- */
148
- /** Allowed value types for interpolation. Mirrors what ICU MessageFormat will accept. */
149
- type ParamPrimitive = string | number | Date | boolean;
150
- /**
151
- * Pull placeholder names out of a phrase.
152
- * @example
153
- * ExtractParamKeys<"Hello, {name}!"> // "name"
154
- * ExtractParamKeys<"{first} and {last}"> // "first" | "last"
155
- * ExtractParamKeys<"no placeholders here"> // never
156
- */
157
- type ExtractParamKeys<S extends string> = S extends `${string}{${infer K}}${infer Rest}` ? K | ExtractParamKeys<Rest> : never;
158
- /** Build the params object type required for a given phrase. */
159
- type ParamsFor<S extends string> = {
160
- [K in ExtractParamKeys<S>]: ParamPrimitive;
161
- };
162
- /**
163
- * Rest-tuple for the optional params argument.
164
- * Empty when the phrase has no placeholders → 3rd arg is forbidden.
165
- * Required tuple when the phrase has placeholders → 3rd arg must be passed.
519
+ * The client has no credentials (WIRE-4): `init()` was refused for a missing project id,
520
+ * API key or locale store, or was never called. An explicit write throws this rather than
521
+ * sending, and so does `t()` under `requireCredentials`. A read renders source text instead,
522
+ * and a call whose result reports a skipped write resolves skipped with reason
523
+ * `not-configured`, since a component reaches it and a component path never throws.
166
524
  */
167
- type TArgs<P extends string> = [ExtractParamKeys<P>] extends [never] ? [] : [params: ParamsFor<P>];
168
- /** Loose params type — used inside generic stores where the phrase isn't a known literal. */
169
- type TranslationParams = Record<string, ParamPrimitive>;
525
+ declare class LangsysConfigurationError extends Error {
526
+ constructor(message: string);
527
+ }
170
528
  /**
171
- * The translation function signature.
172
- *
173
- * t('Save') // no category, no params
174
- * t('Save', 'UI') // categorized
175
- * t('Hello, {name}!', { name: 'X' }) // no category, with params
176
- * t('Hello, {name}!', 'Greetings', { name: 'X' }) // category + params
177
- *
178
- * Two overloads discriminate on the type at position 2 (string → category,
179
- * object → params). `category` is required for the second overload —
180
- * pass any non-empty string. There is no "null category" wire concept:
181
- * absent category means absent.
529
+ * What a request sends under (SRV-2): a project id and API key, with the write grant that
530
+ * goes with them. The module-wide ones are what `init()` set; a request scope may carry
531
+ * its own.
182
532
  */
183
- interface TFunction {
184
- <P extends string>(phrase: P, ...args: TArgs<P>): string;
185
- <P extends string>(phrase: P, category: string, ...args: TArgs<P>): string;
533
+ interface ApiCredentials {
534
+ readonly projectid: string;
535
+ readonly key: string;
536
+ readonly writeGrant?: WriteGrant;
537
+ }
538
+ interface SendOptions {
539
+ /**
540
+ * Let the request outlive the page that started it (`fetch` keepalive).
541
+ * Used for the teardown flush, where the document is being torn down and a
542
+ * normal request would be cancelled with it.
543
+ *
544
+ * NOT `navigator.sendBeacon`, which is the usual answer to this problem but
545
+ * cannot carry custom headers — our authorization is `x-Authorization`, so
546
+ * a beacon would be rejected. `keepalive` supports headers and survives
547
+ * teardown; the trade is a ~64KB body cap across in-flight keepalive
548
+ * requests, which is why the teardown path chunks conservatively.
549
+ */
550
+ keepalive?: boolean;
551
+ /**
552
+ * Send under these credentials. Without them a request sends under those of the
553
+ * request scope the calling code runs in, and outside every scope under the
554
+ * module-wide ones (SRV-2). The page's own lanes pass the module-wide ones, so a page
555
+ * request that starts inside a scope's async context still goes out under the page's key.
556
+ */
557
+ credentials?: ApiCredentials;
558
+ }
559
+ declare class LangsysAppAPIClass {
560
+ private apiurl;
561
+ config: iLangsysConfig;
562
+ constructor();
563
+ setup(config: iLangsysConfig): void;
564
+ /** Forget the credentials: `init()` was refused for missing ones, so nothing may send under old ones. */
565
+ clearCredentials(): void;
566
+ /** True when the module-wide credentials carry a write grant (in either form). */
567
+ hasWriteGrant(): boolean;
568
+ /** The module-wide credentials `init()` set: the page's, and a scope's that was given none. */
569
+ moduleCredentials(): ApiCredentials;
570
+ /** What a request made now sends under, unless it names its own (SRV-2): the active scope's, else the module-wide ones. */
571
+ currentCredentials(): ApiCredentials;
572
+ /**
573
+ * Resolve the write grant a request sends with, calling the consumer's provider
574
+ * if that is the configured form.
575
+ *
576
+ * The result is deliberately NOT cached — not here, not on the config
577
+ * singleton. Grants are ~5 minute JWTs and per-user, while this client is
578
+ * a process-wide singleton; in a long-lived SSR server one cached token
579
+ * would be attached to every later user's requests. Resolving per request
580
+ * is also what lets the host app refresh by simply returning a new token.
581
+ */
582
+ private resolveWriteGrant;
583
+ /** Build request headers fresh per call. Never memoized — see `resolveWriteGrant`. */
584
+ private buildHeaders;
585
+ /** Optional hook to point the client at a different API host. */
586
+ setBaseUrl(url: string): void;
587
+ validate(config: iLangsysConfig): Promise<ResponseObject>;
588
+ getTranslations(locale: string, opts?: SendOptions): Promise<ResponseObject>;
589
+ /**
590
+ * Register phrases and/or content blocks via the current unified endpoint.
591
+ * Each item carries `type: 'phrase' | 'content_block'`. Replaces the
592
+ * deprecated `projects/{id}/tokens` and `projects/{id}/content-blocks` routes.
593
+ */
594
+ createTranslatableItems(items: Array<Record<string, unknown>>, opts?: SendOptions): Promise<ResponseObject>;
595
+ /**
596
+ * Report that a page appears to contain unregistered content, so the
597
+ * discovery renderer can visit it from an allow-listed IP and register
598
+ * what it finds.
599
+ *
600
+ * URL only — never a phrase payload. This is the read-only lane: anonymous
601
+ * callers can say "look here", never "store this". Answers 204 whether or
602
+ * not the hint is acted on, deliberately so it can't be used as an oracle
603
+ * for key validity or registration state. No `project_id`: the key already
604
+ * resolves to exactly one project server-side.
605
+ */
606
+ postDiscoveryHint(pageUrl: string, opts?: SendOptions): Promise<ResponseObject>;
607
+ post(path: string, data?: Record<string, unknown>, opts?: SendOptions): Promise<ResponseObject>;
608
+ get(path: string, data?: Record<string, unknown>, opts?: SendOptions): Promise<ResponseObject>;
609
+ delete(path: string, data?: Record<string, unknown>, opts?: SendOptions): Promise<ResponseObject>;
610
+ patch(path: string, data?: Record<string, unknown>, opts?: SendOptions): Promise<ResponseObject>;
611
+ put(path: string, data?: Record<string, unknown>, opts?: SendOptions): Promise<ResponseObject>;
612
+ private send;
186
613
  }
614
+ declare const LangsysAppAPI: LangsysAppAPIClass;
187
615
 
188
- /**
189
- * Lifecycle for app translations:
190
- * - on-miss: any `t(cat, phrase)` whose phrase isn't in the catalog is
191
- * queued as a "missing token" and (with write keys) flushed to the API.
192
- * - locale changes trigger `change()` which fetches and replaces.
193
- * - consumers can `subscribe(fn)` to react to translation snapshots.
194
- * - frameworks subscribe to `tSignal` (which re-emits a fresh `TFunction`
195
- * every time translations or the loaded locale change) so reactive
196
- * templates re-render automatically.
197
- */
616
+ /** Where `t()` reads and records: the page's state, or a request scope's (SRV-7). */
617
+ interface CatalogView {
618
+ catalog(): iCategories;
619
+ locale(): string;
620
+ miss(category: string, key: string, onlyForRegistration: boolean): void;
621
+ fromSnapshot(): boolean;
622
+ }
198
623
  declare class Translations {
199
624
  private config;
200
625
  private locale;
201
626
  private lastLoaded;
202
627
  private missingTokens;
628
+ /** Phrases already reported under REG-11, so the warning fires once each. */
629
+ private warnedEllipsis;
630
+ /** The legacy-key mode (MIG-1): null unless configured, and then `t()` does no key lookup at all. */
631
+ private legacyKeys;
632
+ private warnedLegacy;
203
633
  private timer;
634
+ /**
635
+ * Catalog fetches still in flight. While any is, queued tokens are held: the flush
636
+ * deduplicates against the catalog, and the catalog being fetched is not there yet.
637
+ */
638
+ private catalogFetchesInFlight;
639
+ /**
640
+ * The published catalog is a snapshot, not the live catalog (SNAP-2). Lookups
641
+ * read it, but registration is decided only against the live catalog
642
+ * (REG-13), so until one arrives every phrase is queued and the queue is held.
643
+ */
644
+ private catalogFromSnapshot;
645
+ /**
646
+ * CACHE-2: a failed catalog fetch, remembered per project and locale. Until `retryAt` a
647
+ * lookup for that pair renders source text and nothing is fetched. The window follows
648
+ * REG-8's clock on the read side: 3s, doubling on each consecutive failure to 5 minutes,
649
+ * cleared by the first success. Held on this instance, so it lasts the page session in a
650
+ * browser and the life of the server object on a server, and is never written anywhere.
651
+ */
652
+ private catalogFailures;
653
+ /** CACHE-2: a catalog request in flight per project and locale, shared by concurrent fetches. */
654
+ private catalogRequests;
204
655
  private flushScheduled;
656
+ private updateInFlight;
657
+ private debounceTimer;
658
+ private consecutiveFailures;
659
+ /** Epoch ms before which no send may be attempted. See RETRY_BACKOFF_*. */
660
+ private retryNotBefore;
661
+ private teardownInstalled;
662
+ /** Unregisters this instance from an injected teardown signal (`setTeardownSignal`). */
663
+ private releaseTeardownFlush;
205
664
  private isFirstClientRun;
665
+ /** Process-level write decision for the SSR lane. See `canWrite`. */
666
+ private ssrWriteEnabled;
667
+ /** Thrown by `t()` when `init()` was refused for missing credentials under `requireCredentials`. */
668
+ private credentialsError;
206
669
  private readyResolve;
207
670
  private readyPromise;
208
671
  debug: Logger;
@@ -217,6 +680,54 @@ declare class Translations {
217
680
  * to avoid acting on an empty cache during the cold-start race.
218
681
  */
219
682
  ready(): Promise<void>;
683
+ /**
684
+ * Resolve `ready()` without a catalog, for the paths where none is coming:
685
+ * `init` refused the config, or authorization failed.
686
+ *
687
+ * `Phrase` and `Translate` both await this gate before rendering, so
688
+ * leaving it pending on those paths is not a degraded mode — it is a
689
+ * permanent blank render, with no error, on the commonest misconfigurations
690
+ * there are. Settling makes them fall back to their source text, which is
691
+ * what an SDK that cannot reach its backend should show.
692
+ *
693
+ * Idempotent. A later successful load still updates the stores and
694
+ * re-renders every subscriber.
695
+ */
696
+ settle(): void;
697
+ /**
698
+ * WIRE-4: `init()` was refused for missing credentials. Reads render source through the
699
+ * catalog-failed path, so no miss is recorded and nothing is sent; and the capability
700
+ * settles as not writable, so nothing waits on it, and the read-only release drops what
701
+ * an earlier render queued, since it can never be sent. With `requireCredentials`, `t()`
702
+ * throws `error`.
703
+ */
704
+ markUnconfigured(error: LangsysConfigurationError, requireCredentials: boolean): void;
705
+ /**
706
+ * Under `requireCredentials`, throw the configuration error while the client is
707
+ * unconfigured (WIRE-4). `t()` calls it, and so does `Translate`'s render, whose
708
+ * multi-phrase blocks never reach `t()`.
709
+ */
710
+ assertConfigured(): void;
711
+ /** `init()` accepted credentials: undo an earlier refusal, so the first catalog decides afresh. */
712
+ markConfigured(): void;
713
+ /**
714
+ * The capability is known to be "no": the client has no credentials, or authorization was
715
+ * refused. Without this a block registration waiting for the capability would wait forever.
716
+ */
717
+ settleNotWritable(): void;
718
+ /**
719
+ * A route change re-enters the SDK (HINT-13). Call it from the router's after-navigation
720
+ * hook. It publishes a fresh `t` through `tSignal` — the same dependency a locale change
721
+ * advances — so a component bound to `t` re-renders its mounted nodes, and each miss is
722
+ * recorded at the URL the page is now on. The core's own `Translate` and `Phrase` re-enter
723
+ * too, but only while their host is attached to the document, so an instance left
724
+ * undestroyed on a removed node records nothing.
725
+ *
726
+ * It sends nothing itself. What follows is the ordinary miss path: one report per URL,
727
+ * the jitter, the decline rules, GATE-9 and GATE-10 all apply, and a phrase already queued
728
+ * or registered is not queued again. Calling it twice for the same URL reports nothing more.
729
+ */
730
+ notifyNavigation(): void;
220
731
  /** Current translation function. Reads fresh state on every call. */
221
732
  get t(): TFunction;
222
733
  /** Subscribe to translation snapshot changes. Fires immediately with the current value. */
@@ -236,8 +747,148 @@ declare class Translations {
236
747
  */
237
748
  lookup(phrase: string, category: string): string | null;
238
749
  private buildTFn;
750
+ /** The page's catalog, locale and miss queue: what `t()` uses outside every request scope. */
751
+ private readonly pageView;
752
+ /**
753
+ * The body of `t()` over a catalog view: the page's, or a request scope's
754
+ * (SRV-7). One implementation, so a scope looks up, converts legacy keys,
755
+ * strips C0 controls and interpolates exactly as the page does, and differs
756
+ * only in whose catalog it reads and whose misses it records.
757
+ */
758
+ renderWith(view: CatalogView, phrase: string, rest: unknown[]): string;
759
+ /**
760
+ * Turn on the legacy-key mode with the app's kept source-language files, or
761
+ * turn it off with null (MIG-1). Throws `LegacyFormatError` for a file this
762
+ * core does not read (MIG-7), before anything resolves through it.
763
+ */
764
+ setLegacyKeys(files: LegacyKeyFile[] | null | undefined): void;
765
+ /**
766
+ * A key hit is the key's converted value, and the key's namespace is the
767
+ * category unless the call passed one (MIG-3, MIG-5). A miss is the argument
768
+ * as literal source text, which Langsys `t()` does not convert (MIG-2), and
769
+ * is noted at debug so a mistyped or removed key is visible (MIG-6). A value
770
+ * the conversion does not recognise registers as written and warns at every
771
+ * log level, naming the file and the key (MIG-4).
772
+ */
773
+ private resolveLegacyKey;
774
+ private warnOnce;
775
+ /**
776
+ * Record a miss on the page's lanes, as `t()` outside every scope does: for a
777
+ * request scope rebuilt from a seed on the client (`scopeFromSeed`), whose
778
+ * render's misses belong to the page.
779
+ */
780
+ recordPageMiss(category: string, token: string): void;
239
781
  private missingToken;
782
+ /**
783
+ * Whether this session may write. The single decision point for the write
784
+ * lane — never infer capability from `key_type`.
785
+ *
786
+ * In the browser the server-computed signal is authoritative, and `undefined`
787
+ * (not yet authorized) correctly holds rather than writing or hinting.
788
+ *
789
+ * Under SSR the signal is deliberately never written (it is a process-wide
790
+ * singleton and write capability is per-session), so we fall back to the
791
+ * value the last catalog fetch reported for THIS process. That is only sound
792
+ * while capability depends on the server's IP alone, which is constant for
793
+ * the process. A configured write grant makes it per-user instead, so the
794
+ * server lane is unsafe and we degrade to flushing after hydration — where
795
+ * the decision is per-user and correct.
796
+ */
797
+ /** Whether this session may write now, by the same decision the flush makes (see `canWrite`). */
798
+ mayWrite(): boolean;
799
+ private canWrite;
800
+ /**
801
+ * Record the server's write decision from an `authorize-project` or catalog
802
+ * response. Browser: updates the shared signal. Server: keeps a
803
+ * process-level copy used only when no grant is configured (see `canWrite`).
804
+ *
805
+ * `keyType` is the key type the same response describes. The authorization path
806
+ * passes it, because it runs before `init()` hands this instance the new config:
807
+ * reading `this.config` there would describe the PREVIOUS init's key on a re-init.
808
+ */
809
+ applyWriteEnabled(value: boolean | undefined, keyType?: string | undefined): void;
810
+ /**
811
+ * Whether a miss is worth holding in the REGISTRATION queue. Distinct from
812
+ * `canWrite()`, which decides whether to send: this decides whether keeping
813
+ * it could ever lead to a send.
814
+ *
815
+ * What happens to a declined miss differs by arm, and the difference is
816
+ * load-bearing rather than incidental:
817
+ *
818
+ * - In the BROWSER the discovery lane has already recorded it, so the page
819
+ * is still reported and nothing is lost.
820
+ * - Under SSR it is recorded NOWHERE — `recordMissForDiscovery` bails
821
+ * without a `window`. The safety net there is that the client re-runs
822
+ * `t()` after hydration and re-collects, which covers content rendered
823
+ * on both sides and does NOT cover content that only ever executes
824
+ * server-side. That gap is real and documented rather than closed.
825
+ *
826
+ * Two ways a queue becomes undrainable:
827
+ *
828
+ * SSR under `'client'` — the queue is meant to be flushed after hydration
829
+ * by `setup()`, but that reads the BROWSER's instance of this module. The
830
+ * push happened in the Node process's instance. Under real SSR (Next,
831
+ * Remix, Astro) those are different objects in different processes, and no
832
+ * channel carries tokens client-ward — `initialTranslations` goes the other
833
+ * way. So the tokens are silently dropped AND retained forever, since the
834
+ * 3s drain timer only exists in the browser branch of `setup()`. In a
835
+ * long-lived server that plateaus at "every phrase the app has ever
836
+ * server-rendered", held live and never sent. Declining to collect makes
837
+ * the limitation explicit instead of silent: server-side discovery requires
838
+ * `ssrTokenStrategy: 'server'`.
839
+ *
840
+ * Browser, definitively read-only — `writeEnabled` is `false` and no grant
841
+ * is configured, so no path exists by which this session becomes writable.
842
+ * (`undefined` still queues: capability isn't known yet and those misses
843
+ * must be held, not dropped. A configured grant also queues, since one
844
+ * arriving flips the session write-enabled.)
845
+ */
846
+ private shouldQueueForWrite;
240
847
  private scheduleTokenFlush;
848
+ /**
849
+ * Start the backstop interval if it isn't already running.
850
+ *
851
+ * Demand-driven deliberately. It used to start in `setup()` and run for the
852
+ * life of the page whether or not anything was ever queued — a wakeup every
853
+ * three seconds forever, for a queue that is empty almost all of the time.
854
+ * That is a battery and background-work cost on mobile, and React Native
855
+ * takes this same branch because it defines `window`.
856
+ *
857
+ * The tick disarms itself once the queue drains; the next miss re-arms it
858
+ * through `scheduleTokenFlush`. While sends keep failing the queue stays
859
+ * non-empty and the interval keeps running, which is the case a backstop
860
+ * exists for.
861
+ */
862
+ private ensureBackstop;
863
+ private stopBackstop;
864
+ /**
865
+ * Stop background work. Queued tokens stay queued, and further discovery
866
+ * re-arms the backstop — this releases the timer, it does not discard work.
867
+ */
868
+ destroy(): void;
869
+ /**
870
+ * Send whatever is still queued while the page is going away.
871
+ *
872
+ * Even with the debounce there is always a window where a miss is queued
873
+ * and the document is torn down — a tab closing, a client-side navigation,
874
+ * or the discovery renderer finishing its grace period. A normal request is
875
+ * cancelled with the document; `keepalive` survives it.
876
+ *
877
+ * Deliberately NOT `navigator.sendBeacon`, the usual answer here: it cannot
878
+ * set custom headers, and our authorization is `x-Authorization`, so every
879
+ * beacon would be rejected.
880
+ *
881
+ * Fire-and-forget with no local bookkeeping — the page is leaving, so there
882
+ * is no "after" in which a cache write or queue mutation could matter, and
883
+ * skipping them keeps this off the critical path of teardown.
884
+ */
885
+ private flushOnTeardown;
886
+ /**
887
+ * `visibilitychange → hidden` is the reliable teardown signal; `pagehide`
888
+ * backs it up. `unload`/`beforeunload` are deliberately not used — they are
889
+ * unreliable on mobile and block bfcache.
890
+ */
891
+ private installTeardownFlush;
241
892
  setup(config: iLangsysConfig): void;
242
893
  /**
243
894
  * Record that a locale's catalog is already present without fetching —
@@ -247,42 +898,66 @@ declare class Translations {
247
898
  * later switch *to* it still flows through `change()` normally.
248
899
  */
249
900
  markLoaded(locale: string): void;
901
+ /**
902
+ * A catalog is present for rendering, but it is not the catalog of record:
903
+ * a snapshot (SNAP-3). Unlike `markLoaded` nothing is recorded as fetched,
904
+ * so the next settle for the locale still fetches the catalog and replaces
905
+ * it. What waits on `ready()` renders from it now.
906
+ */
907
+ markSeeded(): void;
250
908
  /**
251
909
  * React to a user-locale change. Fetches translations if we haven't
252
910
  * loaded this locale within the last 60s (or if forced).
253
911
  */
254
912
  change(locale: string, force?: boolean, skipFetch?: boolean): Promise<boolean>;
913
+ /** The project and locale a catalog belongs to: its cache key, request key and CACHE-2 pair. */
914
+ private catalogScope;
915
+ /** One catalog request for a project and locale, recording the outcome for CACHE-2's window. */
916
+ private fetchCatalog;
917
+ /**
918
+ * Record a failed send and push the next attempt out exponentially:
919
+ * 3s, 6s, 12s … capped at 5 minutes. Reset on the first success.
920
+ */
921
+ private noteSendFailure;
255
922
  private updateTokens;
923
+ /**
924
+ * WIRE-4: the catalog fetch failed. Settle `ready()` so consumers render their
925
+ * source text, and mark the catalog unusable for deciding what is missing.
926
+ *
927
+ * The second half is the one that needs saying. Measured before it existed, with
928
+ * write capability already granted: a network-failed fetch let `t()` misses queue
929
+ * and POST phrases that were already registered, and a failed locale switch did
930
+ * the same, because the new scope had just reset the catalog to empty.
931
+ */
932
+ private markCatalogUnavailable;
933
+ /**
934
+ * Fetch and publish the catalog for `this.locale`. Resolves false when it could not.
935
+ *
936
+ * The response publishes only while the locale and cache scope it was requested for are
937
+ * still current. A locale change while it is in flight supersedes it, and it is dropped
938
+ * when it lands: published, it would show the previous locale's catalog under the new
939
+ * one, persist it under the new locale's cache key, and stamp the new locale as loaded,
940
+ * so `change()` would not fetch that locale for another 60s. A superseded response still
941
+ * resolves true or false, which `fetchCatalog` records against its own pair (CACHE-2).
942
+ */
256
943
  private getTranslations;
257
944
  }
258
945
 
259
- interface HttpResponse {
260
- status: number;
261
- statusText: string;
262
- url: string;
263
- data: string;
264
- }
265
- interface ResponseObject {
266
- status: boolean;
267
- page?: number;
268
- page_count?: number;
269
- records_per_page?: number;
270
- data?: object[] | object;
271
- errors?: string[];
272
- http?: HttpResponse;
273
- }
274
-
275
946
  interface iCountry {
947
+ /** Country name in the requested locale */
276
948
  label: string;
949
+ /** ISO 3166-1 alpha-2 country code (e.g., "ES") */
277
950
  code: string;
951
+ /** In the EU: VAT applies, and a business VAT ID makes it a reverse charge */
952
+ eu?: boolean;
278
953
  }
279
954
  type iCountryList = iCountry[];
280
955
  interface iCountryDialCode {
281
956
  /** ISO 3166-1 alpha-2 country code (e.g., "US", "CA", "GB") */
282
957
  country_code: string;
283
- /** International dialing code with + prefix (e.g., "+1", "+44", "+33") */
958
+ /** International dialing code, digits only (e.g., "1", "44", "506") */
284
959
  dial_code: string;
285
- /** Localized country name in the requested locale */
960
+ /** Country name in the requested locale with its dialing code (e.g., "Costa Rica (+506)") */
286
961
  name: string;
287
962
  }
288
963
 
@@ -317,34 +992,192 @@ declare class LangsysAppClass {
317
992
  private config;
318
993
  Translations: Translations;
319
994
  translationsLoadingPromise: Promise<unknown>;
320
- private locales;
321
- private countries;
322
- private countriesLocale;
323
- private dialCodes;
324
- private dialCodesLocale;
325
- private currencies;
326
- private currenciesLocale;
995
+ /**
996
+ * The reference lists read so far (REF-1), keyed by list and display locale: a list the API
997
+ * answered is held for the core's lifetime, a read in flight is shared, and a failed read is
998
+ * remembered for CACHE-2's window under the credentials it was sent with. Memory only: on a server the core is the long-lived
999
+ * module, so the hold already outlives the request, and nothing goes to a cache another
1000
+ * host reads (CACHE-1).
1001
+ */
1002
+ private referenceLists;
327
1003
  debug: Logger;
328
1004
  constructor();
329
1005
  /** Current translation function. Reads fresh state on every call. */
330
1006
  get t(): TFunction;
1007
+ /**
1008
+ * Supply (or replace) the write grant after `init()` has run.
1009
+ *
1010
+ * This is for apps whose token only exists once the user logs in — i.e.
1011
+ * after the SDK has already booted. It is NOT the refresh path: the token
1012
+ * is resolved fresh before every request and never cached, so refresh is a
1013
+ * pull. Pass a provider function and let your auth layer decide when to
1014
+ * mint; that also makes this safe to call while a request is in flight,
1015
+ * since an in-flight request has already resolved its own token.
1016
+ *
1017
+ * Pass `undefined` to drop the grant (e.g. on logout).
1018
+ */
1019
+ setWriteGrant(grant: WriteGrant | undefined): Promise<void>;
1020
+ /**
1021
+ * Apply an `authorize-project` payload. Shared by `init` and the
1022
+ * re-authorization in `setWriteGrant`, so a policy or capability change is
1023
+ * picked up identically by both — previously the re-auth path silently
1024
+ * ignored `auto_discovery` even though the response carries it.
1025
+ */
1026
+ private applyAuthorization;
1027
+ /** A route change re-enters the SDK (HINT-13). See `Translations.notifyNavigation`. */
1028
+ notifyNavigation(): void;
1029
+ /**
1030
+ * Put a catalog in place SYNCHRONOUSLY, with no network and no await.
1031
+ *
1032
+ * The hydration hand-off primitive: a framework's client entry calls this
1033
+ * with the catalog the server already rendered from, before mount, so the
1034
+ * first paint has translations instead of source text. `t()` returns the
1035
+ * translation on the very next line — there is nothing to await, which is
1036
+ * the whole point. `init()` can run afterwards, at its own pace, to
1037
+ * authorize and take over refreshes.
1038
+ *
1039
+ * Does exactly what the `initialTranslations` hand-off inside `init()` does
1040
+ * — injects `__uncategorized__`, stamps every `__category__`, publishes the
1041
+ * catalog and the locale — because it IS that code now. Two
1042
+ * implementations of "seed a catalog" would be two sets of rules about what
1043
+ * a seeded catalog looks like.
1044
+ *
1045
+ * Safe to call before `init()`, after it, or both: `init()` will not
1046
+ * re-seed a locale that is already seeded, so an SSR payload passed through
1047
+ * config cannot clobber a catalog a client entry put there first.
1048
+ *
1049
+ * `served`, a request scope's `seed()`, hands over what the server rendered
1050
+ * before any DOM class mounts: each block's source tokens, which a served
1051
+ * translation re-renders from, and which blocks and phrases the server sends
1052
+ * itself (`collected`), which the client then never registers. Nothing is
1053
+ * registered here; `registerBlock(seededBlock)` after `init()` does that.
1054
+ */
1055
+ /**
1056
+ * The request header that asks the app's own API for the user's language
1057
+ * (FRM-6): `{ 'Accept-Language': 'es-es' }` for the locale the user chose,
1058
+ * inside a request scope the scope's, else the loaded catalog's. Empty before
1059
+ * any locale is known. Spread it into the headers of the app's API calls, so an
1060
+ * API that answers per the negotiated language (FRM-5) answers in the user's,
1061
+ * not the browser's default.
1062
+ */
1063
+ localeHeaders(): Record<string, string>;
1064
+ seedCatalog(catalog: iCategories, locale: string, served?: {
1065
+ blocks?: Record<string, SeededBlock>;
1066
+ phrases?: readonly SeededPhrase[];
1067
+ }): void;
1068
+ /**
1069
+ * Load a catalog snapshot (spec SNAP-2) SYNCHRONOUSLY as the preloaded
1070
+ * catalog for `locale` — the current user locale by default, else the
1071
+ * snapshot's base locale. A first render or an offline session then has
1072
+ * its translations with no network call.
1073
+ *
1074
+ * The snapshot is a cache, never the catalog of record (SNAP-3): once
1075
+ * `init()` runs, the catalog is fetched as usual and replaces it, and a
1076
+ * phrase the snapshot lacks is found there. With no network the snapshot
1077
+ * keeps rendering, and anything it lacks shows its source text.
1078
+ *
1079
+ * Throws `SnapshotError`, naming the reason, for a file that is not a
1080
+ * snapshot, an unsupported version, a missing member, or a checksum the
1081
+ * contents no longer match — an edited snapshot is refused, not served.
1082
+ * Returns false when the snapshot holds no catalog for the locale.
1083
+ */
1084
+ loadSnapshot(snapshot: string | CatalogSnapshot, locale?: string): boolean;
1085
+ /** Publish a catalog and its locale — the one set of rules for what a seeded catalog looks like. */
1086
+ private publishSeed;
1087
+ /**
1088
+ * Render a server message entry (spec MSG-5): its `template` through `t()`
1089
+ * under the messages category, filled from its `params`. When the catalog
1090
+ * holds no translation for the template, the entry's own `message` is shown
1091
+ * instead, which is the text the server filled and possibly localised; an
1092
+ * entry with no `message` shows its template filled. An entry with no
1093
+ * `template` is not looked up and shows its `message`. `message` is never
1094
+ * used as a lookup key.
1095
+ *
1096
+ * The lookup goes through `t()` on a miss too, so the template is recorded
1097
+ * like any other missing phrase. A plural in the translation renders from a
1098
+ * numeric param through the catalog's ICU, with no ICU written by the caller.
1099
+ */
1100
+ renderServerMessage(entry: ServerMessage, category?: string): string;
1101
+ /** Whether a catalog is already published for this locale. */
1102
+ private isAlreadySeeded;
1103
+ /** Read the current locale's catalog again, and the locale data `getLocaleName` reads, on its next load. */
331
1104
  refresh(): Promise<boolean>;
332
1105
  /**
333
1106
  * Initialize Langsys. Configuration must include at least `projectid`,
334
1107
  * `key`, and `UserLocaleStore` (anything satisfying `LocaleSource` — see
335
1108
  * its docstring for framework-specific adapters).
336
1109
  */
1110
+ /**
1111
+ * Whether `init()` accepted a project id, an API key and a locale store (WIRE-4). False
1112
+ * before `init()` runs and after one refused for missing credentials. Authorization is a
1113
+ * separate question: a configured client whose key the server refuses is still configured.
1114
+ */
1115
+ isConfigured(): boolean;
1116
+ /**
1117
+ * `init()` without credentials (WIRE-4). Nothing throws here: `t()` renders source text with
1118
+ * its params filled, no miss is queued and no request is made, a component's block
1119
+ * registration resolves skipped with reason `not-configured`, and an explicit write throws
1120
+ * `LangsysConfigurationError`. With `requireCredentials`, `t()` throws it too.
1121
+ */
1122
+ private refuseUnconfigured;
337
1123
  init(initConfig: iLangsysInitConfig): Promise<ResponseObject>;
1124
+ /** The countries' dial codes, named in the display locale (REF-1). */
338
1125
  getDialCodes(inLocale?: string): Promise<iCountryDialCode[]>;
1126
+ /** The currencies, named in the display locale (REF-1). */
339
1127
  getCurrencies(inLocale?: string): Promise<iCurrencyList>;
1128
+ /** The countries, named in the display locale (REF-1). */
340
1129
  getCountries(inLocale?: string): Promise<iCountryList>;
341
1130
  getCountryName(forCountryCode: string, inLocale?: string): Promise<string>;
342
1131
  getCurrencyName(forCurrencyCode: string, inLocale?: string): Promise<string>;
1132
+ /** The locales in one of the API's three forms, named in the display locale (REF-1). */
343
1133
  getLocalesFormat(format?: '' | 'flat' | 'data', inLocale?: string): Promise<iLocaleDefault | iLocaleFlat[] | iLocaleData[]>;
344
1134
  getLocalesFlat(inLocale?: string): Promise<iLocaleFlat[]>;
1135
+ /** `forceRefresh` is an explicit refresh: it reads the list again, held or inside a failure window, and holds what that read answers. */
345
1136
  getLocalesData(inLocale?: string, forceRefresh?: boolean): Promise<iLocaleData[]>;
346
1137
  getLocales(inLocale?: string): Promise<iLocaleDefault>;
1138
+ /**
1139
+ * Read one reference list in a display locale (REF-1), or answer the one held for it.
1140
+ *
1141
+ * The display locale is the one given, else the current one, canonical as the core serves
1142
+ * it; one not shaped as a locale is refused before anything is sent, since it is written
1143
+ * into the request. A locale form is asked for that locale alone and answers that locale's entry,
1144
+ * whose key and codes are in the organisation's code format, so it is matched and recoded
1145
+ * in WIRE-3's form; a locale the API does not hold answers `empty()`, held like any
1146
+ * answer. Nothing here throws but `requireCredentials` (WIRE-4): a refused locale, a failed
1147
+ * read and an answer not of the list's shape answer `empty()` with the cause logged, and
1148
+ * a failed read is remembered for CACHE-2's window under its credentials, inside which a
1149
+ * call under them sends nothing; a call under other credentials still reads, though one made
1150
+ * while a read is in flight shares it, whoever sent it.
1151
+ * An unconfigured core sends nothing and logs nothing, whatever the display locale,
1152
+ * `init()`'s refusal being its one warning. Each caller gets its own copy of a held list,
1153
+ * so what one caller does to it never reaches the next; `copy: false` is for the core's own
1154
+ * lookups, which only read it.
1155
+ */
1156
+ private readReferenceList;
1157
+ /**
1158
+ * An explicit refresh of one list and display locale: the held list and every failure
1159
+ * window go, so the next call reads. Each credentials' count of failures in a row stays, so
1160
+ * a refresh that fails again widens CACHE-2's window as any failure in a row does.
1161
+ */
1162
+ private forgetReferenceList;
1163
+ /**
1164
+ * A reference list's display locale (REF-1): the one given, else the current one, each
1165
+ * canonical, as the core normalises the locale it serves. The current locale is the one
1166
+ * `t()` and the catalog serve, on both profiles: the request scope's, else the store's. So
1167
+ * a given `iw-IL` and a store holding `iw-IL` both name their lists in `he-il`.
1168
+ */
1169
+ private displayLocale;
347
1170
  getLocaleNameWithLookup(forLocale: string, shortName?: boolean, inLocale?: string): Promise<string>;
1171
+ /**
1172
+ * Synchronous locale-name lookup against the in-memory locales cache.
1173
+ *
1174
+ * The cache is only populated once `await getLocalesData(inLocale)` — or
1175
+ * any `getLocaleNameWithLookup` call — has settled for that display locale.
1176
+ * Called before then this returns `''`, the same value a genuinely unknown
1177
+ * locale returns, so the two cases are told apart by the warning rather
1178
+ * than by the return value. Use `getLocaleNameWithLookup` when you can
1179
+ * await; it loads the data first.
1180
+ */
348
1181
  getLocaleName(forLocale: string, shortName?: boolean, inLocale?: string): string;
349
1182
  /** @deprecated use `getLocaleNameWithLookup` or `getLocaleName` */
350
1183
  getLanguageName(forLocale: string, shortName?: boolean, inLocale?: string): Promise<string>;
@@ -353,17 +1186,21 @@ declare class LangsysAppClass {
353
1186
  *
354
1187
  * - In browsers, uses `navigator.languages` with fallback to `navigator.language`.
355
1188
  * - In SSR environments, parses the `Accept-Language` header.
1189
+ *
1190
+ * With `supportedLocales`, returns the best CLDR-aware match from that list
1191
+ * or `false` when none of the user's preferences are supported — so the
1192
+ * documented `detectPreferredLocale(header, supported) || 'en-us'` idiom
1193
+ * actually reaches your default. Returning the user's own unsupported
1194
+ * locale instead would send the app to fetch a catalog that does not
1195
+ * exist, and the caller's `||` would never fire.
1196
+ *
1197
+ * Without `supportedLocales`, returns the user's first preference in
1198
+ * canonical form, or `false` when no preference is detectable.
356
1199
  */
357
1200
  detectPreferredLocale(acceptLanguageHeader?: string | null, supportedLocales?: string[]): string | false;
358
1201
  private getUserLanguagePreferences;
359
1202
  private parseAcceptLanguageHeader;
360
1203
  private findBestLocaleMatch;
361
- /**
362
- * Resolve the effective locale for a data-fetching helper: explicit
363
- * argument, else the user's current locale, else the base locale —
364
- * always in BCP 47 canonical form so cache keys and API routes agree.
365
- */
366
- private resolveLocale;
367
1204
  }
368
1205
  declare const LangsysApp: LangsysAppClass;
369
1206
 
@@ -383,6 +1220,12 @@ interface TranslateOptions {
383
1220
  * normalized back to `{name}` at capture.
384
1221
  */
385
1222
  params?: Record<string, ParamPrimitive>;
1223
+ /**
1224
+ * `false` renders from the catalog and registers nothing, on either lane (VAR-7):
1225
+ * for a binding whose content holds values from variables it cannot name without
1226
+ * its build-time transform. One debug notice says so. Default `true`.
1227
+ */
1228
+ register?: boolean;
386
1229
  }
387
1230
  /**
388
1231
  * Wrap a DOM element and manage translation for its contents.
@@ -405,15 +1248,206 @@ declare class Translate {
405
1248
  private isTokenizing;
406
1249
  private lastTranslatedLocale;
407
1250
  private custom_id;
408
- private unsubscribeLocale;
1251
+ private unsubscribers;
409
1252
  /** Sorted params key-set already checked, so value-only updates don't re-warn. */
410
1253
  private checkedParamKeys;
411
- constructor(element: HTMLElement, options?: TranslateOptions);
1254
+ /** The block this host registered as, kept so a navigation can re-record its miss. */
1255
+ private contentBlock;
1256
+ /**
1257
+ * The id a renderer stamped on this host (MARK-3's identity), adopted rather
1258
+ * than derived. Such a host renders from the catalog entry under that id, or
1259
+ * its source text, and registers nothing.
1260
+ */
1261
+ private adoptedId;
1262
+ /**
1263
+ * Whether an adopted host's source tokens have been recovered: its DOM holds
1264
+ * the text a server translated, never the source the catalog is keyed by.
1265
+ */
1266
+ private sourcesRecovered;
1267
+ /** The locale an adopted host was served in; its served text is right for that locale. */
1268
+ private servedLocale;
1269
+ /** What the unit's value markers say (VAR-3): the runs among its tokens, and their values. */
1270
+ private vars;
1271
+ /** Each marked run's source, by its first node, where it was recovered rather than read. */
1272
+ private runSources;
1273
+ /** Each marked run's text nodes as first seen, before any render wrote them. */
1274
+ private runOriginals;
1275
+ /** Each marked value as first seen, for a render that emptied it. */
1276
+ private markedValues;
1277
+ /** Marked hosts inside this one that no instance managed, taken over by this walk. */
1278
+ private takenOver;
1279
+ /**
1280
+ * SRV-5: the content captured at mount is provisional. A framework can mount a
1281
+ * block showing a placeholder (a Suspense fallback, a lazy child's spinner)
1282
+ * and swap in the real content a moment later. An observer watches the host's
1283
+ * STRUCTURE (child lists, never text: a value update must not re-key a block);
1284
+ * the first registration waits until it has been quiet for `SETTLE_MS`, and a
1285
+ * later structural change re-derives the block and registers what it shows.
1286
+ */
1287
+ private observer;
1288
+ private settleTimer;
1289
+ /** Whether the subtree has been quiet for the settle window since mount. */
1290
+ private settled;
1291
+ private settleWaiters;
1292
+ /** A structural change the tokens do not reflect yet. */
1293
+ private dirty;
1294
+ /** Whether the id is derived from the content, and so follows it when it changes. */
1295
+ private derivedId;
1296
+ /** The id this instance last registered under, so a re-key after the window can be reported. */
1297
+ private registeredId;
1298
+ /** The text this class last wrote into each text node (see `isUnitChange`). */
1299
+ private written;
1300
+ /** The whitespace at each end of a single-token unit's text as the page gave it (`writeKeepingEdges`). */
1301
+ private edges;
1302
+ /**
1303
+ * @param byWalk Internal: set when an enclosing walk creates this instance for
1304
+ * a host nothing managed, so an instance the author constructs replaces it.
1305
+ */
1306
+ constructor(element: HTMLElement, options?: TranslateOptions, byWalk?: boolean);
1307
+ /**
1308
+ * HINT-13: after `notifyNavigation()`, re-enter the lookup as a re-render would, so a miss is
1309
+ * recorded at the page's new URL — only while the host is attached to the document. An
1310
+ * instance whose node was removed but never destroyed stays subscribed, and re-entering it
1311
+ * would report the old page's content for the new one.
1312
+ */
1313
+ private reenterAfterNavigation;
412
1314
  /** Update interpolation params (e.g. a changed count) and re-render. Mirrors `Phrase.setParams`. */
413
1315
  setParams(params: Record<string, ParamPrimitive> | undefined): void;
414
- /** Stop reacting to locale changes. Safe to call multiple times. */
1316
+ /**
1317
+ * Watch the host's structure (SRV-5). Not for an adopted host or one in a
1318
+ * resolved scope: served content is already what it shows, and registers
1319
+ * nothing. Where there is no `MutationObserver`, the content is settled at mount.
1320
+ */
1321
+ private observeStructure;
1322
+ /**
1323
+ * Whether a mutation changes what this unit shows. A changed child list always
1324
+ * does. A changed text does only until the window first closes, and only when it
1325
+ * is not the text this class wrote: a framework can swap a placeholder's text for
1326
+ * its content in place, and what registers is what the block shows when the
1327
+ * window closes. After that, a text change is a value update and never re-keys.
1328
+ */
1329
+ private isUnitChange;
1330
+ /**
1331
+ * Whether the unit changed since its tokens were read, counting changes the
1332
+ * observer has not delivered yet: a framework's swap and a catalog arriving can
1333
+ * fall in the same task, before the observer's callback runs.
1334
+ */
1335
+ private hasUnreadChange;
1336
+ /** Write a text node, remembering the text, so the observer can tell this write from a framework's. */
1337
+ private writeText;
1338
+ /**
1339
+ * Write a unit's one token with the text node's own whitespace kept at each end, as
1340
+ * the block path keeps it: beside an excluded element (TOK-1) it is the space between
1341
+ * two words. The ends are the page's, so a re-render never adds to them, and a text
1342
+ * the framework has rewritten since gives its own.
1343
+ */
1344
+ private writeKeepingEdges;
1345
+ /**
1346
+ * Whether a changed child list belongs to this unit. An excised subtree, and
1347
+ * each nested host, is another instance's. An `<option>`'s list is changed only
1348
+ * by this class, writing its text.
1349
+ */
1350
+ private isUnitStructure;
1351
+ private armSettle;
1352
+ private markSettled;
1353
+ private whenSettled;
1354
+ /**
1355
+ * The block's structure changed after it was read: read it again, under the id
1356
+ * its content now derives (an app's or a stamped id stays), stamp it, render it,
1357
+ * and register it if unknown. An id already sent for the earlier content stays
1358
+ * registered; the core cannot take a registration back.
1359
+ */
1360
+ private rederive;
1361
+ /** Stop reacting to locale and catalog changes. Safe to call multiple times. */
415
1362
  destroy(): void;
1363
+ /**
1364
+ * Take over every marked host inside this one that no instance manages
1365
+ * (MARK-2, MARK-3). The walk excises such a host from this unit (MARK-4), and
1366
+ * without an instance of its own it would never register: a phrase host
1367
+ * registers whole, as the one string its markup defines, and a block host
1368
+ * registers as its own block or, when it carries a stamped id, renders under
1369
+ * that id and registers nothing. Excluded subtrees are not entered, and a
1370
+ * marked host's own inside is its instance's to walk.
1371
+ */
1372
+ private takeOverUnmanagedHosts;
416
1373
  private translateUpdate;
1374
+ /**
1375
+ * An adopted host holds the text a renderer served, a translation when the
1376
+ * page was served in another locale, and the catalog is keyed by source. Its
1377
+ * source tokens come from the scope's seed, or the catalog entry the served text
1378
+ * was rendered from, never from the DOM (SRV-4): each text node and attribute is
1379
+ * given its source as the original it renders from. Until they are recovered the
1380
+ * host is left as served, which is right for the locale it was served in; a
1381
+ * later switch tries again, since a binding may hand the seed over after mount.
1382
+ */
1383
+ /**
1384
+ * An adopted host re-rendered in another locale names that locale in its own
1385
+ * resolved marker, so the marker keeps describing the text it holds. A marker
1386
+ * with no locale (bare, `true`) is left as it is.
1387
+ */
1388
+ private restampResolvedLocale;
1389
+ private recoverServedSources;
1390
+ /**
1391
+ * Render a single-token block.
1392
+ *
1393
+ * Prefers the content-block entry: the token may be stored as a block —
1394
+ * registered under an explicit `custom_id`, by an older SDK, or seeded
1395
+ * server-side — and flat `t()` cannot see inside block entries, so it would
1396
+ * report a miss for content that is present. Falls back to `t()`, which
1397
+ * also queues the miss for registration.
1398
+ */
1399
+ private renderSingleToken;
1400
+ /**
1401
+ * Whether the single-token fast path may claim this element.
1402
+ *
1403
+ * "One token" is NOT the same question as "one text node", and treating
1404
+ * them as one was the defect. A translatable ATTRIBUTE is also a token, and
1405
+ * an element carrying only an attribute token — `<img alt="…">`,
1406
+ * `<input placeholder="…">` — has no text node anywhere in its subtree. The
1407
+ * fast path would then write the translation as the element's text, and the
1408
+ * element itself is gone.
1409
+ *
1410
+ * So the fast path requires an actual text node to write into. Everything
1411
+ * else goes to the node-walking path, which already translates attributes
1412
+ * in place and recurses, so a token on a nested element is reached too.
1413
+ */
1414
+ private usesSingleTextNodeFastPath;
1415
+ /** A unit whose one token is a marked run is a phrase too (VAR-3, TOK-6): one sentence, with its values. */
1416
+ private isSingleRun;
1417
+ /** Whether this unit registers: not with `register: false`, and not when it is made only of markers (VAR-3, VAR-7). */
1418
+ private registers;
1419
+ /**
1420
+ * The unique non-whitespace text node under `root`, or `null` when there is
1421
+ * none or more than one — in which case the caller falls back to a flat
1422
+ * text render, since there is no single place to put the result.
1423
+ */
1424
+ /**
1425
+ * Publish the resolved `custom_id` on the host element.
1426
+ *
1427
+ * A server-rendered page is otherwise unreadable: the id is derivable only
1428
+ * by re-running the tokenizer over the same subtree, which a reader holding
1429
+ * just the HTML cannot do identically — and the tokenizer's own rules are
1430
+ * what decide it. Stamping it makes the block self-describing, the same way
1431
+ * a `Phrase` host already carries `PHRASE_MARKER_ATTR`.
1432
+ *
1433
+ * Written on every resolution, including when the migration fallback adopts
1434
+ * a legacy id, so the attribute always names the id actually in use rather
1435
+ * than the one first derived.
1436
+ */
1437
+ private stampContentBlockMarker;
1438
+ /** The unit's first marked run, where its single-run token comes from. */
1439
+ private findRun;
1440
+ /** The node a run is known by: its first text node or marker. */
1441
+ private runKey;
1442
+ /** A run text node's text before any render wrote it. */
1443
+ private runOriginal;
1444
+ /** A marked value: the text the framework shows now, or, where a render emptied it, what it showed. */
1445
+ private markedValue;
1446
+ /** Write `template` into a marked run, around its values where it can (see `renderRun`). */
1447
+ private writeRun;
1448
+ /** A marked run inside a block: its translation under the block's id, else its source. */
1449
+ private translateRun;
1450
+ private findSingleTextNode;
417
1451
  private tokenizeContent;
418
1452
  private handleContentBlock;
419
1453
  private translate;
@@ -427,15 +1461,185 @@ declare class Translate {
427
1461
  /**
428
1462
  * Interpolate `{name}`-style params into a resolved text, matching `t()`:
429
1463
  * unknown keys fall through untouched, and the untranslated fallback is
430
- * interpolated too. No-op when no params were supplied.
1464
+ * interpolated too. Runs without params as well, since a text carrying ICU
1465
+ * renders its `other` branch rather than its raw source (ICU-1). Plain text,
1466
+ * braces included, comes back exactly as written.
431
1467
  */
432
1468
  private applyParams;
433
1469
  private translateAttributes;
434
1470
  private translateAttribute;
435
1471
  }
436
1472
 
437
- /** Attribute marker the `Translate` tokenizer uses to skip a `<Phrase>` subtree. */
438
- declare const PHRASE_MARKER_ATTR = "data-ls-phrase";
1473
+ /**
1474
+ * Framework-agnostic content-block helpers.
1475
+ *
1476
+ * The DOM-mutating `Translate` class in `translate.ts` is one consumer of
1477
+ * these — it walks a live element, tokenizes it, registers the content
1478
+ * block, and mutates the same element in place. That model fits Svelte
1479
+ * and vanilla JS, where the SDK genuinely owns the DOM node.
1480
+ *
1481
+ * For React/Vue/Angular wrappers, mutating the DOM directly is an anti-
1482
+ * pattern — those frameworks own their rendered output. Wrappers should
1483
+ * import the helpers below to handle the framework-agnostic parts (token
1484
+ * discovery, content-block registration, dedup against the local cache)
1485
+ * and render translated content through their framework's native means.
1486
+ *
1487
+ * Wrapper pattern:
1488
+ *
1489
+ * 1. After mount, grab the rendered DOM via the framework's ref/binding.
1490
+ * 2. `const { tokens, content } = tokenizeElement(el);`
1491
+ * 3. `const customId = generateCustomId(category, tokens);`
1492
+ * 4. `if (!isContentBlockKnown(category, customId)) {`
1493
+ * ` await registerContentBlock({ custom_id: customId, category, content, tokens, label });`
1494
+ * `}`
1495
+ * 5. Subscribe to `sTranslations` + `currentlyLoadedLocale`; on change,
1496
+ * look up each token via `Translations.lookupContent(category, customId, token)`
1497
+ * and render via your framework's templating.
1498
+ */
1499
+
1500
+ /**
1501
+ * Attributes that mark a subtree as a self-managed "keep-together" phrase,
1502
+ * which the block tokenizer must skip rather than split at tag boundaries.
1503
+ *
1504
+ * Two spellings, because two SDKs mark the same thing:
1505
+ * - `data-ls-phrase` — set by this SDK's framework `<Phrase>` components.
1506
+ * - `data-langsys-phrase` — `langsys-php`'s author-facing marker, which
1507
+ * survives into `translatePage()` output.
1508
+ *
1509
+ * The distinction is invisible in normal use, but on SSR handoff there is only
1510
+ * ONE DOM: a page rendered by PHP and then hydrated by a JS SDK is walked by
1511
+ * both implementations, so this tokenizer has to recognise PHP's marker or it
1512
+ * re-tokenizes a subtree PHP deliberately kept whole. Recognising both is
1513
+ * additive — `data-langsys-phrase` never appears in DOM our components emit.
1514
+ */
1515
+ /**
1516
+ * True when an element is marked as a self-managed phrase by either SDK.
1517
+ *
1518
+ * Presence alone means intent, like any boolean HTML attribute — but an
1519
+ * explicit `="false"` or `="0"` opts OUT, compared after trimming. Mirrors
1520
+ * `langsys-php`'s `isPhraseMarked()` exactly (same method name in both SDKs,
1521
+ * so the correspondence is checkable at a glance). Without the opt-out we
1522
+ * would skip a subtree the author had deliberately un-marked, and it would
1523
+ * then be translated by neither SDK.
1524
+ */
1525
+ declare function isPhraseMarked(element: Element): boolean;
1526
+ /**
1527
+ * True iff the local translations cache already has the content block
1528
+ * for the given `(category, customId)` — meaning the backend knows about
1529
+ * it and there's no need to re-register.
1530
+ *
1531
+ * The cache is keyed under `'__uncategorized__'` for null/empty categories,
1532
+ * matching the convention the server uses in its GET /translations response.
1533
+ */
1534
+ declare function isContentBlockKnown(category: string, customId: string): boolean;
1535
+ /**
1536
+ * Why a registration did not reach the server, or did not take (REG-10).
1537
+ *
1538
+ * - `not-configured`: the client has no credentials, so nothing can be sent
1539
+ * (WIRE-4);
1540
+ * - `not-write-enabled`: the session may not write, so the block was handed to
1541
+ * the discovery lane instead;
1542
+ * - `catalog-unavailable`: the catalog fetch failed, so an unknown block cannot
1543
+ * be told from a registered one (WIRE-4);
1544
+ * - `base-locale-gate`: the project registers only at its base locale (GATE-9);
1545
+ * - `deferred`: the block was found inside a request scope, which holds it for its
1546
+ * flush after the response (SRV-3, SRV-7);
1547
+ * - `client-strategy`: the SSR token strategy leaves collection to the client
1548
+ * (SSR-1), or a write grant made capability per-user (SSR-2);
1549
+ * - `refused`: the server answered the request with a failure;
1550
+ * - `failed`: the request did not complete.
1551
+ */
1552
+ type RegistrationReason = 'not-configured' | 'not-write-enabled' | 'catalog-unavailable' | 'base-locale-gate' | 'deferred' | 'client-strategy' | 'refused' | 'failed';
1553
+ /**
1554
+ * What a registration did. Success only when the server stored the block. A
1555
+ * write skipped on purpose is `{ status: false, skipped: true, reason }`, and one
1556
+ * that failed is `{ status: false, reason, errors }`, so a caller can tell them
1557
+ * apart and neither reads as success.
1558
+ */
1559
+ type RegistrationResult = {
1560
+ status: true;
1561
+ } | {
1562
+ status: false;
1563
+ skipped: true;
1564
+ reason: 'not-configured' | 'not-write-enabled' | 'catalog-unavailable' | 'base-locale-gate' | 'deferred' | 'client-strategy';
1565
+ } | {
1566
+ status: false;
1567
+ skipped?: false;
1568
+ reason: 'refused' | 'failed';
1569
+ errors?: unknown[];
1570
+ };
1571
+ /**
1572
+ * POST a content block to the backend, then stamp its custom_id into the local
1573
+ * translations cache, so later mounts in this session, and later reloads since
1574
+ * the cache is persisted, skip the POST.
1575
+ *
1576
+ * A block has one request in flight at a time under one set of credentials
1577
+ * (REG-7). A call for a block whose request has not answered yet resolves with
1578
+ * that request's result and sends nothing: a binding calls `registerBlock` on
1579
+ * every change `t` goes through, and a page load makes several of those before
1580
+ * the first request returns. Once it answers, the block is known, or, refused or
1581
+ * failed, may be sent again. A request answers only for the project and key it
1582
+ * went out under, so a call under others, after a re-init, sends its own.
1583
+ *
1584
+ * One behaviour on every path (REG-10): never throws, always logs, and never
1585
+ * reports success for work that did not happen. A skipped write returns a
1586
+ * non-success result naming its reason.
1587
+ */
1588
+ declare function registerContentBlock(contentBlock: iContentBlock): Promise<RegistrationResult>;
1589
+ /**
1590
+ * Walk an `HTMLElement`, collecting tokens for translation and producing a
1591
+ * cloned-and-stylized snapshot of its content for the Translation Manager.
1592
+ *
1593
+ * Tokens harvested:
1594
+ * - text-node content (whitespace-normalized, trimmed)
1595
+ * - translatable attribute values (`placeholder`, `alt`, `title`, `aria-*`, etc.)
1596
+ * - button/input/select value text
1597
+ *
1598
+ * Skips subtrees marked with `translate="no"`.
1599
+ *
1600
+ * The returned `content` is a string snapshot of the element's children, as the
1601
+ * tree path registers a block's nodes: the host is the unit, not part of its
1602
+ * content. The live element is not mutated. Computed semantic styles (color,
1603
+ * font, spacing, etc.) are inlined onto the clone so translators see content
1604
+ * styled the way end-users do. It holds no value and no nested unit's content
1605
+ * (`scrubContent`).
1606
+ *
1607
+ * Requires that `element` is mounted in the document so `getComputedStyle`
1608
+ * can read its style. For SSR / pre-mount callers, the style capture step
1609
+ * silently no-ops and the snapshot is plain (without inlined styles).
1610
+ */
1611
+ declare function tokenizeElement(element: HTMLElement): {
1612
+ tokens: string[];
1613
+ content: string;
1614
+ vars: UnitVars;
1615
+ };
1616
+ /**
1617
+ * The token list this element would have produced before 0.6.3, when every
1618
+ * `<option>`'s text was harvested twice — once by a `<select>` special case and
1619
+ * again by the ordinary text-node walk.
1620
+ *
1621
+ * **Lookup only.** Content blocks registered by an older SDK are keyed by an id
1622
+ * derived from this list, so `Translate` falls back to it when the corrected id
1623
+ * misses; without that, every block containing a `<select>` would silently
1624
+ * re-key and lose its translations on upgrade. Registration always uses the
1625
+ * corrected list, so the legacy-keyed population can only shrink.
1626
+ *
1627
+ * Skips the computed-style capture that `tokenizeElement` performs — this is a
1628
+ * cache-key computation, not a snapshot for translators.
1629
+ *
1630
+ * @deprecated Migration aid; will be removed once catalogs have been rebased.
1631
+ */
1632
+ declare function legacyTokenizeElement(element: HTMLElement): string[];
1633
+ /** What the walk learns from value markers (VAR-3), alongside the tokens. */
1634
+ interface UnitVars {
1635
+ /** Each marker's value, by name: the params the unit renders with, unless the caller names them. */
1636
+ values: Record<string, string>;
1637
+ /** The indices of tokens that are marked runs. */
1638
+ runs: number[];
1639
+ /** The indices of tokens made only of markers, with no text of their own. */
1640
+ markerOnly: number[];
1641
+ }
1642
+
439
1643
  interface PhraseOptions {
440
1644
  /** Category the phrase registers under (disambiguation for translators). */
441
1645
  category?: string;
@@ -448,6 +1652,12 @@ interface PhraseOptions {
448
1652
  * normalized back to `{n}` at capture.
449
1653
  */
450
1654
  params?: Record<string, ParamPrimitive>;
1655
+ /**
1656
+ * `false` renders from the catalog and registers nothing (VAR-7): for a binding
1657
+ * whose phrase holds values from variables it cannot name without its build-time
1658
+ * transform. One debug notice says so. Default `true`.
1659
+ */
1660
+ register?: boolean;
451
1661
  }
452
1662
  /**
453
1663
  * DOM handler for a markup-bearing "rich" phrase (`<Phrase>`).
@@ -465,13 +1675,37 @@ declare class Phrase {
465
1675
  private host;
466
1676
  private category;
467
1677
  private params;
1678
+ private register;
1679
+ /** Each marked value in the host (VAR-3), by name: params the caller's own override. */
1680
+ private values;
468
1681
  private phrase;
469
1682
  private slots;
1683
+ /** The key a phrase holding an opaque element was registered under before TOK-1: looked up on a miss, never registered. */
1684
+ private earlier;
1685
+ /**
1686
+ * A phrase made only of value markers (VAR-3) or of opaque elements (TOK-1) has no
1687
+ * text of its own, and registers nothing, on mount or on a navigation.
1688
+ */
1689
+ private textless;
1690
+ /** The text of each opaque element that takes params, as the page gave it: where a caller's param can land (TOK-1). */
1691
+ private opaqueTexts;
1692
+ /**
1693
+ * The markup slot each element in the host renders. An element's place is its
1694
+ * slot only while the host shows the source's order; a translation that
1695
+ * reorders its markup, served or rendered here, moves them.
1696
+ */
1697
+ private slotOf;
1698
+ /** A served translation whose source phrase is not recovered yet (see `recoverServed`). */
1699
+ private servedUnrecovered;
470
1700
  private unsubscribers;
471
1701
  private ready;
472
1702
  /** Sorted params key-set already checked, so value-only updates don't re-warn. */
473
1703
  private checkedParamKeys;
474
- constructor(host: HTMLElement, options?: PhraseOptions);
1704
+ /**
1705
+ * @param byWalk Internal: set when an enclosing walk creates this instance for
1706
+ * a host nothing managed, so an instance the author constructs replaces it.
1707
+ */
1708
+ constructor(host: HTMLElement, options?: PhraseOptions, byWalk?: boolean);
475
1709
  /** Update interpolation params (e.g. a changed count) and re-render. */
476
1710
  setParams(params: Record<string, ParamPrimitive> | undefined): void;
477
1711
  /** Stop reacting to locale/translation changes. Safe to call multiple times. */
@@ -483,6 +1717,15 @@ declare class Phrase {
483
1717
  * re-warn on every render.
484
1718
  */
485
1719
  private _checkParams;
1720
+ /**
1721
+ * Inside a resolved scope the host holds a translation a server served, and the
1722
+ * catalog is keyed by source: the source phrase is the one catalog entry that
1723
+ * renders as the served text (`recoverPhraseSource`), never the DOM's text. Its
1724
+ * elements are mapped to the source's slots by the order the served translation
1725
+ * opened them. Until it is recovered the host renders from its served text,
1726
+ * which is right for the locale it was served in.
1727
+ */
1728
+ private recoverServed;
486
1729
  private _render;
487
1730
  }
488
1731
 
@@ -514,6 +1757,12 @@ declare class Phrase {
514
1757
  interface RichSlot {
515
1758
  /** Shallow clone of the original element — preserves tag, class, href, etc. */
516
1759
  template: HTMLElement;
1760
+ /**
1761
+ * For an element opaque in the phrase (TOK-1), the element itself: it renders its
1762
+ * own content as given, so reconstitution moves it rather than a copy, and what a
1763
+ * framework renders inside it stays the framework's.
1764
+ */
1765
+ source?: HTMLElement;
517
1766
  }
518
1767
  interface EncodedRichText {
519
1768
  /**
@@ -523,6 +1772,18 @@ interface EncodedRichText {
523
1772
  phrase: string;
524
1773
  /** Captured inline elements, indexed by markup-token slot number. */
525
1774
  slots: RichSlot[];
1775
+ /** Each marked value, by name (VAR-3): the params the phrase renders with, unless the caller names them. */
1776
+ values: Record<string, string>;
1777
+ /**
1778
+ * For a phrase holding an opaque element, the key it was registered under before
1779
+ * TOK-1 bound this path, with that key's slots and values: looked up on a miss for
1780
+ * `phrase`, never registered.
1781
+ */
1782
+ earlier?: {
1783
+ phrase: string;
1784
+ slots: RichSlot[];
1785
+ values: Record<string, string>;
1786
+ };
526
1787
  }
527
1788
  /**
528
1789
  * Walk an element's subtree and produce the encoded phrase + captured slots.
@@ -530,6 +1791,24 @@ interface EncodedRichText {
530
1791
  * Text nodes contribute their (whitespace-collapsed) text. Each child element
531
1792
  * becomes a markup-token pair wrapping its recursively-encoded contents, and a
532
1793
  * shallow clone of the element is captured as a slot. Nesting is preserved.
1794
+ *
1795
+ * COALESCING ADJACENT TEXT NODES IS CORRECT HERE — do not "fix" it.
1796
+ *
1797
+ * `_encodeNodes` concatenates adjacent text nodes and this function collapses
1798
+ * whitespace, producing ONE phrase string. That string is the lookup key:
1799
+ * `Phrase` calls `Translations.t(phrase, category)` and never computes a
1800
+ * `custom_id`. A sentence must survive the round trip whole — that is the
1801
+ * entire reason `<Phrase>` exists, since splitting `Based on {n}
1802
+ * <strong>reviews</strong>` into fragments makes correct agreement impossible
1803
+ * in languages with more than two plural forms.
1804
+ *
1805
+ * The `_walkForTokens` contract in `content-block.ts` says the OPPOSITE, for
1806
+ * the content-block path, where token-array arity is identity. Both are right
1807
+ * for their own path. If you arrived here from that comment intending to make
1808
+ * this consistent with it, the paths are not meant to agree.
1809
+ *
1810
+ * Pinned by `tests/content-block-identity.test.ts` ("Phrase path"), which
1811
+ * fails if this coalescing is removed.
533
1812
  */
534
1813
  declare function encodeRichText(root: HTMLElement): EncodedRichText;
535
1814
  /**
@@ -538,340 +1817,306 @@ declare function encodeRichText(root: HTMLElement): EncodedRichText;
538
1817
  * carries machine-findable delimiters for reconstitution.
539
1818
  */
540
1819
  declare function markupTokenValues(slotCount: number): Record<string, string>;
541
- /**
542
- * Rebuild DOM nodes from a resolved string containing sentinel-delimited spans.
543
- *
544
- * The original (framework-owned) elements are reused as wrappers — so scoped
545
- * CSS classes / attributes survive — with the translated text placed inside.
546
- *
547
- * Robust to model mistakes: if sentinels are unbalanced (a tag dropped or a
548
- * stray close), we fall back to a single plain-text node (markers stripped) —
549
- * "lose the markup, keep the meaning" rather than throw.
550
- */
551
- declare function reconstitute(resolved: string, slots: RichSlot[], doc?: Document): Node[];
1820
+ declare function reconstitute(resolved: string, slots: RichSlot[], doc?: Document, onElement?: (element: HTMLElement, slot: number) => void): Node[];
552
1821
  /** Remove any sentinel markers from a string, leaving the bare text. */
553
1822
  declare function stripSentinels(value: string): string;
554
1823
 
555
- declare class LangsysAppAPIClass {
556
- private apiurl;
557
- config: iLangsysConfig;
558
- private headers;
559
- constructor();
560
- setup(config: iLangsysConfig): void;
561
- /** Optional hook to point the client at a different API host. */
562
- setBaseUrl(url: string): void;
563
- validate(config: iLangsysConfig): Promise<ResponseObject>;
564
- getTranslations(locale: string): Promise<ResponseObject>;
565
- /**
566
- * Register phrases and/or content blocks via the current unified endpoint.
567
- * Each item carries `type: 'phrase' | 'content_block'`. Replaces the
568
- * deprecated `projects/{id}/tokens` and `projects/{id}/content-blocks` routes.
569
- */
570
- createTranslatableItems(items: Array<Record<string, unknown>>): Promise<ResponseObject>;
571
- post(path: string, data?: Record<string, unknown>): Promise<ResponseObject>;
572
- get(path: string, data?: Record<string, unknown>): Promise<ResponseObject>;
573
- delete(path: string, data?: Record<string, unknown>): Promise<ResponseObject>;
574
- patch(path: string, data?: Record<string, unknown>): Promise<ResponseObject>;
575
- put(path: string, data?: Record<string, unknown>): Promise<ResponseObject>;
576
- private send;
577
- }
578
- declare const LangsysAppAPI: LangsysAppAPIClass;
579
-
580
- interface iContentBlock {
581
- custom_id: string;
582
- category: string;
583
- content: string;
584
- label?: string;
585
- tokens: string[];
586
- }
587
-
588
1824
  /**
589
- * Framework-agnostic content-block helpers.
1825
+ * Minimal synchronous storage contract for `persist` — structurally a subset of
1826
+ * the DOM `Storage` interface, so `localStorage` satisfies it as-is.
590
1827
  *
591
- * The DOM-mutating `Translate` class in `translate.ts` is one consumer of
592
- * these — it walks a live element, tokenizes it, registers the content
593
- * block, and mutates the same element in place. That model fits Svelte
594
- * and vanilla JS, where the SDK genuinely owns the DOM node.
595
- *
596
- * For React/Vue/Angular wrappers, mutating the DOM directly is an anti-
597
- * pattern — those frameworks own their rendered output. Wrappers should
598
- * import the helpers below to handle the framework-agnostic parts (token
599
- * discovery, content-block registration, dedup against the local cache)
600
- * and render translated content through their framework's native means.
601
- *
602
- * Wrapper pattern:
603
- *
604
- * 1. After mount, grab the rendered DOM via the framework's ref/binding.
605
- * 2. `const { tokens, content } = tokenizeElement(el);`
606
- * 3. `const customId = generateCustomId(category, tokens);`
607
- * 4. `if (!isContentBlockKnown(category, customId)) {`
608
- * ` await registerContentBlock({ custom_id: customId, category, content, tokens, label });`
609
- * `}`
610
- * 5. Subscribe to `sTranslations` + `currentlyLoadedLocale`; on change,
611
- * look up each token via `Translations.lookupContent(category, customId, token)`
612
- * and render via your framework's templating.
1828
+ * Synchronous on purpose: `persist` seeds its signal during module
1829
+ * initialization, so an async backend could not be read in time. React Native
1830
+ * apps inject a synchronous adapter (MMKV, or an in-memory mirror of
1831
+ * AsyncStorage hydrated at startup) via `setPersistStorage`.
613
1832
  */
614
-
1833
+ interface PersistStorage {
1834
+ getItem(key: string): string | null;
1835
+ setItem(key: string, value: string): void;
1836
+ /** Optional. Used to drop superseded keys; absent on minimal adapters. */
1837
+ removeItem?(key: string): void;
1838
+ }
615
1839
  /**
616
- * Attributes that mark a subtree as a self-managed "keep-together" phrase,
617
- * which the block tokenizer must skip rather than split at tag boundaries.
1840
+ * Inject the storage backend used by every `persist` signal — those created
1841
+ * afterwards AND those that already exist.
618
1842
  *
619
- * Two spellings, because two SDKs mark the same thing:
620
- * - `data-ls-phrase` — set by this SDK's framework `<Phrase>` components.
621
- * - `data-langsys-phrase` — `langsys-php`'s author-facing marker, which
622
- * survives into `translatePage()` output.
1843
+ * Re-hydrating the existing ones is the point, not a nicety: the catalog store
1844
+ * is built at module load, long before a React Native app can hand us MMKV, so
1845
+ * an injection that only applied going forward would apply to none of the
1846
+ * signals anyone cares about.
623
1847
  *
624
- * The distinction is invisible in normal use, but on SSR handoff there is only
625
- * ONE DOM: a page rendered by PHP and then hydrated by a JS SDK is walked by
626
- * both implementations, so this tokenizer has to recognise PHP's marker or it
627
- * re-tokenizes a subtree PHP deliberately kept whole. Recognising both is
628
- * additive — `data-langsys-phrase` never appears in DOM our components emit.
1848
+ * Pass `null` to drop the injection and return to auto-detection.
629
1849
  */
630
- declare const PHRASE_MARKER_ATTRS: readonly ["data-ls-phrase", "data-langsys-phrase"];
1850
+ declare function setPersistStorage(storage: PersistStorage | null): void;
631
1851
  /**
632
- * True when an element opts out of translation entirely — it and its subtree
633
- * are skipped by the tokenizer.
1852
+ * Create a Signal whose value is persisted to storage when one is available: an
1853
+ * injected `PersistStorage` first, else `localStorage`. Falls back to in-memory
1854
+ * behaviour during SSR, or when storage is unavailable or quota-exceeded.
1855
+ * Stored value is JSON-encoded.
634
1856
  *
635
- * Mirrors `langsys-php`'s `HtmlParser::isTranslationExcluded()` exactly:
636
- * - `translate="no"` — the HTML standard attribute, matched case-insensitively.
637
- * - `data-notrans` — PHP's author-facing alias, for hosts whose templating
638
- * strips unknown bare attributes or where `translate` collides with another
639
- * tool. Presence is intent; an explicit `"false"`/`"0"` opts out of the
640
- * opt-out, compared after trimming.
1857
+ * `legacyKey` is READ but never written, so a renamed key migrates its cache on
1858
+ * first load instead of silently dropping it. A host app that downgrades the
1859
+ * SDK still finds its old value where it left it.
641
1860
  *
642
- * Honoring PHP's alias matters for the same reason as the phrase marker: on SSR
643
- * handoff there is ONE DOM, and content an author marked "do not translate"
644
- * must not be extracted and registered by whichever SDK happens to walk it.
1861
+ * DO NOT persist `writeEnabled`, `autoDiscovery`, or any other session
1862
+ * capability. "Cache it like translations for a faster warm start" is the
1863
+ * obvious next optimization and it is wrong twice over:
645
1864
  *
646
- * Both this and `isPhraseMarked` trim before comparing. They briefly differed
647
- * — PHP's phrase check predated the trim its exclusion check gained — and the
648
- * gap was resolved by asking rather than tidying: mirror exactly, flag the
649
- * wart, let the owner decide. Divergence-by-tidying is the same failure class
650
- * as divergence-by-oversight, just better intentioned.
651
- */
652
- declare function isTranslationExcluded(element: Element): boolean;
653
- /**
654
- * True when an element is marked as a self-managed phrase by either SDK.
1865
+ * 1. A persisted value is restored SYNCHRONOUSLY at module init, so the client
1866
+ * would mount with a concrete capability while the server rendered from
1867
+ * `undefined` — a hydration mismatch on every consumer, not just the ones
1868
+ * that resolve auth before first render.
1869
+ * 2. Write capability is per-session and IP-dependent: the same key answers
1870
+ * differently from different networks, and a grant makes it per-user.
1871
+ * Persisting it caches one session's answer across later sessions on a
1872
+ * shared device — the same unsoundness as caching it on an SSR singleton.
655
1873
  *
656
- * Presence alone means intent, like any boolean HTML attribute — but an
657
- * explicit `="false"` or `="0"` opts OUT, compared after trimming. Mirrors
658
- * `langsys-php`'s `isPhraseMarked()` exactly (same method name in both SDKs,
659
- * so the correspondence is checkable at a glance). Without the opt-out we
660
- * would skip a subtree the author had deliberately un-marked, and it would
661
- * then be translated by neither SDK.
1874
+ * Translations are safe to persist because they are project-scoped content, not
1875
+ * a permission decision. Capability is re-derived from the server every time.
662
1876
  */
663
- declare function isPhraseMarked(element: Element): boolean;
1877
+ declare function persist<T>(key: string, initial: T, legacyKey?: string): Signal<T>;
1878
+
664
1879
  /**
665
- * Compute the deterministic content-block id from a `(category, tokens)` pair.
1880
+ * A host's own "the app is going away" signal, for runtimes with no `document` to listen to.
666
1881
  *
667
- * JSON-stringifies the tuple so each token is unambiguously delimited
668
- * (the previous `tokens.join('-')` was collision-prone for tokens
669
- * containing a hyphen, e.g. `'e-mail'`).
1882
+ * In a browser the core flushes queued registrations itself, on `visibilitychange → hidden` and
1883
+ * `pagehide` (REG-3). React Native defines `window` but has no `document`, so those listeners
1884
+ * cannot exist there, and anything still queued when the app is backgrounded or killed would be
1885
+ * lost. `setTeardownSignal` lets the host supply the signal instead: the React Native binding
1886
+ * adapts `AppState` leaving `active`, and the core keeps the send.
670
1887
  *
671
- * Pure function — same inputs always produce the same id. Exported so
672
- * framework wrappers can compute the id themselves before deciding
673
- * whether to call `registerContentBlock`.
1888
+ * Same shape as `setPersistStorage`: a module-level injection that reaches every `Translations`
1889
+ * instance, those set up before it and after.
674
1890
  */
675
- declare function generateCustomId(category: string, tokens: string[]): string;
676
1891
  /**
677
- * The id this block would have had before the 0.6.0 MD5 fix.
678
- *
679
- * **Lookup only — never register under this.** Blocks registered by an older
680
- * SDK are keyed by it, so `Translate` falls back to it when the corrected id
681
- * misses, which keeps existing translations resolving instead of orphaning
682
- * them. Registering under it would keep minting ids from a hash that both
683
- * diverges across SDKs and can collide.
684
- *
685
- * Note the collision is a property of the FINAL hashed string, not the phrase:
686
- * `JSON.stringify` shifts every character's offset, so the same phrase pair can
687
- * collide standalone and not collide here — and changing `category` moves every
688
- * character into different lanes. Always reason at this level, not at `md5()`.
689
- *
690
- * @deprecated Migration aid; will be removed once catalogs have been rebased.
1892
+ * Subscribe `fire` to the host's teardown signal and return a function that unsubscribes it.
1893
+ * The core calls `fire` nothing else: firing it runs the ordinary teardown flush.
691
1894
  */
692
- declare function generateLegacyCustomId(category: string, tokens: string[]): string;
1895
+ type TeardownSubscribe = (fire: () => void) => (() => void) | void;
693
1896
  /**
694
- * True iff the local translations cache already has the content block
695
- * for the given `(category, customId)` — meaning the backend knows about
696
- * it and there's no need to re-register.
1897
+ * Inject the host's teardown signal, replacing any previous one. The previous subscription is
1898
+ * released first, so a re-injection — React Native's Fast Refresh, a test — never leaves two
1899
+ * live subscriptions that would flush twice. `null` releases the current one and injects none.
697
1900
  *
698
- * The cache is keyed under `'__uncategorized__'` for null/empty categories,
699
- * matching the convention the server uses in its GET /translations response.
1901
+ * Fire it once per departure, edge-triggered, as the app leaves the foreground: the flush sends
1902
+ * what is queued, and firing again while nothing is queued sends nothing.
700
1903
  */
701
- declare function isContentBlockKnown(category: string, customId: string): boolean;
1904
+ declare function setTeardownSignal(subscribe: TeardownSubscribe | null): void;
1905
+
702
1906
  /**
703
- * POST a content block to the backend, then stamp its custom_id into the
704
- * local translations cache so subsequent mounts in this session — and
705
- * subsequent reloads, since the cache is persisted — skip the POST.
1907
+ * Request scopes (spec SRV-7): rendering on a server without one visitor's
1908
+ * state reaching the next.
706
1909
  *
707
- * Mirrors the standalone-phrase path's cache writeback semantics. Respects
708
- * `key_type === 'write'`: read-only keys silently no-op (resolves with
709
- * `{ status: true }` so the caller can still render the cached translation).
1910
+ * The core keeps one catalog, one locale and one miss queue in module state,
1911
+ * which is right for a page and wrong for a server process rendering for many
1912
+ * visitors at once. A scope holds its own: its locale, its view of the catalog,
1913
+ * the misses its render records, and the seed its page hands the client. A
1914
+ * binding opens one in its framework's request hook, renders inside `run()`,
1915
+ * serialises `seed()` into the page, and calls `close()` after the response.
710
1916
  *
711
- * Errors are logged via the SDK's logger and returned as `{ status: false }`
712
- * — never thrown. Callers can decide whether to surface to UI or swallow.
1917
+ * A catalog is fetched at most once per request, and shared read-only (frozen)
1918
+ * with scopes rendering the same locale under the same credentials within a
1919
+ * minute, so a busy locale costs one fetch rather than one per visitor. Nothing a
1920
+ * scope writes is visible to another scope or to the page.
1921
+ *
1922
+ * A scope may carry its own project id and API key (SRV-2), for a process that
1923
+ * renders for more than one project: every request the scope makes goes out under
1924
+ * them, and a scope given none sends under the module-wide ones `init()` set.
713
1925
  */
714
- declare function registerContentBlock(contentBlock: iContentBlock): Promise<{
715
- status: boolean;
716
- errors?: unknown[];
717
- }>;
1926
+ interface RequestScopeOptions {
1927
+ /** The request's locale, as the framework or SRV-6 resolved it. */
1928
+ locale: string;
1929
+ /** A catalog already in hand (a snapshot, an upstream fetch); otherwise it is fetched. */
1930
+ catalog?: iCategories;
1931
+ /** The page's URL, for the records this scope keeps. */
1932
+ url?: string;
1933
+ /**
1934
+ * The project id and API key this scope's requests send under, with the write grant
1935
+ * that goes with them (SRV-2): its catalog fetch, its `close()`, and every request a
1936
+ * render inside it makes. Without them the scope sends under the module-wide ones.
1937
+ * Never part of `seed()`.
1938
+ *
1939
+ * Its key's write capability is the one its catalog fetch reports, so a scope handed
1940
+ * both a catalog and credentials of its own has no answer for its key and sends
1941
+ * nothing at `close()`; the client registers what it missed.
1942
+ */
1943
+ credentials?: ApiCredentials;
1944
+ }
1945
+ /** A phrase a scope's render missed. */
1946
+ interface ScopeMiss {
1947
+ category: string;
1948
+ phrase: string;
1949
+ }
1950
+ interface RequestScope {
1951
+ readonly locale: string;
1952
+ /** `t()` over this scope's catalog; its misses are this scope's. */
1953
+ readonly t: TFunction;
1954
+ /**
1955
+ * Run `fn` inside the scope: top-level `t()`, `LangsysApp.t`, `tSignal.get()`,
1956
+ * and the lookups and registrations `Translate` and `Phrase` make, resolve to
1957
+ * it. Across `await` only once `setRequestScopeStorage` has an
1958
+ * `AsyncLocalStorage`; otherwise only code that finishes inside `fn`.
1959
+ *
1960
+ * The `t` that `tSignal.get()` returns reads the catalog when it is CALLED, not
1961
+ * when it was taken: kept and called after `fn` returns, it reads the page's.
1962
+ * A host wrapping `t` for its components returns `scope.t`, which is this
1963
+ * scope's wherever it is called.
1964
+ */
1965
+ run<R>(fn: () => R): R;
1966
+ /**
1967
+ * Make the scope current for the rest of the current async context, where the
1968
+ * render cannot be wrapped in `run()`. Requires `setRequestScopeStorage` to
1969
+ * have been given an `AsyncLocalStorage`; throws otherwise.
1970
+ *
1971
+ * It holds only in the continuation that itself performs the render: called
1972
+ * inside a function the host awaits and returns from (a Nuxt plugin, a Nitro
1973
+ * request hook, an awaited helper), the scope is gone when that function
1974
+ * returns, before or after its own `await` alike. So call `enter()` in the
1975
+ * function that renders, `run()` everywhere else, and hand the scope to the
1976
+ * app through the framework's own injection when the host awaits you.
1977
+ */
1978
+ enter(): void;
1979
+ /**
1980
+ * The hydration seed (SRV-4): on the client, `LangsysApp.seedCatalog(seed.catalog,
1981
+ * seed.locale, seed)` before hydration. `blocks` names every block the scope
1982
+ * rendered through `renderBlock`, by id, with its category, source tokens and
1983
+ * shape, for a client that cannot recover a block's source from DOM already
1984
+ * holding the translation; hand each to `registerBlock` after `init()`.
1985
+ * `phrases` lists the phrases the render missed. A block or phrase the scope
1986
+ * sends itself at `close()` is marked `collected`, and the client never
1987
+ * registers it.
1988
+ */
1989
+ seed(): RequestSeed;
1990
+ /** The phrases this scope's render missed. */
1991
+ misses(): readonly ScopeMiss[];
1992
+ /**
1993
+ * After the response (SRV-3): send what the render missed, when the key the
1994
+ * scope sends under may write and the SSR strategy collects on the server,
1995
+ * then stop recording. Resolves what happened; never throws. Calling it
1996
+ * again resolves the first result.
1997
+ */
1998
+ close(): Promise<RegistrationResult>;
1999
+ }
718
2000
  /**
719
- * Walk an `HTMLElement`, collecting tokens for translation and producing a
720
- * cloned-and-stylized snapshot of its outerHTML for the Translation Manager.
721
- *
722
- * Tokens harvested:
723
- * - text-node content (whitespace-normalized, trimmed)
724
- * - translatable attribute values (`placeholder`, `alt`, `title`, `aria-*`, etc.)
725
- * - button/input/select value text
726
- *
727
- * Skips subtrees marked with `translate="no"`.
728
- *
729
- * The returned `content` is a string snapshot — the live element is not
730
- * mutated. Computed semantic styles (color, font, spacing, etc.) are inlined
731
- * onto the clone so translators see content styled the way end-users do.
732
- *
733
- * Requires that `element` is mounted in the document so `getComputedStyle`
734
- * can read its style. For SSR / pre-mount callers, the style capture step
735
- * silently no-ops and the snapshot is plain (without inlined styles).
2001
+ * Open a request scope. Resolves once its catalog is in hand: the one passed,
2002
+ * one fetched within the last minute for the same credentials and locale, or a
2003
+ * fresh fetch. A fetch that fails leaves the scope rendering source text and
2004
+ * recording nothing, as the page does while its catalog is unavailable
2005
+ * (WIRE-4). Credentials without both a project id and a key reject with
2006
+ * `LangsysConfigurationError`, before anything is sent.
736
2007
  */
737
- declare function tokenizeElement(element: HTMLElement): {
738
- tokens: string[];
739
- content: string;
740
- };
2008
+ declare function createRequestScope(options: RequestScopeOptions): Promise<RequestScope>;
741
2009
  /**
742
- * The token list this element would have produced before 0.6.3, when every
743
- * `<option>`'s text was harvested twice — once by a `<select>` special case and
744
- * again by the ordinary text-node walk.
2010
+ * The request scope a server rendered, rebuilt on the client from its seed, at
2011
+ * once and synchronously: for a host whose client render must run inside the
2012
+ * same scope as the server's (React's hydration render, which reads the server
2013
+ * snapshot), and whose provider cannot await. Its `t` reads the seed's catalog
2014
+ * in the seed's locale, so the hydration render produces the served text.
745
2015
  *
746
- * **Lookup only.** Content blocks registered by an older SDK are keyed by an id
747
- * derived from this list, so `Translate` falls back to it when the corrected id
748
- * misses; without that, every block containing a `<select>` would silently
749
- * re-key and lose its translations on upgrade. Registration always uses the
750
- * corrected list, so the legacy-keyed population can only shrink.
751
- *
752
- * Skips the computed-style capture that `tokenizeElement` performs — this is a
753
- * cache-key computation, not a snapshot for translators.
2016
+ * It sends nothing itself: the server's scope decided at its own `close()`. What
2017
+ * its render misses, and the blocks it registers, go to the page's lanes as a
2018
+ * render outside any scope would, which skip what the seed marks `collected`
2019
+ * (the seed's blocks and phrases are remembered here, as `seedCatalog` remembers
2020
+ * them), so under the `client` strategy the client still registers them.
754
2021
  *
755
- * @deprecated Migration aid; will be removed once catalogs have been rebased.
756
- */
757
- declare function legacyTokenizeElement(element: HTMLElement): string[];
758
-
759
- /**
760
- * Create a Signal whose value is persisted to localStorage (when available).
761
- * Falls back to in-memory behavior during SSR or when storage is unavailable
762
- * or quota-exceeded. Stored value is JSON-encoded.
2022
+ * A host that wraps `t` for its components returns `scope.t`, never a `t` taken
2023
+ * from `tSignal.get()` inside `run()`: that one reads the catalog when it is
2024
+ * called, and called later in the render, outside `run()`, it reads the page's.
763
2025
  */
764
- declare function persist<T>(key: string, initial: T): Signal<T>;
2026
+ declare function scopeFromSeed(seed: RequestSeed): RequestScope;
2027
+ /** The request scope the current code runs in, if any. */
2028
+ declare function currentRequestScope(): RequestScope | undefined;
2029
+ /** Forget every shared catalog, so the next scope for each locale fetches afresh. */
2030
+ declare function clearSharedCatalogs(): void;
765
2031
 
766
- declare const sTranslations: Signal<iCategories>;
767
- declare const currentlyLoadedLocale: Signal<string>;
768
-
769
- declare const isEmpty: (data: unknown) => boolean;
770
- /**
771
- * MD5 of a string, matching a standard UTF-8 MD5 for all input.
772
- *
773
- * Content-block `custom_id`s are derived from this, so it's a wire identity —
774
- * see `generateCustomId`. Before 0.6.0 this packed UTF-16 code units directly
775
- * into byte lanes, which agreed with a real MD5 only for ASCII: non-ASCII
776
- * input both diverged from every other Langsys SDK and could collide (a
777
- * character's high byte was OR-ed into the neighbouring lane, vanishing
778
- * whenever that lane already carried those bits — so `éa` and `ǩa` hashed
779
- * alike). `md5Legacy` preserves the old behavior for migration lookups only.
780
- */
781
- declare function md5(inputString: string): string;
2032
+ /** The ambient-context shape `AsyncLocalStorage` has, so the core never imports `node:async_hooks`. */
2033
+ interface ScopeStorage {
2034
+ getStore(): unknown;
2035
+ run<R>(store: unknown, fn: () => R): R;
2036
+ /** Makes a store current for the rest of the async context; needed only by `scope.enter()`. */
2037
+ enterWith?(store: unknown): void;
2038
+ }
782
2039
  /**
783
- * The pre-0.6.0 MD5, retained ONLY so existing content blocks stay resolvable:
784
- * ids minted by an older SDK are keyed by this. Lookup-only — never register
785
- * new content under it, or the colliding-id population keeps growing.
2040
+ * Hand the core an `AsyncLocalStorage` (or anything of its shape), so a scope
2041
+ * entered with `scope.run()` stays current across `await` inside the render.
2042
+ * Pass `null` to go back to synchronous nesting.
786
2043
  *
787
- * @deprecated Migration aid; will be removed once catalogs have been rebased.
2044
+ * The storage is opt-in, and what it guarantees depends on the host's async
2045
+ * model: under zone.js, for one, promises do not carry it across concurrent
2046
+ * renders. A binding may instead carry the scope through its own dependency
2047
+ * injection, one scope per request, and read `scope.t` and the scope's values
2048
+ * directly, with no storage at all.
788
2049
  */
789
- declare function md5Legacy(inputString: string): string;
2050
+ declare function setRequestScopeStorage(value: ScopeStorage | null): void;
790
2051
 
791
2052
  /**
792
- * Detects ICU MessageFormat syntax in a translation string.
793
- *
794
- * - Matches `{var, plural, …}`, `{var, select, …}`, `{var, selectordinal, …}`,
795
- * `{var, number, …}`, `{var, date, …}`, `{var, time, …}`.
796
- * - Also matches the style-less forms `{var, number}`, `{var, date}`,
797
- * `{var, time}` — valid ICU with the locale-default style.
798
- * - Doesn't match plain interpolation slots like `{name}` or `{count}`.
799
- *
800
- * Exported because the Translation Manager UI needs the same detection logic
801
- * to decide whether to render a plural-slot editor vs. a single text field.
2053
+ * Report, as a debug notice once per process per reason, that a unit registers
2054
+ * nothing because the text it holds came from a variable that cannot be named
2055
+ * (VAR-7): `register: false` on a DOM class, or a binding without its build-time
2056
+ * transform. Its catalogued translations still render. The reason names the cause,
2057
+ * such as the transform to enable.
802
2058
  */
803
- declare function isICU(template: string): boolean;
2059
+ declare function warnUnregistered(reason: string): void;
2060
+
804
2061
  /**
805
- * Normalize `%name%` markup placeholders to canonical `{name}`.
806
- *
807
- * Framework compilers consume bare `{name}` written in markup before the DOM
808
- * walker ever sees it (Svelte compiles it to an expression; JSX evaluates it),
809
- * so DOM content accepts `%name%` as a collision-free authoring escape.
810
- * Normalization runs at every markup capture boundary (content-block
811
- * tokenizer, `Translate` original-value snapshots, `Phrase` encoding), so the
812
- * catalog, the wire, and translators only ever see the canonical `{name}`
813
- * form — and plain `{name}` keeps working for vanilla-HTML authors.
814
- *
815
- * Keys must be identifiers (`[A-Za-z_][A-Za-z0-9_]*`), so literal `%` in
816
- * prose ("20% off", "50% to 60%") can't match. `t()` phrases are JS strings
817
- * with no compiler collision and stay `{name}`-only.
2062
+ * The catalog. Inside a request scope `get()` answers with the scope's catalog,
2063
+ * as `t()` does, so a binding that reads the catalog through this signal on a
2064
+ * server renders the visitor's, never another request's. `set`, `update` and
2065
+ * `subscribe` are the page's.
818
2066
  */
819
- declare function normalizeMarkupPlaceholders(text: string): string;
2067
+ declare const sTranslations: Signal<iCategories>;
820
2068
  /**
821
- * Params keys that have NO matching placeholder in `texts`.
822
- *
823
- * Texts are expected in canonical form (post-`normalizeMarkupPlaceholders`),
824
- * so a key counts as used when it appears as `{key}` or as the argument of an
825
- * ICU slot (`{key, plural, …}`). Surrounding whitespace is tolerated.
2069
+ * The locale the catalog is for. Inside a request scope `get()` answers with the
2070
+ * scope's locale (SRV-7), for the same reason as `sTranslations`.
826
2071
  */
827
- declare function findUnusedParamKeys(texts: string[], params: Record<string, unknown> | undefined): string[];
2072
+ declare const currentlyLoadedLocale: Signal<string>;
828
2073
  /**
829
- * Substitute placeholders in a translated string with values from `params`.
2074
+ * Server-computed write capability for THIS session. The single source of
2075
+ * truth for every write decision — never infer it from `key_type`, which
2076
+ * cannot express `ip_write` (read-only everywhere except from allow-listed
2077
+ * IPs, which is exactly how the discovery renderer registers content).
830
2078
  *
831
- * Two paths:
832
- * - ICU MessageFormat (target string contains `{var, plural|select|…, …}`):
833
- * parsed and formatted via `intl-messageformat`, which knows every
834
- * target locale's plural rules (Arabic's 6 categories, Russian's 4,
835
- * etc.) and gender-select branches.
836
- * - Simple `{name}` interpolation: cheap regex replacement. Unknown
837
- * placeholders are left untouched so missing data is visible to the
838
- * developer rather than silently rendering empty strings. Number and
839
- * Date values are formatted per the target locale's CLDR rules
840
- * (`Intl.NumberFormat` / `Intl.DateTimeFormat`) — pass numbers as
841
- * strings to opt out (e.g. IDs and codes that must not get grouping
842
- * separators).
2079
+ * Tri-state, and the three states are genuinely distinct:
2080
+ * - `undefined` — not yet authorized. HOLD: neither register nor hint.
2081
+ * Collapsing this to `false` would drop every miss that lands before
2082
+ * `validate()` resolves and fire a spurious discovery hint for it.
2083
+ * - `true` — register missing tokens (never hint).
2084
+ * - `false` — never attempt a write (no 403 churn); hint instead.
843
2085
  *
844
- * Malformed ICU falls through to simple interpolation rather than throwing,
845
- * so the developer sees a broken-but-visible string instead of a runtime crash.
846
- * Backend-side validation in langsys-ai is supposed to prevent this — the
847
- * fall-through is defense in depth.
2086
+ * Refreshed from `authorize-project` at init and from every catalog fetch.
848
2087
  *
849
- * `locale` is required for ICU formatting (drives plural-rule selection).
850
- * Falls back to `'en'` if absent — simple-interpolation path ignores it.
2088
+ * BROWSER-AUTHORITATIVE: only ever written when `window` exists. Write
2089
+ * capability is per-session (it varies by requester IP and by write grant),
2090
+ * while this module is a process-wide singleton — in a long-lived SSR server
2091
+ * shared across every request and every user, caching one request's answer
2092
+ * here would apply it to every later visitor. Under SSR it stays `undefined`,
2093
+ * which is the honest answer rather than a leaked one.
851
2094
  */
852
- declare function interpolate(template: string, params: Record<string, unknown>, locale?: string): string;
853
-
2095
+ declare const writeEnabled: Signal<boolean | undefined>;
854
2096
  /**
855
- * Locale-identifier handling, CLDR/BCP 47 flavored. Everything here rides on
856
- * the platform's native `Intl` — no bundled locale data.
2097
+ * Whether this API key is permitted to report page URLs for content discovery.
857
2098
  *
858
- * The SDK's convention: canonicalize once at every input boundary (init
859
- * config, user-locale store emissions, detectPreferredLocale, API routes) and
860
- * treat the canonical string as the identity everywhere downstream — cache
861
- * keys, equality checks, and the wire format all use it.
862
- */
863
- /**
864
- * Canonicalize a locale identifier to BCP 47 canonical form (the form CLDR
865
- * data is keyed by): language lowercase, script Titlecase, region uppercase —
866
- * `en-us` → `en-US`, `zh-hant-tw` → `zh-Hant-TW`. Underscores are accepted
867
- * and converted (`en_US` → `en-US`). Legacy aliases are resolved (`iw` → `he`,
868
- * `in` → `id`).
2099
+ * A POLICY, not a capability — deliberately independent of `writeEnabled`, and
2100
+ * the two must not be collapsed. `writeEnabled` answers "can this session write
2101
+ * from here"; this answers "may this key report at all". The canonical public
2102
+ * website is `writeEnabled: false` **and** `autoDiscovery: true` — read-only,
2103
+ * and permitted to report — which is exactly the case a single merged flag
2104
+ * would break.
2105
+ *
2106
+ * It exists because the SDK cannot tell whether it is running on a public
2107
+ * website. A mobile app holding a read key would otherwise report "page URLs"
2108
+ * that are not pages, and no heuristic settles it — our own URL heuristic is
2109
+ * already defeated by a routerless SPA. Beyond that, a customer may simply not
2110
+ * want visitors' page paths leaving the browser, whatever the SDK infers.
869
2111
  *
870
- * Structurally invalid tags fall back to best-effort manual casing rather
871
- * than throwing, so a bad config value degrades to a failed catalog lookup
872
- * instead of a crash.
2112
+ * Plain boolean rather than the tri-state `writeEnabled` uses, and false until
2113
+ * known. That is only safe because it is evaluated where the report is SENT
2114
+ * rather than where the miss is recorded: by then authorization has long since
2115
+ * resolved, and if it somehow hasn't, not reporting is the correct conservative
2116
+ * default. Checked at collection time instead, false-until-known would silently
2117
+ * drop early misses for keys that do permit reporting.
873
2118
  */
874
- declare function canonicalizeLocale(locale: string): string;
2119
+ declare const autoDiscovery: Signal<boolean | undefined>;
875
2120
 
876
2121
  interface iProject {
877
2122
  id: string;
@@ -892,5 +2137,29 @@ interface iProject {
892
2137
 
893
2138
  declare const t: TFunction;
894
2139
  declare const tSignal: Signal<TFunction>;
2140
+ /**
2141
+ * Tell the SDK the route changed (HINT-13). Call it from your router's after-navigation hook,
2142
+ * so content that stays mounted across routes — a persistent layout — is looked up again and
2143
+ * its misses are recorded for the new URL. Framework bindings wire this for you.
2144
+ */
2145
+ declare const notifyNavigation: () => void;
2146
+ /**
2147
+ * The header that asks your own API for the user's language (FRM-6):
2148
+ * `fetch(url, { headers: { ...localeHeaders() } })`. See `LangsysApp.localeHeaders`.
2149
+ */
2150
+ declare const localeHeaders: () => Record<string, string>;
2151
+ /**
2152
+ * Render a server message entry (MSG-5): its template through `t()` under the
2153
+ * messages category, or the entry's `message` when there is no translation.
2154
+ * Standalone alias for `LangsysApp.renderServerMessage`.
2155
+ */
2156
+ declare const renderServerMessage: (entry: ServerMessage, category?: string) => string;
2157
+ /**
2158
+ * Standalone alias for `LangsysApp.setWriteGrant` — supply the write grant
2159
+ * after `init()`, e.g. once the user logs in. Not the refresh path: the token
2160
+ * is resolved fresh before every request and never cached, so pass a provider
2161
+ * function and let your auth layer decide when to mint.
2162
+ */
2163
+ declare const setWriteGrant: (grant: WriteGrant | undefined) => Promise<void>;
895
2164
 
896
- export { type EncodedRichText, type ExtractParamKeys, LangsysApp, LangsysAppAPI, type LocaleSource, Logger, PHRASE_MARKER_ATTR, PHRASE_MARKER_ATTRS, type ParamPrimitive, type ParamsFor, Phrase, type PhraseOptions, type RichSlot, type Signal, type Subscriber, type TArgs, type TFunction, Translate, type TranslateOptions, type TranslationParams, Translations, type Unsubscriber, type Updater, canonicalizeLocale, createSignal, currentlyLoadedLocale, encodeRichText, findUnusedParamKeys, generateCustomId, generateLegacyCustomId, getValue, type iCategories, type iContentBlock, type iCountry, type iCountryDialCode, type iCountryList, type iCurrency, type iCurrencyList, type iLangsysConfig, type iLangsysInitConfig, type ResponseObject as iLangsysResponse, type iLanguageName, type iLocaleData, type iLocaleDefault, type iLocaleFlat, type iProject, type iTranslations, interpolate, isContentBlockKnown, isEmpty, isICU, isPhraseMarked, isTranslationExcluded, legacyTokenizeElement, logger, markupTokenValues, md5, md5Legacy, normalizeMarkupPlaceholders, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };
2165
+ export { type ApiCredentials, type BlockNode, type BlockOptions, type BlockShape, CatalogSnapshot, type EncodedRichText, LangsysApp, LangsysAppAPI, LangsysConfigurationError, LegacyKeyFile, type LocaleSource, Logger, ParamPrimitive, type PersistStorage, Phrase, type PhraseOptions, type RegistrationReason, type RegistrationResult, type RenderedBlock, type RenderedNode, type RequestScope, type RequestScopeOptions, type RequestSeed, type RichSlot, type ScopeMiss, type ScopeStorage, type SeededBlock, type SeededPhrase, type SendOptions, ServerMessage, type Signal, type Subscriber, TFunction, type TeardownSubscribe, Translate, type TranslateOptions, Translations, type Unsubscriber, type Updater, type WriteGrant, applyRendered, autoDiscovery, blockNodesOf, clearSharedCatalogs, createRequestScope, createSignal, currentRequestScope, currentlyLoadedLocale, encodeRichText, getValue, type iCategories, iContentBlock, type iCountry, type iCountryDialCode, type iCountryList, type iCurrency, type iCurrencyList, type iLangsysConfig, type iLangsysInitConfig, type ResponseObject as iLangsysResponse, type iLanguageName, type iLocaleData, type iLocaleDefault, type iLocaleFlat, type iProject, type iTranslations, isContentBlockKnown, isPhraseMarked, legacyTokenizeElement, localeHeaders, logger, markupTokenValues, notifyNavigation, persist, reconstitute, registerBlock, registerContentBlock, renderBlock, renderServerMessage, sTranslations, scopeFromSeed, serializeTree, setPersistStorage, setRequestScopeStorage, setTeardownSignal, setWriteGrant, stripSentinels, t, tSignal, tokenizeElement, tokenizeTree, warnUnregistered, warnUnrenderedBlock, writeEnabled };