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 <[email protected]> Authored: Wed Sep 28 11:41:20 2016 +0300 Committer: Oleg Nechiporenko <[email protected]> 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
