langsys-js-typescript 0.1.0 → 0.2.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/README.md +17 -17
- package/dist/index.d.mts +288 -31
- package/dist/index.d.ts +288 -31
- package/dist/index.js +361 -127
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +351 -127
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -2
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { IntlMessageFormat } from 'intl-messageformat';
|
|
2
|
+
|
|
1
3
|
// src/signal.ts
|
|
2
4
|
function createSignal(initial) {
|
|
3
5
|
let value = initial;
|
|
@@ -67,7 +69,11 @@ var LangsysAppAPIClass = class {
|
|
|
67
69
|
};
|
|
68
70
|
this.headers = {
|
|
69
71
|
"Content-Type": "application/json; charset=utf-8",
|
|
70
|
-
"x-Authorization": this.config.key
|
|
72
|
+
"x-Authorization": this.config.key,
|
|
73
|
+
// Declare ICU MessageFormat rendering so the server sends raw ICU
|
|
74
|
+
// (plural/select) instead of flat-template downgrades. Pre-ICU
|
|
75
|
+
// builds omit this header and keep receiving flat strings.
|
|
76
|
+
"X-Langsys-Capabilities": "icu"
|
|
71
77
|
};
|
|
72
78
|
}
|
|
73
79
|
setup(config2) {
|
|
@@ -209,7 +215,6 @@ var initialTranslations = {
|
|
|
209
215
|
};
|
|
210
216
|
var sTranslations = persist("translations", initialTranslations);
|
|
211
217
|
var currentlyLoadedLocale = createSignal("");
|
|
212
|
-
var contentBlocks = createSignal([]);
|
|
213
218
|
var config = {
|
|
214
219
|
projectid: "",
|
|
215
220
|
key: "",
|
|
@@ -218,10 +223,22 @@ var config = {
|
|
|
218
223
|
debug: false,
|
|
219
224
|
key_type: "read"
|
|
220
225
|
};
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
226
|
+
function isICU(template) {
|
|
227
|
+
return ICU_PATTERN.test(template);
|
|
228
|
+
}
|
|
229
|
+
var ICU_PATTERN = /\{[^{}]+,\s*(plural|select|selectordinal|number|date|time)\s*,/;
|
|
230
|
+
function interpolate(template, params, locale) {
|
|
231
|
+
if (isICU(template)) {
|
|
232
|
+
try {
|
|
233
|
+
return new IntlMessageFormat(template, locale ?? "en").format(params);
|
|
234
|
+
} catch {
|
|
235
|
+
return simpleInterpolate(template, params);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return simpleInterpolate(template, params);
|
|
239
|
+
}
|
|
240
|
+
function simpleInterpolate(template, params) {
|
|
241
|
+
return template.replace(/\{([^{},]+)\}/g, (match, rawKey) => {
|
|
225
242
|
const key = rawKey.trim();
|
|
226
243
|
if (!(key in params)) return match;
|
|
227
244
|
const value = params[key];
|
|
@@ -245,6 +262,11 @@ var Translations = class {
|
|
|
245
262
|
this.timer = null;
|
|
246
263
|
this.flushScheduled = false;
|
|
247
264
|
this.isFirstClientRun = true;
|
|
265
|
+
this.readyResolve = () => {
|
|
266
|
+
};
|
|
267
|
+
this.readyPromise = new Promise((resolve) => {
|
|
268
|
+
this.readyResolve = resolve;
|
|
269
|
+
});
|
|
248
270
|
this.config = config2;
|
|
249
271
|
this.debug = new Logger(!!config2.debug);
|
|
250
272
|
this.locale = config2.baseLocale || "";
|
|
@@ -253,6 +275,16 @@ var Translations = class {
|
|
|
253
275
|
currentlyLoadedLocale.subscribe(() => this.tSignal.set(this.buildTFn()));
|
|
254
276
|
if (config2.key && config2.projectid) this.setup(config2);
|
|
255
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Resolves once the first `getTranslations` call has completed (or
|
|
280
|
+
* immediately if no fetch is configured). Callers that need to read
|
|
281
|
+
* `sTranslations` before issuing a write — e.g. the `Translate` DOM
|
|
282
|
+
* class deciding whether to POST a content block — should await this
|
|
283
|
+
* to avoid acting on an empty cache during the cold-start race.
|
|
284
|
+
*/
|
|
285
|
+
ready() {
|
|
286
|
+
return this.readyPromise;
|
|
287
|
+
}
|
|
256
288
|
/** Current translation function. Reads fresh state on every call. */
|
|
257
289
|
get t() {
|
|
258
290
|
return this.tSignal.get();
|
|
@@ -275,8 +307,22 @@ var Translations = class {
|
|
|
275
307
|
const value = contentData[token];
|
|
276
308
|
return typeof value === "string" ? value : null;
|
|
277
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Raw stored translation for a phrase, without interpolation or
|
|
312
|
+
* missing-token registration. Returns null when no (non-empty) translation
|
|
313
|
+
* is cached. Used by the `Phrase` rich-text handler, which needs the
|
|
314
|
+
* un-interpolated template (it supplies its own markup-token values).
|
|
315
|
+
*/
|
|
316
|
+
lookup(phrase, category) {
|
|
317
|
+
const cats = sTranslations.get();
|
|
318
|
+
const lookupCat = category || "__uncategorized__";
|
|
319
|
+
const value = cats[lookupCat]?.[phrase];
|
|
320
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
321
|
+
}
|
|
278
322
|
buildTFn() {
|
|
279
|
-
|
|
323
|
+
const fn = ((phrase, ...rest) => {
|
|
324
|
+
const category = typeof rest[0] === "string" ? rest[0] : "";
|
|
325
|
+
const params = typeof rest[0] === "object" ? rest[0] : rest[1];
|
|
280
326
|
const cats = sTranslations.get();
|
|
281
327
|
const lookupCat = category || "__uncategorized__";
|
|
282
328
|
const value = cats[lookupCat]?.[phrase];
|
|
@@ -285,12 +331,12 @@ var Translations = class {
|
|
|
285
331
|
this.debug.log("TRANSLATION FOUND", [lookupCat, phrase, value]);
|
|
286
332
|
translated = value;
|
|
287
333
|
} else {
|
|
288
|
-
this.missingToken(
|
|
334
|
+
this.missingToken(category, phrase);
|
|
289
335
|
translated = phrase;
|
|
290
336
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
337
|
+
return params ? interpolate(translated, params, currentlyLoadedLocale.get()) : translated;
|
|
338
|
+
});
|
|
339
|
+
return fn;
|
|
294
340
|
}
|
|
295
341
|
missingToken(category, token) {
|
|
296
342
|
if (token === void 0 || token === null) {
|
|
@@ -418,11 +464,12 @@ var Translations = class {
|
|
|
418
464
|
const currentData = sTranslations.get();
|
|
419
465
|
this.missingTokens = this.missingTokens.filter((tokenObj) => {
|
|
420
466
|
tokenObj.projectid = this.config.projectid;
|
|
421
|
-
|
|
467
|
+
const lookupCat = tokenObj.category || "__uncategorized__";
|
|
468
|
+
if (lookupCat in currentData && tokenObj.token in currentData[lookupCat]) {
|
|
422
469
|
this.debug.log("Missing token already exists! Skipping:", {
|
|
423
470
|
category: tokenObj.category,
|
|
424
471
|
token: tokenObj.token,
|
|
425
|
-
existing: currentData[
|
|
472
|
+
existing: currentData[lookupCat][tokenObj.token]
|
|
426
473
|
});
|
|
427
474
|
return false;
|
|
428
475
|
}
|
|
@@ -431,8 +478,13 @@ var Translations = class {
|
|
|
431
478
|
if (!this.missingTokens.length) return false;
|
|
432
479
|
this.debug.log("CREATE MISSING TOKENS", this.missingTokens);
|
|
433
480
|
try {
|
|
481
|
+
const tokensForApi = this.missingTokens.map((tokenObj) => ({
|
|
482
|
+
projectid: tokenObj.projectid,
|
|
483
|
+
token: tokenObj.token,
|
|
484
|
+
category: tokenObj.category || null
|
|
485
|
+
}));
|
|
434
486
|
const response = await LangsysAppAPI.post("projects/[projectid]/tokens", {
|
|
435
|
-
tokens:
|
|
487
|
+
tokens: tokensForApi
|
|
436
488
|
});
|
|
437
489
|
if (!response.status) {
|
|
438
490
|
if (response.errors) {
|
|
@@ -442,14 +494,15 @@ var Translations = class {
|
|
|
442
494
|
return false;
|
|
443
495
|
}
|
|
444
496
|
this.missingTokens.forEach((tokenObj) => {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
497
|
+
const lookupCat = tokenObj.category || "__uncategorized__";
|
|
498
|
+
if (!currentData[lookupCat]) {
|
|
499
|
+
currentData[lookupCat] = {
|
|
500
|
+
__category__: lookupCat,
|
|
501
|
+
__symbol__: lookupCat
|
|
449
502
|
};
|
|
450
503
|
}
|
|
451
|
-
currentData[
|
|
452
|
-
currentData[
|
|
504
|
+
currentData[lookupCat][tokenObj.token] = tokenObj.token;
|
|
505
|
+
currentData[lookupCat]["__category__"] = lookupCat;
|
|
453
506
|
});
|
|
454
507
|
sTranslations.set({ ...currentData });
|
|
455
508
|
this.missingTokens = [];
|
|
@@ -468,6 +521,7 @@ var Translations = class {
|
|
|
468
521
|
this.debug.log("GET TRANSLATIONS API RESPONSE", response);
|
|
469
522
|
if (response.errors) {
|
|
470
523
|
this.debug.error("Error", response.errors[0]);
|
|
524
|
+
this.readyResolve();
|
|
471
525
|
return;
|
|
472
526
|
}
|
|
473
527
|
const trans = response.data;
|
|
@@ -484,6 +538,7 @@ var Translations = class {
|
|
|
484
538
|
sTranslations.set(trans);
|
|
485
539
|
setTimeout(() => currentlyLoadedLocale.set(this.locale), 100);
|
|
486
540
|
this.lastLoaded[this.locale] = (/* @__PURE__ */ new Date()).getTime() / 1e3;
|
|
541
|
+
this.readyResolve();
|
|
487
542
|
}
|
|
488
543
|
};
|
|
489
544
|
|
|
@@ -513,7 +568,8 @@ var LangsysAppClass = class {
|
|
|
513
568
|
}
|
|
514
569
|
/**
|
|
515
570
|
* Initialize Langsys. Configuration must include at least `projectid`,
|
|
516
|
-
* `key`, and `UserLocaleStore` (
|
|
571
|
+
* `key`, and `UserLocaleStore` (anything satisfying `LocaleSource` — see
|
|
572
|
+
* its docstring for framework-specific adapters).
|
|
517
573
|
*/
|
|
518
574
|
async init(initConfig) {
|
|
519
575
|
const projectid = initConfig.projectid;
|
|
@@ -542,9 +598,9 @@ var LangsysAppClass = class {
|
|
|
542
598
|
this.debug.error("LangsysApp.init missing API key in configuration object!");
|
|
543
599
|
return { status: false, errors: ["Missing API key"] };
|
|
544
600
|
}
|
|
545
|
-
if (!UserLocaleStore?.subscribe) {
|
|
601
|
+
if (!UserLocaleStore?.subscribe || typeof UserLocaleStore.get !== "function") {
|
|
546
602
|
this.debug.error(
|
|
547
|
-
"LangsysApp.init missing UserLocaleStore \u2014 pass
|
|
603
|
+
"LangsysApp.init missing UserLocaleStore \u2014 pass any object satisfying LocaleSource (get + subscribe). createSignal('en-us') works if you have nothing of your own."
|
|
548
604
|
);
|
|
549
605
|
return { status: false, errors: ["Missing UserLocaleStore"] };
|
|
550
606
|
}
|
|
@@ -908,7 +964,7 @@ function md5(inputString) {
|
|
|
908
964
|
return rh(a) + rh(b) + rh(c) + rh(d);
|
|
909
965
|
}
|
|
910
966
|
|
|
911
|
-
// src/
|
|
967
|
+
// src/content-block.ts
|
|
912
968
|
var TRANSLATABLE_ATTRIBUTES = [
|
|
913
969
|
"placeholder",
|
|
914
970
|
"alt",
|
|
@@ -973,9 +1029,141 @@ var SEMANTIC_STYLE_PROPERTIES = [
|
|
|
973
1029
|
"list-style-type",
|
|
974
1030
|
"cursor"
|
|
975
1031
|
];
|
|
1032
|
+
function generateCustomId(category, tokens) {
|
|
1033
|
+
return md5(JSON.stringify([category, tokens]));
|
|
1034
|
+
}
|
|
1035
|
+
function isContentBlockKnown(category, customId) {
|
|
1036
|
+
const cats = sTranslations.get();
|
|
1037
|
+
const lookupCat = category || "__uncategorized__";
|
|
1038
|
+
const cbData = cats[lookupCat]?.[customId];
|
|
1039
|
+
return typeof cbData === "object" && cbData !== null;
|
|
1040
|
+
}
|
|
1041
|
+
async function registerContentBlock(contentBlock) {
|
|
1042
|
+
if (config.key_type !== "write") {
|
|
1043
|
+
if (config.debug) {
|
|
1044
|
+
logger.log(`Skipping content block save (API key is ${config.key_type || "unknown"})`);
|
|
1045
|
+
}
|
|
1046
|
+
return { status: true };
|
|
1047
|
+
}
|
|
1048
|
+
try {
|
|
1049
|
+
const response = await LangsysAppAPI.post(
|
|
1050
|
+
"projects/[projectid]/content-blocks",
|
|
1051
|
+
contentBlock
|
|
1052
|
+
);
|
|
1053
|
+
if (!response.status) {
|
|
1054
|
+
logger.error("Could not save content block", response.errors);
|
|
1055
|
+
return { status: false, errors: response.errors };
|
|
1056
|
+
}
|
|
1057
|
+
_writeKnownContentBlockToCache(contentBlock.category, contentBlock.custom_id);
|
|
1058
|
+
return { status: true };
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
logger.error("Could not save content block", err);
|
|
1061
|
+
return { status: false, errors: [err] };
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
function tokenizeElement(element) {
|
|
1065
|
+
const tokens = [];
|
|
1066
|
+
const clone = element.cloneNode(true);
|
|
1067
|
+
_walkForTokens(element, Array.from(clone.childNodes), tokens, []);
|
|
1068
|
+
return { tokens, content: clone.outerHTML };
|
|
1069
|
+
}
|
|
1070
|
+
function _writeKnownContentBlockToCache(category, customId) {
|
|
1071
|
+
const lookupCat = category || "__uncategorized__";
|
|
1072
|
+
sTranslations.update((current) => {
|
|
1073
|
+
if (!current[lookupCat]) {
|
|
1074
|
+
current[lookupCat] = {
|
|
1075
|
+
__category__: lookupCat,
|
|
1076
|
+
__symbol__: lookupCat
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
current[lookupCat][customId] = {};
|
|
1080
|
+
return { ...current };
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
function _walkForTokens(liveRoot, cloneNodes, tokens, indices) {
|
|
1084
|
+
cloneNodes.forEach((node, index) => {
|
|
1085
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1086
|
+
const el = node;
|
|
1087
|
+
if (el.getAttribute("translate") === "no") return;
|
|
1088
|
+
if (el.hasAttribute("data-ls-phrase")) return;
|
|
1089
|
+
}
|
|
1090
|
+
if (node.hasChildNodes()) {
|
|
1091
|
+
_applyStylesToClone(liveRoot, node, [...indices, index]);
|
|
1092
|
+
}
|
|
1093
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1094
|
+
_tokenizeAttributes(node, tokens);
|
|
1095
|
+
}
|
|
1096
|
+
const contentToken = node.nodeValue?.replace(/\s+/g, " ").trim();
|
|
1097
|
+
if (node.nodeType === Node.TEXT_NODE && contentToken) {
|
|
1098
|
+
tokens.push(contentToken);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
if (!node.hasChildNodes()) return;
|
|
1102
|
+
_walkForTokens(liveRoot, Array.from(node.childNodes), tokens, [...indices, index]);
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
function _tokenizeAttributes(element, tokens) {
|
|
1106
|
+
const tagName = element.tagName.toLowerCase();
|
|
1107
|
+
if (tagName === "img") {
|
|
1108
|
+
const img = element;
|
|
1109
|
+
if (img.src) element.setAttribute("src", img.src);
|
|
1110
|
+
}
|
|
1111
|
+
for (const attr of TRANSLATABLE_ATTRIBUTES) {
|
|
1112
|
+
const value = element.getAttribute(attr)?.trim();
|
|
1113
|
+
if (value) tokens.push(value);
|
|
1114
|
+
}
|
|
1115
|
+
if (VALUE_TRANSLATABLE_ELEMENTS.includes(tagName)) {
|
|
1116
|
+
const value = element.getAttribute("value")?.trim();
|
|
1117
|
+
if (value) tokens.push(value);
|
|
1118
|
+
}
|
|
1119
|
+
if (tagName === "input") {
|
|
1120
|
+
const inputType = element.getAttribute("type")?.toLowerCase();
|
|
1121
|
+
if (inputType && VALUE_TRANSLATABLE_INPUT_TYPES.includes(inputType)) {
|
|
1122
|
+
const value = element.getAttribute("value")?.trim();
|
|
1123
|
+
if (value) tokens.push(value);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (tagName === "select") {
|
|
1127
|
+
const options = element.querySelectorAll("option");
|
|
1128
|
+
options.forEach((option) => {
|
|
1129
|
+
const optionText = option.textContent?.trim();
|
|
1130
|
+
if (optionText) tokens.push(optionText);
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
function _applyStylesToClone(liveRoot, cloneNode, indices) {
|
|
1135
|
+
if (typeof window === "undefined") return;
|
|
1136
|
+
const domNode = _findNode(Array.from(liveRoot.childNodes), indices);
|
|
1137
|
+
if (!domNode) return;
|
|
1138
|
+
const tagName = domNode.tagName.toLowerCase();
|
|
1139
|
+
const reference = document.createElement(tagName);
|
|
1140
|
+
reference.style.visibility = "hidden";
|
|
1141
|
+
reference.style.position = "absolute";
|
|
1142
|
+
document.body.appendChild(reference);
|
|
1143
|
+
const computed = window.getComputedStyle(domNode);
|
|
1144
|
+
const defaults = window.getComputedStyle(reference);
|
|
1145
|
+
for (const prop of SEMANTIC_STYLE_PROPERTIES) {
|
|
1146
|
+
const value = computed.getPropertyValue(prop);
|
|
1147
|
+
const defaultValue = defaults.getPropertyValue(prop);
|
|
1148
|
+
if (value && value !== defaultValue) {
|
|
1149
|
+
cloneNode.style.setProperty(prop, value);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
document.body.removeChild(reference);
|
|
1153
|
+
cloneNode.removeAttribute("class");
|
|
1154
|
+
}
|
|
1155
|
+
function _findNode(nodes, indices) {
|
|
1156
|
+
let currentNode = nodes[indices[0]];
|
|
1157
|
+
for (let i = 1; i < indices.length; i++) {
|
|
1158
|
+
if (!currentNode || !currentNode.hasChildNodes()) return void 0;
|
|
1159
|
+
currentNode = Array.from(currentNode.childNodes)[indices[i]];
|
|
1160
|
+
}
|
|
1161
|
+
return currentNode;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// src/translate.ts
|
|
976
1165
|
var Translate = class {
|
|
977
1166
|
constructor(element, options = {}) {
|
|
978
|
-
this.contentClone = null;
|
|
979
1167
|
this.tokens = [];
|
|
980
1168
|
this.parseComplete = false;
|
|
981
1169
|
this.isTokenizing = false;
|
|
@@ -1001,7 +1189,7 @@ var Translate = class {
|
|
|
1001
1189
|
if (this.lastTranslatedLocale === currentLocale) return;
|
|
1002
1190
|
const { category = "" } = this.options;
|
|
1003
1191
|
if (this.tokens.length === 1) {
|
|
1004
|
-
this.element.innerText = LangsysApp.Translations.t(
|
|
1192
|
+
this.element.innerText = LangsysApp.Translations.t(this.tokens[0], category);
|
|
1005
1193
|
} else {
|
|
1006
1194
|
this.translate(Array.from(this.element.childNodes));
|
|
1007
1195
|
this.lastTranslatedLocale = currentLocale;
|
|
@@ -1015,18 +1203,17 @@ var Translate = class {
|
|
|
1015
1203
|
return false;
|
|
1016
1204
|
}
|
|
1017
1205
|
this.isTokenizing = true;
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
this.tokenize(nodes);
|
|
1206
|
+
const { tokens, content } = tokenizeElement(this.element);
|
|
1207
|
+
this.tokens = tokens;
|
|
1021
1208
|
const { category = "" } = this.options;
|
|
1022
1209
|
if (this.tokens.length === 1) {
|
|
1023
|
-
this.element.innerText = LangsysApp.Translations.t(
|
|
1210
|
+
this.element.innerText = LangsysApp.Translations.t(this.tokens[0], category);
|
|
1024
1211
|
} else {
|
|
1025
1212
|
const contentBlock = {
|
|
1026
1213
|
custom_id: "",
|
|
1027
1214
|
category,
|
|
1028
1215
|
label: this.options.label,
|
|
1029
|
-
content
|
|
1216
|
+
content,
|
|
1030
1217
|
tokens: this.tokens
|
|
1031
1218
|
};
|
|
1032
1219
|
await this.handleContentBlock(contentBlock);
|
|
@@ -1036,85 +1223,23 @@ var Translate = class {
|
|
|
1036
1223
|
return true;
|
|
1037
1224
|
}
|
|
1038
1225
|
async handleContentBlock(contentBlock) {
|
|
1039
|
-
if (isEmpty(this.custom_id))
|
|
1226
|
+
if (isEmpty(this.custom_id)) {
|
|
1227
|
+
this.custom_id = generateCustomId(contentBlock.category, contentBlock.tokens);
|
|
1228
|
+
}
|
|
1040
1229
|
contentBlock.custom_id = this.custom_id;
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
if (!existing) {
|
|
1044
|
-
if (config.key_type === "write") {
|
|
1045
|
-
LangsysAppAPI.post("projects/[projectid]/content-blocks", contentBlock).then((response) => {
|
|
1046
|
-
if (!response.status) {
|
|
1047
|
-
logger.error("Could not save content block", response.errors);
|
|
1048
|
-
} else {
|
|
1049
|
-
contentBlocks.update((blocks) => [...blocks, contentBlock]);
|
|
1050
|
-
}
|
|
1051
|
-
}).catch((err) => logger.error("Could not save content block", err));
|
|
1052
|
-
} else if (config.debug) {
|
|
1053
|
-
logger.log(`Skipping content block save (API key is ${config.key_type || "unknown"})`);
|
|
1054
|
-
}
|
|
1230
|
+
await LangsysApp.Translations.ready();
|
|
1231
|
+
if (isContentBlockKnown(contentBlock.category, this.custom_id)) {
|
|
1055
1232
|
if (this.tokens.length > 1 && this.element) {
|
|
1056
1233
|
this.translate(Array.from(this.element.childNodes));
|
|
1057
1234
|
this.lastTranslatedLocale = currentlyLoadedLocale.get();
|
|
1058
1235
|
}
|
|
1236
|
+
return;
|
|
1059
1237
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
const element = node;
|
|
1065
|
-
if (element.getAttribute("translate") === "no") return;
|
|
1066
|
-
}
|
|
1067
|
-
if (node?.hasChildNodes()) {
|
|
1068
|
-
this.applyStylesToNode(node, [...indices, index]);
|
|
1069
|
-
}
|
|
1070
|
-
if (node?.nodeType === Node.ELEMENT_NODE) {
|
|
1071
|
-
this.tokenizeAttributes(node);
|
|
1072
|
-
}
|
|
1073
|
-
const contentToken = node.nodeValue?.replace(/\s+/g, " ").trim();
|
|
1074
|
-
if (node?.nodeType === Node.TEXT_NODE && contentToken) {
|
|
1075
|
-
this.tokens.push(contentToken);
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
if (!node?.hasChildNodes()) return;
|
|
1079
|
-
this.tokenize(Array.from(node.childNodes), [...indices, index]);
|
|
1080
|
-
});
|
|
1081
|
-
}
|
|
1082
|
-
tokenizeAttributes(element) {
|
|
1083
|
-
const tagName = element.tagName.toLowerCase();
|
|
1084
|
-
if (tagName === "img") {
|
|
1085
|
-
const img = element;
|
|
1086
|
-
if (img.src) element.setAttribute("src", img.src);
|
|
1087
|
-
}
|
|
1088
|
-
for (const attr of TRANSLATABLE_ATTRIBUTES) {
|
|
1089
|
-
const value = element.getAttribute(attr)?.trim();
|
|
1090
|
-
if (value) this.tokens.push(value);
|
|
1091
|
-
}
|
|
1092
|
-
if (VALUE_TRANSLATABLE_ELEMENTS.includes(tagName)) {
|
|
1093
|
-
const value = element.getAttribute("value")?.trim();
|
|
1094
|
-
if (value) this.tokens.push(value);
|
|
1095
|
-
}
|
|
1096
|
-
if (tagName === "input") {
|
|
1097
|
-
const inputType = element.getAttribute("type")?.toLowerCase();
|
|
1098
|
-
if (inputType && VALUE_TRANSLATABLE_INPUT_TYPES.includes(inputType)) {
|
|
1099
|
-
const value = element.getAttribute("value")?.trim();
|
|
1100
|
-
if (value) this.tokens.push(value);
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
if (tagName === "select") {
|
|
1104
|
-
const options = element.querySelectorAll("option");
|
|
1105
|
-
options.forEach((option) => {
|
|
1106
|
-
const optionText = option.textContent?.trim();
|
|
1107
|
-
if (optionText) this.tokens.push(optionText);
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
findNode(nodes, indices) {
|
|
1112
|
-
let currentNode = nodes[indices[0]];
|
|
1113
|
-
for (let i = 1; i < indices.length; i++) {
|
|
1114
|
-
if (!currentNode || !currentNode.hasChildNodes()) return void 0;
|
|
1115
|
-
currentNode = Array.from(currentNode.childNodes)[indices[i]];
|
|
1238
|
+
void registerContentBlock(contentBlock);
|
|
1239
|
+
if (this.tokens.length > 1 && this.element) {
|
|
1240
|
+
this.translate(Array.from(this.element.childNodes));
|
|
1241
|
+
this.lastTranslatedLocale = currentlyLoadedLocale.get();
|
|
1116
1242
|
}
|
|
1117
|
-
return currentNode;
|
|
1118
1243
|
}
|
|
1119
1244
|
translate(nodes) {
|
|
1120
1245
|
const currentLocale = currentlyLoadedLocale.get();
|
|
@@ -1125,6 +1250,7 @@ var Translate = class {
|
|
|
1125
1250
|
if (node?.nodeType === Node.ELEMENT_NODE) {
|
|
1126
1251
|
const element = node;
|
|
1127
1252
|
if (element.getAttribute("translate") === "no") return;
|
|
1253
|
+
if (element.hasAttribute("data-ls-phrase")) return;
|
|
1128
1254
|
this.translateAttributes(node);
|
|
1129
1255
|
}
|
|
1130
1256
|
if (node?.nodeType !== Node.TEXT_NODE) {
|
|
@@ -1194,38 +1320,136 @@ var Translate = class {
|
|
|
1194
1320
|
if (translation) element.setAttribute(attr, translation);
|
|
1195
1321
|
else element.setAttribute(attr, element.originalAttributes[attr]);
|
|
1196
1322
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
// src/richtext.ts
|
|
1326
|
+
var SENT_OPEN_START = String.fromCharCode(57344);
|
|
1327
|
+
var SENT_OPEN_END = String.fromCharCode(57345);
|
|
1328
|
+
var SENT_CLOSE_START = String.fromCharCode(57346);
|
|
1329
|
+
var SENT_CLOSE_END = String.fromCharCode(57347);
|
|
1330
|
+
var SENT_SCAN = /\uE000(\d+)\uE001|\uE002(\d+)\uE003/g;
|
|
1331
|
+
function encodeRichText(root) {
|
|
1332
|
+
const slots = [];
|
|
1333
|
+
const phrase = _encodeNodes(Array.from(root.childNodes), slots);
|
|
1334
|
+
return { phrase: phrase.replace(/\s+/g, " ").trim(), slots };
|
|
1335
|
+
}
|
|
1336
|
+
function _encodeNodes(nodes, slots) {
|
|
1337
|
+
let out = "";
|
|
1338
|
+
for (const node of nodes) {
|
|
1339
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1340
|
+
out += node.nodeValue ?? "";
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
1344
|
+
const element = node;
|
|
1345
|
+
const index = slots.length;
|
|
1346
|
+
slots.push({ template: element.cloneNode(false) });
|
|
1347
|
+
out += `{m${index}o}` + _encodeNodes(Array.from(element.childNodes), slots) + `{m${index}c}`;
|
|
1348
|
+
}
|
|
1349
|
+
return out;
|
|
1350
|
+
}
|
|
1351
|
+
function markupTokenValues(slotCount) {
|
|
1352
|
+
const values = {};
|
|
1353
|
+
for (let i = 0; i < slotCount; i++) {
|
|
1354
|
+
values[`m${i}o`] = `${SENT_OPEN_START}${i}${SENT_OPEN_END}`;
|
|
1355
|
+
values[`m${i}c`] = `${SENT_CLOSE_START}${i}${SENT_CLOSE_END}`;
|
|
1356
|
+
}
|
|
1357
|
+
return values;
|
|
1358
|
+
}
|
|
1359
|
+
function reconstitute(resolved, slots, doc = document) {
|
|
1360
|
+
const root = doc.createDocumentFragment();
|
|
1361
|
+
const stack = [root];
|
|
1362
|
+
const top = () => stack[stack.length - 1];
|
|
1363
|
+
let lastIndex = 0;
|
|
1364
|
+
let match;
|
|
1365
|
+
SENT_SCAN.lastIndex = 0;
|
|
1366
|
+
try {
|
|
1367
|
+
while ((match = SENT_SCAN.exec(resolved)) !== null) {
|
|
1368
|
+
const text = resolved.slice(lastIndex, match.index);
|
|
1369
|
+
if (text) top().appendChild(doc.createTextNode(text));
|
|
1370
|
+
lastIndex = SENT_SCAN.lastIndex;
|
|
1371
|
+
const isOpen = match[1] !== void 0;
|
|
1372
|
+
const slotIndex = Number(isOpen ? match[1] : match[2]);
|
|
1373
|
+
if (isOpen) {
|
|
1374
|
+
const slot = slots[slotIndex];
|
|
1375
|
+
if (!slot) throw new Error("unknown markup slot");
|
|
1376
|
+
const el = slot.template.cloneNode(false);
|
|
1377
|
+
top().appendChild(el);
|
|
1378
|
+
stack.push(el);
|
|
1379
|
+
} else {
|
|
1380
|
+
if (stack.length <= 1) throw new Error("unbalanced close");
|
|
1381
|
+
stack.pop();
|
|
1216
1382
|
}
|
|
1217
1383
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1384
|
+
const tail = resolved.slice(lastIndex);
|
|
1385
|
+
if (tail) top().appendChild(doc.createTextNode(tail));
|
|
1386
|
+
if (stack.length !== 1) throw new Error("unbalanced open");
|
|
1387
|
+
} catch {
|
|
1388
|
+
return [doc.createTextNode(stripSentinels(resolved))];
|
|
1389
|
+
}
|
|
1390
|
+
return Array.from(root.childNodes);
|
|
1391
|
+
}
|
|
1392
|
+
function stripSentinels(value) {
|
|
1393
|
+
return value.replace(SENT_SCAN, "");
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// src/phrase.ts
|
|
1397
|
+
var PHRASE_MARKER_ATTR = "data-ls-phrase";
|
|
1398
|
+
var Phrase = class {
|
|
1399
|
+
constructor(host, options = {}) {
|
|
1400
|
+
this.phrase = "";
|
|
1401
|
+
this.slots = [];
|
|
1402
|
+
this.unsubscribers = [];
|
|
1403
|
+
this.ready = false;
|
|
1404
|
+
this.host = host;
|
|
1405
|
+
this.category = options.category ?? "";
|
|
1406
|
+
this.params = options.params ?? {};
|
|
1407
|
+
void this._init();
|
|
1408
|
+
const rerender = () => {
|
|
1409
|
+
if (this.ready) this._render();
|
|
1410
|
+
};
|
|
1411
|
+
this.unsubscribers.push(currentlyLoadedLocale.subscribe(rerender));
|
|
1412
|
+
this.unsubscribers.push(sTranslations.subscribe(rerender));
|
|
1413
|
+
}
|
|
1414
|
+
/** Update interpolation params (e.g. a changed count) and re-render. */
|
|
1415
|
+
setParams(params) {
|
|
1416
|
+
this.params = params ?? {};
|
|
1417
|
+
if (this.ready) this._render();
|
|
1418
|
+
}
|
|
1419
|
+
/** Stop reacting to locale/translation changes. Safe to call multiple times. */
|
|
1420
|
+
destroy() {
|
|
1421
|
+
this.unsubscribers.forEach((unsub) => unsub());
|
|
1422
|
+
this.unsubscribers = [];
|
|
1423
|
+
}
|
|
1424
|
+
async _init() {
|
|
1425
|
+
if (!this.host || !this.host.childNodes.length) {
|
|
1426
|
+
this.ready = true;
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
const { phrase, slots } = encodeRichText(this.host);
|
|
1430
|
+
this.phrase = phrase;
|
|
1431
|
+
this.slots = slots;
|
|
1432
|
+
await LangsysApp.Translations.ready();
|
|
1433
|
+
LangsysApp.Translations.t(this.phrase, this.category);
|
|
1434
|
+
this.ready = true;
|
|
1435
|
+
this._render();
|
|
1436
|
+
}
|
|
1437
|
+
_render() {
|
|
1438
|
+
if (!this.phrase) return;
|
|
1439
|
+
const raw = LangsysApp.Translations.lookup(this.phrase, this.category) ?? this.phrase;
|
|
1440
|
+
const params = { ...this.params, ...markupTokenValues(this.slots.length) };
|
|
1441
|
+
const resolved = interpolate(raw, params, currentlyLoadedLocale.get());
|
|
1442
|
+
const nodes = reconstitute(resolved, this.slots, this.host.ownerDocument ?? document);
|
|
1443
|
+
this.host.replaceChildren(...nodes);
|
|
1220
1444
|
}
|
|
1221
1445
|
};
|
|
1222
1446
|
|
|
1223
1447
|
// src/index.ts
|
|
1224
|
-
var t = ((
|
|
1225
|
-
return LangsysApp.Translations.t(
|
|
1448
|
+
var t = ((phrase, ...rest) => {
|
|
1449
|
+
return LangsysApp.Translations.t(phrase, ...rest);
|
|
1226
1450
|
});
|
|
1227
1451
|
var tSignal = LangsysApp.Translations.tSignal;
|
|
1228
1452
|
|
|
1229
|
-
export { LangsysApp, LangsysAppAPI, Logger, Translate, Translations,
|
|
1453
|
+
export { LangsysApp, LangsysAppAPI, Logger, PHRASE_MARKER_ATTR, Phrase, Translate, Translations, createSignal, currentlyLoadedLocale, encodeRichText, generateCustomId, getValue, interpolate, isContentBlockKnown, isEmpty, isICU, logger, markupTokenValues, md5, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };
|
|
1230
1454
|
//# sourceMappingURL=index.mjs.map
|
|
1231
1455
|
//# sourceMappingURL=index.mjs.map
|