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.

01

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.json

One 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.

02

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 stack

src/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`,
  );
}
03

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.

01

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.

checkout.json · title common.json · title
02

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.

en/ common checkout errors fr/ common errors
03

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.

feature/express-checkout → checkout.json +11 → errors.json +3

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.

04

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.

file you uploadlands in
common.jsoncommon
checkout.jsoncheckout
de.jsondefault — a language, not a file
pt-BR.jsondefault
What working across those files looks like, on the feature tour
05

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
Every flag, and the migration off a single file
# 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
06

When 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 follow

Merging 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.offline

Choosing 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.

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