serde_saphyr/localizer.rs
1//! Localization / wording customization.
2//!
3//! The [`Localizer`] trait is the central hook for customizing *crate-authored wording*.
4//! It is intentionally designed to be low-boilerplate:
5//!
6//! - Every method has a reasonable English default.
7//! - You can override only the pieces you care about, while inheriting all other defaults.
8//!
9//! This crate may also show *external* message text coming from dependencies (for example
10//! `saphyr-parser` scan errors, or validator messages). Where such texts are used, the
11//! rendering pipeline should provide a best-effort opportunity to override them via
12//! [`Localizer::override_external_message`].
13//!
14//! ## Example: override a single phrase
15//!
16//! ```rust
17//! use serde_saphyr::{Error, Location};
18//! use serde_saphyr::localizer::{Localizer, DEFAULT_ENGLISH_LOCALIZER};
19//! use std::borrow::Cow;
20//!
21//! /// A wrapper that overrides only location suffix wording, delegating everything else.
22//! struct Pirate<'a> {
23//! base: &'a dyn Localizer,
24//! }
25//!
26//! impl Localizer for Pirate<'_> {
27//! fn attach_location<'b>(&self, base: Cow<'b, str>, loc: Location) -> Cow<'b, str> {
28//! if loc == Location::UNKNOWN {
29//! return base;
30//! }
31//! // Note: you can also delegate to `self.base.attach_location(...)` if you want.
32//! Cow::Owned(format!(
33//! "{base}. Bug lurks on line {}, then {} runes in",
34//! loc.line(),
35//! loc.column()
36//! ))
37//! }
38//! }
39//!
40//! // This snippet shows the customization building blocks; the crate's rendering APIs
41//! // obtain a `Localizer` via the `MessageFormatter`.
42//! # let _ = (Error::InvalidUtf8Input, &DEFAULT_ENGLISH_LOCALIZER);
43//! ```
44
45use crate::Location;
46use std::borrow::Cow;
47
48/// Where an “external” message comes from.
49///
50/// External messages are those primarily produced by dependencies (parser / validators).
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum ExternalMessageSource {
53 /// Text produced by `saphyr-parser` (e.g. scanning errors).
54 SaphyrParser,
55 /// Text produced by `garde` validation rules.
56 Garde,
57 /// Text produced by `validator` validation rules.
58 Validator,
59}
60
61/// A best-effort description of an external message.
62///
63/// The crate should pass as much stable metadata as it has (e.g. `code` and `params` for
64/// `validator`) so the localizer can override *specific* messages without string matching.
65#[derive(Debug, Clone)]
66pub struct ExternalMessage<'a> {
67 pub source: ExternalMessageSource,
68 /// The original text as provided by the external library.
69 pub original: &'a str,
70 /// Stable-ish identifier when available (e.g. validator error code).
71 pub code: Option<&'a str>,
72 /// Optional structured parameters when available.
73 pub params: &'a [(String, String)],
74}
75
76/// All crate-authored wording customization points.
77///
78/// Implementors should typically override *only a few* methods.
79/// Everything else should default to English (via the default method bodies).
80pub trait Localizer {
81 // ---------------- Common tiny building blocks ----------------
82
83 /// Attach a location suffix to `base`.
84 ///
85 /// Renderers must use this instead of hard-coding English wording like
86 /// `" at line X, column Y"`.
87 ///
88 /// Default:
89 /// - If `loc == Location::UNKNOWN`: returns `base` unchanged.
90 /// - Otherwise: returns `"{base} at line {line}, column {column}"`.
91 fn attach_location<'a>(&self, base: Cow<'a, str>, loc: Location) -> Cow<'a, str> {
92 if loc == Location::UNKNOWN {
93 base
94 } else {
95 Cow::Owned(format!("{base} at line {}, column {}", loc.line, loc.column))
96 }
97 }
98
99 /// Label used when a path has no leaf.
100 ///
101 /// Default <root>
102 fn root_path_label(&self) -> Cow<'static, str> {
103 Cow::Borrowed("<root>")
104 }
105
106 /// Suffix for alias-related errors when a distinct defined-location is available.
107 ///
108 /// Default wording matches the crate's historical English output:
109 /// `" (defined at line X, column Y)"`.
110 ///
111 /// Default: `format!(" (defined at line {line}, column {column})", ...)`.
112 fn alias_defined_at(&self, defined: Location) -> String {
113 format!(" (defined at line {}, column {})", defined.line, defined.column)
114 }
115
116 // ---------------- Validation (plain text) glue ----------------
117
118 /// Render one validation issue line.
119 ///
120 /// The crate provides `resolved_path`, `entry` and the chosen `loc`.
121 ///
122 /// Default:
123 /// - Base text: `"validation error at {resolved_path}: {entry}"`.
124 /// - If `loc` is `Some` and not `Location::UNKNOWN`, appends a location suffix via
125 /// [`Localizer::attach_location`].
126 fn validation_issue_line(
127 &self,
128 resolved_path: &str,
129 entry: &str,
130 loc: Option<Location>,
131 ) -> String {
132 let base = format!("validation error at {resolved_path}: {entry}");
133 match loc {
134 Some(l) if l != Location::UNKNOWN => self.attach_location(Cow::Owned(base), l).into_owned(),
135 _ => base,
136 }
137 }
138
139 /// Join multiple validation issues into one message.
140 ///
141 /// Default: joins `lines` with a single newline (`"\n"`).
142 fn join_validation_issues(&self, lines: &[String]) -> String {
143 lines.join("\n")
144 }
145
146 // ---------------- Validation snippets / diagnostic labels ----------------
147
148 /// Label used for a snippet window when the location is known and considered the
149 /// “definition” site.
150 ///
151 /// Default: `"(defined)"`.
152 fn defined(&self) -> Cow<'static, str> {
153 Cow::Borrowed("(defined)")
154 }
155
156 /// Label used for a snippet window when we only have a “defined here” location.
157 ///
158 /// Default: `"(defined here)"`.
159 fn defined_here(&self) -> Cow<'static, str> {
160 Cow::Borrowed("(defined here)")
161 }
162
163 /// Label used for the primary snippet window when an aliased/anchored value is used
164 /// at a different location than where it was defined.
165 ///
166 /// Default: `"the value is used here"`.
167 fn value_used_here(&self) -> Cow<'static, str> {
168 Cow::Borrowed("the value is used here")
169 }
170
171 /// Label used for the secondary snippet window that points at the anchor definition.
172 ///
173 /// Default: `"defined here"`.
174 fn defined_window(&self) -> Cow<'static, str> {
175 Cow::Borrowed("defined here")
176 }
177
178 /// Compose the base validation message used in snippet rendering.
179 ///
180 /// Default: `"validation error: {entry} for `{resolved_path}`"`.
181 fn validation_base_message(&self, entry: &str, resolved_path: &str) -> String {
182 format!("validation error: {entry} for `{resolved_path}`")
183 }
184
185 /// Compose the “invalid here” prefix for the primary snippet message.
186 ///
187 /// Default: `"invalid here, {base}"`.
188 fn invalid_here(&self, base: &str) -> String {
189 format!("invalid here, {base}")
190 }
191
192 /// Intro line printed between the primary and secondary snippet windows for
193 /// anchor/alias (“indirect value”) cases.
194 ///
195 /// Default:
196 /// `" | This value comes indirectly from the anchor at line {line} column {column}:"`.
197 fn value_comes_from_the_anchor(&self, def: Location) -> String {
198 format!(
199 " | This value comes indirectly from the anchor at line {} column {}:",
200 def.line, def.column
201 )
202 }
203
204 // ---------------- External overrides ----------------
205
206 /// Optional hook to override the location prefix used for snippet titles
207 ///
208 /// Default:
209 /// - If `loc == Location::UNKNOWN`: returns an empty string.
210 /// - Otherwise: returns `"line {line} column {column}"`.
211 fn snippet_location_prefix(&self, loc: Location) -> String {
212 if loc == Location::UNKNOWN {
213 String::new()
214 } else {
215 format!("line {} column {}", loc.line(), loc.column())
216 }
217 }
218
219 /// Best-effort hook to override/translate dependency-provided message text.
220 ///
221 /// Default: returns `None` (keep the external message as-is).
222 fn override_external_message<'a>(&self, _msg: ExternalMessage<'a>) -> Option<Cow<'a, str>> {
223 None
224 }
225}
226
227/// Default English localizer used by the crate.
228#[derive(Debug, Default, Clone, Copy)]
229pub struct DefaultEnglishLocalizer;
230
231impl Localizer for DefaultEnglishLocalizer {}
232
233/// A single shared instance of the default English localizer.
234///
235/// This avoids repeated instantiation and provides a convenient reference for wrappers.
236pub static DEFAULT_ENGLISH_LOCALIZER: DefaultEnglishLocalizer = DefaultEnglishLocalizer;