deltachat/
stock_str.rs

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