Skip to content

Commit

Permalink
Merge branch 'master' into test262
Browse files Browse the repository at this point in the history
  • Loading branch information
eemeli committed Sep 3, 2022
2 parents 5b264e8 + fd7ce84 commit 58fa97d
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 38 deletions.
31 changes: 15 additions & 16 deletions src/factory.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
// does not check for duplicate subtags
const isStructurallyValidLanguageTag = locale =>
locale.split('-').every(subtag => /[a-z0-9]+/i.test(subtag))

const canonicalizeLocaleList = locales => {
if (!locales) return []
if (!Array.isArray(locales)) locales = [locales]
Expand All @@ -16,25 +12,27 @@ const canonicalizeLocaleList = locales => {
const msg = `Locales should be strings, ${JSON.stringify(tag)} isn't.`
throw new TypeError(msg)
}
if (tag[0] === '*') continue
if (!isStructurallyValidLanguageTag(tag)) {

const parts = tag.split('-')

// does not check for duplicate subtags
if (!parts.every(subtag => /[a-z0-9]+/i.test(subtag))) {
const strTag = JSON.stringify(tag)
const msg = `The locale ${strTag} is not a structurally valid BCP 47 language tag.`
throw new RangeError(msg)
}
res[tag] = true

// always use lower case for primary language subtag
let lc = parts[0].toLowerCase()
// replace deprecated codes for Indonesian, Hebrew & Yiddish
parts[0] = { in: 'id', iw: 'he', ji: 'yi' }[lc] ?? lc

res[parts.join('-')] = true
}
return Object.keys(res)
}

const defaultLocale = () =>
/* istanbul ignore next */
(typeof navigator !== 'undefined' &&
navigator &&
(navigator.userLanguage || navigator.language)) ||
'en-US'

