Skip to content

Commit ed37131

Browse files
authored
Use next-intl for internationalization, replacing i18next (#260)
## Ticket Part of #66 ## Changes - Replace all I18next dependencies with [`next-intl`](https://next-intl-docs.vercel.app/) - Add new `tests/test-utils` file that would be used for rendering and querying a React tree in tests, rather than directly importing from `@testing-library/react`. This is so that the i18n content is provided to the component being tested. ## Context for reviewers This is one of the main pieces of the larger migration effort from the Pages router to the App Router (#66). To reduce the size of that PR, I've broken out the i18n migration work into this PR. Next.js App Router doesn't support internationalized routing or locale detection, like Pages router, and `next-intl` makes it easy to restore that functionality via middleware.
1 parent eeb1a0d commit ed37131

35 files changed

+627
-1097
lines changed

Diff for: app/.eslintrc.js

+17-14
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module.exports = {
33
extends: [
44
"eslint:recommended",
55
"plugin:storybook/recommended",
6+
"plugin:you-dont-need-lodash-underscore/compatible",
67
// Disable ESLint code formatting rules which conflict with Prettier
78
"prettier",
89
// `next` should be extended last according to their docs
@@ -14,35 +15,37 @@ module.exports = {
1415
// dependencies to work in standalone mode. It may be overkill for most projects at
1516
// Nava which aren't image heavy.
1617
"@next/next/no-img-element": "off",
17-
"no-restricted-imports": [
18-
"error",
19-
{
20-
paths: [
21-
{
22-
message:
23-
'Import from "next-i18next" instead of "react-i18next" so server-side translations work.',
24-
name: "react-i18next",
25-
importNames: ["useTranslation", "Trans"],
26-
},
27-
],
28-
},
29-
],
3018
},
3119
// Additional lint rules. These get layered onto the top-level rules.
3220
overrides: [
3321
// Lint config specific to Test files
3422
{
35-
files: ["tests/**"],
23+
files: ["tests/**/?(*.)+(spec|test).[jt]s?(x)"],
3624
plugins: ["jest"],
3725
extends: [
3826
"plugin:jest/recommended",
3927
"plugin:jest-dom/recommended",
4028
"plugin:testing-library/react",
4129
],
30+
rules: {
31+
"no-restricted-imports": [
32+
"error",
33+
{
34+
paths: [
35+
{
36+
message:
37+
'Import from "tests/react-utils" instead so that translations work.',
38+
name: "@testing-library/react",
39+
},
40+
],
41+
},
42+
],
43+
},
4244
},
4345
// Lint config specific to TypeScript files
4446
{
4547
files: "**/*.+(ts|tsx)",
48+
excludedFiles: [".storybook/*.ts?(x)"],
4649
parserOptions: {
4750
// These paths need defined to support rules that require type information
4851
tsconfigRootDir: __dirname,

Diff for: app/.prettierrc.js

-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ module.exports = {
1515
"<BUILTIN_MODULES>",
1616
"<THIRD_PARTY_MODULES>",
1717
"", // blank line
18-
"i18next",
1918
"^next[/-](.*)$",
2019
"^react$",
2120
"uswds",

Diff for: app/.storybook/I18nStoryWrapper.tsx

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* @file Storybook decorator, enabling internationalization for each story.
3+
* @see https://storybook.js.org/docs/writing-stories/decorators
4+
*/
5+
import { StoryContext } from "@storybook/react";
6+
7+
import { NextIntlClientProvider } from "next-intl";
8+
import React from "react";
9+
10+
import { defaultLocale, formats, getLocaleMessages } from "../src/i18n";
11+
12+
const I18nStoryWrapper = (
13+
Story: React.ComponentType,
14+
context: StoryContext
15+
) => {
16+
const locale = context.globals.locale ?? defaultLocale;
17+
18+
return (
19+
<NextIntlClientProvider
20+
formats={formats}
21+
locale={locale}
22+
messages={getLocaleMessages(locale)}
23+
>
24+
<Story />
25+
</NextIntlClientProvider>
26+
);
27+
};
28+
29+
export default I18nStoryWrapper;

Diff for: app/.storybook/i18next.js

-22
This file was deleted.

Diff for: app/.storybook/main.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function blockSearchEnginesInHead(head) {
2525
*/
2626
const config = {
2727
stories: ["../stories/**/*.stories.@(mdx|js|jsx|ts|tsx)"],
28-
addons: ["@storybook/addon-essentials", "storybook-react-i18next"],
28+
addons: ["@storybook/addon-essentials"],
2929
framework: {
3030
name: "@storybook/nextjs",
3131
options: {

Diff for: app/.storybook/preview.js renamed to app/.storybook/preview.tsx

+18-15
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
// @ts-check
2-
// Apply global styling to our stories
1+
/**
2+
* @file Setup the toolbar, styling, and global context for each Storybook story.
3+
* @see https://storybook.js.org/docs/configure#configure-story-rendering
4+
*/
5+
import { Preview } from "@storybook/react";
6+
37
import "../src/styles/styles.scss";
48

5-
// Import i18next config.
6-
import i18n from "./i18next.js";
9+
import { defaultLocale, locales } from "../src/i18n";
10+
import I18nStoryWrapper from "./I18nStoryWrapper";
711

812
const parameters = {
913
actions: { argTypesRegex: "^on[A-Z].*" },
@@ -13,8 +17,6 @@ const parameters = {
1317
date: /Date$/,
1418
},
1519
},
16-
// Configure i18next and locale/dropdown options.
17-
i18n,
1820
options: {
1921
storySort: {
2022
method: "alphabetical",
@@ -33,16 +35,17 @@ const parameters = {
3335
},
3436
};
3537

36-
/**
37-
* @type {import("@storybook/react").Preview}
38-
*/
39-
const preview = {
38+
const preview: Preview = {
39+
decorators: [I18nStoryWrapper],
4040
parameters,
41-
globals: {
42-
locale: "en-US",
43-
locales: {
44-
"en-US": "English",
45-
"es-US": "Español",
41+
globalTypes: {
42+
locale: {
43+
description: "Active language",
44+
defaultValue: defaultLocale,
45+
toolbar: {
46+
icon: "globe",
47+
items: locales,
48+
},
4649
},
4750
},
4851
};

Diff for: app/next-i18next.config.d.ts

-6
This file was deleted.

Diff for: app/next-i18next.config.js

-71
This file was deleted.

Diff for: app/next.config.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// @ts-check
2-
const { i18n } = require("./next-i18next.config");
2+
const withNextIntl = require("next-intl/plugin")("./src/i18n/index.ts");
33
const sassOptions = require("./scripts/sassOptions");
44

55
/**
@@ -15,20 +15,20 @@ const appSassOptions = sassOptions(basePath);
1515
/** @type {import('next').NextConfig} */
1616
const nextConfig = {
1717
basePath,
18-
i18n,
18+
i18n: {
19+
locales: ["en-US", "es-US"],
20+
defaultLocale: "en-US",
21+
},
1922
reactStrictMode: true,
2023
// Output only the necessary files for a deployment, excluding irrelevant node_modules
2124
// https://nextjs.org/docs/app/api-reference/next-config-js/output
2225
output: "standalone",
2326
sassOptions: appSassOptions,
2427
// Continue to support older browsers (ES5)
2528
transpilePackages: [
26-
// https://github.com/i18next/i18next/issues/1948
27-
"i18next",
28-
"react-i18next",
2929
// https://github.com/trussworks/react-uswds/issues/2605
3030
"@trussworks/react-uswds",
3131
],
3232
};
3333

34-
module.exports = nextConfig;
34+
module.exports = withNextIntl(nextConfig);

0 commit comments

Comments
 (0)