Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Translation Mechanism in Magento 2.x Platform

Translation Mechanism in Magento 2.x Platform

This article discusses the translation mechanism in Magento 2.x, and contrasts it with that of Magento 1.x. The intended audience is Magento developers who would like to better learn the Magento architecture.

The Magento 2.x source code described here has been available since Magento 2.0.0.0-dev46 release and can be viewed on the GitHub at https://github.com/magento/magento2/commit/0849373d7636cfa17b70b8188bfc36d13ae749c1 or later commits.

Implementation Overview in Magento 1.x

Magento 1.x can present the user interface in different languages without modifying the actual application source code—it translates the system messages, error messages, and labels for display in the UI. Some messages may be displayed in logs for a system administrator or a developer—those don't need to be translated. By convention, in the source code, the labels and system messages for UI are expressed in English (en_US).

In order to replace these phrases with alternatives in different languages when the source code is interpreted, Magento has a layer of indirection. It allows for translation by providing dictionary files that contain phrases from en_US translated into a different language. The dictionary packages in other languages either ship with Magento code out of the box or are provided by the community.

The business user may need to customize the result through the administrative UI, known as the "Inline Translation Tool."

Triggering Translation in Source Code

In PHP-code, the intent of translating a phrase is indicated by using a __() method, which is available in interfaces of various helpers, blocks and controllers.

For example:

Mage::helper('catalog')->__('Attribute "%s" is required.', $code)

Or in PHTML templates:

