Skip to content

Globalization & Localization

Key Points

  • Globalization = make code work for any culture (parsing, formatting, comparisons). Localization = translate strings/UI per culture.
  • UseRequestLocalization middleware sets CultureInfo.CurrentCulture per request from Accept-Language, query, cookie, or custom provider.
  • IStringLocalizer<T> + .resx files for translated strings. Or use a database/MOM (Message Object Model) for dynamic translation.
  • Always specify culture explicitly when parsing/formatting numbers, dates, currencies. CultureInfo.InvariantCulture for wire formats; CurrentCulture for UI.
  • String.Compare(..., StringComparison.OrdinalIgnoreCase) for non-linguistic comparison. Avoid string.ToLower() for case folding — use StringComparison.OrdinalIgnoreCase.
  • Right-to-left languages (Arabic, Hebrew) need dir="rtl" and CSS support.

Concepts (deep dive)

Setting up request localization

builder.Services.Configure<RequestLocalizationOptions>(o =>
{
    var supported = new[] { "en-US", "es-ES", "fr-FR", "ja-JP" };
    o.SetDefaultCulture("en-US")
     .AddSupportedCultures(supported)
     .AddSupportedUICultures(supported);
});

app.UseRequestLocalization();   // before any localization-using middleware

By default, providers (in order): 1. QueryStringRequestCultureProvider?culture=es-ES&ui-culture=es-ES 2. CookieRequestCultureProvider — cookie set after user selection 3. AcceptLanguageHeaderRequestCultureProvider — browser default

Custom provider:

o.AddInitialRequestCultureProvider(new CustomRequestCultureProvider(async ctx =>
{
    var user = ctx.User.Identity?.Name;
    var pref = await GetUserCultureAsync(user);
    return new ProviderCultureResult(pref);
}));

CurrentCulture vs CurrentUICulture

  • CurrentCulture — drives parsing/formatting (numbers, dates, currency).
  • CurrentUICulture — drives resource lookup (translated strings).

Often the same; sometimes different (a user prefers Spanish UI but US date format, e.g.).

Localization with IStringLocalizer<T>

// Resource file: Resources/HomeController.es.resx with key "Welcome" = "Bienvenido"
public class HomeController(IStringLocalizer<HomeController> localizer) : Controller
{
    public IActionResult Index()
    {
        var greeting = localizer["Welcome"];   // "Welcome" or "Bienvenido" by current UI culture
        return View(model: greeting.Value);
    }
}

builder.Services.AddLocalization(o => o.ResourcesPath = "Resources");
builder.Services.AddControllersWithViews()
    .AddViewLocalization()
    .AddDataAnnotationsLocalization();

