Skip to content

Repository files navigation

npm version

@sveltekit-i18n/base

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.

When to use @sveltekit-i18n/base

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

Key Features

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

Requirements

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.

Installation

npm install @sveltekit-i18n/base
# bun add @sveltekit-i18n/base
# deno add npm:@sveltekit-i18n/base

You'll also need a parser:

# Choose one:
npm install @sveltekit-i18n/parser-curly
npm install @sveltekit-i18n/parser-icu
# or create your own

Quick Start

1. Create translation files

// src/lib/translations/en/common.json
{
  "greeting": "Hello, {{name}}!",
  "farewell": "Goodbye!"
}

2. Setup with a parser

// 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);

3. Load translations in your layout

// 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.

4. Use in components

<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.

Using Different Parsers

ICU Message Format

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}}."
}

Custom Parser

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.

Configuration Options

parser (required)

Message parser instance. See Parsers.

loaders

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.

translations

Synchronous translations loaded immediately:

translations: {
  en: {
    'app.name': 'My App',
  },
}

initLocale

Initialize with a specific locale immediately:

initLocale: 'en'

fallbackLocale

Fallback when translation is missing:

fallbackLocale: 'en'

Note: This loads translations for both current locale and fallback locale, which may impact performance.

fallbackValue

Default return value when translation key is not found:

fallbackValue: '...' // Default: returns the key itself

preprocess

Transform 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

schema

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.

cache

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 load

Set 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().

extensions

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 as instance).

log

Logging configuration:

log: {
  level: 'warn',        // 'error' | 'warn' | 'debug'
  prefix: '[i18n]: ',   // Log prefix
  logger: console,      // Custom logger
}

API Reference

Reactive properties

  • t(key, ...params) – translate for the active locale (reactive function)
  • l(locale, key, ...params) – translate for an explicit locale
  • locale – the ACTIVE locale; assignment is a fire-and-forget setLocale()
  • locales – available locales
  • loadingtrue while any load is in flight
  • initialized – locale and route set, translations present
  • translations / rawTranslations – the (pre/post-preprocess) tables

Methods

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; route defaults to the current one
  • setLocale(locale) – request a locale; loads once a route is known
  • setRoute(route) – update the current route
  • loadConfig(config) – (re)configure the instance
  • addTranslations(translations) – add synchronous translations
  • snapshot() – serialize the active locale (and the fallback) for the current route, shaped like config.translations so the receiving instance hydrates from it
  • invalidate(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 discarded
  • destroy() – detach a per-request or per-component instance: in-flight loads settle discarded, further load and mutation calls are ignored, reads keep working

Utilities

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 behind preprocess, for a custom preprocess that still wants dot notation
  • sanitizeLocales(...locales) – normalizes a locale from a URL, cookie or Accept-Language header the way the instance does, so it can be compared against locale

Full API documentation: docs/README.md

Documentation

TypeScript Support

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 constraint

The 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.

Related Packages

Contributing

For general contribution guidelines, see the Contributing Guide in the main library repository.

For issues specific to base functionality, create a ticket here.

Changelog

See Releases for version history.

Sponsor

You can support the maintenance of this package through GitHub Sponsors.

License

MIT

About

This repository contains the base functionality of sveltekit-i18n and provides support for external message parsers.

Topics

Resources

Stars

11 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages