Skip to content

I18n Content Structures

The structure i18n option defines how localized content is stored on disk. It is set in the top-level or collection-level configuration. Sveltia CMS supports five different structures:

  • single_file: All locales are stored in a single file.
  • single_file_default_root: Like single_file, but the default locale’s fields are stored at the root level of the file (without a locale key), while non-default locales are nested under their locale key. This is designed to support Lume’s Multilanguage plugin.
  • multiple_files: Each locale has its own file with the locale code in the filename.
  • multiple_folders: Each locale has its own folder containing the localized files.
  • multiple_root_folders: Each locale has its own root folder containing all collections.

Deprecation Notice

The multiple_folders_i18n_root structure is deprecated and will be removed in Sveltia CMS v1.0.0. Use multiple_root_folders instead, which has a more intuitive name and the same file structure.

Some frameworks and static site generators may have specific requirements for multilingual content organization. Choose the structure that best fits your project’s needs.

When using a structure other than single_file, Sveltia CMS automatically links localized entries and files based on their filenames or folder paths. From the Content Editor perspective, they appear as a single entry or file with multiple locales, regardless of the underlying file structure. There’s no need to manually link translations.

Entry Collections

The structure i18n option defines how localized content is stored for entry collections. Below are examples of each structure type.

Single File

yaml
i18n:
  structure: single_file
  locales: [en, de, fr]
toml
[i18n]
structure = "single_file"
locales = ["en", "de", "fr"]
json
{
  "i18n": {
    "structure": "single_file",
    "locales": ["en", "de", "fr"]
  }
}
js
{
  i18n: {
    structure: "single_file",
    locales: ["en", "de", "fr"],
  },
}

The single_file structure stores all locales in a single file:

/<folder>/<path>.<extension>

The file path remains the same for all locales:

.
└─ content/
   └─ pages/
      └─ about.md

And the file contains all locales in a single file, with locale keys as top-level properties:

yaml
de:
  title: Über uns
  content: Dies ist die deutsche Version der Seite.
en:
  title: About Us
  content: This is the English version of the page.
fr:
  title: À propos de nous
  content: Ceci est la version française de la page.

Single File (Default Root)

yaml
i18n:
  structure: single_file_default_root
  locales: [en, de, fr]
toml
[i18n]
structure = "single_file_default_root"
locales = ["en", "de", "fr"]
json
{
  "i18n": {
    "structure": "single_file_default_root",
    "locales": ["en", "de", "fr"]
  }
}
js
{
  i18n: {
    structure: "single_file_default_root",
    locales: ["en", "de", "fr"],
  },
}

The single_file_default_root structure stores all locales in a single file, just like single_file. The difference is that the default locale’s fields are written at the root level of the file, while non-default locales are nested under their locale key:

yaml
lang: [en, de, fr] # auto-generated list of all enabled locales
title: About Us
content: This is the English version of the page.
de:
  title: Über uns # default locale (de) at root
  content: Dies ist die deutsche Version der Seite.
fr:
  title: À propos de nous
  content: Ceci est la version française de la page.

This format is compatible with Lume’s Multilanguage plugin, which resolves field values from the root level for the default locale and from the nested locale keys for other locales. The lang field is automatically maintained by Sveltia CMS and lists all currently enabled locales.

Limitation

Unlike the examples in Lume’s documentation, all non-default locales will have a complete set of fields, even if their values are the same as the default locale. Make sure to keep the non-default locale fields in sync with the default locale when making changes manually, otherwise you may end up with inconsistent content across locales.

Multiple Files

yaml
i18n:
  structure: multiple_files
  locales: [en, de, fr]
toml
[i18n]
structure = "multiple_files"
locales = ["en", "de", "fr"]
json
{
  "i18n": {
    "structure": "multiple_files",
    "locales": ["en", "de", "fr"]
  }
}
js
{
  i18n: {
    structure: "multiple_files",
    locales: ["en", "de", "fr"],
  },
}

The multiple_files structure creates separate files for each locale, with the locale code included in the filename:

yaml
/<folder>/<path>.<locale>.<extension>

The file paths for each locale would look like this:

yaml
.
└─ content/
   └─ pages/
      ├─ about.de.md  # German
      ├─ about.en.md  # English (default locale)
      └─ about.fr.md  # French

Omitting Default Locale from File Path

When the omit_default_locale_from_file_path option is set to true, the path depends on the locale being the default locale or not:

yaml
/<folder>/<path>.<extension> # default locale
/<folder>/<path>.<locale>.<extension> # other locales

For example, with en as the default locale, the English file would be named about.md, while the German and French files would retain the locale suffix:

yaml
.
└─ content/
   └─ pages/
      ├─ about.md     # English (default locale)
      ├─ about.de.md  # German
      └─ about.fr.md  # French

Multiple Folders

yaml
i18n:
  structure: multiple_folders
  locales: [en, de, fr]
toml
[i18n]
structure = "multiple_folders"
locales = ["en", "de", "fr"]
json
{
  "i18n": {
    "structure": "multiple_folders",
    "locales": ["en", "de", "fr"]
  }
}
js
{
  i18n: {
    structure: "multiple_folders",
    locales: ["en", "de", "fr"],
  },
}

The multiple_folders structure creates separate folders for each locale, containing the localized files:

yaml
/<folder>/<locale>/<path>.<extension>

The file paths for each locale would look like this:

yaml
.
└─ content/
   └─ pages/
      ├─ de/
      │  └─ about.md  # German
      ├─ en/
      │  └─ about.md  # English (default locale)
      └─ fr/
         └─ about.md  # French

Omitting Default Locale from File Path

When the omit_default_locale_from_file_path option is set to true, the path depends on the locale being the default locale or not:

yaml
/<folder>/<path>.<extension> # default locale
/<folder>/<locale>/<path>.<extension> # other locales

For example, with en as the default locale, the English file would be located at content/pages/about.md, while the German and French files would be located in their respective locale folders:

yaml
.
└─ content/
   └─ pages/
      ├─ about.md     # English (default locale)
      ├─ de/
      │  └─ about.md  # German
      └─ fr/
         └─ about.md  # French

Multiple Root Folders

yaml
i18n:
  structure: multiple_root_folders
  locales: [en, de, fr]
toml
[i18n]
structure = "multiple_root_folders"
locales = ["en", "de", "fr"]
json
{
  "i18n": {
    "structure": "multiple_root_folders",
    "locales": ["en", "de", "fr"]
  }
}
js
{
  i18n: {
    structure: "multiple_root_folders",
    locales: ["en", "de", "fr"],
  },
}

The multiple_root_folders structure creates separate root folders for each locale, containing all collections:

yaml
/<locale>/<folder>/<path>.<extension>

The file paths for each locale would look like this:

yaml
.
├─ de/
│  └─ pages/
│     └─ about.md  # German
├─ en/
│  └─ pages/
│     └─ about.md  # English (default locale)
└─ fr/
   └─ pages/
      └─ about.md  # French

Omitting Default Locale from File Path

When the omit_default_locale_from_file_path option is set to true, the path depends on the locale being the default locale or not:

yaml
/<folder>/<path>.<extension> # default locale
/<locale>/<folder>/<path>.<extension> # other locales

For example, with en as the default locale, the English file would be located at pages/about.md, while the German and French files would be located in their respective locale root folders:

yaml
.
├─ pages/
│  └─ about.md     # English (default locale)
├─ de/
│  └─ pages/
│     └─ about.md  # German
└─ fr/
   └─ pages/
      └─ about.md  # French

Nested Collections

A nested collection organizes entries into a folder tree, which is a separate thing from the folders and file names i18n adds. The two combine: the locale is taken off the file path before the tree is worked out, so the same tree appears whichever structure you choose, and the localized files of one entry are linked as usual.

The examples below use a pages collection with a products/hardware entry. In the default subfolders mode, where each entry is an index file in a folder of its own:

yaml
# single_file
content/pages/products/hardware/_index.md

# multiple_files
content/pages/products/hardware/_index.en.md
content/pages/products/hardware/_index.de.md

# multiple_folders
content/pages/en/products/hardware/_index.md
content/pages/de/products/hardware/_index.md

# multiple_root_folders
en/content/pages/products/hardware/_index.md
de/content/pages/products/hardware/_index.md

And with subfolders: false, where each entry is a regular file that keeps its own name:

yaml
# single_file
content/pages/products/hardware.md

# multiple_files
content/pages/products/hardware.en.md
content/pages/products/hardware.de.md

# multiple_folders
content/pages/en/products/hardware.md
content/pages/de/products/hardware.md

# multiple_root_folders
en/content/pages/products/hardware.md
de/content/pages/products/hardware.md

In every case the entry sits at products/hardware in the tree, and the sidebar shows one products folder rather than one per locale. The omit_default_locale_from_file_path option works as it does elsewhere, dropping the locale from the default locale’s path only.

A few things worth knowing:

  • The depth option counts the path segments below the collection folder, and the locale is not one of them. A locale folder or file-name suffix therefore never uses up part of the depth budget.
  • Moving an entry with the path editor moves every locale’s file in the same commit. In the subfolders mode it takes the entry’s child entries along, in every locale.
  • Entry-relative media is stored once, not once per locale. With the multiple_folders and multiple_root_folders structures, where each locale has a folder of its own, the file is saved in the default locale’s folder and every locale’s entry refers to it by the same relative path. The other structures keep each locale’s file in the folder holding the entry, so the media already sits beside all of them.

