GitHub

Ship in any language

Add translations to your Svelte 5 app in under a minute.

pnpm add @nomideusz/svelte-i18n

Live preview

current: en
myapp.com
Welcome back, Jan!
Runes-native

Built on $state. Reactivity just works.

Tiny

Under 150 lines of TypeScript, zero runtime deps.

Flat keys

Simple JSON files your translators will love.

You have 7 new notifications.
Total: 129 PLN
Try it:

1. i18n.t()

lookup · interpolation · missing-key fallback
Result (current locale: en) Welcome back, Jan!
Placeholders the translation needs
name

1½. Plurals — t(key, { count })

Intl.PluralRules picks key_one / _few / _many / _other — an exact key always wins
t('school', { count: 5 }) — locale en 5 schools CLDR category: other — English never needs few; Polish and Ukrainian live on it (2–4, except 12–14).

2. interpolate()

standalone {variable} substitution
Result Hello Jan, you have 3 unread messages.
Placeholders detected
namecount

3. <LocaleSwitcher />

drop-in <select> — style via --asini-* CSS vars · every switch writes the locale cookie
With labels
Without labels (code only)
Translated accessible name (label)
<script>
  import { LocaleSwitcher } from '@nomideusz/svelte-i18n';
  import { i18n } from '#lib/i18n';
</script>

<LocaleSwitcher {i18n} labels={{ en: 'English', pl: 'Polski' }} />

4. URL routing (SSR)

path-prefix locales — default en bare; uk aliased to /ua (hreflang stays uk) · only the path is prefixed, query and hash ride along
extractLocale → locale en
extractLocale → pathname /blog/yoga-for-beginners?utm_source=demo
localizeHref per locale
en: /blog/yoga-for-beginners?utm_source=demopl: /pl/blog/yoga-for-beginners?utm_source=demode: /de/blog/yoga-for-beginners?utm_source=demouk: /ua/blog/yoga-for-beginners?utm_source=demo
alternates() → <svelte:head> markup
<link rel="alternate" hreflang="en" href="https://example.com/blog/yoga-for-beginners?utm_source=demo" />
<link rel="alternate" hreflang="pl" href="https://example.com/pl/blog/yoga-for-beginners?utm_source=demo" />
<link rel="alternate" hreflang="de" href="https://example.com/de/blog/yoga-for-beginners?utm_source=demo" />
<link rel="alternate" hreflang="uk" href="https://example.com/ua/blog/yoga-for-beginners?utm_source=demo" />
<link rel="alternate" hreflang="x-default" href="https://example.com/blog/yoga-for-beginners?utm_source=demo" />

5. formatDateTime() · formatMoney() · intlLocale()

pure & store-free — safe in load, hooks & emails · one regional policy (en → en-GB)
Per locale — intlLocale · formatDateTime · formatMoney
en → en-GB Monday, 15 June 2026 at 20:00 PLN 90.00
en-US → en-US Monday, June 15, 2026 at 08:00 PM PLN 90.00
pl → pl-PL poniedziałek, 15 czerwca 2026 20:00 90,00 zł
de → de Montag, 15. Juni 2026 um 20:00 90,00 PLN
uk → uk-UA понеділок, 15 червня 2026 р. о 20:00 90,00 PLN
import { formatDateTime, formatMoney, intlLocale } from '@nomideusz/svelte-i18n';

formatDateTime(date, 'en', 'Europe/Warsaw'); // "Monday, 15 June 2026 at 18:00"
formatMoney(9000, 'PLN', 'pl');              // "90,00 zł" — integer minor units in
formatMoney(9000, 'JPY', 'en');              // "JP¥9,000" — JPY has no minor unit
intlLocale('en');                            // "en-GB"

Read the full docs for the complete API.