SvelteKit i18n: Internationalization Setup Guide

From zero to multilingual: set up svelte-i18n in your SvelteKit app with ICU message format, locale-based routing, and smart fallback chains.

1

Install svelte-i18n

svelte-i18n is the standard internationalization library for Svelte and SvelteKit. It provides reactive stores, ICU MessageFormat support, and lazy locale loading out of the box.

svelte-i18n uses ICU MessageFormat for plurals and variables — the same standard used by FormatJS/react-intl. If you're coming from React, the message syntax will be familiar.
Terminal
npm install svelte-i18n
2

Configure svelte-i18n

Create an i18n configuration file that registers your locales with lazy-loaded import functions. svelte-i18n will only fetch a locale's messages when that locale is activated.

src/lib/i18n.ts
// src/lib/i18n.ts
import { register, init, getLocaleFromNavigator } from 'svelte-i18n';

// Register locale loaders (lazy-loaded)
register('en', () => import('../locales/en.json'));
register('de', () => import('../locales/de.json'));
register('ja', () => import('../locales/ja.json'));
register('es', () => import('../locales/es.json'));

init({
  fallbackLocale: 'en',
  initialLocale: getLocaleFromNavigator(),  // Auto-detect browser language
});
You must import your i18n config file in +layout.svelte before any component renders. If translations show raw keys like 'nav.home', the config was not imported early enough.

SvelteKit Layout Integration

Import your i18n config in the root layout and guard rendering with the $isLoading store. This prevents a flash of untranslated keys while locale data loads asynchronously.

src/routes/+layout.svelte
<!-- src/routes/+layout.svelte -->
<script>
  // Import i18n config — must run before any component renders
  import '../lib/i18n';
  import { isLoading } from 'svelte-i18n';
</script>

