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 = "End-to-end encryption available"))]
67 E2eAvailable = 25,
68
69 #[strum(props(fallback = "No encryption"))]
70 EncrNone = 28,
71
72 #[strum(props(fallback = "Fingerprints"))]
73 FingerPrints = 30,
74
75 #[strum(props(fallback = "%1$s verified."))]
76 ContactVerified = 35,
77
78 #[strum(props(fallback = "Archived chats"))]
79 ArchivedChats = 40,
80
81 #[strum(props(
82 fallback = "Cannot login as \"%1$s\". Please check if the email address and the password are correct."
83 ))]
84 CannotLogin = 60,
85
86 #[strum(props(fallback = "Location streaming enabled."))]
87 MsgLocationEnabled = 64,
88
89 #[strum(props(fallback = "Location streaming disabled."))]
90 MsgLocationDisabled = 65,
91
92 #[strum(props(fallback = "Location"))]
93 Location = 66,
94
95 #[strum(props(fallback = "Sticker"))]
96 Sticker = 67,
97
98 #[strum(props(fallback = "Device messages"))]
99 DeviceMessages = 68,
100
101 #[strum(props(fallback = "Saved messages"))]
102 SavedMessages = 69,
103
104 #[strum(props(
105 fallback = "Messages in this chat are generated locally by your Delta Chat app. \
106 Its makers use it to inform about app updates and problems during usage."
107 ))]
108 DeviceMessagesHint = 70,
109
110 #[strum(props(fallback = "Get in contact!\n\n\
111 🙌 Tap \"QR code\" on the main screen of both devices. \
112 Choose \"Scan QR Code\" on one device, and point it at the other\n\n\
113 🌍 If not in the same room, \
114 scan via video call or share an invite link from \"Scan QR code\"\n\n\
115 Then: Enjoy your decentralized messenger experience. \
116 In contrast to other popular apps, \
117 without central control or tracking or selling you, \
118 friends, colleagues or family out to large organizations."))]
119 WelcomeMessage = 71,
120
121 #[strum(props(fallback = "Message from %1$s"))]
122 SubjectForNewContact = 73,
123
124 #[strum(props(fallback = "Failed to send message to %1$s."))]
126 FailedSendingTo = 74,
127
128 #[strum(props(fallback = "Error:\n\n“%1$s”"))]
129 ConfigurationFailed = 84,
130
131 #[strum(props(
132 fallback = "⚠️ Date or time of your device seem to be inaccurate (%1$s).\n\n\
133 Adjust your clock ⏰🔧 to ensure your messages are received correctly."
134 ))]
135 BadTimeMsgBody = 85,
136
137 #[strum(props(fallback = "⚠️ Your Delta Chat version might be outdated.\n\n\
138 This may cause problems because your chat partners use newer versions - \
139 and you are missing the latest features 😳\n\
140 Please check https://get.delta.chat or your app store for updates."))]
141 UpdateReminderMsgBody = 86,
142
143 #[strum(props(
144 fallback = "Could not find your mail server.\n\nPlease check your internet connection."
145 ))]
146 ErrorNoNetwork = 87,
147
148 #[strum(props(fallback = "Reply"))]
150 ReplyNoun = 90,
151
152 #[strum(props(fallback = "You deleted the \"Saved messages\" chat.\n\n\
153 To use the \"Saved messages\" feature again, create a new chat with yourself."))]
154 SelfDeletedMsgBody = 91,
155
156 #[strum(props(fallback = "Forwarded"))]
157 Forwarded = 97,
158
159 #[strum(props(
160 fallback = "⚠️ Your provider's storage is about to exceed, already %1$s%% are used.\n\n\
161 You may not be able to receive message when the storage is 100%% used.\n\n\
162 👉 Please check if you can delete old data in the provider's webinterface \
163 and consider to enable \"Settings / Delete Old Messages\". \
164 You can check your current storage usage anytime at \"Settings / Connectivity\"."
165 ))]
166 QuotaExceedingMsgBody = 98,
167
168 #[strum(props(fallback = "Multi Device Synchronization"))]
169 SyncMsgSubject = 101,
170
171 #[strum(props(
172 fallback = "This message is used to synchronize data between your devices.\n\n\
173 👉 If you see this message in Delta Chat, please update your Delta Chat apps on all devices."
174 ))]
175 SyncMsgBody = 102,
176
177 #[strum(props(fallback = "Incoming Messages"))]
178 IncomingMessages = 103,
179
180 #[strum(props(fallback = "Outgoing Messages"))]
181 OutgoingMessages = 104,
182
183 #[strum(props(fallback = "Storage on %1$s"))]
184 StorageOnDomain = 105,
185
186 #[strum(props(fallback = "Connected"))]
187 Connected = 107,
188
189 #[strum(props(fallback = "Connecting…"))]
190 Connecting = 108,
191
192 #[strum(props(fallback = "Updating…"))]
193 Updating = 109,
194
195 #[strum(props(fallback = "Sending…"))]
196 Sending = 110,
197
198 #[strum(props(fallback = "Your last message was sent successfully."))]
199 LastMsgSentSuccessfully = 111,
200
201 #[strum(props(fallback = "Error: %1$s"))]
202 Error = 112,
203
204 #[strum(props(fallback = "Not supported by your provider."))]
205 NotSupportedByProvider = 113,
206
207 #[strum(props(fallback = "Messages"))]
208 Messages = 114,
209
210 #[strum(props(fallback = "%1$s of %2$s used"))]
211 PartOfTotallUsed = 116,
212
213 #[strum(props(fallback = "%1$s invited you to join this group.\n\n\
214 Waiting for the device of %2$s to reply…"))]
215 SecureJoinStarted = 117,
216
217 #[strum(props(fallback = "%1$s replied, waiting for being added to the group…"))]
218 SecureJoinReplies = 118,
219
220 #[strum(props(fallback = "Scan to chat with %1$s"))]
221 SetupContactQRDescription = 119,
222
223 #[strum(props(fallback = "Scan to join group %1$s"))]
224 SecureJoinGroupQRDescription = 120,
225
226 #[strum(props(fallback = "Not connected"))]
227 NotConnected = 121,
228
229 #[strum(props(fallback = "You changed group name from \"%1$s\" to \"%2$s\"."))]
230 MsgYouChangedGrpName = 124,
231
232 #[strum(props(fallback = "Group name changed from \"%1$s\" to \"%2$s\" by %3$s."))]
233 MsgGrpNameChangedBy = 125,
234
235 #[strum(props(fallback = "You changed the group image."))]
236 MsgYouChangedGrpImg = 126,
237
238 #[strum(props(fallback = "Group image changed by %1$s."))]
239 MsgGrpImgChangedBy = 127,
240
241 #[strum(props(fallback = "You added member %1$s."))]
242 MsgYouAddMember = 128,
243
244 #[strum(props(fallback = "Member %1$s added by %2$s."))]
245 MsgAddMemberBy = 129,
246
247 #[strum(props(fallback = "You removed member %1$s."))]
248 MsgYouDelMember = 130,
249
250 #[strum(props(fallback = "Member %1$s removed by %2$s."))]
251 MsgDelMemberBy = 131,
252
253 #[strum(props(fallback = "You left the group."))]
254 MsgYouLeftGroup = 132,
255
256 #[strum(props(fallback = "Group left by %1$s."))]
257 MsgGroupLeftBy = 133,
258
259 #[strum(props(fallback = "You deleted the group image."))]
260 MsgYouDeletedGrpImg = 134,
261
262 #[strum(props(fallback = "Group image deleted by %1$s."))]
263 MsgGrpImgDeletedBy = 135,
264
265 #[strum(props(fallback = "You enabled location streaming."))]
266 MsgYouEnabledLocation = 136,
267
268 #[strum(props(fallback = "Location streaming enabled by %1$s."))]
269 MsgLocationEnabledBy = 137,
270
271 #[strum(props(fallback = "You disabled message deletion timer."))]
272 MsgYouDisabledEphemeralTimer = 138,
273
274 #[strum(props(fallback = "Message deletion timer is disabled by %1$s."))]
275 MsgEphemeralTimerDisabledBy = 139,
276
277 #[strum(props(fallback = "You set message deletion timer to %1$s s."))]
280 MsgYouEnabledEphemeralTimer = 140,
281
282 #[strum(props(fallback = "Message deletion timer is set to %1$s s by %2$s."))]
283 MsgEphemeralTimerEnabledBy = 141,
284
285 #[strum(props(fallback = "You set message deletion timer to 1 hour."))]
286 MsgYouEphemeralTimerHour = 144,
287
288 #[strum(props(fallback = "Message deletion timer is set to 1 hour by %1$s."))]
289 MsgEphemeralTimerHourBy = 145,
290
291 #[strum(props(fallback = "You set message deletion timer to 1 day."))]
292 MsgYouEphemeralTimerDay = 146,
293
294 #[strum(props(fallback = "Message deletion timer is set to 1 day by %1$s."))]
295 MsgEphemeralTimerDayBy = 147,
296
297 #[strum(props(fallback = "You set message deletion timer to 1 week."))]
298 MsgYouEphemeralTimerWeek = 148,
299
300 #[strum(props(fallback = "Message deletion timer is set to 1 week by %1$s."))]
301 MsgEphemeralTimerWeekBy = 149,
302
303 #[strum(props(fallback = "You set message deletion timer to %1$s minutes."))]
304 MsgYouEphemeralTimerMinutes = 150,
305
306 #[strum(props(fallback = "Message deletion timer is set to %1$s minutes by %2$s."))]
307 MsgEphemeralTimerMinutesBy = 151,
308
309 #[strum(props(fallback = "You set message deletion timer to %1$s hours."))]
310 MsgYouEphemeralTimerHours = 152,
311
312 #[strum(props(fallback = "Message deletion timer is set to %1$s hours by %2$s."))]
313 MsgEphemeralTimerHoursBy = 153,
314
315 #[strum(props(fallback = "You set message deletion timer to %1$s days."))]
316 MsgYouEphemeralTimerDays = 154,
317
318 #[strum(props(fallback = "Message deletion timer is set to %1$s days by %2$s."))]
319 MsgEphemeralTimerDaysBy = 155,
320
321 #[strum(props(fallback = "You set message deletion timer to %1$s weeks."))]
322 MsgYouEphemeralTimerWeeks = 156,
323
324 #[strum(props(fallback = "Message deletion timer is set to %1$s weeks by %2$s."))]
325 MsgEphemeralTimerWeeksBy = 157,
326
327 #[strum(props(fallback = "You set message deletion timer to 1 year."))]
328 MsgYouEphemeralTimerYear = 158,
329
330 #[strum(props(fallback = "Message deletion timer is set to 1 year by %1$s."))]
331 MsgEphemeralTimerYearBy = 159,
332
333 #[strum(props(fallback = "Scan to set up second device for %1$s"))]
334 BackupTransferQr = 162,
335
336 #[strum(props(fallback = "ℹ️ Account transferred to your second device."))]
337 BackupTransferMsgBody = 163,
338
339 #[strum(props(fallback = "Messages are end-to-end encrypted."))]
340 ChatProtectionEnabled = 170,
341
342 #[strum(props(fallback = "Others will only see this group after you sent a first message."))]
343 NewGroupSendFirstMessage = 172,
344
345 #[strum(props(fallback = "Member %1$s added."))]
346 MsgAddMember = 173,
347
348 #[strum(props(
349 fallback = "⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet."
350 ))]
351 InvalidUnencryptedMail = 174,
352
353 #[strum(props(fallback = "You reacted %1$s to \"%2$s\""))]
354 MsgYouReacted = 176,
355
356 #[strum(props(fallback = "%1$s reacted %2$s to \"%3$s\""))]
357 MsgReactedBy = 177,
358
359 #[strum(props(fallback = "Member %1$s removed."))]
360 MsgDelMember = 178,
361
362 #[strum(props(fallback = "Establishing connection, please wait…"))]
363 SecurejoinWait = 190,
364
365 #[strum(props(fallback = "❤️ Seems you're enjoying Delta Chat!
366
367Please consider donating to help that Delta Chat stays free for everyone.
368
369While Delta Chat is free to use and open source, development costs money.
370Help keeping us to keep Delta Chat independent and make it more awesome in the future.
371
372https://delta.chat/donate"))]
373 DonationRequest = 193,
374
375 #[strum(props(fallback = "Outgoing call"))]
376 OutgoingCall = 194,
377
378 #[strum(props(fallback = "Incoming call"))]
379 IncomingCall = 195,
380
381 #[strum(props(fallback = "Declined call"))]
382 DeclinedCall = 196,
383
384 #[strum(props(fallback = "Canceled call"))]
385 CanceledCall = 197,
386
387 #[strum(props(fallback = "Missed call"))]
388 MissedCall = 198,
389
390 #[strum(props(fallback = "You left the channel."))]
391 MsgYouLeftBroadcast = 200,
392
393 #[strum(props(fallback = "Scan to join channel %1$s"))]
394 SecureJoinBrodcastQRDescription = 201,
395
396 #[strum(props(fallback = "You joined the channel."))]
397 MsgYouJoinedBroadcast = 202,
398
399 #[strum(props(fallback = "%1$s invited you to join this channel.\n\n\
400 Waiting for the device of %2$s to reply…"))]
401 SecureJoinBroadcastStarted = 203,
402
403 #[strum(props(
404 fallback = "The attachment contains anonymous usage statistics, which helps us improve Delta Chat. Thank you!"
405 ))]
406 StatsMsgBody = 210,
407
408 #[strum(props(fallback = "Proxy Enabled"))]
409 ProxyEnabled = 220,
410
411 #[strum(props(
412 fallback = "You are using a proxy. If you're having trouble connecting, try a different proxy."
413 ))]
414 ProxyEnabledDescription = 221,
415
416 #[strum(props(fallback = "Messages in this chat use classic email and are not encrypted."))]
417 ChatUnencryptedExplanation = 230,
418}
419
420impl StockMessage {
421 fn fallback(self) -> &'static str {
425 self.get_str("fallback").unwrap_or_default()
426 }
427}
428
429impl Default for StockStrings {
430 fn default() -> Self {
431 StockStrings::new()
432 }
433}
434
435impl StockStrings {
436 pub fn new() -> Self {
438 Self {
439 translated_stockstrings: Arc::new(RwLock::new(Default::default())),
440 }
441 }
442
443 async fn translated(&self, id: StockMessage) -> String {
444 self.translated_stockstrings
445 .read()
446 .await
447 .get(&(id as usize))
448 .map(AsRef::as_ref)
449 .unwrap_or_else(|| id.fallback())
450 .to_string()
451 }
452
453 async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
454 if stockstring.contains("%1") && !id.fallback().contains("%1") {
455 bail!(
456 "translation {} contains invalid %1 placeholder, default is {}",
457 stockstring,
458 id.fallback()
459 );
460 }
461 if stockstring.contains("%2") && !id.fallback().contains("%2") {
462 bail!(
463 "translation {} contains invalid %2 placeholder, default is {}",
464 stockstring,
465 id.fallback()
466 );
467 }
468 self.translated_stockstrings
469 .write()
470 .await
471 .insert(id as usize, stockstring);
472 Ok(())
473 }
474}
475
476async fn translated(context: &Context, id: StockMessage) -> String {
477 context.translated_stockstrings.translated(id).await
478}
479
480trait StockStringMods: AsRef<str> + Sized {
482 fn replace1(&self, replacement: &str) -> String {
484 self.as_ref()
485 .replacen("%1$s", replacement, 1)
486 .replacen("%1$d", replacement, 1)
487 .replacen("%1$@", replacement, 1)
488 }
489
490 fn replace2(&self, replacement: &str) -> String {
495 self.as_ref()
496 .replacen("%2$s", replacement, 1)
497 .replacen("%2$d", replacement, 1)
498 .replacen("%2$@", replacement, 1)
499 }
500
501 fn replace3(&self, replacement: &str) -> String {
506 self.as_ref()
507 .replacen("%3$s", replacement, 1)
508 .replacen("%3$d", replacement, 1)
509 .replacen("%3$@", replacement, 1)
510 }
511}
512
513impl ContactId {
514 async fn get_stock_name(self, context: &Context) -> String {
516 Contact::get_by_id(context, self)
517 .await
518 .map(|contact| contact.get_display_name().to_string())
519 .unwrap_or_else(|_| self.to_string())
520 }
521}
522
523impl StockStringMods for String {}
524
525pub(crate) async fn no_messages(context: &Context) -> String {
527 translated(context, StockMessage::NoMessages).await
528}
529
530pub(crate) async fn self_msg(context: &Context) -> String {
532 translated(context, StockMessage::SelfMsg).await
533}
534
535pub(crate) async fn draft(context: &Context) -> String {
537 translated(context, StockMessage::Draft).await
538}
539
540pub(crate) async fn voice_message(context: &Context) -> String {
542 translated(context, StockMessage::VoiceMessage).await
543}
544
545pub(crate) async fn image(context: &Context) -> String {
547 translated(context, StockMessage::Image).await
548}
549
550pub(crate) async fn video(context: &Context) -> String {
552 translated(context, StockMessage::Video).await
553}
554
555pub(crate) async fn audio(context: &Context) -> String {
557 translated(context, StockMessage::Audio).await
558}
559
560pub(crate) async fn file(context: &Context) -> String {
562 translated(context, StockMessage::File).await
563}
564
565pub(crate) async fn msg_grp_name(
567 context: &Context,
568 from_group: &str,
569 to_group: &str,
570 by_contact: ContactId,
571) -> String {
572 if by_contact == ContactId::SELF {
573 translated(context, StockMessage::MsgYouChangedGrpName)
574 .await
575 .replace1(from_group)
576 .replace2(to_group)
577 } else {
578 translated(context, StockMessage::MsgGrpNameChangedBy)
579 .await
580 .replace1(from_group)
581 .replace2(to_group)
582 .replace3(&by_contact.get_stock_name(context).await)
583 }
584}
585
586pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
587 if by_contact == ContactId::SELF {
588 translated(context, StockMessage::MsgYouChangedGrpImg).await
589 } else {
590 translated(context, StockMessage::MsgGrpImgChangedBy)
591 .await
592 .replace1(&by_contact.get_stock_name(context).await)
593 }
594}
595
596pub(crate) async fn msg_add_member_local(
601 context: &Context,
602 added_member: ContactId,
603 by_contact: ContactId,
604) -> String {
605 let whom = added_member.get_stock_name(context).await;
606 if by_contact == ContactId::UNDEFINED {
607 translated(context, StockMessage::MsgAddMember)
608 .await
609 .replace1(&whom)
610 } else if by_contact == ContactId::SELF {
611 translated(context, StockMessage::MsgYouAddMember)
612 .await
613 .replace1(&whom)
614 } else {
615 translated(context, StockMessage::MsgAddMemberBy)
616 .await
617 .replace1(&whom)
618 .replace2(&by_contact.get_stock_name(context).await)
619 }
620}
621
622pub(crate) async fn msg_del_member_local(
627 context: &Context,
628 removed_member: ContactId,
629 by_contact: ContactId,
630) -> String {
631 let whom = removed_member.get_stock_name(context).await;
632 if by_contact == ContactId::UNDEFINED {
633 translated(context, StockMessage::MsgDelMember)
634 .await
635 .replace1(&whom)
636 } else if by_contact == ContactId::SELF {
637 translated(context, StockMessage::MsgYouDelMember)
638 .await
639 .replace1(&whom)
640 } else {
641 translated(context, StockMessage::MsgDelMemberBy)
642 .await
643 .replace1(&whom)
644 .replace2(&by_contact.get_stock_name(context).await)
645 }
646}
647
648pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
650 if by_contact == ContactId::SELF {
651 translated(context, StockMessage::MsgYouLeftGroup).await
652 } else {
653 translated(context, StockMessage::MsgGroupLeftBy)
654 .await
655 .replace1(&by_contact.get_stock_name(context).await)
656 }
657}
658
659pub(crate) async fn msg_you_left_broadcast(context: &Context) -> String {
661 translated(context, StockMessage::MsgYouLeftBroadcast).await
662}
663
664pub(crate) async fn msg_you_joined_broadcast(context: &Context) -> String {
666 translated(context, StockMessage::MsgYouJoinedBroadcast).await
667}
668
669pub(crate) async fn secure_join_broadcast_started(
671 context: &Context,
672 inviter_contact_id: ContactId,
673) -> String {
674 if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
675 translated(context, StockMessage::SecureJoinBroadcastStarted)
676 .await
677 .replace1(contact.get_display_name())
678 .replace2(contact.get_display_name())
679 } else {
680 format!("secure_join_started: unknown contact {inviter_contact_id}")
681 }
682}
683
684pub(crate) async fn msg_reacted(
686 context: &Context,
687 by_contact: ContactId,
688 reaction: &str,
689 summary: &str,
690) -> String {
691 if by_contact == ContactId::SELF {
692 translated(context, StockMessage::MsgYouReacted)
693 .await
694 .replace1(reaction)
695 .replace2(summary)
696 } else {
697 translated(context, StockMessage::MsgReactedBy)
698 .await
699 .replace1(&by_contact.get_stock_name(context).await)
700 .replace2(reaction)
701 .replace3(summary)
702 }
703}
704
705pub(crate) async fn gif(context: &Context) -> String {
707 translated(context, StockMessage::Gif).await
708}
709
710pub(crate) async fn e2e_available(context: &Context) -> String {
712 translated(context, StockMessage::E2eAvailable).await
713}
714
715pub(crate) async fn encr_none(context: &Context) -> String {
717 translated(context, StockMessage::EncrNone).await
718}
719
720pub(crate) async fn finger_prints(context: &Context) -> String {
722 translated(context, StockMessage::FingerPrints).await
723}
724
725pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
727 if by_contact == ContactId::SELF {
728 translated(context, StockMessage::MsgYouDeletedGrpImg).await
729 } else {
730 translated(context, StockMessage::MsgGrpImgDeletedBy)
731 .await
732 .replace1(&by_contact.get_stock_name(context).await)
733 }
734}
735
736pub(crate) async fn secure_join_started(
738 context: &Context,
739 inviter_contact_id: ContactId,
740) -> String {
741 if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
742 translated(context, StockMessage::SecureJoinStarted)
743 .await
744 .replace1(contact.get_display_name())
745 .replace2(contact.get_display_name())
746 } else {
747 format!("secure_join_started: unknown contact {inviter_contact_id}")
748 }
749}
750
751pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
753 translated(context, StockMessage::SecureJoinReplies)
754 .await
755 .replace1(&contact_id.get_stock_name(context).await)
756}
757
758pub(crate) async fn securejoin_wait(context: &Context) -> String {
760 translated(context, StockMessage::SecurejoinWait).await
761}
762
763pub(crate) async fn donation_request(context: &Context) -> String {
765 translated(context, StockMessage::DonationRequest).await
766}
767
768pub(crate) async fn outgoing_call(context: &Context) -> String {
770 translated(context, StockMessage::OutgoingCall).await
771}
772
773pub(crate) async fn incoming_call(context: &Context) -> String {
775 translated(context, StockMessage::IncomingCall).await
776}
777
778pub(crate) async fn declined_call(context: &Context) -> String {
780 translated(context, StockMessage::DeclinedCall).await
781}
782
783pub(crate) async fn canceled_call(context: &Context) -> String {
785 translated(context, StockMessage::CanceledCall).await
786}
787
788pub(crate) async fn missed_call(context: &Context) -> String {
790 translated(context, StockMessage::MissedCall).await
791}
792
793pub(crate) async fn setup_contact_qr_description(
795 context: &Context,
796 display_name: &str,
797 addr: &str,
798) -> String {
799 let name = if display_name.is_empty() {
800 addr.to_owned()
801 } else {
802 display_name.to_owned()
803 };
804 translated(context, StockMessage::SetupContactQRDescription)
805 .await
806 .replace1(&name)
807}
808
809pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
811 translated(context, StockMessage::SecureJoinGroupQRDescription)
812 .await
813 .replace1(chat.get_name())
814}
815
816pub(crate) async fn secure_join_broadcast_qr_description(context: &Context, chat: &Chat) -> String {
818 translated(context, StockMessage::SecureJoinBrodcastQRDescription)
819 .await
820 .replace1(chat.get_name())
821}
822
823#[allow(dead_code)]
825pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
826 let addr = contact.get_display_name();
827 translated(context, StockMessage::ContactVerified)
828 .await
829 .replace1(addr)
830}
831
832pub(crate) async fn archived_chats(context: &Context) -> String {
834 translated(context, StockMessage::ArchivedChats).await
835}
836
837pub(crate) async fn sync_msg_subject(context: &Context) -> String {
839 translated(context, StockMessage::SyncMsgSubject).await
840}
841
842pub(crate) async fn sync_msg_body(context: &Context) -> String {
844 translated(context, StockMessage::SyncMsgBody).await
845}
846
847pub(crate) async fn cannot_login(context: &Context, user: &str) -> String {
849 translated(context, StockMessage::CannotLogin)
850 .await
851 .replace1(user)
852}
853
854pub(crate) async fn msg_location_enabled(context: &Context) -> String {
856 translated(context, StockMessage::MsgLocationEnabled).await
857}
858
859pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
861 if contact == ContactId::SELF {
862 translated(context, StockMessage::MsgYouEnabledLocation).await
863 } else {
864 translated(context, StockMessage::MsgLocationEnabledBy)
865 .await
866 .replace1(&contact.get_stock_name(context).await)
867 }
868}
869
870pub(crate) async fn msg_location_disabled(context: &Context) -> String {
872 translated(context, StockMessage::MsgLocationDisabled).await
873}
874
875pub(crate) async fn location(context: &Context) -> String {
877 translated(context, StockMessage::Location).await
878}
879
880pub(crate) async fn sticker(context: &Context) -> String {
882 translated(context, StockMessage::Sticker).await
883}
884
885pub(crate) async fn device_messages(context: &Context) -> String {
887 translated(context, StockMessage::DeviceMessages).await
888}
889
890pub(crate) async fn saved_messages(context: &Context) -> String {
892 translated(context, StockMessage::SavedMessages).await
893}
894
895pub(crate) async fn device_messages_hint(context: &Context) -> String {
897 translated(context, StockMessage::DeviceMessagesHint).await
898}
899
900pub(crate) async fn welcome_message(context: &Context) -> String {
902 translated(context, StockMessage::WelcomeMessage).await
903}
904
905pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
908 translated(context, StockMessage::SubjectForNewContact)
909 .await
910 .replace1(self_name)
911}
912
913pub(crate) async fn msg_ephemeral_timer_disabled(
915 context: &Context,
916 by_contact: ContactId,
917) -> String {
918 if by_contact == ContactId::SELF {
919 translated(context, StockMessage::MsgYouDisabledEphemeralTimer).await
920 } else {
921 translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
922 .await
923 .replace1(&by_contact.get_stock_name(context).await)
924 }
925}
926
927pub(crate) async fn msg_ephemeral_timer_enabled(
929 context: &Context,
930 timer: &str,
931 by_contact: ContactId,
932) -> String {
933 if by_contact == ContactId::SELF {
934 translated(context, StockMessage::MsgYouEnabledEphemeralTimer)
935 .await
936 .replace1(timer)
937 } else {
938 translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
939 .await
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).await
949 } else {
950 translated(context, StockMessage::MsgEphemeralTimerHourBy)
951 .await
952 .replace1(&by_contact.get_stock_name(context).await)
953 }
954}
955
956pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
958 if by_contact == ContactId::SELF {
959 translated(context, StockMessage::MsgYouEphemeralTimerDay).await
960 } else {
961 translated(context, StockMessage::MsgEphemeralTimerDayBy)
962 .await
963 .replace1(&by_contact.get_stock_name(context).await)
964 }
965}
966
967pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
969 if by_contact == ContactId::SELF {
970 translated(context, StockMessage::MsgYouEphemeralTimerWeek).await
971 } else {
972 translated(context, StockMessage::MsgEphemeralTimerWeekBy)
973 .await
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).await
982 } else {
983 translated(context, StockMessage::MsgEphemeralTimerYearBy)
984 .await
985 .replace1(&by_contact.get_stock_name(context).await)
986 }
987}
988
989pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
991 translated(context, StockMessage::ConfigurationFailed)
992 .await
993 .replace1(details)
994}
995
996pub(crate) async fn bad_time_msg_body(context: &Context, now: &str) -> String {
999 translated(context, StockMessage::BadTimeMsgBody)
1000 .await
1001 .replace1(now)
1002}
1003
1004pub(crate) async fn update_reminder_msg_body(context: &Context) -> String {
1006 translated(context, StockMessage::UpdateReminderMsgBody).await
1007}
1008
1009pub(crate) async fn error_no_network(context: &Context) -> String {
1011 translated(context, StockMessage::ErrorNoNetwork).await
1012}
1013
1014pub(crate) async fn messages_e2e_encrypted(context: &Context) -> String {
1016 translated(context, StockMessage::ChatProtectionEnabled).await
1017}
1018
1019pub(crate) async fn reply_noun(context: &Context) -> String {
1021 translated(context, StockMessage::ReplyNoun).await
1022}
1023
1024pub(crate) async fn self_deleted_msg_body(context: &Context) -> String {
1026 translated(context, StockMessage::SelfDeletedMsgBody).await
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)
1037 .await
1038 .replace1(minutes)
1039 } else {
1040 translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
1041 .await
1042 .replace1(minutes)
1043 .replace2(&by_contact.get_stock_name(context).await)
1044 }
1045}
1046
1047pub(crate) async fn msg_ephemeral_timer_hours(
1049 context: &Context,
1050 hours: &str,
1051 by_contact: ContactId,
1052) -> String {
1053 if by_contact == ContactId::SELF {
1054 translated(context, StockMessage::MsgYouEphemeralTimerHours)
1055 .await
1056 .replace1(hours)
1057 } else {
1058 translated(context, StockMessage::MsgEphemeralTimerHoursBy)
1059 .await
1060 .replace1(hours)
1061 .replace2(&by_contact.get_stock_name(context).await)
1062 }
1063}
1064
1065pub(crate) async fn msg_ephemeral_timer_days(
1067 context: &Context,
1068 days: &str,
1069 by_contact: ContactId,
1070) -> String {
1071 if by_contact == ContactId::SELF {
1072 translated(context, StockMessage::MsgYouEphemeralTimerDays)
1073 .await
1074 .replace1(days)
1075 } else {
1076 translated(context, StockMessage::MsgEphemeralTimerDaysBy)
1077 .await
1078 .replace1(days)
1079 .replace2(&by_contact.get_stock_name(context).await)
1080 }
1081}
1082
1083pub(crate) async fn msg_ephemeral_timer_weeks(
1085 context: &Context,
1086 weeks: &str,
1087 by_contact: ContactId,
1088) -> String {
1089 if by_contact == ContactId::SELF {
1090 translated(context, StockMessage::MsgYouEphemeralTimerWeeks)
1091 .await
1092 .replace1(weeks)
1093 } else {
1094 translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
1095 .await
1096 .replace1(weeks)
1097 .replace2(&by_contact.get_stock_name(context).await)
1098 }
1099}
1100
1101pub(crate) async fn forwarded(context: &Context) -> String {
1103 translated(context, StockMessage::Forwarded).await
1104}
1105
1106pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
1108 translated(context, StockMessage::QuotaExceedingMsgBody)
1109 .await
1110 .replace1(&format!("{highest_usage}"))
1111 .replace("%%", "%")
1112}
1113
1114pub(crate) async fn incoming_messages(context: &Context) -> String {
1116 translated(context, StockMessage::IncomingMessages).await
1117}
1118
1119pub(crate) async fn outgoing_messages(context: &Context) -> String {
1121 translated(context, StockMessage::OutgoingMessages).await
1122}
1123
1124pub(crate) async fn not_connected(context: &Context) -> String {
1126 translated(context, StockMessage::NotConnected).await
1127}
1128
1129pub(crate) async fn connected(context: &Context) -> String {
1131 translated(context, StockMessage::Connected).await
1132}
1133
1134pub(crate) async fn connecting(context: &Context) -> String {
1136 translated(context, StockMessage::Connecting).await
1137}
1138
1139pub(crate) async fn updating(context: &Context) -> String {
1141 translated(context, StockMessage::Updating).await
1142}
1143
1144pub(crate) async fn sending(context: &Context) -> String {
1146 translated(context, StockMessage::Sending).await
1147}
1148
1149pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
1151 translated(context, StockMessage::LastMsgSentSuccessfully).await
1152}
1153
1154pub(crate) async fn error(context: &Context, error: &str) -> String {
1157 translated(context, StockMessage::Error)
1158 .await
1159 .replace1(error)
1160}
1161
1162pub(crate) async fn not_supported_by_provider(context: &Context) -> String {
1164 translated(context, StockMessage::NotSupportedByProvider).await
1165}
1166
1167pub(crate) async fn messages(context: &Context) -> String {
1170 translated(context, StockMessage::Messages).await
1171}
1172
1173pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
1175 translated(context, StockMessage::PartOfTotallUsed)
1176 .await
1177 .replace1(part)
1178 .replace2(total)
1179}
1180
1181pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
1183 translated(context, StockMessage::InvalidUnencryptedMail)
1184 .await
1185 .replace1(provider)
1186}
1187
1188pub(crate) async fn stats_msg_body(context: &Context) -> String {
1190 translated(context, StockMessage::StatsMsgBody).await
1191}
1192
1193pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
1195 translated(context, StockMessage::NewGroupSendFirstMessage).await
1196}
1197
1198pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
1205 let name = if let Some(name) = context.get_config(Config::Displayname).await? {
1206 name
1207 } else {
1208 context.get_primary_self_addr().await?
1209 };
1210 Ok(translated(context, StockMessage::BackupTransferQr)
1211 .await
1212 .replace1(&name))
1213}
1214
1215pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String {
1216 translated(context, StockMessage::BackupTransferMsgBody).await
1217}
1218
1219pub(crate) async fn proxy_enabled(context: &Context) -> String {
1221 translated(context, StockMessage::ProxyEnabled).await
1222}
1223
1224pub(crate) async fn proxy_description(context: &Context) -> String {
1226 translated(context, StockMessage::ProxyEnabledDescription).await
1227}
1228
1229pub(crate) async fn chat_unencrypted_explanation(context: &Context) -> String {
1231 translated(context, StockMessage::ChatUnencryptedExplanation).await
1232}
1233
1234impl Viewtype {
1235 pub async fn to_locale_string(&self, context: &Context) -> String {
1237 match self {
1238 Viewtype::Image => image(context).await,
1239 Viewtype::Gif => gif(context).await,
1240 Viewtype::Sticker => sticker(context).await,
1241 Viewtype::Audio => audio(context).await,
1242 Viewtype::Voice => voice_message(context).await,
1243 Viewtype::Video => video(context).await,
1244 Viewtype::File => file(context).await,
1245 Viewtype::Webxdc => "Mini App".to_owned(),
1246 Viewtype::Vcard => "👤".to_string(),
1247 Viewtype::Unknown | Viewtype::Text | Viewtype::Call => self.to_string(),
1249 }
1250 }
1251}
1252
1253impl Context {
1254 pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1257 self.translated_stockstrings
1258 .set_stock_translation(id, stockstring)
1259 .await?;
1260 Ok(())
1261 }
1262
1263 pub(crate) async fn update_device_chats(&self) -> Result<()> {
1264 if self.get_config_bool(Config::Bot).await? {
1265 return Ok(());
1266 }
1267
1268 if !self.sql.get_raw_config_bool("self-chat-added").await? {
1271 self.sql
1272 .set_raw_config_bool("self-chat-added", true)
1273 .await?;
1274 ChatId::create_for_contact(self, ContactId::SELF).await?;
1275 }
1276
1277 let image = include_bytes!("../assets/welcome-image.jpg");
1280 let blob = BlobObject::create_and_deduplicate_from_bytes(self, image, "welcome.jpg")?;
1281 let mut msg = Message::new(Viewtype::Image);
1282 msg.param.set(Param::File, blob.as_name());
1283 msg.param.set(Param::Filename, "welcome-image.jpg");
1284 chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
1285
1286 let mut msg = Message::new_text(welcome_message(self).await);
1287 chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
1288 Ok(())
1289 }
1290}
1291
1292impl Accounts {
1293 pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1296 self.stockstrings
1297 .set_stock_translation(id, stockstring)
1298 .await?;
1299 Ok(())
1300 }
1301}
1302
1303#[cfg(test)]
1304mod stock_str_tests;