@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,305 @@
1
+ /**
2
+ * @file packages/cosmos/src/HMRWatcher.ts
3
+ * @module @gravito/cosmos/hmr
4
+ * @description 翻譯檔案的熱重載 (Hot Module Replacement) 監視器
5
+ *
6
+ * ⚠️ **Node.js Only**
7
+ *
8
+ * HMR 功能依賴 Node.js 的 fs.watch,無法在 Edge Runtime 使用
9
+ *
10
+ * Edge Runtime 環境請使用 RemoteLoader 的 ETag 快取機制
11
+ *
12
+ * @since 3.1.0
13
+ * @platform node
14
+ */
15
+
16
+ import type { FSWatcher } from 'node:fs'
17
+ import { watch } from 'node:fs'
18
+ import { join } from 'node:path'
19
+ import { detectRuntime } from './runtime/detector'
20
+
21
+ /**
22
+ * HMR 配置
23
+ *
24
+ * @public
25
+ * @since 3.1.0
26
+ */
27
+ export interface HMRConfig {
28
+ /**
29
+ * 是否啟用 HMR
30
+ *
31
+ * 建議僅在開發模式啟用
32
+ *
33
+ * @default false
34
+ */
35
+ enabled: boolean
36
+
37
+ /**
38
+ * 要監視的目錄路徑陣列
39
+ *
40
+ * @example ['/app/lang', '/app/i18n']
41
+ */
42
+ watchDirs: string[]
43
+
44
+ /**
45
+ * 防抖延遲 (毫秒)
46
+ *
47
+ * 避免短時間內多次觸發重新載入
48
+ *
49
+ * @default 300
50
+ */
51
+ debounce?: number
52
+
53
+ /**
54
+ * 要監視的檔案副檔名
55
+ *
56
+ * @default ['.json']
57
+ */
58
+ extensions?: string[]
59
+
60
+ /**
61
+ * 是否在檔案變更時顯示日誌
62
+ *
63
+ * @default true
64
+ */
65
+ verbose?: boolean
66
+ }
67
+
68
+ /**
69
+ * 翻譯檔案變更事件
70
+ *
71
+ * @public
72
+ * @since 3.1.0
73
+ */
74
+ export interface TranslationChangeEvent {
75
+ /** 變更的檔案路徑 */
76
+ filePath: string
77
+ /** 受影響的語言代碼 */
78
+ locale: string
79
+ /** 變更類型 */
80
+ type: 'change' | 'rename'
81
+ }
82
+
83
+ /**
84
+ * 變更監聽器函數
85
+ *
86
+ * @public
87
+ * @since 3.1.0
88
+ */
89
+ export type ChangeListener = (event: TranslationChangeEvent) => void | Promise<void>
90
+
91
+ /**
92
+ * HMR 監視器
93
+ *
94
+ * 監視翻譯檔案的變更,並在檔案變更時觸發回調
95
+ * 支援防抖、多目錄監視、副檔名篩選等功能
96
+ *
97
+ * @public
98
+ * @since 3.1.0
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * const watcher = new HMRWatcher({
103
+ * enabled: true,
104
+ * watchDirs: ['./lang'],
105
+ * debounce: 300,
106
+ * verbose: true
107
+ * })
108
+ *
109
+ * watcher.onChange(async ({ locale, filePath }) => {
110
+ * console.log(`Reloading ${locale} from ${filePath}`)
111
+ * await i18nManager.reloadLocale(locale)
112
+ * })
113
+ *
114
+ * watcher.start()
115
+ * ```
116
+ */
117
+ export class HMRWatcher {
118
+ private config: Required<HMRConfig>
119
+ private watchers: FSWatcher[] = []
120
+ private listeners: ChangeListener[] = []
121
+ private debounceTimers = new Map<string, NodeJS.Timeout>()
122
+ private isWatching = false
123
+
124
+ /**
125
+ * 建立 HMR 監視器實例
126
+ *
127
+ * @param config - HMR 配置
128
+ */
129
+ constructor(config: HMRConfig) {
130
+ // 運行時檢查 - Edge 環境靜默失敗
131
+ const runtime = detectRuntime()
132
+ if (runtime === 'edge') {
133
+ if (config.verbose ?? true) {
134
+ console.warn(
135
+ '[HMRWatcher] HMR is not supported in Edge Runtime. ' +
136
+ 'Consider using RemoteLoader with ETag caching instead.'
137
+ )
138
+ }
139
+ // 靜默處理,不拋出錯誤
140
+ this.config = {
141
+ enabled: false, // 強制停用
142
+ watchDirs: [],
143
+ debounce: 300,
144
+ extensions: ['.json'],
145
+ verbose: false,
146
+ }
147
+ return
148
+ }
149
+
150
+ this.config = {
151
+ enabled: config.enabled,
152
+ watchDirs: config.watchDirs,
153
+ debounce: config.debounce ?? 300,
154
+ extensions: config.extensions ?? ['.json'],
155
+ verbose: config.verbose ?? true,
156
+ }
157
+ }
158
+
159
+ /**
160
+ * 註冊變更監聽器
161
+ *
162
+ * @param listener - 監聽器函數
163
+ * @returns 當前實例,支援鏈式調用
164
+ */
165
+ onChange(listener: ChangeListener): HMRWatcher {
166
+ this.listeners.push(listener)
167
+ return this
168
+ }
169
+
170
+ /**
171
+ * 移除變更監聽器
172
+ *
173
+ * @param listener - 要移除的監聽器函數
174
+ */
175
+ removeListener(listener: ChangeListener): void {
176
+ const index = this.listeners.indexOf(listener)
177
+ if (index > -1) {
178
+ this.listeners.splice(index, 1)
179
+ }
180
+ }
181
+
182
+ /**
183
+ * 開始監視
184
+ */
185
+ start(): void {
186
+ if (!this.config.enabled) {
187
+ return
188
+ }
189
+
190
+ if (this.isWatching) {
191
+ console.warn('[HMRWatcher] Already watching')
192
+ return
193
+ }
194
+
195
+ for (const dir of this.config.watchDirs) {
196
+ try {
197
+ const watcher = watch(dir, { recursive: false }, (eventType, filename) => {
198
+ if (!filename) {
199
+ return
200
+ }
201
+
202
+ // 檢查副檔名
203
+ const hasValidExtension = this.config.extensions.some((ext) => filename.endsWith(ext))
204
+ if (!hasValidExtension) {
205
+ return
206
+ }
207
+
208
+ // 提取語言代碼 (假設檔案名稱為 {locale}.json)
209
+ const locale = filename.replace(/\.[^/.]+$/, '')
210
+ const filePath = join(dir, filename)
211
+
212
+ this.handleChange({
213
+ filePath,
214
+ locale,
215
+ type: eventType === 'rename' ? 'rename' : 'change',
216
+ })
217
+ })
218
+
219
+ this.watchers.push(watcher)
220
+
221
+ if (this.config.verbose) {
222
+ console.log(`[HMRWatcher] Watching directory: ${dir}`)
223
+ }
224
+ } catch (error) {
225
+ console.error(`[HMRWatcher] Failed to watch directory: ${dir}`, error)
226
+ }
227
+ }
228
+
229
+ this.isWatching = true
230
+ }
231
+
232
+ /**
233
+ * 停止監視
234
+ */
235
+ stop(): void {
236
+ for (const watcher of this.watchers) {
237
+ watcher.close()
238
+ }
239
+
240
+ this.watchers = []
241
+ this.isWatching = false
242
+
243
+ // 清除所有防抖計時器
244
+ for (const timer of this.debounceTimers.values()) {
245
+ clearTimeout(timer)
246
+ }
247
+ this.debounceTimers.clear()
248
+
249
+ if (this.config.verbose) {
250
+ console.log('[HMRWatcher] Stopped watching')
251
+ }
252
+ }
253
+
254
+ /**
255
+ * 處理檔案變更事件
256
+ *
257
+ * @param event - 變更事件
258
+ */
259
+ private handleChange(event: TranslationChangeEvent): void {
260
+ const key = `${event.locale}:${event.type}`
261
+
262
+ // 清除舊的計時器
263
+ if (this.debounceTimers.has(key)) {
264
+ clearTimeout(this.debounceTimers.get(key)!)
265
+ }
266
+
267
+ // 設定新的防抖計時器
268
+ const timer = setTimeout(async () => {
269
+ this.debounceTimers.delete(key)
270
+
271
+ if (this.config.verbose) {
272
+ console.log(`[HMRWatcher] File ${event.type}: ${event.filePath} (${event.locale})`)
273
+ }
274
+
275
+ // 通知所有監聽器
276
+ for (const listener of this.listeners) {
277
+ try {
278
+ await listener(event)
279
+ } catch (error) {
280
+ console.error('[HMRWatcher] Error in change listener:', error)
281
+ }
282
+ }
283
+ }, this.config.debounce)
284
+
285
+ this.debounceTimers.set(key, timer)
286
+ }
287
+
288
+ /**
289
+ * 檢查是否正在監視
290
+ *
291
+ * @returns 是否正在監視
292
+ */
293
+ isActive(): boolean {
294
+ return this.isWatching
295
+ }
296
+
297
+ /**
298
+ * 取得監視的目錄列表
299
+ *
300
+ * @returns 目錄路徑陣列
301
+ */
302
+ getWatchDirs(): readonly string[] {
303
+ return [...this.config.watchDirs]
304
+ }
305
+ }
@@ -5,7 +5,10 @@
5
5
  */
