|
文档
预约演示平台
平台MCPCLIAPI工作流
指南
更新日志

本地化

  • 概览
  • 翻译 API
  • Web 应用本地化
  • 移动应用本地化
  • iOS 与 String Catalogs
  • Android 与 strings.xml
  • 邮件本地化
  • 静态内容(如 .md、.json)
  • Next.js + Markdoc
  • Rails + i18n

工作流

  • 通过 MCP 配置引擎
  • Jira 智能分诊
  • CI/CD

使用 Markdoc 为 Next.js App Router 实现本地化

Lingo.dev 的 CLI 可通过已配置的 localization engine,翻译 Markdoc 文件和 JSON UI 文案目录。Markdoc 是一种基于 Markdown 的内容编写格式,支持类型化、由 React 驱动的自定义标签,非常适合那些将长篇内容与交互式组件结合在一起的 Next.js App Router 站点。

本指南将带你完整走通 Next.js App Router 站点本地化的全流程:配置 CLI、按语言区域组织内容、在动态路由中渲染 Markdoc,以及借助 Lingo.dev GitHub App 实现翻译自动化。

示例仓库

克隆或 fork lingodotdev/markdoc-nextjs-localization-example,边看边做。这个仓库包含一个可直接运行的 Next.js App Router 应用、Markdoc 内容、Lingo.dev CLI 配置以及一套 CI 工作流。

Next.js + Markdoc 本地化如何运作#

大多数 Next.js App Router 站点都会把本地化内容拆成两层:

层级内容类型示例文件
长篇内容营销页面、文档、博客文章src/content/en/pages/home.md
UI 文案导航栏标签、CTA、按钮状态src/content/en/ui.json

路由位于 src/app/[lang]/ 下,并在请求到来时读取对应语言区域的文件。中间件会根据浏览器的 Accept-Language 请求头选择默认语言区域,并将 / 这类不带前缀的路径重定向到 /en(或最佳匹配项)。

CLI 会解析带有 frontmatter 的 Markdoc 文件,并完整保留自定义标签;UI 文案目录则按 JSON 处理。两类内容都会通过你的本地化引擎翻译增量变更,并按语言区域将文件写回源文件旁。

准备工作#

1

创建 localization engine

每次运行 CLI 时,内容都会经过一个 本地化引擎——它的配置决定了要使用哪个 LLM 模型、glossary、品牌语调 以及 规则。你可以在 Lingo.dev dashboard 中创建一个,并为 CI 生成一个 API key。

2

确认 Node.js 版本

CLI 需要 Node.js 22 或更高版本:

bash
node -v
3

设置 Next.js 项目

你的项目需要启用 App Router(src/app/),并按语言区域拆分内容目录。演示仓库的做法是在 src/content/ 下为每个语言区域建立一个目录(例如 src/content/en/),其中包含两个子文件夹(pages/ 和 blog/)以及一个 ui.json 文件。路由基础可参考 Next.js internationalization。

组织内容#

按内容职责拆分最清晰:长篇页面和文章使用 Markdoc 编写,简短的 UI 文案则放在 JSON 中,方便组件直接加载。

text
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 组件的自定义标签。一个最小示例如下:

markdown
---
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#

安装 CLI 并登录:

bash
npm install -g @lingo.dev/cli
lingo login

接着生成配置,并将其关联到你的引擎:

bash
lingo init
lingo link

lingo init 会创建 .lingo/config.json,写入源语言区域、目标语言区域以及待翻译的文件模式;lingo link 会添加你的 orgId 和 engineId。记得提交 .lingo/config.json,这样团队成员和 CI 每次运行都会使用同一套配置。

在这个项目中,配置声明了两个文件模式:一个用于 Markdoc 内容,另一个用于 UI 文案目录:

json
{
  "orgId": "org_...",
  "engineId": "eng_...",
  "sourceLocale": "en",
  "targetLocales": ["es", "fr", "de"],
  "files": [
    { "pattern": "src/content/en/pages/*.md" },
    { "pattern": "src/content/en/blog/*.md" },
    { "pattern": "src/content/en/ui.json" }
  ]
}

每条路径中的语言区域片段都会按目标语言区域替换:src/content/en/pages/home.md 会变成 src/content/es/pages/home.md,src/content/en/ui.json 会变成 src/content/de/ui.json。源路径中必须包含语言区域代码。格式会根据文件扩展名自动识别,因此 Markdoc(.md)和 JSON(.json)文件都无需显式指定类型。详情可参见 Configuration 和 Formats。

单文件目录

新版 CLI 要求每个语言区域对应一个文件,且路径中必须带有语言区域代码(如上所示)。如果你的 UI 文案仍保存在单个多语言区域 JSON 文件中,这种布局(也就是之前的 json-per-locale bucket)目前还不受新版 CLI 支持——请继续使用 legacy CLI,并关注更新日志以获取支持进展。推荐做法是拆分为每个语言区域一个文件。

在 App Router 中渲染 Markdoc#

典型的动态路由会加载文档并渲染转换后的内容树。示例仓库提供了一个简洁的辅助函数:

ts
// 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 文案配对:

tsx
// 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 请求头,将不带前缀的路径重定向到最佳匹配的语言区域:

ts
// 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。

本地翻译#

执行完 lingo login 后,运行一次 push。首次运行时,或新增目标语言区域后,需要先回填全部内容:

bash
lingo push --backfill-missing

后续运行时,只需推送增量:

bash
lingo push

lingo push 会读取所有匹配文件模式的文件,借助已提交的 lock file(.lingo/lock.json)识别尚未翻译的条目,通过你的本地化引擎翻译增量内容,等待完成后,再将结果写入各个目标语言区域目录。Frontmatter 键、Markdoc 自定义标签和 JSON 结构都会原样保留——变化的只有可翻译文本。若要拉取在其他地方生成的翻译(例如 CI),请运行 lingo pull。

如果只想针对特定文件运行,可传入一个 glob:

bash
lingo push "src/content/en/blog/*.md"

在 CI 中实现自动化#

安装 Lingo.dev GitHub App 并将其连接到你的仓库。它会在服务端读取 .lingo/config.json 和已关联的 engineId,并在源内容发生变更时自动创建翻译 Pull Request——无需工作流文件、无需 runner、无需 API key 密钥,也不用来回处理 lock file。

部署前校验#

可将 lingo check 作为部署前的门禁,确保未翻译内容不会进入生产环境。只要还有条目待翻译,它就会以非零状态码退出:

bash
lingo check

可在 Next.js 构建前,将这一步单独加入 CI:

yaml
- name: Verify translations
  run: lingo check
  env:
    LINGO_API_KEY: ${{ secrets.LINGO_API_KEY }}
- name: Build
  run: pnpm build

下一步#

静态内容本地化
支持 Markdown、MDX、JSON、YAML 等更多文件格式
Web 应用本地化
适用于常见 Web 框架的 UI 文案模式
CI/CD 工作流
GitHub App 与自托管 runner 方案
术语表
锁定品牌名和技术术语,避免被翻译

这个页面对你有帮助吗?

Max PrilutskiyMax Prilutskiy·已更新 8 天前·3 分钟阅读