langsys-js-typescript 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +37 -0
- package/dist/index.d.mts +27 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.js +41 -18
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +41 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## 0.4.1 - 2026-07-08
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **`%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.
|
|
6
|
+
- **`normalizeMarkupPlaceholders(text)`** — the normalizer, exported for framework wrappers that render content-block tokens through their own templating.
|
|
7
|
+
|
|
8
|
+
## 0.4.0 - 2026-07-08
|
|
9
|
+
|
|
10
|
+
Params interpolation for the `Translate` DOM class — content blocks can now carry runtime values the same way `t()` phrases do.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`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.
|
|
15
|
+
- **`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.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Content-block text replacement no longer treats `$`-patterns (`$&`, `` $` ``, `$1`…) in translations or interpolated values as `String.replace` directives — they now render literally.
|
|
20
|
+
|
|
1
21
|
## 0.3.0 - 2026-07-03
|
|
2
22
|
|
|
3
23
|
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,43 @@ 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
|
+
|
|
161
198
|
## Server-Side Rendering
|
|
162
199
|
|
|
163
200
|
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
|
@@ -375,6 +375,8 @@ interface TranslateOptions {
|
|
|
375
375
|
custom_id?: string;
|
|
376
376
|
/** Optional human-readable label shown in the Translation Manager. */
|
|
377
377
|
label?: string;
|
|
378
|
+
/** Interpolation params — `{name}`, `{count}`, etc. Same single-brace syntax as `t()`. */
|
|
379
|
+
params?: Record<string, ParamPrimitive>;
|
|
378
380
|
}
|
|
379
381
|
/**
|
|
380
382
|
* Wrap a DOM element and manage translation for its contents.
|
|
@@ -399,6 +401,8 @@ declare class Translate {
|
|
|
399
401
|
private custom_id;
|
|
400
402
|
private unsubscribeLocale;
|
|
401
403
|
constructor(element: HTMLElement, options?: TranslateOptions);
|
|
404
|
+
/** Update interpolation params (e.g. a changed count) and re-render. Mirrors `Phrase.setParams`. */
|
|
405
|
+
setParams(params: Record<string, ParamPrimitive> | undefined): void;
|
|
402
406
|
/** Stop reacting to locale changes. Safe to call multiple times. */
|
|
403
407
|
destroy(): void;
|
|
404
408
|
private translateUpdate;
|
|
@@ -406,6 +410,12 @@ declare class Translate {
|
|
|
406
410
|
private handleContentBlock;
|
|
407
411
|
private translate;
|
|
408
412
|
private getTranslation;
|
|
413
|
+
/**
|
|
414
|
+
* Interpolate `{name}`-style params into a resolved text, matching `t()`:
|
|
415
|
+
* unknown keys fall through untouched, and the untranslated fallback is
|
|
416
|
+
* interpolated too. No-op when no params were supplied.
|
|
417
|
+
*/
|
|
418
|
+
private applyParams;
|
|
409
419
|
private translateAttributes;
|
|
410
420
|
private translateAttribute;
|
|
411
421
|
}
|
|
@@ -660,6 +670,22 @@ declare function md5(inputString: string): string;
|
|
|
660
670
|
* to decide whether to render a plural-slot editor vs. a single text field.
|
|
661
671
|
*/
|
|
662
672
|
declare function isICU(template: string): boolean;
|
|
673
|
+
/**
|
|
674
|
+
* Normalize `%name%` markup placeholders to canonical `{name}`.
|
|
675
|
+
*
|
|
676
|
+
* Framework compilers consume bare `{name}` written in markup before the DOM
|
|
677
|
+
* walker ever sees it (Svelte compiles it to an expression; JSX evaluates it),
|
|
678
|
+
* so DOM content accepts `%name%` as a collision-free authoring escape.
|
|
679
|
+
* Normalization runs at every markup capture boundary (content-block
|
|
680
|
+
* tokenizer, `Translate` original-value snapshots, `Phrase` encoding), so the
|
|
681
|
+
* catalog, the wire, and translators only ever see the canonical `{name}`
|
|
682
|
+
* form — and plain `{name}` keeps working for vanilla-HTML authors.
|
|
683
|
+
*
|
|
684
|
+
* Keys must be identifiers (`[A-Za-z_][A-Za-z0-9_]*`), so literal `%` in
|
|
685
|
+
* prose ("20% off", "50% to 60%") can't match. `t()` phrases are JS strings
|
|
686
|
+
* with no compiler collision and stay `{name}`-only.
|
|
687
|
+
*/
|
|
688
|
+
declare function normalizeMarkupPlaceholders(text: string): string;
|
|
663
689
|
/**
|
|
664
690
|
* Substitute placeholders in a translated string with values from `params`.
|
|
665
691
|
*
|
|
@@ -728,4 +754,4 @@ interface iProject {
|
|
|
728
754
|
declare const t: TFunction;
|
|
729
755
|
declare const tSignal: Signal<TFunction>;
|
|
730
756
|
|
|
731
|
-
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 };
|
|
757
|
+
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, normalizeMarkupPlaceholders, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };
|
package/dist/index.d.ts
CHANGED
|
@@ -375,6 +375,8 @@ interface TranslateOptions {
|
|
|
375
375
|
custom_id?: string;
|
|
376
376
|
/** Optional human-readable label shown in the Translation Manager. */
|
|
377
377
|
label?: string;
|
|
378
|
+
/** Interpolation params — `{name}`, `{count}`, etc. Same single-brace syntax as `t()`. */
|
|
379
|
+
params?: Record<string, ParamPrimitive>;
|
|
378
380
|
}
|
|
379
381
|
/**
|
|
380
382
|
* Wrap a DOM element and manage translation for its contents.
|
|
@@ -399,6 +401,8 @@ declare class Translate {
|
|
|
399
401
|
private custom_id;
|
|
400
402
|
private unsubscribeLocale;
|
|
401
403
|
constructor(element: HTMLElement, options?: TranslateOptions);
|
|
404
|
+
/** Update interpolation params (e.g. a changed count) and re-render. Mirrors `Phrase.setParams`. */
|
|
405
|
+
setParams(params: Record<string, ParamPrimitive> | undefined): void;
|
|
402
406
|
/** Stop reacting to locale changes. Safe to call multiple times. */
|
|
403
407
|
destroy(): void;
|
|
404
408
|
private translateUpdate;
|
|
@@ -406,6 +410,12 @@ declare class Translate {
|
|
|
406
410
|
private handleContentBlock;
|
|
407
411
|
private translate;
|
|
408
412
|
private getTranslation;
|
|
413
|
+
/**
|
|
414
|
+
* Interpolate `{name}`-style params into a resolved text, matching `t()`:
|
|
415
|
+
* unknown keys fall through untouched, and the untranslated fallback is
|
|
416
|
+
* interpolated too. No-op when no params were supplied.
|
|
417
|
+
*/
|
|
418
|
+
private applyParams;
|
|
409
419
|
private translateAttributes;
|
|
410
420
|
private translateAttribute;
|
|
411
421
|
}
|
|
@@ -660,6 +670,22 @@ declare function md5(inputString: string): string;
|
|
|
660
670
|
* to decide whether to render a plural-slot editor vs. a single text field.
|
|
661
671
|
*/
|
|
662
672
|
declare function isICU(template: string): boolean;
|
|
673
|
+
/**
|
|
674
|
+
* Normalize `%name%` markup placeholders to canonical `{name}`.
|
|
675
|
+
*
|
|
676
|
+
* Framework compilers consume bare `{name}` written in markup before the DOM
|
|
677
|
+
* walker ever sees it (Svelte compiles it to an expression; JSX evaluates it),
|
|
678
|
+
* so DOM content accepts `%name%` as a collision-free authoring escape.
|
|
679
|
+
* Normalization runs at every markup capture boundary (content-block
|
|
680
|
+
* tokenizer, `Translate` original-value snapshots, `Phrase` encoding), so the
|
|
681
|
+
* catalog, the wire, and translators only ever see the canonical `{name}`
|
|
682
|
+
* form — and plain `{name}` keeps working for vanilla-HTML authors.
|
|
683
|
+
*
|
|
684
|
+
* Keys must be identifiers (`[A-Za-z_][A-Za-z0-9_]*`), so literal `%` in
|
|
685
|
+
* prose ("20% off", "50% to 60%") can't match. `t()` phrases are JS strings
|
|
686
|
+
* with no compiler collision and stay `{name}`-only.
|
|
687
|
+
*/
|
|
688
|
+
declare function normalizeMarkupPlaceholders(text: string): string;
|
|
663
689
|
/**
|
|
664
690
|
* Substitute placeholders in a translated string with values from `params`.
|
|
665
691
|
*
|
|
@@ -728,4 +754,4 @@ interface iProject {
|
|
|
728
754
|
declare const t: TFunction;
|
|
729
755
|
declare const tSignal: Signal<TFunction>;
|
|
730
756
|
|
|
731
|
-
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 };
|
|
757
|
+
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, normalizeMarkupPlaceholders, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };
|
package/dist/index.js
CHANGED
|
@@ -265,6 +265,9 @@ 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
|
+
}
|
|
268
271
|
function interpolate(template, params, locale) {
|
|
269
272
|
if (isICU(template)) {
|
|
270
273
|
try {
|
|
@@ -1141,7 +1144,7 @@ function tokenizeElement(element) {
|
|
|
1141
1144
|
const tokens = [];
|
|
1142
1145
|
const clone = element.cloneNode(true);
|
|
1143
1146
|
_walkForTokens(element, Array.from(clone.childNodes), tokens, []);
|
|
1144
|
-
return { tokens, content: clone.outerHTML };
|
|
1147
|
+
return { tokens, content: normalizeMarkupPlaceholders(clone.outerHTML) };
|
|
1145
1148
|
}
|
|
1146
1149
|
function _writeKnownContentBlockToCache(category, customId) {
|
|
1147
1150
|
const lookupCat = category || "__uncategorized__";
|
|
@@ -1171,7 +1174,7 @@ function _walkForTokens(liveRoot, cloneNodes, tokens, indices) {
|
|
|
1171
1174
|
}
|
|
1172
1175
|
const contentToken = node.nodeValue?.replace(/\s+/g, " ").trim();
|
|
1173
1176
|
if (node.nodeType === Node.TEXT_NODE && contentToken) {
|
|
1174
|
-
tokens.push(contentToken);
|
|
1177
|
+
tokens.push(normalizeMarkupPlaceholders(contentToken));
|
|
1175
1178
|
return;
|
|
1176
1179
|
}
|
|
1177
1180
|
if (!node.hasChildNodes()) return;
|
|
@@ -1186,24 +1189,24 @@ function _tokenizeAttributes(element, tokens) {
|
|
|
1186
1189
|
}
|
|
1187
1190
|
for (const attr of TRANSLATABLE_ATTRIBUTES) {
|
|
1188
1191
|
const value = element.getAttribute(attr)?.trim();
|
|
1189
|
-
if (value) tokens.push(value);
|
|
1192
|
+
if (value) tokens.push(normalizeMarkupPlaceholders(value));
|
|
1190
1193
|
}
|
|
1191
1194
|
if (VALUE_TRANSLATABLE_ELEMENTS.includes(tagName)) {
|
|
1192
1195
|
const value = element.getAttribute("value")?.trim();
|
|
1193
|
-
if (value) tokens.push(value);
|
|
1196
|
+
if (value) tokens.push(normalizeMarkupPlaceholders(value));
|
|
1194
1197
|
}
|
|
1195
1198
|
if (tagName === "input") {
|
|
1196
1199
|
const inputType = element.getAttribute("type")?.toLowerCase();
|
|
1197
1200
|
if (inputType && VALUE_TRANSLATABLE_INPUT_TYPES.includes(inputType)) {
|
|
1198
1201
|
const value = element.getAttribute("value")?.trim();
|
|
1199
|
-
if (value) tokens.push(value);
|
|
1202
|
+
if (value) tokens.push(normalizeMarkupPlaceholders(value));
|
|
1200
1203
|
}
|
|
1201
1204
|
}
|
|
1202
1205
|
if (tagName === "select") {
|
|
1203
1206
|
const options = element.querySelectorAll("option");
|
|
1204
1207
|
options.forEach((option) => {
|
|
1205
1208
|
const optionText = option.textContent?.trim();
|
|
1206
|
-
if (optionText) tokens.push(optionText);
|
|
1209
|
+
if (optionText) tokens.push(normalizeMarkupPlaceholders(optionText));
|
|
1207
1210
|
});
|
|
1208
1211
|
}
|
|
1209
1212
|
}
|
|
@@ -1253,6 +1256,14 @@ var Translate = class {
|
|
|
1253
1256
|
if (locale && this.parseComplete) this.translateUpdate(locale);
|
|
1254
1257
|
});
|
|
1255
1258
|
}
|
|
1259
|
+
/** Update interpolation params (e.g. a changed count) and re-render. Mirrors `Phrase.setParams`. */
|
|
1260
|
+
setParams(params) {
|
|
1261
|
+
this.options.params = params;
|
|
1262
|
+
if (!this.parseComplete) return;
|
|
1263
|
+
this.lastTranslatedLocale = "";
|
|
1264
|
+
const locale = currentlyLoadedLocale.get();
|
|
1265
|
+
if (locale) this.translateUpdate(locale);
|
|
1266
|
+
}
|
|
1256
1267
|
/** Stop reacting to locale changes. Safe to call multiple times. */
|
|
1257
1268
|
destroy() {
|
|
1258
1269
|
if (this.unsubscribeLocale) {
|
|
@@ -1265,7 +1276,7 @@ var Translate = class {
|
|
|
1265
1276
|
if (this.lastTranslatedLocale === currentLocale) return;
|
|
1266
1277
|
const { category = "" } = this.options;
|
|
1267
1278
|
if (this.tokens.length === 1) {
|
|
1268
|
-
this.element.innerText = LangsysApp.Translations.t(this.tokens[0], category);
|
|
1279
|
+
this.element.innerText = this.applyParams(LangsysApp.Translations.t(this.tokens[0], category));
|
|
1269
1280
|
} else {
|
|
1270
1281
|
this.translate(Array.from(this.element.childNodes));
|
|
1271
1282
|
this.lastTranslatedLocale = currentLocale;
|
|
@@ -1283,7 +1294,7 @@ var Translate = class {
|
|
|
1283
1294
|
this.tokens = tokens;
|
|
1284
1295
|
const { category = "" } = this.options;
|
|
1285
1296
|
if (this.tokens.length === 1) {
|
|
1286
|
-
this.element.innerText = LangsysApp.Translations.t(this.tokens[0], category);
|
|
1297
|
+
this.element.innerText = this.applyParams(LangsysApp.Translations.t(this.tokens[0], category));
|
|
1287
1298
|
} else {
|
|
1288
1299
|
const contentBlock = {
|
|
1289
1300
|
custom_id: "",
|
|
@@ -1319,7 +1330,7 @@ var Translate = class {
|
|
|
1319
1330
|
}
|
|
1320
1331
|
translate(nodes) {
|
|
1321
1332
|
const currentLocale = currentlyLoadedLocale.get();
|
|
1322
|
-
if (currentLocale === config.baseLocale && (this.lastTranslatedLocale === "" || this.lastTranslatedLocale === config.baseLocale)) {
|
|
1333
|
+
if (currentLocale === config.baseLocale && (this.lastTranslatedLocale === "" || this.lastTranslatedLocale === config.baseLocale) && isEmpty(this.options.params)) {
|
|
1323
1334
|
return;
|
|
1324
1335
|
}
|
|
1325
1336
|
nodes.forEach((node) => {
|
|
@@ -1336,15 +1347,16 @@ var Translate = class {
|
|
|
1336
1347
|
}
|
|
1337
1348
|
if (isEmpty(node.nodeValue?.trim())) return;
|
|
1338
1349
|
if (isEmpty(node.originalNodeValue)) {
|
|
1339
|
-
node.originalNodeValue = node.nodeValue;
|
|
1350
|
+
node.originalNodeValue = normalizeMarkupPlaceholders(node.nodeValue);
|
|
1340
1351
|
}
|
|
1341
1352
|
const contentToken = node.originalNodeValue?.replace(/\s+/g, " ").trim();
|
|
1342
1353
|
if (!contentToken) return;
|
|
1343
1354
|
const translation = this.getTranslation(contentToken);
|
|
1344
1355
|
if (translation && contentToken && node.originalNodeValue) {
|
|
1345
|
-
|
|
1356
|
+
const resolved = this.applyParams(translation);
|
|
1357
|
+
node.nodeValue = node.originalNodeValue.replace(contentToken, () => resolved);
|
|
1346
1358
|
} else if (node.originalNodeValue) {
|
|
1347
|
-
node.nodeValue = node.originalNodeValue;
|
|
1359
|
+
node.nodeValue = this.applyParams(node.originalNodeValue);
|
|
1348
1360
|
}
|
|
1349
1361
|
});
|
|
1350
1362
|
}
|
|
@@ -1353,6 +1365,16 @@ var Translate = class {
|
|
|
1353
1365
|
const { category = "" } = this.options;
|
|
1354
1366
|
return LangsysApp.Translations.lookupContent(category, this.custom_id, token);
|
|
1355
1367
|
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Interpolate `{name}`-style params into a resolved text, matching `t()`:
|
|
1370
|
+
* unknown keys fall through untouched, and the untranslated fallback is
|
|
1371
|
+
* interpolated too. No-op when no params were supplied.
|
|
1372
|
+
*/
|
|
1373
|
+
applyParams(text) {
|
|
1374
|
+
const { params } = this.options;
|
|
1375
|
+
if (isEmpty(params)) return text;
|
|
1376
|
+
return interpolate(text, params, currentlyLoadedLocale.get());
|
|
1377
|
+
}
|
|
1356
1378
|
translateAttributes(element) {
|
|
1357
1379
|
const tagName = element.tagName.toLowerCase();
|
|
1358
1380
|
if (!element.originalAttributes) element.originalAttributes = {};
|
|
@@ -1374,12 +1396,12 @@ var Translate = class {
|
|
|
1374
1396
|
const optionEl = option;
|
|
1375
1397
|
if (!optionEl.originalAttributes) optionEl.originalAttributes = {};
|
|
1376
1398
|
if (optionEl.originalAttributes["textContent"] === void 0) {
|
|
1377
|
-
optionEl.originalAttributes["textContent"] = option.textContent || "";
|
|
1399
|
+
optionEl.originalAttributes["textContent"] = normalizeMarkupPlaceholders(option.textContent || "");
|
|
1378
1400
|
}
|
|
1379
1401
|
const originalText = optionEl.originalAttributes["textContent"].trim();
|
|
1380
1402
|
if (originalText) {
|
|
1381
1403
|
const translation = this.getTranslation(originalText);
|
|
1382
|
-
option.textContent = translation || optionEl.originalAttributes["textContent"];
|
|
1404
|
+
option.textContent = this.applyParams(translation || optionEl.originalAttributes["textContent"]);
|
|
1383
1405
|
}
|
|
1384
1406
|
});
|
|
1385
1407
|
}
|
|
@@ -1388,13 +1410,13 @@ var Translate = class {
|
|
|
1388
1410
|
const currentValue = element.getAttribute(attr);
|
|
1389
1411
|
if (!currentValue) return;
|
|
1390
1412
|
if (element.originalAttributes[attr] === void 0) {
|
|
1391
|
-
element.originalAttributes[attr] = currentValue;
|
|
1413
|
+
element.originalAttributes[attr] = normalizeMarkupPlaceholders(currentValue);
|
|
1392
1414
|
}
|
|
1393
1415
|
const originalValue = element.originalAttributes[attr].trim();
|
|
1394
1416
|
if (!originalValue) return;
|
|
1395
1417
|
const translation = this.getTranslation(originalValue);
|
|
1396
|
-
if (translation) element.setAttribute(attr, translation);
|
|
1397
|
-
else element.setAttribute(attr, element.originalAttributes[attr]);
|
|
1418
|
+
if (translation) element.setAttribute(attr, this.applyParams(translation));
|
|
1419
|
+
else element.setAttribute(attr, this.applyParams(element.originalAttributes[attr]));
|
|
1398
1420
|
}
|
|
1399
1421
|
};
|
|
1400
1422
|
|
|
@@ -1407,7 +1429,7 @@ var SENT_SCAN = /\uE000(\d+)\uE001|\uE002(\d+)\uE003/g;
|
|
|
1407
1429
|
function encodeRichText(root) {
|
|
1408
1430
|
const slots = [];
|
|
1409
1431
|
const phrase = _encodeNodes(Array.from(root.childNodes), slots);
|
|
1410
|
-
return { phrase: phrase.replace(/\s+/g, " ").trim(), slots };
|
|
1432
|
+
return { phrase: normalizeMarkupPlaceholders(phrase.replace(/\s+/g, " ").trim()), slots };
|
|
1411
1433
|
}
|
|
1412
1434
|
function _encodeNodes(nodes, slots) {
|
|
1413
1435
|
let out = "";
|
|
@@ -1546,6 +1568,7 @@ exports.isICU = isICU;
|
|
|
1546
1568
|
exports.logger = logger;
|
|
1547
1569
|
exports.markupTokenValues = markupTokenValues;
|
|
1548
1570
|
exports.md5 = md5;
|
|
1571
|
+
exports.normalizeMarkupPlaceholders = normalizeMarkupPlaceholders;
|
|
1549
1572
|
exports.persist = persist;
|
|
1550
1573
|
exports.reconstitute = reconstitute;
|
|
1551
1574
|
exports.registerContentBlock = registerContentBlock;
|