1

I have a question about React Router V6 nested with i18n.

This is my first multi-language service.

const MainPage:React.FC = () => {

  const lang = i18n.language;

  return (
    <>
      <Wrapper>
        <Routes>
          {/* Main */}
          <Route path={`/`} element={<Home />}>
            <Route path={`${lang}`}>
              <Route path={`service`}>
                <Route path={'slack'} element={<Slack />} />
              </Route>
            </Route>
            {/* <Route path={`service/dooray`}element={<Dooray />} /> */}
            {/* <Route path={`contact`} element={<Contact />} /> */}

            {/* <Route path={`app/sign-in`} element={<SignIn />} /> */}
            {/* <Route path={`app/sign-up`} element={<SignUp />} /> */}
            {/* <Route path={`app/mail-code`} element={<MailCode />} /> */}
            {/* <Route path={`app/password/reset`} element={<PwdReset />} /> */}

            {/* <Route path={`policies/privac`} element={<Privacy />} /> */}
            {/* <Route path={`policies/terms`} element={<Terms />} /> */}
          </Route>
          {/* <Route path={`*`} element={<>NOT FOUND</>} /> */}
          {/* test */}
        </Routes>
      </Wrapper>
      <ParentModal />
    </>

If I enter localhost:3000/en, there is an error 'This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.'

How can I fix it..

I want /en => go to english page, /jp => go to japanese page


const MainPage:React.FC =() => {

...

<Route path={`/`} element={<Home />}>
    <Route path={`/${lang}/*`}>
       <Route path={`service`}>
           <Route path="slack" element={<Slack />} />
       </Route>
    </Route>
</Route>
}
const Home:React.FC = () => {

 return (
 <>
   ... UI, JSX
   <Outlet />
 </>
 )
}

I add a <Outlet />. But if I entered '/ko/service/slack', render <Home /> now


<Route path={`/`} element={<Home />}>
    <Route path="service">
       <Route path="slack" element={<Slack />} />
       <Route path="dooray" element={<Dooray />} />
    </Route>
</Route>

nested-routes doesn't work.. :(

Jerry
  • 41
  • 1
  • 6

3 Answers3

1

Issue

The error 'This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.' means the parent route isn't rendering an Outlet component for the nested routes to be rendered into. The route rendering the Home component doesn't appear to be rendering an Outlet.

Solution

Update the Home component to render an Outlet. Note that Route components without an element prop will render an Outlet by default.

Example:

import { Outlet } from 'react-router-dom';

const Home = () => {
  ...

  return (
    <>
      ... home page UI/JSX ...
      <Outlet />
    </>
  );
};

...

const MainPage:React.FC = () => {
  const lang = i18n.language;

  return (
    <>
      <Wrapper>
        <Routes>
          {/* Main */}
          <Route path="/" element={<Home />}>
            <Route path={lang}>   // <-- renders Outlet by default
              <Route path="service"> // <-- renders Outlet by default
                <Route path="slack" element={<Slack />} />
              </Route>
            </Route>
            ...
          </Route>
          ...
          {/* test */}
        </Routes>
      </Wrapper>
      <ParentModal />
    </>
  );
};

Update

If the Home and Slack components are separate and independent, then move the Home component into an index route and simplify the routing to the Slack component.

<Routes>
  <Route path="/">
    <Route index element={<Home />} />
    <Route path={`${lang}/service/slack`} element={<Slack />} />
  </Route>
</Routes>

Edit react-router-v6-nested-routes-with-i18n

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • @Jerry I don't quite understand what you are saying. Are you saying that `Home` is its own page component and shouldn't render with nested route content? Can you clarify? – Drew Reese Apr 07 '22 at 02:34
  • I am sorry. I am not good english. The comment I written before 9 minutes ago is deleted. I mean nested-routes doesn't work. I update my question. '/ko/service/slack' render – Jerry Apr 07 '22 at 02:56
  • ``component works like render `` and under `` component render `` now. But I don't want render under some component – Jerry Apr 07 '22 at 03:19
  • @Jerry Ok, so are you saying that you want the `Home` component rendered on path `"/"` and `Slack` component to be rendered on a `"/{lang}/service/slack"` path, both independently? – Drew Reese Apr 07 '22 at 04:19
  • Yes ! they work both independently. – Jerry Apr 07 '22 at 05:46
  • @Jerry Ok, I've updated my answer with a suggested routing configuration. – Drew Reese Apr 07 '22 at 06:11
  • May I ask more question about multi-language router? – Jerry Apr 08 '22 at 01:58
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/243706/discussion-between-drew-reese-and-jerry). – Drew Reese Apr 08 '22 at 02:42
1

I had the exact same useCase (localize react router v6) and came up with the following LangRouter repository link:

const LangRouter = () => {
  const { i18n } = useTranslation(),
    { pathname, search, hash } = useLocation(),
    navigate = useNavigate(),
    availableLocales = ['en', 'ar'],
    defaultLocale = (
      getDefaultLanguage() === 'en' || getDefaultLanguage() === 'ar' ? getDefaultLanguage() : 'en'
    ) as string,
    pathnameLocale = pathname.substring(1, 3).toLowerCase(),
    [locale, setLocale] = useState(defaultLocale),
    loaderTimerRef = useRef<any>(),
    [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    loaderTimerRef.current = setTimeout(() => {
      setIsLoading(false);
      clearTimeout(loaderTimerRef.current);
    }, 300);
  }, []);

  useEffect(() => {
    if (availableLocales.includes(pathnameLocale)) {
      updateLocale(pathnameLocale);
    } else if (pathname === '/') {
      updateLocale(defaultLocale);
    }
    // eslint-disable-next-line
  }, [pathname]);

  useEffect(() => {
    let lang = defaultLocale;

    if (availableLocales.includes(pathnameLocale)) {
      lang = pathnameLocale;
      setLanguageHandler(lang);
    } else if (pathname === '/') {
      setLanguageHandler(lang);
    }
    // eslint-disable-next-line
  }, [locale]);

  const setLanguageHandler = (lang: string) => {
    if (lang === 'en') {
      i18n.changeLanguage('en-US');
    } else {
      i18n.changeLanguage('ar-SA');
    }
  };

  const updateLocale = (newLocale: string) => {
    const newPath = `/${newLocale}` + pathname.substring(3);

    if (locale !== newLocale) {
      if (newPath === `/${newLocale}/` || newPath === `/${newLocale}` || pathname === '/') {
        navigate(getHomePageUrl(newLocale));
      } else {
        navigate(`${newPath}${hash}${search}`);
      }
      setLocale(newLocale);
    } else if (newPath === `/${newLocale}/` || newPath === `/${newLocale}` || pathname === '/') {
      if (isAuthenticated()) {
        navigate(getHomePageUrl(newLocale));
      } else {
        navigate(getLoginPageUrl(newLocale));
      }
    }
  };

  if (isLoading) {
    return (
      <div className="loader-wrapper">
        <LoadingIcon />
      </div>
    );
  }

  return (
    <LocaleContext.Provider value={{ locale, setLocale: updateLocale }}>
      <Routes>
        <Route path={`/${locale}`} element={<App />}>
          {publicRoutes.map((el, i) => (
            <Route
              key={i}
              path={el.path(locale)}
              element={
                <PublicRouteGuard
                  restricted={el.restricted}
                  redirect={el.redirect ? el.redirect(locale) : undefined}
                >
                  {el.element}
                </PublicRouteGuard>
              }
            />
          ))}

          {privateRoutes.map((el, i) => (
            <Route
              key={i}
              path={el.path(locale)}
              element={
                el.permissions ? (
                  <RestrictedRouteGuard requiredPermissions={el.permissions}>
                    {el.element}
                  </RestrictedRouteGuard>
                ) : (
                  <PrivateRouteGuard>{el.element}</PrivateRouteGuard>
                )
              }
            >
              {el.children &&
                el.children.map((innerEl, innerI) => (
                  <Route key={innerI} path={innerEl.path(locale)} element={innerEl.element} />
                ))}
            </Route>
          ))}
        </Route>
        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    </LocaleContext.Provider>
  );
};

