Skip to content

Commit

Permalink
feat: Propose new APIs from using i18n outside of components
Browse files Browse the repository at this point in the history
  • Loading branch information
amannn committed Jun 21, 2023
1 parent 808acab commit 6020d99
Show file tree
Hide file tree
Showing 12 changed files with 259 additions and 52 deletions.
4 changes: 2 additions & 2 deletions packages/next-intl/src/react-server/useLocale.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type {useLocale as useLocaleType} from 'use-intl';
import getLocale from '../server/getLocale';
import getLocaleFromHeader from '../server/getLocaleFromHeader';

export default function useLocale(
// eslint-disable-next-line no-empty-pattern
...[]: Parameters<typeof useLocaleType>
): ReturnType<typeof useLocaleType> {
return getLocale();
return getLocaleFromHeader();
}
13 changes: 8 additions & 5 deletions packages/next-intl/src/server/getConfig.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import {cache} from 'react';
import getInitializedConfig from 'use-intl/dist/src/react/getInitializedConfig';
import createRequestConfig from '../server/createRequestConfig';
import getLocale from './getLocale';
import getLocaleFromHeader from './getLocaleFromHeader';

// Make sure `now` is consistent across the request in case none was configured
const getDefaultNow = cache(() => new Date());

const receiveRuntimeConfig = cache(
async (locale: string, getConfig?: typeof createRequestConfig) => {
Expand All @@ -11,14 +14,14 @@ const receiveRuntimeConfig = cache(
}
return {
...result,
// Make sure `now` is consistent across the request in case none was configured
now: result?.now || new Date()
now: result?.now || getDefaultNow()
};
}
);

