Search code examples
reactjsreact-routerreact-i18next

React router with i18next localized urls doesn't update the whole url when language changes


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>
    </>
  );
};

Solution

  • 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.