Contexto e Requisitos
Queremos implementar suporte multilíngue sob as seguintes restrições:
- Next.js 15 App Router com TypeScript
- Static Export (
output: 'export') — Deploy via S3 + CloudFront - Sem Server-Side Rendering — APIs dinâmicas e middleware não podem ser usados
- SEO Necessário — Deve fornecer conteúdo no idioma apropriado para os motores de busca
Visão Geral da Arquitetura
1. Design de Roteamento
/ # Página raiz (padrão: en)
/en/ # Página em inglês
/ja/ # Página em japonês
Usa Segmento Dinâmico [locale] para pre-renderizar páginas de cada idioma durante a geração estática.
2. Gerenciamento de Dados de Tradução
Os dados de tradução são gerenciados no formato JSON:
src/locales/en.json
{
"title": "title"
}
src/locales/ja.json
{
"title": "タイトル"
}
3. Gerenciamento de Estado com a API React Context
src/i18n/context.tsx
interface I18nContextType {
messages: Record<string, any>;
locale: string;
}
const I18nContext = createContext<I18nContextType | undefined>(undefined);
export function I18nProvider({
children,
messages,
locale
}: I18nProviderProps) {
return (
<I18nContext.Provider value={{ messages, locale }}>
{children}
</I18nContext.Provider>
);
}
export function useTranslations() {
const context = useContext(I18nContext);
if (!context) {
throw new Error('useTranslations must be used within I18nProvider');
}
const t = (key: string): string => {
// Suporte a chaves aninhadas
const keys = key.split('.');
let value: any = context.messages;
for (const k of keys) {
value = value?.[k];
}
return typeof value === 'string' ? value : key;
};
return {
t,
locale: context.locale,
messages: context.messages,
isLoaded: true
};
}
4. Fornecimento de Context na Hierarquia de Layout
src/app/layout.tsx
export default async function RootLayout({ children }: Props) {
const locale = defaultLocale;
const messages = await getMessages(locale);
return (
<html lang={locale}>
<body>
<I18nProvider messages={messages} locale={locale}>
<LanguageSwitcherClient />
{children}
</I18nProvider>
</body>
</html>
);
}
src/app/[locale]/layout.tsx
export default async function LocaleLayout({ children, params }: Props) {
const { locale } = await params;
const messages = await getMessages(locale);
return (
<div lang={locale}>
<I18nProvider messages={messages} locale={locale}>
{children}
</I18nProvider>
</div>
);
}
Idioma Padrão e Redirecionamento no Cliente
No Servidor (Durante a Geração Estática)
A página raiz (/) é sempre gerada em inglês (en):
src/app/page.tsx
export default async function RootPage() {
const locale = defaultLocale; // 'en'
const messages = await getMessages(locale);
return (
<I18nProvider messages={messages} locale={locale}>
<ClientLocaleRedirect />
<PageContent />
</I18nProvider>
);
}
Justificativa:
- Estratégia de SEO: Fornecer conteúdo estável para crawlers de motores de busca
- Restrições de exportação estática: Não é possível determinar o idioma do usuário no lado do servidor
No Cliente (Navegador)
No acesso inicial, redireciona automaticamente após detectar as configurações de idioma do navegador:
src/components/ClientLocaleRedirect.tsx
'use client';
export default function ClientLocaleRedirect() {
useEffect(() => {
// Verificar se o redirecionamento já foi realizado
if (typeof window === 'undefined' ||
sessionStorage.getItem('localeRedirectDone')) {
return;
}
const supportedLocales = ['en', 'ja'];
const pathname = window.location.pathname;
// Redirecionar apenas para o caminho raiz
if (pathname === '/') {
// Obter configurações de idioma do navegador
const browserLocales = navigator.languages || [navigator.language];
// Encontrar idioma suportado
const preferredLocale = browserLocales
.map(lang => lang.split('-')[0]) // 'ja-JP' -> 'ja'
.find(lang => supportedLocales.includes(lang));
// Redirecionar se diferente do padrão
if (preferredLocale && preferredLocale !== defaultLocale) {
sessionStorage.setItem('localeRedirectDone', 'true');
window.location.replace(`/${preferredLocale}`);
}
}
}, []);
return null;
}
Cenários de Comportamento
| Situação | Durante SSG | Comportamento no Cliente |
|---|---|---|
| Crawler de motor de busca | Gerar / em inglês |
Sem redirecionamento |
| Usuário com navegador em japonês | Gerar / em inglês |
Redirecionar para /ja |
| Usuário com navegador em inglês | Gerar / em inglês |
Sem redirecionamento (exibir como está) |
| Usuário com idioma não suportado | Gerar / em inglês |
Sem redirecionamento (exibir em inglês) |
Uso em Componentes
'use client';
function NetworkInfoClient() {
const { t } = useTranslations();
return (
<div>
<h2>{t('title')}</h2>
</div>
);
}
Vantagens Técnicas
- Segurança de Tipos: TypeScript + API Context permite verificações de existência de chaves de tradução
- Dependências Claras: Componentes usando
useTranslationsdevem estar dentro do Provider, tornando o design explícito - Performance: Entrega rápida de páginas por meio de geração estática
- SEO: Cada página de idioma tem uma URL independente, otimizada para motores de busca
- UX: Redirecionamento automático com base nas configurações de idioma do navegador reduz o esforço do usuário
Resumo
Implementamos suporte multilíngue usando a API React Context em um ambiente de exportação estática com Next.js App Router. Complementamos as restrições do lado do servidor com lógica do lado do cliente, alcançando boa UX.
Alterar o idioma padrão requer apenas editar defaultLocale em config.ts, garantindo extensibilidade.
