langsys-js-typescript 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +42 -0
- package/dist/index.d.mts +17 -4
- package/dist/index.d.ts +17 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## 0.5.0 - 2026-08-10
|
|
2
|
+
|
|
3
|
+
### Changed (breaking, types only)
|
|
4
|
+
|
|
5
|
+
- **`PhraseOptions.params` is now `Record<string, ParamPrimitive>`**, matching `TranslateOptions.params` and `t()`. It was `Record<string, unknown>`, which allowed values — objects, arrays, functions — that interpolation can only render as `[object Object]`. No runtime behavior changes; this is a compile-time narrowing that surfaces those calls as errors instead of silently rendering garbage. `Phrase.setParams()` now also accepts `undefined`, matching `Translate.setParams()`.
|
|
6
|
+
|
|
7
|
+
**Impact:** code typing its params as `Record<string, unknown>` and passing it straight through no longer compiles — including all three framework bindings as of this release. The fix is one line at each call site: declare the params as `Record<string, ParamPrimitive>` (exported from the package root). Runtime is unaffected, so a wrapper that skips the type change still *works*; it just fails `tsc`.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`Phrase` is documented in the README for the first time.** It's been exported since 0.1.0 but had no section, so the only documented tool for markup-bearing content was `Translate` — which splits at tag boundaries and is the wrong choice for a sentence. The new section covers the three reasons it exists (grammatical agreement, reordering, phrase-key stability), and the `Translate` section now shows the literal per-text-node split so the trade-off is visible at the point of decision.
|
|
12
|
+
- Warning against building phrases with template literals (`` t(`Hello, ${name}!`) ``), which registers every runtime value as its own catalog phrase and pollutes the catalog shared across every Langsys SDK.
|
|
13
|
+
- Params doc comments now name the `%name%` markup spelling alongside the canonical `{name}`, including Vue's `{{ name }}` case.
|
|
14
|
+
|
|
1
15
|
## 0.4.3 - 2026-07-08
|
|
2
16
|
|
|
3
17
|
### Fixed
|
package/README.md
CHANGED
|
@@ -72,6 +72,15 @@ Allowed value types: `string | number | Date | boolean`. Numbers and Dates forma
|
|
|
72
72
|
|
|
73
73
|
> Translations containing ICU MessageFormat syntax — `{count, plural, one {# item} other {# items}}`, `{gender, select, …}`, `{n, number}`, `{d, date}` — are rendered with full CLDR plural rules for the active locale (Arabic's six categories, Russian's four, etc.). Plain `{name}` slots keep working unchanged.
|
|
74
74
|
|
|
75
|
+
> **⚠️ Always pass values through `params` — never build the phrase yourself.**
|
|
76
|
+
>
|
|
77
|
+
> ```ts
|
|
78
|
+
> t(`Hello, ${name}!`, 'Greetings'); // ❌ never do this
|
|
79
|
+
> t('Hello, {name}!', 'Greetings', { name }); // ✓
|
|
80
|
+
> ```
|
|
81
|
+
>
|
|
82
|
+
> The template literal resolves *before* `t()` runs, so with a write key the SDK registers `"Hello, Sarah!"` as a brand-new phrase — and every distinct runtime value mints another one ("Hello, Priya!", "Hello, Ahmed!"). Your Translation Manager fills with untranslatable one-offs, and since projects share one catalog across SDKs, it pollutes what every other binding reads too. The SDK cannot detect this: an interpolated string is indistinguishable from an authored one. The `params` form registers the phrase once, with the placeholder intact for translators.
|
|
83
|
+
|
|
75
84
|
#### Categorization disambiguates context
|
|
76
85
|
|
|
77
86
|
```ts
|
|
@@ -158,6 +167,14 @@ Attributes honored on contained elements:
|
|
|
158
167
|
- Several validation-message `data-*` attributes
|
|
159
168
|
- `translate="no"` — elements marked this way (and their children) are skipped
|
|
160
169
|
|
|
170
|
+
> **`Translate` tokenizes per text node — so inline markup splits a sentence.**
|
|
171
|
+
>
|
|
172
|
+
> ```html
|
|
173
|
+
> <p>My content <strong>is the best</strong> when internationalized.</p>
|
|
174
|
+
> ```
|
|
175
|
+
>
|
|
176
|
+
> registers **three** phrases: `"My content"`, `"is the best"`, `"when internationalized."` That's correct for a block of independent runs, and it's what `Translate` is for. But a translator sees three fragments with no context, and neither reordering nor grammatical agreement can cross a fragment boundary — fatal for a sentence whose word order or noun forms change with the language. When inline markup sits *inside a sentence*, reach for [`Phrase`](#the-phrase-class) instead.
|
|
177
|
+
|
|
161
178
|
### Interpolation params
|
|
162
179
|
|
|
163
180
|
`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:
|
|
@@ -206,6 +223,31 @@ instead.
|
|
|
206
223
|
|
|
207
224
|
The check runs whenever the params key-set changes, so a ticking `count` won't spam the console, and it's silent in production.
|
|
208
225
|
|
|
226
|
+
## The `Phrase` class
|
|
227
|
+
|
|
228
|
+
`Phrase` wraps an element whose contents are **one** translatable sentence *even though it contains inline markup*. Where `Translate` splits at tag boundaries, `Phrase` keeps the run whole.
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import { Phrase } from 'langsys-js-typescript';
|
|
232
|
+
|
|
233
|
+
const el = document.querySelector<HTMLElement>('#reviews')!;
|
|
234
|
+
new Phrase(el, { category: 'ProductCard', params: { n: reviewCount } });
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```html
|
|
238
|
+
<span id="reviews">Based on %n% <strong>reviews</strong></span>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
That registers as a single phrase — `Based on {n} {m0o}reviews{m0c}` — rather than `"Based on {n}"` and `"reviews"`. Three things depend on it:
|
|
242
|
+
|
|
243
|
+
1. **Grammatical agreement.** The count and the noun it inflects must live in one phrase, or no ICU plural rule can select the right noun form. Russian has four plural categories, Arabic six, Polish four — split them apart and the output is confidently wrong, not merely awkward.
|
|
244
|
+
2. **Reordering.** The markup tokens are ordinary ICU placeholders, so a translator or model *places* them around the translated word: `{m0o}White{m0c} House` → `Casa {m0o}Blanca{m0c}`. Impossible once split at the tag boundary.
|
|
245
|
+
3. **Phrase-key stability.** Your real elements never reach the translator — they're replaced by neutral tokens and reconstituted at render. Framework scoped-CSS classes (`svelte-a1b2c3`) change every build; if they rode along in the phrase, the key would drift and silently re-translate everything.
|
|
246
|
+
|
|
247
|
+
Use it standalone, or nested inside a `Translate` block to protect specific runs — the block tokenizer skips any subtree carrying `data-ls-phrase` (exported as `PHRASE_MARKER_ATTR`), letting the `Phrase` own it. `setParams()` re-renders on a changed count, same as `Translate`.
|
|
248
|
+
|
|
249
|
+
If a translation drops or unbalances a markup token, rendering degrades to plain text with the markers stripped — the meaning survives even when the markup doesn't.
|
|
250
|
+
|
|
209
251
|
## Server-Side Rendering
|
|
210
252
|
|
|
211
253
|
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,7 +375,13 @@ interface TranslateOptions {
|
|
|
375
375
|
custom_id?: string;
|
|
376
376
|
/** Optional human-readable label shown in the Translation Manager. */
|
|
377
377
|
label?: string;
|
|
378
|
-
/**
|
|
378
|
+
/**
|
|
379
|
+
* Interpolation params — `{name}`, `{count}`, etc. Same single-brace syntax
|
|
380
|
+
* as `t()`, and what you write in plain DOM. In a framework component's
|
|
381
|
+
* markup write `%name%` / `%count%` instead: Svelte and JSX consume
|
|
382
|
+
* `{name}` at compile time (Vue consumes `{{ name }}`), and `%name%` is
|
|
383
|
+
* normalized back to `{name}` at capture.
|
|
384
|
+
*/
|
|
379
385
|
params?: Record<string, ParamPrimitive>;
|
|
380
386
|
}
|
|
381
387
|
/**
|
|
@@ -433,8 +439,15 @@ declare const PHRASE_MARKER_ATTR = "data-ls-phrase";
|
|
|
433
439
|
interface PhraseOptions {
|
|
434
440
|
/** Category the phrase registers under (disambiguation for translators). */
|
|
435
441
|
category?: string;
|
|
436
|
-
/**
|
|
437
|
-
|
|
442
|
+
/**
|
|
443
|
+
* Interpolation params — `{n}` for pluralization, `{name}`, etc.
|
|
444
|
+
*
|
|
445
|
+
* Those are the canonical spellings, and what you write in plain DOM. In a
|
|
446
|
+
* framework component's markup write `%n%` / `%name%` instead: Svelte and
|
|
447
|
+
* JSX consume `{n}` at compile time (Vue consumes `{{ n }}`), and `%n%` is
|
|
448
|
+
* normalized back to `{n}` at capture.
|
|
449
|
+
*/
|
|
450
|
+
params?: Record<string, ParamPrimitive>;
|
|
438
451
|
}
|
|
439
452
|
/**
|
|
440
453
|
* DOM handler for a markup-bearing "rich" phrase (`<Phrase>`).
|
|
@@ -460,7 +473,7 @@ declare class Phrase {
|
|
|
460
473
|
private checkedParamKeys;
|
|
461
474
|
constructor(host: HTMLElement, options?: PhraseOptions);
|
|
462
475
|
/** Update interpolation params (e.g. a changed count) and re-render. */
|
|
463
|
-
setParams(params: Record<string,
|
|
476
|
+
setParams(params: Record<string, ParamPrimitive> | undefined): void;
|
|
464
477
|
/** Stop reacting to locale/translation changes. Safe to call multiple times. */
|
|
465
478
|
destroy(): void;
|
|
466
479
|
private _init;
|
package/dist/index.d.ts
CHANGED
|
@@ -375,7 +375,13 @@ interface TranslateOptions {
|
|
|
375
375
|
custom_id?: string;
|
|
376
376
|
/** Optional human-readable label shown in the Translation Manager. */
|
|
377
377
|
label?: string;
|
|
378
|
-
/**
|
|
378
|
+
/**
|
|
379
|
+
* Interpolation params — `{name}`, `{count}`, etc. Same single-brace syntax
|
|
380
|
+
* as `t()`, and what you write in plain DOM. In a framework component's
|
|
381
|
+
* markup write `%name%` / `%count%` instead: Svelte and JSX consume
|
|
382
|
+
* `{name}` at compile time (Vue consumes `{{ name }}`), and `%name%` is
|
|
383
|
+
* normalized back to `{name}` at capture.
|
|
384
|
+
*/
|
|
379
385
|
params?: Record<string, ParamPrimitive>;
|
|
380
386
|
}
|
|
381
387
|
/**
|
|
@@ -433,8 +439,15 @@ declare const PHRASE_MARKER_ATTR = "data-ls-phrase";
|
|
|
433
439
|
interface PhraseOptions {
|
|
434
440
|
/** Category the phrase registers under (disambiguation for translators). */
|
|
435
441
|
category?: string;
|
|
436
|
-
/**
|
|
437
|
-
|
|
442
|
+
/**
|
|
443
|
+
* Interpolation params — `{n}` for pluralization, `{name}`, etc.
|
|
444
|
+
*
|
|
445
|
+
* Those are the canonical spellings, and what you write in plain DOM. In a
|
|
446
|
+
* framework component's markup write `%n%` / `%name%` instead: Svelte and
|
|
447
|
+
* JSX consume `{n}` at compile time (Vue consumes `{{ n }}`), and `%n%` is
|
|
448
|
+
* normalized back to `{n}` at capture.
|
|
449
|
+
*/
|
|
450
|
+
params?: Record<string, ParamPrimitive>;
|
|
438
451
|
}
|
|
439
452
|
/**
|
|
440
453
|
* DOM handler for a markup-bearing "rich" phrase (`<Phrase>`).
|
|
@@ -460,7 +473,7 @@ declare class Phrase {
|
|
|
460
473
|
private checkedParamKeys;
|
|
461
474
|
constructor(host: HTMLElement, options?: PhraseOptions);
|
|
462
475
|
/** Update interpolation params (e.g. a changed count) and re-render. */
|
|
463
|
-
setParams(params: Record<string,
|
|
476
|
+
setParams(params: Record<string, ParamPrimitive> | undefined): void;
|
|
464
477
|
/** Stop reacting to locale/translation changes. Safe to call multiple times. */
|
|
465
478
|
destroy(): void;
|
|
466
479
|
private _init;
|