Communitygithub.com

agentmemoir/skills

Conseils d'expert pour construire des applications i18n performantes et typées avec Paraglide JS 2.x (basé sur un compilateur, tree-shakable) et Svelte 5 (runes). Couvre la configuration, la rédaction de messages, les stratégies de locale, le routage i18n, SSR/SSG, le texte enrichi avec les snippets Svelte 5, les patterns de réactivité, les tests et le dépannage.

Qu'est-ce que skills ?

Ce skill fournit des directives complètes et des bonnes pratiques pour intégrer Paraglide JS 2.x avec Svelte 5 et SvelteKit. 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, ce qui réduit la taille des bundles i18n jusqu'à ~70 % par rapport aux bibliothèques d'exécution (environ 47 Ko contre environ 205 Ko dans les benchmarks publiés avec 5 locales / 200 messages). Le guide couvre tous les aspects essentiels : l'installation via `npx sv add paraglide`, la configuration manuelle du plugin Vite et des hooks SvelteKit, l'utilisation des messages avec paramètres typés, la gestion de la locale via des stratégies (URL, cookie, localStorage) et la navigation complète du document, et la création de liens localisés. Il explique également comment utiliser le composant `<ParaglideMessage>` pour le texte enrichi avec les snippets Svelte 5, et comment intégrer les runes Svelte 5 avec Paraglide de manière idiomatique, en précisant que la locale est un état de requête/document, et non un état réactif de l'application. Les sections sur le SSR, l'hydratation, la génération de sites statiques (SSG), et les benchmarks de performance sont également incluses.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/agentmemoir/skills/tree/HEAD/skills/frontend/paraglide-svelte5-best-practices

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

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écisionValeur par défaut recommandéePourquoi
Chemin d'installationnpx sv add paraglide="languageTags:en,de" sur une nouvelle application SvelteKitCrée le plugin Vite, les hooks, les attributs app.html, .gitignore en une seule étape
Package@inlang/paraglide-js v2.xL'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 localePar 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
AsyncLocalStorageGardez 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 TSemitTsDeclarations: 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 messagesClé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érenduexport const prerender = true + entries() par localeSSG a besoin de chaque locale pour résoudre en un fichier URL distinct
Réactivité pour la localeAucune dans les composants par défautgetLocale() 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 : mettez paraglideHandle en premier (ou avant les gestionnaires d'authentification qui ont besoin de event.locals indexé par locale) afin que les hooks en aval voient la requête délocalisée.
  • Derrière un proxy de terminaison TLS, passez effectiveRequestUrl pour que la correspondance d'URL et les redirections utilisent l'origine côté navigateur.
  • Gardez AsyncLocalStorage activé ; c'est ce qui rend getLocale() sûr sous des requêtes concurrentes et SSR en streaming. Le repli disableAsyncLocalStorage est uniquement pour les runtimes sans node:async_hooks et 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, localStorage si 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ômeCause probableCorrectif
getLocale() retourne la mauvaise locale côté serveurOrdre de stratégie incorrect ou disableAsyncLocalStorage activéPassez ["url", "cookie", "baseLocale"] et gardez AsyncLocalStorage activé
setLocale() ne met pas à jour l'URLLa stratégie url n'est pas incluse ou le hook reroute manqueAjoutez 'url' à la stratégie et implémentez reroute dans hooks.ts
Les messages ne sont pas typésemitTsDeclarations: false ou TypeScript < 5.6Passez emitTsDeclarations: true et mettez à jour TypeScript
Le bundle contient trop de messagesAccè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 localeUtilisation de setLocale(locale, { reload: false }) sur une page SSRUtilisez 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 localeAjoutez des patterns comme { pattern: '/:path(.*)?', localized: [[…]] }

10. Référence

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

agentmemoir/skills

>- Build, migrate, or review server-rendered Svelte 5 apps using runes ($state, $derived, $effect, $props, $bindable) with SvelteKit SSR (adapter-auto/node/cloudflare/vercel/netlify) or plain Vite SSR using svelte/server render() and hydrate(). Use whenever I mention Svelte 5 SSR, server-side rendering, SvelteKit load functions, +page.server.js, +layout.server.js, hooks.server.js, form actions, streaming SSR, CSP nonces, hydration mismatches, or request-scoped state. Also use to review .svelte and server files for SSR-safety bugs: effects that never run on the server, module-level state leaking between users, non-deterministic render values, and secrets reaching the client. Trigger even on a bare pasted component that "renders differently on the server and client".

agentmemoir/skills

>- Build, migrate, or review static (no server) Svelte 5 apps using runes ($state, $derived, $effect, $props, $bindable) instead of legacy Svelte 4 reactivity (export let, reactive dollar-colon statements, stores-as-default). Use whenever I mention Svelte 5, runes, a "static Svelte site", "Svelte SPA", "SvelteKit static site"/"adapter-static"/prerendered SvelteKit, deploying Svelte to GitHub Pages/Netlify/Cloudflare Pages/S3/any CDN, or converting Svelte 4 code (export let, on:click, writable stores) to Svelte 5. Also use when writing or reviewing .svelte files that must ship with no backend, to flag accidental server-only SvelteKit files (+page.server.js, +server.js, form actions) or exposed secrets, and to apply the bundled XSS/CSP/secrets security checklist. Trigger even for a bare pasted .svelte file or a vague "make this a static site" request.

Skills associés