Guide 05 · Developer docs

Content types and custom entities

Which built-in type fits which purpose, how categories, tags, slugs, custom URLs and SEO settings behave across them, how revisions and publishing gate what the public sees, how to model a domain the platform has no type for, and where uploaded files live.

L1–L3
Unit 02 · Taxonomy, URLs and SEO

Translations: source and related records

A translation is a record of its own, in its own language, linked to the record it was translated from. How the source and related links are wired, which records have them, and how a template finds a record's equivalent in another language for hreflang links and a language switcher.

Who this is for. The content modeller planning a multi-language site and the theme developer writing its language switcher. Afterwards you can wire translated records together and find any record's version in another language from a template. Interface strings, the words in the templates themselves, are a separate subject: Interface strings.

The model

Every content record has a language. A translation is a separate record in its own language: a German page is a page, with its own slug, revisions, layout and publish date. It names the record it was translated from as its source, and the source lists its translations as its related records. The links can be edited in the admin once the account setting Enable localization is on; it is off by default.

RecordsSourceRelated
Pages, book pagesgetSourcePage()getRelatedPages()
BlogsgetSourceBlog()getRelatedBlogs()
Blog postsgetSourcePost()getRelatedPosts()
FAQ pagesgetSourcePage()getRelatedPages()
FAQ questionsgetSourceQuestion()getRelatedQuestions()

Books, units and custom entity records have a language but no source and related links. A site that translates custom entity records models the link itself, with a relationship from each translation to the original record.

Where the translations sit is the site's choice. A common shape is one tree per language under the home page, /en/... and /de/..., each page linked to its counterpart; another is a site per language. The links do not depend on either.

Finding the equivalents from a template

getOtherLanguageVersions(), on pages, book pages, blogs, posts, FAQ pages and questions, returns the whole family, keyed by language code (en-au, de-de): the record itself, its source, and the source's other translations, whichever member of the family you call it on. The version in the site's default language is listed a second time under x-default. That is the list a set of hreflang links needs, and the list a language switcher shows:

Template code In the head, then in the header
{$versions = $page->getOtherLanguageVersions()}
{if count($versions) > 2}
  {foreach $versions as $code => $version}
    <link rel="alternate" hreflang="{$code}" href="{$_request->getSchemeAndHost()}{$version->getFullPath()}">
  {/foreach}
{/if}

<nav class="languages">
  {foreach $versions as $code => $version}
    {if $code != 'x-default' && $version->getId() != $page->getId()}
      <a href="{$version->getFullPath()}" hreflang="{$code}">{$version->getLanguage()->getName()|escape}</a>
    {/if}
  {/foreach}
</nav>
{% set versions = page.getOtherLanguageVersions() %}
{% if versions|length > 2 %}
  {% for code, version in versions %}
    <link rel="alternate" hreflang="{{ code }}" href="{{ _request.getSchemeAndHost() }}{{ version.getFullPath() }}">
  {% endfor %}
{% endif %}

<nav class="languages">
  {% for code, version in versions %}
    {% if code != 'x-default' and version.getId() != page.getId() %}
      <a href="{{ version.getFullPath() }}" hreflang="{{ code }}">{{ version.getLanguage().getName()|e }}</a>
    {% endif %}
  {% endfor %}
</nav>

A page in the default language with no translations returns itself twice, under its own code and under x-default, which is why the hreflang block waits for more than two entries. To link to one particular language, look it up by its code and guard the lookup, because a record that has not been translated into that language has no entry:

Template code A link to the German version, when there is one
{$versions = $page->getOtherLanguageVersions()}
{if array_key_exists('de-de', $versions)}
  <a href="{$versions['de-de']->getFullPath()}" hreflang="de-de">Deutsch</a>
{/if}
{% set versions = page.getOtherLanguageVersions() %}
{% if versions['de-de'] is defined %}
  <a href="{{ versions['de-de'].getFullPath() }}" hreflang="de-de">Deutsch</a>
{% endif %}

The sitemap uses the same links to list each record's alternates.

Each language's records form their own tree, so menus, child lists and breadcrumbs stay within one language without any filtering, as long as each language has its own section. The language switcher is the one place that crosses between them, and it should link to the equivalent record, not to the other language's home page: a reader switching language on a product page expects the same product. Where a record has no translation, link to the nearest translated ancestor or leave the language out of the switcher for that page. Translated content and the language switcher covers the switcher in full, and Multi-brand and languages the choice between one site and a site per language.

Go deeper

Was this page helpful?

Your answer goes to the docs team.