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