Produtos globais precisam de emails localizados. Idioma, formatação e considerações culturais impactam o engajamento. Veja como construir um sistema de email localizado.
Componentes de localização
- —Idioma/traduções
- —Formatação de data e hora
- —Formatação de números e moeda
- —Suporte a RTL (da direita para a esquerda)
- —Considerações culturais
Localização de templates
Estrutura de tradução
interface LocalizedTemplate {
templateId: string;
translations: {
[locale: string]: {
subject: string;
preheader?: string;
content: Record<string, string>;
};
};
fallbackLocale: string;
}
const welcomeEmail: LocalizedTemplate = {
templateId: 'welcome',
fallbackLocale: 'en',
translations: {
en: {
subject: 'Welcome to {{appName}}',
preheader: 'Get started with your new account',
content: {
greeting: 'Hi {{name}},',
body: 'Thanks for signing up!',
cta: 'Get started'
}
},
es: {
subject: 'Bienvenido a {{appName}}',
preheader: 'Comienza con tu nueva cuenta',
content: {
greeting: 'Hola {{name}},',
body: '¡Gracias por registrarte!',
cta: 'Comenzar'
}
}
}
};
Detecção de locale
function getUserLocale(user: User): string {
// Priority: explicit preference > browser > IP geolocation > default
return (
user.preferredLocale ||
user.browserLocale ||
user.geoLocale ||
'en'
);
}
async function sendLocalizedEmail(user: User, templateId: string, data: any) {
const locale = getUserLocale(user);
const template = await getLocalizedTemplate(templateId, locale);
await sendEmail({
to: user.email,
subject: renderTemplate(template.subject, data),
html: renderTemplate(template.html, data),
headers: {
'Content-Language': locale
}
});
}
Formatação de data e hora
Formatação sensível ao fuso horário
function formatDateTime(date: Date, locale: string, timezone: string): string {
return new Intl.DateTimeFormat(locale, {
dateStyle: 'long',
timeStyle: 'short',
timeZone: timezone
}).format(date);
}
// Examples:
// en-US, America/New_York: "January 15, 2026 at 3:30 PM"
// de-DE, Europe/Berlin: "15. Januar 2026 um 21:30"
// ja-JP, Asia/Tokyo: "2026年1月16日 5:30"
Tempo relativo
function formatRelativeTime(date: Date, locale: string): string {
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
const diff = date.getTime() - Date.now();
const days = Math.round(diff / (1000 * 60 * 60 * 24));
if (Math.abs(days) < 1) {
const hours = Math.round(diff / (1000 * 60 * 60));
return rtf.format(hours, 'hour');
}
return rtf.format(days, 'day');
}
// "in 2 days" (en), "dans 2 jours" (fr), "2日後" (ja)
Formatação de moeda e números
function formatCurrency(amount: number, currency: string, locale: string): string {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency
}).format(amount);
}
// formatCurrency(1234.56, 'USD', 'en-US') → "$1,234.56"
// formatCurrency(1234.56, 'EUR', 'de-DE') → "1.234,56 €"
// formatCurrency(1234.56, 'JPY', 'ja-JP') → "¥1,235"
Suporte a RTL
Detectando idiomas RTL
const rtlLocales = ['ar', 'he', 'fa', 'ur'];
function isRTL(locale: string): boolean {
return rtlLocales.includes(locale.split('-')[0]);
}
Template de email RTL
<!DOCTYPE html>
<html lang="{{locale}}" dir="{{#if isRTL}}rtl{{else}}ltr{{/if}}">
<head>
<style>
{{#if isRTL}}
body {
direction: rtl;
text-align: right;
}
.button {
/* Flip margins for RTL */
margin-left: 0;
margin-right: auto;
}
{{/if}}
</style>
</head>
<body>
<!-- Content automatically flows RTL -->
</body>
</html>
Considerações culturais
Saudações por cultura
const greetings = {
en: { formal: 'Dear {{name}}', casual: 'Hi {{name}}' },
de: { formal: 'Sehr geehrte/r {{name}}', casual: 'Hallo {{name}}' },
ja: { formal: '{{name}}様', casual: '{{name}}さん' },
es: { formal: 'Estimado/a {{name}}', casual: 'Hola {{name}}' }
};
function getGreeting(locale: string, formality: 'formal' | 'casual'): string {
return greetings[locale]?.[formality] || greetings.en[formality];
}
Preferências de formato de data
const datePreferences = {
'en-US': 'MM/DD/YYYY',
'en-GB': 'DD/MM/YYYY',
'de-DE': 'DD.MM.YYYY',
'ja-JP': 'YYYY年MM月DD日'
};
Fluxo de tradução
Gerenciando traduções
// Translation file structure
// locales/
// en/
// emails.json
// es/
// emails.json
interface TranslationFile {
[key: string]: string | TranslationFile;
}
// emails.json
{
"welcome": {
"subject": "Welcome to {{appName}}",
"greeting": "Hi {{name}},",
"body": "Thanks for joining us!",
"cta": "Get started"
}
}
Cadeia de fallback
async function getTranslation(key: string, locale: string): Promise<string> {
// Try exact locale (e.g., 'en-GB')
let translation = await loadTranslation(key, locale);
if (translation) return translation;
// Try language only (e.g., 'en')
const language = locale.split('-')[0];
translation = await loadTranslation(key, language);
if (translation) return translation;
// Fall back to default
return loadTranslation(key, 'en');
}
Testando a localização
describe('Email Localization', () => {
const locales = ['en', 'es', 'de', 'ja', 'ar'];
locales.forEach(locale => {
it(`renders welcome email in ${locale}`, async () => {
const html = await renderEmail('welcome', locale, testData);
// Check language attribute
expect(html).toContain(`lang="${locale}"`);
// Check RTL if applicable
if (isRTL(locale)) {
expect(html).toContain('dir="rtl"');
}
// Check no missing translations
expect(html).not.toContain('{{');
});
});
});
Boas práticas
- —Use o formato de mensagens ICU - Lida com plurais, gênero, etc.
- —Armazene a preferência de locale do usuário - Não adivinhe sempre
- —Teste todos os locales - Testes automatizados de renderização
- —Trate traduções faltantes - Fallbacks adequados
- —Considere a expansão de texto - Alemão é ~30% mais longo que o inglês
- —Respeite normas culturais - Formalidade, formatos de data, etc.
Localização é mais do que tradução. É fazer com que os usuários sintam que seu produto foi feito para eles.