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