Building Apps for a Global Audience
Picture this: you've built an e-commerce site that works perfectly for US customers. Sales are growing, so you expand to Europe. Suddenly, customers in France complain that prices look wrong and dates are confusing. German users report validation errors when entering phone numbers. Your application assumed everyone formats data like Americans do.
.NET's globalization APIs solve this by handling culture-specific formatting automatically. You get correct date formats, currency symbols, number separators, and text direction for every locale. The CultureInfo class adapts your application to user preferences without rewriting business logic.
You'll learn how to format data for different cultures, manage localized resources, handle time zones properly, and integrate globalization into ASP.NET Core applications. By the end, you'll build applications that feel native to users anywhere in the world.
Working with CultureInfo for Formatting
The CultureInfo class represents a specific culture and controls how .NET formats dates, numbers, and currencies. Each culture has its own rules for decimal separators, date orders, and currency symbols. Instead of writing custom formatting logic for each country, you pass a CultureInfo instance to formatting methods.
You'll use CurrentCulture for formatting data displayed to users and InvariantCulture when storing data. This separation prevents bugs where French date formats get saved to databases or American decimal separators break German number parsing.
using System.Globalization;
var price = 1299.99m;
var saleDate = new DateTime(2025, 11, 4);
// US formatting
var usCulture = new CultureInfo("en-US");
Console.WriteLine($"US: {price.ToString("C", usCulture)}");
Console.WriteLine($"Date: {saleDate.ToString("d", usCulture)}");
// French formatting
var frCulture = new CultureInfo("fr-FR");
Console.WriteLine($"\nFrance: {price.ToString("C", frCulture)}");
Console.WriteLine($"Date: {saleDate.ToString("d", frCulture)}");
// German formatting
var deCulture = new CultureInfo("de-DE");
Console.WriteLine($"\nGermany: {price.ToString("C", deCulture)}");
Console.WriteLine($"Date: {saleDate.ToString("d", deCulture)}");
// Invariant culture for storage
Console.WriteLine($"\nStorage: {price.ToString(CultureInfo.InvariantCulture)}");
Console.WriteLine($"ISO Date: {saleDate.ToString("o", CultureInfo.InvariantCulture)}");
Output:
US: $1,299.99
Date: 11/4/2025
France: 1 299,99 €
Date: 04/11/2025
Germany: 1.299,99 €
Date: 04.11.2025
Storage: 1299.99
ISO Date: 2025-11-04T00:00:00.0000000
Notice how the same decimal value displays differently: US uses periods for decimals and commas for thousands, while France and Germany reverse this. The date format changes too. Using InvariantCulture ensures consistent storage formats that parse correctly regardless of server location.
Managing Translations with Resource Files
Resource files (.resx) store translated strings separate from your code. You create one resource file per language, and .NET loads the correct file based on CurrentUICulture. This lets translators work without touching your C# code and makes adding new languages straightforward.
Resource files compile into satellite assemblies that deploy alongside your application. The ResourceManager class loads strings automatically from the correct assembly based on culture settings.
// In Visual Studio, create Resources.resx with these entries:
// Key: WelcomeMessage, Value: Welcome to our application
// Key: GoodbyeMessage, Value: Thank you for visiting
// Key: ItemCount, Value: You have {0} items in your cart
// Create Resources.fr-FR.resx with French translations:
// Key: WelcomeMessage, Value: Bienvenue dans notre application
// Key: GoodbyeMessage, Value: Merci de votre visite
// Key: ItemCount, Value: Vous avez {0} articles dans votre panier
using System.Globalization;
using System.Resources;
var resourceManager = new ResourceManager("MyApp.Resources",
typeof(Program).Assembly);
// Display in English
CultureInfo.CurrentUICulture = new CultureInfo("en-US");
Console.WriteLine("English:");
Console.WriteLine(resourceManager.GetString("WelcomeMessage"));
Console.WriteLine(string.Format(
resourceManager.GetString("ItemCount"), 5));
// Display in French
CultureInfo.CurrentUICulture = new CultureInfo("fr-FR");
Console.WriteLine("\nFrançais:");
Console.WriteLine(resourceManager.GetString("WelcomeMessage"));
Console.WriteLine(string.Format(
resourceManager.GetString("ItemCount"), 5));
Output:
English:
Welcome to our application
You have 5 items in your cart
Français:
Bienvenue dans notre application
Vous avez 5 articles dans votre panier
The ResourceManager finds the correct .resx file based on CurrentUICulture. If a translation doesn't exist for the requested culture, it falls back to the default resource file. This ensures your app always displays something instead of throwing errors.
Parsing User Input Safely
When users enter dates or numbers, they use their local format. Parsing this input requires knowing their culture. If you parse a French date "04/11/2025" using US culture, you'll get April 11 instead of November 4. Always parse with the same culture the user entered data in.
The Parse and TryParse methods accept IFormatProvider parameters where you pass CultureInfo instances. For user input, use the current culture. For stored data, use InvariantCulture.
using System.Globalization;
// Parsing with different cultures
var germanInput = "1.299,99";
var usInput = "1,299.99";
var dateInput = "04/11/2025";
// Parse German number format
if (decimal.TryParse(germanInput, NumberStyles.Number,
new CultureInfo("de-DE"), out decimal germanValue))
{
Console.WriteLine($"German value: {germanValue}");
}
// Parse US number format
if (decimal.TryParse(usInput, NumberStyles.Number,
new CultureInfo("en-US"), out decimal usValue))
{
Console.WriteLine($"US value: {usValue}");
}
// Parse date with different interpretations
var frenchDate = DateTime.Parse(dateInput, new CultureInfo("fr-FR"));
var usDate = DateTime.Parse(dateInput, new CultureInfo("en-US"));
Console.WriteLine($"\nFrench interpretation: {frenchDate:yyyy-MM-dd}");
Console.WriteLine($"US interpretation: {usDate:yyyy-MM-dd}");
// Safe parsing with invariant culture for storage
var storedValue = "1299.99";
var parsed = decimal.Parse(storedValue, CultureInfo.InvariantCulture);
Console.WriteLine($"\nParsed from storage: {parsed}");
Output:
German value: 1299.99
US value: 1299.99
French interpretation: 2025-11-04
US interpretation: 2025-04-11
Parsed from storage: 1299.99
The same string "04/11/2025" parses to different dates depending on culture. French culture reads it as November 4, while US culture reads April 11. This shows why you must track the user's culture when collecting input and use InvariantCulture for stored values.
Mistakes to Avoid
Mixing CurrentCulture and InvariantCulture causes subtle bugs. You might format a decimal with CurrentCulture, save it to a file, then fail to parse it when the server culture changes. Always use InvariantCulture for serialization, file I/O, and database storage. Use CurrentCulture only when displaying values to users.
Another mistake is assuming string comparisons work the same everywhere. The string "file" sorts before "File" in English but after in Swedish. Use StringComparison.Ordinal for case-sensitive comparisons or StringComparison.OrdinalIgnoreCase for case-insensitive. Avoid CurrentCulture string comparisons for logic that should behave consistently.
Many developers forget that culture affects thread behavior. When you create new threads or use Task.Run, those threads inherit the current culture. If you change culture on one thread, others keep the old culture. Set CultureInfo.DefaultThreadCurrentCulture at startup to ensure consistent behavior across all threads.
Try It Yourself
Build a simple invoice generator that formats prices and dates for different countries. This example shows how culture affects real business documents.
using System.Globalization;
var invoice = new Invoice
{
InvoiceNumber = "INV-2025-001",
Date = DateTime.Now,
Subtotal = 1500.00m,
Tax = 225.00m,
Total = 1725.00m
};
Console.WriteLine("US Invoice:");
invoice.Print(new CultureInfo("en-US"));
Console.WriteLine("\n\nGerman Invoice:");
invoice.Print(new CultureInfo("de-DE"));
Console.WriteLine("\n\nJapanese Invoice:");
invoice.Print(new CultureInfo("ja-JP"));
class Invoice
{
public string InvoiceNumber { get; set; }
public DateTime Date { get; set; }
public decimal Subtotal { get; set; }
public decimal Tax { get; set; }
public decimal Total { get; set; }
public void Print(CultureInfo culture)
{
Console.WriteLine($"Invoice: {InvoiceNumber}");
Console.WriteLine($"Date: {Date.ToString("D", culture)}");
Console.WriteLine(new string('-', 40));
Console.WriteLine($"Subtotal: {Subtotal.ToString("C", culture),20}");
Console.WriteLine($"Tax: {Tax.ToString("C", culture),20}");
Console.WriteLine(new string('-', 40));
Console.WriteLine($"Total: {Total.ToString("C", culture),20}");
}
}
Output:
US Invoice:
Invoice: INV-2025-001
Date: Tuesday, November 4, 2025
----------------------------------------
Subtotal: $1,500.00
Tax: $225.00
----------------------------------------
Total: $1,725.00
German Invoice:
Invoice: INV-2025-001
Date: Dienstag, 4. November 2025
----------------------------------------
Subtotal: 1.500,00 €
Tax: 225,00 €
----------------------------------------
Total: 1.725,00 €
Japanese Invoice:
Invoice: INV-2025-001
Date: 2025年11月4日火曜日
----------------------------------------
Subtotal: ¥1,500
Tax: ¥225
----------------------------------------
Total: ¥1,725
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
This shows how one Invoice class adapts to multiple cultures without changing logic. The culture parameter controls all formatting, making the code easy to maintain and extend to new languages.
Integration with ASP.NET Core
ASP.NET Core includes middleware that sets culture based on user preferences. You configure supported cultures and localization sources, then the middleware handles everything automatically. Users can select their language through browser settings, cookies, or URL parameters.
The RequestLocalizationMiddleware reads culture from Accept-Language headers, query strings, or cookies. You specify fallback cultures for when users request unsupported languages. This approach works for APIs, Razor Pages, and MVC applications.
using System.Globalization;
using Microsoft.AspNetCore.Localization;
var builder = WebApplication.CreateBuilder(args);
// Configure supported cultures
var supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("fr-FR"),
new CultureInfo("de-DE"),
new CultureInfo("ja-JP")
};
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
options.DefaultRequestCulture = new RequestCulture("en-US");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
var app = builder.Build();
// Enable request localization
app.UseRequestLocalization();
app.MapGet("/invoice", (HttpContext context) =>
{
var culture = CultureInfo.CurrentCulture;
var total = 1725.00m;
var date = DateTime.Now;
return new
{
Culture = culture.Name,
Total = total.ToString("C", culture),
Date = date.ToString("D", culture)
};
});
app.Run();
When a user visits the /invoice endpoint, ASP.NET Core automatically sets the culture based on their browser preferences. The response formats currency and dates correctly for their locale. You can test different cultures by setting the Accept-Language header or adding a query parameter like ?culture=fr-FR.
For production applications, store user language preferences in cookies or database profiles. This lets users override browser defaults and persist their choice across sessions. The middleware supports multiple culture providers that you can prioritize to check cookies before headers.