How does translation scope work in Magento 2?
Does this mean there is no such thing as a module scope anymore?
Yes
If so, what happens if two different modules define different translations for one string?
Magento 2 load translation from all modules and then load from current (based on current controller). In most cases this resolve problem.
I'm thinking of ambiguous words like "state" that will be translated different depending on the context. How does Magento 2 deal with these?
Looks like is no big issue there. In any case Magento can add inline context like State (Address)
, State (order)
Here's what I've found, not sure if it will answer all of your questions though.
So the translation is something that's happening in the \Magento\Framework\Phrase
class (an object of this class is being returned in the __
function).
If you check \Magento\Framework\Phrase\README.md
, you can get interesting information regarding this class:
Class
\Magento\Framework\Phrase
calls renderer to make the translation of the text.Phrase
providesRendererInterface
and a few renderers to support different kinds of needs of translation of the text. Here are list of renderers in this library:
- Placeholder render - it replaces placeholders with parameters for substitution. It is the default render if none is set for the Phrase.
- Translate render - it is a base renderer that implements text translations.
- Inline render - it adds inline translate part to text translation and returns the strings by a template.
- Composite render - it can have several renderers, calls each renderer for processing the text. Array of renderer class names pass into composite render constructor as a parameter.
So the translation is initiated via the _initTranslate
method of \Magento\Framework\App\Area.php
, here a renderer is set on the Phrase
class:
protected function _initTranslate()
{
$this->_translator->loadData(null, false);
\Magento\Framework\Phrase::setRenderer(
$this->_objectManager->get('Magento\Framework\Phrase\RendererInterface')
);
return $this;
}
Looking at \Magento\Translation\etc\di.xml
we can see the following preference is set:
<preference for="Magento\Framework\Phrase\RendererInterface" type="Magento\Framework\Phrase\Renderer\Composite" />
And later in the same file we find the declaration:
<type name="Magento\Framework\Phrase\Renderer\Composite">
<arguments>
<argument name="renderers" xsi:type="array">
<item name="translation" xsi:type="object">Magento\Framework\Phrase\Renderer\Translate</item>
<item name="placeholder" xsi:type="object">Magento\Framework\Phrase\Renderer\Placeholder</item>
<item name="inline" xsi:type="object">Magento\Framework\Phrase\Renderer\Inline</item>
</argument>
</arguments>
</type>
So looking at the render
function of this Composite
class we can see that it renders the source using each argument items render
function:
public function render(array $source, array $arguments = [])
{
$result = $source;
foreach ($this->_renderers as $render) {
$result[] = $render->render($result, $arguments);
}
return end($result);
}
When we check \Magento\Framework\Phrase\Renderer\Translate.php
(the first argument of the Composite
class) we can see that the render
function uses the TranslateInterface
class (corresponds to $this->_translator
in the code below) to get the corresponding translation:
public function render(array $source, array $arguments)
{
$text = end($source);
try {
$data = $this->translator->getData();
} catch (\Exception $e) {
$this->logger->critical($e->getMessage());
throw $e;
}
return array_key_exists($text, $data) ? $data[$text] : $text;
}
Remember the _initTranslate
method mentionned above ? Right before setting the \Magento\Framework\Phrase
renderer we have the following code:
$this->_translator->loadData(null, false);
Where $this->_translator
is a \Magento\Framework\TranslateInterface
Under app/etc/di.xml
we can find the following preference:
<preference for="Magento\Framework\TranslateInterface" type="Magento\Framework\Translate" />
Finally, we can find how each module translation is loaded in the loadData
method of \Magento\Framework\Translate
class:
public function loadData($area = null, $forceReload = false)
{
$this->setConfig(
['area' => isset($area) ? $area : $this->_appState->getAreaCode()]
);
if (!$forceReload) {
$this->_data = $this->_loadCache();
if ($this->_data !== false) {
return $this;
}
}
$this->_data = [];
$this->_loadModuleTranslation();
$this->_loadThemeTranslation();
$this->_loadPackTranslation();
$this->_loadDbTranslation();
$this->_saveCache();
return $this;
}
Then in _loadModuleTranslation
we get the current module and get the corresponding translations:
protected function _loadModuleTranslation()
{
$currentModule = $this->getControllerModuleName();
$allModulesExceptCurrent = array_diff($this->_moduleList->getNames(), [$currentModule]);
$this->loadModuleTranslationByModulesList($allModulesExceptCurrent);
$this->loadModuleTranslationByModulesList([$currentModule]);
return $this;
}
There's probably more to say about it but I reckon that summarizes (even if my post is super long) the translation process in Magento 2.