@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.
Files changed (55) hide show
  1. package/MIGRATION.md +331 -0
  2. package/README.md +105 -45
  3. package/README.zh-TW.md +102 -22
  4. package/docs/plans/01-performance.md +187 -0
  5. package/docs/plans/02-architecture.md +309 -0
  6. package/docs/plans/03-api-enhancement.md +345 -0
  7. package/docs/plans/04-testing.md +431 -0
  8. package/docs/plans/README.md +47 -0
  9. package/ion/src/index.js +1179 -1138
  10. package/package.json +22 -6
  11. package/scripts/check-coverage.ts +64 -0
  12. package/src/HMRWatcher.ts +305 -0
  13. package/src/I18nService.ts +715 -91
  14. package/src/index.edge.ts +35 -0
  15. package/src/index.node.ts +20 -0
  16. package/src/index.ts +39 -6
  17. package/src/loader.ts +64 -14
  18. package/src/loaders/ChainedLoader.ts +117 -0
  19. package/src/loaders/CloudflareKVLoader.ts +194 -0
  20. package/src/loaders/EdgeKVLoader.ts +248 -0
  21. package/src/loaders/FileSystemLoader.ts +125 -0
  22. package/src/loaders/MemoryLoader.ts +161 -0
  23. package/src/loaders/RemoteLoader.ts +235 -0
  24. package/src/loaders/TranslationLoader.ts +98 -0
  25. package/src/loaders/VercelKVLoader.ts +192 -0
  26. package/src/runtime/detector.ts +97 -0
  27. package/src/runtime/path-utils.ts +169 -0
  28. package/tests/helpers/factory.ts +41 -0
  29. package/tests/performance/translate.bench.ts +27 -0
  30. package/tests/unit/api.test.ts +37 -0
  31. package/tests/unit/detector.test.ts +65 -0
  32. package/tests/unit/edge-kv-loader.test.ts +202 -0
  33. package/tests/unit/edge.test.ts +100 -0
  34. package/tests/unit/fallback.test.ts +66 -0
  35. package/tests/unit/hmr.test.ts +255 -0
  36. package/tests/unit/lazy.test.ts +35 -0
  37. package/tests/unit/loader.test.ts +72 -0
  38. package/tests/unit/loaders.test.ts +332 -0
  39. package/tests/{manager.test.ts → unit/manager.test.ts} +1 -1
  40. package/tests/unit/memory-loader.test.ts +130 -0
  41. package/tests/unit/path-utils.test.ts +135 -0
  42. package/tests/unit/plural.test.ts +58 -0
  43. package/tests/unit/runtime-detector.test.ts +86 -0
  44. package/tests/{service.test.ts → unit/service.test.ts} +2 -2
  45. package/tsconfig.json +12 -24
  46. package/.turbo/turbo-build.log +0 -20
  47. package/.turbo/turbo-test$colon$ci.log +0 -35
  48. package/.turbo/turbo-test$colon$coverage.log +0 -35
  49. package/.turbo/turbo-test.log +0 -27
  50. package/.turbo/turbo-typecheck.log +0 -2
  51. package/dist/index.cjs +0 -309
  52. package/dist/index.d.cts +0 -274
  53. package/dist/index.d.ts +0 -274
  54. package/dist/index.js +0 -277
  55. package/tests/loader.test.ts +0 -44
