diff --git a/Projects/Server/Localization/Localization.cs b/Projects/Server/Localization/Localization.cs
index fe3128c67..e178bbd95 100644
--- a/Projects/Server/Localization/Localization.cs
+++ b/Projects/Server/Localization/Localization.cs
@@ -96,20 +96,6 @@ public static class Localization
public static string GetText(int number, string lang = FallbackLanguage) =>
TryGetLocalization(lang, number, out var entry) ? entry.Text : null;
- public static string Format(int number, string lang = FallbackLanguage) => GetText(number, lang);
-
- ///
- /// Creates a formatted string of the localization entry using the .
- /// Uses 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.
- ///
- /// Localization number
- /// An object array containing zero or more objects to format
- /// A copy of the localization text where the placeholder arguments have been replaced with string representations of the provided arguments
- public static string Format(int number, params object[] args) =>
- !TryGetLocalization(number, out var entry) ? null : string.Format(entry.StringFormatter, args);
-
///
/// Creates a formatted string of the localization entry using the specified language.
/// Uses under the hood.
@@ -120,8 +106,8 @@ public static class Localization
/// Language in ISO 639-2 format
/// An object array containing zero or more objects to format
/// A copy of the localization text where the placeholder arguments have been replaced with string representations of the provided arguments
- public static string Format(int number, string lang, params object[] args) =>
- !TryGetLocalization(lang, number, out var entry) ? null : string.Format(entry.StringFormatter, args);
+ public static string Format(int number, string lang = FallbackLanguage, params object[] args) =>
+ TryGetLocalization(lang, number, out var entry) ? entry.Format(args) : null;
///
/// Gets a localization entry using the .
@@ -171,7 +157,7 @@ public static class Localization
public static PooledArraySpanFormattable Format(
int number, string lang,
[InterpolatedStringHandlerArgument("number", "lang")]
- ref LocalizationInterpolationHandler handler
+ ref LocalizationEntry.LocalizationInterpolationHandler handler
)
{
var chars = handler.ToPooledArray(out var length);
@@ -191,7 +177,7 @@ public static class Localization
public static PooledArraySpanFormattable Format(
int number,
[InterpolatedStringHandlerArgument("number")]
- ref LocalizationInterpolationHandler handler
+ ref LocalizationEntry.LocalizationInterpolationHandler handler
)
{
var chars = handler.ToPooledArray(out var length);
diff --git a/Projects/Server/Localization/LocalizationEntry.cs b/Projects/Server/Localization/LocalizationEntry.cs
index 61211fbdb..c2914601f 100644
--- a/Projects/Server/Localization/LocalizationEntry.cs
+++ b/Projects/Server/Localization/LocalizationEntry.cs
@@ -13,6 +13,8 @@
* along with this program. If not, see . *
*************************************************************************/
+using System;
+using System.Buffers;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using Server.Buffers;
@@ -82,7 +84,23 @@ public class LocalizationEntry
builder.Dispose();
}
- public string Format(params object[] args) => string.Format(StringFormatter, args);
+ 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);
+ }
///
/// Creates a formatted string of the localization entry.
@@ -101,4 +119,437 @@ public class LocalizationEntry
handler = default; // Defensive clear
return new PooledArraySpanFormattable(chars, length);
}
+
+ [InterpolatedStringHandler]
+ public ref struct LocalizationInterpolationHandler
+ {
+ private static string[] _empty = Array.Empty();
+
+ private char[]? _arrayToReturnToPool;
+ private Span _chars;
+ private int _pos;
+
+ private int _index;
+ private string?[] _slices;
+ private string? _current;
+ private string _lang;
+
+ public LocalizationInterpolationHandler(int literalLength, int formattedCount, LocalizationEntry entry, out bool isValid)
+ {
+ _slices = entry.TextSlices;
+ _chars = _arrayToReturnToPool = ArrayPool.Shared.Rent(256);
+ isValid = true;
+
+ _pos = 0;
+ _index = 0;
+ _current = null;
+ _lang = entry.Language;
+ }
+
+ public LocalizationInterpolationHandler(int literalLength, int formattedCount, int number, string lang, out bool isValid)
+ {
+ if (Localization.TryGetLocalization(lang, number, out var entry))
+ {
+ _slices = entry.TextSlices;
+ _chars = _arrayToReturnToPool = ArrayPool.Shared.Rent(256);
+ isValid = true;
+ }
+ else
+ {
+ _slices = _empty;
+ _chars = _arrayToReturnToPool = default;
+ isValid = false;
+ }
+
+ _pos = 0;
+ _index = 0;
+ _current = null;
+ _lang = lang;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool MoveNext()
+ {
+ if ((uint)_index >= (uint)_slices.Length)
+ {
+ return false;
+ }
+
+ _current = _slices[_index++];
+ return true;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool ReadyToAppend()
+ {
+ if (!MoveNext())
+ {
+ return false;
+ }
+
+ if (_current == null)
+ {
+ return true;
+ }
+
+ AppendStringDirect(_current);
+ return MoveNext();
+ }
+
+ public void AppendLiteral(string value)
+ {
+ }
+
+ private void AppendStringDirect(string value)
+ {
+ if (value.TryCopyTo(_chars[_pos..]))
+ {
+ _pos += value.Length;
+ }
+ else
+ {
+ GrowThenCopyString(value);
+ }
+ }
+
+ public void AppendFormatted(T value)
+ {
+ if (!ReadyToAppend())
+ {
+ return;
+ }
+
+ string? s;
+ if (value is IFormattable)
+ {
+ // If the value can format itself directly into our buffer, do so.
+ if (value is ISpanFormattable)
+ {
+ int charsWritten;
+ while (!((ISpanFormattable)value).TryFormat(_chars[_pos..], out charsWritten, default, default)) // constrained call avoiding boxing for value types
+ {
+ Grow();
+ }
+
+ _pos += charsWritten;
+ return;
+ }
+
+ s = ((IFormattable)value).ToString(format: null, default); // constrained call avoiding boxing for value types
+ }
+ else
+ {
+ s = value?.ToString();
+ }
+
+ if (s is not null)
+ {
+ AppendStringDirect(s);
+ }
+ }
+
+ public void AppendFormatted(T value, string? format)
+ {
+ if (!ReadyToAppend())
+ {
+ return;
+ }
+
+ string? s;
+ if (value is IFormattable)
+ {
+ // If the value can format itself directly into our buffer, do so.
+ if (value is ISpanFormattable)
+ {
+ int charsWritten;
+ while (!((ISpanFormattable)value).TryFormat(_chars[_pos..], out charsWritten, format, default)) // constrained call avoiding boxing for value types
+ {
+ Grow();
+ }
+
+ _pos += charsWritten;
+ return;
+ }
+
+ s = ((IFormattable)value).ToString(format, default); // constrained call avoiding boxing for value types
+ }
+ else
+ {
+ s = value?.ToString();
+ }
+
+ if (s is not null)
+ {
+ AppendStringDirect(s);
+ }
+ }
+
+ public void AppendFormatted(T value, int alignment)
+ {
+ if (!ReadyToAppend())
+ {
+ return;
+ }
+
+ var startingPos = _pos;
+ AppendFormatted(value);
+ if (alignment != 0)
+ {
+ AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
+ }
+ }
+
+ public void AppendFormatted(T value, int alignment, string? format)
+ {
+ if (!ReadyToAppend())
+ {
+ return;
+ }
+
+ var startingPos = _pos;
+ AppendFormatted(value, format);
+ if (alignment != 0)
+ {
+ AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
+ }
+ }
+
+ public void AppendFormatted(ReadOnlySpan value)
+ {
+ if (!ReadyToAppend() || TryAppendClilocNumber(value))
+ {
+ return;
+ }
+
+ if (value.TryCopyTo(_chars[_pos..]))
+ {
+ _pos += value.Length;
+ }
+ else
+ {
+ GrowThenCopySpan(value);
+ }
+ }
+
+ public void AppendFormatted(ReadOnlySpan value, int alignment, string? format = null)
+ {
+ if (!ReadyToAppend())
+ {
+ return;
+ }
+
+ var leftAlign = false;
+ if (alignment < 0)
+ {
+ leftAlign = true;
+ alignment = -alignment;
+ }
+
+ var paddingRequired = alignment - value.Length;
+ if (paddingRequired <= 0)
+ {
+ // The value is as large or larger than the required amount of padding,
+ // so just write the value.
+ AppendFormatted(value);
+ return;
+ }
+
+ // Write the value along with the appropriate padding.
+ EnsureCapacityForAdditionalChars(value.Length + paddingRequired);
+ if (leftAlign)
+ {
+ value.CopyTo(_chars[_pos..]);
+ _pos += value.Length;
+ _chars.Slice(_pos, paddingRequired).Fill(' ');
+ _pos += paddingRequired;
+ }
+ else
+ {
+ _chars.Slice(_pos, paddingRequired).Fill(' ');
+ _pos += paddingRequired;
+ value.CopyTo(_chars[_pos..]);
+ _pos += value.Length;
+ }
+ }
+
+ public void AppendFormatted(object? value, int alignment = 0, string? format = null) =>
+ AppendFormatted