♻️ Refactored localization service
This commit is contained in:
6
DysonNetwork.Shared/Localization/ILocalizationService.cs
Normal file
6
DysonNetwork.Shared/Localization/ILocalizationService.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace DysonNetwork.Shared.Localization;
|
||||
|
||||
public interface ILocalizationService
|
||||
{
|
||||
string Get(string key, string? locale = null, object? args = null);
|
||||
}
|
||||
262
DysonNetwork.Shared/Localization/JsonLocalizationService.cs
Normal file
262
DysonNetwork.Shared/Localization/JsonLocalizationService.cs
Normal file
@@ -0,0 +1,262 @@
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DysonNetwork.Shared.Localization;
|
||||
|
||||
public class JsonLocalizationService : ILocalizationService
|
||||
{
|
||||
private readonly Dictionary<string, Dictionary<string, LocalizationEntry>> _localeCache = new();
|
||||
private readonly Assembly _assembly;
|
||||
private readonly string _resourceNamespace;
|
||||
private readonly object _lock = new();
|
||||
private readonly List<string> _availableLocales = new();
|
||||
|
||||
public JsonLocalizationService(Assembly? assembly = null, string? resourceNamespace = null)
|
||||
{
|
||||
_assembly = assembly ?? Assembly.GetEntryAssembly() ?? Assembly.GetCallingAssembly();
|
||||
_resourceNamespace = resourceNamespace ?? "DysonNetwork.Pass.Resources.Locales";
|
||||
DiscoverAvailableLocales();
|
||||
}
|
||||
|
||||
private void DiscoverAvailableLocales()
|
||||
{
|
||||
var resourceNames = _assembly.GetManifestResourceNames();
|
||||
var prefix = $"{_resourceNamespace}.";
|
||||
var suffix = ".json";
|
||||
|
||||
foreach (var resourceName in resourceNames)
|
||||
{
|
||||
if (resourceName.StartsWith(prefix) && resourceName.EndsWith(suffix))
|
||||
{
|
||||
var locale = resourceName.Substring(prefix.Length, resourceName.Length - prefix.Length - suffix.Length);
|
||||
_availableLocales.Add(locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Get(string key, string? locale = null, object? args = null)
|
||||
{
|
||||
locale ??= CultureInfo.CurrentUICulture.Name;
|
||||
|
||||
// Try the requested locale first
|
||||
var entries = GetLocaleEntries(locale);
|
||||
if (entries.TryGetValue(key, out var entry))
|
||||
{
|
||||
return FormatEntry(entry, args);
|
||||
}
|
||||
|
||||
// Fallback: search all available locales
|
||||
foreach (var availableLocale in _availableLocales)
|
||||
{
|
||||
if (availableLocale.Equals(locale, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
var fallbackEntries = GetLocaleEntries(availableLocale);
|
||||
if (fallbackEntries.TryGetValue(key, out entry))
|
||||
{
|
||||
return FormatEntry(entry, args);
|
||||
}
|
||||
}
|
||||
|
||||
// If no translation found, return key with args joined
|
||||
return FormatFallback(key, args);
|
||||
}
|
||||
|
||||
private string FormatEntry(LocalizationEntry entry, object? args)
|
||||
{
|
||||
string template;
|
||||
if (args != null && entry.IsPlural)
|
||||
{
|
||||
template = SelectPluralForm(entry, args);
|
||||
}
|
||||
else
|
||||
{
|
||||
template = !string.IsNullOrEmpty(entry.Value) ? entry.Value : (entry.Other ?? entry.One ?? string.Empty);
|
||||
}
|
||||
|
||||
return FormatTemplate(template, args);
|
||||
}
|
||||
|
||||
private string FormatFallback(string key, object? args)
|
||||
{
|
||||
if (args == null)
|
||||
{
|
||||
return key;
|
||||
}
|
||||
|
||||
// Extract argument values and join them with the key
|
||||
var argValues = new List<string>();
|
||||
var type = args.GetType();
|
||||
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var value = prop.GetValue(args);
|
||||
if (value != null)
|
||||
{
|
||||
argValues.Add(value.ToString() ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var field in fields)
|
||||
{
|
||||
var value = field.GetValue(args);
|
||||
if (value != null)
|
||||
{
|
||||
argValues.Add(value.ToString() ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
if (argValues.Count == 0)
|
||||
{
|
||||
return key;
|
||||
}
|
||||
|
||||
return $"{key} {string.Join(" ", argValues)}";
|
||||
}
|
||||
|
||||
private Dictionary<string, LocalizationEntry> GetLocaleEntries(string locale)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_localeCache.TryGetValue(locale, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
var entries = LoadLocale(locale);
|
||||
_localeCache[locale] = entries;
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, LocalizationEntry> LoadLocale(string locale)
|
||||
{
|
||||
var resourceName = $"{_resourceNamespace}.{locale}.json";
|
||||
|
||||
using var stream = _assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream == null)
|
||||
{
|
||||
return new Dictionary<string, LocalizationEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
var json = reader.ReadToEnd();
|
||||
|
||||
var root = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
|
||||
if (root == null)
|
||||
{
|
||||
return new Dictionary<string, LocalizationEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
var entries = new Dictionary<string, LocalizationEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var kvp in root)
|
||||
{
|
||||
entries[kvp.Key] = ParseLocalizationEntry(kvp.Value);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private LocalizationEntry ParseLocalizationEntry(JsonElement element)
|
||||
{
|
||||
if (element.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return new LocalizationEntry { Value = element.GetString() ?? string.Empty };
|
||||
}
|
||||
|
||||
if (element.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
var entry = new LocalizationEntry();
|
||||
if (element.TryGetProperty("value", out var valueProp))
|
||||
{
|
||||
entry.Value = valueProp.GetString() ?? string.Empty;
|
||||
}
|
||||
if (element.TryGetProperty("one", out var oneProp))
|
||||
{
|
||||
entry.One = oneProp.GetString();
|
||||
}
|
||||
if (element.TryGetProperty("other", out var otherProp))
|
||||
{
|
||||
entry.Other = otherProp.GetString();
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
return new LocalizationEntry { Value = element.ToString() };
|
||||
}
|
||||
|
||||
private string SelectPluralForm(LocalizationEntry entry, object args)
|
||||
{
|
||||
var countValue = GetCountValue(args);
|
||||
if (countValue.HasValue)
|
||||
{
|
||||
var count = countValue.Value;
|
||||
if (count == 1 && !string.IsNullOrEmpty(entry.One))
|
||||
{
|
||||
return entry.One;
|
||||
}
|
||||
}
|
||||
|
||||
return entry.Other ?? entry.One ?? string.Empty;
|
||||
}
|
||||
|
||||
private int? GetCountValue(object args)
|
||||
{
|
||||
if (args == null) return null;
|
||||
|
||||
var type = args.GetType();
|
||||
var countProperty = type.GetProperty("Count", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
if (countProperty != null)
|
||||
{
|
||||
var value = countProperty.GetValue(args);
|
||||
if (value is int intValue) return intValue;
|
||||
if (value is long longValue) return (int)longValue;
|
||||
if (value is double doubleValue) return (int)doubleValue;
|
||||
if (value is decimal decimalValue) return (int)decimalValue;
|
||||
}
|
||||
|
||||
var countField = type.GetField("Count", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
if (countField != null)
|
||||
{
|
||||
var value = countField.GetValue(args);
|
||||
if (value is int intValue) return intValue;
|
||||
if (value is long longValue) return (int)longValue;
|
||||
if (value is double doubleValue) return (int)doubleValue;
|
||||
if (value is decimal decimalValue) return (int)decimalValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string FormatTemplate(string template, object? args)
|
||||
{
|
||||
if (args == null || string.IsNullOrEmpty(template))
|
||||
{
|
||||
return template;
|
||||
}
|
||||
|
||||
var result = template;
|
||||
var type = args.GetType();
|
||||
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var placeholder = $"{{{prop.Name}}}";
|
||||
var value = prop.GetValue(args);
|
||||
result = result.Replace(placeholder, value?.ToString() ?? string.Empty);
|
||||
}
|
||||
|
||||
foreach (var field in fields)
|
||||
{
|
||||
var placeholder = $"{{{field.Name}}}";
|
||||
var value = field.GetValue(args);
|
||||
result = result.Replace(placeholder, value?.ToString() ?? string.Empty);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
12
DysonNetwork.Shared/Localization/LocalizationEntry.cs
Normal file
12
DysonNetwork.Shared/Localization/LocalizationEntry.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace DysonNetwork.Shared.Localization;
|
||||
|
||||
public class LocalizationEntry
|
||||
{
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
public string? One { get; set; }
|
||||
|
||||
public string? Other { get; set; }
|
||||
|
||||
public bool IsPlural => One != null || Other != null;
|
||||
}
|
||||
54
DysonNetwork.Shared/Localization/LocalizationExtensions.cs
Normal file
54
DysonNetwork.Shared/Localization/LocalizationExtensions.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace DysonNetwork.Shared.Localization;
|
||||
|
||||
public static class LocalizationExtensions
|
||||
{
|
||||
public static string Localize(this string key, string? locale = null, object? args = null)
|
||||
{
|
||||
var service = LocalizationServiceLocator.Service;
|
||||
if (service == null)
|
||||
{
|
||||
return key;
|
||||
}
|
||||
return service.Get(key, locale, args);
|
||||
}
|
||||
|
||||
public static string LocalizeCount(this string key, int count, string? locale = null, object? additionalArgs = null)
|
||||
{
|
||||
object args;
|
||||
if (additionalArgs != null)
|
||||
{
|
||||
args = new CountWrapper(count, additionalArgs);
|
||||
}
|
||||
else
|
||||
{
|
||||
args = new { count };
|
||||
}
|
||||
|
||||
var service = LocalizationServiceLocator.Service;
|
||||
if (service == null)
|
||||
{
|
||||
return key;
|
||||
}
|
||||
return service.Get(key, locale, args);
|
||||
}
|
||||
|
||||
public class CountWrapper
|
||||
{
|
||||
public int Count { get; }
|
||||
|
||||
private readonly object? _additionalArgs;
|
||||
|
||||
public CountWrapper(int count, object? additionalArgs)
|
||||
{
|
||||
Count = count;
|
||||
_additionalArgs = additionalArgs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class LocalizationServiceLocator
|
||||
{
|
||||
public static ILocalizationService? Service { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user