@@ -0,0 +1,248 @@
1
+ /**
2
+ * @file packages/cosmos/src/loaders/EdgeKVLoader.ts
3
+ * @module @gravito/cosmos/loaders
4
+ * @description 通用 Edge KV 儲存載入器
5
+ */
6
+
7
+ import type { TranslationMap } from '../I18nService'
8
+ import type { LoaderConfig, TranslationLoader, TranslationLoaderChain } from './TranslationLoader'
9
+
10
+ /**
11
+ * KV 儲存介面(通用抽象)
12
+ *
13
+ * 定義通用的 KV 儲存操作介面
14
+ * 可以被不同的 KV 儲存後端實現
15
+ *
16
+ * @public
17
+ * @since 3.2.0
18
+ */
19
+ export interface KVStorage {
20
+ /**
21
+ * 從 KV 儲存讀取值
22
+ *
23
+ * @param key - 鍵
24
+ * @returns 值,如果不存在則返回 null
25
+ */
26
+ get(key: string): Promise<string | null>
27
+
28
+ /**
29
+ * 寫入值到 KV 儲存(可選)
30
+ *
31
+ * @param key - 鍵
32
+ * @param value - 值
33
+ */
34
+ put?(key: string, value: string): Promise<void>
35
+
36
+ /**
37
+ * 從 KV 儲存刪除值(可選)
38
+ *
39
+ * @param key - 鍵
40
+ */
41
+ delete?(key: string): Promise<void>
42
+ }
43
+
44
+ /**
45
+ * Edge KV 載入器配置
46
+ *
47
+ * @public
48
+ * @since 3.2.0
49
+ */
50
+ export interface EdgeKVLoaderConfig extends LoaderConfig {
51
+ /**
52
+ * KV 儲存實例
53
+ */
54
+ storage: KVStorage
55
+
56
+ /**
57
+ * Key 前綴
58
+ *
59
+ * @default 'i18n'
60
+ */
61
+ prefix?: string
62
+
63
+ /**
64
+ * Key 模板
65
+ *
66
+ * 可用的佔位符:
67
+ * - `:prefix` - Key 前綴
68
+ * - `:locale` - 語言代碼
69
+ *
70
+ * @default ':prefix::locale'
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * // 使用冒號分隔
75
+ * keyTemplate: ':prefix::locale' // 'i18n:zh-TW'
76
+ *
77
+ * // 使用斜線分隔
78
+ * keyTemplate: ':prefix/:locale.json' // 'i18n/zh-TW.json'
79
+ *
80
+ * // 僅使用語言代碼
81
+ * keyTemplate: ':locale' // 'zh-TW'
82
+ * ```
83
+ */
84
+ keyTemplate?: string
85
+ }
86
+
87
+ /**
88
+ * 通用 Edge KV 載入器
89
+ *
90
+ * 支援任何符合 KVStorage 介面的儲存後端
91
+ * 適用於 Edge Runtime 環境的 KV 儲存
92
+ *
93
+ * @public
94
+ * @since 3.2.0
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * // 自訂 KV 儲存實現
99
+ * const customStorage: KVStorage = {
100
+ * async get(key: string) {
101
+ * // 從你的 KV 儲存讀取
102
+ * return await myKV.get(key)
103
+ * }
104
+ * }
105
+ *
106
+ * const loader = new EdgeKVLoader({
107
+ * storage: customStorage,
108
+ * prefix: 'translations',
109
+ * keyTemplate: ':prefix/:locale.json'
110
+ * })
111
+ *
112
+ * const translations = await loader.load('zh-TW')
113
+ * // 讀取 key: 'translations/zh-TW.json'
114
+ * ```
115
+ *
116
+ * ## 適用場景
117
+ *
118
+ * - **Cloudflare Workers KV**: 使用 CloudflareKVLoader
119
+ * - **Vercel KV**: 使用 VercelKVLoader
120
+ * - **自訂 KV 儲存**: 實現 KVStorage 介面
121
+ *
122
+ * ## 優缺點
123
+ *
124
+ * **優點**:
125
+ * - Edge Runtime 原生支援
126
+ * - 低延遲讀取(邊緣快取)
127
+ * - 支援動態更新
128
+ *
129
+ * **缺點**:
130
+ * - 需要額外的儲存服務
131
+ * - 可能有額外成本
132
+ * - 需要預先上傳翻譯到 KV
133
+ */
134
+ export class EdgeKVLoader implements TranslationLoaderChain {
135
+ public readonly name: string
136
+ private storage: KVStorage
137
+ private prefix: string
138
+ private keyTemplate: string
139
+ private fallbackLoader?: TranslationLoader
140
+
141
+ /**
142
+ * 建立 Edge KV 載入器實例
143
+ *
144
+ * @param config - 載入器配置
145
+ */
146
+ constructor(config: EdgeKVLoaderConfig) {
147
+ this.name = config.name || 'EdgeKVLoader'
148
+ this.storage = config.storage
149
+ this.prefix = config.prefix || 'i18n'
150
+ this.keyTemplate = config.keyTemplate || ':prefix::locale'
151
+ }
152
+
153
+ /**
154
+ * 載入指定語言的翻譯資源
155
+ *
156
+ * @param locale - 語言代碼
157
+ * @returns 翻譯資源,載入失敗則返回 null 或嘗試 fallback
158
+ */
159
+ async load(locale: string): Promise<TranslationMap | null> {
160
+ try {
161
+ const key = this.buildKey(locale)
162
+ const value = await this.storage.get(key)
163
+
164
+ if (!value) {
165
+ return this.tryFallback(locale)
166
+ }
167
+
168
+ return JSON.parse(value) as TranslationMap
169
+ } catch (error) {
170
+ console.error(`[EdgeKVLoader] Failed to load ${locale}:`, error)
171
+ return this.tryFallback(locale)
172
+ }
173
+ }
174
+
175
+ /**
176
+ * 建立 KV 儲存的 key
177
+ *
178
+ * @param locale - 語言代碼
179
+ * @returns KV key
180
+ */
181
+ private buildKey(locale: string): string {
182
+ return this.keyTemplate.replace(':prefix', this.prefix).replace(':locale', locale)
183
+ }
184
+
185
+ /**
186
+ * 嘗試使用備用載入器
187
+ *
188
+ * @param locale - 語言代碼
189
+ * @returns 翻譯資源或 null
190
+ */
191
+ private async tryFallback(locale: string): Promise<TranslationMap | null> {
192
+ if (this.fallbackLoader) {
193
+ return this.fallbackLoader.load(locale)
194
+ }
195
+ return null
196
+ }
197
+
198
+ /**
199
+ * 設定備用載入器
200
+ *
201
+ * @param loader - 備用載入器
202
+ * @returns 當前實例,支援鏈式調用
203
+ */
204
+ fallback(loader: TranslationLoader): TranslationLoaderChain {
205
+ this.fallbackLoader = loader
206
+ return this
207
+ }
208
+
209
+ /**
210
+ * 儲存翻譯資源到 KV 儲存(如果儲存支援 put 操作)
211
+ *
212
+ * @param locale - 語言代碼
213
+ * @param translations - 翻譯資源
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * await loader.put('zh-TW', { hello: '你好' })
218
+ * ```
219
+ */
220
+ async put(locale: string, translations: TranslationMap): Promise<void> {
221
+ if (!this.storage.put) {
222
+ throw new Error('[EdgeKVLoader] Storage does not support put operation')
223
+ }
224
+
225
+ const key = this.buildKey(locale)
226
+ const value = JSON.stringify(translations)
227
+ await this.storage.put(key, value)
228
+ }
229
+
230
+ /**
231
+ * 從 KV 儲存刪除翻譯資源(如果儲存支援 delete 操作)
232
+ *
233
+ * @param locale - 語言代碼
234
+ *
235
+ * @example
236
+ * ```typescript
237
+ * await loader.delete('zh-TW')
238
+ * ```
239
+ */
240
+ async delete(locale: string): Promise<void> {
241
+ if (!this.storage.delete) {
242
+ throw new Error('[EdgeKVLoader] Storage does not support delete operation')
243
+ }
244
+
245
+ const key = this.buildKey(locale)
246
+ await this.storage.delete(key)
247
+ }
248
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * @file packages/cosmos/src/loaders/FileSystemLoader.ts
3
+ * @module @gravito/cosmos/loaders
4
+ * @description 從檔案系統載入翻譯資源的實現
5
+ *
6
+ * ⚠️ **Node.js Only**
7
+ *
8
+ * 此載入器依賴 Node.js 的 fs 模組,無法在 Edge Runtime 使用
9
+ *
10
+ * Edge Runtime 替代方案:
11
+ * - MemoryLoader: 靜態翻譯
12
+ * - RemoteLoader: HTTP API
13
+ * - EdgeKVLoader: KV 儲存
14
+ *
15
+ * @since 3.1.0
16
+ * @platform node
17
+ */
18
+
19
+ import { readFile } from 'node:fs/promises'
20
+ import { join } from 'node:path'
21
+ import type { TranslationMap } from '../I18nService'
22
+ import { detectRuntime } from '../runtime/detector'
23
+ import type { LoaderConfig, TranslationLoader, TranslationLoaderChain } from './TranslationLoader'
24
+
25
+ /**
26
+ * 檔案系統載入器配置
27
+ *
28
+ * @public
29
+ * @since 3.1.0
30
+ */
31
+ export interface FileSystemLoaderConfig extends LoaderConfig {
32
+ /**
33
+ * 翻譯檔案所在的基礎目錄
34
+ *
35
+ * @example '/app/lang'
36
+ */
37
+ baseDir: string
38
+
39
+ /**
40
+ * 檔案副檔名
41
+ *
42
+ * @default '.json'
43
+ */
44
+ extension?: string
45
+ }
46
+
47
+ /**
48
+ * 檔案系統翻譯載入器
49
+ *
50
+ * 從本地檔案系統載入 JSON 格式的翻譯檔案
51
+ * 預設尋找 `{baseDir}/{locale}.json` 格式的檔案
52
+ *
53
+ * @public
54
+ * @since 3.1.0
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * const loader = new FileSystemLoader({
59
+ * baseDir: '/app/lang',
60
+ * name: 'fs-loader'
61
+ * })
62
+ *
63
+ * const translations = await loader.load('zh-TW')
64
+ * // 載入 /app/lang/zh-TW.json
65
+ * ```
66
+ */
67
+ export class FileSystemLoader implements TranslationLoaderChain {
68
+ public readonly name: string
69
+ private baseDir: string
70
+ private extension: string
71
+ private fallbackLoader?: TranslationLoader
72
+
73
+ /**
74
+ * 建立檔案系統載入器實例
75
+ *
76
+ * @param config - 載入器配置
77
+ * @throws {Error} 如果在 Edge Runtime 環境中使用
78
+ */
79
+ constructor(config: FileSystemLoaderConfig) {
80
+ // 運行時檢查
81
+ const runtime = detectRuntime()
82
+ if (runtime === 'edge') {
83
+ throw new Error(
84
+ '[FileSystemLoader] This loader requires Node.js and cannot run in Edge Runtime. ' +
85
+ 'Use MemoryLoader, RemoteLoader, or EdgeKVLoader instead.'
86
+ )
87
+ }
88
+
89
+ this.name = config.name || 'FileSystemLoader'
90
+ this.baseDir = config.baseDir
91
+ this.extension = config.extension || '.json'
92
+ }
93
+
94
+ /**
95
+ * 載入指定語言的翻譯資源
96
+ *
97
+ * @param locale - 語言代碼
98
+ * @returns 翻譯資源,載入失敗則返回 null
99
+ */
100
+ async load(locale: string): Promise<TranslationMap | null> {
101
+ try {
102
+ const filePath = join(this.baseDir, `${locale}${this.extension}`)
103
+ const content = await readFile(filePath, 'utf-8')
104
+ const translations = JSON.parse(content) as TranslationMap
105
+ return translations
106
+ } catch (_error) {
107
+ // 如果有備用載入器,嘗試使用備用載入器
108
+ if (this.fallbackLoader) {
109
+ return this.fallbackLoader.load(locale)
110
+ }
111
+ return null
112
+ }
113
+ }
114
+
115
+ /**
116
+ * 設定備用載入器
117
+ *
118
+ * @param loader - 備用載入器
119
+ * @returns 當前實例,支援鏈式調用
120
+ */
121
+ fallback(loader: TranslationLoader): TranslationLoaderChain {
122
+ this.fallbackLoader = loader
123
+ return this
124
+ }
125
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * @file packages/cosmos/src/loaders/MemoryLoader.ts
3
+ * @module @gravito/cosmos/loaders
4
+ * @description 從記憶體載入翻譯資源的實現
5
+ */
6
+
7
+ import type { TranslationMap } from '../I18nService'
8
+ import type { LoaderConfig, TranslationLoader, TranslationLoaderChain } from './TranslationLoader'
9
+
10
+ /**
11
+ * 記憶體載入器配置
12
+ *
13
+ * @public
14
+ * @since 3.2.0
15
+ */
16
+ export interface MemoryLoaderConfig extends LoaderConfig {
17
+ /**
18
+ * 預先載入的翻譯資源
19
+ *
20
+ * Key 為語言代碼,Value 為翻譯 Map
21
+ */
22
+ translations: Record<string, TranslationMap>
23
+ }
24
+
25
+ /**
26
+ * 記憶體翻譯載入器
27
+ *
28
+ * 從記憶體載入預先打包的翻譯資源
29
+ * 適用於 Edge Runtime 環境或靜態翻譯場景
30
+ *
31
+ * @public
32
+ * @since 3.2.0
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * import { MemoryLoader } from '@gravito/cosmos'
37
+ * import en from './lang/en.json'
38
+ * import zhTW from './lang/zh-TW.json'
39
+ *
40
+ * const loader = new MemoryLoader({
41
+ * translations: {
42
+ * en,
43
+ * 'zh-TW': zhTW
44
+ * }
45
+ * })
46
+ *
47
+ * const translations = await loader.load('zh-TW')
48
+ * ```
49
+ *
50
+ * ## 適用場景
51
+ *
52
+ * - **靜態翻譯**: Build-time 打包的翻譯資源
53
+ * - **Edge Runtime**: 無檔案系統的邊緣環境
54
+ * - **測試環境**: 快速的測試資料載入
55
+ * - **小型應用**: 翻譯量較少的應用(<100KB)
56
+ *
57
+ * ## 優缺點
58
+ *
59
+ * **優點**:
60
+ * - 極快的載入速度(記憶體讀取)
61
+ * - 無需網路請求
62
+ * - Edge Runtime 相容
63
+ *
64
+ * **缺點**:
65
+ * - 增加 Bundle Size
66
+ * - 無法動態更新翻譯
67
+ * - 不適合大量翻譯
68
+ */
69
+ export class MemoryLoader implements TranslationLoaderChain {
70
+ public readonly name: string
71
+ private translations: Record<string, TranslationMap>
72
+ private fallbackLoader?: TranslationLoader
73
+
74
+ /**
75
+ * 建立記憶體載入器實例
76
+ *
77
+ * @param config - 載入器配置
78
+ */
79
+ constructor(config: MemoryLoaderConfig) {
80
+ this.name = config.name || 'MemoryLoader'
81
+ this.translations = config.translations
82
+ }
83
+
84
+ /**
85
+ * 載入指定語言的翻譯資源
86
+ *
87
+ * @param locale - 語言代碼
88
+ * @returns 翻譯資源,如果不存在則嘗試使用 fallback loader
89
+ */
90
+ async load(locale: string): Promise<TranslationMap | null> {
91
+ const translations = this.translations[locale]
92
+
93
+ if (translations) {
94
+ return translations
95
+ }
96
+
97
+ // 嘗試使用備用載入器
98
+ if (this.fallbackLoader) {
99
+ return this.fallbackLoader.load(locale)
100
+ }
101
+
102
+ return null
103
+ }
104
+
105
+ /**
106
+ * 設定備用載入器
107
+ *
108
+ * @param loader - 備用載入器
109
+ * @returns 當前實例,支援鏈式調用
110
+ */
111
+ fallback(loader: TranslationLoader): TranslationLoaderChain {
112
+ this.fallbackLoader = loader
113
+ return this
114
+ }
115
+
116
+ /**
117
+ * 新增或更新語言的翻譯資源
118
+ *
119
+ * @param locale - 語言代碼
120
+ * @param translations - 翻譯資源
121
+ *
122
+ * @example
123
+ * ```typescript
124
+ * loader.addTranslations('fr', { hello: 'Bonjour' })
125
+ * ```
126
+ */
127
+ addTranslations(locale: string, translations: TranslationMap): void {
128
+ this.translations[locale] = {
129
+ ...(this.translations[locale] || {}),
130
+ ...translations,
131
+ }
132
+ }
133
+
134
+ /**
135
+ * 移除語言的翻譯資源
136
+ *
137
+ * @param locale - 語言代碼
138
+ */
139
+ removeTranslations(locale: string): void {
140
+ delete this.translations[locale]
141
+ }
142
+
143
+ /**
144
+ * 取得所有已載入的語言列表
145
+ *
146
+ * @returns 語言代碼陣列
147
+ */
148
+ getLoadedLocales(): string[] {
149
+ return Object.keys(this.translations)
150
+ }
151
+
152
+ /**
153
+ * 檢查是否已載入指定語言
154
+ *
155
+ * @param locale - 語言代碼
156
+ * @returns 是否已載入
157
+ */
158
+ hasLocale(locale: string): boolean {
159
+ return locale in this.translations
160
+ }
161
+ }