summaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-20 09:28:08 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-20 09:28:08 +0200
commit0f94c388880972f8e665ed01a6b38190bcc61499 (patch)
tree7f684dcd07c19eab798a5a8837677faf0556a58d /bin
parent286e362bd02e7ef4130cccb36fbeb16b4fbb5a56 (diff)
downloadcolitur-0f94c388880972f8e665ed01a6b38190bcc61499.tar.gz
colitur-0f94c388880972f8e665ed01a6b38190bcc61499.zip
feat(cli): --lang and --raw, colitur lang, colitur config
Names are Latin by default now; --raw restores the old byte-exact output, verified against a pre-branch binary built from a clean HEAD checkout, not merely asserted. day and readings gain a trailing display-name field (day's own name suffix, readings' own " | name" suffix) rather than a substitution in the slug's existing position: a name contains spaces and would otherwise break the space-separated columns, the same reasoning that already keeps readings a separate command. The field is present only when it differs from the slug already shown, which is a direct consequence of Lang.raw being the identity table (no lookup ever misses), not a special case threaded through day_line/readings_line -- under --raw, or any language with no entry for a given day, the trailing field is simply absent, byte for byte what this project printed before naming existed. An unknown language is an error naming what is available, never a silent fallback to Latin: a booklet quietly printed in the wrong language is worse than one that refuses to print. A declared fallback still chains, so a partial translation shows its fallback language rather than slugs. lang_dir mirrors data_dir's probe order exactly -- installed prefix, then build tree -- because an installed binary that finds calendar data but no language files is the failure this would otherwise ship. Config (lang/overlay/template/format) is loaded once, unconditionally, before dispatch: a config file that exists but fails to parse is fatal on every command, not only the ones that consult it, because a config the user wrote and colitur cannot honour is not something to silently carry on past. An unknown key or unknown section is reported and ignored, worded differently so a misspelled section reads differently from a misspelled key inside a recognised one. overlay's config default applies only when zero --overlay flags were given, never merged with a partial CLI list, so precedence stays flag > config > default in one direction throughout. colitur lang --list/--dump/--check and colitur config --show are what make "anyone can write a language file" true rather than merely permitted, the same pattern the overlay system already established with new-overlay and check. --check reports what is missing AND rejects an unknown slug (exit 1), because a key matching nothing is silently dead and its author would otherwise never learn why their own name does not appear. --dump emits sections in a fixed order (Lang.keys' own global sort keeps every section's keys contiguous), so two dumps of the same table are byte-identical. config --show resolves through the same Config.resolve every other command uses -- deliberately no separate provenance function, so the two cannot disagree -- and previews exactly what flags given on the same command line would resolve to anywhere else. One authorized change outside this task's own file list: emit_ics.ml's SUMMARY line was reading the unlocalised rank/colour pair ("class-1, white") beside an already-localised name, where the JSON schema right next to it correctly carries rank_name/colour_name ("II classis"/"albus"). Found while reviewing the published site; fixed at the source.
Diffstat (limited to 'bin')
-rw-r--r--bin/main.ml628
1 files changed, 556 insertions, 72 deletions
diff --git a/bin/main.ml b/bin/main.ml
index b2eef28..0a09a63 100644
--- a/bin/main.ml
+++ b/bin/main.ml
@@ -131,41 +131,20 @@ let data_dir () =
end
| _ -> if has_data installed then installed else build_tree
-(* TEMPORARY BRIDGE (naming-and-config Task 5): [View.of_days] now takes
- [~lang], but real CLI flag/config resolution (--lang, --raw,
- Colitur_naming.Config) is Task 6's own scope, not this one's. Rather than
- leave `emit`/`table`/`publish` showing bare slugs as their "resolved
- name" -- which is precisely the defect this whole branch exists to fix --
- this loads the shipped Latin table by default, using EXACTLY the probe
- order [data_dir] above already uses (installed prefix, then the build
- tree), so an installed binary finds its language file the same way it
- finds its calendar data. Task 6 replaces this wholesale with
- `--lang`/`--raw`/config resolution and per-command defaults; nothing
- here is meant to survive that task unchanged. A missing or malformed
- language file degrades to [Lang.raw] (name = slug) rather than crashing
- the CLI -- Task 6 is what makes that case a proper, reported error. *)
+(* [lang_dir] mirrors [data_dir]'s own probe order exactly -- installed
+ prefix first, then the build tree -- because a language file must resolve
+ the same way calendar data does, or an installed binary could find one
+ and not the other (exactly the defect a prior fix round found: lang/ had
+ no install rule at all, and the installed binary silently fell back to
+ raw slugs with no error). Accepted only if la.ini is actually readable
+ there, matching [data_dir]'s own "a candidate counts only if the data is
+ really there" discipline. *)
let lang_dir () =
let prefix = Filename.dirname (Filename.dirname Sys.executable_name) in
let installed = List.fold_left Filename.concat prefix [ "share"; "colitur"; "lang" ] in
if Sys.file_exists (Filename.concat installed "la.ini") then installed
else Filename.concat prefix "lang"
-let default_lang =
- lazy
- (let path = Filename.concat (lang_dir ()) "la.ini" in
- match open_in_bin path with
- | exception Sys_error _ -> Colitur_naming.Lang.raw
- | ic -> (
- match
- Fun.protect ~finally:(fun () -> close_in_noerr ic) (fun () ->
- really_input_string ic (in_channel_length ic))
- with
- | exception Sys_error _ -> Colitur_naming.Lang.raw
- | text -> (
- match Colitur_naming.Lang.of_string text with
- | Ok t -> t
- | Error _ -> Colitur_naming.Lang.raw)))
-
(* Loads the universal sanctoral layer and applies the one hand-authored
overlay over it (data/ef/adjustments.sexp -- see that file's own header):
[Overlay.apply]'s diagnostics are never silently dropped (Overlay.mli),
@@ -244,7 +223,26 @@ let load_ef_commons () =
| Error e -> Error (Printf.sprintf "failed to load %s: %s" path e)
| Ok commons -> Ok commons
-let day_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t)
+(* [~lang] resolves the observed slug to a display NAME, appended as a new
+ LAST field rather than substituted into the slug's own position: a name
+ contains spaces (Latin and English both), and inserting it where the slug
+ used to sit would break every fixed-position field after it (rank,
+ colour, the commemoration tail) for anyone parsing this line by column --
+ the same reasoning that keeps `readings` a separate command rather than
+ extra columns on `day`. [slug] itself is therefore untouched by [lang] in
+ this row, exactly as [Colitur_render.View]'s own [slug] field is.
+
+ The trailing field is present only when the resolved name actually
+ DIFFERS from the slug -- not gated on [--raw] as a special case, but as a
+ direct consequence of [Lang.raw] being the identity table (lang.mli):
+ under [--raw], [name = slug] always, so the field is always absent and
+ this row is BYTE-IDENTICAL to what it printed before this feature
+ existed, with no `if raw then ...` branch anywhere in this function. The
+ same holds for any language file that has genuinely no entry for a given
+ slug -- a miss also returns the slug (lang.mli), so an untranslated day
+ quietly gets no trailing field rather than a field that redundantly
+ repeats the slug it already printed. *)
+let day_line ~lang (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t)
=
let t = d.Colitur_kernel.Liturgical_day.temporal in
let cel = d.Colitur_kernel.Liturgical_day.observed in
@@ -257,14 +255,16 @@ let day_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kerne
let commemorations =
String.concat "" (List.map commemoration_suffix d.Colitur_kernel.Liturgical_day.commemorations)
in
- Printf.printf "%s %s %s %s %s %s %s%s\n" (D.to_iso8601 d.Colitur_kernel.Liturgical_day.date)
+ let slug_s = Colitur_kernel.Slug.to_string cel.Colitur_kernel.Celebration.slug in
+ let name = Colitur_naming.Lang.celebration lang slug_s in
+ let name_suffix = if name = slug_s then "" else " " ^ name in
+ Printf.printf "%s %s %s %s %s %s %s%s%s\n" (D.to_iso8601 d.Colitur_kernel.Liturgical_day.date)
(D.weekday_to_string t.Colitur_kernel.Temporal.weekday)
(Rite_ef.Vocab_ef.season_to_string t.Colitur_kernel.Temporal.season)
- week
- (Colitur_kernel.Slug.to_string cel.Colitur_kernel.Celebration.slug)
+ week slug_s
(Rite_ef.Vocab_ef.rank_to_string cel.Colitur_kernel.Celebration.rank)
(Colitur_kernel.Colour.to_string cel.Colitur_kernel.Celebration.colour)
- commemorations
+ commemorations name_suffix
(* The reading citations for a day, as its own row shape rather than extra
columns on [day_line]'s.
@@ -293,7 +293,14 @@ let day_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kerne
"citations-unresolved" checks assert exactly one First and one Gospel on
every day of every year 1583..9999 -- but the CLI must not assume a
guarantee the kernel makes about DATA rather than about types. *)
-let readings_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t)
+(* [~lang]: same append-only rule as [day_line] above, using this row's own
+ " | " field separator (chosen there precisely because a citation may
+ contain spaces or commas) rather than a plain space -- a resolved name
+ can equally contain spaces, and " | " is what already keeps this row's
+ fields unambiguous. Present only when the resolved name differs from the
+ slug, for the identical reason [day_line] gives: under [--raw] this row
+ is therefore byte-identical to what it printed before. *)
+let readings_line ~lang (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t)
=
let cel = d.Colitur_kernel.Liturgical_day.observed in
let part_ref p =
@@ -305,11 +312,15 @@ let readings_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_
| Some c -> c.Colitur_kernel.Citation.reference
| None -> "-"
in
- Printf.printf "%s %s | %s | %s\n"
+ let slug_s = Colitur_kernel.Slug.to_string cel.Colitur_kernel.Celebration.slug in
+ let name = Colitur_naming.Lang.celebration lang slug_s in
+ let name_suffix = if name = slug_s then "" else " | " ^ name in
+ Printf.printf "%s %s | %s | %s%s\n"
(D.to_iso8601 d.Colitur_kernel.Liturgical_day.date)
- (Colitur_kernel.Slug.to_string cel.Colitur_kernel.Celebration.slug)
+ slug_s
(part_ref Colitur_kernel.Citation.First)
(part_ref Colitur_kernel.Citation.Gospel)
+ name_suffix
(* One civil year, Jan 1 - Dec 31, matching [temporal_report]'s own scan --
NOT one liturgical year: [Colitur_kernel.Calendar.year] resolves a single
@@ -402,8 +413,8 @@ let resolved_year_days ~overlays y =
let resolved_year_report ~line ~overlays y =
List.iter line (resolved_year_days ~overlays y)
-let day_report ~overlays y = resolved_year_report ~line:day_line ~overlays y
-let readings_report ~overlays y = resolved_year_report ~line:readings_line ~overlays y
+let day_report ~lang ~overlays y = resolved_year_report ~line:(day_line ~lang) ~overlays y
+let readings_report ~lang ~overlays y = resolved_year_report ~line:(readings_line ~lang) ~overlays y
(* Task 8: `colitur emit` -- the five template-family emitters built in
Tasks 5-7, wired to a year RANGE rather than a single year, because a
@@ -454,7 +465,7 @@ let check_dtstamp = function
s;
exit 2
-let emit_report ~format ~overlays ~dtstamp ~from_y ~to_y =
+let emit_report ~lang ~format ~overlays ~dtstamp ~from_y ~to_y =
check_dtstamp dtstamp;
if from_y > to_y then begin
Printf.eprintf "colitur: --from %d is after --to %d\n" from_y to_y;
@@ -463,7 +474,7 @@ let emit_report ~format ~overlays ~dtstamp ~from_y ~to_y =
for y = from_y to to_y do
let days = resolved_year_days ~overlays y in
let v =
- Colitur_render.View.of_days ~lang:(Lazy.force default_lang) ~vocab:Rite_ef.Vocab_ef.vocab ~rite:"ef" ~year:y days
+ Colitur_render.View.of_days ~lang ~vocab:Rite_ef.Vocab_ef.vocab ~rite:"ef" ~year:y days
in
match format with
| "csv" ->
@@ -538,6 +549,98 @@ let read_file path =
Ok s)
with exn -> Error (Printf.sprintf "cannot read template %s: %s" path (Printexc.to_string exn)))
+(* The config file supplies DEFAULTS for --lang, --overlay, --template and
+ --format when the corresponding flag is absent -- Colitur_naming.Config
+ owns precedence (flag > config > default). Location follows the XDG base
+ directory convention: $XDG_CONFIG_HOME/colitur/config.ini, or
+ ~/.config/colitur/config.ini when that variable is unset or blank. An
+ unresolvable HOME (neither variable set) means no config path at all --
+ [load_config] below treats that exactly like a missing file, not an
+ error, since colitur without any config must keep working. *)
+let config_path () =
+ match Sys.getenv_opt "XDG_CONFIG_HOME" with
+ | Some d when String.trim d <> "" -> Filename.concat d "colitur/config.ini"
+ | _ -> (
+ match Sys.getenv_opt "HOME" with
+ | Some h -> Filename.concat h ".config/colitur/config.ini"
+ | None -> "")
+
+(* A config file is OPTIONAL: none at [config_path ()] behaves exactly as
+ colitur always has (Colitur_naming.Config.empty). A file that EXISTS but
+ fails to read or parse is fatal -- a config the user wrote and colitur
+ cannot honour is not something to silently carry on past. An unknown key
+ or section is reported but never fatal (Config.mli's own contract): a
+ config written for a newer colitur must still work on an older one, but a
+ silently-ignored typo is how a setting the user believes is active quietly
+ does nothing. Two separate warnings, not one, so a misspelled SECTION
+ (e.g. [deafults]) reads differently from a misspelled KEY inside a
+ recognised one -- Config.mli documents exactly this distinction. *)
+let load_config () =
+ let p = config_path () in
+ if p = "" || not (Sys.file_exists p) then Colitur_naming.Config.empty
+ else
+ match read_file p with
+ | Error _ ->
+ Printf.eprintf "colitur: cannot read config file %s\n" p;
+ exit 2
+ | Ok text -> (
+ match Colitur_naming.Config.of_string text with
+ | Error msg ->
+ Printf.eprintf "colitur: %s: %s\n" p msg;
+ exit 2
+ | Ok c ->
+ List.iter
+ (fun k -> Printf.eprintf "colitur: %s: unknown setting %S (ignored)\n" p k)
+ (Colitur_naming.Config.unknown_keys c);
+ List.iter
+ (fun s -> Printf.eprintf "colitur: %s: unknown section [%s] (ignored)\n" p s)
+ (Colitur_naming.Config.unknown_sections c);
+ c)
+
+(* An unknown language is an ERROR naming what is available, never a silent
+ fallback to Latin: a booklet quietly printed in the wrong language is
+ worse than one that refuses to print. [raw] takes priority
+ unconditionally and short-circuits everything else -- [--raw] IS
+ [Lang.raw], the identity table, handed to the caller directly, rather
+ than a second "is this raw" case threaded through every call site
+ downstream (View.of_days, day_line, readings_line all just take a
+ [Lang.t] and do not know or care whether it came from [--raw] or a real
+ file). *)
+let load_lang ~raw ~flag ~config =
+ if raw then Colitur_naming.Lang.raw
+ else
+ let code, _src = Colitur_naming.Config.resolve ~flag ~config ~default:"la" in
+ let path =
+ if String.contains code '/' || Filename.check_suffix code ".ini" then code
+ else Filename.concat (lang_dir ()) (code ^ ".ini")
+ in
+ match read_file path with
+ | Error _ ->
+ Printf.eprintf "colitur: no language %S (looked in %s); try: colitur lang --list\n" code
+ (lang_dir ());
+ exit 2
+ | Ok text -> (
+ match Colitur_naming.Lang.of_string text with
+ | Error msg ->
+ Printf.eprintf "colitur: %s: %s\n" path msg;
+ exit 2
+ | Ok t -> (
+ (* Chain to the declared fallback, so a partial translation shows
+ its fallback language rather than bare slugs. A fallback that
+ itself fails to load or parse does not take [t] down with it
+ -- [t] is already a good table; losing only the fallback field
+ is better than losing the whole language over a defect in a
+ file [t] merely NAMES. *)
+ match Colitur_naming.Lang.fallback_code t with
+ | None -> t
+ | Some fb -> (
+ match read_file (Filename.concat (lang_dir ()) (fb ^ ".ini")) with
+ | Error _ -> t
+ | Ok ftext -> (
+ match Colitur_naming.Lang.of_string ftext with
+ | Error _ -> t
+ | Ok base -> Colitur_naming.Lang.with_fallback t base))))
+
let extension path =
match String.rindex_opt path '.' with
| Some i -> String.sub path i (String.length path - i)
@@ -548,7 +651,7 @@ let extension path =
output (unescaped LaTeX/HTML metacharacters) that looks fine until it does
not -- the same "never silently substitute" discipline [data_dir]'s own
[COLITUR_DATA_DIR] handling documents above. *)
-let table_report ~template ~flavour_opt ~overlays y =
+let table_report ~lang ~template ~flavour_opt ~overlays y =
let flavour =
match flavour_opt with
| Some name -> (
@@ -573,7 +676,7 @@ let table_report ~template ~flavour_opt ~overlays y =
exit 2
| Ok src -> (
let days = resolved_year_days ~overlays y in
- let v = Colitur_render.View.of_days ~lang:(Lazy.force default_lang) ~vocab:Rite_ef.Vocab_ef.vocab ~rite:"ef" ~year:y days in
+ let v = Colitur_render.View.of_days ~lang ~vocab:Rite_ef.Vocab_ef.vocab ~rite:"ef" ~year:y days in
match Colitur_render.Template.render_string ~flavour src v with
| Error e ->
(* The template is user input; a parse failure is reported with the
@@ -760,7 +863,7 @@ let index_html ~from_y ~to_y =
</body></html>\n";
Buffer.contents b
-let publish_report ~from_y ~to_y ~out ~overlays ~dtstamp ~prune =
+let publish_report ~lang ~from_y ~to_y ~out ~overlays ~dtstamp ~prune =
check_dtstamp dtstamp;
if from_y > to_y then begin
Printf.eprintf "colitur: --from %d is after --to %d\n" from_y to_y;
@@ -792,7 +895,7 @@ let publish_report ~from_y ~to_y ~out ~overlays ~dtstamp ~prune =
in
for y = from_y to to_y do
let days = resolved_year_days ~overlays y in
- let v = Colitur_render.View.of_days ~lang:(Lazy.force default_lang) ~vocab:Rite_ef.Vocab_ef.vocab ~rite:"ef" ~year:y days in
+ let v = Colitur_render.View.of_days ~lang ~vocab:Rite_ef.Vocab_ef.vocab ~rite:"ef" ~year:y days in
let ys = string_of_int y in
emit ("ef/" ^ ys ^ ".json") (Colitur_render.Emit_json.year v);
emit ("ef/" ^ ys ^ ".csv") (Colitur_render.Emit_csv.year v);
@@ -805,7 +908,7 @@ let publish_report ~from_y ~to_y ~out ~overlays ~dtstamp ~prune =
(fun d ->
let iso = D.to_iso8601 d.Colitur_kernel.Liturgical_day.date in
let mm = String.sub iso 5 2 and dd = String.sub iso 8 2 in
- let one = Colitur_render.View.of_days ~lang:(Lazy.force default_lang) ~vocab:Rite_ef.Vocab_ef.vocab ~rite:"ef" ~year:y [ d ] in
+ let one = Colitur_render.View.of_days ~lang ~vocab:Rite_ef.Vocab_ef.vocab ~rite:"ef" ~year:y [ d ] in
emit (Printf.sprintf "ef/%s/%s/%s.json" ys mm dd) (Colitur_render.Emit_json.year one))
days
done;
@@ -866,20 +969,26 @@ usage:
colitur temporal <year> the temporal cycle, one line per day
colitur day <year> the resolved day identity, one line per day
colitur readings <year> the Mass reading citations, one line per day
- colitur day|readings <year> --overlay FILE [--overlay FILE ...]
+ colitur day|readings <year> [--overlay FILE ...] [--lang CODE|FILE] [--raw]
colitur emit --format csv|json|sexp|xml|ics --from Y --to Y
- [--overlay FILE ...] [--dtstamp S]
+ [--overlay FILE ...] [--dtstamp S] [--lang CODE|FILE] [--raw]
render a resolved year range through one of five emitters
colitur table --year Y --template FILE [--flavour X] [--overlay FILE ...]
+ [--lang CODE|FILE] [--raw]
colitur render --template FILE --year Y [--flavour X] [--overlay FILE ...]
+ [--lang CODE|FILE] [--raw]
compute year Y and render it through FILE, a logic-less
Mustache-family template; table and render are the same
operation, two names (see "rendering" below)
colitur publish --from Y --to Y --out DIR [--overlay FILE ...] [--prune]
- [--dtstamp S]
+ [--dtstamp S] [--lang CODE|FILE] [--raw]
write the static tree: per-year csv/json/xml/ics, one JSON
file per day, the schema and a generated index (see
"publish" below)
+ colitur lang --list which language files this build can find
+ colitur lang --dump CODE a language's full key set, in INI form, on stdout
+ colitur lang --check FILE what a language file is missing, and any typo'd key
+ colitur config --show every setting, its value and where it came from
colitur new-overlay print a starter overlay file to stdout
colitur convert FILE.ini flat INI overlay -> S-expression, on stdout
colitur check FILE ... load an overlay, say what it does, exit 2 if not
@@ -890,14 +999,21 @@ usage:
31 December of that year, not a liturgical year.
output formats:
- day date weekday season week slug rank colour [+commemoration ...]
+ day date weekday season week slug rank colour [+commemoration ...] [name]
2026-04-05 sunday paschaltide 1 ef-easter-sunday class-1 white
- readings date slug | Epistle | Gospel
+ readings date slug | Epistle | Gospel [| name]
2026-12-25 ef-nativity | Heb 1:1-12 | John 1:1-14
A citation contains spaces, so readings uses " | " between its fields while
day stays space-separated; that is why they are separate commands rather
- than extra columns.
+ than extra columns. The same reasoning is why the resolved display NAME,
+ present by default (see "naming" below), is appended as the LAST field
+ rather than substituted for the slug shown above: a name may itself
+ contain spaces, and inserting it earlier in the row would break every
+ fixed-position field that follows it. It is present only when it differs
+ from the slug already shown -- under --raw, or any language with no entry
+ for that particular day, the trailing field is simply absent, which is
+ what makes --raw byte-identical to this program's pre-naming output.
emit one schema (season, week, slug, rank, colour, subject, names,
citations, commemorations), rendered five ways: csv (RFC 4180,
@@ -942,6 +1058,44 @@ overlays:
(Nth_weekday (month M) (nth N) (weekday W)) with N negative to
count from the end of the month.
+naming:
+ --lang CODE|FILE applies to `day`, `readings`, `emit`, `table`, `render`
+ and `publish`. A CODE (e.g. `la`, `en`) is looked up as
+ <lang-dir>/CODE.ini; a value containing '/' or ending ".ini" is
+ read as a literal path instead. Default `la`, overridable by a
+ config file (see below). An unknown language is a hard ERROR
+ naming what is available (try `colitur lang --list`), never a
+ silent fallback to Latin -- a booklet quietly printed in the
+ wrong language is worse than one that refuses to print. A
+ language file may declare `fallback = CODE` in its [meta]
+ section, so a partial translation shows its fallback language
+ for the keys it does not itself carry, rather than bare slugs.
+
+ --raw restores every command's pre-naming output: `name` (and every
+ other localised field `emit`'s schema carries) equals the bare
+ machine slug, exactly as if no language had ever been resolved.
+ This is not a special case threaded through the naming code --
+ it is `colitur lang`'s own identity table, under which every
+ lookup echoes its key back unchanged.
+
+ colitur lang --list which language files this build can find, and
+ each one's own declared fallback, if any.
+ colitur lang --dump CODE the named language's full key set, in INI
+ form, on stdout -- a starting point for a new
+ translation, or a way to diff two versions of
+ one.
+ colitur lang --check FILE what a language file is MISSING (a real slug
+ with no entry) and, separately, any entry
+ naming a slug that does not exist at all (a
+ typo, silently dead otherwise) -- exits 1 if
+ anything is unknown, so it fits a Makefile or
+ a pre-commit hook.
+ colitur config --show every effective setting -- lang, overlay,
+ template, format -- its resolved value, and
+ where it came from: `flag`, `config` or
+ `default`. See colitur-config(5) for the
+ config file's location and full precedence.
+
rendering:
--template FILE (required on `table`/`render`) is a logic-less Mustache-
family template: {{placeholder}}, {{#section}}...{{/section}},
@@ -953,10 +1107,11 @@ rendering:
slug, rank, colour, subject, names, citations, commemorations),
reshaped into a booklet (`days`) and a month grid (`weeks`, with
padding cells for the leading/trailing blanks); see
- colitur-templates(5) for the full field list, the syntax and the
- scope-shadowing hazard (an inner key silently loses to an outer
- key of the same name -- a bare {{name.la}} inside a day resolves
- to the enclosing MONTH's name, not the day's own).
+ colitur-templates(5) for the full field list, the syntax, and
+ the one remaining scope hazard (`num` -- both a month and a week
+ carry it, and only the innermost one is ever meant). `name` is a
+ plain resolved string, not a lang-keyed object, so it carries no
+ equivalent hazard of its own.
--flavour X selects how interpolated VALUES are escaped (never the
template's own literal markup, which is the author's). One of:
@@ -1033,10 +1188,11 @@ exit status:
Reading references only (e.g. "Jn 3:16"); never scripture text.
See colitur(1) for the full description and the sources it computes against,
-colitur-overlay(5) for the overlay file format in full, and
+colitur-overlay(5) for the overlay file format in full,
colitur-templates(5) for the template format in full -- the syntax, the
-scope-shadowing hazard, the six flavours' escaping, and the full view-model
-field reference.|}
+remaining scope hazard, the six flavours' escaping, and the full view-model
+field reference -- and colitur-config(5) for the config file's location and
+precedence in full.|}
let print_help () =
print_endline help_text;
@@ -1047,7 +1203,8 @@ let usage () =
"colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> | colitur \
readings <year> | colitur emit --format FMT --from Y --to Y | colitur table --year Y --template \
FILE | colitur render --template FILE --year Y | colitur publish --from Y --to Y --out DIR | \
- colitur check FILE | colitur new-overlay (try: colitur --help)";
+ colitur lang --list|--dump CODE|--check FILE | colitur config --show | colitur check FILE | \
+ colitur new-overlay (try: colitur --help)";
exit 2
let with_year ys f =
@@ -1077,9 +1234,14 @@ let with_year ys f =
`table`/`render` take one year and one template file, never a range or a
repeatable list. *)
(* [out] (Task 12, `publish`) is single-valued like [format]/[year]/etc.
- [prune] is the one plain boolean flag in this whole record -- every other
- field here takes a value, but [--prune] does not, so it cannot reuse the
- `"--flag" :: v :: rest` shape the value-taking flags share below. *)
+ [prune] is a plain boolean flag -- every other field up to here takes a
+ value, but [--prune] does not, so it cannot reuse the `"--flag" :: v ::
+ rest` shape the value-taking flags share below. *)
+(* [lang]/[raw] (Task 6): [lang] is single-valued like [format]/[template]/
+ etc (a language CODE or a file path); [raw] is a second plain boolean,
+ the same shape as [prune]. [dump]/[check]/[list]/[show] (Task 7,
+ `colitur lang`/`colitur config`) follow the identical two shapes --
+ [dump]/[check] each take one value, [list]/[show] are bare. *)
type parsed_args = {
overlays : string list;
format : string option;
@@ -1091,6 +1253,12 @@ type parsed_args = {
flavour : string option;
out : string option;
prune : bool;
+ lang : string option;
+ raw : bool;
+ dump : string option;
+ check : string option;
+ list : bool;
+ show : bool;
positional : string list;
}
@@ -1116,6 +1284,15 @@ let parse_args argv =
| "--out" :: v :: rest -> go { acc with out = Some v } rest
| [ "--out" ] -> Error "--out needs a directory path"
| "--prune" :: rest -> go { acc with prune = true } rest
+ | "--lang" :: v :: rest -> go { acc with lang = Some v } rest
+ | [ "--lang" ] -> Error "--lang needs a language code or file path"
+ | "--raw" :: rest -> go { acc with raw = true } rest
+ | "--dump" :: v :: rest -> go { acc with dump = Some v } rest
+ | [ "--dump" ] -> Error "--dump needs a language code"
+ | "--check" :: v :: rest -> go { acc with check = Some v } rest
+ | [ "--check" ] -> Error "--check needs a file path"
+ | "--list" :: rest -> go { acc with list = true } rest
+ | "--show" :: rest -> go { acc with show = true } rest
(* The recognised bare flags pass through as positional words for the
dispatch below to match; anything else beginning with '-' is rejected
rather than silently treated as a command or a year. *)
@@ -1128,7 +1305,8 @@ let parse_args argv =
in
go
{ overlays = []; format = None; from_y = None; to_y = None; dtstamp = None; year = None;
- template = None; flavour = None; out = None; prune = false; positional = [] }
+ template = None; flavour = None; out = None; prune = false; lang = None; raw = false;
+ dump = None; check = None; list = false; show = false; positional = [] }
argv
(* Sibling to [reject_overlays_for]: `emit`'s own four flags have no meaning
@@ -1186,6 +1364,29 @@ let reject_publish_flags_for cmd ~out ~prune =
exit 2
end
+(* Sibling to the four rejectors above: `--lang`/`--raw` (Task 6) have
+ meaning only on the commands that resolve display names --
+ `day`/`readings`/`emit`/`table`/`render`/`publish` -- and not on
+ `easter`/`temporal` (read no sanctoral data, exactly like `--overlay`),
+ `check`/`convert`/`new-overlay` (operate on overlay files, not a
+ rendered calendar), or `lang`/`config` themselves (which take their OWN
+ flags, `--dump`/`--check`/`--list`/`--show`, disjoint from these). *)
+let reject_lang_for cmd ~lang ~raw =
+ if lang <> None || raw then begin
+ Printf.eprintf "colitur: --lang/--raw have no effect on `%s`; refusing rather than ignoring them\n" cmd;
+ exit 2
+ end
+
+(* Sibling again: `--dump`/`--check`/`--list`/`--show` (Task 7) belong only
+ to `colitur lang` and `colitur config` respectively. *)
+let reject_lang_subcommand_flags_for cmd ~dump ~check ~list ~show =
+ if dump <> None || check <> None || list || show then begin
+ Printf.eprintf
+ "colitur: --dump/--check/--list/--show have no effect on `%s`; refusing rather than ignoring them\n"
+ cmd;
+ exit 2
+ end
+
(* `colitur check FILE...` -- load a user overlay, apply it to the real
shipped calendar, and say what it did, without printing a year of output.
@@ -1290,6 +1491,184 @@ let convert_report path =
print_string sexp;
exit 0)
+(* `colitur lang --list` -- what language files this build can find, from
+ [lang_dir ()], the same probe [load_lang] itself uses. Each is opened and
+ parsed (not merely listed by filename) so a malformed file is flagged
+ here rather than only failing later when someone actually tries to use
+ it. *)
+let lang_list () =
+ let dir = lang_dir () in
+ match Sys.readdir dir with
+ | exception Sys_error _ ->
+ Printf.eprintf "colitur: no language directory at %s\n" dir;
+ exit 2
+ | files ->
+ Array.sort compare files;
+ Array.iter
+ (fun f ->
+ if Filename.check_suffix f ".ini" then begin
+ let code = Filename.remove_extension f in
+ match read_file (Filename.concat dir f) with
+ | Ok t -> (
+ match Colitur_naming.Lang.of_string t with
+ | Ok l ->
+ Printf.printf "%-6s %s\n" code
+ (match Colitur_naming.Lang.fallback_code l with
+ | Some fb -> "(falls back to " ^ fb ^ ")"
+ | None -> "")
+ | Error e -> Printf.printf "%-6s (unreadable: %s)\n" code e)
+ | Error e -> Printf.printf "%-6s (unreadable: %s)\n" code e
+ end)
+ files
+
+(* Every slug the engine can actually emit -- the observed office AND every
+ commemoration/transfer, exactly the four-field walk
+ test/test_lang_coverage.ml's own [slugs_of_day] performs -- over the same
+ 2020-2045 window that test measures against. This is [colitur lang
+ --check]'s own reference set: a language file is judged against what the
+ engine can really produce, not against an arbitrarily chosen sample. *)
+let all_known_slugs () =
+ match load_ef_data () with
+ | Error msg ->
+ Printf.eprintf "colitur: %s\n" msg;
+ exit 2
+ | Ok (layer, lectionary, commons) ->
+ let context = Rite_ef.context ~lectionary ~commons in
+ let slug (c : _ Colitur_kernel.Celebration.t) =
+ Colitur_kernel.Slug.to_string c.Colitur_kernel.Celebration.slug
+ in
+ let slugs_of_day
+ (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) =
+ slug d.Colitur_kernel.Liturgical_day.observed
+ :: List.map (fun (c, _priv) -> slug c) d.Colitur_kernel.Liturgical_day.commemorations
+ @ (match d.Colitur_kernel.Liturgical_day.transferred_in with
+ | None -> []
+ | Some c -> [ slug c ])
+ @ List.map (fun (c, _date) -> slug c) d.Colitur_kernel.Liturgical_day.transferred_out
+ in
+ let seen = Hashtbl.create 1024 in
+ for y = 2020 to 2045 do
+ Array.iter
+ (fun d -> List.iter (fun s -> Hashtbl.replace seen s ()) (slugs_of_day d))
+ (Colitur_kernel.Calendar.year context layer y)
+ done;
+ Hashtbl.fold (fun k () acc -> k :: acc) seen [] |> List.sort compare
+
+(* `colitur lang --dump CODE` -- the named language's full key set, in INI
+ form, on stdout for redirection. [Lang.keys] already returns every
+ (section-qualified key, value) pair SORTED as one flat list (lang.mli):
+ because every key sharing a section also shares that section's own
+ string prefix, and no two section names are a prefix of one another, the
+ sort keeps every section's own keys contiguous -- so a single pass
+ emitting a fresh `[section]` header each time the prefix changes
+ reconstructs proper INI blocks, in a FIXED (alphabetical) order, with no
+ second grouping step. Two dumps of the same table are therefore
+ byte-identical, which is what makes `--dump` diffable across two
+ versions of a translation. *)
+let lang_dump code =
+ let path = Filename.concat (lang_dir ()) (code ^ ".ini") in
+ match read_file path with
+ | Error _ ->
+ Printf.eprintf "colitur: no language %S (looked in %s); try: colitur lang --list\n" code
+ (lang_dir ());
+ exit 2
+ | Ok text -> (
+ match Colitur_naming.Lang.of_string text with
+ | Error msg ->
+ Printf.eprintf "colitur: %s: %s\n" path msg;
+ exit 2
+ | Ok t ->
+ Printf.printf "[meta]\nlang = %s\n" (Colitur_naming.Lang.code t);
+ (match Colitur_naming.Lang.fallback_code t with
+ | Some fb -> Printf.printf "fallback = %s\n" fb
+ | None -> ());
+ let cur = ref "" in
+ List.iter
+ (fun (qk, v) ->
+ match String.index_opt qk '.' with
+ | None -> ()
+ | Some i ->
+ let section = String.sub qk 0 i in
+ let key = String.sub qk (i + 1) (String.length qk - i - 1) in
+ if section <> !cur then begin
+ print_newline ();
+ Printf.printf "[%s]\n" section;
+ cur := section
+ end;
+ Printf.printf "%s = %s\n" key v)
+ (Colitur_naming.Lang.keys t))
+
+(* `colitur lang --check FILE` -- the difference between "you may write a
+ language file" and "you can": without this, a translator has to
+ reverse-engineer the key set from source. Reports what is MISSING
+ (a real slug the file has no entry for) AND what is UNKNOWN (a
+ [celebration] key matching no real slug at all -- a typo, or a slug from
+ a version of colitur this file was not written against) -- a key
+ matching nothing is silently dead, and its author would otherwise never
+ learn why their translation does not appear. Exits 2 only on UNKNOWN: a
+ partial file (some slugs missing) is exactly the shippable, in-progress
+ state [Lang.with_fallback] exists for, but a typo naming nothing is
+ always worth flagging as a failure, the same "never silently dead"
+ standard [Config.unknown_keys] holds a config file to. *)
+let lang_check path =
+ match read_file path with
+ | Error msg ->
+ Printf.eprintf "colitur: %s\n" msg;
+ exit 2
+ | Ok text -> (
+ match Colitur_naming.Lang.of_string text with
+ | Error msg ->
+ Printf.eprintf "colitur: %s: %s\n" path msg;
+ exit 2
+ | Ok t ->
+ let known = all_known_slugs () in
+ let have =
+ List.filter_map
+ (fun (k, _) ->
+ match String.index_opt k '.' with
+ | Some i when String.sub k 0 i = "celebration" ->
+ Some (String.sub k (i + 1) (String.length k - i - 1))
+ | _ -> None)
+ (Colitur_naming.Lang.keys t)
+ in
+ let missing = List.filter (fun s -> not (List.mem s have)) known in
+ let unknown = List.filter (fun s -> not (List.mem s known)) have in
+ List.iter (fun s -> Printf.printf "missing: %s\n" s) (List.sort compare missing);
+ List.iter (fun s -> Printf.printf "unknown slug: %s\n" s) (List.sort compare unknown);
+ Printf.printf "%s: %d of %d celebrations named, %d missing, %d unknown\n" path
+ (List.length known - List.length missing)
+ (List.length known) (List.length missing) (List.length unknown);
+ if unknown <> [] then exit 1)
+
+(* `colitur config --show` -- each effective setting, its resolved value,
+ and where it came from (`flag`/`config`/`default`), via
+ [Config.resolve]. There is deliberately no separate "provenance"
+ function: [resolve] already returns the source alongside the value
+ (config.mli), and a second entry point recomputing it independently
+ would let the two disagree. [lang]/[template]/[format] are scalars, each
+ resolved the same way `colitur day`/`table`/`emit` would resolve them
+ given these SAME command-line flags (so `config --show --lang fr` shows
+ exactly what a real `--lang fr` run would use); [overlay] is a list, so
+ it has no single "value" to resolve -- shown as one line per effective
+ entry instead, with its own source. *)
+let config_show ~lang_flag ~template_flag ~format_flag ~overlays_flag config =
+ let cpath = config_path () in
+ Printf.printf "config file: %s (%s)\n" cpath
+ (if cpath <> "" && Sys.file_exists cpath then "exists" else "not found");
+ let scalar name flag cfgval default =
+ let v, src = Colitur_naming.Config.resolve ~flag ~config:cfgval ~default in
+ Printf.printf "%-10s %-24s (%s)\n" name v src
+ in
+ scalar "lang" lang_flag (Colitur_naming.Config.lang config) "la";
+ scalar "template" template_flag (Colitur_naming.Config.template config) "(none)";
+ scalar "format" format_flag (Colitur_naming.Config.format config) "(none)";
+ (match overlays_flag with
+ | _ :: _ as l -> List.iter (fun o -> Printf.printf "%-10s %-24s (%s)\n" "overlay" o "flag") l
+ | [] -> (
+ match Colitur_naming.Config.overlays config with
+ | [] -> Printf.printf "%-10s %-24s (%s)\n" "overlay" "(none)" "default"
+ | l -> List.iter (fun o -> Printf.printf "%-10s %-24s (%s)\n" "overlay" o "config") l))
+
(* `colitur new-overlay` -- a starter file on stdout, for redirection.
Deliberately printed rather than written: the user picks the path, and a
command that creates files where it likes is a worse citizen. Every value is
@@ -1350,22 +1729,51 @@ let () =
| Error msg ->
Printf.eprintf "colitur: %s\n" msg;
usage ()
- | Ok { overlays; format; from_y; to_y; dtstamp; year; template; flavour; out; prune; positional } -> (
+ | Ok { overlays; format; from_y; to_y; dtstamp; year; template; flavour; out; prune; lang;
+ raw; dump; check; list; show; positional } -> (
let reject_emit = reject_emit_flags_for ~format ~from_y ~to_y ~dtstamp in
let reject_table = reject_table_flags_for ~year ~template ~flavour in
let reject_publish = reject_publish_flags_for ~out ~prune in
+ let reject_lang = reject_lang_for ~lang ~raw in
+ let reject_lang_sub = reject_lang_subcommand_flags_for ~dump ~check ~list ~show in
+ (* Loaded once, unconditionally: a config file the user wrote and
+ colitur cannot honour (missing HOME aside, [load_config] treats
+ that as "no config" rather than an error) is worth surfacing on
+ EVERY invocation, `--help` included, not only the commands that
+ happen to consult it -- the same "never silently ignored"
+ discipline [COLITUR_DATA_DIR] already gets. *)
+ let config = load_config () in
+ (* Resolved only inside the branches that actually consume it, as a
+ thunk rather than eagerly here: [load_lang] can [exit 2] (an
+ unknown language, a malformed file), and a command that never asked
+ for naming at all (`easter`, `check`, ...) must not be able to fail
+ on account of a language it never uses -- those commands reject
+ [--lang]/[--raw] outright instead, via [reject_lang] above. *)
+ let resolved_lang () = load_lang ~raw ~flag:lang ~config:(Colitur_naming.Config.lang config) in
+ (* Config supplies a DEFAULT overlay list only when NO --overlay was
+ given at all -- not merged with a partial CLI list -- so the
+ precedence stays exactly flag > config > default, the same
+ direction every other setting resolves in, rather than a list
+ merge whose ordering nothing documents. *)
+ let effective_overlays =
+ if overlays = [] then Colitur_naming.Config.overlays config else overlays
+ in
match positional with
| [ ("-h" | "--help" | "help") ] ->
reject_overlays_for "--help" overlays;
reject_emit "--help";
reject_table "--help";
reject_publish "--help";
+ reject_lang "--help";
+ reject_lang_sub "--help";
print_help ()
| [ ("-V" | "--version" | "version") ] ->
reject_overlays_for "--version" overlays;
reject_emit "--version";
reject_table "--version";
reject_publish "--version";
+ reject_lang "--version";
+ reject_lang_sub "--version";
print_endline version;
exit 0
| [ "easter"; ys ] ->
@@ -1373,46 +1781,110 @@ let () =
reject_emit "easter";
reject_table "easter";
reject_publish "easter";
+ reject_lang "easter";
+ reject_lang_sub "easter";
with_year ys easter_report
| [ "temporal"; ys ] ->
reject_overlays_for "temporal" overlays;
reject_emit "temporal";
reject_table "temporal";
reject_publish "temporal";
+ reject_lang "temporal";
+ reject_lang_sub "temporal";
with_year ys temporal_report
| "check" :: (_ :: _ as files) ->
reject_overlays_for "check" overlays;
reject_emit "check";
reject_table "check";
reject_publish "check";
+ reject_lang "check";
+ reject_lang_sub "check";
check_report files
| [ "convert"; path ] ->
reject_overlays_for "convert" overlays;
reject_emit "convert";
reject_table "convert";
reject_publish "convert";
+ reject_lang "convert";
+ reject_lang_sub "convert";
convert_report path
| [ "new-overlay" ] ->
reject_overlays_for "new-overlay" overlays;
reject_emit "new-overlay";
reject_table "new-overlay";
reject_publish "new-overlay";
+ reject_lang "new-overlay";
+ reject_lang_sub "new-overlay";
print_string new_overlay_template;
exit 0
+ | [ "lang" ] -> (
+ reject_overlays_for "lang" overlays;
+ reject_emit "lang";
+ reject_table "lang";
+ reject_publish "lang";
+ reject_lang "lang";
+ if show then begin
+ Printf.eprintf "colitur: --show has no effect on `lang`; refusing rather than ignoring it\n";
+ exit 2
+ end;
+ match (list, dump, check) with
+ | true, None, None -> lang_list ()
+ | false, Some code, None -> lang_dump code
+ | false, None, Some path -> lang_check path
+ | false, None, None ->
+ Printf.eprintf "colitur: lang requires one of --list, --dump CODE or --check FILE\n";
+ exit 2
+ | _ ->
+ Printf.eprintf "colitur: lang takes only one of --list, --dump CODE or --check FILE\n";
+ exit 2)
+ | [ "config" ] ->
+ (* Unlike every other subcommand's own rejector, `config --show`
+ deliberately ACCEPTS --lang/--template/--format/--overlay: they
+ are the very settings it previews the resolution of (so
+ `config --show --lang fr` reports exactly what a real `--lang
+ fr` run on any other command would resolve to), so none of
+ [reject_lang]/[reject_overlays_for]/the format half of
+ [reject_emit] apply here. Everything with no meaning for a
+ config preview is still refused, not silently ignored. *)
+ reject_table "config";
+ reject_publish "config";
+ if from_y <> None || to_y <> None || dtstamp <> None then begin
+ Printf.eprintf
+ "colitur: --from/--to/--dtstamp have no effect on `config`; refusing rather than ignoring them\n";
+ exit 2
+ end;
+ if raw then begin
+ Printf.eprintf "colitur: --raw has no effect on `config`; refusing rather than ignoring it\n";
+ exit 2
+ end;
+ if dump <> None || check <> None || list then begin
+ Printf.eprintf
+ "colitur: --dump/--check/--list have no effect on `config`; refusing rather than ignoring them\n";
+ exit 2
+ end;
+ if not show then begin
+ Printf.eprintf "colitur: config requires --show\n";
+ exit 2
+ end;
+ config_show ~lang_flag:lang ~template_flag:template ~format_flag:format ~overlays_flag:overlays
+ config
| [ "day"; ys ] ->
reject_emit "day";
reject_table "day";
reject_publish "day";
- with_year ys (day_report ~overlays)
+ reject_lang_sub "day";
+ with_year ys (day_report ~lang:(resolved_lang ()) ~overlays:effective_overlays)
| [ "readings"; ys ] ->
reject_emit "readings";
reject_table "readings";
reject_publish "readings";
- with_year ys (readings_report ~overlays)
+ reject_lang_sub "readings";
+ with_year ys (readings_report ~lang:(resolved_lang ()) ~overlays:effective_overlays)
| [ "emit" ] -> (
reject_table "emit";
reject_publish "emit";
- match format with
+ reject_lang_sub "emit";
+ match (match format with Some f -> Some f | None -> Colitur_naming.Config.format config) with
| None ->
Printf.eprintf "colitur: emit requires --format csv|json|sexp|xml|ics\n";
exit 2
@@ -1422,12 +1894,16 @@ let () =
Printf.eprintf "colitur: emit requires --from YEAR and --to YEAR\n";
exit 2
| Some from_ys, Some to_ys ->
+ let lang_t = resolved_lang () in
with_year from_ys (fun from_y ->
- with_year to_ys (fun to_y -> emit_report ~format ~overlays ~dtstamp ~from_y ~to_y))))
+ with_year to_ys (fun to_y ->
+ emit_report ~lang:lang_t ~format ~overlays:effective_overlays ~dtstamp ~from_y
+ ~to_y))))
| [ ("table" | "render") as cmd ] -> (
reject_emit cmd;
reject_publish cmd;
- match template with
+ reject_lang_sub cmd;
+ match (match template with Some t -> Some t | None -> Colitur_naming.Config.template config) with
| None ->
Printf.eprintf "colitur: %s requires --year YEAR and --template FILE\n" cmd;
exit 2
@@ -1436,11 +1912,15 @@ let () =
| None ->
Printf.eprintf "colitur: %s requires --year YEAR and --template FILE\n" cmd;
exit 2
- | Some ys -> with_year ys (fun y -> table_report ~template ~flavour_opt:flavour ~overlays y)
+ | Some ys ->
+ let lang_t = resolved_lang () in
+ with_year ys (fun y ->
+ table_report ~lang:lang_t ~template ~flavour_opt:flavour ~overlays:effective_overlays y)
))
| [ "publish" ] -> (
reject_table "publish";
reject_format_for "publish" format;
+ reject_lang_sub "publish";
match out with
| None ->
Printf.eprintf "colitur: publish requires --out DIR\n";
@@ -1451,6 +1931,7 @@ let () =
Printf.eprintf "colitur: publish requires --from YEAR and --to YEAR\n";
exit 2
| Some from_ys, Some to_ys ->
+ let lang_t = resolved_lang () in
with_year from_ys (fun from_y ->
with_year to_ys (fun to_y ->
(* [publish_report] writes many files across a whole
@@ -1468,7 +1949,10 @@ let () =
(stdlib, not the Unix module) wraps the same
underlying errno in [Sys_error] instead -- both
are real on this path, so both are caught. *)
- try publish_report ~from_y ~to_y ~out ~overlays ~dtstamp ~prune with
+ try
+ publish_report ~lang:lang_t ~from_y ~to_y ~out ~overlays:effective_overlays
+ ~dtstamp ~prune
+ with
| Unix.Unix_error (e, fn, arg) ->
Printf.eprintf "colitur: %s: %s: %s\n" fn arg (Unix.error_message e);
exit 2