langsys-js-typescript 0.4.2 → 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 CHANGED
@@ -1,3 +1,23 @@
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
+
15
+ ## 0.4.3 - 2026-07-08
16
+
17
+ ### Fixed
18
+
19
+ - **The params diagnostic no longer names the wrong framework.** 0.4.2's warning hardcoded "the framework compiler (Svelte/JSX)" and the `{key}` spelling, which misdiagnoses Vue users — Vue consumes `{{ key }}`, not single braces, so a Vue developer hitting the warning was pointed at a syntax their framework never touches. The message is now framework-neutral and names both brace spellings.
20
+
1
21
  ## 0.4.2 - 2026-07-08
2
22
 
3
23
  ### Added
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:
@@ -199,12 +216,38 @@ Writing `{key}` in markup fails *silently* — the base locale still looks right
199
216
 
200
217
  ```
201
218
  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.
219
+ content: %count%. If you wrote {count} or {{ count }} in markup, your framework's
220
+ template compiler substituted it before Langsys saw the text — write %count%
221
+ instead.
204
222
  ```
205
223
 
206
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.
207
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
+
208
251
  ## Server-Side Rendering
209
252
 
210
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
- /** Interpolation params — `{name}`, `{count}`, etc. Same single-brace syntax as `t()`. */
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
- /** Interpolation params — `{n}` for pluralization, `{name}`, etc. */
437
- params?: Record<string, unknown>;
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, unknown>): void;
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
- /** Interpolation params — `{name}`, `{count}`, etc. Same single-brace syntax as `t()`. */
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
- /** Interpolation params — `{n}` for pluralization, `{name}`, etc. */
437
- params?: Record<string, unknown>;
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, unknown>): void;
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.js CHANGED
@@ -284,7 +284,7 @@ function warnUnmatchedParams(source, texts, params, context) {
284
284
  if (!unused.length) return;
285
285
  const keyList = unused.map((key) => `%${key}%`).join(", ");
286
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}]` : "")
287
+ `${source} received params with no matching placeholder in its content: ${keyList}. If you wrote {${unused[0]}} or {{ ${unused[0]} }} in markup, your framework's template compiler substituted it before Langsys saw the text \u2014 write %${unused[0]}% instead.` + (context ? ` [${context}]` : "")
288
288
  );
289
289
  }
290
290
  function interpolate(template, params, locale) {