text: /* global tryCatch */
  /* exported tHTML formatDate */
  '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) {}
    })() || {};
    t.RX_WORD_BREAK = new RegExp([
      '(',
      /[\d\w\u007B-\uFFFF]{10}/,
      '|',
      /[\d\w\u007B-\uFFFF]{5,10}[!-/]/,
      '|',
      /((?!\s)\W){10}/,
      ')',
      /(?!\b|\s|$)/,
    ].map(rx => rx.source || rx).join(''), 'g');
  
    // 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(t.RX_WORD_BREAK, '$&\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;
    }) || '';
  }
  