# sveltekit-i18n: per-request locales, lazy loading and typed keys for SvelteKit

> sveltekit-i18n is an internationalization library built specifically for SvelteKit, and its distinguishing choices are that it creates a separate translation instance per request on the server and per tab in the browser, loads translations only for the routes a visitor actually visits, and derives TypeScript key and payload types from a schema registered once for the whole app. It is MIT licensed, ESM-only, and works on Node, Bun and Deno.

**sveltekit-i18n/lib** — Internationalization library built for SvelteKit.

- Repository: https://github.com/sveltekit-i18n/lib
- Website: https://sveltekit-i18n.github.io
- Stars: 576 · Forks: 33
- Language: TypeScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/sveltekit-i18n-lib

## What it is, and the seven decisions in the feature list

sveltekit-i18n is described as a lightweight, powerful internationalization library designed specifically for SvelteKit, and it is really two packages in one. It combines @sveltekit-i18n/base with @sveltekit-i18n/parser-curly, and the README makes a point of that: the core and the parser come with it, so there is nothing else to add.

The feature list is the clearest statement of the design, and each entry is a decision rather than a feature name.

SvelteKit-optimized means the kit entry wires hooks, layouts and components, with an instance per request on the server and one per tab in the browser. That single sentence contains the whole architecture: on the server, each request gets its own instance, so two users with different locales never interfere; in the browser, each tab keeps its own instance, so switching language in one tab does not change another.

One install means the core and the parser are bundled, and the README warns in bold terms not to install the sub-packages alongside, because your app would end up with two copies of the core and two reactive graphs.

Smart loading means translations load only for visited pages, lazily. Route-based means the loading is driven by your routes rather than by a manual list. Flexible means custom data sources are supported, local files, APIs or databases. Extensible means you add surfaces through the extensions pipe, including Svelte stores, keys as members of t, and markup rendered as elements and components.

Component-scoped means you can create several translation instances for different parts of the app, which is how a widget embedded in a host page can carry its own locale.

TypeScript support is the most substantial of the seven. The README describes complete type definitions with keys and payloads typed by a schema registered once for the whole app, generated by @sveltekit-i18n/typegen and read as members of t with the extension-typed-access extension.

The primary language is TypeScript, the licence is MIT, the default branch is master, and the recent releases are 3.4.1 from 2026-10-07, 3.4.0 from 2026-10-07 and 3.3.0 from 2026-09-30. The last push was on 2026-10-18, so this is an actively maintained project on a 3.x line rather than a 0.x experiment.

## Requirements, and why the package is ESM-only and runtime-agnostic

The Requirements section is short and specific, and it reads like a compatibility statement rather than a wish list.

Svelte 5 or newer is required, 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. That is a deliberate constraint and it has a clear payoff: the same package works in a Node server, in a Bun server and in a Deno deployment without a conditional export or a polyfill, because there is nothing platform-specific to polyfill.

The documentation snippets need SvelteKit 2.12 or newer, which ships $app/state. The typegen package needs Vite 8, which SvelteKit supports from 2.53, and so Node 22.12 or newer.

So there is a version floor in two layers. The runtime floor is Svelte 5 plus a modern JavaScript runtime, and the tooling floor is SvelteKit 2.53 or later if you want the generated types. The package manifest agrees: the engines field lists node >=22, bun >=1.2 and deno >=2, and the peer dependency is svelte >=5.

The ESM-only decision also shows up in the manifest exports. There are three entry points, the root, ./utils and ./kit, and each one is declared with a types condition, a svelte condition and a default condition. The svelte condition is the interesting one, because it is how a bundler picks the Svelte-aware build of the same file. The manifest is also marked sideEffects false, which is the standard signal that the package is safe to tree-shake.

The build is tsup, the tests are vitest, the lint is eslint with the stylistic plugin, and the pretest script runs a full build and a typecheck before the test suite, so the type definitions are always verified against the built output rather than against the source.

## Installing it, and the CLI add-on that does the whole setup

The installation section starts with the newer path, and it is the reason the quick start below is as short as it is.

In a SvelteKit 3 project that does not use sveltekit-i18n yet, the Svelte CLI add-on sets it up for you. It asks for your locales, the message format, the routing, the extensions, typed keys and a demo page, then installs the packages and writes what the quick start walks through: the translations, src/lib/i18n, the hooks, the layouts and app.html.

```bash
npx sv add @sveltekit-i18n
```

So the add-on does the configuration interview and writes the files, and the README then walks through what it wrote. That is a better onboarding shape than a page of manual steps, and the fact that the questions cover the routing mode and the extensions tells you those are the two decisions with real consequences.

Installing by hand is one package:

```bash
npm install sveltekit-i18n
# bun add sveltekit-i18n
# deno add npm:sveltekit-i18n
```

Then the README repeats the warning about the sub-packages. @sveltekit-i18n/base and @sveltekit-i18n/parser-curly come with the main package: the core's whole API, the parser's types and its build-time extractParamsFactory and cst are re-exported here. Do not install them alongside, or your app ends up with two copies of the core and two reactive graphs.

