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