using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading; namespace PKHeX.Core; /// /// Language code & string asset loading. /// public static class GameLanguage { public const string DefaultLanguage = "en"; // English public const int DefaultLanguageIndex = 1; /// /// Language codes supported; mirrors . /// private static readonly string[] LanguageCodes = ["ja", "en", "fr", "it", "de", "es", "es-419", "ko", "zh-Hans", "zh-Hant"]; public static string LanguageCode(int localizationIndex) => (uint)localizationIndex >= LanguageCodes.Length ? DefaultLanguage : LanguageCodes[localizationIndex]; public static int LanguageCount => LanguageCodes.Length; /// /// Gets the language from the requested language code. /// /// Language code /// Index of the language code; if not a valid language code, returns the . public static int GetLanguageIndex(string lang) { int l = LanguageCodes.IndexOf(lang); return l < 0 ? DefaultLanguageIndex : l; } public static LanguageID GetLanguage(string lang) => lang switch { "ja" => LanguageID.Japanese, "en" => LanguageID.English, "fr" => LanguageID.French, "it" => LanguageID.Italian, "de" => LanguageID.German, "es" => LanguageID.Spanish, "es-419" => LanguageID.SpanishL, "ko" => LanguageID.Korean, "zh-Hans" => LanguageID.ChineseS, "zh-Hant" => LanguageID.ChineseT, _ => LanguageID.English, }; /// /// Checks whether the language code is supported. /// /// Language code /// True if valid, False otherwise public static bool IsLanguageValid(string lang) => LanguageCodes.Contains(lang); /// /// Language codes supported for loading string resources /// /// public static ReadOnlySpan AllSupportedLanguages => LanguageCodes; /// /// Gets a list of strings for the specified language and file type. /// public static string[] GetStrings(string ident, string lang, [ConstantExpected] string type = "text") { string[] data = Util.GetStringList(ident, lang, type); if (data.Length == 0) data = Util.GetStringList(ident, DefaultLanguage, type); return data; } } /// /// Wrapper to store language-specific data that is lazily loaded, with non-negligible load time/allocation. /// /// /// Provides a thread-safe way to cache loaded objects for only the languages that are supported. /// Slightly faster than using a ConcurrentDictionary, as we only need a fixed number of entries (one for each language). /// public abstract record LanguageStorage where T : notnull { private readonly T?[] _entries = new T[GameLanguage.LanguageCount]; // Lock for thread safety. Get operations are frequent, and usually will not require entering the lock as the entry is already populated. private readonly Lock _sync = new(); /// /// Not present in the cache, create a new instance for the specified language. /// protected abstract T Create(string language); private bool IsAllLoaded() { using var scope = _sync.EnterScope(); foreach (var entry in _entries) { if (entry is null) return false; } return true; } public T Get(string language) { int index = GameLanguage.GetLanguageIndex(language); var current = _entries[index]; if (current is not null) return current; using var scope = _sync.EnterScope(); // Now that we have the lock, check again. Another thread may have populated it while we were waiting. current = _entries[index]; if (current is not null) return current; return _entries[index] = Create(language); } /// /// Force loads all localizations. /// public bool ForceLoadAll() { var result = !IsAllLoaded(); // Load all languages if not already loaded. foreach (var lang in GameLanguage.AllSupportedLanguages) _ = Get(lang); return result; } /// /// Gets all localizations. /// /// /// If the entries are not already loaded, this will load all entries via . /// public IEnumerable<(string Key, T Value)> GetAll() { _ = ForceLoadAll(); for (var i = 0; i < _entries.Length; i++) { var entry = _entries[i]!; var lang = GameLanguage.LanguageCode(i); yield return (lang, entry); } } }