Skip to content
This repository has been archived by the owner on Oct 16, 2024. It is now read-only.

Commit

Permalink
Merge pull request #10 from LedgerHQ/support/i18n
Browse files Browse the repository at this point in the history
Support/i18n
  • Loading branch information
sshmaxime authored Sep 21, 2023
2 parents f11fb0f + 510614b commit 3ecb827
Show file tree
Hide file tree
Showing 19 changed files with 881 additions and 626 deletions.
2 changes: 1 addition & 1 deletion __tests__/unit/Home.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @jest-environment jsdom
*/

import Home from "@/app/page";
import Home from "@/app/[locale]/page";
import { render, screen } from "@/TestTools";

describe("Home", () => {
Expand Down
6 changes: 5 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const nextJest = require("next/jest");

const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig");

const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: "./",
});

// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const customJestConfig = {
setupFilesAfterEnv: ["<rootDir>/jest.setup.js", "<rootDir>/tools/setEnvVars.js"],
testEnvironment: "jest-environment-jsdom",
collectCoverageFrom: ["src/**/*.{ts,tsx}", "!src/layout.tsx"],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths),
};

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
Expand Down
9 changes: 9 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ beforeEach(() => {
mockUseUserId.mockReturnValue(mockUseUserIdData);
});

/**
* @dev temporary fix due to https://github.com/QuiiBz/next-international/issues/178
* This code completely override anything related to i18n in our test.
*/
jest.mock("@/i18n/client", () => ({
I18nProvider: ({ children }) => <>{children}</>,
useI18n: () => ({ t: jest.fn(), changeLocale: jest.fn(), locale: "en" }),
}));

// MSW integration

// Establish API mocking before all tests.
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"eslint": "8.49.0",
"eslint-config-next": "13.4.19",
"next": "13.4.19",
"next-international": "^1.0.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-redux": "^8.1.2",
Expand All @@ -46,6 +47,7 @@
"jest-environment-jsdom": "29.6.4",
"jest-styled-components": "^7.1.1",
"msw": "^1.3.0",
"prettier": "3.0.3"
"prettier": "3.0.3",
"ts-jest": "^29.1.1"
}
}
File renamed without changes.
5 changes: 4 additions & 1 deletion src/app/layout.tsx → src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Inter } from "next/font/google";
import TransportProvider from "@/components/TransportProvider";
import { StyleProvider } from "@/styles/provider";
import { ReduxProvider } from "@/redux/provider";
import { I18nProvider } from "@/i18n/client";

const inter = Inter({ subsets: ["latin"] });

