Build Localisation Before You Need It

Seek added Russian in an afternoon. Ascend added a bunch of languages after one Greek request and it hurt.

Every greenfield project I’ve started recently began the same way. Pick a framework, ship the happy path in English, tell myself I’ll add other languages when someone asks.

With Ascend, someone did ask. A Greek user wanted the app in their language. Fair enough. What followed was a painful rollout to a bunch of languages we should probably have planned for from the start.

With Seek, the story was different. A wave of Russian-speaking users showed up on YouTube and wanted the extension in Russian. The surface area was small enough that localising it felt almost easy by comparison.

Same lesson. Opposite scales of pain.

The work is never just swapping strings

The naive version goes like this. Export your copy to JSON, hand it to a translator, wire up a language switcher, done.

What actually breaks is everything you built when you assumed one language. Concatenated labels ("Save " + count + " items"). Hard-coded plural rules. Layouts that fit in English and clip in German. Dates formatted with toLocaleDateString() in one component and a hand-rolled format in another. Error messages assembled from three string fragments because it read fine at the time.

You end up in grep-archaeology, hunting for user-visible text that never lived in a messages file because it lived in a conditional, a tooltip, a validation rule, or a widget label someone added six months later.

Ascend is a Flutter app with identities, habits, check-ins, a journey view, weekly reflection, widgets, a paywall. That’s a lot of screens that grew up English-first. The Greek request didn’t create one translation task. It surfaced every place we’d treated copy as implementation detail.

Seek was the easy version

Seek is a Chrome extension with a command palette, a results list, and not much else. Ctrl+K, search the transcript, jump to the timestamp. When Russian-speaking users started showing up in numbers, wrapping the UI strings wasn’t the hard part. The product is thin. The copy is mostly chrome around someone else’s video.

That’s the version people imagine when they say they’ll add languages later. Small surface, obvious strings, one locale file, ship it. It really can be that simple when the app is small and the audience surprise is the first time you’ve had to think about it.

It also sets a trap. Easy once makes you think it’ll stay easy.

Ascend was the invoice

Ascend lives on app stores. Anyone in the world can download it. The Greek request wasn’t random. It was the first explicit ask in a product that had already been global by accident.

Retrofitting a bunch of languages into a mobile app that shipped English-first meant touching far more than the settings screen. Onboarding language. Empty states. Reflection prompts. Paywall copy. Widget text. Every identity label the UI shows by default. Flutter makes localisation workable once you’re in it, but it doesn’t forgive six months of raw strings in widgets.

We got there. It was painful in the way refactors are painful: not because any single string was hard, but because there were hundreds of them and no map of where they lived.

Early is boring on purpose

Building localisation early feels like over-engineering. You don’t have Greek users yet. You don’t have a translation budget. You might never need Hindi.

But the cheap version of i18n isn’t translating anything. It’s refusing to bake English into the structure.

That means message keys from day one, even if every locale file is identical for months. It means leaving room in the UI for text that grows. It means storing user-facing copy outside components, the same way you wouldn’t hard-code API URLs across forty files.

On Ascend I’d wire Flutter’s l10n tooling from the first screen. On Seek-sized projects I’d still extract strings early, because the habit matters more than the current line count.

What I’d do differently

I’d treat user-visible strings like dependencies. Centralise them early. Name them. Switch the dev build to a pseudo-locale or a longer language occasionally, just to see what breaks in the layout before a real translator sends German back.

I’d also stop pretending dates and numbers are free. Pick a locale strategy for formatting and use it everywhere, not wherever someone remembered Intl exists.

And I’d be honest about scope. Not every app needs twelve languages on launch. Plenty need zero. But if your product ships on a global store, or sits on a global platform like YouTube, a second language isn’t a someday problem. It’s a when problem.

What still isn’t solved

User-generated content doesn’t translate itself. Ascend identities and habits are whatever the user types. Localising the app chrome doesn’t localise someone’s goal to “run a marathon” or “be a more patient parent.” That’s fine. It’s just a limit worth naming.

I’m also writing this on an English-only personal site with no i18n config. The lesson isn’t “localise everything on day one.” It’s don’t let English-only become invisible architecture you only notice when a real user asks in Greek.


Seek proved localisation can be easy when the app is small. Ascend proved what happens when you wait until someone asks.

Related