langsys-js-typescript 0.4.1 → 0.4.3

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,16 @@
1
+ ## 0.4.3 - 2026-07-08
2
+
3
+ ### Fixed
4
+
5
+ - **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.
6
+
7
+ ## 0.4.2 - 2026-07-08
8
+
9
+ ### Added
10
+
11
+ - **Debug-mode diagnostic for the `{key}`-in-markup mistake.** Writing `{name}` instead of `%name%` inside `<Translate>`/`<Phrase>` content fails silently — the framework compiler substitutes it before the SDK sees the text, so the base locale still renders correctly and only translated locales break. The SDK can't observe the original braces, but the fingerprint is unmistakable: `params` supplied with no matching placeholder in the captured content. When `debug` is on, that now emits a warning naming the offending key and the exact fix. The check re-runs only when the params key-SET changes, so a ticking counter doesn't spam the console, and it's silent in production.
12
+ - **`findUnusedParamKeys(texts, params)`** — the underlying detector, exported for wrappers that want the same check around their own rendering. Treats both `{key}` and ICU `{key, plural, …}` as used.
13
+
1
14
  ## 0.4.1 - 2026-07-08
2
15
 
3
16
  ### Added
package/README.md CHANGED
@@ -195,6 +195,17 @@ In compiled frameworks, bare `{key}` written in markup never reaches the SDK —
195
195
 
196
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
197
 
198
+ Writing `{key}` in markup fails *silently* — the base locale still looks right, so the mistake surfaces only once someone switches language. With `debug: true`, the SDK catches it for you: if you pass `params` whose keys have no matching placeholder in the captured content, it warns and names the fix.
199
+
200
+ ```
201
+ Langsys Warning <Translate> received params with no matching placeholder in its
202
+ content: %count%. If you wrote {count} or {{ count }} in markup, your framework's
203
+ template compiler substituted it before Langsys saw the text — write %count%
204
+ instead.
205
+ ```
206
+
207
+ 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
+
198
209
  ## Server-Side Rendering
199
210
 
200
211
  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
