Skip to content

Commit

Permalink
feat: Add format.dateTimeRange (#769 by @martinmunillas)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
martinmunillas and amannn committed Feb 19, 2024
1 parent a9d12d3 commit 9f12521
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 22 deletions.
37 changes: 35 additions & 2 deletions docs/pages/docs/usage/dates-times.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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');
```

<details>
<summary>How can I parse dates or manipulate them?</summary>

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

</details>

## Formatting relative time
## Formatting relative times [#relative-times]

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

Expand Down Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions docs/pages/docs/usage/numbers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions packages/next-intl/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -134,7 +134,7 @@
},
{
"path": "dist/production/server.react-server.js",
"limit": "12.945 KB"
"limit": "13.05 KB"
},
{
"path": "dist/production/middleware.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/use-intl/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"size-limit": [
{
"path": "dist/production/index.js",
"limit": "12.5 kB"
"limit": "12.565 kB"
}
]
}
57 changes: 41 additions & 16 deletions packages/use-intl/src/core/createFormatter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Options>(
typeFormats: Record<string, Options> | undefined,
formatOrOptions?: string | Options
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -288,5 +313,5 @@ export default function createFormatter({
);
}

return {dateTime, number, relativeTime, list};
return {dateTime, number, relativeTime, list, dateTimeRange};
}
83 changes: 83 additions & 0 deletions packages/use-intl/test/core/createFormatter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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({
Expand Down

0 comments on commit 9f12521

Please sign in to comment.