Cover Image for Magento: Translate dynamic values in KnockOut templates

Magento: Translate dynamic values in KnockOut templates

TLDR;

You can add placeholders in the knockout template

<!-- ko if: false -->
    <!-- ko i18n: 'value1' --><!-- /ko -->
    <!-- ko i18n: 'value2' --><!-- /ko -->
    <!-- ko i18n: 'value3' --><!-- /ko -->
<!-- /ko -->
<ul data-bind="foreach: getValues()">
    <li>
        <span data-bind="i18n: $data"></span>
    </li>
</ul>

Download and install install Mecodeninja_DynamicTranslation and add dynamic attribute to some translations.

composer require mecodeninja/module-dynamic-translation

"value1","translated value1",dynamic "value2","translated value2",dynamic "value3","value3",dynamic

Let's go over two way in which you can translate dynamic values in knockout templates. First lets go over the background context

Background

I'm going to be writing about two different Magento features and how they interact so let me first describe each feature enough so that we can understand the result.

Knockout

Magento users KockoutJs to render certain presentations on the frontend. The advantage of this method is that Magento can deliver dynamic data to the frontend using AJAX queries which return JSON objects and then hydrate static HTML templates with the data contained within.

Translation

Magento uses a few different methods to allow developers to translate strings within the framework. The one we'll be discussing, the most frequently used, is Dictionary. In the implementation Magento creates a JSON file which has key-value pairs. The original string as the key and the translation as the value.

See: js-translation.json as an example

{"You have no items in your shopping cart.":"Foo Test"}

When Knockout hydrates the template with the data it will use this mapping to translate the string in the key replacing it with the string in the value.

Issue

The issue you might experience is that when Magento is generating the contents of js-translation.json it collects all of the knockout templates and JS files, uses a regex pattern to look for "translateable" strings, then collects only those translations into the js-translation.json dictionary that is served.

Therefore; if you have a knockout template that is going to receive a dynamic value it will not have an entry in the dictionary (it's not found by the regex) and it will not be translated since there is no entry.

Example

You have a knockout component like the following:

Willwright/Demo/view/frontend/templates/demo.phtml

<div id="willwright-demo-component" data-bind="scope:'willwright-demo'">
    <!-- ko template: getTemplate() --><!-- /ko -->
    <script type="text/x-magento-init">
    {
        "#willwright-demo-component": {
            "Magento_Ui/js/core/app": {
                "components": {
                    "willwright-demo": {
                        "component": "Willwright_Demo/js/demo",
                        "template": "Willwright_Demo/demo"
                    }
                }
            }
        }
    }
    </script>
</div>

Willwright/Demo/view/frontend/web/template/demo.html

<ul data-bind="foreach: getValues()">
    <li>
        <span data-bind="i18n: $data"></span>
    </li>
</ul>

Willwright/Demo/view/frontend/web/js/demo.js

define(['jquery', 'uiComponent', 'ko'], function ($, Component, ko) {
        'use strict';
        return Component.extend({
            initialize: function () {
                this._super();
            },
            getValues: function(){
                return [
                    'value1',
                    'value2',
                    'value3'
                ];
            }
        });
    }
);

Willwright/Demo/i18n/en_US.csv

"value1","translated value1"
"value2","translated value2"
"value3","value3"

This results in the following output:

Untranslated Knockout Strings

js-translation.json

[]

Note that none of the values are translated and that the dictionary returned by Magento is empty.

Solution1: Placeholders

Solution one is to put some placeholders for all the values that could possibly be returned by the JS. This will cause the preprocessor to find the values and include their translations into the dictionary, in the case that they do indeed need to be translated.

All that is needed is to change Willwright/Demo/view/frontend/web/template/demo.html to the following

<!-- ko if: false -->
    <!-- ko i18n: 'value1' --><!-- /ko -->
    <!-- ko i18n: 'value2' --><!-- /ko -->
    <!-- ko i18n: 'value3' --><!-- /ko -->
<!-- /ko -->
<ul data-bind="foreach: getValues()">
    <li>
        <span data-bind="i18n: $data"></span>
    </li>
</ul>

This produces:

Solution1 Translated Strings

js-translation.json

{"value1":"translated value1","value2":"translated value2"}

This time the preprocessor matched the knockout placeholders and included the translation KVPs into the dictionary.

Solution2: Extend Translation

Solution1 was a little inelegent to me and I didn't like splitting the reponsibility of translation into both the translation area and the knockout template. I wrote a small module that registers a new preprocessor which allows the developer to add a third column to the translation file CSV. When the third column is set, the preprocessor will add the translation KVP to the dictionary no matter whether the key is matched by the regex or not.

To implement do the following:

composer require mecodeninja/module-dynamic-translation

Update Willwright/Demo/i18n/en_US.csv to:

"value1","translated value1",dynamic
"value2","translated value2",dynamic
"value3","value3",dynamic

This produces:

Solution2 Translated Strings