Skip to content

06 - i18n

i18n

  • Globalization (G11N): The process of making an application support different languages and regions.
  • Localization (L10N): The process of customizing an application for a given language and region.
  • Internationalization (I18N): Describes both globalization and localization.
  • Culture: It is a language and, optionally, a region.
  • Locale: A locale is the same as a culture.
  • Neutral culture: A culture that has a specified language, but not a region. (e.g. "en", "es")
  • Specific culture: A culture that has a specified language and region. (e.g. "en-US", "en-GB", "es-CL")

https://msdn.microsoft.com/en-us/goglobal/bb896001.aspx

Cultures

Languages

i18n example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int value = 5600;
 
Thread.CurrentThread.CurrentCulture = 
    new System.Globalization.CultureInfo("es-CL");
Console.WriteLine(DateTime.Now.ToShortDateString());
Console.WriteLine(value.ToString("c"));
 
Thread.CurrentThread.CurrentCulture = 
    new System.Globalization.CultureInfo("es-MX");
Console.WriteLine(DateTime.Now.ToShortDateString());
Console.WriteLine(value.ToString("c"));
1
2
3
4
5
6
// Output
26-07-2011 // Date in es-CL, Spanish (Chile)
$5.600,00 // Currency in es-CL, Spanish (Chile)
 
26/07/2011 // Date in es-MX, Spanish (Mexico)
$5,600.00 // Currency in es-MX, Spanish (Mexico)

Culture and UICulture

ASP.NET Core keeps track in each thread:

  • Culture
    determines the results of culture-dependent functions, such as the date, number, and currency formatting, and so on
  • UICulture
    determines which resources are loaded for the page

Culture switching 1

Add needed services and conf (.Build()).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var supportedCultures = builder.Configuration
    .GetSection("SupportedCultures")
    .GetChildren()
    .Select(x => new CultureInfo(x.Value))
    .ToArray();

builder.Services.Configure<RequestLocalizationOptions>(options =>
{
    // datetime and currency support
    options.SupportedCultures = supportedCultures;
    // UI translated strings
    options.SupportedUICultures = supportedCultures;
    // if nothing is found, use this
    options.DefaultRequestCulture =
        new RequestCulture(builder.Configuration["DefaultCulture"], builder.Configuration["DefaultCulture"]);
    options.SetDefaultCulture(builder.Configuration["DefaultCulture"]);

    options.RequestCultureProviders = new List<IRequestCultureProvider>
    {
        // Order is important, its in which order they will be evaluated
        // add support for ?culture=ru-RU
        new QueryStringRequestCultureProvider(),
        new CookieRequestCultureProvider()
    };
});

Culture switching 2

Add middleware into pipeline

1
2
app.UseRouting();
app.UseRequestLocalization(options: app.Services.GetService<IOptions<RequestLocalizationOptions>>()?.Value!);

Culture switching 3

Define settings in appsettings.

1
2
3
4
5
6
7
8
  "DefaultCulture": "et-EE",
  "SupportedCultures": [
    "en-GB",
    "et-EE",
    "ru-RU",
    "lv-LV",
    "lt-LT"
  ],

Resources

Add new class library for language resources.
Add resource file - Resources (.resx).
Use subdirectories, mimicking your solution projects.

When new resx file is added, change .csproj file manually (ResXFileCodeGenerator => PublicResXFileCodeGenerator).

1
2
3
4
5
6
    <ItemGroup>
      <EmbeddedResource Update="App.Domain\Foo.resx">
        <Generator>PublicResXFileCodeGenerator</Generator>
        <LastGenOutput>Foo.Designer.cs</LastGenOutput>
      </EmbeddedResource>
    </ItemGroup>

Add needed cultures (first + sign), and needed keys and translations (second +).

Rider

Using resource based translations

  • To get MVC to automatically display translated field names – use [Display] attribute
1
2
3
4
5
6
7
8
[Display(
    Name = nameof(StringValue) + "Name", 
    ShortName = nameof(StringValue) + "ShortName",
    Prompt = nameof(StringValue) + "Prompt", 
    Description = nameof(StringValue) + "Description",
    ResourceType = typeof(Resources.Common)
    )]
public string StringValue { get; set; }
  • Mvc uses Name (label value) and Prompt (for input placeholder) fields. WPF (desktop) uses also the other atrributes. It is also possible to read these attribute values in code.

DB Content translation

Several ways how to handle


  • 2 extra tables
    • one for all the possible translations
    • second to hold default value and id (1:m relationship to translations)


Keep strings in json dictionary.

DB Content translation - json

Extend dictionary to handle cultures and string values.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
namespace App.Domain;

public class LangStr : Dictionary<string, string>
{
    private const string DefaultCulture = "en";

    public new string this[string key]
    {
        get { return base[key]; }
        set { base[key] = value; }
    }

    public LangStr(string value) : this(value, Thread.CurrentThread.CurrentUICulture.Name)
    {
    }


    public LangStr()
    {
    }

    public LangStr(string value, string culture)
    {
        this[culture] = value;
    }

    public void SetTranslation(string value)
    {
        this[Thread.CurrentThread.CurrentUICulture.Name] = value;
    }

    public string? Translate(string? culture = null)
    {
        if (this.Count == 0) return null;

        culture = culture?.Trim() ?? Thread.CurrentThread.CurrentUICulture.Name;

        /*
         in database - en, en-GB
         in query - en, en-GB, en-US
         */

        // do we have exact match - en-GB == en-GB
        if (this.ContainsKey(culture))
        {
            return this[culture];
        }

        // do we have match without the region en-US.StartsWith(en)
        var key = this.Keys.FirstOrDefault(t => culture.StartsWith(t));
        if (key != null)
        {
            return this[key];
        }

        // try to find the default culture
        key = this.Keys.FirstOrDefault(t => culture.StartsWith(DefaultCulture));
        if (key != null)
        {
            return this[key];
        }

        // just return the first in list or null
        return null;
    }

    public override string ToString()
    {
        return Translate() ?? "????";
    }

    public static implicit operator string(LangStr? l) => l?.ToString() ?? "null";
    public static implicit operator LangStr(string s) => new LangStr(s);
}

Use it in db, EF provider will store dictionary as json object and do automatic serialization/deserialization.

1
2
3
4
5
6
public class Foo: DomainEntityId
{
    [Display(ResourceType = typeof(App.Resources.App.Domain.Foo), Name = nameof(SomeStr))]
    [Column(TypeName = "jsonb")]
    public LangStr SomeStr { get; set; } = new();
}
  • Use ToString() method to get translation using current culture.
  • When updating/adding new translations, take care of loading old values from db first.