skip to Main Content

I’m trying to fetch translation JSONs from an API for my Next app that uses the /app directory. It all works fine on the server-side, since I can await the API call and pass the data globally as a "hook".
I’ve managed to do something similar on the client components using a Context, but there is a delay between the loading of the page and loading the translations. There’s also no simple way for me to cache the data, like it’s cached on the server side.

Is there a way to fetch this data on the server side (maybe in layout.tsx or elsewhere) and have it accessible globally for the client components? Similarly to how it worked with getInitialProps / getServerSideProps before.

Here’s my clientside context:

'use client';

import React, { createContext, useCallback, useEffect, useState } from 'react';
import { getActiveBrand } from '../actions/actions';
import { defaultLocale } from './translationProvider';

interface TranslationContextProps {
  translateProvider: (
    locale?: string
  ) => (path: string, templates?: { [key: string]: string }) => string;
}

const TranslationContext = createContext<TranslationContextProps>(
  {} as TranslationContextProps
); // ts hack :/

interface AuthProviderProps {
  children: React.ReactNode | React.ReactNode[];
}

const TranslationProvider = ({ children }: AuthProviderProps) => {
  const [translations, setTranslations] = useState(
    null as { [key: string]: any } | null
  );

  const translateProvider = useCallback(
    (locale?: string) => {
      const lang = locale || defaultLocale;
      const translation = translations ? translations[lang] : {};

      const translate = (
        path: string,
        templates?: { [key: string]: string }
      ) => {
        if (translation !== null) {
          const keys = path.split('.');

          let value: string;
          try {
            value = keys.reduce((a, c) => a[c], translation);
            if (templates && typeof value === 'string')
              Object.keys(templates).forEach(key => {
                value = value.replace(`{${key}}`, `${templates[key]}`);
              });
          } catch (e: any) {
            return path;
          }
          return value;
        }
        return path;
      };
      return translate;
    },
    [translations]
  );

  const getTranslations = async () => {
    const brand = await getActiveBrand();
    const languages = {} as { [key: string]: any };
    brand.data.languages.forEach(lang => {
      languages[lang.language] = JSON.parse(JSON.parse(lang.languageData));
    });
    setTranslations(languages);
  };

  useEffect(() => {
    getTranslations();
  }, []);

  return (
    <TranslationContext.Provider
      value={{
        translateProvider,
      }}
    >
      {children}
    </TranslationContext.Provider>
  );
};

export { TranslationProvider, TranslationContext };

And my server side "hook":

import { getActiveBrand } from '../actions/actions';

export const availableLanguages = ['en', 'de', 'cs', 'sk', 'sl', 'hu'];
export const defaultLocale = 'de';

export async function translationProvider(lang: string) {
  const brand = await getActiveBrand();
  const languageRemote = brand.data.languages.find(
    l => l.language === lang
  )?.languageData;

  const translation = languageRemote
    ? JSON.parse(JSON.parse(languageRemote))
    : null;

  const t = (path: string, templates?: { [key: string]: string }): string => {
    if (translation !== null) {
      const keys = path.split('.');
      let value: string;
      try {
        value = keys.reduce((a, c) => a[c], translation);
        if (templates && typeof value === 'string')
          Object.keys(templates).forEach(key => {
            value = value.replace(`{${key}}`, `${templates[key]}`);
          });
      } catch (e: any) {
        return path;
      }
      return value;
    }
    return path;
  };

  return t;
}

2

Answers


  1. Chosen as BEST ANSWER

    Solved, with enormous thanks to @nordic70!

    Essentially, I created a context to pass down the translation content:

    'use client';
    
    import { createContext } from 'react';
    
    interface TranslationContextProps {
      translation: any; // probably should be a type, not sure
    }
    
    const TranslationContext = createContext<TranslationContextProps>({} as TranslationContextProps); // ts hack :/
    
    // VERY IMPORTANT!
    // If you export just the provider as
    // export const TranslationProvider = TranslationContext.Provider;
    // it will not work!
    // you also cannot use it just as <TranslationContext.Provider>...</TranslationContext.Provider>
    // it HAS to be a component like this one:
    export const TranslationProvider = ({
      children,
      translation,
    }: {
      translation: any;
      children: React.ReactNode | React.ReactNode[];
    }) => <TranslationContext.Provider value={{ translation: translation }}>{children}</TranslationContext.Provider>;
    
    export default TranslationContext;
    

    In your layout.tsx:

    // this is some pseudo code, you get the idea
    const Layout = async ({children, params}) => {
      const translationJson = await callYourApi();
      //OR I did this for easy access in all server components:
      const { t, translation } = await translationProvider(params.locale);
      return (
          ...
          <TranslationProvider translation={translation}>
              {children}
          </TranslationProvider>
          ...
      );
    }
    

    Here's the translationProvider function:

    export async function translationProvider(lang: string) {
      // this is the fetch call to the API
      const translation = await getLanguageJson(lang);
    
      const t = (path: string, templates?: { [key: string]: string }): string => {
        if (Boolean(translation)) {
          const keys = path.split('.');
    
          let value: string;
          try {
            value = keys.reduce((a, c) => a[c], translation);
            if (templates && typeof value === 'string')
              Object.keys(templates).forEach((key) => {
                value = value.replace(`{${key}}`, `${templates[key]}`);
              });
          } catch (e: any) {
            return path;
          }
          return value;
        }
        return path;
      };
    
      return { t, translation };
    }
    

    Now, for the client components, I made a similar function, where instead of calling the API, you take the active translation from the context:

    export function useTranslationProvider() {
      const { translation } = useContext(TranslationContext);
      
      const t = (path: string, templates?: { [key: string]: string }): string => {
        if (Boolean(translation)) {
          const keys = path.split('.');
    
          let value: string;
          try {
            value = keys.reduce((a, c) => a[c], translation);
            if (templates && typeof value === 'string')
              Object.keys(templates).forEach((key) => {
                value = value.replace(`{${key}}`, `${templates[key]}`);
              });
          } catch (e: any) {
            return path;
          }
          return value;
        }
        return path;
      };
    
      return { t };
    }
    

    And then you have access to the translations you got from the API in client components!

     const { t } = useTranslationProvider();
    

    Big thanks to @nordic70 again, his answer saved me countless hours. Hope this will help more people having the same issues.


  2. I would create a context provider client component called within a server component that gets the data. Than pass the data from the server component as prop into the context provider.

    Some pseudo code.

    export default async function MyLayout({ children }) {
       const trans = await getTranslations(); // React server component
       return (
          <TranslationProvider translations={trans}>{children}</TranslationProvider>
       )
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search