Skip to content

Commit 05bfbfd

Browse files
committed
Update README to sync with example repo and to stop using deprecated hooks
1 parent 991e966 commit 05bfbfd

File tree

1 file changed

+118
-59
lines changed

1 file changed

+118
-59
lines changed

README.md

Lines changed: 118 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -40,24 +40,44 @@ npm install i18next-fetch-backend
4040
First let's create some translation files in `app/locales`:
4141

4242
```ts
43-
// app/locales/en.ts
43+
// app/locales/en/translation.ts
4444
export default {
4545
title: "remix-i18next (en)",
46-
description: "A Remix + Vite + remix-i18next example",
46+
description: "A React Router + remix-i18next example",
4747
};
48+
49+
// app/locales/en/index.ts
50+
import type { ResourceLanguage } from "i18next";
51+
import translation from "./translation"; // import your namespaced locales
52+
53+
export default { translation } satisfies ResourceLanguage;
4854
```
4955

5056
```ts
51-
// app/locales/es.ts
52-
import type en from "./en";
53-
57+
// app/locales/en/translation.ts
5458
export default {
5559
title: "remix-i18next (es)",
56-
description: "Un ejemplo de Remix + Vite + remix-i18next",
57-
} satisfies typeof en;
60+
description: "Un ejemplo de React Router + remix-i18next",
61+
} satisfies typeof import("~/locales/en/translation").default;
62+
63+
// app/locales/es/index.ts
64+
import type { ResourceLanguage } from "i18next";
65+
import translation from "./translation"; // import your namespaced locales
66+
67+
export default { translation } satisfies ResourceLanguage;
5868
```
5969

60-
The type import and the `satisfies` are optional, but it will help us ensure that if we add or remove a key from the `en` locale (our default one) we will get a type error in the `es` locale so we can keep them in sync.
70+
The `satisfies typeof import("~/locales/en/translation").default` is optional, but it will help to ensure that if we add or remove a key from the `en` locale (the default one) we will get a type error in the `es` locale so we can keep them in sync.
71+
72+
Then re-export them all the locales in `app/locales/index.ts`:
73+
74+
```ts
75+
import type { Resource } from "i18next";
76+
import en from "./en";
77+
import es from "./es";
78+
79+
export default { en, es } satisfies Resource;
80+
```
6181

6282
### Setup the Middleware
6383

@@ -71,21 +91,38 @@ Create a file named `app/middleware/i18next.ts` with the following code:
7191
> Check older versions of the README for a guide on how to use RemixI18next class instead if you are using an older version of React Router or don't want to use the middleware.
7292
7393
```ts
94+
import { initReactI18next } from "react-i18next";
95+
import { createCookie } from "react-router";
7496
import { createI18nextMiddleware } from "remix-i18next/middleware";
75-
import en from "~/locales/en";
76-
import es from "~/locales/es";
97+
import resources from "~/locales"; // Import your locales
98+
import "i18next";
99+
100+
// This cookie will be used to store the user locale preference
101+
export const localeCookie = createCookie("lng", {
102+
path: "/",
103+
sameSite: "lax",
104+
secure: process.env.NODE_ENV === "production",
105+
httpOnly: true,
106+
});
77107

78108
export const [i18nextMiddleware, getLocale, getInstance] =
79109
createI18nextMiddleware({
80110
detection: {
81-
supportedLanguages: ["en", "es"],
82-
fallbackLanguage: "en",
83-
},
84-
i18next: {
85-
resources: { en: { translation: en }, es: { translation: es } },
86-
// Other i18next options are available here
111+
supportedLanguages: ["es", "en"], // Your supported languages, the fallback should be last
112+
fallbackLanguage: "en", // Your fallback language
113+
cookie: localeCookie, // The cookie to store the user preference
87114
},
115+
i18next: { resources }, // Your locales
116+
plugins: [initReactI18next], // Plugins you may need, like react-i18next
88117
});
118+
119+
// This adds type-safety to the `t` function
120+
declare module "i18next" {
121+
interface CustomTypeOptions {
122+
defaultNS: "translation";
123+
resources: typeof resources.en; // Use `en` as source of truth for the types
124+
}
125+
}
89126
```
90127

