Lingo.dev CLI は、設定した ローカライゼーションエンジン を通して Markdoc ファイルと JSON の UI 文字列カタログを翻訳します。Markdoc は Markdown ベースのオーサリング形式で、型付きの React ベースカスタムタグに対応しており、長文コンテンツとインタラクティブなコンポーネントを組み合わせる Next.js App Router サイトに適しています。
このガイドでは、Next.js App Router サイトをエンドツーエンドでローカライズする手順を解説します。CLI の設定、ロケール別コンテンツの整理、動的ルートでの Markdoc のレンダリング、そして GitHub Actions を使った翻訳の自動化までをカバーします。
デモリポジトリ
一緒に進めるには、lingodotdev/markdoc-nextjs-localization-example を clone または fork してください。このリポジトリには、Markdoc コンテンツを含む動作する Next.js App Router アプリ、Lingo.dev CLI の設定、GitHub Actions のワークフローが含まれています。
Next.js + Markdoc のローカライゼーションの仕組み#
多くの Next.js App Router サイトでは、ローカライズ対象のコンテンツを 2 つのレイヤーに分けています。
| レイヤー | 内容 | ファイル例 |
|---|---|---|
| 長文コンテンツ | マーケティングページ、ドキュメント、ブログ記事 | src/content/[locale]/pages/home.md |
| UI 文字列 | ナビゲーションバーのラベル、CTA、ボタンの状態 | src/content/[locale]/ui.json |
ルートは src/app/[lang]/ 配下に置かれ、リクエスト時に対応するロケールのファイルを読み込みます。middleware はブラウザーの Accept-Language ヘッダーからデフォルトのロケールを判定し、/ のようなプレフィックスなしのパスを /en(または最適な一致先)へリダイレクトします。
CLI の markdoc バケットは、frontmatter とカスタムタグを保ったまま Markdoc ファイルを解析し、json バケットは UI 文字列カタログを処理します。どちらも差分のみをローカライゼーションエンジン経由で翻訳し、ソースと並ぶ形でロケール別ファイルを書き出します。
前提条件#
ローカライゼーションエンジンを作成する
CLI を実行するたびに、コンテンツは ローカライゼーションエンジン を通過します。これは、適用する LLM モデル、glossary、ブランドボイス、instructions を決める設定です。Lingo.dev dashboard で作成し、API key を生成してください。
Node.js を確認する
CLI の利用には Node.js 18 以上が必要です。
node -vNext.js プロジェクトをセットアップする
プロジェクトには App Router(src/app/)と、ロケールごとのコンテンツディレクトリが必要です。デモリポジトリでは src/content/[locale]/ を使い、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 を設定する#
プロジェクトルートに i18n.json ファイルを作成します。Markdoc コンテンツ用と UI 文字列カタログ用の 2 つのバケットを定義します。
{
"$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 文字列がロケールごとの 1 ファイルではなく、単一の多ロケール JSON ファイルにまとまっている場合は、json-per-locale バケットタイプを使ってください。バケットタイプの一覧は 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 を参照してください。
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 にアクセスできます。
ローカルで翻訳する#
API key を設定して CLI を実行します。
export LINGO_API_KEY="your-api-key"
npx lingo.dev@latest runCLI はバケットパターンに一致するすべてのファイルを読み込み、lockfile を使って未翻訳のエントリを特定し、差分のみをローカライゼーションエンジン経由で翻訳して、各対象ロケールのディレクトリに結果を書き出します。frontmatter のキー、Markdoc のカスタムタグ、JSON の構造は保持され、変わるのは翻訳対象のテキストだけです。
開発中に特定のロケールだけを対象にするには:
npx lingo.dev@latest run --target-locale esGitHub Actions で自動化する#
プッシュのたびに翻訳を実行するには、.github/workflows/translate.yml にワークフローファイルを追加します。
翻訳は 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 リポジトリの Settings > Secrets and variables > Actions で、API key を LINGODOTDEV_API_KEY として保存してください。
デプロイ前に確認する#
未翻訳のコンテンツが本番環境に出ないよう、デプロイゲートとして --frozen フラグを使ってください。翻訳が必要なエントリが 1 つでもあれば、CLI は非ゼロステータスで終了します。
npx lingo.dev@latest run --frozenこれを Next.js の build 前に実行する、独立した CI ステップとして追加します。
- name: Verify translations
run: npx lingo.dev@latest run --frozen
- name: Build
run: pnpm build