EDD9ADFBC6E745A6AD7251F8A545020A
  • Thomas Pollinger
  • 23.05.2019
  • DE

SmartExtensions: Mehrsprachenfähigkeit

Damit man mit den Extensions (Erweiterungen) auch alle Vorteile des Management Servers nutzen kann. Wird innerhalb des Frameworks Mehrsprachingkeit, angelehnt an die Vorgaben des Management Servers, unterstützt. Dies wird sehr einfach mit CSS-Dateien ermöglicht, welche in der Basis-Version Deutsch und Englisch für alle vorhandenen Extensions verfügbar sind.

Funktionsweise

Je Extension und einmal zentral liegen im Ordner ./config/ die s.g. localized.{Sprachencode}.css Dateien.

localized.de-DE.css
localized.en-US.css

und zusätzlich auch immer die minfizierte Version:

localized.de-DE.min.css
localized.en-US.min.css

Die Extensions starten primär mit der Sprache Englisch und das ist auch immer der Fallback. Jedoch sobald die Verbindung zum Management Server steht, wird die aktuelle GUI-Sprache angefragt und dann versucht die entsprechenden Sprachdateien zu laden. Sollte dies nicht Möglich sein, weil nicht vorhanden, wird automatisch wieder die englische Version geladen. 

Das wird mit dieser Funktion in jeder Extension ermittelt und umgesetzt:

function setExtensionDialogLanguage(rqlConnectorObj) {
    let thisFunction = {
        Name: arguments.callee.name,
        DebugMode: rqlConnectorObj.debugMode,
        BaseHref: rqlConnectorObj.baseHref.root,
        ConnectorMode: rqlConnectorObj.connectorMode
    };
    var valueFileExtension = ".min";
    if (thisFunction.DebugMode) {
        var valueFileExtension = "";
    }
    thisFunction.DebugMode && console.log(`\n\n${arguments.callee.name}()\nfn =>`);
    var valueDialogLanguageId = "";
    switch (rqlConnectorObj.info.session.DialogLanguageId) {
        case "CHS":
            /* Chinese */
            valueDialogLanguageId = "zh-CN";
            break;
        case "CSY":
            /* Czech */
            valueDialogLanguageId = "cs-CZ";
            break;
        case "NLD":
            /* Dutch */
            valueDialogLanguageId = "nl-NL";
            break;
        case "ENU":
            /* English */
            valueDialogLanguageId = "en-US";
            break;
        case "FRA":
            /* French */
            valueDialogLanguageId = "fr-FR";
            break;
        case "DEU":
            /* German */
            valueDialogLanguageId = "de-DE";
            break;
        case "ELL":
            /* Greek */
            valueDialogLanguageId = "el-GR";
            break;
        case "HUN":
            /* Hungarian */
            valueDialogLanguageId = "hu-HU";
            break;
        case "ITA":
            /* Italian */
            valueDialogLanguageId = "it-IT";
            break;
        case "JPN":
            /* Japanese */
            valueDialogLanguageId = "ja-JP";
            break;
        case "PLK":
            /* Polish */
            valueDialogLanguageId = "pl-PL";
            break;
        case "PTB":
            /* Portuguese */
            valueDialogLanguageId = "pt-PT";
            break;
        case "RUS":
            /* Russian */
            valueDialogLanguageId = "ru-RU";
            break;
        case "ESP":
            /* Spanish */
            valueDialogLanguageId = "es-ES";
            break;
        case "SVE":
            /* Swedish */
            valueDialogLanguageId = "sv-SE";
            break;
        default:
            /* English */
            valueDialogLanguageId = "en-US";
    }
    thisFunction.DebugMode && console.log(`Try to load localized files for ${valueDialogLanguageId}${valueFileExtension}...`);
    /* Core */
    $.ajax({
            url: `${thisFunction.BaseHref}/global/config/localized.${valueDialogLanguageId}${valueFileExtension}.css?release=${valueBuildNumber}`,
            dataType: `text`
        })
        .done(
            function (dialogLanguageCSS) {
                var style = "<style>" + dialogLanguageCSS + "</style>";
                $("head").append(style);
                $("body").attr("lang-core", valueDialogLanguageId);
                thisFunction.DebugMode && console.log(`Localized core file for ${valueDialogLanguageId}${valueFileExtension} successfully loaded`);
            })
        .fail(
            function () {
                console.warn(`Localized core file for ${valueDialogLanguageId}${valueFileExtension} not availble - Loading fallback for core (en-US)`);
                var valueFallbackDialogLanguageId = "en-US";
                $.ajax({
                        url: `${thisFunction.BaseHref}/global/config/localized.${valueFallbackDialogLanguageId}${valueFileExtension}.css?release=${valueBuildNumber}`,
                        dataType: `text`
                    })
                    .done(
                        function (dialogLanguageCSS) {
                            var style = "<style>" + dialogLanguageCSS + "</style>";
                            $("head").append(style);
                            $("body").attr("lang-core", valueFallbackDialogLanguageId);
                            thisFunction.DebugMode && console.log(`Localized core file for ${valueFallbackDialogLanguageId}${valueFileExtension} successfully loaded`);
                        })
                    .fail(
                        function () {
                            console.warn(`Localized core (fallback) file for ${valueFallbackDialogLanguageId}${valueFileExtension} not availble - Switch to debug mode!`);
                            $("body").attr("lang-core", "debug");
                        }
                    );
            });
    /* Extension */
    if (thisFunction.ConnectorMode == "extension") {
        $.ajax({
                url: `config/localized.${valueDialogLanguageId}${valueFileExtension}.css?release=${valueBuildNumber}`,
                dataType: `text`
            })
            .done(
                function (dialogLanguageCSS) {
                    var style = "<style>" + dialogLanguageCSS + "</style>";
                    $("head").append(style);
                    $("body").attr("lang-extension", valueDialogLanguageId);
                    thisFunction.DebugMode && console.log(`Localized extension file for ${valueDialogLanguageId}${valueFileExtension} successfully loaded`);
                })
            .fail(
                function () {
                    console.warn(`Localized extension file for ${valueDialogLanguageId}${valueFileExtension} not availble - Loading fallback for extension (en-US)`);
                    var valueFallbackDialogLanguageId = "en-US";
                    $.ajax({
                            url: `config/localized.${valueFallbackDialogLanguageId}${valueFileExtension}.css?release=${valueBuildNumber}`,
                            dataType: `text`
                        })
                        .done(
                            function (dialogLanguageCSS) {
                                var style = "<style>" + dialogLanguageCSS + "</style>";
                                $("head").append(style);
                                $("body").attr("lang-extension", valueFallbackDialogLanguageId);
                                thisFunction.DebugMode && console.log(`Localized extension file for ${valueFallbackDialogLanguageId}${valueFileExtension} successfully loaded`);
                            })
                        .fail(
                            function () {
                                console.warn(`Localized extension (fallback) file for ${valueFallbackDialogLanguageId}${valueFileExtension} not availble - Switch to debug mode!`);
                                $("body").attr("lang-extension", "debug");
                            }
                        );
                });
    }
    /* Embedded */
    if (thisFunction.ConnectorMode == "embedded") {
        $.ajax({
                url: `${thisFunction.BaseHref}/embedded/config/localized.${valueDialogLanguageId}${valueFileExtension}.css?release=${valueBuildNumber}`,
                dataType: `text`
            })
            .done(
                function (dialogLanguageCSS) {
                    var style = "<style>" + dialogLanguageCSS + "</style>";
                    $("head").append(style);
                    $("body").attr("lang-embedded", valueDialogLanguageId);
                    thisFunction.DebugMode && console.log(`Localized embedded file for ${valueDialogLanguageId}${valueFileExtension} successfully loaded`);
                })
            .fail(
                function () {
                    console.warn(`Localized embedded file for ${valueDialogLanguageId}${valueFileExtension} not availble - Loading fallback for embedded (en-US)`);
                    var valueFallbackDialogLanguageId = "en-US";
                    $.ajax({
                            url: `${thisFunction.BaseHref}/embedded/config/localized.${valueFallbackDialogLanguageId}${valueFileExtension}.css?release=${valueBuildNumber}`,
                            dataType: `text`
                        })
                        .done(
                            function (dialogLanguageCSS) {
                                var style = "<style>" + dialogLanguageCSS + "</style>";
                                $("head").append(style);
                                $("body").attr("lang-embedded", valueFallbackDialogLanguageId);
                                thisFunction.DebugMode && console.log(`Localized embedded file for ${valueFallbackDialogLanguageId}${valueFileExtension} successfully loaded`);
                            })
                        .fail(
                            function () {
                                console.warn(`Localized embedded (fallback) file for ${valueFallbackDialogLanguageId}${valueFileExtension} not availble - Switch to debug mode!`);
                                $("body").attr("lang-embedded", "debug");
                            }
                        );
                });
    }
    thisFunction.DebugMode && console.log(`<= fn\n\n\n`);
}

