Localizely logo
  • Home
  • Getting started
  • How it works
  • Pricing
  • FAQ
Sign up

I18n questions · Software localization

How should I structure a database for multilingual content? 

First decide what is actually multilingual. Interface strings (button labels, validation messages, email templates) belong in resource files that ship with the code and are translated in a TMS such as Localizely; they do not need tables. What belongs in the database is content that users or editors create per language: product names, articles, category descriptions.

For that content there are three schema patterns.

A translation table per entity is the usual recommendation. The parent row holds language-neutral data and a child table holds one row per locale:

CREATE TABLE products (
  id          bigint PRIMARY KEY,
  sku         text NOT NULL,
  price_cents integer NOT NULL
);

CREATE TABLE product_translations (
  product_id  bigint NOT NULL REFERENCES products (id) ON DELETE CASCADE,
  locale      text NOT NULL,   -- BCP 47 tag: 'en', 'de', 'pt-BR'
  name        text NOT NULL,
  description text,
  PRIMARY KEY (product_id, locale)
);

Adding a language is an insert, not a migration; NOT NULL works per field; and you can index, search and sort per locale with ordinary SQL. The cost is one join per query and some ORM boilerplate.

One column per language (name_en, name_de, name_fr) is the simplest to query and fine for two or three languages that will never change, but every new language is a schema migration across every translated table, and rows get wide quickly.

A JSON column (name jsonb holding {"en": "Chair", "de": "Stuhl"}) is flexible and keeps the row count flat. PostgreSQL's jsonb can be indexed with GIN, but you give up per-locale constraints such as NOT NULL or uniqueness, need an expression index for every key you sort or search on, and queries get harder to express in an ORM. Reasonable for loosely structured content, weak for catalogs.

Whichever pattern you pick:

  • Store locales as BCP 47 tags (en, de-AT, zh-Hant) in one consistent casing, the same tags your app uses for its UI locales.
  • Implement fallback in the query layer, not in the schema: fetch the requested locale and the default locale, take the first non-null value, and an untranslated product still renders.
  • Record which locale is the source text, so you know what to retranslate when an editor changes it.
  • For a website, also decide the URL strategy per language (/de/... or de.example.com) and emit hreflang links between the versions; the hreflang generator produces the tag set.

Published Oct 9, 2026 · Markdown version

Related software localization questions:

Localization workflow:

Learn how a localization workflow works
Localizely logo
About
Free tools
Boring stuff
  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • Compare
Newsletter
Copyrights 2026 © Localizely