about summary refs log tree commit diff
diff options
context:
space:
mode:
authorDavid Wood <david.wood@huawei.com>2022-03-28 09:36:20 +0100
committerDavid Wood <david.wood@huawei.com>2022-04-05 07:01:02 +0100
commitd5119c5b9f1f71090d078e945ea6b5d39d08cffa (patch)
treed63f8049bf2692b7fe2d34bf9e86ec7c26af6fe3
parent7f91697b5035f8620df4de47057024c3539b55a6 (diff)
downloadrust-d5119c5b9f1f71090d078e945ea6b5d39d08cffa.tar.gz
rust-d5119c5b9f1f71090d078e945ea6b5d39d08cffa.zip
errors: implement sysroot/testing bundle loading
Extend loading of Fluent bundles so that bundles can be loaded from the
sysroot based on the language requested by the user, or using a nightly
flag.

Sysroot bundles are loaded from `$sysroot/share/locale/$locale/*.ftl`.

Signed-off-by: David Wood <david.wood@huawei.com>
-rw-r--r--Cargo.lock13
-rw-r--r--compiler/rustc_driver/src/lib.rs4
-rw-r--r--compiler/rustc_error_messages/Cargo.toml3
-rw-r--r--compiler/rustc_error_messages/src/lib.rs167
-rw-r--r--compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs13
-rw-r--r--compiler/rustc_errors/src/emitter.rs9
-rw-r--r--compiler/rustc_errors/src/json.rs10
-rw-r--r--compiler/rustc_errors/src/json/tests.rs4
-rw-r--r--compiler/rustc_errors/src/lib.rs7
-rw-r--r--compiler/rustc_expand/src/tests.rs4
-rw-r--r--compiler/rustc_session/src/config.rs2
-rw-r--r--compiler/rustc_session/src/options.rs20
-rw-r--r--compiler/rustc_session/src/parse.rs9
-rw-r--r--compiler/rustc_session/src/session.rs25
-rw-r--r--src/librustdoc/core.rs5
-rw-r--r--src/librustdoc/doctest.rs5
-rw-r--r--src/librustdoc/passes/check_code_block_syntax.rs3
-rw-r--r--src/test/run-make/translation/Makefile33
-rw-r--r--src/test/run-make/translation/basic-translation.ftl2
-rw-r--r--src/test/run-make/translation/basic-translation.rs18
-rw-r--r--src/tools/clippy/clippy_lints/src/doc.rs4
-rw-r--r--src/tools/clippy/src/driver.rs4
-rw-r--r--src/tools/rustfmt/src/parse/session.rs4
23 files changed, 322 insertions, 46 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 7983b79d5d6..6940406a766 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1239,16 +1239,6 @@ dependencies = [
 ]
 
 [[package]]
