inline-i18n-multi 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,6 +67,9 @@ See "Hello" in your app? Just search for "Hello" in your codebase. **Done.**
67
67
  - **Interpolation Guards** - Handle missing variables gracefully (`missingVarHandler`)
68
68
  - **Locale Detection** - Auto-detect user locale from navigator, cookie, URL, or header (`detectLocale()`)
69
69
  - **Selectordinal** - Ordinal plural formatting (`{rank, selectordinal, one {#st} two {#nd} ...}`)
70
+ - **ICU Message Cache** - Memoize parsed ICU ASTs for performance (`icuCacheSize`, `clearICUCache()`)
71
+ - **Plural Shorthand** - Concise plural syntax (`{count, p, item|items}`)
72
+ - **Locale Persistence** - Auto-save/restore locale to cookie or localStorage (`persistLocale`, `restoreLocale()`)
70
73
 
71
74
  ---
72
75
 
@@ -551,6 +554,109 @@ Uses `Intl.PluralRules` with `{ type: 'ordinal' }` for locale-aware ordinal cate
551
554
 
552
555
  ---
553
556
 
557
+ ## ICU Message Cache
558
+
559
+ Memoize parsed ICU ASTs to avoid re-parsing the same message patterns:
560
+
561
+ ```typescript
562
+ import { configure, clearICUCache } from 'inline-i18n-multi'
563
+
564
+ // Enable caching (default: 500 entries)
565
+ configure({ icuCacheSize: 500 })
566
+
567
+ // Disable caching
568
+ configure({ icuCacheSize: 0 })
569
+
570
+ // Manually clear the cache
571
+ clearICUCache()
572
+ ```
573
+
574
+ The cache uses FIFO (First-In, First-Out) eviction when the maximum size is reached. Repeated calls to `it()` or `t()` with the same ICU pattern will reuse the cached AST instead of re-parsing.
575
+
576
+ ---
577
+
578
+ ## Plural Shorthand
579
+
580
+ Concise syntax for common plural patterns using `p` as a short type name:
581
+
582
+ ```typescript
583
+ import { it, setLocale } from 'inline-i18n-multi'
584
+
585
+ setLocale('en')
586
+
587
+ // 2-part: singular|plural (value is prepended automatically)
588
+ it({
589
+ en: '{count, p, item|items}',
590
+ ko: '{count, p, 개|개}'
591
+ }, { count: 1 }) // → "1 item"
592
+
593
+ it({
594
+ en: '{count, p, item|items}',
595
+ ko: '{count, p, 개|개}'
596
+ }, { count: 5 }) // → "5 items"
597
+
598
+ // 3-part: zero|singular|plural
599
+ it({
600
+ en: '{count, p, no items|item|items}',
601
+ ko: '{count, p, 항목 없음|개|개}'
602
+ }, { count: 0 }) // → "no items"
603
+
604
+ it({
605
+ en: '{count, p, no items|item|items}',
606
+ ko: '{count, p, 항목 없음|개|개}'
607
+ }, { count: 1 }) // → "1 item"
608
+
609
+ it({
610
+ en: '{count, p, no items|item|items}',
611
+ ko: '{count, p, 항목 없음|개|개}'
612
+ }, { count: 5 }) // → "5 items"
613
+ ```
614
+
615
+ The shorthand is preprocessed into standard ICU `plural` syntax before parsing.
616
+
617
+ ---
618
+
619
+ ## Locale Persistence
620
+
621
+ Auto-save and restore the user's locale to `cookie` or `localStorage`:
622
+
623
+ ```typescript
624
+ import { configure, setLocale, restoreLocale } from 'inline-i18n-multi'
625
+
626
+ // Configure persistence
627
+ configure({
628
+ persistLocale: {
629
+ storage: 'cookie', // 'cookie' | 'localStorage'
630
+ key: 'LOCALE', // storage key (default: 'LOCALE')
631
+ expires: 365 // cookie expiry in days (default: 365)
632
+ }
633
+ })
634
+
635
+ // Restore locale from storage (returns the saved locale or undefined)
636
+ const saved = restoreLocale()
637
+ if (saved) {
638
+ // locale was restored from storage
639
+ }
640
+
641
+ // setLocale() automatically saves to the configured storage
642
+ setLocale('ko') // also saves 'ko' to cookie or localStorage
643
+ ```
644
+
645
+ ```typescript
646
+ // localStorage example
647
+ configure({
648
+ persistLocale: {
649
+ storage: 'localStorage',
650
+ key: 'APP_LOCALE'
651
+ }
652
+ })
653
+
654
+ restoreLocale() // reads from localStorage
655
+ setLocale('ja') // saves 'ja' to localStorage
656
+ ```
657
+
658
+ ---
659
+
554
660
  ## Configuration
