1#![allow(internal_features)]
3#![feature(rustc_attrs)]
4use std::borrow::Cow;
7use std::error::Error;
8use std::path::Path;
9use std::sync::{Arc, LazyLock};
10use std::{fmt, fs, io};
11
12use fluent_bundle::FluentResource;
13pub use fluent_bundle::types::FluentType;
14pub use fluent_bundle::{self, FluentArgs, FluentError, FluentValue};
15use fluent_syntax::parser::ParserError;
16use intl_memoizer::concurrent::IntlLangMemoizer;
17use rustc_data_structures::sync::{DynSend, IntoDynSyncSend};
18use rustc_macros::{Decodable, Encodable};
19use rustc_span::Span;
20use tracing::{instrument, trace};
21pub use unic_langid::{LanguageIdentifier, langid};
22
23mod diagnostic_impls;
24pub use diagnostic_impls::DiagArgFromDisplay;
25
26pub type FluentBundle =
27 IntoDynSyncSend<fluent_bundle::bundle::FluentBundle<FluentResource, IntlLangMemoizer>>;
28
29fn new_bundle(locales: Vec<LanguageIdentifier>) -> FluentBundle {
30 IntoDynSyncSend(fluent_bundle::bundle::FluentBundle::new_concurrent(locales))
31}
32
33#[derive(Debug)]
34pub enum TranslationBundleError {
35 ReadFtl(io::Error),
37 ParseFtl(ParserError),
39 AddResource(FluentError),
41 MissingLocale,
43 ReadLocalesDir(io::Error),
45 ReadLocalesDirEntry(io::Error),
47 LocaleIsNotDir,
49}
50
51impl fmt::Display for TranslationBundleError {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 match self {
54 TranslationBundleError::ReadFtl(e) => write!(f, "could not read ftl file: {e}"),
55 TranslationBundleError::ParseFtl(e) => {
56 write!(f, "could not parse ftl file: {e}")
57 }
58 TranslationBundleError::AddResource(e) => write!(f, "failed to add resource: {e}"),
59 TranslationBundleError::MissingLocale => write!(f, "missing locale directory"),
60 TranslationBundleError::ReadLocalesDir(e) => {
61 write!(f, "could not read locales dir: {e}")
62 }
63 TranslationBundleError::ReadLocalesDirEntry(e) => {
64 write!(f, "could not read locales dir entry: {e}")
65 }
66 TranslationBundleError::LocaleIsNotDir => {
67 write!(f, "`$sysroot/share/locales/$locale` is not a directory")
68 }
69 }
70 }
71}
72
73impl Error for TranslationBundleError {
74 fn source(&self) -> Option<&(dyn Error + 'static)> {
75 match self {
76 TranslationBundleError::ReadFtl(e) => Some(e),
77 TranslationBundleError::ParseFtl(e) => Some(e),
78 TranslationBundleError::AddResource(e) => Some(e),
79 TranslationBundleError::MissingLocale => None,
80 TranslationBundleError::ReadLocalesDir(e) => Some(e),
81 TranslationBundleError::ReadLocalesDirEntry(e) => Some(e),
82 TranslationBundleError::LocaleIsNotDir => None,
83 }
84 }
85}
86
87impl From<(FluentResource, Vec<ParserError>)> for TranslationBundleError {
88 fn from((_, mut errs): (FluentResource, Vec<ParserError>)) -> Self {
89 TranslationBundleError::ParseFtl(errs.pop().expect("failed ftl parse with no errors"))
90 }
91}
92
93impl From<Vec<FluentError>> for TranslationBundleError {
94 fn from(mut errs: Vec<FluentError>) -> Self {
95 TranslationBundleError::AddResource(
96 errs.pop().expect("failed adding resource to bundle with no errors"),
97 )
98 }
99}
100
101#[instrument(level = "trace")]
107pub fn fluent_bundle(
108 sysroot_candidates: &[&Path],
109 requested_locale: Option<LanguageIdentifier>,
110 additional_ftl_path: Option<&Path>,
111 with_directionality_markers: bool,
112) -> Result<Option<Arc<FluentBundle>>, TranslationBundleError> {
113 if requested_locale.is_none() && additional_ftl_path.is_none() {
114 return Ok(None);
115 }
116
117 let fallback_locale = langid!("en-US");
118 let requested_fallback_locale = requested_locale.as_ref() == Some(&fallback_locale);
119 trace!(?requested_fallback_locale);
120 if requested_fallback_locale && additional_ftl_path.is_none() {
121 return Ok(None);
122 }
123 let locale = requested_locale.clone().unwrap_or(fallback_locale);
126 trace!(?locale);
127 let mut bundle = new_bundle(vec![locale]);
128
129 register_functions(&mut bundle);
131
132 bundle.set_use_isolating(with_directionality_markers);
138
139 if let Some(requested_locale) = requested_locale {
141 let mut found_resources = false;
142 for sysroot in sysroot_candidates {
143 let mut sysroot = sysroot.to_path_buf();
144 sysroot.push("share");
145 sysroot.push("locale");
146 sysroot.push(requested_locale.to_string());
147 trace!(?sysroot);
148
149 if !sysroot.exists() {
150 trace!("skipping");
151 continue;
152 }
153
154 if !sysroot.is_dir() {
155 return Err(TranslationBundleError::LocaleIsNotDir);
156 }
157
158 for entry in sysroot.read_dir().map_err(TranslationBundleError::ReadLocalesDir)? {
159 let entry = entry.map_err(TranslationBundleError::ReadLocalesDirEntry)?;
160 let path = entry.path();
161 trace!(?path);
162 if path.extension().and_then(|s| s.to_str()) != Some("ftl") {
163 trace!("skipping");
164 continue;
165 }
166
167 let resource_str =
168 fs::read_to_string(path).map_err(TranslationBundleError::ReadFtl)?;
169 let resource =
170 FluentResource::try_new(resource_str).map_err(TranslationBundleError::from)?;
171 trace!(?resource);
172 bundle.add_resource(resource).map_err(TranslationBundleError::from)?;
173 found_resources = true;
174 }
175 }
176
177 if !found_resources {
178 return Err(TranslationBundleError::MissingLocale);
179 }
180 }
181
182 if let Some(additional_ftl_path) = additional_ftl_path {
183 let resource_str =
184 fs::read_to_string(additional_ftl_path).map_err(TranslationBundleError::ReadFtl)?;
185 let resource =
186 FluentResource::try_new(resource_str).map_err(TranslationBundleError::from)?;
187 trace!(?resource);
188 bundle.add_resource_overriding(resource);
189 }
190
191 let bundle = Arc::new(bundle);
192 Ok(Some(bundle))
193}
194
195fn register_functions(bundle: &mut FluentBundle) {
196 bundle
197 .add_function("STREQ", |positional, _named| match positional {
198 [FluentValue::String(a), FluentValue::String(b)] => format!("{}", (a == b)).into(),
199 _ => FluentValue::Error,
200 })
201 .expect("Failed to add a function to the bundle.");
202}
203
204pub type LazyFallbackBundle =
207 Arc<LazyLock<FluentBundle, Box<dyn FnOnce() -> FluentBundle + DynSend>>>;
208
209#[instrument(level = "trace", skip(resources))]
211pub fn fallback_fluent_bundle(
212 resources: Vec<&'static str>,
213 with_directionality_markers: bool,
214) -> LazyFallbackBundle {
215 Arc::new(LazyLock::new(Box::new(move || {
216 let mut fallback_bundle = new_bundle(vec![langid!("en-US")]);
217
218 register_functions(&mut fallback_bundle);
219
220 fallback_bundle.set_use_isolating(with_directionality_markers);
222
223 for resource in resources {
224 let resource = FluentResource::try_new(resource.to_string())
225 .expect("failed to parse fallback fluent resource");
226 fallback_bundle.add_resource_overriding(resource);
227 }
228
229 fallback_bundle
230 })))
231}
232
233type FluentId = Cow<'static, str>;
235
236#[rustc_diagnostic_item = "SubdiagMessage"]
244pub enum SubdiagMessage {
245 Str(Cow<'static, str>),
247 Translated(Cow<'static, str>),
254 FluentIdentifier(FluentId),
257 FluentAttr(FluentId),
263}
264
265impl From<String> for SubdiagMessage {
266 fn from(s: String) -> Self {
267 SubdiagMessage::Str(Cow::Owned(s))
268 }
269}
270impl From<&'static str> for SubdiagMessage {
271 fn from(s: &'static str) -> Self {
272 SubdiagMessage::Str(Cow::Borrowed(s))
273 }
274}
275impl From<Cow<'static, str>> for SubdiagMessage {
276 fn from(s: Cow<'static, str>) -> Self {
277 SubdiagMessage::Str(s)
278 }
279}
280
281#[derive(Clone, Debug, PartialEq, Eq, Hash, Encodable, Decodable)]
286#[rustc_diagnostic_item = "DiagMessage"]
287pub enum DiagMessage {
288 Str(Cow<'static, str>),
290 Translated(Cow<'static, str>),
297 FluentIdentifier(FluentId, Option<FluentId>),
303}
304
305impl DiagMessage {
306 pub fn with_subdiagnostic_message(&self, sub: SubdiagMessage) -> Self {
312 let attr = match sub {
313 SubdiagMessage::Str(s) => return DiagMessage::Str(s),
314 SubdiagMessage::Translated(s) => return DiagMessage::Translated(s),
315 SubdiagMessage::FluentIdentifier(id) => {
316 return DiagMessage::FluentIdentifier(id, None);
317 }
318 SubdiagMessage::FluentAttr(attr) => attr,
319 };
320
321 match self {
322 DiagMessage::Str(s) => DiagMessage::Str(s.clone()),
323 DiagMessage::Translated(s) => DiagMessage::Translated(s.clone()),
324 DiagMessage::FluentIdentifier(id, _) => {
325 DiagMessage::FluentIdentifier(id.clone(), Some(attr))
326 }
327 }
328 }
329
330 pub fn as_str(&self) -> Option<&str> {
331 match self {
332 DiagMessage::Translated(s) | DiagMessage::Str(s) => Some(s),
333 DiagMessage::FluentIdentifier(_, _) => None,
334 }
335 }
336}
337
338impl From<String> for DiagMessage {
339 fn from(s: String) -> Self {
340 DiagMessage::Str(Cow::Owned(s))
341 }
342}
343impl From<&'static str> for DiagMessage {
344 fn from(s: &'static str) -> Self {
345 DiagMessage::Str(Cow::Borrowed(s))
346 }
347}
348impl From<Cow<'static, str>> for DiagMessage {
349 fn from(s: Cow<'static, str>) -> Self {
350 DiagMessage::Str(s)
351 }
352}
353
354impl From<DiagMessage> for SubdiagMessage {
360 fn from(val: DiagMessage) -> Self {
361 match val {
362 DiagMessage::Str(s) => SubdiagMessage::Str(s),
363 DiagMessage::Translated(s) => SubdiagMessage::Translated(s),
364 DiagMessage::FluentIdentifier(id, None) => SubdiagMessage::FluentIdentifier(id),
365 DiagMessage::FluentIdentifier(_, Some(attr)) => SubdiagMessage::FluentAttr(attr),
368 }
369 }
370}
371
372#[derive(Clone, Debug)]
374pub struct SpanLabel {
375 pub span: Span,
377
378 pub is_primary: bool,
381
382 pub label: Option<DiagMessage>,
384}
385
386#[derive(Clone, Debug, Hash, PartialEq, Eq, Encodable, Decodable)]
395pub struct MultiSpan {
396 primary_spans: Vec<Span>,
397 span_labels: Vec<(Span, DiagMessage)>,
398}
399
400impl MultiSpan {
401 #[inline]
402 pub fn new() -> MultiSpan {
403 MultiSpan { primary_spans: vec![], span_labels: vec![] }
404 }
405
406 pub fn from_span(primary_span: Span) -> MultiSpan {
407 MultiSpan { primary_spans: vec![primary_span], span_labels: vec![] }
408 }
409
410 pub fn from_spans(mut vec: Vec<Span>) -> MultiSpan {
411 vec.sort();
412 MultiSpan { primary_spans: vec, span_labels: vec![] }
413 }
414
415 pub fn push_span_label(&mut self, span: Span, label: impl Into<DiagMessage>) {
416 self.span_labels.push((span, label.into()));
417 }
418
419 pub fn primary_span(&self) -> Option<Span> {
421 self.primary_spans.first().cloned()
422 }
423
424 pub fn primary_spans(&self) -> &[Span] {
426 &self.primary_spans
427 }
428
429 pub fn has_primary_spans(&self) -> bool {
431 !self.is_dummy()
432 }
433
434 pub fn is_dummy(&self) -> bool {
436 self.primary_spans.iter().all(|sp| sp.is_dummy())
437 }
438
439 pub fn replace(&mut self, before: Span, after: Span) -> bool {
442 let mut replacements_occurred = false;
443 for primary_span in &mut self.primary_spans {
444 if *primary_span == before {
445 *primary_span = after;
446 replacements_occurred = true;
447 }
448 }
449 for span_label in &mut self.span_labels {
450 if span_label.0 == before {
451 span_label.0 = after;
452 replacements_occurred = true;
453 }
454 }
455 replacements_occurred
456 }
457
458 pub fn span_labels(&self) -> Vec<SpanLabel> {
464 let is_primary = |span| self.primary_spans.contains(&span);
465
466 let mut span_labels = self
467 .span_labels
468 .iter()
469 .map(|&(span, ref label)| SpanLabel {
470 span,
471 is_primary: is_primary(span),
472 label: Some(label.clone()),
473 })
474 .collect::<Vec<_>>();
475
476 for &span in &self.primary_spans {
477 if !span_labels.iter().any(|sl| sl.span == span) {
478 span_labels.push(SpanLabel { span, is_primary: true, label: None });
479 }
480 }
481
482 span_labels
483 }
484
485 pub fn has_span_labels(&self) -> bool {
487 self.span_labels.iter().any(|(sp, _)| !sp.is_dummy())
488 }
489
490 pub fn clone_ignoring_labels(&self) -> Self {
495 Self { primary_spans: self.primary_spans.clone(), ..MultiSpan::new() }
496 }
497}
498
499impl From<Span> for MultiSpan {
500 fn from(span: Span) -> MultiSpan {
501 MultiSpan::from_span(span)
502 }
503}
504
505impl From<Vec<Span>> for MultiSpan {
506 fn from(spans: Vec<Span>) -> MultiSpan {
507 MultiSpan::from_spans(spans)
508 }
509}
510
511fn icu_locale_from_unic_langid(lang: LanguageIdentifier) -> Option<icu_locale::Locale> {
512 icu_locale::Locale::try_from_str(&lang.to_string()).ok()
513}
514
515pub fn fluent_value_from_str_list_sep_by_and(l: Vec<Cow<'_, str>>) -> FluentValue<'_> {
516 #[derive(Clone, PartialEq, Debug)]
518 struct FluentStrListSepByAnd(Vec<String>);
519
520 impl FluentType for FluentStrListSepByAnd {
521 fn duplicate(&self) -> Box<dyn FluentType + Send> {
522 Box::new(self.clone())
523 }
524
525 fn as_string(&self, intls: &intl_memoizer::IntlLangMemoizer) -> Cow<'static, str> {
526 let result = intls
527 .with_try_get::<MemoizableListFormatter, _, _>((), |list_formatter| {
528 list_formatter.format_to_string(self.0.iter())
529 })
530 .unwrap();
531 Cow::Owned(result)
532 }
533
534 fn as_string_threadsafe(
535 &self,
536 intls: &intl_memoizer::concurrent::IntlLangMemoizer,
537 ) -> Cow<'static, str> {
538 let result = intls
539 .with_try_get::<MemoizableListFormatter, _, _>((), |list_formatter| {
540 list_formatter.format_to_string(self.0.iter())
541 })
542 .unwrap();
543 Cow::Owned(result)
544 }
545 }
546
547 struct MemoizableListFormatter(icu_list::ListFormatter);
548
549 impl std::ops::Deref for MemoizableListFormatter {
550 type Target = icu_list::ListFormatter;
551 fn deref(&self) -> &Self::Target {
552 &self.0
553 }
554 }
555
556 impl intl_memoizer::Memoizable for MemoizableListFormatter {
557 type Args = ();
558 type Error = ();
559
560 fn construct(lang: LanguageIdentifier, _args: Self::Args) -> Result<Self, Self::Error>
561 where
562 Self: Sized,
563 {
564 let locale = icu_locale_from_unic_langid(lang)
565 .unwrap_or_else(|| rustc_baked_icu_data::supported_locales::EN);
566 let list_formatter = icu_list::ListFormatter::try_new_and_unstable(
567 &rustc_baked_icu_data::BakedDataProvider,
568 locale.into(),
569 icu_list::options::ListFormatterOptions::default()
570 .with_length(icu_list::options::ListLength::Wide),
571 )
572 .expect("Failed to create list formatter");
573
574 Ok(MemoizableListFormatter(list_formatter))
575 }
576 }
577
578 let l = l.into_iter().map(|x| x.into_owned()).collect();
579
580 FluentValue::Custom(Box::new(FluentStrListSepByAnd(l)))
581}
582
583pub type DiagArg<'iter> = (&'iter DiagArgName, &'iter DiagArgValue);
587
588pub type DiagArgName = Cow<'static, str>;
590
591#[derive(Clone, Debug, PartialEq, Eq, Hash, Encodable, Decodable)]
594pub enum DiagArgValue {
595 Str(Cow<'static, str>),
596 Number(i32),
600 StrListSepByAnd(Vec<Cow<'static, str>>),
601}
602
603pub trait IntoDiagArg {
608 fn into_diag_arg(self, path: &mut Option<std::path::PathBuf>) -> DiagArgValue;
615}
616
617impl IntoDiagArg for DiagArgValue {
618 fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
619 self
620 }
621}
622
623impl From<DiagArgValue> for FluentValue<'static> {
624 fn from(val: DiagArgValue) -> Self {
625 match val {
626 DiagArgValue::Str(s) => From::from(s),
627 DiagArgValue::Number(n) => From::from(n),
628 DiagArgValue::StrListSepByAnd(l) => fluent_value_from_str_list_sep_by_and(l),
629 }
630 }
631}