@ooneex/translation 0.0.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/LICENSE +21 -0
- package/README.md +657 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +12 -0
- package/dist/ooneex-translation-0.0.1.tgz +0 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ooneex
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
# @ooneex/translation
|
|
2
|
+
|
|
3
|
+
A comprehensive TypeScript/JavaScript library for internationalization (i18n) and localization (l10n). This package provides powerful translation utilities with support for multiple languages, parameter interpolation, pluralization, and nested key structures.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
✅ **Multi-Language Support** - 31 supported locales including Arabic, Chinese, French, Spanish, and more
|
|
15
|
+
|
|
16
|
+
✅ **Type-Safe** - Full TypeScript support with proper type definitions
|
|
17
|
+
|
|
18
|
+
✅ **Parameter Interpolation** - Replace placeholders with dynamic values
|
|
19
|
+
|
|
20
|
+
✅ **Pluralization Support** - Handle singular, plural, and zero forms automatically
|
|
21
|
+
|
|
22
|
+
✅ **Nested Keys** - Support for dot notation in translation keys
|
|
23
|
+
|
|
24
|
+
✅ **Flexible Input** - Accept both dictionary keys and direct translation objects
|
|
25
|
+
|
|
26
|
+
✅ **Fallback System** - Graceful fallbacks to English or base keys
|
|
27
|
+
|
|
28
|
+
✅ **Error Handling** - Custom exceptions for missing translations
|
|
29
|
+
|
|
30
|
+
✅ **Lightweight** - Minimal dependencies and optimized bundle size
|
|
31
|
+
|
|
32
|
+
✅ **Cross-Platform** - Works in Browser, Node.js, Bun, and Deno
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
### Bun
|
|
37
|
+
```bash
|
|
38
|
+
bun add @ooneex/translation
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### pnpm
|
|
42
|
+
```bash
|
|
43
|
+
pnpm add @ooneex/translation
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Yarn
|
|
47
|
+
```bash
|
|
48
|
+
yarn add @ooneex/translation
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### npm
|
|
52
|
+
```bash
|
|
53
|
+
npm install @ooneex/translation
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
### Basic Usage
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { trans } from '@ooneex/translation';
|
|
62
|
+
|
|
63
|
+
// Using dictionary with string keys
|
|
64
|
+
const dictionary = {
|
|
65
|
+
greeting: {
|
|
66
|
+
en: 'Hello, World!',
|
|
67
|
+
fr: 'Bonjour, le monde!',
|
|
68
|
+
es: 'Hola, mundo!'
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Get English translation (default)
|
|
73
|
+
const english = trans('greeting', { dict: dictionary });
|
|
74
|
+
console.log(english); // "Hello, World!"
|
|
75
|
+
|
|
76
|
+
// Get French translation
|
|
77
|
+
const french = trans('greeting', {
|
|
78
|
+
lang: 'fr',
|
|
79
|
+
dict: dictionary
|
|
80
|
+
});
|
|
81
|
+
console.log(french); // "Bonjour, le monde!"
|
|
82
|
+
|
|
83
|
+
// Using direct translation objects
|
|
84
|
+
const directTranslation = {
|
|
85
|
+
en: 'Welcome',
|
|
86
|
+
fr: 'Bienvenue',
|
|
87
|
+
es: 'Bienvenido'
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const welcome = trans(directTranslation, { lang: 'fr' });
|
|
91
|
+
console.log(welcome); // "Bienvenue"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Parameter Interpolation
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { trans } from '@ooneex/translation';
|
|
98
|
+
|
|
99
|
+
const dictionary = {
|
|
100
|
+
welcome: {
|
|
101
|
+
en: 'Welcome, {{ name }}!',
|
|
102
|
+
fr: 'Bienvenue, {{ name }}!',
|
|
103
|
+
es: '¡Bienvenido, {{ name }}!'
|
|
104
|
+
},
|
|
105
|
+
userInfo: {
|
|
106
|
+
en: 'User {{ name }} has {{ count }} points',
|
|
107
|
+
fr: 'L\'utilisateur {{ name }} a {{ count }} points'
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Single parameter
|
|
112
|
+
const greeting = trans('welcome', {
|
|
113
|
+
lang: 'en',
|
|
114
|
+
dict: dictionary,
|
|
115
|
+
params: { name: 'John' }
|
|
116
|
+
});
|
|
117
|
+
console.log(greeting); // "Welcome, John!"
|
|
118
|
+
|
|
119
|
+
// Multiple parameters
|
|
120
|
+
const info = trans('userInfo', {
|
|
121
|
+
lang: 'en',
|
|
122
|
+
dict: dictionary,
|
|
123
|
+
params: { name: 'Alice', count: 150 }
|
|
124
|
+
});
|
|
125
|
+
console.log(info); // "User Alice has 150 points"
|
|
126
|
+
|
|
127
|
+
// Different parameter types
|
|
128
|
+
const stats = trans('stats', {
|
|
129
|
+
dict: {
|
|
130
|
+
stats: {
|
|
131
|
+
en: 'Active: {{ active }}, Score: {{ score }}, ID: {{ id }}'
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
params: {
|
|
135
|
+
active: true,
|
|
136
|
+
score: 95.5,
|
|
137
|
+
id: 12345
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
console.log(stats); // "Active: true, Score: 95.5, ID: 12345"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Pluralization
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { trans } from '@ooneex/translation';
|
|
147
|
+
|
|
148
|
+
const dictionary = {
|
|
149
|
+
item: {
|
|
150
|
+
en: '{{ count }} item',
|
|
151
|
+
fr: '{{ count }} élément'
|
|
152
|
+
},
|
|
153
|
+
item_plural: {
|
|
154
|
+
en: '{{ count }} items',
|
|
155
|
+
fr: '{{ count }} éléments'
|
|
156
|
+
},
|
|
157
|
+
message: {
|
|
158
|
+
en: '{{ count }} message',
|
|
159
|
+
fr: '{{ count }} message'
|
|
160
|
+
},
|
|
161
|
+
message_plural: {
|
|
162
|
+
en: '{{ count }} messages',
|
|
163
|
+
fr: '{{ count }} messages'
|
|
164
|
+
},
|
|
165
|
+
message_zero: {
|
|
166
|
+
en: 'No messages',
|
|
167
|
+
fr: 'Aucun message'
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Singular form (count = 1)
|
|
172
|
+
const singular = trans('item', {
|
|
173
|
+
lang: 'en',
|
|
174
|
+
dict: dictionary,
|
|
175
|
+
count: 1
|
|
176
|
+
});
|
|
177
|
+
console.log(singular); // "1 item"
|
|
178
|
+
|
|
179
|
+
// Plural form (count > 1)
|
|
180
|
+
const plural = trans('item', {
|
|
181
|
+
lang: 'en',
|
|
182
|
+
dict: dictionary,
|
|
183
|
+
count: 5
|
|
184
|
+
});
|
|
185
|
+
console.log(plural); // "5 items"
|
|
186
|
+
|
|
187
|
+
// Zero form (count = 0, when available)
|
|
188
|
+
const zero = trans('message', {
|
|
189
|
+
lang: 'en',
|
|
190
|
+
dict: dictionary,
|
|
191
|
+
count: 0
|
|
192
|
+
});
|
|
193
|
+
console.log(zero); // "No messages"
|
|
194
|
+
|
|
195
|
+
// Zero form fallback to plural (when zero form not available)
|
|
196
|
+
const zeroFallback = trans('item', {
|
|
197
|
+
lang: 'en',
|
|
198
|
+
dict: dictionary,
|
|
199
|
+
count: 0
|
|
200
|
+
});
|
|
201
|
+
console.log(zeroFallback); // "0 items"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Nested Keys
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { trans } from '@ooneex/translation';
|
|
208
|
+
|
|
209
|
+
const dictionary = {
|
|
210
|
+
user: {
|
|
211
|
+
profile: {
|
|
212
|
+
name: {
|
|
213
|
+
en: 'Full Name',
|
|
214
|
+
fr: 'Nom complet',
|
|
215
|
+
es: 'Nombre completo'
|
|
216
|
+
},
|
|
217
|
+
email: {
|
|
218
|
+
en: 'Email Address',
|
|
219
|
+
fr: 'Adresse e-mail'
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
app: {
|
|
224
|
+
navigation: {
|
|
225
|
+
home: {
|
|
226
|
+
en: 'Home',
|
|
227
|
+
fr: 'Accueil'
|
|
228
|
+
},
|
|
229
|
+
about: {
|
|
230
|
+
en: 'About',
|
|
231
|
+
fr: 'À propos'
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Access nested keys with dot notation
|
|
238
|
+
const profileName = trans('user.profile.name', {
|
|
239
|
+
lang: 'fr',
|
|
240
|
+
dict: dictionary
|
|
241
|
+
});
|
|
242
|
+
console.log(profileName); // "Nom complet"
|
|
243
|
+
|
|
244
|
+
const homeNav = trans('app.navigation.home', {
|
|
245
|
+
lang: 'en',
|
|
246
|
+
dict: dictionary
|
|
247
|
+
});
|
|
248
|
+
console.log(homeNav); // "Home"
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Advanced Usage
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { trans, locales, TranslationException } from '@ooneex/translation';
|
|
255
|
+
|
|
256
|
+
// Check supported locales
|
|
257
|
+
console.log(locales); // ['ar', 'bg', 'cs', 'da', 'de', 'el', 'en', ...]
|
|
258
|
+
|
|
259
|
+
// Error handling
|
|
260
|
+
try {
|
|
261
|
+
const result = trans('nonexistent.key', {
|
|
262
|
+
dict: {},
|
|
263
|
+
lang: 'en'
|
|
264
|
+
});
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (error instanceof TranslationException) {
|
|
267
|
+
console.log('Translation not found:', error.message);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Complex real-world example
|
|
272
|
+
const appDictionary = {
|
|
273
|
+
notifications: {
|
|
274
|
+
unread: {
|
|
275
|
+
en: 'You have {{ count }} unread notification',
|
|
276
|
+
fr: 'Vous avez {{ count }} notification non lue'
|
|
277
|
+
},
|
|
278
|
+
unread_plural: {
|
|
279
|
+
en: 'You have {{ count }} unread notifications',
|
|
280
|
+
fr: 'Vous avez {{ count }} notifications non lues'
|
|
281
|
+
},
|
|
282
|
+
unread_zero: {
|
|
283
|
+
en: 'No unread notifications',
|
|
284
|
+
fr: 'Aucune notification non lue'
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
validation: {
|
|
288
|
+
required: {
|
|
289
|
+
en: 'The {{ field }} field is required',
|
|
290
|
+
fr: 'Le champ {{ field }} est requis'
|
|
291
|
+
},
|
|
292
|
+
minLength: {
|
|
293
|
+
en: 'The {{ field }} must be at least {{ min }} characters',
|
|
294
|
+
fr: 'Le {{ field }} doit contenir au moins {{ min }} caractères'
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Notification with pluralization
|
|
300
|
+
const notification = trans('notifications.unread', {
|
|
301
|
+
lang: 'en',
|
|
302
|
+
dict: appDictionary,
|
|
303
|
+
count: 3
|
|
304
|
+
});
|
|
305
|
+
console.log(notification); // "You have 3 unread notifications"
|
|
306
|
+
|
|
307
|
+
// Form validation with parameters
|
|
308
|
+
const validation = trans('validation.minLength', {
|
|
309
|
+
lang: 'fr',
|
|
310
|
+
dict: appDictionary,
|
|
311
|
+
params: { field: 'mot de passe', min: 8 }
|
|
312
|
+
});
|
|
313
|
+
console.log(validation); // "Le mot de passe doit contenir au moins 8 caractères"
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## API Reference
|
|
317
|
+
|
|
318
|
+
### `trans<T extends string>(key, options?): T`
|
|
319
|
+
|
|
320
|
+
The main translation function that handles all translation scenarios.
|
|
321
|
+
|
|
322
|
+
**Parameters:**
|
|
323
|
+
- `key`: `string | Record<LocaleType, string>` - Translation key (dot notation supported) or direct translation object
|
|
324
|
+
- `options?`: `Object` - Translation options
|
|
325
|
+
- `lang?`: `LocaleType` - Target language (defaults to 'en')
|
|
326
|
+
- `params?`: `Record<string, boolean | number | bigint | string>` - Parameters for interpolation
|
|
327
|
+
- `dict?`: `Record<string, unknown>` - Translation dictionary (required when using string keys)
|
|
328
|
+
- `count?`: `number` - Count for pluralization
|
|
329
|
+
|
|
330
|
+
**Returns:** Translated string
|
|
331
|
+
|
|
332
|
+
**Example:**
|
|
333
|
+
```typescript
|
|
334
|
+
// String key with dictionary
|
|
335
|
+
trans('greeting', { lang: 'fr', dict: dictionary });
|
|
336
|
+
|
|
337
|
+
// Direct translation object
|
|
338
|
+
trans({ en: 'Hello', fr: 'Bonjour' }, { lang: 'fr' });
|
|
339
|
+
|
|
340
|
+
// With parameters
|
|
341
|
+
trans('welcome', {
|
|
342
|
+
dict: dictionary,
|
|
343
|
+
params: { name: 'John' }
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// With pluralization
|
|
347
|
+
trans('item', {
|
|
348
|
+
dict: dictionary,
|
|
349
|
+
count: 5
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Types
|
|
354
|
+
|
|
355
|
+
#### `LocaleType`
|
|
356
|
+
TypeScript type representing all supported locale codes.
|
|
357
|
+
|
|
358
|
+
**Supported locales:**
|
|
359
|
+
```typescript
|
|
360
|
+
type LocaleType = 'ar' | 'bg' | 'cs' | 'da' | 'de' | 'el' | 'en' | 'eo' | 'es' | 'et' | 'eu' | 'fi' | 'fr' | 'hu' | 'hy' | 'it' | 'ja' | 'ko' | 'lt' | 'nl' | 'no' | 'pl' | 'pt' | 'ro' | 'ru' | 'sk' | 'sv' | 'th' | 'uk' | 'zh' | 'zh-tw';
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
#### `LocaleInfoType`
|
|
364
|
+
Type for locale information including region data.
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
type LocaleInfoType = {
|
|
368
|
+
code: LocaleType;
|
|
369
|
+
region: string | null;
|
|
370
|
+
};
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Constants
|
|
374
|
+
|
|
375
|
+
#### `locales`
|
|
376
|
+
Array of all supported locale codes.
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
const locales: readonly LocaleType[];
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Exceptions
|
|
383
|
+
|
|
384
|
+
#### `TranslationException`
|
|
385
|
+
Custom exception thrown when translations are not found or invalid.
|
|
386
|
+
|
|
387
|
+
**Properties:**
|
|
388
|
+
- Extends base `Exception` class
|
|
389
|
+
- HTTP status: 404 (Not Found)
|
|
390
|
+
- Includes translation key and context information
|
|
391
|
+
|
|
392
|
+
**Example:**
|
|
393
|
+
```typescript
|
|
394
|
+
try {
|
|
395
|
+
trans('missing.key', { dict: {} });
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (error instanceof TranslationException) {
|
|
398
|
+
console.log(error.message); // "Translation key 'missing.key' not found"
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## Translation Dictionary Structure
|
|
404
|
+
|
|
405
|
+
### Basic Structure
|
|
406
|
+
```typescript
|
|
407
|
+
const dictionary = {
|
|
408
|
+
keyName: {
|
|
409
|
+
en: 'English translation',
|
|
410
|
+
fr: 'French translation',
|
|
411
|
+
es: 'Spanish translation'
|
|
412
|
+
// ... other locales
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Nested Structure
|
|
418
|
+
```typescript
|
|
419
|
+
const dictionary = {
|
|
420
|
+
section: {
|
|
421
|
+
subsection: {
|
|
422
|
+
keyName: {
|
|
423
|
+
en: 'English translation',
|
|
424
|
+
fr: 'French translation'
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Pluralization Structure
|
|
432
|
+
```typescript
|
|
433
|
+
const dictionary = {
|
|
434
|
+
// Singular form (count = 1)
|
|
435
|
+
item: {
|
|
436
|
+
en: '{{ count }} item',
|
|
437
|
+
fr: '{{ count }} élément'
|
|
438
|
+
},
|
|
439
|
+
// Plural form (count != 1, except 0 if zero form exists)
|
|
440
|
+
item_plural: {
|
|
441
|
+
en: '{{ count }} items',
|
|
442
|
+
fr: '{{ count }} éléments'
|
|
443
|
+
},
|
|
444
|
+
// Zero form (count = 0, optional)
|
|
445
|
+
item_zero: {
|
|
446
|
+
en: 'No items',
|
|
447
|
+
fr: 'Aucun élément'
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## Pluralization Rules
|
|
453
|
+
|
|
454
|
+
1. **count = 1**: Uses the base key (singular form)
|
|
455
|
+
2. **count = 0**: Uses `key_zero` if available, otherwise falls back to `key_plural`
|
|
456
|
+
3. **count > 1 or count < 0**: Uses `key_plural`
|
|
457
|
+
4. **Fallback**: If plural forms don't exist, falls back to singular form
|
|
458
|
+
|
|
459
|
+
## Fallback Strategy
|
|
460
|
+
|
|
461
|
+
1. **Language fallback**: If requested language not available, falls back to English ('en')
|
|
462
|
+
2. **Key fallback**: If translation key not found, returns the key itself
|
|
463
|
+
3. **Pluralization fallback**: If plural forms don't exist, uses singular form
|
|
464
|
+
4. **Empty translation**: Throws `TranslationException` for empty translations
|
|
465
|
+
|
|
466
|
+
## Best Practices
|
|
467
|
+
|
|
468
|
+
### 1. Organize Dictionary Structure
|
|
469
|
+
```typescript
|
|
470
|
+
// Good: Organized by feature/section
|
|
471
|
+
const dictionary = {
|
|
472
|
+
auth: {
|
|
473
|
+
login: { en: 'Log In', fr: 'Se connecter' },
|
|
474
|
+
logout: { en: 'Log Out', fr: 'Se déconnecter' }
|
|
475
|
+
},
|
|
476
|
+
navigation: {
|
|
477
|
+
home: { en: 'Home', fr: 'Accueil' },
|
|
478
|
+
about: { en: 'About', fr: 'À propos' }
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### 2. Use Consistent Parameter Names
|
|
484
|
+
```typescript
|
|
485
|
+
// Good: Consistent naming
|
|
486
|
+
const dictionary = {
|
|
487
|
+
welcome: { en: 'Welcome, {{ username }}!' },
|
|
488
|
+
goodbye: { en: 'Goodbye, {{ username }}!' }
|
|
489
|
+
};
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### 3. Handle Pluralization Properly
|
|
493
|
+
```typescript
|
|
494
|
+
// Good: Complete pluralization support
|
|
495
|
+
const dictionary = {
|
|
496
|
+
notification: { en: '{{ count }} notification' },
|
|
497
|
+
notification_plural: { en: '{{ count }} notifications' },
|
|
498
|
+
notification_zero: { en: 'No notifications' }
|
|
499
|
+
};
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### 4. Provide Fallbacks
|
|
503
|
+
```typescript
|
|
504
|
+
// Good: Always include English translations
|
|
505
|
+
const dictionary = {
|
|
506
|
+
greeting: {
|
|
507
|
+
en: 'Hello',
|
|
508
|
+
fr: 'Bonjour',
|
|
509
|
+
es: 'Hola'
|
|
510
|
+
// en is always available as fallback
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## Error Handling
|
|
516
|
+
|
|
517
|
+
The package throws `TranslationException` in the following cases:
|
|
518
|
+
|
|
519
|
+
- Translation key not found in dictionary
|
|
520
|
+
- Nested key path doesn't exist
|
|
521
|
+
- Translation value is empty
|
|
522
|
+
- String key used without dictionary
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
try {
|
|
526
|
+
const result = trans('missing.key', { dict: dictionary });
|
|
527
|
+
} catch (error) {
|
|
528
|
+
if (error instanceof TranslationException) {
|
|
529
|
+
// Handle translation error
|
|
530
|
+
console.warn('Translation missing:', error.message);
|
|
531
|
+
// Provide fallback or default value
|
|
532
|
+
return 'Fallback text';
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
## Performance Considerations
|
|
538
|
+
|
|
539
|
+
- **Dictionary Caching**: Cache translation dictionaries to avoid repeated parsing
|
|
540
|
+
- **Lazy Loading**: Load only required translations for better performance
|
|
541
|
+
- **Nested Key Optimization**: Avoid deeply nested structures when possible
|
|
542
|
+
- **Parameter Validation**: Validate parameters before passing to avoid runtime errors
|
|
543
|
+
|
|
544
|
+
## Real-world Examples
|
|
545
|
+
|
|
546
|
+
### E-commerce Application
|
|
547
|
+
```typescript
|
|
548
|
+
const ecommerceDict = {
|
|
549
|
+
product: {
|
|
550
|
+
price: {
|
|
551
|
+
en: 'Price: ${{ amount }}',
|
|
552
|
+
fr: 'Prix : {{ amount }} $'
|
|
553
|
+
},
|
|
554
|
+
availability: {
|
|
555
|
+
en: '{{ count }} in stock',
|
|
556
|
+
fr: '{{ count }} en stock'
|
|
557
|
+
},
|
|
558
|
+
availability_zero: {
|
|
559
|
+
en: 'Out of stock',
|
|
560
|
+
fr: 'Rupture de stock'
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
cart: {
|
|
564
|
+
item: {
|
|
565
|
+
en: '{{ count }} item',
|
|
566
|
+
fr: '{{ count }} article'
|
|
567
|
+
},
|
|
568
|
+
item_plural: {
|
|
569
|
+
en: '{{ count }} items',
|
|
570
|
+
fr: '{{ count }} articles'
|
|
571
|
+
},
|
|
572
|
+
total: {
|
|
573
|
+
en: 'Total: ${{ amount }}',
|
|
574
|
+
fr: 'Total : {{ amount }} $'
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// Usage
|
|
580
|
+
const price = trans('product.price', {
|
|
581
|
+
dict: ecommerceDict,
|
|
582
|
+
lang: 'en',
|
|
583
|
+
params: { amount: '29.99' }
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
const cartItems = trans('cart.item', {
|
|
587
|
+
dict: ecommerceDict,
|
|
588
|
+
lang: 'fr',
|
|
589
|
+
count: 3
|
|
590
|
+
});
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### User Dashboard
|
|
594
|
+
```typescript
|
|
595
|
+
const dashboardDict = {
|
|
596
|
+
dashboard: {
|
|
597
|
+
welcome: {
|
|
598
|
+
en: 'Welcome back, {{ username }}!',
|
|
599
|
+
fr: 'Bon retour, {{ username }} !'
|
|
600
|
+
},
|
|
601
|
+
stats: {
|
|
602
|
+
unread: {
|
|
603
|
+
en: 'You have {{ count }} unread message',
|
|
604
|
+
fr: 'Vous avez {{ count }} message non lu'
|
|
605
|
+
},
|
|
606
|
+
unread_plural: {
|
|
607
|
+
en: 'You have {{ count }} unread messages',
|
|
608
|
+
fr: 'Vous avez {{ count }} messages non lus'
|
|
609
|
+
},
|
|
610
|
+
unread_zero: {
|
|
611
|
+
en: 'No unread messages',
|
|
612
|
+
fr: 'Aucun message non lu'
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
// Usage
|
|
619
|
+
const welcome = trans('dashboard.welcome', {
|
|
620
|
+
dict: dashboardDict,
|
|
621
|
+
lang: 'fr',
|
|
622
|
+
params: { username: 'Marie' }
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
const messages = trans('dashboard.stats.unread', {
|
|
626
|
+
dict: dashboardDict,
|
|
627
|
+
lang: 'en',
|
|
628
|
+
count: 0
|
|
629
|
+
});
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
## License
|
|
633
|
+
|
|
634
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
635
|
+
|
|
636
|
+
## Contributing
|
|
637
|
+
|
|
638
|
+
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
|
|
639
|
+
|
|
640
|
+
### Development Setup
|
|
641
|
+
|
|
642
|
+
1. Clone the repository
|
|
643
|
+
2. Install dependencies: `bun install`
|
|
644
|
+
3. Run tests: `bun run test`
|
|
645
|
+
4. Build the project: `bun run build`
|
|
646
|
+
|
|
647
|
+
### Guidelines
|
|
648
|
+
|
|
649
|
+
- Write tests for new features
|
|
650
|
+
- Follow the existing code style
|
|
651
|
+
- Update documentation for API changes
|
|
652
|
+
- Ensure all tests pass before submitting PR
|
|
653
|
+
- Add new locales to the supported list when needed
|
|
654
|
+
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
Made with ❤️ by the Ooneex team
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
declare const locales: readonly ["ar", "bg", "cs", "da", "de", "el", "en", "eo", "es", "et", "eu", "fi", "fr", "hu", "hy", "it", "ja", "ko", "lt", "nl", "no", "pl", "pt", "ro", "ru", "sk", "sv", "th", "uk", "zh", "zh-tw"];
|
|
2
|
+
import { Exception } from "@ooneex/exception";
|
|
3
|
+
declare class TranslationException extends Exception {
|
|
4
|
+
constructor(message: string, data?: Record<string, unknown>);
|
|
5
|
+
}
|
|
6
|
+
type LocaleType = (typeof locales)[number];
|
|
7
|
+
type LocaleInfoType = {
|
|
8
|
+
code: LocaleType;
|
|
9
|
+
region: string | null;
|
|
10
|
+
};
|
|
11
|
+
declare const trans: <T extends string>(key: string | Record<LocaleType, string>, options?: {
|
|
12
|
+
lang?: LocaleType;
|
|
13
|
+
params?: Record<string, boolean | number | bigint | string>;
|
|
14
|
+
dict?: Record<string, unknown>;
|
|
15
|
+
count?: number;
|
|
16
|
+
}) => T;
|
|
17
|
+
export { trans, locales, TranslationException, LocaleType, LocaleInfoType };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
var q=["ar","bg","cs","da","de","el","en","eo","es","et","eu","fi","fr","hu","hy","it","ja","ko","lt","nl","no","pl","pt","ro","ru","sk","sv","th","uk","zh","zh-tw"];import{Exception as I}from"@ooneex/exception";import{HttpStatus as S}from"@ooneex/http-status";class C extends I{constructor(o,h={}){super(o,{status:S.Code.NotFound,data:h});this.name="TranslationException"}}var H=(o,h)=>h.split(".").reduce((p,L)=>p&&typeof p==="object"&&p!==null&&(L in p)?p[L]:void 0,o),J=(o,h)=>{let{lang:p="en",params:L,dict:F,count:w}=h||{},m="";if(typeof o==="string"&&F){let f=o;if(w!==void 0)if(w===0){let s=`${o}_zero`;if(H(F,s))f=s;else f=`${o}_plural`}else if(w===1)f=o;else f=`${o}_plural`;let j=H(F,f);if(j)m=j[p]||j.en||o;else if(f!==o){let s=H(F,o);if(s)m=s[p]||s.en||o;else throw new C(`Translation key "${o}" not found`)}else throw new C(`Translation key "${o}" not found`)}else if(typeof o==="object")m=o[p]||o.en;if(!m)throw new C("Translation value is empty");if(L)for(let[f,j]of Object.entries(L))m=m.replace(`{{ ${f} }}`,j.toString());if(w!==void 0)m=m.replace(/\{\{ count \}\}/g,w.toString());return m};export{J as trans,q as locales,C as TranslationException};
|
|
2
|
+
|
|
3
|
+
//# debugId=9CE459E1BD46321164756E2164756E21
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["src/locales.ts", "src/TranslationException.ts", "src/trans.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"export const locales = [\n \"ar\",\n \"bg\",\n \"cs\",\n \"da\",\n \"de\",\n \"el\",\n \"en\",\n \"eo\",\n \"es\",\n \"et\",\n \"eu\",\n \"fi\",\n \"fr\",\n \"hu\",\n \"hy\",\n \"it\",\n \"ja\",\n \"ko\",\n \"lt\",\n \"nl\",\n \"no\",\n \"pl\",\n \"pt\",\n \"ro\",\n \"ru\",\n \"sk\",\n \"sv\",\n \"th\",\n \"uk\",\n \"zh\",\n \"zh-tw\",\n] as const;\n",
|
|
6
|
+
"import { Exception } from \"@ooneex/exception\";\nimport { HttpStatus } from \"@ooneex/http-status\";\n\nexport class TranslationException extends Exception {\n constructor(message: string, data: Record<string, unknown> = {}) {\n super(message, {\n status: HttpStatus.Code.NotFound,\n data,\n });\n this.name = \"TranslationException\";\n }\n}\n",
|
|
7
|
+
"import { TranslationException } from \"./TranslationException\";\nimport type { LocaleType } from \"./types\";\n\nconst getNestedValue = (dict: unknown, path: string): unknown =>\n path\n .split(\".\")\n .reduce(\n (current, key) =>\n current && typeof current === \"object\" && current !== null && key in current\n ? (current as Record<string, unknown>)[key]\n : undefined,\n dict,\n );\n\nexport const trans = <T extends string>(\n key: string | Record<LocaleType, string>,\n options?: {\n lang?: LocaleType;\n params?: Record<string, boolean | number | bigint | string>;\n dict?: Record<string, unknown>;\n count?: number;\n },\n): T => {\n const { lang = \"en\", params, dict, count } = options || {};\n let text = \"\";\n\n if (typeof key === \"string\" && dict) {\n let translationKey = key;\n\n // Handle pluralization\n if (count !== undefined) {\n if (count === 0) {\n // Try zero form first, fallback to plural\n const zeroKey = `${key}_zero`;\n const zeroEntry = getNestedValue(dict, zeroKey) as Record<LocaleType, string>;\n if (zeroEntry) {\n translationKey = zeroKey;\n } else {\n translationKey = `${key}_plural`;\n }\n } else if (count === 1) {\n // Use singular form (original key)\n translationKey = key;\n } else {\n // Use plural form\n translationKey = `${key}_plural`;\n }\n }\n\n const translationEntry = getNestedValue(dict, translationKey) as Record<LocaleType, string>;\n\n if (translationEntry) {\n text = translationEntry[lang as keyof typeof translationEntry] || translationEntry.en || key;\n } else {\n // Fallback to original key if plural form not found\n if (translationKey !== key) {\n const fallbackEntry = getNestedValue(dict, key) as Record<LocaleType, string>;\n if (fallbackEntry) {\n text = fallbackEntry[lang as keyof typeof fallbackEntry] || fallbackEntry.en || key;\n } else {\n throw new TranslationException(`Translation key \"${key}\" not found`);\n }\n } else {\n throw new TranslationException(`Translation key \"${key}\" not found`);\n }\n }\n } else if (typeof key === \"object\") {\n text = key[lang as keyof typeof key] || key.en;\n }\n\n if (!text) {\n throw new TranslationException(\"Translation value is empty\");\n }\n\n // Replace parameters\n if (params) {\n for (const [paramKey, value] of Object.entries(params)) {\n text = (text as string).replace(`{{ ${paramKey} }}`, value.toString());\n }\n }\n\n // Replace count parameter if provided\n if (count !== undefined) {\n text = (text as string).replace(/\\{\\{ count \\}\\}/g, count.toString());\n }\n\n return text as T;\n};\n"
|
|
8
|
+
],
|
|
9
|
+
"mappings": "AAAO,IAAM,EAAU,CACrB,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,OACF,EChCA,oBAAS,0BACT,qBAAS,4BAEF,MAAM,UAA6B,CAAU,CAClD,WAAW,CAAC,EAAiB,EAAgC,CAAC,EAAG,CAC/D,MAAM,EAAS,CACb,OAAQ,EAAW,KAAK,SACxB,MACF,CAAC,EACD,KAAK,KAAO,uBAEhB,CCRA,IAAM,EAAiB,CAAC,EAAe,IACrC,EACG,MAAM,GAAG,EACT,OACC,CAAC,EAAS,IACR,GAAW,OAAO,IAAY,UAAY,IAAY,OAAQ,KAAO,GAChE,EAAoC,GACrC,OACN,CACF,EAES,EAAQ,CACnB,EACA,IAMM,CACN,IAAQ,OAAO,KAAM,SAAQ,OAAM,SAAU,GAAW,CAAC,EACrD,EAAO,GAEX,GAAI,OAAO,IAAQ,UAAY,EAAM,CACnC,IAAI,EAAiB,EAGrB,GAAI,IAAU,OACZ,GAAI,IAAU,EAAG,CAEf,IAAM,EAAU,GAAG,SAEnB,GADkB,EAAe,EAAM,CAAO,EAE5C,EAAiB,EAEjB,OAAiB,GAAG,WAEjB,QAAI,IAAU,EAEnB,EAAiB,EAGjB,OAAiB,GAAG,WAIxB,IAAM,EAAmB,EAAe,EAAM,CAAc,EAE5D,GAAI,EACF,EAAO,EAAiB,IAA0C,EAAiB,IAAM,EAGzF,QAAI,IAAmB,EAAK,CAC1B,IAAM,EAAgB,EAAe,EAAM,CAAG,EAC9C,GAAI,EACF,EAAO,EAAc,IAAuC,EAAc,IAAM,EAEhF,WAAM,IAAI,EAAqB,oBAAoB,cAAgB,EAGrE,WAAM,IAAI,EAAqB,oBAAoB,cAAgB,EAGlE,QAAI,OAAO,IAAQ,SACxB,EAAO,EAAI,IAA6B,EAAI,GAG9C,GAAI,CAAC,EACH,MAAM,IAAI,EAAqB,4BAA4B,EAI7D,GAAI,EACF,QAAY,EAAU,KAAU,OAAO,QAAQ,CAAM,EACnD,EAAQ,EAAgB,QAAQ,MAAM,OAAe,EAAM,SAAS,CAAC,EAKzE,GAAI,IAAU,OACZ,EAAQ,EAAgB,QAAQ,mBAAoB,EAAM,SAAS,CAAC,EAGtE,OAAO",
|
|
10
|
+
"debugId": "9CE459E1BD46321164756E2164756E21",
|
|
11
|
+
"names": []
|
|
12
|
+
}
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ooneex/translation",
|
|
3
|
+
"description": "",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"LICENSE",
|
|
9
|
+
"README.md",
|
|
10
|
+
"package.json"
|
|
11
|
+
],
|
|
12
|
+
"module": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "bun test tests",
|
|
26
|
+
"build": "bunup",
|
|
27
|
+
"lint": "tsgo --noEmit && bunx biome lint",
|
|
28
|
+
"publish:prod": "bun publish --tolerate-republish --access public",
|
|
29
|
+
"publish:pack": "bun pm pack --destination ./dist",
|
|
30
|
+
"publish:dry": "bun publish --dry-run"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@ooneex/exception": "0.0.1",
|
|
34
|
+
"@ooneex/http-status": "0.0.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {}
|
|
37
|
+
}
|