Skip to content

Commit 9f12521

Browse files
feat: Add format.dateTimeRange (#769 by @martinmunillas)
Contributes to #774 Add support for [Intl.DateTimeFormat.prototype.formatRange()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatRange) --------- Co-authored-by: Jan Amann <jan@amann.me>
1 parent a9d12d3 commit 9f12521

6 files changed

Lines changed: 169 additions & 22 deletions

File tree

docs/pages/docs/usage/dates-times.mdx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import PartnerContentLink from 'components/PartnerContentLink';
55

66
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.
77

8-
## Formatting dates and times
8+
## Formatting dates and times [#dates-times]
99

1010
You can format plain dates that are not part of a message with the `dateTime` function that is returned from the `useFormatter` hook:
1111

@@ -30,6 +30,12 @@ function Component() {
3030

3131
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).
3232

33+
If you have [global formats](/docs/usage/configuration#formats) configured, you can reference them by passing a name as the second argument:
34+
35+
```js
36+
format.dateTime(dateTime, 'short');
37+
```
38+
3339
<details>
3440
<summary>How can I parse dates or manipulate them?</summary>
3541

@@ -49,7 +55,7 @@ const twoDaysAgo = subDays(date, 2);
4955

5056
</details>
5157

52-
## Formatting relative time
58+
## Formatting relative times [#relative-times]
5359

5460
You can format plain dates that are not part of a message with the `relativeTime` function:
5561

@@ -124,6 +130,33 @@ function Component() {
124130
}
125131
```
126132

133+
## Formatting date and time ranges [#date-time-ranges]
134+
135+
You can format ranges of dates and times with the `dateTimeRange` function:
136+
137+
```js
138+
import {useFormatter} from 'next-intl';
139+
140+
function Component() {
141+
const format = useFormatter();
142+
const dateTimeA = new Date('2020-11-20T08:30:00.000Z');
143+
const dateTimeB = new Date('2021-01-24T08:30:00.000Z');
144+
145+
// Renders "Nov 20, 2020 – Jan 24, 2021"
146+
format.dateTimeRange(dateTimeA, dateTimeB, {
147+
year: 'numeric',
148+
month: 'short',
149+
day: 'numeric'
150+
});
151+
}
152+
```
153+
154+
If you have [global formats](/docs/usage/configuration#formats) configured, you can reference them by passing a name as the trailing argument:
155+
156+
```js
157+
format.dateTimeRange(dateTimeA, dateTimeB, 'short');
158+
```
159+
127160
## Dates and times within messages
128161

129162
Dates and times can be embedded within messages by using the ICU syntax.

docs/pages/docs/usage/numbers.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ function Component() {
2828

2929
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).
3030

31+
If you have [global formats](/docs/usage/configuration#formats) configured, you can reference them by passing a name as the second argument:
32+
33+
```js
34+
format.number(499.9, 'precise');
35+
```
36+
3137
## Numbers within messages
3238

3339
Numbers can be embedded within messages by using the ICU syntax.

packages/next-intl/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,11 @@
114114
"size-limit": [
115115
{
116116
"path": "dist/production/index.react-client.js",
117-
"limit": "12.99 KB"
117+
"limit": "13.055 KB"
118118
},
119119
{
120120
"path": "dist/production/index.react-server.js",
121-
"limit": "13.75 KB"
121+
"limit": "13.765 KB"
122122
},
123123
{
124124
"path": "dist/production/navigation.react-client.js",
@@ -134,7 +134,7 @@
134134
},
135135
{
136136
"path": "dist/production/server.react-server.js",
137-
"limit": "12.945 KB"
137+
"limit": "13.05 KB"
138138
},
139139
{
140140
"path": "dist/production/middleware.js",

packages/use-intl/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
"size-limit": [
9191
{
9292
"path": "dist/production/index.js",
93-
"limit": "12.5 kB"
93+
"limit": "12.565 kB"
9494
}
9595
]
9696
}

packages/use-intl/src/core/createFormatter.tsx

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,25 @@ export default function createFormatter({
7878
onError = defaultOnError,
7979
timeZone: globalTimeZone
8080
}: Props) {
81+
function applyTimeZone(options?: DateTimeFormatOptions) {
82+
if (!options?.timeZone) {
83+
if (globalTimeZone) {
84+
options = {...options, timeZone: globalTimeZone};
85+
} else {
86+
onError(
87+
new IntlError(
88+
IntlErrorCode.ENVIRONMENT_FALLBACK,
89+
process.env.NODE_ENV !== 'production'
90+
? `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`
91+
: undefined
92+
)
93+
);
94+
}
95+
}
96+
97+
return options;
98+
}
99+
81100
function resolveFormatOrOptions<Options>(
82101
typeFormats: Record<string, Options> | undefined,
83102
formatOrOptions?: string | Options
@@ -138,27 +157,33 @@ export default function createFormatter({
138157
formatOrOptions,
139158
formats?.dateTime,
140159
(options) => {
141-
if (!options?.timeZone) {
142-
if (globalTimeZone) {
143-
options = {...options, timeZone: globalTimeZone};
144-
} else {
145-
onError(
146-
new IntlError(
147-
IntlErrorCode.ENVIRONMENT_FALLBACK,
148-
process.env.NODE_ENV !== 'production'
149-
? `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`
150-
: undefined
151-
)
152-
);
153-
}
154-
}
155-
160+
options = applyTimeZone(options);
156161
return new Intl.DateTimeFormat(locale, options).format(value);
157162
},
158163
() => String(value)
159164
);
160165
}
161166

167+
function dateTimeRange(
168+
/** If a number is supplied, this is interpreted as a UTC timestamp. */
169+
start: Date | number,
170+
/** If a number is supplied, this is interpreted as a UTC timestamp. */
171+
end: Date | number,
172+
/** If a time zone is supplied, the values are converted to that time zone.
173+
* Otherwise the user time zone will be used. */
174+
formatOrOptions?: string | DateTimeFormatOptions
175+
) {
176+
return getFormattedValue(
177+
formatOrOptions,
178+
formats?.dateTime,
179+
(options) => {
180+
options = applyTimeZone(options);
181+
return new Intl.DateTimeFormat(locale, options).formatRange(start, end);
182+
},
183+
() => [dateTime(start), dateTime(end)].join(' – ')
184+
);
185+
}
186+
162187
function number(
163188
value: number | bigint,
164189
formatOrOptions?: string | NumberFormatOptions
@@ -288,5 +313,5 @@ export default function createFormatter({
288313
);
289314
}
290315

291-
return {dateTime, number, relativeTime, list};
316+
return {dateTime, number, relativeTime, list, dateTimeRange};
292317
}

packages/use-intl/test/core/createFormatter.test.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ describe('dateTime', () => {
1414
})
1515
).toBe('Nov 20, 2020');
1616
});
17+
18+
it('allows to override a time zone', () => {
19+
const formatter = createFormatter({
20+
locale: 'en',
21+
timeZone: 'Europe/Berlin'
22+
});
23+
expect(
24+
formatter.dateTime(parseISO('2020-11-20T10:36:01.516Z'), {
25+
timeStyle: 'medium',
26+
dateStyle: 'medium',
27+
timeZone: 'America/New_York'
28+
})
29+
).toBe('Nov 20, 2020, 5:36:01 AM');
30+
});
1731
});
1832

1933
describe('number', () => {
@@ -253,6 +267,75 @@ describe('relativeTime', () => {
253267
});
254268
});
255269

270+
describe('dateTimeRange', () => {
271+
it('formats a date range', () => {
272+
const formatter = createFormatter({
273+
locale: 'en',
274+
timeZone: 'Europe/Berlin'
275+
});
276+
expect(
277+
formatter.dateTimeRange(
278+
new Date(2007, 0, 10, 10, 0, 0),
279+
new Date(2008, 0, 10, 11, 0, 0),
280+
{
281+
weekday: 'long',
282+
year: 'numeric',
283+
month: 'long',
284+
day: 'numeric'
285+
}
286+
)
287+
).toBe('Wednesday, January 10, 2007 – Thursday, January 10, 2008');
288+
289+
expect(
290+
formatter.dateTimeRange(
291+
new Date(Date.UTC(1906, 0, 10, 10, 0, 0)), // Wed, 10 Jan 1906 10:00:00 GMT
292+
new Date(Date.UTC(1906, 0, 10, 11, 0, 0)), // Wed, 10 Jan 1906 11:00:00 GMT
293+
{
294+
year: '2-digit',
295+
month: 'numeric',
296+
day: 'numeric',
297+
hour: 'numeric',
298+
minute: 'numeric'
299+
}
300+
)
301+
)
302+
// 1 hour more given that the timezone is Europe/Berlin and the date is in UTC
303+
.toBe('1/10/06, 11:00 AM – 12:00 PM');
304+
});
305+
306+
it('returns a reasonable fallback if an invalid format is provided', () => {
307+
const formatter = createFormatter({
308+
locale: 'en',
309+
timeZone: 'Europe/Berlin'
310+
});
311+
expect(
312+
formatter.dateTimeRange(
313+
new Date(2007, 0, 10, 10, 0, 0),
314+
new Date(2008, 0, 10, 11, 0, 0),
315+
'unknown'
316+
)
317+
).toBe('1/10/2007 – 1/10/2008');
318+
});
319+
320+
it('allows to override the time zone', () => {
321+
const formatter = createFormatter({
322+
locale: 'en',
323+
timeZone: 'Europe/Berlin'
324+
});
325+
expect(
326+
formatter.dateTimeRange(
327+
new Date(2007, 0, 10, 10, 0, 0),
328+
new Date(2008, 0, 10, 11, 0, 0),
329+
{
330+
timeStyle: 'medium',
331+
dateStyle: 'medium',
332+
timeZone: 'America/New_York'
333+
}
334+
)
335+
).toBe('Jan 10, 2007, 4:00:00 AM – Jan 10, 2008, 5:00:00 AM');
336+
});
337+
});
338+
256339
describe('list', () => {
257340
it('formats a list', () => {
258341
const formatter = createFormatter({

0 commit comments

Comments
 (0)