Criação:2025-09-10Última atualização:2025-09-10

    i18n por componente vs. centralizado

    A abordagem por componente não é um conceito novo. Por exemplo, no ecossistema Vue, o vue-i18n suporta i18n SFC (Single File Component). O Nuxt também oferece traduções por componente, e o Angular emprega um padrão similar através dos seus Feature Modules.

    Mesmo em um app Flutter, muitas vezes encontramos este padrão:

    lib/
    └── features/
        └── login/
            ├── login_screen.dart
            └── login_screen.i18n.dart  # <- As traduções ficam aqui
    import 'package:i18n_extension/i18n_extension.dart';
    
    extension Localization on String {
      static var _t = Translations.byText("en") +
          {
            "Hello": {
              "en": "Hello",
              "fr": "Bonjour",
            },
          };
    
      String get i18n => localize(this, _t);
    }

    No entanto, no mundo React, vemos principalmente abordagens diferentes, que agruparei em três categorias:

    Abordagem centralizada (i18next, next-intl, react-intl, lingui)

    • (sem namespaces) considera uma única fonte para recuperar conteúdo. Por padrão, você carrega o conteúdo de todas as páginas quando seu app é carregado.

    Abordagem granular (intlayer, inlang)

    • refina a recuperação de conteúdo por chave ou por componente.

    Neste blog, não vou focar em soluções baseadas em compilador, que já abordei aqui: Compiler vs Declarative i18n. Note que i18n baseado em compilador (por exemplo, Lingui) simplesmente automatiza a extração e o carregamento do conteúdo. Por baixo do capô, eles frequentemente compartilham as mesmas limitações que outras abordagens.

    Note que quanto mais você refina a forma como recupera seu conteúdo, maior o risco de inserir estado e lógica adicionais nos seus componentes.

    As abordagens granulares são mais flexíveis do que as centralizadas, mas frequentemente implicam um compromisso. Mesmo que o "tree shaking" seja divulgado por essas bibliotecas, na prática acaba por carregar uma página em todas as línguas.

    Portanto, de forma geral, a decisão resume-se assim:

    • Se a sua aplicação tem mais páginas do que línguas, deve favorecer uma abordagem granular.
    • Se tiver mais línguas do que páginas, deve optar por uma abordagem centralizada.

    Obviamente, os autores das bibliotecas estão cientes dessas limitações e oferecem soluções alternativas. Entre elas: dividir em namespaces, carregar ficheiros JSON dinamicamente (await import()), ou purgar conteúdo em build time.

    Ao mesmo tempo, deve saber que quando carrega dinamicamente o seu conteúdo, introduz pedidos adicionais ao seu servidor. Cada useState extra ou hook significa um pedido extra ao servidor.

    Para resolver este ponto, o Intlayer sugere agrupar múltiplas definições de conteúdo sob a mesma chave; o Intlayer irá então mesclar esse conteúdo.

    Mas, de todas essas soluções, fica claro que a abordagem mais popular é a centralizada.

    • Primeiro, o i18next foi a primeira solução a tornar-se amplamente utilizada, seguindo uma filosofia inspirada nas arquiteturas PHP e Java (MVC), que se baseiam numa separação estrita de responsabilidades (mantendo o conteúdo separado do código). Chegou em 2011, estabelecendo os seus padrões mesmo antes da grande mudança para arquiteturas baseadas em Componentes (como o React).
    • Depois, uma vez que uma biblioteca é amplamente adotada, torna-se difícil migrar o ecossistema para outros padrões.
    • Usar uma abordagem centralizada também facilita as coisas em sistemas de gestão de traduções (Translation Management Systems) como Crowdin, Phrase ou Localized.
    • A lógica da abordagem por componente é mais complexa do que a centralizada e requer tempo extra de desenvolvimento, especialmente quando é necessário resolver problemas como identificar onde o conteúdo está localizado.

    Ok, mas por que não ficar apenas com uma abordagem Centralizada?

    Deixe-me explicar por que isso pode ser problemático para a sua app:

    • Dados não utilizados: Quando uma página carrega, costuma-se carregar o conteúdo de todas as outras páginas. (Numa app de 10 páginas, isso significa 90% de conteúdo carregado que não é utilizado). Você faz lazy load de um modal? A biblioteca i18n não se importa, ela carrega as strings primeiro de qualquer forma.
    • Desempenho: A cada re-render, todos os seus componentes são hidratados com um payload JSON massivo, o que impacta a reatividade da sua app à medida que ela cresce.
    • Manutenção: Manter ficheiros JSON grandes é doloroso. Você tem de saltar entre ficheiros para inserir uma tradução, garantindo que não faltam traduções e que não restam chaves órfãs.
    • Sistema de design: Isto cria incompatibilidade com design systems (por exemplo, um componente LoginForm) e restringe a duplicação de componentes entre diferentes apps.

    "Mas inventámos Namespaces!"

    Claro, e isso é um enorme avanço. Vamos ver a comparação do tamanho do bundle principal de uma stack Vite + React + React Router v7 + Intlayer. Simulámos uma aplicação com 20 páginas.

    O primeiro exemplo não inclui traduções lazy-loaded por locale nem divisão por namespaces. O segundo inclui purga de conteúdo + carregamento dinâmico das traduções.

    Bundle otimizado Bundle não otimizado
    bundle não otimizado bundle otimizado

    Portanto, graças aos namespaces, mudámos desta estrutura:

    locale/
    ├── en.json
    ├── fr.json
    └── es.json

    To this one:

    locale/
    ├── en/
       ├── common.json
       ├── navbar.json
       ├── footer.json
       ├── home.json
       └── about.json
    ├── fr/
       └── ...
    └── es/
        └── ...
    

    Agora tem de gerir com precisão que parte do conteúdo da sua app deve ser carregada e onde. Em conclusão, a grande maioria dos projetos simplesmente ignora esta etapa devido à complexidade (veja o guia do next-i18next, por exemplo, para ver os desafios que isso representa (apenas) ao seguir as boas práticas). Consequentemente, esses projetos acabam com o problema do carregamento massivo de JSON explicado anteriormente.

    Note que este problema não é específico do i18next, mas de todas as abordagens centralizadas listadas acima.

    No entanto, quero relembrar que nem todas as abordagens granulares resolvem isto. Por exemplo, as abordagens vue-i18n SFC ou inlang não fazem, por si só, lazy load das traduções por locale, pelo que você está simplesmente a trocar o problema do tamanho do bundle por outro.

    Além disso, sem uma separação de responsabilidades adequada, torna-se muito mais difícil extrair e fornecer as suas traduções aos tradutores para revisão.

    Como a abordagem por componente do Intlayer resolve isto

    O Intlayer procede em vários passos:

    1. Declaração: Declare o seu conteúdo em qualquer parte da sua codebase usando ficheiros *.content.{ts|jsx|cjs|json|json5|...}. Isto assegura separação de responsabilidades enquanto mantém o conteúdo co-localizado com o código. Um ficheiro de conteúdo pode ser por locale ou multilíngue.
    2. Processamento: Intlayer executa uma etapa de build para processar a lógica JS, tratar fallbacks de traduções ausentes, gerar tipos TypeScript, gerenciar conteúdo duplicado, buscar conteúdo do seu CMS e mais.
    3. Purgamento: Quando sua aplicação é construída, o Intlayer purga o conteúdo não utilizado (um pouco como o Tailwind gerencia suas classes) substituindo o conteúdo da seguinte forma:

    Declaração:

    // src/MyComponent.tsx
    export const MyComponent = () => {
      const content = useIntlayer("my-key");
      return <h1>{content.title}</h1>;
    };
    // src/myComponent.content.ts
    export const {
      key: "my-key",
      content: t({
        en: { title: "My title" },
        fr: { title: "Mon titre" }
      })
    }
    

    Processamento: Intlayer constrói o dicionário com base no arquivo .content e gera:

    // .intlayer/dynamic_dictionary/en/my-key.json
    {
      "key": "my-key",
      "content": { "title": "My title" },
    }

    Substituição: intlayer extracta seu componente durante o build da aplicação.

    - Modo de Importação Estática:

    // Representação do componente em sintaxe semelhante a JSX
    export const MyComponent = () => {
      const content = useDictionary({
        key: "my-key",
        content: {
          nodeType: "translation",
          translation: {
            pt: { title: "Meu título" },
            en: { title: "My title" },
            fr: { title: "Mon titre" },
          },
        },
      });
    
      return <h1>{content.title}</h1>;
    };

    - Modo de Importação Dinâmica:

    // Representação do componente em sintaxe semelhante a JSX
    export const MyComponent = () => {
      const content = useDictionaryAsync({
        en: () =>
          import(".intlayer/dynamic_dictionary/en/my-key.json", {
            with: { type: "json" },
          }).then((mod) => mod.default),
        // O mesmo para outras línguas
      });
    
      return <h1>{content.title}</h1>;
    };
    useDictionaryAsync usa um mecanismo semelhante ao Suspense para carregar o JSON localizado apenas quando necessário.

    Principais vantagens desta abordagem por componente:

    • Manter a declaração do conteúdo próxima dos seus componentes permite uma melhor facilidade de manutenção (por exemplo, mover um componente para outra app ou design system. Eliminar a pasta do componente remove também o conteúdo relacionado, como provavelmente já faz com os seus .test, .stories)

    /// Uma abordagem por componente impede que agentes de IA precisem saltar entre todos os seus diferentes ficheiros. Trata todas as traduções num só lugar, limitando a complexidade da tarefa e a quantidade de tokens utilizados.

    Limitações

    Claro, esta abordagem implica compensações:

    • É mais difícil ligar a outros sistemas de l10n e a ferramentas adicionais.
    • Fica-se preso (o que basicamente já acontece com qualquer solução i18n devido à sua sintaxe específica).

    É por isso que o Intlayer tenta fornecer um conjunto completo de ferramentas para i18n (100% free and OSS), incluindo tradução por IA usando o seu próprio AI Provider e as suas chaves de API. O Intlayer também fornece ferramentas para sincronizar os seus ficheiros JSON, funcionando como formatadores de mensagens do ICU / vue-i18n / i18next para mapear o conteúdo para os seus formatos específicos.