From 510ba031dcf119e81834babf83e671b2cc00416a Mon Sep 17 00:00:00 2001 From: David Wood Date: Wed, 10 Aug 2022 17:30:47 +0100 Subject: [PATCH 1/3] errors: move translation logic into module Just moving code around so that triagebot can ping relevant parties when translation logic is modified. Signed-off-by: David Wood --- compiler/rustc_codegen_ssa/src/back/write.rs | 20 +-- .../src/annotate_snippet_emitter_writer.rs | 19 +-- compiler/rustc_errors/src/emitter.rs | 128 +++--------------- compiler/rustc_errors/src/json.rs | 19 +-- compiler/rustc_errors/src/lib.rs | 1 + compiler/rustc_errors/src/translation.rs | 103 ++++++++++++++ .../passes/check_code_block_syntax.rs | 21 +-- src/tools/rustfmt/src/parse/session.rs | 52 ++++--- 8 files changed, 200 insertions(+), 163 deletions(-) create mode 100644 compiler/rustc_errors/src/translation.rs diff --git a/compiler/rustc_codegen_ssa/src/back/write.rs b/compiler/rustc_codegen_ssa/src/back/write.rs index dbd55590e5c08..2930d09d71f1a 100644 --- a/compiler/rustc_codegen_ssa/src/back/write.rs +++ b/compiler/rustc_codegen_ssa/src/back/write.rs @@ -15,7 +15,7 @@ use rustc_data_structures::profiling::TimingGuard; use rustc_data_structures::profiling::VerboseTimingGuard; use rustc_data_structures::sync::Lrc; use rustc_errors::emitter::Emitter; -use rustc_errors::{DiagnosticId, FatalError, Handler, Level}; +use rustc_errors::{translation::Translate, DiagnosticId, FatalError, Handler, Level}; use rustc_fs_util::link_or_copy; use rustc_hir::def_id::{CrateNum, LOCAL_CRATE}; use rustc_incremental::{ @@ -1740,6 +1740,16 @@ impl SharedEmitter { } } +impl Translate for SharedEmitter { + fn fluent_bundle(&self) -> Option<&Lrc> { + None + } + + fn fallback_fluent_bundle(&self) -> &rustc_errors::FluentBundle { + panic!("shared emitter attempted to translate a diagnostic"); + } +} + impl Emitter for SharedEmitter { fn emit_diagnostic(&mut self, diag: &rustc_errors::Diagnostic) { let fluent_args = self.to_fluent_args(diag.args()); @@ -1761,14 +1771,6 @@ impl Emitter for SharedEmitter { fn source_map(&self) -> Option<&Lrc> { None } - - fn fluent_bundle(&self) -> Option<&Lrc> { - None - } - - fn fallback_fluent_bundle(&self) -> &rustc_errors::FluentBundle { - panic!("shared emitter attempted to translate a diagnostic"); - } } impl SharedEmitterMain { diff --git a/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs b/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs index 3df562c7edac7..b32fc3c719bbd 100644 --- a/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs +++ b/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs @@ -7,6 +7,7 @@ use crate::emitter::FileWithAnnotatedLines; use crate::snippet::Line; +use crate::translation::Translate; use crate::{ CodeSuggestion, Diagnostic, DiagnosticId, DiagnosticMessage, Emitter, FluentBundle, LazyFallbackBundle, Level, MultiSpan, Style, SubDiagnostic, @@ -32,6 +33,16 @@ pub struct AnnotateSnippetEmitterWriter { macro_backtrace: bool, } +impl Translate for AnnotateSnippetEmitterWriter { + fn fluent_bundle(&self) -> Option<&Lrc> { + self.fluent_bundle.as_ref() + } + + fn fallback_fluent_bundle(&self) -> &FluentBundle { + &**self.fallback_bundle + } +} + impl Emitter for AnnotateSnippetEmitterWriter { /// The entry point for the diagnostics generation fn emit_diagnostic(&mut self, diag: &Diagnostic) { @@ -63,14 +74,6 @@ impl Emitter for AnnotateSnippetEmitterWriter { self.source_map.as_ref() } - fn fluent_bundle(&self) -> Option<&Lrc> { - self.fluent_bundle.as_ref() - } - - fn fallback_fluent_bundle(&self) -> &FluentBundle { - &**self.fallback_bundle - } - fn should_show_explain(&self) -> bool { !self.short_message } diff --git a/compiler/rustc_errors/src/emitter.rs b/compiler/rustc_errors/src/emitter.rs index 753e2f07c042e..6c1bfcb9919eb 100644 --- a/compiler/rustc_errors/src/emitter.rs +++ b/compiler/rustc_errors/src/emitter.rs @@ -14,10 +14,10 @@ use rustc_span::{FileLines, SourceFile, Span}; use crate::snippet::{Annotation, AnnotationType, Line, MultilineAnnotation, Style, StyledString}; use crate::styled_buffer::StyledBuffer; +use crate::translation::Translate; use crate::{ - CodeSuggestion, Diagnostic, DiagnosticArg, DiagnosticId, DiagnosticMessage, FluentBundle, - Handler, LazyFallbackBundle, Level, MultiSpan, SubDiagnostic, SubstitutionHighlight, - SuggestionStyle, + CodeSuggestion, Diagnostic, DiagnosticId, DiagnosticMessage, FluentBundle, Handler, + LazyFallbackBundle, Level, MultiSpan, SubDiagnostic, SubstitutionHighlight, SuggestionStyle, }; use rustc_lint_defs::pluralize; @@ -200,7 +200,7 @@ impl Margin { const ANONYMIZED_LINE_NUM: &str = "LL"; /// Emitter trait for emitting errors. -pub trait Emitter { +pub trait Emitter: Translate { /// Emit a structured diagnostic. fn emit_diagnostic(&mut self, diag: &Diagnostic); @@ -231,102 +231,6 @@ pub trait Emitter { fn source_map(&self) -> Option<&Lrc>; - /// Return `FluentBundle` with localized diagnostics for the locale requested by the user. If no - /// language was requested by the user then this will be `None` and `fallback_fluent_bundle` - /// should be used. - fn fluent_bundle(&self) -> Option<&Lrc>; - - /// Return `FluentBundle` with localized diagnostics for the default locale of the compiler. - /// Used when the user has not requested a specific language or when a localized diagnostic is - /// unavailable for the requested locale. - fn fallback_fluent_bundle(&self) -> &FluentBundle; - - /// Convert diagnostic arguments (a rustc internal type that exists to implement - /// `Encodable`/`Decodable`) into `FluentArgs` which is necessary to perform translation. - /// - /// Typically performed once for each diagnostic at the start of `emit_diagnostic` and then - /// passed around as a reference thereafter. - fn to_fluent_args<'arg>(&self, args: &[DiagnosticArg<'arg>]) -> FluentArgs<'arg> { - FromIterator::from_iter(args.to_vec().drain(..)) - } - - /// Convert `DiagnosticMessage`s to a string, performing translation if necessary. - fn translate_messages( - &self, - messages: &[(DiagnosticMessage, Style)], - args: &FluentArgs<'_>, - ) -> Cow<'_, str> { - Cow::Owned( - messages.iter().map(|(m, _)| self.translate_message(m, args)).collect::(), - ) - } - - /// Convert a `DiagnosticMessage` to a string, performing translation if necessary. - fn translate_message<'a>( - &'a self, - message: &'a DiagnosticMessage, - args: &'a FluentArgs<'_>, - ) -> Cow<'_, str> { - trace!(?message, ?args); - let (identifier, attr) = match message { - DiagnosticMessage::Str(msg) => return Cow::Borrowed(&msg), - DiagnosticMessage::FluentIdentifier(identifier, attr) => (identifier, attr), - }; - - let translate_with_bundle = |bundle: &'a FluentBundle| -> Option<(Cow<'_, str>, Vec<_>)> { - let message = bundle.get_message(&identifier)?; - let value = match attr { - Some(attr) => message.get_attribute(attr)?.value(), - None => message.value()?, - }; - debug!(?message, ?value); - - let mut errs = vec![]; - let translated = bundle.format_pattern(value, Some(&args), &mut errs); - debug!(?translated, ?errs); - Some((translated, errs)) - }; - - self.fluent_bundle() - .and_then(|bundle| translate_with_bundle(bundle)) - // If `translate_with_bundle` returns `None` with the primary bundle, this is likely - // just that the primary bundle doesn't contain the message being translated, so - // proceed to the fallback bundle. - // - // However, when errors are produced from translation, then that means the translation - // is broken (e.g. `{$foo}` exists in a translation but `foo` isn't provided). - // - // In debug builds, assert so that compiler devs can spot the broken translation and - // fix it.. - .inspect(|(_, errs)| { - debug_assert!( - errs.is_empty(), - "identifier: {:?}, attr: {:?}, args: {:?}, errors: {:?}", - identifier, - attr, - args, - errs - ); - }) - // ..otherwise, for end users, an error about this wouldn't be useful or actionable, so - // just hide it and try with the fallback bundle. - .filter(|(_, errs)| errs.is_empty()) - .or_else(|| translate_with_bundle(self.fallback_fluent_bundle())) - .map(|(translated, errs)| { - // Always bail out for errors with the fallback bundle. - assert!( - errs.is_empty(), - "identifier: {:?}, attr: {:?}, args: {:?}, errors: {:?}", - identifier, - attr, - args, - errs - ); - translated - }) - .expect("failed to find message in primary or fallback fluent bundles") - } - /// Formats the substitutions of the primary_span /// /// There are a lot of conditions to this method, but in short: @@ -616,11 +520,7 @@ pub trait Emitter { } } -impl Emitter for EmitterWriter { - fn source_map(&self) -> Option<&Lrc> { - self.sm.as_ref() - } - +impl Translate for EmitterWriter { fn fluent_bundle(&self) -> Option<&Lrc> { self.fluent_bundle.as_ref() } @@ -628,6 +528,12 @@ impl Emitter for EmitterWriter { fn fallback_fluent_bundle(&self) -> &FluentBundle { &**self.fallback_bundle } +} + +impl Emitter for EmitterWriter { + fn source_map(&self) -> Option<&Lrc> { + self.sm.as_ref() + } fn emit_diagnostic(&mut self, diag: &Diagnostic) { let fluent_args = self.to_fluent_args(diag.args()); @@ -672,11 +578,7 @@ pub struct SilentEmitter { pub fatal_note: Option, } -impl Emitter for SilentEmitter { - fn source_map(&self) -> Option<&Lrc> { - None - } - +impl Translate for SilentEmitter { fn fluent_bundle(&self) -> Option<&Lrc> { None } @@ -684,6 +586,12 @@ impl Emitter for SilentEmitter { fn fallback_fluent_bundle(&self) -> &FluentBundle { panic!("silent emitter attempted to translate message") } +} + +impl Emitter for SilentEmitter { + fn source_map(&self) -> Option<&Lrc> { + None + } fn emit_diagnostic(&mut self, d: &Diagnostic) { if d.level == Level::Fatal { diff --git a/compiler/rustc_errors/src/json.rs b/compiler/rustc_errors/src/json.rs index b8cd334b4c6c6..1680c6accd78c 100644 --- a/compiler/rustc_errors/src/json.rs +++ b/compiler/rustc_errors/src/json.rs @@ -13,6 +13,7 @@ use rustc_span::source_map::{FilePathMapping, SourceMap}; use crate::emitter::{Emitter, HumanReadableErrorType}; use crate::registry::Registry; +use crate::translation::Translate; use crate::DiagnosticId; use crate::{ CodeSuggestion, FluentBundle, LazyFallbackBundle, MultiSpan, SpanLabel, SubDiagnostic, @@ -122,6 +123,16 @@ impl JsonEmitter { } } +impl Translate for JsonEmitter { + fn fluent_bundle(&self) -> Option<&Lrc> { + self.fluent_bundle.as_ref() + } + + fn fallback_fluent_bundle(&self) -> &FluentBundle { + &**self.fallback_bundle + } +} + impl Emitter for JsonEmitter { fn emit_diagnostic(&mut self, diag: &crate::Diagnostic) { let data = Diagnostic::from_errors_diagnostic(diag, self); @@ -189,14 +200,6 @@ impl Emitter for JsonEmitter { Some(&self.sm) } - fn fluent_bundle(&self) -> Option<&Lrc> { - self.fluent_bundle.as_ref() - } - - fn fallback_fluent_bundle(&self) -> &FluentBundle { - &**self.fallback_bundle - } - fn should_show_explain(&self) -> bool { !matches!(self.json_rendered, HumanReadableErrorType::Short(_)) } diff --git a/compiler/rustc_errors/src/lib.rs b/compiler/rustc_errors/src/lib.rs index d2eb4f212eb88..53e787afc38f1 100644 --- a/compiler/rustc_errors/src/lib.rs +++ b/compiler/rustc_errors/src/lib.rs @@ -58,6 +58,7 @@ mod lock; pub mod registry; mod snippet; mod styled_buffer; +pub mod translation; pub use snippet::Style; diff --git a/compiler/rustc_errors/src/translation.rs b/compiler/rustc_errors/src/translation.rs new file mode 100644 index 0000000000000..65338f56d9ccb --- /dev/null +++ b/compiler/rustc_errors/src/translation.rs @@ -0,0 +1,103 @@ +use crate::snippet::Style; +use crate::{DiagnosticArg, DiagnosticMessage, FluentBundle}; +use rustc_data_structures::sync::Lrc; +use rustc_error_messages::FluentArgs; +use std::borrow::Cow; + +pub trait Translate { + /// Return `FluentBundle` with localized diagnostics for the locale requested by the user. If no + /// language was requested by the user then this will be `None` and `fallback_fluent_bundle` + /// should be used. + fn fluent_bundle(&self) -> Option<&Lrc>; + + /// Return `FluentBundle` with localized diagnostics for the default locale of the compiler. + /// Used when the user has not requested a specific language or when a localized diagnostic is + /// unavailable for the requested locale. + fn fallback_fluent_bundle(&self) -> &FluentBundle; + + /// Convert diagnostic arguments (a rustc internal type that exists to implement + /// `Encodable`/`Decodable`) into `FluentArgs` which is necessary to perform translation. + /// + /// Typically performed once for each diagnostic at the start of `emit_diagnostic` and then + /// passed around as a reference thereafter. + fn to_fluent_args<'arg>(&self, args: &[DiagnosticArg<'arg>]) -> FluentArgs<'arg> { + FromIterator::from_iter(args.to_vec().drain(..)) + } + + /// Convert `DiagnosticMessage`s to a string, performing translation if necessary. + fn translate_messages( + &self, + messages: &[(DiagnosticMessage, Style)], + args: &FluentArgs<'_>, + ) -> Cow<'_, str> { + Cow::Owned( + messages.iter().map(|(m, _)| self.translate_message(m, args)).collect::(), + ) + } + + /// Convert a `DiagnosticMessage` to a string, performing translation if necessary. + fn translate_message<'a>( + &'a self, + message: &'a DiagnosticMessage, + args: &'a FluentArgs<'_>, + ) -> Cow<'_, str> { + trace!(?message, ?args); + let (identifier, attr) = match message { + DiagnosticMessage::Str(msg) => return Cow::Borrowed(&msg), + DiagnosticMessage::FluentIdentifier(identifier, attr) => (identifier, attr), + }; + + let translate_with_bundle = |bundle: &'a FluentBundle| -> Option<(Cow<'_, str>, Vec<_>)> { + let message = bundle.get_message(&identifier)?; + let value = match attr { + Some(attr) => message.get_attribute(attr)?.value(), + None => message.value()?, + }; + debug!(?message, ?value); + + let mut errs = vec![]; + let translated = bundle.format_pattern(value, Some(&args), &mut errs); + debug!(?translated, ?errs); + Some((translated, errs)) + }; + + self.fluent_bundle() + .and_then(|bundle| translate_with_bundle(bundle)) + // If `translate_with_bundle` returns `None` with the primary bundle, this is likely + // just that the primary bundle doesn't contain the message being translated, so + // proceed to the fallback bundle. + // + // However, when errors are produced from translation, then that means the translation + // is broken (e.g. `{$foo}` exists in a translation but `foo` isn't provided). + // + // In debug builds, assert so that compiler devs can spot the broken translation and + // fix it.. + .inspect(|(_, errs)| { + debug_assert!( + errs.is_empty(), + "identifier: {:?}, attr: {:?}, args: {:?}, errors: {:?}", + identifier, + attr, + args, + errs + ); + }) + // ..otherwise, for end users, an error about this wouldn't be useful or actionable, so + // just hide it and try with the fallback bundle. + .filter(|(_, errs)| errs.is_empty()) + .or_else(|| translate_with_bundle(self.fallback_fluent_bundle())) + .map(|(translated, errs)| { + // Always bail out for errors with the fallback bundle. + assert!( + errs.is_empty(), + "identifier: {:?}, attr: {:?}, args: {:?}, errors: {:?}", + identifier, + attr, + args, + errs + ); + translated + }) + .expect("failed to find message in primary or fallback fluent bundles") + } +} diff --git a/src/librustdoc/passes/check_code_block_syntax.rs b/src/librustdoc/passes/check_code_block_syntax.rs index 0172ef5700b62..381ac7a5deef1 100644 --- a/src/librustdoc/passes/check_code_block_syntax.rs +++ b/src/librustdoc/passes/check_code_block_syntax.rs @@ -1,7 +1,8 @@ //! Validates syntax inside Rust code blocks (\`\`\`rust). use rustc_data_structures::sync::{Lock, Lrc}; use rustc_errors::{ - emitter::Emitter, Applicability, Diagnostic, Handler, LazyFallbackBundle, LintDiagnosticBuilder, + emitter::Emitter, translation::Translate, Applicability, Diagnostic, Handler, + LazyFallbackBundle, LintDiagnosticBuilder, }; use rustc_parse::parse_stream_from_source_str; use rustc_session::parse::ParseSess; @@ -181,6 +182,16 @@ struct BufferEmitter { fallback_bundle: LazyFallbackBundle, } +impl Translate for BufferEmitter { + fn fluent_bundle(&self) -> Option<&Lrc> { + None + } + + fn fallback_fluent_bundle(&self) -> &rustc_errors::FluentBundle { + &**self.fallback_bundle + } +} + impl Emitter for BufferEmitter { fn emit_diagnostic(&mut self, diag: &Diagnostic) { let mut buffer = self.buffer.borrow_mut(); @@ -194,12 +205,4 @@ impl Emitter for BufferEmitter { fn source_map(&self) -> Option<&Lrc> { None } - - fn fluent_bundle(&self) -> Option<&Lrc> { - None - } - - fn fallback_fluent_bundle(&self) -> &rustc_errors::FluentBundle { - &**self.fallback_bundle - } } diff --git a/src/tools/rustfmt/src/parse/session.rs b/src/tools/rustfmt/src/parse/session.rs index 23db542191036..6efeee98fea6c 100644 --- a/src/tools/rustfmt/src/parse/session.rs +++ b/src/tools/rustfmt/src/parse/session.rs @@ -3,6 +3,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use rustc_data_structures::sync::{Lrc, Send}; use rustc_errors::emitter::{Emitter, EmitterWriter}; +use rustc_errors::translation::Translate; use rustc_errors::{ColorConfig, Diagnostic, Handler, Level as DiagnosticLevel}; use rustc_session::parse::ParseSess as RawParseSess; use rustc_span::{ @@ -28,19 +29,24 @@ pub(crate) struct ParseSess { /// Emitter which discards every error. struct SilentEmitter; -impl Emitter for SilentEmitter { - fn source_map(&self) -> Option<&Lrc> { - None - } - fn emit_diagnostic(&mut self, _db: &Diagnostic) {} +impl Translate for SilentEmitter { fn fluent_bundle(&self) -> Option<&Lrc> { None } + fn fallback_fluent_bundle(&self) -> &rustc_errors::FluentBundle { panic!("silent emitter attempted to translate a diagnostic"); } } +impl Emitter for SilentEmitter { + fn source_map(&self) -> Option<&Lrc> { + None + } + + fn emit_diagnostic(&mut self, _db: &Diagnostic) {} +} + fn silent_emitter() -> Box { Box::new(SilentEmitter {}) } @@ -62,10 +68,21 @@ impl SilentOnIgnoredFilesEmitter { } } +impl Translate for SilentOnIgnoredFilesEmitter { + fn fluent_bundle(&self) -> Option<&Lrc> { + self.emitter.fluent_bundle() + } + + fn fallback_fluent_bundle(&self) -> &rustc_errors::FluentBundle { + self.emitter.fallback_fluent_bundle() + } +} + impl Emitter for SilentOnIgnoredFilesEmitter { fn source_map(&self) -> Option<&Lrc> { None } + fn emit_diagnostic(&mut self, db: &Diagnostic) { if db.level() == DiagnosticLevel::Fatal { return self.handle_non_ignoreable_error(db); @@ -88,14 +105,6 @@ impl Emitter for SilentOnIgnoredFilesEmitter { } self.handle_non_ignoreable_error(db); } - - fn fluent_bundle(&self) -> Option<&Lrc> { - self.emitter.fluent_bundle() - } - - fn fallback_fluent_bundle(&self) -> &rustc_errors::FluentBundle { - self.emitter.fallback_fluent_bundle() - } } fn default_handler( @@ -340,19 +349,24 @@ mod tests { num_emitted_errors: Lrc, } + impl Translate for TestEmitter { + fn fluent_bundle(&self) -> Option<&Lrc> { + None + } + + fn fallback_fluent_bundle(&self) -> &rustc_errors::FluentBundle { + panic!("test emitter attempted to translate a diagnostic"); + } + } + impl Emitter for TestEmitter { fn source_map(&self) -> Option<&Lrc> { None } + fn emit_diagnostic(&mut self, _db: &Diagnostic) { self.num_emitted_errors.fetch_add(1, Ordering::Release); } - fn fluent_bundle(&self) -> Option<&Lrc> { - None - } - fn fallback_fluent_bundle(&self) -> &rustc_errors::FluentBundle { - panic!("test emitter attempted to translate a diagnostic"); - } } fn build_diagnostic(level: DiagnosticLevel, span: Option) -> Diagnostic { From a9e61677fc56c664bc4727814564b1390f8a1812 Mon Sep 17 00:00:00 2001 From: David Wood Date: Wed, 10 Aug 2022 17:13:18 +0100 Subject: [PATCH 2/3] triagebot: add mention groups for diag translation Add mention groups to `triagebot.toml` for diagnostic derive macros and diagnostic translation sources/resources. Signed-off-by: David Wood --- triagebot.toml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/triagebot.toml b/triagebot.toml index ea654192bbf78..19f24a3f2ecb7 100644 --- a/triagebot.toml +++ b/triagebot.toml @@ -342,3 +342,15 @@ cc = ["@rust-lang/rustfmt"] [mentions."compiler/rustc_middle/src/mir/syntax.rs"] message = "This PR changes MIR" cc = ["@oli-obk", "@RalfJung", "@JakobDegen", "@davidtwco", "@celinval", "@vakaras"] + +[mentions."compiler/rustc_error_messages"] +message = "`rustc_error_messages` was changed" +cc = ["@davidtwco", "@compiler-errors", "@JohnTitor", "@estebank"] + +[mentions."compiler/rustc_errors/src/translation.rs"] +message = "`rustc_errors::translation` was changed" +cc = ["@davidtwco", "@compiler-errors", "@JohnTitor", "@estebank"] + +[mentions."compiler/rustc_macros/src/diagnostics"] +message = "`rustc_macros::diagnostics` was changed" +cc = ["@davidtwco", "@compiler-errors", "@JohnTitor", "@estebank"] From ae02014b2eb5bd0fe26f64eb5cefd9c4f92e5216 Mon Sep 17 00:00:00 2001 From: David Wood Date: Fri, 12 Aug 2022 09:14:40 +0100 Subject: [PATCH 3/3] triagebot: autolabel `A-translation` Instruct triagebot to autolabel pull requests that modify the translation sources with the `A-translation` label. Signed-off-by: David Wood --- triagebot.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/triagebot.toml b/triagebot.toml index 19f24a3f2ecb7..b7532e016b68e 100644 --- a/triagebot.toml +++ b/triagebot.toml @@ -178,6 +178,13 @@ trigger_files = [ "src/tools/bump-stage0", ] +[autolabel."A-translation"] +trigger_files = [ + "compiler/rustc_error_messages", + "compiler/rustc_errors/src/translation.rs", + "compiler/rustc_macros/src/diagnostics" +] + [notify-zulip."I-prioritize"] zulip_stream = 245100 # #t-compiler/wg-prioritization/alerts topic = "#{number} {title}"