Skip to content

Commit

Permalink
feat: Allow to override options of global formats (#1693)
Browse files Browse the repository at this point in the history
**Example:**

```tsx
formatter.dateTime(date, 'short', {
  timeZone: 'America/New_York'
})
```
  • Loading branch information
amannn authored Feb 3, 2025
1 parent 40d535a commit 77949ef
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 21 deletions.
10 changes: 9 additions & 1 deletion docs/src/pages/docs/usage/dates-times.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ See [the MDN docs about `DateTimeFormat`](https://developer.mozilla.org/en-US/do
If you have [global formats](/docs/usage/configuration#formats) configured, you can reference them by passing a name as the second argument:

```js
// Use a global format
format.dateTime(dateTime, 'short');

// Optionally override some options
format.dateTime(dateTime, 'short', {year: 'numeric'});
```

<Details id="parsing-manipulation">
Expand Down Expand Up @@ -204,10 +208,14 @@ function Component() {
}
```

If you have [global formats](/docs/usage/configuration#formats) configured, you can reference them by passing a name as the trailing argument:
If you have [global formats](/docs/usage/configuration#formats) configured, you can reference them by passing a name as the third argument:

```js
// Use a global format
format.dateTimeRange(dateTimeA, dateTimeB, 'short');

// Optionally override some options
format.dateTimeRange(dateTimeA, dateTimeB, 'short', {year: 'numeric'});
```

## Dates and times within messages
Expand Down
4 changes: 4 additions & 0 deletions docs/src/pages/docs/usage/numbers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ See [the MDN docs about `NumberFormat`](https://developer.mozilla.org/en-US/docs
If you have [global formats](/docs/usage/configuration#formats) configured, you can reference them by passing a name as the second argument:

```js
// Use a global format
format.number(499.9, 'precise');

// Optionally override some options
format.number(499.9, 'price', {currency: 'USD'});
```

## Numbers within messages
Expand Down
2 changes: 1 addition & 1 deletion packages/next-intl/.size-limit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const config: SizeLimitConfig = [
{
name: "import * from 'next-intl' (react-client)",
path: 'dist/esm/production/index.react-client.js',
limit: '13.065 KB'
limit: '13.125 KB'
},
{
name: "import {NextIntlClientProvider} from 'next-intl' (react-client)",
Expand Down
4 changes: 2 additions & 2 deletions packages/use-intl/.size-limit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ const config: SizeLimitConfig = [
name: "import * from 'use-intl' (production)",
import: '*',
path: 'dist/esm/production/index.js',
limit: '12.955 kB'
limit: '13.015 kB'
},
{
name: "import {IntlProvider, useLocale, useNow, useTimeZone, useMessages, useFormatter} from 'use-intl' (production)",
path: 'dist/esm/production/index.js',
import:
'{IntlProvider, useLocale, useNow, useTimeZone, useMessages, useFormatter}',
limit: '1.995 kB'
limit: '2.005 kB'
}
];

Expand Down
82 changes: 82 additions & 0 deletions packages/use-intl/src/core/createFormatter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,26 @@ describe('dateTime', () => {
})
).toBe('Nov 20, 2020, 5:36:01 AM');
});

it('can combine a global format with an override', () => {
const formatter = createFormatter({
locale: 'en',
timeZone: 'Europe/Berlin',
formats: {
dateTime: {
short: {
dateStyle: 'short',
timeStyle: 'short'
}
}
}
});
expect(
formatter.dateTime(parseISO('2020-11-20T10:36:01.516Z'), 'short', {
timeZone: 'America/New_York'
})
).toBe('11/20/20, 5:36 AM');
});
});

describe('number', () => {
Expand Down Expand Up @@ -71,6 +91,25 @@ describe('number', () => {
})
).toBe('$123,456,789,123,456,789.00');
});

it('can combine a global format with an override', () => {
const formatter = createFormatter({
locale: 'en',
timeZone: 'Europe/Berlin',
formats: {
number: {
price: {
style: 'currency',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}
}
}
});
expect(formatter.number(123456.789, 'price', {currency: 'EUR'})).toBe(
'€123,456.79'
);
});
});

describe('relativeTime', () => {
Expand Down Expand Up @@ -349,6 +388,31 @@ describe('dateTimeRange', () => {
)
).toBe('Jan 10, 2007, 4:00:00 AM – Jan 10, 2008, 5:00:00 AM');
});

it('can combine a global format with an override', () => {
const formatter = createFormatter({
locale: 'en',
timeZone: 'Europe/Berlin',
formats: {
dateTime: {
short: {
dateStyle: 'short',
timeStyle: 'short'
}
}
}
});
expect(
formatter.dateTimeRange(
new Date(2007, 0, 10, 10, 0, 0),
new Date(2008, 0, 10, 11, 0, 0),
'short',
{
timeZone: 'America/New_York'
}
)
).toBe('1/10/07, 4:00 AM – 1/10/08, 5:00 AM');
});
});

describe('list', () => {
Expand All @@ -373,4 +437,22 @@ describe('list', () => {
})
).toBe('apple, banana, or orange');
});

it('can combine a global format with an override', () => {
const formatter = createFormatter({
locale: 'en',
formats: {
list: {
short: {
type: 'disjunction'
}
}
}
});
expect(
formatter.list(['apple', 'banana', 'orange'], 'short', {
type: 'conjunction'
})
).toBe('apple, banana, and orange');
});
});
71 changes: 61 additions & 10 deletions packages/use-intl/src/core/createFormatter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ export default function createFormatter(props: Props) {

function resolveFormatOrOptions<Options>(
typeFormats: Record<string, Options> | undefined,
formatOrOptions?: string | Options
formatOrOptions?: string | Options,
overrides?: Options
) {
let options;
if (typeof formatOrOptions === 'string') {
Expand All @@ -135,18 +136,23 @@ export default function createFormatter(props: Props) {
options = formatOrOptions;
}

if (overrides) {
options = {...options, ...overrides};
}

return options;
}

function getFormattedValue<Options, Output>(
formatOrOptions: string | Options | undefined,
overrides: Options | undefined,
typeFormats: Record<string, Options> | undefined,
formatter: (options?: Options) => Output,
getFallback: () => Output
) {
let options;
try {
options = resolveFormatOrOptions(typeFormats, formatOrOptions);
options = resolveFormatOrOptions(typeFormats, formatOrOptions, overrides);
} catch {
return getFallback();
}
Expand All @@ -164,12 +170,22 @@ export default function createFormatter(props: Props) {
function dateTime(
/** If a number is supplied, this is interpreted as a UTC timestamp. */
value: Date | number,
/** If a time zone is supplied, the `value` is converted to that time zone.
* Otherwise the user time zone will be used. */
formatOrOptions?: FormatNames['dateTime'] | DateTimeFormatOptions
options?: DateTimeFormatOptions
): string;
function dateTime(
/** If a number is supplied, this is interpreted as a UTC timestamp. */
value: Date | number,
format?: FormatNames['dateTime'],
options?: DateTimeFormatOptions
): string;
function dateTime(
value: Date | number,
formatOrOptions?: FormatNames['dateTime'] | DateTimeFormatOptions,
overrides?: DateTimeFormatOptions
) {
return getFormattedValue(
formatOrOptions,
overrides,
formats?.dateTime,
(options) => {
options = applyTimeZone(options);
Expand All @@ -184,12 +200,25 @@ export default function createFormatter(props: Props) {
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?: FormatNames['dateTime'] | DateTimeFormatOptions
options?: DateTimeFormatOptions
): string;
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,
format?: FormatNames['dateTime'],
options?: DateTimeFormatOptions
): string;
function dateTimeRange(
start: Date | number,
end: Date | number,
formatOrOptions?: FormatNames['dateTime'] | DateTimeFormatOptions,
overrides?: DateTimeFormatOptions
) {
return getFormattedValue(
formatOrOptions,
overrides,
formats?.dateTime,
(options) => {
options = applyTimeZone(options);
Expand All @@ -203,10 +232,21 @@ export default function createFormatter(props: Props) {

function number(
value: number | bigint,
formatOrOptions?: FormatNames['number'] | NumberFormatOptions
options?: NumberFormatOptions
): string;
function number(
value: number | bigint,
format?: FormatNames['number'],
options?: NumberFormatOptions
): string;
function number(
value: number | bigint,
formatOrOptions?: FormatNames['number'] | NumberFormatOptions,
overrides?: NumberFormatOptions
) {
return getFormattedValue(
formatOrOptions,
overrides,
formats?.number,
(options) => formatters.getNumberFormat(locale, options).format(value),
() => String(value)
Expand Down Expand Up @@ -289,7 +329,17 @@ export default function createFormatter(props: Props) {
type FormattableListValue = string | ReactElement;
function list<Value extends FormattableListValue>(
value: Iterable<Value>,
formatOrOptions?: FormatNames['list'] | Intl.ListFormatOptions
options?: Intl.ListFormatOptions
): Value extends string ? string : Iterable<ReactElement>;
function list<Value extends FormattableListValue>(
value: Iterable<Value>,
format?: FormatNames['list'],
options?: Intl.ListFormatOptions
): Value extends string ? string : Iterable<ReactElement>;
function list<Value extends FormattableListValue>(
value: Iterable<Value>,
formatOrOptions?: FormatNames['list'] | Intl.ListFormatOptions,
overrides?: Intl.ListFormatOptions
): Value extends string ? string : Iterable<ReactElement> {
const serializedValue: Array<string> = [];
const richValues = new Map<string, Value>();
Expand All @@ -315,6 +365,7 @@ export default function createFormatter(props: Props) {
Value extends string ? string : Iterable<ReactElement>
>(
formatOrOptions,
overrides,
formats?.list,
// @ts-expect-error -- `richValues.size` is used to determine the return type, but TypeScript can't infer the meaning of this correctly
(options) => {
Expand Down
16 changes: 9 additions & 7 deletions packages/use-intl/src/react/useFormatter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import {parseISO} from 'date-fns';
import type {ComponentProps, ReactElement, ReactNode} from 'react';
import {type SpyImpl, spyOn} from 'tinyspy';
import {beforeEach, describe, expect, it, vi} from 'vitest';
import {type IntlError, IntlErrorCode} from '../core.js';
import {
type DateTimeFormatOptions,
type IntlError,
IntlErrorCode,
type NumberFormatOptions
} from '../core.js';
import IntlProvider from './IntlProvider.js';
import useFormatter from './useFormatter.js';

Expand All @@ -25,7 +30,7 @@ describe('dateTime', () => {

function renderDateTime(
value: Date | number,
options?: Parameters<ReturnType<typeof useFormatter>['dateTime']>['1']
options?: DateTimeFormatOptions
) {
function Component() {
const format = useFormatter();
Expand Down Expand Up @@ -287,10 +292,7 @@ describe('dateTime', () => {
});

describe('number', () => {
function renderNumber(
value: number | bigint,
options?: Parameters<ReturnType<typeof useFormatter>['number']>['1']
) {
function renderNumber(value: number | bigint, options?: NumberFormatOptions) {
function Component() {
const format = useFormatter();
return <>{format.number(value, options)}</>;
Expand Down Expand Up @@ -629,7 +631,7 @@ describe('relativeTime', () => {
describe('list', () => {
function renderList(
value: Iterable<string>,
options?: Parameters<ReturnType<typeof useFormatter>['list']>['1']
options?: Intl.ListFormatOptions
) {
function Component() {
const format = useFormatter();
Expand Down

0 comments on commit 77949ef

Please sign in to comment.