Fix inconsistent number formatting between mobile and web (#6384)
* Manual truncation & identify factor points for each lang * Reduce indirection * Add test Co-authored-by: khuddite <biliie811028@hotmail.com> * Handle big numbers, clarify special case * Clarify the reason --------- Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import {describe, expect, it} from '@jest/globals'
|
||||
|
||||
import {APP_LANGUAGES} from '#/locale/languages'
|
||||
import {formatCount} from '../format'
|
||||
|
||||
const formatCountRound = (locale: string, num: number) => {
|
||||
const options: Intl.NumberFormatOptions = {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1,
|
||||
}
|
||||
return new Intl.NumberFormat(locale, options).format(num)
|
||||
}
|
||||
|
||||
const formatCountTrunc = (locale: string, num: number) => {
|
||||
const options: Intl.NumberFormatOptions = {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1,
|
||||
// @ts-ignore
|
||||
roundingMode: 'trunc',
|
||||
}
|
||||
return new Intl.NumberFormat(locale, options).format(num)
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
const testNums = [
|
||||
1,
|
||||
5,
|
||||
9,
|
||||
11,
|
||||
55,
|
||||
99,
|
||||
111,
|
||||
555,
|
||||
999,
|
||||
1111,
|
||||
5555,
|
||||
9999,
|
||||
11111,
|
||||
55555,
|
||||
99999,
|
||||
111111,
|
||||
555555,
|
||||
999999,
|
||||
1111111,
|
||||
5555555,
|
||||
9999999,
|
||||
11111111,
|
||||
55555555,
|
||||
99999999,
|
||||
111111111,
|
||||
555555555,
|
||||
999999999,
|
||||
1111111111,
|
||||
5555555555,
|
||||
9999999999,
|
||||
11111111111,
|
||||
55555555555,
|
||||
99999999999,
|
||||
111111111111,
|
||||
555555555555,
|
||||
999999999999,
|
||||
1111111111111,
|
||||
5555555555555,
|
||||
9999999999999,
|
||||
11111111111111,
|
||||
55555555555555,
|
||||
99999999999999,
|
||||
111111111111111,
|
||||
555555555555555,
|
||||
999999999999999,
|
||||
1111111111111111,
|
||||
5555555555555555,
|
||||
]
|
||||
|
||||
describe('formatCount', () => {
|
||||
for (const appLanguage of APP_LANGUAGES) {
|
||||
const locale = appLanguage.code2
|
||||
it('truncates for ' + locale, () => {
|
||||
const mockI8nn = {
|
||||
locale,
|
||||
number(num: number) {
|
||||
return formatCountRound(locale, num)
|
||||
},
|
||||
}
|
||||
for (const num of testNums) {
|
||||
const formatManual = formatCount(mockI8nn as any, num)
|
||||
const formatOriginal = formatCountTrunc(locale, num)
|
||||
expect(formatManual).toEqual(formatOriginal)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,12 +1,47 @@
|
||||
import type {I18n} from '@lingui/core'
|
||||
import {I18n} from '@lingui/core'
|
||||
|
||||
const truncateRounding = (num: number, factors: Array<number>): number => {
|
||||
for (let i = factors.length - 1; i >= 0; i--) {
|
||||
let factor = factors[i]
|
||||
if (num >= 10 ** factor) {
|
||||
if (factor === 10) {
|
||||
// CA and ES abruptly jump from "9999,9 M" to "10 mil M"
|
||||
factor--
|
||||
}
|
||||
const precision = 1
|
||||
const divisor = 10 ** (factor - precision)
|
||||
return Math.floor(num / divisor) * divisor
|
||||
}
|
||||
}
|
||||
return num
|
||||
}
|
||||
|
||||
const koFactors = [3, 4, 8, 12]
|
||||
const hiFactors = [3, 5, 7, 9, 11, 13]
|
||||
const esCaFactors = [3, 6, 10, 12]
|
||||
const itDeFactors = [6, 9, 12]
|
||||
const jaZhFactors = [4, 8, 12]
|
||||
const restFactors = [3, 6, 9, 12]
|
||||
|
||||
export const formatCount = (i18n: I18n, num: number) => {
|
||||
return i18n.number(num, {
|
||||
const locale = i18n.locale
|
||||
let truncatedNum: number
|
||||
if (locale === 'hi') {
|
||||
truncatedNum = truncateRounding(num, hiFactors)
|
||||
} else if (locale === 'ko') {
|
||||
truncatedNum = truncateRounding(num, koFactors)
|
||||
} else if (locale === 'es' || locale === 'ca') {
|
||||
truncatedNum = truncateRounding(num, esCaFactors)
|
||||
} else if (locale === 'ja' || locale === 'zh-CN' || locale === 'zh-TW') {
|
||||
truncatedNum = truncateRounding(num, jaZhFactors)
|
||||
} else if (locale === 'it' || locale === 'de') {
|
||||
truncatedNum = truncateRounding(num, itDeFactors)
|
||||
} else {
|
||||
truncatedNum = truncateRounding(num, restFactors)
|
||||
}
|
||||
return i18n.number(truncatedNum, {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1,
|
||||
// `1,953` shouldn't be rounded up to 2k, it should be truncated.
|
||||
// @ts-expect-error: `roundingMode` doesn't seem to be in the typings yet
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingmode
|
||||
roundingMode: 'trunc',
|
||||
// Ideally we'd use roundingMode: 'trunc' but it isn't supported on RN.
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user