Skip to content

Commit ba85dc1

Browse files
Remove fallback implementation of Intl.PluralRules
The intersection of browsers that support ES6 modules (and therefore run our JavaScript) but do not support `Intl.PluralRules` is relatively small: - Safari 10.3 - 12.5 on iOS - Safari 10.1 - 12.1 on macOS - Edge 16-18 - Chrome 61-62 - Opera 48-49 These browsers represent a tiny percentage of users and do not justify the overhead of including the extra JavaScript to implement the fallback logic, so let’s remove it. These browsers will instead fall back to the ‘other’ translation. For example, when using the Character Count in English it will display ‘You have 1 characters remaining’ rather than the singular translation ‘You have 1 character remaining’.
1 parent 87b9f6e commit ba85dc1

2 files changed

Lines changed: 3 additions & 306 deletions

File tree

packages/nhsuk-frontend/src/nhsuk/i18n.jsdom.test.mjs

Lines changed: 2 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ describe('I18n', () => {
261261
expect(IntlPluralRulesSelect).toHaveBeenCalledWith(1)
262262
})
263263

264-
it('falls back to internal fallback rules', () => {
264+
it('falls back to "other" when the browser does not support Intl.PluralRules', () => {
265265
const i18n = new I18n(
266266
{
267267
test: {
@@ -278,13 +278,7 @@ describe('I18n', () => {
278278
.spyOn(i18n, 'hasIntlPluralRulesSupport')
279279
.mockImplementation(() => false)
280280

281-
const selectPluralFormUsingFallbackRules = jest.spyOn(
282-
i18n,
283-
'selectPluralFormUsingFallbackRules'
284-
)
285-
286-
i18n.getPluralSuffix('test', 1)
287-
expect(selectPluralFormUsingFallbackRules).toHaveBeenCalledWith(1)
281+
expect(i18n.getPluralSuffix('test', 1)).toBe('other')
288282
})
289283

290284
it('returns the preferred plural form for the locale if a translation exists', () => {
@@ -390,68 +384,4 @@ describe('I18n', () => {
390384
expect(i18n.getPluralSuffix('test', 'nonsense')).toBe('other')
391385
})
392386
})
393-
394-
describe('.getPluralRulesForLocale', () => {
395-
it('returns the correct rules for a locale in the map', () => {
396-
const locale = 'ar'
397-
const i18n = new I18n({}, { locale })
398-
expect(i18n.getPluralRulesForLocale()).toBe('arabic')
399-
})
400-
401-
it('returns the correct rules for a locale in the map with regional indicator', () => {
402-
const locale = 'pt-PT'
403-
const i18n = new I18n({}, { locale })
404-
expect(i18n.getPluralRulesForLocale()).toBe('spanish')
405-
})
406-
407-
it('returns the correct rules for a locale allowing for no regional indicator', () => {
408-
const locale = 'cy-GB'
409-
const i18n = new I18n({}, { locale })
410-
expect(i18n.getPluralRulesForLocale()).toBe('welsh')
411-
})
412-
})
413-
414-
describe('.selectPluralFormUsingFallbackRules', () => {
415-
// The locales we want to test, with numbers for any 'special cases' in
416-
// those locales we want to ensure are handled correctly
417-
/** @type {[string, number[]][]} */
418-
const locales = [
419-
['ar', [105, 125]],
420-
['zh', []],
421-
['fr', []],
422-
['de', []],
423-
['ga', [9]],
424-
['ru', [3, 13, 101]],
425-
['gd', [15]],
426-
['es', [1000000, 2000000]],
427-
['cy', [3, 6]]
428-
]
429-
430-
it.each(locales)(
431-
'matches `Intl.PluralRules.select()` for %s locale',
432-
(locale, localeNumbers) => {
433-
const i18n = new I18n({}, { locale })
434-
const intl = new Intl.PluralRules(locale)
435-
436-
const numbersToTest = [0, 1, 2, 5, 25, 100, ...localeNumbers]
437-
438-
numbersToTest.forEach((num) => {
439-
expect(i18n.selectPluralFormUsingFallbackRules(num)).toBe(
440-
intl.select(num)
441-
)
442-
})
443-
}
444-
)
445-
446-
it('returns "other" for unsupported locales', () => {
447-
const locale = 'la'
448-
const i18n = new I18n({}, { locale })
449-
450-
const numbersToTest = [0, 1, 2, 5, 25, 100]
451-
452-
numbersToTest.forEach((num) => {
453-
expect(i18n.selectPluralFormUsingFallbackRules(num)).toBe('other')
454-
})
455-
})
456-
})
457387
})

packages/nhsuk-frontend/src/nhsuk/i18n.mjs

Lines changed: 1 addition & 234 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export class I18n {
179179
// use the hardcoded fallback.
180180
const preferredForm = this.hasIntlPluralRulesSupport()
181181
? new Intl.PluralRules(this.locale).select(count)
182-
: this.selectPluralFormUsingFallbackRules(count)
182+
: 'other'
183183

184184
// Use the correct plural form if provided
185185
if (isObject(translation)) {
@@ -201,239 +201,6 @@ export class I18n {
201201
`i18n: Plural form ".other" is required for "${this.locale}" locale`
202202
)
203203
}
204-
205-
/**
206-
* Get the plural form using our fallback implementation
207-
*
208-
* This is split out into a separate function to make it easier to test the
209-
* fallback behaviour in an environment where Intl.PluralRules exists.
210-
*
211-
* @param {number} count - Number used to determine which pluralisation to use.
212-
* @returns {PluralRule} The pluralisation form for count in this locale.
213-
*/
214-
selectPluralFormUsingFallbackRules(count) {
215-
// Currently our custom code can only handle positive integers, so let's
216-
// make sure our number is one of those.
217-
count = Math.abs(Math.floor(count))
218-
219-
const ruleset = this.getPluralRulesForLocale()
220-
221-
if (ruleset) {
222-
return I18n.pluralRules[ruleset](count)
223-
}
224-
225-
return 'other'
226-
}
227-
228-
/**
229-
* Work out which pluralisation rules to use for the current locale
230-
*
231-
* The locale may include a regional indicator (such as en-GB), but we don't
232-
* usually care about this part, as pluralisation rules are usually the same
233-
* regardless of region. There are exceptions, however, (e.g. Portuguese) so
234-
* this searches by both the full and shortened locale codes, just to be sure.
235-
*
236-
* @returns {string | undefined} The name of the pluralisation rule to use (a key for one
237-
* of the functions in this.pluralRules)
238-
*/
239-
getPluralRulesForLocale() {
240-
const localeShort = this.locale.split('-')[0]
241-
242-
// Look through the plural rules map to find which `pluralRule` is
243-
// appropriate for our current `locale`.
244-
for (const pluralRule in I18n.pluralRulesMap) {
245-
const languages = I18n.pluralRulesMap[pluralRule]
246-
if (languages.includes(this.locale) || languages.includes(localeShort)) {
247-
return pluralRule
248-
}
249-
}
250-
}
251-
252-
/**
253-
* Map of plural rules to languages where those rules apply.
254-
*
255-
* Note: These groups are named for the most dominant or recognisable language
256-
* that uses each system. The groupings do not imply that the languages are
257-
* related to one another. Many languages have evolved the same systems
258-
* independently of one another.
259-
*
260-
* Code to support more languages can be found in the i18n spike:
261-
* {@link https://github.com/alphagov/govuk-frontend/blob/spike-i18n-support/src/govuk/i18n.mjs}
262-
*
263-
* Languages currently supported:
264-
*
265-
* Arabic: Arabic (ar)
266-
* Chinese: Burmese (my), Chinese (zh), Indonesian (id), Japanese (ja),
267-
* Javanese (jv), Korean (ko), Malay (ms), Thai (th), Vietnamese (vi)
268-
* French: Armenian (hy), Bangla (bn), French (fr), Gujarati (gu), Hindi (hi),
269-
* Persian Farsi (fa), Punjabi (pa), Zulu (zu)
270-
* German: Afrikaans (af), Albanian (sq), Azerbaijani (az), Basque (eu),
271-
* Bulgarian (bg), Catalan (ca), Danish (da), Dutch (nl), English (en),
272-
* Estonian (et), Finnish (fi), Georgian (ka), German (de), Greek (el),
273-
* Hungarian (hu), Luxembourgish (lb), Norwegian (no), Somali (so),
274-
* Swahili (sw), Swedish (sv), Tamil (ta), Telugu (te), Turkish (tr),
275-
* Urdu (ur)
276-
* Irish: Irish Gaelic (ga)
277-
* Russian: Russian (ru), Ukrainian (uk)
278-
* Scottish: Scottish Gaelic (gd)
279-
* Spanish: European Portuguese (pt-PT), Italian (it), Spanish (es)
280-
* Welsh: Welsh (cy)
281-
*
282-
* @type {{ [key: string]: string[] }}
283-
*/
284-
static pluralRulesMap = {
285-
arabic: ['ar'],
286-
chinese: ['my', 'zh', 'id', 'ja', 'jv', 'ko', 'ms', 'th', 'vi'],
287-
french: ['hy', 'bn', 'fr', 'gu', 'hi', 'fa', 'pa', 'zu'],
288-
german: [
289-
'af',
290-
'sq',
291-
'az',
292-
'eu',
293-
'bg',
294-
'ca',
295-
'da',
296-
'nl',
297-
'en',
298-
'et',
299-
'fi',
300-
'ka',
301-
'de',
302-
'el',
303-
'hu',
304-
'lb',
305-
'no',
306-
'so',
307-
'sw',
308-
'sv',
309-
'ta',
310-
'te',
311-
'tr',
312-
'ur'
313-
],
314-
irish: ['ga'],
315-
russian: ['ru', 'uk'],
316-
scottish: ['gd'],
317-
spanish: ['pt-PT', 'it', 'es'],
318-
welsh: ['cy']
319-
}
320-
321-
/**
322-
* Different pluralisation rule sets
323-
*
324-
* Returns the appropriate suffix for the plural form associated with `n`.
325-
* Possible suffixes: 'zero', 'one', 'two', 'few', 'many', 'other' (the actual
326-
* meaning of each differs per locale). 'other' should always exist, even in
327-
* languages without plurals, such as Chinese.
328-
* {@link https://cldr.unicode.org/index/cldr-spec/plural-rules}
329-
*
330-
* The count must be a positive integer. Negative numbers and decimals aren't accounted for
331-
*
332-
* @type {{ [key: string]: (count: number) => PluralRule }}
333-
*/
334-
static pluralRules = {
335-
arabic(n) {
336-
if (n === 0) {
337-
return 'zero'
338-
}
339-
if (n === 1) {
340-
return 'one'
341-
}
342-
if (n === 2) {
343-
return 'two'
344-
}
345-
if (n % 100 >= 3 && n % 100 <= 10) {
346-
return 'few'
347-
}
348-
if (n % 100 >= 11 && n % 100 <= 99) {
349-
return 'many'
350-
}
351-
return 'other'
352-
},
353-
chinese() {
354-
return 'other'
355-
},
356-
french(n) {
357-
return n === 0 || n === 1 ? 'one' : 'other'
358-
},
359-
german(n) {
360-
return n === 1 ? 'one' : 'other'
361-
},
362-
irish(n) {
363-
if (n === 1) {
364-
return 'one'
365-
}
366-
if (n === 2) {
367-
return 'two'
368-
}
369-
if (n >= 3 && n <= 6) {
370-
return 'few'
371-
}
372-
if (n >= 7 && n <= 10) {
373-
return 'many'
374-
}
375-
return 'other'
376-
},
377-
russian(n) {
378-
const lastTwo = n % 100
379-
const last = lastTwo % 10
380-
if (last === 1 && lastTwo !== 11) {
381-
return 'one'
382-
}
383-
if (last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14)) {
384-
return 'few'
385-
}
386-
if (
387-
last === 0 ||
388-
(last >= 5 && last <= 9) ||
389-
(lastTwo >= 11 && lastTwo <= 14)
390-
) {
391-
return 'many'
392-
}
393-
// Note: The 'other' suffix is only used by decimal numbers in Russian.
394-
// We don't anticipate it being used, but it's here for consistency.
395-
return 'other'
396-
},
397-
scottish(n) {
398-
if (n === 1 || n === 11) {
399-
return 'one'
400-
}
401-
if (n === 2 || n === 12) {
402-
return 'two'
403-
}
404-
if ((n >= 3 && n <= 10) || (n >= 13 && n <= 19)) {
405-
return 'few'
406-
}
407-
return 'other'
408-
},
409-
spanish(n) {
410-
if (n === 1) {
411-
return 'one'
412-
}
413-
if (n % 1000000 === 0 && n !== 0) {
414-
return 'many'
415-
}
416-
return 'other'
417-
},
418-
welsh(n) {
419-
if (n === 0) {
420-
return 'zero'
421-
}
422-
if (n === 1) {
423-
return 'one'
424-
}
425-
if (n === 2) {
426-
return 'two'
427-
}
428-
if (n === 3) {
429-
return 'few'
430-
}
431-
if (n === 6) {
432-
return 'many'
433-
}
434-
return 'other'
435-
}
436-
}
437204
}
438205

439206
/**

0 commit comments

Comments
 (0)