@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.
- package/MIGRATION.md +331 -0
- package/package.json +20 -5
- package/scripts/check-coverage.ts +64 -0
- package/src/HMRWatcher.ts +305 -0
- package/src/I18nService.ts +149 -9
- package/src/index.edge.ts +35 -0
- package/src/index.node.ts +20 -0
- package/src/index.ts +14 -0
- package/src/loader.ts +31 -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/unit/detector.test.ts +1 -6
- package/tests/unit/edge-kv-loader.test.ts +202 -0
- package/tests/unit/hmr.test.ts +255 -0
- package/tests/unit/loader.test.ts +57 -29
- package/tests/unit/loaders.test.ts +332 -0
- package/tests/unit/memory-loader.test.ts +130 -0
- package/tests/unit/path-utils.test.ts +135 -0
- package/tests/unit/runtime-detector.test.ts +86 -0
|
@@ -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
|
+
}
|