1use crate::SharedString;
5use core::fmt::Display;
6pub use formatter::FormatArgs;
7
8mod formatter {
9 use core::fmt::{Display, Formatter, Result};
10
11 pub trait FormatArgs {
12 type Output<'a>: Display
13 where
14 Self: 'a;
15 #[allow(clippy::wrong_self_convention)]
16 fn from_index(&self, index: usize) -> Option<Self::Output<'_>>;
17 #[allow(clippy::wrong_self_convention)]
18 fn from_name(&self, _name: &str) -> Option<Self::Output<'_>> {
19 None
20 }
21 }
22
23 impl<T: Display> FormatArgs for [T] {
24 type Output<'a>
25 = &'a T
26 where
27 T: 'a;
28 fn from_index(&self, index: usize) -> Option<&T> {
29 self.get(index)
30 }
31 }
32
33 impl<const N: usize, T: Display> FormatArgs for [T; N] {
34 type Output<'a>
35 = &'a T
36 where
37 T: 'a;
38 fn from_index(&self, index: usize) -> Option<&T> {
39 self.get(index)
40 }
41 }
42
43 pub fn format<'a>(
44 format_str: &'a str,
45 args: &'a (impl FormatArgs + ?Sized),
46 ) -> impl Display + 'a {
47 FormatResult { format_str, args }
48 }
49
50 struct FormatResult<'a, T: ?Sized> {
51 format_str: &'a str,
52 args: &'a T,
53 }
54
55 impl<T: FormatArgs + ?Sized> Display for FormatResult<'_, T> {
56 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
57 let mut arg_idx = 0;
58 let mut pos = 0;
59 while let Some(mut p) = self.format_str[pos..].find(['{', '}']) {
60 if self.format_str.len() - pos < p + 1 {
61 break;
62 }
63 p += pos;
64
65 if self.format_str.get(p..=p) == Some("}") {
67 self.format_str[pos..=p].fmt(f)?;
68 if self.format_str.get(p + 1..=p + 1) == Some("}") {
69 pos = p + 2;
70 } else {
71 pos = p + 1;
73 }
74 continue;
75 }
76
77 if self.format_str.get(p + 1..=p + 1) == Some("{") {
79 self.format_str[pos..=p].fmt(f)?;
80 pos = p + 2;
81 continue;
82 }
83
84 let end = if let Some(end) = self.format_str[p..].find('}') {
86 end + p
87 } else {
88 self.format_str[pos..=p].fmt(f)?;
90 pos = p + 1;
91 continue;
92 };
93 let argument = self.format_str[p + 1..end].trim();
94 let pa = if p == end - 1 {
95 arg_idx += 1;
96 self.args.from_index(arg_idx - 1)
97 } else if let Ok(n) = argument.parse::<usize>() {
98 self.args.from_index(n)
99 } else {
100 self.args.from_name(argument)
101 };
102
103 self.format_str[pos..p].fmt(f)?;
105 if let Some(a) = pa {
106 a.fmt(f)?;
107 } else {
108 self.format_str[p..=end].fmt(f)?;
110 }
111 pos = end + 1;
112 }
113 self.format_str[pos..].fmt(f)
114 }
115 }
116
117 #[cfg(test)]
118 mod tests {
119 use super::format;
120 use core::fmt::Display;
121 use std::string::{String, ToString};
122 #[test]
123 fn test_format() {
124 assert_eq!(format("Hello", (&[]) as &[String]).to_string(), "Hello");
125 assert_eq!(format("Hello {}!", &["world"]).to_string(), "Hello world!");
126 assert_eq!(format("Hello {0}!", &["world"]).to_string(), "Hello world!");
127 assert_eq!(
128 format("Hello -{1}- -{0}-", &[&(40 + 5) as &dyn Display, &"World"]).to_string(),
129 "Hello -World- -45-"
130 );
131 assert_eq!(
132 format(
133 format("Hello {{}}!", (&[]) as &[String]).to_string().as_str(),
134 &[format("{}", &["world"])]
135 )
136 .to_string(),
137 "Hello world!"
138 );
139 assert_eq!(
140 format("Hello -{}- -{}-", &[&(40 + 5) as &dyn Display, &"World"]).to_string(),
141 "Hello -45- -World-"
142 );
143 assert_eq!(format("Hello {{0}} {}", &["world"]).to_string(), "Hello {0} world");
144 }
145 }
146}
147
148struct WithPlural<'a, T: ?Sized>(&'a T, i32);
149
150enum DisplayOrInt<T> {
151 Display(T),
152 Int(i32),
153}
154impl<T: Display> Display for DisplayOrInt<T> {
155 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
156 match self {
157 DisplayOrInt::Display(d) => d.fmt(f),
158 DisplayOrInt::Int(i) => i.fmt(f),
159 }
160 }
161}
162
163impl<T: FormatArgs + ?Sized> FormatArgs for WithPlural<'_, T> {
164 type Output<'b>
165 = DisplayOrInt<T::Output<'b>>
166 where
167 Self: 'b;
168
169 fn from_index(&self, index: usize) -> Option<Self::Output<'_>> {
170 self.0.from_index(index).map(DisplayOrInt::Display)
171 }
172
173 fn from_name<'b>(&'b self, name: &str) -> Option<Self::Output<'b>> {
174 if name == "n" {
175 Some(DisplayOrInt::Int(self.1))
176 } else {
177 self.0.from_name(name).map(DisplayOrInt::Display)
178 }
179 }
180}
181
182pub fn translate(
184 original: &str,
185 contextid: &str,
186 domain: &str,
187 arguments: &(impl FormatArgs + ?Sized),
188 n: i32,
189 plural: &str,
190) -> SharedString {
191 #![allow(unused)]
192 let mut output = SharedString::default();
193 let translated = if plural.is_empty() || n == 1 { original } else { plural };
194 #[cfg(all(target_family = "unix", feature = "gettext-rs"))]
195 let translated = translate_gettext(original, contextid, domain, n, plural);
196 use core::fmt::Write;
197 write!(output, "{}", formatter::format(&translated, &WithPlural(arguments, n))).unwrap();
198 output
199}
200
201#[cfg(all(target_family = "unix", feature = "gettext-rs"))]
202fn translate_gettext(
203 string: &str,
204 ctx: &str,
205 domain: &str,
206 n: i32,
207 plural: &str,
208) -> std::string::String {
209 use std::string::String;
210 global_translation_property();
211 fn mangle_context(ctx: &str, s: &str) -> String {
212 std::format!("{ctx}\u{4}{s}")
213 }
214 fn demangle_context(r: String) -> String {
215 if let Some(x) = r.split('\u{4}').last() {
216 return x.into();
217 }
218 r
219 }
220
221 if plural.is_empty() {
222 if !ctx.is_empty() {
223 demangle_context(gettextrs::dgettext(domain, mangle_context(ctx, string)))
224 } else {
225 gettextrs::dgettext(domain, string)
226 }
227 } else if !ctx.is_empty() {
228 demangle_context(gettextrs::dngettext(
229 domain,
230 mangle_context(ctx, string),
231 mangle_context(ctx, plural),
232 n as u32,
233 ))
234 } else {
235 gettextrs::dngettext(domain, string, plural, n as u32)
236 }
237}
238
239fn global_translation_property() -> usize {
241 crate::context::GLOBAL_CONTEXT.with(|ctx| {
242 let Some(ctx) = ctx.get() else { return 0 };
243 ctx.0.translations_dirty.as_ref().get()
244 })
245}
246
247pub fn mark_all_translations_dirty() {
248 #[cfg(all(feature = "gettext-rs", target_family = "unix"))]
249 {
250 #[allow(unsafe_code)]
254 unsafe {
255 extern "C" {
256 static mut _nl_msg_cat_cntr: std::ffi::c_int;
257 }
258 _nl_msg_cat_cntr += 1;
259 }
260 }
261
262 crate::context::GLOBAL_CONTEXT.with(|ctx| {
263 let Some(ctx) = ctx.get() else { return };
264 ctx.0.translations_dirty.mark_dirty();
265 })
266}
267
268#[cfg(feature = "gettext-rs")]
269pub fn gettext_bindtextdomain(_domain: &str, _dirname: std::path::PathBuf) -> std::io::Result<()> {
271 #[cfg(target_family = "unix")]
272 {
273 gettextrs::bindtextdomain(_domain, _dirname)?;
274 static START: std::sync::Once = std::sync::Once::new();
275 START.call_once(|| {
276 gettextrs::setlocale(gettextrs::LocaleCategory::LcAll, "");
277 });
278 mark_all_translations_dirty();
279 }
280 Ok(())
281}
282
283pub fn translate_from_bundle(
284 strs: &[Option<&str>],
285 arguments: &(impl FormatArgs + ?Sized),
286) -> SharedString {
287 let idx = global_translation_property();
288 let mut output = SharedString::default();
289 let Some(translated) = strs.get(idx).and_then(|x| *x).or_else(|| strs.first().and_then(|x| *x))
290 else {
291 return output;
292 };
293 use core::fmt::Write;
294 write!(output, "{}", formatter::format(translated, arguments)).unwrap();
295 output
296}
297
298pub fn translate_from_bundle_with_plural(
299 strs: &[Option<&[&str]>],
300 plural_rules: &[Option<fn(i32) -> usize>],
301 arguments: &(impl FormatArgs + ?Sized),
302 n: i32,
303) -> SharedString {
304 let idx = global_translation_property();
305 let mut output = SharedString::default();
306 let en = |n| (n != 1) as usize;
307 let (translations, rule) = match strs.get(idx) {
308 Some(Some(x)) => (x, plural_rules.get(idx).and_then(|x| *x).unwrap_or(en)),
309 _ => match strs.first() {
310 Some(Some(x)) => (x, plural_rules.first().and_then(|x| *x).unwrap_or(en)),
311 _ => return output,
312 },
313 };
314 let Some(translated) = translations.get(rule(n)).or_else(|| translations.first()).cloned()
315 else {
316 return output;
317 };
318 use core::fmt::Write;
319 write!(output, "{}", formatter::format(translated, &WithPlural(arguments, n))).unwrap();
320 output
321}
322
323pub fn set_bundled_languages(languages: &[&'static str]) {
326 crate::context::GLOBAL_CONTEXT.with(|ctx| {
327 let Some(ctx) = ctx.get() else { return };
328 if ctx.0.translations_bundle_languages.borrow().is_none() {
329 ctx.0.translations_bundle_languages.replace(Some(languages.to_vec()));
330 #[cfg(feature = "std")]
331 if let Some(idx) = index_for_locale(languages) {
332 ctx.0.translations_dirty.as_ref().set(idx);
333 }
334 }
335 });
336}
337
338#[cfg(feature = "std")]
340fn index_for_locale(languages: &[&'static str]) -> Option<usize> {
341 let locale = sys_locale::get_locale()?;
342 let idx = languages.iter().position(|x| *x == locale);
344 fn base(l: &str) -> &str {
346 l.find(['-', '_', '@']).map_or(l, |i| &l[..i])
347 }
348 idx.or_else(|| {
349 let locale = base(&locale);
350 languages.iter().position(|x| base(x) == locale)
351 })
352}
353
354#[i_slint_core_macros::slint_doc]
355pub fn select_bundled_translation(language: &str) -> Result<(), SelectBundledTranslationError> {
367 crate::context::GLOBAL_CONTEXT.with(|ctx| {
368 let Some(ctx) = ctx.get() else {
369 return Err(SelectBundledTranslationError::NoTranslationsBundled);
370 };
371 let languages = ctx.0.translations_bundle_languages.borrow();
372 let Some(languages) = &*languages else {
373 return Err(SelectBundledTranslationError::NoTranslationsBundled);
374 };
375 let idx = languages.iter().position(|x| *x == language);
376 if let Some(idx) = idx {
377 ctx.0.translations_dirty.as_ref().set(idx);
378 Ok(())
379 } else if language.is_empty() || language == "en" {
380 ctx.0.translations_dirty.as_ref().set(0);
381 Ok(())
382 } else {
383 Err(SelectBundledTranslationError::LanguageNotFound {
384 available_languages: languages.iter().map(|x| (*x).into()).collect(),
385 })
386 }
387 })
388}
389
390#[derive(Debug)]
392pub enum SelectBundledTranslationError {
393 LanguageNotFound { available_languages: crate::SharedVector<SharedString> },
395 NoTranslationsBundled,
398}
399
400impl core::fmt::Display for SelectBundledTranslationError {
401 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
402 match self {
403 SelectBundledTranslationError::LanguageNotFound { available_languages } => {
404 write!(f, "The specified language was not found. Available languages are: {available_languages:?}")
405 }
406 SelectBundledTranslationError::NoTranslationsBundled => {
407 write!(f, "There are no bundled translations. Either select_bundled_translation was called before creating a component, or the application's `.slint` file was compiled without the bundle translation option")
408 }
409 }
410 }
411}
412
413#[cfg(feature = "std")]
414impl std::error::Error for SelectBundledTranslationError {}
415
416#[cfg(feature = "ffi")]
417mod ffi {
418 #![allow(unsafe_code)]
419 use super::*;
420 use crate::slice::Slice;
421
422 #[unsafe(no_mangle)]
424 pub extern "C" fn slint_translate(
425 to_translate: &mut SharedString,
426 context: &SharedString,
427 domain: &SharedString,
428 arguments: Slice<SharedString>,
429 n: i32,
430 plural: &SharedString,
431 ) {
432 *to_translate =
433 translate(to_translate.as_str(), context, domain, arguments.as_slice(), n, plural)
434 }
435
436 #[unsafe(no_mangle)]
438 pub extern "C" fn slint_translations_mark_dirty() {
439 mark_all_translations_dirty();
440 }
441
442 #[unsafe(no_mangle)]
444 pub unsafe extern "C" fn slint_translate_from_bundle(
445 strs: Slice<*const core::ffi::c_char>,
446 arguments: Slice<SharedString>,
447 output: &mut SharedString,
448 ) {
449 *output = SharedString::default();
450 let idx = global_translation_property();
451 let Some(translated) = strs
452 .get(idx)
453 .filter(|x| !x.is_null())
454 .or_else(|| strs.first())
455 .map(|x| core::ffi::CStr::from_ptr(*x).to_str().unwrap())
456 else {
457 return;
458 };
459 use core::fmt::Write;
460 write!(output, "{}", formatter::format(translated, arguments.as_slice())).unwrap();
461 }
462 #[unsafe(no_mangle)]
469 pub unsafe extern "C" fn slint_translate_from_bundle_with_plural(
470 strs: Slice<*const core::ffi::c_char>,
471 indices: Slice<u32>,
472 plural_rules: Slice<Option<fn(i32) -> usize>>,
473 arguments: Slice<SharedString>,
474 n: i32,
475 output: &mut SharedString,
476 ) {
477 *output = SharedString::default();
478 let idx = global_translation_property();
479 let en = |n| (n != 1) as usize;
480 let begin = *indices.get(idx.wrapping_sub(1)).unwrap_or(&0);
481 let (translations, rule) = match indices.get(idx) {
482 Some(end) if *end != begin => (
483 &strs.as_slice()[begin as usize..*end as usize],
484 plural_rules.get(idx).and_then(|x| *x).unwrap_or(en),
485 ),
486 _ => (
487 &strs.as_slice()[..*indices.first().unwrap_or(&0) as usize],
488 plural_rules.first().and_then(|x| *x).unwrap_or(en),
489 ),
490 };
491 let Some(translated) = translations
492 .get(rule(n))
493 .or_else(|| translations.first())
494 .map(|x| core::ffi::CStr::from_ptr(*x).to_str().unwrap())
495 else {
496 return;
497 };
498 use core::fmt::Write;
499 write!(output, "{}", formatter::format(translated, &WithPlural(arguments.as_slice(), n)))
500 .unwrap();
501 }
502
503 #[unsafe(no_mangle)]
504 pub extern "C" fn slint_translate_set_bundled_languages(languages: Slice<Slice<'static, u8>>) {
505 let languages = languages
506 .iter()
507 .map(|x| core::str::from_utf8(x.as_slice()).unwrap())
508 .collect::<alloc::vec::Vec<_>>();
509 set_bundled_languages(&languages);
510 }
511
512 #[unsafe(no_mangle)]
513 pub extern "C" fn slint_translate_select_bundled_translation(language: Slice<u8>) -> bool {
514 let language = core::str::from_utf8(&language).unwrap();
515 select_bundled_translation(language).is_ok()
516 }
517}