diff --git a/app/.storybook/I18nStoryWrapper.tsx b/app/.storybook/I18nStoryWrapper.tsx
index d158ea19..bf067374 100644
--- a/app/.storybook/I18nStoryWrapper.tsx
+++ b/app/.storybook/I18nStoryWrapper.tsx
@@ -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, timeZone } from "../src/i18n/config";
const I18nStoryWrapper = (
Story: React.ComponentType,
@@ -18,8 +18,9 @@ const I18nStoryWrapper = (
return (
diff --git a/app/.storybook/preview.tsx b/app/.storybook/preview.tsx
index c8b6da25..923ada0b 100644
--- a/app/.storybook/preview.tsx
+++ b/app/.storybook/preview.tsx
@@ -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 { defaultLocale, locales } from "../src/i18n/config";
+import { getMessagesWithFallbacks } from "../src/i18n/getMessagesWithFallbacks";
import I18nStoryWrapper from "./I18nStoryWrapper";
const parameters = {
@@ -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],
decorators: [I18nStoryWrapper],
parameters,
globalTypes: {
diff --git a/app/README.md b/app/README.md
index 18eb5aa2..aac1d93b 100644
--- a/app/README.md
+++ b/app/README.md
@@ -13,6 +13,9 @@
│ └── locales # Internationalized content
├── src # Source code
│ ├── components # Reusable UI components
+│ ├── i18n # Internationalization
+│ │ ├── config.ts # Supported locales, timezone, and formatters
+│ │ └── messages # Translated strings
│ ├── pages # Page routes and data fetching
│ │ ├── api # API routes (optional)
│ │ └── _app.tsx # Global entry point
diff --git a/app/next.config.js b/app/next.config.js
index d0e0b044..5d07eb07 100644
--- a/app/next.config.js
+++ b/app/next.config.js
@@ -1,5 +1,5 @@
// @ts-check
-const withNextIntl = require("next-intl/plugin")("./src/i18n/index.ts");
+const withNextIntl = require("next-intl/plugin")("./src/i18n/config.ts");
const sassOptions = require("./scripts/sassOptions");
/**
diff --git a/app/src/i18n/config.ts b/app/src/i18n/config.ts
new file mode 100644
index 00000000..d3ff6bcd
--- /dev/null
+++ b/app/src/i18n/config.ts
@@ -0,0 +1,35 @@
+/**
+ * @file Shared i18n configuration for use across the server and client
+ */
+import type { getRequestConfig } from "next-intl/server";
+
+type RequestConfig = Awaited<
+ ReturnType[0]>
+>;
+
+/**
+ * List of languages supported by the application. Other tools (Storybook, tests) reference this.
+ * These must be BCP47 language tags: https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags
+ */
+export const locales = ["en-US", "es-US"] as const;
+export type Locale = (typeof locales)[number];
+export const defaultLocale: Locale = "en-US";
+
+/**
+ * Specifying a time zone affects the rendering of dates and times.
+ * When not defined, the time zone of the server runtime is used.
+ * @see https://next-intl-docs.vercel.app/docs/usage/configuration#time-zone
+ */
+export const timeZone: RequestConfig["timeZone"] = "America/New_York";
+
+/**
+ * Define the default formatting for date, time, and numbers.
+ * @see https://next-intl-docs.vercel.app/docs/usage/configuration#formats
+ */
+export const formats: RequestConfig["formats"] = {
+ number: {
+ currency: {
+ currency: "USD",
+ },
+ },
+};
diff --git a/app/src/i18n/getMessagesWithFallbacks.ts b/app/src/i18n/getMessagesWithFallbacks.ts
new file mode 100644
index 00000000..bd2bdb8e
--- /dev/null
+++ b/app/src/i18n/getMessagesWithFallbacks.ts
@@ -0,0 +1,38 @@
+import { merge } from "lodash";
+import { defaultLocale, Locale, locales } from "src/i18n/config";
+
+interface LocaleFile {
+ messages: Messages;
+}
+
+async function importMessages(locale: Locale) {
+ const { messages } = (await import(`./messages/${locale}`)) as LocaleFile;
+ 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 messages;
+}
diff --git a/app/src/i18n/index.ts b/app/src/i18n/index.ts
deleted file mode 100644
index 70a6f178..00000000
--- a/app/src/i18n/index.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-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";
-
-type RequestConfig = Awaited<
- ReturnType[0]>
->;
-export type Messages = RequestConfig["messages"];
-
-/**
- * List of languages supported by the application. Other tools (Storybook, tests) reference this.
- * These must be BCP47 language tags: https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags
- */
-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
- */
-export const formats: RequestConfig["formats"] = {
- number: {
- currency: {
- currency: "USD",
- },
- },
-};
-
-/**
- * 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;
-}
diff --git a/app/src/pages/_app.tsx b/app/src/pages/_app.tsx
index 314ebaff..dcd6c79a 100644
--- a/app/src/pages/_app.tsx
+++ b/app/src/pages/_app.tsx
@@ -5,7 +5,7 @@ import Layout from "../components/Layout";
import "../styles/styles.scss";
-import { defaultLocale, formats, Messages } from "src/i18n";
+import { defaultLocale, formats, timeZone } from "src/i18n/config";
import { NextIntlClientProvider } from "next-intl";
import { useRouter } from "next/router";
@@ -23,6 +23,7 @@ function MyApp({ Component, pageProps }: AppProps<{ messages: Messages }>) {
diff --git a/app/src/pages/index.tsx b/app/src/pages/index.tsx
index 5d57d105..b954ea5a 100644
--- a/app/src/pages/index.tsx
+++ b/app/src/pages/index.tsx
@@ -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";
@@ -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;
diff --git a/app/src/types/i18n.d.ts b/app/src/types/i18n.d.ts
index b4e6411e..733cc674 100644
--- a/app/src/types/i18n.d.ts
+++ b/app/src/types/i18n.d.ts
@@ -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;
type IntlMessages = Messages;
diff --git a/app/tests/react-utils.tsx b/app/tests/react-utils.tsx
index a9b39690..484b1a42 100644
--- a/app/tests/react-utils.tsx
+++ b/app/tests/react-utils.tsx
@@ -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, timeZone } from "src/i18n/config";
+import { messages } from "src/i18n/messages/en-US";
import { NextIntlClientProvider } from "next-intl";
@@ -16,9 +17,10 @@ import { NextIntlClientProvider } from "next-intl";
const GlobalProviders = ({ children }: { children: React.ReactNode }) => {
return (
{children}
diff --git a/docs/internationalization.md b/docs/internationalization.md
index fd174fba..bdf3bbf6 100644
--- a/docs/internationalization.md
+++ b/docs/internationalization.md
@@ -1,7 +1,7 @@
# Internationalization (i18n)
- [next-intl](https://next-intl-docs.vercel.app) is used for internationalization. Toggling between languages is done by changing the URL's path prefix (e.g. `/about` ➡️ `/es-US/about`).
-- Configuration and helpers are located in [`i18n/index.ts`](../app/src/i18n/index.ts). For the most part, you shouldn't need to edit this file unless adding a new formatter or new language.
+- Configuration and helpers are located in [`i18n/config.ts`](../app/src/i18n/config.ts). For the most part, you shouldn't need to edit this file unless adding a new formatter or new language.
## Managing translations
@@ -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/`
1. Add a language file: `touch src/i18n/messages//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/config.ts`](../app/src/i18n/config.ts) to include the new language in the `locales` array.