Internationaliser un site Astro avec Locize
Astro est le framework web static-first pour les sites orientés contenu : routage basé sur les fichiers, zéro JavaScript par défaut, des « islands » pour l'interactivité opt-in, un mode SSR à la demande pour les parties qui en ont besoin. Depuis la v4.0 (fin 2023, stable en v5+, actuelle en v6.x), Astro embarque le routage i18n intégré : préfixage d'URL (/en/, /de/), le helper Astro.currentLocale, la détection de la langue du navigateur et les tags SEO <link rel="alternate" hreflang> automatiques.
Ce que l'i18n intégré d'Astro ne fournit pas, c'est une fonction de traduction (t()), la pluralisation ou l'interpolation de messages. La recette i18n officielle d'Astro montre le pattern canonique « construisez-le vous-même » : importez un fichier JSON par locale, écrivez cinq lignes de helper, terminé. C'est exactement là que Locize se branche : locize-cli télécharge le dernier JSON publié depuis Locize dans src/i18n/locales/{lng}/{ns}.json au moment du build, un petit module ui.ts les assemble en un arbre de lookup à plat, et le build statique d'Astro les récupère au compile time.
Cet article décrit l'intégration de bout en bout. Le code complet vit sur github.com/locize/locize-astro-example.
Si vous êtes sur Nuxt à la place, suivez le walkthrough Nuxt 4. Si vous êtes sur React Router v7 (mode Framework), voir le walkthrough React Router v7 : tous deux couvrent des intégrations plus riches (éditeur in-context, saveMissing à l'exécution) qui ne s'insèrent pas directement dans le modèle static-by-default d'Astro.
TL;DR
- Configurez le routage i18n intégré d'Astro dans
astro.config.mjs: liste des locales +prefixDefaultLocale: truepour des URLs uniformes. - Utilisez des routes de pages dynamiques
[lang]/avecgetStaticPaths, pour que chaque locale soit pré-rendue statiquement. - Synchronisez les traductions dans l'app au moment du build via la commande
downloaddelocize-cli. Pas de saut CDN à l'exécution, pas de considération SSR. - Écrivez un helper
useTranslations(lang)de cinq lignes danssrc/i18n/utils.ts. Astro ne livre délibérément pas det(); c'est le pattern canonique de leur propre documentation. - Posez de nouvelles clés via
npm run syncLocales, l'extraction statique de i18next-cli ou l'app web Locize, selon ce qui correspond à votre workflow. AucunsaveMissingà l'exécution n'est possible depuis la couche statique. - Besoin de saveMissing à l'exécution, de fetch CDN live ou de l'éditeur in-context ? Montez n'importe quelle island React/Vue/Svelte via
@astrojs/<framework>et utilisezi18next-locize-backend+ le packagelocizeà l'intérieur de l'island. - Exemple complet fonctionnel : github.com/locize/locize-astro-example
Comment les pièces s'emboîtent
Astro est static-by-default. Les pages sont pré-rendues en HTML au moment du build et servies comme fichiers statiques (ou, en mode SSR, via un adaptateur à la demande). L'i18n intégré d'Astro est propriétaire de la couche routage : préfixage d'URL, redirections sensibles à la locale, tags hreflang, helpers de détection navigateur, mais reste délibérément en dehors de la couche traduction des messages. C'est pourquoi chaque tuto i18n Astro se termine par « maintenant écrivez votre propre helper t() » : la philosophie d'Astro est de vous donner des primitives, pas des opinions sur la traduction.
Locize est la couche de gestion des traductions. L'intégration est bundle-only à l'exécution :
- Au build :
locize-clitélécharge le dernier JSON publié depuis le CDN Locize danssrc/i18n/locales/{lng}/{ns}.json. Les fichiers JSON committés sont importés directement parsrc/i18n/ui.tsau chargement du module et assemblés en un arbre de lookup à plat. t()est du TypeScript local : cinq lignes qui gèrent le fallback de locale + l'interpolation au format{name}. Pas de bibliothèque runtime, pas de peer dep.- Les mises à jour nécessitent un rebuild : comme tout autre changement de contenu Astro. Lancez
npm run downloadLocaleset redéployez.
Ce dernier point est le compromis. Un aller-retour « traduction publiée dans Locize → utilisateurs finaux la voient » signifie relancer le build. Si vous préférez un fetch CDN live (publication → la vue de page suivante le récupère, sans redéploiement), vous montez une framework island et utilisez i18next-locize-backend à l'intérieur. Le walkthrough React Router v7 couvre cette forme, et vous pouvez adopter le même pattern par-island dans Astro. Nous y reviendrons à la fin.
C'est parti
Prérequis
Node.js 22+ (Astro 6 l'exige), npm/pnpm/yarn, et une familiarité de base avec Astro. L'exemple ci-dessous cible Astro 6.3.
Layout du projet
Générez une nouvelle app Astro, ou faites git clone de l'exemple :
npm create astro@latest my-app
cd my-appLa forme de l'arborescence pertinente après câblage de i18n + Locize :
my-app/
├── astro.config.mjs — Astro's built-in i18n config
├── package.json — locize-cli download/sync scripts
└── src/
├── pages/
│ ├── index.astro — root /, redirects to /en/
│ └── [lang]/
│ ├── index.astro — home, renders for each locale via getStaticPaths
│ └── second.astro — secondary page, same pattern
├── layouts/
│ └── Layout.astro — shared <html>/<head>/<body>
├── components/
│ └── LanguagePicker.astro — swaps the /{lang}/ prefix
└── i18n/
├── ui.ts — assembles namespaced JSON into a flat tree
├── utils.ts — getLangFromUrl + useTranslations helpers
└── locales/ — locize-cli download target (one JSON per ns)
├── en/
│ ├── common.json
│ ├── index.json
│ └── second.json
└── de/
├── common.json
├── index.json
└── second.jsonInstaller les dépendances
npm install astro
npm install --save-dev locize-cliC'est tout. Pas de bibliothèque runtime de traduction, pas de package d'intégration framework. L'i18n intégré d'Astro + locize-cli + ~50 lignes de helpers couvrent toute l'histoire.
Configurer l'i18n intégré d'Astro dans astro.config.mjs
import { defineConfig } from 'astro/config'
export default defineConfig({
i18n: {
defaultLocale: 'en',
locales: ['en', 'de'],
routing: {
prefixDefaultLocale: true // /en/, /de/ — uniform URLs
}
}
})Astro prend désormais en charge :
- Le routage des URLs pour chaque page sous
src/pages/[lang]/:/en/,/de/,/en/second,/de/second, etc. Astro.currentLocaledisponible dans chaque page/composant gratuitement.- La détection de la langue du navigateur via
Astro.preferredLocaleetAstro.preferredLocaleList(routes SSR/à la demande uniquement). - Les tags
<link rel="alternate" hreflang>automatiques quand vous l'activez.
Avec prefixDefaultLocale: true, chaque locale reçoit un préfixe d'URL : uniforme, SEO-clean et facile à gérer avec des routes dynamiques [lang]/ + getStaticPaths. (Si vous préférez voir la locale par défaut servie à / sans préfixe, mettez-le à false et utilisez des pages basées sur les fichiers au lieu de [lang]/. L'exemple utilise la forme préfixée parce qu'elle passe mieux à l'échelle.)
Télécharger les traductions depuis Locize au build
Ajoutez à package.json :
{
"scripts": {
"downloadLocales": "locize download --project-id=<your-id> --ver=latest --cdn-type=standard --clean=true --path=./src/i18n/locales",
"syncLocales": "locize sync --project-id=<your-id> --ver=latest --cdn-type=standard --api-key=<your-write-key> --path=./src/i18n/locales --dry=true"
}
}Exécutez npm run downloadLocales avant npm run build (manuellement, en CI ou comme hook prebuild), pour que le JSON empaqueté soit toujours frais.
Standard vs Pro CDN. Locize propose deux infrastructures CDN (comparaison complète dans Types de CDN : Standard vs Pro) : Standard sur
api.lite.locize.app, basé sur BunnyCDN, gratuit pour des volumes mensuels de téléchargement généreux, public uniquement (par défaut pour les nouveaux projets) ; Pro surapi.locize.app, basé sur CloudFront, payant, prend en charge les téléchargements privés + un contrôle de cache personnalisé. Faites correspondre--cdn-typesur le flag de la cli avec ce sur quoi est votre projet.
L'arbre des messages — src/i18n/ui.ts
Locize émet un JSON par namespace par langue. Astro n'a pas de notion de namespaces i18n, donc nous assemblons la disposition sur disque en un arbre de lookup à plat unique au chargement du module, avec le namespace inscrit en préfixe dans chaque clé :
import enCommon from './locales/en/common.json' with { type: 'json' }
import enIndex from './locales/en/index.json' with { type: 'json' }
import enSecond from './locales/en/second.json' with { type: 'json' }
import deCommon from './locales/de/common.json' with { type: 'json' }
import deIndex from './locales/de/index.json' with { type: 'json' }
import deSecond from './locales/de/second.json' with { type: 'json' }
type Messages = Record<string, string>
function prefix (ns: string, obj: Messages): Messages {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [`${ns}.${k}`, v])
)
}
export const ui = {
en: { ...prefix('common', enCommon), ...prefix('index', enIndex), ...prefix('second', enSecond) },
de: { ...prefix('common', deCommon), ...prefix('index', deIndex), ...prefix('second', deSecond) }
} as const
export const defaultLang = 'en'
export const languages = {
en: 'English',
de: 'Deutsch'
} as const
export type Lang = keyof typeof ui
export type TranslationKey = keyof typeof ui[typeof defaultLang]La sortie ressemble à :
ui.en = {
'common.headTitle': 'Locize + Astro',
'common.skipToContent': 'Skip to content',
'index.title': 'Hello, {name}!',
'second.title': 'The second page',
// ...
}TranslationKey est dérivé de l'arbre de la locale par défaut, donc TypeScript autocomplète la clé dans les fichiers .astro et rejette les fautes de frappe au compile time.
Le helper t() — src/i18n/utils.ts
import { ui, defaultLang, type Lang, type TranslationKey } from './ui'
export function getLangFromUrl (url: URL): Lang {
const [, lang] = url.pathname.split('/')
if (lang && lang in ui) return lang as Lang
return defaultLang
}
export function useTranslations (lang: Lang) {
return function t (
key: TranslationKey,
values?: Record<string, string | number>
): string {
const raw: string = ui[lang][key] ?? ui[defaultLang][key] ?? key
if (!values) return raw
return raw.replace(/\{(\w+)\}/g, (_, k: string) =>
values[k] !== undefined ? String(values[k]) : `{${k}}`
)
}
}Cinq lignes de logique. Chaîne de fallback : la clé demandée dans la locale active, puis la locale par défaut, puis la chaîne brute de la clé. Interpolation optionnelle au format {name} qui passe les valeurs via le second argument. C'est toute l'API d'exécution.
Ce pattern est le même que celui que montre la recette i18n officielle d'Astro ; nous avons juste superposé la disposition sur disque par namespace de Locize par-dessus.
Utiliser t() dans les pages
Les routes dynamiques [lang]/ + getStaticPaths signifient qu'Astro pré-rend un fichier HTML par locale au moment du build :
---
// src/pages/[lang]/index.astro
import Layout from '../../layouts/Layout.astro'
import { ui, type Lang } from '../../i18n/ui'
import { useTranslations } from '../../i18n/utils'
import { getRelativeLocaleUrl } from 'astro:i18n'
export function getStaticPaths () {
return Object.keys(ui).map(lang => ({ params: { lang } }))
}
const { lang } = Astro.params as { lang: Lang }
const t = useTranslations(lang)
---
<Layout lang={lang} title={t('common.headTitle')}>
<h1>{t('index.title', { name: 'Astro' })}</h1>
<p>{t('index.subtitle')}</p>
<p>
<a href={getRelativeLocaleUrl(lang, 'second/')}>
{t('index.goto.second')}
</a>
</p>
</Layout>La fonction getStaticPaths mappe chaque locale à un chemin statique ; lang arrive comme paramètre de route. Du pur travail au compile time : pas de runtime côté serveur, pas de considération d'hydratation.
Le sélecteur de langue
La recette i18n des docs d'Astro montre un sélecteur simple qui échange le préfixe de locale sur l'URL courante :
---
// src/components/LanguagePicker.astro
import { languages, type Lang } from '../i18n/ui'
interface Props {
currentLang: Lang
}
const { currentLang } = Astro.props
function pathForLang (target: Lang): string {
const { pathname } = Astro.url
const parts = pathname.split('/')
// "/en/foo/bar" → ["", "en", "foo", "bar"]
parts[1] = target
return parts.join('/')
}
---
<nav>
<ul>
{Object.entries(languages).map(([lang, label]) => (
<li>
{lang === currentLang ? (
<strong>{label}</strong>
) : (
<a href={pathForLang(lang as Lang)}>{label}</a>
)}
</li>
))}
</ul>
</nav>Déposez-le dans src/layouts/Layout.astro et il est disponible sur chaque page.
Trois façons d'amener de nouvelles clés dans Locize
Astro est static-by-default : les pages sont pré-rendues, donc un push saveMissing à l'exécution depuis des utilisateurs en production n'est pas possible sans un adaptateur SSR, et Astro décourage de toute façon les écritures depuis la couche statique. Il existe trois chemins viables à la place :
npm run syncLocales(câblé danspackage.json, retirez le flag--dry=truepour réellement pousser).locize-clilit vossrc/i18n/locales/{lng}/{ns}.jsonlocaux et envoie toutes les clés pas encore présentes dans Locize. Manuel ou déclenché en CI, sans write-key dans le navigateur.- Extraction statique via
i18next-cliou similaire : scanne vos sources.astroà la recherche d'appelst('…'), écrit les nouvelles clés dans le JSON local, puis synchronise comme ci-dessus. - L'app web Locize : ajoutez des clés directement dans l'éditeur et récupérez-les avec
downloadLocalesavant le prochain build.
Choisissez ce qui correspond à votre workflow. Aucun n'exige de livrer une write API key dans votre artefact de build.
Ce que cette forme n'inclut intentionnellement pas
Trois choses que vous pourriez attendre des walkthroughs Nuxt ou React Router v7 sont délibérément absentes ici :
- Pas de
saveMissingà l'exécution. Voir ci-dessus : les builds statiques Astro ne peuvent pas pousser, et une app Astro SSR ferait cela depuis l'intérieur d'une island, pas depuis la couche statique. - Pas d'éditeur in-context sur les pages statiques. L'éditeur
locizea besoin d'un DOM qui se re-rend quand l'éditeur met à jour une chaîne. La sortie statique d'Astro n'a pas ce hook. - Pas de fetch CDN live. Les traductions sont empaquetées au moment du build. Pour servir une traduction fraîche, lancez
npm run downloadLocaleset redéployez. Cela correspond au modèle static-first d'Astro et garde l'artefact de build autonome.
Tous ces trois points fonctionnent à l'intérieur de framework islands. Montez une island React, Vue, Svelte, Solid ou Preact via l'intégration @astrojs/<framework> correspondante, et à l'intérieur de cette island utilisez directement i18next-locize-backend + le package locize. La couche statique d'Astro gère le routage + l'habillage de la page ; les islands gèrent les parties qui ont besoin d'un runtime.
Quand ajouter une framework island
L'intégration static-only couvre la plupart des sites de contenu : blogs, pages marketing, docs, landing pages. Ajoutez une framework island quand vous avez besoin de :
- Mises à jour live des traductions sans redéploiement. Utilisez
i18next-locize-backendà l'intérieur de l'island pour fetcher les traductions depuis le CDN Locize à chaque visite (ou par route, si vous cachez). - Édition in-context pour les traducteurs. L'overlay
?incontext=truedu packagelocizea besoin d'un runtime qui se re-rend sur changement de chaîne : disponible uniquement à l'intérieur d'une island hydratée. saveMissingà l'exécution. Même contrainte : une island hydratée peut pousser de nouvelles clés ; le HTML statique ne le peut pas.
Le pattern ressemble à ceci (pour une island React) :
---
// src/pages/[lang]/interactive.astro
import Layout from '../../layouts/Layout.astro'
import I18nIsland from '../../components/I18nIsland.tsx'
---
<Layout lang={Astro.currentLocale}>
<I18nIsland client:load lang={Astro.currentLocale} />
</Layout>// src/components/I18nIsland.tsx
import i18next from 'i18next'
import Backend from 'i18next-locize-backend'
import { initReactI18next, useTranslation } from 'react-i18next'
i18next
.use(Backend)
.use(initReactI18next)
.init({
fallbackLng: 'en',
saveMissing: import.meta.env.DEV,
backend: {
projectId: import.meta.env.PUBLIC_LOCIZE_PROJECT_ID,
apiKey: import.meta.env.DEV ? import.meta.env.LOCIZE_API_KEY : undefined,
version: 'latest'
}
})
export default function I18nIsland ({ lang }: { lang: string }) {
const { t, i18n } = useTranslation()
if (i18n.language !== lang) i18n.changeLanguage(lang)
return <h2>{t('island.heading')}</h2>
}client:load indique à Astro d'hydrater ce composant immédiatement au chargement de la page ; vous pouvez utiliser client:visible pour différer l'hydratation jusqu'à ce que l'island entre dans le viewport. La configuration exacte de i18next-locize-backend est documentée dans le walkthrough React Router v7 : le même câblage de backend fonctionne dans n'importe quel contexte React/Vue/Svelte/Solid, y compris à l'intérieur d'une island Astro.
🎉 Félicitations
Une configuration Astro 6 + Locize fonctionnelle vous offre :
- i18n static-first sans flash de contenu non traduit (tout est pré-rendu par locale)
- Préfixage d'URL SEO-clean via le routage i18n intégré d'Astro
- Synchronisation des traductions au build via
locize-cli, compatible serverless, sans saut CDN par requête t()type-safe avec autocomplétion sur les clés de traduction- Une porte de sortie vers des islands dynamiques quand vous avez besoin de mises à jour à l'exécution ou d'édition in-context
🧑💻 Le code complet : github.com/locize/locize-astro-example.
Les fondateurs de Locize sont aussi les créateurs d'i18next. En utilisant Locize, vous soutenez directement l'avenir d'i18next.
Voir aussi
- Comment internationaliser une app Nuxt 4 avec @nuxtjs/i18n et Locize — pendant côté Vue avec une intégration plus riche (vue-i18n, éditeur in-context, saveMissing à l'exécution)
- Comment internationaliser une app React Router v7 avec remix-i18next — pendant côté React avec fetch CDN live via
i18next-locize-backend - Le routage i18n intégré d'Astro — la primitive de routage sur laquelle ce walkthrough s'appuie
- Recette i18n Astro — le pattern construisez-le-vous-même dans lequel Locize se branche
locize-cli— l'outil de synchronisation au build