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

@plcharriere/svelte-i18n

v0.2.7

Published

Typed, ICU-aware, SSR-safe i18n for SvelteKit 2 + Svelte 5 with per-route scoping.

Readme

svelte-i18n

Typed, ICU-aware, SSR-safe i18n for SvelteKit 2 + Svelte 5 with per-route scoping.

t('cart.items', { count: 2 });   // ✅ typed from the schema
t('cart.items', { count: '2' }); // ❌ count: number
t('car.items');                  // ❌ autocomplete catches the typo

Install

npm install @plcharriere/svelte-i18n
pnpm add @plcharriere/svelte-i18n
yarn add @plcharriere/svelte-i18n
bun add @plcharriere/svelte-i18n

Usage

1. Write a locale file

// src/locales/en.ts
import { schema, typed } from '@plcharriere/svelte-i18n';

export default schema({
  nav: { home: 'Home', about: 'About' },
  cart: {
    items: typed<{ count: number }>(
      '{count, plural, one {# item} other {# items}}'
    )
  }
});

2. Register your locales

// src/i18n.ts
import { createI18n } from '@plcharriere/svelte-i18n';

export const {
  t,
  setLocale,
  getCurrentLocale,
  getDefaultLocale,
  getLocales,
  isLoadingLocale,
  getLoadingLocale
} = createI18n({
  mode: 'path',
  defaultLocale: 'en',
  locales: {
    en: {
      label: 'English',
      nativeLabel: 'English',
      load: () => import('./locales/en')
    },
    fr: {
      label: 'French',
      nativeLabel: 'Français',
      load: () => import('./locales/fr')
    },
    'en-GB': {
      label: 'English (UK)',
      parent: 'en',
      load: () => import('./locales/en-GB')
    },
    ar: {
      label: 'Arabic',
      nativeLabel: 'العربية',
      rtl: true,
      load: () => import('./locales/ar')
    }
  }
});

createI18n() returns the typed locale bundle: t (typed against your schema), and setLocale, getCurrentLocale, getDefaultLocale, getLocales, isLoadingLocale, getLoadingLocale — all typed against the locale codes you configured. getSeoLinks is schema-agnostic and imported directly from the package.

Each helper is also re-exported from @plcharriere/svelte-i18n directly with loose string typing — handy if you don't need strict type-checking on locale codes:

// Typed (recommended) — destructured from createI18n
import { setLocale } from './i18n';
setLocale('xx'); // ❌ TS error: not in your locales

// Untyped (escape hatch)
import { setLocale } from '@plcharriere/svelte-i18n';
setLocale('xx'); // ✓ compiles; no-op + warn at runtime

3. Wire SvelteKit

// src/hooks.server.ts
import './i18n';
import { createI18nHandle } from '@plcharriere/svelte-i18n/server';

export const handle = createI18nHandle();
// src/hooks.ts — path mode only
import './i18n';
import { createI18nReroute } from '@plcharriere/svelte-i18n';

export const reroute = createI18nReroute();
// src/app.d.ts
import type { I18nLocals } from '@plcharriere/svelte-i18n/server';

declare global {
  namespace App {
    interface Locals {
      i18n: I18nLocals;
    }
  }
}

export {};
// src/routes/+layout.server.ts
import { getSeoLinks } from '@plcharriere/svelte-i18n';

export const load = ({ locals, url }) => ({
  i18n: {
    ...locals.i18n,
    seo: getSeoLinks({ url, locale: locals.i18n.locale })
  }
});
<!-- src/routes/+layout.svelte -->
<script>
  import { I18n } from '@plcharriere/svelte-i18n';
  import { t, setLocale, getLocales } from '../i18n';
</script>

<I18n />

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

