All insightsEngineering

Multilingual is a data problem.

How we build for many languages from the first schema, and what goes wrong later.

Written by
Agan AliuFounder & CEO
Published
Reading time
6 min read
Rows of black letterpress blocks with raised brass letters

Most of what we build is used in more than one language. Test-Start and Instruktori AI work in Albanian and Macedonian. The Caffè de Paris site serves guests in Albanian, Macedonian and Turkish. Our own site is written in English.

In each case, the hard part was not translating words. It was deciding where text lives, what happens when a translation is missing, and how the product behaves when the same content has to make sense in two alphabets.

That is why we treat multilingual support as a data problem, settled in the first schema, rather than a translation task at the end.

Two kinds of text

The first decision is to separate two kinds of text that are often mixed together.

Interface text is part of the product: button labels, error messages, navigation, emails. It changes when the software changes, so it lives with the code, in translation files keyed by message.

Content is part of the business: menu items, service descriptions, exam questions, articles. It changes when the business changes, so it lives in the database, with every translatable field stored per language.

Mixing them causes trouble in both directions. Content kept in code means a developer must deploy to correct a menu description. Interface text kept in the database means one missing row can break a button.

A schema built for languages

For content, we model translations explicitly from the start. A simplified sketch, using driving-exam questions as the example:

SQL
CREATE TABLE question (
  id           UUID PRIMARY KEY,
  category     TEXT NOT NULL,  -- 'B' or 'C1'
  format       TEXT NOT NULL,  -- 'choice', 'sign' or 'matching'
  image_path   TEXT
);

CREATE TABLE question_text (
  question_id  UUID REFERENCES question (id),
  locale       TEXT NOT NULL,  -- 'sq' or 'mk'
  body         TEXT NOT NULL,
  explanation  TEXT,
  reviewed_at  TIMESTAMP,
  PRIMARY KEY (question_id, locale)
);

The structure says something important: a question is one thing with several texts. Its category, format and image are shared; only the words differ.

It also makes gaps visible. One query lists every question without a reviewed Macedonian text, which is far better than a candidate discovering the gap in the middle of a test.

A question is one thing with several texts.

Fallbacks are a product decision

Sooner or later a translation will be missing. The product needs a rule for what happens next, and the right rule depends on the content.

  • Interface text falls back to a default language, and the missing key is logged so it gets fixed.
  • Marketing content, such as a service description, can fall back to the main language.
  • Exam content should not fall back at all. A candidate practising in Macedonian should never meet an Albanian question halfway through a test. The question is left out until its translation exists.

Writing these rules down early prevents the worst outcome: a page that silently mixes languages.

Translations need owners

A schema can make room for languages, but someone still has to write and check the words. Decide who that is for each language before any content is written, and make the product support their work.

In practice that means a status for every translated text: missing, draft or reviewed. The reviewed_at column in the sketch above exists for this reason. Reviewers see a list of what is waiting for them, and nothing marked as draft appears in places where a mistake would matter, such as an exam.

Machine translation can be a useful first draft. It is never the last step for content people rely on. A native speaker reviews it, and the review is recorded.

Typography and alphabets

Albanian, Turkish and English use the Latin alphabet; Macedonian uses Cyrillic. That has consequences well beyond translation.

Fonts must cover every alphabet. A typeface chosen for its Latin letters may have no Cyrillic, or a weak one. If the browser falls back to a system font for Macedonian, the same page looks like two different brands. We check coverage for every language before a font is approved, including ë and ç in Albanian and ğ, ş and ı in Turkish.

Text length varies. The same label can be noticeably longer in one language than in another. Buttons, navigation and headings are designed with room to grow, and tested in the longest language, not the one the designer happens to read.

Letter case depends on the language. Turkish has a dotted and a dotless i, so uppercasing must use the right locale:

Java
"istanbul".toUpperCase(Locale.ENGLISH);               // ISTANBUL
"istanbul".toUpperCase(Locale.forLanguageTag("tr"));  // İSTANBUL

An uppercase heading built with the wrong locale is a small error that every native reader notices.

Dates, numbers and prices

Dates, numbers and currencies should be formatted by the platform's locale tools, never assembled by hand. The order of day and month, the decimal separator and the way a price is written all vary by language and country.

Using the standard tools (Intl in the browser, java.time and NumberFormat on the server) removes a whole category of bugs, and makes each new language cheaper to add.

Search across languages

Search is where multilingual products fail quietly. A few rules we follow:

  • Search the user's language first, then the others if that makes sense for the content.
  • Normalise letters, so someone typing "e" still finds "ë", and capitalisation never matters.
  • Never transliterate silently. Matching Cyrillic input to Latin text, or the reverse, can help, but it should be a deliberate choice tested with real queries.

Addresses and search engines

Each language gets its own address, such as /sq/ and /mk/, rather than one page that changes language based on a cookie. Visitors can share a link in the language they read, and search engines can index every version.

Pages tell search engines which versions belong together, so a Macedonian searcher is shown the Macedonian page. And we never switch language automatically based on a visitor's location: someone in Skopje may read Albanian, and someone in Tirana may prefer English. The visitor chooses, and the choice is remembered.

What goes wrong when languages come later

We are sometimes asked to add a language to an existing product. The same problems appear every time:

  1. Sentences assembled from fragments, such as "You have" + count + "bookings", which cannot be translated correctly because word order and plural rules differ between languages.
  2. One text column per field, with no room for a second language, so translation becomes a migration across every table.
  3. Hard-coded formats: dates built as strings, prices with a fixed symbol and separator.
  4. Layouts sized for one language, which break as soon as a longer word arrives.
  5. No owner for translations, so new content ships in one language and the others drift behind.

Each problem is fixable on its own. Together they explain why retrofitting a language costs much more than designing for it.

Decide it before the first schema

Every point above is cheap to get right at the start and expensive to fix later. The questions to settle first are short: which languages, who writes and reviews them, and which content may fall back and which may not.

Related: see where these questions are asked in a project →

What this means for your project

If your product will ever speak more than one language, we design for it in the first schema: content stored per language, explicit fallback rules, fonts that cover every alphabet, and locale-aware formatting and search. Adding a language later becomes content work, not a rebuild.

ShareLinkedIn (opens in a new tab)X (opens in a new tab)

Continue reading

Engineering

The handover is part of the product.

What owning your software really means, with a checklist you can run on any system.

Agan Aliu · Founder, ALYNO7 min read
Engineering

Good software starts before the first line of code.

Why we map the work before writing code, and how it shaped Test-Start's data model.

Agan Aliu · Founder, ALYNO3 min read