1#![allow(internal_features)]
3#![doc(rust_logo)]
4#![feature(rustc_attrs)]
5#![feature(rustdoc_internals)]
6#![feature(type_alias_impl_trait)]
7use std::borrow::Cow;
10use std::error::Error;
11use std::path::{Path, PathBuf};
12use std::sync::{Arc, LazyLock};
13use std::{fmt, fs, io};
14
15use fluent_bundle::FluentResource;
16pub use fluent_bundle::types::FluentType;
17pub use fluent_bundle::{self, FluentArgs, FluentError, FluentValue};
18use fluent_syntax::parser::ParserError;
19use icu_provider_adapters::fallback::{LocaleFallbackProvider, LocaleFallbacker};
20use intl_memoizer::concurrent::IntlLangMemoizer;
21use rustc_data_structures::sync::IntoDynSyncSend;
22use rustc_macros::{Decodable, Encodable};
23use rustc_span::Span;
24use tracing::{instrument, trace};
25pub use unic_langid::{LanguageIdentifier, langid};
26
27pub type FluentBundle =
28 IntoDynSyncSend<fluent_bundle::bundle::FluentBundle<FluentResource, IntlLangMemoizer>>;
29
30fn new_bundle(locales: Vec<LanguageIdentifier>) -> FluentBundle {
31 IntoDynSyncSend(fluent_bundle::bundle::FluentBundle::new_concurrent(locales))
32}
33
34#[derive(Debug)]
35pub enum TranslationBundleError {
36 ReadFtl(io::Error),
38 ParseFtl(ParserError),
40 AddResource(FluentError),
42 MissingLocale,
44 ReadLocalesDir(io::Error),
46 ReadLocalesDirEntry(io::Error),
48 LocaleIsNotDir,
50}
51
52impl fmt::Display for TranslationBundleError {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 match self {
55 TranslationBundleError::ReadFtl(e) => write!(f, "could not read ftl file: {e}"),
56 TranslationBundleError::ParseFtl(e) => {
57 write!(f, "could not parse ftl file: {e}")
58 }
59 TranslationBundleError::AddResource(e) => write!(f, "failed to add resource: {e}"),
60 TranslationBundleError::MissingLocale => write!(f, "missing locale directory"),
61 TranslationBundleError::ReadLocalesDir(e) => {
62 write!(f, "could not read locales dir: {e}")
63 }
64 TranslationBundleError::ReadLocalesDirEntry(e) => {
65 write!(f, "could not read locales dir entry: {e}")
66 }
67 TranslationBundleError::LocaleIsNotDir => {
68 write!(f, "`$sysroot/share/locales/$locale` is not a directory")
69 }
70 }
71 }
72}
73
74impl Error for TranslationBundleError {
75 fn source(&self) -> Option<&(dyn Error + 'static)> {
76 match self {
77 TranslationBundleError::ReadFtl(e) => Some(e),
78 TranslationBundleError::ParseFtl(e) => Some(e),
79 TranslationBundleError::AddResource(e) => Some(e),
80 TranslationBundleError::MissingLocale => None,
81 TranslationBundleError::ReadLocalesDir(e) => Some(e),
82 TranslationBundleError::ReadLocalesDirEntry(e) => Some(e),
83 TranslationBundleError::LocaleIsNotDir => None,
84 }
85 }
86}
87
88impl From<(FluentResource, Vec<ParserError>)> for TranslationBundleError {
89 fn from((_, mut errs): (FluentResource, Vec<ParserError>)) -> Self {
90 TranslationBundleError::ParseFtl(errs.pop().expect("failed ftl parse with no errors"))
91 }
92}
93
94impl From<Vec<FluentError>> for TranslationBundleError {
95 fn from(mut errs: Vec<FluentError>) -> Self {
96 TranslationBundleError::AddResource(
97 errs.pop().expect("failed adding resource to bundle with no errors"),
98 )
99 }
100}
101
102#[instrument(level = "trace")]
108pub fn fluent_bundle(
109 sysroot: PathBuf,
110 sysroot_candidates: Vec<PathBuf>,
111 requested_locale: Option<LanguageIdentifier>,
112 additional_ftl_path: Option<&Path>,
113 with_directionality_markers: bool,
114) -> Result<Option<Arc<FluentBundle>>, TranslationBundleError> {
115 if requested_locale.is_none() && additional_ftl_path.is_none() {
116 return Ok(None);
117 }
118
119 let fallback_locale = langid!("en-US");
120 let requested_fallback_locale = requested_locale.as_ref() == Some(&fallback_locale);
121 trace!(?requested_fallback_locale);
122 if requested_fallback_locale && additional_ftl_path.is_none() {
123 return Ok(None);
124 }
125 let locale = requested_locale.clone().unwrap_or(fallback_locale);
128 trace!(?locale);
129 let mut bundle = new_bundle(vec![locale]);
130
131 register_functions(&mut bundle);
133
134 bundle.set_use_isolating(with_directionality_markers);
140
141 if let Some(requested_locale) = requested_locale {
143 let mut found_resources = false;
144 for mut sysroot in Some(sysroot).into_iter().chain(sysroot_candidates.into_iter()) {
145 sysroot.push("share");
146 sysroot.push("locale");
147 sysroot.push(requested_locale.to_string());
148 trace!(?sysroot);
149
150 if !sysroot.exists() {
151 trace!("skipping");
152 continue;
153 }
154
155 if !sysroot.is_dir() {
156 return Err(TranslationBundleError::LocaleIsNotDir);
157 }
158
159 for entry in sysroot.read_dir().map_err(TranslationBundleError::ReadLocalesDir)? {
160 let entry = entry.map_err(TranslationBundleError::ReadLocalesDirEntry)?;
161 let path = entry.path();
162 trace!(?path);
163 if path.extension().and_then(|s| s.to_str()) != Some("ftl") {
164 trace!("skipping");
165 continue;
166 }
167
168 let resource_str =
169 fs::read_to_string(path).map_err(TranslationBundleError::ReadFtl)?;
170 let resource =
171 FluentResource::try_new(resource_str).map_err(TranslationBundleError::from)?;
172 trace!(?resource);
173 bundle.add_resource(resource).map_err(TranslationBundleError::from)?;
174 found_resources = true;
175 }
176 }
177
178 if !found_resources {
179 return Err(TranslationBundleError::MissingLocale);
180 }
181 }
182
183 if let Some(additional_ftl_path) = additional_ftl_path {
184 let resource_str =
185 fs::read_to_string(additional_ftl_path).map_err(TranslationBundleError::ReadFtl)?;
186 let resource =
187 FluentResource::try_new(resource_str).map_err(TranslationBundleError::from)?;
188 trace!(?resource);
189 bundle.add_resource_overriding(resource);
190 }
191
192 let bundle = Arc::new(bundle);
193 Ok(Some(bundle))
194}
195
196fn register_functions(bundle: &mut FluentBundle) {
197 bundle
198 .add_function("STREQ", |positional, _named| match positional {
199 [FluentValue::String(a), FluentValue::String(b)] => format!("{}", (a == b)).into(),
200 _ => FluentValue::Error,
201 })
202 .expect("Failed to add a function to the bundle.");
203}
204
205pub type LazyFallbackBundle = Arc<LazyLock<FluentBundle, impl FnOnce() -> FluentBundle>>;
208
209#[instrument(level = "trace", skip(resources))]
211#[cfg_attr(not(bootstrap), define_opaque(LazyFallbackBundle))]
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}