Skip to content

Commit 77949ef

Browse files
authored
feat: Allow to override options of global formats (#1693)
**Example:** ```tsx formatter.dateTime(date, 'short', { timeZone: 'America/New_York' }) ```
1 parent 40d535a commit 77949ef

File tree

7 files changed

+168
-21
lines changed

7 files changed

+168
-21
lines changed

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ See [the MDN docs about `DateTimeFormat`](https://developer.mozilla.org/en-US/do
3434
If you have [global formats](/docs/usage/configuration#formats) configured, you can reference them by passing a name as the second argument:
3535

3636
```js
37+
// Use a global format
3738
format.dateTime(dateTime, 'short');
39+
40+
// Optionally override some options
41+
format.dateTime(dateTime, 'short', {year: 'numeric'});
3842
```
3943

4044
<Details id="parsing-manipulation">
@@ -204,10 +208,14 @@ function Component() {
204208
}
205209
```
206210

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

209213
```js
214+
// Use a global format
210215
format.dateTimeRange(dateTimeA, dateTimeB, 'short');
216+
217+
// Optionally override some options
218+
format.dateTimeRange(dateTimeA, dateTimeB, 'short', {year: 'numeric'});
211219
```
212220

213221
## Dates and times within messages

docs/src/pages/docs/usage/numbers.mdx

+4
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ See [the MDN docs about `NumberFormat`](https://developer.mozilla.org/en-US/docs
3131
If you have [global formats](/docs/usage/configuration#formats) configured, you can reference them by passing a name as the second argument:
3232

3333
```js
34+
// Use a global format
3435
format.number(499.9, 'precise');
36+
37+
// Optionally override some options
38+
format.number(499.9, 'price', {currency: 'USD'});
3539
```
3640

3741
## Numbers within messages

packages/next-intl/.size-limit.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const config: SizeLimitConfig = [
44
{
55
name: "import * from 'next-intl' (react-client)",
66
path: 'dist/esm/production/index.react-client.js',
7-
limit: '13.065 KB'
7+
limit: '13.125 KB'
88
},
99
{
1010
name: "import {NextIntlClientProvider} from 'next-intl' (react-client)",

packages/use-intl/.size-limit.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ const config: SizeLimitConfig = [
55
name: "import * from 'use-intl' (production)",
66
import: '*',
77
path: 'dist/esm/production/index.js',
8-
limit: '12.955 kB'
8+
limit: '13.015 kB'
99
},
1010
{
1111
name: "import {IntlProvider, useLocale, useNow, useTimeZone, useMessages, useFormatter} from 'use-intl' (production)",
1212
path: 'dist/esm/production/index.js',
1313
import:
1414
'{IntlProvider, useLocale, useNow, useTimeZone, useMessages, useFormatter}',
15-
limit: '1.995 kB'
15+
limit: '2.005 kB'
1616
}
1717
];
1818

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

+82
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,26 @@ describe('dateTime', () => {
2828
})
2929
).toBe('Nov 20, 2020, 5:36:01 AM');
3030
});
31+
32+
it('can combine a global format with an override', () => {
33+
const formatter = createFormatter({
34+
locale: 'en',
35+
timeZone: 'Europe/Berlin',
36+
formats: {
37+
dateTime: {
38+
short: {
39+
dateStyle: 'short',
40+
timeStyle: 'short'
41+
}
42+
}
43+
}
44+
});
45+
expect(
46+
formatter.dateTime(parseISO('2020-11-20T10:36:01.516Z'), 'short', {
47+
timeZone: 'America/New_York'
48+
})
49+
).toBe('11/20/20, 5:36 AM');
50+
});
3151
});
3252

3353
describe('number', () => {
@@ -71,6 +91,25 @@ describe('number', () => {
7191
})
7292
).toBe('$123,456,789,123,456,789.00');
7393
});
94+
95+
it('can combine a global format with an override', () => {
96+
const formatter = createFormatter({
97+
locale: 'en',
98+
timeZone: 'Europe/Berlin',
99+
formats: {
100+
number: {
101+
price: {
102+
style: 'currency',
103+
minimumFractionDigits: 2,
104+
maximumFractionDigits: 2
105+
}
106+
}
107+
}
108+
});
109+
expect(formatter.number(123456.789, 'price', {currency: 'EUR'})).toBe(
110+
'€123,456.79'
111+
);
112+
});
74113
});
75114

76115
describe('relativeTime', () => {
@@ -349,6 +388,31 @@ describe('dateTimeRange', () => {
349388
)
350389
).toBe('Jan 10, 2007, 4:00:00 AM – Jan 10, 2008, 5:00:00 AM');
351390
});
391+
392+
it('can combine a global format with an override', () => {
393+
const formatter = createFormatter({
394+
locale: 'en',
395+
timeZone: 'Europe/Berlin',
396+
formats: {
397+
dateTime: {
398+
short: {
399+
dateStyle: 'short',
400+
timeStyle: 'short'
401+
}
402+
}
403+
}
404+
});
405+
expect(
406+
formatter.dateTimeRange(
407+
new Date(2007, 0, 10, 10, 0, 0),
408+
new Date(2008, 0, 10, 11, 0, 0),
409+
'short',
410+
{
411+
timeZone: 'America/New_York'
412+
}
413+
)
414+
).toBe('1/10/07, 4:00 AM – 1/10/08, 5:00 AM');
415+
});
352416
});
353417

354418
describe('list', () => {
@@ -373,4 +437,22 @@ describe('list', () => {
373437
})
374438
).toBe('apple, banana, or orange');
375439
});
440+
441+
it('can combine a global format with an override', () => {
442+
const formatter = createFormatter({
443+
locale: 'en',
444+
formats: {
445+
list: {
446+
short: {
447+
type: 'disjunction'
448+
}
449+
}
450+
}
451+
});
452+
expect(
453+
formatter.list(['apple', 'banana', 'orange'], 'short', {
454+
type: 'conjunction'
455+
})
456+
).toBe('apple, banana, and orange');
457+
});
376458
});

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

+61-10
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ export default function createFormatter(props: Props) {
114114

115115
function resolveFormatOrOptions<Options>(
116116
typeFormats: Record<string, Options> | undefined,
117-
formatOrOptions?: string | Options
117+
formatOrOptions?: string | Options,
118+
overrides?: Options
118119
) {
119120
let options;
120121
if (typeof formatOrOptions === 'string') {
@@ -135,18 +136,23 @@ export default function createFormatter(props: Props) {
135136
options = formatOrOptions;
136137
}
137138

139+
if (overrides) {
140+
options = {...options, ...overrides};
141+
}
142+
138143
return options;
139144
}
140145

141146
function getFormattedValue<Options, Output>(
142147
formatOrOptions: string | Options | undefined,
148+
overrides: Options | undefined,
143149
typeFormats: Record<string, Options> | undefined,
144150
formatter: (options?: Options) => Output,
145151
getFallback: () => Output
146152
) {
147153
let options;
148154
try {
149-
options = resolveFormatOrOptions(typeFormats, formatOrOptions);
155+
options = resolveFormatOrOptions(typeFormats, formatOrOptions, overrides);
150156
} catch {
151157
return getFallback();
152158
}
@@ -164,12 +170,22 @@ export default function createFormatter(props: Props) {
164170
function dateTime(
165171
/** If a number is supplied, this is interpreted as a UTC timestamp. */
166172
value: Date | number,
167-
/** If a time zone is supplied, the `value` is converted to that time zone.
168-
* Otherwise the user time zone will be used. */
169-
formatOrOptions?: FormatNames['dateTime'] | DateTimeFormatOptions
173+
options?: DateTimeFormatOptions
174+
): string;
175+
function dateTime(
176+
/** If a number is supplied, this is interpreted as a UTC timestamp. */
177+
value: Date | number,
178+
format?: FormatNames['dateTime'],
179+
options?: DateTimeFormatOptions
180+
): string;
181+
function dateTime(
182+
value: Date | number,
183+
formatOrOptions?: FormatNames['dateTime'] | DateTimeFormatOptions,
184+
overrides?: DateTimeFormatOptions
170185
) {
171186
return getFormattedValue(
172187
formatOrOptions,
188+
overrides,
173189
formats?.dateTime,
174190
(options) => {
175191
options = applyTimeZone(options);
@@ -184,12 +200,25 @@ export default function createFormatter(props: Props) {
184200
start: Date | number,
185201
/** If a number is supplied, this is interpreted as a UTC timestamp. */
186202
end: Date | number,
187-
/** If a time zone is supplied, the values are converted to that time zone.
188-
* Otherwise the user time zone will be used. */
189-
formatOrOptions?: FormatNames['dateTime'] | DateTimeFormatOptions
203+
options?: DateTimeFormatOptions
204+
): string;
205+
function dateTimeRange(
206+
/** If a number is supplied, this is interpreted as a UTC timestamp. */
207+
start: Date | number,
208+
/** If a number is supplied, this is interpreted as a UTC timestamp. */
209+
end: Date | number,
210+
format?: FormatNames['dateTime'],
211+
options?: DateTimeFormatOptions
212+
): string;
213+
function dateTimeRange(
214+
start: Date | number,
215+
end: Date | number,
216+
formatOrOptions?: FormatNames['dateTime'] | DateTimeFormatOptions,
217+
overrides?: DateTimeFormatOptions
190218
) {
191219
return getFormattedValue(
192220
formatOrOptions,
221+
overrides,
193222
formats?.dateTime,
194223
(options) => {
195224
options = applyTimeZone(options);
@@ -203,10 +232,21 @@ export default function createFormatter(props: Props) {
203232

204233
function number(
205234
value: number | bigint,
206-
formatOrOptions?: FormatNames['number'] | NumberFormatOptions
235+
options?: NumberFormatOptions
236+
): string;
237+
function number(
238+
value: number | bigint,
239+
format?: FormatNames['number'],
240+
options?: NumberFormatOptions
241+
): string;
242+
function number(
243+
value: number | bigint,
244+
formatOrOptions?: FormatNames['number'] | NumberFormatOptions,
245+
overrides?: NumberFormatOptions
207246
) {
208247
return getFormattedValue(
209248
formatOrOptions,
249+
overrides,
210250
formats?.number,
211251
(options) => formatters.getNumberFormat(locale, options).format(value),
212252
() => String(value)
@@ -289,7 +329,17 @@ export default function createFormatter(props: Props) {
289329
type FormattableListValue = string | ReactElement;
290330
function list<Value extends FormattableListValue>(
291331
value: Iterable<Value>,
292-
formatOrOptions?: FormatNames['list'] | Intl.ListFormatOptions
332+
options?: Intl.ListFormatOptions
333+
): Value extends string ? string : Iterable<ReactElement>;
334+
function list<Value extends FormattableListValue>(
335+
value: Iterable<Value>,
336+
format?: FormatNames['list'],
337+
options?: Intl.ListFormatOptions
338+
): Value extends string ? string : Iterable<ReactElement>;
339+
function list<Value extends FormattableListValue>(
340+
value: Iterable<Value>,
341+
formatOrOptions?: FormatNames['list'] | Intl.ListFormatOptions,
342+
overrides?: Intl.ListFormatOptions
293343
): Value extends string ? string : Iterable<ReactElement> {
294344
const serializedValue: Array<string> = [];
295345
const richValues = new Map<string, Value>();
@@ -315,6 +365,7 @@ export default function createFormatter(props: Props) {
315365
Value extends string ? string : Iterable<ReactElement>
316366
>(
317367
formatOrOptions,
368+
overrides,
318369
formats?.list,
319370
// @ts-expect-error -- `richValues.size` is used to determine the return type, but TypeScript can't infer the meaning of this correctly
320371
(options) => {

packages/use-intl/src/react/useFormatter.test.tsx

+9-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import {parseISO} from 'date-fns';
33
import type {ComponentProps, ReactElement, ReactNode} from 'react';
44
import {type SpyImpl, spyOn} from 'tinyspy';
55
import {beforeEach, describe, expect, it, vi} from 'vitest';
6-
import {type IntlError, IntlErrorCode} from '../core.js';
6+
import {
7+
type DateTimeFormatOptions,
8+
type IntlError,
9+
IntlErrorCode,
10+
type NumberFormatOptions
11+
} from '../core.js';
712
import IntlProvider from './IntlProvider.js';
813
import useFormatter from './useFormatter.js';
914

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

2631
function renderDateTime(
2732
value: Date | number,
28-
options?: Parameters<ReturnType<typeof useFormatter>['dateTime']>['1']
33+
options?: DateTimeFormatOptions
2934
) {
3035
function Component() {
3136
const format = useFormatter();
@@ -287,10 +292,7 @@ describe('dateTime', () => {
287292
});
288293

289294
describe('number', () => {
290-
function renderNumber(
291-
value: number | bigint,
292-
options?: Parameters<ReturnType<typeof useFormatter>['number']>['1']
293-
) {
295+
function renderNumber(value: number | bigint, options?: NumberFormatOptions) {
294296
function Component() {
295297
const format = useFormatter();
296298
return <>{format.number(value, options)}</>;
@@ -629,7 +631,7 @@ describe('relativeTime', () => {
629631
describe('list', () => {
630632
function renderList(
631633
value: Iterable<string>,
632-
options?: Parameters<ReturnType<typeof useFormatter>['list']>['1']
634+
options?: Intl.ListFormatOptions
633635
) {
634636
function Component() {
635637
const format = useFormatter();

0 commit comments

Comments
 (0)