diff --git a/src/Volo.Abp.Identity.Web/Volo.Abp.Identity.Web.csproj b/src/Volo.Abp.Identity.Web/Volo.Abp.Identity.Web.csproj
index 8351793d79..77797d9147 100644
--- a/src/Volo.Abp.Identity.Web/Volo.Abp.Identity.Web.csproj
+++ b/src/Volo.Abp.Identity.Web/Volo.Abp.Identity.Web.csproj
@@ -10,17 +10,11 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
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..0f609d4b01
--- /dev/null
+++ b/src/Volo.Abp/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Globalization;
+using Microsoft.Extensions.Localization;
+
+namespace Volo.Abp.Localization
+{
+ public class AbpDictionaryBasedStringLocalizer : IStringLocalizer
+ {
+ public LocalizationResource Resource { get; }
+
+ public LocalizedString this[string name] => GetLocalizedString(name, CultureInfo.CurrentUICulture.Name);
+
+ public LocalizedString this[string name, params object[] arguments] => GetLocalizedStringFormatted(name, CultureInfo.CurrentUICulture.Name, arguments);
+
+ public AbpDictionaryBasedStringLocalizer(LocalizationResource resource)
+ {
+ Resource = resource;
+ }
+
+ public IEnumerable GetAllStrings(bool includeParentCultures)
+ {
+ return GetAllStrings(CultureInfo.CurrentUICulture.Name, includeParentCultures);
+ }
+
+ public IStringLocalizer WithCulture(CultureInfo culture)
+ {
+ return new CultureWrapperStringLocalizer(culture.Name, this);
+ }
+
+ protected virtual LocalizedString GetLocalizedStringFormatted(string name, string cultureName, params object[] arguments)
+ {
+ var localizedString = GetLocalizedString(name, cultureName);
+ return new LocalizedString(name, string.Format(localizedString.Value, arguments, localizedString.ResourceNotFound, localizedString.SearchedLocation));
+ }
+
+ protected virtual LocalizedString GetLocalizedString(string name, string cultureName)
+ {
+ var value = GetLocalizedStringOrNull(name, cultureName);
+
+ if (value == null)
+ {
+ return new LocalizedString(name, name, true);
+ }
+
+ return value;
+ }
+
+ protected virtual LocalizedString GetLocalizedStringOrNull(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);
+ }
+
+ protected virtual IReadOnlyList GetAllStrings(string cultureName, bool includeParentCultures = 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 (includeParentCultures)
+ {
+ //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();
+ }
+
+ protected virtual string GetBaseCultureName(string cultureName)
+ {
+ return cultureName.Contains("-")
+ ? cultureName.Left(cultureName.IndexOf("-", StringComparison.Ordinal))
+ : cultureName;
+ }
+
+ public class CultureWrapperStringLocalizer : IStringLocalizer
+ {
+ private readonly string _cultureName;
+ private readonly AbpDictionaryBasedStringLocalizer _innerLocalizer;
+
+ LocalizedString IStringLocalizer.this[string name] => _innerLocalizer.GetLocalizedString(name, _cultureName);
+
+ LocalizedString IStringLocalizer.this[string name, params object[] arguments] => _innerLocalizer.GetLocalizedStringFormatted(name, _cultureName, arguments);
+
+ public CultureWrapperStringLocalizer(string cultureName, AbpDictionaryBasedStringLocalizer innerLocalizer)
+ {
+ _cultureName = cultureName;
+ _innerLocalizer = innerLocalizer;
+ }
+
+ public IEnumerable GetAllStrings(bool includeParentCultures)
+ {
+ return _innerLocalizer.GetAllStrings(_cultureName, includeParentCultures);
+ }
+
+ public IStringLocalizer WithCulture(CultureInfo culture)
+ {
+ return new CultureWrapperStringLocalizer(culture.Name, _innerLocalizer);
+ }
+ }
+ }
+}
\ 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..2d838e3b19
--- /dev/null
+++ b/src/Volo.Abp/Volo/Abp/Localization/AbpStringLocalizerFactory.cs
@@ -0,0 +1,74 @@
+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);
+
+ //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/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..88346e2521
--- /dev/null
+++ b/test/Volo.Abp.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs
@@ -0,0 +1,67 @@
+using System.Globalization;
+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");
+ }
+ }
+
+ [Fact]
+ public void Should_Get_Localized_Text_If_Defined_In_Requested_Culture()
+ {
+ _localizer.WithCulture(CultureInfo.GetCultureInfo("en"))["Car"].Value.ShouldBe("Car");
+ _localizer.WithCulture(CultureInfo.GetCultureInfo("en"))["CarPlural"].Value.ShouldBe("Cars");
+
+ _localizer.WithCulture(CultureInfo.GetCultureInfo("tr"))["CarPlural"].Value.ShouldBe("Araba");
+ _localizer.WithCulture(CultureInfo.GetCultureInfo("tr"))["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