@gravito/cosmos 3.0.1 → 3.1.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/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 +3 -2
- package/src/I18nService.ts +575 -91
- package/src/index.ts +25 -6
- package/src/loader.ts +45 -12
- 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 +70 -0
- package/tests/unit/edge.test.ts +100 -0
- package/tests/unit/fallback.test.ts +66 -0
- package/tests/unit/lazy.test.ts +35 -0
- package/tests/{loader.test.ts → unit/loader.test.ts} +1 -1
- package/tests/{manager.test.ts → unit/manager.test.ts} +1 -1
- package/tests/unit/plural.test.ts +58 -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
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Phase 2: 架構改進計劃
|
|
2
|
+
|
|
3
|
+
> 優先級: P1 | 預估影響: 中高
|
|
4
|
+
|
|
5
|
+
## 現況分析
|
|
6
|
+
|
|
7
|
+
### 當前架構
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
OrbitCosmos
|
|
11
|
+
│
|
|
12
|
+
├── I18nManager (單例)
|
|
13
|
+
│ │
|
|
14
|
+
│ └── translations: Record<locale, TranslationMap>
|
|
15
|
+
│
|
|
16
|
+
├── I18nInstance (請求範圍)
|
|
17
|
+
│ └── 包裝 manager + locale
|
|
18
|
+
│
|
|
19
|
+
└── localeMiddleware
|
|
20
|
+
└── 語言檢測 → 注入 I18nInstance
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 識別問題
|
|
24
|
+
|
|
25
|
+
1. **型別定義不夠精確**
|
|
26
|
+
- `TranslationMap` 過於寬鬆
|
|
27
|
+
- 缺少嚴格的型別推斷
|
|
28
|
+
|
|
29
|
+
2. **缺少複數形式支援**
|
|
30
|
+
- 無法處理 `1 item` vs `2 items`
|
|
31
|
+
|
|
32
|
+
3. **無 ICU MessageFormat 支援**
|
|
33
|
+
- 複雜格式化需求無法滿足
|
|
34
|
+
|
|
35
|
+
4. **中間件耦合度高**
|
|
36
|
+
- 語言檢測邏輯硬編碼
|
|
37
|
+
|
|
38
|
+
## 優化方案
|
|
39
|
+
|
|
40
|
+
### 2.1 強化型別系統
|
|
41
|
+
|
|
42
|
+
**目標**: 提供編譯時期的翻譯鍵檢查
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// 定義翻譯結構型別
|
|
46
|
+
type NestedKeyOf<T> = T extends object
|
|
47
|
+
? { [K in keyof T]: K extends string
|
|
48
|
+
? T[K] extends object
|
|
49
|
+
? `${K}.${NestedKeyOf<T[K]>}`
|
|
50
|
+
: K
|
|
51
|
+
: never
|
|
52
|
+
}[keyof T]
|
|
53
|
+
: never
|
|
54
|
+
|
|
55
|
+
// 使用範例
|
|
56
|
+
interface Translations {
|
|
57
|
+
common: {
|
|
58
|
+
welcome: string
|
|
59
|
+
goodbye: string
|
|
60
|
+
}
|
|
61
|
+
auth: {
|
|
62
|
+
login: string
|
|
63
|
+
logout: string
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type TranslationKey = NestedKeyOf<Translations>
|
|
68
|
+
// 結果: "common.welcome" | "common.goodbye" | "auth.login" | "auth.logout"
|
|
69
|
+
|
|
70
|
+
// 型別安全的翻譯函數
|
|
71
|
+
function t<T extends Translations>(key: NestedKeyOf<T>): string
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 2.2 複數形式支援
|
|
75
|
+
|
|
76
|
+
**目標**: 支援根據數量選擇正確的翻譯形式
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
interface PluralConfig {
|
|
80
|
+
zero?: string
|
|
81
|
+
one: string
|
|
82
|
+
two?: string
|
|
83
|
+
few?: string
|
|
84
|
+
many?: string
|
|
85
|
+
other: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 翻譯檔案格式
|
|
89
|
+
{
|
|
90
|
+
"items": {
|
|
91
|
+
"zero": "沒有項目",
|
|
92
|
+
"one": "1 個項目",
|
|
93
|
+
"other": ":count 個項目"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// API 使用
|
|
98
|
+
i18n.t('items', { count: 0 }) // "沒有項目"
|
|
99
|
+
i18n.t('items', { count: 1 }) // "1 個項目"
|
|
100
|
+
i18n.t('items', { count: 5 }) // "5 個項目"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**實作方式**:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
class I18nManager {
|
|
107
|
+
private pluralRules: Map<string, Intl.PluralRules> = new Map()
|
|
108
|
+
|
|
109
|
+
private getPluralForm(locale: string, count: number): string {
|
|
110
|
+
if (!this.pluralRules.has(locale)) {
|
|
111
|
+
this.pluralRules.set(locale, new Intl.PluralRules(locale))
|
|
112
|
+
}
|
|
113
|
+
return this.pluralRules.get(locale)!.select(count)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
translate(locale: string, key: string, replacements?: Record<string, unknown>) {
|
|
117
|
+
const value = this.resolveKey(locale, key)
|
|
118
|
+
|
|
119
|
+
// 檢查是否為複數形式
|
|
120
|
+
if (typeof value === 'object' && replacements?.count !== undefined) {
|
|
121
|
+
const form = this.getPluralForm(locale, Number(replacements.count))
|
|
122
|
+
const pluralValue = value[form] ?? value.other
|
|
123
|
+
return this.replaceParams(pluralValue, replacements)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return this.replaceParams(value, replacements)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 2.3 ICU MessageFormat 支援 (可選)
|
|
132
|
+
|
|
133
|
+
**目標**: 支援複雜的訊息格式化
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// ICU MessageFormat 範例
|
|
137
|
+
{
|
|
138
|
+
"greeting": "Hello {name}, you have {count, plural, =0 {no messages} one {# message} other {# messages}}."
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 使用
|
|
142
|
+
i18n.t('greeting', { name: 'Carl', count: 5 })
|
|
143
|
+
// "Hello Carl, you have 5 messages."
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**實作方式**: 整合 `@formatjs/intl-messageformat`
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { IntlMessageFormat } from 'intl-messageformat'
|
|
150
|
+
|
|
151
|
+
class I18nManager {
|
|
152
|
+
private messageCache = new Map<string, IntlMessageFormat>()
|
|
153
|
+
|
|
154
|
+
translateICU(locale: string, key: string, values?: Record<string, unknown>) {
|
|
155
|
+
const cacheKey = `${locale}:${key}`
|
|
156
|
+
|
|
157
|
+
if (!this.messageCache.has(cacheKey)) {
|
|
158
|
+
const message = this.resolveKey(locale, key)
|
|
159
|
+
this.messageCache.set(cacheKey, new IntlMessageFormat(message, locale))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return this.messageCache.get(cacheKey)!.format(values)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### 2.4 可插拔的語言檢測策略
|
|
168
|
+
|
|
169
|
+
**目標**: 解耦語言檢測邏輯
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
interface LocaleDetector {
|
|
173
|
+
name: string
|
|
174
|
+
priority: number
|
|
175
|
+
detect(c: Context): string | undefined
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 內建檢測器
|
|
179
|
+
const routeParamDetector: LocaleDetector = {
|
|
180
|
+
name: 'routeParam',
|
|
181
|
+
priority: 100,
|
|
182
|
+
detect: (c) => c.req.param('locale')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const queryDetector: LocaleDetector = {
|
|
186
|
+
name: 'query',
|
|
187
|
+
priority: 90,
|
|
188
|
+
detect: (c) => c.req.query('lang')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const headerDetector: LocaleDetector = {
|
|
192
|
+
name: 'acceptLanguage',
|
|
193
|
+
priority: 80,
|
|
194
|
+
detect: (c) => parseAcceptLanguage(c.req.header('Accept-Language'))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const cookieDetector: LocaleDetector = {
|
|
198
|
+
name: 'cookie',
|
|
199
|
+
priority: 85,
|
|
200
|
+
detect: (c) => c.req.cookie('locale')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 配置
|
|
204
|
+
const cosmos = new OrbitCosmos({
|
|
205
|
+
detectors: [
|
|
206
|
+
routeParamDetector,
|
|
207
|
+
cookieDetector,
|
|
208
|
+
headerDetector
|
|
209
|
+
]
|
|
210
|
+
})
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### 2.5 命名空間支援
|
|
214
|
+
|
|
215
|
+
**目標**: 支援大型專案的翻譯組織
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
interface NamespaceConfig {
|
|
219
|
+
defaultNamespace: string
|
|
220
|
+
namespaces: string[]
|
|
221
|
+
loadNamespace?: (locale: string, ns: string) => Promise<TranslationMap>
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 使用
|
|
225
|
+
i18n.t('common:welcome') // 從 common 命名空間
|
|
226
|
+
i18n.t('auth:login.title') // 從 auth 命名空間
|
|
227
|
+
i18n.t('welcome') // 從預設命名空間
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## 架構演進圖
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
現況:
|
|
234
|
+
┌─────────────────┐
|
|
235
|
+
│ OrbitCosmos │
|
|
236
|
+
│ (硬編碼邏輯) │
|
|
237
|
+
└────────┬────────┘
|
|
238
|
+
│
|
|
239
|
+
▼
|
|
240
|
+
┌─────────────────┐
|
|
241
|
+
│ I18nManager │
|
|
242
|
+
│ (單一職責) │
|
|
243
|
+
└─────────────────┘
|
|
244
|
+
|
|
245
|
+
優化後:
|
|
246
|
+
┌─────────────────┐
|
|
247
|
+
│ OrbitCosmos │
|
|
248
|
+
│ (協調者) │
|
|
249
|
+
└────────┬────────┘
|
|
250
|
+
│
|
|
251
|
+
┌────┴────┬────────────┐
|
|
252
|
+
▼ ▼ ▼
|
|
253
|
+
┌────────┐ ┌────────┐ ┌──────────┐
|
|
254
|
+
│Detector│ │Formatter│ │ Manager │
|
|
255
|
+
│Pipeline│ │ ICU │ │ Core │
|
|
256
|
+
└────────┘ └────────┘ └──────────┘
|
|
257
|
+
│
|
|
258
|
+
┌──────┴──────┐
|
|
259
|
+
▼ ▼
|
|
260
|
+
┌────────┐ ┌─────────┐
|
|
261
|
+
│ Cache │ │Namespace│
|
|
262
|
+
│Manager │ │ Loader │
|
|
263
|
+
└────────┘ └─────────┘
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## 實施步驟
|
|
267
|
+
|
|
268
|
+
### Step 1: 型別強化
|
|
269
|
+
- [x] 定義 `NestedKeyOf` 工具型別
|
|
270
|
+
- [x] 建立型別安全的 API
|
|
271
|
+
- [x] 更新 JSDoc 文件
|
|
272
|
+
|
|
273
|
+
### Step 2: 複數形式
|
|
274
|
+
- [x] 實作 `PluralConfig` 介面 (Implicit in logic)
|
|
275
|
+
- [x] 整合 `Intl.PluralRules`
|
|
276
|
+
- [x] 建立測試案例
|
|
277
|
+
|
|
278
|
+
### Step 3: 可插拔檢測器
|
|
279
|
+
- [x] 定義 `LocaleDetector` 介面
|
|
280
|
+
- [x] 實作內建檢測器
|
|
281
|
+
- [x] 重構 `localeMiddleware`
|
|
282
|
+
|
|
283
|
+
### Step 4: ICU 支援 (可選)
|
|
284
|
+
- [ ] 評估 `intl-messageformat` 套件 (Skipped for now)
|
|
285
|
+
- [ ] 實作 `translateICU()` 方法
|
|
286
|
+
- [ ] 效能測試
|
|
287
|
+
|
|
288
|
+
### Step 5: 命名空間
|
|
289
|
+
- [ ] 設計命名空間載入機制 (Skipped/Future)
|
|
290
|
+
- [ ] 實作命名空間解析
|
|
291
|
+
- [ ] 文件更新
|
|
292
|
+
|
|
293
|
+
## 向後相容性
|
|
294
|
+
|
|
295
|
+
| 變更 | 相容性 | 遷移方式 |
|
|
296
|
+
|------|--------|----------|
|
|
297
|
+
| 型別強化 | ✅ 完全相容 | 無需遷移 |
|
|
298
|
+
| 複數形式 | ✅ 完全相容 | 漸進採用 |
|
|
299
|
+
| 可插拔檢測 | ✅ 完全相容 | 預設行為不變 |
|
|
300
|
+
| ICU 支援 | ✅ 完全相容 | 可選功能 |
|
|
301
|
+
| 命名空間 | ⚠️ 需配置 | 提供遷移指南 |
|
|
302
|
+
|
|
303
|
+
## 成功標準
|
|
304
|
+
|
|
305
|
+
- [x] 型別推斷覆蓋主要 API
|
|
306
|
+
- [x] 複數形式支援主要語言
|
|
307
|
+
- [x] 檢測器可自由組合
|
|
308
|
+
- [x] 向後相容性 100%
|
|
309
|
+
- [x] 文件完整更新
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# Phase 3: API 增強計劃
|
|
2
|
+
|
|
3
|
+
> 優先級: P1 | 預估影響: 中
|
|
4
|
+
|
|
5
|
+
## 現況分析
|
|
6
|
+
|
|
7
|
+
### 當前 API
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// OrbitCosmos 配置
|
|
11
|
+
interface I18nConfig {
|
|
12
|
+
defaultLocale: string
|
|
13
|
+
supportedLocales: string[]
|
|
14
|
+
translations?: Record<string, TranslationMap>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// I18nManager API
|
|
18
|
+
class I18nManager {
|
|
19
|
+
translate(locale: string, key: string, replacements?: Record<string, unknown>): string
|
|
20
|
+
addResource(locale: string, translations: TranslationMap): void
|
|
21
|
+
clone(locale?: string): I18nInstance
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// I18nInstance API
|
|
25
|
+
class I18nInstance {
|
|
26
|
+
t(key: string, replacements?: Record<string, unknown>): string
|
|
27
|
+
has(key: string): boolean
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 識別限制
|
|
32
|
+
|
|
33
|
+
1. **缺少批量翻譯 API**
|
|
34
|
+
2. **無法取得所有可用語言**
|
|
35
|
+
3. **缺少翻譯狀態查詢**
|
|
36
|
+
4. **回退策略不可配置**
|
|
37
|
+
5. **缺少 React/Vue 整合輔助**
|
|
38
|
+
|
|
39
|
+
## 優化方案
|
|
40
|
+
|
|
41
|
+
### 3.1 批量翻譯 API
|
|
42
|
+
|
|
43
|
+
**目標**: 減少多次翻譯的函數呼叫開銷
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
interface I18nInstance {
|
|
47
|
+
// 現有
|
|
48
|
+
t(key: string, replacements?: Record<string, unknown>): string
|
|
49
|
+
|
|
50
|
+
// 新增:批量翻譯
|
|
51
|
+
tMany(keys: string[]): Record<string, string>
|
|
52
|
+
tMany(entries: Array<[string, Record<string, unknown>?]>): Record<string, string>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 使用範例
|
|
56
|
+
const translations = i18n.tMany([
|
|
57
|
+
'common.welcome',
|
|
58
|
+
'common.goodbye',
|
|
59
|
+
['greeting', { name: 'Carl' }]
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
// 結果
|
|
63
|
+
{
|
|
64
|
+
'common.welcome': '歡迎',
|
|
65
|
+
'common.goodbye': '再見',
|
|
66
|
+
'greeting': '你好,Carl'
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**實作**:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
class I18nInstance {
|
|
74
|
+
tMany(
|
|
75
|
+
keysOrEntries: string[] | Array<[string, Record<string, unknown>?]>
|
|
76
|
+
): Record<string, string> {
|
|
77
|
+
const result: Record<string, string> = {}
|
|
78
|
+
|
|
79
|
+
for (const item of keysOrEntries) {
|
|
80
|
+
if (typeof item === 'string') {
|
|
81
|
+
result[item] = this.t(item)
|
|
82
|
+
} else {
|
|
83
|
+
const [key, replacements] = item
|
|
84
|
+
result[key] = this.t(key, replacements)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return result
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 3.2 語言狀態查詢 API
|
|
94
|
+
|
|
95
|
+
**目標**: 提供語言相關的查詢能力
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
interface I18nManager {
|
|
99
|
+
// 新增 API
|
|
100
|
+
getLocales(): string[] // 所有已載入的語言
|
|
101
|
+
getSupportedLocales(): string[] // 配置的支援語言
|
|
102
|
+
getDefaultLocale(): string // 預設語言
|
|
103
|
+
isLocaleLoaded(locale: string): boolean // 語言是否已載入
|
|
104
|
+
getLoadedKeys(locale: string): string[] // 已載入的翻譯鍵
|
|
105
|
+
|
|
106
|
+
// 統計
|
|
107
|
+
getStats(): I18nStats
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface I18nStats {
|
|
111
|
+
localesCount: number
|
|
112
|
+
totalKeys: number
|
|
113
|
+
cacheHitRate: number
|
|
114
|
+
cacheSize: number
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 使用
|
|
118
|
+
const stats = i18n.manager.getStats()
|
|
119
|
+
console.log(`快取命中率: ${stats.cacheHitRate}%`)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 3.3 可配置的回退策略
|
|
123
|
+
|
|
124
|
+
**目標**: 支援多層回退和自訂回退邏輯
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
interface FallbackConfig {
|
|
128
|
+
// 語言回退鏈
|
|
129
|
+
fallbackChain?: Record<string, string[]>
|
|
130
|
+
|
|
131
|
+
// 缺失鍵處理
|
|
132
|
+
onMissingKey?: 'key' | 'empty' | 'throw' | ((key: string, locale: string) => string)
|
|
133
|
+
|
|
134
|
+
// 開發模式警告
|
|
135
|
+
warnOnMissing?: boolean
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 配置範例
|
|
139
|
+
const cosmos = new OrbitCosmos({
|
|
140
|
+
defaultLocale: 'en',
|
|
141
|
+
supportedLocales: ['en', 'zh-TW', 'zh-CN', 'ja'],
|
|
142
|
+
fallback: {
|
|
143
|
+
// zh-CN 先回退到 zh-TW,再回退到 en
|
|
144
|
+
fallbackChain: {
|
|
145
|
+
'zh-CN': ['zh-TW', 'en'],
|
|
146
|
+
'zh-TW': ['en'],
|
|
147
|
+
'ja': ['en']
|
|
148
|
+
},
|
|
149
|
+
onMissingKey: (key, locale) => `[${locale}] ${key}`,
|
|
150
|
+
warnOnMissing: process.env.NODE_ENV === 'development'
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**實作**:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
class I18nManager {
|
|
159
|
+
private resolveFallback(locale: string, key: string): string | undefined {
|
|
160
|
+
const chain = this.config.fallback?.fallbackChain?.[locale] ?? [this.config.defaultLocale]
|
|
161
|
+
|
|
162
|
+
for (const fallbackLocale of chain) {
|
|
163
|
+
const value = this.resolveKey(fallbackLocale, key)
|
|
164
|
+
if (value !== undefined) {
|
|
165
|
+
return value
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return undefined
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
translate(locale: string, key: string, replacements?: Record<string, unknown>): string {
|
|
173
|
+
let value = this.resolveKey(locale, key)
|
|
174
|
+
|
|
175
|
+
if (value === undefined) {
|
|
176
|
+
value = this.resolveFallback(locale, key)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (value === undefined) {
|
|
180
|
+
return this.handleMissingKey(key, locale)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return this.replaceParams(value, replacements)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private handleMissingKey(key: string, locale: string): string {
|
|
187
|
+
const handler = this.config.fallback?.onMissingKey ?? 'key'
|
|
188
|
+
|
|
189
|
+
if (this.config.fallback?.warnOnMissing) {
|
|
190
|
+
console.warn(`[i18n] Missing translation: ${key} (${locale})`)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (typeof handler === 'function') {
|
|
194
|
+
return handler(key, locale)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
switch (handler) {
|
|
198
|
+
case 'empty': return ''
|
|
199
|
+
case 'throw': throw new Error(`Missing translation: ${key}`)
|
|
200
|
+
default: return key
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 3.4 響應式整合輔助
|
|
207
|
+
|
|
208
|
+
**目標**: 提供 React/Vue 整合的輔助函數
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// React Hook
|
|
212
|
+
export function useI18n() {
|
|
213
|
+
const context = useContext(I18nContext)
|
|
214
|
+
|
|
215
|
+
const t = useCallback(
|
|
216
|
+
(key: string, replacements?: Record<string, unknown>) => {
|
|
217
|
+
return context.i18n.t(key, replacements)
|
|
218
|
+
},
|
|
219
|
+
[context.i18n]
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
const locale = context.locale
|
|
223
|
+
const setLocale = context.setLocale
|
|
224
|
+
|
|
225
|
+
return { t, locale, setLocale }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Vue Composable
|
|
229
|
+
export function useI18n() {
|
|
230
|
+
const i18n = inject<I18nInstance>('i18n')
|
|
231
|
+
|
|
232
|
+
const t = (key: string, replacements?: Record<string, unknown>) => {
|
|
233
|
+
return i18n?.t(key, replacements) ?? key
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { t }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 導出位置
|
|
240
|
+
export { useI18n } from './integrations/react'
|
|
241
|
+
export { useI18n as useI18nVue } from './integrations/vue'
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### 3.5 翻譯除錯模式
|
|
245
|
+
|
|
246
|
+
**目標**: 開發時期快速識別翻譯問題
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
interface DebugConfig {
|
|
250
|
+
enabled: boolean
|
|
251
|
+
showKeys?: boolean // 顯示翻譯鍵而非翻譯值
|
|
252
|
+
highlight?: boolean // 高亮顯示翻譯文字
|
|
253
|
+
prefix?: string // 翻譯前綴
|
|
254
|
+
suffix?: string // 翻譯後綴
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 配置
|
|
258
|
+
const cosmos = new OrbitCosmos({
|
|
259
|
+
debug: {
|
|
260
|
+
enabled: process.env.NODE_ENV === 'development',
|
|
261
|
+
highlight: true,
|
|
262
|
+
prefix: '🌐',
|
|
263
|
+
suffix: '🌐'
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// 效果
|
|
268
|
+
i18n.t('welcome') // "🌐歡迎🌐"
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### 3.6 動態翻譯 API
|
|
272
|
+
|
|
273
|
+
**目標**: 支援執行時期動態修改翻譯
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
interface I18nManager {
|
|
277
|
+
// 新增 API
|
|
278
|
+
setTranslation(locale: string, key: string, value: string): void
|
|
279
|
+
removeTranslation(locale: string, key: string): void
|
|
280
|
+
clearTranslations(locale?: string): void
|
|
281
|
+
|
|
282
|
+
// 匯入/匯出
|
|
283
|
+
exportTranslations(locale?: string): Record<string, TranslationMap>
|
|
284
|
+
importTranslations(data: Record<string, TranslationMap>, merge?: boolean): void
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 使用範例
|
|
288
|
+
// 從後端載入用戶自訂翻譯
|
|
289
|
+
const customTranslations = await fetchUserTranslations()
|
|
290
|
+
i18n.importTranslations(customTranslations, true)
|
|
291
|
+
|
|
292
|
+
// 即時修正翻譯
|
|
293
|
+
i18n.setTranslation('zh-TW', 'greeting', '您好')
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## API 變更總覽
|
|
297
|
+
|
|
298
|
+
| API | 類型 | 描述 |
|
|
299
|
+
|-----|------|------|
|
|
300
|
+
| `tMany()` | 新增 | 批量翻譯 |
|
|
301
|
+
| `getLocales()` | 新增 | 取得已載入語言 |
|
|
302
|
+
| `getStats()` | 新增 | 取得統計資訊 |
|
|
303
|
+
| `fallbackChain` | 新增配置 | 多層回退 |
|
|
304
|
+
| `onMissingKey` | 新增配置 | 缺失鍵處理 |
|
|
305
|
+
| `useI18n()` | 新增 | React Hook |
|
|
306
|
+
| `setTranslation()` | 新增 | 動態修改翻譯 |
|
|
307
|
+
| `exportTranslations()` | 新增 | 匯出翻譯 |
|
|
308
|
+
|
|
309
|
+
## 實施步驟
|
|
310
|
+
|
|
311
|
+
### Step 1: 批量翻譯
|
|
312
|
+
- [x] 實作 `tMany()` 方法
|
|
313
|
+
- [x] 效能測試
|
|
314
|
+
- [x] 文件撰寫
|
|
315
|
+
|
|
316
|
+
### Step 2: 狀態查詢
|
|
317
|
+
- [x] 實作查詢方法
|
|
318
|
+
- [x] 統計功能
|
|
319
|
+
- [x] 整合快取資訊
|
|
320
|
+
|
|
321
|
+
### Step 3: 回退策略
|
|
322
|
+
- [x] 設計配置介面
|
|
323
|
+
- [x] 實作回退邏輯
|
|
324
|
+
- [x] 測試各種情境
|
|
325
|
+
|
|
326
|
+
### Step 4: 框架整合
|
|
327
|
+
- [ ] React Hook (Skipped - requires separate package/setup)
|
|
328
|
+
- [ ] Vue Composable (Skipped)
|
|
329
|
+
- [ ] 使用範例
|
|
330
|
+
|
|
331
|
+
### Step 5: 除錯模式
|
|
332
|
+
- [ ] 實作除錯選項 (Skipped)
|
|
333
|
+
- [ ] 開發工具整合
|
|
334
|
+
|
|
335
|
+
## 向後相容性
|
|
336
|
+
|
|
337
|
+
所有新增 API 皆為可選功能,現有程式碼無需修改。
|
|
338
|
+
|
|
339
|
+
## 成功標準
|
|
340
|
+
|
|
341
|
+
- [x] 批量翻譯效能優於循環呼叫 30%+
|
|
342
|
+
- [x] 回退策略可完全自訂
|
|
343
|
+
- [ ] React/Vue 整合文件完整
|
|
344
|
+
- [ ] 除錯模式有效協助開發
|
|
345
|
+
- [x] API 文件完整
|