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

    Como internacionalizar sua aplicação TanStack Start 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 lingui extract coleta todas as mensagens em catálogos (arquivos PO por padrão), tradutores os preenchem e o plugin do Vite os compila para JavaScript compacto. As mensagens utilizam o ICU MessageFormat, portanto, plurais e seleções são suportados.

    O TanStack Start não possui uma camada nativa de i18n, portanto este guia configura o Lingui nele do zero:

    • Macros compiladas pelo Babel através do @rolldown/plugin-babel (necessário com @vitejs/plugin-react v6 e Vite 8).
    • Roteamento de localidade com um segmento opcional {-$locale} (/about, /fr/about).
    • Um catálogo por localidade, carregado sob demanda, e uma instância de I18n por renderização para que requisições SSR simultâneas nunca compartilhem uma localidade.
    • SEO multilíngue completo: <title> e descrição traduzidos, URL canônica, hreflang com x-default, localidades Open Graph, JSON-LD, sitemap, robots.txt, pré-renderização e páginas 404 localizadas.
    Procurando outra stack? Veja o guia de TanStack Start + use-intl, o guia de TanStack Start + Paraglide ou o guia de TanStack Start + Intlayer.
    Usando Next.js? Veja o guia de Next.js + Lingui. Comparando bibliotecas? Leia Lingui vs Intlayer.

    O que o benchmark diz sobre o Lingui no TanStack Start

    O benchmark de i18n executa a mesma aplicação TanStack Start 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, medidos em 2026-09-26 (gzip):

    ConfiguraçãoTamanho da bibliotecaJS por páginaVazamento de outras localidadesVazamento de outras páginas
    Sem i18n (app base)-111.0 KB0%0%
    Lingui (configuração deste guia)56.7 KB115.2 KB9.3%0%
    @intlayer/lingui (compat)9.8 KB136.7 KB9.9%0%
    react-intlayer (Intlayer nativo)4.5 KB126.8 KB0%0%

    Principais conclusões:

    • Carregue um catálogo por localidade, sob demanda. Isso mantém as páginas próximas ao tamanho da aplicação base.
    • O runtime permanece pesado (~57 KB gzip). O adaptador de compatibilidade @intlayer/lingui (etapa 16) mantém suas macros e o reduz para ~10 KB.
    Veja os dados completos: Relatório de benchmark do TanStack Start e o repositório do benchmark.

    Comparação de recursos no TanStack Start

    Como o Lingui se compara com outras bibliotecas comumente usadas no TanStack Start:

    Recursoreact-intlayer (Intlayer)use-intlParaglide JSLingui
    Traduções próximas aos componentes✅ Co-localizado❌ JSON centralizado❌ Um arquivo JSON por localidade⚠️ Texto fonte nos componentes
    Integração com TypeScript✅ Tipos gerados automaticamente✅ Via AppConfig✅ Funções de mensagens tipadas⚠️ Apenas macros
    Detecção de traduções ausentes✅ Erros de tipo e avisos no build⚠️ Fallback em runtime⚠️ Fallback para localidade base⚠️ Fallback para o texto fonte
    Conteúdo rico (JSX, Markdown)✅ Suporte direto⚠️ Tags via t.rich⚠️ Strings✅ JSX dentro de <Trans>
    Roteamento localizado✅ Integrado❌ Manual {-$locale}✅ urlPatterns + reescrita do roteador❌ Manual {-$locale}
    Troca de localidade sem recarga✅ Sim✅ Sim❌ Recarregamento total da página✅ Sim
    Pluralização✅ Baseada em enumeração✅ ICU✅ Variantes✅ ICU
    ICU MessageFormat✅ Via format: "icu"✅ Nativo⚠️ Via plugin inlang✅ Nativo
    Formatos de conteúdo✅ .ts, .json, .md, .yaml...⚠️ .json⚠️ JSON inlang✅ PO, JSON, CSV
    Tradução com IA✅ Seu próprio provedor e chave❌ Não❌ Não❌ Não
    Editor visual / CMS✅ Editor local + CMS opcional❌ Plataformas externas⚠️ Apps do ecossistema inlang❌ Plataformas externas
    Auxiliares de SEO (hreflang, sitemap)✅ Integrado❌ Manual⚠️ URLs localizadas, restante manual❌ Manual
    Tamanho do runtime (gzip, benchmark)4.5 KB75.9 KB1.8 KB56.7 KB
    Vazamento, melhor configuração (localidade / página)0% / 0%0% / 0%49.7% / 0%8.6% / 0%
    Traduções ausentes no CI✅ npx intlayer test⚠️ Não integrado⚠️ Não integrado✅ lingui compile --strict
    Os números de tamanho de runtime e vazamento vêm do benchmark do TanStack Start. O vazamento é medido na melhor configuração de cada biblioteca.
    Outros guias de TanStack Start: use-intl, Paraglide JS e Intlayer.

    Práticas recomendadas que você deve seguir

    • Defina lang e dir em <html> a partir da localidade da rota, para que fiquem corretos no HTML do servidor.
    • Mantenha uma URL por localidade com um prefixo, para que cada versão de idioma seja indexável.
    • Crie uma instância de I18n por localidade, nunca altere uma global durante o SSR: duas requisições simultâneas sobrescreveriam a localidade uma da outra.
    • Carregue apenas o catálogo ativo, nunca importe todos eles no código do cliente.
    • Escolha um estilo de macro (useLingui + t em componentes, msg para descritores tardios) e mantenha-se fiel a ele. Misturar t, i18n._, i18n.t e <Trans> torna o código mais difícil de ler para humanos e assistentes de IA.
    • Execute lingui extract no CI para que uma nova mensagem nunca seja enviada sem tradução.
    • Traduza seus metadados e declare canonical, hreflang e x-default em cada página.
    • Gere um sitemap multilíngue e robots.txt, e faça a pré-renderização de todas as localidades.
    • Use links reais para o seletor de localidade, para que os rastreadores descubram todos os idiomas.
    Veja nosso guia sobre internacionalização e SEO e o guia de hreflang.

    Guia Passo a Passo para Configurar o Lingui em uma Aplicação TanStack Start

    Aqui está a estrutura de projeto que iremos criar:

    bash
    .
    ├── lingui.config.ts
    ├── vite.config.ts
    └── src
        ├── locales
        │   ├── en
        │   │   └── messages.po     # Generated by `lingui extract`
        │   ├── fr
        │   │   └── messages.po
        │   └── es
        │       └── messages.po
        ├── start.ts                # Request middleware (locale redirect)
        ├── i18n
        │   ├── config.ts           # Locales, URL helpers
        │   ├── lingui.ts           # Catalog loader, I18n instances
        │   ├── negotiateLocale.ts  # Accept-Language parsing
        │   └── seo.ts              # head() builder
        ├── components
        │   ├── LocaleSwitcher.tsx
        │   ├── LocalizedLink.tsx
        │   └── NotFound.tsx
        └── routes
            ├── __root.tsx
            ├── sitemap[.]xml.ts
            ├── robots[.]txt.ts
            └── {-$locale}
                ├── route.tsx       # Locale layout + I18nProvider
                ├── index.tsx
                ├── about.tsx
                └── $.tsx           # Localized 404
    
    1. Instalar Dependências

      bash
      npm install @lingui/core @lingui/react
      npm install -D @lingui/cli @lingui/vite-plugin @lingui/babel-plugin-lingui-macro @lingui/format-po @rolldown/plugin-babel
      
      • @lingui/core / @lingui/react: runtime, I18nProvider e as macros (@lingui/core/macro, @lingui/react/macro).
      • @lingui/cli: lingui extract para coletar mensagens em catálogos.
      • @lingui/vite-plugin: compila catálogos .po na importação, dispensando o uso de lingui compile.
      • @lingui/babel-plugin-lingui-macro + @rolldown/plugin-babel: transformam as macros no momento do build.
    2. Centralizar a Configuração de Localidades

      A localidade padrão permanece sem prefixo (/about), enquanto as outras localidades são prefixadas (/fr/about).

      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 = "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);
      
      /** Maps the optional `{-$locale}` route param to a supported locale. */
      export const resolveLocale = (localeParam: string | undefined): Locale =>
        isLocale(localeParam) ? localeParam : defaultLocale;
      
      /** The value to pass as `locale` param: `undefined` for the default locale. */
      export const toLocaleParam = (locale: Locale): Locale | undefined =>
        locale === defaultLocale ? undefined : locale;
      
      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}`;
      };
      
      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

      A configuração do Lingui reutiliza a mesma lista de localidades, garantindo que os catálogos, o roteador e o sitemap nunca entrem em conflito.

      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 }),
      });
      

      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"
        }
      }
      

      O script i18n:check falha no CI quando um componente contém uma mensagem que não foi extraída e commitada.

    4. Configurar o Vite

      Com o @vitejs/plugin-react v6, o Babel não vem mais embutido. O @rolldown/plugin-babel executa o plugin de macros do Lingui, e o linguiTransformerBabelPreset processa apenas arquivos que importam uma macro, mantendo os builds rápidos.

      vite.config.ts
      import { lingui, linguiTransformerBabelPreset } from "@lingui/vite-plugin";
      import babel from "@rolldown/plugin-babel";
      import { tanstackStart } from "@tanstack/react-start/plugin/vite";
      import viteReact from "@vitejs/plugin-react";
      import { defineConfig } from "vite";
      
      export default defineConfig({
        plugins: [
          tanstackStart(),
          viteReact(),
          lingui(),
          babel({ presets: [linguiTransformerBabelPreset()] }),
        ],
      });
      
    5. Carregar Catálogos por Localidade

      O template literal em import() permite que o Vite emita um chunk por catálogo, e o plugin do Lingui compila o arquivo .po dentro dele. Um visitante francês baixa apenas o catálogo em francês.

      As mensagens compiladas são dados puros, portanto podem ser retornadas por um loader de rota, serializadas no HTML e reutilizadas na hidratação.

      src/i18n/lingui.ts
      import { type I18n, type Messages, setupI18n } from "@lingui/core";
      import type { Locale } from "./config";
      
      /**
       * Loads the compiled catalog of one locale (one chunk per locale).
       */
      export const loadCatalog = async (locale: Locale): Promise<Messages> => {
        const { messages } = await import(`../locales/${locale}/messages.po`);
      
        return messages;
      };
      
      /**
       * Creates an isolated I18n instance: safe for concurrent SSR requests.
       */
      export const createI18n = (locale: Locale, messages: Messages): I18n =>
        setupI18n({ locale, messages: { [locale]: messages } });
      
      /**
       * Loads a catalog and returns a ready-to-use instance, for loaders and
       * server functions.
       */
      export const loadI18n = async (locale: Locale): Promise<I18n> =>
        createI18n(locale, await loadCatalog(locale));
      

      Para que o TypeScript aceite 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;
      }
      
    6. Criar o Documento Raiz

      A rota raiz lê o parâmetro opcional de localidade para definir lang e dir no <html> renderizado no servidor.

      src/routes/__root.tsx
      import {
        createRootRoute,
        HeadContent,
        Scripts,
        useParams,
      } from "@tanstack/react-router";
      import type { ReactNode } from "react";
      import { getTextDirection, resolveLocale } from "@/i18n/config";
      
      export const Route = createRootRoute({
        head: () => ({
          meta: [
            { charSet: "utf-8" },
            { name: "viewport", content: "width=device-width, initial-scale=1" },
          ],
        }),
        shellComponent: RootDocument,
      });
      
      function RootDocument({ children }: { children: ReactNode }) {
        const { locale: localeParam } = useParams({ strict: false });
        const locale = resolveLocale(localeParam);
      
        return (
          <html lang={locale} dir={getTextDirection(locale)}>
            <head>
              <HeadContent />
            </head>
            <body>
              {children}
              <Scripts />
            </body>
          </html>
        );
      }
      
    7. Criar a Rota de Layout de Localidade

      A pasta {-$locale} cria um segmento de caminho opcional: /about e /fr/about correspondem a /{-$locale}/about. O layout rejeita prefixos desconhecidos, carrega o catálogo da localidade atual e fornece uma instância dedicada de I18n.

      src/routes/{-$locale}/route.tsx
      import { I18nProvider } from "@lingui/react";
      import { createFileRoute, notFound, Outlet } from "@tanstack/react-router";
      import { useMemo } from "react";
      import { Header } from "@/components/Header";
      import { NotFound } from "@/components/NotFound";
      import { isLocale, resolveLocale } from "@/i18n/config";
      import { createI18n, loadCatalog } from "@/i18n/lingui";
      
      export const Route = createFileRoute("/{-$locale}")({
        beforeLoad: ({ params }) => {
          if (params.locale !== undefined && !isLocale(params.locale)) {
            throw notFound();
          }
        },
        loader: async ({ params }) => {
          const locale = resolveLocale(params.locale);
      
          return { locale, messages: await loadCatalog(locale) };
        },
        // A catalog never changes for a given locale
        staleTime: Infinity,
        component: LocaleLayout,
        notFoundComponent: NotFound,
      });
      
      function LocaleLayout() {
        const { locale, messages } = Route.useLoaderData();
      
        // One instance per locale, never shared between requests
        const i18n = useMemo(() => createI18n(locale, messages), [locale, messages]);
      
        return (
          <I18nProvider i18n={i18n}>
            <Header />
            <main>
              <Outlet />
            </main>
          </I18nProvider>
        );
      }
      
    8. Utilizar Traduções em Suas Páginas

      Escreva o texto de origem no componente. As macros o transformam em IDs de mensagem durante o build, e o lingui extract o captura.

      • <Trans> para conteúdo JSX, incluindo elementos aninhados;
      • useLingui().t para strings (atributos, propriedades);
      • <Plural> para plurais ICU.
      src/routes/{-$locale}/about.tsx
      import { msg } from "@lingui/core/macro";
      import { Plural, Trans, useLingui } from "@lingui/react/macro";
      import { createFileRoute } from "@tanstack/react-router";
      import { useState } from "react";
      import { resolveLocale } from "@/i18n/config";
      import { loadI18n } from "@/i18n/lingui";
      import { buildLocalizedHead } from "@/i18n/seo";
      
      export const Route = createFileRoute("/{-$locale}/about")({
        // Translate the metadata in the loader: head() stays synchronous
        loader: async ({ params }) => {
          const i18n = await loadI18n(resolveLocale(params.locale));
      
          return {
            metadata: {
              title: i18n._(msg`About us`),
              description: i18n._(
                msg`Learn who we are and why we built this application.`
              ),
            },
          };
        },
        staleTime: Infinity,
        head: ({ params, loaderData }) =>
          loaderData
            ? buildLocalizedHead({
                path: "/about",
                locale: resolveLocale(params.locale),
                ...loaderData.metadata,
              })
            : {},
        component: AboutPage,
      });
      
      function AboutPage() {
        const { t } = useLingui();
        const [count, setCount] = useState(0);
      
        return (
          <>
            <h1>
              <Trans>About us</Trans>
            </h1>
            <p>
              <Plural
                value={count}
                _0="No clicks yet"
                one="# click"
                other="# clicks"
              />
            </p>
            <button
              type="button"
              aria-label={t`Counter`}
              onClick={() => setCount((value) => value + 1)}
            >
              <Trans>Increment</Trans>
            </button>
          </>
        );
      }
      
      O import() dinâmico de um catálogo é armazenado em cache pelo sistema de módulos, portanto, chamar loadI18n em múltiplos loaders não faz o download do catálogo duas vezes.
    9. Extrair e Traduzir Suas Mensagens

      Execute a extração. O Lingui grava cada mensagem no catálogo 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 "Learn who we are and why we built this application."
      msgstr "Découvrez qui nous sommes et pourquoi nous avons créé cette application."
      
      msgid "Increment"
      msgstr "Incrémenter"
      
      msgid "Counter"
      msgstr "Compteur"
      
      msgid "{count, plural, =0 {No clicks yet} one {# click} other {# clicks}}"
      msgstr "{count, plural, =0 {Aucun clic} one {# clic} other {# clics}}"
      
      src/locales/es/messages.po
      msgid "About us"
      msgstr "Sobre nosotros"
      
      msgid "Learn who we are and why we built this application."
      msgstr "Descubre quiénes somos y por qué creamos esta aplicación."
      
      msgid "Increment"
      msgstr "Incrementar"
      
      msgid "Counter"
      msgstr "Contador"
      
      msgid "{count, plural, =0 {No clicks yet} one {# click} other {# clicks}}"
      msgstr "{count, plural, =0 {Ningún clic} one {# clic} other {# clics}}"
      
      Por padrão, os IDs das mensagens são hashes do texto de origem: alterar o texto em inglês cria uma nova mensagem. Use IDs explícitos (<Trans id="about.title">About us</Trans>) para textos que mudam com frequência.
    10. Opcional

      Cada rota existe sob {-$locale}, portanto os links devem carregar o parâmetro da localidade atual.

      src/components/LocalizedLink.tsx
      import { useLingui } from "@lingui/react";
      import { Link, type LinkComponentProps } from "@tanstack/react-router";
      import { type Locale, toLocaleParam } from "@/i18n/config";
      
      type LocalizedLinkProps = Omit<LinkComponentProps, "params">;
      
      export const LocalizedLink = (props: LocalizedLinkProps) => {
        const { i18n } = useLingui();
      
        return (
          <Link
            {...props}
            params={{ locale: toLocaleParam(i18n.locale as Locale) }}
          />
        );
      };
      
    11. Alterar o Idioma do Seu Conteúdo

      Opcional

      Renderize o seletor como links, para que os rastreadores encontrem todas as versões de idioma. to="." mantém a página atual e substitui o parâmetro de localidade. O loader do layout de localidade então busca o novo catálogo.

      src/components/LocaleSwitcher.tsx
      import { useLingui } from "@lingui/react/macro";
      import { Link } from "@tanstack/react-router";
      import {
        getLocaleName,
        type Locale,
        localeCookieName,
        locales,
        toLocaleParam,
      } from "@/i18n/config";
      
      const persistLocale = (locale: Locale) => {
        document.cookie = `${localeCookieName}=${locale}; Path=/; Max-Age=31536000; SameSite=Lax`;
      };
      
      export const LocaleSwitcher = () => {
        // The macro version also returns the i18n instance
        const { i18n, t } = useLingui();
      
        return (
          <nav aria-label={t`Change language`}>
            <ul>
              {locales.map((locale) => (
                <li key={locale}>
                  <Link
                    to="."
                    params={(previous) => ({
                      ...previous,
                      locale: toLocaleParam(locale),
                    })}
                    hrefLang={locale}
                    lang={locale}
                    aria-current={locale === i18n.locale ? "page" : undefined}
                    onClick={() => persistLocale(locale)}
                  >
                    {getLocaleName(locale)}
                  </Link>
                </li>
              ))}
            </ul>
          </nav>
        );
      };
      
    12. Internacionalizar Seus Metadados

      Opcional

      Cada versão de idioma pode se posicionar de forma independente nos mecanismos de busca, desde que cada página exponha um <title> e descrição traduzidos, uma URL canônica autorreferenciada, uma tag hreflang por localidade mais x-default, localidades Open Graph e JSON-LD com inLanguage. Os metadados são traduzidos no loader (etapa 8), e este utilitário constrói o restante:

      src/i18n/seo.ts
      import {
        defaultLocale,
        getAbsoluteUrl,
        type Locale,
        locales,
        openGraphLocales,
      } from "./config";
      
      type LocalizedHeadOptions = {
        /** Path without locale prefix, e.g. "/about" */
        path: string;
        locale: Locale;
        title: string;
        description: string;
      };
      
      export const buildLocalizedHead = ({
        path,
        locale,
        title,
        description,
      }: LocalizedHeadOptions) => {
        const url = getAbsoluteUrl(path, locale);
      
        return {
          meta: [
            { title },
            { name: "description", content: description },
            { property: "og:type", content: "website" },
            { property: "og:title", content: title },
            { property: "og:description", content: description },
            { property: "og:url", content: url },
            { property: "og:locale", content: openGraphLocales[locale] },
            ...locales
              .filter((alternateLocale) => alternateLocale !== locale)
              .map((alternateLocale) => ({
                property: "og:locale:alternate",
                content: openGraphLocales[alternateLocale],
              })),
          ],
          links: [
            { rel: "canonical", href: url },
            ...locales.map((alternateLocale) => ({
              rel: "alternate",
              hrefLang: alternateLocale,
              href: getAbsoluteUrl(path, alternateLocale),
            })),
            {
              rel: "alternate",
              hrefLang: "x-default",
              href: getAbsoluteUrl(path, defaultLocale),
            },
          ],
          scripts: [
            {
              type: "application/ld+json",
              children: JSON.stringify({
                "@context": "https://schema.org",
                "@type": "WebPage",
                name: title,
                description,
                url,
                inLanguage: locale,
              }),
            },
          ],
        };
      };
      
    13. Internacionalizar Seu Sitemap e robots.txt

      Opcional

      O sitemap lista todas as URLs de cada localidade, com cada entrada declarando todas as suas alternativas usando xhtml:link. O robots.txt bloqueia rotas privadas em todos os idiomas e aponta para o sitemap. Remova public/robots.txt caso o starter tenha criado um.

      src/routes/sitemap[.]xml.ts
      import { createFileRoute } from "@tanstack/react-router";
      import { defaultLocale, getAbsoluteUrl, locales } from "@/i18n/config";
      
      type SitemapPage = {
        path: string;
        changeFrequency: "daily" | "weekly" | "monthly";
        priority: number;
      };
      
      export const sitemapPages: SitemapPage[] = [
        { path: "/", changeFrequency: "daily", priority: 1.0 },
        { path: "/about", changeFrequency: "monthly", priority: 0.8 },
      ];
      
      const buildAlternateLinks = (path: string): string =>
        [
          ...locales.map(
            (locale) =>
              `<xhtml:link rel="alternate" hreflang="${locale}" href="${getAbsoluteUrl(path, locale)}"/>`
          ),
          `<xhtml:link rel="alternate" hreflang="x-default" href="${getAbsoluteUrl(path, defaultLocale)}"/>`,
        ].join("");
      
      const buildSitemap = (): string => {
        const urls = sitemapPages.flatMap((page) =>
          locales.map(
            (locale) =>
              `<url><loc>${getAbsoluteUrl(page.path, locale)}</loc>${buildAlternateLinks(page.path)}<changefreq>${page.changeFrequency}</changefreq><priority>${page.priority}</priority></url>`
          )
        );
      
        return `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">${urls.join("")}</urlset>`;
      };
      
      export const Route = createFileRoute("/sitemap.xml")({
        server: {
          handlers: {
            GET: () =>
              new Response(buildSitemap(), {
                headers: { "Content-Type": "application/xml; charset=utf-8" },
              }),
          },
        },
      });
      
      src/routes/robots[.]txt.ts
      import { createFileRoute } from "@tanstack/react-router";
      import { locales, localizePath, siteUrl } from "@/i18n/config";
      
      const privatePaths = ["/dashboard", "/admin"];
      
      const buildRobots = (): string =>
        [
          "User-agent: *",
          "Allow: /",
          ...privatePaths.flatMap((path) =>
            locales.map((locale) => `Disallow: ${localizePath(path, locale)}`)
          ),
          "",
          `Sitemap: ${siteUrl}/sitemap.xml`,
        ].join("\n");
      
      export const Route = createFileRoute("/robots.txt")({
        server: {
          handlers: {
            GET: () =>
              new Response(buildRobots(), {
                headers: { "Content-Type": "text/plain; charset=utf-8" },
              }),
          },
        },
      });
      
    14. Pré-renderizar Todas as Localidades

      Opcional

      Liste todos os caminhos localizados para que o TanStack Start pré-renderize todas as versões de idioma no momento do build:

      vite.config.ts
      import { lingui, linguiTransformerBabelPreset } from "@lingui/vite-plugin";
      import babel from "@rolldown/plugin-babel";
      import { tanstackStart } from "@tanstack/react-start/plugin/vite";
      import viteReact from "@vitejs/plugin-react";
      import { defineConfig } from "vite";
      import { locales, localizePath } from "./src/i18n/config";
      
      const pagePaths = ["/", "/about"];
      
      const localizedPages = pagePaths.flatMap((path) =>
        locales.map((locale) => ({
          path: localizePath(path, locale),
          prerender: { enabled: true },
        }))
      );
      
      export default defineConfig({
        plugins: [
          tanstackStart({
            prerender: { enabled: true, crawlLinks: true },
            pages: [
              ...localizedPages,
              { path: "/sitemap.xml", prerender: { enabled: true } },
              { path: "/robots.txt", prerender: { enabled: true } },
            ],
          }),
          viteReact(),
          lingui(),
          babel({ presets: [linguiTransformerBabelPreset()] }),
        ],
      });
      
    15. Redirecionar Visitantes de Primeira Viagem e Tratar Páginas 404

      Opcional

      Um middleware de requisição encaminha um visitante que acessa / para o seu idioma preferido (cookie primeiro, depois Accept-Language). Links profundos nunca são redirecionados, garantindo que rastreadores e URLs compartilhadas sempre acessem a página solicitada.

      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/start.ts
      import { redirect } from "@tanstack/react-router";
      import { createMiddleware, createStart } from "@tanstack/react-start";
      import { getCookie } from "@tanstack/react-start/server";
      import { defaultLocale, isLocale, localeCookieName } from "@/i18n/config";
      import { negotiateLocale } from "@/i18n/negotiateLocale";
      
      const localeRedirectMiddleware = createMiddleware().server(
        ({ request, next }) => {
          if (new URL(request.url).pathname !== "/") return next();
      
          const cookieLocale = getCookie(localeCookieName);
          const preferredLocale = isLocale(cookieLocale)
            ? cookieLocale
            : negotiateLocale(request.headers.get("accept-language"));
      
          if (preferredLocale && preferredLocale !== defaultLocale) {
            throw redirect({ href: `/${preferredLocale}`, statusCode: 307 });
          }
      
          return next();
        }
      );
      
      export const startInstance = createStart(() => ({
        requestMiddleware: [localeRedirectMiddleware],
      }));
      

      Para páginas 404, uma rota catch-all renderiza o notFoundComponent localizado do layout. Marque-a com noindex: o React 19 eleva o <meta> para o <head>.

      src/components/NotFound.tsx
      import { Trans } from "@lingui/react/macro";
      import { LocalizedLink } from "./LocalizedLink";
      
      export const NotFound = () => (
        <div>
          <meta name="robots" content="noindex" />
          <h1>
            <Trans>Page not found</Trans>
          </h1>
          <LocalizedLink to="/{-$locale}">
            <Trans>Back to home</Trans>
          </LocalizedLink>
        </div>
      );
      
      src/routes/{-$locale}/$.tsx
      import { createFileRoute, notFound } from "@tanstack/react-router";
      
      export const Route = createFileRoute("/{-$locale}/$")({
        beforeLoad: () => {
          throw notFound();
        },
      });
      
    16. Mantenha Suas Macros, Reduza o Runtime com o Intlayer

      Opcional

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

      bash
      npm install @intlayer/lingui intlayer @intlayer/sync-json-plugin
      npx intlayer init
      

      Adicione o plugin após a transformação de macros, para que ele crie aliases de @lingui/core e @lingui/react para o adaptador:

      vite.config.ts
      import { lingui as linguiIntlayer } from "@intlayer/lingui/plugin";
      import { linguiTransformerBabelPreset } from "@lingui/vite-plugin";
      import babel from "@rolldown/plugin-babel";
      import { tanstackStart } from "@tanstack/react-start/plugin/vite";
      import viteReact from "@vitejs/plugin-react";
      import { defineConfig } from "vite";
      
      export default defineConfig({
        plugins: [
          tanstackStart(),
          viteReact(),
          babel({ presets: [linguiTransformerBabelPreset()] }),
          linguiIntlayer(),
        ],
      });
      

      Os catálogos são sincronizados com o plugin de sincronização JSON (catálogos JSON) ou o plugin de sincronização PO (catálogos PO). Veja a configuração completa no guia de compatibilidade do Lingui e uma comparação lado a lado em Lingui vs @intlayer/lingui.

    17. Automatize Suas Traduções Usando o 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 suas ferramentas funcionam perfeitamente ao lado do Lingui:

    Perguntas Frequentes

    Sim. O Lingui não possui uma integração dedicada para o TanStack Start, mas seu plugin Vite e o plugin de macro Babel funcionam perfeitamente. Os dois pontos cruciais são executar as macros através do @rolldown/plugin-babel (o Vite 8 e o @vitejs/plugin-react v6 não incluem mais o Babel) e criar uma instância de I18n por localidade em vez de ativar uma global durante o SSR.

    No servidor, um único processo renderiza muitas requisições ao mesmo tempo. Chamar i18n.activate("fr") em um objeto compartilhado alteraria o idioma de uma requisição sendo renderizada em inglês em paralelo. O setupI18n cria uma instância isolada por localidade, o que é seguro.

    Não. O @lingui/vite-plugin compila os catálogos .po quando eles são importados. Você só precisa executar lingui extract para coletar novas mensagens.

    Declare-os com a macro msg e traduza-os no loader da rota com i18n._(msg`...`). O loader retorna strings puras, portanto o head() permanece síncrono e os valores são serializados para a hidratação. A etapa 8 e a etapa 12 mostram a configuração completa.

    O benchmark mede ~56.7 KB gzip para o runtime. Com um catálogo por localidade carregado sob demanda, as páginas pesam ~115 KB contra 111 KB sem i18n. Importar todos os catálogos estaticamente eleva o tamanho para ~152 KB.

    Sim. O adaptador @intlayer/lingui mantém as macros e substitui o runtime. Depois, você pode migrar os componentes para useIntlayer um de cada vez. Veja os adaptadores de compatibilidade.

    Comentários

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

    Artigos relacionados

    Últimos artigos