Text And Localization
This guide documents the reusable FOnline Engine contract for raw .fotxt
files, prototype-localized $Text fields, language baking, runtime lookup, and
renderer-owned inline color tags. Use the generated
text-format reference and canonical JSON
model for the exact current-revision rule set.
Bitmap-font descriptors, slot binding, glyph coverage, measurement, wrapping,
and the complete renderer flag contract are owned by
Font Formats And Text Layout.
An embedding game owns its language priorities, pack catalog, semantic key names, translation workflow, and any formatter layered on top of retrieved strings. Engine documentation must not depend on one project’s packs or lexems.
Text pack model
Every text value is stored under a TextPackKey:
Collection + Key1 + Key2 + Key3
Collection is a TextPackName; the remaining fields are hstring values.
Language is deliberately not part of the key. The same key may therefore exist
in each baked language pack.
The backing container is a vector sorted by the complete key. Multiple adjacent
values under one key are variants. Every load, merge, and fallback-repair path
restores the order before returning; read methods verify the invariant and use
one binary-search range rather than mutating or repairing shared state. This
keeps concurrent reads safe. Game.GetTextCount(key) reports the variant count
and Game.GetText(key, index) performs zero-based indexed selection.
Raw .fotxt files
Name every source:
<TextPack>.<Language>.fotxt
The basename must contain exactly those two dot-separated segments. The Engine treats the language suffix as an opaque string and does not require a four-character locale code. Projects may adopt a naming convention, but it is not part of the reusable parser contract.
Each logical entry has three brace-delimited fields:
{Key1}{Key2}{Text}
For example:
{Welcome}{}{Welcome to the wasteland.}
{QuestName}{Short}{A difficult choice}
{LongMessage}{}{First line
Second line}
The filename supplies Collection; raw entries supply Key1, Key2, and the
value. Key3 remains empty. Collection and Key1 must be non-empty. Key2
and the text value may be empty.
Only the third field may continue across physical lines. The parser appends newline characters until it finds the first closing brace. There is no escape for a literal closing brace inside the value, and trailing data after the third field is not part of the entry.
Comment boundary
Raw text parsing has no comment-token grammar. A physical line is skipped only
when the parser cannot find the first opening brace. A project may use
brace-free headings or # lines for readability, but a supposed comment that
contains brace groups can become an entry or make the bake fail.
Treat this as a content-review rule:
- keep comments and headings free of
{and}; - demonstrate key shapes in fenced documentation, not inside
.fotxtcomments; - reject editor tooling that inserts annotation braces into text-pack sources.
Variants
Duplicate complete keys remain separate variants:
{Ambient}{Dust}{The wind scrapes across the road.}
{Ambient}{Dust}{A sheet of dust hides the horizon.}
The script API does not choose randomly by default. Game.GetText(key) selects
the first variant. For deliberate random selection,
query Game.GetTextCount(key), select an index in project code, and pass that
index to Game.GetText(key, index).
Do not assume that every language has the same number or ordering of variants. Language normalization aligns key presence, not duplicate cardinality.
Language normalization
Baking.BakeLanguages is ordered and must not be empty. Each entry is either a
bare language or child:parent. The first entry is the default base language;
an explicit parent must appear earlier, and language names must be unique. A
bare later language falls back to the first language. For example,
russ engl ru18:russ en18:engl creates two adult-overlay fallback chains. The
Engine default is engl; embedding projects normally override it in their
.fomain.
For a changed raw pack, TextBaker gathers all configured language files for
that pack. The base-language source must be present. Unsupported filename
suffixes are warned and skipped.
TextPack::FixPacks then normalizes every non-base language against its explicit
parent, or against the first language when no parent is declared:
- remove languages not listed in
Baking.BakeLanguages; - add missing configured languages;
- remove packs that do not exist in the selected fallback language;
- copy packs missing from the child language;
- add keys missing from a child pack using fallback-language values;
- remove keys that are absent from the fallback pack.
This fallback is completed during baking. Runtime lookup does not consult the base language after a binary pack is loaded.
The binary name is:
<ResourcePack>.<TextPack>.<Language>.fotxt-bin
TextPack::LoadFromResources requires exactly those three basename segments.
Prototype $Text fields
Prototype sections can author localized values without separate raw text files:
[ProtoItem]
$Name = LaserRifle
$Text engl Name = Laser rifle
$Text engl Desc Short = Compact description
$Text russ Name = Localized name
The grammar is:
$Text [Language] [Key2] [Key3] = Value
There may be at most four key tokens including $Text. The prototype id
becomes Key1. When Language is omitted, the field uses the first
Baking.BakeLanguages entry. Values pass through
StringEscaping::DecodeString, so sequences such as \n become actual
newlines.
Parent $Text fields are collected recursively before child fields. A child
inherits missing exact keys and replaces an inherited value when it defines the
same $Text Language Key2 Key3 key.
ProtoTextBaker emits five packs for every configured language:
| Prototype type | Generated pack |
|---|---|
| Item | Items |
| Critter | Critters |
| Map | Maps |
| Location | Locations |
Other non-exported entity or fixed type with HasProtos |
Protos |
All five outputs exist even when some are empty. Unsupported $Text languages
are warned and omitted. If multiple prototype sources would generate the same
complete key in one pack and language, baking fails instead of depending on
iteration order.
Runtime script API
The generated method reference owns exact exported signatures. The important behavioral contract is:
| API | Sides | Behavior |
|---|---|---|
Game.GetLanguage() |
server, client, mapper | Return the current Language setting as LanguageName. |
Game.GetText(key) |
client, mapper | Return the first variant from the current language. A missing key returns an empty string. |
Game.GetText(key, index) |
client, mapper | Return the zero-based indexed variant from the current language. Missing or out-of-range returns an empty string; negative index throws. |
Game.GetText(langName, key) |
client, mapper | Use the current pack when langName is empty or current; otherwise load/cache that language and return its first variant. No runtime fallback is applied for an absent non-empty language. |
Game.GetTextCount(key) |
server, client, mapper | Return variant count, or zero when absent. |
Game.IsTextPresent(key) |
server, client, mapper | Return whether at least one variant exists. |
Game.ChangeLanguage(langName) |
client, mapper | Replace the current runtime pack; the immutable startup setting is unchanged. |
The client loads Client.Language during startup, then owns the current language as live engine state exposed by Game.CurrentLanguage / Game.GetLanguage(). Game.ChangeLanguage does
not validate the identifier and does not invoke a game GUI refresh callback.
An embedding project owns its allowed-language selector, persistence policy,
and refresh/rebuild sequence.
The server loads one pack from the startup Client.Language setting and then owns the same current-language state. It exposes only presence and
count queries to scripts; there is no server-side script Game.GetText
overload in the Engine contract.
Engine and project formatting boundary
TextPack stores opaque strings. The Engine does not define @pname@,
@nname@, @sex@, @rnd@, @arg@, @text@, variant separators, named
argument serialization, or dialog-specific lexem expansion. Those features,
when present, belong to the embedding project’s scripts and documentation.
The client font renderer does own inline color tags. The exact byte-order and flag interaction is also pinned in the generated font rendering reference:
@color:BBGGRR@
@color:AABBGGRR@
@color:0xBBGGRR@
@color:0xAABBGGRR@
@color@
Six hex digits set a color without an explicit alpha byte; eight include alpha.
The empty @color@ tag restores the previous color. Tags are stripped during
font formatting. With FontFlag.NoColorize, valid tags are still stripped but
all text uses the base color.
If a project adds another formatting pass, document and test:
- which side performs it;
- whether it runs before or after text retrieval;
- whether nested lookups are allowed;
- escaping and malformed-input behavior;
- random-selection ownership;
- interaction with renderer color tags.
Authoring workflow
- Choose the owning text pack and a semantic complete key.
- Author the base language first.
- Add configured translations without inventing keys absent from the base.
- Use duplicate keys only when the caller deliberately handles variants.
- Keep raw
.fotxtcomments brace-free. - Use prototype
$Textfor prototype-owned names and descriptions. - Bake resources and treat every parser, missing-base, or intersection error as a source-content failure.
- Exercise language switching and every project formatter in a visible client.
Validation workflow
Engine maintainers changing TextPack, TextBaker, ProtoTextBaker, language
settings, script text methods, or inline color parsing must update
BuildTools/TextFormatInterface.json, this guide, and generated outputs in the
same change:
python BuildTools\docs_text_format.py --write
python BuildTools\docs_text_format.py --check
python -m unittest BuildTools.tests.test_docs_text_format
Run focused native tests for TextPack, TextBaker, and ProtoTextBaker when
their behavior changes. Validate color-parser changes through the nearest
rendering tests and a visible client. Then rebake an embedding project.
Project-owned pack catalogs, translation guards, lexem formatters, and GUI
refresh behavior require project tests rather than Engine fixtures.
Maintenance routing
- raw syntax, key identity, variants, binary loading, or normalization:
Source/Common/TextPack.*; - filename selection, incremental pack completion, or raw output:
Source/Tools/TextBaker.cpp; - prototype
$Text, inheritance, pack routing, or intersections:Source/Tools/ProtoTextBaker.cpp; - script lookup and switching:
Source/Scripting/*GlobalScriptMethods.cppplusSource/Client/Client.cpporSource/Server/Server.cpp; - inline color tags and all font layout/rendering behavior:
Source/Client/FontManager.*and Font Formats And Text Layout; - project pack names, language order, translations, lexems, or GUI refresh: the embedding project.