Content

Localisation

The product is built in British English, then localised — language, formats, conventions — for each market. Localisation is not translation; translation is one step inside it.

Documentedby Derek Fidler

Definitions

Four words, often used as if they meant the same thing. They don't.

Internationalisation (i18n)

Building the product so that it can be used in any locale — currency formatting plugs in, dates render per region, layouts breathe under longer text. The engineering side.

Localisation (l10n)

Adapting the product for a specific locale. Translates copy, swaps formats, accounts for cultural fit. The product side.

Translation

Converting text from one language to another. One step inside localisation; not the whole job.

Locale

A linguistic region — a language paired with a country. en-GB ≠ en-US ≠ en-AU. Always specify both halves; the language alone hides decisions about format, spelling, and convention.

The British English default

Source copy is written in British English (en-GB). Every other locale is a translation of that source. There's no “international English” fallback — that's a euphemism for stripping voice and leaving stiff sentences nobody enjoys.

Why en-GB and not en-US? Two reasons. The product is European-first — seven of eight markets sit in Europe — and en-GB is closer to the conventions our local translators expect. And en-US is one more locale that gets translated, so the codebase doesn't privilege it as the default.

The team's working language is also en-GB

Strings live in en-GB in the source. PRs use en-GB. Slack messages about product copy use en-GB. The translation step happens at build / handoff time — not in the middle of a discussion about whether a button should say “Save” or “Save changes”.

Markets and locales

Eight markets, four currencies, ten languages — counting English in each country as its own register. Primary is the local language; alternate is what a non-resident English speaker in that country falls back to.

MarketLocaleCurrencyPrimary languageAlternate
United Kingdomen-GBGBPBritish English
Denmarkda-DKDKKDanishEnglish (en-GB)
Finlandfi-FIEURFinnishSwedish (sv-FI), English (en-GB)
Germanyde-DEEURGermanEnglish (en-GB)
Francefr-FREURFrenchEnglish (en-GB)
Italyit-ITEURItalianEnglish (en-GB)
Netherlandsnl-NLEURDutchEnglish (en-GB)
United Statesen-USUSDAmerican English

Finland gets two primary languages — Finnish and Swedish — both official. Translation has to ship for both at the same time; we don't backfill Swedish later.

Writing copy that survives

A copy decision the writer makes in en-GB has consequences for the translator and the layout. These four rules are the difference between a string that translates cleanly and one that breaks.

Plan for text expansion

Translated copy is, on average, 30–50% longer than English. Short labels can inflate by 200–300% — German articles alone add weight, and Finnish compounds things further. Layouts that scrape past in English break in Helsinki.

en-GB

Settings

8 chars

de-DE

Einstellungen

13 chars · +63%

fi-FI

Asetukset

9 chars · +13%

  • Use flexible layouts. Single column or grid with auto-fit, minmax() over fixed widths.
  • Don't set narrow column widths on labels or buttons that pretend the English string is the upper bound.
  • Test components with pseudo-localised strings — bracket each token with extra characters — to surface overflow before translation lands.

Plan for word order

German puts the verb at the end. Finnish reorders things English speakers expect to be subject-verb-object. If the source copy assumes English word order, the translation will twist the layout to match.

  • Don't hyperlink full phrases. Word order shifts may fragment them in other languages.
  • Don't place icons or buttons inside the middle of a sentence. The translator can't move them.
  • Treat each sentence as one atomic string. Don't expect translators to piece sub-strings together.

No concatenation

Stitching strings together at runtime makes it impossible for translators to reorder words. Pass the variables into the template; let the translation framework decide where they sit.

ts

// ✗ Concatenated — translators can't move "files"
const msg = t("upload.success.prefix") + " " + count + " " + t("upload.success.suffix");

// ✓ One template, named placeholders
const msg = t("upload.success", { count });
// "Uploaded 12 files"   (en-GB)
// "12 Dateien hochgeladen"  (de-DE)
// "Tiedostoa ladattu: 12"   (fi-FI)

No UI inside sentences

A button or input rendered inside a sentence locks word order. Lift the UI out, write the sentence as a sentence, and let the form sit beside it.

No

Show me 10 prototypes per page

The number-input is glued into the middle of the sentence. Word order in de-DE or fi-FI lands the input somewhere it doesn't fit visually.

