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 = " | 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