using System; using System.Collections.Generic; using System.Drawing.Drawing2D; using System.Globalization; using System.IO; using System.Text; using System.Text.RegularExpressions; namespace MinecraftClient { /// /// Allows to localize MinecraftClient in different languages /// /// /// By ORelio (c) 2015-2018 - CDDL 1.0 /// public static class Translations { private static readonly Dictionary translations = new(); private static readonly string translationFilePath = "lang" + Path.DirectorySeparatorChar + "mcc"; private static readonly string defaultTranslation = "en.ini"; private static readonly Regex translationKeyRegex = new(@"\(\[(.*?)\]\)", RegexOptions.Compiled); // Extract string inside ([ ]) /// /// Return a tranlation for the requested text. Support string formatting /// /// text identifier /// returns translation for this identifier public static string Get(string msgName, params object?[] args) { if (translations.ContainsKey(msgName)) { if (args.Length > 0) return string.Format(translations[msgName], args); else return translations[msgName]; } return msgName.ToUpper(); } /// /// Return a tranlation for the requested text. Support string formatting. If not found, return the original text /// /// text identifier /// /// Translated text or original text if not found /// Useful when not sure msgName is a translation mapping key or a normal text public static string TryGet(string msgName, params object?[] args) { if (translations.ContainsKey(msgName)) return Get(msgName, args); else return msgName; } /// /// Return a tranlation for the requested text. Support string formatting. If not found, return the original text /// /// text identifier /// /// Translated text or original text if not found /// Useful when not sure msgName is a translation mapping key or a normal text public static string? GetOrNull(string msgName, params object?[] args) { if (translations.ContainsKey(msgName)) return Get(msgName, args); else return null; } /// /// Replace the translation key inside a sentence to translated text. Wrap the key in ([translation.key]) /// /// /// e.g. I only want to replace ([this]) /// would only translate "this" without touching other words. /// /// Sentence for replace /// /// Translated sentence public static string Replace(string msg, params object[] args) { string translated = translationKeyRegex.Replace(msg, new MatchEvaluator(ReplaceKey)); if (args.Length > 0) return string.Format(translated, args); else return translated; } private static string ReplaceKey(Match m) { return Get(m.Groups[1].Value); } public static Tuple GetTranslationPriority() { string gameLanguage = "en_gb"; List name = new(); string systemLanguage = string.IsNullOrWhiteSpace(CultureInfo.CurrentCulture.Name) ? CultureInfo.CurrentCulture.Parent.Name : CultureInfo.CurrentCulture.Name; switch (systemLanguage) { case "af": case "af-ZA": gameLanguage = "af_za"; break; case "ar": case "ar-AE": case "ar-BH": case "ar-DZ": case "ar-EG": case "ar-IQ": case "ar-JO": case "ar-KW": case "ar-LB": case "ar-LY": case "ar-MA": case "ar-OM": case "ar-QA": case "ar-SA": case "ar-SY": case "ar-TN": case "ar-YE": gameLanguage = "ar_sa"; break; case "az": case "az-Cyrl-AZ": case "az-Latn-AZ": gameLanguage = "az_az"; break; case "be": case "be-BY": gameLanguage = "be_by"; break; case "bg": case "bg-BG": gameLanguage = "bg_bg"; break; case "bs-Latn-BA": gameLanguage = "bs_ba"; break; case "ca": case "ca-ES": gameLanguage = "ca_es"; break; case "cs": case "cs-CZ": gameLanguage = "cs_cz"; break; case "cy-GB": gameLanguage = "cy_gb"; break; case "da": case "da-DK": gameLanguage = "da_dk"; break; case "de": case "de-DE": case "de-LI": case "de-LU": gameLanguage = "de_de"; name.Add("de"); break; case "de-AT": gameLanguage = "de_at"; name.Add("de"); break; case "de-CH": gameLanguage = "de_ch"; name.Add("de"); break; case "dv": case "dv-MV": break; case "el": case "el-GR": gameLanguage = "el_gr"; break; case "en": case "en-029": case "en-BZ": case "en-IE": case "en-JM": case "en-PH": case "en-TT": case "en-ZA": case "en-ZW": case "en-GB": gameLanguage = "en_gb"; break; case "en-AU": gameLanguage = "en_au"; break; case "en-CA": gameLanguage = "en_ca"; break; case "en-US": gameLanguage = "en_us"; break; case "en-NZ": gameLanguage = "en_nz"; break; case "es": case "es-BO": case "es-CO": case "es-CR": case "es-DO": case "es-ES": case "es-GT": case "es-HN": case "es-NI": case "es-PA": case "es-PE": case "es-PR": case "es-PY": case "es-SV": gameLanguage = "es_es"; break; case "es-AR": gameLanguage = "es_ar"; break; case "es-CL": gameLanguage = "es_cl"; break; case "es-EC": gameLanguage = "es_ec"; break; case "es-MX": gameLanguage = "es_mx"; break; case "es-UY": gameLanguage = "es_uy"; break; case "es-VE": gameLanguage = "es_ve"; break; case "et": case "et-EE": gameLanguage = "et_ee"; break; case "eu": case "eu-ES": gameLanguage = "eu_es"; break; case "fa": case "fa-IR": gameLanguage = "fa_ir"; break; case "fi": case "fi-FI": gameLanguage = "fi_fi"; break; case "fo": case "fo-FO": gameLanguage = "fo_fo"; break; case "fr": case "fr-BE": case "fr-FR": case "fr-CH": case "fr-LU": case "fr-MC": gameLanguage = "fr_fr"; name.Add("fr"); break; case "fr-CA": gameLanguage = "fr_ca"; name.Add("fr"); break; case "gl": case "gl-ES": gameLanguage = "gl_es"; break; case "gu": case "gu-IN": break; case "he": case "he-IL": gameLanguage = "he_il"; break; case "hi": case "hi-IN": gameLanguage = "hi_in"; break; case "hr": case "hr-BA": case "hr-HR": gameLanguage = "hr_hr"; break; case "hu": case "hu-HU": gameLanguage = "hu_hu"; break; case "hy": case "hy-AM": gameLanguage = "hy_am"; break; case "id": case "id-ID": gameLanguage = "id_id"; break; case "is": case "is-IS": gameLanguage = "is_is"; break; case "it": case "it-CH": case "it-IT": gameLanguage = "it_it"; break; case "ja": case "ja-JP": gameLanguage = "ja_jp"; break; case "ka": case "ka-GE": gameLanguage = "ka_ge"; break; case "kk": case "kk-KZ": gameLanguage = "kk_kz"; break; case "kn": case "kn-IN": gameLanguage = "kn_in"; break; case "kok": case "kok-IN": break; case "ko": case "ko-KR": gameLanguage = "ko_kr"; break; case "ky": case "ky-KG": break; case "lt": case "lt-LT": gameLanguage = "lt_lt"; break; case "lv": case "lv-LV": gameLanguage = "lv_lv"; break; case "mi-NZ": break; case "mk": case "mk-MK": gameLanguage = "mk_mk"; break; case "mn": case "mn-MN": gameLanguage = "mn_mn"; break; case "mr": case "mr-IN": break; case "ms": case "ms-BN": case "ms-MY": gameLanguage = "ms_my"; break; case "mt-MT": gameLanguage = "mt_mt"; break; case "nb-NO": break; case "nl": case "nl-NL": gameLanguage = "nl_nl"; break; case "nl-BE": gameLanguage = "nl_be"; break; case "nn-NO": gameLanguage = "nn_no"; break; case "no": gameLanguage = "no_no‌"; break; case "ns-ZA": break; case "pa": case "pa-IN": break; case "pl": case "pl-PL": gameLanguage = "pl_pl‌"; break; case "pt": case "pt-PT": gameLanguage = "pt_pt‌"; break; case "pt-BR": gameLanguage = "pt_br‌"; break; case "quz-BO": break; case "quz-EC": break; case "quz-PE": break; case "ro": case "ro-RO": gameLanguage = "ro_ro‌"; break; case "ru": case "ru-RU": gameLanguage = "ru_ru"; name.Add("ru"); break; case "sa": case "sa-IN": break; case "se-FI": case "se-NO": case "se-SE": gameLanguage = "se_no"; break; case "sk": case "sk-SK": gameLanguage = "sk_sk"; break; case "sl": case "sl-SI": gameLanguage = "sl_si"; break; case "sma-NO": break; case "sma-SE": break; case "smj-NO": break; case "smj-SE": break; case "smn-FI": break; case "sms-FI": break; case "sq": case "sq-AL": gameLanguage = "sq_al"; break; case "sr": case "sr-Cyrl-BA": case "sr-Cyrl-CS": case "sr-Latn-BA": case "sr-Latn-CS": gameLanguage = "sr_sp"; break; case "sv": case "sv-FI": case "sv-SE": gameLanguage = "sv_se"; break; case "sw": case "sw-KE": break; case "syr": case "syr-SY": break; case "ta": case "ta-IN": gameLanguage = "ta_in"; break; case "te": case "te-IN": break; case "th": case "th-TH": gameLanguage = "th_th"; break; case "tn-ZA": break; case "tr": case "tr-TR": gameLanguage = "tr_tr"; break; case "tt": case "tt-RU": gameLanguage = "tt_ru"; break; case "uk": case "uk-UA": gameLanguage = "uk_ua"; break; case "ur": case "ur-PK": break; case "uz": case "uz-Cyrl-UZ": case "uz-Latn-UZ": break; case "vi": case "vi-VN": gameLanguage = "vi_vn"; name.Add("vi"); break; case "xh-ZA": break; case "zh-Hans": /* CurrentCulture.Parent.Name */ case "zh": case "zh-CN": case "zh-CHS": case "zh-SG": gameLanguage = "zh_cn"; name.Add("zh_Hans"); name.Add("zh_Hant"); break; case "zh-Hant": /* CurrentCulture.Parent.Name */ case "zh-HK": case "zh-CHT": case "zh-MO": gameLanguage = "zh_hk"; name.Add("zh_Hant"); name.Add("zh_Hans"); break; case "zh-TW": gameLanguage = "zh_tw"; name.Add("zh_Hant"); name.Add("zh_Hans"); break; case "zu-ZA": break; } name.Add("en"); return new(gameLanguage, name.ToArray()); } public static string[] GetTranslationPriority(string gameLanguage) { List name = new(); switch (gameLanguage) { case "de_at": case "de_ch": case "de_de": name.Add("de"); break; case "en_au": case "en_ca": case "en_gb": case "en_nz": case "en_pt": case "en_ud": case "en_us": break; case "fr_ca": case "fr_fr": name.Add("fr"); break; case "ru_ru": name.Add("ru"); break; case "vi_vn": name.Add("vi"); break; case "zh_cn": name.Add("zh_Hans"); name.Add("zh_Hant"); break; case "zh_hk": case "zh_tw": name.Add("zh_Hant"); name.Add("zh_Hans"); break; } name.Add("en"); return name.ToArray(); } /// /// Load translation files /// public static void LoadTranslationFile(string[] languageList) { /* * External translation files * These files are loaded from the installation directory as: * Lang/abc.ini, e.g. Lang/eng.ini which is the default language file * Useful for adding new translations of fixing typos without recompiling */ string baseDir = AppDomain.CurrentDomain.BaseDirectory; string langDir = baseDir + ((baseDir.EndsWith(Path.DirectorySeparatorChar) ? String.Empty : Path.DirectorySeparatorChar) + translationFilePath + Path.DirectorySeparatorChar); foreach (string lang in languageList) { bool fileLoaded = false; string langFileName = string.Format("{0}{1}.ini", langDir, lang); if (File.Exists(langFileName)) // Language set in ini config { fileLoaded = true; Dictionary trans = ParseTranslationContent(File.ReadAllLines(langFileName)); foreach ((string key, string value) in trans) if (!string.IsNullOrWhiteSpace(value) && !translations.ContainsKey(key)) translations.Add(key, value); } string? resourseLangFile = DefaultConfigResource.ResourceManager.GetString("Translation_" + lang); if (resourseLangFile != null) { fileLoaded = true; Dictionary trans = ParseTranslationContent(resourseLangFile.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)); foreach ((string key, string value) in trans) if (!string.IsNullOrWhiteSpace(value) && !translations.ContainsKey(key)) translations.Add(key, value); } if (!fileLoaded) { if (Settings.Config.Logging.DebugMessages) ConsoleIO.WriteLogLine("[Translations] No translation file found for " + lang + ". (Looked '" + langFileName + "'"); } } } /// /// Parse the given array to translation map /// /// Content of the translation file (in ini format) private static Dictionary ParseTranslationContent(string[] content) { Dictionary translations = new(); foreach (string lineRaw in content) { string line = lineRaw.Trim(); if (line.Length < 3) continue; if (!char.IsLetterOrDigit(line[0])) // ignore comment line started with # continue; int index = line.IndexOf('='); if (line.Length > (index + 1)) translations[line[..index]] = line[(index + 1)..].Replace("\\n", "\n"); } return translations; } /// /// Write the default translation file (English) to the disk. /// private static void WriteDefaultTranslation() { string defaultPath = AppDomain.CurrentDomain.BaseDirectory + Path.DirectorySeparatorChar + translationFilePath + Path.DirectorySeparatorChar + defaultTranslation; if (!Directory.Exists(translationFilePath)) Directory.CreateDirectory(translationFilePath); File.WriteAllText(defaultPath, DefaultConfigResource.Translation_en, Encoding.UTF8); } public static void TrimAllTranslations() { string[] transEn = DefaultConfigResource.ResourceManager.GetString("Translation_en")! .Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); foreach (string lang in new string[] { "de", "fr", "ru", "vi", "zh_Hans" }) { Dictionary trans = ParseTranslationContent( DefaultConfigResource.ResourceManager.GetString("Translation_" + lang)! .Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None) ); using FileStream file = File.OpenWrite(AppDomain.CurrentDomain.BaseDirectory + Path.DirectorySeparatorChar + translationFilePath + Path.DirectorySeparatorChar + lang + ".ini"); int total = 0, translated = 0; for (int i = 0; i < transEn.Length; ++i) { string line = transEn[i].Trim(); int index = transEn[i].IndexOf('='); if (line.Length < 3 || !char.IsLetterOrDigit(line[0]) || index == -1 || line.Length <= (index + 1)) { file.Write(Encoding.UTF8.GetBytes(line)); } else { string key = line[..index]; file.Write(Encoding.UTF8.GetBytes(key)); file.Write(Encoding.UTF8.GetBytes("=")); if (trans.TryGetValue(key, out string? value)) { file.Write(Encoding.UTF8.GetBytes(value.Replace("\n", "\\n"))); ++total; ++translated; } else { ++total; } } file.Write(Encoding.UTF8.GetBytes(Environment.NewLine)); } ConsoleIO.WriteLine(string.Format("Language {0}: Translated {1} of {2}, {3:0.00}%", lang, translated, total, 100.0 * (double)translated / total)); } } #region Console writing method wrapper /// /// Translate the key and write the result to the standard output, without newline character /// /// Translation key public static void Write(string key) { ConsoleIO.WriteLine(Get(key)); } /// /// Translate the key and write a Minecraft-Like formatted string to the standard output, using §c color codes /// See minecraft.gamepedia.com/Classic_server_protocol#Color_Codes for more info /// /// Translation key /// If false, space are printed instead of newlines /// /// If false, no timestamp is prepended. /// If true, "hh-mm-ss" timestamp will be prepended. /// If unspecified, value is retrieved from EnableTimestamps. /// public static void WriteLineFormatted(string key, bool acceptnewlines = true, bool? displayTimestamp = null) { ConsoleIO.WriteLineFormatted(Get(key), acceptnewlines, displayTimestamp); } /// /// Translate the key, format the result and write it to the standard output with a trailing newline. Support string formatting /// /// Translation key /// public static void WriteLine(string key, params object[] args) { if (args.Length > 0) ConsoleIO.WriteLine(string.Format(Get(key), args)); else ConsoleIO.WriteLine(Get(key)); } /// /// Translate the key and write the result with a prefixed log line. Prefix is set in LogPrefix. /// /// Translation key /// Allow line breaks public static void WriteLogLine(string key, bool acceptnewlines = true) { ConsoleIO.WriteLogLine(Get(key), acceptnewlines); } #endregion } }