You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by jo...@apache.org on 2016/09/28 17:28:08 UTC

[15/17] ambari git commit: AMBARI-18478. Ambari UI - Service Actions menu for pluralized value has grammatical error (onechiporenko)

AMBARI-18478. Ambari UI - Service Actions menu for pluralized value has grammatical error (onechiporenko)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/e44b8805
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/e44b8805
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/e44b8805

Branch: refs/heads/branch-feature-AMBARI-18456
Commit: e44b880514109011fadb9c274b3b8163f13390d8
Parents: 39858cc
Author: Oleg Nechiporenko <on...@apache.org>
Authored: Wed Sep 28 11:41:20 2016 +0300
Committer: Oleg Nechiporenko <on...@apache.org>
Committed: Wed Sep 28 15:29:33 2016 +0300

----------------------------------------------------------------------
 ambari-web/app/assets/licenses/NOTICE.txt       |   3 +
 ambari-web/app/messages.js                      |  13 +-
 ambari-web/app/utils/string_utils.js            |   7 +-
 .../app/views/common/rolling_restart_view.js    |  19 +-
 ambari-web/app/views/main/service/item.js       |   3 +-
 .../service/widgets/create/expression_view.js   |   2 +-
 ambari-web/brunch-config.js                     |   3 +-
 .../resourceManager/wizard_controller_test.js   |   1 -
 ambari-web/test/models/cluster_test.js          |  12 +-
 .../objects/service_config_property_test.js     |  31 +-
 .../configs/theme/sub_section_tab_test.js       |   2 +-
 .../test/views/main/host/log_metrics_test.js    |   1 -
 ambari-web/test/views/main/host_test.js         |   4 +-
 ambari-web/vendor/scripts/pluralize.js          | 461 +++++++++++++++++++
 14 files changed, 506 insertions(+), 56 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/e44b8805/ambari-web/app/assets/licenses/NOTICE.txt
----------------------------------------------------------------------
diff --git a/ambari-web/app/assets/licenses/NOTICE.txt b/ambari-web/app/assets/licenses/NOTICE.txt
index c750a37..75a13ea 100644
--- a/ambari-web/app/assets/licenses/NOTICE.txt
+++ b/ambari-web/app/assets/licenses/NOTICE.txt
@@ -60,3 +60,6 @@ Copyright (C) 2015 Leaf Corcoran (leafot [at] gmail [*dot*] com)
 
 This product includes bootstrap-contextmenu v.0.3.3 (https://github.com/sydcanem/bootstrap-contextmenu - MIT License)
 Copyright (C) 2015 James Santos
+
+This product includes pluralize v.3.0.0 (https://github.com/blakeembrey/pluralize - MIT License)
+Copyright (C) 2016 Blake Embrey

http://git-wip-us.apache.org/repos/asf/ambari/blob/e44b8805/ambari-web/app/messages.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js
index 2c819e5..1c53839 100644
--- a/ambari-web/app/messages.js
+++ b/ambari-web/app/messages.js
@@ -2913,14 +2913,13 @@ Em.I18n.translations = {
   'tableView.filters.filteredAlertInstancesInfo': '{0} of {1} instances showing',
   'tableView.filters.filteredLogsInfo': '{0} of {1} logs showing',
 
-  'rollingrestart.dialog.title': 'Restart {0}s',
+  'rollingrestart.dialog.title': 'Restart {0}',
   'rollingrestart.dialog.primary': 'Trigger Rolling Restart',
   'rollingrestart.notsupported.hostComponent': 'Rolling restart not supported for {0} components',
-  'rollingrestart.dialog.msg.restart': 'This will restart a specified number of {0}s at a time.',
-  'rollingrestart.dialog.msg.noRestartHosts': 'There are no {0}s to do rolling restarts',
+  'rollingrestart.dialog.msg.restart': 'This will restart a specified number of {0} at a time.',
+  'rollingrestart.dialog.msg.noRestartHosts': 'There are no {0} to do rolling restarts',
   'rollingrestart.dialog.msg.maintainance': 'Note: {0} {1} in Maintenance Mode will not be restarted',
-  'rollingrestart.dialog.msg.maintainance.plural': 'Note: {0} {1}s in Maintenance Mode will not be restarted',
-  'rollingrestart.dialog.msg.componentsAtATime': '{0}s at a time',
+  'rollingrestart.dialog.msg.componentsAtATime': '{0} at a time',
   'rollingrestart.dialog.msg.timegap.prefix': 'Wait ',
   'rollingrestart.dialog.msg.timegap.suffix': 'seconds between batches ',
   'rollingrestart.dialog.msg.toleration.prefix': 'Tolerate up to ',
@@ -2930,7 +2929,7 @@ Em.I18n.translations = {
   'rollingrestart.dialog.err.invalid.toleratesize': 'Invalid failure toleration count: {0}',
   'rollingrestart.dialog.warn.datanode.batch.size': 'Restarting more than one DataNode at a time is not recommended. Doing so can lead to data unavailability and/or possible loss of data being actively written to HDFS.',
   'rollingrestart.dialog.msg.serviceNotInMM':'Note: This will trigger alerts. To suppress alerts, turn on Maintenance Mode for {0} prior to triggering a rolling restart',
-  'rollingrestart.dialog.msg.staleConfigsOnly': 'Only restart {0}s with stale configs',
+  'rollingrestart.dialog.msg.staleConfigsOnly': 'Only restart {0} with stale configs',
   'rollingrestart.rest.context': 'Rolling Restart of {0}s - batch {1} of {2}',
   'rollingrestart.context.allOnSelectedHosts':'Restart all components on the selected hosts',
   'rollingrestart.context.allForSelectedService':'Restart all components for {0}',
@@ -2962,7 +2961,7 @@ Em.I18n.translations = {
   'widget.create.wizard.step2.addExpression': 'Add Expression',
   'widget.create.wizard.step2.addDataset': 'Add data set',
   'widget.create.wizard.step2.body.gauge.overflow.warning':'Overflowed! Gauge can only display number between 0 and 1.',
-  'widget.create.wizard.step2.allComponents': 'All {0}s',
+  'widget.create.wizard.step2.allComponents': 'All {0}',
   'widget.create.wizard.step2.activeComponents': 'Active {0}',
   'widget.create.wizard.step2.noMetricFound': 'No metric found',
   'widget.create.wizard.step3.widgetName': 'Name',

http://git-wip-us.apache.org/repos/asf/ambari/blob/e44b8805/ambari-web/app/utils/string_utils.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/string_utils.js b/ambari-web/app/utils/string_utils.js
index 3754ba1..f4e3674 100644
--- a/ambari-web/app/utils/string_utils.js
+++ b/ambari-web/app/utils/string_utils.js
@@ -201,11 +201,8 @@ module.exports = {
    * @method pluralize
    */
   pluralize: function(count, singular, plural) {
-    plural = plural || singular + 's';
-    if (count > 1) {
-      return plural;
-    }
-    return singular;
+    var _plural = plural || pluralize(singular);
+    return count > 1 ? _plural : singular;
   },
 
   /**

http://git-wip-us.apache.org/repos/asf/ambari/blob/e44b8805/ambari-web/app/views/common/rolling_restart_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/common/rolling_restart_view.js b/ambari-web/app/views/common/rolling_restart_view.js
index 0d849a2..64b8610 100644
--- a/ambari-web/app/views/common/rolling_restart_view.js
+++ b/ambari-web/app/views/common/rolling_restart_view.js
@@ -119,7 +119,7 @@ App.RollingRestartView = Em.View.extend({
    * List of errors is saved to <code>errors</code>
    */
   validate : function() {
-    var displayName = this.get('hostComponentDisplayName');
+    var displayName = pluralize(this.get('hostComponentDisplayName'));
     var componentName = this.get('hostComponentName');
     var totalCount = this.get('restartHostComponents.length');
     var bs = this.get('batchSize');
@@ -207,7 +207,9 @@ App.RollingRestartView = Em.View.extend({
   /**
    * @type {String}
    */
-  restartMessage: Em.computed.i18nFormat('rollingrestart.dialog.msg.restart', 'hostComponentDisplayName'),
+  restartMessage : function() {
+    return Em.I18n.t('rollingrestart.dialog.msg.restart').format(pluralize(this.get('hostComponentDisplayName')));
+  }.property('hostComponentDisplayName'),
 
   /**
    * @type {String}
@@ -216,10 +218,7 @@ App.RollingRestartView = Em.View.extend({
     var count = this.get('componentsWithMaintenanceHost.length');
     if (count > 0) {
       var name = this.get('hostComponentDisplayName');
-      if (count > 1) {
-        return Em.I18n.t('rollingrestart.dialog.msg.maintainance.plural').format(count, name)
-      }
-      return Em.I18n.t('rollingrestart.dialog.msg.maintainance').format(count, name)
+      return Em.I18n.t('rollingrestart.dialog.msg.maintainance').format(count, pluralize(name));
     }
     return null;
   }.property('componentsWithMaintenanceHost', 'hostComponentDisplayName'),
@@ -227,11 +226,15 @@ App.RollingRestartView = Em.View.extend({
   /**
    * @type {String}
    */
-  batchSizeMessage: Em.computed.i18nFormat('rollingrestart.dialog.msg.componentsAtATime', 'hostComponentDisplayName'),
+  batchSizeMessage : function() {
+    return Em.I18n.t('rollingrestart.dialog.msg.componentsAtATime').format(pluralize(this.get('hostComponentDisplayName')));
+  }.property('hostComponentDisplayName'),
 
   /**
    * @type {String}
    */
-  staleConfigsOnlyMessage: Em.computed.i18nFormat('rollingrestart.dialog.msg.staleConfigsOnly', 'hostComponentDisplayName')
+  staleConfigsOnlyMessage : function() {
+    return Em.I18n.t('rollingrestart.dialog.msg.staleConfigsOnly').format(pluralize(this.get('hostComponentDisplayName')));
+  }.property('hostComponentDisplayName')
 
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/e44b8805/ambari-web/app/views/main/service/item.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/main/service/item.js b/ambari-web/app/views/main/service/item.js
index a007e17..fc9c4f3 100644
--- a/ambari-web/app/views/main/service/item.js
+++ b/ambari-web/app/views/main/service/item.js
@@ -146,9 +146,10 @@ App.MainServiceItemView = Em.View.extend({
       allSlaves.concat(allMasters).filter(function (_component) {
         return App.get('components.rollinRestartAllowed').contains(_component);
       }).forEach(function(_component) {
+        var _componentNamePluralized = pluralize(App.format.role(_component, false));
         options.push(self.createOption(actionMap.ROLLING_RESTART, {
           context: _component,
-          label: actionMap.ROLLING_RESTART.label.format(App.format.role(_component, false))
+          label: actionMap.ROLLING_RESTART.label.format(_componentNamePluralized)
         }));
       });
       allMasters.filter(function(master) {

http://git-wip-us.apache.org/repos/asf/ambari/blob/e44b8805/ambari-web/app/views/main/service/widgets/create/expression_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/main/service/widgets/create/expression_view.js b/ambari-web/app/views/main/service/widgets/create/expression_view.js
index a12bf99..7afe287 100644
--- a/ambari-web/app/views/main/service/widgets/create/expression_view.js
+++ b/ambari-web/app/views/main/service/widgets/create/expression_view.js
@@ -358,7 +358,7 @@ App.AddMetricExpressionView = Em.View.extend({
                 return Em.I18n.t('widget.create.wizard.step2.activeComponents').format(stackComponent.get('displayName'));
               }
             }
-            return Em.I18n.t('widget.create.wizard.step2.allComponents').format(stackComponent.get('displayName'));
+            return Em.I18n.t('widget.create.wizard.step2.allComponents').format(pluralize(stackComponent.get('displayName')));
           }.property('componentName', 'level'),
           count: servicesMap[serviceName].components[componentId].count,
           metrics: servicesMap[serviceName].components[componentId].metrics.uniq().sort(),

http://git-wip-us.apache.org/repos/asf/ambari/blob/e44b8805/ambari-web/brunch-config.js
----------------------------------------------------------------------
diff --git a/ambari-web/brunch-config.js b/ambari-web/brunch-config.js
index 64ac946..d71f8da 100644
--- a/ambari-web/brunch-config.js
+++ b/ambari-web/brunch-config.js
@@ -74,7 +74,8 @@ module.exports.config = {
           'vendor/scripts/spin.js',
           'vendor/scripts/jquery.flexibleArea.js',
           'vendor/scripts/FileSaver.js',
-          'vendor/scripts/Blob.js'
+          'vendor/scripts/Blob.js',
+          'vendor/scripts/pluralize.js'
         ]
       }
     },

http://git-wip-us.apache.org/repos/asf/ambari/blob/e44b8805/ambari-web/test/controllers/main/admin/highAvailability/resourceManager/wizard_controller_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/controllers/main/admin/highAvailability/resourceManager/wizard_controller_test.js b/ambari-web/test/controllers/main/admin/highAvailability/resourceManager/wizard_controller_test.js
index 5a991d3..19fbea6 100644
--- a/ambari-web/test/controllers/main/admin/highAvailability/resourceManager/wizard_controller_test.js
+++ b/ambari-web/test/controllers/main/admin/highAvailability/resourceManager/wizard_controller_test.js
@@ -18,7 +18,6 @@
 
 var App = require('app');
 require('controllers/main/admin/highAvailability/resourceManager/wizard_controller');
-var testHelpers = require('test/helpers');
 
 describe('App.RMHighAvailabilityWizardController', function () {
   var controller;

http://git-wip-us.apache.org/repos/asf/ambari/blob/e44b8805/ambari-web/test/models/cluster_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/cluster_test.js b/ambari-web/test/models/cluster_test.js
index a6bafba..604e50a 100644
--- a/ambari-web/test/models/cluster_test.js
+++ b/ambari-web/test/models/cluster_test.js
@@ -29,7 +29,7 @@ describe('App.Cluster', function () {
 
   describe('#isKerberosEnabled', function () {
 
-    var cases = [
+    [
       {
         securityType: 'KERBEROS',
         isKerberosEnabled: true,
@@ -40,9 +40,7 @@ describe('App.Cluster', function () {
         isKerberosEnabled: false,
         title: 'Kerberos disabled'
       }
-    ];
-
-    cases.forEach(function (item) {
+    ].forEach(function (item) {
 
       it(item.title, function () {
         cluster.set('securityType', item.securityType);
@@ -53,7 +51,7 @@ describe('App.Cluster', function () {
 
     describe('#isCredentialStorePersistent', function () {
 
-      var cases = [
+      [
         {
           propertyValue: 'false',
           isCredentialStorePersistent: false,
@@ -69,9 +67,7 @@ describe('App.Cluster', function () {
           isCredentialStorePersistent: true,
           title: 'persistent credential store'
         }
-      ];
-
-      cases.forEach(function (item) {
+      ].forEach(function (item) {
 
         it(item.title, function () {
           cluster.set('credentialStoreProperties', {

http://git-wip-us.apache.org/repos/asf/ambari/blob/e44b8805/ambari-web/test/models/configs/objects/service_config_property_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/configs/objects/service_config_property_test.js b/ambari-web/test/models/configs/objects/service_config_property_test.js
index 49613a44..ef0bd61 100644
--- a/ambari-web/test/models/configs/objects/service_config_property_test.js
+++ b/ambari-web/test/models/configs/objects/service_config_property_test.js
@@ -189,9 +189,7 @@ describe('App.ServiceConfigProperty', function () {
   App.TestAliases.testAsComputedAnd(getProperty(), 'hideFinalIcon', ['!isFinal', 'isNotEditable']);
 
   describe('#placeholder', function () {
-    it('should equal foo', function() {
-      serviceConfigProperty.set('isEditable', true);
-      var testCases = [
+      [
         {
           placeholderText: 'foo',
           savedValue: ''
@@ -204,26 +202,19 @@ describe('App.ServiceConfigProperty', function () {
           placeholderText: 'foo',
           savedValue: 'bar'
         }
-      ];
-      testCases.forEach(function (item) {
-        serviceConfigProperty.set('placeholderText', item.placeholderText);
-        serviceConfigProperty.set('savedValue', item.savedValue);
-        expect(serviceConfigProperty.get('placeholder')).to.equal('foo');
-      });
+      ].forEach(function (item) {
+        it('should equal foo, placeholder = ' + JSON.stringify(item.placeholderText), function() {
+          serviceConfigProperty.set('isEditable', true);
+          serviceConfigProperty.set('placeholderText', item.placeholderText);
+          serviceConfigProperty.set('savedValue', item.savedValue);
+          expect(serviceConfigProperty.get('placeholder')).to.equal('foo');
+        });
     });
     it('should equal null', function() {
       serviceConfigProperty.set('isEditable', false);
-      var testCases = [
-        {
-          placeholderText: 'foo',
-          savedValue: 'bar'
-        }
-      ];
-      testCases.forEach(function (item) {
-        serviceConfigProperty.set('placeholderText', item.placeholderText);
-        serviceConfigProperty.set('savedValue', item.savedValue);
-        expect(serviceConfigProperty.get('placeholder')).to.equal(null);
-      });
+      serviceConfigProperty.set('placeholderText', 'foo');
+      serviceConfigProperty.set('savedValue', 'bar');
+      expect(serviceConfigProperty.get('placeholder')).to.equal(null);
     });
   });
   describe('#isPropertyOverridable', function () {

http://git-wip-us.apache.org/repos/asf/ambari/blob/e44b8805/ambari-web/test/models/configs/theme/sub_section_tab_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/configs/theme/sub_section_tab_test.js b/ambari-web/test/models/configs/theme/sub_section_tab_test.js
index 6044432..0c3b98c 100644
--- a/ambari-web/test/models/configs/theme/sub_section_tab_test.js
+++ b/ambari-web/test/models/configs/theme/sub_section_tab_test.js
@@ -155,7 +155,7 @@ describe('App.SubSectionTab', function () {
 
     it('should include visible properties with errors', function () {
       subSectionTab.set('configs', configs);
-      expect(subSectionTab.get('errorsCount')).to.eql(8);
+      expect(subSectionTab.get('errorsCount')).to.be.equal(8);
     });
 
   });

http://git-wip-us.apache.org/repos/asf/ambari/blob/e44b8805/ambari-web/test/views/main/host/log_metrics_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/views/main/host/log_metrics_test.js b/ambari-web/test/views/main/host/log_metrics_test.js
index a0a3c6c..52f4e55 100644
--- a/ambari-web/test/views/main/host/log_metrics_test.js
+++ b/ambari-web/test/views/main/host/log_metrics_test.js
@@ -17,7 +17,6 @@
  */
 
 var App = require('app');
-var fileUtils = require('utils/file_utils');
 
 describe('App.MainHostLogMetrics', function() {
   var view;

http://git-wip-us.apache.org/repos/asf/ambari/blob/e44b8805/ambari-web/test/views/main/host_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/views/main/host_test.js b/ambari-web/test/views/main/host_test.js
index 0b789f6..79cc65c 100644
--- a/ambari-web/test/views/main/host_test.js
+++ b/ambari-web/test/views/main/host_test.js
@@ -773,7 +773,7 @@ describe('App.MainHostView', function () {
     describe("#restartRequiredComponentsMessage", function () {
 
       it("5 components require restart", function() {
-        var content = 'c1, c2, c3, c4, c5' + ' ' + Em.I18n.t('common.components').toLowerCase();
+        var content = 'c1, c2, c3, c4, c5 ' + Em.I18n.t('common.components').toLowerCase();
         hostView.set('content.componentsWithStaleConfigsCount', 5);
         hostView.set('content.componentsWithStaleConfigs', [
           {displayName: 'c1'},
@@ -789,7 +789,7 @@ describe('App.MainHostView', function () {
       });
 
       it("1 component require restart", function() {
-        var content = 'c1' + ' ' + Em.I18n.t('common.component').toLowerCase();
+        var content = 'c1 ' + Em.I18n.t('common.component').toLowerCase();
         hostView.set('content.componentsWithStaleConfigsCount', 1);
         hostView.set('content.componentsWithStaleConfigs', [
           {displayName: 'c1'}

http://git-wip-us.apache.org/repos/asf/ambari/blob/e44b8805/ambari-web/vendor/scripts/pluralize.js
----------------------------------------------------------------------
diff --git a/ambari-web/vendor/scripts/pluralize.js b/ambari-web/vendor/scripts/pluralize.js
new file mode 100644
index 0000000..7246db1
--- /dev/null
+++ b/ambari-web/vendor/scripts/pluralize.js
@@ -0,0 +1,461 @@
+/* global define */
+
+(function (root, pluralize) {
+  /* istanbul ignore else */
+  if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') {
+    // Node.
+    module.exports = pluralize();
+  } else if (typeof define === 'function' && define.amd) {
+    // AMD, registers as an anonymous module.
+    define(function () {
+      return pluralize();
+    });
+  } else {
+    // Browser global.
+    root.pluralize = pluralize();
+  }
+})(this, function () {
+  // Rule storage - pluralize and singularize need to be run sequentially,
+  // while other rules can be optimized using an object for instant lookups.
+  var pluralRules = [];
+  var singularRules = [];
+  var uncountables = {};
+  var irregularPlurals = {};
+  var irregularSingles = {};
+
+  /**
+   * Title case a string.
+   *
+   * @param  {string} str
+   * @return {string}
+   */
+  function toTitleCase (str) {
+    return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase();
+  }
+
+  /**
+   * Sanitize a pluralization rule to a usable regular expression.
+   *
+   * @param  {(RegExp|string)} rule
+   * @return {RegExp}
+   */
+  function sanitizeRule (rule) {
+    if (typeof rule === 'string') {
+      return new RegExp('^' + rule + '$', 'i');
+    }
+
+    return rule;
+  }
+
+  /**
+   * Pass in a word token to produce a function that can replicate the case on
+   * another word.
+   *
+   * @param  {string}   word
+   * @param  {string}   token
+   * @return {Function}
+   */
+  function restoreCase (word, token) {
+    // Tokens are an exact match.
+    if (word === token) {
+      return token;
+    }
+
+    // Upper cased words. E.g. "HELLO".
+    if (word === word.toUpperCase()) {
+      return token.toUpperCase();
+    }
+
+    // Title cased words. E.g. "Title".
+    if (word[0] === word[0].toUpperCase()) {
+      return toTitleCase(token);
+    }
+
+    // Lower cased words. E.g. "test".
+    return token.toLowerCase();
+  }
+
+  /**
+   * Interpolate a regexp string.
+   *
+   * @param  {string} str
+   * @param  {Array}  args
+   * @return {string}
+   */
+  function interpolate (str, args) {
+    return str.replace(/\$(\d{1,2})/g, function (match, index) {
+      return args[index] || '';
+    });
+  }
+
+  /**
+   * Sanitize a word by passing in the word and sanitization rules.
+   *
+   * @param  {string}   token
+   * @param  {string}   word
+   * @param  {Array}    collection
+   * @return {string}
+   */
+  function sanitizeWord (token, word, collection) {
+    // Empty string or doesn't need fixing.
+    if (!token.length || uncountables.hasOwnProperty(token)) {
+      return word;
+    }
+
+    var len = collection.length;
+
+    // Iterate over the sanitization rules and use the first one to match.
+    while (len--) {
+      var rule = collection[len];
+
+      // If the rule passes, return the replacement.
+      if (rule[0].test(word)) {
+        return word.replace(rule[0], function (match, index, word) {
+          var result = interpolate(rule[1], arguments);
+
+          if (match === '') {
+            return restoreCase(word[index - 1], result);
+          }
+
+          return restoreCase(match, result);
+        });
+      }
+    }
+
+    return word;
+  }
+
+  /**
+   * Replace a word with the updated word.
+   *
+   * @param  {Object}   replaceMap
+   * @param  {Object}   keepMap
+   * @param  {Array}    rules
+   * @return {Function}
+   */
+  function replaceWord (replaceMap, keepMap, rules) {
+    return function (word) {
+      // Get the correct token and case restoration functions.
+      var token = word.toLowerCase();
+
+      // Check against the keep object map.
+      if (keepMap.hasOwnProperty(token)) {
+        return restoreCase(word, token);
+      }
+
+      // Check against the replacement map for a direct word replacement.
+      if (replaceMap.hasOwnProperty(token)) {
+        return restoreCase(word, replaceMap[token]);
+      }
+
+      // Run all the rules against the word.
+      return sanitizeWord(token, word, rules);
+    };
+  }
+
+  /**
+   * Pluralize or singularize a word based on the passed in count.
+   *
+   * @param  {string}  word
+   * @param  {number}  count
+   * @param  {boolean} inclusive
+   * @return {string}
+   */
+  function pluralize (word, count, inclusive) {
+    var pluralized = count === 1
+      ? pluralize.singular(word) : pluralize.plural(word);
+
+    return (inclusive ? count + ' ' : '') + pluralized;
+  }
+
+  /**
+   * Pluralize a word.
+   *
+   * @type {Function}
+   */
+  pluralize.plural = replaceWord(
+    irregularSingles, irregularPlurals, pluralRules
+  );
+
+  /**
+   * Singularize a word.
+   *
+   * @type {Function}
+   */
+  pluralize.singular = replaceWord(
+    irregularPlurals, irregularSingles, singularRules
+  );
+
+  /**
+   * Add a pluralization rule to the collection.
+   *
+   * @param {(string|RegExp)} rule
+   * @param {string}          replacement
+   */
+  pluralize.addPluralRule = function (rule, replacement) {
+    pluralRules.push([sanitizeRule(rule), replacement]);
+  };
+
+  /**
+   * Add a singularization rule to the collection.
+   *
+   * @param {(string|RegExp)} rule
+   * @param {string}          replacement
+   */
+  pluralize.addSingularRule = function (rule, replacement) {
+    singularRules.push([sanitizeRule(rule), replacement]);
+  };
+
+  /**
+   * Add an uncountable word rule.
+   *
+   * @param {(string|RegExp)} word
+   */
+  pluralize.addUncountableRule = function (word) {
+    if (typeof word === 'string') {
+      uncountables[word.toLowerCase()] = true;
+      return;
+    }
+
+    // Set singular and plural references for the word.
+    pluralize.addPluralRule(word, '$0');
+    pluralize.addSingularRule(word, '$0');
+  };
+
+  /**
+   * Add an irregular word definition.
+   *
+   * @param {string} single
+   * @param {string} plural
+   */
+  pluralize.addIrregularRule = function (single, plural) {
+    plural = plural.toLowerCase();
+    single = single.toLowerCase();
+
+    irregularSingles[single] = plural;
+    irregularPlurals[plural] = single;
+  };
+
+  /**
+   * Irregular rules.
+   */
+  [
+    // Pronouns.
+    ['I', 'we'],
+    ['me', 'us'],
+    ['he', 'they'],
+    ['she', 'they'],
+    ['them', 'them'],
+    ['myself', 'ourselves'],
+    ['yourself', 'yourselves'],
+    ['itself', 'themselves'],
+    ['herself', 'themselves'],
+    ['himself', 'themselves'],
+    ['themself', 'themselves'],
+    ['is', 'are'],
+    ['was', 'were'],
+    ['has', 'have'],
+    ['this', 'these'],
+    ['that', 'those'],
+    // Words ending in with a consonant and `o`.
+    ['echo', 'echoes'],
+    ['dingo', 'dingoes'],
+    ['volcano', 'volcanoes'],
+    ['tornado', 'tornadoes'],
+    ['torpedo', 'torpedoes'],
+    // Ends with `us`.
+    ['genus', 'genera'],
+    ['viscus', 'viscera'],
+    // Ends with `ma`.
+    ['stigma', 'stigmata'],
+    ['stoma', 'stomata'],
+    ['dogma', 'dogmata'],
+    ['lemma', 'lemmata'],
+    ['schema', 'schemata'],
+    ['anathema', 'anathemata'],
+    // Other irregular rules.
+    ['ox', 'oxen'],
+    ['axe', 'axes'],
+    ['die', 'dice'],
+    ['yes', 'yeses'],
+    ['foot', 'feet'],
+    ['eave', 'eaves'],
+    ['goose', 'geese'],
+    ['tooth', 'teeth'],
+    ['quiz', 'quizzes'],
+    ['human', 'humans'],
+    ['proof', 'proofs'],
+    ['carve', 'carves'],
+    ['valve', 'valves'],
+    ['looey', 'looies'],
+    ['thief', 'thieves'],
+    ['groove', 'grooves'],
+    ['pickaxe', 'pickaxes'],
+    ['whiskey', 'whiskies']
+  ].forEach(function (rule) {
+    return pluralize.addIrregularRule(rule[0], rule[1]);
+  });
+
+  /**
+   * Pluralization rules.
+   */
+  [
+    [/s?$/i, 's'],
+    [/([^aeiou]ese)$/i, '$1'],
+    [/(ax|test)is$/i, '$1es'],
+    [/(alias|[^aou]us|tlas|gas|ris)$/i, '$1es'],
+    [/(e[mn]u)s?$/i, '$1s'],
+    [/([^l]ias|[aeiou]las|[emjzr]as|[iu]am)$/i, '$1'],
+    [/(alumn|syllab|octop|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i, '$1i'],
+    [/(alumn|alg|vertebr)(?:a|ae)$/i, '$1ae'],
+    [/(seraph|cherub)(?:im)?$/i, '$1im'],
+    [/(her|at|gr)o$/i, '$1oes'],
+    [/(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|automat|quor)(?:a|um)$/i, '$1a'],
+    [/(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)(?:a|on)$/i, '$1a'],
+    [/sis$/i, 'ses'],
+    [/(?:(kni|wi|li)fe|(ar|l|ea|eo|oa|hoo)f)$/i, '$1$2ves'],
+    [/([^aeiouy]|qu)y$/i, '$1ies'],
+    [/([^ch][ieo][ln])ey$/i, '$1ies'],
+    [/(x|ch|ss|sh|zz)$/i, '$1es'],
+    [/(matr|cod|mur|sil|vert|ind|append)(?:ix|ex)$/i, '$1ices'],
+    [/(m|l)(?:ice|ouse)$/i, '$1ice'],
+    [/(pe)(?:rson|ople)$/i, '$1ople'],
+    [/(child)(?:ren)?$/i, '$1ren'],
+    [/eaux$/i, '$0'],
+    [/m[ae]n$/i, 'men'],
+    ['thou', 'you']
+  ].forEach(function (rule) {
+    return pluralize.addPluralRule(rule[0], rule[1]);
+  });
+
+  /**
+   * Singularization rules.
+   */
+  [
+    [/s$/i, ''],
+    [/(ss)$/i, '$1'],
+    [/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(?:sis|ses)$/i, '$1sis'],
+    [/(^analy)(?:sis|ses)$/i, '$1sis'],
+    [/(wi|kni|(?:after|half|high|low|mid|non|night|[^\w]|^)li)ves$/i, '$1fe'],
+    [/(ar|(?:wo|[ae])l|[eo][ao])ves$/i, '$1f'],
+    [/ies$/i, 'y'],
+    [/\b([pl]|zomb|(?:neck|cross)?t|coll|faer|food|gen|goon|group|lass|talk|goal|cut)ies$/i, '$1ie'],
+    [/\b(mon|smil)ies$/i, '$1ey'],
+    [/(m|l)ice$/i, '$1ouse'],
+    [/(seraph|cherub)im$/i, '$1'],
+    [/(x|ch|ss|sh|zz|tto|go|cho|alias|[^aou]us|tlas|gas|(?:her|at|gr)o|ris)(?:es)?$/i, '$1'],
+    [/(e[mn]u)s?$/i, '$1'],
+    [/(movie|twelve)s$/i, '$1'],
+    [/(cris|test|diagnos)(?:is|es)$/i, '$1is'],
+    [/(alumn|syllab|octop|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i, '$1us'],
+    [/(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|quor)a$/i, '$1um'],
+    [/(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)a$/i, '$1on'],
+    [/(alumn|alg|vertebr)ae$/i, '$1a'],
+    [/(cod|mur|sil|vert|ind)ices$/i, '$1ex'],
+    [/(matr|append)ices$/i, '$1ix'],
+    [/(pe)(rson|ople)$/i, '$1rson'],
+    [/(child)ren$/i, '$1'],
+    [/(eau)x?$/i, '$1'],
+    [/men$/i, 'man']
+  ].forEach(function (rule) {
+    return pluralize.addSingularRule(rule[0], rule[1]);
+  });
+
+  /**
+   * Uncountable rules.
+   */
+  [
+    // Singular words with no plurals.
+    'advice',
+    'adulthood',
+    'agenda',
+    'aid',
+    'alcohol',
+    'ammo',
+    'athletics',
+    'bison',
+    'blood',
+    'bream',
+    'buffalo',
+    'butter',
+    'carp',
+    'cash',
+    'chassis',
+    'chess',
+    'clothing',
+    'commerce',
+    'cod',
+    'cooperation',
+    'corps',
+    'digestion',
+    'debris',
+    'diabetes',
+    'energy',
+    'equipment',
+    'elk',
+    'excretion',
+    'expertise',
+    'flounder',
+    'fun',
+    'gallows',
+    'garbage',
+    'graffiti',
+    'headquarters',
+    'health',
+    'herpes',
+    'highjinks',
+    'homework',
+    'housework',
+    'information',
+    'jeans',
+    'justice',
+    'kudos',
+    'labour',
+    'literature',
+    'machinery',
+    'mackerel',
+    'mail',
+    'media',
+    'mews',
+    'moose',
+    'music',
+    'news',
+    'pike',
+    'plankton',
+    'pliers',
+    'pollution',
+    'premises',
+    'rain',
+    'research',
+    'rice',
+    'salmon',
+    'scissors',
+    'series',
+    'sewage',
+    'shambles',
+    'shrimp',
+    'species',
+    'staff',
+    'swine',
+    'trout',
+    'traffic',
+    'transporation',
+    'tuna',
+    'wealth',
+    'welfare',
+    'whiting',
+    'wildebeest',
+    'wildlife',
+    'you',
+    // Regexes.
+    /pox$/i, // "chickpox", "smallpox"
+    /ois$/i,
+    /deer$/i, // "deer", "reindeer"
+    /fish$/i, // "fish", "blowfish", "angelfish"
+    /sheep$/i,
+    /measles$/i,
+    /[^aeiou]ese$/i // "chinese", "japanese"
+  ].forEach(pluralize.addUncountableRule);
+
+  return pluralize;
+});
\ No newline at end of file