Files
chatmail-core/src/stock_str.rs
B. Petersen 3c5af7a559 fix: use correct string for encryption info
encryption info needs a dedicated string for "Messages are end-to-end encrypted"
as the UI will add more infomation to the info messages,
smth. as "Tap for more information".

an alternative fix would have been to let the UI render the info-message
differently, but adding another string to core causes less friction.
2026-03-11 15:03:07 +01:00

1375 lines
46 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Module to work with translatable stock strings.
use std::collections::HashMap;
use std::sync::Arc;
use anyhow::{Result, bail};
use strum::EnumProperty as EnumPropertyTrait;
use strum_macros::EnumProperty;
use tokio::sync::RwLock;
use crate::accounts::Accounts;
use crate::blob::BlobObject;
use crate::chat::{self, Chat, ChatId};
use crate::config::Config;
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::message::{Message, Viewtype};
use crate::param::Param;
/// Storage for string translations.
#[derive(Debug, Clone)]
pub struct StockStrings {
/// Map from stock string ID to the translation.
translated_stockstrings: Arc<RwLock<HashMap<usize, String>>>,
}
/// Stock strings
///
/// These identify the string to return in [Context.stock_str]. The
/// numbers must stay in sync with `deltachat.h` `DC_STR_*` constants.
///
/// See the `stock_*` methods on [Context] to use these.
///
/// [Context]: crate::context::Context
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, EnumProperty)]
#[repr(u32)]
pub enum StockMessage {
#[strum(props(fallback = "No messages."))]
NoMessages = 1,
#[strum(props(fallback = "Me"))]
SelfMsg = 2,
#[strum(props(fallback = "Draft"))]
Draft = 3,
#[strum(props(fallback = "Voice message"))]
VoiceMessage = 7,
#[strum(props(fallback = "Image"))]
Image = 9,
#[strum(props(fallback = "Video"))]
Video = 10,
#[strum(props(fallback = "Audio"))]
Audio = 11,
#[strum(props(fallback = "File"))]
File = 12,
#[strum(props(fallback = "GIF"))]
Gif = 23,
#[strum(props(fallback = "No encryption."))]
EncrNone = 28,
#[strum(props(fallback = "Fingerprints"))]
FingerPrints = 30,
#[strum(props(fallback = "%1$s verified."))]
ContactVerified = 35,
#[strum(props(fallback = "Archived chats"))]
ArchivedChats = 40,
#[strum(props(
fallback = "Cannot login as \"%1$s\". Please check if the email address and the password are correct."
))]
CannotLogin = 60,
#[strum(props(fallback = "Location streaming enabled."))]
MsgLocationEnabled = 64,
#[strum(props(fallback = "Location streaming disabled."))]
MsgLocationDisabled = 65,
#[strum(props(fallback = "Location"))]
Location = 66,
#[strum(props(fallback = "Sticker"))]
Sticker = 67,
#[strum(props(fallback = "Device messages"))]
DeviceMessages = 68,
#[strum(props(fallback = "Saved messages"))]
SavedMessages = 69,
#[strum(props(
fallback = "Messages in this chat are generated locally by your Delta Chat app. \
Its makers use it to inform about app updates and problems during usage."
))]
DeviceMessagesHint = 70,
#[strum(props(fallback = "Get in contact!\n\n\
🙌 Tap \"QR code\" on the main screen of both devices. \
Choose \"Scan QR Code\" on one device, and point it at the other\n\n\
🌍 If not in the same room, \
scan via video call or share an invite link from \"Scan QR code\"\n\n\
Then: Enjoy your decentralized messenger experience. \
In contrast to other popular apps, \
without central control or tracking or selling you, \
friends, colleagues or family out to large organizations."))]
WelcomeMessage = 71,
#[strum(props(fallback = "Message from %1$s"))]
SubjectForNewContact = 73,
/// Unused. Was used in group chat status messages.
#[strum(props(fallback = "Failed to send message to %1$s."))]
FailedSendingTo = 74,
#[strum(props(fallback = "Error:\n\n“%1$s”"))]
ConfigurationFailed = 84,
#[strum(props(
fallback = "⚠️ Date or time of your device seem to be inaccurate (%1$s).\n\n\
Adjust your clock ⏰🔧 to ensure your messages are received correctly."
))]
BadTimeMsgBody = 85,
#[strum(props(fallback = "⚠️ Your Delta Chat version might be outdated.\n\n\
This may cause problems because your chat partners use newer versions - \
and you are missing the latest features 😳\n\
Please check https://get.delta.chat or your app store for updates."))]
UpdateReminderMsgBody = 86,
#[strum(props(
fallback = "Could not find your mail server.\n\nPlease check your internet connection."
))]
ErrorNoNetwork = 87,
// used in summaries, a noun, not a verb (not: "to reply")
#[strum(props(fallback = "Reply"))]
ReplyNoun = 90,
#[strum(props(fallback = "You deleted the \"Saved messages\" chat.\n\n\
To use the \"Saved messages\" feature again, create a new chat with yourself."))]
SelfDeletedMsgBody = 91,
#[strum(props(fallback = "Forwarded"))]
Forwarded = 97,
#[strum(props(
fallback = "⚠️ Your provider's storage is about to exceed, already %1$s%% are used.\n\n\
You may not be able to receive message when the storage is 100%% used.\n\n\
👉 Please check if you can delete old data in the provider's webinterface \
and consider to enable \"Settings / Delete Old Messages\". \
You can check your current storage usage anytime at \"Settings / Connectivity\"."
))]
QuotaExceedingMsgBody = 98,
#[strum(props(fallback = "Multi Device Synchronization"))]
SyncMsgSubject = 101,
#[strum(props(
fallback = "This message is used to synchronize data between your devices.\n\n\
👉 If you see this message in Delta Chat, please update your Delta Chat apps on all devices."
))]
SyncMsgBody = 102,
#[strum(props(fallback = "Incoming Messages"))]
IncomingMessages = 103,
#[strum(props(fallback = "Outgoing Messages"))]
OutgoingMessages = 104,
#[strum(props(fallback = "Storage on %1$s"))]
StorageOnDomain = 105,
#[strum(props(fallback = "Connected"))]
Connected = 107,
#[strum(props(fallback = "Connecting…"))]
Connecting = 108,
#[strum(props(fallback = "Updating…"))]
Updating = 109,
#[strum(props(fallback = "Sending…"))]
Sending = 110,
#[strum(props(fallback = "Your last message was sent successfully."))]
LastMsgSentSuccessfully = 111,
#[strum(props(fallback = "Error: %1$s"))]
Error = 112,
#[strum(props(fallback = "Not supported by your provider."))]
NotSupportedByProvider = 113,
#[strum(props(fallback = "Messages"))]
Messages = 114,
#[strum(props(fallback = "%1$s of %2$s used"))]
PartOfTotallUsed = 116,
#[strum(props(fallback = "%1$s invited you to join this group.\n\n\
Waiting for the device of %2$s to reply…"))]
SecureJoinStarted = 117,
#[strum(props(fallback = "%1$s replied, waiting for being added to the group…"))]
SecureJoinReplies = 118,
#[strum(props(fallback = "Scan to chat with %1$s"))]
SetupContactQRDescription = 119,
#[strum(props(fallback = "Scan to join group %1$s"))]
SecureJoinGroupQRDescription = 120,
#[strum(props(fallback = "Not connected"))]
NotConnected = 121,
#[strum(props(fallback = "You changed group name from \"%1$s\" to \"%2$s\"."))]
MsgYouChangedGrpName = 124,
#[strum(props(fallback = "Group name changed from \"%1$s\" to \"%2$s\" by %3$s."))]
MsgGrpNameChangedBy = 125,
#[strum(props(fallback = "You changed the group image."))]
MsgYouChangedGrpImg = 126,
#[strum(props(fallback = "Group image changed by %1$s."))]
MsgGrpImgChangedBy = 127,
#[strum(props(fallback = "You added member %1$s."))]
MsgYouAddMember = 128,
#[strum(props(fallback = "Member %1$s added by %2$s."))]
MsgAddMemberBy = 129,
#[strum(props(fallback = "You removed member %1$s."))]
MsgYouDelMember = 130,
#[strum(props(fallback = "Member %1$s removed by %2$s."))]
MsgDelMemberBy = 131,
#[strum(props(fallback = "You left the group."))]
MsgYouLeftGroup = 132,
#[strum(props(fallback = "Group left by %1$s."))]
MsgGroupLeftBy = 133,
#[strum(props(fallback = "You deleted the group image."))]
MsgYouDeletedGrpImg = 134,
#[strum(props(fallback = "Group image deleted by %1$s."))]
MsgGrpImgDeletedBy = 135,
#[strum(props(fallback = "You enabled location streaming."))]
MsgYouEnabledLocation = 136,
#[strum(props(fallback = "Location streaming enabled by %1$s."))]
MsgLocationEnabledBy = 137,
#[strum(props(fallback = "You disabled message deletion timer."))]
MsgYouDisabledEphemeralTimer = 138,
#[strum(props(fallback = "Message deletion timer is disabled by %1$s."))]
MsgEphemeralTimerDisabledBy = 139,
// A fallback message for unknown timer values.
// "s" stands for "second" SI unit here.
#[strum(props(fallback = "You set message deletion timer to %1$s s."))]
MsgYouEnabledEphemeralTimer = 140,
#[strum(props(fallback = "Message deletion timer is set to %1$s s by %2$s."))]
MsgEphemeralTimerEnabledBy = 141,
#[strum(props(fallback = "You set message deletion timer to 1 hour."))]
MsgYouEphemeralTimerHour = 144,
#[strum(props(fallback = "Message deletion timer is set to 1 hour by %1$s."))]
MsgEphemeralTimerHourBy = 145,
#[strum(props(fallback = "You set message deletion timer to 1 day."))]
MsgYouEphemeralTimerDay = 146,
#[strum(props(fallback = "Message deletion timer is set to 1 day by %1$s."))]
MsgEphemeralTimerDayBy = 147,
#[strum(props(fallback = "You set message deletion timer to 1 week."))]
MsgYouEphemeralTimerWeek = 148,
#[strum(props(fallback = "Message deletion timer is set to 1 week by %1$s."))]
MsgEphemeralTimerWeekBy = 149,
#[strum(props(fallback = "You set message deletion timer to %1$s minutes."))]
MsgYouEphemeralTimerMinutes = 150,
#[strum(props(fallback = "Message deletion timer is set to %1$s minutes by %2$s."))]
MsgEphemeralTimerMinutesBy = 151,
#[strum(props(fallback = "You set message deletion timer to %1$s hours."))]
MsgYouEphemeralTimerHours = 152,
#[strum(props(fallback = "Message deletion timer is set to %1$s hours by %2$s."))]
MsgEphemeralTimerHoursBy = 153,
#[strum(props(fallback = "You set message deletion timer to %1$s days."))]
MsgYouEphemeralTimerDays = 154,
#[strum(props(fallback = "Message deletion timer is set to %1$s days by %2$s."))]
MsgEphemeralTimerDaysBy = 155,
#[strum(props(fallback = "You set message deletion timer to %1$s weeks."))]
MsgYouEphemeralTimerWeeks = 156,
#[strum(props(fallback = "Message deletion timer is set to %1$s weeks by %2$s."))]
MsgEphemeralTimerWeeksBy = 157,
#[strum(props(fallback = "You set message deletion timer to 1 year."))]
MsgYouEphemeralTimerYear = 158,
#[strum(props(fallback = "Message deletion timer is set to 1 year by %1$s."))]
MsgEphemeralTimerYearBy = 159,
#[strum(props(fallback = "Scan to set up second device for %1$s"))]
BackupTransferQr = 162,
#[strum(props(fallback = " Account transferred to your second device."))]
BackupTransferMsgBody = 163,
#[strum(props(fallback = "Messages are end-to-end encrypted."))]
ChatProtectionEnabled = 170,
#[strum(props(fallback = "Others will only see this group after you sent a first message."))]
NewGroupSendFirstMessage = 172,
#[strum(props(fallback = "Member %1$s added."))]
MsgAddMember = 173,
#[strum(props(
fallback = "⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet."
))]
InvalidUnencryptedMail = 174,
#[strum(props(fallback = "You reacted %1$s to \"%2$s\""))]
MsgYouReacted = 176,
#[strum(props(fallback = "%1$s reacted %2$s to \"%3$s\""))]
MsgReactedBy = 177,
#[strum(props(fallback = "Member %1$s removed."))]
MsgDelMember = 178,
#[strum(props(fallback = "Establishing connection, please wait…"))]
SecurejoinWait = 190,
#[strum(props(fallback = "❤️ Seems you're enjoying Delta Chat!
Please consider donating to help that Delta Chat stays free for everyone.
While Delta Chat is free to use and open source, development costs money.
Help keeping us to keep Delta Chat independent and make it more awesome in the future.
https://delta.chat/donate"))]
DonationRequest = 193,
#[strum(props(fallback = "Declined call"))]
DeclinedCall = 196,
#[strum(props(fallback = "Canceled call"))]
CanceledCall = 197,
#[strum(props(fallback = "Missed call"))]
MissedCall = 198,
#[strum(props(fallback = "You left the channel."))]
MsgYouLeftBroadcast = 200,
#[strum(props(fallback = "Scan to join channel %1$s"))]
SecureJoinBrodcastQRDescription = 201,
#[strum(props(fallback = "You joined the channel."))]
MsgYouJoinedBroadcast = 202,
#[strum(props(fallback = "%1$s invited you to join this channel.\n\n\
Waiting for the device of %2$s to reply…"))]
SecureJoinBroadcastStarted = 203,
#[strum(props(fallback = "Channel name changed from \"%1$s\" to \"%2$s\"."))]
MsgBroadcastNameChanged = 204,
#[strum(props(fallback = "Channel image changed."))]
MsgBroadcastImgChanged = 205,
#[strum(props(
fallback = "The attachment contains anonymous usage statistics, which helps us improve Delta Chat. Thank you!"
))]
StatsMsgBody = 210,
#[strum(props(fallback = "Proxy Enabled"))]
ProxyEnabled = 220,
#[strum(props(
fallback = "You are using a proxy. If you're having trouble connecting, try a different proxy."
))]
ProxyEnabledDescription = 221,
#[strum(props(fallback = "Messages in this chat use classic email and are not encrypted."))]
ChatUnencryptedExplanation = 230,
#[strum(props(
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"
))]
MvboxMoveDeprecation = 231,
#[strum(props(fallback = "Outgoing audio call"))]
OutgoingAudioCall = 232,
#[strum(props(fallback = "Outgoing video call"))]
OutgoingVideoCall = 233,
#[strum(props(fallback = "Incoming audio call"))]
IncomingAudioCall = 234,
#[strum(props(fallback = "Incoming video call"))]
IncomingVideoCall = 235,
#[strum(props(fallback = "You changed the chat description."))]
MsgYouChangedDescription = 240,
#[strum(props(fallback = "Chat description changed by %1$s."))]
MsgChatDescriptionChangedBy = 241,
#[strum(props(fallback = "Messages are end-to-end encrypted."))]
MessagesAreE2ee = 242,
}
impl StockMessage {
/// Default untranslated strings for stock messages.
///
/// These could be used in logging calls, so no logging here.
fn fallback(self) -> &'static str {
self.get_str("fallback").unwrap_or_default()
}
}
impl Default for StockStrings {
fn default() -> Self {
StockStrings::new()
}
}
impl StockStrings {
/// Creates a new translated string storage.
pub fn new() -> Self {
Self {
translated_stockstrings: Arc::new(RwLock::new(Default::default())),
}
}
async fn translated(&self, id: StockMessage) -> String {
self.translated_stockstrings
.read()
.await
.get(&(id as usize))
.map(AsRef::as_ref)
.unwrap_or_else(|| id.fallback())
.to_string()
}
async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
if stockstring.contains("%1") && !id.fallback().contains("%1") {
bail!(
"translation {} contains invalid %1 placeholder, default is {}",
stockstring,
id.fallback()
);
}
if stockstring.contains("%2") && !id.fallback().contains("%2") {
bail!(
"translation {} contains invalid %2 placeholder, default is {}",
stockstring,
id.fallback()
);
}
self.translated_stockstrings
.write()
.await
.insert(id as usize, stockstring);
Ok(())
}
}
async fn translated(context: &Context, id: StockMessage) -> String {
context.translated_stockstrings.translated(id).await
}
/// Helper trait only meant to be implemented for [`String`].
trait StockStringMods: AsRef<str> + Sized {
/// Substitutes the first replacement value if one is present.
fn replace1(&self, replacement: &str) -> String {
self.as_ref()
.replacen("%1$s", replacement, 1)
.replacen("%1$d", replacement, 1)
.replacen("%1$@", replacement, 1)
}
/// Substitutes the second replacement value if one is present.
///
/// Be aware you probably should have also called [`StockStringMods::replace1`] if
/// you are calling this.
fn replace2(&self, replacement: &str) -> String {
self.as_ref()
.replacen("%2$s", replacement, 1)
.replacen("%2$d", replacement, 1)
.replacen("%2$@", replacement, 1)
}
/// Substitutes the third replacement value if one is present.
///
/// Be aware you probably should have also called [`StockStringMods::replace1`] and
/// [`StockStringMods::replace2`] if you are calling this.
fn replace3(&self, replacement: &str) -> String {
self.as_ref()
.replacen("%3$s", replacement, 1)
.replacen("%3$d", replacement, 1)
.replacen("%3$@", replacement, 1)
}
}
impl ContactId {
/// Get contact name, e.g. `Bob`, or `bob@example.net` if no name is set.
async fn get_stock_name(self, context: &Context) -> String {
Contact::get_by_id(context, self)
.await
.map(|contact| contact.get_display_name().to_string())
.unwrap_or_else(|_| self.to_string())
}
}
impl StockStringMods for String {}
/// Stock string: `No messages.`.
pub(crate) async fn no_messages(context: &Context) -> String {
translated(context, StockMessage::NoMessages).await
}
/// Stock string: `Me`.
pub(crate) async fn self_msg(context: &Context) -> String {
translated(context, StockMessage::SelfMsg).await
}
/// Stock string: `Draft`.
pub(crate) async fn draft(context: &Context) -> String {
translated(context, StockMessage::Draft).await
}
/// Stock string: `Voice message`.
pub(crate) async fn voice_message(context: &Context) -> String {
translated(context, StockMessage::VoiceMessage).await
}
/// Stock string: `Image`.
pub(crate) async fn image(context: &Context) -> String {
translated(context, StockMessage::Image).await
}
/// Stock string: `Video`.
pub(crate) async fn video(context: &Context) -> String {
translated(context, StockMessage::Video).await
}
/// Stock string: `Audio`.
pub(crate) async fn audio(context: &Context) -> String {
translated(context, StockMessage::Audio).await
}
/// Stock string: `File`.
pub(crate) async fn file(context: &Context) -> String {
translated(context, StockMessage::File).await
}
/// Stock string: `Group name changed from "%1$s" to "%2$s".`.
pub(crate) async fn msg_grp_name(
context: &Context,
from_group: &str,
to_group: &str,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouChangedGrpName)
.await
.replace1(from_group)
.replace2(to_group)
} else {
translated(context, StockMessage::MsgGrpNameChangedBy)
.await
.replace1(from_group)
.replace2(to_group)
.replace3(&by_contact.get_stock_name(context).await)
}
}
pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouChangedGrpImg).await
} else {
translated(context, StockMessage::MsgGrpImgChangedBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
}
}
pub(crate) async fn msg_chat_description_changed(
context: &Context,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouChangedDescription).await
} else {
translated(context, StockMessage::MsgChatDescriptionChangedBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Member %1$s added.`, `You added member %1$s.` or `Member %1$s added by %2$s.`.
///
/// The `added_member` and `by_contact` contacts
/// are looked up in the database to get the display names.
pub(crate) async fn msg_add_member_local(
context: &Context,
added_member: ContactId,
by_contact: ContactId,
) -> String {
let whom = added_member.get_stock_name(context).await;
if by_contact == ContactId::UNDEFINED {
translated(context, StockMessage::MsgAddMember)
.await
.replace1(&whom)
} else if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouAddMember)
.await
.replace1(&whom)
} else {
translated(context, StockMessage::MsgAddMemberBy)
.await
.replace1(&whom)
.replace2(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Member %1$s removed.` or `You removed member %1$s.` or `Member %1$s removed by %2$s.`
///
/// The `removed_member` and `by_contact` contacts
/// are looked up in the database to get the display names.
pub(crate) async fn msg_del_member_local(
context: &Context,
removed_member: ContactId,
by_contact: ContactId,
) -> String {
let whom = removed_member.get_stock_name(context).await;
if by_contact == ContactId::UNDEFINED {
translated(context, StockMessage::MsgDelMember)
.await
.replace1(&whom)
} else if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouDelMember)
.await
.replace1(&whom)
} else {
translated(context, StockMessage::MsgDelMemberBy)
.await
.replace1(&whom)
.replace2(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `You left the group.` or `Group left by %1$s.`.
pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouLeftGroup).await
} else {
translated(context, StockMessage::MsgGroupLeftBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `You left the channel.`
pub(crate) async fn msg_you_left_broadcast(context: &Context) -> String {
translated(context, StockMessage::MsgYouLeftBroadcast).await
}
/// Stock string: `You joined the channel.`
pub(crate) async fn msg_you_joined_broadcast(context: &Context) -> String {
translated(context, StockMessage::MsgYouJoinedBroadcast).await
}
/// Stock string: `%1$s invited you to join this channel. Waiting for the device of %2$s to reply…`.
pub(crate) async fn secure_join_broadcast_started(
context: &Context,
inviter_contact_id: ContactId,
) -> String {
if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
translated(context, StockMessage::SecureJoinBroadcastStarted)
.await
.replace1(contact.get_display_name())
.replace2(contact.get_display_name())
} else {
format!("secure_join_started: unknown contact {inviter_contact_id}")
}
}
/// Stock string: `Channel name changed from "1%s" to "2$s".`
pub(crate) async fn msg_broadcast_name_changed(context: &Context, from: &str, to: &str) -> String {
translated(context, StockMessage::MsgBroadcastNameChanged)
.await
.replace1(from)
.replace2(to)
}
/// Stock string `Channel image changed.`
pub(crate) async fn msg_broadcast_img_changed(context: &Context) -> String {
translated(context, StockMessage::MsgBroadcastImgChanged).await
}
/// Stock string: `You reacted %1$s to "%2$s"` or `%1$s reacted %2$s to "%3$s"`.
pub(crate) async fn msg_reacted(
context: &Context,
by_contact: ContactId,
reaction: &str,
summary: &str,
) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouReacted)
.await
.replace1(reaction)
.replace2(summary)
} else {
translated(context, StockMessage::MsgReactedBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace2(reaction)
.replace3(summary)
}
}
/// Stock string: `GIF`.
pub(crate) async fn gif(context: &Context) -> String {
translated(context, StockMessage::Gif).await
}
/// Stock string: `No encryption.`.
pub(crate) async fn encr_none(context: &Context) -> String {
translated(context, StockMessage::EncrNone).await
}
/// Stock string: `Fingerprints`.
pub(crate) async fn finger_prints(context: &Context) -> String {
translated(context, StockMessage::FingerPrints).await
}
/// Stock string: `Group image deleted.`.
pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouDeletedGrpImg).await
} else {
translated(context, StockMessage::MsgGrpImgDeletedBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `%1$s invited you to join this group. Waiting for the device of %2$s to reply…`.
pub(crate) async fn secure_join_started(
context: &Context,
inviter_contact_id: ContactId,
) -> String {
if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
translated(context, StockMessage::SecureJoinStarted)
.await
.replace1(contact.get_display_name())
.replace2(contact.get_display_name())
} else {
format!("secure_join_started: unknown contact {inviter_contact_id}")
}
}
/// Stock string: `%1$s replied, waiting for being added to the group…`.
pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
translated(context, StockMessage::SecureJoinReplies)
.await
.replace1(&contact_id.get_stock_name(context).await)
}
/// Stock string: `Establishing connection, please wait…`.
pub(crate) async fn securejoin_wait(context: &Context) -> String {
translated(context, StockMessage::SecurejoinWait).await
}
/// Stock string: `❤️ Seems you're enjoying Delta Chat!`…
pub(crate) async fn donation_request(context: &Context) -> String {
translated(context, StockMessage::DonationRequest).await
}
/// Stock string: `Outgoing video call` or `Outgoing audio call`.
pub(crate) async fn outgoing_call(context: &Context, has_video: bool) -> String {
translated(
context,
if has_video {
StockMessage::OutgoingVideoCall
} else {
StockMessage::OutgoingAudioCall
},
)
.await
}
/// Stock string: `Incoming video call` or `Incoming audio call`.
pub(crate) async fn incoming_call(context: &Context, has_video: bool) -> String {
translated(
context,
if has_video {
StockMessage::IncomingVideoCall
} else {
StockMessage::IncomingAudioCall
},
)
.await
}
/// Stock string: `Declined call`.
pub(crate) async fn declined_call(context: &Context) -> String {
translated(context, StockMessage::DeclinedCall).await
}
/// Stock string: `Canceled call`.
pub(crate) async fn canceled_call(context: &Context) -> String {
translated(context, StockMessage::CanceledCall).await
}
/// Stock string: `Missed call`.
pub(crate) async fn missed_call(context: &Context) -> String {
translated(context, StockMessage::MissedCall).await
}
/// Stock string: `Scan to chat with %1$s`.
pub(crate) async fn setup_contact_qr_description(
context: &Context,
display_name: &str,
addr: &str,
) -> String {
let name = if display_name.is_empty() {
addr.to_owned()
} else {
display_name.to_owned()
};
translated(context, StockMessage::SetupContactQRDescription)
.await
.replace1(&name)
}
/// Stock string: `Scan to join group %1$s`.
pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
translated(context, StockMessage::SecureJoinGroupQRDescription)
.await
.replace1(chat.get_name())
}
/// Stock string: `Scan to join channel %1$s`.
pub(crate) async fn secure_join_broadcast_qr_description(context: &Context, chat: &Chat) -> String {
translated(context, StockMessage::SecureJoinBrodcastQRDescription)
.await
.replace1(chat.get_name())
}
/// Stock string: `%1$s verified.`.
#[allow(dead_code)]
pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
let addr = contact.get_display_name();
translated(context, StockMessage::ContactVerified)
.await
.replace1(addr)
}
/// Stock string: `Archived chats`.
pub(crate) async fn archived_chats(context: &Context) -> String {
translated(context, StockMessage::ArchivedChats).await
}
/// Stock string: `Multi Device Synchronization`.
pub(crate) async fn sync_msg_subject(context: &Context) -> String {
translated(context, StockMessage::SyncMsgSubject).await
}
/// Stock string: `This message is used to synchronize data between your devices.`.
pub(crate) async fn sync_msg_body(context: &Context) -> String {
translated(context, StockMessage::SyncMsgBody).await
}
/// Stock string: `Cannot login as \"%1$s\". Please check...`.
pub(crate) async fn cannot_login(context: &Context, user: &str) -> String {
translated(context, StockMessage::CannotLogin)
.await
.replace1(user)
}
/// Stock string: `Location streaming enabled.`.
pub(crate) async fn msg_location_enabled(context: &Context) -> String {
translated(context, StockMessage::MsgLocationEnabled).await
}
/// Stock string: `Location streaming enabled by ...`.
pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
if contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEnabledLocation).await
} else {
translated(context, StockMessage::MsgLocationEnabledBy)
.await
.replace1(&contact.get_stock_name(context).await)
}
}
/// Stock string: `Location streaming disabled.`.
pub(crate) async fn msg_location_disabled(context: &Context) -> String {
translated(context, StockMessage::MsgLocationDisabled).await
}
/// Stock string: `Location`.
pub(crate) async fn location(context: &Context) -> String {
translated(context, StockMessage::Location).await
}
/// Stock string: `Sticker`.
pub(crate) async fn sticker(context: &Context) -> String {
translated(context, StockMessage::Sticker).await
}
/// Stock string: `Device messages`.
pub(crate) async fn device_messages(context: &Context) -> String {
translated(context, StockMessage::DeviceMessages).await
}
/// Stock string: `Saved messages`.
pub(crate) async fn saved_messages(context: &Context) -> String {
translated(context, StockMessage::SavedMessages).await
}
/// Stock string: `Messages in this chat are generated locally by...`.
pub(crate) async fn device_messages_hint(context: &Context) -> String {
translated(context, StockMessage::DeviceMessagesHint).await
}
/// Stock string: `Welcome to Delta Chat! ...`.
pub(crate) async fn welcome_message(context: &Context) -> String {
translated(context, StockMessage::WelcomeMessage).await
}
/// Stock string: `Message from %1$s`.
// TODO: This can compute `self_name` itself instead of asking the caller to do this.
pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
translated(context, StockMessage::SubjectForNewContact)
.await
.replace1(self_name)
}
/// Stock string: `Message deletion timer is disabled.`.
pub(crate) async fn msg_ephemeral_timer_disabled(
context: &Context,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouDisabledEphemeralTimer).await
} else {
translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Message deletion timer is set to %1$s s.`.
pub(crate) async fn msg_ephemeral_timer_enabled(
context: &Context,
timer: &str,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEnabledEphemeralTimer)
.await
.replace1(timer)
} else {
translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
.await
.replace1(timer)
.replace2(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Message deletion timer is set to 1 hour.`.
pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEphemeralTimerHour).await
} else {
translated(context, StockMessage::MsgEphemeralTimerHourBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Message deletion timer is set to 1 day.`.
pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEphemeralTimerDay).await
} else {
translated(context, StockMessage::MsgEphemeralTimerDayBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Message deletion timer is set to 1 week.`.
pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEphemeralTimerWeek).await
} else {
translated(context, StockMessage::MsgEphemeralTimerWeekBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Message deletion timer is set to 1 year.`.
pub(crate) async fn msg_ephemeral_timer_year(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEphemeralTimerYear).await
} else {
translated(context, StockMessage::MsgEphemeralTimerYearBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Error:\n\n“%1$s”`.
pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
translated(context, StockMessage::ConfigurationFailed)
.await
.replace1(details)
}
/// Stock string: `⚠️ Date or time of your device seem to be inaccurate (%1$s)...`.
// TODO: This could compute now itself.
pub(crate) async fn bad_time_msg_body(context: &Context, now: &str) -> String {
translated(context, StockMessage::BadTimeMsgBody)
.await
.replace1(now)
}
/// Stock string: `⚠️ Your Delta Chat version might be outdated...`.
pub(crate) async fn update_reminder_msg_body(context: &Context) -> String {
translated(context, StockMessage::UpdateReminderMsgBody).await
}
/// Stock string: `Could not find your mail server...`.
pub(crate) async fn error_no_network(context: &Context) -> String {
translated(context, StockMessage::ErrorNoNetwork).await
}
/// Stock string: `Messages are end-to-end encrypted.`, used in info-messages, UI may add smth. as `Tap to learn more.`
pub(crate) async fn messages_e2ee_info_msg(context: &Context) -> String {
translated(context, StockMessage::ChatProtectionEnabled).await
}
/// Stock string: `Messages are end-to-end encrypted.`
pub(crate) async fn messages_are_e2ee(context: &Context) -> String {
translated(context, StockMessage::MessagesAreE2ee).await
}
/// Stock string: `Reply`.
pub(crate) async fn reply_noun(context: &Context) -> String {
translated(context, StockMessage::ReplyNoun).await
}
/// Stock string: `You deleted the \"Saved messages\" chat...`.
pub(crate) async fn self_deleted_msg_body(context: &Context) -> String {
translated(context, StockMessage::SelfDeletedMsgBody).await
}
/// Stock string: `Message deletion timer is set to %1$s minutes.`.
pub(crate) async fn msg_ephemeral_timer_minutes(
context: &Context,
minutes: &str,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEphemeralTimerMinutes)
.await
.replace1(minutes)
} else {
translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
.await
.replace1(minutes)
.replace2(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Message deletion timer is set to %1$s hours.`.
pub(crate) async fn msg_ephemeral_timer_hours(
context: &Context,
hours: &str,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEphemeralTimerHours)
.await
.replace1(hours)
} else {
translated(context, StockMessage::MsgEphemeralTimerHoursBy)
.await
.replace1(hours)
.replace2(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Message deletion timer is set to %1$s days.`.
pub(crate) async fn msg_ephemeral_timer_days(
context: &Context,
days: &str,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEphemeralTimerDays)
.await
.replace1(days)
} else {
translated(context, StockMessage::MsgEphemeralTimerDaysBy)
.await
.replace1(days)
.replace2(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Message deletion timer is set to %1$s weeks.`.
pub(crate) async fn msg_ephemeral_timer_weeks(
context: &Context,
weeks: &str,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEphemeralTimerWeeks)
.await
.replace1(weeks)
} else {
translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
.await
.replace1(weeks)
.replace2(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Forwarded`.
pub(crate) async fn forwarded(context: &Context) -> String {
translated(context, StockMessage::Forwarded).await
}
/// Stock string: `⚠️ Your provider's storage is about to exceed...`.
pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
translated(context, StockMessage::QuotaExceedingMsgBody)
.await
.replace1(&format!("{highest_usage}"))
.replace("%%", "%")
}
/// Stock string: `Incoming Messages`.
pub(crate) async fn incoming_messages(context: &Context) -> String {
translated(context, StockMessage::IncomingMessages).await
}
/// Stock string: `Outgoing Messages`.
pub(crate) async fn outgoing_messages(context: &Context) -> String {
translated(context, StockMessage::OutgoingMessages).await
}
/// Stock string: `Not connected`.
pub(crate) async fn not_connected(context: &Context) -> String {
translated(context, StockMessage::NotConnected).await
}
/// Stock string: `Connected`.
pub(crate) async fn connected(context: &Context) -> String {
translated(context, StockMessage::Connected).await
}
/// Stock string: `Connecting…`.
pub(crate) async fn connecting(context: &Context) -> String {
translated(context, StockMessage::Connecting).await
}
/// Stock string: `Updating…`.
pub(crate) async fn updating(context: &Context) -> String {
translated(context, StockMessage::Updating).await
}
/// Stock string: `Sending…`.
pub(crate) async fn sending(context: &Context) -> String {
translated(context, StockMessage::Sending).await
}
/// Stock string: `Your last message was sent successfully.`.
pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
translated(context, StockMessage::LastMsgSentSuccessfully).await
}
/// Stock string: `Error: %1$s…`.
/// `%1$s` will be replaced by a possibly more detailed, typically english, error description.
pub(crate) async fn error(context: &Context, error: &str) -> String {
translated(context, StockMessage::Error)
.await
.replace1(error)
}
/// Stock string: `Not supported by your provider.`.
pub(crate) async fn not_supported_by_provider(context: &Context) -> String {
translated(context, StockMessage::NotSupportedByProvider).await
}
/// Stock string: `Messages`.
/// Used as a subtitle in quota context; can be plural always.
pub(crate) async fn messages(context: &Context) -> String {
translated(context, StockMessage::Messages).await
}
/// Stock string: `%1$s of %2$s used`.
pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
translated(context, StockMessage::PartOfTotallUsed)
.await
.replace1(part)
.replace2(total)
}
/// Stock string: `⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet. Tap to learn more.`.
pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
translated(context, StockMessage::InvalidUnencryptedMail)
.await
.replace1(provider)
}
/// Stock string: `The attachment contains anonymous usage statistics, which helps us improve Delta Chat. Thank you!`
pub(crate) async fn stats_msg_body(context: &Context) -> String {
translated(context, StockMessage::StatsMsgBody).await
}
/// Stock string: `Others will only see this group after you sent a first message.`.
pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
translated(context, StockMessage::NewGroupSendFirstMessage).await
}
/// Text to put in the [`Qr::Backup2`] rendered SVG image.
///
/// The default is "Scan to set up second device for NAME".
/// The account name (or address as fallback) are looked up from the context.
///
/// [`Qr::Backup2`]: crate::qr::Qr::Backup2
pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
let name = if let Some(name) = context.get_config(Config::Displayname).await? {
name
} else {
context.get_primary_self_addr().await?
};
Ok(translated(context, StockMessage::BackupTransferQr)
.await
.replace1(&name))
}
pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String {
translated(context, StockMessage::BackupTransferMsgBody).await
}
/// Stock string: `Proxy Enabled`.
pub(crate) async fn proxy_enabled(context: &Context) -> String {
translated(context, StockMessage::ProxyEnabled).await
}
/// Stock string: `You are using a proxy. If you're having trouble connecting, try a different proxy.`.
pub(crate) async fn proxy_description(context: &Context) -> String {
translated(context, StockMessage::ProxyEnabledDescription).await
}
/// Stock string: `Messages in this chat use classic email and are not encrypted.`.
pub(crate) async fn chat_unencrypted_explanation(context: &Context) -> String {
translated(context, StockMessage::ChatUnencryptedExplanation).await
}
/// Stock string: `You are using the legacy option "Move automatically to DeltaChat Folder`…
pub(crate) async fn mvbox_move_deprecation(context: &Context) -> String {
translated(context, StockMessage::MvboxMoveDeprecation).await
}
impl Viewtype {
/// returns Localized name for message viewtype
pub async fn to_locale_string(&self, context: &Context) -> String {
match self {
Viewtype::Image => image(context).await,
Viewtype::Gif => gif(context).await,
Viewtype::Sticker => sticker(context).await,
Viewtype::Audio => audio(context).await,
Viewtype::Voice => voice_message(context).await,
Viewtype::Video => video(context).await,
Viewtype::File => file(context).await,
Viewtype::Webxdc => "Mini App".to_owned(),
Viewtype::Vcard => "👤".to_string(),
// The following shouldn't normally be shown to users, so translations aren't needed.
Viewtype::Unknown | Viewtype::Text | Viewtype::Call => self.to_string(),
}
}
}
impl Context {
/// Set the stock string for the [StockMessage].
///
pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
self.translated_stockstrings
.set_stock_translation(id, stockstring)
.await?;
Ok(())
}
pub(crate) async fn update_device_chats(&self) -> Result<()> {
if self.get_config_bool(Config::Bot).await? {
return Ok(());
}
// create saved-messages chat; we do this only once, if the user has deleted the chat,
// he can recreate it manually (make sure we do not re-add it when configure() was called a second time)
if !self.sql.get_raw_config_bool("self-chat-added").await? {
self.sql
.set_raw_config_bool("self-chat-added", true)
.await?;
ChatId::create_for_contact(self, ContactId::SELF).await?;
}
// add welcome-messages. by the label, this is done only once,
// if the user has deleted the message or the chat, it is not added again.
let image = include_bytes!("../assets/welcome-image.jpg");
let blob = BlobObject::create_and_deduplicate_from_bytes(self, image, "welcome.jpg")?;
let mut msg = Message::new(Viewtype::Image);
msg.param.set(Param::File, blob.as_name());
msg.param.set(Param::Filename, "welcome-image.jpg");
chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
let mut msg = Message::new_text(welcome_message(self).await);
chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
Ok(())
}
}
impl Accounts {
/// Set the stock string for the [StockMessage].
///
pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
self.stockstrings
.set_stock_translation(id, stockstring)
.await?;
Ok(())
}
}
#[cfg(test)]
mod stock_str_tests;