Pular para o conteúdo

15. Internacionalização

Este documento apresenta estratégias e práticas para implementar internacionalização (i18n) em soluções web desenvolvidas com Astro 5, permitindo que seu site atenda a usuários de diferentes idiomas e regiões.

Fundamentos da Internacionalização

Conceitos Básicos

  • Internacionalização (i18n): Processo de design e desenvolvimento que permite a adaptação para diferentes idiomas e regiões.
  • Localização (l10n): Processo de adaptar o conteúdo para um idioma ou região específica.
  • Tradução: Conversão de texto de um idioma para outro.
  • Pluralização: Adaptação de mensagens para formas singular e plural, que variam entre idiomas.
  • Formatação de Data/Hora/Números: Apresentação de datas, horas e números de acordo com convenções regionais.

Benefícios da Internacionalização

  1. Alcance Global: Amplia o público potencial do seu site.
  2. Experiência do Usuário: Melhora a experiência para usuários não nativos do idioma principal.
  3. SEO: Melhora o posicionamento em mecanismos de busca para consultas em diferentes idiomas.
  4. Conformidade Legal: Atende a requisitos legais em certas regiões.

Implementação com Astro

Estrutura de Diretórios

Organize seu projeto para suportar múltiplos idiomas:

src/
├── i18n/
│ ├── ui.ts # Definições de UI para todos os idiomas
│ ├── translations/ # Traduções por idioma
│ │ ├── pt-BR.ts
│ │ ├── en.ts
│ │ └── es.ts
│ └── utils.ts # Utilitários de i18n
├── layouts/
│ └── BaseLayout.astro # Layout com suporte a i18n
├── pages/
│ ├── [lang]/ # Páginas com parâmetro de idioma
│ │ ├── index.astro
│ │ ├── about.astro
│ │ └── contact.astro
│ └── index.astro # Redirecionamento para idioma padrão
└── components/
└── LanguageSwitcher.astro

Configuração Básica

src/i18n/ui.ts
export const languages = {
'pt-BR': 'Português',
'en': 'English',
'es': 'Español',
};
export const defaultLanguage = 'pt-BR';
// Tipo para as chaves de tradução
export type UI = typeof import('./translations/pt-BR').default;

Traduções

src/i18n/translations/pt-BR.ts
export default {
nav: {
home: 'Início',
about: 'Sobre',
contact: 'Contato',
},
home: {
title: 'Bem-vindo ao nosso site',
description: 'Somos uma empresa especializada em soluções web.',
cta: 'Saiba mais',
},
about: {
title: 'Sobre nós',
description: 'Conheça nossa história e valores.',
},
contact: {
title: 'Entre em contato',
name: 'Nome',
email: 'E-mail',
message: 'Mensagem',
submit: 'Enviar',
success: 'Mensagem enviada com sucesso!',
error: 'Ocorreu um erro ao enviar sua mensagem.',
},
footer: {
rights: 'Todos os direitos reservados.',
},
};
// src/i18n/translations/en.ts
export default {
nav: {
home: 'Home',
about: 'About',
contact: 'Contact',
},
home: {
title: 'Welcome to our website',
description: 'We are a company specialized in web solutions.',
cta: 'Learn more',
},
about: {
title: 'About us',
description: 'Learn about our history and values.',
},
contact: {
title: 'Contact us',
name: 'Name',
email: 'Email',
message: 'Message',
submit: 'Submit',
success: 'Message sent successfully!',
error: 'An error occurred while sending your message.',
},
footer: {
rights: 'All rights reserved.',
},
};

Utilitários de i18n

src/i18n/utils.ts
import { defaultLanguage, languages, type UI } from './ui';
export function getLanguageFromUrl(url: URL) {
const [, lang] = url.pathname.split('/');
if (lang in languages) return lang as keyof typeof languages;
return defaultLanguage;
}
export function useTranslations(lang: keyof typeof languages) {
return function t(key: keyof UI) {
const keys = key.split('.');
let value: any = import(`./translations/${lang}.ts`).default;
for (const k of keys) {
value = value[k];
if (!value) break;
}
return value || key;
};
}
export function getLocalizedPath(path: string, lang: keyof typeof languages) {
return `/${lang}${path.startsWith('/') ? path : `/${path}`}`;
}
export function getAlternateLinks(path: string) {
return Object.keys(languages).map((lang) => ({
href: getLocalizedPath(path, lang as keyof typeof languages),
hreflang: lang,
}));
}

