Skip to content

Internationalization in C# — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Internationalization in C#. We cover key concepts, practical examples, and best practices to help you master this topic.

Hook

The world speaks many languages, and your applications should too. C# and .NET provide comprehensive support for building globally-aware applications through the System.Globalization namespace, resource files, and culture-aware formatting. Internationalization (i18n) lets you reach users worldwide.

Learning Path

graph LR
  A[Internationalization] --> B[Cultures]
  A --> C[Resource Files]
  B --> D[Formatting]
  B --> E[String Comparison]
  C --> F[Localization]
  style A fill:#4a90d9,color:#fff
  style B fill:#4a90d9,color:#fff
  style C fill:#4a90d9,color:#fff
  style D fill:#4a90d9,color:#fff
  style E fill:#4a90d9,color:#fff
  style F fill:#4a90d9,color:#fff

Understanding Cultures

A culture defines language, number formats, dates, and sorting rules.

using System;
using System.Globalization;

public static class CultureDemo
{
    public static void ShowCultures()
    {
        // Get current culture
        CultureInfo current = CultureInfo.CurrentCulture;
        Console.WriteLine($"Current culture: {current.DisplayName}");

        // List all cultures
        CultureInfo[] cultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
        Console.WriteLine($"Total cultures: {cultures.Length}");

        // Specific cultures
        var us = new CultureInfo("en-US");
        var de = new CultureInfo("de-DE");
        var ja = new CultureInfo("ja-JP");
        var ar = new CultureInfo("ar-SA");

        Console.WriteLine($"\nUS: {us.DisplayName}");
        Console.WriteLine($"Germany: {de.DisplayName}");
        Console.WriteLine($"Japan: {ja.DisplayName}");
        Console.WriteLine($"Saudi Arabia: {ar.DisplayName}");
    }
}

Culture-Aware Formatting

Numbers, dates, and currencies should be formatted per culture.

public static class FormattingDemo
{
    public static void ShowFormatting()
    {
        double amount = 1234.56;
        DateTime date = new(2026, 6, 28, 14, 30, 0);

        var cultures = new[] {
            new CultureInfo("en-US"),
            new CultureInfo("de-DE"),
            new CultureInfo("fr-FR"),
            new CultureInfo("ja-JP"),
            new CultureInfo("ar-SA")
        };

        foreach (var culture in cultures)
        {
            Console.WriteLine($"\n{culture.DisplayName}:");
            Console.WriteLine($"  Number: {amount.ToString("N2", culture)}");
            Console.WriteLine($"  Currency: {amount.ToString("C2", culture)}");
            Console.WriteLine($"  Date: {date.ToString("D", culture)}");
            Console.WriteLine($"  Short date: {date.ToString("d", culture)}");
            Console.WriteLine($"  Percent: {(0.25).ToString("P1", culture)}");
        }
    }
}

Output:

English (United States):
  Number: 1,234.56
  Currency: $1,234.56
  Date: Sunday, June 28, 2026

German (Germany):
  Number: 1.234,56
  Currency: 1.234,56 €
  Date: Sonntag, 28. Juni 2026

Resource Files

Resource files (.resx) store locale-specific strings.

<!-- Resources.resx (default/English) -->
<data name="WelcomeMessage" xml:space="preserve">
  <value>Welcome to our application!</value>
</data>
<data name="GoodbyeMessage" xml:space="preserve">
  <value>Thank you for using our app.</value>
</data>

<!-- Resources.de.resx (German) -->
<data name="WelcomeMessage" xml:space="preserve">
  <value>Willkommen in unserer Anwendung!</value>
</data>
// Strongly-typed resources (auto-generated)
Console.WriteLine(Resources.WelcomeMessage);
Console.WriteLine(Resources.GoodbyeMessage);

// Manual resource lookup
ResourceManager rm = new ResourceManager("MyApp.Resources", Assembly.GetExecutingAssembly());
string welcome = rm.GetString("WelcomeMessage", new CultureInfo("de-DE"))!;

Localizing ASP.NET Core

Use the localization middleware in web applications.

// Program.cs
builder.Services.AddLocalization(options =>
{
    options.ResourcesPath = "Resources";
});

builder.Services.AddControllersWithViews()
    .AddViewLocalization()
    .AddDataAnnotationsLocalization();

var supportedCultures = new[]
{
    new CultureInfo("en"),
    new CultureInfo("de"),
    new CultureInfo("fr"),
    new CultureInfo("ja")
};

builder.Services.Configure<RequestLocalizationOptions>(options =>
{
    options.DefaultRequestCulture = new RequestCulture("en");
    options.SupportedCultures = supportedCultures;
    options.SupportedUICultures = supportedCultures;
    options.RequestCultureProviders.Insert(0,
        new QueryStringRequestCultureProvider());
});

var app = builder.Build();
app.UseRequestLocalization();
app.Run();

// In a controller
public class HomeController : Controller
{
    private readonly IStringLocalizer<HomeController> _localizer;

    public HomeController(IStringLocalizer<HomeController> localizer)
    {
        _localizer = localizer;
    }

    public IActionResult Index()
    {
        ViewBag.Message = _localizer["WelcomeMessage"];
        return View();
    }
}

String Comparison Across Cultures

String comparison varies by culture.

