Translations#

Note

Translations are implemented for Plone 5 or greater.

Since Plone 5, the product plone.app.multilingual is included in the base Plone installation. It is not enabled by default.

Site interface texts include the configuration menus, error messages, information messages, and other static text. Multilingualism in Plone not only allows the managers of the site to configure the site interface texts to be in one language or another, but also to configure Plone to handle multilingual content. To achieve that, Plone provides the user interface for managing content translations.

You can get additional information about the multilingual capabilities of Plone in the documentation.

In connection with those capabilities, plone.restapi provides a @translations endpoint to handle the translation information of the content objects.

Once we have installed plone.app.multilingual and enabled more than one language, we can link two content items of different languages to be the translation of each other issuing a POST query to the @translations endpoint, including the id of the content to which it should be linked. The id of the content must be a full URL of the content object:

http

POST /plone/en/test-document/@translations HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json

{
    "id": "http://localhost:55001/plone/es/test-document"
}

curl

curl -i -X POST http://nohost/plone/en/test-document/@translations -H "Accept: application/json" -H "Content-Type: application/json" --data-raw '{"id": "http://localhost:55001/plone/es/test-document"}' --user admin:secret

httpie

echo '{
  "id": "http://localhost:55001/plone/es/test-document"
}' | http POST http://nohost/plone/en/test-document/@translations Accept:application/json Content-Type:application/json -a admin:secret

python-requests

requests.post('http://nohost/plone/en/test-document/@translations', headers={'Accept': 'application/json', 'Content-Type': 'application/json'}, json={'id': 'http://localhost:55001/plone/es/test-document'}, auth=('admin', 'secret'))

Note

id is a required field, and needs to point to existing content on the site.

The API will return a 201 Created response, if the linking was successful:

HTTP/1.1 201 Created
Content-Type: application/json
Location: http://localhost:55001/plone/en/test-document

{}

We can also use the object's path to link the translation instead of the full URL:

http

POST /plone/en/test-document/@translations HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json

{
    "id": "/es/test-document"
}

curl

curl -i -X POST http://nohost/plone/en/test-document/@translations -H "Accept: application/json" -H "Content-Type: application/json" --data-raw '{"id": "/es/test-document"}' --user admin:secret

httpie

echo '{
  "id": "/es/test-document"
}' | http POST http://nohost/plone/en/test-document/@translations Accept:application/json Content-Type:application/json -a admin:secret

python-requests

requests.post('http://nohost/plone/en/test-document/@translations', headers={'Accept': 'application/json', 'Content-Type': 'application/json'}, json={'id': '/es/test-document'}, auth=('admin', 'secret'))
HTTP/1.1 201 Created
Content-Type: application/json
Location: http://localhost:55001/plone/en/test-document

{}

We can also use the object's UID to link the translation:

http

POST /plone/en/test-document/@translations HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json

{
    "id": "SomeUUID000000000000000000000003"
}

curl

curl -i -X POST http://nohost/plone/en/test-document/@translations -H "Accept: application/json" -H "Content-Type: application/json" --data-raw '{"id": "SomeUUID000000000000000000000003"}' --user admin:secret

httpie

echo '{
  "id": "SomeUUID000000000000000000000003"
}' | http POST http://nohost/plone/en/test-document/@translations Accept:application/json Content-Type:application/json -a admin:secret

python-requests

requests.post('http://nohost/plone/en/test-document/@translations', headers={'Accept': 'application/json', 'Content-Type': 'application/json'}, json={'id': 'SomeUUID000000000000000000000003'}, auth=('admin', 'secret'))
HTTP/1.1 201 Created
Content-Type: application/json
Location: http://localhost:55001/plone/en/test-document

{}

After linking the contents, we can get the list of the translations of that content item by issuing a GET request on the @translations endpoint of that content item:

http

GET /plone/en/test-document/@translations HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i -X GET http://nohost/plone/en/test-document/@translations -H "Accept: application/json" --user admin:secret

httpie