Diese Funktion kann sowohl mit den Extensions als auch im Embedded-Modus (SmartEdit) genutzt werden. Es wird dann am Body-Tag mehrere passende Attribute eingefügt, auf die dann eine CSS-Kaskade entsprechend reagiert:

<body class="" lang="DEU" lang-core="de-DE" lang-extension="de-DE">
...
</body>

Dabei wird zwischen den s.g. Core und Extension Anteilen unterschieden. Core steht für alles was man immer verwenden kann, wie z.B. Speicher (de-DE) und Save (en-US). Extension beschreibt nur die Sprachanteile, welche explizit für die Erweiterung bestimmt sind und in der Regel an anderer Stelle nicht mehr genutzt werden können.

In den entsprechenden CSS-Dateien (hier am Beispiel der core-CSS), stehen dann diese Blöcke, welche die Texte je Sprachvariante der GUI enthalten:

de-DE

/* ----- ----- ----- ----- ----- ----- ----- -----
   Package release {build-release}
   File UUID: 5c2a31c5-f9a0-4de1-9a78-5fd1e7eee56c
   ----- ----- ----- ----- ----- ----- ----- ----- */
[lang-core="de-DE"] [data-localized-core="label-name"]>.logo::after {
    content: "owug";
}

[lang-core="de-DE"] [data-localized-core="label-name"]>.separator::after {
    content: "|";
}

[lang-core="de-DE"] [data-localized-core="label-name"]>.version::before {
    content: "Version";
}

[lang-core="de-DE"] [data-localized-core="label-about"]::after {
    content: "Info";
}

/* ----- ----- ----- ----- ----- ----- ----- ----- */
[lang-core="de-DE"] [data-localized-core="text-status-forProcessing"]::after {
    content: "Bitte warten! Die Verarbeitung kann einen Moment dauern.";
}

/* ----- ----- ----- ----- ----- ----- ----- ----- */
[lang-core="de-DE"] [data-localized-core="text-loading"]::after {
    content: "Daten werden ermittelt.";
}

[lang-core="de-DE"] [data-localized-core="text-noData"]::after {
    content: "Keine Daten vorhanden!";
}

/* ----- ----- ----- ----- ----- ----- ----- ----- */
[lang-core="de-DE"] [data-localized-core="label-tabAbout"]::after {
    content: "Info";
}

[lang-core="de-DE"] [data-localized-core="label-tabContact"]::after {
    content: "Kontakt";
}

[lang-core="de-DE"] [data-localized-core="label-tabLibraries"]::after {
    content: "Lizenzen";
}

/* ----- ----- ----- ----- ----- ----- ----- ----- */
[lang-core="de-DE"] [data-localized-core="tooltip-showInTree"]::after {
    content: "Im Baum anzeigen";
}

[lang-core="de-DE"] [data-localized-core="tooltip-closeDialog"]::after {
    content: "Schließen";
}

[lang-core="de-DE"] [data-localized-core="tooltip-openAbout"]::after {
    content: "Info";
}

[lang-core="de-DE"] [data-localized-core="tooltip-openSettings"]::after {
    content: "Einstellungen";
}

[lang-core="de-DE"] [data-localized-core="tooltip-reloadAll"]::after {
    content: "Neu laden";
}

[lang-core="de-DE"] [data-localized-core="tooltip-resetSelection"]::after {
    content: "Auswahl zurücksetzen";
}

[lang-core="de-DE"] [data-localized-core="tooltip-selectAll"]::after {
    content: "Alles auswählen";
}

[lang-core="de-DE"] [data-localized-core="tooltip-deleteSelection"]::after {
    content: "Auswahl löschen";
}

[lang-core="de-DE"] [data-localized-core="tooltip-repairSelection"]::after {
    content: "Auswahl reparieren";
}

[lang-core="de-DE"] [data-localized-core="tooltip-shareSelection"]::after {
    content: "Auswahl teilen";
}

