DEV Community

Cover image for Usar next-i18next & NextAuth.js en la misma aplicación de NextJS
Ángel Quiroz
Ángel Quiroz

Posted on • Updated on

Usar next-i18next & NextAuth.js en la misma aplicación de NextJS

Este es mi primer tutorial, la intención de este es facilitar la vida y ahorrar el tiempo de quien necesite implementar estas tecnologías en un mismo proyecto, a mí me costó un poco implementarlo y quiero compartirlo con quien lo pueda necesitar.
Espero les sirva...

1. Creamos un nuevo proyecto como indica la documentación.

Lo voy a hacer con Typescript pero con Javascript los pasos son los mismos.

npx create-next-app@latest --typescript
Enter fullscreen mode Exit fullscreen mode

Escribes el nombre de tu proyecto y esperas a que se instalen las dependencias, luego ejecutas VSCode o tu editor de código en la carpeta del proyecto.
Ahora en la terminal escribes

npm run dev
Enter fullscreen mode Exit fullscreen mode

Esto iniciará el servidor de desarrollo en el puerto 3000.

Initial App

2. Instalamos y configuramos next-i18next

Como indica el repositorio del proyecto ejecutamos

yarn add next-i18next 
# Si usas NPM ejecuta:
npm i --save next-i18next
Enter fullscreen mode Exit fullscreen mode

Ahora creamos dos carpetas con archivos json para probar las traducciones:

└─ public
   └ locales
     ├─en
     | └─ common.json
     └─es
       └─ common.json
Enter fullscreen mode Exit fullscreen mode

Después creamos un archivo next-i18next.config.js en la raíz del proyecto con el siguiente contenido:

module.exports = {
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'es'],
  },
};
Enter fullscreen mode Exit fullscreen mode

Luego en el archivo next.config.js importamos la configuración:

/** @type {import('next').NextConfig} */
const { i18n } = require("./next-i18next.config");
const nextConfig = {
  reactStrictMode: true,
  i18n,
};

module.exports = nextConfig;
Enter fullscreen mode Exit fullscreen mode

Finalmente editamos el archivo pages/_app.tsx para ejecutar la aplicación con traducciones, el cambio se hace en la última línea.

import '../styles/globals.css';
import type { AppProps } from 'next/app';
import { appWithTranslation } from 'next-i18next';

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

export default appWithTranslation(MyApp);
Enter fullscreen mode Exit fullscreen mode

3. Agregamos y probamos las traducciones.

En los archivos public/locales/en/common.json y public/locales/es/common.json agregamos las traducciones de esta manera:
public/locales/en/common.json

{
  "welcome": "Welcome to the world of Next.js!",
  "content": "This is the blog index page"
}
Enter fullscreen mode Exit fullscreen mode

public/locales/en/common.json

{
  "welcome": "Bienvenido al mundo de Next.js!",
  "content": "Esta es la página de inicio del blog"
}
Enter fullscreen mode Exit fullscreen mode

Ahora, este paso es importante, para que las traducciones funcionen correctamente, deben ser llamadas desde el servidor en todos los componentes a nivel de página para que este realice la "hidratación" de manera correcta, en este caso editamos nuestro archivo pages/index.tsx para pasar las traducciones, para poder llamar cada traducción de manera individual usamos

const { t } = useTranslation('common');
Enter fullscreen mode Exit fullscreen mode

El archivo en este caso queda así:

import type { NextPage, NextPageContext } from 'next';
import Head from 'next/head';
import Image from 'next/image';
import styles from '../styles/Home.module.css';

const Home: NextPage = () => {
  const { t } = useTranslation('common');
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>{t('welcome')}</h1>

        <p className={styles.description}>{t('content')}</p>
      </main>

      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{' '}
          <span className={styles.logo}>
            <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
          </span>
        </a>
      </footer>
    </div>
  );
};

export default Home;
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'next-i18next';

export async function getStaticProps({ locale }: NextPageContext) {
  return {
    props: {
      ...(await serverSideTranslations(locale || 'en', ['common'])),
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

El resultado es la página traducida con el idioma detectado en el sistema.
Translated Page

Si queremos probar manualmente el cambio de idioma hay que cambiar la configuración, además agregaré otra línea que servirá para subir nuestro proyecto a Vercel, el archivo queda así:

const path = require('path');

module.exports = {
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'es'],
    localeDetection: false,
    localePath: path.resolve('./public/locales'), // for deployment on Vercel
  },
};
Enter fullscreen mode Exit fullscreen mode

También crearemos un boton para hacer el cambio de idioma de manera manual, el archivo pages/index.tsx quedaría así:

import type { NextPage, NextPageContext } from 'next';
import Head from 'next/head';
import Image from 'next/image';
import styles from '../styles/Home.module.css';
import { useRouter } from 'next/router';

const Home: NextPage = () => {
  const router = useRouter();
  const { pathname, asPath, query } = router;
  const { t } = useTranslation('common');
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>{t('welcome')}</h1>

        {/* Language Change Button */}
        <button
          type="button"
          onClick={() => {
            router.push({ pathname, query }, asPath, {
              locale: router.locale === 'es' ? 'en' : 'es',
            });
          }}
        >
          {router.locale === 'es' ? 'English' : 'Español'}
        </button>

        <p className={styles.description}>{t('content')}</p>
      </main>

      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{' '}
          <span className={styles.logo}>
            <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
          </span>
        </a>
      </footer>
    </div>
  );
};

