It works, but it's wasteful. It causes all translations to be loaded on every page. With lots of languages and lots of pages, it'll cause the downloaded JS size to significantly grow.
After trying several other ideas, I found a working solution:
As we currently use explicit id for every translated text, for texts that are page-specific, not shared, we changed translation ids to format [somenamespace]::[sometranslation]
. The separator is ::
but it could be actually any character or character sequence that doesn't otherwise exist in any existing translation id.
For example:
intl.formatMessage({
id: "some-translation-id",
defaultMessage: "some text"
})
became:
intl.formatMessage({
id: "mynamespace::some-translation-id",
defaultMessage: "some text"
})
and this:
<FormattedMessage
id="some-translation-id-2"
defaultMessage="another text"
/>
became:
<FormattedMessage
id="anothernamespace::some-translation-id-2"
defaultMessage="another text"
/>
Shared messages, that don't go to any namespace, don't have namespace name and separator prepended.
Note that moving translations to namespaces can be done gradually - messages not moved to a namespace so far will stay in shared messages.
After running compilation script (formatjs compile
), the compiled files (pl.json, en.json) contain all translations, including namespaced ones. Then, a custom-written Node.js script reads the compiled files, identifies what namespaces exist and what keys are in each, and writes files with namespace name prepended (or nothing prepended for shared messages):
pl.json
en.json
...
mynamespace-pl.json
mynamespace-en.json
...
anothernamespace-pl.json
anothernamespace-en.json
...
...
...
The [namespacename]-[languageid].json
files have identical format to compiled [languageid].json
, but contain files only from a given namespace.
Because useIntl
hook returns current locale and all available messages, it's possible to get the shared messages and locale from there, and pass them (with namespaced messages added) to another IntlProvider
. Then, every component inside the new IntlProvider
will use the new, larger translations set.
A component that provides namespaced translations to its children could look like that:
import translationsPL from "../locales/compiled/mynamespace-pl.json";
import translationsEN from "../locales/compiled/mynamespace-en.json";
// ... other languages ...
const MyNamespaceTranslationsProvider = (props) => {
const { children, translations } = props;
const { locale, messages: commonMessages } = useIntl(); // <- imports messages from the upper IntlProvider
const allMessages = useMemo(() => {
let namespacedMessages = translationsPL;
switch (locale) {
case "pl":
namespacedMessages = translationsPL;
break;
case "en":
namespacedMessages = translationsEN;
break;
// ... other languages ...
}
// Merge messages from the upper IntlProvider with locally imported one
return { ...commonMessages, ...namespacedMessages };
}, [locale, commonMessages]);
// Provide merged messages to the components inside
return (
<IntlProvider locale={locale} messages={allMessages}>
{children}
</IntlProvider>
);
};
Then, it can be used e.g. on a file in src/pages/pageName.tsx
:
import NamespacedTranslationsProvider from "../../components-v2/NamespacedTranslationsProvider";
// ... other imports
const SomePage = () => {
return (
<MyNamespaceTranslationsProvider>
{/* .... other content .... */}
</MyNamespaceTranslationsProvider>
);
};
export default SomePage;
How much it's going to help? It very much depends on size of translations on your site, how many languages you support, and how many messages can be moved to their namespaces.
In my case, moving content of several pages to namespaces (not all of them, we can move more texts) decreased _app.tsx
size by tens of kB.
See the article about dynamic import too - it's another thing that could reduce bundle size when using Next.js localization.
Go back to home page