La CLI de Lingo.dev traduce archivos de Markdoc y catálogos JSON de cadenas de interfaz mediante un motor de localización configurado. Markdoc es un formato de autoría basado en Markdown con etiquetas personalizadas tipadas y respaldadas por React, ideal para sitios con Next.js App Router que combinan contenido extenso con componentes interactivos.
Esta guía te muestra cómo localizar de principio a fin un sitio con Next.js App Router: configurar la CLI, organizar el contenido por idioma, renderizar Markdoc en rutas dinámicas y automatizar las traducciones con la GitHub App de Lingo.dev.
Repositorio de demostración
Clona o haz un fork de lingodotdev/markdoc-nextjs-localization-example para seguir el proceso paso a paso. El repositorio incluye una aplicación funcional de Next.js App Router con contenido de Markdoc, una configuración de la CLI de Lingo.dev y un flujo de trabajo de CI.
Cómo funciona la localización con Next.js + Markdoc#
La mayoría de los sitios con Next.js App Router dividen el contenido localizado en dos capas:
| Capa | Qué incluye | Archivo de ejemplo |
|---|---|---|
| Contenido extenso | Páginas de marketing, documentación y artículos del blog | src/content/en/pages/home.md |
| Cadenas de interfaz | Etiquetas de la barra de navegación, CTA y estados de botones | src/content/en/ui.json |
Las rutas viven en src/app/[lang]/ y leen los archivos del idioma correspondiente en tiempo de ejecución. Un middleware selecciona un idioma predeterminado a partir de la cabecera Accept-Language del navegador y redirige rutas sin prefijo como / a /en (o a la mejor coincidencia).
La CLI analiza archivos de Markdoc sin alterar el frontmatter ni las etiquetas personalizadas, y gestiona el catálogo de cadenas de la interfaz como JSON. Ambos pasan el delta por tu motor de localización y generan archivos por idioma junto al original.
Requisitos previos#
Crea un motor de localización
Cada ejecución de la CLI envía el contenido a través de un motor de localización, la configuración que determina qué modelo de LLM, glosario, voz de marca y reglas se aplican. Crea uno en el panel de Lingo.dev y genera una API key para CI.
Comprueba Node.js
La CLI requiere Node.js 22 o superior:
node -vPrepara tu proyecto de Next.js
Tu proyecto necesita App Router (src/app/) y un directorio de contenido por idioma. El repositorio de demostración usa un directorio por idioma dentro de src/content/ (por ejemplo, src/content/en/) con dos subcarpetas (pages/ y blog/) y un archivo ui.json. Consulta la internacionalización de Next.js para conocer los conceptos básicos del enrutado.
Organiza el contenido#
Divide el contenido según su función. Las páginas y publicaciones largas se redactan en Markdoc; las cadenas cortas de la interfaz se guardan en JSON para que los componentes puedan cargarlas directamente.
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/Los archivos de Markdoc admiten frontmatter para metadatos por página (título, descripción, fecha y autor) y etiquetas personalizadas que se renderizan como componentes de React. Una página mínima tiene este aspecto:
---
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.Configura la CLI#
Instala la CLI e inicia sesión:
npm install -g @lingo.dev/cli
lingo loginDespués, genera la configuración y vincúlala a tu motor:
lingo init
lingo linklingo init crea .lingo/config.json con tus idiomas de origen y destino, además de los patrones de archivo que se van a traducir; lingo link añade tu orgId y engineId. Haz commit de .lingo/config.json para que todo el equipo y cada ejecución de CI compartan la misma configuración.
En este proyecto, la configuración define dos patrones de archivo: uno para el contenido de Markdoc y otro para el catálogo de cadenas de la interfaz:
{
"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" }
]
}El segmento de idioma de cada ruta se sustituye por el de cada idioma de destino: src/content/en/pages/home.md pasa a ser src/content/es/pages/home.md y src/content/en/ui.json pasa a ser src/content/de/ui.json. La ruta de origen debe incluir el código de idioma. Los formatos se detectan automáticamente a partir de la extensión del archivo, así que los archivos de Markdoc (.md) y JSON (.json) no necesitan un tipo explícito. Consulta Configuración y Formatos para ver los detalles.
Catálogos de un solo archivo
La nueva CLI espera un archivo por idioma, con el código de idioma en la ruta (como arriba). Si tus cadenas de interfaz están en un único archivo JSON multidioma, esa estructura (el antiguo bucket json-per-locale) todavía no es compatible con la nueva CLI. En ese caso, mantenla en la legacy CLI y sigue el changelog para enterarte de cuándo habrá compatibilidad. Dividirlo en un archivo por idioma es la opción recomendada.
Renderiza Markdoc en App Router#
Una ruta dinámica típica carga un documento y renderiza el árbol transformado. El repositorio de demostración incluye un pequeño 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 };
}La página de App Router es un contenedor ligero que combina el documento con cadenas de interfaz específicas de cada 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>
);
}Las etiquetas personalizadas de Markdoc (callout, bento, blog-hero, etc.) se declaran en markdoc.schema.ts y se conectan a componentes de React en src/components/markdoc/. Consulta la documentación del esquema de Markdoc para ver la API completa.
Detecta el idioma en el middleware#
El middleware de Next.js inspecciona la solicitud antes de que se renderice una ruta. Úsalo para redirigir rutas sin prefijo al idioma que mejor encaje según la cabecera 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|.*\\..*).*)", ],
};Los visitantes llegan a /en, /es, /fr o /de sin tener que escribir nunca el prefijo.
Traduce en local#
Después de lingo login, ejecuta un push. En la primera ejecución, o tras añadir un nuevo idioma de destino, rellena todo el contenido pendiente:
lingo push --backfill-missingEn las ejecuciones posteriores, solo tienes que enviar el delta:
lingo pushlingo push lee todos los archivos que coinciden con tus patrones, identifica las entradas sin traducir mediante el lock file (.lingo/lock.json, versionado en el repositorio), traduce el delta a través de tu motor de localización, espera a que termine y escribe los resultados en el directorio de cada idioma de destino. Se conservan las claves del frontmatter, las etiquetas personalizadas de Markdoc y la estructura del JSON: solo cambia el texto traducible. Para recuperar traducciones generadas en otro entorno (por ejemplo, en CI), ejecuta lingo pull.
Para limitar una ejecución a archivos concretos, pasa un glob:
lingo push "src/content/en/blog/*.md"Automatiza en CI#
Instala la GitHub App de Lingo.dev y conéctala a tu repositorio. Lee .lingo/config.json y el engineId vinculado del lado del servidor, y abre una pull request de traducción cada vez que cambia el contenido de origen, sin archivo de flujo de trabajo, sin runner, sin secretos de clave de API y sin tener que andar gestionando archivos de bloqueo.
Verifica antes de desplegar#
Usa lingo check como barrera de despliegue para asegurarte de que no llegue contenido sin traducir a producción. Devuelve un estado distinto de cero si alguna entrada sigue necesitando traducción:
lingo checkAñádelo como un paso de CI independiente antes de tu build de Next.js:
- name: Verify translations
run: lingo check
env:
LINGO_API_KEY: ${{ secrets.LINGO_API_KEY }}
- name: Build
run: pnpm build