1 #![feature(let_chains)]
3 #![feature(rustc_attrs)]
4 #![feature(type_alias_impl_trait)]
6 use fluent_bundle
::FluentResource
;
7 use fluent_syntax
::parser
::ParserError
;
8 use rustc_data_structures
::sync
::Lrc
;
9 use rustc_macros
::{fluent_messages, Decodable, Encodable}
;
12 use std
::error
::Error
;
16 use std
::path
::{Path, PathBuf}
;
17 use tracing
::{instrument, trace}
;
19 #[cfg(not(parallel_compiler))]
20 use std
::cell
::LazyCell
as Lazy
;
21 #[cfg(parallel_compiler)]
22 use std
::sync
::LazyLock
as Lazy
;
24 #[cfg(parallel_compiler)]
25 use intl_memoizer
::concurrent
::IntlLangMemoizer
;
26 #[cfg(not(parallel_compiler))]
27 use intl_memoizer
::IntlLangMemoizer
;
29 pub use fluent_bundle
::{FluentArgs, FluentError, FluentValue}
;
30 pub use unic_langid
::{langid, LanguageIdentifier}
;
32 // Generates `DEFAULT_LOCALE_RESOURCES` static and `fluent_generated` module.
34 parser
=> "../locales/en-US/parser.ftl",
35 typeck
=> "../locales/en-US/typeck.ftl",
36 builtin_macros
=> "../locales/en-US/builtin_macros.ftl",
39 pub use fluent_generated
::{self as fluent, DEFAULT_LOCALE_RESOURCES}
;
41 pub type FluentBundle
= fluent_bundle
::bundle
::FluentBundle
<FluentResource
, IntlLangMemoizer
>;
43 #[cfg(parallel_compiler)]
44 fn new_bundle(locales
: Vec
<LanguageIdentifier
>) -> FluentBundle
{
45 FluentBundle
::new_concurrent(locales
)
48 #[cfg(not(parallel_compiler))]
49 fn new_bundle(locales
: Vec
<LanguageIdentifier
>) -> FluentBundle
{
50 FluentBundle
::new(locales
)
54 pub enum TranslationBundleError
{
55 /// Failed to read from `.ftl` file.
57 /// Failed to parse contents of `.ftl` file.
58 ParseFtl(ParserError
),
59 /// Failed to add `FluentResource` to `FluentBundle`.
60 AddResource(FluentError
),
61 /// `$sysroot/share/locale/$locale` does not exist.
63 /// Cannot read directory entries of `$sysroot/share/locale/$locale`.
64 ReadLocalesDir(io
::Error
),
65 /// Cannot read directory entry of `$sysroot/share/locale/$locale`.
66 ReadLocalesDirEntry(io
::Error
),
67 /// `$sysroot/share/locale/$locale` is not a directory.
71 impl fmt
::Display
for TranslationBundleError
{
72 fn fmt(&self, f
: &mut fmt
::Formatter
<'_
>) -> fmt
::Result
{
74 TranslationBundleError
::ReadFtl(e
) => write
!(f
, "could not read ftl file: {}", e
),
75 TranslationBundleError
::ParseFtl(e
) => {
76 write
!(f
, "could not parse ftl file: {}", e
)
78 TranslationBundleError
::AddResource(e
) => write
!(f
, "failed to add resource: {}", e
),
79 TranslationBundleError
::MissingLocale
=> write
!(f
, "missing locale directory"),
80 TranslationBundleError
::ReadLocalesDir(e
) => {
81 write
!(f
, "could not read locales dir: {}", e
)
83 TranslationBundleError
::ReadLocalesDirEntry(e
) => {
84 write
!(f
, "could not read locales dir entry: {}", e
)
86 TranslationBundleError
::LocaleIsNotDir
=> {
87 write
!(f
, "`$sysroot/share/locales/$locale` is not a directory")
93 impl Error
for TranslationBundleError
{
94 fn source(&self) -> Option
<&(dyn Error
+ '
static)> {
96 TranslationBundleError
::ReadFtl(e
) => Some(e
),
97 TranslationBundleError
::ParseFtl(e
) => Some(e
),
98 TranslationBundleError
::AddResource(e
) => Some(e
),
99 TranslationBundleError
::MissingLocale
=> None
,
100 TranslationBundleError
::ReadLocalesDir(e
) => Some(e
),
101 TranslationBundleError
::ReadLocalesDirEntry(e
) => Some(e
),
102 TranslationBundleError
::LocaleIsNotDir
=> None
,
107 impl From
<(FluentResource
, Vec
<ParserError
>)> for TranslationBundleError
{
108 fn from((_
, mut errs
): (FluentResource
, Vec
<ParserError
>)) -> Self {
109 TranslationBundleError
::ParseFtl(errs
.pop().expect("failed ftl parse with no errors"))
113 impl From
<Vec
<FluentError
>> for TranslationBundleError
{
114 fn from(mut errs
: Vec
<FluentError
>) -> Self {
115 TranslationBundleError
::AddResource(
116 errs
.pop().expect("failed adding resource to bundle with no errors"),
121 /// Returns Fluent bundle with the user's locale resources from
122 /// `$sysroot/share/locale/$requested_locale/*.ftl`.
124 /// If `-Z additional-ftl-path` was provided, load that resource and add it to the bundle
125 /// (overriding any conflicting messages).
126 #[instrument(level = "trace")]
127 pub fn fluent_bundle(
128 mut user_provided_sysroot
: Option
<PathBuf
>,
129 mut sysroot_candidates
: Vec
<PathBuf
>,
130 requested_locale
: Option
<LanguageIdentifier
>,
131 additional_ftl_path
: Option
<&Path
>,
132 with_directionality_markers
: bool
,
133 ) -> Result
<Option
<Lrc
<FluentBundle
>>, TranslationBundleError
> {
134 if requested_locale
.is_none() && additional_ftl_path
.is_none() {
138 let fallback_locale
= langid
!("en-US");
139 let requested_fallback_locale
= requested_locale
.as_ref() == Some(&fallback_locale
);
141 // If there is only `-Z additional-ftl-path`, assume locale is "en-US", otherwise use user
143 let locale
= requested_locale
.clone().unwrap_or(fallback_locale
);
145 let mut bundle
= new_bundle(vec
![locale
]);
147 // Fluent diagnostics can insert directionality isolation markers around interpolated variables
148 // indicating that there may be a shift from right-to-left to left-to-right text (or
149 // vice-versa). These are disabled because they are sometimes visible in the error output, but
150 // may be worth investigating in future (for example: if type names are left-to-right and the
151 // surrounding diagnostic messages are right-to-left, then these might be helpful).
152 bundle
.set_use_isolating(with_directionality_markers
);
154 // If the user requests the default locale then don't try to load anything.
155 if !requested_fallback_locale
&& let Some(requested_locale
) = requested_locale
{
156 let mut found_resources
= false;
157 for sysroot
in user_provided_sysroot
.iter_mut().chain(sysroot_candidates
.iter_mut()) {
158 sysroot
.push("share");
159 sysroot
.push("locale");
160 sysroot
.push(requested_locale
.to_string());
163 if !sysroot
.exists() {
168 if !sysroot
.is_dir() {
169 return Err(TranslationBundleError
::LocaleIsNotDir
);
172 for entry
in sysroot
.read_dir().map_err(TranslationBundleError
::ReadLocalesDir
)?
{
173 let entry
= entry
.map_err(TranslationBundleError
::ReadLocalesDirEntry
)?
;
174 let path
= entry
.path();
176 if path
.extension().and_then(|s
| s
.to_str()) != Some("ftl") {
182 fs
::read_to_string(path
).map_err(TranslationBundleError
::ReadFtl
)?
;
184 FluentResource
::try_new(resource_str
).map_err(TranslationBundleError
::from
)?
;
186 bundle
.add_resource(resource
).map_err(TranslationBundleError
::from
)?
;
187 found_resources
= true;
191 if !found_resources
{
192 return Err(TranslationBundleError
::MissingLocale
);
196 if let Some(additional_ftl_path
) = additional_ftl_path
{
198 fs
::read_to_string(additional_ftl_path
).map_err(TranslationBundleError
::ReadFtl
)?
;
200 FluentResource
::try_new(resource_str
).map_err(TranslationBundleError
::from
)?
;
202 bundle
.add_resource_overriding(resource
);
205 let bundle
= Lrc
::new(bundle
);
209 /// Type alias for the result of `fallback_fluent_bundle` - a reference-counted pointer to a lazily
210 /// evaluated fluent bundle.
211 pub type LazyFallbackBundle
= Lrc
<Lazy
<FluentBundle
, impl FnOnce() -> FluentBundle
>>;
213 /// Return the default `FluentBundle` with standard "en-US" diagnostic messages.
214 #[instrument(level = "trace")]
215 pub fn fallback_fluent_bundle(
216 resources
: &'
static [&'
static str],
217 with_directionality_markers
: bool
,
218 ) -> LazyFallbackBundle
{
219 Lrc
::new(Lazy
::new(move || {
220 let mut fallback_bundle
= new_bundle(vec
![langid
!("en-US")]);
221 // See comment in `fluent_bundle`.
222 fallback_bundle
.set_use_isolating(with_directionality_markers
);
224 for resource
in resources
{
225 let resource
= FluentResource
::try_new(resource
.to_string())
226 .expect("failed to parse fallback fluent resource");
228 fallback_bundle
.add_resource_overriding(resource
);
235 /// Identifier for the Fluent message/attribute corresponding to a diagnostic message.
236 type FluentId
= Cow
<'
static, str>;
238 /// Abstraction over a message in a subdiagnostic (i.e. label, note, help, etc) to support both
239 /// translatable and non-translatable diagnostic messages.
241 /// Translatable messages for subdiagnostics are typically attributes attached to a larger Fluent
242 /// message so messages of this type must be combined with a `DiagnosticMessage` (using
243 /// `DiagnosticMessage::with_subdiagnostic_message`) before rendering. However, subdiagnostics from
244 /// the `SessionSubdiagnostic` derive refer to Fluent identifiers directly.
245 #[rustc_diagnostic_item = "SubdiagnosticMessage"]
246 pub enum SubdiagnosticMessage
{
247 /// Non-translatable diagnostic message.
248 // FIXME(davidtwco): can a `Cow<'static, str>` be used here?
250 /// Identifier of a Fluent message. Instances of this variant are generated by the
251 /// `SessionSubdiagnostic` derive.
252 FluentIdentifier(FluentId
),
253 /// Attribute of a Fluent message. Needs to be combined with a Fluent identifier to produce an
254 /// actual translated message. Instances of this variant are generated by the `fluent_messages`
257 /// <https://projectfluent.org/fluent/guide/attributes.html>
258 FluentAttr(FluentId
),
261 impl SubdiagnosticMessage
{
262 /// Create a `SubdiagnosticMessage` for the provided Fluent attribute.
263 pub fn attr(id
: impl Into
<FluentId
>) -> Self {
264 SubdiagnosticMessage
::FluentAttr(id
.into())
267 /// Create a `SubdiagnosticMessage` for the provided Fluent identifier.
268 pub fn message(id
: impl Into
<FluentId
>) -> Self {
269 SubdiagnosticMessage
::FluentIdentifier(id
.into())
273 /// `From` impl that enables existing diagnostic calls to functions which now take
274 /// `impl Into<SubdiagnosticMessage>` to continue to work as before.
275 impl<S
: Into
<String
>> From
<S
> for SubdiagnosticMessage
{
276 fn from(s
: S
) -> Self {
277 SubdiagnosticMessage
::Str(s
.into())
281 /// Abstraction over a message in a diagnostic to support both translatable and non-translatable
282 /// diagnostic messages.
284 /// Intended to be removed once diagnostics are entirely translatable.
285 #[derive(Clone, Debug, PartialEq, Eq, Hash, Encodable, Decodable)]
286 #[rustc_diagnostic_item = "DiagnosticMessage"]
287 pub enum DiagnosticMessage
{
288 /// Non-translatable diagnostic message.
289 // FIXME(davidtwco): can a `Cow<'static, str>` be used here?
291 /// Identifier for a Fluent message (with optional attribute) corresponding to the diagnostic
294 /// <https://projectfluent.org/fluent/guide/hello.html>
295 /// <https://projectfluent.org/fluent/guide/attributes.html>
296 FluentIdentifier(FluentId
, Option
<FluentId
>),
299 impl DiagnosticMessage
{
300 /// Given a `SubdiagnosticMessage` which may contain a Fluent attribute, create a new
301 /// `DiagnosticMessage` that combines that attribute with the Fluent identifier of `self`.
303 /// - If the `SubdiagnosticMessage` is non-translatable then return the message as a
304 /// `DiagnosticMessage`.
305 /// - If `self` is non-translatable then return `self`'s message.
306 pub fn with_subdiagnostic_message(&self, sub
: SubdiagnosticMessage
) -> Self {
307 let attr
= match sub
{
308 SubdiagnosticMessage
::Str(s
) => return DiagnosticMessage
::Str(s
.clone()),
309 SubdiagnosticMessage
::FluentIdentifier(id
) => {
310 return DiagnosticMessage
::FluentIdentifier(id
, None
);
312 SubdiagnosticMessage
::FluentAttr(attr
) => attr
,
316 DiagnosticMessage
::Str(s
) => DiagnosticMessage
::Str(s
.clone()),
317 DiagnosticMessage
::FluentIdentifier(id
, _
) => {
318 DiagnosticMessage
::FluentIdentifier(id
.clone(), Some(attr
))
323 /// Returns the `String` contained within the `DiagnosticMessage::Str` variant, assuming that
324 /// this diagnostic message is of the legacy, non-translatable variety. Panics if this
325 /// assumption does not hold.
327 /// Don't use this - it exists to support some places that do comparison with diagnostic
329 pub fn expect_str(&self) -> &str {
331 DiagnosticMessage
::Str(s
) => s
,
332 _
=> panic
!("expected non-translatable diagnostic message"),
336 /// Create a `DiagnosticMessage` for the provided Fluent identifier.
337 pub fn new(id
: impl Into
<FluentId
>) -> Self {
338 DiagnosticMessage
::FluentIdentifier(id
.into(), None
)
342 /// `From` impl that enables existing diagnostic calls to functions which now take
343 /// `impl Into<DiagnosticMessage>` to continue to work as before.
344 impl<S
: Into
<String
>> From
<S
> for DiagnosticMessage
{
345 fn from(s
: S
) -> Self {
346 DiagnosticMessage
::Str(s
.into())
350 /// A span together with some additional data.
351 #[derive(Clone, Debug)]
352 pub struct SpanLabel
{
353 /// The span we are going to include in the final snippet.
356 /// Is this a primary span? This is the "locus" of the message,
357 /// and is indicated with a `^^^^` underline, versus `----`.
358 pub is_primary
: bool
,
360 /// What label should we attach to this span (if any)?
361 pub label
: Option
<DiagnosticMessage
>,
364 /// A collection of `Span`s.
366 /// Spans have two orthogonal attributes:
368 /// - They can be *primary spans*. In this case they are the locus of
369 /// the error, and would be rendered with `^^^`.
370 /// - They can have a *label*. In this case, the label is written next
371 /// to the mark in the snippet when we render.
372 #[derive(Clone, Debug, Hash, PartialEq, Eq, Encodable, Decodable)]
373 pub struct MultiSpan
{
374 primary_spans
: Vec
<Span
>,
375 span_labels
: Vec
<(Span
, DiagnosticMessage
)>,
380 pub fn new() -> MultiSpan
{
381 MultiSpan { primary_spans: vec![], span_labels: vec![] }
384 pub fn from_span(primary_span
: Span
) -> MultiSpan
{
385 MultiSpan { primary_spans: vec![primary_span], span_labels: vec![] }
388 pub fn from_spans(mut vec
: Vec
<Span
>) -> MultiSpan
{
390 MultiSpan { primary_spans: vec, span_labels: vec![] }
393 pub fn push_span_label(&mut self, span
: Span
, label
: impl Into
<DiagnosticMessage
>) {
394 self.span_labels
.push((span
, label
.into()));
397 /// Selects the first primary span (if any).
398 pub fn primary_span(&self) -> Option
<Span
> {
399 self.primary_spans
.first().cloned()
402 /// Returns all primary spans.
403 pub fn primary_spans(&self) -> &[Span
] {
407 /// Returns `true` if any of the primary spans are displayable.
408 pub fn has_primary_spans(&self) -> bool
{
412 /// Returns `true` if this contains only a dummy primary span with any hygienic context.
413 pub fn is_dummy(&self) -> bool
{
414 self.primary_spans
.iter().all(|sp
| sp
.is_dummy())
417 /// Replaces all occurrences of one Span with another. Used to move `Span`s in areas that don't
418 /// display well (like std macros). Returns whether replacements occurred.
419 pub fn replace(&mut self, before
: Span
, after
: Span
) -> bool
{
420 let mut replacements_occurred
= false;
421 for primary_span
in &mut self.primary_spans
{
422 if *primary_span
== before
{
423 *primary_span
= after
;
424 replacements_occurred
= true;
427 for span_label
in &mut self.span_labels
{
428 if span_label
.0 == before
{
429 span_label
.0 = after
;
430 replacements_occurred
= true;
433 replacements_occurred
436 /// Returns the strings to highlight. We always ensure that there
437 /// is an entry for each of the primary spans -- for each primary
438 /// span `P`, if there is at least one label with span `P`, we return
439 /// those labels (marked as primary). But otherwise we return
440 /// `SpanLabel` instances with empty labels.
441 pub fn span_labels(&self) -> Vec
<SpanLabel
> {
442 let is_primary
= |span
| self.primary_spans
.contains(&span
);
444 let mut span_labels
= self
447 .map(|&(span
, ref label
)| SpanLabel
{
449 is_primary
: is_primary(span
),
450 label
: Some(label
.clone()),
452 .collect
::<Vec
<_
>>();
454 for &span
in &self.primary_spans
{
455 if !span_labels
.iter().any(|sl
| sl
.span
== span
) {
456 span_labels
.push(SpanLabel { span, is_primary: true, label: None }
);
463 /// Returns `true` if any of the span labels is displayable.
464 pub fn has_span_labels(&self) -> bool
{
465 self.span_labels
.iter().any(|(sp
, _
)| !sp
.is_dummy())
469 impl From
<Span
> for MultiSpan
{
470 fn from(span
: Span
) -> MultiSpan
{
471 MultiSpan
::from_span(span
)
475 impl From
<Vec
<Span
>> for MultiSpan
{
476 fn from(spans
: Vec
<Span
>) -> MultiSpan
{
477 MultiSpan
::from_spans(spans
)