RevereCRE / relay-nextjs

⚡️ Relay integration for Next.js apps
https://reverecre.github.io/relay-nextjs/
MIT License
253 stars 29 forks source link

Issue with react-i18next / next-i18next #66

Open MoOx opened 2 years ago

MoOx commented 2 years ago

I am trying to use withRelay on a page that need preloading of info (classic /account/edit where I want fields to be filled with user data upfront). So I started converting a simple page by adding withRelay required logic.

I started with this (working, but no query to prefill)

import page from "../../src/pages/PageAccountEdit.bs.js";
import { node as pageQuery } from "../../src/__generated__/PageAccountEdit_getUser_Query_graphql.bs.js";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

import { getSession } from "next-auth/react";

export default page;
export async function getServerSideProps(context) {
  const session = await getSession(context);
  if (session === null) {
    return {
      redirect: {
        destination: "/auth/login?next=" + encodeURIComponent(context.pathname),
        permanent: false,
      },
    };
  }

  return {
    props: {
      session,
      ...(await serverSideTranslations(context.locale, ["common"])),
    },
  };
}

to this (same page, with a new query and withRelay call)

import page from "../../src/pages/PageAccountEdit.bs.js";
import { node as pageQuery } from "../../src/__generated__/PageAccountEdit_getUser_Query_graphql.bs.js";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

import { getSession } from "next-auth/react";

async function getServerSideProps(context) {
  const session = await getSession(context);
  if (session === null) {
    return {
      redirect: {
        destination: "/auth/login?next=" + encodeURIComponent(context.pathname),
        permanent: false,
      },
    };
  }

  return {
    props: {
      session,
      ...(await serverSideTranslations(context.locale, ["common"])),
    },
  };
}

import { withRelay } from "relay-nextjs";
import * as RelayEnvClient from "../../src/RelayEnvClient.bs";
import * as RelayEnvServer from "../../src/RelayEnvServer.bs";

export default withRelay(page, pageQuery, {
  createClientEnvironment: () => RelayEnvClient.getClientEnvironment(),
  serverSideProps: getServerSideProps,
  createServerEnvironment: async (ctx, serverSideProps) => {
    return RelayEnvServer.createServerEnvironment(
      serverSideProps.props.session
    );
  },
});

I am getting an warning in the browser

react-i18next:: You will need to pass in an i18next instance by using initReactI18next

And the translations are not available. What is weird is that my getServerSideProps is exactly the same, export serverSideTranslations properly. Any idea why withRelay might conflict somehow with next-i18next ?

MoOx commented 2 years ago

FYI, getInitialProps used here is causing trouble to next-i18next (see details here) poke @isaachinman

Does any of you see a way to be able to interact with each other ? I would love to be able to use both of you libraries together !

MoOx commented 2 years ago

For those looking for something good as I am writing this, I encourage you to have a look to this https://github.com/vercel/next.js/pull/33892

isaachinman commented 2 years ago

To be frank, getInitialProps only still exists because a lot of older packages/utils rely on it, unfortunately. Its use should be phased out.

FINDarkside commented 2 years ago

That's not true, getInitiaProps is still used because it's superior to the alternatives. More detailed issues of getServerSideProps here: https://github.com/RevereCRE/relay-nextjs/issues/61#issuecomment-1110674697

I've personally hacked next-i18next to work with getInitialProps, but only for the features which we use in our app.

isaachinman commented 2 years ago

"Superior" is a subjective measure – it's not the direction the framework is headed in.

rrdelaney commented 2 years ago

This library uses getInitialProps because it's impossible to implement certain patterns using getServerSideProps. Those issues are being addressed in the Next.js router RFC, but if we waited for that our users would have a bad experience until it's rolled out. Once that changes + Next.js integrates server components I imagine a few things will change and we'll re-evaluate our approach. Regrading react-i18next, we don't have a need to use that library at Revere so I probably won't be writing up a guide on how to properly integrate it, as I would not be able to find all the edge cases + what real-world usage looks like, happy to add something to the docs if someone writes something up though!

mortezaalizadeh commented 1 year ago

I managed to get everything working using relay-nextjs except making it work with i18next. Has anyone found a workaround that can make it work?

rrdelaney commented 1 year ago

