diff --git a/internal/base/validator/validator.go b/internal/base/validator/validator.go index 9c7f6ec41..2b7374b19 100644 --- a/internal/base/validator/validator.go +++ b/internal/base/validator/validator.go @@ -24,6 +24,7 @@ import ( "fmt" "reflect" "strings" + "sync" "unicode" "github.com/apache/answer/internal/base/reason" @@ -96,7 +97,10 @@ type FormErrorField struct { } // GlobalValidatorMapping is a mapping from validator to translator used -var GlobalValidatorMapping = make(map[i18n.Language]*MyValidator, 0) +var ( + GlobalValidatorMapping = make(map[i18n.Language]*MyValidator, 0) + validatorMappingMu sync.RWMutex +) func init() { for _, t := range allLanguageTranslators { @@ -180,9 +184,44 @@ func createDefaultValidator(la i18n.Language) *validator.Validate { } func GetValidatorByLang(lang i18n.Language) *MyValidator { - if GlobalValidatorMapping[lang] != nil { - return GlobalValidatorMapping[lang] + validatorMappingMu.RLock() + if v := GlobalValidatorMapping[lang]; v != nil { + validatorMappingMu.RUnlock() + return v + } + validatorMappingMu.RUnlock() + + baseLang := string(lang) + if idx := strings.IndexAny(baseLang, "_-"); idx > 0 { + baseLang = baseLang[:idx] } + + for _, t := range allLanguageTranslators { + tBase := string(t.La) + if idx := strings.IndexAny(tBase, "_-"); idx > 0 { + tBase = tBase[:idx] + } + if strings.EqualFold(baseLang, tBase) { + validatorMappingMu.Lock() + defer validatorMappingMu.Unlock() + if v := GlobalValidatorMapping[lang]; v != nil { + return v + } + tran, val := getTran(t.Lo), createDefaultValidator(lang) + if t.RegisterFunc != nil { + if err := t.RegisterFunc(val, tran); err != nil { + log.Error(err) + break + } + } + v := &MyValidator{Validate: val, Tran: tran, Lang: lang} + GlobalValidatorMapping[lang] = v + return v + } + } + + validatorMappingMu.RLock() + defer validatorMappingMu.RUnlock() return GlobalValidatorMapping[i18n.DefaultLanguage] } @@ -196,12 +235,16 @@ func (m *MyValidator) Check(value any) (errFields []*FormErrorField, err error) if len(field.ErrorField) == 0 { continue } - firstRune := []rune(field.ErrorMsg)[0] + runes := []rune(field.ErrorMsg) + if len(runes) == 0 { + continue + } + firstRune := runes[0] if !unicode.IsLetter(firstRune) || !unicode.Is(unicode.Latin, firstRune) { continue } upperFirstRune := unicode.ToUpper(firstRune) - field.ErrorMsg = string(upperFirstRune) + field.ErrorMsg[1:] + field.ErrorMsg = string(upperFirstRune) + string(runes[1:]) if !strings.HasSuffix(field.ErrorMsg, ".") { field.ErrorMsg += "." } diff --git a/internal/base/validator/validator_test.go b/internal/base/validator/validator_test.go new file mode 100644 index 000000000..583e2bf2e --- /dev/null +++ b/internal/base/validator/validator_test.go @@ -0,0 +1,73 @@ +/* + * 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 + * + * http://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 validator + +import ( + "strings" + "testing" + "unicode/utf8" + + "github.com/segmentfault/pacman/i18n" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type sampleCommentReq struct { + OriginalText string `validate:"required,notblank,gte=2,lte=5" json:"original_text"` +} + +type sampleAccentedFieldReq struct { + Email string `validate:"required,email" json:"émail"` +} + +func TestGetValidatorByLang_RegionalLocaleFallback(t *testing.T) { + req := &sampleCommentReq{OriginalText: "123456"} + + esMXValidator := GetValidatorByLang(i18n.Language("es_MX")) + require.NotNil(t, esMXValidator) + assert.Equal(t, i18n.Language("es_MX"), esMXValidator.Lang) + + errFields, err := esMXValidator.Check(req) + require.Error(t, err) + require.Len(t, errFields, 1) + assert.Contains(t, errFields[0].ErrorMsg, "debe tener un máximo de 5") + + ptBRValidator := GetValidatorByLang(i18n.Language("pt_BR")) + require.NotNil(t, ptBRValidator) + assert.Equal(t, i18n.Language("pt_BR"), ptBRValidator.Lang) + + errFieldsPT, errPT := ptBRValidator.Check(req) + require.Error(t, errPT) + require.Len(t, errFieldsPT, 1) + assert.Contains(t, errFieldsPT[0].ErrorMsg, "máximo") +} + +func TestCheck_MultiByteLatinCapitalization(t *testing.T) { + req := &sampleAccentedFieldReq{Email: "not-an-email"} + + esValidator := GetValidatorByLang(i18n.LanguageSpanish) + errFields, err := esValidator.Check(req) + require.Error(t, err) + require.Len(t, errFields, 1) + + msg := errFields[0].ErrorMsg + assert.True(t, utf8.ValidString(msg), "error message must be valid UTF-8, got: %q", msg) + assert.True(t, strings.HasPrefix(msg, "Émail "), "expected prefix 'Émail ', got: %q", msg) +}