Tutoriels//10 min de lecture

SvelteKit i18n : Créer des applications multilingues type-safe avec Svelte 5

Eray Gündoğmuş
Partager

SvelteKit i18n : Créer des applications multilingues type-safe avec Svelte 5

L'architecture de SvelteKit se prête étonnamment bien à l'internationalisation. Ses fonctions de chargement côté serveur s'exécutent avant le rendu, ce qui rend la détection de la locale et le chargement des messages parfaitement naturels. Le routage basé sur les URL avec des hiérarchies de layouts signifie que vous pouvez préfixer les routes avec /en/ ou /de/ sans lutter contre le framework. Et les runes de Svelte 5 — $state, $derived, $effect — vous offrent un changement de locale réactif sans le code répétitif des stores requis dans Svelte 4.

Ce guide explique comment créer une application SvelteKit multilingue prête pour la production avec Svelte 5. Nous aborderons la détection de la locale, les fonctions de traduction type-safe, la pluralisation avec l'API Intl, et les considérations SEO. Nous examinerons aussi honnêtement les compromis entre le regroupement des fichiers de traduction et leur diffusion depuis un CDN.

Configuration du projet

Commencez avec un nouveau projet SvelteKit (template TypeScript) :

npx sv create my-i18n-app
cd my-i18n-app
npm install

Pour ce guide, nous maintiendrons les dépendances au minimum — les API Intl intégrées aux navigateurs modernes gèrent le formatage, et nous écrirons nous-mêmes une fine couche de traduction. Si vous avez besoin du support du format de message ICU, des bibliothèques comme @messageformat/core peuvent s'intégrer.

Structure de dossiers visée :

src/
  lib/
    i18n/
      index.ts          # Utilitaires de traduction principaux
      types.ts          # Types générés/partagés
      locales/
        en.json
        de.json
        fr.json
  hooks.server.ts       # Détection de locale
  routes/
    [locale]/
      +layout.server.ts # Charger les traductions
      +layout.svelte    # Fournir les traductions à l'arbre
      +page.svelte

Définissez les locales supportées dans une configuration partagée :

// src/lib/i18n/config.ts
export const SUPPORTED_LOCALES = ['en', 'de', 'fr'] as const;
export type Locale = typeof SUPPORTED_LOCALES[number];
export const DEFAULT_LOCALE: Locale = 'en';

export function isSupportedLocale(locale: string): locale is Locale {
  return SUPPORTED_LOCALES.includes(locale as Locale);
}

Détection de locale et routage

Le fichier hooks.server.ts de SvelteKit est l'endroit idéal pour détecter la locale d'un utilisateur avant l'exécution de toute route. Nous lirons d'abord depuis l'URL, nous nous rabattrons sur l'en-tête Accept-Language, puis sur la locale par défaut.

// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { DEFAULT_LOCALE, isSupportedLocale } from '$lib/i18n/config';

export const handle: Handle = async ({ event, resolve }) => {
  // Extraire la locale depuis le segment de chemin URL
  const pathLocale = event.url.pathname.split('/')[1];

  const locale = isSupportedLocale(pathLocale)
    ? pathLocale
    : detectFromHeader(event.request.headers.get('accept-language'));

  event.locals.locale = locale;

  return resolve(event, {
    transformPageChunk: ({ html }) => html.replace('%lang%', locale),
  });
};

function detectFromHeader(header: string | null): Locale {
  if (!header) return DEFAULT_LOCALE;

  const accepted = header
    .split(',')
    .map((part) => {
      const [lang, q = 'q=1'] = part.trim().split(';');
      return { lang: lang.split('-')[0].trim(), q: parseFloat(q.split('=')[1]) };
    })
    .sort((a, b) => b.q - a.q);

  for (const { lang } of accepted) {
    if (isSupportedLocale(lang)) return lang;
  }

  return DEFAULT_LOCALE;
}

Ajoutez le champ locale au type locals de votre application :

// src/app.d.ts
import type { Locale } from '$lib/i18n/config';

declare global {
  namespace App {
    interface Locals {
      locale: Locale;
    }
  }
}

Pour le routage basé sur les URL, créez un segment [locale] au niveau supérieur et ajoutez un matcher de paramètres pour le valider :

// src/params/locale.ts
import { isSupportedLocale } from '$lib/i18n/config';

export function match(param: string): boolean {
  return isSupportedLocale(param);
}

