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 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
- node.nodeValue = node.originalNodeValue.replace(contentToken, translation);
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;