From 4e3c5c04e2cf405cdcdaf374dc315f29c52111f7 Mon Sep 17 00:00:00 2001
From: Manuel Kocher <manuel.kocher@tugraz.at>
Date: Thu, 2 Jun 2022 10:37:01 +0200
Subject: [PATCH] Add support of translation overrides to dbp-translation

---
 packages/app-shell/src/app-shell.js           |  2 +
 packages/common/dbp-common-demo.js            | 10 +++--
 packages/common/i18next.js                    | 45 +++++++++++++++++++
 packages/common/src/i18n.js                   |  5 ++-
 packages/common/src/i18n/de/translation.json  |  3 +-
 packages/common/src/i18n/en/translation.json  |  3 +-
 packages/common/src/translation.js            | 36 +++++++++++++--
 .../assets/dbp-toolkit-showcase.html.ejs      |  1 +
 .../assets/i18n/overrides/de/translation.json |  6 +++
 .../assets/i18n/overrides/en/translation.json |  6 +++
 10 files changed, 104 insertions(+), 13 deletions(-)
 create mode 100644 toolkit-showcase/assets/i18n/overrides/de/translation.json
 create mode 100644 toolkit-showcase/assets/i18n/overrides/en/translation.json

diff --git a/packages/app-shell/src/app-shell.js b/packages/app-shell/src/app-shell.js
index 65892521..15eb1cc0 100644
--- a/packages/app-shell/src/app-shell.js
+++ b/packages/app-shell/src/app-shell.js
@@ -75,6 +75,7 @@ export class AppShell extends ScopedElementsMixin(DBPLitElement) {
 
         this.auth = {};
         this.langFiles = '';
+        this.overrideFiles = '';
     }
 
     static get scopedElements() {
@@ -273,6 +274,7 @@ export class AppShell extends ScopedElementsMixin(DBPLitElement) {
             env: {type: String},
             auth: {type: Object},
             langFiles: {type: String, attribute: 'lang-files'},
+            overrideFiles: {type: String, attribute: 'override-files'},
         };
     }
 
diff --git a/packages/common/dbp-common-demo.js b/packages/common/dbp-common-demo.js
index ef8c2622..7893841f 100644
--- a/packages/common/dbp-common-demo.js
+++ b/packages/common/dbp-common-demo.js
@@ -15,13 +15,14 @@ import {
     Translation,
 } from './index.js';
 
