Internationalization

Locale files load per tier, site, layout, page, driven by route meta, and only fetch the files that aren't already cached for the active locale. That feeds into the same load-state used for page transitions, so a page doesn't reveal until its translations have actually arrived. Switching locale replays whatever file set is currently loaded against the new locale, skipping anything already cached rather than refetching everything.

That covers when a file loads. This doc is about the other half, how the keys inside those files are organized once they're there.

Resolving the locale

The active locale is resolved once on load, taken from the first of three sources that answers: a locale the visitor has already settled on, the language their browser asks for, or the configured default. The browser match prefers an exact regional locale like fr-CA and falls back to any French locale when the browser only asks for fr. Whatever it resolves to is remembered from then on, so the browser is only ever consulted on a first visit and every load after stays on the same locale.

Locale and auth

The account carries its own locale, applied on login, so a signed-in user's preference survives across devices instead of living only in the browser it was set from. That locale is written at registration from whatever was active at signup, whether auto detected or chosen from the auth layout switcher.

Files are the namespace, not the tier

Whatever a locale file exports gets merged into the message tree under the file's own name, not under the tier it was loaded through. A file named site.json loaded through the site tier lands at site.*. The tier only decides when the file gets fetched. The filename decides where its keys live when you call $t().

Organize by feature

Features are the main glue logic in this codebase, most of what happens, including the text, happens there. So a locale file's top-level keys are feature names, and a feature's translations live together under its own key rather than scattered across a generic strings file.

In practice this means the file itself is usually named after the feature it belongs to, and gets loaded under whichever tier matches how widely that feature is actually used, site tier if it shows up everywhere, layout tier if it's shared across the pages under one layout, page tier if it's scoped to a single page. site.json is the exception. It's the file that's always loaded, so besides hosting any truly site-wide feature it's also home to the two standing cross-cutting namespaces below.

common and rules

common holds the repetitive stuff, button labels and generic actions like cancel, save, delete, refresh. These are strings whose wording doesn't change based on where they show up, so they don't belong to any one feature.

rules will do the same job for form validation once that lands. A required field says the same thing wherever it's required, so that message belongs at rules.required, not duplicated inside every feature that happens to have a required field.

Default to common, go custom when it actually diverges

The working rule, default to common.<key> for anything repetitive. If a feature ever needs different wording for that same control, add it directly under the feature, <filename>.<feature>.<key>, with the key spelled out in full, message, button, title, not abbreviated. Translators and whoever touches this file later shouldn't need a legend to read it.

This is what keeps the two namespaces easy to move between. common.refresh is just a string like any other, so before repurposing or deleting it, grep for who's still calling it. If nothing else in the app needs it, moving it under the one feature that does is a plain find-and-replace, not a restructure.

See also

Component Architecture draws the same primitive vs. composite line for markup, common is that same idea applied to text.

© Websanova 2026 About Privacy