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({