Compare commits

...

4 Commits

Author SHA1 Message Date
holger krekel
3738cd6af8 address @flub comments 2019-10-09 15:04:34 +02:00
holger krekel
da99bba025 expose and test set_stock_translation to Python 2019-10-09 12:24:06 +02:00
holger krekel
bdfd548779 error out if %1 %2 replacements are not contained in default english version 2019-10-09 10:02:49 +02:00
holger krekel
af9ae153ae introduce set_stock_translation and remove call to DC_EVENT_GET_STRING 2019-10-09 09:48:24 +02:00
12 changed files with 197 additions and 75 deletions

View File

@@ -402,6 +402,18 @@ int dc_set_config (dc_context_t* context, const char*
*/ */
char* dc_get_config (dc_context_t* context, const char* key); char* dc_get_config (dc_context_t* context, const char* key);
/**
* Set stock string translation.
*
* The function will emit warnings if it returns an error state.
*
* @param context The context object
* @param stock_id the integer id of the stock message (DC_STR_*)
* @param stock_msg the message to be used
* @return int (==0 on error, 1 on success)
*/
int dc_set_stock_translation(dc_context_t* context, uint32_t, const char* value);
/** /**
* Get information about the context. * Get information about the context.
@@ -4189,16 +4201,10 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/** /**
* Requeste a localized string from the frontend. * (DEPRECATED, DC_EVENT_GET_STRING is not emmitted anymore. Use
* dc_set_stock_translation() to pre-fill translations for stock
* messages.
* *
* @param data1 (int) ID of the string to request, one of the DC_STR_* constants.
* @param data2 (int) The count. If the requested string contains a placeholder for a numeric value,
* the ui may use this value to return different strings on different plural forms.
* @return (const char*) Null-terminated UTF-8 string.
* The string will be free()'d by the core,
* so it must be allocated using malloc() or a compatible function.
* Return 0 if the ui cannot provide the requested string
* the core will use a default string in english language then.
*/ */
#define DC_EVENT_GET_STRING 2091 #define DC_EVENT_GET_STRING 2091
@@ -4316,7 +4322,8 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
#define DC_STR_MSGLOCATIONENABLED 64 #define DC_STR_MSGLOCATIONENABLED 64
#define DC_STR_MSGLOCATIONDISABLED 65 #define DC_STR_MSGLOCATIONDISABLED 65
#define DC_STR_LOCATION 66 #define DC_STR_LOCATION 66
#define DC_STR_COUNT 66 #define DC_STR_STICKER 67
#define DC_STR_COUNT 67
void dc_str_unref (char*); void dc_str_unref (char*);

View File

@@ -25,6 +25,7 @@ use num_traits::{FromPrimitive, ToPrimitive};
use deltachat::contact::Contact; use deltachat::contact::Contact;
use deltachat::context::Context; use deltachat::context::Context;
use deltachat::dc_tools::{as_path, as_str, dc_strdup, to_string_lossy, OsStrExt, StrExt}; use deltachat::dc_tools::{as_path, as_str, dc_strdup, to_string_lossy, OsStrExt, StrExt};
use deltachat::stock::StockMessage;
use deltachat::*; use deltachat::*;
// as C lacks a good and portable error handling, // as C lacks a good and portable error handling,
@@ -166,12 +167,6 @@ impl ContextWrapper {
contact_id as uintptr_t, contact_id as uintptr_t,
progress as uintptr_t, progress as uintptr_t,
), ),
Event::GetString { id, count } => ffi_cb(
self,
event_id,
id.to_u32().unwrap_or_default() as uintptr_t,
count as uintptr_t,
),
} }
} }
None => 0, None => 0,
@@ -342,6 +337,35 @@ pub unsafe extern "C" fn dc_get_config(
} }
} }
#[no_mangle]
pub unsafe extern "C" fn dc_set_stock_translation(
context: *mut dc_context_t,
stock_id: u32,
stock_msg: *mut libc::c_char,
) -> libc::c_int {
if context.is_null() || stock_msg.is_null() {
eprintln!("ignoring careless call to dc_set_stock_string");
return 0;
}
let msg = as_str(stock_msg).to_string();
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| match StockMessage::from_u32(stock_id) {
Some(id) => match ctx.set_stock_translation(id, msg) {
Ok(()) => 1,
Err(err) => {
warn!(ctx, "set_stock_translation failed: {}", err);
0
}
},
None => {
warn!(ctx, "invalid stock message id {}", stock_id);
0
}
})
.unwrap_or(0)
}
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn dc_get_info(context: *mut dc_context_t) -> *mut libc::c_char { pub unsafe extern "C" fn dc_get_info(context: *mut dc_context_t) -> *mut libc::c_char {
if context.is_null() { if context.is_null() {

View File

@@ -43,7 +43,6 @@ use self::cmdline::*;
fn receive_event(_context: &Context, event: Event) -> libc::uintptr_t { fn receive_event(_context: &Context, event: Event) -> libc::uintptr_t {
match event { match event {
Event::GetString { .. } => {}
Event::Info(msg) => { Event::Info(msg) => {
/* do not show the event as this would fill the screen */ /* do not show the event as this would fill the screen */
println!("{}", msg); println!("{}", msg);

View File

@@ -71,6 +71,18 @@ class Account(object):
d[key.lower()] = value d[key.lower()] = value
return d return d
def set_stock_translation(self, id, string):
""" set stock translation string.
:param id: id of stock string (const.DC_STR_*)
:param value: string to set as new transalation
:returns: None
"""
string = string.encode("utf8")
res = lib.dc_set_stock_translation(self._dc_context, id, string)
if res == 0:
raise ValueError("could not set translation string")
def set_config(self, name, value): def set_config(self, name, value):
""" set configuration values. """ set configuration values.

View File

@@ -85,14 +85,61 @@ DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061
DC_EVENT_GET_STRING = 2091 DC_EVENT_GET_STRING = 2091
DC_EVENT_FILE_COPIED = 2055 DC_EVENT_FILE_COPIED = 2055
DC_EVENT_IS_OFFLINE = 2081 DC_EVENT_IS_OFFLINE = 2081
DC_STR_SELFNOTINGRP = 21
DC_PROVIDER_STATUS_OK = 1 DC_PROVIDER_STATUS_OK = 1
DC_PROVIDER_STATUS_PREPARATION = 2 DC_PROVIDER_STATUS_PREPARATION = 2
DC_PROVIDER_STATUS_BROKEN = 3 DC_PROVIDER_STATUS_BROKEN = 3
DC_STR_NOMESSAGES = 1
DC_STR_SELF = 2
DC_STR_DRAFT = 3
DC_STR_MEMBER = 4
DC_STR_CONTACT = 6
DC_STR_VOICEMESSAGE = 7
DC_STR_DEADDROP = 8
DC_STR_IMAGE = 9
DC_STR_VIDEO = 10
DC_STR_AUDIO = 11
DC_STR_FILE = 12
DC_STR_STATUSLINE = 13
DC_STR_NEWGROUPDRAFT = 14
DC_STR_MSGGRPNAME = 15
DC_STR_MSGGRPIMGCHANGED = 16
DC_STR_MSGADDMEMBER = 17
DC_STR_MSGDELMEMBER = 18
DC_STR_MSGGROUPLEFT = 19
DC_STR_GIF = 23
DC_STR_ENCRYPTEDMSG = 24
DC_STR_E2E_AVAILABLE = 25
DC_STR_ENCR_TRANSP = 27
DC_STR_ENCR_NONE = 28
DC_STR_CANTDECRYPT_MSG_BODY = 29
DC_STR_FINGERPRINTS = 30
DC_STR_READRCPT = 31
DC_STR_READRCPT_MAILBODY = 32
DC_STR_MSGGRPIMGDELETED = 33
DC_STR_E2E_PREFERRED = 34
DC_STR_CONTACT_VERIFIED = 35
DC_STR_CONTACT_NOT_VERIFIED = 36
DC_STR_CONTACT_SETUP_CHANGED = 37
DC_STR_ARCHIVEDCHATS = 40
DC_STR_STARREDMSGS = 41
DC_STR_AC_SETUP_MSG_SUBJECT = 42
DC_STR_AC_SETUP_MSG_BODY = 43
DC_STR_SELFTALK_SUBTITLE = 50
DC_STR_CANNOT_LOGIN = 60
DC_STR_SERVER_RESPONSE = 61
DC_STR_MSGACTIONBYUSER = 62
DC_STR_MSGACTIONBYME = 63
DC_STR_MSGLOCATIONENABLED = 64
DC_STR_MSGLOCATIONDISABLED = 65
DC_STR_LOCATION = 66
DC_STR_STICKER = 67
DC_STR_COUNT = 67
# end const generated # end const generated
def read_event_defines(f): def read_event_defines(f):
rex = re.compile(r'#define\s+((?:DC_EVENT_|DC_QR|DC_MSG|DC_STATE_|' rex = re.compile(r'#define\s+((?:DC_EVENT_|DC_QR|DC_MSG|DC_STATE_|DC_STR|'
r'DC_CONTACT_ID_|DC_GCL|DC_CHAT|DC_PROVIDER)\S+)\s+([x\d]+).*') r'DC_CONTACT_ID_|DC_GCL|DC_CHAT|DC_PROVIDER)\S+)\s+([x\d]+).*')
for line in f: for line in f:
m = rex.match(line) m = rex.match(line)

View File

@@ -148,6 +148,27 @@ class TestOfflineChat:
chat.set_name("title2") chat.set_name("title2")
assert chat.get_name() == "title2" assert chat.get_name() == "title2"
def test_group_chat_creation_with_translation(self, ac1):
ac1.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %1$s")
ac1._evlogger.consume_events()
with pytest.raises(ValueError):
ac1.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %2$s")
ac1._evlogger.get_matching("DC_EVENT_WARNING")
with pytest.raises(ValueError):
ac1.set_stock_translation(500, "xyz %1$s")
ac1._evlogger.get_matching("DC_EVENT_WARNING")
contact1 = ac1.create_contact("some1@hello.com", name="some1")
contact2 = ac1.create_contact("some2@hello.com", name="some2")
chat = ac1.create_group_chat(name="title1")
chat.add_contact(contact1)
chat.add_contact(contact2)
assert chat.get_name() == "title1"
assert contact1 in chat.get_contacts()
assert contact2 in chat.get_contacts()
assert not chat.is_promoted()
msg = chat.get_draft()
assert msg.text == "xyz title1"
@pytest.mark.parametrize("verified", [True, False]) @pytest.mark.parametrize("verified", [True, False])
def test_group_chat_qr(self, acfactory, ac1, verified): def test_group_chat_qr(self, acfactory, ac1, verified):
ac2 = acfactory.get_configured_offline_account() ac2 = acfactory.get_configured_offline_account()

View File

@@ -257,7 +257,7 @@ const DC_SHOW_EMAILS_ACCEPTED_CONTACTS: usize = 1;
const DC_SHOW_EMAILS_ALL: usize = 2; const DC_SHOW_EMAILS_ALL: usize = 2;
// TODO: Strings need some doumentation about used placeholders. // TODO: Strings need some doumentation about used placeholders.
// These constants are used to request strings using #DC_EVENT_GET_STRING. // These constants are used to set stock translation strings
const DC_STR_NOMESSAGES: usize = 1; const DC_STR_NOMESSAGES: usize = 1;
const DC_STR_SELF: usize = 2; const DC_STR_SELF: usize = 2;

View File

@@ -63,6 +63,7 @@ pub struct Context {
pub running_state: Arc<RwLock<RunningState>>, pub running_state: Arc<RwLock<RunningState>>,
/// Mutex to avoid generating the key for the user more than once. /// Mutex to avoid generating the key for the user more than once.
pub generating_key_mutex: Mutex<()>, pub generating_key_mutex: Mutex<()>,
pub translated_stockstrings: RwLock<HashMap<usize, String>>,
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
@@ -145,6 +146,7 @@ impl Context {
probe_imap_network: Arc::new(RwLock::new(false)), probe_imap_network: Arc::new(RwLock::new(false)),
perform_inbox_jobs_needed: Arc::new(RwLock::new(false)), perform_inbox_jobs_needed: Arc::new(RwLock::new(false)),
generating_key_mutex: Mutex::new(()), generating_key_mutex: Mutex::new(()),
translated_stockstrings: RwLock::new(HashMap::new()),
}; };
ensure!( ensure!(

View File

@@ -2,8 +2,6 @@ use std::path::PathBuf;
use strum::EnumProperty; use strum::EnumProperty;
use crate::stock::StockMessage;
impl Event { impl Event {
/// Returns the corresponding Event id. /// Returns the corresponding Event id.
pub fn as_id(&self) -> i32 { pub fn as_id(&self) -> i32 {
@@ -237,17 +235,4 @@ pub enum Event {
/// @return 0 /// @return 0
#[strum(props(id = "2061"))] #[strum(props(id = "2061"))]
SecurejoinJoinerProgress { contact_id: u32, progress: usize }, SecurejoinJoinerProgress { contact_id: u32, progress: usize },
// the following events are functions that should be provided by the frontends
/// Requeste a localized string from the frontend.
/// @param data1 (int) ID of the string to request, one of the DC_STR_/// constants.
/// @param data2 (int) The count. If the requested string contains a placeholder for a numeric value,
/// the ui may use this value to return different strings on different plural forms.
/// @return (const char*) Null-terminated UTF-8 string.
/// The string will be free()'d by the core,
/// so it must be allocated using malloc() or a compatible function.
/// Return 0 if the ui cannot provide the requested string
/// the core will use a default string in english language then.
#[strum(props(id = "2091"))]
GetString { id: StockMessage, count: usize },
} }

View File

@@ -805,19 +805,12 @@ mod tests {
assert!(msg.contains("-----END PGP MESSAGE-----\n")); assert!(msg.contains("-----END PGP MESSAGE-----\n"));
} }
fn ac_setup_msg_cb(ctx: &Context, evt: Event) -> libc::uintptr_t {
match evt {
Event::GetString {
id: StockMessage::AcSetupMsgBody,
..
} => unsafe { "hello\r\nthere".strdup() as usize },
_ => logging_cb(ctx, evt),
}
}
#[test] #[test]
fn otest_render_setup_file_newline_replace() { fn test_render_setup_file_newline_replace() {
let t = test_context(Some(Box::new(ac_setup_msg_cb))); let t = dummy_context();
t.ctx
.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
.unwrap();
configure_alice_keypair(&t.ctx); configure_alice_keypair(&t.ctx);
let msg = render_setup_file(&t.ctx, "pw").unwrap(); let msg = render_setup_file(&t.ctx, "pw").unwrap();
println!("{}", &msg); println!("{}", &msg);

View File

@@ -57,7 +57,7 @@ pub mod qr;
pub mod securejoin; pub mod securejoin;
mod smtp; mod smtp;
pub mod sql; pub mod sql;
mod stock; pub mod stock;
mod token; mod token;
#[macro_use] #[macro_use]
mod wrapmime; mod wrapmime;

View File

@@ -5,8 +5,7 @@ use strum_macros::EnumProperty;
use crate::contact::*; use crate::contact::*;
use crate::context::Context; use crate::context::Context;
use crate::dc_tools::*; use crate::error::Error;
use crate::events::Event;
/// Stock strings /// Stock strings
/// ///
@@ -123,19 +122,47 @@ impl StockMessage {
} }
impl Context { impl Context {
/// Set the stock string for the [StockMessage].
///
pub fn set_stock_translation(
&self,
id: StockMessage,
stockstring: String,
) -> Result<(), Error> {
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()
.unwrap()
.insert(id as usize, stockstring);
Ok(())
}
/// Return the stock string for the [StockMessage]. /// Return the stock string for the [StockMessage].
/// ///
/// If the context callback responds with a string to use, e.g. a /// Return a translation (if it was set with set_stock_translation before)
/// translation, then this string will be returned. Otherwise a /// or a default (English) string.
/// default (English) string is returned.
pub fn stock_str(&self, id: StockMessage) -> Cow<str> { pub fn stock_str(&self, id: StockMessage) -> Cow<str> {
let ptr = self.call_cb(Event::GetString { id, count: 0 }) as *mut libc::c_char; match self
if ptr.is_null() { .translated_stockstrings
Cow::Borrowed(id.fallback()) .read()
} else { .unwrap()
let ret = to_string_lossy(ptr); .get(&(id as usize))
unsafe { libc::free(ptr as *mut libc::c_void) }; {
Cow::Owned(ret) Some(ref x) => Cow::Owned(x.to_string()),
None => Cow::Borrowed(id.fallback()),
} }
} }
@@ -239,7 +266,6 @@ mod tests {
use crate::test_utils::*; use crate::test_utils::*;
use crate::constants::DC_CONTACT_ID_SELF; use crate::constants::DC_CONTACT_ID_SELF;
use libc::uintptr_t;
use num_traits::ToPrimitive; use num_traits::ToPrimitive;
@@ -255,25 +281,31 @@ mod tests {
} }
#[test] #[test]
fn test_stock_str() { fn test_set_stock_translation() {
let t = dummy_context(); let t = dummy_context();
assert_eq!(t.ctx.stock_str(StockMessage::NoMessages), "No messages."); t.ctx
} .set_stock_translation(StockMessage::NoMessages, "xyz".to_string())
.unwrap();
fn test_stock_str_no_fallback_cb(_ctx: &Context, evt: Event) -> uintptr_t { assert_eq!(t.ctx.stock_str(StockMessage::NoMessages), "xyz")
match evt {
Event::GetString {
id: StockMessage::NoMessages,
..
} => unsafe { "Hello there".strdup() as usize },
_ => 0,
}
} }
#[test] #[test]
fn test_stock_str_no_fallback() { fn test_set_stock_translation_wrong_replacements() {
let t = test_context(Some(Box::new(test_stock_str_no_fallback_cb))); let t = dummy_context();
assert_eq!(t.ctx.stock_str(StockMessage::NoMessages), "Hello there"); assert!(t
.ctx
.set_stock_translation(StockMessage::NoMessages, "xyz %1$s ".to_string())
.is_err());
assert!(t
.ctx
.set_stock_translation(StockMessage::NoMessages, "xyz %2$s ".to_string())
.is_err());
}
#[test]
fn test_stock_str() {
let t = dummy_context();
assert_eq!(t.ctx.stock_str(StockMessage::NoMessages), "No messages.");
} }
#[test] #[test]