SolidJS i18n leverages the framework's fine-grained reactivity system to provide highly performant internationalization with minimal overhead and automatic UI updates when locale changes. Unlike React's re-render-based approach, SolidJS updates only the specific DOM nodes containing translated text through reactive primitives like signals and memos, resulting in zero virtual DOM diffing and instant locale switching. The @solid-primitives/i18n package provides createI18nContext() for creating reactive translation stores, template function syntax for interpolation, and automatic resource loading for code-splitting translation bundles. SolidJS i18n supports nested translation keys, pluralization rules via Intl.PluralRules, date and number formatting through Intl APIs, and seamless SSR integration with SolidStart for server-rendered multilingual applications. The reactive nature enables patterns impossible in other frameworks: translations update instantly without component re-mounting, locale changes propagate through the component tree automatically via Context API, and translation loading states integrate naturally with Suspense boundaries. This guide covers SolidJS i18n from basic setup through advanced patterns including lazy-loaded translations, type-safe translation keys with TypeScript, fallback locale chains, IntlPull integration for dynamic translation updates, and performance optimization techniques unique to SolidJS's reactivity model.
SolidJS Reactivity and i18n
SolidJS's reactive system provides unique advantages for internationalization that set it apart from React and other frameworks.
Fine-grained Reactivity: When locale changes, only text nodes containing translations update—not entire components. This is possible because SolidJS tracks dependencies at the DOM node level, not component level.
Signals for Locale State: The current locale is stored in a signal, making it a reactive primitive. Any component reading translations automatically subscribes to locale changes without explicit event handlers or context consumers.
Memos for Expensive Translations: Pluralization logic and complex interpolation can be wrapped in memos that only recompute when their dependencies (locale, count variables) change.
No Virtual DOM: Translation updates modify the DOM directly through compiled JavaScript, eliminating diffing overhead. This makes locale switching instantaneous even with thousands of translated strings on screen.
Context Without Re-renders: SolidJS Context doesn't trigger re-renders—it's a reactive store. Components reading from i18n context only update when the specific translation they access changes.
The architectural difference:
JavaScript1// React pattern (re-renders component) 2const [locale, setLocale] = useState('en'); 3const t = useTranslation(locale); // Component re-renders on locale change 4 5// SolidJS pattern (updates only text nodes) 6const [locale, setLocale] = createSignal('en'); 7const t = createI18n(locale); // Only translated DOM nodes update
Initial Setup with @solid-primitives/i18n
Install the official i18n primitive:
Terminalnpm install @solid-primitives/i18n
Basic Configuration
TypeScript1// src/i18n/config.ts 2 3import { Component, createContext, useContext, JSX, createSignal, createMemo } from 'solid-js'; 4import { createI18nContext, I18nContext } from '@solid-primitives/i18n'; 5 6// Translation dictionaries 7const en = { 8 hello: 'Hello', 9 greeting: (name: string) => `Hello, ${name}!`, 10 items: { 11 zero: 'No items', 12 one: '1 item', 13 other: (count: number) => `${count} items`, 14 }, 15 user: { 16 profile: 'User Profile', 17 settings: 'Settings', 18 }, 19}; 20 21const es = { 22 hello: 'Hola', 23 greeting: (name: string) => `¡Hola, ${name}!`, 24 items: { 25 zero: 'Sin artículos', 26 one: '1 artículo', 27 other: (count: number) => `${count} artículos`, 28 }, 29 user: { 30 profile: 'Perfil de Usuario', 31 settings: 'Configuración', 32 }, 33}; 34 35type Dictionary = typeof en; 36 37// Create i18n context 38export const createAppI18n = () => { 39 const [locale, setLocale] = createSignal<'en' | 'es'>('en'); 40 41 const dict = createMemo(() => { 42 const l = locale(); 43 return l === 'es' ? es : en; 44 }); 45 46 const i18n = createI18nContext(dict, locale); 47 48 return { ...i18n, locale, setLocale }; 49}; 50 51type I18nContextType = ReturnType<typeof createAppI18n>; 52 53const I18nContext = createContext<I18nContextType>(); 54 55export const I18nProvider: Component<{ children: JSX.Element }> = (props) => { 56 const i18n = createAppI18n(); 57 58 return ( 59 <I18nContext.Provider value={i18n}> 60 {props.children} 61 </I18nContext.Provider> 62 ); 63}; 64 65export const useI18n = () => { 66 const ctx = useContext(I18nContext); 67 if (!ctx) throw new Error('useI18n must be used within I18nProvider'); 68 return ctx; 69};
App Integration
TSX1// src/App.tsx 2 3import { Component } from 'solid-js'; 4import { I18nProvider } from './i18n/config'; 5import Home from './pages/Home'; 6 7const App: Component = () => { 8 return ( 9 <I18nProvider> 10 <Home /> 11 </I18nProvider> 12 ); 13}; 14 15export default App;
Using Translations in Components
Access translations through the reactive context:
TSX1// src/pages/Home.tsx 2 3import { Component, For } from 'solid-js'; 4import { useI18n } from '../i18n/config'; 5 6const Home: Component = () => { 7 const { t, locale, setLocale } = useI18n(); 8 9 const items = [1, 2, 3]; 10 11 return ( 12 <div> 13 <h1>{t('hello')}</h1> 14 15 <p>{t('greeting', 'Alice')}</p> 16 17 <p>Current locale: {locale()}</p> 18 19 <button onClick={() => setLocale(locale() === 'en' ? 'es' : 'en')}> 20 Switch Language 21 </button> 22 23 <For each={items}> 24 {(count) => ( 25 <div>{t('items', count)}</div> 26 )} 27 </For> 28 29 <div> 30 <h2>{t('user.profile')}</h2> 31 <a href="/settings">{t('user.settings')}</a> 32 </div> 33 </div> 34 ); 35}; 36 37export default Home;
Template Strings vs Functions
TypeScript1// Template string (simple) 2const dict = { 3 welcome: 'Welcome to MyApp', 4}; 5 6// Function (with interpolation) 7const dict = { 8 greeting: (name: string) => `Hello, ${name}!`, 9 itemCount: (count: number) => `You have ${count} items`, 10}; 11 12// Usage 13t('welcome') // "Welcome to MyApp" 14t('greeting', 'Alice') // "Hello, Alice!" 15t('itemCount', 5) // "You have 5 items"
Pluralization
Implement pluralization using Intl.PluralRules:
TypeScript1// src/i18n/plural.ts 2 3type PluralForms<T> = { 4 zero?: T; 5 one?: T; 6 two?: T; 7 few?: T; 8 many?: T; 9 other: T; 10}; 11 12export const createPlural = (locale: () => string) => { 13 return <T>(count: number, forms: PluralForms<T>): T => { 14 const pluralRules = new Intl.PluralRules(locale()); 15 const rule = pluralRules.select(count); 16 17 // Handle zero explicitly 18 if (count === 0 && forms.zero !== undefined) { 19 return forms.zero; 20 } 21 22 return forms[rule] ?? forms.other; 23 }; 24};
Usage in dictionaries:
TypeScript1import { createPlural } from './plural'; 2 3const en = { 4 items: (count: number, plural: ReturnType<typeof createPlural>) => 5 plural(count, { 6 zero: 'No items', 7 one: '1 item', 8 other: `${count} items`, 9 }), 10}; 11 12// In context 13export const createAppI18n = () => { 14 const [locale, setLocale] = createSignal<'en' | 'es'>('en'); 15 const dict = createMemo(() => (locale() === 'es' ? es : en)); 16 const plural = createPlural(locale); 17 18 const i18n = createI18nContext(dict, locale); 19 20 return { ...i18n, locale, setLocale, plural }; 21}; 22 23// Component usage 24const { t, plural } = useI18n(); 25<div>{t('items', 5, plural)}</div>
Lazy Loading Translations
Code-split translation bundles for better performance:
TypeScript1// src/i18n/loaders.ts 2 3export const loadTranslations = async (locale: string) => { 4 switch (locale) { 5 case 'es': 6 return (await import('./locales/es')).default; 7 case 'fr': 8 return (await import('./locales/fr')).default; 9 case 'de': 10 return (await import('./locales/de')).default; 11 default: 12 return (await import('./locales/en')).default; 13 } 14};
TypeScript1// src/i18n/context.tsx 2 3import { createResource, Suspense } from 'solid-js'; 4import { loadTranslations } from './loaders'; 5 6export const createAppI18n = () => { 7 const [locale, setLocale] = createSignal<string>('en'); 8 9 const [dict] = createResource(locale, loadTranslations); 10 11 const i18n = createI18nContext(dict, locale); 12 13 return { ...i18n, locale, setLocale }; 14}; 15 16// App wrapper 17<Suspense fallback={<div>Loading translations...</div>}> 18 <I18nProvider> 19 <App /> 20 </I18nProvider> 21</Suspense>
Type-Safe Translations with TypeScript
Ensure type safety for translation keys:
TypeScript1// src/i18n/types.ts 2 3export type TranslationKey = 4 | 'hello' 5 | 'greeting' 6 | 'user.profile' 7 | 'user.settings' 8 | 'items.zero' 9 | 'items.one' 10 | 'items.other'; 11 12type NestedKeys<T> = T extends object 13 ? { [K in keyof T]: K extends string ? `${K}` | `${K}.${NestedKeys<T[K]>}` : never }[keyof T] 14 : never; 15 16type Dictionary = typeof import('./locales/en').default; 17type DictKey = NestedKeys<Dictionary>; 18 19// Type-safe t function 20export type Translate = { 21 <K extends DictKey>(key: K): string; 22 <K extends DictKey, P>(key: K, ...params: P[]): string; 23};
Usage:
TSX1const { t } = useI18n(); 2 3// TypeScript autocomplete and error checking 4t('user.profile') // ✅ Valid 5t('user.invalid') // ❌ Type error
Date and Number Formatting
Leverage Intl APIs for locale-aware formatting:
TypeScript1// src/i18n/formatters.ts 2 3import { Accessor } from 'solid-js'; 4 5export const createFormatters = (locale: Accessor<string>) => { 6 const formatDate = (date: Date, options?: Intl.DateTimeFormatOptions) => { 7 return new Intl.DateTimeFormat(locale(), options).format(date); 8 }; 9 10 const formatNumber = (num: number, options?: Intl.NumberFormatOptions) => { 11 return new Intl.NumberFormat(locale(), options).format(num); 12 }; 13 14 const formatCurrency = (amount: number, currency: string) => { 15 return new Intl.NumberFormat(locale(), { 16 style: 'currency', 17 currency, 18 }).format(amount); 19 }; 20 21 const formatRelativeTime = (value: number, unit: Intl.RelativeTimeFormatUnit) => { 22 return new Intl.RelativeTimeFormat(locale(), { numeric: 'auto' }).format(value, unit); 23 }; 24 25 return { formatDate, formatNumber, formatCurrency, formatRelativeTime }; 26};
Add to context:
TypeScript1export const createAppI18n = () => { 2 const [locale, setLocale] = createSignal('en'); 3 const dict = createMemo(() => (locale() === 'es' ? es : en)); 4 const i18n = createI18nContext(dict, locale); 5 const formatters = createFormatters(locale); 6 7 return { ...i18n, locale, setLocale, ...formatters }; 8};
Component usage:
TSX1const { formatDate, formatCurrency, formatRelativeTime } = useI18n(); 2 3<div> 4 <p>{formatDate(new Date(), { dateStyle: 'long' })}</p> 5 <p>{formatCurrency(99.99, 'USD')}</p> 6 <p>{formatRelativeTime(-1, 'day')}</p> 7</div>
SolidStart SSR Integration
Configure i18n for server-side rendering:
TypeScript1// src/i18n/server.ts 2 3import { createAsync, cache } from '@solidjs/router'; 4import { getRequestEvent } from 'solid-js/web'; 5 6const getLocaleFromHeaders = cache(async () => { 7 'use server'; 8 const event = getRequestEvent(); 9 const acceptLanguage = event?.request.headers.get('accept-language'); 10 11 // Parse Accept-Language header 12 const locale = acceptLanguage?.split(',')[0]?.split('-')[0] || 'en'; 13 return ['en', 'es', 'fr', 'de'].includes(locale) ? locale : 'en'; 14}, 'locale'); 15 16export const useServerLocale = () => { 17 return createAsync(() => getLocaleFromHeaders()); 18};
Root component:
TSX1// src/root.tsx 2 3import { Suspense } from 'solid-js'; 4import { useServerLocale } from './i18n/server'; 5import { I18nProvider } from './i18n/config'; 6 7export default function Root() { 8 const serverLocale = useServerLocale(); 9 10 return ( 11 <Suspense> 12 <I18nProvider initialLocale={serverLocale()}> 13 <App /> 14 </I18nProvider> 15 </Suspense> 16 ); 17}
Update provider to accept initial locale:
TypeScript1export const I18nProvider: Component<{ 2 children: JSX.Element; 3 initialLocale?: string; 4}> = (props) => { 5 const [locale, setLocale] = createSignal(props.initialLocale || 'en'); 6 // ... rest of implementation 7};
IntlPull Integration
Integrate SolidJS with IntlPull for dynamic translation management:
TypeScript1// src/i18n/intlpull.ts 2 3import { createResource, createSignal } from 'solid-js'; 4 5const INTLPULL_API = 'https://api.intlpull.com/v1'; 6const PROJECT_ID = import.meta.env.VITE_INTLPULL_PROJECT_ID; 7 8type Translation = Record<string, string | Record<string, string>>; 9 10export const fetchTranslations = async (locale: string): Promise<Translation> => { 11 const response = await fetch( 12 `${INTLPULL_API}/projects/${PROJECT_ID}/export?language=${locale}&format=json` 13 ); 14 15 if (!response.ok) { 16 throw new Error(`Failed to fetch translations for ${locale}`); 17 } 18 19 return response.json(); 20}; 21 22export const createIntlPullI18n = () => { 23 const [locale, setLocale] = createSignal('en'); 24 const [translations] = createResource(locale, fetchTranslations); 25 26 const t = (key: string, ...params: any[]) => { 27 const dict = translations(); 28 if (!dict) return key; 29 30 // Nested key lookup 31 const value = key.split('.').reduce((obj, k) => obj?.[k], dict); 32 33 if (typeof value === 'function') { 34 return value(...params); 35 } 36 37 return value || key; 38 }; 39 40 return { t, locale, setLocale, translations }; 41};
Locale Persistence
Save user's locale preference:
TypeScript1// src/i18n/persistence.ts 2 3import { createSignal, createEffect } from 'solid-js'; 4 5const STORAGE_KEY = 'user-locale'; 6 7export const createPersistedLocale = (defaultLocale: string = 'en') => { 8 const stored = localStorage.getItem(STORAGE_KEY); 9 const [locale, setLocale] = createSignal(stored || defaultLocale); 10 11 // Persist to localStorage on change 12 createEffect(() => { 13 localStorage.setItem(STORAGE_KEY, locale()); 14 }); 15 16 // Also update html lang attribute 17 createEffect(() => { 18 document.documentElement.lang = locale(); 19 }); 20 21 return [locale, setLocale] as const; 22};
Usage:
TypeScript1export const createAppI18n = () => { 2 const [locale, setLocale] = createPersistedLocale('en'); 3 // ... rest of setup 4};
Language Switcher Component
TSX1// src/components/LanguageSwitcher.tsx 2 3import { Component, For } from 'solid-js'; 4import { useI18n } from '../i18n/config'; 5 6const languages = [ 7 { code: 'en', name: 'English' }, 8 { code: 'es', name: 'Español' }, 9 { code: 'fr', name: 'Français' }, 10 { code: 'de', name: 'Deutsch' }, 11]; 12 13export const LanguageSwitcher: Component = () => { 14 const { locale, setLocale } = useI18n(); 15 16 return ( 17 <select 18 value={locale()} 19 onChange={(e) => setLocale(e.currentTarget.value)} 20 class="language-select" 21 > 22 <For each={languages}> 23 {(lang) => ( 24 <option value={lang.code}>{lang.name}</option> 25 )} 26 </For> 27 </select> 28 ); 29};
Performance Optimization
Memoize Expensive Translations
TSX1import { createMemo } from 'solid-js'; 2 3const ExpensiveComponent = () => { 4 const { t } = useI18n(); 5 6 // Memoize complex translation logic 7 const formattedMessage = createMemo(() => { 8 const message = t('complex.message'); 9 // Expensive formatting 10 return processMarkdown(message); 11 }); 12 13 return <div innerHTML={formattedMessage()} />; 14};
Split Translation Bundles
TypeScript1// Load only needed translations per route 2const loadProductTranslations = () => import('./translations/products'); 3const loadCheckoutTranslations = () => import('./translations/checkout'); 4 5// In route definition 6{ 7 path: '/products', 8 component: lazy(() => import('./pages/Products')), 9 preload: () => loadProductTranslations(), 10}
Comparison with React i18n
| Feature | SolidJS | React |
|---|---|---|
| Updates | Fine-grained (only text nodes) | Re-render component tree |
| Performance | No virtual DOM diffing | Virtual DOM reconciliation |
| Locale switch | Instant | May cause flicker |
| Bundle size | ~2KB (@solid-primitives/i18n) | ~10KB (react-i18next) |
| SSR | Built-in with SolidStart | Requires setup |
| Learning curve | Similar to React patterns | Familiar to React devs |
Best Practices
1. Organize Translations by Feature
TypeScript1const translations = { 2 common: { /* shared strings */ }, 3 auth: { /* login, register */ }, 4 products: { /* product-specific */ }, 5 checkout: { /* checkout flow */ }, 6};
2. Use Functions for Dynamic Content
TypeScript1// ✅ Good 2greeting: (name: string) => `Hello, ${name}!`, 3 4// ❌ Bad (not reactive to params) 5greeting: 'Hello, {name}!',
3. Provide Type Safety
TypeScript1// Export dictionary type 2export type AppDictionary = typeof en; 3 4// Use in components 5const { t } = useI18n<AppDictionary>();
4. Handle Loading States
TSX1<Suspense fallback={<LoadingSpinner />}> 2 <Show when={translations()} fallback={<div>Loading...</div>}> 3 <App /> 4 </Show> 5</Suspense>
Testing i18n
TypeScript1// src/i18n/__tests__/translations.test.ts 2 3import { describe, it, expect } from 'vitest'; 4import { createRoot } from 'solid-js'; 5import { createAppI18n } from '../config'; 6 7describe('i18n', () => { 8 it('translates simple strings', () => { 9 createRoot((dispose) => { 10 const { t } = createAppI18n(); 11 12 expect(t('hello')).toBe('Hello'); 13 14 dispose(); 15 }); 16 }); 17 18 it('switches locale reactively', () => { 19 createRoot((dispose) => { 20 const { t, setLocale } = createAppI18n(); 21 22 expect(t('hello')).toBe('Hello'); 23 24 setLocale('es'); 25 expect(t('hello')).toBe('Hola'); 26 27 dispose(); 28 }); 29 }); 30});
Production Deployment
Build Configuration
TypeScript1// vite.config.ts 2 3export default defineConfig({ 4 plugins: [solid()], 5 build: { 6 rollupOptions: { 7 output: { 8 manualChunks: { 9 'i18n-en': ['./src/i18n/locales/en'], 10 'i18n-es': ['./src/i18n/locales/es'], 11 'i18n-fr': ['./src/i18n/locales/fr'], 12 }, 13 }, 14 }, 15 }, 16});
Pre-Deployment Checklist
- All locales have complete translations
- Type definitions match dictionary structure
- SSR locale detection working
- Translation bundles code-split
- Fallback locale configured
- Loading states handled
- IntlPull sync tested
Frequently Asked Questions
Q: How does SolidJS i18n differ from React i18next?
A: SolidJS uses fine-grained reactivity to update only translated text nodes, not entire components. This eliminates re-renders and makes locale switching instant. React i18next triggers component re-renders when locale changes.
Q: Can I use react-i18next with SolidJS?
A: No, react-i18next depends on React hooks and context. Use @solid-primitives/i18n designed for SolidJS's reactivity model.
Q: How do I handle pluralization?
A: Use Intl.PluralRules API with template functions. SolidJS memos ensure plural logic only recomputes when count or locale changes.
Q: Does SolidJS i18n support SSR?
A: Yes, with SolidStart. Detect locale from headers on server, hydrate client with same locale, and sync changes via signals.
Q: How do I lazy load translations?
A: Use createResource with dynamic imports. Wrap app in Suspense to handle loading states. SolidJS code-splits automatically.
Q: Can I integrate with translation management tools?
A: Yes, IntlPull provides API endpoints for fetching translations. Use createResource to load them reactively.
Q: How do I type-check translation keys?
A: Export dictionary type, create nested key type helper, and constrain t() function parameter. TypeScript autocompletes valid keys.
Q: What's the performance impact?
A: Minimal. Fine-grained reactivity means only text nodes update. No virtual DOM diffing. Locale switching is nearly instant even with thousands of strings.
IntlPull enhances SolidJS applications with collaborative translation workflows, dynamic translation loading via API, and version-controlled translation management. Leverage SolidJS's reactive primitives for performant internationalization, integrate IntlPull for team collaboration, and deliver native experiences to global users with zero rendering overhead.
