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",