Référencez ce matcher dans le répertoire de vos routes : src/routes/[locale=locale]/. SvelteKit ne correspondra qu'aux chemins où le segment est une chaîne de locale valide.

Chargement des traductions

Dans +layout.server.ts sous le segment [locale], chargez les traductions pour la locale courante. Cela s'exécute côté serveur avant le rendu de la page, donc il n'y a pas de flash de contenu non traduit.

// src/routes/[locale=locale]/+layout.server.ts
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ params, fetch }) => {
  const locale = params.locale;

  // Option A : JSON regroupé (importé directement)
  const messages = await import(`$lib/i18n/locales/${locale}.json`);

  // Option B : Endpoint CDN (pas de redéploiement nécessaire pour les mises à jour de traduction)
  // const res = await fetch(`https://cdn.example.com/translations/${locale}.json`);
  // const messages = await res.json();

  return { locale, messages: messages.default };
};

Dans le composant layout, rendez les traductions disponibles aux composants enfants :

<!-- src/routes/[locale=locale]/+layout.svelte -->
<script lang="ts">
  import { setContext } from 'svelte';
  import { createTranslator } from '$lib/i18n';
  import type { LayoutData } from './$types';

  const { data }: { data: LayoutData } = $props();

  const t = createTranslator(data.messages);
  setContext('t', t);
  setContext('locale', data.locale);
</script>

<slot />

Utiliser les traductions dans les composants avec les runes Svelte 5

Avec Svelte 5, vous pouvez utiliser $state et $derived pour rendre les traductions réactives lors du changement de locale. Voici un modèle de store de traduction qui fonctionne parfaitement avec les runes :

// src/lib/i18n/index.ts
import type { Messages } from './types';

export function createTranslator(messages: Messages) {
  return function t(key: keyof Messages, params?: Record<string, string | number>): string {
    const template = messages[key] ?? key;

    if (!params) return template;

    return Object.entries(params).reduce<string>(
      (result, [k, v]) => result.replaceAll(`{${k}}`, String(v)),
      template
    );
  };
}

Dans un composant, récupérez le traducteur depuis le contexte :

<!-- src/routes/[locale=locale]/+page.svelte -->
<script lang="ts">
  import { getContext } from 'svelte';
  import type { Translator } from '$lib/i18n/types';

  const t = getContext<Translator>('t');
  const locale = getContext<string>('locale');

  // Svelte 5 : état dérivé depuis le contexte
  let greeting = $derived(t('home.greeting', { name: 'World' }));
</script>

<h1>{greeting}</h1>
<p>{t('home.description')}</p>

Pour le changement de locale sans rechargement complet de la page, mettez à jour le traducteur de manière réactive avec $state :

<script lang="ts">
  import { createTranslator } from '$lib/i18n';
  import type { Messages } from '$lib/i18n/types';

  let messages = $state<Messages>({} as Messages);
  let t = $derived(createTranslator(messages));

  async function switchLocale(newLocale: string) {
    const res = await fetch(`/api/translations/${newLocale}`);
    messages = await res.json();
    // Mettre à jour l'URL sans rechargement
    history.pushState({}, '', `/${newLocale}${window.location.pathname.replace(/^\/[^\/]+/, '')}`);
  }
</script>

Typage strict

Une fonction de traduction type-safe détecte les clés manquantes à la compilation et active l'autocomplétion de l'IDE. Générez les types depuis votre fichier de locale de base :

// src/lib/i18n/types.ts
// Auto-généré depuis en.json — ne pas modifier manuellement
import type en from './locales/en.json';

export type Messages = typeof en;
export type MessageKey = keyof Messages;
export type Translator = (key: MessageKey, params?: Record<string, string | number>) => string;

Vous pouvez automatiser cette étape. Un script simple pour régénérer les types :

// scripts/generate-i18n-types.ts
import { writeFileSync } from 'fs';

const output = `// Auto-généré depuis en.json — ne pas modifier manuellement
import type en from './locales/en.json';

export type Messages = typeof en;
export type MessageKey = keyof Messages;
export type Translator = (key: MessageKey, params?: Record<string, string | number>) => string;
`;

writeFileSync('src/lib/i18n/types.ts', output);
console.log('Types générés.');

Intégrez-le dans votre processus de build dans package.json :

{
  "scripts": {
    "generate:i18n": "tsx scripts/generate-i18n-types.ts",
    "prebuild": "npm run generate:i18n",
    "predev": "npm run generate:i18n"
  }
}

