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