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