Désormais, t('home.greetng') produit une erreur TypeScript car 'home.greetng' n'existe pas dans Messages. Les fautes de frappe sont détectées à la compilation plutôt que découvertes par les utilisateurs en production.

C'est l'un des domaines où des plateformes comme Better i18n apportent une réelle valeur ajoutée : elles génèrent automatiquement les types SDK depuis votre schéma de traduction, de sorte que chaque fois qu'un traducteur ajoute ou renomme une clé, vos types TypeScript se mettent à jour sans exécution manuelle de scripts.

Si vous développez des applications React ou Next.js en parallèle de Svelte, vous constaterez que les mêmes patterns type-safe s'appliquent — voir React i18n : Le guide complet de l'internationalisation React et Next.js App Router i18n : Server Components et patterns RSC pour des implémentations spécifiques aux frameworks.

Pluralisation et formatage

L'API Intl intégrée à JavaScript gère correctement la pluralisation dans toutes les locales sans dépendances externes :

// src/lib/i18n/format.ts

export function pluralize(
  locale: string,
  count: number,
  forms: Record<Intl.LDMLPluralRule, string>
): string {
  const rule = new Intl.PluralRules(locale).select(count);
  const template = forms[rule] ?? forms.other;
  return template.replace('{count}', String(count));
}

export function formatNumber(locale: string, value: number, options?: Intl.NumberFormatOptions): string {
  return new Intl.NumberFormat(locale, options).format(value);
}

export function formatDate(locale: string, date: Date, options?: Intl.DateTimeFormatOptions): string {
  return new Intl.DateTimeFormat(locale, options).format(date);
}

Utilisation dans un composant :

<script lang="ts">
  import { pluralize, formatNumber, formatDate } from '$lib/i18n/format';
  import { getContext } from 'svelte';

  const locale = getContext<string>('locale');

  const itemCount = 3;
  const price = 1499.99;
  const publishedAt = new Date('2024-06-15');

  let itemLabel = $derived(
    pluralize(locale, itemCount, {
      one: '{count} élément',
      other: '{count} éléments',
      zero: 'aucun élément',
      two: '{count} éléments',
      few: '{count} éléments',
      many: '{count} éléments',
    })
  );

  let formattedPrice = $derived(formatNumber(locale, price, { style: 'currency', currency: 'USD' }));
  let formattedDate = $derived(formatDate(locale, publishedAt, { dateStyle: 'long' }));
</script>

<p>{itemLabel}</p>
<p>{formattedPrice}</p>
<p>{formattedDate}</p>

Pour la locale allemande, 1499.99 s'affiche comme 1.499,99 $ et la date comme 15. Juni 2024 — sans bibliothèques supplémentaires.

SEO pour SvelteKit multilingue

Les moteurs de recherche ont besoin de signaux explicites sur les versions en différentes langues de vos pages. Ajoutez des balises hreflang et des métadonnées localisées dans le <svelte:head> de votre layout. Faire cela correctement est essentiel pour le classement multilingue — notre guide complet sur le SEO i18n, les balises hreflang et les URL de locale couvre chaque détail d'implémentation et les pièges courants.

<!-- src/routes/[locale=locale]/+layout.svelte -->
<script lang="ts">
  import { page } from '$app/stores';
  import { SUPPORTED_LOCALES } from '$lib/i18n/config';

  const { data }: { data: LayoutData } = $props();

  // Construire les URL alternatives pour toutes les locales supportées
  let alternates = $derived(
    SUPPORTED_LOCALES.map((loc) => ({
      locale: loc,
      url: $page.url.href.replace(`/${data.locale}/`, `/${loc}/`),
    }))
  );
</script>

