Skip to main content

finance_query/translation/
translatable.rs

1//! The [`Translatable`] trait and implementations for response models.
2//!
3//! Only human-readable natural-language fields are visited. Symbols,
4//! exchange codes, currency codes, keys, URLs, addresses, and numeric or
5//! date fields are never translated.
6
7use crate::models::chart::spark::Spark;
8use crate::models::chart::{CapitalGain, Chart, Dividend, Split};
9use crate::models::corporate::news::News;
10use crate::models::corporate::recommendation::Recommendation;
11use crate::models::corporate::transcript::{Transcript, TranscriptWithMeta};
12use crate::models::discovery::lookup::{LookupQuote, LookupResults};
13use crate::models::discovery::search::{
14    SearchNews, SearchNewsList, SearchQuote, SearchQuotes, SearchResults,
15};
16use crate::models::format::Format;
17use crate::models::fundamentals::FinancialStatement;
18use crate::models::market::industries::IndustryData;
19use crate::models::market::market_summary::MarketSummaryQuote;
20use crate::models::market::sectors::SectorData;
21use crate::models::options::Options;
22use crate::models::quote::{
23    AssetProfile, CalendarEvents, CompanyOfficer, DefaultKeyStatistics, Earnings, EarningsHistory,
24    EarningsTrend, EquityPerformance, FinancialData, FundOwnership, FundPerformance, FundProfile,
25    IndexTrend, IndustryTrend, InsiderHolders, InsiderTransactions, InstitutionOwnership,
26    MajorHoldersBreakdown, NetSharePurchaseActivity, Price, Quote, QuoteTypeData,
27    RecommendationTrend, SecFilings, SectorTrend, SummaryDetail, SummaryProfile, TopHoldings,
28    UpgradeDowngradeHistory,
29};
30
31/// A response type whose human-readable text fields can be translated.
32///
33/// The default implementation visits nothing, so types without
34/// natural-language content implement this trait as a no-op marker.
35///
36/// `visit_translatable` must visit the same fields in the same order on
37/// every call for an unmodified value: the translation pipeline performs one
38/// pass to collect texts and a second pass to write translations back.
39pub trait Translatable {
40    /// Visit every translatable string field in a stable order.
41    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
42        let _ = visit;
43    }
44
45    /// Hook invoked after translations have been written back, for types
46    /// that maintain derived text (e.g. a transcript's joined full text).
47    fn after_translate(&mut self) {}
48}
49
50fn visit_opt(field: &mut Option<String>, visit: &mut dyn FnMut(&mut String)) {
51    if let Some(value) = field {
52        visit(value);
53    }
54}
55
56impl<T: Translatable> Translatable for Option<T> {
57    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
58        if let Some(value) = self {
59            value.visit_translatable(visit);
60        }
61    }
62    fn after_translate(&mut self) {
63        if let Some(value) = self {
64            value.after_translate();
65        }
66    }
67}
68
69impl<T: Translatable> Translatable for Vec<T> {
70    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
71        for value in self {
72            value.visit_translatable(visit);
73        }
74    }
75    fn after_translate(&mut self) {
76        for value in self {
77            value.after_translate();
78        }
79    }
80}
81
82impl<F: Format> Translatable for Quote<F> {
83    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
84        visit_opt(&mut self.short_name, visit);
85        visit_opt(&mut self.long_name, visit);
86        visit_opt(&mut self.sector_disp, visit);
87        visit_opt(&mut self.industry_disp, visit);
88        visit_opt(&mut self.long_business_summary, visit);
89    }
90}
91
92impl<F: Format> Translatable for Price<F> {
93    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
94        visit_opt(&mut self.short_name, visit);
95        visit_opt(&mut self.long_name, visit);
96    }
97}
98
99impl Translatable for CompanyOfficer {
100    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
101        visit_opt(&mut self.title, visit);
102    }
103}
104
105impl Translatable for AssetProfile {
106    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
107        visit_opt(&mut self.sector_disp, visit);
108        visit_opt(&mut self.industry_disp, visit);
109        visit_opt(&mut self.long_business_summary, visit);
110        self.company_officers.visit_translatable(visit);
111    }
112}
113
114impl Translatable for SummaryProfile {
115    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
116        visit_opt(&mut self.sector_disp, visit);
117        visit_opt(&mut self.industry_disp, visit);
118        visit_opt(&mut self.long_business_summary, visit);
119    }
120}
121
122impl Translatable for QuoteTypeData {
123    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
124        visit_opt(&mut self.short_name, visit);
125        visit_opt(&mut self.long_name, visit);
126    }
127}
128
129impl Translatable for News {
130    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
131        visit(&mut self.title);
132    }
133}
134
135impl Translatable for SearchNews {
136    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
137        visit_opt(&mut self.title, visit);
138    }
139}
140
141impl Translatable for SearchQuote {
142    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
143        visit_opt(&mut self.short_name, visit);
144        visit_opt(&mut self.long_name, visit);
145        visit_opt(&mut self.type_disp, visit);
146        visit_opt(&mut self.exch_disp, visit);
147        visit_opt(&mut self.sector_disp, visit);
148        visit_opt(&mut self.industry_disp, visit);
149    }
150}
151
152impl Translatable for SearchQuotes {
153    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
154        self.0.visit_translatable(visit);
155    }
156}
157
158impl Translatable for SearchNewsList {
159    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
160        self.0.visit_translatable(visit);
161    }
162}
163
164impl Translatable for SearchResults {
165    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
166        self.quotes.visit_translatable(visit);
167        self.news.visit_translatable(visit);
168    }
169}
170
171impl Translatable for LookupQuote {
172    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
173        visit_opt(&mut self.short_name, visit);
174        visit_opt(&mut self.long_name, visit);
175        visit_opt(&mut self.type_disp, visit);
176        visit_opt(&mut self.exch_disp, visit);
177        visit_opt(&mut self.sector, visit);
178        visit_opt(&mut self.industry, visit);
179    }
180}
181
182impl Translatable for LookupResults {
183    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
184        self.quotes.visit_translatable(visit);
185    }
186}
187
188impl Translatable for MarketSummaryQuote {
189    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
190        visit_opt(&mut self.short_name, visit);
191        visit_opt(&mut self.full_exchange_name, visit);
192    }
193}
194
195impl Translatable for SectorData {
196    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
197        visit(&mut self.name);
198        if let Some(overview) = &mut self.overview {
199            visit_opt(&mut overview.description, visit);
200        }
201        for industry in &mut self.industries {
202            visit(&mut industry.name);
203        }
204    }
205}
206
207impl Translatable for IndustryData {
208    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
209        visit(&mut self.name);
210        visit_opt(&mut self.sector_name, visit);
211        if let Some(overview) = &mut self.overview {
212            visit_opt(&mut overview.description, visit);
213        }
214    }
215}
216
217impl Translatable for Transcript {
218    /// Visits paragraph texts (not the joined `text`, to avoid translating the
219    /// same content twice) plus speaker roles and the event title;
220    /// [`after_translate`](Translatable::after_translate) rebuilds the joined
221    /// text from the translated paragraphs. Per-sentence/word timing data is
222    /// left untranslated. When no paragraphs are present, the full text is
223    /// translated directly.
224    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
225        visit(&mut self.transcript_metadata.title);
226        for mapping in &mut self.transcript_content.speaker_mapping {
227            visit_opt(&mut mapping.speaker_data.role, visit);
228        }
229        if let Some(data) = &mut self.transcript_content.transcript {
230            if data.paragraphs.is_empty() {
231                visit(&mut data.text);
232            } else {
233                for paragraph in &mut data.paragraphs {
234                    visit(&mut paragraph.text);
235                }
236            }
237        }
238    }
239
240    fn after_translate(&mut self) {
241        if let Some(data) = &mut self.transcript_content.transcript
242            && !data.paragraphs.is_empty()
243        {
244            data.text = data
245                .paragraphs
246                .iter()
247                .map(|p| p.text.as_str())
248                .collect::<Vec<_>>()
249                .join("\n");
250        }
251    }
252}
253
254impl Translatable for TranscriptWithMeta {
255    fn visit_translatable(&mut self, visit: &mut dyn FnMut(&mut String)) {
256        visit(&mut self.title);
257        self.transcript.visit_translatable(visit);
258    }
259
260    fn after_translate(&mut self) {
261        self.transcript.after_translate();
262    }
263}
264
265/// Marker implementations for response types without natural-language
266/// content, so the `Ticker`/`Tickers` accessors can translate uniformly.
267macro_rules! not_translatable {
268    ($($ty:ty),* $(,)?) => {
269        $(impl Translatable for $ty {})*
270    };
271}
272
273not_translatable!(
274    SummaryDetail,
275    FinancialData,
276    DefaultKeyStatistics,
277    CalendarEvents,
278    Earnings,
279    EarningsTrend,
280    EarningsHistory,
281    RecommendationTrend,
282    InsiderHolders,
283    InsiderTransactions,
284    InstitutionOwnership,
285    FundOwnership,
286    MajorHoldersBreakdown,
287    NetSharePurchaseActivity,
288    SecFilings,
289    UpgradeDowngradeHistory,
290    FundPerformance,
291    FundProfile,
292    TopHoldings,
293    IndexTrend,
294    IndustryTrend,
295    SectorTrend,
296    EquityPerformance,
297    Chart,
298    Spark,
299    Dividend,
300    Split,
301    CapitalGain,
302    FinancialStatement,
303    Recommendation,
304    Options,
305);
306
307#[cfg(feature = "indicators")]
308not_translatable!(crate::indicators::IndicatorsSummary);