Le CLI de Lingo.dev traduit les fichiers Markdoc et les catalogues JSON de chaînes d’interface via un moteur de localisation configuré. Markdoc est un format de rédaction basé sur Markdown, avec des balises personnalisées typées propulsées par React — idéal pour les sites Next.js App Router qui combinent contenus longs et composants interactifs.
Ce guide vous accompagne de bout en bout dans la localisation d’un site Next.js App Router : configuration du CLI, organisation du contenu par langue, rendu de Markdoc dans des routes dynamiques et automatisation des traductions avec l’application GitHub de Lingo.dev.
Dépôt de démonstration
Clonez ou forkez lingodotdev/markdoc-nextjs-localization-example pour suivre ce guide pas à pas. Le dépôt contient une application Next.js App Router prête à l’emploi avec du contenu Markdoc, une configuration du CLI Lingo.dev et un workflow CI.
Comment fonctionne la localisation avec Next.js + Markdoc#
La plupart des sites Next.js App Router répartissent leur contenu localisé en deux couches :
| Couche | Ce qu’elle contient | Fichier d’exemple |
|---|---|---|
| Contenus longs | Pages marketing, documentation, articles de blog | src/content/en/pages/home.md |
| Chaînes d’interface | Libellés de navigation, CTA, états de boutons | src/content/en/ui.json |
Les routes se trouvent sous src/app/[lang]/ et chargent, au moment de la requête, les fichiers correspondant à la langue cible. Un middleware détermine une langue par défaut à partir de l’en-tête Accept-Language du navigateur et redirige les chemins nus comme / vers /en (ou vers la meilleure correspondance).
Le CLI analyse les fichiers Markdoc tout en préservant le frontmatter et les balises personnalisées, et gère le catalogue des chaînes d’interface en JSON. Il traduit le delta via votre moteur de localisation et génère des fichiers par langue à côté de la source.
Prérequis#
Créer un moteur de localisation
À chaque exécution du CLI, le contenu passe par un moteur de localisation — la configuration qui détermine quel modèle de LLM, glossaire, voix de marque et règles s’appliquent. Créez-en un dans le tableau de bord Lingo.dev et générez une clé API pour la CI.
Vérifier Node.js
Le CLI nécessite Node.js 22 ou version ultérieure :
node -vConfigurer votre projet Next.js
Votre projet doit utiliser l’App Router (src/app/) et un répertoire de contenu par langue. Le dépôt de démonstration utilise un répertoire par langue sous src/content/ (par exemple src/content/en/), avec deux sous-dossiers (pages/ et blog/) ainsi qu’un fichier ui.json. Consultez l’internationalisation de Next.js pour les bases du routage.
Organiser le contenu#
Organisez le contenu selon son rôle. Les pages et articles longs sont rédigés en Markdoc ; les chaînes d’interface plus courtes sont stockées en JSON afin que les composants puissent les charger directement.
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/Les fichiers Markdoc prennent en charge le frontmatter pour les métadonnées de page (titre, description, date, auteur), ainsi que des balises personnalisées rendues sous forme de composants React. Voici à quoi ressemble une page minimale :
---
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.Configurer le CLI#
Installez le CLI et connectez-vous :
npm install -g @lingo.dev/cli
lingo loginEnsuite, générez la configuration et reliez-la à votre moteur :
lingo init
lingo linklingo init crée .lingo/config.json avec vos langues source et cibles, ainsi que les motifs de fichiers à traduire ; lingo link ajoute votre orgId et votre engineId. Validez .lingo/config.json afin que toute l’équipe et chaque exécution CI partagent la même configuration.
Pour ce projet, la configuration définit deux motifs de fichiers : un pour le contenu Markdoc et un pour le catalogue de chaînes d’interface :
{
"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" }
]
}Le segment de langue dans chaque chemin est remplacé pour chaque langue cible : src/content/en/pages/home.md devient src/content/es/pages/home.md, et src/content/en/ui.json devient src/content/de/ui.json. Le chemin source doit contenir le code langue. Les formats sont détectés automatiquement à partir de l’extension du fichier ; les fichiers Markdoc (.md) et JSON (.json) n’ont donc pas besoin de type explicite. Consultez Configuration et Formats pour en savoir plus.
Catalogues monofichier
Le nouveau CLI attend un fichier par langue, avec le code langue dans le chemin (comme ci-dessus). Si vos chaînes d’interface se trouvent dans un seul fichier JSON multilingue, cette structure (l’ancien bucket json-per-locale) n’est pas encore prise en charge par le nouveau CLI – conservez-la avec le legacy CLI et suivez le changelog pour savoir quand ce sera pris en charge. La séparation en un fichier par langue est l’approche recommandée.
Rendre Markdoc dans l’App Router#
Une route dynamique typique charge un document et affiche l’arbre transformé. Le dépôt de démonstration expose un petit utilitaire :
// 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 page App Router est une fine couche d’adaptation qui associe le document à des chaînes d’interface propres à chaque langue :
// 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>
);
}Les balises Markdoc personnalisées (callout, bento, blog-hero, etc.) sont déclarées dans markdoc.schema.ts et reliées à des composants React sous src/components/markdoc/. Consultez la documentation du schéma Markdoc pour l’API complète.
Détecter la langue dans le middleware#
Le middleware Next.js inspecte la requête avant le rendu d’une route. Utilisez-le pour rediriger les chemins nus vers la langue la plus pertinente en fonction de l’en-tête 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|.*\\..*).*)", ],
};Les visiteurs arrivent sur /en, /es, /fr ou /de sans jamais avoir à saisir le préfixe.
Traduire en local#
Après lingo login, lancez un push. Lors de la première exécution – ou après l’ajout d’une nouvelle langue cible – remplissez d’abord l’historique complet :
lingo push --backfill-missingLors des exécutions suivantes, poussez simplement le delta :
lingo pushlingo push lit tous les fichiers correspondant à vos motifs, identifie les entrées non traduites à l’aide du fichier de verrouillage (.lingo/lock.json, versionné), traduit le delta via votre moteur de localisation, attend la fin du traitement, puis écrit les résultats dans le répertoire de chaque langue cible. Les clés du frontmatter, les balises personnalisées Markdoc et la structure du JSON sont préservées – seul le texte traduisible change. Pour récupérer des traductions produites ailleurs (par exemple par la CI), exécutez lingo pull.
Pour limiter une exécution à certains fichiers, passez un glob :
lingo push "src/content/en/blog/*.md"Automatiser en CI#
Installez l’application GitHub Lingo.dev et connectez-la à votre dépôt. Elle lit .lingo/config.json et le engineId associé côté serveur, puis ouvre une pull request de traduction à chaque modification du contenu source – sans fichier de workflow, sans runner, sans secret de clé API et sans avoir à jongler avec le fichier de verrouillage.
Vérifier avant le déploiement#
Utilisez lingo check comme garde-fou au déploiement pour vous assurer qu’aucun contenu non traduit ne parte en production. La commande renvoie un statut non nul si certaines entrées nécessitent encore une traduction :
lingo checkAjoutez ceci comme étape de CI distincte avant votre build Next.js :
- name: Verify translations
run: lingo check
env:
LINGO_API_KEY: ${{ secrets.LINGO_API_KEY }}
- name: Build
run: pnpm build