Content languages

Content languages are available since Rigel 1.2.0. They are opt-in and separate from interface languages: contentLocales filters the content you edit, while languages selects translations of editor controls. Neither option translates your website.

Declare localized document types

Each document type can have its own fields, allowed sections, initial content and Astro layout. Locale does not select a layout or limit you to one document type per language. For example, English product pages and English landing pages can be separate types, just as English and French pages can have different structures.

Create src/content/schema.ts:

import { defineDocument, defineSchema } from "@rigel-cms/rigel/content";

export default defineSchema({
  documents: [
    defineDocument({
      name: "pageEn",
      title: "English pages",
      type: "document",
      locale: "en",
      folder: "src/content/pages/en",
      fields: [
        { key: "title", label: "Title", type: "string", required: true },
        { key: "intro", label: "Introduction", type: "text" },
      ],
    }),
    defineDocument({
      name: "pageFr",
      title: "Pages françaises",
      type: "document",
      locale: "fr",
      folder: "src/content/pages/fr",
      fields: [
        { key: "title", label: "Titre", type: "string", required: true },
        { key: "summary", label: "Résumé", type: "text" },
        { key: "contactUrl", label: "Contact", type: "url" },
      ],
    }),
  ],
  objects: [],
});

Rigel reads locale metadata, not folder names, document names or URL prefixes. The annotation belongs to the document type; its records inherit that language. Leave a type unannotated when its records should be visible in every language. A shared: true type can also have a locale if its records are language-specific.

Configure languages, collections and previews

In astro.config.mjs, import that schema and declare each article collection’s locale separately. This is a complete integration configuration; preserve any other integrations your site needs:

import { defineConfig } from "astro/config";
import { rigel } from "@rigel-cms/rigel";
import schema from "./src/content/schema";

export default defineConfig({
  integrations: [
    rigel({
      backend: "local",
      schema,
      contentLocales: {
        defaultLocale: "en",
        locales: [
          { code: "en", label: "English" },
          { code: "fr", label: "Français" },
        ],
      },
      collections: [
        {
          name: "blogEn",
          label: "Blog",
          locale: "en",
          folder: "src/content/blog/en",
          fields: [
            { key: "title", label: "Title", type: "string", required: true },
          ],
        },
        {
          name: "blogFr",
          label: "Articles",
          locale: "fr",
          folder: "src/content/blog/fr",
          fields: [
            { key: "title", label: "Titre", type: "string", required: true },
          ],
        },
      ],
      preview: {
        documents: {
          pageEn: { url: "/en/{slug}/" },
          pageFr: { url: "/fr/{slug}/" },
        },
        collections: {
          blogEn: { url: "/en/blog/{slug}/", new: "/preview/blog-en/" },
          blogFr: { url: "/fr/blog/{slug}/", new: "/preview/blog-fr/" },
        },
      },
    }),
  ],
});

locales must be a nonempty list of unique codes and nonblank labels. Codes accept language tags such as en or fr-CA, not arbitrary identifiers. Codes must be unique ignoring case; defaultLocale and each annotated locale must exactly match a configured code. These content codes are not restricted to the nine built-in interface catalogs.

Omit contentLocales to keep existing unfiltered navigation and no content-language selector. A single configured locale also shows no selector; its content and unannotated content remain visible. Restart the development server after changing configuration and rebuild before deployment.

The URL mappings above describe routes you must implement in Astro, not routes Rigel generates. See Preview for template, index, bindings and prepared new-entry routes. A new schema document needs a first Save before its canvas is available. Article collection new routes must render a prepared preview for unsaved articles.

Add content and render it in Astro

Create src/content/pages/en/welcome.json:

{
  "_id": "pageEn:welcome",
  "_type": "pageEn",
  "slug": "welcome",
  "title": "Welcome",
  "intro": "Meet our studio.",
  "draft": false
}

Create src/content/pages/fr/welcome.json:

{
  "_id": "pageFr:welcome",
  "_type": "pageFr",
  "slug": "welcome",
  "title": "Bienvenue",
  "summary": "Découvrez notre studio.",
  "contactUrl": "/fr/contact/",
  "draft": false
}

Follow Getting started to add the virtual module types and render documents from virtual:rigel-content. Your /en/[slug].astro route should select pageEn documents and render intro; /fr/[slug].astro should select pageFr and render summary and contactUrl. Those routes can import different layouts. Filter drafts from production routes, and add explicit preview bindings to editable elements. Create collection articles through the editor; their JSON bodies are Lexical state, not Markdown strings.

The same storage slug can exist in different document types when their configured public routes differ: these examples map to /en/welcome/ and /fr/welcome/. Document slug validation compares public routes across non-shared types and fixed copy-page mappings. A different locale alone does not prevent a collision: without distinct mappings, both types fall back to /{slug}/ and conflict. Same-type storage IDs remain unique. Preview templates are not public-route namespaces.

Annotate existing copy pages

Existing defineCopy modules accept locale too. For example, src/content/copy/contact-fr.ts can contain:

import { defineCopy } from "@rigel-cms/rigel/content";

export default defineCopy({
  slug: "contact-fr",
  label: "Contact français",
  locale: "fr",
  groups: [
    {
      id: "hero",
      label: "En-tête",
      fields: [{ key: "heading", label: "Titre", type: "string" }],
    },
  ],
  values: { hero: { heading: "Contactez-nous" } },
});

Add pages: { "contact-fr": "/fr/contact/" } inside the existing preview object and implement that Astro page with its copy bindings. This copy page is independent of the JSON document types above; no translation relationship is inferred. GitHub mode still requires data-only, literal defineCopy declarations.

Switch content safely

With multiple locales, Content language appears in the sidebar directly below Search. In the collapsed desktop sidebar it shows the uppercase code, such as EN. The dropdown filters navigation, page lists and collections; new editor sessions start with defaultLocale. Global search has its own scope, described below.

When switching with unsaved changes, choose Save, Discard or Keep editing. Save switches only after a successful save; a failure keeps the current language and edits. Discard switches without saving. Keep editing, Escape or closing the dialog cancels the switch, preserves the selected language and edits, and returns focus to the selector. After a successful switch, focus returns to that control and content hidden by the new filter is closed. A still-visible shared selection can remain open.

Unannotated content stays visible in every language. Use it for shared authors, settings, logos or other common records; media and menus are not localized by this filter. Filtering is not authorization: the manifest and publishing batch still contain the site’s complete content. Repository access and the shared drafts publishing workflow do not change.

Search scope

Since 1.4.0, global search covers all content languages by default, even when the sidebar shows one language. Language badges distinguish localized results. Choosing another language’s result switches the content language and opens the entry; unsaved changes still require Save, Discard or Keep editing.

Set search: { scope: "currentLocale" } in rigel() to search only the selected content language and unannotated content. Settings > Content lets an editor inherit the site default or override it in that browser. This preference does not change the configured locales, repository access or publishing batch. Settings > Interface changes the language of editor controls independently.

What Astro still owns

Your host owns public URLs, HTML lang, translated navigation, public language-switch links and fallback behavior. Rigel does not automatically translate text, pair translated entries or infer localized routes. It does not copy changes between languages or rewrite your website navigation when you switch the editor filter.

The public starter includes an optional English/French example. Enable it with RIGEL_CONTENT_MULTILANG=true pnpm dev or RIGEL_CONTENT_MULTILANG=true pnpm build; its default remains single-language. The example uses separate page layouts and article collections with shared records and existing site navigation. Replace demonstration content and implement your own public language navigation before launch.

Documentation

On this page