$this->__('The product %s has been removed from comparison list.', $helper->escapeHtml($product->getName())

In XML files, the conventional "translate" and "module" attributes indicate that the contents of a node must be translated:

<?xml version="1.0"?>
<config>
    <tabs>
        <catalog translate="label" module="catalog">
            <label>Catalog</label>
            <sort_order>200</sort_order>
        </catalog>
    </tabs>
    <sections>
        <catalog translate="label" module="catalog">
            <class>separator-top</class>
            <label>Catalog</label>
...

The translate indicates a child node that contains the phrase, and "module" indicates context of a module. Context indicates a kind of namespace for this phrase.

CSV Dictionaries

The dictionaries are located in the source code in the files app/locale/<locale>/Namespace_Module.csv. The format is a CSV file with 2 columns, such as app/locale/en_US/Mage_Checkout.csv:

"Add to Cart","Add to Cart"
"Add to Compare","Add to Compare"
"Add to Wishlist","Add to Wishlist"
"Additional Product Info","Additional Product Info"
"Address","Address"
"Address %s of %s","Address %s of %s"

When translated to another language, the dictionary is going to be in a different directory and the second column will have the same phrases, but in a different language. In this case, German, as found in the file app/locale/de_DE/Mage_Checkout.csv:

"Add to Cart","Zum Warenkobrn hinzufügen"
"Add to Compare","Hinzufügen um zu vergleichen"
"Add to Wishlist","Zum Wunschzettel hinzufügen"
"Additional Product Info","Zusätzliche Angaben zum Produkt"
"Address","Adresse"
"Address %s of %s","Adresse %s von %s"

In order for system to recognize a dictionary, it is declared in the module configuration. For example, in app/code/core/Mage/Catalog/etc/config.xml:

<?xml version="1.0"?>
<config>
    <frontend>
        <translate>
            <modules>
                <Mage_Catalog>
                    <files>
                        <default>Mage_Catalog.csv</default>
                    </files>
                </Mage_Catalog>
            </modules>
        </translate>

A similar CSV dictionary can be found in a theme by a conventional name that doesn't need to be declared in config.xml, such as app/design/frontend/default/modern/locale/en_US/translate.csv. This dictionary would match phrases found in theme .phtml or .xml files.

Database Dictionaries

Translation dictionaries are also stored in the database in the core_translate table:

key_idstringstore_idtranslatelocalecrc_string
1Mage_Core::Enter search keyword1Введите поисковый запросru_RU-1839809583

The semantically significant fields are:

  • string—the original phrase in default language (en_US). Also it contains optional context of a module (Mage_Core:: in the example)
  • translate—the translation phrase
  • locale—language identifier

Context of Translating a Phrase

In order to translate a phrase, a certain context is used that consists of different variables:

  • Application context: current locale, application area, theme, store view
  • Invocation context (arguments of the __()): module context, the string literal

Based on the context, Magento system first attempts to load it from the cache. If it is not there, the dictionary is built by combining the following:

  • Merge CSV dictionaries of all active modules
  • Merge CSV dictionaries of the active theme
  • Load dictionary from DB for current store view

Finally, the phrase will be replaced if a match is found in the dictionary. Otherwise, the original value will be rendered.

Inline Translation Feature

The "Inline Translation" tool allows inline editing of phrases that are passed through the translation mechanism. The business user edits the phrases and stores them in database dictionaries.

Out of the box, this tool is the only mechanism to edit database dictionaries.

The only relation of this mechanism to translations is that it is coupled to invocation of __() calls: the logic of "inline translation" is incorporated into the __() method, because it happens to provide a layer of indirection.

Revisiting the Approach in Magento 2.x

In Magento 2.x, the translation mechanism is revisited considerably.

The primary goal regarding changing the translation mechanism was to reduce global code coupling and dependencies. The module's context was deemed detrimental for the developer (and translator) and not that valuable for the user—so context of modules for translating phrases was abolished for simplicity.

The secondary goal was separation of concerns in the implementation. Analysis showed the following different concerns:

  • translation—when a phrase is intended to be translated to a different language without changing meaning. For example: Add to Shopping Cart - Zum Warenkobrn hinzufügen
  • customization—when meaning of a phrase is intended to be changed without changing language. For example: Add to Shopping Cart - Add to Shopping Bag
  • placeholder substitution—when a phrase literal contains placeholders, replace them with variable values which come from context. For example: Product '%s' has been added to shopping cart. - Product 'Multimeter-2000' has been added to shopping cart

Other possible improvements were considered:

  • Shrink and simplify dictionaries by removing area scope. For example, a "My Account" phrase is both valid for the customer account area in the storefront or for the admin account form in the admin UI, so viewing it in the context of area (or module) doesn't seem necessary.
  • "Convention over configuration" for declaring dictionaries would allow reduce configuration footprint and keep the dictionaries.
  • Simplifying format of placeholders in phrases and emphasizing their order of appearance. For example, a translation from English to French may require different order of emerging phrase elements:
sprintf("This is a %1$s %2$s\n", adj, noun); // ex: adj="nice" noun = "car"
VS
sprintf("C'est une %2$s %1$s \n", adj, noun); // ex: adj="verte" noun = "voiture"

The sophisticated formatting options of sprintf() were not necessary for placeholders.

The Phrase Class

When a developer deals with a phrase in source code, he or she decides whether to display it to the user. So there must be a distinction made in source code.

The solution for that was to create an abstraction—the \Magento\Phrase class. This class implements pattern "Replace Data Value with Object"—basically replacing the string literal with an instance of this class, which has __toString() method.

The layer of indirection that existed earlier in Magento 1.x is now encapsulated in Magento 2.x in the Phrase class.

In order to distinguish such a phrase for the UI, the developer would have to create an instance of such class:

$message = new \Magento\Phrase('Value for "%1" is invalid: %2', array($code, $error));
$session->addError($message);

To simplify usage and reduce the impact on the existing code base, a global function __() was created with a polymorphous interface. The purpose of this function is to serve as a shortcut to create a Phrase object:

__('Value for "%1" is invalid: %2', $code, $error);

Note the difference in format of Magento 2.x placeholders (%1, $2) vs. Magento 1.x placeholders (%s).

Rendering of Phrases

But how to make the Phrase render properly when __toString() is triggered? Where is this logic supposed to be? How a layer of indirection could be organized (and encapsulated)?

Hard-coding all this inside the Phrase class without separating concerns was considered not as an option. Logic must be delegated to other classes. With that decided, the ways Phrase can delegate this logic would be:

  • Inject all necessary dependencies to Phrase in constructor
  • Inject them through an API (dependencies as arguments)
  • Inject them through a static interface as part of bootstrap

The first two options were considered too expensive and cumbersome:

  • Since the Phrase object is a replacement for primitive (string literal), it is transient
  • Creating such an object with a proper constructor injection would require involving a factory and referencing each Phrase object to renderers—but isn't that too much infrastructure for the former string literal?
  • Using API injection would make impossible to use automatic type casting __toString() (and lazy initialization)—and would have significant impact on existing client code

So implementation of the Phrase class ended up looking like the following:

<?php
/**
 * Phrase (for replacing Data Value with Object)
 *
 */
namespace Magento;

class Phrase
{
    /**
     * Default phrase renderer. Allows stacking renderers that "don't know about each other"
     *
     * @var \Magento\Phrase\RendererInterface
     */
    private static $_renderer;

    /**
     * String for rendering
     *
     * @var string
     */
    private $_text;

    /**
     * Arguments for placeholder values
     *
     * @var array
     */
    private $_arguments;

    /**
     * Set default Phrase renderer
     *
     * @param \Magento\Phrase\RendererInterface $renderer
     */
    public static function setRenderer(\Magento\Phrase\RendererInterface $renderer)
    {
        self::$_renderer = $renderer;
    }

    /**
     * Phrase construct
     *
     * @param string $text
     * @param array $arguments
     */
    public function __construct($text, array $arguments = array())
    {
        $this->_text = (string)$text;
        $this->_arguments = $arguments;
    }

    /**
     * Render phrase
     *
     * @return string
     */
    public function render()
    {
        return self::$_renderer ? self::$_renderer->render($this->_text, $this->_arguments) : $this->_text;
    }

    /**
     * Defers rendering to the last possible moment (when converted to string)
     *
     * @return string
     */
    public function __toString()
    {
        return $this->render();
    }
}

So while using the static setter in bootstrap has a number of issues and contradicts the principle of dependency injection, this implementation is considered optimally usable in client code.

Behind the \Magento\Phrase\RendererInterface, there is a composite pattern implemented as \Magento\Phrase\Renderer\Composite. It stacks multiple renderers. Each has single responsibility: one for translating, another for placeholders replacement, etc.

Initializing Environment for Rendering

For use in Magento application, the \Magento\Phrase is initialized as follows:

\Magento\Phrase::setRenderer($this->_objectManager->get('Magento\Phrase\RendererInterface'));

The object manager and dependency injection framework do all the legwork to provide the implementation behind the interface—stacking the composition of renderers.

Since the usage of a global function __() and static interface takes place, some rearrangements have to be made in unit tests. For convenience of assertions and fixtures, in unit tests only the "placeholder" renderer is used instead of the "composite":

\Magento\Phrase::setRenderer(new \Magento\Phrase\Renderer\Placeholder());

XML Client Code

In some XML files, the only thing that changed was removing module context, while the "translate" attribute stayed the same:

<field id="lister_widget_scroll" translate="label comment" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
    <label>Events to Scroll per Click in Event Slider Widget</label>
    <comment>You can override this default value in CMS using the limit="x" variable.</comment>
</field>

In other files, parent nodes don't know about their child nodes anymore. Instead, translate="true" is used in a node, the value of which is intended to be translated:

<block class="Magento\Backend\Block\Widget\Grid\Column" as="display_state">
    <arguments>
        <argument name="header" xsi:type="string" translate="true">Countdown Ticker</argument>
        <argument name="type" xsi:type="string">options</argument>
        <argument name="index" xsi:type="string">display_state</argument>
...
    </arguments>
</block>

Translation Dictionary Tools

Magento developer tools include a static code analysis tool that enables generating a CSV dictionary out of Magento source code. It searches for any occurrences of __() in PHP files and translate attributes in XML files, and extracts literals for the dictionary.

The tool is available at https://github.com/magento/magento2/tree/master/dev/tools/Magento/Tools/I18n. It is intended for translators or extension developers who wish to provide language packs for the entire Magento application or a given module/theme. Description of the tool interface and use cases is omitted here intentionally, as it deserves a dedicated article.

Dictionary Files Location

The CSV dictionary files in Magento 2.x were relocated this way:

Was in Magento 1.xBecame in Magento 2.x
`app/locale/en_US/Mage_Checkout.csv``app/code/Magento/Checkout/i18n/pt_BR.csv`
`app/design/frontend/default/modern/locale/en_US/translate.csv``app/design/frontend/magento_demo/i18n/en_US.csv`

Notable differences (in context of translation subsystem):

  • Root directory now belongs to respective modules (or themes)
  • File name corresponds to language code instead of module name or "translate.csv"
  • The "locale" directory renamed into "i18n"

"Convention over configuration" principle is used: location of dictionary files is conventional, so they don't need to be declared in configuration (the former config.xml).

With respect to keeping dictionary files per module, the possible collisions of the same phrase with different translation are handled during assembly of the dictionaries into one. During merging of the CSV files, if a phrase declared by a module is already defined in the merged result, it will be overridden by the latest occurrence.

Known Issues and Workarounds

Despite many improvements, the architecture of translation subsystem in Magento 2.x is of course still not perfect. The team concentrated primarily on the developer experience—that is, reducing code coupling and improving usability in client code. As mentioned, other issues had secondary priority.

Currently known issues:

  • The __toString() method is not always invoked automatically because of specifics of client code. For example if Phrase object leaks into a json_encode(), it will be cast to an object in JSON instead of a string. As a workaround, the __() function forcefully casts the Phrase object to string.
  • The "en_US to en_US" dictionaries actually rendered useless, because using the dictionary tool would be more efficient and wouldn't require maintenance by the Magento Core team.
  • In some XML files, labels are defined as attributes. It is impossible to use translate="true" for them, as it would be ambiguous:
<config>
    <option name="text" label="Text" renderer="Magento\Adminhtml\Block\Catalog\Product\Edit\Tab\Options\Type\Text">
        <inputType name="field" label="Field" />
        <inputType name="area"  label="Area" />
    </option>

As a result, the dictionary tools do not detect some phrases in XML files.

  • Not all concerns are separated in the implementation. Specifically, the "translation" and "customization" operations are still not segregated. The notion of "translate" is equated with distinguishing a phrase for the UI (but these are different concerns and uniting them is misleading)
  • There is still a big fat JavaScript array of phrases loaded into every page, but only some of them are used, and then only on certain pages

Possible Improvements

There can be many more architectural improvements for the translation mechanism. Or, to generalize, for the mechanism of presenting phrases in the user interface:

  • Actually distinguish phrase customization and translation concerns. For the developer, it would mean breaking down \Magento\Core\Model\Translate into 3 different concerns: translation, customization, and inline editing. For the user—"Inline Translation" tool will have to be renamed into "Inline Text Editing" tool or possibly consolidated with another WYSIWYG tool. The user would see the phrases in current language of user interface instead of the original entries in English.
    • Implication: the "Inline Text Editing" tool could become the only way to customize phrases—that would be a user-only capability. But the feature could be still retained for developers, too—for example, in these files:
__/
|__/app
| |__/code/Mage/Catalog
|   |__/phrases.csv
|__/design
   |__/frontend/magento_demo
       |__/phrases.csv

These files would contain phrases only in English (because it is in the source code by convention) and could have replacements for these phrases also only in English. And these phrases would be invoked before translation.

  • In JavaScript on a page, don't load any kind of dictionaries. Instead, all the translation can occur in the server-side on demand. But for the client side, leave only placeholders replacement. As result, the entire component $.mage.translate can be deleted.
  • Delete the en_US CSV dictionaries. They can be always created out of the current code base using the dictionary tools.
  • "Convention over Configuration" for distinguishing phrases in XML-configuration files: in the dictionary generator tool, instead of looking for special "translate" attribute, conventionally scan nodes and attributes called "label", "title", "description", etc. It will make the translation dictionary tools much simpler and more dependable.
  • Other new features of translation that didn't exist before in Magento framework—for example, declension of nouns based on numeral argument value:
%1 [%1|cat|cats] -> 1 cat / 2 cats
%1 [%1|кот|кота|котов] -> 1 кот / 2 кота / 9 котов

It could be yet another renderer behind \Magento\Phrase\RendererInterface but encapsulating a library like Zend_Translate_Plural or similar.

Conclusions

The translation mechanism in Magento 2.x is improved with abstractions, separation of concerns, and reduced dependencies. It provides an overall better experience for developer and translator, and empowers them to create a better user experience than with its predecessor.

Should readers have questions or more ideas of improving architecture of translations in Magento platform—feel free to comment on this page. Contributions with fixes for the known issues are certainly welcome through Magento 2 GitHub repository.


Copyright (C) 2013, Magento Inc (an eBay company)

@mzeis

This comment has been minimized.

Copy link

@mzeis mzeis commented Nov 19, 2013

Thanks for this article and the explanation of the considerations behind the Magento 2 approach.

XML Client Code

In some XML files, the only thing that changed was removing module context, while the "translate" attribute stayed the same:
...
In other files, parent nodes don't know about their child nodes anymore. Instead, translate="true" is used in a node, the value of which is intended to be translated:

Is there a way to tell when I have to use the one way of declaration and when to use the other? If not I'd suggest two always use the second declaration as at first glance I really like it.

<field id="lister_widget_scroll" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
    <label translate="true">Events to Scroll per Click in Event Slider Widget</label>
    <comment translate="true">You can override this default value in CMS using the limit="x" variable.</comment>
</field>

This may be a few more characaters to write but is is very transparent and more consistent. Plus you've got the declaration on the node itself and not on some (possibly far distant) parent node. This also is a kind of separation of concerns thing.

Known Issues and Workarounds

Labels defined as attributes

In some XML files, labels are defined as attributes. It is impossible to use translate="true" for them, as it would be ambiguous: <config> <option name="text" label="Text" renderer="Magento\Adminhtml\Block\Catalog\Product\Edit\Tab\Options\Type\Text"> <inputType name="field" label="Field" /> <inputType name="area" label="Area" /> </option> As a result, the dictionary tools do not detect some phrases in XML files.

A workaround would be to use the translate attribute to tell which other attributes to translate:
<option translate="label" name="text" label="Text" renderer="Magento\Adminhtml\Block\Catalog\Product\Edit\Tab\Options\Type\Text"></option>
But that would be kind of a contradiction to what I suggested before.

To differentiate between translating the node value and the node attribute, there could be two different declarations:

<label translate="true">Events to Scroll per Click in Event Slider Widget</label>
<option translateAttribute="label" name="text" label="Text" renderer="Magento\Adminhtml\Block\Catalog\Product\Edit\Tab\Options\Type\Text"></option>

But to be honest my solution seems kind of quirky. For me it's kind of a "XML structure smell". Perhaps somebody comes up with a better solution but at the moment I'm asking myself if this could be a sign that it's not always to solution to move child node data to attributes for the sake of a few saved lines.

Segregation of translation / customization

Not all concerns are separated in the implementation. Specifically, the "translation" and "customization" operations are still not segregated.

I'm not sure if I see the benefit of segregating between "translation" and "customization". I have to modify some existing string and whether I rephrase it in the same language or replace it with another string never mattered for my or my clients. :-)

JavaScript translation

There is still a big fat JavaScript array of phrases loaded into every page, but only some of them are used, and then only on certain pages

For me it feels a little bit the same as the problem with JavaScript files altogether. There should be a way to group JavaScript files, e.g. to include in in a "global" context (that means on every page), in a product page contect, checkout context, ... Maybe think RequireJS. The same way if there is a way to tell which JS translations are used in which context you could conditionally include it.

Or you use the advantage that in Magento 2 there is kind of a "build" stage (where static files are copied from the modules to the public folder) and in that process create alone-standing JavaScript dictionary files which also get copied to the public folder. Then you only have to download the translation file(s) once and they can be cached client-side using expire headers and the like. My thoughts on this one are very vague. I'll stop now.

@antonmakarenko

This comment has been minimized.

Copy link
Owner Author

@antonmakarenko antonmakarenko commented Nov 19, 2013

@mzeis Thank you for good questions!

Is there a way to tell when I have to use the one way of declaration and when to use the other? If not I'd suggest two always use the second declaration as at first glance I really like it.

For purpose of translation, yes, using a child node seems more appealing. But consider from different perspective: when you design an XML-schema, you anticipate whether a given scalar field is supposed to be a CDATA (multi-line string) or one-line string. Using attribute automatically gives some constraints and may be valuable. Another consideration -- how you intend to do merging of XML-files (if applicable). I think that intent/design of the schema is more important than translation. Ideally, I'd see the mechanism of translation (or in more general, identifying phrases) as transparent and non-intrusive as possible -- I mentioned it as the "Convention over Configuration" point in "Possible Improvements" section.

For me it's kind of a "XML structure smell". Perhaps somebody comes up with a better solution but at the moment I'm asking myself if this could be a sign that it's not always to solution to move child node data to attributes for the sake of a few saved lines.

I agree. And the solution I proposed is not to have any "translation" notion in XML-files in the first place (again, refer to the "Possible Improvements" section).

I'm not sure if I see the benefit of segregating between "translation" and "customization". I have to modify some existing string and whether I rephrase it in the same language or replace it with another string never mattered for my or my clients.

For developer, the value is separation of concerns in implementation -- just makes it more flexible and organized, and less complex. For example, you won't need to have "enabling/disabling inline translation" hack (in this article, I renamed the "inline translation" into "customization", because intent is exactly customization) all over the place: https://github.com/magento/magento2/blob/master/app/code/Magento/Core/Block/AbstractBlock.php

        if ($html === false) {
            if ($this->hasData('translate_inline')) {
                $this->_translator->setTranslateInline($this->getData('translate_inline'));
            }

...

            if ($this->hasData('translate_inline')) {
                $this->_translator->setTranslateInline(true);
            }
        }

For translator and Magento Core team -- no need to maintain the English-to-English dictionary. For end-user, who edits phrases inline -- no necessity to see original phrase in English (it seems irrelevant, if already customized by developer).

For me it feels a little bit the same as the problem with JavaScript files altogether. There should be a way to group JavaScript files, e.g. to include in in a "global" context (that means on every page), in a product page contect, checkout context, ... Maybe think RequireJS.

The necessity of grouping assets and RequireJS are valid problems on their own. I'm trying to convey a different idea: that on the client side, handling the concern of translation is not necessary at all. Take a look at existing implementation: approx 300 phrases are pre-loaded on every page, supposedly for translation. But a page may be using in fact 0 or a few of them. What does the $.mage.translate do? Merely replacing one phrase into another, plus replaced placeholders. Is it aware of language context? No. What about customization? No -- all that already comes from the server side. The only thing that's impossible to handle on server side is replacing placeholder values... Which begs for refactoring and simplifying. I am certain, that separation of concerns is the key here.

@bpetersxnx

This comment has been minimized.

Copy link

@bpetersxnx bpetersxnx commented Feb 19, 2014

Good article, thanks for write up. Will add

@jaimestuardo

This comment has been minimized.

Copy link

@jaimestuardo jaimestuardo commented Jan 8, 2016

Hello... is it possible to get a CSV file with all text in English? I need to translate text into Spanish, and getting each CSV from different modules could be never ending task. I need only one file with the texts to be translated. I tried with https://crowdin.com/project/magento-2/es-ES page before, but this page is terrible buggie, specially because the generated source.csv file is completely wrong, so it is useless.... it is better to translate texts by myself.

@goralgen

This comment has been minimized.

Copy link

@goralgen goralgen commented Apr 1, 2016

Hi. I had spent more than 3 hours, but did not find solution.
If subscribe field empty we will get JS message 'This is a required field.' How it may be translated by inline translate (using book on FE). When I inserted to table TRANSLATE (by sql) string manually, it's working. So, how show book for inline translation for js popups errors?
I try use this in my phtml, but this not work correctly (translated data added to DB, but original string "This is a required field." is empty. why?)

<script>
    require([
        'jquery',
        'jquery/ui',
        'mage/translate'
    ], function ($) {
        $(window).load(function () {
            $.mage.__(<?php echo json_encode(__('This is a required field.'))?>);
        });
    });
</script>
@bentideswell

This comment has been minimized.

Copy link

@bentideswell bentideswell commented Jun 22, 2016

The __ function doesn't cast the response to a string as suggested in this article, however I think this would be a great change for the core code. Is there anyway of getting this changed made in the Magento 2 core?

@danemacmillan

This comment has been minimized.

Copy link

@danemacmillan danemacmillan commented Oct 12, 2017

Note that with the new translation function, any logic that originally in Magento1 checked if the text was not a strict empty string will fail.

For example:

$helpFeedbackMessage = __('');
// Determines that variable has string length greater than 0.
if (strlen($helpFeedbackMessage)) {
    // This will not be run, which is correct.
}
// Determines that variable is not a strict empty string,
// which means that the condition will pass and any logic within
// the braces will run. This happens because the !== operator
// does an OR comparison, and because the variable is not a
// string, strictly speaking, at the time of comparison, it fails
// on that condition.
// http://php.net/manual/en/language.operators.comparison.php
if ($helpFeedbackMessage !== "") {
    // This will be run, which is incorrect.
}

The intention of the developer is clear in both conditional statements that they do not want the logic within braces to run if a string is zero-length/empty, but the way of determining this is different. This is relevant to anyone porting their entire codebase from M1 to M2, as translations are handled differently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.