Lingo.dev CLI は、設定した ローカライゼーションエンジン を通して Markdoc ファイルと JSON の UI 文字列カタログを翻訳します。Markdoc は Markdown ベースのオーサリング形式で、型付きの React ベースカスタムタグに対応しており、長文コンテンツとインタラクティブなコンポーネントを組み合わせる Next.js App Router サイトに適しています。
このガイドでは、Next.js App Router サイトをエンドツーエンドでローカライズする方法を紹介します。CLI の設定から、ロケールごとのコンテンツ整理、動的ルートでの Markdoc のレンダリング、Lingo.dev GitHub App を使った翻訳の自動化までを一通りカバーします。
デモリポジトリ
一緒に進めるには、lingodotdev/markdoc-nextjs-localization-example をクローンまたはフォークしてください。このリポジトリには、動作する Next.js App Router アプリ、Markdoc コンテンツ、Lingo.dev CLI の設定、CI ワークフローが含まれています。
Next.js + Markdoc のローカライゼーションの仕組み#
多くの Next.js App Router サイトでは、ローカライズ対象のコンテンツを 2 つのレイヤーに分けています。
| レイヤー | 内容 | ファイル例 |
|---|---|---|
| 長文コンテンツ | マーケティングページ、ドキュメント、ブログ記事 | src/content/en/pages/home.md |
| UI 文字列 | ナビゲーションバーのラベル、CTA、ボタンの状態 | src/content/en/ui.json |
ルートは src/app/[lang]/ 配下に置かれ、リクエスト時に対応するロケールのファイルを読み込みます。middleware はブラウザーの Accept-Language ヘッダーからデフォルトのロケールを判定し、/ のようなプレフィックスなしのパスを /en(または最適な一致先)へリダイレクトします。
CLI は、frontmatter とカスタムタグを保持したまま Markdoc ファイルを解析し、UI 文字列カタログは JSON として処理します。どちらも差分だけをローカライゼーションエンジン経由で翻訳し、ソースと同じ場所にロケールごとのファイルとして書き出します。
前提条件#
ローカライゼーションエンジンを作成する
CLI を実行するたびに、コンテンツはローカライゼーションエンジンを通じて処理されます。これは、使用する LLM モデルや、適用する用語集、ブランドボイス、ルールを決める設定です。Lingo.dev dashboardで作成し、CI 用のAPI keyを生成してください。
Node.js を確認する
CLI の利用には Node.js 22 以上が必要です。
node -vNext.js プロジェクトをセットアップする
この構成では、App Router(src/app/)とロケールごとのコンテンツディレクトリが必要です。デモ用リポジトリでは、src/content/ 配下にロケールごとのディレクトリ(例: src/content/en/)を置き、その中に 2 つのサブフォルダ(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 ファイルは、ページごとのメタデータ(title、description、date、author)を記述する 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 を設定する#
まずは CLI をインストールしてサインインします。
npm install -g @lingo.dev/cli
lingo login続いて設定を生成し、エンジンにリンクします。
lingo init
lingo linklingo init を実行すると、ソースロケールとターゲットロケール、翻訳対象のファイルパターンを含む .lingo/config.json が作成されます。lingo link は orgId と engineId を追加します。チーム全員と CI で同じ設定を使えるように、.lingo/config.json はコミットしておきましょう。
このプロジェクトでは、設定で 2 つのファイルパターンを定義しています。1 つは Markdoc コンテンツ用、もう 1 つは UI 文字列カタログ用です。
{
"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 は、ロケールごとに 1 ファイル、かつパス内にロケールコードが含まれる構成(上記の形式)を前提としています。UI 文字列を 1 つの多ロケール JSON ファイルで管理している場合、そのレイアウト(以前の json-per-locale バケット)はまだ新しい CLI ではサポートされていません。その場合は引き続き legacy CLI を使い、対応状況は changelog を確認してください。推奨されるのは、ロケールごとに 1 ファイルへ分割する構成です。
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 を参照してください。
Middleware でロケールを判定する#
Next.js の middleware は、ルートがレンダリングされる前にリクエストを検査します。これを使えば、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 にアクセスできます。
ローカルで翻訳する#
lingo login の後に push を実行します。初回実行時、または新しい対象ロケールを追加した直後は、まず全体をバックフィルします。
lingo push --backfill-missingそれ以降は、差分だけを push すれば十分です。
lingo pushlingo push は、設定したパターンに一致するすべてのファイルを読み取り、lock file(.lingo/lock.json、要コミット)を使って未翻訳のエントリを特定し、差分をローカライゼーションエンジン経由で翻訳します。完了後、結果は各対象ロケールのディレクトリに書き込まれます。frontmatter のキー、Markdoc のカスタムタグ、JSON の構造はそのまま維持され、変更されるのは翻訳可能なテキストだけです。別の場所(たとえば CI)で生成された翻訳を取得するには、lingo pull を実行してください。
特定のファイルだけを対象に実行するには、glob を渡します。
lingo push "src/content/en/blog/*.md"CI で自動化する#
Lingo.dev GitHub App をインストールして、対象のリポジトリに接続します。サーバー側で .lingo/config.json とリンク済みの engineId を読み取り、ソースコンテンツが変更されるたびに翻訳用のプルリクエストを作成します。ワークフローファイルも runner も API key の secret も不要で、lock file の管理に悩まされることもありません。
デプロイ前に確認する#
lingo check はデプロイ前のゲートとして使えます。未翻訳コンテンツが本番環境に出るのを防ぎ、翻訳が必要なエントリが 1 つでも残っている場合は非ゼロステータスで終了します。
lingo checkこれを Next.js の build 前に実行する、独立した CI ステップとして追加します。
- name: Verify translations
run: lingo check
env:
LINGO_API_KEY: ${{ secrets.LINGO_API_KEY }}
- name: Build
run: pnpm build