i18n: internationalization of screen messages

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. 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 do the work of deciding in which language 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 display texts in the language of the content object. There is no need to ask anything of 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 for the language in which you want to have the messages. Otherwise you will get the messages on the default language configured in Plone.

To achieve that, the REST API requires the use of the Accept-Language HTTP header being passed with the required language code.

You will also need to configure Plone to use 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 the option Use browser language request negotiation.

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

http

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

curl

curl -i -X GET 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,
        "id": "File",
        "immediately_addable": true,
        "title": "Archivo"
    },
    {
        "@id": "http://localhost:55001/plone/@types/Folder",
        "addable": true,
        "id": "Folder",
        "immediately_addable": true,
        "title": "Carpeta"
    },
    {
        "@id": "http://localhost:55001/plone/@types/Collection",
        "addable": true,
        "id": "Collection",
        "immediately_addable": true,
        "title": "Colecci\u00f3n"
    },
    {
        "@id": "http://localhost:55001/plone/@types/DXTestDocument",
        "addable": true,
        "id": "DXTestDocument",
        "immediately_addable": true,
        "title": "DX Test Document"
    },
    {
        "@id": "http://localhost:55001/plone/@types/Link",
        "addable": true,
        "id": "Link",
        "immediately_addable": true,
        "title": "Enlace"
    },
    {
        "@id": "http://localhost:55001/plone/@types/Event",
        "addable": true,
        "id": "Event",
        "immediately_addable": true,
        "title": "Evento"
    },
    {
        "@id": "http://localhost:55001/plone/@types/Image",
        "addable": true,
        "id": "Image",
        "immediately_addable": true,
        "title": "Imagen"
    },
    {
        "@id": "http://localhost:55001/plone/@types/News Item",
        "addable": true,
        "id": "News Item",
        "immediately_addable": true,
        "title": "Noticia"
    },
    {
        "@id": "http://localhost:55001/plone/@types/Document",
        "addable": true,
        "id": "Document",
        "immediately_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 -X GET 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": [
        {
            "behavior": "plone",
            "fields": [
                "title",
                "description"
            ],
            "id": "default",
            "title": "Por defecto"
        },
        {
            "behavior": "plone",
            "description": "",
            "fields": [
                "subjects",
                "language",
                "relatedItems"
            ],
            "id": "categorization",
            "title": "Categorizaci\u00f3n"
        },
        {
            "behavior": "plone",
            "description": "",
            "fields": [
                "effective",
                "expires"
            ],
            "id": "dates",
            "title": "Fechas"
        },
        {
            "behavior": "plone",
            "description": "",
            "fields": [
                "creators",
                "contributors",
                "rights"
            ],
            "id": "ownership",
            "title": "Propiedad"
        },
        {
            "behavior": "plone",
            "description": "",
            "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": {
            "behavior": "plone.allowdiscussion",
            "choices": [
                [
                    "True",
                    "S\u00ed"
                ],
                [
                    "False",
                    "No"
                ]
            ],
            "description": "Permitir comentarios para este tipo de contenido",
            "enum": [
                "True",
                "False"
            ],
            "enumNames": [
                "S\u00ed",
                "No"
            ],
            "factory": "Choice",
            "title": "Permitir comentarios",
            "type": "string",
            "vocabulary": {
                "@id": "http://localhost:55001/plone/@sources/allow_discussion"
            }
        },
        "contributors": {
            "additionalItems": true,
            "behavior": "plone.dublincore",
            "description": "Los nombres de las personas que han contribuido a este elemento. Cada colaborador deber\u00eda estar en una l\u00ednea independiente.",
            "factory": "Tuple",
            "items": {
                "description": "",
                "factory": "Text line (String)",
                "title": "",
                "type": "string"
            },
            "title": "Colaboradores",
            "type": "array",
            "uniqueItems": true,
            "widgetOptions": {
                "vocabulary": {
                    "@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Users"
                }
            }
        },
        "creators": {
            "additionalItems": true,
            "behavior": "plone.dublincore",
            "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.",
            "factory": "Tuple",
            "items": {
                "description": "",
                "factory": "Text line (String)",
                "title": "",
                "type": "string"
            },
            "title": "Creadores",
            "type": "array",
            "uniqueItems": true,
            "widgetOptions": {
                "vocabulary": {
                    "@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Users"
                }
            }
        },
        "description": {
            "behavior": "plone.dublincore",
            "description": "Usado en listados de elementos y resultados de b\u00fasquedas.",
            "factory": "Text",
            "title": "Descripci\u00f3n",
            "type": "string",
            "widget": "textarea"
        },
        "effective": {
            "behavior": "plone.dublincore",
            "description": "Si la fecha es del futuro, el contenido no se mostrar\u00e1 en los listados y las b\u00fasquedas hasta esa fecha.",
            "factory": "Date/Time",
            "title": "Fecha de Publicaci\u00f3n",
            "type": "string",
            "widget": "datetime"
        },
        "exclude_from_nav": {
            "behavior": "plone.excludefromnavigation",
            "default": false,
            "description": "Si est\u00e1 marcado, este elemento no aparecer\u00e1 en el \u00e1rbol de navegaci\u00f3n",
            "factory": "Yes/No",
            "title": "Excluir de la navegaci\u00f3n",
            "type": "boolean"
        },
        "expires": {
            "behavior": "plone.dublincore",
            "description": "Cuando esta fecha llegue, el contenido no se mostrar\u00e1 en los listados y b\u00fasquedas.",
            "factory": "Date/Time",
            "title": "Fecha de Terminaci\u00f3n",
            "type": "string",
            "widget": "datetime"
        },
        "id": {
            "behavior": "plone.shortname",
            "description": "Este nombre se mostrar\u00e1 en la URL.",
            "factory": "Text line (String)",
            "title": "Nombre corto",
            "type": "string"
        },
        "language": {
            "behavior": "plone.dublincore",
            "default": "en",
            "description": "",
            "factory": "Choice",
            "title": "Idioma",
            "type": "string",
            "vocabulary": {
                "@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.SupportedContentLanguages"
            }
        },
        "nextPreviousEnabled": {
            "behavior": "plone.nextprevioustoggle",
            "default": false,
            "description": "Esto habilita el widget siguiente/pr\u00f3ximo en los elementos contenidos en esta carpeta.",
            "factory": "Yes/No",
            "title": "Habilitar la navegaci\u00f3n siguiente/anterior",
            "type": "boolean"
        },
        "relatedItems": {
            "additionalItems": true,
            "behavior": "plone.relateditems",
            "default": [],
            "description": "",
            "factory": "Relation List",
            "items": {
                "description": "",
                "factory": "Relation Choice",
                "title": "Related",
                "type": "string",
                "vocabulary": {
                    "@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Catalog"
                }
            },
            "title": "Elementos relacionados",
            "type": "array",
            "uniqueItems": true,
            "widgetOptions": {
                "pattern_options": {
                    "recentlyUsed": true
                },
                "vocabulary": {
                    "@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Catalog"
                }
            }
        },
        "rights": {
            "behavior": "plone.dublincore",
            "description": "Declaraci\u00f3n de copyright o informaci\u00f3n de otros derechos sobre este elemento.",
            "factory": "Text",
            "title": "Derechos de Autor",
            "type": "string",
            "widget": "textarea"
        },
        "subjects": {
            "additionalItems": true,
            "behavior": "plone.dublincore",
            "description": "Las etiquetas suelen utilizarse para la organizaci\u00f3n a medida del contenido.",
            "factory": "Tuple",
            "items": {
                "description": "",
                "factory": "Text line (String)",
                "title": "",
                "type": "string"
            },
            "title": "Etiquetas",
            "type": "array",
            "uniqueItems": true,
            "widgetOptions": {
                "vocabulary": {
                    "@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Keywords"
                }
            }
        },
        "title": {
            "behavior": "plone.dublincore",
            "description": "",
            "factory": "Text line (String)",
            "title": "T\u00edtulo",
            "type": "string"
        }
    },
    "required": [
        "title"
    ],
    "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 -X GET 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": "1995-07-31T17:30:00+00:00",
            "title": "Privado"
        }
    ],
    "state": {
        "id": "private",
        "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 -X GET 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

[
    {
        "@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": "Versi\u00f3n inicial",
        "may_revert": true,
        "time": "1995-07-31T17:30:00+00:00",
        "transition_title": "Editado",
        "type": "versioning",
        "version": 0
    },
    {
        "action": "Crear",
        "actor": {
            "@id": "http://localhost:55001/plone/@users/test_user_1_",
            "fullname": "test_user_1_",
            "id": "test_user_1_",
            "username": null
        },
        "comments": "",
        "review_state": "private",
        "state_title": "Privado",
        "time": "1995-07-31T18:30:00+00:00",
        "transition_title": "Crear",
        "type": "workflow"
    }
]

The same happens in the @addons endpoint. All addon names and descriptions will be shown translated:

http

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

curl

curl -i -X GET http://nohost/plone/@addons -H "Accept: application/json" -H "Accept-Language: es" --user admin:secret

httpie

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

python-requests

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

…and the response:

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

{
    "items": [
        {
            "@id": "http://localhost:55001/plone/@addons/Products.CMFPlacefulWorkflow",
            "description": "Agrega a Plone la capacidad de cambiar las cadenas de flujo de trabajo para los tipos en cada objeto. Tiene dependencias de los tipos b\u00e1sicos de Plone.",
            "id": "Products.CMFPlacefulWorkflow",
            "install_profile_id": "Products.CMFPlacefulWorkflow:CMFPlacefulWorkflow",
            "is_installed": false,
            "profile_type": "default",
            "title": "Soporte de Pol\u00edtica de Flujo de trabajo (CMFPlacefulWorkflow)",
            "uninstall_profile_id": "Products.CMFPlacefulWorkflow:uninstall",
            "upgrade_info": {},
            "version": "1.2.3"
        },
        {
            "@id": "http://localhost:55001/plone/@addons/collective.MockMailHost",
            "description": "Installs the collective.MockMailHost package",
            "id": "collective.MockMailHost",
            "install_profile_id": "collective.MockMailHost:default",
            "is_installed": true,
            "profile_type": "default",
            "title": "collective.MockMailHost",
            "uninstall_profile_id": "",
            "upgrade_info": {
                "available": false,
                "hasProfile": true,
                "installedVersion": "3.0.0",
                "newVersion": "1.2.3",
                "required": true
            },
            "version": "1.2.3"
        },
        {
            "@id": "http://localhost:55001/plone/@addons/plone.app.iterate",
            "description": "A\u00f1ade soporte de copia de trabajo (modificaciones in situ) a Plone.",
            "id": "plone.app.iterate",
            "install_profile_id": "plone.app.iterate:default",
            "is_installed": false,
            "profile_type": "default",
            "title": "Soporte de Copia de Trabajo (Repetir)",
            "uninstall_profile_id": "plone.app.iterate:uninstall",
            "upgrade_info": {},
            "version": "1.2.3"
        },
        {
            "@id": "http://localhost:55001/plone/@addons/plone.app.multilingual",
            "description": "Instalar para activar el soporte de contenido multiling\u00fce con plone.app.multilingual",
            "id": "plone.app.multilingual",
            "install_profile_id": "plone.app.multilingual:default",
            "is_installed": false,
            "profile_type": "default",
            "title": "Soporte Multiidioma",
            "uninstall_profile_id": "plone.app.multilingual:uninstall",
            "upgrade_info": {},
            "version": "1.2.3"
        },
        {
            "@id": "http://localhost:55001/plone/@addons/plone.restapi",
            "description": "API hipermedia RESTful para Plone.",
            "id": "plone.restapi",
            "install_profile_id": "plone.restapi:default",
            "is_installed": true,
            "profile_type": "default",
            "title": "plone.restapi",
            "uninstall_profile_id": "plone.restapi:uninstall",
            "upgrade_info": {
                "available": false,
                "hasProfile": true,
                "installedVersion": "0007",
                "newVersion": "0007",
                "required": false
            },
            "version": "1.2.3"
        },
        {
            "@id": "http://localhost:55001/plone/@addons/plone.session",
            "description": "Soporte opcional de refresco de plone.session.",
            "id": "plone.session",
            "install_profile_id": "plone.session:default",
            "is_installed": false,
            "profile_type": "default",
            "title": "Soporte de actualizaci\u00f3n de sesi\u00f3n",
            "uninstall_profile_id": "plone.session:uninstall",
            "upgrade_info": {},
            "version": "1.2.3"
        }
    ]
}