svelte-tiny-i18n 1.0.2 → 1.1.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/README.zh_tw.md CHANGED
@@ -4,366 +4,148 @@
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
5
  [![Bundle Size](https://img.shields.io/bundlephobia/minzip/svelte-tiny-i18n)](https://bundlephobia.com/package/svelte-tiny-i18n)
6
6
 
7
- ( [English](README.md) | 繁體中文 )
7
+ ( [English](./README.md) | 繁體中文 )
8
8
 
9
- `svelte-tiny-i18n` 是一個為 [Svelte](https://svelte.dev/) 和 [SvelteKit](https://kit.svelte.dev/) 設計的輕量級、型別安全、響應式的 i18n (國際化) 函式庫,完全基於 Svelte Stores 構建。
9
+ `svelte-tiny-i18n` 是一個為 [Svelte](https://svelte.dev/) 和 [SvelteKit](https://kit.svelte.dev/) 設計的輕量級、型別安全且響應式的 i18n 函式庫,完全基於 Svelte Stores 構建。
10
10
 
11
- 本函式庫是「Headless」的,代表它只提供核心的邏輯和 Svelte stores,而將 UI 和組件整合完全交給開發者。
11
+ > **💡 請查看 [範例目錄 (Examples)](./examples/README.zh_tw.md) 以查看所有型別安全模式的用法!**
12
12
 
13
- ## TL;DR
13
+ ## 核心價值 (TL;DR)
14
14
 
15
- `svelte-tiny-i18n` 是為那些**追求極致輕量、零依賴、零建構設定**,同時又**享受 Svelte 原生 Store 體驗和 TypeScript 即時型別推斷**的開發者設計的。
15
+ `svelte-tiny-i18n` 專為那些重視 **極致輕量、零依賴且零建構設定**,同時仍希望享受 **Svelte store 響應式體驗與 TypeScript 即時推斷** 的開發者所設計。
16
16
 
17
- 它的核心優勢是:**零設定的型別安全**。您只需在 `i18n.ts` 設定檔中定義支援的語言(例如 `['en', 'es']`),TypeScript 立刻就能為您的 `setLocale` 函式提供即時的型別檢查與自動補全(例如 `setLocale('es')` 合法,`setLocale('fr')` 會報錯)—— 完全不需執行任何程式碼產生器。
17
+ 我們最大的優勢:**混合型別安全**。
18
18
 
19
- 這使它成為中小型專案、或Svelte生態愛好者的理想選擇。
20
-
21
- ### 範例:最精簡的 SvelteKit 整合
22
-
23
- 假設專案的目錄結構如下:
24
-
25
- ```tree
26
- /src
27
- ├── /lib
28
- │ └── i18n.ts <- 1. 設定檔
29
- └── /routes
30
- └── /[lang]
31
- ├── +layout.ts <- 2. SvelteKit 整合
32
- ├── +page.svelte <- 3. 使用
33
- └── /about
34
- └── +page.svelte
35
- ```
36
-
37
- 1. **`src/lib/i18n.ts`** (設定檔)
38
-
39
- ```ts
40
- import { createI18nStore, defineI18nConfig } from 'svelte-tiny-i18n';
41
-
42
- const config = defineI18nConfig({
43
- supportedLocales: ['en', 'es'],
44
- defaultLocale: 'en',
45
- localStorageKey: 'lang',
46
- initialTranslations: [
47
- {
48
- hello: { en: 'Hello', es: 'Hola' }
49
- }
50
- ]
51
- });
52
-
53
- export const i18n = createI18nStore(config);
54
- export type SupportedLocale = inferSupportedLocale<typeof i18n>;
55
- ```
56
-
57
- 2. **`src/routes/[lang]/+layout.ts`** (SvelteKit 整合)
58
-
59
- ```ts
60
- import { i18n } from '$lib/i18n';
61
- import type { LayoutLoad } from './$types';
62
-
63
- export const load: LayoutLoad = ({ params }) => {
64
- // 'lang' 來自您的路由 e.g. /[lang]/
65
- i18n.setLocale(params.lang);
66
- return {};
67
- };
68
- ```
69
-
70
- 3. **`src/routes/[lang]/+page.svelte`** (使用)
71
-
72
- ```svelte
73
- <script lang="ts">
74
- import { i18n } from '$lib/i18n';
75
- const { t, setLocale } = i18n;
76
- </script>
77
-
78
- <h1>{$t('hello')}</h1>
79
- <button on:click={() => setLocale('es')}>Español</button>
80
- ```
81
-
82
- ## 安裝
83
-
84
- ```bash
85
- npm install svelte-tiny-i18n
86
- ```
87
-
88
- ```bash
89
- pnpm add svelte-tiny-i18n
90
- ```
91
-
92
- ```bash
93
- yarn add svelte-tiny-i18n
94
- ```
19
+ 1. **設定即推斷**: 對於靜態翻譯,Key 會自動推斷。
20
+ 2. **全域擴充**: 對於動態載入,只需在 `d.ts` 中指向您的檔案,全域即可獲得型別提示。
21
+ 3. **零設定作用域**: 或者,直接從 `extendTranslations` 取得一個已具備型別的 Store。
95
22
 
96
23
  ## 快速上手
97
24
 
98
- 使用 `svelte-tiny-i18n` 的最佳方式是建立一個專用的單一實例。
99
-
100
25
  ### 1. 建立 i18n 實例
101
26
 
102
- 在 `/src/lib/i18n.ts` (或開發者偏好的位置) 建立一個檔案。
103
-
104
27
  ```ts
105
28
  // /src/lib/i18n.ts
106
- import {
107
- createI18nStore,
108
- defineI18nConfig,
109
- type inferSupportedLocale,
110
- type inferPartialTranslationEntry,
111
- type inferTranslationEntry
112
- } from 'svelte-tiny-i18n';
113
-
114
- // 1. 定義設定
29
+ import { createI18nStore, defineI18nConfig } from 'svelte-tiny-i18n';
30
+
115
31
  const i18nConfig = defineI18nConfig({
116
- // 定義所有支援的語言
117
32
  supportedLocales: ['en', 'es', 'zh-TW'],
118
-
119
- // 預設語言
120
33
  defaultLocale: 'en',
121
-
122
- // 用於在 localStorage 中儲存語言的鍵
123
34
  localStorageKey: 'my-app-language',
124
35
 
125
- // (可選) 如果找不到翻譯鍵,在控制台顯示警告
126
- // 預設為 true
127
- devLogs: true,
36
+ // (可選) 自訂錯誤處理
37
+ // 例如:在生產環境發送至 Sentry
38
+ onError: (err) => {
39
+ console.error('i18n error:', err.type, err.key);
40
+ },
128
41
 
129
- // 定義初始、全域翻譯
42
+ // 1. 完全支援巢狀 JSON
43
+ // 2. TypeScript 會自動推斷這些 Keys!
130
44
  initialTranslations: [
131
45
  {
132
- hello: {
133
- en: 'Hello, {name}!',
134
- es: '¡Hola, {name}!',
135
- 'zh-TW': '你好, {name}!'
136
- },
137
- goodbye: {
138
- en: 'Goodbye',
139
- es: 'Adiós',
140
- 'zh-TW': '再見'
46
+ hello: { en: 'Hello', 'zh-TW': '你好' },
47
+ home: {
48
+ title: { en: 'Home Page' },
49
+ btn: { en: 'Click Me' }
141
50
  }
142
51
  }
143
52
  ]
144
53
  });
145
54
 
146
- // 2. 建立並導出 i18n 實例
147
55
  export const i18n = createI18nStore(i18nConfig);
148
-
149
- // 3. (可選) 導出推斷的型別,以實現全站的型別安全
150
- export type SupportedLocale = inferSupportedLocale<typeof i18n>;
151
- export type TranslationEntry = inferTranslationEntry<typeof i18n>;
152
- export type PartialTranslationEntry = inferPartialTranslationEntry<typeof i18n>;
153
56
  ```
154
57
 
155
58
  ### 2. 在 Svelte 組件中使用
156
59
 
157
- 使用衍生的 store `$t` 來獲取翻譯,並使用 `locale` 來讀取、`setLocale` store 來設定語言。
158
-
159
60
  ```svelte
160
61
  <script lang="ts">
161
62
  import { i18n } from '$lib/i18n';
162
-
163
- // 解構 stores 和函式
164
63
  const { t, locale, setLocale } = i18n;
165
64
  </script>
166
65
 
167
- <h1>{$t('hello', { name: 'World' })}</h1>
168
-
169
- <nav>
170
- <p>目前語言: {$locale}</p>
66
+ <h1>{$t('hello')}</h1>
67
+ <p>{$t('home.title')}</p>
171
68
 
172
- <button on:click={() => setLocale('en')}>English</button>
173
- <button on:click={() => setLocale('es')}>Español</button>
174
- <button on:click={() => setLocale('zh-TW')}>繁體中文</button>
175
- </nav>
176
-
177
- <p>{$t('a.missing.key')}</p>
69
+ <button on:click={() => setLocale('zh-TW')}> 中文 </button>
178
70
  ```
179
71
 
180
- ### 3. 與 SvelteKit 整合 (建議)
72
+ ## 進階用法:動態載入與型別
181
73
 
182
- 為了讓 i18n 狀態在伺服器和客戶端都可用,並從 URL 參數 (例如 `/es/about`) 初始化,請在根 `+layout.ts` 中使用它。
74
+ 當您需要動態載入翻譯(使用 `extendTranslations`)時,`svelte-tiny-i18n` 提供了兩種強大的型別策略。
183
75
 
184
- ```ts
185
- // /src/routes/+layout.ts
186
- import { i18n } from '$lib/i18n';
187
- import type { LayoutLoad } from './$types';
188
-
189
- // 這個 load 函式會在 SSR 和 CSR 上都運行
190
- export const load: LayoutLoad = ({ params }) => {
191
- // 'lang' 必須匹配路由參數,例如 /[lang]/
192
- const { lang } = params;
193
-
194
- // setLocale 函式會驗證語言
195
- // 並設定 'locale' store。
196
- i18n.setLocale(lang);
197
-
198
- // 開發者可以選擇性地回傳 lang,但 store 本身已被設定
199
- return { lang };
200
- };
201
- ```
76
+ ### 策略 A:全域擴充 (Global Augmentation) - 推薦
202
77
 
203
- **注意:** 您的 SvelteKit 路由結構必須類似 `/src/routes/[lang]/...` 才能使 `params.lang` 可用。
78
+ 讓您的 `$t` 保持全域可用,但透過 TypeScript `typeof import` 自動化型別定義。
204
79
 
205
- ## 進階用法
206
-
207
- ### 動態 (非同步) 翻譯載入
208
-
209
- 您不需要在一開始就載入所有的翻譯。可以在頁面的 `+page.ts` 或 `+layout.ts` 中使用 `extendTranslations` 來按需載入。
210
-
211
- 1. 定義特定頁面的翻譯:
80
+ 1. 建立 `src/i18n.d.ts`:
212
81
 
213
82
  ```ts
214
- // /src/locales/profile.ts
215
- import type { PartialTranslationEntry } from '$lib/i18n';
216
-
217
- export const profileTranslations: PartialTranslationEntry = {
218
- 'profile.title': {
219
- en: 'My Profile',
220
- es: 'Mi Perfil',
221
- 'zh-TW': '個人資料'
222
- },
223
- 'profile.edit_button': {
224
- en: 'Edit'
225
- // 'es' 和 'zh-TW' 缺失是允許的!
83
+ import 'svelte-tiny-i18n';
84
+
85
+ declare module 'svelte-tiny-i18n' {
86
+ export interface TinyI18nTranslations {
87
+ // 只需要指向您的翻譯檔案!
88
+ profile: typeof import('./locales/profile.json');
89
+ dashboard: typeof import('./features/dashboard/locales').default;
226
90
  }
227
- };
91
+ }
228
92
  ```
229
93
 
230
- 2. 在頁面的 loader 中載入它們:
94
+ 2. 現在 `$t('profile.name')` 在整個應用程式中都能獲得型別檢查,即便該檔案尚未載入!
231
95
 
232
- ```ts
233
- // /src/routes/profile/+page.ts
234
- import { i18n } from '$lib/i18n';
235
- import { profileTranslations } from '$locales/profile';
236
- import type { PageLoad } from './$types';
237
-
238
- export const load: PageLoad = () => {
239
- // 動態地為此頁面添加翻譯
240
- i18n.extendTranslations([profileTranslations]);
96
+ ### 策略 B:零設定 (Local Scope)
241
97
 
242
- // 您也可以 await 非同步載入
243
- // const { jsonTranslations } = await import('$locales/profile.json');
244
- // i18n.extendTranslations([jsonTranslations]);
245
- };
246
- ```
98
+ 不想碰 `d.ts`?沒問題。`extendTranslations` 會回傳一個專為新內容設定好型別的 Store。
247
99
 
248
- 新的翻譯現在已被合併到 store 中,並可透過 `$t` 函式使用。
100
+ ```ts
101
+ // /src/routes/profile/+page.svelte
102
+ import { profileTranslations } from './locales';
249
103
 
250
- ## 核心優勢
104
+ // 回傳的 't' 已經立即知道 'profile.*' 的 key
105
+ const { t } = i18n.extendTranslations([profileTranslations]);
251
106
 
252
- `svelte-tiny-i18n` 旨在為 Svelte 開發者提供一個「最佳平衡點」—— 一個簡單、快速且型別安全的解決方案,而無需大型函式庫的額外開銷。
107
+ $t('profile.title'); // Typed!
108
+ ```
253
109
 
254
- - **零設定的型別安全 (Zero-Config Type Safety)**: 您**不需**任何建構步驟 (build step)、程式碼產生器或複雜設定,就能立即獲得對語言代碼的完整型別安全。型別安全是透過 TypeScript 對單一設定檔的自動推斷 (inference) 來實現的。
255
- - **極致輕量 (Extremely Lightweight)**: 這個函式庫非常「微小」(gzipped 後約 <1kb)。它**沒有任何外部依賴**,也不包含沉重的 ICU 訊息解析器 (message parser),使您的應用程式啟動更快。
256
- - **Svelte 原生 (Svelte Native)**: 完全基於 Svelte stores (`writable` 和 `derived`) 構建,使其無縫融入 Svelte 的響應式模型。
257
- - **簡單而強大 (Simple but Powerful)**: 提供了所有基本功能:支援 SvelteKit 的 SSR/CSR、動態/非同步翻譯載入 (`extendTranslations`),以及簡單的變數替換。
258
- - **Headless 設計**: 函式庫只提供核心邏輯,讓您對 UI 整合擁有完全的控制權。(這也代表您需要自行更新 `<html>` 上的 `lang` 屬性。請參閱 [FAQ 中的範例](#q-如何動態更新-html-上的-lang-屬性或處理-rtl-由右至左-語言)。)
110
+ 完整程式碼請參閱 [**範例 2: 全域擴充**](./examples/2-global-augmentation.ts) [**範例 3: 零設定**](./examples/3-zero-config.ts)
259
111
 
260
112
  ## 與其他函式庫比較
261
113
 
262
- | 維度 | `svelte-tiny-i18n` (本專案) | `typesafe-i18n` | `svelte-i18n` |
263
- | :------------- | :------------------------------------------------------ | :------------------------------------------------ | :----------------------------------------------- |
264
- | **檔案大小** | **極小 (<1kb)** | **極小 (~1kb)** | **中等 (~15kb+)** |
265
- | **核心機制** | 零依賴的 Svelte Stores + 簡單字串替換 | **建構期 (Build-time) 產生器** | **執行期 (Runtime) ICU 解析器** |
266
- | **型別安全** | **高 (即時推斷)** | **極高 (程式碼產生)** | 中 (需手動定義) |
267
- | **設定複雜度** | **非常低** (單一設定檔) | 中 (需設定並執行產生器) | 低 (安裝即用) |
268
- | **進階格式化** | 僅支援 `{var}` 變數替換 | 支援 (複數、日期、數字格式化) | **支援 (完整 ICU 語法)** |
269
- | **主要取捨** | 捨棄 ICU 功能,換取**極致輕量**與**零設定的型別安全**。 | 需額外建構步驟,換取**最強的型別安全** (含參數)。 | 需載入 runtime 解析器,換取**最強的 ICU 功能**。 |
114
+ | 維度 (Dimension) | `svelte-tiny-i18n` (本專案) | `typesafe-i18n` | `svelte-i18n` |
115
+ | :--------------- | :---------------------------------------------------------- | :------------------------------------------------ | :----------------------------------------------- |
116
+ | **檔案大小** | **極小 (<1kb)** | **極小 (~1kb)** | **中等 (~15kb+)** |
117
+ | **核心機制** | 零依賴的 Svelte Stores + 簡單字串替換 | **建構期 (Build-time) 產生器** | **執行期 (Runtime) ICU 解析器** |
118
+ | **型別安全** | **混合式 (推斷 + 擴充)** | **極高 (程式碼產生)** | 中 (需手動定義) |
119
+ | **設定複雜度** | **非常低** (單一設定檔) | 中 (需設定並執行產生器) | 低 (安裝即用) |
120
+ | **進階格式化** | 僅支援 `{var}` 變數替換 | 支援 (複數、日期、數字格式化) | **支援 (完整 ICU 語法)** |
121
+ | **主要取捨** | 捨棄 ICU 功能,換取**極致輕量**與**零設定**的型別安全體驗。 | 需額外建構步驟,換取**最強的型別安全** (含參數)。 | 需載入 runtime 解析器,換取**最強的 ICU 功能**。 |
270
122
 
271
123
  ### 設計理念比較
272
124
 
273
- - 如果您需要**進階格式化**(例如複雜的複數、日期/數字本地化),且不介意較大的檔案體積,**`svelte-i18n`** 是很好的選擇。它使用業界標準的 `formatjs` 和 ICU 語法。
274
- - 如果您需要**絕對的型別安全**(包含翻譯函式的「參數」也需要型別檢查,例如 `$t('key', { arg: 'val' })`),並且願意設定一個程式碼產生器,**`typesafe-i18n`** 非常出色。
275
- - 如果您最重視以下幾點,**`svelte-tiny-i18n`** 將是理想的選擇:
276
- 1. **簡單易用**:2 分鐘內即可開始使用。
277
- 2. **檔案體積**:極小的 bundle size 是首要考量。
278
- 3. **毫不費力的型別安全**:您希望**不需**任何建構步驟,就能獲得強大的型別保障。
279
-
280
- 本函式庫刻意捨棄了「ICU 進階格式化」(`svelte-i18n` 的強項)和「參數級別的型別安全」(`typesafe-i18n` 的強項),以換取**極致的簡單性、最小的體積、以及零設定的型別安全**。
281
-
282
- ## API 參考
283
-
284
- ### 工廠函式
285
-
286
- #### `createI18nStore(config)`
125
+ - 如果您需要**進階格式化**(例如複雜的複數、日期/數字本地化),且不介意較大的檔案體積,**`svelte-i18n`** 是很好的選擇。
126
+ - 如果您需要**絕對的型別安全**(包含翻譯函式的「參數」也需要型別檢查),並且願意設定一個程式碼產生器,**`typesafe-i18n`** 非常出色。
127
+ - **`svelte-tiny-i18n`** 是 **極簡主義者** 的理想選擇,專注於 **簡單性**、**最小體積** 與 **毫不費力的型別安全** (無需 Build Step)。
287
128
 
288
- 建立核心的 i18n 實例。回傳一個包含 stores 和函式的物件。
289
-
290
- #### `defineI18nConfig(config)`
291
-
292
- 一個輔助函式,用於定義 `I18nConfig`,並提供完整的型別安全和推斷。
293
-
294
- ### 回傳的實例 (`i18n`)
295
-
296
- 當您呼叫 `createI18nStore` 時,您會得到一個物件:
297
-
298
- - `t`: (唯讀的 derived store) 翻譯函式。
299
- - `$t('key')`
300
- - `$t('key', { placeholder: 'value' })`
301
- - `locale`: (Readable store) 當前啟用的語言代碼 (例如 `en`)。此 store 是唯讀的;若要更新它,請使用 `setLocale()` 函式。
302
- - `setLocale(lang: string | null | undefined)`: 一個用於安全設定初始語言的函式,通常在根 `+layout.ts` 中呼叫。
303
- - 如果 `lang` 是支援的語言,它將設定 `locale` store。
304
- - 如果 `lang` 是無效的 (或 `null`/`undefined`),它將被忽略,`locale` store 會**保持其目前的值**。
305
- - `extendTranslations(newTranslations: PartialTranslationEntry[])`: 將新的翻譯 (一個 _陣列_) 合併到主 store 中並觸發更新。
306
- - `supportedLocales`: (唯讀 `readonly string[]`) 來自您設定檔的支援語言陣列。
307
- - `defaultLocale`: (唯讀 `string`) 來自您設定檔的預設語言。
308
- - `localStorageKey`: (唯讀 `string`) 來自您設定檔的 `localStorage` 鍵。
309
-
310
- ### 型別輔助工具 (Type Helpers)
311
-
312
- 為了在應用程式中實現穩健的型別安全,您可以直接從 `svelte-tiny-i18n` 導入型別輔助工具。
313
-
314
- - `inferSupportedLocale<typeof i18n>`: 推斷出支援的語言代碼聯集 (例如 `'en' | 'es' | 'zh-TW'`)。
315
- - `inferTranslationEntry<typeof i18n>`: 推斷出*完整*的翻譯條目型別 (例如 `{ en: string; es: string; 'zh-TW': string; }`)。
316
- - `inferPartialTranslationEntry<typeof i18n>`: 推斷出翻譯檔案的型別 (例如 `{ [key: string]: { en?: string; es?: string; 'zh-TW'?: string; } }`)。
317
-
318
- **範例:**
319
-
320
- ```ts
321
- // /src/lib/i18n.ts
322
- // ... (如「快速上手」中所示)
323
- export type SupportedLocale = inferSupportedLocale<typeof i18n>;
324
- ```
325
-
326
- ```ts
327
- // /src/components/SomeComponent.svelte
328
- import { i18n } from '$lib/i18n';
329
- import type { SupportedLocale } from '$lib/i18n';
330
-
331
- // 'lang' 變數現在受到型別檢查
332
- function setLanguage(lang: SupportedLocale) {
333
- i18n.setLocale(lang);
334
- }
335
-
336
- setLanguage('en'); // OK
337
- setLanguage('fr'); // TypeScript 錯誤
338
- ```
339
-
340
- ## FAQ
129
+ ## 常見問答 (FAQ)
341
130
 
342
131
  ### Q: 如何在**不使用 SvelteKit** 的 Svelte (Vite) 專案中使用?
343
132
 
344
133
  A: 這樣更簡單。您不需要 `+layout.ts` 和 `i18n.setLocale()` 步驟。
345
-
346
134
  Store 在瀏覽器環境中會自動透過 `localStorage` 或 `navigator.language` 來偵測並初始化語言。您可以在組件中隨時呼叫 `i18n.setLocale('new_lang')` 來切換語言。
347
135
 
348
136
  ### Q: 如何動態更新 `<html>` 上的 `lang` 屬性,或處理 RTL (由右至左) 語言?
349
137
 
350
- A: 本函式庫是「Headless」設計,代表它不會主動操作 DOM。您可以輕鬆地在根佈局組件 (SvelteKit 的 `+layout.svelte` 或 Svelte/Vite 的 `App.svelte`) 中訂閱 `locale` store 來自行管理。
351
-
352
- 這是一個 SvelteKit 專案的範例,它會同時設定 `lang` 和 `dir` 屬性:
138
+ A: 本函式庫是「Headless」設計,代表它不會主動操作 DOM。您可以輕鬆地在根佈局組件中自行管理。
353
139
 
354
140
  ```svelte
355
141
  <script lang="ts">
356
142
  import { i18n } from '$lib/i18n';
357
143
  const { locale } = i18n;
358
144
 
359
- // 定義您支援的語言中有哪些是 RTL
360
- // (例如阿拉伯語 'ar', 希伯來語 'he')
361
145
  const rtlLocales: string[] = ['ar', 'he'];
362
146
 
363
147
  $: if (typeof document !== 'undefined') {
364
148
  const direction = rtlLocales.includes($locale) ? 'rtl' : 'ltr';
365
-
366
- // 動態設定 <html> 上的屬性
367
149
  document.documentElement.lang = $locale;
368
150
  document.documentElement.dir = direction;
369
151
  }
@@ -372,6 +154,30 @@ A: 本函式庫是「Headless」設計,代表它不會主動操作 DOM。您
372
154
  <slot />
373
155
  ```
374
156
 
375
- ## 授權 (License)
157
+ ## API 參考
158
+
159
+ ### `defineI18nConfig(config)`
160
+
161
+ 定義設定檔的輔助函式,提供型別推斷。
162
+
163
+ ### `createI18nStore(config)`
164
+
165
+ 建立實例。回傳:
166
+
167
+ - `t`: 翻譯用的 Derived store。
168
+ - `locale`: 目前語言的 Writable store。
169
+ - `setLocale(lang)`: 安全切換語言。
170
+ - `extendTranslations(modules)`:
171
+ - 合併新的翻譯。
172
+ - 回傳 `{ t }`: 一個新的 store 實例,其型別為「既有 keys + 新 keys」的聯集。
173
+
174
+ ### `onError: (error) => void`
175
+
176
+ `config` 中的回呼函式,用於處理遺失的 key 或語言。
177
+
178
+ - `error.type`: `'missing_key' | 'missing_locale'`
179
+ - `error.key`: 失敗的 key 或語言代碼。
180
+
181
+ ## License
376
182
 
377
183
  [MIT](https://opensource.org/licenses/MIT)
@@ -29,10 +29,13 @@ function createI18nStore(config) {
29
29
  const {
30
30
  supportedLocales,
31
31
  defaultLocale,
32
- initialTranslations,
32
+ initialTranslations = [],
33
33
  localStorageKey,
34
- devLogs = true
35
- // Set default value
34
+ onError = (err) => {
35
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
36
+ console.warn(`[svelte-tiny-i18n] ${err.type}: ${err.key} (locale: ${err.locale})`);
37
+ }
38
+ }
36
39
  } = config;
37
40
  const getInitLocale = () => {
38
41
  const browser = typeof window !== "undefined";
@@ -57,23 +60,39 @@ function createI18nStore(config) {
57
60
  }
58
61
  return defaultLocale;
59
62
  };
60
- const makeTranslation = (kv_pair) => {
63
+ const recursiveFlatten = (prefix, item, result) => {
64
+ if (typeof item !== "object" || item === null) return;
65
+ const itemKeys = item;
66
+ const keys = Object.keys(itemKeys);
67
+ const hasSupportedLocale = keys.some(
68
+ (k) => supportedLocales.includes(k)
69
+ );
70
+ if (hasSupportedLocale) {
71
+ supportedLocales.forEach((lang) => {
72
+ if (itemKeys[lang] && typeof itemKeys[lang] === "string") {
73
+ result.get(lang).set(prefix, itemKeys[lang]);
74
+ }
75
+ });
76
+ return;
77
+ }
78
+ Object.entries(itemKeys).forEach(([key, value]) => {
79
+ const newKey = prefix ? `${prefix}.${key}` : key;
80
+ recursiveFlatten(newKey, value, result);
81
+ });
82
+ };
83
+ const makeTranslation = (kv_pairs) => {
61
84
  const result = /* @__PURE__ */ new Map();
62
85
  supportedLocales.forEach((lang) => {
63
86
  result.set(lang, /* @__PURE__ */ new Map());
64
87
  });
65
- kv_pair.forEach((entry) => {
66
- Object.entries(entry).forEach(([key, partialEntry]) => {
67
- Object.entries(partialEntry).forEach(([lang, value]) => {
68
- if (result.has(lang)) {
69
- result.get(lang).set(key, value);
70
- }
71
- });
72
- });
88
+ kv_pairs.forEach((entry) => {
89
+ recursiveFlatten("", entry, result);
73
90
  });
74
91
  return result;
75
92
  };
76
- const translations = makeTranslation(initialTranslations);
93
+ const translations = makeTranslation(
94
+ initialTranslations
95
+ );
77
96
  const initialLanguage = getInitLocale();
78
97
  const locale = (0, import_store.writable)(initialLanguage);
79
98
  const _t = (0, import_store.writable)(translations.get(initialLanguage));
@@ -86,16 +105,16 @@ function createI18nStore(config) {
86
105
  localStorage.setItem(localStorageKey, lang);
87
106
  }
88
107
  });
89
- const t = (0, import_store.derived)(_t, ($_t) => {
108
+ const t = (0, import_store.derived)([_t, locale], ([$_t, $locale]) => {
90
109
  return (key, replacements) => {
91
110
  let translation = $_t.get(key);
92
111
  if (translation === void 0) {
112
+ onError({
113
+ key,
114
+ locale: $locale,
115
+ type: "missing_key"
116
+ });
93
117
  translation = key;
94
- if (devLogs) {
95
- console.warn(
96
- `[i18n] Translation for key "${key}" not found. Using key as fallback.`
97
- );
98
- }
99
118
  }
100
119
  if (replacements) {
101
120
  translation = translation.replace(
@@ -112,51 +131,51 @@ function createI18nStore(config) {
112
131
  });
113
132
  function setLocale(lang) {
114
133
  if (!lang) {
115
- if (devLogs && lang === "") {
116
- console.warn(`[i18n] Initialization failed: ${lang} is not a lang.`);
134
+ if (lang === "") {
135
+ onError({
136
+ key: "",
137
+ locale: "system",
138
+ type: "missing_locale"
139
+ });
117
140
  }
118
141
  return;
119
142
  }
120
143
  if (supportedLocales.includes(lang)) {
121
144
  locale.set(lang);
122
145
  } else {
123
- if (devLogs) {
124
- console.warn(
125
- `[i18n] Initialization failed: Language "${lang}" is not in supportedLocales [${supportedLocales.join(
126
- ", "
127
- )}].`
128
- );
129
- }
146
+ onError({
147
+ key: lang,
148
+ locale: "system",
149
+ type: "missing_locale"
150
+ });
130
151
  }
131
152
  }
132
- function extendTranslations(newTranslations) {
133
- const newTranslationMap = makeTranslation(newTranslations);
153
+ const extendTranslations = (newTranslations) => {
154
+ const newTranslationMap = makeTranslation(
155
+ newTranslations
156
+ );
134
157
  newTranslationMap.forEach((valueMap, lang) => {
135
158
  const existingMap = translations.get(lang);
136
- valueMap.forEach((value, key) => {
137
- existingMap.set(key, value);
138
- });
139
- });
140
- if (devLogs) {
141
- console.group("[i18n] Extended translations...");
142
- let totalCount = 0;
143
- newTranslationMap.forEach((valueMap, lang) => {
144
- const count = valueMap.size;
145
- if (count > 0) {
146
- console.log(`${count} keys added/updated for lang '${lang}'.`);
147
- totalCount += count;
148
- }
149
- });
150
- if (totalCount === 0) {
151
- console.log(`No new translations were added.`);
159
+ if (existingMap) {
160
+ valueMap.forEach((value, key) => {
161
+ existingMap.set(key, value);
162
+ });
163
+ } else if (typeof process !== "undefined" && process.env.NODE_ENV !== "production" && onError) {
164
+ onError({
165
+ key: lang,
166
+ locale: lang,
167
+ type: "missing_locale"
168
+ });
152
169
  }
153
- console.groupEnd();
154
- }
155
- locale.update((lang) => {
156
- _t.set(translations.get(lang));
157
- return lang;
158
170
  });
159
- }
171
+ locale.update((l) => {
172
+ if (translations.has(l)) _t.set(translations.get(l));
173
+ return l;
174
+ });
175
+ return {
176
+ t
177
+ };
178
+ };
160
179
  const _types = null;
161
180
  return {
162
181
  /**