using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Modules.Utils; using MaxMind.Db; using Newtonsoft.Json; using System.Net; using System.Collections.Concurrent; using src.player; using src.player.skills; namespace src.utils { public static class Localization { private static readonly string languagesFolderPath = Path.Combine(jRandomSkills.Instance.ModuleDirectory, "languages"); private static readonly ConcurrentDictionary> _translations = []; private static readonly string playersLanguageFileName = "playersLanguage.json"; private static readonly string configsFolderPath = Path.Combine(jRandomSkills.Instance.ModuleDirectory, "configs"); private static ConcurrentDictionary _playersLanguage = []; private static readonly string geoliteFilePath = Path.Combine(jRandomSkills.Instance.ModuleDirectory, "packages", "GeoLite2-Country.mmdb"); private static string defaultLangCode = "en"; public static void Load() { _translations.Clear(); _playersLanguage.Clear(); SetLangCode(); LoadAllLanguages(); LoadPlayersLanguage(); } private static void SetLangCode() { defaultLangCode = Config.LoadedConfig.LanguageSystem.DefaultLangCode; } private static void LoadAllLanguages() { if (!Directory.Exists(languagesFolderPath)) return; foreach (var file in Directory.GetFiles(languagesFolderPath, "*.json")) LoadLanguage(file); } private static void LoadLanguage(string langPath) { if (!File.Exists(langPath)) return; var code = Path.GetFileNameWithoutExtension(langPath).ToLowerInvariant(); var jsonText = File.ReadAllText(langPath); var translations = JsonConvert.DeserializeObject>(jsonText); if (translations != null) { string redColor = ChatColors.Red.ToString(); string? altButton = Config.LoadedConfig.AlternativeSkillButton; foreach (var tkey in translations.Keys.ToList()) { var val = translations[tkey].Replace("CHATCOLORS.RED", redColor); if (!string.IsNullOrEmpty(altButton)) val = val.Replace("css_useSkill", $"css_useSkill/{altButton}"); translations[tkey] = val; } _translations.AddOrUpdate(code, translations, (k, v) => translations); } } public static bool HasTranslation(string code) { return _translations.ContainsKey(code.ToLowerInvariant()); } public static string GetSkillName(this CCSPlayerController player, Skills skill, float? chance = null) { string langCode = GetLangCode(player); if (chance == null) { var translation = GetTranslation(skill.ToString().ToLowerInvariant(), langCode); if (!translation.Contains("{0}")) return translation; if (!translation.Contains(' ')) return translation.Replace("{0}", "").Trim(); var parts = translation.Split(' ', StringSplitOptions.RemoveEmptyEntries); var filtered = parts.Where(p => !p.Contains("{0}")); return string.Join(' ', filtered); } var value = Math.Round((double)(chance ?? 1), 2); var skillNameText = GetTranslation(skill.ToString().ToLowerInvariant(), langCode, player, value); if (skillNameText.Contains('%')) skillNameText = skillNameText.Replace(value.ToString(), Math.Round(value * 100, 0).ToString()); return skillNameText; } public static string GetSkillDescription(this CCSPlayerController player, Skills skill, float? chance = null) { string langCode = GetLangCode(player); if (chance == null) { var translation = GetTranslation($"{skill.ToString().ToLowerInvariant()}_desc", langCode); if (!translation.Contains("{0}")) return translation; if (!translation.Contains(' ')) return translation.Replace("{0}", "").Trim(); var parts = translation.Split(' ', StringSplitOptions.RemoveEmptyEntries); var filtered = parts.Where(p => !p.Contains("{0}")); return string.Join(' ', filtered); } var skillName = $"{skill.ToString().ToLowerInvariant()}_desc2"; var value = Math.Round((double)(chance ?? 1), 2); var desc2 = GetTranslation(skillName, langCode, player, value); var skilLDescription = desc2 == skillName ? player.GetTranslation($"{skill.ToString().ToLowerInvariant()}_desc") : desc2.Contains('%') ? desc2.Replace(value.ToString(), Math.Round(value * 100, 0).ToString()) : desc2; return skilLDescription; } public static void PrintTranslationToChatAll(string message, string[]? key, params object[][]? args) { foreach (var player in Utilities.GetPlayers().Where(p => !p.IsBot)) { string langCode = GetLangCode(player); if (key == null) { player.PrintToChat(message); return; } ConcurrentBag translations = []; for (int i = 0; i < key.Length; i++) { object[] currentArgs = args != null && i < args.Length ? args[i] : []; string translation = GetTranslation(key[i], langCode, null, currentArgs); translations.Add(translation); } player.PrintToChat(string.Format(message, [.. translations])); } } public static string GetTranslation(this CCSPlayerController player, string key, params object[] args) { string langCode = GetLangCode(player); return GetTranslation(key, langCode, player, args); } public static string GetTranslation(string key, string? langCode = null, CCSPlayerController? player = null, params object[] args) { langCode ??= defaultLangCode; if (_translations.TryGetValue(langCode, out var langDict) && langDict.TryGetValue(key, out var translation)) if (args.Length != 0 && args[0].ToString() == "welcome") return translation; else { string output = args.Length == 0 ? translation : string.Format(translation, args); if (Illiterate.CheckIlliterateSkill(player)) return Illiterate.GetRandomText(output)!; return output; } if (langCode != defaultLangCode) return GetTranslation(key, null, player, args); return key; } // Resolves and caches the player's language at connect time, so the GeoLite // database lookup never runs on the tick path. public static void PreResolveLanguage(CCSPlayerController? player) { if (player == null || !player.IsValid || player.IsBot) return; GetLangCode(player); } private static bool _geoLiteBroken = false; private static string GetLangCode(CCSPlayerController? player) { if (player == null || !player.IsValid || player.IsBot) return defaultLangCode; string? fileLangCode = GetLangCodeFromFile(player.SteamID); if (!string.IsNullOrEmpty(fileLangCode)) return fileLangCode; if (Config.LoadedConfig.LanguageSystem.DisableGeoLite == true || _geoLiteBroken) return defaultLangCode; string? geoliteLandCode; try { geoliteLandCode = GetLangCodeFromDatabase(GetPlayerIP(player)) ?? defaultLangCode; } catch (Exception ex) { // MaxMind.Db can fail to load (e.g. hot-reload while the old AssemblyLoadContext is // unloading) or the .mmdb can be corrupt; fall back to the default language and stop // trying for the rest of the session instead of throwing every HUD tick. _geoLiteBroken = true; Server.PrintToConsole($"[jRandomSkills] GeoLite lookup disabled for this session: {ex.GetType().Name}: {ex.Message}"); geoliteLandCode = defaultLangCode; } ChangePlayerLanguage(player, geoliteLandCode); return geoliteLandCode; } private static string? GetPlayerIP(CCSPlayerController? player) { if (player == null) return null; var playerIP = player.IpAddress; if (playerIP == null) return null; string[] parts = playerIP.Split(':'); return parts.Length > 1 ? parts[0] : playerIP; } // NoInlining keeps the MaxMind.Db type references out of GetLangCode, so an assembly-load // failure surfaces inside the try/catch at the call site instead of when GetLangCode is JITed. [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] private static string? GetLangCodeFromDatabase(string? playerIP) { if (string.IsNullOrEmpty(playerIP)) return null; if (!File.Exists(geoliteFilePath)) return null; using var reader = new Reader(geoliteFilePath); if (!IPAddress.TryParse(playerIP, out var ip)) return null; var data = reader.Find>(ip); if (data == null || data.IsEmpty) return null; if (data.TryGetValue("country", out var _country) && _country is Dictionary country) if (country.TryGetValue("iso_code", out var _isoCode) && _isoCode is string isoCode) { string fileName = Config.LoadedConfig.LanguageSystem.DefaultLangCode; foreach (var langInfo in Config.LoadedConfig.LanguageSystem.LanguageInfos) if (langInfo.IsoCodes.Contains(isoCode)) fileName = langInfo.FileName; if (_translations.ContainsKey(fileName)) return fileName; } return null; } private static void LoadPlayersLanguage() { string filePath = Path.Combine(configsFolderPath, playersLanguageFileName); if (!File.Exists(filePath)) return; var jsonText = File.ReadAllText(filePath); _playersLanguage = JsonConvert.DeserializeObject>(jsonText) ?? []; } private static void SavePlayersLanguage() { Directory.CreateDirectory(configsFolderPath); string filePath = Path.Combine(configsFolderPath, playersLanguageFileName); var json = JsonConvert.SerializeObject(_playersLanguage); if (json != null) File.WriteAllText(filePath, json); } private static string? GetLangCodeFromFile(ulong? playerSteamID) { if (playerSteamID == null || playerSteamID == 0) return null; if (_playersLanguage.TryGetValue((ulong)playerSteamID, out var langCode)) return langCode; return null; } public static void ChangePlayerLanguage(CCSPlayerController? player, string language) { if (player == null || !player.IsValid) return; _playersLanguage.AddOrUpdate(player.SteamID, language, (k,v) => language); SavePlayersLanguage(); } } }