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 GitHub Actions.
Dépôt de démonstration
Clonez ou forkiez lingodotdev/markdoc-nextjs-localization-example pour suivre pas à pas. Le dépôt contient une application Next.js App Router opérationnelle avec du contenu Markdoc, une configuration du CLI de Lingo.dev et un workflow GitHub Actions.
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/[locale]/pages/home.md |
| Chaînes d’interface | Libellés de navigation, CTA, états de boutons | src/content/[locale]/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 bucket markdoc du CLI analyse les fichiers Markdoc en préservant le frontmatter et les balises personnalisées, tandis que le bucket json gère le catalogue de chaînes d’interface. Dans les deux cas, seul le delta est traduit via votre moteur de localisation, puis des fichiers par langue sont écrits à côté de la source.
Prérequis#
Créer un moteur de localisation
À chaque exécution, le CLI fait passer le contenu par un moteur de localisation — la configuration qui détermine le modèle LLM, le glossaire, la voix de marque et les instructions à appliquer. Créez-en un dans le tableau de bord Lingo.dev et générez une clé API.
Vérifier Node.js
Le CLI nécessite Node.js 18 ou une version supérieure :
node -vConfigurer votre projet Next.js
Votre projet doit utiliser l’App Router (src/app/) et disposer d’un répertoire de contenu par langue. Le dépôt de démonstration utilise src/content/[locale]/ avec deux sous-dossiers (pages/ et blog/) ainsi qu’un fichier ui.json. Consultez l’internationalisation dans 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#
Créez un fichier i18n.json à la racine de votre projet. Déclarez deux buckets : un pour le contenu Markdoc, l’autre pour le catalogue de chaînes d’interface :
{
"$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"]
}
}
}L’espace réservé [locale] se résout en chaque code de langue configuré. Avec source: "en", le CLI lit depuis src/content/en/ et écrit les fichiers traduits dans src/content/es/, src/content/fr/ et src/content/de/.
Catalogues monofichier
Si vos chaînes d’interface sont stockées dans un seul fichier JSON multilingue plutôt que dans un fichier par langue, utilisez le type de bucket json-per-locale. Consultez Static Content Localization pour la liste complète des types de bucket.
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#
Définissez votre clé API, puis lancez le CLI :
export LINGO_API_KEY="your-api-key"
npx lingo.dev@latest runLe CLI lit tous les fichiers correspondant aux motifs de vos buckets, identifie les entrées non traduites à l’aide du lockfile, traduit le delta via votre moteur de localisation, 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 est modifié.
Pour cibler une langue spécifique pendant le développement :
npx lingo.dev@latest run --target-locale esAutomatiser avec GitHub Actions#
Ajoutez un fichier de workflow à .github/workflows/translate.yml pour traduire à chaque push :
Les traductions sont commit directement sur main — zéro friction, idéal pour les petites équipes :
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 }}Stockez votre clé API sous LINGODOTDEV_API_KEY dans Settings > Secrets and variables > Actions de votre dépôt GitHub.
Vérifier avant le déploiement#
Utilisez l’option --frozen comme garde-fou de déploiement pour vous assurer qu’aucun contenu non traduit n’arrive en production. Le CLI se termine avec un code de sortie non nul si certaines entrées doivent encore être traduites :
npx lingo.dev@latest run --frozenAjoutez ceci comme étape de CI distincte avant votre build Next.js :
- name: Verify translations
run: npx lingo.dev@latest run --frozen
- name: Build
run: pnpm build