npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sveltekit-i18n

v3.0.0

Published

Internationalization library for SvelteKit

Readme

npm version

sveltekit-i18n

A lightweight, powerful internationalization (i18n) library designed specifically for SvelteKit. This package combines @sveltekit-i18n/base with @sveltekit-i18n/parser-curly to provide the quickest way to add multilingual support to your SvelteKit applications.

Why sveltekit-i18n?

  • 🚀 SvelteKit-optimized – Built specifically for SvelteKit, with per-request instances on the server
  • 📦 One install – The core and the parser come with it; nothing else to add
  • Smart loading – Translations load only for visited pages (lazy loading)
  • 🎯 Route-based – Automatic translation loading based on your routes
  • 🔧 Flexible – Support for custom data sources (local files, APIs, databases)
  • 🧩 Extensible – Add surfaces (Svelte stores, for instance) through the extensions pipe
  • 📝 TypeScript – Complete type definitions, with a schema slot that types keys and payloads
  • 🎨 Component-scoped – Create multiple translation instances for different parts of your app

Requirements

Svelte 5 or newer and Node 22 or newer. The package is ESM-only.

Installation

npm install sveltekit-i18n

That is the whole install. @sveltekit-i18n/base and @sveltekit-i18n/parser-curly come with it: the core's whole API, the parser's types and its parameter extractor are re-exported here — do not install them alongside, or your app ends up with two copies of the core and two reactive graphs.

Quick Start

1. Create your translation files

// src/lib/translations/en/common.json
{
  "greeting": "Hello, {{name}}!",
  "nav.home": "Home",
  "nav.about": "About"
}
// src/lib/translations/cs/common.json
{
  "greeting": "Ahoj, {{name}}!",
  "nav.home": "Domů",
  "nav.about": "O nás"
}

2. Setup i18n configuration

// src/lib/translations/index.js
import { I18n } from 'sveltekit-i18n';

/** @type {import('sveltekit-i18n').Config} */
export const config = {
  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,
    },
  ],
};

export const i18n = new I18n(config);

[!IMPORTANT] That instance is a module-level singleton. On the server it is shared by every request in the process, so it fits a client-only app (export const ssr = false) or one that renders a single locale. Anything that server-renders per visitor needs the per-request wiring in Server-side rendering.

Export the instance, not its parts: locale, locales, loading, initialized and translations are reactive properties, and a destructured value is a one-time snapshot. t and l are functions and stay reactive even when destructured, because their tracked reads happen at call time.

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'; // determine from cookie, user preference, etc.

  await i18n.loadTranslations(initLocale, pathname);

  return {};
};

loadTranslations returns the promise of the matching load, so awaiting it is all the coordination you need — concurrent triggers for the same locale and route join the load already in flight instead of fetching twice.

4. Use translations in your components

<!-- src/routes/+page.svelte -->
<script>
  import { i18n } from '$lib/translations';
</script>

<h1>{i18n.t('common.greeting', { name: 'World' })}</h1>

<nav>
  <a href="/">{i18n.t('common.nav.home')}</a>
  <a href="/about">{i18n.t('common.nav.about')}</a>
</nav>

The call reads the reactive translation table and locale, so the text updates when either changes. If you prefer the $t store form, add @sveltekit-i18n/extension-stores to config.extensions.

The instance

Everything lives on one reactive instance:

| Member | What it is | | --- | --- | | t(key, ...params) | translates for the active locale | | l(locale, key, ...params) | translates for a locale the call names | | locale | the active locale; assigning it is a fire-and-forget setLocale() | | locales | the locales the config knows | | loading | true while any load is in flight | | initialized | true once a locale and a route are set and translations are present | | translations / rawTranslations | the tables, after and before preprocessing | | loadTranslations, setLocale, setRoute | return the promise of the matching load | | loadConfig | returns the promise of the config load | | addTranslations, invalidate, snapshot, destroy | synchronous |

Reading a property is reactive wherever reads are tracked — a component template, $derived, $effect. The full reference is in the API documentation.

Key Features

Route-based Loading

Load translations only for specific routes to optimize performance:

const config = {
  loaders: [
    {
      locale: 'en',
      key: 'home',
      routes: ['/'], // Load only on homepage
      loader: async () => (await import('./en/home.json')).default,
    },
    {
      locale: 'en',
      key: 'about',
      routes: ['/about'], // Load only on about page
      loader: async () => (await import('./en/about.json')).default,
    },
  ],
};

