i18n-typed-store 0.1.0 → 0.1.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/dist/index.d.ts CHANGED
@@ -1,56 +1,208 @@
1
+ import { T as TranslationStore } from './context-Dp43aQ0V.js';
2
+ export { I as II18nTypedStoreContext } from './context-Dp43aQ0V.js';
3
+
4
+ /**
5
+ * Map of translation module loaders.
6
+ * Each namespace key maps to an object where each locale key maps to a loader function.
7
+ *
8
+ * @template T - Type of translations object (e.g., { common: 'common', errors: 'errors' })
9
+ * @template L - Type of locales object (e.g., { en: 'en', ru: 'ru' })
10
+ * @template Module - Type of the raw module loaded from the module loader
11
+ */
12
+ type TranslationModuleMap<T extends Record<string, string>, L extends Record<string, string>, Module = unknown> = Record<keyof T, Record<keyof L, () => Promise<Module>>>;
13
+
1
14
  /**
2
15
  * Creates a map of translation module loaders for all combinations of translations and locales.
16
+ * This map is used internally by the translation store to lazy-load translation modules.
17
+ *
18
+ * @template T - Type of translations object (e.g., { common: 'common', errors: 'errors' })
19
+ * @template L - Type of locales object (e.g., { en: 'en', ru: 'ru' })
20
+ * @template Module - Type of the raw module loaded from the module loader
3
21
  *
4
22
  * @param translations - Object with translation keys
5
23
  * @param locales - Object with locale keys
6
- * @param loadModule - Function to load a translation module for a specific locale and translation
7
- * @returns Map where each translation key contains an object with loader functions for each locale
24
+ * @param loadModule - Function to load a translation module for a specific locale and namespace
25
+ * @returns Immutable map where each namespace key contains an object with loader functions for each locale
26
+ * @throws {TypeError} If translations or locales are empty objects
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const translations = { common: 'common', errors: 'errors' } as const;
31
+ * const locales = { en: 'en', ru: 'ru' } as const;
32
+ * const loadModule = async (locale, namespace) => import(`./${namespace}/${locale}.json`);
33
+ *
34
+ * const moduleMap = createTranslationModuleMap(translations, locales, loadModule);
35
+ * moduleMap.common.en() will load './common/en.json'
36
+ * moduleMap.errors.ru() will load './errors/ru.json'
37
+ * ```
8
38
  */
9
- declare const createTranslationModuleMap: <T extends Record<string, string>, L extends Record<string, string>, Module = unknown>(translations: T, locales: L, loadModule: (locale: keyof L, translation: keyof T) => Promise<Module>) => Record<keyof T, Record<keyof L, () => Promise<Module>>>;
39
+ declare const createTranslationModuleMap: <T extends Record<string, string>, L extends Record<string, string>, Module = unknown>(translations: T, locales: L, loadModule: (locale: keyof L, namespace: keyof T) => Promise<Module>) => TranslationModuleMap<T, L, Module>;
10
40
 
11
41
  /**
12
- * Creates a translation store with typed translations for different locales.
42
+ * Options for creating a translation store.
13
43
  *
14
- * @param translations - Object with translation keys
15
- * @param locales - Object with locale keys
16
- * @param loadModule - Function to load a translation module
17
- * @param extractTranslation - Function to extract translation data from the loaded module.
18
- * Receives three parameters: (module, locale, translation) allowing for locale-specific
19
- * or translation-specific extraction logic.
20
- * @returns Object with a type() method for creating a typed translation store
44
+ * @template T - Type of translations object (e.g., { common: 'common', errors: 'errors' })
45
+ * @template L - Type of locales object (e.g., { en: 'en', ru: 'ru' })
46
+ * @template Module - Type of the raw module loaded from the module loader
47
+ */
48
+ interface CreateTranslationStoreOptions<T extends Record<string, string>, L extends Record<string, string>, Module = unknown> {
49
+ /** Object with translation keys */
50
+ translations: T;
51
+ /** Object with locale keys */
52
+ locales: L;
53
+ /** Function to load a translation module for a specific locale and namespace */
54
+ loadModule: (locale: keyof L, namespace: keyof T) => Promise<Module>;
55
+ /**
56
+ * Function to extract translation data from the loaded module.
57
+ * Receives three parameters: (module, locale, namespace) allowing for locale-specific
58
+ * or namespace-specific extraction logic.
59
+ */
60
+ extractTranslation: (module: Module, locale: keyof L, namespace: keyof T) => unknown | Promise<unknown>;
61
+ /**
62
+ * Whether to delete translations for other locales after loading a new one.
63
+ * Useful for memory-constrained environments.
64
+ * @default false
65
+ */
66
+ deleteOtherLocalesAfterLoad?: boolean;
67
+ /**
68
+ * Whether to load translations from cache by default.
69
+ * If false, will always reload even if translation is already cached.
70
+ * @default true
71
+ */
72
+ loadFromCache?: boolean;
73
+ /** Default locale key to use */
74
+ defaultLocale: keyof L;
75
+ /**
76
+ * Whether to use fallback locale for missing translations.
77
+ * When enabled, translations will be merged with fallback locale translations.
78
+ * @default false
79
+ */
80
+ useFallback?: boolean;
81
+ /**
82
+ * Fallback locale key to use when useFallback is true.
83
+ * If not provided, defaultLocale will be used as fallback.
84
+ * @default defaultLocale
85
+ */
86
+ fallbackLocale?: keyof L;
87
+ /**
88
+ * Event name for locale change events.
89
+ * @default 'change-locale'
90
+ */
91
+ changeLocaleEventName?: string;
92
+ }
93
+
94
+ /**
95
+ * Creates a translation store factory with typed translations for different locales.
96
+ * The store supports lazy loading, caching, error handling, and fallback locale merging.
97
+ *
98
+ * @template T - Type of translations object (e.g., { common: 'common', errors: 'errors' })
99
+ * @template L - Type of locales object (e.g., { en: 'en', ru: 'ru' })
100
+ * @template Module - Type of the raw module loaded from the module loader
101
+ *
102
+ * @param options - Configuration options for the translation store
103
+ * @returns Object with a `type()` method for creating a typed translation store
104
+ * @throws {TypeError} If required options are invalid
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * const translations = { common: 'common', errors: 'errors' } as const;
109
+ * const locales = { en: 'en', ru: 'ru' } as const;
110
+ *
111
+ * const storeFactory = createTranslationStore({
112
+ * translations,
113
+ * locales,
114
+ * loadModule: async (locale, namespace) => import(`./${namespace}/${locale}.json`),
115
+ * extractTranslation: (module) => module.default || module,
116
+ * defaultLocale: 'en',
117
+ * useFallback: true,
118
+ * fallbackLocale: 'en',
119
+ * });
120
+ *
121
+ * const store = storeFactory.type<{
122
+ * common: { greeting: string };
123
+ * errors: { notFound: string };
124
+ * }>();
125
+ * ```
21
126
  */
