Lingo.dev CLI переводит файлы Markdoc и JSON-каталоги строк интерфейса через настроенный движок локализации. Markdoc — это формат для работы с контентом на базе Markdown с типизированными пользовательскими тегами на React — отличный выбор для сайтов на Next.js App Router, где длинные тексты сочетаются с интерактивными компонентами.
В этом руководстве разберём локализацию сайта на Next.js App Router от начала до конца: настройку CLI, организацию контента по локалям, рендеринг Markdoc в динамических маршрутах и автоматизацию переводов через GitHub Actions.
Демо-репозиторий
Клонируйте или форкните lingodotdev/markdoc-nextjs-localization-example, чтобы пройти всё на практике. В репозитории есть готовое приложение на Next.js App Router с контентом Markdoc, конфигурацией CLI Lingo.dev и процессом GitHub Actions.
Как работает локализация в Next.js + Markdoc#
В большинстве сайтов на Next.js App Router локализованный контент делится на два слоя:
| Слой | Что здесь хранится | Пример файла |
|---|---|---|
| Длинные тексты | Маркетинговые страницы, документация, посты в блоге | src/content/[locale]/pages/home.md |
| Строки интерфейса | Подписи в навигации, CTA, состояния кнопок | src/content/[locale]/ui.json |
Маршруты находятся в src/app/[lang]/ и при каждом запросе читают файлы нужной локали. Middleware определяет локаль по умолчанию из заголовка браузера Accept-Language и перенаправляет пути без префикса, например /, на /en (или на наиболее подходящий вариант).
Бакет markdoc в CLI разбирает файлы Markdoc, сохраняя frontmatter и пользовательские теги, а бакет json обрабатывает каталог строк интерфейса. Оба переводят только изменения через ваш движок локализации и записывают файлы для каждой локали рядом с исходниками.
Что понадобится#
Создайте движок локализации
При каждом запуске CLI контент проходит через движок локализации — конфигурацию, которая определяет, какая LLM-модель, глоссарий, тональность бренда и инструкции будут применяться. Создайте его в панели Lingo.dev и сгенерируйте API-ключ.
Проверьте Node.js
Для CLI требуется Node.js 18 или выше:
node -vНастройте проект Next.js
В проекте должны быть App Router (src/app/) и директория контента по локалям. В демо-репозитории используется src/content/[locale]/ с двумя вложенными папками (pages/ и blog/) и файлом ui.json. Базовые принципы маршрутизации см. в интернационализации Next.js.
Организуйте контент#
Разделяйте контент по назначению. Длинные страницы и посты создаются в Markdoc, а короткие строки интерфейса хранятся в 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 для метаданных страницы (title, description, date, author) и пользовательские теги, которые рендерятся как 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, второй — для каталога строк интерфейса:
{
"$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/.
Каталоги в одном файле
Если строки интерфейса хранятся в одном многоязычном JSON-файле, а не в отдельных файлах для каждой локали, используйте тип бакета json-per-locale. Полный список типов бакетов — в разделе Локализация статического контента.
Рендеринг Markdoc в App Router#
Обычно динамический маршрут загружает документ и рендерит преобразованное дерево. В демо-репозитории для этого есть небольшой helper:
// 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 — это тонкая обёртка, которая связывает документ со строками интерфейса для конкретной локали:
// 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 и подключаются к React-компонентам из src/components/markdoc/. Полное API смотрите в документации по схеме Markdoc.
Определяйте локаль в middleware#
Middleware в 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-ключ и запустите CLI:
export LINGO_API_KEY="your-api-key"
npx lingo.dev@latest runCLI читает все файлы, подходящие под шаблоны ваших бакетов, определяет непереведённые записи с помощью lockfile, переводит только изменения через ваш движок локализации и записывает результат в директорию каждой целевой локали. Ключи 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 }}Сохраните API-ключ как LINGODOTDEV_API_KEY в Settings > Secrets and variables > Actions вашего репозитория GitHub.
Проверяйте перед деплоем#
Используйте флаг --frozen как барьер перед деплоем, чтобы гарантировать, что в production не попадёт непереведённый контент. Если какие-либо записи требуют перевода, CLI завершится с ненулевым статусом:
npx lingo.dev@latest run --frozenДобавьте это как отдельный шаг CI перед сборкой Next.js:
- name: Verify translations
run: npx lingo.dev@latest run --frozen
- name: Build
run: pnpm build