@@ -400,6 +400,8 @@ declare class Translate {
400
400
  private lastTranslatedLocale;
401
401
  private custom_id;
402
402
  private unsubscribeLocale;
403
+ /** Sorted params key-set already checked, so value-only updates don't re-warn. */
404
+ private checkedParamKeys;
403
405
  constructor(element: HTMLElement, options?: TranslateOptions);
404
406
  /** Update interpolation params (e.g. a changed count) and re-render. Mirrors `Phrase.setParams`. */
405
407
  setParams(params: Record<string, ParamPrimitive> | undefined): void;
@@ -410,6 +412,12 @@ declare class Translate {
410
412
  private handleContentBlock;
411
413
  private translate;
412
414
  private getTranslation;
415
+ /**
416
+ * Debug-time check that every supplied param has a placeholder to land in.
417
+ * Re-runs only when the key SET changes — a changing `count` value must not
418
+ * re-warn on every render.
419
+ */
420
+ private checkParams;
413
421
  /**
414
422
  * Interpolate `{name}`-style params into a resolved text, matching `t()`:
415
423
  * unknown keys fall through untouched, and the untranslated fallback is
@@ -448,12 +456,20 @@ declare class Phrase {
448
456
  private slots;
449
457
  private unsubscribers;
450
458
  private ready;
459
+ /** Sorted params key-set already checked, so value-only updates don't re-warn. */
460
+ private checkedParamKeys;
451
461
  constructor(host: HTMLElement, options?: PhraseOptions);
452
462
  /** Update interpolation params (e.g. a changed count) and re-render. */
453
463
  setParams(params: Record<string, unknown>): void;
454
464
  /** Stop reacting to locale/translation changes. Safe to call multiple times. */
455
465
  destroy(): void;
456
466
  private _init;
467
+ /**
468
+ * Debug-time check that every supplied param has a placeholder to land in.
469
+ * Re-runs only when the key SET changes — a changing `n` value must not
470
+ * re-warn on every render.
471
+ */
472
+ private _checkParams;
457
473
  private _render;
458
474
  }
459
475
 
@@ -686,6 +702,14 @@ declare function isICU(template: string): boolean;
686
702
  * with no compiler collision and stay `{name}`-only.
687
703
  */
688
704
  declare function normalizeMarkupPlaceholders(text: string): string;
705
+ /**
706
+ * Params keys that have NO matching placeholder in `texts`.
707
+ *
708
+ * Texts are expected in canonical form (post-`normalizeMarkupPlaceholders`),
709
+ * so a key counts as used when it appears as `{key}` or as the argument of an
710
+ * ICU slot (`{key, plural, …}`). Surrounding whitespace is tolerated.
711
+ */
712
+ declare function findUnusedParamKeys(texts: string[], params: Record<string, unknown> | undefined): string[];
689
713
  /**
690
714
  * Substitute placeholders in a translated string with values from `params`.
691
715
  *
@@ -754,4 +778,4 @@ interface iProject {
754
778
  declare const t: TFunction;
755
779
  declare const tSignal: Signal<TFunction>;
756
780
 
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 };
781
+ 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, findUnusedParamKeys, 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
@@ -400,6 +400,8 @@ declare class Translate {
400
400
  private lastTranslatedLocale;
401
401
  private custom_id;
402
402
  private unsubscribeLocale;
403
+ /** Sorted params key-set already checked, so value-only updates don't re-warn. */
404
+ private checkedParamKeys;
403
405
  constructor(element: HTMLElement, options?: TranslateOptions);
404
406
  /** Update interpolation params (e.g. a changed count) and re-render. Mirrors `Phrase.setParams`. */
405
407
  setParams(params: Record<string, ParamPrimitive> | undefined): void;
@@ -410,6 +412,12 @@ declare class Translate {
410
412
  private handleContentBlock;
411
413
  private translate;
412
414
  private getTranslation;
415
+ /**
416
+ * Debug-time check that every supplied param has a placeholder to land in.
417
+ * Re-runs only when the key SET changes — a changing `count` value must not
418
+ * re-warn on every render.
419
+ */
420
+ private checkParams;
413
421
  /**
414
422
  * Interpolate `{name}`-style params into a resolved text, matching `t()`:
415
423
  * unknown keys fall through untouched, and the untranslated fallback is
@@ -448,12 +456,20 @@ declare class Phrase {
448
456
  private slots;
449
457
  private unsubscribers;
450
458
  private ready;
459
+ /** Sorted params key-set already checked, so value-only updates don't re-warn. */
460
+ private checkedParamKeys;
451
461
  constructor(host: HTMLElement, options?: PhraseOptions);
452
462
  /** Update interpolation params (e.g. a changed count) and re-render. */
453
463
  setParams(params: Record<string, unknown>): void;
454
464
  /** Stop reacting to locale/translation changes. Safe to call multiple times. */
455
465
  destroy(): void;
456
466
  private _init;
467
+ /**
468
+ * Debug-time check that every supplied param has a placeholder to land in.
469
+ * Re-runs only when the key SET changes — a changing `n` value must not
470
+ * re-warn on every render.
471
+ */
472
+ private _checkParams;
457
473
  private _render;
458
474
  }
459
475
 
@@ -686,6 +702,14 @@ declare function isICU(template: string): boolean;
686
702
  * with no compiler collision and stay `{name}`-only.
687
703
  */
688
704
  declare function normalizeMarkupPlaceholders(text: string): string;
705
+ /**
706
+ * Params keys that have NO matching placeholder in `texts`.
707
+ *
708
+ * Texts are expected in canonical form (post-`normalizeMarkupPlaceholders`),
709
+ * so a key counts as used when it appears as `{key}` or as the argument of an
710
+ * ICU slot (`{key, plural, …}`). Surrounding whitespace is tolerated.
711
+ */
712
+ declare function findUnusedParamKeys(texts: string[], params: Record<string, unknown> | undefined): string[];
689
713
  /**
690
714
  * Substitute placeholders in a translated string with values from `params`.
691
715
  *
@@ -754,4 +778,4 @@ interface iProject {
754
778
  declare const t: TFunction;
755
779
  declare const tSignal: Signal<TFunction>;
756
780
 
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 };
781
+ 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, findUnusedParamKeys, 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
@@ -268,6 +268,25 @@ var ICU_PATTERN = /\{[^{}]+,\s*(plural|select|selectordinal|number|date|time)\s*
268
268
  function normalizeMarkupPlaceholders(text) {
269
269
  return text.replace(/%([A-Za-z_][A-Za-z0-9_]*)%/g, "{$1}");
270
270
  }
271
+ function escapeForRegExp(key) {
272
+ return key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
273
+ }
274
+ function findUnusedParamKeys(texts, params) {
275
+ if (!params) return [];
276
+ const keys = Object.keys(params);
277
+ if (!keys.length || !texts.length) return keys;
278
+ const haystack = texts.join("\0");
279
+ return keys.filter((key) => !new RegExp(`\\{\\s*${escapeForRegExp(key)}\\s*[,}]`).test(haystack));
280
+ }
281
+ function warnUnmatchedParams(source, texts, params, context) {
282
+ if (!logger.debugEnabled) return;
283
+ const unused = findUnusedParamKeys(texts, params);
284
+ if (!unused.length) return;
285
+ const keyList = unused.map((key) => `%${key}%`).join(", ");
286
+ logger.warn(
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
+ );
289
+ }
271
290
  function interpolate(template, params, locale) {
272
291
  if (isICU(template)) {
273
292
  try {
@@ -1248,6 +1267,8 @@ var Translate = class {
1248
1267
  this.isTokenizing = false;
1249
1268
  this.lastTranslatedLocale = "";
1250
1269
  this.unsubscribeLocale = null;
1270
+ /** Sorted params key-set already checked, so value-only updates don't re-warn. */
1271
+ this.checkedParamKeys = null;
1251
1272
  this.element = element;
1252
1273
  this.options = { ...options };
1253
1274
  this.custom_id = options.custom_id || "";
@@ -1259,6 +1280,7 @@ var Translate = class {
1259
1280
  /** Update interpolation params (e.g. a changed count) and re-render. Mirrors `Phrase.setParams`. */
1260
1281
  setParams(params) {
1261
1282
  this.options.params = params;
1283
+ this.checkParams();
1262
1284
  if (!this.parseComplete) return;
1263
1285
  this.lastTranslatedLocale = "";
1264
1286
  const locale = currentlyLoadedLocale.get();
@@ -1292,6 +1314,7 @@ var Translate = class {
1292
1314
  this.isTokenizing = true;
1293
1315
  const { tokens, content } = tokenizeElement(this.element);
1294
1316
  this.tokens = tokens;
1317
+ this.checkParams();
1295
1318
  const { category = "" } = this.options;
1296
1319
  if (this.tokens.length === 1) {
1297
1320
  this.element.innerText = this.applyParams(LangsysApp.Translations.t(this.tokens[0], category));
@@ -1365,6 +1388,19 @@ var Translate = class {
1365
1388
  const { category = "" } = this.options;
1366
1389
  return LangsysApp.Translations.lookupContent(category, this.custom_id, token);
1367
1390
  }
1391
+ /**
1392
+ * Debug-time check that every supplied param has a placeholder to land in.
1393
+ * Re-runs only when the key SET changes — a changing `count` value must not
1394
+ * re-warn on every render.
1395
+ */
1396
+ checkParams() {
1397
+ if (!this.tokens.length) return;
1398
+ const { params, category = "" } = this.options;
1399
+ const signature = params ? Object.keys(params).sort().join(",") : "";
1400
+ if (signature === this.checkedParamKeys) return;
1401
+ this.checkedParamKeys = signature;
1402
+ warnUnmatchedParams("<Translate>", this.tokens, params, category || void 0);
1403
+ }
1368
1404
  /**
1369
1405
  * Interpolate `{name}`-style params into a resolved text, matching `t()`:
1370
1406
  * unknown keys fall through untouched, and the untranslated fallback is
@@ -1499,6 +1535,8 @@ var Phrase = class {
1499
1535
  this.slots = [];
1500
1536
  this.unsubscribers = [];
1501
1537
  this.ready = false;
1538
+ /** Sorted params key-set already checked, so value-only updates don't re-warn. */
1539
+ this.checkedParamKeys = null;
1502
1540
  this.host = host;
1503
1541
  this.category = options.category ?? "";
1504
1542
  this.params = options.params ?? {};
@@ -1512,7 +1550,9 @@ var Phrase = class {
1512
1550
  /** Update interpolation params (e.g. a changed count) and re-render. */
1513
1551
  setParams(params) {
1514
1552
  this.params = params ?? {};
1515
- if (this.ready) this._render();
1553
+ this._checkParams();
1554
+ if (!this.ready) return;
1555
+ this._render();
1516
1556
  }
1517
1557
  /** Stop reacting to locale/translation changes. Safe to call multiple times. */
1518
1558
  destroy() {
@@ -1527,11 +1567,24 @@ var Phrase = class {
1527
1567
  const { phrase, slots } = encodeRichText(this.host);
1528
1568
  this.phrase = phrase;
1529
1569
  this.slots = slots;
1570
+ this._checkParams();
1530
1571
  await LangsysApp.Translations.ready();
1531
1572
  LangsysApp.Translations.t(this.phrase, this.category);
1532
1573
  this.ready = true;
1533
1574
  this._render();
1534
1575
  }
1576
+ /**
1577
+ * Debug-time check that every supplied param has a placeholder to land in.
1578
+ * Re-runs only when the key SET changes — a changing `n` value must not
1579
+ * re-warn on every render.
1580
+ */
1581
+ _checkParams() {
1582
+ if (!this.phrase) return;
1583
+ const signature = Object.keys(this.params).sort().join(",");
1584
+ if (signature === this.checkedParamKeys) return;
1585
+ this.checkedParamKeys = signature;
1586
+ warnUnmatchedParams("<Phrase>", [this.phrase], this.params, this.category || void 0);
1587
+ }
1535
1588
  _render() {
1536
1589
  if (!this.phrase) return;
1537
1590
  const raw = LangsysApp.Translations.lookup(this.phrase, this.category) ?? this.phrase;
@@ -1559,6 +1612,7 @@ exports.canonicalizeLocale = canonicalizeLocale;
1559
1612
  exports.createSignal = createSignal;
1560
1613
  exports.currentlyLoadedLocale = currentlyLoadedLocale;
1561
1614
  exports.encodeRichText = encodeRichText;
1615
+ exports.findUnusedParamKeys = findUnusedParamKeys;
1562
1616
  exports.generateCustomId = generateCustomId;
1563
1617
  exports.getValue = getValue;
1564
1618
  exports.interpolate = interpolate;