Core i18n functionality for SvelteKit with support for custom message parsers. This package provides the foundation for sveltekit-i18n and can be used standalone when you need maximum flexibility with custom parsers.
Use this package if you:
- Need a custom message parser (like ICU, Fluent, or your own format)
- Want full control over message interpolation
- Are building a custom i18n solution
Use sveltekit-i18n if you:
- Want the quickest setup with sensible defaults
- Are happy with the default placeholder/modifier syntax
- Don't need custom parsers
✅ Svelte 5 runes – One reactive instance, no stores
✅ Framework ready – Full SSR and CSR support
✅ Parser-agnostic – Use any message syntax you need
✅ Custom data sources – Load translations from anywhere (files, APIs, databases)
✅ Module-based – Translations load only for visited pages
✅ Route-aware – Automatic loading based on SvelteKit routes
✅ Component-scoped – Multiple translation instances with custom definitions
✅ Extensible – Pipe the instance through extensions to reshape or augment its surface
✅ TypeScript – Locales inferred from your config, keys and payloads from a schema
✅ Zero dependencies – Lightweight and fast
Svelte 5 or newer, and one of Node 22+, Bun 1.2+ or Deno 2+. The package is
ESM-only and imports no node: module, so every runtime that runs your
SvelteKit build runs it.
npm install @sveltekit-i18n/base
# bun add @sveltekit-i18n/base
# deno add npm:@sveltekit-i18n/baseYou'll also need a parser:
# Choose one:
npm install @sveltekit-i18n/parser-curly
npm install @sveltekit-i18n/parser-icu
# or create your own// src/lib/translations/index.js
import { I18n } from '@sveltekit-i18n/base';
import parser from '@sveltekit-i18n/parser-curly';
/** @type {import('@sveltekit-i18n/base').Config.T} */
const config = {
parser: parser({ onReport: null, /* other parser options */ }),
loaders: [
{
locale: 'en',
key: 'common',
loader: async () => (await import('./en/common.json')).default,
},
{
locale: 'cs',
key: 'common',
loader: async () => (await import('./cs/common.json')).default,
},
],
};
// One reactive instance. Do NOT destructure its value properties — reading
// them off the instance is what makes templates reactive. (`t`/`l` are
// functions and stay reactive even when destructured, since the tracked reads
// happen at call time. In a component, `const { loading } = $derived(i18n)`
// destructures value reads without losing reactivity.)
export const i18n = new I18n(config);// src/routes/+layout.js
import { i18n } from '$lib/translations';
/** @type {import('./$types').LayoutLoad} */
export const load = async ({ url }) => {
const { pathname } = url;
const initLocale = 'en';
await i18n.loadTranslations(initLocale, pathname);
return {};
};Rendering per-visitor locales on the server? The instance above is a module-level singleton — on the server it is shared by every request in the process, so concurrent visitors overwrite each other's locale. Use one instance per request and hand its data to the client with
snapshot(): see Server-Side Rendering.
<script>
import { i18n } from '$lib/translations';
</script>
<p>{i18n.t('common.greeting', { name: 'World' })}</p>The call reads the reactive translation table and locale, so the text updates
automatically when either changes — no stores, no $ prefix.
import i18n from '@sveltekit-i18n/base';
import parser from '@sveltekit-i18n/parser-icu';
const config = {
parser: parser({ onReport: null }),
loaders: [/* ... */],
};{
"items": "You have {count, plural, =0 {no items} one {# item} other {# items}}."
}import i18n from '@sveltekit-i18n/base';
const customParser = () => ({
parse: (value, params) => {
// Your custom interpolation logic
return value.replace(/\{(\w+)\}/g, (_, key) => params[0]?.[key] ?? key);
},
});
const config = {
parser: customParser(),
loaders: [/* ... */],
};Learn more about creating custom parsers.
Message parser instance. See Parsers.
Array of loader configurations:
loaders: [
{
locale: 'en', // Required: locale identifier
key: 'common', // Required: translation namespace
loader: async () => {}, // Required: async function returning translations
routes: ['/about'], // Optional: load only for specific routes
},
]Both loaders and a loader's routes accept readonly arrays, so a whole-config as const is fine.
Synchronous translations loaded immediately:
translations: {
en: {
'app.name': 'My App',
},
}Initialize with a specific locale immediately:
initLocale: 'en'Fallback when translation is missing:
fallbackLocale: 'en'Note: This loads translations for both current locale and fallback locale, which may impact performance.
Default return value when translation key is not found:
fallbackValue: '...' // Default: returns the key itselfTransform translations after loading:
preprocess: 'full' // 'full' | 'preserveArrays' | 'none' | custom function'full'(default): Flattens all nested objects to dot notation'preserveArrays': Flattens objects but preserves arrays'none': No preprocessing- Custom function:
(input) => transformedOutput
A map of translation key to the payload its message expects (never for a message that takes none). Supplying it types t/l — keys autocomplete, an unknown key is a type error, and the payload argument is checked. Only its type is read, so the value can stay empty at runtime:
type TranslationSchema = {
'common.greeting': { name: string };
'common.farewell': never;
};
const i18n = new I18n({ ...config, schema: {} as TranslationSchema });Hand-write it for a small set of messages, or point the slot at a generated artifact. A schema whose keys are not a closed set is ignored, and keys stay plain strings. See schema for the full rules.
Time in milliseconds the loaded translations stay fresh for. By default, loaded translations never expire — loaders run once per locale and key (a loader's routes only decide whether a load trigger considers it, not how often it runs).
Set a finite value when your loaders fetch from a source that can change at runtime (e.g. a CMS):
cache: 3600000 // Translations older than 1 hour refetch on the next loadSet to 0 to treat translations as always stale (refetch on every load trigger). You can also drop the loaded state manually at any time with invalidate().
Pipes the constructed instance through extension functions, left to right. Each extension receives the surface produced so far (the raw instance for the first one) and returns the surface handed on — new I18n(config) evaluates to the last extension's output:
import stores from '@sveltekit-i18n/extension-stores';
const { t, locale, loading } = new I18n({
...config,
extensions: [stores],
});An extension may augment the instance in place, or replace the surface entirely (like the store adapter above). Official extensions live in the extensions repository; a custom extension is just a function:
const withGreeting = (i18n) => Object.assign(i18n, {
greet: (name) => i18n.t('common.greeting', { name }),
});
export const i18n = new I18n({ ...config, extensions: [withGreeting] });
i18n.greet('World');Notes:
- Applied at construction time only — a later
loadConfig()call ignores this property. - When an extension returns a new object, the result is no longer
instanceof I18n; the original instance stays reachable through whatever the extension exposes (the official extensions expose it asinstance).
Logging configuration:
log: {
level: 'warn', // 'error' | 'warn' | 'debug'
prefix: '[i18n]: ', // Log prefix
logger: console, // Custom logger
}t(key, ...params)– translate for the active locale (reactive function)l(locale, key, ...params)– translate for an explicit localelocale– the ACTIVE locale; assignment is a fire-and-forgetsetLocale()locales– available localesloading–truewhile any load is in flightinitialized– locale and route set, translations presenttranslations/rawTranslations– the (pre/post-preprocess) tables
Load-triggering methods return the promise of the matching load — concurrent duplicate triggers share one in-flight load (and its promise) instead of fetching twice.
loadTranslations(locale, route?)– load translations for locale and route;routedefaults to the current onesetLocale(locale)– request a locale; loads once a route is knownsetRoute(route)– update the current routeloadConfig(config)– (re)configure the instanceaddTranslations(translations)– add synchronous translationssnapshot()– serialize the active locale (and the fallback) for the current route, shaped likeconfig.translationsso the receiving instance hydrates from itinvalidate(locale?)– mark loaded translations stale (one locale, or all); loaders run again on the next load trigger, and a load still in flight for an invalidated locale settles with its data discardeddestroy()– detach a per-request or per-component instance: in-flight loads settle discarded, further load and mutation calls are ignored, reads keep working
Two helpers the instance uses internally ship from a separate subpath, for code that has to match the library's own behavior:
import { sanitizeLocales, toDotNotation } from '@sveltekit-i18n/base/utils';toDotNotation(input, preserveArrays?)– the flattening behindpreprocess, for a custompreprocessthat still wants dot notationsanitizeLocales(...locales)– normalizes a locale from a URL, cookie orAccept-Languageheader the way the instance does, so it can be compared againstlocale
Full API documentation: docs/README.md
- 🌐 sveltekit-i18n.github.io – The documentation site, with a live playground
- 📖 Full API Documentation – Complete reference
- 📚 Main Library Docs – Guides, tutorials, and best practices
- 🎨 Parsers – Available parsers and how to create your own
- 💡 Examples – Real-world usage examples
import { I18n, type Config } from '@sveltekit-i18n/base';
import parser from '@sveltekit-i18n/parser-curly';
// The parser's params – the rest parameters of `t`/`l`. Annotate only when the
// config lives on its own; `new I18n({ ... })` infers them.
type Params = [payload?: Record<string, unknown>];
const config: Config.T<Params> = {
parser: parser({ onReport: null }),
loaders: [/* ... */],
};Two more things are inferred from the config itself. schema types the keys and payloads of t/l, and every locale the config names — loader locales, initLocale, fallbackLocale and the keys of translations — completes the locale arguments and reads (setLocale, loadTranslations, invalidate, l, locale, locales):
const i18n = new I18n({ parser: parser({ onReport: null }), initLocale: 'en', fallbackLocale: 'de' });
i18n.setLocale('en'); // 'en' | 'de' autocomplete here
i18n.setLocale('sv'); // still accepted — the union is a hint, not a constraintThe locales survive only when the config reaches the constructor as a literal — inline, as above, or a separate object with as const. An annotated or separately widened config, and any config with one dynamic locale source (loaders: locales.map(...)), leaves them plain string. See TypeScript for both.
- sveltekit-i18n – Complete solution, with the Curly Message Format parser included
- @sveltekit-i18n/parser-curly – Curly Message Format parser
- @sveltekit-i18n/parser-icu – ICU message format parser
- Extensions – Official extensions for the
config.extensionspipe
For general contribution guidelines, see the Contributing Guide in the main library repository.
For issues specific to base functionality, create a ticket here.
See Releases for version history.
You can support the maintenance of this package through GitHub Sponsors.
MIT