Placeholders and Modifiers

Use dynamic values in your translations:

{
  "welcome": "Welcome, {{name}}!",
  "items": "You have {{count:number;}} {{count; 1:item; default:items;}}."
}
<script>
  import { i18n } from '$lib/translations';
</script>

<p>{i18n.t('welcome', { name: 'Alice' })}</p>
<p>{i18n.t('items', { count: 5 })}</p>

The syntax is the Curly Message Format. Its parser options — custom modifiers, modifier defaults and a report channel — go under config.parserOptions:

const config = {
  parserOptions: {
    modifierDefaults: { number: { maximumFractionDigits: 2 } },
    onReport: (report) => console.warn(report.message, report),
  },
  loaders: [/* … */],
};

Reports are silent by default; onReport is where you route them.

Server-side rendering

Build one instance per request on the server — a module-level instance is shared between concurrent requests, which leaks one visitor's locale into another's page. Export the config, and let each request build from it:

// src/routes/+layout.server.js
import { I18n } from 'sveltekit-i18n';
import { config } from '$lib/translations';

export const load = async ({ url, locals }) => {
  const i18n = new I18n(config);

  await i18n.loadTranslations(locals.locale, url.pathname);

  return { locale: locals.locale, translations: i18n.snapshot() };
};

The client hydrates by handing that payload back through config.translations, so the loaders behind it do not run a second time. The full wiring — including the browser-side instance and passing it down through Svelte context — is in the Getting Started guide.

Documentation

📖 Complete Documentation Index – Find everything in one place

Quick Links

Examples

[!NOTE] The examples still show the v2 API. Their rework is tracked in #230; until it lands, the Getting Started guide is the reference for v3 wiring.

Explore working examples for different use cases:

Advanced Usage

Need a different parser?

This package wires @sveltekit-i18n/parser-curly and fills the core's parser slot itself, so a different message format means building on @sveltekit-i18n/base directly:

import { I18n } from '@sveltekit-i18n/base';
import parser from '@sveltekit-i18n/parser-icu';

const config = {
  parser: parser({ onReport: null }),
  // ... rest of config
};

That is the one case where installing the core directly is right — you are then not using this package at all. Learn more about parsers.

Extensions

config.extensions pipes the constructed instance through adapter functions, left to right, and new I18n(config) evaluates to the last one's output. That is how the store surface ships:

import { I18n } from 'sveltekit-i18n';
import stores from '@sveltekit-i18n/extension-stores';

export const { t, locale, loading } = new I18n({ ...config, extensions: [stores] });

TypeScript Support

Full TypeScript support with complete type definitions for configuration and API:

import { I18n, type Config } from 'sveltekit-i18n';

const config: Config = {
  loaders: [
    // ... your loaders
  ],
};

export const i18n = new I18n(config);

Annotating the config (const config: Config = …) widens it, which costs the locale completion a config literal would have given setLocale and l. Pass the literal straight to the constructor where you want that.

To have payloads checked, give the config a schema — keys autocomplete and a wrong payload is a type error:

import { I18n } from 'sveltekit-i18n';

const i18n = new I18n({
  ...config,
  schema: {} as { 'common.greeting': { name: string } },
});

i18n.t('common.greeting', { name: 'Alice' }); // ok
i18n.t('common.greting', { name: 'Alice' });  // Error: not a key of the schema
i18n.t('common.greeting', {});                // Error: `name` is required

Only the schema's type is read, so the slot may hold an empty value. A single payload type for every message is stated through the type arguments instead:

import { I18n, type Config } from 'sveltekit-i18n';

type Payload = { name: string };

const config: Config<Payload> = { /* … */ };

export const i18n = new I18n<Config<Payload>, Payload>(config);

Note: The library provides the type slots but does not generate them from your JSON files. A generator that fills schema from your translations is planned for 3.1 (#234); until then, write the schema by hand or generate it yourself with the re-exported extractParamsFactory, which reports what each message expects of its payload (see Best Practices).

Contributing

We welcome contributions! Please read our Contributing Guide for details on:

  • Development setup and workflow
  • Git workflow (rebase-based, linear history)
  • Commit guidelines (atomic commits)
  • Pull request process
  • Code standards and testing

Changelog

See Releases for version history.

Related Packages

License

MIT