Executando verificação de segurança...
16

Internacionalizando seu Aplicativo Next.js com Next-intl

Recentemente, embarquei em uma jornada de aprimoramento do meu portfólio online, construído com Next.js. A experiência, embora produtiva, revelou um desafio inesperado: a crescente quantidade de textos diretamente no HTML. A preocupação com a desorganização e a dificuldade de manutenção a longo prazo me levou a buscar uma solução mais robusta e eficiente.
Inspirado pela minha experiência com internacionalização (i18n) em projetos corporativos, decidi aplicar essa prática ao meu portfólio. Foi então que descobri o Next-intl, uma biblioteca de i18n poderosa e flexível, projetada especificamente para o Next.js.

Next-intl: Um Aliado na Organização e Eficiência

O Next-intl se destacou como a solução ideal para o meu projeto, oferecendo uma série de vantagens que otimizaram o desempenho, simplificaram o processo de internacionalização e proporcionaram uma experiência de desenvolvimento mais agradável.

Por que o Next-intl?

  • Desempenho Otimizado: Carrega apenas os dados de idioma necessários, minimizando o impacto no desempenho do aplicativo.
  • Facilidade de Uso: API intuitiva e fácil de usar, tornando a implementação de i18n acessível mesmo para iniciantes no Next.js.
  • Suporte Abrangente: Recursos avançados de formatação de mensagens, incluindo suporte a plurais, formatação de números e datas, e traduções personalizadas.
  • Integração Perfeita: Integra-se perfeitamente com o ecossistema Next.js, aproveitando seus recursos de roteamento e renderização do lado do servidor (SSR).
  • Organização e Manutenção: Facilita a organização de arquivos de tradução, simplificando a manutenção e atualização do conteúdo multilíngue.

Implementação Passo a Passo

A documentação do Next-intl é clara e concisa, facilitando a implementação da biblioteca em projetos Next.js com App Router. Para o meu portfólio, segui os seguintes passos:

  1. Instalação: Instalei a biblioteca com o comando npm install next-intl.
  2. Estrutura de Pastas: Organizei os arquivos de tradução em uma pasta public/locales, com arquivos en.json e pt.json para inglês e português, respectivamente.
├── public
│   ├── locales
│       ├── en.json
│       ├── pt.json
├── next.config.ts
└── src
    ├── i18n
    │   ├── routing.ts
    │   ├── navigation.ts
    │   └── request.ts
    ├── middleware.ts
    └── app
        └── [locale]
            ├── layout.tsx
            └── page.tsx
  1. Configuração do next.config.ts: Adicionei o plugin createNextIntlPlugin para habilitar o Next-intl no projeto.
import { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";

const nextConfig: NextConfig = {};

const withNextIntl = createNextIntlPlugin();
export default withNextIntl(nextConfig);
  1. Criação dos Arquivos de Configuração: Criei os arquivos routing.ts, navigation.ts, middleware.ts e request.ts dentro da pasta src/i18n para configurar o roteamento, a navegação, o middleware e as requisições da biblioteca.

    src/i18n/routing.ts

    import { defineRouting } from "next-intl/routing";
    
    export const routing = defineRouting({
      // A list of all locales that are supported
      locales: ["pt", "en"],
    
      // Used when no locale matches
      defaultLocale: "pt",
    });
    
    export type Locale = (typeof routing.locales)[number];
    

    src/i18n/navigation.ts

    import { createNavigation } from "next-intl/navigation";
    import { routing } from "./routing";
    
    export const { Link, redirect, usePathname, useRouter, getPathname } =
      createNavigation(routing);
    

    src/middleware.ts

    import createMiddleware from "next-intl/middleware";
    import { routing } from "./i18n/routing";
    
    export default createMiddleware(routing);
    
    export const config = {
      // Match only internationalized pathnames
      matcher: ["/", "/(pt|en)/:path*"],
    };
    

    src/i18n/request.ts

    import { getRequestConfig } from "next-intl/server";
    import { Locale, routing } from "./routing";
    
    export default getRequestConfig(async ({ requestLocale }) => {
      // This typically corresponds to the `[locale]` segment
      let locale = await requestLocale;
    
      // Ensure that a valid locale is used
      if (!locale || !routing.locales.includes(locale as Locale)) {
        locale = routing.defaultLocale;
      }
    
      return {
        locale,
        messages: (await import(`../../public/locales/${locale}.json`)).default,
      };
    });
    
  2. Configuração do src/app/[locale]/layout.tsx: Integrei o NextIntlClientProvider para fornecer as mensagens de tradução aos componentes do cliente.

    import { NextIntlClientProvider } from "next-intl";
    import { getMessages } from "next-intl/server";
    import { notFound } from "next/navigation";
    import { Locale, routing } from "@/i18n/routing";
    import Header from "@/components/Header";
    import "./globals.css";
    import { ThemeProvider } from "@/components/ThemeProvider";
    
    export default async function LocaleLayout({
      children,
      params,
    }: {
      children: React.ReactNode;
      params: Promise<{ locale: string }>;
    }) {
      // Ensure that the incoming `locale` is valid
      const { locale } = await params;
      if (!routing.locales.includes(locale as Locale)) {
        notFound();
      }
    
      // Providing all messages to the client
      // side is the easiest way to get started
      const messages = await getMessages();
    
      return (
        <html lang={locale} suppressHydrationWarning>
          <title>Giovana</title>
          <body className="bg-neutral-200 dark:bg-neutral-900 text-neutral-800 dark:text-neutral-200">
            <ThemeProvider
              attribute="class"
              defaultTheme="system"
              enableSystem
              disableTransitionOnChange
            >
              <NextIntlClientProvider messages={messages}>
                <Header />
                {children}
              </NextIntlClientProvider>
            </ThemeProvider>
          </body>
        </html>
      );
    }
    
  3. Utilização do useTranslations em src/app/[locale]/page.tsx: Utilize o hook useTranslations para acessar as traduções nos componentes da página.

    import { useTranslations } from "next-intl";
    import Skills from "@/components/Skills";
    import { skills } from "@/constants/skills";
    import AboutSection from "./_components/aboutSection";
    import ContactSection from "./_components/contactSection";
    import ExperienceSection from "./_components/experienceSection";
    import ProjectSection from "./_components/projectSection";
    
    export default function HomePage() {
      const t = useTranslations("home");
    
      return (
        <div>
          <main className="flex flex-col p-5 gap-4 justify-center items-center">
            <div className="flex flex-col gap-4 w-full md:w-1/2 ">
              <section id="home" className="flex flex-col gap-4">
                <h1 className="font-semibold text-md">{t("title")}</h1>
                <p className="text-neutral-500 text-md">{t("about")}</p>
                {...}
    
  4. Com os passos anteriores já temos o projeto com a internacionalização funcionando, mas como podemos mudar o idioma sem precisar mexer diretamente na URL? Bom podemos fazer um componente pra isso

"use client";

import {
  Select,
  SelectTrigger,
  SelectValue,
  SelectContent,
  SelectItem,
} from "@/components/ui/select";
import { usePathname, useRouter } from "@/i18n/navigation";
import { Locale, routing } from "@/i18n/routing";
import { GlobeIcon } from "lucide-react";
import { useLocale } from "next-intl";
import { useParams } from "next/navigation";

export default function Component() {
  const router = useRouter();
  const locale = useLocale();

  const pathname = usePathname();
  const params = useParams();

  function onSelectChange(nextLocale: string) {
    router.replace(
      // @ts-expect-error -- TypeScript will validate that only known `params`
      // are used in combination with a given `pathname`. Since the two will
      // always match for the current route, we can skip runtime checks.
      { pathname, params },
      { locale: nextLocale as Locale }
    );
  }

  return (
    <Select defaultValue={locale} onValueChange={onSelectChange}>
      <SelectTrigger
        className="w-[95px] h-8 border-none bg-transparent focus:ring-0 focus:ring-offset-0"
        aria-label={"select a locale"}
      >
        <GlobeIcon />
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        {routing.locales.map((locale) => (
          <SelectItem key={locale} value={locale}>
            {locale.toUpperCase()}
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  );
}
  1. Agora vamos ver tudo funcionando

Resultados e Considerações Finais

A implementação do Next-intl no meu portfólio online resultou em um projeto mais organizado, eficiente e fácil de manter. A separação dos textos em arquivos de tradução facilitou a gestão do conteúdo multilíngue, enquanto a otimização do desempenho proporcionou uma experiência de usuário mais agradável.

Carregando publicação patrocinada...