deltachat/
stock_str.rs

1//! Module to work with translatable stock strings.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use anyhow::{Result, bail};
7use humansize::{BINARY, format_size};
8use strum::EnumProperty as EnumPropertyTrait;
9use strum_macros::EnumProperty;
10use tokio::sync::RwLock;
11
12use crate::accounts::Accounts;
13use crate::blob::BlobObject;
14use crate::chat::{self, Chat, ChatId};
15use crate::config::Config;
16use crate::contact::{Contact, ContactId, Origin};
17use crate::context::Context;
18use crate::message::{Message, Viewtype};
19use crate::param::Param;
20use crate::tools::timestamp_to_str;
21
22/// Storage for string translations.
23#[derive(Debug, Clone)]
24pub struct StockStrings {
25    /// Map from stock string ID to the translation.
26    translated_stockstrings: Arc<RwLock<HashMap<usize, String>>>,
27}
28
29/// Stock strings
30///
31/// These identify the string to return in [Context.stock_str].  The
32/// numbers must stay in sync with `deltachat.h` `DC_STR_*` constants.
33///
34/// See the `stock_*` methods on [Context] to use these.
35///
36/// [Context]: crate::context::Context
37#[allow(missing_docs)]
38#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, EnumProperty)]
39#[repr(u32)]
40pub enum StockMessage {
41    #[strum(props(fallback = "No messages."))]
42    NoMessages = 1,
43
44    #[strum(props(fallback = "Me"))]
45    SelfMsg = 2,
46
47    #[strum(props(fallback = "Draft"))]
48    Draft = 3,
49
50    #[strum(props(fallback = "Voice message"))]
51    VoiceMessage = 7,
52
53    #[strum(props(fallback = "Image"))]
54    Image = 9,
55
56    #[strum(props(fallback = "Video"))]
57    Video = 10,
58
59    #[strum(props(fallback = "Audio"))]
60    Audio = 11,
61
62    #[strum(props(fallback = "File"))]
63    File = 12,
64
65    #[strum(props(fallback = "GIF"))]
66    Gif = 23,
67
68    #[strum(props(fallback = "End-to-end encryption available"))]
69    E2eAvailable = 25,
70
71    #[strum(props(fallback = "No encryption"))]
72    EncrNone = 28,
73
74    #[strum(props(fallback = "Fingerprints"))]
75    FingerPrints = 30,
76
77    #[strum(props(fallback = "%1$s verified."))]
78    ContactVerified = 35,
79
80    #[strum(props(fallback = "Archived chats"))]
81    ArchivedChats = 40,
82
83    #[strum(props(
84        fallback = "Cannot login as \"%1$s\". Please check if the email address and the password are correct."
85    ))]
86    CannotLogin = 60,
87
88    #[strum(props(fallback = "Location streaming enabled."))]
89    MsgLocationEnabled = 64,
90
91    #[strum(props(fallback = "Location streaming disabled."))]
92    MsgLocationDisabled = 65,
93
94    #[strum(props(fallback = "Location"))]
95    Location = 66,
96
97    #[strum(props(fallback = "Sticker"))]
98    Sticker = 67,
99
100    #[strum(props(fallback = "Device messages"))]
101    DeviceMessages = 68,
102
103    #[strum(props(fallback = "Saved messages"))]
104    SavedMessages = 69,
105
106    #[strum(props(
107        fallback = "Messages in this chat are generated locally by your Delta Chat app. \
108                    Its makers use it to inform about app updates and problems during usage."
109    ))]
110    DeviceMessagesHint = 70,
111
112    #[strum(props(fallback = "Welcome to Delta Chat! – \
113                    Delta Chat looks and feels like other popular messenger apps, \
114                    but does not involve centralized control, \
115                    tracking or selling you, friends, colleagues or family out to large organizations.\n\n\
116                    Technically, Delta Chat is an email application with a modern chat interface. \
117                    Email in a new dress if you will 👻\n\n\
118                    Use Delta Chat with anyone out of billions of people: just use their e-mail address. \
119                    Recipients don't need to install Delta Chat, visit websites or sign up anywhere - \
120                    however, of course, if they like, you may point them to 👉 https://get.delta.chat"))]
121    WelcomeMessage = 71,
122
123    #[strum(props(fallback = "Message from %1$s"))]
124    SubjectForNewContact = 73,
125
126    /// Unused. Was used in group chat status messages.
127    #[strum(props(fallback = "Failed to send message to %1$s."))]
128    FailedSendingTo = 74,
129
130    #[strum(props(fallback = "Error:\n\n“%1$s”"))]
131    ConfigurationFailed = 84,
132
133    #[strum(props(
134        fallback = "⚠️ Date or time of your device seem to be inaccurate (%1$s).\n\n\
135                    Adjust your clock ⏰🔧 to ensure your messages are received correctly."
136    ))]
137    BadTimeMsgBody = 85,
138
139    #[strum(props(fallback = "⚠️ Your Delta Chat version might be outdated.\n\n\
140                    This may cause problems because your chat partners use newer versions - \
141                    and you are missing the latest features 😳\n\
142                    Please check https://get.delta.chat or your app store for updates."))]
143    UpdateReminderMsgBody = 86,
144
145    #[strum(props(
146        fallback = "Could not find your mail server.\n\nPlease check your internet connection."
147    ))]
148    ErrorNoNetwork = 87,
149
150    // used in summaries, a noun, not a verb (not: "to reply")
151    #[strum(props(fallback = "Reply"))]
152    ReplyNoun = 90,
153
154    #[strum(props(fallback = "You deleted the \"Saved messages\" chat.\n\n\
155                    To use the \"Saved messages\" feature again, create a new chat with yourself."))]
156    SelfDeletedMsgBody = 91,
157
158    #[strum(props(
159        fallback = "⚠️ The \"Delete messages from server\" feature now also deletes messages in folders other than Inbox, DeltaChat and Sent.\n\n\
160                    ℹ️ To avoid accidentally deleting messages, we turned it off for you. Please turn it on again at \
161                    Settings → \"Chats and Media\" → \"Delete messages from server\" to continue using it."
162    ))]
163    DeleteServerTurnedOff = 92,
164
165    #[strum(props(fallback = "Forwarded"))]
166    Forwarded = 97,
167
168    #[strum(props(
169        fallback = "⚠️ Your provider's storage is about to exceed, already %1$s%% are used.\n\n\
170                    You may not be able to receive message when the storage is 100%% used.\n\n\
171                    👉 Please check if you can delete old data in the provider's webinterface \
172                    and consider to enable \"Settings / Delete Old Messages\". \
173                    You can check your current storage usage anytime at \"Settings / Connectivity\"."
174    ))]
175    QuotaExceedingMsgBody = 98,
176
177    #[strum(props(fallback = "%1$s message"))]
178    PartialDownloadMsgBody = 99,
179
180    #[strum(props(fallback = "Download maximum available until %1$s"))]
181    DownloadAvailability = 100,
182
183    #[strum(props(fallback = "Multi Device Synchronization"))]
184    SyncMsgSubject = 101,
185
186    #[strum(props(
187        fallback = "This message is used to synchronize data between your devices.\n\n\
188                    👉 If you see this message in Delta Chat, please update your Delta Chat apps on all devices."
189    ))]
190    SyncMsgBody = 102,
191
192    #[strum(props(fallback = "Incoming Messages"))]
193    IncomingMessages = 103,
194
195    #[strum(props(fallback = "Outgoing Messages"))]
196    OutgoingMessages = 104,
197
198    #[strum(props(fallback = "Storage on %1$s"))]
199    StorageOnDomain = 105,
200
201    #[strum(props(fallback = "Connected"))]
202    Connected = 107,
203
204    #[strum(props(fallback = "Connecting…"))]
205    Connecting = 108,
206
207    #[strum(props(fallback = "Updating…"))]
208    Updating = 109,
209
210    #[strum(props(fallback = "Sending…"))]
211    Sending = 110,
212
213    #[strum(props(fallback = "Your last message was sent successfully."))]
214    LastMsgSentSuccessfully = 111,
215
216    #[strum(props(fallback = "Error: %1$s"))]
217    Error = 112,
218
219    #[strum(props(fallback = "Not supported by your provider."))]
220    NotSupportedByProvider = 113,
221
222    #[strum(props(fallback = "Messages"))]
223    Messages = 114,
224
225    #[strum(props(fallback = "%1$s of %2$s used"))]
226    PartOfTotallUsed = 116,
227
228    #[strum(props(fallback = "%1$s invited you to join this group.\n\n\
229                             Waiting for the device of %2$s to reply…"))]
230    SecureJoinStarted = 117,
231
232    #[strum(props(fallback = "%1$s replied, waiting for being added to the group…"))]
233    SecureJoinReplies = 118,
234
235    #[strum(props(fallback = "Scan to chat with %1$s"))]
236    SetupContactQRDescription = 119,
237
238    #[strum(props(fallback = "Scan to join group %1$s"))]
239    SecureJoinGroupQRDescription = 120,
240
241    #[strum(props(fallback = "Not connected"))]
242    NotConnected = 121,
243
244    #[strum(props(
245        fallback = "You changed your email address from %1$s to %2$s.\n\nIf you now send a message to a verified group, contacts there will automatically replace the old with your new address.\n\nIt's highly advised to set up your old email provider to forward all emails to your new email address. Otherwise you might miss messages of contacts who did not get your new address yet."
246    ))]
247    AeapExplanationAndLink = 123,
248
249    #[strum(props(fallback = "You changed group name from \"%1$s\" to \"%2$s\"."))]
250    MsgYouChangedGrpName = 124,
251
252    #[strum(props(fallback = "Group name changed from \"%1$s\" to \"%2$s\" by %3$s."))]
253    MsgGrpNameChangedBy = 125,
254
255    #[strum(props(fallback = "You changed the group image."))]
256    MsgYouChangedGrpImg = 126,
257
258    #[strum(props(fallback = "Group image changed by %1$s."))]
259    MsgGrpImgChangedBy = 127,
260
261    #[strum(props(fallback = "You added member %1$s."))]
262    MsgYouAddMember = 128,
263
264    #[strum(props(fallback = "Member %1$s added by %2$s."))]
265    MsgAddMemberBy = 129,
266
267    #[strum(props(fallback = "You removed member %1$s."))]
268    MsgYouDelMember = 130,
269
270    #[strum(props(fallback = "Member %1$s removed by %2$s."))]
271    MsgDelMemberBy = 131,
272
273    #[strum(props(fallback = "You left the group."))]
274    MsgYouLeftGroup = 132,
275
276    #[strum(props(fallback = "Group left by %1$s."))]
277    MsgGroupLeftBy = 133,
278
279    #[strum(props(fallback = "You deleted the group image."))]
280    MsgYouDeletedGrpImg = 134,
281
282    #[strum(props(fallback = "Group image deleted by %1$s."))]
283    MsgGrpImgDeletedBy = 135,
284
285    #[strum(props(fallback = "You enabled location streaming."))]
286    MsgYouEnabledLocation = 136,
287
288    #[strum(props(fallback = "Location streaming enabled by %1$s."))]
289    MsgLocationEnabledBy = 137,
290
291    #[strum(props(fallback = "You disabled message deletion timer."))]
292    MsgYouDisabledEphemeralTimer = 138,
293
294    #[strum(props(fallback = "Message deletion timer is disabled by %1$s."))]
295    MsgEphemeralTimerDisabledBy = 139,
296
297    // A fallback message for unknown timer values.
298    // "s" stands for "second" SI unit here.
299    #[strum(props(fallback = "You set message deletion timer to %1$s s."))]
300    MsgYouEnabledEphemeralTimer = 140,
301
302    #[strum(props(fallback = "Message deletion timer is set to %1$s s by %2$s."))]
303    MsgEphemeralTimerEnabledBy = 141,
304
305    #[strum(props(fallback = "You set message deletion timer to 1 minute."))]
306    MsgYouEphemeralTimerMinute = 142,
307
308    #[strum(props(fallback = "Message deletion timer is set to 1 minute by %1$s."))]
309    MsgEphemeralTimerMinuteBy = 143,
310
311    #[strum(props(fallback = "You set message deletion timer to 1 hour."))]
312    MsgYouEphemeralTimerHour = 144,
313
314    #[strum(props(fallback = "Message deletion timer is set to 1 hour by %1$s."))]
315    MsgEphemeralTimerHourBy = 145,
316
317    #[strum(props(fallback = "You set message deletion timer to 1 day."))]
318    MsgYouEphemeralTimerDay = 146,
319
320    #[strum(props(fallback = "Message deletion timer is set to 1 day by %1$s."))]
321    MsgEphemeralTimerDayBy = 147,
322
323    #[strum(props(fallback = "You set message deletion timer to 1 week."))]
324    MsgYouEphemeralTimerWeek = 148,
325
326    #[strum(props(fallback = "Message deletion timer is set to 1 week by %1$s."))]
327    MsgEphemeralTimerWeekBy = 149,
328
329    #[strum(props(fallback = "You set message deletion timer to %1$s minutes."))]
330    MsgYouEphemeralTimerMinutes = 150,
331
332    #[strum(props(fallback = "Message deletion timer is set to %1$s minutes by %2$s."))]
333    MsgEphemeralTimerMinutesBy = 151,
334
335    #[strum(props(fallback = "You set message deletion timer to %1$s hours."))]
336    MsgYouEphemeralTimerHours = 152,
337
338    #[strum(props(fallback = "Message deletion timer is set to %1$s hours by %2$s."))]
339    MsgEphemeralTimerHoursBy = 153,
340
341    #[strum(props(fallback = "You set message deletion timer to %1$s days."))]
342    MsgYouEphemeralTimerDays = 154,
343
344    #[strum(props(fallback = "Message deletion timer is set to %1$s days by %2$s."))]
345    MsgEphemeralTimerDaysBy = 155,
346
347    #[strum(props(fallback = "You set message deletion timer to %1$s weeks."))]
348    MsgYouEphemeralTimerWeeks = 156,
349
350    #[strum(props(fallback = "Message deletion timer is set to %1$s weeks by %2$s."))]
351    MsgEphemeralTimerWeeksBy = 157,
352
353    #[strum(props(fallback = "You set message deletion timer to 1 year."))]
354    MsgYouEphemeralTimerYear = 158,
355
356    #[strum(props(fallback = "Message deletion timer is set to 1 year by %1$s."))]
357    MsgEphemeralTimerYearBy = 159,
358
359    #[strum(props(fallback = "Scan to set up second device for %1$s"))]
360    BackupTransferQr = 162,
361
362    #[strum(props(fallback = "ℹ️ Account transferred to your second device."))]
363    BackupTransferMsgBody = 163,
364
365    #[strum(props(fallback = "I added member %1$s."))]
366    MsgIAddMember = 164,
367
368    #[strum(props(fallback = "I removed member %1$s."))]
369    MsgIDelMember = 165,
370
371    #[strum(props(fallback = "I left the group."))]
372    MsgILeftGroup = 166,
373
374    #[strum(props(fallback = "Messages are end-to-end encrypted."))]
375    ChatProtectionEnabled = 170,
376
377    // deprecated 2025-07
378    #[strum(props(fallback = "%1$s sent a message from another device."))]
379    ChatProtectionDisabled = 171,
380
381    #[strum(props(fallback = "Others will only see this group after you sent a first message."))]
382    NewGroupSendFirstMessage = 172,
383
384    #[strum(props(fallback = "Member %1$s added."))]
385    MsgAddMember = 173,
386
387    #[strum(props(
388        fallback = "⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet."
389    ))]
390    InvalidUnencryptedMail = 174,
391
392    #[strum(props(fallback = "You reacted %1$s to \"%2$s\""))]
393    MsgYouReacted = 176,
394
395    #[strum(props(fallback = "%1$s reacted %2$s to \"%3$s\""))]
396    MsgReactedBy = 177,
397
398    #[strum(props(fallback = "Member %1$s removed."))]
399    MsgDelMember = 178,
400
401    #[strum(props(fallback = "Establishing guaranteed end-to-end encryption, please wait…"))]
402    SecurejoinWait = 190,
403
404    #[strum(props(fallback = "❤️ Seems you're enjoying Delta Chat!
405
406Please consider donating to help that Delta Chat stays free for everyone.
407
408While Delta Chat is free to use and open source, development costs money.
409Help keeping us to keep Delta Chat independent and make it more awesome in the future.
410
411https://delta.chat/donate"))]
412    DonationRequest = 193,
413
414    #[strum(props(fallback = "Outgoing call"))]
415    OutgoingCall = 194,
416
417    #[strum(props(fallback = "Incoming call"))]
418    IncomingCall = 195,
419
420    #[strum(props(fallback = "Declined call"))]
421    DeclinedCall = 196,
422
423    #[strum(props(fallback = "Canceled call"))]
424    CanceledCall = 197,
425
426    #[strum(props(fallback = "Missed call"))]
427    MissedCall = 198,
428
429    #[strum(props(fallback = "You left the channel."))]
430    MsgYouLeftBroadcast = 200,
431
432    #[strum(props(fallback = "Scan to join channel %1$s"))]
433    SecureJoinBrodcastQRDescription = 201,
434
435    #[strum(props(fallback = "You joined the channel."))]
436    MsgYouJoinedBroadcast = 202,
437
438    #[strum(props(
439        fallback = "The attachment contains anonymous usage statistics, which helps us improve Delta Chat. Thank you!"
440    ))]
441    StatsMsgBody = 210,
442
443    #[strum(props(fallback = "Proxy Enabled"))]
444    ProxyEnabled = 220,
445
446    #[strum(props(
447        fallback = "You are using a proxy. If you're having trouble connecting, try a different proxy."
448    ))]
449    ProxyEnabledDescription = 221,
450}
451
452impl StockMessage {
453    /// Default untranslated strings for stock messages.
454    ///
455    /// These could be used in logging calls, so no logging here.
456    fn fallback(self) -> &'static str {
457        self.get_str("fallback").unwrap_or_default()
458    }
459}
460
461impl Default for StockStrings {
462    fn default() -> Self {
463        StockStrings::new()
464    }
465}
466
467impl StockStrings {
468    /// Creates a new translated string storage.
469    pub fn new() -> Self {
470        Self {
471            translated_stockstrings: Arc::new(RwLock::new(Default::default())),
472        }
473    }
474
475    async fn translated(&self, id: StockMessage) -> String {
476        self.translated_stockstrings
477            .read()
478            .await
479            .get(&(id as usize))
480            .map(AsRef::as_ref)
481            .unwrap_or_else(|| id.fallback())
482            .to_string()
483    }
484
485    async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
486        if stockstring.contains("%1") && !id.fallback().contains("%1") {
487            bail!(
488                "translation {} contains invalid %1 placeholder, default is {}",
489                stockstring,
490                id.fallback()
491            );
492        }
493        if stockstring.contains("%2") && !id.fallback().contains("%2") {
494            bail!(
495                "translation {} contains invalid %2 placeholder, default is {}",
496                stockstring,
497                id.fallback()
498            );
499        }
500        self.translated_stockstrings
501            .write()
502            .await
503            .insert(id as usize, stockstring);
504        Ok(())
505    }
506}
507
508async fn translated(context: &Context, id: StockMessage) -> String {
509    context.translated_stockstrings.translated(id).await
510}
511
512/// Helper trait only meant to be implemented for [`String`].
513trait StockStringMods: AsRef<str> + Sized {
514    /// Substitutes the first replacement value if one is present.
515    fn replace1(&self, replacement: &str) -> String {
516        self.as_ref()
517            .replacen("%1$s", replacement, 1)
518            .replacen("%1$d", replacement, 1)
519            .replacen("%1$@", replacement, 1)
520    }
521
522    /// Substitutes the second replacement value if one is present.
523    ///
524    /// Be aware you probably should have also called [`StockStringMods::replace1`] if
525    /// you are calling this.
526    fn replace2(&self, replacement: &str) -> String {
527        self.as_ref()
528            .replacen("%2$s", replacement, 1)
529            .replacen("%2$d", replacement, 1)
530            .replacen("%2$@", replacement, 1)
531    }
532
533    /// Substitutes the third replacement value if one is present.
534    ///
535    /// Be aware you probably should have also called [`StockStringMods::replace1`] and
536    /// [`StockStringMods::replace2`] if you are calling this.
537    fn replace3(&self, replacement: &str) -> String {
538        self.as_ref()
539            .replacen("%3$s", replacement, 1)
540            .replacen("%3$d", replacement, 1)
541            .replacen("%3$@", replacement, 1)
542    }
543}
544
545impl ContactId {
546    /// Get contact name, e.g. `Bob`, or `bob@example.net` if no name is set.
547    async fn get_stock_name(self, context: &Context) -> String {
548        Contact::get_by_id(context, self)
549            .await
550            .map(|contact| contact.get_display_name().to_string())
551            .unwrap_or_else(|_| self.to_string())
552    }
553}
554
555impl StockStringMods for String {}
556
557/// Stock string: `No messages.`.
558pub(crate) async fn no_messages(context: &Context) -> String {
559    translated(context, StockMessage::NoMessages).await
560}
561
562/// Stock string: `Me`.
563pub(crate) async fn self_msg(context: &Context) -> String {
564    translated(context, StockMessage::SelfMsg).await
565}
566
567/// Stock string: `Draft`.
568pub(crate) async fn draft(context: &Context) -> String {
569    translated(context, StockMessage::Draft).await
570}
571
572/// Stock string: `Voice message`.
573pub(crate) async fn voice_message(context: &Context) -> String {
574    translated(context, StockMessage::VoiceMessage).await
575}
576
577/// Stock string: `Image`.
578pub(crate) async fn image(context: &Context) -> String {
579    translated(context, StockMessage::Image).await
580}
581
582/// Stock string: `Video`.
583pub(crate) async fn video(context: &Context) -> String {
584    translated(context, StockMessage::Video).await
585}
586
587/// Stock string: `Audio`.
588pub(crate) async fn audio(context: &Context) -> String {
589    translated(context, StockMessage::Audio).await
590}
591
592/// Stock string: `File`.
593pub(crate) async fn file(context: &Context) -> String {
594    translated(context, StockMessage::File).await
595}
596
597/// Stock string: `Group name changed from "%1$s" to "%2$s".`.
598pub(crate) async fn msg_grp_name(
599    context: &Context,
600    from_group: &str,
601    to_group: &str,
602    by_contact: ContactId,
603) -> String {
604    if by_contact == ContactId::SELF {
605        translated(context, StockMessage::MsgYouChangedGrpName)
606            .await
607            .replace1(from_group)
608            .replace2(to_group)
609    } else {
610        translated(context, StockMessage::MsgGrpNameChangedBy)
611            .await
612            .replace1(from_group)
613            .replace2(to_group)
614            .replace3(&by_contact.get_stock_name(context).await)
615    }
616}
617
618pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
619    if by_contact == ContactId::SELF {
620        translated(context, StockMessage::MsgYouChangedGrpImg).await
621    } else {
622        translated(context, StockMessage::MsgGrpImgChangedBy)
623            .await
624            .replace1(&by_contact.get_stock_name(context).await)
625    }
626}
627
628/// Stock string: `I added member %1$s.`.
629/// This one is for sending in group chats.
630///
631/// The `added_member_addr` parameter should be an email address and is looked up in the
632/// contacts to combine with the authorized display name.
633pub(crate) async fn msg_add_member_remote(context: &Context, added_member_addr: &str) -> String {
634    let addr = added_member_addr;
635    let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
636        Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
637            .await
638            .map(|contact| contact.get_authname_or_addr())
639            .unwrap_or_else(|_| addr.to_string()),
640        _ => addr.to_string(),
641    };
642    translated(context, StockMessage::MsgIAddMember)
643        .await
644        .replace1(whom)
645}
646
647/// Stock string: `You added member %1$s.` or `Member %1$s added by %2$s.`.
648///
649/// The `added_member_addr` parameter should be an email address and is looked up in the
650/// contacts to combine with the display name.
651pub(crate) async fn msg_add_member_local(
652    context: &Context,
653    added_member: ContactId,
654    by_contact: ContactId,
655) -> String {
656    let whom = added_member.get_stock_name(context).await;
657    if by_contact == ContactId::UNDEFINED {
658        translated(context, StockMessage::MsgAddMember)
659            .await
660            .replace1(&whom)
661    } else if by_contact == ContactId::SELF {
662        translated(context, StockMessage::MsgYouAddMember)
663            .await
664            .replace1(&whom)
665    } else {
666        translated(context, StockMessage::MsgAddMemberBy)
667            .await
668            .replace1(&whom)
669            .replace2(&by_contact.get_stock_name(context).await)
670    }
671}
672
673/// Stock string: `I removed member %1$s.`.
674///
675/// The `removed_member_addr` parameter should be an email address and is looked up in
676/// the contacts to combine with the display name.
677pub(crate) async fn msg_del_member_remote(context: &Context, removed_member_addr: &str) -> String {
678    let addr = removed_member_addr;
679    let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
680        Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
681            .await
682            .map(|contact| contact.get_authname_or_addr())
683            .unwrap_or_else(|_| addr.to_string()),
684        _ => addr.to_string(),
685    };
686    translated(context, StockMessage::MsgIDelMember)
687        .await
688        .replace1(whom)
689}
690
691/// Stock string: `I added member %1$s.` or `Member %1$s removed by %2$s.`.
692///
693/// The `removed_member_addr` parameter should be an email address and is looked up in
694/// the contacts to combine with the display name.
695pub(crate) async fn msg_del_member_local(
696    context: &Context,
697    removed_member: ContactId,
698    by_contact: ContactId,
699) -> String {
700    let whom = removed_member.get_stock_name(context).await;
701    if by_contact == ContactId::UNDEFINED {
702        translated(context, StockMessage::MsgDelMember)
703            .await
704            .replace1(&whom)
705    } else if by_contact == ContactId::SELF {
706        translated(context, StockMessage::MsgYouDelMember)
707            .await
708            .replace1(&whom)
709    } else {
710        translated(context, StockMessage::MsgDelMemberBy)
711            .await
712            .replace1(&whom)
713            .replace2(&by_contact.get_stock_name(context).await)
714    }
715}
716
717/// Stock string: `I left the group.`.
718pub(crate) async fn msg_group_left_remote(context: &Context) -> String {
719    translated(context, StockMessage::MsgILeftGroup).await
720}
721
722/// Stock string: `You left the group.` or `Group left by %1$s.`.
723pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
724    if by_contact == ContactId::SELF {
725        translated(context, StockMessage::MsgYouLeftGroup).await
726    } else {
727        translated(context, StockMessage::MsgGroupLeftBy)
728            .await
729            .replace1(&by_contact.get_stock_name(context).await)
730    }
731}
732
733/// Stock string: `You left the channel.`
734pub(crate) async fn msg_you_left_broadcast(context: &Context) -> String {
735    translated(context, StockMessage::MsgYouLeftBroadcast).await
736}
737
738/// Stock string: `You joined the channel.`
739pub(crate) async fn msg_you_joined_broadcast(context: &Context) -> String {
740    translated(context, StockMessage::MsgYouJoinedBroadcast).await
741}
742
743/// Stock string: `You reacted %1$s to "%2$s"` or `%1$s reacted %2$s to "%3$s"`.
744pub(crate) async fn msg_reacted(
745    context: &Context,
746    by_contact: ContactId,
747    reaction: &str,
748    summary: &str,
749) -> String {
750    if by_contact == ContactId::SELF {
751        translated(context, StockMessage::MsgYouReacted)
752            .await
753            .replace1(reaction)
754            .replace2(summary)
755    } else {
756        translated(context, StockMessage::MsgReactedBy)
757            .await
758            .replace1(&by_contact.get_stock_name(context).await)
759            .replace2(reaction)
760            .replace3(summary)
761    }
762}
763
764/// Stock string: `GIF`.
765pub(crate) async fn gif(context: &Context) -> String {
766    translated(context, StockMessage::Gif).await
767}
768
769/// Stock string: `End-to-end encryption available.`.
770pub(crate) async fn e2e_available(context: &Context) -> String {
771    translated(context, StockMessage::E2eAvailable).await
772}
773
774/// Stock string: `No encryption.`.
775pub(crate) async fn encr_none(context: &Context) -> String {
776    translated(context, StockMessage::EncrNone).await
777}
778
779/// Stock string: `Fingerprints`.
780pub(crate) async fn finger_prints(context: &Context) -> String {
781    translated(context, StockMessage::FingerPrints).await
782}
783
784/// Stock string: `Group image deleted.`.
785pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
786    if by_contact == ContactId::SELF {
787        translated(context, StockMessage::MsgYouDeletedGrpImg).await
788    } else {
789        translated(context, StockMessage::MsgGrpImgDeletedBy)
790            .await
791            .replace1(&by_contact.get_stock_name(context).await)
792    }
793}
794
795/// Stock string: `%1$s invited you to join this group. Waiting for the device of %2$s to reply…`.
796pub(crate) async fn secure_join_started(
797    context: &Context,
798    inviter_contact_id: ContactId,
799) -> String {
800    if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
801        translated(context, StockMessage::SecureJoinStarted)
802            .await
803            .replace1(contact.get_display_name())
804            .replace2(contact.get_display_name())
805    } else {
806        format!("secure_join_started: unknown contact {inviter_contact_id}")
807    }
808}
809
810/// Stock string: `%1$s replied, waiting for being added to the group…`.
811pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
812    translated(context, StockMessage::SecureJoinReplies)
813        .await
814        .replace1(&contact_id.get_stock_name(context).await)
815}
816
817/// Stock string: `Establishing guaranteed end-to-end encryption, please wait…`.
818pub(crate) async fn securejoin_wait(context: &Context) -> String {
819    translated(context, StockMessage::SecurejoinWait).await
820}
821
822/// Stock string: `❤️ Seems you're enjoying Delta Chat!`…
823pub(crate) async fn donation_request(context: &Context) -> String {
824    translated(context, StockMessage::DonationRequest).await
825}
826
827/// Stock string: `Outgoing call`.
828pub(crate) async fn outgoing_call(context: &Context) -> String {
829    translated(context, StockMessage::OutgoingCall).await
830}
831
832/// Stock string: `Incoming call`.
833pub(crate) async fn incoming_call(context: &Context) -> String {
834    translated(context, StockMessage::IncomingCall).await
835}
836
837/// Stock string: `Declined call`.
838pub(crate) async fn declined_call(context: &Context) -> String {
839    translated(context, StockMessage::DeclinedCall).await
840}
841
842/// Stock string: `Canceled call`.
843pub(crate) async fn canceled_call(context: &Context) -> String {
844    translated(context, StockMessage::CanceledCall).await
845}
846
847/// Stock string: `Missed call`.
848pub(crate) async fn missed_call(context: &Context) -> String {
849    translated(context, StockMessage::MissedCall).await
850}
851
852/// Stock string: `Scan to chat with %1$s`.
853pub(crate) async fn setup_contact_qr_description(
854    context: &Context,
855    display_name: &str,
856    addr: &str,
857) -> String {
858    let name = if display_name.is_empty() {
859        addr.to_owned()
860    } else {
861        display_name.to_owned()
862    };
863    translated(context, StockMessage::SetupContactQRDescription)
864        .await
865        .replace1(&name)
866}
867
868/// Stock string: `Scan to join group %1$s`.
869pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
870    translated(context, StockMessage::SecureJoinGroupQRDescription)
871        .await
872        .replace1(chat.get_name())
873}
874
875/// Stock string: `Scan to join channel %1$s`.
876pub(crate) async fn secure_join_broadcast_qr_description(context: &Context, chat: &Chat) -> String {
877    translated(context, StockMessage::SecureJoinBrodcastQRDescription)
878        .await
879        .replace1(chat.get_name())
880}
881
882/// Stock string: `%1$s verified.`.
883#[allow(dead_code)]
884pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
885    let addr = contact.get_display_name();
886    translated(context, StockMessage::ContactVerified)
887        .await
888        .replace1(addr)
889}
890
891/// Stock string: `Archived chats`.
892pub(crate) async fn archived_chats(context: &Context) -> String {
893    translated(context, StockMessage::ArchivedChats).await
894}
895
896/// Stock string: `Multi Device Synchronization`.
897pub(crate) async fn sync_msg_subject(context: &Context) -> String {
898    translated(context, StockMessage::SyncMsgSubject).await
899}
900
901/// Stock string: `This message is used to synchronize data between your devices.`.
902pub(crate) async fn sync_msg_body(context: &Context) -> String {
903    translated(context, StockMessage::SyncMsgBody).await
904}
905
906/// Stock string: `Cannot login as \"%1$s\". Please check...`.
907pub(crate) async fn cannot_login(context: &Context, user: &str) -> String {
908    translated(context, StockMessage::CannotLogin)
909        .await
910        .replace1(user)
911}
912
913/// Stock string: `Location streaming enabled.`.
914pub(crate) async fn msg_location_enabled(context: &Context) -> String {
915    translated(context, StockMessage::MsgLocationEnabled).await
916}
917
918/// Stock string: `Location streaming enabled by ...`.
919pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
920    if contact == ContactId::SELF {
921        translated(context, StockMessage::MsgYouEnabledLocation).await
922    } else {
923        translated(context, StockMessage::MsgLocationEnabledBy)
924            .await
925            .replace1(&contact.get_stock_name(context).await)
926    }
927}
928
929/// Stock string: `Location streaming disabled.`.
930pub(crate) async fn msg_location_disabled(context: &Context) -> String {
931    translated(context, StockMessage::MsgLocationDisabled).await
932}
933
934/// Stock string: `Location`.
935pub(crate) async fn location(context: &Context) -> String {
936    translated(context, StockMessage::Location).await
937}
938
939/// Stock string: `Sticker`.
940pub(crate) async fn sticker(context: &Context) -> String {
941    translated(context, StockMessage::Sticker).await
942}
943
944/// Stock string: `Device messages`.
945pub(crate) async fn device_messages(context: &Context) -> String {
946    translated(context, StockMessage::DeviceMessages).await
947}
948
949/// Stock string: `Saved messages`.
950pub(crate) async fn saved_messages(context: &Context) -> String {
951    translated(context, StockMessage::SavedMessages).await
952}
953
954/// Stock string: `Messages in this chat are generated locally by...`.
955pub(crate) async fn device_messages_hint(context: &Context) -> String {
956    translated(context, StockMessage::DeviceMessagesHint).await
957}
958
959/// Stock string: `Welcome to Delta Chat! – ...`.
960pub(crate) async fn welcome_message(context: &Context) -> String {
961    translated(context, StockMessage::WelcomeMessage).await
962}
963
964/// Stock string: `Message from %1$s`.
965// TODO: This can compute `self_name` itself instead of asking the caller to do this.
966pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
967    translated(context, StockMessage::SubjectForNewContact)
968        .await
969        .replace1(self_name)
970}
971
972/// Stock string: `Message deletion timer is disabled.`.
973pub(crate) async fn msg_ephemeral_timer_disabled(
974    context: &Context,
975    by_contact: ContactId,
976) -> String {
977    if by_contact == ContactId::SELF {
978        translated(context, StockMessage::MsgYouDisabledEphemeralTimer).await
979    } else {
980        translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
981            .await
982            .replace1(&by_contact.get_stock_name(context).await)
983    }
984}
985
986/// Stock string: `Message deletion timer is set to %1$s s.`.
987pub(crate) async fn msg_ephemeral_timer_enabled(
988    context: &Context,
989    timer: &str,
990    by_contact: ContactId,
991) -> String {
992    if by_contact == ContactId::SELF {
993        translated(context, StockMessage::MsgYouEnabledEphemeralTimer)
994            .await
995            .replace1(timer)
996    } else {
997        translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
998            .await
999            .replace1(timer)
1000            .replace2(&by_contact.get_stock_name(context).await)
1001    }
1002}
1003
1004/// Stock string: `Message deletion timer is set to 1 minute.`.
1005pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: ContactId) -> String {
1006    if by_contact == ContactId::SELF {
1007        translated(context, StockMessage::MsgYouEphemeralTimerMinute).await
1008    } else {
1009        translated(context, StockMessage::MsgEphemeralTimerMinuteBy)
1010            .await
1011            .replace1(&by_contact.get_stock_name(context).await)
1012    }
1013}
1014
1015/// Stock string: `Message deletion timer is set to 1 hour.`.
1016pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
1017    if by_contact == ContactId::SELF {
1018        translated(context, StockMessage::MsgYouEphemeralTimerHour).await
1019    } else {
1020        translated(context, StockMessage::MsgEphemeralTimerHourBy)
1021            .await
1022            .replace1(&by_contact.get_stock_name(context).await)
1023    }
1024}
1025
1026/// Stock string: `Message deletion timer is set to 1 day.`.
1027pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
1028    if by_contact == ContactId::SELF {
1029        translated(context, StockMessage::MsgYouEphemeralTimerDay).await
1030    } else {
1031        translated(context, StockMessage::MsgEphemeralTimerDayBy)
1032            .await
1033            .replace1(&by_contact.get_stock_name(context).await)
1034    }
1035}
1036
1037/// Stock string: `Message deletion timer is set to 1 week.`.
1038pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
1039    if by_contact == ContactId::SELF {
1040        translated(context, StockMessage::MsgYouEphemeralTimerWeek).await
1041    } else {
1042        translated(context, StockMessage::MsgEphemeralTimerWeekBy)
1043            .await
1044            .replace1(&by_contact.get_stock_name(context).await)
1045    }
1046}
1047
1048/// Stock string: `Message deletion timer is set to 1 year.`.
1049pub(crate) async fn msg_ephemeral_timer_year(context: &Context, by_contact: ContactId) -> String {
1050    if by_contact == ContactId::SELF {
1051        translated(context, StockMessage::MsgYouEphemeralTimerYear).await
1052    } else {
1053        translated(context, StockMessage::MsgEphemeralTimerYearBy)
1054            .await
1055            .replace1(&by_contact.get_stock_name(context).await)
1056    }
1057}
1058
1059/// Stock string: `Error:\n\n“%1$s”`.
1060pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
1061    translated(context, StockMessage::ConfigurationFailed)
1062        .await
1063        .replace1(details)
1064}
1065
1066/// Stock string: `⚠️ Date or time of your device seem to be inaccurate (%1$s)...`.
1067// TODO: This could compute now itself.
1068pub(crate) async fn bad_time_msg_body(context: &Context, now: &str) -> String {
1069    translated(context, StockMessage::BadTimeMsgBody)
1070        .await
1071        .replace1(now)
1072}
1073
1074/// Stock string: `⚠️ Your Delta Chat version might be outdated...`.
1075pub(crate) async fn update_reminder_msg_body(context: &Context) -> String {
1076    translated(context, StockMessage::UpdateReminderMsgBody).await
1077}
1078
1079/// Stock string: `Could not find your mail server...`.
1080pub(crate) async fn error_no_network(context: &Context) -> String {
1081    translated(context, StockMessage::ErrorNoNetwork).await
1082}
1083
1084/// Stock string: `Messages are end-to-end encrypted.`
1085pub(crate) async fn messages_e2e_encrypted(context: &Context) -> String {
1086    translated(context, StockMessage::ChatProtectionEnabled).await
1087}
1088
1089/// Stock string: `Reply`.
1090pub(crate) async fn reply_noun(context: &Context) -> String {
1091    translated(context, StockMessage::ReplyNoun).await
1092}
1093
1094/// Stock string: `You deleted the \"Saved messages\" chat...`.
1095pub(crate) async fn self_deleted_msg_body(context: &Context) -> String {
1096    translated(context, StockMessage::SelfDeletedMsgBody).await
1097}
1098
1099/// Stock string: `⚠️ The "Delete messages from server" feature now also...`.
1100pub(crate) async fn delete_server_turned_off(context: &Context) -> String {
1101    translated(context, StockMessage::DeleteServerTurnedOff).await
1102}
1103
1104/// Stock string: `Message deletion timer is set to %1$s minutes.`.
1105pub(crate) async fn msg_ephemeral_timer_minutes(
1106    context: &Context,
1107    minutes: &str,
1108    by_contact: ContactId,
1109) -> String {
1110    if by_contact == ContactId::SELF {
1111        translated(context, StockMessage::MsgYouEphemeralTimerMinutes)
1112            .await
1113            .replace1(minutes)
1114    } else {
1115        translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
1116            .await
1117            .replace1(minutes)
1118            .replace2(&by_contact.get_stock_name(context).await)
1119    }
1120}
1121
1122/// Stock string: `Message deletion timer is set to %1$s hours.`.
1123pub(crate) async fn msg_ephemeral_timer_hours(
1124    context: &Context,
1125    hours: &str,
1126    by_contact: ContactId,
1127) -> String {
1128    if by_contact == ContactId::SELF {
1129        translated(context, StockMessage::MsgYouEphemeralTimerHours)
1130            .await
1131            .replace1(hours)
1132    } else {
1133        translated(context, StockMessage::MsgEphemeralTimerHoursBy)
1134            .await
1135            .replace1(hours)
1136            .replace2(&by_contact.get_stock_name(context).await)
1137    }
1138}
1139
1140/// Stock string: `Message deletion timer is set to %1$s days.`.
1141pub(crate) async fn msg_ephemeral_timer_days(
1142    context: &Context,
1143    days: &str,
1144    by_contact: ContactId,
1145) -> String {
1146    if by_contact == ContactId::SELF {
1147        translated(context, StockMessage::MsgYouEphemeralTimerDays)
1148            .await
1149            .replace1(days)
1150    } else {
1151        translated(context, StockMessage::MsgEphemeralTimerDaysBy)
1152            .await
1153            .replace1(days)
1154            .replace2(&by_contact.get_stock_name(context).await)
1155    }
1156}
1157
1158/// Stock string: `Message deletion timer is set to %1$s weeks.`.
1159pub(crate) async fn msg_ephemeral_timer_weeks(
1160    context: &Context,
1161    weeks: &str,
1162    by_contact: ContactId,
1163) -> String {
1164    if by_contact == ContactId::SELF {
1165        translated(context, StockMessage::MsgYouEphemeralTimerWeeks)
1166            .await
1167            .replace1(weeks)
1168    } else {
1169        translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
1170            .await
1171            .replace1(weeks)
1172            .replace2(&by_contact.get_stock_name(context).await)
1173    }
1174}
1175
1176/// Stock string: `Forwarded`.
1177pub(crate) async fn forwarded(context: &Context) -> String {
1178    translated(context, StockMessage::Forwarded).await
1179}
1180
1181/// Stock string: `⚠️ Your provider's storage is about to exceed...`.
1182pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
1183    translated(context, StockMessage::QuotaExceedingMsgBody)
1184        .await
1185        .replace1(&format!("{highest_usage}"))
1186        .replace("%%", "%")
1187}
1188
1189/// Stock string: `%1$s message` with placeholder replaced by human-readable size.
1190pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32) -> String {
1191    let size = &format_size(org_bytes, BINARY);
1192    translated(context, StockMessage::PartialDownloadMsgBody)
1193        .await
1194        .replace1(size)
1195}
1196
1197/// Stock string: `Download maximum available until %1$s`.
1198pub(crate) async fn download_availability(context: &Context, timestamp: i64) -> String {
1199    translated(context, StockMessage::DownloadAvailability)
1200        .await
1201        .replace1(&timestamp_to_str(timestamp))
1202}
1203
1204/// Stock string: `Incoming Messages`.
1205pub(crate) async fn incoming_messages(context: &Context) -> String {
1206    translated(context, StockMessage::IncomingMessages).await
1207}
1208
1209/// Stock string: `Outgoing Messages`.
1210pub(crate) async fn outgoing_messages(context: &Context) -> String {
1211    translated(context, StockMessage::OutgoingMessages).await
1212}
1213
1214/// Stock string: `Storage on %1$s`.
1215/// `%1$s` will be replaced by the domain of the configured email-address.
1216pub(crate) async fn storage_on_domain(context: &Context, domain: &str) -> String {
1217    translated(context, StockMessage::StorageOnDomain)
1218        .await
1219        .replace1(domain)
1220}
1221
1222/// Stock string: `Not connected`.
1223pub(crate) async fn not_connected(context: &Context) -> String {
1224    translated(context, StockMessage::NotConnected).await
1225}
1226
1227/// Stock string: `Connected`.
1228pub(crate) async fn connected(context: &Context) -> String {
1229    translated(context, StockMessage::Connected).await
1230}
1231
1232/// Stock string: `Connecting…`.
1233pub(crate) async fn connecting(context: &Context) -> String {
1234    translated(context, StockMessage::Connecting).await
1235}
1236
1237/// Stock string: `Updating…`.
1238pub(crate) async fn updating(context: &Context) -> String {
1239    translated(context, StockMessage::Updating).await
1240}
1241
1242/// Stock string: `Sending…`.
1243pub(crate) async fn sending(context: &Context) -> String {
1244    translated(context, StockMessage::Sending).await
1245}
1246
1247/// Stock string: `Your last message was sent successfully.`.
1248pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
1249    translated(context, StockMessage::LastMsgSentSuccessfully).await
1250}
1251
1252/// Stock string: `Error: %1$s…`.
1253/// `%1$s` will be replaced by a possibly more detailed, typically english, error description.
1254pub(crate) async fn error(context: &Context, error: &str) -> String {
1255    translated(context, StockMessage::Error)
1256        .await
1257        .replace1(error)
1258}
1259
1260/// Stock string: `Not supported by your provider.`.
1261pub(crate) async fn not_supported_by_provider(context: &Context) -> String {
1262    translated(context, StockMessage::NotSupportedByProvider).await
1263}
1264
1265/// Stock string: `Messages`.
1266/// Used as a subtitle in quota context; can be plural always.
1267pub(crate) async fn messages(context: &Context) -> String {
1268    translated(context, StockMessage::Messages).await
1269}
1270
1271/// Stock string: `%1$s of %2$s used`.
1272pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
1273    translated(context, StockMessage::PartOfTotallUsed)
1274        .await
1275        .replace1(part)
1276        .replace2(total)
1277}
1278
1279/// Stock string: `⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet. Tap to learn more.`.
1280pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
1281    translated(context, StockMessage::InvalidUnencryptedMail)
1282        .await
1283        .replace1(provider)
1284}
1285
1286/// Stock string: `The attachment contains anonymous usage statistics, which helps us improve Delta Chat. Thank you!`
1287pub(crate) async fn stats_msg_body(context: &Context) -> String {
1288    translated(context, StockMessage::StatsMsgBody).await
1289}
1290
1291pub(crate) async fn aeap_explanation_and_link(
1292    context: &Context,
1293    old_addr: &str,
1294    new_addr: &str,
1295) -> String {
1296    translated(context, StockMessage::AeapExplanationAndLink)
1297        .await
1298        .replace1(old_addr)
1299        .replace2(new_addr)
1300}
1301
1302/// Stock string: `Others will only see this group after you sent a first message.`.
1303pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
1304    translated(context, StockMessage::NewGroupSendFirstMessage).await
1305}
1306
1307/// Text to put in the [`Qr::Backup2`] rendered SVG image.
1308///
1309/// The default is "Scan to set up second device for NAME".
1310/// The account name (or address as fallback) are looked up from the context.
1311///
1312/// [`Qr::Backup2`]: crate::qr::Qr::Backup2
1313pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
1314    let name = if let Some(name) = context.get_config(Config::Displayname).await? {
1315        name
1316    } else {
1317        context.get_primary_self_addr().await?
1318    };
1319    Ok(translated(context, StockMessage::BackupTransferQr)
1320        .await
1321        .replace1(&name))
1322}
1323
1324pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String {
1325    translated(context, StockMessage::BackupTransferMsgBody).await
1326}
1327
1328/// Stock string: `Proxy Enabled`.
1329pub(crate) async fn proxy_enabled(context: &Context) -> String {
1330    translated(context, StockMessage::ProxyEnabled).await
1331}
1332
1333/// Stock string: `You are using a proxy. If you're having trouble connecting, try a different proxy.`.
1334pub(crate) async fn proxy_description(context: &Context) -> String {
1335    translated(context, StockMessage::ProxyEnabledDescription).await
1336}
1337
1338impl Context {
1339    /// Set the stock string for the [StockMessage].
1340    ///
1341    pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1342        self.translated_stockstrings
1343            .set_stock_translation(id, stockstring)
1344            .await?;
1345        Ok(())
1346    }
1347
1348    pub(crate) async fn update_device_chats(&self) -> Result<()> {
1349        if self.get_config_bool(Config::Bot).await? {
1350            return Ok(());
1351        }
1352
1353        // create saved-messages chat; we do this only once, if the user has deleted the chat,
1354        // he can recreate it manually (make sure we do not re-add it when configure() was called a second time)
1355        if !self.sql.get_raw_config_bool("self-chat-added").await? {
1356            self.sql
1357                .set_raw_config_bool("self-chat-added", true)
1358                .await?;
1359            ChatId::create_for_contact(self, ContactId::SELF).await?;
1360        }
1361
1362        // add welcome-messages. by the label, this is done only once,
1363        // if the user has deleted the message or the chat, it is not added again.
1364        let image = include_bytes!("../assets/welcome-image.jpg");
1365        let blob = BlobObject::create_and_deduplicate_from_bytes(self, image, "welcome.jpg")?;
1366        let mut msg = Message::new(Viewtype::Image);
1367        msg.param.set(Param::File, blob.as_name());
1368        msg.param.set(Param::Filename, "welcome-image.jpg");
1369        chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
1370
1371        let mut msg = Message::new_text(welcome_message(self).await);
1372        chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
1373        Ok(())
1374    }
1375}
1376
1377impl Accounts {
1378    /// Set the stock string for the [StockMessage].
1379    ///
1380    pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1381        self.stockstrings
1382            .set_stock_translation(id, stockstring)
1383            .await?;
1384        Ok(())
1385    }
1386}
1387
1388#[cfg(test)]
1389mod stock_str_tests;