const getType = opt => {
function getType(opt) {
const type = Object.prototype.hasOwnProperty.call(opt, 'type') && opt.type
if (!type) return 'cardinal'
if (type === 'cardinal' || type === 'ordinal') return type
Expand Down Expand Up @@ -72,7 +70,8 @@ export default function getPluralRules(
const lc = findLocale(canonicalLocales[i])
if (lc) return lc
}
return findLocale(defaultLocale())
const lc = new NumberFormat().resolvedOptions().locale
return findLocale(lc)
}

class PluralRules {
Expand Down
2 changes: 1 addition & 1 deletion src/plural-rules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const NumberFormat =
(typeof Intl === 'object' && Intl.NumberFormat) || PseudoNumberFormat

// make-plural exports are cast with safe-identifier to be valid JS identifiers
const id = lc => (lc === 'in' ? '_in' : lc === 'pt-PT' ? 'pt_PT' : lc)
const id = lc => (lc === 'pt-PT' ? 'pt_PT' : lc)

const getSelector = lc => Plurals[id(lc)]
const getCategories = (lc, ord) =>
Expand Down
73 changes: 54 additions & 19 deletions src/plural-rules.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ function suite(PluralRules) {
const res = PluralRules.supportedLocalesOf(locales)
expect(res).toMatchObject(locales)
})
test('should ignore wildcards', () => {
const locales = ['en', '*', '*-foo', 'fi-FI']
const res = PluralRules.supportedLocalesOf(locales)
expect(res).toMatchObject(['en', 'fi-FI'])
})
test('should accept String objects', () => {
const res = PluralRules.supportedLocalesOf(new String('en'))
expect(res).toMatchObject(['en'])
Expand All @@ -39,6 +34,7 @@ function suite(PluralRules) {
test('should complain about bad tags', () => {
expect(() => PluralRules.supportedLocalesOf('en-')).toThrow(RangeError)
expect(() => PluralRules.supportedLocalesOf('-en')).toThrow(RangeError)
expect(() => PluralRules.supportedLocalesOf('*-en')).toThrow(RangeError)
})
})

Expand All @@ -56,27 +52,16 @@ function suite(PluralRules) {
expect(typeof opt.locale).toBe('string')
expect(opt.locale.length).toBeGreaterThan(1)
})
test('should use navigator.language for default locale', () => {
const spy = jest.spyOn(navigator, 'language', 'get')
try {
spy.mockReturnValue('fi-FI')
const p = new PluralRules()
const opt = p.resolvedOptions()
expect(opt.locale).toMatch(/^fi\b/)
} finally {
spy.mockRestore()
}
})
test('should handle valid simple arguments correctly', () => {
const p = new PluralRules('pt-PT', { type: 'ordinal' })
const p = new PluralRules('PT-PT', { type: 'ordinal' })
expect(p).toBeInstanceOf(Object)
expect(p.select).toBeInstanceOf(Function)
const opt = p.resolvedOptions()
expect(opt.type).toBe('ordinal')
expect(opt.locale).toMatch(/^pt\b/)
})
test('should choose a locale correctly from multiple choices', () => {
const p = new PluralRules(['tlh', 'id', 'en'])
const p = new PluralRules(['tlh', 'IN', 'en'])
expect(p).toBeInstanceOf(Object)
expect(p.select).toBeInstanceOf(Function)
const opt = p.resolvedOptions()
Expand Down Expand Up @@ -235,7 +220,7 @@ function suite(PluralRules) {
describe('With native Intl.NumberFormat', () => suite(ActualPluralRules))

describe('With PseudoNumberFormat', () => {
const id = lc => (lc === 'in' ? '_in' : lc === 'pt-PT' ? 'pt_PT' : lc)
const id = lc => (lc === 'pt-PT' ? 'pt_PT' : lc)
const getSelector = lc => Plurals[id(lc)]
const getCategories = (lc, ord) =>
Categories[id(lc)][ord ? 'ordinal' : 'cardinal']
Expand All @@ -247,4 +232,54 @@ describe('With PseudoNumberFormat', () => {
getRangeSelector
)
suite(PluralRules)

describe('default locale', () => {
test('should use same default locale as other Intl formatters', () => {
const Intl_ = global.Intl
try {
class MockFormat {
resolvedOptions = () => ({ locale: 'fi-FI' })
}
global.Intl = { DateTimeFormat: MockFormat, NumberFormat: MockFormat }
const p = new PluralRules()
const opt = p.resolvedOptions()
expect(opt.locale).toMatch(/^fi\b/)
} finally {
global.Intl = Intl_
}
})
test('should use navigator.language as fallback', () => {
const Intl_ = global.Intl
const spy = jest.spyOn(navigator, 'language', 'get')
try {
delete global.Intl
spy.mockReturnValue('fi-FI')
const p0 = new PluralRules()
const opt0 = p0.resolvedOptions()
expect(opt0.locale).toMatch(/^fi\b/)

spy.mockReturnValue(undefined)
const p1 = new PluralRules()
const opt1 = p1.resolvedOptions()
expect(opt1.locale).toMatch(/^en\b/)
} finally {
global.Intl = Intl_
spy.mockRestore()
}
})
test('should use "en-US" as ultimate fallback', () => {
const Intl_ = global.Intl
const navigator_ = global.navigator
try {
delete global.Intl
delete global.navigator
const p2 = new PluralRules()
const opt2 = p2.resolvedOptions()
expect(opt2.locale).toMatch(/^en\b/)
} finally {
global.Intl = Intl_
global.navigator = navigator_
}
})
})
})
18 changes: 16 additions & 2 deletions src/pseudo-number-format.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ export default class PseudoNumberFormat {
#maxSD

constructor(
lc, // locale is ignored; always use 'en'
lc, // locale is ignored; always use 'en-US' in format()
{
minimumIntegerDigits: minID,
minimumFractionDigits: minFD,
maximumFractionDigits: maxFD,
minimumSignificantDigits: minSD,
maximumSignificantDigits: maxSD
}
} = {}
) {
this.#minID = typeof minID === 'number' ? minID : 1
this.#minFD = typeof minFD === 'number' ? minFD : 0
Expand All @@ -34,6 +34,7 @@ export default class PseudoNumberFormat {
opt.minimumSignificantDigits = this.#minSD
opt.maximumSignificantDigits = this.#maxSD
}
Object.defineProperty(opt, 'locale', { get: getDefaultLocale })
return opt
}

Expand All @@ -54,3 +55,16 @@ export default class PseudoNumberFormat {
return String(n)
}
}

function getDefaultLocale() {
if (
typeof Intl !== 'undefined' &&
typeof Intl.DateTimeFormat === 'function'
) {
return new Intl.DateTimeFormat().resolvedOptions().locale
} else if (typeof navigator !== 'undefined') {
return navigator.userLanguage || navigator.language || 'en-US'
} else {
return 'en-US'
}
}

0 comments on commit 58fa97d

Please sign in to comment.