555
661
 
556
662
  Configure global settings for fallback behavior and warnings:
@@ -636,12 +742,14 @@ Available helpers:
636
742
 
637
743
  | Function | Description |
638
744
  |----------|-------------|
639
- | `configure(options)` | Configure global settings (fallback, warnings, debug, missingVarHandler) |
745
+ | `configure(options)` | Configure global settings (fallback, warnings, debug, missingVarHandler, icuCacheSize, persistLocale) |
640
746
  | `getConfig()` | Get current configuration |
641
747
  | `resetConfig()` | Reset configuration to defaults |
642
748
  | `loadAsync(locale, namespace?)` | Asynchronously load dictionary using configured loader |
643
749
  | `isLoaded(locale, namespace?)` | Check if dictionary has been loaded |
644
750
  | `parseRichText(template, names)` | Parse rich text template into segments |
751
+ | `clearICUCache()` | Clear the ICU message AST cache |
752
+ | `restoreLocale()` | Restore locale from configured persistent storage (cookie or localStorage) |
645
753
 
646
754
  ### Custom Formatters
647
755
 
@@ -681,6 +789,17 @@ interface Config {
681
789
  debugMode?: boolean | DebugModeOptions
682
790
  loader?: (locale: Locale, namespace: string) => Promise<Record<string, unknown>>
683
791
  missingVarHandler?: (varName: string, locale: string) => string
792
+ icuCacheSize?: number
793
+ persistLocale?: PersistLocaleOptions
794
+ }
795
+
796
+ interface PersistLocaleOptions {
797
+ /** Storage backend */
798
+ storage: 'cookie' | 'localStorage'
799
+ /** Storage key (default: 'LOCALE') */
800
+ key?: string
801
+ /** Cookie expiry in days (default: 365, cookie only) */
802
+ expires?: number
684
803
  }
685
804
 
