emailr_
Tous les articles
usecase·9 min

Localisation des emails : stratégies multilingues

localizationi18nglobal

Les produits mondiaux ont besoin d’emails localisés. La langue, la mise en forme et les considérations culturelles influencent toutes l’engagement. Voici comment construire un système d’email localisé.

Composants de la localisation

  • Langue/traductions
  • Formatage des dates et des heures
  • Formatage des nombres et des devises
  • Prise en charge RTL (right-to-left)
  • Considérations culturelles

Localisation des modèles

Structure de traduction

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'
      }
    }
  }
};

Détection de la 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
    }
  });
}

Formatage des dates et des heures

Formatage tenant compte du fuseau horaire

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"

Temps relatif

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)

Formatage des devises et des nombres

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"

Prise en charge RTL

Détection des langues RTL

const rtlLocales = ['ar', 'he', 'fa', 'ur'];

function isRTL(locale: string): boolean {
  return rtlLocales.includes(locale.split('-')[0]);
}

Modèle d’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>

Considérations culturelles

Formules de salutation selon la culture

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];
}

Préférences de format de date

const datePreferences = {
  'en-US': 'MM/DD/YYYY',
  'en-GB': 'DD/MM/YYYY',
  'de-DE': 'DD.MM.YYYY',
  'ja-JP': 'YYYY年MM月DD日'
};

Workflow de traduction

Gestion des traductions

// 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"
  }
}

Chaîne de repli

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');
}

Tester la localisation

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;');
    });
  });
});

Bonnes pratiques

  1. Utiliser ICU message format - Gère les pluriels, le genre, etc.
  2. Enregistrer la préférence de locale de l’utilisateur - Ne pas deviner à chaque fois
  3. Tester toutes les locales - Tests de rendu automatisés
  4. Gérer les traductions manquantes - Mécanismes de repli élégants
  5. Prendre en compte l’expansion du texte - L’allemand est ~30 % plus long que l’anglais
  6. Respecter les normes culturelles - Formalité, formats de date, etc.

La localisation va au-delà de la traduction. Il s’agit de donner aux utilisateurs le sentiment que votre produit a été conçu pour eux.

e_

Écrit par l'équipe emailr

Nous construisons l'infrastructure email pour les développeurs

Prêt à commencer à envoyer ?

Obtenez votre clé API et envoyez votre premier email en moins de 5 minutes. Aucune carte de crédit requise.