That failure mode is worth taking seriously. Two copies of the core means two module instances, so a store created by one is not the store read by the other, and the result is a locale that silently never updates rather than an error. The failure is quiet, which is why the README puts it in bold.

## The config and the loader model

The configuration object is where the whole system is described, and it is short.

```javascript
import { defineI18n } from 'sveltekit-i18n/kit';

export const config = {
  initLocale: 'en',
  loaders: [
    {
      locale: ['en', 'cs'],
      namespace: 'common',
      loader: async ({ locale, namespace }) => (await import(`./translations/${locale}/${namespace}.json`)).default,
    },
  ],
};

export const { handle, load, use, get } = defineI18n(config, {
  preferredLocale: (event) => event.cookies?.get('lang'),
});
```

Two details in that snippet do the interesting work. The loader descriptor lists a locale array and a namespace, and its loader is a dynamic import of the translation JSON at that path, so the translations are separate modules fetched on demand. The locale array contains both en and cs, which is the point of the loader model: one descriptor can cover many locale and namespace pairs, and the loader is called once per pair, with the pair in its props.

The README adds an important clarification: no parser is stated in the config. This package fills that slot. In other words, the parser is a property of the package rather than a per-project choice, which is why the main package is the one that ships with the curly parser pre-selected.

The second detail is the preferredLocale function, which reads a lang cookie off the request event. The whole negotiation order is then given in the prose that follows the wiring steps: the server negotiates the locale on every request, using preferredLocale first, then Accept-Language, then initLocale, then fallbackLocale, and finally the first locale the config serves. That last fallback matters for a user whose Accept-Language header asks for a language you do not ship.

The defineI18n call returns the four names the framework needs: handle, load, use and get. handle is the SvelteKit hook that fills the %lang% and %dir% placeholders in app.html, load is the load function for both the server and the universal layout, use is called in the layout component to bind the instance to the page data, and get is the function components call to get the instance.

## The wiring, and the reactivity rule that catches people

Step three of the quick start is the wiring, and it is four small edits rather than one big one.

The hooks file re-exports the handle function:

```javascript
export { handle } from '#lib/i18n.js';
```

The layout server file and the layout file export the same load line, and the README notes that the same line goes in both.

The layout component calls use with the page data:

```svelte
<!-- src/routes/+layout.svelte -->
<script>
  import { use } from '#lib/i18n.js';

  let { data, children } = $props();

  use(() => data);
</script>

{@render children()}
```

And the app.html carries two placeholders:

```html
<!-- src/app.html -->
<html lang="%lang%" dir="%dir%">
```

The dir attribute matters more than it looks. Setting the text direction on the document element from the server-rendered locale is what makes a right-to-left language render correctly from the first byte, rather than after hydration flips it.

The reactivity rule is the part worth internalising, and the README is unusually emphatic about it. The call reads the reactive translation table and locale, so the text updates when either changes. Keep 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.

That is a real footgun in a Svelte 5 codebase, because destructuring is idiomatic and the failure is silent: a snapshot simply stops updating. The mitigation offered is the $t store form, which requires adding the extension-stores extension to config.extensions.

The client-only path is separate. An app that sets export const ssr = false can skip the wiring entirely and export one instance constructed directly from the config with new I18n(config). Its load function then calls i18n.loadTranslations for the locale and the current path. And the README notes a subtlety worth knowing: that load runs for a preload too, meaning the page a hovered link leads to, and it switches the instance to it, unless every later pass uses preload() and commits in the root layout instead.

## The repository, and the examples that document the edge cases

The top-level listing shows a well-organised project with several things that are not the library itself.

There is docs/ and a site/ directory, which is where the documentation site at sveltekit-i18n.github.io is built from. There is bench/ and a BENCH.md, and the package manifest has a bench script that runs node bench/run.ts, so performance work is part of the project rather than an afterthought. There is tests/ with vitest.config.ts, and the CI workflow badge in the README header points at a tests workflow. The build is tsup with tsup.config.js, and there is a pnpm workspace with a pnpm-lock.yaml.

There are also agent instruction files at the root, AGENTS.md and CLAUDE.md, which is the same pattern several projects in this space have adopted.

The examples directory is the most useful part of the repository for understanding the library, because its names are a list of the situations the library has to handle: component-scoped-csr, component-scoped-ssr, html, locale-param, locale-router-advanced, locale-router-static, locale-router, mdsvex, multi-page, stores and typed-access.

Each pair is informative. locale-router, locale-router-static and locale-router-advanced are three ways of putting the locale in the URL. component-scoped-csr and component-scoped-ssr show the component-scoped feature in the two rendering modes, which is exactly where that feature is most often to differ. locale-param is the case where the locale comes from a route parameter instead of a prefix. mdsvex covers SvelteKit projects whose content is written in Markdown, which means the translation pipeline has to interact with a different source format. multi-page covers the older multi-page routing model. stores covers the $t store extension, and typed-access covers the typed key access extension.