6
6
 
7
7
  import type { GravitoMiddleware } from '@gravito/core'
8
+ import { LRUCache } from 'lru-cache'
9
+ import { type HMRConfig, HMRWatcher } from './HMRWatcher'
8
10
  import { loadLocale } from './loader'
11
+ import type { TranslationLoader } from './loaders/TranslationLoader'
9
12
 
10
13
  /**
11
14
  * Interface for locale detectors used to determine the user's preferred locale from a request context.
@@ -60,6 +63,13 @@ export interface LazyLoadConfig {
60
63
  baseDir: string
61
64
  /** Optional list of locales to preload on startup. */
62
65
  preload?: string[]
66
+ /**
67
+ * Custom loader function for testing or specialized loading strategies.
68
+ * If not provided, defaults to the filesystem loader.
69
+ *
70
+ * @deprecated 自 v3.1.0 起建議使用 loaders 陣列
71
+ */
72
+ loader?: (baseDir: string, locale: string) => Promise<Record<string, string> | null>
63
73
  }
64
74
 
65
75
  /**
@@ -109,6 +119,50 @@ export interface I18nConfig {
109
119
  * Configuration for fallback strategies and missing key handling.
110
120
  */
111
121
  fallback?: FallbackConfig
122
+ /**
123
+ * 翻譯載入器陣列
124
+ *
125
+ * 支援多個載入器的鏈式組合,實現降級策略
126
+ * 當第一個載入器失敗時,會自動嘗試下一個載入器
127
+ *
128
+ * @since 3.1.0
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * import { FileSystemLoader, RemoteLoader } from '@gravito/cosmos'
133
+ *
134
+ * const config: I18nConfig = {
135
+ * defaultLocale: 'zh-TW',
136
+ * supportedLocales: ['zh-TW', 'en'],
137
+ * loaders: [
138
+ * new FileSystemLoader({ baseDir: './lang' }),
139
+ * new RemoteLoader({ url: 'https://api.example.com/i18n/:locale' })
140
+ * ]
141
+ * }
142
+ * ```
143
+ */
144
+ loaders?: TranslationLoader[]
145
+ /**
146
+ * HMR (熱重載) 配置
147
+ *
148
+ * 在開發模式下啟用,可以在翻譯檔案變更時自動重新載入
149
+ *
150
+ * @since 3.1.0
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * const config: I18nConfig = {
155
+ * defaultLocale: 'zh-TW',
156
+ * supportedLocales: ['zh-TW', 'en'],
157
+ * hmr: {
158
+ * enabled: process.env.NODE_ENV === 'development',
159
+ * watchDirs: ['./lang'],
160
+ * debounce: 300
161
+ * }
162
+ * }
163
+ * ```
164
+ */
165
+ hmr?: HMRConfig
112
166
  }
