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

    Lingui VS Intlayer | Benchmark de Internacionalização (i18n) para React e Next.js

    Lingui e Intlayer são as duas bibliotecas deste benchmark que utilizam um compilador em vez de operarem estritamente como um runtime. O Lingui extrai mensagens a partir de macros durante o build e compila catálogos por idioma. O Intlayer compila dicionários por componente e aplica tree-shaking por idioma. Na teoria, deveriam ser muito parecidos. Os números revelam onde eles se distanciam.

    Os dados são provenientes do Benchmark Bloom, uma suíte de código aberto que constrói a mesma aplicação com cada biblioteca e afere o que o navegador efetivamente baixa e executa.

    tl;dr: O Lingui é a biblioteca mais próxima do Intlayer em volume bruto de JavaScript por página: 115-120 KB contra 118,6 KB no TanStack Start após a configuração de lazy loading, e 148,6 KB contra 141,3 KB no Next.js. A diferença real surge nos demais indicadores: um componente do Lingui compilado isoladamente pesa 58-153 KB contra 6-8 KB no Intlayer, a hidratação leva 28-34 ms contra 11-14 ms, o fallback do idioma original vaza entre 3% e 15% de textos em inglês nas páginas em francês em qualquer configuração otimizada, e atingir essa otimização requer extrair, compilar e selecionar catálogos manualmente por rota. O Intlayer alcança esses números sem qualquer configuração complexa.

    Em resumo

    • Lingui - Baseado em macros ( t`...` , <Trans>, msg), formato ICU MessageFormat, catálogos em .po / JSON, fluxo de trabalho lingui extract + lingui compile. Compila IDs de mensagem em hashes compactos, suporta carregamento dinâmico de catálogos por idioma. Consolidado, agnóstico de framework e com excelente ecossistema de ferramentas de tradução em torno do formato .po.
    • Intlayer - Modelo de conteúdo orientado a componentes. Dicionários .content.ts residem ao lado do componente ao qual atendem; um compilador de build realiza tree-shaking e carregamento sob demanda por componente e por idioma; tipos TypeScript rigorosos são gerados automaticamente a partir do conteúdo, e traduções ausentes geram erro de compilação. Inclui middleware, utilitários de SEO, um Editor Visual / CMS e tradução assistida por IA.
    BibliotecaEstrelas no GitHubCommits TotaisÚltimo CommitPrimeira VersãoVersão NPMDownloads no NPM
    aymericzip/intlayerGitHub Repo starsGitHub commit activityLast CommitAbril 2024npmnpm downloads
    lingui/js-linguiGitHub Repo starsGitHub commit activityLast CommitDezembro 2016npmnpm downloads
    Os badges são atualizados de forma automática. Instantâneos podem mudar ao longo do tempo.

    Comparação funcional lado a lado

    RecursoIntlayer (react-intlayer / next-intlayer)Lingui (@lingui/core / @lingui/react)
    Traduções próximas aos componentes✅ Sim, .content.ts colocalizado com cada componente⚠️ Strings originais no JSX via macros; traduções em catálogos .po centralizados
    Integração com TypeScript✅ Tipos estritos gerados automaticamente a partir do conteúdo⚠️ Macros tipadas; IDs de mensagem sem tipos, chaves ausentes não são apontadas
    Detecção de traduções ausentes✅ Erro no TypeScript + erro/aviso no build⚠️ lingui extract exibe estatísticas; no runtime recorre silenciosamente ao texto original
    Conteúdo rico (JSX / Markdown / componentes)✅ Suporte nativo e direto✅ Elemento <Trans> com componentes aninhados
    Suporte a ICU⚠️ Em andamento✅ Sim (macros plural, select, selectOrdinal)
    Formatação (datas, números, moedas)useNumber, useDate, ... (suportado por Intl)i18n.date(), i18n.number()
    Roteamento localizado e middleware✅ Proxy e middleware integrados, utilitário getMultilingualUrls❌ Não faz parte do núcleo
    Auxiliares de SEO (hreflang, sitemap...)✅ Utilitários prontos para uso❌ Configuração manual
    Componentes de servidor síncronos (RSC)useIntlayer de next-intlayer/server funciona em qualquer subcomponente de servidor⚠️ Exige uma instância I18n por requisição, repassada manualmente ou via setI18n
    Tree-shaking (entregar só o que for usado)✅ Por componente e por idioma, automatizado pelo compilador⚠️ Por idioma via lingui compile; por rota exige divisão manual de catálogos
    Carregamento sob demanda (Lazy loading)importMode: 'dynamic' (uma única linha de configuração)⚠️ Uso manual de import() de catálogos + i18n.load() / i18n.activate()
    Limpeza de conteúdo não utilizado✅ Dicionários órfãos são descartados no buildlingui extract --clean remove mensagens obsoletas
    Testar traduções ausentes (CLI / CI)npx intlayer content test⚠️ Estatísticas de lingui extract (sem código de falha por padrão)
    Pipeline de compilação✅ Um único plugin (@intlayer/swc / @intlayer/babel / vite-intlayer)⚠️ Plugin de macro (Babel ou SWC) + etapas extract + compile
    Tradução assistida por IA✅ Integrada, utiliza suas próprias chaves de API❌ Não
    Editor Visual / CMS✅ Editor Visual gratuito + CMS opcional❌ Não (.po integra com ferramentas TMS externas)
    Servidor MCP e Agent Skills✅ Sim❌ Não
    Ecossistema e comunidade⚠️ Mais recente, porém em expansão acelerada✅ Etabelecido, agnóstico de framework

    O benchmark

    O que foi medido

    A suíte Benchmark Bloom compila a mesma aplicação com cada biblioteca: 10 páginas (home, about, blog, careers, contact, FAQ, pricing, products, settings, team), 10 idiomas (en, fr, es, de, it, pt, zh, ja, ko, ru), componentes idênticos e conteúdo idêntico. As páginas são avaliadas em en e fr. Cada biblioteca foi avaliada em até quatro estratégias de carregamento:

    EstratégiaDescriçãoCenário de aplicação comum
    staticTodos os catálogos compilados são importados e carregados no inícioProtótipos rápidos, código de IA
    dynamicApenas o catálogo do idioma ativo é importado via import(), mas engloba todas as páginasMaioria dos projetos cotidianos
    scoped-staticUm catálogo por rota, todos embutidos no pacote inicialRaro
    scoped-dynamicUm catálogo por rota + import() dinâmico. Apenas a página atual e o idioma ativoApps com orçamento rigoroso de bytes

    O Intlayer não tem variante "scoped" isolada: o compilador delimita o escopo de conteúdo por componente automaticamente, logo suas linhas static e dynamic já nascem perfeitamente segmentadas.

    Para cada compilação, registram-se:

    • Lib size: tamanho gzip de um componente vazio que importa unicamente a biblioteca de i18n (o custo fixo do runtime).
    • Page JS: média de JavaScript gzip baixado por página, calculada sobre todas as páginas e idiomas.
    • Locale leak %: proporção de strings traduzidas no JS baixado pertencentes a idiomas que o usuário não está visualizando.
    • Page leak %: proporção de strings traduzidas no JS baixado pertencentes a páginas onde o usuário não está navegando.
    • Component avg: tamanho gzip médio de cada componente compilado de forma isolada.
    • E2E reactivity: intervalo de tempo real entre selecionar um novo idioma e o DOM atualizar a tag html[lang] (Playwright, 5 iterações).
    • Hydration: duração da fase de hidratação do React.
    Os dados abaixo referem-se ao teste de 2026-09-12 com @lingui/react 6.6.0 e intlayer 9.5.1. A aplicação de testes é enxuta por design (poucas dezenas de strings por idioma), portanto os índices de vazamento expressam um padrão estrutural que se amplifica com o volume de dados.

    Resultados no Next.js

    BibliotecaEstratégiaLib size (gz)Page JS méd (gz)Vazamento idiomaVazamento páginaComponente méd (gz)Reatividade E2EHidratação
    base (sem i18n)-0,0 KB141,0 KB0,0%0,0%0,9 KB13,4 ms11,8 ms
    Linguistatic11,9 KB207,4 KB50,0%90,0%73,3 KB15,3 ms15,2 ms
    Linguidynamic11,9 KB145,4 KB2,8%89,9%19,9 KB15,7 ms12,7 ms
    Linguiscoped-static11,9 KB148,2 KB2,7%89,1%20,4 KB15,1 ms13,1 ms
    Linguiscoped-dynamic11,9 KB148,6 KB14,8%0,0%152,6 KB16,1 ms14,8 ms
    next-intlayerstatic5,5 KB141,3 KB0,0%0,0%8,5 KB15,5 ms16,9 ms
    next-intlayerdynamic5,5 KB141,3 KB0,0%0,0%6,9 KB15,3 ms15,9 ms

    Análise dos números

    • Custo do runtime. Um componente vazio pesa 11,9 KB gzip no Lingui contra 5,5 KB no Intlayer. Na página completa, o melhor arranjo do Lingui fica em +7,3 KB em relação ao Intlayer (148,6 contra 141,3 KB); o Intlayer situa-se em modestos +0,3 KB acima da aplicação base sem i18n.
    • A abordagem ingênua tem custo elevado. Carregar todos os catálogos compilados no início resulta em 207,4 KB por página, +66 KB sobre o app base. Metade das strings pertence ao idioma errado e 90% a páginas não visualizadas.
    • Carregamento dinâmico ajusta o idioma, mas não a página. Com um catálogo por idioma, o vazamento de página permanece em ~90%: o catálogo completo de francês é entregue em cada página em francês. Para alcançar 0% de vazamento de página no Lingui é necessário recorrer a scoped-dynamic: um catálogo por rota, extraído e compilado individualmente, importado manualmente em cada página.
    • O fallback do idioma de origem vaza por construção. Mesmo nas configurações mais bem planejadas, entre 3% e 15% das strings em inglês são enviadas nas páginas em francês. As macros do Lingui guardam o texto fonte como salvaguarda, de modo que ele aterrissa no bundle. O Intlayer resolve fallbacks no build e entrega exclusivamente o idioma ativo.
    • O tamanho dos componentes isolados dispara em scoped-dynamic. Cada componente compilado isoladamente projeta uma média de 152,6 KB, pois os catálogos das rotas permanecem alcançáveis pelos imports. O mesmo componente usando useIntlayer() registra uma média de 6,9 KB.

    Resultados no TanStack Start

    BibliotecaEstratégiaLib size (gz)Page JS méd (gz)Vazamento idiomaVazamento páginaComponente méd (gz)Reatividade E2EHidratação
    base (sem i18n)-0,0 KB111,0 KB0,0%0,0%0,7 KB8,1 ms21,6 ms
    Linguistatic11,2 KB152,2 KB50,0%90,0%58,0 KB3,9 ms19,9 ms
    Linguidynamic11,2 KB115,2 KB9,3%0,0%85,5 KB5,9 ms28,0 ms
    Linguiscoped-static11,2 KB120,8 KB4,0%0,0%147,9 KB7,1 ms33,9 ms
    Linguiscoped-dynamic11,2 KB120,2 KB8,6%0,0%83,7 KB42,1 ms32,9 ms
    intlayerstatic5,0 KB125,8 KB50,0%0,0%8,1 KB3,2 ms11,5 ms
    intlayerdynamic5,0 KB118,6 KB0,0%0,0%6,3 KB3,6 ms14,1 ms
    @intlayer/lingui (compat)dynamic10,3 KB137,0 KB9,9%0,0%12,8 KB2,9 ms19,7 ms

    Análise dos números

    • No volume de JavaScript por página, o Lingui vence por margem mínima. O Lingui em modo dynamic atinge 115,2 KB, apenas 3,4 KB abaixo dos 118,6 KB do Intlayer. Seus catálogos compilados com hashes são extremamente densos, e o roteador do TanStack Start particiona as rotas tão bem que o vazamento de página é nulo logo na linha dynamic.
    • Todos os outros índices favorecem o Intlayer. A hidratação toma 28-34 ms no Lingui contra 11-14 ms no Intlayer: i18n.load() + i18n.activate() rodam no cliente antes que o React possa hidratar. Os componentes isolados pesam 58-148 KB em vez de 6-8 KB. O vazamento de idioma nunca zera (fica entre 4% e 9%) por causa do fallback inglês.
    • A troca de idioma fica sensivelmente lenta no modo otimizado. O Lingui em scoped-dynamic demanda 42 ms para refletir a nova língua no html[lang], já que o novo catálogo precisa ser baixado, inserido e ativado antes da renderização visual. O Intlayer alterna em 3-4 ms em qualquer modo.
    • A linha static do Intlayer já apresenta 0% de vazamento de página, pois inclui unicamente os dicionários consumidos pelos componentes daquela tela. Uma única linha de configuração (importMode: 'dynamic') extingue igualmente o vazamento de idioma.
    • @intlayer/lingui mantém a sintaxe das macros do Lingui consumindo dicionários do Intlayer. Ele cede um pouco no tamanho final da página (137 KB, devido à presença do runtime de macros) em troca de componentes muito mais leves (12,8 KB) e hidratação mais ágil. Uma excelente rota de transição.

    Qual é a origem desse contraste? Dois compiladores, duas unidades de trabalho

    Ambas as soluções utilizam um compilador. O divisor de águas é o que elas compilam.

    O Lingui compila catálogos. As macros espalhadas pelo seu código são recolhidas em um arquivo .po por idioma e compiladas em um módulo JS por idioma. A unidade central é o idioma. Dividir além disso, por rota ou por componente, pressupõe criar catálogos adicionais, configurar o lingui.config.ts para extrair cada pedaço separadamente e administrar o carregamento manualmente. A instância I18n é global; cada useLingui() subscreve o componente a ela.

    bash
    .
    ├── lingui.config.ts
    └── src
        ├── i18n.ts                          # setupI18n(), load(), activate()
        ├── locales
       ├── en
       ├── messages.po
       └── messages.mjs             # saída de lingui compile
       └── fr
           ├── messages.po
           └── messages.mjs
        ├── components
       └── Counter.tsx                  # const { t } = useLingui(); t`Increment`
        └── routes
            └── $locale
                └── about.tsx                # await import(`../locales/${locale}/messages.mjs`)
    

    O Intlayer compila dicionários. Cada arquivo .content.ts é um dicionário atrelado a uma chave; o compilador identifica qual componente requisita qual chave e gera, por dicionário e por idioma, rigorosamente o JSON que aquele componente precisa. A unidade central é o componente. O escopo por rotas é uma consequência natural: uma tela apenas baixa os dicionários dos componentes que efetivamente renderiza.

    bash
    .
    ├── intlayer.config.ts
    └── src
        ├── components
       └── Counter
           ├── index.tsx                # useIntlayer("counter")
           └── index.content.ts
        └── routes
            └── $locale
                ├── about.tsx
                └── about.content.ts
    

    É por essa razão que o padrão scoped-dynamic é uma entrega nativa e automática do Intlayer, enquanto no Lingui constitui um projeto próprio de engenharia de configuração.

    Para reproduzir as marcas da linha dynamic, defina dictionary.importMode: 'dynamic' no seu intlayer.config.ts. Acesse a documentação sobre otimização de bundle.

    Experiência do desenvolvedor

    Configuração

    Lingui

    lingui.config.ts
    import { defineConfig } from "@lingui/cli";
    
    export default defineConfig({
      sourceLocale: "en",
      locales: ["en", "fr"],
      catalogs: [
        {
          path: "<rootDir>/src/locales/{locale}/messages",
          include: ["src"],
        },
      ],
    });
    
    src/i18n.ts
    import { setupI18n } from "@lingui/core";
    
    export const loadCatalog = async (locale: string) => {
      const { messages } = await import(`./locales/${locale}/messages.mjs`);
      const i18n = setupI18n();
      i18n.load(locale, messages);
      i18n.activate(locale);
      return i18n;
    };
    

    Depois é necessário incluir @lingui/babel-plugin-lingui-macro (ou @lingui/swc-plugin) ao bundler, rodar lingui extract após alterar textos, rodar lingui compile antes do build e encapsular a aplicação em <I18nProvider i18n={i18n}>.

    Intlayer

    intlayer.config.ts
    import { type IntlayerConfig, Locales } from "intlayer";
    
    const config: IntlayerConfig = {
      internationalization: {
        locales: [Locales.ENGLISH, Locales.FRENCH],
        defaultLocale: Locales.ENGLISH,
      },
    };
    
    export default config;
    

    Basta adicionar intlayer() ao seu vite.config.ts (ou withIntlayer() no next.config.ts) e envolver a árvore de renderização com <IntlayerProvider>. Sem etapas de extração ou compilação manuais: os dicionários são montados diretamente quando o empacotador roda.

    Componente

    Lingui

    src/components/Counter.tsx
    import { useState } from "react";
    import { useLingui } from "@lingui/react/macro";
    import { Trans } from "@lingui/react/macro";
    
    export const Counter = () => {
      const { t, i18n } = useLingui();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{i18n.number(count)}</p>
          <button aria-label={t`Counter`} onClick={() => setCount((c) => c + 1)}>
            <Trans>Increment</Trans>
          </button>
        </div>
      );
    };
    

    O texto em inglês vive no próprio componente; a tradução em francês fica em src/locales/fr/messages.po sob um ID em hash após a execução de lingui extract. Esquecer de extrair ou compilar aciona silenciosamente a string em inglês como fallback.

    Intlayer

    src/components/Counter/index.content.ts
    import { t, type Dictionary } from "intlayer";
    
    const counterContent = {
      key: "counter",
      content: {
        label: t({ en: "Counter", fr: "Compteur" }),
        increment: t({ en: "Increment", fr: "Incrémenter" }),
      },
    } satisfies Dictionary;
    
    export default counterContent;
    
    src/components/Counter/index.tsx
    import { useState } from "react";
    import { useIntlayer } from "react-intlayer";
    import { useNumber } from "react-intlayer/format";
    
    export const Counter = () => {
      const { label, increment } = useIntlayer("counter");
      const number = useNumber();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{number(count)}</p>
          <button aria-label={label} onClick={() => setCount((c) => c + 1)}>
            {increment}
          </button>
        </div>
      );
    };
    

    Ambos os idiomas coexistem no mesmo arquivo ao lado do componente. Um valor de fr em falta gera erro no build; uma chave digitada de forma errada aciona o verificador de tipos do TypeScript na hora.

    Fora dos componentes

    Metadados, funções de carregamento (loaders), funções de servidor: em qualquer ponto fora da árvore do React.

    Lingui

    src/routes/$locale/about.tsx
    import { setupI18n } from "@lingui/core";
    import { msg } from "@lingui/core/macro";
    
    const title = msg`About us`;
    
    export const loader = async ({ params }: { params: { locale: string } }) => {
      const { messages } = await import(
        `../../locales/${params.locale}/messages.mjs`
      );
      const i18n = setupI18n({
        locale: params.locale,
        messages: { [params.locale]: messages },
      });
    
      return { title: i18n._(title) };
    };
    

    Gera-se uma nova instância I18n por execução, faz-se a importação manual do catálogo específico e emprega-se msg + i18n._() em vez de t. Conforme registrado nas notas do benchmark, saber a hora exata de recorrer a t, t` ` , i18n.t(), msg ou <Trans> é reconhecidamente confuso.

    Intlayer

    src/routes/$locale/about.tsx
    import { getIntlayer } from "intlayer";
    
    export const loader = async ({ params }: { params: { locale: string } }) => {
      const { title } = getIntlayer("about-metadata", params.locale);
    
      return { title };
    };
    

    Mantenha as macros do Lingui, adote dicionários do Intlayer

    O @intlayer/lingui atua como um adaptador compatível para @lingui/core e @lingui/react. As macros continuam compilando normalmente; as chamadas resultantes para i18n._() passam a ser atendidas pelos dicionários do Intlayer, enquanto os plugins de sincronização .po sustentam seus catálogos originais como fonte da verdade. Plurais e condicionais ICU comportam-se com paridade absoluta.

    vite.config.ts
    import { defineConfig } from "vite";
    import { lingui } from "@intlayer/lingui/plugin";
    
    export default defineConfig({
      plugins: [lingui()],
    });
    

    Mantenha @lingui/babel-plugin-lingui-macro ou @lingui/swc-plugin no processo de build, executando antes do compilador do Intlayer. Verifique a documentação de compatibilidade com Lingui.

    Qual ferramenta escolher?

    • Escolha o Lingui se você precisa do formato ICU MessageFormat com macros tipadas, seus tradutores trabalham em arquivos .po acoplados a sistemas TMS consolidados, sua equipe tem preferência por strings declaradas inline no JSX e domina o ciclo operacional de extração, compilação e divisão de catálogos. Seu peso em JavaScript por página é muito competitivo com lazy loading.
    • Escolha o Intlayer se você busca conteúdo encapsulado por componente, TypeScript rigoroso, erros de compilação para chaves ausentes, tree-shaking e lazy loading automatizados sem esforço de configuração, componentes leves, hidratação instantânea, alternância imediata de idiomas e suíte editorial nativa (Editor Visual, CMS, tradução por IA, servidor MCP). Uma escolha natural para aplicações modulares e design systems.
    • Escolha o @intlayer/lingui se você já opera sobre uma base de código em Lingui e quer evoluir para o modelo de dicionários do Intlayer de modo gradativo sem mexer nas macros.

    Comparações correlatas

    Estrelas no GitHub

    As estrelas no GitHub expressam a aprovação do público, a confiança técnica da comunidade e a sustentabilidade de uma iniciativa a longo prazo. Embora não sejam um aferidor estrito de qualidade de código, revelam o volume de desenvolvedores que confiam na solução.

    Histórico de estrelas

    Conclusão

    O Lingui desponta como a biblioteca híbrida de runtime e compilador mais robusta deste teste comparativo. Seus catálogos compactados com chaves hasheadas proporcionam uma contagem de JavaScript por página a escassos kilobytes do Intlayer, chegando a superá-lo por fração mínima no TanStack Start. Se o peso total por página fosse a única meta, teríamos um empate prático.

    Contudo, não é. O compilador do Lingui encerra sua atuação no limite do idioma; qualquer subdivisão abaixo disso (catálogos por rota, carregamento sob demanda, supressão de textos de fallback) é responsabilidade manual do desenvolvedor. O benchmark explicita o custo dessa barreira: componentes 10 a 20 vezes maiores, hidratação 2 a 3 vezes mais lenta, 3-15% de vazamento perpétuo de textos de fallback e um atraso de 42 ms ao alternar idiomas no modo otimizado. O compilador do Intlayer atua no nível atômico do componente: essas marcas caem por padrão para 6-8 KB, 11-14 ms, 0% e 3-4 ms, sem qualquer esforço de configuração.

    A totalidade dos dados brutos, dos projetos de teste e dos scripts analíticos está aberta no repositório Benchmark Bloom. Fique à vontade para executar as medições em sua máquina.

    Consulte a documentação 'Por que o Intlayer?' para aprofundar seu conhecimento.

    Comentários

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

    Artigos relacionados

    Últimos artigos