Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,24 @@ t('richText', {
<a href={t('attributeUrl')}>Link</a>
```

## Raw messages

Messages are always parsed and therefore e.g. for rich text you need to supply the necessary tags. If you want to avoid the parsing, e.g. because you have raw HTML stored in a message, there's a separate API for this:

```js
{
"content": "<h1>Headline<h1><p>This is raw HTML</p>"
}
```

```js
<div dangerouslySetInnerHTML={{__html: t.raw('content')}} />
```

**Important**: Note that you should sanitize content that you pass to `dangerouslySetInnerHTML` to avoid XSS attacks.

The value of a raw message can be any valid JSON value: strings, booleans, objects and arrays.

## Numbers

### `formatNumber`
Expand Down
78 changes: 52 additions & 26 deletions packages/use-intl/src/useTranslations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
ReactElement,
ReactNode,
ReactNodeArray,
useCallback,
useMemo,
useRef
} from 'react';
Expand Down Expand Up @@ -124,23 +123,27 @@ export default function useTranslations(namespace?: string) {
}
}, [allMessages, namespace, onError]);

const translate = useCallback(
(
const translate = useMemo(() => {
function getFallbackFromError(
key: string,
code: IntlErrorCode,
message?: string
) {
const error = new IntlError(code, message);
onError(error);
return getMessageFallback({error, key, namespace});
}

function translateFn(
/** Use a dot to indicate a level of nesting (e.g. `namespace.nestedLabel`). */
key: string,
/** Key value pairs for values to interpolate into the message. */
values?: {rawMessage?: boolean} & TranslationValues,
values?: TranslationValues,
/** Provide custom formats for numbers, dates and times. */
formats?: Partial<Formats>
): string | ReactElement | ReactNodeArray => {
): string | ReactElement | ReactNodeArray {
const cachedFormatsByLocale = cachedFormatsByLocaleRef.current;

function getFallbackFromError(code: IntlErrorCode, message?: string) {
const error = new IntlError(code, message);
onError(error);
return getMessageFallback({error, key, namespace});
}

if (messagesOrError instanceof IntlError) {
// We have already warned about this during render
return getMessageFallback({
Expand All @@ -160,24 +163,22 @@ export default function useTranslations(namespace?: string) {
message = resolvePath(messages, key, namespace);
} catch (error) {
return getFallbackFromError(
key,
IntlErrorCode.MISSING_MESSAGE,
error.message
);
}

if (typeof message === 'object') {
return getFallbackFromError(
key,
IntlErrorCode.INSUFFICIENT_PATH,
__DEV__
? `Insufficient path specified for \`${key}\` in \`${namespace}\`.`
: undefined
);
}

if (values?.rawMessage === true) {
return message;
}

try {
messageFormat = new IntlMessageFormat(
message,
Expand All @@ -189,6 +190,7 @@ export default function useTranslations(namespace?: string) {
);
} catch (error) {
return getFallbackFromError(
key,
IntlErrorCode.INVALID_MESSAGE,
error.message
);
Expand Down Expand Up @@ -222,21 +224,45 @@ export default function useTranslations(namespace?: string) {
: String(formattedMessage);
} catch (error) {
return getFallbackFromError(
key,
IntlErrorCode.FORMATTING_ERROR,
error.message
);
}
},
[
getMessageFallback,
globalFormats,
locale,
messagesOrError,
namespace,
onError,
timeZone
]
);
}

translateFn.raw = (key: string): any => {
if (messagesOrError instanceof IntlError) {
// We have already warned about this during render
return getMessageFallback({
error: messagesOrError,
key,
namespace
});
}
const messages = messagesOrError;

try {
return resolvePath(messages, key, namespace);
} catch (error) {
return getFallbackFromError(
key,
IntlErrorCode.MISSING_MESSAGE,
error.message
);
}
};

return translateFn;
}, [
getMessageFallback,
globalFormats,
locale,
messagesOrError,
namespace,
onError,
timeZone
]);

return translate;
}
83 changes: 57 additions & 26 deletions packages/use-intl/test/useTranslations.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,32 +122,6 @@ it('handles rich text', () => {
);
});

it('can return raw messages without processing them', () => {
function Component() {
const t = useTranslations();
return (
<span
dangerouslySetInnerHTML={{
__html: String(t('message', {rawMessage: true}))
}}
/>
);
}

const {container} = render(
<IntlProvider
locale="en"
messages={{message: '<a href="/test">Test</a><p>{hello}</p>'}}
>
<Component />
</IntlProvider>
);

expect(container.innerHTML).toBe(
'<span><a href="/test">Test</a><p>{hello}</p></span>'
);
});

it('handles nested rich text', () => {
const {container} = renderMessage(
'This is <bold><italic>very</italic> important</bold>',
Expand Down Expand Up @@ -283,6 +257,63 @@ it('has a stable reference', () => {
screen.getByText('2');
});

describe('t.raw', () => {
function renderRawMessage(
message: any,
callback: (message: string) => ReactNode
) {
function Component() {
const t = useTranslations();
return <>{callback(t.raw('message'))}</>;
}

return render(
<IntlProvider locale="en" messages={{message}}>
<Component />
</IntlProvider>
);
}

it('can return raw messages without processing them', () => {
const {
container
} = renderRawMessage(
'<a href="/test">Test</a><p>{hello}</p>',
(message) => <span dangerouslySetInnerHTML={{__html: message}} />
);

expect(container.innerHTML).toBe(
'<span><a href="/test">Test</a><p>{hello}</p></span>'
);
});

it('can return objects', () => {
const {container} = renderRawMessage(
{nested: {object: true}},
(message) => <span>{JSON.stringify(message)}</span>
);
expect(container.innerHTML).toBe('<span>{"nested":{"object":true}}</span>');
});

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

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

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

expect(onError).toHaveBeenCalled();
screen.getByText('foo');
});
});

describe('error handling', () => {
it('allows to configure a fallback', () => {
const onError = jest.fn();
Expand Down