1#![allow(internal_features)]
3#![doc(rust_logo)]
4#![feature(rustc_attrs)]
5#![feature(rustdoc_internals)]
6#![feature(type_alias_impl_trait)]
7#![warn(unreachable_pub)]
8use std::borrow::Cow;
11use std::error::Error;
12use std::path::{Path, PathBuf};
13use std::sync::{Arc, LazyLock};
14use std::{fmt, fs, io};
15
16use fluent_bundle::FluentResource;
17pub use fluent_bundle::types::FluentType;
18pub use fluent_bundle::{self, FluentArgs, FluentError, FluentValue};
19use fluent_syntax::parser::ParserError;
20use icu_provider_adapters::fallback::{LocaleFallbackProvider, LocaleFallbacker};
21use intl_memoizer::concurrent::IntlLangMemoizer;
22use rustc_data_structures::sync::IntoDynSyncSend;
23use rustc_macros::{Decodable, Encodable};
24use rustc_span::Span;
25use tracing::{instrument, trace};
26pub use unic_langid::{LanguageIdentifier, langid};
27
28pub type FluentBundle =
29 IntoDynSyncSend<fluent_bundle::bundle::FluentBundle<FluentResource, IntlLangMemoizer>>;
30
31fn new_bundle(locales: Vec<LanguageIdentifier>) -> FluentBundle {
32 IntoDynSyncSend(fluent_bundle::bundle::FluentBundle::new_concurrent(locales))
33}
34
35#[derive(Debug)]
36pub enum TranslationBundleError {
37 ReadFtl(io::Error),
39 ParseFtl(ParserError),
41 AddResource(FluentError),
43 MissingLocale,
45 ReadLocalesDir(io::Error),
47 ReadLocalesDirEntry(io::Error),
49 LocaleIsNotDir,
51}
52
53impl fmt::Display for TranslationBundleError {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 match self {
56 TranslationBundleError::ReadFtl(e) => write!(f, "could not read ftl file: {e}"),
57 TranslationBundleError::ParseFtl(e) => {
58 write!(f, "could not parse ftl file: {e}")
59 }
60 TranslationBundleError::AddResource(e) => write!(f, "failed to add resource: {e}"),
61 TranslationBundleError::MissingLocale => write!(f, "missing locale directory"),
62 TranslationBundleError::ReadLocalesDir(e) => {
63 write!(f, "could not read locales dir: {e}")
64 }
65 TranslationBundleError::ReadLocalesDirEntry(e) => {
66 write!(f, "could not read locales dir entry: {e}")
67 }
68 TranslationBundleError::LocaleIsNotDir => {
69 write!(f, "`$sysroot/share/locales/$locale` is not a directory")
70 }
71 }
72 }
73}
74
75impl Error for TranslationBundleError {
76 fn source(&self) -> Option<&(dyn Error + 'static)> {
77 match self {
78 TranslationBundleError::ReadFtl(e) => Some(e),
79 TranslationBundleError::ParseFtl(e) => Some(e),
80 TranslationBundleError::AddResource(e) => Some(e),
81 TranslationBundleError::MissingLocale => None,
82 TranslationBundleError::ReadLocalesDir(e) => Some(e),
83 TranslationBundleError::ReadLocalesDirEntry(e) => Some(e),
84 TranslationBundleError::LocaleIsNotDir => None,
85 }
86 }
87}
88
89impl From<(FluentResource, Vec<ParserError>)> for TranslationBundleError {
90 fn from((_, mut errs): (FluentResource, Vec<ParserError>)) -> Self {
91 TranslationBundleError::ParseFtl(errs.pop().expect("failed ftl parse with no errors"))
92 }
93}
94
95impl From<Vec<FluentError>> for TranslationBundleError {
96 fn from(mut errs: Vec<FluentError>) -> Self {
97 TranslationBundleError::AddResource(
98 errs.pop().expect("failed adding resource to bundle with no errors"),
99 )
100 }
101}
102
103#[instrument(level = "trace")]
109pub fn fluent_bundle(
110 mut user_provided_sysroot: Option<PathBuf>,
111 mut sysroot_candidates: Vec<PathBuf>,
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 user_provided_sysroot.iter_mut().chain(sysroot_candidates.iter_mut()) {
146 sysroot.push("share");
147 sysroot.push("locale");
148 sysroot.push(requested_locale.to_string());
149 trace!(?sysroot);
150
151 if !sysroot.exists() {
152 trace!("skipping");
153 continue;
154 }
155
156 if !sysroot.is_dir() {
157 return Err(TranslationBundleError::LocaleIsNotDir);
158 }
159
160 for entry in sysroot.read_dir().map_err(TranslationBundleError::ReadLocalesDir)? {
161 let entry = entry.map_err(TranslationBundleError::ReadLocalesDirEntry)?;
162 let path = entry.path();
163 trace!(?path);
164 if path.extension().and_then(|s| s.to_str()) != Some("ftl") {
165 trace!("skipping");
166 continue;
167 }
168
169 let resource_str =
170 fs::read_to_string(path).map_err(TranslationBundleError::ReadFtl)?;
171 let resource =
172 FluentResource::try_new(resource_str).map_err(TranslationBundleError::from)?;
173 trace!(?resource);
174 bundle.add_resource(resource).map_err(TranslationBundleError::from)?;
175 found_resources = true;
176 }
177 }
178
179 if !found_resources {
180 return Err(TranslationBundleError::MissingLocale);
181 }
182 }
183
184 if let Some(additional_ftl_path) = additional_ftl_path {
185 let resource_str =
186 fs::read_to_string(additional_ftl_path).map_err(TranslationBundleError::ReadFtl)?;
187 let resource =
188 FluentResource::try_new(resource_str).map_err(TranslationBundleError::from)?;
189 trace!(?resource);
190 bundle.add_resource_overriding(resource);
191 }
192
193 let bundle = Arc::new(bundle);
194 Ok(Some(bundle))
195}
196
197fn register_functions(bundle: &mut FluentBundle) {
198 bundle
199 .add_function("STREQ", |positional, _named| match positional {
200 [FluentValue::String(a), FluentValue::String(b)] => format!("{}", (a == b)).into(),
201 _ => FluentValue::Error,
202 })
203 .expect("Failed to add a function to the bundle.");
204}
205
206pub type LazyFallbackBundle = Arc<LazyLock<FluentBundle, impl FnOnce() -> FluentBundle>>;
209
210#[instrument(level = "trace", skip(resources))]
212pub fn fallback_fluent_bundle(
213 resources: Vec<&'static str>,
214 with_directionality_markers: bool,
215) -> LazyFallbackBundle {
216 Arc::new(LazyLock::new(move || {
217 let mut fallback_bundle = new_bundle(vec![langid!("en-US")]);
218
219 register_functions(&mut fallback_bundle);
220
221 fallback_bundle.set_use_isolating(with_directionality_markers);
223
224 for resource in resources {
225 let resource = FluentResource::try_new(resource.to_string())
226 .expect("failed to parse fallback fluent resource");
227 fallback_bundle.add_resource_overriding(resource);
228 }
229
230 fallback_bundle
231 }))
232}
233
234type FluentId = Cow<'static, str>;
236
237#[rustc_diagnostic_item = "SubdiagMessage"]
245pub enum SubdiagMessage {
246 Str(Cow<'static, str>),
248 Translated(Cow<'static, str>),
255 FluentIdentifier(FluentId),
258 FluentAttr(FluentId),
264}
265
266impl From<String> for SubdiagMessage {
267 fn from(s: String) -> Self {
268 SubdiagMessage::Str(Cow::Owned(s))
269 }
270}
271impl From<&'static str> for SubdiagMessage {
272 fn from(s: &'static str) -> Self {
273 SubdiagMessage::Str(Cow::Borrowed(s))
274 }
275}
276impl From<Cow<'static, str>> for SubdiagMessage {
277 fn from(s: Cow<'static, str>) -> Self {
278 SubdiagMessage::Str(s)
279 }
280}
281
282#[derive(Clone, Debug, PartialEq, Eq, Hash, Encodable, Decodable)]
287#[rustc_diagnostic_item = "DiagMessage"]
288pub enum DiagMessage {
289 Str(Cow<'static, str>),
291 Translated(Cow<'static, str>),
298 FluentIdentifier(FluentId, Option<FluentId>),
304}
305
306impl DiagMessage {
307 pub fn with_subdiagnostic_message(&self, sub: SubdiagMessage) -> Self {
313 let attr = match sub {
314 SubdiagMessage::Str(s) => return DiagMessage::Str(s),
315 SubdiagMessage::Translated(s) => return DiagMessage::Translated(s),
316 SubdiagMessage::FluentIdentifier(id) => {
317 return DiagMessage::FluentIdentifier(id, None);
318 }
319 SubdiagMessage::FluentAttr(attr) => attr,
320 };
321
322 match self {
323 DiagMessage::Str(s) => DiagMessage::Str(s.clone()),
324 DiagMessage::Translated(s) => DiagMessage::Translated(s.clone()),
325 DiagMessage::FluentIdentifier(id, _) => {
326 DiagMessage::FluentIdentifier(id.clone(), Some(attr))
327 }
328 }
329 }
330
331 pub fn as_str(&self) -> Option<&str> {
332 match self {
333 DiagMessage::Translated(s) | DiagMessage::Str(s) => Some(s),
334 DiagMessage::FluentIdentifier(_, _) => None,
335 }
336 }
337}
338
339impl From<String> for DiagMessage {
340 fn from(s: String) -> Self {
341 DiagMessage::Str(Cow::Owned(s))
342 }
343}
344impl From<&'static str> for DiagMessage {
345 fn from(s: &'static str) -> Self {
346 DiagMessage::Str(Cow::Borrowed(s))
347 }
348}
349impl From<Cow<'static, str>> for DiagMessage {
350 fn from(s: Cow<'static, str>) -> Self {
351 DiagMessage::Str(s)
352 }
353}
354
355impl From<DiagMessage> for SubdiagMessage {
361 fn from(val: DiagMessage) -> Self {
362 match val {
363 DiagMessage::Str(s) => SubdiagMessage::Str(s),
364 DiagMessage::Translated(s) => SubdiagMessage::Translated(s),
365 DiagMessage::FluentIdentifier(id, None) => SubdiagMessage::FluentIdentifier(id),
366 DiagMessage::FluentIdentifier(_, Some(attr)) => SubdiagMessage::FluentAttr(attr),
369 }
370 }
371}
372
373#[derive(Clone, Debug)]
375pub struct SpanLabel {
376 pub span: Span,
378
379 pub is_primary: bool,
382
383 pub label: Option<DiagMessage>,
385}
386
387#[derive(Clone, Debug, Hash, PartialEq, Eq, Encodable, Decodable)]
396pub struct MultiSpan {
397 primary_spans: Vec<Span>,
398 span_labels: Vec<(Span, DiagMessage)>,
399}
400
401impl MultiSpan {
402 #[inline]
403 pub fn new() -> MultiSpan {
404 MultiSpan { primary_spans: vec![], span_labels: vec![] }
405 }
406
407 pub fn from_span(primary_span: Span) -> MultiSpan {
408 MultiSpan { primary_spans: vec![primary_span], span_labels: vec![] }
409 }
410
411 pub fn from_spans(mut vec: Vec<Span>) -> MultiSpan {
412 vec.sort();
413 MultiSpan { primary_spans: vec, span_labels: vec![] }
414 }
415
416 pub fn push_span_label(&mut self, span: Span, label: impl Into<DiagMessage>) {
417 self.span_labels.push((span, label.into()));
418 }
419
420 pub fn primary_span(&self) -> Option<Span> {
422 self.primary_spans.first().cloned()
423 }
424
425 pub fn primary_spans(&self) -> &[Span] {
427 &self.primary_spans
428 }
429
430 pub fn has_primary_spans(&self) -> bool {
432 !self.is_dummy()
433 }
434
435 pub fn is_dummy(&self) -> bool {
437 self.primary_spans.iter().all(|sp| sp.is_dummy())
438 }
439
440 pub fn replace(&mut self, before: Span, after: Span) -> bool {
443 let mut replacements_occurred = false;
444 for primary_span in &mut self.primary_spans {
445 if *primary_span == before {
446 *primary_span = after;
447 replacements_occurred = true;
448 }
449 }
450 for span_label in &mut self.span_labels {
451 if span_label.0 == before {
452 span_label.0 = after;
453 replacements_occurred = true;
454 }
455 }
456 replacements_occurred
457 }
458
459 pub fn pop_span_label(&mut self) -> Option<(Span, DiagMessage)> {
460 self.span_labels.pop()
461 }
462
463 pub fn span_labels(&self) -> Vec<SpanLabel> {
469 let is_primary = |span| self.primary_spans.contains(&span);
470
471 let mut span_labels = self
472 .span_labels
473 .iter()
474 .map(|&(span, ref label)| SpanLabel {
475 span,
476 is_primary: is_primary(span),
477 label: Some(label.clone()),
478 })
479 .collect::<Vec<_>>();
480
481 for &span in &self.primary_spans {
482 if !span_labels.iter().any(|sl| sl.span == span) {
483 span_labels.push(SpanLabel { span, is_primary: true, label: None });
484 }
485 }
486
487 span_labels
488 }
489
490 pub fn has_span_labels(&self) -> bool {
492 self.span_labels.iter().any(|(sp, _)| !sp.is_dummy())
493 }
494
495 pub fn clone_ignoring_labels(&self) -> Self {
500 Self { primary_spans: self.primary_spans.clone(), ..MultiSpan::new() }
501 }
502}
503
504impl From<Span> for MultiSpan {
505 fn from(span: Span) -> MultiSpan {
506 MultiSpan::from_span(span)
507 }
508}
509
510impl From<Vec<Span>> for MultiSpan {
511 fn from(spans: Vec<Span>) -> MultiSpan {
512 MultiSpan::from_spans(spans)
513 }
514}
515
516fn icu_locale_from_unic_langid(lang: LanguageIdentifier) -> Option<icu_locid::Locale> {
517 icu_locid::Locale::try_from_bytes(lang.to_string().as_bytes()).ok()
518}
519
520pub fn fluent_value_from_str_list_sep_by_and(l: Vec<Cow<'_, str>>) -> FluentValue<'_> {
521 #[derive(Clone, PartialEq, Debug)]
523 struct FluentStrListSepByAnd(Vec<String>);
524
525 impl FluentType for FluentStrListSepByAnd {
526 fn duplicate(&self) -> Box<dyn FluentType + Send> {
527 Box::new(self.clone())
528 }
529
530 fn as_string(&self, intls: &intl_memoizer::IntlLangMemoizer) -> Cow<'static, str> {
531 let result = intls
532 .with_try_get::<MemoizableListFormatter, _, _>((), |list_formatter| {
533 list_formatter.format_to_string(self.0.iter())
534 })
535 .unwrap();
536 Cow::Owned(result)
537 }
538
539 fn as_string_threadsafe(
540 &self,
541 intls: &intl_memoizer::concurrent::IntlLangMemoizer,
542 ) -> Cow<'static, str> {
543 let result = intls
544 .with_try_get::<MemoizableListFormatter, _, _>((), |list_formatter| {
545 list_formatter.format_to_string(self.0.iter())
546 })
547 .unwrap();
548 Cow::Owned(result)
549 }
550 }
551
552 struct MemoizableListFormatter(icu_list::ListFormatter);
553
554 impl std::ops::Deref for MemoizableListFormatter {
555 type Target = icu_list::ListFormatter;
556 fn deref(&self) -> &Self::Target {
557 &self.0
558 }
559 }
560
561 impl intl_memoizer::Memoizable for MemoizableListFormatter {
562 type Args = ();
563 type Error = ();
564
565 fn construct(lang: LanguageIdentifier, _args: Self::Args) -> Result<Self, Self::Error>
566 where
567 Self: Sized,
568 {
569 let baked_data_provider = rustc_baked_icu_data::baked_data_provider();
570 let locale_fallbacker =
571 LocaleFallbacker::try_new_with_any_provider(&baked_data_provider)
572 .expect("Failed to create fallback provider");
573 let data_provider =
574 LocaleFallbackProvider::new_with_fallbacker(baked_data_provider, locale_fallbacker);
575 let locale = icu_locale_from_unic_langid(lang)
576 .unwrap_or_else(|| rustc_baked_icu_data::supported_locales::EN);
577 let list_formatter =
578 icu_list::ListFormatter::try_new_and_with_length_with_any_provider(
579 &data_provider,
580 &locale.into(),
581 icu_list::ListLength::Wide,
582 )
583 .expect("Failed to create list formatter");
584
585 Ok(MemoizableListFormatter(list_formatter))
586 }
587 }
588
589 let l = l.into_iter().map(|x| x.into_owned()).collect();
590
591 FluentValue::Custom(Box::new(FluentStrListSepByAnd(l)))
592}