We are actively hiring talented engineers and designers. Check our open positions. View openings
← Back to blog Web Development

Storing translations without a column per language

A title_en and title_ar pair works until the third language. Here is the storage shape we standardised on across every bilingual product we run.

S

Super Admin

Author

Sep 23, 2026 · 5 min read · 5 views

Every bilingual project starts the same way: add title_en and title_ar and move on. It works. It keeps working until a client asks for French, at which point the cost of the shortcut arrives all at once — a migration on every content table, a change to every form, every table column, every query that touched a title.

One JSON column per translatable attribute

We store translations as a keyed map inside a single JSON column: the attribute is title, and its value is a map of locale to text. Adding a language becomes a configuration change rather than a schema change, and the model exposes the attribute as a plain string in the active locale.

Decide the fallback deliberately

The question that actually matters is what happens when a translation is missing. Falling back to the default locale keeps pages whole, which is usually right for marketing content. For legal pages it is often wrong: a partially translated privacy policy is worse than a page that says the translation is pending. Make that call per content type rather than globally.

Keep the keys identical across stacks

When a web panel and a mobile client render the same content, they must agree on locale keys exactly. We treat the key list as a shared contract and validate it in continuous integration, because a mismatch here fails silently — the string simply renders empty for one audience.

Comments

Sign in to join the discussion.

We use cookies to enhance your experience, analyze traffic, and deliver personalized content. By clicking "Accept All", you consent to our use of cookies.