Breaking Language Barriers: Practicing Localization in .NET Core Web API

Localization in .NET Core refers to the process of adapting an application to meet the language and cultural expectations of a specific region or audience. Instead of having a one-size-fits-all approach, localization allows you to tailor your application for different languages, regions, and user preferences. Here’s why it’s crucial and the key advantages:
User Experience Enhancement:
- Why: Users prefer applications in their native language.
- Advantage: Improved user satisfaction and engagement.
Cultural Sensitivity:
- Why: Different cultures have unique conventions.
- Advantage: Avoids cultural misunderstandings, making the app more relatable.
Global Market Reach:
- Why: Targets users worldwide.
- Advantage: Increases the app’s marketability and user base.
Legal and Regulatory Compliance:
- Why: Some regions have legal requirements for language use.
- Advantage: Ensures compliance with local regulations.
Customization for Regional Preferences:
- Why: Date formats, number systems, and symbols vary globally.
- Advantage: Adapts to regional preferences, enhancing user interaction.
In summary, localization is essential for creating a user-friendly, culturally sensitive, and globally accessible application. It improves user satisfaction, expands your market reach, and ensures compliance with language-related regulations, making your application more adaptable and appealing on a global scale.
I think you now understand my style. I prefer concise explanations and comprehend things through writing. So, let’s code together:)
Let’s consider a .NET Core project implementing the Clean Architecture with layers such as Api, Application, Core, Domain, and Repository. Here are the steps I will take to implement localization:
- I will define a LanguageChangeMiddleware in the Api layer. This middleware will dynamically detect the culture variable that we send as a query parameter.
- I will create a LocalizationService where the Translate method will retrieve the corresponding key-value pair from the resource file. I defined this in Core layer because i need to reach it another layers.
- I will define a base Exception, and I will derive my custom exceptions from it. I defined this in Core layer too because of the same reason as above.
- To avoid creating dependencies every time I define an exception for LocalizationService, I will introduce a factory class. Using the singleton pattern, I will continue by obtaining the LocalizationService.
- I will utilize this in my custom exceptions. My custom exceptions in application layer and domain layer.
Step 1
public class LanguageChangeMiddleware
{
private readonly RequestDelegate _next;
public LanguageChangeMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var culture = context.Request.Query["culture"].ToString();
if (!string.IsNullOrEmpty(culture))
{
var cookieOptions = new CookieOptions
{
Expires = DateTimeOffset.UtcNow.AddYears(1),
IsEssential = true,
HttpOnly = false
};
context.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
cookieOptions
);
}
await _next(context);
}
}
This middleware intercepts incoming requests, extracts the "culture" parameter from the query string, and sets it as a cookie for language preference. If the "culture" parameter is present, it creates a cookie with a one-year expiration, marking it as essential and non-HTTP-only. The middleware then continues to process the request.
Step 2
public class LocalizationService
{
private readonly IStringLocalizer _localizer;
public LocalizationService(IStringLocalizerFactory factory)
{
var type = typeof(ApplicationResource);
var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
_localizer = factory.Create(nameof(ApplicationResource), assemblyName.Name);
}
public string Translate(string key, params object[] parameters)
{
var translation = string.Format(_localizer[key], parameters);
return translation ?? string.Format(key, parameters);
}
}
In this code, we determine the location of ApplicationResource
using reflection and perform the process of finding values associated with the keys present in it.
Step 3
public class YtException : Exception
{
protected readonly LocalizationService _localizationService;
public YtException(string key, params object[] parameters)
: base(GetFormattedMessage(key, parameters))
{
_localizationService = LocalizationServiceFactory.GetLocalizationServiceInstance();
}
private static string GetFormattedMessage(string key, params object[] parameters)
{
var localizationService = LocalizationServiceFactory.GetLocalizationServiceInstance();
return localizationService.Translate(key, parameters);
}
}
This code defines a class YtException
that inherits from Exception
. It takes a key
and optional parameters, and its constructor utilizes a _localizationService
to set the exception message using the translated value of the key obtained through the LocalizationService
. The GetFormattedMessage
method performs the translation using the factory method to get an instance of the LocalizationService
.
Step 4
public class LocalizationServiceFactory
{
private static readonly Lazy<LocalizationService> _localizationServiceInstance =
new Lazy<LocalizationService>(() =>
{
var factory = new ResourceManagerStringLocalizerFactory(
new OptionsWrapper<LocalizationOptions>(new LocalizationOptions()),
new LoggerFactory()
);
return new LocalizationService(factory);
});
public static LocalizationService GetLocalizationServiceInstance()
{
return _localizationServiceInstance.Value;
}
}
My intention is to avoid introducing dependencies in the location where I defined the exception class. Instead, instantiate it using the singleton pattern.
Step 5
public class AlreadyExistsException : YtException
{
public AlreadyExistsException(string entityName)
: base("AlreadyExistsErrorMessage", entityName)
{
}
}
Usage:
if (existingUser is not null)
{
throw new AlreadyExistsException(existingUser.Username);
}
Define as Dependency Injection
public static IServiceCollection AddLocalizationOperations(this IServiceCollection services)
{
services.AddSingleton<LocalizationService>();
services.AddLocalization(opt => { opt.ResourcesPath = "Resources"; });
services.Configure<RequestLocalizationOptions>(opt =>
{
var cultures = new List<CultureInfo>()
{
new CultureInfo("tr-TR"),
new CultureInfo("en-US")
};
opt.SupportedCultures = cultures;
opt.SupportedUICultures = cultures;
opt.RequestCultureProviders = new List<IRequestCultureProvider>()
{
new QueryStringRequestCultureProvider(),
new CookieRequestCultureProvider(),
new AcceptLanguageHeaderRequestCultureProvider()
};
});
return services;
}
Screen Shots:
When culture is TR:

When culture is EN:

That’s all I have to share on this topic. I hope this article has been informative and productive for everyone.
To get in touch with me and explore my open-source projects, you can reach me at:
LinkedIn : https://www.linkedin.com/in/yigittanyel/
GitHub: https://github.com/yigittanyel