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