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
| `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 |
......
......@@ -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'},
};
}
......
......@@ -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>
......
......@@ -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();
}
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};
......@@ -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."
}
}
......@@ -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."
}
}
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}
`;
}
}
......@@ -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"
}
......@@ -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 %>"
......
{
"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:
{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',
......
......@@ -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>
`;
}
......
{
"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