IntlPull
Tutorial
11 min read

SolidJS i18n: Reactive Localization Guide for 2026

Master SolidJS internationalization with @solid-primitives/i18n, reactive translations, createI18nContext, dynamic locale switching, and SSR with SolidStart.

IntlPull Team
IntlPull Team
Feb 12, 2026
On this page
Summary

Master SolidJS internationalization with @solid-primitives/i18n, reactive translations, createI18nContext, dynamic locale switching, and SSR with SolidStart.

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:

JavaScript
1// 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:

Terminal
npm install @solid-primitives/i18n

Basic Configuration

TypeScript
1// 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

TSX
1// 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:

TSX
1// 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

TypeScript
1// 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:

TypeScript
1// 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:

TypeScript
1import { 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:

TypeScript
1// 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};
TypeScript
1// 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:

TypeScript
1// 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:

TSX
1const { 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:

TypeScript
1// 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:

TypeScript
1export 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:

TSX
1const { 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:

TypeScript
1// 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:

TSX
1// 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:

TypeScript
1export 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:

TypeScript
1// 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:

TypeScript
1// 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:

TypeScript
1export const createAppI18n = () => {
2  const [locale, setLocale] = createPersistedLocale('en');
3  // ... rest of setup
4};

Language Switcher Component

TSX
1// 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

TSX
1import { 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

TypeScript
1// 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

FeatureSolidJSReact
UpdatesFine-grained (only text nodes)Re-render component tree
PerformanceNo virtual DOM diffingVirtual DOM reconciliation
Locale switchInstantMay cause flicker
Bundle size~2KB (@solid-primitives/i18n)~10KB (react-i18next)
SSRBuilt-in with SolidStartRequires setup
Learning curveSimilar to React patternsFamiliar to React devs

Best Practices

1. Organize Translations by Feature

TypeScript
1const translations = {
2  common: { /* shared strings */ },
3  auth: { /* login, register */ },
4  products: { /* product-specific */ },
5  checkout: { /* checkout flow */ },
6};

2. Use Functions for Dynamic Content

TypeScript
1// ✅ Good
2greeting: (name: string) => `Hello, ${name}!`,
3
4// ❌ Bad (not reactive to params)
5greeting: 'Hello, {name}!',

3. Provide Type Safety

TypeScript
1// Export dictionary type
2export type AppDictionary = typeof en;
3
4// Use in components
5const { t } = useI18n<AppDictionary>();

4. Handle Loading States

TSX
1<Suspense fallback={<LoadingSpinner />}>
2  <Show when={translations()} fallback={<div>Loading...</div>}>
3    <App />
4  </Show>
5</Suspense>

Testing i18n

TypeScript
1// 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

TypeScript
1// 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.

Tags
solidjs
i18n
localization
reactive
javascript
frontend
IntlPull Team
IntlPull Team
Engineering

Building tools to help teams ship products globally. Follow us for more insights on localization and i18n.