TemplateCore
00 / 00

国际化

TanStack Start 下的 Paraglide 国际化接入、翻译管理与路由策略

策略概览

项目使用 Paraglide 接入 TanStack Start,默认内置四种语言,也可以继续添加新的 BCP 47 locale:

语言Locale 代码URL
简体中文(默认)zh//pricing
繁体中文zh-Hant/zh-Hant/zh-Hant/pricing
Englishen/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_KEYOPENAI_BASE_URLOPENAI_MODEL
  • 不绑定第三方翻译平台账号,也不需要给额外的 i18n SaaS 付费。
  • 支持增量翻译:translation.lock.json 记录每个 key 的源文案 hash,只有新增或源文案变化的 key 会请求模型。
  • 支持批量、多 locale、自动新增 locale、指定 key 前缀、dry-run 和 check,适合本地开发、CI 和 AI agent 自动化。
  • language_nameIntl.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.mjsOpenAI-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.tsxgetLocale() 设置 <html lang>
products/01mvp/apps/web/src/routes/{-$locale}TanStack Router 的可选 locale 前缀
products/01mvp/packages/i18n/src/tanstack-startLink、navigate、redirect、route helper 的本地适配层

products/01mvp/packages/i18n/src/vite/plugin.js 的 URL 规则会把无前缀路径解析为中文,所以首访 / 会显示中文,不会因为浏览器语言是 English 自动跳到 /en。如果以后希望英文浏览器自动进入英文 URL,需要单独调整策略和重定向规则,并重新验证 SEO 与登录后路由。

目录结构

zh.json
zh-Hant.json
en.json
ja.json
settings.json
translate-messages.mjs
unused-messages.mjs
products/01mvp/packages/i18n/translation.lock.json

使用翻译

本页命令默认从仓库根目录运行,使用 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-check

products/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:prune

prune 会按 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#test

CI 里可以先跑:

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.jsonmessages/{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 配置能识别该语言。

常见问题

相关资源

这篇文档有问题?