Bug 1265887 - port PluralForm.jsm to plural-form.js without chrome APIs;r=tromey
authorJulian Descottes <jdescottes@mozilla.com>
Mon, 29 Aug 2016 12:17:42 +0200
changeset 311781 cb74e60bc40a18ffa711ca07129855c094b792c1
parent 311718 26e22af660e543ebb69930f082188b69ec756185
child 311782 2c763524c0b6e92987fb18fc6e6cdd05fa9ae6f0
push id30622
push userryanvm@gmail.com
push dateTue, 30 Aug 2016 13:52:15 +0000
treeherdermozilla-central@cd0957de64a0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstromey
bugs1265887
milestone51.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1265887 - port PluralForm.jsm to plural-form.js without chrome APIs;r=tromey MozReview-Commit-ID: GpGFgtdnzek
devtools/shared/moz.build
devtools/shared/plural-form.js
devtools/shared/tests/unit/test_pluralForm-english.js
devtools/shared/tests/unit/test_pluralForm-makeGetter.js
devtools/shared/tests/unit/xpcshell.ini
--- a/devtools/shared/moz.build
+++ b/devtools/shared/moz.build
@@ -58,13 +58,14 @@ DevToolsModules(
     'event-emitter.js',
     'flags.js',
     'indentation.js',
     'l10n.js',
     'loader-plugin-raw.jsm',
     'Loader.jsm',
     'Parser.jsm',
     'path.js',
+    'plural-form.js',
     'protocol.js',
     'system.js',
     'task.js',
     'ThreadSafeDevToolsUtils.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/shared/plural-form.js
@@ -0,0 +1,196 @@
+/* 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/. */
+
+/*
+ * The code below is mostly is a slight modification of intl/locale/PluralForm.jsm that
+ * removes dependencies on chrome privileged APIs. To make maintenance easier, this file
+ * is kept as close as possible to the original in terms of implementation.
+ * The modified methods here are
+ * - makeGetter (remove code adding the caller name to the log)
+ * - get ruleNum() (rely on LocalizationHelper instead of String.services)
+ * - log() (rely on console.log)
+ *
+ * Disable eslint warnings to preserve original code style.
+ */
+
+/* eslint-disable */
+
+/**
+ * This module provides the PluralForm object which contains a method to figure
+ * out which plural form of a word to use for a given number based on the
+ * current localization. There is also a makeGetter method that creates a get
+ * function for the desired plural rule. This is useful for extensions that
+ * specify their own plural rule instead of relying on the browser default.
+ * (I.e., the extension hasn't been localized to the browser's locale.)
+ *
+ * See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+ *
+ * List of methods:
+ *
+ * string pluralForm
+ * get(int aNum, string aWords)
+ *
+ * int numForms
+ * numForms()
+ *
+ * [string pluralForm get(int aNum, string aWords), int numForms numForms()]
+ * makeGetter(int aRuleNum)
+ * Note: Basically, makeGetter returns 2 functions that do "get" and "numForm"
+ */
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("global/locale/intl.properties");
+
+// These are the available plural functions that give the appropriate index
+// based on the plural rule number specified. The first element is the number
+// of plural forms and the second is the function to figure out the index.
+var gFunctions = [
+  // 0: Chinese
+  [1, (n) => 0],
+  // 1: English
+  [2, (n) => n!=1?1:0],
+  // 2: French
+  [2, (n) => n>1?1:0],
+  // 3: Latvian
+  [3, (n) => n%10==1&&n%100!=11?1:n!=0?2:0],
+  // 4: Scottish Gaelic
+  [4, (n) => n==1||n==11?0:n==2||n==12?1:n>0&&n<20?2:3],
+  // 5: Romanian
+  [3, (n) => n==1?0:n==0||n%100>0&&n%100<20?1:2],
+  // 6: Lithuanian
+  [3, (n) => n%10==1&&n%100!=11?0:n%10>=2&&(n%100<10||n%100>=20)?2:1],
+  // 7: Russian
+  [3, (n) => n%10==1&&n%100!=11?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2],
+  // 8: Slovak
+  [3, (n) => n==1?0:n>=2&&n<=4?1:2],
+  // 9: Polish
+  [3, (n) => n==1?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2],
+  // 10: Slovenian
+  [4, (n) => n%100==1?0:n%100==2?1:n%100==3||n%100==4?2:3],
+  // 11: Irish Gaeilge
+  [5, (n) => n==1?0:n==2?1:n>=3&&n<=6?2:n>=7&&n<=10?3:4],
+  // 12: Arabic
+  [6, (n) => n==0?5:n==1?0:n==2?1:n%100>=3&&n%100<=10?2:n%100>=11&&n%100<=99?3:4],
+  // 13: Maltese
+  [4, (n) => n==1?0:n==0||n%100>0&&n%100<=10?1:n%100>10&&n%100<20?2:3],
+  // 14: Macedonian
+  [3, (n) => n%10==1?0:n%10==2?1:2],
+  // 15: Icelandic
+  [2, (n) => n%10==1&&n%100!=11?0:1],
+  // 16: Breton
+  [5, (n) => n%10==1&&n%100!=11&&n%100!=71&&n%100!=91?0:n%10==2&&n%100!=12&&n%100!=72&&n%100!=92?1:(n%10==3||n%10==4||n%10==9)&&n%100!=13&&n%100!=14&&n%100!=19&&n%100!=73&&n%100!=74&&n%100!=79&&n%100!=93&&n%100!=94&&n%100!=99?2:n%1000000==0&&n!=0?3:4],
+];
+
+this.PluralForm = {
+  /**
+   * Get the correct plural form of a word based on the number
+   *
+   * @param aNum
+   *        The number to decide which plural form to use
+   * @param aWords
+   *        A semi-colon (;) separated string of words to pick the plural form
+   * @return The appropriate plural form of the word
+   */
+  get get()
+  {
+    // This method will lazily load to avoid perf when it is first needed and
+    // creates getPluralForm function. The function it creates is based on the
+    // value of pluralRule specified in the intl stringbundle.
+    // See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+
+    // Delete the getters to be overwritten
+    delete PluralForm.numForms;
+    delete PluralForm.get;
+
+    // Make the plural form get function and set it as the default get
+    [PluralForm.get, PluralForm.numForms] = PluralForm.makeGetter(PluralForm.ruleNum);
+    return PluralForm.get;
+  },
+
+  /**
+   * Create a pair of plural form functions for the given plural rule number.
+   *
+   * @param aRuleNum
+   *        The plural rule number to create functions
+   * @return A pair: [function that gets the right plural form,
+   *                  function that returns the number of plural forms]
+   */
+  makeGetter: function(aRuleNum)
+  {
+    // Default to "all plural" if the value is out of bounds or invalid
+    if (aRuleNum < 0 || aRuleNum >= gFunctions.length || isNaN(aRuleNum)) {
+      log(["Invalid rule number: ", aRuleNum, " -- defaulting to 0"]);
+      aRuleNum = 0;
+    }
+
+    // Get the desired pluralRule function
+    let [numForms, pluralFunc] = gFunctions[aRuleNum];
+
+    // Return functions that give 1) the number of forms and 2) gets the right
+    // plural form
+    return [function(aNum, aWords) {
+      // Figure out which index to use for the semi-colon separated words
+      let index = pluralFunc(aNum ? Number(aNum) : 0);
+      let words = aWords ? aWords.split(/;/) : [""];
+
+      // Explicitly check bounds to avoid strict warnings
+      let ret = index < words.length ? words[index] : undefined;
+
+      // Check for array out of bounds or empty strings
+      if ((ret == undefined) || (ret == "")) {
+        // Display a message in the error console
+        log(["Index #", index, " of '", aWords, "' for value ", aNum,
+            " is invalid -- plural rule #", aRuleNum, ";"]);
+
+        // Default to the first entry (which might be empty, but not undefined)
+        ret = words[0];
+      }
+
+      return ret;
+    }, () => numForms];
+  },
+
+  /**
+   * Get the number of forms for the current plural rule
+   *
+   * @return The number of forms
+   */
+  get numForms()
+  {
+    // We lazily load numForms, so trigger the init logic with get()
+    PluralForm.get();
+    return PluralForm.numForms;
+  },
+
+  /**
+   * Get the plural rule number from the intl stringbundle
+   *
+   * @return The plural rule number
+   */
+  get ruleNum()
+  {
+    try {
+      return parseInt(L10N.getStr("pluralRule"), 10);
+    } catch (e) {
+      // Fallback to English if the pluralRule property is not available.
+      return 1;
+    }
+  }
+};
+
+/**
+ * Private helper function to log errors to the error console and command line
+ *
+ * @param aMsg
+ *        Error message to log or an array of strings to concat
+ */
+function log(aMsg)
+{
+  let msg = "plural-form.js: " + (aMsg.join ? aMsg.join("") : aMsg);
+  console.log(msg + "\n");
+}
+
+exports.PluralForm = this.PluralForm;
+
+/* eslint-ensable */
new file mode 100644
--- /dev/null
+++ b/devtools/shared/tests/unit/test_pluralForm-english.js
@@ -0,0 +1,29 @@
+/* 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";
+
+/**
+ * This unit test makes sure the plural form for Irish Gaeilge is working by
+ * using the makeGetter method instead of using the default language (by
+ * development), English.
+ */
+
+const {PluralForm} = require("devtools/shared/plural-form");
+
+function run_test() {
+  // English has 2 plural forms
+  do_check_eq(2, PluralForm.numForms());
+
+  // Make sure for good inputs, things work as expected
+  for (let num = 0; num <= 200; num++) {
+    do_check_eq(num == 1 ? "word" : "words", PluralForm.get(num, "word;words"));
+  }
+
+  // Not having enough plural forms defaults to the first form
+  do_check_eq("word", PluralForm.get(2, "word"));
+
+  // Empty forms defaults to the first form
+  do_check_eq("word", PluralForm.get(2, "word;"));
+}
new file mode 100644
--- /dev/null
+++ b/devtools/shared/tests/unit/test_pluralForm-makeGetter.js
@@ -0,0 +1,38 @@
+/* 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";
+
+/**
+ * This unit test makes sure the plural form for Irish Gaeilge is working by
+ * using the makeGetter method instead of using the default language (by
+ * development), English.
+ */
+
+const {PluralForm} = require("devtools/shared/plural-form");
+
+function run_test() {
+  // Irish is plural rule #11
+  let [get, numForms] = PluralForm.makeGetter(11);
+
+  // Irish has 5 plural forms
+  do_check_eq(5, numForms());
+
+  // I don't really know Irish, so I'll stick in some dummy text
+  let words = "is 1;is 2;is 3-6;is 7-10;everything else";
+
+  let test = function (text, low, high) {
+    for (let num = low; num <= high; num++) {
+      do_check_eq(text, get(num, words));
+    }
+  };
+
+  // Make sure for good inputs, things work as expected
+  test("everything else", 0, 0);
+  test("is 1", 1, 1);
+  test("is 2", 2, 2);
+  test("is 3-6", 3, 6);
+  test("is 7-10", 7, 10);
+  test("everything else", 11, 200);
+}
--- a/devtools/shared/tests/unit/xpcshell.ini
+++ b/devtools/shared/tests/unit/xpcshell.ini
@@ -18,15 +18,17 @@ support-files =
 [test_indentation.js]
 [test_independent_loaders.js]
 [test_invisible_loader.js]
 [test_isSet.js]
 [test_safeErrorString.js]
 [test_defineLazyPrototypeGetter.js]
 [test_async-utils.js]
 [test_console_filtering.js]
+[test_pluralForm-english.js]
+[test_pluralForm-makeGetter.js]
 [test_prettifyCSS.js]
 [test_require_lazy.js]
 [test_require_raw.js]
 [test_require.js]
 [test_stack.js]
 [test_defer.js]
 [test_executeSoon.js]