国际化
TanStack Start 下的 Paraglide 国际化接入、翻译管理与路由策略
策略概览
项目使用 Paraglide 接入 TanStack Start,默认内置四种语言,也可以继续添加新的 BCP 47 locale:
| 语言 | Locale 代码 | URL |
|---|---|---|
| 简体中文(默认) | zh | /、/pricing |
| 繁体中文 | zh-Hant | /zh-Hant、/zh-Hant/pricing |
| English | en | /en、/en/pricing |
| 日本語 | ja | /ja、/ja/pricing |
Paraglide 会从 JSON 消息文件生成类型安全的 message 函数。组件里直接调用生成后的 m.*() 方法,不需要在页面里手写 namespace hook。
为什么选 Paraglide
这套模板优先选择 Paraglide,原因主要有四个:
- 编译期生成:消息文件会编译成
m.*()函数,调用时有类型提示,key 写错会在开发期暴露。 - 适合 Vite 和 TanStack Start:官方 TanStack Start 示例采用 Paraglide,Vite 插件能直接接入当前构建链路。
- 输出轻量:Paraglide 以 message module 形式生成代码,生产构建可以 tree-shake 未使用的消息。
- 框架绑定少:它围绕 Inlang project 和消息文件工作,迁移到其他 Vite/React 场景时成本低。
主应用是 TanStack Start,Paraglide 的编译式 message、SSR middleware 和 URL locale 策略更贴近现有结构。
为什么维护自己的翻译脚本
Paraglide 负责运行时、路由和代码生成;翻译自动化由模板内的 @01mvp/i18n 脚本处理。
这样做的考虑:
- 直接复用项目已有的 OpenAI-compatible 配置:
OPENAI_API_KEY、OPENAI_BASE_URL、OPENAI_MODEL。 - 不绑定第三方翻译平台账号,也不需要给额外的 i18n SaaS 付费。
- 支持增量翻译:
translation.lock.json记录每个 key 的源文案 hash,只有新增或源文案变化的 key 会请求模型。 - 支持批量、多 locale、自动新增 locale、指定 key 前缀、dry-run 和 check,适合本地开发、CI 和 AI agent 自动化。
language_name由Intl.DisplayNames按目标 locale 自动生成,新增语言时不需要改脚本代码。- 保留 Paraglide 的标准消息文件格式,未来需要拆分大文件时,可以先扩展翻译脚本的 catalog 输入,再调整 Inlang path pattern。
Inlang CLI 仍然适合做项目检查和生态工具接入。模板默认翻译流程选用本地脚本,是为了让使用者只配置自己的 OpenAI-compatible API key。
当前接入方式
当前代码里有几处关键 wiring:
| 位置 | 作用 |
|---|---|
products/01mvp/packages/i18n/project.inlang/settings.json | 定义 baseLocale: "zh" 和 locales: ["zh", "zh-Hant", "en", "ja"] |
products/01mvp/packages/i18n/messages/*.json | 维护业务 UI 文案 |
products/01mvp/packages/i18n/translation.lock.json | 记录翻译增量状态 |
products/01mvp/packages/i18n/scripts/translate-messages.mjs | OpenAI-compatible 翻译脚本 |
products/01mvp/packages/i18n/src/vite/plugin.js | 包装 Paraglide Vite plugin,生成多语言 URL 规则 |
products/01mvp/apps/web/vite.config.ts | 加载 @01mvp/i18n/vite/plugin |
products/01mvp/apps/web/src/server.ts | 通过 paraglideMiddleware 给 SSR 请求设置 locale 上下文 |
products/01mvp/apps/web/src/routes/__root.tsx | 用 getLocale() 设置 <html lang> |
products/01mvp/apps/web/src/routes/{-$locale} | TanStack Router 的可选 locale 前缀 |
products/01mvp/packages/i18n/src/tanstack-start | Link、navigate、redirect、route helper 的本地适配层 |
products/01mvp/packages/i18n/src/vite/plugin.js 的 URL 规则会把无前缀路径解析为中文,所以首访 / 会显示中文,不会因为浏览器语言是 English 自动跳到 /en。如果以后希望英文浏览器自动进入英文 URL,需要单独调整策略和重定向规则,并重新验证 SEO 与登录后路由。
目录结构
使用翻译
本页命令默认从仓库根目录运行,使用 vpr @01mvp/product#<script> 短写。进入 products/01mvp 目录后,可以把 vpr @01mvp/product#i18n:sync 写成 vpr i18n:sync。
在组件里从 @01mvp/i18n/messages 导入生成后的消息函数:
import { m } from "@01mvp/i18n/messages";
export function SignInTitle() {
return <h1>{m.auth__sign_in_title()}</h1>;
}消息 key 来自 products/01mvp/packages/i18n/messages/*.json。新增或修改消息后,通常直接运行同步命令;它会翻译增量内容并更新 products/01mvp/packages/i18n/src/paraglide:
vpr @01mvp/product#i18n:sync消息 Key 命名
消息 key 使用扁平命名,格式是:
{功能或页面}__{具体含义}例如:
{
"navbar__dashboard": "我的",
"user_dropdown__privacy_policy": "隐私政策",
"feedback_page__success": "谢谢,反馈已经收到。"
}这里刻意使用 flat keys,不把消息写成嵌套 JSON。Paraglide 支持 nested keys,但官方更推荐 flat keys;flat catalog 更适合编译生成、tree-shaking、缺失 key 检查和增量翻译锁。我们的翻译脚本也按 flat key 计算 hash,所以新增、修改、删除消息时更容易判断影响范围。
这种命名方式的好处是上下文明确。即使中文里两个位置都显示“隐私政策”,它们在导航、用户菜单、登录协议里的语气、长度或可访问性说明以后也可能分开调整。按使用场景保留 key,能避免改一个位置时误伤其它位置。
只有在文案确实应该全站一致时,才建议抽成 common__...。例如产品名、固定品牌词、全局法律链接标签、非常通用的按钮动作。如果只是当前字面一样,但后续可能按位置变化,继续使用 navbar__...、user_dropdown__...、footer__... 这类上下文 key 更稳。
维护翻译
当前推荐把 zh.json 当作源文案,其它 locale 通过增量翻译脚本维护。每次新增 UI 文案时按这个顺序处理:
新增中文源文案
在 products/01mvp/packages/i18n/messages/zh.json 增加 key。命名继续使用 {页面或功能}__{具体含义},例如 billing_page__empty_title。
同步其它语言
环境变量先从 products/01mvp/packages/config/.env 读取,再从 packages/config/.env 兜底,默认使用这三个配置:
OPENAI_API_KEY="your-api-key"
OPENAI_BASE_URL="https://api.deepseek.com"
OPENAI_MODEL="deepseek-v4-flash"运行:
vpr @01mvp/product#i18n:sync如果只想翻译消息文件,暂时不生成 Paraglide,可以运行:
vpr @01mvp/product#i18n:translate脚本会读取 messages/zh.json,根据 Inlang settings 里的 locale 列表更新所有目标消息文件,并维护 translation.lock.json。已有翻译会保留;新增 key 或源文案变化时才会重新请求模型。
需要预览变更范围时运行:
vpr @01mvp/product#i18n:translate:dry-run需要在 CI 或提交前检查是否同步时运行:
vpr @01mvp/product#i18n:translate:check检查 i18n 包
vpr @01mvp/i18n#test
vpr @01mvp/i18n#type-checkproducts/01mvp/packages/i18n/src/__tests__/messages.test.ts 会检查所有 locale 的 key 是否对齐。缺 key 的翻译不应该进主分支。
清理未使用消息
删除页面、重命名组件或合并文案时,消息文件里可能留下已经没有代码引用的 key。模板内置了 unused 检查:
vpr @01mvp/product#i18n:unused这个命令会扫描生产源码里对 @01mvp/i18n/messages 的引用,列出没有被 m.* 使用的 key,并按前缀分组。它不会修改文件。
需要在 CI 或提交前阻止未使用 key 继续累积时,可以运行:
vpr @01mvp/product#i18n:unused:check如果确认这些 key 可以删除,运行:
vpr @01mvp/product#i18n:unused:pruneprune 会按 project.inlang/settings.json 里的 locale 列表,同步删除所有 products/01mvp/packages/i18n/messages/*.json 里的未使用 key,清理 translation.lock.json 中对应的翻译记录,并重新生成 Paraglide 代码。
清理脚本有几个保护条件:如果没有扫描到消息引用、发现动态消息访问,或者发现代码引用了 catalog 中不存在的 key,它会停止删除。动态消息访问通常指 m[someKey] 这类写法。日常开发里建议继续使用直接调用:
m.dashboard_home__my()清理后建议再跑一次:
vpr @01mvp/product#i18n:unused
vpr @01mvp/product#i18n:translate:check
vpr @01mvp/i18n#test
vpr @01mvp/i18n#type-check自动化建议
本地开发时,最简单的做法是修改 zh.json 后直接让 AI agent 运行:
vpr @01mvp/product#i18n:sync
vpr @01mvp/i18n#testCI 里可以先跑:
vpr @01mvp/product#i18n:translate:check如果检查失败,说明某个目标语言缺 key、源文案变了,或者目标文件有多余 key。此时在本地或自动化任务里运行 vpr @01mvp/product#i18n:sync,再提交更新后的消息文件、translation.lock.json 和生成结果。
新增语言
运行同步命令
新增语言不需要手动复制 JSON 文件。直接让脚本更新 Inlang settings、生成消息文件并重新生成 Paraglide,例如新增韩语:
vpr @01mvp/product#i18n:sync -- --add-locale ko如果想先看范围,可以加 --dry-run,这个模式不会写文件:
vpr @01mvp/product#i18n:translate -- --add-locale ko --dry-run检查页面语言切换
公共导航使用 products/01mvp/apps/web/src/shared/ui/locale-switcher.tsx。新增 locale 后,语言切换器会读取 Paraglide runtime 中的 locale 列表。
同步文档语言配置
如果新语言也要用于 MDX 文档,需要同步 products/01mvp/apps/web/src/lib/docs/server.ts 的 Fumadocs languages,并补充对应的 meta.<locale>.json 和 *.{locale}.mdx 文件。
拆分消息文件
当前 Paraglide 配置使用 ./messages/{locale}.json,也就是每个 locale 一个 JSON 文件。这个结构最简单,适合模板默认规模。
如果后续 key 很多,可以把翻译脚本扩展为多 catalog 输入,例如 messages/{locale}/common.json、messages/{locale}/auth.json。做这个改动时需要同步三处:
- Inlang
pathPattern - 翻译脚本的 catalog 读取和锁文件 key 命名
- message 结构对齐测试
在拆分前,继续保持扁平 key 命名即可。
路由与链接
模板使用 TanStack Router 文件路由,locale 前缀由 products/01mvp/packages/i18n/src/tanstack-start 里的适配层处理。需要保留当前语言时,优先使用 @01mvp/i18n/tanstack-start/components/link 中的 Link 组件,避免手写 URL 拼接。
MDX 文档国际化
Fumadocs 文档当前以中文页面为主,例如 page.zh.mdx。新增其他语言版本时,在同一目录补充对应 locale 后缀的 MDX 文件,并确保文档 source 配置能识别该语言。
常见问题
相关资源
这篇文档有问题?