Yes

Prototypes per page

10

Label, input. Two atoms. Translation rewrites the label freely; the input never moves.

Plurals and gender

Six plural categories exist across our languages — zero, one, two, few, many, other — and not every language uses the same rules. Hard-coding count === 1 ? "item" : "items" breaks Russian, Polish, Welsh, and Arabic the moment they land. Use ICU MessageFormat. The framework picks the right form per locale.

ts

// ICU plural form
t("prototype.count", { count }, {
  en: "{count, plural, =0 {No prototypes} one {1 prototype} other {{count} prototypes}}",
  da: "{count, plural, =0 {Ingen prototyper} one {1 prototype} other {{count} prototyper}}",
  de: "{count, plural, =0 {Keine Prototypen} one {1 Prototyp} other {{count} Prototypen}}",
  fi: "{count, plural, =0 {Ei prototyyppejä} one {1 prototyyppi} other {{count} prototyyppiä}}",
});

Same applies to gender. French agreements, German articles, and Italian adjective endings change with the noun's gender — the source copy needs to expose enough context for the translator to pick the right form.

Names, addresses, and forms

  • Don't split names into first / last. Use one full name field. Some cultures order family-name-first; some list multiple surnames; some use a single mononym.
  • Address formats vary by market. Postal code position (before vs. after the city), state/region requirement (US yes, EU mostly no), house-number-then-street vs. street-then-number.
  • Phone-number inputs need a country prefix.Pre-fill from the merchant's registered country, but allow change.
  • Mark required fields consistently. Conventions on whether an asterisk indicates required or optional differ — pick required-with-asterisk and stay with it across the product.

Icons and color

Most icons travel well; a few don't. Hand gestures, religious symbols, and anything that pairs with text in a culturally specific way (a piggy bank for savings, a mailbox shape) are worth checking with someone in-market.

Color associations also shift — green and red mean “up/down” in some markets, “down/up” in others, and stand for entirely different political signals across them. The system pairs color with copy rather than relying on color alone, which side-steps most of the trap.

RTL preparedness

None of our current markets are right-to-left. We still write CSS as if one could be — logical properties (margin-inline-start, padding-inline) over physical ones (margin-left). It costs nothing now and saves a refactor when the first RTL market lands.

Translation handoff

Five steps. The order matters; skipping any of them loads work onto the next.

  1. 1

    Lock the source

    Source copy in en-GB ships to translators only after product review. Late edits to en-GB cascade through every locale.

  2. 2

    Provide context

    Each string carries a short note for the translator — what it labels, where it appears, and the variable types if any. A button label and a column header that read identically in en-GB often need different translations.

  3. 3

    Translate

    In-market translators, not machine translation. Machines miss the voice every time.

  4. 4

    QA in-app

    Pseudo-localise first to catch overflow. Then run each locale through the product on real screens — staff in-market or a localisation QA pass.

  5. 5

    Ship

    All locales ship together with the en-GB source. Don't ship en-GB and add Finnish two weeks later — the surface looks half-finished to half the audience.

In code

Two non-negotiables: every user-facing string lives in the i18n catalog, and every formatted value (date, time, currency, number) goes through Intl with an explicit locale. Hardcoded strings and hand-rolled formatters are the two ways localisation rots quietly.

tsx

// ✓ String through the catalog, locale on every Intl call
import { useTranslations, useLocale } from "@/lib/i18n";

export function PayoutSummary({ amount, when }: Props) {
  const t = useTranslations("payouts");
  const locale = useLocale();

  const money = new Intl.NumberFormat(locale, {
    style: "currency",
    currency: "EUR",
  }).format(amount);

  const date = new Intl.DateTimeFormat(locale, {
    day: "numeric",
    month: "long",
    year: "numeric",
  }).format(when);

  return <p>{t("summary", { money, date })}</p>;
}

Pseudo-localise on every PR

Add a CI step that swaps the en-GB strings for a pseudo-locale — bracketed, length-inflated versions like [Şéţţîñĝš ──]. It catches hardcoded strings, missing keys, and overflow at review time instead of in production.

Page history

1 revision
  1. Documented
    Derek Fidler@derekfidler

    First documented version. Definitions, market matrix, source-copy rules, plurals, RTL preparedness, and the translation handoff flow.