From 4546749b98f96fb975d416fac8ae13971e4e5561 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 5 Jun 2026 15:29:17 +0100 Subject: [PATCH 1/3] WS-NA-temporal-migrate: Add dep to simorgh package.json. Adds internal adapter --- package.json | 1 + .../src/utilities/index.js | 45 +++++++++++++++---- yarn.lock | 1 + 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index aa251f7ce3c..5014052fb3c 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "react-dom": "19.2.6", "react-helmet": "6.1.0", "react-lazyload": "3.2.1", + "temporal-polyfill": "0.3.0", "undici": "8.3.0", "uuid": "13.0.1", "winston": "patch:winston@3.8.2#./patches/winston-file-descriptor.patch" diff --git a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.js b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.js index a0120c705a2..95c2cc49b5f 100644 --- a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.js +++ b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.js @@ -1,5 +1,28 @@ +import 'temporal-polyfill/global'; import moment from 'moment-timezone'; +const createDateAdapter = () => ({ + // Temporary seam for gradual Temporal adoption in follow-up PRs. + createLocalisedMoment: ({ locale, timestamp }) => + moment(timestamp).locale(locale), + createMomentInTimezone: ({ locale, timestamp, timezone }) => + moment(timestamp).locale(locale).tz(timezone), + formatDuration: ({ duration, format, locale }) => { + const defaultDurationFormat = duration?.includes('H') ? 'h:mm:ss' : 'mm:ss'; + const durationInMilliseconds = moment.duration(duration).asMilliseconds(); + + return moment + .utc(durationInMilliseconds) + .locale(locale) + .format(format || defaultDurationFormat); + }, + // Exposed to make Temporal available at runtime without changing behavior yet. + toTemporalInstant: timestamp => + globalThis.Temporal?.Instant?.fromEpochMilliseconds(timestamp), +}); + +const dateAdapter = createDateAdapter(); + // Note that this next section is globally configuring moment. // It is not possible to configure these on specific moment instances. // The current requirements for rounding & thresholding are the same universally @@ -20,12 +43,11 @@ moment.relativeTimeThreshold('d', 30); moment.relativeTimeThreshold('M', 12); export const formatDuration = ({ duration, format, locale = 'en-gb' }) => { - const defaultDurationFormat = duration?.includes('H') ? 'h:mm:ss' : 'mm:ss'; - const durationInMilliseconds = moment.duration(duration).asMilliseconds(); - return moment - .utc(durationInMilliseconds) - .locale(locale) - .format(format || defaultDurationFormat); + return dateAdapter.formatDuration({ + duration, + format, + locale, + }); }; // if the date is invalid return false - https://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript#answer-1353711 @@ -39,7 +61,10 @@ export const isValidDateTime = dateTime => { // when using the following 2 functions, we recommend using webpack configuration to only load in the relevant timezone, rather than all of moment-timezone export const localisedMoment = ({ locale, timestamp }) => { - return moment(timestamp).locale(locale); + return dateAdapter.createLocalisedMoment({ + locale, + timestamp, + }); }; export const formatUnixTimestamp = ({ @@ -51,7 +76,11 @@ export const formatUnixTimestamp = ({ }) => { if (!timestamp) return undefined; - const momentObj = moment(timestamp).locale(locale).tz(timezone); + const momentObj = dateAdapter.createMomentInTimezone({ + locale, + timestamp, + timezone, + }); if (isRelative) { return momentObj.fromNow(); diff --git a/yarn.lock b/yarn.lock index 39610cba7c4..cc5bf54b616 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15866,6 +15866,7 @@ __metadata: retry: "npm:0.13.1" storybook: "npm:10.3.6" supertest: "npm:7.2.2" + temporal-polyfill: "npm:0.3.0" timemachine: "npm:0.3.2" ts-jest: "npm:29.4.9" typescript: "npm:5.9.3" From ca48859a1bbe4b630f68598aef7c301a87fa88ae Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 17 Jul 2026 11:39:55 +0100 Subject: [PATCH 2/3] WS-NA-Temporal-PR2: Lint --- .eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.js b/.eslintrc.js index bce2049712f..28f206631bf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -38,6 +38,7 @@ module.exports = { 'no-only-tests', ], globals: { + globalThis: false, cy: false, Cypress: false, expect: false, From 9830a64438e2b4d7e4b9e3ceb2c4857e88470b87 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 17 Jul 2026 15:23:41 +0100 Subject: [PATCH 3/3] WS-NA-temporal: Converts to TS --- .../src/utilities/{index.js => index.ts} | 79 ++++++++++++++----- 1 file changed, 60 insertions(+), 19 deletions(-) rename src/app/legacy/psammead/psammead-timestamp-container/src/utilities/{index.js => index.ts} (61%) diff --git a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.js b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.ts similarity index 61% rename from src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.js rename to src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.ts index 95c2cc49b5f..d565aab101b 100644 --- a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.js +++ b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.ts @@ -1,23 +1,46 @@ import 'temporal-polyfill/global'; import moment from 'moment-timezone'; +type Locale = string; +type ISODuration = string; + const createDateAdapter = () => ({ // Temporary seam for gradual Temporal adoption in follow-up PRs. - createLocalisedMoment: ({ locale, timestamp }) => - moment(timestamp).locale(locale), - createMomentInTimezone: ({ locale, timestamp, timezone }) => - moment(timestamp).locale(locale).tz(timezone), - formatDuration: ({ duration, format, locale }) => { + createLocalisedMoment: ({ + locale, + timestamp, + }: { + locale: Locale; + timestamp: number; + }): moment.Moment => moment(timestamp).locale(locale), + createMomentInTimezone: ({ + locale, + timestamp, + timezone, + }: { + locale: Locale; + timestamp: number; + timezone: string; + }): moment.Moment => moment(timestamp).locale(locale).tz(timezone), + formatDuration: ({ + duration, + format, + locale, + }: { + duration: ISODuration; + format?: string; + locale?: Locale; + }): string => { const defaultDurationFormat = duration?.includes('H') ? 'h:mm:ss' : 'mm:ss'; const durationInMilliseconds = moment.duration(duration).asMilliseconds(); return moment .utc(durationInMilliseconds) - .locale(locale) + .locale(locale ?? '') .format(format || defaultDurationFormat); }, // Exposed to make Temporal available at runtime without changing behavior yet. - toTemporalInstant: timestamp => + toTemporalInstant: (timestamp: number): Temporal.Instant | undefined => globalThis.Temporal?.Instant?.fromEpochMilliseconds(timestamp), }); @@ -42,30 +65,42 @@ moment.relativeTimeThreshold('h', 24); moment.relativeTimeThreshold('d', 30); moment.relativeTimeThreshold('M', 12); -export const formatDuration = ({ duration, format, locale = 'en-gb' }) => { - return dateAdapter.formatDuration({ +export const formatDuration = ({ + duration, + format, + locale = 'en-gb', +}: { + duration: ISODuration; + format?: string; + locale?: Locale; +}): string => + dateAdapter.formatDuration({ duration, format, locale, }); -}; // if the date is invalid return false - https://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript#answer-1353711 -export const isValidDateTime = dateTime => { +export const isValidDateTime = (dateTime: unknown): boolean => { // eslint-disable-next-line no-restricted-globals - if (isNaN(dateTime) || dateTime === null) { + if (isNaN(dateTime as number) || dateTime === null) { return false; } - return !isNaN(new Date(dateTime)); // eslint-disable-line no-restricted-globals + return !isNaN(new Date(dateTime as number).getTime()); // eslint-disable-line no-restricted-globals }; // when using the following 2 functions, we recommend using webpack configuration to only load in the relevant timezone, rather than all of moment-timezone -export const localisedMoment = ({ locale, timestamp }) => { - return dateAdapter.createLocalisedMoment({ +export const localisedMoment = ({ + locale, + timestamp, +}: { + locale: Locale; + timestamp: number; +}): moment.Moment => + dateAdapter.createLocalisedMoment({ locale, timestamp, }); -}; export const formatUnixTimestamp = ({ format, @@ -73,13 +108,19 @@ export const formatUnixTimestamp = ({ locale, timestamp, timezone, -}) => { +}: { + format?: string | null; + isRelative?: boolean; + locale?: Locale; + timestamp?: number; + timezone?: string; +}): string | undefined => { if (!timestamp) return undefined; const momentObj = dateAdapter.createMomentInTimezone({ - locale, + locale: locale ?? '', timestamp, - timezone, + timezone: timezone ?? '', }); if (isRelative) {