跳转到主要内容
vibestrap 开箱即支持中英双语。两套系统协作:next-intl 负责 UI 字符串(在 messages/ 下),加上一层薄薄的命名约定负责 MDX 内容(在 content/ 下)。 熟悉这套规则后,写两遍就行,不需要额外仪式。

前置条件

无。管线都接好了,你只管加文件。

路由如何识别语言

路由策略是 as-needed(见 src/i18n/routing.ts):
  • 英文(默认)走根路径:/docs/quickstart
  • 中文加前缀:/zh/docs/quickstart
  • 语言列表来自 siteConfig.i18n.locales['en', 'zh']
每个带 locale 前缀的页面必须在顶部调用 setRequestLocale(locale)—— 不调的话 server component 会退回到默认语言,所有 t(...) 都默默用错语言。
const { locale } = await params;
setRequestLocale(locale);

MDX 命名约定

每篇内容两个文件并排放:
content/blog/welcome-to-vibestrap.mdx       # 英文
content/blog/welcome-to-vibestrap.zh.mdx    # 中文
.zh.mdx 后缀是唯一的信号——没有单独的目录,没有 JSON 清单。 src/lib/source-helpers.ts 里的 helper 会去掉后缀算出 canonical slug。

按语言查找

三个 helper 覆盖所有场景:
import { pickByLocale, findBySlug, canonicalSlugs } from '@/lib/source';

// 全部博客,按当前语言过滤(zh 缺失时自动回落到 en)
const posts = pickByLocale(blog.docs, locale);

// 按 slug 找单篇
const post = findBySlug(blog.docs, slug, locale);

// generateStaticParams 用的 slug 列表(只取 en 文件)
const slugs = canonicalSlugs(blog.docs);
pickByLocale('zh') 优先返回 .zh.mdx,没有就回落到英文版—— 所以 /zh/ 永远不会因为缺翻译而 404。

UI 字符串——两份文件,同一套 key

翻译放在 messages/en.jsonmessages/zh.json。两份文件的 key 结构必须一致, 靠 scripts/check-i18n.mjs 做结构 diff。
import { useTranslations } from 'next-intl';

const t = useTranslations('Home.hero');
return <h1>{t('title')}</h1>;
en.json 加新 key 时,请同时在 zh.json 同位置加上—— 哪怕中文是占位符。否则 validator 会报错。

新增一种语言

  1. 追加到 siteConfig.i18n.locales(如 ['en', 'zh', 'ja'])。
  2. 创建 messages/ja.json,结构镜像 en.json
  3. 任何想翻译的 MDX,加上 slug.ja.mdx
  4. 如果要非 en 的回落策略,在 pickByLocale 里加分支。
完事——路由会自动识别新语言。

验证一下

node scripts/check-i18n.mjs   # 结构 diff + 字面 t() 校验
pnpm typecheck                # 类型 OK
pnpm dev                      # 访问 /docs 和 /zh/docs
Validator 会报告只在一份文件里存在的 key,以及那些在当前命名空间下找不到的 t('foo.bar') 字面调用。

常见坑(5 个)

  1. 忘写 .zh.mdx——/zh/ 页面悄悄回落到英文。开发时切到中文模式手动点一遍才能发现。
  2. 两份文件的日期对不上——更新英文博客时记得把 .zh.mdx 的 date 也改了, 否则首页排序顺序会分叉。
  3. 模板字符串 t() 调用——t(\Foo.$`)` 没法静态校验, validator 只能检查前缀存在。要靠实际跑页面来兜底。
  4. 少了 setRequestLocale(locale)——会让 locale 前缀页面用默认语言渲染, 极易漏掉。
  5. 直接用 next/link——一定要从 @/i18n/navigation 导入 LinkuseRouter,否则 client 跳转会丢掉 locale 前缀。

官方文档