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
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 pop_span_label(&mut self) -> Option<(Span, DiagMessage)> {
459 self.span_labels.pop()
460 }
461
462 pub fn span_labels(&self) -> Vec<SpanLabel> {
468 let is_primary = |span| self.primary_spans.contains(&span);
469
470 let mut span_labels = self
471 .span_labels
472 .iter()
473 .map(|&(span, ref label)| SpanLabel {
474 span,
475 is_primary: is_primary(span),
476 label: Some(label.clone()),
477 })
478 .collect::<Vec<_>>();
479
480 for &span in &self.primary_spans {
481 if !span_labels.iter().any(|sl| sl.span == span) {
482 span_labels.push(SpanLabel { span, is_primary: true, label: None });
483 }
484 }
485
486 span_labels
487 }
488
489 pub fn has_span_labels(&self) -> bool {
491 self.span_labels.iter().any(|(sp, _)| !sp.is_dummy())
492 }
493
494 pub fn clone_ignoring_labels(&self) -> Self {
499 Self { primary_spans: self.primary_spans.clone(), ..MultiSpan::new() }
500 }
501}
502
503impl From<Span> for MultiSpan {
504 fn from(span: Span) -> MultiSpan {
505 MultiSpan::from_span(span)
506 }
507}
508
509impl From<Vec<Span>> for MultiSpan {
510 fn from(spans: Vec<Span>) -> MultiSpan {
511 MultiSpan::from_spans(spans)
512 }
513}
514
515fn icu_locale_from_unic_langid(lang: LanguageIdentifier) -> Option<icu_locid::Locale> {
516 icu_locid::Locale::try_from_bytes(lang.to_string().as_bytes()).ok()
517}
518
519pub fn fluent_value_from_str_list_sep_by_and(l: Vec<Cow<'_, str>>) -> FluentValue<'_> {
520 #[derive(Clone, PartialEq, Debug)]
522 struct FluentStrListSepByAnd(Vec<String>);
523
524 impl FluentType for FluentStrListSepByAnd {
525 fn duplicate(&self) -> Box<dyn FluentType + Send> {
526 Box::new(self.clone())
527 }
528
529 fn as_string(&self, intls: &intl_memoizer::IntlLangMemoizer) -> Cow<'static, str> {
530 let result = intls
531 .with_try_get::<MemoizableListFormatter, _, _>((), |list_formatter| {
532 list_formatter.format_to_string(self.0.iter())
533 })
534 .unwrap();
535 Cow::Owned(result)
536 }
537
538 fn as_string_threadsafe(
539 &self,
540 intls: &intl_memoizer::concurrent::IntlLangMemoizer,
541 ) -> Cow<'static, str> {
542 let result = intls
543 .with_try_get::<MemoizableListFormatter, _, _>((), |list_formatter| {
544 list_formatter.format_to_string(self.0.iter())
545 })
546 .unwrap();
547 Cow::Owned(result)
548 }
549 }
550
551 struct MemoizableListFormatter(icu_list::ListFormatter);
552
553 impl std::ops::Deref for MemoizableListFormatter {
554 type Target = icu_list::ListFormatter;
555 fn deref(&self) -> &Self::Target {
556 &self.0
557 }
558 }
559
560 impl intl_memoizer::Memoizable for MemoizableListFormatter {
561 type Args = ();
562 type Error = ();
563
564 fn construct(lang: LanguageIdentifier, _args: Self::Args) -> Result<Self, Self::Error>
565 where
566 Self: Sized,
567 {
568 let baked_data_provider = rustc_baked_icu_data::baked_data_provider();
569 let locale_fallbacker =
570 LocaleFallbacker::try_new_with_any_provider(&baked_data_provider)
571 .expect("Failed to create fallback provider");
572 let data_provider =
573 LocaleFallbackProvider::new_with_fallbacker(baked_data_provider, locale_fallbacker);
574 let locale = icu_locale_from_unic_langid(lang)
575 .unwrap_or_else(|| rustc_baked_icu_data::supported_locales::EN);
576 let list_formatter =
577 icu_list::ListFormatter::try_new_and_with_length_with_any_provider(
578 &data_provider,
579 &locale.into(),
580 icu_list::ListLength::Wide,
581 )
582 .expect("Failed to create list formatter");
583
584 Ok(MemoizableListFormatter(list_formatter))
585 }
586 }
587
588 let l = l.into_iter().map(|x| x.into_owned()).collect();
589
590 FluentValue::Custom(Box::new(FluentStrListSepByAnd(l)))
591}