Translation

The translation logic in SSE is fully implemented, but the JSON files for other languages referenced below do not ship due to concerns over the accuracy of the translation.

Types of Translations

There are four types of translations that occur in SSE:

  1. JavaScript Strings
  2. SimpleXML Labels, Titles, Etc.
  3. SimpleXML HTML Panels
  4. JSON files

JavaScript Strings

JavaScript string translation is handled natively by Splunk using underscore. While normally you might write:

$("#myObj").append( $("<div>").text("Hello World!"))

You can add underscore and instead run:

$("#myObj").append( $("<div>").text( _("Hello World!").t() ))

And Splunk itself will natively translate that.

SimpleXML Labels, Titles, Etc.

These are also natively handled by Splunk without any extra effort.

SimpleXML HTML Panels

For SimpleXML HTML Panels there are in general two options:

i18ntag Route

It is possible to add i18ntag=“” attributes to any HTML Tags that you would like Splunk to natively translate. (There is also an i18nattr=“myattr”.) See an example here (as of this writing). If you have complicated HTML, e.g.,:

<p>We have many important items:
    <ul>
        <li data-element="Item One">Here is one (<a href="https://github.com/bdalpe/Splunk_TA_okta/blob/master/default/data/ui/manager/data_inputs_okta.xml">link</a>)</li>
        <li data-element="Item Two">This is the <b>most</b> important item.</li>
    </ul>
</p>

This would quickly become very problematic:

<p><span i18ntag="">>We have many important items:</span>
    <ul>
        <li data-element="Item One" i18nattr="data-element"><span i18ntag="">Here is one</span> (<a i18ntag="" href="https://github.com/bdalpe/Splunk_TA_okta/blob/master/default/data/ui/manager/data_inputs_okta.xml">link</a>)</li>
        <li data-element="Item Two" i18nattr="data-element"><span i18ntag="">This is the <b i18ntag="">most</b> <span i18ntag="">important item.</span></li>
    </ul>
</p>

Resulting in the following translation po:

EnglishJapanese.. Etc ..
We have many important items:
Here is one
link
This is the
most
important item.

This is viable, but creates maintenance hassles and is uglier.

SSE Route

Instead, in SSE you specify at a higher level tag that you wish to replace the contents, and then it will replace the entire HTML of that tag. So the above example would become:

<p data-translate-id="important-items">We have many important items:
    <ul>
        <li data-element="Item One">Here is one (<a href="https://github.com/bdalpe/Splunk_TA_okta/blob/master/default/data/ui/manager/data_inputs_okta.xml">link</a>)</li>
        <li data-element="Item Two">This is the <b>most</b> important item.</li>
    </ul>
</p>

Resulting in the following translation po:

EnglishJapanese.. Etc ..
<p data-translate-id="important-items">We have many important items:\n <ul>\n <li data-element="Item One">Here is one (<a href="https://github.com/bdalpe/Splunk_TA_okta/blob/master/default/data/ui/manager/data_inputs_okta.xml">link</a>)</li>\n <li data-element="Item Two">This is the <b>most</b> important item.</li>\n </ul>\n </p>

This is implemented in SSE by adding the following code at the top of runPageScript.js. This will run before any other content, and generally swaps content with an imperceptible delay (first page load can be up to a couple of seconds, but afterward it is cached in the browser).

