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