name: text
description: Use when a sysop wants to customize the text strings/prompts Synchronet sends to remote users — the runtime text[] string database. Covers ctrl/text.dat syntax (Ctrl-A codes, @-codes, %-specifiers, mnemonics, the decimal-\NNN-vs-hex-\xNN trap, multi-line continuation, %-vs-@-code mutual exclusion, missing-line fallback to compiled default), ctrl/text.ini override file (three sections: by-ID overrides, [substr] global substitution, [JS] for gettext() strings), ctrl/text.<lang>.ini per-language overlays, and the recycle-required consequence. For display files see menus; for gettext()/js.load_text() see javascript; for recycling see control. Trigger on "change a BBS prompt", "translate strings", "override text.dat without editing it", "what's text.ini", "retheme the BBS colors", "replace \1g with \1m everywhere" (or any global Ctrl-A code substitution), or "why didn't my text.dat change take effect".
Synchronet — customizing terminal-server text strings
Reference (authoritative):
text.datformat and syntax: https://wiki.synchro.net/custom:text.dattext.iniruntime overrides: https://wiki.synchro.net/custom:text.ini- Localization (language overlays): https://wiki.synchro.net/custom:localization
- Ctrl-A attribute codes: https://wiki.synchro.net/custom:ctrl-a_codes
- @-codes (variable substitution): https://wiki.synchro.net/custom:atcodes
- Mnemonics (
~-prefix highlighted keys): https://wiki.synchro.net/custom:mnemonics gettext()JavaScript helper: https://wiki.synchro.net/custom:javascript:lib:gettext.js- Related — display files (
.ans/.msg/etc.): see themenusskill. - Related — JS
gettext()/js.load_text(): see thejavascriptskill. - Related — making changes take effect: see the
controlskill.
This skill is the string-database side of Synchronet customization. The display-file side (the full-screen .ans/.msg/.utf8 art and menus in text/ and text/menu/) belongs to menus. Both layers share the same Ctrl-A and @-code rendering vocabulary, but the files, workflows, and traps are different.
The two-layer model
Every Synchronet string the Terminal Server (and other servers) sends to a user resolves through a runtime text[] array, indexed by string ID. The array is populated in this order:
┌─────────────────────────────────────────────────────────────┐
│ 1. COMPILED-IN DEFAULTS (text_defaults.c, baked into │
│ sbbs.dll / libsbbs.so by the build's textgen step) │
└────────────────────────────┬────────────────────────────────┘
│ on startup, replaced by:
┌────────────────────────────▼────────────────────────────────┐
│ 2. ctrl/text.dat (sysop's file; lines present here win │
│ over compiled defaults; missing/commented lines fall │
│ back to compiled default) │
└────────────────────────────┬────────────────────────────────┘
│ then optionally overlaid by:
┌────────────────────────────▼────────────────────────────────┐
│ 3. ctrl/text.ini (v3.20+ runtime override; by-ID and │
│ [substr] and [JS] sections) │
└────────────────────────────┬────────────────────────────────┘
│ then per-user language overlay:
┌────────────────────────────▼────────────────────────────────┐
│ 4. ctrl/text.<lang>.ini (loaded when useron.lang matches │
│ the LANG: value in the file) │
└─────────────────────────────────────────────────────────────┘
As a sysop, prefer the top of the stack that does the job. For one-off customization, use text.ini (your changes survive upgrades and don't get overwritten when text.dat ships an update). Edit text.dat directly only when you're maintaining a heavily-customized BBS where the override file would be unwieldy.
Adding entirely new string IDs is developer territory, not sysop customization — it requires editing text.dat and C/C++ source that calls text[NewIdentifier], and regenerating the auto-generated headers with the textgen build tool. That workflow lives in the project's developer instructions, not in this skill.
text.dat — the file format
The wiki page (custom:text.dat) is the authoritative reference. The points below are the ones sysops most often trip over.
Line format
"<value>" <ID-number> <IdentifierName>
Everything after the terminating double-quote is comment — the wiki notes the trailing <ID> and <IdentifierName> are conventional but not required by the parser. (For consistency and so future grepping by identifier name works, leave them in place when you modify a line.)
Escape sequences
| Sequence | Means |
|---|---|
\\, \?, \', \" |
literal \, ?, ', " |
\xNN |
byte by hexadecimal (\x00..\xFF) |
\NNN |
byte by decimal (\000..\255) — note: not octal, unlike C |
\r \n \t \b \a \f \v |
the usual ASCII control characters |
\1X |
byte 0x01 followed by X — this is the Ctrl-A code form (\1n = normal, \1h = high-intensity, \1c = cyan, etc., see the Ctrl-A wiki) |
The trap. \NNN is decimal, not octal. The wiki calls this out directly: writing "\14" to try to set background color 4 (\1 is Ctrl-A, 4 should be a parameter) doesn't do what you'd guess — it's parsed as decimal 14, which is a Ctrl-N (shift-out). Use "\x014" instead — hex \x01 (Ctrl-A) followed by literal 4. Most stock text.dat lines use the \1X shorthand for Ctrl-A; reach for \xNN only when you need an arbitrary byte.
Multi-line strings
A trailing \ after the closing double-quote continues the string on the next line:
"\1n\1h\1c\xda\xc4\xc4\xc4\xc4\xc4\xc4"\
"\1n\xc4\xc4\xc4\xc4\xc4\xc4"\
"\1h\1k\xc4\xc4\xc4\xc4\xc4\xc4\xc4"
The strings concatenate. Max line length is 255 chars; max total string length is 2000 chars.
Mnemonics (~-prefix)
A ~ before a character marks it as a command key. On ANSI terminals it gets highlighted; on non-ANSI it gets parenthesised:
"~Yes, ~No, or ~Quit: "
becomes Yes, No, or Quit: with Y/N/Q highlighted, or (Y)es, (N)o, or (Q)uit: on non-ANSI. Colours come from ctrl/attr.cfg. See custom:mnemonics.
printf %-specifiers
Many lines embed dynamic content via %s, %d, %u, etc. — fed by the calling C code. Don't reorder, add, or remove them when modifying a line; the C code expects exactly the specifiers in the order they appear.
To suppress a %s argument from display without removing it (so the C code's argument list stays balanced): change %s to %.0s (precision-zero — prints zero characters of the string).
"User: %s, Level: %d" → keeps both
"User: %.0s, Level: %d" → hides the username, still consumes the %s argument
Mutually exclusive: %-specifiers and @-codes
Lines that contain %-specifiers cannot also contain @-codes (security rule, called out on the custom:text.dat wiki page). If you need both dynamic data and an @-code, use one of them — typically replace the %s with an @-code if the value is exposed via the @-code system, or vice versa.
Suppressing a whole line
Setting a text.dat entry to an empty string "" suppresses display of that line at runtime (most call sites tolerate an empty string).
Missing / commented lines
Any string commented out with # (or simply missing from the file) is replaced by the compiled default from text_defaults.c. This means a minimal text.dat containing only your modifications is perfectly valid — you're not required to keep every stock string.
Tooling gotcha — editors that classify .dat as binary
ctrl/text.dat is plain ASCII, but tools that classify by file extension (e.g. some AI-coding tooling's Read/Edit tools that refuse .dat) won't open it. Workaround:
cp <sbbs>/ctrl/text.dat /tmp/text_dat.txt
# edit /tmp/text_dat.txt
cp /tmp/text_dat.txt <sbbs>/ctrl/text.dat
(Some sysops keep ctrl/text.txt as a working copy for the same reason.)
text.ini — the v3.20+ override file (recommended for sysop customization)
The wiki page (custom:text.ini) is the authoritative reference. The bullet form below summarises the file's three independent sections.
text.ini is optional. If absent or empty, the runtime uses text.dat (or the compiled default for missing entries) as-is. If present, its contents are layered on top of text.dat at startup.
Why prefer text.ini over editing text.dat:
- Your customizations survive Synchronet upgrades —
text.datmay ship updates;text.iniis sysop-owned and never overwritten. - Order doesn't matter — group your overrides by topic, not by stock-file order.
- Only your changes need to live in the file — no need to keep a full copy of the stock strings.
- Customizations also reach other servers (not just the Terminal Server), which Baja/JS-based runtime overrides can't.
Section 1: by-ID overrides (no […] header)
The default section at the top of the file overrides individual text.dat strings by identifier name:
; ctrl/text.ini
Email: "\1_\1?\1c\1hE-mail (User name or number): \1w"
; without quotes also works, but quotes preserve leading/trailing whitespace
NoDOS = This BBS can't run DOS doors; connect via SSH instead.
- The key is the identifier name from
text.dat(e.g.Email,NoDOS,NodeStatusWaitingForCall), not the numeric ID. ID: valuesyntax (colon) is required if the value contains control characters (Ctrl-A, embedded escapes);ID = value(equals) is fine for plain text. Seeconfig:ini_files#string_literals.- Double-quotes around the value are optional but required to preserve leading/trailing whitespace.
- No multi-line continuation — each override is a single line, max 1023 chars.
- The same escape sequences as
text.datapply (\1Xfor Ctrl-A,\xNNfor hex bytes,\r/\n, etc.). - Lines starting with
;are comments.
Section 2: [substr] — global substring substitution
[substr]
\1g: \1m
Group: Area
"old phrase": "new phrase"
Every string that goes out to a user — from any source (text.dat, text.ini overrides, JavaScript-built output) — is passed through these substitutions before being sent. The example above:
- Replaces every Ctrl-A green (
\1g) with Ctrl-A magenta (\1m) — a quick way to retheme without touching individual entries. - Replaces every occurrence of the word
GroupwithArea.
The trap: [substr] is global and unconditional. Every output line is rewritten. Useful patterns:
- Wholesale colour retheming (e.g. green→magenta, blue→cyan).
- Renaming a term consistently (e.g. "Sysop" → "Admin", "Sub-board" → "Forum").
- Suppressing a recurring phrase (replace with empty).
Risky patterns to avoid:
- Replacing common short tokens. Replacing
towithforwill hit every "to" anywhere — including inside other words ("automatic" → "auformatic"). Use longer, more specific patterns. - Case-sensitive substitution. Matches are case-sensitive;
Groupwon't matchgrouporGROUP. - Quoting matters for whitespace. If you want to substitute literal whitespace, quote the value.
Section 3: [JS] (also [default.js]) — JavaScript gettext() overrides
For strings emitted from JavaScript via gettext():
[JS]
Find Text in Messages = Find a string in messages
You have @USERMAIL@ message(s) waiting. = You've got mail!
- The key is the literal English source string as passed to
gettext(). - For this to take effect, the script must
require("gettext.js")(orload("gettext.js")per the older idiom) — seecustom:javascript:lib:gettext.jsand thejavascriptskill. - A
[default.js]-style section can scope overrides to a specific JS module (default.js, the main shell); other module-named sections work similarly.
text.<lang>.ini — per-language overlays
For non-English users, the runtime loads ctrl/text.<lang>.ini when the user's useron.lang matches the file's LANG: declaration. The format is identical to text.ini: by-ID overrides, [substr], [JS] — all in the target language.
Stock examples ship for German, Spanish, and French (text.de.ini, text.es.ini, text.fr.ini). They start with:
LANG: Deutsch
Language: Sprache
On: Auf
Off: Aus
Yes: Ja
No: Nein
...
The LANG: line is the human-readable language name that the user sees when selecting a language; the rest are per-ID overrides.
Language selection is per-user — see custom:localization for the user-side language preference flow and the parallel text/menu/<lang>/ directory for translating display files (which is the menus skill's territory).
CP437 is the native character set; the wiki's localization page is honest about the limits for non-Western scripts and the UTF-8 work-in-progress.
Making changes take effect
text.ini (and text.<lang>.ini) overrides are loaded at server startup. After editing, you need to either:
- Recycle the server(s) that use the strings (Terminal Server for almost everything, other servers if your overrides include their strings). The cross-platform way:
touch <sbbs>/ctrl/recycle(all servers) ortouch <sbbs>/ctrl/recycle.term(just the Terminal Server). See thecontrolskill for the full menu of mechanisms. - Have users log off and back on — minimum requirement; some text is only loaded once per server lifetime, but per-session text is re-read on each login.
text.dat edits work the same way — startup parses the file, so a recycle is required for changes to be visible.
Quick recipes
"Change the colour of the e-mail prompt from bright blue to bright cyan."
; ctrl/text.ini (default section, top of file)
Email: "\1_\1?\1c\1hE-mail (User name or number): \1w"
Then touch <sbbs>/ctrl/recycle.term.
"Replace every occurrence of 'Group' with 'Area' in every BBS message."
; ctrl/text.ini
[substr]
Group: Area
"Retheme green to magenta system-wide."
[substr]
\1g: \1m
"Translate a single JavaScript prompt without touching any .js file."
; ctrl/text.de.ini (German overlay)
LANG: Deutsch
...
[JS]
Press any key to continue = Eine Taste drücken zum Fortfahren
The JS code must use gettext("Press any key to continue") rather than the bare string for this to apply.
"Find the identifier name for a stock prompt I want to override."
grep -F 'the prompt text I see' <sbbs>/ctrl/text.dat
The line will end with the ID and IdentifierName. (If your text.dat is minimal, grep src/sbbs3/text_defaults.c in the source tree instead — those are the compiled defaults.)
Common mistakes
- Editing
text.datfor a single colour tweak whentext.iniwould do. The next Synchronet update may overwrite yourtext.datchanges if a new line is added or fixed;text.inisurvives. The wiki'scustom:text.inipage is direct about this preference. "\14"for "Ctrl-A code 4". It's not —\NNNis decimal, so\14is the byte0x0E(Ctrl-N), not Ctrl-A +4. Use"\x014"or"\1" "4"instead.- Adding @-codes to a line that contains
%s/%d. The two are mutually exclusive; the @-code won't be expanded. Pick one or split the line into pieces. - Reordering
%-specifiers. Each one is consumed by the calling C code's printf arguments in order. Reorder them and you'll print the wrong values or crash. [substr]replacements that are too short.to: forwill hit every "to" anywhere — inside other words, in URLs, in user names. Use distinctive multi-character tokens.- Forgetting to recycle.
text.iniis loaded at startup. After editing,touch <sbbs>/ctrl/recycle(or the per-server variant) — seecontrol. - Multi-line value in
text.ini. Values must be on one line, max 1023 chars. The\-continuation that works intext.datdoes not work intext.ini. - Using
ID = valuefor a value with control characters. UseID: value(colon) instead — seeconfig:ini_files#string_literalsfor why. - Expecting
gettext()overrides to "just work". The script mustrequire('gettext.js')and callgettext(theString). A bare string literal in JS isn't routed through the override table. - Translating only
text.<lang>.iniand forgettingtext/menu/<lang>/. The two cover different surfaces — prompts vs. display files. Translate both for a coherent non-English experience. (Seemenus.) - Confusing the
text.datidentifier with the numeric ID.text.iniuses the identifier name (e.g.Email), not the number (e.g.010). The number is a comment; the identifier is the key.
Cross-references
- Display files (
.ans,.msg,.asc,.rip,.utf8intext/andtext/menu/;data/subs/<code>.*info files;mods/text/overrides; per-languagetext/menu/<lang>/): themenusskill. - JavaScript-emitted strings (
gettext(),js.load_text(), the JS dialect): thejavascriptskill. - Making your changes take effect (
ctrl/recycle,ctrl/recycle.term, signals, MQTT control topics): thecontrolskill. - Where the runtime writes about loading these files (look for "Loading text.ini" lines on startup): the
logsskill — console stream / syslog / journal. - Source of truth in the codebase:
src/sbbs3/load_cfg.creadstext.iniat startup;src/xpdev/ini_file.cis the parser. The compiled defaults live in the auto-generatedsrc/sbbs3/text_defaults.c(built fromtext.datbytextgen.c).