i18n-typed-store 0.1.0 → 0.1.2
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 +786 -166
- 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
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
# i18n-typed-store
|
|
2
2
|
|
|
3
|
-
Type-safe translation store for managing i18n locales with full TypeScript support. A lightweight, zero-dependency library for handling internationalization with compile-time type safety.
|
|
3
|
+
Type-safe translation store for managing i18n locales with full TypeScript support. A lightweight, zero-dependency library for handling internationalization with compile-time type safety. Designed to work with TypeScript classes for translations, providing full IDE support (go-to definition, autocomplete) and support for JSX elements and methods in translations.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- ✅ **Full TypeScript support** - Complete type safety for translations and locales
|
|
8
|
+
- ✅ **IDE integration** - Go-to definition, autocomplete, and refactoring support with translation classes
|
|
8
9
|
- ✅ **Lazy loading** - Load translations only when needed
|
|
9
10
|
- ✅ **Type-safe API** - Compile-time validation of translation keys and locales
|
|
11
|
+
- ✅ **Translation classes/objects** - Use TypeScript classes or objects for translations with JSX support and methods
|
|
10
12
|
- ✅ **Pluralization support** - Built-in plural form selector using `Intl.PluralRules`
|
|
11
13
|
- ✅ **Flexible module loading** - Support for any module format (ESM, CommonJS, dynamic imports)
|
|
12
14
|
- ✅ **Zero runtime dependencies** - Lightweight and framework-agnostic
|
|
15
|
+
- ✅ **React integration** - Hooks and components for React applications
|
|
16
|
+
- ✅ **SSR/SSG support** - Built-in utilities for server-side rendering
|
|
17
|
+
- ✅ **Fallback locales** - Automatic merging with fallback translations
|
|
18
|
+
- ✅ **Caching** - Built-in translation caching for better performance
|
|
13
19
|
|
|
14
20
|
## Installation
|
|
15
21
|
|
|
@@ -25,6 +31,17 @@ yarn add i18n-typed-store
|
|
|
25
31
|
pnpm add i18n-typed-store
|
|
26
32
|
```
|
|
27
33
|
|
|
34
|
+
## Example Project
|
|
35
|
+
|
|
36
|
+
For a complete working example, check out the [React example project](https://github.com/ialexanderlvov/i18n-typed-store-react-example):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/ialexanderlvov/i18n-typed-store-react-example
|
|
40
|
+
cd i18n-typed-store-react-example
|
|
41
|
+
yarn
|
|
42
|
+
yarn dev
|
|
43
|
+
```
|
|
44
|
+
|
|
28
45
|
## Quick Start
|
|
29
46
|
|
|
30
47
|
### Basic Usage
|
|
@@ -32,6 +49,10 @@ pnpm add i18n-typed-store
|
|
|
32
49
|
```typescript
|
|
33
50
|
import { createTranslationStore } from 'i18n-typed-store';
|
|
34
51
|
|
|
52
|
+
// Import translation types for type safety
|
|
53
|
+
import type CommonTranslationsEn from './translations/common/en';
|
|
54
|
+
import type ErrorsTranslationsEn from './translations/errors/en';
|
|
55
|
+
|
|
35
56
|
// Define your translation keys
|
|
36
57
|
const translations = {
|
|
37
58
|
common: 'common',
|
|
@@ -44,35 +65,267 @@ const locales = {
|
|
|
44
65
|
ru: 'ru',
|
|
45
66
|
} as const;
|
|
46
67
|
|
|
47
|
-
// Define your translation data structure
|
|
48
|
-
|
|
49
|
-
common:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
};
|
|
53
|
-
errors: {
|
|
54
|
-
notFound: string;
|
|
55
|
-
};
|
|
56
|
-
};
|
|
68
|
+
// Define your translation data structure using imported types
|
|
69
|
+
interface TranslationData extends Record<keyof typeof translations, any> {
|
|
70
|
+
common: CommonTranslationsEn;
|
|
71
|
+
errors: ErrorsTranslationsEn;
|
|
72
|
+
}
|
|
57
73
|
|
|
58
|
-
// Create the store
|
|
59
|
-
const storeFactory = createTranslationStore(
|
|
74
|
+
// Create the store factory
|
|
75
|
+
const storeFactory = createTranslationStore({
|
|
60
76
|
translations,
|
|
61
77
|
locales,
|
|
62
|
-
async (locale, translation) => {
|
|
63
|
-
// Load translation
|
|
64
|
-
|
|
65
|
-
return module.default;
|
|
78
|
+
loadModule: async (locale, translation) => {
|
|
79
|
+
// Load translation class dynamically
|
|
80
|
+
return await import(`./translations/${translation}/${locale}.ts`);
|
|
66
81
|
},
|
|
67
|
-
(module
|
|
68
|
-
|
|
82
|
+
extractTranslation: (module) => {
|
|
83
|
+
// Instantiate the translation class
|
|
84
|
+
return new module.default();
|
|
85
|
+
},
|
|
86
|
+
defaultLocale: 'en',
|
|
87
|
+
useFallback: true,
|
|
88
|
+
fallbackLocale: 'en',
|
|
89
|
+
});
|
|
69
90
|
|
|
70
91
|
// Create typed store
|
|
71
92
|
const store = storeFactory.type<TranslationData>();
|
|
72
93
|
|
|
73
94
|
// Load and use translations
|
|
74
|
-
await store.common.load('en');
|
|
75
|
-
|
|
95
|
+
await store.translations.common.load('en');
|
|
96
|
+
const title = store.translations.common.currentTranslation?.title; // Type-safe access with IDE go-to support
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### React Usage
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// constants.ts
|
|
103
|
+
export const TRANSLATIONS = {
|
|
104
|
+
common: 'common',
|
|
105
|
+
} as const;
|
|
106
|
+
|
|
107
|
+
export const LOCALES = {
|
|
108
|
+
en: 'en',
|
|
109
|
+
ru: 'ru',
|
|
110
|
+
} as const;
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// store.ts
|
|
115
|
+
import { createTranslationStore } from 'i18n-typed-store';
|
|
116
|
+
import type CommonTranslationsEn from './translations/common/en';
|
|
117
|
+
import { TRANSLATIONS, LOCALES } from './constants';
|
|
118
|
+
|
|
119
|
+
export interface ITranslationStoreTypes extends Record<keyof typeof TRANSLATIONS, any> {
|
|
120
|
+
common: CommonTranslationsEn;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const store = createTranslationStore({
|
|
124
|
+
translations: TRANSLATIONS,
|
|
125
|
+
locales: LOCALES,
|
|
126
|
+
loadModule: async (locale, translation) => {
|
|
127
|
+
return await import(`./translations/${translation}/${locale}.tsx`);
|
|
128
|
+
},
|
|
129
|
+
extractTranslation: (module) => new module.default(),
|
|
130
|
+
defaultLocale: 'en',
|
|
131
|
+
}).type<ITranslationStoreTypes>();
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// hooks/useTranslation.ts
|
|
136
|
+
import { useI18nTranslation } from 'i18n-typed-store/react/useI18nTranslation';
|
|
137
|
+
import type { TRANSLATIONS, LOCALES } from '../constants';
|
|
138
|
+
import type { ITranslationStoreTypes } from '../store';
|
|
139
|
+
|
|
140
|
+
export const useTranslation = <K extends keyof typeof TRANSLATIONS>(translation: K) => {
|
|
141
|
+
return useI18nTranslation<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes, K>(translation);
|
|
142
|
+
};
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
// App.tsx
|
|
147
|
+
import { I18nTypedStoreProvider, useI18nLocale } from 'i18n-typed-store/react';
|
|
148
|
+
import { store } from './store';
|
|
149
|
+
import { useTranslation } from './hooks/useTranslation';
|
|
150
|
+
|
|
151
|
+
function App() {
|
|
152
|
+
return (
|
|
153
|
+
<I18nTypedStoreProvider store={store}>
|
|
154
|
+
<MyComponent />
|
|
155
|
+
</I18nTypedStoreProvider>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
// MyComponent.tsx
|
|
162
|
+
function MyComponent() {
|
|
163
|
+
const translations = useTranslation('common');
|
|
164
|
+
const { locale, setLocale } = useI18nLocale();
|
|
165
|
+
|
|
166
|
+
if (!translations) {
|
|
167
|
+
return <div>Loading...</div>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div>
|
|
172
|
+
<h1>{translations.title}</h1>
|
|
173
|
+
<p>{translations.greeting}</p>
|
|
174
|
+
<button onClick={() => setLocale('ru')}>Switch to Russian</button>
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### React Suspense Support
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// hooks/useTranslationLazy.ts
|
|
184
|
+
import { useI18nTranslationLazy } from 'i18n-typed-store/react/useI18nTranslationLazy';
|
|
185
|
+
import type { TRANSLATIONS, LOCALES } from '../constants';
|
|
186
|
+
import type { ITranslationStoreTypes } from '../store';
|
|
187
|
+
|
|
188
|
+
export const useTranslationLazy = <K extends keyof typeof TRANSLATIONS>(translation: K) => {
|
|
189
|
+
return useI18nTranslationLazy<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes, K>(translation);
|
|
190
|
+
};
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
// MyComponent.tsx
|
|
195
|
+
import { useTranslationLazy } from './hooks/useTranslationLazy';
|
|
196
|
+
|
|
197
|
+
function MyComponent() {
|
|
198
|
+
// This hook throws a promise if translation is not loaded (for Suspense)
|
|
199
|
+
const translations = useTranslationLazy('common');
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div>
|
|
203
|
+
<h1>{translations.title}</h1>
|
|
204
|
+
<p>{translations.greeting}</p>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
// App.tsx
|
|
212
|
+
import { Suspense } from 'react';
|
|
213
|
+
import { I18nTypedStoreProvider } from 'i18n-typed-store/react';
|
|
214
|
+
import { store } from './store';
|
|
215
|
+
import { MyComponent } from './MyComponent';
|
|
216
|
+
|
|
217
|
+
function App() {
|
|
218
|
+
return (
|
|
219
|
+
<I18nTypedStoreProvider store={store} suspenseMode="first-load-locale">
|
|
220
|
+
<Suspense fallback={<div>Loading translations...</div>}>
|
|
221
|
+
<MyComponent />
|
|
222
|
+
</Suspense>
|
|
223
|
+
</I18nTypedStoreProvider>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### SSR with Next.js
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// lib/i18n.ts
|
|
232
|
+
import { createTranslationStore } from 'i18n-typed-store';
|
|
233
|
+
import type CommonTranslationsEn from './translations/common/en';
|
|
234
|
+
import type ErrorsTranslationsEn from './translations/errors/en';
|
|
235
|
+
|
|
236
|
+
const translations = { common: 'common', errors: 'errors' } as const;
|
|
237
|
+
const locales = { en: 'en', ru: 'ru' } as const;
|
|
238
|
+
|
|
239
|
+
export const storeFactory = createTranslationStore({
|
|
240
|
+
translations,
|
|
241
|
+
locales,
|
|
242
|
+
loadModule: async (locale, translation) => {
|
|
243
|
+
return await import(`./translations/${translation}/${locale}.tsx`);
|
|
244
|
+
},
|
|
245
|
+
extractTranslation: (module) => new module.default(),
|
|
246
|
+
defaultLocale: 'en',
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
interface TranslationData extends Record<keyof typeof translations, any> {
|
|
250
|
+
common: CommonTranslationsEn;
|
|
251
|
+
errors: ErrorsTranslationsEn;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export type Store = ReturnType<typeof storeFactory.type<TranslationData>>;
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// pages/_app.tsx or app/layout.tsx
|
|
259
|
+
import { I18nTypedStoreProvider } from 'i18n-typed-store/react';
|
|
260
|
+
import { storeFactory } from '../lib/i18n';
|
|
261
|
+
|
|
262
|
+
const store = storeFactory.type<TranslationData>();
|
|
263
|
+
|
|
264
|
+
function MyApp({ Component, pageProps }: AppProps) {
|
|
265
|
+
return (
|
|
266
|
+
<I18nTypedStoreProvider store={store}>
|
|
267
|
+
<Component {...pageProps} />
|
|
268
|
+
</I18nTypedStoreProvider>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
// pages/index.tsx (getServerSideProps)
|
|
275
|
+
import type { GetServerSidePropsContext } from 'next';
|
|
276
|
+
import { getLocaleFromRequest, initializeStore } from 'i18n-typed-store/react';
|
|
277
|
+
import { storeFactory } from '../lib/i18n';
|
|
278
|
+
|
|
279
|
+
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|
280
|
+
const locale = getLocaleFromRequest(context, {
|
|
281
|
+
defaultLocale: 'en',
|
|
282
|
+
availableLocales: ['en', 'ru'],
|
|
283
|
+
cookieName: 'locale',
|
|
284
|
+
queryParamName: 'locale',
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const store = storeFactory.type<TranslationData>();
|
|
288
|
+
initializeStore(store, locale);
|
|
289
|
+
|
|
290
|
+
// Preload translations if needed
|
|
291
|
+
await store.translations.common.load(locale);
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
props: {
|
|
295
|
+
locale,
|
|
296
|
+
// You can pass translations as props or use context
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// app/page.tsx (App Router)
|
|
304
|
+
import { getLocaleFromRequest, initializeStore } from 'i18n-typed-store/react';
|
|
305
|
+
import { storeFactory } from '../lib/i18n';
|
|
306
|
+
import { headers, cookies } from 'next/headers';
|
|
307
|
+
|
|
308
|
+
export default async function Page() {
|
|
309
|
+
const headersList = await headers();
|
|
310
|
+
const cookieStore = await cookies();
|
|
311
|
+
|
|
312
|
+
const locale = getLocaleFromRequest(
|
|
313
|
+
{
|
|
314
|
+
headers: Object.fromEntries(headersList),
|
|
315
|
+
cookies: Object.fromEntries(cookieStore),
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
defaultLocale: 'en',
|
|
319
|
+
availableLocales: ['en', 'ru'],
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const store = storeFactory.type<TranslationData>();
|
|
324
|
+
initializeStore(store, locale);
|
|
325
|
+
await store.translations.common.load(locale);
|
|
326
|
+
|
|
327
|
+
return <div>...</div>;
|
|
328
|
+
}
|
|
76
329
|
```
|
|
77
330
|
|
|
78
331
|
## Core API
|
|
@@ -82,46 +335,65 @@ console.log(store.common.translation?.title); // Type-safe access
|
|
|
82
335
|
Creates a type-safe translation store with lazy loading support.
|
|
83
336
|
|
|
84
337
|
```typescript
|
|
85
|
-
|
|
86
|
-
translations: T
|
|
87
|
-
locales: L
|
|
88
|
-
loadModule: (locale: keyof L,
|
|
89
|
-
extractTranslation: (module: Module, locale: keyof L,
|
|
90
|
-
|
|
338
|
+
function createTranslationStore<T, L, Module>(options: {
|
|
339
|
+
translations: T;
|
|
340
|
+
locales: L;
|
|
341
|
+
loadModule: (locale: keyof L, namespace: keyof T) => Promise<Module>;
|
|
342
|
+
extractTranslation: (module: Module, locale: keyof L, namespace: keyof T) => unknown | Promise<unknown>;
|
|
343
|
+
defaultLocale: keyof L;
|
|
344
|
+
useFallback?: boolean;
|
|
345
|
+
fallbackLocale?: keyof L;
|
|
346
|
+
deleteOtherLocalesAfterLoad?: boolean;
|
|
347
|
+
loadFromCache?: boolean;
|
|
348
|
+
changeLocaleEventName?: string;
|
|
349
|
+
}): {
|
|
350
|
+
type<M extends { [K in keyof T]: any }>(): TranslationStore<T, L, M>;
|
|
351
|
+
}
|
|
91
352
|
```
|
|
92
353
|
|
|
93
|
-
**
|
|
354
|
+
**Options:**
|
|
94
355
|
|
|
95
356
|
- `translations` - Object with translation keys (e.g., `{ common: 'common', errors: 'errors' }`)
|
|
96
357
|
- `locales` - Object with locale keys (e.g., `{ en: 'en', ru: 'ru' }`)
|
|
97
358
|
- `loadModule` - Async function to load a translation module
|
|
98
|
-
- `extractTranslation` - Function to extract translation data from the loaded module. Receives the module, locale, and
|
|
359
|
+
- `extractTranslation` - Function to extract translation data from the loaded module. Receives the module, locale, and namespace key as parameters
|
|
360
|
+
- `defaultLocale` - Default locale key to use
|
|
361
|
+
- `useFallback` - Whether to use fallback locale for missing translations (default: `false`)
|
|
362
|
+
- `fallbackLocale` - Fallback locale key (default: `defaultLocale`)
|
|
363
|
+
- `deleteOtherLocalesAfterLoad` - Whether to delete translations for other locales after loading (default: `false`)
|
|
364
|
+
- `loadFromCache` - Whether to load translations from cache by default (default: `true`)
|
|
365
|
+
- `changeLocaleEventName` - Event name for locale change events (default: `'change-locale'`)
|
|
99
366
|
|
|
100
367
|
**Returns:** Object with `type<M>()` method that creates a typed store.
|
|
101
368
|
|
|
102
369
|
**Example:**
|
|
103
370
|
|
|
104
371
|
```typescript
|
|
105
|
-
|
|
106
|
-
{ common: 'common' },
|
|
107
|
-
{ en: 'en', ru: 'ru' },
|
|
108
|
-
async (locale, translation) => {
|
|
109
|
-
return await import(`./locales/${locale}/${translation}.json`);
|
|
110
|
-
},
|
|
111
|
-
(module, locale, translation) => module.default
|
|
112
|
-
);
|
|
372
|
+
import type CommonTranslationsEn from './translations/common/en';
|
|
113
373
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
374
|
+
const storeFactory = createTranslationStore({
|
|
375
|
+
translations: { common: 'common' },
|
|
376
|
+
locales: { en: 'en', ru: 'ru' },
|
|
377
|
+
loadModule: async (locale, translation) => {
|
|
378
|
+
return await import(`./translations/${translation}/${locale}.tsx`);
|
|
379
|
+
},
|
|
380
|
+
extractTranslation: (module) => new module.default(),
|
|
381
|
+
defaultLocale: 'en',
|
|
382
|
+
useFallback: true,
|
|
383
|
+
fallbackLocale: 'en',
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
interface TranslationData extends Record<keyof typeof translations, any> {
|
|
387
|
+
common: CommonTranslationsEn;
|
|
388
|
+
}
|
|
117
389
|
|
|
118
390
|
const store = storeFactory.type<TranslationData>();
|
|
119
391
|
|
|
120
392
|
// Load translation
|
|
121
|
-
await store.common.load('en');
|
|
393
|
+
await store.translations.common.load('en');
|
|
122
394
|
|
|
123
|
-
// Access translation (type-safe)
|
|
124
|
-
const title = store.common.
|
|
395
|
+
// Access translation (type-safe with IDE go-to support)
|
|
396
|
+
const title = store.translations.common.currentTranslation?.title;
|
|
125
397
|
```
|
|
126
398
|
|
|
127
399
|
### `createTranslationModuleMap`
|
|
@@ -129,11 +401,11 @@ const title = store.common.translation?.title;
|
|
|
129
401
|
Creates a map of translation module loaders for all combinations of translations and locales.
|
|
130
402
|
|
|
131
403
|
```typescript
|
|
132
|
-
|
|
404
|
+
function createTranslationModuleMap<T, L, Module>(
|
|
133
405
|
translations: T,
|
|
134
406
|
locales: L,
|
|
135
407
|
loadModule: (locale: keyof L, translation: keyof T) => Promise<Module>
|
|
136
|
-
)
|
|
408
|
+
): Record<keyof T, Record<keyof L, () => Promise<Module>>>
|
|
137
409
|
```
|
|
138
410
|
|
|
139
411
|
**Example:**
|
|
@@ -143,7 +415,7 @@ const moduleMap = createTranslationModuleMap(
|
|
|
143
415
|
{ common: 'common' },
|
|
144
416
|
{ en: 'en', ru: 'ru' },
|
|
145
417
|
async (locale, translation) => {
|
|
146
|
-
return await import(`./
|
|
418
|
+
return await import(`./translations/${translation}/${locale}.tsx`);
|
|
147
419
|
}
|
|
148
420
|
);
|
|
149
421
|
|
|
@@ -157,7 +429,10 @@ const module = await loader();
|
|
|
157
429
|
Creates a plural form selector function for a specific locale using `Intl.PluralRules`.
|
|
158
430
|
|
|
159
431
|
```typescript
|
|
160
|
-
|
|
432
|
+
function createPluralSelector(
|
|
433
|
+
locale: string,
|
|
434
|
+
options?: { strict?: boolean }
|
|
435
|
+
): (count: number, variants: PluralVariants) => string
|
|
161
436
|
```
|
|
162
437
|
|
|
163
438
|
**Example:**
|
|
@@ -194,57 +469,360 @@ selectPlural(2, variants); // => 'яблока'
|
|
|
194
469
|
selectPlural(5, variants); // => 'яблок'
|
|
195
470
|
```
|
|
196
471
|
|
|
472
|
+
## React API
|
|
473
|
+
|
|
474
|
+
### `I18nTypedStoreProvider`
|
|
475
|
+
|
|
476
|
+
Provider component that wraps your application to provide translation store context.
|
|
477
|
+
|
|
478
|
+
```tsx
|
|
479
|
+
<I18nTypedStoreProvider
|
|
480
|
+
store={store}
|
|
481
|
+
suspenseMode="first-load-locale"
|
|
482
|
+
>
|
|
483
|
+
{children}
|
|
484
|
+
</I18nTypedStoreProvider>
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Props:**
|
|
488
|
+
|
|
489
|
+
- `store` - Translation store instance
|
|
490
|
+
- `suspenseMode` - Suspense mode: `'once'` | `'first-load-locale'` | `'change-locale'` (default: `'first-load-locale'`)
|
|
491
|
+
- `children` - React children
|
|
492
|
+
|
|
493
|
+
### `useI18nTranslation`
|
|
494
|
+
|
|
495
|
+
Hook for accessing translations with automatic loading. Returns `undefined` if translation is not yet loaded.
|
|
496
|
+
|
|
497
|
+
```tsx
|
|
498
|
+
// Direct usage
|
|
499
|
+
const translations = useI18nTranslation('common', fromCache?: boolean);
|
|
500
|
+
|
|
501
|
+
// Typed wrapper (recommended)
|
|
502
|
+
import { useI18nTranslation } from 'i18n-typed-store/react/useI18nTranslation';
|
|
503
|
+
import type { TRANSLATIONS, LOCALES } from './constants';
|
|
504
|
+
import type { ITranslationStoreTypes } from './store';
|
|
505
|
+
|
|
506
|
+
export const useTranslation = <K extends keyof typeof TRANSLATIONS>(translation: K) => {
|
|
507
|
+
return useI18nTranslation<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes, K>(translation);
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// Usage
|
|
511
|
+
const translations = useTranslation('common');
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### `useI18nTranslationLazy`
|
|
515
|
+
|
|
516
|
+
Hook for accessing translations with React Suspense support. Throws a promise if translation is not loaded.
|
|
517
|
+
|
|
518
|
+
```tsx
|
|
519
|
+
// Direct usage
|
|
520
|
+
const translations = useI18nTranslationLazy('common', fromCache?: boolean);
|
|
521
|
+
|
|
522
|
+
// Typed wrapper (recommended)
|
|
523
|
+
import { useI18nTranslationLazy } from 'i18n-typed-store/react/useI18nTranslationLazy';
|
|
524
|
+
import type { TRANSLATIONS, LOCALES } from './constants';
|
|
525
|
+
import type { ITranslationStoreTypes } from './store';
|
|
526
|
+
|
|
527
|
+
export const useTranslationLazy = <K extends keyof typeof TRANSLATIONS>(translation: K) => {
|
|
528
|
+
return useI18nTranslationLazy<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes, K>(translation);
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
// Usage
|
|
532
|
+
const translations = useTranslationLazy('common');
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### `useI18nLocale`
|
|
536
|
+
|
|
537
|
+
Hook for accessing and managing the current locale.
|
|
538
|
+
|
|
539
|
+
```tsx
|
|
540
|
+
const { locale, setLocale } = useI18nLocale();
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### `Safe`
|
|
544
|
+
|
|
545
|
+
Component that safely extracts strings from translation objects, catching errors.
|
|
546
|
+
|
|
547
|
+
```tsx
|
|
548
|
+
<Safe
|
|
549
|
+
errorComponent={<span>N/A</span>}
|
|
550
|
+
errorHandler={(error) => console.error(error)}
|
|
551
|
+
>
|
|
552
|
+
{() => translations.common.pages.main.title}
|
|
553
|
+
</Safe>
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
## SSR API
|
|
557
|
+
|
|
558
|
+
### `getLocaleFromRequest`
|
|
559
|
+
|
|
560
|
+
Gets locale from SSR request context (query params, cookies, headers).
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
function getLocaleFromRequest<L extends Record<string, string>>(
|
|
564
|
+
context: RequestContext,
|
|
565
|
+
options: {
|
|
566
|
+
defaultLocale: string;
|
|
567
|
+
availableLocales: readonly string[];
|
|
568
|
+
headerName?: string;
|
|
569
|
+
cookieName?: string;
|
|
570
|
+
queryParamName?: string;
|
|
571
|
+
parseAcceptLanguage?: boolean;
|
|
572
|
+
}
|
|
573
|
+
): keyof L
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**Example:**
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
const locale = getLocaleFromRequest(context, {
|
|
580
|
+
defaultLocale: 'en',
|
|
581
|
+
availableLocales: ['en', 'ru'],
|
|
582
|
+
cookieName: 'locale',
|
|
583
|
+
queryParamName: 'locale',
|
|
584
|
+
headerName: 'accept-language',
|
|
585
|
+
parseAcceptLanguage: true,
|
|
586
|
+
});
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### `initializeStore`
|
|
590
|
+
|
|
591
|
+
Initializes translation store with a specific locale for SSR.
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
function initializeStore<T, L, M>(
|
|
595
|
+
store: TranslationStore<T, L, M>,
|
|
596
|
+
locale: keyof L
|
|
597
|
+
): void
|
|
598
|
+
```
|
|
599
|
+
|
|
197
600
|
## Advanced Usage
|
|
198
601
|
|
|
602
|
+
### Translation Classes Structure
|
|
603
|
+
|
|
604
|
+
The library is designed to work with TypeScript classes for translations, providing full type safety and IDE support (go-to definition, autocomplete). Here's an example of a translation class:
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
// translations/common/en.tsx
|
|
608
|
+
import { createPluralSelector } from 'i18n-typed-store';
|
|
609
|
+
|
|
610
|
+
const plur = createPluralSelector('en');
|
|
611
|
+
|
|
612
|
+
export default class CommonTranslationsEn {
|
|
613
|
+
title = 'Welcome';
|
|
614
|
+
loading = 'Loading...';
|
|
615
|
+
error = 'An error occurred';
|
|
616
|
+
|
|
617
|
+
greeting = (
|
|
618
|
+
<>
|
|
619
|
+
Hello, <strong>World</strong>!
|
|
620
|
+
</>
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
buttons = {
|
|
624
|
+
save: 'Save',
|
|
625
|
+
cancel: 'Cancel',
|
|
626
|
+
delete: 'Delete',
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
messages = {
|
|
630
|
+
notFound: 'Not found',
|
|
631
|
+
unauthorized: (
|
|
632
|
+
<>
|
|
633
|
+
You are <strong>not authorized</strong> to perform this action
|
|
634
|
+
</>
|
|
635
|
+
),
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// Pluralization method
|
|
639
|
+
items = (count: number) =>
|
|
640
|
+
count + ' ' + plur(count, {
|
|
641
|
+
one: 'item',
|
|
642
|
+
other: 'items',
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
// lib/i18n.ts
|
|
649
|
+
import { createTranslationStore } from 'i18n-typed-store';
|
|
650
|
+
import type CommonTranslationsEn from './translations/common/en';
|
|
651
|
+
import type MainTranslationsEn from './translations/main/en';
|
|
652
|
+
import type NewsTranslationsEn from './translations/news/en';
|
|
653
|
+
import type SettingsTranslationsEn from './translations/settings/en';
|
|
654
|
+
|
|
655
|
+
const translations = {
|
|
656
|
+
common: 'common',
|
|
657
|
+
main: 'main',
|
|
658
|
+
news: 'news',
|
|
659
|
+
settings: 'settings',
|
|
660
|
+
} as const;
|
|
661
|
+
|
|
662
|
+
const locales = {
|
|
663
|
+
en: 'en',
|
|
664
|
+
ru: 'ru',
|
|
665
|
+
} as const;
|
|
666
|
+
|
|
667
|
+
export interface ITranslationStoreTypes extends Record<keyof typeof translations, any> {
|
|
668
|
+
common: CommonTranslationsEn;
|
|
669
|
+
main: MainTranslationsEn;
|
|
670
|
+
news: NewsTranslationsEn;
|
|
671
|
+
settings: SettingsTranslationsEn;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export const store = createTranslationStore({
|
|
675
|
+
translations,
|
|
676
|
+
locales,
|
|
677
|
+
loadModule: (locale, namespace) => {
|
|
678
|
+
return import(`./translations/${namespace}/${locale}.tsx`);
|
|
679
|
+
},
|
|
680
|
+
extractTranslation: (module) => new module.default(),
|
|
681
|
+
defaultLocale: 'en',
|
|
682
|
+
deleteOtherLocalesAfterLoad: false,
|
|
683
|
+
loadFromCache: false,
|
|
684
|
+
}).type<ITranslationStoreTypes>();
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
**Benefits of using classes:**
|
|
688
|
+
|
|
689
|
+
- ✅ Full TypeScript type safety with IDE go-to definition support
|
|
690
|
+
- ✅ Support for JSX elements in translations
|
|
691
|
+
- ✅ Methods for pluralization and dynamic translations
|
|
692
|
+
- ✅ Better code organization and maintainability
|
|
693
|
+
- ✅ Compile-time validation of translation keys
|
|
694
|
+
|
|
695
|
+
### Creating Typed Hook Wrappers
|
|
696
|
+
|
|
697
|
+
For better type safety and IDE support, it's recommended to create typed wrapper hooks. This ensures full type inference and autocomplete when using translations in your components.
|
|
698
|
+
|
|
699
|
+
```typescript
|
|
700
|
+
// hooks/useTranslation.ts
|
|
701
|
+
import { useI18nTranslation } from 'i18n-typed-store/react/useI18nTranslation';
|
|
702
|
+
import type { TRANSLATIONS, LOCALES } from '../constants';
|
|
703
|
+
import type { ITranslationStoreTypes } from '../store';
|
|
704
|
+
|
|
705
|
+
export const useTranslation = <K extends keyof typeof TRANSLATIONS>(translation: K) => {
|
|
706
|
+
return useI18nTranslation<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes, K>(translation);
|
|
707
|
+
};
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
```typescript
|
|
711
|
+
// hooks/useTranslationLazy.ts
|
|
712
|
+
import { useI18nTranslationLazy } from 'i18n-typed-store/react/useI18nTranslationLazy';
|
|
713
|
+
import type { TRANSLATIONS, LOCALES } from '../constants';
|
|
714
|
+
import type { ITranslationStoreTypes } from '../store';
|
|
715
|
+
|
|
716
|
+
export const useTranslationLazy = <K extends keyof typeof TRANSLATIONS>(translation: K) => {
|
|
717
|
+
return useI18nTranslationLazy<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes, K>(translation);
|
|
718
|
+
};
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
```typescript
|
|
722
|
+
// constants.ts
|
|
723
|
+
export const TRANSLATIONS = {
|
|
724
|
+
common: 'common',
|
|
725
|
+
main: 'main',
|
|
726
|
+
news: 'news',
|
|
727
|
+
settings: 'settings',
|
|
728
|
+
} as const;
|
|
729
|
+
|
|
730
|
+
export const LOCALES = {
|
|
731
|
+
en: 'en',
|
|
732
|
+
ru: 'ru',
|
|
733
|
+
} as const;
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
**Usage in components:**
|
|
737
|
+
|
|
738
|
+
```tsx
|
|
739
|
+
import { useTranslation } from './hooks/useTranslation';
|
|
740
|
+
import { useTranslationLazy } from './hooks/useTranslationLazy';
|
|
741
|
+
|
|
742
|
+
// With useTranslation (returns undefined if not loaded)
|
|
743
|
+
function MyComponent() {
|
|
744
|
+
const translations = useTranslation('common');
|
|
745
|
+
|
|
746
|
+
if (!translations) {
|
|
747
|
+
return <div>Loading...</div>;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return <div>{translations.title}</div>;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// With useTranslationLazy (for Suspense)
|
|
754
|
+
function MyComponentLazy() {
|
|
755
|
+
const translations = useTranslationLazy('common');
|
|
756
|
+
|
|
757
|
+
return <div>{translations.title}</div>;
|
|
758
|
+
}
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
**Benefits:**
|
|
762
|
+
|
|
763
|
+
- ✅ Full type safety with autocomplete
|
|
764
|
+
- ✅ IDE go-to definition support
|
|
765
|
+
- ✅ Compile-time validation of translation keys
|
|
766
|
+
- ✅ Consistent API across your application
|
|
767
|
+
|
|
199
768
|
### Working with Dynamic Imports
|
|
200
769
|
|
|
201
770
|
```typescript
|
|
202
|
-
const storeFactory = createTranslationStore(
|
|
771
|
+
const storeFactory = createTranslationStore({
|
|
203
772
|
translations,
|
|
204
773
|
locales,
|
|
205
|
-
async (locale, translation) => {
|
|
774
|
+
loadModule: async (locale, translation) => {
|
|
206
775
|
// Dynamic import with error handling
|
|
207
776
|
try {
|
|
208
777
|
const module = await import(
|
|
209
|
-
`./
|
|
778
|
+
`./translations/${translation}/${locale}.tsx`
|
|
210
779
|
);
|
|
211
|
-
return module
|
|
780
|
+
return module;
|
|
212
781
|
} catch (error) {
|
|
213
782
|
console.error(`Failed to load ${translation} for ${locale}`);
|
|
214
783
|
throw error;
|
|
215
784
|
}
|
|
216
785
|
},
|
|
217
|
-
(module
|
|
218
|
-
|
|
786
|
+
extractTranslation: (module) => {
|
|
787
|
+
// Instantiate the translation class
|
|
788
|
+
return new module.default();
|
|
789
|
+
},
|
|
790
|
+
defaultLocale: 'en',
|
|
791
|
+
});
|
|
219
792
|
```
|
|
220
793
|
|
|
221
794
|
### Custom Module Extraction
|
|
222
795
|
|
|
223
|
-
The `extractTranslation` function receives the module, locale, and
|
|
796
|
+
The `extractTranslation` function receives the module, locale, and namespace key, allowing for advanced extraction logic:
|
|
224
797
|
|
|
225
798
|
```typescript
|
|
226
|
-
const storeFactory = createTranslationStore(
|
|
799
|
+
const storeFactory = createTranslationStore({
|
|
227
800
|
translations,
|
|
228
801
|
locales,
|
|
229
|
-
async (locale, translation) => {
|
|
230
|
-
//
|
|
231
|
-
|
|
802
|
+
loadModule: async (locale, translation) => {
|
|
803
|
+
// Special handling for certain namespaces
|
|
804
|
+
if (translation === 'lang') {
|
|
805
|
+
return await import(`./translations/${translation}/index.tsx`);
|
|
806
|
+
}
|
|
807
|
+
return await import(`./translations/${translation}/${locale}.tsx`);
|
|
232
808
|
},
|
|
233
|
-
(module
|
|
234
|
-
//
|
|
809
|
+
extractTranslation: (module) => {
|
|
810
|
+
// Instantiate the translation class
|
|
235
811
|
// You can use locale and translation parameters for custom logic
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
return module.default || module;
|
|
241
|
-
}
|
|
242
|
-
);
|
|
812
|
+
return new module.default();
|
|
813
|
+
},
|
|
814
|
+
defaultLocale: 'en',
|
|
815
|
+
});
|
|
243
816
|
```
|
|
244
817
|
|
|
245
818
|
### Handling Multiple Translation Namespaces
|
|
246
819
|
|
|
247
820
|
```typescript
|
|
821
|
+
import type CommonTranslationsEn from './translations/common/en';
|
|
822
|
+
import type ErrorsTranslationsEn from './translations/errors/en';
|
|
823
|
+
import type UiTranslationsEn from './translations/ui/en';
|
|
824
|
+
import type AdminTranslationsEn from './translations/admin/en';
|
|
825
|
+
|
|
248
826
|
const translations = {
|
|
249
827
|
common: 'common',
|
|
250
828
|
errors: 'errors',
|
|
@@ -252,22 +830,44 @@ const translations = {
|
|
|
252
830
|
admin: 'admin',
|
|
253
831
|
} as const;
|
|
254
832
|
|
|
255
|
-
|
|
256
|
-
common:
|
|
257
|
-
errors:
|
|
258
|
-
ui:
|
|
259
|
-
admin:
|
|
260
|
-
}
|
|
833
|
+
interface TranslationData extends Record<keyof typeof translations, any> {
|
|
834
|
+
common: CommonTranslationsEn;
|
|
835
|
+
errors: ErrorsTranslationsEn;
|
|
836
|
+
ui: UiTranslationsEn;
|
|
837
|
+
admin: AdminTranslationsEn;
|
|
838
|
+
}
|
|
261
839
|
|
|
262
840
|
const store = storeFactory.type<TranslationData>();
|
|
263
841
|
|
|
264
842
|
// Load specific translations
|
|
265
|
-
await store.common.load('en');
|
|
266
|
-
await store.ui.load('en');
|
|
843
|
+
await store.translations.common.load('en');
|
|
844
|
+
await store.translations.ui.load('en');
|
|
267
845
|
|
|
268
|
-
// Access translations
|
|
269
|
-
const title = store.common.
|
|
270
|
-
const saveButton = store.ui.
|
|
846
|
+
// Access translations (with full IDE support)
|
|
847
|
+
const title = store.translations.common.currentTranslation?.title;
|
|
848
|
+
const saveButton = store.translations.ui.currentTranslation?.buttons.save;
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
### Using Fallback Locales
|
|
852
|
+
|
|
853
|
+
When `useFallback` is enabled, missing translations are automatically filled from the fallback locale:
|
|
854
|
+
|
|
855
|
+
```typescript
|
|
856
|
+
const storeFactory = createTranslationStore({
|
|
857
|
+
translations: { common: 'common' },
|
|
858
|
+
locales: { en: 'en', ru: 'ru' },
|
|
859
|
+
loadModule: async (locale, translation) => {
|
|
860
|
+
return await import(`./translations/${translation}/${locale}.tsx`);
|
|
861
|
+
},
|
|
862
|
+
extractTranslation: (module) => new module.default(),
|
|
863
|
+
defaultLocale: 'en',
|
|
864
|
+
useFallback: true,
|
|
865
|
+
fallbackLocale: 'en',
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
// If 'ru' translation is missing some keys, they will be filled from 'en'
|
|
869
|
+
await store.translations.common.load('ru');
|
|
870
|
+
// Result: merged translation with 'en' as fallback
|
|
271
871
|
```
|
|
272
872
|
|
|
273
873
|
## Type Safety
|
|
@@ -276,16 +876,16 @@ The library provides complete type safety:
|
|
|
276
876
|
|
|
277
877
|
```typescript
|
|
278
878
|
// ✅ TypeScript knows all available translation keys
|
|
279
|
-
const title = store.common.
|
|
879
|
+
const title = store.translations.common.currentTranslation?.title;
|
|
280
880
|
|
|
281
881
|
// ❌ TypeScript error: 'invalidKey' doesn't exist
|
|
282
|
-
const invalid = store.common.
|
|
882
|
+
const invalid = store.translations.common.currentTranslation?.invalidKey;
|
|
283
883
|
|
|
284
884
|
// ✅ TypeScript knows all available locales
|
|
285
|
-
await store.common.load('en');
|
|
885
|
+
await store.translations.common.load('en');
|
|
286
886
|
|
|
287
887
|
// ❌ TypeScript error: 'fr' is not a valid locale
|
|
288
|
-
await store.common.load('fr');
|
|
888
|
+
await store.translations.common.load('fr');
|
|
289
889
|
```
|
|
290
890
|
|
|
291
891
|
## Pluralization
|
|
@@ -307,6 +907,100 @@ The library uses `Intl.PluralRules` for plural form selection, supporting all Un
|
|
|
307
907
|
- Arabic (zero/one/two/few/many/other)
|
|
308
908
|
- And many more...
|
|
309
909
|
|
|
910
|
+
## Examples
|
|
911
|
+
|
|
912
|
+
### Complete Example Project
|
|
913
|
+
|
|
914
|
+
For a full-featured example with React, TypeScript, and all features demonstrated, see the [example repository](https://github.com/ialexanderlvov/i18n-typed-store-react-example).
|
|
915
|
+
|
|
916
|
+
### Example: E-commerce Application
|
|
917
|
+
|
|
918
|
+
```typescript
|
|
919
|
+
import type ProductsTranslationsEn from './translations/products/en';
|
|
920
|
+
import type CartTranslationsEn from './translations/cart/en';
|
|
921
|
+
import type CheckoutTranslationsEn from './translations/checkout/en';
|
|
922
|
+
|
|
923
|
+
const translations = {
|
|
924
|
+
products: 'products',
|
|
925
|
+
cart: 'cart',
|
|
926
|
+
checkout: 'checkout',
|
|
927
|
+
} as const;
|
|
928
|
+
|
|
929
|
+
const locales = {
|
|
930
|
+
en: 'en',
|
|
931
|
+
ru: 'ru',
|
|
932
|
+
de: 'de',
|
|
933
|
+
} as const;
|
|
934
|
+
|
|
935
|
+
interface TranslationData extends Record<keyof typeof translations, any> {
|
|
936
|
+
products: ProductsTranslationsEn;
|
|
937
|
+
cart: CartTranslationsEn;
|
|
938
|
+
checkout: CheckoutTranslationsEn;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const storeFactory = createTranslationStore({
|
|
942
|
+
translations,
|
|
943
|
+
locales,
|
|
944
|
+
loadModule: async (locale, translation) => {
|
|
945
|
+
return await import(`./translations/${translation}/${locale}.ts`);
|
|
946
|
+
},
|
|
947
|
+
extractTranslation: (module) => new module.default(),
|
|
948
|
+
defaultLocale: 'en',
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
const store = storeFactory.type<TranslationData>();
|
|
952
|
+
|
|
953
|
+
// Load translations
|
|
954
|
+
await store.translations.products.load('en');
|
|
955
|
+
await store.translations.cart.load('en');
|
|
956
|
+
|
|
957
|
+
// Use translations (with full IDE go-to support)
|
|
958
|
+
const productTitle = store.translations.products.currentTranslation?.title;
|
|
959
|
+
const cartTitle = store.translations.cart.currentTranslation?.title;
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
### Example: Pluralization in Product List
|
|
963
|
+
|
|
964
|
+
```typescript
|
|
965
|
+
// translations/products/en.tsx
|
|
966
|
+
import { createPluralSelector } from 'i18n-typed-store';
|
|
967
|
+
|
|
968
|
+
const plur = createPluralSelector('en');
|
|
969
|
+
|
|
970
|
+
export default class ProductsTranslationsEn {
|
|
971
|
+
title = 'Products';
|
|
972
|
+
addToCart = 'Add to Cart';
|
|
973
|
+
price = 'Price';
|
|
974
|
+
|
|
975
|
+
// Pluralization method
|
|
976
|
+
productCount = (count: number) =>
|
|
977
|
+
count + ' ' + plur(count, {
|
|
978
|
+
one: 'product',
|
|
979
|
+
other: 'products',
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
itemsInCart = (count: number) =>
|
|
983
|
+
count + ' ' + plur(count, {
|
|
984
|
+
zero: 'No items',
|
|
985
|
+
one: 'item',
|
|
986
|
+
other: 'items',
|
|
987
|
+
}) + ' in cart';
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Usage in component with typed hook
|
|
991
|
+
import { useTranslation } from './hooks/useTranslation';
|
|
992
|
+
|
|
993
|
+
const translations = useTranslation('products');
|
|
994
|
+
|
|
995
|
+
if (translations) {
|
|
996
|
+
translations.productCount(1); // => "1 product"
|
|
997
|
+
translations.productCount(5); // => "5 products"
|
|
998
|
+
translations.itemsInCart(0); // => "0 No items in cart"
|
|
999
|
+
translations.itemsInCart(1); // => "1 item in cart"
|
|
1000
|
+
translations.itemsInCart(5); // => "5 items in cart"
|
|
1001
|
+
}
|
|
1002
|
+
```
|
|
1003
|
+
|
|
310
1004
|
## API Reference
|
|
311
1005
|
|
|
312
1006
|
### `createTranslationStore`
|
|
@@ -316,13 +1010,8 @@ function createTranslationStore<
|
|
|
316
1010
|
T extends Record<string, string>,
|
|
317
1011
|
L extends Record<string, string>,
|
|
318
1012
|
Module = unknown
|
|
319
|
-
>(
|
|
320
|
-
|
|
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>;
|
|
1013
|
+
>(options: CreateTranslationStoreOptions<T, L, Module>): {
|
|
1014
|
+
type<M extends { [K in keyof T]: any }>(): TranslationStore<T, L, M>;
|
|
326
1015
|
}
|
|
327
1016
|
```
|
|
328
1017
|
|
|
@@ -343,10 +1032,10 @@ function createTranslationModuleMap<
|
|
|
343
1032
|
### `createPluralSelector`
|
|
344
1033
|
|
|
345
1034
|
```typescript
|
|
346
|
-
function createPluralSelector(
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
) => string
|
|
1035
|
+
function createPluralSelector(
|
|
1036
|
+
locale: string,
|
|
1037
|
+
options?: { strict?: boolean }
|
|
1038
|
+
): (count: number, variants: PluralVariants) => string
|
|
350
1039
|
```
|
|
351
1040
|
|
|
352
1041
|
### `PluralVariants`
|
|
@@ -362,79 +1051,6 @@ type PluralVariants = {
|
|
|
362
1051
|
};
|
|
363
1052
|
```
|
|
364
1053
|
|
|
365
|
-
## Examples
|
|
366
|
-
|
|
367
|
-
### Example: E-commerce Application
|
|
368
|
-
|
|
369
|
-
```typescript
|
|
370
|
-
const translations = {
|
|
371
|
-
products: 'products',
|
|
372
|
-
cart: 'cart',
|
|
373
|
-
checkout: 'checkout',
|
|
374
|
-
} as const;
|
|
375
|
-
|
|
376
|
-
const locales = {
|
|
377
|
-
en: 'en',
|
|
378
|
-
ru: 'ru',
|
|
379
|
-
de: 'de',
|
|
380
|
-
} as const;
|
|
381
|
-
|
|
382
|
-
type TranslationData = {
|
|
383
|
-
products: {
|
|
384
|
-
title: string;
|
|
385
|
-
addToCart: string;
|
|
386
|
-
price: string;
|
|
387
|
-
};
|
|
388
|
-
cart: {
|
|
389
|
-
title: string;
|
|
390
|
-
empty: string;
|
|
391
|
-
total: string;
|
|
392
|
-
};
|
|
393
|
-
checkout: {
|
|
394
|
-
title: string;
|
|
395
|
-
placeOrder: string;
|
|
396
|
-
};
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
const storeFactory = createTranslationStore(
|
|
400
|
-
translations,
|
|
401
|
-
locales,
|
|
402
|
-
async (locale, translation) => {
|
|
403
|
-
return await import(`./locales/${locale}/${translation}.json`);
|
|
404
|
-
},
|
|
405
|
-
(module, locale, translation) => module.default
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
const store = storeFactory.type<TranslationData>();
|
|
409
|
-
|
|
410
|
-
// Load translations
|
|
411
|
-
await store.products.load('en');
|
|
412
|
-
await store.cart.load('en');
|
|
413
|
-
|
|
414
|
-
// Use translations
|
|
415
|
-
const productTitle = store.products.translation?.title;
|
|
416
|
-
const cartTitle = store.cart.translation?.title;
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
### Example: Pluralization in Product List
|
|
420
|
-
|
|
421
|
-
```typescript
|
|
422
|
-
import { createPluralSelector } from 'i18n-typed-store';
|
|
423
|
-
|
|
424
|
-
const selectPlural = createPluralSelector('en');
|
|
425
|
-
|
|
426
|
-
function getProductCountText(count: number): string {
|
|
427
|
-
return selectPlural(count, {
|
|
428
|
-
one: `${count} product`,
|
|
429
|
-
other: `${count} products`,
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Usage
|
|
434
|
-
getProductCountText(1); // => "1 product"
|
|
435
|
-
getProductCountText(5); // => "5 products"
|
|
436
|
-
```
|
|
437
|
-
|
|
438
1054
|
## Contributing
|
|
439
1055
|
|
|
440
1056
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
@@ -450,3 +1066,7 @@ Alexander Lvov
|
|
|
450
1066
|
## Repository
|
|
451
1067
|
|
|
452
1068
|
[GitHub](https://github.com/ialexanderlvov/i18n-typed-store)
|
|
1069
|
+
|
|
1070
|
+
## Example Project Git
|
|
1071
|
+
|
|
1072
|
+
[React Example](https://github.com/ialexanderlvov/i18n-typed-store-react-example) - Complete working example with React, TypeScript, and all features demonstrated.
|