langsys-js-typescript 0.6.2 → 0.6.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,29 @@
1
+ ## 0.6.3 - 2026-08-16
2
+
3
+ ### Fixed
4
+
5
+ - **`<select>` option text was harvested twice, diverging `custom_id` from every other Langsys SDK.** A `<select>` special case pushed each `<option>`'s text, and the ordinary text-node walk then pushed it again — `<option>` cannot contain elements, so the walker already covered it. Any content block containing a `<select>` therefore produced `["S","S"]` where `langsys-php` produced `["S"]`, and had a different `custom_id` in the two SDKs since before either was audited. The fixture suite could not see it: those fixtures are synthetic token lists that never touch a DOM.
6
+
7
+ **Migration is automatic and lookup-only**, extending the mechanism 0.6.0 introduced. Three historical id shapes are now tried before registering:
8
+
9
+ | tokens | hash | registered by |
10
+ |---|---|---|
11
+ | corrected | corrected | 0.6.3+ (current) |
12
+ | legacy (duplicated) | corrected | 0.6.0 – 0.6.2 |
13
+ | legacy (duplicated) | legacy | before 0.6.0 |
14
+
15
+ Registration always uses the corrected id, so the legacy-keyed population can only shrink. Content without a `<select>` is unaffected — its token list is identical either way, so nothing needlessly rebases. Without this fallback the upgrade would have been silent in the worst direction: a dropdown quietly reverting to source text, with nothing erroring to explain it.
16
+
17
+ - **`legacyTokenizeElement(element)`** — the pre-0.6.3 token list, exported for migration tooling. Lookup-only; never register under an id derived from it. Deprecated on arrival.
18
+
19
+ ### Added
20
+
21
+ - **Four standard attributes are now harvested: `label`, `aria-description`, `aria-valuetext`, `aria-roledescription`.** `langsys-php` harvests a superset of our list; diffing the two showed nothing JS-only and 16 PHP-only, of which these four carry real user-visible text. `label` is what a user reads in a `<select>` picker (`<option>`, `<optgroup>`, `<track>`) and is invisible to sighted testing that only inspects option text; the three ARIA attributes are read aloud by screen readers, so leaving them untranslated degrades accessibility specifically for the users least able to work around it.
22
+
23
+ The remaining twelve PHP-only attributes are framework conventions (Bootstrap `data-bs-*`, Rails-style `data-confirm`, loading/notification text) and are deliberately **not** mirrored — they're written by server-rendered templates, and a JS app renders those strings through its own components.
24
+
25
+ **Note:** a content block containing one of these attributes now yields a different token list, and therefore a different `custom_id`. Such blocks re-register and their existing translations orphan. The 0.6.0 legacy-id fallback cannot help here — it re-hashes the *same* tokens, and this changes the tokens themselves. Affected population is only blocks that use these four attributes.
26
+
1
27
  ## 0.6.2 - 2026-08-16
2
28
 
3
29
  ### Added
package/dist/index.d.mts CHANGED
@@ -738,6 +738,23 @@ declare function tokenizeElement(element: HTMLElement): {
738
738
  tokens: string[];
739
739
  content: string;
740
740
  };
741
+ /**
742
+ * The token list this element would have produced before 0.6.3, when every
743
+ * `<option>`'s text was harvested twice — once by a `<select>` special case and
744
+ * again by the ordinary text-node walk.
745
+ *
746
+ * **Lookup only.** Content blocks registered by an older SDK are keyed by an id
747
+ * derived from this list, so `Translate` falls back to it when the corrected id
748
+ * misses; without that, every block containing a `<select>` would silently
749
+ * re-key and lose its translations on upgrade. Registration always uses the
750
+ * corrected list, so the legacy-keyed population can only shrink.
751
+ *
752
+ * Skips the computed-style capture that `tokenizeElement` performs — this is a
753
+ * cache-key computation, not a snapshot for translators.
754
+ *
755
+ * @deprecated Migration aid; will be removed once catalogs have been rebased.
756
+ */
757
+ declare function legacyTokenizeElement(element: HTMLElement): string[];
741
758
 
