Svelte/SvelteKit 多語(yǔ)言配置指南

方案對(duì)比

方案 適用場(chǎng)景 復(fù)雜度 依賴(lài)大小
自定義 Store SvelteKit 全棧 0
svelte-i18n 純 Svelte 應(yīng)用 ~3KB
typesafe-i18n 類(lèi)型安全優(yōu)先 ~5KB
paraglide-js 編譯時(shí)優(yōu)化 ~2KB

方案一:自定義 Store(推薦 SvelteKit)

最輕量的方案,無(wú)需額外依賴(lài),代碼完全可控。

GitHub: 無(wú)(純手寫(xiě))

1. 目錄結(jié)構(gòu)

src/lib/i18n/
├── translations.ts      # 翻譯數(shù)據(jù)聚合
├── index.ts             # 導(dǎo)出接口
└── locales/
    ├── zh.ts            # 中文
    └── en.ts            # 英文

2. 翻譯文件

// src/lib/i18n/locales/zh.ts
export const zh = {
    nav: {
        home: '首頁(yè)',
        about: '關(guān)于'
    },
    welcome: '歡迎'
};

// src/lib/i18n/locales/en.ts
export const en = {
    nav: {
        home: 'Home',
        about: 'About'
    },
    welcome: 'Welcome'
};

3. 核心實(shí)現(xiàn)

// src/lib/i18n/translations.ts
import { zh } from './locales/zh';
import { en } from './locales/en';

export const translations = { zh, en };
export type Language = keyof typeof translations;
export type TranslationType = typeof zh;

// 檢測(cè)瀏覽器語(yǔ)言
export function detectLang(): Language {
    if (typeof navigator === 'undefined') return 'zh';
    const lang = navigator.language.toLowerCase();
    return lang.startsWith('zh') ? 'zh' : 'en';
}

// 從 localStorage 讀取
export function getStoredLang(): Language | null {
    if (typeof localStorage === 'undefined') return null;
    const stored = localStorage.getItem('lang');
    return stored === 'zh' || stored === 'en' ? stored : null;
}
// src/lib/i18n/index.ts
import { writable, derived, get } from 'svelte/store';
import { translations, detectLang, getStoredLang, type Language } from './translations';

// 優(yōu)先從 localStorage 讀取,否則檢測(cè)瀏覽器語(yǔ)言
const initialLang = getStoredLang() || detectLang();

// 當(dāng)前語(yǔ)言 Store
export const currentLang = writable<Language>(initialLang);

// 翻譯函數(shù) Store
export const t = derived(currentLang, ($lang) => {
    return (key: string): string => {
        const keys = key.split('.');
        let value: any = translations[$lang];
        for (const k of keys) {
            value = value?.[k];
        }
        // 回退到 key 本身
        return typeof value === 'string' ? value : key;
    };
});

// 切換語(yǔ)言
export function setLang(lang: Language) {
    currentLang.set(lang);
    if (typeof localStorage !== 'undefined') {
        localStorage.setItem('lang', lang);
    }
}

// 獲取當(dāng)前語(yǔ)言(非響應(yīng)式,用于腳本)
export function getLang(): Language {
    return get(currentLang);
}

4. 組件中使用

$ 前綴的作用:Svelte 中 $storeNamestoreName.subscribe() 的語(yǔ)法糖,表示自動(dòng)訂閱該 Store,值變化時(shí)組件自動(dòng)更新。

<!-- +layout.svelte -->
<script lang="ts">
    import { currentLang, t, setLang } from '$lib/i18n';
    
    // 不帶 $:獲取 Store 對(duì)象本身
    console.log(currentLang);  // Store 對(duì)象 { subscribe, set, update }
    
    // 帶 $:獲取 Store 的當(dāng)前值(自動(dòng)訂閱)
    console.log($currentLang); // 'zh' 或 'en'
</script>

<nav>
    <!-- 使用 $t() 獲取翻譯,$currentLang 獲取當(dāng)前語(yǔ)言 -->
    <a href="/">{$t('nav.home')}</a>
    <a href="/about">{$t('nav.about')}</a>
    
    <button on:click={() => setLang($currentLang === 'zh' ? 'en' : 'zh')}>
        {$currentLang === 'zh' ? 'EN' : '中文'}
    </button>
</nav>

對(duì)比

寫(xiě)法 含義 使用場(chǎng)景
currentLang Store 對(duì)象 傳遞給函數(shù)、調(diào)用方法
$currentLang Store 的值 模板中顯示、讀取當(dāng)前值

5. SSR 服務(wù)端渲染支持

SvelteKit 原生支持 SSR,語(yǔ)言從 URL/Cookie 檢測(cè),服務(wù)端預(yù)加載翻譯。

服務(wù)端與客戶(hù)端的差異

環(huán)境 可用 不可用
服務(wù)端 URL、Cookie、Header localStorage、navigator
客戶(hù)端 全部

Cookie 工具函數(shù)

