@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
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file packages/cosmos/src/loaders/RemoteLoader.ts
|
|
3
|
+
* @module @gravito/cosmos/loaders
|
|
4
|
+
* @description 從遠端 HTTP API 載入翻譯資源的實現
|
|
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.1.0
|
|
15
|
+
*/
|
|
16
|
+
export interface RemoteLoaderConfig extends LoaderConfig {
|
|
17
|
+
/**
|
|
18
|
+
* 遠端 API 的 URL 模板
|
|
19
|
+
*
|
|
20
|
+
* 使用 `:locale` 作為語言代碼的佔位符
|
|
21
|
+
*
|
|
22
|
+
* @example 'https://cms.example.com/i18n/:locale'
|
|
23
|
+
*/
|
|
24
|
+
url: string
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* HTTP 請求標頭
|
|
28
|
+
*
|
|
29
|
+
* @example { 'Authorization': 'Bearer token', 'Accept': 'application/json' }
|
|
30
|
+
*/
|
|
31
|
+
headers?: Record<string, string>
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 請求超時時間 (毫秒)
|
|
35
|
+
*
|
|
36
|
+
* @default 10000 (10 秒)
|
|
37
|
+
*/
|
|
38
|
+
timeout?: number
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 重試次數
|
|
42
|
+
*
|
|
43
|
+
* @default 3
|
|
44
|
+
*/
|
|
45
|
+
retries?: number
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 重試延遲 (毫秒)
|
|
49
|
+
*
|
|
50
|
+
* 使用指數退避策略: delay * (2 ^ attempt)
|
|
51
|
+
*
|
|
52
|
+
* @default 1000 (1 秒)
|
|
53
|
+
*/
|
|
54
|
+
retryDelay?: number
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 是否啟用 ETag 快取
|
|
58
|
+
*
|
|
59
|
+
* 啟用後,會在後續請求中發送 If-None-Match 標頭
|
|
60
|
+
* 如果伺服器返回 304 Not Modified,則使用快取的翻譯
|
|
61
|
+
*
|
|
62
|
+
* @default true
|
|
63
|
+
*/
|
|
64
|
+
etagCache?: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 遠端翻譯載入器
|
|
69
|
+
*
|
|
70
|
+
* 從遠端 HTTP API 載入翻譯資源
|
|
71
|
+
* 支援 ETag 快取、重試機制、超時處理
|
|
72
|
+
*
|
|
73
|
+
* @public
|
|
74
|
+
* @since 3.1.0
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const loader = new RemoteLoader({
|
|
79
|
+
* url: 'https://api.example.com/i18n/:locale',
|
|
80
|
+
* headers: { 'Authorization': 'Bearer your-token' },
|
|
81
|
+
* timeout: 5000,
|
|
82
|
+
* retries: 3,
|
|
83
|
+
* etagCache: true
|
|
84
|
+
* })
|
|
85
|
+
*
|
|
86
|
+
* const translations = await loader.load('zh-TW')
|
|
87
|
+
* // GET https://api.example.com/i18n/zh-TW
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export class RemoteLoader implements TranslationLoaderChain {
|
|
91
|
+
public readonly name: string
|
|
92
|
+
private url: string
|
|
93
|
+
private headers: Record<string, string>
|
|
94
|
+
private timeout: number
|
|
95
|
+
private retries: number
|
|
96
|
+
private retryDelay: number
|
|
97
|
+
private etagCache: boolean
|
|
98
|
+
private fallbackLoader?: TranslationLoader
|
|
99
|
+
|
|
100
|
+
/** ETag 快取:locale -> etag */
|
|
101
|
+
private etagMap = new Map<string, string>()
|
|
102
|
+
/** 翻譯快取:locale -> translations (用於 ETag 304 回應) */
|
|
103
|
+
private translationCache = new Map<string, TranslationMap>()
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 建立遠端載入器實例
|
|
107
|
+
*
|
|
108
|
+
* @param config - 載入器配置
|
|
109
|
+
*/
|
|
110
|
+
constructor(config: RemoteLoaderConfig) {
|
|
111
|
+
this.name = config.name || 'RemoteLoader'
|
|
112
|
+
this.url = config.url
|
|
113
|
+
this.headers = config.headers || {}
|
|
114
|
+
this.timeout = config.timeout ?? 10000
|
|
115
|
+
this.retries = config.retries ?? 3
|
|
116
|
+
this.retryDelay = config.retryDelay ?? 1000
|
|
117
|
+
this.etagCache = config.etagCache ?? true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 載入指定語言的翻譯資源
|
|
122
|
+
*
|
|
123
|
+
* @param locale - 語言代碼
|
|
124
|
+
* @returns 翻譯資源,載入失敗則返回 null
|
|
125
|
+
*/
|
|
126
|
+
async load(locale: string): Promise<TranslationMap | null> {
|
|
127
|
+
let _lastError: Error | null = null
|
|
128
|
+
|
|
129
|
+
// 重試邏輯
|
|
130
|
+
for (let attempt = 0; attempt <= this.retries; attempt++) {
|
|
131
|
+
try {
|
|
132
|
+
const result = await this.fetchWithTimeout(locale)
|
|
133
|
+
return result
|
|
134
|
+
} catch (error) {
|
|
135
|
+
_lastError = error as Error
|
|
136
|
+
|
|
137
|
+
// 如果不是最後一次嘗試,等待後重試
|
|
138
|
+
if (attempt < this.retries) {
|
|
139
|
+
const delay = this.retryDelay * 2 ** attempt
|
|
140
|
+
await this.sleep(delay)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 所有重試都失敗,嘗試備用載入器
|
|
146
|
+
if (this.fallbackLoader) {
|
|
147
|
+
return this.fallbackLoader.load(locale)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 發送 HTTP 請求並處理 ETag 快取
|
|
155
|
+
*
|
|
156
|
+
* @param locale - 語言代碼
|
|
157
|
+
* @returns 翻譯資源
|
|
158
|
+
*/
|
|
159
|
+
private async fetchWithTimeout(locale: string): Promise<TranslationMap | null> {
|
|
160
|
+
const url = this.url.replace(':locale', locale)
|
|
161
|
+
const controller = new AbortController()
|
|
162
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout)
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const headers: Record<string, string> = { ...this.headers }
|
|
166
|
+
|
|
167
|
+
// 如果啟用 ETag 快取且有快取的 ETag,加入 If-None-Match 標頭
|
|
168
|
+
if (this.etagCache && this.etagMap.has(locale)) {
|
|
169
|
+
headers['If-None-Match'] = this.etagMap.get(locale)!
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const response = await fetch(url, {
|
|
173
|
+
method: 'GET',
|
|
174
|
+
headers,
|
|
175
|
+
signal: controller.signal,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// 處理 304 Not Modified
|
|
179
|
+
if (response.status === 304) {
|
|
180
|
+
const cached = this.translationCache.get(locale)
|
|
181
|
+
if (cached) {
|
|
182
|
+
return cached
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 處理錯誤狀態碼
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
throw new Error(`HTTP error! status: ${response.status}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const data = (await response.json()) as TranslationMap
|
|
192
|
+
|
|
193
|
+
// 儲存 ETag 和翻譯快取
|
|
194
|
+
if (this.etagCache) {
|
|
195
|
+
const etag = response.headers.get('ETag')
|
|
196
|
+
if (etag) {
|
|
197
|
+
this.etagMap.set(locale, etag)
|
|
198
|
+
this.translationCache.set(locale, data)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return data
|
|
203
|
+
} finally {
|
|
204
|
+
clearTimeout(timeoutId)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 延遲函數
|
|
210
|
+
*
|
|
211
|
+
* @param ms - 延遲時間 (毫秒)
|
|
212
|
+
*/
|
|
213
|
+
private sleep(ms: number): Promise<void> {
|
|
214
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 設定備用載入器
|
|
219
|
+
*
|
|
220
|
+
* @param loader - 備用載入器
|
|
221
|
+
* @returns 當前實例,支援鏈式調用
|
|
222
|
+
*/
|
|
223
|
+
fallback(loader: TranslationLoader): TranslationLoaderChain {
|
|
224
|
+
this.fallbackLoader = loader
|
|
225
|
+
return this
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 清除 ETag 和翻譯快取
|
|
230
|
+
*/
|
|
231
|
+
clearCache(): void {
|
|
232
|
+
this.etagMap.clear()
|
|
233
|
+
this.translationCache.clear()
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file packages/cosmos/src/loaders/TranslationLoader.ts
|
|
3
|
+
* @module @gravito/cosmos/loaders
|
|
4
|
+
* @description 抽象的翻譯載入器介面,支援多種翻譯來源
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TranslationMap } from '../I18nService'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 翻譯載入器介面
|
|
11
|
+
*
|
|
12
|
+
* 定義統一的載入介面,支援多種實現:
|
|
13
|
+
* - FileSystemLoader: 從檔案系統載入
|
|
14
|
+
* - RemoteLoader: 從 HTTP API 載入
|
|
15
|
+
* - DatabaseLoader: 從資料庫載入
|
|
16
|
+
* - EdgeLoader: Edge Runtime 相容的載入器
|
|
17
|
+
*
|
|
18
|
+
* @public
|
|
19
|
+
* @since 3.1.0
|
|
20
|
+
*/
|
|
21
|
+
export interface TranslationLoader {
|
|
22
|
+
/**
|
|
23
|
+
* 載入器名稱,用於識別和除錯
|
|
24
|
+
*/
|
|
25
|
+
readonly name: string
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 載入指定語言的翻譯資源
|
|
29
|
+
*
|
|
30
|
+
* @param locale - 要載入的語言代碼 (如 'en', 'zh-TW')
|
|
31
|
+
* @returns 翻譯資源,如果載入失敗則返回 null
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* const translations = await loader.load('zh-TW')
|
|
36
|
+
* if (translations) {
|
|
37
|
+
* console.log(translations.welcome) // "歡迎"
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
load(locale: string): Promise<TranslationMap | null>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 支援鏈式組合的翻譯載入器介面
|
|
46
|
+
*
|
|
47
|
+
* 允許多個載入器組合,實現降級策略:
|
|
48
|
+
* 主要載入器失敗時,自動嘗試備用載入器
|
|
49
|
+
*
|
|
50
|
+
* @public
|
|
51
|
+
* @since 3.1.0
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const loader = new FileSystemLoader(config)
|
|
56
|
+
* .fallback(new RemoteLoader(remoteConfig))
|
|
57
|
+
* .fallback(new MemoryLoader(fallbackTranslations))
|
|
58
|
+
*
|
|
59
|
+
* // 載入順序: FileSystem -> Remote -> Memory
|
|
60
|
+
* const translations = await loader.load('zh-TW')
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export interface TranslationLoaderChain extends TranslationLoader {
|
|
64
|
+
/**
|
|
65
|
+
* 設定備用載入器
|
|
66
|
+
*
|
|
67
|
+
* 當當前載入器失敗時,會嘗試使用備用載入器
|
|
68
|
+
*
|
|
69
|
+
* @param loader - 備用載入器
|
|
70
|
+
* @returns 鏈式載入器實例,支援繼續串接
|
|
71
|
+
*/
|
|
72
|
+
fallback(loader: TranslationLoader): TranslationLoaderChain
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 載入器配置的基礎介面
|
|
77
|
+
*
|
|
78
|
+
* 所有載入器都應該接受的通用配置選項
|
|
79
|
+
*
|
|
80
|
+
* @public
|
|
81
|
+
* @since 3.1.0
|
|
82
|
+
*/
|
|
83
|
+
export interface LoaderConfig {
|
|
84
|
+
/**
|
|
85
|
+
* 載入器名稱 (可選)
|
|
86
|
+
*/
|
|
87
|
+
name?: string
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 快取策略 (可選)
|
|
91
|
+
*/
|
|
92
|
+
cache?: {
|
|
93
|
+
/** 是否啟用快取 */
|
|
94
|
+
enabled: boolean
|
|
95
|
+
/** 快取過期時間 (毫秒) */
|
|
96
|
+
ttl?: number
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file packages/cosmos/src/loaders/VercelKVLoader.ts
|
|
3
|
+
* @module @gravito/cosmos/loaders
|
|
4
|
+
* @description Vercel KV 載入器
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TranslationMap } from '../I18nService'
|
|
8
|
+
import type { KVStorage } from './EdgeKVLoader'
|
|
9
|
+
import { EdgeKVLoader } from './EdgeKVLoader'
|
|
10
|
+
import type { LoaderConfig, TranslationLoader, TranslationLoaderChain } from './TranslationLoader'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Vercel KV 適配器
|
|
14
|
+
*
|
|
15
|
+
* 將 @vercel/kv 適配到 KVStorage 介面
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
* @since 3.2.0
|
|
19
|
+
*/
|
|
20
|
+
export class VercelKVAdapter implements KVStorage {
|
|
21
|
+
constructor(private kv: any) {} // @vercel/kv 型別
|
|
22
|
+
|
|
23
|
+
async get(key: string): Promise<string | null> {
|
|
24
|
+
return this.kv.get(key)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async put(key: string, value: string): Promise<void> {
|
|
28
|
+
await this.kv.set(key, value)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async delete(key: string): Promise<void> {
|
|
32
|
+
await this.kv.del(key)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Vercel KV 載入器配置
|
|
38
|
+
*
|
|
39
|
+
* @public
|
|
40
|
+
* @since 3.2.0
|
|
41
|
+
*/
|
|
42
|
+
export interface VercelKVLoaderConfig extends LoaderConfig {
|
|
43
|
+
/**
|
|
44
|
+
* Vercel KV 實例
|
|
45
|
+
*
|
|
46
|
+
* 從 @vercel/kv 導入
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* import { kv } from '@vercel/kv'
|
|
51
|
+
*
|
|
52
|
+
* const loader = new VercelKVLoader({ kv })
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
kv: any // @vercel/kv
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Key 前綴
|
|
59
|
+
*
|
|
60
|
+
* @default 'i18n'
|
|
61
|
+
*/
|
|
62
|
+
prefix?: string
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Key 模板
|
|
66
|
+
*
|
|
67
|
+
* @default ':prefix::locale'
|
|
68
|
+
*/
|
|
69
|
+
keyTemplate?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Vercel KV 載入器
|
|
74
|
+
*
|
|
75
|
+
* 從 Vercel KV (基於 Redis) 載入翻譯資源
|
|
76
|
+
*
|
|
77
|
+
* @public
|
|
78
|
+
* @since 3.2.0
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* // app/api/route.ts (Vercel Edge Function)
|
|
83
|
+
* import { OrbitCosmos, VercelKVLoader } from '@gravito/cosmos/edge'
|
|
84
|
+
* import { kv } from '@vercel/kv'
|
|
85
|
+
*
|
|
86
|
+
* const cosmos = new OrbitCosmos({
|
|
87
|
+
* defaultLocale: 'en',
|
|
88
|
+
* supportedLocales: ['en', 'zh-TW'],
|
|
89
|
+
* loaders: [
|
|
90
|
+
* new VercelKVLoader({
|
|
91
|
+
* kv,
|
|
92
|
+
* prefix: 'i18n'
|
|
93
|
+
* })
|
|
94
|
+
* ]
|
|
95
|
+
* })
|
|
96
|
+
*
|
|
97
|
+
* export async function GET() {
|
|
98
|
+
* const i18n = cosmos.clone('zh-TW')
|
|
99
|
+
* await i18n.ensureLocale('zh-TW')
|
|
100
|
+
* return new Response(i18n.t('welcome'))
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* ## 上傳翻譯到 Vercel KV
|
|
105
|
+
*
|
|
106
|
+
* 使用 Vercel CLI 或 API 上傳翻譯:
|
|
107
|
+
*
|
|
108
|
+
* ```typescript
|
|
109
|
+
* // scripts/upload-translations.ts
|
|
110
|
+
* import { kv } from '@vercel/kv'
|
|
111
|
+
* import { readFileSync } from 'fs'
|
|
112
|
+
*
|
|
113
|
+
* const locales = ['en', 'zh-TW', 'ja']
|
|
114
|
+
*
|
|
115
|
+
* for (const locale of locales) {
|
|
116
|
+
* const translations = JSON.parse(
|
|
117
|
+
* readFileSync(`./lang/${locale}.json`, 'utf-8')
|
|
118
|
+
* )
|
|
119
|
+
* await kv.set(`i18n:${locale}`, JSON.stringify(translations))
|
|
120
|
+
* console.log(`Uploaded ${locale}`)
|
|
121
|
+
* }
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* ## 特點
|
|
125
|
+
*
|
|
126
|
+
* - **基於 Redis**: 高效能的記憶體資料庫
|
|
127
|
+
* - **全球複製**: 資料複製到多個區域
|
|
128
|
+
* - **低延遲**: Edge 環境快速存取
|
|
129
|
+
* - **動態更新**: 即時更新翻譯
|
|
130
|
+
* - **TTL 支援**: 可設定過期時間
|
|
131
|
+
*
|
|
132
|
+
* ## 環境變數
|
|
133
|
+
*
|
|
134
|
+
* 需要在 Vercel 專案中設定:
|
|
135
|
+
*
|
|
136
|
+
* ```env
|
|
137
|
+
* KV_URL=your-kv-url
|
|
138
|
+
* KV_REST_API_URL=your-api-url
|
|
139
|
+
* KV_REST_API_TOKEN=your-api-token
|
|
140
|
+
* KV_REST_API_READ_ONLY_TOKEN=your-read-only-token
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export class VercelKVLoader implements TranslationLoaderChain {
|
|
144
|
+
private loader: EdgeKVLoader
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 建立 Vercel KV 載入器實例
|
|
148
|
+
*
|
|
149
|
+
* @param config - 載入器配置
|
|
150
|
+
*/
|
|
151
|
+
constructor(config: VercelKVLoaderConfig) {
|
|
152
|
+
const adapter = new VercelKVAdapter(config.kv)
|
|
153
|
+
this.loader = new EdgeKVLoader({
|
|
154
|
+
storage: adapter,
|
|
155
|
+
prefix: config.prefix,
|
|
156
|
+
keyTemplate: config.keyTemplate,
|
|
157
|
+
name: config.name || 'VercelKVLoader',
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
get name(): string {
|
|
162
|
+
return this.loader.name
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async load(locale: string): Promise<TranslationMap | null> {
|
|
166
|
+
return this.loader.load(locale)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fallback(loader: TranslationLoader): TranslationLoaderChain {
|
|
170
|
+
this.loader.fallback(loader)
|
|
171
|
+
return this
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 儲存翻譯資源到 Vercel KV
|
|
176
|
+
*
|
|
177
|
+
* @param locale - 語言代碼
|
|
178
|
+
* @param translations - 翻譯資源
|
|
179
|
+
*/
|
|
180
|
+
async put(locale: string, translations: TranslationMap): Promise<void> {
|
|
181
|
+
return this.loader.put(locale, translations)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 從 Vercel KV 刪除翻譯資源
|
|
186
|
+
*
|
|
187
|
+
* @param locale - 語言代碼
|
|
188
|
+
*/
|
|
189
|
+
async delete(locale: string): Promise<void> {
|
|
190
|
+
return this.loader.delete(locale)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file packages/cosmos/src/runtime/detector.ts
|
|
3
|
+
* @module @gravito/cosmos/runtime
|
|
4
|
+
* @description 運行環境檢測工具
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 運行環境類型
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
* @since 3.2.0
|
|
12
|
+
*/
|
|
13
|
+
export type RuntimeEnvironment = 'node' | 'edge' | 'unknown'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 檢測當前運行環境
|
|
17
|
+
*
|
|
18
|
+
* 支援的環境:
|
|
19
|
+
* - Node.js (包含 Bun)
|
|
20
|
+
* - Cloudflare Workers
|
|
21
|
+
* - Vercel Edge Functions
|
|
22
|
+
* - Deno Deploy
|
|
23
|
+
* - 其他 Edge Runtime
|
|
24
|
+
*
|
|
25
|
+
* @returns 當前運行環境類型
|
|
26
|
+
* @public
|
|
27
|
+
* @since 3.2.0
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* import { detectRuntime } from '@gravito/cosmos/runtime'
|
|
32
|
+
*
|
|
33
|
+
* const runtime = detectRuntime()
|
|
34
|
+
* if (runtime === 'node') {
|
|
35
|
+
* // 使用 Node.js 特定功能
|
|
36
|
+
* } else if (runtime === 'edge') {
|
|
37
|
+
* // 使用 Edge Runtime 功能
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function detectRuntime(): RuntimeEnvironment {
|
|
42
|
+
// Cloudflare Workers
|
|
43
|
+
// 檢查 Cloudflare Workers 特有的全域物件
|
|
44
|
+
if (
|
|
45
|
+
typeof globalThis.caches !== 'undefined' &&
|
|
46
|
+
typeof (globalThis as any).WebSocketPair !== 'undefined'
|
|
47
|
+
) {
|
|
48
|
+
return 'edge'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Vercel Edge Functions
|
|
52
|
+
// 檢查 Vercel Edge Runtime 特有的全域變數
|
|
53
|
+
if (typeof (globalThis as any).EdgeRuntime !== 'undefined') {
|
|
54
|
+
return 'edge'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Deno Deploy
|
|
58
|
+
// 檢查 Deno 特有的全域物件
|
|
59
|
+
if (typeof (globalThis as any).Deno !== 'undefined') {
|
|
60
|
+
return 'edge'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Node.js (包含 Bun)
|
|
64
|
+
// 檢查 process 物件和 Node.js 版本
|
|
65
|
+
if (typeof process !== 'undefined' && process.versions?.node !== undefined) {
|
|
66
|
+
return 'node'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Bun (當 process.versions.node 不存在時)
|
|
70
|
+
if (typeof process !== 'undefined' && (process.versions as any)?.bun !== undefined) {
|
|
71
|
+
return 'node' // Bun 相容 Node.js API
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return 'unknown'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 檢查是否在 Node.js 環境
|
|
79
|
+
*
|
|
80
|
+
* @returns 是否為 Node.js 環境
|
|
81
|
+
* @public
|
|
82
|
+
* @since 3.2.0
|
|
83
|
+
*/
|
|
84
|
+
export function isNode(): boolean {
|
|
85
|
+
return detectRuntime() === 'node'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 檢查是否在 Edge Runtime 環境
|
|
90
|
+
*
|
|
91
|
+
* @returns 是否為 Edge Runtime 環境
|
|
92
|
+
* @public
|
|
93
|
+
* @since 3.2.0
|
|
94
|
+
*/
|
|
95
|
+
export function isEdge(): boolean {
|
|
96
|
+
return detectRuntime() === 'edge'
|
|
97
|
+
}
|