stylus/js/localization.js
2017-12-29 17:52:47 +03:00

212 lines
5.8 KiB
JavaScript

'use strict';
const template = {};
tDocLoader();
function t(key, params) {
const cache = !params && t.cache[key];
const s = cache || chrome.i18n.getMessage(key, params);
if (s === '') {
throw `Missing string "${key}"`;
}
if (!params && !cache) {
t.cache[key] = s;
}
return s;
}
function tHTML(html, tag) {
// body is a text node without HTML tags
if (typeof html === 'string' && !tag && /<\w+/.test(html) === false) {
return document.createTextNode(html);
}
if (typeof html === 'string') {
// spaces are removed; use &nbsp; for an explicit space
html = html.replace(/>\s+</g, '><').trim();
if (tag) {
html = `<${tag}>${html}</${tag}>`;
}
const body = t.DOMParser.parseFromString(html, 'text/html').body;
if (html.includes('i18n-')) {
tNodeList(body.getElementsByTagName('*'));
}
// the html string may contain more than one top-level node
if (!body.childNodes[1]) {
return body.firstChild;
}
const fragment = document.createDocumentFragment();
while (body.firstChild) {
fragment.appendChild(body.firstChild);
}
return fragment;
}
return html;
}
function tNodeList(nodes) {
const PREFIX = 'i18n-';
for (let n = nodes.length; --n >= 0;) {
const node = nodes[n];
if (node.nodeType !== Node.ELEMENT_NODE) {
continue;
}
if (node.localName === 'template') {
createTemplate(node);
continue;
}
for (let a = node.attributes.length; --a >= 0;) {
const attr = node.attributes[a];
const name = attr.nodeName;
if (!name.startsWith(PREFIX)) {
continue;
}
const type = name.substr(PREFIX.length);
const value = t(attr.value);
let toInsert, before;
switch (type) {
case 'word-break':
// we already know that: hasWordBreak
break;
case 'text':
before = node.firstChild;
// fallthrough to text-append
case 'text-append':
toInsert = createText(value);
break;
case 'html': {
toInsert = createHtml(value);
break;
}
default:
node.setAttribute(type, value);
}
tDocLoader.pause();
if (toInsert) {
node.insertBefore(toInsert, before || null);
}
node.removeAttribute(name);
}
}
function createTemplate(node) {
const elements = node.content.querySelectorAll('*');
tNodeList(elements);
template[node.dataset.id] = elements[0];
// compress inter-tag whitespace to reduce number of DOM nodes by 25%
const walker = document.createTreeWalker(elements[0], NodeFilter.SHOW_TEXT);
const toRemove = [];
while (walker.nextNode()) {
const textNode = walker.currentNode;
if (!textNode.nodeValue.trim()) {
toRemove.push(textNode);
}
}
tDocLoader.pause();
toRemove.forEach(el => el.remove());
}
function createText(str) {
return document.createTextNode(tWordBreak(str));
}
function createHtml(value) {
// <a href=foo>bar</a> are the only recognizable HTML elements
const rx = /(?:<a\s([^>]*)>([^<]*)<\/a>)?([^<]*)/gi;
const bin = document.createDocumentFragment();
for (let m; (m = rx.exec(value)) && m[0];) {
const [, linkParams, linkText, nextText] = m;
if (linkText) {
const href = /\bhref\s*=\s*(\S+)/.exec(linkParams);
const a = bin.appendChild(document.createElement('a'));
a.href = href && href[1].replace(/^(["'])(.*)\1$/, '$2') || '';
a.appendChild(createText(linkText));
}
if (nextText) {
bin.appendChild(createText(nextText));
}
}
return bin;
}
}
function tDocLoader() {
t.DOMParser = new DOMParser();
t.cache = (() => {
try {
return JSON.parse(localStorage.L10N);
} catch (e) {}
})() || {};
// reset L10N cache on UI language change
const UIlang = chrome.i18n.getUILanguage();
if (t.cache.browserUIlanguage !== UIlang) {
t.cache = {browserUIlanguage: UIlang};
localStorage.L10N = JSON.stringify(t.cache);
}
const cacheLength = Object.keys(t.cache).length;
Object.assign(tDocLoader, {
observer: new MutationObserver(process),
start() {
if (!tDocLoader.observing) {
tDocLoader.observing = true;
tDocLoader.observer.observe(document, {subtree: true, childList: true});
}
},
stop() {
tDocLoader.pause();
document.removeEventListener('DOMContentLoaded', onLoad);
},
pause() {
if (tDocLoader.observing) {
tDocLoader.observing = false;
tDocLoader.observer.disconnect();
}
},
});
tNodeList(document.getElementsByTagName('*'));
tDocLoader.start();
document.addEventListener('DOMContentLoaded', onLoad);
function process(mutations) {
for (const mutation of mutations) {
tNodeList(mutation.addedNodes);
}
tDocLoader.start();
}
function onLoad() {
document.removeEventListener('DOMContentLoaded', onLoad);
process(tDocLoader.observer.takeRecords());
tDocLoader.stop();
if (cacheLength !== Object.keys(t.cache).length) {
localStorage.L10N = JSON.stringify(t.cache);
}
}
}
function tWordBreak(text) {
// adds soft hyphens every 10 characters to ensure the long words break before breaking the layout
return text.length <= 10 ? text :
text.replace(/([\d\w\u007B-\uFFFF]{10}|[\d\w\u007B-\uFFFF]{5,10}[!-/]|((?!\s)\W){10})(?!\b|\s)/g, '$&\u00AD');
}
function formatDate(date) {
return !date ? '' : tryCatch(() => {
const newDate = new Date(Number(date) || date);
const string = newDate.toLocaleDateString([t.cache.browserUIlanguage, 'en'], {
day: '2-digit',
month: 'short',
year: newDate.getYear() === new Date().getYear() ? undefined : '2-digit',
});
return string === 'Invalid Date' ? '' : string;
}) || '';
}