export default LangRouter;
Adam Morsi
  • 351
  • 1
  • 7
1

You can create a Route with an Outlet that detects the language and assigns it to i18next directly:

<Routes>
  <Route path="/" element={<Home />}>
    <Route path=":lang" element={<LanguagePath />}>
      <Route path="/" element={<Home />} />
      <Route path="service/dooray" element={<Dooray />} />
      <Route path="contact" element={<Contact />} />
      <Route path="app/sign-in" element={<SignIn />
      <Route path="app/sign-up" element={<SignUp />
      <Route path="*" element={<>NOT FOUND</>} />
    </Route>
  </Route>
</Routes>

Then inside LanguagePath.js you can detect the lang and either set it in i18n.changeLanguage or redirect the user if it is missing.

export default function LanguagePath() {
  const { i18n } = useTranslation();
  const { lang } = useParams();
  const navigate = useNavigate();
  const curPath = location.pathname;
  useEffect(() => {
    if (lang && i18n.resolvedLanguage !== lang) {
      if (i18n.options.fallbackLng.includes(lang)) {
        i18n.changeLanguage(lang);
      } else {
        navigate("/" + i18n.resolvedLanguage + curPath, {replace: true});
      }
    }
  }, [lang]);
  return <Outlet />;
}

Now if a user arrives at:

/ => /
/contact => /en/contact
/en/contact => /en/contact
/fr/contact => /fr/contact
Kernel James
  • 3,752
  • 25
  • 32