@gravito/cosmos 3.1.0 → 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.
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @file packages/cosmos/src/loaders/ChainedLoader.ts
3
+ * @module @gravito/cosmos/loaders
4
+ * @description 組合多個載入器的鏈式載入器實現
5
+ */
6
+
7
+ import type { TranslationMap } from '../I18nService'
8
+ import type { TranslationLoader, TranslationLoaderChain } from './TranslationLoader'
9
+
10
+ /**
11
+ * 鏈式翻譯載入器
12
+ *
13
+ * 組合多個載入器,實現降級策略
14
+ * 當第一個載入器失敗時,自動嘗試下一個載入器
15
+ *
16
+ * @public
17
+ * @since 3.1.0
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * import { ChainedLoader, FileSystemLoader, RemoteLoader } from '@gravito/cosmos'
22
+ *
23
+ * const loader = new ChainedLoader([
24
+ * new FileSystemLoader({ baseDir: './lang' }),
25
+ * new RemoteLoader({ url: 'https://api.example.com/i18n/:locale' }),
26
+ * new MemoryLoader({ fallbackTranslations })
27
+ * ])
28
+ *
29
+ * // 載入順序: FileSystem -> Remote -> Memory
30
+ * const translations = await loader.load('zh-TW')
31
+ * ```
32
+ */
33
+ export class ChainedLoader implements TranslationLoaderChain {
34
+ public readonly name: string
35
+ private loaders: TranslationLoader[]
36
+ private fallbackLoader?: TranslationLoader
37
+
38
+ /**
39
+ * 建立鏈式載入器實例
40
+ *
41
+ * @param loaders - 載入器陣列,按照優先順序排列
42
+ * @param name - 載入器名稱 (可選)
43
+ */
44
+ constructor(loaders: TranslationLoader[], name?: string) {
45
+ this.name = name || 'ChainedLoader'
46
+ this.loaders = loaders
47
+ }
48
+
49
+ /**
50
+ * 載入指定語言的翻譯資源
51
+ *
52
+ * 按順序嘗試每個載入器,直到成功載入或所有載入器都失敗
53
+ *
54
+ * @param locale - 語言代碼
55
+ * @returns 翻譯資源,所有載入器都失敗則返回 null
56
+ */
57
+ async load(locale: string): Promise<TranslationMap | null> {
58
+ // 嘗試每個載入器
59
+ for (const loader of this.loaders) {
60
+ try {
61
+ const result = await loader.load(locale)
62
+ if (result) {
63
+ return result
64
+ }
65
+ } catch (_error) {}
66
+ }
67
+
68
+ // 所有載入器都失敗,嘗試備用載入器
69
+ if (this.fallbackLoader) {
70
+ return this.fallbackLoader.load(locale)
71
+ }
72
+
73
+ return null
74
+ }
75
+
76
+ /**
77
+ * 設定備用載入器
78
+ *
79
+ * @param loader - 備用載入器
80
+ * @returns 當前實例,支援鏈式調用
81
+ */
82
+ fallback(loader: TranslationLoader): TranslationLoaderChain {
83
+ this.fallbackLoader = loader
84
+ return this
85
+ }
86
+
87
+ /**
88
+ * 在鏈的末尾新增載入器
89
+ *
90
+ * @param loader - 要新增的載入器
91
+ * @returns 當前實例,支援鏈式調用
92
+ */
93
+ append(loader: TranslationLoader): ChainedLoader {
94
+ this.loaders.push(loader)
95
+ return this
96
+ }
97
+
98
+ /**
99
+ * 在鏈的開頭插入載入器
100
+ *
101
+ * @param loader - 要插入的載入器
102
+ * @returns 當前實例,支援鏈式調用
103
+ */
104
+ prepend(loader: TranslationLoader): ChainedLoader {
105
+ this.loaders.unshift(loader)
106
+ return this
107
+ }
108
+
109
+ /**
110
+ * 取得載入器陣列
111
+ *
112
+ * @returns 載入器陣列的複本
113
+ */
114
+ getLoaders(): readonly TranslationLoader[] {
115
+ return [...this.loaders]
116
+ }
117
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * @file packages/cosmos/src/loaders/CloudflareKVLoader.ts
3
+ * @module @gravito/cosmos/loaders
4
+ * @description Cloudflare Workers KV 載入器
5
+ */
6
+
7
+ /**
8
+ * Cloudflare Workers KV Namespace 類型聲明
9
+ * @see https://developers.cloudflare.com/workers/runtime-apis/kv/
10
+ */
11
+ declare global {
12
+ interface KVNamespace {
13
+ get(key: string, type?: 'text'): Promise<string | null>
14
+ get(key: string, type: 'json'): Promise<any>
15
+ get(key: string, type: 'arrayBuffer'): Promise<ArrayBuffer | null>
16
+ get(key: string, type: 'stream'): Promise<ReadableStream | null>
17
+ put(key: string, value: string | ArrayBuffer | ArrayBufferView | ReadableStream): Promise<void>
18
+ delete(key: string): Promise<void>
19
+ }
20
+ }
21
+
22
+ import type { TranslationMap } from '../I18nService'
23
+ import type { KVStorage } from './EdgeKVLoader'
24
+ import { EdgeKVLoader } from './EdgeKVLoader'
25
+ import type { LoaderConfig, TranslationLoader, TranslationLoaderChain } from './TranslationLoader'
26
+
27
+ /**
28
+ * Cloudflare Workers KV 適配器
29
+ *
30
+ * 將 Cloudflare KV Namespace 適配到 KVStorage 介面
31
+ *
32
+ * @internal
33
+ * @since 3.2.0
34
+ */
35
+ export class CloudflareKVAdapter implements KVStorage {
36
+ constructor(private namespace: KVNamespace) {}
37
+
38
+ async get(key: string): Promise<string | null> {
39
+ return this.namespace.get(key, 'text')
40
+ }
41
+
42
+ async put(key: string, value: string): Promise<void> {
43
+ await this.namespace.put(key, value)
44
+ }
45
+
46
+ async delete(key: string): Promise<void> {
47
+ await this.namespace.delete(key)
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Cloudflare KV 載入器配置
53
+ *
54
+ * @public
55
+ * @since 3.2.0
56
+ */
57
+ export interface CloudflareKVLoaderConfig extends LoaderConfig {
58
+ /**
59
+ * Cloudflare KV Namespace
60
+ *
61
+ * 在 wrangler.toml 中定義的 KV binding
62
+ *
63
+ * @example
64
+ * ```toml
65
+ * # wrangler.toml
66
+ * [[kv_namespaces]]
67
+ * binding = "I18N_KV"
68
+ * id = "your-namespace-id"
69
+ * ```
70
+ */
71
+ namespace: KVNamespace
72
+
73
+ /**
74
+ * Key 前綴
75
+ *
76
+ * @default 'i18n'
77
+ */
78
+ prefix?: string
79
+
80
+ /**
81
+ * Key 模板
82
+ *
83
+ * @default ':prefix::locale'
84
+ */
85
+ keyTemplate?: string
86
+ }
87
+
88
+ /**
89
+ * Cloudflare Workers KV 載入器
90
+ *
91
+ * 從 Cloudflare Workers KV 載入翻譯資源
92
+ *
93
+ * @public
94
+ * @since 3.2.0
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * // worker.ts
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: 'en',
109
+ * supportedLocales: ['en', 'zh-TW'],
110
+ * loaders: [
111
+ * new CloudflareKVLoader({
112
+ * namespace: env.I18N_KV,
113
+ * prefix: 'translations'
114
+ * })
115
+ * ]
116
+ * })
117
+ *
118
+ * // ... rest of your worker
119
+ * }
120
+ * }
121
+ * ```
122
+ *
123
+ * ## 上傳翻譯到 KV
124
+ *
125
+ * 使用 wrangler 命令上傳翻譯檔案:
126
+ *
127
+ * ```bash
128
+ * # 上傳單個語言
129
+ * wrangler kv:key put --binding I18N_KV "i18n:zh-TW" "$(cat lang/zh-TW.json)"
130
+ *
131
+ * # 批次上傳
132
+ * for file in lang/*.json; do
133
+ * locale=$(basename "$file" .json)
134
+ * wrangler kv:key put --binding I18N_KV "i18n:$locale" "$(cat $file)"
135
+ * done
136
+ * ```
137
+ *
138
+ * ## 特點
139
+ *
140
+ * - **全球邊緣快取**: 翻譯資料快取在全球邊緣節點
141
+ * - **低延遲**: 通常 <100ms 讀取時間
142
+ * - **高可用性**: Cloudflare 的全球網路
143
+ * - **動態更新**: 可以動態更新翻譯而無需重新部署
144
+ */
145
+ export class CloudflareKVLoader implements TranslationLoaderChain {
146
+ private loader: EdgeKVLoader
147
+
148
+ /**
149
+ * 建立 Cloudflare KV 載入器實例
150
+ *
151
+ * @param config - 載入器配置
152
+ */
153
+ constructor(config: CloudflareKVLoaderConfig) {
154
+ const adapter = new CloudflareKVAdapter(config.namespace)
155
+ this.loader = new EdgeKVLoader({
156
+ storage: adapter,
157
+ prefix: config.prefix,
158
+ keyTemplate: config.keyTemplate,
159
+ name: config.name || 'CloudflareKVLoader',
160
+ })
161
+ }
162
+
163
+ get name(): string {
164
+ return this.loader.name
165
+ }
166
+
167
+ async load(locale: string): Promise<TranslationMap | null> {
168
+ return this.loader.load(locale)
169
+ }
170
+
171
+ fallback(loader: TranslationLoader): TranslationLoaderChain {
172
+ this.loader.fallback(loader)
173
+ return this
174
+ }
175
+
176
+ /**
177
+ * 儲存翻譯資源到 Cloudflare KV
178
+ *
179
+ * @param locale - 語言代碼
180
+ * @param translations - 翻譯資源
181
+ */
182
+ async put(locale: string, translations: TranslationMap): Promise<void> {
183
+ return this.loader.put(locale, translations)
184
+ }
185
+
186
+ /**
187
+ * 從 Cloudflare KV 刪除翻譯資源
188
+ *
189
+ * @param locale - 語言代碼
190
+ */
191
+ async delete(locale: string): Promise<void> {
192
+ return this.loader.delete(locale)
193
+ }
194
+ }
@@ -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
+ }