Localization and Languages in .NET

Creating applications that support multiple languages in .NET is very simple, and it's a very useful feature when we have clients from various countries, whether they are web clients or API clients.

 

In today's post, we are going to look at both a multilingual application, which will provide a response to the client in different languages, in this case an API.

 

 

1 - Best way to configure multiple languages in .NET

The best way to create applications or web pages in different languages is by using the Resource files that Microsoft suggests us to use, which have the extension .resx

resource files

As we can see, we indicate a name, this file will be our default file, what does this mean? It means that if we try to access a language that does not exist, this file will be chosen. 

 

The content of the file is an XML but if we open it with Visual Studio we will see that it opens as a file with three columns, where they are:

  • Name: Where we indicate the name or key of the translation, which must be unique
  • Value: Where we define the value for that key.
  • Comment: This field is optional and helps us understand what this field does, it is very useful for example if we use SaaS-type software for translations in languages we don't know. 

localization file .net

Which translates into the following .xml file (obviously I skipped all the autogenerated code)

<data name="IdentityNotFound" xml:space="preserve">
    <value>Usuario no encontrado</value>
</data>
<data name="PersonalProfileNotFound" xml:space="preserve">
    <value>Perfil personal no encontrado</value>
</data>

Once we have our language file, let's add a second language, for example, English.

To do this, we must create another file with the same name, but when we indicate the extension we must specify the language, for this, we indicate a dot and the two letters of the language code like this: filename.{languageCode}.resx

 

For our English example, the code is en while Spanish is es, but in this case, we don't need the Spanish file since we already have our default file.

Note: it is in Spanish by default if our machine is set to Spanish.

 

So we create a file called TraduccionErrores.en.resx and we must include all the translation elements we created in the first file.

 

If for example you want to filter not only by language but also by region or country, you can do it. For example, Spanish from Spain is `es-ES` while Argentinian Spanish is `es-AR`, so we could filter for those countries if we create files called: 

TraduccionErrores.es-ES.resx and TraduccionErrores.es-AR.resx

 

 

1.1 - Access language resources in .NET

Once we have our resource files, we want to access them. 

If you noticed, when we created the .resx file, Visual Studio also created another file called TraduccionErrores.Designer.cs.

This file contains a "link" through static methods to those translations. 

Note: the file is created and updated automatically as we edit the resource file. 

 

Therefore, to print the error we just have to call that error through its static method. 

Example of printing the value:

Console.WriteLine(TraduccionErrores.PersonalProfileNotFound);

 

Result in the console:

ejemplo traducción .net

 

Still, this way of getting the translations is not the best.

This is because it has two limitations:

  • You can only access one language.
  • You cannot access translations dynamically. Although this option has other limitations

 

 

2 - How languages work in .NET

.NET provides us with a rather particular way of choosing languages, which is through the host, and this information is available in Thread.CurrentThread.CurrentUICulture and Thread.CurrentThread.CurrentCulture and it can be modified simply by assigning it a value using the CultureInfo type.

Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en-IE");
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("en-IE");

If we change the "culture" and run the same code as before, we will see how the result changes to English:

multiple languages .net

But this is not the only way to change the language—we will see another way later.

 

 

2.1 - Difference between CurrentUICulture and CurrentCulture

I mentioned two ways to define the culture of our application, which are CurrentCulture and CurrentUICulture, but what is the difference?

 

When we refer to CurrentCulture, we are indicating the system's "user locale", which controls the number or date format. For example, in the United States the date format is month-day-year while in Spain it is day-month-year.

 

When we set CurrentUICulture we are referring to the interface language, or essentially the translation. 

 

 

3 - Multiple languages in .NET

First of all, we must consider the use case, as we might want access to multiple languages in our code.

 

The simplest example I can think of is, imagine you work for a big multinational company, and that company sends you for a few months to work in a country whose language you do not master, such as Norway.

 

In your new office, since they work with clients from that country, all the documents are in Norwegian, with Norwegian date formats, etc.

But you, since you do not know the language at all, change the language to English or Spanish.

 

And now suppose we have access to an API where we query data. If there is an error in the API, you want the user making that request to be able to understand it, while behind the scenes you store the error message in the system's native language (Norwegian). 

 

 

To accomplish this, we need to change the language just before querying that translation, and set it back to the original one right after.

 

Note: it is very common to set the culture to the selected client company language by default for each request, so you can have companies from multiple countries each with their country-specific formats. 

 

 

3.1- Creating the culture scope. 

To do this, we are going to use a scope.

 

For those who don't know, a scope is a block of code, which can be a single line or half the program. 

 

The logic of this scope is simple: assign the language value sent by the user and when the scope ends, restore the original language. 