So the library's own example set doubles as a compatibility matrix, and reading it before choosing a routing strategy is cheaper than discovering a limitation later.

## How to evaluate it, and the questions to ask first

There are four things to settle before you adopt it.

First, the routing mode, because it is the decision that shapes your URLs. The locale-router examples exist in three variants, so the library does not force one, but each has consequences: a locale prefix in the path changes every URL and every link, a locale parameter keeps the paths stable but puts the locale in a segment, and the static variant implies prerendering per locale. The add-on asks you about this during setup, which is the right moment to decide.

Second, the type generation. The TypeScript story is a real differentiator: keys and payloads typed by a schema registered once for the whole app, generated by a separate typegen package and read as members of t. The cost is that the schema has to be registered and regenerated, so keys added without regenerating will not typecheck. For a team that wants a missing translation key to be a build error rather than a runtime fallback, that is the point of the feature. For a team that does not, it is an extra step in the workflow.

Third, the parser. The curly parser is bundled, with the parameter syntax in the shape {{name}} and the keys written as dotted paths such as nav.home. The base package and the parser are separate repositories under the same organisation, and the README mentions a parsers directory, which suggests other parsers exist. If your existing translation files use a different format, the migration is a matter of choosing a different parser rather than rewriting every string.

Fourth, the version floors, which are not trivial. Svelte 5, Node 22 or Bun 1.2 or Deno 2, SvelteKit 2.12 for the documented snippets and 2.53 if you want Vite 8 and therefore the type generation. A project on Svelte 4 cannot use this.

And one operational point worth stating plainly. The per-request and per-tab instance model is the library's central architectural choice, and it means the locale lives in the server's response state rather than in a global. That is what makes concurrent visitors with different locales safe, and it is why the same code works in an edge runtime without a shared store. The cost is that the instance has to be created per request and handed to the browser, which the handle and load hooks take care of, but it does mean the library is not a drop-in for an existing global i18n setup.

## Conclusion

Adopt sveltekit-i18n if you are building a SvelteKit application on Svelte 5 that needs more than one locale, and if you want translations loaded per route rather than up front, keys and payloads typed from a single schema, and an instance per request on the server so concurrent visitors with different locales cannot interfere. Do not adopt it if your project is still on Svelte 4, or if you need a global singleton i18n object, because the per-request and per-tab instance model is the library's central design and is not a drop-in for that. Before you commit, decide how the locale appears in your URLs using the three locale-router examples, run the add-on in a scratch project to see the files it writes, and register the type schema so a missing translation key fails at build time rather than falling back silently at runtime.

## FAQ

### What is sveltekit-i18n?

It is an internationalization library designed specifically for SvelteKit, published on npm as sveltekit-i18n and MIT licensed. It combines @sveltekit-i18n/base with @sveltekit-i18n/parser-curly so that both the core and the parser come with a single install, and it is ESM-only with no dependency on node: modules.

### How do I install sveltekit-i18n?

In a SvelteKit 3 project you can run npx sv add @sveltekit-i18n, and the add-on asks for your locales, message format, routing, extensions, typed keys and a demo page, then writes the translations, the i18n module, the hooks, the layouts and app.html for you. Installing by hand is npm install sveltekit-i18n, with equivalent bun and deno add commands.

### Why should I not install @sveltekit-i18n/base separately?

The core and the parser are re-exported by the main package, so installing them alongside gives your app two copies of the core and two reactive graphs. The failure is silent rather than an error, so the README warns against it explicitly.

### How does sveltekit-i18n decide which locale to use?

The server negotiates on every request, taking preferredLocale first, then the Accept-Language header, then initLocale, then fallbackLocale, and finally the first locale the config serves. The chosen locale is loaded into an instance of its own and handed to the browser, which keeps one instance per tab and does not refetch what the server already loaded.

### What does the TypeScript support in sveltekit-i18n give me?

Complete type definitions with keys and payloads typed by a schema registered once for the whole app, generated by the separate @sveltekit-i18n/typegen package and read as members of t with the extension-typed-access extension. The typegen package needs Vite 8, which SvelteKit supports from 2.53, so Node 22.12 or newer.

### What versions does sveltekit-i18n need?

Svelte 5 or newer, and one of Node 22+, Bun 1.2+ or Deno 2+. The package is ESM-only. The documentation snippets need SvelteKit 2.12 or newer, which ships $app.state, and the manifest declares engines for node, bun and deno with a peer dependency on svelte >=5.

## Sources

- [License: MIT](https://github.com/sveltekit-i18n/lib/blob/master/LICENSE)
- [Project website](https://sveltekit-i18n.github.io)
- [README](https://github.com/sveltekit-i18n/lib/blob/master/README.md)
- [Releases](https://github.com/sveltekit-i18n/lib/releases)
- [sveltekit-i18n/lib on GitHub](https://github.com/sveltekit-i18n/lib)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sveltekit-i18n-lib