{#each getLocales() as locale (locale.code)}
  <button onclick={() => setLocale(locale.code)}>{locale.nativeLabel}</button>
{/each}

Done. / renders English, /fr renders French, setLocale('fr') client-navigates, <html lang dir> tracks the active locale.

Features

  • Typed keys + params, merged across every locale.
  • Three routing modespath / cookie / domain.
  • Locale variantsen-GB → en → default, partial dictionaries supported.
  • Full ICU — plural, select, selectordinal, number / date / currency formats via intl-messageformat.
  • Per-route dictionary scoping — optional Vite plugin ships only the keys each page actually uses. Landing visitors don't download admin strings. No namespaces, no opt-in lists.
  • SSR-safe — concurrent requests can't leak locales into each other.
  • SEO — canonical + hreflang + x-default, one URL per page.
  • <html lang dir> tracks the active locale, on the server and client.
  • Hot-swap locales in dev — edit a string in fr.ts and every visible translation updates in place. No reload, no state loss.

API

| Export | Purpose | | --- | --- | | createI18n(config) | Setup. Returns the typed bundle (t, setLocale, getCurrentLocale, getDefaultLocale, getLocales, isLoadingLocale, getLoadingLocale). | | t(key, params?) | Typed translator. | | setLocale(code) | Switch locale, per-mode side effects. | | getCurrentLocale() | Active locale metadata. | | getDefaultLocale() | Default locale metadata (the one configured via defaultLocale). | | getLocales() | All configured locales. | | isLoadingLocale(code?) | Reactive: true while a setLocale is in flight. With code, only true while switching to that specific locale. | | getLoadingLocale() | Reactive: the locale currently being switched to, or undefined. | | getSeoLinks(ctx?) | Canonical / alternates / xDefault. On by default; pass seo: false to disable. | | <I18n /> | Mount once in root layout. | | schema() / typed<T>() | Locale-file authoring. |

The locale helpers (setLocale and friends) are re-exported standalone from @plcharriere/svelte-i18n with loose string typing — use those if you don't need locale-code type-checking.

Server entry (@plcharriere/svelte-i18n/server): createI18nHandle({ keyManifest? }).

Vite entry (@plcharriere/svelte-i18n/vite): svelteI18n() — see Per-route scoping.

Routing modes

Pick how the active locale is determined on each request. Set via mode on createI18n().

path

The locale is the first URL segment: /en/about, /fr/about. The default language can optionally be served unprefixed (/about).

  • Best for: SEO-critical sites — each translation has a distinct, crawlable URL.
  • Switching: setLocale('fr') client-navigates to the equivalent /fr/... URL, no full reload.
  • Internal links: write <a href="/about"> as-is. The library rewrites unprefixed internal hrefs to carry the active locale — both in the SSR HTML (so crawlers, hover previews, copy-link, and middle-click see /fr/about) and in the DOM after a client-side switch. Default-locale pages stay unprefixed.

cookie

URLs stay the same across locales (/about). The active locale is read from a cookie (locale by default), with ?lang=xx as a one-shot override that also writes the cookie.

  • Best for: apps where URL stability matters (auth flows, shared links, deep-linked state) and SEO per-language isn't a priority.
  • Switching: setLocale('fr') writes the cookie and re-runs server loads in the new locale.
  • Cross-tab sync: when a tab calls setLocale, every other tab on the same origin updates automatically via BroadcastChannel. Toggle with syncTabs: false; rename the channel with syncChannel: 'my-app' if you need to isolate from another app sharing the origin.

domain

The locale is picked by event.url.host. Each language declares one or more domains: ['example.fr', 'fr.example.com'].

  • Best for: multi-region deployments where each language lives on its own domain or subdomain.
  • Switching: setLocale('fr') navigates to the configured domain for fr. Unmapped hosts fall back to the default (or reject, see domainFallback).

Config options

createI18n({
  mode: 'path',              // 'path' | 'cookie' | 'domain' (defaults to 'path')
  defaultLocale: 'en',
  locales: { ... },

  strict: false,             // throw instead of warn on missing keys / params
  cookieName: 'locale',      // cookie mode only
  domainFallback: 'default', // 'default' | 'reject' (domain mode)
  seo: true,                 // pass `false` to suppress getSeoLinks() output

  // cookie mode only — cross-tab locale sync via BroadcastChannel
  syncTabs: true,            // disable with `false`
  syncChannel: 'svelte-i18n' // override if multiple apps share the origin
});

Per-route scoping

By default every request ships the full dictionary for the active locale plus its fallback chain. For a marketing + app + admin codebase that's wasteful — visitors to / don't need the admin strings, and ungated copy shouldn't leak via view-source.

Opt into per-route pruning by adding the Vite plugin:

// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { svelteI18n } from '@plcharriere/svelte-i18n/vite';

export default {
  plugins: [svelteI18n(), sveltekit()]
};

That's it. Your existing hooks.server.ts stays exactly as it was — createI18nHandle() with no arguments picks up the per-route manifest automatically.

What you get: /cart only ships cart.*, nav.*, common.*, profile.*. Zero bytes from admin, home, seo, etc.

Limitations: dynamic keys (t(someVar)) can't be discovered. Reference them as literals somewhere on the route — e.g. t('errors.generic') — to force them into the shipped set, or accept they'll resolve from the fallback chain instead.

Dev HMR: edit a locale file and translations swap in place. No reload, no state loss.

Gotcha: reactivity

t() is reactive only if you don't capture it.

<!-- ❌ stays in initial locale forever -->
<script>
  const label = t('cart.addToCart');
</script>

<!-- ✅ updates on locale change -->
<script>
  const label = $derived(t('cart.addToCart'));
</script>

<!-- ✅ inline is already reactive -->
<button>{t('cart.addToCart')}</button>

Svelte 5 flags the broken pattern with a state_referenced_locally warning. Listen to it: wrap in $derived or call t() inline in the template.

Details

See SPECS.md for the full specification — every requirement, every config option, every warning code.

License

MIT