deltachat/
stock_str.rs

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