/**
* Translates WebExtension's HTML document by attributes.
*
* @public
* @module Localizer
* @requires ./replaceInnerContent
*/
import { replaceInnerContent } from "./replaceInnerContent.js";
const I18N_ATTRIBUTE = "data-i18n";
const I18N_DATASET = "i18n";
const I18N_DATASET_INT = I18N_DATASET.length;
/**
* Splits the _MSG__*__ format and returns the actual tag.
*
* The format is defined in {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n/Locale-Specific_Message_reference#name}.
*
* @private
* @param {string} tag
* @returns {string}
* @throws {Error} if pattern does not match
*/
function getMessageTag(tag) {
/** {@link https://regex101.com/r/LAC5Ib/2} **/
const splitMessage = tag.split(/^__MSG_([\w@]+)__$/);
// throw custom exception if input is invalid
if (splitMessage.length < 2) {
throw new Error(`invalid message tag pattern "${tag}"`);
}
return splitMessage[1];
}
/**
* Converts a dataset value back to a real attribute.
*
* This is intended for substrings of datasets too, i.e. it does not add the "data" prefix
* in front of the attribute.
*
* @private
* @param {string} dataSetValue
* @returns {string}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset#Name_conversion}
*/
function convertDatasetToAttribute(dataSetValue) {
// if beginning of string is capital letter, only lowercase that
/** {@link https://regex101.com/r/GaVoVi/1} **/
dataSetValue = dataSetValue.replace(/^[A-Z]/, (char) => char.toLowerCase());
// replace all other capital letters with dash in front of them
/** {@link https://regex101.com/r/GaVoVi/3} **/
dataSetValue = dataSetValue.replace(/[A-Z]/, (char) => {
return `-${char.toLowerCase()}`;
});
return dataSetValue;
}
/**
* Returns the translated message when a key is given.
*
* @private
* @param {string} messageName
* @param {string[]} [substitutions]
* @returns {string} translated string
* @throws {Error} if no translation could be found
* @see {@link https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/i18n/getMessage}
*/
function getTranslatedMessage(messageName, substitutions) {
const translatedMessage = browser.i18n.getMessage(messageName, substitutions);
if (!translatedMessage) {
throw new Error(`no translation string for "${messageName}" could be found`);
}
return translatedMessage;
}
/**
* Replaces attribute or inner text of element with string.
*
* @private
* @param {HTMLElement} elem
* @param {string} attribute attribute to replace, set to "null" to replace inner content
* @param {string} translatedMessage
* @returns {void}
*/
function replaceWith(elem, attribute, translatedMessage) {
const isHTML = translatedMessage.startsWith("!HTML!");
if (isHTML) {
translatedMessage = translatedMessage.replace("!HTML!", "").trimLeft();
}
switch (attribute) {
case null:
replaceInnerContent(elem, translatedMessage, isHTML);
break;
default:
// attributes are never allowed to contain unbescaped HTML
elem.setAttribute(attribute, translatedMessage);
}
}
/**
* Localises the strings to localize in the HTMLElement.
*
* @private
* @param {HTMLElement} elem
* @param {string} tag the translation tag
* @returns {void}
*/
function replaceI18n(elem, tag) {
// localize main content
if (tag !== "") {
try {
const translatedMessage = getTranslatedMessage(getMessageTag(tag));
replaceWith(elem, null, translatedMessage);
} catch (error) {
// log error but continue translating as it was likely just one problem in one translation
console.error(error.message, "for element", elem);
}
}
// replace attributes
for (const [dataAttribute, dataValue] of Object.entries(elem.dataset)) {
if (
!dataAttribute.startsWith(I18N_DATASET) || // ignore other data attributes
dataAttribute.length === I18N_DATASET_INT // ignore non-attribute replacements
) {
continue;
}
const replaceAttribute = convertDatasetToAttribute(dataAttribute.slice(I18N_DATASET_INT));
try {
const translatedMessage = getTranslatedMessage(getMessageTag(dataValue));
replaceWith(elem, replaceAttribute, translatedMessage);
} catch (error) {
// log error but continue translating as it was likely just one problem in one translation
console.error(error.message, "for element", elem, "while replacing attribute", replaceAttribute);
}
}
}
/**
* Localizes static strings in the HTML file.
*
* @public
* @returns {void}
*/
export function init() {
document.querySelectorAll(`[${I18N_ATTRIBUTE}]`).forEach((currentElem) => {
const contentString = currentElem.dataset[I18N_DATASET];
replaceI18n(currentElem, contentString);
});
// replace html lang attribut after translation
document.querySelector("html").setAttribute("lang", browser.i18n.getUILanguage());
}
// automatically init module
init();