langsys-js-typescript 0.4.0 → 0.4.2

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/dist/index.mjs CHANGED
@@ -263,6 +263,28 @@ function isICU(template) {
263
263
  return ICU_PATTERN.test(template);
264
264
  }
265
265
  var ICU_PATTERN = /\{[^{}]+,\s*(plural|select|selectordinal|number|date|time)\s*[,}]/;
266
+ function normalizeMarkupPlaceholders(text) {
267
+ return text.replace(/%([A-Za-z_][A-Za-z0-9_]*)%/g, "{$1}");
268
+ }
269
+ function escapeForRegExp(key) {
270
+ return key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
271
+ }
272
+ function findUnusedParamKeys(texts, params) {
273
+ if (!params) return [];
274
+ const keys = Object.keys(params);
275
+ if (!keys.length || !texts.length) return keys;
276
+ const haystack = texts.join("\0");
277
+ return keys.filter((key) => !new RegExp(`\\{\\s*${escapeForRegExp(key)}\\s*[,}]`).test(haystack));
278
+ }
279
+ function warnUnmatchedParams(source, texts, params, context) {
280
+ if (!logger.debugEnabled) return;
281
+ const unused = findUnusedParamKeys(texts, params);
282
+ if (!unused.length) return;
283
+ const keyList = unused.map((key) => `%${key}%`).join(", ");
284
+ logger.warn(
285
+ `${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}]` : "")
286
+ );
287
+ }
266
288
  function interpolate(template, params, locale) {
267
289
  if (isICU(template)) {
268
290
  try {
@@ -1139,7 +1161,7 @@ function tokenizeElement(element) {
1139
1161
  const tokens = [];
1140
1162
  const clone = element.cloneNode(true);
1141
1163
  _walkForTokens(element, Array.from(clone.childNodes), tokens, []);
1142
- return { tokens, content: clone.outerHTML };
1164
+ return { tokens, content: normalizeMarkupPlaceholders(clone.outerHTML) };
1143
1165
  }
1144
1166
  function _writeKnownContentBlockToCache(category, customId) {
1145
1167
  const lookupCat = category || "__uncategorized__";
@@ -1169,7 +1191,7 @@ function _walkForTokens(liveRoot, cloneNodes, tokens, indices) {
1169
1191
  }
1170
1192
  const contentToken = node.nodeValue?.replace(/\s+/g, " ").trim();
1171
1193
  if (node.nodeType === Node.TEXT_NODE && contentToken) {
1172
- tokens.push(contentToken);
1194
+ tokens.push(normalizeMarkupPlaceholders(contentToken));
1173
1195
  return;
1174
1196
  }
1175
1197
  if (!node.hasChildNodes()) return;
@@ -1184,24 +1206,24 @@ function _tokenizeAttributes(element, tokens) {
1184
1206
  }
1185
1207
  for (const attr of TRANSLATABLE_ATTRIBUTES) {
1186
1208
  const value = element.getAttribute(attr)?.trim();
1187
- if (value) tokens.push(value);
1209
+ if (value) tokens.push(normalizeMarkupPlaceholders(value));
1188
1210
  }
1189
1211
  if (VALUE_TRANSLATABLE_ELEMENTS.includes(tagName)) {
1190
1212
  const value = element.getAttribute("value")?.trim();
1191
- if (value) tokens.push(value);
1213
+ if (value) tokens.push(normalizeMarkupPlaceholders(value));
1192
1214
  }
1193
1215
  if (tagName === "input") {
1194
1216
  const inputType = element.getAttribute("type")?.toLowerCase();
1195
1217
  if (inputType && VALUE_TRANSLATABLE_INPUT_TYPES.includes(inputType)) {
1196
1218
  const value = element.getAttribute("value")?.trim();
1197
- if (value) tokens.push(value);
1219
+ if (value) tokens.push(normalizeMarkupPlaceholders(value));
1198
1220
  }
1199
1221
  }
1200
1222
  if (tagName === "select") {
1201
1223
  const options = element.querySelectorAll("option");
1202
1224
  options.forEach((option) => {
1203
1225
  const optionText = option.textContent?.trim();
1204
- if (optionText) tokens.push(optionText);
1226
+ if (optionText) tokens.push(normalizeMarkupPlaceholders(optionText));
1205
1227
  });
1206
1228
  }
1207
1229
  }
@@ -1243,6 +1265,8 @@ var Translate = class {
1243
1265
  this.isTokenizing = false;
1244
1266
  this.lastTranslatedLocale = "";
1245
1267
  this.unsubscribeLocale = null;
1268
+ /** Sorted params key-set already checked, so value-only updates don't re-warn. */
1269
+ this.checkedParamKeys = null;
1246
1270
  this.element = element;
1247
1271
  this.options = { ...options };
1248
1272
  this.custom_id = options.custom_id || "";
@@ -1254,6 +1278,7 @@ var Translate = class {
1254
1278
  /** Update interpolation params (e.g. a changed count) and re-render. Mirrors `Phrase.setParams`. */
1255
1279
  setParams(params) {
1256
1280
  this.options.params = params;
1281
+ this.checkParams();
1257
1282
  if (!this.parseComplete) return;
1258
1283
  this.lastTranslatedLocale = "";
1259
1284
  const locale = currentlyLoadedLocale.get();
@@ -1287,6 +1312,7 @@ var Translate = class {
1287
1312
  this.isTokenizing = true;
1288
1313
  const { tokens, content } = tokenizeElement(this.element);
1289
1314
  this.tokens = tokens;
1315
+ this.checkParams();
1290
1316
  const { category = "" } = this.options;
1291
1317
  if (this.tokens.length === 1) {
1292
1318
  this.element.innerText = this.applyParams(LangsysApp.Translations.t(this.tokens[0], category));
@@ -1342,7 +1368,7 @@ var Translate = class {
1342
1368
  }
1343
1369
  if (isEmpty(node.nodeValue?.trim())) return;
1344
1370
  if (isEmpty(node.originalNodeValue)) {
1345
- node.originalNodeValue = node.nodeValue;
1371
+ node.originalNodeValue = normalizeMarkupPlaceholders(node.nodeValue);
1346
1372
  }
1347
1373
  const contentToken = node.originalNodeValue?.replace(/\s+/g, " ").trim();
1348
1374
  if (!contentToken) return;
@@ -1360,6 +1386,19 @@ var Translate = class {
1360
1386
  const { category = "" } = this.options;
1361
1387
  return LangsysApp.Translations.lookupContent(category, this.custom_id, token);
1362
1388
  }
1389
+ /**
1390
+ * Debug-time check that every supplied param has a placeholder to land in.
1391
+ * Re-runs only when the key SET changes — a changing `count` value must not
1392
+ * re-warn on every render.
1393
+ */
1394
+ checkParams() {
1395
+ if (!this.tokens.length) return;
1396
+ const { params, category = "" } = this.options;
1397
+ const signature = params ? Object.keys(params).sort().join(",") : "";
1398
+ if (signature === this.checkedParamKeys) return;
1399
+ this.checkedParamKeys = signature;
1400
+ warnUnmatchedParams("<Translate>", this.tokens, params, category || void 0);
1401
+ }
1363
1402
  /**
1364
1403
  * Interpolate `{name}`-style params into a resolved text, matching `t()`:
1365
1404
  * unknown keys fall through untouched, and the untranslated fallback is
@@ -1391,7 +1430,7 @@ var Translate = class {
1391
1430
  const optionEl = option;
1392
1431
  if (!optionEl.originalAttributes) optionEl.originalAttributes = {};
1393
1432
  if (optionEl.originalAttributes["textContent"] === void 0) {
1394
- optionEl.originalAttributes["textContent"] = option.textContent || "";
1433
+ optionEl.originalAttributes["textContent"] = normalizeMarkupPlaceholders(option.textContent || "");
1395
1434
  }
1396
1435
  const originalText = optionEl.originalAttributes["textContent"].trim();
1397
1436
  if (originalText) {
@@ -1405,7 +1444,7 @@ var Translate = class {
1405
1444
  const currentValue = element.getAttribute(attr);
1406
1445
  if (!currentValue) return;
1407
1446
  if (element.originalAttributes[attr] === void 0) {
1408
- element.originalAttributes[attr] = currentValue;
1447
+ element.originalAttributes[attr] = normalizeMarkupPlaceholders(currentValue);
1409
1448
  }
1410
1449
  const originalValue = element.originalAttributes[attr].trim();
1411
1450
  if (!originalValue) return;
@@ -1424,7 +1463,7 @@ var SENT_SCAN = /\uE000(\d+)\uE001|\uE002(\d+)\uE003/g;
1424
1463
  function encodeRichText(root) {
1425
1464
  const slots = [];
1426
1465
  const phrase = _encodeNodes(Array.from(root.childNodes), slots);
1427
- return { phrase: phrase.replace(/\s+/g, " ").trim(), slots };
1466
+ return { phrase: normalizeMarkupPlaceholders(phrase.replace(/\s+/g, " ").trim()), slots };
1428
1467
  }
1429
1468
  function _encodeNodes(nodes, slots) {
1430
1469
  let out = "";
@@ -1494,6 +1533,8 @@ var Phrase = class {
1494
1533
  this.slots = [];
1495
1534
  this.unsubscribers = [];
1496
1535
  this.ready = false;
1536
+ /** Sorted params key-set already checked, so value-only updates don't re-warn. */
1537
+ this.checkedParamKeys = null;
1497
1538
  this.host = host;
1498
1539
  this.category = options.category ?? "";
1499
1540
  this.params = options.params ?? {};
@@ -1507,7 +1548,9 @@ var Phrase = class {
1507
1548
  /** Update interpolation params (e.g. a changed count) and re-render. */
1508
1549
  setParams(params) {
1509
1550
  this.params = params ?? {};
1510
- if (this.ready) this._render();
1551
+ this._checkParams();
1552
+ if (!this.ready) return;
1553
+ this._render();
1511
1554
  }
1512
1555
  /** Stop reacting to locale/translation changes. Safe to call multiple times. */
1513
1556
  destroy() {
@@ -1522,11 +1565,24 @@ var Phrase = class {
1522
1565
  const { phrase, slots } = encodeRichText(this.host);
1523
1566
  this.phrase = phrase;
1524
1567
  this.slots = slots;
1568
+ this._checkParams();
1525
1569
  await LangsysApp.Translations.ready();
1526
1570
  LangsysApp.Translations.t(this.phrase, this.category);
1527
1571
  this.ready = true;
1528
1572
  this._render();
1529
1573
  }
1574
+ /**
1575
+ * Debug-time check that every supplied param has a placeholder to land in.
1576
+ * Re-runs only when the key SET changes — a changing `n` value must not
1577
+ * re-warn on every render.
1578
+ */
1579
+ _checkParams() {
1580
+ if (!this.phrase) return;
1581
+ const signature = Object.keys(this.params).sort().join(",");
1582
+ if (signature === this.checkedParamKeys) return;
1583
+ this.checkedParamKeys = signature;
1584
+ warnUnmatchedParams("<Phrase>", [this.phrase], this.params, this.category || void 0);
1585
+ }
1530
1586
  _render() {
1531
1587
  if (!this.phrase) return;
1532
1588
  const raw = LangsysApp.Translations.lookup(this.phrase, this.category) ?? this.phrase;
@@ -1543,6 +1599,6 @@ var t = ((phrase, ...rest) => {
1543
1599
  });
1544
1600
  var tSignal = LangsysApp.Translations.tSignal;
1545
1601
 
1546
- export { LangsysApp, LangsysAppAPI, Logger, PHRASE_MARKER_ATTR, Phrase, Translate, Translations, canonicalizeLocale, createSignal, currentlyLoadedLocale, encodeRichText, generateCustomId, getValue, interpolate, isContentBlockKnown, isEmpty, isICU, logger, markupTokenValues, md5, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };
1602
+ export { LangsysApp, LangsysAppAPI, Logger, PHRASE_MARKER_ATTR, Phrase, Translate, Translations, canonicalizeLocale, createSignal, currentlyLoadedLocale, encodeRichText, findUnusedParamKeys, generateCustomId, getValue, interpolate, isContentBlockKnown, isEmpty, isICU, logger, markupTokenValues, md5, normalizeMarkupPlaceholders, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };
1547
1603
  //# sourceMappingURL=index.mjs.map
1548
1604
  //# sourceMappingURL=index.mjs.map