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(
436        fallback = "The attachment contains anonymous usage statistics, which helps us improve Delta Chat. Thank you!"
437    ))]
438    StatsMsgBody = 210,
439}
440
441impl StockMessage {
442    /// Default untranslated strings for stock messages.
443    ///
444    /// These could be used in logging calls, so no logging here.
445    fn fallback(self) -> &'static str {
446        self.get_str("fallback").unwrap_or_default()
447    }
448}
449
450impl Default for StockStrings {
451    fn default() -> Self {
452        StockStrings::new()
453    }
454}
455
456impl StockStrings {
457    /// Creates a new translated string storage.
458    pub fn new() -> Self {
459        Self {
460            translated_stockstrings: Arc::new(RwLock::new(Default::default())),
461        }
462    }
463
464    async fn translated(&self, id: StockMessage) -> String {
465        self.translated_stockstrings
466            .read()
467            .await
468            .get(&(id as usize))
469            .map(AsRef::as_ref)
470            .unwrap_or_else(|| id.fallback())
471            .to_string()
472    }
473
474    async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
475        if stockstring.contains("%1") && !id.fallback().contains("%1") {
476            bail!(
477                "translation {} contains invalid %1 placeholder, default is {}",
478                stockstring,
479                id.fallback()
480            );
481        }
482        if stockstring.contains("%2") && !id.fallback().contains("%2") {
483            bail!(
484                "translation {} contains invalid %2 placeholder, default is {}",
485                stockstring,
486                id.fallback()
487            );
488        }
489        self.translated_stockstrings
490            .write()
491            .await
492            .insert(id as usize, stockstring);
493        Ok(())
494    }
495}
496
497async fn translated(context: &Context, id: StockMessage) -> String {
498    context.translated_stockstrings.translated(id).await
499}
500
501/// Helper trait only meant to be implemented for [`String`].
502trait StockStringMods: AsRef<str> + Sized {
503    /// Substitutes the first replacement value if one is present.
504    fn replace1(&self, replacement: &str) -> String {
505        self.as_ref()
506            .replacen("%1$s", replacement, 1)
507            .replacen("%1$d", replacement, 1)
508            .replacen("%1$@", replacement, 1)
509    }
510
511    /// Substitutes the second replacement value if one is present.
512    ///
513    /// Be aware you probably should have also called [`StockStringMods::replace1`] if
514    /// you are calling this.
515    fn replace2(&self, replacement: &str) -> String {
516        self.as_ref()
517            .replacen("%2$s", replacement, 1)
518            .replacen("%2$d", replacement, 1)
519            .replacen("%2$@", replacement, 1)
520    }
521
522    /// Substitutes the third replacement value if one is present.
523    ///
524    /// Be aware you probably should have also called [`StockStringMods::replace1`] and
525    /// [`StockStringMods::replace2`] if you are calling this.
526    fn replace3(&self, replacement: &str) -> String {
527        self.as_ref()
528            .replacen("%3$s", replacement, 1)
529            .replacen("%3$d", replacement, 1)
530            .replacen("%3$@", replacement, 1)
531    }
532}
533
534impl ContactId {
535    /// Get contact name, e.g. `Bob`, or `bob@example.net` if no name is set.
536    async fn get_stock_name(self, context: &Context) -> String {
537        Contact::get_by_id(context, self)
538            .await
539            .map(|contact| contact.get_display_name().to_string())
540            .unwrap_or_else(|_| self.to_string())
541    }
542}
543
544impl StockStringMods for String {}
545
546/// Stock string: `No messages.`.
547pub(crate) async fn no_messages(context: &Context) -> String {
548    translated(context, StockMessage::NoMessages).await
549}
550
551/// Stock string: `Me`.
552pub(crate) async fn self_msg(context: &Context) -> String {
553    translated(context, StockMessage::SelfMsg).await
554}
555
556/// Stock string: `Draft`.
557pub(crate) async fn draft(context: &Context) -> String {
558    translated(context, StockMessage::Draft).await
559}
560
561/// Stock string: `Voice message`.
562pub(crate) async fn voice_message(context: &Context) -> String {
563    translated(context, StockMessage::VoiceMessage).await
564}
565
566/// Stock string: `Image`.
567pub(crate) async fn image(context: &Context) -> String {
568    translated(context, StockMessage::Image).await
569}
570
571/// Stock string: `Video`.
572pub(crate) async fn video(context: &Context) -> String {
573    translated(context, StockMessage::Video).await
574}
575
576/// Stock string: `Audio`.
577pub(crate) async fn audio(context: &Context) -> String {
578    translated(context, StockMessage::Audio).await
579}
580
581/// Stock string: `File`.
582pub(crate) async fn file(context: &Context) -> String {
583    translated(context, StockMessage::File).await
584}
585
586/// Stock string: `Group name changed from "%1$s" to "%2$s".`.
587pub(crate) async fn msg_grp_name(
588    context: &Context,
589    from_group: &str,
590    to_group: &str,
591    by_contact: ContactId,
592) -> String {
593    if by_contact == ContactId::SELF {
594        translated(context, StockMessage::MsgYouChangedGrpName)
595            .await
596            .replace1(from_group)
597            .replace2(to_group)
598    } else {
599        translated(context, StockMessage::MsgGrpNameChangedBy)
600            .await
601            .replace1(from_group)
602            .replace2(to_group)
603            .replace3(&by_contact.get_stock_name(context).await)
604    }
605}
606
607pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
608    if by_contact == ContactId::SELF {
609        translated(context, StockMessage::MsgYouChangedGrpImg).await
610    } else {
611        translated(context, StockMessage::MsgGrpImgChangedBy)
612            .await
613            .replace1(&by_contact.get_stock_name(context).await)
614    }
615}
616
617/// Stock string: `I added member %1$s.`.
618/// This one is for sending in group chats.
619///
620/// The `added_member_addr` parameter should be an email address and is looked up in the
621/// contacts to combine with the authorized display name.
622pub(crate) async fn msg_add_member_remote(context: &Context, added_member_addr: &str) -> String {
623    let addr = added_member_addr;
624    let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
625        Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
626            .await
627            .map(|contact| contact.get_authname_or_addr())
628            .unwrap_or_else(|_| addr.to_string()),
629        _ => addr.to_string(),
630    };
631    translated(context, StockMessage::MsgIAddMember)
632        .await
633        .replace1(whom)
634}
635
636/// Stock string: `You added member %1$s.` or `Member %1$s added by %2$s.`.
637///
638/// The `added_member_addr` parameter should be an email address and is looked up in the
639/// contacts to combine with the display name.
640pub(crate) async fn msg_add_member_local(
641    context: &Context,
642    added_member: ContactId,
643    by_contact: ContactId,
644) -> String {
645    let whom = added_member.get_stock_name(context).await;
646    if by_contact == ContactId::UNDEFINED {
647        translated(context, StockMessage::MsgAddMember)
648            .await
649            .replace1(&whom)
650    } else if by_contact == ContactId::SELF {
651        translated(context, StockMessage::MsgYouAddMember)
652            .await
653            .replace1(&whom)
654    } else {
655        translated(context, StockMessage::MsgAddMemberBy)
656            .await
657            .replace1(&whom)
658            .replace2(&by_contact.get_stock_name(context).await)
659    }
660}
661
662/// Stock string: `I removed member %1$s.`.
663///
664/// The `removed_member_addr` parameter should be an email address and is looked up in
665/// the contacts to combine with the display name.
666pub(crate) async fn msg_del_member_remote(context: &Context, removed_member_addr: &str) -> String {
667    let addr = removed_member_addr;
668    let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
669        Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
670            .await
671            .map(|contact| contact.get_authname_or_addr())
672            .unwrap_or_else(|_| addr.to_string()),
673        _ => addr.to_string(),
674    };
675    translated(context, StockMessage::MsgIDelMember)
676        .await
677        .replace1(whom)
678}
679
680/// Stock string: `I added member %1$s.` or `Member %1$s removed by %2$s.`.
681///
682/// The `removed_member_addr` parameter should be an email address and is looked up in
683/// the contacts to combine with the display name.
684pub(crate) async fn msg_del_member_local(
685    context: &Context,
686    removed_member: ContactId,
687    by_contact: ContactId,
688) -> String {
689    let whom = removed_member.get_stock_name(context).await;
690    if by_contact == ContactId::UNDEFINED {
691        translated(context, StockMessage::MsgDelMember)
692            .await
693            .replace1(&whom)
694    } else if by_contact == ContactId::SELF {
695        translated(context, StockMessage::MsgYouDelMember)
696            .await
697            .replace1(&whom)
698    } else {
699        translated(context, StockMessage::MsgDelMemberBy)
700            .await
701            .replace1(&whom)
702            .replace2(&by_contact.get_stock_name(context).await)
703    }
704}
705
706/// Stock string: `I left the group.`.
707pub(crate) async fn msg_group_left_remote(context: &Context) -> String {
708    translated(context, StockMessage::MsgILeftGroup).await
709}
710
711/// Stock string: `You left the group.` or `Group left by %1$s.`.
712pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
713    if by_contact == ContactId::SELF {
714        translated(context, StockMessage::MsgYouLeftGroup).await
715    } else {
716        translated(context, StockMessage::MsgGroupLeftBy)
717            .await
718            .replace1(&by_contact.get_stock_name(context).await)
719    }
720}
721
722/// Stock string: `You left the channel.`
723pub(crate) async fn msg_you_left_broadcast(context: &Context) -> String {
724    translated(context, StockMessage::MsgYouLeftBroadcast).await
725}
726
727/// Stock string: `You reacted %1$s to "%2$s"` or `%1$s reacted %2$s to "%3$s"`.
728pub(crate) async fn msg_reacted(
729    context: &Context,
730    by_contact: ContactId,
731    reaction: &str,
732    summary: &str,
733) -> String {
734    if by_contact == ContactId::SELF {
735        translated(context, StockMessage::MsgYouReacted)
736            .await
737            .replace1(reaction)
738            .replace2(summary)
739    } else {
740        translated(context, StockMessage::MsgReactedBy)
741            .await
742            .replace1(&by_contact.get_stock_name(context).await)
743            .replace2(reaction)
744            .replace3(summary)
745    }
746}
747
748/// Stock string: `GIF`.
749pub(crate) async fn gif(context: &Context) -> String {
750    translated(context, StockMessage::Gif).await
751}
752
753/// Stock string: `End-to-end encryption available.`.
754pub(crate) async fn e2e_available(context: &Context) -> String {
755    translated(context, StockMessage::E2eAvailable).await
756}
757
758/// Stock string: `No encryption.`.
759pub(crate) async fn encr_none(context: &Context) -> String {
760    translated(context, StockMessage::EncrNone).await
761}
762
763/// Stock string: `Fingerprints`.
764pub(crate) async fn finger_prints(context: &Context) -> String {
765    translated(context, StockMessage::FingerPrints).await
766}
767
768/// Stock string: `Group image deleted.`.
769pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
770    if by_contact == ContactId::SELF {
771        translated(context, StockMessage::MsgYouDeletedGrpImg).await
772    } else {
773        translated(context, StockMessage::MsgGrpImgDeletedBy)
774            .await
775            .replace1(&by_contact.get_stock_name(context).await)
776    }
777}
778
779/// Stock string: `%1$s invited you to join this group. Waiting for the device of %2$s to reply…`.
780pub(crate) async fn secure_join_started(
781    context: &Context,
782    inviter_contact_id: ContactId,
783) -> String {
784    if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
785        translated(context, StockMessage::SecureJoinStarted)
786            .await
787            .replace1(contact.get_display_name())
788            .replace2(contact.get_display_name())
789    } else {
790        format!("secure_join_started: unknown contact {inviter_contact_id}")
791    }
792}
793
794/// Stock string: `%1$s replied, waiting for being added to the group…`.
795pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
796    translated(context, StockMessage::SecureJoinReplies)
797        .await
798        .replace1(&contact_id.get_stock_name(context).await)
799}
800
801/// Stock string: `Establishing guaranteed end-to-end encryption, please wait…`.
802pub(crate) async fn securejoin_wait(context: &Context) -> String {
803    translated(context, StockMessage::SecurejoinWait).await
804}
805
806/// Stock string: `❤️ Seems you're enjoying Delta Chat!`…
807pub(crate) async fn donation_request(context: &Context) -> String {
808    translated(context, StockMessage::DonationRequest).await
809}
810
811/// Stock string: `Outgoing call`.
812pub(crate) async fn outgoing_call(context: &Context) -> String {
813    translated(context, StockMessage::OutgoingCall).await
814}
815
816/// Stock string: `Incoming call`.
817pub(crate) async fn incoming_call(context: &Context) -> String {
818    translated(context, StockMessage::IncomingCall).await
819}
820
821/// Stock string: `Declined call`.
822pub(crate) async fn declined_call(context: &Context) -> String {
823    translated(context, StockMessage::DeclinedCall).await
824}
825
826/// Stock string: `Canceled call`.
827pub(crate) async fn canceled_call(context: &Context) -> String {
828    translated(context, StockMessage::CanceledCall).await
829}
830
831/// Stock string: `Missed call`.
832pub(crate) async fn missed_call(context: &Context) -> String {
833    translated(context, StockMessage::MissedCall).await
834}
835
836/// Stock string: `Scan to chat with %1$s`.
837pub(crate) async fn setup_contact_qr_description(
838    context: &Context,
839    display_name: &str,
840    addr: &str,
841) -> String {
842    let name = if display_name.is_empty() {
843        addr.to_owned()
844    } else {
845        display_name.to_owned()
846    };
847    translated(context, StockMessage::SetupContactQRDescription)
848        .await
849        .replace1(&name)
850}
851
852/// Stock string: `Scan to join group %1$s`.
853pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
854    translated(context, StockMessage::SecureJoinGroupQRDescription)
855        .await
856        .replace1(chat.get_name())
857}
858
859/// Stock string: `Scan to join channel %1$s`.
860pub(crate) async fn secure_join_broadcast_qr_description(context: &Context, chat: &Chat) -> String {
861    translated(context, StockMessage::SecureJoinBrodcastQRDescription)
862        .await
863        .replace1(chat.get_name())
864}
865
866/// Stock string: `%1$s verified.`.
867#[allow(dead_code)]
868pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
869    let addr = contact.get_display_name();
870    translated(context, StockMessage::ContactVerified)
871        .await
872        .replace1(addr)
873}
874
875/// Stock string: `Archived chats`.
876pub(crate) async fn archived_chats(context: &Context) -> String {
877    translated(context, StockMessage::ArchivedChats).await
878}
879
880/// Stock string: `Multi Device Synchronization`.
881pub(crate) async fn sync_msg_subject(context: &Context) -> String {
882    translated(context, StockMessage::SyncMsgSubject).await
883}
884
885/// Stock string: `This message is used to synchronize data between your devices.`.
886pub(crate) async fn sync_msg_body(context: &Context) -> String {
887    translated(context, StockMessage::SyncMsgBody).await
888}
889
890/// Stock string: `Cannot login as \"%1$s\". Please check...`.
891pub(crate) async fn cannot_login(context: &Context, user: &str) -> String {
892    translated(context, StockMessage::CannotLogin)
893        .await
894        .replace1(user)
895}
896
897/// Stock string: `Location streaming enabled.`.
898pub(crate) async fn msg_location_enabled(context: &Context) -> String {
899    translated(context, StockMessage::MsgLocationEnabled).await
900}
901
902/// Stock string: `Location streaming enabled by ...`.
903pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
904    if contact == ContactId::SELF {
905        translated(context, StockMessage::MsgYouEnabledLocation).await
906    } else {
907        translated(context, StockMessage::MsgLocationEnabledBy)
908            .await
909            .replace1(&contact.get_stock_name(context).await)
910    }
911}
912
913/// Stock string: `Location streaming disabled.`.
914pub(crate) async fn msg_location_disabled(context: &Context) -> String {
915    translated(context, StockMessage::MsgLocationDisabled).await
916}
917
918/// Stock string: `Location`.
919pub(crate) async fn location(context: &Context) -> String {
920    translated(context, StockMessage::Location).await
921}
922
923/// Stock string: `Sticker`.
924pub(crate) async fn sticker(context: &Context) -> String {
925    translated(context, StockMessage::Sticker).await
926}
927
928/// Stock string: `Device messages`.
929pub(crate) async fn device_messages(context: &Context) -> String {
930    translated(context, StockMessage::DeviceMessages).await
931}
932
933/// Stock string: `Saved messages`.
934pub(crate) async fn saved_messages(context: &Context) -> String {
935    translated(context, StockMessage::SavedMessages).await
936}
937
938/// Stock string: `Messages in this chat are generated locally by...`.
939pub(crate) async fn device_messages_hint(context: &Context) -> String {
940    translated(context, StockMessage::DeviceMessagesHint).await
941}
942
943/// Stock string: `Welcome to Delta Chat! – ...`.
944pub(crate) async fn welcome_message(context: &Context) -> String {
945    translated(context, StockMessage::WelcomeMessage).await
946}
947
948/// Stock string: `Message from %1$s`.
949// TODO: This can compute `self_name` itself instead of asking the caller to do this.
950pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
951    translated(context, StockMessage::SubjectForNewContact)
952        .await
953        .replace1(self_name)
954}
955
956/// Stock string: `Message deletion timer is disabled.`.
957pub(crate) async fn msg_ephemeral_timer_disabled(
958    context: &Context,
959    by_contact: ContactId,
960) -> String {
961    if by_contact == ContactId::SELF {
962        translated(context, StockMessage::MsgYouDisabledEphemeralTimer).await
963    } else {
964        translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
965            .await
966            .replace1(&by_contact.get_stock_name(context).await)
967    }
968}
969
970/// Stock string: `Message deletion timer is set to %1$s s.`.
971pub(crate) async fn msg_ephemeral_timer_enabled(
972    context: &Context,
973    timer: &str,
974    by_contact: ContactId,
975) -> String {
976    if by_contact == ContactId::SELF {
977        translated(context, StockMessage::MsgYouEnabledEphemeralTimer)
978            .await
979            .replace1(timer)
980    } else {
981        translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
982            .await
983            .replace1(timer)
984            .replace2(&by_contact.get_stock_name(context).await)
985    }
986}
987
988/// Stock string: `Message deletion timer is set to 1 minute.`.
989pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: ContactId) -> String {
990    if by_contact == ContactId::SELF {
991        translated(context, StockMessage::MsgYouEphemeralTimerMinute).await
992    } else {
993        translated(context, StockMessage::MsgEphemeralTimerMinuteBy)
994            .await
995            .replace1(&by_contact.get_stock_name(context).await)
996    }
997}
998
999/// Stock string: `Message deletion timer is set to 1 hour.`.
1000pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
1001    if by_contact == ContactId::SELF {
1002        translated(context, StockMessage::MsgYouEphemeralTimerHour).await
1003    } else {
1004        translated(context, StockMessage::MsgEphemeralTimerHourBy)
1005            .await
1006            .replace1(&by_contact.get_stock_name(context).await)
1007    }
1008}
1009
1010/// Stock string: `Message deletion timer is set to 1 day.`.
1011pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
1012    if by_contact == ContactId::SELF {
1013        translated(context, StockMessage::MsgYouEphemeralTimerDay).await
1014    } else {
1015        translated(context, StockMessage::MsgEphemeralTimerDayBy)
1016            .await
1017            .replace1(&by_contact.get_stock_name(context).await)
1018    }
1019}
1020
1021/// Stock string: `Message deletion timer is set to 1 week.`.
1022pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
1023    if by_contact == ContactId::SELF {
1024        translated(context, StockMessage::MsgYouEphemeralTimerWeek).await
1025    } else {
1026        translated(context, StockMessage::MsgEphemeralTimerWeekBy)
1027            .await
1028            .replace1(&by_contact.get_stock_name(context).await)
1029    }
1030}
1031
1032/// Stock string: `Message deletion timer is set to 1 year.`.
1033pub(crate) async fn msg_ephemeral_timer_year(context: &Context, by_contact: ContactId) -> String {
1034    if by_contact == ContactId::SELF {
1035        translated(context, StockMessage::MsgYouEphemeralTimerYear).await
1036    } else {
1037        translated(context, StockMessage::MsgEphemeralTimerYearBy)
1038            .await
1039            .replace1(&by_contact.get_stock_name(context).await)
1040    }
1041}
1042
1043/// Stock string: `Error:\n\n“%1$s”`.
1044pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
1045    translated(context, StockMessage::ConfigurationFailed)
1046        .await
1047        .replace1(details)
1048}
1049
1050/// Stock string: `⚠️ Date or time of your device seem to be inaccurate (%1$s)...`.
1051// TODO: This could compute now itself.
1052pub(crate) async fn bad_time_msg_body(context: &Context, now: &str) -> String {
1053    translated(context, StockMessage::BadTimeMsgBody)
1054        .await
1055        .replace1(now)
1056}
1057
1058/// Stock string: `⚠️ Your Delta Chat version might be outdated...`.
1059pub(crate) async fn update_reminder_msg_body(context: &Context) -> String {
1060    translated(context, StockMessage::UpdateReminderMsgBody).await
1061}
1062
1063/// Stock string: `Could not find your mail server...`.
1064pub(crate) async fn error_no_network(context: &Context) -> String {
1065    translated(context, StockMessage::ErrorNoNetwork).await
1066}
1067
1068/// Stock string: `Messages are end-to-end encrypted.`
1069pub(crate) async fn messages_e2e_encrypted(context: &Context) -> String {
1070    translated(context, StockMessage::ChatProtectionEnabled).await
1071}
1072
1073/// Stock string: `Reply`.
1074pub(crate) async fn reply_noun(context: &Context) -> String {
1075    translated(context, StockMessage::ReplyNoun).await
1076}
1077
1078/// Stock string: `You deleted the \"Saved messages\" chat...`.
1079pub(crate) async fn self_deleted_msg_body(context: &Context) -> String {
1080    translated(context, StockMessage::SelfDeletedMsgBody).await
1081}
1082
1083/// Stock string: `⚠️ The "Delete messages from server" feature now also...`.
1084pub(crate) async fn delete_server_turned_off(context: &Context) -> String {
1085    translated(context, StockMessage::DeleteServerTurnedOff).await
1086}
1087
1088/// Stock string: `Message deletion timer is set to %1$s minutes.`.
1089pub(crate) async fn msg_ephemeral_timer_minutes(
1090    context: &Context,
1091    minutes: &str,
1092    by_contact: ContactId,
1093) -> String {
1094    if by_contact == ContactId::SELF {
1095        translated(context, StockMessage::MsgYouEphemeralTimerMinutes)
1096            .await
1097            .replace1(minutes)
1098    } else {
1099        translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
1100            .await
1101            .replace1(minutes)
1102            .replace2(&by_contact.get_stock_name(context).await)
1103    }
1104}
1105
1106/// Stock string: `Message deletion timer is set to %1$s hours.`.
1107pub(crate) async fn msg_ephemeral_timer_hours(
1108    context: &Context,
1109    hours: &str,
1110    by_contact: ContactId,
1111) -> String {
1112    if by_contact == ContactId::SELF {
1113        translated(context, StockMessage::MsgYouEphemeralTimerHours)
1114            .await
1115            .replace1(hours)
1116    } else {
1117        translated(context, StockMessage::MsgEphemeralTimerHoursBy)
1118            .await
1119            .replace1(hours)
1120            .replace2(&by_contact.get_stock_name(context).await)
1121    }
1122}
1123
1124/// Stock string: `Message deletion timer is set to %1$s days.`.
1125pub(crate) async fn msg_ephemeral_timer_days(
1126    context: &Context,
1127    days: &str,
1128    by_contact: ContactId,
1129) -> String {
1130    if by_contact == ContactId::SELF {
1131        translated(context, StockMessage::MsgYouEphemeralTimerDays)
1132            .await
1133            .replace1(days)
1134    } else {
1135        translated(context, StockMessage::MsgEphemeralTimerDaysBy)
1136            .await
1137            .replace1(days)
1138            .replace2(&by_contact.get_stock_name(context).await)
1139    }
1140}
1141
1142/// Stock string: `Message deletion timer is set to %1$s weeks.`.
1143pub(crate) async fn msg_ephemeral_timer_weeks(
1144    context: &Context,
1145    weeks: &str,
1146    by_contact: ContactId,
1147) -> String {
1148    if by_contact == ContactId::SELF {
1149        translated(context, StockMessage::MsgYouEphemeralTimerWeeks)
1150            .await
1151            .replace1(weeks)
1152    } else {
1153        translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
1154            .await
1155            .replace1(weeks)
1156            .replace2(&by_contact.get_stock_name(context).await)
1157    }
1158}
1159
1160/// Stock string: `Forwarded`.
1161pub(crate) async fn forwarded(context: &Context) -> String {
1162    translated(context, StockMessage::Forwarded).await
1163}
1164
1165/// Stock string: `⚠️ Your provider's storage is about to exceed...`.
1166pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
1167    translated(context, StockMessage::QuotaExceedingMsgBody)
1168        .await
1169        .replace1(&format!("{highest_usage}"))
1170        .replace("%%", "%")
1171}
1172
1173/// Stock string: `%1$s message` with placeholder replaced by human-readable size.
1174pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32) -> String {
1175    let size = &format_size(org_bytes, BINARY);
1176    translated(context, StockMessage::PartialDownloadMsgBody)
1177        .await
1178        .replace1(size)
1179}
1180
1181/// Stock string: `Download maximum available until %1$s`.
1182pub(crate) async fn download_availability(context: &Context, timestamp: i64) -> String {
1183    translated(context, StockMessage::DownloadAvailability)
1184        .await
1185        .replace1(&timestamp_to_str(timestamp))
1186}
1187
1188/// Stock string: `Incoming Messages`.
1189pub(crate) async fn incoming_messages(context: &Context) -> String {
1190    translated(context, StockMessage::IncomingMessages).await
1191}
1192
1193/// Stock string: `Outgoing Messages`.
1194pub(crate) async fn outgoing_messages(context: &Context) -> String {
1195    translated(context, StockMessage::OutgoingMessages).await
1196}
1197
1198/// Stock string: `Storage on %1$s`.
1199/// `%1$s` will be replaced by the domain of the configured email-address.
1200pub(crate) async fn storage_on_domain(context: &Context, domain: &str) -> String {
1201    translated(context, StockMessage::StorageOnDomain)
1202        .await
1203        .replace1(domain)
1204}
1205
1206/// Stock string: `Not connected`.
1207pub(crate) async fn not_connected(context: &Context) -> String {
1208    translated(context, StockMessage::NotConnected).await
1209}
1210
1211/// Stock string: `Connected`.
1212pub(crate) async fn connected(context: &Context) -> String {
1213    translated(context, StockMessage::Connected).await
1214}
1215
1216/// Stock string: `Connecting…`.
1217pub(crate) async fn connecting(context: &Context) -> String {
1218    translated(context, StockMessage::Connecting).await
1219}
1220
1221/// Stock string: `Updating…`.
1222pub(crate) async fn updating(context: &Context) -> String {
1223    translated(context, StockMessage::Updating).await
1224}
1225
1226/// Stock string: `Sending…`.
1227pub(crate) async fn sending(context: &Context) -> String {
1228    translated(context, StockMessage::Sending).await
1229}
1230
1231/// Stock string: `Your last message was sent successfully.`.
1232pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
1233    translated(context, StockMessage::LastMsgSentSuccessfully).await
1234}
1235
1236/// Stock string: `Error: %1$s…`.
1237/// `%1$s` will be replaced by a possibly more detailed, typically english, error description.
1238pub(crate) async fn error(context: &Context, error: &str) -> String {
1239    translated(context, StockMessage::Error)
1240        .await
1241        .replace1(error)
1242}
1243
1244/// Stock string: `Not supported by your provider.`.
1245pub(crate) async fn not_supported_by_provider(context: &Context) -> String {
1246    translated(context, StockMessage::NotSupportedByProvider).await
1247}
1248
1249/// Stock string: `Messages`.
1250/// Used as a subtitle in quota context; can be plural always.
1251pub(crate) async fn messages(context: &Context) -> String {
1252    translated(context, StockMessage::Messages).await
1253}
1254
1255/// Stock string: `%1$s of %2$s used`.
1256pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
1257    translated(context, StockMessage::PartOfTotallUsed)
1258        .await
1259        .replace1(part)
1260        .replace2(total)
1261}
1262
1263/// Stock string: `⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet. Tap to learn more.`.
1264pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
1265    translated(context, StockMessage::InvalidUnencryptedMail)
1266        .await
1267        .replace1(provider)
1268}
1269
1270/// Stock string: `The attachment contains anonymous usage statistics, which helps us improve Delta Chat. Thank you!`
1271pub(crate) async fn stats_msg_body(context: &Context) -> String {
1272    translated(context, StockMessage::StatsMsgBody).await
1273}
1274
1275pub(crate) async fn aeap_explanation_and_link(
1276    context: &Context,
1277    old_addr: &str,
1278    new_addr: &str,
1279) -> String {
1280    translated(context, StockMessage::AeapExplanationAndLink)
1281        .await
1282        .replace1(old_addr)
1283        .replace2(new_addr)
1284}
1285
1286/// Stock string: `Others will only see this group after you sent a first message.`.
1287pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
1288    translated(context, StockMessage::NewGroupSendFirstMessage).await
1289}
1290
1291/// Text to put in the [`Qr::Backup2`] rendered SVG image.
1292///
1293/// The default is "Scan to set up second device for NAME".
1294/// The account name (or address as fallback) are looked up from the context.
1295///
1296/// [`Qr::Backup2`]: crate::qr::Qr::Backup2
1297pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
1298    let name = if let Some(name) = context.get_config(Config::Displayname).await? {
1299        name
1300    } else {
1301        context.get_primary_self_addr().await?
1302    };
1303    Ok(translated(context, StockMessage::BackupTransferQr)
1304        .await
1305        .replace1(&name))
1306}
1307
1308pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String {
1309    translated(context, StockMessage::BackupTransferMsgBody).await
1310}
1311
1312impl Context {
1313    /// Set the stock string for the [StockMessage].
1314    ///
1315    pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1316        self.translated_stockstrings
1317            .set_stock_translation(id, stockstring)
1318            .await?;
1319        Ok(())
1320    }
1321
1322    pub(crate) async fn update_device_chats(&self) -> Result<()> {
1323        if self.get_config_bool(Config::Bot).await? {
1324            return Ok(());
1325        }
1326
1327        // create saved-messages chat; we do this only once, if the user has deleted the chat,
1328        // he can recreate it manually (make sure we do not re-add it when configure() was called a second time)
1329        if !self.sql.get_raw_config_bool("self-chat-added").await? {
1330            self.sql
1331                .set_raw_config_bool("self-chat-added", true)
1332                .await?;
1333            ChatId::create_for_contact(self, ContactId::SELF).await?;
1334        }
1335
1336        // add welcome-messages. by the label, this is done only once,
1337        // if the user has deleted the message or the chat, it is not added again.
1338        let image = include_bytes!("../assets/welcome-image.jpg");
1339        let blob = BlobObject::create_and_deduplicate_from_bytes(self, image, "welcome.jpg")?;
1340        let mut msg = Message::new(Viewtype::Image);
1341        msg.param.set(Param::File, blob.as_name());
1342        msg.param.set(Param::Filename, "welcome-image.jpg");
1343        chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
1344
1345        let mut msg = Message::new_text(welcome_message(self).await);
1346        chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
1347        Ok(())
1348    }
1349}
1350
1351impl Accounts {
1352    /// Set the stock string for the [StockMessage].
1353    ///
1354    pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1355        self.stockstrings
1356            .set_stock_translation(id, stockstring)
1357            .await?;
1358        Ok(())
1359    }
1360}
1361
1362#[cfg(test)]
1363mod stock_str_tests;