El CLI de Lingo.dev traduce archivos de Markdoc y catálogos JSON de cadenas de IU 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 lleva de principio a fin por el proceso de localización de un sitio con Next.js App Router: configurar el CLI, organizar el contenido por idioma, renderizar Markdoc en rutas dinámicas y automatizar las traducciones con GitHub Actions.
Repositorio de ejemplo
Clona o haz fork de lingodotdev/markdoc-nextjs-localization-example para seguir el paso a paso. El repositorio incluye una app funcional de Next.js App Router con contenido en Markdoc, una configuración del CLI de Lingo.dev y un flujo de trabajo de GitHub Actions.
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 de blog | src/content/[locale]/pages/home.md |
| Cadenas de IU | Etiquetas de navegación, CTA y estados de botones | src/content/[locale]/ui.json |
Las rutas viven bajo src/app/[lang]/ y leen los archivos del idioma correspondiente en tiempo de solicitud. Un middleware elige un idioma predeterminado a partir del encabezado Accept-Language del navegador y redirige rutas sin prefijo, como /, a /en (o a la mejor coincidencia).
El bucket markdoc del CLI procesa archivos de Markdoc conservando el frontmatter y las etiquetas personalizadas, mientras que el bucket json se encarga del catálogo de cadenas de IU. Ambos traducen el delta a través de tu motor de localización y escriben archivos por idioma junto al contenido de origen.
Requisitos previos#
Crea un motor de localización
Cada ejecución del CLI envía el contenido a través de un motor de localización: la configuración que define qué modelo de LLM, glossary, voz de marca e instructions se aplican. Crea uno en el panel de Lingo.dev y genera una API key.
Verifica Node.js
El CLI requiere Node.js 18 o superior:
node -vConfigura tu proyecto de Next.js
Tu proyecto necesita App Router (src/app/) y un directorio de contenido por idioma. El repositorio de ejemplo usa src/content/[locale]/ con dos subcarpetas (pages/ y blog/) además de un archivo ui.json. Consulta internacionalización de Next.js para conocer los conceptos básicos del enrutamiento.
Organiza el contenido#
Separa el contenido según su función. Las páginas y publicaciones extensas se escriben en Markdoc; las cadenas cortas de IU viven 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), además de etiquetas personalizadas que se renderizan como componentes de React. Una página mínima se ve así:
---
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 el CLI#
Crea un archivo i18n.json en la raíz de tu proyecto. Declara dos buckets: uno para el contenido de Markdoc y otro para el catálogo de cadenas de IU:
{
"$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"]
}
}
}El placeholder [locale] se resuelve al código de cada idioma configurado. Con source: "en", el CLI lee desde src/content/en/ y escribe los archivos traducidos en src/content/es/, src/content/fr/ y src/content/de/.
Catálogos de un solo archivo
Si tus cadenas de IU viven en un único archivo JSON multiidioma, en lugar de un archivo por idioma, usa el tipo de bucket json-per-locale. Consulta Localización de contenido estático para ver la lista completa de tipos de bucket.
Renderiza Markdoc en App Router#
Una ruta dinámica típica carga un documento y renderiza el árbol transformado. El repositorio de ejemplo expone un helper pequeño:
// 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 IU específicas por 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 bajo 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 coincida según el encabezado 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 el prefijo.
Traduce localmente#
Configura tu API key y ejecuta el CLI:
export LINGO_API_KEY="your-api-key"
npx lingo.dev@latest runEl CLI lee cada archivo que coincide con los patrones de tus buckets, identifica las entradas sin traducir usando el lockfile, traduce el delta a través de tu motor de localización y escribe los resultados en el directorio de cada idioma de destino. Se conservan las claves de frontmatter, las etiquetas personalizadas de Markdoc y la estructura del JSON; solo cambia el texto traducible.
Para apuntar a un idioma específico durante el desarrollo:
npx lingo.dev@latest run --target-locale esAutomatiza con GitHub Actions#
Agrega un archivo de flujo de trabajo en .github/workflows/translate.yml para traducir en cada push:
Las traducciones se confirman directamente en main: sin fricción, ideal para equipos pequeños:
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 }}Guarda tu API key como LINGODOTDEV_API_KEY en Settings > Secrets and variables > Actions de tu repositorio de GitHub.
Verifica antes de desplegar#
Usa la bandera --frozen como compuerta de despliegue para asegurarte de que no llegue contenido sin traducir a producción. El CLI finaliza con un estado distinto de cero si alguna entrada necesita traducción:
npx lingo.dev@latest run --frozenAgrega esto como un paso de CI independiente antes del build de Next.js:
- name: Verify translations
run: npx lingo.dev@latest run --frozen
- name: Build
run: pnpm build