Skip to main content

lingora_core/renderers/
analysis_renderer.rs

1use std::{collections::BTreeMap, io};
2
3use crate::{
4    audit::{AuditIssue, AuditResult, Workspace},
5    domain::Locale,
6    error::LingoraError,
7};
8
9/// A hierarchical text renderer for `AuditResult`.
10///
11/// Produces structured console output showing:
12/// - Workspace-level issues (parse errors, ambiguous roots, etc.)
13/// - Canonical locale status
14/// - Each primary language (grouped under its language root)
15///   - The primary/base document
16///   - Any regional/script variants of that language
17/// - Orphaned locales (no matching base root)
18///
19/// Issues for each locale are sorted by `Kind` for consistent readability.
20pub struct AnalysisRenderer {
21    workspace: Workspace,
22    issues: BTreeMap<Option<Locale>, Vec<AuditIssue>>,
23}
24
25impl AnalysisRenderer {
26    /// Creates a new renderer from an `AuditResult`.
27    ///
28    /// Groups all issues by their associated locale (extracted via `AuditIssue::locale()`).
29    /// Global/workspace issues (no locale) are stored under `None`.
30    pub fn new(audit_result: &AuditResult) -> Self {
31        let workspace = audit_result.workspace().clone();
32
33        let issues = audit_result.issues().fold(BTreeMap::new(), |mut acc, i| {
34            let locale = i.locale();
35            acc.entry(locale).or_insert_with(Vec::new).push(i.clone());
36            acc
37        });
38
39        Self { workspace, issues }
40    }
41
42    /// Renders the full audit report to the given writer.
43    ///
44    /// Order of output:
45    /// 1. Workspace-level issues (if any)
46    /// 2. Canonical locale
47    /// 3. Each primary language group:
48    ///    - Primary/base locale
49    ///    - All variants (sorted)
50    /// 4. Orphaned locales (sorted)
51    pub fn render<W: io::Write>(&self, out: &mut W) -> Result<(), LingoraError> {
52        self.render_workspace(out)?;
53        self.render_language(out, "Canonical:", self.workspace.canonical_locale())?;
54        self.workspace
55            .primary_locales()
56            .try_for_each(|primary| self.render_language(out, "Primary:", primary))?;
57        self.workspace
58            .orphan_locales()
59            .try_for_each(|locale| self.render_locale(out, "Orphaned:", locale))
60    }
61
62    fn render_workspace<W: io::Write>(&self, out: &mut W) -> Result<(), LingoraError> {
63        if let Some(issues) = self.issues.get(&None) {
64            let issues = issues.iter().fold(BTreeMap::new(), |mut acc, issue| {
65                acc.entry(issue.subject())
66                    .or_insert_with(Vec::new)
67                    .push(issue);
68                acc
69            });
70
71            issues.iter().try_for_each(|(subject, issues)| {
72                writeln!(out, "Workspace: {subject}")?;
73
74                issues
75                    .iter()
76                    .try_for_each(|issue| writeln!(out, "{:10} {}", "", issue.message()))
77            })?;
78        }
79
80        Ok(())
81    }
82
83    fn render_language<W: io::Write>(
84        &self,
85        out: &mut W,
86        title: &str,
87        base: &Locale,
88    ) -> Result<(), LingoraError> {
89        writeln!(out, "Language:  {}", base.language())?;
90
91        self.render_locale(out, title, base)?;
92
93        let mut variants_locales = Vec::from_iter(self.workspace.variant_locales(base));
94
95        if !variants_locales.is_empty() {
96            variants_locales.sort();
97            variants_locales
98                .iter()
99                .try_for_each(|v| self.render_locale(out, "Variant:", v))?
100        }
101
102        Ok(())
103    }
104
105    fn render_locale<W: io::Write>(
106        &self,
107        out: &mut W,
108        title: &str,
109        locale: &Locale,
110    ) -> Result<(), LingoraError> {
111        if let Some(issues) = self.issues.get(&Some(locale.clone())) {
112            writeln!(out, "{:10} {}", title, locale,)?;
113
114            let mut issues = issues.clone();
115            issues.sort_by(|a, b| match a.kind().cmp(b.kind()) {
116                std::cmp::Ordering::Less => std::cmp::Ordering::Less,
117                std::cmp::Ordering::Equal => a.message().cmp(b.message()),
118                std::cmp::Ordering::Greater => std::cmp::Ordering::Greater,
119            });
120
121            issues
122                .iter()
123                .try_for_each(|issue| writeln!(out, "{:11}{issue}", ""))?;
124        } else {
125            writeln!(out, "{:10} {} - Ok", title, locale)?;
126        }
127
128        Ok(())
129    }
130}