Skip to content
Snippets Groups Projects
Commit 0fa4ce98 authored by Kocher, Manuel's avatar Kocher, Manuel
Browse files

Merge branch 'dbp-translation-component' into master

Add interpolation support and overrides to translation component
Rename lang-file attribute to lang-dir
Add caching for efficiency
parents 4ae016e7 1deedae8
No related branches found
No related tags found
No related merge requests found
Pipeline #182304 failed
Showing
with 135 additions and 60 deletions
...@@ -35,7 +35,7 @@ the version number in its `package.json` is higher than the version number on np ...@@ -35,7 +35,7 @@ the version number in its `package.json` is higher than the version number on np
| `unsubscribe` | Reserved for future use | | `unsubscribe` | Reserved for future use |
| `auth` | Authentication information, set by the authentication component | | `auth` | Authentication information, set by the authentication component |
| `lang` | Currently selected language, set by the language selector | | `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 | | `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 | | `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 | | `initial-file-handling-state` | Used by the file-handling component to sync file source/sink at first time open |
......
...@@ -74,7 +74,7 @@ export class AppShell extends ScopedElementsMixin(DBPLitElement) { ...@@ -74,7 +74,7 @@ export class AppShell extends ScopedElementsMixin(DBPLitElement) {
this.initateOpenMenu = false; this.initateOpenMenu = false;
this.auth = {}; this.auth = {};
this.langFile = ''; this.langDir = '';
} }
static get scopedElements() { static get scopedElements() {
...@@ -272,7 +272,7 @@ export class AppShell extends ScopedElementsMixin(DBPLitElement) { ...@@ -272,7 +272,7 @@ export class AppShell extends ScopedElementsMixin(DBPLitElement) {
buildTime: {type: String, attribute: 'build-time'}, buildTime: {type: String, attribute: 'build-time'},
env: {type: String}, env: {type: String},
auth: {type: Object}, auth: {type: Object},
langFile: {type: String, attribute: 'lang-file'}, langDir: {type: String, attribute: 'lang-dir'},
}; };
} }
......
...@@ -15,13 +15,15 @@ import { ...@@ -15,13 +15,15 @@ import {
Translation, Translation,
} from './index.js'; } from './index.js';
export class DbpCommonDemo extends ScopedElementsMixin(LitElement) { export class DbpCommonDemo extends ScopedElementsMixin(LitElement) {
constructor() { constructor() {
super(); super();
this._i18n = createInstance(); this._i18n = createInstance();
this.lang = this._i18n.language; this.lang = this._i18n.language;
this.noAuth = false; this.noAuth = false;
this.langFile = ''; this.langDir = '';
} }
static get scopedElements() { static get scopedElements() {
...@@ -47,7 +49,7 @@ export class DbpCommonDemo extends ScopedElementsMixin(LitElement) { ...@@ -47,7 +49,7 @@ export class DbpCommonDemo extends ScopedElementsMixin(LitElement) {
return { return {
lang: {type: String}, lang: {type: String},
noAuth: {type: Boolean, attribute: 'no-auth'}, noAuth: {type: Boolean, attribute: 'no-auth'},
langFile: {type: String, attribute: 'lang-file'}, langDir: {type: String, attribute: 'lang-dir'},
}; };
} }
...@@ -302,7 +304,9 @@ html { ...@@ -302,7 +304,9 @@ html {
</dbp-translated> </dbp-translated>
</div> </div>
<div class="control" id="dbp-translation-demo"> <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>
</div> </div>
</section> </section>
......
...@@ -124,3 +124,35 @@ export function setOverrides(i18n, element, overrides) { ...@@ -124,3 +124,35 @@ export function setOverrides(i18n, element, overrides) {
} }
i18n.setDefaultNamespace(hasOverrides ? overrideNamespace : namespace); 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();
}
import {createInstance as _createInstance} from '../i18next.js'; import {createInstance as _createInstance, setOverridesByPromise} from '../i18next.js';
import de from './i18n/de/translation.json'; import de from './i18n/de/translation.json';
import en from './i18n/en/translation.json'; import en from './i18n/en/translation.json';
...@@ -7,25 +7,8 @@ export function createInstance() { ...@@ -7,25 +7,8 @@ export function createInstance() {
return _createInstance({en: en, de: de}, 'de', 'en'); return _createInstance({en: en, de: de}, 'de', 'en');
} }
export async function createInstanceAsync(langFile) { export function createInstanceGivenResources(en, de) {
// 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');
}
return _createInstance({en: en, de: de}, 'de', 'en'); return _createInstance({en: en, de: de}, 'de', 'en');
} }
export {setOverridesByPromise};
...@@ -7,6 +7,5 @@ ...@@ -7,6 +7,5 @@
"api-documentation-server": "Verbindung zum apiDocumentation API Server {{apiDocUrl}} fehlgeschlagen!", "api-documentation-server": "Verbindung zum apiDocumentation API Server {{apiDocUrl}} fehlgeschlagen!",
"error-api-server": "Verbindung zum API Server {{apiUrl}} 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!" "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."
} }
...@@ -7,6 +7,5 @@ ...@@ -7,6 +7,5 @@
"api-documentation-server": "Connection to apiDocumentation server {{apiDocUrl}} failed!", "api-documentation-server": "Connection to apiDocumentation server {{apiDocUrl}} failed!",
"error-api-server": "Connection to api server {{apiUrl}} 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}}!" "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."
} }
import {css, html} from 'lit'; 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 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 { export class Translation extends DBPLitElement {
constructor() { constructor() {
super(); super();
this.key = ''; this.key = '';
this.lang = ''; this.lang = '';
this.langFile = ''; this.interpolation = '';
this.langDir = '';
this.unsafe = false;
} }
static get properties() { static get properties() {
...@@ -16,7 +44,9 @@ export class Translation extends DBPLitElement { ...@@ -16,7 +44,9 @@ export class Translation extends DBPLitElement {
...super.properties, ...super.properties,
key: {type: String}, key: {type: String},
lang: {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 { ...@@ -31,7 +61,21 @@ export class Translation extends DBPLitElement {
connectedCallback() { connectedCallback() {
super.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) { update(changedProperties) {
...@@ -39,10 +83,7 @@ export class Translation extends DBPLitElement { ...@@ -39,10 +83,7 @@ export class Translation extends DBPLitElement {
changedProperties.forEach((oldValue, propName) => { changedProperties.forEach((oldValue, propName) => {
switch (propName) { switch (propName) {
case 'lang': case 'lang':
this._i18n.changeLanguage(lang);
this._i18n.then(function(response) {
response.changeLanguage(lang);
});
break; break;
} }
}); });
...@@ -51,17 +92,28 @@ export class Translation extends DBPLitElement { ...@@ -51,17 +92,28 @@ export class Translation extends DBPLitElement {
} }
render() { render() {
// save global key in local variable for async use // request to i18n translation
let key = this.key; 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 // if translation == "" key was not found
const translation = this._i18n.then(function(response){ let key = "";
return response.t(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` return html`
${until(translation, html`<span>Loading..</span>`)} ${key}
${translation}
`; `;
} }
} }
...@@ -14,5 +14,5 @@ ...@@ -14,5 +14,5 @@
"de": "Gemeinsame Web Components", "de": "Gemeinsame Web Components",
"en": "Common web components" "en": "Common web components"
}, },
"subscribe": "lang,entry-point-url,lang-file" "subscribe": "lang,entry-point-url,lang-dir"
} }
...@@ -113,7 +113,7 @@ ...@@ -113,7 +113,7 @@
<<%= name %> <<%= name %>
provider-root provider-root
lang="de" lang="de"
lang-file="<%= getPrivateUrl('i18n/') %>" lang-dir="<%= getPrivateUrl('translation-overrides/') %>"
entry-point-url="<%= entryPointURL %>" entry-point-url="<%= entryPointURL %>"
nextcloud-auth-url="<%= nextcloudWebAppPasswordURL %>" nextcloud-auth-url="<%= nextcloudWebAppPasswordURL %>"
nextcloud-web-dav-url="<%= nextcloudWebDavURL %>" nextcloud-web-dav-url="<%= nextcloudWebDavURL %>"
......
{
"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."
}
}
{
"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."
}
}
...@@ -168,7 +168,7 @@ Dependencies: ...@@ -168,7 +168,7 @@ Dependencies:
{src: 'assets/icon-*.png', dest: 'dist/' + (await getDistPath(pkg.name))}, {src: 'assets/icon-*.png', dest: 'dist/' + (await getDistPath(pkg.name))},
{src: 'assets/apple-*.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: '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', src: 'assets/manifest.json',
dest: 'dist', dest: 'dist',
......
...@@ -13,7 +13,7 @@ class DbpCommonDemoActivity extends ScopedElementsMixin(AdapterLitElement) { ...@@ -13,7 +13,7 @@ class DbpCommonDemoActivity extends ScopedElementsMixin(AdapterLitElement) {
super(); super();
this.lang = 'en'; this.lang = 'en';
this.entryPointUrl = ''; this.entryPointUrl = '';
this.langFile = ''; this.langDir = '';
} }
static get scopedElements() { static get scopedElements() {
...@@ -26,7 +26,7 @@ class DbpCommonDemoActivity extends ScopedElementsMixin(AdapterLitElement) { ...@@ -26,7 +26,7 @@ class DbpCommonDemoActivity extends ScopedElementsMixin(AdapterLitElement) {
return { return {
...super.properties, ...super.properties,
lang: {type: String}, lang: {type: String},
langFile: {type: String, attribute: 'lang-file'}, langDir: {type: String, attribute: 'lang-dir'},
entryPointUrl: {type: String, attribute: 'entry-point-url'}, entryPointUrl: {type: String, attribute: 'entry-point-url'},
}; };
} }
...@@ -65,7 +65,7 @@ class DbpCommonDemoActivity extends ScopedElementsMixin(AdapterLitElement) { ...@@ -65,7 +65,7 @@ class DbpCommonDemoActivity extends ScopedElementsMixin(AdapterLitElement) {
<dbp-common-demo <dbp-common-demo
id="demo" id="demo"
lang="${this.lang}" lang="${this.lang}"
lang-file="${this.langFile}" lang-dir="${this.langDir}"
entry-point-url="${this.entryPointUrl}"></dbp-common-demo> entry-point-url="${this.entryPointUrl}"></dbp-common-demo>
`; `;
} }
......
{
"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": "This text will be translated to german using i18n with a user defined language file when the language is changed to german."
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment