1use crate::SharedString;
6use core::fmt::Display;
7pub use formatter::FormatArgs;
8use i_slint_common::TranslationsBundled;
9#[cfg(feature = "tr")]
10pub use tr::Translator;
11
12mod formatter {
13 use core::fmt::{Display, Formatter, Result};
14
15 pub trait FormatArgs {
16 type Output<'a>: Display
17 where
18 Self: 'a;
19 #[allow(clippy::wrong_self_convention)]
20 fn from_index(&self, index: usize) -> Option<Self::Output<'_>>;
21 #[allow(clippy::wrong_self_convention)]
22 fn from_name(&self, _name: &str) -> Option<Self::Output<'_>> {
23 None
24 }
25 }
26
27 impl<T: Display> FormatArgs for [T] {
28 type Output<'a>
29 = &'a T
30 where
31 T: 'a;
32 fn from_index(&self, index: usize) -> Option<&T> {
33 self.get(index)
34 }
35 }
36
37 impl<const N: usize, T: Display> FormatArgs for [T; N] {
38 type Output<'a>
39 = &'a T
40 where
41 T: 'a;
42 fn from_index(&self, index: usize) -> Option<&T> {
43 self.get(index)
44 }
45 }
46
47 pub fn format<'a>(
48 format_str: &'a str,
49 args: &'a (impl FormatArgs + ?Sized),
50 ) -> impl Display + 'a {
51 FormatResult { format_str, args }
52 }
53
54 struct FormatResult<'a, T: ?Sized> {
55 format_str: &'a str,
56 args: &'a T,
57 }
58
59 impl<T: FormatArgs + ?Sized> Display for FormatResult<'_, T> {
60 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
61 let mut arg_idx = 0;
62 let mut pos = 0;
63 while let Some(mut p) = self.format_str[pos..].find(['{', '}']) {
64 if self.format_str.len() - pos < p + 1 {
65 break;
66 }
67 p += pos;
68
69 if self.format_str.get(p..=p) == Some("}") {
71 self.format_str[pos..=p].fmt(f)?;
72 if self.format_str.get(p + 1..=p + 1) == Some("}") {
73 pos = p + 2;
74 } else {
75 pos = p + 1;
77 }
78 continue;
79 }
80
81 if self.format_str.get(p + 1..=p + 1) == Some("{") {
83 self.format_str[pos..=p].fmt(f)?;
84 pos = p + 2;
85 continue;
86 }
87
88 let end = if let Some(end) = self.format_str[p..].find('}') {
90 end + p
91 } else {
92 self.format_str[pos..=p].fmt(f)?;
94 pos = p + 1;
95 continue;
96 };
97 let argument = self.format_str[p + 1..end].trim();
98 let pa = if p == end - 1 {
99 arg_idx += 1;
100 self.args.from_index(arg_idx - 1)
101 } else if let Ok(n) = argument.parse::<usize>() {
102 self.args.from_index(n)
103 } else {
104 self.args.from_name(argument)
105 };
106
107 self.format_str[pos..p].fmt(f)?;
109 if let Some(a) = pa {
110 a.fmt(f)?;
111 } else {
112 self.format_str[p..=end].fmt(f)?;
114 }
115 pos = end + 1;
116 }
117 self.format_str[pos..].fmt(f)
118 }
119 }
120
121 #[cfg(test)]
122 mod tests {
123 use super::format;
124 use core::fmt::Display;
125 use std::string::{String, ToString};
126 #[test]
127 fn test_format() {
128 assert_eq!(format("Hello", (&[]) as &[String]).to_string(), "Hello");
129 assert_eq!(format("Hello {}!", &["world"]).to_string(), "Hello world!");
130 assert_eq!(format("Hello {0}!", &["world"]).to_string(), "Hello world!");
131 assert_eq!(
132 format("Hello -{1}- -{0}-", &[&(40 + 5) as &dyn Display, &"World"]).to_string(),
133 "Hello -World- -45-"
134 );
135 assert_eq!(
136 format(
137 format("Hello {{}}!", (&[]) as &[String]).to_string().as_str(),
138 &[format("{}", &["world"])]
139 )
140 .to_string(),
141 "Hello world!"
142 );
143 assert_eq!(
144 format("Hello -{}- -{}-", &[&(40 + 5) as &dyn Display, &"World"]).to_string(),
145 "Hello -45- -World-"
146 );
147 assert_eq!(format("Hello {{0}} {}", &["world"]).to_string(), "Hello {0} world");
148 }
149 }
150}
151
152struct WithPlural<'a, T: ?Sized>(&'a T, i32);
153
154enum DisplayOrInt<T> {
155 Display(T),
156 Int(i32),
157}
158impl<T: Display> Display for DisplayOrInt<T> {
159 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
160 match self {
161 DisplayOrInt::Display(d) => d.fmt(f),
162 DisplayOrInt::Int(i) => i.fmt(f),
163 }
164 }
165}
166
167impl<T: FormatArgs + ?Sized> FormatArgs for WithPlural<'_, T> {
168 type Output<'b>
169 = DisplayOrInt<T::Output<'b>>
170 where
171 Self: 'b;
172
173 fn from_index(&self, index: usize) -> Option<Self::Output<'_>> {
174 self.0.from_index(index).map(DisplayOrInt::Display)
175 }
176
177 fn from_name<'b>(&'b self, name: &str) -> Option<Self::Output<'b>> {
178 if name == "n" {
179 Some(DisplayOrInt::Int(self.1))
180 } else {
181 self.0.from_name(name).map(DisplayOrInt::Display)
182 }
183 }
184}
185
186pub fn translate(
188 original: &str,
189 contextid: &str,
190 domain: &str,
191 arguments: &(impl FormatArgs + ?Sized),
192 n: i32,
193 plural: &str,
194) -> SharedString {
195 #![allow(unused)]
196 let mut output = SharedString::default();
197
198 #[cfg(any(feature = "tr", all(target_family = "unix", feature = "gettext-rs")))]
201 global_translation_property();
202
203 let mut translated: Option<alloc::borrow::Cow<'_, str>> = None;
204
205 #[cfg(feature = "tr")]
206 {
207 translated = crate::context::GLOBAL_CONTEXT.with(|ctx| {
208 let ctx = ctx.get()?;
209 let external_translator = ctx.external_translator()?;
210 let context = if !contextid.is_empty() { Some(contextid) } else { None };
211 Some(
212 if plural.is_empty() {
213 external_translator.translate(original, context)
214 } else {
215 external_translator.ntranslate(n.try_into().ok()?, original, plural, context)
216 }
217 .into_owned()
218 .into(),
219 )
220 });
221 }
222
223 #[cfg(all(target_family = "unix", feature = "gettext-rs"))]
224 if translated.is_none() {
225 translated = Some(alloc::borrow::Cow::Owned(translate_gettext(
226 original, contextid, domain, n, plural,
227 )));
228 }
229
230 let translated = translated
231 .unwrap_or_else(|| if plural.is_empty() || n == 1 { original } else { plural }.into());
232
233 use core::fmt::Write;
234 write!(output, "{}", formatter::format(&translated, &WithPlural(arguments, n))).unwrap();
235 output
236}
237
238#[cfg(all(target_family = "unix", feature = "gettext-rs"))]
239fn translate_gettext(
240 string: &str,
241 ctx: &str,
242 domain: &str,
243 n: i32,
244 plural: &str,
245) -> std::string::String {
246 use std::string::String;
247 fn mangle_context(ctx: &str, s: &str) -> String {
248 std::format!("{ctx}\u{4}{s}")
249 }
250 fn demangle_context(r: String) -> String {
251 if let Some(x) = r.split('\u{4}').next_back() {
252 return x.into();
253 }
254 r
255 }
256
257 if plural.is_empty() {
258 if !ctx.is_empty() {
259 demangle_context(gettextrs::dgettext(domain, mangle_context(ctx, string)))
260 } else {
261 gettextrs::dgettext(domain, string)
262 }
263 } else if !ctx.is_empty() {
264 demangle_context(gettextrs::dngettext(
265 domain,
266 mangle_context(ctx, string),
267 mangle_context(ctx, plural),
268 n as u32,
269 ))
270 } else {
271 gettextrs::dngettext(domain, string, plural, n as u32)
272 }
273}
274
275fn global_translation_property() -> usize {
277 crate::context::GLOBAL_CONTEXT.with(|ctx| {
278 let Some(ctx) = ctx.get() else { return 0 };
279 ctx.0.as_ref().project_ref().translations_dirty.get()
280 })
281}
282
283pub fn mark_all_translations_dirty() {
284 #[cfg(all(feature = "gettext-rs", target_family = "unix"))]
285 {
286 #[allow(unsafe_code)]
290 unsafe {
291 unsafe extern "C" {
292 static mut _nl_msg_cat_cntr: std::ffi::c_int;
293 }
294 _nl_msg_cat_cntr += 1;
295 }
296 }
297
298 crate::context::GLOBAL_CONTEXT.with(|ctx| {
299 let Some(ctx) = ctx.get() else { return };
300 let pinned = ctx.0.as_ref().project_ref();
301 pinned.translations_dirty.mark_dirty();
302
303 #[cfg(all(feature = "gettext-rs", target_family = "unix"))]
305 if let Some(locale) = sys_locale::get_locale() {
306 pinned
307 .locale_decimal_separator
308 .set(i_slint_common::decimal_separator_for_locale(&locale))
309 }
310 })
311}
312
313#[cfg(feature = "gettext-rs")]
314pub fn gettext_bindtextdomain(_domain: &str, _dirname: std::path::PathBuf) -> std::io::Result<()> {
316 #[cfg(target_family = "unix")]
317 {
318 gettextrs::bindtextdomain(_domain, _dirname)?;
319 static START: std::sync::Once = std::sync::Once::new();
320 START.call_once(|| {
321 gettextrs::setlocale(gettextrs::LocaleCategory::LcAll, "");
322 });
323
324 mark_all_translations_dirty();
325 }
326 Ok(())
327}
328
329pub fn translate_from_bundle(
334 strs: &[Option<&str>],
335 arguments: &(impl FormatArgs + ?Sized),
336) -> SharedString {
337 let idx = global_translation_property();
338 let mut output = SharedString::default();
339 let Some(translated) = strs.get(idx).and_then(|x| *x).or_else(|| strs.first().and_then(|x| *x))
340 else {
341 return output;
342 };
343 use core::fmt::Write;
344 write!(output, "{}", formatter::format(translated, arguments)).unwrap();
345 output
346}
347
348pub fn translate_from_bundle_with_plural(
354 strs: &[Option<&[&str]>],
355 plural_rules: &[Option<fn(i32) -> usize>],
356 arguments: &(impl FormatArgs + ?Sized),
357 n: i32,
358) -> SharedString {
359 let idx = global_translation_property();
360 let mut output = SharedString::default();
361 let en = |n| (n != 1) as usize;
362 let (translations, rule) = match strs.get(idx) {
363 Some(Some(x)) => (x, plural_rules.get(idx).and_then(|x| *x).unwrap_or(en)),
364 _ => match strs.first() {
365 Some(Some(x)) => (x, plural_rules.first().and_then(|x| *x).unwrap_or(en)),
366 _ => return output,
367 },
368 };
369 let Some(translated) = translations.get(rule(n)).or_else(|| translations.first()).cloned()
370 else {
371 return output;
372 };
373 use core::fmt::Write;
374 write!(output, "{}", formatter::format(translated, &WithPlural(arguments, n))).unwrap();
375 output
376}
377
378pub fn set_bundled_languages(translations: &[TranslationsBundled]) {
383 crate::context::GLOBAL_CONTEXT.with(|ctx| {
384 let Some(ctx) = ctx.get() else { return };
385
386 if ctx.0.translations_bundle.borrow().is_none() {
387 ctx.0.translations_bundle.replace(Some(translations.to_vec()));
388 #[cfg(feature = "std")]
389 if let Some(idx) = language_index_from_sys_locale(translations) {
390 ctx.0.as_ref().project_ref().translations_dirty.set(idx);
391 }
392 }
393 });
394}
395
396#[cfg(feature = "std")]
398fn language_index_from_sys_locale(languages: &[TranslationsBundled]) -> Option<usize> {
399 let locale = sys_locale::get_locale()?;
400 let idx = languages.iter().position(|x| *x.language == locale);
402 fn base(l: &str) -> &str {
404 l.find(['-', '_', '@']).map_or(l, |i| &l[..i])
405 }
406 idx.or_else(|| {
407 let locale = base(&locale);
408 languages.iter().position(|x| base(x.language) == locale)
409 })
410}
411
412#[i_slint_core_macros::slint_doc]
413pub fn select_bundled_translation(language: &str) -> Result<(), SelectBundledTranslationError> {
425 crate::context::GLOBAL_CONTEXT.with(|ctx| {
426 let Some(ctx) = ctx.get() else {
427 return Err(SelectBundledTranslationError::NoTranslationsBundled);
428 };
429 let translations = ctx.0.translations_bundle.borrow();
430 let Some(translations) = &*translations else {
431 return Err(SelectBundledTranslationError::NoTranslationsBundled);
432 };
433 let pinned = ctx.0.as_ref().project_ref();
434 if let Some((idx, translation_bundle)) =
435 translations.iter().enumerate().find(|(_i, x)| x.language == language)
436 {
437 pinned.translations_dirty.as_ref().set(idx);
438 pinned.locale_decimal_separator.as_ref().set(translation_bundle.decimal_separator);
440 Ok(())
441 } else if language.is_empty() || language == "en" {
442 pinned.translations_dirty.as_ref().set(0);
443 pinned.locale_decimal_separator.as_ref().set(i_slint_common::DEFAULT_DECIMAL_SEPARATOR);
444 Ok(())
445 } else {
446 Err(SelectBundledTranslationError::LanguageNotFound {
447 available_languages: translations.iter().map(|x| (*x.language).into()).collect(),
448 })
449 }
450 })
451}
452
453#[derive(Debug)]
455pub enum SelectBundledTranslationError {
456 LanguageNotFound { available_languages: crate::SharedVector<SharedString> },
458 NoTranslationsBundled,
461}
462
463impl core::fmt::Display for SelectBundledTranslationError {
464 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
465 match self {
466 SelectBundledTranslationError::LanguageNotFound { available_languages } => {
467 write!(
468 f,
469 "The specified language was not found. Available languages are: {available_languages:?}"
470 )
471 }
472 SelectBundledTranslationError::NoTranslationsBundled => {
473 write!(
474 f,
475 "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"
476 )
477 }
478 }
479 }
480}
481
482#[cfg(feature = "std")]
483impl std::error::Error for SelectBundledTranslationError {}
484
485#[cfg(feature = "ffi")]
486mod ffi {
487 #![allow(unsafe_code)]
488 use super::*;
489 use crate::slice::Slice;
490
491 #[unsafe(no_mangle)]
493 pub extern "C" fn slint_decimal_separator(out: &mut SharedString) {
494 crate::context::GLOBAL_CONTEXT.with(|ctx| {
495 let separator = if let Some(ctx) = ctx.get() {
496 ctx.0.as_ref().project_ref().locale_decimal_separator.get()
497 } else {
498 i_slint_common::DEFAULT_DECIMAL_SEPARATOR
499 };
500 *out = crate::SharedString::from(separator)
501 })
502 }
503
504 #[unsafe(no_mangle)]
506 pub extern "C" fn slint_translate(
507 to_translate: &mut SharedString,
508 context: &SharedString,
509 domain: &SharedString,
510 arguments: Slice<SharedString>,
511 n: i32,
512 plural: &SharedString,
513 ) {
514 *to_translate =
515 translate(to_translate.as_str(), context, domain, arguments.as_slice(), n, plural)
516 }
517
518 #[unsafe(no_mangle)]
520 pub extern "C" fn slint_translations_mark_dirty() {
521 mark_all_translations_dirty();
522 }
523
524 #[unsafe(no_mangle)]
526 pub unsafe extern "C" fn slint_translate_from_bundle(
527 strs: Slice<*const core::ffi::c_char>,
528 arguments: Slice<SharedString>,
529 output: &mut SharedString,
530 ) {
531 *output = SharedString::default();
532 let idx = global_translation_property();
533 let Some(translated) = strs
534 .get(idx)
535 .filter(|x| !x.is_null())
536 .or_else(|| strs.first())
537 .map(|x| unsafe { core::ffi::CStr::from_ptr(*x) }.to_str().unwrap())
538 else {
539 return;
540 };
541 use core::fmt::Write;
542 write!(output, "{}", formatter::format(translated, arguments.as_slice())).unwrap();
543 }
544 #[unsafe(no_mangle)]
551 pub unsafe extern "C" fn slint_translate_from_bundle_with_plural(
552 strs: Slice<*const core::ffi::c_char>,
553 indices: Slice<u32>,
554 plural_rules: Slice<Option<fn(i32) -> usize>>,
555 arguments: Slice<SharedString>,
556 n: i32,
557 output: &mut SharedString,
558 ) {
559 *output = SharedString::default();
560 let idx = global_translation_property();
561 let en = |n| (n != 1) as usize;
562 let begin = *indices.get(idx.wrapping_sub(1)).unwrap_or(&0);
563 let (translations, rule) = match indices.get(idx) {
564 Some(end) if *end != begin => (
565 &strs.as_slice()[begin as usize..*end as usize],
566 plural_rules.get(idx).and_then(|x| *x).unwrap_or(en),
567 ),
568 _ => (
569 &strs.as_slice()[..*indices.first().unwrap_or(&0) as usize],
570 plural_rules.first().and_then(|x| *x).unwrap_or(en),
571 ),
572 };
573 let Some(translated) = translations
574 .get(rule(n))
575 .or_else(|| translations.first())
576 .map(|x| unsafe { core::ffi::CStr::from_ptr(*x) }.to_str().unwrap())
577 else {
578 return;
579 };
580 use core::fmt::Write;
581 write!(output, "{}", formatter::format(translated, &WithPlural(arguments.as_slice(), n)))
582 .unwrap();
583 }
584
585 #[unsafe(no_mangle)]
586 pub extern "C" fn slint_translate_set_bundled_languages(
587 languages: Slice<Slice<'static, u8>>,
588 separators: Slice<u32>,
589 ) {
590 let translations = languages
591 .iter()
592 .zip(separators.as_slice().iter())
593 .map(|(language, separator)| TranslationsBundled {
594 language: core::str::from_utf8(language.as_slice()).unwrap(),
595 decimal_separator: core::char::from_u32(*separator)
596 .unwrap_or(i_slint_common::DEFAULT_DECIMAL_SEPARATOR),
597 })
598 .collect::<alloc::vec::Vec<_>>();
599 set_bundled_languages(&translations);
600 }
601
602 #[unsafe(no_mangle)]
603 pub extern "C" fn slint_translate_select_bundled_translation(language: Slice<u8>) -> bool {
604 let language = core::str::from_utf8(&language).unwrap();
605 select_bundled_translation(language).is_ok()
606 }
607}