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(
417 fallback = "You are using the legacy option \"Settings → Advanced → Move automatically to DeltaChat Folder\".\n\nThis option will be removed in a few weeks and you should disable it already today.\n\nIf having chat messages mixed into your inbox is a problem, see https://delta.chat/legacy-move"
418 ))]
419 MvboxMoveDeprecation = 231,
420
421 #[strum(props(fallback = "Outgoing audio call"))]
422 OutgoingAudioCall = 232,
423
424 #[strum(props(fallback = "Outgoing video call"))]
425 OutgoingVideoCall = 233,
426
427 #[strum(props(fallback = "Incoming audio call"))]
428 IncomingAudioCall = 234,
429
430 #[strum(props(fallback = "Incoming video call"))]
431 IncomingVideoCall = 235,
432
433 #[strum(props(fallback = "You changed the chat description."))]
434 MsgYouChangedDescription = 240,
435
436 #[strum(props(fallback = "Chat description changed by %1$s."))]
437 MsgChatDescriptionChangedBy = 241,
438
439 #[strum(props(fallback = "Messages are end-to-end encrypted."))]
440 MessagesAreE2ee = 242,
441}
442
443impl StockMessage {
444 fn fallback(self) -> &'static str {
448 self.get_str("fallback").unwrap_or_default()
449 }
450}
451
452impl Default for StockStrings {
453 fn default() -> Self {
454 StockStrings::new()
455 }
456}
457
458impl StockStrings {
459 pub fn new() -> Self {
461 Self {
462 translated_stockstrings: Arc::new(RwLock::new(Default::default())),
463 }
464 }
465
466 fn translated(&self, id: StockMessage) -> String {
467 self.translated_stockstrings
468 .read()
469 .get(&(id as usize))
470 .map(AsRef::as_ref)
471 .unwrap_or_else(|| id.fallback())
472 .to_string()
473 }
474
475 fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
476 if stockstring.contains("%1") && !id.fallback().contains("%1") {
477 bail!(
478 "translation {} contains invalid %1 placeholder, default is {}",
479 stockstring,
480 id.fallback()
481 );
482 }
483 if stockstring.contains("%2") && !id.fallback().contains("%2") {
484 bail!(
485 "translation {} contains invalid %2 placeholder, default is {}",
486 stockstring,
487 id.fallback()
488 );
489 }
490 self.translated_stockstrings
491 .write()
492 .insert(id as usize, stockstring);
493 Ok(())
494 }
495}
496
497fn translated(context: &Context, id: StockMessage) -> String {
498 context.translated_stockstrings.translated(id)
499}
500
501trait StockStringMods: AsRef<str> + Sized {
503 fn replace1(&self, replacement: &str) -> String {
505 self.as_ref()
506 .replacen("%1$s", replacement, 1)
507 .replacen("%1$d", replacement, 1)
508 .replacen("%1$@", replacement, 1)
509 }
510
511 fn replace2(&self, replacement: &str) -> String {
516 self.as_ref()
517 .replacen("%2$s", replacement, 1)
518 .replacen("%2$d", replacement, 1)
519 .replacen("%2$@", replacement, 1)
520 }
521
522 fn replace3(&self, replacement: &str) -> String {
527 self.as_ref()
528 .replacen("%3$s", replacement, 1)
529 .replacen("%3$d", replacement, 1)
530 .replacen("%3$@", replacement, 1)
531 }
532}
533
534impl ContactId {
535 async fn get_stock_name(self, context: &Context) -> String {
537 Contact::get_by_id(context, self)
538 .await
539 .map(|contact| contact.get_display_name().to_string())
540 .unwrap_or_else(|_| self.to_string())
541 }
542}
543
544impl StockStringMods for String {}
545
546pub(crate) fn no_messages(context: &Context) -> String {
548 translated(context, StockMessage::NoMessages)
549}
550
551pub(crate) fn self_msg(context: &Context) -> String {
553 translated(context, StockMessage::SelfMsg)
554}
555
556pub(crate) fn draft(context: &Context) -> String {
558 translated(context, StockMessage::Draft)
559}
560
561pub(crate) fn voice_message(context: &Context) -> String {
563 translated(context, StockMessage::VoiceMessage)
564}
565
566pub(crate) fn image(context: &Context) -> String {
568 translated(context, StockMessage::Image)
569}
570
571pub(crate) fn video(context: &Context) -> String {
573 translated(context, StockMessage::Video)
574}
575
576pub(crate) fn audio(context: &Context) -> String {
578 translated(context, StockMessage::Audio)
579}
580
581pub(crate) fn file(context: &Context) -> String {
583 translated(context, StockMessage::File)
584}
585
586pub(crate) async fn msg_grp_name(
588 context: &Context,
589 from_group: &str,
590 to_group: &str,
591 by_contact: ContactId,
592) -> String {
593 if by_contact == ContactId::SELF {
594 translated(context, StockMessage::MsgYouChangedGrpName)
595 .replace1(from_group)
596 .replace2(to_group)
597 } else {
598 translated(context, StockMessage::MsgGrpNameChangedBy)
599 .replace1(from_group)
600 .replace2(to_group)
601 .replace3(&by_contact.get_stock_name(context).await)
602 }
603}
604
605pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
606 if by_contact == ContactId::SELF {
607 translated(context, StockMessage::MsgYouChangedGrpImg)
608 } else {
609 translated(context, StockMessage::MsgGrpImgChangedBy)
610 .replace1(&by_contact.get_stock_name(context).await)
611 }
612}
613
614pub(crate) async fn msg_chat_description_changed(
615 context: &Context,
616 by_contact: ContactId,
617) -> String {
618 if by_contact == ContactId::SELF {
619 translated(context, StockMessage::MsgYouChangedDescription)
620 } else {
621 translated(context, StockMessage::MsgChatDescriptionChangedBy)
622 .replace1(&by_contact.get_stock_name(context).await)
623 }
624}
625
626pub(crate) async fn msg_add_member_local(
631 context: &Context,
632 added_member: ContactId,
633 by_contact: ContactId,
634) -> String {
635 let whom = added_member.get_stock_name(context).await;
636 if by_contact == ContactId::UNDEFINED {
637 translated(context, StockMessage::MsgAddMember).replace1(&whom)
638 } else if by_contact == ContactId::SELF {
639 translated(context, StockMessage::MsgYouAddMember).replace1(&whom)
640 } else {
641 translated(context, StockMessage::MsgAddMemberBy)
642 .replace1(&whom)
643 .replace2(&by_contact.get_stock_name(context).await)
644 }
645}
646
647pub(crate) async fn msg_del_member_local(
652 context: &Context,
653 removed_member: ContactId,
654 by_contact: ContactId,
655) -> String {
656 let whom = removed_member.get_stock_name(context).await;
657 if by_contact == ContactId::UNDEFINED {
658 translated(context, StockMessage::MsgDelMember).replace1(&whom)
659 } else if by_contact == ContactId::SELF {
660 translated(context, StockMessage::MsgYouDelMember).replace1(&whom)
661 } else {
662 translated(context, StockMessage::MsgDelMemberBy)
663 .replace1(&whom)
664 .replace2(&by_contact.get_stock_name(context).await)
665 }
666}
667
668pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
670 if by_contact == ContactId::SELF {
671 translated(context, StockMessage::MsgYouLeftGroup)
672 } else {
673 translated(context, StockMessage::MsgGroupLeftBy)
674 .replace1(&by_contact.get_stock_name(context).await)
675 }
676}
677
678pub(crate) fn msg_you_left_broadcast(context: &Context) -> String {
680 translated(context, StockMessage::MsgYouLeftBroadcast)
681}
682
683pub(crate) fn msg_you_joined_broadcast(context: &Context) -> String {
685 translated(context, StockMessage::MsgYouJoinedBroadcast)
686}
687
688pub(crate) async fn secure_join_broadcast_started(
690 context: &Context,
691 inviter_contact_id: ContactId,
692) -> String {
693 if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
694 translated(context, StockMessage::SecureJoinBroadcastStarted)
695 .replace1(contact.get_display_name())
696 .replace2(contact.get_display_name())
697 } else {
698 format!("secure_join_started: unknown contact {inviter_contact_id}")
699 }
700}
701
702pub(crate) fn msg_broadcast_name_changed(context: &Context, from: &str, to: &str) -> String {
704 translated(context, StockMessage::MsgBroadcastNameChanged)
705 .replace1(from)
706 .replace2(to)
707}
708
709pub(crate) fn msg_broadcast_img_changed(context: &Context) -> String {
711 translated(context, StockMessage::MsgBroadcastImgChanged)
712}
713
714pub(crate) async fn msg_reacted(
716 context: &Context,
717 by_contact: ContactId,
718 reaction: &str,
719 summary: &str,
720) -> String {
721 if by_contact == ContactId::SELF {
722 translated(context, StockMessage::MsgYouReacted)
723 .replace1(reaction)
724 .replace2(summary)
725 } else {
726 translated(context, StockMessage::MsgReactedBy)
727 .replace1(&by_contact.get_stock_name(context).await)
728 .replace2(reaction)
729 .replace3(summary)
730 }
731}
732
733pub(crate) fn gif(context: &Context) -> String {
735 translated(context, StockMessage::Gif)
736}
737
738pub(crate) fn encr_none(context: &Context) -> String {
740 translated(context, StockMessage::EncrNone)
741}
742
743pub(crate) fn finger_prints(context: &Context) -> String {
745 translated(context, StockMessage::FingerPrints)
746}
747
748pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
750 if by_contact == ContactId::SELF {
751 translated(context, StockMessage::MsgYouDeletedGrpImg)
752 } else {
753 translated(context, StockMessage::MsgGrpImgDeletedBy)
754 .replace1(&by_contact.get_stock_name(context).await)
755 }
756}
757
758pub(crate) async fn secure_join_started(
760 context: &Context,
761 inviter_contact_id: ContactId,
762) -> String {
763 if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
764 translated(context, StockMessage::SecureJoinStarted)
765 .replace1(contact.get_display_name())
766 .replace2(contact.get_display_name())
767 } else {
768 format!("secure_join_started: unknown contact {inviter_contact_id}")
769 }
770}
771
772pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
774 translated(context, StockMessage::SecureJoinReplies)
775 .replace1(&contact_id.get_stock_name(context).await)
776}
777
778pub(crate) fn securejoin_wait(context: &Context) -> String {
780 translated(context, StockMessage::SecurejoinWait)
781}
782
783pub(crate) fn donation_request(context: &Context) -> String {
785 translated(context, StockMessage::DonationRequest)
786}
787
788pub(crate) fn outgoing_call(context: &Context, has_video: bool) -> String {
790 translated(
791 context,
792 if has_video {
793 StockMessage::OutgoingVideoCall
794 } else {
795 StockMessage::OutgoingAudioCall
796 },
797 )
798}
799
800pub(crate) fn incoming_call(context: &Context, has_video: bool) -> String {
802 translated(
803 context,
804 if has_video {
805 StockMessage::IncomingVideoCall
806 } else {
807 StockMessage::IncomingAudioCall
808 },
809 )
810}
811
812pub(crate) fn declined_call(context: &Context) -> String {
814 translated(context, StockMessage::DeclinedCall)
815}
816
817pub(crate) fn canceled_call(context: &Context) -> String {
819 translated(context, StockMessage::CanceledCall)
820}
821
822pub(crate) fn missed_call(context: &Context) -> String {
824 translated(context, StockMessage::MissedCall)
825}
826
827pub(crate) fn setup_contact_qr_description(
829 context: &Context,
830 display_name: &str,
831 addr: &str,
832) -> String {
833 let name = if display_name.is_empty() {
834 addr.to_owned()
835 } else {
836 display_name.to_owned()
837 };
838 translated(context, StockMessage::SetupContactQRDescription).replace1(&name)
839}
840
841pub(crate) fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
843 translated(context, StockMessage::SecureJoinGroupQRDescription).replace1(chat.get_name())
844}
845
846pub(crate) fn secure_join_broadcast_qr_description(context: &Context, chat: &Chat) -> String {
848 translated(context, StockMessage::SecureJoinBrodcastQRDescription).replace1(chat.get_name())
849}
850
851#[allow(dead_code)]
853pub(crate) fn contact_verified(context: &Context, contact: &Contact) -> String {
854 let addr = contact.get_display_name();
855 translated(context, StockMessage::ContactVerified).replace1(addr)
856}
857
858pub(crate) fn archived_chats(context: &Context) -> String {
860 translated(context, StockMessage::ArchivedChats)
861}
862
863pub(crate) fn sync_msg_subject(context: &Context) -> String {
865 translated(context, StockMessage::SyncMsgSubject)
866}
867
868pub(crate) fn sync_msg_body(context: &Context) -> String {
870 translated(context, StockMessage::SyncMsgBody)
871}
872
873pub(crate) fn cannot_login(context: &Context, user: &str) -> String {
875 translated(context, StockMessage::CannotLogin).replace1(user)
876}
877
878pub(crate) fn msg_location_enabled(context: &Context) -> String {
880 translated(context, StockMessage::MsgLocationEnabled)
881}
882
883pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
885 if contact == ContactId::SELF {
886 translated(context, StockMessage::MsgYouEnabledLocation)
887 } else {
888 translated(context, StockMessage::MsgLocationEnabledBy)
889 .replace1(&contact.get_stock_name(context).await)
890 }
891}
892
893pub(crate) fn msg_location_disabled(context: &Context) -> String {
895 translated(context, StockMessage::MsgLocationDisabled)
896}
897
898pub(crate) fn location(context: &Context) -> String {
900 translated(context, StockMessage::Location)
901}
902
903pub(crate) fn sticker(context: &Context) -> String {
905 translated(context, StockMessage::Sticker)
906}
907
908pub(crate) fn device_messages(context: &Context) -> String {
910 translated(context, StockMessage::DeviceMessages)
911}
912
913pub(crate) fn saved_messages(context: &Context) -> String {
915 translated(context, StockMessage::SavedMessages)
916}
917
918pub(crate) fn device_messages_hint(context: &Context) -> String {
920 translated(context, StockMessage::DeviceMessagesHint)
921}
922
923pub(crate) fn welcome_message(context: &Context) -> String {
925 translated(context, StockMessage::WelcomeMessage)
926}
927
928pub(crate) fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
931 translated(context, StockMessage::SubjectForNewContact).replace1(self_name)
932}
933
934pub(crate) async fn msg_ephemeral_timer_disabled(
936 context: &Context,
937 by_contact: ContactId,
938) -> String {
939 if by_contact == ContactId::SELF {
940 translated(context, StockMessage::MsgYouDisabledEphemeralTimer)
941 } else {
942 translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
943 .replace1(&by_contact.get_stock_name(context).await)
944 }
945}
946
947pub(crate) async fn msg_ephemeral_timer_enabled(
949 context: &Context,
950 timer: &str,
951 by_contact: ContactId,
952) -> String {
953 if by_contact == ContactId::SELF {
954 translated(context, StockMessage::MsgYouEnabledEphemeralTimer).replace1(timer)
955 } else {
956 translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
957 .replace1(timer)
958 .replace2(&by_contact.get_stock_name(context).await)
959 }
960}
961
962pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
964 if by_contact == ContactId::SELF {
965 translated(context, StockMessage::MsgYouEphemeralTimerHour)
966 } else {
967 translated(context, StockMessage::MsgEphemeralTimerHourBy)
968 .replace1(&by_contact.get_stock_name(context).await)
969 }
970}
971
972pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
974 if by_contact == ContactId::SELF {
975 translated(context, StockMessage::MsgYouEphemeralTimerDay)
976 } else {
977 translated(context, StockMessage::MsgEphemeralTimerDayBy)
978 .replace1(&by_contact.get_stock_name(context).await)
979 }
980}
981
982pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
984 if by_contact == ContactId::SELF {
985 translated(context, StockMessage::MsgYouEphemeralTimerWeek)
986 } else {
987 translated(context, StockMessage::MsgEphemeralTimerWeekBy)
988 .replace1(&by_contact.get_stock_name(context).await)
989 }
990}
991
992pub(crate) async fn msg_ephemeral_timer_year(context: &Context, by_contact: ContactId) -> String {
994 if by_contact == ContactId::SELF {
995 translated(context, StockMessage::MsgYouEphemeralTimerYear)
996 } else {
997 translated(context, StockMessage::MsgEphemeralTimerYearBy)
998 .replace1(&by_contact.get_stock_name(context).await)
999 }
1000}
1001
1002pub(crate) fn configuration_failed(context: &Context, details: &str) -> String {
1004 translated(context, StockMessage::ConfigurationFailed).replace1(details)
1005}
1006
1007pub(crate) fn bad_time_msg_body(context: &Context, now: &str) -> String {
1010 translated(context, StockMessage::BadTimeMsgBody).replace1(now)
1011}
1012
1013pub(crate) fn update_reminder_msg_body(context: &Context) -> String {
1015 translated(context, StockMessage::UpdateReminderMsgBody)
1016}
1017
1018pub(crate) fn error_no_network(context: &Context) -> String {
1020 translated(context, StockMessage::ErrorNoNetwork)
1021}
1022
1023pub(crate) fn messages_e2ee_info_msg(context: &Context) -> String {
1025 translated(context, StockMessage::ChatProtectionEnabled)
1026}
1027
1028pub(crate) fn messages_are_e2ee(context: &Context) -> String {
1030 translated(context, StockMessage::MessagesAreE2ee)
1031}
1032
1033pub(crate) fn reply_noun(context: &Context) -> String {
1035 translated(context, StockMessage::ReplyNoun)
1036}
1037
1038pub(crate) fn self_deleted_msg_body(context: &Context) -> String {
1040 translated(context, StockMessage::SelfDeletedMsgBody)
1041}
1042
1043pub(crate) async fn msg_ephemeral_timer_minutes(
1045 context: &Context,
1046 minutes: &str,
1047 by_contact: ContactId,
1048) -> String {
1049 if by_contact == ContactId::SELF {
1050 translated(context, StockMessage::MsgYouEphemeralTimerMinutes).replace1(minutes)
1051 } else {
1052 translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
1053 .replace1(minutes)
1054 .replace2(&by_contact.get_stock_name(context).await)
1055 }
1056}
1057
1058pub(crate) async fn msg_ephemeral_timer_hours(
1060 context: &Context,
1061 hours: &str,
1062 by_contact: ContactId,
1063) -> String {
1064 if by_contact == ContactId::SELF {
1065 translated(context, StockMessage::MsgYouEphemeralTimerHours).replace1(hours)
1066 } else {
1067 translated(context, StockMessage::MsgEphemeralTimerHoursBy)
1068 .replace1(hours)
1069 .replace2(&by_contact.get_stock_name(context).await)
1070 }
1071}
1072
1073pub(crate) async fn msg_ephemeral_timer_days(
1075 context: &Context,
1076 days: &str,
1077 by_contact: ContactId,
1078) -> String {
1079 if by_contact == ContactId::SELF {
1080 translated(context, StockMessage::MsgYouEphemeralTimerDays).replace1(days)
1081 } else {
1082 translated(context, StockMessage::MsgEphemeralTimerDaysBy)
1083 .replace1(days)
1084 .replace2(&by_contact.get_stock_name(context).await)
1085 }
1086}
1087
1088pub(crate) async fn msg_ephemeral_timer_weeks(
1090 context: &Context,
1091 weeks: &str,
1092 by_contact: ContactId,
1093) -> String {
1094 if by_contact == ContactId::SELF {
1095 translated(context, StockMessage::MsgYouEphemeralTimerWeeks).replace1(weeks)
1096 } else {
1097 translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
1098 .replace1(weeks)
1099 .replace2(&by_contact.get_stock_name(context).await)
1100 }
1101}
1102
1103pub(crate) fn forwarded(context: &Context) -> String {
1105 translated(context, StockMessage::Forwarded)
1106}
1107
1108pub(crate) fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
1110 translated(context, StockMessage::QuotaExceedingMsgBody)
1111 .replace1(&format!("{highest_usage}"))
1112 .replace("%%", "%")
1113}
1114
1115pub(crate) fn incoming_messages(context: &Context) -> String {
1117 translated(context, StockMessage::IncomingMessages)
1118}
1119
1120pub(crate) fn outgoing_messages(context: &Context) -> String {
1122 translated(context, StockMessage::OutgoingMessages)
1123}
1124
1125pub(crate) fn not_connected(context: &Context) -> String {
1127 translated(context, StockMessage::NotConnected)
1128}
1129
1130pub(crate) fn connected(context: &Context) -> String {
1132 translated(context, StockMessage::Connected)
1133}
1134
1135pub(crate) fn connecting(context: &Context) -> String {
1137 translated(context, StockMessage::Connecting)
1138}
1139
1140pub(crate) fn updating(context: &Context) -> String {
1142 translated(context, StockMessage::Updating)
1143}
1144
1145pub(crate) fn sending(context: &Context) -> String {
1147 translated(context, StockMessage::Sending)
1148}
1149
1150pub(crate) fn last_msg_sent_successfully(context: &Context) -> String {
1152 translated(context, StockMessage::LastMsgSentSuccessfully)
1153}
1154
1155pub(crate) fn error(context: &Context, error: &str) -> String {
1158 translated(context, StockMessage::Error).replace1(error)
1159}
1160
1161pub(crate) fn not_supported_by_provider(context: &Context) -> String {
1163 translated(context, StockMessage::NotSupportedByProvider)
1164}
1165
1166pub(crate) fn messages(context: &Context) -> String {
1169 translated(context, StockMessage::Messages)
1170}
1171
1172pub(crate) fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
1174 translated(context, StockMessage::PartOfTotallUsed)
1175 .replace1(part)
1176 .replace2(total)
1177}
1178
1179pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
1181 translated(context, StockMessage::InvalidUnencryptedMail).replace1(provider)
1182}
1183
1184pub(crate) fn stats_msg_body(context: &Context) -> String {
1186 translated(context, StockMessage::StatsMsgBody)
1187}
1188
1189pub(crate) fn new_group_send_first_message(context: &Context) -> String {
1191 translated(context, StockMessage::NewGroupSendFirstMessage)
1192}
1193
1194pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
1201 let name = if let Some(name) = context.get_config(Config::Displayname).await? {
1202 name
1203 } else {
1204 context.get_primary_self_addr().await?
1205 };
1206 Ok(translated(context, StockMessage::BackupTransferQr).replace1(&name))
1207}
1208
1209pub(crate) fn backup_transfer_msg_body(context: &Context) -> String {
1210 translated(context, StockMessage::BackupTransferMsgBody)
1211}
1212
1213pub(crate) fn proxy_enabled(context: &Context) -> String {
1215 translated(context, StockMessage::ProxyEnabled)
1216}
1217
1218pub(crate) fn proxy_description(context: &Context) -> String {
1220 translated(context, StockMessage::ProxyEnabledDescription)
1221}
1222
1223pub(crate) fn chat_unencrypted_explanation(context: &Context) -> String {
1225 translated(context, StockMessage::ChatUnencryptedExplanation)
1226}
1227
1228pub(crate) fn mvbox_move_deprecation(context: &Context) -> String {
1230 translated(context, StockMessage::MvboxMoveDeprecation)
1231}
1232
1233impl Viewtype {
1234 pub fn to_locale_string(&self, context: &Context) -> String {
1236 match self {
1237 Viewtype::Image => image(context),
1238 Viewtype::Gif => gif(context),
1239 Viewtype::Sticker => sticker(context),
1240 Viewtype::Audio => audio(context),
1241 Viewtype::Voice => voice_message(context),
1242 Viewtype::Video => video(context),
1243 Viewtype::File => file(context),
1244 Viewtype::Webxdc => "Mini App".to_owned(),
1245 Viewtype::Vcard => "👤".to_string(),
1246 Viewtype::Unknown | Viewtype::Text | Viewtype::Call => self.to_string(),
1248 }
1249 }
1250}
1251
1252impl Context {
1253 pub fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1256 self.translated_stockstrings
1257 .set_stock_translation(id, stockstring)?;
1258 Ok(())
1259 }
1260
1261 pub(crate) async fn update_device_chats(&self) -> Result<()> {
1262 if self.get_config_bool(Config::Bot).await? {
1263 return Ok(());
1264 }
1265
1266 if !self.sql.get_raw_config_bool("self-chat-added").await? {
1269 self.sql
1270 .set_raw_config_bool("self-chat-added", true)
1271 .await?;
1272 ChatId::create_for_contact(self, ContactId::SELF).await?;
1273 }
1274
1275 let image = include_bytes!("../assets/welcome-image.jpg");
1278 let blob = BlobObject::create_and_deduplicate_from_bytes(self, image, "welcome.jpg")?;
1279 let mut msg = Message::new(Viewtype::Image);
1280 msg.param.set(Param::File, blob.as_name());
1281 msg.param.set(Param::Filename, "welcome-image.jpg");
1282 chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
1283
1284 let mut msg = Message::new_text(welcome_message(self));
1285 chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
1286 Ok(())
1287 }
1288}
1289
1290impl Accounts {
1291 pub fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1294 self.stockstrings.set_stock_translation(id, stockstring)?;
1295 Ok(())
1296 }
1297}
1298
1299#[cfg(test)]
1300mod stock_str_tests;