@mortezaalizadeh relay-nextjs doesn't currently support getServerSideProps which is required by react-i18next. The best workaround right now is to put translation data in your GraphQL API, query that as part of your page's query, and pass that data to the translation library.

FINDarkside commented 1 year ago

I've found it simpler to just to call serverSideTranslations in getInitialProps and then "cache" it with useState in _app so that we don't lose translations when navigating. This simple solution only works if you can load all translations on first pageload (not using page specific namespaces) and if you are fine with full refresh when user changes language.

I'm using next-i18next though, but I think if you're using only i18next you can solve it the same way. I have this kind of helper function. You just call withTranslations(PageComponent) and the page will have translations.

export default function withTranslations<T>(Page: NextPageWithLayout<T>) {
  const originalGetInitialProps = Page.getInitialProps;
  // Load translations in getInitialProps if running on server
  Page.getInitialProps = async (ctx) => {
    const originalProps = originalGetInitialProps
      ? await originalGetInitialProps(ctx)
      : {};

    if (typeof window === 'undefined') {
      const getTranslations = (
        await import('src/util/getTranslations')
      ).default;
      const translationProps = await getTranslations({
        locale: ctx.locale || 'en',
      });
      return {
        ...originalProps,
        ...translationProps,
      };
    }

    return originalProps;
  };

  return Page;
}

You can implement getTranslations function however you like depending on how your translations are stored. Note that you can call any backend only code inside getTranslations so you can just call whatever you'd call inside getServerSideProps. As noted, the difference is that with the above solution translations are only loaded in the initial full page load instead of every navigation.

mo-rally-dev commented 1 year ago

Hi, I just ran into this issue and I was able to fix it by following this: https://github.com/i18next/next-i18next/issues/1917#issuecomment-1574295163

I just had to pass

import i18nextConfig from '../../next-i18next.config';
...
export default appWithTranslation(MyApp, i18nextConfig);
rrdelaney commented 1 year ago

@mo-rally-dev Thank you for continuing to look into this! Would there be any way to see a full example of how your app is working with i18n? Would be great to put something in the docs.

mo-rally-dev commented 1 year ago

Hey @rrdelaney ,

I sadly can't share my app as it's a private one but here's my next-i18next.config.js file.

const HttpBackend = require('i18next-http-backend/cjs');
const ChainedBackend = require('i18next-chained-backend').default
const LocalStorageBackend = require('i18next-localstorage-backend').default
const yaml = require('js-yaml');

const isBrowser = typeof window !== 'undefined'
const isDev = process.env.NODE_ENV === 'development'

// @ts-check

/**
 * @type {import('next-i18next').UserConfig}
 */
module.exports = {
  debug: isDev,
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'fr', 'es', 'it', 'ro'],
  },
  localePath:  isBrowser ? '/lang' : require('path').resolve('./public/lang'),
  localeExtension: 'yml',
  reloadOnPrerender: isDev,
  lng: 'en',
  ns: ['app', 'research'],
  defaultNS: 'app',
  interpolation: {
    escapeValue: false,
  },
  serializeConfig: false,
  use: isBrowser ? [ChainedBackend] : [],
  backend: {
    // For the client side, we save the translations in localstorage and fetch them if they are missing
    backends: isBrowser ? [LocalStorageBackend, HttpBackend] : [],
    backendOptions: [
      {
        prefix: 'test_i18next_',
        expirationTime: isDev ? 0 : 60 * 60 * 1000, // 1 hour
        store: isBrowser ? window.localStorage : null
      },
      {
        loadPath: '/lang/{{lng}}/{{ns}}.yml',
        parse: function(data) { return yaml.load(data); },
      },
    ],
  },
};

I am storing my translations in the public folder under public/lang/<languageCode>/<namspace>.yml.

Server side rendering works because I am calling serverSideTranslations in my withRelay method for the serverSideProps. Here's a snippet of the code block. You might need to return { props: ...(await etc)} based on your setup.

if (typeof window === 'undefined') {
      const { serverSideTranslations } = await import(
        'next-i18next/serverSideTranslations'
      );

      const locale = context.locale; // TODO: when we are ready plug in this code below: (locale ?? 'en')
      const namespaces = ['app', ...additionalLocaleNamespacesToLoad];

      return {
        ...(await serverSideTranslations(
          'en',
          namespaces,
          null,
          SUPPORTED_LANGUAGES
        )),
      };
    }

Hope this helps!