I have a React app that needs to have localized URL's. I've managed to translate the urls and have implemented the logic for figuring out the new URL after language change, but I've run into an issue that I just can't get over: After I change the language, only the last part of the URL gets translated, not the part which is the lang /en/...
.
Here's a demo version of my code, and link to a prototype: https://stackblitz.com/edit/vitejs-vite-mxzpnq
EDIT: Just to point out, the actual reproduction of the issue is: When the app first loads and you hover over a link it displays the link correctly, eg: "../en/route-1-en". Change the language to fr and hover over the link, it'll be ".../en/route-1-fr". I have no idea why the "en" stays there. I think it's tied to how NavLink resolves it's "to" prop.
function App() {
const { i18n, t } = useTranslation();
// For the case where the user navigates to the root path without language part
const rootRoute = {
path: '/',
loader: () => redirect(`/${i18n.language}`),
};
const unexpectedRoute = {
path: '*',
element: (
<div>
Not found, <NavLink to="/">Go back to home</NavLink>
</div>
),
};
const routes = ['en', 'es', 'fr'].map((lng) => ({
lang: lng,
routes: [
{
id: lng,
path: '/:lng',
element: <Root />,
children: [
{
index: true,
element: <Home />,
},
{
path: t(`routes.route-1`, { lng }),
element: <TestPage />,
},
{
path: t(`routes.route-2`, { lng }),
element: <TestPage />,
},
{
path: t(`routes.route-3`, { lng }),
element: <TestPage />,
},
],
},
],
}));
const langRoutes: RouteObject[] = [...routes.flatMap((r) => r.routes)];
const router = createBrowserRouter([
...langRoutes,
rootRoute,
unexpectedRoute,
]);
return (
<>
<select
onChange={async (e) => {
console.log(e.target.value);
await i18n.changeLanguage(e.target.value);
// Navigate to new path
}}
value={i18n.language}
>
<option value="en">En</option>
<option value="es">Es</option>
<option value="fr">Fr</option>
</select>
<div>
<RouterProvider router={router} />
</div>
</>
);
}
export const Home = () => {
const { t, i18n } = useTranslation();
return (
<>
<h1>Home sweet home</h1>
<h2>Current language is: {i18n.language}</h2>
<ul>
<li>
<NavLink to={t('routes.route-1')}>Route 1</NavLink>
</li>
<li>
<NavLink to={t('routes.route-2')}>Route 2</NavLink>
</li>
<li>
<NavLink to={t('routes.route-3')}>Route 3</NavLink>
</li>
</ul>
</>
);
};
I have no idea why the "en" stays there.
This is because your links use relative paths and the parent path is "en"
until you manually update it to match the language. In other words, the current path is "/en"
rendering the Home
component that renders links using relative paths that are effectively something like "./route-2-es"
, so the entire computed target path is the current route + path => "/en/route-2-es"
.
What you could do in the meantime is compute a more complete absolute (starts with "/"
) target path using the generatePath
utility function and the currently selected language.
Example:
Home.tsx
import { useTranslation } from 'react-i18next';
import { NavLink, generatePath } from 'react-router-dom';
export const Home = () => {
const { t, i18n } = useTranslation();
return (
<>
<h1>Home sweet home</h1>
<h2>Current language is: {i18n.language}</h2>
<ul>
<li>
<NavLink
to={generatePath(`/:lang/${t('routes.route-1')}`, {
lang: i18n.language,
})}
>
Route 1
</NavLink>
</li>
<li>
<NavLink
to={generatePath(`/:lang/${t('routes.route-2')}`, {
lang: i18n.language,
})}
>
Route 2
</NavLink>
</li>
<li>
<NavLink
to={generatePath(`/:lang/${t('routes.route-3')}`, {
lang: i18n.language,
})}
>
Route 3
</NavLink>
</li>
</ul>
</>
);
};
To handle updating the URL to the correct path you can apply some logic in Root
to check the current path match and language and redirect to the same route but with the new language path segment substituted in.
Example:
Root.tsx
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Outlet, useNavigate, useMatch, generatePath } from 'react-router-dom';
export const Root = () => {
const { i18n } = useTranslation();
const navigate = useNavigate();
const match = useMatch("/:lang/*");
const lang = i18n.language;
useEffect(() => {
if (match) {
navigate(generatePath("/:lang/*", {
"*": match.params["*"] ?? "",
lang
}), { replace: true })
}
}, [match, lang]);
return <Outlet />;
};
With this change you don't need the suggestion above in Home
as the relative paths will still work as before from the now-updated parent route path.