Repository navigation
L10n Cpp
TL;DR: C++ output is localized by wrapping the Russian literal in
_("...")(resolved per recipient) orl(ch, "...")(resolved now for one viewer) and adding the English and Ukrainian text to the catalog. A wrapped line with no catalog entry prints exactly as before, so wrapping is always safe. The work is in choosing the right idiom and in the sites where no viewer is in scope. Use when: you write or change a hard-coded message insrc/orplug-ins/, convert abuf << "русский текст"builder, or find Russian text shown to English or Ukrainian players. Source of truth:src/l10n/l10n.h(_,l,l10n_file_key),src/core/multimessage.h,plug-ins/output/act.h(fmt,fmtLang,lmsg),src/xml/xmlmultistring.cpp,dreamland_world/config/translations/plug-ins.json. Verified: 2026-11-02 against dreamland_code 88da8302, dreamland_world 43880467
Every player-facing line resolves through the recipient's language (viewerLang(ch), see L10n).
-
_(msg)builds aMultiMessage(msg, FILE). The output overloads (pecho,recho,echo,oldact,fmt,say_fmtand others) resolve it separately for each recipient. -
l(ch, msg)resolves now, for one known viewer, and returns a stableconst char *. Use it for string building, table lookups and anywhere a plain C string is needed. - The catalog entry is keyed by (file, Russian text) and holds
{en, ua}. The file key comes from__FILE__, cut at the lastsrc/orplug-ins/component, so it does not depend on the build layout (plug-ins/comm/look.cpp). A file with neither component degrades to Russian only. - Russian is byte-identical. For a Russian reader
_()returns the source string itself. Zero regression is the invariant: a line you wrap but do not translate must print exactly what it printed before.
Include l10n.h only in .cpp files that wrap literals. It defines the function-like macros _ and l, and a header that includes it leaks them into every file that follows.
| You have | Write |
|---|---|
ch->pecho("рус", args) |
ch->pecho(_("рус"), args) |
buf << "рус" |
buf << fmt(ch, _("рус")) (needs act.h and l10n.h) |
buf << "рус " << x << " рус2" |
buf << fmt(ch, _("рус %1$s рус2"), x), one whole sentence with numbered codes |
A literal where no fmt recipient exists |
buf << _("рус").getMessage(viewerLang(ch)) |
X.get(RU) or .get(LANG_DEFAULT) on data, a viewer in scope |
X.getForLang(Player::displayLang(ch)) |
room->getName(), area->getName() (default language) |
->getName(viewerLang(ch)) |
skill->getName(), command->getName()
|
->getNameFor(ch) or ->getNameFor(lang)
|
fmt(0, _("рус"), ...) with a viewer available |
fmt(ch, _("рус"), ...) |
| A broadcast that names a direction |
direction_word(viewerLang(ch), door, DIR_CASE_TO), or store LangText dir = direction_langtext(door, dcase); and pass &dir to a %w code so each reader gets the word in their language. %w reads a LangText *, so passing the struct by value compiles and then crashes |
| A message whose three texts are in hand |
MultiMessage(en, ru, ua) (no catalog) |
fmt(0, ...) and fmt(NULL, ...) have no recipient and resolve to Russian even when English and Ukrainian exist in the catalog. This is the most common leak.
fmt(ch, _("literal")) treats the resolved string as a printf format. It is safe only if the literal has no bare % or $. Colour tags pass through.
For a sink with no viewer use fmtLang(lang, _("..."), ...). See L10n.
Check every site, not every file. A raw-stream builder can live in a helper that has no Character * (a list printer that only takes a std::ostream &). Wrapping fmt(ch, _()) there does not compile, and the fix is a different design:
- The helper has a
chparameter, or the class has aCharacter *chmember: wrap it. - Neither: add a
Character *orlang_tparameter and thread it through the callers, or store the viewer in a member set before display, or defer the site.
Read the enclosing function signature yourself before you skip a site because a search tool claimed it has no viewer. Pattern-based finders mistake control lines and buf << f(this, ch) for function headers.
Some player text is data: socials, area strings, help bodies. It lives in XMLMultiString fields and never touches the catalog. Build the per-viewer message from the three-language constructor and give it to the MultiMessage overloads of oldact, pecho and recho.
Two traps:
-
XMLMultiString::fromXMLguesses the language of a legacy node with nolattribute from its content: Cyrillic is Russian, anything else is English. A wordless Latin line (***BONK***) lands in the English slot. - The Russian slot of a
MultiMessagehas no fallback.getMessagefalls back to Russian for an empty English or Ukrainian slot, but hands a Russian reader the Russian slot as it is. Combined with the first trap, a bareget(RU)prints a blank line to every Russian reader.
So build from getForLang(lang) (falls back Russian, then English), never from get(lang). Keep legacy const DLString & getters on getForLang(RU), that reproduces the old single-value behaviour.
Converting a field to XMLMultiString is source-compatible for reads and not for OLC editing. editor(argument, field, ...) wants a DLString &, so pass field[LANG] (see redit.cpp, runame and uaname) or give the edit session a language (socedit.cpp, language en|ru|ua). A header type change breaks callers in other plugins and a compile of the changed files will not show it. Grep the field name across *.cpp and *.h before you push.
Several helpers add a speech frame of their own and choose it by overload. tell_raw(ch, npc, const char *, ...) hard-codes the Russian frame "%^C1 говорит тебе '{G", on purpose, so that unwrapped callers stay byte-identical. The MultiMessage overload builds the frame in all three languages.
// English reader gets: The quest keeper говорит тебе '...'
tell_raw(pch, questman, "%s", text.getForLang(viewerLang(pch)).c_str());
// Frame localized, data stays an argument:
tell_raw(pch, questman, _("%s"), text.getForLang(viewerLang(pch)).c_str());"%s" has no catalog entry, so every language falls back to the Russian key, which is "%s" itself. The format is identical everywhere and only the frame changes. Keep the data as an argument. Building the format out of the data makes any % in the data a conversion.
The same trap exists when buf is already localized (fmt(ch, ...)) and is then passed as a plain char * to tell_raw: wrap it as MultiMessage(s, s, s) so the frame matches.
-
%Icount agreement replaces the oldGET_COUNT(n, "а", "ы", "")ending injection, which leaks the Russian ending into the other languages. The three forms are for 1, for 2 to 4, for everything else: Russianминут%1$Iа|ы|, Englishminute%1$I|s|s, Ukrainianхвилин%1$Iа|и|. A form ends at a space, so a multi-word branch needs two%I. -
%Gtakes neuter, masculine, feminine, plural in that order. English drops it and restructures the sentence to be genderless. Ukrainian keeps it. -
%P,%p,%Tuse a hard-coded Russian table. Never emit them in English or Ukrainian text, reword. For English use%nit|themand for Ukrainian%Gйого|його|її|їх. -
%Cwith a case digit is the character noun and%Othe object noun. Both localize per reader.%Nonly declines the string it is given, in whatever language that string is, so pass a per-language string (getForLang(viewerLang(ch)))
See Output-Format for the full code table.
The goal is that every output line is in one language. Do not ship a mixed line.
- A Russian-only declined noun inside a localized frame (
getShortDescr('6'),.ruscase('N'), a Flexer pad returned as a plain string). Wrapping only the frame givesOpens a lock on дверь., which is worse than leaving it Russian. Wait until the noun has a per-language renderer (getForLang, aMultiInflectedString, ortoNoun(viewer)). - Fixed-width tables with manual space padding. A localized header changes the width. Re-pad per language or defer. A screen-reader branch (
config screenreader) is needed anyway. - Broadcasts where the subject is not the recipient (clan talk, room echoes, announcements). They need a per-viewer primitive that takes a
MultiMessage, not a one-time wrap. Helpers that callfmt(NULL, ...)render once in Russian for everybody. - Command keywords in help or usage text. Localize only to words the parser actually accepts (check
dreamland_world/grammar/synonyms.json, which is JSON5, edit it as text). A dead Ukrainian keyword is an inaccuracy. Add the synonym first.
- The key is the Russian source string verbatim, the exact bytes the runtime looks up.
- Put the English where it already exists in the original DikuMUD, ROM or Anatolia text (Anatolia is the closest ancestor). Write fresh English otherwise.
- Merge under the
plug-ins/<dir>/<file>.cppkey indreamland_world/config/translations/plug-ins.json. - A composed sentence never resolves. The key is the finished Russian sentence, and anything assembled from fragments is not in the catalog. Write each combination as one whole
_()literal. Four booleans means four literals. - A catalog key is (file, Russian phrase), so one Russian word cannot carry two genders in one file. If the same bare word is used for a masculine and a feminine noun, give one site a distinct source phrase.
- Editing a Russian literal renames its catalog key. Move the English and Ukrainian values in the same change.
- Use ASCII punctuation. In catalog values write the Ukrainian apostrophe as ASCII
'. See L10n. - Run
scripts/lint-catalog-coverage.py <file.cpp>andscripts/lint-catalog-placeholders.pyfrom the world repository. The first lists wrapped strings without a complete entry, the second finds%codes that read a different argument type than the Russian key.
| Helper | Note |
|---|---|
transfer_char(ch, actor, room, mRoomLeave, mSelfLeave, mRoomEnter, mSelfEnter) |
The MultiMessage overload requires its first three message arguments, so message-less calls stay on the const char * overload. |
hint_fmt, echo_master, echo_char, say_fmt, tell_fmt, tell_raw
|
All have a MultiMessage overload. Use _() with them. |
direction_word, direction_doorname
|
Per-language direction and door words. English is caseless, so the English frame carries the preposition. |
Start here
Get running
Contribute
- Contribute
- Contributing Workflow
- Code Style
- Accessibility
- Writing Player Text
- Game Design Principles
Build areas
- Building
- OLC
- OLC Rooms
- OLC Mobiles
- OLC Objects
- OLC Resets
- OLC Line Editor
- OLC Other Editors
- Area Format
- Area Quests
- Mob Forms and Parts
Fenia
- Fenia
- Fenia Language
- Fenia Triggers
- Fenia Gotchas
- Fenia API
- Fenia Scripts
- Fenia Behaviors
- Fenia Skills and Spells
- Fenia Cookbook
- Fenia Grammar
Engine
- Engine Architecture
- Engine Time and Pulses
- Output Format
- Mechanics Combat
- Mechanics World
- Data Configs
- Item Power
Localization
Clients
More