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.jsonCultureModel.csWebAssemblyHostExtension.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的静态映射类那样操作

效果

Blazor WebAssembly本地化 结束

posted @ 2023-07-04 15:55  .NET好耶  阅读(222)  评论(0编辑  收藏  举报