var appName = 'Splunk_Security_Essentials';
window.localeString = location.href.replace(/\/app\/.*/, "").replace(/^.*\//, "")
require(['jquery'], function($) {
        window.translationLoaded = $.Deferred();
        let languageLoaded = $.Deferred();
        let splunkJSLoaded = $.Deferred();

        $.when(languageLoaded, splunkJSLoaded).then(function(localizeStrings) {
            function runTranslation() {
                // console.log("TRANSLATE: Running Translation!", localizeStrings)
                let translatable = $("[data-translate-id]");
                for (let i = 0; i < translatable.length; i++) {
                    let id = $(translatable[i]).attr("data-translate-id");
                    // console.log("TRANSLATE: Working on " + id)
                    if (localizeStrings[id]) {
                        // console.log("TRANSLATE: For " + id + " got ", localizeStrings[id])
                        translatable[i].innerHTML = localizeStrings[id]
                    }

                }
                // console.log("TRANSLATE: Language Loading Complete", Date.now() - window.startTime)
            }

            function runTranslationOnElement(element) {
                for (let id in localizeStrings) {
                    if (element.find("#" + id).length) {
                        element.find("#" + id).html(localizeStrings[id])
                    }
                }
            }
            runTranslation()
            window.runTranslationOnElement = runTranslationOnElement
            window.translationLoaded.resolve()
        })
        if (typeof localStorage[appName + "-i18n-" + window.localeString] != "undefined" && localStorage[appName + "-i18n-" + window.localeString] != "") {
            let langObject = JSON.parse(localStorage[appName + "-i18n-" + window.localeString])
            if (langObject['build'] == build) {
                languageLoaded.resolve(langObject)
                if (window.location.href.indexOf("127.0.0.1") >= 0 || window.location.href.indexOf("localhost") >= 0) { // only refresh in a dev env (or one without latency), otherwise it's not necessary as long as the build is the same.
                    // console.log("Found cache hit in localStorage", Date.now() - window.startTime)
                    $.ajax({
                        url: $C['SPLUNKD_PATH'] + '/services/pullJSON?config=htmlpanels&locale=' + window.localeString,
                        async: true,
                        success: function(localizeStrings) {
                            localizeStrings['build'] = build;
                            localStorage[appName + "-i18n-" + window.localeString] = JSON.stringify(localizeStrings)
                        }
                    });
                    $.ajax({
                        url: $C['SPLUNKD_PATH'] + '/services/pullJSON?config=sselabels&locale=' + window.localeString,
                        async: true,
                        success: function(localizeStrings) {
                            localStorage[appName + "-i18n-labels-" + window.localeString] = JSON.stringify(localizeStrings)
                        }
                    });

                }
            } else {
                // console.log("localStorage out of date, starting to grab file", Date.now() - window.startTime)
                $.ajax({
                    url: $C['SPLUNKD_PATH'] + '/services/pullJSON?config=htmlpanels&locale=' + window.localeString,
                    async: true,
                    success: function(localizeStrings) {
                        languageLoaded.resolve(localizeStrings)
                        localizeStrings['build'] = build;
                        localStorage[appName + "-i18n-" + window.localeString] = JSON.stringify(localizeStrings)
                    }
                });
                $.ajax({
                    url: $C['SPLUNKD_PATH'] + '/services/pullJSON?config=sselabels&locale=' + window.localeString,
                    async: true,
                    success: function(localizeStrings) {
                        localStorage[appName + "-i18n-labels-" + window.localeString] = JSON.stringify(localizeStrings)
                    }
                });
            }
        } else {
            // console.log("Not in localStorage, starting to grab file", Date.now() - window.startTime)
            $.ajax({
                url: $C['SPLUNKD_PATH'] + '/services/pullJSON?config=htmlpanels&locale=' + window.localeString,
                async: true,
                success: function(localizeStrings) {

                    languageLoaded.resolve(localizeStrings)
                    localizeStrings['build'] = build;
                    localStorage[appName + "-i18n-" + window.localeString] = JSON.stringify(localizeStrings)
                }
            });
            $.ajax({
                url: $C['SPLUNKD_PATH'] + '/services/pullJSON?config=sselabels&locale=' + window.localeString,
                async: true,
                success: function(localizeStrings) {
                    localStorage[appName + "-i18n-labels-" + window.localeString] = JSON.stringify(localizeStrings)
                }
            });
        }

        require(['jquery',"splunkjs/mvc/simplexml/ready!"], function($) {
                splunkJSLoaded.resolve()
            })
    })

In effect, it first checks the cache for the language files. If it doesn’t have a hit or if the build is different (aka, SSE is uploaded) it downloads the language files and stores them to the cache. Once it has the files (either from cache or not), it will then look for any translatable divs and swap them with the translated objects.

JSON Files

There is no native way in SSE to translate JSON files or pull different JSON files based on locale.

The pullJSON rest handler will handle any files that are completely translated (e.g., usecases.json). For elements in ShowcaseInfo though, a partial translation is required. Those files are grabbed via pullJSON.py (to grab the right locale sselabels.json) and then applied via javascript built into a few respective scripts:

  1. ProcessSummaryUI.js
  2. bookmarked_content.js
  3. contents.js (most important)
  4. data_inventory.js

Supporting Scripts

In the internal-only build_scripts there are a couple of wrappers around this process of generating pot files and integrating changes (via po files) back in from downstream translators.

  1. update_i18n_pot.pl will run ./splunk extract i18n -app Splunk_Security_Essentials and then grab all of the required JSON and SimpleXML HTML panels. It will combine them and output a unified pot file.
  2. parse_po_files.pl will read in rendered po files from downstream translators and push any required changes into htmlpanels.json and sselabels.json files.