export default Home;
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'next-i18next';

export async function getStaticProps({ locale }: NextPageContext) {
  return {
    props: {
      ...(await serverSideTranslations(locale || 'en', ['common'])),
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

De esta forma ya tenemos nuestra aplicación trabajando perfectamente con dos idiomas basados en la ruta

4. Configuramos la autenticación con next-auth

Primero instalamos el paquete:

npm i --save next-auth
Enter fullscreen mode Exit fullscreen mode

Para propósitos de prueba usaremos credenciales (email y contraseña), además haremos una validación sencilla, el hacerla de manera correcta depende de lo que quieras en tu proyecto, si lo comentas puedo hacer un tutorial explicando cómo hacer la autenticación con diferentes proveedores y usando una página de inicio customizable. Dicho eso seguimos...

Creamos un archivo pages/api/auth/[...nextauth].ts
Con el siguiente contenido:

import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';

export default NextAuth({
  providers: [
    CredentialsProvider({
      name: 'credentials',
      credentials: {
        email: {
          label: 'Email',
          type: 'email',
          placeholder: 'email@example.com',
        },
        password: {
          label: 'Password',
          type: 'password',
          placeholder: '********',
        },
      },
      async authorize(credentials) {
        if (credentials && credentials.email && credentials.password) {
          if (
            credentials.email === 'test@test.com' &&
            credentials.password === 'test'
          ) {
            return {
              email: credentials.email,
              image: 'https://i.pravatar.cc/500',
              name: 'Test User',
            };
          }
        }
        return null;
      },
    }),
  ],
  callbacks: {
    jwt: async ({ token }) => {
      return token;
    },
    session: ({ session, token }) => {
      if (token) {
        session.id = token.id;
      }
      return session;
    },
  },
  secret: 'my-secret',
  jwt: {
    secret: 'my-secret',
    maxAge: 60 * 60 * 24 * 30,
  },
});
Enter fullscreen mode Exit fullscreen mode

Luego aditamos el archivo pages/index.tsx para tener un botón de autenticación o de logout además del cambio de idioma.

import type { NextPage, NextPageContext } from 'next';
import Head from 'next/head';
import Image from 'next/image';
import styles from '../styles/Home.module.css';
import { useRouter } from 'next/router';
import { useSession, signIn, signOut } from 'next-auth/react';

const Home: NextPage = () => {
  const router = useRouter();
  const { pathname, asPath, query } = router;
  const { t } = useTranslation('common');
  const { data: session } = useSession();

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>{t('welcome')}</h1>

        {/* Language Change Button */}
        <button
          type="button"
          onClick={() => {
            router.push({ pathname, query }, asPath, {
              locale: router.locale === 'es' ? 'en' : 'es',
            });
          }}
        >
          {router.locale === 'es' ? 'English' : 'Español'}
        </button>

        {/* Authentication Button */}
        {session ? (
          <>
            <h4>
              {t('welcome')} {JSON.stringify(session.user)}
            </h4>
            <button
              type="button"
              onClick={() => {
                signOut();
              }}
            >
              {t('signOut')}
            </button>
          </>
        ) : (
          <button
            type="button"
            onClick={() => {
              signIn();
            }}
          >
            {t('signIn')}
          </button>
        )}

        <p className={styles.description}>{t('content')}</p>
      </main>

      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{' '}
          <span className={styles.logo}>
            <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
          </span>
        </a>
      </footer>
    </div>
  );
};

export default Home;
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'next-i18next';

export async function getStaticProps({ locale }: NextPageContext) {
  return {
    props: {
      ...(await serverSideTranslations(locale || 'en', ['common'])),
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

Ahora lo más importante, seguramente la razón por la que vienes a este post, y personalmente lo que más me costó solucionar, solo hay que hacer un pequeño cambio en el archivo pages/_app.tsx para que el SessionProvider no choque con el Provider de Traducción:

import '../styles/globals.css';
import type { AppProps } from 'next/app';
import { appWithTranslation } from 'next-i18next';
import { SessionProvider } from 'next-auth/react';

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

const AppWithI18n = appWithTranslation(MyApp);

const AppWithAuth = (props: AppProps) => (
  <SessionProvider session={props.pageProps.session}>
    <AppWithI18n {...props} />
  </SessionProvider>
);

export default AppWithAuth;
Enter fullscreen mode Exit fullscreen mode

¡Y listo!, ahora podemos cambiar el idioma de nuestra página y tener la autenticación en la misma aplicación de NextJS.

Auth in English

Auth in Spanish

Espero haberte ayudado, si tienes alguna observación o duda déjala en los comentarios para ayudar a más desarrolladores.

Latest comments (0)