How to fix react-i18next missing translation keys from a malformed excel export
- Step 1Confirm your i18next keySeparator setting — If your i18next init uses the default
keySeparator: '.', you need a nested resource (so turnnestedon here). If you setkeySeparator: false, you need flat dotted keys (leavenestedoff). The mismatch between these is the #1 cause of keys rendering literally. - Step 2Drop the source spreadsheet onto the tool — Use the original translation sheet, not the broken JSON. SheetJS reads the first sheet in your browser; the first row is the header. Re-deriving from source avoids re-introducing the formatting bug.
- Step 3Set keyColumn and valueColumn to your headers — Type the exact column names — e.g.
keyColumn: key,valueColumn: en. Matching is case-sensitive. A trailing space the translator left in a key cell is trimmed automatically, fixing one common no-match cause. - Step 4Pick nested to match your config —
nested: truefor default i18next (nested object).nested: falseforkeySeparator: falsesetups (flat dotted keys). This single choice is what makest('home.title')resolve. - Step 5Generate and replace your resource file — Download the JSON (named after the value column) and drop it in as your i18next resource for that language. Re-run the app — keys should now render their values, not the literal key.
- Step 6Regenerate every language the same way — Run once per
valueColumn. Identical key structure across languages means i18next's fallback chain works and missing-key warnings become trustworthy.
Why react-i18next renders the key instead of the value
The mismatch between your resource shape and i18next's keySeparator. The fix column is what to set in this tool.
| Symptom | Likely cause | i18next config | Fix in this tool |
|---|---|---|---|
t('home.title') shows home.title | Flat dotted resource but i18next expects nesting | Default keySeparator: '.' | Set nested: true and regenerate |
t('home.title') shows home.title | Nested resource but separator disabled | keySeparator: false | Set nested: false (flat keys) |
| One key never resolves, others do | Trailing space in the key cell | any | Keys are trimmed automatically — regenerate |
| Half the keys missing after a certain row | Empty-key separator row breaking a hand-built JSON | any | Empty-key rows are skipped — regenerate |
The three options for an i18next resource
The complete option contract. nested is the lever that decides flat vs nested; the other two name your columns.
| Option | Default | For i18next | Effect |
|---|---|---|---|
keyColumn | key | Your key header | Cell values become resource keys (trimmed) |
valueColumn | value | The language column | One language per run → one resource file |
nested | false | true for default i18next, false for keySeparator:false | Splits dotted keys into a tree, or keeps them flat |
Tier limits
Per-file caps for the i18n generator. Debug resources are tiny; the caps matter only for large master sheets.
| Tier | Max file size | Max rows | Files per session |
|---|---|---|---|
| Free | 5 MB | 10,000 | 1 |
| Pro | 50 MB | 100,000 | 5 |
| Pro-media | 200 MB | 500,000 | 20 |
| Developer | 500 MB | unlimited | unlimited |
Cookbook
Each block shows the spreadsheet, the i18next config it targets, and the resource JSON that makes t() resolve. Pick the nested setting that matches your config.
Nested resource for default i18next
With the default keySeparator ('.'), i18next expects nesting. nested: true builds it, and t('home.title') resolves.
Sheet (first tab):
key | en
-------------|------------
home.title | Welcome
home.cta | Sign up
Options: keyColumn=key, valueColumn=en, nested=true
Output → en.json:
{
"home": {
"title": "Welcome",
"cta": "Sign up"
}
}
// i18next default config → t('home.title') === 'Welcome'Flat resource for keySeparator: false
If you deliberately disabled the key separator, i18next treats the whole dotted string as one literal key. Then you need flat output, so leave nested off.
Sheet:
key | en
-------------|------------
home.title | Welcome
home.cta | Sign up
Options: nested=false
Output → en.json:
{
"home.title": "Welcome",
"home.cta": "Sign up"
}
// i18next init: { keySeparator: false } → t('home.title') worksTrailing-space key that never matched
The classic 'works for everyone but me' key. The sheet has 'home.title ' with a trailing space; t('home.title') never matched it. Regenerating trims the key.
Sheet cell (note trailing space):
key | en
---------------|--------
'home.title ' | Welcome
Before: hand-built JSON had "home.title " (space) → t('home.title') missed
After regenerate (key trimmed):
{ "home": { "title": "Welcome" } }
→ t('home.title') === 'Welcome'Separator row that broke a hand-built file
A blank row in the sheet had become a "": "" entry in a manually built JSON, which some loaders choke on. The tool skips empty-key rows, so the resource is clean.
Sheet:
key | en
-------------|----------
login.title | Log in
| ← blank separator
login.error | Wrong password
Output → en.json (no empty key):
{
"login": {
"title": "Log in",
"error": "Wrong password"
}
}Interpolation values pass through
i18next interpolation uses {{var}}. That's part of the value and is written verbatim, so your interpolated strings keep working after regeneration.
Sheet:
key | en
---------------|--------------------------
welcome.user | Welcome back, {{name}}!
cart.items | You have {{count}} items
Output → en.json:
{
"welcome": { "user": "Welcome back, {{name}}!" },
"cart": { "items": "You have {{count}} items" }
}
(i18next {{name}} interpolation preserved)Edge cases and what actually happens
nested setting doesn't match keySeparator
Keys render literallyThis is the core bug. Nested JSON with keySeparator: false, or flat dotted JSON with the default separator, makes i18next return the key string instead of the value. Match nested to your config: true for default i18next, false for keySeparator: false.
Key and parent collide under nesting
Collision — last winsWith nested: true, having both home (a value) and home.title (a child) collides on home. One overwrites the other by row order, so t('home.title') or t('home') will miss depending on which survived. Rename so a key is never both a leaf and a parent.
Duplicate key in the sheet
Last winsTwo home.title rows mean the second value silently overwrites the first. Output key count drops below row count. If the value you see in the app is the 'wrong' one, you likely have a duplicate; dedupe with the duplicate-purge tool.
valueColumn header mismatch
Empty valuesSet valueColumn: EN against a header en and i18next renders blanks (every value is empty string). It looks like a translation bug but it's a column-name typo. Match the header exactly.
Translations on a non-first sheet
Not readOnly sheet 1 is parsed. If your debug copy lives on sheet 2, the tool reads sheet 1 instead and the resource won't contain your keys. Move them to the first sheet or export that sheet as CSV.
Namespaces expected but not in the keys
Expectedi18next namespaces (t('ns:home.title')) come from separate resource files per namespace, not from the colon inside one file. Generate one file per namespace by filtering the sheet, or keep one default namespace. The tool doesn't split on : — only . (and only when nested).
Value contains a real double quote
Escaped automaticallyA translation like He said "hi" is escaped correctly by JSON.stringify (\"). This is exactly why regenerating beats hand-editing — the manual route is where the unescaped quote that breaks the file gets introduced.
Keys not in alphabetical order
By designOutput follows sheet row order; there's no sort. i18next doesn't care about order, but if you want tidy diffs, sort the spreadsheet first. Order has no effect on whether t() resolves.
Pluralisation suffix keys (key_one, key_other)
Supportedi18next plurals use suffixed keys like item_one and item_other. Put each suffix on its own row in the sheet; they're ordinary keys to this tool and pass through unchanged into the resource.
File over the tier limit
RejectedAbove 5 MB or 10,000 rows on Free, the file is rejected before processing. A single-language debug resource is far smaller; only a giant multi-language master would hit this. Split or upgrade if needed.
Frequently asked questions
Why does react-i18next show the key instead of the translation?
Almost always a shape mismatch. If i18next uses the default keySeparator: '.' but your resource is flat dotted keys, the lookup fails and the raw key renders. Regenerate with nested: true to produce the nested object i18next expects. If you disabled the separator, do the opposite — nested: false.
Flat or nested — which do I pick?
Match your i18next config. Default i18next (keySeparator: '.') needs nested, so turn nested on. If your init has keySeparator: false, i18next treats home.title as one literal key, so you need flat output — leave nested off.
One key works for my teammate but not for me — why?
Look for an invisible trailing space in the key cell. home.title in the sheet won't match t('home.title'). This tool trims keys, so regenerating from the source spreadsheet fixes it. The missing-key warning disappears once the trimmed resource loads.
Will my {{name}} interpolation placeholders survive?
Yes. Interpolation markers are part of the value, which is written verbatim — only keys are trimmed. Welcome, {{name}}! round-trips unchanged. The tool doesn't touch i18next's {{ }} syntax.
Does it handle i18next plurals?
Yes, indirectly. i18next plurals are just suffixed keys (item_one, item_other). Put each on its own row; they're regular keys to this tool. It doesn't auto-generate the plural suffixes — your translators add those rows.
Can it create per-namespace files?
Not automatically. i18next namespaces are separate resource files, not a split inside one file. Filter the spreadsheet to one namespace's keys and generate, then repeat per namespace. The tool only nests on . (when nested is on); it doesn't split on the : namespace separator.
I hand-edited my JSON and now it's invalid — does this help?
Yes — regenerate from the source spreadsheet instead of fixing the JSON by hand. JSON.stringify escapes quotes and backslashes correctly, so the unescaped-quote and trailing-comma errors that broke your file can't recur. Replace the broken resource with the freshly generated one.
Why do I have fewer keys than rows?
Empty-key rows are skipped and duplicate keys collapse (last value wins). The metrics show input rows vs output keys. If a key you expected is missing, check for a blank key cell or a duplicate that overwrote it.
Does it read all worksheets?
No — only the first sheet. If your translations are on sheet 2, move them to sheet 1 or export that sheet as CSV. There is no sheet picker in this tool.
Is it free, and is my UI copy safe?
Yes — free tier (5 MB / 10,000 rows) and fully browser-side via SheetJS. Your spreadsheet and the generated resource never leave your machine, so unreleased UI strings stay private while you debug.
My source is a wide sheet with many language columns — fine?
Yes. Run once per language column. If you'd rather reshape it long first, the un-pivot tool turns wide to long, then you generate from one value column.
What other dev-bridge tools help with a React i18n setup?
The same Excel-to-code family includes excel-python-gen for Python data structures (handy for a backend), excel-trpc-router for a typed router, and excel-tailwind-export for rendering a sheet as a styled HTML table. All run browser-side and read the first sheet the same way.
Privacy first
Every JAD Excel tool runs entirely in your browser using SheetJS and ExcelJS. Your spreadsheets, formulas, and data never leave your device — verified by zero outbound network requests during processing.