Skip to content

Commit

Permalink
fix: Lazy init message formatter for improved tree shaking in case `u…
Browse files Browse the repository at this point in the history
…seTranslations` is only used in Server Components (#1279)

Fixes a regression of #1193.

In case you're only using `useTranslations` in Server Components, this
will no longer bundle `intl-messageformat` on the client side (even if
you're using other functionality like `useFormatter` there).
  • Loading branch information
amannn authored Aug 23, 2024
1 parent 7f423be commit 9f1725c
Show file tree
Hide file tree
Showing 21 changed files with 803 additions and 258 deletions.
6 changes: 3 additions & 3 deletions docs/components/CodeSnippets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ function buildOutput() {
<span style={{color: 'var(--shiki-color-text)'}}> kB</span>
<span style={{color: 'var(--shiki-color-text)'}}>{' '}</span>
<span style={{color: 'var(--shiki-token-string-expression)'}}>
87.6 kB
89.7 kB
</span>
</span>
<span className="line">
Expand All @@ -344,7 +344,7 @@ function buildOutput() {
<span style={{color: 'var(--shiki-color-text)'}}> B</span>
<span style={{color: 'var(--shiki-color-text)'}}>{' '}</span>
<span style={{color: 'var(--shiki-token-string-expression)'}}>
86.2 kB
89.3 kB
</span>
</span>
<span className="line">
Expand All @@ -354,7 +354,7 @@ function buildOutput() {
<span style={{color: 'var(--shiki-color-text)'}}> kB</span>
<span style={{color: 'var(--shiki-color-text)'}}>{' '}</span>
<span style={{color: 'var(--shiki-token-string-expression)'}}>
89.3 kB
91.1 kB
</span>
</span>
<span className="line"> </span>
Expand Down
48 changes: 48 additions & 0 deletions packages/next-intl/.size-limit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type {SizeLimitConfig} from 'size-limit';

const config: SizeLimitConfig = [
{
path: 'dist/production/index.react-client.js',
limit: '14.084 KB'
},
{
path: 'dist/production/index.react-server.js',
limit: '14.665 KB'
},
{
path: 'dist/production/navigation.react-client.js',
limit: '3.155 KB'
},
{
path: 'dist/production/navigation.react-server.js',
limit: '15.975 KB'
},
{
path: 'dist/production/server.react-client.js',
limit: '1 KB'
},
{
path: 'dist/production/server.react-server.js',
limit: '13.975 KB'
},
{
path: 'dist/production/middleware.js',
limit: '9.535 KB'
},
{
path: 'dist/production/routing.js',
limit: '0 KB'
},
{
path: 'dist/esm/index.react-client.js',
import: '*',
limit: '14.265 kB'
},
{
path: 'dist/esm/index.react-client.js',
import: '{NextIntlClientProvider}',
limit: '1.425 kB'
}
];

module.exports = config;
40 changes: 3 additions & 37 deletions packages/next-intl/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
"devDependencies": {
"@arethetypeswrong/cli": "^0.15.3",
"@edge-runtime/vm": "^3.2.0",
"@size-limit/preset-big-lib": "^8.2.6",
"@size-limit/preset-big-lib": "^11.1.4",
"@testing-library/react": "^16.0.0",
"@types/negotiator": "^0.6.3",
"@types/node": "^20.14.5",
Expand All @@ -112,43 +112,9 @@
"react-dom": "^18.3.1",
"rollup": "^4.18.0",
"rollup-plugin-preserve-directives": "0.4.0",
"size-limit": "^8.2.6",
"size-limit": "^11.1.4",
"typescript": "^5.5.3",
"vitest": "^2.0.2"
},
"prettier": "../../.prettierrc.json",
"size-limit": [
{
"path": "dist/production/index.react-client.js",
"limit": "16.055 KB"
},
{
"path": "dist/production/index.react-server.js",
"limit": "16.875 KB"
},
{
"path": "dist/production/navigation.react-client.js",
"limit": "3.55 KB"
},
{
"path": "dist/production/navigation.react-server.js",
"limit": "18.355 KB"
},
{
"path": "dist/production/server.react-client.js",
"limit": "1 KB"
},
{
"path": "dist/production/server.react-server.js",
"limit": "16.025 KB"
},
{
"path": "dist/production/middleware.js",
"limit": "11.515 KB"
},
{
"path": "dist/production/routing.js",
"limit": "0 KB"
}
]
"prettier": "../../.prettierrc.json"
}
21 changes: 7 additions & 14 deletions packages/next-intl/src/react-server/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React from 'react';
import {describe, expect, vi, it} from 'vitest';
import {getTranslations} from '../server.react-server';
import {renderToStream} from './utils';
import {renderToStream} from './testUtils';
import {
createTranslator,
useFormatter,
useLocale,
useMessages,
useNow,
useTranslations
useTranslations,
_createCache
} from '.';

vi.mock('react');
Expand All @@ -32,10 +32,9 @@ vi.mock('../../src/server/react-server/RequestLocale', () => ({

vi.mock('use-intl/core', async (importActual) => {
const actual: any = await importActual();
const {createTranslator: actualCreateTranslator} = actual;
return {
...actual,
createTranslator: vi.fn(actualCreateTranslator)
_createCache: vi.fn(actual._createCache)
};
});

Expand Down Expand Up @@ -73,8 +72,7 @@ describe('performance', () => {
});

it('shares a formatter cache between `useTranslations` and `getTranslations`', async () => {
// First invocation
// (simulate React rendering)
// First invocation (simulate React rendering)
try {
useTranslations('Component');
} catch (promiseOrError) {
Expand All @@ -89,12 +87,7 @@ describe('performance', () => {
// Second invocation with a different namespace
await getTranslations('Component2');

// Verify the cached formatters are shared
expect(vi.mocked(createTranslator).mock.calls[0][0]._formatters).toBe(
vi.mocked(createTranslator).mock.calls[1][0]._formatters
);
expect(vi.mocked(createTranslator).mock.calls.length).toBe(2);

vi.mocked(createTranslator).mockReset();
expect(vi.mocked(_createCache).mock.calls.length).toBe(1);
vi.mocked(_createCache).mockReset();
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, {cache} from 'react';
import {describe, expect, it, vi, beforeEach} from 'vitest';
import {renderToStream} from './utils';
import {renderToStream} from './testUtils';
import {createTranslator, useTranslations} from '.';

vi.mock('../../src/server/react-server/createRequestConfig', () => ({
Expand Down
17 changes: 13 additions & 4 deletions packages/next-intl/src/server/react-server/getConfig.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import {cache} from 'react';
import {initializeConfig, IntlConfig, _createFormatters} from 'use-intl/core';
import {
initializeConfig,
IntlConfig,
_createIntlFormatters,
_createCache
} from 'use-intl/core';
import {getRequestLocale} from './RequestLocale';
import createRequestConfig from './createRequestConfig';

Expand Down Expand Up @@ -52,22 +57,26 @@ async function receiveRuntimeConfigImpl(
}
const receiveRuntimeConfig = cache(receiveRuntimeConfigImpl);

const getFormatters = cache(_createFormatters);
const getFormatters = cache(_createIntlFormatters);
const getCache = cache(_createCache);

async function getConfigImpl(localeOverride?: string): Promise<
IntlConfig & {
getMessageFallback: NonNullable<IntlConfig['getMessageFallback']>;
now: NonNullable<IntlConfig['now']>;
onError: NonNullable<IntlConfig['onError']>;
timeZone: NonNullable<IntlConfig['timeZone']>;
_formatters: ReturnType<typeof _createFormatters>;
_formatters: ReturnType<typeof _createIntlFormatters>;
}
> {
const runtimeConfig = await receiveRuntimeConfig(
createRequestConfig,
localeOverride
);
return {...initializeConfig(runtimeConfig), _formatters: getFormatters()};
return {
...initializeConfig(runtimeConfig),
_formatters: getFormatters(getCache())
};
}
const getConfig = cache(getConfigImpl);
export default getConfig;
24 changes: 24 additions & 0 deletions packages/use-intl/.size-limit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type {SizeLimitConfig} from 'size-limit';

const config: SizeLimitConfig = [
{
name: './ (ESM)',
import: '*',
path: 'dist/esm/index.js',
limit: '14.065 kB'
},
{
name: './ (no useTranslations, ESM)',
path: 'dist/esm/index.js',
import:
'{IntlProvider, useLocale, useNow, useTimeZone, useMessages, useFormatter}',
limit: '2.865 kB'
},
{
name: './ (CJS)',
path: 'dist/production/index.js',
limit: '15.65 kB'
}
];

module.exports = config;
12 changes: 3 additions & 9 deletions packages/use-intl/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.15.3",
"@size-limit/preset-big-lib": "^8.2.6",
"@size-limit/preset-big-lib": "^11.1.4",
"@testing-library/react": "^16.0.0",
"@types/node": "^20.14.5",
"@types/react": "^18.3.3",
Expand All @@ -83,16 +83,10 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"rollup": "^4.18.0",
"size-limit": "^8.2.6",
"size-limit": "^11.1.4",
"tinyspy": "^3.0.0",
"typescript": "^5.5.3",
"vitest": "^2.0.2"
},
"prettier": "../../.prettierrc.json",
"size-limit": [
{
"path": "dist/production/index.js",
"limit": "15.545 kB"
}
]
"prettier": "../../.prettierrc.json"
}
6 changes: 3 additions & 3 deletions packages/use-intl/src/core/IntlConfig.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type AbstractIntlMessages from './AbstractIntlMessages';
import Formats from './Formats';
import IntlError from './IntlError';
import TimeZone from './TimeZone';
import type Formats from './Formats';
import type IntlError from './IntlError';
import type TimeZone from './TimeZone';
import type {RichTranslationValues} from './TranslationValues';

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/use-intl/src/core/NumberFormatOptions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Formats} from 'intl-messageformat';
import type {Formats} from 'intl-messageformat';

// Use the already bundled version of `NumberFormat` from `@formatjs/ecma402-abstract`
// that comes with `intl-messageformat`
Expand Down
34 changes: 33 additions & 1 deletion packages/use-intl/src/core/createBaseTranslator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,36 @@ import TranslationValues, {
} from './TranslationValues';
import convertFormatsToIntlMessageFormat from './convertFormatsToIntlMessageFormat';
import {defaultGetMessageFallback, defaultOnError} from './defaults';
import {Formatters} from './formatters';
import {
Formatters,
IntlCache,
IntlFormatters,
memoFn,
MessageFormatter
} from './formatters';
import joinPath from './joinPath';
import MessageKeys from './utils/MessageKeys';
import NestedKeyOf from './utils/NestedKeyOf';
import NestedValueOf from './utils/NestedValueOf';

// Placed here for improved tree shaking. Somehow when this is placed in
// `formatters.tsx`, then it can't be shaken off from `next-intl`.
function createMessageFormatter(
cache: IntlCache,
intlFormatters: IntlFormatters
): MessageFormatter {
const getMessageFormat = memoFn(
(...args: ConstructorParameters<typeof IntlMessageFormat>) =>
new IntlMessageFormat(args[0], args[1], args[2], {
formatters: intlFormatters,
...args[3]
}),
cache.message
);

return getMessageFormat;
}

function resolvePath(
locale: string,
messages: AbstractIntlMessages | undefined,
Expand Down Expand Up @@ -125,6 +149,7 @@ function getMessagesOrError<Messages extends AbstractIntlMessages>(
}

export type CreateBaseTranslatorProps<Messages> = InitializedIntlConfig & {
cache: IntlCache;
formatters: Formatters;
defaultTranslationValues?: RichTranslationValues;
namespace?: string;
Expand Down Expand Up @@ -169,6 +194,7 @@ function createBaseTranslatorImpl<
Messages extends AbstractIntlMessages,
NestedKey extends NestedKeyOf<Messages>
>({
cache,
defaultTranslationValues,
formats: globalFormats,
formatters,
Expand Down Expand Up @@ -247,6 +273,12 @@ function createBaseTranslatorImpl<
const plainMessage = getPlainMessage(message as string, values);
if (plainMessage) return plainMessage;

// Lazy init the message formatter for better tree
// shaking in case message formatting is not used.
if (!formatters.getMessageFormat) {
formatters.getMessageFormat = createMessageFormatter(cache, formatters);
}

try {
messageFormat = formatters.getMessageFormat(
message,
Expand Down
Loading

0 comments on commit 9f1725c

Please sign in to comment.