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