diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-19 13:34:53 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-19 13:34:53 +0200 |
| commit | 45bcbde502e07ad73b96039fcbb62471a3c94781 (patch) | |
| tree | 515bd1465c75072ece7c70f8e2bcabd493f80417 /lib | |
| parent | 6762ce46af3cb12bc6ae37cda762c5d95add7903 (diff) | |
| download | colitur-45bcbde502e07ad73b96039fcbb62471a3c94781.tar.gz colitur-45bcbde502e07ad73b96039fcbb62471a3c94781.zip | |
feat(naming): the language table
Maps strings to strings and nothing else -- no calendars, no dates, no
filesystem. That is what lets every command use it without the kernel
learning about presentation.
Every lookup is total, and a miss returns THE KEY rather than the empty
string. A partial translation is therefore usable from its first line,
and the fully-degraded case is exactly today's output (bare slugs) rather
than a blank page.
--raw is a real identity table, not a special case threaded through every
call site: one value the whole program passes around.
Reuses Overlay_ini's INI reader rather than growing a second one that
would drift in its comment, quoting and trimming rules; parse_sections is
exposed in the .mli for that, with no behaviour change.
Fixes one defect found while running the brief's own tests rather than
transcribing them blind: weekday's internal lookup key is an English
day-name word (month's is already the numeral string), so on a miss it
echoed that word instead of the documented numeral, breaking both the
0=Sunday convention and Lang.raw's own identity contract for weekday.
weekday/month now fall back to string_of_int n directly on a miss
instead of through get's generic echo-the-search-key path; month is
byte-identical since its key already equals string_of_int n.
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/kernel/overlay_ini.mli | 9 | ||||
| -rw-r--r-- | lib/naming/dune | 3 | ||||
| -rw-r--r-- | lib/naming/lang.ml | 96 | ||||
| -rw-r--r-- | lib/naming/lang.mli | 44 |
4 files changed, 152 insertions, 0 deletions
diff --git a/lib/kernel/overlay_ini.mli b/lib/kernel/overlay_ini.mli index bace93b..321cac1 100644 --- a/lib/kernel/overlay_ini.mli +++ b/lib/kernel/overlay_ini.mli @@ -32,6 +32,15 @@ Dates take three forms, matching {!Date_spec}: [MM-DD], [easter+N] or [easter-N], and [mon/day/nth] such as [oct/sun/1] or [oct/sun/-1]. *) +(** One [section] of a flat INI file. Exposed so other libraries (the language + and config files) reuse this reader rather than growing a second one that + would drift in its comment, quoting and trimming rules. *) +type section = { name : string; fields : (string * string) list } + +(** Split INI text into sections. [\[section\]] headers, [key = value] lines, + ';' and '#' comments, blank lines ignored. Never raises. *) +val parse_sections : string -> (section list, string) result + (** [parse ~rank_of_string text] is the overlay [text] denotes. [rank_of_string] is supplied by the rite, exactly as [Overlay.load] takes diff --git a/lib/naming/dune b/lib/naming/dune new file mode 100644 index 0000000..8af06c5 --- /dev/null +++ b/lib/naming/dune @@ -0,0 +1,3 @@ +(library + (name colitur_naming) + (libraries colitur_kernel)) diff --git a/lib/naming/lang.ml b/lib/naming/lang.ml new file mode 100644 index 0000000..0d902d5 --- /dev/null +++ b/lib/naming/lang.ml @@ -0,0 +1,96 @@ +module OI = Colitur_kernel.Overlay_ini + +module SM = Map.Make (String) + +type table = string SM.t + +type t = { + code : string; + fallback_code : string option; + celebration : table; + weekday : table; + month : table; + season : table; + rank : table; + colour : table; + term : table; + chain : t option; (** consulted when this table misses *) +} + +let empty_table = SM.empty + +let rec lookup t sel key = + match SM.find_opt key (sel t) with + | Some v -> Some v + | None -> ( match t.chain with Some b -> lookup b sel key | None -> None) + +(* A miss returns the KEY, never "". See lang.mli for why. *) +let get t sel key = match lookup t sel key with Some v -> v | None -> key + +let celebration t k = get t (fun x -> x.celebration) k +let season t k = get t (fun x -> x.season) k +let rank t k = get t (fun x -> x.rank) k +let colour t k = get t (fun x -> x.colour) k +let term t k = get t (fun x -> x.term) k + +let weekday_key = [| "sunday"; "monday"; "tuesday"; "wednesday"; "thursday"; "friday"; "saturday" |] + +(* weekday/month deliberately do NOT go through [get]: their SM key is a + presentation detail (an English day-name word for weekday, a numeral for + month), not the value a miss should degrade to. [get]'s generic + echo-the-search-key fallback would leak that word ("sunday") out of an + empty/raw table instead of the documented numeral -- [month] never showed + the bug because its key already equals [string_of_int n], but [weekday] + did, and it broke [Lang.raw]'s own identity contract (0 = Sunday, not + "sunday"). Falling back to [string_of_int n] directly keeps [month] + byte-identical and fixes [weekday]. *) +let weekday t n = + if n < 0 || n > 6 then string_of_int n + else match lookup t (fun x -> x.weekday) weekday_key.(n) with Some v -> v | None -> string_of_int n + +let month t n = + if n < 1 || n > 12 then string_of_int n + else match lookup t (fun x -> x.month) (string_of_int n) with Some v -> v | None -> string_of_int n + +let code t = t.code +let fallback_code t = t.fallback_code + +let raw = + { code = "raw"; fallback_code = None; celebration = empty_table; weekday = empty_table; + month = empty_table; season = empty_table; rank = empty_table; colour = empty_table; + term = empty_table; chain = None } + +let with_fallback t base = { t with chain = Some base } + +let of_string text = + match OI.parse_sections text with + | Error e -> Error e + | Ok sections -> + let find name = + match List.find_opt (fun (s : OI.section) -> s.OI.name = name) sections with + | Some s -> List.fold_left (fun m (k, v) -> SM.add k v m) empty_table s.OI.fields + | None -> empty_table + in + let meta = find "meta" in + (match SM.find_opt "lang" meta with + | None -> Error "language file has no [meta] lang = <code>" + | Some code -> + Ok + { code; + fallback_code = SM.find_opt "fallback" meta; + celebration = find "celebration"; + weekday = find "weekday"; + month = find "month"; + season = find "season"; + rank = find "rank"; + colour = find "colour"; + term = find "term"; + chain = None }) + +let keys t = + let qualify prefix m = SM.bindings m |> List.map (fun (k, v) -> (prefix ^ "." ^ k, v)) in + List.concat + [ qualify "celebration" t.celebration; qualify "weekday" t.weekday; + qualify "month" t.month; qualify "season" t.season; qualify "rank" t.rank; + qualify "colour" t.colour; qualify "term" t.term ] + |> List.sort compare diff --git a/lib/naming/lang.mli b/lib/naming/lang.mli new file mode 100644 index 0000000..c4432f6 --- /dev/null +++ b/lib/naming/lang.mli @@ -0,0 +1,44 @@ +(** A language table: strings to strings, nothing more. + + Knows nothing about calendars, dates or rites, and never touches the + filesystem -- callers hand it text. That is what lets every command use it + without the kernel learning about presentation. + + Every lookup is TOTAL. A key with no entry returns THE KEY ITSELF, never the + empty string: a partial translation must be usable from its first line, and + an untranslated day must still print something a reader can act on. This is + also why the pre-naming output (bare slugs) is exactly what an empty table + produces -- the degraded case is the old behaviour, not a blank page. *) + +type t + +(** Parse INI text. Never raises. [Error] on a malformed file or a missing + [\[meta\] lang]. *) +val of_string : string -> (t, string) result + +val code : t -> string +val fallback_code : t -> string option + +(** [with_fallback t base] resolves through [t] first, then [base], then the key. *) +val with_fallback : t -> t -> t + +(** The identity table: every lookup returns its key. This is what [--raw] uses, + so raw output is one table passed around rather than a special case threaded + through every call site. *) +val raw : t + +val celebration : t -> string -> string +val season : t -> string -> string +val rank : t -> string -> string +val colour : t -> string -> string +val term : t -> string -> string + +(** [weekday t n], 0 = Sunday. Out-of-range [n] returns [string_of_int n]. *) +val weekday : t -> int -> string + +(** [month t n], 1 = January. Out-of-range [n] returns [string_of_int n]. *) +val month : t -> int -> string + +(** Every (section-qualified key, value) pair, sorted. Used by [lang --dump] and + [lang --check]. Keys are qualified as e.g. ["celebration.ef-epiphany"]. *) +val keys : t -> (string * string) list |
