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.md +76 -269
- package/README.zh_tw.md +87 -281
- package/dist/svelte-tiny-i18n.cjs +71 -52
- package/dist/svelte-tiny-i18n.d.cts +41 -19
- package/dist/svelte-tiny-i18n.d.ts +41 -19
- package/dist/svelte-tiny-i18n.js +71 -52
- package/package.json +68 -63
package/README.zh_tw.md
CHANGED
|
@@ -4,366 +4,148 @@
|
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](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/)
|
|
9
|
+
`svelte-tiny-i18n` 是一個為 [Svelte](https://svelte.dev/) 和 [SvelteKit](https://kit.svelte.dev/) 設計的輕量級、型別安全且響應式的 i18n 函式庫,完全基於 Svelte Stores 構建。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
> **💡 請查看 [範例目錄 (Examples)](./examples/README.zh_tw.md) 以查看所有型別安全模式的用法!**
|
|
12
12
|
|
|
13
|
-
## TL;DR
|
|
13
|
+
## 核心價值 (TL;DR)
|
|
14
14
|
|
|
15
|
-
`svelte-tiny-i18n`
|
|
15
|
+
`svelte-tiny-i18n` 專為那些重視 **極致輕量、零依賴且零建構設定**,同時仍希望享受 **Svelte store 響應式體驗與 TypeScript 即時推斷** 的開發者所設計。
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
我們最大的優勢:**混合型別安全**。
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
127
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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'
|
|
168
|
-
|
|
169
|
-
<nav>
|
|
170
|
-
<p>目前語言: {$locale}</p>
|
|
66
|
+
<h1>{$t('hello')}</h1>
|
|
67
|
+
<p>{$t('home.title')}</p>
|
|
171
68
|
|
|
172
|
-
|
|
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
|
-
|
|
72
|
+
## 進階用法:動態載入與型別
|
|
181
73
|
|
|
182
|
-
|
|
74
|
+
當您需要動態載入翻譯(使用 `extendTranslations`)時,`svelte-tiny-i18n` 提供了兩種強大的型別策略。
|
|
183
75
|
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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.
|
|
94
|
+
2. 現在 `$t('profile.name')` 在整個應用程式中都能獲得型別檢查,即便該檔案尚未載入!
|
|
231
95
|
|
|
232
|
-
|
|
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
|
-
|
|
243
|
-
// const { jsonTranslations } = await import('$locales/profile.json');
|
|
244
|
-
// i18n.extendTranslations([jsonTranslations]);
|
|
245
|
-
};
|
|
246
|
-
```
|
|
98
|
+
不想碰 `d.ts`?沒問題。`extendTranslations` 會回傳一個專為新內容設定好型別的 Store。
|
|
247
99
|
|
|
248
|
-
|
|
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
|
-
|
|
107
|
+
$t('profile.title'); // ✅ Typed!
|
|
108
|
+
```
|
|
253
109
|
|
|
254
|
-
|
|
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
|
-
| 維度
|
|
263
|
-
|
|
|
264
|
-
| **檔案大小**
|
|
265
|
-
| **核心機制**
|
|
266
|
-
| **型別安全**
|
|
267
|
-
| **設定複雜度**
|
|
268
|
-
| **進階格式化**
|
|
269
|
-
| **主要取捨**
|
|
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`**
|
|
274
|
-
-
|
|
275
|
-
-
|
|
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
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
66
|
-
|
|
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(
|
|
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 (
|
|
116
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
);
|
|
129
|
-
}
|
|
146
|
+
onError({
|
|
147
|
+
key: lang,
|
|
148
|
+
locale: "system",
|
|
149
|
+
type: "missing_locale"
|
|
150
|
+
});
|
|
130
151
|
}
|
|
131
152
|
}
|
|
132
|
-
|
|
133
|
-
const newTranslationMap = makeTranslation(
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
/**
|