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
--pruneflag 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
--checkflag 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.
Quick Start
Install via npm (it's a Go binary distributed through npm for convenience):
npm i -g dire
Initialize configuration in your project:
dire init
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"
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
Run translation:
dire
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"
}
}
But your fr.json is missing the "register" key:
{
"auth": {
"login": "Se connecter",
"logout": "Se déconnecter"
}
}
Run dire and it automatically adds:
{
"auth": {
"login": "Se connecter",
"logout": "Se déconnecter",
"register": "S'inscrire"
}
}
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" },
]
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
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
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"
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
Switch providers without editing the config:
dire --provider-active openai
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:
- Developer adds new English strings
- Runs
npm run translate(which callsdire) - Reviews translations (AI gets it right ~80% of the time)
- Commits everything together
- 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)