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