This is an automated email from the ASF dual-hosted git repository.

jamesfredley pushed a commit to branch fix/negative-number-locale-15178
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 7e3a86252019cfc9a06308ec9e4f7399b0427c16
Author: James Fredley <[email protected]>
AuthorDate: Sat Feb 28 19:08:43 2026 -0500

    fix: normalize Unicode minus sign to ASCII in GSP number formatting for 
locale compatibility
    
    Some locales (e.g., Norwegian nb-NO) use Unicode minus U+2212 instead of
    ASCII hyphen-minus U+002D when formatting negative numbers. This causes
    issues in HTML number input fields that expect standard ASCII minus.
    
    Normalize DecimalFormatSymbols.minusSign to ASCII '-' in FormatTagLib,
    ValidationTagLib, and FormFieldsTagLib before formatting numbers.
    
    Fixes #15178
    
    Assisted-by: Claude Code <[email protected]>
---
 .../plugin/formfields/FormFieldsTagLib.groovy      | 12 +++++++++-
 .../grails/plugins/web/taglib/FormatTagLib.groovy  | 14 ++++++++++-
 .../plugins/web/taglib/ValidationTagLib.groovy     |  8 ++++++-
 .../org/grails/web/taglib/FormatTagLibSpec.groovy  | 11 +++++++++
 .../org/grails/web/taglib/FormatTagLibTests.groovy | 28 ++++++++++++++++++++++
 .../grails/web/taglib/ValidationTagLibSpec.groovy  | 17 +++++++++++++
 6 files changed, 87 insertions(+), 3 deletions(-)

diff --git 
a/grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy
 
b/grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy
index 1d1c25d35f..cd46eaa716 100644
--- 
a/grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy
+++ 
b/grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy
@@ -19,6 +19,8 @@
 package grails.plugin.formfields
 
 import java.sql.Blob
+import java.text.DecimalFormat
+import java.text.DecimalFormatSymbols
 import java.text.NumberFormat
 import java.time.Instant
 import java.time.LocalDate
@@ -833,7 +835,15 @@ class FormFieldsTagLib {
 
     @CompileStatic
     protected NumberFormat getNumberFormatter() {
-        NumberFormat.getInstance(getLocale())
+        NumberFormat numberFormat = NumberFormat.getInstance(getLocale())
+        // Normalize Unicode minus sign (U+2212) to ASCII hyphen-minus (U+002D)
+        // for HTML compatibility (fixes grails-core#15178)
+        if (numberFormat instanceof DecimalFormat) {
+            DecimalFormatSymbols symbols = ((DecimalFormat) 
numberFormat).decimalFormatSymbols
+            symbols.minusSign = '-' as char
+            ((DecimalFormat) numberFormat).decimalFormatSymbols = symbols
+        }
+        return numberFormat
     }
 
     @CompileStatic
diff --git 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormatTagLib.groovy
 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormatTagLib.groovy
index cbdbc072bf..7bb4cbeed8 100644
--- 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormatTagLib.groovy
+++ 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormatTagLib.groovy
@@ -327,13 +327,25 @@ class FormatTagLib implements TagLibrary {
             number = decimalFormat.parse(number as String)
         }
 
+        // Normalize Unicode minus sign (U+2212) to ASCII hyphen-minus (U+002D)
+        // for HTML compatibility (fixes grails-core#15178)
+        DecimalFormatSymbols formatSymbols = decimalFormat.decimalFormatSymbols
+        formatSymbols.minusSign = '-' as char
+        decimalFormat.decimalFormatSymbols = formatSymbols
+
         def formatted
         try {
             formatted = decimalFormat.format(number)
         }
         catch (ArithmeticException e) {
             // if roundingMode is UNNECESSARY and ArithemeticException raises, 
just return original number formatted with default number formatting
-            formatted = NumberFormat.getNumberInstance(locale).format(number)
+            NumberFormat fallbackFormat = 
NumberFormat.getNumberInstance(locale)
+            if (fallbackFormat instanceof DecimalFormat) {
+                DecimalFormatSymbols fallbackSymbols = ((DecimalFormat) 
fallbackFormat).decimalFormatSymbols
+                fallbackSymbols.minusSign = '-' as char
+                ((DecimalFormat) fallbackFormat).decimalFormatSymbols = 
fallbackSymbols
+            }
+            formatted = fallbackFormat.format(number)
         }
         return formatted
     }
diff --git 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ValidationTagLib.groovy
 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ValidationTagLib.groovy