For this, we will implement IDisposable (link to IDisposable explanation).  

Which looks as follows:

public class CultureScope : IDisposable
{
    private readonly CultureInfo _originalCulture;
    private readonly CultureInfo _originalUICulture;

    public CultureScope(CultureInfo culture)
    {
        _originalCulture = Thread.CurrentThread.CurrentCulture;
        _originalUICulture = Thread.CurrentThread.CurrentUICulture;

        Thread.CurrentThread.CurrentCulture = culture;
        Thread.CurrentThread.CurrentUICulture = culture;
    }

    public void Dispose()
    {
        Thread.CurrentThread.CurrentCulture = _originalCulture;
        Thread.CurrentThread.CurrentUICulture = _originalUICulture;
    }
} 

 

This way, we can run multiple languages in the same block of code, as we can see in the example:

using (new CultureScope(CultureInfo.GetCultureInfo("en-IE")))
{
    Console.WriteLine($"This translation is in English: {TraduccionErrores.PersonalProfileNotFound}");
}

using (new CultureScope(CultureInfo.GetCultureInfo("es-ES")))
{
    Console.WriteLine($"This translation is in Spanish: {TraduccionErrores.PersonalProfileNotFound}");
}

 

And as we can see, the result contains both languages:

mas de un idioma .net

 

 

4 - Library to manage multiple languages in .NET

From here, it is not entirely necessary, but I do find it useful, although it's true that not all cases need logic like what I'm going to explain:

 

A use case for this library is if you need the same text in multiple languages.

 

As I mentioned in point 1.1, this is not the only way to use multiple languages in .NET, right now we are using an auto-generated class, which I personally hate. 

So the first step is to remove that class and create a new empty one, but with the same name

public class TraduccionErrores
{
}

 

In addition, with the previous version we were not translating only the text we wanted, but everything inside the scope, which in this case is the message, but the message could include a date, do we want that date to use the selected language or the default?  

I want only the text to be translated, and no, the solution is not to update our CultureScope to use only CurrentUICulture, the solution is to create an intermediate class that acts as a "selector" for the translation. 

 

As we see, our code fails

 

For this, we will create a class that receives a generic type along with the type IStringLocalizer provided by Microsoft in its Microsoft.Extensions.Localization package.

 

We must assign the value of our IStringLocalizer to the file we just created, and we do this in the static constructor of our class.

public class LocalizationUtils<TEntity>
{

    private static readonly IStringLocalizer _localizer
    static LocalizationUtils()
    {
        var options = Options.Create(new LocalizationOptions());
        var factory = new ResourceManagerStringLocalizerFactory(options, NullLoggerFactory.Instance);
        var type = typeof(TEntity);

        _localizer = factory.Create(type);
    }
}

 

And to read from it, we just have to indicate the field we want to read through an indexer -https://www.netmentor.es/Entrada/indexer-csharp -

public static string GetValue(string field)
{
    return _localizer[field];
}

But here we are not indicating the language anywhere, so we create a method that receives that language and applies it only to our translation read. 

This would be the complete code:

public class LocalizationUtils<TEntity>
{

    private static readonly IStringLocalizer _localizer;

    static LocalizationUtils()
    {
        var options = Options.Create(new LocalizationOptions());
        var factory = new ResourceManagerStringLocalizerFactory(options, NullLoggerFactory.Instance);
        var type = typeof(TEntity);

        _localizer = factory.Create(type);
    }


    public static string GetValue(string field)
    {
        return _localizer[field];
    }

    public static string GetValue(string field, CultureInfo cultureinfo)
    {
        using (new CultureScope(cultureinfo))
        {
            return GetValue(field);
        }
    }
}

 

This library has a "problem" and that is, since it is generic, we must know which code or key of the translation to access it. 

 

And the way to access the value is through the extension method .GetValue that we just created.

 

{
    var traduccion = LocalizationUtils<TraduccionErrores>.GetValue("PersonalProfileNotFound", CultureInfo.GetCultureInfo("en-IE"));
    Console.WriteLine($"This translation is in English: {traduccion}");
}

{
    var traduccion = LocalizationUtils<TraduccionErrores>.GetValue("PersonalProfileNotFound", CultureInfo.GetCultureInfo("sp-ES"));
    Console.WriteLine($"This translation is in Spanish: {traduccion}");
}

///Result:
This translation is in English: Personal profile not found
This translation is in Spanish: Perfil personal no encontrado

 

Using this library we can simulate the use of the default library, we just have to modify our TraduccionErrores class, which is empty, to read the fields, and we have several options, read a single language by passing the language in the constructor and then creating properties in the class

