Skip to content

Commit

Permalink
Use next-intl for internationalization, replacing i18next (#260)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
sawyerh authored Dec 6, 2023
1 parent eeb1a0d commit ed37131
Show file tree
Hide file tree
Showing 35 changed files with 627 additions and 1,097 deletions.
31 changes: 17 additions & 14 deletions app/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module.exports = {
extends: [
"eslint:recommended",
"plugin:storybook/recommended",
"plugin:you-dont-need-lodash-underscore/compatible",
// Disable ESLint code formatting rules which conflict with Prettier
"prettier",
// `next` should be extended last according to their docs
Expand All @@ -14,35 +15,37 @@ module.exports = {
// dependencies to work in standalone mode. It may be overkill for most projects at
// Nava which aren't image heavy.
"@next/next/no-img-element": "off",
"no-restricted-imports": [
"error",
{
paths: [
{
message:
'Import from "next-i18next" instead of "react-i18next" so server-side translations work.',
name: "react-i18next",
importNames: ["useTranslation", "Trans"],
},
],
},
],
},
// Additional lint rules. These get layered onto the top-level rules.
overrides: [
// Lint config specific to Test files
{
files: ["tests/**"],
files: ["tests/**/?(*.)+(spec|test).[jt]s?(x)"],
plugins: ["jest"],
extends: [
"plugin:jest/recommended",
"plugin:jest-dom/recommended",
"plugin:testing-library/react",
],
rules: {
"no-restricted-imports": [
"error",
{
paths: [
{
message:
'Import from "tests/react-utils" instead so that translations work.',
name: "@testing-library/react",
},
],
},
],
},
},
// Lint config specific to TypeScript files
{
files: "**/*.+(ts|tsx)",
excludedFiles: [".storybook/*.ts?(x)"],
parserOptions: {
// These paths need defined to support rules that require type information
tsconfigRootDir: __dirname,
Expand Down
1 change: 0 additions & 1 deletion app/.prettierrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ module.exports = {
"<BUILTIN_MODULES>",
"<THIRD_PARTY_MODULES>",
"", // blank line
"i18next",
"^next[/-](.*)$",
"^react$",
"uswds",
Expand Down
29 changes: 29 additions & 0 deletions app/.storybook/I18nStoryWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @file Storybook decorator, enabling internationalization for each story.
* @see https://storybook.js.org/docs/writing-stories/decorators
*/
import { StoryContext } from "@storybook/react";

import { NextIntlClientProvider } from "next-intl";
import React from "react";

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

const I18nStoryWrapper = (
Story: React.ComponentType,
context: StoryContext
) => {
const locale = context.globals.locale ?? defaultLocale;

return (
<NextIntlClientProvider
formats={formats}
locale={locale}
messages={getLocaleMessages(locale)}
>
<Story />
</NextIntlClientProvider>
);
};

export default I18nStoryWrapper;
22 changes: 0 additions & 22 deletions app/.storybook/i18next.js

This file was deleted.

2 changes: 1 addition & 1 deletion app/.storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function blockSearchEnginesInHead(head) {
*/
const config = {
stories: ["../stories/**/*.stories.@(mdx|js|jsx|ts|tsx)"],
addons: ["@storybook/addon-essentials", "storybook-react-i18next"],
addons: ["@storybook/addon-essentials"],
framework: {
name: "@storybook/nextjs",
options: {
Expand Down
33 changes: 18 additions & 15 deletions app/.storybook/preview.js → app/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
// @ts-check
// Apply global styling to our stories
/**
* @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 "../src/styles/styles.scss";

// Import i18next config.
import i18n from "./i18next.js";
import { defaultLocale, locales } from "../src/i18n";
import I18nStoryWrapper from "./I18nStoryWrapper";

const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
Expand All @@ -13,8 +17,6 @@ const parameters = {
date: /Date$/,
},
},
// Configure i18next and locale/dropdown options.
i18n,
options: {
storySort: {
method: "alphabetical",
Expand All @@ -33,16 +35,17 @@ const parameters = {
},
};

/**
* @type {import("@storybook/react").Preview}
*/
const preview = {
const preview: Preview = {
decorators: [I18nStoryWrapper],
parameters,
globals: {
locale: "en-US",
locales: {
"en-US": "English",
"es-US": "Español",
globalTypes: {
locale: {
description: "Active language",
defaultValue: defaultLocale,
toolbar: {
icon: "globe",
items: locales,
},
},
},
};
Expand Down
6 changes: 0 additions & 6 deletions app/next-i18next.config.d.ts

This file was deleted.

71 changes: 0 additions & 71 deletions app/next-i18next.config.js

This file was deleted.

12 changes: 6 additions & 6 deletions app/next.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @ts-check
const { i18n } = require("./next-i18next.config");
const withNextIntl = require("next-intl/plugin")("./src/i18n/index.ts");
const sassOptions = require("./scripts/sassOptions");

/**
Expand All @@ -15,20 +15,20 @@ const appSassOptions = sassOptions(basePath);
/** @type {import('next').NextConfig} */
const nextConfig = {
basePath,
i18n,
i18n: {
locales: ["en-US", "es-US"],
defaultLocale: "en-US",
},
reactStrictMode: true,
// Output only the necessary files for a deployment, excluding irrelevant node_modules
// https://nextjs.org/docs/app/api-reference/next-config-js/output
output: "standalone",
sassOptions: appSassOptions,
// Continue to support older browsers (ES5)
transpilePackages: [
// https://github.com/i18next/i18next/issues/1948
"i18next",
"react-i18next",
// https://github.com/trussworks/react-uswds/issues/2605
"@trussworks/react-uswds",
],
};

module.exports = nextConfig;
module.exports = withNextIntl(nextConfig);
Loading

0 comments on commit ed37131

Please sign in to comment.