[Abp 源码分析]十3、多语言(本地化)处理

0.简介

若是你所开发的须要走向世界的话,那么确定须要针对每个用户进行不一样的本地化处理,有可能你的客户在日本,须要使用日语做为显示文本,也有可能你的客户在美国,须要使用英语做为显示文本。若是你仍是同样的写死错误信息,或者描述信息,那么就没法作到多语言适配。html

Abp 框架自己提供了一套多语言机制来帮助咱们实现本地化,基本思路是 Abp 自己维护一个键值对集合。只须要将展现给客户的文字信息处都使用一个语言 Key 来进行填充,当用户登陆系统以后,会取得当前用户的区域文化信息进行文本渲染。前端

0.1 如何使用

咱们首先来看一下如何定义一个多语言资源并使用。首先 Abp 自身支持三种类型的本地化资源来源,第一种是 XML 文件,第二种则是 JSON 文件,第三种则是内嵌资源文件,若是这三种都不能知足你的需求,你能够自行实现 ILocalizationSource  接口来返回多语言资源。数据库

小提示:app

Abp Zero 模块就提供了数据库持久化存储多语言资源的功能。框架

0.1.1 定义应用程序支持的语言

若是你须要为你的应用程序添加不一样语言的支持,就必须在你任意模块的预加载方法当中添加语言来进行配置:ide

Configuration.Localization.Languages.Add(new LanguageInfo("en", "English", "famfamfam-flag-england", true));
Configuration.Localization.Languages.Add(new LanguageInfo("tr", "Türkçe", "famfamfam-flag-tr"));

例如以上代码,就可以让咱们的程序拥有针对英语与土耳其语的多语言处理能力。ui

这里的 famfamfam-flag-englandfamfamfam-flag-tr 是一个 CSS 类型,是 Abp 为前端展现所封装的小国旗图标。3d

0.1.2 创建多语言资源文件

有了语言以后,Abp 还须要你提供标准的多语言资源文件,这里咱们以 自带的 XML 资源文件为例,其文件名称为 Abp-zh-Hans.xml ,路径为 Abp\Localization\Sources\AbpXmlSourcecode

<?xml version="1.0" encoding="utf-8" ?>
<localizationDictionary culture="zh-Hans">
  <texts>
    <text name="SmtpHost">SMTP主机</text>
    <text name="SmtpPort">SMTP端口</text>
    <text name="Username">用户名</text>
    <text name="Password">密码</text>
    <text name="DomainName">域名</text>
    <text name="UseSSL">使用SSL</text>
    <text name="UseDefaultCredentials">使用默认验证</text>
    <text name="DefaultFromSenderEmailAddress">默认发件人邮箱地址</text>
    <text name="DefaultFromSenderDisplayName">默认发件人名字</text>
    <text name="DefaultLanguage">预设语言</text>
    <text name="ReceiveNotifications">接收通知</text>
    <text name="CurrentUserDidNotLoginToTheApplication">当前用户没有登陆到系统!</text>
    <text name="TimeZone">时区</text>
    <text name="AllOfThesePermissionsMustBeGranted">您没有权限进行此操做,您须要如下权限: {0}</text>
    <text name="AtLeastOneOfThesePermissionsMustBeGranted">您没有权限进行此操做,您至少须要下列权限的其中一项: {0}</text>
    <text name="MainMenu">主菜单</text>
  </texts>
</localizationDictionary>

每一个文件内部,会有一个 <localizationDictionary culture="zh-Hans"> 节点用于说明当前文件是针对于哪一个区域适用的,而在其 <texts> 内部则就是结合键值对的形式,name 里面的内容就是多语言文本项的键,在标签内部的就是其真正的值。xml

打开一个针对俄语国家的 XML 资源文件,文件名称叫作 Abp-ru.xml

