astro-i18next icon indicating copy to clipboard operation
astro-i18next copied to clipboard

localizePath not working on react client

Open florian-lefebvre opened this issue 2 years ago • 3 comments

Hi there, I'm setting up localized links thanks to localizePath. However, it throws an error when used in a React component with client:load. I guess it's somehow linked to the fact that it's supposed to run server side. Here are the errors show in the console: image My app's output is set to static, see the repo: https://github.com/florian-lefebvre/portfolio/tree/v5/astro

florian-lefebvre avatar Dec 05 '22 20:12 florian-lefebvre

This seems related to localizePath relying on Astro.url.pathname which becomes an issue for interactive, non-astro components.

I personally set the Astro.url.pathname to a svelte store to access outside the astro context, but again this does not work when the component is interactive.

---
// Shared state, e.g. svelte store
import { currentPathname } from "./store";

// Set svelte store in astro layout
currentPathname.set(Astro.url.pathname);
---

Perhaps localizePath could accept the current pathname as a parameter instead of relying on Astro.url.pathname?

<script>
  import { localizePath } from "astro-i18next";
  import { currentPathname } from "./store";
  
  localizePath('about', currentPathname);
</script>

mvllow avatar Jan 03 '23 19:01 mvllow

Hey @florian-lefebvre and @mvllow, in case you didn't find a better solution, this is mine:

astro-i18next.config.js

/** @type {import('astro-i18next').AstroI18nextConfig} */
export default {
  ...,
  routes: {
    en: {
      'huis-ibiza': {
        index: 'ibiza-villas',
        '[slug]': {
          index: '[slug]',
          book: {
            index: 'book',
            'thank-you': 'thank-you',
          },
          enquiry: {
            index: 'enquiry',
            'thank-you': 'thank-you',
          },
        },
      },
      contact: {
        index: 'contact-us',
      },
      'over-ons': {
        index: 'about-us',
      },
    },
  },
  ...
};

src/utils/routing.js

import i18nConfig from '../../astro-i18next.config';

const routes = i18nConfig.routes;
const flattenedRoutes = flattenRoutes(routes);

function flattenRoutes(routes) {
  const flattenedRoutes = {};
  for (const [langCode, langRoutes] of Object.entries(routes)) {
    flattenedRoutes[langCode] = {};
    for (const [route, routeData] of Object.entries(langRoutes)) {
      if (typeof routeData === 'string') {
        flattenedRoutes[langCode][`/${route}`] = `/en/${routeData}`;
      } else {
        flattenedRoutes[langCode][`/${route}`] = `/en/${routeData.index}`;
        flattenRoutesHelper(
          flattenedRoutes[langCode],
          route,
          routeData,
          `/en/${routeData.index}`
        );
      }
    }
  }
  return flattenedRoutes;
}

function flattenRoutesHelper(flattenedRoutes, route, routeData, baseRoute) {
  for (const [subRoute, subRouteData] of Object.entries(routeData)) {
    if (subRoute === 'index') continue;
    if (typeof subRouteData === 'string') {
      flattenedRoutes[`/${route}/${subRoute}`] = `${baseRoute}/${subRouteData}`;
    } else {
      flattenedRoutes[
        `/${route}/${subRoute}`
      ] = `${baseRoute}/${subRouteData.index}`;
      flattenRoutesHelper(
        flattenedRoutes,
        `${route}/${subRoute}`,
        subRouteData,
        `${baseRoute}/${subRouteData.index}`
      );
    }
  }
}

export function localizePath(url, locale = 'nl') {
  const defaultLanguage = 'nl';

  // If the default language is the same as the specified language, return the original url
  if (defaultLanguage === locale) {
    return url;
  }

  return flattenedRoutes[locale][url];
}

this will return an object like this:

{
    "en": {
        "/huis-ibiza": "/en/ibiza-villas",
        "/huis-ibiza/[slug]": "/en/ibiza-villas/[slug]",
        "/huis-ibiza/[slug]/book": "/en/ibiza-villas/[slug]/book",
        "/huis-ibiza/[slug]/book/thank-you": "/en/ibiza-villas/[slug]/book/thank-you",
        "/huis-ibiza/[slug]/enquiry": "/en/ibiza-villas/[slug]/enquiry",
        "/huis-ibiza/[slug]/enquiry/thank-you": "/en/ibiza-villas/[slug]/enquiry/thank-you",
        "/contact": "/en/contact-us",
        "/over-ons": "/en/about-us"
    }
}

client-side component

import { localizePath } from '@utils/routing';

// normal href
const href = localizePath('/huis-ibiza', 'en');

// in case you have a dynamic key you can use .replace method
const hrefWithDynamicRoute = localizePath('/huis-ibiza/[slug]', 'en').replace('[slug]', props.house.slug);

Regards

fprl avatar Jan 06 '23 17:01 fprl

@fprl I found a new way:

npm i react-i18next@^12 
#The astro-i18next depends on i18next@^22, so we need to install react-i18next with the same dependencies.
/** @type {import('astro-i18next').AstroI18nextConfig} */
export default {
  defaultLocale: "en",
  locales: ["en", "zh"],
  i18next: {
    debug: true, // convenient during development to check for missing keys
  },
  load: ["server", "client"], // load i18next server and client side
  i18nextServerPlugins: {
    "{initReactI18next}": "react-i18next",
  },
  i18nextClientPlugins: {
    "{initReactI18next}": "react-i18next",
  },
};

Add a custom hook:

import { useCallback } from "react";
import { useTranslation } from "react-i18next";

export const useLocalization = () => {
  const { i18n } = useTranslation();
  const localizePath = useCallback(
    (path: string) => {
      const currentLanguage = i18n.language;
      if (currentLanguage === "en") {
        return path;
      }
      return `/${currentLanguage}${path}`;
    },
    [i18n.language]
  );
  // todo: localize url
  // const localizeUrl = useCallback(
  //   (url: string) => {
  //   },
  //   [i18n.language]
  // );
  return { localizePath };
};

use custom hook in react component:

import { useTranslation } from "react-i18next";
import { useLocalization } from "hooks/useLocalization";
// ...
  const { t } = useTranslation();
  const { localizePath } = useLocalization();
// ...

bowencool avatar Jan 07 '24 07:01 bowencool