diff --git a/src/app/legacy/containers/ArticleTimestamp/index.stories.jsx b/src/app/legacy/containers/ArticleTimestamp/index.stories.jsx
index 17568ac090f..9995cbbf4fb 100644
--- a/src/app/legacy/containers/ArticleTimestamp/index.stories.jsx
+++ b/src/app/legacy/containers/ArticleTimestamp/index.stories.jsx
@@ -1,4 +1,5 @@
import WithTimeMachine from '../../../../testHelpers/withTimeMachine';
+import { ServiceContextProvider } from '#app/contexts/ServiceContext';
import ArticleTimestamp from '.';
import { timestampGenerator } from './testHelpers';
@@ -93,3 +94,46 @@ export const H = () => (
);
H.storyName =
'lastPublished today and more than 10 hours ago and firstPublished before today';
+
+// Timezone and locale migration checks for Moment → Temporal
+const timezoneLocaleServices = [
+ { service: 'news', label: 'English (Europe/London)' },
+ { service: 'azeri', label: 'Azeri (Asia/Baku)' },
+ { service: 'persian', label: 'Persian (GMT, Jalali calendar)' },
+ { service: 'arabic', label: 'Arabic (GMT, RTL)' },
+ { service: 'portuguese', label: 'Portuguese (America/Sao_Paulo)' },
+ { service: 'nepali', label: 'Nepali (Asia/Kathmandu)' },
+];
+
+const dstBoundaryTimestamp = Date.UTC(2021, 2, 28, 1, 0, 0); // 28 March 2021 01:00 UTC (DST boundary)
+const fixedOlderTimestamp = Date.UTC(2021, 2, 27, 12, 0, 0); // 27 March 2021 12:00 UTC
+
+export const TimezoneAndLocaleChecks = () => (
+
+ {timezoneLocaleServices.map(({ service, label }) => (
+
+
+
+ ))}
+
+);
+TimezoneAndLocaleChecks.parameters = {
+ chromatic: {
+ disable: false,
+ },
+};
+TimezoneAndLocaleChecks.tags = ['!dev'];
diff --git a/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/README.md b/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/README.md
index 5b5b23d8e8c..81f21d60b07 100644
--- a/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/README.md
+++ b/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/README.md
@@ -12,7 +12,7 @@ A couple of scenarios that we expect these tests would fail and need updating wo
- Change in the timeformat logic [here](../timeFormats)
-- Changes in DST times across different timezones.
+- Changes in Daylight Saving Time (DST) times across different timezones.
- ... please add more if you find any
diff --git a/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/testUtils.js b/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/testUtils.js
index 8d774147929..769ca1f96da 100644
--- a/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/testUtils.js
+++ b/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/testUtils.js
@@ -51,3 +51,23 @@ export const format = ({ datetimeLocale, timezone, fixture, altCalendar }) => {
return formatted;
};
+
+// allows precise checks for edge cases without fixture data
+export const formatTimestamp = ({
+ datetimeLocale,
+ timezone,
+ time,
+ altCalendar,
+ dateTimeFormat = formatDateAndTime,
+}) => {
+ const dateWithTimezone = moment.tz(time, timezone).locale(datetimeLocale);
+ const formatted = dateWithTimezone.format(dateTimeFormat(datetimeLocale));
+
+ if (altCalendar) {
+ const altCalendarFormatted = altCalendar.formatDate(dateWithTimezone);
+
+ return `${altCalendarFormatted} - ${formatted}`;
+ }
+
+ return formatted;
+};
diff --git a/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/timeFormat.test.js b/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/timeFormat.test.js
index 8d2d8e33d48..ae40af92ca1 100644
--- a/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/timeFormat.test.js
+++ b/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/timeFormat.test.js
@@ -1,6 +1,6 @@
import services from '#utilities/serviceConfigs';
import expectedFormats from './expectedFormats.json';
-import { format, timestampsFixtures } from './testUtils';
+import { format, formatTimestamp, timestampsFixtures } from './testUtils';
describe('Timestamp Formats', () => {
Object.keys(services).forEach(service => {
@@ -22,4 +22,81 @@ describe('Timestamp Formats', () => {
});
});
});
+
+ describe('Timestamp Edge Cases', () => {
+ it('should preserve timezone output around DST start for Europe/London', () => {
+ const beforeDSTStart = Date.UTC(2021, 2, 28, 0, 30, 0); // 00:30 GMT
+ const afterDSTStart = Date.UTC(2021, 2, 28, 1, 30, 0); // 02:30 BST
+
+ expect(
+ formatTimestamp({
+ datetimeLocale: services.news.default.datetimeLocale,
+ timezone: services.news.default.timezone,
+ time: beforeDSTStart,
+ }),
+ ).toEqual('28 March 2021, 00:30 GMT');
+
+ expect(
+ formatTimestamp({
+ datetimeLocale: services.news.default.datetimeLocale,
+ timezone: services.news.default.timezone,
+ time: afterDSTStart,
+ }),
+ ).toEqual('28 March 2021, 02:30 BST');
+ });
+
+ it('should preserve timezone output around DST end for Europe/London', () => {
+ const beforeDSTEnd = Date.UTC(2021, 9, 31, 0, 30, 0); // 01:30 BST
+ const afterDSTEnd = Date.UTC(2021, 9, 31, 1, 30, 0); // 01:30 GMT
+
+ expect(
+ formatTimestamp({
+ datetimeLocale: services.news.default.datetimeLocale,
+ timezone: services.news.default.timezone,
+ time: beforeDSTEnd,
+ }),
+ ).toEqual('31 October 2021, 01:30 BST');
+
+ expect(
+ formatTimestamp({
+ datetimeLocale: services.news.default.datetimeLocale,
+ timezone: services.news.default.timezone,
+ time: afterDSTEnd,
+ }),
+ ).toEqual('31 October 2021, 01:30 GMT');
+ });
+
+ it('should preserve half-hour offset output for Asia/Kabul', () => {
+ const utcTimestamp = Date.UTC(2021, 5, 15, 12, 0, 0); // 15 June 2021 12:00 UTC
+
+ expect(
+ formatTimestamp({
+ datetimeLocale: services.dari.default.datetimeLocale,
+ timezone: services.dari.default.timezone,
+ time: utcTimestamp,
+ }),
+ ).toEqual('۱۵ جون ۲۰۲۱ ۱۶:۳۰');
+ });
+
+ it('should preserve timezone output around DST start for Europe/Bucharest', () => {
+ const beforeDSTStart = Date.UTC(2021, 2, 28, 0, 30, 0); // 02:30 EET
+ const afterDSTStart = Date.UTC(2021, 2, 28, 1, 30, 0); // 04:30 EEST
+
+ expect(
+ formatTimestamp({
+ datetimeLocale: services.romania.default.datetimeLocale,
+ timezone: services.romania.default.timezone,
+ time: beforeDSTStart,
+ }),
+ ).toEqual('28 martie 2021, 02:30 EET');
+
+ expect(
+ formatTimestamp({
+ datetimeLocale: services.romania.default.datetimeLocale,
+ timezone: services.romania.default.timezone,
+ time: afterDSTStart,
+ }),
+ ).toEqual('28 martie 2021, 04:30 EEST');
+ });
+ });
});
diff --git a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js
index b08ed082a76..92d84eda326 100644
--- a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js
+++ b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js
@@ -132,6 +132,143 @@ describe('Timestamp utility functions', () => {
});
expect(result).toBeUndefined();
});
+
+ it('should correctly format timezone labels around DST start in Europe/London', () => {
+ const beforeDSTStart = Date.UTC(2021, 2, 28, 0, 30, 0); // 00:30 GMT
+ const afterDSTStart = Date.UTC(2021, 2, 28, 1, 30, 0); // 02:30 BST
+
+ expect(
+ formatUnixTimestamp({
+ timestamp: beforeDSTStart,
+ format: 'D MMMM YYYY, HH:mm z',
+ timezone,
+ locale,
+ }),
+ ).toEqual('28 March 2021, 00:30 GMT');
+
+ expect(
+ formatUnixTimestamp({
+ timestamp: afterDSTStart,
+ format: 'D MMMM YYYY, HH:mm z',
+ timezone,
+ locale,
+ }),
+ ).toEqual('28 March 2021, 02:30 BST');
+ });
+
+ it('should correctly format timezone labels around DST end in Europe/London', () => {
+ const beforeDSTEnd = Date.UTC(2021, 9, 31, 0, 30, 0); // 01:30 BST
+ const afterDSTEnd = Date.UTC(2021, 9, 31, 1, 30, 0); // 01:30 GMT
+
+ expect(
+ formatUnixTimestamp({
+ timestamp: beforeDSTEnd,
+ format: 'D MMMM YYYY, HH:mm z',
+ timezone,
+ locale,
+ }),
+ ).toEqual('31 October 2021, 01:30 BST');
+
+ expect(
+ formatUnixTimestamp({
+ timestamp: afterDSTEnd,
+ format: 'D MMMM YYYY, HH:mm z',
+ timezone,
+ locale,
+ }),
+ ).toEqual('31 October 2021, 01:30 GMT');
+ });
+
+ it('should apply a fixed UTC+1 offset for Africa/Lagos (no DST)', () => {
+ const utcTimestamp = Date.UTC(2021, 5, 15, 12, 0, 0); // 15 June 2021 12:00 UTC
+
+ expect(
+ formatUnixTimestamp({
+ timestamp: utcTimestamp,
+ format: 'D MMMM YYYY, HH:mm z',
+ timezone: 'Africa/Lagos',
+ locale,
+ }),
+ ).toEqual('15 June 2021, 13:00 WAT');
+ });
+
+ it('should apply a +05:45 offset for Asia/Kathmandu (no DST, non-whole-hour offset)', () => {
+ const utcTimestamp = Date.UTC(2021, 5, 15, 12, 0, 0); // 15 June 2021 12:00 UTC
+
+ expect(
+ formatUnixTimestamp({
+ timestamp: utcTimestamp,
+ format: 'D MMMM YYYY, HH:mm z',
+ timezone: 'Asia/Kathmandu',
+ locale,
+ }),
+ ).toEqual('15 June 2021, 17:45 +0545');
+ });
+
+ it('should apply a negative UTC-3 offset for America/Sao_Paulo', () => {
+ const utcTimestamp = Date.UTC(2021, 5, 15, 12, 0, 0); // 15 June 2021 12:00 UTC
+
+ expect(
+ formatUnixTimestamp({
+ timestamp: utcTimestamp,
+ format: 'D MMMM YYYY, HH:mm z',
+ timezone: 'America/Sao_Paulo',
+ locale,
+ }),
+ ).toEqual('15 June 2021, 09:00 -03');
+ });
+
+ it('should format correctly for GMT (non-region-style IANA identifier)', () => {
+ const utcTimestamp = Date.UTC(2021, 5, 15, 12, 0, 0); // 15 June 2021 12:00 UTC
+
+ expect(
+ formatUnixTimestamp({
+ timestamp: utcTimestamp,
+ format: 'D MMMM YYYY, HH:mm z',
+ timezone: 'GMT',
+ locale,
+ }),
+ ).toEqual('15 June 2021, 12:00 GMT');
+ });
+
+ it('should translate month names for a non-Latin locale', () => {
+ expect(
+ formatUnixTimestamp({
+ timestamp,
+ format: 'D MMMM YYYY',
+ timezone: 'GMT',
+ locale: 'ar',
+ }),
+ ).toEqual('١٩ أكتوبر ٢٠١٨');
+ });
+
+ it('should apply locale-sensitive format tokens (LL, LT) for a non-Latin locale', () => {
+ expect(
+ formatUnixTimestamp({
+ timestamp,
+ format: null,
+ timezone: 'GMT',
+ locale: 'ar',
+ }),
+ ).toEqual('١٩ أكتوبر ٢٠١٨، ١٧:١٠ GMT');
+ });
+
+ it('should return relative timestamp in the provided locale', () => {
+ const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(1704110400000); // 1 January 2024 12:00:00 UTC
+ try {
+ const nineHoursAgo = timestampGenerator({ hours: 9 });
+ const output = formatUnixTimestamp({
+ timestamp: nineHoursAgo,
+ format: 'D MMMM YYYY',
+ timezone: 'GMT',
+ locale: 'ar',
+ isRelative: true,
+ });
+ expect(output).toEqual('منذ ٩ ساعات');
+ } finally {
+ nowSpy.mockRestore();
+ }
+ });
});
});
diff --git a/src/app/lib/config/services/azeri.ts b/src/app/lib/config/services/azeri.ts
index 72d91879179..0a2790f51dd 100644
--- a/src/app/lib/config/services/azeri.ts
+++ b/src/app/lib/config/services/azeri.ts
@@ -464,7 +464,7 @@ export const service: DefaultServiceConfig = {
],
copyrightText: 'BBC. BBC kənar saytların məzmununa məsul deyil.',
},
- timezone: 'Asia/baku',
+ timezone: 'Asia/Baku',
navigation: [
{
title: 'Xəbərlər',