Skip to content

Commit

Permalink
fix(RSC): Return string from (await getTranslator()).rich
Browse files Browse the repository at this point in the history
  • Loading branch information
amannn committed Jul 11, 2023
1 parent 22fe5d9 commit 0caa848
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 40 deletions.
2 changes: 1 addition & 1 deletion packages/next-intl/dts.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* global module */

/**
* @type {import('dts-cli').DtsConfig}
* @type {import('dts-cli').DtsOptions}
*/
module.exports = {
rollup(config) {
Expand Down
1 change: 1 addition & 0 deletions packages/next-intl/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"devDependencies": {
"@edge-runtime/vm": "^3.0.3",
"@size-limit/preset-big-lib": "^8.2.6",
"@testing-library/react": "^13.0.0",
"@types/negotiator": "^0.6.1",
Expand Down
128 changes: 128 additions & 0 deletions packages/next-intl/src/react-server/getBaseTranslator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/* eslint-disable import/default -- False positives */

import {ReactElement, ReactNodeArray, cache} from 'react';
import type Formats from 'use-intl/dist/src/core/Formats';
import type TranslationValues from 'use-intl/dist/src/core/TranslationValues';
import type {RichTranslationValues} from 'use-intl/dist/src/core/TranslationValues';
import createBaseTranslator, {
getMessagesOrError
} from 'use-intl/dist/src/core/createBaseTranslator';
import MessageKeys from 'use-intl/dist/src/core/utils/MessageKeys';
import NamespaceKeys from 'use-intl/dist/src/core/utils/NamespaceKeys';
import NestedKeyOf from 'use-intl/dist/src/core/utils/NestedKeyOf';
import NestedValueOf from 'use-intl/dist/src/core/utils/NestedValueOf';
import getConfig from '../server/getConfig';

let hasWarned = false;

async function getTranslatorImpl<
NestedKey extends NamespaceKeys<
IntlMessages,
NestedKeyOf<IntlMessages>
> = never
>(
locale:
| string
| {
namespace?: NestedKey;
locale: string;
},
namespace?: NestedKey
): // Explicitly defining the return type is necessary as TypeScript would get it wrong
Promise<{
// Default invocation
<
TargetKey extends MessageKeys<
NestedValueOf<
{'!': IntlMessages},
[NestedKey] extends [never] ? '!' : `!.${NestedKey}`
>,
NestedKeyOf<
NestedValueOf<
{'!': IntlMessages},
[NestedKey] extends [never] ? '!' : `!.${NestedKey}`
>
>
>
>(
key: TargetKey,
values?: TranslationValues,
formats?: Partial<Formats>
): string;

// `rich`
rich<
TargetKey extends MessageKeys<
NestedValueOf<
{'!': IntlMessages},
[NestedKey] extends [never] ? '!' : `!.${NestedKey}`
>,
NestedKeyOf<
NestedValueOf<
{'!': IntlMessages},
[NestedKey] extends [never] ? '!' : `!.${NestedKey}`
>
>
>
>(
key: TargetKey,
values?: RichTranslationValues,
formats?: Partial<Formats>
): string | ReactElement | ReactNodeArray;

// `raw`
raw<
TargetKey extends MessageKeys<
NestedValueOf<
{'!': IntlMessages},
[NestedKey] extends [never] ? '!' : `!.${NestedKey}`
>,
NestedKeyOf<
NestedValueOf<
{'!': IntlMessages},
[NestedKey] extends [never] ? '!' : `!.${NestedKey}`
>
>
>
>(
key: TargetKey
): any;
}> {
if (typeof locale === 'object') {
const opts = locale;
namespace = opts.namespace;
locale = opts.locale;
if (!hasWarned) {
console.warn(
`
DEPRECATION WARNING: Calling \`getTranslator\` with an object argument is deprecated, please update your call site accordingly.
// Previously
getTranslator({locale: 'en', namespace: 'About'});
// Now
getTranslator('en', 'About');
See also https://next-intl-docs.vercel.app/docs/environments/metadata-route-handlers
`
);
hasWarned = true;
}
}

const config = await getConfig(locale);

const messagesOrError = getMessagesOrError({
messages: config.messages as any,
namespace,
onError: config.onError
});

return createBaseTranslator({
...config,
namespace,
messagesOrError
});
}

export default cache(getTranslatorImpl);
11 changes: 6 additions & 5 deletions packages/next-intl/src/react-server/useTranslations.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import type {useTranslations as useTranslationsType} from 'use-intl';
import getTranslator from '../server/getTranslator';
import getBaseTranslator from './getBaseTranslator';
import useHook from './useHook';
import useLocale from './useLocale';

export default function useTranslations(
...[namespace]: Parameters<typeof useTranslationsType>
): ReturnType<typeof useTranslationsType> {
const locale = useLocale();
const result = useHook('useTranslations', getTranslator(locale, namespace));
const result = useHook(
'useTranslations',
getBaseTranslator(locale, namespace)
);

// The types are slightly off here and indicate that rich text formatting
// doesn't integrate with React - this is not the case.
return result as any;
return result;
}
18 changes: 3 additions & 15 deletions packages/next-intl/src/server/getTranslations.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
/* eslint-disable import/default */

import {cache} from 'react';
import {createTranslator} from 'use-intl/dist/src/core';
import type Formats from 'use-intl/dist/src/core/Formats';
import type TranslationValues from 'use-intl/dist/src/core/TranslationValues';
import createBaseTranslator, {
getMessagesOrError
} from 'use-intl/dist/src/core/createBaseTranslator';
import {CoreRichTranslationValues} from 'use-intl/dist/src/core/createTranslatorImpl';
import MessageKeys from 'use-intl/dist/src/core/utils/MessageKeys';
import NamespaceKeys from 'use-intl/dist/src/core/utils/NamespaceKeys';
Expand Down Expand Up @@ -95,20 +93,10 @@ Learn more: https://next-intl-docs.vercel.app/docs/environments/metadata-route-h
const locale = getLocaleFromHeader();
const config = await getConfig(locale);

const messagesOrError = getMessagesOrError({
messages: config.messages as any,
namespace,
onError: config.onError
});

// We allow to resolve rich text formatting here, but the types forbid it when
// `getTranslations` is used directly. Supporting rich text is important when
// the react-server implementation calls into this function.
// @ts-ignore
return createBaseTranslator({
return createTranslator({
...config,
namespace,
messagesOrError
messages: config.messages || {}
});
}

Expand Down
18 changes: 3 additions & 15 deletions packages/next-intl/src/server/getTranslator.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
/* eslint-disable import/default */

import {cache} from 'react';
import {createTranslator} from 'use-intl/dist/src/core';
import type Formats from 'use-intl/dist/src/core/Formats';
import type TranslationValues from 'use-intl/dist/src/core/TranslationValues';
import createBaseTranslator, {
getMessagesOrError
} from 'use-intl/dist/src/core/createBaseTranslator';
import {CoreRichTranslationValues} from 'use-intl/dist/src/core/createTranslatorImpl';
import MessageKeys from 'use-intl/dist/src/core/utils/MessageKeys';
import NamespaceKeys from 'use-intl/dist/src/core/utils/NamespaceKeys';
Expand Down Expand Up @@ -112,20 +110,10 @@ See also https://next-intl-docs.vercel.app/docs/environments/metadata-route-hand

const config = await getConfig(locale);

const messagesOrError = getMessagesOrError({
messages: config.messages as any,
namespace,
onError: config.onError
});

// We allow to resolve rich text formatting here, but the types forbid it when
// `getTranslations` is used directly. Supporting rich text is important when
// the react-server implementation calls into this function.
// @ts-ignore
return createBaseTranslator({
return createTranslator({
...config,
namespace,
messagesOrError
messages: config.messages || {}
});
}

Expand Down
84 changes: 84 additions & 0 deletions packages/next-intl/test/server/getTranslator.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// @vitest-environment edge-runtime

import {it, vi, expect, describe} from 'vitest';
import {
getTranslator,
getMessages,
getFormatter,
getNow,
getTimeZone
} from '../../src/server';

vi.mock('next-intl/config', () => ({
default: async () =>
((await vi.importActual('../../src/server')) as any).getRequestConfig({
locale: 'en',
now: new Date('2020-01-01T00:00:00.000Z'),
timeZone: 'Europe/London',
messages: {
About: {
interpolation: 'Hello {name}',
rich: '<link>{name}</link>'
}
}
})
}));

vi.mock('react', async (importOriginal) => {
const React = (await importOriginal()) as typeof import('react');
return {
...React,
cache(fn: (...args: Array<unknown>) => unknown) {
return (...args: Array<unknown>) => fn(...args);
}
};
});

describe('getTranslator', () => {
it('can interpolate variables', async () => {
const t = await getTranslator('en', 'About');
expect(t('interpolation', {name: 'Jane'})).toBe('Hello Jane');
});

it('renders rich text to a string', async () => {
const t = await getTranslator('en', 'About');
expect(
t.rich('rich', {
name: 'Example',
link: (chunks) => `<a href="https://example.com">${chunks}</a>`
})
).toBe('<a href="https://example.com">Example</a>');
});

it('renders raw text to a string', async () => {
const t = await getTranslator('en', 'About');
expect(t.raw('rich')).toBe('<link>{name}</link>');
});
});

describe('getFormatter', () => {
it('can format a date', async () => {
const format = await getFormatter('en');
expect(format.dateTime(new Date('2020-01-01T00:00:00.000Z'))).toBe(
'1/1/2020'
);
});
});

describe('getNow', () => {
it('returns the current time', async () => {
expect((await getNow('en')).toISOString()).toBe('2020-01-01T00:00:00.000Z');
});
});

describe('getMessages', () => {
it('returns the messages', async () => {
expect(await getMessages('en')).toHaveProperty('About');
});
});

describe('getTimeZone', () => {
it('returns the time zone', async () => {
expect(await getTimeZone('en')).toBe('Europe/London');
});
});
2 changes: 1 addition & 1 deletion packages/use-intl/dts.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* global module */

/**
* @type {import('dts-cli').DtsConfig}
* @type {import('dts-cli').DtsOptions}
*/
module.exports = {
rollup(config) {
Expand Down
22 changes: 19 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0caa848

Please sign in to comment.