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

Yiğit Tanyel
4 min readDec 27, 2023

--

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:

  1. I will define a LanguageChangeMiddleware in the Api layer. This middleware will dynamically detect the culture variable that we send as a query parameter.
  2. 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.
  3. 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.
  4. 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.
  5. 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

--

--

Responses (2)

Write a response