deltachat/
stock_str.rs

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