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
i18n example
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"));
// 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()
).
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
app.UseRouting();
app.UseRequestLocalization(options: app.Services.GetService<IOptions<RequestLocalizationOptions>>()?.Value!);
Culture switching 3
Define settings in appsettings.
"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).
<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 +
).
Using resource based translations
- To get MVC to automatically display translated field names – use
[Display]
attribute
[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.
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.
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.