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(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 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 pub fn new() -> Self {
461 Self {
462 translated_stockstrings: Arc::new(RwLock::new(Default::default())),
463 }
464 }
465
466 async fn translated(&self, id: StockMessage) -> String {
467 self.translated_stockstrings
468 .read()
469 .await
470 .get(&(id as usize))
471 .map(AsRef::as_ref)
472 .unwrap_or_else(|| id.fallback())
473 .to_string()
474 }
475
476 async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
477 if stockstring.contains("%1") && !id.fallback().contains("%1") {
478 bail!(
479 "translation {} contains invalid %1 placeholder, default is {}",
480 stockstring,
481 id.fallback()
482 );
483 }
484 if stockstring.contains("%2") && !id.fallback().contains("%2") {
485 bail!(
486 "translation {} contains invalid %2 placeholder, default is {}",
487 stockstring,
488 id.fallback()
489 );
490 }
491 self.translated_stockstrings
492 .write()
493 .await
494 .insert(id as usize, stockstring);
495 Ok(())
496 }
497}
498
499async fn translated(context: &Context, id: StockMessage) -> String {
500 context.translated_stockstrings.translated(id).await
501}
502
503trait StockStringMods: AsRef<str> + Sized {
505 fn replace1(&self, replacement: &str) -> String {
507 self.as_ref()
508 .replacen("%1$s", replacement, 1)
509 .replacen("%1$d", replacement, 1)
510 .replacen("%1$@", replacement, 1)
511 }
512
513 fn replace2(&self, replacement: &str) -> String {
518 self.as_ref()
519 .replacen("%2$s", replacement, 1)
520 .replacen("%2$d", replacement, 1)
521 .replacen("%2$@", replacement, 1)
522 }
523
524 fn replace3(&self, replacement: &str) -> String {
529 self.as_ref()
530 .replacen("%3$s", replacement, 1)
531 .replacen("%3$d", replacement, 1)
532 .replacen("%3$@", replacement, 1)
533 }
534}
535
536impl ContactId {
537 async fn get_stock_name(self, context: &Context) -> String {
539 Contact::get_by_id(context, self)
540 .await
541 .map(|contact| contact.get_display_name().to_string())
542 .unwrap_or_else(|_| self.to_string())
543 }
544}
545
546impl StockStringMods for String {}
547
548pub(crate) async fn no_messages(context: &Context) -> String {
550 translated(context, StockMessage::NoMessages).await
551}
552
553pub(crate) async fn self_msg(context: &Context) -> String {
555 translated(context, StockMessage::SelfMsg).await
556}
557
558pub(crate) async fn draft(context: &Context) -> String {
560 translated(context, StockMessage::Draft).await
561}
562
563pub(crate) async fn voice_message(context: &Context) -> String {
565 translated(context, StockMessage::VoiceMessage).await
566}
567
568pub(crate) async fn image(context: &Context) -> String {
570 translated(context, StockMessage::Image).await
571}
572
573pub(crate) async fn video(context: &Context) -> String {
575 translated(context, StockMessage::Video).await
576}
577
578pub(crate) async fn audio(context: &Context) -> String {
580 translated(context, StockMessage::Audio).await
581}
582
583pub(crate) async fn file(context: &Context) -> String {
585 translated(context, StockMessage::File).await
586}
587
588pub(crate) async fn msg_grp_name(
590 context: &Context,
591 from_group: &str,
592 to_group: &str,
593 by_contact: ContactId,
594) -> String {
595 if by_contact == ContactId::SELF {
596 translated(context, StockMessage::MsgYouChangedGrpName)
597 .await
598 .replace1(from_group)
599 .replace2(to_group)
600 } else {
601 translated(context, StockMessage::MsgGrpNameChangedBy)
602 .await
603 .replace1(from_group)
604 .replace2(to_group)
605 .replace3(&by_contact.get_stock_name(context).await)
606 }
607}
608
609pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
610 if by_contact == ContactId::SELF {
611 translated(context, StockMessage::MsgYouChangedGrpImg).await
612 } else {
613 translated(context, StockMessage::MsgGrpImgChangedBy)
614 .await
615 .replace1(&by_contact.get_stock_name(context).await)
616 }
617}
618
619pub(crate) async fn msg_chat_description_changed(
620 context: &Context,
621 by_contact: ContactId,
622) -> String {
623 if by_contact == ContactId::SELF {
624 translated(context, StockMessage::MsgYouChangedDescription).await
625 } else {
626 translated(context, StockMessage::MsgChatDescriptionChangedBy)
627 .await
628 .replace1(&by_contact.get_stock_name(context).await)
629 }
630}
631
632pub(crate) async fn msg_add_member_local(
637 context: &Context,
638 added_member: ContactId,
639 by_contact: ContactId,
640) -> String {
641 let whom = added_member.get_stock_name(context).await;
642 if by_contact == ContactId::UNDEFINED {
643 translated(context, StockMessage::MsgAddMember)
644 .await
645 .replace1(&whom)
646 } else if by_contact == ContactId::SELF {
647 translated(context, StockMessage::MsgYouAddMember)
648 .await
649 .replace1(&whom)
650 } else {
651 translated(context, StockMessage::MsgAddMemberBy)
652 .await
653 .replace1(&whom)
654 .replace2(&by_contact.get_stock_name(context).await)
655 }
656}
657
658pub(crate) async fn msg_del_member_local(
663 context: &Context,
664 removed_member: ContactId,
665 by_contact: ContactId,
666) -> String {
667 let whom = removed_member.get_stock_name(context).await;
668 if by_contact == ContactId::UNDEFINED {
669 translated(context, StockMessage::MsgDelMember)
670 .await
671 .replace1(&whom)
672 } else if by_contact == ContactId::SELF {
673 translated(context, StockMessage::MsgYouDelMember)
674 .await
675 .replace1(&whom)
676 } else {
677 translated(context, StockMessage::MsgDelMemberBy)
678 .await
679 .replace1(&whom)
680 .replace2(&by_contact.get_stock_name(context).await)
681 }
682}
683
684pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
686 if by_contact == ContactId::SELF {
687 translated(context, StockMessage::MsgYouLeftGroup).await
688 } else {
689 translated(context, StockMessage::MsgGroupLeftBy)
690 .await
691 .replace1(&by_contact.get_stock_name(context).await)
692 }
693}
694
695pub(crate) async fn msg_you_left_broadcast(context: &Context) -> String {
697 translated(context, StockMessage::MsgYouLeftBroadcast).await
698}
699
700pub(crate) async fn msg_you_joined_broadcast(context: &Context) -> String {
702 translated(context, StockMessage::MsgYouJoinedBroadcast).await
703}
704
705pub(crate) async fn secure_join_broadcast_started(
707 context: &Context,
708 inviter_contact_id: ContactId,
709) -> String {
710 if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
711 translated(context, StockMessage::SecureJoinBroadcastStarted)
712 .await
713 .replace1(contact.get_display_name())
714 .replace2(contact.get_display_name())
715 } else {
716 format!("secure_join_started: unknown contact {inviter_contact_id}")
717 }
718}
719
720pub(crate) async fn msg_broadcast_name_changed(context: &Context, from: &str, to: &str) -> String {
722 translated(context, StockMessage::MsgBroadcastNameChanged)
723 .await
724 .replace1(from)
725 .replace2(to)
726}
727
728pub(crate) async fn msg_broadcast_img_changed(context: &Context) -> String {
730 translated(context, StockMessage::MsgBroadcastImgChanged).await
731}
732
733pub(crate) async fn msg_reacted(
735 context: &Context,
736 by_contact: ContactId,
737 reaction: &str,
738 summary: &str,
739) -> String {
740 if by_contact == ContactId::SELF {
741 translated(context, StockMessage::MsgYouReacted)
742 .await
743 .replace1(reaction)
744 .replace2(summary)
745 } else {
746 translated(context, StockMessage::MsgReactedBy)
747 .await
748 .replace1(&by_contact.get_stock_name(context).await)
749 .replace2(reaction)
750 .replace3(summary)
751 }
752}
753
754pub(crate) async fn gif(context: &Context) -> String {
756 translated(context, StockMessage::Gif).await
757}
758
759pub(crate) async fn encr_none(context: &Context) -> String {
761 translated(context, StockMessage::EncrNone).await
762}
763
764pub(crate) async fn finger_prints(context: &Context) -> String {
766 translated(context, StockMessage::FingerPrints).await
767}
768
769pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
771 if by_contact == ContactId::SELF {
772 translated(context, StockMessage::MsgYouDeletedGrpImg).await
773 } else {
774 translated(context, StockMessage::MsgGrpImgDeletedBy)
775 .await
776 .replace1(&by_contact.get_stock_name(context).await)
777 }
778}
779
780pub(crate) async fn secure_join_started(
782 context: &Context,
783 inviter_contact_id: ContactId,
784) -> String {
785 if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
786 translated(context, StockMessage::SecureJoinStarted)
787 .await
788 .replace1(contact.get_display_name())
789 .replace2(contact.get_display_name())
790 } else {
791 format!("secure_join_started: unknown contact {inviter_contact_id}")
792 }
793}
794
795pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
797 translated(context, StockMessage::SecureJoinReplies)
798 .await
799 .replace1(&contact_id.get_stock_name(context).await)
800}
801
802pub(crate) async fn securejoin_wait(context: &Context) -> String {
804 translated(context, StockMessage::SecurejoinWait).await
805}
806
807pub(crate) async fn donation_request(context: &Context) -> String {
809 translated(context, StockMessage::DonationRequest).await
810}
811
812pub(crate) async fn outgoing_call(context: &Context, has_video: bool) -> String {
814 translated(
815 context,
816 if has_video {
817 StockMessage::OutgoingVideoCall
818 } else {
819 StockMessage::OutgoingAudioCall
820 },
821 )
822 .await
823}
824
825pub(crate) async fn incoming_call(context: &Context, has_video: bool) -> String {
827 translated(
828 context,
829 if has_video {
830 StockMessage::IncomingVideoCall
831 } else {
832 StockMessage::IncomingAudioCall
833 },
834 )
835 .await
836}
837
838pub(crate) async fn declined_call(context: &Context) -> String {
840 translated(context, StockMessage::DeclinedCall).await
841}
842
843pub(crate) async fn canceled_call(context: &Context) -> String {
845 translated(context, StockMessage::CanceledCall).await
846}
847
848pub(crate) async fn missed_call(context: &Context) -> String {
850 translated(context, StockMessage::MissedCall).await
851}
852
853pub(crate) async fn setup_contact_qr_description(
855 context: &Context,
856 display_name: &str,
857 addr: &str,
858) -> String {
859 let name = if display_name.is_empty() {
860 addr.to_owned()
861 } else {
862 display_name.to_owned()
863 };
864 translated(context, StockMessage::SetupContactQRDescription)
865 .await
866 .replace1(&name)
867}
868
869pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
871 translated(context, StockMessage::SecureJoinGroupQRDescription)
872 .await
873 .replace1(chat.get_name())
874}
875
876pub(crate) async fn secure_join_broadcast_qr_description(context: &Context, chat: &Chat) -> String {
878 translated(context, StockMessage::SecureJoinBrodcastQRDescription)
879 .await
880 .replace1(chat.get_name())
881}
882
883#[allow(dead_code)]
885pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
886 let addr = contact.get_display_name();
887 translated(context, StockMessage::ContactVerified)
888 .await
889 .replace1(addr)
890}
891
892pub(crate) async fn archived_chats(context: &Context) -> String {
894 translated(context, StockMessage::ArchivedChats).await
895}
896
897pub(crate) async fn sync_msg_subject(context: &Context) -> String {
899 translated(context, StockMessage::SyncMsgSubject).await
900}
901
902pub(crate) async fn sync_msg_body(context: &Context) -> String {
904 translated(context, StockMessage::SyncMsgBody).await
905}
906
907pub(crate) async fn cannot_login(context: &Context, user: &str) -> String {
909 translated(context, StockMessage::CannotLogin)
910 .await
911 .replace1(user)
912}
913
914pub(crate) async fn msg_location_enabled(context: &Context) -> String {
916 translated(context, StockMessage::MsgLocationEnabled).await
917}
918
919pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
921 if contact == ContactId::SELF {
922 translated(context, StockMessage::MsgYouEnabledLocation).await
923 } else {
924 translated(context, StockMessage::MsgLocationEnabledBy)
925 .await
926 .replace1(&contact.get_stock_name(context).await)
927 }
928}
929
930pub(crate) async fn msg_location_disabled(context: &Context) -> String {
932 translated(context, StockMessage::MsgLocationDisabled).await
933}
934
935pub(crate) async fn location(context: &Context) -> String {
937 translated(context, StockMessage::Location).await
938}
939
940pub(crate) async fn sticker(context: &Context) -> String {
942 translated(context, StockMessage::Sticker).await
943}
944
945pub(crate) async fn device_messages(context: &Context) -> String {
947 translated(context, StockMessage::DeviceMessages).await
948}
949
950pub(crate) async fn saved_messages(context: &Context) -> String {
952 translated(context, StockMessage::SavedMessages).await
953}
954
955pub(crate) async fn device_messages_hint(context: &Context) -> String {
957 translated(context, StockMessage::DeviceMessagesHint).await
958}
959
960pub(crate) async fn welcome_message(context: &Context) -> String {
962 translated(context, StockMessage::WelcomeMessage).await
963}
964
965pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
968 translated(context, StockMessage::SubjectForNewContact)
969 .await
970 .replace1(self_name)
971}
972
973pub(crate) async fn msg_ephemeral_timer_disabled(
975 context: &Context,
976 by_contact: ContactId,
977) -> String {
978 if by_contact == ContactId::SELF {
979 translated(context, StockMessage::MsgYouDisabledEphemeralTimer).await
980 } else {
981 translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
982 .await
983 .replace1(&by_contact.get_stock_name(context).await)
984 }
985}
986
987pub(crate) async fn msg_ephemeral_timer_enabled(
989 context: &Context,
990 timer: &str,
991 by_contact: ContactId,
992) -> String {
993 if by_contact == ContactId::SELF {
994 translated(context, StockMessage::MsgYouEnabledEphemeralTimer)
995 .await
996 .replace1(timer)
997 } else {
998 translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
999 .await
1000 .replace1(timer)
1001 .replace2(&by_contact.get_stock_name(context).await)
1002 }
1003}
1004
1005pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
1007 if by_contact == ContactId::SELF {
1008 translated(context, StockMessage::MsgYouEphemeralTimerHour).await
1009 } else {
1010 translated(context, StockMessage::MsgEphemeralTimerHourBy)
1011 .await
1012 .replace1(&by_contact.get_stock_name(context).await)
1013 }
1014}
1015
1016pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
1018 if by_contact == ContactId::SELF {
1019 translated(context, StockMessage::MsgYouEphemeralTimerDay).await
1020 } else {
1021 translated(context, StockMessage::MsgEphemeralTimerDayBy)
1022 .await
1023 .replace1(&by_contact.get_stock_name(context).await)
1024 }
1025}
1026
1027pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
1029 if by_contact == ContactId::SELF {
1030 translated(context, StockMessage::MsgYouEphemeralTimerWeek).await
1031 } else {
1032 translated(context, StockMessage::MsgEphemeralTimerWeekBy)
1033 .await
1034 .replace1(&by_contact.get_stock_name(context).await)
1035 }
1036}
1037
1038pub(crate) async fn msg_ephemeral_timer_year(context: &Context, by_contact: ContactId) -> String {
1040 if by_contact == ContactId::SELF {
1041 translated(context, StockMessage::MsgYouEphemeralTimerYear).await
1042 } else {
1043 translated(context, StockMessage::MsgEphemeralTimerYearBy)
1044 .await
1045 .replace1(&by_contact.get_stock_name(context).await)
1046 }
1047}
1048
1049pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
1051 translated(context, StockMessage::ConfigurationFailed)
1052 .await
1053 .replace1(details)
1054}
1055
1056pub(crate) async fn bad_time_msg_body(context: &Context, now: &str) -> String {
1059 translated(context, StockMessage::BadTimeMsgBody)
1060 .await
1061 .replace1(now)
1062}
1063
1064pub(crate) async fn update_reminder_msg_body(context: &Context) -> String {
1066 translated(context, StockMessage::UpdateReminderMsgBody).await
1067}
1068
1069pub(crate) async fn error_no_network(context: &Context) -> String {
1071 translated(context, StockMessage::ErrorNoNetwork).await
1072}
1073
1074pub(crate) async fn messages_e2ee_info_msg(context: &Context) -> String {
1076 translated(context, StockMessage::ChatProtectionEnabled).await
1077}
1078
1079pub(crate) async fn messages_are_e2ee(context: &Context) -> String {
1081 translated(context, StockMessage::MessagesAreE2ee).await
1082}
1083
1084pub(crate) async fn reply_noun(context: &Context) -> String {
1086 translated(context, StockMessage::ReplyNoun).await
1087}
1088
1089pub(crate) async fn self_deleted_msg_body(context: &Context) -> String {
1091 translated(context, StockMessage::SelfDeletedMsgBody).await
1092}
1093
1094pub(crate) async fn msg_ephemeral_timer_minutes(
1096 context: &Context,
1097 minutes: &str,
1098 by_contact: ContactId,
1099) -> String {
1100 if by_contact == ContactId::SELF {
1101 translated(context, StockMessage::MsgYouEphemeralTimerMinutes)
1102 .await
1103 .replace1(minutes)
1104 } else {
1105 translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
1106 .await
1107 .replace1(minutes)
1108 .replace2(&by_contact.get_stock_name(context).await)
1109 }
1110}
1111
1112pub(crate) async fn msg_ephemeral_timer_hours(
1114 context: &Context,
1115 hours: &str,
1116 by_contact: ContactId,
1117) -> String {
1118 if by_contact == ContactId::SELF {
1119 translated(context, StockMessage::MsgYouEphemeralTimerHours)
1120 .await
1121 .replace1(hours)
1122 } else {
1123 translated(context, StockMessage::MsgEphemeralTimerHoursBy)
1124 .await
1125 .replace1(hours)
1126 .replace2(&by_contact.get_stock_name(context).await)
1127 }
1128}
1129
1130pub(crate) async fn msg_ephemeral_timer_days(
1132 context: &Context,
1133 days: &str,
1134 by_contact: ContactId,
1135) -> String {
1136 if by_contact == ContactId::SELF {
1137 translated(context, StockMessage::MsgYouEphemeralTimerDays)
1138 .await
1139 .replace1(days)
1140 } else {
1141 translated(context, StockMessage::MsgEphemeralTimerDaysBy)
1142 .await
1143 .replace1(days)
1144 .replace2(&by_contact.get_stock_name(context).await)
1145 }
1146}
1147
1148pub(crate) async fn msg_ephemeral_timer_weeks(
1150 context: &Context,
1151 weeks: &str,
1152 by_contact: ContactId,
1153) -> String {
1154 if by_contact == ContactId::SELF {
1155 translated(context, StockMessage::MsgYouEphemeralTimerWeeks)
1156 .await
1157 .replace1(weeks)
1158 } else {
1159 translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
1160 .await
1161 .replace1(weeks)
1162 .replace2(&by_contact.get_stock_name(context).await)
1163 }
1164}
1165
1166pub(crate) async fn forwarded(context: &Context) -> String {
1168 translated(context, StockMessage::Forwarded).await
1169}
1170
1171pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
1173 translated(context, StockMessage::QuotaExceedingMsgBody)
1174 .await
1175 .replace1(&format!("{highest_usage}"))
1176 .replace("%%", "%")
1177}
1178
1179pub(crate) async fn incoming_messages(context: &Context) -> String {
1181 translated(context, StockMessage::IncomingMessages).await
1182}
1183
1184pub(crate) async fn outgoing_messages(context: &Context) -> String {
1186 translated(context, StockMessage::OutgoingMessages).await
1187}
1188
1189pub(crate) async fn not_connected(context: &Context) -> String {
1191 translated(context, StockMessage::NotConnected).await
1192}
1193
1194pub(crate) async fn connected(context: &Context) -> String {
1196 translated(context, StockMessage::Connected).await
1197}
1198
1199pub(crate) async fn connecting(context: &Context) -> String {
1201 translated(context, StockMessage::Connecting).await
1202}
1203
1204pub(crate) async fn updating(context: &Context) -> String {
1206 translated(context, StockMessage::Updating).await
1207}
1208
1209pub(crate) async fn sending(context: &Context) -> String {
1211 translated(context, StockMessage::Sending).await
1212}
1213
1214pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
1216 translated(context, StockMessage::LastMsgSentSuccessfully).await
1217}
1218
1219pub(crate) async fn error(context: &Context, error: &str) -> String {
1222 translated(context, StockMessage::Error)
1223 .await
1224 .replace1(error)
1225}
1226
1227pub(crate) async fn not_supported_by_provider(context: &Context) -> String {
1229 translated(context, StockMessage::NotSupportedByProvider).await
1230}
1231
1232pub(crate) async fn messages(context: &Context) -> String {
1235 translated(context, StockMessage::Messages).await
1236}
1237
1238pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
1240 translated(context, StockMessage::PartOfTotallUsed)
1241 .await
1242 .replace1(part)
1243 .replace2(total)
1244}
1245
1246pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
1248 translated(context, StockMessage::InvalidUnencryptedMail)
1249 .await
1250 .replace1(provider)
1251}
1252
1253pub(crate) async fn stats_msg_body(context: &Context) -> String {
1255 translated(context, StockMessage::StatsMsgBody).await
1256}
1257
1258pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
1260 translated(context, StockMessage::NewGroupSendFirstMessage).await
1261}
1262
1263pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
1270 let name = if let Some(name) = context.get_config(Config::Displayname).await? {
1271 name
1272 } else {
1273 context.get_primary_self_addr().await?
1274 };
1275 Ok(translated(context, StockMessage::BackupTransferQr)
1276 .await
1277 .replace1(&name))
1278}
1279
1280pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String {
1281 translated(context, StockMessage::BackupTransferMsgBody).await
1282}
1283
1284pub(crate) async fn proxy_enabled(context: &Context) -> String {
1286 translated(context, StockMessage::ProxyEnabled).await
1287}
1288
1289pub(crate) async fn proxy_description(context: &Context) -> String {
1291 translated(context, StockMessage::ProxyEnabledDescription).await
1292}
1293
1294pub(crate) async fn chat_unencrypted_explanation(context: &Context) -> String {
1296 translated(context, StockMessage::ChatUnencryptedExplanation).await
1297}
1298
1299pub(crate) async fn mvbox_move_deprecation(context: &Context) -> String {
1301 translated(context, StockMessage::MvboxMoveDeprecation).await
1302}
1303
1304impl Viewtype {
1305 pub async fn to_locale_string(&self, context: &Context) -> String {
1307 match self {
1308 Viewtype::Image => image(context).await,
1309 Viewtype::Gif => gif(context).await,
1310 Viewtype::Sticker => sticker(context).await,
1311 Viewtype::Audio => audio(context).await,
1312 Viewtype::Voice => voice_message(context).await,
1313 Viewtype::Video => video(context).await,
1314 Viewtype::File => file(context).await,
1315 Viewtype::Webxdc => "Mini App".to_owned(),
1316 Viewtype::Vcard => "👤".to_string(),
1317 Viewtype::Unknown | Viewtype::Text | Viewtype::Call => self.to_string(),
1319 }
1320 }
1321}
1322
1323impl Context {
1324 pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1327 self.translated_stockstrings
1328 .set_stock_translation(id, stockstring)
1329 .await?;
1330 Ok(())
1331 }
1332
1333 pub(crate) async fn update_device_chats(&self) -> Result<()> {
1334 if self.get_config_bool(Config::Bot).await? {
1335 return Ok(());
1336 }
1337
1338 if !self.sql.get_raw_config_bool("self-chat-added").await? {
1341 self.sql
1342 .set_raw_config_bool("self-chat-added", true)
1343 .await?;
1344 ChatId::create_for_contact(self, ContactId::SELF).await?;
1345 }
1346
1347 let image = include_bytes!("../assets/welcome-image.jpg");
1350 let blob = BlobObject::create_and_deduplicate_from_bytes(self, image, "welcome.jpg")?;
1351 let mut msg = Message::new(Viewtype::Image);
1352 msg.param.set(Param::File, blob.as_name());
1353 msg.param.set(Param::Filename, "welcome-image.jpg");
1354 chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
1355
1356 let mut msg = Message::new_text(welcome_message(self).await);
1357 chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
1358 Ok(())
1359 }
1360}
1361
1362impl Accounts {
1363 pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1366 self.stockstrings
1367 .set_stock_translation(id, stockstring)
1368 .await?;
1369 Ok(())
1370 }
1371}
1372
1373#[cfg(test)]
1374mod stock_str_tests;