Asp.Net
This article describes how to localize a asp application. There is a sample library on github.
To run Avalanche.Localization on Asp.Net, make sure the Application's .csproj is targeting platform .NET6 or higher.
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework><EnablePreviewFeatures>true</EnablePreviewFeatures>
</PropertyGroup>
Add package references.
<PropertyGroup>
<RestoreAdditionalProjectSources>https://avalanche.fi/Avalanche.Core/nupkg/index.json</RestoreAdditionalProjectSources>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalanche.Localization"/>
<PackageReference Include="Avalanche.Localization.Cldr"/>
<PackageReference Include="Avalanche.Localization.Extensions"/>
<PackageReference Include="Avalanche.Localization.Asp"/>
</ItemGroup>
Go to appsettings.json and add "Debug" level for "Avalanche" to get notified on localization issues.
"Logging": {
"LogLevel": {
"Default": "Warning",
"Avalanche": "Debug"
}
},
Get IServiceCollection reference from WebApplicationBuilder or run this in Startup.cs.
IServiceCollection services = builder.Services;
Remove possible previous .AddLocalization() service, ...
services.AddLocalization(options => { options.ResourcesPath = "Resources"; });
Add .AddAvalancheLocalizationService().
using Avalanche.Localization;
services
.AddAvalancheLocalizationService()
.AddAvalancheLocalizationEmbeddedResourceProviderDefault()
.AddAvalancheLocalizationResourceManagerProvider()
.AddAvalancheLocalizationAspSupport()
.AddAvalancheStringLocalizer()
.AddSingleton(typeof(ILocalizationFilePatterns), new LocalizationFilePatterns("Pages/{Key}", "Pages/{Culture}/{Key}"))
.AddAvalancheLocalizationEmbeddedResourceProvider("*/*.Pages.{Key}", "*/*.Pages.{Culture}.{Key}")
.Configure<Microsoft.Extensions.Localization.LocalizationOptions>(options =>
{
options.ResourcesPath = null!;
});
Choose localization file source. The .AddAvalancheLocalizationService() above adds application root (calls AddAvalancheLocalizationFileSystemApplicationRoot()). This can be replaced with a file provider.
services.Remove(LocalizationServiceDescriptors.Instance.LocalizationFileSystemApplicationRoot);
services.AddAvalancheLocalizationToUseFileProvider();
Possibly add IFileProvider.
services
.AddSingleton(typeof(IFileProvider), new PhysicalFileProvider(AppDomain.CurrentDomain.BaseDirectory));
You may or may not want to add IViewLocalizer and IHtmlLocalizer services from Microsoft.AspNetCore.Mvc.Localization.
using Microsoft.Extensions.DependencyInjection;
services.AddRazorPages().AddViewLocalization();
After 'app' construction, add workaround to a known issue: Flushes cache if Assembly is loaded later. Late loaded assembly may supply localization lines that have already been queried and cached. This issue will be fixed properly later.
AppDomain.CurrentDomain.AssemblyLoad += (object? sender, AssemblyLoadEventArgs args) =>
(app.Services.GetRequiredService<ILocalization>() as ICached)?.InvalidateCache(true);
More information in Microsoft.Extensions.DependencyInjection.
Note
AspNetCore support is experimental.
Supplying localization
Localization can be supplied in both .resx and .yaml files. .yaml can be dropped in the Resources/ folder either as embedded resource or as output copied resource.
Note that it may be good idea to have "null" as Resources path in Startup.cs to have easier keys and consistent across service interfaces.
services.Configure<Microsoft.Extensions.Localization.LocalizationOptions>(options =>
{
options.ResourcesPath = null;
});
Example: Resources/samples.asp.Pages.IndexModel.yaml (Mark: Copy to Output Directory: Copy if newver)
TemplateFormat: BraceNumeric
PluralRules: Unicode.CLDR41
English:
- Culture: "en"
Items:
- Key: samples.asp.Pages.IndexModel.Home
Text: "Home page"
- Key: samples.asp.Pages.IndexModel.Welcome
Text: "Welcome"
- Key: samples.asp.Pages.IndexModel.Content
Text: Learn about <a href="http://avalanche.fi/Avalanche.Core/Avalanche.Localization/docs/aspnet/index.html">Avalanche.Localization</a>.
Example: Resources/fi/samples.asp.Pages.IndexModel.yaml
TemplateFormat: BraceNumeric
PluralRules: Unicode.CLDR41
Finnish:
- Culture: "fi"
Items:
- Key: samples.asp.Pages.IndexModel.Home
Text: "Kotisivu"
- Key: samples.asp.Pages.IndexModel.Welcome
Text: "Tervetuloa"
- Key: samples.asp.Pages.IndexModel.Content
Text: Lue lisää <a href="http://avalanche.fi/Avalanche.Core/Avalanche.Localization/docs/aspnet/index.html">Avalanche.Localization:sta</a>.
Injecting to pages
Page can be designed to use either Microsoft's service interfaces or Avalanche's localization interfaces, or both.
If Microsoft's interface is used then @inject with IStringLocalizer<T>, IViewLocalizer and IHtmlLocalizer<T>. The localization files must be supply lines to key "Assembly[.Resources].Namespace.Key".
Open Pages/_ViewImports.cshtml and add:
// Ms.Localization
@using System.Globalization
@using Microsoft.AspNetCore.Localization
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Localization
@inject IViewLocalizer ViewLocalizer
@page
@inject IStringLocalizer<IndexModel> Localizer
@inject IHtmlLocalizer<IndexModel> HtmlLocalizer
@model IndexModel
@{
ViewData["Title"] = Localizer["Home"];
}
<div class="text-center">
<h1 class="display-4">@Localizer["Welcome"]</h1>
<p>@HtmlLocalizer["Content"]</p>
</div>
If page uses Avalanche's interface then @inject with ITextLocalizer, ITextLocalizer<T>, IFileLocalizer, IFileLocalizer<T>, ILocalization interfaces.
Open Pages/_ViewImports.cshtml and add:
// Av.Localization
@using System.Globalization
@using Avalanche.Localization
@inject ITextLocalizer TextLocalizer
@inject IFileLocalizer FileLocalizer
There is extension method .Html(args?) and .Localize(args?) (Avalanche.Localization.Asp.dll) that adapt to LocalizedHtmlString and LocalizedString respective.
@{
ViewData["Title"] = TextLocalizer["samples.asp.Pages.IndexModel.Home"];
}
<div class="text-center">
<h1 class="display-4">@Localizer["samples.asp.Pages.IndexModel.Welcome"]</h1>
<p>@TextLocalizer["samples.asp.Pages.IndexModel.Content"].LocalizeHtml()</p>
</div>
Or use the model specific localizer ITextLocalizer<T>:
@page
@inject ITextLocalizer<IndexModel> IndexLocalizer
@model IndexModel
@{
ViewData["Title"] = IndexLocalizer["Home"];
}
<div class="text-center">
<h1 class="display-4">@Localizer["Welcome"]</h1>
<p>@IndexLocalizer["Content"].LocalizeHtml()</p>
</div>
Warning
Asp <form> element is based on ResourceTypeAttribute on model seems to use direct ResourceManager reference.
Element localization doesn't go through DependencyInjection, IServiceProvider and IStringLocalizer stack and thus doesn't get localized. There is inconsistent design, some parts are dependency injectible, others not.
<form method="post" asp-page="Index">
<div class="form-group">
<label asp-for="Hero" class="control-label"></label>
<input asp-for="Hero" class="form-control"/>
<span asp-validation-for="Hero" class="text-danger"></span>
</div>
<button type="submit">@Localizer["Submit"]</button>
</form>
Culture Assigner
This section describes how to add a culture assigner selection dropdown.
Add IOptions<RequestLozalizationOptions> service.
services.Configure<RequestLocalizationOptions>(options =>
{
options.AddSupportedCultures("en", "fi", "sv");
options.AddSupportedUICultures("en", "fi", "sv");
options.SetDefaultCulture("en");
options.FallBackToParentCultures = true;
options.FallBackToParentUICultures = true;
});
Add .UseRequestLocalization to 'app'.
RequestLocalizationOptions localizationOptions = app.Services.GetRequiredService<IOptions<RequestLocalizationOptions>>().Value;
app.UseRequestLocalization(localizationOptions);
Add Models/CultureAssignmentModel.cs
namespace samples.asp.Models;
using System.Globalization;
/// <summary>Culture assignment model</summary>
public record CultureAssignmentModel(CultureInfo CurrentUICulture, IList<CultureInfo> SupportedCultures);
Add Pages/Components/CultureAssignment/default.cshtml.
@using System.Globalization
@model samples.asp.Models.CultureAssignmentModel
<form id="culture-assignment">
<select name="culture" onchange="this.parentElement.submit();">
@foreach (CultureInfo culture in Model.SupportedCultures)
{
<option value="@culture.Name" selected="@(Model!.CurrentUICulture.Name == culture.Name)">@culture.DisplayName</option>
}
</select>
</form>
Add Pages/ViewComponents/CultureAssignmentViewComponent.cs.
namespace samples.asp.ViewComponents;
using System.Globalization;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using samples.asp.Models;
/// <summary></summary>
public class CultureAssignmentViewComponent : ViewComponent
{
/// <summary>Options</summary>
IOptions<RequestLocalizationOptions> localizationOptions;
/// <summary>Create component</summary>
public CultureAssignmentViewComponent(IOptions<RequestLocalizationOptions> localizationOptions)
{
// Assert not null
ArgumentNullException.ThrowIfNull(localizationOptions);
this.localizationOptions = localizationOptions;
}
/// <summary>Assign culture</summary>
public IViewComponentResult Invoke()
{
// Get culture feature with assigned culture
IRequestCultureFeature cultureFeature = HttpContext.Features.Get<IRequestCultureFeature>()!;
// Create model with culture assignment record
CultureAssignmentModel model = new CultureAssignmentModel(
CurrentUICulture: cultureFeature.RequestCulture.UICulture,
SupportedCultures: localizationOptions.Value.SupportedUICultures?.ToArray() ?? Array.Empty<CultureInfo>()
);
// Return partial view
return View(model);
}
}
Add @addTagHelper *, samples.asp to Pages/_ViewImports.cshtml.
@using samples.asp
@using samples.asp.Models
@namespace samples.asp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, samples.asp
Add <vc:culture-assignment/> to Pages/Shared/_Layout.cshtml.
<div class="container">
<a class="navbar-brand" asp-area="" asp-page="/Index">samples.asp</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
</li>
</ul>
</div>
<vc:culture-assignment/>
</div>
Diagnostics
If localization issues require diagnosis, the reference for localization can be acquired in Program.cs.
IHostBuilder hostBuilder = CreateHostBuilder(args);
IHost host = hostBuilder.Build();
ILocalization localization = (ILocalization)host.Services.GetService(typeof(ILocalization));
Following snippet in Program.cs prints accessible localization lines.
var app = builder.Build();
// Localization diagnostics
{
// Get localization context
ILocalization localization = app.Services.GetService<ILocalization>()!;
// Get logger
ILogger logger = app.Services.GetService<ILogger<Program>>()!;
// Print lines that are visible to localization
if (localization.LineQueryCached.TryGetValue((null, null), out IEnumerable<IEnumerable<KeyValuePair<string, MarkedText>>> lines))
{
foreach (var line in lines)
{
// Read values
line.ReadValues(out MarkedText pluralRules, out MarkedText culture, out MarkedText key, out MarkedText plurals, out MarkedText template, out MarkedText text);
// No value
if (!key.HasValue || !text.HasValue) continue;
// Log
logger.LogInformation("Culture={Culture}, Key={Key}, Text={Text}, Template={Template}, PluralRules={PluralRules}, Plurals={Plurals}", culture.Text, key.Text, text.Text, template.Text, pluralRules.Text, plurals.Text);
}
}
}