From 9a7b17e99db506b701ca601b09a4315db0a7e1be Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 11 Oct 2024 10:02:50 +0200 Subject: [PATCH] feat: Deprecate `defaultTranslationValues` (#1411) `defaultTranslationValues` allow to share global values to be used in messages across your app. The most common case are shared rich text elements (e.g. `b: (chunks) => {chunks}`). However, over time this feature has shown drawbacks: 1. We can't serialize them automatically across the RSC boundary (see https://github.com/amannn/next-intl/issues/611) 2. They get in the way of type-safe arguments (see https://github.com/amannn/next-intl/issues/410) Due to this, the feature will be deprecated and the docs will suggest a better alternative for common tags in rich text that doesn't have the limitations mentioned above ([updated docs](https://next-intl-docs-git-feat-deprecate-defaulttrans-f52ebe-next-intl.vercel.app/docs/usage/messages#rich-text-reuse-tags)). Shared values don't get a direct replacement from `next-intl`, but should be handled as part of your app logic (e.g. a shared module, React Context, etc.). **Note**: https://github.com/amannn/next-intl/issues/410 can not be implemented immediately as part of this, as long as `defaultTranslationValues` are still available (even if deprecated). Instead, this feature could be added as part of the next major release. Contributes to https://github.com/amannn/next-intl/issues/611 --- docs/pages/docs/usage/configuration.mdx | 7 ++- docs/pages/docs/usage/messages.mdx | 43 +++++++++++++++++-- .../messages/de.json | 5 +-- .../messages/en.json | 5 +-- .../messages/es.json | 5 +-- .../messages/ja.json | 3 +- .../src/app/[locale]/page.tsx | 8 ++-- .../src/components/AsyncComponent.tsx | 6 ++- .../src/components/RichText.tsx | 18 ++++++++ .../src/i18n/request.tsx | 4 -- .../tests/main.spec.ts | 16 +++---- packages/use-intl/src/core/IntlConfig.tsx | 5 ++- 12 files changed, 89 insertions(+), 36 deletions(-) create mode 100644 examples/example-app-router-playground/src/components/RichText.tsx diff --git a/docs/pages/docs/usage/configuration.mdx b/docs/pages/docs/usage/configuration.mdx index 0c3bef682..3c9090804 100644 --- a/docs/pages/docs/usage/configuration.mdx +++ b/docs/pages/docs/usage/configuration.mdx @@ -541,7 +541,12 @@ function Component() { } ``` -## Default translation values +## Default translation values (deprecated) [#default-translation-values] + + + This feature is deprecated and will be removed in the next major version of `next-intl` ([alternative](/docs/usage/messages#rich-text-reuse-tags)). + + To achieve consistent usage of translation values and reduce redundancy, you can define a set of global default values. This configuration can also be used to apply consistent styling of commonly used rich text elements. diff --git a/docs/pages/docs/usage/messages.mdx b/docs/pages/docs/usage/messages.mdx index 322a5e6b8..632fb14b5 100644 --- a/docs/pages/docs/usage/messages.mdx +++ b/docs/pages/docs/usage/messages.mdx @@ -333,7 +333,7 @@ t('message'); // "Escape curly braces with single quotes (e.g. {name})" ## Rich text -You can format rich text with custom tags and map them to React components: +You can format rich text with custom tags and map them to React components via `t.rich`: ```json filename="en.json" { @@ -351,9 +351,46 @@ t.rich('message', { Tags can be arbitrarily nested (e.g. `This is very important`).
-How can I reuse a tag across my app? +How can I reuse tags across my app? -If you want to use the same tag across your app, you can configure it via [default translation values](/docs/usage/configuration#default-translation-values). +Common tags for rich text that you want to share across your app can be defined in a shared module and imported where relevant for usage with `t.rich`. + +A convenient pattern is to use a component that provides common tags via a render prop: + +```js +import {useTranslations} from 'next-intl'; +import RichText from '@/components/RichText'; + +function AboutPage() { + const t = useTranslations('AboutPage'); + return {(tags) => t.rich('description', tags)}; +} +``` + +In this case, the `RichText` component can provide styled tags and also a general layout for the text: + +```js filename="components/RichText.tsx" +import {ReactNode} from 'react'; + +// These tags are available +type Tag = 'p' | 'b' | 'i'; + +type Props = { + children(tags: Record ReactNode>): ReactNode +}; + +export default function RichText({children}: Props) { + return ( +
+ {children({ + p: (chunks: ReactNode) =>

{chunks}

, + b: (chunks: ReactNode) => {chunks}, + i: (chunks: ReactNode) => {chunks} + })} +
+ ); +} +```
diff --git a/examples/example-app-router-playground/messages/de.json b/examples/example-app-router-playground/messages/de.json index e07eb2d61..42dfccb6c 100644 --- a/examples/example-app-router-playground/messages/de.json +++ b/examples/example-app-router-playground/messages/de.json @@ -4,7 +4,7 @@ }, "AsyncComponent": { "basic": "AsyncComponent", - "markup": "Markup with {globalString}", + "markup": "Markup with bold content", "rich": "This is a rich text." }, "Client": { @@ -21,8 +21,7 @@ }, "Index": { "description": "Das ist die Startseite.", - "globalDefaults": "{globalString}", - "rich": "Das ist formatierter Test.", + "rich": "Das ist formatierter Test.", "title": "Start" }, "JustIn": { diff --git a/examples/example-app-router-playground/messages/en.json b/examples/example-app-router-playground/messages/en.json index d5cb38fe8..bb38eebcf 100644 --- a/examples/example-app-router-playground/messages/en.json +++ b/examples/example-app-router-playground/messages/en.json @@ -4,7 +4,7 @@ }, "AsyncComponent": { "basic": "AsyncComponent", - "markup": "Markup with {globalString}", + "markup": "Markup with bold content", "rich": "This is a rich text." }, "Client": { @@ -21,8 +21,7 @@ }, "Index": { "description": "This is the home page.", - "globalDefaults": "{globalString}", - "rich": "This is a rich text.", + "rich": "This is a rich text.", "title": "Home" }, "JustIn": { diff --git a/examples/example-app-router-playground/messages/es.json b/examples/example-app-router-playground/messages/es.json index 21a96164b..587900a5a 100644 --- a/examples/example-app-router-playground/messages/es.json +++ b/examples/example-app-router-playground/messages/es.json @@ -4,7 +4,7 @@ }, "AsyncComponent": { "basic": "AsyncComponent", - "markup": "Markup with {globalString}", + "markup": "Markup with bold content", "rich": "This is a rich text." }, "Client": { @@ -21,8 +21,7 @@ }, "Index": { "description": "Esta es la página de inicio.", - "globalDefaults": "{globalString}", - "rich": "Este es un texto importante.", + "rich": "Este es un texto importante.", "title": "Inicio" }, "JustIn": { diff --git a/examples/example-app-router-playground/messages/ja.json b/examples/example-app-router-playground/messages/ja.json index 03365ac49..c949c3f65 100644 --- a/examples/example-app-router-playground/messages/ja.json +++ b/examples/example-app-router-playground/messages/ja.json @@ -21,8 +21,7 @@ }, "Index": { "description": "This is the home page (ja).", - "globalDefaults": "{globalString} (ja)", - "rich": "This is a rich text (ja).", + "rich": "This is a rich text (ja).", "title": "Home (ja)" }, "JustIn": { diff --git a/examples/example-app-router-playground/src/app/[locale]/page.tsx b/examples/example-app-router-playground/src/app/[locale]/page.tsx index e4424df57..d99b2dd07 100644 --- a/examples/example-app-router-playground/src/app/[locale]/page.tsx +++ b/examples/example-app-router-playground/src/app/[locale]/page.tsx @@ -12,6 +12,7 @@ import PageLayout from '../../components/PageLayout'; import MessagesAsPropsCounter from '../../components/client/01-MessagesAsPropsCounter'; import MessagesOnClientCounter from '../../components/client/02-MessagesOnClientCounter'; import DropdownMenu from '@/components/DropdownMenu'; +import RichText from '@/components/RichText'; import {Link} from '@/i18n/routing'; type Props = { @@ -27,14 +28,13 @@ export default function Index({searchParams}: Props) { return (

{t('description')}

-

- {t.rich('rich', {important: (chunks) => {chunks}})} -

+ + {(tags) => t.rich('rich', tags)} +

-

{t.rich('globalDefaults')}

{/* @ts-expect-error Purposefully trigger an error */}

{t('missing')}

diff --git a/examples/example-app-router-playground/src/components/AsyncComponent.tsx b/examples/example-app-router-playground/src/components/AsyncComponent.tsx index e7cd0efe4..d3f985e35 100644 --- a/examples/example-app-router-playground/src/components/AsyncComponent.tsx +++ b/examples/example-app-router-playground/src/components/AsyncComponent.tsx @@ -7,7 +7,11 @@ export default async function AsyncComponent() {

{t('basic')}

{t.rich('rich', {important: (chunks) => {chunks}})}

-

{t.markup('markup', {b: (chunks) => `${chunks}`})}

+

+ {t.markup('markup', { + important: (chunks) => `${chunks}` + })} +

{String(t.has('basic'))}

); diff --git a/examples/example-app-router-playground/src/components/RichText.tsx b/examples/example-app-router-playground/src/components/RichText.tsx new file mode 100644 index 000000000..c51edd90a --- /dev/null +++ b/examples/example-app-router-playground/src/components/RichText.tsx @@ -0,0 +1,18 @@ +import {ComponentProps, ReactNode} from 'react'; + +type Tag = 'b' | 'i'; + +type Props = { + children(tags: Record ReactNode>): ReactNode; +} & Omit, 'children'>; + +export default function RichText({children, ...rest}: Props) { + return ( +

+ {children({ + b: (chunks: ReactNode) => {chunks}, + i: (chunks: ReactNode) => {chunks} + })} +

+ ); +} diff --git a/examples/example-app-router-playground/src/i18n/request.tsx b/examples/example-app-router-playground/src/i18n/request.tsx index 79af138bc..d132c19e2 100644 --- a/examples/example-app-router-playground/src/i18n/request.tsx +++ b/examples/example-app-router-playground/src/i18n/request.tsx @@ -45,10 +45,6 @@ export default getRequestConfig(async ({requestLocale}) => { now: now ? new Date(now) : undefined, timeZone, messages, - defaultTranslationValues: { - globalString: 'Global string', - highlight: (chunks) => {chunks} - }, formats, onError(error) { if ( diff --git a/examples/example-app-router-playground/tests/main.spec.ts b/examples/example-app-router-playground/tests/main.spec.ts index 89d6df713..f364e7a22 100644 --- a/examples/example-app-router-playground/tests/main.spec.ts +++ b/examples/example-app-router-playground/tests/main.spec.ts @@ -215,21 +215,15 @@ it('can use next-intl on the client side', async ({page}) => { it('can use rich text', async ({page}) => { await page.goto('/en'); const element = page.getByTestId('RichText'); - expect(await element.innerHTML()).toBe('This is a rich text.'); -}); - -it('can use raw text', async ({page}) => { - await page.goto('/en'); - const element = page.getByTestId('RawText'); expect(await element.innerHTML()).toBe( - 'This is a rich text.' + 'This is a rich text.' ); }); -it('can use global defaults', async ({page}) => { +it('can use raw text', async ({page}) => { await page.goto('/en'); - const element = page.getByTestId('GlobalDefaults'); - expect(await element.innerHTML()).toBe('Global string'); + const element = page.getByTestId('RawText'); + expect(await element.innerHTML()).toBe('This is a rich text.'); }); it('can use `getMessageFallback`', async ({page}) => { @@ -642,7 +636,7 @@ it('can use async APIs in async components', async ({page}) => { const element1 = page.getByTestId('AsyncComponent'); element1.getByText('AsyncComponent'); expect(await element1.innerHTML()).toContain('This is a rich text.'); - element1.getByText('Markup with Global string'); + element1.getByText('Markup with bold content'); page .getByTestId('AsyncComponentWithoutNamespace') diff --git a/packages/use-intl/src/core/IntlConfig.tsx b/packages/use-intl/src/core/IntlConfig.tsx index 7b4ce5db1..e2f87b1b8 100644 --- a/packages/use-intl/src/core/IntlConfig.tsx +++ b/packages/use-intl/src/core/IntlConfig.tsx @@ -43,7 +43,10 @@ type IntlConfig = { messages?: Messages; /** Global default values for translation values and rich text elements. * Can be used for consistent usage or styling of rich text elements. - * Defaults will be overidden by locally provided values. */ + * Defaults will be overidden by locally provided values. + * + * @deprecated See https://next-intl-docs.vercel.app/docs/usage/messages#rich-text-reuse-tags + * */ defaultTranslationValues?: RichTranslationValues; };