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/README.md +445 -110
- package/dist/context-Dp43aQ0V.d.mts +91 -0
- package/dist/context-Dp43aQ0V.d.ts +91 -0
- package/dist/index.d.mts +201 -22
- package/dist/index.d.ts +201 -22
- package/dist/index.js +338 -24
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +338 -24
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.d.mts +296 -0
- package/dist/react/index.d.ts +296 -0
- package/dist/react/index.js +231 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +221 -0
- package/dist/react/index.mjs.map +1 -0
- package/package.json +15 -1
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
|
|
7
|
-
* @returns
|
|
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,
|
|
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
|
-
*
|
|
42
|
+
* Options for creating a translation store.
|
|
13
43
|
*
|
|
14
|
-
* @
|
|
15
|
-
* @
|
|
16
|
-
* @
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
|
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; }>() =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
18
|
+
namespaceModules[namespaceKey][localeKey] = () => loadModule(localeKey, namespaceKey);
|
|
10
19
|
}
|
|
11
20
|
}
|
|
12
|
-
return
|
|
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 = (
|
|
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
|
|
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
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
366
|
+
if (variants.other !== void 0) {
|
|
367
|
+
return variants.other;
|
|
368
|
+
}
|
|
369
|
+
return "";
|
|
56
370
|
};
|
|
57
371
|
};
|
|
58
372
|
|