首页 > 技术文章 > ASP.NET Core搭建多层网站架构【13-扩展之支持全球化和本地化多语言】

kasnti 2020-02-03 15:53 原文

2020/02/03, ASP.NET Core 3.1, VS2019, ResXManager

摘要:基于ASP.NET Core 3.1 WebApi搭建后端多层网站架构【13-扩展之支持全球化和本地化多语言】
使用资源管理多语言文件实现网站本地化支持多语言显示

文章目录

此分支项目代码

官方文档请点击:ASP.NET Core 全球化和本地化

本章节介绍了使用资源管理多语言文件实现网站本地化支持多语言显示

说明:本文的方法是采取所有的语言资源都在同一个地方,例如在MS.WebCore路径下有SharedResource.zh-Hans.resxSharedResource.zh-Hant.resx两个语言资源文件,里面包含了整个网站所有的翻译,不管是Controller中的翻译还是业务Service的翻译都从SharedResource中读取。
而官方的做法是:HomeController的语言资源文件在Resources/Controllers.HomeController.fr.resxResources/Controllers/HomeController.fr.resx中,HomeService的语言资源文件则可能在Resources/Services.HomeService.fr.resxResources/Services/HomeService.fr.resx中,各个语言资源文件分散在各处,没有统一管理,官方语言资源文件命名的规则请查阅文档

网站添加默认语言

MS.WebApi应用程序的appsettings.jsonSiteSetting节点中,添加DefaultLanguage子节点:"DefaultLanguage": "zh-Hans"

MS.WebCore类库的SiteSetting.cs类中,对应添加网站默认语言的成员变量public string DefaultLanguage { get; set; }

说明:

  • 网站配置中添加了默认语言(DefaultLanguage)配置,默认为简体中文zh-Hans
  • zh-Hans表示简体中文,未区分地区,如果要精确可以使用zh-Hans-CN表示中国大陆地区使用的简体中文,zh-Hans-SG则表示新加坡地区使用的简体中文,标准文档
  • 本文还将建立其他两种语言:zh-Hant繁体中文、en英语(也都是未区分地区的)

添加语言资源文件

MS.WebCore类库中新建MultiLanguages文件夹,在该文件夹下新建SharedResource.cs类:

namespace MS.WebCore.MultiLanguages
{
    public class SharedResource
    {
    }
}

注意是public类型

在MultiLanguages文件夹下新建资源文件SharedResource.zh-Hans.resx:

如果新建项中找不到资源文件的选项,把MS.WebCore类库中的Project Sdk从Microsoft.NET.Sdk改为Microsoft.NET.Sdk.Web再试试(记得添加完资源文件再把sdk改回来)
同样的方式再添加SharedResource.en.resxSharedResource.zh-Hant.resx
新建完成后,如下图所示:

后面的操作中,会主要编辑简体中文的翻译内容,然后使用工具同步翻译繁体中文、英语的翻译内容

封装注册

在MultiLanguages文件夹下新建MultiLangExtensions.cs类:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using System.Collections.Generic;
using System.Globalization;

namespace MS.WebCore.MultiLanguages
{
    public static class MultiLangExtensions
    {
        /// <summary>
        /// 支持的语言类型
        /// 此处内容要与真实的文件对应
        /// </summary>
        public static readonly List<string> supportLangs = new List<string>
        {
            "zh-Hans",
            "zh-Hant",
            "en"
        };

        /// <summary>
        /// 更改当前UI线程语言
        /// </summary>
        /// <param name="name"></param>
        public static void SetCurrentUICulture(string name)
        {
            CultureInfo.CurrentUICulture = new CultureInfo(name, false);
        }
        /// <summary>
        /// 获取指定语言的文字内容
        /// </summary>
        /// <param name="localizer"></param>
        /// <param name="specificLang"></param>
        /// <param name="key"></param>
        /// <returns></returns>
        public static string GetSpecificLanguageString(this IStringLocalizer localizer, string specificLang, string key)
        {
#pragma warning disable CS0618 // 类型或成员已过时
            return localizer.WithCulture(new CultureInfo(specificLang))[key].ToString();
#pragma warning restore CS0618 // 类型或成员已过时
        }


