tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

l10n.js (8581B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 "use strict";
      5 
      6 const parsePropertiesFile = require("devtools/shared/node-properties/node-properties");
      7 const { sprintf } = require("devtools/shared/sprintfjs/sprintf");
      8 
      9 const propertiesMap = {};
     10 
     11 // Map used to memoize Number formatters.
     12 const numberFormatters = new Map();
     13 const getNumberFormatter = function (decimals) {
     14  let formatter = numberFormatters.get(decimals);
     15  if (!formatter) {
     16    // Create and memoize a formatter for the provided decimals
     17    formatter = Intl.NumberFormat(undefined, {
     18      maximumFractionDigits: decimals,
     19      minimumFractionDigits: decimals,
     20    });
     21    numberFormatters.set(decimals, formatter);
     22  }
     23 
     24  return formatter;
     25 };
     26 
     27 /**
     28 * Memoized getter for properties files that ensures a given url is only required and
     29 * parsed once.
     30 *
     31 * @param {string} url
     32 *        The URL of the properties file to parse.
     33 * @return {object} parsed properties mapped in an object.
     34 */
     35 function getProperties(url) {
     36  if (!propertiesMap[url]) {
     37    let propertiesFile;
     38    let isNodeEnv = false;
     39    try {
     40      // eslint-disable-next-line no-undef
     41      isNodeEnv = process?.release?.name == "node";
     42    } catch (e) {}
     43 
     44    if (isNodeEnv) {
     45      // In Node environment (e.g. when running jest test), we need to prepend the en-US
     46      // to the filename in order to have the actual location of the file in source.
     47      const lastDelimIndex = url.lastIndexOf("/");
     48      const defaultLocaleUrl =
     49        url.substring(0, lastDelimIndex) +
     50        "/en-US" +
     51        url.substring(lastDelimIndex);
     52 
     53      const path = require("path");
     54      // eslint-disable-next-line no-undef
     55      const rootPath = path.join(__dirname, "../../");
     56      const absoluteUrl = path.join(rootPath, defaultLocaleUrl);
     57      const { readFileSync } = require("fs");
     58      // In Node environment we directly use readFileSync to get the file content instead
     59      // of relying on custom raw loader, like we do in regular environment.
     60      propertiesFile = readFileSync(absoluteUrl, { encoding: "utf8" });
     61    } else {
     62      propertiesFile = require("raw!" + url);
     63    }
     64 
     65    propertiesMap[url] = parsePropertiesFile(propertiesFile);
     66  }
     67 
     68  return propertiesMap[url];
     69 }
     70 
     71 /**
     72 * Localization convenience methods.
     73 */
     74 class LocalizationHelper {
     75  /**
     76   *
     77   * @param {string} stringBundleName
     78   *        The desired string bundle's name.
     79   * @param {boolean} strict
     80   *        (legacy) pass true to force the helper to throw if the l10n id cannot be found.
     81   */
     82  constructor(stringBundleName, strict = false) {
     83    this.stringBundleName = stringBundleName;
     84    this.strict = strict;
     85  }
     86  /**
     87   * L10N shortcut function.
     88   *
     89   * @param string name
     90   * @return string
     91   */
     92  getStr(name) {
     93    const properties = getProperties(this.stringBundleName);
     94    if (name in properties) {
     95      return properties[name];
     96    }
     97 
     98    if (this.strict) {
     99      throw new Error("No localization found for [" + name + "]");
    100    }
    101 
    102    console.error("No localization found for [" + name + "]");
    103    return name;
    104  }
    105 
    106  /**
    107   * L10N shortcut function.
    108   *
    109   * @param string name
    110   * @param array args
    111   * @return string
    112   */
    113  getFormatStr(name, ...args) {
    114    return sprintf(this.getStr(name), ...args);
    115  }
    116 
    117  /**
    118   * L10N shortcut function for numeric arguments that need to be formatted.
    119   * All numeric arguments will be fixed to 2 decimals and given a localized
    120   * decimal separator. Other arguments will be left alone.
    121   *
    122   * @param string name
    123   * @param array args
    124   * @return string
    125   */
    126  getFormatStrWithNumbers(name, ...args) {
    127    const newArgs = args.map(x => {
    128      return typeof x == "number" ? this.numberWithDecimals(x, 2) : x;
    129    });
    130 
    131    return this.getFormatStr(name, ...newArgs);
    132  }
    133 
    134  /**
    135   * Converts a number to a locale-aware string format and keeps a certain
    136   * number of decimals.
    137   *
    138   * @param number number
    139   *        The number to convert.
    140   * @param number decimals [optional]
    141   *        Total decimals to keep.
    142   * @return string
    143   *         The localized number as a string.
    144   */
    145  numberWithDecimals(number, decimals = 0) {
    146    // Do not show decimals for integers.
    147    if (number === (number | 0)) {
    148      return getNumberFormatter(0).format(number);
    149    }
    150 
    151    // If this isn't a number (and yes, `isNaN(null)` is false), return zero.
    152    if (isNaN(number) || number === null) {
    153      return getNumberFormatter(0).format(0);
    154    }
    155 
    156    // Localize the number using a memoized Intl.NumberFormat formatter.
    157    const localized = getNumberFormatter(decimals).format(number);
    158 
    159    // Convert the localized number to a number again.
    160    const localizedNumber = localized * 1;
    161    // Check if this number is now equal to an integer.
    162    if (localizedNumber === (localizedNumber | 0)) {
    163      // If it is, remove the fraction part.
    164      return getNumberFormatter(0).format(localizedNumber);
    165    }
    166 
    167    return localized;
    168  }
    169 }
    170 
    171 function getPropertiesForNode(node) {
    172  const bundleEl = node.closest("[data-localization-bundle]");
    173  if (!bundleEl) {
    174    return null;
    175  }
    176 
    177  const propertiesUrl = bundleEl.getAttribute("data-localization-bundle");
    178  return getProperties(propertiesUrl);
    179 }
    180 
    181 /**
    182 * Translate existing markup annotated with data-localization attributes.
    183 *
    184 * How to use data-localization in markup:
    185 *
    186 *   <div data-localization="content=myContent;title=myTitle"/>
    187 *
    188 * The data-localization attribute identifies an element as being localizable.
    189 * The content of the attribute is semi-colon separated list of descriptors.
    190 * - "title=myTitle" means the "title" attribute should be replaced with the localized
    191 *   string corresponding to the key "myTitle".
    192 * - "content=myContent" means the text content of the node should be replaced by the
    193 *   string corresponding to "myContent"
    194 *
    195 * How to define the localization bundle in markup:
    196 *
    197 *   <div data-localization-bundle="url/to/my.properties">
    198 *     [...]
    199 *       <div data-localization="content=myContent;title=myTitle"/>
    200 *
    201 * Set the data-localization-bundle on an ancestor of the nodes that should be localized.
    202 *
    203 * @param {Element} root
    204 *        The root node to use for the localization
    205 */
    206 function localizeMarkup(root) {
    207  const elements = root.querySelectorAll("[data-localization]");
    208  for (const element of elements) {
    209    const properties = getPropertiesForNode(element);
    210    if (!properties) {
    211      continue;
    212    }
    213 
    214    const attributes = element.getAttribute("data-localization").split(";");
    215    for (const attribute of attributes) {
    216      const [name, value] = attribute.trim().split("=");
    217      if (name === "content") {
    218        element.textContent = properties[value];
    219      } else {
    220        element.setAttribute(name, properties[value]);
    221      }
    222    }
    223 
    224    element.removeAttribute("data-localization");
    225  }
    226 }
    227 
    228 /**
    229 * A helper for having the same interface as LocalizationHelper, but for more
    230 * than one file. Useful for abstracting l10n string locations.
    231 */
    232 class MultiLocalizationHelper {
    233  constructor(...stringBundleNames) {
    234    const instances = stringBundleNames.map(bundle => {
    235      // Use strict = true because the MultiLocalizationHelper logic relies on try/catch
    236      // around the underlying LocalizationHelper APIs.
    237      return new LocalizationHelper(bundle, true);
    238    });
    239 
    240    // Get all function members of the LocalizationHelper class, making sure we're
    241    // not executing any potential getters while doing so, and wrap all the
    242    // methods we've found to work on all given string bundles.
    243    Object.getOwnPropertyNames(LocalizationHelper.prototype)
    244      .map(name => ({
    245        name,
    246        descriptor: Object.getOwnPropertyDescriptor(
    247          LocalizationHelper.prototype,
    248          name
    249        ),
    250      }))
    251      .filter(({ descriptor }) => descriptor.value instanceof Function)
    252      .forEach(method => {
    253        this[method.name] = (...args) => {
    254          for (const l10n of instances) {
    255            try {
    256              return method.descriptor.value.apply(l10n, args);
    257            } catch (e) {
    258              // Do nothing
    259            }
    260          }
    261          return null;
    262        };
    263      });
    264  }
    265 }
    266 
    267 exports.LocalizationHelper = LocalizationHelper;
    268 exports.localizeMarkup = localizeMarkup;
    269 exports.MultiLocalizationHelper = MultiLocalizationHelper;
    270 Object.defineProperty(exports, "ELLIPSIS", {
    271  get: () =>
    272    typeof Services == "undefined" ? "\u2026" : Services.locale.ellipsis,
    273 });