// src/lib/i18n/cookies.ts
import type { Cookies } from '@sveltejs/kit';

export function getLangFromCookies(cookies: Cookies): 'zh' | 'en' {
    const stored = cookies.get('lang');
    return stored === 'zh' || stored === 'en' ? stored : 'zh';
}

export function setLangCookie(cookies: Cookies, lang: 'zh' | 'en') {
    cookies.set('lang', lang, {
        path: '/',
        maxAge: 60 * 60 * 24 * 365  // 1年
    });
}

Layout Load(服務(wù)端預(yù)加載):

// src/routes/+layout.ts
import type { LayoutLoad } from './$types';
import { translations } from '$lib/i18n/translations';
import { getLangFromCookies, setLangCookie } from '$lib/i18n/cookies';

export const load: LayoutLoad = ({ cookies, url }) => {
    // 服務(wù)端:從 Cookie 或 URL 參數(shù)獲取語(yǔ)言
    const langParam = url.searchParams.get('lang');
    const lang = (langParam === 'en' ? 'en' : 'zh');

    // 同步 Cookie
    setLangCookie(cookies, lang);

    // 預(yù)加載翻譯數(shù)據(jù)
    const t = translations[lang];

    return { lang, t };
};

Layout(接管切換):

<!-- src/routes/+layout.svelte -->
<script lang="ts">
    import { onMount } from 'svelte';
    import { setLang } from '$lib/i18n';

    let { data, children } = $props();
    
    // 初始化語(yǔ)言
    setLang(data.lang);
    
    // 語(yǔ)言切換
    function switchLang() {
        const newLang = $currentLang === 'zh' ? 'en' : 'zh';
        setLang(newLang);
        // 跳轉(zhuǎn)刷新
        window.location.href = `/?lang=${newLang}`;
    }
</script>

<nav>
    <a href="/?lang=zh">中文</a>
    <a href="/?lang=en">EN</a>
    <button on:click={switchLang}>
        當(dāng)前: {$currentLang}
    </button>
</nav>

{@render children()}

工作原理

  1. 用戶(hù)訪(fǎng)問(wèn) /?lang=en
  2. 服務(wù)端從 URL 讀取參數(shù),同步到 Cookie,返回預(yù)加載了英文的 HTML
  3. 客戶(hù)端 setLang(data.lang) 同步 Store,頁(yè)面已有翻譯
  4. 用戶(hù)切換語(yǔ)言 → 跳轉(zhuǎn) /?lang=zh → 服務(wù)端返回中文 HTML

SEO 友好

  • 搜索引擎爬蟲(chóng)訪(fǎng)問(wèn) /?lang=zh 抓中文內(nèi)容
  • 訪(fǎng)問(wèn) /?lang=en 抓英文內(nèi)容
  • 每個(gè)語(yǔ)言都有獨(dú)立 URL

5. SSR 注意事項(xiàng)

<!-- 安全訪(fǎng)問(wèn) localStorage -->
<script lang="ts">
    import { onMount } from 'svelte';
    import { setLang } from '$lib/i18n';
    
    onMount(() => {
        // 客戶(hù)端才執(zhí)行
        const saved = localStorage.getItem('lang');
        if (saved) setLang(saved as 'zh' | 'en');
    });
</script>

方案二:svelte-i18n (純svelte推薦)

社區(qū)最流行的方案,API 設(shè)計(jì)簡(jiǎn)潔。

npm install svelte-i18n

初始化文件

// src/lib/i18n.ts
import { register, init, getLocaleFromNavigator, locale } from 'svelte-i18n';

// 注冊(cè)語(yǔ)言文件(懶加載)
register('zh', () => import('./locales/zh.json'));
register('en', () => import('./locales/en.json'));

// 初始化配置
init({
    fallbackLocale: 'zh',
    initialLocale: getLocaleFromNavigator()
});

// 導(dǎo)出切換函數(shù)
export { locale };
export const setLocale = (lang: string) => locale.set(lang);

應(yīng)用入口引入

// src/main.ts (純 Svelte)
import './lib/i18n';  // ← 必須先導(dǎo)入初始化
import App from './App.svelte';

const app = new App({ target: document.body });
export default app;
// src/routes/+layout.ts (SvelteKit)
import { browser } from '$app/environment';
import { locale, waitLocale } from 'svelte-i18n';
import '$lib/i18n';  // 導(dǎo)入執(zhí)行初始化
import type { LayoutLoad } from './$types';

export const load: LayoutLoad = async () => {
    if (browser) {
        const saved = localStorage.getItem('lang');
        if (saved) locale.set(saved);
    }
    await waitLocale();  // 等待翻譯加載完成
    return {};
};

組件使用

<script>
    import { _, locale } from 'svelte-i18n';
    import { setLocale } from '$lib/i18n';
</script>

<h1>{$_('welcome')}</h1>
<p>{$_('footer.copyright')}</p>

<button on:click={() => setLocale($locale === 'zh' ? 'en' : 'zh')}>
    切換
