diff --git a/README.md b/README.md index 2ea0ba3a980e9c416e284576522d6eb8b8400eb4..52c91eafcbfde87468e92a8304fea499bb2c933f 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ the version number in its `package.json` is higher than the version number on np | `unsubscribe` | Reserved for future use | | `auth` | Authentication information, set by the authentication component | | `lang` | Currently selected language, set by the language selector | -| `lang-file` | Location of the i18n language file where all required i18n translations are | +| `lang-dir` | Location of the i18n language file where all required i18n translations are | | `entry-point-url` | Entry point url for all api requests | | `requested-login-status` | Used by the login buttons to trigger a login in auth components | | `initial-file-handling-state` | Used by the file-handling component to sync file source/sink at first time open | diff --git a/packages/app-shell/src/app-shell.js b/packages/app-shell/src/app-shell.js index 53397c4e4edd3ae6a8c8d12aaa31d53ecf9276f0..0c7d2a8805f13dafeeabff56f604b1d3a618fcb9 100644 --- a/packages/app-shell/src/app-shell.js +++ b/packages/app-shell/src/app-shell.js @@ -74,7 +74,7 @@ export class AppShell extends ScopedElementsMixin(DBPLitElement) { this.initateOpenMenu = false; this.auth = {}; - this.langFile = ''; + this.langDir = ''; } static get scopedElements() { @@ -272,7 +272,7 @@ export class AppShell extends ScopedElementsMixin(DBPLitElement) { buildTime: {type: String, attribute: 'build-time'}, env: {type: String}, auth: {type: Object}, - langFile: {type: String, attribute: 'lang-file'}, + langDir: {type: String, attribute: 'lang-dir'}, }; } diff --git a/packages/common/dbp-common-demo.js b/packages/common/dbp-common-demo.js index 5f5b9bc0f94bd8b218ca979e82d531b869e46193..f9e6ac69f43615471c7a23b7cafc8dd91ab12d83 100644 --- a/packages/common/dbp-common-demo.js +++ b/packages/common/dbp-common-demo.js @@ -15,13 +15,15 @@ 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.langFile = ''; + this.langDir = ''; } static get scopedElements() { @@ -47,7 +49,7 @@ export class DbpCommonDemo extends ScopedElementsMixin(LitElement) { return { lang: {type: String}, noAuth: {type: Boolean, attribute: 'no-auth'}, - langFile: {type: String, attribute: 'lang-file'}, + langDir: {type: String, attribute: 'lang-dir'}, }; } @@ -302,7 +304,9 @@ html { </dbp-translated> </div> <div class="control" id="dbp-translation-demo"> - <dbp-translation key="toolkit-showcase" subscribe="lang, lang-file"></dbp-translation> + <dbp-translation key="toolkit-showcase" subscribe="lang, lang-dir"></dbp-translation> + <dbp-translation key="toolkit-showcase-link" var='{"link1": "https://www.i18next.com/translation-function/interpolation"}' subscribe="lang, lang-dir" unsafe></dbp-translation> + <dbp-translation key="abc" subscribe="lang, lang-dir"></dbp-translation> </div> </div> </section> diff --git a/packages/common/i18next.js b/packages/common/i18next.js index 2b9244b3608c23c74e0123a7b4076c3363346981..d72b9abcc272045f9d50fff1928d33f6eb8358fa 100644 --- a/packages/common/i18next.js +++ b/packages/common/i18next.js @@ -124,3 +124,35 @@ export function setOverrides(i18n, element, overrides) { } i18n.setDefaultNamespace(hasOverrides ? overrideNamespace : namespace); } + +/** + * 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. + * Expects overrides as promise and requests update after overrides have been set. + * + * @param {i18next.i18n} i18n - The i18next instance + * @param {HTMLElement} element - The element at which the overrides are targeted + * @param {object} overrides - The override data as promise + */ +export async function setOverridesByPromise(i18n, element, overrides) { + // 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) { + overrides[lng] = await overrides[lng]; + i18n.removeResourceBundle(lng, overrideNamespace); + if (overrides[lng] === undefined || overrides[lng][tagName] === undefined) continue; + let resources = overrides[lng][tagName]; + hasOverrides = true; + i18n.addResourceBundle(lng, overrideNamespace, resources); + } + i18n.setDefaultNamespace(hasOverrides ? overrideNamespace : namespace); + element.requestUpdate(); +} diff --git a/packages/common/src/i18n.js b/packages/common/src/i18n.js index 0374a825419d2f2dea5fd68c5b9b0f76c019b9bb..1db56149c8772428260cc630cbcd4559b37d2eba 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, setOverridesByPromise} from '../i18next.js'; import de from './i18n/de/translation.json'; import en from './i18n/en/translation.json'; @@ -7,25 +7,8 @@ export function createInstance() { return _createInstance({en: en, de: de}, 'de', 'en'); } -export async function createInstanceAsync(langFile) { - // check if a path to language files is given - if(langFile) { - // request german lang file asynchronously - let result = await - fetch(langFile + 'de/translation.json', { - headers: {'Content-Type': 'application/json'}, - }); - const dynDe = await result.json(); - - // request english lang file asynchronously - result = await - fetch(langFile + 'en/translation.json', { - headers: {'Content-Type': 'application/json'}, - }); - const dynEn = await result.json(); - - return _createInstance({en: dynEn, de: dynDe}, 'de', 'en'); - } - +export function createInstanceGivenResources(en, de) { return _createInstance({en: en, de: de}, 'de', 'en'); } + +export {setOverridesByPromise}; diff --git a/packages/common/src/i18n/de/translation.json b/packages/common/src/i18n/de/translation.json index b9daed00bc4e840bf1e885e4cff6f5a5e9c4d229..51cf793c6cb4108b18d8cab47b63959663ec8590 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 1e17839ab74d17ebb29bd8bf73c721a13533280a..e1036a6580fd180af131a4c2744c8c4fef2745fb 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 034978f3163b3fe1d63718f81b1219fcd7ef26be..07bb756e091f3df29ceb010ade2caf7805de8ad5 100644 --- a/packages/common/src/translation.js +++ b/packages/common/src/translation.js @@ -1,14 +1,42 @@ 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 {createInstanceGivenResources, setOverridesByPromise} from './i18n.js'; + +// global variable as cache for translations +const translationCache = {}; + +// fetches overrides for given language +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; +} + +// handles translation cache promises +async function cacheOverrides(overridesFile, lng) { + // use global var as cache + if (translationCache[lng] === undefined) { + // get translation.json for each lang + let response = fetchOverridesByLanguage(overridesFile, lng); + translationCache[lng] = response; + return response; + } else { + return translationCache[lng]; + } +} export class Translation extends DBPLitElement { constructor() { super(); this.key = ''; this.lang = ''; - this.langFile = ''; + this.interpolation = ''; + this.langDir = ''; + this.unsafe = false; } static get properties() { @@ -16,7 +44,9 @@ export class Translation extends DBPLitElement { ...super.properties, key: {type: String}, lang: {type: String}, - langFile: {type: String, attribute: 'lang-file'}, + interpolation: {type: Object, attribute: 'var'}, + unsafe: {type: Boolean, attribute: 'unsafe'}, + langDir: {type: String, attribute: 'lang-dir'}, }; } @@ -31,7 +61,21 @@ export class Translation extends DBPLitElement { connectedCallback() { super.connectedCallback(); - this._i18n = createInstanceAsync(this.langFile); + // init objects with empty string as value for key + const de = {}; + const en = {}; + de[this.key] = ""; + en[this.key] = ""; + + // create i18n instance with given translations + this._i18n = createInstanceGivenResources(en, de); + + if (this.langDir) { + for(let lng of this._i18n.languages) { + cacheOverrides(this.langDir, lng); + setOverridesByPromise(this._i18n, this, translationCache); + } + } } update(changedProperties) { @@ -39,10 +83,7 @@ export class Translation extends DBPLitElement { changedProperties.forEach((oldValue, propName) => { switch (propName) { case 'lang': - - this._i18n.then(function(response) { - response.changeLanguage(lang); - }); + this._i18n.changeLanguage(lang); break; } }); @@ -51,17 +92,28 @@ export class Translation extends DBPLitElement { } render() { - // save global key in local variable for async use - let key = this.key; + // request to i18n translation + const translation = (() => { + if (this.interpolation && this.unsafe) + return unsafeHTML(this._i18n.t(this.key, this.interpolation)); + else if (this.interpolation) + return this._i18n.t(this.key, this.interpolation); + else + return this._i18n.t(this.key); + })(); - // async request to i18n translation - const translation = this._i18n.then(function(response){ - return response.t(key); - }); + // if translation == "" key was not found + let key = ""; + if (translation != "") { + key = unsafeHTML("<!-- key: " + this.key + "-->"); + } else { + key = unsafeHTML("<!-- key \"" + this.key + "\" not found! -->"); + } - // load translation text when available, otherweise display "Loading.." + // load translation text return html` - ${until(translation, html`<span>Loading..</span>`)} + ${key} + ${translation} `; } } diff --git a/toolkit-showcase/assets/common.metadata.json b/toolkit-showcase/assets/common.metadata.json index 51e4892cb53180deb82a6224e9dd0c836c9f6452..8cad700454234b68cd4b1e0c803d4be0d77daa39 100644 --- a/toolkit-showcase/assets/common.metadata.json +++ b/toolkit-showcase/assets/common.metadata.json @@ -14,5 +14,5 @@ "de": "Gemeinsame Web Components", "en": "Common web components" }, - "subscribe": "lang,entry-point-url,lang-file" + "subscribe": "lang,entry-point-url,lang-dir" } diff --git a/toolkit-showcase/assets/dbp-toolkit-showcase.html.ejs b/toolkit-showcase/assets/dbp-toolkit-showcase.html.ejs index 89c2a191d9e98c2d5aa3b01bde70137b38f8b17c..07b213117747aed86247d1a50c87cd681e226cf3 100644 --- a/toolkit-showcase/assets/dbp-toolkit-showcase.html.ejs +++ b/toolkit-showcase/assets/dbp-toolkit-showcase.html.ejs @@ -113,7 +113,7 @@ <<%= name %> provider-root lang="de" - lang-file="<%= getPrivateUrl('i18n/') %>" + lang-dir="<%= getPrivateUrl('translation-overrides/') %>" entry-point-url="<%= entryPointURL %>" nextcloud-auth-url="<%= nextcloudWebAppPasswordURL %>" nextcloud-web-dav-url="<%= nextcloudWebDavURL %>" diff --git a/toolkit-showcase/assets/translation-overrides/de/translation.json b/toolkit-showcase/assets/translation-overrides/de/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..27d226ddd06de5cf6471160a27d98a31f25a8f79 --- /dev/null +++ b/toolkit-showcase/assets/translation-overrides/de/translation.json @@ -0,0 +1,6 @@ +{ + "dbp-translation": { + "toolkit-showcase": "Dieser Text wird mithilfe von i18n aus einer benutzerdefinierten Sprachdatei gelesen und ins Englische übersetzt wenn man die Sprache auf Englisch stellt.", + "toolkit-showcase-link": "Es können sogar links mittels <a href=\"{{- link1}}\">interpolation</a> und escaping dargestellt werden." + } +} diff --git a/toolkit-showcase/assets/translation-overrides/en/translation.json b/toolkit-showcase/assets/translation-overrides/en/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..e3cd5c3858abaac1396241f2235a71fdb2f17d86 --- /dev/null +++ b/toolkit-showcase/assets/translation-overrides/en/translation.json @@ -0,0 +1,6 @@ +{ + "dbp-translation": { + "toolkit-showcase": "This text will be translated to german using i18n with a user defined language file when the language is changed to german.", + "toolkit-showcase-link": "Furthermore its possible to display links through <a href=\"{{- link1}}\">interpolation</a> and escaping." + } +} diff --git a/toolkit-showcase/rollup.config.js b/toolkit-showcase/rollup.config.js index 1a85c45ac58362d43c3a8968ce7c6152323f7211..5320e77ed687b0b881ccfab0f897f88527849982 100644 --- a/toolkit-showcase/rollup.config.js +++ b/toolkit-showcase/rollup.config.js @@ -168,7 +168,7 @@ Dependencies: {src: 'assets/icon-*.png', dest: 'dist/' + (await getDistPath(pkg.name))}, {src: 'assets/apple-*.png', dest: 'dist/' + (await getDistPath(pkg.name))}, {src: 'assets/safari-*.svg', dest: 'dist/' + (await getDistPath(pkg.name))}, - {src: 'src/i18n', dest: 'dist/' + (await getDistPath(pkg.name))}, + {src: 'assets/translation-overrides', dest: 'dist/' + (await getDistPath(pkg.name))}, { src: 'assets/manifest.json', dest: 'dist', diff --git a/toolkit-showcase/src/dbp-common-demo-activity.js b/toolkit-showcase/src/dbp-common-demo-activity.js index 6735845541387b0098d6f61bf329470508bf84c3..2f959f9a7ee9886da423cdd9b444ea17ab58a477 100644 --- a/toolkit-showcase/src/dbp-common-demo-activity.js +++ b/toolkit-showcase/src/dbp-common-demo-activity.js @@ -13,7 +13,7 @@ class DbpCommonDemoActivity extends ScopedElementsMixin(AdapterLitElement) { super(); this.lang = 'en'; this.entryPointUrl = ''; - this.langFile = ''; + this.langDir = ''; } static get scopedElements() { @@ -26,7 +26,7 @@ class DbpCommonDemoActivity extends ScopedElementsMixin(AdapterLitElement) { return { ...super.properties, lang: {type: String}, - langFile: {type: String, attribute: 'lang-file'}, + langDir: {type: String, attribute: 'lang-dir'}, entryPointUrl: {type: String, attribute: 'entry-point-url'}, }; } @@ -65,7 +65,7 @@ class DbpCommonDemoActivity extends ScopedElementsMixin(AdapterLitElement) { <dbp-common-demo id="demo" lang="${this.lang}" - lang-file="${this.langFile}" + lang-dir="${this.langDir}" entry-point-url="${this.entryPointUrl}"></dbp-common-demo> `; } diff --git a/toolkit-showcase/src/i18n/de/translation.json b/toolkit-showcase/src/i18n/de/translation.json deleted file mode 100644 index d51b20bd684577823c83c44264231d626cbcfdbf..0000000000000000000000000000000000000000 --- a/toolkit-showcase/src/i18n/de/translation.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "toolkit-showcase": "Dieser Text wird mithilfe von i18n aus einer benutzerdefinierten Sprachdatei gelesen und ins Englische übersetzt wenn man die Sprache auf Englisch stellt." -} diff --git a/toolkit-showcase/src/i18n/en/translation.json b/toolkit-showcase/src/i18n/en/translation.json deleted file mode 100644 index a2a415677f590d5467181ffe68644925ca542ee6..0000000000000000000000000000000000000000 --- a/toolkit-showcase/src/i18n/en/translation.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "toolkit-showcase": "This text will be translated to german using i18n with a user defined language file when the language is changed to german." -}