@gravito/cosmos 3.0.1 → 3.2.0
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/MIGRATION.md +331 -0
- package/README.md +105 -45
- package/README.zh-TW.md +102 -22
- package/docs/plans/01-performance.md +187 -0
- package/docs/plans/02-architecture.md +309 -0
- package/docs/plans/03-api-enhancement.md +345 -0
- package/docs/plans/04-testing.md +431 -0
- package/docs/plans/README.md +47 -0
- package/ion/src/index.js +1179 -1138
- package/package.json +22 -6
- package/scripts/check-coverage.ts +64 -0
- package/src/HMRWatcher.ts +305 -0
- package/src/I18nService.ts +715 -91
- package/src/index.edge.ts +35 -0
- package/src/index.node.ts +20 -0
- package/src/index.ts +39 -6
- package/src/loader.ts +64 -14
- package/src/loaders/ChainedLoader.ts +117 -0
- package/src/loaders/CloudflareKVLoader.ts +194 -0
- package/src/loaders/EdgeKVLoader.ts +248 -0
- package/src/loaders/FileSystemLoader.ts +125 -0
- package/src/loaders/MemoryLoader.ts +161 -0
- package/src/loaders/RemoteLoader.ts +235 -0
- package/src/loaders/TranslationLoader.ts +98 -0
- package/src/loaders/VercelKVLoader.ts +192 -0
- package/src/runtime/detector.ts +97 -0
- package/src/runtime/path-utils.ts +169 -0
- package/tests/helpers/factory.ts +41 -0
- package/tests/performance/translate.bench.ts +27 -0
- package/tests/unit/api.test.ts +37 -0
- package/tests/unit/detector.test.ts +65 -0
- package/tests/unit/edge-kv-loader.test.ts +202 -0
- package/tests/unit/edge.test.ts +100 -0
- package/tests/unit/fallback.test.ts +66 -0
- package/tests/unit/hmr.test.ts +255 -0
- package/tests/unit/lazy.test.ts +35 -0
- package/tests/unit/loader.test.ts +72 -0
- package/tests/unit/loaders.test.ts +332 -0
- package/tests/{manager.test.ts → unit/manager.test.ts} +1 -1
- package/tests/unit/memory-loader.test.ts +130 -0
- package/tests/unit/path-utils.test.ts +135 -0
- package/tests/unit/plural.test.ts +58 -0
- package/tests/unit/runtime-detector.test.ts +86 -0
- package/tests/{service.test.ts → unit/service.test.ts} +2 -2
- package/tsconfig.json +12 -24
- package/.turbo/turbo-build.log +0 -20
- package/.turbo/turbo-test$colon$ci.log +0 -35
- package/.turbo/turbo-test$colon$coverage.log +0 -35
- package/.turbo/turbo-test.log +0 -27
- package/.turbo/turbo-typecheck.log +0 -2
- package/dist/index.cjs +0 -309
- package/dist/index.d.cts +0 -274
- package/dist/index.d.ts +0 -274
- package/dist/index.js +0 -277
- package/tests/loader.test.ts +0 -44
package/MIGRATION.md
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# Migration Guide: Cosmos v1.x → v2.0
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Cosmos v2.0 introduces Edge Runtime support while maintaining full backward compatibility with v1.x. This guide helps you migrate to v2.0 and leverage new features.
|
|
6
|
+
|
|
7
|
+
## Breaking Changes
|
|
8
|
+
|
|
9
|
+
**None!** v2.0 is fully backward compatible with v1.x.
|
|
10
|
+
|
|
11
|
+
All existing code will continue to work without modifications.
|
|
12
|
+
|
|
13
|
+
## What's New in v2.0
|
|
14
|
+
|
|
15
|
+
### Edge Runtime Support
|
|
16
|
+
|
|
17
|
+
Cosmos now runs in Edge Runtime environments:
|
|
18
|
+
- ✅ Cloudflare Workers
|
|
19
|
+
- ✅ Vercel Edge Functions
|
|
20
|
+
- ✅ Deno Deploy
|
|
21
|
+
- ✅ Node.js (existing support)
|
|
22
|
+
|
|
23
|
+
### New Loaders
|
|
24
|
+
|
|
25
|
+
- **MemoryLoader**: Static translations for Edge environments
|
|
26
|
+
- **EdgeKVLoader**: Generic KV storage abstraction
|
|
27
|
+
- **CloudflareKVLoader**: Cloudflare Workers KV
|
|
28
|
+
- **VercelKVLoader**: Vercel KV
|
|
29
|
+
|
|
30
|
+
### Runtime Detection
|
|
31
|
+
|
|
32
|
+
- `detectRuntime()`: Auto-detect execution environment
|
|
33
|
+
- `isNode()`, `isEdge()`: Helper functions
|
|
34
|
+
|
|
35
|
+
## Migration Paths
|
|
36
|
+
|
|
37
|
+
### For Node.js Projects
|
|
38
|
+
|
|
39
|
+
**No changes required!** Your existing code works as-is.
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// ✅ This still works
|
|
43
|
+
import { OrbitCosmos } from '@gravito/cosmos'
|
|
44
|
+
|
|
45
|
+
const cosmos = new OrbitCosmos({
|
|
46
|
+
defaultLocale: 'zh-TW',
|
|
47
|
+
supportedLocales: ['zh-TW', 'en'],
|
|
48
|
+
lazyLoad: {
|
|
49
|
+
baseDir: './lang'
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### For Edge Runtime Projects
|
|
55
|
+
|
|
56
|
+
#### Option 1: Static Translations (Recommended for <100KB)
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// Before: Not possible
|
|
60
|
+
// After: Use MemoryLoader
|
|
61
|
+
import { OrbitCosmos, MemoryLoader } from '@gravito/cosmos/edge'
|
|
62
|
+
import en from './lang/en.json'
|
|
63
|
+
import zhTW from './lang/zh-TW.json'
|
|
64
|
+
|
|
65
|
+
const cosmos = new OrbitCosmos({
|
|
66
|
+
defaultLocale: 'zh-TW',
|
|
67
|
+
supportedLocales: ['zh-TW', 'en'],
|
|
68
|
+
loaders: [
|
|
69
|
+
new MemoryLoader({
|
|
70
|
+
translations: { en, 'zh-TW': zhTW }
|
|
71
|
+
})
|
|
72
|
+
]
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### Option 2: Remote API (Recommended for dynamic content)
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { OrbitCosmos, RemoteLoader } from '@gravito/cosmos/edge'
|
|
80
|
+
|
|
81
|
+
const cosmos = new OrbitCosmos({
|
|
82
|
+
defaultLocale: 'zh-TW',
|
|
83
|
+
supportedLocales: ['zh-TW', 'en'],
|
|
84
|
+
loaders: [
|
|
85
|
+
new RemoteLoader({
|
|
86
|
+
url: 'https://api.example.com/i18n/:locale',
|
|
87
|
+
etagCache: true,
|
|
88
|
+
retries: 3
|
|
89
|
+
})
|
|
90
|
+
]
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### Option 3: KV Storage (Recommended for Cloudflare/Vercel)
|
|
95
|
+
|
|
96
|
+
**Cloudflare Workers:**
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { OrbitCosmos, CloudflareKVLoader } from '@gravito/cosmos/edge'
|
|
100
|
+
|
|
101
|
+
export interface Env {
|
|
102
|
+
I18N_KV: KVNamespace
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export default {
|
|
106
|
+
async fetch(request: Request, env: Env) {
|
|
107
|
+
const cosmos = new OrbitCosmos({
|
|
108
|
+
defaultLocale: 'zh-TW',
|
|
109
|
+
supportedLocales: ['zh-TW', 'en'],
|
|
110
|
+
loaders: [
|
|
111
|
+
new CloudflareKVLoader({
|
|
112
|
+
namespace: env.I18N_KV
|
|
113
|
+
})
|
|
114
|
+
]
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// ... use cosmos
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Vercel Edge Functions:**
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { OrbitCosmos, VercelKVLoader } from '@gravito/cosmos/edge'
|
|
126
|
+
import { kv } from '@vercel/kv'
|
|
127
|
+
|
|
128
|
+
const cosmos = new OrbitCosmos({
|
|
129
|
+
defaultLocale: 'zh-TW',
|
|
130
|
+
supportedLocales: ['zh-TW', 'en'],
|
|
131
|
+
loaders: [
|
|
132
|
+
new VercelKVLoader({ kv })
|
|
133
|
+
]
|
|
134
|
+
})
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Best Practices
|
|
138
|
+
|
|
139
|
+
### 1. Use Fallback Chains
|
|
140
|
+
|
|
141
|
+
Combine loaders for reliability:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { MemoryLoader, RemoteLoader, CloudflareKVLoader } from '@gravito/cosmos/edge'
|
|
145
|
+
|
|
146
|
+
const cosmos = new OrbitCosmos({
|
|
147
|
+
loaders: [
|
|
148
|
+
new CloudflareKVLoader({ namespace: env.I18N_KV })
|
|
149
|
+
.fallback(new RemoteLoader({ url: env.I18N_API }))
|
|
150
|
+
.fallback(new MemoryLoader({ translations: fallbackData }))
|
|
151
|
+
]
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 2. Choose the Right Loader
|
|
156
|
+
|
|
157
|
+
| Scenario | Recommended Loader | Reason |
|
|
158
|
+
|----------|-------------------|--------|
|
|
159
|
+
| Small static translations (<100KB) | MemoryLoader | Fastest, no network |
|
|
160
|
+
| Dynamic CMS translations | RemoteLoader | Real-time updates |
|
|
161
|
+
| Cloudflare Workers | CloudflareKVLoader | Edge-optimized |
|
|
162
|
+
| Vercel Edge | VercelKVLoader | Edge-optimized |
|
|
163
|
+
| Node.js server | FileSystemLoader | Native fs access |
|
|
164
|
+
|
|
165
|
+
### 3. Optimize Bundle Size
|
|
166
|
+
|
|
167
|
+
Edge environments have size limits:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
// ✅ Good: Import only what you need
|
|
171
|
+
import { OrbitCosmos, MemoryLoader } from '@gravito/cosmos/edge'
|
|
172
|
+
|
|
173
|
+
// ❌ Avoid: Importing Node.js-only features in Edge
|
|
174
|
+
import { FileSystemLoader } from '@gravito/cosmos/node'
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Deprecated Features
|
|
178
|
+
|
|
179
|
+
### lazyLoad.loader (Still works, but deprecated)
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// ⚠️ Deprecated
|
|
183
|
+
{
|
|
184
|
+
lazyLoad: {
|
|
185
|
+
baseDir: './lang',
|
|
186
|
+
loader: customLoaderFn
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ✅ Recommended
|
|
191
|
+
{
|
|
192
|
+
loaders: [
|
|
193
|
+
new FileSystemLoader({ baseDir: './lang' })
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Troubleshooting
|
|
199
|
+
|
|
200
|
+
### Error: "FileSystemLoader requires Node.js"
|
|
201
|
+
|
|
202
|
+
**Cause**: Using FileSystemLoader in Edge Runtime
|
|
203
|
+
|
|
204
|
+
**Solution**: Use MemoryLoader, RemoteLoader, or EdgeKVLoader
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// ❌ Wrong
|
|
208
|
+
import { FileSystemLoader } from '@gravito/cosmos/edge'
|
|
209
|
+
|
|
210
|
+
// ✅ Correct
|
|
211
|
+
import { MemoryLoader } from '@gravito/cosmos/edge'
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Warning: "HMR is not supported in Edge Runtime"
|
|
215
|
+
|
|
216
|
+
**Cause**: HMRWatcher is Node.js-only
|
|
217
|
+
|
|
218
|
+
**Solution**: Use RemoteLoader with ETag caching for dynamic updates
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// Edge Runtime alternative to HMR
|
|
222
|
+
new RemoteLoader({
|
|
223
|
+
url: 'https://api.example.com/i18n/:locale',
|
|
224
|
+
etagCache: true // Automatically refetches when content changes
|
|
225
|
+
})
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Translations not loading
|
|
229
|
+
|
|
230
|
+
**Debug steps:**
|
|
231
|
+
|
|
232
|
+
1. Check loader configuration
|
|
233
|
+
2. Verify translations are uploaded (for KV loaders)
|
|
234
|
+
3. Check network requests (for RemoteLoader)
|
|
235
|
+
4. Enable verbose logging
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
const loader = new RemoteLoader({
|
|
239
|
+
url: 'https://api.example.com/i18n/:locale',
|
|
240
|
+
// Add console.log in load method for debugging
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
console.log('Testing load:', await loader.load('en'))
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Examples
|
|
247
|
+
|
|
248
|
+
### Cloudflare Workers Complete Example
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
import { OrbitCosmos, CloudflareKVLoader, MemoryLoader } from '@gravito/cosmos/edge'
|
|
252
|
+
|
|
253
|
+
export interface Env {
|
|
254
|
+
I18N_KV: KVNamespace
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Fallback translations
|
|
258
|
+
const fallback = {
|
|
259
|
+
en: { error: 'An error occurred' },
|
|
260
|
+
'zh-TW': { error: '發生錯誤' }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export default {
|
|
264
|
+
async fetch(request: Request, env: Env) {
|
|
265
|
+
const cosmos = new OrbitCosmos({
|
|
266
|
+
defaultLocale: 'zh-TW',
|
|
267
|
+
supportedLocales: ['zh-TW', 'en'],
|
|
268
|
+
loaders: [
|
|
269
|
+
new CloudflareKVLoader({ namespace: env.I18N_KV })
|
|
270
|
+
.fallback(new MemoryLoader({ translations: fallback }))
|
|
271
|
+
]
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
const i18n = cosmos.clone('zh-TW')
|
|
275
|
+
await i18n.ensureLocale('zh-TW')
|
|
276
|
+
|
|
277
|
+
return new Response(i18n.t('welcome'))
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Vercel Edge Functions Complete Example
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
import { OrbitCosmos, VercelKVLoader } from '@gravito/cosmos/edge'
|
|
286
|
+
import { kv } from '@vercel/kv'
|
|
287
|
+
|
|
288
|
+
const cosmos = new OrbitCosmos({
|
|
289
|
+
defaultLocale: 'zh-TW',
|
|
290
|
+
supportedLocales: ['zh-TW', 'en'],
|
|
291
|
+
loaders: [
|
|
292
|
+
new VercelKVLoader({ kv, prefix: 'i18n' })
|
|
293
|
+
]
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
export async function GET(request: Request) {
|
|
297
|
+
const url = new URL(request.url)
|
|
298
|
+
const locale = url.searchParams.get('lang') || 'zh-TW'
|
|
299
|
+
|
|
300
|
+
const i18n = cosmos.clone(locale)
|
|
301
|
+
await i18n.ensureLocale(locale)
|
|
302
|
+
|
|
303
|
+
return new Response(JSON.stringify({
|
|
304
|
+
message: i18n.t('welcome'),
|
|
305
|
+
locale: i18n.getLocale()
|
|
306
|
+
}))
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## FAQ
|
|
311
|
+
|
|
312
|
+
**Q: Do I need to update my code for v2.0?**
|
|
313
|
+
A: No, v2.0 is fully backward compatible.
|
|
314
|
+
|
|
315
|
+
**Q: Can I use FileSystemLoader in Edge Runtime?**
|
|
316
|
+
A: No, use MemoryLoader, RemoteLoader, or EdgeKVLoader instead.
|
|
317
|
+
|
|
318
|
+
**Q: How do I upload translations to KV storage?**
|
|
319
|
+
A: See the CloudflareKVLoader and VercelKVLoader documentation for upload scripts.
|
|
320
|
+
|
|
321
|
+
**Q: What's the performance difference between loaders?**
|
|
322
|
+
A: MemoryLoader (fastest) > KV Loaders (fast) > RemoteLoader (depends on network)
|
|
323
|
+
|
|
324
|
+
**Q: Can I mix Node.js and Edge loaders?**
|
|
325
|
+
A: In Node.js, yes. In Edge, only Edge-compatible loaders work.
|
|
326
|
+
|
|
327
|
+
## Support
|
|
328
|
+
|
|
329
|
+
- Documentation: [README.md](./README.md)
|
|
330
|
+
- Architecture: [docs/architecture/cosmos.md](../../docs/architecture/cosmos.md)
|
|
331
|
+
- Issues: [GitHub Issues](https://github.com/gravito-framework/gravito/issues)
|
package/README.md
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
# @gravito/cosmos
|
|
1
|
+
# @gravito/cosmos 🌌
|
|
2
2
|
|
|
3
|
-
> Lightweight Internationalization (i18n) Orbit for Gravito.
|
|
3
|
+
> Lightweight, high-performance Internationalization (i18n) Orbit for the Gravito framework.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`@gravito/cosmos` brings seamless localization support to your Gravito applications. It features request-scoped i18n instances, lazy loading of translation files, parameter replacement, and flexible locale detection.
|
|
6
|
+
|
|
7
|
+
## ✨ Features
|
|
8
|
+
|
|
9
|
+
- **🚀 Performance-First**: Highly optimized translation resolution with internal caching.
|
|
10
|
+
- **🛡️ Type-Safe**: Support for type-safe translation keys using TypeScript generics.
|
|
11
|
+
- **🔄 Request-Scoped**: Clones i18n instances per request to maintain locale state without resource duplication.
|
|
12
|
+
- **📂 Lazy Loading**: Load translation files from the filesystem only when needed.
|
|
13
|
+
- **🔗 Flexible Fallbacks**: Define custom fallback chains and missing key strategies.
|
|
14
|
+
- **🌍 Pluralization**: Integrated support for `Intl.PluralRules` based pluralization.
|
|
15
|
+
- **📡 Auto-Detection**: Detects locale from Route Params, Query Strings, or `Accept-Language` headers.
|
|
6
16
|
|
|
7
17
|
## 📦 Installation
|
|
8
18
|
|
|
@@ -12,55 +22,105 @@ bun add @gravito/cosmos
|
|
|
12
22
|
|
|
13
23
|
## 🚀 Quick Start
|
|
14
24
|
|
|
15
|
-
1.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
2. **Create Locale Files**:
|
|
35
|
-
Create `./lang/en.json` and `./lang/zh-TW.json`:
|
|
36
|
-
```json
|
|
37
|
-
// en.json
|
|
38
|
-
{
|
|
39
|
-
"welcome": "Welcome, {name}!"
|
|
25
|
+
### 1. Register the Orbit
|
|
26
|
+
|
|
27
|
+
Add `OrbitCosmos` to your PlanetCore configuration:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { PlanetCore } from '@gravito/core';
|
|
31
|
+
import { OrbitCosmos } from '@gravito/cosmos';
|
|
32
|
+
|
|
33
|
+
const core = new PlanetCore({
|
|
34
|
+
config: {
|
|
35
|
+
// Optional static translations
|
|
36
|
+
translations: {
|
|
37
|
+
en: { welcome: 'Welcome, :name!' },
|
|
38
|
+
'zh-TW': { welcome: '歡迎,:name!' }
|
|
40
39
|
}
|
|
41
|
-
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
core.addOrbit(new OrbitCosmos({
|
|
44
|
+
defaultLocale: 'en',
|
|
45
|
+
supportedLocales: ['en', 'zh-TW'],
|
|
46
|
+
// Optional: configure lazy loading from a directory
|
|
47
|
+
lazyLoad: {
|
|
48
|
+
baseDir: './lang'
|
|
49
|
+
}
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
await core.bootstrap();
|
|
53
|
+
```
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
### 2. Create Locale Files (If using lazy loading)
|
|
56
|
+
|
|
57
|
+
Create `./lang/en.json`:
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"auth": {
|
|
61
|
+
"login_success": "Welcome back!",
|
|
62
|
+
"failed": "Invalid credentials."
|
|
63
|
+
},
|
|
64
|
+
"items": {
|
|
65
|
+
"count": {
|
|
66
|
+
"zero": "No items found",
|
|
67
|
+
"one": "Found :count item",
|
|
68
|
+
"other": "Found :count items"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
50
73
|
|
|
51
|
-
|
|
74
|
+
### 3. Use in Routes
|
|
52
75
|
|
|
53
|
-
|
|
54
|
-
- **Parameter Replacement**: Supports `{key}` style parameter replacement.
|
|
55
|
-
- **Locale Detection**: Automatically detects locale from `Accept-Language` header or query parameter `?lang=`.
|
|
56
|
-
- **Fallback**: gracefully falls back to default locale if key is missing.
|
|
76
|
+
The `i18n` service is automatically injected into the context:
|
|
57
77
|
|
|
58
|
-
|
|
78
|
+
```typescript
|
|
79
|
+
app.get('/hello', (c) => {
|
|
80
|
+
const t = c.get('i18n').t;
|
|
81
|
+
return c.text(t('welcome', { name: 'Carl' }));
|
|
82
|
+
});
|
|
59
83
|
|
|
60
|
-
|
|
84
|
+
// Pluralization
|
|
85
|
+
app.get('/items', (c) => {
|
|
86
|
+
const i18n = c.get('i18n');
|
|
87
|
+
return c.text(i18n.t('items.count', { count: 5 })); // "Found 5 items"
|
|
88
|
+
});
|
|
89
|
+
```
|
|
61
90
|
|
|
62
|
-
|
|
91
|
+
## 📚 Core Concepts
|
|
92
|
+
|
|
93
|
+
### I18nManager
|
|
94
|
+
The central hub that holds shared configuration and translation resources. It handles the heavy lifting of loading files and resolving keys.
|
|
95
|
+
|
|
96
|
+
### I18nInstance
|
|
97
|
+
A lightweight, request-scoped object that holds the current locale. It delegates to the `I18nManager` for translations.
|
|
98
|
+
|
|
99
|
+
### Locale Detectors
|
|
100
|
+
Built-in detectors allow automatic locale selection:
|
|
101
|
+
- `RouteParamDetector`: Looks for `:locale` in route parameters.
|
|
102
|
+
- `QueryDetector`: Looks for `?lang=` in the URL.
|
|
103
|
+
- `HeaderDetector`: Uses the `Accept-Language` header.
|
|
104
|
+
|
|
105
|
+
## 🛠️ Configuration
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
export interface I18nConfig {
|
|
109
|
+
defaultLocale: string;
|
|
110
|
+
supportedLocales: string[];
|
|
111
|
+
translations?: Record<string, TranslationMap>;
|
|
112
|
+
lazyLoad?: {
|
|
113
|
+
baseDir: string;
|
|
114
|
+
preload?: string[];
|
|
115
|
+
};
|
|
116
|
+
fallback?: {
|
|
117
|
+
fallbackChain?: Record<string, string[]>;
|
|
118
|
+
onMissingKey?: 'key' | 'empty' | 'throw' | ((key: string, locale: string) => string);
|
|
119
|
+
warnOnMissing?: boolean;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
```
|
|
63
123
|
|
|
64
124
|
## License
|
|
65
125
|
|
|
66
|
-
MIT
|
|
126
|
+
MIT © Carl Lee
|
package/README.zh-TW.md
CHANGED
|
@@ -1,46 +1,126 @@
|
|
|
1
|
-
# @gravito/cosmos
|
|
1
|
+
# @gravito/cosmos 🌌
|
|
2
2
|
|
|
3
|
-
> Gravito
|
|
3
|
+
> 為 Gravito 框架設計的輕量級、高效能國際化 (i18n) 擴展。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`@gravito/cosmos` 為您的 Gravito 應用程式提供無縫的在地化支援。具備請求級別的 i18n 實例、翻譯檔案懶加載、參數替換以及靈活的語言偵測功能。
|
|
6
|
+
|
|
7
|
+
## ✨ 特性
|
|
8
|
+
|
|
9
|
+
- **🚀 效能優先**:高度優化的翻譯查找與內部快取機制。
|
|
10
|
+
- **🛡️ 類型安全**:支援 TypeScript 泛型,提供類型安全的翻譯鍵值。
|
|
11
|
+
- **🔄 請求級別實例**:為每個請求克隆輕量實例,保持語言狀態而不重複佔用資源。
|
|
12
|
+
- **📂 懶加載**:僅在需要時從檔案系統載入翻譯檔案。
|
|
13
|
+
- **🔗 靈活的回退機制**:可自定義回退鏈與缺失鍵值的處理策略。
|
|
14
|
+
- **🌍 複數支援**:整合 `Intl.PluralRules` 的複數處理。
|
|
15
|
+
- **📡 自動偵測**:支援從路由參數、查詢字串或 `Accept-Language` 標頭偵測語言。
|
|
16
|
+
|
|
17
|
+
## 📦 安裝
|
|
6
18
|
|
|
7
19
|
```bash
|
|
8
20
|
bun add @gravito/cosmos
|
|
9
21
|
```
|
|
10
22
|
|
|
11
|
-
## 快速開始
|
|
23
|
+
## 🚀 快速開始
|
|
12
24
|
|
|
13
|
-
|
|
14
|
-
import { PlanetCore } from '@gravito/core'
|
|
15
|
-
import { OrbitI18n } from '@gravito/cosmos'
|
|
25
|
+
### 1. 註冊 Orbit
|
|
16
26
|
|
|
17
|
-
|
|
27
|
+
將 `OrbitCosmos` 加入您的 PlanetCore 配置中:
|
|
18
28
|
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
```typescript
|
|
30
|
+
import { PlanetCore } from '@gravito/core';
|
|
31
|
+
import { OrbitCosmos } from '@gravito/cosmos';
|
|
32
|
+
|
|
33
|
+
const core = new PlanetCore({
|
|
21
34
|
config: {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
// 可選的靜態翻譯
|
|
36
|
+
translations: {
|
|
37
|
+
en: { welcome: 'Welcome, :name!' },
|
|
38
|
+
'zh-TW': { welcome: '歡迎,:name!' }
|
|
26
39
|
}
|
|
27
40
|
}
|
|
28
|
-
})
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
core.addOrbit(new OrbitCosmos({
|
|
44
|
+
defaultLocale: 'en',
|
|
45
|
+
supportedLocales: ['en', 'zh-TW'],
|
|
46
|
+
// 可選:配置語言檔案目錄
|
|
47
|
+
lazyLoad: {
|
|
48
|
+
baseDir: './lang'
|
|
49
|
+
}
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
await core.bootstrap();
|
|
29
53
|
```
|
|
30
54
|
|
|
31
|
-
|
|
55
|
+
### 2. 建立語言檔案 (若使用懶加載)
|
|
32
56
|
|
|
57
|
+
建立 `./lang/en.json`:
|
|
33
58
|
```json
|
|
34
59
|
{
|
|
35
|
-
"
|
|
60
|
+
"auth": {
|
|
61
|
+
"login_success": "Welcome back!",
|
|
62
|
+
"failed": "Invalid credentials."
|
|
63
|
+
},
|
|
64
|
+
"items": {
|
|
65
|
+
"count": {
|
|
66
|
+
"zero": "No items found",
|
|
67
|
+
"one": "Found :count item",
|
|
68
|
+
"other": "Found :count items"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
36
71
|
}
|
|
37
72
|
```
|
|
38
73
|
|
|
39
|
-
|
|
74
|
+
### 3. 在路由中使用
|
|
75
|
+
|
|
76
|
+
`i18n` 服務會自動注入到上下文 (Context) 中:
|
|
40
77
|
|
|
41
78
|
```typescript
|
|
42
|
-
app.get('/', (c) => {
|
|
43
|
-
const t = c.get('
|
|
44
|
-
return c.text(t('welcome', { name: 'Carl' }))
|
|
45
|
-
})
|
|
79
|
+
app.get('/hello', (c) => {
|
|
80
|
+
const t = c.get('i18n').t;
|
|
81
|
+
return c.text(t('welcome', { name: 'Carl' }));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// 複數處理
|
|
85
|
+
app.get('/items', (c) => {
|
|
86
|
+
const i18n = c.get('i18n');
|
|
87
|
+
return c.text(i18n.t('items.count', { count: 5 })); // "Found 5 items"
|
|
88
|
+
});
|
|
46
89
|
```
|
|
90
|
+
|
|
91
|
+
## 📚 核心概念
|
|
92
|
+
|
|
93
|
+
### I18nManager
|
|
94
|
+
核心管理中心,持有共享配置與翻譯資源。負責載入檔案、處理翻譯邏輯、回退策略與快取。
|
|
95
|
+
|
|
96
|
+
### I18nInstance
|
|
97
|
+
輕量級的請求範圍物件,持有當前的語言狀態。實際翻譯邏輯委託給 `I18nManager` 執行。
|
|
98
|
+
|
|
99
|
+
### 語言偵測器 (Locale Detectors)
|
|
100
|
+
內建多種偵測器:
|
|
101
|
+
- `RouteParamDetector`:從路由參數 `:locale` 獲取。
|
|
102
|
+
- `QueryDetector`:從 URL 查詢字串 `?lang=` 獲取。
|
|
103
|
+
- `HeaderDetector`:從 `Accept-Language` HTTP 標頭獲取。
|
|
104
|
+
|
|
105
|
+
## 🛠️ 配置選項
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
export interface I18nConfig {
|
|
109
|
+
defaultLocale: string;
|
|
110
|
+
supportedLocales: string[];
|
|
111
|
+
translations?: Record<string, TranslationMap>;
|
|
112
|
+
lazyLoad?: {
|
|
113
|
+
baseDir: string;
|
|
114
|
+
preload?: string[];
|
|
115
|
+
};
|
|
116
|
+
fallback?: {
|
|
117
|
+
fallbackChain?: Record<string, string[]>;
|
|
118
|
+
onMissingKey?: 'key' | 'empty' | 'throw' | ((key: string, locale: string) => string);
|
|
119
|
+
warnOnMissing?: boolean;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT © Carl Lee
|