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