langsys-js-typescript 0.1.0 → 0.2.1
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 +10 -0
- package/README.md +17 -17
- package/dist/index.d.mts +294 -31
- package/dist/index.d.ts +294 -31
- package/dist/index.js +389 -135
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +379 -135
- 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) {
|
|
@@ -85,7 +91,18 @@ var LangsysAppAPIClass = class {
|
|
|
85
91
|
return await this.get("authorize-project/[projectid]");
|
|
86
92
|
}
|
|
87
93
|
async getTranslations(locale) {
|
|
88
|
-
return await this.get("
|
|
94
|
+
return await this.get("translations", { project_id: this.config.projectid, locale });
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Register phrases and/or content blocks via the current unified endpoint.
|
|
98
|
+
* Each item carries `type: 'phrase' | 'content_block'`. Replaces the
|
|
99
|
+
* deprecated `projects/{id}/tokens` and `projects/{id}/content-blocks` routes.
|
|
100
|
+
*/
|
|
101
|
+
async createTranslatableItems(items) {
|
|
102
|
+
return await this.post("translatable-items", {
|
|
103
|
+
project_id: this.config.projectid,
|
|
104
|
+
translatable_items: items
|
|
105
|
+
});
|
|
89
106
|
}
|
|
90
107
|
async post(path, data = {}) {
|
|
91
108
|
return await this.send("POST", path, data);
|
|
@@ -209,7 +226,6 @@ var initialTranslations = {
|
|
|
209
226
|
};
|
|
210
227
|
var sTranslations = persist("translations", initialTranslations);
|
|
211
228
|
var currentlyLoadedLocale = createSignal("");
|
|
212
|
-
var contentBlocks = createSignal([]);
|
|
213
229
|
var config = {
|
|
214
230
|
projectid: "",
|
|
215
231
|
key: "",
|
|
@@ -218,10 +234,22 @@ var config = {
|
|
|
218
234
|
debug: false,
|
|
219
235
|
key_type: "read"
|
|
220
236
|
};
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
237
|
+
function isICU(template) {
|
|
238
|
+
return ICU_PATTERN.test(template);
|
|
239
|
+
}
|
|
240
|
+
var ICU_PATTERN = /\{[^{}]+,\s*(plural|select|selectordinal|number|date|time)\s*,/;
|
|
241
|
+
function interpolate(template, params, locale) {
|
|
242
|
+
if (isICU(template)) {
|
|
243
|
+
try {
|
|
244
|
+
return new IntlMessageFormat(template, locale ?? "en").format(params);
|
|
245
|
+
} catch {
|
|
246
|
+
return simpleInterpolate(template, params);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return simpleInterpolate(template, params);
|
|
250
|
+
}
|
|
251
|
+
function simpleInterpolate(template, params) {
|
|
252
|
+
return template.replace(/\{([^{},]+)\}/g, (match, rawKey) => {
|
|
225
253
|
const key = rawKey.trim();
|
|
226
254
|
if (!(key in params)) return match;
|
|
227
255
|
const value = params[key];
|
|
@@ -245,6 +273,11 @@ var Translations = class {
|
|
|
245
273
|
this.timer = null;
|
|
246
274
|
this.flushScheduled = false;
|
|
247
275
|
this.isFirstClientRun = true;
|
|
276
|
+
this.readyResolve = () => {
|
|
277
|
+
};
|
|
278
|
+
this.readyPromise = new Promise((resolve) => {
|
|
279
|
+
this.readyResolve = resolve;
|
|
280
|
+
});
|
|
248
281
|
this.config = config2;
|
|
249
282
|
this.debug = new Logger(!!config2.debug);
|
|
250
283
|
this.locale = config2.baseLocale || "";
|
|
@@ -253,6 +286,16 @@ var Translations = class {
|
|
|
253
286
|
currentlyLoadedLocale.subscribe(() => this.tSignal.set(this.buildTFn()));
|
|
254
287
|
if (config2.key && config2.projectid) this.setup(config2);
|
|
255
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* Resolves once the first `getTranslations` call has completed (or
|
|
291
|
+
* immediately if no fetch is configured). Callers that need to read
|
|
292
|
+
* `sTranslations` before issuing a write — e.g. the `Translate` DOM
|
|
293
|
+
* class deciding whether to POST a content block — should await this
|
|
294
|
+
* to avoid acting on an empty cache during the cold-start race.
|
|
295
|
+
*/
|
|
296
|
+
ready() {
|
|
297
|
+
return this.readyPromise;
|
|
298
|
+
}
|
|
256
299
|
/** Current translation function. Reads fresh state on every call. */
|
|
257
300
|
get t() {
|
|
258
301
|
return this.tSignal.get();
|
|
@@ -275,8 +318,22 @@ var Translations = class {
|
|
|
275
318
|
const value = contentData[token];
|
|
276
319
|
return typeof value === "string" ? value : null;
|
|
277
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* Raw stored translation for a phrase, without interpolation or
|
|
323
|
+
* missing-token registration. Returns null when no (non-empty) translation
|
|
324
|
+
* is cached. Used by the `Phrase` rich-text handler, which needs the
|
|
325
|
+
* un-interpolated template (it supplies its own markup-token values).
|
|
326
|
+
*/
|
|
327
|
+
lookup(phrase, category) {
|
|
328
|
+
const cats = sTranslations.get();
|
|
329
|
+
const lookupCat = category || "__uncategorized__";
|
|
330
|
+
const value = cats[lookupCat]?.[phrase];
|
|
331
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
332
|
+
}
|
|
278
333
|
buildTFn() {
|
|
279
|
-
|
|
334
|
+
const fn = ((phrase, ...rest) => {
|
|
335
|
+
const category = typeof rest[0] === "string" ? rest[0] : "";
|
|
336
|
+
const params = typeof rest[0] === "object" ? rest[0] : rest[1];
|
|
280
337
|
const cats = sTranslations.get();
|
|
281
338
|
const lookupCat = category || "__uncategorized__";
|
|
282
339
|
const value = cats[lookupCat]?.[phrase];
|
|
@@ -285,12 +342,12 @@ var Translations = class {
|
|
|
285
342
|
this.debug.log("TRANSLATION FOUND", [lookupCat, phrase, value]);
|
|
286
343
|
translated = value;
|
|
287
344
|
} else {
|
|
288
|
-
this.missingToken(
|
|
345
|
+
this.missingToken(category, phrase);
|
|
289
346
|
translated = phrase;
|
|
290
347
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
348
|
+
return params ? interpolate(translated, params, currentlyLoadedLocale.get()) : translated;
|
|
349
|
+
});
|
|
350
|
+
return fn;
|
|
294
351
|
}
|
|
295
352
|
missingToken(category, token) {
|
|
296
353
|
if (token === void 0 || token === null) {
|
|
@@ -418,11 +475,12 @@ var Translations = class {
|
|
|
418
475
|
const currentData = sTranslations.get();
|
|
419
476
|
this.missingTokens = this.missingTokens.filter((tokenObj) => {
|
|
420
477
|
tokenObj.projectid = this.config.projectid;
|
|
421
|
-
|
|
478
|
+
const lookupCat = tokenObj.category || "__uncategorized__";
|
|
479
|
+
if (lookupCat in currentData && tokenObj.token in currentData[lookupCat]) {
|
|
422
480
|
this.debug.log("Missing token already exists! Skipping:", {
|
|
423
481
|
category: tokenObj.category,
|
|
424
482
|
token: tokenObj.token,
|
|
425
|
-
existing: currentData[
|
|
483
|
+
existing: currentData[lookupCat][tokenObj.token]
|
|
426
484
|
});
|
|
427
485
|
return false;
|
|
428
486
|
}
|
|
@@ -431,25 +489,34 @@ var Translations = class {
|
|
|
431
489
|
if (!this.missingTokens.length) return false;
|
|
432
490
|
this.debug.log("CREATE MISSING TOKENS", this.missingTokens);
|
|
433
491
|
try {
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
492
|
+
const phraseItems = this.missingTokens.map((tokenObj) => ({
|
|
493
|
+
type: "phrase",
|
|
494
|
+
phrase: tokenObj.token,
|
|
495
|
+
category: tokenObj.category || null
|
|
496
|
+
}));
|
|
497
|
+
const BATCH_SIZE = 200;
|
|
498
|
+
for (let offset = 0; offset < phraseItems.length; offset += BATCH_SIZE) {
|
|
499
|
+
const response = await LangsysAppAPI.createTranslatableItems(
|
|
500
|
+
phraseItems.slice(offset, offset + BATCH_SIZE)
|
|
501
|
+
);
|
|
502
|
+
if (!response.status) {
|
|
503
|
+
if (response.errors) {
|
|
504
|
+
this.debug.log("TOKEN UPDATE FAIL", this.missingTokens);
|
|
505
|
+
this.debug.error("Error updating project tokens", response.errors);
|
|
506
|
+
}
|
|
507
|
+
return false;
|
|
441
508
|
}
|
|
442
|
-
return false;
|
|
443
509
|
}
|
|
444
510
|
this.missingTokens.forEach((tokenObj) => {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
511
|
+
const lookupCat = tokenObj.category || "__uncategorized__";
|
|
512
|
+
if (!currentData[lookupCat]) {
|
|
513
|
+
currentData[lookupCat] = {
|
|
514
|
+
__category__: lookupCat,
|
|
515
|
+
__symbol__: lookupCat
|
|
449
516
|
};
|
|
450
517
|
}
|
|
451
|
-
currentData[
|
|
452
|
-
currentData[
|
|
518
|
+
currentData[lookupCat][tokenObj.token] = tokenObj.token;
|
|
519
|
+
currentData[lookupCat]["__category__"] = lookupCat;
|
|
453
520
|
});
|
|
454
521
|
sTranslations.set({ ...currentData });
|
|
455
522
|
this.missingTokens = [];
|
|
@@ -468,6 +535,7 @@ var Translations = class {
|
|
|
468
535
|
this.debug.log("GET TRANSLATIONS API RESPONSE", response);
|
|
469
536
|
if (response.errors) {
|
|
470
537
|
this.debug.error("Error", response.errors[0]);
|
|
538
|
+
this.readyResolve();
|
|
471
539
|
return;
|
|
472
540
|
}
|
|
473
541
|
const trans = response.data;
|
|
@@ -484,6 +552,7 @@ var Translations = class {
|
|
|
484
552
|
sTranslations.set(trans);
|
|
485
553
|
setTimeout(() => currentlyLoadedLocale.set(this.locale), 100);
|
|
486
554
|
this.lastLoaded[this.locale] = (/* @__PURE__ */ new Date()).getTime() / 1e3;
|
|
555
|
+
this.readyResolve();
|
|
487
556
|
}
|
|
488
557
|
};
|
|
489
558
|
|
|
@@ -513,7 +582,8 @@ var LangsysAppClass = class {
|
|
|
513
582
|
}
|
|
514
583
|
/**
|
|
515
584
|
* Initialize Langsys. Configuration must include at least `projectid`,
|
|
516
|
-
* `key`, and `UserLocaleStore` (
|
|
585
|
+
* `key`, and `UserLocaleStore` (anything satisfying `LocaleSource` — see
|
|
586
|
+
* its docstring for framework-specific adapters).
|
|
517
587
|
*/
|
|
518
588
|
async init(initConfig) {
|
|
519
589
|
const projectid = initConfig.projectid;
|
|
@@ -542,9 +612,9 @@ var LangsysAppClass = class {
|
|
|
542
612
|
this.debug.error("LangsysApp.init missing API key in configuration object!");
|
|
543
613
|
return { status: false, errors: ["Missing API key"] };
|
|
544
614
|
}
|
|
545
|
-
if (!UserLocaleStore?.subscribe) {
|
|
615
|
+
if (!UserLocaleStore?.subscribe || typeof UserLocaleStore.get !== "function") {
|
|
546
616
|
this.debug.error(
|
|
547
|
-
"LangsysApp.init missing UserLocaleStore \u2014 pass
|
|
617
|
+
"LangsysApp.init missing UserLocaleStore \u2014 pass any object satisfying LocaleSource (get + subscribe). createSignal('en-us') works if you have nothing of your own."
|
|
548
618
|
);
|
|
549
619
|
return { status: false, errors: ["Missing UserLocaleStore"] };
|
|
550
620
|
}
|
|
@@ -908,7 +978,7 @@ function md5(inputString) {
|
|
|
908
978
|
return rh(a) + rh(b) + rh(c) + rh(d);
|
|
909
979
|
}
|
|
910
980
|
|
|
911
|
-
// src/
|
|
981
|
+
// src/content-block.ts
|
|
912
982
|
var TRANSLATABLE_ATTRIBUTES = [
|
|
913
983
|
"placeholder",
|
|
914
984
|
"alt",
|
|
@@ -973,9 +1043,147 @@ var SEMANTIC_STYLE_PROPERTIES = [
|
|
|
973
1043
|
"list-style-type",
|
|
974
1044
|
"cursor"
|
|
975
1045
|
];
|
|
1046
|
+
function generateCustomId(category, tokens) {
|
|
1047
|
+
return md5(JSON.stringify([category, tokens]));
|
|
1048
|
+
}
|
|
1049
|
+
function isContentBlockKnown(category, customId) {
|
|
1050
|
+
const cats = sTranslations.get();
|
|
1051
|
+
const lookupCat = category || "__uncategorized__";
|
|
1052
|
+
const cbData = cats[lookupCat]?.[customId];
|
|
1053
|
+
return typeof cbData === "object" && cbData !== null;
|
|
1054
|
+
}
|
|
1055
|
+
async function registerContentBlock(contentBlock) {
|
|
1056
|
+
if (config.key_type !== "write") {
|
|
1057
|
+
if (config.debug) {
|
|
1058
|
+
logger.log(`Skipping content block save (API key is ${config.key_type || "unknown"})`);
|
|
1059
|
+
}
|
|
1060
|
+
return { status: true };
|
|
1061
|
+
}
|
|
1062
|
+
try {
|
|
1063
|
+
const response = await LangsysAppAPI.createTranslatableItems([
|
|
1064
|
+
{
|
|
1065
|
+
type: "content_block",
|
|
1066
|
+
custom_id: contentBlock.custom_id,
|
|
1067
|
+
category: contentBlock.category,
|
|
1068
|
+
content: contentBlock.content,
|
|
1069
|
+
label: contentBlock.label,
|
|
1070
|
+
phrases: contentBlock.tokens.map((phrase) => ({ phrase }))
|
|
1071
|
+
}
|
|
1072
|
+
]);
|
|
1073
|
+
if (!response.status) {
|
|
1074
|
+
logger.error("Could not save content block", response.errors);
|
|
1075
|
+
return { status: false, errors: response.errors };
|
|
1076
|
+
}
|
|
1077
|
+
_writeKnownContentBlockToCache(contentBlock.category, contentBlock.custom_id);
|
|
1078
|
+
return { status: true };
|
|
1079
|
+
} catch (err) {
|
|
1080
|
+
logger.error("Could not save content block", err);
|
|
1081
|
+
return { status: false, errors: [err] };
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
function tokenizeElement(element) {
|
|
1085
|
+
const tokens = [];
|
|
1086
|
+
const clone = element.cloneNode(true);
|
|
1087
|
+
_walkForTokens(element, Array.from(clone.childNodes), tokens, []);
|
|
1088
|
+
return { tokens, content: clone.outerHTML };
|
|
1089
|
+
}
|
|
1090
|
+
function _writeKnownContentBlockToCache(category, customId) {
|
|
1091
|
+
const lookupCat = category || "__uncategorized__";
|
|
1092
|
+
sTranslations.update((current) => {
|
|
1093
|
+
if (!current[lookupCat]) {
|
|
1094
|
+
current[lookupCat] = {
|
|
1095
|
+
__category__: lookupCat,
|
|
1096
|
+
__symbol__: lookupCat
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
current[lookupCat][customId] = {};
|
|
1100
|
+
return { ...current };
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
function _walkForTokens(liveRoot, cloneNodes, tokens, indices) {
|
|
1104
|
+
cloneNodes.forEach((node, index) => {
|
|
1105
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1106
|
+
const el = node;
|
|
1107
|
+
if (el.getAttribute("translate") === "no") return;
|
|
1108
|
+
if (el.hasAttribute("data-ls-phrase")) return;
|
|
1109
|
+
}
|
|
1110
|
+
if (node.hasChildNodes()) {
|
|
1111
|
+
_applyStylesToClone(liveRoot, node, [...indices, index]);
|
|
1112
|
+
}
|
|
1113
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1114
|
+
_tokenizeAttributes(node, tokens);
|
|
1115
|
+
}
|
|
1116
|
+
const contentToken = node.nodeValue?.replace(/\s+/g, " ").trim();
|
|
1117
|
+
if (node.nodeType === Node.TEXT_NODE && contentToken) {
|
|
1118
|
+
tokens.push(contentToken);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
if (!node.hasChildNodes()) return;
|
|
1122
|
+
_walkForTokens(liveRoot, Array.from(node.childNodes), tokens, [...indices, index]);
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
function _tokenizeAttributes(element, tokens) {
|
|
1126
|
+
const tagName = element.tagName.toLowerCase();
|
|
1127
|
+
if (tagName === "img") {
|
|
1128
|
+
const img = element;
|
|
1129
|
+
if (img.src) element.setAttribute("src", img.src);
|
|
1130
|
+
}
|
|
1131
|
+
for (const attr of TRANSLATABLE_ATTRIBUTES) {
|
|
1132
|
+
const value = element.getAttribute(attr)?.trim();
|
|
1133
|
+
if (value) tokens.push(value);
|
|
1134
|
+
}
|
|
1135
|
+
if (VALUE_TRANSLATABLE_ELEMENTS.includes(tagName)) {
|
|
1136
|
+
const value = element.getAttribute("value")?.trim();
|
|
1137
|
+
if (value) tokens.push(value);
|
|
1138
|
+
}
|
|
1139
|
+
if (tagName === "input") {
|
|
1140
|
+
const inputType = element.getAttribute("type")?.toLowerCase();
|
|
1141
|
+
if (inputType && VALUE_TRANSLATABLE_INPUT_TYPES.includes(inputType)) {
|
|
1142
|
+
const value = element.getAttribute("value")?.trim();
|
|
1143
|
+
if (value) tokens.push(value);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
if (tagName === "select") {
|
|
1147
|
+
const options = element.querySelectorAll("option");
|
|
1148
|
+
options.forEach((option) => {
|
|
1149
|
+
const optionText = option.textContent?.trim();
|
|
1150
|
+
if (optionText) tokens.push(optionText);
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
function _applyStylesToClone(liveRoot, cloneNode, indices) {
|
|
1155
|
+
if (typeof window === "undefined") return;
|
|
1156
|
+
const domNode = _findNode(Array.from(liveRoot.childNodes), indices);
|
|
1157
|
+
if (!domNode) return;
|
|
1158
|
+
const tagName = domNode.tagName.toLowerCase();
|
|
1159
|
+
const reference = document.createElement(tagName);
|
|
1160
|
+
reference.style.visibility = "hidden";
|
|
1161
|
+
reference.style.position = "absolute";
|
|
1162
|
+
document.body.appendChild(reference);
|
|
1163
|
+
const computed = window.getComputedStyle(domNode);
|
|
1164
|
+
const defaults = window.getComputedStyle(reference);
|
|
1165
|
+
for (const prop of SEMANTIC_STYLE_PROPERTIES) {
|
|
1166
|
+
const value = computed.getPropertyValue(prop);
|
|
1167
|
+
const defaultValue = defaults.getPropertyValue(prop);
|
|
1168
|
+
if (value && value !== defaultValue) {
|
|
1169
|
+
cloneNode.style.setProperty(prop, value);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
document.body.removeChild(reference);
|
|
1173
|
+
cloneNode.removeAttribute("class");
|
|
1174
|
+
}
|
|
1175
|
+
function _findNode(nodes, indices) {
|
|
1176
|
+
let currentNode = nodes[indices[0]];
|
|
1177
|
+
for (let i = 1; i < indices.length; i++) {
|
|
1178
|
+
if (!currentNode || !currentNode.hasChildNodes()) return void 0;
|
|
1179
|
+
currentNode = Array.from(currentNode.childNodes)[indices[i]];
|
|
1180
|
+
}
|
|
1181
|
+
return currentNode;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// src/translate.ts
|
|
976
1185
|
var Translate = class {
|
|
977
1186
|
constructor(element, options = {}) {
|
|
978
|
-
this.contentClone = null;
|
|
979
1187
|
this.tokens = [];
|
|
980
1188
|
this.parseComplete = false;
|
|
981
1189
|
this.isTokenizing = false;
|
|
@@ -1001,7 +1209,7 @@ var Translate = class {
|
|
|
1001
1209
|
if (this.lastTranslatedLocale === currentLocale) return;
|
|
1002
1210
|
const { category = "" } = this.options;
|
|
1003
1211
|
if (this.tokens.length === 1) {
|
|
1004
|
-
this.element.innerText = LangsysApp.Translations.t(
|
|
1212
|
+
this.element.innerText = LangsysApp.Translations.t(this.tokens[0], category);
|
|
1005
1213
|
} else {
|
|
1006
1214
|
this.translate(Array.from(this.element.childNodes));
|
|
1007
1215
|
this.lastTranslatedLocale = currentLocale;
|
|
@@ -1015,18 +1223,17 @@ var Translate = class {
|
|
|
1015
1223
|
return false;
|
|
1016
1224
|
}
|
|
1017
1225
|
this.isTokenizing = true;
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
this.tokenize(nodes);
|
|
1226
|
+
const { tokens, content } = tokenizeElement(this.element);
|
|
1227
|
+
this.tokens = tokens;
|
|
1021
1228
|
const { category = "" } = this.options;
|
|
1022
1229
|
if (this.tokens.length === 1) {
|
|
1023
|
-
this.element.innerText = LangsysApp.Translations.t(
|
|
1230
|
+
this.element.innerText = LangsysApp.Translations.t(this.tokens[0], category);
|
|
1024
1231
|
} else {
|
|
1025
1232
|
const contentBlock = {
|
|
1026
1233
|
custom_id: "",
|
|
1027
1234
|
category,
|
|
1028
1235
|
label: this.options.label,
|
|
1029
|
-
content
|
|
1236
|
+
content,
|
|
1030
1237
|
tokens: this.tokens
|
|
1031
1238
|
};
|
|
1032
1239
|
await this.handleContentBlock(contentBlock);
|
|
@@ -1036,85 +1243,23 @@ var Translate = class {
|
|
|
1036
1243
|
return true;
|
|
1037
1244
|
}
|
|
1038
1245
|
async handleContentBlock(contentBlock) {
|
|
1039
|
-
if (isEmpty(this.custom_id))
|
|
1246
|
+
if (isEmpty(this.custom_id)) {
|
|
1247
|
+
this.custom_id = generateCustomId(contentBlock.category, contentBlock.tokens);
|
|
1248
|
+
}
|
|
1040
1249
|
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
|
-
}
|
|
1250
|
+
await LangsysApp.Translations.ready();
|
|
1251
|
+
if (isContentBlockKnown(contentBlock.category, this.custom_id)) {
|
|
1055
1252
|
if (this.tokens.length > 1 && this.element) {
|
|
1056
1253
|
this.translate(Array.from(this.element.childNodes));
|
|
1057
1254
|
this.lastTranslatedLocale = currentlyLoadedLocale.get();
|
|
1058
1255
|
}
|
|
1256
|
+
return;
|
|
1059
1257
|
}
|
|
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]];
|
|
1258
|
+
void registerContentBlock(contentBlock);
|
|
1259
|
+
if (this.tokens.length > 1 && this.element) {
|
|
1260
|
+
this.translate(Array.from(this.element.childNodes));
|
|
1261
|
+
this.lastTranslatedLocale = currentlyLoadedLocale.get();
|
|
1116
1262
|
}
|
|
1117
|
-
return currentNode;
|
|
1118
1263
|
}
|
|
1119
1264
|
translate(nodes) {
|
|
1120
1265
|
const currentLocale = currentlyLoadedLocale.get();
|
|
@@ -1125,6 +1270,7 @@ var Translate = class {
|
|
|
1125
1270
|
if (node?.nodeType === Node.ELEMENT_NODE) {
|
|
1126
1271
|
const element = node;
|
|
1127
1272
|
if (element.getAttribute("translate") === "no") return;
|
|
1273
|
+
if (element.hasAttribute("data-ls-phrase")) return;
|
|
1128
1274
|
this.translateAttributes(node);
|
|
1129
1275
|
}
|
|
1130
1276
|
if (node?.nodeType !== Node.TEXT_NODE) {
|
|
@@ -1194,38 +1340,136 @@ var Translate = class {
|
|
|
1194
1340
|
if (translation) element.setAttribute(attr, translation);
|
|
1195
1341
|
else element.setAttribute(attr, element.originalAttributes[attr]);
|
|
1196
1342
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
// src/richtext.ts
|
|
1346
|
+
var SENT_OPEN_START = String.fromCharCode(57344);
|
|
1347
|
+
var SENT_OPEN_END = String.fromCharCode(57345);
|
|
1348
|
+
var SENT_CLOSE_START = String.fromCharCode(57346);
|
|
1349
|
+
var SENT_CLOSE_END = String.fromCharCode(57347);
|
|
1350
|
+
var SENT_SCAN = /\uE000(\d+)\uE001|\uE002(\d+)\uE003/g;
|
|
1351
|
+
function encodeRichText(root) {
|
|
1352
|
+
const slots = [];
|
|
1353
|
+
const phrase = _encodeNodes(Array.from(root.childNodes), slots);
|
|
1354
|
+
return { phrase: phrase.replace(/\s+/g, " ").trim(), slots };
|
|
1355
|
+
}
|
|
1356
|
+
function _encodeNodes(nodes, slots) {
|
|
1357
|
+
let out = "";
|
|
1358
|
+
for (const node of nodes) {
|
|
1359
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1360
|
+
out += node.nodeValue ?? "";
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
1364
|
+
const element = node;
|
|
1365
|
+
const index = slots.length;
|
|
1366
|
+
slots.push({ template: element.cloneNode(false) });
|
|
1367
|
+
out += `{m${index}o}` + _encodeNodes(Array.from(element.childNodes), slots) + `{m${index}c}`;
|
|
1368
|
+
}
|
|
1369
|
+
return out;
|
|
1370
|
+
}
|
|
1371
|
+
function markupTokenValues(slotCount) {
|
|
1372
|
+
const values = {};
|
|
1373
|
+
for (let i = 0; i < slotCount; i++) {
|
|
1374
|
+
values[`m${i}o`] = `${SENT_OPEN_START}${i}${SENT_OPEN_END}`;
|
|
1375
|
+
values[`m${i}c`] = `${SENT_CLOSE_START}${i}${SENT_CLOSE_END}`;
|
|
1376
|
+
}
|
|
1377
|
+
return values;
|
|
1378
|
+
}
|
|
1379
|
+
function reconstitute(resolved, slots, doc = document) {
|
|
1380
|
+
const root = doc.createDocumentFragment();
|
|
1381
|
+
const stack = [root];
|
|
1382
|
+
const top = () => stack[stack.length - 1];
|
|
1383
|
+
let lastIndex = 0;
|
|
1384
|
+
let match;
|
|
1385
|
+
SENT_SCAN.lastIndex = 0;
|
|
1386
|
+
try {
|
|
1387
|
+
while ((match = SENT_SCAN.exec(resolved)) !== null) {
|
|
1388
|
+
const text = resolved.slice(lastIndex, match.index);
|
|
1389
|
+
if (text) top().appendChild(doc.createTextNode(text));
|
|
1390
|
+
lastIndex = SENT_SCAN.lastIndex;
|
|
1391
|
+
const isOpen = match[1] !== void 0;
|
|
1392
|
+
const slotIndex = Number(isOpen ? match[1] : match[2]);
|
|
1393
|
+
if (isOpen) {
|
|
1394
|
+
const slot = slots[slotIndex];
|
|
1395
|
+
if (!slot) throw new Error("unknown markup slot");
|
|
1396
|
+
const el = slot.template.cloneNode(false);
|
|
1397
|
+
top().appendChild(el);
|
|
1398
|
+
stack.push(el);
|
|
1399
|
+
} else {
|
|
1400
|
+
if (stack.length <= 1) throw new Error("unbalanced close");
|
|
1401
|
+
stack.pop();
|
|
1216
1402
|
}
|
|
1217
1403
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1404
|
+
const tail = resolved.slice(lastIndex);
|
|
1405
|
+
if (tail) top().appendChild(doc.createTextNode(tail));
|
|
1406
|
+
if (stack.length !== 1) throw new Error("unbalanced open");
|
|
1407
|
+
} catch {
|
|
1408
|
+
return [doc.createTextNode(stripSentinels(resolved))];
|
|
1409
|
+
}
|
|
1410
|
+
return Array.from(root.childNodes);
|
|
1411
|
+
}
|
|
1412
|
+
function stripSentinels(value) {
|
|
1413
|
+
return value.replace(SENT_SCAN, "");
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/phrase.ts
|
|
1417
|
+
var PHRASE_MARKER_ATTR = "data-ls-phrase";
|
|
1418
|
+
var Phrase = class {
|
|
1419
|
+
constructor(host, options = {}) {
|
|
1420
|
+
this.phrase = "";
|
|
1421
|
+
this.slots = [];
|
|
1422
|
+
this.unsubscribers = [];
|
|
1423
|
+
this.ready = false;
|
|
1424
|
+
this.host = host;
|
|
1425
|
+
this.category = options.category ?? "";
|
|
1426
|
+
this.params = options.params ?? {};
|
|
1427
|
+
void this._init();
|
|
1428
|
+
const rerender = () => {
|
|
1429
|
+
if (this.ready) this._render();
|
|
1430
|
+
};
|
|
1431
|
+
this.unsubscribers.push(currentlyLoadedLocale.subscribe(rerender));
|
|
1432
|
+
this.unsubscribers.push(sTranslations.subscribe(rerender));
|
|
1433
|
+
}
|
|
1434
|
+
/** Update interpolation params (e.g. a changed count) and re-render. */
|
|
1435
|
+
setParams(params) {
|
|
1436
|
+
this.params = params ?? {};
|
|
1437
|
+
if (this.ready) this._render();
|
|
1438
|
+
}
|
|
1439
|
+
/** Stop reacting to locale/translation changes. Safe to call multiple times. */
|
|
1440
|
+
destroy() {
|
|
1441
|
+
this.unsubscribers.forEach((unsub) => unsub());
|
|
1442
|
+
this.unsubscribers = [];
|
|
1443
|
+
}
|
|
1444
|
+
async _init() {
|
|
1445
|
+
if (!this.host || !this.host.childNodes.length) {
|
|
1446
|
+
this.ready = true;
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
const { phrase, slots } = encodeRichText(this.host);
|
|
1450
|
+
this.phrase = phrase;
|
|
1451
|
+
this.slots = slots;
|
|
1452
|
+
await LangsysApp.Translations.ready();
|
|
1453
|
+
LangsysApp.Translations.t(this.phrase, this.category);
|
|
1454
|
+
this.ready = true;
|
|
1455
|
+
this._render();
|
|
1456
|
+
}
|
|
1457
|
+
_render() {
|
|
1458
|
+
if (!this.phrase) return;
|
|
1459
|
+
const raw = LangsysApp.Translations.lookup(this.phrase, this.category) ?? this.phrase;
|
|
1460
|
+
const params = { ...this.params, ...markupTokenValues(this.slots.length) };
|
|
1461
|
+
const resolved = interpolate(raw, params, currentlyLoadedLocale.get());
|
|
1462
|
+
const nodes = reconstitute(resolved, this.slots, this.host.ownerDocument ?? document);
|
|
1463
|
+
this.host.replaceChildren(...nodes);
|
|
1220
1464
|
}
|
|
1221
1465
|
};
|
|
1222
1466
|
|
|
1223
1467
|
// src/index.ts
|
|
1224
|
-
var t = ((
|
|
1225
|
-
return LangsysApp.Translations.t(
|
|
1468
|
+
var t = ((phrase, ...rest) => {
|
|
1469
|
+
return LangsysApp.Translations.t(phrase, ...rest);
|
|
1226
1470
|
});
|
|
1227
1471
|
var tSignal = LangsysApp.Translations.tSignal;
|
|
1228
1472
|
|
|
1229
|
-
export { LangsysApp, LangsysAppAPI, Logger, Translate, Translations,
|
|
1473
|
+
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
1474
|
//# sourceMappingURL=index.mjs.map
|
|
1231
1475
|
//# sourceMappingURL=index.mjs.map
|