wuchale
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.
wuchale
?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:
wuchale
+ pofile
), no bloated node_modules
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
// package.json
{
"scripts": {
"extract": "wuchale",
"clean": "wuchale --clean"
}
}
// 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.
wuchaleTrans(n)
callsAll 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
wuchale
uses a built-in heuristic to determine which text fragments to extract. Here's how it works:
markup
(<p>Text</p>
):attribute
(<div title="Info">
):[a-z]
), it is ignored.<path>
, it is ignored (e.g., for SVG d="M10 10..."
attributes).script
(<script>
and .svelte.js/ts
):$derived
or $derived.by
, it is ignored.console.*()
or $inspect()
calls, it is ignored.[a-z]
) or is any non-letter, it is ignored.This heuristic strikes a balance between useful automation and practical exclusion of irrelevant strings.
If you need more control, you can supply your own heuristic function in the
configuration. Custom heuristics can return undefined
or null
to fall back
to the default. For convenience, the default heuristic is exported by the
package.
š” You can override extraction with comment directives:
@wc-ignore
ā skips extraction@wc-include
ā forces extraction
These always take precedence.
A common scenario is needing to prevent string extraction inside functions, but you may not want to modify the global heuristic or litter your code with comment directives. A cleaner approach is to extract constants to the top level, which are ignored by default:
const keys = {
Escape: 'Escape',
ArrowUp: 'ArrowUp',
// ...
};
function eventHandler(event) {
if (event.key === keys.Escape) {
// ...
}
}
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: string, details: object) => boolean | undefined
// details has the following properties:
// scope: "markup" | "attribute" | "script",
// topLevelDef?: "variable" | "function",
// topLevelCall?: string,
// call?: string,
// element?: string,
// attribute?: string,
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!