diff --git a/common/src/DbLocalizationProvider/FallbackLanguagesCollection.cs b/common/src/DbLocalizationProvider/FallbackLanguagesCollection.cs index 964a00d4..ee418e9e 100644 --- a/common/src/DbLocalizationProvider/FallbackLanguagesCollection.cs +++ b/common/src/DbLocalizationProvider/FallbackLanguagesCollection.cs @@ -12,14 +12,16 @@ namespace DbLocalizationProvider; /// public class FallbackLanguagesCollection { - private readonly Dictionary _collection = new(); + private readonly Dictionary _collection = []; + + private readonly FallbackLanguages _defaultFallbackLanguages; /// /// Creates new instance of this collection. /// public FallbackLanguagesCollection() { - _collection.Add("default", new FallbackLanguages(this)); + _defaultFallbackLanguages = new FallbackLanguages(this); } /// @@ -28,8 +30,7 @@ public FallbackLanguagesCollection() /// Specifies default fallback language. public FallbackLanguagesCollection(CultureInfo fallbackCulture) { - var fallbackLanguages = new FallbackLanguages(this) { fallbackCulture }; - _collection.Add("default", fallbackLanguages); + _defaultFallbackLanguages = new FallbackLanguages(this) { fallbackCulture }; } /// @@ -39,10 +40,7 @@ public FallbackLanguagesCollection(CultureInfo fallbackCulture) /// The list of registered fallback languages for given . public FallbackLanguages GetFallbackLanguages(CultureInfo language) { - if (language == null) - { - throw new ArgumentNullException(nameof(language)); - } + ArgumentNullException.ThrowIfNull(language); return GetFallbackLanguages(language.Name); } @@ -54,14 +52,11 @@ public FallbackLanguages GetFallbackLanguages(CultureInfo language) /// The list of registered fallback languages for given . public FallbackLanguages GetFallbackLanguages(string language) { - if (language == null) - { - throw new ArgumentNullException(nameof(language)); - } + ArgumentNullException.ThrowIfNull(language); - return !_collection.ContainsKey(language) - ? _collection["default"] - : _collection[language]; + return _collection.TryGetValue(language, out var fallbackLanguages) + ? fallbackLanguages + : _defaultFallbackLanguages; } /// @@ -71,10 +66,7 @@ public FallbackLanguages GetFallbackLanguages(string language) /// List of fallback languages on which you can call extension methods to get list configured. public FallbackLanguages Add(CultureInfo notFoundCulture) { - if (notFoundCulture == null) - { - throw new ArgumentNullException(nameof(notFoundCulture)); - } + ArgumentNullException.ThrowIfNull(notFoundCulture); if (_collection.ContainsKey(notFoundCulture.Name)) { diff --git a/common/src/DbLocalizationProvider/Json/JsonConverter.cs b/common/src/DbLocalizationProvider/Json/JsonConverter.cs index 95661ddf..3bc00757 100644 --- a/common/src/DbLocalizationProvider/Json/JsonConverter.cs +++ b/common/src/DbLocalizationProvider/Json/JsonConverter.cs @@ -76,7 +76,7 @@ public JObject GetJson( } internal JObject Convert( - ICollection resources, + List resources, string language, CultureInfo fallbackCulture, bool camelCase) @@ -89,8 +89,8 @@ internal JObject Convert( } internal JObject Convert( - ICollection resources, - ICollection allResources, + List resources, + List allResources, string language, FallbackLanguagesCollection fallbackCollection, bool camelCase) @@ -101,15 +101,17 @@ internal JObject Convert( { // we need to process key names and supported nested classes with "+" symbols in keys // so we replace those with dots to have proper object nesting on client side - var key = resource.ResourceKey.Replace("+", "."); - if (!key.Contains(".")) + var key = resource.ResourceKey.Replace('+', '.'); + if (!key.Contains('.')) { continue; } - var segments = key.Split(new[] { "." }, StringSplitOptions.None) - .Select(k => camelCase ? CamelCase(k) : k) - .ToList(); + var segments = key.Split('.', StringSplitOptions.None); + if (segments.Length > 0 && camelCase) + { + segments = [.. segments.Select(CamelCase)]; + } // let's try to look for translation explicitly in requested language // if there is no translation in given language -> worth to look in fallback culture *and* invariant (if configured to do so) @@ -152,29 +154,35 @@ internal JObject Convert( private static void Aggregate( JObject seed, - ICollection segments, + string[] segments, Func act, Action last) { - if (segments == null || !segments.Any()) + if (segments == null || segments.Length == 0) { return; } - var lastElement = segments.Last(); - var seqWithNoLast = segments.Take(segments.Count - 1); + var lastElement = segments[^1]; + var seqWithNoLast = segments.Take(..^1); var s = seqWithNoLast.Aggregate(seed, act); last(s, lastElement); } - private static string CamelCase(string that) + private static string CamelCase(string text) { - if (that.Length > 1) + ArgumentNullException.ThrowIfNull(text); + + if (text.Length == 0 || char.IsLower(text, 0)) { - return that.Substring(0, 1).ToLower() + that.Substring(1); + return text; } - return that.ToLower(); + return string.Create(text.Length, text, (chars, state) => + { + state.AsSpan().CopyTo(chars); + chars[0] = char.ToLower(chars[0]); + }); } } diff --git a/common/src/DbLocalizationProvider/LocalizationProvider.cs b/common/src/DbLocalizationProvider/LocalizationProvider.cs index 26a0994a..e627435c 100644 --- a/common/src/DbLocalizationProvider/LocalizationProvider.cs +++ b/common/src/DbLocalizationProvider/LocalizationProvider.cs @@ -18,7 +18,7 @@ namespace DbLocalizationProvider; /// /// Main class to use when resource translation is needed. /// -public class LocalizationProvider : ILocalizationProvider +public partial class LocalizationProvider : ILocalizationProvider { private readonly ExpressionHelper _expressionHelper; private readonly FallbackLanguagesCollection _fallbackCollection; @@ -51,6 +51,12 @@ public LocalizationProvider( _queryExecutor = queryExecutor; _scanState = scanState; + // _converter = new JsonConverter(_queryExecutor, _scanState); + // _serializer = new JsonSerializer + // { + // ContractResolver = new StaticPropertyContractResolver() + // }; + _reflectionConverter = new ReflectionConverter(_queryExecutor, _scanState, _keyBuilder); } @@ -418,28 +424,89 @@ private static string FormatWithAnonymousObject(string message, object model) return string.Format(message, model); } - var placeHolders = Regex.Matches(message, "{.*?}").Select(m => m.Value).ToList(); - - if (!placeHolders.Any()) + const char Prefix = '{'; + const char Postfix = '}'; + if (!message.Contains(Prefix) || !message.Contains(Postfix)) { return message; } - var placeholderMap = new Dictionary(); var properties = type.GetProperties(); - foreach (var placeHolder in placeHolders) + ReadOnlySpan tmp = message; + + // The rest of the method is based on https://stackoverflow.com/a/74391485/11963 (Licensed under CC BY-SA 4.0) + // we store the occurrences in the queue while calculating the length of the final string + // so we don't have to search for them the 2nd time later + var occurrences = new Queue<(int at, int task)>(); + var offset = 0; + var resultLength = tmp.Length; + + int prefixIndex; + while ((prefixIndex = tmp.IndexOf(Prefix)) != -1) { - var propertyInfo = properties.FirstOrDefault(p => p.Name == placeHolder.Trim('{', '}')); + (int at, int task) next = (prefixIndex, -1); + for (var i = 0; i < properties.Length; i++) + { + // we expect the postfix to be at this place + var postfixIndex = prefixIndex + properties[i].Name.Length + 1; + if (tmp.Length > postfixIndex // check that we don't cross the bounds + && tmp[postfixIndex] == Postfix // check that the postfix IS were we expect it to be + && tmp.Slice(prefixIndex + 1, postfixIndex - prefixIndex - 1).SequenceEqual(properties[i].Name)) // compare all the characters in between the delimiters + { + next.task = i; + break; + } + } - // property found - extract value and add to the map - var val = propertyInfo?.GetValue(model); - if (val != null && !placeholderMap.ContainsKey(placeHolder)) + if (next.task == -1) { - placeholderMap.Add(placeHolder, val); + // this delimiter character is just part of the string, so skip it + tmp = tmp[(prefixIndex + 1)..]; + offset += prefixIndex + 1; + continue; } + + var newStart = next.at + properties[next.task].Name.Length + 2; + tmp = tmp[newStart..]; + + occurrences.Enqueue((next.at + offset, next.task)); + offset += newStart; + + resultLength += (properties[next.task].GetValue(model)?.ToString()?.Length ?? 0) - properties[next.task].Name.Length - 2; } - return placeholderMap.Aggregate(message, (current, pair) => current.Replace(pair.Key, pair.Value.ToString())); + var result = string.Create(resultLength, (message, model, properties, occurrences), (chars, state) => + { + var message = state.message; + var model = state.model; + var replaceTasks = state.properties; + var occurrences = state.occurrences; + + var position = 0; + + ReadOnlySpan origin = message; + var lastStart = 0; + + while (occurrences.Count != 0) + { + var next = occurrences.Dequeue(); + + var value = replaceTasks[next.task].GetValue(model)?.ToString(); + if (value is null) + { + continue; + } + + origin[lastStart..next.at].CopyTo(chars[position..]); + value.CopyTo(chars[(position + next.at - lastStart)..]); + position += next.at - lastStart + value.Length; + lastStart = next.at + replaceTasks[next.task].Name.Length + 2; + } + + origin[lastStart..].CopyTo(chars[position..]); + }); + + return result; } }