@@ -332,6 +332,58 @@ pub fn get_message_with_args(id: &str, ftl_args: FluentArgs) -> String {
332332 get_message_internal ( id, Some ( ftl_args) )
333333}
334334
335+ /// Helper function to safely set a FluentArgs value, preserving precision for large integers.
336+ ///
337+ /// This function handles the limitation of fluent-rs where numeric values are stored as f64,
338+ /// which can only safely represent integers in the range [-2^53+1, 2^53-1]. For values
339+ /// outside this range, they are passed as strings to preserve exact precision.
340+ ///
341+ /// # Arguments
342+ ///
343+ /// * `args` - The FluentArgs to add the value to
344+ /// * `key` - The argument key name
345+ /// * `value_str` - The string representation of the value
346+ ///
347+ /// # Implementation Notes
348+ ///
349+ /// This is a workaround for <https://github.com/projectfluent/fluent-rs/issues/337>
350+ /// Once fluent-rs supports full i64/u64 precision, this can be simplified.
351+ ///
352+ /// The function tries parsing in this order:
353+ /// 1. i64: For signed integers
354+ /// 2. u64: For unsigned integers (only if i64 parsing fails)
355+ /// 3. f64: For actual floating-point numbers
356+ /// 4. String: For non-numeric values or to preserve precision
357+ #[ inline]
358+ pub fn safe_set_fluent_arg < ' a > ( args : & mut FluentArgs < ' a > , key : & ' a str , value_str : & ' a str ) {
359+ const F64_SAFE_INTEGER_MAX : u64 = ( 1_u64 << 53 ) - 1 ;
360+
361+ if let Ok ( num_val) = value_str. parse :: < i64 > ( ) {
362+ // Signed integer - check absolute value against safe range
363+ if num_val. unsigned_abs ( ) <= F64_SAFE_INTEGER_MAX {
364+ args. set ( key, num_val) ;
365+ } else {
366+ // Beyond safe range - pass as string to preserve precision
367+ args. set ( key, value_str. to_string ( ) ) ;
368+ }
369+ } else if let Ok ( unsigned_val) = value_str. parse :: < u64 > ( ) {
370+ // Unsigned integer - check against safe range
371+ if unsigned_val <= F64_SAFE_INTEGER_MAX {
372+ // Safe to convert to i64 for FluentArgs
373+ args. set ( key, unsigned_val as i64 ) ;
374+ } else {
375+ // Beyond safe range - pass as string to preserve precision
376+ args. set ( key, value_str. to_string ( ) ) ;
377+ }
378+ } else if let Ok ( float_val) = value_str. parse :: < f64 > ( ) {
379+ // Actual floating-point number
380+ args. set ( key, float_val) ;
381+ } else {
382+ // Not a number - pass as string
383+ args. set ( key, value_str. to_string ( ) ) ;
384+ }
385+ }
386+
335387/// Function to detect system locale from environment variables
336388fn detect_system_locale ( ) -> Result < LanguageIdentifier , LocalizationError > {
337389 let locale_str = std:: env:: var ( "LANG" )
@@ -529,17 +581,12 @@ macro_rules! translate {
529581 // Case 2: Message ID with key-value arguments
530582 ( $id: expr, $( $key: expr => $value: expr) ,+ $( , ) ?) => {
531583 {
584+ // Use helper function to handle large integer precision safely.
585+ // This reduces macro expansion bloat while preserving precision.
532586 let mut args = fluent:: FluentArgs :: new( ) ;
533587 $(
534- let value_str = $value. to_string( ) ;
535- if let Ok ( num_val) = value_str. parse:: <i64 >( ) {
536- args. set( $key, num_val) ;
537- } else if let Ok ( float_val) = value_str. parse:: <f64 >( ) {
538- args. set( $key, float_val) ;
539- } else {
540- // Keep as string if not a number
541- args. set( $key, value_str) ;
542- }
588+ let value_string = $value. to_string( ) ;
589+ $crate:: locale:: safe_set_fluent_arg( & mut args, $key, & value_string) ;
543590 ) +
544591 $crate:: locale:: get_message_with_args( $id, args)
545592 }
@@ -1388,6 +1435,152 @@ invalid-syntax = This is { $missing
13881435 . join ( )
13891436 . unwrap ( ) ;
13901437 }
1438+
1439+ #[ test]
1440+ fn test_translate_macro_big_number_precision ( ) {
1441+ std:: thread:: spawn ( || {
1442+ let temp_dir = create_test_locales_dir ( ) ;
1443+ let locale = LanguageIdentifier :: from_str ( "en-US" ) . unwrap ( ) ;
1444+
1445+ init_test_localization ( & locale, temp_dir. path ( ) ) . unwrap ( ) ;
1446+
1447+ // Test with i64::MAX - should preserve precision
1448+ let result = translate ! ( "welcome" , "name" => i64 :: MAX ) ;
1449+ assert ! (
1450+ result. contains( "9223372036854775807" ) ,
1451+ "Expected i64::MAX (9223372036854775807) to be preserved, got: {result}"
1452+ ) ;
1453+ assert ! (
1454+ !result. contains( "9223372036854776000" ) ,
1455+ "Should not have precision loss, but got: {result}"
1456+ ) ;
1457+
1458+ // Test with number within safe range
1459+ let result_safe = translate ! ( "welcome" , "name" => 1000 ) ;
1460+ assert ! ( result_safe. contains( "1000" ) ) ;
1461+
1462+ // Test with number at safe boundary
1463+ const F64_SAFE_MAX : i64 = ( 1_i64 << 53 ) - 1 ;
1464+ let result_boundary = translate ! ( "welcome" , "name" => F64_SAFE_MAX ) ;
1465+ assert ! ( result_boundary. contains( & F64_SAFE_MAX . to_string( ) ) ) ;
1466+
1467+ // Test with number just beyond safe boundary
1468+ let beyond_safe = F64_SAFE_MAX + 1 ;
1469+ let result_beyond = translate ! ( "welcome" , "name" => beyond_safe) ;
1470+ assert ! ( result_beyond. contains( & beyond_safe. to_string( ) ) ) ;
1471+ } )
1472+ . join ( )
1473+ . unwrap ( ) ;
1474+ }
1475+
1476+ #[ test]
1477+ fn test_translate_macro_preserves_existing_behavior ( ) {
1478+ std:: thread:: spawn ( || {
1479+ let temp_dir = create_test_locales_dir ( ) ;
1480+ let locale = LanguageIdentifier :: from_str ( "en-US" ) . unwrap ( ) ;
1481+
1482+ init_test_localization ( & locale, temp_dir. path ( ) ) . unwrap ( ) ;
1483+
1484+ // Test string values still work
1485+ let result_str = translate ! ( "welcome" , "name" => "Alice" ) ;
1486+ assert_eq ! ( result_str, "Welcome, Alice!" ) ;
1487+
1488+ // Test small numbers still work
1489+ let result_num = translate ! ( "count-items" , "count" => 5 ) ;
1490+ assert_eq ! ( result_num, "You have 5 items" ) ;
1491+
1492+ // Test with multiple arguments
1493+ let count = 10 ;
1494+ let result_multi = translate ! (
1495+ "count-items" ,
1496+ "count" => count
1497+ ) ;
1498+ assert ! ( result_multi. contains( "10" ) ) ;
1499+ } )
1500+ . join ( )
1501+ . unwrap ( ) ;
1502+ }
1503+
1504+ #[ test]
1505+ fn test_translate_macro_u64_precision ( ) {
1506+ std:: thread:: spawn ( || {
1507+ let temp_dir = create_test_locales_dir ( ) ;
1508+ let locale = LanguageIdentifier :: from_str ( "en-US" ) . unwrap ( ) ;
1509+
1510+ init_test_localization ( & locale, temp_dir. path ( ) ) . unwrap ( ) ;
1511+
1512+ // Test u64::MAX - should preserve precision
1513+ let result = translate ! ( "welcome" , "name" => u64 :: MAX ) ;
1514+ assert ! (
1515+ result. contains( "18446744073709551615" ) ,
1516+ "Expected u64::MAX (18446744073709551615) to be preserved, got: {result}"
1517+ ) ;
1518+ assert ! (
1519+ !result. contains( "18446744073709552000" ) ,
1520+ "Should not have precision loss, but got: {result}"
1521+ ) ;
1522+
1523+ // Test i64::MAX + 1 as u64 - should preserve precision
1524+ let big_u64 = i64:: MAX as u64 + 1 ;
1525+ let result2 = translate ! ( "welcome" , "name" => big_u64) ;
1526+ assert ! (
1527+ result2. contains( "9223372036854775808" ) ,
1528+ "Expected i64::MAX+1 (9223372036854775808) to be preserved, got: {result2}"
1529+ ) ;
1530+ assert ! (
1531+ !result2. contains( "9223372036854776000" ) ,
1532+ "Should not have precision loss, but got: {result2}"
1533+ ) ;
1534+
1535+ // Test u64 within safe range - should work as number
1536+ let safe_u64: u64 = 1000 ;
1537+ let result_safe = translate ! ( "welcome" , "name" => safe_u64) ;
1538+ assert ! ( result_safe. contains( "1000" ) ) ;
1539+
1540+ // Test u64 at boundary of safe range
1541+ const F64_SAFE_MAX : u64 = ( 1_u64 << 53 ) - 1 ;
1542+ let result_boundary = translate ! ( "welcome" , "name" => F64_SAFE_MAX ) ;
1543+ assert ! ( result_boundary. contains( & F64_SAFE_MAX . to_string( ) ) ) ;
1544+
1545+ // Test u64 just beyond safe boundary
1546+ let beyond_safe = F64_SAFE_MAX + 1 ;
1547+ let result_beyond = translate ! ( "welcome" , "name" => beyond_safe) ;
1548+ assert ! (
1549+ result_beyond. contains( & beyond_safe. to_string( ) ) ,
1550+ "Expected {beyond_safe} to be preserved, got: {result_beyond}"
1551+ ) ;
1552+ } )
1553+ . join ( )
1554+ . unwrap ( ) ;
1555+ }
1556+
1557+ #[ test]
1558+ fn test_translate_macro_negative_large_numbers ( ) {
1559+ std:: thread:: spawn ( || {
1560+ let temp_dir = create_test_locales_dir ( ) ;
1561+ let locale = LanguageIdentifier :: from_str ( "en-US" ) . unwrap ( ) ;
1562+
1563+ init_test_localization ( & locale, temp_dir. path ( ) ) . unwrap ( ) ;
1564+
1565+ // Test i64::MIN - should preserve precision
1566+ let result = translate ! ( "welcome" , "name" => i64 :: MIN ) ;
1567+ assert ! (
1568+ result. contains( "-9223372036854775808" ) ,
1569+ "Expected i64::MIN to be preserved, got: {result}"
1570+ ) ;
1571+
1572+ // Test large negative number beyond safe range
1573+ const F64_SAFE_MAX : i64 = ( 1_i64 << 53 ) - 1 ;
1574+ let large_negative = -F64_SAFE_MAX - 1 ;
1575+ let result2 = translate ! ( "welcome" , "name" => large_negative) ;
1576+ assert ! (
1577+ result2. contains( & large_negative. to_string( ) ) ,
1578+ "Expected {large_negative} to be preserved, got: {result2}"
1579+ ) ;
1580+ } )
1581+ . join ( )
1582+ . unwrap ( ) ;
1583+ }
13911584}
13921585
13931586#[ cfg( all( test, not( debug_assertions) ) ) ]
@@ -1414,3 +1607,4 @@ mod fhs_tests {
14141607 assert_eq ! ( result, share_dir) ;
14151608 }
14161609}
1610+
0 commit comments