Every multilingual app starts the same way:
locales/
├── en.json ← 12 keys
├── tr.json ← 12 keys
└── de.json ← 12 keys
Six months later:
locales/
├── en.json ← 847 keys
├── tr.json ← 791 keys (56 missing, nobody knows which)
├── de.json ← 823 keys (24 orphaned from a deleted feature)
├── ja.json ← 402 keys (translator quit halfway)
└── ar.json ← 0 keys (we said we'd get to it)
If this looks familiar, keep reading.
The Real Problem
The issue isn't JSON files. JSON is fine. The problem is that translations live outside your development workflow.
Your code goes through pull requests, CI checks, type checking, and automated tests. Your translations? Someone exports a spreadsheet, a translator fills in cells, and someone else copies the values into JSON files. No review. No validation. No automated checks.
This means:
-
Keys drift — a developer renames
save_drafttosave_as_draftin code, but the translation files still havesave_draft - Dead keys accumulate — features get deleted, translation keys don't
-
Format errors hide —
{count, plural, one {# item} other {# items}}is easy to break and hard to spot - Deploys block on translations — fixing a typo requires a full rebuild
What if translations worked like code?
That's what we built with Better i18n. Here's the workflow:
Developer adds t('pricing.title') in code
↓
CLI scans the codebase, detects the new key
↓
Key appears in the translation dashboard
↓
Translator (or AI) adds the translation
↓
Better i18n creates a PR in your GitHub repo
↓
Team reviews the PR → CI checks pass → merge
↓
CDN cache updates → users see the new translation
No JSON file juggling. No manual exports. No "did we translate this?" Slack messages.
Let's Build It
I'll walk through setting up a Next.js app with Better i18n. The same principles apply to React and React Native — I'll cover those at the end.
Step 1: Install
npm install @better-i18n/next @better-i18n/cli
Two packages:
-
@better-i18n/next— middleware, hooks, server component support -
@better-i18n/cli— scans your code, syncs with the platform
Step 2: Middleware for Locale Routing
// middleware.ts
import { createI18nMiddleware } from '@better-i18n/next';
export default createI18nMiddleware({
locales: ['en', 'tr', 'de', 'ja'],
defaultLocale: 'en',
});
export const config = {
matcher: ['/((?!api|_next|.*\\..*).*)'],
};
This gives you:
-
/about→ English (default) -
/tr/about→ Turkish -
/de/about→ German - Automatic detection from
Accept-Languageheader - Locale persistence via cookies
Step 3: Use Translations in Your Components
import { useTranslations } from '@better-i18n/next';
export default function PricingPage() {
const t = useTranslations('pricing');
return (
<main>
<h1>{t('title')}</h1>
<p>{t('subtitle')}</p>
{plans.map(plan => (
<div key={plan.id}>
<h3>{t(`plans.${plan.id}.name`)}</h3>
<p>
{t('price_per_month', {
amount: plan.price,
currency: plan.currency
})}
</p>
<ul>
{plan.features.map(f => (
<li key={f}>{t(`features.${f}`)}</li>
))}
</ul>
</div>
))}
</main>
);
}
The translation file for this might look like:
{
"pricing": {
"title": "Simple, transparent pricing",
"subtitle": "No hidden fees. Cancel anytime.",
"price_per_month": "{amount, number, currency} / month",
"plans": {
"starter": { "name": "Starter" },
"pro": { "name": "Pro" },
"enterprise": { "name": "Enterprise" }
},
"features": {
"unlimited_projects": "Unlimited projects",
"github_sync": "GitHub sync",
"ai_translation": "AI-powered translation",
"cdn_delivery": "CDN delivery"
}
}
}
Better i18n supports ICU MessageFormat — the same standard used by FormatJS, next-intl, and use-intl. Plurals, dates, numbers, select statements — all handled.
Step 4: Scan Your Codebase
Here's where it gets interesting. Instead of manually tracking which keys exist:
npx @better-i18n/cli scan
Output:
Scanning src/ for translation keys...
✓ Found 847 translation keys across 124 files
New keys (not yet translated):
+ pricing.plans.enterprise.name
+ pricing.features.cdn_delivery
Unused keys (not found in code):
- settings.old_theme_toggle
- onboarding.deprecated_step_3
Missing translations:
⚠ tr: 12 keys missing
⚠ de: 3 keys missing
⚠ ja: 45 keys missing
The CLI uses AST parsing, not regex — so it understands your code structure. It catches dynamic keys, template literals, and namespace patterns.
Step 5: Sync
npx @better-i18n/cli sync
This pushes the scan results to the Better i18n dashboard where translators can work on missing translations. When they're done, a PR lands in your repo.
AI Translation That Understands Context
Most machine translation tools translate strings in isolation. Better i18n sends context to Google Gemini:
- Namespace — "this string is in the pricing section"
- Glossary — "we translate 'workspace' as 'çalışma alanı' in Turkish"
- Nearby keys — "the strings around this one say..."
- ICU structure — "this is a plural form, handle it correctly"
The difference is noticeable. Instead of:
// Generic MT
"Workspace" → "Çalışma Alanı" (correct but formal)
"{count} items selected" → "{count} öğe seçildi" (wrong plural form)
You get:
// Context-aware
"Workspace" → "Çalışma alanı" (matches glossary casing)
"{count, plural, one {# öğe seçildi} other {# öğe seçildi}}" (correct ICU)
Bonus: Let AI Agents Handle It
We built an MCP server so AI assistants can manage translations directly:
npm install @better-i18n/mcp
Connect it to Claude, Cursor, or Windsurf, and you can:
You: "Add the key 'dashboard.welcome_back' with value
'Welcome back, {name}!' and translate it to Turkish and German"
AI: Creates the key, translates it, and the changes flow
through your GitHub PR workflow.
The Model Context Protocol is becoming the standard for connecting AI assistants to external tools. If you're not using it yet, it's worth exploring.
React Native / Expo
Mobile apps need offline support and OTA translation updates. The @better-i18n/expo package handles both:
import { BetterI18nExpoProvider, useTranslations } from '@better-i18n/expo';
export default function App() {
return (
<BetterI18nExpoProvider
projectId="your-project-id"
defaultLocale="en"
fallbackMessages={require('./messages/en.json')}
>
<RootNavigator />
</BetterI18nExpoProvider>
);
}
What you get:
- Offline-first — translations cached locally, works without network
- Background updates — new translations fetched silently, no app store update
- Bundled fallback — ships with default translations so the app works on first launch
- Automatic locale detection — reads device language settings
The Full Picture
Here's what the complete workflow looks like:
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Developer │────▶│ GitHub Repo │◀────│ Translator │
│ (adds key) │ │ (source of │ │ (via Better │
│ │ │ truth) │ │ i18n UI) │
└─────────────┘ └──────┬───────┘ └─────────────┘
│
┌──────▼───────┐
│ CDN Edge │
│ (Cloudflare) │
└──────┬───────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Next.js │ │ React │ │ Expo │
│ App │ │ App │ │ App │
└────────┘ └────────┘ └────────┘
No separate translation pipeline. No manual exports. No "which JSON file has the latest version?" conversations.
Try It
The SDKs are open source under MIT:
better-i18n
/
oss
Official TypeScript SDKs for Better i18n — Next.js, React, Expo/React Native integrations with CLI, MCP server, and CDN-powered translation delivery
TypeScript SDKs for the Better i18n localization platform
Manage translations with CDN delivery, GitHub sync, and AI-powered workflows
Documentation · Website · Blog
Why Better i18n?
Most localization tools weren't built for modern developer workflows. Better i18n is different:
- GitHub-native — translations sync as PRs, reviews happen in your existing workflow
- CDN-delivered — translations served from Cloudflare's edge network, updated without redeployment
- AI-powered — context-aware translation with Google Gemini, not word-by-word machine output
- TypeScript-first — full type safety across Next.js, React, Expo, and React Native
- MCP-ready — AI agents can manage translations through the Model Context Protocol
How it compares
Links:
- better-i18n.com — platform (free tier available)
- docs.better-i18n.com — documentation
- npm packages — all published SDKs
- AI agent skills — i18n best practices for AI assistants
If you have questions, drop a comment below or open an issue on GitHub.
Top comments (0)