Bonnes pratiques Paraglide JS + Svelte 5
Traitez la locale comme un état de requête/construction résolu par un compilateur, et non comme un état réactif de l'application — laissez la navigation complète du document de Paraglide gérer les changements de locale, et laissez les runes Svelte 5 gérer tout le reste. Paraglide JS est l'intégration i18n officielle de SvelteKit : il compile les fichiers messages/{locale}.json en fonctions ESM typées et tree-shakables, offrant des bundles i18n jusqu'à ~70 % plus petits que les bibliothèques d'exécution (≈47 Ko contre ≈205 Ko dans les benchmarks publiés avec 5 locales / 200 messages), sans cascades asynchrones ni fournisseur i18n à câbler dans les composants Svelte 5.
1. Tableau de décision de référence rapide
| Décision | Valeur par défaut recommandée | Pourquoi |
|---|---|---|
| Chemin d'installation | npx sv add paraglide="languageTags:en,de" sur une nouvelle application SvelteKit | Crée le plugin Vite, les hooks, les attributs app.html, .gitignore en une seule étape |
| Package | @inlang/paraglide-js v2.x | L'adaptateur v1 @inlang/paraglide-js-adapter-sveltekit est obsolète — utilisez le package core directement |
| Stratégie (SSR + URLs localisées) | ["url", "cookie", "baseLocale"] | L'URL est la source de vérité SEO ; le cookie persiste le choix de l'utilisateur de manière visible côté serveur ; baseLocale se résout toujours en dernier |
| Stratégie (ajout d'une substitution stockée) | ["localStorage", "cookie", "preferredLanguage", "url", "baseLocale"] | localStorage seul est invisible à la première requête SSR ; le cookie fait le pont entre le serveur et le client |
| Comportement du changement de locale | Par défaut setLocale(locale) (navigation complète du document) | La réactivité du framework ne re-rend pas <html lang>, dir, <title>, ni les URLs localisées ; la navigation le fait |
| AsyncLocalStorage | Gardez activé (par défaut) | Vercel Edge et Cloudflare Workers (compatibilité Node) le supportent ; le désactiver risque de faire fuiter la locale entre des requêtes concurrentes |
| Déclarations TS | emitTsDeclarations: true (TypeScript ≥ 5.6) | Maintient la synchronisation des serveurs de langage Svelte + TS lorsque les clés changent pendant le développement |
| Clés de messages | Clés aléatoires plates, stables et lisibles (calm_green_otter) | Recommandation officielle ; les clés imbriquées fonctionnent mais les clés plates sont préférées |
| Prérendu | export const prerender = true + entries() par locale | SSG a besoin de chaque locale pour résoudre en un fichier URL distinct |
| Réactivité pour la locale | Aucune dans les composants par défaut | getLocale() est limité à la requête ; un nouveau document est rendu par locale, donc les runes n'ont pas besoin de le suivre |
2. Configuration et mise en place (Paraglide 2.x + SvelteKit + Svelte 5)
2.1 Chemin le plus rapide
npx sv create my-app
npx sv add paraglide="languageTags:en,de,demo:yes"
L'add-on CLI Svelte configure le projet inlang, le plugin Vite, les hooks handle/reroute de SvelteKit, %lang%/%dir% dans app.html, et une page de démonstration optionnelle.
2.2 Configuration manuelle (boilerplate complet)
Installez et créez la structure du projet, puis connectez quatre fichiers :
vite.config.ts — plugin avec stratégie explicite :
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { paraglideVitePlugin } from '@inlang/paraglide-js';
export default defineConfig({
plugins: [
sveltekit(),
paraglideVitePlugin({
project: './project.inlang',
outdir: './src/lib/paraglide',
emitTsDeclarations: true,
strategy: ['url', 'cookie', 'baseLocale'],
}),
],
});
emitTsDeclarations: true regénère les .d.ts au fur et à mesure que vous éditez les messages afin que les serveurs Svelte et TypeScript restent synchronisés ; cela nécessite TypeScript 5.6+ (mettez false pour le chemin d'inférence JSDoc plus rapide).
src/app.html — remplacez la langue codée en dur :
<!doctype html>
<html lang="%lang%" dir="%dir%">
src/hooks.server.ts — middleware serveur avec locale limitée à la requête :
import type { Handle } from '@sveltejs/kit';
import { paraglideMiddleware } from '$lib/paraglide/server';
import { getTextDirection } from '$lib/paraglide/runtime';
const paraglideHandle: Handle = ({ event, resolve }) =>
paraglideMiddleware(event.request, ({ request: localizedRequest, locale }) => {
event.request = localizedRequest;
return resolve(event, {
transformPageChunk: ({ html }) =>
html.replace('%lang%', locale).replace('%dir%', getTextDirection(locale)),
});
});
export const handle: Handle = paraglideHandle;
Le middleware détecte la locale (stratégies dans l'ordre), redirige les requêtes de document lorsque l'URL est en désaccord avec la locale détectée, délocalise /de/about → /about avant que SvelteKit ne le voie, et enveloppe la requête dans AsyncLocalStorage afin que getLocale() retourne la bonne locale par requête concurrente.
src/hooks.ts — limite de routage (doit être hooks.ts, pas hooks.server.ts) :
import type { Reroute } from '@sveltejs/kit';
import { deLocalizeUrl } from '$lib/paraglide/runtime';
export const reroute: Reroute = (request) => deLocalizeUrl(request.url).pathname;
// project.inlang/paraglide.config.ts
import { defineConfig } from '@inlang/paraglide-js';
export default defineConfig({
outdir: './src/lib/paraglide',
strategy: ['url', 'cookie', 'baseLocale'],
urlPatterns: [
{ pattern: '/', localized: [['en', '/']] },
{
pattern: '/:path(.*)?',
localized: [
['en', '/en/:path(.*)?'],
['de', '/de/:path(.*)?'],
],
},
],
});
Précédence : drapeaux CLI / options du plugin > fichier de configuration > valeurs par défaut intégrées. Le mode watch recharge à chaud la configuration.
3. Patterns d'utilisation principaux (Messages, Locale, Routage)
3.1 Messages
import { m } from '$lib/paraglide/messages';
import type { LocalizedString } from '$lib/paraglide/runtime';
m.hello_world(); // "Bonjour le monde !"
m.greeting({ name: 'Ada' }); // "Bonjour Ada !" — les paramètres sont typés
m.greeting({ name: 'Ada' }, { locale: 'de' }); // force la locale pour un appel
const title: LocalizedString = m.welcome_title(); // type de chaîne sécurisé
Les messages vivent dans messages/{locale}.json ; les locales sont déclarées dans project.inlang/settings.json (baseLocale + locales). Préférez les clés plates et stables ; les clés imbriquées sont supportées via m['user.profile.title'](). L'accès dynamique aux clés nuit au tree-shaking — mappez les identifiants dynamiques à des références de messages explicites à la place.
3.2 Obtenir et définir la locale
import { getLocale, locales, setLocale, getTextDirection } from '$lib/paraglide/runtime';
getLocale(); // p. ex. "de" — limité à la requête côté serveur
getTextDirection(); // "ltr" | "rtl" pour la locale actuelle
setLocale('de'); // navigation complète du document vers l'URL localisée
setLocale() met à jour vos stratégies configurées puis effectue une navigation complète du document — vers l'URL localisée lorsque la stratégie url est active, sinon un rechargement. Ceci est intentionnel : <html lang>, dir, <title>, les métadonnées et l'URL localisée sont des états au niveau du document que la réactivité du framework ne peut pas mettre à jour.
L'échappatoire setLocale(locale, { reload: false }) existe uniquement pour les surfaces entièrement rendues côté client (widgets intégrés, pages d'extension) qui doivent préserver un état mémoire non restaurable. Ne l'utilisez jamais pour les sélecteurs de langue ordinaires, sur les documents SSR/SSG, ou sur les routes dont la stratégie inclut url — cela laisse l'ancienne URL et le shell en place, donc getLocale() continue de résoudre l'ancienne locale.
3.3 URLs localisées et liens
import { localizeHref, deLocalizeUrl, localizeUrl } from '$lib/paraglide/runtime';
localizeHref('/blog'); // "/de/blog" pour la locale courante
localizeHref('/blog', { locale: 'de' }); // explicite
localizeUrl('https://example.com/about', { locale: 'de' }).href; // forme d'URL complète
Les patterns d'URL utilisent la syntaxe standard URLPattern ; l'ordre compte — placez les patterns spécifiques avant les jokers, et testez-les sur urlpattern.com.
3.4 Sélecteur de locale Svelte 5 (idiomatique)
<script lang="ts">
import { page } from '$app/state'; // Svelte 5 : objet, pas un store
import { resolve } from '$app/paths';
import { locales, localizeHref } from '$lib/paraglide/runtime';
</script>
<nav aria-label="Langues">
{#each locales as locale (locale)}
<a
href={resolve(localizeHref(page.url.pathname, { locale }))}
data-sveltekit-reload
aria-current={page.params.locale === locale ? 'page' : undefined}
>
{locale}
</a>
{/each}
</nav>
Deux détails Svelte 5 importent ici : $app/state expose page comme un objet réactif (le store $page et $app/stores sont le pattern hérité), et data-sveltekit-reload sur les liens inter-locales maintient la synchronisation entre l'URL localisée, le document rendu côté serveur et la locale d'exécution — également requis pour le crawl prérendu.
4. Texte enrichi avec les snippets Svelte 5 (ParaglideMessage)
Le balisage permet aux traducteurs de contrôler la structure pendant que votre application contrôle le rendu. Les noms de balises sont définis par l'application — rien n'est mappé automatiquement au HTML.
// messages/en.json
{
"cta": "{#link to=|/docs|}Lire la documentation{/link}",
"welcome": "{#b}Salut {name}{/b}{#icon/}"
}
<script lang="ts">
import { ParaglideMessage } from '@inlang/paraglide-js-svelte';
import { m } from '$lib/paraglide/messages';
const name = $props().name; // ou tout $state
</script>
<ParaglideMessage message={m.welcome} inputs={{ name }}>
{#snippet b({ children })}
<strong>{@render children?.()}</strong>
{/snippet}
{#snippet icon({ attributes })}
<Icon variant={attributes?.variant} />
{/snippet}
</ParaglideMessage>
<ParaglideMessage> utilise message.parts() lorsqu'il y a du balisage et revient à message() pour le texte brut ; la syntaxe {#snippet tag({ children, options, attributes })} est exactement le pattern de snippet Svelte 5 — pas de slots, pas de <slot> hérité. Notez la syntaxe littérale to=|/docs| (délimitée par des pipes) et que m.welcome() seul retourne du texte brut sans balisage.
5. Intégration des runes Svelte 5 : Ce qui est réactif (et ce qui ne l'est pas)
Le modèle mental clé : Paraglide possède la locale en tant qu'état de document/requête ; les runes Svelte 5 possèdent l'état de l'interface utilisateur. Comme les changements de locale entraînent un nouveau document, vous avez rarement besoin de runes pour suivre getLocale(). Patterns qui apparaissent dans la pratique :
Valeurs dérivées dépendant de la locale à l'intérieur d'un seul document (par exemple, une surface côté client utilisant reload: false, ou un volet d'aperçu rendant plusieurs locales explicitement) :
<script lang="ts">
import { getLocale, locales } from '$lib/paraglide/runtime';
import { m } from '$lib/paraglide/messages';
let { locale = getLocale() } = $props(); // props typées
// $derived se réévalue lorsque la prop `locale` change — pas lorsque
// la locale interne de Paraglide change (elle ne change pas, sans navigation) :
const greeting = $derived(m.greeting({ name: 'Ada' }, { locale }));
const isRtl = $derived(getTextDirection(locale) === 'rtl');
</script>
Passer la locale forcée vers le bas plutôt que de lire l'état global est le pattern compatible avec les runes : les options { locale } sur chaque appel de message rendent les composants des fonctions pures de leurs props, ce qui est exactement comme Svelte 5 veut que l'état circule. Réservez $effect pour les véritables effets de bord (par exemple, persister une locale d'aperçu) — n'affectez jamais à un $derived, et n'écrivez pas $state depuis l'intérieur de $effect pour "pousser" la locale ; passez plutôt des props.
État client global (par exemple, "dernière locale prévisualisée" pour un outil de traduction intégré à l'application) :
// src/lib/locale-preview.svelte.ts — les fichiers .svelte.ts supportent les runes au niveau du module
export const preview = $state({ locale: 'en' as string | undefined });
$state au niveau du module dans les fichiers .svelte.ts est partagé entre tous les importateurs — acceptable pour les singletons à l'échelle de l'application, mais rappelez-vous que ce n'est pas ainsi que fonctionne la locale de production (c'est la chaîne de stratégie + la navigation du document).
6. SSR, Hydratation et SSG
6.1 Règles SSR
- Ordre d'une chaîne
handle: mettezparaglideHandleen premier (ou avant les gestionnaires d'authentification qui ont besoin deevent.localsindexé par locale) afin que les hooks en aval voient la requête délocalisée. - Derrière un proxy de terminaison TLS, passez
effectiveRequestUrlpour que la correspondance d'URL et les redirections utilisent l'origine côté navigateur. - Gardez
AsyncLocalStorageactivé ; c'est ce qui rendgetLocale()sûr sous des requêtes concurrentes et SSR en streaming. Le replidisableAsyncLocalStorageest uniquement pour les runtimes sansnode:async_hookset une isolation par requête garantie — l'utiliser ailleurs peut faire fuiter la locale entre les utilisateurs.
6.2 Exactitude de l'hydratation
Le serveur et le client doivent résoudre la locale à partir du même ordre de stratégie. L'incompatibilité classique : le serveur résout depuis l'URL mais la stratégie client lit localStorage en premier, qui n'existe pas au moment du SSR. Corrigez en incluant une stratégie cookie visible par le serveur lorsqu'un choix persistant doit façonner la première réponse.
6.3 SSG (adapter-static / prerender)
Le prérendu n'exécute jamais le middleware — définissez la locale par page pendant la construction :
// src/routes/[locale]/+layout.ts
import { setLocale, assertIsLocale } from '$lib/paraglide/runtime';
import type { LayoutLoad } from './$types';
export const prerender = true;
export function load({ params }: { params: { locale: string } }) {
setLocale(assertIsLocale(params.locale));
}
// src/routes/[locale]/+page.ts
import { locales } from '$lib/paraglide/runtime';
export const prerender = true;
export function entries() {
return locales.map((locale) => ({ locale }));
}
SSG nécessite que chaque locale porte un préfixe d'URL (configurez urlPatterns pour que /en/… et /de/… existent tous les deux), sinon SvelteKit ne peut pas émettre de fichiers distincts par locale. Pour le fallback SPA de l'adaptateur statique, définissez kit.paths.relative = false pour éviter les 404 d'actifs préfixés par locale, et utilisez generateStaticLocalizedUrls() pour les sitemaps.
7. Performance et Tree-Shaking
- Appelez les fonctions de message directement.
m.hello()se compile en une petite fonction ESM ; votre bundler supprime automatiquement les messages inutilisés et leurs traductions. La taille du bundle reste stable à mesure que le catalogue grandit (benchmark : 47 Ko, que le projet contienne 200 ou 1 000 messages). - Pas de namespace d'objet dynamique. Évitez
m[someKey]()— cela brise le tree-shaking. Mappez plutôt les clés dynamiques à des références de messages explicites. - Le compilateur tree-shake aussi les stratégies et les patterns d'URL. Les stratégies inutilisées (par exemple,
localStoragesi vous ne l'avez pas configuré) sont supprimées du bundle. - Les messages sont chargés de manière paresseuse par locale. Seule la locale active est chargée ; le bundle du client ne contient que les messages de la locale active.
8. Tests
8.1 Tests unitaires pour les messages
import { describe, it, expect } from 'vitest';
import { m } from '$lib/paraglide/messages';
describe('messages', () => {
it('affiche le message d'accueil avec les paramètres', () => {
expect(m.greeting({ name: 'Monde' })).toBe('Bonjour Monde !');
});
it('force la locale pour un appel', () => {
const result = m.greeting({ name: 'Mundo' }, { locale: 'de' });
expect(result).toBe('Hallo Mundo!');
});
});
8.2 Tests d'intégration pour le routage localisé
import { describe, it, expect } from 'vitest';
import { localizeHref, deLocalizeUrl } from '$lib/paraglide/runtime';
describe('localisation d'URL', () => {
it('localise un chemin pour la locale de', () => {
expect(localizeHref('/blog', { locale: 'de' })).toBe('/de/blog');
});
it('délocalise une URL', () => {
expect(deLocalizeUrl('/de/blog').pathname).toBe('/blog');
});
});
8.3 Tests bout en bout (Playwright / Vitest avec SvelteKit)
import { expect, test } from '@playwright/test';
test('passe de l'anglais à l'allemand via le sélecteur', async ({ page }) => {
await page.goto('/');
await page.click('a[href="/de"]');
await expect(page.locator('html')).toHaveAttribute('lang', 'de');
await expect(page).toHaveURL(/\/de/);
});
9. Dépannage et pièges courants
| Symptôme | Cause probable | Correctif |
|---|---|---|
getLocale() retourne la mauvaise locale côté serveur | Ordre de stratégie incorrect ou disableAsyncLocalStorage activé | Passez ["url", "cookie", "baseLocale"] et gardez AsyncLocalStorage activé |
setLocale() ne met pas à jour l'URL | La stratégie url n'est pas incluse ou le hook reroute manque | Ajoutez 'url' à la stratégie et implémentez reroute dans hooks.ts |
| Les messages ne sont pas typés | emitTsDeclarations: false ou TypeScript < 5.6 | Passez emitTsDeclarations: true et mettez à jour TypeScript |
| Le bundle contient trop de messages | Accès dynamique aux messages (m[someVar]()) | Mappez les clés à des appels de messages explicites |
<html lang> n'est pas mis à jour après le changement de locale | Utilisation de setLocale(locale, { reload: false }) sur une page SSR | Utilisez la navigation complète du document (par défaut) |
| Le prérendu échoue avec "URL pattern not found" | urlPatterns manquant pour le préfixe de locale | Ajoutez des patterns comme { pattern: '/:path(.*)?', localized: [[…]] } |
10. Référence
- Documentation Paraglide JS
- Documentation SvelteKit
- Documentation des runes Svelte 5
- Dépôt GitHub du benchmark — résultats : ~47 Ko vs ~205 Ko pour l'approche runtime