Add per-player menu localization and refactor menus

Introduces per-player localization for menu categories and items using translation keys and IStringLocalizer, allowing modules and the main plugin to display menu names in the player's language. Refactors menu registration and builder logic to use translation keys, updates API and documentation, and adds database provider upsert query abstraction for player IPs. Also updates version to 1.7.8-beta-4 and corrects a translation string typo.
This commit is contained in:
Dawid Bepierszcz
2025-10-30 01:41:08 +01:00
parent b0d8696756
commit a03964c08a
20 changed files with 759 additions and 138 deletions

View File

@@ -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!");
}

View File

@@ -366,66 +366,73 @@ public partial class CS2_SimpleAdmin_FunCommands : BasePlugin, IPluginConfig<Con
try
{
_sharedApi.RegisterMenuCategory("fun", Localizer?["fun_category_name"] ?? "Fun Commands", "@css/generic");
// 🆕 NEW: Per-player localization support for modules!
// - This module has its own lang/ folder with translations
// - We pass translation KEYS and the module's Localizer to the API
// - SimpleAdmin will translate menu names per-player based on their css_lang
// - Each player sees menus in their own language!
_sharedApi.RegisterMenuCategory("fun", "fun_category_name", "@css/generic", Localizer!);
// Register menus with command names for permission override support
// Server admins can override default permissions via CounterStrikeSharp admin system
// Example: If "css_god" is overdden to "@css/vip", only VIPs will see the God Mode menu
// Example: If "css_god" is overridden to "@css/vip", only VIPs will see the God Mode menu
//
// 🆕 NEW: All menus use translation keys and module's Localizer for per-player localization!
if (Config.GodCommands.Count > 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!");

View File

@@ -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
/// </summary>
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
/// </summary>
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
/// </summary>
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[]

View File

@@ -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");
```