Files
UmbraClient/MareSynchronos/Localization/LocalizationService.cs
2025-09-21 17:01:12 +02:00

232 lines
7.6 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text.Json;
using System.Threading;
using Dalamud.Plugin;
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Localization;
public class LocalizationService
{
private readonly ILogger<LocalizationService> _logger;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly MareConfigService _configService;
private readonly Dictionary<LocalizationLanguage, Dictionary<string, string>> _translations = new();
private readonly HashSet<string> _missingLocalizationsLogged = new(StringComparer.Ordinal);
private readonly Lock _missingLocalizationsLock = new();
private static readonly JsonSerializerOptions JsonOptions = new()
{
AllowTrailingCommas = true
};
public static LocalizationService? Instance { get; private set; }
public LocalizationService(ILogger<LocalizationService> logger, IDalamudPluginInterface pluginInterface, MareConfigService configService)
{
_logger = logger;
_pluginInterface = pluginInterface;
_configService = configService;
Instance = this;
foreach (var language in Enum.GetValues<LocalizationLanguage>())
{
_translations[language] = LoadLanguage(language);
}
}
public LocalizationLanguage CurrentLanguage => _configService.Current.Language;
public static IEnumerable<LocalizationLanguage> SupportedLanguages => Enum.GetValues<LocalizationLanguage>();
public string GetString(string fallbackEnglish, params object[] formatArgs)
{
return GetStringInternal(null, fallbackEnglish, formatArgs);
}
public string GetString(string key, string fallbackEnglish, params object[] formatArgs)
{
return GetStringInternal(key, fallbackEnglish, formatArgs);
}
public string GetLanguageDisplayName(LocalizationLanguage language)
{
var fallback = language switch
{
LocalizationLanguage.French => "Français",
LocalizationLanguage.English => "English",
_ => language.ToString()
};
return GetRawString($"Language.DisplayName.{language}", fallback);
}
public void ReloadLanguage(LocalizationLanguage language)
{
_translations[language] = LoadLanguage(language);
}
public void ReloadAll()
{
foreach (var language in Enum.GetValues<LocalizationLanguage>())
{
ReloadLanguage(language);
}
}
private string GetStringInternal(string? key, string fallbackEnglish, params object[] formatArgs)
{
var usedKey = string.IsNullOrWhiteSpace(key) ? fallbackEnglish : key;
var text = GetRawString(usedKey, fallbackEnglish);
if (formatArgs != null && formatArgs.Length > 0)
{
try
{
return string.Format(CultureInfo.CurrentCulture, text, formatArgs);
}
catch (FormatException ex)
{
_logger.LogWarning(ex, "Localization format mismatch for key {Key} with text '{Text}'", usedKey, text);
try
{
return string.Format(CultureInfo.CurrentCulture, fallbackEnglish, formatArgs);
}
catch
{
return fallbackEnglish;
}
}
}
return text;
}
private string GetRawString(string key, string fallbackEnglish)
{
if (TryGetString(CurrentLanguage, key, out var localized))
{
return localized;
}
LogMissingLocalization(CurrentLanguage, key, fallbackEnglish);
if (TryGetString(LocalizationLanguage.English, key, out var english))
{
return english;
}
if (CurrentLanguage != LocalizationLanguage.English)
{
LogMissingLocalization(LocalizationLanguage.English, key, fallbackEnglish);
}
return fallbackEnglish;
}
private bool TryGetString(LocalizationLanguage language, string key, out string value)
{
var dictionary = GetDictionary(language);
if (dictionary.TryGetValue(key, out var text) && !string.IsNullOrWhiteSpace(text))
{
value = text;
return true;
}
value = string.Empty;
return false;
}
private Dictionary<string, string> GetDictionary(LocalizationLanguage language)
{
if (!_translations.TryGetValue(language, out var dictionary))
{
dictionary = LoadLanguage(language);
_translations[language] = dictionary;
}
return dictionary;
}
private Dictionary<string, string> LoadLanguage(LocalizationLanguage language)
{
var baseDirectory = _pluginInterface.AssemblyLocation.DirectoryName ?? string.Empty;
var localizationDirectory = Path.Combine(baseDirectory, "Localization");
var languageCode = GetLanguageCode(language);
var filePath = Path.Combine(localizationDirectory, $"{languageCode}.json");
Dictionary<string, string>? translations = null;
if (File.Exists(filePath))
{
try
{
using var stream = File.OpenRead(filePath);
translations = DeserializeTranslations(stream);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to load localization data from {FilePath}", filePath);
}
}
if (translations == null)
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = $"{assembly.GetName().Name}.Localization.{languageCode}.json";
using var resourceStream = assembly.GetManifestResourceStream(resourceName);
if (resourceStream != null)
{
try
{
translations = DeserializeTranslations(resourceStream);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to load embedded localization resource {Resource}", resourceName);
}
}
}
if (translations == null)
{
_logger.LogDebug("Localization data for {Language} not found on disk or embedded, using empty dictionary.", language);
translations = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
return translations;
}
private void LogMissingLocalization(LocalizationLanguage language, string key, string fallback)
{
var marker = $"{language}:{key}";
using var scope = _missingLocalizationsLock.EnterScope();
if (_missingLocalizationsLogged.Contains(marker)) return;
_missingLocalizationsLogged.Add(marker);
_logger.LogDebug("Missing localization for {Language} ({Key}). Using fallback '{Fallback}'.", language, key, fallback);
}
private static string GetLanguageCode(LocalizationLanguage language) => language switch
{
LocalizationLanguage.French => "fr",
LocalizationLanguage.English => "en",
_ => language.ToString().ToLowerInvariant(),
};
private static Dictionary<string, string>? DeserializeTranslations(Stream stream)
{
var translations = JsonSerializer.Deserialize<Dictionary<string, string>>(stream, JsonOptions);
return translations != null
? new Dictionary<string, string>(translations, StringComparer.OrdinalIgnoreCase)
: null;
}
}