hubl/src/scripts/intl.js

281 lines
8.2 KiB
JavaScript

/*
js intl, inspired by danabr/jsI18n
*/
class JsI18n {
constructor() {
this.locale = ""; //Current locale
this.locales = new Array(); //Available locales
this.overwrites = new Array();
this.defaultLocale = "fr";
}
/*
Method for automatically detecting the language, does not work in every browser.
*/
async detectLanguage() {
const customLangs = document.querySelectorAll('orbit-lang');
if (customLangs) {
for (let lang of customLangs) {
let name = lang.getAttribute('lang'),
file = lang.getAttribute('file');
let result = await fetch(file);
if (result.ok) {
let json = await result.json();
if(this.overwrites[name.toString()] != undefined) {
this.mergeDeep(this.overwrites[name.toString()], json);
} else {
this.overwrites[name.toString()] = json;
}
}
}
}
this.resumeDetection();
}
resumeDetection() {
const langComponent = document.querySelector('orbit-fallback-lang');
if (langComponent) {
if (langComponent.hasAttribute('lang')) {
this.defaultLocale = langComponent.getAttribute('lang');
if (langComponent.hasAttribute('force')) {
localStorage.setItem('language', this.defaultLocale);
}
}
}
if (localStorage.getItem('language') || (window.navigator.language !== null && window.navigator.language !== undefined)) {
this.fetchLocale(this.defaultLocale);
this.setLocale(localStorage.getItem('language') || window.navigator.language.slice(0, 2));
} else {
console.error('Language not found');
this.setLocale(this.defaultLocale);
}
};
isObject(item) {
return (item && typeof item === 'object' && !Array.isArray(item));
}
mergeDeep(target, ...sources) {
if (!sources.length) return target;
const source = sources.shift();
if (this.isObject(target) && this.isObject(source)) {
for (const key in source) {
if (this.isObject(source[key])) {
if (!target[key]) Object.assign(target, {
[key]: {}
});
this.mergeDeep(target[key], source[key]);
} else {
Object.assign(target, {
[key]: source[key]
});
}
}
}
return this.mergeDeep(target, ...sources);
}
/*
Translates tag contents and
attributes depending on the
value of key.
*/
translateTag(node, key) {
if (key.indexOf("=") == -1) { //Simple key
this.translateNodeContent(node, key);
} else { //Attribute/key pairs
var parts = key.split(";");
for (var i = 0; i < parts.length; i++) {
var pair = parts[i].split("=");
var attr = pair[0].toLowerCase().trim();
var k = pair[1].trim();
if (attr == "html") {
this.translateNodeContent(node, k);
} else {
// https://git.startinblox.com/framework/sib-core/issues/733
if (attr.startsWith('label-')) {
let label = node.querySelector('[name="' + attr.replace("label-", "") + '"] > label');
if (label != null) {
this.translateNodeContent(label, k);
}
}
// https://git.startinblox.com/framework/sib-core/issues/755
if (attr.startsWith('placeholder-')) {
let placeholder = node.querySelector('[placeholder="' + attr.replace("placeholder-", "") + '"]');
if (placeholder != null) {
this.translateNodeContent(placeholder.attributes['placeholder'], k);
let input = node.querySelector('[name="' + attr.replace("placeholder-", "") + '"] > input');
if (input != null) {
this.translateNodeContent(input.attributes['placeholder'], k);
}
}
}
this.translateNodeContent(node.attributes[attr], k);
}
}
}
}
/**
Replace the content of the given node
if there is a translation for the given key.
**/
translateNodeContent(node, key) {
var translation = this.t(key);
if (node != null && translation != undefined) {
if (node.nodeType == 1) { //Element
try {
if (node.innerHTML != translation)
node.innerHTML = translation;
} catch (e) {
if (node.text != translation)
node.text = translation;
}
} else if (node.nodeType == 2) { //Attribute
if (node.value != translation)
node.value = translation;
}
}
}
/*
Helper for translating a node
and all its child nodes.
*/
processNode(node) {
if (node != undefined) {
if (node.nodeType == 1) { //Element node
var key = node.attributes["data-trans"];
if (key != null) {
this.translateTag(node, key.nodeValue);
}
}
//Process child nodes
var children = node.childNodes;
for (var i = 0; i < children.length; i++) {
this.processNode(children[i]);
}
}
}
/*
Adds a locale to the list,
replacing the translations
if the locale is already defined.
*/
addLocale(locale, translations) {
if (this.overwrites[locale.toString()] != undefined) {
this.mergeDeep(translations, this.overwrites[locale.toString()]);
}
this.locales[locale.toString()] = translations;
}
fetchLocale(locale) {
return fetch(`/locales/${locale}.json`).then((result) => {
if (result.ok) {
result.json().then(e => {
this.addLocale(locale, e);
});
}
});
}
/*
Sets the locale to use when translating.
*/
setLocale(locale) {
try {
this.fetchLocale(locale).then(() => {
if (this.locale) {
localStorage.setItem('language', this.locale);
}
this.processPage();
this.locale = locale;
});
} catch {
if (locale != this.defaultLocale) {
console.warn(`Locale not found: ${locale}, fallback to ${this.defaultLocale}`);
this.setLocale(this.defaultLocale);
} else {
console.error("Language not found");
}
}
}
/*
Fetches the translation associated with the given key.
*/
t(key) {
var translations = this.locales[this.locale];
if (translations == undefined) {
if (this.locales.length > 1) {
translations = this.locales[this.defaultLocale];
}
}
if (translations != undefined) {
let translation = key.toString().split('.').reduce((o, i) => (o ? o[i] : undefined), translations);
if (typeof translation == "string") {
return translation;
} else {
try {
let keySplitted = key.toString().split('.');
let first = keySplitted.shift();
translation = translations[first][keySplitted.join('.')];
return translation;
} catch {
return translations[key.toString()];
}
}
}
return undefined;
}
/*
Alias for JsI18n.t
*/
translate(key) {
this.t(key);
}
/**
Replaces the contents of all tags
that have the data-trans attribute set.
**/
processPage() {
this.processNode(document.getElementsByTagName("html")[0]);
}
}
//Global
window.orbit.intl = new JsI18n;
document.addEventListener("DOMContentLoaded", () => {
// Detect the lang & initialize, based on the browser or "language" item from localstorage
window.orbit.intl.detectLanguage();
let timer;
(new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.target.attributes["data-trans"] != null) {
// Render the target of the mutation instantly
window.orbit.intl.processNode(mutation.target);
// Then wait one arbitrary second to re-render the whole document in case a widget re-rendered
clearTimeout(timer);
timer = setTimeout(() => window.orbit.intl.processNode(document.querySelector('body')), 500);
}
});
}).observe(document.body, {
subtree: true,
childList: true
}));
document.addEventListener('widgetRendered', event => {
window.orbit.intl.processNode(event.target);
// Then wait one arbitrary second to re-render the whole document in case a widget re-rendered
clearTimeout(timer);
timer = setTimeout(() => window.orbit.intl.processNode(document.querySelector('body')), 500);
});
});