Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support pluralize method expected by web app #2

Merged
merged 4 commits into from
Feb 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ s.get(Strings.EXTENDED_AMOUNT) //=> Revenue
Each method can also accept an additional options object supporting the following properties:

- `plural` (boolean) or `count` (number): to more easily support conditional plurality using `get()`
- `suffix` (string): to customize what gets added to the value when auto-pluralization is used
- `lc` (boolean) or `uc` (boolean): to transform the display value to lowercase or uppercase, respectively
- `abbrev` (boolean): to abbreviate the display value to the first letter of each word in the string
- `strict` (boolean, default `true`): to use an empty string (strict=true) or key (strict=false) when key not found in strings or defaults
- `locale` (string): to make sure case-sensitivity respects rules and characters for the user's language and region

## Primary API
Expand All @@ -94,9 +96,11 @@ For convenience, the first two arguments are interchangeable.

- `plural` (boolean): whether to use the plural form or not (mutually exclusive with `count`)
- `count` (number): number of items that should be translated for plurality (mutually exclusive with `plural`)
- `suffix` (string): a custom suffix to add to the display string value when plurality is needed and no explicit plural value is defined
- `lc` (boolean): transform the display string to lowercase (mutually exclusive with `uc`)
- `uc` (boolean): transform the display string to uppercase (mutually exclusive with `lc`)
- `abbrev` (boolean): transform the display string to its abbreviated form
- `strict` (boolean, default `true`): whether keys should be interpreted strictly (required in strings or defaults) or loosely (not required in strings or defaults) - in strict mode, an empty string will be returned when key is not found; in loose mode, the key will be used as the value when key is not found
- `locale` (string): the user's locale e.g. `'en-US'` or `'en_US'`

### `Strings.getSingular(strings, key, opts)`
Expand Down Expand Up @@ -155,7 +159,9 @@ Shortcut to get the plural value defined for `key`. If a singular value is defin

## Supported Keys

- `Strings.ADJUSTMENT`
- `Strings.ANNUAL_CONTRACT_VALUE`
- `Strings.BATCH`
- `Strings.CATEGORY`
- `Strings.CLOSED`
- `Strings.COMPENSATION`
Expand Down Expand Up @@ -184,6 +190,10 @@ Abbreviate the given string by grabbing the first letter/character of each word,

E.g. turns `'Annual Contract Value'` into `'ACV'`

### `Strings.formatInt(int, locale)`

Attempts to format the given integer into a string per the given locale.

### `Strings.isUpper(str, locale)`

Returns a boolean indicating whether the given string is uppercase (in the given locale) or not.
Expand All @@ -194,16 +204,30 @@ Naively attempts to normalize the given locale string into the language tag form

E.g. turns `'en_US'` into `'en-US'`

### `Strings.pluralize(str, locale)`
### `Strings.pluralize(count, noun, opts)`

Attempts to apply pluralization to the given string in a case-sensitive manner.
Combine the given count and noun into a single formatted string, pluralizing the noun if necessary.

E.g. turns `'plan'` into `'plans'`, turns `'box'` into `'boxes'`, and turns `'fly'` into `'flies'`
Supported options include:

- `suffix` (string): add this to the string instead of making a best-guess effort when pluralization is needed
- `locale` (string): to format the integer and respect case-sensitivity in a language-specific way

### `Strings.toLower(str, locale)`

Attempts to safely transform the given string into lowercase. If a locale is given, it will be normalized. If the locale-specific operation fails, it will fall back to a locale-agnostic operation.

### `Strings.toPlural(str, opts)`

Transforms the given string into its plural form, respecting case.

E.g. turns `'plan'` into `'plans'`, turns `'box'` into `'boxes'`, and turns `'fly'` into `'flies'`

Supported options include:

- `suffix` (string): add this to the string instead of making a best-guess effort
- `locale` (string): to respect case-sensitivity in a language-specific way

### `Strings.toUpper(str, locale)`

Attempts to safely transform the given string into uppercase. If a locale is given, it will be normalized. If the locale-specific operation fails, it will fall back to a locale-agnostic operation.
Expand Down
98 changes: 77 additions & 21 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,66 @@ class Strings {
return String(c) !== Strings.toLower(c, locale)
}

static pluralize (str, locale) {
// valid opts:
// - suffix (string, default based on str) to customize pluralization
// - locale (string, no default) to respect user's language + region
static toPlural (str, opts) {
if (!str) return ''
let add = 's'
opts = opts || {}
str = String(str)
const last = str.slice(-1)
if (~['y', 'Y'].indexOf(last)) {
str = str.slice(0, -1)
add = 'ies'
} else if (~['j', 'o', 's', 'x', 'z', 'J', 'O', 'S', 'X', 'Z'].indexOf(last)) {
add = 'es'
let add
if (opts.suffix) add = opts.suffix
else {
add = 's'
if (~['y', 'Y'].indexOf(last)) {
str = str.slice(0, -1)
add = 'ies'
} else if (~['h', 'j', 'o', 's', 'x', 'z', 'H', 'J', 'O', 'S', 'X', 'Z'].indexOf(last)) {
add = 'es'
}
}
return Strings.isUpper(last, locale) ? str + Strings.toUpper(add, locale) : str + add
return Strings.isUpper(last, opts.locale) ? str + Strings.toUpper(add, opts.locale) : str + add
}

static formatInt (integer, locale) {
try {
return new Intl.NumberFormat(Strings.normalizeLocale(locale), { maximumFractionDigits: 0 }).format(integer)
} catch (_) {}
return String(integer)
}

// valid opts:
// - suffix (string, default based on str) to customize pluralization
// - locale (string, no default) to respect user's language + region
static pluralize (count, noun, opts) {
if (!noun) return ''
noun = String(noun)
if (typeof count !== 'number') count = Number(count)
if (Number.isNaN(count)) count = 0
let suffix, locale
if (typeof opts === 'string') {
suffix = opts
} else {
opts = opts || {}
suffix = opts.suffix
locale = opts.locale
}
return Strings.formatInt(count, locale) + ' ' + (count !== 1 ? Strings.toPlural(noun, { suffix, locale }) : noun)
}

static abbreviate (str) {
if (!str) return ''
return String(str).split(/\s/).reduce((abbr, word) => abbr + word.slice(0, 1), '')
}

// valid opts: plural (boolean), count (number), locale (string), lc (boolean), uc (boolean), abbrev (boolean)
// valid opts:
// - plural (boolean, no default) or count (number, no default) to determine whether singular or plural value should be used
// - suffix (string, default based on value) to customize auto-pluralization
// - lc (boolean, default false) or uc (boolean, default false) to transform value to lower or upper case before returning
// - abbrev (boolean, default false) to abbreviate the value before returning
// - strict (boolean, default true) to return empty string if key not in strings nor in DEFAULTS
// - locale (string, no default) to respect user's language + region
static get (strings, key, opts) {
// allow first two args to be interchangeable
if (typeof strings === 'string') {
Expand All @@ -66,24 +106,27 @@ class Strings {
if (typeof opts === 'boolean') opts = { plural: opts }
else if (typeof opts === 'number') opts = { count: opts }
else opts = opts || {}
// check for locale
const locale = opts.locale || strings.locale
if (typeof opts.strict !== 'boolean') opts.strict = true
// get value (object or singular string)
let val = typeof strings.strings === 'object' ? strings.strings[key] : strings[key]
if (val == null) val = Strings.DEFAULTS[key]

// if (val == null) val = key
if (val == null) return ''
if (val == null) {
if (opts.strict) return ''
val = key
}

// check for locale
const locale = opts.locale || strings.locale
// determine plurality
let usePlural = false
if (typeof opts.plural === 'boolean') usePlural = opts.plural
else if (typeof opts.count === 'number') usePlural = opts.count !== 1
// extract value
if (usePlural) {
if (typeof val === 'string') val = Strings.pluralize(val, locale)
if (typeof val === 'string') val = Strings.toPlural(val, { locale, suffix: opts.suffix })
else if (val.plural || val.other) val = val.plural || val.other
else if (val.singular || val.one) val = Strings.pluralize(val.singular || val.one, locale)
else if (val.singular || val.one) val = Strings.toPlural(val.singular || val.one, { locale, suffix: opts.suffix })
} else if (typeof val !== 'string' && (val.singular || val.one)) {
val = val.singular || val.one
}
Expand All @@ -94,12 +137,21 @@ class Strings {
return opts.abbrev ? Strings.abbreviate(val) : val
}

// valid opts: locale (string), lc (boolean), uc (boolean), abbrev (boolean)
// valid opts:
// - lc (boolean, default false) or uc (boolean, default false) to transform value to lower or upper case before returning
// - abbrev (boolean, default false) to abbreviate the value before returning
// - strict (boolean, default true) to return empty string if key not in strings nor in DEFAULTS
// - locale (string, no default) to respect user's language + region
static getSingular (strings, key, opts) {
return Strings.get(strings, key, Object.assign({}, opts, { plural: false }))
}

// valid opts: locale (string), lc (boolean), uc (boolean), abbrev (boolean)
// valid opts:
// - suffix (string, default based on value) to customize auto-pluralization
// - lc (boolean, default false) or uc (boolean, default false) to transform value to lower or upper case before returning
// - abbrev (boolean, default false) to abbreviate the value before returning
// - strict (boolean, default true) to return empty string if key not in strings nor in DEFAULTS
// - locale (string, no default) to respect user's language + region
static getPlural (strings, key, opts) {
return Strings.get(strings, key, Object.assign({}, opts, { plural: true }))
}
Expand All @@ -112,17 +164,17 @@ class Strings {
this.strings = strings
}

// valid opts: plural (boolean), count (number), locale (string), lc (boolean), uc (boolean), abbrev (boolean)
// valid opts: plural (boolean), count (number), suffix (string), lc (boolean), uc (boolean), abbrev (boolean), strict (boolean), locale (string)
get (key, opts) {
return Strings.get(this.strings, key, opts)
}

// valid opts: locale (string), lc (boolean), uc (boolean), abbrev (boolean)
// valid opts: lc (boolean), uc (boolean), abbrev (boolean), strict (boolean), locale (string)
getSingular (key, opts) {
return Strings.getSingular(this.strings, key, opts)
}

// valid opts: locale (string), lc (boolean), uc (boolean), abbrev (boolean)
// valid opts: suffix (string), lc (boolean), uc (boolean), abbrev (boolean), strict (boolean), locale (string)
getPlural (key, opts) {
return Strings.getPlural(this.strings, key, opts)
}
Expand All @@ -148,6 +200,8 @@ Strings.DISPUTE = 'dispute'
Strings.MEMBER = 'member'
Strings.REP = 'rep'
Strings.TEAM = 'team'
Strings.ADJUSTMENT = 'adjustment'
Strings.BATCH = 'batch'

Strings.DEFAULTS = {
[Strings.GROSS_MARGIN]: {
Expand Down Expand Up @@ -196,7 +250,9 @@ Strings.DEFAULTS = {
[Strings.DISPUTE]: 'Dispute',
[Strings.MEMBER]: 'Member',
[Strings.REP]: 'Rep',
[Strings.TEAM]: 'Team'
[Strings.TEAM]: 'Team',
[Strings.ADJUSTMENT]: 'Adjustment',
[Strings.BATCH]: 'Batch'
}

module.exports = Strings
Loading