Localization in ConnectSoft Microservice Template¶
Purpose & Overview¶
Localization (i18n) in the ConnectSoft Microservice Template enables applications to support multiple languages and cultures, providing translated content, culture-specific formatting, and region-aware behavior. The template uses ASP.NET Core's built-in localization infrastructure with .NET resource files (.resx) to provide a robust, maintainable localization system.
Localization provides:
- Multi-Language Support: Support for multiple languages and cultures
- Resource Files: .NET resource files (.resx) for string localization
- Culture Detection: Automatic culture detection from HTTP headers and request context
- Strongly-Typed Resources: Type-safe resource access via generated classes
- IStringLocalizer Integration: Flexible string localization via dependency injection
- Validation Message Localization: Localized validation error messages
- Culture-Specific Formatting: Automatic formatting of dates, numbers, and currencies
- Fallback Support: Automatic fallback to parent cultures or default culture
Localization Philosophy
Localization enables microservices to serve global audiences by providing culturally appropriate content and formatting. The template uses industry-standard .NET resource files and ASP.NET Core localization infrastructure, ensuring maintainability, type safety, and performance. All user-facing strings are localized, from validation messages to error responses.
Architecture Overview¶
Localization Stack¶
HTTP Request
↓
Request Localization Middleware
├── Detects culture from Accept-Language header
├── Detects culture from query string (?culture=)
├── Detects culture from cookie
└── Falls back to default culture
↓
Sets Thread Culture
├── CultureInfo.CurrentCulture (formatting)
└── CultureInfo.CurrentUICulture (resource lookup)
↓
Resource Resolution
├── IStringLocalizer<T> (Dependency Injection)
├── ResourceManager (Strongly-typed resources)
└── .resx Resource Files
├── ValidationMessages.resx (default)
├── ValidationMessages.en-us.resx (English)
└── ValidationMessages.ru.resx (Russian)
↓
Localized Response
├── Content-Language header set
└── Localized strings in response
Localization Components¶
| Component | Purpose | Location |
|---|---|---|
| LocalizationExtensions | Service registration and middleware configuration | ApplicationModel |
| MicroserviceLocalizationOptions | Configuration options | Options |
| Resource Files (.resx) | String resources per culture | Resources folder |
| IStringLocalizer |
Dependency injection for string localization | ASP.NET Core |
| ResourceManagerStringLocalizerFactory | Factory for creating localizers | ASP.NET Core |
| RequestLocalizationMiddleware | Culture detection and setting | ASP.NET Core |
Configuration¶
Service Registration¶
AddMicroserviceLocalization:
// LocalizationExtensions.cs
internal static IServiceCollection AddMicroserviceLocalization(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
services.AddLocalization(options =>
{
options.ResourcesPath = OptionsExtensions.MicroserviceLocalizationOptions.ResourcesPath;
});
// Registers ResourceManagerStringLocalizerFactory to centralize and configure
// localization using .resx resource files across the application.
services.AddSingleton<IStringLocalizerFactory, ResourceManagerStringLocalizerFactory>();
services.Configure<RequestLocalizationOptions>(options =>
{
options.DefaultRequestCulture = new RequestCulture(
OptionsExtensions.MicroserviceLocalizationOptions.DefaultRequestCulture);
options.SupportedCultures = OptionsExtensions.MicroserviceLocalizationOptions
.SupportedCultures.Select(culture => new CultureInfo(culture)).ToList();
options.SupportedUICultures = OptionsExtensions.MicroserviceLocalizationOptions
.SupportedUICultures.Select(culture => new CultureInfo(culture)).ToList();
options.ApplyCurrentCultureToResponseHeaders =
OptionsExtensions.MicroserviceLocalizationOptions.ApplyCurrentCultureToResponseHeaders;
options.CultureInfoUseUserOverride =
OptionsExtensions.MicroserviceLocalizationOptions.CultureInfoUseUserOverride;
options.FallBackToParentCultures =
OptionsExtensions.MicroserviceLocalizationOptions.FallBackToParentCultures;
options.FallBackToParentUICultures =
OptionsExtensions.MicroserviceLocalizationOptions.FallBackToParentUICultures;
});
return services;
}
Registration in Startup:
Middleware Configuration¶
UseMicroserviceLocalization:
// LocalizationExtensions.cs
internal static IApplicationBuilder UseMicroserviceLocalization(this IApplicationBuilder application)
{
ArgumentNullException.ThrowIfNull(application);
application.UseRequestLocalization();
return application;
}
Middleware Order:
// MicroserviceRegistrationExtensions.cs
application.UseRouting();
application.UseMicroserviceRateLimiter();
application.UseMicroserviceLocalization(); // After routing, before endpoints
application.UseMicroserviceHeaderPropagation();
application.UseEndpoints(endpoints => { /* ... */ });
Configuration Options¶
MicroserviceLocalizationOptions:
{
"MicroserviceLocalization": {
"ResourcesPath": "Resources",
"DefaultRequestCulture": "en-US",
"CultureInfoUseUserOverride": true,
"FallBackToParentCultures": true,
"FallBackToParentUICultures": true,
"ApplyCurrentCultureToResponseHeaders": true,
"SupportedCultures": [
"en-US",
"ru-RU",
"fr-FR"
],
"SupportedUICultures": [
"en-US",
"ru-RU",
"fr-FR"
]
}
}
Configuration Properties:
| Property | Type | Default | Description |
|---|---|---|---|
ResourcesPath |
string |
Required | Relative path under application root where resource files are located |
DefaultRequestCulture |
string |
"en-US" |
Default culture when no culture can be determined |
CultureInfoUseUserOverride |
bool |
true |
Whether to use user-selected culture settings |
FallBackToParentCultures |
bool |
true |
Fallback to parent culture if specific culture not found |
FallBackToParentUICultures |
bool |
true |
Fallback to parent UI culture if specific culture not found |
ApplyCurrentCultureToResponseHeaders |
bool |
true |
Set Content-Language header in responses |
SupportedCultures |
List<string> |
["en-US"] |
List of supported cultures for formatting |
SupportedUICultures |
List<string> |
["en-US"] |
List of supported UI cultures for resource lookup |
Resource Files¶
Resource File Structure¶
Resource files are organized by culture:
Resources/
├── ValidationMessages.resx (Default/neutral)
├── ValidationMessages.en-us.resx (English - United States)
├── ValidationMessages.ru.resx (Russian)
└── ValidationMessages.fr-FR.resx (French - France)
Creating Resource Files¶
1. Default Resource File (ValidationMessages.resx):
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="ObjectIdRequired">
<value>Object id is required.</value>
</data>
<data name="MicroserviceAggregateRootAlreadyExists">
<value>MicroserviceAggregateRoot already exists.</value>
</data>
<data name="MicroserviceAggregateRootNotFound">
<value>MicroserviceAggregateRoot not found.</value>
</data>
</root>
2. Culture-Specific Resource File (ValidationMessages.ru.resx):
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="ObjectIdRequired">
<value>Требуется идентификатор объекта.</value>
</data>
<data name="MicroserviceAggregateRootAlreadyExists">
<value>MicroserviceAggregateRoot уже существует.</value>
</data>
<data name="MicroserviceAggregateRootNotFound">
<value>MicroserviceAggregateRoot не найден.</value>
</data>
</root>
Strongly-Typed Resource Classes¶
Resource files automatically generate strongly-typed classes:
// ValidationMessages.Designer.cs (auto-generated)
namespace ConnectSoft.MicroserviceTemplate.Resources
{
public class ValidationMessages
{
private static ResourceManager resourceMan;
private static CultureInfo resourceCulture;
public static ResourceManager ResourceManager
{
get
{
if (ReferenceEquals(resourceMan, null))
{
resourceMan = new ResourceManager(
"ConnectSoft.MicroserviceTemplate.Resources.ValidationMessages",
typeof(ValidationMessages).Assembly);
}
return resourceMan;
}
}
public static CultureInfo Culture
{
get { return resourceCulture; }
set { resourceCulture = value; }
}
public static string ObjectIdRequired
{
get
{
return ResourceManager.GetString("ObjectIdRequired", resourceCulture);
}
}
public static string MicroserviceAggregateRootAlreadyExists
{
get
{
return ResourceManager.GetString(
"MicroserviceAggregateRootAlreadyExists",
resourceCulture);
}
}
public static string MicroserviceAggregateRootNotFound
{
get
{
return ResourceManager.GetString(
"MicroserviceAggregateRootNotFound",
resourceCulture);
}
}
}
}
Usage:
// Direct access (uses current UI culture)
string message = ValidationMessages.ObjectIdRequired;
// Explicit culture
ValidationMessages.Culture = new CultureInfo("ru-RU");
string russianMessage = ValidationMessages.ObjectIdRequired;
Resource File Best Practices¶
- Always Include All Keys in Default Resource
- Default resource file should contain all keys
-
Culture-specific files only override translations
-
Keep Keys Consistent
- All culture-specific files should have the same keys
-
Missing keys fall back to parent/default culture
-
Use Descriptive Key Names
-
Include Comments in Resource Files
Using IStringLocalizer¶
Dependency Injection¶
IStringLocalizer
public class MyService
{
private readonly IStringLocalizer<MyService> localizer;
public MyService(IStringLocalizer<MyService> localizer)
{
this.localizer = localizer;
}
public string GetErrorMessage()
{
return this.localizer["ObjectIdRequired"];
}
}
Resource-Scoped Localizer¶
For resource-specific localizers, use the resource type:
public class ValidationService
{
private readonly IStringLocalizer<ValidationMessages> localizer;
public ValidationService(IStringLocalizer<ValidationMessages> localizer)
{
this.localizer = localizer;
}
public string GetValidationMessage(string key)
{
return this.localizer[key];
}
}
Localized Strings with Parameters¶
Use LocalizedString for formatted strings:
// Resource file
<data name="ItemNotFound">
<value>Item with ID '{0}' not found.</value>
</data>
// Code
var message = this.localizer["ItemNotFound", itemId];
// Returns: "Item with ID '123' not found." (en-US)
// Returns: "Элемент с ID '123' не найден." (ru-RU)
LocalizedString Properties¶
var localized = this.localizer["Key"];
// Properties
localized.Name; // "Key"
localized.Value; // Localized string value
localized.ResourceNotFound; // true if key not found
Getting All Strings¶
// Get all strings for current culture
var allStrings = this.localizer.GetAllStrings();
foreach (var localizedString in allStrings)
{
Console.WriteLine($"{localizedString.Name}: {localizedString.Value}");
}
// Get all strings including parent cultures
var allWithParents = this.localizer.GetAllStrings(includeParentCultures: true);
Request Culture Detection¶
Culture Providers¶
ASP.NET Core uses multiple culture providers in order:
- Query String Provider (
?culture=ru-RU) - Cookie Provider (
CookieRequestCultureProvider) - Accept-Language Header Provider (
AcceptLanguageHeaderRequestCultureProvider)
Accept-Language Header¶
HTTP Request:
The middleware:
1. Parses Accept-Language header
2. Selects first supported culture (ru-RU)
3. Falls back to parent culture (ru) if ru-RU not supported
4. Falls back to default culture (en-US) if no match
Query String Culture¶
URL:
The query string provider takes precedence over Accept-Language header.
Cookie Culture¶
Set Culture Cookie:
// Set culture cookie
Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("ru-RU")),
new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) });
Read Culture from Cookie:
The middleware automatically reads the culture cookie and sets the request culture.
Custom Culture Provider¶
Creating Custom Provider:
public class CustomCultureProvider : RequestCultureProvider
{
public override Task<ProviderCultureResult?> DetermineProviderCultureResult(
HttpContext httpContext)
{
// Custom logic to determine culture
var culture = httpContext.Request.Headers["X-Custom-Culture"].FirstOrDefault();
if (culture != null)
{
return Task.FromResult<ProviderCultureResult?>(
new ProviderCultureResult(culture));
}
return NullProviderCultureResult;
}
}
// Registration
services.Configure<RequestLocalizationOptions>(options =>
{
options.RequestCultureProviders.Insert(0, new CustomCultureProvider());
});
Culture Fallback¶
Parent Culture Fallback¶
Example:
- Requested:
fr-CA(French - Canada) - Supported:
fr-FR(French - France) - With
FallBackToParentCultures: true: Falls back tofr(French) - If
frnot supported: Falls back to default culture
Configuration:
{
"MicroserviceLocalization": {
"FallBackToParentCultures": true,
"FallBackToParentUICultures": true
}
}
Fallback Chain¶
Requested: fr-CA
↓
Check: fr-CA (not found)
↓
Check: fr (found) ← Uses parent culture
↓
If not found: en-US (default)
Validation Message Localization¶
FluentValidation Localization¶
Using Localized Messages:
public class CreateAggregateValidator : AbstractValidator<CreateInput>
{
public CreateAggregateValidator(IStringLocalizer<ValidationMessages> localizer)
{
RuleFor(x => x.ObjectId)
.NotEmpty()
.WithMessage(localizer["ObjectIdRequired"]);
}
}
Exception Message Localization¶
Localized Exception Messages:
public class AggregateService
{
private readonly IStringLocalizer<ValidationMessages> localizer;
public AggregateService(IStringLocalizer<ValidationMessages> localizer)
{
this.localizer = localizer;
}
public void ValidateObjectId(string objectId)
{
if (string.IsNullOrEmpty(objectId))
{
throw new ObjectIdRequiredException(
this.localizer["ObjectIdRequired"]);
}
}
}
gRPC Error Localization¶
Localized gRPC Errors:
// GrpcRichErrorInterceptor.cs
public class GrpcRichErrorInterceptor : Interceptor
{
private readonly IStringLocalizer<GrpcRichErrorInterceptor> localizer;
public GrpcRichErrorInterceptor(
IStringLocalizer<GrpcRichErrorInterceptor> localizer)
{
this.localizer = localizer;
}
private RpcException CreateRpcException(ServerCallContext context, Exception exception)
{
switch (exception)
{
case ObjectIdRequiredException:
additionalErrors = Any.Pack(
new BadRequest
{
FieldViolations =
{
new BadRequest.Types.FieldViolation
{
Field = "ObjectId",
Description = this.localizer["ObjectIdRequired"],
},
},
});
break;
}
}
}
Culture-Specific Formatting¶
Date Formatting¶
// Uses current culture for formatting
var date = DateTime.Now;
// en-US: "12/31/2023"
// ru-RU: "31.12.2023"
// fr-FR: "31/12/2023"
string formatted = date.ToString("d");
Number Formatting¶
// Uses current culture for formatting
var number = 1234.56;
// en-US: "1,234.56"
// ru-RU: "1 234,56"
// fr-FR: "1 234,56"
string formatted = number.ToString("N2");
Currency Formatting¶
// Uses current culture for currency
var amount = 1234.56m;
// en-US: "$1,234.56"
// ru-RU: "1 234,56 ₽"
// fr-FR: "1 234,56 €"
string formatted = amount.ToString("C");
Explicit Culture Formatting¶
// Format with specific culture
var date = DateTime.Now;
var culture = new CultureInfo("ru-RU");
string formatted = date.ToString("d", culture); // "31.12.2023"
Response Headers¶
Content-Language Header¶
When ApplyCurrentCultureToResponseHeaders is true, the middleware automatically sets the Content-Language header:
Configuration:
Testing¶
Unit Testing with Culture¶
Setting Culture in Tests:
[TestMethod]
public void LocalizedMessage_ShouldReturnRussian_WhenCultureIsRuRu()
{
// Arrange
var originalCulture = CultureInfo.CurrentUICulture;
try
{
CultureInfo.CurrentUICulture = new CultureInfo("ru-RU");
// Act
var message = ValidationMessages.ObjectIdRequired;
// Assert
Assert.AreEqual("Требуется идентификатор объекта.", message);
}
finally
{
CultureInfo.CurrentUICulture = originalCulture;
}
}
Helper Method:
private static void WithUiCulture(string cultureName, Action action)
{
var originalUi = CultureInfo.CurrentUICulture;
var original = CultureInfo.CurrentCulture;
try
{
var ci = cultureName is null
? CultureInfo.InvariantCulture
: new CultureInfo(cultureName);
CultureInfo.CurrentUICulture = ci;
CultureInfo.CurrentCulture = ci;
action();
}
finally
{
CultureInfo.CurrentUICulture = originalUi;
CultureInfo.CurrentCulture = original;
}
}
// Usage
[TestMethod]
public void TestWithRussianCulture()
{
WithUiCulture("ru-RU", () =>
{
Assert.AreEqual(
"Требуется идентификатор объекта.",
ValidationMessages.ObjectIdRequired);
});
}
Testing IStringLocalizer¶
Mock Localizer:
[TestMethod]
public void Service_ShouldUseLocalizedMessage()
{
// Arrange
var mockLocalizer = new Mock<IStringLocalizer<MyService>>();
mockLocalizer
.Setup(l => l["Key"])
.Returns(new LocalizedString("Key", "Localized Value"));
var service = new MyService(mockLocalizer.Object);
// Act
var result = service.GetMessage();
// Assert
Assert.AreEqual("Localized Value", result);
}
Integration Testing¶
Testing with Different Cultures:
[TestMethod]
public async Task Api_ShouldReturnLocalizedError_WhenCultureIsRuRu()
{
// Arrange
var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("Accept-Language", "ru-RU");
// Act
var response = await client.PostAsync("/api/aggregates", content);
// Assert
var error = await response.Content.ReadAsStringAsync();
Assert.IsTrue(error.Contains("Требуется идентификатор объекта."));
}
Best Practices¶
Do's¶
-
Use Resource Files for All User-Facing Strings
-
Keep Resource Keys Consistent Across Cultures
-
Use Descriptive Resource Key Names
-
Test All Cultures
-
Use IStringLocalizer for Dependency Injection
Don'ts¶
-
Don't Hard-Code Strings
-
Don't Mix Localized and Non-Localized Strings
-
Don't Forget to Add Keys to All Cultures
-
Don't Use Culture-Specific Formatting Without Consideration
-
Don't Ignore Fallback Behavior
Troubleshooting¶
Issue: Localized Strings Not Appearing¶
Symptoms: Always returns English/default strings.
Solutions:
1. Verify culture is set correctly: CultureInfo.CurrentUICulture
2. Check resource file naming: ValidationMessages.{culture}.resx
3. Verify resource files are included in build output
4. Check ResourcesPath configuration matches actual path
5. Verify satellite assemblies are deployed (culture-specific DLLs)
Issue: Missing Resource Keys¶
Symptoms: Key not found, returns key name.
Solutions: 1. Verify key exists in default resource file 2. Check key name matches exactly (case-sensitive) 3. Rebuild project to regenerate .Designer.cs files 4. Verify resource files are marked as "Embedded Resource"
Issue: Culture Not Detected¶
Symptoms: Always uses default culture.
Solutions:
1. Check Accept-Language header is sent
2. Verify culture is in SupportedCultures list
3. Check middleware order (must be after routing)
4. Verify UseRequestLocalization() is called
5. Check query string format: ?culture=ru-RU
Issue: Resource Files Not Found¶
Symptoms: ResourceNotFoundException or missing translations.
Solutions:
1. Verify resource files are in correct folder (Resources/)
2. Check ResourcesPath configuration
3. Ensure resource files are marked as "Embedded Resource" in project
4. Verify satellite assemblies are deployed (check output folder)
5. Rebuild project to ensure satellite assemblies are generated
Summary¶
Localization in the ConnectSoft Microservice Template provides:
- ✅ Multi-Language Support: Support for multiple languages and cultures
- ✅ Resource Files: .NET resource files (.resx) for maintainable localization
- ✅ Culture Detection: Automatic detection from HTTP headers and request context
- ✅ Strongly-Typed Resources: Type-safe resource access via generated classes
- ✅ IStringLocalizer Integration: Flexible dependency injection-based localization
- ✅ Validation Localization: Localized validation and error messages
- ✅ Culture-Specific Formatting: Automatic formatting of dates, numbers, currencies
- ✅ Fallback Support: Automatic fallback to parent or default cultures
- ✅ Response Headers: Automatic
Content-Languageheader setting - ✅ Testing Support: Comprehensive testing patterns and helpers
By following these patterns, teams can:
- Support Global Audiences: Serve users in multiple languages and regions
- Maintain Consistency: Keep localized strings organized and versioned
- Ensure Quality: Test all cultures to verify translations
- Improve UX: Provide culturally appropriate content and formatting
- Simplify Maintenance: Use strongly-typed resources and dependency injection
- Handle Edge Cases: Graceful fallback when translations are missing
Localization is a critical component for microservices serving international audiences, enabling culturally appropriate content, proper formatting, and improved user experience across diverse regions and languages.