Simple Internationalization (i18n) for Nextjs (App Router) with Server Actions and cookies
Tags: nextjs
Published: 2023-09-21 01:11:35

I write a simple implementation for translations using only server actions and cookies in Next.js 13 with App Router and Server Actions enabled.

If do you want the source code:

[Github Repo](https://aquiroz.dev/i18n-repo)

First, enable experimental Server Actions feature in `next.config.js`:
```js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverActions: true,
  },
};

module.exports = nextConfig;
```

Then, create a simple file with translations:
`src/lib/i18n.ts`
```ts
export enum Locale {
  en = "en",
  es = "es",
}

export const DEFAULT_LOCALE = Locale.en;

export const translations = {
  [Locale.en]: {
    home: {
      title: "Simple i18n example with Next.js Server Actions",
      paragraph:
        "This example provides a simple i18n implementation with Next.js Server Actions using cookies.",
      button: "Join the waitlist",
      subtitle: "Join the waitlist to get early access to the app.",
      terms: "Terms & Conditions",
      placeholder: "Enter your email",
    },
    buttons: {
      en: "English",
      es: "Spanish",
    },
  },
  [Locale.es]: {
    home: {
      title: "Ejemplo simple de i18n con Next.js Server Actions",
      paragraph:
        "Este ejemplo proporciona una implementación simple de i18n con Next.js Server Actions usando cookies.",
      button: "Únete a la lista de espera",
      subtitle:
        "Únete a la lista de espera para obtener acceso anticipado a la aplicación.",
      terms: "Términos y Condiciones",
      placeholder: "Ingresa tu email",
    },
    buttons: {
      en: "Inglés",
      es: "Español",
    },
  },
};

export type TranslationKey = keyof (typeof translations)[Locale];
```
Next, create a file for server action for set cookie and redirect to home:
`src/app/actions.ts`
```ts
"use server";

import { DEFAULT_LOCALE, Locale } from "@/lib/i18n";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

export async function changeLocale(formData: FormData) {
  const localeValue = formData.get("locale");
  if (!localeValue || typeof localeValue !== "string") {
    return redirect("/");
  }

  const locale = localeValue as Locale;
  if (!(locale in Locale)) {
    cookies().set("locale", DEFAULT_LOCALE);
    return redirect("/");
  }

  cookies().set("locale", localeValue);
  return redirect("/");
}

```

Finally, read cookies for get translation in any page:
`src/app/page.tsx`
```ts
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import Link from "next/link";
import { changeLocale } from "./actions";
import { cookies } from "next/headers";
import { Locale, translations } from "@/lib/i18n";

import type { Metadata, ResolvingMetadata } from "next";

type Props = {
  params: { id: string };
  searchParams: { [key: string]: string | string[] | undefined };
};

export async function generateMetadata(
  { params, searchParams }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  const cookieStore = cookies();
  const locale = (cookieStore.get("locale")?.value || "en") as Locale;

  return {
    title: translations[locale].home.title,
  };
}

export default function Home() {
  const cookieStore = cookies();
  const locale = (cookieStore.get("locale")?.value || "en") as Locale;
  return (
    <main className="w-full h-screen py-12 md:py-24 lg:py-32 xl:py-48 bg-black">
      <div className="container px-4 md:px-6">
        <div className="grid gap-6 items-center">
          <div className="flex flex-col justify-center space-y-4 text-center">
            <div className="space-y-2">
              <h1 className="text-3xl font-bold tracking-tighter  sm:text-5xl xl:text-6xl/none bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-500">
                {translations[locale].home.title}
              </h1>
              <p className="max-w-[600px] text-zinc-200 md:text-xl dark:text-zinc-100 mx-auto">
                {translations[locale].home.paragraph}
              </p>
            </div>
            <div className="w-full max-w-sm space-y-2 mx-auto">
              <form className="flex flex-col sm:flex-row items-center gap-2">
                <Input
                  className="max-w-lg flex-1 bg-gray-800 text-white border-gray-900"
                  placeholder={translations[locale].home.placeholder}
                  type="email"
                />
                <Button className="bg-white text-black" type="submit">
                  {translations[locale].home.button}
                </Button>
              </form>
              <p className="text-xs text-zinc-200 dark:text-zinc-100 space-x-2">
                <span>{translations[locale].home.subtitle}</span>
                <Link
                  className="underline underline-offset-2 text-white"
                  href="/terms">
                  {translations[locale].home.terms}
                </Link>
              </p>
              <form action={changeLocale} method="post">
                <input type="hidden" name="locale" value="en" />
                <button type="submit">{translations[locale].buttons.en}</button>
              </form>
              <form action={changeLocale} method="post">
                <input type="hidden" name="locale" value="es" />
                <button type="submit">{translations[locale].buttons.es}</button>
              </form>
            </div>
          </div>
        </div>
      </div>
    </main>
  );
}

```

I using [shadcn/ui](https://ui.shadcn.com) for UI Components.

The results are in [this url](https://nextjs-i18n-server-actions.vercel.app):


![English](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/upwdvdtvatp3icp5mbhw.png)


![Spanish](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o0hu8yuzpkn6p7yl628p.png)