<?xml version="1.0" encoding="utf-8" ?>
<localizationDictionary culture="ru">
  <texts>
    <text name="SmtpHost">SMTP сервер</text>
    <text name="SmtpPort">SMTP порт</text>
    <text name="Username">Имя пользователя</text>
    <text name="Password">Пароль</text>
    <text name="DomainName">Домен</text>
    <text name="UseSSL">Использовать SSL</text>
    <text name="UseDefaultCredentials">Использовать учетные данные по умолчанию</text>
    <text name="DefaultFromSenderEmailAddress">Электронный адрес отправителя по умолчанию</text>
    <text name="DefaultFromSenderDisplayName">Имя отправителя по умолчанию</text>
    <text name="DefaultLanguage">Язык по умолчанию</text>
    <text name="ReceiveNotifications">Получать уведомления</text>
    <text name="CurrentUserDidNotLoginToTheApplication">Текущий пользователь не вошёл в приложение!</text>
  </texts>
</localizationDictionary>

能够看到 Key 值都是同样的,只是其 <text> 内部的值根据区域国家的不一样值不同而已。

其次从文件名咱们就能够看到须要使用 XML 资源文件对于文件的命名格式会有必定要求,仍是以 Abp 自带的资源文件为例,能够看一下他们基本上都是由 {SourceName}-{CultureInfo}.xml 这样构成的。

0.1.3 注册本地化的 XML 资源

那么若是咱们须要注册以前的两个 XML 资源到 Abp 框架当中的话,则须要在预加载模块处经过以下代码来执行注册,而且须要右键 XML 文件,更改其构建操做为 内嵌资源

Configuration.Localization.Sources.Add(
    new DictionaryBasedLocalizationSource(
        // 本地化资源名称
        AbpConsts.LocalizationSourceName,
        // 数据源提供者,这里使用的是 XML ,除了 XML 提供者,还有 JSON 等
        new XmlEmbeddedFileLocalizationDictionaryProvider(
            typeof(AbpKernelModule).GetAssembly(), "Abp.Localization.Sources.AbpXmlSource"
        )));

0.1.4 获取多语言文本

若是你须要在某处获取指定 Key 所对应的具体显示文本,只须要注入 ILocalizationManager ,经过其 GetString() 方法就能够得到具体的值。若是你须要获取本地化资源的地方不可以使用依赖注入,你可使用 LocalizationHelper 静态类来进行操做。

var @string = _localizationManager.GetString("Abp", "MainMenu");

它默认是从 Thread.CurrentThread.CurrentUICulture 获取到的当前区域信息,从而来取得某个 Key 所对应的显示值,而当前区域信息是由 Abp 注入的一系列 RequestCultureProviders 所提供的,他按照如下顺序来进行设置。

  1. QueryStringRequestCultureProvider(ASP .NET Core 默认提供):该默认提供器使用的是 QueryStringculture&ui-culture 所提供的区域文化信息来初始化该值,例如:culture=es-MX&ui-culture=es-MX
  2. AbpUserRequestCultureProvider (Abp 提供):该提供器会读取当前用户的 IAbpSession 信息,而且从 ISettingManager 中获取用户所配置的 "Abp.Localization.DefaultLanguageName" 属性,将其做为默认的区域文化信息。
  3. AbpLocalizationHeaderRequestCultureProvider (Abp 提供):使用每次请求头当中的 .AspNetCore.Culture 值做为当前的区域文化信息,例如 c=en|uic=en-US
  4. CookieRequestCultureProvider (ASP .NET Core 提供):使用每次请求的 Cookie 当中 Key 为 .AspNetCore.Culture 值做为当前区域文化信息。
  5. AbpDefaultRequestCultureProvider (Abp 提供):若是以前这些提供器都没有为当前区域文化赋值,则从 ISettingMananger 当中取得 Abp.Localization.DefaultLanguageName 的默认值。
  6. AcceptLanguageHeaderRequestCultureProvider (ASP .NET Core 默认提供):该提供器最终会使用用户每次请求时传递的 Accept-Language 头部做为当前区域文化信息。

小提示:

这里 Abp 注入的提供器是有顺序的,注入这么多提供器就是为了最后肯定当前用户的区域文化信息以便展现相应的语言文本。

1.启动流程

1.1 启动流程图

1.2 代码流程

根据使用方法咱们能够得知,要配置 Abp 的多语言,必须得等 IAbpStartupConfiguration 初始化完毕才能够。即在 AbpBootstrapperInitialize() 方法之中:

public virtual void Initialize()
{
    // ... 其余代码
    // 注入 IAbpStartupConfiguration 配置与本地化资源配置
    IocManager.IocContainer.Install(new AbpCoreInstaller());

    // ... 其余代码
    // 初始化 AbpStartupConfiguration 类型
    IocManager.Resolve<AbpStartupConfiguration>().Initialize();

    // ... 其余代码
}

配置类里面包含了用户所配置的全部语言与多语言资源信息,在被成功注入到 Ioc 容器以后,Abp 就开始使用本地化资源管理器来初始化这些多语言数据了。

public override void PostInitialize()
{
    // 注册缺乏的组件,防止遗漏注册组件
    RegisterMissingComponents();

    IocManager.Resolve<SettingDefinitionManager>().Initialize();
    IocManager.Resolve<FeatureManager>().Initialize();
    IocManager.Resolve<PermissionManager>().Initialize();
    
    // 重点在这里,这个 PostInitialize 方法是存放在核心模块当中的,在这里调用了本地化资源管理器的初始化方法
    IocManager.Resolve<LocalizationManager>().Initialize();
    IocManager.Resolve<NotificationDefinitionManager>().Initialize();
    IocManager.Resolve<NavigationManager>().Initialize();

    if (Configuration.BackgroundJobs.IsJobExecutionEnabled)
    {
        var workerManager = IocManager.Resolve<IBackgroundWorkerManager>();
        workerManager.Start();
        workerManager.Add(IocManager.Resolve<IBackgroundJobManager>());
    }
}

具体 LocalizationManager 及其内部的实现咱们在下一节代码分析中详细进行讲述。

这些动做仅仅是在注入 Abp 框架的时候所须要执行的一些步骤,若是你要启用多语言,须要在 ASP .NET Core 程序的 Startup 类中的 Configure() 处经过更改 UseAbpRequestLocalization 状态为 True,才会将区域文化识别中间件注入到程序当中。

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    app.UseAbp(options =>
    {
        options.UseAbpRequestLocalization = false; //disable automatic adding of request localization
    });

    //...authentication middleware(s)

    app.UseAbpRequestLocalization(); //manually add request localization

    //...other middlewares

    app.UseMvc(routes =>
    {
        //...
    });
}

其实这里的 UseAbpRequestLocalization() 就已经将上文说的那些 RequestProvider 按照顺序依次注入到 MVC 之中了。

2.代码分析

Abp 框架针对本地化处理相关的类型与方法定义都存放在 Abp 库的 Localization 文件夹下。关系仍是相对复杂的,这里咱们先从其核心的 Abp 库针对于多语言的处理开始讲起。

2.1 多语言模块配置

Abp 须要使用的全部信息都是由用户在本身启动模块的 PreInitialize() 当中,经过 ILocalizationConfiguration 进行注入配置。也就是说在 ILocalizationConfiguration 内部,主要是包含了语言,与多语言资源提供者两种重点信息。

public interface ILocalizationConfiguration
{
    // 当前应用程序可配置的语言列表
    IList<LanguageInfo> Languages { get; }

    // 本地化资源列表
    ILocalizationSourceList Sources { get; }

    // 是否启用多语言(本地化) 系统
    bool IsEnabled { get; set; }

    // 如下四个布尔类型的参数主要用于肯定当没有找到多语言文本时的处理逻辑,默认都为 True
    bool ReturnGivenTextIfNotFound { get; set; }

    bool WrapGivenTextIfNotFound { get; set; }

    bool HumanizeTextIfNotFound { get; set; }

    bool LogWarnMessageIfNotFound { get; set; }
}

2.2 语言信息

