There's a conversation that happens in almost every startup around Series A. Someone looks at the analytics and notices that 30% of signups are coming from non-English-speaking countries. The product is English-only. Churn from those cohorts is brutal. The engineering lead says, "we need to internationalize."
Then the estimate comes back: six months minimum.
That estimate is almost always accurate, and almost always avoidable.
The Real Cost of Retrofitting i18n
According to CSA Research, 87% of consumers won't buy from an English-only product when alternatives exist in their language. The localization market hit $6.27 billion in 2023 and is growing. These aren't niche numbers — they represent a ceiling on your addressable market that you've quietly installed in your codebase.
The reason retrofitting is expensive isn't because internationalization is technically hard. It's because of how applications get built without i18n in mind:
- String literals scattered across 200 components
- Date and number formatting hardcoded to US conventions
- Database columns sized for English text
- Right-to-left layout never considered
- Content structured around English grammar assumptions
- Timezone handling tied to a single region
Each of these is cheap to get right on day one. Each is painful to untangle at month 18, when the codebase has 40,000 lines and four engineers who've each built on top of the original assumptions.
The six-month estimate isn't for adding translations. It's for auditing and rewriting the parts of your application that were built without i18n in mind. Translation is 20% of the work. The other 80% is archaeology.
When Should You Actually Internationalize?
This question has two parts: when to make your architecture i18n-ready, and when to actually ship multiple languages.
Architecture: Day one, always.
The cost of building i18n-ready from the start is about two hours of setup. You're not translating anything. You're not hiring translators. You're just making decisions that don't close doors.
- Externalize your strings into resource files instead of hardcoding them
- Use a date/time library that's locale-aware (Luxon, date-fns with locale support, Temporal)
- Pick a number formatting approach that wraps
Intl.NumberFormat - Leave room in your UI for text expansion (German can run 30% longer than English)
- Store locale as a user preference from the start
This is not premature optimization. It's not gold-plating. It's the same reason you use environment variables for config values you might want to change — you're not anticipating a specific need, you're avoiding a specific trap.
Shipping translations: After you have signal.
You don't need to ship French on day one. You need to be ready to. The decision to actually invest in a second language should be driven by data:
- You're seeing organic signups from a target market
- A sales conversation requires it to close a deal
- You're entering a market where English penetration is low
- A customer cohort with a specific language is churning at a higher rate
When that signal appears, "i18n-ready" means you can ship a second language in a week, not six months.
The 3-Month Timeline Trap
Here's how teams get caught: they know they need i18n, they don't want to do it wrong, so they plan a "proper i18n project." They scope it at three months. That estimate includes:
- Auditing existing strings
- Building a translation pipeline
- Integrating a translation management system
- Coordinating with a translation vendor
- QA across every locale
While the project is in planning, the roadmap keeps moving. The three-month project keeps getting deprioritized because it doesn't ship features. By the time it finally gets prioritized, it's a five-month project because the codebase grew.
The trap is treating i18n as a big-bang migration. The alternative is treating it as a foundation you lay incrementally, starting with architecture.
The Minimal Viable i18n Setup
Here's what "i18n-ready" looks like in practice for a React or Next.js app at seed stage. It costs two hours to set up and zero ongoing overhead until you're ready to translate.
Step 1: Pick a string externalization approach.
Don't hardcode display strings. At minimum, put them in JSON files. Better: use a typed solution from the start.
// Bad — hardcoded string
<button>Get started for free</button>
// Better — externalized, even if only English for now
// messages/en.json
{
"cta.getStarted": "Get started for free"
}
// Component
const t = useTranslations();
<button>{t('cta.getStarted')}</button>
You don't need a translation platform yet. One JSON file per locale, even if it's just English, is enough. The habit of using t() instead of inline strings is what matters.
Step 2: Locale-aware formatting from day one.
// Bad — hardcoded US formatting
const formatted = `$${price.toFixed(2)}`;
const date = new Date(ts).toLocaleDateString('en-US');
// Good — locale-aware
const formatted = new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
}).format(price);
const date = new Intl.DateTimeFormat(locale, {
dateStyle: 'medium'
}).format(new Date(ts));
When you add a second locale later, the formatting just works. No grep-and-replace across 80 files.
Step 3: Design with text expansion in mind.
Leave breathing room in your UI. Use CSS that degrades gracefully when text runs long. Avoid pixel-fixed widths on text containers. This costs nothing in design but saves real money in QA when you ship German.
Step 4: Store locale as a first-class user attribute.
Even if you're shipping one language, store preferred_locale in your user model. When you add a second language, your existing users already have a locale preference stored. You're not migrating a schema under live data.
Common Startup Mistakes
"We'll add i18n when we need it." The moment you need it is the moment you have no time to do it properly. The signal that triggers the decision — a major customer requiring French, a regional go-to-market push — always comes with urgency attached.
Building a custom translation pipeline. Translation management is a solved problem. A custom solution costs weeks to build and months to maintain. Use tooling that already exists.
Treating localization as just translation. Language is 20% of localization. Currency, date formats, address formats, legal requirements, RTL support, locale-specific UX conventions — these are the other 80%. A translated string in a broken layout is still a broken experience.
Giving translators access to JSON files directly. Human translators should never edit JSON manually. The error rate is high and the feedback cycle is slow. They need a UI built for translation work, with context, character limits, and term consistency enforcement.
Assuming English word order generalizes. Concatenating translated strings with JavaScript string interpolation breaks in languages with different grammar structures. Use ICU message format for anything with variables.
// Bad — assumes English word order
t('welcome') + ' ' + userName + '!'
// Good — ICU format, locale can reorder tokens
t('welcome', { name: userName })
// en: "Welcome, {name}!"
// ja: "{name}さん、ようこそ!"
A Decision Framework
Use this to decide where you are and what to do:
| Stage | i18n Architecture | Active Translations |
|---|---|---|
| Pre-product | Set up externalized strings, locale-aware formatting | No |
| Pre-PMF | Maintain the foundation, don't translate yet | No |
| Post-PMF, pre-Series A | If you're seeing international signal, translate top 2 languages | Maybe |
| Series A+ | Treat localization as a product requirement | Yes |
The key insight: the architecture decision and the translation decision are independent. Make the architecture decision now. Make the translation decision when the data tells you to.
The Practical Checklist
Before you write another component, run through this:
- Strings are externalized (not hardcoded in JSX/templates)
- Date formatting uses locale-aware APIs
- Number and currency formatting uses
Intl.NumberFormat - User model has a
localefield - UI layouts tested with 30% longer text
- No string concatenation with translatable parts
- ICU message format used for strings with variables
- Pluralization handled correctly (not just appending "s")
Checking all of these off takes two hours. Missing any of them at scale costs weeks.
What This Looks Like in Practice
When you're ready to actually ship multiple languages, the workflow changes from "archaeological dig" to "translation project." You need:
- A way to export source strings to translators
- A review process for translations
- A deployment pipeline that updates strings without a code deploy
- Runtime delivery of the right locale to each user
At Better i18n, we built a platform around this specific problem — type-safe SDKs for React, Next.js, Vue, and Svelte, Git-based translation workflows so translations go through review like code does, and CDN delivery so you can update translations without redeploying. The free tier on our pricing page is sized for early-stage teams that want to build the foundation without budget commitment. If you want to see how the technical pieces fit together, the for developers page has the full picture.
The six-month rewrite is not inevitable. It's the result of a two-hour decision deferred too long.