i18n: internationalization of screen messages

Plone already provides user-interface translations using the plone.app.locales packages.

In plone.restapi we also use those translations where the end user needs to have those translated strings, this way the front-end work is easier, because you directly get from the server everything you need, instead of needing to query yet another endpoint to get the translations.

To do so, plone.restapi relies on Plone’s language-negotiation configuration and lets Plone to do the work of deciding the language in which the messages should be shown.

For the content of a multilingual site built using plone.app.multilingual this is an easy task: Plone is configured to show in the language of the content-object, so there is no need to ask anything to the REST API.

Nevertheless, when you want to query the Plone Site object of a multilingual site, or any other endpoint in a plain Plone site with multiple languages configured, you need to query the REST API which language do you want to have the messages on, otherwise you will get the messages on the default language configured in Plone.

To achieve that, the REST API requires to use the Accept-Language HTTP header passing as the value the code of the required language.

You will also need to configure Plone to use the browser request language negotiation. To do so, you need to go the Plone Control Panel, go to the Language Control Panel, open the Negotiation configuration tab and select “Use browser language request negotiation” option.

Using this option we can get the content-type titles translated:

http

GET /plone/@types HTTP/1.1
Accept: application/json
Accept-Language: es
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i http://nohost/plone/@types -H 'Accept: application/json' -H 'Accept-Language: es' --user admin:secret

httpie

http http://nohost/plone/@types Accept:application/json Accept-Language:es -a admin:secret

python-requests

requests.get('http://nohost/plone/@types', headers={
    'Accept': 'application/json',
    'Accept-Language': 'es',
}, auth=('admin', 'secret'))

And the response:

HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "@id": "http://localhost:55001/plone/@types/File", 
    "addable": true, 
    "title": "Archivo"
  }, 
  {
    "@id": "http://localhost:55001/plone/@types/Folder", 
    "addable": true, 
    "title": "Carpeta"
  }, 
  {
    "@id": "http://localhost:55001/plone/@types/Collection", 
    "addable": true, 
    "title": "Colecci\u00f3n"
  }, 
  {
    "@id": "http://localhost:55001/plone/@types/DXTestDocument", 
    "addable": true, 
    "title": "DX Test Document"
  }, 
  {
    "@id": "http://localhost:55001/plone/@types/Link", 
    "addable": true, 
    "title": "Enlace"
  }, 
  {
    "@id": "http://localhost:55001/plone/@types/Event", 
    "addable": true, 
    "title": "Evento"
  }, 
  {
    "@id": "http://localhost:55001/plone/@types/Image", 
    "addable": true, 
    "title": "Imagen"
  }, 
  {
    "@id": "http://localhost:55001/plone/@types/News Item", 
    "addable": true, 
    "title": "Noticia"
  }, 
  {
    "@id": "http://localhost:55001/plone/@types/Document", 
    "addable": true, 
    "title": "P\u00e1gina"
  }
]

All the field titles and descriptions, will also be translated. For instance for the Folder content type:

http

GET /plone/@types/Folder HTTP/1.1
Accept: application/json
Accept-Language: es
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i http://nohost/plone/@types/Folder -H 'Accept: application/json' -H 'Accept-Language: es' --user admin:secret

httpie

http http://nohost/plone/@types/Folder Accept:application/json Accept-Language:es -a admin:secret

python-requests

requests.get('http://nohost/plone/@types/Folder', headers={
    'Accept': 'application/json',
    'Accept-Language': 'es',
}, auth=('admin', 'secret'))

And the response:

HTTP/1.1 200 OK
Content-Type: application/json+schema