public static class StringComparisonDemo
{
    public static void Compare()
    {
        string a = "Straße";
        string b = "STRASSE";

        // Invariant culture
        bool equal1 = string.Equals(a, b, StringComparison.InvariantCultureIgnoreCase);
        Console.WriteLine($"Invariant ignore case: {equal1}");

        // Ordinal (byte-by-byte)
        bool equal2 = string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
        Console.WriteLine($"Ordinal ignore case: {equal2}");

        // German culture
        var de = new CultureInfo("de-DE");
        int result = string.Compare(a, b, de, CompareOptions.IgnoreCase);
        Console.WriteLine($"German ignore case: {result == 0}");

        // Sort order differs by culture
        string[] words = { "apple", "Apfel", "uber", "über" };
        Array.Sort(words, StringComparer.CurrentCulture);
        Console.WriteLine($"Sorted (current culture): {string.Join(", ", words)}");

        Array.Sort(words, StringComparer.Ordinal);
        Console.WriteLine($"Sorted (ordinal): {string.Join(", ", words)}");
    }
}

Pluralization and Gender

Handle plural forms and gender-specific text.

public static string GetItemsMessage(int count, IStringLocalizer localizer)
{
    // Simple approach for English
    if (count == 0)
        return localizer["NoItems"];
    if (count == 1)
        return localizer["OneItem"];
    return localizer["MultipleItems", count];
}

// For complex pluralization, use libraries like PluralizationService
// or ICU MessageFormat via NuGet packages

Common Mistakes

  1. Ignoring culture in string formatting: Use CultureInfo.InvariantCulture for machine-readable output (Serialization, logs) and current culture for user-facing text.

  2. Hardcoding strings: Never hardcode user-facing strings. Always use resource files or localization services.

  3. Assuming date/time formats: Different cultures use different date separators, order (MM/dd vs dd/MM), and calendars.

  4. Not handling right-to-left languages: Arabic and Hebrew text require special layout considerations in UI.

  5. Using string concatenation for localized messages: Word order changes across languages. Use format strings with placeholders instead.

Practice Questions

  1. Create resource files for an application in English, French, and Japanese, and display localized messages based on the current thread culture.

  2. Write a custom IFormatProvider that displays numbers in a specific format (e.g., engineering notation).

  3. Implement a date picker component that respects different calendar systems (Gregorian, Hijri, Japanese).

  4. Challenge: Build a localization system that loads translations from a JSON file instead of .resx resources.

FAQ

What is the difference between invariant culture and ordinal comparison?

InvariantCulture is based on English rules but is consistent across systems. Ordinal compares byte-by-byte and is the fastest but least linguistically correct.

How do I detect the user's preferred language in a web app?

Use the Accept-Language HTTP header. ASP.NET Core's RequestLocalizationMiddleware handles this automatically.

Should I use resx files or JSON for localization?

resx files integrate with Visual Studio's resource editor and generate strongly-typed classes. JSON is easier to manage with translation services.

How do I localize error messages in DataAnnotations?

Use IStringLocalizer with AddDataAnnotationsLocalization() in ASP.NET Core to localize validation attributes.

What is the best way to store translations?

For small apps, .resx files work well. For larger applications, consider a database-backed localization store or a translation management system.

Mini Project: Multi-Language Greeting App

Build a console application that greets the user in their preferred language.

using System;
using System.Globalization;
using System.Resources;

public static class GreetingApp
{
    // Simulated resource strings (in production, use .resx files)
    private static readonly Dictionary<string, Dictionary<string, string>> Translations = new()
    {
        ["en"] = new()
        {
            ["Greeting"] = "Hello!",
            ["TimeMessage"] = "Current time: {0}",
            ["DayMessage"] = "Today is {0}",
            ["Goodbye"] = "Goodbye!"
        },
        ["de"] = new()
        {
            ["Greeting"] = "Hallo!",
            ["TimeMessage"] = "Aktuelle Zeit: {0}",
            ["DayMessage"] = "Heute ist {0}",
            ["Goodbye"] = "Auf Wiedersehen!"
        },
        ["fr"] = new()
        {
            ["Greeting"] = "Bonjour!",
            ["TimeMessage"] = "Heure actuelle: {0}",
            ["DayMessage"] = "Aujourd'hui c'est {0}",
            ["Goodbye"] = "Au revoir!"
        },
        ["ja"] = new()
        {
            ["Greeting"] = "Konnichiwa!",
            ["TimeMessage"] = "Genzai no jikan: {0}",
            ["DayMessage"] = "Kyo wa {0}",
            ["Goodbye"] = "Sayonara!"
        }
    };

    public static void Run(string languageCode)
    {
        if (!Translations.TryGetValue(languageCode, out var strings))
        {
            languageCode = "en";
            strings = Translations["en"];
        }

        var culture = new CultureInfo(languageCode);
        Thread.CurrentThread.CurrentCulture = culture;
        Thread.CurrentThread.CurrentUICulture = culture;

        var now = DateTime.Now;

        Console.WriteLine(strings["Greeting"]);
        Console.WriteLine(strings["TimeMessage"], now.ToString("T", culture));
        Console.WriteLine(strings["DayMessage"], now.ToString("dddd", culture));
        Console.WriteLine();
        Console.WriteLine(strings["Goodbye"]);
    }
}

// Usage
Console.WriteLine("=== English ===");
GreetingApp.Run("en");

Console.WriteLine("\n=== German ===");
GreetingApp.Run("de");

Console.WriteLine("\n=== Japanese ===");
GreetingApp.Run("ja");

Output:

=== English ===
Hello!
Current time: 2:30:00 PM
Today is Sunday
Goodbye!

=== German ===
Hallo!
Aktuelle Zeit: 14:30:00
Heute ist Sonntag
Auf Wiedersehen!

=== Japanese ===
Konnichiwa!
Genzai no jikan: 14:30:00
Kyo wa Sunday
Sayonara!

Internationalization makes your C# applications accessible to a global audience. The .NET platform's culture-aware APIs handle formatting, sorting, and resource management, letting you focus on creating great user experiences in any language.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro