Skip to content

Email i18n Pattern

Type-safe translations for Blazor email templates using typed records.

Structure

Integrations/Email/
├── Models/
│   └── EmailConfirmationEmail.cs    # Email data model
├── Templates/
│   └── EmailConfirmationTemplate.razor
└── Translations/
    ├── IEmailStrings.cs             # Interface
    ├── EmailStrings.cs              # Static factory
    ├── EnglishEmailStrings.cs       # English translations
    ├── CzechEmailStrings.cs         # Czech translations
    └── Records/
        ├── EmailConfirmationStrings.cs
        ├── PasswordResetStrings.cs
        └── CommonStrings.cs

String Records

Each email type has a record defining its translatable strings:

public sealed record EmailConfirmationStrings(
    string TitleInitial,
    string TitleChange,
    Func<string, string> BodyInitial,    // Typed placeholder
    Func<string, string> BodyChange,
    string Button,
    string AdminNote,
    string IgnoreNoteInitial,
    string IgnoreNoteChange);

Language Implementation

public sealed class EnglishEmailStrings : IEmailStrings
{
    public EmailConfirmationStrings EmailConfirmation { get; } = new(
        TitleInitial: "Confirm your email address",
        TitleChange: "Confirm your email change",
        BodyInitial: validity => $"Thank you for registering. The link is valid for {validity}.",
        BodyChange: validity => $"We received a request to change your email. The link is valid for {validity}.",
        Button: "Confirm Email",
        AdminNote: "Your account will be submitted for administrator approval.",
        IgnoreNoteInitial: "If you didn't create an account, you can safely ignore this email.",
        IgnoreNoteChange: "If you didn't request this change, you can safely ignore this email.");
}

Static Factory

public static class EmailStrings
{
    private static readonly IEmailStrings English = new EnglishEmailStrings();
    private static readonly IEmailStrings Czech = new CzechEmailStrings();

    public static IEmailStrings For(Language language) => language switch
    {
        Language.Czech => Czech,
        _ => English
    };
}

Template Usage

<EmailLayout Language="@(Model?.Language ?? Language.English)">
    <h2>@S.EmailConfirmation.TitleInitial</h2>
    <p>@S.EmailConfirmation.BodyInitial(Model?.ValidityText ?? "")</p>
    <EmailButton Text="@S.EmailConfirmation.Button" />
</EmailLayout>

@code {
    [Parameter]
    public EmailConfirmationEmail? Model { get; set; }

    private IEmailStrings S => EmailStrings.For(Model?.Language ?? Language.English);
}

Email Model Subject

public sealed record EmailConfirmationEmail(...) : EmailRequest(...)
{
    public override string GetSubject()
    {
        var s = EmailStrings.For(Language).EmailConfirmation;
        return IsInitialRegistration ? s.TitleInitial : s.TitleChange;
    }
}

Pluralization

Handle language-specific plural forms inside the translation class:

public sealed class CzechEmailStrings : IEmailStrings
{
    public RegistrationExpiringStrings RegistrationExpiring { get; } = new(
        Title: "Vaše registrace brzy vyprší",
        Body: days => $"Registrace vyprší za {days} {PluralDays(days)}.",
        ...);

    private static string PluralDays(int count) => count switch
    {
        1 => "den",
        >= 2 and <= 4 => "dny",
        _ => "dní"
    };
}

Adding a New Language

  1. Create GermanEmailStrings.cs implementing IEmailStrings
  2. Add to factory:
    public static IEmailStrings For(Language language) => language switch
    {
        Language.Czech => Czech,
        Language.German => German,  // Add
        _ => English
    };
    

Adding a New Email Type

  1. Create record in Records/NewEmailStrings.cs
  2. Add property to IEmailStrings interface
  3. Implement in EnglishEmailStrings and CzechEmailStrings
  4. Use in template via S.NewEmail.Title

Benefits

Feature Benefit
Compile-time safety Missing translation = build error
Typed placeholders Func<string, string> prevents missing args
IDE support Autocomplete, find usages, rename
Testable Mock IEmailStrings for unit tests
Side-by-side All English in one file for easy review