tokimeki-i18n
v0.2.0
Published
Tiny, flash-free, race-free i18n runtime for Svelte 5 and beyond
Downloads
342
Maintainers
Readme
tokimeki-i18n
Tiny, flash-free, race-free i18n runtime for Svelte 5 and beyond.
- ~1 KB gzipped runtime, zero dependencies. No ICU parser, no merge library, no memoization layers.
- No text flash, ever. The translator store only emits formatters bound to fully loaded dictionaries. A locale switch is a single atomic swap after its dictionaries are ready.
- No races, by construction.
setLocalecalls are serialized with a generation token — the last call deterministically wins. There is no implicitinitialLocalethat can finish loading later and overwrite your choice. - Only the current locale (+ fallback) is ever downloaded. Dictionaries are lazy
import()ed, loaded in parallel, cached once, and non-active locales are evicted from memory after a switch. - Familiar
$_('key')template notation, so migrating a large existing codebase is mostly an import swap. - Works with any framework that understands the Svelte store contract; no runtime dependency on Svelte itself.
Install
npm install tokimeki-i18nQuick start
// src/lib/i18n.ts — imported once, for its side effect
import { setupI18n } from 'tokimeki-i18n';
setupI18n({
fallback: 'en',
locales: {
en: [() => import('./locales/en.json')],
ja: [() => import('./locales/ja.json'), () => import('./locales/extra/ja.json')],
'pt-BR': 'pt', // alias: shares pt's dictionaries
pt: [() => import('./locales/pt.json')],
},
});// SvelteKit: +layout.ts — gate first render on the resolved locale
import '$lib/i18n';
import { setLocale } from 'tokimeki-i18n';
export async function load() {
await setLocale(savedLanguage ?? navigator.language);
}<script>
import { _, locale } from 'tokimeki-i18n';
</script>
<h1>{$_('hello', { name: 'World' })}</h1>
<p>current locale: {$locale}</p>API
| Export | Description |
| --- | --- |
| setupI18n(config) | Registers locales and the fallback. Pure registration — nothing loads until setLocale. Calling it again fully resets the runtime (tests, HMR). |
| addLocales(locales) | Additive registration for component libraries: merges dictionary sources into the host app's registry instead of resetting it. Already-loaded locales are hot-merged and re-emitted atomically. Creates a minimal registry (fallback en) when the host has not called setupI18n. |
| setLocale(tag) | Resolves tag (exact → BCP-47 subtag narrowing → fallback), loads the locale and fallback dictionaries in parallel, then applies them in one atomic swap. Resolves to the applied locale. If everything is already cached the swap is synchronous. Last concurrent call wins. |
| preloadLocale(tag) | Speculatively warms the dictionary cache (e.g. when a language selector opens) so the following setLocale is synchronous. Load errors are swallowed; setLocale will retry. |
| getLocale() | Current applied locale, synchronously (undefined before the first setLocale). |
| t(key, params?) | Synchronous translation for plain .ts/.js code. |
| _ | Store of the translator function: $_('key'), $_('key', { name }). |
| locale | Read-only store of the applied locale: $locale. Writes go through setLocale. |
Messages
Dictionaries are flat JSON: { "hello": "Hello {name}" }. Placeholders are {param}; params are passed flat: $_('hello', { name: 'World' }). A missing key falls back to the fallback locale's message, then to the key itself (hook onMissing in setupI18n for diagnostics). No ICU MessageFormat — that is what keeps the runtime ~1 KB. Dictionary sources can also be plain objects instead of loader functions (inline a critical subset for zero-network first paint).
Dictionary tree-shaking (optional Vite plugin)
Unused message keys can be pruned from dictionary chunks at build time:
// vite.config.ts
import { pruneMessages } from 'tokimeki-i18n/vite';
plugins: [
pruneMessages({
dictionaries: 'src/lib/i18n/locales/**/*.json',
mode: 'report', // start here: logs prunable keys, changes nothing
// mode: 'prune', // then enable pruning
// keepKeys: [/^external_/], // keys resolved from data unknown at build time
// reportFile: 'i18n-prune-report.json',
}),
]The scan is conservative: a key is kept if it appears as a string literal anywhere in your sources (so $_(preview.key) is safe as long as the key string exists somewhere, e.g. where preview is built). Keys that only arrive from network data must be listed in keepKeys or excluded per dictionary via exclude.
For library authors
A component library that ships its own translations must NOT call setupI18n — that would reset the host app's registry. Use addLocales instead:
// your-lib/src/i18n.ts — imported for its side effect by your components
import { addLocales, getLocale, setLocale } from 'tokimeki-i18n';
import en from './locales/en.json';
import ja from './locales/ja.json';
addLocales({ en: [en], ja: [ja] });
if (!getLocale()) setLocale(navigator.language);- Embedded in a host app that uses tokimeki-i18n: your keys merge into the host registry and your UI follows the host's
setLocaleautomatically. Namespace your keys (myLib.save) to avoid collisions. - Standalone (host has no tokimeki-i18n setup):
addLocalescreates a minimal registry (fallbacken) and thesetLocale(navigator.language)fallback kicks in becausegetLocale()is stillundefined. - Ordering: the host's
setupI18nmust run before youraddLocales(app bootstrap before lazily loaded components — the natural order). Declaretokimeki-i18nas a peerDependency so the host and your library share one runtime instance.
SSR
The runtime is a module-level singleton, designed for client-rendered apps (ssr = false in SvelteKit). It is safe to import on the server, but per-request locale isolation is not provided.
Publishing (maintainer notes)
npm run check && npm test && npm run build
npm publish # prepublishOnly runs check + test + buildLicense
MIT
