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.
29 KiB
Module Development Guide - CS2-SimpleAdmin
🎓 New to module development? Start with the Fun Commands Module - it's a fully documented reference implementation showing all best practices!
This guide explains how to create modules for CS2-SimpleAdmin with custom commands, menus, and translations.
📖 Table of Contents
- Quick Start
- Learning Resources
- MenuContext API (New!)
- API Methods Reference
- Menu Patterns
- Best Practices
- Troubleshooting
🚀 Quick Start
Step 1: Study the Example Module
The CS2-SimpleAdmin_FunCommands module is your best learning resource. It demonstrates:
✅ Command registration from config ✅ Dynamic menu creation ✅ Per-player translation support ✅ Proper cleanup on unload ✅ Code organization with partial classes ✅ All menu patterns you'll need
Start here: Read CS2-SimpleAdmin_FunCommands/README.md
Step 2: Create Your Module Structure
YourModule/
├── YourModule.csproj # Project file
├── YourModule.cs # Main plugin class
├── Commands.cs # Command handlers (optional)
├── Menus.cs # Menu creation (optional)
├── Config.cs # Configuration
└── lang/ # Translations
├── en.json
├── pl.json
└── ru.json
Step 3: Minimal Working Example (NEW MenuContext API!)
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CS2_SimpleAdminApi;
public class MyModule : BasePlugin
{
private ICS2_SimpleAdminApi? _api;
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability = new("simpleadmin:api");
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null)
{
Logger.LogError("CS2-SimpleAdmin API not found");
return;
}
// Register menus after API is ready
_api.OnSimpleAdminReady += RegisterMenus;
RegisterMenus(); // Fallback for hot reload
}
private void RegisterMenus()
{
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");
}
// 🆕 NEW: Factory now receives MenuContext parameter
private object CreateAction1Menu(CCSPlayerController admin, MenuContext context)
{
// Create a menu with automatic back button
// Use context instead of repeating title and category!
var menu = _api!.CreateMenuWithBack(context, admin);
// Add menu options
_api.AddMenuOption(menu, "Option 1", player =>
{
player.PrintToChat("You selected Option 1");
});
_api.AddMenuOption(menu, "Option 2", player =>
{
player.PrintToChat("You selected Option 2");
});
return menu;
}
// 🆕 NEW: MenuContext eliminates duplication here too!
private object CreateAction2Menu(CCSPlayerController admin, MenuContext context)
{
// Use the built-in player selection menu with context
return _api!.CreateMenuWithPlayers(
context, // ← Contains title & category automatically!
admin,
player => player.IsValid && admin.CanTarget(player),
(adminPlayer, targetPlayer) =>
{
adminPlayer.PrintToChat($"You selected {targetPlayer.PlayerName}");
});
}
public override void Unload(bool hotReload)
{
if (_api == null) return;
// Clean up registered menus
_api.UnregisterMenu("mymodule", "action1");
_api.UnregisterMenu("mymodule", "action2");
_api.OnSimpleAdminReady -= RegisterMenus;
}
}
📚 Learning Resources
For Beginners
-
Start Here:
CS2-SimpleAdmin_FunCommands/README.md- Explains every file and pattern
- Shows code organization
- Demonstrates all menu types
-
Read the Code: Study these files in order:
Config.cs- Simple configurationCS2-SimpleAdmin_FunCommands.cs- Plugin initializationCommands.cs- Command registrationMenus.cs- Menu creation patterns
-
Translations:
TRANSLATION_EXAMPLE.md- How to use module translations
- Per-player language support
- Best practices
Key Concepts
| Concept | What It Does | Example File |
|---|---|---|
| API Capability | Get access to SimpleAdmin API | CS2-SimpleAdmin_FunCommands.cs:37 |
| Command Registration | Register console commands | Commands.cs:15-34 |
| Menu Registration | Add menus to admin menu | CS2-SimpleAdmin_FunCommands.cs:130-141 |
| Translations | Per-player language support | Actions.cs:20-31 |
| Cleanup | Unregister on plugin unload | CS2-SimpleAdmin_FunCommands.cs:63-97 |
🎯 Menu Patterns
The FunCommands module demonstrates 3 essential menu patterns you'll use in every module:
Pattern 1: Simple Player Selection (NEW MenuContext API!)
When to use: Select a player and immediately execute an action
// 🆕 NEW: Factory receives MenuContext - eliminates duplication!
private object CreateGodModeMenu(CCSPlayerController admin, MenuContext context)
{
return _api.CreateMenuWithPlayers(
context, // ← Contains title & category automatically!
admin, // Admin
player => player.IsValid && admin.CanTarget(player), // Filter
(adminPlayer, target) => // Action
{
// Execute action immediately
ToggleGodMode(target);
});
}
Why MenuContext is better:
- ❌ Old way:
"God Mode"and"yourmodule"had to be typed twice (in RegisterMenu and CreateMenuWithPlayers) - ✅ New way: Context contains both automatically - no duplication, no mistakes!
See: CS2-SimpleAdmin_FunCommands/Menus.cs:44-51
Pattern 2: Nested Menu (Player → Value) - NEW MenuContext API!
When to use: Select a player, then select a value/option for that player
// 🆕 NEW: Uses MenuContext to eliminate duplication
private object CreateSetHpMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api.CreateMenuWithBack(context, admin); // ← No more repeating title & category!
var players = _api.GetValidPlayers().Where(p => admin.CanTarget(p));
foreach (var player in players)
{
// AddSubMenu automatically adds back button to submenu
_api.AddSubMenu(menu, player.PlayerName,
p => CreateHpValueMenu(admin, player));
}
return menu;
}
// Example: Set HP menu (value selection)
// Note: Submenus don't receive context - they create dynamic titles
private object CreateHpValueMenu(CCSPlayerController admin, CCSPlayerController target)
{
var menu = _api.CreateMenuWithBack($"HP for {target.PlayerName}", "yourmodule", admin);
var values = new[] { 50, 100, 200, 500 };
foreach (var hp in values)
{
_api.AddMenuOption(menu, $"{hp} HP", _ =>
{
if (target.IsValid) // Always validate!
{
target.PlayerPawn.Value.Health = hp;
}
});
}
return menu;
}
Benefits of MenuContext:
- ✅ Change menu title in one place (RegisterMenu) and it updates everywhere
- ✅ Can't accidentally mistype category ID
- ✅ Access to permission and command name from context if needed
See: CS2-SimpleAdmin_FunCommands/Menus.cs:163-178
Pattern 3: Nested Menu with Complex Data
When to use: Need to display more complex options (like weapons with icons, items with descriptions)
// Example: Give Weapon menu
private object CreateGiveWeaponMenu(CCSPlayerController admin)
{
var menu = _api.CreateMenuWithBack("Give Weapon", "yourmodule", admin);
var players = _api.GetValidPlayers().Where(p => admin.CanTarget(p));
foreach (var player in players)
{
_api.AddSubMenu(menu, player.PlayerName,
p => CreateWeaponSelectionMenu(admin, player));
}
return menu;
}
private object CreateWeaponSelectionMenu(CCSPlayerController admin, CCSPlayerController target)
{
var menu = _api.CreateMenuWithBack($"Weapons for {target.PlayerName}", "yourmodule", admin);
// Use cached data for performance
foreach (var weapon in GetWeaponsCache())
{
_api.AddMenuOption(menu, weapon.Value.ToString(), _ =>
{
if (target.IsValid)
{
target.GiveNamedItem(weapon.Value);
}
});
}
return menu;
}
See: CS2-SimpleAdmin_FunCommands/Menus.cs:67-109
🆕 MenuContext API (New!)
What is MenuContext?
MenuContext is a new feature that eliminates code duplication when creating menus. When you register a menu, you provide information like title, category, and permissions. Previously, you had to repeat this information in your menu factory method. Now, this information is automatically passed to your factory via MenuContext.
MenuContext Properties
public class MenuContext
{
public string CategoryId { get; } // e.g., "fun"
public string MenuId { get; } // e.g., "god"
public string MenuTitle { get; } // e.g., "God Mode"
public string? Permission { get; } // e.g., "@css/cheats"
public string? CommandName { get; } // e.g., "css_god"
}
Before vs After Comparison
// ❌ OLD API - Duplication everywhere
_api.RegisterMenu("fun", "god", "God Mode", CreateGodModeMenu, "@css/cheats");
private object CreateGodModeMenu(CCSPlayerController admin)
{
return _api.CreateMenuWithPlayers(
"God Mode", // ← Duplicated from RegisterMenu
"fun", // ← Duplicated from RegisterMenu
admin, filter, action);
}
// ✅ NEW API - No duplication!
_api.RegisterMenu("fun", "god", "God Mode", CreateGodModeMenu, "@css/cheats");
private object CreateGodModeMenu(CCSPlayerController admin, MenuContext context)
{
return _api.CreateMenuWithPlayers(
context, // ← Contains all info automatically!
admin, filter, action);
}
When to Use MenuContext
| Menu Creation Method | Old Signature | New Signature (Recommended) |
|---|---|---|
CreateMenuWithBack |
(string title, string categoryId, ...) |
(MenuContext context, ...) |
CreateMenuWithPlayers |
(string title, string categoryId, ...) |
(MenuContext context, ...) |
Rule of thumb: If you're creating a menu directly from a registered menu factory, use MenuContext. For dynamic submenus (e.g., player-specific menus), use the old API.
Backward Compatibility
The old API still works! Both signatures are supported:
// ✅ Old API - still works
_api.RegisterMenu("cat", "id", "Title",
(CCSPlayerController admin) => CreateOldStyleMenu(admin));
// ✅ New API - recommended
_api.RegisterMenu("cat", "id", "Title",
(CCSPlayerController admin, MenuContext ctx) => CreateNewStyleMenu(admin, ctx));
📋 API Methods Reference
1. Category Management
RegisterMenuCategory(string categoryId, string categoryName, string permission = "@css/generic")
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- 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:
// ✅ 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
RegisterMenu(string categoryId, string menuId, string menuName, Func<CCSPlayerController, object> menuFactory, string? permission = null)
Registers a menu item within a category.
Parameters:
categoryId- The category to add this menu tomenuId- Unique identifier for the menumenuName- 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:
// ✅ 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)
Removes a menu item from a category.
Example:
_api.UnregisterMenu("fun", "godmode");
3. Menu Creation
CreateMenuWithBack(string title, string categoryId, CCSPlayerController player)
CreateMenuWithBack(MenuContext context, CCSPlayerController player) 🆕 NEW!
Creates a menu with an automatic "Back" button that returns to the category menu.
Parameters (Old API):
title- Menu titlecategoryId- Category this menu belongs to (for back navigation)player- The admin player viewing the menu
Parameters (New API - Recommended):
context- MenuContext containing title, category, and other metadataplayer- The admin player viewing the menu
Returns: object (MenuBuilder instance)
Example (Old API):
var menu = _api.CreateMenuWithBack("Weapon Selection", "fun", admin);
Example (New API - Recommended):
private object CreateWeaponMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api.CreateMenuWithBack(context, admin); // ← Uses context!
// ... add options
return menu;
}
CreateMenuWithPlayers(string title, string categoryId, CCSPlayerController admin, Func<CCSPlayerController, bool> filter, Action<CCSPlayerController, CCSPlayerController> onSelect)
CreateMenuWithPlayers(MenuContext context, CCSPlayerController admin, Func<CCSPlayerController, bool> filter, Action<CCSPlayerController, CCSPlayerController> onSelect) 🆕 NEW!
Creates a menu with a list of players, filtered and with automatic back button.
Parameters (Old API):
title- Menu titlecategoryId- Category for back navigationadmin- The admin player viewing the menufilter- Function to filter which players appear in the menuonSelect- Action to execute when a player is selected (receives admin and target)
Parameters (New API - Recommended):
context- MenuContext containing title and categoryadmin- The admin player viewing the menufilter- Function to filter which players appear in the menuonSelect- Action to execute when a player is selected (receives admin and target)
Returns: object (MenuBuilder instance)
Example (Old API):
return _api.CreateMenuWithPlayers("Select Player to Kick", "admin", admin,
player => player.IsValid && admin.CanTarget(player),
(adminPlayer, targetPlayer) =>
{
// Kick the selected player
Server.ExecuteCommand($"css_kick {targetPlayer.UserId}");
});
Example (New API - Recommended):
private object CreateKickMenu(CCSPlayerController admin, MenuContext context)
{
return _api.CreateMenuWithPlayers(context, admin,
player => player.IsValid && admin.CanTarget(player),
(adminPlayer, targetPlayer) =>
{
Server.ExecuteCommand($"css_kick {targetPlayer.UserId}");
});
}
4. Menu Manipulation
AddMenuOption(object menu, string name, Action<CCSPlayerController> action, bool disabled = false, string? permission = null)
Adds a clickable option to a menu.
Parameters:
menu- The menu object (from CreateMenuWithBack)name- Display name of the optionaction- Action to execute when clicked (receives the player who clicked)disabled- Whether the option is disabled (grayed out)permission- Optional permission required to see this option
Example:
_api.AddMenuOption(menu, "Give AK-47", player =>
{
player.GiveNamedItem("weapon_ak47");
}, permission: "@css/cheats");
AddSubMenu(object menu, string name, Func<CCSPlayerController, object> subMenuFactory, bool disabled = false, string? permission = null)
Adds a submenu option that opens another menu. Automatically adds a back button to the submenu.
Parameters:
menu- The parent menuname- Display name of the submenu optionsubMenuFactory- Function that creates the submenu (receives the player)disabled- Whether the option is disabledpermission- Optional permission required
Example:
_api.AddSubMenu(menu, "Weapon Category", player =>
{
var weaponMenu = _api.CreateMenuWithBack("Weapons", "fun", player);
_api.AddMenuOption(weaponMenu, "AK-47", p => p.GiveNamedItem("weapon_ak47"));
_api.AddMenuOption(weaponMenu, "AWP", p => p.GiveNamedItem("weapon_awp"));
return weaponMenu;
});
OpenMenu(object menu, CCSPlayerController player)
Opens a menu for a specific player.
Example:
var menu = _api.CreateMenuWithBack("Custom Menu", "fun", player);
_api.AddMenuOption(menu, "Test", p => p.PrintToChat("Test!"));
_api.OpenMenu(menu, player);
Advanced Examples
Nested Menus with Player Selection
private object CreateGiveWeaponMenu(CCSPlayerController admin)
{
var menu = _api.CreateMenuWithBack("Give Weapon", "fun", admin);
var players = _api.GetValidPlayers()
.Where(p => p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p));
foreach (var player in players)
{
// Add submenu for each player - automatic back button will be added
_api.AddSubMenu(menu, player.PlayerName, p => CreateWeaponSelectionMenu(admin, player));
}
return menu;
}
private object CreateWeaponSelectionMenu(CCSPlayerController admin, CCSPlayerController target)
{
var weaponMenu = _api.CreateMenuWithBack($"Weapons for {target.PlayerName}", "fun", admin);
var weapons = new[] { "weapon_ak47", "weapon_m4a1", "weapon_awp", "weapon_deagle" };
foreach (var weapon in weapons)
{
_api.AddMenuOption(weaponMenu, weapon, _ =>
{
if (target.IsValid)
{
target.GiveNamedItem(weapon);
admin.PrintToChat($"Gave {weapon} to {target.PlayerName}");
}
});
}
return weaponMenu;
}
Dynamic Menu with Value Selection
private object CreateSetHpMenu(CCSPlayerController admin)
{
var menu = _api.CreateMenuWithBack("Set HP", "admin", admin);
var players = _api.GetValidPlayers().Where(p => admin.CanTarget(p));
foreach (var player in players)
{
_api.AddSubMenu(menu, player.PlayerName, p => CreateHpValueMenu(admin, player));
}
return menu;
}
private object CreateHpValueMenu(CCSPlayerController admin, CCSPlayerController target)
{
var hpMenu = _api.CreateMenuWithBack($"HP for {target.PlayerName}", "admin", admin);
var hpValues = new[] { 1, 50, 100, 200, 500, 1000 };
foreach (var hp in hpValues)
{
_api.AddMenuOption(hpMenu, $"{hp} HP", _ =>
{
if (target.IsValid && target.PlayerPawn?.Value != null)
{
target.PlayerPawn.Value.Health = hp;
admin.PrintToChat($"Set {target.PlayerName} HP to {hp}");
}
});
}
return hpMenu;
}
Permission-Based Menu Options
private object CreateAdminToolsMenu(CCSPlayerController admin)
{
var menu = _api.CreateMenuWithBack("Admin Tools", "admin", admin);
// Only root admins can see this
_api.AddMenuOption(menu, "Dangerous Action", player =>
{
player.PrintToChat("Executing dangerous action...");
}, permission: "@css/root");
// All admins can see this
_api.AddMenuOption(menu, "Safe Action", player =>
{
player.PrintToChat("Executing safe action...");
}, permission: "@css/generic");
return menu;
}
Best Practices
-
Use your module's Localizer for translations 🌍
// ✅ 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_langsetting - See "Advanced: Per-Player Localization for Modules" section below for implementation details
-
Always check for API availability
if (_api == null) return; -
Validate player state before actions
if (target.IsValid && target.PlayerPawn?.Value != null) { // Safe to perform action } -
Use descriptive category and menu IDs
- Good:
"economy","vip_features","fun_commands" - Bad:
"cat1","menu","test"
- Good:
-
Clean up on unload
public override void Unload(bool hotReload) { _api?.UnregisterMenu("mymodule", "mymenu"); _api.OnSimpleAdminReady -= RegisterMenus; } -
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
-
Handle hot reload
_api.OnSimpleAdminReady += RegisterMenus; RegisterMenus(); // Fallback for hot reload case
Automatic Back Button
The menu system automatically adds a "Back" button to all submenus created with:
CreateMenuWithBack()- Returns to the category menuAddSubMenu()- Returns to the parent menu
You don't need to manually add back buttons - the system handles navigation automatically!
Getting Valid Players
Use the API helper method to get valid, connected players:
var players = _api.GetValidPlayers();
// With filtering
var alivePlayers = _api.GetValidPlayers()
.Where(p => p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE);
var targetablePlayers = _api.GetValidPlayers()
.Where(p => admin.CanTarget(p));
Complete Module Example
See the CS2-SimpleAdmin_FunCommands module in the Modules/ directory for a complete, production-ready example of:
- Category registration
- Multiple menu types
- Nested menus with automatic back buttons
- Player selection menus
- Permission-based access
- Proper cleanup on unload
Troubleshooting
Q: My category doesn't appear in the admin menu
- Ensure you're calling
RegisterMenuCategory()after the API is ready - Check that the player has the required permission
- Verify the category has at least one menu registered with
RegisterMenu()
Q: Back button doesn't work
- Make sure you're using
CreateMenuWithBack()instead of creating menus manually - The
categoryIdparameter must match the category you registered - Use
AddSubMenu()for nested menus - it handles back navigation automatically
Q: Menu appears but is empty
- Check that you're adding options with
AddMenuOption()orAddSubMenu() - Verify permission checks aren't filtering out all options
- Ensure player validation in filters isn't too restrictive
Q: API is null in OnAllPluginsLoaded
- Wait for the
OnSimpleAdminReadyevent 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:
// 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:
_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:
- Your module passes its
IStringLocalizerand translation key (not translated text) - SimpleAdmin's
MenuManagerstores both the key and the localizer - When displaying menu to a player, MenuManager translates using:
using (new WithTemporaryCulture(player.GetLanguage())) { localizedName = moduleLocalizer[translationKey]; } - Each player sees menus in their own language based on their
css_langsetting!
Complete Example:
See Modules/CS2-SimpleAdmin_FunCommands/ for a real implementation:
// 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:
// Hard-coded text (same for all players)
_api.RegisterMenuCategory("mymodule", "My Module", "@css/generic");
_api.RegisterMenu("mymodule", "action", "Do Action", CreateMenu, "@css/generic");