---
title: "Content languages | Rigel CMS docs"
url: "https://rigelcms.dev/docs/content-languages"
description: "Organize localized pages, article collections and copy with explicit locale metadata, safe editor switching and host-owned Astro routes."
---

Content languages are available since Rigel 1.2.0. They are opt-in and separate from [interface languages](/docs/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](/docs/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](/docs/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](https://github.com/rigel-cms/rigel-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