diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..e4951e3
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,5 @@
+
+
+ true
+
+
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..4eea325
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,22 @@
+
+
+ 8.0.0
+ 12
+ netstandard2.1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Insight.Localizer.sln b/Lclzr.sln
similarity index 61%
rename from Insight.Localizer.sln
rename to Lclzr.sln
index 2d6457e..14c2204 100644
--- a/Insight.Localizer.sln
+++ b/Lclzr.sln
@@ -1,8 +1,8 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Insight.Localizer", "src\Insight.Localizer\Insight.Localizer.csproj", "{192A4E10-2498-409B-A537-F27150C31C1C}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lclzr", "src\Lclzr\Lclzr.csproj", "{192A4E10-2498-409B-A537-F27150C31C1C}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Insight.Localizer.Tests", "tests\Insight.Localizer.Tests\Insight.Localizer.Tests.csproj", "{C8EDC5BA-AA36-44C6-A4D5-E45695EF5867}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lclzr.Tests", "tests\Lclzr.Tests\Lclzr.Tests.csproj", "{C8EDC5BA-AA36-44C6-A4D5-E45695EF5867}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solution Items", "{7C43D58B-6E27-40AC-B59E-D9FE1AB66DB3}"
ProjectSection(SolutionItems) = preProject
@@ -11,8 +11,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solutio
.github\workflows\publish.yml = .github\workflows\publish.yml
.gitignore = .gitignore
README.md = README.md
+ Directory.Build.props = Directory.Build.props
+ Directory.Packages.props = Directory.Packages.props
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lclzr.Extensions", "src\Lclzr.Extensions\Lclzr.Extensions.csproj", "{91FBA765-5291-4A11-9A55-7E4B932E3F7A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -27,5 +31,9 @@ Global
{C8EDC5BA-AA36-44C6-A4D5-E45695EF5867}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8EDC5BA-AA36-44C6-A4D5-E45695EF5867}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8EDC5BA-AA36-44C6-A4D5-E45695EF5867}.Release|Any CPU.Build.0 = Release|Any CPU
+ {91FBA765-5291-4A11-9A55-7E4B932E3F7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {91FBA765-5291-4A11-9A55-7E4B932E3F7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {91FBA765-5291-4A11-9A55-7E4B932E3F7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {91FBA765-5291-4A11-9A55-7E4B932E3F7A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/src/Insight.Localizer/Insight.Localizer.csproj b/src/Insight.Localizer/Insight.Localizer.csproj
deleted file mode 100644
index 1a0ea2a..0000000
--- a/src/Insight.Localizer/Insight.Localizer.csproj
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
- netstandard2.1;net6.0;
- Abstraction to localize your app from json files
- Copyright © 2023 Sergey Nazarov
- https://github.com/InsightAppDev/Insight.Localizer
- https://github.com/InsightAppDev/Insight.Localizer
- Github
-
-
-
-
-
-
-
-
- <_Parameter1>Insight.Localizer.Tests
-
-
-
-
diff --git a/src/Insight.Localizer/Insight.Localizer.csproj.DotSettings b/src/Insight.Localizer/Insight.Localizer.csproj.DotSettings
deleted file mode 100644
index 374f4af..0000000
--- a/src/Insight.Localizer/Insight.Localizer.csproj.DotSettings
+++ /dev/null
@@ -1,2 +0,0 @@
-
- True
\ No newline at end of file
diff --git a/src/Insight.Localizer/Localizer.cs b/src/Insight.Localizer/Localizer.cs
deleted file mode 100644
index f1ad86d..0000000
--- a/src/Insight.Localizer/Localizer.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text.RegularExpressions;
-using System.Threading;
-using Newtonsoft.Json.Linq;
-
-namespace Insight.Localizer
-{
- public sealed class Localizer : ILocalizer
- {
- private static IDictionary _blocks;
-
- private static readonly AsyncLocal _currentCulture = new AsyncLocal();
-
- public static string? CurrentCulture
- {
- get => _currentCulture.Value;
- set
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- throw new ArgumentNullException(nameof(value));
- }
-
- _currentCulture.Value = value.ToLower();
- }
- }
-
- public IReadOnlyCollection AvailableBlockNames => new Lazy>(
- () => Blocks
- .Select(x => x.Key)
- .ToList())
- .Value;
-
- public IDictionary Blocks => _blocks;
-
- public static void Initialize(LocalizerConfiguration configuration)
- {
- if (configuration == null)
- throw new ArgumentNullException(nameof(configuration));
-
- _blocks = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
- Build(configuration);
- }
-
- public string Get(string block, string key)
- {
- return this[block].Get(_currentCulture.Value, key);
- }
-
- public string GetAny(string block, string key)
- {
- return this[block].Get(LocalizerConstants.AnyCultureKey, key);
- }
-
- public string Get(string culture, string block, string key)
- {
- return this[block].Get(culture, key);
- }
-
- private Block this[string name] =>
- Blocks.TryGetValue(name, out var value)
- ? value
- : throw new MissingBlockException($"Block `{name}` missing");
-
- private static void Build(LocalizerConfiguration configuration)
- {
- var pattern = string.IsNullOrWhiteSpace(configuration.Pattern)
- ? "*.json"
- : $"{configuration.Pattern}.*.json";
- var searchOption = configuration.ReadNestedFolders
- ? SearchOption.AllDirectories
- : SearchOption.TopDirectoryOnly;
-
- var files = Directory.GetFiles(configuration.Path, pattern, searchOption);
- foreach (var file in files)
- {
- var localeRegex = new Regex(@"^(.{1,})\.(.{2,})\.json$");
- var filename = Path.GetFileName(file);
- var match = localeRegex.Match(filename);
- if (match.Success)
- {
- var blockName = match.Groups[1].Value;
- var cultureString = match.Groups[^1]?.Value;
-
- var json = File.ReadAllText(file);
- var jObject = JObject.Parse(json);
- var blockContent = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
-
- foreach (var (key, value) in jObject)
- blockContent.Add(key, value.ToString());
-
- if (!_blocks.ContainsKey(blockName))
- {
- var block = new Block(blockName);
- _blocks.Add(block.Name, block);
- }
-
- _blocks[blockName].Add(cultureString, blockContent);
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/Insight.Localizer/LocalizerConfiguration.cs b/src/Insight.Localizer/LocalizerConfiguration.cs
deleted file mode 100644
index 92e7ba4..0000000
--- a/src/Insight.Localizer/LocalizerConfiguration.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Insight.Localizer
-{
- public sealed class LocalizerConfiguration
- {
- public string Path { get; set; }
-
- ///
- /// File pattern. Example 'message' will read all files started with 'message.' like 'message.en-us.json', etc. If null reads all files in directory
- ///
- public string Pattern { get; set; }
-
- public bool ReadNestedFolders { get; set; }
- }
-}
\ No newline at end of file
diff --git a/src/Lclzr.Extensions/AssemblyInfo.cs b/src/Lclzr.Extensions/AssemblyInfo.cs
new file mode 100644
index 0000000..db8e4ef
--- /dev/null
+++ b/src/Lclzr.Extensions/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly:InternalsVisibleTo("Lclzr.Tests")]
\ No newline at end of file
diff --git a/src/Lclzr.Extensions/Lclzr.Extensions.csproj b/src/Lclzr.Extensions/Lclzr.Extensions.csproj
new file mode 100644
index 0000000..fbbf0f9
--- /dev/null
+++ b/src/Lclzr.Extensions/Lclzr.Extensions.csproj
@@ -0,0 +1,17 @@
+
+
+
+ netstandard2.1
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Lclzr.Extensions/RegistryInitializerBackgroundService.cs b/src/Lclzr.Extensions/RegistryInitializerBackgroundService.cs
new file mode 100644
index 0000000..d96dd81
--- /dev/null
+++ b/src/Lclzr.Extensions/RegistryInitializerBackgroundService.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Lclzr.Infrastructure;
+using Lclzr.Registries;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Lclzr.Extensions
+{
+ internal class RegistryInitializerBackgroundService : BackgroundService
+ {
+ private readonly IServiceProvider _serviceProvider;
+
+ public RegistryInitializerBackgroundService(IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
+ }
+
+ protected override Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ var registry = _serviceProvider.GetRequiredService();
+ if (registry is IInitializable initializable)
+ {
+ return initializable.Initialize();
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Lclzr.Extensions/ServiceCollectionExtensions.cs b/src/Lclzr.Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..4d83aa4
--- /dev/null
+++ b/src/Lclzr.Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,42 @@
+using System;
+using Lclzr.Registries;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Lclzr.Extensions
+{
+ public static class ServiceCollectionExtensions
+ {
+ public static IServiceCollection AddLocalizer(this IServiceCollection services,
+ Action builder)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ var localizerBuilder = new LocalizerBuilder();
+ builder.Invoke(localizerBuilder);
+
+ foreach (var provider in localizerBuilder.Providers)
+ {
+ services.AddSingleton(provider);
+ }
+
+ services.TryAddSingleton();
+
+ Func factory = ctx =>
+ localizerBuilder
+ .WithRegistry(ctx.GetRequiredService())
+ .Build();
+
+ var descriptor = new ServiceDescriptor(typeof(ILocalizer), factory, ServiceLifetime.Scoped);
+ services.TryAdd(descriptor);
+
+ services.AddScoped(typeof(ILocalizer<>), typeof(Localizer<>));
+ services.AddHostedService();
+
+ return services;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Lclzr/AssemblyInfo.cs b/src/Lclzr/AssemblyInfo.cs
new file mode 100644
index 0000000..548c792
--- /dev/null
+++ b/src/Lclzr/AssemblyInfo.cs
@@ -0,0 +1,4 @@
+using System.Runtime.CompilerServices;
+
+[assembly:InternalsVisibleTo("Lclzr.Tests")]
+[assembly:InternalsVisibleTo("Lclzr.Extensions")]
\ No newline at end of file
diff --git a/src/Insight.Localizer/Block.cs b/src/Lclzr/Block.cs
similarity index 76%
rename from src/Insight.Localizer/Block.cs
rename to src/Lclzr/Block.cs
index a8833fa..873f68a 100644
--- a/src/Insight.Localizer/Block.cs
+++ b/src/Lclzr/Block.cs
@@ -1,17 +1,25 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using Lclzr.Exceptions;
-namespace Insight.Localizer
+namespace Lclzr
{
public sealed class Block
{
public string Name { get; }
+ public IReadOnlyCollection AvailableCultures =>
+ new Lazy>(() => _localizations.Keys
+ .ToList()
+ .AsReadOnly())
+ .Value;
+
private readonly IDictionary> _localizations;
private Block()
{
- _localizations = new Dictionary>(StringComparer.InvariantCultureIgnoreCase);
+ _localizations = new Dictionary>(StringComparer.OrdinalIgnoreCase);
}
internal Block(string name) : this()
diff --git a/src/Lclzr/Exceptions/LocalizerException.cs b/src/Lclzr/Exceptions/LocalizerException.cs
new file mode 100644
index 0000000..dda7e1e
--- /dev/null
+++ b/src/Lclzr/Exceptions/LocalizerException.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace Lclzr.Exceptions
+{
+ public class LocalizerException : Exception
+ {
+ public LocalizerException(string message) : base(message)
+ {
+ }
+
+ public LocalizerException(string message, Exception inner) : base(message, inner)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Insight.Localizer/Exceptions/MissingBlockException.cs b/src/Lclzr/Exceptions/MissingBlockException.cs
similarity index 85%
rename from src/Insight.Localizer/Exceptions/MissingBlockException.cs
rename to src/Lclzr/Exceptions/MissingBlockException.cs
index 00da805..29cb19d 100644
--- a/src/Insight.Localizer/Exceptions/MissingBlockException.cs
+++ b/src/Lclzr/Exceptions/MissingBlockException.cs
@@ -1,6 +1,6 @@
using System;
-namespace Insight.Localizer
+namespace Lclzr.Exceptions
{
public class MissingBlockException : Exception
{
diff --git a/src/Insight.Localizer/Exceptions/MissingLocalizationException.cs b/src/Lclzr/Exceptions/MissingLocalizationException.cs
similarity index 86%
rename from src/Insight.Localizer/Exceptions/MissingLocalizationException.cs
rename to src/Lclzr/Exceptions/MissingLocalizationException.cs
index d1ba9f8..de291c2 100644
--- a/src/Insight.Localizer/Exceptions/MissingLocalizationException.cs
+++ b/src/Lclzr/Exceptions/MissingLocalizationException.cs
@@ -1,6 +1,6 @@
using System;
-namespace Insight.Localizer
+namespace Lclzr.Exceptions
{
public class MissingLocalizationException : Exception
{
diff --git a/src/Insight.Localizer/ILocalizer.cs b/src/Lclzr/ILocalizer.cs
similarity index 79%
rename from src/Insight.Localizer/ILocalizer.cs
rename to src/Lclzr/ILocalizer.cs
index f4fdd8b..8817af4 100644
--- a/src/Insight.Localizer/ILocalizer.cs
+++ b/src/Lclzr/ILocalizer.cs
@@ -1,18 +1,13 @@
using System.Collections.Generic;
-namespace Insight.Localizer
+namespace Lclzr
{
public interface ILocalizer
{
///
/// Curent culture of the localizer.
///
- public static string? CurrentCulture { get; set; }
-
- ///
- /// Loaded blocks
- ///
- IDictionary Blocks { get; }
+ public ILocalizerCulture? CurrentCulture { get; set; }
///
/// Available block names
@@ -25,7 +20,7 @@ public interface ILocalizer
/// Block name
/// Key
string Get(string block, string key);
-
+
///
/// Get value by block-key for any culture
///
@@ -41,6 +36,6 @@ public interface ILocalizer
/// Block name
/// Key
///
- string Get(string culture, string block, string key);
+ string GetByCulture(string culture, string block, string key);
}
}
\ No newline at end of file
diff --git a/src/Lclzr/ILocalizerCulture.cs b/src/Lclzr/ILocalizerCulture.cs
new file mode 100644
index 0000000..1dc3088
--- /dev/null
+++ b/src/Lclzr/ILocalizerCulture.cs
@@ -0,0 +1,7 @@
+namespace Lclzr
+{
+ public interface ILocalizerCulture
+ {
+ string Value { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Lclzr/ILocalizer{T}.cs b/src/Lclzr/ILocalizer{T}.cs
new file mode 100644
index 0000000..405264c
--- /dev/null
+++ b/src/Lclzr/ILocalizer{T}.cs
@@ -0,0 +1,22 @@
+namespace Lclzr
+{
+ public interface ILocalizer : ILocalizer where T : class
+ {
+ ///
+ /// Get value by key from context - using nameof(T) as block name
+ ///
+ string Get(string key);
+
+ ///
+ /// Get any value by key from context for any culture - using nameof(T) as block name
+ ///
+ /// Key
+ ///
+ string GetAny(string key);
+
+ ///
+ /// Get value from context - using nameof(T) as block name
+ ///
+ string GetByCulture(string culture, string key);
+ }
+}
\ No newline at end of file
diff --git a/src/Lclzr/Infrastructure/IInitializable.cs b/src/Lclzr/Infrastructure/IInitializable.cs
new file mode 100644
index 0000000..31b8f33
--- /dev/null
+++ b/src/Lclzr/Infrastructure/IInitializable.cs
@@ -0,0 +1,9 @@
+using System.Threading.Tasks;
+
+namespace Lclzr.Infrastructure
+{
+ internal interface IInitializable
+ {
+ Task Initialize();
+ }
+}
\ No newline at end of file
diff --git a/src/Lclzr/Lclzr.csproj b/src/Lclzr/Lclzr.csproj
new file mode 100644
index 0000000..6cd3b64
--- /dev/null
+++ b/src/Lclzr/Lclzr.csproj
@@ -0,0 +1,11 @@
+
+
+
+ netstandard2.1
+
+
+
+
+
+
+
diff --git a/src/Lclzr/Localizer.cs b/src/Lclzr/Localizer.cs
new file mode 100644
index 0000000..b48e3c2
--- /dev/null
+++ b/src/Lclzr/Localizer.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using Lclzr.Registries;
+
+namespace Lclzr
+{
+ internal class Localizer : ILocalizer
+ {
+ private readonly ILocalizerRegistry _registry;
+
+ public ILocalizerCulture? CurrentCulture { get; set; }
+
+ public IReadOnlyCollection AvailableBlockNames => _registry.AvailableBlockNames;
+
+ public Localizer(ILocalizerRegistry registry)
+ {
+ _registry = registry ?? throw new ArgumentNullException(nameof(registry));
+ }
+
+ public Localizer(ILocalizerRegistry registry, ILocalizerCulture localizerCulture) : this(registry)
+ {
+ CurrentCulture = localizerCulture ?? throw new ArgumentNullException(nameof(localizerCulture));
+ }
+
+ public string Get(string block, string key)
+ {
+ if (CurrentCulture == null)
+ {
+ throw new InvalidOperationException($"{nameof(CurrentCulture)} is null");
+ }
+
+ return _registry.GetByCulture(CurrentCulture.Value, block, key);
+ }
+
+ public string GetAny(string block, string key)
+ {
+ return _registry.GetAny(block, key);
+ }
+
+ public string GetByCulture(string culture, string block, string key)
+ {
+ return _registry.GetByCulture(culture, block, key);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Lclzr/LocalizerBuilder.cs b/src/Lclzr/LocalizerBuilder.cs
new file mode 100644
index 0000000..d980371
--- /dev/null
+++ b/src/Lclzr/LocalizerBuilder.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Lclzr.Providers;
+using Lclzr.Providers.Files.EmbeddedResources;
+using Lclzr.Providers.Files.RawFiles;
+using Lclzr.Registries;
+
+namespace Lclzr
+{
+ public sealed class LocalizerBuilder
+ {
+ internal readonly List Providers = new List();
+
+ private ILocalizerRegistry? _registry;
+
+ public LocalizerBuilder WithProvider(TProvider provider) where TProvider : class, IBlocksProvider
+ {
+ if (provider == null)
+ {
+ throw new ArgumentNullException(nameof(provider));
+ }
+
+ Providers.Add(provider);
+ return this;
+ }
+
+ public LocalizerBuilder WithRawFilesProvider(RawFilesBlocksProviderOptions options)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ Providers.Add(new RawFilesBlocksProvider(options));
+ return this;
+ }
+
+ public LocalizerBuilder WithEmbeddedResourcesProvider(EmbeddedResourcesBlocksProviderOptions options)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ Providers.Add(new EmbeddedResourcesBlocksProvider(options));
+ return this;
+ }
+
+ public ILocalizer Build()
+ {
+ var registry = _registry ?? BuildAndInitializeRegistry().GetAwaiter().GetResult();
+
+ return new Localizer(registry);
+ }
+
+ public async Task BuildAsync()
+ {
+ var registry = _registry ?? await BuildAndInitializeRegistry();
+
+ return new Localizer(registry);
+ }
+
+ internal LocalizerBuilder WithRegistry(TRegistry registry) where TRegistry : class, ILocalizerRegistry
+ {
+ _registry = registry ?? throw new ArgumentNullException(nameof(registry));
+ return this;
+ }
+
+ private async Task BuildAndInitializeRegistry()
+ {
+ var registry = new LocalizerRegistry(Providers);
+ await registry.Initialize();
+
+ return registry;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Insight.Localizer/CurrentCulture.cs b/src/Lclzr/LocalizerConstants.cs
similarity index 78%
rename from src/Insight.Localizer/CurrentCulture.cs
rename to src/Lclzr/LocalizerConstants.cs
index 1a7b103..510e880 100644
--- a/src/Insight.Localizer/CurrentCulture.cs
+++ b/src/Lclzr/LocalizerConstants.cs
@@ -1,6 +1,5 @@
-namespace Insight.Localizer
+namespace Lclzr
{
-
public static class LocalizerConstants
{
public static string AnyCultureKey = "any";
diff --git a/src/Lclzr/LocalizerCulture.cs b/src/Lclzr/LocalizerCulture.cs
new file mode 100644
index 0000000..e9ede00
--- /dev/null
+++ b/src/Lclzr/LocalizerCulture.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace Lclzr
+{
+ public sealed class LocalizerCulture : ILocalizerCulture
+ {
+ public LocalizerCulture(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ Value = value;
+ }
+
+ public string Value { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Lclzr/Localizer{T}.cs b/src/Lclzr/Localizer{T}.cs
new file mode 100644
index 0000000..c23c3b5
--- /dev/null
+++ b/src/Lclzr/Localizer{T}.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+
+namespace Lclzr
+{
+ internal class Localizer : ILocalizer where T : class
+ {
+ private readonly ILocalizer _localizer;
+
+ public ILocalizerCulture CurrentCulture
+ {
+ get => _localizer.CurrentCulture;
+ set => _localizer.CurrentCulture = value;
+ }
+
+ public IReadOnlyCollection AvailableBlockNames => _localizer.AvailableBlockNames;
+
+ public Localizer(ILocalizer localizer)
+ {
+ _localizer = localizer ?? throw new ArgumentNullException(nameof(localizer));
+ }
+
+ public string Get(string key)
+ {
+ var block = GetBlockName();
+ return Get(block, key);
+ }
+
+ public string GetAny(string key)
+ {
+ var block = GetBlockName();
+ return GetAny(block, key);
+ }
+
+ public string GetByCulture(string culture, string key)
+ {
+ var block = GetBlockName();
+ return GetByCulture(culture, block, key);
+ }
+
+ public string Get(string block, string key)
+ {
+ return _localizer.Get(block, key);
+ }
+
+ public string GetAny(string block, string key)
+ {
+ return _localizer.GetAny(block, key);
+ }
+
+ public string GetByCulture(string culture, string block, string key)
+ {
+ return _localizer.GetByCulture(culture, block, key);
+ }
+
+ private static string GetBlockName()
+ {
+ return typeof(T).Name;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Lclzr/Providers/BlockCultureData.cs b/src/Lclzr/Providers/BlockCultureData.cs
new file mode 100644
index 0000000..4613a68
--- /dev/null
+++ b/src/Lclzr/Providers/BlockCultureData.cs
@@ -0,0 +1,35 @@
+using System;
+
+namespace Lclzr.Providers
+{
+ internal readonly struct BlockCultureData : IEquatable
+ {
+ public BlockCultureData(string block, string culture, string content)
+ {
+ Block = block;
+ Culture = culture;
+ Content = content;
+ }
+
+ public string Block { get; }
+
+ public string Culture { get; }
+
+ public string Content { get; }
+
+ public bool Equals(BlockCultureData other)
+ {
+ return Block == other.Block && Culture == other.Culture && Content == other.Content;
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is BlockCultureData other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Block, Culture, Content);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Lclzr/Providers/BlocksProvider.cs b/src/Lclzr/Providers/BlocksProvider.cs
new file mode 100644
index 0000000..4022e2d
--- /dev/null
+++ b/src/Lclzr/Providers/BlocksProvider.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+
+namespace Lclzr.Providers
+{
+ internal abstract class BlocksProvider : IBlocksProvider
+ {
+ protected readonly IDictionary Blocks = new Dictionary();
+
+ public virtual Task> GetBlocks()
+ {
+ return Task.FromResult>(Blocks.Values.ToArray());
+ }
+
+ protected Task InitializeBlockCulture(in BlockCultureData cultureData)
+ {
+ var blockName = cultureData.Block;
+ var cultureString = cultureData.Culture;
+
+ var json = cultureData.Content;
+ var jObject = JObject.Parse(json);
+ var blockContent = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var (key, value) in jObject)
+ blockContent.Add(key, value.ToString());
+
+ if (!Blocks.ContainsKey(blockName))
+ {
+ var block = new Block(blockName);
+ Blocks.Add(block.Name, block);
+ }
+
+ Blocks[blockName].Add(cultureString, blockContent);
+ return Task.CompletedTask;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Lclzr/Providers/Files/EmbeddedResources/EmbeddedResourcesBlocksProvider.cs b/src/Lclzr/Providers/Files/EmbeddedResources/EmbeddedResourcesBlocksProvider.cs
new file mode 100644
index 0000000..8e7b7cc
--- /dev/null
+++ b/src/Lclzr/Providers/Files/EmbeddedResources/EmbeddedResourcesBlocksProvider.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Lclzr.Exceptions;
+using Newtonsoft.Json.Linq;
+
+namespace Lclzr.Providers.Files.EmbeddedResources
+{
+ internal class EmbeddedResourcesBlocksProvider : BlocksProvider
+ {
+ private static readonly Regex OneFilePerCultureNameRegex =
+ new Regex(@"lclzr.([A-z0-9_-]{1,})\.([a-z\-]{2,})\.json$", RegexOptions.Compiled);
+
+ private static readonly Regex OneFilePerMultipleCulturesNameRegex =
+ new Regex(@"lclzr.([A-z0-9_-]{1,})\.json$", RegexOptions.Compiled);
+
+ private readonly EmbeddedResourcesBlocksProviderOptions _options;
+
+ private bool _initialized;
+
+ public EmbeddedResourcesBlocksProvider(EmbeddedResourcesBlocksProviderOptions options)
+ {
+ _options = options ?? throw new ArgumentNullException(nameof(options));
+ }
+
+ public override async Task> GetBlocks()
+ {
+ if (!_initialized)
+ {
+ await Initialize();
+ _initialized = true;
+ }
+
+ return await base.GetBlocks();
+ }
+
+ private async Task Initialize()
+ {
+ var encoding = Encoding.GetEncoding(_options.ResourceEncodingWebName);
+ foreach (var assembly in _options.Assemblies.Select(Assembly.Load))
+ {
+ var resourceNames = assembly.GetManifestResourceNames();
+
+ foreach (var resourceName in resourceNames)
+ {
+ try
+ {
+ using (var resourceStream = assembly.GetManifestResourceStream(resourceName))
+ {
+ if (resourceStream == null)
+ continue;
+
+ using (var streamReader = new StreamReader(resourceStream, encoding))
+ {
+ var content = await streamReader.ReadToEndAsync();
+
+ var oneFilePerCultureMatch = OneFilePerCultureNameRegex.Match(resourceName);
+ if (oneFilePerCultureMatch.Success)
+ {
+ var block = oneFilePerCultureMatch.Groups[1].Value;
+ var culture = oneFilePerCultureMatch.Groups[2].Value;
+ var info = new BlockCultureData(block, culture, content);
+
+ await InitializeBlockCulture(in info);
+ continue;
+ }
+
+
+ var oneFilePerMultipleCulturesMatch =
+ OneFilePerMultipleCulturesNameRegex.Match(resourceName);
+ if (oneFilePerMultipleCulturesMatch.Success)
+ {
+ var block = oneFilePerMultipleCulturesMatch.Groups[1].Value;
+ var json = content;
+ var jObject = JObject.Parse(json);
+ foreach (var (culture, blockJToken) in jObject)
+ {
+ var info = new BlockCultureData(block, culture, blockJToken.ToString());
+ await InitializeBlockCulture(in info);
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new LocalizerException($"Failed to process embedded resource: {resourceName}", ex);
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Lclzr/Providers/Files/EmbeddedResources/EmbeddedResourcesBlocksProviderOptions.cs b/src/Lclzr/Providers/Files/EmbeddedResources/EmbeddedResourcesBlocksProviderOptions.cs
new file mode 100644
index 0000000..3f1ff7b
--- /dev/null
+++ b/src/Lclzr/Providers/Files/EmbeddedResources/EmbeddedResourcesBlocksProviderOptions.cs
@@ -0,0 +1,11 @@
+using System.Text;
+
+namespace Lclzr.Providers.Files.EmbeddedResources
+{
+ public class EmbeddedResourcesBlocksProviderOptions
+ {
+ public string ResourceEncodingWebName { get; set; } = Encoding.UTF8.WebName;
+
+ public string[] Assemblies { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Lclzr/Providers/Files/RawFiles/RawFilesBlocksProvider.cs b/src/Lclzr/Providers/Files/RawFiles/RawFilesBlocksProvider.cs
new file mode 100644
index 0000000..9e031ba
--- /dev/null
+++ b/src/Lclzr/Providers/Files/RawFiles/RawFilesBlocksProvider.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Lclzr.Exceptions;
+using Newtonsoft.Json.Linq;
+
+namespace Lclzr.Providers.Files.RawFiles
+{
+ internal class RawFilesBlocksProvider : BlocksProvider
+ {
+ private static readonly Regex OneFilePerCultureNameRegex =
+ new Regex(@"^lclzr.([A-z0-9_-]{1,})\.([a-z\-]{2,})\.json$", RegexOptions.Compiled);
+
+ private static readonly Regex OneFilePerMultipleCulturesNameRegex =
+ new Regex(@"^lclzr.([A-z0-9_-]{1,})\.json$", RegexOptions.Compiled);
+
+ private readonly RawFilesBlocksProviderOptions _options;
+
+ private bool _initialied;
+
+ public RawFilesBlocksProvider(RawFilesBlocksProviderOptions options)
+ {
+ _options = options ?? throw new ArgumentNullException(nameof(options));
+ }
+
+ public override async Task> GetBlocks()
+ {
+ if (!_initialied)
+ {
+ await Initialize();
+ _initialied = true;
+ }
+
+ return await base.GetBlocks();
+ }
+
+ private async Task Initialize()
+ {
+ var searchOption = _options.ReadNestedFolders
+ ? SearchOption.AllDirectories
+ : SearchOption.TopDirectoryOnly;
+
+ var files = Directory.GetFiles(_options.Path, "*", searchOption);
+
+ foreach (var file in files)
+ {
+ try
+ {
+ var filename = Path.GetFileName(file);
+ string? content = null;
+ var oneFilePerMultipleLanguagesMatch = OneFilePerMultipleCulturesNameRegex.Match(filename);
+ if (oneFilePerMultipleLanguagesMatch.Success)
+ {
+ content = await File.ReadAllTextAsync(file);
+ var blockName = oneFilePerMultipleLanguagesMatch.Groups[1].Value;
+ var jObject = JObject.Parse(content);
+ foreach (var (culture, blockJToken) in jObject)
+ {
+ var info = new BlockCultureData(blockName, culture, blockJToken.ToString());
+ await InitializeBlockCulture(in info);
+ }
+
+ continue;
+ }
+
+ var oneFilePerLanguageMatch = OneFilePerCultureNameRegex.Match(filename);
+ if (oneFilePerLanguageMatch.Success)
+ {
+ content = await File.ReadAllTextAsync(file);
+ var block = oneFilePerLanguageMatch.Groups[1].Value;
+ var culture = oneFilePerLanguageMatch.Groups[2].Value;
+ var info = new BlockCultureData(block, culture, content);
+
+ await InitializeBlockCulture(in info);
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new LocalizerException($"Failed to process file: {file}", ex);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Lclzr/Providers/Files/RawFiles/RawFilesBlocksProviderOptions.cs b/src/Lclzr/Providers/Files/RawFiles/RawFilesBlocksProviderOptions.cs
new file mode 100644
index 0000000..14f8811
--- /dev/null
+++ b/src/Lclzr/Providers/Files/RawFiles/RawFilesBlocksProviderOptions.cs
@@ -0,0 +1,9 @@
+namespace Lclzr.Providers.Files.RawFiles
+{
+ public class RawFilesBlocksProviderOptions
+ {
+ public string Path { get; set; }
+
+ public bool ReadNestedFolders { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Lclzr/Providers/IBlocksProvider.cs b/src/Lclzr/Providers/IBlocksProvider.cs
new file mode 100644
index 0000000..ef30241
--- /dev/null
+++ b/src/Lclzr/Providers/IBlocksProvider.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Lclzr.Providers
+{
+ public interface IBlocksProvider
+ {
+ Task> GetBlocks();
+ }
+}
\ No newline at end of file
diff --git a/src/Lclzr/Registries/ILocalizerRegistry.cs b/src/Lclzr/Registries/ILocalizerRegistry.cs
new file mode 100644
index 0000000..48b6dd8
--- /dev/null
+++ b/src/Lclzr/Registries/ILocalizerRegistry.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+
+namespace Lclzr.Registries
+{
+ public interface ILocalizerRegistry
+ {
+ ///
+ /// Available block names
+ ///
+ IReadOnlyCollection AvailableBlockNames { get; }
+
+ ///
+ /// Get value by block-key for any culture
+ ///
+ /// Block name
+ /// Key
+ ///
+ string GetAny(string block, string key);
+
+ ///
+ /// Get value by culture-block-key for any culture
+ ///
+ /// Culture
+ /// Block name
+ /// Key
+ ///
+ string GetByCulture(string culture, string block, string key);
+ }
+}
\ No newline at end of file
diff --git a/src/Lclzr/Registries/LocalizerRegistry.cs b/src/Lclzr/Registries/LocalizerRegistry.cs
new file mode 100644
index 0000000..c19b1b7
--- /dev/null
+++ b/src/Lclzr/Registries/LocalizerRegistry.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Lclzr.Exceptions;
+using Lclzr.Infrastructure;
+using Lclzr.Providers;
+
+namespace Lclzr.Registries
+{
+ internal class LocalizerRegistry : ILocalizerRegistry, IInitializable
+ {
+ private Dictionary _blocks = new Dictionary();
+
+ private IBlocksProvider[]? _blockProviders;
+
+ private bool _initialized;
+
+ public IReadOnlyCollection AvailableBlockNames => new Lazy>(
+ () => _blocks
+ .Select(x => x.Key)
+ .ToList()
+ .AsReadOnly())
+ .Value;
+
+ public LocalizerRegistry(IEnumerable blocksProviders) : this(blocksProviders.ToArray())
+ {
+ }
+
+ public LocalizerRegistry(params IBlocksProvider[] blocksProviders)
+ {
+ _blockProviders = blocksProviders;
+ }
+
+ public async Task Initialize()
+ {
+ if (_initialized)
+ throw new InvalidOperationException($"{nameof(LocalizerRegistry)} already initialized");
+
+ if (_blockProviders == null)
+ throw new InvalidOperationException("There is no block providers");
+
+ foreach (var blocksProvider in _blockProviders)
+ {
+ var currentProviderBlocks = await blocksProvider.GetBlocks();
+ foreach (var block in currentProviderBlocks)
+ {
+ if (_blocks.ContainsKey(block.Name))
+ {
+ throw new InvalidOperationException(
+ $"Error occured while adding blocks from {blocksProvider.GetType().Name}. Block already exists.");
+ }
+
+ _blocks.Add(block.Name, block);
+ }
+ }
+
+ _blockProviders = null;
+ _initialized = true;
+ }
+
+ public string GetAny(string block, string key)
+ {
+ return this[block].Get(LocalizerConstants.AnyCultureKey, key);
+ }
+
+ public string GetByCulture(string culture, string block, string key)
+ {
+ return this[block].Get(culture, key);
+ }
+
+ private Block this[string name] =>
+ _blocks.TryGetValue(name, out var value)
+ ? value
+ : throw new MissingBlockException($"Block `{name}` missing");
+ }
+}
\ No newline at end of file
diff --git a/tests/Insight.Localizer.Tests/Insight.Localizer.Tests.csproj b/tests/Insight.Localizer.Tests/Insight.Localizer.Tests.csproj
deleted file mode 100644
index 99ad63a..0000000
--- a/tests/Insight.Localizer.Tests/Insight.Localizer.Tests.csproj
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
- net6.0
-
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
-
-
diff --git a/tests/Insight.Localizer.Tests/LocalizerTest.cs b/tests/Insight.Localizer.Tests/LocalizerTest.cs
deleted file mode 100644
index 9deca15..0000000
--- a/tests/Insight.Localizer.Tests/LocalizerTest.cs
+++ /dev/null
@@ -1,142 +0,0 @@
-using System;
-using System.Linq;
-using Xunit;
-
-namespace Insight.Localizer.Tests
-{
- public sealed class LocalizerTest
- {
- private static LocalizerConfiguration Configuration => new LocalizerConfiguration
- {
- Path = "Resources",
- ReadNestedFolders = true
- };
-
- public LocalizerTest()
- {
- Localizer.Initialize(Configuration);
- Localizer.CurrentCulture = "ru-ru";
- }
-
- [Fact]
- public void Should_throw_ANE_if_configuration_at_Initialize_is_null()
- {
- Assert.Throws(() => Localizer.Initialize(null));
- }
-
- [Fact]
- public void Should_throw_ANE_if_name_at_block_ctor_is_null()
- {
- Assert.Throws(() => new Block(null));
- }
-
- [Fact]
- public void Should_throw_MissingBlockException()
- {
- var localizer = new Localizer();
- Assert.Throws(() => localizer.Get("there_is_no_block", "Hello"));
- }
-
- [Fact]
- public void Should_throw_MissingLocalizationException_if_there_is_no_culture()
- {
- Localizer.CurrentCulture = "ke-ke";
- var localizer = new Localizer();
- Assert.Throws(() => localizer.Get("messages", "Hello"));
- }
-
- [Fact]
- public void Should_throw_MissingLocalizationException_if_there_is_no_key()
- {
- var localizer = new Localizer();
- Assert.Throws(() => localizer.Get("messages", "there_is_no_localization"));
- }
-
- [Fact]
- public void Should_read_all_files()
- {
- var localizer = new Localizer();
- AssertLocalizer(localizer, 3);
- }
-
- [Fact]
- public void Should_get_available_block_names_from_all_Files()
- {
- var localizer = new Localizer();
- AssertLocalizer(localizer, 3);
-
- var names = localizer.AvailableBlockNames;
- Assert.NotNull(names);
- Assert.NotEmpty(names);
- Assert.Equal(3, names.Count);
- Assert.NotNull(names.FirstOrDefault(x => x.Equals("test", StringComparison.InvariantCultureIgnoreCase)));
- Assert.NotNull(names.FirstOrDefault(x =>
- x.Equals("messages", StringComparison.InvariantCultureIgnoreCase)));
- Assert.NotNull(names.FirstOrDefault(x =>
- x.Equals("language", StringComparison.InvariantCultureIgnoreCase)));
- }
-
- [Fact]
- public void Should_read_files_by_pattern()
- {
- var config = Configuration;
- config.Pattern = "test";
- Localizer.Initialize(config);
- var localizer = new Localizer();
- AssertLocalizer(localizer, 1);
- }
-
- [Fact]
- public void Should_get_value_in_all_languages()
- {
- var localizer = new Localizer();
- AssertLocalizer(localizer, 3);
-
- var en = localizer.Get("en-us", "test", "Hello");
- var ru = localizer.Get("test", "Hello");
-
- Assert.Equal("Hi", en, StringComparer.InvariantCultureIgnoreCase);
- Assert.Equal("Привет", ru, StringComparer.InvariantCultureIgnoreCase);
- }
-
- [Fact]
- public void Should_get_any_value()
- {
- var localizer = new Localizer();
- AssertLocalizer(localizer, 3);
-
- var russianLanguage = localizer.GetAny("language", "Russian");
- var englishLanguage = localizer.GetAny("language", "English");
-
- Assert.Equal("Русский", russianLanguage, StringComparer.InvariantCultureIgnoreCase);
- Assert.Equal("English", englishLanguage, StringComparer.InvariantCultureIgnoreCase);
- }
-
- [Fact]
- public void Should_change_current_culture()
- {
- var localizer = new Localizer();
- AssertLocalizer(localizer, 3);
-
- Assert.Equal("ru-ru", Localizer.CurrentCulture, StringComparer.InvariantCultureIgnoreCase);
- Localizer.CurrentCulture = "en-us";
- Assert.Equal("en-us", Localizer.CurrentCulture, StringComparer.InvariantCultureIgnoreCase);
- }
-
- [Fact]
- public void Should_throw_ANE_on_set_culture_if_culture_is_null()
- {
- var localizer = new Localizer();
- AssertLocalizer(localizer, 3);
-
- Assert.Throws(() => Localizer.CurrentCulture = null);
- }
-
- private void AssertLocalizer(ILocalizer localizer, int expectedBlocksCount)
- {
- Assert.NotNull(localizer);
- Assert.NotEmpty(localizer.Blocks);
- Assert.Equal(expectedBlocksCount, localizer.Blocks.Count);
- }
- }
-}
\ No newline at end of file
diff --git a/tests/Lclzr.Tests/BlockTests.cs b/tests/Lclzr.Tests/BlockTests.cs
new file mode 100644
index 0000000..0a2a560
--- /dev/null
+++ b/tests/Lclzr.Tests/BlockTests.cs
@@ -0,0 +1,20 @@
+using Lclzr.Exceptions;
+using Xunit;
+
+namespace Lclzr.Tests;
+
+public sealed class BlockTests
+{
+ private readonly Block _block;
+
+ public BlockTests()
+ {
+ _block = new Block("messages");
+ }
+
+ [Fact]
+ public void Get_throws_MissingLocalizationException_when_key_is_missing()
+ {
+ Assert.Throws(() => _block.Get("ru-ru", "Hello"));
+ }
+}
\ No newline at end of file
diff --git a/tests/Lclzr.Tests/EmbeddedResources/EmbeddedMultipleLanguagesInFile/lclzr.embedded.json b/tests/Lclzr.Tests/EmbeddedResources/EmbeddedMultipleLanguagesInFile/lclzr.embedded.json
new file mode 100644
index 0000000..2f5ec04
--- /dev/null
+++ b/tests/Lclzr.Tests/EmbeddedResources/EmbeddedMultipleLanguagesInFile/lclzr.embedded.json
@@ -0,0 +1,8 @@
+{
+ "ru-ru": {
+ "Hello": "Привет"
+ },
+ "en-us": {
+ "Hello": "Hi"
+ }
+}
\ No newline at end of file
diff --git a/tests/Lclzr.Tests/EmbeddedResources/EmbeddedOneLanguageInFile/lclzr.embedded.en.json b/tests/Lclzr.Tests/EmbeddedResources/EmbeddedOneLanguageInFile/lclzr.embedded.en.json
new file mode 100644
index 0000000..ae44cc6
--- /dev/null
+++ b/tests/Lclzr.Tests/EmbeddedResources/EmbeddedOneLanguageInFile/lclzr.embedded.en.json
@@ -0,0 +1,3 @@
+{
+ "Hello": "Hi"
+}
\ No newline at end of file
diff --git a/tests/Lclzr.Tests/EmbeddedResources/EmbeddedOneLanguageInFile/lclzr.embedded.ru.json b/tests/Lclzr.Tests/EmbeddedResources/EmbeddedOneLanguageInFile/lclzr.embedded.ru.json
new file mode 100644
index 0000000..a57e5b4
--- /dev/null
+++ b/tests/Lclzr.Tests/EmbeddedResources/EmbeddedOneLanguageInFile/lclzr.embedded.ru.json
@@ -0,0 +1,3 @@
+{
+ "Hello": "Привет"
+}
\ No newline at end of file
diff --git a/tests/Lclzr.Tests/GenericLocalizerTests.cs b/tests/Lclzr.Tests/GenericLocalizerTests.cs
new file mode 100644
index 0000000..7879778
--- /dev/null
+++ b/tests/Lclzr.Tests/GenericLocalizerTests.cs
@@ -0,0 +1,34 @@
+using Lclzr.Providers.Files.RawFiles;
+using Lclzr.Registries;
+using Xunit;
+
+namespace Lclzr.Tests;
+
+public sealed class GenericLocalizerTests
+{
+ private readonly Localizer _localizer;
+
+ private static readonly RawFilesBlocksProviderOptions Options = new()
+ {
+ Path = "Resources",
+ ReadNestedFolders = true
+ };
+
+ public GenericLocalizerTests()
+ {
+ var provider = new RawFilesBlocksProvider(Options);
+ var registry = new LocalizerRegistry(provider);
+ registry.Initialize().GetAwaiter().GetResult();
+
+ var lclzr = new Localizer(registry);
+ _localizer = new Localizer(lclzr);
+ _localizer.CurrentCulture = new LocalizerCulture("ru-ru");
+ }
+
+ [Fact]
+ public void Get_returns_value_based_on_generic_argument_name()
+ {
+ var value = _localizer.Get("test");
+ Assert.Equal("Hello!", value);
+ }
+}
\ No newline at end of file
diff --git a/tests/Lclzr.Tests/Infrastructure/LocalizerWithRawFilesProviderSut.cs b/tests/Lclzr.Tests/Infrastructure/LocalizerWithRawFilesProviderSut.cs
new file mode 100644
index 0000000..674114a
--- /dev/null
+++ b/tests/Lclzr.Tests/Infrastructure/LocalizerWithRawFilesProviderSut.cs
@@ -0,0 +1,21 @@
+using Lclzr.Providers.Files.RawFiles;
+
+namespace Lclzr.Tests.Infrastructure;
+
+public sealed class LocalizerWithRawFilesProviderSut
+{
+ public ILocalizer Localizer { get; }
+
+ private static readonly RawFilesBlocksProviderOptions Options = new()
+ {
+ Path = "Resources",
+ ReadNestedFolders = true
+ };
+
+ public LocalizerWithRawFilesProviderSut(RawFilesBlocksProviderOptions? options = null)
+ {
+ Localizer = new LocalizerBuilder()
+ .WithRawFilesProvider(options ?? Options)
+ .Build();
+ }
+}
\ No newline at end of file
diff --git a/tests/Lclzr.Tests/Infrastructure/TestBlocksProvider.cs b/tests/Lclzr.Tests/Infrastructure/TestBlocksProvider.cs
new file mode 100644
index 0000000..b52feb6
--- /dev/null
+++ b/tests/Lclzr.Tests/Infrastructure/TestBlocksProvider.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Lclzr.Providers;
+
+namespace Lclzr.Tests.Infrastructure;
+
+internal sealed class TestBlocksProvider : IBlocksProvider
+{
+ private readonly Block[] _blocks;
+
+ public TestBlocksProvider(params Block[] blocks)
+ {
+ _blocks = blocks;
+ }
+
+ public Task> GetBlocks()
+ {
+ return Task.FromResult>(_blocks);
+ }
+}
\ No newline at end of file
diff --git a/tests/Lclzr.Tests/Lclzr.Tests.csproj b/tests/Lclzr.Tests/Lclzr.Tests.csproj
new file mode 100644
index 0000000..f874736
--- /dev/null
+++ b/tests/Lclzr.Tests/Lclzr.Tests.csproj
@@ -0,0 +1,38 @@
+
+
+
+ net6.0
+ false
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/tests/Lclzr.Tests/LocalizerMultipleLanguagesInFileTests.cs b/tests/Lclzr.Tests/LocalizerMultipleLanguagesInFileTests.cs
new file mode 100644
index 0000000..f3ea628
--- /dev/null
+++ b/tests/Lclzr.Tests/LocalizerMultipleLanguagesInFileTests.cs
@@ -0,0 +1,38 @@
+using System;
+using System.IO;
+using System.Linq;
+using Lclzr.Providers.Files.RawFiles;
+using Lclzr.Tests.Infrastructure;
+using Xunit;
+
+namespace Lclzr.Tests;
+
+public sealed class LocalizerMultipleLanguagesInFileTests
+{
+ private static RawFilesBlocksProviderOptions _options = new()
+ {
+ Path = "Resources" + Path.DirectorySeparatorChar + "MultipleLanguagesInFile",
+ };
+
+ private readonly ILocalizer _localizer;
+
+ public LocalizerMultipleLanguagesInFileTests()
+ {
+ var sut = new LocalizerWithRawFilesProviderSut(_options);
+ _localizer = sut.Localizer;
+ }
+
+ [Fact]
+ public void Ctor_initializes_block_named_multiple()
+ {
+ Assert.Single(_localizer.AvailableBlockNames);
+ Assert.Equal("multiple", _localizer.AvailableBlockNames.First(), StringComparer.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void Ctor_initializes_block_named_multiple_with_two_cultures_and_hello_key()
+ {
+ Assert.Equal("Привет", _localizer.GetByCulture("ru", "multiple", "Hello"), StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("Hi", _localizer.GetByCulture("en", "multiple", "Hello"), StringComparer.OrdinalIgnoreCase);
+ }
+}
\ No newline at end of file
diff --git a/tests/Lclzr.Tests/LocalizerOneLanguageInFileTests.cs b/tests/Lclzr.Tests/LocalizerOneLanguageInFileTests.cs
new file mode 100644
index 0000000..2c2553f
--- /dev/null
+++ b/tests/Lclzr.Tests/LocalizerOneLanguageInFileTests.cs
@@ -0,0 +1,48 @@
+using System;
+using System.IO;
+using System.Linq;
+using Lclzr.Providers.Files.RawFiles;
+using Lclzr.Tests.Infrastructure;
+using Xunit;
+
+namespace Lclzr.Tests;
+
+public sealed class LocalizerOneLanguageInFileTests
+{
+ private static RawFilesBlocksProviderOptions _options = new()
+ {
+ Path = "Resources" + Path.DirectorySeparatorChar + "OneLanguageInFile",
+ ReadNestedFolders = true
+ };
+
+ private readonly ILocalizer _localizer;
+
+ public LocalizerOneLanguageInFileTests()
+ {
+ var sut = new LocalizerWithRawFilesProviderSut(_options);
+ _localizer = sut.Localizer;
+ _localizer.CurrentCulture = new LocalizerCulture("ru-ru");
+ }
+
+ [Fact]
+ public void AvailableBlockNames_returns_all_block_names()
+ {
+ var names = _localizer.AvailableBlockNames;
+ Assert.NotNull(names);
+ Assert.NotEmpty(names);
+ Assert.Equal(4, names.Count);
+ Assert.NotNull(names.FirstOrDefault(x => x.Equals("test", StringComparison.OrdinalIgnoreCase)));
+ Assert.NotNull(names.FirstOrDefault(x =>
+ x.Equals("messages", StringComparison.OrdinalIgnoreCase)));
+ Assert.NotNull(names.FirstOrDefault(x =>
+ x.Equals("language", StringComparison.OrdinalIgnoreCase)));
+ }
+
+ [Fact]
+ public void Get_returns_value_for_current_culture()
+ {
+ var value = _localizer.Get("test", "Hello");
+
+ Assert.Equal("Привет", value, StringComparer.OrdinalIgnoreCase);
+ }
+}
\ No newline at end of file
diff --git a/tests/Lclzr.Tests/LocalizerRegistryTests.cs b/tests/Lclzr.Tests/LocalizerRegistryTests.cs
new file mode 100644
index 0000000..710f935
--- /dev/null
+++ b/tests/Lclzr.Tests/LocalizerRegistryTests.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Lclzr.Exceptions;
+using Lclzr.Registries;
+using Lclzr.Tests.Infrastructure;
+using Xunit;
+
+namespace Lclzr.Tests;
+
+public sealed class LocalizerRegistryTests
+{
+ [Fact]
+ public void Get_throws_MissingBlockException_when_block_is_missing()
+ {
+ Assert.Throws(() =>
+ new LocalizerRegistry().GetByCulture("ru-ru", "there_is_no_block", "Hello"));
+ }
+
+ [Fact]
+ public async Task Get_any_returns_correct_value()
+ {
+ var languageBlock = new Block("language");
+ languageBlock.Add(LocalizerConstants.AnyCultureKey, new Dictionary
+ {
+ {"Russian", "Русский"},
+ {"English", "English"}
+ });
+
+ var testProvider = new TestBlocksProvider(languageBlock);
+ var registry = new LocalizerRegistry(testProvider);
+ await registry.Initialize();
+
+ var russianLanguage = registry.GetAny("language", "Russian");
+ var englishLanguage = registry.GetAny("language", "English");
+
+ Assert.Equal("Русский", russianLanguage, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("English", englishLanguage, StringComparer.OrdinalIgnoreCase);
+ }
+}
\ No newline at end of file
diff --git a/tests/Lclzr.Tests/LocalizierExtensionsTests.cs b/tests/Lclzr.Tests/LocalizierExtensionsTests.cs
new file mode 100644
index 0000000..cdc09ac
--- /dev/null
+++ b/tests/Lclzr.Tests/LocalizierExtensionsTests.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Security;
+using System.Threading;
+using System.Threading.Tasks;
+using Lclzr.Extensions;
+using Lclzr.Tests.Infrastructure;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Xunit;
+
+namespace Lclzr.Tests;
+
+public class LocalizierExtensionsTests
+{
+ [Fact]
+ public void AddLocalizer_registers_non_generic_implementation()
+ {
+ var sp = BuildServiceProvider();
+ var localizer = sp.GetRequiredService();
+
+ Assert.NotNull(localizer);
+ }
+
+ [Fact]
+ public void AddLocalizer_registers_generic_implementation()
+ {
+ var sp = BuildServiceProvider();
+ var localizer = sp.GetRequiredService>();
+
+ Assert.NotNull(localizer);
+ }
+
+ [Fact]
+ public void AddLocalizer_registers_registry_initializer()
+ {
+ var sp = BuildServiceProvider();
+ var initializer = sp.GetRequiredService();
+
+ Assert.NotNull(initializer);
+ Assert.Equal(typeof(RegistryInitializerBackgroundService), initializer.GetType());
+ }
+
+ [Fact]
+ public async Task LocalizerInitializer_initializes_localizer()
+ {
+ var sp = BuildServiceProvider();
+ var localizer = sp.GetRequiredService>();
+
+ var initializer = sp.GetRequiredService();
+ await initializer.StartAsync(CancellationToken.None);
+
+ Assert.NotNull(localizer.AvailableBlockNames);
+ }
+
+ private static IServiceProvider BuildServiceProvider()
+ {
+ IServiceCollection services = new ServiceCollection();
+
+ var block = new Block("test");
+ block.Add("ru-ru", new Dictionary() );
+ services.AddLocalizer(builder => builder.WithProvider(new TestBlocksProvider(block)));
+
+ return services.BuildServiceProvider();
+ }
+}
\ No newline at end of file
diff --git a/tests/Lclzr.Tests/Providers/EmbeddedResourceBlocksProviderTests.cs b/tests/Lclzr.Tests/Providers/EmbeddedResourceBlocksProviderTests.cs
new file mode 100644
index 0000000..821ed99
--- /dev/null
+++ b/tests/Lclzr.Tests/Providers/EmbeddedResourceBlocksProviderTests.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+using Lclzr.Providers.Files.EmbeddedResources;
+using Xunit;
+
+namespace Lclzr.Tests.Providers;
+
+public class EmbeddedResourceBlocksProviderTests
+{
+ [Fact]
+ public async Task Initialize_with_one_language_in_file_creates_provider_with_single_block_named_embedded_and_two_cultures()
+ {
+ var fileBlocksProviderOptions = new EmbeddedResourcesBlocksProviderOptions()
+ {
+ Assemblies = new[] { Assembly.GetExecutingAssembly().FullName }
+ };
+
+ var provider = new EmbeddedResourcesBlocksProvider(fileBlocksProviderOptions);
+
+ var blocks = await provider.GetBlocks();
+ Assert.Single(blocks);
+ var block = blocks.Single();
+
+ Assert.Equal(2, block.AvailableCultures.Count);
+ Assert.Contains("en-us", block.AvailableCultures, StringComparer.OrdinalIgnoreCase);
+ Assert.Contains("ru-ru", block.AvailableCultures, StringComparer.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public async Task Initialize_with_multiple_languages_in_file_creates_provider_with_single_block_named_embedded_and_two_cultures()
+ {
+ var fileBlocksProviderOptions = new EmbeddedResourcesBlocksProviderOptions()
+ {
+ Assemblies = new[] { Assembly.GetExecutingAssembly().FullName }
+ };
+
+ var provider = new EmbeddedResourcesBlocksProvider(fileBlocksProviderOptions);
+
+ var blocks = await provider.GetBlocks();
+ Assert.Single(blocks);
+ var block = blocks.Single();
+
+ Assert.Equal(2, block.AvailableCultures.Count);
+ Assert.Contains("en-us", block.AvailableCultures, StringComparer.OrdinalIgnoreCase);
+ Assert.Contains("ru-ru", block.AvailableCultures, StringComparer.OrdinalIgnoreCase);
+ }
+}
\ No newline at end of file
diff --git a/tests/Lclzr.Tests/Providers/RawFilesBlocksProviderTests.cs b/tests/Lclzr.Tests/Providers/RawFilesBlocksProviderTests.cs
new file mode 100644
index 0000000..398a142
--- /dev/null
+++ b/tests/Lclzr.Tests/Providers/RawFilesBlocksProviderTests.cs
@@ -0,0 +1,23 @@
+using System.Threading.Tasks;
+using Lclzr.Providers.Files.RawFiles;
+using Xunit;
+
+namespace Lclzr.Tests.Providers;
+
+public class RawFilesBlocksProviderTests
+{
+ [Fact]
+ public async Task Initialize_with_one()
+ {
+ var fileBlocksProviderOptions = new RawFilesBlocksProviderOptions
+ {
+ Path = "Resources",
+ ReadNestedFolders = true
+ };
+
+ var provider = new RawFilesBlocksProvider(fileBlocksProviderOptions);
+
+ var blocks = await provider.GetBlocks();
+ Assert.Equal(5, blocks.Count);
+ }
+}
\ No newline at end of file
diff --git a/tests/Lclzr.Tests/Resources/MultipleLanguagesInFile/lclzr.multiple.json b/tests/Lclzr.Tests/Resources/MultipleLanguagesInFile/lclzr.multiple.json
new file mode 100644
index 0000000..8b16d23
--- /dev/null
+++ b/tests/Lclzr.Tests/Resources/MultipleLanguagesInFile/lclzr.multiple.json
@@ -0,0 +1,8 @@
+{
+ "ru": {
+ "Hello": "Привет"
+ },
+ "en": {
+ "Hello": "Hi"
+ }
+}
\ No newline at end of file
diff --git a/tests/Lclzr.Tests/Resources/OneLanguageInFile/lclzr.GenericLocalizerTests.ru-ru.json b/tests/Lclzr.Tests/Resources/OneLanguageInFile/lclzr.GenericLocalizerTests.ru-ru.json
new file mode 100644
index 0000000..36e4e4a
--- /dev/null
+++ b/tests/Lclzr.Tests/Resources/OneLanguageInFile/lclzr.GenericLocalizerTests.ru-ru.json
@@ -0,0 +1,3 @@
+{
+ "Test": "Hello!"
+}
\ No newline at end of file
diff --git a/tests/Insight.Localizer.Tests/Resources/language.any.json b/tests/Lclzr.Tests/Resources/OneLanguageInFile/lclzr.language.any.json
similarity index 100%
rename from tests/Insight.Localizer.Tests/Resources/language.any.json
rename to tests/Lclzr.Tests/Resources/OneLanguageInFile/lclzr.language.any.json
diff --git a/tests/Insight.Localizer.Tests/Resources/messages.ru-ru.json b/tests/Lclzr.Tests/Resources/OneLanguageInFile/lclzr.messages.ru-ru.json
similarity index 100%
rename from tests/Insight.Localizer.Tests/Resources/messages.ru-ru.json
rename to tests/Lclzr.Tests/Resources/OneLanguageInFile/lclzr.messages.ru-ru.json
diff --git a/tests/Insight.Localizer.Tests/Resources/test.en-us.json b/tests/Lclzr.Tests/Resources/OneLanguageInFile/lclzr.test.en-us.json
similarity index 100%
rename from tests/Insight.Localizer.Tests/Resources/test.en-us.json
rename to tests/Lclzr.Tests/Resources/OneLanguageInFile/lclzr.test.en-us.json
diff --git a/tests/Insight.Localizer.Tests/Resources/test.ru-ru.json b/tests/Lclzr.Tests/Resources/OneLanguageInFile/lclzr.test.ru-ru.json
similarity index 100%
rename from tests/Insight.Localizer.Tests/Resources/test.ru-ru.json
rename to tests/Lclzr.Tests/Resources/OneLanguageInFile/lclzr.test.ru-ru.json