1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
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;
month_abbr : table;
season : table;
rank : table;
colour : table;
term : table;
rite : table;
bible : table;
sigla : 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 rite t k = get t (fun x -> x.rite) k
let bible t k = get t (fun x -> x.bible) k
(* Not routed through [get]/[chain]: [sigla] is a set of RENDER SETTINGS
(Render.style_of_fields), not a per-key translation lookup, and it has no
miss-returns-the-key contract to keep -- a caller with no [sigla] table at
all just gets []. Order does not matter to callers (Render.style_of_fields
reads them by name), so [SM.bindings] (alphabetical) is fine here. *)
let sigla_fields t = SM.bindings t.sigla
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 month_abbr t n =
if n < 1 || n > 12 then string_of_int n
else match lookup t (fun x -> x.month_abbr) (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; month_abbr = empty_table; season = empty_table; rank = empty_table;
colour = empty_table; term = empty_table; rite = empty_table; bible = empty_table;
sigla = 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 ->
(* Merge EVERY section sharing [name], not just the first: a hand-edited
595-entry language file (Tasks 3/4's la.ini) WILL grow duplicate
[section] headers as contributors append entries over time -- a
second [celebration] block is the natural way to paste in a new
batch of names. Taking only the first match (the original
[List.find_opt] here) silently dropped every later block; the
failure then surfaces as a coverage report claiming those slugs have
"no Latin name", with nothing pointing back at the parser. Folding
over all matching sections, in file order, keeps this consistent
with the existing within-section behaviour below (last [SM.add]
wins): a key repeated across two blocks resolves to the later one,
exactly what a reader expects when appending to an INI file. *)
let find name =
List.fold_left
(fun m (s : OI.section) ->
if s.OI.name = name then List.fold_left (fun m (k, v) -> SM.add k v m) m s.OI.fields else m)
empty_table sections
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";
month_abbr = find "month_abbr";
season = find "season";
rank = find "rank";
colour = find "colour";
term = find "term";
rite = find "rite";
bible = find "bible";
sigla = find "sigla";
chain = None })
(* [bible] joins this reference set: it is translatable book names, so a file
lacking them is genuinely incomplete and [lang --check] should say so.
[sigla] deliberately does NOT: it is five citation-style settings with
working defaults (Render.default_style), not names a translator owes --
adding it here would make [--check] demand five settings from every
language file that has never needed them. *)
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 "month_abbr" t.month_abbr; qualify "season" t.season;
qualify "rank" t.rank; qualify "colour" t.colour; qualify "term" t.term; qualify "rite" t.rite;
qualify "bible" t.bible ]
|> List.sort compare
|