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