Expand All @@ -18,7 +19,9 @@ export default function RootLayout({ children }: { children: React.ReactElement
<body className={inter.className}>
<ReduxProvider>
<TransportProvider>
<StyleProvider>{children}</StyleProvider>
<StyleProvider>
<I18nProvider>{children}</I18nProvider>
</StyleProvider>
</TransportProvider>
</ReduxProvider>
</body>
Expand Down
8 changes: 0 additions & 8 deletions src/app/page.module.css → src/app/[locale]/page.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,6 @@
padding: 4rem 0;
}

.center::before {
background: var(--secondary-glow);
border-radius: 50%;
width: 480px;
height: 360px;
margin-left: -400px;
}

.center::after {
background: var(--primary-glow);
width: 240px;
Expand Down
11 changes: 10 additions & 1 deletion src/app/page.tsx → src/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
"use client";

import Image from "next/image";
import styles from "./page.module.css";
import ThemeSelector from "@/components/ThemeSelector";
import { Storetester } from "@/components/StoreTester-to-remove";
import LocaleSelector from "@/components/LocaleSelector";
import { useI18n } from "@/i18n/client";

export default function Home() {
const { t, locale } = useI18n();

return (
<main className={styles.main}>
<div className={styles.description}>
<p>
Get started by editing&nbsp;
{t("hello")}, get started by editing&nbsp;
<code className={styles.code}>src/app/page.tsx</code>
</p>

<p>{locale}</p>

<div>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
Expand All @@ -33,6 +41,7 @@ export default function Home() {

<ThemeSelector />
<Storetester />
<LocaleSelector />

<div className={styles.center}>
<Image
Expand Down
18 changes: 18 additions & 0 deletions src/components/LocaleSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client";

import { useI18n } from "@/i18n/client";

const LocaleSelector = () => {
const { locale, changeLocale, t } = useI18n();

return (
<>
<button onClick={() => (locale === "en" ? changeLocale("fr") : changeLocale("en"))}>
Click me to change the current locale to: {locale === "en" ? "fr" : "en"}
<div>{t("welcome")}</div>
</button>
</>
);
};

export default LocaleSelector;
27 changes: 27 additions & 0 deletions src/i18n/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Internationalization

> In order to use internationalization we are using the [`next-international`](https://github.com/QuiiBz/next-international) library.
## Get started

### Locales

The folder `locales` at the root of this directory holds all the translation dictionnaries for each supported locales (e.g "en", "fr").

### Config

The `config` file holds the necessary information regarding the supported locales at the code level, it also provides some typings and export the necessary config object for `next-international`.

### Client

The `client` file expose 2 elements made for client components. In first place it exports a `I18nProvider` wrapper that is to be used at the top level of our app, here in `app/[locale]/layout.tsx`. Last but not least it also exports a hook that handle the logic behind `i18n`. Once wrapped, you can call `useI18n` in your client components.

> Note: One wrapper at the top level of your client components only.
### Server

The `server` file expose a single `useI18n` hook made for server components. You can call it straight away from any server components.

### Middleware

The `middleware` file export a `I18nMiddleware` object that feeds NextJs middleware system.
30 changes: 30 additions & 0 deletions src/i18n/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use client";

import { LocalesI18nConfig } from "@/i18n/config";
import { createI18nClient } from "next-international/client";

/**
* Create i18n client.
*/
const I18nClient = createI18nClient(LocalesI18nConfig);

/**
* Client component wrapper for the app.
*/
export function I18nProvider({ children }: { children?: React.ReactNode }) {
return <I18nClient.I18nProviderClient>{children}</I18nClient.I18nProviderClient>;
}

/**
* i18n client hook.
*
* @returns the necessary functions and variables to use i18n on client components.
*/
export const useI18n = () => {
const t = I18nClient.useI18n();
const changeLocale = I18nClient.useChangeLocale();

const locale = I18nClient.useCurrentLocale();

return { t, changeLocale, locale };
};
29 changes: 29 additions & 0 deletions src/i18n/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @dev Add locales here.
*/
export const LOCALES = ["en", "fr"] as const;

export type Locale = (typeof LOCALES)[number];
export type LocaleInfo = { file: File };
export type LocaleMap<T = string> = { [key in Locale]: T };

export const Locales = {
en: { file: () => import("./locales/en.json") },
fr: { file: () => import("./locales/fr.json") },
} as const satisfies LocaleMap<LocaleInfo>;

export const DEFAULT_LOCALE: Locale = "en";

/**
* Mapping from locales to their respective i18n files.
*/
export const LocalesI18nConfig = Object.values(LOCALES).reduce(
(acc, locale) => ({ ...acc, [locale]: Locales[locale].file }),
{} as LocaleMap<LocaleSchema>,
);

/**
* Utils type.
*/
export type File = () => Promise<unknown>;
export type LocaleSchema = (typeof Locales)[typeof DEFAULT_LOCALE]["file"];
4 changes: 4 additions & 0 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"hello": "Hello",
"welcome": "Welcome"
}
4 changes: 4 additions & 0 deletions src/i18n/locales/fr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"hello": "Bonjour",
"welcome": "Bienvenue"
}
8 changes: 8 additions & 0 deletions src/i18n/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createI18nMiddleware } from "next-international/middleware";
import { DEFAULT_LOCALE, LOCALES } from "./config";

export const I18nMiddleware = createI18nMiddleware({
locales: LOCALES,
defaultLocale: DEFAULT_LOCALE,
urlMappingStrategy: "rewrite",
});
21 changes: 21 additions & 0 deletions src/i18n/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { LocalesI18nConfig } from "@/i18n/config";
import { createI18nServer } from "next-international/server";

/**
* Create i18n server.
*/
const I18nServer = createI18nServer(LocalesI18nConfig);

/**
* i18n server hook.
*
* @returns the necessary functions and variables to use i18n on server components.
*/
export const useI18n = async () => {
const t = await I18nServer.getI18n();
const getStaticParams = I18nServer.getStaticParams;

const locale = I18nServer.getCurrentLocale();

return { t, getStaticParams, locale };
};
10 changes: 10 additions & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { NextRequest } from "next/server";
import { I18nMiddleware } from "@/i18n/middleware";

export function middleware(request: NextRequest) {
return I18nMiddleware(request);
}

export const config = {
matcher: ["/((?!api|static|.*\\..*|_next|favicon.ico|robots.txt).*)"],
};
13 changes: 7 additions & 6 deletions tools/test.tools.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { PropsWithChildren } from "react";
import { render, RenderOptions } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { I18nProvider } from "@/i18n/client";
import { RootState, setupStore } from "@/redux/store";
import { StyleProvider } from "@/styles/provider";
import { PreloadedState } from "@reduxjs/toolkit";
import { Provider } from "react-redux";
import { RootState, setupStore } from "@/redux/store";
import { ToolkitStore } from "@reduxjs/toolkit/dist/configureStore";
import { RenderOptions, render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React, { PropsWithChildren } from "react";
import { Provider } from "react-redux";

// This type interface extends the default options for render from RTL, as well
// as allows the user to specify other things such as initialState, store.
Expand Down Expand Up @@ -39,7 +40,7 @@ export function renderWithProviders(
return (
<Provider store={store}>
<StyleProvider selectedPalette={theme} fontsPath="/fonts">
{children}
<I18nProvider>{children}</I18nProvider>
</StyleProvider>
</Provider>
);
Expand Down
Loading

0 comments on commit 3ecb827

Please sign in to comment.