Shipping in English and Ukrainian is normal for teams selling in the EU and serving users at home. It is also where roadmaps quietly double: every feature becomes two tickets, every screenshot review becomes two rounds, and "we'll translate later" becomes a permanent debt.
You do not need two products. You need one product with a clear locale contract.
The teams that struggle with this are rarely short on translators. They are short on a decision, made early, about which parts of the product are content and which parts are structure. Skip that decision and every subsequent choice — how a component is built, how a ticket is scoped, how a bug is triaged — gets made twice, inconsistently, by whoever happened to touch that screen that week.
Decide what is shared vs translated
Not everything is copy. Treat fields in three buckets:
| Shared (one value) | Localized | Locale-aware behaviour |
|---|---|---|
| Slug, IDs, status | Title, body, CTA labels | Date formats, currency display |
| Category enum | Country names in prose | Default locale fallback |
| Image paths | Meta description | Legal links per market |
If a designer puts translated strings into Figma frames as separate artboards for every screen, engineering will rebuild layouts twice. Prefer one layout, swap strings.
This sounds obvious once it is written down, but it is where most bilingual products quietly fork. A "UK version" of a screen that started life as a separate Figma frame tends to drift — a spacing tweak lands on the English frame during a sprint and nobody remembers to mirror it, a new field gets added to one locale's form and not the other, and six months later the two versions of "the same screen" behave differently enough that QA has to test them as if they were different features. One layout with swapped strings cannot drift that way, because there is only one layout to change.
There is a technical reason this matters beyond design discipline, too. Ukrainian grammar has plural forms that English does not — "1 file", "2 files", "5 files" collapses to two English rules but needs three in Ukrainian (singular, few, many), plus different rules again for numbers like 21 or 22. A locale contract that treats pluralization as a lookup table per language (via ICU MessageFormat or an equivalent plural-rules library) survives this. A locale contract that treats it as "just interpolate the number into the string" produces grammatically wrong UI copy that a Ukrainian-speaking user notices immediately, even if they never say anything about it.
Own strings in one place
Scatter "Submit" across fifty components and you will miss half of them in UK. Centralise:
- UI chrome in locale JSON / message catalogs
- Long-form content (blog, case studies) in files or a CMS with locale tabs
- Emails and PDFs in their own templates — they break i18n silently
Fallback rule that saves support tickets: missing UK string → show EN, never show the raw key.
Centralizing strings is the easy part of this rule; keeping them centralized is the hard part. The failure mode we see most often is not "we never set up a message catalog" — it is "the catalog exists, but a developer under deadline pressure hardcoded a string directly in a component to ship a hotfix, meant to move it into the catalog later, and later never came." Six months on, there are a dozen of these scattered through the codebase, none of them individually a big deal, all of them collectively the reason a "simple" translation pass takes two extra days. A lint rule that flags string literals in JSX outside of the catalog catches this at PR time instead of at audit time, which is the difference between a five-minute fix and a two-day one.
Emails and PDFs deserve their own line item because they are usually generated by a different code path than the UI — a transactional email service, a PDF-generation library, a queue worker — and that code path is easy to forget exists when someone is reasoning about "the product." Password reset emails, invoice PDFs, and shipping confirmations are exactly the kind of content that ships in English forever because nobody put them in the same review checklist as the screens.
Write for translation, not for wordplay
English marketing often leans on idioms that collapse in Ukrainian. Brief writers to:
- Prefer short sentences and concrete verbs
- Avoid puns in primary CTAs
- Keep button labels under ~3–4 words so layout does not break
Ukrainian often needs more characters. Design buttons and nav with overflow room, not pixel-tight English mockups.
Text expansion is not a rounding error — Slavic languages routinely run 15–30% longer than their English source for the same meaning, and short UI strings (a two-word button, a nav label) can expand by even more in relative terms because there is less room to compress. A button sized exactly to fit "Get started" in English will clip or wrap "Розпочати безкоштовно" in Ukrainian if nobody designed for the difference. The fix is not "make Ukrainian shorter" — that fights the language — it is designing components that tolerate a range of string lengths from the start: minimum tap targets with flexible width, text that can wrap to two lines without breaking a layout, icons that can carry meaning when a label has to shrink.
There is a second, less obvious translation trap worth briefing writers on: Ukrainian is a grammatically gendered language, and past-tense or first-person UI copy ("You've completed your profile", confirmation messages phrased as statements) sometimes needs to pick a grammatical form that English copy never has to think about. Writing English source copy in a neutral, present-tense, instruction-style voice ("Complete your profile" rather than "You've completed it!") gives translators a cleaner sentence to work from and avoids copy that reads as oddly gendered or stilted in the target language.
Common mistakes we see
A few patterns show up often enough to call out on their own:
- Treating the second locale as a launch-week task. Bilingual support added after the UI is "done" usually means retrofitting fixed-width components, which is slower than building them flexible from the start.
- No single owner for translation quality. Strings get translated, but nobody is responsible for reading them in context on the actual screen before release, so awkward machine-adjacent phrasing ships and stays.
- Locale switching that loses state. A user mid-checkout who toggles language and gets dropped back to the homepage will not toggle language again — they will assume the feature is broken.
- SEO metadata treated as an afterthought. Missing
hreflangtags, duplicate canonical URLs across locales, or an English-only sitemap quietly cost organic visibility in the Ukrainian market even when the UI itself is fully localized.
Release checklist that actually catches gaps
Before you call a bilingual release done:
- Walk the critical path in both locales (signup, checkout, contact)
- Search for untranslated keys and
TODOplaceholders - Check OG titles / meta for both languages
- Spot-check emails and error messages — they are usually forgotten
- Confirm legal pages exist or correctly fall back
Automate what you can (lint for missing keys). Manually click what users pay for.
The reason this list is ordered the way it is: automated checks catch missing strings, but they cannot catch a string that is present, translated, and wrong — a button that says the right words but triggers the wrong action after a locale-specific conditional was missed, or a confirmation message that translated correctly in isolation but reads as ambiguous next to the field it is confirming. That only shows up when a human walks the actual flow in the actual language, which is why "click what users pay for" is not a nice-to-have step — it is the one check nothing else substitutes for. Error messages and emails make the list explicitly because they are the parts of a product a developer interacts with least during normal QA (you rarely trigger a 500 error or a password-reset flow while testing a new feature) and therefore the parts most likely to still be in English on release day.
CMS and marketing sites
Portfolio and blog content changes weekly. Static-only EN with "UK someday" is how half the site stays English forever. Either:
- Publish both locales together, or
- Mark EN-only clearly and schedule UK as a real task with an owner
Partial localisation is fine if it is intentional. Accidental half-translation feels broken.
The distinction matters because visitors read intent, not process. A page that is clearly, consistently English-only reads as a deliberate choice — maybe this content is aimed at an international audience and that is fine. A site where some pages are Ukrainian, some are English, and the split looks arbitrary reads as neglect, and it makes visitors wonder what else on the site is unfinished or unmaintained. If a UK content team cannot keep pace with an EN content team, the honest fix is a visible backlog with an owner and a rough timeline — not silence.
At Stereasoft we ship bilingual Next.js and React Native products for clients who sell across borders, working with senior-heavy teams on TypeScript and Node.js backends. The teams that win treat locale as architecture — decided during discovery, before the first component is built — not as a final polish pass after the demo.