686
805
  interface DebugModeOptions {
package/dist/index.d.mts CHANGED
@@ -65,6 +65,17 @@ interface Config {
65
65
  loader?: (locale: Locale, namespace: string) => Promise<Record<string, unknown>>;
66
66
  /** Custom handler for missing interpolation variables (v0.6.0) */
67
67
  missingVarHandler?: (varName: string, locale: string) => string;
68
+ /** Maximum number of parsed ICU ASTs to cache (default: 500, 0 to disable) (v0.7.0) */
69
+ icuCacheSize?: number;
70
+ /** Locale persistence configuration (v0.7.0) */
71
+ persistLocale?: {
72
+ /** Storage mechanism */
73
+ storage: 'cookie' | 'localStorage';
74
+ /** Storage key name (default: 'LOCALE') */
75
+ key?: string;
76
+ /** Cookie expiry in days (default: 365). Only applies to cookie storage. */
77
+ expires?: number;
78
+ };
68
79
  }
69
80
 
70
81
  /**
@@ -83,6 +94,11 @@ declare function it(translations: Translations, vars?: TranslationVars): string;
83
94
 
84
95
  declare function setLocale(locale: Locale): void;
85
96
  declare function getLocale(): Locale;
97
+ /**
98
+ * Restore locale from configured persistent storage.
99
+ * Returns the restored locale, or undefined if nothing was found.
100
+ */
101
+ declare function restoreLocale(): Locale | undefined;
86
102
 
87
103
  /**
88
104
  * Runtime lookup function for build-tool-transformed code.
@@ -212,7 +228,7 @@ declare function getDictionary(locale: Locale, namespace?: string): Dictionary |
212
228
  */
213
229
  declare function getLoadedNamespaces(): string[];
214
230
 
215
- type FullConfig = Required<Omit<Config, 'loader' | 'missingVarHandler'>> & Pick<Config, 'loader' | 'missingVarHandler'>;
231
+ type FullConfig = Required<Omit<Config, 'loader' | 'missingVarHandler' | 'persistLocale'>> & Pick<Config, 'loader' | 'missingVarHandler' | 'persistLocale'>;
216
232
  /**
217
233
  * Configure inline-i18n-multi settings
218
234
  *
@@ -233,6 +249,10 @@ declare function getConfig(): FullConfig;
233
249
  */
234
250
  declare function resetConfig(): void;
235
251
 
252
+ /**
253
+ * Clear the ICU message AST cache
254
+ */
255
+ declare function clearICUCache(): void;
236
256
  type CustomFormatter = (value: unknown, locale: string, style?: string) => string;
237
257
  /**
238
258
  * Register a custom formatter
@@ -303,4 +323,4 @@ interface RichTextSegment {
303
323
  */
304
324
  declare function parseRichText(template: string, componentNames: string[]): RichTextSegment[];
305
325
 
306
- export { type Config, type CustomFormatter, type DebugModeOptions, type DetectLocaleOptions, type DetectSource, type Dictionaries, type Dictionary, type Locale, type PluralRules, type RichTextSegment, type TranslationVars, type TranslationWarning, type Translations, type WarningHandler, __i18n_lookup, clearDictionaries, clearFormatters, configure, detectLocale, en_de, en_es, en_fr, en_ja, en_zh, getConfig, getDictionary, getLoadedLocales, getLoadedNamespaces, getLocale, hasTranslation, isLoaded, it, it_de, it_es, it_fr, it_ja, it_zh, ja_es, ja_zh, loadAsync, loadDictionaries, loadDictionary, parseRichText, registerFormatter, resetConfig, setLocale, t, zh_es };
326
+ export { type Config, type CustomFormatter, type DebugModeOptions, type DetectLocaleOptions, type DetectSource, type Dictionaries, type Dictionary, type Locale, type PluralRules, type RichTextSegment, type TranslationVars, type TranslationWarning, type Translations, type WarningHandler, __i18n_lookup, clearDictionaries, clearFormatters, clearICUCache, configure, detectLocale, en_de, en_es, en_fr, en_ja, en_zh, getConfig, getDictionary, getLoadedLocales, getLoadedNamespaces, getLocale, hasTranslation, isLoaded, it, it_de, it_es, it_fr, it_ja, it_zh, ja_es, ja_zh, loadAsync, loadDictionaries, loadDictionary, parseRichText, registerFormatter, resetConfig, restoreLocale, setLocale, t, zh_es };
package/dist/index.d.ts CHANGED
@@ -65,6 +65,17 @@ interface Config {
65
65
  loader?: (locale: Locale, namespace: string) => Promise<Record<string, unknown>>;
66
66
  /** Custom handler for missing interpolation variables (v0.6.0) */
67
67
  missingVarHandler?: (varName: string, locale: string) => string;
68
+ /** Maximum number of parsed ICU ASTs to cache (default: 500, 0 to disable) (v0.7.0) */
69
+ icuCacheSize?: number;
70
+ /** Locale persistence configuration (v0.7.0) */
71
+ persistLocale?: {
72
+ /** Storage mechanism */
73
+ storage: 'cookie' | 'localStorage';
74
+ /** Storage key name (default: 'LOCALE') */
75
+ key?: string;
76
+ /** Cookie expiry in days (default: 365). Only applies to cookie storage. */
77
+ expires?: number;
78
+ };
68
79
  }
69
80
 
70
81
  /**
@@ -83,6 +94,11 @@ declare function it(translations: Translations, vars?: TranslationVars): string;
83
94
 
84
95
  declare function setLocale(locale: Locale): void;
85
96
  declare function getLocale(): Locale;
97
+ /**
98
+ * Restore locale from configured persistent storage.
99
+ * Returns the restored locale, or undefined if nothing was found.
100
+ */
101
+ declare function restoreLocale(): Locale | undefined;
86
102
 
87
103
  /**
88
104
  * Runtime lookup function for build-tool-transformed code.
@@ -212,7 +228,7 @@ declare function getDictionary(locale: Locale, namespace?: string): Dictionary |
212
228
  */
213
229
  declare function getLoadedNamespaces(): string[];
214
230
 
215
- type FullConfig = Required<Omit<Config, 'loader' | 'missingVarHandler'>> & Pick<Config, 'loader' | 'missingVarHandler'>;
231
+ type FullConfig = Required<Omit<Config, 'loader' | 'missingVarHandler' | 'persistLocale'>> & Pick<Config, 'loader' | 'missingVarHandler' | 'persistLocale'>;
216
232
  /**
217
233
  * Configure inline-i18n-multi settings
218
234
  *
@@ -233,6 +249,10 @@ declare function getConfig(): FullConfig;
233
249
  */
234
250
  declare function resetConfig(): void;
235
251
 
252
+ /**
253
+ * Clear the ICU message AST cache
254
+ */
255
+ declare function clearICUCache(): void;
236
256
  type CustomFormatter = (value: unknown, locale: string, style?: string) => string;
237
257
  /**
238
258
  * Register a custom formatter
@@ -303,4 +323,4 @@ interface RichTextSegment {
303
323
  */
304
324
  declare function parseRichText(template: string, componentNames: string[]): RichTextSegment[];
305
325
 
306
- export { type Config, type CustomFormatter, type DebugModeOptions, type DetectLocaleOptions, type DetectSource, type Dictionaries, type Dictionary, type Locale, type PluralRules, type RichTextSegment, type TranslationVars, type TranslationWarning, type Translations, type WarningHandler, __i18n_lookup, clearDictionaries, clearFormatters, configure, detectLocale, en_de, en_es, en_fr, en_ja, en_zh, getConfig, getDictionary, getLoadedLocales, getLoadedNamespaces, getLocale, hasTranslation, isLoaded, it, it_de, it_es, it_fr, it_ja, it_zh, ja_es, ja_zh, loadAsync, loadDictionaries, loadDictionary, parseRichText, registerFormatter, resetConfig, setLocale, t, zh_es };
326
+ export { type Config, type CustomFormatter, type DebugModeOptions, type DetectLocaleOptions, type DetectSource, type Dictionaries, type Dictionary, type Locale, type PluralRules, type RichTextSegment, type TranslationVars, type TranslationWarning, type Translations, type WarningHandler, __i18n_lookup, clearDictionaries, clearFormatters, clearICUCache, configure, detectLocale, en_de, en_es, en_fr, en_ja, en_zh, getConfig, getDictionary, getLoadedLocales, getLoadedNamespaces, getLocale, hasTranslation, isLoaded, it, it_de, it_es, it_fr, it_ja, it_zh, ja_es, ja_zh, loadAsync, loadDictionaries, loadDictionary, parseRichText, registerFormatter, resetConfig, restoreLocale, setLocale, t, zh_es };
package/dist/index.js CHANGED
@@ -2,15 +2,6 @@
2
2
 
3
3
  var icuMessageformatParser = require('@formatjs/icu-messageformat-parser');
4
4
 
5
- // src/context.ts
6
- var currentLocale = "en";
7
- function setLocale(locale) {
8
- currentLocale = locale;
9
- }
10
- function getLocale() {
11
- return currentLocale;
12
- }
13
-
14
5
  // src/config.ts
15
6
  function defaultWarningHandler(warning) {
16
7
  const parts = [`[inline-i18n] Missing translation for locale "${warning.requestedLocale}"`];
@@ -42,7 +33,8 @@ var defaultConfig = {
42
33
  warnOnMissing: isDevMode(),
43
34
  onMissingTranslation: defaultWarningHandler,
44
35
  debugMode: false,
45
- loader: void 0
36
+ loader: void 0,
37
+ icuCacheSize: 500
46
38
  };
47
39
  var config = { ...defaultConfig };
48
40
  function configure(options) {
@@ -110,6 +102,82 @@ function applyDebugFormat(output, debugInfo) {
110
102
  }
111
103
  return output;
112
104
  }
105
+
106
+ // src/context.ts
107
+ var currentLocale = "en";
108
+ function setCookie(name, value, days) {
109
+ if (typeof document === "undefined") return;
110
+ const expires = new Date(Date.now() + days * 864e5).toUTCString();
111
+ document.cookie = `${name}=${value}; expires=${expires}; path=/; SameSite=Lax`;
112
+ }
113
+ function getCookie(name) {
114
+ if (typeof document === "undefined") return void 0;
115
+ const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
116
+ return match?.[1];
117
+ }
118
+ function persistLocaleToStorage(locale) {
119
+ const cfg = getConfig();
120
+ if (!cfg.persistLocale) return;
121
+ const { storage, key = "LOCALE", expires = 365 } = cfg.persistLocale;
122
+ if (storage === "cookie") {
123
+ setCookie(key, locale, expires);
124
+ } else if (storage === "localStorage") {
125
+ if (typeof localStorage !== "undefined") {
126
+ try {
127
+ localStorage.setItem(key, locale);
128
+ } catch {
129
+ }
130
+ }
131
+ }
132
+ }
133
+ function setLocale(locale) {
134
+ currentLocale = locale;
135
+ persistLocaleToStorage(locale);
136
+ }
137
+ function getLocale() {
138
+ return currentLocale;
139
+ }
140
+ function restoreLocale() {
141
+ const cfg = getConfig();
142
+ if (!cfg.persistLocale) return void 0;
143
+ const { storage, key = "LOCALE" } = cfg.persistLocale;
144
+ let stored;
145
+ if (storage === "cookie") {
146
+ stored = getCookie(key);
147
+ } else if (storage === "localStorage") {
148
+ if (typeof localStorage !== "undefined") {
149
+ try {
150
+ stored = localStorage.getItem(key) ?? void 0;
151
+ } catch {
152
+ }
153
+ }
154
+ }
155
+ if (stored) {
156
+ currentLocale = stored;
157
+ return stored;
158
+ }
159
+ return void 0;
160
+ }
161
+ var icuCache = /* @__PURE__ */ new Map();
162
+ function clearICUCache() {
163
+ icuCache.clear();
164
+ }
165
+ function cachedParse(template) {
166
+ const cached = icuCache.get(template);
167
+ if (cached) return cached;
168
+ const ast = icuMessageformatParser.parse(template);
169
+ const cfg = getConfig();
170
+ if (cfg.icuCacheSize > 0) {
171
+ if (icuCache.size >= cfg.icuCacheSize) {
172
+ const firstKey = icuCache.keys().next().value;
173
+ if (firstKey !== void 0) {
174
+ icuCache.delete(firstKey);
175
+ }
176
+ }
177
+ icuCache.set(template, ast);
178
+ }
179
+ return ast;
180
+ }
113
181
  function handleMissingVar(varName, locale) {
114
182
  const cfg = getConfig();
115
183
  if (cfg.missingVarHandler) {
@@ -404,13 +472,34 @@ function formatListValue(variableName, type, style, vars, locale) {
404
472
  return value.join(", ");
405
473
  }
406
474
  }
475
+ var PLURAL_SHORTHAND_PATTERN = /\{(\w+),\s*p,\s*([^}]+)\}/g;
476
+ function hasPluralShorthand(template) {
477
+ PLURAL_SHORTHAND_PATTERN.lastIndex = 0;
478
+ return PLURAL_SHORTHAND_PATTERN.test(template);
479
+ }
480
+ function preprocessPluralShorthand(template) {
481
+ PLURAL_SHORTHAND_PATTERN.lastIndex = 0;
482
+ return template.replace(PLURAL_SHORTHAND_PATTERN, (_, variable, args) => {
483
+ const parts = args.split("|").map((s) => s.trim());
484
+ if (parts.length === 2) {
485
+ const [singular, plural] = parts;
486
+ return `{${variable}, plural, one {# ${singular}} other {# ${plural}}}`;
487
+ }
488
+ if (parts.length === 3) {
489
+ const [zero, singular, plural] = parts;
490
+ return `{${variable}, plural, =0 {${zero}} one {# ${singular}} other {# ${plural}}}`;
491
+ }
492
+ return `{${variable}, p, ${args}}`;
493
+ });
494
+ }
407
495
  function interpolateICU(template, vars, locale) {
408
- const { processed: afterCustom, replacements: customReplacements } = preprocessCustomFormatters(template);
496
+ const afterPluralShorthand = preprocessPluralShorthand(template);
497
+ const { processed: afterCustom, replacements: customReplacements } = preprocessCustomFormatters(afterPluralShorthand);
409
498
  const { processed: afterCurrency, replacements: currencyReplacements } = preprocessCurrency(afterCustom);
410
499
  const { processed: afterCompact, replacements: compactReplacements } = preprocessCompactNumber(afterCurrency);
411
500
  const { processed: afterRelTime, replacements: relTimeReplacements } = preprocessRelativeTime(afterCompact);
412
501
  const { processed: afterList, replacements: listReplacements } = preprocessList(afterRelTime);
413
- const ast = icuMessageformatParser.parse(afterList);
502
+ const ast = cachedParse(afterList);
414
503
  let result = formatElements(ast, vars, locale, null);
415
504
  for (const [placeholder, { variable, formatterName, style }] of customReplacements) {
416
505
  const value = vars[variable];
@@ -507,7 +596,7 @@ function formatSelect(el, vars, locale) {
507
596
  }
508
597
  return handleMissingVar(el.value, locale);
509
598
  }
510
- var ICU_PATTERN = /\{[^}]+,\s*(plural|select|selectordinal|number|date|time|relativeTime|list|currency)\s*[,}]/;
599
+ var ICU_PATTERN = /\{[^}]+,\s*(plural|select|selectordinal|number|date|time|relativeTime|list|currency|p)\s*[,}]/;
511
600
  function hasICUPattern(template) {
512
601
  return ICU_PATTERN.test(template);
513
602
  }
@@ -516,7 +605,7 @@ function hasICUPattern(template) {
516
605
  var VARIABLE_PATTERN = /\{(\w+)\}/g;
517
606
  function interpolate(template, vars, locale) {
518
607
  const resolvedLocale = locale || "en";
519
- if (hasICUPattern(template) || hasCustomFormatter(template)) {
608
+ if (hasICUPattern(template) || hasCustomFormatter(template) || hasPluralShorthand(template)) {
520
609
  if (!vars) {
521
610
  const cfg = getConfig();
522
611
  if (cfg.missingVarHandler) {
@@ -1000,6 +1089,7 @@ function parseRichText(template, componentNames) {
1000
1089
  exports.__i18n_lookup = __i18n_lookup;
1001
1090
  exports.clearDictionaries = clearDictionaries;
1002
1091
  exports.clearFormatters = clearFormatters;
1092
+ exports.clearICUCache = clearICUCache;
1003
1093
  exports.configure = configure;
1004
1094
  exports.detectLocale = detectLocale;
1005
1095
  exports.en_de = en_de;
@@ -1028,6 +1118,7 @@ exports.loadDictionary = loadDictionary;
1028
1118
  exports.parseRichText = parseRichText;
1029
1119
  exports.registerFormatter = registerFormatter;
1030
1120
  exports.resetConfig = resetConfig;
1121
+ exports.restoreLocale = restoreLocale;
1031
1122
  exports.setLocale = setLocale;
1032
1123
  exports.t = t;
1033
1124
  exports.zh_es = zh_es;