{"id":292,"date":"2026-06-11T15:16:43","date_gmt":"2026-06-11T15:16:43","guid":{"rendered":"https:\/\/www.forethought-studio.com\/translate\/json\/"},"modified":"2026-06-11T18:13:39","modified_gmt":"2026-06-11T18:13:39","slug":"json","status":"publish","type":"page","link":"https:\/\/www.forethought-studio.com\/translate\/json\/","title":{"rendered":"JSON i18n translator that runs in your browser"},"content":{"rendered":"<p class=\"dft-page-lede\">Drop a JSON file in below, pick a target language, download the translated file. The JSON file stays on your device.<\/p>\n<div class=\"dft-mount dft-app\" data-format=\"json\"><noscript><p class=\"df-noscript-message\">This translator requires JavaScript in your browser.<\/p><\/noscript><section class=\"dft-step dft-step-file dft-skeleton\" data-state=\"active\" aria-hidden=\"true\"><header class=\"dft-step-header\"><span class=\"dft-step-number\">1<\/span><h2 class=\"dft-step-title\">Add your JSON file<\/h2><span class=\"dft-step-hint\">or paste anywhere<\/span><\/header><div class=\"dft-dropzone-wrap\"><div class=\"dft-dropzone\"><div class=\"dft-dropzone-icon\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"48\" height=\"48\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M14 3v4a1 1 0 0 0 1 1h4\"\/><path d=\"M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2z\"\/><path d=\"M12 11v6\"\/><path d=\"M9 14l3-3 3 3\"\/><\/svg><\/div><p class=\"dft-dropzone-primary\">Drop a JSON file anywhere, <span class=\"dft-dropzone-click\">click to choose<\/span>, or paste contents<\/p><\/div><p class=\"dft-dropzone-secondary\">JSON files only. Files stay on your device.<\/p><\/div><\/section><section class=\"dft-step dft-step-language dft-skeleton\" data-state=\"future\" aria-hidden=\"true\"><header class=\"dft-step-header\"><span class=\"dft-step-number\">2<\/span><h2 class=\"dft-step-title\">Choose target language<\/h2><\/header><div class=\"dft-language-body\"><label class=\"dft-target-label\">Translate to<\/label><select class=\"dft-target-select\" disabled aria-hidden=\"true\"><option>Select language<\/option><\/select><\/div><\/section><section class=\"dft-step dft-step-progress dft-skeleton\" data-state=\"future\" aria-hidden=\"true\"><header class=\"dft-step-header\"><span class=\"dft-step-number\">3<\/span><h2 class=\"dft-step-title\">Translating<\/h2><\/header><\/section><section class=\"dft-step dft-step-review dft-skeleton\" data-state=\"future\" aria-hidden=\"true\"><header class=\"dft-step-header\"><span class=\"dft-step-number\">4<\/span><h2 class=\"dft-step-title\">Review<\/h2><\/header><\/section><div class=\"df-loading-overlay\" role=\"status\" aria-live=\"polite\"><span class=\"df-loading-overlay-spinner\" aria-hidden=\"true\"><\/span><span class=\"df-loading-overlay-label\">Loading translator...<\/span><\/div><\/div>\n<h2>How it works<\/h2>\n<p>Drop a JSON file on the page, click to choose one, or paste the contents straight in. The parser reads the file in your browser and walks every string leaf (top-level and nested), then tells you how many entries still need translation. Pick a target language and translation starts immediately. The translated file is reassembled from the same parsed structure so nested objects, key order, and non-string values (numbers, booleans, arrays) all survive the round trip.<\/p>\n<h2>Quality Warning<\/h2>\n<p>Please don&#8217;t rely on the quality of these translations to ship a finalized product. This is just a starting point. It&#8217;s machine translation and there are no guarantees about its quality.<\/p>\n<h2>What we do not do<\/h2>\n<p>The file itself never leaves your device. We do not see what you are translating, we do not store any of the strings, and there is nothing about your file in our logs. The privacy wedge in DropFormat is structural: the JSON parser, the Google Translate caller, and the file writer all run inside your browser tab.<\/p>\n<p>What does leave your device: the individual strings go to <code>translate.googleapis.com<\/code> the same way they would if you pasted them into translate.google.com yourself. We are not in the middle. If your file contains internal product strings you do not want a third party to see, do not translate it here; use an offline tool.<\/p>\n<h2>What it supports<\/h2>\n<p>JSON is the de facto i18n format for JavaScript and TypeScript apps. The most common shapes all work: i18next nested-key bundles, react-intl flat-dotted extracts, vue-i18n nested catalogs, and ad-hoc &#8220;key to string&#8221; maps any team has assembled. The parser walks every leaf string and treats it as a translatable entry; numbers, booleans, arrays, and null values are preserved verbatim and never sent for translation.<\/p>\n<p>Nested objects are recursed. A bundle like <code>{\"common\": {\"save\": \"Save\"}}<\/code> produces one entry at path <code>common.save<\/code>; on download the nested structure is rebuilt under the original key, with insertion order preserved.<\/p>\n<p>Flat dotted keys are taken as-is. A react-intl extract like <code>{\"app.title\": \"Demo\"}<\/code> stays as the single key <code>app.title<\/code>; the dot is not auto-split into a nested <code>app<\/code> object. That round-trip discipline matters: many extractors treat dotted keys as opaque identifiers, and silently splitting them would break the lookup at runtime.<\/p>\n<p>ICU MessageFormat plural values like <code>{count, plural, =0 {No items} other {# items}}<\/code> are first-class. The parser detects pure ICU plural patterns and fans each case out into its own row in the review pane. Translate each case independently; on download the cases are re-assembled into one ICU pattern under the original key. The plural variable name and the case labels (<code>=0<\/code>, <code>one<\/code>, <code>other<\/code>, etc.) are preserved verbatim.<\/p>\n<p>i18next suffix-style plurals (<code>\"item_one\": \"1 item\"<\/code>, <code>\"item_other\": \"{{count}} items\"<\/code>) are translated as independent flat siblings. Each suffix variant gets its own row; there is no implicit cross-key grouping. That keeps the output predictable for the wide variety of suffix conventions in the wild (v3 <code>_plural<\/code>, v21 <code>_one<\/code>\/<code>_other<\/code>, full CLDR <code>_zero<\/code> through <code>_other<\/code>).<\/p>\n<p>Placeholders like <code>{name}<\/code>, <code>{{name}}<\/code>, <code>%s<\/code>, and <code>{0}<\/code> pass through the translator unchanged. The protection layer that masks placeholders before translation and restores them after is the same one used for the PO, XLIFF, and ARB translators.<\/p>\n<p>Not yet: react-intl <code>{defaultMessage, description}<\/code> object wrappers (the wrapper is preserved but its <code>defaultMessage<\/code> is not extracted as a translatable entry in v1), array-as-plural-set patterns from older i18next versions, and JSON Pointer \/ <code>$ref<\/code> indirection.<\/p>\n<p>Flutter ARB files and Apple String Catalog (.xcstrings) files are also JSON-shaped but have their own translators that handle their richer metadata. The classifier here redirects ARB-shaped input to \/translate\/arb\/ and xcstrings-shaped input to \/translate\/xcstrings\/ rather than translating them as generic JSON, so you always end up with a file that round-trips through the right tool.<\/p>\n<p><!-- TOLERANCES:START --><\/p>\n<h2>What we tolerate, and what we reject<\/h2>\n<p>Some malformed files we repair on the way in. Others we refuse to load so we do not make things worse. This list is the parser&#8217;s actual behaviour, generated from the format module itself, not aspirational copy.<\/p>\n<p><strong>Repaired automatically:<\/strong><\/p>\n<ul class=\"dft-tolerance-list dft-tolerance-repairs\">\n<li data-tolerance-id=\"bom-stripped\">UTF-8 byte order mark (BOM) at the start of the file<\/li>\n<li data-tolerance-id=\"crlf-normalized\">Windows (CRLF) or mixed line endings<\/li>\n<li data-tolerance-id=\"trailing-comma\">Trailing JSON commas before } or ]<\/li>\n<\/ul>\n<p><strong>Refused with an explanation:<\/strong><\/p>\n<ul class=\"dft-tolerance-list dft-tolerance-rejects\">\n<li data-tolerance-id=\"JSON_EMPTY\"><strong>Empty file.<\/strong> Add at least one key\/value pair and re-upload.<\/li>\n<li data-tolerance-id=\"JSON_NOT_JSON\"><strong>Does not parse as JSON.<\/strong> Fix the JSON syntax so the file parses, then re-upload.<\/li>\n<li data-tolerance-id=\"JSON_NOT_OBJECT\"><strong>Top-level value is not a JSON object (e.g. an array).<\/strong> Wrap the entries in a top-level object keyed by string ids.<\/li>\n<li data-tolerance-id=\"JSON_NO_ENTRIES\"><strong>No translatable string values found.<\/strong> Confirm at least one leaf value is a string.<\/li>\n<li data-tolerance-id=\"JSON_NOT_I18N\"><strong>Looks like ARB, .xcstrings, or another i18n JSON variant.<\/strong> Use the \/translate\/ hub to pick the format-specific translator.<\/li>\n<\/ul>\n<p><!-- TOLERANCES:END --><\/p>\n<h2>Common questions<\/h2>\n<p><strong>Why is the translation in some cases worse than the Google Translate website?<\/strong> The free endpoint we use is the same one the Google Translate website uses, but the API does not always know about the context the website&#8217;s interface provides. For names, branded terms, or jargon, expect to override in the review pane.<\/p>\n<p><strong>What if I am translating something Google&#8217;s endpoint will not accept?<\/strong> The free endpoint occasionally returns the source unchanged for content it would rather not translate. Those entries show &#8220;kept original&#8221; in the review pane so you can hand-translate just those.<\/p>\n<p><strong>Does it cache anything?<\/strong> No. Each run starts fresh.<\/p>\n<p><strong>Will the output be valid JSON?<\/strong> Yes. The compiler serializes through <code>JSON.stringify<\/code> with 2-space indentation, so the file is always parseable. It will load cleanly in i18next, react-intl, vue-i18n, and any JSON-aware tool.<\/p>\n<p><strong>What happens to non-string values?<\/strong> Numbers, booleans, arrays, and null values pass through untouched. Only string leaves are sent for translation. That keeps mixed bundles (config + i18n, or bundles with version stamps and feature flags) safe to drop in.<\/p>\n<p><strong>What about my nested key order?<\/strong> Top-level and nested insertion order are both preserved. The output keys appear in the same order as the input. Reviewers who eyeball diffs by file order see only translation changes, not key reshuffles.<\/p>\n<h2>Why this exists<\/h2>\n<p>Most i18n libraries expect you to either pay for a managed translation service or run a CLI against your local files. This tool sits in between: free, browser-based, no upload, designed for the &#8220;I just need a fast first pass on this JSON bundle&#8221; workflow. Drop the .json, pick the language, get back a translated file in the same shape.<\/p>\n<h2>What is JSON i18n?<\/h2>\n<p>JSON i18n is the umbrella term for any JSON file used to hold a UI&#8217;s translatable strings. Unlike Flutter ARB or Apple&#8217;s .xcstrings catalog, generic JSON i18n has no standard metadata schema: each library picks its own conventions. i18next (the most common library for React, Next.js, Remix, and many Node servers) uses nested-key bundles by default, with suffix-style plurals (<code>_one<\/code>, <code>_other<\/code>). react-intl (FormatJS) uses flat-dotted keys and ICU MessageFormat for plurals and selects. vue-i18n uses nested-key bundles and ICU MessageFormat. Many internal apps roll their own flat key\/value JSON without picking a library. This translator handles all of those shapes by being permissive about structure and explicit about what it will and will not transform on round-trip.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Drop a JSON file in below, pick a target language, download the translated file. The JSON file stays on your device. How it works Drop a JSON file on the page, click to choose one, or paste the contents straight in. The parser reads the file in your browser and walks every string leaf (top-level [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":0,"parent":274,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-292","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/www.forethought-studio.com\/convert\/wp-json\/wp\/v2\/pages\/292","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.forethought-studio.com\/convert\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.forethought-studio.com\/convert\/wp-json\/wp\/v2\/types\/page"}],"replies":[{"embeddable":true,"href":"https:\/\/www.forethought-studio.com\/convert\/wp-json\/wp\/v2\/comments?post=292"}],"version-history":[{"count":1,"href":"https:\/\/www.forethought-studio.com\/convert\/wp-json\/wp\/v2\/pages\/292\/revisions"}],"predecessor-version":[{"id":307,"href":"https:\/\/www.forethought-studio.com\/convert\/wp-json\/wp\/v2\/pages\/292\/revisions\/307"}],"up":[{"embeddable":true,"href":"https:\/\/www.forethought-studio.com\/convert\/wp-json\/wp\/v2\/pages\/274"}],"wp:attachment":[{"href":"https:\/\/www.forethought-studio.com\/convert\/wp-json\/wp\/v2\/media?parent=292"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}