+
+
 export class DbpCommonDemo extends ScopedElementsMixin(LitElement) {
     constructor() {
         super();
         this._i18n = createInstance();
         this.lang = this._i18n.language;
         this.noAuth = false;
-        this.langFiles = '';
     }
 
     static get scopedElements() {
@@ -47,7 +48,6 @@ export class DbpCommonDemo extends ScopedElementsMixin(LitElement) {
         return {
             lang: {type: String},
             noAuth: {type: Boolean, attribute: 'no-auth'},
-            langFiles: {type: String, attribute: 'lang-files'},
         };
     }
 
@@ -302,8 +302,10 @@ html {
                         </dbp-translated>
                     </div>
                     <div class="control" id="dbp-translation-demo">
-                        <dbp-translation key="toolkit-showcase" subscribe="lang, lang-files"></dbp-translation>
-                        <dbp-translation key="toolkit-showcase-link" var='{"link1": "https://www.i18next.com/translation-function/interpolation"}' subscribe="lang, lang-files" unsafe></dbp-translation>
+                        <p><dbp-translation key="toolkit-showcase" subscribe="lang, lang-files"></dbp-translation></p>
+                        <p><dbp-translation key="toolkit-showcase" subscribe="lang, lang-files, override-files"></dbp-translation></p>
+                        <p><dbp-translation key="toolkit-showcase-link" var='{"link1": "https://www.i18next.com/translation-function/interpolation"}' subscribe="lang, lang-files" unsafe></dbp-translation></p>
+                        <p><dbp-translation key="toolkit-showcase-link" var='{"link1": "https://dbp-demo.tugraz.at/"}' subscribe="lang, lang-files, override-files" unsafe></dbp-translation></p>
                     </div>
                 </div>
             </section>
diff --git a/packages/common/i18next.js b/packages/common/i18next.js
index 2b9244b3..54550215 100644
--- a/packages/common/i18next.js
+++ b/packages/common/i18next.js
@@ -124,3 +124,48 @@ export function setOverrides(i18n, element, overrides) {
     }
     i18n.setDefaultNamespace(hasOverrides ? overrideNamespace : namespace);
 }
+
+async function fetchOverridesByLanguage(overrides, lng) {
+  let result = await
+      fetch(overrides + lng +'/translation.json', {
+          headers: {'Content-Type': 'application/json'},
+      });
+  let json = await result.json();
+  return json;
+}
+
+/**
+ * Sets translation overrides for the given i18next instance. Any previously
+ * applied overrides will be removed first. So calling this with an empty overrides
+ * object is equal to removing all overrides.
+ *
+ * @param {i18next.i18n} i18n - The i18next instance
+ * @param {HTMLElement} element - The element at which the overrides are targeted
+ * @param {String} overridesFile - Path to the translation file containing the overrides
+ */
+export async function setOverridesByFile(i18n, element, overridesFile) {
+    // We add a special namespace which gets used with priority and falls back
+    // to the original one. This way we an change the overrides at runtime
+    // and can even remove them.
+
+    // The scoped mixin saves the real tag name under data-tag-name
+    let tagName = ((element.dataset && element.dataset.tagName) || element.tagName).toLowerCase();
+    let namespace = i18n.options.fallbackNS;
+    let overrideNamespace = getOverrideNamespace(namespace);
+    let hasOverrides = false;
+    for (let lng of i18n.languages) {
+        // get translation.json for each lang
+        let response = await fetchOverridesByLanguage(overridesFile, lng);
+        // remove old language
+        i18n.removeResourceBundle(lng, overrideNamespace);
+        // if no new translation is available, skip
+        if (response === undefined || response[tagName] === undefined) return;
+        // get new translation
+        let resources = response[tagName];
+        hasOverrides = true;
+        // set new translation
+        i18n.addResourceBundle(lng, overrideNamespace, resources);
+        i18n.setDefaultNamespace(hasOverrides ? overrideNamespace : namespace);
+    }
+    return i18n;
+}
diff --git a/packages/common/src/i18n.js b/packages/common/src/i18n.js
index d9e9630a..ae6a0c68 100644
--- a/packages/common/src/i18n.js
+++ b/packages/common/src/i18n.js
@@ -1,4 +1,4 @@
-import {createInstance as _createInstance} from '../i18next.js';
+import {createInstance as _createInstance, setOverridesByFile} from '../i18next.js';
 
 import de from './i18n/de/translation.json';
 import en from './i18n/en/translation.json';
@@ -12,6 +12,7 @@ export async function createInstanceAsync(langFile, namespace) {
       namespace = 'translation'
     // check if a path to language files is given
     if(langFile) {
+
       // request german lang file asynchronously
       let result = await
           fetch(langFile + 'de/' + namespace +'.json', {
@@ -31,3 +32,5 @@ export async function createInstanceAsync(langFile, namespace) {
 
     return _createInstance({en: en, de: de}, 'de', 'en', namespace);
 }
+
+export {setOverridesByFile};
diff --git a/packages/common/src/i18n/de/translation.json b/packages/common/src/i18n/de/translation.json
index b9daed00..51cf793c 100644
--- a/packages/common/src/i18n/de/translation.json
+++ b/packages/common/src/i18n/de/translation.json
@@ -7,6 +7,5 @@
         "api-documentation-server": "Verbindung zum apiDocumentation API Server {{apiDocUrl}} fehlgeschlagen!",
         "error-api-server": "Verbindung zum API Server {{apiUrl}} fehlgeschlagen!",
         "error-hydra-documentation-url-not-set": "Hydra apiDocumentation URL wurden für server {{apiUrl}} nicht gesetzt!"
-    },
-    "toolkit-showcase": "Dieser Text wird mithilfe von i18n Englisch wenn man die Sprache auf Englisch stellt."
+    }
 }
diff --git a/packages/common/src/i18n/en/translation.json b/packages/common/src/i18n/en/translation.json
index 1e17839a..e1036a65 100644
--- a/packages/common/src/i18n/en/translation.json
+++ b/packages/common/src/i18n/en/translation.json
@@ -7,6 +7,5 @@
         "api-documentation-server": "Connection to apiDocumentation server {{apiDocUrl}} failed!",
         "error-api-server": "Connection to api server {{apiUrl}} failed!",
         "error-hydra-documentation-url-not-set": "Hydra apiDocumentation url was not set for server {{apiUrl}}!"
-    },
-    "toolkit-showcase": "This text will be translated to german using i18n when the user changes the language to german."
+    }
 }
diff --git a/packages/common/src/translation.js b/packages/common/src/translation.js
index 874bcb0a..fd965c2e 100644
--- a/packages/common/src/translation.js
+++ b/packages/common/src/translation.js
@@ -2,7 +2,20 @@ import {css, html} from 'lit';
 import {until} from 'lit/directives/until.js';
 import {unsafeHTML} from 'lit/directives/unsafe-html.js';
 import DBPLitElement from '../dbp-lit-element';