742
759
  /**
743
760
  * Create a Signal whose value is persisted to localStorage (when available).
@@ -876,4 +893,4 @@ interface iProject {
876
893
  declare const t: TFunction;
877
894
  declare const tSignal: Signal<TFunction>;
878
895
 
879
- export { type EncodedRichText, type ExtractParamKeys, LangsysApp, LangsysAppAPI, type LocaleSource, Logger, PHRASE_MARKER_ATTR, PHRASE_MARKER_ATTRS, 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, generateLegacyCustomId, 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, isPhraseMarked, isTranslationExcluded, logger, markupTokenValues, md5, md5Legacy, normalizeMarkupPlaceholders, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };
896
+ export { type EncodedRichText, type ExtractParamKeys, LangsysApp, LangsysAppAPI, type LocaleSource, Logger, PHRASE_MARKER_ATTR, PHRASE_MARKER_ATTRS, 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, generateLegacyCustomId, 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, isPhraseMarked, isTranslationExcluded, legacyTokenizeElement, logger, markupTokenValues, md5, md5Legacy, normalizeMarkupPlaceholders, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };
package/dist/index.d.ts CHANGED
@@ -738,6 +738,23 @@ declare function tokenizeElement(element: HTMLElement): {
738
738
  tokens: string[];
739
739
  content: string;
740
740
  };
741
+ /**
742
+ * The token list this element would have produced before 0.6.3, when every
743
+ * `<option>`'s text was harvested twice — once by a `<select>` special case and
744
+ * again by the ordinary text-node walk.
745
+ *
746
+ * **Lookup only.** Content blocks registered by an older SDK are keyed by an id
747
+ * derived from this list, so `Translate` falls back to it when the corrected id
748
+ * misses; without that, every block containing a `<select>` would silently
749
+ * re-key and lose its translations on upgrade. Registration always uses the
750
+ * corrected list, so the legacy-keyed population can only shrink.
751
+ *
752
+ * Skips the computed-style capture that `tokenizeElement` performs — this is a
753
+ * cache-key computation, not a snapshot for translators.
754
+ *
755
+ * @deprecated Migration aid; will be removed once catalogs have been rebased.
756
+ */
757
+ declare function legacyTokenizeElement(element: HTMLElement): string[];
741
758
 