当前应用程序可以支持哪一些语言,取决于用户在预加载的时候给多语言模块配置对象分配了哪些语言。经过第 0.1.1 节咱们看到用户能够直接经过初始化一个新的 LanguageInfo 对象,将其添加到 Languages 属性之中。

public class LanguageInfo
{
    /// <summary>
    /// 区域文化代码名称
    /// 应该是一个有效的区域文化代码名称,更多的能够经过 CultureInfo 静态类得到全部文化代码。
    /// 例如: "en-US" 是北美适用的, "tr-TR" 适用于土耳其。
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// 该语言默认应该展现的语言名称。
    /// 例如: 英语应该展现为 "English", "zh-Hans" 应该展现为 "简体中文"
    /// </summary>
    public string DisplayName { get; set; }

    /// <summary>
    /// 用于展现的图标 CSS 类名,可选参数
    /// </summary>
    public string Icon { get; set; }

    /// <summary>
    /// 是否为默认语言
    /// </summary>
    public bool IsDefault { get; set; }

    /// <summary>
    /// 该语言是否被禁用
    /// </summary>
    public bool IsDisabled { get; set; }

    /// <summary>
    /// 语言的展现方式是自左向右仍是自右向左
    /// </summary>
    public bool IsRightToLeft
    {
        get
        {
            try
            {
                return CultureInfo.GetCultureInfo(Name).TextInfo?.IsRightToLeft ?? false;
            }
            catch
            {
                return false;
            }
        }
    }

    public LanguageInfo(string name, string displayName, string icon = null, bool isDefault = false, bool isDisabled = false)
    {
        Name = name;
        DisplayName = displayName;
        Icon = icon;
        IsDefault = isDefault;
        IsDisabled = isDisabled;
    }
}

关于语言的定义仍是至关简单的,主要参数就是语言的 区域文化代码展现的名称,其他的均可以是可选参数。

小提示:

关于当前系统所支持的区域文化代码,能够经过执行 CultureInfo.GetCultures(CultureTypes.AllCultures); 获得。

2.3 语言管理器

Abp 针对语言也提供了一个管理器,接口叫作 ILanguageManager,定义简单,两个方法。

public interface ILanguageManager
{
    // 得到当前语言
    LanguageInfo CurrentLanguage { get; }

    // 得到全部语言
    IReadOnlyList<LanguageInfo> GetLanguages();
}

实现也不复杂,它内部的实现就是从一个 ILanguageProvider 拿取有哪一些语言数据。

private readonly ILanguageProvider _languageProvider;

public IReadOnlyList<LanguageInfo> GetLanguages()
{
    return _languageProvider.GetLanguages();
}

// 获取当前语言,其实就是获取的 CultureInfo.CurrentUICulture.Name 的信息,而后去查询语言集合。
private LanguageInfo GetCurrentLanguage()
{
    var languages = _languageProvider.GetLanguages();
    
    // ... 省略了的代码
    var currentCultureName = CultureInfo.CurrentUICulture.Name;

    var currentLanguage = languages.FirstOrDefault(l => l.Name == currentCultureName);
    if (currentLanguage != null)
    {
        return currentLanguage;
    }
    
    // ... 省略了的代码
    
    return languages[0];
}

默认实现就是直接读取以前经过 Configuration 的 Languages 里面的数据。

在 Abp.Zero 模块还有两外一个实现,叫作 ApplicationLanguageProvider ,这个提供者则是从数据库表 ApplicationLanguage 获取的这些语言列表数据,而且这些语言信息还与租户有关,不一样的租户他所可以得到到的语言数据也不同。

public IReadOnlyList<LanguageInfo> GetLanguages()
{
    // 能够看到这里传入的当前登陆用户的租户 Id,经过这个参数去查询的语言表数据
    var languageInfos = AsyncHelper.RunSync(() => _applicationLanguageManager.GetLanguagesAsync(AbpSession.TenantId))
        .OrderBy(l => l.DisplayName)
        .Select(l => l.ToLanguageInfo())
        .ToList();

    SetDefaultLanguage(languageInfos);

    return languageInfos;
}

