From 1af65651e14c28884fa5fd7b03b0924d3ce7c00a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 17 Oct 2017 15:47:47 +0300 Subject: [PATCH] Initial json localization implementation. --- src/Volo.Abp.TestBase/AbpIntegratedTest.cs | 5 +- src/Volo.Abp.TestBase/AbpTestBaseModule.cs | 9 + .../AbpTestBaseWithServiceProvider.cs | 10 + src/Volo.Abp/Volo.Abp.csproj | 1 + src/Volo.Abp/Volo/Abp/AbpKernelModule.cs | 4 + src/Volo.Abp/Volo/Abp/Internal/Utf8Helper.cs | 30 +++ .../AbpCultureHelper.cs | 2 +- .../AbpDictionaryBasedStringLocalizer.cs | 173 ++++++++++++++++++ .../Localization/AbpLocalizationOptions.cs | 13 ++ .../Localization/AbpStringLocalizerFactory.cs | 76 ++++++++ .../Localization/AbpStringLocalizerList.cs | 8 + .../Abp/Localization/IAbpStringLocalizer.cs | 6 + .../Localization/ILocalizationDictionary.cs | 31 ++++ .../ILocalizationDictionaryProvider.cs | 13 ++ ...eddedFileLocalizationDictionaryProvider.cs | 48 +++++ .../JsonFileLocalizationDictionaryProvider.cs | 54 ++++++ .../Json/JsonLocalizationDictionary.cs | 97 ++++++++++ .../Localization/Json/JsonLocalizationFile.cs | 19 ++ .../Volo/Abp/Localization/LocalString.cs | 15 ++ .../Localization/LocalizationDictionary.cs | 68 +++++++ .../LocalizationDictionaryProviderBase.cs | 41 +++++ .../Abp/Localization/LocalizationResource.cs | 39 ++++ .../Localization/LocalizationResourceList.cs | 9 + .../LocalizationResourceListExtensions.cs | 24 +++ .../Mvc/Localization/MvcLocalization_Tests.cs | 2 +- .../System/StringExtensions_Tests.cs | 2 +- test/Volo.Abp.Tests/Volo.Abp.Tests.csproj | 10 + .../DependencyInjection_Tests.cs | 108 +++++++++++ .../Abp/Localization/AbpLocalization_Tests.cs | 56 ++++++ .../Source/LocalizationTestResource.cs | 7 + .../Volo/Abp/Localization/Source/en.json | 8 + .../Volo/Abp/Localization/Source/tr.json | 8 + 32 files changed, 990 insertions(+), 6 deletions(-) create mode 100644 src/Volo.Abp.TestBase/AbpTestBaseModule.cs create mode 100644 src/Volo.Abp/Volo/Abp/Internal/Utf8Helper.cs rename src/Volo.Abp/Volo/Abp/{Globalization => Localization}/AbpCultureHelper.cs (96%) create mode 100644 src/Volo.Abp/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs create mode 100644 src/Volo.Abp/Volo/Abp/Localization/AbpLocalizationOptions.cs create mode 100644 src/Volo.Abp/Volo/Abp/Localization/AbpStringLocalizerFactory.cs create mode 100644 src/Volo.Abp/Volo/Abp/Localization/AbpStringLocalizerList.cs create mode 100644 src/Volo.Abp/Volo/Abp/Localization/IAbpStringLocalizer.cs create mode 100644 src/Volo.Abp/Volo/Abp/Localization/ILocalizationDictionary.cs create mode 100644 src/Volo.Abp/Volo/Abp/Localization/ILocalizationDictionaryProvider.cs create mode 100644 src/Volo.Abp/Volo/Abp/Localization/Json/JsonEmbeddedFileLocalizationDictionaryProvider.cs create mode 100644 src/Volo.Abp/Volo/Abp/Localization/Json/JsonFileLocalizationDictionaryProvider.cs create mode 100644 src/Volo.Abp/Volo/Abp/Localization/Json/JsonLocalizationDictionary.cs create mode 100644 src/Volo.Abp/Volo/Abp/Localization/Json/JsonLocalizationFile.cs create mode 100644 src/Volo.Abp/Volo/Abp/Localization/LocalString.cs create mode 100644 src/Volo.Abp/Volo/Abp/Localization/LocalizationDictionary.cs create mode 100644 src/Volo.Abp/Volo/Abp/Localization/LocalizationDictionaryProviderBase.cs create mode 100644 src/Volo.Abp/Volo/Abp/Localization/LocalizationResource.cs create mode 100644 src/Volo.Abp/Volo/Abp/Localization/LocalizationResourceList.cs create mode 100644 src/Volo.Abp/Volo/Abp/Localization/LocalizationResourceListExtensions.cs create mode 100644 test/Volo.Abp.Tests/Volo/Abp/DependencyInjection/DependencyInjection_Tests.cs create mode 100644 test/Volo.Abp.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs create mode 100644 test/Volo.Abp.Tests/Volo/Abp/Localization/Source/LocalizationTestResource.cs create mode 100644 test/Volo.Abp.Tests/Volo/Abp/Localization/Source/en.json create mode 100644 test/Volo.Abp.Tests/Volo/Abp/Localization/Source/tr.json diff --git a/src/Volo.Abp.TestBase/AbpIntegratedTest.cs b/src/Volo.Abp.TestBase/AbpIntegratedTest.cs index d65cddfb2e..7832e384b5 100644 --- a/src/Volo.Abp.TestBase/AbpIntegratedTest.cs +++ b/src/Volo.Abp.TestBase/AbpIntegratedTest.cs @@ -4,7 +4,7 @@ using Volo.Abp.Modularity; namespace Volo.Abp.TestBase { - public class AbpIntegratedTest : AbpTestBaseWithServiceProvider, IDisposable + public abstract class AbpIntegratedTest : AbpTestBaseWithServiceProvider, IDisposable where TStartupModule : IAbpModule { protected IAbpApplication Application { get; } @@ -54,8 +54,7 @@ namespace Volo.Abp.TestBase { return services.BuildServiceProviderFromFactory(); } - - + public void Dispose() { Application.Shutdown(); diff --git a/src/Volo.Abp.TestBase/AbpTestBaseModule.cs b/src/Volo.Abp.TestBase/AbpTestBaseModule.cs new file mode 100644 index 0000000000..859ccb3e72 --- /dev/null +++ b/src/Volo.Abp.TestBase/AbpTestBaseModule.cs @@ -0,0 +1,9 @@ +using Volo.Abp.Modularity; + +namespace Volo.Abp.TestBase +{ + public class AbpTestBaseModule : AbpModule + { + + } +} diff --git a/src/Volo.Abp.TestBase/AbpTestBaseWithServiceProvider.cs b/src/Volo.Abp.TestBase/AbpTestBaseWithServiceProvider.cs index 6c0ecc066d..7d3fd77143 100644 --- a/src/Volo.Abp.TestBase/AbpTestBaseWithServiceProvider.cs +++ b/src/Volo.Abp.TestBase/AbpTestBaseWithServiceProvider.cs @@ -88,5 +88,15 @@ namespace Volo.Abp.TestBase } } } + + protected virtual T GetService() + { + return ServiceProvider.GetService(); + } + + protected virtual T GetRequiredService() + { + return ServiceProvider.GetRequiredService(); + } } } \ No newline at end of file diff --git a/src/Volo.Abp/Volo.Abp.csproj b/src/Volo.Abp/Volo.Abp.csproj index 583f40b460..5022b66a8f 100644 --- a/src/Volo.Abp/Volo.Abp.csproj +++ b/src/Volo.Abp/Volo.Abp.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Volo.Abp/Volo/Abp/AbpKernelModule.cs b/src/Volo.Abp/Volo/Abp/AbpKernelModule.cs index ad65a649b1..ac8a81416a 100644 --- a/src/Volo.Abp/Volo/Abp/AbpKernelModule.cs +++ b/src/Volo.Abp/Volo/Abp/AbpKernelModule.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.ApiVersioning; +using Volo.Abp.Localization; using Volo.Abp.Modularity; using Volo.Abp.ObjectMapping; using Volo.Abp.Reflection; @@ -32,6 +33,9 @@ namespace Volo.Abp { services.AddOptions(); services.AddLogging(); + services.AddLocalization(); + + AbpStringLocalizerFactory.Replace(services); services.AddAssemblyOf(); diff --git a/src/Volo.Abp/Volo/Abp/Internal/Utf8Helper.cs b/src/Volo.Abp/Volo/Abp/Internal/Utf8Helper.cs new file mode 100644 index 0000000000..b7e7297e68 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Internal/Utf8Helper.cs @@ -0,0 +1,30 @@ +using System.IO; +using System.Text; + +namespace Volo.Abp.Internal +{ + internal static class Utf8Helper + { + public static string ReadStringFromStream(Stream stream) + { + var bytes = stream.GetAllBytes(); + var skipCount = HasBom(bytes) ? 3 : 0; + return Encoding.UTF8.GetString(bytes, skipCount, bytes.Length - skipCount); + } + + private static bool HasBom(byte[] bytes) + { + if (bytes.Length < 3) + { + return false; + } + + if (!(bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF)) + { + return false; + } + + return true; + } + } +} diff --git a/src/Volo.Abp/Volo/Abp/Globalization/AbpCultureHelper.cs b/src/Volo.Abp/Volo/Abp/Localization/AbpCultureHelper.cs similarity index 96% rename from src/Volo.Abp/Volo/Abp/Globalization/AbpCultureHelper.cs rename to src/Volo.Abp/Volo/Abp/Localization/AbpCultureHelper.cs index 6ed492f30e..ba75ab66ea 100644 --- a/src/Volo.Abp/Volo/Abp/Globalization/AbpCultureHelper.cs +++ b/src/Volo.Abp/Volo/Abp/Localization/AbpCultureHelper.cs @@ -2,7 +2,7 @@ using System.Globalization; using JetBrains.Annotations; -namespace Volo.Abp.Globalization +namespace Volo.Abp.Localization { public static class AbpCultureHelper { diff --git a/src/Volo.Abp/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs b/src/Volo.Abp/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs new file mode 100644 index 0000000000..4b848aa832 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using Microsoft.Extensions.Localization; + +namespace Volo.Abp.Localization +{ + //TODO: Remove old/unused methods! + + public class AbpDictionaryBasedStringLocalizer : IStringLocalizer + { + public LocalizationResource Resource { get; } + + public AbpDictionaryBasedStringLocalizer(LocalizationResource resource) + { + Resource = resource; + } + + public IEnumerable GetAllStrings(bool includeParentCultures) + { + return GetAllStrings(CultureInfo.CurrentUICulture.Name, includeParentCultures); + } + + public IStringLocalizer WithCulture(CultureInfo culture) + { + throw new NotImplementedException(); + } + + LocalizedString IStringLocalizer.this[string name] + { + get { return GetString(name); } + } + + LocalizedString IStringLocalizer.this[string name, params object[] arguments] + { + get + { + var localizedString = GetString(name); + return new LocalizedString(name, string.Format(localizedString.Value, arguments, localizedString.ResourceNotFound, localizedString.SearchedLocation)); + } + } + + public LocalizedString GetString(string name) + { + return GetString(name, CultureInfo.CurrentUICulture.Name); + } + + /// + public LocalizedString GetString(string name, string cultureName) + { + var value = GetStringOrNull(name, cultureName); + + if (value == null) + { + return new LocalizedString(name, name, true); + } + + return value; + } + + public LocalizedString GetStringOrNull(string name, bool tryDefaults = true) + { + return GetStringOrNull(name, CultureInfo.CurrentUICulture.Name, tryDefaults); + } + + public LocalizedString GetStringOrNull(string name, string cultureName, bool tryDefaults = true) + { + var dictionaries = Resource.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 new LocalizedString(name, 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 new LocalizedString(name, strLang.Value); + } + } + } + + //Try to get from default language + var defaultDictionary = Resource.DictionaryProvider.Dictionaries[Resource.DefaultCultureName]; //TODO: What if not contains a default dictionary? + if (defaultDictionary == null) + { + return null; + } + + var strDefault = defaultDictionary.GetOrNull(name); + if (strDefault == null) + { + return null; + } + + return new LocalizedString(name, strDefault.Value); + } + + /// + public IReadOnlyList GetAllStrings(string cultureName, bool includeDefaults = true) + { + //TODO: Can be optimized (example: if it's already default dictionary, skip overriding) + + var dictionaries = Resource.DictionaryProvider.Dictionaries; + + //Create a temp dictionary to build + var allStrings = new Dictionary(); + + if (includeDefaults) + { + //Fill all strings from default dictionary + var defaultDictionary = Resource.DictionaryProvider.Dictionaries[Resource.DefaultCultureName]; //TODO: What if not contains a default dictionary? + if (defaultDictionary != null) + { + foreach (var defaultDictString in defaultDictionary.GetAllStrings()) + { + allStrings[defaultDictString.Name] = new LocalizedString(defaultDictString.Name, defaultDictString.Value); + } + } + + //Overwrite all strings from the language based on country culture + if (cultureName.Contains("-")) + { + ILocalizationDictionary langDictionary; + if (dictionaries.TryGetValue(GetBaseCultureName(cultureName), out langDictionary)) + { + foreach (var langString in langDictionary.GetAllStrings()) + { + allStrings[langString.Name] = new LocalizedString(langString.Name, langString.Value); + } + } + } + } + + //Overwrite all strings from the original dictionary + ILocalizationDictionary originalDictionary; + if (dictionaries.TryGetValue(cultureName, out originalDictionary)) + { + foreach (var originalLangString in originalDictionary.GetAllStrings()) + { + allStrings[originalLangString.Name] = new LocalizedString(originalLangString.Name, originalLangString.Value); + } + } + + return allStrings.Values.ToImmutableList(); + } + + private static string GetBaseCultureName(string cultureName) + { + return cultureName.Contains("-") + ? cultureName.Left(cultureName.IndexOf("-", StringComparison.Ordinal)) + : cultureName; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Localization/AbpLocalizationOptions.cs b/src/Volo.Abp/Volo/Abp/Localization/AbpLocalizationOptions.cs new file mode 100644 index 0000000000..42e5d9ef3f --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Localization/AbpLocalizationOptions.cs @@ -0,0 +1,13 @@ +namespace Volo.Abp.Localization +{ + public class AbpLocalizationOptions + { + public LocalizationResourceList Resources { get; } + public AbpStringLocalizerList Resolvers { get; } + + public AbpLocalizationOptions() + { + Resources = new LocalizationResourceList(); + } + } +} diff --git a/src/Volo.Abp/Volo/Abp/Localization/AbpStringLocalizerFactory.cs b/src/Volo.Abp/Volo/Abp/Localization/AbpStringLocalizerFactory.cs new file mode 100644 index 0000000000..285aa7b6ee --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Localization/AbpStringLocalizerFactory.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using Volo.Abp.Localization.Json; + +namespace Volo.Abp.Localization +{ + public class AbpStringLocalizerFactory : IStringLocalizerFactory + { + private readonly ResourceManagerStringLocalizerFactory _innerFactory; + private readonly AbpLocalizationOptions _abpLocalizationOptions; + private readonly IServiceProvider _serviceProvider; + + private readonly ConcurrentDictionary _localizerCache; + + //TODO: It's better to use decorator pattern for IStringLocalizerFactory instead of getting ResourceManagerStringLocalizerFactory as a dependency. + public AbpStringLocalizerFactory( + ResourceManagerStringLocalizerFactory innerFactory, + IOptions abpLocalizationOptions, IServiceProvider serviceProvider) + { + _innerFactory = innerFactory; + _serviceProvider = serviceProvider; + _abpLocalizationOptions = abpLocalizationOptions.Value; + + _localizerCache = new ConcurrentDictionary();; + } + + public virtual IStringLocalizer Create(Type resourceSource) + { + //TODO: Optimize! + + var localizationResource = _abpLocalizationOptions.Resources.FirstOrDefault(l => l.ResourceType == resourceSource); + if (localizationResource == null) + { + return _innerFactory.Create(resourceSource); + } + + return _localizerCache.GetOrAdd(resourceSource, _ => CreateAbpStringLocalizer(localizationResource)); + } + + private AbpDictionaryBasedStringLocalizer CreateAbpStringLocalizer(LocalizationResource resource) + { + resource.Initialize(_serviceProvider); + + //Use JSON/XML/...etc based provider that reads resource from source and creates a dictionary + //Extend dictionary with extensions + //Wrap reader by wrappers (like db wrapper which implement multitenancy/regions and so on...) + + + //Notes: Localizer will be cached, so wrappers are responsible to cache/invalidate themselves! + + var localizer = new AbpDictionaryBasedStringLocalizer(resource); //TODO: !!! + + //TODO: Wrap with DB provider or other premium sources + + return localizer; + } + + public virtual IStringLocalizer Create(string baseName, string location) + { + //TODO: Investigate when this is called? + + return _innerFactory.Create(baseName, location); + } + + internal static void Replace(IServiceCollection services) + { + services.Replace(ServiceDescriptor.Singleton()); + services.AddSingleton(); + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Localization/AbpStringLocalizerList.cs b/src/Volo.Abp/Volo/Abp/Localization/AbpStringLocalizerList.cs new file mode 100644 index 0000000000..80e855c876 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Localization/AbpStringLocalizerList.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Localization +{ + public class AbpStringLocalizerList : List + { + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Localization/IAbpStringLocalizer.cs b/src/Volo.Abp/Volo/Abp/Localization/IAbpStringLocalizer.cs new file mode 100644 index 0000000000..954e1ae984 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Localization/IAbpStringLocalizer.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.Localization +{ + public interface IAbpStringLocalizer + { + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Localization/ILocalizationDictionary.cs b/src/Volo.Abp/Volo/Abp/Localization/ILocalizationDictionary.cs new file mode 100644 index 0000000000..8563400781 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Localization/ILocalizationDictionary.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Localization +{ + /// + /// Represents a dictionary that is used to find a localized string. + /// + public interface ILocalizationDictionary + { + string CultureName { get; } + + /// + /// Gets/sets a string for this dictionary with given name (key). + /// + /// Name to get/set + LocalString this[string name] { get; set; } + + /// + /// Gets a for given . + /// + /// Name (key) to get localized string + /// The localized string or null if not found in this dictionary + LocalString GetOrNull(string name); + + /// + /// Gets a list of all strings in this dictionary. + /// + /// List of all object + IReadOnlyList GetAllStrings(); + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Localization/ILocalizationDictionaryProvider.cs b/src/Volo.Abp/Volo/Abp/Localization/ILocalizationDictionaryProvider.cs new file mode 100644 index 0000000000..1da13bcafb --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Localization/ILocalizationDictionaryProvider.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Localization +{ + public interface ILocalizationDictionaryProvider + { + IDictionary Dictionaries { get; } + + void Initialize(LocalizationResource resource); + + void Extend(ILocalizationDictionary dictionary); + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Localization/Json/JsonEmbeddedFileLocalizationDictionaryProvider.cs b/src/Volo.Abp/Volo/Abp/Localization/Json/JsonEmbeddedFileLocalizationDictionaryProvider.cs new file mode 100644 index 0000000000..d374fda2cf --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Localization/Json/JsonEmbeddedFileLocalizationDictionaryProvider.cs @@ -0,0 +1,48 @@ +using System.Reflection; +using Volo.Abp.Internal; + +namespace Volo.Abp.Localization.Json +{ + /// + /// Provides localization dictionaries from JSON files embedded into an . + /// + public class JsonEmbeddedFileLocalizationDictionaryProvider : LocalizationDictionaryProviderBase + { + private readonly Assembly _assembly; + private readonly string _rootNamespace; + + public JsonEmbeddedFileLocalizationDictionaryProvider(Assembly assembly, string rootNamespace) + { + _assembly = assembly; + _rootNamespace = rootNamespace; + } + + public override void Initialize(LocalizationResource resource) + { + var resourceNames = _assembly.GetManifestResourceNames(); + 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.CultureName)) + { + throw new AbpException(resource.ResourceType.FullName + " source contains more than one dictionary for the culture: " + dictionary.CultureName); + } + + Dictionaries[dictionary.CultureName] = dictionary; + } + } + } + } + + protected virtual JsonLocalizationDictionary CreateJsonLocalizationDictionary(string jsonString) + { + return JsonLocalizationDictionary.BuildFromJsonString(jsonString); + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Localization/Json/JsonFileLocalizationDictionaryProvider.cs b/src/Volo.Abp/Volo/Abp/Localization/Json/JsonFileLocalizationDictionaryProvider.cs new file mode 100644 index 0000000000..8c56a53ca3 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Localization/Json/JsonFileLocalizationDictionaryProvider.cs @@ -0,0 +1,54 @@ +//using System.IO; +//using Abp.Localization.Dictionaries.Xml; +//using Abp.Localization.Sources; + +//namespace Abp.Localization.Dictionaries.Json +//{ +// /// +// /// Provides localization dictionaries from json files in a directory. +// /// +// public class JsonFileLocalizationDictionaryProvider : LocalizationDictionaryProviderBase +// { +// private readonly string _directoryPath; + +// /// +// /// Creates a new . +// /// +// /// Path of the dictionary that contains all related XML files +// public JsonFileLocalizationDictionaryProvider(string directoryPath) +// { +// _directoryPath = directoryPath; +// } + +// public override void Initialize(string sourceName) +// { +// var fileNames = Directory.GetFiles(_directoryPath, "*.json", SearchOption.TopDirectoryOnly); + +// foreach (var fileName in fileNames) +// { +// var dictionary = CreateJsonLocalizationDictionary(fileName); +// 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 (fileName.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 fileName) +// { +// return JsonLocalizationDictionary.BuildFromFile(fileName); +// } +// } +//} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Localization/Json/JsonLocalizationDictionary.cs b/src/Volo.Abp/Volo/Abp/Localization/Json/JsonLocalizationDictionary.cs new file mode 100644 index 0000000000..7a13c5eb05 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Localization/Json/JsonLocalizationDictionary.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Volo.Abp.Localization.Json +{ + /// + /// This class is used to build a localization dictionary from json. + /// + /// + /// Use static Build methods to create instance of this class. + /// + public class JsonLocalizationDictionary : LocalizationDictionary + { + /// + /// Private constructor. + /// + /// Culture of the dictionary + private JsonLocalizationDictionary(string cultureName) + : base(cultureName) + { + } + + /// + /// Builds an from given file. + /// + /// Path of the file + public static JsonLocalizationDictionary BuildFromFile(string filePath) + { + try + { + return BuildFromJsonString(File.ReadAllText(filePath)); + } + catch (Exception ex) + { + throw new AbpException("Invalid localization file format! " + filePath, ex); + } + } + + /// + /// Builds an from given json string. + /// + /// Json string + public static JsonLocalizationDictionary BuildFromJsonString(string jsonString) + { + JsonLocalizationFile jsonFile; + try + { + jsonFile = JsonConvert.DeserializeObject( + 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(cultureCode); + var dublicateNames = new List(); + 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] = new LocalString(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; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Localization/Json/JsonLocalizationFile.cs b/src/Volo.Abp/Volo/Abp/Localization/Json/JsonLocalizationFile.cs new file mode 100644 index 0000000000..c2dc911532 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Localization/Json/JsonLocalizationFile.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Localization.Json +{ + public class JsonLocalizationFile + { + /// + /// Culture name; eg : en , en-us, zh-CN + /// + public string Culture { get; set; } + + public Dictionary Texts { get; } + + public JsonLocalizationFile() + { + Texts = new Dictionary(); + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Localization/LocalString.cs b/src/Volo.Abp/Volo/Abp/Localization/LocalString.cs new file mode 100644 index 0000000000..130e825a53 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Localization/LocalString.cs @@ -0,0 +1,15 @@ +namespace Volo.Abp.Localization +{ + public class LocalString + { + public string Name { get; set; } + + public string Value { get; set; } + + public LocalString(string name, string value) + { + Name = name; + Value = value; + } + } +} diff --git a/src/Volo.Abp/Volo/Abp/Localization/LocalizationDictionary.cs b/src/Volo.Abp/Volo/Abp/Localization/LocalizationDictionary.cs new file mode 100644 index 0000000000..7bb7217549 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Localization/LocalizationDictionary.cs @@ -0,0 +1,68 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; + +namespace Volo.Abp.Localization +{ + /// + /// Represents a simple implementation of interface. + /// + public class LocalizationDictionary : ILocalizationDictionary, IEnumerable + { + /// + public string CultureName { get; private set; } + + /// + public virtual LocalString this[string name] + { + get + { + var localizedString = GetOrNull(name); + return localizedString == null ? null : localizedString; + } + set { _dictionary[name] = value; } + } + + private readonly Dictionary _dictionary; + + /// + /// Creates a new object. + /// + /// Culture of the dictionary + public LocalizationDictionary(string cultureName) + { + CultureName = cultureName; + _dictionary = new Dictionary(); + } + + /// + public virtual LocalString GetOrNull(string name) + { + LocalString localizedString; + return _dictionary.TryGetValue(name, out localizedString) ? localizedString : null; + } + + /// + public virtual IReadOnlyList GetAllStrings() + { + return _dictionary.Values.ToImmutableList(); + } + + /// + public virtual IEnumerator GetEnumerator() + { + return GetAllStrings().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetAllStrings().GetEnumerator(); + } + + protected bool Contains(string name) + { + return _dictionary.ContainsKey(name); + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Localization/LocalizationDictionaryProviderBase.cs b/src/Volo.Abp/Volo/Abp/Localization/LocalizationDictionaryProviderBase.cs new file mode 100644 index 0000000000..21dd462f28 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Localization/LocalizationDictionaryProviderBase.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Localization +{ + public abstract class LocalizationDictionaryProviderBase : ILocalizationDictionaryProvider + { + public LocalizationResource ResourceType { get; private set; } + + public ILocalizationDictionary DefaultDictionary { get; protected set; } + + public IDictionary Dictionaries { get; private set; } + + protected LocalizationDictionaryProviderBase() + { + Dictionaries = new Dictionary(); + } + + public virtual void Initialize(LocalizationResource resourceType) + { + ResourceType = resourceType; + } + + public void Extend(ILocalizationDictionary dictionary) + { + //Add + ILocalizationDictionary existingDictionary; + if (!Dictionaries.TryGetValue(dictionary.CultureName, out existingDictionary)) + { + Dictionaries[dictionary.CultureName] = dictionary; + return; + } + + //Override + var localizedStrings = dictionary.GetAllStrings(); + foreach (var localizedString in localizedStrings) + { + existingDictionary[localizedString.Name] = localizedString; + } + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Localization/LocalizationResource.cs b/src/Volo.Abp/Volo/Abp/Localization/LocalizationResource.cs new file mode 100644 index 0000000000..38e6120bb1 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Localization/LocalizationResource.cs @@ -0,0 +1,39 @@ +using System; +using JetBrains.Annotations; + +namespace Volo.Abp.Localization +{ + public class LocalizationResource + { + public Type ResourceType { get; } + + public string DefaultCultureName { get; set; } + + public ILocalizationDictionaryProvider DictionaryProvider + { + get => _dictionaryProvider; + set + { + Check.NotNull(value, nameof(value)); + _dictionaryProvider = value; + } + } + private ILocalizationDictionaryProvider _dictionaryProvider; + + public LocalizationResource([NotNull] Type resourceType, [NotNull] string defaultCultureName, [NotNull] ILocalizationDictionaryProvider dictionaryProvider) + { + Check.NotNull(resourceType, nameof(resourceType)); + Check.NotNull(defaultCultureName, nameof(defaultCultureName)); + Check.NotNull(dictionaryProvider, nameof(dictionaryProvider)); + + ResourceType = resourceType; + DefaultCultureName = defaultCultureName; + DictionaryProvider = dictionaryProvider; + } + + public virtual void Initialize(IServiceProvider serviceProvider) //TODO: Create a LocalizationResourceInitializationContext! + { + DictionaryProvider.Initialize(this); + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Localization/LocalizationResourceList.cs b/src/Volo.Abp/Volo/Abp/Localization/LocalizationResourceList.cs new file mode 100644 index 0000000000..331c1946be --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Localization/LocalizationResourceList.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Localization +{ + public class LocalizationResourceList : List + { + + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Localization/LocalizationResourceListExtensions.cs b/src/Volo.Abp/Volo/Abp/Localization/LocalizationResourceListExtensions.cs new file mode 100644 index 0000000000..f5166ae7f4 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Localization/LocalizationResourceListExtensions.cs @@ -0,0 +1,24 @@ +using JetBrains.Annotations; +using Volo.Abp.Localization.Json; + +namespace Volo.Abp.Localization +{ + public static class LocalizationResourceListExtensions + { + public static void AddJson(this LocalizationResourceList resourceList, [NotNull] string defaultCultureName) + { + var type = typeof(TResource); + + resourceList.Add( + new LocalizationResource( + type, + defaultCultureName, + new JsonEmbeddedFileLocalizationDictionaryProvider( + type.Assembly, + type.Namespace + ) + ) + ); + } + } +} \ No newline at end of file diff --git a/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/MvcLocalization_Tests.cs b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/MvcLocalization_Tests.cs index 8b16cfe8ed..c042734c87 100644 --- a/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/MvcLocalization_Tests.cs +++ b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/MvcLocalization_Tests.cs @@ -8,7 +8,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Localization { public class MvcLocalization_Tests : AspNetCoreMvcTestBase { - private readonly IStringLocalizer _localizer; + private readonly IStringLocalizer _localizer; public MvcLocalization_Tests() { diff --git a/test/Volo.Abp.Tests/System/StringExtensions_Tests.cs b/test/Volo.Abp.Tests/System/StringExtensions_Tests.cs index 0fc46257c4..853c2cbb83 100644 --- a/test/Volo.Abp.Tests/System/StringExtensions_Tests.cs +++ b/test/Volo.Abp.Tests/System/StringExtensions_Tests.cs @@ -1,5 +1,5 @@ using Shouldly; -using Volo.Abp.Globalization; +using Volo.Abp.Localization; using Xunit; namespace System diff --git a/test/Volo.Abp.Tests/Volo.Abp.Tests.csproj b/test/Volo.Abp.Tests/Volo.Abp.Tests.csproj index 6f578e566d..cd561f31e3 100644 --- a/test/Volo.Abp.Tests/Volo.Abp.Tests.csproj +++ b/test/Volo.Abp.Tests/Volo.Abp.Tests.csproj @@ -15,6 +15,16 @@ + + + + + + + + + + diff --git a/test/Volo.Abp.Tests/Volo/Abp/DependencyInjection/DependencyInjection_Tests.cs b/test/Volo.Abp.Tests/Volo/Abp/DependencyInjection/DependencyInjection_Tests.cs new file mode 100644 index 0000000000..7e823dc3b1 --- /dev/null +++ b/test/Volo.Abp.Tests/Volo/Abp/DependencyInjection/DependencyInjection_Tests.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Xunit; + +namespace Volo.Abp.DependencyInjection +{ + public class DependencyInjection_Tests + { + [Fact] + public void Singletons_Should_Resolve_Transients_Independent_From_Current_Scope() + { + //Arrange + + var services = new ServiceCollection(); + + services + .AddSingleton() + .AddTransient() + .AddTransient(); + + MySingletonService singletonService; + + using (var serviceProvider = services.BuildServiceProvider()) + { + //Act + + using (var scope = serviceProvider.CreateScope()) + { + scope.ServiceProvider.GetRequiredService().DoIt(); + scope.ServiceProvider.GetRequiredService().DoIt(); + } + + using (var scope = serviceProvider.CreateScope()) + { + scope.ServiceProvider.GetRequiredService().DoIt(); + scope.ServiceProvider.GetRequiredService().DoIt(); + scope.ServiceProvider.GetRequiredService().ShouldNotBeDisposed(); + } + + singletonService = serviceProvider.GetRequiredService(); + singletonService.ShouldNotBeDisposed(); + } + + singletonService.ShouldBeDisposed(); + } + + private class MyTransientServiceUsesSingleton + { + private readonly MySingletonService _singletonService; + + public MyTransientServiceUsesSingleton(MySingletonService singletonService) + { + _singletonService = singletonService; + } + + public void DoIt() + { + _singletonService.DoIt(); + } + } + + private class MySingletonService + { + private readonly IServiceProvider _serviceProvider; + + private readonly List _instances; + + public MySingletonService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _instances = new List(); + } + + public void DoIt() + { + _instances.Add(_serviceProvider.GetRequiredService()); + } + + public void ShouldNotBeDisposed() + { + foreach (var instance in _instances) + { + instance.IsDisposed.ShouldBeFalse(); + } + } + + public void ShouldBeDisposed() + { + foreach (var instance in _instances) + { + instance.IsDisposed.ShouldBeTrue(); + } + } + } + + private class MyTransientService : IDisposable + { + public bool IsDisposed { get; private set; } + + public void Dispose() + { + IsDisposed = true; + } + } + } +} diff --git a/test/Volo.Abp.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs b/test/Volo.Abp.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs new file mode 100644 index 0000000000..c413bdc9f9 --- /dev/null +++ b/test/Volo.Abp.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Shouldly; +using Volo.Abp.Localization.Source; +using Volo.Abp.Modularity; +using Volo.Abp.TestBase; +using Xunit; + +namespace Volo.Abp.Localization +{ + public class AbpLocalization_Tests : AbpIntegratedTest + { + private readonly IStringLocalizer _localizer; + + public AbpLocalization_Tests() + { + _localizer = GetRequiredService>(); + } + + [Fact] + public void Should_Get_Same_Text_If_Not_Defined_Anywhere() + { + const string text = "A string that is not defined anywhere!"; + + _localizer[text].Value.ShouldBe(text); + } + + [Fact] + public void Should_Get_Localized_Text_If_Defined_In_Current_Culture() + { + using (AbpCultureHelper.Use("en")) + { + _localizer["Car"].Value.ShouldBe("Car"); + _localizer["CarPlural"].Value.ShouldBe("Cars"); + } + + using (AbpCultureHelper.Use("tr")) + { + _localizer["Car"].Value.ShouldBe("Araba"); + _localizer["CarPlural"].Value.ShouldBe("Araba"); + } + } + + [DependsOn(typeof(AbpTestBaseModule))] + public class TestModule : AbpModule + { + public override void ConfigureServices(IServiceCollection services) + { + services.Configure(options => + { + options.Resources.AddJson("en"); + }); + } + } + } +} \ No newline at end of file diff --git a/test/Volo.Abp.Tests/Volo/Abp/Localization/Source/LocalizationTestResource.cs b/test/Volo.Abp.Tests/Volo/Abp/Localization/Source/LocalizationTestResource.cs new file mode 100644 index 0000000000..7a9b6be6ca --- /dev/null +++ b/test/Volo.Abp.Tests/Volo/Abp/Localization/Source/LocalizationTestResource.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.Localization.Source +{ + public sealed class LocalizationTestResource + { + + } +} diff --git a/test/Volo.Abp.Tests/Volo/Abp/Localization/Source/en.json b/test/Volo.Abp.Tests/Volo/Abp/Localization/Source/en.json new file mode 100644 index 0000000000..a94360c161 --- /dev/null +++ b/test/Volo.Abp.Tests/Volo/Abp/Localization/Source/en.json @@ -0,0 +1,8 @@ +{ + "culture": "en", + "texts": { + "Hello {0}.": "Hello {0}", + "Car": "Car", + "CarPlural": "Cars" + } +} \ No newline at end of file diff --git a/test/Volo.Abp.Tests/Volo/Abp/Localization/Source/tr.json b/test/Volo.Abp.Tests/Volo/Abp/Localization/Source/tr.json new file mode 100644 index 0000000000..caf947e79e --- /dev/null +++ b/test/Volo.Abp.Tests/Volo/Abp/Localization/Source/tr.json @@ -0,0 +1,8 @@ +{ + "culture": "tr", + "texts": { + "Hello {0}.": "Merhaba {0}", + "Car": "Araba", + "CarPlural": "Araba" + } +} \ No newline at end of file