Svelte · svelte-i18n
The dictionaries your app loads, merged like code and synced in CI.
svelte-i18n builds a dictionary per language out of the JSON files you register, and formats every string as ICU. Mergua reads and writes those files where they are, so the loaders in src/lib/i18n.ts stay as they are and every language gains a branch, a review step and a pipeline that fills it in.
Svelte + svelte-i18n
/for/svelte-i18n
locale files
src/lib/locales
file format
nested
The files you already have
Mergua reads what is in your repo. Nothing is renamed, nothing moves, and no package is added to your project — the sync script is fetched with curl when the pipeline runs.
src/lib/locales/
├── en.json ← source locale
├── de.json
└── fr.json{
"cart": {
"summary": {
"title": "Your cart",
"items": "{count, plural, =0 {Nothing here yet} one {# item} other {{count} items}}",
"checkout": "Checkout"
}
}
}How svelte-i18n reads them
This is svelte-i18n's own setup, not Mergua's — if you already have it, skip the panel. It is here so the page is a working path from empty project to translated screen rather than a page about a product.
<script lang="ts">
import { _ } from 'svelte-i18n';
let { count }: { count: number } = $props();
</script>
<section>
<h1>{$_('cart.summary.title')}</h1>
<p>{$_('cart.summary.items', { values: { count } })}</p>
</section>src/lib/i18n.ts
import { getLocaleFromNavigator, init, register } from 'svelte-i18n';
register('en', () => import('./locales/en.json'));
register('de', () => import('./locales/de.json'));
register('fr', () => import('./locales/fr.json'));
init({
fallbackLocale: 'en',
initialLocale: getLocaleFromNavigator(),
});src/routes/+layout.ts
import { browser } from '$app/environment';
import { locale, waitLocale } from 'svelte-i18n';
import '$lib/i18n';
export const load = async () => {
if (browser) {
locale.set(window.navigator.language);
}
await waitLocale();
};npm i svelte-i18nBack into the build
One flag and one job, in a stage before vite build. Keys you added go up, finished languages come back into src/lib/locales as a commit on the branch the job is running on, and the loaders register them on the next build without a change.
The job runs on the branch it is already on, so a feature branch syncs against the matching Mergua branch and main against main. That is the part no other translation tool does.
And you do not have to run it on a timer: a project can POST to a URL of yours when translations change or a branch is merged, so the job above starts on the change. The delivery is signed, it is filtered to the branch you name, and an import of four hundred keys arrives as one call.
# --path, because the CLI defaults to Angular's src/assets/i18n.
# --format stays nested, the shape the files already have.
curl -sf https://api.mergua.com/v1/mergua.sh | sh -s -- sync --path src/lib/locales.gitlab-ci.yml
mergua-sync:
stage: .pre
image: alpine:latest
before_script:
- apk add --no-cache curl jq git
script:
- curl -sf https://api.mergua.com/v1/mergua.sh | sh -s -- sync --path src/lib/locales# If you would rather own the commit yourself, pull and
# commit in your own step:
curl -sf https://api.mergua.com/v1/mergua.sh | sh -s -- pull --path src/lib/localesPlaceholders
svelte-i18n writes a variable as {count}. Mergua treats it as part of the string: if a translation comes back without it, the validator says so on the way out, and the AI translator is told to carry it through.
"items": "{count, plural, =0 {Nothing here yet} one {# item} other {{count} items}}"$_('cart.summary.items', { values: { count: 3 } })Between the push and the pull
The job above is both ends of one loop, and what happens in between is people. A translation gets written, somebody else approves it, and sometimes a whole language leaves for an agency and comes back — none of which touches a file in your repo until the next pull.
Reviewed before it reaches the dictionary
svelte-i18n merges what each loader returns into one dictionary per language, and $_ reads whatever is in it — there is no second check between the file and the screen. A reviewer approves or rejects one key at a time with the reason attached, a review request mails everyone who could answer it, and nothing unreviewed merges, so the dictionary main loads has been read by a second person.
How a key gets from draft to reviewedXLIFF for the agency, JSON for the dictionary
A translation agency gets a language as one XLIFF 2.0 document and sends it back into the same keys. svelte-i18n never registers it and the sync script does not carry it, deliberately: the document is the handoff, and src/lib/locales keeps the JSON your dictionary is built from.
What travels in the document and what does notWorth knowing on Svelte
Values go inside values
svelte-i18n takes interpolation as { values: { count } }, not as the second argument itself. Pass { count } directly and the message is formatted with nothing to fill in. Mergua's placeholder check guards the translation, not the call site, so this one is yours to get right.
Wait for the locale before the first render
register() loads a language only when it is asked for, so the layout awaits waitLocale() before anything reads $_. Nothing in the sync changes that: the CLI writes the files before the build, and the loaders import them as they find them.
Nested or flat, pick one
svelte-i18n resolves cart.summary.title as a literal key and as a path through nested objects, so both formats load. The pull merges onto the file already in the repo, though, so --format has to match what is there — nested by default, --format flat for a repo that writes its keys out in full.
Several files per language
A Mergua project holds a folder per language, src/lib/locales/en/checkout.json, and svelte-i18n accepts several loaders for one language. It merges them all into one tree, though, so give each file its own top-level key — checkout.json holding a checkout object — or two files will answer for the same key.
Splitting a language across several filesA different stack?
Mergua writes plain locale files, so anything that loads one works. These stacks have a page of their own as well.
Run it on one branch.
Upload the src/lib/locales/en.json you already have and see the whole loop on the free tier.
Create your account