        /// <summary>
        /// 添加多语言本地化支持
        /// </summary>
        /// <param name="services"></param>
        /// <returns></returns>
        public static IServiceCollection AddMultiLanguages(this IServiceCollection services)
        {

            services.AddLocalization();

            services.AddSingleton<IStringLocalizer>((sp) =>
            {
                var sharedLocalizer = sp.GetRequiredService<IStringLocalizer<SharedResource>>();
                return sharedLocalizer;
            });
            /*
             //需要在startup中,AddControllers(webapi)的后面或者AddMVC(webmvc)注册
            .AddDataAnnotationsLocalization(options =>
                {
                    options.DataAnnotationLocalizerProvider = (type, factory) =>
                        factory.Create(typeof(SharedResource));//给注解添加本地化资源提供器Localizerprovider
                })
             
             */

            return services;
        }

        /// <summary>
        /// 使用多语言本地化中间件
        /// 默认语言在appsetting中设置
        /// </summary>
        /// <param name="app"></param>
        /// <param name="configuration"></param>
        /// <returns></returns>
        public static IApplicationBuilder UseMultiLanguage(this IApplicationBuilder app, IConfiguration configuration)
        {
            List<CultureInfo> supportedCultures = new List<CultureInfo>();
            foreach (var item in supportLangs)
            {
                supportedCultures.Add(new CultureInfo(item));
            }
            return app.UseRequestLocalization(new RequestLocalizationOptions
            {
                DefaultRequestCulture = new RequestCulture(configuration.GetSection("SiteSetting:DefaultLanguage").Value),
                // Formatting numbers, dates, etc.
                SupportedCultures = supportedCultures,
                // UI strings that we have localized.
                SupportedUICultures = supportedCultures
            });
        }
    }
}

说明:

  • 指定了支持的语言列表:"zh-Hans","zh-Hant","en"
  • SetCurrentUICulture方法用于修改当前UI线程语言,例如一开始默认设定了中文,然后想切换成英文,就使用该方法
  • GetSpecificLanguageString方法是不切换UI线程语言的情况下,获取其他语言的翻译内容
  • AddMultiLanguages是封装了多语言服务
    • 其中AddLocalization是给服务添加本地化
    • AddSingleton是注册获取语言资源提供器为单例,默认取的是SharedResource资源内容,所以后文在构造器中获取IStringLocalizer,用它取到的语言都是从SharedResource语言资源文件中读取到的
  • UseMultiLanguage方法是注册多语言中间件,其中设定了支持的语言列表,根据网站配置设定了默认的语言

使用

注册服务

MS.WebApi应用程序的Startup.cs类ConfigureServices中:

//添加多语言本地化支持
services.AddMultiLanguages();

services
    .AddControllers(options =>
    {
        options.Filters.Add<ApiResultFilter>();
        options.Filters.Add<ApiExceptionFilter>();
    })
    .AddDataAnnotationsLocalization(options =>
    {
        options.DataAnnotationLocalizerProvider = (type, factory) =>
        factory.Create(typeof(SharedResource));//给注解添加本地化资源提供器Localizerprovider
    });

说明:

  • services.AddMultiLanguages();是添加多语言本地化支持
  • services.AddControllers原本就有了,但要在后面追加AddDataAnnotationsLocalization方法,是给DataAnnotations本地化,依旧是注册SharedResource语言文件,这样一来,ViewModel中的注解也都能支持多语言了

MS.WebApi应用程序的Startup.cs类Configure中添加app.UseMultiLanguage(Configuration);

添加语言

以LoginViewModel中的翻译为例,我暂时添加了LoginViewModel字段注解上的简体中文、繁体中文翻译(英语未添加)

启动项目,打开Postman,选取登陆接口进行测试: localhost:5000/account 、
接着修改接口地址为: localhost:5000/account?culture=zh-hant 、
再继续测试未添加的语言:localhost:5000/account?culture=en(图中是us,打错了,但是不影响结果)

说明:

  • 只维护了LoginViewModel注解上的简中和繁中,未维护英文
  • 资源文件resx中,左边是名称,即获取语言的代号,右边是值,获取对应的语言值,也就是意味着各个语言资源文件的名称都是相同的,仅仅是值不一样
  • 默认不附带语言时,获取到的是网站默认语言 简体中文
  • 使用culture来指定需要显示的语言
  • 使用culture=zh-hant来指定显示 繁体中文,在维护了繁体中文时,则会显示出来
  • 指定显示为英文时,由于英文资源未维护,所以culture=us结果显示依然是默认语言中文

