Alternar tema

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

Easton editorial illustration: component assembly loom

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:

  1. O App Router não suporta mais a configuração i18n do Pages Router — essa é a maior armadilha. Configurar o campo i18n em next.config.js não funciona no App Router.

  2. 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ê usar cookies(), headers() e outras APIs dinâmicas, vai dar erro.

  3. 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:

RecursoPages RouterApp Router
Configuraçãocampo i18n em next.config.jsmiddleware + rota dinâmica [lang]
Estrutura de rotasgera automaticamente prefixos /en/, /zh/crie manualmente app/[lang]/page.tsx
Geração estáticausa getStaticPathsusa generateStaticParams
Carregamento de traduçõesfunção serverSideTranslationsServer 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?

  1. Pasta [lang]: núcleo da rota dinâmica — o Next.js repassa o parâmetro de idioma da URL aos componentes de página.
  2. Traduções por namespace: evita um arquivo gigante; divide por funcionalidade e carrega sob demanda.
  3. config.ts centralizado: 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:

  1. as const: sintaxe TypeScript que garante tipos literais precisos, não um string[] genérico.
  2. 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.
  3. 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:

  1. Cache: após o primeiro carregamento, evita releituras desnecessárias.
  2. Tratamento de erros: arquivo ausente não derruba a app — só avisa e retorna objeto vazio.
  3. Substituição de variáveis: suporta placeholders {{nomeDaVariavel}} nas traduções.
  4. 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:

  1. Estrutura hierárquica: use objetos aninhados; não coloque todas as chaves no topo.
  2. Placeholders de variáveis: formato {{nomeDaVariavel}} para tratamento uniforme.
  3. Chaves consistentes: todos os idiomas devem ter a mesma estrutura de chaves.
  4. 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:

  1. Verifica Cookie com preferência salva (ex.: chinês escolhido antes)
  2. Se não houver, lê o header Accept-Language do navegador
  3. Redireciona para https://example.com/zh-CN/blog ou https://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çãoTempo de buildDescrição
Pré-renderizar 8 idiomas~24 minTodas as versões geradas estaticamente
Pré-renderizar 2 idiomas~6 minOutros idiomas gerados no primeiro acesso
Renderizar só 1 idioma~3 minRecomendado 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:

  1. O build gera só 2 idiomas × 10 posts = 20 páginas
  2. Páginas não pré-renderizadas são geradas e cacheadas na primeira visita
  3. 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:

  1. ✅ Verifique se generateStaticParams está em layout.tsx ou page.tsx
  2. ✅ Confirme a grafia (não é getStaticParams nem generateParams)
  3. ✅ Confirme export correto (export async function)
  4. ✅ 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:

  1. ✅ Caminho correto (Linux diferencia maiúsculas/minúsculas)
  2. ✅ Sintaxe JSON válida
  3. ✅ Alias de path em tsconfig.json:
// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"]
    }
  }
}
  1. ✅ 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.ts e diretório de traduções configurados
  • middleware.ts com detecção de idioma implementado
  • generateStaticParams no layout raiz
  • next.config.js configurado (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.readFile em 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/BibliotecaUsoNotaDescrição
next-intlSolução i18n completa⭐⭐⭐⭐⭐Recomendada oficialmente, mais completa, suporta App Router
next-internationalBiblioteca i18n leve⭐⭐⭐⭐Leve, limpa, type-safe
@formatjs/intlFormatação i18n⭐⭐⭐⭐Datas, números, moedas
typesafe-i18nTraduções type-safe⭐⭐⭐⭐Gera definições de tipo automaticamente
i18nextBiblioteca 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:

  1. 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
  2. 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
  3. 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

Pontos-chave:

  • i18n no App Router exige implementação manual — config do Pages Router não serve
  • generateStaticParams deve 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. 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. 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. 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. 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?
Causa: o App Router não suporta mais a configuração i18n do Pages Router.

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?
Problema: 6 idiomas × 50 páginas = 300 páginas, build de 15 minutos.

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?
Causa: o Next.js faz cache de arquivos JSON importados.

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?
Não. Páginas geradas estaticamente não podem usar cookies(), headers() e outras 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?
Onde configurar: defina em layout.tsx ou page.tsx

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?
Recomendações de configuração:

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

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog