svelte-locale
v2.1.1
Published
A SvelteKit-native i18n library for Svelte 5. Zero dependencies.
Maintainers
Readme
svelte-locale
A SvelteKit-native i18n library for Svelte 5. Zero dependencies.
Features
i18n.t()— string translations with{variable}interpolationi18n.plural()— count-based pluralisation viaIntl.PluralRulesi18n.fn()— logic-based translations returning arbitrary values<I18n>— rich component translations with server-side rendering (uses JavaScript for locale switching)<LocaleLink>— locale-aware anchor tags<LocaleSwitcher>— accessible language switcher with custom snippet support<HreflangLinks>— SEO<link rel="alternate">tags- Intl formatters for numbers, currencies, dates, and relative time
- ISO 8601 date formatters (
formatDateISO,formatDateTimeISO) - BCP 47 / ISO 639 regional locale support (
en-US,pt-BR,zh-CN, etc.) - Server locale detection: URL prefix → cookie →
Accept-Language(full BCP 47) → default - RTL support via
<html dir>(auto-detected from locale registry) - Zero flicker — locale is resolved server-side before any HTML is sent
Comparison
| Feature | svelte-locale | Paraglide | typesafe-i18n | svelte-i18n | |---------|---------------|-----------|---------------|-------------| | Zero direct dependencies | ✓ | ✗ | ✓ | ✗ | | Server-first / zero flicker | ✓ | ✓ | △ | △ | | Svelte 5 runes-friendly | ✓ | ✓ | △ | △ | | SvelteKit-native scaffolding | ✓ | ✓ | △ | ✗ | | Plural rules via Intl | ✓ | ✓ | ✓ | ✓ | | Logic-based fn() translations | ✓ | △ | △ | ✗ | | Rich component <I18n> | ✓ | △ | ✗ | ✗ | | Rich text / component interpolation | ✓ | ✓ | ✗ | △ | | Intl formatter helpers | ✓ | ✓ | ✓ | ✗ | | ISO 8601 formatters | ✓ | ✗ | ✗ | ✗ | | BCP 47 / regional locale support | ✓ | ✓ | ✗ | ✗ | | RTL via <html dir> | ✓ | ✓ | ✗ | ✗ | | <LocaleSwitcher> component | ✓ | ✗ | ✗ | ✗ | | <HreflangLinks> SEO component | ✓ | △ | ✗ | ✗ | | URL prefix routing | ✓ | ✓ | ✗ | ✗ | | Type-safe keys and params | ✓ | ✓ | ✓ | ✗ |
Legend: ✓ Built in / first-class | △ Partial / manual / different API | ✗ Not built in
Installation
npx svelte-locale initThat's it. The init command installs the package and writes minimal boilerplate into your project:
src/app.html— patches%lang%and%dir%placeholders onto<html>src/app.d.ts— typesApp.Locals.localesrc/hooks.server.ts— wires uphandleI18n()src/lib/i18n.ts— configuration file with your localesvite.config.ts— patches in therichI18n()Vite plugin
Existing files are never overwritten — the command skips them and tells you.
Then edit src/lib/i18n.ts to configure your locales and create language data files. You'll need to set up your layout files manually to pass the locale to the client and initialize it.
Manual Setup
If you prefer to wire things up yourself, first install the package:
npm install svelte-localeThen create these files:
src/lib/i18n.ts
import { defineConfig } from 'svelte-locale';
export default defineConfig({
locales: ['en-US', 'sv-SE'],
defaultLocale: 'en-US',
fallbackLocale: 'en-US',
cookieName: 'locale',
routing: {
strategy: 'none',
prefixDefaultLocale: false
},
detection: ['url', 'cookie', 'accept-language', 'default']
});vite.config.ts
Add richI18n before sveltekit(). The plugin auto-discovers your i18n.ts config.
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { richI18n } from 'svelte-locale/vite';
export default defineConfig({
plugins: [
richI18n(),
sveltekit()
]
});src/app.html
<html lang="%lang%" dir="%dir%">src/app.d.ts
declare global {
namespace App {
interface Locals {
locale: import('svelte-locale').Locale;
}
}
}
export {};src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { handleI18n } from 'svelte-locale/server';
export const handle: Handle = handleI18n();Defining Language Data
Create language data files anywhere in your project and register them via the i18n namespace:
// src/lib/i18n/messages.ts
import i18n from 'svelte-locale';
i18n.defineMessages({
'en-US': {
'common.save': 'Save',
'common.cancel': 'Cancel'
},
'sv-SE': {
'common.save': 'Spara',
'common.cancel': 'Avbryt'
}
});// src/lib/i18n/plurals.ts
import i18n from 'svelte-locale';
i18n.definePlurals({
'en-US': {
'items.count': { one: '{count} item', other: '{count} items' }
},
'sv-SE': {
'items.count': { one: '{count} sak', other: '{count} saker' }
}
});// src/lib/i18n/functions.ts
import i18n from 'svelte-locale';
i18n.defineFunctions({
'en-US': {
'format.currency': (amount: number) => `$${amount.toFixed(2)}`
},
'sv-SE': {
'format.currency': (amount: number) => `${amount.toFixed(2)} kr`
}
});Note: defineMessages, definePlurals, and defineFunctions can be called anywhere and as many times as you want — they merge into the registry.
The library ships with a default config.ts baked into the library itself. To customise it, create your own src/lib/i18n.ts with defineConfig(). The current defaults are:
export const i18nConfig = {
locales: ['en-US', 'sv-SE'] as const,
defaultLocale: 'en-US',
fallbackLocale: 'en-US',
cookieName: 'locale',
routing: {
strategy: 'none', // 'prefix' | 'prefix-non-default' | 'none'
prefixDefaultLocale: false
},
detection: ['url', 'cookie', 'accept-language', 'default']
} as const;locales — all supported locale codes.
defaultLocale — used when no locale can be detected.
fallbackLocale — used when a translation key is missing for the active locale.
cookieName — the cookie used to persist locale preference across requests.
routing.strategy
'none'— no URL prefix, locale is cookie/header only.'prefix-non-default'— non-default locales get a prefix (e.g./sv/settings), default locale uses clean paths.'prefix'— all locales get a prefix including the default.
detection — order of locale detection sources. 'url' checks the path prefix; 'cookie' reads the cookie; 'accept-language' parses the request header; 'default' falls back to defaultLocale.
Translation Files
Create these anywhere in your project (e.g. src/lib/i18n/). They register data into the shared runtime registry via side-effects.
Messages — t()
import { defineMessages } from 'svelte-locale';
defineMessages({
'en-US': {
'common.save': 'Save',
'common.cancel': 'Cancel',
'auth.welcome': 'Welcome back, {name}'
},
'sv-SE': {
'common.save': 'Spara',
'common.cancel': 'Avbryt',
'auth.welcome': 'Välkommen tillbaka, {name}'
}
});Use {variableName} syntax for interpolation. Missing variables produce a warning in dev and are left as-is in the output.
Plurals — plural()
import { definePlurals } from 'svelte-locale';
definePlurals({
'en-US': {
'tickets.count': {
one: '{count} ticket',
other: '{count} tickets'
}
},
'sv-SE': {
'tickets.count': {
one: '{count} ärende',
other: '{count} ärenden'
}
}
});Each entry can have keys: one, other, zero, two, few, many. Which categories apply depends on the locale — they match the categories returned by Intl.PluralRules. other is always required as a fallback.
Functions — fn()
For translations that require logic — formatting, conditionals, composing strings from data.
import { defineFunctions, createFn } from 'svelte-locale';
export type AppFunctions = {
'billing.status': (input: { status: 'paid' | 'pending' | 'overdue'; days?: number }) => string;
'audit.event': (input: { actor: string; action: string; target: string }) => string;
};
defineFunctions({
'en-US': {
'billing.status': ({ status, days }) => {
if (status === 'paid') return 'Invoice is paid.';
if (status === 'overdue') return `Invoice is overdue by ${days ?? 0} days.`;
return 'Invoice is pending.';
},
'audit.event': ({ actor, action, target }) => `${actor} ${action} ${target}.`
},
'sv-SE': {
'billing.status': ({ status, days }) => {
if (status === 'paid') return 'Fakturan är betald.';
if (status === 'overdue') return `Fakturan är ${days ?? 0} dagar sen.`;
return 'Fakturan väntar på betalning.';
},
'audit.event': ({ actor, action, target }) => `${actor} ${action} ${target}.`
}
});
// Export a typed fn() bound to your function map
export const fn = createFn<AppFunctions>();Import and use fn from your own file — not from svelte-locale directly — to get full type inference on keys and arguments.
API
t(key, values?)
Translate a string key for the current locale. Falls back to fallbackLocale if missing, then returns the key itself.
import { t } from 'svelte-locale';
t('common.save') // → 'Save'
t('auth.welcome', { name: 'Vincent' }) // → 'Welcome back, Vincent'tForLocale(locale, key, values?)
Same as t() but for a specific locale. Useful in server-side load functions.
import { tForLocale } from 'svelte-locale';
tForLocale('sv', 'common.save') // → 'Spara'plural(key, count, values?)
Resolve a plural string using Intl.PluralRules for the current locale. {count} is automatically available as an interpolation variable.
import { plural } from 'svelte-locale';
plural('tickets.count', 1) // → '1 ticket'
plural('tickets.count', 5) // → '5 tickets'pluralForLocale(locale, key, count, values?)
Same as plural() but for a specific locale.
fn(key, input) / createFn<M>()
fn from svelte-locale is untyped. Use createFn<YourFunctionMap>() to get a typed version bound to your own function definitions.
// src/lib/i18n/functions.ts
export const fn = createFn<AppFunctions>();
// In a component
import { fn } from '$lib/i18n/functions';
fn('billing.status', { status: 'overdue', days: 3 }) // fully typedfnForLocale(locale, key, ...args)
Untyped fn call for a specific locale. Useful server-side.
getLocale()
Returns the currently active locale as a reactive value (Svelte 5 $state under the hood). Safe to call in $derived and templates.
import { getLocale } from 'svelte-locale';
const locale = getLocale(); // 'en' | 'sv'setLocale(locale, options?)
Change the active locale at runtime.
- Updates the reactive locale state
- Sets
document.documentElement.langanddir - Writes the locale cookie (1 year,
SameSite=Lax) - If
navigate: trueand routing strategy is'none', callsinvalidateAll()to re-run load functions - If
navigate: trueand a prefix strategy is used, callsgoto()with the locale-swapped path (noScroll,keepFocus)
import { setLocale } from 'svelte-locale';
setLocale('sv', { navigate: true });initLocale(locale)
Set the locale from a server-resolved value. Call this in your root layout from $effect. No navigation side-effects.
defineLocale(code, definition)
Register a custom locale not in the built-in registry, or override an existing entry.
import { defineLocale } from 'svelte-locale';
defineLocale('tok', { name: 'Toki Pona', dir: 'ltr' });getLocaleName(locale)
Get the human-readable name of a locale from the built-in registry.
getLocaleName('sv') // → 'Svenska'
getLocaleName('ar') // → 'العربية'getLocaleDir(locale)
Get the text direction for a locale. Returns 'ltr' or 'rtl'.
getLocaleDir('ar') // → 'rtl'
getLocaleDir('en') // → 'ltr'getLocaleRegistry()
Returns a read-only snapshot of the full locale registry (all built-in + custom locales).
import { getLocaleRegistry } from 'svelte-locale';
const registry = getLocaleRegistry();
// { en: { name: 'English', dir: 'ltr' }, sv: { name: 'Svenska', dir: 'ltr' }, ... }Intl Formatters
All formatters read the current locale automatically. Formatters are cached per locale+options combination.
formatNumber(value, options?)
import { formatNumber } from 'svelte-locale';
formatNumber(1234567.89) // → '1,234,567.89' (en)
formatNumber(0.42, { style: 'percent' }) // → '42%'formatCurrency(value, currency?, options?)
import { formatCurrency } from 'svelte-locale';
formatCurrency(299, 'SEK') // → 'SEK 299.00' (en) / '299,00 kr' (sv)
formatCurrency(9.99, 'USD') // → '$9.99'formatDate(value, options?)
Accepts a Date, ISO string, or timestamp.
import { formatDate } from 'svelte-locale';
formatDate(new Date(), { dateStyle: 'long' }) // → 'July 3, 2026' (en)
formatDate('2026-01-01', { month: 'short', year: 'numeric' })formatRelativeTime(value, unit, options?)
import { formatRelativeTime } from 'svelte-locale';
formatRelativeTime(-2, 'day') // → '2 days ago' (en)
formatRelativeTime(1, 'hour') // → 'in 1 hour'formatDateISO(value)
Returns a date-only ISO 8601 string. Locale-independent.
import { formatDateISO } from 'svelte-locale';
formatDateISO(new Date()) // → '2026-07-03'
formatDateISO('2026-01-15T10:30Z') // → '2026-01-15'formatDateTimeISO(value)
Returns a full UTC ISO 8601 timestamp. Useful for <time datetime> attributes.
import { formatDateTimeISO } from 'svelte-locale';
formatDateTimeISO(new Date()) // → '2026-07-03T10:00:00.000Z'Routing Helpers
withLocale(path, locale?)
Prepend the locale prefix to a path based on the configured routing strategy. If locale is omitted, uses the current active locale.
import { withLocale } from 'svelte-locale';
// strategy: 'prefix-non-default', default: 'en'
withLocale('/settings') // → '/settings' (en, no prefix)
withLocale('/settings', 'sv') // → '/sv/settings'switchLocaleInPath(pathname, locale)
Swap the locale prefix in a path.
switchLocaleInPath('/sv/settings', 'en') // → '/settings'
switchLocaleInPath('/settings', 'sv') // → '/sv/settings'getLocaleFromPath(pathname)
Extract the locale from the first path segment. Returns defaultLocale if no locale prefix is found.
getLocaleFromPath('/sv/settings') // → 'sv'
getLocipeFromPath('/settings') // → 'en'stripLocalePrefix(pathname)
Remove the locale prefix from a path.
stripLocalePrefix('/sv/settings') // → '/settings'ISO & BCP 47 Locale Support
Regional locale codes
The library supports full BCP 47 locale tags. Use them anywhere you use short codes:
// vite.config.ts
richI18n({ locales: ['en-US', 'en-GB', 'pt-BR', 'zh-CN', 'zh-TW'] })// src/lib/i18n/messages.ts
defineMessages({
'en-US': { 'currency.symbol': '$' },
'en-GB': { 'currency.symbol': '£' },
'pt-BR': { 'greeting': 'Olá' }
});The registry ships with entries for all common regional variants. For any unregistered subtag the library automatically falls back to the base language for RTL detection — so ar-DZ correctly resolves as rtl via the ar base entry.
Built-in regional variants
| Tag | Name |
|-----|------|
| en-US | English (US) |
| en-GB | English (UK) |
| en-AU | English (Australia) |
| en-CA | English (Canada) |
| fr-FR / fr-CA / fr-BE / fr-CH | French variants |
| de-DE / de-AT / de-CH | German variants |
| es-ES / es-MX / es-AR / es-CO | Spanish variants |
| pt-PT / pt-BR | Portuguese variants |
| zh-CN / zh-TW / zh-HK | Chinese variants |
| ar-SA / ar-EG | Arabic variants (RTL) |
| nl-NL / nl-BE | Dutch variants |
| it-IT / it-CH | Italian variants |
| sv-SE / nb-NO / nn-NO / fi-FI / da-DK | Nordic |
| ru-RU / uk-UA / pl-PL | Eastern European |
| ja-JP / ko-KR / hi-IN | Asian |
Add any extra variant with defineLocale:
import { defineLocale } from 'svelte-locale';
defineLocale('es-CL', { name: 'Español (Chile)', dir: 'ltr' });Locale utilities
import { getBaseLocale, normalizeLocale, getLocaleDir, getLocaleName } from 'svelte-locale';
getBaseLocale('en-US') // → 'en'
normalizeLocale('pt-br') // → 'pt-BR'
getLocaleDir('ar-SA') // → 'rtl'
getLocaleDir('ar-DZ') // → 'rtl' (falls back to 'ar')
getLocaleName('pt-BR') // → 'Português (Brasil)'Accept-Language detection
The server-side locale detector now matches full BCP 47 tags before falling back to the base language:
Accept-Language: pt-BR, pt;q=0.9, en;q=0.8→ resolves topt-BRif configured, thenpt, thenen- Works automatically — no extra configuration needed.
API Usage
All i18n functions can be accessed in two ways:
Namespace Object
Import the namespace and access all functions through it:
import i18n from 'svelte-locale';
i18n.t('key', { name: 'World' });
i18n.plural('items', 5);
i18n.formatNumber(1234.56);
i18n.setLocale('en-US');Direct Imports
Import individual functions directly:
import { t, plural, formatNumber, setLocale } from 'svelte-locale';
t('key', { name: 'World' });
plural('items', 5);
formatNumber(1234.56);
setLocale('en-US');Both approaches work identically. Choose whichever style you prefer.
Components
<I18n>
Renders only the content block matching the active locale. Use lang attributes on child elements to specify locale-specific content.
<I18n key="home.hero">
<div lang="en">
<h1>Support made simple</h1>
</div>
<div lang="sv">
<h1>Support gjort enkelt</h1>
</div>
</I18n>Props:
key— identifier used in dev warnings (does not look up registry)fallback?— locale to render if the active locale block is missing (defaults tofallbackLocale)as?— wrap the output in an HTML element ('section','div', etc.)class?/style?— passed to the wrapper element (requiresas)
Each direct child must have a lang attribute matching a configured locale. Any valid Svelte content is allowed inside — components, {#if}, {#each}, slots, etc.
The Vite plugin (richI18n) transforms the lang-attributed children into Svelte snippets at compile time. Only the active locale's snippet is rendered, ensuring proper server-side rendering without flash. Missing or duplicate lang values produce warnings during development.
<LocaleSwitcher>
Renders a button per configured locale. The active locale button has aria-pressed="true". Clicking switches the locale and navigates if needed.
<LocaleSwitcher />Default styling props:
class?/style?— applied to the wrapping<div role="group">buttonClass?/buttonStyle?— applied to each default button
Custom button via snippet:
Replace the default button entirely. The snippet receives { locale, active, label, select }.
<LocaleSwitcher>
{#snippet button({ locale, active, label, select })}
<button
onclick={select}
aria-pressed={active}
style="font-weight: {active ? 'bold' : 'normal'}"
>
{locale.toUpperCase()} — {label}
</button>
{/snippet}
</LocaleSwitcher><LocaleLink>
A locale-aware anchor. Automatically prepends the correct locale prefix based on the routing strategy. Accepts all standard <a> attributes.
<LocaleLink href="/settings">Settings</LocaleLink>
<!-- Force a specific locale -->
<LocaleLink href="/settings" locale="sv">Svenska inställningar</LocaleLink><HreflangLinks>
Injects <link rel="alternate" hreflang="..."> tags into <head> for SEO. Place it in your root layout.
<script lang="ts">
import { page } from '$app/stores';
import { HreflangLinks } from 'svelte-locale/svelte';
</script>
<HreflangLinks pathname={$page.url.pathname} origin={$page.url.origin} />Props:
pathname— current page path (required)origin?— base URL for absolute hrefs (e.g.https://example.com)xDefault?— includehreflang="x-default"pointing to the default locale (default:true)extraLocales?— additional{ hreflang, locale }entries for region variants (e.g.en-GB)
Server API
handleI18n()
Returns a SvelteKit Handle function. Does the following on every request:
- Detects locale (URL → cookie →
Accept-Language→ default) - Sets
event.locals.locale - Handles routing redirects for prefix strategies
- Writes the locale cookie (1 year)
- Replaces
%lang%and%dir%in the HTML viatransformPageChunk - Sets
Content-Languageresponse header
import { handleI18n } from 'svelte-locale/server';
export const handle: Handle = handleI18n();detectLocale(event)
Runs just the detection logic without the full handle middleware. Useful if you need to compose handle manually.
import { detectLocale } from 'svelte-locale/server';
const locale = detectLocale(event); // 'en' | 'sv' | ...Routing Strategies
| Strategy | /settings | /sv/settings | /en/settings |
|---|---|---|---|
| none | ✓ (en) | — | — |
| prefix-non-default | ✓ (en) | ✓ (sv) | redirects → /settings |
| prefix | redirects → /en/settings | ✓ (sv) | ✓ (en) |
Built-in Locale Registry
The library includes display names and text directions for 80+ languages out of the box (af, ar, bg, cs, da, de, el, en, es, et, fa, fi, fr, he, hi, hr, hu, id, it, ja, ko, lt, lv, mk, ms, nl, nb, nn, pl, pt, ro, ru, sk, sl, sq, sr, sv, th, tr, uk, ur, vi, zh, and more).
RTL locales include ar, he, fa, ku, ur, yi.
Use defineLocale() to add or override any entry.
Vite Plugin
richI18n({ locales? }) — processes .svelte files at compile time.
- Finds
<I18n>components and transforms theirlang-attributed children into Svelte{#snippet}blocks - Generates proper source maps so stack traces point to the original source
- In development, warns about: invalid
langvalues, duplicatelangvalues, and missing locale blocks locales— array of valid locale codes for validation (should match youri18nConfig.locales)
Publishing a New Version
# Build and validate
npm run prepack
# Bump version (patch / minor / major), auto-commits + tags
npm version patch
# Push commits and tag
git push && git push --tags
# Publish to npm
npm publish --access public