[lang-core="de-DE"] [data-localized-core="tooltip-copySelection"]::after {
    content: "Auswahl kopieren";
}

/* ----- ----- ----- ----- ----- ----- ----- ----- */
[lang-core="de-DE"] [data-localized-core="label-forSubject"]::after {
    content: "Betreff";
}

[lang-core="de-DE"] [data-localized-core="label-forMessage"]::after {
    content: "Nachricht";
}

[lang-core="de-DE"] [data-localized-core="label-forFeatureNotAvailable"]::after {
    content: "Funktion zur Zeit nicht verfügbar";
}

/* ----- ----- ----- ----- ----- ----- ----- ----- */

und hier das passende Gegenstück in en-US:

/* ----- ----- ----- ----- ----- ----- ----- -----
   Package release {build-release}
   File UUID: 379b5ed1-3581-4fd9-b12d-d1f789d89357
   ----- ----- ----- ----- ----- ----- ----- ----- */
[lang-core="en-US"] [data-localized-core="label-name"]>.logo::after {
    content: "owug";
}

[lang-core="en-US"] [data-localized-core="label-name"]>.separator::after {
    content: "|";
}

[lang-core="en-US"] [data-localized-core="label-name"]>.version::before {
    content: "Version";
}

[lang-core="en-US"] [data-localized-core="label-about"]::after {
    content: "About";
}

/* ----- ----- ----- ----- ----- ----- ----- ----- */
[lang-core="en-US"] [data-localized-core="text-status-forProcessing"]::after {
    content: "Please wait! Processing can take a moment.";
}

/* ----- ----- ----- ----- ----- ----- ----- ----- */
[lang-core="en-US"] [data-localized-core="text-loading"]::after {
    content: "Data will be determined.";
}

[lang-core="en-US"] [data-localized-core="text-noData"]::after {
    content: "No data available!";
}

/* ----- ----- ----- ----- ----- ----- ----- ----- */
[lang-core="en-US"] [data-localized-core="label-tabAbout"]::after {
    content: "About";
}

[lang-core="en-US"] [data-localized-core="label-tabContact"]::after {
    content: "Contact";
}

[lang-core="en-US"] [data-localized-core="label-tabLibraries"]::after {
    content: "Licenses";
}

/* ----- ----- ----- ----- ----- ----- ----- ----- */
[lang-core="en-US"] [data-localized-core="tooltip-showInTree"]::after {
    content: "Display in tree";
}

[lang-core="en-US"] [data-localized-core="tooltip-closeDialog"]::after {
    content: "Close";
}

[lang-core="en-US"] [data-localized-core="tooltip-openAbout"]::after {
    content: "About";
}

[lang-core="en-US"] [data-localized-core="tooltip-openSettings"]::after {
    content: "Settings";
}

[lang-core="en-US"] [data-localized-core="tooltip-reloadAll"]::after {
    content: "Reload";
}

[lang-core="en-US"] [data-localized-core="tooltip-resetSelection"]::after {
    content: "Reset selection";
}

[lang-core="en-US"] [data-localized-core="tooltip-selectAll"]::after {
    content: "Select all";
}

[lang-core="en-US"] [data-localized-core="tooltip-deleteSelection"]::after {
    content: "Delete selection";
}

[lang-core="en-US"] [data-localized-core="tooltip-repairSelection"]::after {
    content: "Repair selection";
}

[lang-core="en-US"] [data-localized-core="tooltip-shareSelection"]::after {
    content: "Share selection";
}

[lang-core="en-US"] [data-localized-core="tooltip-copySelection"]::after {
    content: "Copy selection";
}

/* ----- ----- ----- ----- ----- ----- ----- ----- */
[lang-core="en-US"] [data-localized-core="label-forSubject"]::after {
    content: "Subject";
}

[lang-core="en-US"] [data-localized-core="label-forMessage"]::after {
    content: "Message";
}

[lang-core="en-US"] [data-localized-core="label-forFeatureNotAvailable"]::after {
    content: "Feature currently not available";
}

/* ----- ----- ----- ----- ----- ----- ----- ----- */

Wie man sehr gut erkennen kann, werden hier als Kennzeichnung generische IDs verwendet. Diese IDs werden dann in data-Attribute innerhalb im HTML gesetzt:

<div class="btn-group" role="group" aria-label="controlsDefault">
    <button type="button" class="btn btn-outline-secondary" data-toggle="tooltip" data-html="true" data-title="<span data-localized-core='tooltip-deleteSelection'></span>" id="deleteSelection">
        <i class="fas fa-trash"></i>
    </button>
    <button type="button" class="btn btn-outline-secondary" data-toggle="tooltip" data-html="true" data-title="<span data-localized-core='tooltip-selectAll'></span>" id="selectAll">
        <i class="far fa-check-square"></i>
    </button>
    <button type="button" class="btn btn-outline-secondary" data-toggle="tooltip" data-html="true" data-title="<span data-localized-core='tooltip-repairSelection'></span>" title="" id="repairSelection">
        <i class="fas fa-magic"></i>
    </button>
    <button type="button" class="btn btn-outline-secondary" data-toggle="tooltip" data-html="true" data-title="<span data-localized-core='tooltip-resetSelection'></span>" data-localized-core="button-resetSelection" id="resetSelection">
        <i class="fas fa-eraser"></i>
    </button>
    <button type="button" class="btn btn-outline-secondary" data-toggle="tooltip" data-html="true" data-title="<span data-localized-core='tooltip-reloadAll'></span>" id="reloadAll">
        <i class="fas fa-sync fa-spin text-danger"></i>
    </button>
</div>

Durch die Kombination der Attribute am Body-Tag und an anchfolgenden Tags in Verbindung mit den IDs. Ergibt sich eine eindeutige Zuordnung und den Rest erledigen die Pseudo-Klasse ::after und ::before innerhalb des Browsers.

Wenn z.B. im HTML eine CSS-Kaskade wie:

[lang-core="en-US"] [data-localized-core="label-forMessage"]

erzeugt wird. Dann wird an der Stelle, an der das data-localized-core Attribute steht, der Test "Message" eingefügt. Sobald man jedoch diese Kaskade abändert und die entsprechenden CSS-Dateien bereits geladen sind. Kann man just-in-time die Sprache umschalten:

[lang-core="de-DE"] [data-localized-core="label-forMessage"]

und es wird mit der selben ID, jedoch einem anderen Attribute am Body-Tag nun "Nachricht" angezeigt.

Mit dieser Technik lassen sich sehr einfach und ohne viel Mehraufwand die Erweiterungen mit allen verfügbaren GUI-Sprachen des Management Servers darstellen.
 

Entwicklungs-Modus

Damit man während der Entwicklung oder Übersetzung sieht, welche IDs im HTML gesetzt sind. Gibt es auch hier eine Unterstützung in Form einer speziellen, welche in der styles.core.css versteckt ist:

[_lang-core] [data-localized-core]::before,
[lang-core="debug"] [data-localized-core]::before {
    content: "[Core: "attr(data-localized-core) "]";
}

[_lang-extension] [data-localized-extension]::before,
[lang-extension="debug"] [data-localized-extension]::before {
    content: "[Extension: "attr(data-localized-extension) "]";
}

[_lang-embedded] [data-localized-embedded]::before,
[lang-embedded="debug"] [data-localized-embedded]::before {
    content: "[Embedded: "attr(data-localized-embedded) "]";
}

Dieser Block sorgt dafür, sobald man die Attribute mit einem vorangestellten Unterstrich versehen hat. dass man statt den sprachanhängigen Texten die IDs angezeigt bekommt:

Damit ist man dann sehr einfach in der Lage die Sprachdateien zu erstellen bzw. zu erkennen, ob auch alles Mehrsprachenfähig ist :)


Viel Spaß beim ausprobieren, der Source-Code liegt bereits im GitHub zur freien Verfügung.

Im nächsten Artikel beschäftigen wir uns mit SmartExtensions: Build-Process und Build-Pakete ;)


Über den Autor:
Thomas Pollinger

... ist Senior Site Reliability Engineer bei der Vodafone GmbH in Düsseldorf. Seit dem Jahr 2007 betreut er zusammen mit seinen Kollegen die OpenText- (vormals RedDot-) Plattform Web Site Management für die deutsche Konzernzentrale.

Er entwickelt Erweiterungen in Form von Plug-Ins und PowerShell Skripten. Seit den Anfängen in 2001 (RedDot CMS 4.0) kennt er sich speziell mit der Arbeitweise und den Funktionen des Management Server aus.