http http://nohost/plone/en/test-document/@translations Accept:application/json -a admin:secret

python-requests

requests.get('http://nohost/plone/en/test-document/@translations', headers={'Accept': 'application/json'}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json

{
    "@id": "http://localhost:55001/plone/en/test-document/@translations",
    "items": [
        {
            "@id": "http://localhost:55001/plone/es/test-document",
            "language": "es"
        }
    ],
    "root": {
        "de": "http://localhost:55001/plone/de",
        "en": "http://localhost:55001/plone/en",
        "es": "http://localhost:55001/plone/es",
        "fr": "http://localhost:55001/plone/fr"
    }
}

To unlink the content, issue a DELETE request on the @translations endpoint of the content item, and provide the language code you want to unlink:

http

DELETE /plone/en/test-document/@translations HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json

{
    "language": "es"
}

curl

curl -i -X DELETE http://nohost/plone/en/test-document/@translations -H "Accept: application/json" -H "Content-Type: application/json" --data-raw '{"language": "es"}' --user admin:secret

httpie

echo '{
  "language": "es"
}' | http DELETE http://nohost/plone/en/test-document/@translations Accept:application/json Content-Type:application/json -a admin:secret

python-requests

requests.delete('http://nohost/plone/en/test-document/@translations', headers={'Accept': 'application/json', 'Content-Type': 'application/json'}, json={'language': 'es'}, auth=('admin', 'secret'))

Note

language is a required field.

HTTP/1.1 204 No Content

Creating a translation from an existing content#

The POST content endpoint to a folder is also capable of linking this new content with an existing translation using two parameters: translationOf and language.

http

POST /plone/de HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json

{
    "@type": "Document",
    "id": "mydocument",
    "language": "de",
    "title": "My German Document",
    "translation_of": "SomeUUID000000000000000000000003"
}

curl

curl -i -X POST http://nohost/plone/de -H "Accept: application/json" -H "Content-Type: application/json" --data-raw '{"@type": "Document", "id": "mydocument", "language": "de", "title": "My German Document", "translation_of": "SomeUUID000000000000000000000003"}' --user admin:secret

httpie

echo '{
  "@type": "Document",
  "id": "mydocument",
  "language": "de",
  "title": "My German Document",
  "translation_of": "SomeUUID000000000000000000000003"
}' | http POST http://nohost/plone/de Accept:application/json Content-Type:application/json -a admin:secret

python-requests

requests.post('http://nohost/plone/de', headers={'Accept': 'application/json', 'Content-Type': 'application/json'}, json={'@type': 'Document', 'id': 'mydocument', 'language': 'de', 'title': 'My German Document', 'translation_of': 'SomeUUID000000000000000000000003'}, auth=('admin', 'secret'))
HTTP/1.1 201 Created
Content-Type: application/json
Location: http://localhost:55001/plone/de/mydocument

{
    "@components": {
        "actions": {
            "@id": "http://localhost:55001/plone/de/mydocument/@actions"
        },
        "aliases": {
            "@id": "http://localhost:55001/plone/de/mydocument/@aliases"
        },
        "breadcrumbs": {
            "@id": "http://localhost:55001/plone/de/mydocument/@breadcrumbs"
        },
        "contextnavigation": {
            "@id": "http://localhost:55001/plone/de/mydocument/@contextnavigation"
        },
        "navigation": {
            "@id": "http://localhost:55001/plone/de/mydocument/@navigation"
        },
        "navroot": {
            "@id": "http://localhost:55001/plone/de/mydocument/@navroot"
        },
        "translations": {
            "@id": "http://localhost:55001/plone/de/mydocument/@translations"
        },
        "types": {
            "@id": "http://localhost:55001/plone/de/mydocument/@types"
        },
        "workflow": {
            "@id": "http://localhost:55001/plone/de/mydocument/@workflow"
        }
    },
    "@id": "http://localhost:55001/plone/de/mydocument",
    "@type": "Document",
    "UID": "SomeUUID000000000000000000000005",
    "allow_discussion": false,
    "changeNote": "",
    "contributors": [],
    "created": "1995-07-31T13:45:00+00:00",
    "creators": [
        "admin"
    ],
    "description": "",
    "effective": null,
    "exclude_from_nav": false,
    "expires": null,
    "id": "mydocument",
    "is_folderish": false,
    "language": {
        "title": "Deutsch",
        "token": "de"
    },
    "layout": "document_view",
    "lock": {
        "locked": false,
        "stealable": true
    },
    "modified": "1995-07-31T17:30:00+00:00",
    "next_item": {},
    "parent": {
        "@id": "http://localhost:55001/plone/de",
        "@type": "LRF",
        "description": "",
        "review_state": "published",
        "title": "Deutsch",
        "type_title": "Basisordner einer Sprache"
    },
    "previous_item": {},
    "relatedItems": [],
    "review_state": "private",
    "rights": "",
    "subjects": [],
    "table_of_contents": null,
    "text": null,
    "title": "My German Document",
    "type_title": "Seite",
    "version": "current",
    "versioning_enabled": true,
    "working_copy": null,
    "working_copy_of": null
}

Get location in the tree for new translations#

When you create a translation in Plone, there are policies in place for finding a suitable placement for it. This endpoint returns the proper placement for the newly created translation:

http

GET /plone/es/test-document/@translation-locator?target_language=de HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i -X GET 'http://nohost/plone/es/test-document/@translation-locator?target_language=de' -H "Accept: application/json" --user admin:secret

httpie

http 'http://nohost/plone/es/test-document/@translation-locator?target_language=de' Accept:application/json -a admin:secret

python-requests

