diff --git a/CS2-SimpleAdmin/Api/CS2_SimpleAdminApi.cs b/CS2-SimpleAdmin/Api/CS2_SimpleAdminApi.cs index 1ef8886..868539e 100644 --- a/CS2-SimpleAdmin/Api/CS2_SimpleAdminApi.cs +++ b/CS2-SimpleAdmin/Api/CS2_SimpleAdminApi.cs @@ -1,5 +1,6 @@ using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Commands; +using CounterStrikeSharp.API.Core.Translations; using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Commands.Targeting; using CounterStrikeSharp.API.Modules.Entities; @@ -205,6 +206,14 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi Menus.MenuManager.Instance.RegisterCategory(categoryId, categoryName, permission); } + public void RegisterMenuCategory(string categoryId, string categoryNameKey, string permission, object moduleLocalizer) + { + if (moduleLocalizer is not IStringLocalizer localizer) + throw new InvalidOperationException("moduleLocalizer must be an IStringLocalizer instance"); + + Menus.MenuManager.Instance.RegisterCategory(categoryId, categoryNameKey, permission, localizer); + } + public void RegisterMenu(string categoryId, string menuId, string menuName, Func menuFactory, string? permission = null, string? commandName = null) { @@ -263,6 +272,39 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi } } + public void RegisterMenu(string categoryId, string menuId, string menuNameKey, + Func menuFactory, string? permission, string? commandName, object moduleLocalizer) + { + if (moduleLocalizer is not IStringLocalizer localizer) + throw new InvalidOperationException("moduleLocalizer must be an IStringLocalizer instance"); + + Menus.MenuManager.Instance.RegisterMenu(categoryId, menuId, menuNameKey, BuilderFactory, permission, commandName, localizer); + return; + + MenuBuilder BuilderFactory(CCSPlayerController player) + { + var context = new MenuContext(categoryId, menuId, menuNameKey, permission, commandName); + + if (menuFactory(player, context) is not MenuBuilder menuBuilder) + throw new InvalidOperationException("Menu factory must return MenuBuilder"); + + // Dodaj automatyczną obsługę przycisku 'Wróć' + menuBuilder.WithBackAction(p => + { + if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(categoryId, out var category)) + { + Menus.MenuManager.Instance.CreateCategoryMenuPublic(category, p).OpenMenu(p); + } + else + { + Menus.MenuManager.Instance.OpenMainMenu(p); + } + }); + + return menuBuilder; + } + } + public void UnregisterMenu(string categoryId, string menuId) { @@ -289,7 +331,30 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi public object CreateMenuWithBack(MenuContext context, CCSPlayerController player) { - return CreateMenuWithBack(context.MenuTitle, context.CategoryId, player); + // Get translated title if module has localizer + string title = context.MenuTitle; + + if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(context.CategoryId, out var category)) + { + // Check if this specific menu has a localizer + if (category.MenuLocalizers.TryGetValue(context.MenuId, out var menuLocalizer)) + { + using (new WithTemporaryCulture(player.GetLanguage())) + { + title = menuLocalizer[context.MenuTitle] ?? context.MenuTitle; + } + } + // Fallback to category localizer + else if (category.ModuleLocalizer != null) + { + using (new WithTemporaryCulture(player.GetLanguage())) + { + title = category.ModuleLocalizer[context.MenuTitle] ?? context.MenuTitle; + } + } + } + + return CreateMenuWithBack(title, context.CategoryId, player); } public List GetValidPlayers() @@ -321,7 +386,30 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi public object CreateMenuWithPlayers(MenuContext context, CCSPlayerController admin, Func filter, Action onSelect) { - return CreateMenuWithPlayers(context.MenuTitle, context.CategoryId, admin, filter, onSelect); + // Get translated title if module has localizer + string title = context.MenuTitle; + + if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(context.CategoryId, out var category)) + { + // Check if this specific menu has a localizer + if (category.MenuLocalizers.TryGetValue(context.MenuId, out var menuLocalizer)) + { + using (new WithTemporaryCulture(admin.GetLanguage())) + { + title = menuLocalizer[context.MenuTitle] ?? context.MenuTitle; + } + } + // Fallback to category localizer + else if (category.ModuleLocalizer != null) + { + using (new WithTemporaryCulture(admin.GetLanguage())) + { + title = category.ModuleLocalizer[context.MenuTitle] ?? context.MenuTitle; + } + } + } + + return CreateMenuWithPlayers(title, context.CategoryId, admin, filter, onSelect); } public void AddMenuOption(object menu, string name, Action action, bool disabled = false, diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs index d193b3c..ddfab0f 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs @@ -22,7 +22,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig "CS2-SimpleAdmin" + (Helper.IsDebugBuild ? " (DEBUG)" : " (RELEASE)"); public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)"; public override string ModuleAuthor => "daffyy & Dliix66"; - public override string ModuleVersion => "1.7.8-beta-3"; + public override string ModuleVersion => "1.7.8-beta-4"; public override void Load(bool hotReload) { diff --git a/CS2-SimpleAdmin/Database/IDatabaseProvider.cs b/CS2-SimpleAdmin/Database/IDatabaseProvider.cs index 25bc129..02ff3e8 100644 --- a/CS2-SimpleAdmin/Database/IDatabaseProvider.cs +++ b/CS2-SimpleAdmin/Database/IDatabaseProvider.cs @@ -12,6 +12,9 @@ public interface IDatabaseProvider string GetBanSelectQuery(bool multiServer); string GetIpHistoryQuery(); string GetBanUpdateQuery(bool multiServer); + + // PlayerManager + string GetUpsertPlayerIpQuery(); // PermissionManager string GetAdminsQuery(); diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/009_BanAllUsedIpAddress.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/009_BanAllUsedIpAddress.sql index 33e57f2..e9ba400 100644 --- a/CS2-SimpleAdmin/Database/Migrations/Sqlite/009_BanAllUsedIpAddress.sql +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/009_BanAllUsedIpAddress.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS `sa_players_ips` ( `steamid` INTEGER NOT NULL, - `address` INTEGER NOT NULL + `address` INTEGER NOT NULL, `used_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`steamid`, `address`) ); \ No newline at end of file diff --git a/CS2-SimpleAdmin/Database/MysqlDatabaseProvider.cs b/CS2-SimpleAdmin/Database/MysqlDatabaseProvider.cs index 9e79d93..79f0ce2 100644 --- a/CS2-SimpleAdmin/Database/MysqlDatabaseProvider.cs +++ b/CS2-SimpleAdmin/Database/MysqlDatabaseProvider.cs @@ -86,6 +86,17 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider return "SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC"; } + public string GetUpsertPlayerIpQuery() + { + return """ + INSERT INTO `sa_players_ips` (steamid, name, address, used_at) + VALUES (@SteamID, @playerName, @IPAddress, CURRENT_TIMESTAMP) + ON DUPLICATE KEY UPDATE + used_at = CURRENT_TIMESTAMP, + name = @playerName; + """; + } + public string GetBanUpdateQuery(bool multiServer) { return multiServer ? """ diff --git a/CS2-SimpleAdmin/Database/SqliteDatabaseProvider.cs b/CS2-SimpleAdmin/Database/SqliteDatabaseProvider.cs index bd63d7b..93772be 100644 --- a/CS2-SimpleAdmin/Database/SqliteDatabaseProvider.cs +++ b/CS2-SimpleAdmin/Database/SqliteDatabaseProvider.cs @@ -83,6 +83,17 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider public string GetIpHistoryQuery() => "SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC"; + public string GetUpsertPlayerIpQuery() + { + return """ + INSERT INTO sa_players_ips (steamid, name, address, used_at) + VALUES (@SteamID, @playerName, @IPAddress, CURRENT_TIMESTAMP) + ON CONFLICT(steamid, address) DO UPDATE SET + used_at = CURRENT_TIMESTAMP, + name = @playerName; + """; + } + public string GetBanUpdateQuery(bool multiServer) => multiServer ? """ diff --git a/CS2-SimpleAdmin/Helper.cs b/CS2-SimpleAdmin/Helper.cs index 089e083..3b9ff2a 100644 --- a/CS2-SimpleAdmin/Helper.cs +++ b/CS2-SimpleAdmin/Helper.cs @@ -426,7 +426,7 @@ internal static class Helper var communityUrl = caller != null ? "<" + new SteamID(caller.SteamID).ToCommunityUrl() + ">" : ""; var callerName = caller != null ? caller.PlayerName : CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console"; - _ = CS2_SimpleAdmin.DiscordWebhookClientLog.SendMessageAsync(Helper.GenerateMessageDiscord(localizer["sa_discord_log_command", $"[{callerName}]({communityUrl})", command.GetCommandString])); + _ = CS2_SimpleAdmin.DiscordWebhookClientLog.SendMessageAsync(GenerateMessageDiscord(localizer["sa_discord_log_command", $"[{callerName}]({communityUrl})", command.GetCommandString])); } private static void SendDiscordLogMessage(CCSPlayerController? caller, string command, IStringLocalizer? localizer) @@ -1026,7 +1026,9 @@ public static class Time { public static DateTime ActualDateTime() { - return DateTime.UtcNow; + if (CS2_SimpleAdmin.Instance.Config.DatabaseConfig.DatabaseType.ToLower().Equals("sqlite")) + return DateTime.UtcNow; + string timezoneId = CS2_SimpleAdmin.Instance.Config.Timezone; DateTime utcNow = DateTime.UtcNow; diff --git a/CS2-SimpleAdmin/Managers/PlayerManager.cs b/CS2-SimpleAdmin/Managers/PlayerManager.cs index aff7930..cd28d1c 100644 --- a/CS2-SimpleAdmin/Managers/PlayerManager.cs +++ b/CS2-SimpleAdmin/Managers/PlayerManager.cs @@ -99,14 +99,8 @@ internal class PlayerManager var steamId64 = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64; var ipUint = IpHelper.IpToUint(ipAddress); - // MySQL: INSERT ... ON DUPLICATE KEY UPDATE pattern - const string upsertQuery = """ - INSERT INTO `sa_players_ips` (steamid, name, address, used_at) - VALUES (@SteamID, @playerName, @IPAddress, CURRENT_TIMESTAMP) - ON DUPLICATE KEY UPDATE - used_at = CURRENT_TIMESTAMP, - name = @playerName; - """; + // Use database-specific UPSERT query (handles MySQL vs SQLite syntax differences) + var upsertQuery = CS2_SimpleAdmin.DatabaseProvider.GetUpsertPlayerIpQuery(); await connection.ExecuteAsync(upsertQuery, new { diff --git a/CS2-SimpleAdmin/Menus/BasicMenu.cs b/CS2-SimpleAdmin/Menus/BasicMenu.cs index b0fba78..ec2fb6b 100644 --- a/CS2-SimpleAdmin/Menus/BasicMenu.cs +++ b/CS2-SimpleAdmin/Menus/BasicMenu.cs @@ -1,5 +1,6 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Core.Translations; using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Entities; using CounterStrikeSharp.API.Modules.Entities.Constants; @@ -12,32 +13,33 @@ public abstract class BasicMenu { /// /// Initializes all menus in the system by registering them with the MenuManager. + /// Register with translation keys instead of static names - translation happens per-player. /// public static void Initialize() { var manager = MenuManager.Instance; - // Players category menus - manager.RegisterMenu("players", "slap", "Slap Player", CreateSlapMenu, "@css/slay"); - manager.RegisterMenu("players", "slay", "Slay Player", CreateSlayMenu, "@css/slay"); - manager.RegisterMenu("players", "kick", "Kick Player", CreateKickMenu, "@css/kick"); - manager.RegisterMenu("players", "warn", "Warn Player", CreateWarnMenu, "@css/kick"); - manager.RegisterMenu("players", "ban", "Ban Player", CreateBanMenu, "@css/ban"); - manager.RegisterMenu("players", "gag", "Gag Player", CreateGagMenu, "@css/chat"); - manager.RegisterMenu("players", "mute", "Mute Player", CreateMuteMenu, "@css/chat"); - manager.RegisterMenu("players", "silence", "Silence Player", CreateSilenceMenu, "@css/chat"); - manager.RegisterMenu("players", "team", "Force Team", CreateForceTeamMenu, "@css/kick"); + // Players category menus - using translation keys + manager.RegisterMenu("players", "slap", "sa_slap", CreateSlapMenu, "@css/slay"); + manager.RegisterMenu("players", "slay", "sa_slay", CreateSlayMenu, "@css/slay"); + manager.RegisterMenu("players", "kick", "sa_kick", CreateKickMenu, "@css/kick"); + manager.RegisterMenu("players", "warn", "sa_warn", CreateWarnMenu, "@css/kick"); + manager.RegisterMenu("players", "ban", "sa_ban", CreateBanMenu, "@css/ban"); + manager.RegisterMenu("players", "gag", "sa_gag", CreateGagMenu, "@css/chat"); + manager.RegisterMenu("players", "mute", "sa_mute", CreateMuteMenu, "@css/chat"); + manager.RegisterMenu("players", "silence", "sa_silence", CreateSilenceMenu, "@css/chat"); + manager.RegisterMenu("players", "team", "sa_team_force", CreateForceTeamMenu, "@css/kick"); - // Server category menus - manager.RegisterMenu("server", "plugins", "Manage Plugins", CreatePluginsMenu, "@css/root"); - manager.RegisterMenu("server", "changemap", "Change Map", CreateChangeMapMenu, "@css/changemap"); - manager.RegisterMenu("server", "restart", "Restart Game", CreateRestartGameMenu, "@css/generic"); - manager.RegisterMenu("server", "custom", "Custom Commands", CreateCustomCommandsMenu, "@css/generic"); + // Server category menus - using translation keys + manager.RegisterMenu("server", "plugins", "sa_menu_pluginsmanager_title", CreatePluginsMenu, "@css/root"); + manager.RegisterMenu("server", "changemap", "sa_changemap", CreateChangeMapMenu, "@css/changemap"); + manager.RegisterMenu("server", "restart", "sa_restart_game", CreateRestartGameMenu, "@css/generic"); + manager.RegisterMenu("server", "custom", "sa_menu_custom_commands", CreateCustomCommandsMenu, "@css/generic"); - // Admin category menus - manager.RegisterMenu("admin", "add", "Add Admin", CreateAddAdminMenu, "@css/root"); - manager.RegisterMenu("admin", "remove", "Remove Admin", CreateRemoveAdminMenu, "@css/root"); - manager.RegisterMenu("admin", "reload", "Reload Admins", CreateReloadAdminsMenu, "@css/root"); + // Admin category menus - using translation keys + manager.RegisterMenu("admin", "add", "sa_admin_add", CreateAddAdminMenu, "@css/root"); + manager.RegisterMenu("admin", "remove", "sa_admin_remove", CreateRemoveAdminMenu, "@css/root"); + manager.RegisterMenu("admin", "reload", "sa_admin_reload", CreateReloadAdminsMenu, "@css/root"); } /// @@ -49,10 +51,10 @@ public abstract class BasicMenu private static MenuBuilder CreateSlapMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var slapMenu = new MenuBuilder(localizer?["sa_slap"] ?? "Slap Player"); + var slapMenu = new MenuBuilder("sa_slap", admin, localizer); var players = Helper.GetValidPlayers().Where(admin.CanTarget); - + foreach (var player in players) { var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; @@ -70,18 +72,25 @@ public abstract class BasicMenu /// A MenuBuilder instance for the slap damage menu. private static MenuBuilder CreateSlapDamageMenu(CCSPlayerController admin, CCSPlayerController target) { - var slapDamageMenu = new MenuBuilder($"Slap: {target.PlayerName}"); + var localizer = CS2_SimpleAdmin._localizer; + string localizedTitle; + using (new WithTemporaryCulture(admin.GetLanguage())) + { + localizedTitle = $"{localizer?["sa_slap"] ?? "Slap"}: {target.PlayerName}"; + } + + var slapDamageMenu = new MenuBuilder(localizedTitle); var damages = new[] { 0, 1, 5, 10, 50, 100 }; foreach (var damage in damages) { - slapDamageMenu.AddOption($"{damage} HP", _ => + slapDamageMenu.AddOption($"{damage} HP", currentAdmin => { if (target.IsValid) { - CS2_SimpleAdmin.Slap(admin, target, damage); - // Keep menu open for consecutive slaps - CreateSlapDamageMenu(admin, target).OpenMenu(admin); + CS2_SimpleAdmin.Slap(currentAdmin, target, damage); + // Reopen the same menu (not create new one) to keep back button working + slapDamageMenu.OpenMenu(currentAdmin); } }); } @@ -97,10 +106,10 @@ public abstract class BasicMenu private static MenuBuilder CreateSlayMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var slayMenu = new MenuBuilder(localizer?["sa_slay"] ?? "Slay Player"); + var slayMenu = new MenuBuilder("sa_slay", admin, localizer); var players = Helper.GetValidPlayers().Where(admin.CanTarget); - + foreach (var player in players) { var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; @@ -124,14 +133,14 @@ public abstract class BasicMenu private static MenuBuilder CreateKickMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var kickMenu = new MenuBuilder(localizer?["sa_kick"] ?? "Kick Player"); + var kickMenu = new MenuBuilder("sa_kick", admin, localizer); var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); - + foreach (var player in players) { var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; - kickMenu.AddSubMenu(playerName, () => CreateReasonMenu(admin, player, "Kick", PenaltyType.Kick, + kickMenu.AddSubMenu(playerName, () => CreateReasonMenu(admin, player, "Kick", PenaltyType.Kick, (_, _, reason) => { if (player.IsValid) @@ -152,10 +161,10 @@ public abstract class BasicMenu private static MenuBuilder CreateWarnMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var warnMenu = new MenuBuilder(localizer?["sa_warn"] ?? "Warn Player"); + var warnMenu = new MenuBuilder("sa_warn", admin, localizer); var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); - + foreach (var player in players) { var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; @@ -181,10 +190,10 @@ public abstract class BasicMenu private static MenuBuilder CreateBanMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var banMenu = new MenuBuilder(localizer?["sa_ban"] ?? "Ban Player"); + var banMenu = new MenuBuilder("sa_ban", admin, localizer); var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); - + foreach (var player in players) { var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; @@ -210,10 +219,10 @@ public abstract class BasicMenu private static MenuBuilder CreateGagMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var gagMenu = new MenuBuilder(localizer?["sa_gag"] ?? "Gag Player"); + var gagMenu = new MenuBuilder("sa_gag", admin, localizer); var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); - + foreach (var player in players) { var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; @@ -239,10 +248,10 @@ public abstract class BasicMenu private static MenuBuilder CreateMuteMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var muteMenu = new MenuBuilder(localizer?["sa_mute"] ?? "Mute Player"); + var muteMenu = new MenuBuilder("sa_mute", admin, localizer); var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); - + foreach (var player in players) { var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; @@ -268,10 +277,10 @@ public abstract class BasicMenu private static MenuBuilder CreateSilenceMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var silenceMenu = new MenuBuilder(localizer?["sa_silence"] ?? "Silence Player"); + var silenceMenu = new MenuBuilder("sa_silence", admin, localizer); var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); - + foreach (var player in players) { var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; @@ -297,10 +306,10 @@ public abstract class BasicMenu private static MenuBuilder CreateForceTeamMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var teamMenu = new MenuBuilder(localizer?["sa_team_force"] ?? "Force Team"); + var teamMenu = new MenuBuilder("sa_team_force", admin, localizer); var players = Helper.GetValidPlayers().Where(p => admin.CanTarget(p)); - + foreach (var player in players) { var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; @@ -319,14 +328,32 @@ public abstract class BasicMenu private static MenuBuilder CreateTeamSelectionMenu(CCSPlayerController admin, CCSPlayerController target) { var localizer = CS2_SimpleAdmin._localizer; - var teamSelectionMenu = new MenuBuilder($"Force Team: {target.PlayerName}"); + + // Localize title for admin's language + string localizedTitle; + using (new WithTemporaryCulture(admin.GetLanguage())) + { + localizedTitle = $"{localizer?["sa_team_force"] ?? "Force Team"}: {target.PlayerName}"; + } + + var teamSelectionMenu = new MenuBuilder(localizedTitle); + + // Localize team options for admin's language + string ctName, tName, swapName, specName; + using (new WithTemporaryCulture(admin.GetLanguage())) + { + ctName = localizer?["sa_team_ct"] ?? "CT"; + tName = localizer?["sa_team_t"] ?? "T"; + swapName = localizer?["sa_team_swap"] ?? "Swap"; + specName = localizer?["sa_team_spec"] ?? "Spec"; + } var teams = new[] { - (localizer?["sa_team_ct"] ?? "CT", "ct", CsTeam.CounterTerrorist), - (localizer?["sa_team_t"] ?? "T", "t", CsTeam.Terrorist), - (localizer?["sa_team_swap"] ?? "Swap", "swap", CsTeam.Spectator), - (localizer?["sa_team_spec"] ?? "Spec", "spec", CsTeam.Spectator) + (ctName, "ct", CsTeam.CounterTerrorist), + (tName, "t", CsTeam.Terrorist), + (swapName, "swap", CsTeam.Spectator), + (specName, "spec", CsTeam.Spectator) }; foreach (var (name, teamName, teamNum) in teams) @@ -351,7 +378,7 @@ public abstract class BasicMenu private static MenuBuilder CreatePluginsMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var pluginsMenu = new MenuBuilder(localizer?["sa_menu_pluginsmanager_title"] ?? "Manage Plugins"); + var pluginsMenu = new MenuBuilder("sa_menu_pluginsmanager_title", admin, localizer); pluginsMenu.AddOption("Open Plugins Manager", _ => { @@ -369,7 +396,7 @@ public abstract class BasicMenu private static MenuBuilder CreateChangeMapMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var mapMenu = new MenuBuilder(localizer?["sa_changemap"] ?? "Change Map"); + var mapMenu = new MenuBuilder("sa_changemap", admin, localizer); // Add default maps var maps = CS2_SimpleAdmin.Instance.Config.DefaultMaps; @@ -402,7 +429,7 @@ public abstract class BasicMenu private static MenuBuilder CreateRestartGameMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var restartMenu = new MenuBuilder(localizer?["sa_restart_game"] ?? "Restart Game"); + var restartMenu = new MenuBuilder("sa_restart_game", admin, localizer); restartMenu.AddOption("Restart Round", _ => { @@ -420,7 +447,7 @@ public abstract class BasicMenu private static MenuBuilder CreateCustomCommandsMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var customMenu = new MenuBuilder(localizer?["sa_menu_custom_commands"] ?? "Custom Commands"); + var customMenu = new MenuBuilder("sa_menu_custom_commands", admin, localizer); var customCommands = CS2_SimpleAdmin.Instance.Config.CustomServerCommands; @@ -455,10 +482,10 @@ public abstract class BasicMenu private static MenuBuilder CreateAddAdminMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var addAdminMenu = new MenuBuilder(localizer?["sa_admin_add"] ?? "Add Admin"); + var addAdminMenu = new MenuBuilder("sa_admin_add", admin, localizer); var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); - + foreach (var player in players) { var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; @@ -476,7 +503,16 @@ public abstract class BasicMenu /// A MenuBuilder instance for the admin flags menu. private static MenuBuilder CreateAdminFlagsMenu(CCSPlayerController admin, CCSPlayerController target) { - var flagsMenu = new MenuBuilder($"Add Admin: {target.PlayerName}"); + var localizer = CS2_SimpleAdmin._localizer; + + // Localize title for admin's language + string localizedTitle; + using (new WithTemporaryCulture(admin.GetLanguage())) + { + localizedTitle = $"{localizer?["sa_admin_add"] ?? "Add Admin"}: {target.PlayerName}"; + } + + var flagsMenu = new MenuBuilder(localizedTitle); foreach (var adminFlag in CS2_SimpleAdmin.Instance.Config.MenuConfigs.AdminFlags) { @@ -501,7 +537,7 @@ public abstract class BasicMenu private static MenuBuilder CreateRemoveAdminMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var removeAdminMenu = new MenuBuilder(localizer?["sa_admin_remove"] ?? "Remove Admin"); + var removeAdminMenu = new MenuBuilder("sa_admin_remove", admin, localizer); var adminPlayers = Helper.GetValidPlayers().Where(p => AdminManager.GetPlayerAdminData(p)?.Flags.Count > 0 && @@ -531,7 +567,7 @@ public abstract class BasicMenu private static MenuBuilder CreateReloadAdminsMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var reloadMenu = new MenuBuilder(localizer?["sa_admin_reload"] ?? "Reload Admins"); + var reloadMenu = new MenuBuilder("sa_admin_reload", admin, localizer); reloadMenu.AddOption("Reload Admins", _ => { @@ -546,13 +582,35 @@ public abstract class BasicMenu /// /// The admin player selecting duration. /// The target player for the penalty. - /// The name of the penalty action. + /// The name of the penalty action (e.g., "Kick", "Ban"). /// Callback action executed when duration is selected. /// A MenuBuilder instance for the duration menu. private static MenuBuilder CreateDurationMenu(CCSPlayerController admin, CCSPlayerController player, string actionName, Action onSelectAction) { - var durationMenu = new MenuBuilder($"{actionName} Duration: {player.PlayerName}"); + var localizer = CS2_SimpleAdmin._localizer; + + // Convert action name to translation key (e.g., "Ban" -> "sa_ban") + var actionKey = actionName.ToLower() switch + { + "kick" => "sa_kick", + "ban" => "sa_ban", + "warn" => "sa_warn", + "gag" => "sa_gag", + "mute" => "sa_mute", + "silence" => "sa_silence", + _ => actionName + }; + + // Localize title for admin's language + string localizedAction, durationText; + using (new WithTemporaryCulture(admin.GetLanguage())) + { + localizedAction = localizer?[actionKey] ?? actionName; + durationText = localizer?["sa_duration"] ?? "Duration"; + } + + var durationMenu = new MenuBuilder($"{localizedAction} {durationText}: {player.PlayerName}"); foreach (var durationItem in CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations) { @@ -570,14 +628,36 @@ public abstract class BasicMenu /// /// The admin player selecting reason. /// The target player for the penalty. - /// The name of the penalty action. + /// The name of the penalty action (e.g., "Kick", "Ban"). /// The type of penalty to determine which reason list to use. /// Callback action executed when reason is selected. /// A MenuBuilder instance for the reason menu. private static MenuBuilder CreateReasonMenu(CCSPlayerController admin, CCSPlayerController player, string actionName, PenaltyType penaltyType, Action onSelectAction) { - var reasonMenu = new MenuBuilder($"{actionName} Reason: {player.PlayerName}"); + var localizer = CS2_SimpleAdmin._localizer; + + // Convert action name to translation key + var actionKey = actionName.ToLower() switch + { + "kick" => "sa_kick", + "ban" => "sa_ban", + "warn" => "sa_warn", + "gag" => "sa_gag", + "mute" => "sa_mute", + "silence" => "sa_silence", + _ => actionName + }; + + // Localize title for admin's language + string localizedAction, reasonText; + using (new WithTemporaryCulture(admin.GetLanguage())) + { + localizedAction = localizer?[actionKey] ?? actionName; + reasonText = localizer?["sa_reason"] ?? "Reason"; + } + + var reasonMenu = new MenuBuilder($"{localizedAction} {reasonText}: {player.PlayerName}"); var reasons = penaltyType switch { diff --git a/CS2-SimpleAdmin/Menus/MenuBuilder.cs b/CS2-SimpleAdmin/Menus/MenuBuilder.cs index 70655c3..1c8ed3c 100644 --- a/CS2-SimpleAdmin/Menus/MenuBuilder.cs +++ b/CS2-SimpleAdmin/Menus/MenuBuilder.cs @@ -1,28 +1,89 @@ using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Core.Translations; +using Microsoft.Extensions.Localization; namespace CS2_SimpleAdmin.Menus; -public class MenuBuilder(string title) +public class MenuBuilder { + private readonly string _title; + private readonly CCSPlayerController? _player; + private readonly IStringLocalizer? _localizer; private readonly List _options = []; private MenuBuilder? _parentMenu; private Action? _backAction; private Action? _resetAction; + /// + /// Constructor for player-localized menu with translation key + /// + public MenuBuilder(string titleKey, CCSPlayerController player, IStringLocalizer? localizer = null) + { + _title = titleKey; + _player = player; + _localizer = localizer ?? CS2_SimpleAdmin._localizer; + } + + /// + /// Constructor for static title (backward compatibility) + /// + public MenuBuilder(string title) + { + _title = title; + _player = null; + _localizer = null; + } + + /// + /// Gets the localized title for the player + /// + private string GetLocalizedTitle() + { + if (_player != null && _localizer != null) + { + using (new WithTemporaryCulture(_player.GetLanguage())) + { + return _localizer[_title]; + } + } + return _title; + } + /// /// Adds a menu option with an action. /// - public MenuBuilder AddOption(string name, Action action, bool disabled = false, string? permission = null) + /// Display name or translation key + /// Action to perform when selected + /// Whether the option is disabled + /// Required permission + /// If true, name is a translation key to be localized + public MenuBuilder AddOption(string name, Action action, bool disabled = false, string? permission = null, bool isTranslationKey = false) { _options.Add(new MenuOption { Name = name, Action = action, Disabled = disabled, - Permission = permission + Permission = permission, + IsTranslationKey = isTranslationKey }); return this; } + /// + /// Gets the localized name for a menu option + /// + private string GetLocalizedOptionName(MenuOption option) + { + if (option.IsTranslationKey && _player != null && _localizer != null) + { + using (new WithTemporaryCulture(_player.GetLanguage())) + { + return _localizer[option.Name]; + } + } + return option.Name; + } + /// /// Adds a menu option that opens a submenu. /// @@ -99,8 +160,11 @@ public class MenuBuilder(string title) { if (!player.IsValid) return; + // Get localized title + var localizedTitle = GetLocalizedTitle(); + // Use MenuManager dependency - var menu = Helper.CreateMenu(title, _backAction); + var menu = Helper.CreateMenu(localizedTitle, _backAction); if (menu == null) return; foreach (var option in _options) @@ -115,7 +179,10 @@ public class MenuBuilder(string title) } } - menu.AddMenuOption(option.Name, (menuPlayer, menuOption) => + // Get localized option name + var localizedName = GetLocalizedOptionName(option); + + menu.AddMenuOption(localizedName, (menuPlayer, menuOption) => { option.Action?.Invoke(menuPlayer); }, option.Disabled); @@ -166,5 +233,6 @@ public class MenuOption public Action? Action { get; set; } public bool Disabled { get; set; } public string? Permission { get; set; } + public bool IsTranslationKey { get; set; } } \ No newline at end of file diff --git a/CS2-SimpleAdmin/Menus/MenuManager.cs b/CS2-SimpleAdmin/Menus/MenuManager.cs index f1aa450..6abf3ea 100644 --- a/CS2-SimpleAdmin/Menus/MenuManager.cs +++ b/CS2-SimpleAdmin/Menus/MenuManager.cs @@ -1,4 +1,5 @@ using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Core.Translations; using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Entities; @@ -38,6 +39,26 @@ public class MenuManager }; } + /// + /// Registers a new menu category with per-player localization support for modules. + /// 🆕 NEW: Enables modules to provide localized category names based on each player's css_lang! + /// + /// Unique identifier for the category. + /// Translation key from module's lang files. + /// Required permission to access this category. + /// Module's IStringLocalizer for per-player translation. + public void RegisterCategory(string categoryId, string categoryNameKey, string permission, Microsoft.Extensions.Localization.IStringLocalizer moduleLocalizer) + { + _menuCategories[categoryId] = new MenuCategory + { + Id = categoryId, + Name = categoryNameKey, // Store the key, not translated text + Permission = permission, + MenuFactories = new Dictionary>(), + ModuleLocalizer = moduleLocalizer // Store module's localizer + }; + } + /// /// Registers a menu within a category (API for other plugins). /// @@ -66,6 +87,37 @@ public class MenuManager } } + /// + /// Registers a menu with per-player localization support for modules. + /// 🆕 NEW: Enables modules to provide localized menu names based on each player's css_lang! + /// + /// The category to add this menu to. + /// Unique identifier for the menu. + /// Translation key from module's lang files. + /// Factory function that creates the menu for a player. + /// Required permission to access this menu (optional). + /// Command name for permission override checking (optional). + /// Module's IStringLocalizer for per-player translation. + public void RegisterMenu(string categoryId, string menuId, string menuNameKey, Func menuFactory, string? permission, string? commandName, Microsoft.Extensions.Localization.IStringLocalizer moduleLocalizer) + { + if (!_menuCategories.ContainsKey(categoryId)) + { + RegisterCategory(categoryId, categoryId); // Auto-create category if it doesn't exist + } + + _menuCategories[categoryId].MenuFactories[menuId] = menuFactory; + _menuCategories[categoryId].MenuNames[menuId] = menuNameKey; // Store the key + _menuCategories[categoryId].MenuLocalizers[menuId] = moduleLocalizer; // Store localizer + if (permission != null) + { + _menuCategories[categoryId].MenuPermissions[menuId] = permission; + } + if (commandName != null) + { + _menuCategories[categoryId].MenuCommandNames[menuId] = commandName; + } + } + /// /// Unregisters a menu from a category. /// @@ -88,7 +140,7 @@ public class MenuManager public MenuBuilder CreateMainMenu(CCSPlayerController player) { var localizer = CS2_SimpleAdmin._localizer; - var mainMenu = new MenuBuilder(localizer?["sa_title"] ?? "SimpleAdmin"); + var mainMenu = new MenuBuilder("sa_title", player, localizer); foreach (var category in _menuCategories.Values) { @@ -98,8 +150,23 @@ public class MenuManager if (!AdminManager.PlayerHasPermissions(steamId, category.Permission)) continue; + // Get localized category name for this player + // If category has a module localizer, use it; otherwise use main plugin localizer + string localizedCategoryName; + using (new WithTemporaryCulture(player.GetLanguage())) + { + if (category.ModuleLocalizer != null) + { + localizedCategoryName = category.ModuleLocalizer[category.Name] ?? category.Name; + } + else + { + localizedCategoryName = localizer?[category.Name] ?? category.Name; + } + } + // Pass player to CreateCategoryMenu - mainMenu.AddSubMenu(category.Name, () => CreateCategoryMenu(category, player), + mainMenu.AddSubMenu(localizedCategoryName, () => CreateCategoryMenu(category, player), permission: category.Permission); } @@ -114,7 +181,24 @@ public class MenuManager /// A MenuBuilder instance for the category menu. private MenuBuilder CreateCategoryMenu(MenuCategory category, CCSPlayerController player) { - var categoryMenu = new MenuBuilder(category.Name); + var localizer = CS2_SimpleAdmin._localizer; + + // Get localized category name for this player + // If category has a module localizer, use it; otherwise use main plugin localizer + string localizedCategoryName; + using (new WithTemporaryCulture(player.GetLanguage())) + { + if (category.ModuleLocalizer != null) + { + localizedCategoryName = category.ModuleLocalizer[category.Name] ?? category.Name; + } + else + { + localizedCategoryName = localizer?[category.Name] ?? category.Name; + } + } + + var categoryMenu = new MenuBuilder(localizedCategoryName); foreach (var kvp in category.MenuFactories) { @@ -159,8 +243,30 @@ public class MenuManager continue; } + // Get localized menu name for this player + // If menu has its own localizer, use it; otherwise use category or main plugin localizer + string localizedMenuName; + using (new WithTemporaryCulture(player.GetLanguage())) + { + if (category.MenuLocalizers.TryGetValue(menuId, out var menuLocalizer)) + { + // Menu has its own module localizer + localizedMenuName = menuLocalizer[menuName] ?? menuName; + } + else if (category.ModuleLocalizer != null) + { + // Use category's module localizer + localizedMenuName = category.ModuleLocalizer[menuName] ?? menuName; + } + else + { + // Use main plugin localizer + localizedMenuName = localizer?[menuName] ?? menuName; + } + } + // Call the actual factory with player parameter - categoryMenu.AddSubMenu(menuName, () => menuFactory(player), permission: permission); + categoryMenu.AddSubMenu(localizedMenuName, () => menuFactory(player), permission: permission); } return categoryMenu.WithBackButton(); @@ -190,12 +296,12 @@ public class MenuManager /// public void InitializeDefaultCategories() { - var localizer = CS2_SimpleAdmin._localizer; - - RegisterCategory("players", localizer?["sa_menu_players_manage"] ?? "Manage Players", "@css/generic"); - RegisterCategory("server", localizer?["sa_menu_server_manage"] ?? "Server Management", "@css/generic"); - // RegisterCategory("fun", localizer?["sa_menu_fun_commands"] ?? "Fun Commands", "@css/generic"); - RegisterCategory("admin", localizer?["sa_menu_admins_manage"] ?? "Admin Management", "@css/root"); + // Register categories with translation keys instead of translated names + // The actual translation will happen per-player in CreateMainMenu/CreateCategoryMenu + RegisterCategory("players", "sa_menu_players_manage", "@css/generic"); + RegisterCategory("server", "sa_menu_server_manage", "@css/generic"); + // RegisterCategory("fun", "sa_menu_fun_commands", "@css/generic"); + RegisterCategory("admin", "sa_menu_admins_manage", "@css/root"); } /// @@ -222,4 +328,17 @@ public class MenuCategory public Dictionary MenuNames { get; set; } = []; public Dictionary MenuPermissions { get; set; } = []; public Dictionary MenuCommandNames { get; set; } = []; + + // 🆕 NEW: Support for per-player localization in modules + /// + /// Optional IStringLocalizer from external module for per-player translation of category name. + /// If null, Name is used as-is (for CS2-SimpleAdmin's built-in categories with translation keys). + /// + public Microsoft.Extensions.Localization.IStringLocalizer? ModuleLocalizer { get; set; } + + /// + /// Stores IStringLocalizer for each menu that uses module localization. + /// Key: menuId, Value: module's localizer + /// + public Dictionary MenuLocalizers { get; set; } = []; } diff --git a/CS2-SimpleAdmin/VERSION b/CS2-SimpleAdmin/VERSION index 114eb98..66084e8 100644 --- a/CS2-SimpleAdmin/VERSION +++ b/CS2-SimpleAdmin/VERSION @@ -1 +1 @@ -1.7.8-beta-3 \ No newline at end of file +1.7.8-beta-4 \ No newline at end of file diff --git a/CS2-SimpleAdmin/lang/en.json b/CS2-SimpleAdmin/lang/en.json index a768d22..f875b82 100644 --- a/CS2-SimpleAdmin/lang/en.json +++ b/CS2-SimpleAdmin/lang/en.json @@ -31,7 +31,7 @@ "sa_team_spec": "Spec", "sa_slap": "Slap", - "sa_slay": "slay", + "sa_slay": "Slay", "sa_kick": "Kick", "sa_ban": "Ban", "sa_gag": "Gag", diff --git a/CS2-SimpleAdminApi/ICS2-SimpleAdminApi.cs b/CS2-SimpleAdminApi/ICS2-SimpleAdminApi.cs index eddfde5..65de92a 100644 --- a/CS2-SimpleAdminApi/ICS2-SimpleAdminApi.cs +++ b/CS2-SimpleAdminApi/ICS2-SimpleAdminApi.cs @@ -140,6 +140,16 @@ public interface ICS2_SimpleAdminApi /// void RegisterMenuCategory(string categoryId, string categoryName, string permission = "@css/generic"); + /// + /// Registers a menu category with per-player localization support for modules. + /// 🆕 NEW: Supports per-player localization using module's IStringLocalizer! + /// + /// The category ID (unique identifier). + /// Translation key from module's lang files. + /// Required permission to access this category. + /// Module's IStringLocalizer for per-player translation. + void RegisterMenuCategory(string categoryId, string categoryNameKey, string permission, object moduleLocalizer); + /// /// Registers a menu in a category. /// @@ -163,6 +173,19 @@ public interface ICS2_SimpleAdminApi /// Command name for permission override checking (optional, e.g., "css_god"). void RegisterMenu(string categoryId, string menuId, string menuName, Func menuFactory, string? permission = null, string? commandName = null); + /// + /// Registers a menu with per-player localization support for modules. + /// 🆕 NEW: Supports per-player localization using module's IStringLocalizer! + /// + /// The category to add this menu to. + /// Unique identifier for the menu. + /// Translation key from module's lang files. + /// Factory function that receives player and menu context. + /// Required permission to access this menu (optional). + /// Command name for permission override checking (optional). + /// Module's IStringLocalizer for per-player translation. + void RegisterMenu(string categoryId, string menuId, string menuNameKey, Func menuFactory, string? permission, string? commandName, object moduleLocalizer); + /// /// Unregisters a menu from a category. /// diff --git a/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdminApi.dll b/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdminApi.dll index b6fa68c..e8e0151 100644 Binary files a/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdminApi.dll and b/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdminApi.dll differ diff --git a/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdmin_ExampleModule.cs b/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdmin_ExampleModule.cs index d089358..720b4ad 100644 --- a/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdmin_ExampleModule.cs +++ b/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdmin_ExampleModule.cs @@ -145,20 +145,38 @@ public class CS2_SimpleAdmin_ExampleModule: BasePlugin // STEP 1: Register a menu category // This creates a new section in the main admin menu // Permission: @css/generic means all admins can see it + // + // ⚠️ LOCALIZATION OPTIONS: + // + // OPTION A - No translations (hard-coded text): _sharedApi.RegisterMenuCategory( "example", // Category ID (unique identifier) - "Example Features", // Display name in admin menu + "Example Features", // Display name (hard-coded, same for all players) "@css/generic" // Required permission ); + // + // OPTION B - With per-player translations (🆕 NEW!): + // If your module has lang/ folder with translations, use this pattern: + // _sharedApi.RegisterMenuCategory( + // "example", // Category ID + // "example_category_name", // Translation key + // "@css/generic", // Permission + // Localizer! // Module's localizer + // ); + // This will translate the category name per-player based on their css_lang setting! // STEP 2: Register individual menu items in the category // 🆕 NEW: These use MenuContext API - factory receives (admin, context) parameters + // + // ⚠️ LOCALIZATION OPTIONS: + // + // OPTION A - No translations (hard-coded text): // Example 1: Simple menu with options _sharedApi.RegisterMenu( "example", // Category ID "simple_action", // Menu ID (unique within category) - "Simple Actions", // Display name + "Simple Actions", // Display name (hard-coded) CreateSimpleActionMenu, // Factory method "@css/generic" // Required permission ); @@ -167,7 +185,7 @@ public class CS2_SimpleAdmin_ExampleModule: BasePlugin _sharedApi.RegisterMenu( "example", "player_selection", - "Select Player", + "Select Player", // Display name CreatePlayerSelectionMenu, "@css/kick" // Requires kick permission ); @@ -176,7 +194,7 @@ public class CS2_SimpleAdmin_ExampleModule: BasePlugin _sharedApi.RegisterMenu( "example", "nested_menu", - "Give Credits", + "Give Credits", // Display name CreateGiveCreditsMenu, "@css/generic" ); @@ -185,12 +203,26 @@ public class CS2_SimpleAdmin_ExampleModule: BasePlugin _sharedApi.RegisterMenu( "example", "test_command", - "Test Command", + "Test Command", // Display name CreateTestCommandMenu, "@css/root", // Default permission "css_test" // Command name for override checking ); + // OPTION B - With per-player translations (🆕 NEW!): + // If your module has lang/ folder, use this pattern: + // _sharedApi.RegisterMenu( + // "example", // Category ID + // "menu_id", // Menu ID + // "menu_translation_key", // Translation key (NOT translated text!) + // CreateYourMenu, // Factory method + // "@css/generic", // Permission + // "css_command", // Command name (optional) + // Localizer! // Module's localizer + // ); + // This will translate the menu name per-player based on their css_lang! + // See FunCommands module for real example. + _menusRegistered = true; Logger.LogInformation("Example menus registered successfully!"); } diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdminApi.dll b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdminApi.dll index b6fa68c..939cd65 100644 Binary files a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdminApi.dll and b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdminApi.dll differ diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.cs b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.cs index 729ab08..d7cc180 100644 --- a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.cs +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.cs @@ -366,66 +366,73 @@ public partial class CS2_SimpleAdmin_FunCommands : BasePlugin, IPluginConfig 0) _sharedApi.RegisterMenu("fun", "god", - Localizer?["fun_menu_god"] ?? "God Mode", - CreateGodModeMenu, "@css/cheats", "css_god"); + "fun_menu_god", + CreateGodModeMenu, "@css/cheats", "css_god", Localizer!); if (Config.NoclipCommands.Count > 0) _sharedApi.RegisterMenu("fun", "noclip", - Localizer?["fun_menu_noclip"] ?? "No Clip", - CreateNoClipMenu, "@css/cheats", "css_noclip"); + "fun_menu_noclip", + CreateNoClipMenu, "@css/cheats", "css_noclip", Localizer!); if (Config.RespawnCommands.Count > 0) _sharedApi.RegisterMenu("fun", "respawn", - Localizer?["fun_menu_respawn"] ?? "Respawn", - CreateRespawnMenu, "@css/cheats", "css_respawn"); + "fun_menu_respawn", + CreateRespawnMenu, "@css/cheats", "css_respawn", Localizer!); if (Config.GiveCommands.Count > 0) _sharedApi.RegisterMenu("fun", "give", - Localizer?["fun_menu_give"] ?? "Give Weapon", - CreateGiveWeaponMenu, "@css/cheats", "css_give"); + "fun_menu_give", + CreateGiveWeaponMenu, "@css/cheats", "css_give", Localizer!); if (Config.StripCommands.Count > 0) _sharedApi.RegisterMenu("fun", "strip", - Localizer?["fun_menu_strip"] ?? "Strip Weapons", - CreateStripWeaponsMenu, "@css/slay", "css_strip"); + "fun_menu_strip", + CreateStripWeaponsMenu, "@css/slay", "css_strip", Localizer!); if (Config.FreezeCommands.Count > 0) _sharedApi.RegisterMenu("fun", "freeze", - Localizer?["fun_menu_freeze"] ?? "Freeze", - CreateFreezeMenu, "@css/slay", "css_freeze"); + "fun_menu_freeze", + CreateFreezeMenu, "@css/slay", "css_freeze", Localizer!); if (Config.HpCommands.Count > 0) _sharedApi.RegisterMenu("fun", "hp", - Localizer?["fun_menu_hp"] ?? "Set HP", - CreateSetHpMenu, "@css/slay", "css_hp"); + "fun_menu_hp", + CreateSetHpMenu, "@css/slay", "css_hp", Localizer!); if (Config.SpeedCommands.Count > 0) _sharedApi.RegisterMenu("fun", "speed", - Localizer?["fun_menu_speed"] ?? "Set Speed", - CreateSetSpeedMenu, "@css/slay", "css_speed"); + "fun_menu_speed", + CreateSetSpeedMenu, "@css/slay", "css_speed", Localizer!); if (Config.GravityCommands.Count > 0) _sharedApi.RegisterMenu("fun", "gravity", - Localizer?["fun_menu_gravity"] ?? "Set Gravity", - CreateSetGravityMenu, "@css/slay", "css_gravity"); + "fun_menu_gravity", + CreateSetGravityMenu, "@css/slay", "css_gravity", Localizer!); if (Config.MoneyCommands.Count > 0) _sharedApi.RegisterMenu("fun", "money", - Localizer?["fun_menu_money"] ?? "Set Money", - CreateSetMoneyMenu, "@css/slay", "css_money"); + "fun_menu_money", + CreateSetMoneyMenu, "@css/slay", "css_money", Localizer!); if (Config.ResizeCommands.Count > 0) _sharedApi.RegisterMenu("fun", "resize", - Localizer?["fun_menu_resize"] ?? "Resize Player", - CreateSetResizeMenu, "@css/slay", "css_resize"); + "fun_menu_resize", + CreateSetResizeMenu, "@css/slay", "css_resize", Localizer!); _menusRegistered = true; Logger.LogInformation("Fun menus registered successfully!"); diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Menus.cs b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Menus.cs index a492787..ea8d90e 100644 --- a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Menus.cs +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Menus.cs @@ -1,6 +1,7 @@ using System.Globalization; using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Core.Translations; namespace CS2_SimpleAdmin_FunCommands; @@ -139,8 +140,15 @@ public partial class CS2_SimpleAdmin_FunCommands /// private object CreateWeaponSelectionMenu(CCSPlayerController admin, CCSPlayerController target) { + // Translate title per-player based on admin's css_lang + string translatedTitle; + using (new WithTemporaryCulture(admin.GetLanguage())) + { + translatedTitle = Localizer?["fun_menu_give_player", target.PlayerName] ?? $"Give Weapon: {target.PlayerName}"; + } + var weaponMenu = _sharedApi!.CreateMenuWithBack( - Localizer?["fun_menu_give_player", target.PlayerName] ?? $"Give Weapon: {target.PlayerName}", + translatedTitle, "fun", admin); @@ -212,8 +220,15 @@ public partial class CS2_SimpleAdmin_FunCommands /// private object CreateHpSelectionMenu(CCSPlayerController admin, CCSPlayerController target) { + // Translate title per-player based on admin's css_lang + string translatedTitle; + using (new WithTemporaryCulture(admin.GetLanguage())) + { + translatedTitle = Localizer?["fun_menu_hp_player", target.PlayerName] ?? $"Set HP: {target.PlayerName}"; + } + var hpSelectionMenu = _sharedApi!.CreateMenuWithBack( - Localizer?["fun_menu_hp_player", target.PlayerName] ?? $"Set HP: {target.PlayerName}", + translatedTitle, "fun", admin); @@ -222,8 +237,15 @@ public partial class CS2_SimpleAdmin_FunCommands foreach (var hp in hpValues) { + // Translate option label per-player + string optionLabel; + using (new WithTemporaryCulture(admin.GetLanguage())) + { + optionLabel = Localizer?["fun_menu_hp_value", hp] ?? $"{hp} HP"; + } + _sharedApi.AddMenuOption(hpSelectionMenu, - Localizer?["fun_menu_hp_value", hp] ?? $"{hp} HP", + optionLabel, _ => { if (target.IsValid) @@ -261,8 +283,15 @@ public partial class CS2_SimpleAdmin_FunCommands /// private object CreateSpeedSelectionMenu(CCSPlayerController admin, CCSPlayerController target) { + // Translate title per-player based on admin's css_lang + string translatedTitle; + using (new WithTemporaryCulture(admin.GetLanguage())) + { + translatedTitle = Localizer?["fun_menu_speed_player", target.PlayerName] ?? $"Set Speed: {target.PlayerName}"; + } + var speedSelectionMenu = _sharedApi!.CreateMenuWithBack( - Localizer?["fun_menu_speed_player", target.PlayerName] ?? $"Set Speed: {target.PlayerName}", + translatedTitle, "fun", admin); @@ -275,8 +304,15 @@ public partial class CS2_SimpleAdmin_FunCommands foreach (var (speed, display) in speedValues) { + // Translate option label per-player + string optionLabel; + using (new WithTemporaryCulture(admin.GetLanguage())) + { + optionLabel = Localizer?["fun_menu_speed_value", display] ?? $"Speed {display}"; + } + _sharedApi.AddMenuOption(speedSelectionMenu, - Localizer?["fun_menu_speed_value", display] ?? $"Speed {display}", + optionLabel, _ => { if (target.IsValid) @@ -316,8 +352,15 @@ public partial class CS2_SimpleAdmin_FunCommands private object CreateGravitySelectionMenu(CCSPlayerController admin, CCSPlayerController target) { + // Translate title per-player based on admin's css_lang + string translatedTitle; + using (new WithTemporaryCulture(admin.GetLanguage())) + { + translatedTitle = Localizer?["fun_menu_gravity_player", target.PlayerName] ?? $"Set Gravity: {target.PlayerName}"; + } + var gravitySelectionMenu = _sharedApi!.CreateMenuWithBack( - Localizer?["fun_menu_gravity_player", target.PlayerName] ?? $"Set Gravity: {target.PlayerName}", + translatedTitle, "fun", admin); var gravityValues = new[] @@ -365,8 +408,15 @@ public partial class CS2_SimpleAdmin_FunCommands private object CreateMoneySelectionMenu(CCSPlayerController admin, CCSPlayerController target) { + // Translate title per-player based on admin's css_lang + string translatedTitle; + using (new WithTemporaryCulture(admin.GetLanguage())) + { + translatedTitle = Localizer?["fun_menu_money_player", target.PlayerName] ?? $"Set Money: {target.PlayerName}"; + } + var moneySelectionMenu = _sharedApi!.CreateMenuWithBack( - Localizer?["fun_menu_money_player", target.PlayerName] ?? $"Set Money: {target.PlayerName}", + translatedTitle, "fun", admin); var moneyValues = new[] { 0, 1000, 2500, 5000, 10000, 16000 }; @@ -407,8 +457,15 @@ public partial class CS2_SimpleAdmin_FunCommands private object CreateResizeSelectionMenu(CCSPlayerController admin, CCSPlayerController target) { + // Translate title per-player based on admin's css_lang + string translatedTitle; + using (new WithTemporaryCulture(admin.GetLanguage())) + { + translatedTitle = Localizer?["fun_menu_resize_player", target.PlayerName] ?? $"Resize: {target.PlayerName}"; + } + var resizeSelectionMenu = _sharedApi!.CreateMenuWithBack( - Localizer?["fun_menu_resize_player", target.PlayerName] ?? $"Resize: {target.PlayerName}", + translatedTitle, "fun", admin); var resizeValues = new[] diff --git a/Modules/MODULE_DEVELOPMENT.md b/Modules/MODULE_DEVELOPMENT.md index cba01f0..4dee295 100644 --- a/Modules/MODULE_DEVELOPMENT.md +++ b/Modules/MODULE_DEVELOPMENT.md @@ -75,10 +75,17 @@ public class MyModule : BasePlugin if (_api == null) return; // 1. Register a new category + // IMPORTANT: If your module has lang/ folder with translations, use YOUR module's Localizer! + // Example: _api.RegisterMenuCategory("mymodule", Localizer?["mymodule_category_name"] ?? "My Module", "@css/generic"); + // + // This will use YOUR module's translations in server language. + // For modules without translations, you can use hard-coded text: _api.RegisterMenuCategory("mymodule", "My Module", "@css/generic"); // 2. Register menu items in the category // 🆕 NEW: Use MenuContext-aware overload (no duplication!) + // NOTE: Use YOUR module's Localizer if you have lang/ folder with translations: + // _api.RegisterMenu("mymodule", "action1", Localizer?["mymodule_menu_action1"] ?? "Action 1", CreateAction1Menu, "@css/generic"); _api.RegisterMenu("mymodule", "action1", "Action 1", CreateAction1Menu, "@css/generic"); _api.RegisterMenu("mymodule", "action2", "Action 2", CreateAction2Menu, "@css/kick"); } @@ -356,12 +363,27 @@ Registers a new menu category that appears in the main admin menu. **Parameters:** - `categoryId` - Unique identifier for the category (e.g., "fun", "vip", "economy") -- `categoryName` - Display name shown in menu (e.g., "Fun Commands") +- `categoryName` - **TRANSLATION KEY** for the display name (e.g., "vip_category_name", NOT "VIP Features") - `permission` - Required permission to see the category (default: "@css/generic") +**IMPORTANT FOR MODULES WITH TRANSLATIONS:** +- If your module has a `lang/` folder with translation files, use **YOUR module's Localizer** +- This will display menu names in the **server's language** (not per-player) +- For per-player localization, only **CS2-SimpleAdmin's built-in menus** support this currently + **Example:** ```csharp +// ✅ CORRECT: Module with translations uses its own Localizer +_api.RegisterMenuCategory("vip", Localizer?["vip_category_name"] ?? "VIP Features", "@css/vip"); +// In YOUR module's lang/en.json: "vip_category_name": "VIP Features" +// In YOUR module's lang/pl.json: "vip_category_name": "Funkcje VIP" + +// ✅ ALSO CORRECT: Module without translations uses hard-coded text _api.RegisterMenuCategory("vip", "VIP Features", "@css/vip"); + +// ❌ INCORRECT: Using translation key without Localizer +_api.RegisterMenuCategory("vip", "vip_category_name", "@css/vip"); +// This would display "vip_category_name" literally! ``` ### 2. Menu Registration @@ -373,13 +395,28 @@ Registers a menu item within a category. **Parameters:** - `categoryId` - The category to add this menu to - `menuId` - Unique identifier for the menu -- `menuName` - Display name in the category menu -- `menuFactory` - Function that creates the menu when selected (receives admin player) +- `menuName` - Display name for the menu (use **your module's Localizer** if you have translations) +- `menuFactory` - Function that creates the menu when selected (receives admin player and MenuContext) - `permission` - Optional permission required to see this menu item +**IMPORTANT FOR MODULES WITH TRANSLATIONS:** +- Use **your module's Localizer** if you have a `lang/` folder: `Localizer?["key"] ?? "Fallback"` +- This shows menu in **server language**, not per-player +- Per-player localization is only available for CS2-SimpleAdmin's built-in menus + **Example:** ```csharp +// ✅ CORRECT: Module with translations uses its own Localizer +_api.RegisterMenu("fun", "godmode", Localizer?["fun_menu_god"] ?? "God Mode", CreateGodModeMenu, "@css/cheats"); +// In YOUR module's lang/en.json: "fun_menu_god": "God Mode" +// In YOUR module's lang/pl.json: "fun_menu_god": "Tryb Boga" + +// ✅ ALSO CORRECT: Module without translations uses hard-coded text _api.RegisterMenu("fun", "godmode", "God Mode", CreateGodModeMenu, "@css/cheats"); + +// ❌ INCORRECT: Using translation key without Localizer +_api.RegisterMenu("fun", "godmode", "fun_menu_god", CreateGodModeMenu, "@css/cheats"); +// This would display "fun_menu_god" literally! ``` #### `UnregisterMenu(string categoryId, string menuId)` @@ -625,12 +662,32 @@ private object CreateAdminToolsMenu(CCSPlayerController admin) ## Best Practices -1. **Always check for API availability** +1. **Use your module's Localizer for translations** 🌍 + ```csharp + // ✅ CORRECT: Module with translations uses its own Localizer + _api.RegisterMenuCategory("mymodule", Localizer?["mymodule_category"] ?? "My Module", "@css/generic"); + _api.RegisterMenu("mymodule", "action", Localizer?["mymodule_menu_action"] ?? "My Action", CreateMenu, "@css/generic"); + + // ✅ ALSO CORRECT: Module without translations uses hard-coded text + _api.RegisterMenuCategory("mymodule", "My Module", "@css/generic"); + _api.RegisterMenu("mymodule", "action", "My Action", CreateMenu, "@css/generic"); + + // ❌ WRONG: Using translation key without Localizer + _api.RegisterMenuCategory("mymodule", "mymodule_category", "@css/generic"); + // This would display "mymodule_category" literally! + ``` + + **✅ Per-player localization now available!** + - Both CS2-SimpleAdmin built-in menus AND module menus support per-player localization + - Each player sees menus in their own language based on their `css_lang` setting + - See "Advanced: Per-Player Localization for Modules" section below for implementation details + +2. **Always check for API availability** ```csharp if (_api == null) return; ``` -2. **Validate player state before actions** +3. **Validate player state before actions** ```csharp if (target.IsValid && target.PlayerPawn?.Value != null) { @@ -638,11 +695,11 @@ private object CreateAdminToolsMenu(CCSPlayerController admin) } ``` -3. **Use descriptive category and menu IDs** +4. **Use descriptive category and menu IDs** - Good: `"economy"`, `"vip_features"`, `"fun_commands"` - Bad: `"cat1"`, `"menu"`, `"test"` -4. **Clean up on unload** +5. **Clean up on unload** ```csharp public override void Unload(bool hotReload) { @@ -651,14 +708,14 @@ private object CreateAdminToolsMenu(CCSPlayerController admin) } ``` -5. **Use appropriate permissions** +6. **Use appropriate permissions** - `@css/generic` - All admins - `@css/ban` - Admins who can ban - `@css/kick` - Admins who can kick - `@css/root` - Root admins only - Custom permissions from your module -6. **Handle hot reload** +7. **Handle hot reload** ```csharp _api.OnSimpleAdminReady += RegisterMenus; RegisterMenus(); // Fallback for hot reload case @@ -717,3 +774,72 @@ See the `CS2-SimpleAdmin_FunCommands` module in the `Modules/` directory for a c **Q: API is null in OnAllPluginsLoaded** - Wait for the `OnSimpleAdminReady` event instead of immediate registration - Make sure CS2-SimpleAdmin is loaded before your module + +## Advanced: Per-Player Localization for Modules (✅ NOW AVAILABLE!) + +**🆕 NEW:** Module menus now support **per-player localization** based on `css_lang`! + +Both CS2-SimpleAdmin's built-in menus AND module menus can show in each player's configured language. + +### How to Use Per-Player Localization in Your Module + +**1. Register Category with Localizer:** + +```csharp +// Pass translation KEY (not translated text) and module's Localizer +_api.RegisterMenuCategory( + "mymodule", + "mymodule_category_name", // Translation key from your lang/ folder + "@css/generic", + Localizer! // Your module's localizer +); +``` + +**2. Register Menus with Localizer:** + +```csharp +_api.RegisterMenu( + "mymodule", + "action", + "mymodule_menu_action", // Translation key from your lang/ folder + CreateMenu, + "@css/generic", // Permission + "css_mycommand", // Command name for override (optional) + Localizer! // Your module's localizer +); +``` + +**How it works:** +1. Your module passes its `IStringLocalizer` and translation **key** (not translated text) +2. SimpleAdmin's `MenuManager` stores both the key and the localizer +3. When displaying menu to a player, MenuManager translates using: + ```csharp + using (new WithTemporaryCulture(player.GetLanguage())) + { + localizedName = moduleLocalizer[translationKey]; + } + ``` +4. Each player sees menus in their own language based on their `css_lang` setting! + +**Complete Example:** + +See `Modules/CS2-SimpleAdmin_FunCommands/` for a real implementation: + +```csharp +// Register category with per-player localization +_api.RegisterMenuCategory("fun", "fun_category_name", "@css/generic", Localizer!); + +// Register menu with per-player localization +_api.RegisterMenu("fun", "god", "fun_menu_god", + CreateGodModeMenu, "@css/cheats", "css_god", Localizer!); +``` + +**Without Per-Player Localization (backwards compatible):** + +If you don't need per-player localization, the old API still works: + +```csharp +// Hard-coded text (same for all players) +_api.RegisterMenuCategory("mymodule", "My Module", "@css/generic"); +_api.RegisterMenu("mymodule", "action", "Do Action", CreateMenu, "@css/generic"); +```