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