How to merge multiple json i18n translation files
- Step 1Paste your base locale file into JSON Input 1 — Input 1 is the existing translation resource (e.g. the current
en.json). It must be a single valid JSON object with your nested namespace tree. This is the layer the partials fold into. - Step 2Paste the incoming partial into JSON Input 2 — Input 2 is the partial to merge — a single namespace export, a TMS delivery, or a squad's feature strings. Because inputs merge left-to-right with last-wins, keys here override the base for the same path.
- Step 3Add more partials with Add input — Click Add input for each additional partial. Order is precedence: put the most-authoritative (e.g. final reviewed) file last. Free tier merges 2 inputs; Pro up to 10.
- Step 4Keep Deep merge selected — Deep merge (default) folds nested namespaces recursively so partials combine without losing siblings. Use First wins if a machine-translated file should only supply keys that don't already exist, never overriding reviewed strings.
- Step 5Click Merge Objects — Each file is parsed and folded in order. If a translation file has a JSON syntax error the merge stops with the parser message — fix it and re-run. At least 2 non-empty inputs are required.
- Step 6Verify key coverage, then export — Scan the merged tree for the namespaces you expected. Use Copy or Download (
merged.json, rename to your locale). To find which keys are still missing versus another locale, diff the merged output against it with the json-diff.
Strategy behaviour for translation files
How each strategy resolves a collision on a translation key. 'Namespace object' = both sides are nested key trees; 'Array value' = a value that is a JSON array (rare in i18n; some ICU/plural setups). Inputs fold left-to-right; 'later' = a more-rightward file.
| Strategy | Namespace / nested key tree | String value collision | Array value collision | Best for |
|---|---|---|---|---|
| Deep merge (default) | Recursively merged key-by-key | Later string wins | Replaced by later array | assembling a locale from partials |
| Shallow | Whole later tree replaces earlier | Later string wins | Replaced by later array | swapping an entire top-level namespace |
| First wins | Skipped if namespace already present (no recursion) | Earlier string kept | Skipped if already present | MT only fills untranslated keys |
| Array append | Whole later tree replaces earlier | Later string wins | Concatenated | rare: combining array-valued message lists |
i18n merge patterns
Common localization workflows mapped to the right strategy and input order.
| Pattern | Input order | Strategy | Result |
|---|---|---|---|
| Combine per-namespace files into one locale | common, checkout, errors | Deep merge | single tree with all namespaces |
| Apply a reviewed hand-fix over MT base | MT base, reviewed file | Deep merge | reviewed strings win conflicts |
| Fill only untranslated keys from MT | human file, MT file | First wins | human strings kept; MT fills gaps |
| Layer a sprint TMS delivery onto current locale | current locale, TMS export | Deep merge | new keys added; updated keys overwritten |
Cookbook
Real partial/locale merges from localization pipelines, showing what each strategy produces. All runs are client-side.
Merge two namespace partials into one locale
ExampleEach squad ships its own namespace file. Deep merge folds them into a single resource without either dropping the other's top-level namespace.
Input 1 (common.json): { "common": { "save": "Save", "cancel": "Cancel" } }
Input 2 (checkout.json): { "checkout": { "pay": "Pay now" } }
Strategy: Deep merge
Output (en.json):
{
"common": { "save": "Save", "cancel": "Cancel" },
"checkout": { "pay": "Pay now" }
}Reviewed hand-fix overrides the MT base
ExampleThe base is machine-translated; a reviewer corrected a handful of strings in a small file. Place the reviewed file last so last-wins applies the corrections while keeping every untouched MT string.
Input 1 (mt-base.fr.json):
{ "common": { "save": "Sauver", "cancel": "Annuler" } }
Input 2 (reviewed.fr.json):
{ "common": { "save": "Enregistrer" } }
Strategy: Deep merge
Output:
{
"common": { "save": "Enregistrer", "cancel": "Annuler" }
}
→ save corrected; cancel kept from MT.First-wins: keep human strings, let MT fill gaps
ExampleYou don't want machine translation to overwrite anything a human already translated — only to supply keys nobody has touched. First wins keeps the earlier (human) value and adds only new keys.
Input 1 (human.de.json): { "common": { "save": "Speichern" } }
Input 2 (mt.de.json): { "common": { "save": "Sichern", "cancel": "Abbrechen" } }
Strategy: First wins
Output:
{
"common": { "save": "Speichern", "cancel": "Abbrechen" }
}
→ human 'save' kept; MT 'cancel' fills the gap.Layer a sprint TMS delivery onto the current locale
ExampleYour TMS only exports keys touched this sprint. Deep merge adds the new keys and overwrites updated ones while leaving the rest of the locale untouched.
Input 1 (current en.json):
{ "checkout": { "pay": "Pay", "total": "Total" } }
Input 2 (sprint-delivery.json):
{ "checkout": { "pay": "Pay now" }, "cart": { "empty": "Cart is empty" } }
Strategy: Deep merge
Output:
{
"checkout": { "pay": "Pay now", "total": "Total" },
"cart": { "empty": "Cart is empty" }
}Find missing keys between two locales after merging
ExampleAssemble each locale from its partials, then diff en.json against fr.json to surface untranslated keys — the diff's removed/added lists are your missing-key report.
Step 1 — merge en partials here → en.json Step 2 — merge fr partials here → fr.json Step 3 — paste both into json-diff json-diff output (illustrative): - checkout.pay (in en, missing in fr) - cart.empty (in en, missing in fr) → two keys need French translation.
Errors and edge cases
Real errors and silent failures sourced from each platform's own documentation. Match the wording to the row, fix what the row says to fix.
A later partial overwrites a reviewed string
Last winsUnder Deep merge, if two files define the same key, the later one wins. Place a re-imported MT file before your reviewed file (so reviewed wins), or use First wins to make sure human-reviewed strings are never overwritten. Always confirm the input order matches your intended precedence.
A translation file isn't valid JSON
Parse errorTMS exports and hand-edited locale files sometimes carry trailing commas or smart quotes pasted from a doc. JSON.parse rejects these and the merge stops with the parser message. The json-format-fixer repairs many such slips; the json-validator pinpoints the exact line.
Same key as object in one file, string in another
Type mismatchIf one file has "errors": { "required": "..." } (a namespace) and another has "errors": "Something went wrong" (a flat string), the later value replaces the earlier whole — object-vs-string is a type mismatch under last-wins. Keep the same key shape (always object or always string) across partials, or you'll silently lose the nested keys.
Array-valued message (ICU plural list) is replaced, not merged
By designIf a value is a JSON array, the default deep strategy replaces it with the later array. This is uncommon in i18n (most values are strings), but some setups store plural variants as arrays. Use Array append if those lists should combine — though for most i18next/react-intl projects, leaving values as strings is correct and no array handling is needed.
Flattened dotted keys won't deep-merge
Flat keysIf a file uses flat dotted keys ("checkout.pay": "Pay") instead of nested objects, each dotted string is a single top-level key — deep merge can't fold checkout.pay into a checkout object. Unflatten first with the json-unflattener, merge, then re-flatten if your loader needs the dotted form.
More than 2 partials on free tier
Upgrade requiredCombining three or more namespace files exceeds the free tier's 2-input limit; Pro allows up to 10. On free, merge two partials, copy the result back into Input 1, and add the next partial as Input 2 for a second pass.
Locale file exceeds 2 MB
Size limitLarge monolithic locale files can pass 2 MB; the free tier rejects an oversized input and prompts to upgrade. If you only need to combine a couple of namespaces, paste just those namespaces rather than the whole resource to stay under the limit.
Top-level is an array of message objects
Index mergeSome export formats use an array of { id, message } records. The merger merges objects by key, so arrays merge by index — not by message id. Reshape to an id-keyed object first (e.g. via the json-transposer) before merging translation records.
Frequently asked questions
Does this work with i18next / react-intl / vue-i18n files?
Yes — they all use nested JSON objects, which is exactly what deep merge folds. Paste each namespace or partial as an input and merge them into one resource. The tool doesn't know about library-specific features (it's a generic JSON merge), but the nested-object shape those libraries use merges correctly. If your files use flat dotted keys instead of nested objects, unflatten them first with the json-unflattener.
How do I keep human translations from being overwritten by machine translation?
Use the First wins strategy with the human file as Input 1 and the MT file as Input 2. First wins keeps every key already present (the human strings) and only adds keys the MT file introduces that nobody has translated yet, so no reviewed string is ever overwritten.
Which file wins when two define the same key?
Under Deep merge, the later (more-rightward) input wins for a conflicting key. Put your authoritative file — usually the final reviewed translation — last. If you want the opposite (earliest wins), use First wins.
Will merging drop other namespaces?
Not with Deep merge — it folds each namespace independently, so merging a checkout-only partial leaves common, errors, and every other top-level namespace intact. Namespaces only disappear if you use Shallow (which replaces a whole top-level object) or if an input sets a namespace to a non-object value.
Can I combine all my namespace files at once?
Up to 10 files on Pro; 2 at a time on the free tier. For many namespaces on free, merge two, copy the result back as the base, and keep adding one partial per pass. Pro lets you paste up to ten partials and fold them in a single run.
Are my in-progress translations uploaded?
No. The merge runs entirely in your browser. Unreleased feature strings, brand copy, and draft translations never leave your tab — only an anonymous run count is recorded for signed-in dashboard stats.
How do I find which keys are missing in a locale?
Assemble both locales from their partials here, then diff them with the json-diff. Keys present in the reference locale but absent in the target appear in the diff's removed list — that's your missing-translation report.
What if my files use flat dotted keys instead of nested objects?
Deep merge treats "checkout.pay" as a single literal key, so it won't fold dotted keys into nested objects. Run each file through the json-unflattener to turn dotted keys into nested objects, merge, then re-flatten with the json-flattener if your loader expects the dotted form.
Does it preserve key order?
Keys from the base appear first, then new keys from later inputs are appended in encounter order — standard JavaScript object key ordering. i18n loaders don't depend on key order, so this is cosmetic. If you need keys sorted alphabetically, post-process with a dedicated sorter; this tool doesn't reorder keys.
Can I change the output indentation?
The web tool always outputs 2-space indentation. To re-format the merged locale (4-space, tabs, or minified), pass it through the json-prettifier or json-minifier.
Does this validate ICU message syntax?
No — it only merges the JSON structure and treats values as opaque strings. It won't catch a broken ICU placeholder or an unbalanced brace in a message. Validate message syntax with your i18n library's linter; this tool ensures the JSON itself is well-formed once merged (confirm with the json-validator).
Can I automate locale assembly?
Yes — the merger runs as a runner-local tool on Pro, so unreleased strings stay on your machine, with the same deep/first-wins strategies. A localization pipeline can fold per-namespace partials into a locale on every TMS delivery and diff against the previous build.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.