diff --git a/src/TIMEZONES.js b/src/TIMEZONES.ts similarity index 99% rename from src/TIMEZONES.js rename to src/TIMEZONES.ts index 2a596b51e8b3..1eb49f291495 100644 --- a/src/TIMEZONES.js +++ b/src/TIMEZONES.ts @@ -418,4 +418,4 @@ export default [ 'Pacific/Tongatapu', 'Pacific/Wake', 'Pacific/Wallis', -]; +] as const; diff --git a/src/components/LocaleContextProvider.js b/src/components/LocaleContextProvider.js deleted file mode 100644 index 5fbb7716befe..000000000000 --- a/src/components/LocaleContextProvider.js +++ /dev/null @@ -1,134 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {createContext, useMemo} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import compose from '@libs/compose'; -import DateUtils from '@libs/DateUtils'; -import * as LocaleDigitUtils from '@libs/LocaleDigitUtils'; -import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; -import * as Localize from '@libs/Localize'; -import * as NumberFormatUtils from '@libs/NumberFormatUtils'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import withCurrentUserPersonalDetails from './withCurrentUserPersonalDetails'; - -const LocaleContext = createContext(null); - -const localeProviderPropTypes = { - /** The user's preferred locale e.g. 'en', 'es-ES' */ - preferredLocale: PropTypes.string, - - /** Actual content wrapped by this component */ - children: PropTypes.node.isRequired, - - /** The current user's personalDetails */ - currentUserPersonalDetails: PropTypes.shape({ - /** Timezone of the current user */ - timezone: PropTypes.shape({ - /** Value of the selected timezone */ - selected: PropTypes.string, - }), - }), -}; - -const localeProviderDefaultProps = { - preferredLocale: CONST.LOCALES.DEFAULT, - currentUserPersonalDetails: {}, -}; - -function LocaleContextProvider({children, currentUserPersonalDetails, preferredLocale}) { - const selectedTimezone = useMemo(() => lodashGet(currentUserPersonalDetails, 'timezone.selected'), [currentUserPersonalDetails]); - - /** - * @param {String} phrase - * @param {Object} [variables] - * @returns {String} - */ - const translate = useMemo(() => (phrase, variables) => Localize.translate(preferredLocale, phrase, variables), [preferredLocale]); - - /** - * @param {Number} number - * @param {Intl.NumberFormatOptions} options - * @returns {String} - */ - const numberFormat = useMemo(() => (number, options) => NumberFormatUtils.format(preferredLocale, number, options), [preferredLocale]); - - /** - * @param {String} datetime - * @returns {String} - */ - const datetimeToRelative = useMemo(() => (datetime) => DateUtils.datetimeToRelative(preferredLocale, datetime), [preferredLocale]); - - /** - * @param {String} datetime - ISO-formatted datetime string - * @param {Boolean} [includeTimezone] - * @param {Boolean} isLowercase - * @returns {String} - */ - const datetimeToCalendarTime = useMemo( - () => - (datetime, includeTimezone, isLowercase = false) => - DateUtils.datetimeToCalendarTime(preferredLocale, datetime, includeTimezone, selectedTimezone, isLowercase), - [preferredLocale, selectedTimezone], - ); - - /** - * Updates date-fns internal locale to the user preferredLocale - */ - const updateLocale = useMemo(() => () => DateUtils.setLocale(preferredLocale), [preferredLocale]); - - /** - * @param {String} phoneNumber - * @returns {String} - */ - const formatPhoneNumber = LocalePhoneNumber.formatPhoneNumber; - - /** - * @param {String} digit - * @returns {String} - */ - const toLocaleDigit = useMemo(() => (digit) => LocaleDigitUtils.toLocaleDigit(preferredLocale, digit), [preferredLocale]); - - /** - * @param {String} localeDigit - * @returns {String} - */ - const fromLocaleDigit = useMemo(() => (localeDigit) => LocaleDigitUtils.fromLocaleDigit(preferredLocale, localeDigit), [preferredLocale]); - - /** - * The context this component exposes to child components - * @returns {object} translation util functions and locale - */ - const contextValue = useMemo( - () => ({ - translate, - numberFormat, - datetimeToRelative, - datetimeToCalendarTime, - updateLocale, - formatPhoneNumber, - toLocaleDigit, - fromLocaleDigit, - preferredLocale, - }), - [translate, numberFormat, datetimeToRelative, datetimeToCalendarTime, updateLocale, formatPhoneNumber, toLocaleDigit, fromLocaleDigit, preferredLocale], - ); - - return {children}; -} - -LocaleContextProvider.propTypes = localeProviderPropTypes; -LocaleContextProvider.defaultProps = localeProviderDefaultProps; - -const Provider = compose( - withCurrentUserPersonalDetails, - withOnyx({ - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, - }), -)(LocaleContextProvider); - -Provider.displayName = 'withOnyx(LocaleContextProvider)'; - -export {Provider as LocaleContextProvider, LocaleContext}; diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx new file mode 100644 index 000000000000..2fa4e1c749e6 --- /dev/null +++ b/src/components/LocaleContextProvider.tsx @@ -0,0 +1,133 @@ +import React, {createContext, useMemo} from 'react'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; +import compose from '@libs/compose'; +import DateUtils from '@libs/DateUtils'; +import * as LocaleDigitUtils from '@libs/LocaleDigitUtils'; +import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; +import * as Localize from '@libs/Localize'; +import * as NumberFormatUtils from '@libs/NumberFormatUtils'; +import CONST from '@src/CONST'; +import {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import withCurrentUserPersonalDetails, {WithCurrentUserPersonalDetailsProps} from './withCurrentUserPersonalDetails'; + +type Locale = ValueOf; + +type LocaleContextProviderOnyxProps = { + /** The user's preferred locale e.g. 'en', 'es-ES' */ + preferredLocale: OnyxEntry; +}; + +type LocaleContextProviderProps = LocaleContextProviderOnyxProps & + WithCurrentUserPersonalDetailsProps & { + /** Actual content wrapped by this component */ + children: React.ReactNode; + }; + +type LocaleContextProps = { + /** Returns translated string for given locale and phrase */ + translate: (phraseKey: TKey, ...phraseParameters: Localize.PhraseParameters>) => string; + + /** Formats number formatted according to locale and options */ + numberFormat: (number: number, options: Intl.NumberFormatOptions) => string; + + /** Converts a datetime into a localized string representation that's relative to current moment in time */ + datetimeToRelative: (datetime: string) => string; + + /** Formats a datetime to local date and time string */ + datetimeToCalendarTime: (datetime: string, includeTimezone: boolean, isLowercase: boolean) => string; + + /** Updates date-fns internal locale */ + updateLocale: () => void; + + /** Returns a locally converted phone number for numbers from the same region + * and an internationally converted phone number with the country code for numbers from other regions */ + formatPhoneNumber: (phoneNumber: string) => string; + + /** Gets the locale digit corresponding to a standard digit */ + toLocaleDigit: (digit: string) => string; + + /** Gets the standard digit corresponding to a locale digit */ + fromLocaleDigit: (digit: string) => string; + + /** The user's preferred locale e.g. 'en', 'es-ES' */ + preferredLocale: Locale; +}; + +const LocaleContext = createContext({ + translate: () => '', + numberFormat: () => '', + datetimeToRelative: () => '', + datetimeToCalendarTime: () => '', + updateLocale: () => '', + formatPhoneNumber: () => '', + toLocaleDigit: () => '', + fromLocaleDigit: () => '', + preferredLocale: CONST.LOCALES.DEFAULT, +}); + +function LocaleContextProvider({preferredLocale, currentUserPersonalDetails = {}, children}: LocaleContextProviderProps) { + const locale = preferredLocale ?? CONST.LOCALES.DEFAULT; + + const selectedTimezone = useMemo(() => currentUserPersonalDetails?.timezone?.selected, [currentUserPersonalDetails]); + + const translate = useMemo( + () => + (phraseKey, ...phraseParameters) => + Localize.translate(locale, phraseKey, ...phraseParameters), + [locale], + ); + + const numberFormat = useMemo(() => (number, options) => NumberFormatUtils.format(locale, number, options), [locale]); + + const datetimeToRelative = useMemo(() => (datetime) => DateUtils.datetimeToRelative(locale, datetime), [locale]); + + const datetimeToCalendarTime = useMemo( + () => + (datetime, includeTimezone, isLowercase = false) => + DateUtils.datetimeToCalendarTime(locale, datetime, includeTimezone, selectedTimezone, isLowercase), + [locale, selectedTimezone], + ); + + const updateLocale = useMemo(() => () => DateUtils.setLocale(locale), [locale]); + + const formatPhoneNumber = useMemo(() => (phoneNumber) => LocalePhoneNumber.formatPhoneNumber(phoneNumber), []); + + const toLocaleDigit = useMemo(() => (digit) => LocaleDigitUtils.toLocaleDigit(locale, digit), [locale]); + + const fromLocaleDigit = useMemo(() => (localeDigit) => LocaleDigitUtils.fromLocaleDigit(locale, localeDigit), [locale]); + + const contextValue = useMemo( + () => ({ + translate, + numberFormat, + datetimeToRelative, + datetimeToCalendarTime, + updateLocale, + formatPhoneNumber, + toLocaleDigit, + fromLocaleDigit, + preferredLocale: locale, + }), + [translate, numberFormat, datetimeToRelative, datetimeToCalendarTime, updateLocale, formatPhoneNumber, toLocaleDigit, fromLocaleDigit, locale], + ); + + return {children}; +} + +const Provider = compose( + withOnyx({ + preferredLocale: { + key: ONYXKEYS.NVP_PREFERRED_LOCALE, + selector: (preferredLocale) => preferredLocale, + }, + }), + withCurrentUserPersonalDetails, +)(LocaleContextProvider); + +Provider.displayName = 'withOnyx(LocaleContextProvider)'; + +export {Provider as LocaleContextProvider, LocaleContext}; + +export type {LocaleContextProps}; diff --git a/src/components/withCurrentUserPersonalDetails.tsx b/src/components/withCurrentUserPersonalDetails.tsx index 20e536d9d733..a97067c32c72 100644 --- a/src/components/withCurrentUserPersonalDetails.tsx +++ b/src/components/withCurrentUserPersonalDetails.tsx @@ -18,7 +18,7 @@ type HOCProps = { currentUserPersonalDetails: CurrentUserPersonalDetails; }; -type ComponentProps = OnyxProps & HOCProps; +type WithCurrentUserPersonalDetailsProps = OnyxProps & HOCProps; // TODO: remove when all components that use it will be migrated to TS const withCurrentUserPersonalDetailsPropTypes = { @@ -29,7 +29,7 @@ const withCurrentUserPersonalDetailsDefaultProps: HOCProps = { currentUserPersonalDetails: {}, }; -export default function ( +export default function ( WrappedComponent: ComponentType>, ): ComponentType & RefAttributes, keyof OnyxProps>> { function WithCurrentUserPersonalDetails(props: Omit, ref: ForwardedRef) { @@ -62,3 +62,4 @@ export default function ( } export {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps}; +export type {WithCurrentUserPersonalDetailsProps}; diff --git a/src/hooks/useLocalize.js b/src/hooks/useLocalize.js deleted file mode 100644 index 71968cdb6e61..000000000000 --- a/src/hooks/useLocalize.js +++ /dev/null @@ -1,6 +0,0 @@ -import {useContext} from 'react'; -import {LocaleContext} from '@components/LocaleContextProvider'; - -export default function useLocalize() { - return useContext(LocaleContext); -} diff --git a/src/hooks/useLocalize.ts b/src/hooks/useLocalize.ts new file mode 100644 index 000000000000..2875577ef00f --- /dev/null +++ b/src/hooks/useLocalize.ts @@ -0,0 +1,6 @@ +import {useContext} from 'react'; +import {LocaleContext, LocaleContextProps} from '@components/LocaleContextProvider'; + +export default function useLocalize(): LocaleContextProps { + return useContext(LocaleContext); +} diff --git a/src/languages/types.ts b/src/languages/types.ts index 5f6669315041..e2af3222a98f 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -243,10 +243,10 @@ type TranslationFlatObject = { export type { TranslationBase, + TranslationPaths, EnglishTranslation, TranslationFlatObject, AddressLineParams, - TranslationPaths, CharacterLimitParams, MaxParticipantsReachedParams, ZipCodeExampleFormatParams, diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index b956b5adcc51..80eae24d9367 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -21,12 +21,15 @@ import {formatInTimeZone, format as tzFormat, utcToZonedTime, zonedTimeToUtc} fr import {enGB, es} from 'date-fns/locale'; import throttle from 'lodash/throttle'; import Onyx from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {Timezone} from '@src/types/onyx/PersonalDetails'; +import {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails'; import * as CurrentDate from './actions/CurrentDate'; import * as Localize from './Localize'; +type Locale = ValueOf; + let currentUserAccountID: number | undefined; Onyx.connect({ key: ONYXKEYS.SESSION, @@ -60,7 +63,7 @@ Onyx.connect({ /** * Gets the locale string and setting default locale for date-fns */ -function setLocale(localeString: string) { +function setLocale(localeString: Locale) { switch (localeString) { case CONST.LOCALES.EN: setDefaultOptions({locale: enGB}); @@ -77,7 +80,7 @@ function setLocale(localeString: string) { * Gets the user's stored time zone NVP and returns a localized * Date object for the given ISO-formatted datetime string */ -function getLocalDateFromDatetime(locale: string, datetime: string, currentSelectedTimezone = timezone.selected): Date { +function getLocalDateFromDatetime(locale: Locale, datetime: string, currentSelectedTimezone: SelectedTimezone = timezone.selected): Date { setLocale(locale); if (!datetime) { return utcToZonedTime(new Date(), currentSelectedTimezone); @@ -93,7 +96,7 @@ function getLocalDateFromDatetime(locale: string, datetime: string, currentSelec * @param timeZone - The time zone to consider. * @returns True if the date is today; otherwise, false. */ -function isToday(date: Date, timeZone: string): boolean { +function isToday(date: Date, timeZone: SelectedTimezone): boolean { const currentDate = new Date(); const currentDateInTimeZone = utcToZonedTime(currentDate, timeZone); return isSameDay(date, currentDateInTimeZone); @@ -106,7 +109,7 @@ function isToday(date: Date, timeZone: string): boolean { * @param timeZone - The time zone to consider. * @returns True if the date is tomorrow; otherwise, false. */ -function isTomorrow(date: Date, timeZone: string): boolean { +function isTomorrow(date: Date, timeZone: SelectedTimezone): boolean { const currentDate = new Date(); const tomorrow = addDays(currentDate, 1); // Get the date for tomorrow in the current time zone const tomorrowInTimeZone = utcToZonedTime(tomorrow, timeZone); @@ -120,7 +123,7 @@ function isTomorrow(date: Date, timeZone: string): boolean { * @param timeZone - The time zone to consider. * @returns True if the date is yesterday; otherwise, false. */ -function isYesterday(date: Date, timeZone: string): boolean { +function isYesterday(date: Date, timeZone: SelectedTimezone): boolean { const currentDate = new Date(); const yesterday = subDays(currentDate, 1); // Get the date for yesterday in the current time zone const yesterdayInTimeZone = utcToZonedTime(yesterday, timeZone); @@ -135,13 +138,7 @@ function isYesterday(date: Date, timeZone: string): boolean { * Jan 20 at 5:30 PM within the past year * Jan 20, 2019 at 5:30 PM anything over 1 year ago */ -function datetimeToCalendarTime( - locale: 'en' | 'es' | 'es-ES' | 'es_ES', - datetime: string, - includeTimeZone = false, - currentSelectedTimezone = timezone.selected, - isLowercase = false, -): string { +function datetimeToCalendarTime(locale: Locale, datetime: string, includeTimeZone = false, currentSelectedTimezone: SelectedTimezone = timezone.selected, isLowercase = false): string { const date = getLocalDateFromDatetime(locale, datetime, currentSelectedTimezone); const tz = includeTimeZone ? ' [UTC]Z' : ''; let todayAt = Localize.translate(locale, 'common.todayAt'); @@ -186,7 +183,7 @@ function datetimeToCalendarTime( * Jan 20 within the past year * Jan 20, 2019 anything over 1 year */ -function datetimeToRelative(locale: string, datetime: string): string { +function datetimeToRelative(locale: Locale, datetime: string): string { const date = getLocalDateFromDatetime(locale, datetime); return formatDistanceToNow(date, {addSuffix: true}); } @@ -204,7 +201,7 @@ function datetimeToRelative(locale: string, datetime: string): string { * @param selectedTimezone * @returns */ -function getZoneAbbreviation(datetime: string, selectedTimezone: string): string { +function getZoneAbbreviation(datetime: string, selectedTimezone: SelectedTimezone): string { return formatInTimeZone(datetime, selectedTimezone, 'zzz'); } @@ -258,7 +255,7 @@ function startCurrentDateUpdater() { function getCurrentTimezone(): Required { const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; if (timezone.automatic && timezone.selected !== currentTimezone) { - return {...timezone, selected: currentTimezone}; + return {...timezone, selected: currentTimezone as SelectedTimezone}; } return timezone; } @@ -266,7 +263,7 @@ function getCurrentTimezone(): Required { /** * @returns [January, Fabruary, March, April, May, June, July, August, ...] */ -function getMonthNames(preferredLocale: string): string[] { +function getMonthNames(preferredLocale: Locale): string[] { if (preferredLocale) { setLocale(preferredLocale); } @@ -283,7 +280,7 @@ function getMonthNames(preferredLocale: string): string[] { /** * @returns [Monday, Thuesday, Wednesday, ...] */ -function getDaysOfWeek(preferredLocale: string): string[] { +function getDaysOfWeek(preferredLocale: Locale): string[] { if (preferredLocale) { setLocale(preferredLocale); } diff --git a/src/libs/LocaleDigitUtils.ts b/src/libs/LocaleDigitUtils.ts index d9ba23ff4f9f..05d9fea5357d 100644 --- a/src/libs/LocaleDigitUtils.ts +++ b/src/libs/LocaleDigitUtils.ts @@ -1,13 +1,17 @@ import _ from 'lodash'; +import {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; import * as NumberFormatUtils from './NumberFormatUtils'; +type Locale = ValueOf; + const STANDARD_DIGITS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '-', ',']; const INDEX_DECIMAL = 10; const INDEX_MINUS_SIGN = 11; const INDEX_GROUP = 12; -const getLocaleDigits = _.memoize((locale: string): string[] => { +const getLocaleDigits = _.memoize((locale: Locale): string[] => { const localeDigits = [...STANDARD_DIGITS]; for (let i = 0; i <= 9; i++) { localeDigits[i] = NumberFormatUtils.format(locale, i); @@ -38,7 +42,7 @@ const getLocaleDigits = _.memoize((locale: string): string[] => { * * @throws If `digit` is not a valid standard digit. */ -function toLocaleDigit(locale: string, digit: string): string { +function toLocaleDigit(locale: Locale, digit: string): string { const index = STANDARD_DIGITS.indexOf(digit); if (index < 0) { throw new Error(`"${digit}" must be in ${JSON.stringify(STANDARD_DIGITS)}`); @@ -54,7 +58,7 @@ function toLocaleDigit(locale: string, digit: string): string { * * @throws If `localeDigit` is not a valid locale digit. */ -function fromLocaleDigit(locale: string, localeDigit: string): string { +function fromLocaleDigit(locale: Locale, localeDigit: string): string { const index = getLocaleDigits(locale).indexOf(localeDigit); if (index < 0) { throw new Error(`"${localeDigit}" must be in ${JSON.stringify(getLocaleDigits(locale))}`); diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index fd49902af369..6910bc7e9bdb 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -138,3 +138,4 @@ function getDevicePreferredLocale(): string { } export {translate, translateLocal, translateIfPhraseKey, arrayToString, getDevicePreferredLocale}; +export type {PhraseParameters, Phrase}; diff --git a/src/libs/NumberFormatUtils.ts b/src/libs/NumberFormatUtils.ts index 7c81e71f4db8..b077f0ce0862 100644 --- a/src/libs/NumberFormatUtils.ts +++ b/src/libs/NumberFormatUtils.ts @@ -1,8 +1,11 @@ -function format(locale: string, number: number, options?: Intl.NumberFormatOptions): string { +import {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; + +function format(locale: ValueOf, number: number, options?: Intl.NumberFormatOptions): string { return new Intl.NumberFormat(locale, options).format(number); } -function formatToParts(locale: string, number: number, options?: Intl.NumberFormatOptions): Intl.NumberFormatPart[] { +function formatToParts(locale: ValueOf, number: number, options?: Intl.NumberFormatOptions): Intl.NumberFormatPart[] { return new Intl.NumberFormat(locale, options).formatToParts(number); } diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 01f8c2f4916b..db024e8db4cc 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -9,7 +9,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {DateOfBirthForm, PersonalDetails, PrivatePersonalDetails} from '@src/types/onyx'; -import {Timezone} from '@src/types/onyx/PersonalDetails'; +import {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails'; type FirstAndLastName = { firstName: string; @@ -313,7 +313,7 @@ function updateAutomaticTimezone(timezone: Timezone) { * Updates user's 'selected' timezone, then navigates to the * initial Timezone page. */ -function updateSelectedTimezone(selectedTimezone: string) { +function updateSelectedTimezone(selectedTimezone: SelectedTimezone) { const timezone: Timezone = { selected: selectedTimezone, }; diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index 5637d7e5fdcf..d6c22263e142 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -1,8 +1,11 @@ +import TIMEZONES from '@src/TIMEZONES'; import * as OnyxCommon from './OnyxCommon'; +type SelectedTimezone = (typeof TIMEZONES)[number]; + type Timezone = { /** Value of selected timezone */ - selected?: string; + selected?: SelectedTimezone; /** Whether timezone is automatically set */ automatic?: boolean; @@ -67,4 +70,5 @@ type PersonalDetails = { }; export default PersonalDetails; -export type {Timezone}; + +export type {Timezone, SelectedTimezone};