const getConfig = cache(async () => {
const locale = getLocale();
const getConfig = cache(async (locale?: string) => {
if (!locale) locale = getLocaleFromHeader();

const runtimeConfig = await receiveRuntimeConfig(locale, createRequestConfig);
const opts = {...runtimeConfig, locale};
return getInitializedConfig(opts);
Expand Down
32 changes: 29 additions & 3 deletions packages/next-intl/src/server/getFormatter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,35 @@ import {cache} from 'react';
import {createFormatter} from 'use-intl/dist/src/core';
import getConfig from './getConfig';

const getFormatter = cache(async () => {
const config = await getConfig();
return createFormatter(config);
type Opts = Parameters<typeof createFormatter>[0];

let hasWarned = false;

/**
* Returns a formatter based on the given locale.
*
* The formatter automatically receives the request config, but
* you can override it by passing in additional options.
*/
const getFormatter = cache(async (opts?: Opts) => {
if (!opts?.locale && !hasWarned) {
hasWarned = true;
console.warn(`
Calling \`getFormatter\` without a locale is deprecated, please update the call:
// app/[locale]/layout.tsx
export async function generateMetadata({locale}) {
const t = await getFormatter({locale});
// ...
}
Learn more: https://next-intl-docs.vercel.app/docs/next-13/server-components#using-internationalization-outside-of-components
`);
}

const config = await getConfig(opts?.locale);
return createFormatter({...config, ...opts});
});

export default getFormatter;
6 changes: 5 additions & 1 deletion packages/next-intl/src/server/getIntl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ const getIntl = cache(async () => {
if (!hasWarned) {
hasWarned = true;
console.warn(
'`getIntl()` is deprecated and will be removed in the next major version. Please switch to `getFormatter()`.'
`
\`getIntl()\` is deprecated and will be removed in the next major version. Please switch to \`getFormatter()\`.
Learn more: https://next-intl-docs.vercel.app/docs/next-13/server-components#using-internationalization-outside-of-components
`
);
}

Expand Down
51 changes: 16 additions & 35 deletions packages/next-intl/src/server/getLocale.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,21 @@
import {cookies, headers} from 'next/headers';
import {cache} from 'react';
import {COOKIE_LOCALE_NAME, HEADER_LOCALE_NAME} from '../shared/constants';
import getLocaleFromHeader from './getLocaleFromHeader';

const getLocale = cache(() => {
let locale;
let hasWarned = false;

try {
// A header is only set when we're changing the locale,
// otherwise we reuse an existing one from the cookie.
const requestHeaders = headers();
if (requestHeaders.has(HEADER_LOCALE_NAME)) {
locale = requestHeaders.get(HEADER_LOCALE_NAME);
} else {
locale = cookies().get(COOKIE_LOCALE_NAME)?.value;
}
} catch (error) {
if (
error instanceof Error &&
(error as any).digest === 'DYNAMIC_SERVER_USAGE'
) {
throw new Error(
'Usage of next-intl APIs in Server Components is currently only available for dynamic rendering (i.e. no `generateStaticParams`).\n\nSupport for static rendering is under consideration, please refer to the roadmap: https://next-intl-docs.vercel.app/docs/next-13/server-components#roadmap',
{cause: error}
);
} else {
throw error;
}
}
export default function getLocale() {
if (!hasWarned) {
console.warn(`
\`getLocale\` is deprecated. Please use the \`locale\` parameter from Next.js instead:
if (!locale) {
throw new Error(
'Unable to find `next-intl` locale, have you configured the middleware?`'
);
}
// app/[locale]/layout.tsx
export async function generateMetadata({locale}) {
// Use \`locale\` here
}
return locale;
});
Learn more: https://next-intl-docs.vercel.app/docs/next-13/server-components#using-internationalization-outside-of-components
`);
hasWarned = true;
}

export default getLocale;
return getLocaleFromHeader();
}
40 changes: 40 additions & 0 deletions packages/next-intl/src/server/getLocaleFromHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {cookies, headers} from 'next/headers';
import {cache} from 'react';
import {COOKIE_LOCALE_NAME, HEADER_LOCALE_NAME} from '../shared/constants';

const getLocaleFromHeader = cache(() => {
let locale;

try {
// A header is only set when we're changing the locale,
// otherwise we reuse an existing one from the cookie.
const requestHeaders = headers();
if (requestHeaders.has(HEADER_LOCALE_NAME)) {
locale = requestHeaders.get(HEADER_LOCALE_NAME);
} else {
locale = cookies().get(COOKIE_LOCALE_NAME)?.value;
}
} catch (error) {
if (
error instanceof Error &&
(error as any).digest === 'DYNAMIC_SERVER_USAGE'
) {
throw new Error(
'Usage of next-intl APIs in Server Components is currently only available for dynamic rendering (i.e. no `generateStaticParams`).\n\nSupport for static rendering is under consideration, please refer to the roadmap: https://next-intl-docs.vercel.app/docs/next-13/server-components#roadmap',
{cause: error}
);
} else {
throw error;
}
}

if (!locale) {
throw new Error(
'Unable to find `next-intl` locale, have you configured the middleware?`'
);
}

return locale;
});

export default getLocaleFromHeader;
22 changes: 20 additions & 2 deletions packages/next-intl/src/server/getNow.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
import {cache} from 'react';
import getConfig from './getConfig';

const getNow = cache(async () => {
const config = await getConfig();
let hasWarned = false;

const getNow = cache(async (opts?: {locale: string}) => {
if (!opts?.locale && !hasWarned) {
hasWarned = true;
console.warn(`
Calling \`getNow\` without a locale is deprecated. Please update the call:
// app/[locale]/layout.tsx
export async function generateMetadata({locale}) {
const t = await getNow({locale});
// ...
}
Learn more: https://next-intl-docs.vercel.app/docs/next-13/server-components#using-internationalization-outside-of-components
`);
}

const config = await getConfig(opts?.locale);
return config.now;
});

Expand Down
22 changes: 20 additions & 2 deletions packages/next-intl/src/server/getTimeZone.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
import {cache} from 'react';
import getConfig from './getConfig';

const getTimeZone = cache(async () => {
const config = await getConfig();
let hasWarned = false;

const getTimeZone = cache(async (opts?: {locale: string}) => {
if (!opts?.locale && !hasWarned) {
hasWarned = true;
console.warn(`
Calling \`getTimeZone\` without a locale is deprecated. Please update the call:
// app/[locale]/layout.tsx
export async function generateMetadata({locale}) {
const t = await getTimeZone({locale});
// ...
}
Learn more: https://next-intl-docs.vercel.app/docs/next-13/server-components#using-internationalization-outside-of-components
`);
}

const config = await getConfig(opts?.locale);
return config.timeZone;
});

Expand Down
11 changes: 11 additions & 0 deletions packages/next-intl/src/server/getTranslations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import NestedKeyOf from 'use-intl/dist/src/core/utils/NestedKeyOf';
import NestedValueOf from 'use-intl/dist/src/core/utils/NestedValueOf';
import getConfig from './getConfig';

let hasWarned = false;

async function getTranslationsImpl<
NestedKey extends NamespaceKeys<
IntlMessages,
Expand Down Expand Up @@ -80,6 +82,15 @@ Promise<{
key: TargetKey
): any;
}> {
if (!hasWarned) {
console.warn(`
\`getTranslations\` is deprecated, please switch to \`getTranslator\`.
Learn more: https://next-intl-docs.vercel.app/docs/next-13/server-components#using-internationalization-outside-of-components
`);
hasWarned = true;
}

const config = await getConfig();

const messagesOrError = getMessagesOrError({
Expand Down
105 changes: 105 additions & 0 deletions packages/next-intl/src/server/getTranslator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/* eslint-disable import/default */

import {cache} from 'react';
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';
import NestedKeyOf from 'use-intl/dist/src/core/utils/NestedKeyOf';
import NestedValueOf from 'use-intl/dist/src/core/utils/NestedValueOf';
import getConfig from './getConfig';

async function getTranslatorImpl<
NestedKey extends NamespaceKeys<
IntlMessages,
NestedKeyOf<IntlMessages>
> = never
>(
opts: {namespace?: NestedKey} & Omit<
Parameters<typeof createBaseTranslator>[0],
'cachedFormatsByLocale' | ' messagesOrError' | 'namespace'
>
): // 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?: CoreRichTranslationValues,
formats?: Partial<Formats>
): string;

// `raw`
raw<
TargetKey extends MessageKeys<
NestedValueOf<
{'!': IntlMessages},
[NestedKey] extends [never] ? '!' : `!.${NestedKey}`
>,
NestedKeyOf<
NestedValueOf<
{'!': IntlMessages},
[NestedKey] extends [never] ? '!' : `!.${NestedKey}`
>
>
>
>(
key: TargetKey
): any;
}> {
const config = await getConfig();

const messagesOrError = getMessagesOrError({
messages: config.messages as any,
namespace: opts.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({
...config,
...opts,
messagesOrError
});
}

export default cache(getTranslatorImpl);
1 change: 1 addition & 0 deletions packages/next-intl/src/server/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,6 @@ export {default as getLocale} from './getLocale';
export {default as getNow} from './getNow';
export {default as getTimeZone} from './getTimeZone';
export {default as getTranslations} from './getTranslations';
export {default as getTranslator} from './getTranslator';

export {default as redirect} from './redirect';
4 changes: 2 additions & 2 deletions packages/next-intl/src/server/redirect.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import baseRedirect from '../shared/redirect';
import getLocale from './getLocale';
import getLocaleFromHeader from './getLocaleFromHeader';

export default function redirect(pathname: string) {
const locale = getLocale();
const locale = getLocaleFromHeader();
return baseRedirect(pathname, locale);
}

0 comments on commit 6020d99

Please sign in to comment.