.resx files are XML; tooling (Visual Studio's resource editor) generates accessor classes. For modern dev, IStringLocalizerFactory can also load translations from a database or JSON.

Razor view localization

@inject IViewLocalizer L
<h1>@L["Welcome"]</h1>

DataAnnotations localization

public class RegisterModel
{
    [Required(ErrorMessage = "EmailRequired")]
    [EmailAddress(ErrorMessage = "EmailInvalid")]
    public string Email { get; set; } = "";
}

Resource file RegisterModel.resx provides the key EmailRequired"Email is required".

Format strings — IFormatProvider matters

DateTime.Now.ToString("d");                      // depends on CurrentCulture
DateTime.Now.ToString("d", CultureInfo.InvariantCulture);   // explicit
decimal.Parse("1,234.56", CultureInfo.InvariantCulture);     // 1234.56 in en-US
decimal.Parse("1,234.56", new CultureInfo("de-DE"));         // throws — German uses '.' as thousands separator

Rule: for wire formats (JSON, query strings, files), use InvariantCulture. For user-facing display, use CurrentCulture.

StringComparison — avoid culture surprises

"İ".ToLower();                      // depends on culture (Turkish 'I' issue)
"İ".ToLower(CultureInfo.InvariantCulture);   // explicit but still expensive

// Preferred:
str.Equals("foo", StringComparison.OrdinalIgnoreCase);
str.Contains("foo", StringComparison.OrdinalIgnoreCase);

Ordinal* comparisons are byte-wise, fast, and culture-independent. OrdinalIgnoreCase is the right default for non-linguistic string comparisons (URLs, identifiers, JSON keys).

Time zone handling is in DateTime & TimeProvider. Quick rule: store UTC, display in user's zone, never use DateTime.Now in services.

Right-to-left languages

<html lang="@CultureInfo.CurrentUICulture.Name" dir="@(CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft ? "rtl" : "ltr")">

CSS:

[dir="rtl"] .header { padding-right: 16px; padding-left: 0; }

Modern CSS direction-aware properties (padding-inline-start) handle RTL automatically — prefer those.

Number formatting

1234.56.ToString("N", new CultureInfo("en-US"));   // "1,234.56"
1234.56.ToString("N", new CultureInfo("de-DE"));   // "1.234,56"
1234.56.ToString("C", new CultureInfo("ja-JP"));   // "¥1,234.56"

"C" for currency picks the culture's currency symbol. Mind that the user's locale doesn't tell you their currency — a Japanese user paying in USD still wants the "$" sign for that price.

Pluralization

.resx doesn't natively support plurals ({0} item vs {0} items). For real i18n, use PluralRules (e.g., .resx per-form or Microsoft.Extensions.Localization.Plural).

Source-gen alternative

Microsoft.Extensions.Localization is reflection-based. AOT-friendly source-gen alternatives are emerging — keep an eye on LocalizationSourceGenerator for upcoming releases.


Code: correct vs wrong

❌ Wrong: parsing without culture

decimal.Parse(input);   // depends on CurrentCulture; deployment in different region → fails

✅ Correct: explicit

decimal.Parse(input, CultureInfo.InvariantCulture);

❌ Wrong: ToLower() for case-insensitive compare

str1.ToLower() == str2.ToLower();   // ❌ allocations + culture surprises

✅ Correct: StringComparison

str1.Equals(str2, StringComparison.OrdinalIgnoreCase);

❌ Wrong: hard-coded English error messages

return BadRequest("Email is required");   // ❌ untranslatable

✅ Correct: localized

return BadRequest(_localizer["EmailRequired"]);

Design patterns for this topic

Pattern 1 — "InvariantCulture for wire; CurrentCulture for UI"

  • Intent: consistent serialization; correct user display.

Pattern 2 — "StringComparison.OrdinalIgnoreCase over ToLower"

  • Intent: fast, culture-safe comparisons.

Pattern 3 — "IStringLocalizer<T> for app strings"

  • Intent: key-based translation.

Pattern 4 — "Custom culture provider for user-stored preference"

  • Intent: persist user choice across requests.

Pattern 5 — "Direction-aware CSS for RTL"

  • Intent: layout adapts to language.

Pros & cons / trade-offs

Choice Pros Cons
.resx files Built-in tooling XML; static
Database localization Dynamic Custom infra
InvariantCulture Predictable Doesn't display nice
Cookie-based culture Persists user choice Cookie banner laws

When to use / when to avoid

  • Use IStringLocalizer<T> for any user-facing string.
  • Use InvariantCulture for parsing/formatting in wire formats.
  • Use StringComparison.OrdinalIgnoreCase for non-linguistic compares.
  • Avoid DateTime.Now — use TimeProvider.
  • Avoid string.ToLower() for case-insensitive compares.

Interview Q&A

Q1. Difference between Globalization and Localization? Globalization = code works for any culture. Localization = translated strings for a specific culture.

Q2. CurrentCulture vs CurrentUICulture? CurrentCulture controls parsing/formatting. CurrentUICulture controls resource lookup (translated strings).

Q3. Why use StringComparison.OrdinalIgnoreCase over ToLower()? Faster (no allocation), culture-independent (no Turkish 'I' issues).

Q4. What's InvariantCulture for? Wire formats, file names, identifiers — anything that should be culture-independent.

Q5. How does UseRequestLocalization decide the culture? By default: query string → cookie → Accept-Language header. Custom providers can be inserted.

Q6. What's IStringLocalizer<T>? DI-injected localizer scoped to type T (its .resx namespace). [key] lookup returns translated string.

Q7. How do you localize validation messages? AddDataAnnotationsLocalization() + resource files matching the model class name.

Q8. RTL handling? Set dir="rtl" on the root element when CurrentUICulture.TextInfo.IsRightToLeft. Use direction-aware CSS.

Q9. How do you handle pluralization? .resx doesn't support plurals natively. Use external libraries with PluralRules (CLDR) for proper plurals.

Q10. Best practice for storing user culture preference? Cookie or DB column. Custom request culture provider reads it on each request.


Gotchas / common mistakes

  • ⚠️ Parsing without explicit culture — fails in different regions.
  • ⚠️ ToLower() for compare — allocation + culture surprise.
  • ⚠️ Hard-coded English in return BadRequest("...") — can't translate.
  • ⚠️ Currency symbol from culture — culture is locale, not user's bank account.
  • ⚠️ Forgetting dir="rtl" — broken RTL layout.

Further reading