saascode

Internationalization that can change after deployment

Database-backed i18n with runtime locale loading, fallbacks, caching, admin editing, language activation, localized content fields, and honest limits.

fabric · aug 21, 2026 · 4 min read

A database-backed i18n SaaS starter solves a different problem from a folder of JSON files. The question is not only whether the interface can show Spanish today. It is whether an owner can activate another language, edit a bad string, and publish that correction after deployment without rebuilding the application.

The included translation system stores languages and translation rows in Postgres. Each translation is identified by locale, namespace, and key. The runtime loads those rows into a flat dictionary, while the application calls a short translation function with interpolation support.

That structure keeps the code stable. A button still asks for common.save. The value behind that key can change in the database. New locales can carry the same namespace and key set without introducing another translation mechanism.

Runtime loading without a database query per label

The browser translation layer maintains an in-memory dictionary per locale and can persist it in local storage. It uses a cache-version value to invalidate stale dictionaries after a deployment adds keys. The default fallback is straightforward: current locale, default locale, then the raw key.

Server rendering has a separate cache. It loads a locale dictionary with a short time-to-live and falls back from an exact locale such as es-AR to its base language, then to English. The same server function can interpolate variables for metadata, transactional email, and other places where a client hook is not available.

The selected locale is stored in both local storage and a cookie. The cookie gives server-rendered routes a locale before the browser application starts. The translation function also updates the document language attribute, which helps assistive technology and search engines interpret the page correctly.

Locale values pass through a normalizer that produces a canonical short BCP 47 form. That keeps variants such as region-qualified locales predictable before the fallback chain splits them into an exact tag and a base language.

The public translation endpoint returns a flat map for a requested locale. The languages endpoint returns only active languages and orders the default first. It keeps the public hot path lean by omitting expensive translation counts unless the admin requests them.

What the owner can change

Translation writes are database-backed. The API can upsert a value by locale, namespace, and key. Database policies allow public reading while restricting language and translation writes to platform admin roles.

The admin translation surface supports inline editing. It can compare source and target locales, count missing keys, and run an optional bulk translation path through a configured language model. The resulting rows are still regular database translations, so an owner can review and correct them after generation.

Languages themselves are data. The language table stores a code, name, default state, and active state. The admin-facing tools can create a language row, and the public API immediately exposes active rows. That is the concrete meaning of “add a language after deployment”: the runtime does not need a new import just to discover the locale.

The i18n model also covers owner-authored content. A helper resolves localized JSON objects in this order: exact locale, base language, default locale, English, then the first available value. That shape is useful for content fields where one database row needs several language variants rather than a separate translation-key record.

Transactional email uses the recipient's stored locale when one exists. Email subjects and string slots resolve through the server translation function, with English as the fallback for a recipient who has no profile locale.

The honest limit

Translations are platform-global, not organization-specific. An explicit migration removed organization_id from both languages and translations. A multi-tenant product therefore has one shared interface vocabulary. Tenants cannot independently rewrite the same UI key in different ways through this layer.

Adding a language row does not create a good translation. It makes the locale available. Every key still needs a value, whether written by a person or generated and reviewed. Missing values fall back rather than crashing, which keeps the product usable but can also hide incomplete coverage if nobody checks the missing-key count.

The caches need deliberate invalidation. Database edits made through the admin path can clear the browser locale cache, while deployments that add keys must advance the cache version. A stale cache strategy can make a correct database value appear unchanged until expiry or invalidation.

Automatic translation is optional and depends on an external model credential. It is a drafting tool, not proof of linguistic quality. Product terminology, legal copy, and culturally specific language still need a human review.

There is one more boundary: the setup wizard has 15 packaged interface locale files, but those files are for the setup experience. They do not prove that every product screen has complete translations in all 15 languages. Runtime completeness comes from the database rows for the product's actual keys.

Why this starts in every product

Internationalization is cheap to preserve and expensive to retrofit. Hardcoded interface strings spread through pages, validation messages, email, metadata, and admin tools. Replacing them later becomes a product-wide rewrite.

A database-backed key system gives even an English-only launch a stable seam. The first language uses the same path as the second. The owner can correct copy without a release. Server and client rendering share the same vocabulary and fallback rules. Content can carry localized variants where a simple interface key is not enough.

The base does not promise that every language is already written. It supplies the mechanism that lets a real translation remain editable after the code ships. That is the part every product needs before international demand arrives.

end