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

jamesfredley pushed a commit to branch test/expand-integration-test-coverage
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 43672ec977a5cf7c12e6b543f5d682782ea97b4f
Author: James Fredley <[email protected]>
AuthorDate: Sun Jan 25 22:05:52 2026 -0500

    Add data binding, codecs, and i18n integration tests
    
    - Add DataBindingSpec with 20 tests for request parameter binding
    - Tests type coercion, nested objects, collections, dates
    - Add CodecsSpec with 15 tests for encoding/decoding
    - Tests HTML, URL, Base64, JavaScript, MD5, SHA codecs
    - Add I18nSpec with 17 tests for internationalization
    - Tests message resolution, locale switching, pluralization
    - Includes message bundles for English, French, German
---
 .../binding/AdvancedDataBindingController.groovy   | 297 ++++++++++
 .../codecs/CodecTestController.groovy              | 281 ++++++++++
 .../functionaltests/i18n/I18nTestController.groovy | 227 ++++++++
 .../domain/functionaltests/binding/Address.groovy  |  39 ++
 .../functionaltests/binding/Contributor.groovy     |  33 ++
 .../domain/functionaltests/binding/Employee.groovy |  59 ++
 .../domain/functionaltests/binding/Project.groovy  |  36 ++
 .../domain/functionaltests/binding/Team.groovy     |  34 ++
 .../functionaltests/binding/TeamMember.groovy      |  36 ++
 .../app1/grails-app/i18n/messages.properties       |   9 +
 .../app1/grails-app/i18n/messages_de.properties    |   9 +
 .../app1/grails-app/i18n/messages_fr.properties    |   9 +
 .../binding/AdvancedDataBindingSpec.groovy         | 595 ++++++++++++++++++++
 .../codecs/SecurityCodecsSpec.groovy               | 560 +++++++++++++++++++
 .../i18n/InternationalizationSpec.groovy           | 606 +++++++++++++++++++++
 15 files changed, 2830 insertions(+)

diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/binding/AdvancedDataBindingController.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/binding/AdvancedDataBindingController.groovy
new file mode 100644
index 0000000000..e333e50c30
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/binding/AdvancedDataBindingController.groovy
@@ -0,0 +1,297 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package functionaltests.binding
+
+import grails.converters.JSON
+import grails.databinding.SimpleMapDataBindingSource
+import grails.validation.Validateable
+import grails.web.RequestParameter
+
+/**
+ * Controller for testing advanced data binding features.
+ */
+class AdvancedDataBindingController {
+    
+    def grailsWebDataBinder
+    
+    /**
+     * Test basic map-based binding with nested objects.
+     */
+    def bindEmployee() {
+        def employee = new Employee(params)
+        render([
+            firstName: employee.firstName,
+            lastName: employee.lastName,
+            email: employee.email,
+            salary: employee.salary,
+            homeAddress: employee.homeAddress ? [
+                street: employee.homeAddress.street,
+                city: employee.homeAddress.city,
+                state: employee.homeAddress.state
+            ] : null
+        ] as JSON)
+    }
+    
+    /**
+     * Test @BindUsing annotation - email should be lowercased.
+     */
+    def bindWithBindUsing() {
+        def employee = new Employee(params)
+        render([
+            email: employee.email,
+            originalEmail: params.email
+        ] as JSON)
+    }
+    
+    /**
+     * Test @BindingFormat annotation for dates.
+     */
+    def bindWithDateFormat() {
+        def employee = new Employee(params)
+        render([
+            hireDate: employee.hireDate?.format('yyyy-MM-dd'),
+            birthDate: employee.birthDate?.format('yyyy-MM-dd'),
+            hireDateInput: params.hireDate,
+            birthDateInput: params.birthDate
+        ] as JSON)
+    }
+    
+    /**
+     * Test collection binding to List.
+     */
+    def bindTeamWithMembers() {
+        def team = new Team(params)
+        render([
+            name: team.name,
+            members: team.members?.findAll { it != null }?.collect { [name: 
it.name, role: it.role] } ?: []
+        ] as JSON)
+    }
+    
+    /**
+     * Test Map-based collection binding.
+     */
+    def bindProjectWithContributors() {
+        def project = new Project(params)
+        def contributorsMap = [:]
+        project.contributors?.each { key, contributor ->
+            contributorsMap[key] = [name: contributor.name, expertise: 
contributor.expertise]
+        }
+        render([
+            name: project.name,
+            contributors: contributorsMap
+        ] as JSON)
+    }
+    
+    /**
+     * Test binding with @RequestParameter annotation.
+     */
+    def bindWithRequestParameter(
+        @RequestParameter('firstName') String givenName,
+        @RequestParameter('lastName') String familyName,
+        Integer age
+    ) {
+        render([
+            givenName: givenName,
+            familyName: familyName,
+            age: age
+        ] as JSON)
+    }
+    
+    /**
+     * Test bindData method with include/exclude.
+     */
+    def bindWithIncludeExclude() {
+        def employee = new Employee()
+        // Only bind firstName and lastName, exclude email
+        bindData(employee, params, [include: ['firstName', 'lastName']])
+        render([
+            firstName: employee.firstName,
+            lastName: employee.lastName,
+            email: employee.email,  // Should be null
+            salary: employee.salary // Should be null
+        ] as JSON)
+    }
+    
+    /**
+     * Test selective property binding using subscript operator.
+     */
+    def bindSelectiveProperties() {
+        def employee = new Employee()
+        employee.properties['firstName', 'lastName'] = params
+        render([
+            firstName: employee.firstName,
+            lastName: employee.lastName,
+            email: employee.email,  // Should be null
+            salary: employee.salary // Should be null
+        ] as JSON)
+    }
+    
+    /**
+     * Test direct data binder usage in service-like scenario.
+     */
+    def bindUsingDirectBinder() {
+        def employee = new Employee()
+        grailsWebDataBinder.bind(employee, params as 
SimpleMapDataBindingSource)
+        render([
+            firstName: employee.firstName,
+            lastName: employee.lastName,
+            email: employee.email
+        ] as JSON)
+    }
+    
+    /**
+     * Test type conversion errors.
+     */
+    def bindWithTypeConversion(Integer salary, String firstName) {
+        def hasErrors = hasErrors()
+        render([
+            salary: salary,
+            firstName: firstName,
+            hasErrors: hasErrors,
+            errorCount: errors?.errorCount ?: 0
+        ] as JSON)
+    }
+    
+    /**
+     * Test command object binding.
+     */
+    def bindCommandObject(EmployeeCommand cmd) {
+        render([
+            firstName: cmd.firstName,
+            lastName: cmd.lastName,
+            email: cmd.email,
+            valid: cmd.validate(),
+            errors: cmd.errors?.allErrors?.collect { it.field } ?: []
+        ] as JSON)
+    }
+    
+    /**
+     * Test nested command object binding.
+     */
+    def bindNestedCommandObject(ContactCommand cmd) {
+        render([
+            name: cmd.name,
+            address: cmd.address ? [
+                street: cmd.address.street,
+                city: cmd.address.city
+            ] : null,
+            valid: cmd.validate()
+        ] as JSON)
+    }
+    
+    /**
+     * Test binding JSON request body to command object.
+     */
+    def bindJsonBody(EmployeeCommand cmd) {
+        render([
+            firstName: cmd.firstName,
+            lastName: cmd.lastName,
+            email: cmd.email,
+            valid: cmd.validate()
+        ] as JSON)
+    }
+    
+    /**
+     * Test multiple command objects.
+     */
+    def bindMultipleCommandObjects(EmployeeCommand employee, AddressCommand 
address) {
+        render([
+            employee: [
+                firstName: employee.firstName,
+                lastName: employee.lastName
+            ],
+            address: [
+                street: address.street,
+                city: address.city
+            ]
+        ] as JSON)
+    }
+    
+    /**
+     * Test empty string to null conversion.
+     */
+    def bindEmptyStrings() {
+        def employee = new Employee(params)
+        render([
+            firstName: employee.firstName,
+            firstNameIsNull: employee.firstName == null,
+            lastName: employee.lastName,
+            lastNameIsNull: employee.lastName == null
+        ] as JSON)
+    }
+    
+    /**
+     * Test string trimming during binding.
+     */
+    def bindWithTrimming() {
+        def employee = new Employee(params)
+        render([
+            firstName: employee.firstName,
+            firstNameLength: employee.firstName?.length() ?: 0,
+            originalFirstName: params.firstName,
+            originalLength: params.firstName?.length() ?: 0
+        ] as JSON)
+    }
+}
+
+/**
+ * Command object for employee data.
+ */
+class EmployeeCommand implements Validateable {
+    String firstName
+    String lastName
+    String email
+    
+    static constraints = {
+        firstName blank: false
+        lastName blank: false
+        email nullable: true, email: true
+    }
+}
+
+/**
+ * Command object for address data.
+ */
+class AddressCommand implements Validateable {
+    String street
+    String city
+    String state
+    String zipCode
+    
+    static constraints = {
+        street nullable: true
+        city nullable: true
+        state nullable: true
+        zipCode nullable: true
+    }
+}
+
+/**
+ * Command object with nested address.
+ */
+class ContactCommand implements Validateable {
+    String name
+    AddressCommand address
+    
+    static constraints = {
+        name blank: false
+        address nullable: true
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/codecs/CodecTestController.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/codecs/CodecTestController.groovy
new file mode 100644
index 0000000000..ff40193ae5
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/codecs/CodecTestController.groovy
@@ -0,0 +1,281 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package functionaltests.codecs
+
+import grails.converters.JSON
+import grails.core.GrailsApplication
+
+/**
+ * Controller for testing codec functionality in an integration context.
+ * Tests various encoding/decoding methods available in Grails.
+ */
+class CodecTestController {
+
+    GrailsApplication grailsApplication
+
+    /**
+     * Test HTML encoding to prevent XSS attacks.
+     */
+    def encodeHtml() {
+        def input = params.input ?: '<script>alert("XSS")</script>'
+        def encoded = input.encodeAsHTML()
+        render([
+            input: input,
+            encoded: encoded,
+            decodedBack: encoded.decodeHTML()
+        ] as JSON)
+    }
+
+    /**
+     * Test URL encoding for safe URL parameters.
+     */
+    def encodeUrl() {
+        def input = params.input ?: 'hello world&foo=bar'
+        def encoded = input.encodeAsURL()
+        render([
+            input: input,
+            encoded: encoded,
+            decodedBack: encoded.decodeURL()
+        ] as JSON)
+    }
+
+    /**
+     * Test Base64 encoding/decoding.
+     */
+    def encodeBase64() {
+        def input = params.input ?: 'Hello, World!'
+        def encoded = input.encodeAsBase64()
+        def decoded = encoded.decodeBase64()
+        render([
+            input: input,
+            encoded: encoded,
+            decodedBack: new String(decoded)
+        ] as JSON)
+    }
+
+    /**
+     * Test Base64 encoding with binary data.
+     */
+    def encodeBase64Binary() {
+        byte[] data = [0x48, 0x65, 0x6C, 0x6C, 0x6F] as byte[] // "Hello"
+        def encoded = data.encodeAsBase64()
+        def decoded = encoded.decodeBase64()
+        render([
+            originalBytes: data.collect { it },
+            encoded: encoded,
+            decodedBytes: decoded.collect { it }
+        ] as JSON)
+    }
+
+    /**
+     * Test MD5 hashing.
+     */
+    def encodeMd5() {
+        def input = params.input ?: 'password123'
+        def hash = input.encodeAsMD5()
+        render([
+            input: input,
+            md5Hash: hash,
+            hashLength: hash.length()
+        ] as JSON)
+    }
+
+    /**
+     * Test MD5 bytes hashing.
+     */
+    def encodeMd5Bytes() {
+        def input = params.input ?: 'password123'
+        def hashBytes = input.encodeAsMD5Bytes()
+        render([
+            input: input,
+            md5Bytes: hashBytes.collect { it & 0xFF }, // Convert to unsigned
+            bytesLength: hashBytes.length
+        ] as JSON)
+    }
+
+    /**
+     * Test SHA1 hashing.
+     */
+    def encodeSha1() {
+        def input = params.input ?: 'password123'
+        def hash = input.encodeAsSHA1()
+        render([
+            input: input,
+            sha1Hash: hash,
+            hashLength: hash.length()
+        ] as JSON)
+    }
+
+    /**
+     * Test SHA1 bytes hashing.
+     */
+    def encodeSha1Bytes() {
+        def input = params.input ?: 'password123'
+        def hashBytes = input.encodeAsSHA1Bytes()
+        render([
+            input: input,
+            sha1Bytes: hashBytes.collect { it & 0xFF },
+            bytesLength: hashBytes.length
+        ] as JSON)
+    }
+
+    /**
+     * Test SHA256 hashing.
+     */
+    def encodeSha256() {
+        def input = params.input ?: 'password123'
+        def hash = input.encodeAsSHA256()
+        render([
+            input: input,
+            sha256Hash: hash,
+            hashLength: hash.length()
+        ] as JSON)
+    }
+
+    /**
+     * Test SHA256 bytes hashing.
+     */
+    def encodeSha256Bytes() {
+        def input = params.input ?: 'password123'
+        def hashBytes = input.encodeAsSHA256Bytes()
+        render([
+            input: input,
+            sha256Bytes: hashBytes.collect { it & 0xFF },
+            bytesLength: hashBytes.length
+        ] as JSON)
+    }
+
+    /**
+     * Test Hex encoding/decoding.
+     */
+    def encodeHex() {
+        def input = params.input ?: 'Hello'
+        def encoded = input.bytes.encodeAsHex()
+        def decoded = encoded.decodeHex()
+        render([
+            input: input,
+            hexEncoded: encoded,
+            decodedBack: new String(decoded)
+        ] as JSON)
+    }
+
+    /**
+     * Test JavaScript encoding for safe inclusion in JS strings.
+     */
+    def encodeJavaScript() {
+        def input = params.input ?: "alert('hello');\nvar x = \"test\";"
+        def encoded = input.encodeAsJavaScript()
+        render([
+            input: input,
+            encoded: encoded
+        ] as JSON)
+    }
+
+    /**
+     * Test raw output (no encoding).
+     */
+    def encodeRaw() {
+        def input = params.input ?: '<b>Bold</b>'
+        def raw = input.encodeAsRaw()
+        render([
+            input: input,
+            raw: raw.toString(),
+            rawClass: raw.getClass().name
+        ] as JSON)
+    }
+
+    /**
+     * Test multiple encodings combined (HTML then Base64).
+     */
+    def multipleEncodings() {
+        def input = params.input ?: '<script>alert(1)</script>'
+        def htmlEncoded = input.encodeAsHTML()
+        def base64Encoded = htmlEncoded.encodeAsBase64()
+        def base64Decoded = base64Encoded.decodeBase64()
+        def htmlDecoded = new String(base64Decoded).decodeHTML()
+        render([
+            input: input,
+            htmlEncoded: htmlEncoded,
+            base64Encoded: base64Encoded,
+            fullyDecoded: htmlDecoded
+        ] as JSON)
+    }
+
+    /**
+     * Test encoding with special characters.
+     */
+    def encodeSpecialChars() {
+        def input = params.input ?: '日本語 & émoji 👍 <tag>'
+        render([
+            input: input,
+            htmlEncoded: input.encodeAsHTML(),
+            urlEncoded: input.encodeAsURL(),
+            base64Encoded: input.encodeAsBase64()
+        ] as JSON)
+    }
+
+    /**
+     * Test encoding null values - should not throw errors.
+     */
+    def encodeNull() {
+        String nullString = null
+        render([
+            nullBase64: nullString?.encodeAsBase64(),
+            nullMd5: nullString?.encodeAsMD5(),
+            nullHtml: nullString?.encodeAsHTML()
+        ] as JSON)
+    }
+
+    /**
+     * Test encoding empty strings.
+     */
+    def encodeEmpty() {
+        def input = ''
+        render([
+            input: input,
+            base64Encoded: input.encodeAsBase64(),
+            md5Hash: input.encodeAsMD5(),
+            sha256Hash: input.encodeAsSHA256(),
+            htmlEncoded: input.encodeAsHTML()
+        ] as JSON)
+    }
+
+    /**
+     * Test hash consistency - same input should produce same hash.
+     */
+    def hashConsistency() {
+        def input = params.input ?: 'consistent-input'
+        def md5_1 = input.encodeAsMD5()
+        def md5_2 = input.encodeAsMD5()
+        def sha1_1 = input.encodeAsSHA1()
+        def sha1_2 = input.encodeAsSHA1()
+        def sha256_1 = input.encodeAsSHA256()
+        def sha256_2 = input.encodeAsSHA256()
+        render([
+            input: input,
+            md5Consistent: md5_1 == md5_2,
+            sha1Consistent: sha1_1 == sha1_2,
+            sha256Consistent: sha256_1 == sha256_2,
+            md5Hash: md5_1,
+            sha1Hash: sha1_1,
+            sha256Hash: sha256_1
+        ] as JSON)
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/i18n/I18nTestController.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/i18n/I18nTestController.groovy
new file mode 100644
index 0000000000..25fc947264
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/i18n/I18nTestController.groovy
@@ -0,0 +1,227 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package functionaltests.i18n
+
+import grails.converters.JSON
+import org.springframework.context.MessageSource
+import org.springframework.context.i18n.LocaleContextHolder
+import org.springframework.web.servlet.LocaleResolver
+import org.springframework.web.servlet.support.RequestContextUtils
+
+/**
+ * Controller for testing internationalization (i18n) features.
+ * Tests locale switching, message resolution, and formatting.
+ */
+class I18nTestController {
+
+    MessageSource messageSource
+    LocaleResolver localeResolver
+
+    /**
+     * Get a simple message using the current locale.
+     */
+    def getMessage() {
+        def code = params.code ?: 'app.welcome'
+        def locale = resolveLocale()
+        def message = messageSource.getMessage(code, null, locale)
+        render([
+            code: code,
+            locale: locale.toString(),
+            message: message
+        ] as JSON)
+    }
+
+    /**
+     * Get a message with arguments.
+     */
+    def getMessageWithArgs() {
+        def code = params.code ?: 'app.greeting'
+        def arg = params.arg ?: 'World'
+        def locale = resolveLocale()
+        def message = messageSource.getMessage(code, [arg] as Object[], locale)
+        render([
+            code: code,
+            arg: arg,
+            locale: locale.toString(),
+            message: message
+        ] as JSON)
+    }
+
+    /**
+     * Get a message with choice format (pluralization).
+     */
+    def getChoiceMessage() {
+        def count = params.int('count') ?: 0
+        def locale = resolveLocale()
+        def message = messageSource.getMessage('app.itemcount', [count] as 
Object[], locale)
+        render([
+            code: 'app.itemcount',
+            count: count,
+            locale: locale.toString(),
+            message: message
+        ] as JSON)
+    }
+
+    /**
+     * Get a date formatted message.
+     */
+    def getDateMessage() {
+        def locale = resolveLocale()
+        def date = new Date()
+        def message = messageSource.getMessage('app.date.format', [date] as 
Object[], locale)
+        render([
+            code: 'app.date.format',
+            date: date.format('yyyy-MM-dd'),
+            locale: locale.toString(),
+            message: message
+        ] as JSON)
+    }
+
+    /**
+     * Get a currency formatted message.
+     */
+    def getCurrencyMessage() {
+        def amount = params.double('amount') ?: 1234.56
+        def locale = resolveLocale()
+        def message = messageSource.getMessage('app.currency', [amount] as 
Object[], locale)
+        render([
+            code: 'app.currency',
+            amount: amount,
+            locale: locale.toString(),
+            message: message
+        ] as JSON)
+    }
+
+    /**
+     * Get a percentage formatted message.
+     */
+    def getPercentMessage() {
+        def value = params.double('value') ?: 0.75
+        def locale = resolveLocale()
+        def message = messageSource.getMessage('app.percent', [value] as 
Object[], locale)
+        render([
+            code: 'app.percent',
+            value: value,
+            locale: locale.toString(),
+            message: message
+        ] as JSON)
+    }
+
+    /**
+     * Test message using controller's message() method.
+     */
+    def useControllerMessage() {
+        def code = params.code ?: 'app.welcome'
+        def locale = resolveLocale()
+        // Use the controller's built-in message method
+        def msg = message(code: code, locale: locale)
+        render([
+            code: code,
+            locale: locale.toString(),
+            message: msg
+        ] as JSON)
+    }
+
+    /**
+     * Test message with default value.
+     */
+    def getMessageWithDefault() {
+        def code = params.code ?: 'non.existent.code'
+        def defaultMsg = params.defaultMsg ?: 'Default Message'
+        def locale = resolveLocale()
+        def message = messageSource.getMessage(code, null, defaultMsg, locale)
+        render([
+            code: code,
+            defaultMsg: defaultMsg,
+            locale: locale.toString(),
+            message: message
+        ] as JSON)
+    }
+
+    /**
+     * Get validation error messages.
+     */
+    def getValidationMessages() {
+        def locale = resolveLocale()
+        def messages = [:]
+        messages.blank = messageSource.getMessage('default.blank.message', 
['name', 'User'] as Object[], locale)
+        messages.nullable = messageSource.getMessage('default.null.message', 
['email', 'User'] as Object[], locale)
+        messages.paginate_prev = 
messageSource.getMessage('default.paginate.prev', null, locale)
+        messages.paginate_next = 
messageSource.getMessage('default.paginate.next', null, locale)
+        render([
+            locale: locale.toString(),
+            messages: messages
+        ] as JSON)
+    }
+
+    /**
+     * Test multiple messages at once.
+     */
+    def getMultipleMessages() {
+        def locale = resolveLocale()
+        def messages = [:]
+        messages.welcome = messageSource.getMessage('app.welcome', null, 
locale)
+        messages.greeting = messageSource.getMessage('app.greeting', ['User'] 
as Object[], locale)
+        messages.farewell = messageSource.getMessage('app.farewell', ['User'] 
as Object[], locale)
+        render([
+            locale: locale.toString(),
+            messages: messages
+        ] as JSON)
+    }
+
+    /**
+     * Get current locale information.
+     */
+    def getCurrentLocale() {
+        def locale = resolveLocale()
+        render([
+            language: locale.language,
+            country: locale.country,
+            displayName: locale.displayName,
+            full: locale.toString()
+        ] as JSON)
+    }
+
+    /**
+     * Test locale from Accept-Language header.
+     */
+    def getLocaleFromHeader() {
+        def requestLocale = request.locale
+        def contextLocale = LocaleContextHolder.locale
+        render([
+            requestLocale: requestLocale?.toString(),
+            contextLocale: contextLocale?.toString()
+        ] as JSON)
+    }
+
+    private Locale resolveLocale() {
+        def lang = params.lang
+        if (lang) {
+            // Parse locale from parameter
+            def parts = lang.split('_')
+            if (parts.length == 2) {
+                return new Locale(parts[0], parts[1])
+            }
+            return new Locale(lang)
+        }
+        // Fall back to request locale or default
+        return request.locale ?: Locale.ENGLISH
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Address.groovy
 
b/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Address.groovy
new file mode 100644
index 0000000000..9c628c4f84
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Address.groovy
@@ -0,0 +1,39 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package functionaltests.binding
+
+/**
+ * Address domain class for data binding tests.
+ */
+class Address {
+    String street
+    String city
+    String state
+    String zipCode
+    String country
+
+    static constraints = {
+        street nullable: true
+        city nullable: true
+        state nullable: true
+        zipCode nullable: true
+        country nullable: true
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Contributor.groovy
 
b/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Contributor.groovy
new file mode 100644
index 0000000000..202d3d6146
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Contributor.groovy
@@ -0,0 +1,33 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package functionaltests.binding
+
+/**
+ * Contributor domain class for testing Map-based binding.
+ */
+class Contributor {
+    String name
+    String expertise
+    
+    static constraints = {
+        name nullable: true
+        expertise nullable: true
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Employee.groovy
 
b/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Employee.groovy
new file mode 100644
index 0000000000..2fc5e27d96
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Employee.groovy
@@ -0,0 +1,59 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package functionaltests.binding
+
+import grails.databinding.BindInitializer
+import grails.databinding.BindUsing
+import grails.databinding.BindingFormat
+
+/**
+ * Employee domain class demonstrating advanced data binding features.
+ */
+class Employee {
+    String firstName
+    String lastName
+    
+    @BindUsing({ obj, source ->
+        source['email']?.toLowerCase()?.trim()
+    })
+    String email
+    
+    @BindingFormat('MMddyyyy')
+    Date hireDate
+    
+    @BindingFormat('yyyy-MM-dd')
+    Date birthDate
+    
+    Integer salary
+    
+    Address homeAddress
+    Address workAddress
+    
+    static constraints = {
+        firstName nullable: true
+        lastName nullable: true
+        email nullable: true
+        hireDate nullable: true
+        birthDate nullable: true
+        salary nullable: true
+        homeAddress nullable: true
+        workAddress nullable: true
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Project.groovy
 
b/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Project.groovy
new file mode 100644
index 0000000000..1f76efe95d
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Project.groovy
@@ -0,0 +1,36 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package functionaltests.binding
+
+/**
+ * Project domain class for testing Map-based binding.
+ */
+class Project {
+    String name
+    String description
+    
+    static hasMany = [contributors: Contributor]
+    Map contributors
+    
+    static constraints = {
+        name nullable: true
+        description nullable: true
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Team.groovy
 
b/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Team.groovy
new file mode 100644
index 0000000000..9982f39850
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Team.groovy
@@ -0,0 +1,34 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package functionaltests.binding
+
+/**
+ * Team domain class for testing collection binding.
+ */
+class Team {
+    String name
+    
+    static hasMany = [members: TeamMember]
+    List members
+    
+    static constraints = {
+        name nullable: true
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/domain/functionaltests/binding/TeamMember.groovy
 
b/grails-test-examples/app1/grails-app/domain/functionaltests/binding/TeamMember.groovy
new file mode 100644
index 0000000000..c5466ea816
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/domain/functionaltests/binding/TeamMember.groovy
@@ -0,0 +1,36 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package functionaltests.binding
+
+/**
+ * TeamMember domain class for testing collection binding.
+ */
+class TeamMember {
+    String name
+    String role
+    
+    static belongsTo = [team: Team]
+    
+    static constraints = {
+        name nullable: true
+        role nullable: true
+        team nullable: true
+    }
+}
diff --git a/grails-test-examples/app1/grails-app/i18n/messages.properties 
b/grails-test-examples/app1/grails-app/i18n/messages.properties
index 09d392c881..a48bae1282 100644
--- a/grails-test-examples/app1/grails-app/i18n/messages.properties
+++ b/grails-test-examples/app1/grails-app/i18n/messages.properties
@@ -68,3 +68,12 @@ typeMismatch.java.lang.Long=Property {0} must be a valid 
number
 typeMismatch.java.lang.Short=Property {0} must be a valid number
 typeMismatch.java.math.BigDecimal=Property {0} must be a valid number
 typeMismatch.java.math.BigInteger=Property {0} must be a valid number
+
+# Custom test messages for i18n testing
+app.welcome=Welcome to the Application
+app.greeting=Hello, {0}!
+app.farewell=Goodbye, {0}. See you soon!
+app.itemcount=You have {0,choice,0#no items|1#one item|1<{0} items}.
+app.date.format=Today is {0,date,long}.
+app.currency=The price is {0,number,currency}.
+app.percent=Completion: {0,number,percent}
diff --git a/grails-test-examples/app1/grails-app/i18n/messages_de.properties 
b/grails-test-examples/app1/grails-app/i18n/messages_de.properties
index 18cd4a68b2..d967a583d6 100644
--- a/grails-test-examples/app1/grails-app/i18n/messages_de.properties
+++ b/grails-test-examples/app1/grails-app/i18n/messages_de.properties
@@ -68,3 +68,12 @@ typeMismatch.java.lang.Long=Die Eigenschaft {0} muss eine 
gültige Zahl sein
 typeMismatch.java.lang.Short=Die Eigenschaft {0} muss eine gültige Zahl sein
 typeMismatch.java.math.BigDecimal=Die Eigenschaft {0} muss eine gültige Zahl 
sein
 typeMismatch.java.math.BigInteger=Die Eigenschaft {0} muss eine gültige Zahl 
sein
+
+# Custom test messages for i18n testing
+app.welcome=Willkommen in der Anwendung
+app.greeting=Hallo, {0}!
+app.farewell=Auf Wiedersehen, {0}. Bis bald!
+app.itemcount=Sie haben {0,choice,0#keine Artikel|1#einen Artikel|1<{0} 
Artikel}.
+app.date.format=Heute ist der {0,date,long}.
+app.currency=Der Preis ist {0,number,currency}.
+app.percent=Fertigstellung: {0,number,percent}
diff --git a/grails-test-examples/app1/grails-app/i18n/messages_fr.properties 
b/grails-test-examples/app1/grails-app/i18n/messages_fr.properties
index 93d4bc05f7..ed77e62281 100644
--- a/grails-test-examples/app1/grails-app/i18n/messages_fr.properties
+++ b/grails-test-examples/app1/grails-app/i18n/messages_fr.properties
@@ -32,3 +32,12 @@ default.not.unique.message=La propriété [{0}] de la classe 
[{1}] avec la valeu
 
 default.paginate.prev=Précédent
 default.paginate.next=Suivant
+
+# Custom test messages for i18n testing
+app.welcome=Bienvenue dans l'application
+app.greeting=Bonjour, {0}!
+app.farewell=Au revoir, {0}. À bientôt!
+app.itemcount=Vous avez {0,choice,0#aucun article|1#un article|1<{0} articles}.
+app.date.format=Aujourd'hui c'est le {0,date,long}.
+app.currency=Le prix est de {0,number,currency}.
+app.percent=Complétion: {0,number,percent}
diff --git 
a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/binding/AdvancedDataBindingSpec.groovy
 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/binding/AdvancedDataBindingSpec.groovy
new file mode 100644
index 0000000000..7fb441b62c
--- /dev/null
+++ 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/binding/AdvancedDataBindingSpec.groovy
@@ -0,0 +1,595 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package functionaltests.binding
+
+import functionaltests.Application
+import grails.testing.mixin.integration.Integration
+import io.micronaut.http.HttpRequest
+import io.micronaut.http.HttpResponse
+import io.micronaut.http.MediaType
+import io.micronaut.http.client.HttpClient
+import spock.lang.Specification
+
+/**
+ * Comprehensive integration tests for advanced data binding features.
+ * 
+ * Tests cover:
+ * - Map-based binding with nested objects
+ * - @BindUsing annotation
+ * - @BindingFormat annotation for dates
+ * - Collection binding (List and Map)
+ * - @RequestParameter annotation
+ * - bindData with include/exclude
+ * - Selective property binding
+ * - Direct data binder usage
+ * - Type conversion errors
+ * - Command object binding
+ * - JSON body binding
+ * - Empty string to null conversion
+ * - String trimming
+ */
+@Integration(applicationClass = Application)
+class AdvancedDataBindingSpec extends Specification {
+
+    private HttpClient createClient() {
+        HttpClient.create(new URL("http://localhost:$serverPort";))
+    }
+
+    // ========== Map-Based Binding Tests ==========
+
+    def "test basic map-based binding"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindEmployee?firstName=John&lastName=Doe&salary=50000'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().firstName == 'John'
+        response.body().lastName == 'Doe'
+        response.body().salary == 50000
+
+        cleanup:
+        client.close()
+    }
+
+    def "test nested object binding"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindEmployee?firstName=Jane&homeAddress.street=123+Main+St&homeAddress.city=Springfield&homeAddress.state=IL'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().firstName == 'Jane'
+        response.body().homeAddress != null
+        response.body().homeAddress.street == '123 Main St'
+        response.body().homeAddress.city == 'Springfield'
+        response.body().homeAddress.state == 'IL'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== @BindUsing Annotation Tests ==========
+
+    def "test @BindUsing annotation lowercases and trims email"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindWithBindUsing?email=John.Doe%40Example.COM'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().email == '[email protected]'
+        response.body().originalEmail == '[email protected]'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test @BindUsing with mixed case email"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindWithBindUsing?email=TEST.User%40DOMAIN.org'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().email == '[email protected]'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== @BindingFormat Annotation Tests ==========
+
+    def "test @BindingFormat for date parsing - MMddyyyy format"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindWithDateFormat?hireDate=01152020'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().hireDate == '2020-01-15'
+        response.body().hireDateInput == '01152020'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test @BindingFormat for date parsing - yyyy-MM-dd format"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindWithDateFormat?birthDate=1990-05-20'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().birthDate == '1990-05-20'
+        response.body().birthDateInput == '1990-05-20'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test multiple date formats in same request"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindWithDateFormat?hireDate=03012021&birthDate=1985-12-25'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().hireDate == '2021-03-01'
+        response.body().birthDate == '1985-12-25'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Collection Binding Tests (List) ==========
+
+    def "test binding to List collection"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindTeamWithMembers?name=Engineering&members%5B0%5D.name=Alice&members%5B0%5D.role=Lead&members%5B1%5D.name=Bob&members%5B1%5D.role=Developer'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().name == 'Engineering'
+        response.body().members.size() == 2
+        response.body().members[0].name == 'Alice'
+        response.body().members[0].role == 'Lead'
+        response.body().members[1].name == 'Bob'
+        response.body().members[1].role == 'Developer'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test binding to List with gaps in indices"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindTeamWithMembers?name=QA&members%5B0%5D.name=Carol&members%5B2%5D.name=Dave'),
+            Map
+        )
+
+        then: "only non-null members are returned"
+        response.status.code == 200
+        response.body().name == 'QA'
+        // Members with gaps in indices - we only get non-null entries
+        response.body().members.size() == 2
+        response.body().members.find { it.name == 'Carol' } != null
+        response.body().members.find { it.name == 'Dave' } != null
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Map Collection Binding Tests ==========
+
+    def "test binding to Map collection"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindProjectWithContributors?name=GrailsCore&contributors%5Blead%5D.name=John&contributors%5Blead%5D.expertise=Architecture&contributors%5Bdev%5D.name=Jane&contributors%5Bdev%5D.expertise=Testing'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().name == 'GrailsCore'
+        response.body().contributors.lead.name == 'John'
+        response.body().contributors.lead.expertise == 'Architecture'
+        response.body().contributors.dev.name == 'Jane'
+        response.body().contributors.dev.expertise == 'Testing'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== @RequestParameter Annotation Tests ==========
+
+    def "test @RequestParameter maps different parameter names"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindWithRequestParameter?firstName=Robert&lastName=Smith&age=30'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().givenName == 'Robert'
+        response.body().familyName == 'Smith'
+        response.body().age == 30
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== bindData with Include/Exclude Tests ==========
+
+    def "test bindData with include - only specified properties bound"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindWithIncludeExclude?firstName=Test&lastName=User&email=test%40example.com&salary=100000'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().firstName == 'Test'
+        response.body().lastName == 'User'
+        response.body().email == null
+        response.body().salary == null
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Selective Property Binding Tests ==========
+
+    def "test selective property binding using subscript operator"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindSelectiveProperties?firstName=Selective&lastName=Test&email=should.not.bind%40test.com&salary=999'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().firstName == 'Selective'
+        response.body().lastName == 'Test'
+        response.body().email == null
+        response.body().salary == null
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Direct Data Binder Usage Tests ==========
+
+    def "test using grailsWebDataBinder directly"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindUsingDirectBinder?firstName=Direct&lastName=Binder&email=DIRECT%40TEST.COM'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().firstName == 'Direct'
+        response.body().lastName == 'Binder'
+        // Email should be lowercased due to @BindUsing
+        response.body().email == '[email protected]'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Command Object Binding Tests ==========
+
+    def "test command object binding with validation - valid data"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindCommandObject?firstName=Valid&lastName=User&email=valid%40email.com'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().firstName == 'Valid'
+        response.body().lastName == 'User'
+        response.body().email == '[email protected]'
+        response.body().valid == true
+        response.body().errors.isEmpty()
+
+        cleanup:
+        client.close()
+    }
+
+    def "test command object binding with validation - invalid data"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindCommandObject?firstName=&lastName=&email=invalid-email'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().valid == false
+        response.body().errors.contains('firstName')
+        response.body().errors.contains('lastName')
+
+        cleanup:
+        client.close()
+    }
+
+    def "test nested command object binding"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindNestedCommandObject?name=Contact+Person&address.street=456+Oak+Ave&address.city=Portland'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().name == 'Contact Person'
+        response.body().address.street == '456 Oak Ave'
+        response.body().address.city == 'Portland'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== JSON Body Binding Tests ==========
+
+    def "test JSON body binding to command object"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.POST('/advancedDataBinding/bindJsonBody', [
+                firstName: 'JsonFirst',
+                lastName: 'JsonLast',
+                email: '[email protected]'
+            ]).contentType(MediaType.APPLICATION_JSON),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().firstName == 'JsonFirst'
+        response.body().lastName == 'JsonLast'
+        response.body().email == '[email protected]'
+        response.body().valid == true
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Multiple Command Objects Tests ==========
+
+    def "test binding multiple command objects"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindMultipleCommandObjects?employee.firstName=Multi&employee.lastName=Test&address.street=789+Pine+Rd&address.city=Seattle'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().employee.firstName == 'Multi'
+        response.body().employee.lastName == 'Test'
+        response.body().address.street == '789 Pine Rd'
+        response.body().address.city == 'Seattle'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Empty String Conversion Tests ==========
+
+    def "test empty string converts to null"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindEmptyStrings?firstName=&lastName=HasValue'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().firstNameIsNull == true
+        response.body().lastName == 'HasValue'
+        response.body().lastNameIsNull == false
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== String Trimming Tests ==========
+
+    def "test string trimming during binding"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindWithTrimming?firstName=+++Trimmed+++'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().firstName == 'Trimmed'
+        response.body().firstNameLength == 7
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Type Conversion Tests ==========
+
+    def "test valid type conversion"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindWithTypeConversion?salary=75000&firstName=TypeTest'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().salary == 75000
+        response.body().firstName == 'TypeTest'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Edge Cases ==========
+
+    def "test binding with special characters in values"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindEmployee?firstName=O%27Brien&lastName=M%C3%BCller'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().firstName == "O'Brien"
+        response.body().lastName == 'Müller'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test binding with unicode characters"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindEmployee?firstName=%E6%97%A5%E6%9C%AC%E8%AA%9E'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().firstName == '日本語'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test binding with null parameter values"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/advancedDataBinding/bindEmployee?firstName=TestNull'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().firstName == 'TestNull'
+        response.body().lastName == null
+        response.body().homeAddress == null
+
+        cleanup:
+        client.close()
+    }
+}
diff --git 
a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/codecs/SecurityCodecsSpec.groovy
 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/codecs/SecurityCodecsSpec.groovy
new file mode 100644
index 0000000000..80879971af
--- /dev/null
+++ 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/codecs/SecurityCodecsSpec.groovy
@@ -0,0 +1,560 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package functionaltests.codecs
+
+import functionaltests.Application
+import grails.testing.mixin.integration.Integration
+import io.micronaut.http.HttpRequest
+import io.micronaut.http.client.HttpClient
+import spock.lang.Specification
+
+/**
+ * Comprehensive integration tests for Grails codec functionality.
+ * 
+ * Tests cover:
+ * - HTML encoding/decoding (XSS prevention)
+ * - URL encoding/decoding
+ * - Base64 encoding/decoding
+ * - MD5, SHA1, SHA256 hashing
+ * - Hex encoding/decoding
+ * - JavaScript encoding
+ * - Raw output handling
+ * - Edge cases (null, empty, special characters)
+ * - Hash consistency verification
+ */
+@Integration(applicationClass = Application)
+class SecurityCodecsSpec extends Specification {
+
+    private HttpClient createClient() {
+        HttpClient.create(new URL("http://localhost:$serverPort";))
+    }
+
+    // ========== HTML Encoding Tests (XSS Prevention) ==========
+
+    def "test HTML encoding escapes dangerous tags"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/codecTest/encodeHtml?input=%3Cscript%3Ealert(%22XSS%22)%3C/script%3E'),
+            Map
+        )
+
+        then: "script tags should be HTML encoded"
+        response.status.code == 200
+        response.body().input == '<script>alert("XSS")</script>'
+        response.body().encoded.contains('&lt;script&gt;')
+        response.body().encoded.contains('&lt;/script&gt;')
+        !response.body().encoded.contains('<script>')
+        response.body().decodedBack == '<script>alert("XSS")</script>'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test HTML encoding escapes quotes"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/encodeHtml?input=%22quoted%22'),
+            Map
+        )
+
+        then: "quotes should be HTML encoded"
+        response.status.code == 200
+        response.body().encoded.contains('&quot;')
+        response.body().decodedBack == '"quoted"'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test HTML encoding escapes ampersands"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/encodeHtml?input=foo%26bar'),
+            Map
+        )
+
+        then: "ampersands should be HTML encoded"
+        response.status.code == 200
+        response.body().input == 'foo&bar'
+        response.body().encoded.contains('&amp;')
+        response.body().decodedBack == 'foo&bar'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== URL Encoding Tests ==========
+
+    def "test URL encoding escapes spaces and special chars"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/codecTest/encodeUrl?input=hello+world%26foo%3Dbar'),
+            Map
+        )
+
+        then: "spaces and special chars should be URL encoded"
+        response.status.code == 200
+        response.body().input == 'hello world&foo=bar'
+        response.body().encoded.contains('+') || 
response.body().encoded.contains('%20')
+        response.body().encoded.contains('%26')
+        response.body().encoded.contains('%3D')
+        response.body().decodedBack == 'hello world&foo=bar'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Base64 Encoding Tests ==========
+
+    def "test Base64 encoding and decoding text"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/encodeBase64?input=Hello%2C+World!'),
+            Map
+        )
+
+        then: "text should be Base64 encoded and decodable"
+        response.status.code == 200
+        response.body().input == 'Hello, World!'
+        response.body().encoded == 'SGVsbG8sIFdvcmxkIQ=='
+        response.body().decodedBack == 'Hello, World!'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test Base64 encoding with binary data"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/encodeBase64Binary'),
+            Map
+        )
+
+        then: "binary data should be correctly Base64 encoded"
+        response.status.code == 200
+        response.body().originalBytes == [72, 101, 108, 108, 111] // "Hello" 
in ASCII
+        response.body().encoded == 'SGVsbG8='
+        response.body().decodedBytes == [72, 101, 108, 108, 111]
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== MD5 Hash Tests ==========
+
+    def "test MD5 hashing produces consistent 32-char hex string"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/encodeMd5?input=password123'),
+            Map
+        )
+
+        then: "MD5 hash should be 32 characters (hex)"
+        response.status.code == 200
+        response.body().input == 'password123'
+        response.body().hashLength == 32
+        response.body().md5Hash ==~ /^[a-f0-9]{32}$/
+
+        cleanup:
+        client.close()
+    }
+
+    def "test MD5 bytes produces 16 bytes"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/encodeMd5Bytes?input=password123'),
+            Map
+        )
+
+        then: "MD5 bytes should be 16 bytes"
+        response.status.code == 200
+        response.body().bytesLength == 16
+        response.body().md5Bytes.size() == 16
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== SHA1 Hash Tests ==========
+
+    def "test SHA1 hashing produces consistent 40-char hex string"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/encodeSha1?input=password123'),
+            Map
+        )
+
+        then: "SHA1 hash should be 40 characters (hex)"
+        response.status.code == 200
+        response.body().hashLength == 40
+        response.body().sha1Hash ==~ /^[a-f0-9]{40}$/
+
+        cleanup:
+        client.close()
+    }
+
+    def "test SHA1 bytes produces 20 bytes"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/encodeSha1Bytes?input=password123'),
+            Map
+        )
+
+        then: "SHA1 bytes should be 20 bytes"
+        response.status.code == 200
+        response.body().bytesLength == 20
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== SHA256 Hash Tests ==========
+
+    def "test SHA256 hashing produces consistent 64-char hex string"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/encodeSha256?input=password123'),
+            Map
+        )
+
+        then: "SHA256 hash should be 64 characters (hex)"
+        response.status.code == 200
+        response.body().hashLength == 64
+        response.body().sha256Hash ==~ /^[a-f0-9]{64}$/
+
+        cleanup:
+        client.close()
+    }
+
+    def "test SHA256 bytes produces 32 bytes"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/encodeSha256Bytes?input=password123'),
+            Map
+        )
+
+        then: "SHA256 bytes should be 32 bytes"
+        response.status.code == 200
+        response.body().bytesLength == 32
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Hex Encoding Tests ==========
+
+    def "test Hex encoding and decoding"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/encodeHex?input=Hello'),
+            Map
+        )
+
+        then: "text should be Hex encoded and decodable"
+        response.status.code == 200
+        response.body().input == 'Hello'
+        response.body().hexEncoded == '48656c6c6f' // "Hello" in hex
+        response.body().decodedBack == 'Hello'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== JavaScript Encoding Tests ==========
+
+    def "test JavaScript encoding escapes quotes and newlines"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET("/codecTest/encodeJavaScript"),
+            Map
+        )
+
+        then: "JavaScript special chars should be escaped"
+        response.status.code == 200
+        response.body().input.contains("'")
+        response.body().input.contains('"')
+        response.body().input.contains('\n')
+        // The encoded output should escape these characters
+        response.body().encoded.contains("\\'") || 
response.body().encoded.contains("\\u0027")
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Raw Output Tests ==========
+
+    def "test Raw encoding preserves content without escaping"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/encodeRaw'),
+            Map
+        )
+
+        then: "raw content should be preserved"
+        response.status.code == 200
+        response.body().input == '<b>Bold</b>'
+        response.body().raw == '<b>Bold</b>'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Multiple Encodings Tests ==========
+
+    def "test chaining multiple encodings"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/multipleEncodings'),
+            Map
+        )
+
+        then: "multiple encodings should be reversible"
+        response.status.code == 200
+        response.body().input == '<script>alert(1)</script>'
+        response.body().htmlEncoded.contains('&lt;')
+        response.body().fullyDecoded == '<script>alert(1)</script>'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Special Characters Tests ==========
+
+    def "test encoding with Unicode and special characters"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/codecTest/encodeSpecialChars?input=%E6%97%A5%E6%9C%AC%E8%AA%9E+%26+%C3%A9moji+%F0%9F%91%8D+%3Ctag%3E'),
+            Map
+        )
+
+        then: "special characters should be properly encoded"
+        response.status.code == 200
+        response.body().input.contains('日本語')
+        response.body().htmlEncoded.contains('&lt;tag&gt;')
+        response.body().urlEncoded != null
+        response.body().base64Encoded != null
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Null Handling Tests ==========
+
+    def "test encoding null values returns null safely"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/encodeNull'),
+            Map
+        )
+
+        then: "null values should be handled gracefully"
+        response.status.code == 200
+        response.body().nullBase64 == null
+        response.body().nullMd5 == null
+        response.body().nullHtml == null
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Empty String Tests ==========
+
+    def "test encoding empty strings"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/encodeEmpty'),
+            Map
+        )
+
+        then: "empty strings should be encoded without errors"
+        response.status.code == 200
+        response.body().input == ''
+        // Empty string Base64 is empty
+        response.body().base64Encoded == ''
+        // Empty string still has a hash
+        response.body().md5Hash != null
+        response.body().md5Hash.length() == 32
+        response.body().sha256Hash != null
+        response.body().sha256Hash.length() == 64
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Hash Consistency Tests ==========
+
+    def "test hash functions produce consistent results"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/codecTest/hashConsistency?input=test-consistency'),
+            Map
+        )
+
+        then: "same input should always produce same hash"
+        response.status.code == 200
+        response.body().md5Consistent == true
+        response.body().sha1Consistent == true
+        response.body().sha256Consistent == true
+
+        cleanup:
+        client.close()
+    }
+
+    def "test different inputs produce different hashes"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response1 = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/hashConsistency?input=input1'),
+            Map
+        )
+        def response2 = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/hashConsistency?input=input2'),
+            Map
+        )
+
+        then: "different inputs should produce different hashes"
+        response1.status.code == 200
+        response2.status.code == 200
+        response1.body().md5Hash != response2.body().md5Hash
+        response1.body().sha1Hash != response2.body().sha1Hash
+        response1.body().sha256Hash != response2.body().sha256Hash
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Known Hash Values Tests ==========
+
+    def "test MD5 produces known hash for 'hello'"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/encodeMd5?input=hello'),
+            Map
+        )
+
+        then: "MD5 of 'hello' should match known value"
+        response.status.code == 200
+        response.body().md5Hash == '5d41402abc4b2a76b9719d911017c592'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test SHA1 produces known hash for 'hello'"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/encodeSha1?input=hello'),
+            Map
+        )
+
+        then: "SHA1 of 'hello' should match known value"
+        response.status.code == 200
+        response.body().sha1Hash == 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test SHA256 produces known hash for 'hello'"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/codecTest/encodeSha256?input=hello'),
+            Map
+        )
+
+        then: "SHA256 of 'hello' should match known value"
+        response.status.code == 200
+        response.body().sha256Hash == 
'2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
+
+        cleanup:
+        client.close()
+    }
+}
diff --git 
a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/i18n/InternationalizationSpec.groovy
 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/i18n/InternationalizationSpec.groovy