2.4 本地化资源

2.4.1 本地化资源列表

在多语言模块配置内部使用的是 ILocalizationSourceList 类型的一个 Sources 属性,该类型其实就是继承自 IList<ILocalizationSource> 的一个具体实现而已,一个类型为 ILocalizationSource 的集合,不过其扩展了一个

Extensions 属性用于存放扩展的多语言数据字段。

2.4.2 本地化资源

其接口定义为 ILocalizationSource ,Abp 默认为咱们实现了四种本地化资源的实现。

第一个是空实现,能够跳过,第二个则是针对资源文件进行读取的的本地化资源,第三个是基于字典的的本地化资源定义,最后一个是由 Abp Zero 模块所提供的数据库版本的多语言资源定义。

首先看一下该接口的定义:

public interface ILocalizationSource
{
    // 本地化资源惟一的名称
    string Name { get; }

    // 用于初始化本地化资源,在 Abp 框架初始化的时候被调用
    void Initialize(ILocalizationConfiguration configuration, IIocResolver iocResolver);

    // 从当前本地化资源中获取给定关键字的多语言文本项,为用户当前语言
    string GetString(string name);

    // 从当前本地化资源中获取给定关键字与区域文化的多语言文本项
    string GetString(string name, CultureInfo culture);

    // 做用同上,只不过不存在会返回 NULL
    string GetStringOrNull(string name, bool tryDefaults = true);

    // 做用同上,只不过不存在会返回 NULL
    string GetStringOrNull(string name, CultureInfo culture, bool tryDefaults = true);

    // 得到当前语言全部的多语言文本项集合
    IReadOnlyList<LocalizedString> GetAllStrings(bool includeDefaults = true);

    // 得到给定区域文化的全部多语言文本项集合
    IReadOnlyList<LocalizedString> GetAllStrings(CultureInfo culture, bool includeDefaults = true);
}

也就能够这么来看,咱们有几套本地化资源,他们经过 Name 来进行标识,若是你须要在本地化管理器获取某一套本地化资源,那么你能够直接经过 Name 来进行定位。而每一套本地化资源,自身都拥有具体的多语言数据,这些多语言数据有可能来自文件也有可能来自数据库,这取决于你具体的实现。

2.4.3 基于字典的本地化资源

最开始咱们在使用范例当中,经过 DictionaryBasedLocalizationSource 来创建咱们的本地化资源对象。该对象实现了 ILocalizationSourceIDictionaryBasedLocalizationSource 接口,内部定义了一个本地化资源字典提供器。

当调用本地化资源的 Initialize() 方法的时候,会使用具体的本地化资源字典提供器来获取数据,而这个字典提供器能够为 XmlFileLocalizationDictionaryProviderJsonEmbeddedFileLocalizationDictionaryProvider 等。

这些内部字典提供器在初始化的时候,会将自身的数据按照 语言/多语言项 的形式将多语言信息存放在一个字典之中,而这个字典又能够分为 XML、JSON 等等等等...

// 内部字典提供器
public interface ILocalizationDictionaryProvider
{
    // 语言/多语言项字典
    IDictionary<string, ILocalizationDictionary> Dictionaries { get; }

    // 本地化资源初始化时被调用
    void Initialize(string sourceName);
}

而这里的 ILocalizationDictionary 其实就是一个键值对,键关联的是多语言项的标识 KEY,例如 "Home",而 Value 就是具体的展现文本信息了。

而是用字典本地化资源对象获取数据的时候,其实也就是从其内部的字典提供器来获取数据。

例如本地化资源有一个 GetString() 方法,它内部拥有一个字典提供器 DictionaryProvider,我要获取某个 KEY 为 "Home" 所须要通过的步骤以下。

public ILocalizationDictionaryProvider DictionaryProvider { get; }

public string GetString(string name)
{
    // 获取当前用户区域文化,标识为 "Home" 的展现文本
    return GetString(name, CultureInfo.CurrentUICulture);
}