index c8a3f376b9..adeca434a4 100644
--- 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ValidationTagLib.groovy
+++ 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ValidationTagLib.groovy
@@ -471,7 +471,12 @@ class ValidationTagLib implements TagLibrary {
         PropertyEditor editor = registry.findCustomEditor(value.getClass(), 
propertyPath)
         if (editor) {
             editor.setValue(value)
-            return !(value instanceof Number) ? editor.asText?.encodeAsHTML() 
: editor.asText
+            String formattedValue = editor.asText
+            if (value instanceof Number && formattedValue != null) {
+                DecimalFormatSymbols formatSymbols = new 
DecimalFormatSymbols(webRequest.getLocale())
+                formattedValue = 
formattedValue.replace(formatSymbols.minusSign, '-' as char)
+            }
+            return !(value instanceof Number) ? formattedValue?.encodeAsHTML() 
: formattedValue
         }
 
         if (value instanceof Number) {
@@ -482,6 +487,7 @@ class ValidationTagLib implements TagLibrary {
 
             def locale = webRequest.getLocale()
             def dcfs = locale ? new DecimalFormatSymbols(locale) : new 
DecimalFormatSymbols()
+            dcfs.minusSign = '-' as char
             def decimalFormat = new DecimalFormat(pattern, dcfs)
             value = decimalFormat.format(value)
         }
diff --git 
a/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/FormatTagLibSpec.groovy
 
b/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/FormatTagLibSpec.groovy
index f652a5c0c0..7b2c4e2186 100644
--- 
a/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/FormatTagLibSpec.groovy
+++ 
b/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/FormatTagLibSpec.groovy
@@ -21,6 +21,7 @@ package org.grails.web.taglib
 import grails.testing.web.taglib.TagLibUnitTest
 import org.grails.plugins.web.taglib.FormatTagLib
 import spock.lang.IgnoreIf
+import spock.lang.Issue
 import spock.lang.Requires
 import spock.lang.Specification
 
@@ -57,4 +58,14 @@ class FormatTagLibSpec extends Specification implements 
TagLibUnitTest<FormatTag
         expect:
         "3,12${new String([160] as char[])}\$" == 
applyTemplate('<g:formatNumber type="currency" currencyCode="USD" 
number="${number}" locale="fi_FI" />',  [number: number])
     }
+
+    @Issue('https://github.com/apache/grails-core/issues/15178')
+    void "formatNumber uses ASCII minus sign for negative numbers with 
Norwegian locale"() {
+        when:
+        def result = applyTemplate('<g:formatNumber number="${n}" 
type="number" locale="nb_NO"/>', [n: -42])
+
+        then: "ASCII minus (U+002D) is used, not Unicode minus (U+2212)"
+        result.contains('-')
+        !result.contains('\u2212')
+    }
 }
diff --git 
a/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/FormatTagLibTests.groovy
 
b/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/FormatTagLibTests.groovy
index 9c97cc609a..9026984113 100644
--- 
a/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/FormatTagLibTests.groovy
+++ 
b/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/FormatTagLibTests.groovy
@@ -331,4 +331,32 @@ class FormatTagLibTests extends AbstractGrailsTagTests {
         def template = '<g:formatNumber number="${number}" nan="n/a"/>'
         assertOutputEquals("n/a", template, [number: number])
     }
+
+    @Issue('https://github.com/apache/grails-core/issues/15178')
+    @Test
+    void testFormatNumberNegativeWithNorwegianLocale() {
+        def template = '<g:formatNumber number="${myNumber}" type="number" 
locale="nb_NO"/>'
+        def result = applyTemplate(template, [myNumber: -42])
+        // Verify ASCII minus (U+002D) is used, not Unicode minus (U+2212)
+        assert result.charAt(0) == '-' as char
+        assert !result.contains('\u2212')
+    }
+
+    @Issue('https://github.com/apache/grails-core/issues/15178')
+    @Test
+    void testFormatNumberNegativeLongWithNorwegianLocale() {
+        def template = '<g:formatNumber number="${myNumber}" format="0" 
locale="nb_NO"/>'
+        def result = applyTemplate(template, [myNumber: -123456L])
+        assert result.contains('-')
+        assert !result.contains('\u2212')
+    }
+
+    @Issue('https://github.com/apache/grails-core/issues/15178')
+    @Test
+    void testFormatNumberNegativeBigDecimalWithNorwegianLocale() {
+        def template = '<g:formatNumber number="${myNumber}" format="0.00" 
locale="nb_NO"/>'
+        def result = applyTemplate(template, [myNumber: new 
BigDecimal("-99.95")])
+        assert result.contains('-')
+        assert !result.contains('\u2212')
+    }
 }
diff --git 
a/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/ValidationTagLibSpec.groovy
 
b/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/ValidationTagLibSpec.groovy
index c62e1ddbf7..333af4f3c0 100644
--- 
a/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/ValidationTagLibSpec.groovy
+++ 
b/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/ValidationTagLibSpec.groovy
@@ -31,6 +31,7 @@ import 
org.springframework.beans.factory.support.RootBeanDefinition
 import org.springframework.context.MessageSourceResolvable
 import org.springframework.context.i18n.LocaleContextHolder
 import org.springframework.validation.FieldError
+import spock.lang.Issue
 import spock.lang.PendingFeature
 import spock.lang.Specification
 
@@ -259,6 +260,22 @@ class ValidationTagLibSpec extends Specification 
implements TagLibUnitTest<Valid
         applyTemplate(template, [book:b]) == "1.045,456"
     }
 
+    @Issue('https://github.com/apache/grails-core/issues/15178')
+    void testFieldValueTagWithNorwegianLocaleUsesAsciiMinus() {
+        given:
+        def b = new ValidationTagLibBook()
+        b.usPrice = -42.5
+        def template = '<g:fieldValue bean="${book}" field="usPrice" />'
+
+        when:
+        webRequest.currentRequest.addPreferredLocale(new Locale('nb', 'NO'))
+        def result = applyTemplate(template, [book: b])
+
+        then:
+        result.contains('-')
+        !result.contains('\u2212')
+    }
+
     @PendingFeature // Was valid for JVM lower than 14 because space is 
converted to narrow no-break space 8239 and is not encoded
     void testFieldValueTagWithFrenchLocaleInTextField() {
         given:

Reply via email to