Multilingual

The multilingual feature builds on plone.app.multilingual and adds a translation-aware @translations endpoint, a “closest translation” fallback so language switching never dead-ends, content-type constraint checks when creating translations, protection for language root folders, and an optional fixed backend editing language.

Overview

Content lives under per-language root folders (for example /Plone/de and /Plone/en). Each translation group links the language variants of a content item. The frontend uses the @translations endpoint to render a language switcher that always points the visitor at the best available target in each language.

REST API

GET @translations

The @translations endpoint returns the available languages for the current content, with a resolved URL for each. For every supported site language it returns the direct translation if one exists, otherwise the closest translation: it walks up the parent chain of the translated branch and returns the nearest translated ancestor, falling back to the language root or site root. This guarantees every language entry has a usable URL.

GET /Plone/de/some-page/@translations HTTP/1.1
Host: localhost:8080
Accept: application/json
{
    "items": [
        {
            "@id": "http://localhost:8080/Plone/de/some-page",
            "language": "de",
            "current": true,
            "name": "German",
            "native": "Deutsch"
        },
        {
            "@id": "http://localhost:8080/Plone/en",
            "language": "en",
            "current": false,
            "name": "English",
            "native": "English"
        }
    ],
    "root": {
        "de": "http://localhost:8080/Plone/de",
        "en": "http://localhost:8080/Plone/en"
    }
}

Each item carries:

  • @id – the resolved URL for that language (direct or closest translation).

  • language – the language code.

  • current – whether this is the language of the current content.

  • name / native – the English and native display names of the language.

The root mapping gives the translation URLs of the current language root folder, useful for a top-level language switcher independent of the current page. Targets are filtered by view permission, so anonymous users only receive languages they may actually view.

const response = await fetch('/Plone/de/some-page/@translations', {
    headers: { 'Accept': 'application/json' }
});
const data = await response.json();
data.items.forEach(lang => {
    console.log(lang.language, lang.current, lang['@id']);
});

Inline expansion

@translations is also an expandable component. Request it inline with ?expand=translations:

const response = await fetch('/Plone/de/some-page?expand=translations', {
    headers: { 'Accept': 'application/json' }
});
const data = await response.json();
const translations = data['@components']['translations'];
console.log(translations.items);

Translation management

Creating translations goes through plone.app.multilingual, with two behavioral additions:

  • Content-type constraints are enforced. When a translation is requested for a language, the target language folder is checked for whether it allows the content type. If the type is not addable there, the request is refused with an explanatory status message instead of creating an orphaned object.

  • Language root folders are protected. The language root folders themselves cannot be disconnected from their translation group; the disconnect action is hidden for them and blocked server-side. Regular content can still be connected and disconnected normally.

Fixed backend language

By default the editing UI follows the content language. The registry record wcs.backend.backend_language lets you pin a fixed language for authenticated editors regardless of the content language they are viewing.

wcs.backend.backend_language A language code (for example de). When set and present in the site’s available languages, logged-in users (those carrying the authentication cookie) get the editing interface in this language while anonymous visitors continue to be served in the content’s own language. Empty by default, meaning the standard per-content negotiation applies.