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.
Tagged
Comments
Sign in to join the discussion.