Next.js internacionalização e geração estática: guia prático de site multilíngue com SSG

Na primeira vez que tentei fazer geração estática multilíngue em um projeto Next.js com App Router, pisei em todas as armadilhas. Talvez você conheça a sensação: a config parece certa, mas o build estoura erro; ou o build finalmente passa e leva quinze minutos para gerar todas as páginas…
Deixa eu compartilhar alguns cenários típicos que já passei — vê se algum soa familiar.
Você também já passou por isso?
Cenário 1: erro no build
Lembro de rodar npm run build animado e o terminal me devolver um erro na hora:
Error: Page "/en/about" is missing `generateStaticParams()`
so it cannot be used with `output: "export"`.
Fiquei completamente perdido, pensando: “Que diabos? Eu configurei i18n no next.config.js!” Só depois descobri que internacionalização no App Router funciona de um jeito totalmente diferente do Pages Router — aquela config antiga simplesmente não vale mais.
Cenário 2: build demorado demais
Outra vez, meu projeto tinha 6 idiomas e cerca de 50 páginas por idioma. O build levou:
✓ Generating static pages (152/152) - 15m 32s
15 minutos! Sem erro de leitura. Ter que esperar tanto a cada mudança pequena destruía a experiência de desenvolvimento. Eu pensava: “Se isso for para produção, o CI/CD não vai esperar até o fim dos tempos?”
Cenário 3: tradução atualizada que não aparece
O mais frustrante foi este: atualizei o arquivo zh-CN.json, fiz rebuild e redeploy, mas o site continuava mostrando a tradução antiga! Só limpando o cache do navegador aparecia o conteúdo novo. Em produção isso é desastre — o usuário vê conteúdo desatualizado.
Qual é a raiz do problema?
Depois de estudar bastante, entendi a essência desses problemas:
-
O App Router não suporta mais a configuração i18n do Pages Router — essa é a maior armadilha. Configurar o campo
i18nemnext.config.jsnão funciona no App Router. -
Conflito entre export estático e renderização dinâmica — com
output: 'export', o Next.js exige que todas as páginas sejam definidas no build. Se você usarcookies(),headers()e outras APIs dinâmicas, vai dar erro. -
Mecanismo de cache dos arquivos de tradução — o Next.js faz cache de JSON importado. Você atualiza a tradução no desenvolvimento, mas o cache não invalida e o conteúdo novo não aparece.
Se você já passou por isso, este artigo é para você. Vou mostrar passo a passo como implementar geração estática multilíngue no App Router do Next.js e evitar essas armadilhas.
Entendendo o novo paradigma i18n do App Router
Antes de codar, acho importante entender a lógica de internacionalização do App Router. É bem diferente do Pages Router.
Pages Router vs App Router: duas abordagens bem distintas
Montei uma tabela comparativa para você ver a diferença de cara:
| Recurso | Pages Router | App Router |
|---|---|---|
| Configuração | campo i18n em next.config.js | middleware + rota dinâmica [lang] |
| Estrutura de rotas | gera automaticamente prefixos /en/, /zh/ | crie manualmente app/[lang]/page.tsx |
| Geração estática | usa getStaticPaths | usa generateStaticParams |
| Carregamento de traduções | função serverSideTranslations | Server Components importam JSON diretamente |
Viu? Quase tudo mudou. Na primeira vez que vi isso, senti mesmo que tinha aprendido um “Next.js falso”.
O que é generateStaticParams, afinal?
É um dos conceitos centrais do App Router. Em resumo, ele diz ao Next.js: “preciso gerar páginas estáticas para estes parâmetros”.
Exemplo:
// app/[lang]/layout.tsx
export async function generateStaticParams() {
// Retorna todos os parâmetros de idioma para pré-renderização
return [
{ lang: 'en' },
{ lang: 'zh' },
{ lang: 'ja' }
]
}
No build, o Next.js executa essa função, pega a lista de parâmetros e gera um HTML estático para cada combinação. A saída final fica assim:
out/
├── en/
│ └── index.html
├── zh/
│ └── index.html
└── ja/
└── index.html
Ponto-chave: a função deve estar em layout.tsx ou page.tsx, e o nome precisa ser exatamente esse (não getStaticParams, não generateParams — tem que ser generateStaticParams).
Como carregar arquivos de tradução?
Na era Pages Router, usávamos serverSideTranslations do next-i18next. No App Router, você pode importar traduções direto em Server Components:
// Server Components podem fazer isso diretamente
import enTranslations from '@/i18n/locales/en/common.json'
import zhTranslations from '@/i18n/locales/zh-CN/common.json'
const translations = {
'en': enTranslations,
'zh-CN': zhTranslations,
}
export default function Page({ params }: { params: { lang: string } }) {
const t = translations[params.lang]
return <h1>{t.title}</h1>
}
O problema: todas as traduções entram no bundle e o arquivo cresce. Em projetos reais, costumamos criar uma função de carregamento:
// i18n/utils.ts
export async function loadTranslations(locale: string, namespaces: string[]) {
const translations: Record<string, any> = {}
for (const ns of namespaces) {
try {
const translation = await import(`@/i18n/locales/${locale}/${ns}.json`)
translations[ns] = translation.default
} catch (error) {
console.warn(`Translation file not found: ${locale}/${ns}`)
translations[ns] = {}
}
}
return translations
}
Assim você carrega sob demanda, só os namespaces necessários para a página atual.
Na prática: montando um projeto SSG multilíngue do zero
Teoria fechada — agora vamos codar. Vou te guiar na montagem de um site estático multilíngue completo.
Passo 1: estrutura do projeto
Primeiro, defina uma estrutura de pastas clara. Esta é a que uso em projetos reais e funciona bem:
app/
├── [lang]/ # Rota dinâmica de idioma (núcleo)
│ ├── layout.tsx # Layout raiz, contém generateStaticParams
│ ├── page.tsx # Página inicial
│ ├── about/
│ │ └── page.tsx # Página sobre
│ └── blog/
│ ├── page.tsx # Lista do blog
│ └── [slug]/
│ └── page.tsx # Detalhe do post (rota dinâmica aninhada)
├── i18n/
│ ├── locales/ # Diretório de arquivos de tradução
│ │ ├── en/
│ │ │ ├── common.json # Traduções comuns
│ │ │ ├── home.json # Traduções da home
│ │ │ └── blog.json # Traduções do blog
│ │ ├── zh-CN/
│ │ │ ├── common.json
│ │ │ ├── home.json
│ │ │ └── blog.json
│ │ └── ja/
│ │ ├── common.json
│ │ ├── home.json
│ │ └── blog.json
│ ├── config.ts # Arquivo de configuração i18n
│ └── utils.ts # Funções utilitárias de tradução
└── middleware.ts # Detecção de idioma e redirecionamento
Por que essa estrutura?
- Pasta
[lang]: núcleo da rota dinâmica — o Next.js repassa o parâmetro de idioma da URL aos componentes de página. - Traduções por namespace: evita um arquivo gigante; divide por funcionalidade e carrega sob demanda.
config.tscentralizado: toda configuração de idioma num só lugar, fácil de manter.
Passo 2: configurar os arquivos i18n principais
Comece pelo arquivo de configuração — base de todo o sistema:
// i18n/config.ts
export const i18nConfig = {
// Supported languages list
locales: ['en', 'zh-CN', 'ja'],
// Default language
defaultLocale: 'en',
// Path prefix strategy
// 'always': All languages get prefix /en/, /zh-CN/
// 'as-needed': Default language has no prefix, others do
localePrefix: 'always',
// [IMPORTANT] Only pre-render major languages (optimize build time)
localesToPrerender: process.env.NODE_ENV === 'production'
? ['en', 'zh-CN'] // Production: only pre-render English and Chinese
: ['en'], // Development: only render default language
} as const
// Export types for TypeScript type checking
export type Locale = (typeof i18nConfig)['locales'][number]
// Translation namespaces (for code splitting)
export const namespaces = ['common', 'home', 'about', 'blog'] as const
export type Namespace = (typeof namespaces)[number]
Pontos-chave explicados:
as const: sintaxe TypeScript que garante tipos literais precisos, não umstring[]genérico.localesToPrerender: crucial! Se você suporta 10 idiomas mas só pré-renderiza 2 principais, o tempo de build pode cair 80%. Outros idiomas podem usar ISR (regeneração estática incremental) ou geração sob demanda.- Namespaces: divida traduções em vários JSON para não baixar um arquivo enorme no primeiro carregamento.
Passo 3: implementar utilitário de carregamento de traduções
Um carregador simples e prático:
// i18n/utils.ts
import type { Locale, Namespace } from './config'
// Translation file cache (avoid repeated reads)
const translationsCache = new Map<string, any>()
/**
* Load translation files for specified language
*
* @param locale Language code like 'en', 'zh-CN'
* @param namespaces Translation namespace array like ['common', 'home']
* @returns Translation object { common: {...}, home: {...} }
*/
export async function loadTranslations(
locale: Locale,
namespaces: Namespace[]
) {
const translations: Record<string, any> = {}
for (const namespace of namespaces) {
const cacheKey = `${locale}-${namespace}`
// Check cache to avoid repeated loading
if (!translationsCache.has(cacheKey)) {
try {
// Dynamic import of translation file
const translation = await import(
`@/i18n/locales/${locale}/${namespace}.json`
)
translationsCache.set(cacheKey, translation.default)
} catch (error) {
console.warn(`⚠️ Translation file not found: ${locale}/${namespace}.json`)
translationsCache.set(cacheKey, {})
}
}
translations[namespace] = translationsCache.get(cacheKey)
}
return translations
}
/**
* Create type-safe translation function
*
* Usage:
* const t = createTranslator(translations)
* t('common.nav.home')
* t('home.welcome', { name: 'John' }) // Supports variable replacement
*/
export function createTranslator(translations: any) {
return (key: string, params?: Record<string, string>) => {
const keys = key.split('.')
let value = translations
// Access nested properties layer by layer
for (const k of keys) {
value = value?.[k]
}
// Return key itself when translation not found (useful for debugging)
if (!value) {
console.warn(`⚠️ Translation missing: ${key}`)
return key
}
// Support variable replacement: replace {{name}} with actual value
if (params) {
return Object.entries(params).reduce(
(str, [key, val]) => str.replace(`{{${key}}}`, val),
value
)
}
return value
}
}
Destaques deste utilitário:
- Cache: após o primeiro carregamento, evita releituras desnecessárias.
- Tratamento de erros: arquivo ausente não derruba a app — só avisa e retorna objeto vazio.
- Substituição de variáveis: suporta placeholders
{{nomeDaVariavel}}nas traduções. - Amigável a tipos: combina com TypeScript para checagem segura das chaves.
Passo 4: criar o layout raiz (o mais crítico)
Arquivo central de todo o sistema multilíngue:
// app/[lang]/layout.tsx
import { i18nConfig } from '@/i18n/config'
import { loadTranslations } from '@/i18n/utils'
import type { Locale } from '@/i18n/config'
/**
* [CORE] Generate static parameters for all languages
*
* This function executes at build time, Next.js generates corresponding static pages based on return value
*
* Important notes:
* 1. Function name must be generateStaticParams (can't misspell)
* 2. Must be defined in layout.tsx or page.tsx
* 3. Returned parameter names must match route folder names ([lang] → lang)
*/
export async function generateStaticParams() {
console.log(`🌍 Generating static params for ${i18nConfig.localesToPrerender.length} locales...`)
return i18nConfig.localesToPrerender.map((locale) => ({
lang: locale, // ⚠️ Note: Must be 'lang' not 'locale'
}))
}
/**
* Root layout component
*
* This component wraps all pages for setting global configuration
*/
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode
params: { lang: string }
}) {
// Load common translations (navigation, footer, etc.)
const translations = await loadTranslations(params.lang as Locale, ['common'])
return (
<html
lang={params.lang}
// Set right-to-left layout for Arabic
dir={params.lang === 'ar' ? 'rtl' : 'ltr'}
>
<head>
{/* Add global meta tags here */}
</head>
<body>
{/* Place global components like navbar, footer here */}
{children}
</body>
</html>
)
}
/**
* Generate metadata (SEO)
*
* This function generates page <title>, <meta> tags, etc.
*/
export async function generateMetadata({ params }: { params: { lang: string } }) {
return {
// Set language-related meta tags
alternates: {
canonical: `https://example.com/${params.lang}`,
languages: {
'en': 'https://example.com/en',
'zh-CN': 'https://example.com/zh-CN',
'ja': 'https://example.com/ja',
},
},
// Open Graph tags (for social media sharing)
openGraph: {
locale: params.lang,
alternateLocale: i18nConfig.locales.filter(l => l !== params.lang),
},
}
}
Armadilhas comuns aqui:
⚠️ Armadilha 1: nome do parâmetro deve corresponder
// ❌ Wrong: parameter name is locale, but route folder is [lang]
export async function generateStaticParams() {
return [{ locale: 'en' }] // This will error
}
// ✅ Correct: parameter name matches folder name
export async function generateStaticParams() {
return [{ lang: 'en' }] // Must be lang
}
⚠️ Armadilha 2: não use APIs dinâmicas
// ❌ Wrong: using cookies in statically generated page
export default async function Layout({ children }) {
const locale = cookies().get('NEXT_LOCALE') // This causes build failure
return <html lang={locale}>{children}</html>
}
// ✅ Correct: use route parameters
export default async function Layout({ children, params }) {
return <html lang={params.lang}>{children}</html>
}
Passo 5: criar arquivos de tradução
A estrutura dos arquivos também importa. Formato recomendado:
// i18n/locales/en/common.json
{
"nav": {
"home": "Home",
"about": "About",
"blog": "Blog",
"contact": "Contact"
},
"footer": {
"copyright": "© {{year}} All rights reserved",
"privacy": "Privacy Policy",
"terms": "Terms of Service"
},
"actions": {
"readMore": "Read More",
"backToTop": "Back to Top",
"share": "Share",
"edit": "Edit"
},
"messages": {
"loading": "Loading...",
"error": "Something went wrong",
"success": "Success",
"noResults": "No results found"
}
}
// i18n/locales/en/blog.json
{
"title": "Blog Posts",
"publishedAt": "Published on",
"author": "Author",
"tags": "Tags",
"relatedPosts": "Related Posts",
"readingTime": "Reading time: {{minutes}} minutes",
"shareOn": "Share on {{platform}}"
}
Melhores práticas para arquivos de tradução:
- Estrutura hierárquica: use objetos aninhados; não coloque todas as chaves no topo.
- Placeholders de variáveis: formato
{{nomeDaVariavel}}para tratamento uniforme. - Chaves consistentes: todos os idiomas devem ter a mesma estrutura de chaves.
- Comentários: em traduções complexas, documente o contexto de uso.
Passo 6: lidar com rotas dinâmicas aninhadas
Se o projeto tem blog ou páginas de produto, você precisa de rotas aninhadas — uma das maiores armadilhas que já enfrentei.
// app/[lang]/blog/[slug]/page.tsx
import { i18nConfig } from '@/i18n/config'
import { loadTranslations, createTranslator } from '@/i18n/utils'
import type { Locale } from '@/i18n/config'
// Assume you have these helper functions (need to implement yourself in real projects)
async function getBlogSlugs(): Promise<string[]> {
// Get all blog post slugs from filesystem or CMS
return ['getting-started', 'advanced-tips', 'performance-guide']
}
async function getBlogPost(slug: string, locale: Locale) {
// Get blog post content for specific language
// ...
}
/**
* [KEY] generateStaticParams for nested routes
*
* Need to generate all combinations of language × posts
* For example: en/getting-started, zh-CN/getting-started, en/advanced-tips...
*/
export async function generateStaticParams() {
const startTime = Date.now()
console.log('📝 Generating blog post params...')
// Get all post slugs (only need one request)
const slugs = await getBlogSlugs()
// Use flatMap to generate all combinations of languages and posts
const params = i18nConfig.localesToPrerender.flatMap((locale) =>
slugs.map((slug) => ({
lang: locale,
slug: slug,
}))
)
const duration = Date.now() - startTime
console.log(`✅ Generated ${params.length} blog post params in ${duration}ms`)
return params
}
/**
* Blog post page component
*/
export default async function BlogPost({
params,
}: {
params: { lang: string; slug: string }
}) {
// Load translations and post content
const [translations, post] = await Promise.all([
loadTranslations(params.lang as Locale, ['common', 'blog']),
getBlogPost(params.slug, params.lang as Locale),
])
const t = createTranslator(translations)
return (
<article className="prose">
<h1>{post.title}</h1>
<p className="text-gray-600">
{t('blog.publishedAt')}: {new Date(post.date).toLocaleDateString(params.lang)}
</p>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
Ponto-chave de otimização de desempenho:
Erro comum: buscar dados separadamente para cada idioma:
// ❌ Wrong approach: multiple requests, slow build
export async function generateStaticParams() {
const results = []
for (const locale of i18nConfig.localesToPrerender) {
// Query database or CMS once per language - too slow!
const slugs = await getBlogSlugs(locale)
results.push(...slugs.map(slug => ({ lang: locale, slug })))
}
return results
}
A abordagem correta é uma única requisição e flatMap para gerar as combinações:
// ✅ Correct approach: one request, fast generation
export async function generateStaticParams() {
// Only one data request
const slugs = await getBlogSlugs()
// Use flatMap to generate all language × post combinations
return i18nConfig.localesToPrerender.flatMap((locale) =>
slugs.map((slug) => ({ lang: locale, slug }))
)
}
No meu projeto, essa otimização reduziu o build de 18 para 6 minutos — efeito bem visível!
Passo 7: implementar Middleware para detecção de idioma
O Middleware detecta a preferência de idioma do usuário e redireciona para a versão correspondente.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { i18nConfig } from './i18n/config'
/**
* Middleware
*
* This function executes before each request for:
* 1. Detecting user's language preference
* 2. Redirecting to corresponding language path
*/
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Check if path already includes language prefix
const pathnameHasLocale = i18nConfig.locales.some(
(locale) =>
pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
// If language prefix already exists, pass through
if (pathnameHasLocale) return
// Get user's preferred language
const locale = getLocale(request) ?? i18nConfig.defaultLocale
// Redirect to path with language prefix
request.nextUrl.pathname = `/${locale}${pathname}`
return NextResponse.redirect(request.nextUrl)
}
/**
* Language detection function
*
* Priority:
* 1. Language preference saved in Cookie
* 2. Accept-Language request header
* 3. Return null, use default language
*/
function getLocale(request: NextRequest): string | null {
// Priority 1: Check Cookie
const localeCookie = request.cookies.get('NEXT_LOCALE')?.value
if (localeCookie && i18nConfig.locales.includes(localeCookie as any)) {
return localeCookie
}
// Priority 2: Check Accept-Language request header
const acceptLanguage = request.headers.get('accept-language')
if (acceptLanguage) {
// Accept-Language format: zh-CN,zh;q=0.9,en;q=0.8
const preferred = acceptLanguage.split(',')[0].split('-')[0]
const match = i18nConfig.locales.find(locale =>
locale.toLowerCase().startsWith(preferred.toLowerCase())
)
if (match) return match
}
// No matching language found, return null
return null
}
/**
* Middleware configuration
*
* matcher defines which paths need to execute middleware
*/
export const config = {
// Match all paths except:
// - API routes starting with /api
// - Static files at /_next/static
// - Images at /_next/image
// - Static resources like /favicon.ico
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
Casos de uso do Middleware:
Se o usuário acessa https://example.com/blog diretamente, o Middleware:
- Verifica Cookie com preferência salva (ex.: chinês escolhido antes)
- Se não houver, lê o header
Accept-Languagedo navegador - Redireciona para
https://example.com/zh-CN/blogouhttps://example.com/en/blog
Detecção automática de idioma — melhor experiência para o usuário.
Passo 8: criar componente de troca de idioma
Por fim, um seletor para o usuário trocar de idioma manualmente:
// components/LanguageSwitcher.tsx
'use client'
import { usePathname, useRouter } from 'next/navigation'
import { i18nConfig } from '@/i18n/config'
import type { Locale } from '@/i18n/config'
// Language display name mapping
const localeNames: Record<Locale, string> = {
'en': 'English',
'zh-CN': '简体中文',
'ja': '日本語',
}
export function LanguageSwitcher({ currentLocale }: { currentLocale: Locale }) {
const pathname = usePathname()
const router = useRouter()
const handleLocaleChange = (newLocale: Locale) => {
// Save language preference to Cookie
document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=31536000`
// Replace language prefix in path
// Example: /zh-CN/blog → /en/blog
const newPathname = pathname.replace(`/${currentLocale}`, `/${newLocale}`)
// Navigate to new language version
router.push(newPathname)
}
return (
<div className="relative">
<select
value={currentLocale}
onChange={(e) => handleLocaleChange(e.target.value as Locale)}
className="px-4 py-2 border rounded-lg"
>
{i18nConfig.locales.map((locale) => (
<option key={locale} value={locale}>
{localeNames[locale]}
</option>
))}
</select>
</div>
)
}
Uso na barra de navegação:
// components/Navigation.tsx
import { LanguageSwitcher } from './LanguageSwitcher'
export function Navigation({ lang }: { lang: string }) {
return (
<nav className="flex items-center justify-between p-4">
<div className="flex gap-4">
<a href={`/${lang}/`}>Início</a>
<a href={`/${lang}/about`}>Sobre</a>
<a href={`/${lang}/blog`}>Blog</a>
</div>
<LanguageSwitcher currentLocale={lang} />
</nav>
)
}
Otimização de desempenho: acelerar o build
Com a base pronta, sites multilíngues podem demorar muito no build. Algumas técnicas práticas:
Otimização 1: pré-renderização seletiva
A mais eficaz. Se você suporta 10 idiomas mas o tráfego concentra em 2–3, pré-renderize só os principais:
// i18n/config.ts
export const i18nConfig = {
// All supported languages
locales: ['en', 'zh-CN', 'ja', 'ko', 'de', 'fr', 'es', 'pt'],
defaultLocale: 'en',
// [KEY] Only pre-render major languages
localesToPrerender: process.env.NODE_ENV === 'production'
? ['en', 'zh-CN'] // Production: only pre-render English and Chinese
: ['en'], // Development: only render default language (faster development)
}
Comparativo de desempenho:
| Configuração | Tempo de build | Descrição |
|---|---|---|
| Pré-renderizar 8 idiomas | ~24 min | Todas as versões geradas estaticamente |
| Pré-renderizar 2 idiomas | ~6 min | Outros idiomas gerados no primeiro acesso |
| Renderizar só 1 idioma | ~3 min | Recomendado em desenvolvimento |
Economia de 75% no tempo de build!
Otimização 2: regeneração estática incremental (ISR)
Para idiomas secundários ou páginas pouco acessadas, use ISR sob demanda:
// app/[lang]/blog/[slug]/page.tsx
// Enable ISR, revalidate after 1 hour
export const revalidate = 3600
export async function generateStaticParams() {
const slugs = await getBlogSlugs()
// Only pre-render popular posts in major languages
const topSlugs = slugs.slice(0, 10) // Only pre-render top 10
return i18nConfig.localesToPrerender.flatMap((locale) =>
topSlugs.map((slug) => ({ lang: locale, slug }))
)
}
// [IMPORTANT] Allow dynamic generation of non-pre-rendered pages
export const dynamicParams = true
Com essa configuração:
- O build gera só 2 idiomas × 10 posts = 20 páginas
- Páginas não pré-renderizadas são geradas e cacheadas na primeira visita
- O cache renova automaticamente após 1 hora
Otimização 3: busca de dados em paralelo
Em generateStaticParams, se precisar de vários tipos de dados, processe em paralelo:
// ❌ Wrong: serial fetching (slow)
export async function generateStaticParams() {
const posts = await getBlogPosts() // Wait 2 seconds
const categories = await getCategories() // Wait 1 second
// Total: 3 seconds
}
// ✅ Correct: parallel fetching (fast)
export async function generateStaticParams() {
const [posts, categories] = await Promise.all([
getBlogPosts(), // Execute simultaneously
getCategories(), // Execute simultaneously
])
// Total: 2 seconds (whichever is longest)
}
No meu projeto, isso reduziu o tempo de busca de dados em 40%.
Otimização 4: resolver cache de traduções
No desenvolvimento, nada irrita mais que atualizar tradução e a página não mudar — o Next.js faz cache de JSON importado.
Solução: desabilitar cache em desenvolvimento
// i18n/utils.ts
import fs from 'fs/promises'
import path from 'path'
const isDev = process.env.NODE_ENV === 'development'
export async function loadTranslations(
locale: Locale,
namespaces: Namespace[]
) {
// Development environment: re-read file each time
if (isDev) {
const translations: Record<string, any> = {}
for (const ns of namespaces) {
const filePath = path.join(
process.cwd(),
'i18n',
'locales',
locale,
`${ns}.json`
)
try {
const content = await fs.readFile(filePath, 'utf-8')
translations[ns] = JSON.parse(content)
} catch (error) {
console.warn(`Translation file not found: ${filePath}`)
translations[ns] = {}
}
}
return translations
}
// Production environment: use cache
return loadTranslationsWithCache(locale, namespaces)
}
Assim, ao atualizar traduções em desenvolvimento, basta recarregar a página para ver o conteúdo novo.
Guia de solução de problemas comuns
Na prática você pode encontrar outros casos. Reuni os mais frequentes e como resolvi.
Problema 1: erro de build “generateStaticParams not found”
Mensagem de erro:
Error: Page "/en/about" is missing `generateStaticParams()`
so it cannot be used with `output: "export"`.
Passos de diagnóstico:
- ✅ Verifique se
generateStaticParamsestá emlayout.tsxoupage.tsx - ✅ Confirme a grafia (não é
getStaticParamsnemgenerateParams) - ✅ Confirme export correto (
export async function) - ✅ Nome do parâmetro deve corresponder à pasta da rota
// ❌ Wrong example
export async function getStaticParams() { // Wrong function name
return [{ locale: 'en' }] // Wrong parameter name too
}
// ✅ Correct example
export async function generateStaticParams() {
return [{ lang: 'en' }] // Parameter name must match [lang]
}
Problema 2: renderização dinâmica detectada
Mensagem de erro:
Error: Route /[lang]/about couldn't be rendered statically
because it used `headers` or `cookies`.
Causa: uso de APIs dinâmicas (headers(), cookies(), searchParams) em páginas geradas estaticamente.
Solução:
// ❌ Wrong: using cookies in server component
export default async function Page() {
const locale = cookies().get('NEXT_LOCALE') // Triggers dynamic rendering
return <div>...</div>
}
// ✅ Solution 1: Handle in middleware
// middleware.ts
export function middleware(request: NextRequest) {
const locale = request.cookies.get('NEXT_LOCALE')
// Processing logic...
}
// ✅ Solution 2: Use client component
'use client'
export function LanguageSwitcher() {
const [locale, setLocale] = useState(() => {
// Read Cookie on client side
return getCookie('NEXT_LOCALE')
})
// ...
}
Problema 3: arquivo de tradução não encontrado
Mensagem de erro:
Error: Cannot find module './locales/en/common.json'
Checklist:
- ✅ Caminho correto (Linux diferencia maiúsculas/minúsculas)
- ✅ Sintaxe JSON válida
- ✅ Alias de path em
tsconfig.json:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
}
}
- ✅ Traduções incluídas corretamente no build:
// next.config.js
module.exports = {
// Ensure JSON files are included
webpack: (config) => {
config.module.rules.push({
test: /\.json$/,
type: 'json',
})
return config
},
}
Problema 4: parâmetros de rota perdidos ao trocar idioma
Sintoma: de /zh-CN/blog/my-post para inglês vai para /en/ em vez de /en/blog/my-post.
Causa: o seletor de idioma não preserva os parâmetros da rota.
Solução:
// ❌ Wrong: hardcoded path
<Link href="/about">About</Link>
// ✅ Solution 1: manually concatenate language parameter
<Link href={`/${params.lang}/about`}>About</Link>
// ✅ Solution 2: wrap a smart Link component
// components/LocalizedLink.tsx
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
export function LocalizedLink({
href,
children,
...props
}: {
href: string
children: React.ReactNode
[key: string]: any
}) {
const pathname = usePathname()
// Extract language from current path
const locale = pathname.split('/')[1]
// Automatically add language prefix
const localizedHref = `/${locale}${href}`
return (
<Link href={localizedHref} {...props}>
{children}
</Link>
)
}
Problema 5: tags SEO ausentes ou incorretas
Problema: hreflang e canonical mal configurados prejudicam indexação.
Solução: configure corretamente em generateMetadata de cada página:
// app/[lang]/blog/[slug]/page.tsx
export async function generateMetadata({
params,
}: {
params: { lang: string; slug: string }
}) {
const baseUrl = 'https://example.com'
return {
// Page title and description
title: 'My Blog Post',
description: 'This is a blog post',
// Canonical URL
alternates: {
canonical: `${baseUrl}/${params.lang}/blog/${params.slug}`,
// hreflang tags (tell search engines about other language versions)
languages: {
'en': `${baseUrl}/en/blog/${params.slug}`,
'zh-CN': `${baseUrl}/zh-CN/blog/${params.slug}`,
'ja': `${baseUrl}/ja/blog/${params.slug}`,
'x-default': `${baseUrl}/en/blog/${params.slug}`, // Default language
},
},
// Open Graph tags (for social media sharing)
openGraph: {
title: 'My Blog Post',
description: 'This is a blog post',
url: `${baseUrl}/${params.lang}/blog/${params.slug}`,
locale: params.lang,
alternateLocale: i18nConfig.locales.filter(l => l !== params.lang),
},
}
}
Resumo de melhores práticas
Depois de tanta prática, compilei um checklist executável.
Checklist de inicialização do projeto
Antes de codar, confira:
- Lista de idiomas suportados e idioma padrão definidos
- Estrutura
app/[lang]criada -
i18n/config.tse diretório de traduções configurados -
middleware.tscom detecção de idioma implementado -
generateStaticParamsno layout raiz -
next.config.jsconfigurado (se export estático,output: 'export')
Recomendações na fase de desenvolvimento
- Em dev, pré-renderize só o idioma padrão (
localesToPrerender: ['en']) - TypeScript para type safety das chaves de tradução
- Namespaces por módulo (common, home, blog…)
- Desabilite cache de tradução em dev (
fs.readFileem tempo real) - Logs de aviso para traduções faltantes
Checklist de deploy em produção
- Pré-renderização seletiva dos idiomas principais
- Estratégia ISR para idiomas secundários (
revalidate) - Busca de dados em paralelo (
Promise.all) - hreflang e canonical corretos
- Estratégia de cache CDN (paths multilíngues)
- Monitorar tráfego e tempo de build por idioma
Exemplo completo de next.config.js
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Static export (if needed)
output: 'export',
// Image optimization config
images: {
unoptimized: true, // Required for static export
},
// Environment variables
env: {
BUILD_TIME: new Date().toISOString(),
},
// Custom build ID (for cache invalidation)
generateBuildId: async () => {
return `build-${Date.now()}`
},
// Webpack configuration
webpack: (config, { isServer }) => {
// Ensure JSON files are handled correctly
config.module.rules.push({
test: /\.json$/,
type: 'json',
})
return config
},
}
module.exports = nextConfig
Ferramentas e bibliotecas recomendadas
Se não quiser implementar do zero, considere:
| Ferramenta/Biblioteca | Uso | Nota | Descrição |
|---|---|---|---|
| next-intl | Solução i18n completa | ⭐⭐⭐⭐⭐ | Recomendada oficialmente, mais completa, suporta App Router |
| next-international | Biblioteca i18n leve | ⭐⭐⭐⭐ | Leve, limpa, type-safe |
| @formatjs/intl | Formatação i18n | ⭐⭐⭐⭐ | Datas, números, moedas |
| typesafe-i18n | Traduções type-safe | ⭐⭐⭐⭐ | Gera definições de tipo automaticamente |
| i18next | Biblioteca i18n veterana | ⭐⭐⭐ | Poderosa, mas exige adaptação ao App Router |
Recomendo next-intl — feita para App Router, funciona out of the box. Se quiser entender a fundo ou customizar muito, implementação manual (como neste artigo) também vale a pena.
Conclusão
Recapitulando, implementamos geração estática multilíngue completa no App Router:
-
Funcionalidades centrais
- Estrutura multilíngue com rota dinâmica
[lang] - Páginas estáticas via
generateStaticParams - Traduções por namespace
- Detecção e redirecionamento automático no Middleware
- Estrutura multilíngue com rota dinâmica
-
Otimização de desempenho
- Pré-renderização seletiva (75% menos tempo de build)
- ISR para idiomas secundários
- Busca de dados em paralelo
- Cache desabilitado em desenvolvimento
-
Solução de problemas
- Erros de configuração de
generateStaticParams - APIs dinâmicas que quebram o build
- Cache de arquivos de tradução
- Perda de parâmetros ao trocar idioma
- Tags SEO
- Erros de configuração de
Pontos-chave:
- i18n no App Router exige implementação manual — config do Pages Router não serve
generateStaticParamsdeve estar em layout ou page, com nomes de parâmetro corretos- Páginas estáticas não podem usar
cookies(),headers()etc. - Use pré-renderização seletiva e ISR com critério para não estourar o build
Se você também monta sites multilíngues com App Router, espero que este artigo evite algumas armadilhas. Internacionalização não é complexa — o segredo é entender o mecanismo de build do Next.js e configurar conforme as regras.
Por fim, se implementação manual for demais, experimente next-intl — economiza bastante trabalho.
Fluxo completo de configuração de geração estática multilíngue no Next.js
Do erro de build à otimização de tempo de build e atualização de traduções
⏱️ Estimated time: 3 hr
- 1
Step 1: Resolver erro de build: configurar generateStaticParams
Problema: Page "/en/about" is missing generateStaticParams()
Solução: configurar generateStaticParams em layout.tsx
```tsx
// app/[locale]/layout.tsx
export async function generateStaticParams() {
return [
{ locale: 'zh' },
{ locale: 'en' },
]
}
export default async function LocaleLayout({
children,
params: { locale }
}) {
// ...
}
```
Pontos-chave:
• generateStaticParams deve estar definido em layout ou page
• o nome do parâmetro deve corresponder a [locale]
• retorne todas as versões de idioma
Atenção: páginas geradas estaticamente não podem usar APIs dinâmicas como cookies() ou headers() - 2
Step 2: Otimizar tempo de build
Problema: 6 idiomas × 50 páginas = 300 páginas, build de 15 minutos.
Métodos de otimização:
1. Pré-renderização seletiva (só páginas importantes):
```tsx
export async function generateStaticParams() {
// Só pré-renderiza home e about
return [
{ locale: 'zh', slug: 'home' },
{ locale: 'zh', slug: 'about' },
{ locale: 'en', slug: 'home' },
{ locale: 'en', slug: 'about' },
]
}
```
2. Usar ISR (regeneração estática incremental):
```tsx
export const revalidate = 3600 // regenera após 1 hora
```
3. Build paralelo:
```tsx
export async function generateStaticParams() {
const locales = ['zh', 'en', 'ja', 'ko', 'fr', 'de']
const pages = ['home', 'about', 'contact']
return locales.flatMap(locale =>
pages.map(slug => ({ locale, slug }))
)
}
```
Resultado: de 15 minutos para menos de 5 - 3
Step 3: Resolver traduções que não atualizam
Problema: você atualizou o arquivo de tradução, mas o site ainda mostra a versão antiga.
Causa: o Next.js faz cache de arquivos JSON importados.
Soluções:
1. Usar import dinâmico:
```tsx
const messages = await import(`../messages/${locale}.json`)
```
2. Limpar cache:
```bash
rm -rf .next
npm run build
```
3. Usar timestamp:
```tsx
const messages = await import(
`../messages/${locale}.json?v=${Date.now()}`
)
```
Pontos-chave:
• em desenvolvimento, use import dinâmico
• em produção, limpe o cache
• use timestamp para evitar cache - 4
Step 4: Evitar conflito com APIs dinâmicas
Problema: páginas geradas estaticamente não podem usar cookies(), headers() e outras APIs dinâmicas.
Soluções:
1. Verificar o tipo de página:
```tsx
// ❌ Errado: página estática usando API dinâmica
export default async function Page() {
const cookies = await cookies() // erro
return <div>...</div>
}
// ✅ Correto: página dinâmica usando API dinâmica
export const dynamic = 'force-dynamic'
export default async function Page() {
const cookies = await cookies() // ok
return <div>...</div>
}
```
2. Separar páginas estáticas e dinâmicas:
```tsx
// Páginas estáticas: sem APIs dinâmicas
// Páginas dinâmicas: use dynamic = 'force-dynamic'
```
Pontos-chave:
• páginas estáticas não podem usar APIs dinâmicas
• páginas que precisam de APIs dinâmicas devem usar force-dynamic
• separe bem páginas estáticas e dinâmicas
FAQ
Por que o build de site multilíngue com App Router dá erro?
Pages Router:
• configura o campo i18n em next.config.js
• trata troca de idioma automaticamente
• gera todas as versões de idioma automaticamente
App Router:
• ignora a configuração i18n de next.config.js
• exige generateStaticParams manual
• o nome do parâmetro deve corresponder a [locale]
Mensagem de erro:
```
Error: Page "/en/about" is missing generateStaticParams()
so it cannot be used with output: "export".
```
Solução:
```tsx
// app/[locale]/layout.tsx
export async function generateStaticParams() {
return [
{ locale: 'zh' },
{ locale: 'en' },
]
}
```
Ponto-chave: generateStaticParams deve estar em layout ou page, e o nome do parâmetro deve corresponder a [locale].
Como otimizar o tempo de build de um site multilíngue?
Métodos:
1. Pré-renderização seletiva (só páginas importantes):
```tsx
export async function generateStaticParams() {
// Só pré-renderiza home e about
return [
{ locale: 'zh', slug: 'home' },
{ locale: 'en', slug: 'home' },
]
}
```
2. Usar ISR (regeneração estática incremental):
```tsx
export const revalidate = 3600 // regenera após 1 hora
```
3. Build paralelo:
```tsx
export async function generateStaticParams() {
const locales = ['zh', 'en']
const pages = ['home', 'about']
return locales.flatMap(locale =>
pages.map(slug => ({ locale, slug }))
)
}
```
Resultado: de 15 minutos para menos de 5
Recomendação: use pré-renderização seletiva e ISR com critério para evitar builds longos.
Por que a atualização de tradução não surte efeito?
Problema:
• você atualizou o arquivo de tradução
• fez rebuild e redeploy
• o site ainda mostra a tradução antiga
• só limpando o cache do navegador aparece o conteúdo novo
Soluções:
1. Usar import dinâmico:
```tsx
const messages = await import(`../messages/${locale}.json`)
```
2. Limpar cache:
```bash
rm -rf .next
npm run build
```
3. Usar timestamp:
```tsx
const messages = await import(
`../messages/${locale}.json?v=${Date.now()}`
)
```
Pontos-chave:
• em desenvolvimento, use import dinâmico
• em produção, limpe o cache
• use timestamp para evitar cache
Recomendação: prefira import dinâmico para evitar problemas de cache.
Páginas geradas estaticamente podem usar APIs dinâmicas?
Exemplo errado:
```tsx
// ❌ Errado: página estática usando API dinâmica
export default async function Page() {
const cookies = await cookies() // erro
return <div>...</div>
}
```
Soluções:
1. Marcar como página dinâmica:
```tsx
export const dynamic = 'force-dynamic'
export default async function Page() {
const cookies = await cookies() // ok
return <div>...</div>
}
```
2. Separar páginas estáticas e dinâmicas:
```tsx
// Páginas estáticas: sem APIs dinâmicas
// Páginas dinâmicas: use dynamic = 'force-dynamic'
```
Pontos-chave:
• páginas estáticas não podem usar APIs dinâmicas
• páginas que precisam de APIs dinâmicas devem usar force-dynamic
• separe bem páginas estáticas e dinâmicas
Atenção: páginas com force-dynamic não são geradas estaticamente; cada requisição re-renderiza.
Como configurar generateStaticParams?
Exemplo:
```tsx
// app/[locale]/layout.tsx
export async function generateStaticParams() {
return [
{ locale: 'zh' },
{ locale: 'en' },
]
}
export default async function LocaleLayout({
children,
params: { locale }
}) {
// ...
}
```
Páginas dinâmicas:
```tsx
// app/[locale]/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts()
const locales = ['zh', 'en']
return locales.flatMap(locale =>
posts.map(post => ({
locale,
slug: post.slug
}))
)
}
```
Pontos-chave:
• o nome do parâmetro deve corresponder à rota dinâmica ([locale], [slug])
• retorne todas as combinações possíveis
• páginas geradas estaticamente não podem usar APIs dinâmicas
Atenção: se houver muitas combinações, considere pré-renderização seletiva ou ISR.
Quais são as melhores práticas para sites multilíngues?
1. Use generateStaticParams para gerar todas as versões de idioma
2. Evite APIs dinâmicas (se necessário, marque como force-dynamic)
3. Use pré-renderização seletiva e ISR com critério
4. Evite builds excessivamente longos
Gestão de traduções:
• use arquivos JSON para traduções
• suporte estrutura aninhada
• combine com TypeScript para type safety
• use o plugin i18n Ally no VSCode
Otimização de desempenho:
• pré-renderize seletivamente páginas importantes
• use ISR para reduzir tempo de build
• evite pré-renderização total
Pontos-chave:
• entenda o mecanismo de build do App Router
• configure generateStaticParams corretamente
• evite conflito com APIs dinâmicas
• otimize o tempo de build
Recomendação: se implementar manualmente for trabalhoso demais, experimente a biblioteca next-intl — economiza bastante tempo.
21 min de leitura · Publicado em: 25 dez 2025 · Atualizado em: 8 set 2026
Guia completo Next.js
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Guia completo de internacionalização no Next.js: boas práticas com next-intl
Um guia detalhado de internacionalização com o App Router do Next.js, incluindo a configuração completa do next-intl, o design de rotas multilíngues, boas práticas para gerenciar traduções e exemplos de código
Parte 8 de 26
Próximo
Guia completo de SEO multilíngue no Next.js: faça os buscadores indexarem cada idioma corretamente
Mais de 60% dos sites multilíngues têm erros de configuração de SEO. Este guia explica hreflang, sitemaps multilíngues e estratégias de URL para evitar armadilhas e melhorar o posicionamento de cada versão.
Parte 10 de 26



Comentários
Entre com GitHub para comentar