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:
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 -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
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
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:
POST /plone/en/test-document/@translations HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"id": "/es/test-document"
}
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
echo '{
"id": "/es/test-document"
}' | http POST http://nohost/plone/en/test-document/@translations Accept:application/json Content-Type:application/json -a admin:secret
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:
POST /plone/en/test-document/@translations HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"id": "SomeUUID000000000000000000000003"
}
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
echo '{
"id": "SomeUUID000000000000000000000003"
}' | http POST http://nohost/plone/en/test-document/@translations Accept:application/json Content-Type:application/json -a admin:secret
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:
GET /plone/en/test-document/@translations HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl -i -X GET http://nohost/plone/en/test-document/@translations -H "Accept: application/json" --user admin:secret
http http://nohost/plone/en/test-document/@translations Accept:application/json -a admin:secret
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:
DELETE /plone/en/test-document/@translations HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"language": "es"
}
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
echo '{
"language": "es"
}' | http DELETE http://nohost/plone/en/test-document/@translations Accept:application/json Content-Type:application/json -a admin:secret
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
.
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 -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
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
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:
GET /plone/es/test-document/@translation-locator?target_language=de HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl -i -X GET 'http://nohost/plone/es/test-document/@translation-locator?target_language=de' -H "Accept: application/json" --user admin:secret
http 'http://nohost/plone/es/test-document/@translation-locator?target_language=de' Accept:application/json -a admin:secret
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:
GET /plone/en/test-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl -i -X GET http://nohost/plone/en/test-document -H "Accept: application/json" --user admin:secret
http http://nohost/plone/en/test-document Accept:application/json -a admin:secret
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
.
GET /plone/en/test-document?expand=translations HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl -i -X GET 'http://nohost/plone/en/test-document?expand=translations' -H "Accept: application/json" --user admin:secret
http 'http://nohost/plone/en/test-document?expand=translations' Accept:application/json -a admin:secret
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
}