--- name: composable-svelte-i18n description: Internationalization (i18n) system for Composable Svelte. Use when implementing multi-language support, translations, date/number formatting, locale detection, or localizing applications. Covers translation loading, ICU MessageFormat, formatters (dates/numbers/currency), locale detection, SSR integration, and the i18n reducer. --- # Composable Svelte Internationalization (i18n) Complete i18n solution integrated with the Composable Architecture. Handles translations, formatting (dates, numbers, currency), locale detection, and works seamlessly with SSR/SSG. --- ## CRITICAL RULES ### Rule 1: i18n State Lives in Store **Principle**: i18n state is part of your application state, integrated via the i18n reducer. #### ✅ CORRECT - i18n in Store State ```typescript import { createInitialI18nState, i18nReducer } from '@composable-svelte/core/i18n'; interface AppState { todos: Todo[]; i18n: I18nState; // ✅ i18n integrated with app state } type AppAction = | { type: 'addTodo'; text: string } | I18nAction; // ✅ i18n actions part of app actions const appReducer: Reducer = (state, action, deps) => { // Handle i18n actions if (typeof action.type === 'string' && action.type.startsWith('i18n/')) { const [newI18nState, i18nEffect] = i18nReducer(state.i18n, action as any, deps); return [ { ...state, i18n: newI18nState }, i18nEffect as Effect ]; } // Handle other actions... return [state, Effect.none()]; }; ``` #### ❌ WRONG - Separate i18n Store ```typescript // ❌ Don't create a separate store for i18n const i18nStore = createStore({ initialState: i18nState, reducer: i18nReducer }); const appStore = createStore({ initialState: appState, reducer: appReducer }); ``` **WHY**: Single source of truth. i18n changes should flow through the same reducer pipeline as other state changes. --- ### Rule 2: Use Framework Formatters, Not Manual Formatting **Principle**: The framework provides locale-aware formatters. Never manually format dates, numbers, or currency. #### ✅ CORRECT - Framework Formatters ```svelte

{t('welcome', { name: 'Alice' })}

{formatters.date(post.date)}

{formatters.number(1234.56)}

{formatters.currency(99.99, 'USD')}

``` #### ❌ WRONG - Manual Formatting ```svelte

{new Date(post.date).toLocaleDateString()}

{price.toFixed(2)}

${price}

``` **WHY**: Manual formatting doesn't respect locale conventions. Formatters automatically handle regional differences (e.g., "January 5, 2025" vs "5 janvier 2025", "1,234.56" vs "1 234,56"). --- ## CORE CONCEPTS ### 1. Translation System Use `createTranslator()` for accessing translations. It handles interpolation, ICU MessageFormat, and fallback chains automatically. ```svelte

{t('app.title')}

{t('welcome', { name: 'Alice' })}

{t('items', { count: 5 })}

``` **Translation File Example** (`locales/en/common.json`): ```json { "app.title": "My Application", "welcome": "Welcome, {name}!", "items": "{count, plural, one {# item} other {# items}}", "greeting": "{gender, select, male {Hello Mr. {name}} female {Hello Ms. {name}} other {Hello {name}}}" } ``` --- ### 2. Formatters (Dates, Numbers, Currency) Use `createFormatters()` to get locale-bound formatters. All formatters automatically use the current locale. ```svelte

{formatters.date(post.date)}

{formatters.date(post.date, DateFormats.short)}

{formatters.number(1234.56)}

{formatters.currency(99.99, 'USD')}

{formatters.relativeTime(yesterday)}

``` **Available DateFormats**: - `DateFormats.short` - "1/5/25" - `DateFormats.medium` - "Jan 5, 2025" - `DateFormats.long` - "January 5, 2025" (default) - `DateFormats.full` - "Monday, January 5, 2025" --- ### 3. Locale Detection The framework provides three locale detectors for different environments. #### Browser (Client-Side) ```typescript import { createBrowserLocaleDetector } from '@composable-svelte/core/i18n'; const localeDetector = createBrowserLocaleDetector({ supportedLocales: ['en', 'fr', 'es'], defaultLocale: 'en', cookieName: 'locale', // Optional: persist preference storageKey: 'user_locale' // Optional: localStorage key }); const locale = localeDetector.detect(); // Checks: 1. localStorage, 2. cookie, 3. navigator.language, 4. default ``` #### SSR (Server-Side Rendering) ```typescript import { createSSRLocaleDetector } from '@composable-svelte/core/i18n'; // In your server handler const localeDetector = createSSRLocaleDetector({ supportedLocales: ['en', 'fr', 'es'], defaultLocale: 'en', url: request.url, // For ?lang=fr query param cookies: request.headers.get('cookie') ?? '', acceptLanguage: request.headers.get('accept-language') ?? '' }); const locale = localeDetector.detect(); // Checks: 1. URL (?lang=fr), 2. cookie, 3. Accept-Language, 4. default ``` #### SSG (Static Site Generation) ```typescript import { createStaticLocaleDetector } from '@composable-svelte/core/i18n'; // For pre-rendering specific locale const localeDetector = createStaticLocaleDetector('fr', ['en', 'fr', 'es']); const locale = localeDetector.detect(); // Always returns 'fr' ``` --- ### 4. Translation Loading Three strategies for loading translations based on your use case. #### Fetch Loader (Client-Side, Lazy Loading) ```typescript import { FetchTranslationLoader } from '@composable-svelte/core/i18n'; const translationLoader = new FetchTranslationLoader({ baseUrl: '/locales', // Fetches from /locales/{locale}/{namespace}.json supportedLocales: ['en', 'fr', 'es'] }); ``` **File Structure:** ``` public/ locales/ en/ common.json products.json fr/ common.json products.json ``` #### Bundled Loader (SSR, Build-Time) ```typescript import { BundledTranslationLoader } from '@composable-svelte/core/i18n'; // Import translations at build time import enCommon from '../locales/en/common.json'; import frCommon from '../locales/fr/common.json'; const translationLoader = new BundledTranslationLoader({ bundles: { 'en': { common: enCommon }, 'fr': { common: frCommon } } }); ``` #### Glob Loader (Vite-Specific, Auto-Discovery) ```typescript import { createGlobLoader } from '@composable-svelte/core/i18n'; const translationLoader = createGlobLoader( import.meta.glob('/src/locales/*/*.json') ); ``` --- ## COMPLETE SETUP EXAMPLES ### Client-Only Application ```typescript // src/store.ts import { createStore } from '@composable-svelte/core'; import { createInitialI18nState, i18nReducer, createBrowserLocaleDetector, FetchTranslationLoader, browserDOM } from '@composable-svelte/core/i18n'; // Detect locale const localeDetector = createBrowserLocaleDetector({ supportedLocales: ['en', 'fr', 'es'], defaultLocale: 'en', storageKey: 'app_locale' }); const initialLocale = localeDetector.detect(); // Initialize i18n state const i18nState = createInitialI18nState(initialLocale, ['en', 'fr', 'es'], 'en'); const initialState: AppState = { todos: [], i18n: i18nState }; // Create store with i18n dependencies const store = createStore({ initialState, reducer: appReducer, dependencies: { translationLoader: new FetchTranslationLoader({ baseUrl: '/locales', supportedLocales: ['en', 'fr', 'es'] }), localeDetector, storage: localStorage, dom: browserDOM } }); // Load initial translations store.dispatch({ type: 'i18n/loadNamespace', namespace: 'common' }); ``` ```svelte

{t('app.title')}

{t('welcome', { name: 'User' })}

{formatters.date(new Date())}

``` --- ### SSR Application **Server Setup:** ```typescript // src/server/index.ts import { renderToHTML } from '@composable-svelte/core/ssr'; import { createInitialI18nState, BundledTranslationLoader, createSSRLocaleDetector, serverDOM } from '@composable-svelte/core/i18n'; // Import translations import enCommon from '../locales/en/common.json'; import frCommon from '../locales/fr/common.json'; app.get('*', async (request, reply) => { // Detect locale from request const localeDetector = createSSRLocaleDetector({ supportedLocales: ['en', 'fr'], defaultLocale: 'en', url: request.url, cookies: request.headers.cookie ?? '', acceptLanguage: request.headers['accept-language'] ?? '' }); const locale = localeDetector.detect(); // Initialize i18n for this request const i18nState = createInitialI18nState(locale, ['en', 'fr'], 'en'); const initialState: AppState = { todos: [], i18n: i18nState }; // Render with i18n dependencies const html = await renderToHTML(App, { initialState, reducer: appReducer, dependencies: { translationLoader: new BundledTranslationLoader({ bundles: { 'en': { common: enCommon }, 'fr': { common: frCommon } } }), localeDetector, storage: new Map(), // SSR-safe storage dom: serverDOM } }); reply.type('text/html').send(html); }); ``` **Client Hydration:** ```typescript // src/client/index.ts import { hydrateStore } from '@composable-svelte/core/ssr'; import { FetchTranslationLoader, createBrowserLocaleDetector, browserDOM } from '@composable-svelte/core/i18n'; const store = hydrateStore( document.getElementById('__COMPOSABLE_SVELTE_STATE__')!.textContent, { reducer: appReducer, dependencies: { translationLoader: new FetchTranslationLoader({ baseUrl: '/locales', supportedLocales: ['en', 'fr'] }), localeDetector: createBrowserLocaleDetector({ supportedLocales: ['en', 'fr'], defaultLocale: 'en' }), storage: localStorage, dom: browserDOM } } ); ``` --- ## ICU MessageFormat The framework automatically detects and compiles ICU MessageFormat syntax for advanced translations. ### Pluralization ```json { "items": "{count, plural, =0 {No items} one {# item} other {# items}}" } ``` ```typescript t('items', { count: 0 }); // "No items" t('items', { count: 1 }); // "1 item" t('items', { count: 5 }); // "5 items" ``` ### Selection (Gender, etc.) ```json { "greeting": "{gender, select, male {Hello Mr. {name}} female {Hello Ms. {name}} other {Hello {name}}}" } ``` ```typescript t('greeting', { gender: 'male', name: 'Smith' }); // "Hello Mr. Smith" t('greeting', { gender: 'female', name: 'Johnson' }); // "Hello Ms. Johnson" t('greeting', { gender: 'other', name: 'Taylor' }); // "Hello Taylor" ``` ### Nested Plurals ```json { "cart": "{itemCount, plural, one {You have # item} other {You have # items}} in {cartCount, plural, one {# cart} other {# carts}}" } ``` ```typescript t('cart', { itemCount: 1, cartCount: 1 }); // "You have 1 item in 1 cart" t('cart', { itemCount: 5, cartCount: 2 }); // "You have 5 items in 2 carts" ``` --- ## COMMON PATTERNS ### Pattern 1: Language Switcher ```svelte
{#each availableLocales as locale} {/each}
``` ### Pattern 2: Lazy Loading Namespaces Load translations on-demand to reduce initial bundle size. ```typescript // Load translations when user navigates to products page function navigateToProducts() { store.dispatch({ type: 'i18n/loadNamespace', namespace: 'products' }); // Navigate after translations loaded (via effect) store.dispatch({ type: 'navigate', destination: { type: 'products' } }); } ``` **In Reducer:** ```typescript case 'i18n/namespaceLoaded': { // Translation loaded, safe to navigate if (action.namespace === 'products') { return [state, Effect.none()]; } return [state, Effect.none()]; } ``` ### Pattern 3: Formatted Lists ```svelte

Authors: {formattedAuthors}

``` --- ## BEST PRACTICES ### 1. Organize Translation Files by Namespace ``` locales/ en/ common.json # UI labels, buttons, navigation products.json # Product-related translations errors.json # Error messages validation.json # Form validation messages fr/ common.json products.json ... ``` **Benefits:** - Lazy load translations per feature - Easier maintenance - Smaller initial bundle ### 2. Use Descriptive Keys ```json { "nav.home": "Home", "nav.products": "Products", "products.addToCart": "Add to Cart", "products.outOfStock": "Out of Stock", "validation.required": "This field is required" } ``` **WHY**: Namespaced keys make it clear where translations are used and prevent conflicts. ### 3. Provide Context in Plurals ```json { "cart.items": "{count, plural, =0 {Your cart is empty} one {# item in cart} other {# items in cart}}" } ``` **WHY**: The `=0` case lets you provide a more natural message than "0 items". ### 4. Keep Formatters as Derived Values ```svelte ``` ### 5. Test with Multiple Locales ```typescript import { TestStore } from '@composable-svelte/core/test'; test('displays localized date', async () => { const store = new TestStore({ initialState: { post: { date: '2025-01-05' }, i18n: createInitialI18nState('fr', ['en', 'fr']) }, reducer: appReducer, dependencies: { /* ... */ } }); const formatters = createFormatters(store.state.i18n); expect(formatters.date(store.state.post.date)).toBe('5 janvier 2025'); }); ``` --- ## COMMON GOTCHAS ### Gotcha 1: Not Awaiting Translation Load ```typescript // ❌ WRONG - Translations not loaded yet store.dispatch({ type: 'i18n/setLocale', locale: 'fr' }); const t = createTranslator(store.state.i18n, 'common'); // May not have French translations! // ✅ CORRECT - Wait for load or preload store.dispatch({ type: 'i18n/setLocale', locale: 'fr', preloadNamespaces: ['common'] // Loads before completing }); ``` ### Gotcha 2: Forgetting to Handle i18n Actions in Reducer ```typescript // ❌ WRONG - i18n actions not handled const appReducer: Reducer = (state, action, deps) => { switch (action.type) { case 'addTodo': return [/* ... */]; // Missing i18n handling! } }; // ✅ CORRECT - Handle i18n actions const appReducer: Reducer = (state, action, deps) => { // Handle i18n actions first if (typeof action.type === 'string' && action.type.startsWith('i18n/')) { const [newI18nState, i18nEffect] = i18nReducer(state.i18n, action as any, deps); return [ { ...state, i18n: newI18nState }, i18nEffect as Effect ]; } switch (action.type) { case 'addTodo': return [/* ... */]; } }; ``` ### Gotcha 3: Using Wrong Storage on Server ```typescript // ❌ WRONG - localStorage doesn't exist on server const deps = { storage: localStorage, // Crashes in SSR! dom: serverDOM }; // ✅ CORRECT - Use SSR-safe storage const deps = { storage: new Map(), // Or mock storage dom: serverDOM }; ``` ### Gotcha 4: Not Binding Formatters to Locale ```typescript // ❌ WRONG - Formatters created once, won't update const formatters = createFormatters($store.i18n); // ✅ CORRECT - Derived, updates when locale changes const formatters = $derived(createFormatters($store.i18n)); ``` --- ## PERFORMANCE TIPS ### 1. Cache Formatters Per Locale The framework already caches `Intl.DateTimeFormat` and `Intl.NumberFormat` instances. Creating formatters is cheap. ### 2. Lazy Load Namespaces ```typescript // Load translations only when needed store.dispatch({ type: 'i18n/loadNamespace', namespace: 'products' }); ``` ### 3. Use Bundled Loader for SSR Bundled translations are faster than fetch on every request: ```typescript const translationLoader = new BundledTranslationLoader({ bundles: { 'en': { common: enCommon }, 'fr': { common: frCommon } } }); ``` ### 4. Preload Critical Namespaces ```typescript store.dispatch({ type: 'i18n/setLocale', locale: 'fr', preloadNamespaces: ['common', 'navigation'] // Load multiple at once }); ``` --- ## SSR/SSG INTEGRATION For complete SSR/SSG patterns, see the **composable-svelte-ssr** skill. Key points: ### SSR - Server: Use `BundledTranslationLoader`, `createSSRLocaleDetector`, `serverDOM` - Client: Use `FetchTranslationLoader`, `createBrowserLocaleDetector`, `browserDOM` - State serializes automatically via `renderToHTML` ### SSG (Static Sites) - Use `createStaticLocaleDetector` with fixed locale - Generate separate HTML files per locale (`/en/`, `/fr/`, `/es/`) - Language switcher uses links to pre-generated pages **Example**: See `examples/ssr-server/src/build/ssg.ts` for complete SSG setup. --- ## TYPESCRIPT SUPPORT All types are fully typed and exported: ```typescript import type { I18nState, I18nAction, I18nDependencies, Translator, BoundFormatters, TranslationLoader, LocaleDetector } from '@composable-svelte/core/i18n'; ``` --- ## TESTING Use `TestStore` to test translations and locale switching: ```typescript import { TestStore } from '@composable-svelte/core/test'; import { createTranslator } from '@composable-svelte/core/i18n'; test('switches locale', async () => { const store = new TestStore({ initialState: { i18n: createInitialI18nState('en', ['en', 'fr']) }, reducer: appReducer, dependencies: mockI18nDependencies }); await store.send({ type: 'i18n/setLocale', locale: 'fr', preloadNamespaces: ['common'] }); await store.receive({ type: 'i18n/namespaceLoaded', namespace: 'common' }, (state) => { expect(state.i18n.currentLocale).toBe('fr'); }); }); ``` --- ## SUMMARY **Core Principles:** 1. i18n state lives in your app store via `i18nReducer` 2. Use `createTranslator()` for translations, `createFormatters()` for dates/numbers/currency 3. Framework handles all locale-specific formatting automatically 4. ICU MessageFormat for pluralization and selection 5. Three loaders: Fetch (client), Bundled (SSR), Glob (Vite) 6. Three detectors: Browser, SSR, Static (SSG) **Most Common Pattern:** ```svelte

{t('app.title')}

{t('welcome', { name: user.name })}

{formatters.currency(product.price, 'USD')}
``` The i18n system is fully integrated with the Composable Architecture, providing a complete, type-safe, and performant solution for building multi-language applications.