113
167
 
114
168
  /**
@@ -418,17 +472,21 @@ export class I18nManager<Schema = TranslationMap> implements I18nService<Schema>
418
472
  /** Map of translation bundles indexed by locale. */
419
473
  public translations: Record<string, TranslationMap> = {}
420
474
  /** Internal cache for resolved translation strings. */
421
- private cache = new Map<string, string>()
475
+ private cache: LRUCache<string, string>
422
476
  /** Cache for Intl.PluralRules instances. */
423
477
  private pluralRules = new Map<string, Intl.PluralRules>()
424
478
  /** Set of locales that have been successfully loaded. */
425
479
  private loadedLocales = new Set<string>()
480
+ /** Map of pending locale load promises for coalescing. */
481
+ private loadingPromises = new Map<string, Promise<void>>()
426
482
  /** Counter for cache hits. */
427
483
  private cacheHits = 0
428
484
  /** Counter for cache misses. */
429
485
  private cacheMisses = 0
430
486
  /** Default instance for global/CLI usage. */
431
487
  private globalInstance: I18nInstance<Schema>
488
+ /** HMR watcher for development mode. */
489
+ private hmrWatcher?: HMRWatcher
432
490
 
433
491
  /**
434
492
  * Create a new I18nManager.
@@ -439,7 +497,20 @@ export class I18nManager<Schema = TranslationMap> implements I18nService<Schema>
439
497
  if (config.translations) {
440
498
  this.translations = config.translations
441
499
  }
500
+ this.cache = new LRUCache<string, string>({
501
+ max: 10000,
502
+ ttl: 1000 * 60 * 60, // 1 hour
503
+ })
442
504
  this.globalInstance = new I18nInstance(this, config.defaultLocale)
505
+
506
+ // 初始化 HMR
507
+ if (config.hmr?.enabled) {
508
+ this.hmrWatcher = new HMRWatcher(config.hmr)
509
+ this.hmrWatcher.onChange(async ({ locale }) => {
510
+ await this.reloadLocale(locale)
511
+ })
512
+ this.hmrWatcher.start()
513
+ }
443
514
  }
444
515
 
445
516
  // --- I18nService Implementation (Delegates to global instance) ---
@@ -550,17 +621,48 @@ export class I18nManager<Schema = TranslationMap> implements I18nService<Schema>
550
621
  * @param locale - Locale to load.
551
622
  */
552
623
  async ensureLocale(locale: string): Promise<void> {
553
- // If already loaded or no lazy load config, skip
554
- if (this.loadedLocales.has(locale) || !this.config.lazyLoad) {
624
+ // If already loaded, skip
625
+ if (this.loadedLocales.has(locale)) {
555
626
  return
556
627
  }
557
628
 
558
- // Attempt to load
559
- const translations = await loadLocale(this.config.lazyLoad.baseDir, locale)
560
- if (translations) {
561
- this.addResource(locale, translations)
562
- this.loadedLocales.add(locale)
629
+ // Check for pending load promise to coalesce requests
630
+ if (this.loadingPromises.has(locale)) {
631
+ return this.loadingPromises.get(locale)
563
632
  }
633
+
634
+ // Create a new load promise
635
+ const loadPromise = (async () => {
636
+ try {
637
+ let translations: TranslationMap | null = null
638
+
639
+ // 優先使用新的 loaders 配置
640
+ if (this.config.loaders && this.config.loaders.length > 0) {
641
+ // 嘗試每個載入器,直到成功載入
642
+ for (const loader of this.config.loaders) {
643
+ translations = await loader.load(locale)
644
+ if (translations) {
645
+ break
646
+ }
647
+ }
648
+ }
649
+ // 向後相容:使用舊的 lazyLoad 配置
650
+ else if (this.config.lazyLoad) {
651
+ const loaderFn = this.config.lazyLoad.loader || loadLocale
652
+ translations = await loaderFn(this.config.lazyLoad.baseDir, locale)
653
+ }
654
+
655
+ if (translations) {
656
+ this.addResource(locale, translations)
657
+ this.loadedLocales.add(locale)
658
+ }
659
+ } finally {
660
+ this.loadingPromises.delete(locale)
661
+ }
662
+ })()
663
+
664
+ this.loadingPromises.set(locale, loadPromise)
665
+ return loadPromise
564
666
  }
565
667
 
566
668
  // --- Manager Internal API ---
@@ -601,6 +703,11 @@ export class I18nManager<Schema = TranslationMap> implements I18nService<Schema>
601
703
  */
602
704
  private invalidateCache(locale?: string) {
603
705
  if (locale) {
706
+ // LRUCache doesn't support iterating keys efficiently in all versions,
707
+ // but newer versions (v7+) are Map-like.
708
+ // However, iterating over cache to delete by prefix is expensive.
709
+ // For now, we clear everything for simplicity and correctness, or we assume keys are iterable.
710
+ // lru-cache v11 (installed) supports keys().
604
711
  for (const key of this.cache.keys()) {
605
712
  if (key.startsWith(`${locale}:`)) {
606
713
  this.cache.delete(key)
@@ -611,6 +718,37 @@ export class I18nManager<Schema = TranslationMap> implements I18nService<Schema>
611
718
  }
612
719
  }
613
720
 
721
+ /**
722
+ * 重新載入指定語言的翻譯資源
723
+ *
724
+ * 會清除該語言的快取並重新從載入器載入
725
+ * 主要用於 HMR 和手動重新載入場景
726
+ *
727
+ * @param locale - 要重新載入的語言代碼
728
+ * @returns Promise that resolves when reloading is complete
729
+ * @public
730
+ * @since 3.1.0
731
+ *
732
+ * @example
733
+ * ```typescript
734
+ * // 手動重新載入某個語言
735
+ * await i18nManager.reloadLocale('zh-TW')
736
+ *
737
+ * // HMR 自動觸發重新載入
738
+ * hmrWatcher.onChange(({ locale }) => {
739
+ * i18nManager.reloadLocale(locale)
740
+ * })
741
+ * ```
742
+ */
743
+ async reloadLocale(locale: string): Promise<void> {
744
+ // 清除已載入標記
745
+ this.loadedLocales.delete(locale)
746
+ // 清除快取
747
+ this.invalidateCache(locale)
748
+ // 重新載入
749
+ return this.ensureLocale(locale)
750
+ }
751
+
614
752
  /**
615
753
  * Get the plural form for a locale and count.
616
754
  */
@@ -646,7 +784,9 @@ export class I18nManager<Schema = TranslationMap> implements I18nService<Schema>
646
784
 
647
785
  for (const fallbackLocale of chain) {
648
786
  // Avoid infinite recursion if fallback points to itself
649
- if (fallbackLocale === locale) continue
787
+ if (fallbackLocale === locale) {
788
+ continue
789
+ }
650
790
 
651
791
  const value = this.resolveKey(fallbackLocale, key)
652
792
  if (value !== undefined) {
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @file packages/cosmos/src/index.edge.ts
3
+ * @module @gravito/cosmos/edge
4
+ * @description Edge Runtime 入口點
5
+ *
6
+ * 此入口點不包含 Node.js 特定功能:
7
+ * - 不導出 FileSystemLoader (依賴 node:fs)
8
+ * - 不導出 HMRWatcher (依賴 node:fs)
9
+ * - 不導出舊的 loader.ts (依賴 node:fs)
10
+ *
11
+ * Edge Runtime 環境包括:
12
+ * - Cloudflare Workers
13
+ * - Vercel Edge Functions
14
+ * - Deno Deploy
15
+ * - 其他邊緣運行環境
16
+ *
17
+ * @since 3.2.0
18
+ * @platform edge
19
+ */
20
+
21
+ // Core services
22
+ export * from './I18nService'
23
+ export { I18nOrbit, OrbitCosmos } from './index'
24
+ export * from './loaders/ChainedLoader'
25
+ export * from './loaders/CloudflareKVLoader'
26
+ export * from './loaders/EdgeKVLoader'
27
+ export * from './loaders/MemoryLoader'
28
+ export * from './loaders/RemoteLoader'
29
+ // Edge-compatible loaders
30
+ export * from './loaders/TranslationLoader'
31
+ export * from './loaders/VercelKVLoader'
32
+
33
+ // Runtime utilities
34
+ export * from './runtime/detector'
35
+ export * from './runtime/path-utils'
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @file packages/cosmos/src/index.node.ts
3
+ * @module @gravito/cosmos/node
4
+ * @description Node.js 專用入口點
5
+ *
6
+ * 包含完整功能,包括 Node.js 特定功能:
7
+ * - FileSystemLoader
8
+ * - HMRWatcher
9
+ * - 舊的 loader.ts (向後相容)
10
+ *
11
+ * @since 3.2.0
12
+ * @platform node
13
+ */
14
+
15
+ export * from './HMRWatcher'
16
+ // Re-export everything from main index
17
+ export * from './index'
18
+ export * from './loader'
19
+ // Ensure all Node.js features are explicitly available
20
+ export * from './loaders/FileSystemLoader'
package/src/index.ts CHANGED
@@ -77,5 +77,19 @@ export class OrbitCosmos implements GravitoOrbit {
77
77
  */
78
78
  export const I18nOrbit = OrbitCosmos
79
79
 
80
+ export * from './HMRWatcher'
80
81
  export * from './I18nService'
81
82
  export * from './loader'
83
+ export * from './loaders/ChainedLoader'
84
+ export * from './loaders/CloudflareKVLoader'
85
+ export * from './loaders/EdgeKVLoader'
86
+ export * from './loaders/FileSystemLoader'
87
+ export * from './loaders/MemoryLoader'
88
+ export * from './loaders/RemoteLoader'
89
+ // Loaders
90
+ export * from './loaders/TranslationLoader'
91
+ export * from './loaders/VercelKVLoader'
92
+
93
+ // Runtime utilities
94
+ export * from './runtime/detector'
95
+ export * from './runtime/path-utils'
package/src/loader.ts CHANGED
@@ -1,17 +1,37 @@
1
1
  /**
2
2
  * @file packages/cosmos/src/loader.ts
3
3
  * @module @gravito/cosmos/loader
4
- * @description Utilities for loading translation files from the filesystem.
4
+ * @description 載入翻譯檔案的工具函數 (向後相容層)
5
+ *
6
+ * @deprecated 自 v3.1.0 起建議使用 FileSystemLoader 類別
7
+ * 這些函數將在 v4.0.0 中移除
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * // 舊寫法 (deprecated)
12
+ * import { loadLocale } from '@gravito/cosmos/loader'
13
+ * const translations = await loadLocale('/lang', 'zh-TW')
14
+ *
15
+ * // 新寫法 (推薦)
16
+ * import { FileSystemLoader } from '@gravito/cosmos'
17
+ * const loader = new FileSystemLoader({ baseDir: '/lang' })
18
+ * const translations = await loader.load('zh-TW')
19
+ * ```
5
20
  */
6
21
 
7
22
  import { readdir, readFile } from 'node:fs/promises'
8
23
  import { join, parse } from 'node:path'
9
24
 
10
25
  /**
11
- * Load translations from a directory.
26
+ * 從目錄載入所有翻譯檔案
12
27
  *
13
- * Scans the specified directory for JSON files and loads them as translation bundles.
14
- * The filename (without extension) is used as the locale key.
28
+ * 掃描指定目錄中的所有 JSON 檔案並載入為翻譯資源
29
+ * 檔案名稱(不含副檔名)將作為語言代碼
30
+ *
31
+ * @deprecated 自 v3.1.0 起,建議使用 FileSystemLoader
32
+ * @param directory - 翻譯目錄的絕對路徑
33
+ * @returns 語言代碼到翻譯資源的對應表
34
+ * @public
15
35
  *
16
36
  * @example
17
37
  * ```
@@ -19,10 +39,6 @@ import { join, parse } from 'node:path'
19
39
  * /en.json -> { "welcome": "Hello" }
20
40
  * /zh-TW.json -> { "welcome": "你好" }
21
41
  * ```
22
- *
23
- * @param directory - The absolute path to the translations directory.
24
- * @returns A promise that resolves to a map of locale -> translations.
25
- * @public
26
42
  */
27
43
  export async function loadTranslations(
28
44
  directory: string
@@ -45,7 +61,7 @@ export async function loadTranslations(
45
61
  }
46
62
  } catch (_e) {
47
63
  console.warn(
48
- `[Orbit-I18n] Could not load translations from ${directory}. Directory might not exist.`
64
+ `[Cosmos] Could not load translations from ${directory}. Directory might not exist.`
49
65
  )
50
66
  }
51
67
 
@@ -53,13 +69,14 @@ export async function loadTranslations(
53
69
  }
54
70
 
55
71
  /**
56
- * Load translations for a specific locale from a directory.
72
+ * 載入指定語言的翻譯資源
57
73
  *
58
- * Expects a file named `{locale}.json` in the given directory.
74
+ * 期望在指定目錄中找到 `{locale}.json` 格式的檔案
59
75
  *
60
- * @param directory - The directory containing translation files.
61
- * @param locale - The locale string to load.
62
- * @returns A promise that resolves to the translations map, or null if the file could not be read.
76
+ * @deprecated v3.1.0 起,建議使用 FileSystemLoader
77
+ * @param directory - 包含翻譯檔案的目錄
78
+ * @param locale - 要載入的語言代碼
79
+ * @returns 翻譯資源,載入失敗則返回 null
63
80
  * @public
64
81
  */
65
82
  export async function loadLocale(