Skip to content

Translations/themes#28

Draft
amnuts wants to merge 80 commits into
developfrom
feature/translations
Draft

Translations/themes#28
amnuts wants to merge 80 commits into
developfrom
feature/translations

Conversation

@amnuts
Copy link
Copy Markdown
Owner

@amnuts amnuts commented May 12, 2026

Got Claude Code to look into something that I've thought for a while (even since I had to rework Way Out West to be on a newer version of Amnuts) would be a good thing; an easier way to change the text in the talker for either translations or changing the style/tone of the output.

So here's a small set of changes as a start to that. At this point, almost exclusively untested!

amnuts and others added 30 commits May 10, 2026 16:11
Captures the agreed design for per-user language/theme support: catalog
mechanism for source-code literals, file-path resolution with fallback for
on-disk content, frame primitives in strings.yml so themes can redefine
box edges and rule patterns, and a six-phase migration that keeps the
talker working at every step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MOTDFILES is the actual constant; ADMINFILES is a sixth translatable
category that was missed during brainstorming. Both corrections are
discovered codebase facts, not design changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers the first phase of localisation: vendor libyaml, move six content
categories under files/langs/en_GB/, implement the file-path resolver
with user-locale fallback to default, and sweep every call site. Each
of the six categories is converted in its own commit and the talker
stays bootable throughout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
No functional code uses libyaml yet; this is build-system prep for the
string-catalog mechanism in Phase 2 of the localisation plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The guard in yaml_private.h is #if HAVE_CONFIG_H; the macro must be
defined BEFORE config.h is opened for the include to fire. The Makefile
flag -DHAVE_CONFIG_H is what actually makes that work. The define inside
config.h itself was dead and misleading — removing it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
No implementation yet; declarations only. Lets later tasks reference the
locale_*() functions without forward-decl clutter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
locale.h declared struct locale_catalog and struct locale_state as opaque
forward decls but no function signature in the header references them.
They are deferred to Phase 2 (catalog) and Task 5 (locale_state in
globals.h) respectively. Removing the dead scaffolding from the header.

