Skip to content

Commit

Permalink
feat: Deprecate defaultTranslationValues (#1411)
Browse files Browse the repository at this point in the history
`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) => <b>{chunks}</b>`).

However, over time this feature has shown drawbacks:
1. We can't serialize them automatically across the RSC boundary (see
#611)
2. They get in the way of type-safe arguments (see
#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**: #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 #611
  • Loading branch information
amannn authored Oct 11, 2024
1 parent 5502984 commit 9a7b17e
Show file tree
Hide file tree
Showing 12 changed files with 89 additions and 36 deletions.
7 changes: 6 additions & 1 deletion docs/pages/docs/usage/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,12 @@ function Component() {
}
```

## Default translation values
## Default translation values (deprecated) [#default-translation-values]

<Callout>
This feature is deprecated and will be removed in the next major version of `next-intl` ([alternative](/docs/usage/messages#rich-text-reuse-tags)).

</Callout>

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.

Expand Down
43 changes: 40 additions & 3 deletions docs/pages/docs/usage/messages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
{
Expand All @@ -351,9 +351,46 @@ t.rich('message', {
Tags can be arbitrarily nested (e.g. `This is <important><very>very</very> important</important>`).

<Details id="rich-text-reuse-tags">
<summary>How can I reuse a tag across my app?</summary>
<summary>How can I reuse tags across my app?</summary>

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 <RichText>{(tags) => t.rich('description', tags)}</RichText>;
}
```

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<Tag, (chunks: ReactNode) => ReactNode>): ReactNode
};

export default function RichText({children}: Props) {
return (
<div className="prose">
{children({
p: (chunks: ReactNode) => <p>{chunks}</p>,
b: (chunks: ReactNode) => <b className="font-semibold">{chunks}</b>,
i: (chunks: ReactNode) => <i className="italic">{chunks}</i>
})}
</div>
);
}
```

</Details>

Expand Down
5 changes: 2 additions & 3 deletions examples/example-app-router-playground/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
},
"AsyncComponent": {
"basic": "AsyncComponent",
"markup": "Markup with <b>{globalString}</b>",
"markup": "Markup with <important>bold content</important>",
"rich": "This is a <important>rich</important> text."
},
"Client": {
Expand All @@ -21,8 +21,7 @@
},
"Index": {
"description": "Das ist die Startseite.",
"globalDefaults": "<highlight>{globalString}</highlight>",
"rich": "Das ist <important>formatierter</important> Test.",
"rich": "Das ist <b>formatierter</b> Test.",
"title": "Start"
},
"JustIn": {
Expand Down
5 changes: 2 additions & 3 deletions examples/example-app-router-playground/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
},
"AsyncComponent": {
"basic": "AsyncComponent",
"markup": "Markup with <b>{globalString}</b>",
"markup": "Markup with <important>bold content</important>",
"rich": "This is a <important>rich</important> text."
},
"Client": {
Expand All @@ -21,8 +21,7 @@
},
"Index": {
"description": "This is the home page.",
"globalDefaults": "<highlight>{globalString}</highlight>",
"rich": "This is a <important>rich</important> text.",
"rich": "This is a <b>rich</b> text.",
"title": "Home"
},
"JustIn": {
Expand Down
5 changes: 2 additions & 3 deletions examples/example-app-router-playground/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
},
"AsyncComponent": {
"basic": "AsyncComponent",
"markup": "Markup with <b>{globalString}</b>",
"markup": "Markup with <important>bold content</important>",
"rich": "This is a <important>rich</important> text."
},
"Client": {
Expand All @@ -21,8 +21,7 @@
},
"Index": {
"description": "Esta es la página de inicio.",
"globalDefaults": "<highlight>{globalString}</highlight>",
"rich": "Este es un texto <important>importante</important>.",
"rich": "Este es un texto <b>importante</b>.",
"title": "Inicio"
},
"JustIn": {
Expand Down
3 changes: 1 addition & 2 deletions examples/example-app-router-playground/messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@
},
"Index": {
"description": "This is the home page (ja).",
"globalDefaults": "<highlight>{globalString}</highlight> (ja)",
"rich": "This is a <important>rich</important> text (ja).",
"rich": "This is a <b>rich</b> text (ja).",
"title": "Home (ja)"
},
"JustIn": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -27,14 +28,13 @@ export default function Index({searchParams}: Props) {
return (
<PageLayout title={t('title')}>
<p>{t('description')}</p>
<p data-testid="RichText">
{t.rich('rich', {important: (chunks) => <b>{chunks}</b>})}
</p>
<RichText data-testid="RichText">
{(tags) => t.rich('rich', tags)}
</RichText>
<p
dangerouslySetInnerHTML={{__html: t.raw('rich')}}
data-testid="RawText"
/>
<p data-testid="GlobalDefaults">{t.rich('globalDefaults')}</p>
{/* @ts-expect-error Purposefully trigger an error */}
<p data-testid="MissingMessage">{t('missing')}</p>
<p data-testid="CurrentTime">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ export default async function AsyncComponent() {
<div data-testid="AsyncComponent">
<p>{t('basic')}</p>
<p>{t.rich('rich', {important: (chunks) => <b>{chunks}</b>})}</p>
<p>{t.markup('markup', {b: (chunks) => `<b>${chunks}</b>`})}</p>
<p>
{t.markup('markup', {
important: (chunks) => `<b>${chunks}</b>`
})}
</p>
<p>{String(t.has('basic'))}</p>
</div>
);
Expand Down
18 changes: 18 additions & 0 deletions examples/example-app-router-playground/src/components/RichText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {ComponentProps, ReactNode} from 'react';

type Tag = 'b' | 'i';

type Props = {
children(tags: Record<Tag, (chunks: ReactNode) => ReactNode>): ReactNode;
} & Omit<ComponentProps<'p'>, 'children'>;

export default function RichText({children, ...rest}: Props) {
return (
<p {...rest}>
{children({
b: (chunks: ReactNode) => <b style={{fontWeight: 'bold'}}>{chunks}</b>,
i: (chunks: ReactNode) => <i style={{fontStyle: 'italic'}}>{chunks}</i>
})}
</p>
);
}
4 changes: 0 additions & 4 deletions examples/example-app-router-playground/src/i18n/request.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,6 @@ export default getRequestConfig(async ({requestLocale}) => {
now: now ? new Date(now) : undefined,
timeZone,
messages,
defaultTranslationValues: {
globalString: 'Global string',
highlight: (chunks) => <strong>{chunks}</strong>
},
formats,
onError(error) {
if (
Expand Down
16 changes: 5 additions & 11 deletions examples/example-app-router-playground/tests/main.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <b>rich</b> 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 <important>rich</important> text.'
'This is a <b style="font-weight:bold">rich</b> 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('<strong>Global string</strong>');
const element = page.getByTestId('RawText');
expect(await element.innerHTML()).toBe('This is a <b>rich</b> text.');
});

it('can use `getMessageFallback`', async ({page}) => {
Expand Down Expand Up @@ -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 <b>rich</b> text.');
element1.getByText('Markup with <b>Global string</b>');
element1.getByText('Markup with <b>bold content</b>');

page
.getByTestId('AsyncComponentWithoutNamespace')
Expand Down
5 changes: 4 additions & 1 deletion packages/use-intl/src/core/IntlConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ type IntlConfig<Messages = AbstractIntlMessages> = {
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;
};

Expand Down

0 comments on commit 9a7b17e

Please sign in to comment.