232 lines
7.6 KiB
C#
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;
|
|
}
|
|
}
|