langsys-js-typescript 0.4.0 → 0.4.2
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 +27 -0
- package/README.md +47 -0
- package/dist/index.d.mts +41 -1
- package/dist/index.d.ts +41 -1
- package/dist/index.js +69 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +68 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
## 0.4.2 - 2026-07-08
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **Debug-mode diagnostic for the `{key}`-in-markup mistake.** Writing `{name}` instead of `%name%` inside `<Translate>`/`<Phrase>` content fails silently — the framework compiler substitutes it before the SDK sees the text, so the base locale still renders correctly and only translated locales break. The SDK can't observe the original braces, but the fingerprint is unmistakable: `params` supplied with no matching placeholder in the captured content. When `debug` is on, that now emits a warning naming the offending key and the exact fix. The check re-runs only when the params key-SET changes, so a ticking counter doesn't spam the console, and it's silent in production.
|
|
6
|
+
- **`findUnusedParamKeys(texts, params)`** — the underlying detector, exported for wrappers that want the same check around their own rendering. Treats both `{key}` and ICU `{key, plural, …}` as used.
|
|
7
|
+
|
|
8
|
+
## 0.4.1 - 2026-07-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`%key%` — markup-safe placeholder spelling for DOM content.** Bare `{key}` written inside `<Translate>`/`<Phrase>` markup is consumed by framework compilers before the SDK ever sees it (Svelte compiles it to an expression; JSX evaluates it). Markup now also accepts `%key%`, which survives compilation as literal text. It's additive — plain `{key}` in vanilla HTML keeps working — and `%key%` is normalized to canonical `{key}` at every capture boundary (content-block tokenizer, `Translate` snapshots, `Phrase` encoding), so the Translation Manager, the wire payloads, and translators only ever see the `{key}` form. Keys are restricted to identifiers (`[A-Za-z_][A-Za-z0-9_]*`), so literal `%` in prose ("20% off", "50% to 60%") can never false-match. `t()` phrases are JS strings with no compiler collision and stay `{key}`-only.
|
|
13
|
+
- **`normalizeMarkupPlaceholders(text)`** — the normalizer, exported for framework wrappers that render content-block tokens through their own templating.
|
|
14
|
+
|
|
15
|
+
## 0.4.0 - 2026-07-08
|
|
16
|
+
|
|
17
|
+
Params interpolation for the `Translate` DOM class — content blocks can now carry runtime values the same way `t()` phrases do.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **`TranslateOptions.params`** (`Record<string, ParamPrimitive>`) — single-brace `{key}` placeholders, same syntax and semantics as `t()`: unknown keys fall through untouched so missing data stays visible, and Number/Date values are formatted per the active locale's CLDR rules. Interpolation applies everywhere resolved text lands in the DOM — content-block text nodes, translatable attributes, `<select>` option text, and the single-text-run path. Untranslated fallbacks interpolate too, and the base locale gets a render pass when params are present, so placeholders never leak into the UI.
|
|
22
|
+
- **`Translate.setParams(params)`** — swap params after mount (e.g. a changed count) and re-render, mirroring `Phrase.setParams`. The framework bindings expose this as a reactive `params` prop on their `<Translate>` components: `langsys-js-svelte` ≥ 3.3.0, `langsys-js-react` ≥ 0.4.0.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Content-block text replacement no longer treats `$`-patterns (`$&`, `` $` ``, `$1`…) in translations or interpolated values as `String.replace` directives — they now render literally.
|
|
27
|
+
|
|
1
28
|
## 0.3.0 - 2026-07-03
|
|
2
29
|
|
|
3
30
|
Full CLDR compliance, matching the backend's CLDR migration. Everything rides on the platform's native `Intl` APIs — no new dependencies, no bundled locale data.
|
package/README.md
CHANGED
|
@@ -158,6 +158,53 @@ Attributes honored on contained elements:
|
|
|
158
158
|
- Several validation-message `data-*` attributes
|
|
159
159
|
- `translate="no"` — elements marked this way (and their children) are skipped
|
|
160
160
|
|
|
161
|
+
### Interpolation params
|
|
162
|
+
|
|
163
|
+
`Translate` accepts the same single-brace `{key}` placeholders as `t()`. Pass `params` and every translated text node and attribute is interpolated after lookup — unknown keys stay visible as-is, and `Number`/`Date` values are formatted per the active locale's CLDR rules:
|
|
164
|
+
|
|
165
|
+
```html
|
|
166
|
+
<div id="stats">
|
|
167
|
+
<h2>Hello {name}</h2>
|
|
168
|
+
<p>You have {count} new messages.</p>
|
|
169
|
+
</div>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const stats = document.querySelector<HTMLElement>('#stats')!;
|
|
174
|
+
const handle = new Translate(stats, {
|
|
175
|
+
category: 'Dashboard',
|
|
176
|
+
params: { name: 'Sarah', count: 5 },
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Update reactively — e.g. when the count changes:
|
|
180
|
+
handle.setParams({ name: 'Sarah', count: 6 });
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The placeholders are part of the registered phrase, so translators see `{name}` and `{count}` and keep them in the translation.
|
|
184
|
+
|
|
185
|
+
#### `%key%` — the markup-safe spelling
|
|
186
|
+
|
|
187
|
+
In compiled frameworks, bare `{key}` written in markup never reaches the SDK — Svelte compiles it to an expression and JSX evaluates it. Content passed through `<Translate>`/`<Phrase>` therefore also accepts `%key%`, which survives compilation as literal text:
|
|
188
|
+
|
|
189
|
+
```svelte
|
|
190
|
+
<Translate category="Dashboard" params={{ name, count }}>
|
|
191
|
+
<h2>Hello %name%</h2>
|
|
192
|
+
<p>You have %count% new messages.</p>
|
|
193
|
+
</Translate>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Both spellings are equivalent: `%key%` is normalized to canonical `{key}` at tokenization, so the Translation Manager, the wire, and translators only ever see `{key}`, and existing `{key}` content in vanilla HTML keeps working unchanged. Keys must be identifiers (`[A-Za-z_][A-Za-z0-9_]*`), so literal `%` signs in prose ("20% off") are never touched. `t()` phrases are plain JS strings with no compiler in the way — they stay `{key}`-only.
|
|
197
|
+
|
|
198
|
+
Writing `{key}` in markup fails *silently* — the base locale still looks right, so the mistake surfaces only once someone switches language. With `debug: true`, the SDK catches it for you: if you pass `params` whose keys have no matching placeholder in the captured content, it warns and names the fix.
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
Langsys Warning <Translate> received params with no matching placeholder in its
|
|
202
|
+
content: %count%. If you wrote {count} in markup, the framework compiler
|
|
203
|
+
(Svelte/JSX) substituted it before Langsys saw the text — write %count% instead.
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The check runs whenever the params key-set changes, so a ticking `count` won't spam the console, and it's silent in production.
|
|
207
|
+
|
|
161
208
|
## Server-Side Rendering
|
|
162
209
|
|
|
163
210
|
Pre-fetch translations on the server and seed them through `initialTranslations` to skip the duplicate client fetch on hydration:
|
package/dist/index.d.mts
CHANGED
|
@@ -400,6 +400,8 @@ declare class Translate {
|
|
|
400
400
|
private lastTranslatedLocale;
|
|
401
401
|
private custom_id;
|
|
402
402
|
private unsubscribeLocale;
|
|
403
|
+
/** Sorted params key-set already checked, so value-only updates don't re-warn. */
|
|
404
|
+
private checkedParamKeys;
|
|
403
405
|
constructor(element: HTMLElement, options?: TranslateOptions);
|
|
404
406
|
/** Update interpolation params (e.g. a changed count) and re-render. Mirrors `Phrase.setParams`. */
|
|
405
407
|
setParams(params: Record<string, ParamPrimitive> | undefined): void;
|
|
@@ -410,6 +412,12 @@ declare class Translate {
|
|
|
410
412
|
private handleContentBlock;
|
|
411
413
|
private translate;
|
|
412
414
|
private getTranslation;
|
|
415
|
+
/**
|
|
416
|
+
* Debug-time check that every supplied param has a placeholder to land in.
|
|
417
|
+
* Re-runs only when the key SET changes — a changing `count` value must not
|
|
418
|
+
* re-warn on every render.
|
|
419
|
+
*/
|
|
420
|
+
private checkParams;
|
|
413
421
|
/**
|
|
414
422
|
* Interpolate `{name}`-style params into a resolved text, matching `t()`:
|
|
415
423
|
* unknown keys fall through untouched, and the untranslated fallback is
|
|
@@ -448,12 +456,20 @@ declare class Phrase {
|
|
|
448
456
|
private slots;
|
|
449
457
|
private unsubscribers;
|
|
450
458
|
private ready;
|
|
459
|
+
/** Sorted params key-set already checked, so value-only updates don't re-warn. */
|
|
460
|
+
private checkedParamKeys;
|
|
451
461
|
constructor(host: HTMLElement, options?: PhraseOptions);
|
|
452
462
|
/** Update interpolation params (e.g. a changed count) and re-render. */
|
|
453
463
|
setParams(params: Record<string, unknown>): void;
|
|
454
464
|
/** Stop reacting to locale/translation changes. Safe to call multiple times. */
|
|
455
465
|
destroy(): void;
|
|
456
466
|
private _init;
|
|
467
|
+
/**
|
|
468
|
+
* Debug-time check that every supplied param has a placeholder to land in.
|
|
469
|
+
* Re-runs only when the key SET changes — a changing `n` value must not
|
|
470
|
+
* re-warn on every render.
|
|
471
|
+
*/
|
|
472
|
+
private _checkParams;
|
|
457
473
|
private _render;
|
|
458
474
|
}
|
|
459
475
|
|
|
@@ -670,6 +686,30 @@ declare function md5(inputString: string): string;
|
|
|
670
686
|
* to decide whether to render a plural-slot editor vs. a single text field.
|
|
671
687
|
*/
|
|
672
688
|
declare function isICU(template: string): boolean;
|
|
689
|
+
/**
|
|
690
|
+
* Normalize `%name%` markup placeholders to canonical `{name}`.
|
|
691
|
+
*
|
|
692
|
+
* Framework compilers consume bare `{name}` written in markup before the DOM
|
|
693
|
+
* walker ever sees it (Svelte compiles it to an expression; JSX evaluates it),
|
|
694
|
+
* so DOM content accepts `%name%` as a collision-free authoring escape.
|
|
695
|
+
* Normalization runs at every markup capture boundary (content-block
|
|
696
|
+
* tokenizer, `Translate` original-value snapshots, `Phrase` encoding), so the
|
|
697
|
+
* catalog, the wire, and translators only ever see the canonical `{name}`
|
|
698
|
+
* form — and plain `{name}` keeps working for vanilla-HTML authors.
|
|
699
|
+
*
|
|
700
|
+
* Keys must be identifiers (`[A-Za-z_][A-Za-z0-9_]*`), so literal `%` in
|
|
701
|
+
* prose ("20% off", "50% to 60%") can't match. `t()` phrases are JS strings
|
|
702
|
+
* with no compiler collision and stay `{name}`-only.
|
|
703
|
+
*/
|
|
704
|
+
declare function normalizeMarkupPlaceholders(text: string): string;
|
|
705
|
+
/**
|
|
706
|
+
* Params keys that have NO matching placeholder in `texts`.
|
|
707
|
+
*
|
|
708
|
+
* Texts are expected in canonical form (post-`normalizeMarkupPlaceholders`),
|
|
709
|
+
* so a key counts as used when it appears as `{key}` or as the argument of an
|
|
710
|
+
* ICU slot (`{key, plural, …}`). Surrounding whitespace is tolerated.
|
|
711
|
+
*/
|
|
712
|
+
declare function findUnusedParamKeys(texts: string[], params: Record<string, unknown> | undefined): string[];
|
|
673
713
|
/**
|
|
674
714
|
* Substitute placeholders in a translated string with values from `params`.
|
|
675
715
|
*
|
|
@@ -738,4 +778,4 @@ interface iProject {
|
|
|
738
778
|
declare const t: TFunction;
|
|
739
779
|
declare const tSignal: Signal<TFunction>;
|
|
740
780
|
|
|
741
|
-
export { type EncodedRichText, type ExtractParamKeys, LangsysApp, LangsysAppAPI, type LocaleSource, Logger, PHRASE_MARKER_ATTR, 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, generateCustomId, 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, logger, markupTokenValues, md5, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };
|
|
781
|
+
export { type EncodedRichText, type ExtractParamKeys, LangsysApp, LangsysAppAPI, type LocaleSource, Logger, PHRASE_MARKER_ATTR, 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, 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, logger, markupTokenValues, md5, normalizeMarkupPlaceholders, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };
|
package/dist/index.d.ts
CHANGED
|
@@ -400,6 +400,8 @@ declare class Translate {
|
|
|
400
400
|
private lastTranslatedLocale;
|
|
401
401
|
private custom_id;
|
|
402
402
|
private unsubscribeLocale;
|
|
403
|
+
/** Sorted params key-set already checked, so value-only updates don't re-warn. */
|
|
404
|
+
private checkedParamKeys;
|
|
403
405
|
constructor(element: HTMLElement, options?: TranslateOptions);
|
|
404
406
|
/** Update interpolation params (e.g. a changed count) and re-render. Mirrors `Phrase.setParams`. */
|
|
405
407
|
setParams(params: Record<string, ParamPrimitive> | undefined): void;
|
|
@@ -410,6 +412,12 @@ declare class Translate {
|
|
|
410
412
|
private handleContentBlock;
|
|
411
413
|
private translate;
|
|
412
414
|
private getTranslation;
|
|
415
|
+
/**
|
|
416
|
+
* Debug-time check that every supplied param has a placeholder to land in.
|
|
417
|
+
* Re-runs only when the key SET changes — a changing `count` value must not
|
|
418
|
+
* re-warn on every render.
|
|
419
|
+
*/
|
|
420
|
+
private checkParams;
|
|
413
421
|
/**
|
|
414
422
|
* Interpolate `{name}`-style params into a resolved text, matching `t()`:
|
|
415
423
|
* unknown keys fall through untouched, and the untranslated fallback is
|
|
@@ -448,12 +456,20 @@ declare class Phrase {
|
|
|
448
456
|
private slots;
|
|
449
457
|
private unsubscribers;
|
|
450
458
|
private ready;
|
|
459
|
+
/** Sorted params key-set already checked, so value-only updates don't re-warn. */
|
|
460
|
+
private checkedParamKeys;
|
|
451
461
|
constructor(host: HTMLElement, options?: PhraseOptions);
|
|
452
462
|
/** Update interpolation params (e.g. a changed count) and re-render. */
|
|
453
463
|
setParams(params: Record<string, unknown>): void;
|
|
454
464
|
/** Stop reacting to locale/translation changes. Safe to call multiple times. */
|
|
455
465
|
destroy(): void;
|
|
456
466
|
private _init;
|
|
467
|
+
/**
|
|
468
|
+
* Debug-time check that every supplied param has a placeholder to land in.
|
|
469
|
+
* Re-runs only when the key SET changes — a changing `n` value must not
|
|
470
|
+
* re-warn on every render.
|
|
471
|
+
*/
|
|
472
|
+
private _checkParams;
|
|
457
473
|
private _render;
|
|
458
474
|
}
|
|
459
475
|
|
|
@@ -670,6 +686,30 @@ declare function md5(inputString: string): string;
|
|
|
670
686
|
* to decide whether to render a plural-slot editor vs. a single text field.
|
|
671
687
|
*/
|
|
672
688
|
declare function isICU(template: string): boolean;
|
|
689
|
+
/**
|
|
690
|
+
* Normalize `%name%` markup placeholders to canonical `{name}`.
|
|
691
|
+
*
|
|
692
|
+
* Framework compilers consume bare `{name}` written in markup before the DOM
|
|
693
|
+
* walker ever sees it (Svelte compiles it to an expression; JSX evaluates it),
|
|
694
|
+
* so DOM content accepts `%name%` as a collision-free authoring escape.
|
|
695
|
+
* Normalization runs at every markup capture boundary (content-block
|
|
696
|
+
* tokenizer, `Translate` original-value snapshots, `Phrase` encoding), so the
|
|
697
|
+
* catalog, the wire, and translators only ever see the canonical `{name}`
|
|
698
|
+
* form — and plain `{name}` keeps working for vanilla-HTML authors.
|
|
699
|
+
*
|
|
700
|
+
* Keys must be identifiers (`[A-Za-z_][A-Za-z0-9_]*`), so literal `%` in
|
|
701
|
+
* prose ("20% off", "50% to 60%") can't match. `t()` phrases are JS strings
|
|
702
|
+
* with no compiler collision and stay `{name}`-only.
|
|
703
|
+
*/
|
|
704
|
+
declare function normalizeMarkupPlaceholders(text: string): string;
|
|
705
|
+
/**
|
|
706
|
+
* Params keys that have NO matching placeholder in `texts`.
|
|
707
|
+
*
|
|
708
|
+
* Texts are expected in canonical form (post-`normalizeMarkupPlaceholders`),
|
|
709
|
+
* so a key counts as used when it appears as `{key}` or as the argument of an
|
|
710
|
+
* ICU slot (`{key, plural, …}`). Surrounding whitespace is tolerated.
|
|
711
|
+
*/
|
|
712
|
+
declare function findUnusedParamKeys(texts: string[], params: Record<string, unknown> | undefined): string[];
|
|
673
713
|
/**
|
|
674
714
|
* Substitute placeholders in a translated string with values from `params`.
|
|
675
715
|
*
|
|
@@ -738,4 +778,4 @@ interface iProject {
|
|
|
738
778
|
declare const t: TFunction;
|
|
739
779
|
declare const tSignal: Signal<TFunction>;
|
|
740
780
|
|
|
741
|
-
export { type EncodedRichText, type ExtractParamKeys, LangsysApp, LangsysAppAPI, type LocaleSource, Logger, PHRASE_MARKER_ATTR, 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, generateCustomId, 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, logger, markupTokenValues, md5, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };
|
|
781
|
+
export { type EncodedRichText, type ExtractParamKeys, LangsysApp, LangsysAppAPI, type LocaleSource, Logger, PHRASE_MARKER_ATTR, 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, 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, logger, markupTokenValues, md5, normalizeMarkupPlaceholders, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };
|
package/dist/index.js
CHANGED
|
@@ -265,6 +265,28 @@ function isICU(template) {
|
|
|
265
265
|
return ICU_PATTERN.test(template);
|
|
266
266
|
}
|
|
267
267
|
var ICU_PATTERN = /\{[^{}]+,\s*(plural|select|selectordinal|number|date|time)\s*[,}]/;
|
|
268
|
+
function normalizeMarkupPlaceholders(text) {
|
|
269
|
+
return text.replace(/%([A-Za-z_][A-Za-z0-9_]*)%/g, "{$1}");
|
|
270
|
+
}
|
|
271
|
+
function escapeForRegExp(key) {
|
|
272
|
+
return key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
273
|
+
}
|
|
274
|
+
function findUnusedParamKeys(texts, params) {
|
|
275
|
+
if (!params) return [];
|
|
276
|
+
const keys = Object.keys(params);
|
|
277
|
+
if (!keys.length || !texts.length) return keys;
|
|
278
|
+
const haystack = texts.join("\0");
|
|
279
|
+
return keys.filter((key) => !new RegExp(`\\{\\s*${escapeForRegExp(key)}\\s*[,}]`).test(haystack));
|
|
280
|
+
}
|
|
281
|
+
function warnUnmatchedParams(source, texts, params, context) {
|
|
282
|
+
if (!logger.debugEnabled) return;
|
|
283
|
+
const unused = findUnusedParamKeys(texts, params);
|
|
284
|
+
if (!unused.length) return;
|
|
285
|
+
const keyList = unused.map((key) => `%${key}%`).join(", ");
|
|
286
|
+
logger.warn(
|
|
287
|
+
`${source} received params with no matching placeholder in its content: ${keyList}. If you wrote {${unused[0]}} in markup, the framework compiler (Svelte/JSX) substituted it before Langsys saw the text \u2014 write %${unused[0]}% instead.` + (context ? ` [${context}]` : "")
|
|
288
|
+
);
|
|
289
|
+
}
|
|
268
290
|
function interpolate(template, params, locale) {
|
|
269
291
|
if (isICU(template)) {
|
|
270
292
|
try {
|
|
@@ -1141,7 +1163,7 @@ function tokenizeElement(element) {
|
|
|
1141
1163
|
const tokens = [];
|
|
1142
1164
|
const clone = element.cloneNode(true);
|
|
1143
1165
|
_walkForTokens(element, Array.from(clone.childNodes), tokens, []);
|
|
1144
|
-
return { tokens, content: clone.outerHTML };
|
|
1166
|
+
return { tokens, content: normalizeMarkupPlaceholders(clone.outerHTML) };
|
|
1145
1167
|
}
|
|
1146
1168
|
function _writeKnownContentBlockToCache(category, customId) {
|
|
1147
1169
|
const lookupCat = category || "__uncategorized__";
|
|
@@ -1171,7 +1193,7 @@ function _walkForTokens(liveRoot, cloneNodes, tokens, indices) {
|
|
|
1171
1193
|
}
|
|
1172
1194
|
const contentToken = node.nodeValue?.replace(/\s+/g, " ").trim();
|
|
1173
1195
|
if (node.nodeType === Node.TEXT_NODE && contentToken) {
|
|
1174
|
-
tokens.push(contentToken);
|
|
1196
|
+
tokens.push(normalizeMarkupPlaceholders(contentToken));
|
|
1175
1197
|
return;
|
|
1176
1198
|
}
|
|
1177
1199
|
if (!node.hasChildNodes()) return;
|
|
@@ -1186,24 +1208,24 @@ function _tokenizeAttributes(element, tokens) {
|
|
|
1186
1208
|
}
|
|
1187
1209
|
for (const attr of TRANSLATABLE_ATTRIBUTES) {
|
|
1188
1210
|
const value = element.getAttribute(attr)?.trim();
|
|
1189
|
-
if (value) tokens.push(value);
|
|
1211
|
+
if (value) tokens.push(normalizeMarkupPlaceholders(value));
|
|
1190
1212
|
}
|
|
1191
1213
|
if (VALUE_TRANSLATABLE_ELEMENTS.includes(tagName)) {
|
|
1192
1214
|
const value = element.getAttribute("value")?.trim();
|
|
1193
|
-
if (value) tokens.push(value);
|
|
1215
|
+
if (value) tokens.push(normalizeMarkupPlaceholders(value));
|
|
1194
1216
|
}
|
|
1195
1217
|
if (tagName === "input") {
|
|
1196
1218
|
const inputType = element.getAttribute("type")?.toLowerCase();
|
|
1197
1219
|
if (inputType && VALUE_TRANSLATABLE_INPUT_TYPES.includes(inputType)) {
|
|
1198
1220
|
const value = element.getAttribute("value")?.trim();
|
|
1199
|
-
if (value) tokens.push(value);
|
|
1221
|
+
if (value) tokens.push(normalizeMarkupPlaceholders(value));
|
|
1200
1222
|
}
|
|
1201
1223
|
}
|
|
1202
1224
|
if (tagName === "select") {
|
|
1203
1225
|
const options = element.querySelectorAll("option");
|
|
1204
1226
|
options.forEach((option) => {
|
|
1205
1227
|
const optionText = option.textContent?.trim();
|
|
1206
|
-
if (optionText) tokens.push(optionText);
|
|
1228
|
+
if (optionText) tokens.push(normalizeMarkupPlaceholders(optionText));
|
|
1207
1229
|
});
|
|
1208
1230
|
}
|
|
1209
1231
|
}
|
|
@@ -1245,6 +1267,8 @@ var Translate = class {
|
|
|
1245
1267
|
this.isTokenizing = false;
|
|
1246
1268
|
this.lastTranslatedLocale = "";
|
|
1247
1269
|
this.unsubscribeLocale = null;
|
|
1270
|
+
/** Sorted params key-set already checked, so value-only updates don't re-warn. */
|
|
1271
|
+
this.checkedParamKeys = null;
|
|
1248
1272
|
this.element = element;
|
|
1249
1273
|
this.options = { ...options };
|
|
1250
1274
|
this.custom_id = options.custom_id || "";
|
|
@@ -1256,6 +1280,7 @@ var Translate = class {
|
|
|
1256
1280
|
/** Update interpolation params (e.g. a changed count) and re-render. Mirrors `Phrase.setParams`. */
|
|
1257
1281
|
setParams(params) {
|
|
1258
1282
|
this.options.params = params;
|
|
1283
|
+
this.checkParams();
|
|
1259
1284
|
if (!this.parseComplete) return;
|
|
1260
1285
|
this.lastTranslatedLocale = "";
|
|
1261
1286
|
const locale = currentlyLoadedLocale.get();
|
|
@@ -1289,6 +1314,7 @@ var Translate = class {
|
|
|
1289
1314
|
this.isTokenizing = true;
|
|
1290
1315
|
const { tokens, content } = tokenizeElement(this.element);
|
|
1291
1316
|
this.tokens = tokens;
|
|
1317
|
+
this.checkParams();
|
|
1292
1318
|
const { category = "" } = this.options;
|
|
1293
1319
|
if (this.tokens.length === 1) {
|
|
1294
1320
|
this.element.innerText = this.applyParams(LangsysApp.Translations.t(this.tokens[0], category));
|
|
@@ -1344,7 +1370,7 @@ var Translate = class {
|
|
|
1344
1370
|
}
|
|
1345
1371
|
if (isEmpty(node.nodeValue?.trim())) return;
|
|
1346
1372
|
if (isEmpty(node.originalNodeValue)) {
|
|
1347
|
-
node.originalNodeValue = node.nodeValue;
|
|
1373
|
+
node.originalNodeValue = normalizeMarkupPlaceholders(node.nodeValue);
|
|
1348
1374
|
}
|
|
1349
1375
|
const contentToken = node.originalNodeValue?.replace(/\s+/g, " ").trim();
|
|
1350
1376
|
if (!contentToken) return;
|
|
@@ -1362,6 +1388,19 @@ var Translate = class {
|
|
|
1362
1388
|
const { category = "" } = this.options;
|
|
1363
1389
|
return LangsysApp.Translations.lookupContent(category, this.custom_id, token);
|
|
1364
1390
|
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Debug-time check that every supplied param has a placeholder to land in.
|
|
1393
|
+
* Re-runs only when the key SET changes — a changing `count` value must not
|
|
1394
|
+
* re-warn on every render.
|
|
1395
|
+
*/
|
|
1396
|
+
checkParams() {
|
|
1397
|
+
if (!this.tokens.length) return;
|
|
1398
|
+
const { params, category = "" } = this.options;
|
|
1399
|
+
const signature = params ? Object.keys(params).sort().join(",") : "";
|
|
1400
|
+
if (signature === this.checkedParamKeys) return;
|
|
1401
|
+
this.checkedParamKeys = signature;
|
|
1402
|
+
warnUnmatchedParams("<Translate>", this.tokens, params, category || void 0);
|
|
1403
|
+
}
|
|
1365
1404
|
/**
|
|
1366
1405
|
* Interpolate `{name}`-style params into a resolved text, matching `t()`:
|
|
1367
1406
|
* unknown keys fall through untouched, and the untranslated fallback is
|
|
@@ -1393,7 +1432,7 @@ var Translate = class {
|
|
|
1393
1432
|
const optionEl = option;
|
|
1394
1433
|
if (!optionEl.originalAttributes) optionEl.originalAttributes = {};
|
|
1395
1434
|
if (optionEl.originalAttributes["textContent"] === void 0) {
|
|
1396
|
-
optionEl.originalAttributes["textContent"] = option.textContent || "";
|
|
1435
|
+
optionEl.originalAttributes["textContent"] = normalizeMarkupPlaceholders(option.textContent || "");
|
|
1397
1436
|
}
|
|
1398
1437
|
const originalText = optionEl.originalAttributes["textContent"].trim();
|
|
1399
1438
|
if (originalText) {
|
|
@@ -1407,7 +1446,7 @@ var Translate = class {
|
|
|
1407
1446
|
const currentValue = element.getAttribute(attr);
|
|
1408
1447
|
if (!currentValue) return;
|
|
1409
1448
|
if (element.originalAttributes[attr] === void 0) {
|
|
1410
|
-
element.originalAttributes[attr] = currentValue;
|
|
1449
|
+
element.originalAttributes[attr] = normalizeMarkupPlaceholders(currentValue);
|
|
1411
1450
|
}
|
|
1412
1451
|
const originalValue = element.originalAttributes[attr].trim();
|
|
1413
1452
|
if (!originalValue) return;
|
|
@@ -1426,7 +1465,7 @@ var SENT_SCAN = /\uE000(\d+)\uE001|\uE002(\d+)\uE003/g;
|
|
|
1426
1465
|
function encodeRichText(root) {
|
|
1427
1466
|
const slots = [];
|
|
1428
1467
|
const phrase = _encodeNodes(Array.from(root.childNodes), slots);
|
|
1429
|
-
return { phrase: phrase.replace(/\s+/g, " ").trim(), slots };
|
|
1468
|
+
return { phrase: normalizeMarkupPlaceholders(phrase.replace(/\s+/g, " ").trim()), slots };
|
|
1430
1469
|
}
|
|
1431
1470
|
function _encodeNodes(nodes, slots) {
|
|
1432
1471
|
let out = "";
|
|
@@ -1496,6 +1535,8 @@ var Phrase = class {
|
|
|
1496
1535
|
this.slots = [];
|
|
1497
1536
|
this.unsubscribers = [];
|
|
1498
1537
|
this.ready = false;
|
|
1538
|
+
/** Sorted params key-set already checked, so value-only updates don't re-warn. */
|
|
1539
|
+
this.checkedParamKeys = null;
|
|
1499
1540
|
this.host = host;
|
|
1500
1541
|
this.category = options.category ?? "";
|
|
1501
1542
|
this.params = options.params ?? {};
|
|
@@ -1509,7 +1550,9 @@ var Phrase = class {
|
|
|
1509
1550
|
/** Update interpolation params (e.g. a changed count) and re-render. */
|
|
1510
1551
|
setParams(params) {
|
|
1511
1552
|
this.params = params ?? {};
|
|
1512
|
-
|
|
1553
|
+
this._checkParams();
|
|
1554
|
+
if (!this.ready) return;
|
|
1555
|
+
this._render();
|
|
1513
1556
|
}
|
|
1514
1557
|
/** Stop reacting to locale/translation changes. Safe to call multiple times. */
|
|
1515
1558
|
destroy() {
|
|
@@ -1524,11 +1567,24 @@ var Phrase = class {
|
|
|
1524
1567
|
const { phrase, slots } = encodeRichText(this.host);
|
|
1525
1568
|
this.phrase = phrase;
|
|
1526
1569
|
this.slots = slots;
|
|
1570
|
+
this._checkParams();
|
|
1527
1571
|
await LangsysApp.Translations.ready();
|
|
1528
1572
|
LangsysApp.Translations.t(this.phrase, this.category);
|
|
1529
1573
|
this.ready = true;
|
|
1530
1574
|
this._render();
|
|
1531
1575
|
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Debug-time check that every supplied param has a placeholder to land in.
|
|
1578
|
+
* Re-runs only when the key SET changes — a changing `n` value must not
|
|
1579
|
+
* re-warn on every render.
|
|
1580
|
+
*/
|
|
1581
|
+
_checkParams() {
|
|
1582
|
+
if (!this.phrase) return;
|
|
1583
|
+
const signature = Object.keys(this.params).sort().join(",");
|
|
1584
|
+
if (signature === this.checkedParamKeys) return;
|
|
1585
|
+
this.checkedParamKeys = signature;
|
|
1586
|
+
warnUnmatchedParams("<Phrase>", [this.phrase], this.params, this.category || void 0);
|
|
1587
|
+
}
|
|
1532
1588
|
_render() {
|
|
1533
1589
|
if (!this.phrase) return;
|
|
1534
1590
|
const raw = LangsysApp.Translations.lookup(this.phrase, this.category) ?? this.phrase;
|
|
@@ -1556,6 +1612,7 @@ exports.canonicalizeLocale = canonicalizeLocale;
|
|
|
1556
1612
|
exports.createSignal = createSignal;
|
|
1557
1613
|
exports.currentlyLoadedLocale = currentlyLoadedLocale;
|
|
1558
1614
|
exports.encodeRichText = encodeRichText;
|
|
1615
|
+
exports.findUnusedParamKeys = findUnusedParamKeys;
|
|
1559
1616
|
exports.generateCustomId = generateCustomId;
|
|
1560
1617
|
exports.getValue = getValue;
|
|
1561
1618
|
exports.interpolate = interpolate;
|
|
@@ -1565,6 +1622,7 @@ exports.isICU = isICU;
|
|
|
1565
1622
|
exports.logger = logger;
|
|
1566
1623
|
exports.markupTokenValues = markupTokenValues;
|
|
1567
1624
|
exports.md5 = md5;
|
|
1625
|
+
exports.normalizeMarkupPlaceholders = normalizeMarkupPlaceholders;
|
|
1568
1626
|
exports.persist = persist;
|
|
1569
1627
|
exports.reconstitute = reconstitute;
|
|
1570
1628
|
exports.registerContentBlock = registerContentBlock;
|