langsys-js-typescript 0.4.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 +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +14 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +14 -11
- 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
|
@@ -670,6 +670,22 @@ declare function md5(inputString: string): string;
|
|
|
670
670
|
* to decide whether to render a plural-slot editor vs. a single text field.
|
|
671
671
|
*/
|
|
672
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;
|
|
673
689
|
/**
|
|
674
690
|
* Substitute placeholders in a translated string with values from `params`.
|
|
675
691
|
*
|
|
@@ -738,4 +754,4 @@ interface iProject {
|
|
|
738
754
|
declare const t: TFunction;
|
|
739
755
|
declare const tSignal: Signal<TFunction>;
|
|
740
756
|
|
|
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 };
|
|
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
|
@@ -670,6 +670,22 @@ declare function md5(inputString: string): string;
|
|
|
670
670
|
* to decide whether to render a plural-slot editor vs. a single text field.
|
|
671
671
|
*/
|
|
672
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;
|
|
673
689
|
/**
|
|
674
690
|
* Substitute placeholders in a translated string with values from `params`.
|
|
675
691
|
*
|
|
@@ -738,4 +754,4 @@ interface iProject {
|
|
|
738
754
|
declare const t: TFunction;
|
|
739
755
|
declare const tSignal: Signal<TFunction>;
|
|
740
756
|
|
|
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 };
|
|
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
|
}
|
|
@@ -1344,7 +1347,7 @@ var Translate = class {
|
|
|
1344
1347
|
}
|
|
1345
1348
|
if (isEmpty(node.nodeValue?.trim())) return;
|
|
1346
1349
|
if (isEmpty(node.originalNodeValue)) {
|
|
1347
|
-
node.originalNodeValue = node.nodeValue;
|
|
1350
|
+
node.originalNodeValue = normalizeMarkupPlaceholders(node.nodeValue);
|
|
1348
1351
|
}
|
|
1349
1352
|
const contentToken = node.originalNodeValue?.replace(/\s+/g, " ").trim();
|
|
1350
1353
|
if (!contentToken) return;
|
|
@@ -1393,7 +1396,7 @@ var Translate = class {
|
|
|
1393
1396
|
const optionEl = option;
|
|
1394
1397
|
if (!optionEl.originalAttributes) optionEl.originalAttributes = {};
|
|
1395
1398
|
if (optionEl.originalAttributes["textContent"] === void 0) {
|
|
1396
|
-
optionEl.originalAttributes["textContent"] = option.textContent || "";
|
|
1399
|
+
optionEl.originalAttributes["textContent"] = normalizeMarkupPlaceholders(option.textContent || "");
|
|
1397
1400
|
}
|
|
1398
1401
|
const originalText = optionEl.originalAttributes["textContent"].trim();
|
|
1399
1402
|
if (originalText) {
|
|
@@ -1407,7 +1410,7 @@ var Translate = class {
|
|
|
1407
1410
|
const currentValue = element.getAttribute(attr);
|
|
1408
1411
|
if (!currentValue) return;
|
|
1409
1412
|
if (element.originalAttributes[attr] === void 0) {
|
|
1410
|
-
element.originalAttributes[attr] = currentValue;
|
|
1413
|
+
element.originalAttributes[attr] = normalizeMarkupPlaceholders(currentValue);
|
|
1411
1414
|
}
|
|
1412
1415
|
const originalValue = element.originalAttributes[attr].trim();
|
|
1413
1416
|
if (!originalValue) return;
|
|
@@ -1426,7 +1429,7 @@ var SENT_SCAN = /\uE000(\d+)\uE001|\uE002(\d+)\uE003/g;
|
|
|
1426
1429
|
function encodeRichText(root) {
|
|
1427
1430
|
const slots = [];
|
|
1428
1431
|
const phrase = _encodeNodes(Array.from(root.childNodes), slots);
|
|
1429
|
-
return { phrase: phrase.replace(/\s+/g, " ").trim(), slots };
|
|
1432
|
+
return { phrase: normalizeMarkupPlaceholders(phrase.replace(/\s+/g, " ").trim()), slots };
|
|
1430
1433
|
}
|
|
1431
1434
|
function _encodeNodes(nodes, slots) {
|
|
1432
1435
|
let out = "";
|
|
@@ -1565,6 +1568,7 @@ exports.isICU = isICU;
|
|
|
1565
1568
|
exports.logger = logger;
|
|
1566
1569
|
exports.markupTokenValues = markupTokenValues;
|
|
1567
1570
|
exports.md5 = md5;
|
|
1571
|
+
exports.normalizeMarkupPlaceholders = normalizeMarkupPlaceholders;
|
|
1568
1572
|
exports.persist = persist;
|
|
1569
1573
|
exports.reconstitute = reconstitute;
|
|
1570
1574
|
exports.registerContentBlock = registerContentBlock;
|