Translations
Phrases per plugin in each player's language — phrase files, phrases pushed at runtime from JSON or code, formatting them for a player, and how a player's language is chosen.
ISource2ModTranslations gives every plugin its own set of phrases: a
name ("Welcome") with a text per language. When the plugin prints a
phrase for a player, it gets the text in that player's language.
#include "source2mod/ISource2ModTranslations.h"SourcePawn has it as the Source2ModTranslations methodmap in
<source2mod/api>, plus %t / %T in Format.
Phrases come from wherever the plugin has them: a phrase file in SourceMod's layout, a JSON text, or one call per text. A plugin that downloads its texts (HTTP, SQL) pushes them when they arrive and can change or remove them later.
Phrase files
A phrase file lives in addons/source2mod/translations/. Its format is
SourceMod's:
"Phrases"
{
"Welcome"
{
"#format" "{1:s},{2:d}"
"en" "Welcome, {1}! You have {2} points."
}
"Points"
{
"#format" "{1:d}"
"en" "You have {1} points."
}
}Other languages can go in the same file, under their code, or in a file of the same name in the language's folder:
"Phrases"
{
"Welcome"
{
"cze" "Vítej, {1}! Máš {2} bodů."
}
}| Key | |
|---|---|
#format | What the placeholders are: {1:s},{2:d} — the number, then a format spec (s, d, .2f, ...). Only needed when the text has placeholders. |
| a language code | The text in that language. {1}, {2}, ... may appear in any order and more than once. |
The language codes are those of configs/languages.cfg — SourceMod's:
en, de, fr, ru, cze, jp, chi, pt, ...
Loading
LoadPhraseFile takes the name without .txt. It reads
translations/<file>.txt and then translations/<code>/<file>.txt for every
known language. It fails only when the base file is missing or does not
parse; a broken language file is logged and skipped.
bool MyPlugin::Load(PluginId id, ISource2ModAPI* api, char* error, size_t maxlen, bool late)
{
SOURCE2MOD_SAVEVARS();
if (!g_pSource2ModTranslations->LoadPhraseFile(g_PluginID, "myplugin.phrases", error, maxlen))
return false;
return true;
}public void OnPluginStart()
{
char error[256];
if (!Source2ModTranslations.LoadPhraseFile("myplugin.phrases", error, sizeof(error)))
Source2ModPlugins.SetFailState("%s", error);
}Loading the file again (after editing it) replaces the texts it has; phrases it no longer has stay until removed.
Printing a phrase
The target is a player slot, or SOURCE2MOD_LANG_SERVER (slot -1) for the
server's language.
void MyPlugin::OnClientPutInServer(CPlayerSlot slot, const char* name, int type, uint64 xuid)
{
CCSPlayerController* player = CCSPlayerController::FromSlot(slot);
if (!player)
return;
const std::string points = std::to_string(10);
const char* args[] = { name, points.c_str() };
char text[256];
g_pSource2ModTranslations->FormatPhrase(g_PluginID, "Welcome", slot, args, 2, text, sizeof(text));
player->PrintToChat(text);
}public void Source2Mod_OnClientPutInServer(CPlayerSlot slot, const char[] name, int type, const char[] xuid)
{
CCSPlayerController player = CCSPlayerController.FromSlot(slot);
if (player == NULL_ENTITY)
return;
char text[256];
Source2ModApi.Format(text, sizeof(text), "%T", "Welcome", slot.Get(), name, 10);
player.PrintToChat(text);
}The two languages differ in how they fill the placeholders:
C++ FormatPhrase | SourcePawn %t / %T / FormatPhrase | |
|---|---|---|
| Arguments | An array of strings, put in as they are | Any values, each formatted by its #format spec |
#format | Not applied | Applied ({1:.2f} with 3.14159 gives 3.14) |
| Missing argument | Left empty | Script error ("needs N more argument(s)") |
Translate gives the raw text with the {1}, {2} left in, for a plugin
that fills them itself.
Replies to a command
ReplyToCommand answers where the command was typed. The phrase should be
in the language of whoever typed it — ctx.GetPlayerSlot(), which is -1
(the server's language) for the server console:
REGISTER_CON_COMMAND("points", [](const Source2ModCommandContext& ctx, const Source2ModCommandArgs& args, bool post)
{
const std::string points = std::to_string(42);
const char* phraseArgs[] = { points.c_str() };
char text[256];
g_pSource2ModTranslations->FormatPhrase(g_PluginID, "Points", ctx.GetPlayerSlot(), phraseArgs, 1, text, sizeof(text));
g_pSource2ModCommands->ReplyToCommand(ctx, text);
});public void OnPluginStart()
{
Source2ModTranslations.LoadPhraseFile("myplugin.phrases");
Source2ModCommands.RegisterConCommand("points", Cmd_Points);
}
public void Cmd_Points(Source2ModCommandContext ctx, Source2ModCommandArgs args, bool post)
{
int points = 42;
// %T: phrase, then whom it is for (-1 = server), then the phrase's arguments.
Source2ModCommands.ReplyToCommand(ctx, "%T", "Points", ctx.GetPlayerSlot().Get(), points);
}%t and %T (SourcePawn)
Every SourcePawn function that formats (Format, ReplyToCommand,
Log, Kick, ...) understands two more specifiers:
| Takes | Translates for | |
|---|---|---|
%T | the phrase, the target's slot as an int (slot.Get(), -1 = server), then the phrase's arguments | the given target |
%t | the phrase, then its arguments | the format target — the server, unless SetFormatTarget changed it |
SetFormatTarget is SourceMod's SetGlobalTransTarget: useful when one
format string has several phrases for the same player. Set it back to
SOURCE2MOD_LANG_SERVER when done; it stays for every later %t of the
plugin otherwise.
void PrintToEveryone()
{
char text[256];
for (int i = 0; i < 64; i++)
{
CPlayerSlot slot = CPlayerSlot(i);
if (!Source2ModClients.IsInGame(slot) || Source2ModClients.IsFakeClient(slot))
continue;
Source2ModTranslations.SetFormatTarget(slot);
Source2ModApi.Format(text, sizeof(text), "%t %t", "Prefix", "Round started");
CCSPlayerController.FromSlot(slot).PrintToChat(text);
}
Source2ModTranslations.SetFormatTarget(SOURCE2MOD_LANG_SERVER);
}Source2ModTranslations.FormatPhrase is the same as a lone %T, without a
format string:
char text[256];
Source2ModTranslations.FormatPhrase(text, sizeof(text), SOURCE2MOD_LANG_SERVER, "Welcome", "Bob", 10);Phrases at runtime
Files are only one source. AddPhrase adds or replaces one text of a phrase;
a language code not known yet is added. The format is given with any of the
texts (or none, to keep the one the phrase has):
auto* tr = g_pSource2ModTranslations;
tr->AddPhrase(g_PluginID, "Rank", "en", "Your rank: {1} of {2}", "{1:d},{2:d}");
tr->AddPhrase(g_PluginID, "Rank", "de", "Dein Rang: {1} von {2}");
// e.g. from an HTTP or SQL callback, with the body the backend sent
void OnTextsDownloaded(const char* json)
{
char error[256];
if (!g_pSource2ModTranslations->LoadPhrasesJson(g_PluginID, json, error, sizeof(error)))
SOURCE2MOD_CONPRINTF("bad phrases: %s\n", error);
}
tr->RemovePhrase(g_PluginID, "Rank", "de"); // one language
tr->RemovePhrase(g_PluginID, "Rank"); // the whole phrase
tr->ClearPhrases(g_PluginID); // everything this plugin hasSource2ModTranslations.AddPhrase("Rank", "en", "Your rank: {1} of {2}", "{1:d},{2:d}");
Source2ModTranslations.AddPhrase("Rank", "de", "Dein Rang: {1} von {2}");
// e.g. from an HTTP or SQL callback, with the body the backend sent
void OnTextsDownloaded(const char[] json)
{
char error[256];
if (!Source2ModTranslations.LoadPhrasesJson(json, error, sizeof(error)))
Source2ModApi.LogError("bad phrases: %s", error);
}
Source2ModTranslations.RemovePhrase("Rank", "de"); // one language
Source2ModTranslations.RemovePhrase("Rank"); // the whole phrase
Source2ModTranslations.ClearPhrases(); // everything this plugin hasThe JSON is one object of phrases, each an object of #format and texts by
language code. Unknown codes are added as languages:
{
"Rank": { "#format": "{1:d},{2:d}", "en": "Your rank: {1} of {2}", "de": "Dein Rang: {1} von {2}" },
"Bye": { "#format": "{1:s}", "en": "Bye {1}" }
}A phrase that is not there (yet) prints as its name, without an error — a
plugin can use its phrases before the download finished.
PhraseExists(phrase) and IsTranslatedFor(phrase, language) tell what is
there.
Languages
A language is an index. configs/languages.cfg lists the known ones (code
and name), AddLanguage(code, name) adds more at runtime, and
FindLanguage finds one by code or name ("cze" or "Czech"; -1 when
unknown). GetLanguageInfo(index, ...) gives both back.
The server's language is ServerLang in core.json (en by default).
A player's language is, in this order:
- the one a plugin set with
SetPlayerLanguage(until they disconnect), - the client's own
cl_language(czech,schinese,brazilian, ... are mapped to the codes of languages.cfg), - the server's.
Bots have no cl_language, so they get the server's unless a plugin set
one. A plugin can let players choose:
REGISTER_CON_COMMAND("language", [](const Source2ModCommandContext& ctx, const Source2ModCommandArgs& args, bool post)
{
auto* tr = g_pSource2ModTranslations;
const CPlayerSlot slot = ctx.GetPlayerSlot();
if (args.ArgC() < 2)
{
const char *code = nullptr, *name = nullptr;
tr->GetLanguageInfo(tr->GetPlayerLanguage(slot), &code, &name);
REPLY_TO_COMMAND(ctx, "Your language: %s (%s)", name, code);
return;
}
const int language = tr->FindLanguage(args[1]); // "de" or "German"
if (language == -1)
{
REPLY_TO_COMMAND(ctx, "Unknown language %s.", args[1]);
return;
}
tr->SetPlayerLanguage(slot, language);
});public void Cmd_Language(Source2ModCommandContext ctx, Source2ModCommandArgs args, bool post)
{
CPlayerSlot slot = ctx.GetPlayerSlot();
char code[16], name[64];
if (args.ArgC() < 2)
{
Source2ModTranslations.GetLanguageInfo(Source2ModTranslations.GetPlayerLanguage(slot), code, sizeof(code), name, sizeof(name));
Source2ModCommands.ReplyToCommand(ctx, "Your language: %s (%s)", name, code);
return;
}
args.Arg(1, code, sizeof(code));
int language = Source2ModTranslations.FindLanguage(code); // "de" or "German"
if (language == -1)
{
Source2ModCommands.ReplyToCommand(ctx, "Unknown language %s.", code);
return;
}
Source2ModTranslations.SetPlayerLanguage(slot, language);
}SetPlayerLanguage(slot, -1) goes back to the client's own. The override is
in memory only — a plugin that remembers the choice stores it itself (a
cookie, a database) and sets it again
when the player joins.
Fallbacks
A lookup never fails. A phrase without the target's language uses the
server's language, then English, then prints the phrase name. So a phrase
needs at least an en text (or one in ServerLang).
Phrases are per plugin
Every plugin has its own phrases, also two plugins loading the same file:
"Welcome" in one plugin is not "Welcome" in another. A plugin's phrases
are dropped when it unloads.
The core ships translations/common.phrases.txt with SourceMod's shared
names: "Yes", "No", "Back", the targeting errors ("No matching client", ...) and what the @ targets are called ("all players", "all terrorists", ...). A plugin that wants them loads it like any other file —
see Clients and targeting.
Pitfalls
%tis not the reply's recipient. In SourcePawn,%ttranslates for the format target, which is the server unlessSetFormatTargetchanged it — also insideReplyToCommand. Use%Twithctx.GetPlayerSlot().Get()when answering a player.#formatis SourcePawn only. C++FormatPhraseputs its string arguments in as they are; format numbers yourself (std::to_string,snprintf) before passing them.- A missing phrase eats no arguments. A
%twhose phrase is not loaded prints the name and takes none of the phrase's arguments, so the values after it land on the next specifiers. Load the file inOnPluginStart/Load(), not later. - A phrase needs a format for its placeholders. Without
#format, SourcePawn passes no arguments to it and{1}stays in the text as it is. - Phrase names are case-sensitive (
"Welcome"is not"welcome"); language codes and names are not.
Reference
ISource2ModTranslations — every
call with its parameters.
Clients and targeting
Player state by slot — in game, bot, Steam-validated, SteamIDs in every form, latency, client cvars, kicking — and turning command text like @ct,
Menus
Menus with numbered options, paging, per-player instances and cleanup on unload — in the center panel (CenterHtmlMenu) or on the Panorama HUD (HudMenu).