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