How to convert an excel key-value sheet to i18n json translation files
- Step 1Lay the sheet out as key + one column per language — Put your message keys in one column (header
keyby default) and each language in its own column (en,fr,de, or full names likeFrench). The tool reads the first sheet only, so keep translations on sheet 1 — extra sheets are ignored, not merged. - Step 2Drop the .xlsx or .csv onto the tool — SheetJS parses the file in your browser. The first row becomes the headers; every following row becomes a key/value pair. Empty cells are read as empty strings, never as
null. - Step 3Set keyColumn and valueColumn to your exact headers —
keyColumndefaults tokeyandvalueColumntovalue. Type the real header text from your sheet — for examplekeyColumn: keyandvalueColumn: French. Matching is by exact header name, soFrenchandfrenchare different columns. - Step 4Decide flat or nested — Leave
nestedoff for a flat object ("home.title": "..."). Turnnestedon to expand dotted keys into nested objects ({ "home": { "title": "..." } }). Choose whichever shape your i18n library loads. - Step 5Generate and download one language's JSON — The tool produces a single JSON file named after the value column —
french.jsonifvalueColumnwasFrench. The metrics panel shows input rows read and output keys written (output keys can be lower than input rows because empty-key rows are skipped and duplicate keys collapse). - Step 6Repeat per language column — To build
en.json,fr.json, andde.json, run the tool three times, changing onlyvalueColumneach pass (English, thenFrench, thenGerman). The key column andnestedsetting stay the same, so every locale file shares an identical key structure.
The three options this tool exposes
The complete option contract from the tool schema. There are no other controls — no sort, no escape toggle, no auto-detect, no multi-language export. These three are everything.
| Option | Type | Default | What it does |
|---|---|---|---|
keyColumn | string | key | Exact header of the column whose cell values become JSON keys. Values are .trim()-ed; rows with an empty key are skipped |
valueColumn | string | value | Exact header of the column whose cell values become the translations (the JSON values). One run reads exactly one value column |
nested | boolean | false | When on, a key containing . is split on every dot into a nested object path. When off, the dotted key is kept verbatim as a flat string key |
Flat vs nested output for the same sheet
Identical input rows, the only difference is the nested flag. Both are valid JSON; pick the shape your library expects.
| Key cell | Value cell | Output with nested: false | Output with nested: true |
|---|---|---|---|
nav.home | Accueil | "nav.home": "Accueil" | "nav": { "home": "Accueil" } |
nav.about | À propos | "nav.about": "À propos" | "nav": { "about": "À propos" } (merged under same nav) |
greeting | Bonjour | "greeting": "Bonjour" | "greeting": "Bonjour" (no dot, stays flat either way) |
home.title (padded) | Bienvenue | "home.title": "Bienvenue" (key trimmed) | "home": { "title": "Bienvenue" } (key trimmed first) |
Tier limits for the i18n generator (excel family)
Per-file limits, enforced before processing. The tool produces one JSON file per run regardless of tier; the batch-file number is the cap on files processed in one session.
| 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
Real before/after for a key-value translation sheet. Each block shows the spreadsheet rows on the left and the exact JSON the tool emits on the right.
Flat JSON from a two-language sheet
The default case: a key column and one value column. You name the value column you want and get a flat object. To also produce the English file, run again with valueColumn: English.
Sheet (first tab):
key | English | French
-------------|-----------|----------
greeting | Hello | Bonjour
farewell | Goodbye | Au revoir
Options: keyColumn=key, valueColumn=French, nested=false
Output → french.json:
{
"greeting": "Bonjour",
"farewell": "Au revoir"
}Nested object from dotted keys
Turn nested on and dotted keys expand into a tree. Keys that share a prefix merge under the same parent object, which is what most modern i18n libraries expect.
Sheet:
key | French
---------------|------------
nav.home | Accueil
nav.settings | Paramètres
footer.legal | Mentions légales
Options: keyColumn=key, valueColumn=French, nested=true
Output → french.json:
{
"nav": {
"home": "Accueil",
"settings": "Paramètres"
},
"footer": {
"legal": "Mentions légales"
}
}Blank separator rows are skipped
Translators often leave an empty row between sections for readability. Because empty keys are skipped, those rows simply vanish from the output — no "": "" entry, no error.
Sheet:
key | French
------------|----------
login | Connexion
| ← blank separator row
signup | Inscription
Options: keyColumn=key, valueColumn=French
Output → french.json:
{
"login": "Connexion",
"signup": "Inscription"
}
(input rows: 3, output keys: 2)Building three locale files from one sheet
There is no single-click multi-language export. You run the tool once per language column, changing only valueColumn. The key structure is identical across all three, which is exactly what you want for parallel locale files.
One sheet, three runs: key | en | fr | de -----------|----------|-----------|---------- save | Save | Enregistrer | Speichern cancel | Cancel | Annuler | Abbrechen Run 1: valueColumn=en → en.json Run 2: valueColumn=fr → fr.json Run 3: valueColumn=de → de.json Drop each into your /locales folder.
Values keep their exact whitespace and characters
Only the KEY is trimmed. The value is written verbatim — leading spaces, interpolation placeholders, and ICU braces all survive untouched, so your {count} and {{name}} placeholders are preserved.
Sheet:
key | French
---------------|----------------------------
welcome.user | Bonjour, {name} !
items.count | Vous avez {count} articles
indented.note | Texte avec espaces
Output → french.json:
{
"welcome.user": "Bonjour, {name} !",
"items.count": "Vous avez {count} articles",
"indented.note": " Texte avec espaces"
}
(value whitespace preserved exactly)Edge cases and what actually happens
Two rows share the same key
Last winsKeys are written into a single object, so if greeting appears on two rows, the second row's value overwrites the first — silently, with no warning. The output key count will be lower than the input row count. If you expected both, the spreadsheet has an accidental duplicate; dedupe it first with the duplicate-purge tool before generating.
keyColumn or valueColumn header doesn't exist
Empty / invalid outputMatching is by exact header text. If you set valueColumn: french but the header is French, every cell reads as empty string, and you get keys mapped to "". Check the header capitalisation and spelling against the first row of the sheet.
Key cell is blank
Skipped by designA row whose key cell is empty (or whitespace-only, which trims to empty) is skipped entirely — neither key nor value reaches the output. This is intentional so separator rows don't pollute the JSON, but it also means a row where the translator forgot the key is dropped without notice.
nested off but keys contain dots
PreservedWith nested: false, a key like home.title stays a literal flat string key "home.title". This is valid and fine for libraries that accept flat dotted keys (some i18next setups). It only becomes a problem if your library expects nesting — then turn nested on.
A key is both a leaf and a parent (nested on)
Collision — last winsWith nested: true, if menu has a value AND menu.home also exists, they collide on the menu path. The nesting logic overwrites a string with an object (or vice versa) depending on row order, so one of them is lost. Rename the conflicting key — menu and menu.home can't coexist in a nested tree.
Translations live on a second sheet
Not readOnly the first sheet is parsed. If your workbook keeps notes on sheet 1 and translations on sheet 2, the tool reads the notes. Move the translation table to the first sheet, or save that sheet as a standalone CSV before uploading.
Number or date cells in the value column
StringifiedCell values are converted with String(...). A numeric 0.5 becomes the string "0.5"; a date cell becomes its displayed string form. JSON values are always strings here — if you need typed values you'll cast them in your app, not in this tool.
File exceeds the tier row or size cap
RejectedA Free-tier file over 5 MB or 10,000 rows is rejected before processing. Split the sheet, trim to one language column, or upgrade. Locale files rarely approach these limits, but a master sheet with thousands of keys across dozens of languages can.
Quotes or backslashes in the value
Escaped automaticallyOutput is built with JSON.stringify, so a value containing a double quote or backslash is escaped correctly (\", \\). You never need to pre-escape the spreadsheet — that is the whole point of generating rather than hand-editing JSON.
Value column is empty for some keys
Empty string keptAn untranslated row (key present, value blank) writes "key": "". The key is kept because it has a key; only the value is empty. This is useful as a checklist of untranslated strings — search the output for : "" to find gaps.
Frequently asked questions
Does this create one file with all my languages at once?
No. Each run reads exactly one value column and produces one JSON file, named after that column (e.g. french.json). To build en.json, fr.json, and de.json, run the tool three times, changing only the valueColumn each time. The key column and nesting setting stay the same, so all three files share an identical structure.
What's the difference between flat and nested output?
Flat (nested: false) keeps dotted keys as literal strings: "home.title": "...". Nested (nested: true) splits on every dot into a tree: { "home": { "title": "..." } }. next-intl and vue-i18n generally expect nested; some i18next configs accept either. Pick the one your library loads.
Why are there fewer keys in my output than rows in my sheet?
Two reasons. Rows with an empty key are skipped, and duplicate keys collapse (last value wins). The metrics panel shows input rows vs output keys so you can spot the gap. If the drop is unexpected, check for blank-key rows or accidental duplicate keys in the spreadsheet.
Does it read all the sheets in my workbook?
No — only the first sheet. There is no sheet picker in this tool. Keep your translation table on sheet 1, or save just that sheet as a CSV before uploading. Other sheets are ignored, not merged.
How do I tell it which columns are the key and the value?
Type the exact header text into keyColumn and valueColumn. Defaults are key and value. Matching is case-sensitive and exact, so French and french are treated as different columns. If you get all-empty values, it's almost always a header-name mismatch.
Are my interpolation placeholders like {count} or {{name}} preserved?
Yes. Only the key is trimmed; the value is written verbatim. ICU placeholders {count}, i18next {{name}}, and HTML in values all pass through untouched. The tool doesn't validate placeholder syntax — that's your library's job at runtime.
Will accented and non-Latin characters survive?
Yes. The output is standard UTF-8 JSON, so Über, Café, 日本語, and emoji round-trip correctly. JSON.stringify keeps them as literal characters (not \u escapes) which every modern i18n loader reads fine.
Does it sort the keys alphabetically?
No. Keys appear in the order they're encountered in the sheet (top to bottom). There is no sort option. If you want sorted locale files for cleaner diffs, sort the rows in the spreadsheet before generating, or sort the JSON afterward in your editor.
Can I use it for free, and where does my data go?
Yes — the i18n generator is on the free tier (5 MB / 10,000 rows per file). Processing is 100% in your browser via SheetJS; the spreadsheet and the generated JSON never leave your machine. That's the safe choice for unreleased product copy and brand strings.
My spreadsheet is CSV, not xlsx — does that work?
Yes. The tool accepts both .xlsx and .csv. A CSV is treated as a single sheet, which sidesteps the first-sheet-only limitation entirely. Make sure the first row is your header row with the key and value column names.
Can I generate from a wide sheet that has many language columns?
The tool reads one value column per run, so a wide sheet works — you just run it once per language. If your sheet is so wide you'd rather reshape it to a long key/lang/value layout first, the un-pivot tool converts wide to long, then you generate from the single value column.
Where does this fit alongside the other dev-bridge tools?
It's the localisation member of the Excel-to-code family. Sibling generators include excel-python-gen for Python data structures, excel-tailwind-export for HTML tables, and excel-trpc-router for typed routers. All run browser-side and read your 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.