Paraglide JS + Svelte 5 最佳实践
将区域设置视为由编译器处理的请求/构建状态,而非响应式应用状态——让 Paraglide 的完整文档导航来处理区域切换,其余一切交给 Svelte 5 runes 管理。 Paraglide JS 是 SvelteKit 官方的 i18n 集成方案:它将 messages/{locale}.json 编译为可摇树优化、完全类型化的 ESM 函数,相比于运行时库,可实现高达约 70% 的 i18n 包体积缩减(基准测试中,5 个区域/200 条消息约为 47 KB 对比 205 KB),且无异步瀑布流,也无需将 i18n provider 集成到 Svelte 5 组件中。
1. 快速决策参考表
| 决策点 | 推荐默认值 | 原因 |
|---|---|---|
| 安装路径 | 在新 SvelteKit 应用上执行 npx sv add paraglide="languageTags:en,de" | 一步完成 Vite 插件、hooks、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 标志 / 插件选项 > 配置文件 > 内置默认值。Watch 模式会热重载配置。
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']() 支持。动态键访问会破坏摇树优化——请将动态 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 Snippets 的富文本 (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 的 snippet 模式——没有 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>
传递强制区域而不是读取全局状态 是 runes 友好的模式:每次消息调用上的 { 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、水合与 SSG
6.1 SSR 规则
- 一个
handle链顺序:将paraglideHandle放在前面(或者放在需要按区域设置键控event.locals的认证处理程序之前),这样下游钩子就能看到去本地化的请求。 - 在 TLS 终结代理后面时,传递
effectiveRequestUrl,以便 URL 匹配和重定向使用浏览器面对的源。 - 保持
AsyncLocalStorage开启;正是它使得getLocale()在并发请求和流式 SSR 下是安全的。disableAsyncLocalStorage回退仅适用于没有node:async_hooks并且保证每个请求隔离的运行时——在其他地方使用它可能会跨用户泄漏区域设置。
6.2 水合正确性
服务器和客户端必须从相同的策略顺序解析区域设置。经典的失配:服务器从 URL 解析,但客户端策略首先读取 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. 性能与摇树优化
- 直接调用消息函数。
m.hello()编译为一个小的 ESM 函数;您的打包工具会自动丢弃未使用的消息及其翻译。包体积在目录增长时保持稳定(基准测试:无论项目包含 200 条还是 1000 条消息,包体积都是 47 KB)。