Localized Folder Names

The folder names can be localized as well, giving nested content fully localized permalinks. In the subfolders mode, a folder is named after the slug of the entry stored in it, so localizing the entry slugs with the localize filter localizes the whole folder chain above an entry along with the entry itself. With the multiple_folders structure, a products/hardware entry in English could then be stored as follows:

yaml
content/pages/en/products/hardware/_index.md
content/pages/de/produkte/hardware/_index.md

The tree in the sidebar and the path editor still go by the default locale’s names, since that’s how the entries are identified, but the other locales’ path editor panes show the folder as it’s named in their own locale. The localized folder names are taken from the entries stored in the folders, including unpublished ones with the Editorial Workflow, so a section and its sub-pages can be created in one sitting. A folder whose entry lacks a locale keeps its default name in that locale.

When an entry is moved to a different folder, each locale’s file goes below the localized folder chain, and its child entries are moved along in every locale. Renaming the folder is done with the Slug Editor, which offers a folder name per locale when the slugs are localized.

This requires the multiple_files or multiple_folders structure, like localized slugs in general. With subfolders: false, entries are files rather than folders, so a folder takes its localized name from the entry it belongs to: its index file, as named with the meta.path.index_file option or conventionally index or _index, or else a file of the same name stored beside it, such as about.md next to the about folder in an Eleventy or Jekyll project. A folder with neither keeps the same name in every locale.

Because an entry in a nested collection is identified by its path within the collection folder rather than by a bare slug, that path is what the translationKey property holds there, e.g. products/hardware/_index, so two entries with the same slug in different folders stay apart. When an entry is moved, the property is updated in every file it takes along.

Page Bundles

A page bundle keeps an entry’s content file and its media together in one folder. Give a collection a path option ending in a fixed file name to store each entry as a leaf bundle, or use the subfolders mode of a nested collection to store each one as a branch bundle. Combined with entry-relative media, uploads land in the bundle instead of a shared media folder.

The examples below use a posts collection with path: '{{slug}}/index', media_folder: '' and public_folder: '', holding a my-first-post entry with a cover image.

The single_file and multiple_files structures keep every locale in the same bundle, so the media sits beside all of them. The latter is how Hugo’s own translation by file name is organized:

yaml
# single_file
content/posts/my-first-post/index.md      # cover: image1.jpg, under each locale key
content/posts/my-first-post/image1.jpg

# multiple_files
content/posts/my-first-post/index.en.md   # cover: image1.jpg
content/posts/my-first-post/index.de.md   # cover: image1.jpg
content/posts/my-first-post/image1.jpg

The multiple_folders and multiple_root_folders structures give each locale a bundle of its own. The media is not copied into each one: it’s stored in the default locale’s bundle, and every locale’s entry refers to it by the same relative path:

yaml
# multiple_folders
content/posts/en/my-first-post/index.md   # cover: image1.jpg
content/posts/de/my-first-post/index.md   # cover: image1.jpg
content/posts/en/my-first-post/image1.jpg

# multiple_root_folders
en/content/posts/my-first-post/index.md   # cover: image1.jpg
de/content/posts/my-first-post/index.md   # cover: image1.jpg
en/content/posts/my-first-post/image1.jpg

Sveltia CMS resolves that path across locales, so the image appears in the content editor whichever locale you are editing. Hugo resolves it as well, because a page bundle inherits the resources of its translated pages — but only where the two are linked as translations, which Hugo determines from each language’s contentDir. If your framework has no equivalent mechanism, prefer single_file or multiple_files, which keep the media in the same bundle as every locale’s content file.

File Collections

The structure i18n option only applies to entry collections, except for the single_file_default_root structure as described below. Other structure option values are ignored for file collections, and the organization of localized files is determined by the file path option in the file collection configuration.

You can use the {{locale}} placeholder in the file path option to create separate files or folders for each locale. If the placeholder is not used, the structure defaults to single_file, meaning the same file is shared across all locales.

The following example demonstrates how to set up a file collection with different i18n structures using the {{locale}} placeholder:

yaml
collections:
  - name: pages
    label: Pages
    i18n: true
    files:
      - name: contact
        label: Contact Page
        i18n: true
        file: content/contact.md # equivalent to single_file
      - name: about
        label: About Page
        i18n: true
        file: content/about.{{locale}}.md # multiple_files
      - name: products
        label: Products Page
        i18n: true
        file: content/{{locale}}/products.md # multiple_folders
      - name: settings
        label: Site Settings
        i18n: true
        file: '{{locale}}/settings.yaml' # multiple_root_folders
