A non-invasive compile-time internationalization (i18n) system for Svelte. Inspired by Lingui, built from scratch with performance, clarity, and simplicity in mind.
π― Smart translations, tiny runtime, full HMR. Extract strings at build time, generate optimized translation catalogs, support live translations (even with Gemini AI), and ship minimal code to production.
Traditional i18n solutions require you to wrap every translatable string with
function calls or components. wuchale
doesn't.
<!-- Traditional i18n -->
<p>{t('Hello')}</p>
<p><Trans>Welcome {userName}</Trans></p>
<!-- With wuchale -->
<p>Hello</p>
<p>Welcome {userName}</p>
Write your Svelte code naturally. No imports, no wrappers, no annotations.
wuchale
handles everything at compile time.
Try live examples in your browser, no setup required:
- π§ Zero-effort integration - Add i18n to existing projects without rewriting code
- π Compile-time optimization - All transformations happen during build, minimal runtime overhead
- π Full, granular HMR support - Live updates during development, including auto-translation
- π¦ Tiny footprint - Only 2 additional dependencies (
wuchale
+pofile
), no bloatednode_modules
- π― Smart extraction - Uses AST analysis: handles nested markup, conditionals, loops, and complex interpolations
- π Standard .po files - Compatible with existing translation tools and workflows
- π€ Optional AI translation - Gemini integration for automatic translations during development
- β‘ Svelte 5 ready - Built for the future with runes and snippets support
npm install wuchale
// vite.config.js
import { svelte } from '@sveltejs/vite-plugin-svelte'
import { wuchale } from 'wuchale'
export default {
plugins: [
wuchale(),
svelte(),
]
}
Create wuchale.config.js
in your project root:
// @ts-check
import { defineConfig } from "wuchale"
export default defineConfig({
locales: {
// English included by default
es: { name: 'Spanish' },
fr: { name: 'French' }
},
})
mkdir src/locales
// src/routes/+layout.js
import { setTranslations } from 'wuchale/runtime.svelte.js'
export async function load({ url }) {
const locale = url.searchParams.get('locale') ?? 'en'
// or you can use [locale] in your dir names to get something like /en/path as params here
setTranslations(await import(`../locales/${locale}.svelte.js`))
return { locale }
}
<!-- src/App.svelte -->
<script>
import { setTranslations } from 'wuchale/runtime.svelte.js'
let locale = $state('en')
async function loadTranslations(locale) {
setTranslations(await import(`./locales/${locale}.svelte.js`))
}
</script>
{#await loadTranslations(locale)}
<!-- @wc-ignore -->
Loading translations...
{:then}
<!-- Your app content -->
{/await}
Write your Svelte components naturally. wuchale
will extract and compile translations automatically:
<h1>Welcome to our store!</h1>
<p>Hello {userName}, you have {itemCount} items in your cart.</p>
For full usage examples, look inside the examples directory.
- Extract - AST traversal identifies translatable text
- Transform - Text nodes replaced with
wuchaleTrans(n)
calls - Catalog - Updates .po files with new/changed messages
- Translate - Optional Gemini AI translatio 8000 n for new messages
- Compile - Generates optimized JavaScript modules
- Bundle - Vite handles HMR in dev, optimized builds for production
All text inside elements is extracted by default:
<p>This is extracted</p>
<!-- @wc-ignore -->
<p>This is not extracted</p>
Text attributes starting with upper case letters:
<img alt="Profile Picture" class="not-extracted" />
Capitalized strings in specific contexts:
// In $derived or functions
const message = $derived('This is extracted')
const lowercase = $derived('not extracted')
// Force extraction with comment
const forced = $derived(/* @wc-include */ 'force extracted')
<p title={'Extracted'}>{/* @wc-ignore */ 'Ignore this'}</p>
Complex nested structures are preserved:
<p>Welcome to <strong>{appName}</strong>, {userName}!</p>
Extracted as:
Welcome to <0/>, {0}!
Define your function
// in e.g. src/utils.js
export function plural(num, candidates, rule = n => n === 1 ? 0 : 1) {
const index = rule(num)
return candidates[index].replace('#', num)
}
Use it
<script>
import {plural} from '/src/utils.js'
let itemCount = 5
</script>
<p>{plural(itemCount, ['One item', '# items'])}</p>
Disambiguate identical texts:
<!-- @wc-context: navigation -->
<button>Home</button>
<!-- @wc-context: building -->
<span>Home</span>
Enable Gemini translations by setting GEMINI_API_KEY
:
GEMINI_API_KEY=your-key npm run dev
src/
βββ locales/
β βββ en.po # Source catalog (commit this)
β βββ en.svelte.js # Compiled data module (gitignore)
β βββ es.po # Translation catalog (commit this)
β βββ es.svelte.js # Compiled data module (gitignore)
βββ App.svelte # Your components
export default {
// Source language code
sourceLocale: 'en',
// Available locales with plural rules
locales: {
en: {
name: 'English',
// the number of plurals in the language
nPlurals: 2,
// The expression to use to decide which candidate to choose when using your plural() function
// The number should be used as 'n' because this will be the body of an arrow function with n as an argument.
pluralRule: 'n == 1 ? 0 : 1'
}
},
// Where to store translation files
localesDir: './src/locales',
// Files to scan for translations
// You can technically specify non svelte js/ts files, but they would not be reactive
files: ['src/**/*.svelte', 'src/**/*.svelte.{js,ts}'],
// Custom extraction logic
// signature should be: (text, details: {scope, element, attribute}) => boolean
heuristic: defaultHeuristic,
// Your plural function name
pluralFunc: 'plural',
// Enable HMR updates during development. You can disable this to avoid the small overhead
// of live translation updates and work solely with the source language.
// HMR is highly optimized -- it updates only the affected components,
// preserving application state and avoiding full reloads.
hmr: true,
// Gemini API key (or 'env' to use GEMINI_API_KEY)
// if it's 'env', and GEMINI_API_KEY is not set, it is disabled
// set it to null to disable it entirely
geminiAPIKey: 'env'
}
Contributions are welcome! Please check out our test suite for examples of supported scenarios.
Thank you @hayzamjs for sponsoring the project and using it in Sylve, giving valuable feedback!