Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamically load locale messages #261

Merged
merged 5 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 2 additions & 2 deletions app/.storybook/I18nStoryWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { StoryContext } from "@storybook/react";
import { NextIntlClientProvider } from "next-intl";
import React from "react";

import { defaultLocale, formats, getLocaleMessages } from "../src/i18n";
import { defaultLocale, formats } from "../src/i18n";

const I18nStoryWrapper = (
Story: React.ComponentType,
Expand All @@ -19,7 +19,7 @@ const I18nStoryWrapper = (
<NextIntlClientProvider
formats={formats}
locale={locale}
messages={getLocaleMessages(locale)}
messages={context.loaded.messages}
>
<Story />
</NextIntlClientProvider>
Expand Down
9 changes: 8 additions & 1 deletion app/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
* @file Setup the toolbar, styling, and global context for each Storybook story.
* @see https://storybook.js.org/docs/configure#configure-story-rendering
*/
import { Preview } from "@storybook/react";
import { Loader, Preview } from "@storybook/react";

import "../src/styles/styles.scss";

import { defaultLocale, locales } from "../src/i18n";
import { getMessagesWithFallbacks } from "../src/i18n/getMessagesWithFallbacks";
import I18nStoryWrapper from "./I18nStoryWrapper";

const parameters = {
Expand Down Expand Up @@ -35,7 +36,13 @@ const parameters = {
},
};

const i18nMessagesLoader: Loader = async (context) => {
const messages = await getMessagesWithFallbacks(context.globals.locale);
return { messages };
};

const preview: Preview = {
loaders: [i18nMessagesLoader],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This puts messages on context.loaded.messages, which is available to decorators and stories

decorators: [I18nStoryWrapper],
parameters,
globalTypes: {
Expand Down
38 changes: 38 additions & 0 deletions app/src/i18n/getMessagesWithFallbacks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { merge } from "lodash";
import { defaultLocale, Locale, locales } from "src/i18n";

interface LocaleFile {
messages: Messages;
}

async function importMessages(locale: Locale) {
const { messages } = (await import(`./messages/${locale}`)) as LocaleFile;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Is this the part you mentioned in the PR description about using a dynamic import to get all the content strings for the specified locale?

Newb question and probably a better way to ask this but does this have any ramifications for like.. the point in build / run time where content strings are brought in?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the part you mentioned in the PR description about using a dynamic import to get all the content strings for the specified locale?

Yep, this is the main part of the show. This is the dynamic import.

does this have any ramifications for like.. the point in build / run time where content strings are brought in?

My understanding is that the dynamic import happens at runtime. This avoids statically loading a bunch of content strings that aren't needed, which could impact memory usage.

return messages;
}

/**
* Get all messages for the given locale. If any translations are missing
* from the current locale, the missing key will fallback to the default locale
*/
export async function getMessagesWithFallbacks(
requestedLocale: string = defaultLocale
) {
const isValidLocale = locales.includes(requestedLocale as Locale); // https://github.com/microsoft/TypeScript/issues/26255
if (!isValidLocale) {
console.error(
"Unsupported locale was requested. Falling back to the default locale.",
{ locale: requestedLocale, defaultLocale }
);
requestedLocale = defaultLocale;
}

const targetLocale = requestedLocale as Locale;
let messages = await importMessages(targetLocale);

if (targetLocale !== defaultLocale) {
const fallbackMessages = await importMessages(defaultLocale);
messages = merge({}, fallbackMessages, messages);
}

return Promise.resolve(messages);
}
49 changes: 4 additions & 45 deletions app/src/i18n/index.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to rename this file to config.ts but it makes for a confusing diff. I'll rename before merging unless anyone objects.

Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { merge } from "lodash";

import { getRequestConfig } from "next-intl/server";

import { messages as enUs } from "./messages/en-US";
import { messages as esUs } from "./messages/es-US";
/**
* @file Shared i18n configuration for use across the server and client
*/
import type { getRequestConfig } from "next-intl/server";

type RequestConfig = Awaited<
ReturnType<Parameters<typeof getRequestConfig>[0]>
>;
export type Messages = RequestConfig["messages"];

/**
* List of languages supported by the application. Other tools (Storybook, tests) reference this.
Expand All @@ -18,17 +15,6 @@ export const locales = ["en-US", "es-US"] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = "en-US";

/**
* All messages for the application for each locale.
* Don't export this object!! Use `getLocaleMessages` instead,
* which handles fallbacks to the default locale when a locale
* is missing a translation.
*/
const _messages: { [locale in Locale]: Messages } = {
"en-US": enUs,
"es-US": esUs,
};

/**
* Define the default formatting for date, time, and numbers.
* @see https://next-intl-docs.vercel.app/docs/usage/configuration#formats
Expand All @@ -40,30 +26,3 @@ export const formats: RequestConfig["formats"] = {
},
},
};

/**
* Get the entire locale messages object for the given locale. If any
* translations are missing from the current locale, the missing key will
* fallback to the default locale
*/
export function getLocaleMessages(
requestedLocale: string = defaultLocale
): Messages {
if (requestedLocale in _messages === false) {
console.error(
"Unsupported locale was requested. Falling back to the default locale.",
{ locale: requestedLocale, defaultLocale }
);
requestedLocale = defaultLocale;
}

const targetLocale = requestedLocale as Locale;
let messages = _messages[targetLocale];

if (targetLocale !== defaultLocale) {
const fallbackMessages = _messages[defaultLocale];
messages = merge({}, fallbackMessages, messages);
}

return messages;
}
2 changes: 1 addition & 1 deletion app/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Layout from "../components/Layout";

import "../styles/styles.scss";

import { defaultLocale, formats, Messages } from "src/i18n";
import { defaultLocale, formats } from "src/i18n";

import { NextIntlClientProvider } from "next-intl";
import { useRouter } from "next/router";
Expand Down
10 changes: 5 additions & 5 deletions app/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { GetServerSideProps, NextPage } from "next";
import { getLocaleMessages } from "src/i18n";
import { getMessagesWithFallbacks } from "src/i18n/getMessagesWithFallbacks";

import { useTranslations } from "next-intl";
import Head from "next/head";
Expand Down Expand Up @@ -42,12 +42,12 @@ const Home: NextPage = () => {
};

// Change this to getStaticProps if you're not using server-side rendering
export const getServerSideProps: GetServerSideProps = ({ locale }) => {
return Promise.resolve({
export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
return {
props: {
messages: getLocaleMessages(locale),
messages: await getMessagesWithFallbacks(locale),
},
});
};
};

export default Home;
2 changes: 1 addition & 1 deletion app/src/types/i18n.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Use type safe message keys with `next-intl`
type Messages = typeof import("src/i18n/messages/en-US").default;
type Messages = typeof import("src/i18n/messages/en-US").messages;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was just wrong before

type IntlMessages = Messages;
5 changes: 3 additions & 2 deletions app/tests/react-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* @see https://testing-library.com/docs/react-testing-library/setup#custom-render
*/
import { render as _render, RenderOptions } from "@testing-library/react";
import { defaultLocale, formats, getLocaleMessages } from "src/i18n";
import { defaultLocale, formats } from "src/i18n";
import { messages } from "src/i18n/messages/en-US";

import { NextIntlClientProvider } from "next-intl";

Expand All @@ -17,7 +18,7 @@ const GlobalProviders = ({ children }: { children: React.ReactNode }) => {
return (
<NextIntlClientProvider
locale={defaultLocale}
messages={getLocaleMessages(defaultLocale)}
messages={messages}
formats={formats}
>
{children}
Expand Down
2 changes: 1 addition & 1 deletion docs/internationalization.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ Locale messages should only ever be loaded on the server-side, to avoid bloating

1. Add a language folder, using the same BCP47 language tag: `mkdir -p src/i18n/messages/<lang>`
1. Add a language file: `touch src/i18n/messages/<lang>/index.ts` and add the translated content. The JSON structure should be the same across languages. However, non-default languages can omit keys, in which case the default language will be used as a fallback.
1. Update [`i18n/index.ts`](../app/src/i18n/index.ts) to include the new language in the `_messages` object and `locales` array.
1. Update [`i18n/index.ts`](../app/src/i18n/index.ts) to include the new language in the `locales` array.