Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: Minor optimizations #343

Merged
merged 10 commits into from
Dec 5, 2024
30 changes: 11 additions & 19 deletions common/src/DbLocalizationProvider/FallbackLanguagesCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ namespace DbLocalizationProvider;
/// </summary>
public class FallbackLanguagesCollection
{
private readonly Dictionary<string, FallbackLanguages> _collection = new();
private readonly Dictionary<string, FallbackLanguages> _collection = [];

private readonly FallbackLanguages _defaultFallbackLanguages;

/// <summary>
/// Creates new instance of this collection.
/// </summary>
public FallbackLanguagesCollection()
{
_collection.Add("default", new FallbackLanguages(this));
_defaultFallbackLanguages = new FallbackLanguages(this);
}

/// <summary>
Expand All @@ -28,8 +30,7 @@ public FallbackLanguagesCollection()
/// <param name="fallbackCulture">Specifies default fallback language.</param>
public FallbackLanguagesCollection(CultureInfo fallbackCulture)
{
var fallbackLanguages = new FallbackLanguages(this) { fallbackCulture };
_collection.Add("default", fallbackLanguages);
_defaultFallbackLanguages = new FallbackLanguages(this) { fallbackCulture };
}

/// <summary>
Expand All @@ -39,10 +40,7 @@ public FallbackLanguagesCollection(CultureInfo fallbackCulture)
/// <returns>The list of registered fallback languages for given <paramref name="language" />.</returns>
public FallbackLanguages GetFallbackLanguages(CultureInfo language)
{
if (language == null)
{
throw new ArgumentNullException(nameof(language));
}
ArgumentNullException.ThrowIfNull(language);

return GetFallbackLanguages(language.Name);
}
Expand All @@ -54,14 +52,11 @@ public FallbackLanguages GetFallbackLanguages(CultureInfo language)
/// <returns>The list of registered fallback languages for given <paramref name="language" />.</returns>
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;
}

/// <summary>
Expand All @@ -71,10 +66,7 @@ public FallbackLanguages GetFallbackLanguages(string language)
/// <returns>List of fallback languages on which you can call extension methods to get list configured.</returns>
public FallbackLanguages Add(CultureInfo notFoundCulture)
{
if (notFoundCulture == null)
{
throw new ArgumentNullException(nameof(notFoundCulture));
}
ArgumentNullException.ThrowIfNull(notFoundCulture);

if (_collection.ContainsKey(notFoundCulture.Name))
{
Expand Down
40 changes: 24 additions & 16 deletions common/src/DbLocalizationProvider/Json/JsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public JObject GetJson(
}

internal JObject Convert(
ICollection<LocalizationResource> resources,
List<LocalizationResource> resources,
string language,
CultureInfo fallbackCulture,
bool camelCase)
Expand All @@ -89,8 +89,8 @@ internal JObject Convert(
}

internal JObject Convert(
ICollection<LocalizationResource> resources,
ICollection<LocalizationResource> allResources,
List<LocalizationResource> resources,
List<LocalizationResource> allResources,
string language,
FallbackLanguagesCollection fallbackCollection,
bool camelCase)
Expand All @@ -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)
Expand Down Expand Up @@ -152,29 +154,35 @@ internal JObject Convert(

private static void Aggregate(
JObject seed,
ICollection<string> segments,
string[] segments,
Func<JObject, string, JObject> act,
Action<JObject, string> 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]);
});
}
}
91 changes: 79 additions & 12 deletions common/src/DbLocalizationProvider/LocalizationProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace DbLocalizationProvider;
/// <summary>
/// Main class to use when resource translation is needed.
/// </summary>
public class LocalizationProvider : ILocalizationProvider
public partial class LocalizationProvider : ILocalizationProvider
{
private readonly ExpressionHelper _expressionHelper;
private readonly FallbackLanguagesCollection _fallbackCollection;
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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<string, object>();
var properties = type.GetProperties();

foreach (var placeHolder in placeHolders)
ReadOnlySpan<char> 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<char> 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;
}
}
Loading