The translation helper in Neos CMS

sebobo

Sebastian Helzle

Posted on February 9, 2021

The translation helper in Neos CMS

Neos CMS has a very powerful translation helper in its own rendering language Fusion. It can handle simple translations matching the users language but also more complex localisation options for plural forms and to fill placeholders.
All features derive directly from the the i18n & l10n framework provided by Neos underlying PHP framework Flow.

The translation helper

Usually the helper is called in its shorthand form like this:

myLabel = ${I18n.translate('Vendor.Package:Main:component.label')}
Enter fullscreen mode Exit fullscreen mode

This call means, that in the Vendor.Package package a Resources/Private/Translations/en folder exists. Where "en" stands for an English variant and I just used as an example. If you use a different or more languages (and their variants) you should have a folder for each language code.
For our example each language folder would contain at least one file called Main.xlf and it contains an entry for the id component.label.

The text for that entry is retrieved and myLabel will contain it. When the user uses a different language and a translation exists, the correct label will be retrieved. If not, a fallback chain is triggered.
The file is structured like this:

<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file original="" product-name="Vendor.Package" source-language="en" datatype="plaintext">
        <body>
            <trans-unit id="component.label">
                <source>My component</source>
            </trans-unit>
        </body>
    </file>
</xliff>
Enter fullscreen mode Exit fullscreen mode

Each trans-unit is a translation unit that can be retrieved in Fusion.

When having multiple languages defined, the Flow Framework used by Neos will provide the correct translation based on the current frontend language dimension and defined fallback chain.

The translation file in another language other than the default would look like this:

<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file original="" product-name="Vendor.Package" source-language="en" target-language="de" datatype="plaintext">
        <body>
            <trans-unit id="component.label">
                <source>My component</source>
                <target>Meine Komponente</source>
            </trans-unit>
        </body>
    </file>
</xliff>
Enter fullscreen mode Exit fullscreen mode

It defines the target language and has a target entry for each unit.

Using arguments and quantities

The translation helper also allows you to provide named and unnamed arguments. This allows you to use placeholders in your text. For named arguments you could use a placeholder like {amount} to show the number of items in the middle of a text. And in another language the placeholder could be at the end.
Unnamed arguments can be inserted by their index. So the first item would be assigned via {0} and so on. I always recommend to use named arguments, as it makes managing those translations so much easier. Especially when there are more arguments.

You can also provide a quantity. This would allow use to not only have a variable in our text like described above, but also have text variants based on the number itself. Many languages have variants for 0, 1 or more items. Some languages have even more differentiations.

Here is an example of an xliff file using quantities and arguments:

<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file original="" product-name="Vendor.Package" source-language="en" target-language="de" datatype="plaintext">
        <body>
            <group id="some.label" restype="x-gettext-plurals">
                <trans-unit id="component.label[0]">
                    <source>You see {amount} item</source>
                    <target>Du siehst {amount} Element</source>
                </trans-unit>               
                <trans-unit id="component.label[1]">
                    <source>You see {amount} items</source>
                    <target>Du siehst {amount} Elemente</source>
                </trans-unit>
            </group>
        </body>
    </file>
</xliff>
Enter fullscreen mode Exit fullscreen mode

Using the helper

The translation helper looks in its full form like this:

translate($id, $originalLabel = null, array $arguments = [], $source = 'Main', $package = null, $quantity = null, $locale = null)
Enter fullscreen mode Exit fullscreen mode

You can use it in Fusion like this:

amount = 5
myAmountLabel = ${I18n.translate('component.amount', '{amount} Items', { amount: this.amount }, 'Main', 'Vendor.Package', this.amount)}
Enter fullscreen mode Exit fullscreen mode

Every argument of the helper that has a default value can be skipped if not needed. But usually you have to set most to make sure that the translation is fetched from the correct package. As the framework assumes Neos.Neos as default package name (in the Neos CMS context).

A less cluttered variant

The translation helper also has another method that provides a TranslationParameterToken.

You can generate one with this call:

myLabel = ${I18n.id('component.label')}
Enter fullscreen mode Exit fullscreen mode

Or this one:

myLabel = ${I18n.value('My component')}
Enter fullscreen mode Exit fullscreen mode

But this will not yet return a valid translation. The token that is returned provides more methods that can be chained. To make it fully work we again need to provide the same parameters as we did with the previous calls:

myLabel = ${I18n.id('component.label').package('Vendor.Package').source('Main').translate()}
Enter fullscreen mode Exit fullscreen mode

So what did we gain? Nothing yet.

But if we don‘t call translate() at the end we can reuse our token:

i18n = ${I18n.id('').package('Vendor.Package').source('Main')}
myLabel = ${this.i18n.id('component.label').translate()}
myOtherLabel = ${this.i18n.value('Other label').translate()}
Enter fullscreen mode Exit fullscreen mode

As you see, we can now skip setting the package and source over and over again.
This approach is quite helpful when you use the helper with AFX and the code is less cluttered.

Note: If you skip the translate() at the end it will still work correct in most cases as the token has a toString() implementation. But being explicit can prevent unforeseen errors.

It‘s always good to have a look at the implementation of the helper. Maybe you spot some more nice features that help you in your project.

Please contact me if you do and I can add some more hints to this post.
Translating by value

You might have seen that you can also retrieve translation by their text instead of an id. My personal experience is that this can cause problems when the text changes, meanings change or duplicates appear. So I only use and recommend unique ids. And you can still derive the meaning in the code by providing fallback values if a translation is not available.
Using the helper without Fusion

If you have Fluid templates, you have a similar helper there. It looks like this:

<f:translate id="label.id"/>
Enter fullscreen mode Exit fullscreen mode

If you write JavaScript for custom Neos UI plugins or backend modules you can also use the JS version of the helper.
It is available in the Neos.UI via the i18nRegistry. And in backend modules as global object:

window.NeosCMS.I18n.translate()
Enter fullscreen mode Exit fullscreen mode

Summary

Translating labels in Neos allows more flexibility than you might know. Use the token form, quantities and named arguments to make it easier to maintain your code and provide better translations & localisations which take the user into account.

💖 💪 🙅 🚩
sebobo
Sebastian Helzle

Posted on February 9, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

The translation helper in Neos CMS
neoscms The translation helper in Neos CMS

February 9, 2021