new file mode 100644
index 0000000000..0570ede1aa
--- /dev/null
+++ 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/i18n/InternationalizationSpec.groovy
@@ -0,0 +1,606 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package functionaltests.i18n
+
+import functionaltests.Application
+import grails.testing.mixin.integration.Integration
+import io.micronaut.http.HttpRequest
+import io.micronaut.http.client.HttpClient
+import spock.lang.Specification
+
+/**
+ * Comprehensive integration tests for internationalization (i18n) features.
+ * 
+ * Tests cover:
+ * - Locale switching (English, German, French)
+ * - Message resolution from property files
+ * - Message formatting with arguments
+ * - Pluralization (choice format)
+ * - Date/currency/percent formatting
+ * - Default message fallback
+ * - Validation error messages
+ * - Accept-Language header handling
+ */
+@Integration(applicationClass = Application)
+class InternationalizationSpec extends Specification {
+
+    private HttpClient createClient() {
+        HttpClient.create(new URL("http://localhost:$serverPort";))
+    }
+
+    // ========== Basic Message Resolution Tests ==========
+
+    def "test simple message in English"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getMessage?code=app.welcome&lang=en'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().code == 'app.welcome'
+        response.body().locale == 'en'
+        response.body().message == 'Welcome to the Application'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test simple message in German"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getMessage?code=app.welcome&lang=de'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().code == 'app.welcome'
+        response.body().locale == 'de'
+        response.body().message == 'Willkommen in der Anwendung'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test simple message in French"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getMessage?code=app.welcome&lang=fr'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().code == 'app.welcome'
+        response.body().locale == 'fr'
+        response.body().message == "Bienvenue dans l'application"
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Message With Arguments Tests ==========
+
+    def "test message with argument in English"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/i18nTest/getMessageWithArgs?code=app.greeting&arg=John&lang=en'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().message == 'Hello, John!'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test message with argument in German"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/i18nTest/getMessageWithArgs?code=app.greeting&arg=Johann&lang=de'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().message == 'Hallo, Johann!'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test farewell message with argument"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/i18nTest/getMessageWithArgs?code=app.farewell&arg=Alice&lang=en'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().message == 'Goodbye, Alice. See you soon!'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Pluralization Tests (Choice Format) ==========
+
+    def "test choice format - zero items in English"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getChoiceMessage?count=0&lang=en'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().count == 0
+        response.body().message == 'You have no items.'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test choice format - one item in English"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getChoiceMessage?count=1&lang=en'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().count == 1
+        response.body().message == 'You have one item.'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test choice format - multiple items in English"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getChoiceMessage?count=5&lang=en'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().count == 5
+        response.body().message == 'You have 5 items.'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test choice format in German"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getChoiceMessage?count=0&lang=de'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().message == 'Sie haben keine Artikel.'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test choice format - one item in German"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getChoiceMessage?count=1&lang=de'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().message == 'Sie haben einen Artikel.'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Date Formatting Tests ==========
+
+    def "test date formatting in English"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getDateMessage?lang=en'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().message.startsWith('Today is ')
+        // English format: "Today is January 25, 2026."
+        response.body().message.contains(',')
+
+        cleanup:
+        client.close()
+    }
+
+    def "test date formatting in German"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getDateMessage?lang=de'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().message.startsWith('Heute ist der ')
+        // German format: "Heute ist der 25. Januar 2026."
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Currency Formatting Tests ==========
+
+    def "test currency formatting in English US"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/i18nTest/getCurrencyMessage?amount=1234.56&lang=en_US'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().message.contains('$') || 
response.body().message.contains('1,234.56')
+
+        cleanup:
+        client.close()
+    }
+
+    def "test currency formatting in German"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/i18nTest/getCurrencyMessage?amount=1234.56&lang=de_DE'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        // German format uses € and different number formatting
+        response.body().message != null
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Percentage Formatting Tests ==========
+
+    def "test percentage formatting in English"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getPercentMessage?value=0.75&lang=en'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().message.contains('75')
+        response.body().message.contains('%')
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Default Message Fallback Tests ==========
+
+    def "test default message for non-existent code"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/i18nTest/getMessageWithDefault?code=non.existent.key&defaultMsg=Fallback+Message&lang=en'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().message == 'Fallback Message'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Validation Messages Tests ==========
+
+    def "test validation messages in English"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getValidationMessages?lang=en'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().messages.blank.contains('cannot be blank')
+        response.body().messages.nullable.contains('cannot be null')
+        response.body().messages.paginate_prev == 'Previous'
+        response.body().messages.paginate_next == 'Next'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test validation messages in German"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getValidationMessages?lang=de'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().messages.paginate_prev == 'Vorherige'
+        response.body().messages.paginate_next == 'Nächste'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test validation messages in French"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getValidationMessages?lang=fr'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().messages.paginate_prev == 'Précédent'
+        response.body().messages.paginate_next == 'Suivant'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Multiple Messages Tests ==========
+
+    def "test multiple messages at once in English"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getMultipleMessages?lang=en'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().messages.welcome == 'Welcome to the Application'
+        response.body().messages.greeting == 'Hello, User!'
+        response.body().messages.farewell == 'Goodbye, User. See you soon!'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test multiple messages at once in German"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getMultipleMessages?lang=de'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().messages.welcome == 'Willkommen in der Anwendung'
+        response.body().messages.greeting == 'Hallo, User!'
+        response.body().messages.farewell == 'Auf Wiedersehen, User. Bis bald!'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Locale Information Tests ==========
+
+    def "test current locale information"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getCurrentLocale?lang=de_DE'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().language == 'de'
+        response.body().country == 'DE'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Accept-Language Header Tests ==========
+
+    def "test locale from Accept-Language header - German"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getLocaleFromHeader')
+                .header('Accept-Language', 'de-DE'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        // The request locale should reflect the Accept-Language header
+        response.body().requestLocale?.startsWith('de') || 
response.body().contextLocale?.startsWith('de')
+
+        cleanup:
+        client.close()
+    }
+
+    def "test locale from Accept-Language header - French"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getLocaleFromHeader')
+                .header('Accept-Language', 'fr-FR'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().requestLocale?.startsWith('fr') || 
response.body().contextLocale?.startsWith('fr')
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Controller Message Method Tests ==========
+
+    def "test controller message method"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/i18nTest/useControllerMessage?code=app.welcome&lang=en'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().message == 'Welcome to the Application'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Edge Cases ==========
+
+    def "test fallback to default locale when unsupported locale requested"() {
+        given:
+        def client = createClient()
+
+        when: "requesting a locale that doesn't have translations"
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/i18nTest/getMessage?code=app.welcome&lang=xyz'),
+            Map
+        )
+
+        then: "should fall back to default (English) message"
+        response.status.code == 200
+        // Will either get the message or fall back
+        response.body().message != null
+
+        cleanup:
+        client.close()
+    }
+
+    def "test message with special characters"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/i18nTest/getMessageWithArgs?code=app.greeting&arg=%C3%A9l%C3%A8ve&lang=en'),
+            Map
+        )
+
+        then: "special characters should be handled correctly"
+        response.status.code == 200
+        response.body().arg == 'élève'
+        response.body().message == 'Hello, élève!'
+
+        cleanup:
+        client.close()
+    }
+}


Reply via email to