locl-js 1.0.1 → 2.0.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
@@ -18,11 +18,13 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
 
20
20
  // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ LocL: () => LocL,
24
+ defineResources: () => defineResources,
23
25
  initLocL: () => initLocL
24
26
  });
25
- module.exports = __toCommonJS(index_exports);
27
+ module.exports = __toCommonJS(src_exports);
26
28
 
27
29
  // src/formatters.ts
28
30
  var defaultFormatters = {
@@ -58,7 +60,7 @@ var defaultFormatters = {
58
60
  * @param {string} [args[1]="..."] - The suffix to append if the string is truncated.
59
61
  * @returns The truncated string.
60
62
  */
61
- truncate: (val, args) => {
63
+ truncate: (val, args = []) => {
62
64
  const length = parseInt(args[0]) || 10;
63
65
  const suffix = args[1] || "...";
64
66
  const str = String(val);
@@ -72,7 +74,7 @@ var defaultFormatters = {
72
74
  * @param {string} [args[1]] - The style of formatting to use (e.g., "decimal", "percent").
73
75
  * @returns The formatted number.
74
76
  */
75
- number: (val, args) => {
77
+ number: (val, args = []) => {
76
78
  const locales = args[0] || void 0;
77
79
  const options = {};
78
80
  if (args[1]) options.style = args[1];
@@ -86,7 +88,7 @@ var defaultFormatters = {
86
88
  * @param {string} [args[1]] - The locale to use.
87
89
  * @returns The formatted currency string.
88
90
  */
89
- currency: (val, args) => {
91
+ currency: (val, args = []) => {
90
92
  const currency = args[0] || "USD";
91
93
  const locales = args[1] || void 0;
92
94
  return new Intl.NumberFormat(locales, {
@@ -102,7 +104,7 @@ var defaultFormatters = {
102
104
  * @param {string} [args[1]] - The date style to use (e.g., "short", "long").
103
105
  * @returns The formatted date string.
104
106
  */
105
- date: (val, args) => {
107
+ date: (val, args = []) => {
106
108
  const locales = args[0] || void 0;
107
109
  const options = {};
108
110
  if (args[1]) options.dateStyle = args[1];
@@ -115,7 +117,7 @@ var defaultFormatters = {
115
117
  * @param {string} [args[0]] - The locale to use.
116
118
  * @returns The relative time string.
117
119
  */
118
- relativeDate: (val, args) => {
120
+ relativeDate: (val, args = []) => {
119
121
  const locales = args[0] || void 0;
120
122
  const options = { numeric: "auto" };
121
123
  const rtf = new Intl.RelativeTimeFormat(locales, options);
@@ -139,7 +141,7 @@ var defaultFormatters = {
139
141
  * @param {string} [args[1]="No"] - The string to return for a falsy value.
140
142
  * @returns "Yes" or "No".
141
143
  */
142
- yesNo: (val, args) => {
144
+ yesNo: (val, args = []) => {
143
145
  const [yesVal, noVal] = args || [];
144
146
  return val ? yesVal || "Yes" : noVal || "No";
145
147
  },
@@ -151,7 +153,7 @@ var defaultFormatters = {
151
153
  * @param {string} [args[1]="false"] - The string to return for a falsy value.
152
154
  * @returns "true" or "false".
153
155
  */
154
- boolean: (val, args) => {
156
+ boolean: (val, args = []) => {
155
157
  const [trueVal, falseVal] = args || [];
156
158
  return val ? trueVal || "true" : falseVal || "false";
157
159
  },
@@ -163,7 +165,7 @@ var defaultFormatters = {
163
165
  * @param {string} [args[1]=" "] - The string to pad with.
164
166
  * @returns The padded string.
165
167
  */
166
- padStart: (val, args) => {
168
+ padStart: (val, args = []) => {
167
169
  const length = parseInt(args[0]) || 0;
168
170
  const fill = args[1] || " ";
169
171
  return String(val).padStart(length, fill);
@@ -176,7 +178,7 @@ var defaultFormatters = {
176
178
  * @param {string} [args[1]=" "] - The string to pad with.
177
179
  * @returns The padded string.
178
180
  */
179
- padEnd: (val, args) => {
181
+ padEnd: (val, args = []) => {
180
182
  const length = parseInt(args[0]) || 0;
181
183
  const fill = args[1] || " ";
182
184
  return String(val).padEnd(length, fill);
@@ -192,25 +194,26 @@ var LocL = class _LocL {
192
194
  constructor(config) {
193
195
  this.cache = /* @__PURE__ */ new Map();
194
196
  this.proxyCache = /* @__PURE__ */ new WeakMap();
197
+ this.pluralRulesCache = /* @__PURE__ */ new Map();
198
+ this.subscribers = /* @__PURE__ */ new Set();
199
+ this.rootInstance = this;
195
200
  if (!config.resources) {
196
201
  throw new Error("[LocL] `resources` is required");
197
202
  }
198
203
  if (!config.fallbackLanguage) {
199
204
  throw new Error("[LocL] `fallbackLanguage` is required");
200
205
  }
201
- config = {
206
+ this.config = {
202
207
  useDefaultFormatters: true,
203
208
  devMode: false,
204
209
  useCache: true,
205
210
  ...config
206
211
  };
207
- config.resources = this.makeReadOnly(config.resources);
208
- this.config = config;
212
+ this.resources = { ...config.resources };
209
213
  this.language = this.isLanguage(config.language) ?? config.fallbackLanguage;
210
214
  this.fallbackLanguage = config.fallbackLanguage;
211
215
  this.scope = config.scope;
212
- this.pluralRules = new Intl.PluralRules(this.language);
213
- this.formatters = config.useDefaultFormatters ? { ...defaultFormatters, ...config.formatters ?? {} } : { ...config.formatters ?? {} };
216
+ this.formatters = this.config.useDefaultFormatters ? { ...defaultFormatters, ...config.formatters ?? {} } : { ...config.formatters ?? {} };
214
217
  }
215
218
  /**
216
219
  * Creates a new proxy translator instance with a different configuration.
@@ -222,21 +225,40 @@ var LocL = class _LocL {
222
225
  * @returns A new proxy `LocL` instance.
223
226
  */
224
227
  withConfig(config) {
228
+ const root = this.rootInstance;
225
229
  const cacheKey = `proxy::${config.language ?? this.language}::${Array.isArray(config.scope) ? config.scope.join("|") : config.scope ?? "*"}`;
226
- if (this.cache.has(cacheKey)) {
227
- return this.cache.get(cacheKey);
230
+ if (root.cache.has(cacheKey)) {
231
+ return root.cache.get(cacheKey);
228
232
  }
229
- return new Proxy(this, {
230
- get(target, prop) {
233
+ const result = new Proxy(root, {
234
+ get(target, prop, receiver) {
231
235
  if (prop === "language") {
232
236
  return config.language ?? target.language;
233
237
  }
234
238
  if (prop === "scope") {
235
- return config.scope;
239
+ return config.scope !== void 0 ? config.scope : target.scope;
240
+ }
241
+ if (prop === "rootInstance") {
242
+ return target;
243
+ }
244
+ if (prop === "subscribe" && config.language !== void 0) {
245
+ return (listener) => {
246
+ try {
247
+ listener(config.language, config.language);
248
+ } catch (err) {
249
+ console.error("[LocL] Error in subscriber:", err);
250
+ }
251
+ return () => {
252
+ };
253
+ };
236
254
  }
237
- return target[prop];
255
+ return Reflect.get(target, prop, receiver);
238
256
  }
239
257
  });
258
+ if (root.config.useCache) {
259
+ root.cache.set(cacheKey, result);
260
+ }
261
+ return result;
240
262
  }
241
263
  /**
242
264
  * Creates a new `LocL` instance with a different language or scope.
@@ -247,18 +269,106 @@ var LocL = class _LocL {
247
269
  clone(language = this.language, scope) {
248
270
  return new _LocL({
249
271
  ...this.config,
272
+ resources: this.resources,
250
273
  language,
251
274
  scope
252
275
  });
253
276
  }
277
+ /**
278
+ * Subscribes a listener to language changes.
279
+ * Conforms to the standard reactive store contract (e.g. Svelte stores).
280
+ * Calls the listener immediately with the current language and returns an unsubscribe function.
281
+ * @param listener - The subscriber callback function.
282
+ * @returns An unsubscribe function.
283
+ */
284
+ subscribe(listener) {
285
+ this.subscribers.add(listener);
286
+ try {
287
+ listener(this.language, this.language);
288
+ } catch (err) {
289
+ console.error("[LocL] Error in initial subscriber call:", err);
290
+ }
291
+ return () => {
292
+ this.subscribers.delete(listener);
293
+ };
294
+ }
295
+ /**
296
+ * Gets the current active language.
297
+ */
298
+ getLanguage() {
299
+ return this.language;
300
+ }
301
+ /**
302
+ * Adds or overrides a single translation key at runtime.
303
+ * @param lang - Target language code.
304
+ * @param key - Dotted path key.
305
+ * @param value - The translation value.
306
+ */
307
+ addResource(lang, key, value) {
308
+ if (!this.resources[lang]) {
309
+ this.resources[lang] = {};
310
+ }
311
+ const keys = key.split(".");
312
+ let current = this.resources[lang];
313
+ for (let i = 0; i < keys.length - 1; i++) {
314
+ const k = keys[i];
315
+ if (k === "__proto__" || k === "constructor" || k === "prototype") {
316
+ return;
317
+ }
318
+ if (!current[k] || typeof current[k] !== "object") {
319
+ current[k] = {};
320
+ }
321
+ current = current[k];
322
+ }
323
+ const lastKey = keys[keys.length - 1];
324
+ if (lastKey !== "__proto__" && lastKey !== "constructor" && lastKey !== "prototype") {
325
+ current[lastKey] = value;
326
+ }
327
+ this.invalidateCacheForLang(lang);
328
+ }
329
+ /**
330
+ * Deeply merges a resource bundle into the specified language at runtime.
331
+ * @param lang - Target language code.
332
+ * @param bundle - Object of translations to merge.
333
+ */
334
+ addResources(lang, bundle) {
335
+ if (!this.resources[lang]) {
336
+ this.resources[lang] = {};
337
+ }
338
+ const deepMerge = (target, source) => {
339
+ for (const k of Object.keys(source)) {
340
+ if (k === "__proto__" || k === "constructor" || k === "prototype") {
341
+ continue;
342
+ }
343
+ if (source[k] && typeof source[k] === "object" && !Array.isArray(source[k])) {
344
+ if (!target[k] || typeof target[k] !== "object") {
345
+ target[k] = {};
346
+ }
347
+ deepMerge(target[k], source[k]);
348
+ } else {
349
+ target[k] = source[k];
350
+ }
351
+ }
352
+ };
353
+ deepMerge(this.resources[lang], bundle);
354
+ this.invalidateCacheForLang(lang);
355
+ }
254
356
  /**
255
357
  * Changes the current language of the translator.
256
358
  * @param lang - The new language to set.
257
359
  */
258
360
  changeLanguage(lang) {
259
- if (Object.keys(this.config.resources).includes(lang)) {
260
- this.language = lang;
261
- this.pluralRules = new Intl.PluralRules(this.language);
361
+ const validated = this.isLanguage(lang);
362
+ if (validated && validated !== this.language) {
363
+ const prev = this.language;
364
+ this.language = validated;
365
+ for (const sub of this.subscribers) {
366
+ try {
367
+ sub(this.language, prev);
368
+ } catch (err) {
369
+ console.error("[LocL] Error in subscriber:", err);
370
+ }
371
+ }
262
372
  }
263
373
  }
264
374
  t(key, values, format) {
@@ -267,6 +377,9 @@ var LocL = class _LocL {
267
377
  }
268
378
  const pluralCheckResult = this.checkPlural(key, values);
269
379
  if (pluralCheckResult !== void 0) {
380
+ if (format && format.formatter) {
381
+ return this.applyFormat(pluralCheckResult, format.formatter, format.args);
382
+ }
270
383
  return pluralCheckResult;
271
384
  }
272
385
  const result = this.lookupWithFallback(key);
@@ -297,45 +410,54 @@ var LocL = class _LocL {
297
410
  format(value, formatter, args) {
298
411
  return this.applyFormat(value, formatter, args);
299
412
  }
413
+ plural(key, values, format) {
414
+ return this.t(key, values, format);
415
+ }
300
416
  /**
301
- * Translates a key with pluralization.
302
- * It automatically selects the correct plural form based on the `count` value.
303
- * @param key - The base key for the pluralization.
304
- * @param values - An object with a `count` property and other values to interpolate.
305
- * @param format - An object with formatting options.
306
- * @returns The translated, pluralized, and formatted string.
417
+ * Translates a key and interpolates rich elements using tag functions.
418
+ * Matches `<tag>content</tag>` in translation templates.
419
+ * @param key - The key to translate.
420
+ * @param tags - An object mapping tag names to functions that transform their inner content.
421
+ * @param values - Values to interpolate into variable placeholders ({name}, {{name}}).
422
+ * @returns An array of chunks (strings and custom rendered tag values).
423
+ * @example
424
+ * ```ts
425
+ * translator.rich("terms", {
426
+ * link: (content) => `<a href="/terms">${content}</a>`
427
+ * }, { name: "Alice" });
428
+ * ```
307
429
  */
308
- plural(key, values, format) {
309
- const category = this.pluralRules.select(values.count);
310
- const baseKey = key;
311
- const baseValue = this.lookupWithFallback(baseKey);
312
- let interpolated;
313
- if (baseValue && typeof baseValue === "object") {
314
- if (category in baseValue) {
315
- interpolated = this.interpolate(baseValue[category], values) ?? baseKey;
316
- } else if ("other" in baseValue) {
317
- this.config.devMode && console.warn(`[LocL] Missing plural: "${baseKey}" in "${this.language}"`);
318
- interpolated = this.interpolate(baseValue.other, values) ?? baseKey;
319
- } else {
320
- this.config.devMode && console.warn(`[LocL] Missing plural: "${baseKey}" in "${this.language}"`);
321
- interpolated = baseKey;
430
+ rich(key, tags, values) {
431
+ const rawStr = this.t(key, values);
432
+ if (typeof rawStr !== "string") {
433
+ return [rawStr];
434
+ }
435
+ const tagRegex = /<([a-zA-Z0-9_-]+)>([\s\S]*?)<\/\1>/g;
436
+ const result = [];
437
+ let lastIndex = 0;
438
+ let match;
439
+ while ((match = tagRegex.exec(rawStr)) !== null) {
440
+ if (match.index > lastIndex) {
441
+ result.push(rawStr.slice(lastIndex, match.index));
322
442
  }
323
- } else {
324
- const pluralKey = `${baseKey}_${category}`;
325
- const result = this.lookupWithFallback(pluralKey) ?? this.lookupWithFallback(`${baseKey}_other`) ?? this.lookupWithFallback(baseKey);
326
- interpolated = this.interpolate(result, values) ?? baseKey;
443
+ const tagName = match[1];
444
+ const content = match[2];
445
+ const renderer = tags[tagName];
446
+ typeof renderer === "function" ? result.push(renderer(content)) : result.push(match[0]);
447
+ lastIndex = match.index + match[0].length;
327
448
  }
328
- if (format && format.formatter) {
329
- return this.applyFormat(interpolated, format.formatter, format.args);
449
+ if (lastIndex < rawStr.length) {
450
+ result.push(rawStr.slice(lastIndex));
330
451
  }
331
- return interpolated;
452
+ return result;
332
453
  }
333
454
  get(key) {
334
455
  const base = this.buildTranslationObject(this.language);
335
456
  if (!key) {
336
- return base;
457
+ return base ? this.makeReadOnly(base) : void 0;
337
458
  }
338
- return this.lookupWithFallback(key, base);
459
+ const val = this.lookupWithFallback(key, base);
460
+ return val && typeof val === "object" ? this.makeReadOnly(val) : val;
339
461
  }
340
462
  /**
341
463
  * Gets a nested object from the translations.
@@ -350,12 +472,27 @@ var LocL = class _LocL {
350
472
  if (!value) {
351
473
  return null;
352
474
  }
353
- const language = value.substring(0, 2);
354
- return Object.keys(this.config.resources).includes(language) ? language : null;
475
+ const available = Object.keys(this.resources);
476
+ if (available.includes(value)) {
477
+ return value;
478
+ }
479
+ const prefix = value.substring(0, 2);
480
+ if (available.includes(prefix)) {
481
+ return prefix;
482
+ }
483
+ return null;
355
484
  }
356
485
  getCacheKey(language) {
357
- const scopeKey = Array.isArray(this.scope) ? this.scope.join("|") : this.scope ?? "*";
358
- return `${language ?? this.language}::${scopeKey}`;
486
+ const scopeKey = Array.isArray(this.scope) ? this.scope.join("|") : this.scope;
487
+ return `${language}::${scopeKey}`;
488
+ }
489
+ invalidateCacheForLang(lang) {
490
+ const prefix = `${lang}::`;
491
+ for (const key of this.cache.keys()) {
492
+ if (key.startsWith(prefix)) {
493
+ this.cache.delete(key);
494
+ }
495
+ }
359
496
  }
360
497
  lookupWithFallback(key, base) {
361
498
  const keys = key.split(".");
@@ -373,7 +510,7 @@ var LocL = class _LocL {
373
510
  return result;
374
511
  }
375
512
  buildTranslationObject(language) {
376
- const langObject = this.config.resources[language];
513
+ const langObject = this.resources[language];
377
514
  if (!this.scope) {
378
515
  return langObject;
379
516
  }
@@ -404,11 +541,10 @@ var LocL = class _LocL {
404
541
  }, combined);
405
542
  }
406
543
  }
407
- const roCombined = this.makeReadOnly(combined);
408
544
  if (this.config.useCache) {
409
- this.cache.set(cacheKey, roCombined);
545
+ this.cache.set(cacheKey, combined);
410
546
  }
411
- return roCombined;
547
+ return combined;
412
548
  }
413
549
  if (this.config.useCache) {
414
550
  this.cache.set(cacheKey, void 0);
@@ -438,13 +574,54 @@ var LocL = class _LocL {
438
574
  }
439
575
  return current;
440
576
  }
577
+ getPluralRules(lang) {
578
+ let rules = this.pluralRulesCache.get(lang);
579
+ if (!rules) {
580
+ rules = new Intl.PluralRules(lang);
581
+ this.pluralRulesCache.set(lang, rules);
582
+ }
583
+ return rules;
584
+ }
441
585
  checkPlural(key, values) {
442
- if (values?.count !== void 0 && !key.match(/_(one|few|many|other)$/)) {
443
- const category = this.pluralRules.select(values.count);
444
- const pluralKey = `${key}_${category}`;
445
- const pluralResult = this.lookupWithFallback(pluralKey);
446
- return this.interpolate(pluralResult, values);
586
+ if (values?.count !== void 0 && !key.match(/_(zero|one|two|few|many|other)$/)) {
587
+ const category = this.getPluralRules(this.language).select(values.count);
588
+ const baseValue = this.lookupWithFallback(key, void 0);
589
+ if (baseValue && typeof baseValue === "object") {
590
+ if (values.count === 0 && "zero" in baseValue) {
591
+ return this.interpolate(baseValue.zero, values);
592
+ }
593
+ if (category in baseValue) {
594
+ return this.interpolate(baseValue[category], values);
595
+ } else if ("other" in baseValue) {
596
+ this.config.devMode && console.warn(`[LocL] Missing plural: "${key}" in "${this.language}"`);
597
+ return this.interpolate(baseValue.other, values);
598
+ } else {
599
+ this.config.devMode && console.warn(`[LocL] Missing plural: "${key}" in "${this.language}"`);
600
+ return key;
601
+ }
602
+ }
603
+ if (values.count === 0) {
604
+ const zeroResult = this.lookupWithFallback(`${key}_zero`, void 0);
605
+ if (zeroResult !== void 0) {
606
+ return this.interpolate(zeroResult, values);
607
+ }
608
+ }
609
+ const categoryResult = this.lookupWithFallback(`${key}_${category}`, void 0);
610
+ if (categoryResult !== void 0) {
611
+ return this.interpolate(categoryResult, values);
612
+ }
613
+ const otherResult = this.lookupWithFallback(`${key}_other`, void 0);
614
+ if (otherResult !== void 0) {
615
+ this.config.devMode && console.warn(`[LocL] Missing plural: "${key}" in "${this.language}"`);
616
+ return this.interpolate(otherResult, values);
617
+ }
618
+ const anySuffixExists = ["zero", "one", "two", "few", "many"].some((cat) => this.lookupWithFallback(`${key}_${cat}`, void 0) !== void 0);
619
+ if (anySuffixExists) {
620
+ this.config.devMode && console.warn(`[LocL] Missing plural: "${key}" in "${this.language}"`);
621
+ return key;
622
+ }
447
623
  }
624
+ return void 0;
448
625
  }
449
626
  interpolate(result, values) {
450
627
  if (!values || typeof result !== "string") {
@@ -462,34 +639,66 @@ var LocL = class _LocL {
462
639
  depth--;
463
640
  if (depth === 0 && start >= 0) {
464
641
  const raw = result.slice(start, i + 1);
465
- tokens.push({ raw, inner: raw.slice(1, -1).trim() });
642
+ let inner = raw.slice(1, -1);
643
+ if (inner.startsWith("{") && inner.endsWith("}")) {
644
+ inner = inner.slice(1, -1);
645
+ }
646
+ tokens.push({ raw, inner });
466
647
  start = -1;
467
648
  }
468
649
  }
469
650
  }
470
651
  let output = result;
652
+ const processed = /* @__PURE__ */ new Set();
471
653
  for (const token of tokens) {
654
+ if (processed.has(token.raw)) {
655
+ continue;
656
+ }
657
+ processed.add(token.raw);
472
658
  let replacement = "";
473
- if (token.inner.includes("select")) {
474
- const [field, type, ...rest] = token.inner.split(",").map((x) => x.trim());
475
- if (type === "select") {
476
- const value = String(values[field] ?? "other");
477
- const caseRegex = /(\w+)\s*\{([^}]*)\}/g;
478
- const caseMap = {};
479
- let match;
480
- while ((match = caseRegex.exec(rest.join(" "))) !== null) {
481
- const [, key, text] = match;
482
- caseMap[key] = text.trim();
659
+ const selectMatch = token.inner.match(/^\s*([^,]+),\s*select\s*,\s*([\s\S]+)$/);
660
+ if (selectMatch) {
661
+ const field = selectMatch[1].trim();
662
+ const casesBody = selectMatch[2];
663
+ const value = String(values[field] ?? "other");
664
+ const caseMap = {};
665
+ let i = 0;
666
+ while (i < casesBody.length) {
667
+ while (i < casesBody.length && /\s/.test(casesBody[i])) i++;
668
+ if (i >= casesBody.length) break;
669
+ let keyStart = i;
670
+ while (i < casesBody.length && casesBody[i] !== "{" && !/\s/.test(casesBody[i])) i++;
671
+ const caseKey = casesBody.slice(keyStart, i).trim();
672
+ while (i < casesBody.length && casesBody[i] !== "{") i++;
673
+ if (casesBody[i] === "{") {
674
+ let bodyDepth = 1;
675
+ let bodyStart = i + 1;
676
+ i++;
677
+ while (i < casesBody.length && bodyDepth > 0) {
678
+ if (casesBody[i] === "{") bodyDepth++;
679
+ else if (casesBody[i] === "}") bodyDepth--;
680
+ i++;
681
+ }
682
+ const caseText = casesBody.slice(bodyStart, i - 1).trim();
683
+ if (caseKey) {
684
+ caseMap[caseKey] = caseText;
685
+ }
483
686
  }
484
- replacement = caseMap[value] ?? caseMap.other ?? "";
485
687
  }
688
+ const rawReplacement = caseMap[value] ?? caseMap.other ?? "";
689
+ replacement = rawReplacement.includes("{") ? this.interpolate(rawReplacement, values) : rawReplacement;
486
690
  } else {
487
691
  const [fieldName, formatterAndArgs] = token.inner.split("|").map((x) => x.trim());
488
692
  const [formatterName, argsStr] = formatterAndArgs ? formatterAndArgs.split(":") : [];
489
- const args = argsStr ? argsStr.split(",") : [];
693
+ const args = argsStr ? argsStr.split(",").map((a) => a.trim()) : [];
490
694
  const val = values[fieldName];
491
- if (formatterName && this.formatters?.[formatterName]) {
492
- replacement = this.formatters[formatterName](val, args);
695
+ if (formatterName) {
696
+ if (this.formatters?.[formatterName]) {
697
+ replacement = this.formatters[formatterName](val, args);
698
+ } else {
699
+ this.config.devMode && console.warn(`[LocL] Formatter "${formatterName}" not found.`);
700
+ replacement = val !== void 0 ? String(val) : token.raw;
701
+ }
493
702
  } else if (val instanceof Date) {
494
703
  replacement = new Intl.DateTimeFormat(this.language).format(val);
495
704
  } else if (typeof val === "number") {
@@ -498,7 +707,7 @@ var LocL = class _LocL {
498
707
  replacement = val !== void 0 ? String(val) : token.raw;
499
708
  }
500
709
  }
501
- output = output.replace(token.raw, replacement);
710
+ output = output.split(token.raw).join(replacement);
502
711
  }
503
712
  return output;
504
713
  }
@@ -534,7 +743,12 @@ var LocL = class _LocL {
534
743
  function initLocL(config) {
535
744
  return new LocL(config);
536
745
  }
746
+ function defineResources(resources) {
747
+ return resources;
748
+ }
537
749
  // Annotate the CommonJS export names for ESM import in node:
538
750
  0 && (module.exports = {
751
+ LocL,
752
+ defineResources,
539
753
  initLocL
540
754
  });