Internationalisation (i18n)

EmberKit fournit un cœur i18n léger et tree-shakeable — aucun runtime tiers requis. Utilisez des catalogues TypeScript ou des fichiers JSON de traduction.

Vue d’ensemble

L’i18n EmberKit repose sur quatre idées :

  • Catalogues de messages — cartes clé/valeur plates ou imbriquées par locale
  • Interpolation {placeholder}t('greeting', { name: 'Ada' })
  • Syntaxe plurielle avec pipeone {count} item | other {count} items
  • Formateurs Intl — dates, nombres et temps relatif sans coût bundle supplémentaire

La locale circule via le contexte (createI18nContext) et peut être résolue par requête côté serveur.

Structure du projet (JSON — recommandé)

src/
├── lib/
│   └── i18n.ts              # createI18nFromGlob + export du provider
├── locales/
│   ├── en.json
│   └── es.json
└── routes/
    ├── [locale]/
    │   ├── _layout.tsx      # I18nProvider + locale depuis le loader
    │   └── index.tsx
    └── index.tsx            # redirection optionnelle vers la locale par défaut
public/
└── locales/                 # uniquement pour chargement différé via fetch
    ├── en.json
    └── es.json

src/locales/en.json

{
  "greeting": "Hello {name}",
  "nav": {
    "home": "Home",
    "about": "About"
  },
  "items": "one {count} item | other {count} items"
}

Les objets imbriqués sont aplatis en clés pointées — nav.home dans le code.

src/lib/i18n.ts

import { createI18nFromGlob, createI18nContext, resolveLocaleFromRequest } from '@emberkit/core';

const modules = import.meta.glob('../locales/*.json', { eager: true });

export const i18n = createI18nFromGlob(modules, {
  locales: ['en', 'es'] as const,
  defaultLocale: 'en',
  fallbackLocale: 'en',
});

export const { Provider: I18nProvider, useI18n } = createI18nContext();

export function resolveRequestLocale(request: Request): string {
  return resolveLocaleFromRequest(request, {
    locales: i18n.locales,
    defaultLocale: i18n.defaultLocale,
    strategy: ['path-prefix', 'header', 'cookie'],
  });
}

Les noms de fichier deviennent des codes locale : en.jsonen, es-MX.jsones-MX.

Chargement des fichiers de traduction

MéthodeAPIQuand l’utiliser
Vite globcreateI18nFromGlobPar défaut — empaquette toutes les locales au build
Import statiquecreateI18nFromJsonUne seule locale ou imports explicites
Fetch runtimecreateI18nFromUrlsChargement différé depuis public/locales/
Système de fichiers NodecreateI18nFromDirectoryScripts SSR, CLI, outillage Node uniquement

Import statique

import en from './locales/en.json';
import es from './locales/es.json';
import { createI18nFromJson } from '@emberkit/core';

export const i18n = createI18nFromJson({
  locales: ['en', 'es'] as const,
  defaultLocale: 'en',
  messages: { en, es },
});

Vite et TypeScript résolvent les imports JSON lorsque resolveJsonModule est activé dans tsconfig.json.

Chargement différé au runtime (fetch)

import { createI18nFromUrls } from '@emberkit/core';

export const i18n = await createI18nFromUrls(
  {
    en: '/locales/en.json',
    es: '/locales/es.json',
  },
  { locales: ['en', 'es'] as const, defaultLocale: 'en' },
);

Placez le JSON sous public/locales/ pour que le serveur de dev et la prod les servent en statique.

Node / SSR (système de fichiers)

import { createI18nFromDirectory } from '@emberkit/core/i18n/node';

export const i18n = createI18nFromDirectory('./src/locales', {
  locales: ['en', 'es'] as const,
  defaultLocale: 'en',
});

Également disponible depuis @emberkit/core/i18n/node :

  • readLocaleCatalog(filePath) — lire et parser un fichier JSON
  • loadLocalesFromDirectory(dir) — charger tous les *.json d’un dossier

Catalogues TypeScript

Utilisez defineMessages pour des catalogues colocalisés en TypeScript avec inférence complète des clés :

import { createI18n, defineMessages } from '@emberkit/core';

const en = defineMessages({
  'nav.home': 'Home',
  greeting: 'Hello {name}',
  items: 'one {count} item | other {count} items',
});

const es = defineMessages({
  'nav.home': 'Inicio',
  greeting: 'Hola {name}',
  items: 'one {count} artículo | other {count} artículos',
});

