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:
- Instalação: Instalei a biblioteca com o comando
npm install next-intl
. - Estrutura de Pastas: Organizei os arquivos de tradução em uma pasta
public/locales
, com arquivosen.json
ept.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
- 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);
-
Criação dos Arquivos de Configuração: Criei os arquivos
routing.ts
,navigation.ts
,middleware.ts
erequest.ts
dentro da pastasrc/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, }; });
-
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> ); }
-
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> {...}
-
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>
);
}
- 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.