Blazor WebAssembly本地化
Blazor WebAssembly本地化
版本:.net 6.0
有两种本地化的方式,使用resx
文件或json
文件
先创建一个项目,然后给添加本地化配置
<PropertyGroup>
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
</PropertyGroup>
没有上面的配置会报这个异常
System.InvalidOperationException: Blazor detected a change in the application's culture that is not supported with the current project configuration. To change culture dynamically during startup, set <BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData> in the application's project file.
本案例实现一个语言选择下拉框,通过localStorage
读取用户设置
本案例使用的NuGet库
Microsoft.Fast.Components.FluentUI
Blazored.LocalStorage
Microsoft.Fast.Components.FluentUI
是微软的Fluent Design
组件框架
Blazored.LocalStorage
是操作localStorage
的,也可以使用IJSRuntime
操作,不过我不喜欢
resx
这里需要一个NuGet包
Microsoft.Extensions.Localization
在wwwroot
目录下新建appsettings.json
文件
{
"CultureKey": "culture",
"DefaultCulture": {
"Id": "zh-CN",
"Language": "简体中文"
},
"Cultures": [
{
"Id": "zh-CN",
"Language": "简体中文"
},
{
"Id": "en-US",
"Language": "English"
}
]
}
在Shared
目录下创建存放本地化文本的资源目录ResourceFiles
,并在资源目录下创建Resource.resx
文件,添加一个测试用的键值对,并设置访问修饰符,这个目录会生成Resource.Designer.cs
文件
然后再创建Resource.zh-CN.resx
文件
再来一个Resource.en-US.resx
创建CultureModel
,用于实例化Configuration
读取
public class CultureModel
{
/// <summary>
/// 区域编号
/// </summary>
public string Id { get; set; }
/// <summary>
/// 语言名称
/// </summary>
public string Language { get; set; }
}
创建WebAssemblyHostExtension
扩展方法,在这里设置区域,这里的主要操作是读取localStorage
设置区域,没有用户设置则使用默认区域设置
public static class WebAssemblyHostExtension
{
/// <summary>
/// 设置默认区域
/// </summary>
/// <param name="host"></param>
/// <returns></returns>
public async static Task SetDefaultCulture(this WebAssemblyHost host)
{
var configuration = host.Services.GetService<IConfiguration>();
string cultureKey = configuration["CultureKey"];
var localStorageService = host.Services.GetService<ILocalStorageService>();
string result = await localStorageService.GetItemAsync<string>(cultureKey);
CultureInfo culture;
if (false == string.IsNullOrWhiteSpace(result))
{
culture = new CultureInfo(result);
}
else
{
var defaultCulture = configuration.GetSection("DefaultCulture").Get<CultureModel>();
culture = new CultureInfo(defaultCulture.Id);
}
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
}
}
主函数配置服务,SetDefaultCulture()
是刚才写的扩展方法,AddLocalization
添加本地化服务,用于依赖注入
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
ConfigureServices(builder.Services);
var host = builder.Build();
//设置默认区域性
await host.SetDefaultCulture();
await host.RunAsync();
}
/// <summary>
/// 配置服务
/// </summary>
/// <param name="services"></param>
public static void ConfigureServices(IServiceCollection services)
{
//添加本地化
services.AddLocalization();
//添加LocalStorage
services.AddBlazoredLocalStorage();
}
}
然后在Shared
目录下创建Components
目录,创建CultureSelector.razor
组件
<FluentSelect TOption="CultureModel" Items="this._cultureModels" @bind-SelectedOption="this.Culture" OptionValue="(x=>x.Id)" OptionText="(x=>x.Language)"></FluentSelect>
@code {
[Inject]
private IConfiguration _configuration { get; set; }
[Inject]
private NavigationManager _navigationManager { get; set; }
[Inject]
private ILocalStorageService _localStorageService { get; set; }
/// <summary>
/// localStorage中区域的key
/// </summary>
private string _cultureKey { get; set; }
/// <summary>
/// 区域列表
/// </summary>
private List<CultureModel> _cultureModels { get; set; }
private CultureModel? _culture;
/// <summary>
/// 用于显示地区
/// </summary>
public CultureModel? Culture
{
get
{
var currentCulture = CultureInfo.CurrentCulture;
var culture = this._cultureModels.FirstOrDefault(x => currentCulture.Name == x.Id);
return culture;
}
set
{
_culture = value;
this.CultureSetterAsync(value);
}
}
protected async override void OnInitialized()
{
this._cultureKey = this._configuration["CultureKey"];
//初始化区域列表
var list = this._configuration.GetSection("Cultures").Get<List<CultureModel>>();
if (null == list)
{
this._cultureModels = new List<CultureModel>();
}
else
{
this._cultureModels = list;
}
}
/// <summary>
/// 异步设置Culture属性
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private async Task CultureSetterAsync(CultureModel value)
{
//判断当前值是否一致,不一致则重新写入并刷新
if (null != value)
{
string cultureId = value.Id;
var currentCulture = CultureInfo.CurrentCulture;
if (currentCulture.Name != cultureId)
{
await this._localStorageService.SetItemAsync<string>(this._cultureKey, cultureId);
//forceLoad重新触发Program.cs的流程
this._navigationManager.NavigateTo(this._navigationManager.Uri, forceLoad: true);
}
}
}
}
使用resx
文件本地化的方式需要刷新,所以使用forceLoad
,不过因为是刷新,所以getter直接返回CultureInfo.CurrentCulture
就可以了,setter那里因为属性不是异步的,所以写一个异步函数放最后执行即可
Index.razor
,这里是主页,只要注入IStringLocalizer<Resource>
即可,这个Resource
就是默认资源文件Resource.resx
生成的
<div>
<CultureSelector></CultureSelector>
</div>
<span>@this.Localizer["Test"]</span>
@code {
[Inject]
private IStringLocalizer<Resource> Localizer { get; set; }
}
效果
json
这个方法需要使用额外的NuGet库
Toolbelt.Blazor.I18nText
直接看作者的README.md
也可以
https://github.com/jsakamoto/Toolbelt.Blazor.I18nText
项目项目下新建i18ntext
目录,用于存放本地化文本,文件命名规则
<Text Table Name>.<Language Code>.{json|csv}
貌似en.json
是必须的
Text.en.json
{
"Hello": "Default"
}
Text.zh-CN.json
{
"Hello": "你好"
}
Text.en-US.json
{
"Hello": "Hello"
}
重新编译一下就会生成代码
resx
案例的appsettings.json
、CultureModel.cs
和WebAssemblyHostExtension.cs
不变
Program.cs
文件内容变成
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
ConfigureServices(builder.Services);
var host = builder.Build();
//设置默认区域性
await host.SetDefaultCulture();
await host.RunAsync();
}
/// <summary>
/// 配置服务
/// </summary>
/// <param name="services"></param>
public static void ConfigureServices(IServiceCollection services)
{
//添加i18next
services.AddI18nText();
//添加LocalStorage
services.AddBlazoredLocalStorage();
}
}
也就改成了AddI18nText()
CultureSelector.razor
改动较大
<FluentSelect TOption="CultureModel" Items="this._cultureModels" @bind-SelectedOption="this.Culture" OptionValue="(x=>x.Id)" OptionText="(x=>x.Language)"></FluentSelect>
@code {
[Inject]
private IConfiguration _configuration { get; set; }
[Inject]
private ILocalStorageService _localStorageService { get; set; }
[Inject]
private Toolbelt.Blazor.I18nText.I18nText _i18nTextService { get; set; }
/// <summary>
/// localStorage中区域的key
/// </summary>
private string _cultureKey { get; set; }
/// <summary>
/// 区域列表
/// </summary>
private List<CultureModel> _cultureModels { get; set; }
private CultureModel? _culture;
/// <summary>
/// 用于显示地区
/// </summary>
public CultureModel? Culture
{
get
{
var currentCulture = CultureInfo.CurrentCulture;
var culture = this._cultureModels.FirstOrDefault(x => currentCulture.Name == x.Id);
return culture;
}
set
{
_culture = value;
this.CultureSetterAsync(value);
}
}
protected async override Task OnInitializedAsync()
{
this._cultureKey = this._configuration["CultureKey"];
//初始化区域列表
var list = this._configuration.GetSection("Cultures").Get<List<CultureModel>>();
if (null == list)
{
this._cultureModels = new List<CultureModel>();
}
else
{
this._cultureModels = list;
}
//初始化默认culture
await this.InitCurrentCulture();
}
/// <summary>
/// 初始化culture
/// </summary>
/// <returns></returns>
private async Task InitCurrentCulture()
{
string localCulture = await this._localStorageService.GetItemAsync<string>(this._cultureKey);
CultureModel culture;
if (false == string.IsNullOrWhiteSpace(localCulture))
{
culture = this._cultureModels.FirstOrDefault(x => localCulture == x.Id);
}
else
{
culture = this._configuration.GetSection("DefaultCulture").Get<CultureModel>();
}
if (null != culture)
{
//设置默认culture
await this._i18nTextService.SetCurrentLanguageAsync(culture.Id);
}
}
/// <summary>
/// 异步设置Culture属性
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private async Task CultureSetterAsync(CultureModel value)
{
//判断当前值是否一致,不一致则重新写入
if (null != value)
{
string cultureId = value.Id;
var currentCulture = CultureInfo.CurrentCulture;
if (currentCulture.Name != cultureId)
{
//用于Getter读取
var culture = new CultureInfo(cultureId);
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
//设置本地存储
await this._localStorageService.SetItemAsync<string>(this._cultureKey, cultureId);
//设置当前culture
await this._i18nTextService.SetCurrentLanguageAsync(cultureId);
}
}
}
}
也就是从CultureInfo.CurrentCulture
控制区域,改成I18nText
控制,不再需要刷新了,但是WebAssemblyHostExtension
里的初始化就失效了,需要手动初始化区域,不过还是因为属性不是异步的关系,所以还是需要通过设置CultureInfo
读取当前区域
Index.razor
,这里有两种方式读取本地化文本,像属性一样引用,或者使用索引器,引用属性是有智能提示的
<div>
<CultureSelector></CultureSelector>
</div>
<p>@this._text?.Hello</p>
<p>@this._text?["Hello"]</p>
@code
{
[Inject]
private Toolbelt.Blazor.I18nText.I18nText _i18nTextService { get; set; }
private I18nText.Text _text { get; set; }
protected async override Task OnInitializedAsync()
{
this._text = await this._i18nTextService.GetTextTableAsync<I18nText.Text>(this);
}
}
还有要注意的点,Toolbelt.Blazor.I18nText.I18nText
是依赖注入,I18nText.Text
这个是生成的代码,I18nText
是项目的命名空间,不是Toolbelt.Blazor.I18nText
的命名空间
可以在App.razor
中加载本地化配置
@code {
[Inject]
private IConfiguration _configuration { get; set; }
[Inject]
private ILocalStorageService _localStorageService { get; set; }
[Inject]
private Toolbelt.Blazor.I18nText.I18nText _i18nTextService { get; set; }
/// <summary>
/// localStorage中区域的key
/// </summary>
private string _cultureKey { get; set; }
/// <summary>
/// 区域列表
/// </summary>
private List<CultureModel> _cultureModels { get; set; }
protected override void OnInitialized()
{
this._cultureKey = this._configuration["CultureKey"];
//初始化区域列表
var list = this._configuration.GetSection("Cultures").Get<List<CultureModel>>();
if (null == list)
{
this._cultureModels = new List<CultureModel>();
}
else
{
this._cultureModels = list;
}
}
protected async override Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
//初始化默认culture
await this.InitCurrentCulture();
}
/// <summary>
/// 初始化culture
/// </summary>
/// <returns></returns>
private async Task InitCurrentCulture()
{
string localCulture = await this._localStorageService.GetItemAsync<string>(this._cultureKey);
CultureModel culture;
if (false == string.IsNullOrWhiteSpace(localCulture))
{
culture = this._cultureModels.FirstOrDefault(x => localCulture == x.Id);
}
else
{
culture = this._configuration.GetSection("DefaultCulture").Get<CultureModel>();
}
if (null != culture)
{
//设置默认culture
await this._i18nTextService.SetCurrentLanguageAsync(culture.Id);
}
}
}
如果不想每次手动创建本地化对象,可以创建LocalizerComponentBase
组件,初始化I18nText.Text
本地化,再用于继承,不过感觉内存占用不太妙
也可以用一个全局对象GlobalVariable
,使用static
创建本地化对象作为属性,再初始化这个全局变量即可,就像appsettings.json
的静态映射类那样操作