refactor Own all user-facing phrasing in Present

Present existed to be the one place wording lives, yet it rendered six messages by calling core and app pp functions. A copy change or a future translation would have reached the entity layer, which Clean Architecture says no UI change may do. Present now matches every error variant itself: Workout.error, Feedback.level and signal, Trainee.username_error, and Service.error. The variants are closed, so a new case is a compile error here rather than a silent fall-through. Exercise ids resolve to catalog names for display. The overview's duplicate duration and readiness wording moves into Present so the recovery status has one voice. The core pp functions are unchanged and remain debugging output.

Commit
9d98c1cd1d75832a7ba318ca526bd43e643dec90
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/web/handlers_kit.ml
index 7297267b..aca2b64d 100644..100644
@@ -49,26 +49,72 @@
49 49
50 50 Every domain and service error becomes a user-facing sentence here, so a
51 51 handler renders a message rather than deciding its wording. One module owns
52 Removed: the phrasing, and adding an error variant surfaces as a missing case. *)
52 Added: the phrasing, and adding an error variant surfaces as a missing case. The
53 Added: core's [pp] functions are debugging output and are not used for copy: a
54 Added: wording change must never reach the entity layer. *)
53 55 module Present = struct
54 56 let form_invalid = "The submitted form is not valid."
55 57
58 Added: (* Hours, with one decimal only when the duration is not whole. *)
59 Added: let duration_hours duration =
60 Added: let hours =
61 Added: float_of_int (Recovery.duration_to_seconds duration) /. 3600.
62 Added: in
63 Added: if Float.equal hours (Float.round hours) then Printf.sprintf "%.0fh" hours
64 Added: else Printf.sprintf "%.1fh" hours
65 Added:
66 Added: let recovery_status ~rested ~recommended =
67 Added: Printf.sprintf "Recovery: %s of %s." (duration_hours rested)
68 Added: (duration_hours recommended)
69 Added:
70 Added: (* The catalog name when the id still resolves, else the id itself. *)
71 Added: let exercise_name (id : Exercise.id) =
72 Added: match Exercise.find_id (id :> string) with
73 Added: | Some exercise -> Exercise.name exercise
74 Added: | None -> (id :> string)
75 Added:
76 Added: let username_error : Trainee.username_error -> string = function
77 Added: | Trainee.Too_short ->
78 Added: Printf.sprintf "A username needs at least %d characters."
79 Added: Trainee.username_min_length
80 Added: | Trainee.Too_long ->
81 Added: Printf.sprintf "A username can have at most %d characters."
82 Added: Trainee.username_max_length
83 Added:
56 84 let registration : Service.register_error -> string = function
57 Removed: | `Username e -> Format.asprintf "%a" Trainee.pp_username_error e
85 Added: | `Username e -> username_error e
58 86 | `Username_taken -> "That username is already registered."
59 87
60 88 let sign_in_failed = "That username and password do not match."
61 Removed: let routine = Format.asprintf "%a" Service.pp_error
62 89
90 Added: let routine : Service.error -> string = function
91 Added: | Service.Unknown_routine -> "That routine is not in the catalogue."
92 Added: | Service.Not_recovered Recovery.Ready ->
93 Added: "Recovery is complete; begin the workout."
94 Added: | Service.Not_recovered (Recovery.Recovering { rested; recommended }) ->
95 Added: "Recovery is not complete. " ^ recovery_status ~rested ~recommended
96 Added:
97 Added: let shape : Evidence.Workout.shape -> string = function
98 Added: | Evidence.Workout.As_single -> "a single set"
99 Added: | Evidence.Workout.As_pair -> "a pre-exhaust pair"
100 Added:
101 Added: let workout_error : Evidence.Workout.error -> string = function
102 Added: | Evidence.Workout.Not_prescribed id ->
103 Added: Printf.sprintf "%s is not prescribed for this workout."
104 Added: (exercise_name id)
105 Added: | Evidence.Workout.Delivery_mismatch { exercise; prescribed; logged } ->
106 Added: Printf.sprintf "%s is prescribed as %s but was logged as %s."
107 Added: (exercise_name exercise) (shape prescribed) (shape logged)
108 Added: | Evidence.Workout.No_such_slot slot ->
109 Added: Printf.sprintf "Slot %d is not part of this workout." slot
110 Added:
63 111 let log_error : Service.log_error -> string = function
64 112 | Service.No_workout_in_progress -> "No workout is in progress."
65 Removed: | Service.Rejected error ->
66 Removed: Format.asprintf "%a" Evidence.Workout.pp_error error
113 Added: | Service.Rejected error -> workout_error error
67 114
68 115 let edit_error : Service.edit_error -> string = function
69 116 | Service.Unknown_workout -> "That saved workout no longer exists."
70 Removed: | Service.Rejected_edit error ->
71 Removed: Format.asprintf "%a" Evidence.Workout.pp_error error
117 Added: | Service.Rejected_edit error -> workout_error error
72 118
73 119 let unknown_routine = "That routine is not in the catalogue."
74 120 let unknown_record = "That saved workout no longer exists."
@@ -76,13 +122,22 @@
76 122 let slot_not_awaiting = "That slot is not awaiting a record."
77 123
78 124 let change_username : Service.change_username_error -> string = function
79 Removed: | `Username e -> Format.asprintf "%a" Trainee.pp_username_error e
125 Added: | `Username e -> username_error e
80 126 | `Username_taken -> "That username is already registered."
81 127 | `Unknown -> "That account no longer exists."
82 128
83 129 let change_password : Service.change_password_error -> string = function
84 130 | `Incorrect_password -> "The current password is not correct."
85 131 | `Unknown -> "That account no longer exists."
132 Added:
133 Added: let feedback_level : Evidence.Feedback.level -> string = function
134 Added: | Evidence.Feedback.Worse_than_usual -> "worse than usual"
135 Added: | Evidence.Feedback.As_usual -> "as usual"
136 Added: | Evidence.Feedback.Better_than_usual -> "better than usual"
137 Added:
138 Added: let feedback_signal : Evidence.Feedback.signal -> string = function
139 Added: | Evidence.Feedback.Motivation l -> "Motivation " ^ feedback_level l
140 Added: | Evidence.Feedback.Difficulty l -> "Difficulty " ^ feedback_level l
86 141
87 142 let app_feedback_empty = "Enter feedback before submitting."
88 143 let username_changed = "Username changed."
lib/web/handlers_logbook.ml
index 518584f8..fbdb91a3 100644..100644
@@ -79,12 +79,7 @@
79 79 leveled "difficulty" (fun l -> Evidence.Feedback.Difficulty l);
80 80 ]
81 81
82 Removed: let describe_signal (signal : Evidence.Feedback.signal) =
83 Removed: let level l = Format.asprintf "%a" Evidence.Feedback.pp_level l in
84 Removed: match signal with
85 Removed: | Motivation l -> Printf.sprintf "Motivation %s" (level l)
86 Removed: | Difficulty l -> Printf.sprintf "Difficulty %s" (level l)
87 Removed:
82 Added: let describe_signal = Present.feedback_signal
88 83 let factor_fields = [ "motivation"; "difficulty" ]
89 84
90 85 let factor_score field (report : Evidence.Feedback.t) =
lib/web/handlers_overview.ml
index 01621e50..e98729d9 100644..100644
@@ -45,11 +45,6 @@
45 45 (Prescription.Routine.workouts r);
46 46 }
47 47
48 Removed: let duration_hours duration =
49 Removed: let hours = float_of_int (Recovery.duration_to_seconds duration) /. 3600. in
50 Removed: if Float.equal hours (Float.round hours) then Printf.sprintf "%.0fh" hours
51 Removed: else Printf.sprintf "%.1fh" hours
52 Removed:
53 48 let home_view ~(routine : Repository.routine_id) ~routine_name ~next
54 49 ~readiness : View_model.Home.t =
55 50 {
@@ -61,11 +56,7 @@
61 56 | Recovery.Ready -> View_model.Home.Ready
62 57 | Recovery.Recovering { rested; recommended } ->
63 58 View_model.Home.Recovering
64 Removed: {
65 Removed: status =
66 Removed: Printf.sprintf "Recovery: %s of %s." (duration_hours rested)
67 Removed: (duration_hours recommended);
68 Removed: });
59 Added: { status = Present.recovery_status ~rested ~recommended });
69 60 }
70 61
71 62 let page t trainee request =