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:
