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", not(target_env = "musl")))]
287 {
288 #[allow(unsafe_code)]
292 unsafe {
293 unsafe extern "C" {
294 static mut _nl_msg_cat_cntr: std::ffi::c_int;
295 }
296 _nl_msg_cat_cntr += 1;
297 }
298 }
299
300 crate::context::GLOBAL_CONTEXT.with(|ctx| {
301 let Some(ctx) = ctx.get() else { return };
302 let pinned = ctx.0.as_ref().project_ref();
303 pinned.translations_dirty.mark_dirty();
304
305 #[cfg(all(feature = "gettext-rs", target_family = "unix"))]
307 if let Some(locale) = sys_locale::get_locale() {
308 pinned
309 .locale_decimal_separator
310 .set(i_slint_common::decimal_separator_for_locale(&locale))
311 }
312 })
313}
314
315#[cfg(feature = "gettext-rs")]
316pub fn gettext_bindtextdomain(_domain: &str, _dirname: std::path::PathBuf) -> std::io::Result<()> {
318 #[cfg(target_family = "unix")]
319 {
320 gettextrs::bindtextdomain(_domain, _dirname)?;
321 static START: std::sync::Once = std::sync::Once::new();
322 START.call_once(|| {
323 gettextrs::setlocale(gettextrs::LocaleCategory::LcAll, "");
324 });
325
326 mark_all_translations_dirty();
327 }
328 Ok(())
329}
330
331pub fn translate_from_bundle(
336 strs: &[Option<&str>],
337 arguments: &(impl FormatArgs + ?Sized),
338) -> SharedString {
339 let idx = global_translation_property();
340 let mut output = SharedString::default();
341 let Some(translated) = strs.get(idx).and_then(|x| *x).or_else(|| strs.first().and_then(|x| *x))
342 else {
343 return output;
344 };
345 use core::fmt::Write;
346 write!(output, "{}", formatter::format(translated, arguments)).unwrap();
347 output
348}
349
350pub fn translate_from_bundle_with_plural(
356 strs: &[Option<&[&str]>],
357 plural_rules: &[Option<fn(i32) -> usize>],
358 arguments: &(impl FormatArgs + ?Sized),
359 n: i32,
360) -> SharedString {
361 let idx = global_translation_property();
362 let mut output = SharedString::default();
363 let en = |n| (n != 1) as usize;
364 let (translations, rule) = match strs.get(idx) {
365 Some(Some(x)) => (x, plural_rules.get(idx).and_then(|x| *x).unwrap_or(en)),
366 _ => match strs.first() {
367 Some(Some(x)) => (x, plural_rules.first().and_then(|x| *x).unwrap_or(en)),
368 _ => return output,
369 },
370 };
371 let Some(translated) = translations.get(rule(n)).or_else(|| translations.first()).cloned()
372 else {
373 return output;
374 };
375 use core::fmt::Write;
376 write!(output, "{}", formatter::format(translated, &WithPlural(arguments, n))).unwrap();
377 output
378}
379
380pub fn set_bundled_languages(translations: &[TranslationsBundled]) {
385 crate::context::GLOBAL_CONTEXT.with(|ctx| {
386 let Some(ctx) = ctx.get() else { return };
387
388 if ctx.0.translations_bundle.borrow().is_none() {
389 ctx.0.translations_bundle.replace(Some(translations.to_vec()));
390 #[cfg(feature = "std")]
391 if let Some(idx) = language_index_from_sys_locale(translations) {
392 ctx.0.as_ref().project_ref().translations_dirty.set(idx);
393 }
394 }
395 });
396}
397
398#[cfg(feature = "std")]
400fn language_index_from_sys_locale(languages: &[TranslationsBundled]) -> Option<usize> {
401 let locale = sys_locale::get_locale()?;
402 let idx = languages.iter().position(|x| *x.language == locale);
404 fn base(l: &str) -> &str {
406 l.find(['-', '_', '@']).map_or(l, |i| &l[..i])
407 }
408 idx.or_else(|| {
409 let locale = base(&locale);
410 languages.iter().position(|x| base(x.language) == locale)
411 })
412}
413
414#[i_slint_core_macros::slint_doc]
415pub fn select_bundled_translation(language: &str) -> Result<(), SelectBundledTranslationError> {
427 crate::context::GLOBAL_CONTEXT.with(|ctx| {
428 let Some(ctx) = ctx.get() else {
429 return Err(SelectBundledTranslationError::NoTranslationsBundled);
430 };
431 let translations = ctx.0.translations_bundle.borrow();
432 let Some(translations) = &*translations else {
433 return Err(SelectBundledTranslationError::NoTranslationsBundled);
434 };
435 let pinned = ctx.0.as_ref().project_ref();
436 if let Some((idx, translation_bundle)) =
437 translations.iter().enumerate().find(|(_i, x)| x.language == language)
438 {
439 pinned.translations_dirty.as_ref().set(idx);
440 pinned.locale_decimal_separator.as_ref().set(translation_bundle.decimal_separator);
442 Ok(())
443 } else if language.is_empty() || language == "en" {
444 pinned.translations_dirty.as_ref().set(0);
445 pinned.locale_decimal_separator.as_ref().set(i_slint_common::DEFAULT_DECIMAL_SEPARATOR);
446 Ok(())
447 } else {
448 Err(SelectBundledTranslationError::LanguageNotFound {
449 available_languages: translations.iter().map(|x| (*x.language).into()).collect(),
450 })
451 }
452 })
453}
454
455#[derive(Debug)]
457pub enum SelectBundledTranslationError {
458 LanguageNotFound { available_languages: crate::SharedVector<SharedString> },
460 NoTranslationsBundled,
463}
464
465impl core::fmt::Display for SelectBundledTranslationError {
466 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
467 match self {
468 SelectBundledTranslationError::LanguageNotFound { available_languages } => {
469 write!(
470 f,
471 "The specified language was not found. Available languages are: {available_languages:?}"
472 )
473 }
474 SelectBundledTranslationError::NoTranslationsBundled => {
475 write!(
476 f,
477 "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"
478 )
479 }
480 }
481 }
482}
483
484#[cfg(feature = "std")]
485impl std::error::Error for SelectBundledTranslationError {}
486
487#[cfg(feature = "ffi")]
488mod ffi {
489 #![allow(unsafe_code)]
490 use super::*;
491 use crate::slice::Slice;
492
493 #[unsafe(no_mangle)]
495 pub extern "C" fn slint_decimal_separator(out: &mut SharedString) {
496 crate::context::GLOBAL_CONTEXT.with(|ctx| {
497 let separator = if let Some(ctx) = ctx.get() {
498 ctx.0.as_ref().project_ref().locale_decimal_separator.get()
499 } else {
500 i_slint_common::DEFAULT_DECIMAL_SEPARATOR
501 };
502 *out = crate::SharedString::from(separator)
503 })
504 }
505
506 #[unsafe(no_mangle)]
508 pub extern "C" fn slint_translate(
509 to_translate: &mut SharedString,
510 context: &SharedString,
511 domain: &SharedString,
512 arguments: Slice<SharedString>,
513 n: i32,
514 plural: &SharedString,
515 ) {
516 *to_translate =
517 translate(to_translate.as_str(), context, domain, arguments.as_slice(), n, plural)
518 }
519
520 #[unsafe(no_mangle)]
522 pub extern "C" fn slint_translations_mark_dirty() {
523 mark_all_translations_dirty();
524 }
525
526 #[unsafe(no_mangle)]
528 pub unsafe extern "C" fn slint_translate_from_bundle(
529 strs: Slice<*const core::ffi::c_char>,
530 arguments: Slice<SharedString>,
531 output: &mut SharedString,
532 ) {
533 *output = SharedString::default();
534 let idx = global_translation_property();
535 let Some(translated) = strs
536 .get(idx)
537 .filter(|x| !x.is_null())
538 .or_else(|| strs.first())
539 .map(|x| unsafe { core::ffi::CStr::from_ptr(*x) }.to_str().unwrap())
540 else {
541 return;
542 };
543 use core::fmt::Write;
544 write!(output, "{}", formatter::format(translated, arguments.as_slice())).unwrap();
545 }
546 #[unsafe(no_mangle)]
553 pub unsafe extern "C" fn slint_translate_from_bundle_with_plural(
554 strs: Slice<*const core::ffi::c_char>,
555 indices: Slice<u32>,
556 plural_rules: Slice<Option<fn(i32) -> usize>>,
557 arguments: Slice<SharedString>,
558 n: i32,
559 output: &mut SharedString,
560 ) {
561 *output = SharedString::default();
562 let idx = global_translation_property();
563 let en = |n| (n != 1) as usize;
564 let begin = *indices.get(idx.wrapping_sub(1)).unwrap_or(&0);
565 let (translations, rule) = match indices.get(idx) {
566 Some(end) if *end != begin => (
567 &strs.as_slice()[begin as usize..*end as usize],
568 plural_rules.get(idx).and_then(|x| *x).unwrap_or(en),
569 ),
570 _ => (
571 &strs.as_slice()[..*indices.first().unwrap_or(&0) as usize],
572 plural_rules.first().and_then(|x| *x).unwrap_or(en),
573 ),
574 };
575 let Some(translated) = translations
576 .get(rule(n))
577 .or_else(|| translations.first())
578 .map(|x| unsafe { core::ffi::CStr::from_ptr(*x) }.to_str().unwrap())
579 else {
580 return;
581 };
582 use core::fmt::Write;
583 write!(output, "{}", formatter::format(translated, &WithPlural(arguments.as_slice(), n)))
584 .unwrap();
585 }
586
587 #[unsafe(no_mangle)]
588 pub extern "C" fn slint_translate_set_bundled_languages(
589 languages: Slice<Slice<'static, u8>>,
590 separators: Slice<u32>,
591 ) {
592 let translations = languages
593 .iter()
594 .zip(separators.as_slice().iter())
595 .map(|(language, separator)| TranslationsBundled {
596 language: core::str::from_utf8(language.as_slice()).unwrap(),
597 decimal_separator: core::char::from_u32(*separator)
598 .unwrap_or(i_slint_common::DEFAULT_DECIMAL_SEPARATOR),
599 })
600 .collect::<alloc::vec::Vec<_>>();
601 set_bundled_languages(&translations);
602 }
603
604 #[unsafe(no_mangle)]
605 pub extern "C" fn slint_translate_select_bundled_translation(language: Slice<u8>) -> bool {
606 let language = core::str::from_utf8(&language).unwrap();
607 select_bundled_translation(language).is_ok()
608 }
609}