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/README.md
CHANGED
|
@@ -10,6 +10,10 @@ Type-safe translation store for managing i18n locales with full TypeScript suppo
|
|
|
10
10
|
- ✅ **Pluralization support** - Built-in plural form selector using `Intl.PluralRules`
|
|
11
11
|
- ✅ **Flexible module loading** - Support for any module format (ESM, CommonJS, dynamic imports)
|
|
12
12
|
- ✅ **Zero runtime dependencies** - Lightweight and framework-agnostic
|
|
13
|
+
- ✅ **React integration** - Hooks and components for React applications
|
|
14
|
+
- ✅ **SSR/SSG support** - Built-in utilities for server-side rendering
|
|
15
|
+
- ✅ **Fallback locales** - Automatic merging with fallback translations
|
|
16
|
+
- ✅ **Caching** - Built-in translation caching for better performance
|
|
13
17
|
|
|
14
18
|
## Installation
|
|
15
19
|
|
|
@@ -55,24 +59,212 @@ type TranslationData = {
|
|
|
55
59
|
};
|
|
56
60
|
};
|
|
57
61
|
|
|
58
|
-
// Create the store
|
|
59
|
-
const storeFactory = createTranslationStore(
|
|
62
|
+
// Create the store factory
|
|
63
|
+
const storeFactory = createTranslationStore({
|
|
60
64
|
translations,
|
|
61
65
|
locales,
|
|
62
|
-
async (locale, translation) => {
|
|
66
|
+
loadModule: async (locale, translation) => {
|
|
63
67
|
// Load translation module dynamically
|
|
64
68
|
const module = await import(`./locales/${locale}/${translation}.json`);
|
|
65
|
-
return module
|
|
69
|
+
return module;
|
|
66
70
|
},
|
|
67
|
-
(module, locale, translation) =>
|
|
68
|
-
|
|
71
|
+
extractTranslation: (module, locale, translation) => {
|
|
72
|
+
// Extract translation data from module
|
|
73
|
+
return module.default || module;
|
|
74
|
+
},
|
|
75
|
+
defaultLocale: 'en',
|
|
76
|
+
useFallback: true,
|
|
77
|
+
fallbackLocale: 'en',
|
|
78
|
+
});
|
|
69
79
|
|
|
70
80
|
// Create typed store
|
|
71
81
|
const store = storeFactory.type<TranslationData>();
|
|
72
82
|
|
|
73
83
|
// Load and use translations
|
|
74
|
-
await store.common.load('en');
|
|
75
|
-
|
|
84
|
+
await store.translations.common.load('en');
|
|
85
|
+
const title = store.translations.common.currentTranslation?.title; // Type-safe access
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### React Usage
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import { createTranslationStore } from 'i18n-typed-store';
|
|
92
|
+
import { I18nTypedStoreProvider, useI18nTranslation, useI18nLocale } from 'i18n-typed-store/react';
|
|
93
|
+
|
|
94
|
+
// Setup store (same as above)
|
|
95
|
+
const storeFactory = createTranslationStore({
|
|
96
|
+
translations: { common: 'common' },
|
|
97
|
+
locales: { en: 'en', ru: 'ru' },
|
|
98
|
+
loadModule: async (locale, translation) => {
|
|
99
|
+
return await import(`./locales/${locale}/${translation}.json`);
|
|
100
|
+
},
|
|
101
|
+
extractTranslation: (module) => module.default,
|
|
102
|
+
defaultLocale: 'en',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
type TranslationData = {
|
|
106
|
+
common: { greeting: string; title: string };
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const store = storeFactory.type<TranslationData>();
|
|
110
|
+
|
|
111
|
+
// In your App component
|
|
112
|
+
function App() {
|
|
113
|
+
return (
|
|
114
|
+
<I18nTypedStoreProvider store={store}>
|
|
115
|
+
<MyComponent />
|
|
116
|
+
</I18nTypedStoreProvider>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// In your components
|
|
121
|
+
function MyComponent() {
|
|
122
|
+
const translations = useI18nTranslation('common');
|
|
123
|
+
const { locale, setLocale } = useI18nLocale();
|
|
124
|
+
|
|
125
|
+
if (!translations) {
|
|
126
|
+
return <div>Loading...</div>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div>
|
|
131
|
+
<h1>{translations.title}</h1>
|
|
132
|
+
<p>{translations.greeting}</p>
|
|
133
|
+
<button onClick={() => setLocale('ru')}>Switch to Russian</button>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### React Suspense Support
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
import { Suspense } from 'react';
|
|
143
|
+
import { useI18nTranslationLazy } from 'i18n-typed-store/react';
|
|
144
|
+
|
|
145
|
+
function MyComponent() {
|
|
146
|
+
// This hook throws a promise if translation is not loaded (for Suspense)
|
|
147
|
+
const translations = useI18nTranslationLazy('common');
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div>
|
|
151
|
+
<h1>{translations.title}</h1>
|
|
152
|
+
<p>{translations.greeting}</p>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function App() {
|
|
158
|
+
return (
|
|
159
|
+
<I18nTypedStoreProvider store={store} suspenseMode="first-load-locale">
|
|
160
|
+
<Suspense fallback={<div>Loading translations...</div>}>
|
|
161
|
+
<MyComponent />
|
|
162
|
+
</Suspense>
|
|
163
|
+
</I18nTypedStoreProvider>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### SSR with Next.js
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// lib/i18n.ts
|
|
172
|
+
import { createTranslationStore } from 'i18n-typed-store';
|
|
173
|
+
import { getLocaleFromRequest, initializeStore } from 'i18n-typed-store/react';
|
|
174
|
+
|
|
175
|
+
const translations = { common: 'common', errors: 'errors' } as const;
|
|
176
|
+
const locales = { en: 'en', ru: 'ru' } as const;
|
|
177
|
+
|
|
178
|
+
export const storeFactory = createTranslationStore({
|
|
179
|
+
translations,
|
|
180
|
+
locales,
|
|
181
|
+
loadModule: async (locale, translation) => {
|
|
182
|
+
return await import(`./locales/${locale}/${translation}.json`);
|
|
183
|
+
},
|
|
184
|
+
extractTranslation: (module) => module.default,
|
|
185
|
+
defaultLocale: 'en',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
type TranslationData = {
|
|
189
|
+
common: { title: string };
|
|
190
|
+
errors: { notFound: string };
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export type Store = ReturnType<typeof storeFactory.type<TranslationData>>;
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// pages/_app.tsx or app/layout.tsx
|
|
198
|
+
import { I18nTypedStoreProvider } from 'i18n-typed-store/react';
|
|
199
|
+
import { storeFactory } from '../lib/i18n';
|
|
200
|
+
|
|
201
|
+
const store = storeFactory.type<TranslationData>();
|
|
202
|
+
|
|
203
|
+
function MyApp({ Component, pageProps }: AppProps) {
|
|
204
|
+
return (
|
|
205
|
+
<I18nTypedStoreProvider store={store}>
|
|
206
|
+
<Component {...pageProps} />
|
|
207
|
+
</I18nTypedStoreProvider>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// pages/index.tsx (getServerSideProps)
|
|
214
|
+
import type { GetServerSidePropsContext } from 'next';
|
|
215
|
+
import { getLocaleFromRequest, initializeStore } from 'i18n-typed-store/react';
|
|
216
|
+
import { storeFactory } from '../lib/i18n';
|
|
217
|
+
|
|
218
|
+
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|
219
|
+
const locale = getLocaleFromRequest(context, {
|
|
220
|
+
defaultLocale: 'en',
|
|
221
|
+
availableLocales: ['en', 'ru'],
|
|
222
|
+
cookieName: 'locale',
|
|
223
|
+
queryParamName: 'locale',
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const store = storeFactory.type<TranslationData>();
|
|
227
|
+
initializeStore(store, locale);
|
|
228
|
+
|
|
229
|
+
// Preload translations if needed
|
|
230
|
+
await store.translations.common.load(locale);
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
props: {
|
|
234
|
+
locale,
|
|
235
|
+
// You can pass translations as props or use context
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// app/page.tsx (App Router)
|
|
243
|
+
import { getLocaleFromRequest, initializeStore } from 'i18n-typed-store/react';
|
|
244
|
+
import { storeFactory } from '../lib/i18n';
|
|
245
|
+
import { headers, cookies } from 'next/headers';
|
|
246
|
+
|
|
247
|
+
export default async function Page() {
|
|
248
|
+
const headersList = await headers();
|
|
249
|
+
const cookieStore = await cookies();
|
|
250
|
+
|
|
251
|
+
const locale = getLocaleFromRequest(
|
|
252
|
+
{
|
|
253
|
+
headers: Object.fromEntries(headersList),
|
|
254
|
+
cookies: Object.fromEntries(cookieStore),
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
defaultLocale: 'en',
|
|
258
|
+
availableLocales: ['en', 'ru'],
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const store = storeFactory.type<TranslationData>();
|
|
263
|
+
initializeStore(store, locale);
|
|
264
|
+
await store.translations.common.load(locale);
|
|
265
|
+
|
|
266
|
+
return <div>...</div>;
|
|
267
|
+
}
|
|
76
268
|
```
|
|
77
269
|
|
|
78
270
|
## Core API
|
|
@@ -82,34 +274,51 @@ console.log(store.common.translation?.title); // Type-safe access
|
|
|
82
274
|
Creates a type-safe translation store with lazy loading support.
|
|
83
275
|
|
|
84
276
|
```typescript
|
|
85
|
-
|
|
86
|
-
translations: T
|
|
87
|
-
locales: L
|
|
88
|
-
loadModule: (locale: keyof L,
|
|
89
|
-
extractTranslation: (module: Module, locale: keyof L,
|
|
90
|
-
|
|
277
|
+
function createTranslationStore<T, L, Module>(options: {
|
|
278
|
+
translations: T;
|
|
279
|
+
locales: L;
|
|
280
|
+
loadModule: (locale: keyof L, namespace: keyof T) => Promise<Module>;
|
|
281
|
+
extractTranslation: (module: Module, locale: keyof L, namespace: keyof T) => unknown | Promise<unknown>;
|
|
282
|
+
defaultLocale: keyof L;
|
|
283
|
+
useFallback?: boolean;
|
|
284
|
+
fallbackLocale?: keyof L;
|
|
285
|
+
deleteOtherLocalesAfterLoad?: boolean;
|
|
286
|
+
loadFromCache?: boolean;
|
|
287
|
+
changeLocaleEventName?: string;
|
|
288
|
+
}): {
|
|
289
|
+
type<M extends { [K in keyof T]: any }>(): TranslationStore<T, L, M>;
|
|
290
|
+
}
|
|
91
291
|
```
|
|
92
292
|
|
|
93
|
-
**
|
|
293
|
+
**Options:**
|
|
94
294
|
|
|
95
295
|
- `translations` - Object with translation keys (e.g., `{ common: 'common', errors: 'errors' }`)
|
|
96
296
|
- `locales` - Object with locale keys (e.g., `{ en: 'en', ru: 'ru' }`)
|
|
97
297
|
- `loadModule` - Async function to load a translation module
|
|
98
|
-
- `extractTranslation` - Function to extract translation data from the loaded module. Receives the module, locale, and
|
|
298
|
+
- `extractTranslation` - Function to extract translation data from the loaded module. Receives the module, locale, and namespace key as parameters
|
|
299
|
+
- `defaultLocale` - Default locale key to use
|
|
300
|
+
- `useFallback` - Whether to use fallback locale for missing translations (default: `false`)
|
|
301
|
+
- `fallbackLocale` - Fallback locale key (default: `defaultLocale`)
|
|
302
|
+
- `deleteOtherLocalesAfterLoad` - Whether to delete translations for other locales after loading (default: `false`)
|
|
303
|
+
- `loadFromCache` - Whether to load translations from cache by default (default: `true`)
|
|
304
|
+
- `changeLocaleEventName` - Event name for locale change events (default: `'change-locale'`)
|
|
99
305
|
|
|
100
306
|
**Returns:** Object with `type<M>()` method that creates a typed store.
|
|
101
307
|
|
|
102
308
|
**Example:**
|
|
103
309
|
|
|
104
310
|
```typescript
|
|
105
|
-
const storeFactory = createTranslationStore(
|
|
106
|
-
{ common: 'common' },
|
|
107
|
-
{ en: 'en', ru: 'ru' },
|
|
108
|
-
async (locale, translation) => {
|
|
311
|
+
const storeFactory = createTranslationStore({
|
|
312
|
+
translations: { common: 'common' },
|
|
313
|
+
locales: { en: 'en', ru: 'ru' },
|
|
314
|
+
loadModule: async (locale, translation) => {
|
|
109
315
|
return await import(`./locales/${locale}/${translation}.json`);
|
|
110
316
|
},
|
|
111
|
-
(module, locale, translation) => module.default
|
|
112
|
-
|
|
317
|
+
extractTranslation: (module, locale, translation) => module.default,
|
|
318
|
+
defaultLocale: 'en',
|
|
319
|
+
useFallback: true,
|
|
320
|
+
fallbackLocale: 'en',
|
|
321
|
+
});
|
|
113
322
|
|
|
114
323
|
type TranslationData = {
|
|
115
324
|
common: { title: string; description: string };
|
|
@@ -118,10 +327,10 @@ type TranslationData = {
|
|
|
118
327
|
const store = storeFactory.type<TranslationData>();
|
|
119
328
|
|
|
120
329
|
// Load translation
|
|
121
|
-
await store.common.load('en');
|
|
330
|
+
await store.translations.common.load('en');
|
|
122
331
|
|
|
123
332
|
// Access translation (type-safe)
|
|
124
|
-
const title = store.common.
|
|
333
|
+
const title = store.translations.common.currentTranslation?.title;
|
|
125
334
|
```
|
|
126
335
|
|
|
127
336
|
### `createTranslationModuleMap`
|
|
@@ -129,11 +338,11 @@ const title = store.common.translation?.title;
|
|
|
129
338
|
Creates a map of translation module loaders for all combinations of translations and locales.
|
|
130
339
|
|
|
131
340
|
```typescript
|
|
132
|
-
|
|
341
|
+
function createTranslationModuleMap<T, L, Module>(
|
|
133
342
|
translations: T,
|
|
134
343
|
locales: L,
|
|
135
344
|
loadModule: (locale: keyof L, translation: keyof T) => Promise<Module>
|
|
136
|
-
)
|
|
345
|
+
): Record<keyof T, Record<keyof L, () => Promise<Module>>>
|
|
137
346
|
```
|
|
138
347
|
|
|
139
348
|
**Example:**
|
|
@@ -157,7 +366,10 @@ const module = await loader();
|
|
|
157
366
|
Creates a plural form selector function for a specific locale using `Intl.PluralRules`.
|
|
158
367
|
|
|
159
368
|
```typescript
|
|
160
|
-
|
|
369
|
+
function createPluralSelector(
|
|
370
|
+
locale: string,
|
|
371
|
+
options?: { strict?: boolean }
|
|
372
|
+
): (count: number, variants: PluralVariants) => string
|
|
161
373
|
```
|
|
162
374
|
|
|
163
375
|
**Example:**
|
|
@@ -194,43 +406,147 @@ selectPlural(2, variants); // => 'яблока'
|
|
|
194
406
|
selectPlural(5, variants); // => 'яблок'
|
|
195
407
|
```
|
|
196
408
|
|
|
409
|
+
## React API
|
|
410
|
+
|
|
411
|
+
### `I18nTypedStoreProvider`
|
|
412
|
+
|
|
413
|
+
Provider component that wraps your application to provide translation store context.
|
|
414
|
+
|
|
415
|
+
```tsx
|
|
416
|
+
<I18nTypedStoreProvider
|
|
417
|
+
store={store}
|
|
418
|
+
suspenseMode="first-load-locale"
|
|
419
|
+
>
|
|
420
|
+
{children}
|
|
421
|
+
</I18nTypedStoreProvider>
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Props:**
|
|
425
|
+
|
|
426
|
+
- `store` - Translation store instance
|
|
427
|
+
- `suspenseMode` - Suspense mode: `'once'` | `'first-load-locale'` | `'change-locale'` (default: `'first-load-locale'`)
|
|
428
|
+
- `children` - React children
|
|
429
|
+
|
|
430
|
+
### `useI18nTranslation`
|
|
431
|
+
|
|
432
|
+
Hook for accessing translations with automatic loading. Returns `undefined` if translation is not yet loaded.
|
|
433
|
+
|
|
434
|
+
```tsx
|
|
435
|
+
const translations = useI18nTranslation('common', fromCache?: boolean);
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### `useI18nTranslationLazy`
|
|
439
|
+
|
|
440
|
+
Hook for accessing translations with React Suspense support. Throws a promise if translation is not loaded.
|
|
441
|
+
|
|
442
|
+
```tsx
|
|
443
|
+
const translations = useI18nTranslationLazy('common', fromCache?: boolean);
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### `useI18nLocale`
|
|
447
|
+
|
|
448
|
+
Hook for accessing and managing the current locale.
|
|
449
|
+
|
|
450
|
+
```tsx
|
|
451
|
+
const { locale, setLocale } = useI18nLocale();
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### `Safe`
|
|
455
|
+
|
|
456
|
+
Component that safely extracts strings from translation objects, catching errors.
|
|
457
|
+
|
|
458
|
+
```tsx
|
|
459
|
+
<Safe
|
|
460
|
+
errorComponent={<span>N/A</span>}
|
|
461
|
+
errorHandler={(error) => console.error(error)}
|
|
462
|
+
>
|
|
463
|
+
{() => translations.common.pages.main.title}
|
|
464
|
+
</Safe>
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## SSR API
|
|
468
|
+
|
|
469
|
+
### `getLocaleFromRequest`
|
|
470
|
+
|
|
471
|
+
Gets locale from SSR request context (query params, cookies, headers).
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
function getLocaleFromRequest<L extends Record<string, string>>(
|
|
475
|
+
context: RequestContext,
|
|
476
|
+
options: {
|
|
477
|
+
defaultLocale: string;
|
|
478
|
+
availableLocales: readonly string[];
|
|
479
|
+
headerName?: string;
|
|
480
|
+
cookieName?: string;
|
|
481
|
+
queryParamName?: string;
|
|
482
|
+
parseAcceptLanguage?: boolean;
|
|
483
|
+
}
|
|
484
|
+
): keyof L
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Example:**
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
const locale = getLocaleFromRequest(context, {
|
|
491
|
+
defaultLocale: 'en',
|
|
492
|
+
availableLocales: ['en', 'ru'],
|
|
493
|
+
cookieName: 'locale',
|
|
494
|
+
queryParamName: 'locale',
|
|
495
|
+
headerName: 'accept-language',
|
|
496
|
+
parseAcceptLanguage: true,
|
|
497
|
+
});
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### `initializeStore`
|
|
501
|
+
|
|
502
|
+
Initializes translation store with a specific locale for SSR.
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
function initializeStore<T, L, M>(
|
|
506
|
+
store: TranslationStore<T, L, M>,
|
|
507
|
+
locale: keyof L
|
|
508
|
+
): void
|
|
509
|
+
```
|
|
510
|
+
|
|
197
511
|
## Advanced Usage
|
|
198
512
|
|
|
199
513
|
### Working with Dynamic Imports
|
|
200
514
|
|
|
201
515
|
```typescript
|
|
202
|
-
const storeFactory = createTranslationStore(
|
|
516
|
+
const storeFactory = createTranslationStore({
|
|
203
517
|
translations,
|
|
204
518
|
locales,
|
|
205
|
-
async (locale, translation) => {
|
|
519
|
+
loadModule: async (locale, translation) => {
|
|
206
520
|
// Dynamic import with error handling
|
|
207
521
|
try {
|
|
208
522
|
const module = await import(
|
|
209
523
|
`./locales/${locale}/${translation}.json`
|
|
210
524
|
);
|
|
211
|
-
return module
|
|
525
|
+
return module;
|
|
212
526
|
} catch (error) {
|
|
213
527
|
console.error(`Failed to load ${translation} for ${locale}`);
|
|
214
528
|
throw error;
|
|
215
529
|
}
|
|
216
530
|
},
|
|
217
|
-
(module, locale, translation) =>
|
|
218
|
-
|
|
531
|
+
extractTranslation: (module, locale, translation) => {
|
|
532
|
+
return module.default || module;
|
|
533
|
+
},
|
|
534
|
+
defaultLocale: 'en',
|
|
535
|
+
});
|
|
219
536
|
```
|
|
220
537
|
|
|
221
538
|
### Custom Module Extraction
|
|
222
539
|
|
|
223
|
-
The `extractTranslation` function receives the module, locale, and
|
|
540
|
+
The `extractTranslation` function receives the module, locale, and namespace key, allowing for advanced extraction logic:
|
|
224
541
|
|
|
225
542
|
```typescript
|
|
226
|
-
const storeFactory = createTranslationStore(
|
|
543
|
+
const storeFactory = createTranslationStore({
|
|
227
544
|
translations,
|
|
228
545
|
locales,
|
|
229
|
-
async (locale, translation) => {
|
|
230
|
-
// Load module that exports default
|
|
546
|
+
loadModule: async (locale, translation) => {
|
|
231
547
|
return await import(`./locales/${locale}/${translation}.ts`);
|
|
232
548
|
},
|
|
233
|
-
(module, locale, translation) => {
|
|
549
|
+
extractTranslation: (module, locale, translation) => {
|
|
234
550
|
// Extract from module.default or module
|
|
235
551
|
// You can use locale and translation parameters for custom logic
|
|
236
552
|
if (locale === 'en' && translation === 'common') {
|
|
@@ -238,8 +554,9 @@ const storeFactory = createTranslationStore(
|
|
|
238
554
|
return module.default?.en || module.default;
|
|
239
555
|
}
|
|
240
556
|
return module.default || module;
|
|
241
|
-
}
|
|
242
|
-
|
|
557
|
+
},
|
|
558
|
+
defaultLocale: 'en',
|
|
559
|
+
});
|
|
243
560
|
```
|
|
244
561
|
|
|
245
562
|
### Handling Multiple Translation Namespaces
|
|
@@ -262,12 +579,34 @@ type TranslationData = {
|
|
|
262
579
|
const store = storeFactory.type<TranslationData>();
|
|
263
580
|
|
|
264
581
|
// Load specific translations
|
|
265
|
-
await store.common.load('en');
|
|
266
|
-
await store.ui.load('en');
|
|
582
|
+
await store.translations.common.load('en');
|
|
583
|
+
await store.translations.ui.load('en');
|
|
267
584
|
|
|
268
585
|
// Access translations
|
|
269
|
-
const title = store.common.
|
|
270
|
-
const saveButton = store.ui.
|
|
586
|
+
const title = store.translations.common.currentTranslation?.title;
|
|
587
|
+
const saveButton = store.translations.ui.currentTranslation?.buttons.save;
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### Using Fallback Locales
|
|
591
|
+
|
|
592
|
+
When `useFallback` is enabled, missing translations are automatically filled from the fallback locale:
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
const storeFactory = createTranslationStore({
|
|
596
|
+
translations: { common: 'common' },
|
|
597
|
+
locales: { en: 'en', ru: 'ru' },
|
|
598
|
+
loadModule: async (locale, translation) => {
|
|
599
|
+
return await import(`./locales/${locale}/${translation}.json`);
|
|
600
|
+
},
|
|
601
|
+
extractTranslation: (module) => module.default,
|
|
602
|
+
defaultLocale: 'en',
|
|
603
|
+
useFallback: true,
|
|
604
|
+
fallbackLocale: 'en',
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// If 'ru' translation is missing some keys, they will be filled from 'en'
|
|
608
|
+
await store.translations.common.load('ru');
|
|
609
|
+
// Result: merged translation with 'en' as fallback
|
|
271
610
|
```
|
|
272
611
|
|
|
273
612
|
## Type Safety
|
|
@@ -276,16 +615,16 @@ The library provides complete type safety:
|
|
|
276
615
|
|
|
277
616
|
```typescript
|
|
278
617
|
// ✅ TypeScript knows all available translation keys
|
|
279
|
-
const title = store.common.
|
|
618
|
+
const title = store.translations.common.currentTranslation?.title;
|
|
280
619
|
|
|
281
620
|
// ❌ TypeScript error: 'invalidKey' doesn't exist
|
|
282
|
-
const invalid = store.common.
|
|
621
|
+
const invalid = store.translations.common.currentTranslation?.invalidKey;
|
|
283
622
|
|
|
284
623
|
// ✅ TypeScript knows all available locales
|
|
285
|
-
await store.common.load('en');
|
|
624
|
+
await store.translations.common.load('en');
|
|
286
625
|
|
|
287
626
|
// ❌ TypeScript error: 'fr' is not a valid locale
|
|
288
|
-
await store.common.load('fr');
|
|
627
|
+
await store.translations.common.load('fr');
|
|
289
628
|
```
|
|
290
629
|
|
|
291
630
|
## Pluralization
|
|
@@ -307,61 +646,6 @@ The library uses `Intl.PluralRules` for plural form selection, supporting all Un
|
|
|
307
646
|
- Arabic (zero/one/two/few/many/other)
|
|
308
647
|
- And many more...
|
|
309
648
|
|
|
310
|
-
## API Reference
|
|
311
|
-
|
|
312
|
-
### `createTranslationStore`
|
|
313
|
-
|
|
314
|
-
```typescript
|
|
315
|
-
function createTranslationStore<
|
|
316
|
-
T extends Record<string, string>,
|
|
317
|
-
L extends Record<string, string>,
|
|
318
|
-
Module = unknown
|
|
319
|
-
>(
|
|
320
|
-
translations: T,
|
|
321
|
-
locales: L,
|
|
322
|
-
loadModule: (locale: keyof L, translation: keyof T) => Promise<Module>,
|
|
323
|
-
extractTranslation: (module: Module, locale: keyof L, translation: keyof T) => unknown
|
|
324
|
-
): {
|
|
325
|
-
type<M extends { [K in keyof T]: Record<string, unknown> }>(): TranslationStore<T, L, M>;
|
|
326
|
-
}
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
### `createTranslationModuleMap`
|
|
330
|
-
|
|
331
|
-
```typescript
|
|
332
|
-
function createTranslationModuleMap<
|
|
333
|
-
T extends Record<string, string>,
|
|
334
|
-
L extends Record<string, string>,
|
|
335
|
-
Module = unknown
|
|
336
|
-
>(
|
|
337
|
-
translations: T,
|
|
338
|
-
locales: L,
|
|
339
|
-
loadModule: (locale: keyof L, translation: keyof T) => Promise<Module>
|
|
340
|
-
): Record<keyof T, Record<keyof L, () => Promise<Module>>>
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
### `createPluralSelector`
|
|
344
|
-
|
|
345
|
-
```typescript
|
|
346
|
-
function createPluralSelector(locale: string): (
|
|
347
|
-
count: number,
|
|
348
|
-
variants: PluralVariants
|
|
349
|
-
) => string
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
### `PluralVariants`
|
|
353
|
-
|
|
354
|
-
```typescript
|
|
355
|
-
type PluralVariants = {
|
|
356
|
-
zero?: string;
|
|
357
|
-
one?: string;
|
|
358
|
-
two?: string;
|
|
359
|
-
few?: string;
|
|
360
|
-
many?: string;
|
|
361
|
-
other?: string;
|
|
362
|
-
};
|
|
363
|
-
```
|
|
364
|
-
|
|
365
649
|
## Examples
|
|
366
650
|
|
|
367
651
|
### Example: E-commerce Application
|
|
@@ -396,24 +680,25 @@ type TranslationData = {
|
|
|
396
680
|
};
|
|
397
681
|
};
|
|
398
682
|
|
|
399
|
-
const storeFactory = createTranslationStore(
|
|
683
|
+
const storeFactory = createTranslationStore({
|
|
400
684
|
translations,
|
|
401
685
|
locales,
|
|
402
|
-
async (locale, translation) => {
|
|
686
|
+
loadModule: async (locale, translation) => {
|
|
403
687
|
return await import(`./locales/${locale}/${translation}.json`);
|
|
404
688
|
},
|
|
405
|
-
(module
|
|
406
|
-
|
|
689
|
+
extractTranslation: (module) => module.default,
|
|
690
|
+
defaultLocale: 'en',
|
|
691
|
+
});
|
|
407
692
|
|
|
408
693
|
const store = storeFactory.type<TranslationData>();
|
|
409
694
|
|
|
410
695
|
// Load translations
|
|
411
|
-
await store.products.load('en');
|
|
412
|
-
await store.cart.load('en');
|
|
696
|
+
await store.translations.products.load('en');
|
|
697
|
+
await store.translations.cart.load('en');
|
|
413
698
|
|
|
414
699
|
// Use translations
|
|
415
|
-
const productTitle = store.products.
|
|
416
|
-
const cartTitle = store.cart.
|
|
700
|
+
const productTitle = store.translations.products.currentTranslation?.title;
|
|
701
|
+
const cartTitle = store.translations.cart.currentTranslation?.title;
|
|
417
702
|
```
|
|
418
703
|
|
|
419
704
|
### Example: Pluralization in Product List
|
|
@@ -435,6 +720,56 @@ getProductCountText(1); // => "1 product"
|
|
|
435
720
|
getProductCountText(5); // => "5 products"
|
|
436
721
|
```
|
|
437
722
|
|
|
723
|
+
## API Reference
|
|
724
|
+
|
|
725
|
+
### `createTranslationStore`
|
|
726
|
+
|
|
727
|
+
```typescript
|
|
728
|
+
function createTranslationStore<
|
|
729
|
+
T extends Record<string, string>,
|
|
730
|
+
L extends Record<string, string>,
|
|
731
|
+
Module = unknown
|
|
732
|
+
>(options: CreateTranslationStoreOptions<T, L, Module>): {
|
|
733
|
+
type<M extends { [K in keyof T]: any }>(): TranslationStore<T, L, M>;
|
|
734
|
+
}
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
### `createTranslationModuleMap`
|
|
738
|
+
|
|
739
|
+
```typescript
|
|
740
|
+
function createTranslationModuleMap<
|
|
741
|
+
T extends Record<string, string>,
|
|
742
|
+
L extends Record<string, string>,
|
|
743
|
+
Module = unknown
|
|
744
|
+
>(
|
|
745
|
+
translations: T,
|
|
746
|
+
locales: L,
|
|
747
|
+
loadModule: (locale: keyof L, translation: keyof T) => Promise<Module>
|
|
748
|
+
): Record<keyof T, Record<keyof L, () => Promise<Module>>>
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
### `createPluralSelector`
|
|
752
|
+
|
|
753
|
+
```typescript
|
|
754
|
+
function createPluralSelector(
|
|
755
|
+
locale: string,
|
|
756
|
+
options?: { strict?: boolean }
|
|
757
|
+
): (count: number, variants: PluralVariants) => string
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
### `PluralVariants`
|
|
761
|
+
|
|
762
|
+
```typescript
|
|
763
|
+
type PluralVariants = {
|
|
764
|
+
zero?: string;
|
|
765
|
+
one?: string;
|
|
766
|
+
two?: string;
|
|
767
|
+
few?: string;
|
|
768
|
+
many?: string;
|
|
769
|
+
other?: string;
|
|
770
|
+
};
|
|
771
|
+
```
|
|
772
|
+
|
|
438
773
|
## Contributing
|
|
439
774
|
|
|
440
775
|
Contributions are welcome! Please feel free to submit a Pull Request.
|