Drupal 8 multilingual tidbits 19: content translation development

Gábor Hojtsy 3 min read

Up to date as of March 14th, 2017.

Now that we covered how content translation workflow works in Drupal 8, its time to look a bit at the API side. In Drupal 7 this meant dealing with scary seemingly infinitely nested arrays with language codes, field names, deltas, etc. Drupal 8 makes this a whole lot simpler.

Translation basics on an entity

Drupal 8's entity API handles entities as full fledged objects. Translations of entities may be requested from this object and returned as a reusable entity object as well, which can be treated the same way as the original entity that we loaded. Here are some quick examples:

<?php
use Drupal\node\Entity\Node;

// Load node 4. In terms of language, this will get us an entity
// in the original submission language.
$node = Node::load(4);

// Get a list of all translations and collect titles.
$titles = [];
$languages = $node->getTranslationLanguages();
foreach ($languages as $langcode => $language) {
  // The object returned by getTranslation() behaves the same way as $node.
  $translation = $node->getTranslation($langcode);
  $titles[$langcode] = $translation->title;
}

// If the node has no Hungarian translation, add one.
if (!$node->hasTranslation('hu')) {
  $translation = $node->addTranslation('hu', array('title' => 'Hungarian title'));
  $translation->save();
}

// If the node has a Spanish translation, update the title. In case of a missing
// Spanish translation this will throw an InvalidArgumentException.
$node->getTranslation('es')->setTitle('Spanish title')->save();

// Remove the Hungarian translation.
$node->removeTranslation('hu');
$node->save();
?>

Loading the right translation

Drupal 8 also comes with several supporting systems for dealing with content language. For example, a content language is selected for the page to be used for translated content display. You can retrieve that language from the language manager and use to deal with the right translation.

<?php
use Drupal\Core\Language\LanguageInterface;
use Drupal\taxonomy\Entity\Term;

$term = Term::load(23);
$langcode = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT->getId());
$translation = $term->getTranslation($langcode);
// Now deal with the right language from here on.
?>

A possible problem with that is getTranslation() will return an exception if the translation requested does not exist. If you want to be sure to load only from existing translations and apply a fallback mechanism for loading suitable translations when something is not available, you need entity language negotiation. That is provided by the entity repository and helps you figure out which translation to load.

<?php
use Drupal\user\Entity\User;

$account = User::load(15);
$langcode = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
// Get the user profile with language fallback, if it did not have a translation for $langcode.
$translation = \Drupal::service('entity.repository')->getTranslationFromContext($account, $langcode);
?>

The entity repository uses the language manager and possibly contributed modules to figure out which language to load for the entity. Ideally $langcode would be that language, but if that is not available, modules get a chance to provide fallback schemes for which other language may be best suitable. See LanguageManager::getFallbackCandidates() as well as hook_language_fallback_candidates_alter() and hook_language_fallback_candidates_OPERATION_alter() for more information.

That's it for a short summary of how the much improved content translation API works in Drupal 8. There is of course a lot more to this topic from creating translatable entity types, new field types to dealing with revisions and entity field queries. For some of those deeper details on how content translations are stored and queried, see Francesco Placella's (unfortunately already slightly outdated) articles in Drupal Watchdog titled Wait, $langcode? What the Heck? and Entity Storage, the Drupal 8 Way.

Next up, I plan to look at complex core use cases where interface, content and configuration translation meet. If you know how the pieces map together, you can really translate practically anything!

Issues to work on

None at the moment.

Comments 20

David Park (not verified) Mon, 11/30/2015 - 23:42 Permalink

You say that calling $langcode = \Drupal::languageManager()->getLanguage(LanguageInterface::TYPE_CONTENT); would give the language object of the current content language? How is that? In my site i'm calling that and getLanguage will only return a language object if the LanguageInterface::TYPE_CONTENT witch is string "language_content" from the list of all languages that this content is translated with, ie array('en', 'sv', 'und', 'zxx') for a site with Swedish and English. I don't se that the array in getLanguage, that $languages = $this->getLanguages(LanguageInterface::STATE_ALL); is returning will include a object with key "language_content". I guess I'm missing something here?

Thanks for this nice introduction, it really helped to get started quickly. I am working with the 8.1.x branch and noticed that $translation->title; becomes $translation->getTitle() (or $translation->get('title')->value;). Probably the same for $node->getTranslation('es')->title->value = 'Spanish title'; that should be replaced by $node->getTranslation('es')->setTitle('Spanish title');.

Thanks, updated. That also allowed us to inline the save() since setTitle() returns the object. Also I've been notified via https://www.drupal.org/node/2840299 that the invalid Spanish translation will not get created anymore.

<?php
// If the node has a Spanish translation, update the title. In case of a missing
// Spanish translation this will throw an InvalidArgumentException.
$node->getTranslation('es')->setTitle('Spanish title')->save();
?>
Nehal Rupani (not verified) Mon, 07/04/2016 - 14:31 Permalink

Hello Gábor,

Thanks for sharing multilingual functionality in detail. I am trying to import nodes automatically from excel file.

if ($node->hasTranslation($fields_values['langcode']) != 1) {
$translation = $node->addTranslation($fields_values['langcode']);

foreach($fields_values as $key => $value) {
$translation->set($key, $value);
}
$translation->save();
}
else {
$node->save();
}
With above code I was able to get node translated in to other language then english but What i found is when try to view those pages in admin/content area all my earlier path alias with english language are gone and it showing me nodes with other language which is not default language of my website. It should show English nodes by default. Do you think anything else is missing in above code.

What I can guess from admin screen is may be i need to add content_translation_source with is field in DB. how to do that while updating node.

Thanks for the really useful tips, as always!
I think the langcode stuff has updated since you wrote this, as instead of:

<?php $langcode = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT); ?>

I think it should be:

<?php $langcode = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); ?>

You've got the following code in your term snippet.

$langcode = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT);
$translation = $term->getTranslation($langcode);

I believe you should actually be using
$translation = $term->getTranslation($langcode->getId());

I think maybe you overlooked this because you did it differently above

$languages = $node->getTranslationLanguages();
foreach ($languages as $langcode => $language) {
  // The object returned by getTranslation() behaves the same way as $node.
  $translation = $node->getTranslation($langcode);
  $titles[$langcode] = $translation->title;
}
Dhara (not verified) Thu, 09/07/2017 - 18:28 Permalink

I want to update my title field
using this code : $node->getTranslation('es')->setTitle('Spanish title')->save();

It is not working, execution gets stopped. Please help

This is a great reference page -- thanks!

Is it possible to determine the current language of $node? Please consider the following:

$node = Node::load($nid);
foreach(['en','es'] as $lang) {
$node = $node->getTranslation($lang);
service_function($node);
}
function service_function($node) {
$lang = $node->?????; // determine the language of the node from the caller
}

The alternative is to pass the $lang of the $node as a parameter.

Thanks, Greg

a.genchev (not verified) Mon, 12/17/2018 - 11:34 Permalink

I mean in D8 (at least my version) the things are getting drawn not with PHP but with sth. called twig. Hense, the multilang problems are not limited to the PHP code but extend over this (newly?) invented language . If I need something drawn I need to fetch the lang code and translations in twig, not in php (unless I went headless mode). Do you have an idea how to do that in the right way ?