Why teams migrate away from key-based i18n
If you ask why a team migrated away from i18next or react-intl, you'll hear specific stories — not abstract critiques. "We shipped a missing-key bug to French users for three weeks." "Our PM keeps asking what homepage.cta.v2.final actually says." "We tried to add Spanish and it became a six-week project."
This article documents the real friction points, drawn from actual migration conversations. If any of these feel familiar, you're not imagining it — they're systemic costs of the key-based workflow.
TL;DR
The seven most common pains that drive teams away from key-based i18n:
- Missing-key bugs that ship silently
- Stale translations after copy updates
- Code review where nobody knows what the keys mean
- Onboarding cost for new engineers
- Locale-file merge conflicts
- The "add a new language" project tax
- Name-collision arguments at PR review
We'll go through each, with examples and how keyless avoids them.
Pain 1: Missing-key bugs that ship silently
You add a key in en.json:
{ "checkout": { "newPaymentMethodCta": "Add a payment method" } }
You forget to add it in fr.json. You ship.
What French users see depends on your i18next config:
- Default fallback strategy: French users see the English source ("Add a payment method").
- Strict missing-key handler: they see
[checkout.newPaymentMethodCta]. - Some teams' setup: silent UI breakage with the key as text.
Either way, this is a bug class that lives in your codebase forever. You can mitigate with linters (i18next-parser, missing-key extraction) but not eliminate. Every active engineer will hit it at least once.
How keyless avoids this: there are no keys, so missing keys can't exist. If a translation isn't yet generated for a target locale, the SDK falls back to the source string (which is the JSX content) and queues a translation. Always graceful, never broken.
Pain 2: Stale translations after copy updates
Product manager says: "Change 'Confirm' to 'Confirm payment' on the checkout button."
You update en.json:
- { "checkout.confirm.cta": "Confirm" }
+ { "checkout.confirm.cta": "Confirm payment" }
The key (checkout.confirm.cta) didn't change. So fr.json still says "Confirmer". Your French users now see "Confirmer" — which means "Confirm" — when the English now says "Confirm payment". The translation is stale but the lookup still resolves.
This is the most insidious i18n bug class because nothing fires an error. A linter can't catch it (the key exists, the value is non-empty). It's only caught by:
- A French-speaking user filing a bug.
- A translator noticing during review (if you have one).
- A PM doing locale QA on every release (you don't).
How keyless avoids this: the source string IS the key. If you change the English from "Confirm" to "Confirm payment", that's a different lookup. The French translation for "Confirm" stays cached, and "Confirm payment" gets translated fresh. No stale state.
Pain 3: Code review with opaque keys
A PR diff shows:
- {t("checkout.actions.primary.label")}
+ {t("checkout.actions.primaryV2.label")}
The reviewer asks: "What did this change?" The author answers: "I renamed the key, the actual copy is the same."
Now they both have to open en.json, find the key, scroll to the right namespace, and verify. Every i18n change in code review is two-step — open the JSON, find the key, read the value. Multiply by every PR with a copy change.
Keyless equivalent:
- <I18nKeyless>Confirm</I18nKeyless>
+ <I18nKeyless>Confirm payment</I18nKeyless>
The reviewer reads the diff and immediately knows what changed. Nothing to look up.
Pain 4: Onboarding cost
New engineer joins. They want to add a "Settings" link to the navbar.
In a key-based codebase, here's what they have to learn:
- Where the locale files live (
public/locales/en/common.json? Orsrc/i18n/locales/en.json? Or split by namespace?). - What namespace to put the new key in (
common?navigation?nav?). - The naming convention (camelCase? dot.notation? per-page nesting?).
- Whether to add the key to all locale files or just English (which depends on whether your TMS is configured to auto-translate).
- How to actually reference the key (
useTranslation()?withTranslation? Namespace prefixing?). - What the missing-key behavior is (so they know if their work is "done" when only English is filled in).
That's typically a half-day of investigation, often done by reading existing examples and copying. Some engineers never fully internalize it and just keep grepping for similar strings.
Keyless equivalent:
<I18nKeyless>Settings</I18nKeyless>
That's the entire onboarding. New engineers are productive on day one.
Pain 5: Locale-file merge conflicts
Two engineers both add new keys to en.json on the same day. Different parts of the file. Git merge handles it most of the time. Sometimes it doesn't — JSON conflict markers in nested objects are messy to resolve, especially when both branches added keys with similar names.
Worse: fr.json lags behind. Your TMS pushes a PR with hundreds of new translations. That PR sits open while CI runs. Then someone merges main. The TMS PR has merge conflicts. Re-resolve. Re-run TMS. Repeat.
This isn't unique to i18n but it's a class of friction that keyless removes by not having locale files at all.
Keyless equivalent: translations live in our backend, not in files in your repo. There's nothing to conflict on.
Pain 6: The "add a new language" tax
Marketing decides to launch in Spanish. With i18next:
- Add
estosupportedLngs. - Create
es/common.json,es/checkout.json,es/marketing.json, etc. (one per namespace). - Open a translation project in your TMS (Lokalise, Crowdin) or hand the JSON files to a freelance translator.
- Wait 2–4 weeks for translations.
- Review for consistency, glossary terms, brand voice.
- Push translated files back to the repo.
- QA the Spanish version of the app (find missing keys, find broken interpolations, find bad pluralization).
- Ship.
This is a 4–6 week project, often. With keyless:
languages: {
primary: "en",
- supported: ["en", "fr", "de"],
+ supported: ["en", "fr", "de", "es"],
},
That's the project. The SDK auto-translates every existing string in the background. Within minutes, your app speaks Spanish. You can audit and override the AI translations from the dashboard at your leisure — but they're already shipping.
The cost difference between "6-week project" and "config change" is what makes keyless feel categorically different.
Pain 7: Naming arguments
Real PR comment from a real codebase:
"Should this key be
homepage.hero.titleorpages.home.heroTitle? We've been inconsistent. Maybe we should refactor all keys to one convention before merging this?"
This ends in either:
- A 30-minute Slack thread that doesn't resolve.
- A "naming convention RFC" that never gets fully adopted.
- The PR sitting in review for two days while people argue.
- An eventual refactor PR that touches 800 files.
Multiply across the team's lifetime. Real cost.
Keyless equivalent: there are no key names. The argument doesn't exist.
When the pain isn't enough to justify migration
Honest counterpoint: some teams have pain but not enough to justify migrating. Signals:
- ✅ You ship a localized release every 2 weeks without missing-key bugs.
- ✅ Your translators are happy with the workflow.
- ✅ Adding a new language has happened in the past 6 months and was painless.
- ✅ Onboarding new engineers takes < 1 day for i18n specifically.
- ✅ You don't have locale-file merge conflicts more than once a quarter.
If 4+ of these are true, your setup works. Don't migrate.
The migration calculus is: does the pain × frequency × team size exceed the migration cost? For most early-stage teams, the answer is yes. For mature teams with optimized workflows, it's no.
What teams who migrated tell us
The most common quote, paraphrased across many conversations:
"I didn't realize how much time we spent on i18n bookkeeping until we stopped doing it."
The cost is invisible because it's distributed. Two minutes per PR. Half a day per onboarding. A week per new locale. A month-equivalent per year of stale-translation bugs no one catches. It adds up, but it doesn't show up on a dashboard.
The second most common quote:
"We weren't using the TMS features anyway. We were just using Lokalise as expensive JSON storage."
Many teams adopt a TMS "because that's what real companies do" and discover six months later that the translator UI is empty, the glossary is empty, the screenshot annotations are empty. They're paying for unused features. That's the moment migrating to keyless makes sense.
What you give up by migrating
So this isn't one-sided cheerleading, the things you genuinely lose:
❌ Static analysis of missing keys (replaced by "the bug class doesn't exist"). ❌ ICU MessageFormat plural rules (you branch explicitly in code). ❌ TMS workflows (you don't have a translator UI). ❌ Locale files in your repo (replaced by backend storage with export endpoint). ❌ Some control (you depend on a hosted backend; cached translations mitigate this).
These are real. Some teams need them. Most don't.
Migration paths
If the pain points in this article resonate and you want to migrate, we have step-by-step guides:
- Migrate from i18next to i18n-keyless
- Migrate from react-intl to i18n-keyless
- Migrate from Lokalise to i18n-keyless
- Migration buyer's guide — picking the right destination.
Next steps
- Compare options: Best i18next alternatives in 2026.
- Read the philosophy: Why keyless i18n is the future.
- The complete guide: The complete guide to keyless i18n.
- Try it: Quick setup — 5 minutes.