-import {createInstanceAsync} from './i18n.js';
+import {createInstanceAsync, setOverridesByFile} from './i18n.js';
+
+let OVERRIDE = {
+  'de': {
+    'dbp-translation': {
+      'toolkit-showcase': 'testText'
+    }
+  },
+  'en': {
+    'dbp-translation': {
+      'toolkit-showcase': 'testText2'
+    }
+  }
+}
 
 export class Translation extends DBPLitElement {
     constructor() {
@@ -12,6 +25,7 @@ export class Translation extends DBPLitElement {
         this.langFiles = '';
         this.interpolation = '';
         this.namespace = '';
+        this.overrideFiles = '';
     }
 
     static get properties() {
@@ -23,6 +37,7 @@ export class Translation extends DBPLitElement {
             interpolation: {type: Object, attribute: 'var'},
             unsafe: {type: Boolean, attribute: 'unsafe'},
             namespace: {type: String, attribute: 'ns'},
+            overrideFiles: {type: String, attribute: 'override-files'},
         };
     }
 
@@ -37,11 +52,24 @@ export class Translation extends DBPLitElement {
 
     connectedCallback() {
       super.connectedCallback();
-      if (this.namespace == '')
+      if (this.namespace == '') {
         this._i18n = createInstanceAsync(this.langFiles);
+      }
       else {
         this._i18n = createInstanceAsync(this.langFiles, this.namespace);
       }
+
+      let local = this;
+      let overrideFiles = this.overrideFiles;
+
+      if (this.overrideFiles) {
+        this._i18n.then(function(response) {
+          setOverridesByFile(response, local, overrideFiles).then(function(response) {
+              local._i18n = response;
+              local.requestUpdate();
+          });
+        })
+      }
     }
 
     update(changedProperties) {
@@ -49,7 +77,7 @@ export class Translation extends DBPLitElement {
         changedProperties.forEach((oldValue, propName) => {
             switch (propName) {
                 case 'lang':
-                    this._i18n.then(function(response) {
+                    Promise.resolve(this._i18n).then(function(response) {
                       response.changeLanguage(lang);
                     });
                     break;
@@ -66,7 +94,7 @@ export class Translation extends DBPLitElement {
         let unsafe = this.unsafe;
 
         // async request to i18n translation
-        const translation = this._i18n.then(function(response){
+        const translation = Promise.resolve(this._i18n).then(function(response){
           if (interpolation && unsafe)
             return unsafeHTML(response.t(key, interpolation));
           else if (interpolation)
diff --git a/toolkit-showcase/assets/dbp-toolkit-showcase.html.ejs b/toolkit-showcase/assets/dbp-toolkit-showcase.html.ejs
index feaee44c..9d45cbe4 100644
--- a/toolkit-showcase/assets/dbp-toolkit-showcase.html.ejs
+++ b/toolkit-showcase/assets/dbp-toolkit-showcase.html.ejs
@@ -114,6 +114,7 @@
     provider-root
     lang="de"
     lang-files="<%= getPrivateUrl('i18n/') %>"
+    override-files="<%= getPrivateUrl('i18n/overrides/') %>"
     entry-point-url="<%= entryPointURL %>"
     nextcloud-auth-url="<%= nextcloudWebAppPasswordURL %>"
     nextcloud-web-dav-url="<%= nextcloudWebDavURL %>"
diff --git a/toolkit-showcase/assets/i18n/overrides/de/translation.json b/toolkit-showcase/assets/i18n/overrides/de/translation.json
new file mode 100644
index 00000000..b3b136fb
--- /dev/null
+++ b/toolkit-showcase/assets/i18n/overrides/de/translation.json
@@ -0,0 +1,6 @@
+{
+    "dbp-translation": {
+      "toolkit-showcase": "Ãœberschriebener i18n toolkit-showcase Text",
+      "toolkit-showcase-link": "Ãœberschriebener i18n toolkit-showcase-link Text mit <a href=\"{{- link1}}\">TestLink</a>"
+    }
+}
diff --git a/toolkit-showcase/assets/i18n/overrides/en/translation.json b/toolkit-showcase/assets/i18n/overrides/en/translation.json
new file mode 100644
index 00000000..3892525b
--- /dev/null
+++ b/toolkit-showcase/assets/i18n/overrides/en/translation.json
@@ -0,0 +1,6 @@
+{
+    "dbp-translation": {
+      "toolkit-showcase": "Overriden i18n toolkit-showcase text",
+      "toolkit-showcase-link": "Overriden i18n toolkit-showcase-link text with a <a href=\"{{- link1}}\">test link</a>"
+    }
+}
-- 
GitLab