Skip to content

Commit bbae06f

Browse files
authored
Merge pull request Expensify#69945 from sofi-a/69562-fix-localization-dates-not-being-localized-correctly
2 parents 969bbac + 59c0017 commit bbae06f

47 files changed

Lines changed: 442 additions & 202 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/CONST/index.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,8 @@ const CONST = {
451451
SHORT_DATE_FORMAT: 'MM-dd',
452452
MONTH_DAY_YEAR_ABBR_FORMAT: 'MMM d, yyyy',
453453
MONTH_DAY_YEAR_FORMAT: 'MMMM d, yyyy',
454+
MONTH_DAY_WEEKDAY_ABBR_FORMAT: 'EEEE, MMM d',
455+
MONTH_DAY_WEEKDAY_YEAR_ABBR_FORMAT: 'EEEE, MMM d, yyyy',
454456
FNS_TIMEZONE_FORMAT_STRING: "yyyy-MM-dd'T'HH:mm:ssXXX",
455457
FNS_DB_FORMAT_STRING: 'yyyy-MM-dd HH:mm:ss.SSS',
456458
LONG_DATE_FORMAT_WITH_WEEKDAY: 'eeee, MMMM d, yyyy',
@@ -7500,6 +7502,20 @@ const FRAUD_PROTECTION_EVENT = {
75007502
NEW_EMAILS_INVITED: 'NewEmailsInvited',
75017503
};
75027504

7505+
const DATE_TIME_FORMAT_OPTIONS: Record<string, Intl.DateTimeFormatOptions> = {
7506+
[CONST.DATE.FNS_FORMAT_STRING]: {year: 'numeric', month: '2-digit', day: '2-digit'},
7507+
[CONST.DATE.LOCAL_TIME_FORMAT]: {timeStyle: 'short'},
7508+
[CONST.DATE.MONTH_FORMAT]: {month: 'long'},
7509+
[CONST.DATE.WEEKDAY_TIME_FORMAT]: {weekday: 'long'},
7510+
[CONST.DATE.MONTH_DAY_WEEKDAY_ABBR_FORMAT]: {weekday: 'long', month: 'short', day: 'numeric'},
7511+
[CONST.DATE.MONTH_DAY_WEEKDAY_YEAR_ABBR_FORMAT]: {weekday: 'long', month: 'short', day: 'numeric', year: 'numeric'},
7512+
[CONST.DATE.MONTH_DAY_YEAR_FORMAT]: {dateStyle: 'long'},
7513+
[CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT]: {month: 'short', day: 'numeric', year: 'numeric'},
7514+
[CONST.DATE.LONG_DATE_FORMAT_WITH_WEEKDAY]: {dateStyle: 'full'},
7515+
FNS_DATE_WITH_LOCAL_TIME_FORMAT: {year: 'numeric', month: '2-digit', day: '2-digit', hour: 'numeric', minute: '2-digit'},
7516+
SHORT_DATE_WITH_LOCAL_TIME_FORMAT: {month: '2-digit', day: '2-digit', hour: 'numeric', minute: '2-digit'},
7517+
};
7518+
75037519
type Country = keyof typeof CONST.ALL_COUNTRIES;
75047520

75057521
type IOUType = ValueOf<typeof CONST.IOU.TYPE>;
@@ -7513,6 +7529,6 @@ type CancellationType = ValueOf<typeof CONST.CANCELLATION_TYPE>;
75137529

75147530
export type {Country, IOUAction, IOUType, IOURequestType, SubscriptionType, FeedbackSurveyOptionID, CancellationType, OnboardingInvite, OnboardingAccounting, IOUActionParams};
75157531

7516-
export {CONTINUATION_DETECTION_SEARCH_FILTER_KEYS, TASK_TO_FEATURE, FRAUD_PROTECTION_EVENT};
7532+
export {CONTINUATION_DETECTION_SEARCH_FILTER_KEYS, TASK_TO_FEATURE, FRAUD_PROTECTION_EVENT, DATE_TIME_FORMAT_OPTIONS};
75177533

75187534
export default CONST;

src/components/AutoUpdateTime.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type AutoUpdateTimeProps = {
1616
};
1717

1818
function AutoUpdateTime({timezone}: AutoUpdateTimeProps) {
19-
const {translate, getLocalDateFromDatetime} = useLocalize();
19+
const {translate, getLocalDateFromDatetime, preferredLocale} = useLocalize();
2020
const styles = useThemeStyles();
2121
/** @returns Returns the locale Date object */
2222
const getCurrentUserLocalTime = useCallback(() => getLocalDateFromDatetime(undefined, timezone.selected), [getLocalDateFromDatetime, timezone.selected]);
@@ -50,7 +50,7 @@ function AutoUpdateTime({timezone}: AutoUpdateTimeProps) {
5050
<View style={[styles.w100, styles.detailsPageSectionContainer]}>
5151
<MenuItemWithTopDescription
5252
style={[styles.ph0]}
53-
title={`${DateUtils.formatToLocalTime(currentUserLocalTime)} ${timezoneName}`}
53+
title={`${DateUtils.formatToLocalTime(currentUserLocalTime, preferredLocale)} ${timezoneName}`}
5454
description={translate('detailsPage.localTime')}
5555
interactive={false}
5656
/>

src/components/DatePicker/CalendarPicker/generateMonthMatrix.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {addDays, format, getDay, getDaysInMonth, startOfMonth} from 'date-fns';
22
import DateUtils from '@libs/DateUtils';
3+
import CONST from '@src/CONST';
4+
import type Locale from '@src/types/onyx/Locale';
35

46
/**
57
* Generates a matrix representation of a month's calendar given the year and month.
@@ -8,7 +10,7 @@ import DateUtils from '@libs/DateUtils';
810
* @param month - The month (0-indexed) for which to generate the month matrix.
911
* @returns A 2D array of the month's calendar days, with null values representing days outside the current month.
1012
*/
11-
export default function generateMonthMatrix(year: number, month: number) {
13+
export default function generateMonthMatrix(year: number, month: number, locale: Locale = CONST.LOCALES.DEFAULT) {
1214
if (year < 0) {
1315
throw new Error('Year cannot be less than 0');
1416
}
@@ -20,7 +22,7 @@ export default function generateMonthMatrix(year: number, month: number) {
2022
}
2123

2224
// Get the week day for the end of week
23-
const weekEndsOn = DateUtils.getWeekEndsOn();
25+
const weekEndsOn = DateUtils.getWeekEndsOn(locale);
2426

2527
// Get the number of days in the month and the first day of the month
2628
const firstDayOfMonth = startOfMonth(new Date(year, month, 1));

src/components/DatePicker/CalendarPicker/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,15 @@ function CalendarPicker({
6161
const {isSmallScreenWidth} = useResponsiveLayout();
6262
const styles = useThemeStyles();
6363
const themeStyles = useThemeStyles();
64-
const {translate} = useLocalize();
64+
const {translate, preferredLocale} = useLocalize();
6565
const pressableRef = useRef<View>(null);
6666
const [currentDateView, setCurrentDateView] = useState(() => getInitialCurrentDateView(value, minDate, maxDate));
6767
const [isYearPickerVisible, setIsYearPickerVisible] = useState(false);
6868
const isFirstRender = useRef(true);
6969

7070
const currentMonthView = currentDateView.getMonth();
7171
const currentYearView = currentDateView.getFullYear();
72-
const calendarDaysMatrix = generateMonthMatrix(currentYearView, currentMonthView);
72+
const calendarDaysMatrix = generateMonthMatrix(currentYearView, currentMonthView, preferredLocale);
7373
const initialHeight = (calendarDaysMatrix?.length || CONST.MAX_CALENDAR_PICKER_ROWS) * CONST.CALENDAR_PICKER_DAY_HEIGHT;
7474
const heightValue = useSharedValue(initialHeight);
7575

@@ -150,8 +150,8 @@ function CalendarPicker({
150150
});
151151
};
152152

153-
const monthNames = DateUtils.getMonthNames().map((month) => Str.UCFirst(month));
154-
const daysOfWeek = DateUtils.getDaysOfWeek().map((day) => day.toUpperCase());
153+
const monthNames = DateUtils.getMonthNames(preferredLocale).map((month) => Str.UCFirst(month));
154+
const daysOfWeek = DateUtils.getDaysOfWeek(preferredLocale).map((day) => day.toUpperCase());
155155
const hasAvailableDatesNextMonth = startOfDay(new Date(maxDate)) > endOfMonth(new Date(currentDateView));
156156
const hasAvailableDatesPrevMonth = endOfDay(new Date(minDate)) < startOfMonth(new Date(currentDateView));
157157

src/components/DatePicker/index.tsx

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
99
import useWindowDimensions from '@hooks/useWindowDimensions';
1010
import mergeRefs from '@libs/mergeRefs';
1111
import {setDraftValues} from '@userActions/FormActions';
12-
import CONST from '@src/CONST';
12+
import CONST, {DATE_TIME_FORMAT_OPTIONS} from '@src/CONST';
1313
import DatePickerModal from './DatePickerModal';
1414
import type {DateInputWithPickerProps} from './types';
1515

@@ -36,7 +36,7 @@ function DatePicker({
3636
const icons = useMemoizedLazyExpensifyIcons(['Calendar'] as const);
3737
const styles = useThemeStyles();
3838
const {windowHeight, windowWidth} = useWindowDimensions();
39-
const {translate} = useLocalize();
39+
const {preferredLocale} = useLocalize();
4040
const [isModalVisible, setIsModalVisible] = useState(false);
4141
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
4242
const [selectedDate, setSelectedDate] = useState(value || defaultValue || undefined);
@@ -46,6 +46,38 @@ function DatePicker({
4646
const [isInverted, setIsInverted] = useState(false);
4747
const isAutoFocused = useRef(false);
4848

49+
const formattedValue = useMemo(() => {
50+
if (!selectedDate) {
51+
return '';
52+
}
53+
const date = new Date(selectedDate);
54+
if (Number.isNaN(date.getTime())) {
55+
return '';
56+
}
57+
return Intl.DateTimeFormat(preferredLocale, DATE_TIME_FORMAT_OPTIONS[CONST.DATE.FNS_FORMAT_STRING]).format(date);
58+
}, [selectedDate, preferredLocale]);
59+
60+
const computedPlaceholder = useMemo(() => {
61+
if (placeholder) {
62+
return placeholder;
63+
}
64+
return Intl.DateTimeFormat(preferredLocale, DATE_TIME_FORMAT_OPTIONS[CONST.DATE.FNS_FORMAT_STRING])
65+
.formatToParts()
66+
.map((part) => {
67+
switch (part.type) {
68+
case 'day':
69+
return 'DD';
70+
case 'month':
71+
return 'MM';
72+
case 'year':
73+
return 'YYYY';
74+
default:
75+
return part.value;
76+
}
77+
})
78+
.join('');
79+
}, [placeholder, preferredLocale]);
80+
4981
useEffect(() => {
5082
if (shouldSaveDraft && formID) {
5183
setDraftValues(formID, {[inputID]: selectedDate});
@@ -133,8 +165,8 @@ function DatePicker({
133165
label={label}
134166
accessibilityLabel={label}
135167
role={CONST.ROLE.PRESENTATION}
136-
value={selectedDate}
137-
placeholder={placeholder ?? translate('common.dateFormat')}
168+
value={formattedValue}
169+
placeholder={computedPlaceholder}
138170
errorText={errorText}
139171
inputStyle={styles.pointerEventsNone}
140172
disabled={disabled}

src/components/DistanceEReceipt.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils';
99
import {getTransactionDetails} from '@libs/ReportUtils';
1010
import {getWaypointIndex, hasReceipt, isFetchingWaypointsFromServer} from '@libs/TransactionUtils';
1111
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
12+
import CONST from '@src/CONST';
1213
import type {TranslationPaths} from '@src/languages/types';
1314
import type {Transaction} from '@src/types/onyx';
1415
import type {WaypointCollection} from '@src/types/onyx/Transaction';
@@ -29,10 +30,15 @@ type DistanceEReceiptProps = {
2930

3031
function DistanceEReceipt({transaction, hoverPreview = false}: DistanceEReceiptProps) {
3132
const styles = useThemeStyles();
32-
const {translate} = useLocalize();
33+
const {translate, preferredLocale} = useLocalize();
3334
const icons = useMemoizedLazyExpensifyIcons(['ExpensifyWordmark'] as const);
3435
const thumbnail = hasReceipt(transaction) ? getThumbnailAndImageURIs(transaction).thumbnail : null;
35-
const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = getTransactionDetails(transaction) ?? {};
36+
const {
37+
amount: transactionAmount,
38+
currency: transactionCurrency,
39+
merchant: transactionMerchant,
40+
created: transactionDate,
41+
} = getTransactionDetails(transaction, CONST.DATE.FNS_FORMAT_STRING, undefined, undefined, undefined, undefined, preferredLocale) ?? {};
3642
const formattedTransactionAmount = convertToDisplayString(transactionAmount, transactionCurrency);
3743
const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail ?? '');
3844
const waypoints = useMemo(() => transaction?.comment?.waypoints ?? {}, [transaction?.comment?.waypoints]);

src/components/EReceipt.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const backgroundImageMinWidth: number = variables.eReceiptBackgroundImageMinWidt
3939
function EReceipt({transactionID, transactionItem, onLoad, isThumbnail = false}: EReceiptProps) {
4040
const styles = useThemeStyles();
4141
const StyleUtils = useStyleUtils();
42-
const {translate} = useLocalize();
42+
const {translate, preferredLocale} = useLocalize();
4343
const theme = useTheme();
4444
const icons = useMemoizedLazyExpensifyIcons(['ReceiptBody', 'ExpensifyWordmark'] as const);
4545
const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true});
@@ -56,7 +56,7 @@ function EReceipt({transactionID, transactionItem, onLoad, isThumbnail = false}:
5656
created: transactionDate,
5757
cardID: transactionCardID,
5858
cardName: transactionCardName,
59-
} = getTransactionDetails(transactionItem ?? transaction, CONST.DATE.MONTH_DAY_YEAR_FORMAT) ?? {};
59+
} = getTransactionDetails(transactionItem ?? transaction, CONST.DATE.MONTH_DAY_YEAR_FORMAT, undefined, undefined, undefined, undefined, preferredLocale) ?? {};
6060
const formattedAmount = convertToDisplayString(transactionAmount, transactionCurrency);
6161
const currency = getCurrencySymbol(transactionCurrency ?? '');
6262
const amount = currency ? formattedAmount.replace(currency, '') : formattedAmount;

src/components/LHNOptionsList/OptionRowLHN.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ function OptionRowLHN({
7979

8080
const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(tooltipToRender, shouldShowTooltip);
8181

82-
const {translate} = useLocalize();
82+
const {translate, preferredLocale} = useLocalize();
8383
const [isContextMenuActive, setIsContextMenuActive] = useState(false);
8484
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
8585

@@ -160,7 +160,7 @@ function OptionRowLHN({
160160
const statusText = optionItem.status?.text ?? '';
161161
const statusClearAfterDate = optionItem.status?.clearAfter ?? '';
162162
const currentSelectedTimezone = currentUserPersonalDetails?.timezone?.selected ?? CONST.DEFAULT_TIME_ZONE.selected;
163-
const formattedDate = DateUtils.getStatusUntilDate(statusClearAfterDate, optionItem?.timezone?.selected ?? CONST.DEFAULT_TIME_ZONE.selected, currentSelectedTimezone);
163+
const formattedDate = DateUtils.getStatusUntilDate(statusClearAfterDate, optionItem?.timezone?.selected ?? CONST.DEFAULT_TIME_ZONE.selected, currentSelectedTimezone, preferredLocale);
164164
const statusContent = formattedDate ? `${statusText ? `${statusText} ` : ''}(${formattedDate})` : statusText;
165165
const isStatusVisible = !!emojiCode && isOneOnOneChat(!isEmptyObject(report) ? report : undefined);
166166

src/components/MoneyRequestConfirmationListFooter.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {emailSelector} from '@selectors/Session';
2-
import {format} from 'date-fns';
32
import {Str} from 'expensify-common';
43
import {deepEqual} from 'fast-equals';
54
import React, {memo, useMemo} from 'react';
@@ -36,7 +35,7 @@ import {
3635
} from '@libs/TransactionUtils';
3736
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
3837
import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow';
39-
import CONST from '@src/CONST';
38+
import CONST, {DATE_TIME_FORMAT_OPTIONS} from '@src/CONST';
4039
import type {IOUAction, IOUType} from '@src/CONST';
4140
import ONYXKEYS from '@src/ONYXKEYS';
4241
import ROUTES from '@src/ROUTES';
@@ -265,7 +264,7 @@ function MoneyRequestConfirmationListFooter({
265264
}: MoneyRequestConfirmationListFooterProps) {
266265
const icons = useMemoizedLazyExpensifyIcons(['Stopwatch', 'CalendarSolid'] as const);
267266
const styles = useThemeStyles();
268-
const {translate, toLocaleDigit, localeCompare} = useLocalize();
267+
const {translate, toLocaleDigit, localeCompare, preferredLocale} = useLocalize();
269268
const {isOffline} = useNetwork();
270269

271270
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
@@ -280,6 +279,11 @@ function MoneyRequestConfirmationListFooter({
280279
const isUnreported = transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;
281280
const isCreatingTrackExpense = action === CONST.IOU.ACTION.CREATE && iouType === CONST.IOU.TYPE.TRACK;
282281

282+
const formattedCreatedDate = useMemo(() => {
283+
const formatter = new Intl.DateTimeFormat(preferredLocale, DATE_TIME_FORMAT_OPTIONS[CONST.DATE.FNS_FORMAT_STRING]);
284+
return formatter.format(new Date(iouCreated ?? Date.now()));
285+
}, [iouCreated, preferredLocale]);
286+
283287
const decodedCategoryName = useMemo(() => getDecodedCategoryName(iouCategory), [iouCategory]);
284288

285289
const allOutstandingReports = useMemo(() => {
@@ -346,6 +350,7 @@ function MoneyRequestConfirmationListFooter({
346350
}, [allReports, shouldUseTransactionReport, transaction?.reportID, outstandingReportID]);
347351

348352
const reportName = useMemo(() => {
353+
// eslint-disable-next-line @typescript-eslint/no-deprecated
349354
const name = getReportName(selectedReport, selectedPolicy);
350355
if (!name) {
351356
return isUnreported ? translate('common.none') : translate('iou.newReport');
@@ -624,7 +629,7 @@ function MoneyRequestConfirmationListFooter({
624629
key={translate('common.date')}
625630
shouldShowRightIcon={!isReadOnly}
626631
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
627-
title={iouCreated || format(new Date(), CONST.DATE.FNS_FORMAT_STRING)}
632+
title={formattedCreatedDate}
628633
description={translate('common.date')}
629634
style={[styles.moneyRequestMenuItem]}
630635
titleStyle={styles.flex1}

src/components/PerDiemEReceipt.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function getPerDiemDates(merchant: string) {
5151
function PerDiemEReceipt({transactionID}: PerDiemEReceiptProps) {
5252
const styles = useThemeStyles();
5353
const StyleUtils = useStyleUtils();
54-
const {translate} = useLocalize();
54+
const {translate, preferredLocale} = useLocalize();
5555
const icons = useMemoizedLazyExpensifyIcons(['ExpensifyWordmark'] as const);
5656
const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transactionID)}`, {
5757
canBeMissing: true,
@@ -60,7 +60,11 @@ function PerDiemEReceipt({transactionID}: PerDiemEReceiptProps) {
6060
// Get receipt colorway, or default to Yellow.
6161
const {backgroundColor: primaryColor, color: secondaryColor} = StyleUtils.getEReceiptColorStyles(StyleUtils.getEReceiptColorCode(transaction)) ?? {};
6262

63-
const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant} = getTransactionDetails(transaction, CONST.DATE.MONTH_DAY_YEAR_FORMAT) ?? {};
63+
const {
64+
amount: transactionAmount,
65+
currency: transactionCurrency,
66+
merchant: transactionMerchant,
67+
} = getTransactionDetails(transaction, CONST.DATE.MONTH_DAY_YEAR_FORMAT, undefined, undefined, undefined, undefined, preferredLocale) ?? {};
6468
const ratesDescription = computeDefaultPerDiemExpenseRates(transaction?.comment?.customUnit ?? {}, transactionCurrency ?? '');
6569
const datesDescription = getPerDiemDates(transactionMerchant ?? '');
6670
const destination = getPerDiemDestination(transactionMerchant ?? '');

0 commit comments

Comments
 (0)