1use std::collections::HashMap;
4use std::sync::Arc;
5
6use anyhow::{Result, bail};
7use strum::EnumProperty as EnumPropertyTrait;
8use strum_macros::EnumProperty;
9use tokio::sync::RwLock;
10
11use crate::accounts::Accounts;
12use crate::blob::BlobObject;
13use crate::chat::{self, Chat, ChatId};
14use crate::config::Config;
15use crate::contact::{Contact, ContactId};
16use crate::context::Context;
17use crate::message::{Message, Viewtype};
18use crate::param::Param;
19
20#[derive(Debug, Clone)]
22pub struct StockStrings {
23 translated_stockstrings: Arc<RwLock<HashMap<usize, String>>>,
25}
26
27#[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 #[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 #[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 #[strum(props(fallback = "You set message deletion timer to %1$s s."))]
277 MsgYouEnabledEphemeralTimer = 140,
278
279 #[strum(props(fallback = "Message deletion timer is set to %1$s s by %2$s."))]
280 MsgEphemeralTimerEnabledBy = 141,
281
282 #[strum(props(fallback = "You set message deletion timer to 1 hour."))]
283 MsgYouEphemeralTimerHour = 144,
284
285 #[strum(props(fallback = "Message deletion timer is set to 1 hour by %1$s."))]
286 MsgEphemeralTimerHourBy = 145,
287
288 #[strum(props(fallback = "You set message deletion timer to 1 day."))]
289 MsgYouEphemeralTimerDay = 146,
290
291 #[strum(props(fallback = "Message deletion timer is set to 1 day by %1$s."))]
292 MsgEphemeralTimerDayBy = 147,
293
294 #[strum(props(fallback = "You set message deletion timer to 1 week."))]
295 MsgYouEphemeralTimerWeek = 148,
296
297 #[strum(props(fallback = "Message deletion timer is set to 1 week by %1$s."))]
298 MsgEphemeralTimerWeekBy = 149,
299
300 #[strum(props(fallback = "You set message deletion timer to %1$s minutes."))]
301 MsgYouEphemeralTimerMinutes = 150,
302
303 #[strum(props(fallback = "Message deletion timer is set to %1$s minutes by %2$s."))]
304 MsgEphemeralTimerMinutesBy = 151,
305
306 #[strum(props(fallback = "You set message deletion timer to %1$s hours."))]
307 MsgYouEphemeralTimerHours = 152,
308
309 #[strum(props(fallback = "Message deletion timer is set to %1$s hours by %2$s."))]
310 MsgEphemeralTimerHoursBy = 153,
311
312 #[strum(props(fallback = "You set message deletion timer to %1$s days."))]
313 MsgYouEphemeralTimerDays = 154,
314
315 #[strum(props(fallback = "Message deletion timer is set to %1$s days by %2$s."))]
316 MsgEphemeralTimerDaysBy = 155,
317
318 #[strum(props(fallback = "You set message deletion timer to %1$s weeks."))]
319 MsgYouEphemeralTimerWeeks = 156,
320
321 #[strum(props(fallback = "Message deletion timer is set to %1$s weeks by %2$s."))]
322 MsgEphemeralTimerWeeksBy = 157,
323
324 #[strum(props(fallback = "You set message deletion timer to 1 year."))]
325 MsgYouEphemeralTimerYear = 158,
326
327 #[strum(props(fallback = "Message deletion timer is set to 1 year by %1$s."))]
328 MsgEphemeralTimerYearBy = 159,
329
330 #[strum(props(fallback = "Scan to set up second device for %1$s"))]
331 BackupTransferQr = 162,
332
333 #[strum(props(fallback = "ℹ️ Account transferred to your second device."))]
334 BackupTransferMsgBody = 163,
335
336 #[strum(props(fallback = "Messages are end-to-end encrypted."))]
337 ChatProtectionEnabled = 170,
338
339 #[strum(props(fallback = "Others will only see this group after you sent a first message."))]
340 NewGroupSendFirstMessage = 172,
341
342 #[strum(props(fallback = "Member %1$s added."))]
343 MsgAddMember = 173,
344
345 #[strum(props(
346 fallback = "⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet."
347 ))]
348 InvalidUnencryptedMail = 174,
349
350 #[strum(props(fallback = "You reacted %1$s to \"%2$s\""))]
351 MsgYouReacted = 176,
352
353 #[strum(props(fallback = "%1$s reacted %2$s to \"%3$s\""))]
354 MsgReactedBy = 177,
355
356 #[strum(props(fallback = "Member %1$s removed."))]
357 MsgDelMember = 178,
358
359 #[strum(props(fallback = "Establishing connection, please wait…"))]
360 SecurejoinWait = 190,
361
362 #[strum(props(fallback = "❤️ Seems you're enjoying Delta Chat!
363
364Please consider donating to help that Delta Chat stays free for everyone.
365
366While Delta Chat is free to use and open source, development costs money.
367Help keeping us to keep Delta Chat independent and make it more awesome in the future.
368
369https://delta.chat/donate"))]
370 DonationRequest = 193,
371
372 #[strum(props(fallback = "Declined call"))]
373 DeclinedCall = 196,
374
375 #[strum(props(fallback = "Canceled call"))]
376 CanceledCall = 197,
377
378 #[strum(props(fallback = "Missed call"))]
379 MissedCall = 198,
380
381 #[strum(props(fallback = "You left the channel."))]
382 MsgYouLeftBroadcast = 200,
383
384 #[strum(props(fallback = "Scan to join channel %1$s"))]
385 SecureJoinBrodcastQRDescription = 201,
386
387 #[strum(props(fallback = "You joined the channel."))]
388 MsgYouJoinedBroadcast = 202,
389
390 #[strum(props(fallback = "%1$s invited you to join this channel.\n\n\
391 Waiting for the device of %2$s to reply…"))]
392 SecureJoinBroadcastStarted = 203,
393
394 #[strum(props(
395 fallback = "The attachment contains anonymous usage statistics, which helps us improve Delta Chat. Thank you!"
396 ))]
397 StatsMsgBody = 210,
398
399 #[strum(props(fallback = "Proxy Enabled"))]
400 ProxyEnabled = 220,
401
402 #[strum(props(
403 fallback = "You are using a proxy. If you're having trouble connecting, try a different proxy."
404 ))]
405 ProxyEnabledDescription = 221,
406
407 #[strum(props(fallback = "Messages in this chat use classic email and are not encrypted."))]
408 ChatUnencryptedExplanation = 230,
409
410 #[strum(props(
411 fallback = "You are using the legacy option \"Settings → Advanced → Move automatically to DeltaChat Folder\".\n\nThis option will be removed in a few weeks and you should disable it already today.\n\nIf having chat messages mixed into your inbox is a problem, see https://delta.chat/legacy-move"
412 ))]
413 MvboxMoveDeprecation = 231,
414
415 #[strum(props(fallback = "Outgoing audio call"))]
416 OutgoingAudioCall = 232,
417
418 #[strum(props(fallback = "Outgoing video call"))]
419 OutgoingVideoCall = 233,
420
421 #[strum(props(fallback = "Incoming audio call"))]
422 IncomingAudioCall = 234,
423
424 #[strum(props(fallback = "Incoming video call"))]
425 IncomingVideoCall = 235,
426
427 #[strum(props(fallback = "You changed the chat description."))]
428 MsgYouChangedDescription = 240,
429
430 #[strum(props(fallback = "Chat description changed by %1$s."))]
431 MsgChatDescriptionChangedBy = 241,
432}
433
434impl StockMessage {
435 fn fallback(self) -> &'static str {
439 self.get_str("fallback").unwrap_or_default()
440 }
441}
442
443impl Default for StockStrings {
444 fn default() -> Self {
445 StockStrings::new()
446 }
447}
448
449impl StockStrings {
450 pub fn new() -> Self {
452 Self {
453 translated_stockstrings: Arc::new(RwLock::new(Default::default())),
454 }
455 }
456
457 async fn translated(&self, id: StockMessage) -> String {
458 self.translated_stockstrings
459 .read()
460 .await
461 .get(&(id as usize))
462 .map(AsRef::as_ref)
463 .unwrap_or_else(|| id.fallback())
464 .to_string()
465 }
466
467 async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
468 if stockstring.contains("%1") && !id.fallback().contains("%1") {
469 bail!(
470 "translation {} contains invalid %1 placeholder, default is {}",
471 stockstring,
472 id.fallback()
473 );
474 }
475 if stockstring.contains("%2") && !id.fallback().contains("%2") {
476 bail!(
477 "translation {} contains invalid %2 placeholder, default is {}",
478 stockstring,
479 id.fallback()
480 );
481 }
482 self.translated_stockstrings
483 .write()
484 .await
485 .insert(id as usize, stockstring);
486 Ok(())
487 }
488}
489
490async fn translated(context: &Context, id: StockMessage) -> String {
491 context.translated_stockstrings.translated(id).await
492}
493
494trait StockStringMods: AsRef<str> + Sized {
496 fn replace1(&self, replacement: &str) -> String {
498 self.as_ref()
499 .replacen("%1$s", replacement, 1)
500 .replacen("%1$d", replacement, 1)
501 .replacen("%1$@", replacement, 1)
502 }
503
504 fn replace2(&self, replacement: &str) -> String {
509 self.as_ref()
510 .replacen("%2$s", replacement, 1)
511 .replacen("%2$d", replacement, 1)
512 .replacen("%2$@", replacement, 1)
513 }
514
515 fn replace3(&self, replacement: &str) -> String {
520 self.as_ref()
521 .replacen("%3$s", replacement, 1)
522 .replacen("%3$d", replacement, 1)
523 .replacen("%3$@", replacement, 1)
524 }
525}
526
527impl ContactId {
528 async fn get_stock_name(self, context: &Context) -> String {
530 Contact::get_by_id(context, self)
531 .await
532 .map(|contact| contact.get_display_name().to_string())
533 .unwrap_or_else(|_| self.to_string())
534 }
535}
536
537impl StockStringMods for String {}
538
539pub(crate) async fn no_messages(context: &Context) -> String {
541 translated(context, StockMessage::NoMessages).await
542}
543
544pub(crate) async fn self_msg(context: &Context) -> String {
546 translated(context, StockMessage::SelfMsg).await
547}
548
549pub(crate) async fn draft(context: &Context) -> String {
551 translated(context, StockMessage::Draft).await
552}
553
554pub(crate) async fn voice_message(context: &Context) -> String {
556 translated(context, StockMessage::VoiceMessage).await
557}
558
559pub(crate) async fn image(context: &Context) -> String {
561 translated(context, StockMessage::Image).await
562}
563
564pub(crate) async fn video(context: &Context) -> String {
566 translated(context, StockMessage::Video).await
567}
568
569pub(crate) async fn audio(context: &Context) -> String {
571 translated(context, StockMessage::Audio).await
572}
573
574pub(crate) async fn file(context: &Context) -> String {
576 translated(context, StockMessage::File).await
577}
578
579pub(crate) async fn msg_grp_name(
581 context: &Context,
582 from_group: &str,
583 to_group: &str,
584 by_contact: ContactId,
585) -> String {
586 if by_contact == ContactId::SELF {
587 translated(context, StockMessage::MsgYouChangedGrpName)
588 .await
589 .replace1(from_group)
590 .replace2(to_group)
591 } else {
592 translated(context, StockMessage::MsgGrpNameChangedBy)
593 .await
594 .replace1(from_group)
595 .replace2(to_group)
596 .replace3(&by_contact.get_stock_name(context).await)
597 }
598}
599
600pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
601 if by_contact == ContactId::SELF {
602 translated(context, StockMessage::MsgYouChangedGrpImg).await
603 } else {
604 translated(context, StockMessage::MsgGrpImgChangedBy)
605 .await
606 .replace1(&by_contact.get_stock_name(context).await)
607 }
608}
609
610pub(crate) async fn msg_chat_description_changed(
611 context: &Context,
612 by_contact: ContactId,
613) -> String {
614 if by_contact == ContactId::SELF {
615 translated(context, StockMessage::MsgYouChangedDescription).await
616 } else {
617 translated(context, StockMessage::MsgChatDescriptionChangedBy)
618 .await
619 .replace1(&by_contact.get_stock_name(context).await)
620 }
621}
622
623pub(crate) async fn msg_add_member_local(
628 context: &Context,
629 added_member: ContactId,
630 by_contact: ContactId,
631) -> String {
632 let whom = added_member.get_stock_name(context).await;
633 if by_contact == ContactId::UNDEFINED {
634 translated(context, StockMessage::MsgAddMember)
635 .await
636 .replace1(&whom)
637 } else if by_contact == ContactId::SELF {
638 translated(context, StockMessage::MsgYouAddMember)
639 .await
640 .replace1(&whom)
641 } else {
642 translated(context, StockMessage::MsgAddMemberBy)
643 .await
644 .replace1(&whom)
645 .replace2(&by_contact.get_stock_name(context).await)
646 }
647}
648
649pub(crate) async fn msg_del_member_local(
654 context: &Context,
655 removed_member: ContactId,
656 by_contact: ContactId,
657) -> String {
658 let whom = removed_member.get_stock_name(context).await;
659 if by_contact == ContactId::UNDEFINED {
660 translated(context, StockMessage::MsgDelMember)
661 .await
662 .replace1(&whom)
663 } else if by_contact == ContactId::SELF {
664 translated(context, StockMessage::MsgYouDelMember)
665 .await
666 .replace1(&whom)
667 } else {
668 translated(context, StockMessage::MsgDelMemberBy)
669 .await
670 .replace1(&whom)
671 .replace2(&by_contact.get_stock_name(context).await)
672 }
673}
674
675pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
677 if by_contact == ContactId::SELF {
678 translated(context, StockMessage::MsgYouLeftGroup).await
679 } else {
680 translated(context, StockMessage::MsgGroupLeftBy)
681 .await
682 .replace1(&by_contact.get_stock_name(context).await)
683 }
684}
685
686pub(crate) async fn msg_you_left_broadcast(context: &Context) -> String {
688 translated(context, StockMessage::MsgYouLeftBroadcast).await
689}
690
691pub(crate) async fn msg_you_joined_broadcast(context: &Context) -> String {
693 translated(context, StockMessage::MsgYouJoinedBroadcast).await
694}
695
696pub(crate) async fn secure_join_broadcast_started(
698 context: &Context,
699 inviter_contact_id: ContactId,
700) -> String {
701 if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
702 translated(context, StockMessage::SecureJoinBroadcastStarted)
703 .await
704 .replace1(contact.get_display_name())
705 .replace2(contact.get_display_name())
706 } else {
707 format!("secure_join_started: unknown contact {inviter_contact_id}")
708 }
709}
710
711pub(crate) async fn msg_reacted(
713 context: &Context,
714 by_contact: ContactId,
715 reaction: &str,
716 summary: &str,
717) -> String {
718 if by_contact == ContactId::SELF {
719 translated(context, StockMessage::MsgYouReacted)
720 .await
721 .replace1(reaction)
722 .replace2(summary)
723 } else {
724 translated(context, StockMessage::MsgReactedBy)
725 .await
726 .replace1(&by_contact.get_stock_name(context).await)
727 .replace2(reaction)
728 .replace3(summary)
729 }
730}
731
732pub(crate) async fn gif(context: &Context) -> String {
734 translated(context, StockMessage::Gif).await
735}
736
737pub(crate) async fn encr_none(context: &Context) -> String {
739 translated(context, StockMessage::EncrNone).await
740}
741
742pub(crate) async fn finger_prints(context: &Context) -> String {
744 translated(context, StockMessage::FingerPrints).await
745}
746
747pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
749 if by_contact == ContactId::SELF {
750 translated(context, StockMessage::MsgYouDeletedGrpImg).await
751 } else {
752 translated(context, StockMessage::MsgGrpImgDeletedBy)
753 .await
754 .replace1(&by_contact.get_stock_name(context).await)
755 }
756}
757
758pub(crate) async fn secure_join_started(
760 context: &Context,
761 inviter_contact_id: ContactId,
762) -> String {
763 if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
764 translated(context, StockMessage::SecureJoinStarted)
765 .await
766 .replace1(contact.get_display_name())
767 .replace2(contact.get_display_name())
768 } else {
769 format!("secure_join_started: unknown contact {inviter_contact_id}")
770 }
771}
772
773pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
775 translated(context, StockMessage::SecureJoinReplies)
776 .await
777 .replace1(&contact_id.get_stock_name(context).await)
778}
779
780pub(crate) async fn securejoin_wait(context: &Context) -> String {
782 translated(context, StockMessage::SecurejoinWait).await
783}
784
785pub(crate) async fn donation_request(context: &Context) -> String {
787 translated(context, StockMessage::DonationRequest).await
788}
789
790pub(crate) async fn outgoing_call(context: &Context, has_video: bool) -> String {
792 translated(
793 context,
794 if has_video {
795 StockMessage::OutgoingVideoCall
796 } else {
797 StockMessage::OutgoingAudioCall
798 },
799 )
800 .await
801}
802
803pub(crate) async fn incoming_call(context: &Context, has_video: bool) -> String {
805 translated(
806 context,
807 if has_video {
808 StockMessage::IncomingVideoCall
809 } else {
810 StockMessage::IncomingAudioCall
811 },
812 )
813 .await
814}
815
816pub(crate) async fn declined_call(context: &Context) -> String {
818 translated(context, StockMessage::DeclinedCall).await
819}
820
821pub(crate) async fn canceled_call(context: &Context) -> String {
823 translated(context, StockMessage::CanceledCall).await
824}
825
826pub(crate) async fn missed_call(context: &Context) -> String {
828 translated(context, StockMessage::MissedCall).await
829}
830
831pub(crate) async fn setup_contact_qr_description(
833 context: &Context,
834 display_name: &str,
835 addr: &str,
836) -> String {
837 let name = if display_name.is_empty() {
838 addr.to_owned()
839 } else {
840 display_name.to_owned()
841 };
842 translated(context, StockMessage::SetupContactQRDescription)
843 .await
844 .replace1(&name)
845}
846
847pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
849 translated(context, StockMessage::SecureJoinGroupQRDescription)
850 .await
851 .replace1(chat.get_name())
852}
853
854pub(crate) async fn secure_join_broadcast_qr_description(context: &Context, chat: &Chat) -> String {
856 translated(context, StockMessage::SecureJoinBrodcastQRDescription)
857 .await
858 .replace1(chat.get_name())
859}
860
861#[allow(dead_code)]
863pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
864 let addr = contact.get_display_name();
865 translated(context, StockMessage::ContactVerified)
866 .await
867 .replace1(addr)
868}
869
870pub(crate) async fn archived_chats(context: &Context) -> String {
872 translated(context, StockMessage::ArchivedChats).await
873}
874
875pub(crate) async fn sync_msg_subject(context: &Context) -> String {
877 translated(context, StockMessage::SyncMsgSubject).await
878}
879
880pub(crate) async fn sync_msg_body(context: &Context) -> String {
882 translated(context, StockMessage::SyncMsgBody).await
883}
884
885pub(crate) async fn cannot_login(context: &Context, user: &str) -> String {
887 translated(context, StockMessage::CannotLogin)
888 .await
889 .replace1(user)
890}
891
892pub(crate) async fn msg_location_enabled(context: &Context) -> String {
894 translated(context, StockMessage::MsgLocationEnabled).await
895}
896
897pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
899 if contact == ContactId::SELF {
900 translated(context, StockMessage::MsgYouEnabledLocation).await
901 } else {
902 translated(context, StockMessage::MsgLocationEnabledBy)
903 .await
904 .replace1(&contact.get_stock_name(context).await)
905 }
906}
907
908pub(crate) async fn msg_location_disabled(context: &Context) -> String {
910 translated(context, StockMessage::MsgLocationDisabled).await
911}
912
913pub(crate) async fn location(context: &Context) -> String {
915 translated(context, StockMessage::Location).await
916}
917
918pub(crate) async fn sticker(context: &Context) -> String {
920 translated(context, StockMessage::Sticker).await
921}
922
923pub(crate) async fn device_messages(context: &Context) -> String {
925 translated(context, StockMessage::DeviceMessages).await
926}
927
928pub(crate) async fn saved_messages(context: &Context) -> String {
930 translated(context, StockMessage::SavedMessages).await
931}
932
933pub(crate) async fn device_messages_hint(context: &Context) -> String {
935 translated(context, StockMessage::DeviceMessagesHint).await
936}
937
938pub(crate) async fn welcome_message(context: &Context) -> String {
940 translated(context, StockMessage::WelcomeMessage).await
941}
942
943pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
946 translated(context, StockMessage::SubjectForNewContact)
947 .await
948 .replace1(self_name)
949}
950
951pub(crate) async fn msg_ephemeral_timer_disabled(
953 context: &Context,
954 by_contact: ContactId,
955) -> String {
956 if by_contact == ContactId::SELF {
957 translated(context, StockMessage::MsgYouDisabledEphemeralTimer).await
958 } else {
959 translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
960 .await
961 .replace1(&by_contact.get_stock_name(context).await)
962 }
963}
964
965pub(crate) async fn msg_ephemeral_timer_enabled(
967 context: &Context,
968 timer: &str,
969 by_contact: ContactId,
970) -> String {
971 if by_contact == ContactId::SELF {
972 translated(context, StockMessage::MsgYouEnabledEphemeralTimer)
973 .await
974 .replace1(timer)
975 } else {
976 translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
977 .await
978 .replace1(timer)
979 .replace2(&by_contact.get_stock_name(context).await)
980 }
981}
982
983pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
985 if by_contact == ContactId::SELF {
986 translated(context, StockMessage::MsgYouEphemeralTimerHour).await
987 } else {
988 translated(context, StockMessage::MsgEphemeralTimerHourBy)
989 .await
990 .replace1(&by_contact.get_stock_name(context).await)
991 }
992}
993
994pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
996 if by_contact == ContactId::SELF {
997 translated(context, StockMessage::MsgYouEphemeralTimerDay).await
998 } else {
999 translated(context, StockMessage::MsgEphemeralTimerDayBy)
1000 .await
1001 .replace1(&by_contact.get_stock_name(context).await)
1002 }
1003}
1004
1005pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
1007 if by_contact == ContactId::SELF {
1008 translated(context, StockMessage::MsgYouEphemeralTimerWeek).await
1009 } else {
1010 translated(context, StockMessage::MsgEphemeralTimerWeekBy)
1011 .await
1012 .replace1(&by_contact.get_stock_name(context).await)
1013 }
1014}
1015
1016pub(crate) async fn msg_ephemeral_timer_year(context: &Context, by_contact: ContactId) -> String {
1018 if by_contact == ContactId::SELF {
1019 translated(context, StockMessage::MsgYouEphemeralTimerYear).await
1020 } else {
1021 translated(context, StockMessage::MsgEphemeralTimerYearBy)
1022 .await
1023 .replace1(&by_contact.get_stock_name(context).await)
1024 }
1025}
1026
1027pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
1029 translated(context, StockMessage::ConfigurationFailed)
1030 .await
1031 .replace1(details)
1032}
1033
1034pub(crate) async fn bad_time_msg_body(context: &Context, now: &str) -> String {
1037 translated(context, StockMessage::BadTimeMsgBody)
1038 .await
1039 .replace1(now)
1040}
1041
1042pub(crate) async fn update_reminder_msg_body(context: &Context) -> String {
1044 translated(context, StockMessage::UpdateReminderMsgBody).await
1045}
1046
1047pub(crate) async fn error_no_network(context: &Context) -> String {
1049 translated(context, StockMessage::ErrorNoNetwork).await
1050}
1051
1052pub(crate) async fn messages_e2e_encrypted(context: &Context) -> String {
1054 translated(context, StockMessage::ChatProtectionEnabled).await
1055}
1056
1057pub(crate) async fn reply_noun(context: &Context) -> String {
1059 translated(context, StockMessage::ReplyNoun).await
1060}
1061
1062pub(crate) async fn self_deleted_msg_body(context: &Context) -> String {
1064 translated(context, StockMessage::SelfDeletedMsgBody).await
1065}
1066
1067pub(crate) async fn msg_ephemeral_timer_minutes(
1069 context: &Context,
1070 minutes: &str,
1071 by_contact: ContactId,
1072) -> String {
1073 if by_contact == ContactId::SELF {
1074 translated(context, StockMessage::MsgYouEphemeralTimerMinutes)
1075 .await
1076 .replace1(minutes)
1077 } else {
1078 translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
1079 .await
1080 .replace1(minutes)
1081 .replace2(&by_contact.get_stock_name(context).await)
1082 }
1083}
1084
1085pub(crate) async fn msg_ephemeral_timer_hours(
1087 context: &Context,
1088 hours: &str,
1089 by_contact: ContactId,
1090) -> String {
1091 if by_contact == ContactId::SELF {
1092 translated(context, StockMessage::MsgYouEphemeralTimerHours)
1093 .await
1094 .replace1(hours)
1095 } else {
1096 translated(context, StockMessage::MsgEphemeralTimerHoursBy)
1097 .await
1098 .replace1(hours)
1099 .replace2(&by_contact.get_stock_name(context).await)
1100 }
1101}
1102
1103pub(crate) async fn msg_ephemeral_timer_days(
1105 context: &Context,
1106 days: &str,
1107 by_contact: ContactId,
1108) -> String {
1109 if by_contact == ContactId::SELF {
1110 translated(context, StockMessage::MsgYouEphemeralTimerDays)
1111 .await
1112 .replace1(days)
1113 } else {
1114 translated(context, StockMessage::MsgEphemeralTimerDaysBy)
1115 .await
1116 .replace1(days)
1117 .replace2(&by_contact.get_stock_name(context).await)
1118 }
1119}
1120
1121pub(crate) async fn msg_ephemeral_timer_weeks(
1123 context: &Context,
1124 weeks: &str,
1125 by_contact: ContactId,
1126) -> String {
1127 if by_contact == ContactId::SELF {
1128 translated(context, StockMessage::MsgYouEphemeralTimerWeeks)
1129 .await
1130 .replace1(weeks)
1131 } else {
1132 translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
1133 .await
1134 .replace1(weeks)
1135 .replace2(&by_contact.get_stock_name(context).await)
1136 }
1137}
1138
1139pub(crate) async fn forwarded(context: &Context) -> String {
1141 translated(context, StockMessage::Forwarded).await
1142}
1143
1144pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
1146 translated(context, StockMessage::QuotaExceedingMsgBody)
1147 .await
1148 .replace1(&format!("{highest_usage}"))
1149 .replace("%%", "%")
1150}
1151
1152pub(crate) async fn incoming_messages(context: &Context) -> String {
1154 translated(context, StockMessage::IncomingMessages).await
1155}
1156
1157pub(crate) async fn outgoing_messages(context: &Context) -> String {
1159 translated(context, StockMessage::OutgoingMessages).await
1160}
1161
1162pub(crate) async fn not_connected(context: &Context) -> String {
1164 translated(context, StockMessage::NotConnected).await
1165}
1166
1167pub(crate) async fn connected(context: &Context) -> String {
1169 translated(context, StockMessage::Connected).await
1170}
1171
1172pub(crate) async fn connecting(context: &Context) -> String {
1174 translated(context, StockMessage::Connecting).await
1175}
1176
1177pub(crate) async fn updating(context: &Context) -> String {
1179 translated(context, StockMessage::Updating).await
1180}
1181
1182pub(crate) async fn sending(context: &Context) -> String {
1184 translated(context, StockMessage::Sending).await
1185}
1186
1187pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
1189 translated(context, StockMessage::LastMsgSentSuccessfully).await
1190}
1191
1192pub(crate) async fn error(context: &Context, error: &str) -> String {
1195 translated(context, StockMessage::Error)
1196 .await
1197 .replace1(error)
1198}
1199
1200pub(crate) async fn not_supported_by_provider(context: &Context) -> String {
1202 translated(context, StockMessage::NotSupportedByProvider).await
1203}
1204
1205pub(crate) async fn messages(context: &Context) -> String {
1208 translated(context, StockMessage::Messages).await
1209}
1210
1211pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
1213 translated(context, StockMessage::PartOfTotallUsed)
1214 .await
1215 .replace1(part)
1216 .replace2(total)
1217}
1218
1219pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
1221 translated(context, StockMessage::InvalidUnencryptedMail)
1222 .await
1223 .replace1(provider)
1224}
1225
1226pub(crate) async fn stats_msg_body(context: &Context) -> String {
1228 translated(context, StockMessage::StatsMsgBody).await
1229}
1230
1231pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
1233 translated(context, StockMessage::NewGroupSendFirstMessage).await
1234}
1235
1236pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
1243 let name = if let Some(name) = context.get_config(Config::Displayname).await? {
1244 name
1245 } else {
1246 context.get_primary_self_addr().await?
1247 };
1248 Ok(translated(context, StockMessage::BackupTransferQr)
1249 .await
1250 .replace1(&name))
1251}
1252
1253pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String {
1254 translated(context, StockMessage::BackupTransferMsgBody).await
1255}
1256
1257pub(crate) async fn proxy_enabled(context: &Context) -> String {
1259 translated(context, StockMessage::ProxyEnabled).await
1260}
1261
1262pub(crate) async fn proxy_description(context: &Context) -> String {
1264 translated(context, StockMessage::ProxyEnabledDescription).await
1265}
1266
1267pub(crate) async fn chat_unencrypted_explanation(context: &Context) -> String {
1269 translated(context, StockMessage::ChatUnencryptedExplanation).await
1270}
1271
1272pub(crate) async fn mvbox_move_deprecation(context: &Context) -> String {
1274 translated(context, StockMessage::MvboxMoveDeprecation).await
1275}
1276
1277impl Viewtype {
1278 pub async fn to_locale_string(&self, context: &Context) -> String {
1280 match self {
1281 Viewtype::Image => image(context).await,
1282 Viewtype::Gif => gif(context).await,
1283 Viewtype::Sticker => sticker(context).await,
1284 Viewtype::Audio => audio(context).await,
1285 Viewtype::Voice => voice_message(context).await,
1286 Viewtype::Video => video(context).await,
1287 Viewtype::File => file(context).await,
1288 Viewtype::Webxdc => "Mini App".to_owned(),
1289 Viewtype::Vcard => "👤".to_string(),
1290 Viewtype::Unknown | Viewtype::Text | Viewtype::Call => self.to_string(),
1292 }
1293 }
1294}
1295
1296impl Context {
1297 pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1300 self.translated_stockstrings
1301 .set_stock_translation(id, stockstring)
1302 .await?;
1303 Ok(())
1304 }
1305
1306 pub(crate) async fn update_device_chats(&self) -> Result<()> {
1307 if self.get_config_bool(Config::Bot).await? {
1308 return Ok(());
1309 }
1310
1311 if !self.sql.get_raw_config_bool("self-chat-added").await? {
1314 self.sql
1315 .set_raw_config_bool("self-chat-added", true)
1316 .await?;
1317 ChatId::create_for_contact(self, ContactId::SELF).await?;
1318 }
1319
1320 let image = include_bytes!("../assets/welcome-image.jpg");
1323 let blob = BlobObject::create_and_deduplicate_from_bytes(self, image, "welcome.jpg")?;
1324 let mut msg = Message::new(Viewtype::Image);
1325 msg.param.set(Param::File, blob.as_name());
1326 msg.param.set(Param::Filename, "welcome-image.jpg");
1327 chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
1328
1329 let mut msg = Message::new_text(welcome_message(self).await);
1330 chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
1331 Ok(())
1332 }
1333}
1334
1335impl Accounts {
1336 pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1339 self.stockstrings
1340 .set_stock_translation(id, stockstring)
1341 .await?;
1342 Ok(())
1343 }
1344}
1345
1346#[cfg(test)]
1347mod stock_str_tests;