toml
[[collections]]
name = "pages"
label = "Pages"
i18n = true

[[collections.files]]
name = "contact"
label = "Contact Page"
i18n = true
file = "content/contact.md"

[[collections.files]]
name = "about"
label = "About Page"
i18n = true
file = "content/about.{{locale}}.md"

[[collections.files]]
name = "products"
label = "Products Page"
i18n = true
file = "content/{{locale}}/products.md"

[[collections.files]]
name = "settings"
label = "Site Settings"
i18n = true
file = "{{locale}}/settings.yaml"
json
{
  "collections": [
    {
      "name": "pages",
      "label": "Pages",
      "i18n": true,
      "files": [
        {
          "name": "contact",
          "label": "Contact Page",
          "i18n": true,
          "file": "content/contact.md"
        },
        {
          "name": "about",
          "label": "About Page",
          "i18n": true,
          "file": "content/about.{{locale}}.md"
        },
        {
          "name": "products",
          "label": "Products Page",
          "i18n": true,
          "file": "content/{{locale}}/products.md"
        },
        {
          "name": "settings",
          "label": "Site Settings",
          "i18n": true,
          "file": "{{locale}}/settings.yaml"
        }
      ]
    }
  ]
}
js
{
  collections: [
    {
      name: "pages",
      label: "Pages",
      i18n: true,
      files: [
        {
          name: "contact",
          label: "Contact Page",
          i18n: true,
          file: "content/contact.md",
        },
        {
          name: "about",
          label: "About Page",
          i18n: true,
          file: "content/about.{{locale}}.md",
        },
        {
          name: "products",
          label: "Products Page",
          i18n: true,
          file: "content/{{locale}}/products.md",
        },
        {
          name: "settings",
          label: "Site Settings",
          i18n: true,
          file: "{{locale}}/settings.yaml",
        },
      ],
    },
  ],
}

The resulting file structure would be:

yaml
.
├─ content/
│  ├─ contact.md      # shared across all locales (single_file)
│  ├─ about.de.md     # German
│  ├─ about.en.md     # English (default locale)
│  ├─ about.fr.md     # French
│  ├─ de/
│  │  └─ products.md  # German
│  ├─ en/
│  │  └─ products.md  # English (default locale)
│  └─ fr/
│     └─ products.md  # French
├─ de/
│  └─ settings.yaml   # German
├─ en/
│  └─ settings.yaml   # English (default locale)
└─ fr/
   └─ settings.yaml   # French

Omitting Default Locale from File Path

As with entry collections, the omit_default_locale_from_file_path option can be used to omit the locale code from the file path for the default locale when using the {{locale}} placeholder in file collections. The above example would result in the following file structure if en is the default locale and omit_default_locale_from_file_path is set to true:

yaml
.
├─ content/
│  ├─ contact.md      # shared across all locales (single_file)
│  ├─ about.md        # English (default locale)
│  ├─ about.de.md     # German
│  ├─ about.fr.md     # French
│  ├─ products.md     # English (default locale)
│  ├─ de/
│  │  └─ products.md  # German
│  └─ fr/
│     └─ products.md  # French
├─ settings.yaml      # English (default locale)
├─ de/
│  └─ settings.yaml   # German
└─ fr/
   └─ settings.yaml   # French

Single File (Default Root) Structure for File Collections

The single_file_default_root structure can also be used for file collections. The example below shows how to configure a file collection with this structure:

yaml
i18n:
  structure: single_file_default_root
  locales: [en, de, fr]

collections:
  - name: pages
    label: Pages
    files:
      - name: about
        label: About Page
        file: content/about.md
toml
[i18n]
structure = "single_file_default_root"
locales = ["en", "de", "fr"]

[[collections]]
name = "pages"
label = "Pages"

[[collections.files]]
name = "about"
label = "About Page"
file = "content/about.md"
json
{
  "i18n": {
    "structure": "single_file_default_root",
    "locales": ["en", "de", "fr"]
  },
  "collections": [
    {
      "name": "pages",
      "label": "Pages",
      "files": [
        {
          "name": "about",
          "label": "About Page",
          "file": "content/about.md"
        }
      ]
    }
  ]
}
js
{
  i18n: {
    structure: "single_file_default_root",
    locales: ["en", "de", "fr"],
  },
  collections: [
    {
      name: "pages",
      label: "Pages",
      files: [
        {
          name: "about",
          label: "About Page",
          file: "content/about.md",
        },
      ],
    },
  ],
}