diff --git a/docs/pages/docs/usage/dates-times.mdx b/docs/pages/docs/usage/dates-times.mdx index 52832724f..b752e2f41 100644 --- a/docs/pages/docs/usage/dates-times.mdx +++ b/docs/pages/docs/usage/dates-times.mdx @@ -5,7 +5,7 @@ import PartnerContentLink from 'components/PartnerContentLink'; The formatting of dates and times varies greatly between locales (e.g. "Apr 24, 2023" in `en-US` vs. "24 квіт. 2023 р." in `uk-UA`). By using the formatting capabilities of `next-intl`, you can handle i18n differences in your Next.js app automatically. -## Formatting dates and times +## Formatting dates and times [#dates-times] You can format plain dates that are not part of a message with the `dateTime` function that is returned from the `useFormatter` hook: @@ -30,6 +30,12 @@ function Component() { See [the MDN docs about `DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat#Using_options) to learn more about the options that you can provide to the `dateTime` function or [try the interactive explorer for `Intl.DateTimeFormat`](https://www.intl-explorer.com/DateTimeFormat). +If you have [global formats](/docs/usage/configuration#formats) configured, you can reference them by passing a name as the second argument: + +```js +format.dateTime(dateTime, 'short'); +``` +
How can I parse dates or manipulate them? @@ -49,7 +55,7 @@ const twoDaysAgo = subDays(date, 2);
-## Formatting relative time +## Formatting relative times [#relative-times] You can format plain dates that are not part of a message with the `relativeTime` function: @@ -124,6 +130,33 @@ function Component() { } ``` +## Formatting date and time ranges [#date-time-ranges] + +You can format ranges of dates and times with the `dateTimeRange` function: + +```js +import {useFormatter} from 'next-intl'; + +function Component() { + const format = useFormatter(); + const dateTimeA = new Date('2020-11-20T08:30:00.000Z'); + const dateTimeB = new Date('2021-01-24T08:30:00.000Z'); + + // Renders "Nov 20, 2020 – Jan 24, 2021" + format.dateTimeRange(dateTimeA, dateTimeB, { + year: 'numeric', + month: 'short', + day: 'numeric' + }); +} +``` + +If you have [global formats](/docs/usage/configuration#formats) configured, you can reference them by passing a name as the trailing argument: + +```js +format.dateTimeRange(dateTimeA, dateTimeB, 'short'); +``` + ## Dates and times within messages Dates and times can be embedded within messages by using the ICU syntax. diff --git a/docs/pages/docs/usage/numbers.mdx b/docs/pages/docs/usage/numbers.mdx index a4561713e..b2be19307 100644 --- a/docs/pages/docs/usage/numbers.mdx +++ b/docs/pages/docs/usage/numbers.mdx @@ -28,6 +28,12 @@ function Component() { See [the MDN docs about `NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat#Using_options) to learn more about the options you can pass to the `number` function or [try the interactive explorer for `Intl.NumberFormat`](https://www.intl-explorer.com/NumberFormat). +If you have [global formats](/docs/usage/configuration#formats) configured, you can reference them by passing a name as the second argument: + +```js +format.number(499.9, 'precise'); +``` + ## Numbers within messages Numbers can be embedded within messages by using the ICU syntax. diff --git a/packages/next-intl/package.json b/packages/next-intl/package.json index 10e54b7ff..14ce10b15 100644 --- a/packages/next-intl/package.json +++ b/packages/next-intl/package.json @@ -114,11 +114,11 @@ "size-limit": [ { "path": "dist/production/index.react-client.js", - "limit": "12.99 KB" + "limit": "13.055 KB" }, { "path": "dist/production/index.react-server.js", - "limit": "13.75 KB" + "limit": "13.765 KB" }, { "path": "dist/production/navigation.react-client.js", @@ -134,7 +134,7 @@ }, { "path": "dist/production/server.react-server.js", - "limit": "12.945 KB" + "limit": "13.05 KB" }, { "path": "dist/production/middleware.js", diff --git a/packages/use-intl/package.json b/packages/use-intl/package.json index b070f4667..f8d7845a5 100644 --- a/packages/use-intl/package.json +++ b/packages/use-intl/package.json @@ -90,7 +90,7 @@ "size-limit": [ { "path": "dist/production/index.js", - "limit": "12.5 kB" + "limit": "12.565 kB" } ] } diff --git a/packages/use-intl/src/core/createFormatter.tsx b/packages/use-intl/src/core/createFormatter.tsx index 45105bca0..10452d58b 100644 --- a/packages/use-intl/src/core/createFormatter.tsx +++ b/packages/use-intl/src/core/createFormatter.tsx @@ -78,6 +78,25 @@ export default function createFormatter({ onError = defaultOnError, timeZone: globalTimeZone }: Props) { + function applyTimeZone(options?: DateTimeFormatOptions) { + if (!options?.timeZone) { + if (globalTimeZone) { + options = {...options, timeZone: globalTimeZone}; + } else { + onError( + new IntlError( + IntlErrorCode.ENVIRONMENT_FALLBACK, + process.env.NODE_ENV !== 'production' + ? `The \`timeZone\` parameter wasn't provided and there is no global default configured. Consider adding a global default to avoid markup mismatches caused by environment differences. Learn more: https://next-intl-docs.vercel.app/docs/configuration#time-zone` + : undefined + ) + ); + } + } + + return options; + } + function resolveFormatOrOptions( typeFormats: Record | undefined, formatOrOptions?: string | Options @@ -138,27 +157,33 @@ export default function createFormatter({ formatOrOptions, formats?.dateTime, (options) => { - if (!options?.timeZone) { - if (globalTimeZone) { - options = {...options, timeZone: globalTimeZone}; - } else { - onError( - new IntlError( - IntlErrorCode.ENVIRONMENT_FALLBACK, - process.env.NODE_ENV !== 'production' - ? `The \`timeZone\` parameter wasn't provided and there is no global default configured. Consider adding a global default to avoid markup mismatches caused by environment differences. Learn more: https://next-intl-docs.vercel.app/docs/configuration#time-zone` - : undefined - ) - ); - } - } - + options = applyTimeZone(options); return new Intl.DateTimeFormat(locale, options).format(value); }, () => String(value) ); } + function dateTimeRange( + /** If a number is supplied, this is interpreted as a UTC timestamp. */ + start: Date | number, + /** If a number is supplied, this is interpreted as a UTC timestamp. */ + end: Date | number, + /** If a time zone is supplied, the values are converted to that time zone. + * Otherwise the user time zone will be used. */ + formatOrOptions?: string | DateTimeFormatOptions + ) { + return getFormattedValue( + formatOrOptions, + formats?.dateTime, + (options) => { + options = applyTimeZone(options); + return new Intl.DateTimeFormat(locale, options).formatRange(start, end); + }, + () => [dateTime(start), dateTime(end)].join(' – ') + ); + } + function number( value: number | bigint, formatOrOptions?: string | NumberFormatOptions @@ -288,5 +313,5 @@ export default function createFormatter({ ); } - return {dateTime, number, relativeTime, list}; + return {dateTime, number, relativeTime, list, dateTimeRange}; } diff --git a/packages/use-intl/test/core/createFormatter.test.tsx b/packages/use-intl/test/core/createFormatter.test.tsx index f72f8f1ca..52d9e16c4 100644 --- a/packages/use-intl/test/core/createFormatter.test.tsx +++ b/packages/use-intl/test/core/createFormatter.test.tsx @@ -14,6 +14,20 @@ describe('dateTime', () => { }) ).toBe('Nov 20, 2020'); }); + + it('allows to override a time zone', () => { + const formatter = createFormatter({ + locale: 'en', + timeZone: 'Europe/Berlin' + }); + expect( + formatter.dateTime(parseISO('2020-11-20T10:36:01.516Z'), { + timeStyle: 'medium', + dateStyle: 'medium', + timeZone: 'America/New_York' + }) + ).toBe('Nov 20, 2020, 5:36:01 AM'); + }); }); describe('number', () => { @@ -253,6 +267,75 @@ describe('relativeTime', () => { }); }); +describe('dateTimeRange', () => { + it('formats a date range', () => { + const formatter = createFormatter({ + locale: 'en', + timeZone: 'Europe/Berlin' + }); + expect( + formatter.dateTimeRange( + new Date(2007, 0, 10, 10, 0, 0), + new Date(2008, 0, 10, 11, 0, 0), + { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + } + ) + ).toBe('Wednesday, January 10, 2007 – Thursday, January 10, 2008'); + + expect( + formatter.dateTimeRange( + new Date(Date.UTC(1906, 0, 10, 10, 0, 0)), // Wed, 10 Jan 1906 10:00:00 GMT + new Date(Date.UTC(1906, 0, 10, 11, 0, 0)), // Wed, 10 Jan 1906 11:00:00 GMT + { + year: '2-digit', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric' + } + ) + ) + // 1 hour more given that the timezone is Europe/Berlin and the date is in UTC + .toBe('1/10/06, 11:00 AM – 12:00 PM'); + }); + + it('returns a reasonable fallback if an invalid format is provided', () => { + const formatter = createFormatter({ + locale: 'en', + timeZone: 'Europe/Berlin' + }); + expect( + formatter.dateTimeRange( + new Date(2007, 0, 10, 10, 0, 0), + new Date(2008, 0, 10, 11, 0, 0), + 'unknown' + ) + ).toBe('1/10/2007 – 1/10/2008'); + }); + + it('allows to override the time zone', () => { + const formatter = createFormatter({ + locale: 'en', + timeZone: 'Europe/Berlin' + }); + expect( + formatter.dateTimeRange( + new Date(2007, 0, 10, 10, 0, 0), + new Date(2008, 0, 10, 11, 0, 0), + { + timeStyle: 'medium', + dateStyle: 'medium', + timeZone: 'America/New_York' + } + ) + ).toBe('Jan 10, 2007, 4:00:00 AM – Jan 10, 2008, 5:00:00 AM'); + }); +}); + describe('list', () => { it('formats a list', () => { const formatter = createFormatter({