Skip to content

Commit

Permalink
fix: Improve error message when trying to render an array as a message (
Browse files Browse the repository at this point in the history
  • Loading branch information
amannn committed Apr 14, 2023
1 parent c7c9549 commit c6a4f7a
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 42 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
<br>
</h1>

> Internationalization for Next.js that gets out of your way.
> Internationalization (i18n) for Next.js that gets out of your way.
![Gzipped size](https://badgen.net/bundlephobia/minzip/next-intl) ![Tree shaking supported](https://badgen.net/bundlephobia/tree-shaking/next-intl) [<img src="https://img.shields.io/npm/dw/next-intl.svg" />](https://www.npmjs.com/package/next-intl)

<hr />

📣 [Support for Next.js 13 and the app directory is coming →](https://next-intl-docs.vercel.app/docs/next-13)
📣 [Support for Server Components in Next.js 13 is coming →](https://next-intl-docs.vercel.app/docs/next-13)

<hr />

Expand Down
37 changes: 25 additions & 12 deletions packages/use-intl/src/core/createBaseTranslator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,24 +181,37 @@ export default function createBaseTranslator<
);
}

const cacheKey = [namespace, key, message]
.filter((part) => part != null)
.join('.');
function joinPath(parts: Array<string | undefined>) {
return parts.filter((part) => part != null).join('.');
}

const cacheKey = joinPath([namespace, key, String(message)]);

let messageFormat;
if (cachedFormatsByLocale?.[locale]?.[cacheKey]) {
messageFormat = cachedFormatsByLocale?.[locale][cacheKey];
} else {
if (typeof message === 'object') {
return getFallbackFromErrorAndNotify(
key,
IntlErrorCode.INSUFFICIENT_PATH,
process.env.NODE_ENV !== 'production'
? `Insufficient path specified for \`${key}\` in \`${
namespace ? `\`${namespace}\`` : 'messages'
}\`.`
: undefined
);
let code, errorMessage;
if (Array.isArray(message)) {
code = IntlErrorCode.INVALID_MESSAGE;
if (process.env.NODE_ENV !== 'production') {
errorMessage = `Message at \`${joinPath([
namespace,
key
])}\` resolved to an array, but only strings are supported. See https://next-intl-docs.vercel.app/docs/usage/messages#arrays-of-messages`;
}
} else {
code = IntlErrorCode.INSUFFICIENT_PATH;
if (process.env.NODE_ENV !== 'production') {
errorMessage = `Message at \`${joinPath([
namespace,
key
])}\` resolved to an object, but only strings are supported. Use a \`.\` to retrieve nested messages. See https://next-intl-docs.vercel.app/docs/usage/messages#structuring-messages`;
}
}

return getFallbackFromErrorAndNotify(key, code, errorMessage);
}

try {
Expand Down
112 changes: 112 additions & 0 deletions packages/use-intl/test/react/useTranslations.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,16 @@ describe('t.raw', () => {
expect(container.innerHTML).toBe('<span>{"nested":{"object":true}}</span>');
});

it('can return arrays', () => {
const {container} = renderRawMessage(
{array: [1, '2', {three: true}]},
(message) => <span>{JSON.stringify(message)}</span>
);
expect(container.innerHTML).toBe(
'<span>{"array":[1,"2",{"three":true}]}</span>'
);
});

it('renders a fallback for unknown messages', () => {
const onError = jest.fn();

Expand Down Expand Up @@ -664,6 +674,108 @@ describe('error handling', () => {
'INVALID_KEY: Namespace keys can not contain the character "." as this is used to express nesting. Please remove it or replace it with another character.\n\nInvalid keys: a.b, c.d (at a.b), h.j (at f.g)'
);
});

it('shows an error when trying to render an object with `t`', () => {
const onError = jest.fn();

function Component() {
const t = useTranslations('Component');
return <>{t('object')}</>;
}

render(
<IntlProvider
locale="en"
messages={{Component: {object: {a: 'a'}}}}
onError={onError}
>
<Component />
</IntlProvider>
);

const error: IntlError = onError.mock.calls[0][0];
expect(error.code).toBe(IntlErrorCode.INSUFFICIENT_PATH);
expect(error.message).toBe(
'INSUFFICIENT_PATH: Message at `Component.object` resolved to an object, but only strings are supported. Use a `.` to retrieve nested messages. See https://next-intl-docs.vercel.app/docs/usage/messages#structuring-messages'
);
});

it('shows an error when trying to render an object with `t.rich`', () => {
const onError = jest.fn();

function Component() {
const t = useTranslations('Component');
return <>{t.rich('object')}</>;
}

render(
<IntlProvider
locale="en"
messages={{Component: {object: {a: 'a'}}}}
onError={onError}
>
<Component />
</IntlProvider>
);

const error: IntlError = onError.mock.calls[0][0];
expect(error.code).toBe(IntlErrorCode.INSUFFICIENT_PATH);
expect(error.message).toBe(
'INSUFFICIENT_PATH: Message at `Component.object` resolved to an object, but only strings are supported. Use a `.` to retrieve nested messages. See https://next-intl-docs.vercel.app/docs/usage/messages#structuring-messages'
);
});

it('shows an error when trying to render an array with `t`', () => {
const onError = jest.fn();

function Component() {
const t = useTranslations('Component');
return <>{t('array')}</>;
}

render(
<IntlProvider
locale="en"
// @ts-expect-error Arrays are not allowed
messages={{Component: {array: ['a', 'b']}}}
onError={onError}
>
<Component />
</IntlProvider>
);

const error: IntlError = onError.mock.calls[0][0];
expect(error.code).toBe(IntlErrorCode.INVALID_MESSAGE);
expect(error.message).toBe(
'INVALID_MESSAGE: Message at `Component.array` resolved to an array, but only strings are supported. See https://next-intl-docs.vercel.app/docs/usage/messages#arrays-of-messages'
);
});

it('shows an error when trying to render an array with `t.rich`', () => {
const onError = jest.fn();

function Component() {
const t = useTranslations('Component');
return <>{t.rich('array')}</>;
}

render(
<IntlProvider
locale="en"
// @ts-expect-error Arrays are not allowed
messages={{Component: {array: ['a', 'b']}}}
onError={onError}
>
<Component />
</IntlProvider>
);

const error: IntlError = onError.mock.calls[0][0];
expect(error.code).toBe(IntlErrorCode.INVALID_MESSAGE);
expect(error.message).toBe(
'INVALID_MESSAGE: Message at `Component.array` resolved to an array, but only strings are supported. See https://next-intl-docs.vercel.app/docs/usage/messages#arrays-of-messages'
);
});
});

describe('global formats', () => {
Expand Down
19 changes: 16 additions & 3 deletions packages/website/components/Callout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import {ReactNode} from 'react';

type Props = {
children: ReactNode;
type?: 'default' | 'warning';
className?: string;
emoji?: string;
title?: string;
type?: 'default' | 'warning';
};

const TypeToEmoji = {
default: '💡',
warning: '⚠️'
warning: '⚠️',
question: '🤔'
};

const classes = {
Expand All @@ -19,12 +21,16 @@ const classes = {
),
warning: cn(
'border-yellow-700/20 bg-yellow-50 text-yellow-900 dark:border-yellow-400/40 dark:bg-yellow-700/30 dark:text-white/90'
),
question: cn(
'border-sky-700/20 bg-sky-50 text-sky-800 dark:border-sky-400/40 dark:bg-sky-700/30 dark:text-white/90'
)
};

export default function Callout({
children,
type = 'default',
title,
className,
emoji = TypeToEmoji[type]
}: Props) {
Expand All @@ -45,7 +51,14 @@ export default function Callout({
>
{emoji}
</div>
<div className="nx-w-full nx-min-w-0 nx-leading-7">{children}</div>
<div className="nx-w-full nx-min-w-0 nx-leading-7">
{title && (
<p className="mb-1">
<strong>{title}</strong>
</p>
)}
<div>{children}</div>
</div>
</div>
);
}
4 changes: 2 additions & 2 deletions packages/website/pages/docs/next-13/index.mdx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Callout from 'components/Callout';

# Next.js 13
# Next.js 13 with the App Router

With the introduction of [the App Router](https://beta.nextjs.org/docs/routing/fundamentals), Next.js 13 provides a new level of flexibility for React apps.
With the introduction of [the App Router](https://beta.nextjs.org/docs/routing/fundamentals) (i.e. the `app` directory), Next.js 13 provides a new level of flexibility for React apps and new possibilities when it comes to internationalization (i18n).

There are two major changes that affect `next-intl`:

Expand Down
6 changes: 2 additions & 4 deletions packages/website/pages/docs/next-13/server-components.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -268,11 +268,9 @@ function Expandable({title, children}) {
}
```

<Callout emoji="">
<Callout emoji="" title="Benefits">

**Benefits:**

<ol className="ml-4 mt-1 list-decimal">
<ol className="ml-4 list-decimal">
<li>
Your messages never leave the server and don't need to be serialized for the
client side.
Expand Down
28 changes: 17 additions & 11 deletions packages/website/pages/docs/usage/messages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -210,22 +210,22 @@ If you need to render a list of messages, the recommended approach is to map an

```js filename="en.json"
{
"Features": {
"trust": "Built by experts",
"Benefits": {
"zero-config": "Works with zero config",
"customizable": "Easy to customize",
"fast": "Blazingly fast"
}
}
```

```js filename="Features.js"
```js filename="Benefits.js"
import {useTranslations} from 'next-intl';

function Features() {
const t = useTranslations('Features');
function Benefits() {
const t = useTranslations('Benefits');
return (
<ul>
{['trust', 'customizable', 'fast'].map((key) => (
{['zero-config', 'customizable', 'fast'].map((key) => (
<li key={key}>{t(key)}</li>
))}
</ul>
Expand All @@ -237,17 +237,17 @@ If the number of items varies between locales, you can solve this by using [rich

```js filename="en.json"
{
"Features": {
"items": "<item>Built by experts</item><item>Easy to customize</item><item>Blazingly fast</item>"
"Benefits": {
"items": "<item>Works with zero config</item><item>Easy to customize</item><item>Blazingly fast</item>"
}
}
```

```js filename="Features.js"
```js filename="Benefits.js"
import {useTranslations} from 'next-intl';

function Features() {
const t = useTranslations('Features');
function Benefits() {
const t = useTranslations('Benefits');
return (
<ul>
{t.rich('items', {
Expand All @@ -257,3 +257,9 @@ function Features() {
);
}
```

<Callout type="question" title='"Why can‘t I just use arrays in my messages?"'>
The advantage of this approach over supporting arrays in messages is that this
way you can use the formatting capabilities, e.g. to interpolate values into
messages.
</Callout>
6 changes: 2 additions & 4 deletions packages/website/pages/docs/usage/typescript.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,9 @@ declare interface IntlMessages extends Messages {}

You can freely define the interface, but if you have your messages available locally, it can be helpful to automatically create the interface based on a messages sample by importing it.

<Callout type="warning">
<Callout type="warning" title="If you're encountering problems, please double check that:">

**If you're encountering problems, please double check that:**

<ol className="mt-1 ml-4 list-decimal">
<ol className="ml-4 list-decimal">
<li>Your interface is called `IntlMessages`.</li>
<li>You're using TypeScript version 4 or later.</li>
<li>The path of your `import` is correct.</li>
Expand Down
4 changes: 2 additions & 2 deletions packages/website/pages/index.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Internationalization for Next.js
title: Internationalization (i18n) for Next.js
---

import Callout from 'components/Callout';
Expand All @@ -19,7 +19,7 @@ import Hero from 'components/Hero';
description="Support multiple languages, with your app code becoming simpler instead of more complex."
getStarted="Get started"
viewOnGithub="View on GitHub"
rscAnnouncement="Support for Next.js 13 and the App Router is coming →"
rscAnnouncement="Support for Server Components in Next.js 13 is coming →"
/>
<PartnerBanner intro="Presented by" />

Expand Down
4 changes: 2 additions & 2 deletions packages/website/theme.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export default {

<meta content="next-intl" name="og:title" />
<meta
content="Internationalization for Next.js that gets out of your way."
content="Internationalization (i18n) for Next.js that gets out of your way."
name="og:description"
/>
<meta content="summary_large_image" name="twitter:card" />
Expand All @@ -155,7 +155,7 @@ export default {
name="og:image"
/>
<meta
content="Internationalization for Next.js that gets out of your way."
content="Internationalization (i18n) for Next.js that gets out of your way."
name="description"
/>
</>
Expand Down

2 comments on commit c6a4f7a

@vercel
Copy link

@vercel vercel bot commented on c6a4f7a Apr 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-intl-docs – ./packages/website

next-intl-docs-git-main-amann.vercel.app
next-intl-docs-amann.vercel.app
next-intl-docs.vercel.app

@vercel
Copy link

@vercel vercel bot commented on c6a4f7a Apr 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-intl-examples-next-13 – ./packages/example-next-13

next-intl-examples-next-13-git-main-amann.vercel.app
next-intl-examples-next-13.vercel.app
next-intl-examples-next-13-amann.vercel.app

Please sign in to comment.