From dd74126dc667f3aa6e9161f457743aba0bf60bd6 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Sun, 14 Jun 2026 12:53:34 +0200 Subject: [PATCH 01/10] convert path command string without localization Reason: Because otherwise the path parser is not able to parse it properly --- api/cpp/include/private/slint_string.h | 12 ++++++++++++ api/rs/slint/private_unstable_api.rs | 2 +- internal/compiler/generator/cpp.rs | 7 +++++++ internal/compiler/generator/rust.rs | 2 +- internal/core/context.rs | 20 ++++++++++++++++++++ internal/core/string.rs | 19 ++++++++++++++++++- internal/interpreter/eval.rs | 7 ++++++- 7 files changed, 65 insertions(+), 4 deletions(-) diff --git a/api/cpp/include/private/slint_string.h b/api/cpp/include/private/slint_string.h index f6b54f08359..9f4f1886840 100644 --- a/api/cpp/include/private/slint_string.h +++ b/api/cpp/include/private/slint_string.h @@ -252,6 +252,18 @@ inline std::string_view slice_to_string_view(cbindgen_private::Slice st return std::string_view(reinterpret_cast(str.ptr), str.len); } +/// Evaluate `f` while number-to-string casts use a non-localized decimal separator +/// (always '.'), and return its result. Used by generated code when building path +/// command strings, which must not depend on the current locale. +template +inline auto non_localized_cast(F &&f) -> decltype(f()) +{ + auto prev = cbindgen_private::slint_set_localize_cast(false); + auto result = f(); + cbindgen_private::slint_set_localize_cast(prev); + return result; +} + } /// Styled text that has been parsed and separated into paragraphs. diff --git a/api/rs/slint/private_unstable_api.rs b/api/rs/slint/private_unstable_api.rs index 7a98ef32a3d..64ebb6810c5 100644 --- a/api/rs/slint/private_unstable_api.rs +++ b/api/rs/slint/private_unstable_api.rs @@ -174,7 +174,7 @@ pub mod re_exports { pub use i_slint_core::animations::{EasingCurve, animation_tick, current_tick}; pub use i_slint_core::api::LogicalPosition; pub use i_slint_core::callbacks::Callback; - pub use i_slint_core::context::SlintContext; + pub use i_slint_core::context::{SlintContext, non_localized_cast}; pub use i_slint_core::data_transfer::DataTransfer; pub use i_slint_core::date_time::*; pub use i_slint_core::detect_operating_system; diff --git a/internal/compiler/generator/cpp.rs b/internal/compiler/generator/cpp.rs index 7e25100e605..780407343cb 100644 --- a/internal/compiler/generator/cpp.rs +++ b/internal/compiler/generator/cpp.rs @@ -4004,6 +4004,13 @@ fn compile_expression(expr: &llr::Expression, ctx: &EvaluationContext) -> String }}({events}, {points})"# ) } + (Type::String, Type::PathData) => { + // Path command strings must always use '.' as decimal separator, so + // evaluate the string with number casts non-localized. + format!( + "slint::private_api::non_localized_cast([&]{{ return slint::private_api::PathData({f}); }})" + ) + } (Type::Enumeration(e), Type::String) => { let mut cases = e.values.iter().enumerate().map(|(idx, v)| { let c = compile_expression( diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs index 5f3f8c65160..db0aba73469 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -3062,7 +3062,7 @@ fn compile_expression(expr: &Expression, ctx: &EvaluationContext) -> TokenStream quote!(sp::PathData::Events(sp::SharedVector::<_>::from_slice(&#events), sp::SharedVector::<_>::from_slice(&#points))) } (Type::String, Type::PathData) => { - quote!(sp::PathData::Commands(#f)) + quote!(sp::non_localized_cast(|| sp::PathData::Commands(#f))) } (Type::Enumeration(e), Type::String) => { let cases = e.values.iter().enumerate().map(|(idx, v)| { diff --git a/internal/core/context.rs b/internal/core/context.rs index c7f7158619a..fad67fedbb2 100644 --- a/internal/core/context.rs +++ b/internal/core/context.rs @@ -97,6 +97,7 @@ pub(crate) struct SlintContextInner { #[cfg(feature = "shared-swash")] pub(crate) swash_scale_context: core::cell::RefCell, pub(crate) modifiers: Cell, + pub(crate) localize_cast: Cell, } /// This context is meant to hold the state and the backend. @@ -148,6 +149,7 @@ impl SlintContext { #[cfg(feature = "shared-swash")] swash_scale_context: core::cell::RefCell::new(swash::scale::ScaleContext::new()), modifiers: Cell::new(Default::default()), + localize_cast: Cell::new(true), })) } @@ -394,3 +396,21 @@ pub fn set_window_event_hook( None => Err(PlatformError::NoPlatform), }) } + +pub fn non_localized_cast T>(func: F) -> T { + let mut prev_state = true; + crate::context::GLOBAL_CONTEXT.with(|ctx| { + if let Some(ctx) = ctx.get() { + let pinned = ctx.0.as_ref().project_ref(); + prev_state = pinned.localize_cast.replace(false); + } + }); + let res = func(); + crate::context::GLOBAL_CONTEXT.with(|ctx| { + if let Some(ctx) = ctx.get() { + let pinned = ctx.0.as_ref().project_ref(); + pinned.localize_cast.set(prev_state); + } + }); + res +} diff --git a/internal/core/string.rs b/internal/core/string.rs index a4a998a706f..f3c18b64e06 100644 --- a/internal/core/string.rs +++ b/internal/core/string.rs @@ -351,7 +351,9 @@ pub fn shared_string_from_number(n: f64) -> SharedString { if let Some(ctx) = ctx.get() { let pinned = ctx.0.as_ref().project_ref(); let decimal_separator = pinned.locale_decimal_separator.get(); - if decimal_separator != i_slint_common::DEFAULT_DECIMAL_SEPARATOR { + if pinned.localize_cast.get() + && decimal_separator != i_slint_common::DEFAULT_DECIMAL_SEPARATOR + { result.replace_characters('.', decimal_separator, 1); } } @@ -552,6 +554,21 @@ pub(crate) mod ffi { unsafe { core::ptr::write(out, str) }; } + /// Enable or disable localization of number-to-string casts (e.g. the decimal + /// separator). The generated C++ code disables it around the construction of path + /// command strings, which must always use '.' as decimal separator. + /// See [`crate::context::non_localized_cast`] for the Rust equivalent. + #[unsafe(no_mangle)] + pub extern "C" fn slint_set_localize_cast(localize: bool) -> bool { + let mut prev_value = true; + crate::context::GLOBAL_CONTEXT.with(|ctx| { + if let Some(ctx) = ctx.get() { + prev_value = ctx.0.as_ref().project_ref().localize_cast.replace(localize); + } + }); + prev_value + } + #[test] fn test_slint_shared_string_from_number() { unsafe { diff --git a/internal/interpreter/eval.rs b/internal/interpreter/eval.rs index a6ee751baa4..44e861df093 100644 --- a/internal/interpreter/eval.rs +++ b/internal/interpreter/eval.rs @@ -2406,7 +2406,12 @@ pub fn convert_path(path: &ExprPath, local_context: &mut EvalLocalContext) -> Pa convert_from_lyon_path(events.iter(), points.iter(), local_context) } ExprPath::Commands(commands) => { - if let Value::String(commands) = eval_expression(commands, local_context) { + // Path command strings must always use '.' as decimal separator, so evaluate + // the string (including any number-to-string casts) without localization. + let value = i_slint_core::context::non_localized_cast(|| { + eval_expression(commands, local_context) + }); + if let Value::String(commands) = value { PathData::Commands(commands) } else { panic!("binding to path commands does not evaluate to string"); From e5a89addef49b3ac7ac6a57bf9138d32bef4dec7 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Sun, 14 Jun 2026 12:53:34 +0200 Subject: [PATCH 02/10] Revert "convert path command string without localization" Reason: This does not work reliable, because if the svg command will be assigned to the path not in the init it will fail. The idea now is add an explicit localization function This reverts commit dd74126dc667f3aa6e9161f457743aba0bf60bd6. --- api/cpp/include/private/slint_string.h | 12 ------------ api/rs/slint/private_unstable_api.rs | 2 +- internal/compiler/generator/cpp.rs | 7 ------- internal/compiler/generator/rust.rs | 2 +- internal/core/context.rs | 20 -------------------- internal/core/string.rs | 19 +------------------ internal/interpreter/eval.rs | 7 +------ 7 files changed, 4 insertions(+), 65 deletions(-) diff --git a/api/cpp/include/private/slint_string.h b/api/cpp/include/private/slint_string.h index 9f4f1886840..f6b54f08359 100644 --- a/api/cpp/include/private/slint_string.h +++ b/api/cpp/include/private/slint_string.h @@ -252,18 +252,6 @@ inline std::string_view slice_to_string_view(cbindgen_private::Slice st return std::string_view(reinterpret_cast(str.ptr), str.len); } -/// Evaluate `f` while number-to-string casts use a non-localized decimal separator -/// (always '.'), and return its result. Used by generated code when building path -/// command strings, which must not depend on the current locale. -template -inline auto non_localized_cast(F &&f) -> decltype(f()) -{ - auto prev = cbindgen_private::slint_set_localize_cast(false); - auto result = f(); - cbindgen_private::slint_set_localize_cast(prev); - return result; -} - } /// Styled text that has been parsed and separated into paragraphs. diff --git a/api/rs/slint/private_unstable_api.rs b/api/rs/slint/private_unstable_api.rs index 64ebb6810c5..7a98ef32a3d 100644 --- a/api/rs/slint/private_unstable_api.rs +++ b/api/rs/slint/private_unstable_api.rs @@ -174,7 +174,7 @@ pub mod re_exports { pub use i_slint_core::animations::{EasingCurve, animation_tick, current_tick}; pub use i_slint_core::api::LogicalPosition; pub use i_slint_core::callbacks::Callback; - pub use i_slint_core::context::{SlintContext, non_localized_cast}; + pub use i_slint_core::context::SlintContext; pub use i_slint_core::data_transfer::DataTransfer; pub use i_slint_core::date_time::*; pub use i_slint_core::detect_operating_system; diff --git a/internal/compiler/generator/cpp.rs b/internal/compiler/generator/cpp.rs index 780407343cb..7e25100e605 100644 --- a/internal/compiler/generator/cpp.rs +++ b/internal/compiler/generator/cpp.rs @@ -4004,13 +4004,6 @@ fn compile_expression(expr: &llr::Expression, ctx: &EvaluationContext) -> String }}({events}, {points})"# ) } - (Type::String, Type::PathData) => { - // Path command strings must always use '.' as decimal separator, so - // evaluate the string with number casts non-localized. - format!( - "slint::private_api::non_localized_cast([&]{{ return slint::private_api::PathData({f}); }})" - ) - } (Type::Enumeration(e), Type::String) => { let mut cases = e.values.iter().enumerate().map(|(idx, v)| { let c = compile_expression( diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs index db0aba73469..5f3f8c65160 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -3062,7 +3062,7 @@ fn compile_expression(expr: &Expression, ctx: &EvaluationContext) -> TokenStream quote!(sp::PathData::Events(sp::SharedVector::<_>::from_slice(&#events), sp::SharedVector::<_>::from_slice(&#points))) } (Type::String, Type::PathData) => { - quote!(sp::non_localized_cast(|| sp::PathData::Commands(#f))) + quote!(sp::PathData::Commands(#f)) } (Type::Enumeration(e), Type::String) => { let cases = e.values.iter().enumerate().map(|(idx, v)| { diff --git a/internal/core/context.rs b/internal/core/context.rs index fad67fedbb2..c7f7158619a 100644 --- a/internal/core/context.rs +++ b/internal/core/context.rs @@ -97,7 +97,6 @@ pub(crate) struct SlintContextInner { #[cfg(feature = "shared-swash")] pub(crate) swash_scale_context: core::cell::RefCell, pub(crate) modifiers: Cell, - pub(crate) localize_cast: Cell, } /// This context is meant to hold the state and the backend. @@ -149,7 +148,6 @@ impl SlintContext { #[cfg(feature = "shared-swash")] swash_scale_context: core::cell::RefCell::new(swash::scale::ScaleContext::new()), modifiers: Cell::new(Default::default()), - localize_cast: Cell::new(true), })) } @@ -396,21 +394,3 @@ pub fn set_window_event_hook( None => Err(PlatformError::NoPlatform), }) } - -pub fn non_localized_cast T>(func: F) -> T { - let mut prev_state = true; - crate::context::GLOBAL_CONTEXT.with(|ctx| { - if let Some(ctx) = ctx.get() { - let pinned = ctx.0.as_ref().project_ref(); - prev_state = pinned.localize_cast.replace(false); - } - }); - let res = func(); - crate::context::GLOBAL_CONTEXT.with(|ctx| { - if let Some(ctx) = ctx.get() { - let pinned = ctx.0.as_ref().project_ref(); - pinned.localize_cast.set(prev_state); - } - }); - res -} diff --git a/internal/core/string.rs b/internal/core/string.rs index f3c18b64e06..a4a998a706f 100644 --- a/internal/core/string.rs +++ b/internal/core/string.rs @@ -351,9 +351,7 @@ pub fn shared_string_from_number(n: f64) -> SharedString { if let Some(ctx) = ctx.get() { let pinned = ctx.0.as_ref().project_ref(); let decimal_separator = pinned.locale_decimal_separator.get(); - if pinned.localize_cast.get() - && decimal_separator != i_slint_common::DEFAULT_DECIMAL_SEPARATOR - { + if decimal_separator != i_slint_common::DEFAULT_DECIMAL_SEPARATOR { result.replace_characters('.', decimal_separator, 1); } } @@ -554,21 +552,6 @@ pub(crate) mod ffi { unsafe { core::ptr::write(out, str) }; } - /// Enable or disable localization of number-to-string casts (e.g. the decimal - /// separator). The generated C++ code disables it around the construction of path - /// command strings, which must always use '.' as decimal separator. - /// See [`crate::context::non_localized_cast`] for the Rust equivalent. - #[unsafe(no_mangle)] - pub extern "C" fn slint_set_localize_cast(localize: bool) -> bool { - let mut prev_value = true; - crate::context::GLOBAL_CONTEXT.with(|ctx| { - if let Some(ctx) = ctx.get() { - prev_value = ctx.0.as_ref().project_ref().localize_cast.replace(localize); - } - }); - prev_value - } - #[test] fn test_slint_shared_string_from_number() { unsafe { diff --git a/internal/interpreter/eval.rs b/internal/interpreter/eval.rs index 44e861df093..a6ee751baa4 100644 --- a/internal/interpreter/eval.rs +++ b/internal/interpreter/eval.rs @@ -2406,12 +2406,7 @@ pub fn convert_path(path: &ExprPath, local_context: &mut EvalLocalContext) -> Pa convert_from_lyon_path(events.iter(), points.iter(), local_context) } ExprPath::Commands(commands) => { - // Path command strings must always use '.' as decimal separator, so evaluate - // the string (including any number-to-string casts) without localization. - let value = i_slint_core::context::non_localized_cast(|| { - eval_expression(commands, local_context) - }); - if let Value::String(commands) = value { + if let Value::String(commands) = eval_expression(commands, local_context) { PathData::Commands(commands) } else { panic!("binding to path commands does not evaluate to string"); From 157e63cd92f5843297c70b291c085d9e02a31b97 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Mon, 15 Jun 2026 15:31:19 +0200 Subject: [PATCH 03/10] Implement to-string-unlocalized() function --- api/rs/slint/private_unstable_api.rs | 1 + internal/compiler/expression_tree.rs | 8 ++++++-- internal/compiler/generator/cpp.rs | 5 +++++ internal/compiler/generator/rust.rs | 4 ++++ .../llr/optim_passes/inline_expressions.rs | 1 + internal/compiler/lookup.rs | 3 +++ internal/core/string.rs | 18 +++++++++++++++--- internal/interpreter/eval.rs | 4 ++++ .../cases/translations/decimal_separator.slint | 8 +++++++- tools/lsp/preview/eval.rs | 5 +++++ 10 files changed, 51 insertions(+), 6 deletions(-) diff --git a/api/rs/slint/private_unstable_api.rs b/api/rs/slint/private_unstable_api.rs index 7a98ef32a3d..d5382dd4937 100644 --- a/api/rs/slint/private_unstable_api.rs +++ b/api/rs/slint/private_unstable_api.rs @@ -207,6 +207,7 @@ pub mod re_exports { pub use i_slint_core::string::shared_string_from_number; pub use i_slint_core::string::shared_string_from_number_fixed; pub use i_slint_core::string::shared_string_from_number_precision; + pub use i_slint_core::string::shared_string_from_number_unlocalized; pub use i_slint_core::timers::{Timer, TimerMode}; pub use i_slint_core::translations::{ set_bundled_languages, translate_from_bundle, translate_from_bundle_with_plural, diff --git a/internal/compiler/expression_tree.rs b/internal/compiler/expression_tree.rs index ece5fbe63ea..20375fa305d 100644 --- a/internal/compiler/expression_tree.rs +++ b/internal/compiler/expression_tree.rs @@ -46,6 +46,7 @@ pub enum BuiltinFunction { Exp, ToFixed, ToPrecision, + ToStringUnlocalized, SetFocusItem, ClearFocusItem, ShowPopupWindow, @@ -213,6 +214,7 @@ declare_builtin_function_types!( Exp: (Type::Float32) -> Type::Float32, ToFixed: (Type::Float32, Type::Int32) -> Type::String, ToPrecision: (Type::Float32, Type::Int32) -> Type::String, + ToStringUnlocalized: (Type::Float32) -> Type::String, SetFocusItem: (Type::ElementReference) -> Type::Void, ClearFocusItem: (Type::ElementReference) -> Type::Void, ShowPopupWindow: (Type::ElementReference) -> Type::Void, @@ -370,7 +372,8 @@ impl BuiltinFunction { | BuiltinFunction::ATan | BuiltinFunction::ATan2 | BuiltinFunction::ToFixed - | BuiltinFunction::ToPrecision => true, + | BuiltinFunction::ToPrecision + | BuiltinFunction::ToStringUnlocalized => true, BuiltinFunction::SetFocusItem | BuiltinFunction::ClearFocusItem => false, BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow @@ -465,7 +468,8 @@ impl BuiltinFunction { | BuiltinFunction::ATan | BuiltinFunction::ATan2 | BuiltinFunction::ToFixed - | BuiltinFunction::ToPrecision => true, + | BuiltinFunction::ToPrecision + | BuiltinFunction::ToStringUnlocalized => true, BuiltinFunction::SetFocusItem | BuiltinFunction::ClearFocusItem => false, BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow diff --git a/internal/compiler/generator/cpp.rs b/internal/compiler/generator/cpp.rs index 7e25100e605..43475fa4900 100644 --- a/internal/compiler/generator/cpp.rs +++ b/internal/compiler/generator/cpp.rs @@ -4566,6 +4566,11 @@ fn compile_builtin_function_call( a.next().unwrap(), a.next().unwrap(), ) } + BuiltinFunction::ToStringUnlocalized => { + format!("[](double n) {{ slint::SharedString out; slint::cbindgen_private::slint_shared_string_from_number_unlocalized(&out, n); return out; }}({})", + a.next().unwrap(), + ) + } BuiltinFunction::SetFocusItem => { if let [llr::Expression::PropertyReference(pr)] = arguments { let window = access_window_field(ctx); diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs index 5f3f8c65160..ef332e8c270 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -4009,6 +4009,10 @@ fn compile_builtin_function_call( let (a1, a2) = (a.next().unwrap(), a.next().unwrap()); quote!(sp::shared_string_from_number_precision(#a1 as f64, (#a2 as i32).max(0) as usize)) } + BuiltinFunction::ToStringUnlocalized => { + let a1 = a.next().unwrap(); + quote!(sp::shared_string_from_number_unlocalized(#a1 as f64)) + } BuiltinFunction::StringToFloat => { quote!(sp::string_to_float(#(#a)*.as_str()).unwrap_or_default()) } diff --git a/internal/compiler/llr/optim_passes/inline_expressions.rs b/internal/compiler/llr/optim_passes/inline_expressions.rs index c2cfbedddd4..0b123310cec 100644 --- a/internal/compiler/llr/optim_passes/inline_expressions.rs +++ b/internal/compiler/llr/optim_passes/inline_expressions.rs @@ -114,6 +114,7 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize { BuiltinFunction::Exp => 10, BuiltinFunction::ToFixed => ALLOC_COST, BuiltinFunction::ToPrecision => ALLOC_COST, + BuiltinFunction::ToStringUnlocalized => ALLOC_COST, BuiltinFunction::SetFocusItem | BuiltinFunction::ClearFocusItem => isize::MAX, BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow diff --git a/internal/compiler/lookup.rs b/internal/compiler/lookup.rs index 573eb1e9a47..3fd5300ef7b 100644 --- a/internal/compiler/lookup.rs +++ b/internal/compiler/lookup.rs @@ -1163,6 +1163,9 @@ impl LookupObject for NumberExpression<'_> { .or_else(|| f2("sign", member_macro(BuiltinMacroFunction::Sign))) .or_else(|| f2("to-fixed", member_function(BuiltinFunction::ToFixed))) .or_else(|| f2("to-precision", member_function(BuiltinFunction::ToPrecision))) + .or_else(|| { + f2("to-string-unlocalized", member_function(BuiltinFunction::ToStringUnlocalized)) + }) .or_else(|| NumberWithUnitExpression(self.0).for_each_entry(ctx, f)) } } diff --git a/internal/core/string.rs b/internal/core/string.rs index a4a998a706f..645cb8640f6 100644 --- a/internal/core/string.rs +++ b/internal/core/string.rs @@ -341,12 +341,16 @@ where } } +/// Convert a f64 to a SharedString +pub fn shared_string_from_number_unlocalized(n: f64) -> SharedString { + // Number from which the increment of f32 is 1, so that we print enough precision to be able to represent all integers + if n < 16777216. { crate::format!("{}", n as f32) } else { crate::format!("{}", n) } +} + /// Convert a f64 to a SharedString pub fn shared_string_from_number(n: f64) -> SharedString { crate::context::GLOBAL_CONTEXT.with(|ctx| { - // Number from which the increment of f32 is 1, so that we print enough precision to be able to represent all integers - let mut result = - if n < 16777216. { crate::format!("{}", n as f32) } else { crate::format!("{}", n) }; + let mut result = shared_string_from_number_unlocalized(n); if let Some(ctx) = ctx.get() { let pinned = ctx.0.as_ref().project_ref(); @@ -544,6 +548,14 @@ pub(crate) mod ffi { } } + /// Create a string from a number but unlocalized. + /// The resulting structure must be passed to slint_shared_string_drop + #[unsafe(no_mangle)] + pub unsafe extern "C" fn slint_shared_string_from_number(out: *mut SharedString, n: f64) { + let str = shared_string_from_number_unlocalized(n); + unsafe { core::ptr::write(out, str) }; + } + /// Create a string from a number. /// The resulting structure must be passed to slint_shared_string_drop #[unsafe(no_mangle)] diff --git a/internal/interpreter/eval.rs b/internal/interpreter/eval.rs index a6ee751baa4..4a3ead5d6b1 100644 --- a/internal/interpreter/eval.rs +++ b/internal/interpreter/eval.rs @@ -852,6 +852,10 @@ fn call_builtin_function( let precision: usize = precision.max(0) as usize; Value::String(i_slint_core::string::shared_string_from_number_precision(n, precision)) } + BuiltinFunction::ToStringUnlocalized => { + let n: f64 = eval_expression(&arguments[0], local_context).try_into().unwrap(); + Value::String(i_slint_core::string::shared_string_from_number_unlocalized(n)) + } BuiltinFunction::SetFocusItem => { if arguments.len() != 1 { panic!("internal error: incorrect argument count to SetFocusItem") diff --git a/tests/cases/translations/decimal_separator.slint b/tests/cases/translations/decimal_separator.slint index 2f934eac19b..08dc790ac98 100644 --- a/tests/cases/translations/decimal_separator.slint +++ b/tests/cases/translations/decimal_separator.slint @@ -15,6 +15,8 @@ export component TestCase { in-out property string_as_float: value_string.to-float(); in-out property is_float: value_string.is-float(); + in-out property float_as_string_unlocalized: value.to-string-unlocalized(); + // \{ cannot be in a slint macro, so we add it here, because we wanna execute the Test with // compiled source (otherwise bundling translations is not possible) out property same_or_diff: "\{the-decimal-separator}"; @@ -30,6 +32,7 @@ assert!(slint::select_bundled_translation("fr").is_ok()); assert_eq!(instance.get_the_decimal_separator(), ","); instance.set_value(-5.5); assert_eq!(instance.get_float_as_string(), "-5,5"); +assert_eq!(instance.get_float_as_string_unlocalized(), "-5.5"); instance.set_value_string("5.5".into()); assert_eq!(instance.get_is_float(), false); @@ -42,6 +45,7 @@ assert!(slint::select_bundled_translation("en").is_ok()); assert_eq!(instance.get_the_decimal_separator(), "."); instance.set_value(7.123); assert_eq!(instance.get_float_as_string(), "7.123"); +assert_eq!(instance.get_float_as_string_unlocalized(), "7.123"); instance.set_value_string("5,5".into()); assert_eq!(instance.get_is_float(), false); @@ -63,6 +67,7 @@ assert(slint::select_bundled_translation("fr")); assert_eq(instance.get_the_decimal_separator(), ","); instance.set_value(-5.5); assert_eq(instance.get_float_as_string(), "-5,5"); +assert_eq(instance.get_float_as_string_unlocalized(), "-5.5"); instance.set_value_string("5.5"); assert_eq(instance.get_is_float(), false); @@ -75,6 +80,7 @@ assert(slint::select_bundled_translation("en")); assert_eq(instance.get_the_decimal_separator(), "."); instance.set_value(7.123); assert_eq(instance.get_float_as_string(), "7.123"); +assert_eq(instance.get_float_as_string_unlocalized(), "7.123"); instance.set_value_string("5,5"); assert_eq(instance.get_is_float(), false); @@ -84,4 +90,4 @@ instance.set_value_string("3.2"); assert_eq(instance.get_is_float(), true); assert_eq(instance.get_string_as_float(), 3.2); ``` -*/ \ No newline at end of file +*/ diff --git a/tools/lsp/preview/eval.rs b/tools/lsp/preview/eval.rs index 564d8e60498..62f5d34ca92 100644 --- a/tools/lsp/preview/eval.rs +++ b/tools/lsp/preview/eval.rs @@ -437,6 +437,11 @@ fn handle_builtin_function( let precision: usize = precision.max(0) as usize; Value::String(i_slint_core::string::shared_string_from_number_precision(n, precision)) } + BuiltinFunction::ToStringUnlocalized => { + let n: f64 = + eval_expression(&arguments[0], local_context, None).try_into().unwrap_or_default(); + Value::String(i_slint_core::string::shared_string_from_number_unlocalized(n)) + } BuiltinFunction::StringIsFloat => { if arguments.len() != 1 { return Value::Void; From 7b6b59241897dc7079c1f10a2f6e462eed5967d7 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Mon, 15 Jun 2026 16:05:33 +0200 Subject: [PATCH 04/10] add inline --- internal/core/string.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/core/string.rs b/internal/core/string.rs index 27b680b6b49..6512d03c033 100644 --- a/internal/core/string.rs +++ b/internal/core/string.rs @@ -341,7 +341,8 @@ where } } -/// Convert a f64 to a SharedString +/// Convert a f64 to a SharedString but unlocalized with "." as decimal separator +#[inline] pub fn shared_string_from_number_unlocalized(n: f64) -> SharedString { crate::format!("{}", i_slint_common::FormattedNumber(n)) } From dba242a413e3bfb3edd8c10428aabcca2541093a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:09:43 +0000 Subject: [PATCH 05/10] [autofix.ci] apply automated fixes --- internal/compiler/expression_tree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/compiler/expression_tree.rs b/internal/compiler/expression_tree.rs index 7490050a0b6..a698f0ab98a 100644 --- a/internal/compiler/expression_tree.rs +++ b/internal/compiler/expression_tree.rs @@ -371,7 +371,7 @@ impl BuiltinFunction { | BuiltinFunction::Exp | BuiltinFunction::ATan | BuiltinFunction::ATan2 - | BuiltinFunction::ToStringUnlocalized=> true, + | BuiltinFunction::ToStringUnlocalized => true, // The result depends on the locale's decimal separator, like DecimalSeparator. // The constant propagation folds the locale-independent cases and promotes // their binding back to constant. From 18c69f80629ab90995ff33d03678292ddebb012f Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Mon, 15 Jun 2026 16:30:33 +0200 Subject: [PATCH 06/10] add unlocalized as known word --- cspell.json | 702 ++++++++++++++++++++++++++-------------------------- 1 file changed, 352 insertions(+), 350 deletions(-) diff --git a/cspell.json b/cspell.json index 73df0d2ee02..3fac359406d 100644 --- a/cspell.json +++ b/cspell.json @@ -1,348 +1,350 @@ { - "version": "0.2", - "language": "en", - "languageSettings": [ - { - "languageId": "rust", - "words": [ - "bbox", - "bindgen", - "boxshadowcache", - "builtins", - "canonicalize", // std::path::Path::canonicalize - "codemap", - "concat", - "Consts", - "evenodd", - "FBOs", // plural of FBO (frame buffer object) - "fontdb", - "fullscreen", - "glutin", - "glutin's", - "maxx", - "maxy", - "minx", - "miny", - "miri", - "peekable", - "Realloc", - "refcell", - "rtti", - "Smol", - "smolstr", - "subspan", - "uninit", - "unmap", - "unsync", - "vsync" - ], - "ignoreRegExpList": [ - "/#\\[cfg\\b.+\\]/", - "/#\\[repr\\b.+\\]/", - // Exclude crate paths - "/(\\b|::)[a-z0-9_]+::/", - "/::[a-z0-9_]+(\\b|::)/", - // Qt things: - "/\\bQt_[a-zA-Z0-9_]+/" - ] - }, - { - "languageId": "restructuredtext", + "version": "0.2", + "language": "en", + "languageSettings": [ + { + "languageId": "rust", + "words": [ + "bbox", + "bindgen", + "boxshadowcache", + "builtins", + "canonicalize", // std::path::Path::canonicalize + "codemap", + "concat", + "Consts", + "evenodd", + "FBOs", // plural of FBO (frame buffer object) + "fontdb", + "fullscreen", + "glutin", + "glutin's", + "maxx", + "maxy", + "minx", + "miny", + "miri", + "peekable", + "Realloc", + "refcell", + "rtti", + "Smol", + "smolstr", + "subspan", + "uninit", + "unlocalized", + "Unlocalized", + "unmap", + "unsync", + "vsync" + ], + "ignoreRegExpList": [ + "/#\\[cfg\\b.+\\]/", + "/#\\[repr\\b.+\\]/", + // Exclude crate paths + "/(\\b|::)[a-z0-9_]+::/", + "/::[a-z0-9_]+(\\b|::)/", + // Qt things: + "/\\bQt_[a-zA-Z0-9_]+/" + ] + }, + { + "languageId": "restructuredtext", "words": [ "genindex" ], "ignoreRegExpList": [ "/:.+:/" ] - }, - { + }, + { "languageId": [ "markdown", "md", "mdx" ], - "words": [ - "DSLINT", - "Espressif", - "libname", - "linebuffer", - "mylibrary", - "mywidgets", - "otherlibrary", - "Lmylibrary", - "progressindicator", - "standardbutton", - "standardlistview", - "standardtableview", - "rustup", - "setdefault", - "Sonoma", - "SUBDIR", - "Yocto", - "relpath", - "classslint", - "vectormodel", - "structslint", - "namespaceslint", - "compilerconfiguration", - "componentweakhandle", - "listmodel", - "skiarenderer", - "softwarerenderer", - "tschüß", - "TSCHÜSS", - "ὈΔΥΣΣΕΎΣ", - "ὀδυσσεύς", - "commonmark", - "strikethroughs" - ] - } - ], - "words": [ - "aarch", - "AARRGGBB", - "accesskit", - "antialiasing", - "Argb", - "armv", - "astrojs", - "Bezier", - "Bézier", - "cbindgen", - "cdylib", - "cmake", - "colspan", - "combobox", - "radiobutton", - "contexte", - "cupertino", - "datastructures", - "dealloc", - "distros", - "embedders", - "espflash", - "Femto", - "femtovg", - "flexboxlayout", - "flickable", - "focusable", - "frameless", - "fullscreen", - "gles", - "Goffart", - "gradians", - "grayscale", - "gridlayout", - "groupbox", - "Hausmann", - "Helvetica", - "imagefilter", - "inout", - "knip", - "layouting", - "linebreak", - "lineedit", - "LINUXFB", - "listview", - "lvalue", - "mdns", - "microcontroller", - "microcontrollers", - "MSVC", - "muda", - "napi", - "ogoffart", - "oneshot", - "Oooops", - "opengl", - "opengles", - "Pandoc", - "pico", - "pixmap", - "prestart", - "printerdemo", - "pythonw", - "replacen", - "rehype", - "Rgba", - "riscv", - "rowspan", - "RRGGBB", - "RRGGBBAA", - "rustc", - "rustdoc", - "rustflags", - "rvalue", - "SANDBOXING", - "scrollview", - "SDK", - "seti", - "sixtyfps", - "skia", - "slint", - "slintdoc", - "slintdocs", - "slintpad", - "spdx", - "spinbox", - "Srgb", - "streetsidesoftware", - "tableview", - "tabwidget", - "testcase", - "textedit", - "tmpobj", - "toolchain", - "toolkit", - "Toradex", - "touchpad", - "trackpad", - "trackpads", - "tronical", - "typeloader", - "uefi", - "uncompiled", - "Unshiftable", - "unerase", - "unmapping", - "unminimize", - "untracked", - "viewbox", - "virtualizes", - "Vivante", - "vtable", - "vulkan", - "wasm", - "webassembly", - "wgpu", - "windowrc", - "winit", - "xaarrggbb", - "Xcodegen", - "Xcodeproj", - "xtask", - "xtensa", - // CSS Color 4 named-color keywords not accepted by the default English dictionary (W3C CSS Color Module Level 4). - "aliceblue", - "antiquewhite", - "blanchedalmond", - "blueviolet", - "burlywood", - "cadetblue", - "cornflowerblue", - "cornsilk", - "darkcyan", - "darkgoldenrod", - "darkgray", - "darkgreen", - "darkgrey", - "darkkhaki", - "darkmagenta", - "darkolivegreen", - "darkorange", - "darkorchid", - "darkred", - "darksalmon", - "darkseagreen", - "darkslateblue", - "darkslategray", - "darkslategrey", - "darkturquoise", - "darkviolet", - "deeppink", - "deepskyblue", - "dimgray", - "dimgrey", - "dodgerblue", - "floralwhite", - "forestgreen", - "gainsboro", - "ghostwhite", - "greenyellow", - "hotpink", - "indianred", - "lavenderblush", - "lawngreen", - "lemonchiffon", - "lightcoral", - "lightcyan", - "lightgoldenrodyellow", - "lightgray", - "lightgreen", - "lightpink", - "lightsalmon", - "lightseagreen", - "lightskyblue", - "lightslategray", - "lightslategrey", - "lightsteelblue", - "lightyellow", - "limegreen", - "mediumaquamarine", - "mediumblue", - "mediumorchid", - "mediumpurple", - "mediumseagreen", - "mediumslateblue", - "mediumspringgreen", - "mediumturquoise", - "mediumvioletred", - "midnightblue", - "mintcream", - "mistyrose", - "navajowhite", - "oldlace", - "olivedrab", - "orangered", - "palegoldenrod", - "palegreen", - "paleturquoise", - "palevioletred", - "papayawhip", - "peachpuff", - "powderblue", - "rebeccapurple", - "rosybrown", - "royalblue", - "saddlebrown", - "sandybrown", - "seagreen", - "skyblue", - "slateblue", - "slategray", - "slategrey", - "springgreen", - "steelblue", - "whitesmoke", - "yellowgreen" - ], - "ignorePaths": [ - ".github/**", - "api/cpp/docs/conf.py", - "CHANGELOG.md", - "cspell.json", - "LICENSES", - "examples/*/lang/**", - "pnpm-lock.yaml", - "pnpm-workspace.yaml", - "REUSE.toml", - "**/Cargo.lock", - "docs/common/src/utils/slint.tmLanguage.json", - "ui-libraries/material/docs/vendor/**", - ".cspell/slint-project-words.txt", - ".cspell/slint-project-words-audit.tsv", - "tools/figma-inspector/tests/figma_output.json", - "**/*.gltf", - "**/*.po", - "**/*.pot", - "**/*.svg", - "target/**", - "**/node_modules/**" - ], - "overrides": [ - { - "filename": "demos/printerdemo/lang/fr/**", - "language": "en,fr", + "words": [ + "DSLINT", + "Espressif", + "libname", + "linebuffer", + "mylibrary", + "mywidgets", + "otherlibrary", + "Lmylibrary", + "progressindicator", + "standardbutton", + "standardlistview", + "standardtableview", + "rustup", + "setdefault", + "Sonoma", + "SUBDIR", + "Yocto", + "relpath", + "classslint", + "vectormodel", + "structslint", + "namespaceslint", + "compilerconfiguration", + "componentweakhandle", + "listmodel", + "skiarenderer", + "softwarerenderer", + "tschüß", + "TSCHÜSS", + "ὈΔΥΣΣΕΎΣ", + "ὀδυσσεύς", + "commonmark", + "strikethroughs" + ] + } + ], + "words": [ + "aarch", + "AARRGGBB", + "accesskit", + "antialiasing", + "Argb", + "armv", + "astrojs", + "Bezier", + "Bézier", + "cbindgen", + "cdylib", + "cmake", + "colspan", + "combobox", + "radiobutton", + "contexte", + "cupertino", + "datastructures", + "dealloc", + "distros", + "embedders", + "espflash", + "Femto", + "femtovg", + "flexboxlayout", + "flickable", + "focusable", + "frameless", + "fullscreen", + "gles", + "Goffart", + "gradians", + "grayscale", + "gridlayout", + "groupbox", + "Hausmann", + "Helvetica", + "imagefilter", + "inout", + "knip", + "layouting", + "linebreak", + "lineedit", + "LINUXFB", + "listview", + "lvalue", + "mdns", + "microcontroller", + "microcontrollers", + "MSVC", + "muda", + "napi", + "ogoffart", + "oneshot", + "Oooops", + "opengl", + "opengles", + "Pandoc", + "pico", + "pixmap", + "prestart", + "printerdemo", + "pythonw", + "replacen", + "rehype", + "Rgba", + "riscv", + "rowspan", + "RRGGBB", + "RRGGBBAA", + "rustc", + "rustdoc", + "rustflags", + "rvalue", + "SANDBOXING", + "scrollview", + "SDK", + "seti", + "sixtyfps", + "skia", + "slint", + "slintdoc", + "slintdocs", + "slintpad", + "spdx", + "spinbox", + "Srgb", + "streetsidesoftware", + "tableview", + "tabwidget", + "testcase", + "textedit", + "tmpobj", + "toolchain", + "toolkit", + "Toradex", + "touchpad", + "trackpad", + "trackpads", + "tronical", + "typeloader", + "uefi", + "uncompiled", + "Unshiftable", + "unerase", + "unmapping", + "unminimize", + "untracked", + "viewbox", + "virtualizes", + "Vivante", + "vtable", + "vulkan", + "wasm", + "webassembly", + "wgpu", + "windowrc", + "winit", + "xaarrggbb", + "Xcodegen", + "Xcodeproj", + "xtask", + "xtensa", + // CSS Color 4 named-color keywords not accepted by the default English dictionary (W3C CSS Color Module Level 4). + "aliceblue", + "antiquewhite", + "blanchedalmond", + "blueviolet", + "burlywood", + "cadetblue", + "cornflowerblue", + "cornsilk", + "darkcyan", + "darkgoldenrod", + "darkgray", + "darkgreen", + "darkgrey", + "darkkhaki", + "darkmagenta", + "darkolivegreen", + "darkorange", + "darkorchid", + "darkred", + "darksalmon", + "darkseagreen", + "darkslateblue", + "darkslategray", + "darkslategrey", + "darkturquoise", + "darkviolet", + "deeppink", + "deepskyblue", + "dimgray", + "dimgrey", + "dodgerblue", + "floralwhite", + "forestgreen", + "gainsboro", + "ghostwhite", + "greenyellow", + "hotpink", + "indianred", + "lavenderblush", + "lawngreen", + "lemonchiffon", + "lightcoral", + "lightcyan", + "lightgoldenrodyellow", + "lightgray", + "lightgreen", + "lightpink", + "lightsalmon", + "lightseagreen", + "lightskyblue", + "lightslategray", + "lightslategrey", + "lightsteelblue", + "lightyellow", + "limegreen", + "mediumaquamarine", + "mediumblue", + "mediumorchid", + "mediumpurple", + "mediumseagreen", + "mediumslateblue", + "mediumspringgreen", + "mediumturquoise", + "mediumvioletred", + "midnightblue", + "mintcream", + "mistyrose", + "navajowhite", + "oldlace", + "olivedrab", + "orangered", + "palegoldenrod", + "palegreen", + "paleturquoise", + "palevioletred", + "papayawhip", + "peachpuff", + "powderblue", + "rebeccapurple", + "rosybrown", + "royalblue", + "saddlebrown", + "sandybrown", + "seagreen", + "skyblue", + "slateblue", + "slategray", + "slategrey", + "springgreen", + "steelblue", + "whitesmoke", + "yellowgreen" + ], + "ignorePaths": [ + ".github/**", + "api/cpp/docs/conf.py", + "CHANGELOG.md", + "cspell.json", + "LICENSES", + "examples/*/lang/**", + "pnpm-lock.yaml", + "pnpm-workspace.yaml", + "REUSE.toml", + "**/Cargo.lock", + "docs/common/src/utils/slint.tmLanguage.json", + "ui-libraries/material/docs/vendor/**", + ".cspell/slint-project-words.txt", + ".cspell/slint-project-words-audit.tsv", + "tools/figma-inspector/tests/figma_output.json", + "**/*.gltf", + "**/*.po", + "**/*.pot", + "**/*.svg", + "target/**", + "**/node_modules/**" + ], + "overrides": [ + { + "filename": "demos/printerdemo/lang/fr/**", + "language": "en,fr", "dictionaries": [ "slint-project-words", "fr-fr-printerdemo" @@ -355,23 +357,23 @@ "nplurals", "YCMB" ] - }, - { - "filename": "**/*.rst", - "languageId": "restructuredtext" - }, - { - "filename": "**/*.slint", - "languageId": "rust" - } - ], - "dictionaryDefinitions": [ - { - "name": "slint-project-words", - "path": "./.cspell/slint-project-words.txt", - "addWords": true - } - ], + }, + { + "filename": "**/*.rst", + "languageId": "restructuredtext" + }, + { + "filename": "**/*.slint", + "languageId": "rust" + } + ], + "dictionaryDefinitions": [ + { + "name": "slint-project-words", + "path": "./.cspell/slint-project-words.txt", + "addWords": true + } + ], "dictionaries": [ "slint-project-words" ] From 302cde58f55394092e7045ea0722c6e8ee096e19 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Mon, 15 Jun 2026 17:21:32 +0200 Subject: [PATCH 07/10] add documentation --- docs/astro/src/content/docs/reference/primitive-types.mdx | 4 ++++ internal/compiler/builtins.slint | 3 +++ 2 files changed, 7 insertions(+) diff --git a/docs/astro/src/content/docs/reference/primitive-types.mdx b/docs/astro/src/content/docs/reference/primitive-types.mdx index dccd007f674..fba5c87bfcd 100644 --- a/docs/astro/src/content/docs/reference/primitive-types.mdx +++ b/docs/astro/src/content/docs/reference/primitive-types.mdx @@ -186,6 +186,10 @@ export component FloatToString { In addition, call the in postfix style on any numeric value, for example `(-10).abs()` or `x.round()`. +#### to-string-unlocalized() -> string + +Returns a string representation of the number but not localized. This means the decimal separator will always be a dot. + ### int Signed integral number. diff --git a/internal/compiler/builtins.slint b/internal/compiler/builtins.slint index b8d14349837..6954d5aa8fd 100644 --- a/internal/compiler/builtins.slint +++ b/internal/compiler/builtins.slint @@ -2375,6 +2375,9 @@ export component Path { //! //! A string providing the commands according to the SVG path specification. //! This property can only be set in a binding and cannot be accessed in an expression. + //! **Important:** if you concatenate strings with float values, use `.to-string-unlocalized()` + //! instead of implicit string conversion otherwise the path generation will not work on systems + //! with different localization. //! //! //! ## Path Using SVG Path Elements From 5bf005cc59ccc3b0357d2af9dccd59eb1f3296cc Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Mon, 15 Jun 2026 17:22:45 +0200 Subject: [PATCH 08/10] fix merge conflict --- internal/core/string.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/core/string.rs b/internal/core/string.rs index 6512d03c033..01f7aaad234 100644 --- a/internal/core/string.rs +++ b/internal/core/string.rs @@ -551,7 +551,10 @@ pub(crate) mod ffi { /// Create a string from a number but unlocalized. /// The resulting structure must be passed to slint_shared_string_drop #[unsafe(no_mangle)] - pub unsafe extern "C" fn slint_shared_string_from_number(out: *mut SharedString, n: f64) { + pub unsafe extern "C" fn slint_shared_string_from_number_unlocalized( + out: *mut SharedString, + n: f64, + ) { let str = shared_string_from_number_unlocalized(n); unsafe { core::ptr::write(out, str) }; } From fa76d54e00f9567b52f4d3aff57c6fc4336952e8 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Mon, 15 Jun 2026 21:57:30 +0200 Subject: [PATCH 09/10] add unlocalized as allowed word --- cspell.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cspell.json b/cspell.json index 3fac359406d..3b61227f891 100644 --- a/cspell.json +++ b/cspell.json @@ -96,7 +96,8 @@ "ὈΔΥΣΣΕΎΣ", "ὀδυσσεύς", "commonmark", - "strikethroughs" + "strikethroughs", + "unlocalized" ] } ], From aa14508cb13f83086ed18fa865d0a9e03d82bfbf Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Wed, 17 Jun 2026 10:18:55 +0200 Subject: [PATCH 10/10] add comment for float to string conversion --- docs/astro/src/content/docs/reference/primitive-types.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/astro/src/content/docs/reference/primitive-types.mdx b/docs/astro/src/content/docs/reference/primitive-types.mdx index fba5c87bfcd..40fc01c8bd2 100644 --- a/docs/astro/src/content/docs/reference/primitive-types.mdx +++ b/docs/astro/src/content/docs/reference/primitive-types.mdx @@ -482,6 +482,7 @@ export component Example { `int` and `float` convert implicitly to `string`. To control how many digits appear in the result, use the [`to-fixed()` and `to-precision()`](#to-fixeddigits-int---string) member functions instead of the implicit conversion. +To always use a dot as decimal separator regardless of the locale, use [`to-string-unlocalized()`](#to-string-unlocalized---string). For the other direction, convert a `string` to a number with the [`to-float()`](#to-float---float) member function, and check whether a string holds a valid number with `is-float()`.