React-i18next: How to implement a custom locale detector?

Created on 7 Feb 2021  Â·  9Comments  Â·  Source: i18next/react-i18next

I have used i18next-browser-languagedetector but it was always returning "en" while my browser is in a different language. Also, I would only like to fetch the locale based on the browser preference or the user store.

So I've wrote my own function. But it is never called during the initialization. How to fix this?

To Reproduce

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import { userStore } from "src/stores/user";
import en from "./en.json";
import fr from "./fr.json";

const languages = {
  EN: "en",
  FR: "fr",
};

const resources = {
  en: { translation: en },
  fr: { translation: fr },
};

const detectLocale = () => {
  const browserLocale = navigator.language.split("-")[0];
  const { locale } = userStore();
  if (locale) return locale;
  if (browserLocale) return browserLocale;
  return languages.EN;
};

export default i18n.use(initReactI18next).init({
  resources,
  lng: languages.EN,
  detection: detectLocale,
  fallbackLng: languages.EN,
  keySeparator: ".",
  whitelist: [languages.EN, languages.FR],
  interpolation: { escapeValue: false },
});

Expected behavior

Fetch the user locale or the browser locale.

Your Environment

  • i18next version:^19.8.5
  • os: Mac

Most helpful comment

Seems zustand uses react hooks... and the language-detector knows nothing about react...

Based on a short google search, it seems replacing const { locale } = userStore(); with const { locale } = userStore.getState(); works

https://codesandbox.io/s/blissful-river-3h1hu?file=/src/i18n/index.ts:355-395

All 9 comments

Have a look at: https://www.i18next.com/misc/creating-own-plugins#languagedetector

and then pass it to the ’.use(myLngDet)’ function

Hi @andai, so I've read the doc and implemented as suggested, but it makes the app crash. I must do something wrong. Could you please provide me with a more detailed exemple?

const detectLocale = () => {
  const browserLocale = navigator.language.split("-")[0];
  const { locale } = userStore();
  if (locale) return locale;
  if (browserLocale) return browserLocale;
  return languages.EN;
};

const myLngDet = {
  type: "languageDetector",
  async: true,
  detect: detectLocale(),
};

const resources = {
  en: { translation: en },
  fr: { translation: fr },
};

export default i18n
  .use(myLngDet)
  .use(initReactI18next)
  .init({
    resources,
    lng: languages.EN,
    detection: langDetectorOptions,
    fallbackLng: languages.EN,
    keySeparator: ".",
    whitelist: [languages.EN, languages.FR],
    interpolation: { escapeValue: false },
  });

If this is set to true, your detect function receives a callback function that you should call with your language
image

Here you have a simple example: https://codesandbox.io/s/react-i18next-with-custom-language-detector-tcf9f?file=/src/i18n.js

Does it work now?

Hi, sorry I was not connected this afternoon. So I've tried your example. It almost works. I now receive the browser default language, which is great, but I still can't access the user's locale, that is stored in a zustand store (it's another global state management library, like redux).

I wrote:

const detectLocale = () => {
const browserLocale = navigator.language.split("-")[0];
const { locale } = userStore();
 if (locale) return locale;
 if (browserLocale) return browserLocale;
 return languages.EN;
 };

const LanguageDetector = {
  type: "languageDetector",
  async: false,
  init: function (services, detectorOptions, i18nextOptions) {
    /* use services and options */
  },
  detect: detectLocale(),
  cacheUserLanguage: function (lng) {
    /* cache language */
  },
};

const resources = {
  en: { translation: en },
  fr: { translation: fr },
};

export default i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources,
    fallbackLng: languages.EN,
    keySeparator: ".",
    whitelist: [languages.EN, languages.FR],
    interpolation: { escapeValue: false },
  });

The advantage of Zustand is that I can interact with the store outside React components. Yet, it crashes here, claiming that I can't use any hook outside React component. Weird.

When the user connects, I fetch his infos on my server and populate a global store with the data received. I need this locale to make the i18n match his preference on any browser/device he might use. What would be your recommendation to make it work here (I'm not going off-topic, here it's really the core of the initial post).

Also, typescript is not happy with .use(LanguageDetector) . It claims that:

Argument of type '{ type: string; async: boolean; init: (services: any, detectorOptions: any, i18nextOptions: any) => void; detect: (callback: any) => string; cacheUserLanguage: (lng: any) => void; }' is not assignable to parameter of type 'Module | ThirdPartyModule[] | Newable[] | Newable'.

I don't know zustand...
and without having a reproducable example it's not easy...
but maybe instead of "detect: detectLocale()," don't call the function, but just pass it, like "detect: detectLocale,"

Here is a reproductible example: https://codesandbox.io/s/holy-mountain-663k8?file=/src/i18n/index.ts

Please note the typescript error on i18n/index.ts at line 39. To reproduce the issue, just add detectLocale() on line 25 (same file: i18n/index.ts)

If there is a better way to retrieve the user's locale on any device, I'd be happy to hear your thoughts. Maybe my strategy is not that good with this library.

For example, I could:

  • create a myAppLocale token in localeStorage inside the cacheUserLanguage:function (I think it is its purpose right?)
  • when retrieving the user, compare the myAppLocale token to my user locale.
  • if user's locale and token's locale are different, update i18next and update myAppLocale.

But if I do this, I'am afraid there would be performance issues (it might display the whole app in english for 1 second and immediately refresh into french for example). Also, it seems I can only update i18n locale from a React component, not inside an async function (if it is possible, how to do so?). Or maybe there is a possibility to make i18next subscribe to any change in a localStorage token, so I could only update the localStorage in a function and let i18next apdapt?

Seems zustand uses react hooks... and the language-detector knows nothing about react...

Based on a short google search, it seems replacing const { locale } = userStore(); with const { locale } = userStore.getState(); works

https://codesandbox.io/s/blissful-river-3h1hu?file=/src/i18n/index.ts:355-395

It works! Well played! Thank you so much for your time and your help @adrai!

EDIT: if anyone has a the same typescript issue, just write:

  type: "languageDetector" as LanguageDetectorAsyncModule["type"],
Was this page helpful?
0 / 5 - 0 ratings

Related issues

Jessidhia picture Jessidhia  Â·  4Comments

dawsbot picture dawsbot  Â·  4Comments

ChCosmin picture ChCosmin  Â·  4Comments

nicholasmaddren picture nicholasmaddren  Â·  4Comments

aniket-dalvi picture aniket-dalvi  Â·  4Comments