</button>

帶參數(shù)的翻譯

{
    "hello": "Hello {name}!",
    "items": "You have {count} item | You have {count} items"
}
<p>{$_('hello', { values: { name: 'World' } })}</p>
<p>{$_('items', { values: { count: 5 } })}</p>

方案三:typesafe-i18n

類(lèi)型安全的國(guó)際化方案,IDE 自動(dòng)補(bǔ)全翻譯 key。

npm install typesafe-i18n
npx typesafe-i18n --setup  # 生成配置文件

自動(dòng)生成類(lèi)型

// src/i18n/i18n-types.ts(自動(dòng)生成)
export type Translation = {
    nav: {
        home: string;
        about: string;
    };
    welcome: string;
};

使用

<script lang="ts">
    import { LL } from '$lib/i18n/i18n-svelte';
    import { setLocale } from '$lib/i18n/i18n-util';
</script>

<h1>{$LL.welcome()}</h1>
<a href="/about">{$LL.nav.about()}</a>

方案四:paraglide-js

編譯時(shí)優(yōu)化的國(guó)際化方案,零運(yùn)行時(shí)開(kāi)銷(xiāo)。

npm install @inlang/paraglide-js

特點(diǎn)

  • 編譯時(shí)將翻譯內(nèi)聯(lián)到代碼中
  • 只打包用到的翻譯
  • 支持 Tree Shaking
// 編譯后直接使用
import * as m from '$lib/paraglide/messages.js';

console.log(m.hello_world()); // "Hello World!"

方案五:URL 路由級(jí)多語(yǔ)言(SvelteKit)

SEO 友好的方案,語(yǔ)言體現(xiàn)在 URL 中。

/zh/about    → 中文關(guān)于頁(yè)
/en/about    → 英文關(guān)于頁(yè)
/about       → 默認(rèn)語(yǔ)言

路由配置

// src/params/lang.ts
import type { ParamMatcher } from '@sveltejs/kit';

export const match: ParamMatcher = (param) => {
    return ['zh', 'en'].includes(param);
};
src/routes/
├── [[lang=lang]]/         # 可選語(yǔ)言前綴
│   ├── +page.svelte
│   └── about/
│       └── +page.svelte
└── +layout.ts

加載翻譯

// src/routes/[[lang=lang]]/+layout.ts
import type { LayoutLoad } from './$types';
import { translations } from '$lib/i18n/translations';

export const load: LayoutLoad = ({ params }) => {
    const lang = (params.lang as 'zh' | 'en') || 'zh';
    return {
        lang,
        t: translations[lang]
    };
};

關(guān)鍵決策點(diǎn)

場(chǎng)景 推薦方案 理由
快速上線(xiàn) svelte-i18n 生態(tài)成熟,文檔豐富
類(lèi)型安全 typesafe-i18n 編譯時(shí)檢查,IDE 提示
SEO 優(yōu)先 URL 路由級(jí) 語(yǔ)言在 URL,搜索引擎友好
極簡(jiǎn)依賴(lài) 自定義 Store 零依賴(lài),完全可控
大型應(yīng)用 paraglide-js 編譯優(yōu)化,性能最好
SSR + SEO 自定義 Store + Cookie 服務(wù)端預(yù)加載,客戶(hù)端接管切換

最佳實(shí)踐

1. 延遲加載翻譯

// 不要:import zh from './locales/zh';  // 打包進(jìn)主 bundle
// 要:
register('zh', () => import('./locales/zh.json'));  // 按需加載

2. SSR 安全訪(fǎng)問(wèn)瀏覽器 API

<script>
    import { browser } from '$app/environment';
    import { onMount } from 'svelte';
    
    // 方式一:onMount
    onMount(() => {
        localStorage.getItem('lang');  // 安全
    });
    
    // 方式二:browser 判斷
    if (browser) {
        localStorage.getItem('lang');  // 安全
    }
</script>

3. 回退機(jī)制

// 找不到翻譯時(shí)回退到 key
export function t(key: string): string {
    const value = getNestedValue(translations[lang], key);
    return value || key;  // 回退到 key
}

4. 類(lèi)型安全(自定義 Store 版)

// 生成翻譯 key 的類(lèi)型
type DotPrefix<T extends string> = T extends '' ? '' : `.${T}`;

type DotPath<T> = (
    T extends object ?
        { [K in keyof T]:
            `${Exclude<K, symbol>}${DotPrefix<DotPath<T[K]>>}`
        }[keyof T] :
        ''
) extends infer D ? Extract<D, string> : never;

export type TranslationKey = DotPath<typeof zh>;

// 使用
export const t = (key: TranslationKey) => ...
// IDE 提示: 'nav.home' | 'nav.about' | 'welcome' ...

5. 語(yǔ)言切換動(dòng)畫(huà)

{#key $currentLang}
    <div in:fade={{ duration: 150 }}>
        <h1>{$t('welcome')}</h1>
    </div>
{/key}

參考資源

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容