using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; using Facepunch.Steamworks; using JetBrains.Annotations; using RoR2.ConVar; using SimpleJSON; using UnityEngine; namespace RoR2; public class Language { public class LanguageLoaderCoroutine { private Language language; public LanguageLoaderCoroutine(Language language) { this.language = language; } public IEnumerator LoadStringsWithYield() { if (!language.stringsLoaded) { language.stringsLoaded = true; List> tokenStringPairs = new List>(); LoadAllTokensFromFolders(language.folders, tokenStringPairs); yield return null; yield return SetStringsByTokenYield(tokenStringPairs); } } private IEnumerator SetStringsByTokenYield(List> tokenStringPairs) { int maxStringsPerYield = 200; int stringsThisYield = 0; foreach (KeyValuePair tokenStringPair in tokenStringPairs) { language.SetStringByToken(tokenStringPair.Key, tokenStringPair.Value); if (stringsThisYield++ >= maxStringsPerYield) { stringsThisYield = 0; yield return null; } } } } private struct SteamLanguageDef { public readonly string englishName; public readonly string nativeName; public readonly string apiName; public readonly string webApiName; public SteamLanguageDef(string englishName, string nativeName, string apiName, string webApiName) { this.englishName = englishName; this.nativeName = nativeName; this.apiName = apiName; this.webApiName = webApiName; } } public class LanguageConVar : BaseConVar { private static readonly string platformString = "platform"; public static LanguageConVar instance = new LanguageConVar("language", ConVarFlags.Archive, platformString, "Which language to use."); private string internalValue = platformString; public LanguageConVar(string name, ConVarFlags flags, string defaultValue, string helpText) : base(name, flags, defaultValue, helpText) { } public override void SetString(string newValue) { if (!IsAnyLanguageLoaded()) { internalValue = newValue; return; } if (string.Equals(newValue, "EN_US", StringComparison.Ordinal) || FindLanguageByName(newValue) == null) { newValue = platformString; } internalValue = newValue; if (string.Equals(internalValue, platformString, StringComparison.Ordinal)) { newValue = GetPlatformLanguageName() ?? "en"; } SetCurrentLanguage(newValue); } public override string GetString() { return internalValue; } } private static readonly Dictionary languagesByName = new Dictionary(StringComparer.OrdinalIgnoreCase); private static bool isDebugStringOverrideEnabled = false; private static bool isDummyStringOverrideEnabled = false; private static string dummyString = ".•."; private readonly Dictionary stringsByToken = new Dictionary(); private Language fallbackLanguage; private string[] folders = Array.Empty(); private bool foundManifest; public readonly string name; private static readonly Dictionary steamLanguageTable = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["arabic"] = new SteamLanguageDef("Arabic", "العربية", "arabic", "ar"), ["bulgarian"] = new SteamLanguageDef("Bulgarian", "български език", "bulgarian", "bg"), ["schinese"] = new SteamLanguageDef("Chinese (Simplified)", "简体中文", "schinese", "zh-CN"), ["tchinese"] = new SteamLanguageDef("Chinese (Traditional)", "繁體中文", "tchinese", "zh-TW"), ["czech"] = new SteamLanguageDef("Czech", "čeština", "czech", "cs"), ["danish"] = new SteamLanguageDef("Danish", "Dansk", "danish", "da"), ["dutch"] = new SteamLanguageDef("Dutch", "Nederlands", "dutch", "nl"), ["english"] = new SteamLanguageDef("English", "English", "english", "en"), ["finnish"] = new SteamLanguageDef("Finnish", "Suomi", "finnish", "fi"), ["french"] = new SteamLanguageDef("French", "Français", "french", "fr"), ["german"] = new SteamLanguageDef("German", "Deutsch", "german", "de"), ["greek"] = new SteamLanguageDef("Greek", "Ελληνικά", "greek", "el"), ["hungarian"] = new SteamLanguageDef("Hungarian", "Magyar", "hungarian", "hu"), ["italian"] = new SteamLanguageDef("Italian", "Italiano", "italian", "it"), ["japanese"] = new SteamLanguageDef("Japanese", "日本語", "japanese", "ja"), ["koreana"] = new SteamLanguageDef("Korean", "한국어", "koreana", "ko"), ["korean"] = new SteamLanguageDef("Korean", "한국어", "korean", "ko"), ["norwegian"] = new SteamLanguageDef("Norwegian", "Norsk", "norwegian", "no"), ["polish"] = new SteamLanguageDef("Polish", "Polski", "polish", "pl"), ["portuguese"] = new SteamLanguageDef("Portuguese", "Português", "portuguese", "pt"), ["brazilian"] = new SteamLanguageDef("Portuguese-Brazil", "Português-Brasil", "brazilian", "pt-BR"), ["romanian"] = new SteamLanguageDef("Romanian", "Română", "romanian", "ro"), ["russian"] = new SteamLanguageDef("Russian", "Русский", "russian", "ru"), ["spanish"] = new SteamLanguageDef("Spanish-Spain", "Español-España", "spanish", "es"), ["latam"] = new SteamLanguageDef("Spanish-Latin America", "Español-Latinoamérica", "latam", "es-419"), ["swedish"] = new SteamLanguageDef("Swedish", "Svenska", "swedish", "sv"), ["thai"] = new SteamLanguageDef("Thai", "ไทย", "thai", "th"), ["turkish"] = new SteamLanguageDef("Turkish", "Türkçe", "turkish", "tr"), ["ukrainian"] = new SteamLanguageDef("Ukrainian", "Українська", "ukrainian", "uk"), ["vietnamese"] = new SteamLanguageDef("Vietnamese", "Tiếng Việt", "vietnamese", "vn") }; public string selfName { get; private set; } = string.Empty; public Sprite iconSprite { get; private set; } public bool stringsLoaded { get; private set; } public bool hasEntries => stringsByToken.Count > 0; public static string currentLanguageName { get; private set; } = ""; public static Language currentLanguage { get; private set; } = null; public static Language english { get; private set; } public static event Action onCurrentLanguageChanged; public static event Action> collectLanguageRootFolders; private Language() { } private Language(string name) { this.name = name; } private void SetFolders([NotNull] IEnumerable newFolders) { folders = newFolders.ToArray(); foundManifest = false; for (int num = folders.Length - 1; num >= 0; num--) { string path = folders[num]; if (Directory.Exists(path)) { string text = Directory.EnumerateFiles(path, "language.json").FirstOrDefault(); foundManifest |= text != null; if (text != null) { LoadManifest(text); } string text2 = Directory.EnumerateFiles(path, "icon.png").FirstOrDefault(); if (text2 != null) { SetIconSprite(BuildSpriteFromTextureFile(text2)); break; } } } } private void SetIconSprite(Sprite newIconSprite) { if ((bool)iconSprite) { UnityEngine.Object.Destroy(iconSprite.texture); UnityEngine.Object.Destroy(iconSprite); } iconSprite = newIconSprite; } private void LoadManifest(string file) { using Stream stream = File.Open(file, FileMode.Open, FileAccess.Read); using StreamReader streamReader = new StreamReader(stream, Encoding.UTF8); JSONNode jSONNode = JSON.Parse(streamReader.ReadToEnd()); if (!(jSONNode != null)) { return; } JSONNode jSONNode2 = jSONNode["language"]; if (jSONNode2 != null) { string text = jSONNode2["selfname"]; if (text != null) { selfName = text; } } } [NotNull] public string GetLocalizedStringByToken([NotNull] string token) { if (isDummyStringOverrideEnabled) { return dummyString; } if (isDebugStringOverrideEnabled) { return token; } if (stringsByToken.TryGetValue(token, out var value)) { return value; } if (fallbackLanguage != null) { return fallbackLanguage.GetLocalizedStringByToken(token); } return token; } [NotNull] public string GetLocalizedFormattedStringByToken([NotNull] string token, params object[] args) { return string.Format(GetLocalizedStringByToken(token), args); } public void SetStringByToken([NotNull] string token, [NotNull] string localizedString) { stringsByToken[token] = localizedString; } public void SetStringsByTokens([NotNull] IEnumerable> tokenPairs) { foreach (KeyValuePair tokenPair in tokenPairs) { SetStringByToken(tokenPair.Key, tokenPair.Value); } } public void LoadStrings() { if (!stringsLoaded) { stringsLoaded = true; List> list = new List>(); LoadAllTokensFromFolders(folders, list); SetStringsByTokens(list); } } public void UnloadStrings() { if (stringsLoaded) { stringsLoaded = false; stringsByToken.Clear(); } } public bool TokenIsRegistered([NotNull] string token) { return stringsByToken.ContainsKey(token); } [CanBeNull] public static Language FindLanguageByName([NotNull] string languageName) { if (languagesByName.TryGetValue(languageName, out var value)) { return value; } return null; } [NotNull] public static string GetString([NotNull] string token, [NotNull] string language) { return FindLanguageByName(language)?.GetLocalizedStringByToken(token) ?? token; } [NotNull] public static string GetString([NotNull] string token) { return currentLanguage?.GetLocalizedStringByToken(token) ?? token; } [NotNull] public static string GetStringFormatted([NotNull] string token, params object[] args) { return currentLanguage?.GetLocalizedFormattedStringByToken(token, args) ?? string.Format(token, args); } public static bool IsTokenInvalid([NotNull] string token) { return !(currentLanguage?.TokenIsRegistered(token) ?? false); } public static IEnumerable GetAllLanguages() { return languagesByName.Values; } [NotNull] private static Language GetOrCreateLanguage([NotNull] string languageName) { if (!languagesByName.TryGetValue(languageName, out var value)) { value = (languagesByName[languageName] = new Language(languageName)); } return value; } private static void LoadAllTokensFromFolders([NotNull] IEnumerable folders, [NotNull] List> output) { foreach (string folder in folders) { LoadAllTokensFromFolder(folder, output); } } private static void LoadAllTokensFromFolder([NotNull] string folder, [NotNull] List> output) { PlatformSystems.textDataManager.GetLocFiles(folder, delegate(string[] contents) { int num = contents.Length; for (int i = 0; i < num; i++) { LoadTokensFromData(contents[i], output); } }); } private static void LoadTokensFromData([NotNull] string contents, [NotNull] List> output) { JSONNode jSONNode = JSON.Parse(contents); if (!(jSONNode != null)) { return; } JSONNode jSONNode2 = jSONNode["strings"]; if (!(jSONNode2 != null)) { return; } foreach (string key in jSONNode2.Keys) { output.Add(new KeyValuePair(key, jSONNode2[key].Value)); } } private static void LoadTokensFromFile([NotNull] string file, [NotNull] List> output) { using Stream stream = File.Open(file, FileMode.Open, FileAccess.Read); using StreamReader streamReader = new StreamReader(stream, Encoding.UTF8); JSONNode jSONNode = JSON.Parse(streamReader.ReadToEnd()); if (!(jSONNode != null)) { return; } JSONNode jSONNode2 = jSONNode["strings"]; if (!(jSONNode2 != null)) { return; } foreach (string key in jSONNode2.Keys) { output.Add(new KeyValuePair(key, jSONNode2[key].Value)); } } [NotNull] private static List GetLanguageRootFolders() { List list = new List(); try { Language.collectLanguageRootFolders?.Invoke(list); } catch (Exception ex) { Debug.LogErrorFormat("Encountered error loading language folders: {0}", ex); } return list; } private static IEnumerator BuildLanguagesFromFolders() { List languageRootFolders = GetLanguageRootFolders(); IGrouping[] allLanguageFolderGroups = languageRootFolders.Where((string i) => Directory.Exists(i)).SelectMany((string languageRootFolder) => Directory.EnumerateDirectories(languageRootFolder)).GroupBy((string languageRootFolder) => new DirectoryInfo(languageRootFolder).Name, StringComparer.OrdinalIgnoreCase) .ToArray(); yield return null; IGrouping[] array = allLanguageFolderGroups; foreach (IGrouping grouping in array) { Language orCreateLanguage = GetOrCreateLanguage(grouping.Key); orCreateLanguage.SetFolders(grouping); if (!orCreateLanguage.foundManifest) { languagesByName.Remove(grouping.Key); } } yield return null; } private static bool IsAnyLanguageLoaded() { return languagesByName.Count > 0; } private static void SetCurrentLanguage([NotNull] string newCurrentLanguageName) { Debug.LogFormat("Setting current language to \"{0}\"", newCurrentLanguageName); if (currentLanguage != english) { currentLanguage?.UnloadStrings(); } currentLanguageName = newCurrentLanguageName; currentLanguage = FindLanguageByName(currentLanguageName); if (currentLanguage == null && string.Compare(currentLanguageName, "en", StringComparison.OrdinalIgnoreCase) != 0) { Debug.LogFormat("Could not load files for language \"{0}\". Falling back to \"en\".", newCurrentLanguageName); currentLanguageName = "en"; currentLanguage = FindLanguageByName(currentLanguageName); } CultureInfo.CurrentCulture = new CultureInfo(currentLanguage.name); currentLanguage?.LoadStrings(); Language.onCurrentLanguageChanged?.Invoke(); } public static IEnumerator Init() { yield return BuildLanguagesFromFolders(); if (LanguageConVar.instance != null) { LanguageConVar.instance.SetString(LanguageConVar.instance.GetString()); } english = GetOrCreateLanguage("en"); LanguageLoaderCoroutine languageLoaderCoroutine = new LanguageLoaderCoroutine(english); yield return languageLoaderCoroutine.LoadStringsWithYield(); foreach (Language allLanguage in GetAllLanguages()) { if (allLanguage != english) { allLanguage.fallbackLanguage = english; } } } private static Sprite BuildSpriteFromTextureFile(string file) { Texture2D texture2D = new Texture2D(2, 2, TextureFormat.ARGB32, mipChain: false, linear: false); texture2D.wrapMode = TextureWrapMode.Clamp; texture2D.LoadImage(File.ReadAllBytes(file), markNonReadable: true); return Sprite.Create(texture2D, new Rect(0f, 0f, texture2D.width, texture2D.height), new Vector2(0.5f, 0.5f), 1f, 1u, SpriteMeshType.FullRect, Vector4.zero); } [CanBeNull] public static string GetPlatformLanguageName() { string text = Client.Instance?.CurrentLanguage; if (text == null) { return null; } if (steamLanguageTable.TryGetValue(text, out var value)) { return value.webApiName; } return null; } [ConCommand(commandName = "language_reload", flags = ConVarFlags.None, helpText = "Reloads the current language.")] public static void CCLanguageReload(ConCommandArgs args) { SetCurrentLanguage(currentLanguageName); } [ConCommand(commandName = "language_dump_to_json", flags = ConVarFlags.None, helpText = "Combines all files for the given language into a single JSON file.")] private static void CCLanguageDumpToJson(ConCommandArgs args) { string argString = args.GetArgString(0); Language obj = FindLanguageByName(argString) ?? throw new ConCommandException($"'{argString}' is not a valid language name."); List> list = new List>(); LoadAllTokensFromFolders(obj.folders, list); new StringBuilder(); JSONNode jSONNode = new JSONObject(); JSONNode jSONNode3 = (jSONNode["strings"] = new JSONObject()); foreach (KeyValuePair item in list) { jSONNode3[item.Key] = item.Value; } File.WriteAllText("output.json", jSONNode.ToString(1), Encoding.UTF8); } [ConCommand(commandName = "language_dummy_strings", flags = ConVarFlags.None, helpText = "Toggles use of a dummy string for all text")] private static void CCLanguageDummyStringsToggle(ConCommandArgs args) { isDummyStringOverrideEnabled = !isDummyStringOverrideEnabled; } [ConCommand(commandName = "language_debug_strings", flags = ConVarFlags.None, helpText = "Toggles use of debug strings for all text")] private static void CCLanguageDebugStringsToggle(ConCommandArgs args) { isDebugStringOverrideEnabled = !isDebugStringOverrideEnabled; Debug.Log($"isDummyStringOverrideEnabled={isDummyStringOverrideEnabled}"); } }