91128
Then in your `app/root.tsx` setup the middleware:
@@ -151,7 +188,7 @@ This will return a new `TFunction` instance with the locale set to `es`.
151188

152189
### Usage with react-i18next
153190

154-
So far this has configured the i18next instance to inside React Router loaders and actions, but in many cases we will need to use it directly in our React components.
191+
So far this has configured the i18next instance to use inside React Router loaders and actions, but in many cases we will need to use it directly in our React components.
155192

156193
To do this, we need to setup react-i18next.
157194

@@ -160,7 +197,7 @@ Let's start by updating the `entry.client.tsx` and `entry.server.tsx` files to u
160197
> [!TIP]
161198
> If you don't have these files, run `npx react-router reveal` to generate them. They are hidden by default.
162199
163-
### Update the root route
200+
#### Update the root route
164201

165202
First of all, we want to send the locale detected server-side by the middleware to the UI. To do this, we will return the locale from the `app/root.tsx` route.
166203

@@ -173,7 +210,7 @@ import {
173210
Scripts,
174211
ScrollRestoration,
175212
} from "react-router";
176-
import { useChangeLanguage } from "remix-i18next/react";
213+
import { useEffect } from "react";
177214
import type { Route } from "./+types/root";
178215
import {
179216
getLocale,
@@ -187,8 +224,8 @@ export const middleware = [i18nextMiddleware];
187224
export async function loader({ context }: Route.LoaderArgs) {
188225
let locale = getLocale(context);
189226
return data(
190-
{ locale },
191-
{ headers: { "Set-Cookie": await localeCookie.serialize(locale) } }
227+
{ locale }, // Return the locale to the UI
228+
{ headers: { "Set-Cookie": await localeCookie.serialize(locale) } },
192229
);
193230
}
194231

@@ -212,17 +249,20 @@ export function Layout({ children }: { children: React.ReactNode }) {
212249
);
213250
}
214251

