DEV Community

Julian
Julian

Posted on

I Automated JSON i18n Translation After Hitting the Same Workflow Issues on Every Project

Every multilingual project I've worked on follows the same frustrating pattern:

Someone adds a new English string, forgets to add it to the other locale files, and two weeks later we discover half the app is untranslated in Swedish. Or worse, translation keys pile up in one locale but get deleted in another, and nobody notices until a customer complains.

The "proper" solution seems to be paying for a translation management SaaS (Lokalise, Locize, etc.), but I didn't want vendor lock-in or another subscription. I just wanted the grunt work automated.

What I Built

I created Dire, a CLI tool that handles the boring parts of i18n maintenance:

  • Auto-sync locale files: Diffs your JSON files, finds missing keys, translates them using your choice of provider (DeepL, Claude, OpenAI, Google Translate, etc.), and updates everything. One command, done.

  • Cleanup orphaned keys: The --prune flag removes keys from non-reference locales that don't exist in your most complete locale. No more "keys that should've been deleted months ago" hanging around.

  • Translation memory + glossary: Remembers translations so identical strings don't get re-translated. Saves API costs and keeps terminology consistent across your app.

  • CI/CD validation: The --check flag validates that all translations are complete and exits with the appropriate code. Catches missing translations in PRs before they hit production.

  • Zero lock-in: It's just a CLI that manipulates your JSON files. You own the data, use any provider, switch providers anytime.

Dire in action

Quick Start

Install via npm (it's a Go binary distributed through npm for convenience):

npm i -g dire
Enter fullscreen mode Exit fullscreen mode

Initialize configuration in your project:

dire init
Enter fullscreen mode Exit fullscreen mode

This creates a .dire.toml file. Here's a minimal config:

[files]
directory = "./locales"

[files.locales]
"en.json" = "en-US"
"fr.json" = "fr-FR"
"de.json" = "de-DE"

[providers]
active = "deepl"
Enter fullscreen mode Exit fullscreen mode

Note: The config file is optional - you can configure everything via CLI flags instead. The TOML file is just convenient for storing project settings, managing glossaries, and switching between multiple providers.

Add your API key to a .env file:

DIRE_DEEPL_API_KEY=your-key-here
Enter fullscreen mode Exit fullscreen mode

Run translation:

dire
Enter fullscreen mode Exit fullscreen mode

That's it. Missing keys get translated and your files stay in sync.

Real-World Example

Let's say your en.json has:

{
  "auth": {
    "login": "Log in",
    "logout": "Log out",
    "register": "Sign up"
  }
}
Enter fullscreen mode Exit fullscreen mode

But your fr.json is missing the "register" key:

{
  "auth": {
    "login": "Se connecter",
    "logout": "Se déconnecter"
  }
}
Enter fullscreen mode Exit fullscreen mode

Run dire and it automatically adds:

{
  "auth": {
    "login": "Se connecter",
    "logout": "Se déconnecter",
    "register": "S'inscrire"
  }
}
Enter fullscreen mode Exit fullscreen mode

Advanced Features

Translation Memory

Dire remembers every translation it makes. If you use "Dashboard" in multiple places, it translates it once and reuses the translation everywhere. This:

  • Saves API costs
  • Ensures consistency
  • Speeds up processing

Glossary Management

Define custom terminology that should always be translated the same way:

[glossary]
autoSort = true
entries = [
    { "en-US" = "API", "fr-FR" = "API", "de-DE" = "API" },
    { "en-US" = "dashboard", "fr-FR" = "tableau de bord", "de-DE" = "Dashboard" },
]
Enter fullscreen mode Exit fullscreen mode

Glossary translations:

  • Override provider translations
  • Work bidirectionally (en→fr and fr→en)
  • Don't consume API credits

CI/CD Integration

Add this to your CI pipeline:

dire --check
Enter fullscreen mode Exit fullscreen mode

It validates that all locales are complete and exits with code 1 if translations are missing. Catches incomplete i18n before deployment.

Cleanup Orphaned Keys

Over time, locale files accumulate keys that were deleted from the reference locale but not from translations:

dire --prune
Enter fullscreen mode Exit fullscreen mode

This removes keys from non-reference locales that don't exist in your most complete locale. Keeps your files clean.

Translate Specific Keys

Only want to translate specific keys?

dire --keys "auth.login,auth.register"
Enter fullscreen mode Exit fullscreen mode

Multiple Provider Support

Configure multiple providers and switch between them:

[providers]
active = "claude"

[[providers.configuration]]
name = "claude"
model = "claude-3-5-haiku-latest"
maxTokens = 8192
temperature = 0.2

[[providers.configuration]]
name = "openai"
model = "gpt-4o-mini"
maxTokens = 16000
temperature = 0.2
Enter fullscreen mode Exit fullscreen mode

Switch providers without editing the config:

dire --provider-active openai
Enter fullscreen mode Exit fullscreen mode

Supported Providers

  • Translation services: DeepL, Google Translate, Azure AI Translator
  • AI models: Claude, OpenAI, Gemini, Mistral, DeepSeek

You bring your own API key. No middleman, no markup, full control over costs.

Why I Built This in Go

  • Native binary: Single executable with no runtime dependencies
  • Performance: Concurrent processing and smart batching handle large translation files efficiently
  • Cross-platform: Builds for Linux, macOS, Windows (amd64 + arm64)
  • Simplicity: Distribute via npm but runs as a native binary

Design Decisions

No framework assumptions: Works with any i18n library that uses JSON files. Doesn't care if you use react-i18next, vue-i18n, or something custom.

BYOK (Bring Your Own Key): You own the relationship with your translation provider. Switch providers anytime, no data migration needed.

Local-first: Processes files locally, only talks to translation APIs when needed. Your translation data stays in your git repo.

Single command: Team members don't need to understand how it works. npm run translate and they're done.

How We Use It

Our workflow:

  1. Developer adds new English strings
  2. Runs npm run translate (which calls dire)
  3. Reviews translations (AI gets it right ~80% of the time)
  4. Commits everything together
  5. CI validates completeness with dire --check

The --prune flag runs periodically in a cleanup PR to remove orphaned keys.

Honest Trade-offs

What it solves:

  • Manual translation grunt work
  • Keeping files in sync
  • Orphaned key cleanup
  • CI validation

What it doesn't solve:

  • Real-time collaboration (it's a CLI, not a translation platform with team features)
  • The t() function spam in your components (that's a framework issue)
  • Context-specific translations (AI struggles with ambiguity)
  • 100% accuracy (you still need to review translations)

AI translation gets you 80% of the way there. You still need human review for context, tone, and domain-specific language.

Questions I Get Asked

Q: How accurate are the translations?
A: Depends on the provider. DeepL and Claude are consistently 80-90% accurate for most languages. You still need human review, but it's much faster than translating from scratch.

Q: What if I want to switch providers?
A: Change one line in your config or use the --provider-active flag. Your translation files don't change, so switching is instant.

Q: Does it work with my i18n library?
A: If your library uses JSON files, yes. It doesn't care about your framework or i18n implementation.

Q: How much does it cost?
A: The tool is free. You pay only for API usage to your chosen provider. DeepL, for example, costs ~$5-20/month for most projects.

Q: Can I use it without AI?
A: Yes. Use --stub to create placeholder translations (empty strings), then fill them in manually. Or use --sourced to only apply glossary and memory translations.

Try It Out

GitHub: https://github.com/juliandreas/dire-cli

I built this to scratch my own itch, but I'm curious if it solves the same problems for others. Let me know what you think or what features would make it more useful for your workflow.

Top comments (0)