fix: Adds optimized formatter for clilocs (#1045)
Adds an optimized formatter for localization. Example:
```cs
string localizationText = Localization.Format(1050039, "enu", $"{m_Amount}\t{LabelNumber:#}");
```
Fixes #1044
This commit is contained in:
parent
ecbee17690
commit
5f00330a66
6 changed files with 248 additions and 53 deletions
|
|
@ -4,6 +4,7 @@ using Xunit;
|
||||||
|
|
||||||
namespace Server.Tests.Tests.Buffers;
|
namespace Server.Tests.Tests.Buffers;
|
||||||
|
|
||||||
|
[Collection("Sequential Tests")]
|
||||||
public class STArrayPoolTests
|
public class STArrayPoolTests
|
||||||
{
|
{
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -48,7 +49,7 @@ public class STArrayPoolTests
|
||||||
weakReferences1[i] = new WeakReference(arrays1[i]);
|
weakReferences1[i] = new WeakReference(arrays1[i]);
|
||||||
|
|
||||||
arrays2[i] = STArrayPool<byte>.Shared.Rent(64);
|
arrays2[i] = STArrayPool<byte>.Shared.Rent(64);
|
||||||
weakReferences2[i] = new WeakReference(arrays1[i]);
|
weakReferences2[i] = new WeakReference(arrays2[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < arrays1.Length; i++)
|
for (var i = 0; i < arrays1.Length; i++)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Server.Tests;
|
||||||
|
|
||||||
|
public class LocalizationEntryTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void TestClilocAsParameter()
|
||||||
|
{
|
||||||
|
Localization.Add("enu", 500002, "This tests ~1_NUMBER~ as parameters.");
|
||||||
|
Localization.Add("enu", 500003, "clilocs");
|
||||||
|
|
||||||
|
string numericFormatter = Localization.Format(500002, "enu", $"{500003:#}");
|
||||||
|
string stringParam = Localization.Format(500002, "enu", $"{"#500003"}");
|
||||||
|
|
||||||
|
Assert.Equal("This tests clilocs as parameters", numericFormatter);
|
||||||
|
Assert.Equal("This tests clilocs as parameters", stringParam);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,11 +22,13 @@ public struct PooledArraySpanFormattable : ISpanFormattable, IDisposable
|
||||||
{
|
{
|
||||||
private char[] _arrayToReturnToPool;
|
private char[] _arrayToReturnToPool;
|
||||||
private int _pos;
|
private int _pos;
|
||||||
|
private string _value;
|
||||||
|
|
||||||
public PooledArraySpanFormattable(char[] arrayToReturnToPool, int length)
|
public PooledArraySpanFormattable(char[] arrayToReturnToPool, int length)
|
||||||
{
|
{
|
||||||
_arrayToReturnToPool = arrayToReturnToPool;
|
_arrayToReturnToPool = arrayToReturnToPool;
|
||||||
_pos = length;
|
_pos = length;
|
||||||
|
_value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReadOnlySpan<char> Chars => _arrayToReturnToPool.AsSpan(.._pos);
|
public ReadOnlySpan<char> Chars => _arrayToReturnToPool.AsSpan(.._pos);
|
||||||
|
|
@ -35,10 +37,12 @@ public struct PooledArraySpanFormattable : ISpanFormattable, IDisposable
|
||||||
|
|
||||||
public string ToString(string? format = null, IFormatProvider formatProvider = null)
|
public string ToString(string? format = null, IFormatProvider formatProvider = null)
|
||||||
{
|
{
|
||||||
var result = new string(_arrayToReturnToPool.AsSpan(0, _pos));
|
_value ??= new string(_arrayToReturnToPool.AsSpan(0, _pos));
|
||||||
Dispose();
|
|
||||||
|
|
||||||
return result;
|
STArrayPool<char>.Shared.Return(_arrayToReturnToPool);
|
||||||
|
_arrayToReturnToPool = null;
|
||||||
|
|
||||||
|
return _value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryFormat(
|
public bool TryFormat(
|
||||||
|
|
@ -53,18 +57,14 @@ public struct PooledArraySpanFormattable : ISpanFormattable, IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
_arrayToReturnToPool.AsSpan(0, _pos).CopyTo(destination);
|
_arrayToReturnToPool.AsSpan(0, _pos).CopyTo(destination);
|
||||||
Dispose();
|
|
||||||
|
|
||||||
charsWritten = _pos;
|
charsWritten = _pos;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_arrayToReturnToPool != null)
|
STArrayPool<char>.Shared.Return(_arrayToReturnToPool);
|
||||||
{
|
_arrayToReturnToPool = null;
|
||||||
STArrayPool<char>.Shared.Return(_arrayToReturnToPool);
|
this = default; // Defensive clear
|
||||||
_arrayToReturnToPool = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,11 @@ public class STArrayPool<T> : ArrayPool<T>
|
||||||
buckets[i]?.Trim(ticks, pressure, GetMaxSizeForBucket(i));
|
buckets[i]?.Trim(ticks, pressure, GetMaxSizeForBucket(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_cacheBuckets == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Under high pressure, release all cached buckets
|
// Under high pressure, release all cached buckets
|
||||||
if (pressure == MemoryPressure.High)
|
if (pressure == MemoryPressure.High)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,37 @@ public static class Localization
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void Add(string lang, int number, string text)
|
||||||
|
{
|
||||||
|
var entry = new LocalizationEntry(lang, number, text);
|
||||||
|
if (!_localizations.TryGetValue(lang, out var entries))
|
||||||
|
{
|
||||||
|
entries = new Dictionary<int, LocalizationEntry>();
|
||||||
|
_localizations[lang] = entries;
|
||||||
|
if (lang == FallbackLanguage)
|
||||||
|
{
|
||||||
|
_fallbackEntries ??= entries;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.Add(number, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Remove(string lang, int number)
|
||||||
|
{
|
||||||
|
if (!_localizations.TryGetValue(lang, out var entries) || !entries.Remove(number))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.Count == 0)
|
||||||
|
{
|
||||||
|
_localizations.Remove(lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public static Dictionary<int, LocalizationEntry> LoadClilocs(string lang) =>
|
public static Dictionary<int, LocalizationEntry> LoadClilocs(string lang) =>
|
||||||
LoadClilocs(lang, Core.FindDataFile($"cliloc.{lang}", false));
|
LoadClilocs(lang, Core.FindDataFile($"cliloc.{lang}", false));
|
||||||
|
|
||||||
|
|
@ -96,19 +127,6 @@ public static class Localization
|
||||||
public static string GetText(int number, string lang = FallbackLanguage) =>
|
public static string GetText(int number, string lang = FallbackLanguage) =>
|
||||||
TryGetLocalization(lang, number, out var entry) ? entry.Text : null;
|
TryGetLocalization(lang, number, out var entry) ? entry.Text : null;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a formatted string of the localization entry using the specified language.
|
|
||||||
/// Uses <see cref="string.Format"/> under the hood.
|
|
||||||
/// Note: This method is not recommended since it uses almost double the memory and 50% more processing.
|
|
||||||
/// Instead use Format with string interpolation.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="number">Localization number</param>
|
|
||||||
/// <param name="lang">Language in ISO 639-2 format</param>
|
|
||||||
/// <param name="args">An object array containing zero or more objects to format</param>
|
|
||||||
/// <returns>A copy of the localization text where the placeholder arguments have been replaced with string representations of the provided arguments</returns>
|
|
||||||
public static string Format(int number, string lang = FallbackLanguage, params object[] args) =>
|
|
||||||
TryGetLocalization(lang, number, out var entry) ? entry.Format(args) : null;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a localization entry using the <see cref="FallbackLanguage" />.
|
/// Gets a localization entry using the <see cref="FallbackLanguage" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -84,24 +84,6 @@ public class LocalizationEntry
|
||||||
builder.Dispose();
|
builder.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Format(params object[] args)
|
|
||||||
{
|
|
||||||
if (args == null || args.Length == 0 || StringFormatter == null)
|
|
||||||
{
|
|
||||||
return Text;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < args.Length; i++)
|
|
||||||
{
|
|
||||||
if (args[i] is string s && s[0] == '#' && int.TryParse(s.AsSpan(1), out var number))
|
|
||||||
{
|
|
||||||
args[i] = Localization.GetText(number, Language);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Format(StringFormatter, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a formatted string of the localization entry.
|
/// Creates a formatted string of the localization entry.
|
||||||
/// Uses string interpolation under the hood. This method is preferably relative to the object array method signature.
|
/// Uses string interpolation under the hood. This method is preferably relative to the object array method signature.
|
||||||
|
|
@ -256,13 +238,69 @@ public class LocalizationEntry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AppendFormatted<T>(T value, string? format)
|
// Each numeric needs its own override
|
||||||
|
public void AppendFormatted(int value, string? format)
|
||||||
{
|
{
|
||||||
if (!ReadyToAppend())
|
if (!ReadyToAppend())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!TryAppendCliloc(value, format))
|
||||||
|
{
|
||||||
|
AppendFormattedDirect(value, format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendFormatted(uint value, string? format)
|
||||||
|
{
|
||||||
|
if (!ReadyToAppend())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAppendCliloc((int)value, format))
|
||||||
|
{
|
||||||
|
AppendFormattedDirect(value, format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendFormatted(long value, string? format)
|
||||||
|
{
|
||||||
|
if (!ReadyToAppend())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAppendCliloc((int)value, format))
|
||||||
|
{
|
||||||
|
AppendFormattedDirect(value, format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendFormatted(ulong value, string? format)
|
||||||
|
{
|
||||||
|
if (!ReadyToAppend())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAppendCliloc((int)value, format))
|
||||||
|
{
|
||||||
|
AppendFormattedDirect(value, format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendFormatted<T>(T value, string? format)
|
||||||
|
{
|
||||||
|
if (ReadyToAppend())
|
||||||
|
{
|
||||||
|
AppendFormattedDirect(value, format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendFormattedDirect<T>(T value, string? format)
|
||||||
|
{
|
||||||
string? s;
|
string? s;
|
||||||
if (value is IFormattable)
|
if (value is IFormattable)
|
||||||
{
|
{
|
||||||
|
|
@ -307,6 +345,95 @@ public class LocalizationEntry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Each numeric needs its own override
|
||||||
|
public void AppendFormatted(int value, int alignment, string? format)
|
||||||
|
{
|
||||||
|
if (!ReadyToAppend())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var startingPos = _pos;
|
||||||
|
if (TryAppendCliloc(value, format))
|
||||||
|
{
|
||||||
|
AppendFormatted(value, format);
|
||||||
|
if (alignment != 0)
|
||||||
|
{
|
||||||
|
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AppendFormattedDirect(value, alignment, format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendFormatted(uint value, int alignment, string? format)
|
||||||
|
{
|
||||||
|
if (!ReadyToAppend())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var startingPos = _pos;
|
||||||
|
if (TryAppendCliloc((int)value, format))
|
||||||
|
{
|
||||||
|
AppendFormatted(value, format);
|
||||||
|
if (alignment != 0)
|
||||||
|
{
|
||||||
|
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AppendFormattedDirect(value, alignment, format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendFormatted(long value, int alignment, string? format)
|
||||||
|
{
|
||||||
|
if (!ReadyToAppend())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var startingPos = _pos;
|
||||||
|
if (TryAppendCliloc((int)value, format))
|
||||||
|
{
|
||||||
|
AppendFormatted(value, format);
|
||||||
|
if (alignment != 0)
|
||||||
|
{
|
||||||
|
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AppendFormattedDirect(value, alignment, format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendFormatted(ulong value, int alignment, string? format)
|
||||||
|
{
|
||||||
|
if (!ReadyToAppend())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var startingPos = _pos;
|
||||||
|
if (TryAppendCliloc((int)value, format))
|
||||||
|
{
|
||||||
|
AppendFormatted(value, format);
|
||||||
|
if (alignment != 0)
|
||||||
|
{
|
||||||
|
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AppendFormattedDirect(value, alignment, format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void AppendFormatted<T>(T value, int alignment, string? format)
|
public void AppendFormatted<T>(T value, int alignment, string? format)
|
||||||
{
|
{
|
||||||
if (!ReadyToAppend())
|
if (!ReadyToAppend())
|
||||||
|
|
@ -314,6 +441,11 @@ public class LocalizationEntry
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppendFormattedDirect(value, alignment, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendFormattedDirect<T>(T value, int alignment, string? format)
|
||||||
|
{
|
||||||
var startingPos = _pos;
|
var startingPos = _pos;
|
||||||
AppendFormatted(value, format);
|
AppendFormatted(value, format);
|
||||||
if (alignment != 0)
|
if (alignment != 0)
|
||||||
|
|
@ -324,7 +456,7 @@ public class LocalizationEntry
|
||||||
|
|
||||||
public void AppendFormatted(ReadOnlySpan<char> value)
|
public void AppendFormatted(ReadOnlySpan<char> value)
|
||||||
{
|
{
|
||||||
if (!ReadyToAppend() || TryAppendClilocNumber(value))
|
if (!ReadyToAppend() || TryAppendClilocByNumericString(value))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -380,12 +512,33 @@ public class LocalizationEntry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AppendFormatted(object? value, int alignment = 0, string? format = null) =>
|
public void AppendFormatted(object? value, int alignment = 0, string? format = null)
|
||||||
AppendFormatted<object?>(value, alignment, format);
|
{
|
||||||
|
if (value is int i)
|
||||||
|
{
|
||||||
|
AppendFormatted(i, alignment, format);
|
||||||
|
}
|
||||||
|
else if (value is uint ui)
|
||||||
|
{
|
||||||
|
AppendFormatted(ui, alignment, format);
|
||||||
|
}
|
||||||
|
else if (value is long l)
|
||||||
|
{
|
||||||
|
AppendFormatted(l, alignment, format);
|
||||||
|
}
|
||||||
|
else if (value is ulong ul)
|
||||||
|
{
|
||||||
|
AppendFormatted(ul, alignment, format);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AppendFormatted<object?>(value, alignment, format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void AppendFormatted(string? value)
|
public void AppendFormatted(string? value)
|
||||||
{
|
{
|
||||||
if (!ReadyToAppend() || TryAppendClilocNumber(value))
|
if (!ReadyToAppend() || TryAppendClilocByNumericString(value))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -403,13 +556,9 @@ public class LocalizationEntry
|
||||||
public void AppendFormatted(string? value, int alignment, string? format = null) =>
|
public void AppendFormatted(string? value, int alignment, string? format = null) =>
|
||||||
AppendFormatted<string?>(value, alignment, format);
|
AppendFormatted<string?>(value, alignment, format);
|
||||||
|
|
||||||
private bool TryAppendClilocNumber(ReadOnlySpan<char> value)
|
public bool TryAppendCliloc(int number, string? format)
|
||||||
{
|
{
|
||||||
if (
|
if (format != "#" || !Localization.TryGetLocalization(_lang, number, out var entry))
|
||||||
value[0] != '#' ||
|
|
||||||
!int.TryParse(value[1..], out var number) ||
|
|
||||||
!Localization.TryGetLocalization(_lang, number, out var entry)
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -428,6 +577,9 @@ public class LocalizationEntry
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryAppendClilocByNumericString(ReadOnlySpan<char> value) =>
|
||||||
|
value[0] == '#' && long.TryParse(value[1..], out var number) && TryAppendCliloc((int)number, "#");
|
||||||
|
|
||||||
private void AppendOrInsertAlignmentIfNeeded(int startingPos, int alignment)
|
private void AppendOrInsertAlignmentIfNeeded(int startingPos, int alignment)
|
||||||
{
|
{
|
||||||
var charsWritten = _pos - startingPos;
|
var charsWritten = _pos - startingPos;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue