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