<svelte:head>
  {#each alternates as { locale, url }}
    <link rel="alternate" hreflang={locale} href={url} />
  {/each}
  <link rel="alternate" hreflang="x-default" href={alternates.find(a => a.locale === 'en')?.url} />
</svelte:head>

<slot />

Pour le pré-rendu, configurez SvelteKit pour générer toutes les variantes de locale :

// svelte.config.js
const config = {
  kit: {
    prerender: {
      entries: [
        '/en',
        '/de',
        '/fr',
        '/en/about',
        '/de/about',
        '/fr/about',
        // ... ou générer dynamiquement
      ],
    },
  },
};

Pour les entrées de pré-rendu dynamiques, utilisez un fichier +page.server.ts avec un export entries :

// src/routes/[locale=locale]/+page.server.ts
import { SUPPORTED_LOCALES } from '$lib/i18n/config';

export function entries() {
  return SUPPORTED_LOCALES.map((locale) => ({ locale }));
}

CDN-first vs traductions regroupées

Les deux approches fonctionnent — le bon choix dépend de votre flux de travail.

Traductions regroupées (importées depuis JSON) :

  • Aucune requête HTTP supplémentaire par chargement de page
  • Traductions verrouillées sur la version du déploiement
  • Fonctionne hors ligne et sans dépendances externes
  • La correction d'une faute de frappe dans une traduction nécessite un redéploiement

Diffusion CDN :

  • Les mises à jour de traduction sont publiées sans redéployer l'application
  • Les traducteurs peuvent pousser des modifications indépendamment des ingénieurs
  • Ajoute une requête réseau (atténuée par un cache agressif)
  • Nécessite une stratégie d'invalidation du cache

Pour la plupart des équipes au début de leur parcours i18n, le JSON regroupé est plus simple. À mesure que votre équipe de traduction grandit et que le rythme des mises à jour de contenu augmente, le couplage au déploiement devient pénible. Livrer une release de code uniquement pour corriger un libellé de bouton en allemand n'est pas scalable. Automatiser l'i18n dans votre pipeline CI/CD est la prochaine étape naturelle une fois que vous avez dépassé l'approche regroupée.

C'est le problème central pour lequel des plateformes comme Better i18n ont été construites : vos messages de traduction vivent sur leur CDN, servis via des URL versionnées avec de longs TTL de cache. Les ingénieurs mettent à jour le code, les traducteurs mettent à jour les traductions — indépendamment, sans se bloquer mutuellement. La page des fonctionnalités explique en détail comment cela fonctionne, notamment comment l'invalidation du cache est gérée lors de la publication d'une traduction.

Il existe aussi un juste milieu : récupérer les traductions au moment du build depuis un CDN et regrouper le résultat dans l'artefact de déploiement. Vous obtenez des chargements locaux rapides sans regrouper les fichiers de locale dans votre dépôt, mais vous avez toujours besoin d'un redéploiement pour les mises à jour.

Résumé

SvelteKit et Svelte 5 constituent une base solide pour les applications multilingues :

  • Les hooks gèrent la détection de locale côté serveur avant tout rendu
  • Les segments de route dynamiques avec matchers de paramètres vous donnent un routage de locale basé sur des URL propres
  • Les fonctions de chargement récupèrent les traductions une fois par layout et les transmettent dans l'arbre de composants
  • Les runes Svelte 5 ($state, $derived) rendent le changement de locale réactif sans abonnements manuels aux stores
  • Les types TypeScript générés depuis votre locale de base détectent les clés manquantes ou mal orthographiées à la compilation
  • Les API Intl gèrent la pluralisation et le formatage des nombres/dates correctement pour chaque locale
  • Les balises hreflang svelte:head et les routes de locale pré-rendues informent les moteurs de recherche

L'architecture décrite ici répond à la plupart des exigences de production. La principale lacune qu'elle laisse est opérationnelle : à mesure que votre application grandit, la gestion des fichiers de traduction dans le dépôt crée des frictions entre les ingénieurs et les traducteurs. La diffusion CDN et la génération automatique de types sont les deux domaines où les outils spécialisés se rentabilisent le plus rapidement.

Si vous développez une application SvelteKit avec une vraie équipe de traduction, jetez un œil à l'intégration Svelte de Better i18n — elle fournit un SDK type-safe, la diffusion CDN dès le départ, et un workflow de révision basé sur Git pour que les modifications de traduction passent par le même processus de révision que le code.

Better i18n est une plateforme de localisation axée sur les développeurs, conçue pour les équipes frontend modernes. SDK type-safe, workflows basés sur Git, diffusion CDN, et traduction IA avec application du glossaire — sans fichiers de locale dans votre dépôt.


Rendez votre application mondiale avec better-i18n

better-i18n combine des traductions alimentées par l'IA, des workflows natifs Git et une diffusion CDN mondiale en une seule plateforme axée sur les développeurs. Arrêtez de gérer des tableurs et commencez à livrer dans toutes les langues.

Commencer gratuitement → · Explorer les fonctionnalités · Lire la documentation

Comments

Loading comments...