Páginas Localizadas

src/pages/[lang]/index.astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getLanguageFromUrl, useTranslations } from '../../i18n/utils';
import { languages } from '../../i18n/ui';
export async function getStaticPaths() {
return Object.keys(languages).map((lang) => ({
params: { lang },
}));
}
const { lang } = Astro.params;
const t = useTranslations(lang as keyof typeof languages);
---
<BaseLayout title={t('home.title')} description={t('home.description')}>
<main>
<h1>{t('home.title')}</h1>
<p>{t('home.description')}</p>
<a href={`/${lang}/about`} class="cta-button">{t('home.cta')}</a>
</main>
</BaseLayout>

Redirecionamento para Idioma Padrão

src/pages/index.astro
---
import { defaultLanguage } from '../i18n/ui';
// Redireciona para o idioma padrão
return Astro.redirect(`/${defaultLanguage}`);
---

Layout Base com Suporte a i18n

src/layouts/BaseLayout.astro
---
import { getLanguageFromUrl, getAlternateLinks } from '../i18n/utils';
import LanguageSwitcher from '../components/LanguageSwitcher.astro';
import { languages } from '../i18n/ui';
const { title, description } = Astro.props;
const lang = getLanguageFromUrl(Astro.url);
const alternateLinks = getAlternateLinks(Astro.url.pathname.replace(`/${lang}`, ''));
---
<html lang={lang}>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<meta name="description" content={description}>
<!-- Links alternativos para outros idiomas -->
{alternateLinks.map(({ href, hreflang }) => (
<link rel="alternate" href={Astro.site ? new URL(href, Astro.site).href : href} hreflang={hreflang} />
))}
<!-- Canonical para o idioma atual -->
<link rel="canonical" href={Astro.url.href}>
<slot name="head" />
</head>
<body>
<header>
<nav>
<LanguageSwitcher currentLang={lang} />
</nav>
</header>
<slot />
<footer>
<p>&copy; {new Date().getFullYear()} - {languages[lang as keyof typeof languages]}</p>
</footer>
</body>
</html>

Seletor de Idioma

src/components/LanguageSwitcher.astro
---
import { languages } from '../i18n/ui';
import { getLocalizedPath } from '../i18n/utils';
const { currentLang } = Astro.props;
const currentPath = Astro.url.pathname.replace(`/${currentLang}`, '') || '/';
---
<div class="language-switcher">
<select id="language-select" aria-label="Selecionar idioma">
{Object.entries(languages).map(([lang, label]) => (
<option value={getLocalizedPath(currentPath, lang as keyof typeof languages)} selected={lang === currentLang}>
{label}
</option>
))}
</select>
</div>
<script>
// Redirecionar ao mudar o idioma
const select = document.getElementById('language-select') as HTMLSelectElement;
if (select) {
select.addEventListener('change', () => {
window.location.href = select.value;
});
}
</script>
<style>
.language-switcher {
margin: 1rem 0;
}
select {
padding: 0.5rem;
border-radius: 4px;
border: 1px solid #ccc;
background-color: white;
font-size: 0.9rem;
}
</style>

Formatação Localizada

Datas e Números

src/i18n/formatters.ts
export function formatDate(date: Date | string, lang: string, options?: Intl.DateTimeFormatOptions) {
const defaultOptions: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'long',
day: 'numeric',
};
const dateToFormat = typeof date === 'string' ? new Date(date) : date;
return new Intl.DateTimeFormat(lang, { ...defaultOptions, ...options }).format(dateToFormat);
}
export function formatNumber(number: number, lang: string, options?: Intl.NumberFormatOptions) {
const defaultOptions: Intl.NumberFormatOptions = {
style: 'decimal',
};
return new Intl.NumberFormat(lang, { ...defaultOptions, ...options }).format(number);
}
export function formatCurrency(amount: number, lang: string, currency: string = 'BRL') {
return new Intl.NumberFormat(lang, {
style: 'currency',
currency,
}).format(amount);
}

Uso dos formatadores:

src/pages/[lang]/product.astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getLanguageFromUrl, useTranslations } from '../../i18n/utils';
import { formatDate, formatCurrency } from '../../i18n/formatters';
import { languages } from '../../i18n/ui';
const lang = getLanguageFromUrl(Astro.url);
const t = useTranslations(lang as keyof typeof languages);
const product = {
name: 'Produto Exemplo',
price: 99.99,
releaseDate: new Date('2023-01-15'),
};
---
<BaseLayout title={product.name}>
<main>
<h1>{product.name}</h1>
<p class="price">{formatCurrency(product.price, lang)}</p>
<p class="date">
{t('product.releaseDate')}: {formatDate(product.releaseDate, lang)}
</p>
</main>
</BaseLayout>

Pluralização e Formatação Complexa

Implementação de Pluralização

// src/i18n/utils.ts (adição)
export function plural(count: number, forms: { one: string; other: string; zero?: string }, lang: string) {
const pluralRules = new Intl.PluralRules(lang);
const rule = pluralRules.select(count);
if (rule === 'zero' && forms.zero) {
return forms.zero.replace('{count}', count.toString());
}
if (rule === 'one') {
return forms.one.replace('{count}', count.toString());
}
return forms.other.replace('{count}', count.toString());
}

Uso da pluralização:

src/pages/[lang]/cart.astro
---
import { getLanguageFromUrl, useTranslations, plural } from '../../i18n/utils';
import { languages } from '../../i18n/ui';
const lang = getLanguageFromUrl(Astro.url);
const t = useTranslations(lang as keyof typeof languages);
const itemCount = 5;
---
<p>
{plural(itemCount, {
zero: t('cart.empty'),
one: t('cart.oneItem'),
other: t('cart.manyItems'),
}, lang)}
</p>

Detecção Automática de Idioma

Middleware para Detecção de Idioma

src/middleware.ts
import { defaultLanguage, languages } from './i18n/ui';
export function onRequest({ request, redirect }, next) {
const url = new URL(request.url);
const [, lang] = url.pathname.split('/');
// Se o caminho já tem um idioma válido, continue
if (lang && Object.keys(languages).includes(lang)) {
return next();
}
// Detectar idioma preferido do navegador
const acceptLanguage = request.headers.get('accept-language') || '';
const browserLang = acceptLanguage
.split(',')
.map(lang => lang.split(';')[0].trim())
.find(lang => Object.keys(languages).includes(lang)) || defaultLanguage;
// Redirecionar para o idioma detectado
return redirect(`/${browserLang}${url.pathname}`);
}

SEO para Sites Multilíngues

Metadados Específicos por Idioma

src/components/SeoHead.astro
---
import { getAlternateLinks } from '../i18n/utils';
import { languages } from '../i18n/ui';
const {
title,
description,
image,
lang,
path
} = Astro.props;
const alternateLinks = getAlternateLinks(path);
const siteUrl = Astro.site || 'https://exemplo.com';
---
<!-- Metadados básicos -->
<title>{title}</title>
<meta name="description" content={description}>
<!-- Canonical e alternates -->
<link rel="canonical" href={new URL(`/${lang}${path}`, siteUrl).href}>
{alternateLinks.map(({ href, hreflang }) => (
<link rel="alternate" href={new URL(href, siteUrl).href} hreflang={hreflang} />
))}
<link rel="alternate" href={new URL(`/${lang}${path}`, siteUrl).href} hreflang="x-default" />
<!-- Open Graph -->
<meta property="og:title" content={title}>
<meta property="og:description" content={description}>
<meta property="og:url" content={new URL(`/${lang}${path}`, siteUrl).href}>
<meta property="og:locale" content={lang}>
{Object.keys(languages).filter(l => l !== lang).map(l => (
<meta property="og:locale:alternate" content={l}>
))}
<meta property="og:image" content={image ? new URL(image, siteUrl).href : new URL('/images/default-og.jpg', siteUrl).href}>

Uso do componente SEO:

src/pages/[lang]/about.astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import SeoHead from '../../components/SeoHead.astro';
import { getLanguageFromUrl, useTranslations } from '../../i18n/utils';
import { languages } from '../../i18n/ui';
export async function getStaticPaths() {
return Object.keys(languages).map((lang) => ({
params: { lang },
}));
}
const { lang } = Astro.params;
const t = useTranslations(lang as keyof typeof languages);
const path = '/about';
---
<BaseLayout>
<SeoHead
slot="head"
title={t('about.title')}
description={t('about.description')}
lang={lang}
path={path}
/>
<main>
<h1>{t('about.title')}</h1>
<p>{t('about.description')}</p>
</main>
</BaseLayout>

Integração com Sistemas de Tradução

Integração com i18next

Terminal window
# Instalar i18next
npm install i18next
src/i18n/i18next.ts
import i18next from 'i18next';
import ptBR from './translations/pt-BR';
import en from './translations/en';
import es from './translations/es';
i18next.init({
lng: 'pt-BR',
fallbackLng: 'pt-BR',
resources: {
'pt-BR': { translation: ptBR },
'en': { translation: en },
'es': { translation: es },
},
interpolation: {
escapeValue: false,
},
});
export default i18next;
export function t(key: string, options?: any) {
return i18next.t(key, options);
}
export function changeLanguage(lang: string) {
return i18next.changeLanguage(lang);
}

Uso com i18next:

src/pages/[lang]/dynamic.astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { languages } from '../../i18n/ui';
import { t, changeLanguage } from '../../i18n/i18next';
export async function getStaticPaths() {
return Object.keys(languages).map((lang) => ({
params: { lang },
}));
}
const { lang } = Astro.params;
await changeLanguage(lang);
const username = 'João';
const count = 3;
---
<BaseLayout>
<main>
<!-- Interpolação simples -->
<p>{t('welcome', { name: username })}</p>
<!-- Pluralização -->
<p>{t('items', { count })}</p>
<!-- Formatação de data -->
<p>{t('lastLogin', { date: new Date(), formatParams: { date: { month: 'long', day: 'numeric' } } })}</p>
</main>
</BaseLayout>

Gestão de Conteúdo Multilíngue

Estrutura de Conteúdo com Astro Content Collections

src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blogCollection = defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
image: z.string().optional(),
lang: z.enum(['pt-BR', 'en', 'es']),
// Para relacionar traduções do mesmo post
translationKey: z.string(),
}),
});
export const collections = {
'blog': blogCollection,
};

Estrutura de arquivos de conteúdo:

src/content/
└── blog/
├── pt-BR/
│ ├── primeiro-post.md
│ └── segundo-post.md
├── en/
│ ├── first-post.md
│ └── second-post.md
└── es/
├── primer-post.md
└── segundo-post.md

Exemplo de post com traduções:

src/content/blog/pt-BR/primeiro-post.md
---
title: "Meu Primeiro Post"
description: "Este é meu primeiro post no blog."
pubDate: 2023-01-15
image: "/images/post1.jpg"
lang: "pt-BR"
translationKey: "first-post"
---
Conteúdo do post em português...
src/content/blog/en/first-post.md
---
title: "My First Post"
description: "This is my first blog post."
pubDate: 2023-01-15
image: "/images/post1.jpg"
lang: "en"
translationKey: "first-post"
---
Post content in English...

Listagem de posts por idioma:

src/pages/[lang]/blog/index.astro
---
import { getCollection } from 'astro:content';
import BaseLayout from '../../../layouts/BaseLayout.astro';
import { getLanguageFromUrl, useTranslations } from '../../../i18n/utils';
import { languages } from '../../../i18n/ui';
import { formatDate } from '../../../i18n/formatters';
export async function getStaticPaths() {
return Object.keys(languages).map((lang) => ({
params: { lang },
}));
}
const { lang } = Astro.params;
const t = useTranslations(lang as keyof typeof languages);
// Buscar posts no idioma atual
const posts = await getCollection('blog', ({ data }) => {
return data.lang === lang;
});
// Ordenar por data de publicação
const sortedPosts = posts.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
---
<BaseLayout title={t('blog.title')} description={t('blog.description')}>
<main>
<h1>{t('blog.title')}</h1>
<ul class="post-list">
{sortedPosts.map(post => (
<li>
<a href={`/${lang}/blog/${post.slug}`}>
<h2>{post.data.title}</h2>
<p class="date">{formatDate(post.data.pubDate, lang)}</p>
<p>{post.data.description}</p>
</a>
</li>
))}
</ul>
</main>
</BaseLayout>

Página de post com links para traduções:

src/pages/[lang]/blog/[...slug].astro
---
import { getCollection } from 'astro:content';
import BaseLayout from '../../../layouts/BaseLayout.astro';
import { getLanguageFromUrl } from '../../../i18n/utils';
import { languages } from '../../../i18n/ui';
import { formatDate } from '../../../i18n/formatters';
export async function getStaticPaths() {
const allPosts = await getCollection('blog');
return allPosts.map(post => ({
params: {
lang: post.data.lang,
slug: post.slug,
},
props: { post },
}));
}
const { post } = Astro.props;
const { lang } = Astro.params;
const { Content } = await post.render();
// Buscar traduções disponíveis deste post
const translations = await getCollection('blog', ({ data }) => {
return data.translationKey === post.data.translationKey && data.lang !== post.data.lang;
});
---
<BaseLayout title={post.data.title} description={post.data.description}>
<main>
<article>
<h1>{post.data.title}</h1>
<p class="date">{formatDate(post.data.pubDate, lang)}</p>
{translations.length > 0 && (
<div class="translations">
<p>Disponível também em:</p>
<ul>
{translations.map(translation => (
<li>
<a href={`/${translation.data.lang}/blog/${translation.slug}`}>
{languages[translation.data.lang as keyof typeof languages]}
</a>
</li>
))}
</ul>
</div>
)}
<div class="content">
<Content />
</div>
</article>
</main>
</BaseLayout>

Lista de Verificação de Internacionalização

Use esta lista para verificar a implementação de internacionalização do seu site:

Configuração Básica

  • Estrutura de diretórios organizada por idioma
  • Arquivos de tradução para cada idioma suportado
  • Utilitários de i18n para acesso a traduções
  • Redirecionamento para idioma padrão ou detecção automática

Conteúdo e UI

  • Todas as strings de UI são externalizadas e traduzidas
  • Formatação localizada de datas, números e moedas
  • Suporte a pluralização
  • Seletor de idioma funcional

SEO e Metadados

  • Tags <html lang> corretas para cada idioma
  • Links <link rel="alternate" hreflang> para todas as versões
  • Metadados Open Graph com atributos og:locale
  • URLs localizadas e amigáveis

Conteúdo Dinâmico

  • Sistema para gerenciar conteúdo em múltiplos idiomas
  • Links entre versões traduzidas do mesmo conteúdo
  • Fallback para conteúdo não traduzido

Testes

  • Verificação de strings não traduzidas
  • Testes em diferentes configurações regionais
  • Validação de formatação de datas e números

Conclusão

A implementação de internacionalização em soluções web desenvolvidas com Astro 5 permite que seu site alcance um público global, oferecendo uma experiência personalizada para usuários de diferentes idiomas e regiões. Ao seguir as práticas recomendadas neste documento, você estará criando uma base sólida para expandir seu site para novos mercados.

Lembre-se de que a internacionalização é um processo contínuo. À medida que seu site evolui, você precisará manter as traduções atualizadas e possivelmente adicionar suporte para novos idiomas.

Os próximos documentos detalharão outros aspectos do desenvolvimento:


Última atualização: 21 de março de 2025