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:
<?phpuse 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.
<?phpuse 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.
<?phpuse 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.
Get content language
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?my mistake!
My mistake, sorry for wasting your time. It should be
\Drupal::languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)
, fixing.get / set title
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
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.
problem for adding translation with code
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.
looks fine
Adding node translations should not affect path aliases. Check out http://cgit.drupalcode.org/multilingual_demo/tree/multilingual_demo.ins… for an example of creating nodes and translations programatically.
You're not crazy!
This is a real problem, I filed an issue for it to explain what I'm seeing: https://www.drupal.org/node/2824669
getTranslation() creates new translation
A possible problem with that is getTranslation() as we have seen above will just create a new translation if there is none, so you may as well just get back the original language values without a different translation loaded.
I don't believe this is correct based on: https://www.drupal.org/node/2840299
Thanks, Greg
indeed, thanks for the correction
Updating the post. I think this used to be the original behavior but it got changed later.
$langcode correction
Thanks for the really useful tips, as always!
I think the langcode stuff has updated since you wrote this, as instead of:
I think it should be:
You are right, fixed
Fixed, thanks and sorry.
getCurrentLanguage() returns an object not an ID
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;
}
you are right, fixing
You are right, fixing this now.
Use $translation->getTitle(); to get title string
In the first example "... collect titles... $titles[$langcode] = $translation->title...", $translation->title is an object (Drupal\Core\Field\FieldItemList). If you want to get the actual title ('About us'), use $translation->getTitle();
Not working with update
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
what exactly is stopped?
What exactly is stopped?
Language of Node
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
language method
$node->language()
Nice article, but how to use this to draw the content ?
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 ?
twig should be concerned with markup
Twig was not (newly) invented in Drupal, we just adopted it. Twig should be receiving the content in the proper translation based on other environment factors (eg. URL, views filters, etc). You should not place logic about language loading in your twig templates.