This file collects per-utility POSIX conformance audits for the date/time
utilities crate. Each audit follows the playbook in audits.md.
Crate: datetime/ — cal, date, sleep, time (4 single-file
utilities, ~640 lines total).
Spec: POSIX.1-2024 (IEEE Std 1003.1-2024), Vol. 3 §3.
Reference slices: ~/tmp/posix.2024/sliced/xcu-shell-and-utilities/3-utilities/{cal,date,sleep,time}.md
Date: 2026-06-24
Method: static spec-vs-code, every "absent"/"wrong-value" claim grep-verified
against the cited lines.
Remediation status (2026-06-24). All actionable findings — 2 Critical, 3 Major, 6 Minor — have since been fixed on the
datetime-auditbranch across five themed, independently-committed phases, each with regression tests and a cleanbuild/clippy --all-targets/fmt/ datetime test run:
- Phase 1 —
timeCPU accounting + exit-status propagation (#T1, #T2, #T3, #T5).- Phase 2 —
sleepacceptsleep 0(#S1) + firstsleeptest module.- Phase 3 —
datecentury off-by-one (#D1,infer_century+ unit test), grow-until-fitsstrftime(#D2), raw-byte output (#D4).- Phase 4 — i18n adopt
plib::diag::init_locale+gettextdiagnostics fordate/time(#T4, #D3, #T6);#S2deferred (clap-generated, tree-wide).- Phase 5 —
calLC_TIME-aware month/weekday names viaplib::locale::strftime(#C1).The only items left open are minor test-coverage niceties (a 126/127 assertion; empty-
strftime/non-UTF-8-locale display tests) and the tree-wide.mocatalog deferral (soLC_MESSAGESstays inert). All four utilities are promoted to README Stage 6 — Audited. Checkboxes below are ticked with✓ fixed in Phase N.
| Theme | Status | Notes |
|---|---|---|
setlocale(LC_ALL, "") at main |
Present in all four | cal.rs:212, date.rs:183, sleep.rs:25, time.rs:134. |
textdomain/bind_textdomain_codeset |
Present in all four | time.rs:136 additionally calls bindtextdomain("posixutils-rs", "locale") with a relative path; the other three omit bindtextdomain entirely. No .mo catalogs ship, so gettext() is an identity map regardless. |
Runtime diagnostics via gettext() |
Fixed (Phase 4) | cal wraps its one error string; date and time use raw eprintln!/Err(&str) hardcoded English.plib::diag::init_locale and route diagnostics through plib::diag::error + gettext (sleep emits none of its own). Only clap's own parse/range errors stay English (#S2, deferred). .mo catalog shipping remains a tree-wide deferral. |
LC_CTYPE/LC_TIME/TZ honored via libc |
Fixed (Phase 5) | date/cal defer to libc strftime/localtime_r (honor LC_TIME/TZ); cal's month-name/weekday strings LC_TIMEplib::locale::strftime under LC_TIME (#C1). |
As in the dev/ and sys/ audits, crate-wide .mo catalog shipping is a
tree-wide i18n concern and is not counted against individual utilities beyond a
Minor note.
| # | Util | Sev | One-liner |
|---|---|---|---|
| #T1 | time | Critical | tms_end is zeroed but never filled by a second libc::times() call — CPU stats are garbage. |
| #T2 | time | Critical | Reads parent tms_utime/tms_stime (and subtracts backwards) instead of child tms_cutime/tms_cstime — the wrong process is measured. |
| #T3 | time | Major | Child exit status is discarded (let _ = child.wait()); time always exits 0, violating "exit status of time shall be the exit status of utility". |
| #S1 | sleep | Major | range(1..) rejects sleep 0; POSIX time operand is a non-negative integer, so sleep 0 must succeed with exit 0. |
| #D1 | date | Major | 2-digit-year century inference is off by one: yy=69 maps to 2069, but POSIX mandates 69–99 → 1969–1999. |
| #C1 | cal | Minor | LC_TIME not honored for the calendar's month names / weekday header (always English). Spec: "LC_TIME Determine the format and contents of the calendar." |
| #D2 | date | Minor | strftime returning 0 is always treated as an error; a legitimately-empty conversion yields a false "produced no output" exit 1. |
| #T4/#D3/#S2 | time/date/sleep | Minor | Hardcoded-English runtime diagnostics (LC_MESSAGES inert). |
| #D4 | date | Minor | String::from_utf8_lossy on strftime output can mangle bytes in non-UTF-8 locales. |
| #T5 | time | Minor | A child terminated by a signal is not reflected in time's exit status (no 128+n). |
Totals: 2 Critical, 3 Major, 6 Minor. cal is the cleanest (one Minor i18n
gap); time carries both Criticals.
Implementation: datetime/time.rs (159 lines)
Tests: datetime/tests/time/mod.rs (58 lines, 4 #[test]s)
Spec: POSIX.1-2024, Vol. 3 §3, pp. 3488–3492
Reference slice: ~/tmp/posix.2024/sliced/xcu-shell-and-utilities/3-utilities/time.md
Date: 2026-06-24
time correctly forks the utility, inherits stdio, maps 126/127, and emits the
-p format verbatim — but the timing numbers it prints are wrong on every
run. The end-of-run snapshot tms_end is created with std::mem::zeroed()
and never populated by a second libc::times() call (#T1), and the arithmetic
reads the parent's own tms_utime/tms_stime rather than the child's
tms_cutime/tms_cstime, subtracting start−end instead of end−start (#T2). The
net effect is that User/System CPU time is reported as a near-zero constant that
has nothing to do with the invoked utility — the utility's core purpose is
broken. Separately, the child's exit status is discarded, so time always exits
0 (#T3).
- #T1 —
tms_endis never populated; CPU stats are computed against an all-zero struct. ✓ fixed in Phase 1 (libc::times(&mut tms_end)now called afterchild.wait()).time.rs:85.let tms_end: libc::tms = unsafe { std::mem::zeroed() };— there is exactly onelibc::timescall in the file (time.rs:70, fillingtms_start);grep -n 'libc::times' time.rs→ one hit. The end snapshot is required afterchild.wait()to capture accumulated child CPU. As written,tms_end.*is always 0. Fix: calllibc::times(&mut tms_end)after the wait, before computing deltas. - #T2 — Wrong fields and wrong subtraction direction. ✓ fixed in Phase 1 (now
(end.tms_utime+end.tms_cutime) − (start…), likewise system, ÷_SC_CLK_TCK).time.rs:87-88. POSIX 117331–117335: User CPU time is "the sum of thetms_utimeandtms_cutimefields … for the process in which utility is executed", System CPU likewisetms_stime + tms_cstime. The child's CPU is accumulated into the parent'stms_cutime/tms_cstimeafterwait(). The code instead readstms_start.tms_utime - tms_end.tms_utime(parent's own user time, start minus end). Even if #T1 is fixed, this measures the wrong process and has the sign inverted. Fix:user = (tms_end.tms_cutime + tms_end.tms_utime) - (tms_start.tms_cutime + tms_start.tms_utime), likewise for system with thes-variants; divide by_SC_CLK_TCK.
- #T3 — Child exit status is discarded;
timealways exits 0. ✓ fixed in Phase 1 (time()returns the child's code viaStatus::Utility(code);mainexits with it).time.rs:82(let _ = child.wait()...),time.rs:158(Status::Ok.exit()→ 0). POSIX EXIT STATUS 117435: "If the utility utility is invoked, the exit status oftimeshall be the exit status of utility." A successful spawn whose child exits 5 still yieldstimeexit 0. Fix: captureExitStatus, and on normal exit propagatecode(); on signal termination map to 128+signal (see #T5).
- #T4 — Runtime diagnostics are hardcoded English. ✓ fixed in Phase 4 (the three diagnostics now route through
plib::diag::error+gettext()).time.rs:144,148,152. POSIX 117404:LC_MESSAGESshall affect diagnostic message contents. - #T5 — Signal-terminated child not reflected in exit status. ✓ fixed in Phase 1 (
status.code()else128 + status.signal()).time.rs:82. When the utility dies from a signal,timenow exits 128+signum. - #T6 —
bindtextdomain("posixutils-rs", "locale")uses a relative path. ✓ fixed in Phase 4 (all four binaries now shareplib::diag::init_locale, which drops the relativebindtextdomain).time.rs:136.
-
time [-p] utility [argument...]—-pflag (time.rs:28-33),utility(35-36),argumentswithtrailing_var_arg(38-43). CONFORMS. - XBD 12.2 conformance /
--end-of-options — clap default; tests pass--explicitly (tests/time/mod.rs:42). -
utilityinvoked via PATH search —Command::new(&args.utility)usesexecvp-style PATH lookup (time.rs:72). CONFORMS. - Special-built-in/function/intrinsic operand → unspecified — N/A (external binary only; spec leaves this unspecified). CONFORMS.
- STDIN not used; child inherits stdio —
Stdio::inherit()(time.rs:74-75). CONFORMS. - STDOUT not used by
timeitself — timing goes to stderr. CONFORMS.
- Timing statistics written to standard error —
writeln!(io::stderr(), …)(time.rs:91-107). CONFORMS. -
-pformat"real %f\nuser %f\nsys %f\n"—time.rs:91-98emitsreal {:.6}\nuser {:.6}\nsys {:.6}+writeln!trailing\n. Six fractional digits ≥ the mandated ≥1. CONFORMS (format); values are wrong (#T1/#T2). - Default (non-
-p) format unspecified —time.rs:100-107"Elapsed/User/System time" is permitted. CONFORMS. - Timing values incorrect — (#T1/#T2) ✓ fixed in Phase 1; CPU stats now reflect the invoked utility.
| Var | Status | Notes |
|---|---|---|
LANG/LC_ALL/LC_CTYPE |
CONFORMS | setlocale(LC_ALL,"") at time.rs:134. |
LC_MESSAGES |
CONFORMS* | (#T4) ✓ Phase 4 routes diagnostics through gettext; *catalog shipping is a tree-wide deferral. |
LC_NUMERIC |
N/A | Rust {:.6} always uses . radix; POSIX-locale output is correct, other locales unspecified for the default format. |
NLSPATH (XSI) |
MISSING | No catalog support (tree-wide). |
PATH |
CONFORMS | Used by Command for utility lookup. |
- 127 when utility not found —
io::ErrorKind::NotFound → CommandNotFound → 127(time.rs:77-78,126,145). CONFORMS. - 126 when found but not invocable — other spawn errors →
ExecCommand → 126(time.rs:79,125,149). CONFORMS. - Exit status of invoked utility not propagated — (#T3) ✓ fixed in Phase 1;
timeexits with the utility's status. - 1–125 on internal
timeerror —TimeError → 1(time.rs:124,153). CONFORMS.
- Default —
grep -nE 'SIGCONT|SIGWINCH|signal' time.rs→ 0 matches; spec says "Default". CONFORMS.
Not covered:
- No test asserts the reported
user/sysvalues are non-zero for a CPU-bound child (would catch #T1/#T2). — ✓ Phase 1cpu_bound_child_reports_nonzero_cpu_time. - No test asserts
timepropagates a non-zero child exit code (#T3). — ✓ Phase 1propagates_child_exit_status. - No test asserts 126/127 mapping (only "not provided"/clap errors are exercised;
tests/time/mod.rs:51-57). (127 behaviorally spot-checked; no automated assertion yet.)
Implementation: datetime/date.rs (201 lines)
Tests: datetime/tests/date/mod.rs (139 lines, 6 #[test]s)
Spec: POSIX.1-2024, Vol. 3 §3, pp. 2818–2823
Reference slice: ~/tmp/posix.2024/sliced/xcu-shell-and-utilities/3-utilities/date.md
Date: 2026-06-24
The display path is solid: +format is rendered through libc strftime over
localtime_r/gmtime_r (honoring TZ, -u, and LC_TIME), a trailing
<newline> is always appended, and the default format matches the spec's
%a %b %e %H:%M:%S %Z %Y. The XSI set-time path handles the 8/10/12-digit
forms, but the 2-digit-year century inference is off by one against POSIX's
explicit ranges (#D1). Smaller issues: a legitimately-empty strftime result is
misreported as an error (#D2), diagnostics are hardcoded English (#D3), and
non-UTF-8 locale bytes can be mangled (#D4).
- #D1 — 2-digit-year century inference off by one. ✓ fixed in Phase 3 (extracted
infer_century(yy)with the< 69threshold; unit-tested over 68/69/70/99).date.rs:128-133. POSIX 91707–91708: "values in the range [69,99] shall refer to years 1969 to 1999 … values in the range [00,68] shall refer to years 2000 to 2068." The code didif year < 70 { year + 2000 } else { year + 1900 }, soyy=69→ 2069 (should be 1969).
- #D2 —
strftimereturning 0 is always treated as a fatal error. ✓ fixed in Phase 3, refined post-review.date.rs:87-95.strftime(3)returns 0 both when the buffer is too small and when the conversion legitimately produces an empty string. The fix grows the buffer and uses a first-byte sentinel to tell the two apart: on any success (incl. empty output)strftimewrites a terminating NUL at offset 0, sobuf[0]==0⇒ empty-but-valid (emit just the newline), whilebuf[0]!=0⇒ output didn't fit (grow, then error at the 64 KiB cap rather than silently dropping a huge format — addresses a Copilot follow-up; regression testtest_format_exceeds_buffer_errors). - #D3 — Runtime diagnostics hardcoded English. ✓ fixed in Phase 4 (all
eprintln!/Err(&str)diagnostics route throughplib::diag::error+gettext()with a uniformdate:prefix; the awkwardError: date: …double-prefix is gone).date.rs:49,56,70,94,101,144,154,161,175. POSIX 91728. - #D4 —
from_utf8_lossymay manglestrftimeoutput in non-UTF-8 locales. ✓ fixed in Phase 3 (stdout().write_all(&buf[..len])raw bytes + newline, no lossy decode).date.rs:88.strftimeemits bytes in the locale'sLC_CTYPEencoding;String::from_utf8_lossyreplaced invalid sequences with U+FFFD.
-
date [-u] [+format]and XSIdate [-u] mmddhhmm[[cc]yy]— both forms dispatched inmain(date.rs:189-198). CONFORMS. -
-uperforms ops as ifTZ=UTC0— usesgmtime_rfor display (date.rs:62-66) andchrono::Utcfor set (date.rs:150-156). CONFORMS. - XBD 12.2 /
--— clap default;+-prefixed operand is treated as a value, not an option. CONFORMS.
-
+formatrendered "as if bystrftime()" overlocaltime(&now)/gmtime(&now),now = time(0)—date.rs:54-86matches the spec algorithm exactly. CONFORMS. -
<newline>always appended tostrftimeoutput —println!("{}", timestr)(date.rs:89); empty format → bareprintln!()(date.rs:42). CONFORMS. - Set form
mmddhhmm(len 8) → current year —date.rs:116-122. CONFORMS. - Set form
mmddhhmmccyy(len 12) → explicit 4-digit year —date.rs:135-142. CONFORMS. - Set form
mmddhhmmyy(len 10) century inference — (#D1) ✓ fixed in Phase 3 viainfer_century. - Invalid field values rejected —
chrono::with_ymd_and_hmsnon-Single→Err(date.rs:151-164). CONFORMS.
- STDIN not used — no
stdin()call (grep -n stdin date.rs→ 0). CONFORMS. - Default output equals
+%a %b %e %H:%M:%S %Z %Y—DEF_TIMESTR(date.rs:17,190). CONFORMS. - STDERR only for diagnostics — all
eprintln!/error paths target stderr. CONFORMS (#D3 on i18n).
| Var | Status | Notes |
|---|---|---|
LANG/LC_ALL/LC_CTYPE |
CONFORMS | setlocale at date.rs:183; libc strftime honors them. |
LC_MESSAGES |
CONFORMS* | (#D3) ✓ Phase 4 routes diagnostics through gettext; *catalog shipping is a tree-wide deferral. |
LC_TIME |
CONFORMS | strftime over libc honors LC_TIME. |
TZ |
CONFORMS | localtime_r honors TZ; -u overrides to UTC. Verified by tests/date/mod.rs:24-60. |
NLSPATH (XSI) |
MISSING | Tree-wide. |
- 0 on success, >0 on error —
mainreturnsOk/propagatesErr(exit 1); explicitprocess::exit(1)inshow_timeerror paths. CONFORMS. - Default async events — no signal handling; spec says "Default". CONFORMS.
Not covered:
- No test exercises the set-time path at all (would catch #D1) — ✓ Phase 3 added a
century_boundariesunit test over theinfer_centuryhelper (set-time itself needs privilege). - No test asserts
+-empty / unusual formats (#D2). - No test for
-uset-form or non-UTF-8 locale output (#D4).
Implementation: datetime/sleep.rs (39 lines)
Tests: none (no datetime/tests/sleep/).
Spec: POSIX.1-2024, Vol. 3 §3, pp. 3433–3435
Reference slice: ~/tmp/posix.2024/sliced/xcu-shell-and-utilities/3-utilities/sleep.md
Date: 2026-06-24
sleep is nearly conforming: it sleeps integral seconds, accepts the full
2 147 483 647-second range (u64), and handles SIGALRM by ignoring it
(spec-permitted option 2). The one real defect is that sleep 0 is rejected by
the clap range guard, contradicting the "non-negative decimal integer" operand
definition.
- #S1 —
sleep 0is rejected. ✓ fixed in Phase 2 (range(1..)→range(0..); newtests/sleep/mod.rs).sleep.rs:17-18:value_parser!(u64).range(1..)made the minimum 1, sosleep 0exited non-zero with a clap range error. POSIX 115335: thetimeoperand is "A non-negative decimal integer" —0is valid and must suspend for "at least 0 seconds" (i.e. return immediately with exit 0).
-
#S2— clap diagnostics hardcoded English. DEFERRED (Phase 4). The range/parse error text is generated by clap, not by sleep, and is uniformly English across every clap-based utility in the tree. Routing it throughgettextwould require a custom clap error formatter; deferred as a tree-wide concern (same disposition as other audits). sleep itself emits no diagnostics.LC_MESSAGESremains inert for clap's own errors.
-
sleep time— single operand (sleep.rs:17-21). CONFORMS. -
timenon-negative integer — (#S1) ✓ fixed in Phase 2;sleep 0accepted. - Up to 2 147 483 647 seconds supported —
u64covers it;thread::sleep(Duration::from_secs)(sleep.rs:36). CONFORMS (RATIONALE 115397). - Integral seconds only —
u64, no fractional/suffix parsing. CONFORMS (POSIX requires only integral).
- STDIN/STDOUT not used; STDERR diagnostics only — no I/O in the binary beyond clap. CONFORMS.
-
SIGALRMhandled —libc::signal(SIGALRM, SIG_IGN)(sleep.rs:31-34) selects spec option 2 ("Effectively ignore the signal"). CONFORMS. - All other signals take the standard (default) action — no other handlers installed. CONFORMS (spec 115362).
| Var | Status | Notes |
|---|---|---|
LANG/LC_ALL/LC_CTYPE |
CONFORMS | setlocale at sleep.rs:25. |
LC_MESSAGES |
PARTIAL | (#S2) clap-English diagnostics. |
NLSPATH (XSI) |
MISSING | Tree-wide. |
- 0 on success / SIGALRM, >0 on error —
mainreturnsOk(exit 0);SIG_IGNmeans SIGALRM never terminates. CONFORMS.
Not covered:
- No tests exist for
sleepat all. ✓ Phase 2 addedtests/sleep/mod.rs:sleep 0exits 0 (#S1),sleep 1exits 0, non-numeric/negative operand exits >0.
Implementation: datetime/cal.rs (244 lines)
Tests: datetime/tests/cal/mod.rs (377 lines, 24 #[test]s)
Spec: POSIX.1-2024, Vol. 3 §3, pp. 2731–2733 (XSI)
Reference slice: ~/tmp/posix.2024/sliced/xcu-shell-and-utilities/3-utilities/cal.md
Date: 2026-06-24
cal is the cleanest utility in the crate. Operand handling matches the
Issue-6/Issue-7 RATIONALE (no operands → current month; single operand → whole
year; both → one month), the Julian→Gregorian switch with the September 1752
11-day gap is implemented and well-tested, year/month ranges are enforced, and
TZ drives "the current month." The only conformance gap is that LC_TIME does
not affect the calendar's month-name and weekday strings (#C1) — they are a
hardcoded English array passed through gettext() (which has no catalogs).
- #C1 —
LC_TIMEnot honored for month names / weekday header. ✓ fixed in Phase 5.cal.rsnow derives the month name (%B) and the 2-char weekday abbreviations (%a, with the reference weekday read back via%wso the alignment isTZ-correct) fromplib::locale::strftimeunderLC_TIME, with the English literals kept only as a fallback. The C/POSIX-locale output is byte-identical to before (January,Su Mo Tu We Th Fr Sa); a non-CLC_TIME(e.g.fr_FR.UTF-8→janvier) now localizes the names. POSIX 88457: "LC_TIMEDetermine the format and contents of the calendar." The spec leaves the format unspecified (Sunday-first and the column layout are unchanged); the month/day names are now anLC_TIMEresponsibility.
- XSI
cal [[month] year], no options —Args { month, year }(cal.rs:131-149);grep -n 'short\|long' cal.rs→ 0 option flags. CONFORMS. - No operands → current month/year —
cal.rs:219-223viachrono::Local::now(). CONFORMS. - Single operand → twelve-month calendar for that year —
cal.rs:225-228moves the lone operand intoyear. Matches RATIONALE 88486–88488. CONFORMS. - Both operands → one-month calendar —
cal.rs:234-241. CONFORMS. -
month1–12 enforced — claprange(1..=9999)admits the single-operand-is-year trick, thencal.rs:235-237rejectsmonth > 12;cal 0rejected by clap (range floor 1). CONFORMS. -
year1–9999 enforced — claprange(1..=9999)(cal.rs:143).cal 83→ A.D. 83 (APPLICATION USAGE 88478). CONFORMS.
- STDIN not used — no
stdin()call. CONFORMS. - STDOUT displays the calendar in unspecified format —
print_month/print_year(cal.rs:151-209). CONFORMS. - STDERR only for diagnostics — single error via
Err(gettext(...))→main→ stderr. CONFORMS.
- Julian calendar Jan 1, 1 – Sep 2, 1752; Gregorian Sep 14, 1752 – Dec 31, 9999 —
is_julian(cal.rs:43-59). CONFORMS. - September 1752 has 19 days (1, 2, 14–30) —
days_in_monthspecial case (cal.rs:105-110) + gap padding (cal.rs:171-191); verified bytests/cal/mod.rs:109-181. CONFORMS. - Julian vs Gregorian leap-year rules —
is_leap_year(cal.rs:64-70); verified for 100/1600/1700 (Julian) and 1900/2000/2024 (Gregorian) in tests. CONFORMS. - Day-of-week via JDN —
julian_day_number/day_of_week(cal.rs:74-100); Jan 1 year 1 = Saturday verified (tests/cal/mod.rs:238-267). CONFORMS.
| Var | Status | Notes |
|---|---|---|
LANG/LC_ALL/LC_CTYPE |
CONFORMS | setlocale at cal.rs:212. |
LC_MESSAGES |
PARTIAL | Catalogs absent (tree-wide); the one error string is gettext-wrapped (cal.rs:236). |
LC_TIME |
CONFORMS | (#C1) ✓ Phase 5: month/weekday names derived from plib::locale::strftime under LC_TIME. |
NLSPATH (XSI) |
MISSING | Tree-wide. |
TZ |
CONFORMS | Current month derived from chrono::Local::now(), which honors TZ (cal.rs:220). |
- 0 success / >0 error —
mainreturnsOk/Err(cal.rs:236,243). CONFORMS. - Default async events / consequences — no special handling required. CONFORMS.
Well covered (24 tests): operand forms, Sep-1752 gap, leap-year rules across Julian/Gregorian eras, day-of-week, month names, range errors. Gaps:
- No test asserts
LC_TIMEaffects month/weekday names (#C1) — ✓ Phase 5 addedtest_cal_lc_time_french_month_name(best-effort:janvierwhenfr_FRis installed, elseJanuary); the C-locale name assertions are now pinned toLC_ALL=Cfor determinism.
- PR A — "time: fix CPU accounting": #T1, #T2 (one fix site — add the
tms_endsnapshot and rewrite the delta to usetms_cutime/tms_cstime, end−start). Add a test asserting non-zerouserfor a busy-loop child. - PR B — "time: propagate utility exit status": #T3, #T5. Capture
ExitStatus, propagatecode()/ map signal to 128+n. Add a propagation test. - PR C — "sleep: accept
sleep 0": #S1. One-line range change + a small test module (the crate currently has none forsleep). - PR D — "date: century-window off-by-one": #D1 (threshold
70→69) plus a unit test over the 68/69/70/99 boundaries. - PR E — "date: robust strftime output": #D2 (empty-vs-overflow), #D4 (raw-bytes stdout).
- PR F — "datetime: i18n diagnostics": #T4, #D3 (+ #S2, #T6). Route runtime
diagnostics through
gettext()with a uniform<util>:prefix; normalizebindtextdomain. Mirrors thedev/plib::diagmigration. - PR G — "cal: LC_TIME month/weekday names": #C1. Derive names from libc
under
LC_TIME(reuseplib::locale::strftime).