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

    next-intl VS @intlayer/next-intl | Mesma API, Bundle Diferente

    @intlayer/next-intl é um adaptador de compatibilidade: expõe a API next-intl (useTranslations, getTranslations, useLocale, t.rich(), plurais ICU, NextIntlClientProvider...) e a serve a partir de dicionários compilados pelo Intlayer. O código da aplicação não muda. O bundle muda.

    Este artigo compara os dois na mesma aplicação Next.js, construída uma vez com next-intl e outra com o adaptador. Os números vêm de Benchmark Bloom, uma suite open-source que registra o que o navegador realmente baixa. Se você quiser a comparação next-intl vs Intlayer como bibliotecas, leia next-intl vs Intlayer. Este é sobre o que o adaptador muda quando você mantém seus componentes como estão.

    tl;dr: No mesmo aplicativo Next.js, trocar next-intl por @intlayer/next-intl reduziu o JavaScript por página de 153.6 KB para 147.5 KB gzip, o componente médio de 21.8 KB para 8.1 KB, vazamento de strings de página estrangeira de ~90% para 0%, e hidratação de 14.7 ms para 12.8 ms, sem editar nenhum componente. No TanStack Start, o equivalente use-intl (@intlayer/use-intl) reduziu componentes de 76-87 KB para 9-11 KB e alternância de localidade de 7-21 ms para 4-9 ms. O adaptador custa 8.0 KB de runtime versus 14.7 KB para next-intl e 5.5 KB para next-intlayer nativo. Navegação e middleware são re-implementados na configuração de roteamento do Intlayer; nomes de caminho localizados (pathnames) são o único recurso não transferido.

    O que é @intlayer/next-intl

    next-intl é um runtime: getRequestConfig carrega um messages/{locale}.json por requisição, NextIntlClientProvider o envia para o cliente, e useTranslations("about") lê chaves daquele objeto no tempo de renderização. Cada otimização (namespaces, pick(messages, [...]) por página, lazy loading) é sua responsabilidade escrever.

    @intlayer/next-intl mantém a primeira e última parte dessa cadeia e substitui o meio. Seus componentes ainda chamam useTranslations("about"); o que eles recebem vem de um dicionário Intlayer compilado no tempo de construção, escopo para aquele componente, apenas na locale ativa.

    Três mecanismos fazem isso funcionar:

    1. Import aliasing. createNextIntlPlugin() de @intlayer/next-intl/plugin encapsula withIntlayer e adiciona aliases do Webpack / Turbopack para que next-intl, next-intl/server, next-intl/navigation e next-intl/middleware sejam resolvidos para @intlayer/next-intl. Nenhuma importação em sua codebase é renomeada.
    2. JSON como fonte de verdade. O plugin syncJSON lê seus messages/{locale}.json existentes, divide suas chaves de nível superior em um dicionário por namespace e escreve as traduções de volta nos mesmos arquivos quando a CLI ou o CMS as atualiza. O fluxo de trabalho de seus tradutores permanece inalterado.
    3. Call-site binding. A otimização do Intlayer (Babel ou SWC) reescreve useTranslations("about") em uma chamada que recebe o dicionário about diretamente. O componente não alcança mais uma árvore de mensagens global; ele alcança seu próprio conteúdo.
    app/[locale]/about/page.tsx
    // Seu código, inalterado
    import { useTranslations } from "next-intl";
    
    const AboutPage = () => {
      const t = useTranslations("about");
      return <h1>{t("title")}</h1>;
    };
    
    O que o compilador emite (simplificado)
    import _dicHash_about from "../.intlayer/dictionaries/about.mjs";
    import { useDictionary as useTranslations } from "@intlayer/next-intl";
    
    const AboutPage = () => {
      const t = useTranslations(_dicHash_about);
      return <h1>{t("title")}</h1>;
    };
    

    Essa reescrita é por isso que as colunas de tamanho de componente e vazamento de página abaixo se movem: uma página apenas puxa os dicionários dos componentes que renderiza, e apenas na localidade sendo servida.

    O que o adaptador mantém, ignora e não substitui

    next-intl APICom @intlayer/next-intl
    useTranslations("ns") / getTranslations("ns")✅ Mantido. Vinculado ao dicionário ns em tempo de construção. As chaves são tipadas em relação ao seu conteúdo.
    getTranslations({ locale, namespace })✅ Mantido
    t("key", { name }), t.rich(), t.markup(), t.raw()✅ Mantido. Plurais ICU, select, selectordinal, #, {ts, date, long} executados através do resolvedor ICU do Intlayer
    useLocale() / getLocale() / setRequestLocale() / setLocale✅ Mantido
    useFormatter()✅ Mantido. dateTime, number, relativeTime, list, dateTimeRange conectam ao Intl nativo
    NextIntlClientProvider✅ Mantido. Os props messages, timeZone e now são aceitos mas ignorados (um aviso de desenvolvimento o informa)
    getMessages()✅ Mantido para compatibilidade; não é mais necessário
    getRequestConfig() em src/i18n.ts⚠️ Não necessário. Os dicionários são compilados em tempo de build; não há carregamento de mensagens por requisição
    defineRouting()✅ Mantido. Os campos omitidos (locales, defaultLocale, localePrefix) são lidos de intlayer.config.ts
    createNavigation(), Link, redirect, usePathname, useRouter✅ Mantido. Re-implementado na configuração de roteamento do Intlayer; o argumento routing é aceito mas ignorado
    pathnames (nomes de rotas localizadas)❌ Aceito para digitação, não interpolado. Mantenha pathnames simples ou mova esse mapeamento para rewrite do Intlayer
    createMiddleware()✅ Mantido. Retorna o proxy do Intlayer; define o cookie NEXT_LOCALE para que useLocale() e seu comutador continuem funcionando
    NEXT_LOCALE cookie✅ Lido por padrão (a menos que você configure routing.storage por conta própria)
    Bare useTranslations() com nenhum namespace⚠️ Funciona, mas o local da chamada não está vinculado: ele se resolve através do registro de tempo de execução. Passe um namespace para obter os ganhos de bundle

    O benchmark

    O que foi medido

    A suite Benchmark Bloom constrói a mesma aplicação com cada setup: 10 páginas (home, about, blog, careers, contact, FAQ, pricing, products, settings, team), 10 locales (en, fr, es, de, it, pt, zh, ja, ko, ru), componentes idênticos e conteúdo idêntico. As páginas são medidas em en e fr.

    next-intl foi construído em quatro estratégias de carregamento, da configuração ingênua (arquivo messages/{locale}.json carregado integralmente) até a ótima (um namespace por rota + pick() por página). O adapter foi construído sobre os mesmos componentes da configuração ingênua, com apenas next.config.ts e intlayer.config.ts alterados. Não possui uma variante "scoped": o compilador faz o escopo do conteúdo por componente, portanto suas linhas static e dynamic já estão no escopo.

    Para cada build, a suite registra:

    • Lib size: tamanho gzip da biblioteca i18n de um componente vazio que apenas importa a biblioteca. O custo fixo do runtime.
    • Page JS: JavaScript gzip baixado por página, calculado em média em todas as páginas e locales.
    • Locale leak %: compartilha de strings traduzidas encontradas no JS baixado que pertencem a uma locale que o usuário não está visualizando.
    • Page leak %: compartilha de strings traduzidas encontradas no JS baixado que pertencem a uma página na qual o usuário não está.
    • Component avg: tamanho gzip médio de cada componente compilado isoladamente. Mostra quanto runtime i18n e catálogo um único componente carrega.
    • E2E reactivity: tempo de parede entre selecionar uma nova locale e html[lang] atualizar no DOM (Playwright, 5 iterações).
    • Hydration: duração da fase de hydration do React.
    Os números abaixo vêm da execução datada de 2026-09-12 com next-intl / use-intl 4.14.2 e @intlayer/* 9.5.1. A aplicação de teste é deliberadamente pequena (algumas dezenas de strings por locale), então os percentuais de vazamento descrevem um padrão: eles crescem com seu conteúdo enquanto o custo de runtime permanece fixo.

    Resultados no Next.js

    SetupStrategyLib size (gz)Page JS avg (gz)Locale leakPage leakComponent avg (gz)E2E reactivityHydration
    base (sem i18n)-0.0 KB141.0 KB0.0%0.0%0.9 KB13.4 ms11.8 ms
    next-intlstatic14.7 KB153.6 KB4.2%89.8%21.8 KB16.0 ms14.7 ms
    next-intldynamic14.7 KB153.6 KB9.7%89.9%21.8 KB15.6 ms14.8 ms
    next-intlscoped-static14.7 KB153.6 KB0.0%0.0%80.1 KB17.9 ms17.4 ms
    next-intlscoped-dynamic14.7 KB153.6 KB0.0%0.0%22.9 KB17.8 ms16.8 ms
    @intlayer/next-intlstatic8.0 KB147.5 KB0.0%0.0%8.1 KB14.5 ms12.8 ms
    @intlayer/next-intldynamic8.0 KB148.7 KB0.0%0.0%8.1 KB11.7 ms12.8 ms
    next-intlayer (native)static5.5 KB141.3 KB0.0%0.0%8.5 KB15.5 ms16.9 ms
    next-intlayer (native)dynamic5.5 KB141.3 KB0.0%0.0%6.9 KB15.3 ms15.9 ms

    Como ler

    • Mesmos componentes, 6 KB menos por página. A compilação do adapter da aplicação ingênua chega a 147.5 KB, abaixo de todas as configurações next-intl, incluindo a totalmente otimizada (153.6 KB). O próprio runtime é a diferença: 8.0 KB versus 14.7 KB, pagos em cada página.
    • Vazamento vai a 0% sem tocar em um componente. A configuração ingênua de next-intl envia ~90% de strings de páginas estrangeiras em cada página. Atingir 0% com next-intl significa as configurações scoped-*: um namespace por rota, e pick(messages, [...]) em cada página. O adapter atinge 0% a partir do código ingênuo porque a passagem de otimização vincula cada useTranslations("ns") ao seu próprio dicionário.
    • Componentes encolhem 2.7x. Um componente compilado isoladamente tem média de 21.8 KB com next-intl (ele atinge o provider e a árvore de mensagens) e 8.1 KB com o adapter. Na configuração scoped-static do next-intl esse número sobe para 80 KB, porque o arquivo de namespace de cada rota fica acessível a partir da página que o seleciona.
    • Hidratação é 2 ms mais rápida (12.8 vs 14.7 ms): não há objeto de mensagem para desserializar do payload RSC antes do React poder hidratar.
    • O adapter não é o runtime nativo. next-intlayer fica em 141.3 KB, +0.3 KB sobre a app base, com um runtime de 5.5 KB. O adapter carrega a superfície de API do next-intl (useFormatter, t.rich, o resolver ICU) no topo do core do Intlayer, daí 8.0 KB e +6 KB por página. É a ponte, não o destino.

    Resultados em TanStack Start (use-intl)

    use-intl é o core agnóstico de framework do next-intl. Seu adapter, @intlayer/use-intl, segue o mesmo design com um plugin Vite (@intlayer/use-intl/plugin).

    ConfiguraçãoEstratégiaTamanho da biblioteca (gz)JS médio da página (gz)Vazamento de localidadeVazamento de páginaComponente médio (gz)Reatividade E2EHidratação
    base (sem i18n)-0.0 KB111.0 KB0.0%0.0%0.7 KB8.1 ms21.6 ms
    use-intlstatic14.1 KB179.8 KB50.0%89.8%76.0 KB6.7 ms15.3 ms
    use-intldynamic14.1 KB119.4 KB0.0%89.8%75.9 KB7.0 ms15.4 ms
    use-intlscoped-static14.1 KB128.7 KB0.0%0.0%87.1 KB20.9 ms24.8 ms
    use-intlscoped-dynamic14.1 KB128.7 KB0.0%0.0%87.1 KB13.3 ms25.9 ms
    @intlayer/use-intlstatic7.3 KB135.8 KB49.7%0.0%10.9 KB4.2 ms10.5 ms
    @intlayer/use-intldynamic7.3 KB129.7 KB0.0%0.0%9.3 KB8.7 ms16.1 ms
    intlayer (native)static5.0 KB125.8 KB50.0%0.0%8.1 KB3.2 ms11.5 ms
    intlayer (native)dynamic5.0 KB118.6 KB0.0%0.0%6.3 KB3.6 ms14.1 ms

    Como ler

    • Bytes por página são equivalentes ao use-intl otimizado. @intlayer/use-intl em modo dynamic (129.7 KB) está dentro de 1 KB do scoped-dynamic (128.7 KB) do use-intl, e 10 KB acima do dynamic simples (119.4 KB) do use-intl. Essa linha dynamic simples ainda vaza 90% das strings de páginas estrangeiras; a contagem de bytes é baixa porque o conteúdo da aplicação de teste é pequeno. O 0% do adapter é o que permanece constante conforme o conteúdo cresce.
    • Os componentes são 7-9x menores. Os componentes use-intl têm em média 76-87 KB em todas as estratégias, porque useTranslations está vinculado ao objeto de mensagens completo do provedor. O adapter tem em média 9-11 KB.
    • A alternância de locale é mais rápida. As configurações otimizadas de use-intl levam 13-21 ms para atualizar html[lang]; o adapter leva 4-9 ms. Menos componentes são re-renderizados, e nada é re-selecionado de uma árvore de mensagens.
    • static mantém cada locale. A linha static do adapter mostra 49,7% de vazamento de locale, o mesmo que o Intlayer nativo em modo static: todos os locales são agrupados, apenas os dicionários da página. Uma linha de configuração (importMode: 'dynamic') remove isso.

    Por que os números mudam

    Nada no componente mudou, então os ganhos vêm inteiramente do que useTranslations está vinculado.

    Com next-intl, a vinculação é o provedor. NextIntlClientProvider recebe o objeto messages inteiro para a localidade; cada useTranslations("about") lê a partir dele. O bundler vê um componente importando um hook que lê um contexto, e não pode saber que apenas a ramificação about é usada. As rotas abaixo compartilham o mesmo objeto de mensagens, então a coluna page-leak lê ~90% até você dividir o arquivo você mesmo.

    bash
    .
    ├── messages
       ├── en.json                       # cada namespace, cada página
       └── fr.json
    └── src
        ├── i18n.ts                       # getRequestConfig({ messages: await import(...) })
        ├── middleware.ts                 # createMiddleware(routing)
        └── app/[locale]
            ├── layout.tsx                # <NextIntlClientProvider messages={messages}>
            └── about/page.tsx            # useTranslations("about")
    

    Com @intlayer/next-intl, a vinculação é o dicionário. syncJSON transforma messages/en.json em um dicionário por chave de nível superior; o compilador resolve qual componente chama useTranslations("about") e lhe passa about diretamente, na locale ativa, como um import que o bundler pode rastrear e dividir.

    bash
    .
    ├── intlayer.config.ts                # syncJSON({ source: ({ locale }) => `./messages/${locale}.json` })
    ├── messages
       ├── en.json                       # inalterado, ainda a fonte da verdade
       └── fr.json
    ├── .intlayer/                        # gerado: um dicionário por namespace, por locale
    └── src
        ├── middleware.ts                 # createMiddleware() agora retorna o proxy do Intlayer
        └── app/[locale]
            ├── layout.tsx                # <NextIntlClientProvider> (sem prop messages)
            └── about/page.tsx            # useTranslations("about")  ← inalterado
    

    src/i18n.ts e a prop messages desaparecem. Tudo o resto é idêntico.

    Migração em três passos

    1. Instalar

      bash
      npx intlayer init --interactive
      

      O comando detecta next-intl e instala intlayer, next-intlayer, @intlayer/next-intl e @intlayer/sync-json-plugin. Mantenha next-intl instalado: é uma dependência peer do adaptador e fornece os tipos.

    2. Aponte o Intlayer para suas mensagens

      intlayer.config.ts
      import { Locales, type IntlayerConfig } from "intlayer";
      import { syncJSON } from "@intlayer/sync-json-plugin";
      
      const config: IntlayerConfig = {
        internationalization: {
          locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],
          defaultLocale: Locales.ENGLISH,
        },
        dictionary: {
          // "static" agrupa todas as locales; "dynamic" carrega a ativa sob demanda
          importMode: "dynamic",
        },
        plugins: [
          syncJSON({
            // Placeholders ICU: {name}, {count, plural, one {# item} other {# items}}
            format: "icu",
            source: ({ locale }) => `./messages/${locale}.json`,
            location: "messages",
          }),
        ],
      };
      
      export default config;
      

      messages/{locale}.json permanece onde está. Cada chave de nível superior torna-se um dicionário; useTranslations("about") mapeia para o dicionário about.

    3. Envolver next.config.ts

      next.config.ts
      import type { NextConfig } from "next";
      import { createNextIntlPlugin } from "@intlayer/next-intl/plugin";
      
      const withIntlayer = createNextIntlPlugin();
      
      const nextConfig: NextConfig = {};
      
      export default withIntlayer(nextConfig);
      

      createNextIntlPlugin() compõe withIntlayer (monitoramento de conteúdo, compilação de dicionário, a otimização) e os aliases next-intl@intlayer/next-intl para Webpack e Turbopack. Faça o build, e os números nas tabelas acima são seus.

    O que você pode deletar depois

    Arquivo / padrãoPor quê
    getRequestConfig em src/i18n.tsSem carregamento de mensagens por requisição. Mantenha o arquivo apenas se ele também exportar helpers createNavigation
    messages={...} no NextIntlClientProviderO adapter lê a saída compilada; a prop é ignorada e registra um aviso em desenvolvimento
    await getMessages() em layoutsMesmo motivo
    pick(messages, [...]) por páginaO compilador faz a seleção, por componente

    O que você ganha além de bytes

    • Chaves tipadas. useTranslations("about") é tipado contra o dicionário compilado about. t("does.not.exist") é um erro TypeScript, não um fallback em tempo de execução.
    • npx intlayer test falha no CI quando um locale está faltando uma chave. npx intlayer fill traduz as chaves ausentes com o provedor de sua escolha (OpenAI, Anthropic, Mistral, Gemini...) usando sua própria chave, e escreve o resultado de volta em messages/{locale}.json.
    • Visual Editor e CMS funcionam nos mesmos dicionários, portanto, não-desenvolvedores podem editar messages/fr.json através de uma UI e o arquivo é atualizado.
    • Migração incremental para .content.ts. Qualquer componente pode alternar de useTranslations("about") para useIntlayer("about") com um arquivo de conteúdo co-localizado, um de cada vez. Os dicionários JSON e .content.ts coexistem e se mesclam.

    Limites a conhecer antes de começar

    • Routing config move para intlayer.config.ts. createNavigation(routing) e createMiddleware(routing) mantêm sua assinatura mas ignoram o argumento: locales, locale padrão e estratégia de prefixo vêm da config routing do Intlayer. Se você usa pathnames localizados do next-intl (/about/a-propos), o adapter não interpola; o routing.rewrite do Intlayer cobre esse caso mas é uma mudança separada.
    • useTranslations() sem namespace não é vinculado. O passe de otimização precisa de um namespace estático para saber qual dicionário importar. Uma chamada simples ainda funciona, por meio de um registry de runtime que referencia cada dicionário, que é exatamente o vazamento que você estava tentando remover. Passe o namespace.
    • O adaptador não é gratuito. 8.0 KB de runtime versus 5.5 KB para next-intlayer, e +6-7 KB por página em relação ao build nativo. Isso é o custo da surface API do next-intl. Se você chegar ao ponto em que todos os componentes foram movidos para useIntlayer, descarte o adaptador.
    • messages, timeZone, now no provider são ignorados. Os formatadores são suportados pelo Intl nativo e apenas a locale influencia sua saída; se você depender de um fuso horário forçado ou um now fixo para datas estáveis em hidratação, trate isso no local da chamada.

    Quando usar qual?

    • Mantenha-se em next-intl se sua app é pequena, seu bundle não é uma preocupação, e seu time está confortável em gerenciar namespaces e pick() por página.
    • Use @intlayer/next-intl if you are on next-intl today and want the bundle, leakage and hydration gains, typed keys and the CLI / CMS tooling without a rewrite. This is the recommended entry point for any existing next-intl codebase.
    • Go native (next-intlayer) for new projects, or once the adapter has done its job. It is the lightest of the three (5.5 KB, +0.3 KB per page) and unlocks synchronous server components, per-component .content.ts files and the full feature set.

    Conclusão

    @intlayer/next-intl faz uma coisa: muda o que useTranslations está vinculado, de um provider que contém todas as mensagens para um dicionário compilado para esse componente. No mesmo app Next.js que vale 6 KB por página, componentes 2,7x menores, 0% de vazamento e 2 ms de hidratação, antes de qualquer pessoa abrir um arquivo de componente. Navigation e middleware mantêm sua API no topo da configuração de roteamento do Intlayer, e o runtime nativo next-intlayer permanece ainda mais leve.

    Todos os dados brutos, os apps de teste e os scripts estão no repositório Benchmark Bloom. Execute você mesmo.

    Consulte o doc 'Por que Intlayer?' para mais detalhes.

    Comentários

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

    Artigos relacionados

    Últimos artigos