public class TraduccionErrores
{
    private readonly CultureInfo _culture;
    public TraduccionErrores(CultureInfo culture)
    {
        _culture = culture;
    }

    public string PersonalProfile => LocalizationUtils<TraduccionErrores>.GetValue("PersonalProfileNotFound", _culture);
    public string IdentityNotFound => LocalizationUtils<TraduccionErrores>.GetValue("IdentityNotFound", _culture);
}

and this is how to access it:

var traducciones = new TraduccionErrores(CultureInfo.GetCultureInfo("en-IE"));
Console.WriteLine($"This translation is in English: {traducciones.PersonalProfile}");

 

Or we can also load all languages in the file and return them all, although for this option we must know the languages in advance.

public class TraduccionErrores
{
    public static string PersonalProfileEn => LocalizationUtils<TraduccionErrores>.GetValue("PersonalProfileNotFound", CultureInfo.GetCultureInfo("en-IE"));
    public static string PersonalProfileEs => LocalizationUtils<TraduccionErrores>.GetValue("PersonalProfileNotFound", CultureInfo.GetCultureInfo("es-ES"));
    public static string IdentityNotFoundEn => LocalizationUtils<TraduccionErrores>.GetValue("IdentityNotFoundEn", CultureInfo.GetCultureInfo("en-IE"));
    public static string IdentityNotFoundEs => LocalizationUtils<TraduccionErrores>.GetValue("IdentityNotFoundEn", CultureInfo.GetCultureInfo("es-ES"));
}

 

This library is very useful when we're creating a library that is going to be used by other libraries. For example, we receive an error code, and this one translates it automatically or adds it to the Json response.

In fact, we will see how to create a JsonConverter that shows the use of this library

 

 

5 - How to detect the user's language 

Finally, when working with web api we must know how to detect the user's language. In normal circumstances, this will come in the header. 

To access this resource very simply, we'll use the IHttpContextAccessor interface that comes in Microsoft.AspNetCore.Http.Abstractions.

Don't forget that you are injecting a service, which means you need to add it to your dependency container.

Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>()

And once we have access to the interface, we create an extension method that allows us to get the value of the Accept-Language header.

public static CultureInfo GetCultureInfo(this IHeaderDictionary header)
{
    using (new CultureScope(new CultureInfo("en")))
    {
        var languages = new List<(string, decimal)>();
        string acceptedLanguage = header["Accept-Language"];
        if (acceptedLanguage == null || acceptedLanguage.Length == 0)
        {
            return new CultureInfo("es");
        }
        string[] acceptedLanguages = acceptedLanguage.Split(',');
        foreach (string accLang in acceptedLanguages)
        {
            var languageDetails = accLang.Split(';');
            if (languageDetails.Length == 1)
            {
                languages.Add((languageDetails[0], 1));
            }
            else
            {
                languages.Add((languageDetails[0], Convert.ToDecimal(languageDetails[1].Replace("q=", ""))));
            }
        }
        string languageToSet = languages.OrderByDescending(a => a.Item2).First().Item1;
        return new CultureInfo(languageToSet);
    }
}

As a note, I've set it so if the header does not exist, it will choose "es" as the default language. 

Now you only need to instantiate it in the constructor of your service

public class PersonalProfile
{
    private readonly IGetPersonalProfileDependencies _dependencies;
    private readonly IDataProtector _protector;
    private readonly TraduccionErrores _traduccionErrores;

    public PersonalProfile(IGetPersonalProfileDependencies dependencies, IDataProtectionProvider provider, 
        IHttpContextAccessor httpcontextAccessor)
    {
        _dependencies = dependencies;
        _protector = provider.CreateProtector("PersonalProfile.Protector");
        _traduccionErrores = new TraduccionErrores(httpcontextAccessor.HttpContext.Request.Headers.GetCultureInfo());
    }

    /*More code*/
}

 

And you will be able to access the values through the variable _traduccionErrores.{property}

It should be noted that you can also directly inject your translation file into the dependency container.

 

Finally, when we make the call in Postman, we can see the translation, both when using English and Spanish:

ejemplo multiples idiomas c#

 

We can see the same endpoint and the same result, with the only difference being one language or the other. 

 

 

Conclusion

In this post we have seen how to configure a web or application to support multiple languages using .NET.

 

In the business environment, it is very likely to be necessary, since many companies operate in multiple countries and not all countries speak the same language.

 

In a personal environment, having a multilingual website is good for SEO, although if you create a website in multiple languages you also need to translate the content.

This post was translated from Spanish. You can see the original one here.
If there is any problem you can add a comment bellow or contact me in the website's contact form

Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2025 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café