The plan's file-summary listed locale_discover and locale_set_user as
prototypes to add in Task 2; the concrete step did not. The step is
correct — locale_discover is internal, locale_set_user is Phase 2.
Fixed the summary to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New INIT-section key; defaults to en_GB if absent. Stored in
amsys->default_locale, ready for the locale resolver to consume.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use strcpy after the bounds guard (matching all other string fields
in parse_init_option), use *initopt + the "too long" template for the
error message, and move the INITOPT_LIST entry out of the room-defaults
cluster.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Builds <LANGS_ROOT>/<default_locale>/<category>/<name> and stats it.
Returns 1 if present, 0 otherwise. No callers yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
<sys/types.h>, <dirent.h>, and <string.h> were forward-looking for Task 5
directory enumeration; only adding them when the consumer code lands
keeps the TU honest with the codebase's "include what you use"
convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Enumerates files/langs/* directories into amsys->locales. Not yet
called from boot — that wires in once the directory tree exists.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three fixes from review:
  - Replace magic 64 with MAX_LOCALES (defines.h, two call sites).
  - Track default_seen before the cap guard so a default locale
    enumerated past the cap is not falsely reported as missing.
  - Reject overlong names in is_valid_locale_dirname so the strncpy
    cannot silently truncate a name that does not match any real dir.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds user->locale field (zero-init via existing calloc). Phase 1 leaves
the field empty at runtime, so locale_path behaves as locale_default_path.
Phase 2 will wire up persistence and the set lang command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
locale.h duplicated declarations from prototypes.h with no other
content — single-source-of-truth convention says prototypes.h wins.
Doc comments folded into locale.c above each function definition.

user->locale relocates from the identity-fields cluster to the
string-preference cluster (alongside in_phrase/out_phrase) where it
belongs semantically and where Phase 2 save/load logic will expect it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Complete inventory of every reference to ADMINFILES/DATAFILES/HELPFILES/
MISCFILES/MOTDFILES/TEXTFILES in source. Drives the sweep tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Locks in the shape every sweep task uses so style doesn't drift across
the six per-category commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The six translatable category directories move under files/langs/en_GB/.
The category constants stay absolute for now, pointing at the new
locations, so all existing fopen sites continue to work. Tasks 11-16
flip each constant to a bare name as its call sites are converted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Discovery + default-locale validation runs after config parsing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every ADMINFILES call site now resolves through locale_*_path; the
constant flips from an absolute path to the bare category name.

The two ADMINFILES sites listed in the Phase 1 audit ledger live at
src/commands/display.c:32 and src/commands/display.c:61. Both compose
paths of the form "<TEXTFILES>/<ADMINFILES>/<file>", where ADMINFILES
is being used as a subdirectory name under the textfiles tree rather
than as a top-level category. With ADMINFILES now equal to the literal
string "adminfiles", the existing sprintf calls still produce the
correct path (TEXTFILES remains absolute until Task 16). The
structural conversion of these lines to locale_path will be performed
in Task 16 alongside the TEXTFILES sweep, since the two constants are
entangled on the same lines and converting one without the other would
yield a doubled path that won't resolve.

Verified: grep "ADMINFILES.*%s" returns no matches.

Build deferred: local environment lacks clang/make and the docker
daemon is not currently available. The user will verify on their
normal environment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Includes the boot-time config load and room-file loaders. The constant
is now a bare category name; absolute path resolution moves entirely
into the locale helpers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 9 extended MISCFILES from "files/miscfiles" (15 chars) to
"files/langs/en_GB/miscfiles" (27 chars) but suggestions.c declared its
filename buffer as 30 bytes, which is just too small for the new path
+ "/suggestions" + NUL = 40 bytes. Stack overflow on every suggest/rsug
invocation.

Resize to 80 to match every other file. The MISCFILES sprintf sites in
this file remain unchanged — those convert to locale_*_path in Task 14.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Uses the per-user resolver so future non-default locales automatically
pick up their translated help articles.

Build deferred: local environment lacks clang/make and the Docker
daemon is not currently available. The user will verify on their
normal environment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Routes rules/news/wizrules through locale_path (per-user lookup so a
non-default user locale picks up its translated copy) and routes the
suggestions board, hangman dictionary and boot-time suggestion count
through locale_default_path (shared server state, default locale).

Build deferred: local environment lacks clang/make and the Docker
daemon is not currently available. The user will verify on their
normal environment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MOTD now resolves via the per-user helper for post-login displays,
ready for per-locale MOTDs once Phase 2 wires up set lang. Pre-login
MOTD and count_motds use default-locale resolution since no user
context is available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Includes the deferred display.c sites where TEXTFILES and ADMINFILES
were entangled on the same line; ADMINFILES now appears as the
subdirectory component of the name argument passed to locale_path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a partial fallback test locale (one overridden motd) so the
fallback resolution can be smoke-tested when the talker is run in a
working build environment. Seed the migration ledger.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The locale-aware path "files/langs/<locale>/<cat>/<file>" is ~27 chars
longer than the pre-Phase-1 paths. Buffers at 80 bytes still avoid
overflow (snprintf truncates safely) but truncation manifests as silent
fopen failures with vague errors. Upsizing to 128 in admin.c and 160 in
display.c's admin-files branch gives comfortable headroom for any
realistic locale name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce struct lang_entry and struct locale_catalog, extend
struct locale_state with a catalog table, and add a non-owning catalog
pointer to UR_OBJECT. Pure type declarations; no behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the dangling CATALOG_BUCKETS reference (the constant lives in
catalog.c, not in any header) and document default_index's pre-init
sentinel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
amnuts and others added 27 commits May 12, 2026 12:32
align_into's prototype references enum align_value; without the
include, gnu23 -Wpedantic would warn at the prototype site (the
enum isn't visible until uibuilders.h is included separately).
Including it here keeps prototypes.h self-contained.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Default frame appearance matches the existing +----+ boxes used
throughout the talker. Phase 3 builders consume these; Phase 4
conversions of frame-heavy commands then become byte-identical for
en_GB users while remaining themeable for non-default locales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Composes lcap + fill-pattern + optional inline " label " + remaining
fill + rcap over a fixed visible width. Every length calculation
goes through visible_strlen so colour escapes don't push the right
border.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ox_close

Stateful box rendering with opaque BOX handle. Top/separator/bottom
edges drawn from ui.box.{top,sep,bot}.* keys; body rows padded via
align_into so the right border doesn't drift on coloured cells.
box_close frees the handle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tables share the box frame and split inner width into N
visible-column columns. table_emit_row wraps each cell on word
boundaries (hard-break mid-word as a fallback) and emits multiple
body lines until every cell is consumed. Header gets a separator
underneath; close delegates to box_close.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Overrides only meta.* and ui.* — everything else falls back to en_GB.
Used by the Phase 3 pilot to demonstrate end-to-end themed frames
under a non-default locale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First end-to-end consumer of the locale catalog + lang_* API. Every
server-authored literal in wiz_list() is lifted into wizlist.* keys
in files/langs/en_GB/strings.yml; en_GB output is byte-identical to
before, and a themed locale can re-skin the section dividers by
overriding the wizlist.frame.* keys.

Notable deviation from the rough pattern in the Phase 3 plan: the
wizlist body has no |...| side rails, so the per-section frames are
stored as opaque catalog values rather than rendered through box_open
/ rule(). A table_open pass would have shifted bytes. Variable-width
formatting (%-*.*s with widths derived from teslen) is performed
locally with snprintf before the result is handed to the catalog
format string as a plain %s, because the catalog signature framework
rejects %* specifiers.

Ledger updated with a "Wizlist (Phase 3 pilot)" section so the sweep
table has a home before Phase 4 Task 1 promotes it formally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mark Phase 3 converted in the migration ledger and append a
verification block describing the smoke-test steps for a Linux/macOS
host. The wizlist pilot section (already present from Task 8) is left
in place as the canonical example of the conversion pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lock down the six-step checklist every command conversion follows so
the sweep stays consistent. Seed the ledger with the Phase 4 commands.
Notes the wizlist-pilot lessons: opaque frame literals where no side
rails exist; pre-format %* widths with snprintf since the catalog
signature validator rejects them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the inline frame-drawing in show_igusers (in src/amnuts.c) in
favour of the Phase 3 box_open/box_line/box_separator/box_close
pipeline, and lift every server-authored literal — the title, the
per-name cell format, and the empty-state message — into
show_igusers.* keys in files/langs/en_GB/strings.yml.

The row body is assembled by concatenating show_igusers.name_cell
(a single " %1$s" cell holding a pre-snprintf'd 24-col-padded name)
up to three times per logical row, then handed to box_line; the
builder's ALIGN_LEFT pad fills out the box's 76 inner visible
columns, reproducing the trailing-space padding the original
sprintf chain produced manually. The title catalog value carries
a load-bearing leading space to preserve the "| <content>" inset
of the pre-conversion byte stream.

Output on en_GB is byte-identical to the pre-conversion command;
themed locales can re-skin both the frame primitives (via ui.*)
and the visible strings (via the show_igusers.* overrides).

Ledger row in docs/superpowers/specs/localisation-migration.md
flipped to converted (2026-05-12).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the inline frame-drawing in grep_users in favour of the Phase 3
box_open/box_line/box_separator/box_close pipeline, and lift every
server-authored literal — the four early-exit messages, the title row,
the per-match body row halves, the orphan-row padding, the empty-state
message, and the summary footer — into grepusers.* keys in
files/langs/en_GB/strings.yml.

The 78-col frame, title row, empty-state rows, and summary footer are
all 78 visible cols wide and survive the box_line pipeline at
inner = 76 cols, including byte-identical reproduction of the
align_string(ALIGN_LEFT, 78, 1, "|", ...) trailing line — box_line's
ALIGN_LEFT pad fills the inner span the same way align_string's
%*.*s pad did, and the leading space inside grepusers.title /
grepusers.empty_message / grepusers.footer is load-bearing (it
reproduces the first inset byte that align_string used to overwrite
with the marker).

The per-match body rows are NOT handed to box_line. The original
layout deliberately overruns the frame: two halves are concatenated
for a paired row at 86 visible cols between the outer rails, and a
dangling first half is padded out to 82 visible cols. These row
pieces ship as opaque catalog values (grepusers.row_first /
grepusers.row_second / grepusers.row_orphan_pad) that the C side
concatenates and hands to write_user directly, so byte-identity is
preserved without lying to box_line about the inner width per row.

Names and level labels are pre-padded with snprintf so the row
signatures stay plain %s — the catalog signature validator does not
accept width-via-arg (%-*s) specifiers (wizlist pilot lesson).

Output on en_GB is byte-identical to the pre-conversion command;
themed locales can re-skin both the frame primitives (via ui.*) and
the visible strings (via the grepusers.* overrides), with the row
columns themselves staying server-side since shifting them needs
matching C-side row assembly.

Ledger row in docs/superpowers/specs/localisation-migration.md
flipped to converted (2026-05-12).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
defines.h has had #define ALIGN_LEFT/CENTRE/RIGHT macros since long
before Phase 3, used by the pre-existing align_string() in
src/strings.c. Phase 3's enum align_value redeclaration collided
with the macros at every translation unit that included both
headers — preprocessor expanded the enum identifiers into integers,
producing a syntax error.

Drop the enum entirely; use int consistent with the existing
align_string convention. ALIGN_LEFT et al. remain macros in
defines.h and are now the single source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lift every server-authored literal in the `lban` WIZ command into the
listbans.* keys of files/langs/en_GB/strings.yml. The four subcommand
banners (sites/users/swears/new) ship as opaque catalog frame values
rather than being synthesised through box_open: the original layout
has no |...| body rails, just a "~BB*** ... ***" header followed by
either paged file content or an inline swear-word list, so a
box_open / box_line pass would shift bytes. The wizlist pilot's
un-railed-frame pattern applies cleanly here.

The three subcommands that page their bodies via more() against
SITEBAN / USERBAN / NEWBAN are deliberately untouched: paged file
content is outside the catalog surface and already routes through
the Phase 1 locale-aware locale_default_path resolver.

Quirk preserved: the source's `if (strcmp(word[1], "new"))` is
inverted, so the usage key only fires when the user types `lban new`
exactly; any other unrecognised arg (or the no-arg case) falls into
the new-bans banner.

Ledger row for listbans flipped from `pending` to `converted`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lift every server-authored literal in the `system` admin command into
system.* keys in files/langs/en_GB/strings.yml — five sections (header,
users, netlinks, rooms, memory) plus the port-info header matrix, the
inner separator, the "For other options" footer, the "Netlinks not
compiled" notice, and the usage line. 57 keys in total.

The five sections chain through one box_open / box_line / box_separator
/ box_close pipeline at 78 visible cols total (inner = 76). When `-a`
is given, all five sections render as one continuous box; for the
standalone `-u`/`-n`/`-r`/`-m` switches, only the relevant section is
drawn (still inside a freshly-opened-and-closed box of its own). The
no-arg case is Section A plus a "For other options" footer mini-section.

Two divergences from the standard box flow are preserved:

  1. The inner separator that follows each section's title row uses
     `|` caps, not the `+` caps box_separator emits. It therefore ships
     as the opaque catalog literal `system.inner_sep` and is written
     direct via write_user — there's no way to coax box_separator into
     producing `|---|` without breaking other commands.

  2. The port-info header has 8 NETLINKS x IDENTD-state x WIZPORT
     variants; each ships as a (label, value) pair under
     system.ports.label.* and system.ports.value.*. The same #ifdef
     cascade as the pre-conversion code picks one pair per build.

Section-section boundaries inside `-a` (and the +--+ between any
section's main rows and its subsection rows) flow through box_separator
normally — those use the standard `+---+` caps. The final close at the
end of each invocation is a `+---+` from box_close followed by an extra
"\n", matching the original's `+---+\n\n` trailer.

Every row label is baked into its catalog format string with the
appropriate inline whitespace padding so themed locales can rewrite
both wording and column geometry. The server side passes only the
dynamic values; the inline literal label columns and gutter whitespace
are catalog data. Each row's body, once filled, is at most 76 visible
cols, and box_line right-pads to 76 with spaces — so rows that ship a
hair short still emit byte-identically to the pre-conversion
vwrite_user formats (which baked the trailing spaces into the format
string).

Two preserved quirks:

  - The memory section's "total" row mixes %12.3f (Mb) and %d (bytes).
    The catalog signature validator rejects %f, so the Mb value is
    pre-formatted via snprintf to a 12-char string locally and shipped
    through `%12s`. Output bytes are identical to the original
    %12.3f for any value that fits in 12 cols at three decimal places.

  - The Section U "users at level" row uses ` : ` (space-colon-space)
    like Section A's general-info rows, while every other Section U/N/R/M
    row uses `: ` (no leading space). That asymmetry is preserved verbatim
    from the original format strings; the labels carry the appropriate
    inline padding so each row's colon lands at the same column the
    original vwrite_user formats put it at.

Output on en_GB is byte-identical to the pre-conversion command, verified
by a positional-printf-aware comparison against the original format
strings across all rows + title cells + port-info variants. A themed
locale can re-skin both the frame primitives (via ui.*) and the visible
strings (via the system.* overrides) without further C-side changes.

Ledger row in docs/superpowers/specs/localisation-migration.md flipped
from `pending` to `converted (2026-05-12)`; File column set to
`src/commands/system.c`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lifts the framing parts of the `help` command into the locale catalog:
unknown-topic / ambiguous-match messages, the level- and function-grouped
commands-list screens, and the Amnuts + NUTS credits screens. The
per-topic helpfile path (more() against files/langs/<locale>/helpfiles/)
is OUT OF SCOPE — that path is already locale-aware from Phase 1 and
stays untouched.

Picked approach (a) from the Phase 4 plan: keep the existing
align_string() pipeline for the rail-bearing rows and feed it the
catalog format via lang(user, "key") rather than switching to
box_open/box_line. align_string already emits the byte stream the
original sprintf chain produced (including the |…| rails baked into the
marker arg), so this is the safer byte-identical conversion. The two
per-row in-loop format strings (`~FR%s~RS%s %s` / `%s %s` and the
function-grouped pair) ship as help.commands.row.{level,function}_{xcom,plain}
keys; the row LOOP stays in C and the assembled cell is fed to
align_string via the existing sds-based concatenation.

The credits screens are flat (no rails) and ship as three catalog
values each (header, version, body); each body is one double-quoted
scalar containing all remaining static lines from the original chain
of write_user calls — long but byte-identical, and lang_user's
internal ARR_SIZE * 2 (2000 byte) buffer comfortably holds either
credits body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five commands converted (show_igusers, grepusers, listbans, system,
help). Each landed in its own commit with matching en_GB catalog
entries and a ledger update. Notable preserved quirks: grepusers' 86-
vs-78-col pair-row overrun, listbans' new-branch reverse-logic on
unrecognised args, system's asymmetric ` : ` / `: ` spacing and
`|---|` inner separators.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 5 conversions begin to migrate vwrite_level call sites that
pass notify_invis = 0 or record_flag = 1; before this landed, the
flags were silently ignored and a one-shot warning syslog was the
only safety net. Now lang_level fully matches vwrite_level so
migrations are byte-equivalent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three Python tools supporting the Phase 5 sweep and the
translator workflow:

- extract.py walks one C source file and suggests catalog keys.
- refs.py cross-checks lang_*("key", ...) callers against the
  default catalog, reporting missing keys and orphans.
- check.py replicates the C-side format-signature validation
  as a standalone CLI for translators.

All three are dev-only; not shipped with the talker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…esc, suicide, unmuzzle, wake

10 small commands converted to lang_user / lang_room + catalog keys.
Each is a 1-10 string trivial conversion; batched into one commit
because individually they'd swamp the log.

41 new keys added to files/langs/en_GB/strings.yml under
<command>.* namespaces. Every escape (~OL/~FC/~FR/etc.) and every
whitespace byte is preserved verbatim from the C source. Two non-ASCII
control bytes survive the YAML round-trip cleanly: cls.escape_seq
ships the ANSI ESC[2J via "\x1B" and suicide.confirm_prompt ships
the original "\07" bell via "\a" — both libyaml-supported escapes.

Three calls leave write_user / write_room behind:
- wake.c retains write_user(user, notloggedon) — `notloggedon` is a
  shared global string outside the per-command catalog surface.
- home.c's invisible-arrival branch still routes through
  write_room_except with the global `invisenter` constant for the
  same reason.
- The muzzle / unmuzzle "You have been muzzled!" mail-or-write path
  is unchanged — that text is also persisted to mail spool and is
  sender-authored, not recipient-locale content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 5 is the bulk inline-string conversion sweep — open-ended by
design. The catalog framework, the lang_* API, the UI builders, and
the translator tooling (extract.py / refs.py / check.py) are all in
place. Coverage grows commit by commit. The ledger now reflects:

- Phase 5 row marked in progress (framework + tooling shipped).
- Sweep tooling docs describing extract/refs/check usage.
- Batch 1 (10 small commands) landed alongside the lang_level
  notify_invis/record_flag semantic-gap close.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A partial French translation of the Phase 5 batch-1 keys plus
meta.* and the ping smoke-test key. Validated clean by
tools/locale/check.py. Intentionally partial — every non-translated
key falls back to en_GB via the Phase 2 catalog fallback, which is
the behaviour real translators rely on as they grow their locale
incrementally.

Phase 6 is "translator's job, not a code phase" per design spec
§9.1; this commit demonstrates the end-to-end runbook from
docs/superpowers/plans/2026-05-11-localisation-phase6.md works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related structural changes:

1. config / config2 move from files/langs/en_GB/datafiles/ to
   files/datafiles/. They were never user-locale content -- they're
   server-only configuration files. Treating them as a translatable
   category was a Phase 1 oversight; this commit corrects the layout.

2. The locale-aware category that previously held config alongside
   room descriptions, maps, boards, and banned-site lists is renamed
   from "datafiles" to "locations" -- a more accurate name for what
   it actually contains now that the server-config files have moved
   out. Directory rename + matching defines.h constant rename
   (LOCATIONS replaces the bare DATAFILES) propagated through every
   locale_*_path call site.

The DATAFILES constant is repurposed: it now means the absolute
root-level files/datafiles/ directory and has exactly one consumer
(load_and_parse_config) for opening the boot-time config file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3-4 conversions left full-line frame literals in the catalog
(wizlist.frame.*, system.inner_sep, help.commands.frame_*). Those
were a localisation dead end — themed locales couldn't truly re-skin
the frames without overriding both ui.* and command-specific keys.
The right abstraction is for the UI builders to synthesise each
frame from ui.* primitives with the command supplying only the
label text.

Changes:
- ui.rule.label_lpad: 6 -> 5 in en_GB (matches the +----- title -----+
  codebase convention; this was the bug that originally drove the
  wizlist pilot to opaque frame literals).
- New box_inner_separator(BOX b) for the |---| (body-rail-capped)
  separator used inside boxes. Distinct from box_separator's +---+
  which is for between-box boundaries.
- Wizlist: 4 frame keys -> 3 label keys; frames synthesised by rule().
- System: system.inner_sep dropped; box_inner_separator() emits it.
- Help framing: 3 frame keys dropped; rule(user, 78, NULL) emits the
  bare +----+ rails.

Themers can now re-skin every framed screen by overriding only the
ui.* keys; command-specific catalog entries no longer carry frame
appearance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Newline flood and ANSI ESC[2J clear-screen sequence are control
bytes, not translatable text. Put both literals back in
src/commands/cls.c as plain write_user calls; remove the two
catalog keys; flip the ledger row from converted to not-applicable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Several Phase 3-5 conversions left pure glue and control bytes in
the catalog. Translators have nothing to do with these — they're
layout artefacts. Move each back into C and pull rail bytes from
ui.box.body.{lside,rside} so themed locales still re-skin the rails.

Removed:
- wizlist.{wrap_indent, level_prefix, name_cell, row.online_*}
- show_igusers.name_cell
- grepusers.{row_first, row_second, row_orphan_pad}
- listbans.swears.{row, trailer_on}
- home.room_arrival
- help.credits.amnuts.header_bar

Kept (prose with substitutions, or label keys for rule()):
- wizlist.{label.*, none_listed, invisible_count, no_wizzes_on}
- show_igusers.{title, none}
- grepusers.{usage, bad_*, title, empty_message, footer}
- listbans.{sites,users,swears,new}.{header,empty}, swears.trailer_off, usage
- home.{already_home, traverse}
- All system.* (column-coupled to label peers; separate decision)
- All help.* except the standalone header_bar

Byte-identical on en_GB. Themed locales (cowboy) now see consistent
rails wherever rails appear, including the grepusers pair-rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Align catalog write-functions with the existing write_* / vwrite_*
codebase convention. The lang_ prefix didn't read like a write
function despite being one; the new names make the relationship to
write_user / vwrite_user / vwrite_room_except / vwrite_level
unambiguous.

Pure rename - semantics, signatures, and behaviour unchanged. The
non-write helpers (lang, lang_format, locale_*, catalog_*) keep
their names since they're not analogous to a write_* sibling.

tools/locale/refs.py's regex updated to recognise both old and new
names while the migration tooling stabilises.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ings

write_user_lang, write_room_lang and write_level_lang are
catalog-keyed sisters to write_user / vwrite_room_except /
vwrite_level. The renamed functions now sit next to their siblings
in src/amnuts.c, replacing the file-static catalog_resolve call with
the public lang() lookup (same semantics, including the rate-limited
missing-key warning). catalog.c keeps lang, lang_format, and
everything else.

Prototypes moved with them; the printf-format attribute is
intentionally omitted on _lang variants because the second
parameter is a catalog key, not a printf format string.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@amnuts amnuts changed the base branch from main to develop May 12, 2026 21:59
@Uzume
Copy link
Copy Markdown

Uzume commented May 13, 2026

I am glad internationalization (i18n) is finally getting addressed though I wonder if this is the best strategy. I am sure this will allow much easier localization (l10n) in the future. Perhaps we will even see an official Portuguese translation?

@amnuts
Copy link
Copy Markdown
Owner Author

amnuts commented May 13, 2026

... though I wonder if this is the best strategy.

Which part are you not sure about?

I was starting to address this as, "OK, translations", but then it really is just lends itself to "theming, but a theme could just be en-GB or pt-BR" and let the user select what they wanted to use (with the talker having a default).

The reason I got there is because I was looking over my files trying to dig out some old Way Out West and remembered the absolute ball ache it was to go through all the text to make it fit into my theme. The strings.xml would address that, with me just dropping in a langs/WayOutWest/strings.xml to override what I want from the default langauge.

Seemed pretty OK to me, but very open to suggestions. I really just threw this at Claude Code to see what it could come up with under a little prompting. 😁

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants