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 pipe —
one {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.json → en, es-MX.json → es-MX.
Chargement des fichiers de traduction
| Méthode | API | Quand l’utiliser |
|---|---|---|
| Vite glob | createI18nFromGlob | Par défaut — empaquette toutes les locales au build |
| Import statique | createI18nFromJson | Une seule locale ou imports explicites |
| Fetch runtime | createI18nFromUrls | Chargement différé depuis public/locales/ |
| Système de fichiers Node | createI18nFromDirectory | Scripts 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 JSONloadLocalesFromDirectory(dir)— charger tous les*.jsond’un dossier
Edge et Workers
Préférez createI18nFromGlob ou des imports JSON statiques pour empaqueter les catalogues au build.
Les helpers système de fichiers nécessitent Node.js et ne sont pas disponibles sur Cloudflare Workers.
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égie | Source |
|---|---|
path-prefix | Premier segment d’URL (/es/about → es) |
header | Accept-Language |
cookie | Cookie 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
requestdans les loaders - Déploiement edge — Workers et redirections de locale