aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/naming/lang.ml19
-rw-r--r--lib/naming/lang.mli18
-rw-r--r--test/test_lang.ml34
3 files changed, 66 insertions, 5 deletions
diff --git a/lib/naming/lang.ml b/lib/naming/lang.ml
index 0d902d5..147a49b 100644
--- a/lib/naming/lang.ml
+++ b/lib/naming/lang.ml
@@ -66,10 +66,23 @@ 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 =
- 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
+ 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
diff --git a/lib/naming/lang.mli b/lib/naming/lang.mli
index c4432f6..36931d4 100644
--- a/lib/naming/lang.mli
+++ b/lib/naming/lang.mli
@@ -13,7 +13,23 @@
type t
(** Parse INI text. Never raises. [Error] on a malformed file or a missing
- [\[meta\] lang]. *)
+ [\[meta\] lang].
+
+ Two duplicate policies, both LAST-WINS:
+ - A section name repeated in the file (e.g. two [\[celebration\]]
+ blocks) has ALL of its blocks merged, not only the first -- a
+ 595-entry hand-edited language file WILL grow duplicate section
+ headers as contributors append entries over time, and dropping a
+ later block would silently lose real translations.
+ - Where the same key appears more than once -- within one block or
+ across two of them -- the value from further down the file wins.
+
+ Both read the same order a reader would: later in the file overrides
+ earlier. This is the OPPOSITE direction from
+ {!Colitur_kernel.Overlay_ini.get} ([List.assoc_opt], first match) over
+ the very same [section.fields] shape -- the two modules resolve a
+ duplicate key in opposite directions, so do not assume one's behaviour
+ from the other's. *)
val of_string : string -> (t, string) result
val code : t -> string
diff --git a/test/test_lang.ml b/test/test_lang.ml
index 0b16b30..b54e85b 100644
--- a/test/test_lang.ml
+++ b/test/test_lang.ml
@@ -74,6 +74,33 @@ let test_missing_meta_lang_is_error () =
| Error _ -> ()
| Ok _ -> Alcotest.fail "a language file with no [meta] lang must be an Error"
+(* F1 regression: a hand-edited language file WILL grow duplicate [section]
+ headers as contributors append entries over time (Tasks 3/4's 595-entry
+ la.ini). Both blocks' keys must resolve -- silently dropping the second
+ block is a data-loss footgun that surfaces as a false "missing name"
+ report far from its real cause. *)
+let test_duplicate_sections_all_merge () =
+ let t =
+ ok
+ (L.of_string
+ "[meta]\nlang = la\n[celebration]\na = ALPHA\n[weekday]\nsunday = Dominica\n\
+ [celebration]\nb = BETA\n")
+ in
+ Alcotest.(check string) "first block's key" "ALPHA" (L.celebration t "a");
+ Alcotest.(check string) "second block's key" "BETA" (L.celebration t "b")
+
+let test_duplicate_key_across_sections_last_wins () =
+ let t =
+ ok
+ (L.of_string
+ "[meta]\nlang = la\n[celebration]\na = FIRST\n[celebration]\na = SECOND\n")
+ in
+ Alcotest.(check string) "later block's value wins" "SECOND" (L.celebration t "a")
+
+let test_duplicate_key_within_section_last_wins () =
+ let t = ok (L.of_string "[meta]\nlang = la\n[celebration]\na = FIRST\na = SECOND\n") in
+ Alcotest.(check string) "later line's value wins" "SECOND" (L.celebration t "a")
+
let suite =
( "Lang",
[ Alcotest.test_case "meta" `Quick test_meta;
@@ -82,4 +109,9 @@ let suite =
Alcotest.test_case "fallback chain" `Quick test_fallback_chain;
Alcotest.test_case "raw is identity" `Quick test_raw_is_identity;
Alcotest.test_case "malformed is error" `Quick test_malformed_is_error_not_crash;
- Alcotest.test_case "missing meta lang is error" `Quick test_missing_meta_lang_is_error ] )
+ Alcotest.test_case "missing meta lang is error" `Quick test_missing_meta_lang_is_error;
+ Alcotest.test_case "duplicate sections all merge" `Quick test_duplicate_sections_all_merge;
+ Alcotest.test_case "duplicate key across sections: last wins" `Quick
+ test_duplicate_key_across_sections_last_wins;
+ Alcotest.test_case "duplicate key within section: last wins" `Quick
+ test_duplicate_key_within_section_last_wins ] )