How to unflatten flat json for i18n translation files
- Step 1Export flat translations from your TMS — Export the locale file from Phrase, Lokalise, or Crowdin as JSON in the flat-key format, where
button.submit.labelis a single key with a string value. - Step 2Keep the Key delimiter as a dot for i18next — Leave the Key delimiter as
.so namespace segments split correctly. Avoid_for i18next files — it would split plural suffix keys likeitem_oneinto unwanted levels. - Step 3Drop or paste the flat locale JSON and run — Drop the
.jsonfile (free tier up to 2 MB) and click Unflatten JSON. The whole conversion happens in your browser; nothing is uploaded. - Step 4Verify plural and interpolation keys — Confirm plural suffixes sit as siblings under the right parent (
item.item_oneis wrong;item_onebesideitem_otheris right) and that{{name}}/ ICU strings are byte-identical to the source. - Step 5Save as the locale file — Download
<name>.nested.jsonand place it atpublic/locales/en/common.json(next-i18next) orsrc/locales/en.json(vue-i18n). The nested structure loads with no code changes. - Step 6Repeat per locale — Each locale is its own flat object — run them one at a time. The nesting is identical across locales because it comes from the keys, not the translated values.
i18n key shapes and how they unflatten
Behaviour from the unflattener logic with the default dot delimiter. Plural and interpolation handling reflects that only keys are split and values are preserved.
| Flat key / value | Output | i18n note |
|---|---|---|
"button.submit.label": "Submit" | { button: { submit: { label: "Submit" } } } | Standard namespace nesting |
"item_one": "# item", "item_other": "# items" | { item_one: "# item", item_other: "# items" } | Plural siblings kept intact (no dot inside the suffix) |
"greeting": "Hello {{name}}" | { greeting: "Hello {{name}}" } | Interpolation placeholder preserved verbatim |
"cart.count": "{count, plural, one {# item} other {# items}}" | { cart: { count: "{count, plural, ...}" } } | ICU MessageFormat string untouched |
"nav.home": "Home" with delimiter _ | { nav.home: ... } would NOT nest | Wrong delimiter — keep . for i18next |
Framework targets and limits
Where the nested output goes, plus the single UI control and free-tier limit.
| Item | Value | Notes |
|---|---|---|
| i18next / next-i18next path | public/locales/<lng>/<ns>.json | Nested output loads as-is |
| vue-i18n path | src/locales/<lng>.json | Nested messages object |
| react-intl | Often kept flat | react-intl can use flat IDs; nest only if your loader expects it |
| Key delimiter | Default ., 1–3 chars | The only UI control |
| Free file size | 2 MB | Pro removes the limit |
Cookbook
Real before/after locale snippets. Values are preserved exactly, which is why placeholders survive.
Phrase flat export back to i18next namespaces
ExamplePhrase exported dot-joined keys. The default delimiter restores the namespace tree i18next expects.
Flat input:
{
"button.submit.label": "Submit",
"button.cancel.label": "Cancel",
"error.auth.invalidToken": "Session expired"
}
Output:
{
"button": {
"submit": { "label": "Submit" },
"cancel": { "label": "Cancel" }
},
"error": { "auth": { "invalidToken": "Session expired" } }
}Plural suffix keys stay as siblings
Examplei18next plural keys use suffixes, not extra nesting. Because the suffix has no dot, the keys remain side by side under the right parent.
Flat input:
{
"cart.item_one": "{{count}} item",
"cart.item_other": "{{count}} items"
}
Output:
{
"cart": {
"item_one": "{{count}} item",
"item_other": "{{count}} items"
}
}Interpolation and ICU strings are preserved
ExamplePlaceholders live in the value, and values pass through untouched, so nothing in the message string is altered.
Flat input:
{
"welcome.greeting": "Hello {{name}}, you have {{count}} messages"
}
Output:
{
"welcome": {
"greeting": "Hello {{name}}, you have {{count}} messages"
}
}Why not to use underscore as the delimiter for i18next
ExampleSetting the delimiter to _ breaks plural suffix keys. Keep . for i18next files.
Key delimiter: _ (WRONG for i18next)
Flat input: { "item_one": "# item" }
Output: { "item": { "one": "# item" } } ← breaks pluralization
Keep delimiter . so item_one stays a single key.Load the nested locale file
ExampleDrop the restored file into your i18n resources directory; no loader change is needed.
// public/locales/en/common.json (the .nested.json output)
// next-i18next picks it up automatically:
import { useTranslation } from 'next-i18next';
const { t } = useTranslation('common');
t('button.submit.label'); // → "Submit"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.
Underscore delimiter on i18next plural keys
Breaks pluralsDelimiter _ splits item_one into { item: { one: ... } }, which i18next will not resolve as a plural. Keep the delimiter as . for i18next exports.
Interpolation placeholder in a value ({{name}})
PreservedPlaceholders sit inside values, which the tool never modifies. Hello {{name}} comes through byte-identical.
ICU MessageFormat string with braces and commas
PreservedICU strings are values, so braces and commas inside them are untouched — only keys are split on the delimiter.
A translated value contains a dot (e.g. an abbreviation)
PreservedOnly keys are split, never values. A value like e.g. stays exactly as written; dots in values are never treated as separators.
Two keys nest into the same parent inconsistently
Last write winsIf both home (a string) and home.title exist, the later key wins. TMS exports rarely do this, but verify if you hand-edited keys.
Numeric segment in a key (steps.0.text)
Object, not arrayRebuilds as { steps: { "0": { text: ... } } }. For ordered string lists i18next normally uses keyed objects anyway, so this is usually fine — confirm your loader expects an object.
Input is an array of locale objects
Not the expected inputUnflatten one locale object per run. Process en, fr, de separately.
Malformed JSON from a manual edit
Parse errorThe input must be valid JSON. Fix trailing commas or unquoted keys with the JSON Format Fixer first.
Locale file over the free 2 MB limit
Blocked on freeLarge bundled locale files may exceed the 2 MB free cap. Upgrade to Pro to remove the limit, or split namespaces into separate files.
Frequently asked questions
Why does i18next need nested JSON instead of flat keys?
i18next resolves t('button.submit.label') by walking a nested object — button, then submit, then label. While i18next has flat-key plugins, the nested object is the default format that works across all loaders and frameworks, so restoring it avoids extra configuration.
Are my interpolation placeholders like {{name}} preserved?
Yes, exactly. Placeholders and ICU MessageFormat strings live in the value, and the tool never modifies values — it only splits keys on the delimiter. Your {{name}} and {count, plural, ...} strings come through byte-identical.
Do i18next plural suffix keys survive unflattening?
Yes, as long as the delimiter is .. A key like item_one has no dot, so it stays a single sibling key next to item_other. Do not set the delimiter to _, or those suffixes will incorrectly split into extra levels.
Can I unflatten all my locale files at once?
No. Process one locale (one flat object) per run. The nesting is identical for every locale because it comes from the keys, so run en, fr, de in turn and save each output.
Is the translation content — including unreleased copy — sent to JAD Apps?
No. Unflattening runs entirely in your browser. Translation strings, locale-specific content, and unreleased copy are never uploaded to JAD Apps servers.
What delimiter should I use for Phrase, Lokalise, or Crowdin exports?
Keep the default . for i18next-style nested namespaces. These platforms typically flatten with dots, and dots avoid clobbering plural suffix keys.
Does react-intl need nested JSON too?
Not always. react-intl commonly uses flat message IDs, so you may not need to unflatten at all. Nest only if your specific message loader expects a hierarchy.
What if a key over-nests because of a delimiter inside a segment?
Choose a delimiter that does not appear inside your key segments, or rename the offending keys first with the JSON Key Renamer before unflattening.
Can I control the output indentation for committing to git?
The output is always 2-space indented, which matches most i18n repos. If you need a different style, run your project's formatter (Prettier) over the saved file afterward.
How do I go the other way — nested locale to flat for the TMS?
Use the JSON Flattener with a dot delimiter to produce the flat keys your TMS imports.
What is the maximum locale file size on the free tier?
2 MB. Pro removes the file-size limit. If a bundled file is too large, split it into per-namespace files.
My nested file loads but a key is missing — what happened?
Check for a last-write-wins collision (e.g. a string home plus a home.title key) and for any numeric segment that became an object key. Validate the final file with the JSON Validator.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.