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