Stop Editing JSON Translation Files Manually
If you've worked on a multilingual website, you know the drill. Open en.json, find the key you need, make the change, then open fr.json, de.json, ja.json, and repeat for every locale. It's tedious, error-prone, and a terrible use of developer time.
Yet most teams still do it this way. The translation files sit in a locales/ or messages/ directory, and every content update means hand-editing raw JSON. Let's talk about why this is a problem and what to do instead.
The anatomy of i18n JSON files
Most i18n libraries (next-intl, react-i18next, vue-i18n) use a similar structure. Your translation files are JSON objects with nested keys:
{
"common": {
"buttons": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete"
},
"errors": {
"required": "This field is required",
"email": "Please enter a valid email address",
"minLength": "Must be at least {{count}} characters"
}
},
"dashboard": {
"title": "Dashboard",
"welcome": "Welcome back, {{name}}",
"stats": {
"totalUsers": "Total Users",
"activeProjects": "Active Projects",
"pendingReviews": "Pending Reviews"
}
}
}This is fine when your app has ten keys. But real-world apps have hundreds or thousands. A production en.json can easily be 2,000+ lines of deeply nested JSON.
The five most common mistakes
1. Trailing commas
JSON doesn't allow trailing commas, but every developer's muscle memory says otherwise. One misplaced comma and your entire locale file is unparseable:
{
"settings": {
"theme": "Dark",
"language": "English",
}
}Your app won't throw a helpful error. It'll fail silently or crash at runtime with something like SyntaxError: Unexpected token }.
2. Mismatched keys across locales
You add a new feature and update en.json with five new keys. Then you copy-paste to fr.json and start translating. Except you miss one key. Or you accidentally nest it at the wrong depth:
// en.json
{
"checkout": {
"shipping": {
"title": "Shipping Address",
"subtitle": "Where should we deliver?"
}
}
}
// fr.json (oops, "subtitle" is missing)
{
"checkout": {
"shipping": {
"title": "Adresse de livraison"
}
}
}This creates a gap your i18n library might fill with a fallback, or might leave as a blank string in production.
3. Broken interpolation variables
Most i18n libraries use template variables like {{name}} or {count}. When translating, it's easy to accidentally rename the variable, forget a brace, or remove it entirely:
// en.json
"welcome": "Welcome back, {{name}}"
// de.json (wrong variable name)
"welcome": "Willkommen zurück, {{username}}"This renders as a literal {{username}} string in the UI because the variable username was never passed. Only name was.
4. Invalid JSON structure
Large nested JSON files are fragile. One missing bracket collapses the entire tree. Diffs become unreadable when you accidentally re-indent half the file. And merge conflicts in JSON are nightmarish because you can't just accept both changes without validating the structure.
{
"nav": {
"home": "Home",
"about": "About",
"contact": "Contact"
// missing closing brace, everything below is broken
"footer": {
"copyright": "2026 Acme Inc."
}
}5. Stale translations
The English file gets updated regularly. The other locales drift. After a few months, your ja.json is 30 keys behind en.json, and nobody knows which ones are missing without writing a script to diff them.
Why a visual editor changes everything
The root cause of all these problems is the same: developers are editing a data format (JSON) as if it were source code. But translation files aren't code. They're structured content. And structured content deserves a structured editor.
A visual translation editor shows you:
- All locales side by side. You see
en,fr,dein columns. Missing keys are highlighted immediately. No manual diffing. - Validation in real time. Trailing commas, broken brackets, mismatched interpolation variables: caught before you save.
- Nested key navigation. Instead of scrolling through 2,000 lines of JSON, you browse a tree. Click
dashboard.stats.totalUsersand edit the value. - Search and filter. Find the key you need in seconds, across all locales at once.
- Safe saves. The editor writes valid JSON. Always. You can't accidentally break the file structure.
The Git-based approach
Some editors require migrating your translations to a hosted platform or database. That means vendor lock-in, an extra build step, and a deployment pipeline that now depends on a third-party API.
A better approach: keep your translation files exactly where they are, in your Git repo, and layer a visual editor on top. You edit in the UI, preview the changes, and push a pull request. Your existing review workflow stays the same.
This is the approach tools like SkyBlobs take. It connects to your GitHub repository, reads your i18n files (JSON, YAML, or nested key-value formats), and gives you a side-by-side translation editor. Changes go through your normal PR process.
Setting up a better workflow
Here's a practical workflow that eliminates most manual JSON editing mistakes:
1. Organize your translation files consistently
Pick one structure and stick with it. Group by feature, not by component:
locales/
en/
common.json
dashboard.json
checkout.json
fr/
common.json
dashboard.json
checkout.json2. Use a flat key convention for large files
Deeply nested JSON is harder to maintain. Consider flat keys with dot notation:
{
"common.buttons.save": "Save",
"common.buttons.cancel": "Cancel",
"dashboard.title": "Dashboard",
"dashboard.welcome": "Welcome back, {{name}}"
}This makes diffs cleaner, reduces indentation errors, and makes search easier.
3. Add a CI check for missing keys
Write a simple script (or use an existing tool) that compares your locale files and fails the build if keys are missing:
import fs from "fs";
import path from "path";
function getKeys(obj: Record<string, unknown>, prefix = ""): string[] {
return Object.entries(obj).flatMap(([key, value]) => {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === "object" && value !== null) {
return getKeys(value as Record<string, unknown>, fullKey);
}
return [fullKey];
});
}
const localesDir = path.join(process.cwd(), "locales");
const baseLocale = JSON.parse(
fs.readFileSync(path.join(localesDir, "en.json"), "utf-8")
);
const baseKeys = new Set(getKeys(baseLocale));
const locales = fs.readdirSync(localesDir).filter((f) => f !== "en.json");
for (const file of locales) {
const locale = JSON.parse(
fs.readFileSync(path.join(localesDir, file), "utf-8")
);
const localeKeys = new Set(getKeys(locale));
const missing = [...baseKeys].filter((k) => !localeKeys.has(k));
if (missing.length > 0) {
console.error(`${file} is missing ${missing.length} keys:`);
missing.forEach((k) => console.error(` - ${k}`));
process.exit(1);
}
}4. Use a visual editor for day-to-day changes
For any content change (fixing a typo, adding a new string, updating a CTA), use a visual editor instead of opening the JSON file directly. It's faster, safer, and accessible to non-developers on your team.
5. Keep translations in version control
Never move your translations to a platform that owns the data. Your translation files should live in Git, version-controlled alongside your code. This gives you full history, rollback capability, and no vendor lock-in.
The bottom line
Editing JSON translation files by hand is a solved problem. The tooling exists to give you a visual, validated, side-by-side editing experience without leaving your Git workflow.
If your team is still opening JSON files in VS Code and hunting for nested keys across five locale files, it's time to reconsider. Your translations are content, not code. Treat them accordingly.