-name = "fluent"
-version = "0.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61f69378194459db76abd2ce3952b790db103ceb003008d3d50d97c41ff847a7"
-dependencies = [
- "fluent-bundle",
- "unic-langid",
-]
-
-[[package]]
 name = "fluent-bundle"
 version = "0.15.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3719,7 +3709,8 @@ version = "0.0.0"
 name = "rustc_error_messages"
 version = "0.0.0"
 dependencies = [
- "fluent",
+ "fluent-bundle",
+ "fluent-syntax",
  "rustc_data_structures",
  "rustc_macros",
  "rustc_serialize",
diff --git a/compiler/rustc_driver/src/lib.rs b/compiler/rustc_driver/src/lib.rs
index c95c59904d1..7fa3c1d17b0 100644
--- a/compiler/rustc_driver/src/lib.rs
+++ b/compiler/rustc_driver/src/lib.rs
@@ -1172,10 +1172,12 @@ static DEFAULT_HOOK: SyncLazy<Box<dyn Fn(&panic::PanicInfo<'_>) + Sync + Send +
 /// When `install_ice_hook` is called, this function will be called as the panic
 /// hook.
 pub fn report_ice(info: &panic::PanicInfo<'_>, bug_report_url: &str) {
-    let fallback_bundle = rustc_errors::fallback_fluent_bundle();
+    let fallback_bundle =
+        rustc_errors::fallback_fluent_bundle().expect("failed to load fallback fluent bundle");
     let emitter = Box::new(rustc_errors::emitter::EmitterWriter::stderr(
         rustc_errors::ColorConfig::Auto,
         None,
+        None,
         fallback_bundle,
         false,
         false,
diff --git a/compiler/rustc_error_messages/Cargo.toml b/compiler/rustc_error_messages/Cargo.toml
index abd565cd529..bda44d691e5 100644
--- a/compiler/rustc_error_messages/Cargo.toml
+++ b/compiler/rustc_error_messages/Cargo.toml
@@ -7,7 +7,8 @@ edition = "2021"
 doctest = false
 
 [dependencies]
-fluent = "0.16.0"
+fluent-bundle = "0.15.2"
+fluent-syntax = "0.11"
 rustc_data_structures = { path = "../rustc_data_structures" }
 rustc_serialize = { path = "../rustc_serialize" }
 rustc_span = { path = "../rustc_span" }
diff --git a/compiler/rustc_error_messages/src/lib.rs b/compiler/rustc_error_messages/src/lib.rs
index 105bf1413de..15f0db49b07 100644
--- a/compiler/rustc_error_messages/src/lib.rs
+++ b/compiler/rustc_error_messages/src/lib.rs
@@ -1,24 +1,169 @@
+#![feature(path_try_exists)]
+
+use fluent_bundle::FluentResource;
+use fluent_syntax::parser::ParserError;
 use rustc_data_structures::sync::Lrc;
 use rustc_macros::{Decodable, Encodable};
 use rustc_span::Span;
 use std::borrow::Cow;
-use tracing::debug;
+use std::error::Error;
+use std::fmt;
+use std::fs;
+use std::io;
+use std::path::Path;
+use tracing::{instrument, trace};
 
-pub use fluent::{FluentArgs, FluentValue};
+pub use fluent_bundle::{FluentArgs, FluentError, FluentValue};
+pub use unic_langid::{langid, LanguageIdentifier};
 
 static FALLBACK_FLUENT_RESOURCE: &'static str = include_str!("../locales/en-US/diagnostics.ftl");
 
-pub type FluentBundle = fluent::FluentBundle<fluent::FluentResource>;
+pub type FluentBundle = fluent_bundle::FluentBundle<FluentResource>;
+
+#[derive(Debug)]
+pub enum TranslationBundleError {
+    /// Failed to read from `.ftl` file.
+    ReadFtl(io::Error),
+    /// Failed to parse contents of `.ftl` file.
+    ParseFtl(ParserError),
+    /// Failed to add `FluentResource` to `FluentBundle`.
+    AddResource(FluentError),
+    /// `$sysroot/share/locale/$locale` does not exist.
+    MissingLocale(io::Error),
+    /// Cannot read directory entries of `$sysroot/share/locale/$locale`.
+    ReadLocalesDir(io::Error),
+    /// Cannot read directory entry of `$sysroot/share/locale/$locale`.
+    ReadLocalesDirEntry(io::Error),
+    /// `$sysroot/share/locale/$locale` is not a directory.
+    LocaleIsNotDir,
+}
+
+impl fmt::Display for TranslationBundleError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            TranslationBundleError::ReadFtl(e) => write!(f, "could not read ftl file: {}", e),
+            TranslationBundleError::ParseFtl(e) => {
+                write!(f, "could not parse ftl file: {}", e)
+            }
+            TranslationBundleError::AddResource(e) => write!(f, "failed to add resource: {}", e),
+            TranslationBundleError::MissingLocale(e) => {
+                write!(f, "missing locale directory: {}", e)
+            }
+            TranslationBundleError::ReadLocalesDir(e) => {
+                write!(f, "could not read locales dir: {}", e)
+            }
+            TranslationBundleError::ReadLocalesDirEntry(e) => {
+                write!(f, "could not read locales dir entry: {}", e)
+            }
+            TranslationBundleError::LocaleIsNotDir => {
+                write!(f, "`$sysroot/share/locales/$locale` is not a directory")
+            }
+        }
+    }
+}
+
+impl Error for TranslationBundleError {
+    fn source(&self) -> Option<&(dyn Error + 'static)> {
+        match self {
+            TranslationBundleError::ReadFtl(e) => Some(e),
+            TranslationBundleError::ParseFtl(e) => Some(e),
+            TranslationBundleError::AddResource(e) => Some(e),
+            TranslationBundleError::MissingLocale(e) => Some(e),
+            TranslationBundleError::ReadLocalesDir(e) => Some(e),
+            TranslationBundleError::ReadLocalesDirEntry(e) => Some(e),
+            TranslationBundleError::LocaleIsNotDir => None,
+        }
+    }
+}
+
+impl From<(FluentResource, Vec<ParserError>)> for TranslationBundleError {
+    fn from((_, mut errs): (FluentResource, Vec<ParserError>)) -> Self {
+        TranslationBundleError::ParseFtl(errs.pop().expect("failed ftl parse with no errors"))
+    }
+}
+
+impl From<Vec<FluentError>> for TranslationBundleError {
+    fn from(mut errs: Vec<FluentError>) -> Self {
+        TranslationBundleError::AddResource(
+            errs.pop().expect("failed adding resource to bundle with no errors"),
+        )
+    }
+}
+
+/// Returns Fluent bundle with the user's locale resources from
+/// `$sysroot/share/locale/$requested_locale/*.ftl`.
+///
+/// If `-Z additional-ftl-path` was provided, load that resource and add it  to the bundle
+/// (overriding any conflicting messages).
+#[instrument(level = "trace")]
+pub fn fluent_bundle(
+    sysroot: &Path,
+    requested_locale: Option<LanguageIdentifier>,
+    additional_ftl_path: Option<&Path>,
+) -> Result<Option<Lrc<FluentBundle>>, TranslationBundleError> {
+    if requested_locale.is_none() && additional_ftl_path.is_none() {
+        return Ok(None);
+    }
+
+    // If there is only `-Z additional-ftl-path`, assume locale is "en-US", otherwise use user
+    // provided locale.
+    let locale = requested_locale.clone().unwrap_or_else(|| langid!("en-US"));
+    trace!(?locale);
+    let mut bundle = FluentBundle::new(vec![locale]);
+
+    if let Some(requested_locale) = requested_locale {
+        let mut sysroot = sysroot.to_path_buf();
+        sysroot.push("share");
+        sysroot.push("locale");
+        sysroot.push(requested_locale.to_string());
+        trace!(?sysroot);
+
+        let _ = sysroot.try_exists().map_err(TranslationBundleError::MissingLocale)?;
+
+        if !sysroot.is_dir() {
+            return Err(TranslationBundleError::LocaleIsNotDir);
+        }
+
+        for entry in sysroot.read_dir().map_err(TranslationBundleError::ReadLocalesDir)? {
+            let entry = entry.map_err(TranslationBundleError::ReadLocalesDirEntry)?;
+            let path = entry.path();
+            trace!(?path);
+            if path.extension().and_then(|s| s.to_str()) != Some("ftl") {
+                trace!("skipping");
+                continue;
+            }
+
+            let resource_str = fs::read_to_string(path).map_err(TranslationBundleError::ReadFtl)?;
+            let resource =
+                FluentResource::try_new(resource_str).map_err(TranslationBundleError::from)?;
+            trace!(?resource);
+            bundle.add_resource(resource).map_err(TranslationBundleError::from)?;
+        }
+    }
+
+    if let Some(additional_ftl_path) = additional_ftl_path {
+        let resource_str =
+            fs::read_to_string(additional_ftl_path).map_err(TranslationBundleError::ReadFtl)?;
+        let resource =
+            FluentResource::try_new(resource_str).map_err(TranslationBundleError::from)?;
+        trace!(?resource);
+        bundle.add_resource_overriding(resource);
+    }
+
+    let bundle = Lrc::new(bundle);
+    Ok(Some(bundle))
+}
 
-/// Return the default `FluentBundle` with standard en-US diagnostic messages.
-pub fn fallback_fluent_bundle() -> Lrc<FluentBundle> {
-    let fallback_resource = fluent::FluentResource::try_new(FALLBACK_FLUENT_RESOURCE.to_string())
-        .expect("failed to parse ftl resource");
-    debug!(?fallback_resource);
-    let mut fallback_bundle = FluentBundle::new(vec![unic_langid::langid!("en-US")]);
-    fallback_bundle.add_resource(fallback_resource).expect("failed to add resource to bundle");
+/// Return the default `FluentBundle` with standard "en-US" diagnostic messages.
+#[instrument(level = "trace")]
+pub fn fallback_fluent_bundle() -> Result<Lrc<FluentBundle>, TranslationBundleError> {
+    let fallback_resource = FluentResource::try_new(FALLBACK_FLUENT_RESOURCE.to_string())
+        .map_err(TranslationBundleError::from)?;
+    trace!(?fallback_resource);
+    let mut fallback_bundle = FluentBundle::new(vec![langid!("en-US")]);
+    fallback_bundle.add_resource(fallback_resource).map_err(TranslationBundleError::from)?;
     let fallback_bundle = Lrc::new(fallback_bundle);
-    fallback_bundle
+    Ok(fallback_bundle)
 }
 
 /// Identifier for the Fluent message/attribute corresponding to a diagnostic message.
diff --git a/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs b/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs
index 4dc99b8dd0f..003fd1eea3a 100644
--- a/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs
+++ b/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs
@@ -21,6 +21,7 @@ use rustc_span::SourceFile;
 /// Generates diagnostics using annotate-snippet
 pub struct AnnotateSnippetEmitterWriter {
     source_map: Option<Lrc<SourceMap>>,
+    fluent_bundle: Option<Lrc<FluentBundle>>,
     fallback_bundle: Lrc<FluentBundle>,
 
     /// If true, hides the longer explanation text
@@ -63,7 +64,7 @@ impl Emitter for AnnotateSnippetEmitterWriter {
     }
 
     fn fluent_bundle(&self) -> Option<&Lrc<FluentBundle>> {
-        None
+        self.fluent_bundle.as_ref()
     }
 
     fn fallback_fluent_bundle(&self) -> &Lrc<FluentBundle> {
@@ -99,11 +100,19 @@ fn annotation_type_for_level(level: Level) -> AnnotationType {
 impl AnnotateSnippetEmitterWriter {
     pub fn new(
         source_map: Option<Lrc<SourceMap>>,
+        fluent_bundle: Option<Lrc<FluentBundle>>,
         fallback_bundle: Lrc<FluentBundle>,
         short_message: bool,
         macro_backtrace: bool,
     ) -> Self {
-        Self { source_map, fallback_bundle, short_message, ui_testing: false, macro_backtrace }
+        Self {
+            source_map,
+            fluent_bundle,
+            fallback_bundle,
+            short_message,
+            ui_testing: false,
+            macro_backtrace,
+        }
     }
 
     /// Allows to modify `Self` to enable or disable the `ui_testing` flag.
diff --git a/compiler/rustc_errors/src/emitter.rs b/compiler/rustc_errors/src/emitter.rs
index 43bcaa646f3..c8281bd37d1 100644
--- a/compiler/rustc_errors/src/emitter.rs
+++ b/compiler/rustc_errors/src/emitter.rs
@@ -59,6 +59,7 @@ impl HumanReadableErrorType {
         self,
         dst: Box<dyn Write + Send>,
         source_map: Option<Lrc<SourceMap>>,
+        bundle: Option<Lrc<FluentBundle>>,
         fallback_bundle: Lrc<FluentBundle>,
         teach: bool,
         terminal_width: Option<usize>,
@@ -69,6 +70,7 @@ impl HumanReadableErrorType {
         EmitterWriter::new(
             dst,
             source_map,
+            bundle,
             fallback_bundle,
             short,
             teach,
@@ -568,7 +570,7 @@ impl Emitter for EmitterWriter {
     }
 
     fn fluent_bundle(&self) -> Option<&Lrc<FluentBundle>> {
-        None
+        self.fluent_bundle.as_ref()
     }
 
     fn fallback_fluent_bundle(&self) -> &Lrc<FluentBundle> {
@@ -686,6 +688,7 @@ impl ColorConfig {
 pub struct EmitterWriter {
     dst: Destination,
     sm: Option<Lrc<SourceMap>>,
+    fluent_bundle: Option<Lrc<FluentBundle>>,
     fallback_bundle: Lrc<FluentBundle>,
     short_message: bool,
     teach: bool,
@@ -706,6 +709,7 @@ impl EmitterWriter {
     pub fn stderr(
         color_config: ColorConfig,
         source_map: Option<Lrc<SourceMap>>,
+        fluent_bundle: Option<Lrc<FluentBundle>>,
         fallback_bundle: Lrc<FluentBundle>,
         short_message: bool,
         teach: bool,
@@ -716,6 +720,7 @@ impl EmitterWriter {
         EmitterWriter {
             dst,
             sm: source_map,
+            fluent_bundle,
             fallback_bundle,
             short_message,
             teach,
@@ -728,6 +733,7 @@ impl EmitterWriter {
     pub fn new(
         dst: Box<dyn Write + Send>,
         source_map: Option<Lrc<SourceMap>>,
+        fluent_bundle: Option<Lrc<FluentBundle>>,
         fallback_bundle: Lrc<FluentBundle>,
         short_message: bool,
         teach: bool,
@@ -738,6 +744,7 @@ impl EmitterWriter {
         EmitterWriter {
             dst: Raw(dst, colored),
             sm: source_map,
+            fluent_bundle,
             fallback_bundle,
             short_message,
             teach,
diff --git a/compiler/rustc_errors/src/json.rs b/compiler/rustc_errors/src/json.rs
index 3e7bcf2d37e..f78490da245 100644
--- a/compiler/rustc_errors/src/json.rs
+++ b/compiler/rustc_errors/src/json.rs
@@ -37,6 +37,7 @@ pub struct JsonEmitter {
     dst: Box<dyn Write + Send>,
     registry: Option<Registry>,
     sm: Lrc<SourceMap>,
+    fluent_bundle: Option<Lrc<FluentBundle>>,
     fallback_bundle: Lrc<FluentBundle>,
     pretty: bool,
     ui_testing: bool,
@@ -49,6 +50,7 @@ impl JsonEmitter {
     pub fn stderr(
         registry: Option<Registry>,
         source_map: Lrc<SourceMap>,
+        fluent_bundle: Option<Lrc<FluentBundle>>,
         fallback_bundle: Lrc<FluentBundle>,
         pretty: bool,
         json_rendered: HumanReadableErrorType,
@@ -59,6 +61,7 @@ impl JsonEmitter {
             dst: Box::new(io::BufWriter::new(io::stderr())),
             registry,
             sm: source_map,
+            fluent_bundle,
             fallback_bundle,
             pretty,
             ui_testing: false,
@@ -71,6 +74,7 @@ impl JsonEmitter {
     pub fn basic(
         pretty: bool,
         json_rendered: HumanReadableErrorType,
+        fluent_bundle: Option<Lrc<FluentBundle>>,
         fallback_bundle: Lrc<FluentBundle>,
         terminal_width: Option<usize>,
         macro_backtrace: bool,
@@ -79,6 +83,7 @@ impl JsonEmitter {
         JsonEmitter::stderr(
             None,
             Lrc::new(SourceMap::new(file_path_mapping)),
+            fluent_bundle,
             fallback_bundle,
             pretty,
             json_rendered,
@@ -91,6 +96,7 @@ impl JsonEmitter {
         dst: Box<dyn Write + Send>,
         registry: Option<Registry>,
         source_map: Lrc<SourceMap>,
+        fluent_bundle: Option<Lrc<FluentBundle>>,
         fallback_bundle: Lrc<FluentBundle>,
         pretty: bool,
         json_rendered: HumanReadableErrorType,
@@ -101,6 +107,7 @@ impl JsonEmitter {
             dst,
             registry,
             sm: source_map,
+            fluent_bundle,
             fallback_bundle,
             pretty,
             ui_testing: false,
@@ -182,7 +189,7 @@ impl Emitter for JsonEmitter {
     }
 
     fn fluent_bundle(&self) -> Option<&Lrc<FluentBundle>> {
-        None
+        self.fluent_bundle.as_ref()
     }
 
     fn fallback_fluent_bundle(&self) -> &Lrc<FluentBundle> {
@@ -395,6 +402,7 @@ impl Diagnostic {
             .new_emitter(
                 Box::new(buf),
                 Some(je.sm.clone()),
+                je.fluent_bundle.clone(),
                 je.fallback_bundle.clone(),
                 false,
                 je.terminal_width,
diff --git a/compiler/rustc_errors/src/json/tests.rs b/compiler/rustc_errors/src/json/tests.rs
index fa0ccd65d06..4174a85204f 100644
--- a/compiler/rustc_errors/src/json/tests.rs
+++ b/compiler/rustc_errors/src/json/tests.rs
@@ -39,13 +39,15 @@ fn test_positions(code: &str, span: (u32, u32), expected_output: SpanTestData) {
     rustc_span::create_default_session_globals_then(|| {
         let sm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
         sm.new_source_file(Path::new("test.rs").to_owned().into(), code.to_owned());
-        let fallback_bundle = crate::fallback_fluent_bundle();
+        let fallback_bundle =
+            crate::fallback_fluent_bundle().expect("failed to load fallback fluent bundle");
 
         let output = Arc::new(Mutex::new(Vec::new()));
         let je = JsonEmitter::new(
             Box::new(Shared { data: output.clone() }),
             None,
             sm,
+            None,
             fallback_bundle,
             true,
             HumanReadableErrorType::Short(ColorConfig::Never),
diff --git a/compiler/rustc_errors/src/lib.rs b/compiler/rustc_errors/src/lib.rs
index 04a0a9f7b73..5db6614c141 100644
--- a/compiler/rustc_errors/src/lib.rs
+++ b/compiler/rustc_errors/src/lib.rs
@@ -32,7 +32,8 @@ use rustc_data_structures::stable_hasher::StableHasher;
 use rustc_data_structures::sync::{self, Lock, Lrc};
 use rustc_data_structures::AtomicRef;
 pub use rustc_error_messages::{
-    fallback_fluent_bundle, DiagnosticMessage, FluentBundle, MultiSpan, SpanLabel,
+    fallback_fluent_bundle, fluent_bundle, DiagnosticMessage, FluentBundle, LanguageIdentifier,
+    MultiSpan, SpanLabel,
 };
 pub use rustc_lint_defs::{pluralize, Applicability};
 use rustc_serialize::json::Json;
@@ -544,11 +545,13 @@ impl Handler {
         can_emit_warnings: bool,
         treat_err_as_bug: Option<NonZeroUsize>,
         sm: Option<Lrc<SourceMap>>,
+        fluent_bundle: Option<Lrc<FluentBundle>>,
         fallback_bundle: Lrc<FluentBundle>,
     ) -> Self {
         Self::with_tty_emitter_and_flags(
             color_config,
             sm,
+            fluent_bundle,
             fallback_bundle,
             HandlerFlags { can_emit_warnings, treat_err_as_bug, ..Default::default() },
         )
@@ -557,12 +560,14 @@ impl Handler {
     pub fn with_tty_emitter_and_flags(
         color_config: ColorConfig,
         sm: Option<Lrc<SourceMap>>,
+        fluent_bundle: Option<Lrc<FluentBundle>>,
         fallback_bundle: Lrc<FluentBundle>,
         flags: HandlerFlags,
     ) -> Self {
         let emitter = Box::new(EmitterWriter::stderr(
             color_config,
             sm,
+            fluent_bundle,
             fallback_bundle,
             false,
             false,
diff --git a/compiler/rustc_expand/src/tests.rs b/compiler/rustc_expand/src/tests.rs
index 65128b91770..406095ab5e0 100644
--- a/compiler/rustc_expand/src/tests.rs
+++ b/compiler/rustc_expand/src/tests.rs
@@ -127,7 +127,8 @@ fn test_harness(file_text: &str, span_labels: Vec<SpanLabel>, expected_output: &
     create_default_session_if_not_set_then(|_| {
         let output = Arc::new(Mutex::new(Vec::new()));
 
-        let fallback_bundle = rustc_errors::fallback_fluent_bundle();
+        let fallback_bundle =
+            rustc_errors::fallback_fluent_bundle().expect("failed to load fallback fluent bundle");
         let source_map = Lrc::new(SourceMap::new(FilePathMapping::empty()));
         source_map.new_source_file(Path::new("test.rs").to_owned().into(), file_text.to_owned());
 
@@ -143,6 +144,7 @@ fn test_harness(file_text: &str, span_labels: Vec<SpanLabel>, expected_output: &
         let emitter = EmitterWriter::new(
             Box::new(Shared { data: output.clone() }),
             Some(source_map.clone()),
+            None,
             fallback_bundle,
             false,
             false,
diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs
index 4182a5d0711..5a447aa6237 100644
--- a/compiler/rustc_session/src/config.rs
+++ b/compiler/rustc_session/src/config.rs
@@ -2856,6 +2856,7 @@ crate mod dep_tracking {
     use crate::lint;
     use crate::options::WasiExecModel;
     use crate::utils::{NativeLib, NativeLibKind};
+    use rustc_errors::LanguageIdentifier;
     use rustc_feature::UnstableFeatures;
     use rustc_span::edition::Edition;
     use rustc_span::RealFileName;
@@ -2948,6 +2949,7 @@ crate mod dep_tracking {
         LocationDetail,
         BranchProtection,
         OomStrategy,
+        LanguageIdentifier,
     );
 
     impl<T1, T2> DepTrackingHash for (T1, T2)
diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs
index 463c5c9d8b5..7303d701ae1 100644
--- a/compiler/rustc_session/src/options.rs
+++ b/compiler/rustc_session/src/options.rs
@@ -4,6 +4,7 @@ use crate::early_error;
 use crate::lint;
 use crate::search_paths::SearchPath;
 use crate::utils::NativeLib;
+use rustc_errors::LanguageIdentifier;
 use rustc_target::spec::{CodeModel, LinkerFlavor, MergeFunctions, PanicStrategy, SanitizerSet};
 use rustc_target::spec::{
     RelocModel, RelroLevel, SplitDebuginfo, StackProtector, TargetTriple, TlsModel,
@@ -365,6 +366,7 @@ mod desc {
     pub const parse_string: &str = "a string";
     pub const parse_opt_string: &str = parse_string;
     pub const parse_string_push: &str = parse_string;
+    pub const parse_opt_langid: &str = "a language identifier";
     pub const parse_opt_pathbuf: &str = "a path";
     pub const parse_list: &str = "a space-separated list of strings";
     pub const parse_opt_comma_list: &str = "a comma-separated list of strings";
@@ -487,6 +489,17 @@ mod parse {
         }
     }
 
+    /// Parse an optional language identifier, e.g. `en-US` or `zh-CN`.
+    crate fn parse_opt_langid(slot: &mut Option<LanguageIdentifier>, v: Option<&str>) -> bool {
+        match v {
+            Some(s) => {
+                *slot = rustc_errors::LanguageIdentifier::from_str(s).ok();
+                true
+            }
+            None => false,
+        }
+    }
+
     crate fn parse_opt_pathbuf(slot: &mut Option<PathBuf>, v: Option<&str>) -> bool {
         match v {
             Some(s) => {
@@ -1462,6 +1475,13 @@ options! {
         "the directory the intermediate files are written to"),
     terminal_width: Option<usize> = (None, parse_opt_number, [UNTRACKED],
         "set the current terminal width"),
+    // Diagnostics are considered side-effects of a query (see `QuerySideEffects`) and are saved
+    // alongside query results and changes to translation options can affect diagnostics - so
+    // translation options should be tracked.
+    translate_lang: Option<LanguageIdentifier> = (None, parse_opt_langid, [TRACKED],
+        "language identifier for diagnostic output"),
+    translate_additional_ftl: Option<PathBuf> = (None, parse_opt_pathbuf, [TRACKED],
+        "additional fluent translation to preferentially use (for testing translation)"),
     tune_cpu: Option<String> = (None, parse_opt_string, [TRACKED],
         "select processor to schedule for (`rustc --print target-cpus` for details)"),
     thinlto: Option<bool> = (None, parse_opt_bool, [TRACKED],
diff --git a/compiler/rustc_session/src/parse.rs b/compiler/rustc_session/src/parse.rs
index 49c1a1be201..6b99e011c45 100644
--- a/compiler/rustc_session/src/parse.rs
+++ b/compiler/rustc_session/src/parse.rs
@@ -174,13 +174,15 @@ pub struct ParseSess {
 impl ParseSess {
     /// Used for testing.
     pub fn new(file_path_mapping: FilePathMapping) -> Self {
-        let fallback_bundle = fallback_fluent_bundle();
+        let fallback_bundle =
+            fallback_fluent_bundle().expect("failed to load fallback fluent bundle");
         let sm = Lrc::new(SourceMap::new(file_path_mapping));
         let handler = Handler::with_tty_emitter(
             ColorConfig::Auto,
             true,
             None,
             Some(sm.clone()),
+            None,
             fallback_bundle,
         );
         ParseSess::with_span_handler(handler, sm)
@@ -211,10 +213,11 @@ impl ParseSess {
     }
 
     pub fn with_silent_emitter(fatal_note: Option<String>) -> Self {
-        let fallback_bundle = fallback_fluent_bundle();
+        let fallback_bundle =
+            fallback_fluent_bundle().expect("failed to load fallback fluent bundle");
         let sm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
         let fatal_handler =
-            Handler::with_tty_emitter(ColorConfig::Auto, false, None, None, fallback_bundle);
+            Handler::with_tty_emitter(ColorConfig::Auto, false, None, None, None, fallback_bundle);
         let handler = Handler::with_emitter(
             false,
             None,
diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs
index 9a11f537c25..76a2c579990 100644
--- a/compiler/rustc_session/src/session.rs
+++ b/compiler/rustc_session/src/session.rs
@@ -20,8 +20,8 @@ use rustc_errors::emitter::{Emitter, EmitterWriter, HumanReadableErrorType};
 use rustc_errors::json::JsonEmitter;
 use rustc_errors::registry::Registry;
 use rustc_errors::{
-    fallback_fluent_bundle, DiagnosticBuilder, DiagnosticId, DiagnosticMessage, ErrorGuaranteed,
-    FluentBundle, MultiSpan,
+    fallback_fluent_bundle, fluent_bundle, DiagnosticBuilder, DiagnosticId, DiagnosticMessage,
+    ErrorGuaranteed, FluentBundle, MultiSpan,
 };
 use rustc_macros::HashStable_Generic;
 pub use rustc_span::def_id::StableCrateId;
@@ -1069,6 +1069,7 @@ fn default_emitter(
     sopts: &config::Options,
     registry: rustc_errors::registry::Registry,
     source_map: Lrc<SourceMap>,
+    bundle: Option<Lrc<FluentBundle>>,
     fallback_bundle: Lrc<FluentBundle>,
     emitter_dest: Option<Box<dyn Write + Send>>,
 ) -> Box<dyn Emitter + sync::Send> {
@@ -1080,6 +1081,7 @@ fn default_emitter(
             if let HumanReadableErrorType::AnnotateSnippet(_) = kind {
                 let emitter = AnnotateSnippetEmitterWriter::new(
                     Some(source_map),
+                    bundle,
                     fallback_bundle,
                     short,
                     macro_backtrace,
@@ -1090,6 +1092,7 @@ fn default_emitter(
                     None => EmitterWriter::stderr(
                         color_config,
                         Some(source_map),
+                        bundle,
                         fallback_bundle,
                         short,
                         sopts.debugging_opts.teach,
@@ -1099,6 +1102,7 @@ fn default_emitter(
                     Some(dst) => EmitterWriter::new(
                         dst,
                         Some(source_map),
+                        bundle,
                         fallback_bundle,
                         short,
                         false, // no teach messages when writing to a buffer
@@ -1114,6 +1118,7 @@ fn default_emitter(
             JsonEmitter::stderr(
                 Some(registry),
                 source_map,
+                bundle,
                 fallback_bundle,
                 pretty,
                 json_rendered,
@@ -1127,6 +1132,7 @@ fn default_emitter(
                 dst,
                 Some(registry),
                 source_map,
+                bundle,
                 fallback_bundle,
                 pretty,
                 json_rendered,
@@ -1198,9 +1204,15 @@ pub fn build_session(
         hash_kind,
     ));
 
-    let fallback_bundle = fallback_fluent_bundle();
+    let bundle = fluent_bundle(
+        &sysroot,
+        sopts.debugging_opts.translate_lang.clone(),
+        sopts.debugging_opts.translate_additional_ftl.as_deref(),
+    )
+    .expect("failed to load fluent bundle");
+    let fallback_bundle = fallback_fluent_bundle().expect("failed to load fallback fluent bundle");
     let emitter =
-        default_emitter(&sopts, registry, source_map.clone(), fallback_bundle.clone(), write_dest);
+        default_emitter(&sopts, registry, source_map.clone(), bundle, fallback_bundle, write_dest);
 
     let span_diagnostic = rustc_errors::Handler::with_emitter_and_flags(
         emitter,
@@ -1433,13 +1445,14 @@ pub enum IncrCompSession {
 }
 
 fn early_error_handler(output: config::ErrorOutputType) -> rustc_errors::Handler {
-    let fallback_bundle = fallback_fluent_bundle();
+    let fallback_bundle = fallback_fluent_bundle().expect("failed to load fallback fluent bundle");
     let emitter: Box<dyn Emitter + sync::Send> = match output {
         config::ErrorOutputType::HumanReadable(kind) => {
             let (short, color_config) = kind.unzip();
             Box::new(EmitterWriter::stderr(
                 color_config,
                 None,
+                None,
                 fallback_bundle,
                 short,
                 false,
@@ -1448,7 +1461,7 @@ fn early_error_handler(output: config::ErrorOutputType) -> rustc_errors::Handler
             ))
         }
         config::ErrorOutputType::Json { pretty, json_rendered } => {
-            Box::new(JsonEmitter::basic(pretty, json_rendered, fallback_bundle, None, false))
+            Box::new(JsonEmitter::basic(pretty, json_rendered, None, fallback_bundle, None, false))
         }
     };
     rustc_errors::Handler::with_emitter(true, None, emitter)
diff --git a/src/librustdoc/core.rs b/src/librustdoc/core.rs
index 44b6b4b62ce..7cd799d84f0 100644
--- a/src/librustdoc/core.rs
+++ b/src/librustdoc/core.rs
@@ -143,7 +143,8 @@ crate fn new_handler(
     source_map: Option<Lrc<source_map::SourceMap>>,
     debugging_opts: &DebuggingOptions,
 ) -> rustc_errors::Handler {
-    let fallback_bundle = rustc_errors::fallback_fluent_bundle();
+    let fallback_bundle =
+        rustc_errors::fallback_fluent_bundle().expect("failed to load fallback fluent bundle");
     let emitter: Box<dyn Emitter + sync::Send> = match error_format {
         ErrorOutputType::HumanReadable(kind) => {
             let (short, color_config) = kind.unzip();
@@ -151,6 +152,7 @@ crate fn new_handler(
                 EmitterWriter::stderr(
                     color_config,
                     source_map.map(|sm| sm as _),
+                    None,
                     fallback_bundle,
                     short,
                     debugging_opts.teach,
@@ -168,6 +170,7 @@ crate fn new_handler(
                 JsonEmitter::stderr(
                     None,
                     source_map,
+                    None,
                     fallback_bundle,
                     pretty,
                     json_rendered,
diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs
index fc079619786..94e3c996400 100644
--- a/src/librustdoc/doctest.rs
+++ b/src/librustdoc/doctest.rs
@@ -537,10 +537,12 @@ crate fn make_test(
             // Any errors in parsing should also appear when the doctest is compiled for real, so just
             // send all the errors that librustc_ast emits directly into a `Sink` instead of stderr.
             let sm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
-            let fallback_bundle = rustc_errors::fallback_fluent_bundle();
+            let fallback_bundle = rustc_errors::fallback_fluent_bundle()
+                .expect("failed to load fallback fluent bundle");
             supports_color = EmitterWriter::stderr(
                 ColorConfig::Auto,
                 None,
+                None,
                 fallback_bundle.clone(),
                 false,
                 false,
@@ -552,6 +554,7 @@ crate fn make_test(
             let emitter = EmitterWriter::new(
                 box io::sink(),
                 None,
+                None,
                 fallback_bundle,
                 false,
                 false,
diff --git a/src/librustdoc/passes/check_code_block_syntax.rs b/src/librustdoc/passes/check_code_block_syntax.rs
index 10f1fc05404..01bfc522e1b 100644
--- a/src/librustdoc/passes/check_code_block_syntax.rs
+++ b/src/librustdoc/passes/check_code_block_syntax.rs
@@ -32,7 +32,8 @@ struct SyntaxChecker<'a, 'tcx> {
 impl<'a, 'tcx> SyntaxChecker<'a, 'tcx> {
     fn check_rust_syntax(&self, item: &clean::Item, dox: &str, code_block: RustCodeBlock) {
         let buffer = Lrc::new(Lock::new(Buffer::default()));
-        let fallback_bundle = rustc_errors::fallback_fluent_bundle();
+        let fallback_bundle =
+            rustc_errors::fallback_fluent_bundle().expect("failed to load fallback fluent bundle");
         let emitter = BufferEmitter { buffer: Lrc::clone(&buffer), fallback_bundle };
 
         let sm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
diff --git a/src/test/run-make/translation/Makefile b/src/test/run-make/translation/Makefile
new file mode 100644
index 00000000000..22a3bf57ecf
--- /dev/null
+++ b/src/test/run-make/translation/Makefile
@@ -0,0 +1,33 @@
+include ../../run-make-fulldeps/tools.mk
+
+# This test uses `ln -s` rather than copying to save testing time, but its
+# usage doesn't work on Windows.
+# ignore-windows
+
+SYSROOT:=$(shell $(RUSTC) --print sysroot)
+FAKEROOT=$(TMPDIR)/fakeroot
+
+all: normal custom sysroot
+
+normal: basic-translation.rs
+	$(RUSTC) $< 2>&1 | grep "struct literal body without path"
+
+custom: basic-translation.rs basic-translation.ftl
+	$(RUSTC) $< -Ztranslate-additional-ftl=$(CURDIR)/basic-translation.ftl 2>&1 | grep "this is a test message"
+
+# Make a local copy of the sysroot and add the custom locale to it.
+sysroot: basic-translation.rs basic-translation.ftl
+	mkdir $(FAKEROOT)
+	ln -s $(SYSROOT)/* $(FAKEROOT)
+	rm -f $(FAKEROOT)/lib
+	mkdir $(FAKEROOT)/lib
+	ln -s $(SYSROOT)/lib/* $(FAKEROOT)/lib
+	rm -f $(FAKEROOT)/lib/rustlib
+	mkdir $(FAKEROOT)/lib/rustlib
+	ln -s $(SYSROOT)/lib/rustlib/* $(FAKEROOT)/lib/rustlib
+	rm -f $(FAKEROOT)/lib/rustlib/src
+	mkdir $(FAKEROOT)/lib/rustlib/src
+	ln -s $(SYSROOT)/lib/rustlib/src/* $(FAKEROOT)/lib/rustlib/src
+	mkdir -p $(FAKEROOT)/share/locale/zh-CN/
+	ln -s $(CURDIR)/basic-translation.ftl $(FAKEROOT)/share/locale/zh-CN/basic-translation.ftl
+	$(RUSTC) $< --sysroot $(FAKEROOT) -Ztranslate-lang=zh-CN 2>&1 | grep "this is a test message"
diff --git a/src/test/run-make/translation/basic-translation.ftl b/src/test/run-make/translation/basic-translation.ftl
new file mode 100644
index 00000000000..4681b879cda
--- /dev/null
+++ b/src/test/run-make/translation/basic-translation.ftl
@@ -0,0 +1,2 @@
+parser-struct-literal-body-without-path = this is a test message
+    .suggestion = this is a test suggestion
diff --git a/src/test/run-make/translation/basic-translation.rs b/src/test/run-make/translation/basic-translation.rs
new file mode 100644
index 00000000000..b8f5bff3153
--- /dev/null
+++ b/src/test/run-make/translation/basic-translation.rs
@@ -0,0 +1,18 @@
+// Exact error being tested isn't relevant, it just needs to be known that it uses Fluent-backed
+// diagnostics.
+
+struct Foo {
+    val: (),
+}
+
+fn foo() -> Foo {
+    val: (),
+}
+
+fn main() {
+    let x = foo();
+    x.val == 42;
+    let x = {
+        val: (),
+    };
+}
diff --git a/src/tools/clippy/clippy_lints/src/doc.rs b/src/tools/clippy/clippy_lints/src/doc.rs
index b836363b31b..e08e9e49993 100644
--- a/src/tools/clippy/clippy_lints/src/doc.rs
+++ b/src/tools/clippy/clippy_lints/src/doc.rs
@@ -621,10 +621,12 @@ fn check_code(cx: &LateContext<'_>, text: &str, edition: Edition, span: Span) {
                 let filename = FileName::anon_source_code(&code);
 
                 let sm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
-                let fallback_bundle = rustc_errors::fallback_fluent_bundle();
+                let fallback_bundle = rustc_errors::fallback_fluent_bundle()
+                    .expect("failed to load fallback fluent bundle");
                 let emitter = EmitterWriter::new(
                     Box::new(io::sink()),
                     None,
+                    None,
                     fallback_bundle,
                     false,
                     false,
diff --git a/src/tools/clippy/src/driver.rs b/src/tools/clippy/src/driver.rs
index bfce787af5e..f04535b2bea 100644
--- a/src/tools/clippy/src/driver.rs
+++ b/src/tools/clippy/src/driver.rs
@@ -165,10 +165,12 @@ fn report_clippy_ice(info: &panic::PanicInfo<'_>, bug_report_url: &str) {
     // Separate the output with an empty line
     eprintln!();
 
-    let fallback_bundle = rustc_errors::fallback_fluent_bundle();
+    let fallback_bundle = rustc_errors::fallback_fluent_bundle()
+        .expect("failed to load fallback fluent bundle");
     let emitter = Box::new(rustc_errors::emitter::EmitterWriter::stderr(
         rustc_errors::ColorConfig::Auto,
         None,
+        None,
         fallback_bundle,
         false,
         false,
diff --git a/src/tools/rustfmt/src/parse/session.rs b/src/tools/rustfmt/src/parse/session.rs
index 4563dbf6c16..6967028d55f 100644
--- a/src/tools/rustfmt/src/parse/session.rs
+++ b/src/tools/rustfmt/src/parse/session.rs
@@ -114,10 +114,12 @@ fn default_handler(
     let emitter = if hide_parse_errors {
         silent_emitter()
     } else {
-        let fallback_bundle = rustc_errors::fallback_fluent_bundle();
+        let fallback_bundle = rustc_errors::fallback_fluent_bundle()
+            .expect("failed to load fallback fluent bundle");
         Box::new(EmitterWriter::stderr(
             color_cfg,
             Some(source_map.clone()),
+            None,
             fallback_bundle,
             false,
             false,