DEV Community

NathLowe
NathLowe

Posted on

How to add internationalization to a Next JS 13 application (with React i18n)

Hey there, fellow developers! Today, we're going to embark on a journey to add internationalization to our Next.js 13 application.

Picture this: I was knee-deep in a project that needed internationalization, and like any sensible programmer, I turned to our trusty friend Google for help. Lo and behold, I stumbled upon an article that seemed like the perfect solution for my needs. Eagerly, I decided to give it a shot. However, I encountered a series of diverse errors along the way. Let me clarify, the method presented in that article was actually quite solid, but I felt it needed a bit of tweaking. We're not here to reinvent the wheel, though, so I'll save you the trouble of explaining the entire process. If you're curious, you can check out the method I personalized right here: https://locize.com/blog/next-13-app-dir-i18n/#step-4.

Why React intl for a NextJS project instead of Next intl ?

Instead of using Next Intl, I opted for React i18n. Why, you ask? Well, because in my humble opinion, React i18n is simply the bee's knees! They offer fantastic features like changing the page language via the URL, dividing translation files into namespaces, and, as of now, Next Intl is still in beta.

First Error

So, there I was, all excited to get started, and what do I encounter first? "Translation Not Found" error! Turns out, I had been running Next with TurboPack (also in beta at the time). After nearly an hour of fruitless debugging, I decided to give it a shot with a normal run dev command. Voila! Problem solved! Note to self: TurboPack may not recompile files for every refresh, causing translation issues. But hey, I'm sure the Vercel team will sort it out eventually.

Second Error

Now, brace yourself for the worst error of them all: the infamous React Hydration Error! This one really gave me a run for my money. You see, when using the client version of the useTranslation hook, we need a language value that remains the same on both the server and client sides, at least before hydration kicks in. Now, I had a deep component that I didn't want to burden with passing the language through its parent components. So, I thought, "Why not use a global store?" Cue Zustand, my trusty companion (you could use context or Redux too, by the way).

Initially, I set the language value to the default, let's say "en," and in the RootLayout, I passed the "lng" params to a client component to store it. But alas, here's where things got tricky. When the language switched from "en" during SSR to "fr" before hydration, a new value was created for the translation, causing the Hydration Error to rear its ugly head. But fear not, for I found a way out of this predicament.

Enter the useParams hook of Next.js 13! This magnificent hook retrieves the same params value on both the server and client sides. I utilized it in the component where I wanted to call the useTranslation hook. But let's be real, adding this to every client component needing translation would be quite the hassle. And what if I decide to change the method later? Nightmare material right there! So, I had a bright idea: I added the useParams hook to the useTranslation hook itself. Genius, right? Now, when I call useTranslation, it automatically invokes useParams, and guess what? No need to pass the language anymore! I also did away with all the code related to language selection.

i18n/client.ts

i18next
  .use(initReactI18next)
  .use(LanguageDetector)
  .use(resourcesToBackend((language:string, namespace:string) => import(`../locales/${language}/${namespace}.json`)))
  .init({
    ...getOptions(),
    lng: undefined, // let detect the language on client side
    detection: {
      order: ['path', 'htmlTag', 'cookie', 'navigator'],
    }
  })

const runsOnServerSide = typeof window === 'undefined'

export function useTranslation(ns:string, options={}) {
  const lang = useParams()
  const ret = useTranslationOrg(ns, options)
  const { i18n } = ret
  if (runsOnServerSide && i18n.resolvedLanguage !== lang.lng) {
    i18n.changeLanguage(lang.lng)
  }
  return ret
}
Enter fullscreen mode Exit fullscreen mode

Bug solved! Well, almost.

The Final Boss

Now, let's talk about that sneaky button I had to change the language on the page. It worked like a charm, thanks to the next/link component. The only issue? The pages would be prefetched with the current language, meaning the language wouldn't change for the new page we clicked. And to make matters worse, if we kept changing the language continuously, the client components' translation would get stuck in the previous page's language. Yikes!

But fret not, my fellow adventurers, for I have discovered the solution! I introduced a useEffect hook to keep an eye on any changes in the language params. Alongside that, I added a state to store the i18n instance with its trusty translation function (t). By implementing some clever logic, I ensured that when the language changes on the client side, the instance and its translation function change as well. And voila! The translation persists flawlessly in the client components. Crisis averted!

i18n/client.ts

...
export function useTranslation(ns:string, options={}) {
  const lang = useParams()
  const ret = useTranslationOrg(ns, options)
  const [translation, setTranslation] = useState(ret)
  const { i18n } = translation
  if (runsOnServerSide && i18n.resolvedLanguage !== lang.lng) {
    i18n.changeLanguage(lang.lng)
  }
  useEffect(()=>{
    if(!runsOnServerSide && i18n.resolvedLanguage !== lang.lng){
      i18n.changeLanguage(lang.lng)
    }
    setTranslation(state=>({...state,t:i18n.getFixedT(lang.lng, ns)}) )
  },[lang.lng])
  return translation
}
Enter fullscreen mode Exit fullscreen mode

And just like that, we've come to the end of our thrilling internationalization escapade. I hope you enjoyed our little journey and found some useful tips along the way. Thank you for sticking around till the very end. Until next time, happy coding and may your bugs be few and your jokes be plenty! Cheers!

Top comments (0)