22
- declare const createTranslationStore: <T extends Record<string, string>, L extends Record<string, string>, Module = unknown>(translations: T, locales: L, loadModule: (locale: keyof L, translation: keyof T) => Promise<Module>, extractTranslation: (module: Module, locale: keyof L, translation: keyof T) => unknown) => {
127
+ declare const createTranslationStore: <T extends Record<string, string>, L extends Record<string, string>, Module = unknown>({ translations, locales, loadModule, extractTranslation, deleteOtherLocalesAfterLoad, loadFromCache, defaultLocale, useFallback, fallbackLocale, changeLocaleEventName, }: CreateTranslationStoreOptions<T, L, Module>) => {
23
128
  /**
24
129
  * Creates a typed translation store.
130
+ * The store provides methods to load and access translations for each locale.
131
+ * When useFallback is enabled, translations are automatically merged with fallback locale.
25
132
  *
26
- * @template M - Type of translation object where each key corresponds to a key from translations
133
+ * @template M - Type of translation modules mapping where each key corresponds to a key from translations
27
134
  * @returns Store with methods to load translations for each locale
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * const store = storeFactory.type<{
139
+ * common: { greeting: string; goodbye: string };
140
+ * errors: { notFound: string; unauthorized: string };
141
+ * }>();
142
+ *
143
+ * await store.common.load('ru');
144
+ * // If useFallback is true and 'ru' translation is missing some keys,
145
+ * // they will be filled from fallback locale (e.g., 'en')
146
+ * const greeting = store.common.translations.ru.namespace?.greeting;
147
+ * ```
28
148
  */
29
- type: <M extends { [K in keyof T]: any; }>() => { [K in keyof T]: {
30
- translation?: M[K];
31
- load: (locale: keyof L) => Promise<void>;
32
- }; };
149
+ type: <M extends { [K in keyof T]: any; }>() => TranslationStore<T, L, M>;
33
150
  };
34
151
 
35
152
  /**
36
153
  * Plural form variants for different plural categories.
37
154
  * Based on Unicode CLDR plural rules: zero, one, two, few, many, other.
155
+ *
156
+ * @see https://unicode-org.github.io/cldr-staging/charts/latest/supplemental/language_plural_rules.html
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * const variants: PluralVariants = {
161
+ * one: 'item',
162
+ * other: 'items'
163
+ * };
164
+ * ```
38
165
  */
39
166
  type PluralVariants = {
167
+ /** Used for count = 0 (in some languages) */
40
168
  zero?: string;
169
+ /** Used for count = 1 (in most languages) */
41
170
  one?: string;
171
+ /** Used for count = 2 (in some languages like Welsh) */
42
172
  two?: string;
173
+ /** Used for small numbers (e.g., 3-10 in Russian) */
43
174
  few?: string;
175
+ /** Used for large numbers or fractional values */
44
176
  many?: string;
45
- other?: string;
177
+ /** Default/fallback variant - MUST be provided for correct pluralization */
178
+ other: string;
46
179
  };
180
+ /**
181
+ * Valid plural category names according to CLDR.
182
+ */
183
+ type PluralCategory = keyof PluralVariants;
184
+
185
+ /**
186
+ * Options for creating a plural selector.
187
+ */
188
+ interface CreatePluralSelectorOptions {
189
+ /**
190
+ * Whether to throw an error if 'other' variant is missing.
191
+ * @default false
192
+ */
193
+ strict?: boolean;
194
+ }
47
195
 
48
196
  /**
49
197
  * Creates a plural selector function for a specific locale.
50
- * The returned function selects the appropriate plural form based on the count.
198
+ * The returned function selects the appropriate plural form based on the count
199
+ * using Unicode CLDR plural rules.
51
200
  *
52
- * @param locale - Locale string (e.g., 'en', 'ru', 'fr')
201
+ * @param locale - Locale string (e.g., 'en', 'ru', 'fr', 'uk-UA')
202
+ * @param options - Configuration options
53
203
  * @returns Function that takes a count and plural variants, returns the matching variant
204
+ * @throws {TypeError} If locale is not a valid string
205
+ * @throws {Error} If strict mode is enabled and 'other' variant is missing
54
206
  *
55
207
  * @example
56
208
  * ```ts
@@ -58,7 +210,34 @@ type PluralVariants = {
58
210
  * selectPlural(1, { one: 'item', other: 'items' }); // => 'item'
59
211
  * selectPlural(5, { one: 'item', other: 'items' }); // => 'items'
60
212
  * ```
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * const selectPlural = createPluralSelector('ru');
217
+ * selectPlural(1, { one: 'элемент', few: 'элемента', many: 'элементов', other: 'элементов' }); // => 'элемент'
218
+ * selectPlural(2, { one: 'элемент', few: 'элемента', many: 'элементов', other: 'элементов' }); // => 'элемента'
219
+ * selectPlural(5, { one: 'элемент', few: 'элемента', many: 'элементов', other: 'элементов' }); // => 'элементов'
220
+ * ```
221
+ */
222
+ declare const createPluralSelector: (locale: string, options?: CreatePluralSelectorOptions) => ((count: number, variants: PluralVariants) => string);
223
+
224
+ /**
225
+ * Event listener function type.
226
+ *
227
+ * @template T - Type of event arguments array
228
+ */
229
+ type Listener<T extends any[] = any[]> = (...args: T) => void;
230
+ /**
231
+ * Event map type that maps event names to their argument types.
232
+ *
233
+ * @example
234
+ * ```ts
235
+ * type MyEvents = {
236
+ * 'user-login': [userId: string, timestamp: number];
237
+ * 'user-logout': [userId: string];
238
+ * };
239
+ * ```
61
240
  */
62
- declare const createPluralSelector: (locale: string) => (count: number, variants: PluralVariants) => string;
241
+ type EventMap = Record<PropertyKey, any[]>;
63
242
 
64
- export { createPluralSelector, createTranslationModuleMap, createTranslationStore };
243
+ export { type CreatePluralSelectorOptions, type CreateTranslationStoreOptions, type EventMap, type Listener, type PluralCategory, type PluralVariants, type TranslationModuleMap, TranslationStore, createPluralSelector, createTranslationModuleMap, createTranslationStore };
package/dist/index.js CHANGED
@@ -2,39 +2,335 @@
2
2
 
3
3
  // src/utils/create-translation-module-map.ts
4
4
  var createTranslationModuleMap = (translations, locales, loadModule) => {
5
- const translationModules = {};
6
- for (const translationKey of Object.keys(translations)) {
7
- translationModules[translationKey] = {};
5
+ if (!translations || Object.keys(translations).length === 0) {
6
+ throw new TypeError("translations must be a non-empty object");
7
+ }
8
+ if (!locales || Object.keys(locales).length === 0) {
9
+ throw new TypeError("locales must be a non-empty object");
10
+ }
11
+ if (typeof loadModule !== "function") {
12
+ throw new TypeError("loadModule must be a function");
13
+ }
14
+ const namespaceModules = {};
15
+ for (const namespaceKey of Object.keys(translations)) {
16
+ namespaceModules[namespaceKey] = {};
8
17
  for (const localeKey of Object.keys(locales)) {
9
- translationModules[translationKey][localeKey] = () => loadModule(localeKey, translationKey);
18
+ namespaceModules[namespaceKey][localeKey] = () => loadModule(localeKey, namespaceKey);
10
19
  }
11
20
  }
12
- return translationModules;
21
+ return namespaceModules;
13
22
  };
14
23
 
24
+ // src/utils/event-emitter.ts
25
+ var EventEmitter = class {
26
+ constructor() {
27
+ this.listeners = {};
28
+ }
29
+ /**
30
+ * Registers an event listener for the specified event.
31
+ *
32
+ * @param event - Event name
33
+ * @param listener - Listener function
34
+ * @returns This instance for method chaining
35
+ */
36
+ on(event, listener) {
37
+ if (!this.listeners[event]) {
38
+ this.listeners[event] = /* @__PURE__ */ new Set();
39
+ }
40
+ this.listeners[event].add(listener);
41
+ return this;
42
+ }
43
+ /**
44
+ * Registers a one-time event listener that will be automatically removed after the first call.
45
+ *
46
+ * @param event - Event name
47
+ * @param listener - Listener function
48
+ * @returns This instance for method chaining
49
+ */
50
+ once(event, listener) {
51
+ const wrapper = (...args) => {
52
+ this.off(event, wrapper);
53
+ listener(...args);
54
+ };
55
+ return this.on(event, wrapper);
56
+ }
57
+ /**
58
+ * Removes an event listener. If no listener is provided, removes all listeners for the event.
59
+ *
60
+ * @param event - Event name
61
+ * @param listener - Optional listener function to remove
62
+ * @returns This instance for method chaining
63
+ */
64
+ off(event, listener) {
65
+ const set = this.listeners[event];
66
+ if (!set) {
67
+ return this;
68
+ }
69
+ if (!listener) {
70
+ set.clear();
71
+ } else {
72
+ set.delete(listener);
73
+ }
74
+ if (set.size === 0) {
75
+ delete this.listeners[event];
76
+ }
77
+ return this;
78
+ }
79
+ /**
80
+ * Emits an event, calling all registered listeners with the provided arguments.
81
+ *
82
+ * @param event - Event name
83
+ * @param args - Event arguments
84
+ * @returns True if there were listeners, false otherwise
85
+ */
86
+ emit(event, ...args) {
87
+ const set = this.listeners[event];
88
+ if (!set || set.size === 0) {
89
+ return false;
90
+ }
91
+ [...set].forEach((listener) => listener(...args));
92
+ return true;
93
+ }
94
+ /**
95
+ * Returns the number of listeners registered for the specified event.
96
+ *
97
+ * @param event - Event name
98
+ * @returns Number of listeners
99
+ */
100
+ listenerCount(event) {
101
+ return this.listeners[event]?.size ?? 0;
102
+ }
103
+ /**
104
+ * Removes all event listeners from all events.
105
+ *
106
+ * @returns This instance for method chaining
107
+ */
108
+ removeAllListeners() {
109
+ this.listeners = {};
110
+ return this;
111
+ }
112
+ /**
113
+ * Returns an array of all event names that have registered listeners.
114
+ *
115
+ * @returns Array of event names
116
+ */
117
+ eventNames() {
118
+ return Object.keys(this.listeners);
119
+ }
120
+ };
121
+
122
+ // src/utils/smart-merge.ts
123
+ function smartDeepMerge(current, fallback) {
124
+ if (current == null) return fallback;
125
+ if (fallback == null) return current;
126
+ const currentIsObject = typeof current === "object" && !Array.isArray(current);
127
+ const fallbackIsObject = typeof fallback === "object" && !Array.isArray(fallback);
128
+ const currentIsArray = Array.isArray(current);
129
+ const fallbackIsArray = Array.isArray(fallback);
130
+ if (currentIsObject && !fallbackIsObject && !fallbackIsArray || !currentIsObject && !currentIsArray && fallbackIsObject || currentIsArray && !fallbackIsArray || !currentIsArray && fallbackIsArray) {
131
+ return fallback;
132
+ }
133
+ if (!currentIsObject && !fallbackIsObject) {
134
+ return current != null ? current : fallback;
135
+ }
136
+ const result = { ...current };
137
+ for (const key in fallback) {
138
+ if (!(key in result)) {
139
+ result[key] = fallback[key];
140
+ } else {
141
+ const currentValue = result[key];
142
+ const fallbackValue = fallback[key];
143
+ const currentValueIsObject = currentValue != null && typeof currentValue === "object" && !Array.isArray(currentValue);
144
+ const fallbackValueIsObject = fallbackValue != null && typeof fallbackValue === "object" && !Array.isArray(fallbackValue);
145
+ const currentValueIsArray = Array.isArray(currentValue);
146
+ const fallbackValueIsArray = Array.isArray(fallbackValue);
147
+ if (currentValueIsObject && !fallbackValueIsObject && !fallbackValueIsArray || !currentValueIsObject && !currentValueIsArray && fallbackValueIsObject || currentValueIsArray && !fallbackValueIsArray || !currentValueIsArray && fallbackValueIsArray) {
148
+ result[key] = fallbackValue;
149
+ } else if (currentValueIsObject && fallbackValueIsObject) {
150
+ result[key] = smartDeepMerge(currentValue, fallbackValue);
151
+ } else if (currentValue == null) {
152
+ result[key] = fallbackValue;
153
+ }
154
+ }
155
+ }
156
+ return result;
157
+ }
158
+
15
159
  // src/utils/create-translation-store.ts
16
- var createTranslationStore = (translations, locales, loadModule, extractTranslation) => {
160
+ var createTranslationStore = ({
161
+ translations,
162
+ locales,
163
+ loadModule,
164
+ extractTranslation,
165
+ deleteOtherLocalesAfterLoad = false,
166
+ loadFromCache = true,
167
+ defaultLocale,
168
+ useFallback = false,
169
+ fallbackLocale = defaultLocale,
170
+ changeLocaleEventName = "change-locale"
171
+ }) => {
172
+ if (!translations || Object.keys(translations).length === 0) {
173
+ throw new TypeError("translations must be a non-empty object");
174
+ }
175
+ if (!locales || Object.keys(locales).length === 0) {
176
+ throw new TypeError("locales must be a non-empty object");
177
+ }
178
+ if (typeof loadModule !== "function") {
179
+ throw new TypeError("loadModule must be a function");
180
+ }
181
+ if (typeof extractTranslation !== "function") {
182
+ throw new TypeError("extractTranslation must be a function");
183
+ }
184
+ if (!(defaultLocale in locales)) {
185
+ throw new TypeError(`defaultLocale '${String(defaultLocale)}' must be a key in locales`);
186
+ }
187
+ if (useFallback && !(fallbackLocale in locales)) {
188
+ throw new TypeError(`fallbackLocale '${String(fallbackLocale)}' must be a key in locales`);
189
+ }
17
190
  return {
18
191
  /**
19
192
  * Creates a typed translation store.
193
+ * The store provides methods to load and access translations for each locale.
194
+ * When useFallback is enabled, translations are automatically merged with fallback locale.
20
195
  *
21
- * @template M - Type of translation object where each key corresponds to a key from translations
196
+ * @template M - Type of translation modules mapping where each key corresponds to a key from translations
22
197
  * @returns Store with methods to load translations for each locale
198
+ *
199
+ * @example
200
+ * ```ts
201
+ * const store = storeFactory.type<{
202
+ * common: { greeting: string; goodbye: string };
203
+ * errors: { notFound: string; unauthorized: string };
204
+ * }>();
205
+ *
206
+ * await store.common.load('ru');
207
+ * // If useFallback is true and 'ru' translation is missing some keys,
208
+ * // they will be filled from fallback locale (e.g., 'en')
209
+ * const greeting = store.common.translations.ru.namespace?.greeting;
210
+ * ```
23
211
  */
24
212
  type: () => {
25
- const translationModuleMap = createTranslationModuleMap(translations, locales, loadModule);
26
- const store = {};
27
- for (const translationKey of Object.keys(translations)) {
28
- store[translationKey] = {
29
- translation: void 0,
30
- load: async (locale) => {
31
- const moduleLoader = translationModuleMap[translationKey][locale];
32
- const loadedModule = await moduleLoader();
33
- store[translationKey].translation = extractTranslation(
34
- loadedModule,
35
- locale,
36
- translationKey
37
- );
213
+ const namespaceModuleMap = createTranslationModuleMap(translations, locales, loadModule);
214
+ const emitter = new EventEmitter();
215
+ const store = {
216
+ currentLocale: defaultLocale,
217
+ locales,
218
+ translationsMap: translations,
219
+ translations: {},
220
+ addChangeLocaleListener: (listener) => {
221
+ emitter.on(changeLocaleEventName, listener);
222
+ },
223
+ removeChangeLocaleListener: (listener) => {
224
+ emitter.off(changeLocaleEventName, listener);
225
+ },
226
+ changeLocale: (locale) => {
227
+ store.currentLocale = locale;
228
+ emitter.emit(changeLocaleEventName, locale);
229
+ }
230
+ };
231
+ for (const namespaceKey of Object.keys(translations)) {
232
+ store.translations[namespaceKey] = {
233
+ currentTranslation: void 0,
234
+ currentLocale: void 0,
235
+ translations: Object.fromEntries(
236
+ Object.keys(locales).map((localeKey) => [
237
+ localeKey,
238
+ {
239
+ namespace: void 0,
240
+ isLoading: false,
241
+ isError: false,
242
+ loadingPromise: void 0
243
+ }
244
+ ])
245
+ ),
246
+ load: async (locale = store.currentLocale || defaultLocale, fromCache = loadFromCache) => {
247
+ if (!(locale in locales)) {
248
+ throw new TypeError(`Invalid locale: '${String(locale)}' is not a valid locale key`);
249
+ }
250
+ const namespaceState = store.translations[namespaceKey].translations[locale];
251
+ if (namespaceState.loadingPromise) {
252
+ return namespaceState.loadingPromise;
253
+ }
254
+ if (namespaceState.isLoading) {
255
+ return;
256
+ }
257
+ const shouldUseCache = namespaceState.namespace && fromCache !== false;
258
+ if (shouldUseCache) {
259
+ store.translations[namespaceKey].currentTranslation = namespaceState.namespace;
260
+ store.translations[namespaceKey].currentLocale = locale;
261
+ return;
262
+ }
263
+ namespaceState.isError = false;
264
+ namespaceState.isLoading = true;
265
+ namespaceState.loadingPromise = (async () => {
266
+ try {
267
+ const namespaceState2 = store.translations[namespaceKey].translations[locale];
268
+ const loadedModule = await namespaceModuleMap[namespaceKey][locale]();
269
+ let currentTranslation = await extractTranslation(
270
+ loadedModule,
271
+ locale,
272
+ namespaceKey
273
+ );
274
+ if (useFallback && locale !== fallbackLocale) {
275
+ const fallbackState = store.translations[namespaceKey].translations[fallbackLocale];
276
+ let fallbackTranslation = fallbackState.namespace;
277
+ if (!fallbackTranslation) {
278
+ if (fallbackState.loadingPromise) {
279
+ await fallbackState.loadingPromise;
280
+ fallbackTranslation = fallbackState.namespace;
281
+ } else if (!fallbackState.isLoading) {
282
+ fallbackState.isError = false;
283
+ fallbackState.isLoading = true;
284
+ fallbackState.loadingPromise = (async () => {
285
+ try {
286
+ const fallbackModule = await namespaceModuleMap[namespaceKey][fallbackLocale]();
287
+ fallbackTranslation = await extractTranslation(
288
+ fallbackModule,
289
+ fallbackLocale,
290
+ namespaceKey
291
+ );
292
+ fallbackState.namespace = fallbackTranslation;
293
+ } catch (error) {
294
+ fallbackState.isError = true;
295
+ } finally {
296
+ fallbackState.isLoading = false;
297
+ fallbackState.loadingPromise = void 0;
298
+ }
299
+ })();
300
+ await fallbackState.loadingPromise;
301
+ fallbackTranslation = fallbackState.namespace;
302
+ } else {
303
+ fallbackTranslation = void 0;
304
+ }
305
+ }
306
+ if (fallbackTranslation) {
307
+ currentTranslation = smartDeepMerge(
308
+ currentTranslation,
309
+ fallbackTranslation
310
+ );
311
+ }
312
+ }
313
+ namespaceState2.namespace = currentTranslation;
314
+ if (deleteOtherLocalesAfterLoad) {
315
+ for (const otherLocaleKey of Object.keys(
316
+ store.translations[namespaceKey].translations
317
+ )) {
318
+ if (otherLocaleKey !== locale && otherLocaleKey !== store.currentLocale) {
319
+ store.translations[namespaceKey].translations[otherLocaleKey].namespace = void 0;
320
+ }
321
+ }
322
+ }
323
+ store.translations[namespaceKey].currentTranslation = namespaceState2.namespace;
324
+ store.translations[namespaceKey].currentLocale = locale;
325
+ } catch (error) {
326
+ namespaceState.isError = true;
327
+ throw error;
328
+ } finally {
329
+ namespaceState.isLoading = false;
330
+ namespaceState.loadingPromise = void 0;
331
+ }
332
+ })();
333
+ return namespaceState.loadingPromise;
38
334
  }
39
335
  };
40
336
  }
@@ -44,15 +340,33 @@ var createTranslationStore = (translations, locales, loadModule, extractTranslat
44
340
  };
45
341
 
46
342
  // src/utils/create-plural-selector.ts
47
- var createPluralSelector = (locale) => {
48
- const pluralRules = new Intl.PluralRules(locale);
343
+ var createPluralSelector = (locale, options = {}) => {
344
+ if (typeof locale !== "string" || locale.trim().length === 0) {
345
+ throw new TypeError(`Invalid locale: expected non-empty string, got ${typeof locale}`);
346
+ }
347
+ const { strict = false } = options;
348
+ let pluralRules;
349
+ try {
350
+ pluralRules = new Intl.PluralRules(locale);
351
+ } catch (error) {
352
+ throw new TypeError(`Invalid locale format: ${locale}. ${error instanceof Error ? error.message : String(error)}`);
353
+ }
49
354
  return (count, variants) => {
355
+ if (!Number.isFinite(count)) {
356
+ throw new TypeError(`Invalid count: expected finite number, got ${count}`);
357
+ }
358
+ if (strict && !variants.other) {
359
+ throw new Error("Plural variants must include 'other' variant when strict mode is enabled");
360
+ }
50
361
  const pluralCategory = pluralRules.select(count);
51
362
  const selectedVariant = variants[pluralCategory];
52
- if (selectedVariant) {
363
+ if (selectedVariant !== void 0 && selectedVariant !== null) {
53
364
  return selectedVariant;
54
365
  }
55
- return variants.other ?? "";
366
+ if (variants.other !== void 0) {
367
+ return variants.other;
368
+ }
369
+ return "";
56
370
  };
57
371
  };
58
372