/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const parsePropertiesFile = require("devtools/shared/node-properties/node-properties"); const { sprintf } = require("devtools/shared/sprintfjs/sprintf"); const propertiesMap = {}; // Map used to memoize Number formatters. const numberFormatters = new Map(); const getNumberFormatter = function(decimals) { let formatter = numberFormatters.get(decimals); if (!formatter) { // Create and memoize a formatter for the provided decimals formatter = Intl.NumberFormat(undefined, { maximumFractionDigits: decimals, minimumFractionDigits: decimals, }); numberFormatters.set(decimals, formatter); } return formatter; }; /** * Memoized getter for properties files that ensures a given url is only required and * parsed once. * * @param {String} url * The URL of the properties file to parse. * @return {Object} parsed properties mapped in an object. */ function getProperties(url) { if (!propertiesMap[url]) { let propertiesFile; let isNodeEnv = false; try { // eslint-disable-next-line no-undef isNodeEnv = process?.release?.name == "node"; } catch (e) {} if (isNodeEnv) { // In Node environment (e.g. when running jest test), we need to prepend the en-US // to the filename in order to have the actual location of the file in source. const lastDelimIndex = url.lastIndexOf("/"); const defaultLocaleUrl = url.substring(0, lastDelimIndex) + "/en-US" + url.substring(lastDelimIndex); const path = require("path"); // eslint-disable-next-line no-undef const rootPath = path.join(__dirname, "../../"); const absoluteUrl = path.join(rootPath, defaultLocaleUrl); const { readFileSync } = require("fs"); // In Node environment we directly use readFileSync to get the file content instead // of relying on custom raw loader, like we do in regular environment. propertiesFile = readFileSync(absoluteUrl, { encoding: "utf8" }); } else { propertiesFile = require("raw!" + url); } propertiesMap[url] = parsePropertiesFile(propertiesFile); } return propertiesMap[url]; } /** * Localization convenience methods. * * @param string stringBundleName * The desired string bundle's name. * @param boolean strict * (legacy) pass true to force the helper to throw if the l10n id cannot be found. */ function LocalizationHelper(stringBundleName, strict = false) { this.stringBundleName = stringBundleName; this.strict = strict; } LocalizationHelper.prototype = { /** * L10N shortcut function. * * @param string name * @return string */ getStr: function(name) { const properties = getProperties(this.stringBundleName); if (name in properties) { return properties[name]; } if (this.strict) { throw new Error("No localization found for [" + name + "]"); } console.error("No localization found for [" + name + "]"); return name; }, /** * L10N shortcut function. * * @param string name * @param array args * @return string */ getFormatStr: function(name, ...args) { return sprintf(this.getStr(name), ...args); }, /** * L10N shortcut function for numeric arguments that need to be formatted. * All numeric arguments will be fixed to 2 decimals and given a localized * decimal separator. Other arguments will be left alone. * * @param string name * @param array args * @return string */ getFormatStrWithNumbers: function(name, ...args) { const newArgs = args.map(x => { return typeof x == "number" ? this.numberWithDecimals(x, 2) : x; }); return this.getFormatStr(name, ...newArgs); }, /** * Converts a number to a locale-aware string format and keeps a certain * number of decimals. * * @param number number * The number to convert. * @param number decimals [optional] * Total decimals to keep. * @return string * The localized number as a string. */ numberWithDecimals: function(number, decimals = 0) { // Do not show decimals for integers. if (number === (number | 0)) { return getNumberFormatter(0).format(number); } // If this isn't a number (and yes, `isNaN(null)` is false), return zero. if (isNaN(number) || number === null) { return getNumberFormatter(0).format(0); } // Localize the number using a memoized Intl.NumberFormat formatter. const localized = getNumberFormatter(decimals).format(number); // Convert the localized number to a number again. const localizedNumber = localized * 1; // Check if this number is now equal to an integer. if (localizedNumber === (localizedNumber | 0)) { // If it is, remove the fraction part. return getNumberFormatter(0).format(localizedNumber); } return localized; }, }; function getPropertiesForNode(node) { const bundleEl = node.closest("[data-localization-bundle]"); if (!bundleEl) { return null; } const propertiesUrl = bundleEl.getAttribute("data-localization-bundle"); return getProperties(propertiesUrl); } /** * Translate existing markup annotated with data-localization attributes. * * How to use data-localization in markup: * *
* * The data-localization attribute identifies an element as being localizable. * The content of the attribute is semi-colon separated list of descriptors. * - "title=myTitle" means the "title" attribute should be replaced with the localized * string corresponding to the key "myTitle". * - "content=myContent" means the text content of the node should be replaced by the * string corresponding to "myContent" * * How to define the localization bundle in markup: * *