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/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
- // src/interpolate.ts
225
- function interpolate(template, params) {
226
- return template.replace(/\{([^{}]+)\}/g, (match, rawKey) => {
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
- return (category, phrase, ...args) => {
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(lookupCat, phrase);
336
+ this.missingToken(category, phrase);
291
337
  translated = phrase;
292
338
  }
293
- const params = args[0];
294
- return params ? interpolate(translated, params) : translated;
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
- if (tokenObj.category in currentData && tokenObj.token in currentData[tokenObj.category]) {
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[tokenObj.category][tokenObj.token]
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: this.missingTokens
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
- if (!currentData[tokenObj.category]) {
448
- currentData[tokenObj.category] = {
449
- __category__: tokenObj.category,
450
- __symbol__: tokenObj.category
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[tokenObj.category][tokenObj.token] = tokenObj.token;
454
- currentData[tokenObj.category]["__category__"] = tokenObj.category;
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` (any Signal<string>).
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 a Signal<string> (createSignal('en-us') works if you have nothing of your own)."
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/translate.ts
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(category, this.tokens[0]);
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
- this.contentClone = this.element.cloneNode(true);
1021
- const nodes = Array.from(this.contentClone.childNodes || []);
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(category, this.tokens[0]);
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: this.contentClone.outerHTML,
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)) this.custom_id = this.generateCustomId(contentBlock);
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
- const current = contentBlocks.get();
1044
- const existing = current.find((b) => b.custom_id === this.custom_id);
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
- tokenize(nodes, indices = []) {
1064
- nodes.forEach((node, index) => {
1065
- if (node?.nodeType === Node.ELEMENT_NODE) {
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
- generateCustomId(contentBlock) {
1200
- return md5(`${contentBlock.category}-${contentBlock.tokens.join("-")}`);
1201
- }
1202
- applyStylesToNode(node, indices) {
1203
- if (!this.element || typeof window === "undefined") return;
1204
- const domNode = this.findNode(Array.from(this.element.childNodes), indices);
1205
- if (!domNode) return;
1206
- const tagName = domNode.tagName.toLowerCase();
1207
- const reference = document.createElement(tagName);
1208
- reference.style.visibility = "hidden";
1209
- reference.style.position = "absolute";
1210
- document.body.appendChild(reference);
1211
- const computed = window.getComputedStyle(domNode);
1212
- const defaults = window.getComputedStyle(reference);
1213
- for (const prop of SEMANTIC_STYLE_PROPERTIES) {
1214
- const value = computed.getPropertyValue(prop);
1215
- const defaultValue = defaults.getPropertyValue(prop);
1216
- if (value && value !== defaultValue) {
1217
- node.style.setProperty(prop, value);
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
- document.body.removeChild(reference);
1221
- node.removeAttribute("class");
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 = ((category, phrase, ...args) => {
1227
- return LangsysApp.Translations.t(category, phrase, ...args);
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