至此,ViewModel数据注解已经实现了本地化

调用语言

继续维护LoginViewModel中剩下的翻译到简体中文、繁体中文资源文件中:

使用IStringLocalizer来获取语言资源
LoginValidate方法中添加参数IStringLocalizer localizer,然后使用GetString方法获取对应的语言:

public async Task<ExecuteResult<UserData>> LoginValidate(IUnitOfWork<MSDbContext> unitOfWork, IMapper mapper, SiteSetting siteSetting, IStringLocalizer localizer)
{
    ExecuteResult<UserData> result = new ExecuteResult<UserData>();
    //将登录用户查出来
    var loginUserInDB = await unitOfWork.GetRepository<UserLogin>().FindAsync(Account);

    //用户不存在
    if (loginUserInDB is null)
    {
        return result.SetFailMessage(localizer.GetString("DataAnnotations_ErrorMessage_NotExist", localizer.GetString("LoginViewModel_DisplayName_Account")));
    }

    //用户被锁定
    if (loginUserInDB.IsLocked &&
        loginUserInDB.LockedTime.HasValue &&
        (DateTime.Now - loginUserInDB.LockedTime.Value).Minutes < siteSetting.LoginLockedTimeout)
    {
        return result.SetFailMessage(localizer.GetString("LoginViewModel_ServiceError_UserLocked", siteSetting.LoginLockedTimeout));
    }

    //密码正确
    if (Crypto.VerifyHashedPassword(loginUserInDB.HashedPassword, Password))
    {
        //密码正确后才加载用户信息、角色信息
        var userInDB = await unitOfWork.GetRepository<User>().GetFirstOrDefaultAsync(
            predicate: a => a.Id == loginUserInDB.UserId,
            include: source => source
             .Include(u => u.Role));

        //如果用户已失效
        if (userInDB.StatusCode != StatusCode.Enable)
        {
            return result.SetFailMessage(localizer.GetString("LoginViewModel_ServiceError_UserDisabled"));
        }

        //用户正常、密码正确,更新相应字段
        loginUserInDB.IsLocked = false;
        loginUserInDB.AccessFailedCount = 0;
        loginUserInDB.LastLoginTime = DateTime.Now;
        //提交到数据库
        await unitOfWork.SaveChangesAsync();

        //得到userdata
        UserData userData = mapper.Map<UserData>(userInDB);
        return result.SetData(userData);
    }
    //密码错误
    else
    {
        loginUserInDB.AccessFailedCount++;//失败次数累加
        result.SetFailMessage(localizer.GetString("LoginViewModel_ServiceError_LoginError"));
        //超出失败次数限制
        if (loginUserInDB.AccessFailedCount >= siteSetting.LoginFailedCountLimits)
        {
            loginUserInDB.IsLocked = true;
            loginUserInDB.LockedTime = DateTime.Now;
            result.SetFailMessage(localizer.GetString("LoginViewModel_ServiceError_UserLocked", siteSetting.LoginLockedTimeout));
        }
        //提交到数据库
        await unitOfWork.SaveChangesAsync();
        return result;
    }
}

IStringLocalizer的使用方法如上所示。
LoginValidate方法是在AccountService中被调用的,所以AccountService中通过构造函数依赖注入的方式得到IStringLocalizer localizer

启动项目,打开Postman调用登陆接口,可以看到登陆验证的返回内容已经取得多语言:

维护多语言资源

安装插件

这里我推荐一个叫ResXManager的VS插件,用它管理多语言资源:

对着resx文件右击使用ResXManager打开(打开的前提是文件内至少要有一条语言记录):

打开后如下图,多种语言都在一个页面中显示出来,选择某一行可以直接编辑:

导出为Excel

选择将所有资源导出,保存在本地

使用谷歌翻译

打开Excel,将简体中文那部分选择,复制出来,粘贴到谷歌翻译中:

选取好要翻译的目标语言后,将翻译后的内容复制回Excel:

最后在ResXManager插件中,把Excel文件导入回来:

可以看到需要翻译的资源都出来了:

说明:

  • 这个插件也支持调用api自动翻译,但是需要api key,可以自行研究下

多语言都维护好后,启动项目,打开Postman调用登陆接口,可以看到英文被正确翻译了:

推荐阅读