Guide · Locale files
Splitting translations across multiple files per language
Every serious i18n library lets you break a locale into more than one JSON file. Transloco calls it a scope, i18next calls it a namespace, vue-i18n simply lets you import several. Making the split is an afternoon. Keeping every language on the same set of files after it, and reviewing a change that lands in two of them, is the part that is not written down anywhere — so this page writes it down.
Chapters 01 to 03 are about the practice and apply whatever you use to manage it. Chapter 04 onwards is how Mergua handles it.
Where the one big file runs out
A project starts with en.json, and for a while that is right. What ends it is rarely the size on disk — it is that one file has one owner, one review, and one download, while the app it feeds has grown several teams and a router that loads screens on demand.
One file per language
src/assets/i18n/
├── en.json ← every string in the app
├── de.json
└── fr.jsonOne file per area, per language
src/assets/i18n/
├── en/
│ ├── common.json
│ ├── checkout.json
│ └── errors.json
├── de/
│ ├── common.json
│ ├── checkout.json
│ └── errors.json
└── fr/
└── …The bundle
A route that shows a cart does not need the strings for the admin panel. Splitting the locale is what lets the loader fetch a screen’s share of it instead of all of it.
The conflicts
One file that four teams append to is one file that four branches touch at the bottom. Split by area and a merge conflict becomes rare rather than routine.
The ownership
Legal text, error messages and marketing copy get reviewed by different people. As one file they get reviewed by whoever opened the pull request.
What your library already expects
The three loaders below want the same thing on disk and call it three different names. That naming is the whole reason this is hard to search for — and the reason a tool can treat all three the same, because what it sees is a folder of files either way.
Note what the split does not change: the key syntax, the placeholder syntax and the nesting inside each file all stay exactly as they were. Only the boundary between files is new, and only the loader has to know about it.
The full setup, per stacksrc/app/transloco-loader.ts
// Transloco calls a file a scope, and hands the loader
// "checkout/en". The file is en/checkout.json, so the
// loader swaps the two segments.
getTranslation(path: string) {
const [scope, lang] = path.includes('/')
? path.split('/')
: ['default', path];
return this.http.get<Translation>(
`/assets/i18n/${lang}/${scope}.json`,
);
}src/i18n.ts
// i18next calls a file a namespace. It loads the ones you
// name and looks a key up in the one you ask for.
i18next.use(HttpBackend).init({
lng: 'en',
ns: ['common', 'checkout', 'errors'],
defaultNS: 'common',
backend: { loadPath: '/locales/{{lng}}/{{ns}}.json' },
});
t('checkout:summary.title');src/i18n.ts
// vue-i18n has no name for it — you compose the message
// object yourself, so the files are whatever you import.
import common from './locales/en/common.json'
import checkout from './locales/en/checkout.json'
createI18n({
locale: 'en',
messages: { en: { common, checkout } },
})Three things that break, in this order
None of them shows up on the day you split the file. They show up on the day a second language is added, or a fourth developer joins, or a translator hands back a folder.
The same key name in two files
Every file gets its own “title”, its own “save”, its own “error”. That is fine — they are different keys — until something flattens the files into one bag of keys on the way to a translator, and one of them wins.
Languages drifting out of step
A new file is added in English and, three sprints later, still is not in French. Nothing complains at build time; the screen renders the key name at runtime, in production, to the one language nobody on the team reads.
A change that spans two files
One feature adds strings to checkout and errors at once. Reviewed as two files it is two half-changes, and whichever half ships first ships against strings that are not there.
A spreadsheet survives the first of these and loses to the second. What the rest of this page describes is Mergua — how the split stays intact from your repo to a translator and back, and what it does when two of these files have to become one.
A project that holds several files
A Mergua project says once, in its settings, whether it keeps one file per language or several. That setting — not whichever files happen to have been uploaded — is what every view switches on, so a single mistyped upload cannot turn a one-file project into a multi-file one behind your back.
In multi-file mode the file’s own name is the file it lands in, with one exception that is worth knowing before you upload: a name that is nothing but a locale code names a language, not a file. So de.json is German, while checkout.json is a file called checkout — and the import dialog shows you the answer for every picked file before anything lands, so a file that should carry a different name gets corrected there.
On the way out, one file comes back as that file. Ask for all of them and the archive is laid out {locale}/{file}.json — the same shape the loader in chapter 02 already reads, and the same one the CLI writes.
What the CLI does with the folder
Nothing you have to configure. The sync script looks at the directory it is pointed at: locale subdirectories with JSON files inside them means multi-file, a flat en.json next to de.json means single. It pushes the keys of every file, pulls back what has been translated, and writes each file to the path it came from.
Two constraints come with that, and both are worth knowing before you restructure: one layout is understood, and a project is in one mode or the other, never both at once.
{locale}/{file}.json{file}/{locale}.json{locale}.{file}.json{file}.{locale}.json# No flag for the split: the layout is what says
# which mode you are in.
curl -sf https://api.mergua.com/v1/mergua.sh | sh -s -- sync.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# Narrowing to a single file — for a partial deploy or
# while testing. Everything else stays untouched.
curl -sf https://api.mergua.com/v1/mergua.sh | sh -s -- pull \
--path src/assets/i18n --namespace checkoutWhen the split turns out to be wrong
It usually does, once. A file named after a team that no longer exists, two files that were always the same file, a name that was a typo the first time and has been copied into four languages since. Both moves are in the project’s settings, and both are careful with the thing a restructure normally costs you — the history.
Renaming one
The keys keep their identity — only the file they are filed under changes. Their translations, their comments, their history and any branch still working on them stay attached, because nothing was recreated under a new name.
common → shared
148 keys keep their id
history, comments and branch work followMerging two
Keys only one file holds move across and nothing else about them changes. Keys both files hold — the title problem from chapter 03 — stop the merge until you say which version survives. There is no default: a merge that quietly picked one would be the silent overwrite this exists to prevent.
errors → common
33 keys move
2 keys exist in both:
validation.required
network.offlineChoosing the incoming version writes it through the same history an ordinary edit writes, so the key that survives can still show where its text came from and when. And if one of the clashing keys has unmerged work on a branch, the merge stops and names the branch instead of retiring a key somebody is still editing.
Reading on
This page is the problem and the shape of the answer. The product tour shows the same split inside the editor, the CLI reference has every flag, and the three stack guides carry the full setup for the loader you actually use.
The feature tourThe same split in the editor — the file filter, the badges, the download dialog. The CLI referenceEvery flag, the CI jobs for GitLab, GitHub and Gitea, and the migration path. Angular & TranslocoLoader, config and the pipeline job for a Transloco project. React & i18nextThe same, for i18next — and for react-intl’s flat message ids.
Upload the folder you already have.
Every file, every language, in one pass — and the free tier is enough to see all of it come back out in the same shape.
Create your account