Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add format.dateTimeRange #769

Merged
merged 6 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
martinmunillas marked this conversation as resolved.
Show resolved Hide resolved
/** 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');
martinmunillas marked this conversation as resolved.
Show resolved Hide resolved

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
Loading