{
  "fieldsets": [
    {
      "fields": [
        "title", 
        "description"
      ], 
      "id": "default", 
      "title": "Default"
    }, 
    {
      "fields": [
        "subjects", 
        "language", 
        "relatedItems"
      ], 
      "id": "categorization", 
      "title": "Categorizaci\u00f3n"
    }, 
    {
      "fields": [
        "effective", 
        "expires"
      ], 
      "id": "dates", 
      "title": "Fechas"
    }, 
    {
      "fields": [
        "creators", 
        "contributors", 
        "rights"
      ], 
      "id": "ownership", 
      "title": "Propiedad"
    }, 
    {
      "fields": [
        "allow_discussion", 
        "exclude_from_nav", 
        "id", 
        "nextPreviousEnabled"
      ], 
      "id": "settings", 
      "title": "Configuraci\u00f3n"
    }
  ], 
  "layouts": [
    "album_view", 
    "event_listing", 
    "full_view", 
    "listing_view", 
    "summary_view", 
    "tabular_view"
  ], 
  "properties": {
    "allow_discussion": {
      "choices": [
        [
          "True", 
          "S\u00ed"
        ], 
        [
          "False", 
          "No"
        ]
      ], 
      "description": "Permitir comentarios para este tipo de contenido", 
      "enum": [
        "True", 
        "False"
      ], 
      "enumNames": [
        "S\u00ed", 
        "No"
      ], 
      "title": "Permitir comentarios", 
      "type": "string"
    }, 
    "contributors": {
      "additionalItems": true, 
      "description": "Los nombres de las personas que han contribuido a este elemento. Cada colaborador deber\u00eda estar en una l\u00ednea independiente.", 
      "items": {
        "description": "", 
        "title": "", 
        "type": "string"
      }, 
      "title": "Colaboradores", 
      "type": "array", 
      "uniqueItems": true, 
      "widgetOptions": {
        "vocabulary": {
          "@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Users"
        }
      }
    }, 
    "creators": {
      "additionalItems": true, 
      "description": "Personas responsables de la creaci\u00f3n del contenido de este elemento. Por favor, introduzca una lista de nombres de usuario, uno por l\u00ednea. El autor principal deber\u00eda ser el primero.", 
      "items": {
        "description": "", 
        "title": "", 
        "type": "string"
      }, 
      "title": "Creadores", 
      "type": "array", 
      "uniqueItems": true, 
      "widgetOptions": {
        "vocabulary": {
          "@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Users"
        }
      }
    }, 
    "description": {
      "description": "Usado en listados de elementos y resultados de b\u00fasquedas.", 
      "minLength": 0, 
      "title": "Descripci\u00f3n", 
      "type": "string", 
      "widget": "textarea"
    }, 
    "effective": {
      "description": "La fecha en la que el documento ser\u00e1 publicado. Si no selecciona ninguna fecha, el documento ser\u00e1 publicado inmediatamente.", 
      "title": "Fecha de Publicaci\u00f3n", 
      "type": "string", 
      "widget": "datetime"
    }, 
    "exclude_from_nav": {
      "default": false, 
      "description": "Si est\u00e1 marcado, este elemento no aparecer\u00e1 en el \u00e1rbol de navegaci\u00f3n", 
      "title": "Excluir de la navegaci\u00f3n", 
      "type": "boolean"
    }, 
    "expires": {
      "description": "La fecha en la que expira el documento. Esto har\u00e1 autom\u00e1ticamente el documento invisible a otros a una fecha dada. Si no elije ninguna fecha, nunca expirar\u00e1.", 
      "title": "Fecha de Terminaci\u00f3n", 
      "type": "string", 
      "widget": "datetime"
    }, 
    "id": {
      "description": "Este nombre se mostrar\u00e1 en la URL.", 
      "title": "Nombre corto", 
      "type": "string"
    }, 
    "language": {
      "default": "en", 
      "description": "", 
      "title": "Idioma", 
      "type": "string", 
      "vocabulary": {
        "@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.SupportedContentLanguages"
      }
    }, 
    "nextPreviousEnabled": {
      "default": false, 
      "description": "Esto habilita el widget siguiente/pr\u00f3ximo en los elementos contenidos en esta carpeta.", 
      "title": "Habilitar la navegaci\u00f3n siguiente/anterior", 
      "type": "boolean"
    }, 
    "relatedItems": {
      "additionalItems": true, 
      "default": [], 
      "description": "", 
      "items": {
        "description": "", 
        "title": "Related", 
        "type": "string", 
        "vocabulary": {
          "@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Catalog"
        }
      }, 
      "title": "Contenido relacionado", 
      "type": "array", 
      "uniqueItems": true, 
      "widgetOptions": {
        "pattern_options": {
          "recentlyUsed": true
        }, 
        "vocabulary": {
          "@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Catalog"
        }
      }
    }, 
    "rights": {
      "description": "Declaraci\u00f3n de copyright o informaci\u00f3n de otros derechos sobre este elemento.", 
      "minLength": 0, 
      "title": "Derechos de Autor", 
      "type": "string", 
      "widget": "textarea"
    }, 
    "subjects": {
      "additionalItems": true, 
      "description": "Las etiquetas suelen utilizarse para la organizaci\u00f3n a medida del contenido.", 
      "items": {
        "description": "", 
        "title": "", 
        "type": "string"
      }, 
      "title": "Etiquetas", 
      "type": "array", 
      "uniqueItems": true, 
      "widgetOptions": {
        "vocabulary": {
          "@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Keywords"
        }
      }
    }, 
    "title": {
      "description": "", 
      "title": "T\u00edtulo", 
      "type": "string"
    }
  }, 
  "required": [
    "title", 
    "nextPreviousEnabled"
  ], 
  "title": "Carpeta", 
  "type": "object"
}