public string GetString(string name, CultureInfo culture)
{
    // 获取值
    var value = GetStringOrNull(name, culture);

    // 判断值为空的话,根据配置的要求是否抛出异常
    if (value == null)
    {
        return ReturnGivenNameOrThrowException(name, culture);
    }

    return value;
}

// 得到 KEY 关联的文本
public string GetStringOrNull(string name, CultureInfo culture, bool tryDefaults = true)
{
    var cultureName = culture.Name;
    var dictionaries = DictionaryProvider.Dictionaries;

    // 在这里就开始从初始化所加载完成的语言字典里面,获取具体的多语言项字典
    ILocalizationDictionary originalDictionary;
    if (dictionaries.TryGetValue(cultureName, out originalDictionary))
    {
        // 多语言项字典拿取具体的多语言文本值
        var strOriginal = originalDictionary.GetOrNull(name);
        if (strOriginal != null)
        {
            return strOriginal.Value;
        }
    }

    if (!tryDefaults)
    {
        return null;
    }

    //Try to get from same language dictionary (without country code)
    if (cultureName.Contains("-")) //Example: "tr-TR" (length=5)
    {
        ILocalizationDictionary langDictionary;
        if (dictionaries.TryGetValue(GetBaseCultureName(cultureName), out langDictionary))
        {
            var strLang = langDictionary.GetOrNull(name);
            if (strLang != null)
            {
                return strLang.Value;
            }
        }
    }

    //Try to get from default language
    var defaultDictionary = DictionaryProvider.DefaultDictionary;
    if (defaultDictionary == null)
    {
        return null;
    }

    var strDefault = defaultDictionary.GetOrNull(name);
    if (strDefault == null)
    {
        return null;
    }

    return strDefault.Value;
}

2.3.4 基于数据库的本地化资源

若是你有集成 Abp.Zero 模块的话,能够经过在启动模块的预加载方法编写如下代码启用 Zero 的多语言机制。

Configuration.Modules.Zero().LanguageManagement.EnableDbLocalization();

Abp.Zero 针对原有的本地化资源进行了扩展,新增的本地化资源类叫作 MultiTenantLocalizationSource,该类同语言管理器同样,是一个基于多租户实现的本地化资源,内部字典的值是从数据库当中获取的,其大致逻辑与字典本地化资源同样,都是内部维护有一个字典提供器。

在经过 EnableDbLocalization() 方法的时候就直接替换掉了 ILanguageProvider 的默认实现,而且在配置的 Sources 源里面也增长了 MultiTenantLocalizationSource 做为一个本地化资源。

2.5 本地化资源管理器

扯了这么多,让咱们来看一下最为核心的 ILocalizationManager 接口,若是咱们须要获取某个数据源的某个 Key 所对应的多语言值确定是要注入这个本地化资源管理器来进行操做的。

public interface ILocalizationManager
{
    // 根据名称得到本地化数据源
    ILocalizationSource GetSource(string name);

    // 获取全部的本地化数据源
    IReadOnlyList<ILocalizationSource> GetAllSources();
}

这里的数据源标识的就是一个命名空间的做用,好比我在 A 模块当中有一个 Key 为 "Home" 的多语言项,在 B 模块也有一个 Key 为 "Home" 的多语言项,这个时候就能够用数据源标识来区分这两个 "Home"

本地化资源管理器经过在初始化的时候调用其 Initialize() 来初始化全部被注入的本地化资源,最后并将其放在一个字典之中,以便后续使用。

private readonly IDictionary<string, ILocalizationSource> _sources;

foreach (var source in _configuration.Sources)
{
    // ... 其余代码
    _sources[source.Name] = source;
    source.Initialize(_configuration, _iocResolver);
    
    // ... 其余代码
}

3.结语

针对 Abp 的多语言处理本篇文章不太适合做为入门了解,其中大部分知识须要结合 Abp 源码进行阅读才可以加深理解,此文仅做抛砖引玉之用,若有任何意见或建议欢迎你们在评论当中指出。

4.点此跳转到总目录

相关文章
相关标签/搜索