From 11d28ed487ac92cbe8f9821972a2ba2f3b5e1760 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Fri, 26 Apr 2024 13:10:32 +0300 Subject: [PATCH 1/9] Add ITranslationProvider.LoadTranslationsAsync() --- .../ILocalizationManager.cs | 31 +- .../IPluralStringLocalizer.cs | 23 +- .../ITranslationProvider.cs | 33 +- .../LocalizationManager.cs | 88 ++--- .../OrchardCore.Localization.Core.csproj | 4 + .../PoFilesTranslationsProvider.cs | 56 +-- .../PortableObject/PoParser.cs | 329 +++++++++--------- .../PortableObjectStringLocalizer.cs | 16 +- .../Localization/LocalizationManagerTests.cs | 18 +- .../Localization/PoParserTests.cs | 53 +-- .../PortableObjectStringLocalizerTests.cs | 5 +- 11 files changed, 346 insertions(+), 310 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Localization.Abstractions/ILocalizationManager.cs b/src/OrchardCore/OrchardCore.Localization.Abstractions/ILocalizationManager.cs index eb960280761..ef62126cf45 100644 --- a/src/OrchardCore/OrchardCore.Localization.Abstractions/ILocalizationManager.cs +++ b/src/OrchardCore/OrchardCore.Localization.Abstractions/ILocalizationManager.cs @@ -1,17 +1,26 @@ +using System; using System.Globalization; +using System.Threading.Tasks; -namespace OrchardCore.Localization +namespace OrchardCore.Localization; + +/// +/// Contract to manage the localization. +/// +public interface ILocalizationManager { /// - /// Contract to manage the localization. + /// Retrieves a dictionary for a specified culture. + /// + /// The . + /// A for the specified culture. + [Obsolete("This method has been deprecated, please use GetDictionaryAsync instead.")] + CultureDictionary GetDictionary(CultureInfo culture) => GetDictionaryAsync(culture).GetAwaiter().GetResult(); + + /// + /// Retrieves a dictionary for a specified culture. /// - public interface ILocalizationManager - { - /// - /// Retrieves a dictionary for a specified culture. - /// - /// The . - /// A for the specified culture. - CultureDictionary GetDictionary(CultureInfo culture); - } + /// The . + /// A for the specified culture. + Task GetDictionaryAsync(CultureInfo culture); } diff --git a/src/OrchardCore/OrchardCore.Localization.Abstractions/IPluralStringLocalizer.cs b/src/OrchardCore/OrchardCore.Localization.Abstractions/IPluralStringLocalizer.cs index 0ab57486c0b..2fbdd4b4b31 100644 --- a/src/OrchardCore/OrchardCore.Localization.Abstractions/IPluralStringLocalizer.cs +++ b/src/OrchardCore/OrchardCore.Localization.Abstractions/IPluralStringLocalizer.cs @@ -1,18 +1,17 @@ using Microsoft.Extensions.Localization; -namespace OrchardCore.Localization +namespace OrchardCore.Localization; + +/// +/// Contract that extends to support pluralization. +/// +public interface IPluralStringLocalizer : IStringLocalizer { /// - /// Contract that extends to support pluralization. + /// Gets the localized strings. /// - public interface IPluralStringLocalizer : IStringLocalizer - { - /// - /// Gets the localized strings. - /// - /// The resource name. - /// Optional parameters that can be used inside the resource key. - /// A list of localized strings including the plural forms. - (LocalizedString, object[]) GetTranslation(string name, params object[] arguments); - } + /// The resource name. + /// Optional parameters that can be used inside the resource key. + /// A list of localized strings including the plural forms. + (LocalizedString, object[]) GetTranslation(string name, params object[] arguments); } diff --git a/src/OrchardCore/OrchardCore.Localization.Abstractions/ITranslationProvider.cs b/src/OrchardCore/OrchardCore.Localization.Abstractions/ITranslationProvider.cs index e25d92a35df..84ee49d1d4b 100644 --- a/src/OrchardCore/OrchardCore.Localization.Abstractions/ITranslationProvider.cs +++ b/src/OrchardCore/OrchardCore.Localization.Abstractions/ITranslationProvider.cs @@ -1,15 +1,26 @@ -namespace OrchardCore.Localization +using System; +using System.Threading.Tasks; + +namespace OrchardCore.Localization; + +/// +/// Contract to provide a translations. +/// +public interface ITranslationProvider { /// - /// Contract to provide a translations. + /// Loads translations from a certain source for a specific culture. /// - public interface ITranslationProvider - { - /// - /// Loads translations from a certain source for a specific culture. - /// - /// The culture name. - /// The that will contains all loaded translations. - void LoadTranslations(string cultureName, CultureDictionary dictionary); - } + /// The culture name. + /// The that will contains all loaded translations. + [Obsolete("This method has been deprectaed, please use LoadTranslationsAsync instead.")] + void LoadTranslations(string cultureName, CultureDictionary dictionary) + => LoadTranslationsAsync(cultureName, dictionary).GetAwaiter().GetResult(); + + /// + /// Loads translations from a certain source for a specific culture. + /// + /// The culture name. + /// The that will contains all loaded translations. + Task LoadTranslationsAsync(string cultureName, CultureDictionary dictionary); } diff --git a/src/OrchardCore/OrchardCore.Localization.Core/LocalizationManager.cs b/src/OrchardCore/OrchardCore.Localization.Core/LocalizationManager.cs index 828b9d5848c..2e8a9a93944 100644 --- a/src/OrchardCore/OrchardCore.Localization.Core/LocalizationManager.cs +++ b/src/OrchardCore/OrchardCore.Localization.Core/LocalizationManager.cs @@ -3,64 +3,64 @@ using System.Globalization; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; -namespace OrchardCore.Localization +namespace OrchardCore.Localization; + +/// +/// Represents a manager that manage the localization resources. +/// +public class LocalizationManager : ILocalizationManager { + private const string CacheKeyPrefix = "CultureDictionary-"; + + private static readonly PluralizationRuleDelegate _defaultPluralRule = n => (n != 1 ? 1 : 0); + + private readonly IList _pluralRuleProviders; + private readonly IEnumerable _translationProviders; + private readonly IMemoryCache _cache; + /// - /// Represents a manager that manage the localization resources. + /// Creates a new instance of . /// - public class LocalizationManager : ILocalizationManager + /// A list of s. + /// The list of available . + /// The . + public LocalizationManager( + IEnumerable pluralRuleProviders, + IEnumerable translationProviders, + IMemoryCache cache) { - private const string CacheKeyPrefix = "CultureDictionary-"; - - private static readonly PluralizationRuleDelegate _defaultPluralRule = n => (n != 1 ? 1 : 0); - - private readonly IList _pluralRuleProviders; - private readonly IEnumerable _translationProviders; - private readonly IMemoryCache _cache; + _pluralRuleProviders = pluralRuleProviders.OrderBy(o => o.Order).ToArray(); + _translationProviders = translationProviders; + _cache = cache; + } - /// - /// Creates a new instance of . - /// - /// A list of s. - /// The list of available . - /// The . - public LocalizationManager( - IEnumerable pluralRuleProviders, - IEnumerable translationProviders, - IMemoryCache cache) + /// + public async Task GetDictionaryAsync(CultureInfo culture) + { + var cachedDictionary = _cache.GetOrCreate(CacheKeyPrefix + culture.Name, k => new Lazy>(async () => { - _pluralRuleProviders = pluralRuleProviders.OrderBy(o => o.Order).ToArray(); - _translationProviders = translationProviders; - _cache = cache; - } + var rule = _defaultPluralRule; - /// - public CultureDictionary GetDictionary(CultureInfo culture) - { - var cachedDictionary = _cache.GetOrCreate(CacheKeyPrefix + culture.Name, k => new Lazy(() => + foreach (var provider in _pluralRuleProviders) { - var rule = _defaultPluralRule; - - foreach (var provider in _pluralRuleProviders) + if (provider.TryGetRule(culture, out rule)) { - if (provider.TryGetRule(culture, out rule)) - { - break; - } + break; } + } - var dictionary = new CultureDictionary(culture.Name, rule ?? _defaultPluralRule); - foreach (var translationProvider in _translationProviders) - { - translationProvider.LoadTranslations(culture.Name, dictionary); - } + var dictionary = new CultureDictionary(culture.Name, rule ?? _defaultPluralRule); + foreach (var translationProvider in _translationProviders) + { + await translationProvider.LoadTranslationsAsync(culture.Name, dictionary); + } - return dictionary; - }, LazyThreadSafetyMode.ExecutionAndPublication)); + return dictionary; + }, LazyThreadSafetyMode.ExecutionAndPublication)); - return cachedDictionary.Value; - } + return await cachedDictionary.Value; } } diff --git a/src/OrchardCore/OrchardCore.Localization.Core/OrchardCore.Localization.Core.csproj b/src/OrchardCore/OrchardCore.Localization.Core/OrchardCore.Localization.Core.csproj index 9528b3a6787..71ec706f0bd 100644 --- a/src/OrchardCore/OrchardCore.Localization.Core/OrchardCore.Localization.Core.csproj +++ b/src/OrchardCore/OrchardCore.Localization.Core/OrchardCore.Localization.Core.csproj @@ -13,6 +13,10 @@ + + + + diff --git a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoFilesTranslationsProvider.cs b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoFilesTranslationsProvider.cs index 4c49f5f8116..bd83c8691ef 100644 --- a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoFilesTranslationsProvider.cs +++ b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoFilesTranslationsProvider.cs @@ -1,43 +1,43 @@ using System.IO; +using System.Threading.Tasks; using Microsoft.Extensions.FileProviders; -namespace OrchardCore.Localization.PortableObject +namespace OrchardCore.Localization.PortableObject; + +/// +/// Represents a provider that provides a translations for .po files. +/// +public class PoFilesTranslationsProvider : ITranslationProvider { + private readonly ILocalizationFileLocationProvider _poFilesLocationProvider; + private readonly PoParser _parser; + /// - /// Represents a provider that provides a translations for .po files. + /// Creates a new instance of . /// - public class PoFilesTranslationsProvider : ITranslationProvider + /// The . + public PoFilesTranslationsProvider(ILocalizationFileLocationProvider poFileLocationProvider) { - private readonly ILocalizationFileLocationProvider _poFilesLocationProvider; - private readonly PoParser _parser; - - /// - /// Creates a new instance of . - /// - /// The . - public PoFilesTranslationsProvider(ILocalizationFileLocationProvider poFileLocationProvider) - { - _poFilesLocationProvider = poFileLocationProvider; - _parser = new PoParser(); - } + _poFilesLocationProvider = poFileLocationProvider; + _parser = new PoParser(); + } - /// - public void LoadTranslations(string cultureName, CultureDictionary dictionary) + /// + public async Task LoadTranslationsAsync(string cultureName, CultureDictionary dictionary) + { + foreach (var fileInfo in _poFilesLocationProvider.GetLocations(cultureName)) { - foreach (var fileInfo in _poFilesLocationProvider.GetLocations(cultureName)) - { - LoadFileToDictionary(fileInfo, dictionary); - } + await LoadFileToDictionaryAsync(fileInfo, dictionary); } + } - private void LoadFileToDictionary(IFileInfo fileInfo, CultureDictionary dictionary) + private async Task LoadFileToDictionaryAsync(IFileInfo fileInfo, CultureDictionary dictionary) + { + if (fileInfo.Exists && !fileInfo.IsDirectory) { - if (fileInfo.Exists && !fileInfo.IsDirectory) - { - using var stream = fileInfo.CreateReadStream(); - using var reader = new StreamReader(stream); - dictionary.MergeTranslations(_parser.Parse(reader)); - } + using var stream = fileInfo.CreateReadStream(); + using var reader = new StreamReader(stream); + dictionary.MergeTranslations(await _parser.ParseAsync(reader)); } } } diff --git a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoParser.cs b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoParser.cs index c04878f56de..2a95f75a121 100644 --- a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoParser.cs +++ b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoParser.cs @@ -3,224 +3,235 @@ using System.IO; using System.Linq; using System.Text; +using System.Threading.Tasks; -namespace OrchardCore.Localization.PortableObject +namespace OrchardCore.Localization.PortableObject; + +/// +/// Represents a parser for portable objects. +/// +public class PoParser { + private static readonly Dictionary _escapeTranslations = new() + { + { 'n', '\n' }, + { 'r', '\r' }, + { 't', '\t' }, + }; + /// - /// Represents a parser for portable objects. + /// Parses a .po file. /// - public class PoParser - { - private static readonly Dictionary _escapeTranslations = new() - { - { 'n', '\n' }, - { 'r', '\r' }, - { 't', '\t' }, - }; + /// The . + /// A list of culture records. + [Obsolete("This methos has been deprecated, please use ParseAsync instead.")] + public IEnumerable Parse(TextReader reader) => ParseAsync(reader).GetAwaiter().GetResult(); - /// - /// Parses a .po file. - /// - /// The . - /// A list of culture records. + /// + /// Parses a .po file. + /// + /// The . + /// A list of culture records. #pragma warning disable CA1822 // Mark members as static - public IEnumerable Parse(TextReader reader) + public async Task> ParseAsync(TextReader reader) #pragma warning restore CA1822 // Mark members as static + { + var entryBuilder = new DictionaryRecordBuilder(); + var cultureDictionaryRecords = new List(); + string line; + while ((line = await reader.ReadLineAsync()) != null) { - var entryBuilder = new DictionaryRecordBuilder(); - string line; - while ((line = reader.ReadLine()) != null) - { - (var context, var content) = ParseLine(line); - - if (context == PoContext.Other) - { - continue; - } - - // msgid or msgctxt are first lines of the entry. If builder contains valid entry return it and start building a new one. - if ((context == PoContext.MessageId || context == PoContext.MessageContext) && entryBuilder.ShouldFlushRecord) - { - yield return entryBuilder.BuildRecordAndReset(); - } + (var context, var content) = ParseLine(line); - entryBuilder.Set(context, content); + if (context == PoContext.Other) + { + continue; } - if (entryBuilder.ShouldFlushRecord) + // msgid or msgctxt are first lines of the entry. If builder contains valid entry return it and start building a new one. + if ((context == PoContext.MessageId || context == PoContext.MessageContext) && entryBuilder.ShouldFlushRecord) { - yield return entryBuilder.BuildRecordAndReset(); + cultureDictionaryRecords.Add(entryBuilder.BuildRecordAndReset()); } + + entryBuilder.Set(context, content); } - private static string Unescape(string str) + if (entryBuilder.ShouldFlushRecord) + { + cultureDictionaryRecords.Add(entryBuilder.BuildRecordAndReset()); + } + + return cultureDictionaryRecords; + } + + private static string Unescape(string str) + { + StringBuilder sb = null; + var escaped = false; + for (var i = 0; i < str.Length; i++) { - StringBuilder sb = null; - var escaped = false; - for (var i = 0; i < str.Length; i++) + var c = str[i]; + if (escaped) { - var c = str[i]; - if (escaped) + if (sb == null) { - if (sb == null) + sb = new StringBuilder(str.Length); + if (i > 1) { - sb = new StringBuilder(str.Length); - if (i > 1) - { - sb.Append(str[..(i - 1)]); - } + sb.Append(str[..(i - 1)]); } + } - char unescaped; - if (_escapeTranslations.TryGetValue(c, out unescaped)) - { - sb.Append(unescaped); - } - else - { - // General rule: \x ==> x - sb.Append(c); - } - escaped = false; + char unescaped; + if (_escapeTranslations.TryGetValue(c, out unescaped)) + { + sb.Append(unescaped); } else { - if (c == '\\') - { - escaped = true; - } - else - { - sb?.Append(c); - } + // General rule: \x ==> x + sb.Append(c); } + escaped = false; } - - return sb?.ToString() ?? str; - } - - private static string TrimQuote(string str) - { - if (str.StartsWith('\"') && str.EndsWith('\"')) + else { - if (str.Length == 1) + if (c == '\\') { - return ""; + escaped = true; + } + else + { + sb?.Append(c); } - - return str[1..^1]; } - - return str; } - private static (PoContext context, string content) ParseLine(string line) + return sb?.ToString() ?? str; + } + + private static string TrimQuote(string str) + { + if (str.StartsWith('\"') && str.EndsWith('\"')) { - if (line.StartsWith('\"')) + if (str.Length == 1) { - return (PoContext.Text, Unescape(TrimQuote(line.Trim()))); + return ""; } - var keyAndValue = line.Split(null, 2); - if (keyAndValue.Length != 2) - { - return (PoContext.Other, string.Empty); - } + return str[1..^1]; + } - var content = Unescape(TrimQuote(keyAndValue[1].Trim())); - return keyAndValue[0] switch - { - "msgctxt" => (PoContext.MessageContext, content), - "msgid" => (PoContext.MessageId, content), - "msgid_plural" => (PoContext.MessageIdPlural, content), - var key when key.StartsWith("msgstr", StringComparison.Ordinal) => (PoContext.Translation, content), - _ => (PoContext.Other, content), - }; + return str; + } + + private static (PoContext context, string content) ParseLine(string line) + { + if (line.StartsWith('\"')) + { + return (PoContext.Text, Unescape(TrimQuote(line.Trim()))); } - private sealed class DictionaryRecordBuilder + var keyAndValue = line.Split(null, 2); + if (keyAndValue.Length != 2) { - private readonly List _values; - private IEnumerable _validValues => _values.Where(value => !string.IsNullOrEmpty(value)); - private PoContext _context; + return (PoContext.Other, string.Empty); + } - public string MessageId { get; private set; } - public string MessageContext { get; private set; } + var content = Unescape(TrimQuote(keyAndValue[1].Trim())); + return keyAndValue[0] switch + { + "msgctxt" => (PoContext.MessageContext, content), + "msgid" => (PoContext.MessageId, content), + "msgid_plural" => (PoContext.MessageIdPlural, content), + var key when key.StartsWith("msgstr", StringComparison.Ordinal) => (PoContext.Translation, content), + _ => (PoContext.Other, content), + }; + } - public IEnumerable Values => _values; + private sealed class DictionaryRecordBuilder + { + private readonly List _values; + private IEnumerable _validValues => _values.Where(value => !string.IsNullOrEmpty(value)); + private PoContext _context; - public bool IsValid => !string.IsNullOrEmpty(MessageId) && _validValues.Any(); - public bool ShouldFlushRecord => IsValid && _context == PoContext.Translation; + public string MessageId { get; private set; } + public string MessageContext { get; private set; } - public DictionaryRecordBuilder() - { - _values = []; - } + public IEnumerable Values => _values; + + public bool IsValid => !string.IsNullOrEmpty(MessageId) && _validValues.Any(); + public bool ShouldFlushRecord => IsValid && _context == PoContext.Translation; - public void Set(PoContext context, string text) + public DictionaryRecordBuilder() + { + _values = []; + } + + public void Set(PoContext context, string text) + { + switch (context) { - switch (context) - { - case PoContext.MessageId: + case PoContext.MessageId: + { + // If the MessageId has been set to an empty string and now gets set again + // before flushing the values should be reset. + if (string.IsNullOrEmpty(MessageId)) { - // If the MessageId has been set to an empty string and now gets set again - // before flushing the values should be reset. - if (string.IsNullOrEmpty(MessageId)) - { - _values.Clear(); - } - - MessageId = text; - break; + _values.Clear(); } - case PoContext.MessageContext: MessageContext = text; break; - case PoContext.Translation: _values.Add(text); break; - case PoContext.Text: AppendText(text); return; // We don't want to set context to Text. - } - _context = context; + MessageId = text; + break; + } + case PoContext.MessageContext: MessageContext = text; break; + case PoContext.Translation: _values.Add(text); break; + case PoContext.Text: AppendText(text); return; // We don't want to set context to Text. } - private void AppendText(string text) + _context = context; + } + + private void AppendText(string text) + { + switch (_context) { - switch (_context) - { - case PoContext.MessageId: MessageId += text; break; - case PoContext.MessageContext: MessageContext += text; break; - case PoContext.Translation: - if (_values.Count > 0) - { - _values[^1] += text; - } - break; - } + case PoContext.MessageId: MessageId += text; break; + case PoContext.MessageContext: MessageContext += text; break; + case PoContext.Translation: + if (_values.Count > 0) + { + _values[^1] += text; + } + break; } + } - public CultureDictionaryRecord BuildRecordAndReset() + public CultureDictionaryRecord BuildRecordAndReset() + { + if (!IsValid) { - if (!IsValid) - { - return null; - } + return null; + } - var result = new CultureDictionaryRecord(MessageId, MessageContext, _validValues.ToArray()); + var result = new CultureDictionaryRecord(MessageId, MessageContext, _validValues.ToArray()); - MessageId = null; - MessageContext = null; - _values.Clear(); + MessageId = null; + MessageContext = null; + _values.Clear(); - return result; - } + return result; } + } - private enum PoContext - { - MessageId, - MessageIdPlural, - MessageContext, - Translation, - Text, - Other - } + private enum PoContext + { + MessageId, + MessageIdPlural, + MessageContext, + Translation, + Text, + Other } } diff --git a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs index 79120b6ec16..00a6b738f07 100644 --- a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs +++ b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs @@ -71,8 +71,8 @@ public virtual IEnumerable GetAllStrings(bool includeParentCult var culture = CultureInfo.CurrentUICulture; return includeParentCultures - ? GetAllStringsFromCultureHierarchy(culture) - : GetAllStrings(culture); + ? GetAllStringsFromCultureHierarchyAsync(culture) + : GetAllStringsAsync(culture).ToEnumerable(); } /// @@ -109,9 +109,9 @@ public virtual (LocalizedString, object[]) GetTranslation(string name, params ob } } - private IEnumerable GetAllStrings(CultureInfo culture) + private async IAsyncEnumerable GetAllStringsAsync(CultureInfo culture) { - var dictionary = _localizationManager.GetDictionary(culture); + var dictionary = await _localizationManager.GetDictionaryAsync(culture); foreach (var translation in dictionary.Translations) { @@ -119,14 +119,14 @@ private IEnumerable GetAllStrings(CultureInfo culture) } } - private List GetAllStringsFromCultureHierarchy(CultureInfo culture) + private List GetAllStringsFromCultureHierarchyAsync(CultureInfo culture) { var currentCulture = culture; var allLocalizedStrings = new List(); do { - var localizedStrings = GetAllStrings(currentCulture); + var localizedStrings = GetAllStringsAsync(currentCulture).ToEnumerable(); if (localizedStrings != null) { @@ -147,7 +147,7 @@ private List GetAllStringsFromCultureHierarchy(CultureInfo cult protected string GetTranslation(string[] pluralForms, CultureInfo culture, int? count) { - var dictionary = _localizationManager.GetDictionary(culture); + var dictionary = _localizationManager.GetDictionaryAsync(culture).GetAwaiter().GetResult(); var pluralForm = count.HasValue ? dictionary.PluralRule(count.Value) : 0; @@ -190,7 +190,7 @@ protected string GetTranslation(string name, string context, CultureInfo culture string ExtractTranslation() { - var dictionary = _localizationManager.GetDictionary(culture); + var dictionary = _localizationManager.GetDictionaryAsync(culture).GetAwaiter().GetResult(); if (dictionary != null) { diff --git a/test/OrchardCore.Tests/Localization/LocalizationManagerTests.cs b/test/OrchardCore.Tests/Localization/LocalizationManagerTests.cs index d95ebab0e5c..fc768519c5e 100644 --- a/test/OrchardCore.Tests/Localization/LocalizationManagerTests.cs +++ b/test/OrchardCore.Tests/Localization/LocalizationManagerTests.cs @@ -20,32 +20,32 @@ public LocalizationManagerTests() } [Fact] - public void GetDictionaryReturnsDictionaryWithPluralRuleAndCultureIfNoTranslationsExists() + public async Task GetDictionaryReturnsDictionaryWithPluralRuleAndCultureIfNoTranslationsExists() { - _translationProvider.Setup(o => o.LoadTranslations( + _translationProvider.Setup(o => o.LoadTranslationsAsync( It.Is(culture => culture == "cs"), It.IsAny()) ); var manager = new LocalizationManager(new[] { _pluralRuleProvider.Object }, new[] { _translationProvider.Object }, _memoryCache); - var dictionary = manager.GetDictionary(new CultureInfo("cs")); + var dictionary = await manager.GetDictionaryAsync(new CultureInfo("cs")); Assert.Equal("cs", dictionary.CultureName); Assert.Equal(PluralizationRule.Czech, dictionary.PluralRule); } [Fact] - public void GetDictionaryReturnsDictionaryWithTranslationsFromProvider() + public async Task GetDictionaryReturnsDictionaryWithTranslationsFromProvider() { var dictionaryRecord = new CultureDictionaryRecord("ball", "míč", "míče", "míčů"); _translationProvider - .Setup(o => o.LoadTranslations(It.Is(culture => culture == "cs"), It.IsAny())) + .Setup(o => o.LoadTranslationsAsync(It.Is(culture => culture == "cs"), It.IsAny())) .Callback((culture, dictioanry) => dictioanry.MergeTranslations(new[] { dictionaryRecord })); var manager = new LocalizationManager(new[] { _pluralRuleProvider.Object }, new[] { _translationProvider.Object }, _memoryCache); - var dictionary = manager.GetDictionary(new CultureInfo("cs")); + var dictionary = await manager.GetDictionaryAsync(new CultureInfo("cs")); var key = new CultureDictionaryRecordKey { MessageId = "ball" }; dictionary.Translations.TryGetValue(key, out var translations); @@ -54,7 +54,7 @@ public void GetDictionaryReturnsDictionaryWithTranslationsFromProvider() } [Fact] - public void GetDictionarySelectsPluralRuleFromProviderWithHigherPriority() + public async Task GetDictionarySelectsPluralRuleFromProviderWithHigherPriority() { PluralizationRuleDelegate csPluralRuleOverride = n => ((n == 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 0); @@ -62,14 +62,14 @@ public void GetDictionarySelectsPluralRuleFromProviderWithHigherPriority() highPriorityRuleProvider.SetupGet(o => o.Order).Returns(-1); highPriorityRuleProvider.Setup(o => o.TryGetRule(It.Is(culture => culture.Name == "cs"), out csPluralRuleOverride)).Returns(true); - _translationProvider.Setup(o => o.LoadTranslations( + _translationProvider.Setup(o => o.LoadTranslationsAsync( It.Is(culture => culture == "cs"), It.IsAny()) ); var manager = new LocalizationManager(new[] { _pluralRuleProvider.Object, highPriorityRuleProvider.Object }, new[] { _translationProvider.Object }, _memoryCache); - var dictionary = manager.GetDictionary(new CultureInfo("cs")); + var dictionary = await manager.GetDictionaryAsync(new CultureInfo("cs")); Assert.Equal(dictionary.PluralRule, csPluralRuleOverride); } diff --git a/test/OrchardCore.Tests/Localization/PoParserTests.cs b/test/OrchardCore.Tests/Localization/PoParserTests.cs index f392905e5b1..688a8ab213c 100644 --- a/test/OrchardCore.Tests/Localization/PoParserTests.cs +++ b/test/OrchardCore.Tests/Localization/PoParserTests.cs @@ -6,28 +6,28 @@ namespace OrchardCore.Tests.Localization public class PoParserTests { [Fact] - public void ParseRetursSimpleEntry() + public async Task ParseRetursSimpleEntry() { // msgid "Unknown system error" // msgstr "Error desconegut del sistema" - var entries = ParseText("SimpleEntry"); + var entries = await ParseTextAsync("SimpleEntry"); Assert.Equal("Unknown system error", entries[0].Key); Assert.Equal("Error desconegut del sistema", entries[0].Translations[0]); } [Fact] - public void ParseIgnoresEntryWithoutTranslation() + public async Task ParseIgnoresEntryWithoutTranslation() { // "msgid "Unknown system error" // "msgstr "" - var entries = ParseText("EntryWithoutTranslation"); + var entries = await ParseTextAsync("EntryWithoutTranslation"); Assert.Empty(entries); } [Fact] - public void ParseIgnoresPoeditHeader() + public async Task ParseIgnoresPoeditHeader() { // # Translation of kstars.po into Spanish. // # This file is distributed under the same license as the kdeedu package. @@ -48,25 +48,25 @@ public void ParseIgnoresPoeditHeader() // msgid "Unknown system error" // msgstr "Error desconegut del sistema" - var entries = ParseText("PoeditHeader"); + var entries = await ParseTextAsync("PoeditHeader"); Assert.True(entries.Length == 1); Assert.True(entries[0].Translations.Length == 1); } [Fact] - public void ParseSetsContext() + public async Task ParseSetsContext() { // msgctxt "OrchardCore.Localization" // msgid "Unknown system error" // msgstr "Error desconegut del sistema" - var entries = ParseText("EntryWithContext"); + var entries = await ParseTextAsync("EntryWithContext"); Assert.Equal("OrchardCore.Localization|Unknown system error", entries[0].Key, ignoreCase: true); } [Fact] - public void ParseIgnoresComments() + public async Task ParseIgnoresComments() { // # translator-comments // #. extracted-comments @@ -79,38 +79,38 @@ public void ParseIgnoresComments() // msgid "Unknown system error" // msgstr "Error desconegut del sistema" - var entries = ParseText("EntryWithComments"); + var entries = await ParseTextAsync("EntryWithComments"); Assert.Equal("OrchardCore.Localization|Unknown system error", entries[0].Key, ignoreCase: true); Assert.Equal("Error desconegut del sistema", entries[0].Translations[0]); } [Fact] - public void ParseOnlyTrimsLeadingAndTrailingQuotes() + public async Task ParseOnlyTrimsLeadingAndTrailingQuotes() { // msgid "\"{0}\"" // msgstr "\"{0}\"" - var entries = ParseText("EntryWithQuotes"); + var entries = await ParseTextAsync("EntryWithQuotes"); Assert.Equal("\"{0}\"", entries[0].Key); Assert.Equal("\"{0}\"", entries[0].Translations[0]); } [Fact] - public void ParseHandleUnclosedQuote() + public async Task ParseHandleUnclosedQuote() { // msgctxt " // msgid "Foo \"{0}\"" // msgstr "Foo \"{0}\"" - var entries = ParseText("EntryWithUnclosedQuote"); + var entries = await ParseTextAsync("EntryWithUnclosedQuote"); Assert.Equal("Foo \"{0}\"", entries[0].Key); } [Fact] - public void ParseHandlesMultilineEntry() + public async Task ParseHandlesMultilineEntry() { // msgid "" // "Here is an example of how one might continue a very long string\n" @@ -119,26 +119,26 @@ public void ParseHandlesMultilineEntry() // "Here is an example of how one might continue a very long translation\n" // "for the common case the string represents multi-line output." - var entries = ParseText("EntryWithMultilineText"); + var entries = await ParseTextAsync("EntryWithMultilineText"); Assert.Equal("Here is an example of how one might continue a very long string\nfor the common case the string represents multi-line output.", entries[0].Key); Assert.Equal("Here is an example of how one might continue a very long translation\nfor the common case the string represents multi-line output.", entries[0].Translations[0]); } [Fact] - public void ParsePreservesEscapedCharacters() + public async Task ParsePreservesEscapedCharacters() { // msgid "Line:\t\"{0}\"\n" // msgstr "Line:\t\"{0}\"\n" - var entries = ParseText("EntryWithEscapedCharacters"); + var entries = await ParseTextAsync("EntryWithEscapedCharacters"); Assert.Equal("Line:\t\"{0}\"\n", entries[0].Key); Assert.Equal("Line:\t\"{0}\"\n", entries[0].Translations[0]); } [Fact] - public void ParseReadsPluralTranslations() + public async Task ParseReadsPluralTranslations() { // msgid "book" // msgid_plural "books" @@ -146,7 +146,7 @@ public void ParseReadsPluralTranslations() // msgstr[1] "knihy" // msgstr[2] "knih" - var entries = ParseText("EntryWithPlural"); + var entries = await ParseTextAsync("EntryWithPlural"); Assert.Equal("book", entries[0].Key); Assert.Equal("kniha", entries[0].Translations[0]); @@ -155,7 +155,7 @@ public void ParseReadsPluralTranslations() } [Fact] - public void ParseReadsPluralAndMultilineText() + public async Task ParseReadsPluralAndMultilineText() { // msgid "" // "Here is an example of how one might continue a very long string\n" @@ -170,7 +170,7 @@ public void ParseReadsPluralAndMultilineText() // "Here are examples of how one might continue a very long translation\n" // "for the common case the string represents multi-line output." - var entries = ParseText("EntryWithPluralAndMultilineText"); + var entries = await ParseTextAsync("EntryWithPluralAndMultilineText"); Assert.Equal("Here is an example of how one might continue a very long string\nfor the common case the string represents multi-line output.", entries[0].Key); Assert.Equal("Here is an example of how one might continue a very long translation\nfor the common case the string represents multi-line output.", entries[0].Translations[0]); @@ -178,7 +178,7 @@ public void ParseReadsPluralAndMultilineText() } [Fact] - public void ParseReadsMultipleEntries() + public async Task ParseReadsMultipleEntries() { // #. "File {0} does not exist" // msgctxt "OrchardCore.FileSystems.Media.FileSystemStorageProvider" @@ -190,7 +190,7 @@ public void ParseReadsMultipleEntries() // msgid "Directory {0} does not exist" // msgstr "Složka {0} neexistuje" - var entries = ParseText("MultipleEntries"); + var entries = await ParseTextAsync("MultipleEntries"); Assert.Equal(2, entries.Length); @@ -201,14 +201,15 @@ public void ParseReadsMultipleEntries() Assert.Equal("Složka {0} neexistuje", entries[1].Translations[0]); } - private static CultureDictionaryRecord[] ParseText(string resourceName) + private static async Task ParseTextAsync(string resourceName) { var parser = new PoParser(); var testAssembly = typeof(PoParserTests).Assembly; using var resource = testAssembly.GetManifestResourceStream("OrchardCore.Tests.Localization.PoFiles." + resourceName + ".po"); using var reader = new StreamReader(resource); - return parser.Parse(reader).ToArray(); + + return (await parser.ParseAsync(reader)).ToArray(); } } } diff --git a/test/OrchardCore.Tests/Localization/PortableObjectStringLocalizerTests.cs b/test/OrchardCore.Tests/Localization/PortableObjectStringLocalizerTests.cs index be361129d41..82b7eef70b5 100644 --- a/test/OrchardCore.Tests/Localization/PortableObjectStringLocalizerTests.cs +++ b/test/OrchardCore.Tests/Localization/PortableObjectStringLocalizerTests.cs @@ -343,7 +343,7 @@ public void LocalizerWithContextShouldCallGetDictionaryOncePerCulture(string cul var translation = localizer["Hello"]; // Assert - _localizationManager.Verify(lm => lm.GetDictionary(It.IsAny()), Times.Exactly(expectedCalls)); + _localizationManager.Verify(lm => lm.GetDictionaryAsync(It.IsAny()), Times.Exactly(expectedCalls)); Assert.Equal("Hello", translation); } @@ -359,7 +359,8 @@ private void SetupDictionary(string cultureName, IEnumerable o.GetDictionary(It.Is(c => c.Name == cultureName))).Returns(dictionary); + _localizationManager.Setup(o => o.GetDictionaryAsync(It.Is(c => c.Name == cultureName))) + .ReturnsAsync(dictionary); } public class PortableObjectLocalizationStartup From 62a0e0b8357e8c01ff8717604e7b527b370ef395 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Fri, 26 Apr 2024 13:52:24 +0300 Subject: [PATCH 2/9] IPluralStringLocalizer.GetTranslationAsync() --- .../IPluralStringLocalizer.cs | 14 +- .../PortableObjectStringLocalizer.cs | 340 +++++++++--------- 2 files changed, 187 insertions(+), 167 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Localization.Abstractions/IPluralStringLocalizer.cs b/src/OrchardCore/OrchardCore.Localization.Abstractions/IPluralStringLocalizer.cs index 2fbdd4b4b31..f568411892e 100644 --- a/src/OrchardCore/OrchardCore.Localization.Abstractions/IPluralStringLocalizer.cs +++ b/src/OrchardCore/OrchardCore.Localization.Abstractions/IPluralStringLocalizer.cs @@ -1,3 +1,5 @@ +using System; +using System.Threading.Tasks; using Microsoft.Extensions.Localization; namespace OrchardCore.Localization; @@ -13,5 +15,15 @@ public interface IPluralStringLocalizer : IStringLocalizer /// The resource name. /// Optional parameters that can be used inside the resource key. /// A list of localized strings including the plural forms. - (LocalizedString, object[]) GetTranslation(string name, params object[] arguments); + [Obsolete("This method is deprecated, please use GetTranslationAsync instead.")] + (LocalizedString, object[]) GetTranslation(string name, params object[] arguments) + => GetTranslationAsync(name, arguments).GetAwaiter().GetResult(); + + /// + /// Gets the localized strings. + /// + /// The resource name. + /// Optional parameters that can be used inside the resource key. + /// A list of localized strings including the plural forms. + Task<(LocalizedString, object[])> GetTranslationAsync(string name, params object[] arguments); } diff --git a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs index 00a6b738f07..b046ca127d5 100644 --- a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs +++ b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs @@ -2,242 +2,250 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Threading.Tasks; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using OrchardCore.Localization.DataAnnotations; -namespace OrchardCore.Localization.PortableObject +namespace OrchardCore.Localization.PortableObject; + +/// +/// Represents for portable objects. +/// +public class PortableObjectStringLocalizer : IPluralStringLocalizer { + private static readonly string _dataAnnotationsDefaultErrorMessagesContext = typeof(DataAnnotationsDefaultErrorMessages).FullName; + private static readonly string _localizedDataAnnotationsMvcOptionsContext = typeof(LocalizedDataAnnotationsMvcOptions).FullName; + + private readonly ILocalizationManager _localizationManager; + private readonly bool _fallBackToParentCulture; + private readonly ILogger _logger; + private readonly string _context; + /// - /// Represents for portable objects. + /// Creates a new instance of . /// - public class PortableObjectStringLocalizer : IPluralStringLocalizer + /// + /// + /// + /// + public PortableObjectStringLocalizer( + string context, + ILocalizationManager localizationManager, + bool fallBackToParentCulture, + ILogger logger) { - private static readonly string _dataAnnotationsDefaultErrorMessagesContext = typeof(DataAnnotationsDefaultErrorMessages).FullName; - private static readonly string _localizedDataAnnotationsMvcOptionsContext = typeof(LocalizedDataAnnotationsMvcOptions).FullName; - - private readonly ILocalizationManager _localizationManager; - private readonly bool _fallBackToParentCulture; - private readonly ILogger _logger; - private readonly string _context; - - /// - /// Creates a new instance of . - /// - /// - /// - /// - /// - public PortableObjectStringLocalizer( - string context, - ILocalizationManager localizationManager, - bool fallBackToParentCulture, - ILogger logger) - { - _context = context; - _localizationManager = localizationManager; - _fallBackToParentCulture = fallBackToParentCulture; - _logger = logger; - } + _context = context; + _localizationManager = localizationManager; + _fallBackToParentCulture = fallBackToParentCulture; + _logger = logger; + } - /// - public virtual LocalizedString this[string name] + /// + public virtual LocalizedString this[string name] + { + get { - get - { - ArgumentNullException.ThrowIfNull(name); - - var translation = GetTranslation(name, _context, CultureInfo.CurrentUICulture, null); - - return new LocalizedString(name, translation ?? name, translation == null); - } - } + ArgumentNullException.ThrowIfNull(name); - /// - public virtual LocalizedString this[string name, params object[] arguments] - { - get - { - var (translation, argumentsWithCount) = GetTranslation(name, arguments); - var formatted = string.Format(translation.Value, argumentsWithCount); + var translation = GetTranslationAsync(name, _context, CultureInfo.CurrentUICulture, null).GetAwaiter().GetResult(); - return new LocalizedString(name, formatted, translation.ResourceNotFound); - } + return new LocalizedString(name, translation ?? name, translation == null); } + } - /// - public virtual IEnumerable GetAllStrings(bool includeParentCultures) + /// + public virtual LocalizedString this[string name, params object[] arguments] + { + get { - var culture = CultureInfo.CurrentUICulture; + var (translation, argumentsWithCount) = GetTranslationAsync(name, arguments).GetAwaiter().GetResult(); + var formatted = string.Format(translation.Value, argumentsWithCount); - return includeParentCultures - ? GetAllStringsFromCultureHierarchyAsync(culture) - : GetAllStringsAsync(culture).ToEnumerable(); + return new LocalizedString(name, formatted, translation.ResourceNotFound); } + } - /// - public virtual (LocalizedString, object[]) GetTranslation(string name, params object[] arguments) - { - ArgumentNullException.ThrowIfNull(name); + /// + public virtual IEnumerable GetAllStrings(bool includeParentCultures) + { + var culture = CultureInfo.CurrentUICulture; - // Check if a plural form is called, which is when the only argument is of type PluralizationArgument. - if (arguments.Length == 1 && arguments[0] is PluralizationArgument pluralArgument) - { - var translation = GetTranslation(name, _context, CultureInfo.CurrentUICulture, pluralArgument.Count); + return includeParentCultures + ? GetAllStringsFromCultureHierarchyAsync(culture).GetAwaiter().GetResult() + : GetAllStringsAsync(culture).ToEnumerable(); + } - object[] argumentsWithCount; + /// + public virtual async Task<(LocalizedString, object[])> GetTranslationAsync(string name, params object[] arguments) + { + ArgumentNullException.ThrowIfNull(name); - if (pluralArgument.Arguments.Length > 0) - { - argumentsWithCount = new object[pluralArgument.Arguments.Length + 1]; - argumentsWithCount[0] = pluralArgument.Count; - Array.Copy(pluralArgument.Arguments, 0, argumentsWithCount, 1, pluralArgument.Arguments.Length); - } - else - { - argumentsWithCount = [pluralArgument.Count]; - } + // Check if a plural form is called, which is when the only argument is of type PluralizationArgument. + if (arguments.Length == 1 && arguments[0] is PluralizationArgument pluralArgument) + { + var translation = await GetTranslationAsync(name, _context, CultureInfo.CurrentUICulture, pluralArgument.Count); - translation ??= GetTranslation(pluralArgument.Forms, CultureInfo.CurrentUICulture, pluralArgument.Count); + object[] argumentsWithCount; - return (new LocalizedString(name, translation, translation == null), argumentsWithCount); + if (pluralArgument.Arguments.Length > 0) + { + argumentsWithCount = new object[pluralArgument.Arguments.Length + 1]; + argumentsWithCount[0] = pluralArgument.Count; + Array.Copy(pluralArgument.Arguments, 0, argumentsWithCount, 1, pluralArgument.Arguments.Length); } else { - var translation = this[name]; - return (new LocalizedString(name, translation, translation.ResourceNotFound), arguments); + argumentsWithCount = [pluralArgument.Count]; } - } - private async IAsyncEnumerable GetAllStringsAsync(CultureInfo culture) + translation ??= await GetTranslationAsync(pluralArgument.Forms, CultureInfo.CurrentUICulture, pluralArgument.Count); + + return (new LocalizedString(name, translation, translation == null), argumentsWithCount); + } + else { - var dictionary = await _localizationManager.GetDictionaryAsync(culture); + var translation = this[name]; + return (new LocalizedString(name, translation, translation.ResourceNotFound), arguments); + } + } - foreach (var translation in dictionary.Translations) - { - yield return new LocalizedString(translation.Key, translation.Value.FirstOrDefault()); - } + private async IAsyncEnumerable GetAllStringsAsync(CultureInfo culture) + { + var dictionary = await _localizationManager.GetDictionaryAsync(culture); + + foreach (var translation in dictionary.Translations) + { + yield return new LocalizedString(translation.Key, translation.Value.FirstOrDefault()); } + } + + private async Task> GetAllStringsFromCultureHierarchyAsync(CultureInfo culture) + { + var currentCulture = culture; + var allLocalizedStrings = new List(); - private List GetAllStringsFromCultureHierarchyAsync(CultureInfo culture) + do { - var currentCulture = culture; - var allLocalizedStrings = new List(); + var localizedStrings = await GetAllStringsAsync(currentCulture).ToListAsync(); - do + if (localizedStrings != null) { - var localizedStrings = GetAllStringsAsync(currentCulture).ToEnumerable(); - - if (localizedStrings != null) + foreach (var localizedString in localizedStrings) { - foreach (var localizedString in localizedStrings) + if (!allLocalizedStrings.Any(ls => ls.Name == localizedString.Name)) { - if (!allLocalizedStrings.Any(ls => ls.Name == localizedString.Name)) - { - allLocalizedStrings.Add(localizedString); - } + allLocalizedStrings.Add(localizedString); } } + } - currentCulture = currentCulture.Parent; - } while (currentCulture != currentCulture.Parent); + currentCulture = currentCulture.Parent; + } while (currentCulture != currentCulture.Parent); - return allLocalizedStrings; - } + return allLocalizedStrings; + } - protected string GetTranslation(string[] pluralForms, CultureInfo culture, int? count) - { - var dictionary = _localizationManager.GetDictionaryAsync(culture).GetAwaiter().GetResult(); + [Obsolete("This method is deprecated, please use GetTranslationAsync instead.")] + protected string GetTranslation(string[] pluralForms, CultureInfo culture, int? count) + => GetTranslationAsync(pluralForms, culture, count).GetAwaiter().GetResult(); - var pluralForm = count.HasValue ? dictionary.PluralRule(count.Value) : 0; + protected async Task GetTranslationAsync(string[] pluralForms, CultureInfo culture, int? count) + { + var dictionary = await _localizationManager.GetDictionaryAsync(culture); - if (pluralForm >= pluralForms.Length) - { - if (_logger.IsEnabled(LogLevel.Warning)) - { - _logger.LogWarning("Plural form '{PluralForm}' doesn't exist in values provided by the 'IStringLocalizer.Plural' method. Provided values: {PluralForms}", pluralForm, string.Join(", ", pluralForms)); - } + var pluralForm = count.HasValue ? dictionary.PluralRule(count.Value) : 0; - // Use the latest available form. - return pluralForms[^1]; + if (pluralForm >= pluralForms.Length) + { + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning("Plural form '{PluralForm}' doesn't exist in values provided by the 'IStringLocalizer.Plural' method. Provided values: {PluralForms}", pluralForm, string.Join(", ", pluralForms)); } - return pluralForms[pluralForm]; + // Use the latest available form. + return pluralForms[^1]; } - protected string GetTranslation(string name, string context, CultureInfo culture, int? count) + return pluralForms[pluralForm]; + } + + [Obsolete("This method has been deprecated please use instead.")] + protected string GetTranslation(string name, string context, CultureInfo culture, int? count) + => GetTranslationAsync(name, context, culture, count).GetAwaiter().GetResult(); + + protected async Task GetTranslationAsync(string name, string context, CultureInfo culture, int? count) + { + string translation = null; + try { - string translation = null; - try + if (_fallBackToParentCulture) { - if (_fallBackToParentCulture) + do { - do + if (await ExtractTranslationAsync() != null) { - if (ExtractTranslation() != null) - { - break; - } - - culture = culture.Parent; + break; } - while (culture != CultureInfo.InvariantCulture); - } - else - { - ExtractTranslation(); + + culture = culture.Parent; } + while (culture != CultureInfo.InvariantCulture); + } + else + { + await ExtractTranslationAsync(); + } - string ExtractTranslation() + async Task< string> ExtractTranslationAsync() + { + var dictionary = await _localizationManager.GetDictionaryAsync(culture); + + if (dictionary != null) { - var dictionary = _localizationManager.GetDictionaryAsync(culture).GetAwaiter().GetResult(); + var key = CultureDictionaryRecord.GetKey(name, context); - if (dictionary != null) + // Extract translation from data annotations attributes. + if (context == _localizedDataAnnotationsMvcOptionsContext) { - var key = CultureDictionaryRecord.GetKey(name, context); + // Extract translation with context. + key = CultureDictionaryRecord.GetKey(name, _dataAnnotationsDefaultErrorMessagesContext); + translation = dictionary[key]; - // Extract translation from data annotations attributes. - if (context == _localizedDataAnnotationsMvcOptionsContext) + if (translation != null) { - // Extract translation with context. - key = CultureDictionaryRecord.GetKey(name, _dataAnnotationsDefaultErrorMessagesContext); - translation = dictionary[key]; - - if (translation != null) - { - return translation; - } - - // Extract translation without context. - key = CultureDictionaryRecord.GetKey(name, null); - translation = dictionary[key]; - - if (translation != null) - { - return translation; - } + return translation; } - // Extract translation with context. - translation = dictionary[key, count]; + // Extract translation without context. + key = CultureDictionaryRecord.GetKey(name, null); + translation = dictionary[key]; - if (context != null && translation == null) + if (translation != null) { - // Extract translation without context. - key = CultureDictionaryRecord.GetKey(name, null); - translation = dictionary[key, count]; + return translation; } } - return translation; + // Extract translation with context. + translation = dictionary[key, count]; + + if (context != null && translation == null) + { + // Extract translation without context. + key = CultureDictionaryRecord.GetKey(name, null); + translation = dictionary[key, count]; + } } - } - catch (PluralFormNotFoundException ex) - { - _logger.LogWarning(ex, "Plural form not found."); - } - return translation; + return translation; + } } + catch (PluralFormNotFoundException ex) + { + _logger.LogWarning(ex, "Plural form not found."); + } + + return translation; } } From f8246fd2e1806fd2572a36b04bf36ec702d06b3b Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Fri, 26 Apr 2024 13:58:49 +0300 Subject: [PATCH 3/9] Fix the build --- .../PortableObject/PortableObjectHtmlLocalizer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectHtmlLocalizer.cs b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectHtmlLocalizer.cs index 03285d9017f..6576014370a 100644 --- a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectHtmlLocalizer.cs +++ b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectHtmlLocalizer.cs @@ -41,7 +41,7 @@ public override LocalizedHtmlString this[string name] if (_localizer is IPluralStringLocalizer pluralLocalizer && arguments.Length == 1 && arguments[0] is PluralizationArgument) { // Get an unformatted string and all non plural arguments (1st one is the plural count). - var (translation, argumentsWithCount) = pluralLocalizer.GetTranslation(name, arguments); + var (translation, argumentsWithCount) = pluralLocalizer.GetTranslationAsync(name, arguments).GetAwaiter().GetResult(); // Formatting will use non plural arguments if any. return ToHtmlString(translation, argumentsWithCount); From 0041fbf7f936191b3640359140bdd0fe5656bf57 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Fri, 26 Apr 2024 20:33:32 +0300 Subject: [PATCH 4/9] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoltán Lehóczky --- .../ITranslationProvider.cs | 2 +- .../OrchardCore.Localization.Core/PortableObject/PoParser.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Localization.Abstractions/ITranslationProvider.cs b/src/OrchardCore/OrchardCore.Localization.Abstractions/ITranslationProvider.cs index 84ee49d1d4b..8079260cbcb 100644 --- a/src/OrchardCore/OrchardCore.Localization.Abstractions/ITranslationProvider.cs +++ b/src/OrchardCore/OrchardCore.Localization.Abstractions/ITranslationProvider.cs @@ -13,7 +13,7 @@ public interface ITranslationProvider /// /// The culture name. /// The that will contains all loaded translations. - [Obsolete("This method has been deprectaed, please use LoadTranslationsAsync instead.")] + [Obsolete("This method has been deprecated, please use LoadTranslationsAsync instead.")] void LoadTranslations(string cultureName, CultureDictionary dictionary) => LoadTranslationsAsync(cultureName, dictionary).GetAwaiter().GetResult(); diff --git a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoParser.cs b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoParser.cs index 2a95f75a121..084bb8e04b0 100644 --- a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoParser.cs +++ b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoParser.cs @@ -24,7 +24,7 @@ public class PoParser /// /// The . /// A list of culture records. - [Obsolete("This methos has been deprecated, please use ParseAsync instead.")] + [Obsolete("This method has been deprecated, please use ParseAsync instead.")] public IEnumerable Parse(TextReader reader) => ParseAsync(reader).GetAwaiter().GetResult(); /// From ee4a6685c6a1df685fe7d87de68b3d16a8b9c691 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Fri, 26 Apr 2024 21:05:13 +0300 Subject: [PATCH 5/9] Address feedback --- .../LocalizationManager.cs | 8 +++----- .../PortableObject/PoFilesTranslationsProvider.cs | 3 ++- .../PortableObject/PoParser.cs | 13 +++++-------- .../OrchardCore.Tests/Localization/PoParserTests.cs | 2 +- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Localization.Core/LocalizationManager.cs b/src/OrchardCore/OrchardCore.Localization.Core/LocalizationManager.cs index 2e8a9a93944..c8ca5bdba10 100644 --- a/src/OrchardCore/OrchardCore.Localization.Core/LocalizationManager.cs +++ b/src/OrchardCore/OrchardCore.Localization.Core/LocalizationManager.cs @@ -1,8 +1,6 @@ -using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; @@ -40,7 +38,7 @@ public LocalizationManager( /// public async Task GetDictionaryAsync(CultureInfo culture) { - var cachedDictionary = _cache.GetOrCreate(CacheKeyPrefix + culture.Name, k => new Lazy>(async () => + var cachedDictionary = await _cache.GetOrCreateAsync(CacheKeyPrefix + culture.Name, async (e) => { var rule = _defaultPluralRule; @@ -59,8 +57,8 @@ public async Task GetDictionaryAsync(CultureInfo culture) } return dictionary; - }, LazyThreadSafetyMode.ExecutionAndPublication)); + }); - return await cachedDictionary.Value; + return cachedDictionary; } } diff --git a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoFilesTranslationsProvider.cs b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoFilesTranslationsProvider.cs index bd83c8691ef..0651a317cc1 100644 --- a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoFilesTranslationsProvider.cs +++ b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoFilesTranslationsProvider.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.FileProviders; @@ -37,7 +38,7 @@ private async Task LoadFileToDictionaryAsync(IFileInfo fileInfo, CultureDictiona { using var stream = fileInfo.CreateReadStream(); using var reader = new StreamReader(stream); - dictionary.MergeTranslations(await _parser.ParseAsync(reader)); + dictionary.MergeTranslations(await _parser.ParseAsync(reader).ToListAsync()); } } } diff --git a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoParser.cs b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoParser.cs index 084bb8e04b0..e80f2f0a6f0 100644 --- a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoParser.cs +++ b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PoParser.cs @@ -24,8 +24,8 @@ public class PoParser /// /// The . /// A list of culture records. - [Obsolete("This method has been deprecated, please use ParseAsync instead.")] - public IEnumerable Parse(TextReader reader) => ParseAsync(reader).GetAwaiter().GetResult(); + [Obsolete("This methos has been deprecated, please use ParseAsync instead.")] + public IEnumerable Parse(TextReader reader) => ParseAsync(reader).ToEnumerable(); /// /// Parses a .po file. @@ -33,11 +33,10 @@ public class PoParser /// The . /// A list of culture records. #pragma warning disable CA1822 // Mark members as static - public async Task> ParseAsync(TextReader reader) + public async IAsyncEnumerable ParseAsync(TextReader reader) #pragma warning restore CA1822 // Mark members as static { var entryBuilder = new DictionaryRecordBuilder(); - var cultureDictionaryRecords = new List(); string line; while ((line = await reader.ReadLineAsync()) != null) { @@ -51,7 +50,7 @@ public async Task> ParseAsync(TextReader re // msgid or msgctxt are first lines of the entry. If builder contains valid entry return it and start building a new one. if ((context == PoContext.MessageId || context == PoContext.MessageContext) && entryBuilder.ShouldFlushRecord) { - cultureDictionaryRecords.Add(entryBuilder.BuildRecordAndReset()); + yield return entryBuilder.BuildRecordAndReset(); } entryBuilder.Set(context, content); @@ -59,10 +58,8 @@ public async Task> ParseAsync(TextReader re if (entryBuilder.ShouldFlushRecord) { - cultureDictionaryRecords.Add(entryBuilder.BuildRecordAndReset()); + yield return entryBuilder.BuildRecordAndReset(); } - - return cultureDictionaryRecords; } private static string Unescape(string str) diff --git a/test/OrchardCore.Tests/Localization/PoParserTests.cs b/test/OrchardCore.Tests/Localization/PoParserTests.cs index 688a8ab213c..cd227c804e8 100644 --- a/test/OrchardCore.Tests/Localization/PoParserTests.cs +++ b/test/OrchardCore.Tests/Localization/PoParserTests.cs @@ -209,7 +209,7 @@ private static async Task ParseTextAsync(string resou using var resource = testAssembly.GetManifestResourceStream("OrchardCore.Tests.Localization.PoFiles." + resourceName + ".po"); using var reader = new StreamReader(resource); - return (await parser.ParseAsync(reader)).ToArray(); + return (await parser.ParseAsync(reader).ToListAsync()).ToArray(); } } } From 93a802ec231bc0c54f379dec605a11b571ed0a26 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Fri, 26 Apr 2024 21:19:20 +0300 Subject: [PATCH 6/9] Use IAsyncEnumerable --- .../PortableObjectStringLocalizer.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs index b046ca127d5..db7616e9ada 100644 --- a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs +++ b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs @@ -71,9 +71,11 @@ public virtual IEnumerable GetAllStrings(bool includeParentCult { var culture = CultureInfo.CurrentUICulture; - return includeParentCultures - ? GetAllStringsFromCultureHierarchyAsync(culture).GetAwaiter().GetResult() - : GetAllStringsAsync(culture).ToEnumerable(); + var localizedStrings = includeParentCultures + ? GetAllStringsFromCultureHierarchyAsync(culture) + : GetAllStringsAsync(culture); + + return localizedStrings.ToEnumerable(); } /// @@ -120,7 +122,7 @@ private async IAsyncEnumerable GetAllStringsAsync(CultureInfo c } } - private async Task> GetAllStringsFromCultureHierarchyAsync(CultureInfo culture) + private async IAsyncEnumerable GetAllStringsFromCultureHierarchyAsync(CultureInfo culture) { var currentCulture = culture; var allLocalizedStrings = new List(); @@ -135,15 +137,13 @@ private async Task> GetAllStringsFromCultureHierarchyAsync { if (!allLocalizedStrings.Any(ls => ls.Name == localizedString.Name)) { - allLocalizedStrings.Add(localizedString); + yield return localizedString; } } } currentCulture = currentCulture.Parent; } while (currentCulture != currentCulture.Parent); - - return allLocalizedStrings; } [Obsolete("This method is deprecated, please use GetTranslationAsync instead.")] From 3035a04037ffabed0e522f374a731b645eea1a56 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Fri, 26 Apr 2024 22:13:15 +0300 Subject: [PATCH 7/9] Revert "Use IAsyncEnumerable" This reverts commit 93a802ec231bc0c54f379dec605a11b571ed0a26. --- .../PortableObjectStringLocalizer.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs index db7616e9ada..b046ca127d5 100644 --- a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs +++ b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs @@ -71,11 +71,9 @@ public virtual IEnumerable GetAllStrings(bool includeParentCult { var culture = CultureInfo.CurrentUICulture; - var localizedStrings = includeParentCultures - ? GetAllStringsFromCultureHierarchyAsync(culture) - : GetAllStringsAsync(culture); - - return localizedStrings.ToEnumerable(); + return includeParentCultures + ? GetAllStringsFromCultureHierarchyAsync(culture).GetAwaiter().GetResult() + : GetAllStringsAsync(culture).ToEnumerable(); } /// @@ -122,7 +120,7 @@ private async IAsyncEnumerable GetAllStringsAsync(CultureInfo c } } - private async IAsyncEnumerable GetAllStringsFromCultureHierarchyAsync(CultureInfo culture) + private async Task> GetAllStringsFromCultureHierarchyAsync(CultureInfo culture) { var currentCulture = culture; var allLocalizedStrings = new List(); @@ -137,13 +135,15 @@ private async IAsyncEnumerable GetAllStringsFromCultureHierarch { if (!allLocalizedStrings.Any(ls => ls.Name == localizedString.Name)) { - yield return localizedString; + allLocalizedStrings.Add(localizedString); } } } currentCulture = currentCulture.Parent; } while (currentCulture != currentCulture.Parent); + + return allLocalizedStrings; } [Obsolete("This method is deprecated, please use GetTranslationAsync instead.")] From 8fdfc09424f1428ebf85885aca92dd147475c589 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Fri, 3 May 2024 06:10:09 +0300 Subject: [PATCH 8/9] Fix merge conflict --- .../Localization/LocalizationManagerTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/OrchardCore.Tests/Localization/LocalizationManagerTests.cs b/test/OrchardCore.Tests/Localization/LocalizationManagerTests.cs index 8f36dcef36c..e02de5bd80d 100644 --- a/test/OrchardCore.Tests/Localization/LocalizationManagerTests.cs +++ b/test/OrchardCore.Tests/Localization/LocalizationManagerTests.cs @@ -29,7 +29,7 @@ public async Task GetDictionaryReturnsDictionaryWithPluralRuleAndCultureIfNoTran var manager = new LocalizationManager(new[] { _pluralRuleProvider.Object }, new[] { _translationProvider.Object }, _memoryCache); - var dictionary = manager.GetDictionary(CultureInfo.GetCultureInfo("cs")); + var dictionary = await manager.GetDictionaryAsync(CultureInfo.GetCultureInfo("cs")); Assert.Equal("cs", dictionary.CultureName); Assert.Equal(PluralizationRule.Czech, dictionary.PluralRule); @@ -45,7 +45,7 @@ public async Task GetDictionaryReturnsDictionaryWithTranslationsFromProvider() var manager = new LocalizationManager(new[] { _pluralRuleProvider.Object }, new[] { _translationProvider.Object }, _memoryCache); - var dictionary = manager.GetDictionary(CultureInfo.GetCultureInfo("cs")); + var dictionary = await manager.GetDictionaryAsync(CultureInfo.GetCultureInfo("cs")); var key = new CultureDictionaryRecordKey { MessageId = "ball" }; dictionary.Translations.TryGetValue(key, out var translations); @@ -69,7 +69,7 @@ public async Task GetDictionarySelectsPluralRuleFromProviderWithHigherPriority() var manager = new LocalizationManager(new[] { _pluralRuleProvider.Object, highPriorityRuleProvider.Object }, new[] { _translationProvider.Object }, _memoryCache); - var dictionary = manager.GetDictionary(CultureInfo.GetCultureInfo("cs")); + var dictionary = await manager.GetDictionaryAsync(CultureInfo.GetCultureInfo("cs")); Assert.Equal(dictionary.PluralRule, csPluralRuleOverride); } From 47050eb71ca43f95786b174a41d15516aa52c35a Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Fri, 3 May 2024 06:36:21 +0300 Subject: [PATCH 9/9] GetAllStringsFromCultureHierarchyAsync() return IAsyncEnumerable --- .../PortableObjectStringLocalizer.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs index b046ca127d5..a7692932ad6 100644 --- a/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs +++ b/src/OrchardCore/OrchardCore.Localization.Core/PortableObject/PortableObjectStringLocalizer.cs @@ -70,10 +70,11 @@ public virtual LocalizedString this[string name] public virtual IEnumerable GetAllStrings(bool includeParentCultures) { var culture = CultureInfo.CurrentUICulture; + var localizedStrings = includeParentCultures + ? GetAllStringsFromCultureHierarchyAsync(culture) + : GetAllStringsAsync(culture); - return includeParentCultures - ? GetAllStringsFromCultureHierarchyAsync(culture).GetAwaiter().GetResult() - : GetAllStringsAsync(culture).ToEnumerable(); + return localizedStrings.ToEnumerable(); } /// @@ -120,10 +121,10 @@ private async IAsyncEnumerable GetAllStringsAsync(CultureInfo c } } - private async Task> GetAllStringsFromCultureHierarchyAsync(CultureInfo culture) + private async IAsyncEnumerable GetAllStringsFromCultureHierarchyAsync(CultureInfo culture) { var currentCulture = culture; - var allLocalizedStrings = new List(); + var resourcesNames = new HashSet(); do { @@ -133,17 +134,17 @@ private async Task> GetAllStringsFromCultureHierarchyAsync { foreach (var localizedString in localizedStrings) { - if (!allLocalizedStrings.Any(ls => ls.Name == localizedString.Name)) + if (!resourcesNames.Contains(localizedString.Name)) { - allLocalizedStrings.Add(localizedString); + resourcesNames.Add(localizedString.Name); + + yield return localizedString; } } } currentCulture = currentCulture.Parent; } while (currentCulture != currentCulture.Parent); - - return allLocalizedStrings; } [Obsolete("This method is deprecated, please use GetTranslationAsync instead.")]