Localize a Svelte / SvelteKit app: complete guide with Localingos

SvelteKit's load functions and per-request server state pair cleanly with i18n once you know the patterns. This guide takes a fresh SvelteKit app to fully localized using svelte-i18n for the runtime and Localingos for the translation pipeline. About 20 minutes end to end, including SSR-safe locale handling.

Step 1 — Install

npm install svelte-i18n
npm install -g @localingos/cli
localingos login

Step 2 — Wire svelte-i18n

src/lib/i18n/index.ts:

import { init, register, locale } from 'svelte-i18n';
import { browser } from '$app/environment';

export const SUPPORTED = ['en', 'es', 'de', 'fr', 'ja', 'pt-BR', 'zh-CN', 'ko'] as const;

register('en', () => import('./locales/en.json'));
register('es', () => import('./locales/es.json'));
register('de', () => import('./locales/de.json'));
register('fr', () => import('./locales/fr.json'));

const initialLocale = browser
  ? (localStorage.getItem('locale') || window.navigator.language.split('-')[0])
  : 'en';

init({
  fallbackLocale: 'en',
  initialLocale,
});

src/routes/+layout.svelte:

<script lang="ts">
  import '$lib/i18n';
  import { isLoading } from 'svelte-i18n';
</script>

{#if !$isLoading}
  <slot />
{:else}
  <div>Loading…</div>
{/if}

register() with a lazy import means each locale becomes its own chunk — only the active locale ships in the initial bundle.

Step 3 — Source of truth

src/lib/i18n/locales/en.json:

{
  "welcome": "Welcome, {name}",
  "cart": "{count, plural, one {# item in your cart} other {# items in your cart}}"
}

svelte-i18n uses ICU MessageFormat syntax for plurals — works in every CLDR locale automatically.

Step 4 — Configure Localingos

localingos.json:

{
  "projectId": "your-project-id",
  "source": { "path": "src/lib/i18n/locales/en.json", "locale": "en" },
  "targets": {
    "path": "src/lib/i18n/locales/{locale}.json",
    "locales": ["es", "de", "fr", "ja", "pt-BR", "zh-CN", "ko"]
  },
  "placeholders": ["{name}", "{count}"],
  "format": "icu"
}

format: "icu" tells Localingos to preserve ICU MessageFormat syntax exactly when translating plurals.

localingos sync

Step 5 — Use in components

<script lang="ts">
  import { _ } from 'svelte-i18n';
  export let userName: string;
  export let itemCount: number;
</script>

<header>
  <h1>{$_('welcome', { values: { name: userName } })}</h1>
  <p>{$_('cart', { values: { count: itemCount } })}</p>
</header>

$_ is the reactive store version — components automatically re-render when the locale changes.

Step 6 — Language switcher

<script lang="ts">
  import { locale } from 'svelte-i18n';

  const LOCALES = { en: 'English', es: 'Español', de: 'Deutsch', fr: 'Français' };

  function switchLocale(newLocale: string) {
    locale.set(newLocale);
    localStorage.setItem('locale', newLocale);
  }
</script>

<select value={$locale} on:change={e => switchLocale((e.target as HTMLSelectElement).value)}>
  {#each Object.entries(LOCALES) as [code, label]}
    <option value={code}>{label}</option>
  {/each}
</select>

Step 7 — SSR considerations

SvelteKit SSR runs in Node, so localStorage is unavailable. Detect the user's locale from the Accept-Language header in a +layout.server.ts:

// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = ({ request }) => {
  const header = request.headers.get('accept-language') || '';
  const preferred = header.split(',')[0]?.split(';')[0]?.split('-')[0] || 'en';
  return { initialLocale: preferred };
};

Pass it through +layout.svelte to init({ initialLocale: data.initialLocale }). This avoids hydration mismatches between SSR-rendered HTML and client-side hydration.

Step 8 — Automate sync in CI

# .github/workflows/i18n.yml
name: i18n-sync
on:
  push: { branches: [main], paths: ['src/lib/i18n/locales/en.json'] }
jobs:
  sync:
    runs-on: ubuntu-latest
    permissions: { contents: write, pull-requests: write }
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm install -g @localingos/cli
      - run: localingos sync
        env: { LOCALINGOS_API_KEY: '${{ secrets.LOCALINGOS_API_KEY }}' }
      - uses: peter-evans/create-pull-request@v6
        with:
          branch: i18n/auto-sync
          title: 'chore(i18n): sync translations'
          commit-message: 'chore(i18n): sync translations'

Production checklist

  • Locale-prefixed routes for SEO. Use SvelteKit's [lang] dynamic segment: src/routes/[lang]/pricing/+page.svelte. Emit hreflang annotations in +layout.svelte.
  • Adapter compatibility. Works with adapter-static, adapter-node, adapter-vercel, adapter-cloudflare. No special config needed.
  • RTL support. Set <html dir> based on the active locale in +layout.svelte.
  • ICU support is built in. svelte-i18n parses MessageFormat natively — no extra runtime.

Wrap up

Your SvelteKit app handles 56 locales with ICU pluralization, server-side locale detection, lazy-loaded chunks, and CI-driven translation sync. Adding a language is one entry in localingos.json and one register() call.

Free tier covers small to mid-size SvelteKit apps end to end.