Skip to main content

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