Data
Import & Export
Upload existing translation files to get started quickly, or download your translations in the format your application needs — through the UI or the REST API. One import covers one language, and as many files as that language is split across.
Supported formats
Mergua works with JSON files — the standard format used by Angular/Transloco and i18next. Both flat and nested key structures are supported and auto-detected. Two others sit beside it for jobs plain JSON cannot do: ARB, which carries your context notes and character limits through a round trip, and XLIFF 2.0, for the handoff to a translation agency. Both are read as well as written; the files your app loads at runtime stay JSON.
Nested JSON
{
"common": {
"save": "Save",
"cancel": "Cancel"
}
}Flat JSON
{
"common.save": "Save",
"common.cancel": "Cancel"
}ARB (with descriptions)
Plain JSON carries only keys and values, so a download loses the context notes and max lengths your team maintains in the editor. Use ARB (Application Resource Bundle) when you want them to travel with the file. ARB is JSON with @key metadata entries, and Mergua supports it for both import and export — so notes survive a full round-trip.
ARB file
{
"@@locale": "fr",
"@@last_modified": "2026-08-14T09:30:00.000Z",
"common.save": "Enregistrer",
"@common.save": {
"description": "Primary button",
"x-maxLength": 12,
"source_text": "Save",
"type": "text"
}
}How it maps
@key.description→ the key's context note@key.x-maxLength→ the key's max length (ARB has no standard slot for this, so Mergua uses a custom attribute)@key.source_text→ the original this was translated from, written for every language except your source one. Bring it back on import and the preview names the keys whose original has changed since.@@last_modified→ when the exported values last changed. Taken from the data, not from the time of export, so pulling twice gives you the same file.- Attributes Mergua does not edit — Flutter's
placeholders,screenshot, anything else in your file — are kept as they are and written back out, up to 4 KB per key. - Imports are additive — a file without metadata never removes notes you already have.
Import: drop a .arb file into the import dialog. Export: pick ARB in the download dialog, use the downloads in the editor's ⋮ menu, or ask the API below for ?format=arb. For the files your app loads at runtime, stick to plain JSON — ARB is for round-tripping context notes.
XLIFF (agency handoff)
XLIFF 2.0 is the format translation agencies and CAT tools speak. Mergua reads and writes it, and it is the one format here that is not for your app: Transloco cannot load XLIFF at runtime, so it goes out to a translator and comes back in. A document is one language pair — the source language and one target — which is why it is a per-language download and never an all-languages one.
de.xlf
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0"
xmlns:slr="urn:oasis:names:tc:xliff:sizerestriction:2.0"
version="2.0" srcLang="en" trgLang="de">
<file id="f1" original="common.json">
<unit id="common.save" name="common.save"
slr:sizeRestriction="12">
<notes>
<note>Primary button.</note>
</notes>
<segment state="final">
<source>Save</source>
<target>Speichern</target>
</segment>
</unit>
<unit id="cart.items" name="cart.items">
<segment state="initial">
<source>{count, plural, one {# item} other {# items}}</source>
</segment>
</unit>
</file>
</xliff>How it maps
srcLang/trgLang→ your project's source language and the language you picked. They sit on the root in 2.0, so one document is one pair.<file original>→ the JSON file the keys belong to. A project split across several gets a<file>per name, inside the one document.<unit name>→ the key.idcarries it too where the format allows; a key with a space in it gets a generated id and keeps its real name beside it.<note>→ the key's context note,slr:sizeRestriction→ its max length in characters.state→ the translation status, both ways: see the table below.- ICU messages travel verbatim. Splitting a plural into one segment per category would be lossy in both directions, since no two languages have the same categories — so your translator sees the ICU syntax as text and leaves it standing.
Status, in both directions
| In Mergua | Written as | Read back as |
|---|---|---|
| No value yet | state="initial", no <target> | Draft |
| Draft | state="translated" subState="mergua:draft" | Draft |
| Translated | state="translated" | Translated |
| Reviewed | state="final" | Reviewed |
Reading falls downwards on purpose: anything Mergua cannot read with confidence — a missing state, a value the spec does not define, or a subState from another tool — arrives as Draft, which is the status a reviewer still walks past. A file that always carries its own subState therefore imports entirely as draft, and one bulk review clears it.
Sending it out, taking it back
Export: pick XLIFF in the download dialog for one language, or use Download XLIFF in the editor's ⋮ menu. You get one de.xlf — even with All files selected, because a file is a <file> inside the document rather than a document of its own. There is no all-languages XLIFF download: a handoff goes to one translator for one language.
That document is for handing over. Bringing translations back is one file per upload, so a multi-file project imports each in turn — the reader refuses a document with several <file> elements by name rather than guessing which keys belong where.
Import: drop the .xlf or .xliff file into the import dialog. A 2.0 document states the language it is for, so the dialog reads it out of the file and shows it rather than asking — the <target> values go into that language, and conflicts work exactly as they do for JSON. The language is still asked where nothing states it: a JSON file, a .json dropped in beside a .xlf, or a document that omits the attribute. Two documents for different languages are one import too many, and the dialog says so over both files.
The source text can come along. A 2.0 document carries both languages, so the dialog offers Also take the source text from the document — the <source> values are then written into your project's source language as well, out of the same file. It is ticked for you when the project has no originals for the file's keys yet, which is the case it exists for: a project fed nothing but XLIFF has no other way to fill that column, because the same file cannot be uploaded a second time as the source language. An original the project already holds differently is its own conflict with its own keep-or-replace choice, so a translation can be taken while the original is kept. It works on a branch too: there the originals are recorded as their own changes on the source language, so a reviewer approves the translation and the changed original separately.
The reader refuses rather than half-reads, and says which of these it hit: a 1.2 document (what ng extract-i18n writes unless you pass --format=xlf2), one holding more than one <file>, one whose trgLang is not the language being imported, one whose srcLang is not your project's source language while you are taking the source text over, and anything over the upload size limit.
New to handing translations to an agency? The XLIFF handoff, end to end covers what one document carries, the brief that keeps ICU messages intact, and why the version matters.
What the preview tells you
Anything the reader could not map to a field is listed before you commit the import, never swallowed: inline codes the placeholder check cannot read yet, units marked translate="no" (imported all the same — the file says do not translate the value, not that the key does not exist), attributes and foreign elements kept verbatim so the file survives a round trip, and the one loss there is — <file> and <group> attributes, which have nowhere to go because Mergua stores translations per key.
Importing
Drop or pick as many .json and .arb files as one language is split across, or one XLIFF document, which already holds them all. A project that holds several JSON files per language puts each file in its own, named after the file and correctable before the import; a project that holds one file per language puts them all in that one. Either way the language is chosen once, for the whole import.
Import keys (source language)
Upload your source language — a single en.json, or the files it is split across — to import all keys into the project. New keys are added with empty translations for all target languages. Existing keys are preserved — no data is overwritten.
Mergua previews each file on its own before anything is committed.
Import translations
Upload the translations for one language — a single fr.json, or every file that language is split across. Translations are merged into the project. If a key already has a value, you choose how to handle conflicts:
- Keep existing — existing translations stay, only empty keys are filled.
- Use uploaded — uploaded values overwrite existing translations.
Exporting
Download single locale
Download translations for a specific language as a .json file. The file is named by locale code (e.g. fr.json).
A project with several JSON files per language picks which file first — or All files, which arrives as a .zip holding that language's folder. XLIFF is the exception: All files stays one .xlf, since a file is a <file> inside the document.
Download all locales
Download all translations as a .zip archive (UI only). The ZIP mirrors the shape of the project: one JSON file per language, or one folder per language holding its files — either way, ready to drop into your project's i18n folder.
JSON and ARB only. XLIFF is not offered here and the API answers 400 for it, because a document is one language pair — download the language you are handing over.
Format options
Choose between nested and flat JSON output, ARB, or XLIFF for one language at a time. The format is configured per download or via the format query parameter on the API.
API access
Download translations programmatically with the REST API — no pipeline required. Handy for custom build scripts, a quick one-off pull, or wiring Mergua into another tool.
Authentication
Every request needs your project API key in the X-API-Key header. Create one under your project's API Keys settings.
Download a single locale
curl -H "X-API-Key: $MERGUA_API_KEY" \
"https://api.mergua.com/v1/$PROJECT_ID/translations/fr" Returns the locale as JSON. The resolved branch is echoed in the X-Mergua-Branch response header.
Download all locales
Returns a JSON object with one entry per locale (for a ZIP archive, use the UI):
curl -H "X-API-Key: $MERGUA_API_KEY" \
"https://api.mergua.com/v1/$PROJECT_ID/translations"Query parameters
| Parameter | Default | Description |
|---|---|---|
| format | nested | nested or flat JSON output, arb for JSON with @key metadata, or xliff for an XLIFF 2.0 document — single locale only, and XML rather than JSON. Anything else returns 400. |
| branch | main | Mergua branch to pull from |
| namespace | default | Which JSON file to download, for a project that holds several per language. One file per request — the API has no "all files" form. |
# flat JSON from the develop branch
curl -H "X-API-Key: $MERGUA_API_KEY" \
"https://api.mergua.com/v1/$PROJECT_ID/translations/fr?format=flat&branch=develop"
# ARB — same values, plus descriptions and max lengths
curl -H "X-API-Key: $MERGUA_API_KEY" \
"https://api.mergua.com/v1/$PROJECT_ID/translations/fr?format=arb"
# XLIFF 2.0 — XML, saved under the name the header gives it
curl -OJ -H "X-API-Key: $MERGUA_API_KEY" \
"https://api.mergua.com/v1/$PROJECT_ID/translations/fr?format=xliff" ARB works on both download endpoints and on any branch. The response stays application/json — an ARB document is valid JSON, so existing parsers keep working.
XLIFF is the one format that answers with application/xml, and it names the file it is: Content-Disposition: attachment; filename="fr.xlf", the same name the download dialog writes. It works on the single-locale endpoint only — the all-locales one answers 400, since every language at once has no XML shape. The JSON formats name no filename on purpose: they are read by jq in a pipeline, not saved.
Upload translations
Upload translations for a specific locale. The request body is a JSON object with translation keys and values (flat or nested). Add ?namespace= to write into one particular JSON file; without it the values go to default.
JSON and ARB bodies only. XLIFF travels the other way over this API — you download it, hand it over, and bring the document back through the import dialog in the app, which is where the preview of what it changes lives.
curl -X PUT -H "X-API-Key: $MERGUA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"auth.login.title": "Anmelden"}' \
"https://api.mergua.com/v1/$PROJECT_ID/translations/de"Conflict strategy
Control what happens when a translation already exists using the strategy query parameter:
| Strategy | Existing translation | Empty / Draft | New |
|---|---|---|---|
| overwrite | Replaced | Replaced | Created |
| skip | Kept | Kept | Created |
| fill | Kept | Replaced | Created |
Default is overwrite. Use fill to seed missing translations without overwriting work already done by your team.
# Only fill empty/draft translations
curl -X PUT -H "X-API-Key: $MERGUA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"auth.login.title": "Anmelden"}' \
"https://api.mergua.com/v1/$PROJECT_ID/translations/de?strategy=fill"Other endpoints
GET /v1/{projectId}/locales— list locales and the source localeGET /v1/{projectId}/progress— translation progress per locale, optionally for a branch via?branch=…GET /v1/{projectId}/branches?name=…— check whether a branch exists
Want this to run automatically on every push (and commit the files back)? Use the Pipeline Sync script instead — it wraps these endpoints with branch matching, key sync and merge handling.