Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
e5cc60e
Add localisation & theming design
amnuts May 10, 2026
031ab3d
Fix spec: use real constant names; add adminfiles
amnuts May 10, 2026
b64ec40
Add Phase 1 implementation plan
amnuts May 10, 2026
69902cc
Vendor libyaml 0.2.5
amnuts May 10, 2026
dbe51f4
Drop dead HAVE_CONFIG_H define from libyaml config.h
amnuts May 10, 2026
85dcd39
Declare locale API and LANGS_ROOT
amnuts May 10, 2026
91f6986
Drop unused forward decls; align plan summary with step
amnuts May 10, 2026
738704f
Parse default_language config option
amnuts May 10, 2026
7e1021d
Align default_language case with sibling style
amnuts May 10, 2026
47ea4ce
Implement locale_default_path and locale_default
amnuts May 10, 2026
a5d5c18
Drop unused includes from locale.c
amnuts May 10, 2026
dbeb9f0
Implement locale_load_all: discover locales, validate default
amnuts May 10, 2026
dbe1ee3
Harden locale_load_all
amnuts May 10, 2026
17deeee
Implement locale_path with user-locale fallback to default
amnuts May 10, 2026
5bae198
Drop locale.h; relocate user->locale to preference cluster
amnuts May 10, 2026
43fd564
Audit ledger: call sites that touch relocated categories
amnuts May 10, 2026
eae2e39
Document the per-site conversion pattern
amnuts May 10, 2026
dd392df
Relocate content directories; point constants at new tree
amnuts May 10, 2026
12875b6
Wire locale_load_all into boot sequence
amnuts May 10, 2026
dfc7f2f
Sweep ADMINFILES sites and flip constant to bare
amnuts May 10, 2026
c6bd23c
Sweep DATAFILES sites and flip constant to bare
amnuts May 10, 2026
fd7a7fb
Resize filename buffer in suggestions.c (hotfix)
amnuts May 10, 2026
885a9bd
Sweep HELPFILES sites and flip constant to bare
amnuts May 10, 2026
956422f
Sweep MISCFILES sites and flip constant to bare
amnuts May 10, 2026
0fe8517
Sweep MOTDFILES sites and flip constant to bare
amnuts May 10, 2026
213f38a
Sweep TEXTFILES sites and flip constant to bare
amnuts May 10, 2026
7f4c085
Phase 1 complete: file-path mechanism + directory move
amnuts May 10, 2026
830dad6
Upsize filename buffers after path prefix growth
amnuts May 10, 2026
a87e9a6
Add catalog types to globals.h
amnuts May 11, 2026
5c0864e
Tighten catalog type comments
amnuts May 11, 2026
d623492
Declare catalog, lang_*, and helper API surface
amnuts May 11, 2026
303e3ab
Implement shared YAML helpers (yaml_util.c)
amnuts May 11, 2026
79a2eaf
Catalog hash table skeleton: storage and lookup
amnuts May 11, 2026
cc3d24f
Catalog format-signature extraction and compatibility check
amnuts May 11, 2026
2e34e79
Implement catalog YAML loader and wire into locale_load_all
amnuts May 11, 2026
df23d4d
Fix catalog drain loop and tighten docstring
amnuts May 11, 2026
f497bbe
Harden catalog loader: bound log key, drop dead branch
amnuts May 11, 2026
d004202
Ship en_GB/strings.yml with meta.* and a smoke-test key
amnuts May 11, 2026
5dd2d6a
Implement lang, lang_format, and lang_user
amnuts May 11, 2026
45a41d0
Implement lang_room and lang_level broadcast variants
amnuts May 11, 2026
58b6446
Mark lang_level notify_invis as an explicit TODO
amnuts May 11, 2026
9a54423
Persist user->locale to user files via USERDB_LIST
amnuts May 11, 2026
f1d91b1
Resolve user->catalog after load_user_details
amnuts May 11, 2026
eb760a2
Implement locale_set_user and locale_list
amnuts May 11, 2026
7c2576f
Guard empty meta.description against size_t underflow
amnuts May 11, 2026
bf83f6b
Wire `set lang` into the set-attributes dispatch
amnuts May 11, 2026
92ea30c
Implement langreload (WIZ command) with atomic catalog swap
amnuts May 11, 2026
9769f14
Phase 2 complete: catalog framework
amnuts May 11, 2026
75a10e7
Phase 2 fixups from cross-cutting review
amnuts May 11, 2026
ee0c0ce
Add Phase 2 implementation plan
amnuts May 11, 2026
6277db3
Add Phase 3-6 implementation plans
amnuts May 12, 2026
5e25129
UI builders: visible_strlen keystone
amnuts May 12, 2026
3b9a07d
UI builders: align_into for visible-column padding/truncation
amnuts May 12, 2026
158ce2b
Pull uibuilders.h into prototypes.h
amnuts May 12, 2026
1d5ed9b
Ship canonical ui.* keys in en_GB/strings.yml
amnuts May 12, 2026
9c775d9
UI builders: rule() for horizontal section dividers
amnuts May 12, 2026
5fbfc5b
UI builders: box_open/box_line/box_blank/box_centered/box_separator/b…
amnuts May 12, 2026
5bd494e
UI builders: table_* on top of box_*
amnuts May 12, 2026
6993128
Ship a Cowboy test locale with themed ui.* keys
amnuts May 12, 2026
4d4e883
Convert wizlist to catalog + lang_user (Phase 3 pilot)
amnuts May 12, 2026
40e5143
Phase 3 complete: UI builders
amnuts May 12, 2026
1600a80
Document Phase 4 / Phase 5 per-command conversion pattern
amnuts May 12, 2026
c55b66c
Convert show_igusers to catalog + UI builders (Phase 4)
amnuts May 12, 2026
455f021
Convert grepusers to catalog + UI builders (Phase 4)
amnuts May 12, 2026
3a412c0
Fix ALIGN_* macro/enum collision
amnuts May 12, 2026
20da9e1
Convert listbans to catalog + UI builders (Phase 4)
amnuts May 12, 2026
0ad53e0
Convert system to catalog + UI builders (Phase 4)
amnuts May 12, 2026
9861798
Convert help framing to catalog + lang_* (Phase 4)
amnuts May 12, 2026
6c407fc
Phase 4 complete: frame-heavy command sweep
amnuts May 12, 2026
e238fd2
lang_level: port full notify_invis and record_flag semantics
amnuts May 12, 2026
7e27093
tools/locale: extract.py + refs.py + check.py + README
amnuts May 12, 2026
95c15d6
Phase 5 sweep batch 1: cafk, cls, home, muzzle, revafk, revclr, set_d…
amnuts May 12, 2026
5232b3c
Phase 5: declare in-progress; document tooling + first batch
amnuts May 12, 2026
046a0d9
Phase 6: ship fr demonstration locale
amnuts May 12, 2026
e6ea0af
Restructure: server config out of langs tree; datafiles -> locations
amnuts May 12, 2026
49c0ddb
Dynamic frame rendering: drop opaque frame literals, use rule()
amnuts May 12, 2026
90b4cef
cls: drop spurious catalog keys, mark not-applicable
amnuts May 12, 2026
2a320b5
Scrub non-prose catalog entries; inline glue, pull rails from ui.*
amnuts May 12, 2026
1a036e9
Rename lang_{user,room,level} -> write_{user,room,level}_lang
amnuts May 12, 2026
3895995
Move write_*_lang into amnuts.c alongside their write_*/vwrite_* sibl…
amnuts May 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
21 changes: 17 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ VENDOR_LIBTELNET_OBJ_DIR = $(TALKER_OBJ_DIR)
VENDOR_LIBTELNET_SRC = $(wildcard $(VENDOR_LIBTELNET_SRC_DIR)/*.c)
VENDOR_LIBTELNET_OBJS = $(addprefix $(VENDOR_LIBTELNET_OBJ_DIR)/,$(notdir $(VENDOR_LIBTELNET_SRC:.c=.o)))

# libyaml: https://github.com/yaml/libyaml
VENDOR_LIBYAML_SRC_DIR = $(TALKER_SRC_DIR)/vendors/libyaml
VENDOR_LIBYAML_OBJ_DIR = $(TALKER_OBJ_DIR)
VENDOR_LIBYAML_SRC = $(wildcard $(VENDOR_LIBYAML_SRC_DIR)/*.c)
VENDOR_LIBYAML_OBJS = $(addprefix $(VENDOR_LIBYAML_OBJ_DIR)/,$(notdir $(VENDOR_LIBYAML_SRC:.c=.o)))

#
# Platform-specific libraries that need to be included
#
Expand Down Expand Up @@ -94,6 +100,7 @@ distclean: clean
rm -f $(IDENTD_SRC_DIR)/*.[ch]~ $(IDENTD_SRC_DIR)/*.[ch].bak
rm -f $(VENDOR_SDS_SRC_DIR)/*.[ch]~ $(VENDOR_SDS_SRC_DIR)/*.[ch].bak
rm -f $(VENDOR_LIBTELNET_SRC_DIR)/*.[ch]~ $(VENDOR_LIBTELNET_SRC_DIR)/*.[ch].bak
rm -f $(VENDOR_LIBYAML_SRC_DIR)/*.[ch]~ $(VENDOR_LIBYAML_SRC_DIR)/*.[ch].bak
rm -f $(TALKER_BIN) $(BINDIR)/$(TALKER_BIN)
rm -f $(IDENTD_BIN) $(BINDIR)/$(IDENTD_BIN)
rm -f $(INCDIR)/*.[ch]~ $(INCDIR)/*.[ch].bak
Expand All @@ -104,23 +111,24 @@ clean:
rm -f $(IDENTD_OBJS) $(IDENTD_OBJS:.o=.d)
rm -f $(VENDOR_SDS_OBJS) $(VENDOR_SDS_OBJS:.o=.d)
rm -f $(VENDOR_LIBTELNET_OBJS) $(VENDOR_LIBTELNET_OBJS:.o=.d)
rm -f $(VENDOR_LIBYAML_OBJS) $(VENDOR_LIBYAML_OBJS:.o=.d)

install: $(BINDIR)/$(TALKER_BIN) $(BINDIR)/$(IDENTD_BIN)

build: $(TALKER_BIN) $(IDENTD_BIN)

compile: $(TALKER_OBJS) $(IDENTD_OBJS) $(VENDOR_SDS_OBJS) $(VENDOR_LIBTELNET_OBJS)
compile: $(TALKER_OBJS) $(IDENTD_OBJS) $(VENDOR_SDS_OBJS) $(VENDOR_LIBTELNET_OBJS) $(VENDOR_LIBYAML_OBJS)

print-%: ; @echo $* = $($*)

vpath %.c $(TALKER_SRC_DIR) $(TALKER_SRC_DIR)/commands $(IDENTD_SRC_DIR) $(VENDOR_SDS_SRC_DIR) $(VENDOR_LIBTELNET_SRC_DIR)
vpath %.c $(TALKER_SRC_DIR) $(TALKER_SRC_DIR)/commands $(IDENTD_SRC_DIR) $(VENDOR_SDS_SRC_DIR) $(VENDOR_LIBTELNET_SRC_DIR) $(VENDOR_LIBYAML_SRC_DIR)

$(BINDIR)/$(TALKER_BIN) $(BINDIR)/$(IDENTD_BIN): $(BINDIR)/%: %
@echo "Installing $< ..."
chmod $(PERMS) $<
mv $< $(BINDIR)

$(TALKER_BIN): $(TALKER_OBJS) $(VENDOR_SDS_OBJS) $(VENDOR_LIBTELNET_OBJS)
$(TALKER_BIN): $(TALKER_OBJS) $(VENDOR_SDS_OBJS) $(VENDOR_LIBTELNET_OBJS) $(VENDOR_LIBYAML_OBJS)
@echo "Linking $@ ..."
$(CC) $(LD_FLAGS) $^ $(TALKER_LIBS) -o $@

Expand Down Expand Up @@ -148,4 +156,9 @@ $(VENDOR_LIBTELNET_OBJS): $(VENDOR_LIBTELNET_OBJ_DIR)/%.o: %.c
@test -d $(VENDOR_LIBTELNET_OBJ_DIR) || mkdir $(VENDOR_LIBTELNET_OBJ_DIR)
$(CC) $(C_FLAGS) $(CC_FLAGS) $(TALKER_FLAGS) -c -o $@ $<

-include $(TALKER_OBJS:.o=.d) $(IDENTD_OBJS:.o=.d) $(VENDOR_SDS_OBJS:.o=.d) $(VENDOR_LIBTELNET_OBJS:.o=.d)
$(VENDOR_LIBYAML_OBJS): $(VENDOR_LIBYAML_OBJ_DIR)/%.o: %.c
@echo "Compiling libyaml library $< ... ($@)"
@test -d $(VENDOR_LIBYAML_OBJ_DIR) || mkdir $(VENDOR_LIBYAML_OBJ_DIR)
$(CC) -std=gnu99 -g -w -I$(VENDOR_LIBYAML_SRC_DIR) -DHAVE_CONFIG_H -c -o $@ $<

-include $(TALKER_OBJS:.o=.d) $(IDENTD_OBJS:.o=.d) $(VENDOR_SDS_OBJS:.o=.d) $(VENDOR_LIBTELNET_OBJS:.o=.d) $(VENDOR_LIBYAML_OBJS:.o=.d)
156 changes: 156 additions & 0 deletions docs/superpowers/plans/2026-05-10-localisation-phase1-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Phase 1 Audit Ledger

For each call site, capture: file path:line, the function it's in, the operation
(`fopen` / `more` / `opendir` / `sprintf` building a path), and the conversion target
(`locale_path(user, ...)` for per-user lookup, `locale_default_path(...)` for boot/server-internal).

Definition lines in `src/includes/defines.h` (lines 51, 52, 54, 57, 58, 60) are the
macro definitions themselves and are deliberately excluded from this audit.

Two string-literal occurrences of `MOTDFILES` inside `perror`/`write_syslog` messages at
`src/amnuts.c:2402` and `src/amnuts.c:2404` are also excluded; they are diagnostic text,
not macro expansions, so they are not call sites.

Total: 51 call sites across 16 source files.

## ADMINFILES

- [ ] `src/commands/display.c:32` — `display_files` — `sprintf(filename, "%s/%s/%s", TEXTFILES, ADMINFILES, SHOWFILES)` then `more(user, ...)` → `locale_path(user, ...)` (per-user; file is displayed to `user`)

Check notice on line 18 in docs/superpowers/plans/2026-05-10-localisation-phase1-audit.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

docs/superpowers/plans/2026-05-10-localisation-phase1-audit.md#L18

Expected: 80; Actual: 209
- [ ] `src/commands/display.c:61` — `display_files` — `sprintf(filename, "%s/%s/%s", TEXTFILES, ADMINFILES, word[1])` then `more(user, ...)` → `locale_path(user, ...)` (per-user; admin-prefixed text file shown to `user`)

## DATAFILES

- [ ] `src/amnuts.c:1035` — `load_and_parse_config` — `sprintf(filename, "%s/%s", DATAFILES, confile)` then `fopen(filename, "r")` → `locale_default_path(...)` (boot-time config load, no user context)
- [ ] `src/amnuts.c:1226` — `load_and_parse_config` — `sprintf(filename, "%s/%s.R", DATAFILES, rm1->name)` then `fopen(filename, "r")` → `locale_default_path(...)` (boot-time room description load)
- [ ] `src/amnuts.c:5706` — `exec_com` (case `MAP`) — `sprintf(filename, "%s/%s.map", DATAFILES, user->room->map)` then `more(user, user->socket, filename)` → `locale_path(user, ...)` (per-user; map shown to `user`)
- [ ] `src/admin.c:133` — `site_banned` — `sprintf(filename, "%s/%s", DATAFILES, NEWBAN)` then `fopen(filename, "r")` → `locale_default_path(...)` (shared ban state, server-internal lookup)
- [ ] `src/admin.c:135` — `site_banned` — `sprintf(filename, "%s/%s", DATAFILES, SITEBAN)` then `fopen(filename, "r")` → `locale_default_path(...)` (shared ban state)
- [ ] `src/admin.c:183` — `user_banned` — `sprintf(filename, "%s/%s", DATAFILES, USERBAN)` then `fopen(filename, "r")` → `locale_default_path(...)` (shared ban state)
- [ ] `src/admin.c:208` — `auto_ban_site` — `sprintf(filename, "%s/%s", DATAFILES, SITEBAN)` then `fopen(filename, "a")` → `locale_default_path(...)` (shared ban state, append)
- [ ] `src/admin.c:276` — `ban_site` — `sprintf(filename, "%s/%s", DATAFILES, SITEBAN)` then `fopen(filename, "r")` / `"a"` → `locale_default_path(...)` (shared ban state)
- [ ] `src/admin.c:327` — `ban_user` — `sprintf(filename, "%s/%s", DATAFILES, USERBAN)` then `fopen(filename, "r")` / `"a"` → `locale_default_path(...)` (shared ban state)
- [ ] `src/admin.c:420` — `ban_new` — `sprintf(filename, "%s/%s", DATAFILES, NEWBAN)` then `fopen(filename, "r")` / `"a"` → `locale_default_path(...)` (shared ban state)
- [ ] `src/admin.c:465` — `unban_site` — `sprintf(filename, "%s/%s", DATAFILES, SITEBAN)` then `fopen(filename, "r")` → `locale_default_path(...)` (shared ban state, rewrite)
- [ ] `src/admin.c:513` — `unban_user` — `sprintf(filename, "%s/%s", DATAFILES, USERBAN)` then `fopen(filename, "r")` → `locale_default_path(...)` (shared ban state, rewrite)
- [ ] `src/admin.c:565` — `unban_new` — `sprintf(filename, "%s/%s", DATAFILES, NEWBAN)` then `fopen(filename, "r")` → `locale_default_path(...)` (shared ban state, rewrite)
- [ ] `src/messages.c:757` — `read_board_specific` — `sprintf(filename, "%s/%s.B", DATAFILES, rm->name)` then `fopen(filename, "r")` → `locale_default_path(...)` (shared room board state)
- [ ] `src/messages.c:836` — `check_board_wipe` — `sprintf(filename, "%s/%s.B", DATAFILES, rm->name)` then `fopen(filename, "r")` → `locale_default_path(...)` (shared room board state)
- [ ] `src/messages.c:910` — `board_from` — `sprintf(filename, "%s/%s.B", DATAFILES, rm->name)` then `fopen(filename, "r")` → `locale_default_path(...)` (shared room board state)

Check notice on line 38 in docs/superpowers/plans/2026-05-10-localisation-phase1-audit.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

docs/superpowers/plans/2026-05-10-localisation-phase1-audit.md#L38

Expected: 80; Actual: 178
- [ ] `src/commands/listbans.c:28` — `listbans` — `sprintf(filename, "%s/%s", DATAFILES, SITEBAN)` then `more(user, user->socket, filename)` → `locale_default_path(...)` (shared ban state displayed verbatim; not localisable content)

Check notice on line 39 in docs/superpowers/plans/2026-05-10-localisation-phase1-audit.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

docs/superpowers/plans/2026-05-10-localisation-phase1-audit.md#L39

Expected: 80; Actual: 232
- [ ] `src/commands/listbans.c:41` — `listbans` — `sprintf(filename, "%s/%s", DATAFILES, USERBAN)` then `more(user, user->socket, filename)` → `locale_default_path(...)` (shared ban state)
- [ ] `src/commands/listbans.c:71` — `listbans` — `sprintf(filename, "%s/%s", DATAFILES, NEWBAN)` then `more(user, user->socket, filename)` → `locale_default_path(...)` (shared ban state)
- [ ] `src/commands/read_board.c:61` — `read_board` — `sprintf(filename, "%s/%s.B", DATAFILES, rm->name)` (filepos read) → `locale_default_path(...)` (shared room board state)
- [ ] `src/commands/recount.c:67` — `check_messages` — `sprintf(filename, "%s/%s.B", DATAFILES, rm->name)` then `fopen(filename, "r")` → `locale_default_path(...)` (shared room board state)
- [ ] `src/commands/reload_room.c:39` — `reload_room_description` — `sprintf(filename, "%s/%s.R", DATAFILES, rm->name)` then `fopen(filename, "r")` → `locale_default_path(...)` (reload of boot-time room descriptions; admin-initiated, not content shown to a specific locale)

Check notice on line 44 in docs/superpowers/plans/2026-05-10-localisation-phase1-audit.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

docs/superpowers/plans/2026-05-10-localisation-phase1-audit.md#L44

Expected: 80; Actual: 273
- [ ] `src/commands/reload_room.c:91` — `reload_room_description` — `sprintf(filename, "%s/%s.R", DATAFILES, rm->name)` then `fopen(filename, "r")` → `locale_default_path(...)` (same, single-room path)
- [ ] `src/commands/search_boards.c:41` — `search_boards` — `sprintf(filename, "%s/%s.B", DATAFILES, rm->name)` then `fopen(filename, "r")` → `locale_default_path(...)` (shared room board state)

Check notice on line 46 in docs/superpowers/plans/2026-05-10-localisation-phase1-audit.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

docs/superpowers/plans/2026-05-10-localisation-phase1-audit.md#L46

Expected: 80; Actual: 194
- [ ] `src/commands/wipe_board.c:61` — `wipe_board` — `sprintf(filename, "%s/%s.B", DATAFILES, rm->name)` → `locale_default_path(...)` (shared room board state, wipe)
- [ ] `src/commands/write_board.c:72` — `write_board` — `sprintf(filename, "%s/%s.B", DATAFILES, user->room->name)` then `fopen(filename, "a")` → `locale_default_path(...)` (shared room board state, append)

## HELPFILES

- [ ] `src/commands/help.c:83` — `help` — `sprintf(filename, "%s/%s", HELPFILES, com->name)` (subsequently used by `more(user, ...)`) → `locale_path(user, ...)` (per-user; help text shown to `user`)
- [ ] `src/commands/help.c:121` — `help` — `sprintf(filename, "%s/%s_%s", HELPFILES, com->name, attr->type)` (set attribute help) → `locale_path(user, ...)` (per-user)
- [ ] `src/commands/help.c:123` — `help` — `sprintf(filename, "%s/%s", HELPFILES, com->name)` (set fallback help) → `locale_path(user, ...)` (per-user)
- [ ] `src/commands/help.c:127` — `help` — `sprintf(filename, "%s/%s", HELPFILES, com->name)` (generic HELP fallback) → `locale_path(user, ...)` (per-user)

## MISCFILES

- [ ] `src/messages.c:28` — `count_suggestions` — `sprintf(filename, "%s/%s", MISCFILES, SUGBOARD)` then `fopen(filename, "r")` → `locale_default_path(...)` (boot-time count of shared suggestions board)
- [ ] `src/amnuts.c:4558` — `login` — `sprintf(filename, "%s/%s", MISCFILES, RULESFILE)` then `more(NULL, user->socket, filename)` → `locale_path(user, ...)` (per-user; rules shown to newly-created `user`)
- [ ] `src/amnuts.c:5584` — `exec_com` (case `NEWS`) — `sprintf(filename, "%s/%s", MISCFILES, NEWSFILE)` then `more(user, user->socket, filename)` → `locale_path(user, ...)` (per-user; news shown to `user`)
- [ ] `src/amnuts.c:5887` — `exec_com` (case `RULES`) — `sprintf(filename, "%s/%s", MISCFILES, RULESFILE)` then `more(user, user->socket, filename)` → `locale_path(user, ...)` (per-user)
- [ ] `src/amnuts.c:6118` — `exec_com` (case `WIZRULES`) — `sprintf(filename, "%s/%s", MISCFILES, WIZRULESFILE)` then `more(user, user->socket, filename)` → `locale_path(user, ...)` (per-user)
- [ ] `src/games.c:111` — `get_hang_word` — `sprintf(filename, "%s/%s", MISCFILES, HANGDICT)` then `count_lines(filename)` and word picking → `locale_default_path(...)` (shared dictionary used for game RNG; not user-displayed content)
- [ ] `src/commands/delete_suggestions.c:39` — `delete_suggestions` — `sprintf(filename, "%s/%s", MISCFILES, SUGBOARD)` then `remove(filename)` / rewrite → `locale_default_path(...)` (shared suggestions board, admin mutation)
- [ ] `src/commands/sfrom.c:31` — `suggestions_from` — `sprintf(filename, "%s/%s", MISCFILES, SUGBOARD)` then `fopen(filename, "r")` → `locale_default_path(...)` (shared suggestions board read; admin/info view of shared state)
- [ ] `src/commands/suggestions.c:27` — `suggestions` (RSUG branch) — `sprintf(filename, "%s/%s", MISCFILES, SUGBOARD)` then `more(user, user->socket, filename)` → `locale_default_path(...)` (shared suggestions board content; the data is user-written, not localised)
- [ ] `src/commands/suggestions.c:63` — `suggestions` (write branch) — `sprintf(filename, "%s/%s", MISCFILES, SUGBOARD)` then `fopen(filename, "a")` → `locale_default_path(...)` (shared suggestions board append)

## MOTDFILES

- [ ] `src/messages.c:66` — `count_motds` — `sprintf(filename, "%s/motd%d", MOTDFILES, i)` then `opendir(filename)` → `locale_default_path(...)` (boot/runtime enumeration of motd directory; no user in scope)
- [ ] `src/amnuts.c:822` — `accept_connection` — `sprintf(motdname, "%s/motd1/motd%d", MOTDFILES, get_motd_num(1))` then `more(NULL, accept_sock, motdname)` → `locale_default_path(...)` (pre-auth; no user, locale unknown — must use default)
- [ ] `src/amnuts.c:4590` — `login` — `sprintf(motdname, "%s/motd2/motd%d", MOTDFILES, get_motd_num(2))` then `more(user, user->socket, motdname)` → `locale_path(user, ...)` (per-user; post-login motd2 for new user)
- [ ] `src/amnuts.c:4629` — `login` — `sprintf(motdname, "%s/motd2/motd%d", MOTDFILES, get_motd_num(2))` then `more(user, user->socket, motdname)` → `locale_path(user, ...)` (per-user; post-login motd2 for returning user)

## TEXTFILES

- [ ] `src/commands/display.c:30` — `display_files` — `sprintf(filename, "%s/%s", TEXTFILES, SHOWFILES)` then `more(user, ...)` → `locale_path(user, ...)` (per-user; index of displayable text files)
- [ ] `src/commands/display.c:32` — `display_files` — `sprintf(filename, "%s/%s/%s", TEXTFILES, ADMINFILES, SHOWFILES)` then `more(user, ...)` → `locale_path(user, ...)` (per-user; admin index — same entry as ADMINFILES section above, listed here for the `TEXTFILES` token)
- [ ] `src/commands/display.c:59` — `display_files` — `sprintf(filename, "%s/%s", TEXTFILES, word[1])` then `more(user, ...)` → `locale_path(user, ...)` (per-user; arbitrary user-requested text file)
- [ ] `src/commands/display.c:61` — `display_files` — `sprintf(filename, "%s/%s/%s", TEXTFILES, ADMINFILES, word[1])` then `more(user, ...)` → `locale_path(user, ...)` (per-user; admin-prefixed text file — same entry as ADMINFILES section above, listed here for the `TEXTFILES` token)

## Notes / anomalies

- `src/messages.c:66` (`count_motds`) is the only `opendir(DATAFILES/MISCFILES/MOTDFILES/...)`-style call in the audited set. All other uses are `sprintf` + `fopen`/`more`/`remove`. The localisation helpers must therefore continue to yield a directory path (not just a file path) suitable for `opendir`.
- `src/commands/display.c:32` and `src/commands/display.c:61` reference *two* of the six constants on a single line (`TEXTFILES` and `ADMINFILES`). They are listed in both the `ADMINFILES` and `TEXTFILES` sections; the sweep should produce a single edit per line that resolves both constants together.
- `src/amnuts.c:2402` and `src/amnuts.c:2404` contain the literal string "MOTDFILES" inside a `perror`/`write_syslog` message. The preprocessor does not expand macros inside string literals, so these are diagnostic text, not call sites. They should be left alone (or, optionally, retargeted as part of the eventual logging/i18n pass — out of scope for Phase 1).
- Board files (`.B`) and room description files (`.R`) under `DATAFILES` are shared mutable state, not display content; they are routed to `locale_default_path(...)`. If a future phase decides to localise per-room descriptions, the `.R` reads at `src/amnuts.c:1226` and `src/commands/reload_room.c:39,91` are the lines to revisit.
- The `RULESFILE` read at `src/amnuts.c:4558` happens while `user` exists but its locale may still be the default (the user is in the middle of registration). `locale_path(user, ...)` is still correct because the helper is expected to fall back to the default when the user's locale is unset.

## Conversion pattern

**Before — boot-time / global lookup (no user context):**

```c
sprintf(filename, "%s/%s", DATAFILES, confile);
fp = fopen(filename, "r");
if (!fp) { ... }
```

**After:**

```c
locale_default_path(filename, sizeof filename, DATAFILES, confile);
fp = fopen(filename, "r");
if (!fp) { ... }
```

**Before — per-user lookup (user struct in scope):**

```c
sprintf(filename, "%s/%s", HELPFILES, word[1]);
more(user, user->socket, filename);
```

**After:**

```c
if (locale_path(user, filename, sizeof filename, HELPFILES, word[1])) {
more(user, user->socket, filename);
} else {
write_user(user, "No such help file.\n");
}
```

**Notes:**

- The constant moves from being part of the `sprintf` format string to
being the `category` argument of `locale_*_path`.
- The constants' string values flip from absolute to bare in Tasks 11–16
(one task per constant, in the same commit as that category's sweep).
But the *call shape* above works with either, because the helpers
always prepend `LANGS_ROOT/<locale>/`. Until the constant flips, the
helper produces a doubled path that won't resolve — so the talker is
bootable only when sweep + flip happen together for that category.
- Pure existence checks (`stat(DATAFILES "/foo", ...)`) follow the same
pattern: call the resolver, then operate on the path it wrote.
- For `opendir(DATAFILES)` style calls (rare — directory enumeration of
the category root itself, e.g. `count_motds` in `messages.c`), use:

```c
char dir[PATH_MAX];
snprintf(dir, sizeof dir, "%s/%s/%s",
LANGS_ROOT, locale_default(), DATAFILES);
dirp = opendir(dir);
```

Don't try to resolve via `locale_*_path` for these — the helpers resolve
files, not directories.
- Server state files that live under `DATAFILES` (ban lists, board `.B`
files, suggestions board, hangman dictionary) are also routed through
`locale_default_path`. Reads always go to the default locale; writes
also land there. Phase 1 does not relocate these out of `langs/en_GB/`
even though they're server state rather than localisable content;
that's a future cleanup.
Loading