emailr_
Todos os artigos
caso de uso·9 min

Localização de email: estratégias multilíngues

localizaçãoi18nglobal

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 &#123;&#123;appName&#125;&#125;',
      preheader: 'Get started with your new account',
      content: {
        greeting: 'Hi &#123;&#123;name&#125;&#125;,',
        body: 'Thanks for signing up!',
        cta: 'Get started'
      }
    },
    es: {
      subject: 'Bienvenido a &#123;&#123;appName&#125;&#125;',
      preheader: 'Comienza con tu nueva cuenta',
      content: {
        greeting: 'Hola &#123;&#123;name&#125;&#125;,',
        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="&#123;&#123;locale&#125;&#125;" dir="&#123;&#123;#if isRTL&#125;&#125;rtl&#123;&#123;else&#125;&#125;ltr&#123;&#123;/if&#125;&#125;">
<head>
  <style>
    &#123;&#123;#if isRTL&#125;&#125;
    body {
      direction: rtl;
      text-align: right;
    }
    .button {
      /* Flip margins for RTL */
      margin-left: 0;
      margin-right: auto;
    }
    &#123;&#123;/if&#125;&#125;
  </style>
</head>
<body>
  <!-- Content automatically flows RTL -->
</body>
</html>

Considerações culturais

Saudações por cultura

const greetings = {
  en: { formal: 'Dear &#123;&#123;name&#125;&#125;', casual: 'Hi &#123;&#123;name&#125;&#125;' },
  de: { formal: 'Sehr geehrte/r &#123;&#123;name&#125;&#125;', casual: 'Hallo &#123;&#123;name&#125;&#125;' },
  ja: { formal: '&#123;&#123;name&#125;&#125;様', casual: '&#123;&#123;name&#125;&#125;さん' },
  es: { formal: 'Estimado/a &#123;&#123;name&#125;&#125;', casual: 'Hola &#123;&#123;name&#125;&#125;' }
};

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 &#123;&#123;appName&#125;&#125;",
    "greeting": "Hi &#123;&#123;name&#125;&#125;,",
    "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('&#123;&#123;');
    });
  });
});

Boas práticas

  1. Use o formato de mensagens ICU - Lida com plurais, gênero, etc.
  2. Armazene a preferência de locale do usuário - Não adivinhe sempre
  3. Teste todos os locales - Testes automatizados de renderização
  4. Trate traduções faltantes - Fallbacks adequados
  5. Considere a expansão de texto - Alemão é ~30% mais longo que o inglês
  6. 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.

e_

Escrito pela equipe emailr

Construindo infraestrutura de email para desenvolvedores

Pronto para começar a enviar?

Obtenha sua chave API e envie seu primeiro email em menos de 5 minutos. Não é necessário cartão de crédito.