{#if $isLoading}
  <p>Loading translations...</p>
{:else}
  <slot />
{/if}

Locale-Based Routing in SvelteKit

For SEO-friendly URLs like /en/about and /de/about, use a [lang] route parameter. Set the svelte-i18n locale in the layout's load function based on the URL parameter.

SvelteKit locale routing
// src/routes/[lang]/+layout.ts
import { locale } from 'svelte-i18n';

export function load({ params }) {
  // Set the active locale from the URL parameter
  locale.set(params.lang);
  return {};
}

// src/routes/[lang]/+layout.svelte
<script>
  import '../../lib/i18n';
  import { isLoading } from 'svelte-i18n';
</script>

{#if $isLoading}
  <p>Loading...</p>
{:else}
  <slot />
{/if}

Translation File Format

Create one JSON file per locale. svelte-i18n supports nested keys and ICU MessageFormat syntax for plurals, variables, and select expressions.

Translation files
// src/locales/en.json
{
  "nav": {
    "home": "Home",
    "about": "About",
    "settings": "Settings"
  },
  "greeting": "Hello, {name}!",
  "cart": {
    "itemCount": "{count, plural, one {# item} other {# items}}"
  }
}

// src/locales/de.json
{
  "nav": {
    "home": "Startseite",
    "about": "Uber uns",
    "settings": "Einstellungen"
  },
  "greeting": "Hallo, {name}!",
  "cart": {
    "itemCount": "{count, plural, one {# Artikel} other {# Artikel}}"
  }
}
Name keys by what they describe, not where they appear: 'cart.itemCount' is better than 'homepageCartLabel'. Keys should survive UI redesigns.
3

Use Translations in Components

Import the $_ store (or $format) from svelte-i18n and use it in your Svelte templates. The store is reactive — when the locale changes, all translated strings update automatically.

Component.svelte
<script>
  import { _ } from 'svelte-i18n';
</script>

<h1>{$_('greeting', { values: { name: 'World' } })}</h1>

<nav>
  <a href="/">{$_('nav.home')}</a>
  <a href="/about">{$_('nav.about')}</a>
</nav>
Formatting helpers
<script>
  import { _, date, number, time } from 'svelte-i18n';
</script>

<!-- Simple string -->
<p>{$_('greeting', { values: { name: userName } })}</p>

<!-- ICU plurals — handled automatically -->
<p>{$_('cart.itemCount', { values: { count: 3 } })}</p>

<!-- Date formatting -->
<p>{$date(new Date(), { format: 'long' })}</p>

<!-- Number formatting -->
<p>{$number(1999.99, { style: 'currency', currency: 'USD' })}</p>
$_ is a Svelte store — you must use the $ prefix in templates. Writing _('key') without the dollar sign returns the store object, not the translated string.

Language Switching

Build a language selector that binds to the $locale store. When the value changes, svelte-i18n loads the new locale's messages and reactively updates all translated strings.

LanguageSwitcher.svelte
<script>
  import { locale, locales } from 'svelte-i18n';

  const LANGUAGE_NAMES = {
    en: 'English',
    de: 'Deutsch',
    ja: '日本語',
    es: 'Espanol',
  };
</script>

<select bind:value={$locale}>
  {#each $locales as loc}
    <option value={loc}>{LANGUAGE_NAMES[loc] ?? loc}</option>
  {/each}
</select>
4

Handle Plurals with ICU MessageFormat

svelte-i18n uses ICU MessageFormat for pluralization — the international standard that handles all CLDR plural categories. Arabic has 6 forms, Russian has 4, Japanese has 1. Define the forms your target languages need and svelte-i18n selects the correct one automatically.

ICU plural forms by language
// svelte-i18n uses ICU MessageFormat for plurals
// English:
{
  "items": "{count, plural, one {# item} other {# items}}"
}

// Arabic (6 forms):
{
  "items": "{count, plural, zero {no items} one {item} two {two items} few {# items} many {# items} other {# items}}"
}

// Japanese (1 form):
{
  "items": "{count, plural, other {#個のアイテム}}"
}
Never hardcode singular/plural logic in your components. Languages like French treat 0 as singular. Arabic, Russian, and Polish have plural forms that English does not have. Let the ICU plural syntax handle it.

Smart Locale Fallbacks with svelte-i18n-locale-chain

svelte-i18n falls back directly to fallbackLocale when a key is missing — there is no intermediate fallback. A pt-BR user sees English instead of perfectly good pt-PT translations. svelte-i18n-locale-chain fixes this with smart fallback chains that deep-merge messages from regional variants.

Terminal
npm install svelte-i18n-locale-chain svelte-i18n
src/lib/i18n.ts
// src/lib/i18n.ts
import { initLocaleChain, setLocale } from 'svelte-i18n-locale-chain';

// Replace svelte-i18n's init + register with initLocaleChain
await initLocaleChain({
  loadMessages: (locale) =>
    import(`../locales/${locale}.json`).then(m => m.default),
  defaultLocale: 'en',
  initialLocale: 'pt-BR',
});

// Later, to change locale:
await setLocale('fr-CA');
// fr-CA user sees: fr-CA messages -> fr messages -> en messages
// No missing keys — deep-merged automatically
svelte-i18n-locale-chain manages all message loading internally. Do not use svelte-i18n's register() function alongside it — initLocaleChain handles registration, loading, and deep-merging.

Automate Translations

With your i18n setup complete, translate your locale files using AI. In your IDE, ask your AI assistant to translate your source file, or use the i18n Agent CLI in your CI/CD pipeline.

Terminal
# In your IDE, ask your AI assistant:
> Translate src/locales/en.json to German, Japanese, and Spanish

✓ de.json created (1.2s)
✓ ja.json created (1.5s)
✓ es.json created (1.1s)

# Or use the CLI in CI/CD:
npx i18n-agent translate src/locales/en.json --lang de,ja,es
Translate incrementally — when you add new keys to your source file, translate just the diff rather than regenerating all files. This preserves any human-reviewed translations.

Automate Translation Quality

Catch missing keys and broken placeholders before they ship with i18n-validate. Test your UI with pseudo-translations using i18n-pseudo before real translations arrive.

Common Pitfalls

SSR Locale Bleed in SvelteKit

svelte-i18n stores are singletons. In SvelteKit SSR, concurrent requests share the same store — one user's locale can bleed into another's response. Fix: call locale.set() in the handle hook or layout load function so each request gets the correct locale context.

Using register() with svelte-i18n-locale-chain

Do not use svelte-i18n's register() function if you are using svelte-i18n-locale-chain. initLocaleChain handles all message loading internally. Mixing both causes duplicate or conflicting message loading.

ICU Syntax Errors Fail Silently

A mismatched brace or missing plural category in ICU MessageFormat strings causes silent failures — the raw message string is displayed instead of the formatted output. Validate ICU syntax in your CI pipeline.

Flash of Untranslated Content

If you render components before translations finish loading, users see raw keys. Guard your layout with {#if $isLoading}...{:else}...{/if} to show a loading state until messages are ready.

Recommended File Structure

Project Structure
my-sveltekit-app/
├── src/
│   ├── lib/
│   │   └── i18n.ts              # i18n configuration
│   ├── locales/
│   │   ├── en.json              # Source language
│   │   ├── de.json              # German
│   │   ├── ja.json              # Japanese
│   │   └── es.json              # Spanish
│   └── routes/
│       ├── +layout.svelte       # Import i18n, guard isLoading
│       ├── +page.svelte
│       └── [lang]/              # Optional: locale-based routing
│           ├── +layout.ts       # Set locale from URL param
│           ├── +layout.svelte
│           └── +page.svelte
├── svelte.config.js
└── package.json

Try i18n Agent Now

Drop your translation file here

JSON, YAML, PO, XML, CSV, Markdown, Properties

or click to browse

Target languages

No signup requiredInstant estimate

Frequently Asked Questions