742
759
  /**
743
760
  * Create a Signal whose value is persisted to localStorage (when available).
@@ -876,4 +893,4 @@ interface iProject {
876
893
  declare const t: TFunction;
877
894
  declare const tSignal: Signal<TFunction>;
878
895
 
879
- export { type EncodedRichText, type ExtractParamKeys, LangsysApp, LangsysAppAPI, type LocaleSource, Logger, PHRASE_MARKER_ATTR, PHRASE_MARKER_ATTRS, 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, generateLegacyCustomId, 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, isPhraseMarked, isTranslationExcluded, logger, markupTokenValues, md5, md5Legacy, normalizeMarkupPlaceholders, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };
896
+ export { type EncodedRichText, type ExtractParamKeys, LangsysApp, LangsysAppAPI, type LocaleSource, Logger, PHRASE_MARKER_ATTR, PHRASE_MARKER_ATTRS, 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, generateLegacyCustomId, 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, isPhraseMarked, isTranslationExcluded, legacyTokenizeElement, logger, markupTokenValues, md5, md5Legacy, normalizeMarkupPlaceholders, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };
package/dist/index.js CHANGED
@@ -1076,8 +1076,14 @@ var TRANSLATABLE_ATTRIBUTES = [
1076
1076
  "placeholder",
1077
1077
  "alt",
1078
1078
  "title",
1079
+ "label",
1080
+ // <option>, <optgroup>, <track> — the text a user reads in the picker
1079
1081
  "aria-label",
1080
1082
  "aria-placeholder",
1083
+ "aria-description",
1084
+ "aria-valuetext",
1085
+ // the spoken value of a slider/meter
1086
+ "aria-roledescription",
1081
1087
  "data-error",
1082
1088
  "data-error-message",
1083
1089
  "data-validation-message",
@@ -1194,9 +1200,15 @@ async function registerContentBlock(contentBlock) {
1194
1200
  function tokenizeElement(element) {
1195
1201
  const tokens = [];
1196
1202
  const clone = element.cloneNode(true);
1197
- _walkForTokens(element, Array.from(clone.childNodes), tokens, []);
1203
+ _walkForTokens(element, Array.from(clone.childNodes), tokens, [], false, true);
1198
1204
  return { tokens, content: normalizeMarkupPlaceholders(clone.outerHTML) };
1199
1205
  }
1206
+ function legacyTokenizeElement(element) {
1207
+ const tokens = [];
1208
+ const clone = element.cloneNode(true);
1209
+ _walkForTokens(element, Array.from(clone.childNodes), tokens, [], true, false);
1210
+ return tokens;
1211
+ }
1200
1212
  function _writeKnownContentBlockToCache(category, customId) {
1201
1213
  const lookupCat = category || "__uncategorized__";
1202
1214
  sTranslations.update((current) => {
@@ -1210,18 +1222,18 @@ function _writeKnownContentBlockToCache(category, customId) {
1210
1222
  return { ...current };
1211
1223
  });
1212
1224
  }
1213
- function _walkForTokens(liveRoot, cloneNodes, tokens, indices) {
1225
+ function _walkForTokens(liveRoot, cloneNodes, tokens, indices, duplicateSelectOptions, applyStyles) {
1214
1226
  cloneNodes.forEach((node, index) => {
1215
1227
  if (node.nodeType === Node.ELEMENT_NODE) {
1216
1228
  const el = node;
1217
1229
  if (isTranslationExcluded(el)) return;
1218
1230
  if (isPhraseMarked(el)) return;
1219
1231
  }
1220
- if (node.hasChildNodes()) {
1232
+ if (applyStyles && node.hasChildNodes()) {
1221
1233
  _applyStylesToClone(liveRoot, node, [...indices, index]);
1222
1234
  }
1223
1235
  if (node.nodeType === Node.ELEMENT_NODE) {
1224
- _tokenizeAttributes(node, tokens);
1236
+ _tokenizeAttributes(node, tokens, duplicateSelectOptions);
1225
1237
  }
1226
1238
  const contentToken = node.nodeValue?.replace(/\s+/g, " ").trim();
1227
1239
  if (node.nodeType === Node.TEXT_NODE && contentToken) {
@@ -1229,10 +1241,10 @@ function _walkForTokens(liveRoot, cloneNodes, tokens, indices) {
1229
1241
  return;
1230
1242
  }
1231
1243
  if (!node.hasChildNodes()) return;
1232
- _walkForTokens(liveRoot, Array.from(node.childNodes), tokens, [...indices, index]);
1244
+ _walkForTokens(liveRoot, Array.from(node.childNodes), tokens, [...indices, index], duplicateSelectOptions, applyStyles);
1233
1245
  });
1234
1246
  }
1235
- function _tokenizeAttributes(element, tokens) {
1247
+ function _tokenizeAttributes(element, tokens, duplicateSelectOptions) {
1236
1248
  const tagName = element.tagName.toLowerCase();
1237
1249
  if (tagName === "img") {
1238
1250
  const img = element;
@@ -1253,7 +1265,7 @@ function _tokenizeAttributes(element, tokens) {
1253
1265
  if (value) tokens.push(normalizeMarkupPlaceholders(value));
1254
1266
  }
1255
1267
  }
1256
- if (tagName === "select") {
1268
+ if (duplicateSelectOptions && tagName === "select") {
1257
1269
  const options = element.querySelectorAll("option");
1258
1270
  options.forEach((option) => {
1259
1271
  const optionText = option.textContent?.trim();
@@ -1379,9 +1391,15 @@ var Translate = class {
1379
1391
  return;
1380
1392
  }
1381
1393
  if (derivedId) {
1382
- const legacyId = generateLegacyCustomId(contentBlock.category, contentBlock.tokens);
1383
- if (legacyId !== this.custom_id && isContentBlockKnown(contentBlock.category, legacyId)) {
1384
- this.custom_id = legacyId;
1394
+ const legacyTokens = legacyTokenizeElement(this.element);
1395
+ const candidates = [
1396
+ generateCustomId(contentBlock.category, legacyTokens),
1397
+ generateLegacyCustomId(contentBlock.category, legacyTokens)
1398
+ ];
1399
+ for (const candidate of candidates) {
1400
+ if (candidate === this.custom_id) continue;
1401
+ if (!isContentBlockKnown(contentBlock.category, candidate)) continue;
1402
+ this.custom_id = candidate;
1385
1403
  if (this.tokens.length > 1 && this.element) {
1386
1404
  this.translate(Array.from(this.element.childNodes));
1387
1405
  this.lastTranslatedLocale = currentlyLoadedLocale.get();
@@ -1667,6 +1685,7 @@ exports.isEmpty = isEmpty;
1667
1685
  exports.isICU = isICU;
1668
1686
  exports.isPhraseMarked = isPhraseMarked;
1669
1687
  exports.isTranslationExcluded = isTranslationExcluded;
1688
+ exports.legacyTokenizeElement = legacyTokenizeElement;
1670
1689
  exports.logger = logger;
1671
1690
  exports.markupTokenValues = markupTokenValues;
1672
1691
  exports.md5 = md5;