1use 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#[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(fallback = "Multi Device Synchronization"))]
157 SyncMsgSubject = 101,
158
159 #[strum(props(
160 fallback = "This message is used to synchronize data between your devices.\n\n\
161 👉 If you see this message in Delta Chat, please update your Delta Chat apps on all devices."
162 ))]
163 SyncMsgBody = 102,
164
165 #[strum(props(fallback = "Incoming Messages"))]
166 IncomingMessages = 103,
167
168 #[strum(props(fallback = "Outgoing Messages"))]
169 OutgoingMessages = 104,
170
171 #[strum(props(fallback = "Storage on %1$s"))]
172 StorageOnDomain = 105,
173
174 #[strum(props(fallback = "Connected"))]
175 Connected = 107,
176
177 #[strum(props(fallback = "Connecting…"))]
178 Connecting = 108,
179
180 #[strum(props(fallback = "Updating…"))]
181 Updating = 109,
182
183 #[strum(props(fallback = "Sending…"))]
184 Sending = 110,
185
186 #[strum(props(fallback = "Your last message was sent successfully."))]
187 LastMsgSentSuccessfully = 111,
188
189 #[strum(props(fallback = "Error: %1$s"))]
190 Error = 112,
191
192 #[strum(props(fallback = "Not supported by your provider."))]
193 NotSupportedByProvider = 113,
194
195 #[strum(props(fallback = "Messages"))]
196 Messages = 114,
197
198 #[strum(props(fallback = "%1$s of %2$s used"))]
199 PartOfTotallUsed = 116,
200
201 #[strum(props(fallback = "%1$s invited you to join this group.\n\n\
202 Waiting for the device of %2$s to reply…"))]
203 SecureJoinStarted = 117,
204
205 #[strum(props(fallback = "%1$s replied, waiting for being added to the group…"))]
206 SecureJoinReplies = 118,
207
208 #[strum(props(fallback = "Scan to chat with %1$s"))]
209 SetupContactQRDescription = 119,
210
211 #[strum(props(fallback = "Scan to join group %1$s"))]
212 SecureJoinGroupQRDescription = 120,
213
214 #[strum(props(fallback = "Not connected"))]
215 NotConnected = 121,
216
217 #[strum(props(fallback = "You changed group name from \"%1$s\" to \"%2$s\"."))]
218 MsgYouChangedGrpName = 124,
219
220 #[strum(props(fallback = "Group name changed from \"%1$s\" to \"%2$s\" by %3$s."))]
221 MsgGrpNameChangedBy = 125,
222
223 #[strum(props(fallback = "You changed the group image."))]
224 MsgYouChangedGrpImg = 126,
225
226 #[strum(props(fallback = "Group image changed by %1$s."))]
227 MsgGrpImgChangedBy = 127,
228
229 #[strum(props(fallback = "You added member %1$s."))]
230 MsgYouAddMember = 128,
231
232 #[strum(props(fallback = "Member %1$s added by %2$s."))]
233 MsgAddMemberBy = 129,
234
235 #[strum(props(fallback = "You removed member %1$s."))]
236 MsgYouDelMember = 130,
237
238 #[strum(props(fallback = "Member %1$s removed by %2$s."))]
239 MsgDelMemberBy = 131,
240
241 #[strum(props(fallback = "You left the group."))]
242 MsgYouLeftGroup = 132,
243
244 #[strum(props(fallback = "Group left by %1$s."))]
245 MsgGroupLeftBy = 133,
246
247 #[strum(props(fallback = "You deleted the group image."))]
248 MsgYouDeletedGrpImg = 134,
249
250 #[strum(props(fallback = "Group image deleted by %1$s."))]
251 MsgGrpImgDeletedBy = 135,
252
253 #[strum(props(fallback = "You enabled location streaming."))]
254 MsgYouEnabledLocation = 136,
255
256 #[strum(props(fallback = "Location streaming enabled by %1$s."))]
257 MsgLocationEnabledBy = 137,
258
259 #[strum(props(fallback = "You disabled message deletion timer."))]
260 MsgYouDisabledEphemeralTimer = 138,
261
262 #[strum(props(fallback = "Message deletion timer is disabled by %1$s."))]
263 MsgEphemeralTimerDisabledBy = 139,
264
265 #[strum(props(fallback = "You set message deletion timer to %1$s s."))]
268 MsgYouEnabledEphemeralTimer = 140,
269
270 #[strum(props(fallback = "Message deletion timer is set to %1$s s by %2$s."))]
271 MsgEphemeralTimerEnabledBy = 141,
272
273 #[strum(props(fallback = "You set message deletion timer to 1 hour."))]
274 MsgYouEphemeralTimerHour = 144,
275
276 #[strum(props(fallback = "Message deletion timer is set to 1 hour by %1$s."))]
277 MsgEphemeralTimerHourBy = 145,
278
279 #[strum(props(fallback = "You set message deletion timer to 1 day."))]
280 MsgYouEphemeralTimerDay = 146,
281
282 #[strum(props(fallback = "Message deletion timer is set to 1 day by %1$s."))]
283 MsgEphemeralTimerDayBy = 147,
284
285 #[strum(props(fallback = "You set message deletion timer to 1 week."))]
286 MsgYouEphemeralTimerWeek = 148,
287
288 #[strum(props(fallback = "Message deletion timer is set to 1 week by %1$s."))]
289 MsgEphemeralTimerWeekBy = 149,
290
291 #[strum(props(fallback = "You set message deletion timer to %1$s minutes."))]
292 MsgYouEphemeralTimerMinutes = 150,
293
294 #[strum(props(fallback = "Message deletion timer is set to %1$s minutes by %2$s."))]
295 MsgEphemeralTimerMinutesBy = 151,
296
297 #[strum(props(fallback = "You set message deletion timer to %1$s hours."))]
298 MsgYouEphemeralTimerHours = 152,
299
300 #[strum(props(fallback = "Message deletion timer is set to %1$s hours by %2$s."))]
301 MsgEphemeralTimerHoursBy = 153,
302
303 #[strum(props(fallback = "You set message deletion timer to %1$s days."))]
304 MsgYouEphemeralTimerDays = 154,
305
306 #[strum(props(fallback = "Message deletion timer is set to %1$s days by %2$s."))]
307 MsgEphemeralTimerDaysBy = 155,
308
309 #[strum(props(fallback = "You set message deletion timer to %1$s weeks."))]
310 MsgYouEphemeralTimerWeeks = 156,
311
312 #[strum(props(fallback = "Message deletion timer is set to %1$s weeks by %2$s."))]
313 MsgEphemeralTimerWeeksBy = 157,
314
315 #[strum(props(fallback = "You set message deletion timer to 1 year."))]
316 MsgYouEphemeralTimerYear = 158,
317
318 #[strum(props(fallback = "Message deletion timer is set to 1 year by %1$s."))]
319 MsgEphemeralTimerYearBy = 159,
320
321 #[strum(props(fallback = "Scan to set up second device for %1$s"))]
322 BackupTransferQr = 162,
323
324 #[strum(props(fallback = "ℹ️ Account transferred to your second device."))]
325 BackupTransferMsgBody = 163,
326
327 #[strum(props(fallback = "Messages are end-to-end encrypted."))]
328 ChatProtectionEnabled = 170,
329
330 #[strum(props(fallback = "Others will only see this group after you sent a first message."))]
331 NewGroupSendFirstMessage = 172,
332
333 #[strum(props(fallback = "Member %1$s added."))]
334 MsgAddMember = 173,
335
336 #[strum(props(
337 fallback = "⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet."
338 ))]
339 InvalidUnencryptedMail = 174,
340
341 #[strum(props(fallback = "You reacted %1$s to \"%2$s\""))]
342 MsgYouReacted = 176,
343
344 #[strum(props(fallback = "%1$s reacted %2$s to \"%3$s\""))]
345 MsgReactedBy = 177,
346
347 #[strum(props(fallback = "Member %1$s removed."))]
348 MsgDelMember = 178,
349
350 #[strum(props(fallback = "Establishing connection, please wait…"))]
351 SecurejoinWait = 190,
352
353 #[strum(props(fallback = "❤️ Seems you're enjoying Delta Chat!
354
355Please consider donating to help that Delta Chat stays free for everyone.
356
357While Delta Chat is free to use and open source, development costs money.
358Help keeping us to keep Delta Chat independent and make it more awesome in the future.
359
360https://delta.chat/donate"))]
361 DonationRequest = 193,
362
363 #[strum(props(fallback = "Declined call"))]
364 DeclinedCall = 196,
365
366 #[strum(props(fallback = "Canceled call"))]
367 CanceledCall = 197,
368
369 #[strum(props(fallback = "Missed call"))]
370 MissedCall = 198,
371
372 #[strum(props(fallback = "You left the channel."))]
373 MsgYouLeftBroadcast = 200,
374
375 #[strum(props(fallback = "Scan to join channel %1$s"))]
376 SecureJoinBrodcastQRDescription = 201,
377
378 #[strum(props(fallback = "You joined the channel."))]
379 MsgYouJoinedBroadcast = 202,
380
381 #[strum(props(fallback = "%1$s invited you to join this channel.\n\n\
382 Waiting for the device of %2$s to reply…"))]
383 SecureJoinBroadcastStarted = 203,
384
385 #[strum(props(fallback = "Channel name changed from \"%1$s\" to \"%2$s\"."))]
386 MsgBroadcastNameChanged = 204,
387
388 #[strum(props(fallback = "Channel image changed."))]
389 MsgBroadcastImgChanged = 205,
390
391 #[strum(props(
392 fallback = "The attachment contains anonymous usage statistics, which helps us improve Delta Chat. Thank you!"
393 ))]
394 StatsMsgBody = 210,
395
396 #[strum(props(fallback = "Proxy Enabled"))]
397 ProxyEnabled = 220,
398
399 #[strum(props(
400 fallback = "You are using a proxy. If you're having trouble connecting, try a different proxy."
401 ))]
402 ProxyEnabledDescription = 221,
403
404 #[strum(props(fallback = "Messages in this chat use classic email and are not encrypted."))]
405 ChatUnencryptedExplanation = 230,
406
407 #[strum(props(fallback = "Outgoing audio call"))]
408 OutgoingAudioCall = 232,
409
410 #[strum(props(fallback = "Outgoing video call"))]
411 OutgoingVideoCall = 233,
412
413 #[strum(props(fallback = "Incoming audio call"))]
414 IncomingAudioCall = 234,
415
416 #[strum(props(fallback = "Incoming video call"))]
417 IncomingVideoCall = 235,
418
419 #[strum(props(fallback = "You changed the chat description."))]
420 MsgYouChangedDescription = 240,
421
422 #[strum(props(fallback = "Chat description changed by %1$s."))]
423 MsgChatDescriptionChangedBy = 241,
424
425 #[strum(props(fallback = "Messages are end-to-end encrypted."))]
426 MessagesAreE2ee = 242,
427}
428
429impl StockMessage {
430 fn fallback(self) -> &'static str {
434 self.get_str("fallback").unwrap_or_default()
435 }
436}
437
438impl Default for StockStrings {
439 fn default() -> Self {
440 StockStrings::new()
441 }
442}
443
444impl StockStrings {
445 pub fn new() -> Self {
447 Self {
448 translated_stockstrings: Arc::new(RwLock::new(Default::default())),
449 }
450 }
451
452 fn translated(&self, id: StockMessage) -> String {
453 self.translated_stockstrings
454 .read()
455 .get(&(id as usize))
456 .map(AsRef::as_ref)
457 .unwrap_or_else(|| id.fallback())
458 .to_string()
459 }
460
461 fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
462 if stockstring.contains("%1") && !id.fallback().contains("%1") {
463 bail!(
464 "translation {} contains invalid %1 placeholder, default is {}",
465 stockstring,
466 id.fallback()
467 );
468 }
469 if stockstring.contains("%2") && !id.fallback().contains("%2") {
470 bail!(
471 "translation {} contains invalid %2 placeholder, default is {}",
472 stockstring,
473 id.fallback()
474 );
475 }
476 self.translated_stockstrings
477 .write()
478 .insert(id as usize, stockstring);
479 Ok(())
480 }
481}
482
483fn translated(context: &Context, id: StockMessage) -> String {
484 context.translated_stockstrings.translated(id)
485}
486
487trait StockStringMods: AsRef<str> + Sized {
489 fn replace1(&self, replacement: &str) -> String {
491 self.as_ref()
492 .replacen("%1$s", replacement, 1)
493 .replacen("%1$d", replacement, 1)
494 .replacen("%1$@", replacement, 1)
495 }
496
497 fn replace2(&self, replacement: &str) -> String {
502 self.as_ref()
503 .replacen("%2$s", replacement, 1)
504 .replacen("%2$d", replacement, 1)
505 .replacen("%2$@", replacement, 1)
506 }
507
508 fn replace3(&self, replacement: &str) -> String {
513 self.as_ref()
514 .replacen("%3$s", replacement, 1)
515 .replacen("%3$d", replacement, 1)
516 .replacen("%3$@", replacement, 1)
517 }
518}
519
520impl ContactId {
521 async fn get_stock_name(self, context: &Context) -> String {
523 Contact::get_by_id(context, self)
524 .await
525 .map(|contact| contact.get_display_name().to_string())
526 .unwrap_or_else(|_| self.to_string())
527 }
528}
529
530impl StockStringMods for String {}
531
532pub(crate) fn no_messages(context: &Context) -> String {
534 translated(context, StockMessage::NoMessages)
535}
536
537pub(crate) fn self_msg(context: &Context) -> String {
539 translated(context, StockMessage::SelfMsg)
540}
541
542pub(crate) fn draft(context: &Context) -> String {
544 translated(context, StockMessage::Draft)
545}
546
547pub(crate) fn voice_message(context: &Context) -> String {
549 translated(context, StockMessage::VoiceMessage)
550}
551
552pub(crate) fn image(context: &Context) -> String {
554 translated(context, StockMessage::Image)
555}
556
557pub(crate) fn video(context: &Context) -> String {
559 translated(context, StockMessage::Video)
560}
561
562pub(crate) fn audio(context: &Context) -> String {
564 translated(context, StockMessage::Audio)
565}
566
567pub(crate) fn file(context: &Context) -> String {
569 translated(context, StockMessage::File)
570}
571
572pub(crate) async fn msg_grp_name(
574 context: &Context,
575 from_group: &str,
576 to_group: &str,
577 by_contact: ContactId,
578) -> String {
579 if by_contact == ContactId::SELF {
580 translated(context, StockMessage::MsgYouChangedGrpName)
581 .replace1(from_group)
582 .replace2(to_group)
583 } else {
584 translated(context, StockMessage::MsgGrpNameChangedBy)
585 .replace1(from_group)
586 .replace2(to_group)
587 .replace3(&by_contact.get_stock_name(context).await)
588 }
589}
590
591pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
592 if by_contact == ContactId::SELF {
593 translated(context, StockMessage::MsgYouChangedGrpImg)
594 } else {
595 translated(context, StockMessage::MsgGrpImgChangedBy)
596 .replace1(&by_contact.get_stock_name(context).await)
597 }
598}
599
600pub(crate) async fn msg_chat_description_changed(
601 context: &Context,
602 by_contact: ContactId,
603) -> String {
604 if by_contact == ContactId::SELF {
605 translated(context, StockMessage::MsgYouChangedDescription)
606 } else {
607 translated(context, StockMessage::MsgChatDescriptionChangedBy)
608 .replace1(&by_contact.get_stock_name(context).await)
609 }
610}
611
612pub(crate) async fn msg_add_member_local(
617 context: &Context,
618 added_member: ContactId,
619 by_contact: ContactId,
620) -> String {
621 let whom = added_member.get_stock_name(context).await;
622 if by_contact == ContactId::UNDEFINED {
623 translated(context, StockMessage::MsgAddMember).replace1(&whom)
624 } else if by_contact == ContactId::SELF {
625 translated(context, StockMessage::MsgYouAddMember).replace1(&whom)
626 } else {
627 translated(context, StockMessage::MsgAddMemberBy)
628 .replace1(&whom)
629 .replace2(&by_contact.get_stock_name(context).await)
630 }
631}
632
633pub(crate) async fn msg_del_member_local(
638 context: &Context,
639 removed_member: ContactId,
640 by_contact: ContactId,
641) -> String {
642 let whom = removed_member.get_stock_name(context).await;
643 if by_contact == ContactId::UNDEFINED {
644 translated(context, StockMessage::MsgDelMember).replace1(&whom)
645 } else if by_contact == ContactId::SELF {
646 translated(context, StockMessage::MsgYouDelMember).replace1(&whom)
647 } else {
648 translated(context, StockMessage::MsgDelMemberBy)
649 .replace1(&whom)
650 .replace2(&by_contact.get_stock_name(context).await)
651 }
652}
653
654pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
656 if by_contact == ContactId::SELF {
657 translated(context, StockMessage::MsgYouLeftGroup)
658 } else {
659 translated(context, StockMessage::MsgGroupLeftBy)
660 .replace1(&by_contact.get_stock_name(context).await)
661 }
662}
663
664pub(crate) fn msg_you_left_broadcast(context: &Context) -> String {
666 translated(context, StockMessage::MsgYouLeftBroadcast)
667}
668
669pub(crate) fn msg_you_joined_broadcast(context: &Context) -> String {
671 translated(context, StockMessage::MsgYouJoinedBroadcast)
672}
673
674pub(crate) async fn secure_join_broadcast_started(
676 context: &Context,
677 inviter_contact_id: ContactId,
678) -> String {
679 if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
680 translated(context, StockMessage::SecureJoinBroadcastStarted)
681 .replace1(contact.get_display_name())
682 .replace2(contact.get_display_name())
683 } else {
684 format!("secure_join_started: unknown contact {inviter_contact_id}")
685 }
686}
687
688pub(crate) fn msg_broadcast_name_changed(context: &Context, from: &str, to: &str) -> String {
690 translated(context, StockMessage::MsgBroadcastNameChanged)
691 .replace1(from)
692 .replace2(to)
693}
694
695pub(crate) fn msg_broadcast_img_changed(context: &Context) -> String {
697 translated(context, StockMessage::MsgBroadcastImgChanged)
698}
699
700pub(crate) async fn msg_reacted(
702 context: &Context,
703 by_contact: ContactId,
704 reaction: &str,
705 summary: &str,
706) -> String {
707 if by_contact == ContactId::SELF {
708 translated(context, StockMessage::MsgYouReacted)
709 .replace1(reaction)
710 .replace2(summary)
711 } else {
712 translated(context, StockMessage::MsgReactedBy)
713 .replace1(&by_contact.get_stock_name(context).await)
714 .replace2(reaction)
715 .replace3(summary)
716 }
717}
718
719pub(crate) fn gif(context: &Context) -> String {
721 translated(context, StockMessage::Gif)
722}
723
724pub(crate) fn encr_none(context: &Context) -> String {
726 translated(context, StockMessage::EncrNone)
727}
728
729pub(crate) fn finger_prints(context: &Context) -> String {
731 translated(context, StockMessage::FingerPrints)
732}
733
734pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
736 if by_contact == ContactId::SELF {
737 translated(context, StockMessage::MsgYouDeletedGrpImg)
738 } else {
739 translated(context, StockMessage::MsgGrpImgDeletedBy)
740 .replace1(&by_contact.get_stock_name(context).await)
741 }
742}
743
744pub(crate) async fn secure_join_started(
746 context: &Context,
747 inviter_contact_id: ContactId,
748) -> String {
749 if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
750 translated(context, StockMessage::SecureJoinStarted)
751 .replace1(contact.get_display_name())
752 .replace2(contact.get_display_name())
753 } else {
754 format!("secure_join_started: unknown contact {inviter_contact_id}")
755 }
756}
757
758pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
760 translated(context, StockMessage::SecureJoinReplies)
761 .replace1(&contact_id.get_stock_name(context).await)
762}
763
764pub(crate) fn securejoin_wait(context: &Context) -> String {
766 translated(context, StockMessage::SecurejoinWait)
767}
768
769pub(crate) fn donation_request(context: &Context) -> String {
771 translated(context, StockMessage::DonationRequest)
772}
773
774pub(crate) fn outgoing_call(context: &Context, has_video: bool) -> String {
776 translated(
777 context,
778 if has_video {
779 StockMessage::OutgoingVideoCall
780 } else {
781 StockMessage::OutgoingAudioCall
782 },
783 )
784}
785
786pub(crate) fn incoming_call(context: &Context, has_video: bool) -> String {
788 translated(
789 context,
790 if has_video {
791 StockMessage::IncomingVideoCall
792 } else {
793 StockMessage::IncomingAudioCall
794 },
795 )
796}
797
798pub(crate) fn declined_call(context: &Context) -> String {
800 translated(context, StockMessage::DeclinedCall)
801}
802
803pub(crate) fn canceled_call(context: &Context) -> String {
805 translated(context, StockMessage::CanceledCall)
806}
807
808pub(crate) fn missed_call(context: &Context) -> String {
810 translated(context, StockMessage::MissedCall)
811}
812
813pub(crate) fn setup_contact_qr_description(
815 context: &Context,
816 display_name: &str,
817 addr: &str,
818) -> String {
819 let name = if display_name.is_empty() {
820 addr.to_owned()
821 } else {
822 display_name.to_owned()
823 };
824 translated(context, StockMessage::SetupContactQRDescription).replace1(&name)
825}
826
827pub(crate) fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
829 translated(context, StockMessage::SecureJoinGroupQRDescription).replace1(chat.get_name())
830}
831
832pub(crate) fn secure_join_broadcast_qr_description(context: &Context, chat: &Chat) -> String {
834 translated(context, StockMessage::SecureJoinBrodcastQRDescription).replace1(chat.get_name())
835}
836
837#[allow(dead_code)]
839pub(crate) fn contact_verified(context: &Context, contact: &Contact) -> String {
840 let addr = contact.get_display_name();
841 translated(context, StockMessage::ContactVerified).replace1(addr)
842}
843
844pub(crate) fn archived_chats(context: &Context) -> String {
846 translated(context, StockMessage::ArchivedChats)
847}
848
849pub(crate) fn sync_msg_subject(context: &Context) -> String {
851 translated(context, StockMessage::SyncMsgSubject)
852}
853
854pub(crate) fn sync_msg_body(context: &Context) -> String {
856 translated(context, StockMessage::SyncMsgBody)
857}
858
859pub(crate) fn cannot_login(context: &Context, user: &str) -> String {
861 translated(context, StockMessage::CannotLogin).replace1(user)
862}
863
864pub(crate) fn msg_location_enabled(context: &Context) -> String {
866 translated(context, StockMessage::MsgLocationEnabled)
867}
868
869pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
871 if contact == ContactId::SELF {
872 translated(context, StockMessage::MsgYouEnabledLocation)
873 } else {
874 translated(context, StockMessage::MsgLocationEnabledBy)
875 .replace1(&contact.get_stock_name(context).await)
876 }
877}
878
879pub(crate) fn msg_location_disabled(context: &Context) -> String {
881 translated(context, StockMessage::MsgLocationDisabled)
882}
883
884pub(crate) fn location(context: &Context) -> String {
886 translated(context, StockMessage::Location)
887}
888
889pub(crate) fn sticker(context: &Context) -> String {
891 translated(context, StockMessage::Sticker)
892}
893
894pub(crate) fn device_messages(context: &Context) -> String {
896 translated(context, StockMessage::DeviceMessages)
897}
898
899pub(crate) fn saved_messages(context: &Context) -> String {
901 translated(context, StockMessage::SavedMessages)
902}
903
904pub(crate) fn device_messages_hint(context: &Context) -> String {
906 translated(context, StockMessage::DeviceMessagesHint)
907}
908
909pub(crate) fn welcome_message(context: &Context) -> String {
911 translated(context, StockMessage::WelcomeMessage)
912}
913
914pub(crate) fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
917 translated(context, StockMessage::SubjectForNewContact).replace1(self_name)
918}
919
920pub(crate) async fn msg_ephemeral_timer_disabled(
922 context: &Context,
923 by_contact: ContactId,
924) -> String {
925 if by_contact == ContactId::SELF {
926 translated(context, StockMessage::MsgYouDisabledEphemeralTimer)
927 } else {
928 translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
929 .replace1(&by_contact.get_stock_name(context).await)
930 }
931}
932
933pub(crate) async fn msg_ephemeral_timer_enabled(
935 context: &Context,
936 timer: &str,
937 by_contact: ContactId,
938) -> String {
939 if by_contact == ContactId::SELF {
940 translated(context, StockMessage::MsgYouEnabledEphemeralTimer).replace1(timer)
941 } else {
942 translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
943 .replace1(timer)
944 .replace2(&by_contact.get_stock_name(context).await)
945 }
946}
947
948pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
950 if by_contact == ContactId::SELF {
951 translated(context, StockMessage::MsgYouEphemeralTimerHour)
952 } else {
953 translated(context, StockMessage::MsgEphemeralTimerHourBy)
954 .replace1(&by_contact.get_stock_name(context).await)
955 }
956}
957
958pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
960 if by_contact == ContactId::SELF {
961 translated(context, StockMessage::MsgYouEphemeralTimerDay)
962 } else {
963 translated(context, StockMessage::MsgEphemeralTimerDayBy)
964 .replace1(&by_contact.get_stock_name(context).await)
965 }
966}
967
968pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
970 if by_contact == ContactId::SELF {
971 translated(context, StockMessage::MsgYouEphemeralTimerWeek)
972 } else {
973 translated(context, StockMessage::MsgEphemeralTimerWeekBy)
974 .replace1(&by_contact.get_stock_name(context).await)
975 }
976}
977
978pub(crate) async fn msg_ephemeral_timer_year(context: &Context, by_contact: ContactId) -> String {
980 if by_contact == ContactId::SELF {
981 translated(context, StockMessage::MsgYouEphemeralTimerYear)
982 } else {
983 translated(context, StockMessage::MsgEphemeralTimerYearBy)
984 .replace1(&by_contact.get_stock_name(context).await)
985 }
986}
987
988pub(crate) fn configuration_failed(context: &Context, details: &str) -> String {
990 translated(context, StockMessage::ConfigurationFailed).replace1(details)
991}
992
993pub(crate) fn bad_time_msg_body(context: &Context, now: &str) -> String {
996 translated(context, StockMessage::BadTimeMsgBody).replace1(now)
997}
998
999pub(crate) fn update_reminder_msg_body(context: &Context) -> String {
1001 translated(context, StockMessage::UpdateReminderMsgBody)
1002}
1003
1004pub(crate) fn error_no_network(context: &Context) -> String {
1006 translated(context, StockMessage::ErrorNoNetwork)
1007}
1008
1009pub(crate) fn messages_e2ee_info_msg(context: &Context) -> String {
1011 translated(context, StockMessage::ChatProtectionEnabled)
1012}
1013
1014pub(crate) fn messages_are_e2ee(context: &Context) -> String {
1016 translated(context, StockMessage::MessagesAreE2ee)
1017}
1018
1019pub(crate) fn reply_noun(context: &Context) -> String {
1021 translated(context, StockMessage::ReplyNoun)
1022}
1023
1024pub(crate) fn self_deleted_msg_body(context: &Context) -> String {
1026 translated(context, StockMessage::SelfDeletedMsgBody)
1027}
1028
1029pub(crate) async fn msg_ephemeral_timer_minutes(
1031 context: &Context,
1032 minutes: &str,
1033 by_contact: ContactId,
1034) -> String {
1035 if by_contact == ContactId::SELF {
1036 translated(context, StockMessage::MsgYouEphemeralTimerMinutes).replace1(minutes)
1037 } else {
1038 translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
1039 .replace1(minutes)
1040 .replace2(&by_contact.get_stock_name(context).await)
1041 }
1042}
1043
1044pub(crate) async fn msg_ephemeral_timer_hours(
1046 context: &Context,
1047 hours: &str,
1048 by_contact: ContactId,
1049) -> String {
1050 if by_contact == ContactId::SELF {
1051 translated(context, StockMessage::MsgYouEphemeralTimerHours).replace1(hours)
1052 } else {
1053 translated(context, StockMessage::MsgEphemeralTimerHoursBy)
1054 .replace1(hours)
1055 .replace2(&by_contact.get_stock_name(context).await)
1056 }
1057}
1058
1059pub(crate) async fn msg_ephemeral_timer_days(
1061 context: &Context,
1062 days: &str,
1063 by_contact: ContactId,
1064) -> String {
1065 if by_contact == ContactId::SELF {
1066 translated(context, StockMessage::MsgYouEphemeralTimerDays).replace1(days)
1067 } else {
1068 translated(context, StockMessage::MsgEphemeralTimerDaysBy)
1069 .replace1(days)
1070 .replace2(&by_contact.get_stock_name(context).await)
1071 }
1072}
1073
1074pub(crate) async fn msg_ephemeral_timer_weeks(
1076 context: &Context,
1077 weeks: &str,
1078 by_contact: ContactId,
1079) -> String {
1080 if by_contact == ContactId::SELF {
1081 translated(context, StockMessage::MsgYouEphemeralTimerWeeks).replace1(weeks)
1082 } else {
1083 translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
1084 .replace1(weeks)
1085 .replace2(&by_contact.get_stock_name(context).await)
1086 }
1087}
1088
1089pub(crate) fn forwarded(context: &Context) -> String {
1091 translated(context, StockMessage::Forwarded)
1092}
1093
1094pub(crate) fn incoming_messages(context: &Context) -> String {
1096 translated(context, StockMessage::IncomingMessages)
1097}
1098
1099pub(crate) fn outgoing_messages(context: &Context) -> String {
1101 translated(context, StockMessage::OutgoingMessages)
1102}
1103
1104pub(crate) fn not_connected(context: &Context) -> String {
1106 translated(context, StockMessage::NotConnected)
1107}
1108
1109pub(crate) fn connected(context: &Context) -> String {
1111 translated(context, StockMessage::Connected)
1112}
1113
1114pub(crate) fn connecting(context: &Context) -> String {
1116 translated(context, StockMessage::Connecting)
1117}
1118
1119pub(crate) fn updating(context: &Context) -> String {
1121 translated(context, StockMessage::Updating)
1122}
1123
1124pub(crate) fn sending(context: &Context) -> String {
1126 translated(context, StockMessage::Sending)
1127}
1128
1129pub(crate) fn last_msg_sent_successfully(context: &Context) -> String {
1131 translated(context, StockMessage::LastMsgSentSuccessfully)
1132}
1133
1134pub(crate) fn error(context: &Context, error: &str) -> String {
1137 translated(context, StockMessage::Error).replace1(error)
1138}
1139
1140pub(crate) fn not_supported_by_provider(context: &Context) -> String {
1142 translated(context, StockMessage::NotSupportedByProvider)
1143}
1144
1145pub(crate) fn messages(context: &Context) -> String {
1148 translated(context, StockMessage::Messages)
1149}
1150
1151pub(crate) fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
1153 translated(context, StockMessage::PartOfTotallUsed)
1154 .replace1(part)
1155 .replace2(total)
1156}
1157
1158pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
1160 translated(context, StockMessage::InvalidUnencryptedMail).replace1(provider)
1161}
1162
1163pub(crate) fn stats_msg_body(context: &Context) -> String {
1165 translated(context, StockMessage::StatsMsgBody)
1166}
1167
1168pub(crate) fn new_group_send_first_message(context: &Context) -> String {
1170 translated(context, StockMessage::NewGroupSendFirstMessage)
1171}
1172
1173pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
1180 let name = if let Some(name) = context.get_config(Config::Displayname).await? {
1181 name
1182 } else {
1183 context.get_primary_self_addr().await?
1184 };
1185 Ok(translated(context, StockMessage::BackupTransferQr).replace1(&name))
1186}
1187
1188pub(crate) fn backup_transfer_msg_body(context: &Context) -> String {
1189 translated(context, StockMessage::BackupTransferMsgBody)
1190}
1191
1192pub(crate) fn proxy_enabled(context: &Context) -> String {
1194 translated(context, StockMessage::ProxyEnabled)
1195}
1196
1197pub(crate) fn proxy_description(context: &Context) -> String {
1199 translated(context, StockMessage::ProxyEnabledDescription)
1200}
1201
1202pub(crate) fn chat_unencrypted_explanation(context: &Context) -> String {
1204 translated(context, StockMessage::ChatUnencryptedExplanation)
1205}
1206
1207impl Viewtype {
1208 pub fn to_locale_string(&self, context: &Context) -> String {
1210 match self {
1211 Viewtype::Image => image(context),
1212 Viewtype::Gif => gif(context),
1213 Viewtype::Sticker => sticker(context),
1214 Viewtype::Audio => audio(context),
1215 Viewtype::Voice => voice_message(context),
1216 Viewtype::Video => video(context),
1217 Viewtype::File => file(context),
1218 Viewtype::Webxdc => "Mini App".to_owned(),
1219 Viewtype::Vcard => "👤".to_string(),
1220 Viewtype::Unknown | Viewtype::Text | Viewtype::Call => self.to_string(),
1222 }
1223 }
1224}
1225
1226impl Context {
1227 pub fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1230 self.translated_stockstrings
1231 .set_stock_translation(id, stockstring)?;
1232 Ok(())
1233 }
1234
1235 pub(crate) async fn update_device_chats(&self) -> Result<()> {
1236 if self.get_config_bool(Config::Bot).await? {
1237 return Ok(());
1238 }
1239
1240 if !self.sql.get_raw_config_bool("self-chat-added").await? {
1243 self.sql
1244 .set_raw_config_bool("self-chat-added", true)
1245 .await?;
1246 ChatId::create_for_contact(self, ContactId::SELF).await?;
1247 }
1248
1249 let image = include_bytes!("../assets/welcome-image.jpg");
1252 let blob = BlobObject::create_and_deduplicate_from_bytes(self, image, "welcome.jpg")?;
1253 let mut msg = Message::new(Viewtype::Image);
1254 msg.param.set(Param::File, blob.as_name());
1255 msg.param.set(Param::Filename, "welcome-image.jpg");
1256 chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
1257
1258 let mut msg = Message::new_text(welcome_message(self));
1259 chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
1260 Ok(())
1261 }
1262}
1263
1264impl Accounts {
1265 pub fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1268 self.stockstrings.set_stock_translation(id, stockstring)?;
1269 Ok(())
1270 }
1271}
1272
1273#[cfg(test)]
1274mod stock_str_tests;