export const i18n = createI18n({
  locales: ['en', 'es'] as const,
  defaultLocale: 'en',
  fallbackLocale: 'en',
  messages: { en, es },
});

Traduire

i18n.t('greeting', { name: 'World' }); // Hello World
i18n.setLocale('es');
i18n.t('greeting', { name: 'Mundo' }); // Hola Mundo

// Plural pipe syntax
i18n.tp('items', 1, { count: 1 });
i18n.tp('items', 5, { count: 5 });

// Intl helpers (respect current locale)
i18n.formatDate(new Date(), { dateStyle: 'medium' });
i18n.formatNumber(1234.5);
i18n.formatRelativeTime(-1, 'day'); // "1 day ago"

Passez { strict: true } comme second argument à createI18n / createI18nFromJson pour lever MissingTranslationError sur les clés manquantes (utile en CI).

Provider et hooks

import { I18nProvider, useI18n, i18n } from '../lib/i18n';

function App({ locale, children }: { locale: string; children?: unknown }) {
  return (
    <I18nProvider i18n={i18n} locale={locale}>
      {children}
    </I18nProvider>
  );
}

function Header() {
  const { t, setLocale, formatDate } = useI18n();
  return (
    <header>
      <span>{t('nav.home')}</span>
      <button type="button" onClick={() => setLocale('es')}>ES</button>
      <time>{formatDate(new Date(), { dateStyle: 'medium' })}</time>
    </header>
  );
}

Routes avec préfixe de locale

Associez l’i18n à un segment dynamique [locale] :

src/routes/[locale]/index.tsx   →  /en, /es
src/routes/[locale]/about.tsx   →  /en/about, /es/about
// src/routes/[locale]/_layout.tsx
import type { RouteComponent } from '@emberkit/core';
import { I18nProvider, i18n, resolveRequestLocale } from '../../lib/i18n';

export async function loader({ request }: { request: Request }) {
  const locale = resolveRequestLocale(request);
  return { data: { locale } };
}

const LocaleLayout: RouteComponent<{ locale: string }> = ({ locale, children }) => (
  <I18nProvider i18n={i18n} locale={locale}>
    {children}
  </I18nProvider>
);

export default LocaleLayout;

Helpers de chemin pour liens et redirections :

import { extractLocaleFromPath, localizePath } from '@emberkit/core';

const { locale, pathnameWithoutLocale } = extractLocaleFromPath('/es/blog/post', ['en', 'es']);
// locale: 'es', pathnameWithoutLocale: '/blog/post'

localizePath('/en/about', 'es', ['en', 'es']); // '/es/about'

Sur Cloudflare Workers, utilisez beforeAssets dans createCloudflareWorker pour rediriger les visiteurs vers leur préfixe de locale préféré. Voir Déploiement edge.

Détection de locale SSR

resolveLocaleFromRequest vérifie les stratégies dans l’ordre et retombe sur defaultLocale :

StratégieSource
path-prefixPremier segment d’URL (/es/aboutes)
headerAccept-Language
cookieCookie locale (nom configurable)
query?lang=es (paramètre configurable)
import { resolveLocaleFromRequest } from '@emberkit/core';

export async function loader({ request }: RouteParams) {
  const locale = resolveLocaleFromRequest(request, {
    locales: ['en', 'es'],
    defaultLocale: 'en',
    strategy: ['path-prefix', 'header', 'cookie'],
    cookieName: 'locale',
    queryParam: 'lang',
  });
  i18n.setLocale(locale);
  return { data: { locale } };
}

Analyse et validation

Helpers bas niveau pour pipelines personnalisés :

import { parseMessageCatalog, parseMessageCatalogJson } from '@emberkit/core';

const catalog = parseMessageCatalog({ nav: { home: 'Home' } });
// { 'nav.home': 'Home' }

const fromFile = parseMessageCatalogJson('{"hello":"world"}');

Les catalogues invalides lèvent InvalidMessageCatalogError (valeurs feuille non string, JSON mal formé).

Scaffold CLI

emberkit generate i18n app --path src/lib/i18n.ts

Le fichier généré utilise createI18nFromGlob et attend des JSON dans un dossier locales/ à côté.

Référence API

Voir Référence API — Internationalisation pour la liste complète des fonctions.

Liens connexes

  • Contexte — mécanisme de provider sous-jacent
  • Routage — segments dynamiques et loaders
  • SSR — rendu serveur et request dans les loaders
  • Déploiement edge — Workers et redirections de locale