using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using CheapLoc; using Serilog; namespace Dalamud; /// /// Class handling localization. /// [ServiceManager.ProvidedService] public class Localization : IServiceType { /// /// Array of language codes which have a valid translation in Dalamud. /// public static readonly string[] ApplicableLangCodes = { "de", "ja", "fr", "it", "es", "ko", "no", "ru", "zh", "tw" }; private const string FallbackLangCode = "en"; private readonly string locResourceDirectory; private readonly string locResourcePrefix; private readonly bool useEmbedded; private readonly Assembly assembly; /// /// Initializes a new instance of the class. /// /// The working directory to load language files from. /// The prefix on the loc resource file name (e.g. dalamud_). /// Use embedded loc resource files. public Localization(string locResourceDirectory, string locResourcePrefix = "", bool useEmbedded = false) { this.DalamudLanguageCultureInfo = CultureInfo.InvariantCulture; this.locResourceDirectory = locResourceDirectory; this.locResourcePrefix = locResourcePrefix; this.useEmbedded = useEmbedded; this.assembly = Assembly.GetCallingAssembly(); } /// /// Delegate for the event that occurs when the language is changed. /// /// The language code of the new language. public delegate void LocalizationChangedDelegate(string langCode); /// /// Event that occurs when the language is changed. /// public event LocalizationChangedDelegate? LocalizationChanged; /// /// Gets an instance of that corresponds to the language configured from Dalamud Settings. /// public CultureInfo DalamudLanguageCultureInfo { get; private set; } /// /// Gets an instance of that corresponds to a Dalamud . /// /// The language code which should be in . /// The corresponding instance of . public static CultureInfo GetCultureInfoFromLangCode(string langCode) => CultureInfo.GetCultureInfo(langCode switch { "tw" => "zh-hant", "zh" => "zh-hans", _ => langCode, }); /// /// Search the set-up localization data for the provided assembly for the given string key and return it. /// If the key is not present, the fallback is shown. /// The fallback is also required to create the string files to be localized. /// /// The string key to be returned. /// The fallback string, usually your source language. /// The localized string, fallback or string key if not found. // TODO: This breaks loc export, since it's being called without string args. Fix in CheapLoc. public static string Localize(string key, string fallBack) { return Loc.Localize(key, fallBack, Assembly.GetCallingAssembly()); } /// /// Set up the UI language with the users' local UI culture. /// public void SetupWithUiCulture() { try { var currentUiLang = CultureInfo.CurrentUICulture; Log.Information("Trying to set up Loc for culture {0}", currentUiLang.TwoLetterISOLanguageName); if (ApplicableLangCodes.Any(langCode => currentUiLang.TwoLetterISOLanguageName == langCode)) { this.SetupWithLangCode(currentUiLang.TwoLetterISOLanguageName); } else { this.SetupWithFallbacks(); } } catch (Exception ex) { Log.Error(ex, "Could not get language information. Setting up fallbacks."); this.SetupWithFallbacks(); } } /// /// Set up the UI language with "fallbacks" (original English text). /// public void SetupWithFallbacks() { this.DalamudLanguageCultureInfo = CultureInfo.InvariantCulture; Loc.SetupWithFallbacks(this.assembly); foreach (var d in Delegate.EnumerateInvocationList(this.LocalizationChanged)) { try { d(FallbackLangCode); } catch (Exception ex) { Log.Error(ex, "Exception during raise of {handler}", d.Method); } } } /// /// Set up the UI language with the provided language code. /// /// The language code to set up the UI language with. public void SetupWithLangCode(string langCode) { if (langCode.Equals(FallbackLangCode, StringComparison.InvariantCultureIgnoreCase)) { this.SetupWithFallbacks(); return; } this.DalamudLanguageCultureInfo = GetCultureInfoFromLangCode(langCode); try { Loc.Setup(this.ReadLocData(langCode), this.assembly); } catch (Exception ex) { Log.Error(ex, "Could not load loc {0}. Setting up fallbacks.", langCode); this.SetupWithFallbacks(); } foreach (var d in Delegate.EnumerateInvocationList(this.LocalizationChanged)) { try { d(langCode); } catch (Exception ex) { Log.Error(ex, "Exception during raise of {handler}", d.Method); } } } /// /// Saves localizable JSON data in the current working directory for the provided assembly. /// /// If set to true, this ignores malformed Localize functions instead of failing. public void ExportLocalizable(bool ignoreInvalidFunctions = false) { Loc.ExportLocalizableForAssembly(this.assembly, ignoreInvalidFunctions); } /// /// Creates a new instance of the class. /// /// Path to Dalamud assets. /// Optional language override. /// A new instance. internal static Localization FromAssets(string assetDirectory, string? languageOverride) { var t = new Localization(Path.Combine(assetDirectory, "UIRes", "loc", "dalamud"), "dalamud_"); if (!string.IsNullOrEmpty(languageOverride)) t.SetupWithLangCode(languageOverride); else t.SetupWithUiCulture(); return t; } private string ReadLocData(string langCode) { if (this.useEmbedded) { var resourceStream = this.assembly.GetManifestResourceStream($"{this.locResourceDirectory}{this.locResourcePrefix}{langCode}.json"); if (resourceStream == null) return null; using var reader = new StreamReader(resourceStream); return reader.ReadToEnd(); } return File.ReadAllText(Path.Combine(this.locResourceDirectory, $"{this.locResourcePrefix}{langCode}.json")); } }