In a given object, the workflow state and actions will be translated too:

http

GET /plone/front-page/@workflow HTTP/1.1
Accept: application/json
Accept-Language: es
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i http://nohost/plone/front-page/@workflow -H 'Accept: application/json' -H 'Accept-Language: es' --user admin:secret

httpie

http http://nohost/plone/front-page/@workflow Accept:application/json Accept-Language:es -a admin:secret

python-requests

requests.get('http://nohost/plone/front-page/@workflow', headers={
    'Accept': 'application/json',
    'Accept-Language': 'es',
}, auth=('admin', 'secret'))

And the response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "@id": "http://localhost:55001/plone/front-page/@workflow", 
  "history": [
    {
      "action": null, 
      "actor": "test_user_1_", 
      "comments": "", 
      "review_state": "private", 
      "time": "2016-10-21T19:00:00+00:00", 
      "title": "Privado"
    }
  ], 
  "transitions": [
    {
      "@id": "http://localhost:55001/plone/front-page/@workflow/publish", 
      "title": "Publicar"
    }, 
    {
      "@id": "http://localhost:55001/plone/front-page/@workflow/submit", 
      "title": "Enviar para publicaci\u00f3n"
    }
  ]
}

The same happens in the @history endpoint, all the relevant messages, will be shown translated:

http

GET /plone/front-page/@history HTTP/1.1
Accept: application/json
Accept-Language: es
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i http://nohost/plone/front-page/@history -H 'Accept: application/json' -H 'Accept-Language: es' --user admin:secret

httpie

http http://nohost/plone/front-page/@history Accept:application/json Accept-Language:es -a admin:secret

python-requests

requests.get('http://nohost/plone/front-page/@history', headers={
    'Accept': 'application/json',
    'Accept-Language': 'es',
}, auth=('admin', 'secret'))

And the response:

HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "action": "Crear", 
    "actor": {
      "@id": "http://localhost:55001/plone/@users/test_user_1_", 
      "fullname": "", 
      "id": "test_user_1_", 
      "username": "test-user"
    }, 
    "comments": "", 
    "review_state": "private", 
    "state_title": "Privado", 
    "time": "2016-10-21T19:00:00", 
    "transition_title": "Crear", 
    "type": "workflow"
  }, 
  {
    "@id": "http://localhost:55001/plone/front-page/@history/0", 
    "action": "Editado", 
    "actor": {
      "@id": "http://localhost:55001/plone/@users/test-user", 
      "fullname": "test-user", 
      "id": "test-user", 
      "username": null
    }, 
    "comments": null, 
    "may_revert": true, 
    "time": "2016-10-21T19:00:00", 
    "transition_title": "Editado", 
    "type": "versioning", 
    "version": 0
  }
]