Autor:
    Criação:2026-09-26Última atualização:2026-09-26

    Como internacionalizar sua aplicação Next.js usando Lingui em 2026

    Sumário

    O que é o Lingui?

    O Lingui é uma biblioteca de i18n construída em torno de macros e extração de mensagens. Você escreve o texto de origem diretamente em seus componentes ( t`Hello` , <Trans>Hello</Trans>), o comando lingui extract coleta todas as mensagens em catálogos (arquivos PO por padrão) e um loader os compila para JavaScript compacto. As mensagens utilizam o ICU MessageFormat, e o Lingui suporta React Server Components no App Router.

    Este guia configura o Lingui em um projeto Next.js 16 App Router, com:

    • Macros compiladas por SWC, para que o Turbopack mantenha sua velocidade.
    • Server e Client Components compartilhando a mesma API Trans e useLingui.
    • Roteamento de localidade através de proxy.ts: /about para a localidade padrão, /fr/about para as demais, além de detecção de idioma na primeira visita.
    • Renderização estática de todas as localidades com generateStaticParams.
    • SEO multilíngue completo: generateMetadata traduzido, canonical, hreflang com x-default, localidades Open Graph, JSON-LD, sitemap.ts, robots.ts e páginas 404 localizadas.
    Procurando outra biblioteca? Veja o guia de next-intl, o guia de next-i18next ou o guia de Next.js + Intlayer.
    Usando TanStack Start? Veja o guia de TanStack Start + Lingui. Comparando bibliotecas? Leia Lingui vs Intlayer e next-i18next vs next-intl vs Intlayer.

    O que o benchmark diz sobre o Lingui no Next.js

    O benchmark de i18n executa a mesma aplicação Next.js de 10 páginas e 10 localidades com as principais bibliotecas e mede o que o navegador realmente baixa.

    Carregamento JSON dinâmico

    Carrega as traduções tardiamente em tempo de execução

    JSON com escopo (namespacing)

    Namespaces de tradução por página

    Benchmark de Desempenho I18n

    O que é essa métrica?

    O tamanho total compactado em gzip do pacote da biblioteca de internacionalização. Inclui apenas o provedor e a lógica de recuperação de conteúdo após o tree-shaking e a minificação.

    Por que é importante?

    Um tamanho de biblioteca menor reduz a carga útil inicial de JavaScript, resultando em tempos de download e execução mais rápidos no cliente.

    Ver como

    Principais números para @lingui/core@6.6.0 no Next.js 16, medidos em 2026-09-26 (gzip):

    ConfiguraçãoTamanho da bibliotecaJS por páginaVazamento de outras localidadesVazamento de outras páginas
    Sem i18n (app base)-141.0 KB0%0%
    Lingui, um catálogo por localidade72.1 KB145.4 KB2.8%89.9%
    @intlayer/lingui (compat)10.7 KB221.6 KB50%90%
    next-intlayer (Intlayer nativo)4.9 KB141.5 KB0%0%

    Principais conclusões:

    • Um único catálogo por localidade ainda vaza mensagens de outras páginas para o provedor do cliente. Mantenha o máximo de texto possível em Server Components, que enviam HTML renderizado, não catálogos.
    • O runtime do Lingui pesa ~72 KB gzip. O adaptador de compatibilidade @intlayer/lingui reduz o runtime para ~11 KB, mas neste benchmark a configuração de compatibilidade do Next.js ainda envia catálogos inteiros para a página. A API nativa next-intlayer é a configuração que permanece no tamanho da aplicação base.
    Veja os dados completos: Relatório de benchmark do Next.js e o repositório do benchmark.

    Comparação de recursos no Next.js

    Como o Lingui se compara com next-intl e Intlayer nos recursos que um projeto Next.js App Router geralmente necessita:

    Recursonext-intlayer (Intlayer)Linguinext-intl
    Traduções próximas aos componentes✅ Conteúdo co-localizado com cada componente⚠️ Texto fonte nos componentes, catálogos centralizados❌ JSON centralizado
    Integração com TypeScript✅ Tipos estritos gerados automaticamente⚠️ Macros tipadas, catálogos de mensagens não✅ Boa, via ampliação de AppConfig
    Detecção de traduções ausentes✅ Erros de TypeScript e avisos no momento do build⚠️ Fallback em tempo de execução para o texto fonte⚠️ Fallback em tempo de execução
    Conteúdo rico (JSX, Markdown)✅ Suporte direto✅ JSX dentro de <Trans>, sem Markdown⚠️ Tags via t.rich, sem Markdown
    Tradução com IA✅ Seu próprio provedor e chave de API com contexto❌ Não❌ Não
    Editor visual / CMS✅ Editor visual local + CMS opcional❌ Via plataformas externas❌ Via plataformas externas
    Roteamento localizado✅ Integrado❌ Escreva seu próprio proxy.ts✅ Segmento [locale] integrado
    Pluralização✅ Baseada em enumeração✅ ICU, macro <Plural>✅ ICU
    Formatos de conteúdo✅ .ts, .tsx, .js, .json, .md, .yaml✅ PO, JSON, CSV✅ .json, .js, .ts
    ICU MessageFormat✅ Via format: "icu"✅ Nativo✅ Nativo
    Auxiliares de SEO (hreflang, sitemap)✅ Auxiliares de metadados, sitemap e robots.txt❌ Manual✅ Bom
    Server Components✅ Acesso direto em qualquer Server Component⚠️ setI18n em cada layout e página⚠️ await getTranslations() por componente
    Tree-shaking por componente✅ No momento do build (Babel / SWC)⚠️ Um catálogo por localidade, extrator por página é experimental⚠️ Manual, com pick() por rota
    Tamanho do runtime (gzip, benchmark)4.9 KB72.1 KB14.7 KB
    Traduções ausentes no CI✅ npx intlayer test✅ lingui compile --strict⚠️ Não integrado
    Ecossistema / comunidade⚠️ Menor, em rápido crescimento✅ Maduro✅ Grande
    Os tamanhos de runtime vêm do benchmark do Next.js. Para uma discussão detalhada, leia Lingui vs Intlayer.
    Outros guias de Next.js: next-intl, next-i18next e Intlayer.

    Práticas recomendadas que você deve seguir

    • Defina lang e dir em <html> no layout de [locale].
    • Prefira Server Components para texto: eles renderizam HTML no servidor e não precisam do catálogo no cliente.
    • Chame initLingui(locale) em cada layout e página. Layouts não são renderizados novamente na navegação, portanto, uma página não pode depender do fato de seu layout ter definido a localidade.
    • Mantenha uma URL por localidade e pré-renderize todas as localidades com generateStaticParams.
    • Traduza seus metadados em generateMetadata, com canonical, hreflang e x-default.
    • Gere um sitemap e robots.txt multilíngues com as convenções sitemap.ts e robots.ts.
    • Use links reais para o seletor de idiomas, para que os rastreadores descubram todas as versões de idioma.
    • Execute lingui extract no CI para que uma nova mensagem nunca seja lançada sem tradução.
    Veja nosso guia sobre internacionalização e SEO, o guia de hreflang e a comparação de SEO multilíngue no Next.js.

    Guia passo a passo para configurar o Lingui em uma aplicação Next.js

    Aqui está a estrutura do projeto que iremos criar:

    bash
    .
    ├── lingui.config.ts
    ├── next.config.ts
    └── src
        ├── proxy.ts                    # Roteamento e detecção de localidade
        ├── locales
        │   ├── en
        │   │   └── messages.po         # Gerado por `lingui extract`
        │   ├── fr
        │   │   └── messages.po
        │   └── es
        │       └── messages.po
        ├── i18n
        │   ├── config.ts               # Localidades, utilitários de URL
        │   ├── appRouterI18n.ts        # Catálogos e instâncias apenas para servidor
        │   ├── initLingui.ts
        │   ├── negotiateLocale.ts
        │   └── metadata.ts             # Construtor de generateMetadata
        ├── components
        │   ├── LinguiClientProvider.tsx
        │   ├── LocaleSwitcher.tsx
        │   └── LocalizedLink.tsx
        └── app
            ├── sitemap.ts
            ├── robots.ts
            └── [locale]
                ├── layout.tsx
                ├── page.tsx
                ├── not-found.tsx
                ├── [...rest]
                │   └── page.tsx        # 404 localizado para rotas desconhecidas
                └── about
                    └── page.tsx
    
    1. Instalar dependências

      bash
      npm install @lingui/core @lingui/react
      npm install -D @lingui/cli @lingui/swc-plugin @lingui/loader @lingui/format-po
      
      • @lingui/core / @lingui/react: runtime, I18nProvider, setI18n para Server Components e as macros (@lingui/core/macro, @lingui/react/macro).
      • @lingui/swc-plugin: compila as macros dentro do pipeline SWC do Next.js.
      • @lingui/loader: compila catálogos .po na importação, dispensando o uso de lingui compile.
      • @lingui/cli: lingui extract para coletar mensagens em catálogos.
      @lingui/swc-plugin é um plugin WebAssembly vinculado à versão do SWC do Next.js. Se o build falhar após uma atualização do Next.js, atualize o plugin para a versão listada como compatível em seu README.
    2. Centralizar a configuração de localidade

      Um único arquivo define localidades e utilitários de URL. Roteamento, metadados, sitemap e o Lingui leem a partir dele.

      src/i18n/config.ts
      export const locales = ["en", "fr", "es"] as const;
      
      export type Locale = (typeof locales)[number];
      
      export const defaultLocale: Locale = "en";
      
      /** Public origin, used for canonical URLs, hreflang and the sitemap. */
      export const siteUrl = "https://example.com";
      
      /** Cookie storing the locale explicitly chosen by the visitor. */
      export const localeCookieName = "NEXT_LOCALE";
      
      /** Open Graph expects `language_TERRITORY` codes. */
      export const openGraphLocales: Record<Locale, string> = {
        en: "en_US",
        fr: "fr_FR",
        es: "es_ES",
      };
      
      export const isLocale = (value: unknown): value is Locale =>
        typeof value === "string" && (locales as readonly string[]).includes(value);
      
      export const resolveLocale = (value: string | undefined): Locale =>
        isLocale(value) ? value : defaultLocale;
      
      const rightToLeftLanguages = new Set(["ar", "fa", "he", "ur", "ps", "yi"]);
      
      export const getTextDirection = (locale: string): "ltr" | "rtl" =>
        rightToLeftLanguages.has(new Intl.Locale(locale).language) ? "rtl" : "ltr";
      
      /** `localizePath("/about", "fr")` → `/fr/about`, default locale unprefixed. */
      export const localizePath = (path: string, locale: Locale): string => {
        if (locale === defaultLocale) return path;
      
        return path === "/" ? `/${locale}` : `/${locale}${path}`;
      };
      
      /** `/fr/about` → `/about` */
      export const stripLocale = (pathname: string): string => {
        const [, firstSegment, ...rest] = pathname.split("/");
      
        return isLocale(firstSegment) ? `/${rest.join("/")}` : pathname;
      };
      
      export const getAbsoluteUrl = (path: string, locale: Locale): string =>
        `${siteUrl}${localizePath(path, locale)}`;
      
      export const getLocaleName = (locale: Locale): string =>
        new Intl.DisplayNames([locale], { type: "language" }).of(locale) ?? locale;
      
    3. Configurar o Lingui e o Next.js

      lingui.config.ts
      import { defineConfig } from "@lingui/cli";
      import { formatter } from "@lingui/format-po";
      import { defaultLocale, locales } from "./src/i18n/config";
      
      export default defineConfig({
        sourceLocale: defaultLocale,
        locales: [...locales],
        catalogs: [
          {
            path: "<rootDir>/src/locales/{locale}/messages",
            include: ["src"],
          },
        ],
        format: formatter({ lineNumbers: false }),
      });
      

      O plugin SWC compila as macros e o loader compila os arquivos .po, tanto para o Turbopack (padrão no Next.js 16) quanto para o webpack:

      next.config.ts
      import type { NextConfig } from "next";
      
      const nextConfig: NextConfig = {
        experimental: {
          swcPlugins: [["@lingui/swc-plugin", {}]],
        },
        turbopack: {
          rules: {
            "*.po": { loaders: ["@lingui/loader"], as: "*.js" },
          },
        },
        webpack: (config) => {
          config.module.rules.push({ test: /\.po$/, use: "@lingui/loader" });
      
          return config;
        },
      };
      
      export default nextConfig;
      

      Adicione os scripts de extração:

      package.json
      {
        "scripts": {
          "i18n:extract": "lingui extract --clean",
          "i18n:check": "lingui extract --clean && git diff --exit-code src/locales"
        }
      }
      
    4. Carregar catálogos e criar instâncias no servidor

      Server Components não possuem contexto React, portanto o Lingui disponibiliza setI18n para registrar a instância na renderização atual. Este módulo carrega cada catálogo uma vez por processo do servidor e cria uma instância I18n por localidade. Ele é server-only: catálogos de outras localidades nunca chegam ao bundle do cliente.

      src/i18n/appRouterI18n.ts
      import "server-only";
      import { type I18n, type Messages, setupI18n } from "@lingui/core";
      import { type Locale, locales } from "./config";
      
      const loadCatalog = async (locale: Locale): Promise<[Locale, Messages]> => {
        const { messages } = await import(`../locales/${locale}/messages.po`);
      
        return [locale, messages];
      };
      
      const catalogs = Object.fromEntries(
        await Promise.all(locales.map(loadCatalog))
      ) as Record<Locale, Messages>;
      
      const i18nInstances = Object.fromEntries(
        locales.map((locale) => [
          locale,
          setupI18n({ locale, messages: { [locale]: catalogs[locale] } }),
        ])
      ) as Record<Locale, I18n>;
      
      export const getMessages = (locale: Locale): Messages => catalogs[locale];
      
      export const getI18nInstance = (locale: Locale): I18n => i18nInstances[locale];
      
      src/i18n/initLingui.ts
      import { setI18n } from "@lingui/react/server";
      import { getI18nInstance } from "./appRouterI18n";
      import type { Locale } from "./config";
      
      /**
       * Registers the instance for the current Server Component render.
       * Call it in every layout and page.
       */
      export const initLingui = (locale: Locale) => {
        const i18n = getI18nInstance(locale);
      
        setI18n(i18n);
      
        return i18n;
      };
      

      Para que o TypeScript reconheça a importação de .po, declare o módulo uma vez:

      src/i18n/po.d.ts
      declare module "*.po" {
        import type { Messages } from "@lingui/core";
      
        export const messages: Messages;
      }
      
    5. Criar o provedor de cliente

      Client Components leem traduções a partir de um contexto React. O provedor recebe o catálogo da localidade ativa a partir do layout do servidor e cria sua própria instância uma única vez.

      src/components/LinguiClientProvider.tsx
      "use client";
      
      import { type Messages, setupI18n } from "@lingui/core";
      import { I18nProvider } from "@lingui/react";
      import { type ReactNode, useState } from "react";
      
      type LinguiClientProviderProps = {
        children: ReactNode;
        initialLocale: string;
        initialMessages: Messages;
      };
      
      export const LinguiClientProvider = ({
        children,
        initialLocale,
        initialMessages,
      }: LinguiClientProviderProps) => {
        const [i18n] = useState(() =>
          setupI18n({
            locale: initialLocale,
            messages: { [initialLocale]: initialMessages },
          })
        );
      
        return <I18nProvider i18n={i18n}>{children}</I18nProvider>;
      };
      
    6. Definir rotas dinâmicas de localidade

      O segmento [locale] abriga o layout raiz. generateStaticParams pré-renderiza todas as localidades no momento do build, e dynamicParams = false retorna um 404 para qualquer outro prefixo.

      src/app/[locale]/layout.tsx
      import type { Metadata } from "next";
      import { notFound } from "next/navigation";
      import { LinguiClientProvider } from "@/components/LinguiClientProvider";
      import { LocaleSwitcher } from "@/components/LocaleSwitcher";
      import { getMessages } from "@/i18n/appRouterI18n";
      import { getTextDirection, isLocale, locales, siteUrl } from "@/i18n/config";
      import { initLingui } from "@/i18n/initLingui";
      
      export const generateStaticParams = () => locales.map((locale) => ({ locale }));
      
      // Unknown prefixes (/xx/about) → 404
      export const dynamicParams = false;
      
      export const metadata: Metadata = {
        // Resolves relative canonical and Open Graph URLs
        metadataBase: new URL(siteUrl),
      };
      
      const LocaleLayout = async ({ children, params }: LayoutProps<"/[locale]">) => {
        const { locale } = await params;
      
        if (!isLocale(locale)) notFound();
      
        initLingui(locale);
      
        return (
          <html lang={locale} dir={getTextDirection(locale)}>
            <body>
              <LinguiClientProvider
                initialLocale={locale}
                initialMessages={getMessages(locale)}
              >
                <header>
                  <LocaleSwitcher />
                </header>
                <main>{children}</main>
              </LinguiClientProvider>
            </body>
          </html>
        );
      };
      
      export default LocaleLayout;
      
      O provedor do cliente recebe todo o catálogo da localidade ativa. Isso é o que o benchmark mede como "vazamento de outras páginas". Manter o texto em Server Components reduz o que o cliente realmente necessita. Para aplicações grandes, o extrator experimental por página do Lingui (experimental.extractor em lingui.config.ts) divide catálogos por ponto de entrada.
    7. Utilizar traduções em Server Components

      Server Components utilizam as mesmas macros que os Client Components. initLingui também deve ser executado na página, pois um layout não é renderizado novamente ao navegar entre suas páginas.

      src/app/[locale]/about/page.tsx
      import { Trans, useLingui } from "@lingui/react/macro";
      import { Counter } from "@/components/Counter";
      import { resolveLocale } from "@/i18n/config";
      import { initLingui } from "@/i18n/initLingui";
      
      const AboutPage = async ({ params }: PageProps<"/[locale]/about">) => {
        const { locale } = await params;
      
        initLingui(resolveLocale(locale));
      
        return <AboutContent />;
      };
      
      const AboutContent = () => {
        const { t } = useLingui();
      
        return (
          <section aria-label={t`About section`}>
            <h1>
              <Trans>About us</Trans>
            </h1>
            <p>
              <Trans>
                We build <strong>fast</strong>, multilingual applications.
              </Trans>
            </p>
            <Counter />
          </section>
        );
      };
      
      export default AboutPage;
      
    8. Utilizar traduções em Client Components

      Client Components utilizam as mesmas importações. As macros leem a instância a partir do LinguiClientProvider.

      src/components/Counter.tsx
      "use client";
      
      import { Plural, Trans, useLingui } from "@lingui/react/macro";
      import { useState } from "react";
      
      export const Counter = () => {
        const { t, i18n } = useLingui();
        const [count, setCount] = useState(0);
      
        return (
          <div>
            <p>
              <Plural
                value={count}
                _0="No clicks yet"
                one="# click"
                other="# clicks"
              />
            </p>
            <p>{i18n.number(count)}</p>
            <button
              type="button"
              aria-label={t`Counter`}
              onClick={() => setCount((value) => value + 1)}
            >
              <Trans>Increment</Trans>
            </button>
          </div>
        );
      };
      
    9. Extrair e traduzir suas mensagens

      Execute a extração. O Lingui grava cada mensagem encontrada em src nos catálogos de cada localidade:

      bash
      npm run i18n:extract
      

      Em seguida, traduza o msgstr de cada entrada:

      src/locales/fr/messages.po
      msgid "About us"
      msgstr "À propos"
      
      msgid "We build <0>fast</0>, multilingual applications."
      msgstr "Nous créons des applications <0>rapides</0> et multilingues."
      
      msgid "Learn who we are and why we built this application."
      msgstr "Découvrez qui nous sommes et pourquoi nous avons créé cette application."
      
      msgid "{count, plural, =0 {No clicks yet} one {# click} other {# clics}}"
      msgstr "{count, plural, =0 {Aucun clic} one {# clic} other {# clics}}"
      
      src/locales/es/messages.po
      msgid "About us"
      msgstr "Sobre nosotros"
      
      msgid "We build <0>fast</0>, multilingual applications."
      msgstr "Creamos aplicaciones <0>rápidas</0> y multilingües."
      
      msgid "Learn who we are and why we built this application."
      msgstr "Descubre quiénes somos y por qué creamos esta aplicación."
      
      msgid "{count, plural, =0 {No clicks yet} one {# click} other {# clicks}}"
      msgstr "{count, plural, =0 {Ningún clic} one {# clic} other {# clics}}"
      
      Os marcadores <0> mantêm os elementos JSX de um <Trans> no lugar, permitindo que os tradutores os reposicionem sem alterar a marcação.
    10. Configurar o proxy para roteamento de localidade

      Opcional

      O Next.js 16 renomeou middleware.ts para proxy.ts. O proxy implementa a estratégia de prefixo conforme necessário ("as-needed"):

      • /fr/about é servido como está;
      • /en/about redireciona para /about, mantendo uma URL única para a localidade padrão;
      • /about é reescrito internamente para /en/about, sem alterar a URL exibida;
      • uma primeira visita em / redireciona para o idioma preferido (cookie primeiro, depois Accept-Language).
      src/i18n/negotiateLocale.ts
      import { isLocale, type Locale } from "./config";
      
      /** "fr-CA,fr;q=0.9,en;q=0.8" → "fr" */
      export const negotiateLocale = (
        acceptLanguage: string | null | undefined
      ): Locale | undefined => {
        if (!acceptLanguage) return undefined;
      
        return acceptLanguage
          .split(",")
          .map((part) => {
            const [tag = "", quality] = part.trim().split(";q=");
      
            return {
              language: tag.toLowerCase().split("-")[0],
              quality: quality ? Number(quality) : 1,
            };
          })
          .sort((first, second) => second.quality - first.quality)
          .map(({ language }) => language)
          .find(isLocale);
      };
      
      src/proxy.ts
      import { type NextRequest, NextResponse } from "next/server";
      import {
        defaultLocale,
        isLocale,
        localeCookieName,
        localizePath,
        stripLocale,
      } from "@/i18n/config";
      import { negotiateLocale } from "@/i18n/negotiateLocale";
      
      export const proxy = (request: NextRequest) => {
        const { pathname } = request.nextUrl;
        const firstSegment = pathname.split("/")[1];
        const url = request.nextUrl.clone();
      
        if (isLocale(firstSegment)) {
          // /en/about → /about: uma URL para a localidade padrão
          if (firstSegment === defaultLocale) {
            url.pathname = stripLocale(pathname);
      
            return NextResponse.redirect(url, 308);
          }
      
          return NextResponse.next();
        }
      
        // Primeira visita em "/": envia o visitante para o idioma dele
        if (pathname === "/") {
          const cookieLocale = request.cookies.get(localeCookieName)?.value;
          const preferredLocale = isLocale(cookieLocale)
            ? cookieLocale
            : negotiateLocale(request.headers.get("accept-language"));
      
          if (preferredLocale && preferredLocale !== defaultLocale) {
            url.pathname = localizePath("/", preferredLocale);
      
            return NextResponse.redirect(url, 307);
          }
        }
      
        // /about → servido por /en/about, URL inalterada
        url.pathname = `/${defaultLocale}${pathname === "/" ? "" : pathname}`;
      
        return NextResponse.rewrite(url);
      };
      
      export const config = {
        // Ignora rotas de API, arquivos internos do Next.js e arquivos estáticos (sitemap.xml, robots.txt...)
        matcher: ["/((?!api|_next|.*\\..*).*)"],
      };
      
    11. Alterar o idioma do seu conteúdo

      Opcional

      usePathname retorna a URL vista pelo navegador (/about ou /fr/about). Remova a localidade e construa o link para cada idioma. O seletor renderiza links reais, permitindo que os rastreadores acessem cada versão de idioma, e o cookie salva a escolha explícita.

      src/components/LocaleSwitcher.tsx
      "use client";
      
      import { useLingui } from "@lingui/react/macro";
      import Link from "next/link";
      import { usePathname } from "next/navigation";
      import {
        getLocaleName,
        type Locale,
        localeCookieName,
        locales,
        localizePath,
        stripLocale,
      } from "@/i18n/config";
      
      const persistLocale = (locale: Locale) => {
        document.cookie = `${localeCookieName}=${locale}; Path=/; Max-Age=31536000; SameSite=Lax`;
      };
      
      export const LocaleSwitcher = () => {
        const { i18n, t } = useLingui();
        const basePath = stripLocale(usePathname());
      
        return (
          <nav aria-label={t`Change language`}>
            <ul>
              {locales.map((locale) => (
                <li key={locale}>
                  <Link
                    href={localizePath(basePath, locale)}
                    hrefLang={locale}
                    lang={locale}
                    aria-current={locale === i18n.locale ? "page" : undefined}
                    onClick={() => persistLocale(locale)}
                  >
                    {getLocaleName(locale)}
                  </Link>
                </li>
              ))}
            </ul>
          </nav>
        );
      };
      
    12. Opcional
      src/components/LocalizedLink.tsx
      "use client";
      
      import { useLingui } from "@lingui/react";
      import Link from "next/link";
      import type { ComponentProps } from "react";
      import { type Locale, localizePath } from "@/i18n/config";
      
      type LocalizedLinkProps = Omit<ComponentProps<typeof Link>, "href"> & {
        /** Path without locale prefix, e.g. "/about" */
        href: string;
      };
      
      export const LocalizedLink = ({ href, ...props }: LocalizedLinkProps) => {
        const { i18n } = useLingui();
      
        return <Link href={localizePath(href, i18n.locale as Locale)} {...props} />;
      };
      

      Ele também funciona a partir de Server Components, pois é renderizado dentro de LinguiClientProvider:

      tsx
      <LocalizedLink href="/about">
        <Trans>About us</Trans>
      </LocalizedLink>
      
    13. Internacionalizar seus metadados

      Opcional

      Cada versão de idioma pode se posicionar individualmente, desde que cada página forneça:

      • um title e description traduzidos;
      • uma URL canônica apontando para si mesma;
      • uma tag alternativa hreflang por localidade, além de x-default;
      • locale, alternateLocale e url do Open Graph;
      • JSON-LD com inLanguage.

      generateMetadata é executado fora da árvore do React, utilizando a instância do servidor diretamente com a macro msg:

      src/i18n/metadata.ts
      import type { Metadata } from "next";
      import {
        defaultLocale,
        getAbsoluteUrl,
        type Locale,
        locales,
        openGraphLocales,
      } from "./config";
      
      type LocalizedMetadataOptions = {
        /** Path without locale prefix, e.g. "/about" */
        path: string;
        locale: Locale;
        title: string;
        description: string;
      };
      
      export const buildLocalizedMetadata = ({
        path,
        locale,
        title,
        description,
      }: LocalizedMetadataOptions): Metadata => {
        const url = getAbsoluteUrl(path, locale);
      
        return {
          title,
          description,
          alternates: {
            canonical: url,
            languages: {
              ...Object.fromEntries(
                locales.map((alternateLocale) => [
                  alternateLocale,
                  getAbsoluteUrl(path, alternateLocale),
                ])
              ),
              "x-default": getAbsoluteUrl(path, defaultLocale),
            },
          },
          openGraph: {
            type: "website",
            title,
            description,
            url,
            locale: openGraphLocales[locale],
            alternateLocale: locales
              .filter((alternateLocale) => alternateLocale !== locale)
              .map((alternateLocale) => openGraphLocales[alternateLocale]),
          },
        };
      };
      
      src/app/[locale]/about/page.tsx
      import { msg } from "@lingui/core/macro";
      import type { Metadata } from "next";
      import { getI18nInstance } from "@/i18n/appRouterI18n";
      import { resolveLocale } from "@/i18n/config";
      import { buildLocalizedMetadata } from "@/i18n/metadata";
      
      export const generateMetadata = async ({
        params,
      }: PageProps<"/[locale]/about">): Promise<Metadata> => {
        const locale = resolveLocale((await params).locale);
        const i18n = getI18nInstance(locale);
      
        return buildLocalizedMetadata({
          path: "/about",
          locale,
          title: i18n._(msg`About us`),
          description: i18n._(
            msg`Learn who we are and why we built this application.`
          ),
        });
      };
      
      // ... componente de página da etapa 7
      

      O JSON-LD é renderizado pela própria página. Como arquivos de página só devem exportar campos do Next.js, mantenha o componente em seu próprio arquivo:

      src/components/WebPageJsonLd.tsx
      import { getAbsoluteUrl, type Locale } from "@/i18n/config";
      
      type WebPageJsonLdProps = {
        path: string;
        locale: Locale;
        title: string;
      };
      
      export const WebPageJsonLd = ({ path, locale, title }: WebPageJsonLdProps) => (
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{
            __html: JSON.stringify({
              "@context": "https://schema.org",
              "@type": "WebPage",
              name: title,
              url: getAbsoluteUrl(path, locale),
              inLanguage: locale,
            }),
          }}
        />
      );
      
      src/app/[locale]/about/page.tsx
      // Em AboutContent
      <WebPageJsonLd
        path="/about"
        locale={i18n.locale as Locale}
        title={t`About us`}
      />
      
    14. Internacionalizar seu sitemap

      Opcional

      A convenção sitemap.ts suporta alternates.languages, que o Next.js renderiza como alternativas xhtml:link. Liste todas as URLs de todas as localidades:

      src/app/sitemap.ts
      import type { MetadataRoute } from "next";
      import { defaultLocale, getAbsoluteUrl, locales } from "@/i18n/config";
      
      type SitemapPage = {
        path: string;
        changeFrequency: "daily" | "weekly" | "monthly";
        priority: number;
      };
      
      const sitemapPages: SitemapPage[] = [
        { path: "/", changeFrequency: "daily", priority: 1.0 },
        { path: "/about", changeFrequency: "monthly", priority: 0.8 },
      ];
      
      const getAlternateLanguages = (path: string) => ({
        ...Object.fromEntries(
          locales.map((locale) => [locale, getAbsoluteUrl(path, locale)])
        ),
        "x-default": getAbsoluteUrl(path, defaultLocale),
      });
      
      const sitemap = (): MetadataRoute.Sitemap =>
        sitemapPages.flatMap(({ path, changeFrequency, priority }) =>
          locales.map((locale) => ({
            url: getAbsoluteUrl(path, locale),
            lastModified: new Date(),
            changeFrequency,
            priority,
            alternates: { languages: getAlternateLanguages(path) },
          }))
        );
      
      export default sitemap;
      
    15. Internacionalizar seu robots.txt

      Opcional

      Rotas privadas existem em todos os idiomas, portanto o disallow deve abranger todas as rotas localizadas:

      src/app/robots.ts
      import type { MetadataRoute } from "next";
      import { locales, localizePath, siteUrl } from "@/i18n/config";
      
      const privatePaths = ["/dashboard", "/admin"];
      
      const robots = (): MetadataRoute.Robots => ({
        rules: {
          userAgent: "*",
          allow: "/",
          // /dashboard, /fr/dashboard, /es/dashboard...
          disallow: privatePaths.flatMap((path) =>
            locales.map((locale) => localizePath(path, locale))
          ),
        },
        sitemap: `${siteUrl}/sitemap.xml`,
      });
      
      export default robots;
      
    16. Lidar com páginas 404 localizadas

      Opcional

      not-found.tsx é renderizado dentro do layout de [locale], tendo acesso ao provedor do cliente. A rota do tipo catch-all encaminha caminhos desconhecidos dentro de uma localidade para ele. O Next.js adiciona noindex automaticamente às respostas 404.

      src/app/[locale]/not-found.tsx
      "use client";
      
      import { Trans } from "@lingui/react/macro";
      import { LocalizedLink } from "@/components/LocalizedLink";
      
      const NotFound = () => (
        <div>
          <h1>
            <Trans>Page not found</Trans>
          </h1>
          <LocalizedLink href="/">
            <Trans>Back to home</Trans>
          </LocalizedLink>
        </div>
      );
      
      export default NotFound;
      
      src/app/[locale]/[...rest]/page.tsx
      import { notFound } from "next/navigation";
      
      // /fr/does/not/exist → not-found.tsx localizado
      const CatchAllPage = () => notFound();
      
      export default CatchAllPage;
      
    17. Acessar a localidade em Server Actions

      Opcional

      Server Actions não recebem os parâmetros de rota diretamente. A abordagem mais confiável é enviar a localidade com o formulário, a partir da página que a conhece:

      src/app/[locale]/contact/page.tsx
      import { Trans } from "@lingui/react/macro";
      import { sendContactMessage } from "@/app/actions/sendContactMessage";
      import { resolveLocale } from "@/i18n/config";
      import { initLingui } from "@/i18n/initLingui";
      
      const ContactPage = async ({ params }: PageProps<"/[locale]/contact">) => {
        const locale = resolveLocale((await params).locale);
      
        initLingui(locale);
      
        return (
          <form action={sendContactMessage}>
            <input type="hidden" name="locale" value={locale} />
            <textarea name="message" />
            <button type="submit">
              <Trans>Send</Trans>
            </button>
          </form>
        );
      };
      
      export default ContactPage;
      
      src/app/actions/sendContactMessage.ts
      "use server";
      
      import { msg } from "@lingui/core/macro";
      import { getI18nInstance } from "@/i18n/appRouterI18n";
      import { resolveLocale } from "@/i18n/config";
      
      export const sendContactMessage = async (formData: FormData) => {
        const locale = resolveLocale(formData.get("locale")?.toString());
        const i18n = getI18nInstance(locale);
      
        const subject = i18n._(msg`Thanks for your message`);
      
        // await mailer.send({ subject, locale, ... });
        console.log(`[${locale}] ${subject}`);
      };
      
    18. Mantenha suas macros e reduza o runtime com o Intlayer

      Opcional

      O adaptador de compatibilidade @intlayer/lingui mantém seu código-fonte intacto: as macros compilam como antes e as chamadas resultantes de i18n._(), useLingui() e <Trans> são atendidas por dicionários Intlayer. No benchmark do Next.js, o runtime cai de ~72.1 KB para ~10.7 KB gzip.

      No Next.js, o adaptador é configurado criando aliases de @lingui/core e @lingui/react para @intlayer/lingui em next.config.ts (webpack e Turbopack), e envolvendo a configuração com withIntlayer de next-intlayer/server. Mantenha o @lingui/swc-plugin para que as macros continuem sendo compiladas primeiro. A configuração completa está no guia de compatibilidade do Lingui.

      Como a tabela de benchmark demonstra, o adaptador reduz o tamanho do runtime, mas ainda não divide o catálogo enviado para cada página no Next.js. Ele é idealmente utilizado como uma ponte de migração: uma vez em execução, migre os componentes gradualmente para a API nativa useIntlayer, que envia apenas o conteúdo que cada componente renderiza. Veja o guia de Next.js + Intlayer, Lingui vs @intlayer/lingui e todos os adaptadores de compatibilidade.

    19. Automatize suas traduções usando Intlayer

      Opcional

      O Lingui extrai mensagens, mas preencher dezenas de catálogos manualmente é onde a maior parte do tempo é gasta. O Intlayer é gratuito e de código aberto, e seu ferramental funciona perfeitamente ao lado do Lingui:

    Perguntas Frequentes

    Sim. O @lingui/react suporta React Server Components. Os Server Components registram a instância com setI18n de @lingui/react/server, os Client Components a leem a partir de I18nProvider, e ambos utilizam as mesmas macros Trans e useLingui.

    Server Components não possuem contexto, portanto a instância é registrada por renderização. Os layouts são preservados durante as navegações e não são renderizados novamente, logo uma página não pode depender do seu layout para definir a localidade. Chamar initLingui(locale) no topo de cada layout e página os mantém independentes.

    Use @lingui/swc-plugin. Ele preserva o pipeline SWC e o Turbopack. Adicionar uma configuração do Babel desativa o SWC no Next.js e torna os builds mais lentos. A única restrição é manter a versão do plugin compatível com a versão do SWC do seu lançamento do Next.js.

    Obtenha a instância do servidor com getI18nInstance(locale) e traduza os descritores declarados com a macro msg: i18n._(msg`About us`). Retorne alternates.canonical, alternates.languages com x-default e openGraph.locale. A etapa 13 disponibiliza um helper reutilizável.

    O benchmark mede ~72 KB gzip para o runtime. Com um catálogo por localidade, as páginas pesam ~145 KB em comparação com 141 KB sem i18n, mas cada página ainda recebe as mensagens de outras páginas por meio do provedor de cliente.

    O Lingui atende a equipes que preferem escrever o texto de origem nos componentes e trabalhar com arquivos PO e tradutores. O next-intl é indicado para equipes que preferem catálogos JSON e uma API t("key") fortemente integrada ao Next.js. O next-i18next oferece o ecossistema de plugins do i18next. Veja next-i18next vs next-intl vs Intlayer e o benchmark do Next.js.

    Comentários

    Ainda sem comentários. Seja o primeiro a compartilhar seus pensamentos.

    Artigos relacionados

    Últimos artigos