@objectstack/service-i18n 4.0.3 → 4.0.4
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/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +8 -0
- package/README.md +377 -0
- package/package.json +3 -3
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/service-i18n@4.0.
|
|
2
|
+
> @objectstack/service-i18n@4.0.4 build /home/runner/work/framework/framework/packages/services/service-i18n
|
|
3
3
|
> tsup --config ../../../tsup.config.ts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
13
|
[32mCJS[39m [1mdist/index.cjs [22m[32m8.50 KB[39m
|
|
14
14
|
[32mCJS[39m [1mdist/index.cjs.map [22m[32m18.42 KB[39m
|
|
15
|
-
[32mCJS[39m ⚡️ Build success in
|
|
15
|
+
[32mCJS[39m ⚡️ Build success in 123ms
|
|
16
16
|
[32mESM[39m [1mdist/index.js [22m[32m6.84 KB[39m
|
|
17
17
|
[32mESM[39m [1mdist/index.js.map [22m[32m18.02 KB[39m
|
|
18
|
-
[32mESM[39m ⚡️ Build success in
|
|
18
|
+
[32mESM[39m ⚡️ Build success in 130ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 16111ms
|
|
21
21
|
[32mDTS[39m [1mdist/index.d.ts [22m[32m4.41 KB[39m
|
|
22
22
|
[32mDTS[39m [1mdist/index.d.cts [22m[32m4.41 KB[39m
|
package/CHANGELOG.md
CHANGED
package/README.md
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# @objectstack/service-i18n
|
|
2
|
+
|
|
3
|
+
I18n Service for ObjectStack — implements `II18nService` with file-based locale loading and translation management.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multi-Language Support**: Manage translations for unlimited languages
|
|
8
|
+
- **File-Based Locales**: Load translations from JSON/YAML files
|
|
9
|
+
- **Namespace Support**: Organize translations by domain (e.g., `common`, `errors`, `ui`)
|
|
10
|
+
- **Interpolation**: Dynamic variable replacement in translations
|
|
11
|
+
- **Pluralization**: Language-specific plural rules
|
|
12
|
+
- **Fallback Chain**: Graceful fallback from dialect → base language → default
|
|
13
|
+
- **Type-Safe**: TypeScript support with type-safe translation keys
|
|
14
|
+
- **Hot Reload**: Reload translations without restarting (development)
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add @objectstack/service-i18n
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Basic Usage
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { defineStack } from '@objectstack/spec';
|
|
26
|
+
import { ServiceI18n } from '@objectstack/service-i18n';
|
|
27
|
+
|
|
28
|
+
const stack = defineStack({
|
|
29
|
+
services: [
|
|
30
|
+
ServiceI18n.configure({
|
|
31
|
+
defaultLocale: 'en-US',
|
|
32
|
+
supportedLocales: ['en-US', 'es-ES', 'fr-FR', 'de-DE'],
|
|
33
|
+
loadPath: './locales/{{lng}}/{{ns}}.json',
|
|
34
|
+
}),
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
interface I18nServiceConfig {
|
|
43
|
+
/** Default locale (e.g., 'en-US') */
|
|
44
|
+
defaultLocale: string;
|
|
45
|
+
|
|
46
|
+
/** List of supported locales */
|
|
47
|
+
supportedLocales: string[];
|
|
48
|
+
|
|
49
|
+
/** Path template for locale files */
|
|
50
|
+
loadPath: string;
|
|
51
|
+
|
|
52
|
+
/** Fallback locale when translation is missing */
|
|
53
|
+
fallbackLocale?: string;
|
|
54
|
+
|
|
55
|
+
/** Enable hot reload in development */
|
|
56
|
+
hotReload?: boolean;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Directory Structure
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
locales/
|
|
64
|
+
├── en-US/
|
|
65
|
+
│ ├── common.json
|
|
66
|
+
│ ├── errors.json
|
|
67
|
+
│ └── ui.json
|
|
68
|
+
├── es-ES/
|
|
69
|
+
│ ├── common.json
|
|
70
|
+
│ ├── errors.json
|
|
71
|
+
│ └── ui.json
|
|
72
|
+
└── fr-FR/
|
|
73
|
+
├── common.json
|
|
74
|
+
├── errors.json
|
|
75
|
+
└── ui.json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Example `locales/en-US/common.json`:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"welcome": "Welcome to ObjectStack",
|
|
83
|
+
"greeting": "Hello, {{name}}!",
|
|
84
|
+
"item_count": "You have {{count}} item",
|
|
85
|
+
"item_count_plural": "You have {{count}} items",
|
|
86
|
+
"save_button": "Save",
|
|
87
|
+
"cancel_button": "Cancel"
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Service API
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// Get i18n service
|
|
95
|
+
const i18n = kernel.getService<II18nService>('i18n');
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Basic Translation
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// Simple translation
|
|
102
|
+
const text = await i18n.t('common:welcome');
|
|
103
|
+
// "Welcome to ObjectStack"
|
|
104
|
+
|
|
105
|
+
// With interpolation
|
|
106
|
+
const greeting = await i18n.t('common:greeting', { name: 'Alice' });
|
|
107
|
+
// "Hello, Alice!"
|
|
108
|
+
|
|
109
|
+
// With pluralization
|
|
110
|
+
const count1 = await i18n.t('common:item_count', { count: 1 });
|
|
111
|
+
// "You have 1 item"
|
|
112
|
+
|
|
113
|
+
const count5 = await i18n.t('common:item_count', { count: 5 });
|
|
114
|
+
// "You have 5 items"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Change Locale
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// Set locale for current context
|
|
121
|
+
await i18n.setLocale('es-ES');
|
|
122
|
+
|
|
123
|
+
// Get current locale
|
|
124
|
+
const locale = i18n.getLocale();
|
|
125
|
+
// "es-ES"
|
|
126
|
+
|
|
127
|
+
// Translate in specific locale (without changing context)
|
|
128
|
+
const text = await i18n.t('common:welcome', { locale: 'fr-FR' });
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Namespaces
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// Load translation from 'errors' namespace
|
|
135
|
+
const errorMsg = await i18n.t('errors:not_found');
|
|
136
|
+
|
|
137
|
+
// Load multiple namespaces
|
|
138
|
+
await i18n.loadNamespaces(['common', 'ui', 'errors']);
|
|
139
|
+
|
|
140
|
+
// Check if namespace is loaded
|
|
141
|
+
const isLoaded = i18n.isNamespaceLoaded('common');
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Locale Management
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// Get all supported locales
|
|
148
|
+
const locales = i18n.getSupportedLocales();
|
|
149
|
+
// ['en-US', 'es-ES', 'fr-FR', 'de-DE']
|
|
150
|
+
|
|
151
|
+
// Check if locale is supported
|
|
152
|
+
const isSupported = i18n.isLocaleSupported('ja-JP');
|
|
153
|
+
// false
|
|
154
|
+
|
|
155
|
+
// Get locale metadata
|
|
156
|
+
const metadata = i18n.getLocaleMetadata('en-US');
|
|
157
|
+
// {
|
|
158
|
+
// name: 'English (United States)',
|
|
159
|
+
// nativeName: 'English (United States)',
|
|
160
|
+
// direction: 'ltr',
|
|
161
|
+
// pluralRules: 'en'
|
|
162
|
+
// }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Advanced Features
|
|
166
|
+
|
|
167
|
+
### Nested Keys
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"user": {
|
|
172
|
+
"profile": {
|
|
173
|
+
"title": "User Profile",
|
|
174
|
+
"edit": "Edit Profile"
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
await i18n.t('common:user.profile.title');
|
|
182
|
+
// "User Profile"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Arrays
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"days": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
const days = await i18n.t('common:days', { returnObjects: true });
|
|
195
|
+
// ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Context-Based Translations
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"friend": "A friend",
|
|
203
|
+
"friend_male": "A boyfriend",
|
|
204
|
+
"friend_female": "A girlfriend"
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
await i18n.t('common:friend', { context: 'male' });
|
|
210
|
+
// "A boyfriend"
|
|
211
|
+
|
|
212
|
+
await i18n.t('common:friend', { context: 'female' });
|
|
213
|
+
// "A girlfriend"
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Formatting
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
// Date formatting
|
|
220
|
+
const formatted = await i18n.formatDate(new Date(), {
|
|
221
|
+
locale: 'es-ES',
|
|
222
|
+
format: 'long',
|
|
223
|
+
});
|
|
224
|
+
// "15 de enero de 2024"
|
|
225
|
+
|
|
226
|
+
// Number formatting
|
|
227
|
+
const price = await i18n.formatNumber(1234.56, {
|
|
228
|
+
style: 'currency',
|
|
229
|
+
currency: 'EUR',
|
|
230
|
+
locale: 'fr-FR',
|
|
231
|
+
});
|
|
232
|
+
// "1 234,56 €"
|
|
233
|
+
|
|
234
|
+
// Relative time
|
|
235
|
+
const relative = await i18n.formatRelative(new Date('2024-01-01'), {
|
|
236
|
+
locale: 'en-US',
|
|
237
|
+
});
|
|
238
|
+
// "3 months ago"
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Dynamic Loading
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// Add a new locale dynamically
|
|
245
|
+
await i18n.addLocale('ja-JP', {
|
|
246
|
+
loadPath: './locales/ja-JP/{{ns}}.json',
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Remove a locale
|
|
250
|
+
await i18n.removeLocale('ja-JP');
|
|
251
|
+
|
|
252
|
+
// Reload translations (useful in development)
|
|
253
|
+
await i18n.reload();
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Integration with Metadata
|
|
257
|
+
|
|
258
|
+
Translate metadata labels automatically:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { defineObject } from '@objectstack/spec';
|
|
262
|
+
|
|
263
|
+
const contact = defineObject({
|
|
264
|
+
name: 'contact',
|
|
265
|
+
label: 'i18n:objects.contact.label', // References translation key
|
|
266
|
+
fields: [
|
|
267
|
+
{
|
|
268
|
+
name: 'name',
|
|
269
|
+
label: 'i18n:fields.contact.name',
|
|
270
|
+
type: 'text',
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Translation file: locales/en-US/metadata.json
|
|
276
|
+
{
|
|
277
|
+
"objects": {
|
|
278
|
+
"contact": {
|
|
279
|
+
"label": "Contact",
|
|
280
|
+
"label_plural": "Contacts"
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
"fields": {
|
|
284
|
+
"contact": {
|
|
285
|
+
"name": "Full Name"
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## REST API Endpoints
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
GET /api/v1/i18n/locales # Get supported locales
|
|
295
|
+
GET /api/v1/i18n/translations/:locale # Get all translations for locale
|
|
296
|
+
POST /api/v1/i18n/translate # Translate keys (batch)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Client Integration
|
|
300
|
+
|
|
301
|
+
### React Hook Example
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
import { useTranslation } from '@objectstack/client-react';
|
|
305
|
+
|
|
306
|
+
function MyComponent() {
|
|
307
|
+
const { t, locale, setLocale } = useTranslation();
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<div>
|
|
311
|
+
<h1>{t('common:welcome')}</h1>
|
|
312
|
+
<button onClick={() => setLocale('es-ES')}>
|
|
313
|
+
Español
|
|
314
|
+
</button>
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Best Practices
|
|
321
|
+
|
|
322
|
+
1. **Use Namespaces**: Organize translations by domain (common, ui, errors, metadata)
|
|
323
|
+
2. **Consistent Keys**: Use dot notation for nested keys (e.g., `user.profile.title`)
|
|
324
|
+
3. **Provide Context**: Use context for gender, formality, or pluralization variants
|
|
325
|
+
4. **Fallback Values**: Always provide fallback translations in default locale
|
|
326
|
+
5. **Avoid Hardcoding**: Never hardcode user-facing text; use translation keys
|
|
327
|
+
6. **Professional Translation**: Use professional translators for production
|
|
328
|
+
7. **Version Control**: Store translation files in version control
|
|
329
|
+
|
|
330
|
+
## Locale Coverage Detection
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// Get coverage statistics
|
|
334
|
+
const coverage = await i18n.getCoverage();
|
|
335
|
+
// {
|
|
336
|
+
// 'en-US': { total: 245, missing: 0, percentage: 100 },
|
|
337
|
+
// 'es-ES': { total: 245, missing: 12, percentage: 95.1 },
|
|
338
|
+
// 'fr-FR': { total: 245, missing: 45, percentage: 81.6 }
|
|
339
|
+
// }
|
|
340
|
+
|
|
341
|
+
// Get missing keys for a locale
|
|
342
|
+
const missing = await i18n.getMissingKeys('es-ES');
|
|
343
|
+
// ['errors.validation.email', 'ui.dashboard.title', ...]
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Performance Considerations
|
|
347
|
+
|
|
348
|
+
- **Lazy Loading**: Namespaces are loaded on demand
|
|
349
|
+
- **Caching**: Translations are cached in memory
|
|
350
|
+
- **Hot Reload**: Only enable in development
|
|
351
|
+
- **Bundle Size**: Load only required locales on client
|
|
352
|
+
|
|
353
|
+
## Contract Implementation
|
|
354
|
+
|
|
355
|
+
Implements `II18nService` from `@objectstack/spec/contracts`:
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
interface II18nService {
|
|
359
|
+
t(key: string, options?: TranslationOptions): Promise<string>;
|
|
360
|
+
setLocale(locale: string): Promise<void>;
|
|
361
|
+
getLocale(): string;
|
|
362
|
+
getSupportedLocales(): string[];
|
|
363
|
+
loadNamespaces(namespaces: string[]): Promise<void>;
|
|
364
|
+
formatDate(date: Date, options?: FormatOptions): Promise<string>;
|
|
365
|
+
formatNumber(value: number, options?: FormatOptions): Promise<string>;
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## License
|
|
370
|
+
|
|
371
|
+
Apache-2.0
|
|
372
|
+
|
|
373
|
+
## See Also
|
|
374
|
+
|
|
375
|
+
- [i18next Documentation](https://www.i18next.com/)
|
|
376
|
+
- [@objectstack/spec/system (Translation schema)](../../spec/src/system/)
|
|
377
|
+
- [I18n Best Practices Guide](/content/docs/guides/i18n/)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/service-i18n",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.4",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "I18n Service for ObjectStack — implements II18nService with file-based locale loading",
|
|
6
6
|
"type": "module",
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@objectstack/core": "4.0.
|
|
18
|
-
"@objectstack/spec": "4.0.
|
|
17
|
+
"@objectstack/core": "4.0.4",
|
|
18
|
+
"@objectstack/spec": "4.0.4"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/node": "^25.6.0",
|