// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 use crate::SharedString; use core::fmt::Display; pub use formatter::FormatArgs; mod formatter { use core::fmt::{Display, Formatter, Result}; pub trait FormatArgs { type Output<'a>: Display where Self: 'a; #[allow(clippy::wrong_self_convention)] fn from_index(&self, index: usize) -> Option>; #[allow(clippy::wrong_self_convention)] fn from_name(&self, _name: &str) -> Option> { None } } impl FormatArgs for [T] { type Output<'a> = &'a T where T: 'a; fn from_index(&self, index: usize) -> Option<&T> { self.get(index) } } impl FormatArgs for [T; N] { type Output<'a> = &'a T where T: 'a; fn from_index(&self, index: usize) -> Option<&T> { self.get(index) } } pub fn format<'a>( format_str: &'a str, args: &'a (impl FormatArgs + ?Sized), ) -> impl Display + 'a { FormatResult { format_str, args } } struct FormatResult<'a, T: ?Sized> { format_str: &'a str, args: &'a T, } impl<'a, T: FormatArgs + ?Sized> Display for FormatResult<'a, T> { fn fmt(&self, f: &mut Formatter<'_>) -> Result { let mut arg_idx = 0; let mut pos = 0; while let Some(mut p) = self.format_str[pos..].find(['{', '}']) { if self.format_str.len() - pos < p + 1 { break; } p += pos; // Skip escaped } if self.format_str.get(p..=p) == Some("}") { self.format_str[pos..=p].fmt(f)?; if self.format_str.get(p + 1..=p + 1) == Some("}") { pos = p + 2; } else { // FIXME! this is an error, it should be reported ('}' must be escaped) pos = p + 1; } continue; } // Skip escaped { if self.format_str.get(p + 1..=p + 1) == Some("{") { self.format_str[pos..=p].fmt(f)?; pos = p + 2; continue; } // Find the argument let end = if let Some(end) = self.format_str[p..].find('}') { end + p } else { // FIXME! this is an error, it should be reported self.format_str[pos..=p].fmt(f)?; pos = p + 1; continue; }; let argument = self.format_str[p + 1..end].trim(); let pa = if p == end - 1 { arg_idx += 1; self.args.from_index(arg_idx - 1) } else if let Ok(n) = argument.parse::() { self.args.from_index(n) } else { self.args.from_name(argument) }; // format the part before the '{' self.format_str[pos..p].fmt(f)?; if let Some(a) = pa { a.fmt(f)?; } else { // FIXME! this is an error, it should be reported self.format_str[p..=end].fmt(f)?; } pos = end + 1; } self.format_str[pos..].fmt(f) } } #[cfg(test)] mod tests { use super::format; use core::fmt::Display; #[test] fn test_format() { assert_eq!(format("Hello", (&[]) as &[String]).to_string(), "Hello"); assert_eq!(format("Hello {}!", &["world"]).to_string(), "Hello world!"); assert_eq!(format("Hello {0}!", &["world"]).to_string(), "Hello world!"); assert_eq!( format("Hello -{1}- -{0}-", &[&(40 + 5) as &dyn Display, &"World"]).to_string(), "Hello -World- -45-" ); assert_eq!( format( format("Hello {{}}!", (&[]) as &[String]).to_string().as_str(), &[format("{}", &["world"])] ) .to_string(), "Hello world!" ); assert_eq!( format("Hello -{}- -{}-", &[&(40 + 5) as &dyn Display, &"World"]).to_string(), "Hello -45- -World-" ); assert_eq!(format("Hello {{0}} {}", &["world"]).to_string(), "Hello {0} world"); } } } struct WithPlural<'a, T: ?Sized>(&'a T, i32); enum DisplayOrInt { Display(T), Int(i32), } impl Display for DisplayOrInt { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { DisplayOrInt::Display(d) => d.fmt(f), DisplayOrInt::Int(i) => i.fmt(f), } } } impl<'a, T: FormatArgs + ?Sized> FormatArgs for WithPlural<'a, T> { type Output<'b> = DisplayOrInt> where Self: 'b; fn from_index(&self, index: usize) -> Option> { self.0.from_index(index).map(DisplayOrInt::Display) } fn from_name<'b>(&'b self, name: &str) -> Option> { if name == "n" { Some(DisplayOrInt::Int(self.1)) } else { self.0.from_name(name).map(DisplayOrInt::Display) } } } /// Do the translation and formatting pub fn translate( original: &str, contextid: &str, domain: &str, arguments: &(impl FormatArgs + ?Sized), n: i32, plural: &str, ) -> SharedString { #![allow(unused)] let mut output = SharedString::default(); let translated = if plural.is_empty() || n == 1 { original } else { plural }; #[cfg(all(target_family = "unix", feature = "gettext-rs"))] let translated = translate_gettext(original, contextid, domain, n, plural); use core::fmt::Write; write!(output, "{}", formatter::format(&translated, &WithPlural(arguments, n))).unwrap(); output } #[cfg(all(target_family = "unix", feature = "gettext-rs"))] fn translate_gettext(string: &str, ctx: &str, domain: &str, n: i32, plural: &str) -> String { crate::context::GLOBAL_CONTEXT.with(|ctx| { let Some(ctx) = ctx.get() else { return }; ctx.0.translations_dirty.as_ref().get(); }); fn mangle_context(ctx: &str, s: &str) -> String { format!("{}\u{4}{}", ctx, s) } fn demangle_context(r: String) -> String { if let Some(x) = r.split('\u{4}').last() { return x.to_owned(); } r } if plural.is_empty() { if !ctx.is_empty() { demangle_context(gettextrs::dgettext(domain, mangle_context(ctx, string))) } else { gettextrs::dgettext(domain, string) } } else if !ctx.is_empty() { demangle_context(gettextrs::dngettext( domain, mangle_context(ctx, string), mangle_context(ctx, plural), n as u32, )) } else { gettextrs::dngettext(domain, string, plural, n as u32) } } pub fn mark_all_translations_dirty() { #[cfg(all(feature = "gettext-rs", target_family = "unix"))] { // SAFETY: This trick from https://www.gnu.org/software/gettext/manual/html_node/gettext-grok.html // is merely incrementing a generational counter that will invalidate gettext's internal cache for translations. // If in the worst case it won't invalidate, then old translations are shown. #[allow(unsafe_code)] unsafe { extern "C" { static mut _nl_msg_cat_cntr: std::ffi::c_int; } _nl_msg_cat_cntr += 1; } } crate::context::GLOBAL_CONTEXT.with(|ctx| { let Some(ctx) = ctx.get() else { return }; ctx.0.translations_dirty.mark_dirty(); }) } #[cfg(feature = "gettext-rs")] /// Initialize the translation by calling the [`bindtextdomain`](https://man7.org/linux/man-pages/man3/bindtextdomain.3.html) function from gettext pub fn gettext_bindtextdomain(_domain: &str, _dirname: std::path::PathBuf) -> std::io::Result<()> { #[cfg(target_family = "unix")] { gettextrs::bindtextdomain(_domain, _dirname)?; static START: std::sync::Once = std::sync::Once::new(); START.call_once(|| { gettextrs::setlocale(gettextrs::LocaleCategory::LcAll, ""); }); mark_all_translations_dirty(); } Ok(()) } #[cfg(feature = "ffi")] mod ffi { #![allow(unsafe_code)] use super::*; use crate::slice::Slice; /// Perform the translation and formatting. #[no_mangle] pub extern "C" fn slint_translate( to_translate: &mut SharedString, context: &SharedString, domain: &SharedString, arguments: Slice, n: i32, plural: &SharedString, ) { *to_translate = translate(to_translate.as_str(), &context, &domain, arguments.as_slice(), n, &plural) } /// Mark all translated string as dirty to perform re-translation in case the language change #[no_mangle] pub extern "C" fn slint_translations_mark_dirty() { mark_all_translations_dirty(); } }