Guides

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.

plugin.cpp
#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:

translations/myplugin.phrases.txt
"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:

translations/cze/myplugin.phrases.txt
"Phrases"
{
	"Welcome"
	{
		"cze"		"Vítej, {1}! Máš {2} bodů."
	}
}
Key
#formatWhat 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 codeThe 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.

plugin.cpp
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;
}

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.

plugin.cpp
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);
}

The two languages differ in how they fill the placeholders:

C++ FormatPhraseSourcePawn %t / %T / FormatPhrase
ArgumentsAn array of strings, put in as they areAny values, each formatted by its #format spec
#formatNot appliedApplied ({1:.2f} with 3.14159 gives 3.14)
Missing argumentLeft emptyScript 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:

plugin.cpp
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);
});

%t and %T (SourcePawn)

Every SourcePawn function that formats (Format, ReplyToCommand, Log, Kick, ...) understands two more specifiers:

TakesTranslates for
%Tthe phrase, the target's slot as an int (slot.Get(), -1 = server), then the phrase's argumentsthe given target
%tthe phrase, then its argumentsthe 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.

plugin.sp
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:

plugin.sp
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):

plugin.cpp
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 has

The 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:

  1. the one a plugin set with SetPlayerLanguage (until they disconnect),
  2. the client's own cl_language (czech, schinese, brazilian, ... are mapped to the codes of languages.cfg),
  3. 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:

plugin.cpp
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);
});

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

  • %t is not the reply's recipient. In SourcePawn, %t translates for the format target, which is the server unless SetFormatTarget changed it — also inside ReplyToCommand. Use %T with ctx.GetPlayerSlot().Get() when answering a player.
  • #format is SourcePawn only. C++ FormatPhrase puts its string arguments in as they are; format numbers yourself (std::to_string, snprintf) before passing them.
  • A missing phrase eats no arguments. A %t whose 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 in OnPluginStart / 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.

On this page