diff --git a/docs/pages/docs/getting-started/pages-router.mdx b/docs/pages/docs/getting-started/pages-router.mdx index fcaebee94..3f6f4554d 100644 --- a/docs/pages/docs/getting-started/pages-router.mdx +++ b/docs/pages/docs/getting-started/pages-router.mdx @@ -1,7 +1,7 @@ import {Tab, Tabs} from 'nextra-theme-docs'; import Callout from 'components/Callout'; -# Next.js internationalization (i18n) with the Pages Router +# Next.js Pages Router internationalization (i18n) While it's recommended to [use `next-intl` with the App Router](/docs/getting-started/app-router), the Pages Router is still fully supported. diff --git a/docs/pages/docs/usage/dates-times.mdx b/docs/pages/docs/usage/dates-times.mdx index b752e2f41..ad81641cb 100644 --- a/docs/pages/docs/usage/dates-times.mdx +++ b/docs/pages/docs/usage/dates-times.mdx @@ -179,7 +179,8 @@ You can customize the formatting by using date skeletons: ```json filename="en.json" { - "ordered": "Ordered on {orderDate, date, ::yyyyMd}" + // Renders e.g. "Ordered on Jul 9, 2024" + "ordered": "Ordered on {orderDate, date, ::yyyyMMMd}" } ``` @@ -187,24 +188,22 @@ Note the leading `::` that is used to indicate that a skeleton should be used. **These formats from ICU are supported:** -| Symbol | Meaning | -| :----: | :---------------------------- | -| G | Era designator | -| y | Year | -| M | Month in year | -| L | Stand-alone month in year | -| d | Day in month | -| E | Day of week | -| e | Local day of week | -| c | Stand-alone local day of week | -| a | AM/PM marker | -| h | Hour [1-12] | -| H | Hour [0-23] | -| K | Hour [0-11] | -| k | Hour [1-24] | -| m | Minute | -| s | Second | -| z | Time zone | +| Symbol | Meaning | Pattern | Example | +| :----: | :------------------------------------- | ---------------------------------------- | --------------------------------------------------- | +| G | Era designator (includes the date) | G
GGGG
GGGGG | 7/9/2024 AD
7/9/2024 Anno Domini
7/9/2024 A | +| y | Year | y
yy
yyyy | 2024
24
2024 | +| M | Month in year | M
MM
MMM
MMMM
MMMMM
| 7
07
Jul
July
J | +| d | Day in month | d
dd | 9
09 | +| E | Day of week | E
EEEE
EEEEE | Tue
Tuesday
T | +| h | Hour (1-12) | h
hh | 9 AM
09 AM | +| K | Hour (0-11) | K
KK | 0 AM (12 AM with `h`)
00 AM | +| H | Hour (0-23) | HH | 09 | +| k | Hour (1-24) | kk | 24 (00 with `H`) | +| m | Minute (2 digits if used with seconds) | m
mmss | 6
06:03 | +| s | Second (2 digits if used with minutes) | s
mmss | 3
06:03 | +| z | Time zone | z
zzzz | GMT+2
Central European Summer Time | + +Patterns can be combined with each other, therefore e.g. `yyyyMMMd` would return "Jul 9, 2024". ### Custom date and time formats diff --git a/packages/next-intl/package.json b/packages/next-intl/package.json index 67996e60d..7f5980389 100644 --- a/packages/next-intl/package.json +++ b/packages/next-intl/package.json @@ -114,11 +114,11 @@ "size-limit": [ { "path": "dist/production/index.react-client.js", - "limit": "13.055 KB" + "limit": "15.735 KB" }, { "path": "dist/production/index.react-server.js", - "limit": "13.765 KB" + "limit": "16.5 KB" }, { "path": "dist/production/navigation.react-client.js", @@ -134,7 +134,7 @@ }, { "path": "dist/production/server.react-server.js", - "limit": "13.05 KB" + "limit": "15.645 KB" }, { "path": "dist/production/middleware.js", diff --git a/packages/use-intl/package.json b/packages/use-intl/package.json index fd422b337..cc668bbf7 100644 --- a/packages/use-intl/package.json +++ b/packages/use-intl/package.json @@ -64,7 +64,7 @@ ], "dependencies": { "@formatjs/ecma402-abstract": "^1.11.4", - "intl-messageformat": "^9.3.18" + "intl-messageformat": "^10.5.11" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" @@ -90,7 +90,7 @@ "size-limit": [ { "path": "dist/production/index.js", - "limit": "12.575 kB" + "limit": "15.235 kB" } ] } diff --git a/packages/use-intl/src/core/createBaseTranslator.tsx b/packages/use-intl/src/core/createBaseTranslator.tsx index 24f2a1c7b..ddaf5731c 100644 --- a/packages/use-intl/src/core/createBaseTranslator.tsx +++ b/packages/use-intl/src/core/createBaseTranslator.tsx @@ -262,7 +262,11 @@ function createBaseTranslatorImpl< { formatters: { getNumberFormat(locales, options) { - return new Intl.NumberFormat(locales, options); + return new Intl.NumberFormat( + locales, + // `useGrouping` was changed from a boolean later to a string enum or boolean, the type definition is outdated (https://tc39.es/proposal-intl-numberformat-v3/#grouping-enum-ecma-402-367) + options as Intl.NumberFormatOptions + ); }, getDateTimeFormat(locales, options) { // Workaround for https://github.com/formatjs/formatjs/issues/4279 diff --git a/packages/use-intl/test/core/createTranslator.test.tsx b/packages/use-intl/test/core/createTranslator.test.tsx index 821147bee..6fdf5a6b3 100644 --- a/packages/use-intl/test/core/createTranslator.test.tsx +++ b/packages/use-intl/test/core/createTranslator.test.tsx @@ -73,6 +73,105 @@ it('throws an error for non-alphanumeric value names', () => { expect(error.code).toBe('INVALID_MESSAGE'); }); +describe('dates in messages', () => { + it.each([ + ['G', '7/9/2024 AD'], // 🤔 Includes date + ['GG', '7/9/2024 AD'], // 🤔 Includes date + ['GGGG', '7/9/2024 Anno Domini'], // 🤔 Includes date + ['GGGGG', '7/9/2024 A'], // 🤔 Includes date + + ['y', '2024'], + ['yy', '24'], + ['yyyy', '2024'], + + ['M', '7'], + ['MM', '07'], + ['MMM', 'Jul'], + ['MMMM', 'July'], + ['MMMMM', 'J'], + + // Same as M + ['L', '7'], + ['LL', '07'], + ['LLL', 'Jul'], + ['LLLL', 'July'], + ['LLLLL', 'J'], + + ['d', '9'], + ['dd', '09'], + + ['E', 'Tue'], + ['EE', 'Tue'], + ['EEE', 'Tue'], + ['EEEE', 'Tuesday'], + ['EEEEE', 'T'], + // ['e', '7'] // 🤔 Not supported + // ['ee', '07'] // 🤔 Not supported + // ['eee', 'Jul'] // 🤔 Not supported + + // ['eeee', 'Tuesday'], // ❌ "Tue" + // ['eeeee', 'T'], // ❌ "Tuesday" + // ['eeeeee', 'Tu'] // ❌ "T" + + // ['c', '7'] // 🤔 Not supported + // ['cc', '07'], // 🤔 Not supported + // ['ccc', 'Jul'] // 🤔 Not supported + + // ['cccc', 'Tuesday'] // ❌ "Tue" + // ['ccccc', 'T'], // ❌ "Tuesday" + // ['cccccc', 'Tu'] // ❌ "T" + + // 🤔 Only in combination with time? + // ['a', 'PM'] // ❌ "7/9/2024" + // ['aa', 'PM'] // ❌ "7/9/2024" + // ['aaa', 'PM'] // ❌ "7/9/2024" + // ['aaaa', 'PM'] // ❌ "7/9/2024" + // ['aaaaa', 'PM'] // ❌ "7/9/2024" + + ['h', '12 AM', '2024-07-09T22:00:00.000Z'], + ['h', '9 AM'], + ['hh', '09 AM'], + + // ['H', '9'], // ❌ "09" + ['HH', '09'], + ['HH', '00', '2024-07-09T22:00:00.000Z'], + + ['K', '0 AM', '2024-07-09T22:00:00.000Z'], + ['KK', '00 AM', '2024-07-09T22:00:00.000Z'], + ['K', '9 AM'], + ['KK', '09 AM'], + + // ['k', '9'], // ❌ "09" + ['kk', '09'], + ['kk', '24', '2024-07-09T22:00:00.000Z'], + + ['m', '6'], + // ['mm', '06'] // ❌ "6" + + ['s', '3'], + // ['ss', '03'], // ❌ "3" + ['mmss', '06:03'], + + ['z', '7/9/2024, GMT+2'], // 🤔 Includes date + ['zz', '7/9/2024, GMT+2'], // 🤔 Includes date + ['zzz', '7/9/2024, GMT+2'], // 🤔 Includes date + ['zzzz', '7/9/2024, Central European Summer Time'], // 🤔 Includes date + + ['yyyyMMMd', 'Jul 9, 2024'], + [ + 'GGGGyyyyMMMMEdhmszzzz', + 'Tue, July 9, 2024 Anno Domini at 9:06:03 AM Central European Summer Time' + ] + ])('%s: %s', (value, expected, dateString = '2024-07-09T07:06:03.320Z') => { + const date = new Date(dateString); + const t = createTranslator({ + locale: 'en', + messages: {date: `{date, date, ::${value}}`} + }); + expect(t('date', {date})).toBe(expected); + }); +}); + describe('t.rich', () => { it('can translate a message to a ReactNode', () => { const t = createTranslator({ diff --git a/packages/use-intl/test/react/useTranslations.test.tsx b/packages/use-intl/test/react/useTranslations.test.tsx index 457aca8f3..f70a6c1ae 100644 --- a/packages/use-intl/test/react/useTranslations.test.tsx +++ b/packages/use-intl/test/react/useTranslations.test.tsx @@ -643,7 +643,7 @@ describe('error handling', () => { const error: IntlError = onError.mock.calls[0][0]; expect(error.message).toBe( - 'INVALID_MESSAGE: Expected "date", "number", "plural", "select", "selectordinal", or "time" but "c" found.' + 'INVALID_MESSAGE: INVALID_ARGUMENT_TYPE ({value, currency})' ); expect(error.code).toBe(IntlErrorCode.INVALID_MESSAGE); screen.getByText('price'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e80f8ad4..494e643ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -623,8 +623,8 @@ importers: specifier: ^1.11.4 version: 1.11.4 intl-messageformat: - specifier: ^9.3.18 - version: 9.3.18 + specifier: ^10.5.11 + version: 10.5.11 devDependencies: '@arethetypeswrong/cli': specifier: ^0.13.5 @@ -6560,12 +6560,40 @@ packages: tslib: 2.3.1 dev: false + /@formatjs/ecma402-abstract@1.18.2: + resolution: {integrity: sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==} + dependencies: + '@formatjs/intl-localematcher': 0.5.4 + tslib: 2.5.0 + dev: false + /@formatjs/ecma402-abstract@1.4.0: resolution: {integrity: sha512-Mv027hcLFjE45K8UJ8PjRpdDGfR0aManEFj1KzoN8zXNveHGEygpZGfFf/FTTMl+QEVSrPAUlyxaCApvmv47AQ==} dependencies: tslib: 2.5.0 dev: false + /@formatjs/fast-memoize@2.2.0: + resolution: {integrity: sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==} + dependencies: + tslib: 2.5.0 + dev: false + + /@formatjs/icu-messageformat-parser@2.7.6: + resolution: {integrity: sha512-etVau26po9+eewJKYoiBKP6743I1br0/Ie00Pb/S/PtmYfmjTcOn2YCh2yNkSZI12h6Rg+BOgQYborXk46BvkA==} + dependencies: + '@formatjs/ecma402-abstract': 1.18.2 + '@formatjs/icu-skeleton-parser': 1.8.0 + tslib: 2.5.0 + dev: false + + /@formatjs/icu-skeleton-parser@1.8.0: + resolution: {integrity: sha512-QWLAYvM0n8hv7Nq5BEs4LKIjevpVpbGLAJgOaYzg9wABEoX1j0JO1q2/jVkO6CVlq0dbsxZCngS5aXbysYueqA==} + dependencies: + '@formatjs/ecma402-abstract': 1.18.2 + tslib: 2.5.0 + dev: false + /@formatjs/intl-localematcher@0.2.25: resolution: {integrity: sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==} dependencies: @@ -6578,6 +6606,12 @@ packages: tslib: 2.5.0 dev: false + /@formatjs/intl-localematcher@0.5.4: + resolution: {integrity: sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==} + dependencies: + tslib: 2.5.0 + dev: false + /@gar/promisify@1.1.3: resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} @@ -17093,6 +17127,15 @@ packages: tslib: 2.5.0 dev: false + /intl-messageformat@10.5.11: + resolution: {integrity: sha512-eYq5fkFBVxc7GIFDzpFQkDOZgNayNTQn4Oufe8jw6YY6OHVw70/4pA3FyCsQ0Gb2DnvEJEMmN2tOaXUGByM+kg==} + dependencies: + '@formatjs/ecma402-abstract': 1.18.2 + '@formatjs/fast-memoize': 2.2.0 + '@formatjs/icu-messageformat-parser': 2.7.6 + tslib: 2.5.0 + dev: false + /intl-messageformat@9.3.18: resolution: {integrity: sha512-OKrLWppdxXtRdRCPjmRZ9Ru7UZkZJDlMl+1Vpb3sCLWK0mFpr129K+gIlIb5zrWoAH3NiYDzekBXPTRWCyHSIA==} dependencies: