Lingo.dev 的 CLI 可通过已配置的 localization engine,翻译 Markdoc 文件和 JSON UI 文案目录。Markdoc 是一种基于 Markdown 的内容编写格式,支持类型化、由 React 驱动的自定义标签,非常适合那些将长篇内容与交互式组件结合在一起的 Next.js App Router 站点。
本指南将带你从头到尾完成 Next.js App Router 站点的本地化:包括配置 CLI、按语言区域组织内容、在动态路由中渲染 Markdoc,以及借助 GitHub Actions 自动化翻译流程。
示例仓库
你可以 clone 或 fork lingodotdev/markdoc-nextjs-localization-example 跟着一起实践。该仓库包含一个可运行的 Next.js App Router 应用、Markdoc 内容、Lingo.dev CLI 配置,以及 GitHub Actions 工作流。
Next.js + Markdoc 本地化如何运作#
大多数 Next.js App Router 站点都会把本地化内容拆成两层:
| 层级 | 内容类型 | 示例文件 |
|---|---|---|
| 长篇内容 | 营销页面、文档、博客文章 | src/content/[locale]/pages/home.md |
| UI 文案 | 导航栏标签、CTA、按钮状态 | src/content/[locale]/ui.json |
路由位于 src/app/[lang]/ 下,并在请求到来时读取对应语言区域的文件。中间件会根据浏览器的 Accept-Language 请求头选择默认语言区域,并将 / 这类不带前缀的路径重定向到 /en(或最佳匹配项)。
CLI 的 markdoc bucket 会在保留 frontmatter 和自定义标签的前提下解析 Markdoc 文件,而 json bucket 则负责处理 UI 文案目录。二者都会通过你的 localization engine 翻译增量内容,并将各语言区域的文件写回源文件旁边。
准备工作#
创建 localization engine
每次运行 CLI 时,内容都会经过一个 localization engine——也就是决定采用哪个 LLM 模型、glossary、brand voice 和 instructions 的那套配置。你可以在 Lingo.dev dashboard 中创建一个,并生成 API key。
确认 Node.js 版本
CLI 需要 Node.js 18 或更高版本:
node -v设置 Next.js 项目
你的项目需要启用 App Router(src/app/),并按语言区域组织内容目录。示例仓库使用 src/content/[locale]/,其中包含两个子文件夹(pages/ 和 blog/)以及一个 ui.json 文件。关于路由基础,可参考 Next.js internationalization。
组织内容#
按内容职责拆分最清晰:长篇页面和文章使用 Markdoc 编写,简短的 UI 文案则放在 JSON 中,方便组件直接加载。
src/content/
en/ # Source locale
pages/home.md # Long-form Markdoc
blog/hello.md
ui.json # UI strings (navbar, CTAs, button states)
es/ # Target locales – generated by Lingo.dev
fr/
de/Markdoc 文件支持通过 frontmatter 定义页面元数据(标题、描述、日期、作者),也支持可渲染为 React 组件的自定义标签。一个最小示例如下:
---
title: Author once in Markdoc, ship in every language.
description: An example Next.js App Router app that localizes Markdoc with Lingo.
---
{% inline-callout type="info" %}
This page is authored in Markdoc and translated by Lingo.dev.
{% /inline-callout %}
## Built from three pieces
Markdoc custom tags render as React components – even interactive ones.配置 CLI#
在项目根目录创建一个 i18n.json 文件,并声明两个 bucket——一个用于 Markdoc 内容,另一个用于 UI 文案目录:
{
"$schema": "https://lingo.dev/schema/i18n.json",
"version": "1.15",
"locale": {
"source": "en",
"targets": ["es", "fr", "de"]
},
"buckets": {
"markdoc": {
"include": [
"src/content/[locale]/pages/*.md",
"src/content/[locale]/blog/*.md"
]
},
"json": {
"include": ["src/content/[locale]/ui.json"]
}
}
}[locale] 占位符会解析为每个已配置的语言区域代码。使用 source: "en" 时,CLI 会从 src/content/en/ 读取内容,并将翻译后的文件写入 src/content/es/、src/content/fr/ 和 src/content/de/。
单文件目录
如果你的 UI 文案保存在一个多语言 JSON 文件中,而不是每种语言各一个文件,请使用 json-per-locale bucket 类型。完整 bucket 类型列表可参阅 Static Content Localization。
在 App Router 中渲染 Markdoc#
典型的动态路由会加载文档并渲染转换后的内容树。示例仓库提供了一个简洁的辅助函数:
// src/lib/markdoc.ts
export async function loadDoc(
locale: Locale,
collection: "pages" | "blog",
slug: string,
) {
const raw = await fs.readFile(
path.join(process.cwd(), "src/content", locale, collection, `${slug}.md`),
"utf8",
);
const ast = Markdoc.parse(raw);
const frontmatter = ast.attributes.frontmatter
? parseFrontmatter(ast.attributes.frontmatter)
: {};
const content = Markdoc.transform(ast, { ...schema, variables: { frontmatter } });
return { frontmatter, content };
}App Router 页面本身只是一个轻量封装,用来将文档与特定语言区域的 UI 文案配对:
// src/app/[lang]/page.tsx
export default async function Home({ params }: PageProps<"/[lang]">) {
const { lang } = await params;
const doc = await loadDoc(lang, "pages", "home");
const { home } = await getMessages(lang);
return (
<main>
<h1>{doc.frontmatter.title}</h1>
{renderMarkdoc(doc.content)}
</main>
);
}自定义 Markdoc 标签(callout、bento、blog-hero 等)在 markdoc.schema.ts 中声明,并绑定到 src/components/markdoc/ 下的 React 组件。完整 API 请参阅 Markdoc schema docs。
在中间件中检测语言区域#
Next.js 中间件会在路由渲染前检查请求。你可以利用它根据 Accept-Language 请求头,将不带前缀的路径重定向到最佳匹配的语言区域:
// src/middleware.ts
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const hasLocale = locales.some(
(locale) => pathname === `/${locale}` || pathname.startsWith(`/${locale}/`),
);
if (hasLocale) return;
const locale = pickLocale(request); // parses Accept-Language
const url = request.nextUrl.clone();
url.pathname = `/${locale}${pathname === "/" ? "" : pathname}`;
return NextResponse.redirect(url);
}
export const config = {
matcher: ["/((?!_next|api|.*\\..*).*)", ],
};访客无需手动输入前缀,也能直接落到 /en、/es、/fr 或 /de。
本地翻译#
设置好 API key 后,运行 CLI:
export LINGO_API_KEY="your-api-key"
npx lingo.dev@latest runCLI 会读取所有匹配 bucket 模式的文件,借助 lockfile 识别未翻译条目,通过你的 localization engine 翻译增量内容,并将结果写入每个目标语言区域的目录。frontmatter 键、自定义 Markdoc 标签和 JSON 结构都会被完整保留——变化的只有可翻译文本。
如果你在开发过程中只想处理某个特定语言区域:
npx lingo.dev@latest run --target-locale es使用 GitHub Actions 自动化#
在 .github/workflows/translate.yml 中添加一个工作流文件,即可在每次 push 时自动执行翻译:
翻译结果直接提交到 main——几乎零摩擦,非常适合小团队:
name: Translate
on:
push:
branches: [main]
permissions:
contents: write
jobs:
translate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lingo.dev
uses: lingodotdev/lingo.dev@main
with:
api-key: ${{ secrets.LINGODOTDEV_API_KEY }}在你的 GitHub 仓库中,将 API key 以 LINGODOTDEV_API_KEY 的名称保存到 Settings > Secrets and variables > Actions。
部署前校验#
使用 --frozen 标志作为部署门禁,确保未翻译内容不会进入生产环境。如果仍有条目需要翻译,CLI 会以非零状态码退出:
npx lingo.dev@latest run --frozen可在 Next.js 构建前,将这一步单独加入 CI:
- name: Verify translations
run: npx lingo.dev@latest run --frozen
- name: Build
run: pnpm build