Drupal 8 multilingual tidbits 10: context specific text translation APIs

Up to date as of October 16th, 2015.

In the previous tidbit, we looked at how Drupal 8 downloads software translations automatically, but what do software translations include? There are several new translation APIs for software translation in Drupal 8. Prior Drupal versions had the rule that you should use t() and format_plural() with little exceptions. Drupal 8 has much more complexity in this area, which may be a bliss or a curse depending on how you look at it. It definitely brings translation support to several areas that were not or poorly covered before. Buckle up, this 'tidbit' is more like a deep-dive into the woods of the API!

If you are not comfortable with code-talk, your key take-away should be that we now support a bigger part of the software to translate and in more flexible ways. We'll cover configuration translation from the user perspective for example in detail in later tidbits.

No more get_t(), st() and $t

In Drupal 7 and before, the t() translation function has only been usable once the database was set up. Therefore any code that was to run before the installation was done or in maintenance mode needed to use get_t() to get the right callback to use (either t() or st()) for the environment at hand. In Drupal 8, we handle this inside t(), so the proper implementation is injected as relevant for the environment Drupal runs in. In the installer it will work off of .po files read from disk, when a database is available and boostrapped, it will work off of that. Therefore in regular code, the rule that t() and format_plural() should be used is stronger than ever. See https://drupal.org/node/2021435 for the change notice.

The injectability of t() also provides a way to implement some long standing requests like translation lookup fallbacks between languages based on language inheritance using a to be written contributed module.

Most base classes provide a t() and formatPlural() method by implementing Drupal\Core\StringTranslation\StringTranslationTrait. When such a method is available it is preferred you use that to translate strings. That allows you to test the code on its own without global requirements of a translation service and you can inject a test version of the translation service in the test as needed.

Translation methods now return an object

While t() used to immediately translate the passed string, there are situations, when the translation system is not even initialized or you need the text translated later. In many cases, the string that was to be translated is never actually used. To cater for these cases, t() and other translation methods will not return a string anymore, they return a Drupal\Core\StringTranslation\TranslatableMarkup instance. Correspondingly formatPlural() returns a Drupal\Core\StringTranslation\PluralTranslatableMarkup instance. These can be and in fact are casted to strings later on when needed, which is when the translation happens.

The ! placeholder is removed, the : placeholder is added

In Drupal 7 and before, strings to be translated supported three placeholders: @, ! and %. Where @ escaped the replacement, % escaped and applied placeholder theming and ! passed through the value. The ! placeholder is removed in Drupal 8 due to security concerns. See https://www.drupal.org/node/2575819. For one of the prior use cases of passing through values, namely URLs, the : placeholder type was introduced, for example: More information is available in <a href=":help">our help section</a>.. See https://www.drupal.org/node/2571689 for details.

Context in Javascript Drupal.t() and Drupal.formatPlural()

t() lets you provide little bits of additional information to specify the context for shorter strings, so translators know how to translate ambiguous text. This feature is now extended to the Javascript counterparts Drupal.t() and Drupal.formatPlural(). You can retrieve the long month name translation of May (as opposed to the short version) with:

Drupal.t('May', {}, {context: "Long month name"});

See https://drupal.org/node/1323152 for the change notice.

Translation in Twig templates

Drupal 8 incorporates a new templating system for HTML output generation called Twig. Twig does not use the native PHP format that our good old PHPTemplate did, so it needs its own solutions for string translations.

Simple text translation is possible with the 't' (or 'trans') filter, while a whole {% trans %} block is supported (as defined by the Twig i18n extension) for more complex scenarios, such as strings with placeholders and singular/plural combinations.

These two do the same, they run the text through t():

{{ 'Hello Earth.'|trans }}
{{ 'Hello Earth.'|t }}

Singular/plural pairs are possible. This will use format_plural():

{% set count = 1 %}
{% trans %}
  Hello star.
{% plural count %}
  Hello {{ count }} stars.
{% endtrans %}

Values are escaped by default. The 'placeholder' filter can be used to form a placeholder. The default behavior is equivalent to @ in t(), while 'placeholder' matches %. It is not possible at this time to replicate the : placeholder type in Twig.

{% set string = '&"<>' %}
{% trans %}
  Escaped: {{ string }}
{% endtrans %}
{% trans %}
  Placeholder: {{ string|placeholder }}
{% endtrans %}

These will translate Escaped: @string and Placeholder: %string respectively. It is also possible to provide context strings for translatable text, see https://drupal.org/node/2049241.

See more information in Mark Carver's recent article at http://getlevelten.com/blog/mark-carver/drupal-8-twig-templates-and-tran... and the change notice at https://drupal.org/node/2047135

Translation in annotations

Classes with annotations serve to define entity types, fields, views plugins and so on. Annotations are not executable code as-is, so we need a specific syntax to mark strings to be translated. The @Translation() wrapper serves this purpose. It will run the text through t() when the annotation is evaluated. For example:

Drupal 7:

function file_entity_info() {
  return array(
'file' => array(
'label' => t('File'),
// ...

Drupal 8:

 * Defines the file entity class.
 * @ContentEntityType(
 *   id = "file",
 *   label = @Translation("File"),
 *   ....
class File {

More information about annotations at https://drupal.org/node/1882526

Info files became info.yml files

Project information files that used to use the .ini file format now use the YAML file format. Their designated components such as name and description are still taken from the file as usual, no need for any special markers. The new format is still very simple:

name: 'Good judgement'
type: module
description: 'Improves your site with bells and whistles'

hook_menu() becomes *.links.yml files

Drupal 7 hook_menu() was used to define paths and their callback functions as well as menu items, action links, tabs and contextual links. These are all separate things in Drupal 8 now. You define paths under routes. These may have titles (under the defaults._title key). Menu items, tabs, action links and contextual links also may have titles and in some cases descriptions. All of these are automatically collected from these files and there is no need for any markers.

See the upgrade tutorial for more information at https://www.drupal.org/node/2118147.

Translation for shipped configuration and configuration schemas

Default configuration is also shipped with Drupal core and Drupal 8 projects in the YAML file format. Most if not all default elements are shipped with this system, so your default menus, content types, fields, contact categories, vocabularies, views, and so on are included in the package this way.

Drupal 8 needed a way to extract translatable text out of these files, so a description format was added to describe the structure of these files: the configuration schema format. The schema has labels, structure and type information about the configuration data it defines. The labels become translatable pieces and part of the data becomes translatable based on the structure description and types applied. This is how Drupal can translate views, content types or vocabulary names and descriptions.

A simple example for default configuration is the default maintenance message in system.maintenance.yml:

message: '@site is currently under maintenance. We should be back shortly. Thank you for your patience.'
langcode: en

This is described by the configuration schema system as follows (also in the YAML format):

# Root of a configuration object.
  type: mapping
      type: string
      label: 'Language code'

  type: config_object
  label: 'Maintenance mode'
      type: text
      label: 'Message to display when in maintenance mode'

The schema describes system.maintenance.yml as a mapping of key-value pairs (based on the config_object mapping predefined for the top level of configuration files). The keys being: message and langcode (the later one defined in config_object). Each of the keys get their types and labels associated. The labels in the schema become translatable and the message defined as type 'text' also carries on translatability from its type definition, so the shipped default message is available for translation as well on your site.

There is a lot more information about this at https://drupal.org/node/1905070 and you'll read a whole lot more about the configuration system and translatability in further tidbits in my series.

Other smaller changes

There are some other smaller changes as well, for example the Symfony based new validation constraint handling provides some exeptions for the t() rule. See https://drupal.org/node/1903362 for that.

We are tracking all Drupal 8 translation compatibility issues at https://drupal.org/project/issues/search/potx?issue_tags=Drupal%208%20co...

Issues to work on

  1. PARTLY DONE: Localize.drupal.org does not yet fully support all the new APIs in Drupal 8, so the new version is not entirely translatable yet. See details at https://localize.drupal.org/node/5853 - the hardest one is parsing the shipped configuration files using the schema structure. However that needs to be solved sooner than later to let the community translate default configuration like views, menus, content types and so on shipped with Drupal.
  2. DONE! As said above the Twig syntax does not allow to pass context information (yet), so you cannot translate a month name as a 'Long month name' for example. https://drupal.org/node/2049241 is the place to discuss and solve that!
  3. DONE! Injected translations may be slightly different. See https://drupal.org/node/2018411 for ongoing discussion on how the injected translation API will look like (when you don't rely on the global t() but rather inject the translation service itself to an environment).
  4. Twig support for the : placeholder is yet missing. See https://www.drupal.org/node/2575275


tstoeckler's picture

Once again a great write-up! One little addition:

As is documented on the change notice for the Twig translation handler the Twig debug mode is useful for module or theme developers on the one hand, and for translators on the other hand, as it displays the resulting string for translation as an HTML comment.

Gábor Hojtsy's picture

Mark Carver posted a great article about the TWIG translatables which provides even more detail: http://getlevelten.com/blog/mark-carver/drupal-8-twig-templates-and-tran...

Mark_L6n's picture

Great articles! Related to:
The 'passthrough' filter can be used to skip escaping. The 'placeholder' filter can be used to form a placeholder. The default behavior is equivalent to @ in t(), while 'passthrough' matches ! and 'placeholder' matches %
Could you provide some information about placeholders @ ! % in the t() function for D8? Are they the same as D7/6?

Gábor Hojtsy's picture

They were at time of publication, but not anymore. The ! placeholder was removed (and the passthrough twig filter as well). The : placeholder was added but no twig support. See https://www.drupal.org/node/2575275 Updated the post. The @ and % placeholders work the same way as in D6/7.

Add new comment