Localized Struct Validation
Web frameworks and API routers typically bind request payloads to structs and validate them using Go-Playground Validator. However, default validation outputs suffer from two major problems:
- Error descriptions are in English only.
- Field names reflect raw Go struct fields (e.g.,
"ConfirmPassword"or"user_email") rather than user-friendly UI labels.
Rays’ MessageSourceValidator solves both problems by translating validation rules into the active request locale and replacing field names with localized labels defined in your message catalog.
How It Works
MessageSourceValidator wraps the validation engine with localization awareness:
- Validation: It invokes
Validate(&struct)to execute standard Go-Playground validation tags (required,min=8,email,eqfield, etc.). - Field Name Resolution: Field names are determined by the first matching tag among
field,form,json, andxml. If none are present, the struct field name is lowercased. - Namespace Translation: When calling
TranslateValidationErrors(errs, namespace), Rays looks up the localized field name in the active locale’s bundle under<namespace>.<field>(e.g.,fields.registration.username). - Cross-Field Translation: For cross-field rules like
eqfield=Passwordoreqcsfield=Parent.Field, Rays translates both the source field and the target comparison field.
Step-by-Step Implementation
1. Define Your Form Struct
Annotate your struct with validation rules and field identifier tags:
package forms
type UserRegistrationForm struct {
Username string `json:"username" form:"username" field:"username" validate:"required"`
Password string `json:"password" form:"password" field:"password" validate:"required,min=8"`
Confirm string `json:"confirm" form:"confirm" field:"confirm" validate:"required,eqfield=Password"`
}2. Configure Field Labels in YAML
Define the friendly labels for each field under a dedicated namespace (e.g., fields.registration) in your message catalogs:
localization:
messages:
en:
translation:
fields.registration.username: "Username"
fields.registration.password: "Password"
fields.registration.confirm: "Confirm Password"
fr:
translation:
fields.registration.username: "Nom d'utilisateur"
fields.registration.password: "Mot de passe"
fields.registration.confirm: "Confirmation du mot de passe"3. Validate and Translate in Controllers
Inject MessageSourceValidator into your controller or service. When validation fails, extract validator.ValidationErrors and call TranslateValidationErrors():
package controllers
import (
"errors"
"net/http"
"github.com/BeamFoundry/rays/pkg/i18n"
"github.com/go-playground/validator/v10"
)
type RegistrationController struct {
validator i18n.MessageSourceValidator `@:"Inject"`
}
func (c *RegistrationController) Register(form *UserRegistrationForm) map[string]string {
// 1. Validate the form pointer
err := c.validator.Validate(form)
if err == nil {
return nil // Validation succeeded
}
// 2. Extract validation errors
var valErrs validator.ValidationErrors
if errors.As(err, &valErrs) {
// 3. Translate using the "fields.registration" namespace
return c.validator.TranslateValidationErrors(valErrs, "fields.registration")
}
return map[string]string{"error": err.Error()}
}Validation Error Outputs
Assume a user submits a registration form with Password: "short" (less than 8 characters) and Confirm: "mismatch":
English (Locales.EN)
password: 'Password' must be at least 8 characters in length
confirm: 'Confirm Password' must be equal to 'Password'French (Locales.FR)
password: 'Mot de passe' doit faire une taille minimum de 8 caractères
confirm: 'Confirmation du mot de passe' doit être égal à 'Mot de passe'Key Highlights
- Quotes and Labels: Field names are wrapped in quotes (
'Password','Mot de passe') using your translated terms instead of raw struct identifiers. - Cross-Field Equality: Notice that the
confirmfield’s error in French correctly localized both fields:'Confirmation du mot de passe' doit être égal à 'Mot de passe'. - Returned Data Type:
TranslateValidationErrorsreturnsvalidator.ValidationErrorsTranslations(map[string]string), keyed by the field name ("password","confirm"), making it straightforward to serialize directly to JSON or render alongside form inputs in HTML templates.