The last Svelte i18n library you'll ever need. Type-safe, zero-config, with seamless SSR/CSR support.
⚠️ Warning: This library is currently in active development and is not recommended for production use yet. APIs may change in future releases. Documentation may be incomplete or contain errors.
# Install the package
pnpm add @shelchin/svelte-i18n
# or
npm install @shelchin/svelte-i18n
# or
yarn add @shelchin/svelte-i18n
Run the initialization command to auto-generate configuration:
# Run init command (auto-detects project type and generates config)
pnpm exec svelte-i18n init
# or
npx svelte-i18n init
This will:
src/translations/
directory structurelocales/en.json
, locales/zh.json
)i18n.ts
configuration file with type-safe setupThe generated i18n.ts
will look like:
// src/translations/i18n.ts (auto-generated)
import { createI18n } from '@shelchin/svelte-i18n';
import type { I18nPath } from './types/i18n-generated.js';
// Auto-scan and import translations from locales directory
const translationModules = import.meta.glob('./locales/*.json', {
eager: true,
import: 'default'
});
const translations: Record<string, unknown> = {};
// Extract language code from file path and build translations object
for (const [path, module] of Object.entries(translationModules)) {
const match = path.match(/\/([^/]+)\.json$/);
if (match && match[1]) {
const langCode = match[1];
translations[langCode] = module;
}
}
// Create i18n instance with type safety
export const i18n = createI18n<I18nPath>({
namespace: 'app',
isMain: true,
translations,
defaultLocale: 'en',
fallbackLocale: 'en'
});
export default i18n;
+layout.server.ts
for SSR:// src/routes/+layout.server.ts
import { loadI18nSSR } from '@shelchin/svelte-i18n';
import { i18n } from '$src/translations/i18n.js';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ request }) => {
const locale = await loadI18nSSR(i18n, request);
return {
locale
};
};
+layout.ts
for Universal Loading:// src/routes/+layout.ts
import { loadI18nUniversal } from '@shelchin/svelte-i18n';
import { i18n } from '$src/translations/i18n.js';
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async ({ data }) => {
await loadI18nUniversal(i18n, data?.locale);
return {
locale: data?.locale
};
};
+layout.svelte
for Client:<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { setupI18nClient } from '@shelchin/svelte-i18n';
import { i18n } from '$src/translations/i18n.js';
onMount(async () => {
await setupI18nClient(i18n);
});
</script>
<slot />
<script lang="ts">
import { i18n } from '$src/translations/i18n.js';
import { LanguageSwitcher } from '@shelchin/svelte-i18n';
let name = $state('World');
// Type-safe translations with autocomplete
const welcome = i18n.t('welcome');
const hello = i18n.t('hello', { name });
</script>
<h1>{welcome}</h1>
<p>{hello}</p>
<!-- Direct usage -->
<nav>
<a href="/">{i18n.t('navigation.home')}</a>
<a href="/about">{i18n.t('navigation.about')}</a>
<a href="/contact">{i18n.t('navigation.contact')}</a>
</nav>
<!-- Language Switcher Component -->
<LanguageSwitcher {i18n} />
For library packages, use namespace to avoid conflicts:
// In a library: src/lib/translations/i18n.ts
import { createI18n } from '@shelchin/svelte-i18n';
import type { LibI18nPath } from './types/i18n-generated.js';
// Auto-import translations
const translationModules = import.meta.glob('./locales/*.json', {
eager: true,
import: 'default'
});
const translations: Record<string, unknown> = {};
for (const [path, module] of Object.entries(translationModules)) {
const match = path.match(/\/([^/]+)\.json$/);
if (match && match[1]) {
translations[match[1]] = module;
}
}
export const libI18n = createI18n<LibI18nPath>({
namespace: 'my-ui-lib', // Use your package name
translations
});
// Usage in library component
libI18n.t('button.save');
# Generate types from translation files
pnpm exec svelte-i18n generate-types
# or with custom paths
pnpm exec svelte-i18n generate-types --dir ./src/translations/locales --out ./src/lib/types/i18n-generated.ts
# Check for missing translations
pnpm exec svelte-i18n validate src/translations/locales
# Extract keys from source code
pnpm exec svelte-i18n extract ./src ./template.json
The init
command automatically generates TypeScript types. To regenerate after changes:
pnpm exec svelte-i18n generate-types
This creates type definitions that provide autocomplete for all translation keys:
// Auto-generated types in src/translations/types/i18n-generated.d.ts
export type I18nPath =
| 'welcome'
| 'hello'
| 'navigation.home'
| 'navigation.about'
| 'navigation.contact';
// Already configured in your i18n.ts with type safety
import type { I18nPath } from './types/i18n-generated.js';
export const i18n = createI18n<I18nPath>({
// ... config
});
// Now TypeScript ensures only valid keys are used
i18n.t('welcome'); // ✅ Valid
i18n.t('hello', { name: 'John' }); // ✅ Valid with params
i18n.t('invalid.key'); // ❌ TypeScript error
Built-in formatters using native Intl API (zero dependencies):
const i18n = getI18n();
// Numbers
i18n.formatNumber(1234567.89); // "1,234,567.89" (en) / "1.234.567,89" (de)
i18n.formatNumber(0.15, 'percent'); // "15%"
i18n.formatNumber(123456789, 'compact'); // "123M"
// Currency (auto-detects based on locale)
i18n.formatCurrency(99.99); // "$99.99" (en-US) / "99,99 €" (de-DE)
i18n.formatCurrency(99.99, 'EUR'); // "€99.99"
// Dates
i18n.formatDate(new Date()); // "1/15/2024" (en-US) / "15.1.2024" (de)
i18n.formatDate(new Date(), 'full'); // "Monday, January 15, 2024"
// Time
i18n.formatTime(new Date()); // "3:30 PM" / "15:30"
// Relative Time
i18n.formatRelativeTime(-2, 'day'); // "2 days ago"
i18n.formatRelativeTime(3, 'hour'); // "in 3 hours"
// Lists
i18n.formatList(['Apple', 'Banana', 'Orange']); // "Apple, Banana, and Orange"
Pre-built, accessible language switcher component:
<script>
import { LanguageSwitcher } from '@shelchin/svelte-i18n';
import { i18n } from '../app/i18n';
</script>
<!-- Default switcher -->
<LanguageSwitcher {i18n} />
<!-- With custom styling and position -->
<LanguageSwitcher
{i18n}
class="my-custom-class"
position="top-left"
showFlags={true}
showLabels={true}
/>
Shows translation errors during development:
<script>
import { ValidationPopup } from '@shelchin/svelte-i18n';
import { i18n } from '../app/i18n';
</script>
{#if import.meta.env.DEV}
<ValidationPopup {i18n} />
{/if}
Automatically detect locale from URL pathname:
// Supports patterns like:
// /zh/about -> Chinese
// /en-US/products -> American English
// /de-DE/contact -> German
export const load: LayoutLoad = async ({ data, url }) => {
// The url parameter enables pathname locale detection
return await loadI18nUniversal(i18n, data, url);
};
Load translations dynamically for code splitting:
// Option 1: Dynamic imports
async function loadTranslations(locale: string) {
const translations = await import(`../translations/${locale}.json`);
await i18n.loadLanguage(locale, translations.default);
}
// Option 2: Fetch from API
async function fetchTranslations(locale: string) {
const response = await fetch(`/api/translations/${locale}`);
const translations = await response.json();
await i18n.loadLanguage(locale, translations);
}
Libraries can have isolated translations that don't conflict with the app:
// In your library (my-ui-lib)
export const libI18n = createI18n({
namespace: 'my-ui-lib',
translations: {
en: { button: { save: 'Save', cancel: 'Cancel' } },
zh: { button: { save: '保存', cancel: '取消' } }
}
});
// Library translations are automatically namespaced
libI18n.t('button.save'); // Uses "my-ui-lib.button.save" internally
// Libraries automatically inherit app's locale
// When app switches to 'zh', library also switches to 'zh'
Server-side rendering with locale persistence:
// +layout.server.ts
import type { LayoutServerLoad } from './$types';
import { loadI18nSSR } from '@shelchin/svelte-i18n';
export const load: LayoutServerLoad = async ({ cookies }) => {
const locale = cookies.get('i18n-locale') || 'en';
return loadI18nSSR(locale, ['en', 'zh', 'ja']);
};
Handle plural forms correctly for all languages:
// English: 0 = plural, 1 = singular, 2+ = plural
"items.count": "No items | One item | {count} items"
// Polish: Complex plural rules
"items.count": "Brak elementów | Jeden element | {count} elementy | {count} elementów"
// Usage
i18n.t('items.count', { count: 0 }); // "No items"
i18n.t('items.count', { count: 1 }); // "One item"
i18n.t('items.count', { count: 5 }); // "5 items"
Dynamic values in translations:
// Basic interpolation
"welcome": "Welcome {name}!"
i18n.t('welcome', { name: 'John' }); // "Welcome John!"
// Nested values
"user.greeting": "Hello {user.firstName} {user.lastName}"
i18n.t('user.greeting', {
user: { firstName: 'John', lastName: 'Doe' }
}); // "Hello John Doe"
// Custom interpolation markers
const i18n = createI18n({
interpolation: {
prefix: '{{',
suffix: '}}'
}
});
// Now use: "welcome": "Welcome {{name}}!"
Catch translation issues during development:
const i18n = createI18n({
translations,
validateInDev: true, // Enable validation
validateOptions: {
checkInterpolation: true, // Verify {variables} match
checkPluralization: true, // Verify plural forms
checkHTML: false, // Allow HTML in translations
checkMissing: true, // Report missing keys
checkExtra: true // Report extra keys
}
});
// Shows validation popup in development with errors
Set up i18n in your project interactively:
npx svelte-i18n init
This will:
Scan your code and extract all translation keys:
# Extract from source code
npx svelte-i18n extract ./src ./translations/template.json
# Specify file extensions
npx svelte-i18n extract ./src ./translations/template.json js ts svelte
Check for missing or extra keys across all locales:
# Basic validation
npx svelte-i18n validate ./translations
# Strict validation (exit with error code)
npx svelte-i18n validate ./translations --strict
# Use specific base locale
npx svelte-i18n validate ./translations --base zh
Generate type definitions for translation keys:
# Generate for app translations (default)
npx svelte-i18n generate-types
# Custom paths
npx svelte-i18n generate-types \
--dir ./translations \
--out ./src/types/i18n.ts \
--locale en
# Skip validation of other locales
npx svelte-i18n generate-types --no-validate
createI18n<TPath>(config)
Creates a typed i18n instance.
const i18n = createI18n<TranslationPaths>({
translations, // Translation data
defaultLocale: 'en', // Default locale
fallbackLocale: 'en', // Fallback for missing translations
namespace: 'app', // Namespace (for libraries)
isMain: true, // Is main app instance?
validateInDev: true, // Enable dev validation
interpolation: {
// Interpolation options
prefix: '{',
suffix: '}'
}
});
i18n.t(key, params?)
Get translated text with optional interpolation.
i18n.t('welcome', { name: 'John' }); // "Welcome John!"
i18n.t('items.count', { count: 5 }); // "5 items"
i18n.setLocale(locale)
Change the current locale (async).
await i18n.setLocale('zh'); // Switch to Chinese
i18n.setLocaleSync(locale)
Change locale synchronously (for SSR).
i18n.setLocaleSync('zh'); // Immediate switch
i18n.loadLanguage(locale, translations)
Dynamically load translations.
await i18n.loadLanguage('ja', japaneseTranslations);
i18n.locale; // Current locale ('en')
i18n.locales; // Available locales (['en', 'zh', 'ja'])
i18n.isLoading; // Loading state (true/false)
i18n.errors; // Validation errors (dev only)
i18n.meta; // Language metadata (direction, native name, etc.)
loadI18nUniversal(i18n, data, url?, options?)
Universal load function for +layout.ts.
await loadI18nUniversal(i18n, data, url, {
storageKey: 'i18n-locale', // localStorage key
cookieName: 'i18n-locale', // Cookie name
defaultLocale: 'en', // Default locale
detectFromPath: true // Detect from URL path
});
loadI18nSSR(locale, locales, options?)
Server-side load function for +layout.server.ts.
loadI18nSSR('en', ['en', 'zh'], {
cookieName: 'i18n-locale'
});
setupI18nClient(i18n, data, options?)
Synchronous client setup for +layout.svelte.
const result = setupI18nClient(i18n, data, {
defaultLocale: 'en',
restoreFromStorage: true
});
initI18nOnMount(i18n, data, options?)
Async initialization in onMount.
await initI18nOnMount(i18n, data, {
initFunction: async (i18n) => {
// Custom initialization
}
});
All formatters are locale-aware and reactive:
formatNumber(value, style?, options?)
formatCurrency(value, currency?, options?)
formatDate(date, style?, options?)
formatTime(date, style?, options?)
formatRelativeTime(value, unit, options?)
formatList(items, style?, options?)
// Detect browser language
detectBrowserLanguage(); // 'en-US'
// Validate translation schema
validateSchema(translations, options);
// Merge translation objects
mergeTranslations(target, source);
// Get available locales from registry
getAvailableLocales(registry);
// Check if locale is available
isLocaleAvailable(registry, 'zh');
interface I18nConfig {
// Basic
defaultLocale?: string; // Default: 'en'
fallbackLocale?: string; // Default: same as defaultLocale
supportedLocales?: string[]; // Auto-detected if not set
// Features
validateInDev?: boolean; // Default: true
loadingDelay?: number; // Default: 200ms
namespace?: string; // Default: 'app'
isMain?: boolean; // Default: true for 'app'
// Formatting
interpolation?: {
prefix?: string; // Default: '{'
suffix?: string; // Default: '}'
escapeValue?: boolean; // Default: false
};
pluralization?: {
separator?: string; // Default: '|'
};
// Validation
validateOptions?: {
checkInterpolation?: boolean;
checkPluralization?: boolean;
checkHTML?: boolean;
checkMissing?: boolean;
checkExtra?: boolean;
};
}
# .env
VITE_I18N_DEFAULT_LOCALE=en
VITE_I18N_FALLBACK_LOCALE=en
VITE_I18N_SUPPORTED_LOCALES=en,zh,ja,de,fr
VITE_I18N_DEBUG=true
src/
translations/
en.json # English (base)
zh.json # Chinese
ja.json # Japanese
locales/ # Alternative structure
en/
common.json
errors.json
forms.json
Always generate and use types:
// Generate types after translation changes
npm run i18n:types
// Import and use
import type { I18nPath } from '$lib/types/i18n-generated';
export const i18n = createI18n<I18nPath>({ ... });
{#if i18n.isLoading}
<LoadingSpinner />
{:else}
<Content />
{/if}
// ❌ Don't import all translations statically
import * as allTranslations from './translations';
// ✅ Import only needed or use dynamic imports
import en from './translations/en.json';
const zh = await import('./translations/zh.json');
// Run validation in CI/CD
npm run i18n:validate
// Test with different locales
npm run dev -- --locale=zh
We welcome contributions! Please see our Contributing Guide for details.
# Clone the repository
git clone https://github.com/atshelchin/svelte-i18n.git
# Install dependencies
pnpm install
# Start development server
pnpm dev
# Run tests
pnpm test
# Build library
pnpm build
MIT © Shelchin
Built with ❤️ using:
Special thanks to all contributors who helped make this project better!
Documentation • Live Demo • Examples • Report Bug
Made with ❤️ by Shelchin