diff --git a/app/components/formatted_date/__snapshots__/index.test.tsx.snap b/app/components/formatted_date/__snapshots__/index.test.tsx.snap index 75a0d89363a..bfafd8e09ee 100644 --- a/app/components/formatted_date/__snapshots__/index.test.tsx.snap +++ b/app/components/formatted_date/__snapshots__/index.test.tsx.snap @@ -1,5 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[` should default when timezone is not found 1`] = ` + + 10:01 AM + +`; + exports[` should match snapshot for 'bg' locale and '{"dateStyle": "medium"}' format 1`] = ` 26.10.2024 г. @@ -797,3 +803,645 @@ exports[` should render with a manual user time 1`] = ` Oct 26, 2024 `; + +exports[` should render with timezone Africa/Abidjan 1`] = ` + + 10:01 AM + +`; + +exports[` should render with timezone Africa/Addis_Ababa 1`] = ` + + 1:01 PM + +`; + +exports[` should render with timezone Africa/Algiers 1`] = ` + + 11:01 AM + +`; + +exports[` should render with timezone Africa/Blantyre 1`] = ` + + 12:01 PM + +`; + +exports[` should render with timezone Africa/Cairo 1`] = ` + + 1:01 PM + +`; + +exports[` should render with timezone Africa/Casablanca 1`] = ` + + 11:01 AM + +`; + +exports[` should render with timezone Africa/Ceuta 1`] = ` + + 12:01 PM + +`; + +exports[` should render with timezone Africa/Tripoli 1`] = ` + + 12:01 PM + +`; + +exports[` should render with timezone Africa/Windhoek 1`] = ` + + 12:01 PM + +`; + +exports[` should render with timezone America/Anchorage 1`] = ` + + 2:01 AM + +`; + +exports[` should render with timezone America/Anguilla 1`] = ` + + 6:01 AM + +`; + +exports[` should render with timezone America/Araguaina 1`] = ` + + 7:01 AM + +`; + +exports[` should render with timezone America/Argentina/Buenos_Aires 1`] = ` + + 7:01 AM + +`; + +exports[` should render with timezone America/Asuncion 1`] = ` + + 7:01 AM + +`; + +exports[` should render with timezone America/Bahia 1`] = ` + + 7:01 AM + +`; + +exports[` should render with timezone America/Bahia_Banderas 1`] = ` + + 4:01 AM + +`; + +exports[` should render with timezone America/Belize 1`] = ` + + 4:01 AM + +`; + +exports[` should render with timezone America/Bogota 1`] = ` + + 5:01 AM + +`; + +exports[` should render with timezone America/Boise 1`] = ` + + 4:01 AM + +`; + +exports[` should render with timezone America/Campo_Grande 1`] = ` + + 6:01 AM + +`; + +exports[` should render with timezone America/Caracas 1`] = ` + + 6:01 AM + +`; + +exports[` should render with timezone America/Chicago 1`] = ` + + 5:01 AM + +`; + +exports[` should render with timezone America/Chihuahua 1`] = ` + + 4:01 AM + +`; + +exports[` should render with timezone America/Creston 1`] = ` + + 3:01 AM + +`; + +exports[` should render with timezone America/Danmarkshavn 1`] = ` + + 10:01 AM + +`; + +exports[` should render with timezone America/Detroit 1`] = ` + + 6:01 AM + +`; + +exports[` should render with timezone America/Glace_Bay 1`] = ` + + 7:01 AM + +`; + +exports[` should render with timezone America/Godthab 1`] = ` + + 9:01 AM + +`; + +exports[` should render with timezone America/Havana 1`] = ` + + 6:01 AM + +`; + +exports[` should render with timezone America/Indiana/Marengo 1`] = ` + + 6:01 AM + +`; + +exports[` should render with timezone America/Los_Angeles 1`] = ` + + 3:01 AM + +`; + +exports[` should render with timezone America/Montevideo 1`] = ` + + 7:01 AM + +`; + +exports[` should render with timezone America/Noronha 1`] = ` + + 8:01 AM + +`; + +exports[` should render with timezone America/Regina 1`] = ` + + 4:01 AM + +`; + +exports[` should render with timezone America/Santa_Isabel 1`] = ` + + 3:01 AM + +`; + +exports[` should render with timezone America/Santiago 1`] = ` + + 7:01 AM + +`; + +exports[` should render with timezone America/Sao_Paulo 1`] = ` + + 7:01 AM + +`; + +exports[` should render with timezone America/Scoresbysund 1`] = ` + + 9:01 AM + +`; + +exports[` should render with timezone America/St_Johns 1`] = ` + + 7:31 AM + +`; + +exports[` should render with timezone America/Tijuana 1`] = ` + + 3:01 AM + +`; + +exports[` should render with timezone Antarctica/Casey 1`] = ` + + 6:01 PM + +`; + +exports[` should render with timezone Antarctica/Davis 1`] = ` + + 5:01 PM + +`; + +exports[` should render with timezone Antarctica/DumontDUrville 1`] = ` + + 8:01 PM + +`; + +exports[` should render with timezone Antarctica/Macquarie 1`] = ` + + 9:01 PM + +`; + +exports[` should render with timezone Antarctica/Mawson 1`] = ` + + 3:01 PM + +`; + +exports[` should render with timezone Antarctica/McMurdo 1`] = ` + + 11:01 PM + +`; + +exports[` should render with timezone Antarctica/Vostok 1`] = ` + + 3:01 PM + +`; + +exports[` should render with timezone Arctic/Longyearbyen 1`] = ` + + 12:01 PM + +`; + +exports[` should render with timezone Asia/Aden 1`] = ` + + 1:01 PM + +`; + +exports[` should render with timezone Asia/Amman 1`] = ` + + 1:01 PM + +`; + +exports[` should render with timezone Asia/Anadyr 1`] = ` + + 10:01 PM + +`; + +exports[` should render with timezone Asia/Baghdad 1`] = ` + + 1:01 PM + +`; + +exports[` should render with timezone Asia/Baku 1`] = ` + + 2:01 PM + +`; + +exports[` should render with timezone Asia/Beirut 1`] = ` + + 1:01 PM + +`; + +exports[` should render with timezone Asia/Brunei 1`] = ` + + 6:01 PM + +`; + +exports[` should render with timezone Asia/Chita 1`] = ` + + 7:01 PM + +`; + +exports[` should render with timezone Asia/Choibalsan 1`] = ` + + 6:01 PM + +`; + +exports[` should render with timezone Asia/Colombo 1`] = ` + + 3:31 PM + +`; + +exports[` should render with timezone Asia/Damascus 1`] = ` + + 1:01 PM + +`; + +exports[` should render with timezone Asia/Dhaka 1`] = ` + + 4:01 PM + +`; + +exports[` should render with timezone Asia/Dili 1`] = ` + + 7:01 PM + +`; + +exports[` should render with timezone Asia/Dubai 1`] = ` + + 2:01 PM + +`; + +exports[` should render with timezone Asia/Hong_Kong 1`] = ` + + 6:01 PM + +`; + +exports[` should render with timezone Asia/Irkutsk 1`] = ` + + 6:01 PM + +`; + +exports[` should render with timezone Asia/Jerusalem 1`] = ` + + 1:01 PM + +`; + +exports[` should render with timezone Asia/Kabul 1`] = ` + + 2:31 PM + +`; + +exports[` should render with timezone Asia/Kamchatka 1`] = ` + + 10:01 PM + +`; + +exports[` should render with timezone Asia/Karachi 1`] = ` + + 3:01 PM + +`; + +exports[` should render with timezone Asia/Kathmandu 1`] = ` + + 3:46 PM + +`; + +exports[` should render with timezone Asia/Kolkata 1`] = ` + + 3:31 PM + +`; + +exports[` should render with timezone Asia/Krasnoyarsk 1`] = ` + + 5:01 PM + +`; + +exports[` should render with timezone Asia/Nicosia 1`] = ` + + 1:01 PM + +`; + +exports[` should render with timezone Asia/Nicosia 2`] = ` + + 1:01 PM + +`; + +exports[` should render with timezone Asia/Novokuznetsk 1`] = ` + + 5:01 PM + +`; + +exports[` should render with timezone Asia/Pyongyang 1`] = ` + + 7:01 PM + +`; + +exports[` should render with timezone Asia/Rangoon 1`] = ` + + 4:31 PM + +`; + +exports[` should render with timezone Asia/Sakhalin 1`] = ` + + 9:01 PM + +`; + +exports[` should render with timezone Asia/Taipei 1`] = ` + + 6:01 PM + +`; + +exports[` should render with timezone Asia/Tbilisi 1`] = ` + + 2:01 PM + +`; + +exports[` should render with timezone Asia/Tehran 1`] = ` + + 1:31 PM + +`; + +exports[` should render with timezone Asia/Yekaterinburg 1`] = ` + + 3:01 PM + +`; + +exports[` should render with timezone Asia/Yerevan 1`] = ` + + 2:01 PM + +`; + +exports[` should render with timezone Atlantic/Canary 1`] = ` + + 11:01 AM + +`; + +exports[` should render with timezone Atlantic/Cape_Verde 1`] = ` + + 9:01 AM + +`; + +exports[` should render with timezone Australia/Adelaide 1`] = ` + + 8:31 PM + +`; + +exports[` should render with timezone Australia/Brisbane 1`] = ` + + 8:01 PM + +`; + +exports[` should render with timezone Australia/Currie 1`] = ` + + 9:01 PM + +`; + +exports[` should render with timezone Australia/Darwin 1`] = ` + + 7:31 PM + +`; + +exports[` should render with timezone Australia/Melbourne 1`] = ` + + 9:01 PM + +`; + +exports[` should render with timezone Etc/GMT+10 1`] = ` + + 12:01 AM + +`; + +exports[` should render with timezone Etc/GMT+11 1`] = ` + + 11:01 PM + +`; + +exports[` should render with timezone Etc/GMT+12 1`] = ` + + 10:01 PM + +`; + +exports[` should render with timezone Etc/GMT-12 1`] = ` + + 10:01 PM + +`; + +exports[` should render with timezone Etc/GMT-13 1`] = ` + + 11:01 PM + +`; + +exports[` should render with timezone Europe/Astrakhan 1`] = ` + + 2:01 PM + +`; + +exports[` should render with timezone Europe/Belgrade 1`] = ` + + 12:01 PM + +`; + +exports[` should render with timezone Europe/Guernsey 1`] = ` + + 11:01 AM + +`; + +exports[` should render with timezone Europe/Helsinki 1`] = ` + + 1:01 PM + +`; + +exports[` should render with timezone Europe/Isle_of_Man 1`] = ` + + 11:01 AM + +`; + +exports[` should render with timezone Europe/Istanbul 1`] = ` + + 1:01 PM + +`; + +exports[` should render with timezone Europe/Kaliningrad 1`] = ` + + 12:01 PM + +`; + +exports[` should render with timezone Europe/Kirov 1`] = ` + + 1:01 PM + +`; + +exports[` should render with timezone Europe/Sarajevo 1`] = ` + + 12:01 PM + +`; + +exports[` should render with timezone Indian/Mahe 1`] = ` + + 2:01 PM + +`; + +exports[` should render with timezone Pacific/Apia 1`] = ` + + 11:01 PM + +`; + +exports[` should render with timezone Pacific/Fiji 1`] = ` + + 10:01 PM + +`; + +exports[` should render with timezone undefined 1`] = ` + + 10:01 AM + +`; diff --git a/app/components/formatted_date/index.test.tsx b/app/components/formatted_date/index.test.tsx index 6e68039f48e..feab8eb7261 100644 --- a/app/components/formatted_date/index.test.tsx +++ b/app/components/formatted_date/index.test.tsx @@ -2,13 +2,19 @@ // See LICENSE.txt for license information. import React from 'react'; +import timezones from 'timezones.json'; import {renderWithIntl} from '@test/intl-test-helper'; +import {logDebug} from '@utils/log'; import locales from '../../i18n/languages'; import FormattedDate, {type FormattedDateFormat} from './index'; +jest.mock('@utils/log', () => ({ + logDebug: jest.fn(), +})); + const DATE = new Date('2024-10-26T10:01:04.653Z'); const FORMATS = [ undefined, @@ -34,6 +40,24 @@ const TEST_MATRIX = Object.keys(locales). map((locale) => FORMATS.map<[string, FormattedDateFormat | undefined]>((format) => [locale, format])). flat(1); +function getTimezoneTestsCases() { + // Mimics the logic for the timezones offered by the web app + // in webapp/channels/src/components/user_settings/display/manage_timezones/manage_timezones.tsx + let index = 0; + const testCases = []; + let previousTimezone = ''; + for (const timezone of timezones) { + if (timezone.utc[index] === previousTimezone) { + index++; + } else { + index = 0; + } + testCases.push([timezone.utc[index]]); + previousTimezone = timezone.utc[index]; + } + return testCases; +} + describe('', () => { it.each(TEST_MATRIX)("should match snapshot for '%s' locale and '%p' format", (locale, format) => { const wrapper = renderWithIntl( @@ -76,4 +100,42 @@ describe('', () => { // Just check that the component render as automatic timezone is environment dependant expect(wrapper.toJSON()).toBeTruthy(); }); + + it.each(getTimezoneTestsCases())('should render with timezone %s', (timezone) => { + const wrapper = renderWithIntl( + , + ); + expect(wrapper.queryByText('Unknown')).not.toBeTruthy(); + expect(logDebug).not.toHaveBeenCalled(); + expect(wrapper.toJSON()).toMatchSnapshot(); + }); + + it('should default when timezone is not found', () => { + const wrapper = renderWithIntl( + , + ); + expect(wrapper.queryByText('Unknown')).not.toBeTruthy(); + expect(logDebug).toHaveBeenCalledTimes(1); + expect(wrapper.toJSON()).toMatchSnapshot(); + }); + + it('should show unknown on other errors', () => { + const wrapper = renderWithIntl( + , + ); + expect(wrapper.queryByText('Unknown')).toBeTruthy(); + expect(logDebug).toHaveBeenCalledTimes(2); + }); }); diff --git a/app/components/formatted_date/index.tsx b/app/components/formatted_date/index.tsx index 6f56d8df049..f8b318155b4 100644 --- a/app/components/formatted_date/index.tsx +++ b/app/components/formatted_date/index.tsx @@ -5,6 +5,8 @@ import React from 'react'; import {useIntl} from 'react-intl'; import {Text, type TextProps} from 'react-native'; +import {logDebug} from '@utils/log'; + export type FormattedDateFormat = Exclude; type FormattedDateProps = TextProps & { @@ -21,7 +23,7 @@ const FormattedDate = ({ value, ...props }: FormattedDateProps) => { - const {locale} = useIntl(); + const {locale, formatMessage} = useIntl(); let timeZone: string | undefined; if (timezone && typeof timezone === 'object') { @@ -30,10 +32,29 @@ const FormattedDate = ({ timeZone = timezone ?? undefined; } - const formattedDate = new Intl.DateTimeFormat(locale, { - ...format, - timeZone, - }).format(new Date(value)); + let formattedDate; + try { + formattedDate = new Intl.DateTimeFormat(locale, { + ...format, + timeZone, + }).format(new Date(value)); + } catch (error) { + logDebug('Failed to format date', {locale, timezone}, error); + } + + if (!formattedDate) { + try { + formattedDate = new Intl.DateTimeFormat(locale, { + ...format, + }).format(new Date(value)); + } catch (error) { + logDebug('Failed to format default date', {locale}, error); + } + } + + if (!formattedDate) { + formattedDate = formatMessage({id: 'date.unknown', defaultMessage: 'Unknown'}); + } return {formattedDate}; }; diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index b24aedf195b..37f541f3af7 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -293,6 +293,7 @@ "custom_status.suggestions.working_from_home": "Working from home", "date_separator.today": "Today", "date_separator.yesterday": "Yesterday", + "date.unknown": "Unknown", "default_skin_tone": "Default Skin Tone", "display_settings.clock.military": "24-hour", "display_settings.clock.standard": "12-hour", diff --git a/index.ts b/index.ts index 2a296a27ff3..2a87c4cd233 100644 --- a/index.ts +++ b/index.ts @@ -46,7 +46,7 @@ if (global.HermesInternal) { require('@formatjs/intl-pluralrules/polyfill-force'); require('@formatjs/intl-numberformat/polyfill-force'); require('@formatjs/intl-datetimeformat/polyfill-force'); - require('@formatjs/intl-datetimeformat/add-golden-tz'); + require('@formatjs/intl-datetimeformat/add-all-tz'); require('@formatjs/intl-listformat/polyfill-force'); } diff --git a/package-lock.json b/package-lock.json index 5727f1b6762..3121e745c50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -158,6 +158,7 @@ "nock": "13.5.4", "patch-package": "8.0.0", "react-devtools-core": "5.3.1", + "timezones.json": "1.7.1", "tough-cookie": "4.1.4", "ts-jest": "29.2.4", "typescript": "5.5.4", @@ -26667,6 +26668,12 @@ "xtend": "~4.0.1" } }, + "node_modules/timezones.json": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/timezones.json/-/timezones.json-1.7.1.tgz", + "integrity": "sha512-4dB58ulcrRWfiGufzlofLG45RIoalCTZiFUc7tnj0g8za0CpNTyIOVlspg1JD7OFyDeW5up3ntlkukizwB0IJA==", + "dev": true + }, "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", diff --git a/package.json b/package.json index fb9cb76bc08..440aedcd205 100644 --- a/package.json +++ b/package.json @@ -159,6 +159,7 @@ "nock": "13.5.4", "patch-package": "8.0.0", "react-devtools-core": "5.3.1", + "timezones.json": "1.7.1", "tough-cookie": "4.1.4", "ts-jest": "29.2.4", "typescript": "5.5.4",