Paraglide JS + Svelte 5 最佳實踐
將語言環境視為由編譯器解析的請求/建置狀態,而非應用程式的反應狀態 — 讓 Paraglide 的全文件導航處理語言環境切換,並讓 Svelte 5 runes 掌管其他所有事務。 Paraglide JS 是 SvelteKit 的官方 i18n 整合方案:它將 messages/{locale}.json 編譯成可 tree-shaking、完全型別化的 ESM 函式,與執行時期函式庫相比,i18n 套件大小最多可減少約 70%(在 5 個語言環境/200 條訊息的已發布基準測試中,約 47 KB 對比約 205 KB),且無非同步瀑布流,也無需在 Svelte 5 元件中接入 i18n Provider。
1. 快速參考決策表
| 決策項目 | 建議預設值 | 原因 |
|---|---|---|
| 安裝路徑 | npx sv add paraglide="languageTags:en,de" (在新的 SvelteKit 應用程式上) | 一步驟即可建置 Vite 外掛、hook、app.html 屬性、.gitignore |
| 套件 | @inlang/paraglide-js v2.x | v1 的轉接器 @inlang/paraglide-js-adapter-sveltekit 已棄用 — 請直接使用核心套件 |
| 策略 (SSR + 本地化 URL) | ["url", "cookie", "baseLocale"] | URL 是 SEO 的事實來源;cookie 以伺服器可見的方式持久化使用者選擇;baseLocale 總是最後解析 |
| 策略 (新增儲存的覆寫) | ["localStorage", "cookie", "preferredLanguage", "url", "baseLocale"] | 僅使用 localStorage 對首次 SSR 請求不可見;cookie 橋接了伺服器/客戶端 |
| 語言環境切換行為 | 預設 setLocale(locale) (全文件導航) | 框架的反應性不會重新渲染 <html lang>、dir、<title> 或本地化 URL;導航才會 |
| AsyncLocalStorage | 保持啟用 (預設) | Vercel Edge 和 Cloudflare Workers (Node 相容性) 支援它;停用可能導致並行請求間的語言環境洩漏 |
| TS 宣告 | emitTsDeclarations: true (TypeScript ≥ 5.6) | 在開發過程中鍵值變更時,保持 Svelte + TS 語言伺服器同步 |
| 訊息鍵值 | 扁平、穩定、人類可讀的隨機鍵值 (calm_green_otter) | 官方推薦;巢狀鍵值也可運作,但偏好扁平鍵值 |
| 預先渲染 | export const prerender = true + 每個語言環境的 entries() | SSG 需要每個語言環境解析成一個不同的 URL 檔案 |
| 語言環境的反應性 | 元件中預設無 | getLocale() 是請求範圍的;每個語言環境會渲染一個新文件,因此 runes 不需要追蹤它 |
2. 設定與配置 (Paraglide 2.x + SvelteKit + Svelte 5)
2.1 最快路徑
npx sv create my-app
npx sv add paraglide="languageTags:en,de,demo:yes"
Svelte CLI 外掛會配置 inlang 專案、Vite 外掛、SvelteKit handle/reroute hooks、app.html 中的 %lang%/%dir%,以及一個可選的示範頁面。
2.2 手動設定 (完整樣板)
安裝並建置專案,然後連接四個檔案:
vite.config.ts — 包含明確策略的外掛:
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { paraglideVitePlugin } from '@inlang/paraglide-js';
export default defineConfig({
plugins: [
sveltekit(),
paraglideVitePlugin({
project: './project.inlang',
outdir: './src/lib/paraglide',
emitTsDeclarations: true,
strategy: ['url', 'cookie', 'baseLocale'],
}),
],
});
emitTsDeclarations: true 會在您編輯訊息時重新產生 .d.ts,使 Svelte 和 TypeScript 伺服器保持同步;這需要 TypeScript 5.6+(若想使用更快的 JSDoc 推斷路徑,則設定為 false)。
src/app.html — 替換硬編碼的 lang:
<!doctype html>
<html lang="%lang%" dir="%dir%">
src/hooks.server.ts — 包含請求範圍語言環境的伺服器中間件:
import type { Handle } from '@sveltejs/kit';
import { paraglideMiddleware } from '$lib/paraglide/server';
import { getTextDirection } from '$lib/paraglide/runtime';
const paraglideHandle: Handle = ({ event, resolve }) =>
paraglideMiddleware(event.request, ({ request: localizedRequest, locale }) => {
event.request = localizedRequest;
return resolve(event, {
transformPageChunk: ({ html }) =>
html.replace('%lang%', locale).replace('%dir%', getTextDirection(locale)),
});
});
export const handle: Handle = paraglideHandle;
中間件會偵測語言環境(依策略順序),當 URL 與偵測到的語言環境不一致時重新導向文件請求,在 SvelteKit 看到 /de/about → /about 之前將其去本地化,並將請求包裝在 AsyncLocalStorage 中,以便 getLocale() 為每個並行請求返回正確的語言環境。
src/hooks.ts — 路由邊界(必須是 hooks.ts,不是 hooks.server.ts):
import type { Reroute } from '@sveltejs/kit';
import { deLocalizeUrl } from '$lib/paraglide/runtime';
export const reroute: Reroute = (request) => deLocalizeUrl(request.url).pathname;
// project.inlang/paraglide.config.ts
import { defineConfig } from '@inlang/paraglide-js';
export default defineConfig({
outdir: './src/lib/paraglide',
strategy: ['url', 'cookie', 'baseLocale'],
urlPatterns: [
{ pattern: '/', localized: [['en', '/']] },
{
pattern: '/:path(.*)?',
localized: [
['en', '/en/:path(.*)?'],
['de', '/de/:path(.*)?'],
],
},
],
});
優先順序:CLI 旗標 / 外掛選項 > 配置檔案 > 內建預設值。監看模式會熱重載配置。
3. 核心使用模式 (訊息、語言環境、路由)
3.1 訊息
import { m } from '$lib/paraglide/messages';
import type { LocalizedString } from '$lib/paraglide/runtime';
m.hello_world(); // "Hello World!"
m.greeting({ name: 'Ada' }); // "Hello Ada!" — 參數是型別化的
m.greeting({ name: 'Ada' }, { locale: 'de' }); // 強制單次呼叫的語言環境
const title: LocalizedString = m.welcome_title(); // 品牌安全的字串型別
訊息存在於 messages/{locale}.json 中;語言環境在 project.inlang/settings.json 中宣告(baseLocale + locales)。偏好扁平穩定的鍵值;巢狀鍵值透過 m['user.profile.title']() 支援。動態鍵值存取會破壞 tree-shaking — 請改為將動態 ID 映射到明確的訊息參考。
3.2 取得和設定語言環境
import { getLocale, locales, setLocale, getTextDirection } from '$lib/paraglide/runtime';
getLocale(); // 例如 "de" — 在伺服器上是請求範圍的
getTextDirection(); // 目前語言環境的 "ltr" | "rtl"
setLocale('de'); // 全文件導航到本地化 URL
setLocale() 會更新您配置的策略,然後執行全文件導航 — 當 url 策略啟用時,導航到本地化 URL,否則重新載入。這是刻意的:<html lang>、dir、<title>、元資料和本地化 URL 是框架反應性無法更新的文件層級狀態。
setLocale(locale, { reload: false }) 逃脫艙口僅存在於完全客戶端渲染的表面(嵌入式小工具、擴充頁面),這些表面必須保留無法還原的記憶體中狀態。永遠不要在普通的語言環境選擇器、SSR/SSG 文件或策略包含 url 的路由上使用它 — 它會留下舊的 URL 和外殼,導致 getLocale() 持續解析舊的語言環境。
3.3 本地化 URL 和連結
import { localizeHref, deLocalizeUrl, localizeUrl } from '$lib/paraglide/runtime';
localizeHref('/blog'); // 目前語言環境的 "/de/blog"
localizeHref('/blog', { locale: 'de' }); // 明確指定
localizeUrl('https://example.com/about', { locale: 'de' }).href; // 完整 URL 形式
URL 模式使用標準的 URLPattern 語法;順序很重要 — 將特定模式放在萬用字元之前,並在 urlpattern.com 上測試它們。
3.4 Svelte 5 語言環境切換器(慣用寫法)
<script lang="ts">
import { page } from '$app/state'; // Svelte 5: 物件,非 store
import { resolve } from '$app/paths';
import { locales, localizeHref } from '$lib/paraglide/runtime';
</script>
<nav aria-label="Languages">
{#each locales as locale (locale)}
<a
href={resolve(localizeHref(page.url.pathname, { locale }))}
data-sveltekit-reload
aria-current={page.params.locale === locale ? 'page' : undefined}
>
{locale}
</a>
{/each}
</nav>
這裡有兩個 Svelte 5 的細節很重要:$app/state 將 page 暴露為一個反應性物件($page store 和 $app/stores 是舊模式),而跨語言環境連結上的 data-sveltekit-reload 保持本地化 URL、伺服器渲染的文件和執行時期語言環境同步 — 對於預先渲染的爬取也同樣必要。
4. 使用 Svelte 5 程式碼片段 (ParaglideMessage) 的富文字
標記讓翻譯人員控制結構,同時您的應用程式控制渲染。標籤名稱是應用程式定義的 — 沒有任何東西會自動對應到 HTML。
// messages/en.json
{
"cta": "{#link to=|/docs|}Read the docs{/link}",
"welcome": "{#b}Hi {name}{/b}{#icon/}"
}
<script lang="ts">
import { ParaglideMessage } from '@inlang/paraglide-js-svelte';
import { m } from '$lib/paraglide/messages';
const name = $props().name; // 或任何 $state
</script>
<ParaglideMessage message={m.welcome} inputs={{ name }}>
{#snippet b({ children })}
<strong>{@render children?.()}</strong>
{/snippet}
{#snippet icon({ attributes })}
<Icon variant={attributes?.variant} />
{/snippet}
</ParaglideMessage>
<ParaglideMessage> 在存在標記時使用 message.parts(),並在純文字時回退到 message();{#snippet tag({ children, options, attributes })} 語法正是 Svelte 5 的程式碼片段模式 — 沒有 slots,沒有 <slot> 舊語法。注意文字語法 to=|/docs|(以管道符分隔),以及 m.welcome() 單獨使用時會返回去除標記的純文字。
5. Svelte 5 Runes 整合:什麼是反應性的(以及什麼不是)
關鍵心智模型:Paraglide 將語言環境視為文件/請求狀態;Svelte 5 runes 掌管 UI 狀態。 由於語言環境變更會產生一個新文件,您很少需要 runes 來追蹤 getLocale()。實踐中出現的模式:
單一文件內語言環境依賴的衍生值(例如,使用 reload: false 的純客戶端表面,或明確渲染多個語言環境的預覽窗格):
<script lang="ts">
import { getLocale, locales } from '$lib/paraglide/runtime';
import { m } from '$lib/paraglide/messages';
let { locale = getLocale() } = $props(); // 型別化 props
// 當 `locale` prop 變更時,$derived 會重新求值 — 但當
// Paraglide 內部的語言環境變更時則不會(沒有導航就不會變更):
const greeting = $derived(m.greeting({ name: 'Ada' }, { locale }));
const isRtl = $derived(getTextDirection(locale) === 'rtl');
</script>
向下傳遞強制的語言環境而不是讀取全域狀態 是 rune 友善的模式:在每個訊息呼叫上使用 { locale } 選項使元件成為 props 的純函式,這正是 Svelte 5 期望狀態流動的方式。保留 $effect 用於真正的副作用(例如,持久化預覽語言環境)— 永遠不要賦值給 $derived,也不要從 $effect 內部寫入 $state 來「推送」語言環境;改為傳遞 props。
全域客戶端狀態(例如,應用內翻譯工具的「最後預覽的語言環境」):
// src/lib/locale-preview.svelte.ts — .svelte.ts 檔案在模組範圍支援 runes
export const preview = $state({ locale: 'en' as string | undefined });
在 .svelte.ts 檔案中的模組層級 $state 會在所有導入者之間共享 — 對於應用範圍的單例很合適,但請記住它不是生產環境語言環境的運作方式(那是策略鏈 + 文件導航)。
6. SSR、Hydration 和 SSG
6.1 SSR 規則
- 一個
handle鏈順序:將paraglideHandle放在首位(或在使用需要按語言環境鍵入event.locals的 auth handlers 之前),以便下游 hooks 看到去本地化的請求。 - 在 TLS 終止代理後方,傳遞
effectiveRequestUrl以便 URL 匹配和重新導向使用瀏覽器面向的來源。 - 保持
AsyncLocalStorage啟用;這正是getLocale()在並行請求和串流 SSR 下安全的原因。disableAsyncLocalStorage回退僅適用於沒有node:async_hooks且 保證每個請求隔離的執行時期 — 在其他地方使用它可能會跨使用者洩漏語言環境。
6.2 Hydration 正確性
伺服器和客戶端必須從相同的策略順序解析語言環境。經典的不匹配:伺服器從 URL 解析,但客戶端策略首先讀取 localStorage,而 localStorage 在 SSR 時期不存在。透過在持久化選擇必須塑造首次回應時,加入一個伺服器可見的 cookie 策略來修正。
6.3 SSG (adapter-static / prerender)
預先渲染永遠不會執行中間件 — 在建置期間為每個頁面設定語言環境:
// src/routes/[locale]/+layout.ts
import { setLocale, assertIsLocale } from '$lib/paraglide/runtime';
import type { LayoutLoad } from './$types';
export const prerender = true;
export function load({ params }: { params: { locale: string } }) {
setLocale(assertIsLocale(params.locale));
}
// src/routes/[locale]/+page.ts
import { locales } from '$lib/paraglide/runtime';
export const prerender = true;
export function entries() {
return locales.map((locale) => ({ locale }));
}
SSG 需要每個語言環境都帶有一個 URL 前綴(配置 urlPatterns 使 /en/… 和 /de/… 都存在),否則 SvelteKit 無法為每個語言環境產生不同的檔案。對於靜態轉接器的 SPA 回退,設定 kit.paths.relative = false 以避免語言環境前綴的資產 404,並使用 generateStaticLocalizedUrls() 產生站點地圖。
7. 效能與 Tree-Shaking
- 直接呼叫訊息函式。
m.hello()編譯成一個小的 ESM 函式;您的打包工具會自動丟棄未使用的訊息及其翻譯。套件大小在目錄增長時保持穩定(基準測試:無論專案持有 200 或 1,000 條訊息,皆為 47 KB)。 - 避免動態鍵值。
m[messageId]()會阻止 tree-shaking;使用明確的函式呼叫或靜態對應。 - 使用 code splitting。 非同步路由可以延遲載入其訊息,無需特殊配置。Paraglide 的編譯器輸出已經按路由最佳化。
- 利用
emitTsDeclarations。 它使 TypeScript 伺服器保持同步,而不會增加執行時期開銷。
8. 疑難排解
8.1 語言環境切換後 URL 未更新
確保您的策略包含 url 且 setLocale() 正在執行全文件導航(預設)。如果使用 reload: false,URL 將保持不變。
8.2 伺服器/客戶端 Hydration 不匹配
確保伺服器和客戶端解析語言環境的順序相同。使用 cookie 策略持久化使用者的選擇,使其透過 SSR 可見。
8.3 SSG 未為每個語言環境產生檔案
每個語言環境都需要一個 URL 前綴。檢查您的 urlPatterns 是否為每個語言環境定義了明確的路由。
8.4 語言環境洩漏給其他使用者
確保 AsyncLocalStorage 已啟用。停用它可以讓語言環境在並行請求間共用。
9. 進階主題
9.1 自訂語言環境偵測策略
您可以實作自訂策略來偵測語言環境,例如基於使用者設定檔、地理位置或 A/B 測試。將它們作為函式傳遞給策略陣列。
9.2 與其他 Svelte 5 函式庫整合
Paraglide 無縫整合 Svelte 5 的 $state、$derived 和 $effect。使用 props 向下傳遞語言環境,以保持元件的純粹性和可測試性。
9.3 遷移自 v1
從 @inlang/paraglide-js-adapter-sveltekit 遷移到直接使用 @inlang/paraglide-js v2.x。更新您的 Vite 配置和 hooks 以使用新的 API。
10. 結論
Paraglide JS 和 Svelte 5 提供了一個強大且高效的 i18n 解決方案。透過將語言環境視為文件狀態並利用編譯器,您可以實現更小的套件大小、更好的效能和更簡單的程式碼。遵循這些最佳實踐,以確保您的應用程式在全球範圍內都能正常運作。