Features / Model

Multilingual per field

localized: true on the field instead of one copy per language. One entry, each language as a branch — the API returns the requested language with fallback, writes go per language.

An event has a date, a venue, a speaker — and a title in German and English. In many systems that becomes one entry per language, and the date is stored twice until someone corrects only one copy. RustyCMS localizes per field: date, venue and speaker exist once; only the title carries a language map.

A flag on the field, not a folder per language

// types/page.json5
fields: {
  headline:    { type: "string", localized: true, required: true },
  excerpt:     { type: "string", localized: true },
  publishedAt: { type: "datetime" },                        // shared
  author:      { type: "reference", collection: "person" }, // shared
}

Storage is one entry per slug. Localized fields inside it are a map { de: "…", en: "…" }; everything else is stored once. No de/ or en/ folder, no question which copy is the source of truth. Localize only what differs by language — dates, references and layout switches stay shared.

Reading: ask for one language, done

GET /api/content/page/home?_locale=en     # headline: "Home"
GET /api/content/page/home?_locale=de     # headline: "Startseite"

The API unwraps the map and returns flat values; the frontend never sees language maps. If a branch is missing, the space’s default language steps in — a half-translated page has no holes. Resolved references are unwrapped too: an English page gets its author in English without the consumer doing anything.

Writing: touch only your own branch

The translator edits the English teaser and must not overwrite the German one. Add ?_locale=en&_locale_partial=true to the PUT: the server replaces only the en branch and leaves de alone. Sending the full map still works. In the admin UI you switch languages inside the form and edit one at a time.

Validation knows about branches

required is satisfied as soon as one language branch is filled — a draft may exist in German only for now. pattern, minLength, min/max apply per branch. unique is checked per language; a conflict is reported as title[de], not as a vague “duplicate title”. The reference index tells languages apart as well: renaming the German target only affects German content.

Languages per space

RUSTYCMS_LOCALES=de,en sets the global list; the first entry is the default. Every space can deviate — one client site adds fr, another runs de only — through the space settings in the admin UI or via the API. The admin UI also warns about stale language folders left on disk.

Moving from folder-per-language

If you used to keep de/ and en/ as separate directories, run the migration: a dry run shows what would happen, --write merges the files per slug. Old language folders are archived, not deleted. If a non-localized field disagrees between languages, you get a warning instead of a silent decision.

Want to see this on your own project?

Tell me briefly what you're planning. I'll show the admin UI on a real installation and say honestly whether it fits.

Request early access