@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.
@@ -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 文件完整