215-
export default function App({ loaderData }: Route.ComponentProps) {
216-
useChangeLanguage(loaderData.locale);
252+
export default function App({ loaderData: { locale } }: Route.ComponentProps) {
253+
let { i18n } = useTranslation();
254+
useEffect(() => {
255+
if (i18n.language !== locale) i18n.changeLanguage(locale);
256+
}, [locale, i18n]);
217257
return <Outlet />;
218258
}
219259
```
220260

221261
We made a few changes here:
222262

223-
1. We added a `loader` that gets the locale from the context (set by the middleware) and returns it to the UI.
263+
1. We added a `loader` that gets the locale from the context (set by the middleware) and returns it to the UI. It also saves the locale in a cookie so it can be read from there in future requests.
224264
2. In the root Layout component, we use the `useTranslation` hook to get the i18n instance and set the `lang` attribute of the `<html>` tag, along the `dir` attribute.
225-
3. We added the `useChangeLanguage` hook to set the language in the i18next instance, this will keep the language in sync with the locale detected by the middleware after a refresh of the loader data.
265+
3. We added a `useEffect` in the `App` component to change the language of the i18n instance when the locale changes. This keeps the i18n instance in sync with the locale detected server-side.
226266

227267
#### Client-side configuration
228268

@@ -243,8 +283,11 @@ async function main() {
243283
.use(Fetch)
244284
.use(I18nextBrowserLanguageDetector)
245285
.init({
246-
fallbackLng: "en",
286+
fallbackLng: "en", // Change this to your default language
287+
// Here we only want to detect the language from the html tag
288+
// since the middleware already detected the language server-side
247289
detection: { order: ["htmlTag"], caches: [] },
290+
// Update this to the path where your locales will be served
248291
backend: { loadPath: "/api/locales/{{lng}}/{{ns}}" },
249292
});
250293

@@ -255,7 +298,7 @@ async function main() {
255298
<StrictMode>
256299
<HydratedRouter />
257300
</StrictMode>
258-
</I18nextProvider>
301+
</I18nextProvider>,
259302
);
260303
});
261304
}
@@ -265,7 +308,7 @@ main().catch((error) => console.error(error));
265308

266309
We're configuring `i18next-browser-languagedetector` to detect the language based on the `lang` attribute of the `<html>` tag. This way, we can use the same language detected by the middleware server-side.
267310

268-
### API for locales
311+
#### API for locales
269312

270313
The `app/entry.client.tsx` has the i18next backend configured to load the locales from the path `/api/locales/{{lng}}/{{ns}}`. Feel free to customize this but for this guide we will use that path.
271314

@@ -275,32 +318,20 @@ Now we need to create a route to serve the locales. So let's create a file `app/
275318
import { data } from "react-router";
276319
import { cacheHeader } from "pretty-cache-header";
277320
import { z } from "zod";
278-
import enTranslation from "~/locales/en";
279-
import esTranslation from "~/locales/es";
321+
import resources from "~/locales";
280322
import type { Route } from "./+types/locales";
281323

282-
const resources = {
283-
en: { translation: enTranslation },
284-
es: { translation: esTranslation },
285-
};
286-
287324
export async function loader({ params }: Route.LoaderArgs) {
288325
const lng = z
289-
.string()
290-
.refine((lng): lng is keyof typeof resources =>
291-
Object.keys(resources).includes(lng)
292-
)
326+
.enum(Object.keys(resources) as Array<keyof typeof resources>)
293327
.safeParse(params.lng);
294328

295329
if (lng.error) return data({ error: lng.error }, { status: 400 });
296330

297331
const namespaces = resources[lng.data];
298332

299333
const ns = z
300-
.string()
301-
.refine((ns): ns is keyof typeof namespaces => {
302-
return Object.keys(resources[lng.data]).includes(ns);
303-
})
334+
.enum(Object.keys(namespaces) as Array<keyof typeof namespaces>)
304335
.safeParse(params.ns);
305336

306337
if (ns.error) return data({ error: ns.error }, { status: 400 });
@@ -318,7 +349,7 @@ export async function loader({ params }: Route.LoaderArgs) {
318349
staleWhileRevalidate: "7d",
319350
// Serve stale content if there's an error for 7 days
320351
staleIfError: "7d",
321-
})
352+
}),
322353
);
323354
}
324355

@@ -335,17 +366,13 @@ They are not hard requirements, but they are useful for our example, feel free t
335366

336367
Finally, ensure the route is configured in your `app/routes.ts` file. You can set the path to `/api/locales/:lng/:ns` so it matches the path used in the `entry.client.tsx` file, if you use something else remember to update the `loadPath` in the i18next configuration.
337368

338-
### Server-side configuration
369+
#### Server-side configuration
339370

340371
Now in your `entry.server.tsx` replace the default code with this:
341372

