A CLI da Lingo.dev traduz arquivos Markdoc e catálogos JSON de strings de UI por meio de um engine de localização configurado. O Markdoc é um formato de autoria baseado em Markdown com tags personalizadas tipadas e integradas ao React — ideal para sites em Next.js App Router que combinam conteúdo extenso com componentes interativos.
Este guia mostra como localizar um site em Next.js App Router de ponta a ponta: configurar a CLI, organizar o conteúdo por idioma, renderizar Markdoc em rotas dinâmicas e automatizar traduções com o GitHub App da Lingo.dev.
Repositório de demonstração
Faça clone ou fork de lingodotdev/markdoc-nextjs-localization-example para acompanhar o passo a passo. O repositório traz um app funcional em Next.js App Router com conteúdo em Markdoc, configuração da CLI da Lingo.dev e um workflow de CI.
Como funciona a localização com Next.js + Markdoc#
A maioria dos sites em Next.js App Router divide o conteúdo localizado em duas camadas:
| Camada | O que fica aqui | Arquivo de exemplo |
|---|---|---|
| Conteúdo extenso | Páginas de marketing, documentação, posts de blog | src/content/en/pages/home.md |
| Strings de UI | Rótulos da navbar, CTAs, estados de botões | src/content/en/ui.json |
As rotas ficam em src/app/[lang]/ e leem, no momento da requisição, os arquivos do idioma correspondente. Um middleware define um idioma padrão com base no cabeçalho Accept-Language do navegador e redireciona caminhos sem prefixo, como /, para /en (ou para a melhor correspondência disponível).
A CLI processa arquivos Markdoc preservando o frontmatter e as tags personalizadas, e trata o catálogo de strings de UI como JSON. Em ambos os casos, ela traduz o delta pelo seu engine de localização e grava arquivos por idioma ao lado do conteúdo de origem.
Pré-requisitos#
Crie um engine de localização
A cada execução do CLI, o conteúdo passa por um engine de localização — a configuração que define qual modelo de LLM, glossary, voz da marca e rules serão aplicados. Crie um no Lingo.dev dashboard e gere uma API key para CI.
Verifique o Node.js
A CLI exige Node.js 22 ou superior:
node -vConfigure seu projeto Next.js
Seu projeto precisa do App Router (src/app/) e de um diretório de conteúdo por idioma. O repositório de demonstração usa um diretório por idioma em src/content/ (por exemplo, src/content/en/), com duas subpastas (pages/ e blog/) e um arquivo ui.json. Consulte internationalization do Next.js para entender o básico do roteamento.
Organize o conteúdo#
Separe o conteúdo por função. Páginas e posts longos são escritos em Markdoc; strings curtas de UI ficam em JSON para que os componentes possam carregá-las diretamente.
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/Arquivos Markdoc aceitam frontmatter para metadados por página (título, descrição, data, autor) e tags personalizadas renderizadas como componentes React. Uma página mínima se parece com isto:
---
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.Configure a CLI#
Instale a CLI e faça login:
npm install -g @lingo.dev/cli
lingo loginDepois, gere a configuração e vincule-a ao seu engine:
lingo init
lingo linklingo init cria .lingo/config.json com seus idiomas de origem e destino, além dos padrões de arquivo que devem ser traduzidos; lingo link adiciona seu orgId e engineId. Faça commit de .lingo/config.json para que toda a equipe e todas as execuções de CI usem a mesma configuração.
Neste projeto, a configuração define dois padrões de arquivo — um para o conteúdo em Markdoc e outro para o catálogo de strings de 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" }
]
}O segmento de idioma em cada caminho é substituído de acordo com o idioma de destino: src/content/en/pages/home.md vira src/content/es/pages/home.md, e src/content/en/ui.json vira src/content/de/ui.json. O caminho de origem precisa conter o código do idioma. Os formatos são detectados automaticamente pela extensão do arquivo, então arquivos Markdoc (.md) e JSON (.json) não precisam de tipo explícito. Consulte Configuration e Formats para mais detalhes.
Catálogos em arquivo único
A nova CLI espera um arquivo por idioma, com o código do idioma no caminho (como acima). Se suas strings de UI estiverem em um único arquivo JSON com vários idiomas, esse layout (o antigo bucket json-per-locale) ainda não é compatível com a nova CLI — mantenha-o na legacy CLI e acompanhe o changelog para saber quando haverá suporte. Separar em um arquivo por idioma é a abordagem recomendada.
Renderize Markdoc no App Router#
Uma rota dinâmica típica carrega um documento e renderiza a árvore transformada. O repositório de demonstração expõe um helper simples:
// 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 };
}A página do App Router é um wrapper enxuto que combina o documento com strings de UI específicas do idioma:
// 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>
);
}As tags personalizadas do Markdoc (callout, bento, blog-hero etc.) são declaradas em markdoc.schema.ts e conectadas a componentes React em src/components/markdoc/. Consulte a documentação do schema do Markdoc para ver a API completa.
Detecte o idioma no middleware#
O middleware do Next.js inspeciona a requisição antes que a rota seja renderizada. Use-o para redirecionar caminhos sem prefixo para o idioma com melhor correspondência com base no cabeçalho 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|.*\\..*).*)", ],
};Os visitantes chegam a /en, /es, /fr ou /de sem precisar digitar o prefixo.
Traduza localmente#
Depois de lingo login, execute um push. Na primeira execução — ou após adicionar um novo idioma de destino — faça o preenchimento completo:
lingo push --backfill-missingNas próximas execuções, basta enviar o delta:
lingo pushlingo push lê todos os arquivos que correspondem aos seus padrões, identifica as entradas ainda não traduzidas usando o lock file (.lingo/lock.json, versionado no repositório), traduz o delta pelo seu engine de localização, aguarda a conclusão e grava os resultados no diretório de cada idioma de destino. As chaves do frontmatter, as tags personalizadas do Markdoc e as estruturas JSON são preservadas — apenas o texto traduzível muda. Para buscar traduções geradas em outro lugar (por exemplo, no CI), execute lingo pull.
Para limitar uma execução a arquivos específicos, passe um glob:
lingo push "src/content/en/blog/*.md"Automatize no CI#
Instale o GitHub App da Lingo.dev e conecte-o ao seu repositório. Ele lê .lingo/config.json e o engineId vinculado no servidor e depois abre um pull request de tradução sempre que o conteúdo de origem muda — sem arquivo de workflow, sem runner, sem segredo de chave de API e sem precisar gerenciar o lock file manualmente.
Verifique antes de fazer deploy#
Use lingo check como gate de deploy para garantir que nenhum conteúdo sem tradução chegue à produção. Ele retorna um status diferente de zero se ainda houver entradas que precisem ser traduzidas:
lingo checkAdicione isso como uma etapa separada de CI antes do build do Next.js:
- name: Verify translations
run: lingo check
env:
LINGO_API_KEY: ${{ secrets.LINGO_API_KEY }}
- name: Build
run: pnpm build