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 = "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(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: `Storage on %1$s`.
1148/// `%1$s` will be replaced by the domain of the configured email-address.
1149pub(crate) async fn storage_on_domain(context: &Context, domain: &str) -> String {
1150    translated(context, StockMessage::StorageOnDomain)
1151        .await
1152        .replace1(domain)
1153}
1154
1155/// Stock string: `Not connected`.
1156pub(crate) async fn not_connected(context: &Context) -> String {
1157    translated(context, StockMessage::NotConnected).await
1158}
1159
1160/// Stock string: `Connected`.
1161pub(crate) async fn connected(context: &Context) -> String {
1162    translated(context, StockMessage::Connected).await
1163}
1164
1165/// Stock string: `Connecting…`.
1166pub(crate) async fn connecting(context: &Context) -> String {
1167    translated(context, StockMessage::Connecting).await
1168}
1169
1170/// Stock string: `Updating…`.
1171pub(crate) async fn updating(context: &Context) -> String {
1172    translated(context, StockMessage::Updating).await
1173}
1174
1175/// Stock string: `Sending…`.
1176pub(crate) async fn sending(context: &Context) -> String {
1177    translated(context, StockMessage::Sending).await
1178}
1179
1180/// Stock string: `Your last message was sent successfully.`.
1181pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
1182    translated(context, StockMessage::LastMsgSentSuccessfully).await
1183}
1184
1185/// Stock string: `Error: %1$s…`.
1186/// `%1$s` will be replaced by a possibly more detailed, typically english, error description.
1187pub(crate) async fn error(context: &Context, error: &str) -> String {
1188    translated(context, StockMessage::Error)
1189        .await
1190        .replace1(error)
1191}
1192
1193/// Stock string: `Not supported by your provider.`.
1194pub(crate) async fn not_supported_by_provider(context: &Context) -> String {
1195    translated(context, StockMessage::NotSupportedByProvider).await
1196}
1197
1198/// Stock string: `Messages`.
1199/// Used as a subtitle in quota context; can be plural always.
1200pub(crate) async fn messages(context: &Context) -> String {
1201    translated(context, StockMessage::Messages).await
1202}
1203
1204/// Stock string: `%1$s of %2$s used`.
1205pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
1206    translated(context, StockMessage::PartOfTotallUsed)
1207        .await
1208        .replace1(part)
1209        .replace2(total)
1210}
1211
1212/// Stock string: `⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet. Tap to learn more.`.
1213pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
1214    translated(context, StockMessage::InvalidUnencryptedMail)
1215        .await
1216        .replace1(provider)
1217}
1218
1219/// Stock string: `The attachment contains anonymous usage statistics, which helps us improve Delta Chat. Thank you!`
1220pub(crate) async fn stats_msg_body(context: &Context) -> String {
1221    translated(context, StockMessage::StatsMsgBody).await
1222}
1223
1224/// Stock string: `Others will only see this group after you sent a first message.`.
1225pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
1226    translated(context, StockMessage::NewGroupSendFirstMessage).await
1227}
1228
1229/// Text to put in the [`Qr::Backup2`] rendered SVG image.
1230///
1231/// The default is "Scan to set up second device for NAME".
1232/// The account name (or address as fallback) are looked up from the context.
1233///
1234/// [`Qr::Backup2`]: crate::qr::Qr::Backup2
1235pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
1236    let name = if let Some(name) = context.get_config(Config::Displayname).await? {
1237        name
1238    } else {
1239        context.get_primary_self_addr().await?
1240    };
1241    Ok(translated(context, StockMessage::BackupTransferQr)
1242        .await
1243        .replace1(&name))
1244}
1245
1246pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String {
1247    translated(context, StockMessage::BackupTransferMsgBody).await
1248}
1249
1250/// Stock string: `Proxy Enabled`.
1251pub(crate) async fn proxy_enabled(context: &Context) -> String {
1252    translated(context, StockMessage::ProxyEnabled).await
1253}
1254
1255/// Stock string: `You are using a proxy. If you're having trouble connecting, try a different proxy.`.
1256pub(crate) async fn proxy_description(context: &Context) -> String {
1257    translated(context, StockMessage::ProxyEnabledDescription).await
1258}
1259
1260/// Stock string: `Messages in this chat use classic email and are not encrypted.`.
1261pub(crate) async fn chat_unencrypted_explanation(context: &Context) -> String {
1262    translated(context, StockMessage::ChatUnencryptedExplanation).await
1263}
1264
1265impl Context {
1266    /// Set the stock string for the [StockMessage].
1267    ///
1268    pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1269        self.translated_stockstrings
1270            .set_stock_translation(id, stockstring)
1271            .await?;
1272        Ok(())
1273    }
1274
1275    pub(crate) async fn update_device_chats(&self) -> Result<()> {
1276        if self.get_config_bool(Config::Bot).await? {
1277            return Ok(());
1278        }
1279
1280        // create saved-messages chat; we do this only once, if the user has deleted the chat,
1281        // he can recreate it manually (make sure we do not re-add it when configure() was called a second time)
1282        if !self.sql.get_raw_config_bool("self-chat-added").await? {
1283            self.sql
1284                .set_raw_config_bool("self-chat-added", true)
1285                .await?;
1286            ChatId::create_for_contact(self, ContactId::SELF).await?;
1287        }
1288
1289        // add welcome-messages. by the label, this is done only once,
1290        // if the user has deleted the message or the chat, it is not added again.
1291        let image = include_bytes!("../assets/welcome-image.jpg");
1292        let blob = BlobObject::create_and_deduplicate_from_bytes(self, image, "welcome.jpg")?;
1293        let mut msg = Message::new(Viewtype::Image);
1294        msg.param.set(Param::File, blob.as_name());
1295        msg.param.set(Param::Filename, "welcome-image.jpg");
1296        chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
1297
1298        let mut msg = Message::new_text(welcome_message(self).await);
1299        chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
1300        Ok(())
1301    }
1302}
1303
1304impl Accounts {
1305    /// Set the stock string for the [StockMessage].
1306    ///
1307    pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1308        self.stockstrings
1309            .set_stock_translation(id, stockstring)
1310            .await?;
1311        Ok(())
1312    }
1313}
1314
1315#[cfg(test)]
1316mod stock_str_tests;