342373
```tsx
343374
import { PassThrough } from "node:stream";
344-
345-
import type {
346-
EntryContext,
347-
RouterContextProvider,
348-
} from "react-router";
375+
import type { EntryContext, RouterContextProvider } from "react-router";
349376
import { createReadableStreamFromReadable } from "@react-router/node";
350377
import { ServerRouter } from "react-router";
351378
import { isbot } from "isbot";
@@ -361,7 +388,7 @@ export default function handleRequest(
361388
responseStatusCode: number,
362389
responseHeaders: Headers,
363390
entryContext: EntryContext,
364-
routerContext: RouterContextProvider
391+
routerContext: RouterContextProvider,
365392
) {
366393
return new Promise((resolve, reject) => {
367394
let shellRendered = false;
@@ -388,7 +415,7 @@ export default function handleRequest(
388415
new Response(stream, {
389416
headers: responseHeaders,
390417
status: responseStatusCode,
391-
})
418+
}),
392419
);
393420

394421
pipe(body);
@@ -400,7 +427,7 @@ export default function handleRequest(
400427
responseStatusCode = 500;
401428
if (shellRendered) console.error(error);
402429
},
403-
}
430+
},
404431
);
405432

406433
setTimeout(abort, streamTimeout + 1000);
@@ -412,7 +439,9 @@ Here we are using the `getInstance` function from the middleware to get the i18n
412439

413440
This way, we can re-use the instance created in the middleware and avoid creating a new one. And since the instance is already configured with the language we detected, we can use it directly in the `I18nextProvider`.
414441

415-
## Finding the locale from the request URL pathname
442+
## Common Scenarios
443+
444+
### Finding the locale from the request URL pathname
416445

417446
If you want to keep the user locale on the pathname, you have two possible options.
418447

@@ -441,7 +470,7 @@ export const [i18nextMiddleware, getLocale, getInstance] =
441470

442471
The locale returned by `findLocale` will be validated against the list of supported locales, in case it's not valid the fallback locale will be used.
443472

444-
## Querying the locale from the database
473+
### Querying the locale from the database
445474

446475
If your application stores the user locale in the database, you can use `findLocale` function to query the database and return the locale.
447476

@@ -458,7 +487,7 @@ export let i18n = new RemixI18Next({
458487
});
459488
```
460489

461-
## Store the locale in a cookie
490+
### Store the locale in a cookie
462491

463492
If you want to store the locale in a cookie, you can create a cookie using `createCookie` helper from React Router and pass the Cookie object to the middleware.
464493

@@ -505,12 +534,12 @@ export async function loader({ context }: Route.LoaderArgs) {
505534
let locale = getLocale(context);
506535
return data(
507536
{ locale },
508-
{ headers: { "Set-Cookie": await localeCookie.serialize(locale) } }
537+
{ headers: { "Set-Cookie": await localeCookie.serialize(locale) } },
509538
);
510539
}
511540
```
512541

513-
## Store the locale in the session
542+
### Store the locale in the session
514543

515544
Similarly to the cookie, you can store the locale in the session. To do this, you can create a session using `createSessionStorage` helpers from React Router and pass the SessionStorage object to the middleware.
516545

@@ -564,7 +593,37 @@ export async function loader({ request, context }: Route.LoaderArgs) {
564593

565594
return data(
566595
{ locale },
567-
{ headers: { "Set-Cookie": await sessionStorage.commitSession(session) } }
596+
{ headers: { "Set-Cookie": await sessionStorage.commitSession(session) } },
568597
);
569598
}
570599
```
600+
601+
### Handle Not Found Errors
602+
603+
If you want to handle not found errors and show a custom internationalized 404 page, you can create a route with the path `*` and render your custom 404 page there.
604+
605+
```tsx
606+
import { useTranslation } from "react-i18next";
607+
import { data, href, Link } from "react-router";
608+
609+
export async function loader() {
610+
return data(null, { status: 404 }); // Set the status to 404
611+
}
612+
613+
export default function Component() {
614+
let { t } = useTranslation("notFound");
615+
616+
return (
617+
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
618+
<h1>{t("title")}</h1>
619+
<p>{t("description")}</p>
620+
621+
<Link to={href("/")}>{t("backToHome")}</Link>
622+
</div>
623+
);
624+
}
625+
```
626+
627+
If you're using `@react-router/fs-routes` package, you can create a file named `app/routes/$.tsx` and it will be used automatically. If you're configuring your routes manually, create a file `app/routes/not-found.tsx` and add `route("*", "./routes/not-found.tsx")` to your routes configuration.
628+
629+
Without this route, any not found request will not run the middleware in root, causing `entry.server.tsx` to not have the i18next instance configured, resulting in an error.

0 commit comments

Comments
 (0)