reactjstypescriptreact-routerreact-router-domreact-i18next

i18next + react router : My URL isn't changing correctly when language is switched


I am trying to change the app URL when language is switched. When I add the selected language manually in the URL (like "http://localhost:3001/es/forgot-password"), the components load correctly with selected language but when I switch the language using changeLanguage event, translations work correctly but the language doesn't change in the URL and components don't load properly.

I have been stuck on this since long, also have researched a lot, and tried different things; nothing has helped me to figure the issue out properly.

Here's my code:

i18n.tsx

import HttpApi from 'i18next-http-backend';
import XHR from "i18next-http-backend"
import { initReactI18next } from "react-i18next";
import LanguageDetector from 'i18next-browser-languagedetector';
import i18n from 'i18next';

i18n.use(XHR)
  .use(LanguageDetector)
  .use(HttpApi)
  .use(initReactI18next)
  .init({
    supportedLngs: ['en', 'es', 'pt', 'fr', 'gr', 'pl'],
    fallbackLng: 'en',
    detection: {
        order: ['querystring','path','cookie','htmlTag','localStorage','sessionStorage','subdomain'],
        caches: ['cookie', 'localStorage'],
        lookupQuerystring: 'lng',
        lookupCookie: 'i18next',
        lookupLocalStorage: 'i18nextLng',
        lookupSessionStorage: 'i18nextLng'
    },
    backend: {
        loadPath: "/locales/{{lng}}/translation.json"
    },
    interpolation: {escapeValue: false},
    debug: true,
    react: {useSuspense: false}
  });

export default i18n;

LanguageSwitch.tsx

import './LanguageSwitch.scss';
import { useState } from 'react';
import { useTranslation } from "react-i18next";

type LanguateSwitchProps = {
    passLanguageChange: (str: string) => void
};

const LanguageSwitch = (props: LanguateSwitchProps) => {
    const { t, i18n } = useTranslation(['home']);

    const [lang, setLang] = useState('en');

    const onClickLanguageChange = (e: any) => {
        const language = e.target.value;
        i18n.changeLanguage(language); //change the language
        props.passLanguageChange(language); //<---Sending the change language event to App with chosen language
    }

    return (
        <div className="language-switch">
            <select className="custom-select" onChange={onClickLanguageChange}>
                <option value="en">English</option>
                <option value="es">Español</option>
            </select>
            
        </div> 
  );
}

export default LanguageSwitch;

App.tsx

import './App.scss';
import { useState } from 'react';
import { Routes, Route} from 'react-router-dom';
import { AuthProvider } from './auth';
import ChangePassword from './components/ChangePassword/ChangePassword';
import ForgotPassword from './components/ForgotPassword/ForgotPassword';
import Login from './components/Login/Login';
import ResetPassword from './components/ResetPassword/ResetPassword';
import Dashboard from './components/Dashboard/Dashboard';
import PrivateRoutes from './components/PrivateRoutes';
import LanguageSwitch from './components/LanguageSwitch/LanguageSwitch';
import  i18n from './i18n';

const App = () => {
  const [language, setLanguage] = useState(i18n.language);

  const handleLanguageChange = (lang: string) => {
    setLanguage(lang); //<---Capturing the switch language event and language from switch component
  }

  return (
    <AuthProvider>
      <div className="App"> 
        <div className="container">
        <LanguageSwitch passLanguageChange={handleLanguageChange}></LanguageSwitch>
          <div className="row">
            {language}
            <Routes>
              <Route path={`/${language}`} element={<Login/>}></Route> //<---passing the selected language in the path
              <Route path={`${language}/forgot-password`} element={<ForgotPassword/>}></Route>
              <Route element={<PrivateRoutes/>}>
                <Route path={`${language}/dashboard`} element={<Dashboard/>}></Route>
                <Route path={`${language}/change-password`} element={<ChangePassword/>}></Route>
              </Route> 
              <Route path="reset-password" element={<ResetPassword/>}></Route>
            </Routes>
          </div>
        </div>
      </div>
    </AuthProvider>
  );
}

export default App;

Solution

  • Along with updating the language that is stored into state, the code should also issue a navigation change to a route using the new language in order to also update the URL in the address bar. Since it's a bit impractical to know every/all route(s) the app is possibly rendering I suggest using a bit of string manipulation and a set of known language abbreviations to update just the one language path segment.

    Example:

    const languages = {
      en: "English",
      es: "Español"
    };
    
    type LanguageSwitchProps = {
      language: string;
      passLanguageChange: (str: string) => void;
    };
    
    const LanguageSwitch = (props: LanguageSwitchProps) => {
      const { t, i18n } = useTranslation(['home']);
    
      const onClickLanguageChange = (e: any) => {
        const language = e.target.value;
        i18n.changeLanguage(language); // change the language
        props.passLanguageChange(language); // Send the change to App
      };
    
      return (
        <div className="language-switch">
          <select
            className="custom-select"
            value={props.language}
            onChange={onClickLanguageChange}
          >
            {Object.entries(languages).map(([value, label]) => (
              <option key={value} value={value}>
                {label}
              </option>
            ))}
          </select>
        </div>
      );
    };
    
    ...
    import {
      Routes,
      Route,
      Navigate,
      useLocation,
      useNavigate
    } from 'react-router-dom';
    ...
    
    const App = () => {
      const location = useLocation();
      const navigate = useNavigate();
    
      const [language, setLanguage] = useState(i18n.language);
    
      const handleLanguageChange = (lang: string) => {
        setLanguage(lang);
        const [language, ...path] = location.pathname.slice(1).split("/");
    
        if (language in languages) {
          navigate(
            {
              ...location,
              pathname: `/${[lang, ...path].join("/")}`
            },
            { replace: true }
          );
        }
      };
    
      return (
        <AuthProvider>
          <div className="App"> 
            <div className="container">
              <LanguageSwitch
                language={language}
                passLanguageChange={handleLanguageChange}
              />
              <div className="row">
                {language}
    
                <Routes>
                  <Route path=":language">
                    <Route index element={<Login />} />
                    <Route path="forgot-password" element={<ForgotPassword />} />
                    <Route element={<PrivateRoutes />}>
                      <Route path="dashboard" element={<Dashboard />} />
                      <Route path="change-password" element={<ChangePassword />} />
                    </Route>
                  </Route>
                  <Route path="reset-password" element={<ResetPassword />} />
                </Routes>
              </div>
            </div>
          </div>
        </AuthProvider>
      );
    }
    
    export default App;