requests.get('http://nohost/plone/es/test-document/@translation-locator?target_language=de', headers={'Accept': 'application/json'}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json

{
    "@id": "http://localhost:55001/plone/de"
}

Expansion#

This service can be used with the Expansion mechanism which allows getting additional information about a content item in one query, avoiding additional requests.

Translation information can be provided by the API expansion for translatable content items. A content type is translatable if it has the plone.translatable behavior enabled. plone.app.multilingual enables this behavior for all content types at the time that it is installed. For other content types added later, it would be necessary to enable the behavior for them also.

If a simple GET request is done on the content item, a new entry will be shown on the @components entry, with the URL of the @translations endpoint:

http

GET /plone/en/test-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i -X GET http://nohost/plone/en/test-document -H "Accept: application/json" --user admin:secret

httpie

http http://nohost/plone/en/test-document Accept:application/json -a admin:secret

python-requests

requests.get('http://nohost/plone/en/test-document', headers={'Accept': 'application/json'}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json

{
    "@components": {
        "actions": {
            "@id": "http://localhost:55001/plone/en/test-document/@actions"
        },
        "aliases": {
            "@id": "http://localhost:55001/plone/en/test-document/@aliases"
        },
        "breadcrumbs": {
            "@id": "http://localhost:55001/plone/en/test-document/@breadcrumbs"
        },
        "contextnavigation": {
            "@id": "http://localhost:55001/plone/en/test-document/@contextnavigation"
        },
        "navigation": {
            "@id": "http://localhost:55001/plone/en/test-document/@navigation"
        },
        "navroot": {
            "@id": "http://localhost:55001/plone/en/test-document/@navroot"
        },
        "translations": {
            "@id": "http://localhost:55001/plone/en/test-document/@translations"
        },
        "types": {
            "@id": "http://localhost:55001/plone/en/test-document/@types"
        },
        "workflow": {
            "@id": "http://localhost:55001/plone/en/test-document/@workflow"
        }
    },
    "@id": "http://localhost:55001/plone/en/test-document",
    "@type": "Document",
    "UID": "SomeUUID000000000000000000000001",
    "allow_discussion": false,
    "changeNote": "",
    "contributors": [],
    "created": "1995-07-31T13:45:00+00:00",
    "creators": [
        "test_user_1_"
    ],
    "description": "",
    "effective": null,
    "exclude_from_nav": false,
    "expires": null,
    "id": "test-document",
    "is_folderish": false,
    "language": {
        "title": "English",
        "token": "en"
    },
    "layout": "document_view",
    "lock": {
        "locked": false,
        "stealable": true
    },
    "modified": "1995-07-31T17:30:00+00:00",
    "next_item": {},
    "parent": {
        "@id": "http://localhost:55001/plone/en",
        "@type": "LRF",
        "description": "",
        "review_state": "published",
        "title": "English",
        "type_title": "Language Root Folder"
    },
    "previous_item": {},
    "relatedItems": [],
    "review_state": "private",
    "rights": "",
    "subjects": [],
    "table_of_contents": null,
    "text": null,
    "title": "Test document",
    "type_title": "Page",
    "version": "current",
    "versioning_enabled": true,
    "working_copy": null,
    "working_copy_of": null
}

In order to expand and embed the translations component, use the GET parameter expand with the value translations.

http

GET /plone/en/test-document?expand=translations HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i -X GET 'http://nohost/plone/en/test-document?expand=translations' -H "Accept: application/json" --user admin:secret

httpie

http 'http://nohost/plone/en/test-document?expand=translations' Accept:application/json -a admin:secret

python-requests

requests.get('http://nohost/plone/en/test-document?expand=translations', headers={'Accept': 'application/json'}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json

{
    "@components": {
        "actions": {
            "@id": "http://localhost:55001/plone/en/test-document/@actions"
        },
        "aliases": {
            "@id": "http://localhost:55001/plone/en/test-document/@aliases"
        },
        "breadcrumbs": {
            "@id": "http://localhost:55001/plone/en/test-document/@breadcrumbs"
        },
        "contextnavigation": {
            "@id": "http://localhost:55001/plone/en/test-document/@contextnavigation"
        },
        "navigation": {
            "@id": "http://localhost:55001/plone/en/test-document/@navigation"
        },
        "navroot": {
            "@id": "http://localhost:55001/plone/en/test-document/@navroot"
        },
        "translations": {
            "@id": "http://localhost:55001/plone/en/test-document/@translations",
            "items": [
                {
                    "@id": "http://localhost:55001/plone/es/test-document",
                    "language": "es"
                }
            ],
            "root": {
                "de": "http://localhost:55001/plone/de",
                "en": "http://localhost:55001/plone/en",
                "es": "http://localhost:55001/plone/es",
                "fr": "http://localhost:55001/plone/fr"
            }
        },
        "types": {
            "@id": "http://localhost:55001/plone/en/test-document/@types"
        },
        "workflow": {
            "@id": "http://localhost:55001/plone/en/test-document/@workflow"
        }
    },
    "@id": "http://localhost:55001/plone/en/test-document",
    "@type": "Document",
    "UID": "SomeUUID000000000000000000000001",
    "allow_discussion": false,
    "changeNote": "",
    "contributors": [],
    "created": "1995-07-31T13:45:00+00:00",
    "creators": [
        "test_user_1_"
    ],
    "description": "",
    "effective": null,
    "exclude_from_nav": false,
    "expires": null,
    "id": "test-document",
    "is_folderish": false,
    "language": {
        "title": "English",
        "token": "en"
    },
    "layout": "document_view",
    "lock": {
        "locked": false,
        "stealable": true
    },
    "modified": "1995-07-31T17:30:00+00:00",
    "next_item": {},
    "parent": {
        "@id": "http://localhost:55001/plone/en",
        "@type": "LRF",
        "description": "",
        "review_state": "published",
        "title": "English",
        "type_title": "Language Root Folder"
    },
    "previous_item": {},
    "relatedItems": [],
    "review_state": "private",
    "rights": "",
    "subjects": [],
    "table_of_contents": null,
    "text": null,
    "title": "Test document",
    "type_title": "Page",
    "version": "current",
    "versioning_enabled": true,
    "working_copy": null,
    "working_copy_of": null
}