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/CHANGELOG.md +27 -0
- package/README.md +47 -0
- package/dist/index.d.mts +41 -1
- package/dist/index.d.ts +41 -1
- package/dist/index.js +69 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +68 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|