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 {{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'
}
}
}
};
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="{{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>
Considérations culturelles
Formules de salutation selon la culture
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];
}
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 {{appName}}",
"greeting": "Hi {{name}},",
"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('{{');
});
});
});
Bonnes pratiques
- —Utiliser ICU message format - Gère les pluriels, le genre, etc.
- —Enregistrer la préférence de locale de l’utilisateur - Ne pas deviner à chaque fois
- —Tester toutes les locales - Tests de rendu automatisés
- —Gérer les traductions manquantes - Mécanismes de repli élégants
- —Prendre en compte l’expansion du texte - L’allemand est ~30 % plus long que l’anglais
- —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.