Skip to content

refactor(use-i18n): use @formatjs/fast-memoize instead of deprecated intl-format-cache #317

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

Merged
merged 5 commits into from
Aug 9, 2021
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
2 changes: 1 addition & 1 deletion packages/use-i18n/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
},
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "^1.1.2",
"date-fns": "^2.19.0",
"filesize": "^7.0.0",
"intl-format-cache": "^4.3.1",
"intl-messageformat": "^9.5.3",
"intl-pluralrules": "^1.2.2",
"prop-types": "^15.7.2"
Expand Down
8 changes: 2 additions & 6 deletions packages/use-i18n/src/__tests__/formatUnit.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import memoizeIntlConstructor from 'intl-format-cache'
import IntlTranslationFormat from 'intl-messageformat'
import formatUnit, { FormatUnitOptions, supportedUnits } from '../formatUnit'

const locales = ['en', 'fr', 'ro']

const getTranslationFormat = memoizeIntlConstructor(IntlTranslationFormat)

const tests = [
...Object.keys(supportedUnits).map(unit => [
'should work with',
Expand Down Expand Up @@ -57,13 +53,13 @@ describe('formatUnit', () => {
test('should return empty string for unknown unit', () => {
expect(
// @ts-expect-error We test the use case when unit is unknown
formatUnit('fr', 123, { unit: 'unknown' }, getTranslationFormat),
formatUnit('fr', 123, { unit: 'unknown' }),
).toMatchSnapshot()
})

test.each(tests)('%s %o', (_, options, locale, amount) => {
expect(
formatUnit(locale as string, amount as number, options as FormatUnitOptions, getTranslationFormat),
formatUnit(locale as string, amount as number, options as FormatUnitOptions),
).toMatchSnapshot()
})
})
25 changes: 14 additions & 11 deletions packages/use-i18n/src/formatUnit.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import filesize from 'filesize'
import { Options } from 'intl-messageformat'
import formatters from './formatters'

// We are on base 10, so we should use IEC standard here ...
const exponents = [
Expand All @@ -15,6 +15,7 @@ const exponents = [
]

type Exponent = typeof exponents[number]
type ExponentName = '' | 'kilo' | 'mega' | 'giga' | 'tera' | 'peta' | 'exa' | 'zetta' | 'yotta'

const frOctet = {
plural: 'octets',
Expand Down Expand Up @@ -55,7 +56,6 @@ const compoundUnitsSymbols = {

type Unit = 'bit' | 'byte'
type CompoundUnit = 'second'
type FormatPlural = (message: string, locales?: string | string[] | undefined, overrideFormats?: undefined, opts?: Options | undefined) => { format: ({ amount }: { amount: number}) => string}

const formatShortUnit = (locale: string, exponent: Exponent, unit: Unit, compoundUnit?: CompoundUnit) => {
let shortenedUnit = symbols.short[unit]
Expand All @@ -72,7 +72,7 @@ const formatShortUnit = (locale: string, exponent: Exponent, unit: Unit, compoun
}`
}

const formatLongUnit = (locale: string, exponent: Exponent, unit: Unit, amount: number, messageFormat: FormatPlural) => {
const formatLongUnit = (locale: string, exponent: Exponent, unit: Unit, amount: number) => {
let translation = symbols.long[unit]

if (
Expand All @@ -82,14 +82,14 @@ const formatLongUnit = (locale: string, exponent: Exponent, unit: Unit, amount:
translation = localesWhoFavorOctetOverByte[locale as keyof typeof localesWhoFavorOctetOverByte]
}

return `${exponent.name}${messageFormat(
return `${exponent.name}${formatters.getTranslationFormat(
`{amount, plural,
=0 {${translation.singular}}
=1 {${translation.singular}}
other {${translation.plural}}
}`,
locale,
).format({ amount })}`
).format({ amount }) as string}`
}

const format =
Expand All @@ -103,7 +103,6 @@ const format =
locale: string,
amount: number,
{ maximumFractionDigits, minimumFractionDigits, short = true }: { maximumFractionDigits?: number, minimumFractionDigits?: number, short?: boolean },
messageFormat: FormatPlural,
): string => {
let computedExponent = exponent
let computedValue = amount
Expand Down Expand Up @@ -141,12 +140,16 @@ const format =
computedExponent as Exponent,
unit,
computedValue,
messageFormat,
)
}`
}

export const supportedUnits = {
type SimpleUnits = `${ExponentName}${Unit}${'-humanized' | ''}`
type ComplexUnits = `${Unit}${'s' | ''}${'-humanized' | ''}`
type PerSecondUnit = `bit${'s' | ''}${'-per-second' | ''}${'-humanized' | ''}`
type SupportedUnits = SimpleUnits | ComplexUnits | PerSecondUnit

export const supportedUnits: Partial<Record<SupportedUnits, ReturnType<typeof format>>> = {
// bits
'bits-humanized': format({ humanize: true, unit: 'bit' }),
'bits-per-second-humanized': format({
Expand Down Expand Up @@ -195,11 +198,11 @@ export const supportedUnits = {
}

export interface FormatUnitOptions {
unit: keyof typeof supportedUnits
unit: SupportedUnits
short?: boolean
}

const formatUnit = (locale: string, number: number, { unit, ...options }: FormatUnitOptions, messageFormat: FormatPlural): string =>
supportedUnits?.[unit]?.(locale, number, options, messageFormat) ?? ''
const formatUnit = (locale: string, number: number, { unit, ...options }: FormatUnitOptions): string =>
supportedUnits?.[unit]?.(locale, number, options) ?? ''

export default formatUnit
81 changes: 81 additions & 0 deletions packages/use-i18n/src/formatters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import memoize, { Cache, strategies } from '@formatjs/fast-memoize'
import IntlTranslationFormat from 'intl-messageformat'

// Deeply inspired by https://github.com/formatjs/formatjs/blob/7406e526a9c5666cee22cc2316dad1fa1d88697c/packages/intl-messageformat/src/core.ts

interface BaseFormatters {
getNumberFormat(
...args: ConstructorParameters<typeof Intl.NumberFormat>
): Intl.NumberFormat
getDateTimeFormat(
...args: ConstructorParameters<typeof Intl.DateTimeFormat>
): Intl.DateTimeFormat
getPluralRules(
...args: ConstructorParameters<typeof Intl.PluralRules>
): Intl.PluralRules
getListFormat(
...args: ConstructorParameters<typeof Intl.ListFormat>
): Intl.ListFormat
}

function createFastMemoizeCache<V>(): Cache<string, V> {
const store: Record<string, V> = {}

return {
create() {
return {
get(key) {
return store[key]
},
// Can be removed once https://github.com/formatjs/formatjs/pull/3102 is merged
/* istanbul ignore next */
has(key) {
return key in store
},
set(key, value) {
store[key] = value
},
}
},
}
}

const baseFormatters: BaseFormatters = {
getDateTimeFormat: memoize((...args) => new Intl.DateTimeFormat(...args), {
cache: createFastMemoizeCache<Intl.DateTimeFormat>(),
strategy: strategies.variadic,
}),
getListFormat: memoize((...args) => new Intl.ListFormat(...args), {
cache: createFastMemoizeCache<Intl.ListFormat>(),
strategy: strategies.variadic,
}),
getNumberFormat: memoize((...args) => new Intl.NumberFormat(...args), {
cache: createFastMemoizeCache<Intl.NumberFormat>(),
strategy: strategies.variadic,
}),
getPluralRules: memoize((...args) => new Intl.PluralRules(...args), {
cache: createFastMemoizeCache<Intl.PluralRules>(),
strategy: strategies.variadic,
}),
}

type Formatters = BaseFormatters & {
getTranslationFormat(
...args: ConstructorParameters<typeof IntlTranslationFormat>
): IntlTranslationFormat
}

type TranslationFormatParameter = ConstructorParameters<typeof IntlTranslationFormat>

const formatters: Formatters = {
...baseFormatters,
getTranslationFormat: memoize((message: TranslationFormatParameter[0], locales: TranslationFormatParameter[1], overrideFormats: TranslationFormatParameter[2], opts: TranslationFormatParameter[3]) => new IntlTranslationFormat(message, locales, overrideFormats, {
formatters: baseFormatters,
...opts,
}), {
cache: createFastMemoizeCache<IntlTranslationFormat>(),
strategy: strategies.variadic,
}),
}

export default formatters
29 changes: 9 additions & 20 deletions packages/use-i18n/src/usei18n.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { Locale, formatDistanceToNow, formatDistanceToNowStrict } from 'date-fns'
import memoizeIntlConstructor from 'intl-format-cache'
import IntlTranslationFormat from 'intl-messageformat'
import PropTypes from 'prop-types'
import React, {
ReactElement,
Expand All @@ -16,6 +14,7 @@ import ReactDOM from 'react-dom'
import 'intl-pluralrules'
import dateFormat, { FormatDateOptions } from './formatDate'
import unitFormat, { FormatUnitOptions } from './formatUnit'
import formatters from './formatters'

const LOCALE_ITEM_STORAGE = 'locale'

Expand Down Expand Up @@ -60,7 +59,7 @@ interface Context {
dateFnsLocale?: Locale,
datetime?: (date: Date | number, options?: Intl.DateTimeFormatOptions) => string,
formatDate?: (value: Date | number | string, options: FormatDateOptions) => string,
formatList?: (listFormat: string[], options?: Intl.ListFormatOptions) => string,
formatList?: (listFormat: [string | undefined], options?: Intl.ListFormatOptions) => string,
formatNumber?: (numb: number, options?: Intl.NumberFormatOptions) => string,
formatUnit?: (value: number, options: FormatUnitOptions) => string,
loadTranslations?: (namespace: string, load?: LoadTranslationsFn) => Promise<string>,
Expand Down Expand Up @@ -114,12 +113,6 @@ export const useTranslation = (namespaces: string[] = [], load: LoadTranslations
return { ...context, isLoaded }
}

// https://formatjs.io/docs/intl-messageformat/
const getTranslationFormat = memoizeIntlConstructor(IntlTranslationFormat)
const getNumberFormat = memoizeIntlConstructor(Intl.NumberFormat)
const getDateTimeFormat = memoizeIntlConstructor(Intl.DateTimeFormat)
const getListFormat = memoizeIntlConstructor(Intl.ListFormat)

type LoadTranslationsFn = ({ namespace, locale }: { namespace: string, locale: string}) => Promise<{ default: Translations}>
type LoadLocaleFn = (locale: string) => Promise<Locale>

Expand Down Expand Up @@ -216,17 +209,13 @@ const I18nContextProvider = ({
)

const formatNumber = useCallback(
// intl-format-chache does not forwrad return types
// eslint-disable-next-line
(numb: number, options?: Intl.NumberFormatOptions) => getNumberFormat(currentLocale, options).format(numb),
(numb: number, options?: Intl.NumberFormatOptions) => formatters.getNumberFormat(currentLocale, options).format(numb),
[currentLocale],
)

const formatList = useCallback(
(listFormat: string[], options?: Intl.ListFormatOptions) =>
// intl-format-chache does not forwrad return types
// eslint-disable-next-line
getListFormat(currentLocale, options).format(listFormat),
(listFormat: [string | undefined], options?: Intl.ListFormatOptions) =>
formatters.getListFormat(currentLocale, options).format(listFormat),
[currentLocale],
)

Expand All @@ -235,7 +224,7 @@ const I18nContextProvider = ({
// be able to use formatNumber directly
const formatUnit = useCallback(
(value: number, options: FormatUnitOptions) =>
unitFormat(currentLocale, value, options, getTranslationFormat),
unitFormat(currentLocale, value, options),
[currentLocale],
)

Expand All @@ -248,7 +237,7 @@ const I18nContextProvider = ({
const datetime = useCallback(
// intl-format-chache does not forwrad return types
// eslint-disable-next-line
(date: Date | number, options?: Intl.DateTimeFormatOptions): string => getDateTimeFormat(currentLocale, options).format(date),
(date: Date | number, options?: Intl.DateTimeFormatOptions): string => formatters.getDateTimeFormat(currentLocale, options).format(date),
[currentLocale],
)

Expand Down Expand Up @@ -283,7 +272,7 @@ const I18nContextProvider = ({
[dateFnsLocale],
)

const translate = useCallback(
const translate = useCallback<TranslateFn>(
(key: string, context?: Record<string, PrimitiveType>) => {
const value = translations[currentLocale]?.[key]
if (!value) {
Expand All @@ -296,7 +285,7 @@ const I18nContextProvider = ({
if (context) {
// intl-format-chache does not forwrad return types
// eslint-disable-next-line
return getTranslationFormat(value, currentLocale).format(context)
return formatters.getTranslationFormat(value, currentLocale).format(context) as string
}

return value
Expand Down
7 changes: 1 addition & 6 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1209,7 +1209,7 @@
"@formatjs/intl-localematcher" "0.2.19"
tslib "^2.1.0"

"@formatjs/[email protected]":
"@formatjs/[email protected]", "@formatjs/fast-memoize@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-1.1.2.tgz#09c8771484095c07c752b824b142d6c2e2c8264f"
integrity sha512-HfN6D9yd9vhypWwTVpJHqoSb50KFuoxZ2ZS6AM1XNSyihWk7JJXjZ3mgvboJU4eNSsdxAWwR1ke5KIOobEVwBA==
Expand Down Expand Up @@ -5031,11 +5031,6 @@ interpret@^1.0.0:
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==

intl-format-cache@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/intl-format-cache/-/intl-format-cache-4.3.1.tgz#484d31a9872161e6c02139349b259a6229ade377"
integrity sha512-OEUYNA7D06agqPOYhbTkl0T8HA3QKSuwWh1HiClEnpd9vw7N+3XsQt5iZ0GUEchp5CW1fQk/tary+NsbF3yQ1Q==

intl-messageformat@^9.5.3:
version "9.8.2"
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-9.8.2.tgz#5528334506f9af660ec3f7ea8f3584bba77af8d4"
Expand Down