在上篇文章中我们的重点是讲述怎样通过在Domain层通过PreInitialize()配置ILocalizationConfiguration中的Sources(IList<ILocalizationSource>)集合,并且在调用L方法中配置LocalizationSourceName从而找到之前配置的ILocalizationSource对象,这一节主要来讲述ILocalizationSource中的GetString(string name)方法,通过这个方法来体会ABP中的设计思想。
还是和之前一样我们首先来看看ILocalizationSource这个接口的定义。
/// <summary> /// A Localization Source is used to obtain localized strings. /// </summary> public interface ILocalizationSource { /// <summary> /// Unique Name of the source. /// </summary> string Name { get; } /// <summary> /// This method is called by ABP before first usage. /// </summary> void Initialize(ILocalizationConfiguration configuration, IIocResolver iocResolver); /// <summary> /// Gets localized string for given name in current language. /// Fallbacks to default language if not found in current culture. /// </summary> /// <param name="name">Key name</param> /// <returns>Localized string</returns> string GetString(string name); /// <summary> /// Gets localized string for given name and specified culture. /// Fallbacks to default language if not found in given culture. /// </summary> /// <param name="name">Key name</param> /// <param name="culture">culture information</param> /// <returns>Localized string</returns> string GetString(string name, CultureInfo culture); /// <summary> /// Gets localized string for given name in current language. /// Returns null if not found. /// </summary> /// <param name="name">Key name</param> /// <param name="tryDefaults"> /// True: Fallbacks to default language if not found in current culture. /// </param> /// <returns>Localized string</returns> string GetStringOrNull(string name, bool tryDefaults = true); /// <summary> /// Gets localized string for given name and specified culture. /// Returns null if not found. /// </summary> /// <param name="name">Key name</param> /// <param name="culture">culture information</param> /// <param name="tryDefaults"> /// True: Fallbacks to default language if not found in current culture. /// </param> /// <returns>Localized string</returns> string GetStringOrNull(string name, CultureInfo culture, bool tryDefaults = true); /// <summary> /// Gets all strings in current language. /// </summary> /// <param name="includeDefaults"> /// True: Fallbacks to default language texts if not found in current culture. /// </param> IReadOnlyList<LocalizedString> GetAllStrings(bool includeDefaults = true); /// <summary> /// Gets all strings in specified culture. /// </summary> /// <param name="culture">culture information</param> /// <param name="includeDefaults"> /// True: Fallbacks to default language texts if not found in current culture. /// </param> IReadOnlyList<LocalizedString> GetAllStrings(CultureInfo culture, bool includeDefaults = true); }
这个接口默认有四个实现:DictionaryBasedLocalizationSource、MultiTenantLocalizationSource、ResourceFileLocalizationSource、NullLocalizationSource这里我们重点来将我们项目中用到的DictionaryBasedLocalizationSource,这个我们也先来看看源码,热庵后再来做进一步的分析。
/// <summary> /// This class is used to build a localization source /// which works on memory based dictionaries to find strings. /// </summary> public class DictionaryBasedLocalizationSource : IDictionaryBasedLocalizationSource { /// <summary> /// Unique Name of the source. /// </summary> public string Name { get; } public ILocalizationDictionaryProvider DictionaryProvider { get; } protected ILocalizationConfiguration LocalizationConfiguration { get; private set; } private ILogger _logger; /// <summary> /// /// </summary> /// <param name="name"></param> /// <param name="dictionaryProvider"></param> public DictionaryBasedLocalizationSource(string name, ILocalizationDictionaryProvider dictionaryProvider) { Check.NotNullOrEmpty(name, nameof(name)); Check.NotNull(dictionaryProvider, nameof(dictionaryProvider)); Name = name; DictionaryProvider = dictionaryProvider; } /// <inheritdoc/> public virtual void Initialize(ILocalizationConfiguration configuration, IIocResolver iocResolver) { LocalizationConfiguration = configuration; _logger = iocResolver.IsRegistered(typeof(ILoggerFactory)) ? iocResolver.Resolve<ILoggerFactory>().Create(typeof(DictionaryBasedLocalizationSource)) : NullLogger.Instance; DictionaryProvider.Initialize(Name); } /// <inheritdoc/> public string GetString(string name) { return GetString(name, CultureInfo.CurrentUICulture); } /// <inheritdoc/> public string GetString(string name, CultureInfo culture) { var value = GetStringOrNull(name, culture); if (value == null) { return ReturnGivenNameOrThrowException(name, culture); } return value; } public string GetStringOrNull(string name, bool tryDefaults = true) { return GetStringOrNull(name, CultureInfo.CurrentUICulture, tryDefaults); } public string GetStringOrNull(string name, CultureInfo culture, bool tryDefaults = true) { var cultureName = culture.Name; var dictionaries = DictionaryProvider.Dictionaries; //Try to get from original dictionary (with country code) 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; } /// <inheritdoc/> public IReadOnlyList<LocalizedString> GetAllStrings(bool includeDefaults = true) { return GetAllStrings(CultureInfo.CurrentUICulture, includeDefaults); } /// <inheritdoc/> public IReadOnlyList<LocalizedString> GetAllStrings(CultureInfo culture, bool includeDefaults = true) { //TODO: Can be optimized (example: if it's already default dictionary, skip overriding) var dictionaries = DictionaryProvider.Dictionaries; //Create a temp dictionary to build var allStrings = new Dictionary<string, LocalizedString>(); if (includeDefaults) { //Fill all strings from default dictionary var defaultDictionary = DictionaryProvider.DefaultDictionary; if (defaultDictionary != null) { foreach (var defaultDictString in defaultDictionary.GetAllStrings()) { allStrings[defaultDictString.Name] = defaultDictString; } } //Overwrite all strings from the language based on country culture if (culture.Name.Contains("-")) { ILocalizationDictionary langDictionary; if (dictionaries.TryGetValue(GetBaseCultureName(culture.Name), out langDictionary)) { foreach (var langString in langDictionary.GetAllStrings()) { allStrings[langString.Name] = langString; } } } } //Overwrite all strings from the original dictionary ILocalizationDictionary originalDictionary; if (dictionaries.TryGetValue(culture.Name, out originalDictionary)) { foreach (var originalLangString in originalDictionary.GetAllStrings()) { allStrings[originalLangString.Name] = originalLangString; } } return allStrings.Values.ToImmutableList(); } /// <summary> /// Extends the source with given dictionary. /// </summary> /// <param name="dictionary">Dictionary to extend the source</param> public virtual void Extend(ILocalizationDictionary dictionary) { DictionaryProvider.Extend(dictionary); } protected virtual string ReturnGivenNameOrThrowException(string name, CultureInfo culture) { return LocalizationSourceHelper.ReturnGivenNameOrThrowException( LocalizationConfiguration, Name, name, culture, _logger ); } private static string GetBaseCultureName(string cultureName) { return cultureName.Contains("-") ? cultureName.Left(cultureName.IndexOf("-", StringComparison.Ordinal)) : cultureName; } }
在分析这个类之前,我们先来看看这个类的构造函数,分别传入了一个string类型的name和ILocalizationDictionaryProvider对象,这个name按照ABP的注释表示的是资源的名称,这个是不能够重名的,另外一个参数是ILocalizationDictionaryProvider一个对象,这个对象用于处理本地化文件是XML格式还是Json格式,每种格式都会实现这个接口,然后分别处理对应格式的数据,这个接口稍后会进行分析。
在这个类构造完成以后会调用其中的Initialize(ILocalizationConfiguration configuration, IIocResolver iocResolver)方法,用于向当前对象中传递本地化配置信息以及IIocResolver对象。并在这里面调用ILocalizationDictionaryProvider的Initialize方法。那么我们就来具体分析一下这个ILocalizationDictionaryProvider接口吧。
首先看接口定义
/// <summary> /// Used to get localization dictionaries (<see cref="ILocalizationDictionary"/>) /// for a <see cref="IDictionaryBasedLocalizationSource"/>. /// </summary> public interface ILocalizationDictionaryProvider { ILocalizationDictionary DefaultDictionary { get; } IDictionary<string, ILocalizationDictionary> Dictionaries { get; } void Initialize(string sourceName); void Extend(ILocalizationDictionary dictionary); }
这个里面又定义了一个ILocalizationDictionary的接口,我们先来看看在我们的项目中添加的JsonEmbeddedFileLocalizationDictionaryProvider这个类里面的实现,这个类首先继承自一个LocalizationDictionaryProviderBase的抽象基类。
public abstract class LocalizationDictionaryProviderBase : ILocalizationDictionaryProvider { public string SourceName { get; private set; } public ILocalizationDictionary DefaultDictionary { get; protected set; } public IDictionary<string, ILocalizationDictionary> Dictionaries { get; private set; } protected LocalizationDictionaryProviderBase() { Dictionaries = new Dictionary<string, ILocalizationDictionary>(); } public virtual void Initialize(string sourceName) { SourceName = sourceName; } public void Extend(ILocalizationDictionary dictionary) { //Add ILocalizationDictionary existingDictionary; if (!Dictionaries.TryGetValue(dictionary.CultureInfo.Name, out existingDictionary)) { Dictionaries[dictionary.CultureInfo.Name] = dictionary; return; } //Override var localizedStrings = dictionary.GetAllStrings(); foreach (var localizedString in localizedStrings) { existingDictionary[localizedString.Name] = localizedString.Value; } } }
这个抽象基类里面主要定义一些常用的公共的属性以及一个公用的Extend方法。
/// <summary> /// Provides localization dictionaries from JSON files embedded into an <see cref="Assembly"/>. /// </summary> public class JsonEmbeddedFileLocalizationDictionaryProvider : LocalizationDictionaryProviderBase { private readonly Assembly _assembly; private readonly string _rootNamespace; /// <summary> /// Creates a new <see cref="JsonEmbeddedFileLocalizationDictionaryProvider"/> object. /// </summary> /// <param name="assembly">Assembly that contains embedded json files</param> /// <param name="rootNamespace"> /// <para> /// Namespace of the embedded json dictionary files /// </para> /// <para> /// Notice : Json folder name is different from Xml folder name. /// </para> /// <para> /// You must name it like this : Json**** and Xml****; Do not name : ****Json and ****Xml /// </para> /// </param> public JsonEmbeddedFileLocalizationDictionaryProvider(Assembly assembly, string rootNamespace) { _assembly = assembly; _rootNamespace = rootNamespace; } public override void Initialize(string sourceName) { var allCultureInfos = CultureInfo.GetCultures(CultureTypes.AllCultures); var resourceNames = _assembly.GetManifestResourceNames().Where(resouceName => allCultureInfos.Any(culture => resouceName.EndsWith($"{sourceName}.json", true, null) || resouceName.EndsWith($"{sourceName}-{culture.Name}.json", true, null))).ToList(); foreach (var resourceName in resourceNames) { if (resourceName.StartsWith(_rootNamespace)) { using (var stream = _assembly.GetManifestResourceStream(resourceName)) { var jsonString = Utf8Helper.ReadStringFromStream(stream); var dictionary = CreateJsonLocalizationDictionary(jsonString); if (Dictionaries.ContainsKey(dictionary.CultureInfo.Name)) { throw new AbpInitializationException(sourceName + " source contains more than one dictionary for the culture: " + dictionary.CultureInfo.Name); } Dictionaries[dictionary.CultureInfo.Name] = dictionary; if (resourceName.EndsWith(sourceName + ".json")) { if (DefaultDictionary != null) { throw new AbpInitializationException("Only one default localization dictionary can be for source: " + sourceName); } DefaultDictionary = dictionary; } } } } } protected virtual JsonLocalizationDictionary CreateJsonLocalizationDictionary(string jsonString) { return JsonLocalizationDictionary.BuildFromJsonString(jsonString); } }
这个类的构造函数传入的是:Assembly assembly, string rootNamespace这个我们看命名就应该知道,第一个参数指的是包含嵌入式json file的程序集,第二个代表当前根命名空间。在这个类里面重点是Initialize方法,这个方法会循环遍历所有json格式的本地化的文件,然后读取每一个Json文件中定义的JsonString,然后通过JsonLocalizationDictionary类中的静态方法将JsonString反序列化为一个JsonLocalizationFile对象,我们也来看看这段代码的实现。
/// <summary> /// This class is used to build a localization dictionary from json. /// </summary> /// <remarks> /// Use static Build methods to create instance of this class. /// </remarks> public class JsonLocalizationDictionary : LocalizationDictionary { /// <summary> /// Private constructor. /// </summary> /// <param name="cultureInfo">Culture of the dictionary</param> private JsonLocalizationDictionary(CultureInfo cultureInfo) : base(cultureInfo) { } /// <summary> /// Builds an <see cref="JsonLocalizationDictionary" /> from given file. /// </summary> /// <param name="filePath">Path of the file</param> public static JsonLocalizationDictionary BuildFromFile(string filePath) { try { return BuildFromJsonString(File.ReadAllText(filePath)); } catch (Exception ex) { throw new AbpException("Invalid localization file format! " + filePath, ex); } } /// <summary> /// Builds an <see cref="JsonLocalizationDictionary" /> from given json string. /// </summary> /// <param name="jsonString">Json string</param> public static JsonLocalizationDictionary BuildFromJsonString(string jsonString) { JsonLocalizationFile jsonFile; try { jsonFile = JsonConvert.DeserializeObject<JsonLocalizationFile>( jsonString, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); } catch (JsonException ex) { throw new AbpException("Can not parse json string. " + ex.Message); } var cultureCode = jsonFile.Culture; if (string.IsNullOrEmpty(cultureCode)) { throw new AbpException("Culture is empty in language json file."); } var dictionary = new JsonLocalizationDictionary(CultureInfo.GetCultureInfo(cultureCode)); var dublicateNames = new List<string>(); foreach (var item in jsonFile.Texts) { if (string.IsNullOrEmpty(item.Key)) { throw new AbpException("The key is empty in given json string."); } if (dictionary.Contains(item.Key)) { dublicateNames.Add(item.Key); } dictionary[item.Key] = item.Value.NormalizeLineEndings(); } if (dublicateNames.Count > 0) { throw new AbpException( "A dictionary can not contain same key twice. There are some duplicated names: " + dublicateNames.JoinAsString(", ")); } return dictionary; } }
当然这里面也会做一些类似于Key不能重复等一系列的操作,具体的实现我们来看代码。到这里为止我们已经明白了到底通过怎样的方式来获取到Json文件中的所有的信息并最终保存在JsonLocalizationDictionary,有了这个Dictionary我们就能够顺理成章的通过Key值来获取对应的value了。
最后,点击这里返回整个ABP系列的主目录。