From c0be71d6777deb6e7eeeda6003875888c24158bf Mon Sep 17 00:00:00 2001 From: root Date: Sun, 1 Mar 2026 23:29:05 +0300 Subject: [PATCH 1/3] feat(ru): complete Russian translations to match English version --- translations/ru/ru.go | 564 ++++++++++++++++++++----------------- translations/ru/ru_test.go | 153 +++++++++- 2 files changed, 452 insertions(+), 265 deletions(-) diff --git a/translations/ru/ru.go b/translations/ru/ru.go index 362e3e4e..21e3e82e 100644 --- a/translations/ru/ru.go +++ b/translations/ru/ru.go @@ -28,6 +28,94 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er translation: "{0} обязательное поле", override: false, }, + { + tag: "required_if", + translation: "{0} обязательное поле", + override: false, + }, + { + tag: "required_unless", + translation: "{0} обязательное поле", + override: false, + }, + { + tag: "required_with", + translation: "{0} обязательное поле", + override: false, + }, + { + tag: "required_with_all", + translation: "{0} обязательное поле", + override: false, + }, + { + tag: "required_without", + translation: "{0} обязательное поле", + override: false, + }, + { + tag: "required_without_all", + translation: "{0} обязательное поле", + override: false, + }, + { + tag: "excluded_if", + translation: "{0} должно быть исключено", + override: false, + }, + { + tag: "excluded_unless", + translation: "{0} должно быть исключено", + override: false, + }, + { + tag: "excluded_with", + translation: "{0} должно быть исключено", + override: false, + }, + { + tag: "excluded_with_all", + translation: "{0} должно быть исключено", + override: false, + }, + { + tag: "excluded_without", + translation: "{0} должно быть исключено", + override: false, + }, + { + tag: "excluded_without_all", + translation: "{0} должно быть исключено", + override: false, + }, + { + tag: "isdefault", + translation: "{0} должно быть значением по умолчанию", + override: false, + }, + { + tag: "urn_rfc2141", + translation: "{0} должен быть корректным URN согласно RFC 2141", + override: false, + }, + { + tag: "fqdn", + translation: "{0} должен быть корректным полным доменным именем (FQDN)", + override: false, + }, + { + tag: "datetime", + translation: "{0} не соответствует формату {1}", + override: false, + customTransFunc: func(ut ut.Translator, fe validator.FieldError) string { + t, err := ut.T(fe.Tag(), fe.Field(), fe.Param()) + if err != nil { + log.Printf("warning: error translating FieldError: %#v", fe) + return fe.(error).Error() + } + return t + }, + }, { tag: "len", customRegisFunc: func(ut ut.Translator) (err error) { @@ -184,17 +272,16 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er customTransFunc: func(ut ut.Translator, fe validator.FieldError) string { var err error var t string - + var f64 float64 var digits uint64 var kind reflect.Kind - if idx := strings.Index(fe.Param(), "."); idx != -1 { - digits = uint64(len(fe.Param()[idx+1:])) - } - - f64, err := strconv.ParseFloat(fe.Param(), 64) - if err != nil { - goto END + fn := func() (err error) { + if idx := strings.Index(fe.Param(), "."); idx != -1 { + digits = uint64(len(fe.Param()[idx+1:])) + } + f64, err = strconv.ParseFloat(fe.Param(), 64) + return } kind = fe.Kind() @@ -204,27 +291,39 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er switch kind { case reflect.String: - var c string - + err = fn() + if err != nil { + goto END + } c, err = ut.C("min-string-character", f64, digits, ut.FmtNumber(f64, digits)) if err != nil { goto END } - t, err = ut.T("min-string", fe.Field(), c) case reflect.Slice, reflect.Map, reflect.Array: var c string - + err = fn() + if err != nil { + goto END + } c, err = ut.C("min-items-item", f64, digits, ut.FmtNumber(f64, digits)) if err != nil { goto END } - t, err = ut.T("min-items", fe.Field(), c) default: + // Обработка для time.Duration + if fe.Type() == reflect.TypeOf(time.Duration(0)) { + t, err = ut.T("min-number", fe.Field(), fe.Param()) + goto END + } + err = fn() + if err != nil { + goto END + } t, err = ut.T("min-number", fe.Field(), ut.FmtNumber(f64, digits)) } @@ -233,7 +332,6 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er fmt.Printf("warning: error translating FieldError: %s", err) return fe.(error).Error() } - return t }, }, @@ -288,17 +386,16 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er customTransFunc: func(ut ut.Translator, fe validator.FieldError) string { var err error var t string - + var f64 float64 var digits uint64 var kind reflect.Kind - if idx := strings.Index(fe.Param(), "."); idx != -1 { - digits = uint64(len(fe.Param()[idx+1:])) - } - - f64, err := strconv.ParseFloat(fe.Param(), 64) - if err != nil { - goto END + fn := func() (err error) { + if idx := strings.Index(fe.Param(), "."); idx != -1 { + digits = uint64(len(fe.Param()[idx+1:])) + } + f64, err = strconv.ParseFloat(fe.Param(), 64) + return } kind = fe.Kind() @@ -308,27 +405,39 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er switch kind { case reflect.String: - var c string - + err = fn() + if err != nil { + goto END + } c, err = ut.C("max-string-character", f64, digits, ut.FmtNumber(f64, digits)) if err != nil { goto END } - t, err = ut.T("max-string", fe.Field(), c) case reflect.Slice, reflect.Map, reflect.Array: var c string - + err = fn() + if err != nil { + goto END + } c, err = ut.C("max-items-item", f64, digits, ut.FmtNumber(f64, digits)) if err != nil { goto END } - t, err = ut.T("max-items", fe.Field(), c) default: + // Обработка для time.Duration + if fe.Type() == reflect.TypeOf(time.Duration(0)) { + t, err = ut.T("max-number", fe.Field(), fe.Param()) + goto END + } + err = fn() + if err != nil { + goto END + } t, err = ut.T("max-number", fe.Field(), ut.FmtNumber(f64, digits)) } @@ -337,7 +446,6 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er fmt.Printf("warning: error translating FieldError: %s", err) return fe.(error).Error() } - return t }, }, @@ -416,7 +524,7 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er return } - if err = ut.Add("lt-datetime", "{0} must be less than the current Date & Time", false); err != nil { + if err = ut.Add("lt-datetime", "{0} должна быть раньше текущего момента", false); err != nil { return } @@ -549,7 +657,7 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er return } - if err = ut.Add("lte-datetime", "{0} must be less than or equal to the current Date & Time", false); err != nil { + if err = ut.Add("lte-datetime", "{0} должна быть раньше или равна текущему моменту", false); err != nil { return } @@ -636,269 +744,125 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er }, }, { - tag: "gt", - customRegisFunc: func(ut ut.Translator) (err error) { - if err = ut.Add("gt-string", "{0} должен быть длиннее {1}", false); err != nil { - return - } - - if err = ut.AddCardinal("gt-string-character", "{0} символ", locales.PluralRuleOne, false); err != nil { - return - } - - if err = ut.AddCardinal("gt-string-character", "{0} символов", locales.PluralRuleFew, false); err != nil { - return - } - - if err = ut.AddCardinal("gt-string-character", "{0} символов", locales.PluralRuleMany, false); err != nil { - return - } - - if err = ut.AddCardinal("gt-string-character", "{0} символы", locales.PluralRuleOther, false); err != nil { - return - } - - if err = ut.Add("gt-number", "{0} должен быть больше {1}", false); err != nil { - return - } - - if err = ut.Add("gt-items", "{0} должен содержать более {1}", false); err != nil { - return - } - - if err = ut.AddCardinal("gt-items-item", "{0} элемент", locales.PluralRuleOne, false); err != nil { - return - } - - if err = ut.AddCardinal("gt-items-item", "{0} элементов", locales.PluralRuleFew, false); err != nil { - return - } - - if err = ut.AddCardinal("gt-items-item", "{0} элементов", locales.PluralRuleMany, false); err != nil { - return - } - - if err = ut.AddCardinal("gt-items-item", "{0} элементы", locales.PluralRuleOther, false); err != nil { - return - } - - if err = ut.Add("gt-datetime", "{0} должна быть позже текущего момента", false); err != nil { - return - } - - return - }, + tag: "gte", + translation: "{0} должен содержать минимум {1}", + override: false, customTransFunc: func(ut ut.Translator, fe validator.FieldError) string { - var err error - var t string - var f64 float64 - var digits uint64 - var kind reflect.Kind - - fn := func() (err error) { - if idx := strings.Index(fe.Param(), "."); idx != -1 { - digits = uint64(len(fe.Param()[idx+1:])) - } - - f64, err = strconv.ParseFloat(fe.Param(), 64) - return - } + kind := fe.Kind() + typ := fe.Type() - kind = fe.Kind() if kind == reflect.Ptr { - kind = fe.Type().Elem().Kind() + kind = typ.Elem().Kind() + typ = typ.Elem() } switch kind { case reflect.String: - - var c string - - err = fn() - if err != nil { - goto END - } - - c, err = ut.C("gt-string-character", f64, digits, ut.FmtNumber(f64, digits)) - if err != nil { - goto END + numStr := fe.Param() + num, _ := strconv.Atoi(numStr) + + var word string + if num == 1 { + word = "символ" + } else if num >= 2 && num <= 4 { + word = "символа" + } else { + word = "символов" } - - t, err = ut.T("gt-string", fe.Field(), c) + return fmt.Sprintf("%s должен содержать минимум %s %s", + fe.Field(), numStr, word) case reflect.Slice, reflect.Map, reflect.Array: - var c string - - err = fn() - if err != nil { - goto END + numStr := fe.Param() + num, _ := strconv.Atoi(numStr) + + var word string + if num == 1 { + word = "элемент" + } else if num >= 2 && num <= 4 { + word = "элемента" + } else { + word = "элементов" } - - c, err = ut.C("gt-items-item", f64, digits, ut.FmtNumber(f64, digits)) - if err != nil { - goto END - } - - t, err = ut.T("gt-items", fe.Field(), c) + return fmt.Sprintf("%s должен содержать минимум %s %s", + fe.Field(), numStr, word) case reflect.Struct: - if fe.Type() != reflect.TypeOf(time.Time{}) { - err = fmt.Errorf("tag '%s' cannot be used on a struct type", fe.Tag()) - goto END + if typ == reflect.TypeOf(time.Time{}) { + return fmt.Sprintf("%s должна быть позже или равна текущему моменту", + fe.Field()) } - - t, err = ut.T("gt-datetime", fe.Field()) + fallthrough default: - err = fn() - if err != nil { - goto END - } - - t, err = ut.T("gt-number", fe.Field(), ut.FmtNumber(f64, digits)) - } - - END: - if err != nil { - fmt.Printf("warning: error translating FieldError: %s", err) - return fe.(error).Error() + number := strings.Replace(fe.Param(), ".", ",", -1) + return fmt.Sprintf("%s должен быть больше или равно %s", + fe.Field(), number) } - - return t }, }, { - tag: "gte", - customRegisFunc: func(ut ut.Translator) (err error) { - if err = ut.Add("gte-string", "{0} должен содержать минимум {1}", false); err != nil { - return - } - - if err = ut.AddCardinal("gte-string-character", "{0} символ", locales.PluralRuleOne, false); err != nil { - return - } - - if err = ut.AddCardinal("gte-string-character", "{0} символа", locales.PluralRuleFew, false); err != nil { - return - } - - if err = ut.AddCardinal("gte-string-character", "{0} символов", locales.PluralRuleMany, false); err != nil { - return - } - - if err = ut.AddCardinal("gte-string-character", "{0} символы", locales.PluralRuleOther, false); err != nil { - return - } - - if err = ut.Add("gte-number", "{0} должен быть больше или равно {1}", false); err != nil { - return - } - - if err = ut.Add("gte-items", "{0} должен содержать минимум {1}", false); err != nil { - return - } - - if err = ut.AddCardinal("gte-items-item", "{0} элемент", locales.PluralRuleOne, false); err != nil { - return - } - - if err = ut.AddCardinal("gte-items-item", "{0} элемента", locales.PluralRuleFew, false); err != nil { - return - } - - if err = ut.AddCardinal("gte-items-item", "{0} элементов", locales.PluralRuleMany, false); err != nil { - return - } - - if err = ut.AddCardinal("gte-items-item", "{0} элементы", locales.PluralRuleOther, false); err != nil { - return - } - - if err = ut.Add("gte-datetime", "{0} должна быть позже или равна текущему моменту", false); err != nil { - return - } - - return - }, + tag: "gt", + translation: "{0} должен быть больше {1}", + override: false, customTransFunc: func(ut ut.Translator, fe validator.FieldError) string { - var err error - var t string - var f64 float64 - var digits uint64 - var kind reflect.Kind - - fn := func() (err error) { - if idx := strings.Index(fe.Param(), "."); idx != -1 { - digits = uint64(len(fe.Param()[idx+1:])) - } - f64, err = strconv.ParseFloat(fe.Param(), 64) + kind := fe.Kind() + typ := fe.Type() - return - } + log.Printf("DEBUG - gt tag: Field=%s, Kind=%v, Type=%v, Param=%q", + fe.Field(), kind, typ, fe.Param()) - kind = fe.Kind() if kind == reflect.Ptr { - kind = fe.Type().Elem().Kind() + kind = typ.Elem().Kind() + typ = typ.Elem() } switch kind { case reflect.String: - var c string + numStr := fe.Param() + num, _ := strconv.Atoi(numStr) - err = fn() - if err != nil { - goto END + var word string + if num == 1 { + word = "символ" + } else if num >= 2 && num <= 4 { + word = "символа" + } else { + word = "символов" } - - c, err = ut.C("gte-string-character", f64, digits, ut.FmtNumber(f64, digits)) - if err != nil { - goto END - } - - t, err = ut.T("gte-string", fe.Field(), c) + return fmt.Sprintf("%s должен быть длиннее %s %s", + fe.Field(), numStr, word) case reflect.Slice, reflect.Map, reflect.Array: - var c string - - err = fn() - if err != nil { - goto END - } - - c, err = ut.C("gte-items-item", f64, digits, ut.FmtNumber(f64, digits)) - if err != nil { - goto END + numStr := fe.Param() + num, _ := strconv.Atoi(numStr) + + var word string + if num == 1 { + word = "элемент" + } else if num >= 2 && num <= 4 { + word = "элемента" + } else { + word = "элементов" } - - t, err = ut.T("gte-items", fe.Field(), c) + return fmt.Sprintf("%s должен содержать более %s %s", + fe.Field(), numStr, word) case reflect.Struct: - if fe.Type() != reflect.TypeOf(time.Time{}) { - err = fmt.Errorf("tag '%s' cannot be used on a struct type", fe.Tag()) - goto END + if typ == reflect.TypeOf(time.Time{}) { + return fmt.Sprintf("%s должна быть позже текущего момента", + fe.Field()) } - - t, err = ut.T("gte-datetime", fe.Field()) + fallthrough default: - err = fn() - if err != nil { - goto END - } - - t, err = ut.T("gte-number", fe.Field(), ut.FmtNumber(f64, digits)) - } - - END: - if err != nil { - fmt.Printf("warning: error translating FieldError: %s", err) - return fe.(error).Error() + number := strings.Replace(fe.Param(), ".", ",", -1) + return fmt.Sprintf("%s должен быть больше %s", + fe.Field(), number) } - - return t }, }, { @@ -1046,12 +1010,11 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er translation: "{0} должен быть менее {1}", override: false, customTransFunc: func(ut ut.Translator, fe validator.FieldError) string { - t, err := ut.T(fe.Tag(), fe.Field(), fe.Param()) + t, err := ut.T("ltfield", fe.Field(), fe.Param()) if err != nil { log.Printf("warning: error translating FieldError: %#v", fe) return fe.(error).Error() } - return t }, }, @@ -1065,7 +1028,6 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er log.Printf("warning: error translating FieldError: %#v", fe) return fe.(error).Error() } - return t }, }, @@ -1079,6 +1041,27 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er translation: "{0} должен содержать только буквы и цифры", override: false, }, + // Добавленные строковые unicode/space теги + { + tag: "alphaspace", + translation: "{0} может содержать только буквы и пробелы", + override: false, + }, + { + tag: "alphanumspace", + translation: "{0} может содержать только буквы, цифры и пробелы", + override: false, + }, + { + tag: "alphaunicode", + translation: "{0} может содержать только Unicode буквы", + override: false, + }, + { + tag: "alphanumunicode", + translation: "{0} может содержать только Unicode буквы и цифры", + override: false, + }, { tag: "numeric", translation: "{0} должен быть цифровым значением", @@ -1407,6 +1390,79 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er translation: "{0} должно быть допустимым изображением", override: false, }, + + // Новые/добавленные простые теги (из en) + { + tag: "lowercase", + translation: "{0} должен быть в нижнем регистре", + override: false, + }, + { + tag: "uppercase", + translation: "{0} должен быть в верхнем регистре", + override: false, + }, + { + tag: "boolean", + translation: "{0} должен быть логическим значением", + override: false, + }, + { + tag: "json", + translation: "{0} должен быть корректной JSON строкой", + override: false, + }, + { + tag: "jwt", + translation: "{0} должен быть допустимым JWT", + override: false, + }, + { + tag: "cron", + translation: "{0} должен быть валидным cron выражением", + override: false, + }, + { + tag: "cve", + translation: "{0} должен быть корректным идентификатором CVE", + override: false, + }, + { + tag: "validateFn", + translation: "{0} должен быть допустимым объектом", + override: false, + }, + { + tag: "postcode_iso3166_alpha2", + translation: "{0} не соответствует формату почтового индекса страны {1}", + override: false, + customTransFunc: func(ut ut.Translator, fe validator.FieldError) string { + t, err := ut.T(fe.Tag(), fe.Field(), fe.Param()) + if err != nil { + log.Printf("warning: error translating FieldError: %#v", fe) + return fe.(error).Error() + } + return t + }, + }, + { + tag: "postcode_iso3166_alpha2_field", + translation: "{0} не соответствует формату почтового индекса страны в поле {1}", + override: false, + customTransFunc: func(ut ut.Translator, fe validator.FieldError) string { + t, err := ut.T(fe.Tag(), fe.Field(), fe.Param()) + if err != nil { + log.Printf("warning: error translating FieldError: %#v", fe) + return fe.(error).Error() + } + return t + }, + }, + { + tag: "json_err", + translation: "{0} должен быть корректной JSON строкой", + override: false, + }, } for _, t := range translations { diff --git a/translations/ru/ru_test.go b/translations/ru/ru_test.go index 2885d36c..cc181501 100644 --- a/translations/ru/ru_test.go +++ b/translations/ru/ru_test.go @@ -23,12 +23,24 @@ func TestTranslations(t *testing.T) { Equal(t, err, nil) type Inner struct { - EqCSFieldString string - NeCSFieldString string - GtCSFieldString string - GteCSFieldString string - LtCSFieldString string - LteCSFieldString string + EqCSFieldString string + NeCSFieldString string + GtCSFieldString string + GteCSFieldString string + LtCSFieldString string + LteCSFieldString string + RequiredIf string + RequiredUnless string + RequiredWith string + RequiredWithAll string + RequiredWithout string + RequiredWithoutAll string + ExcludedIf string + ExcludedUnless string + ExcludedWith string + ExcludedWithAll string + ExcludedWithout string + ExcludedWithoutAll string } type Test struct { @@ -165,6 +177,22 @@ func TestTranslations(t *testing.T) { UniqueArray [3]string `validate:"unique"` UniqueMap map[string]string `validate:"unique"` Image string `validate:"image"` + RequiredIf string `validate:"required_if=Inner.RequiredIf abcd"` + RequiredUnless string `validate:"required_unless=Inner.RequiredUnless abcd"` + RequiredWith string `validate:"required_with=Inner.RequiredWith"` + RequiredWithAll string `validate:"required_with_all=Inner.RequiredWith Inner.RequiredWithAll"` + RequiredWithout string `validate:"required_without=Inner.RequiredWithout"` + RequiredWithoutAll string `validate:"required_without_all=Inner.RequiredWithout Inner.RequiredWithoutAll"` + ExcludedIf string `validate:"excluded_if=Inner.ExcludedIf abcd"` + ExcludedUnless string `validate:"excluded_unless=Inner.ExcludedUnless abcd"` + ExcludedWith string `validate:"excluded_with=Inner.ExcludedWith"` + ExcludedWithout string `validate:"excluded_with_all=Inner.ExcludedWithAll"` + ExcludedWithAll string `validate:"excluded_without=Inner.ExcludedWithout"` + ExcludedWithoutAll string `validate:"excluded_without_all=Inner.ExcludedWithoutAll"` + IsDefault string `validate:"isdefault"` + URN string `validate:"urn_rfc2141"` + FQDN string `validate:"fqdn"` + DateTime string `validate:"datetime=2006-01-02"` } var test Test @@ -199,6 +227,15 @@ func TestTranslations(t *testing.T) { test.LtCSFieldString = "1234" test.LteCSFieldString = "1234" + test.Inner.RequiredIf = "abcd" + test.Inner.RequiredUnless = "1234" + test.Inner.RequiredWith = "1234" + test.Inner.RequiredWithAll = "1234" + test.Inner.ExcludedIf = "abcd" + test.Inner.ExcludedUnless = "1234" + test.Inner.ExcludedWith = "1234" + test.Inner.ExcludedWithAll = "1234" + test.AlphaString = "abc3" test.AlphanumString = "abc3!" test.NumericString = "12E.00" @@ -220,6 +257,36 @@ func TestTranslations(t *testing.T) { test.UniqueSlice = []string{"1234", "1234"} test.UniqueMap = map[string]string{"key1": "1234", "key2": "1234"} + // Инициализация для новых полей + test.RequiredIf = "" + test.RequiredUnless = "" + test.RequiredWith = "" + test.RequiredWithAll = "" + test.RequiredWithout = "" + test.RequiredWithoutAll = "" + test.ExcludedIf = "1234" + test.ExcludedUnless = "1234" + test.ExcludedWith = "1234" + test.ExcludedWithout = "1234" + test.ExcludedWithAll = "1234" + test.ExcludedWithoutAll = "1234" + test.IsDefault = "not default" + test.URN = "invalid" + test.FQDN = "invalid" + test.DateTime = "2008-Feb-01" + + test.ExcludedIf = "1234" + test.ExcludedUnless = "1234" + test.ExcludedWith = "1234" + test.ExcludedWithAll = "1234" + test.ExcludedWithout = "1234" + test.ExcludedWithoutAll = "1234" + + test.IsDefault = "not default" + test.URN = "invalid" + test.FQDN = "invalid" + test.DateTime = "2008-Feb-01" + err = validate.Struct(test) NotEqual(t, err, nil) @@ -512,7 +579,7 @@ func TestTranslations(t *testing.T) { }, { ns: "Test.GtString", - expected: "GtString должен быть длиннее 3 символов", + expected: "GtString должен быть длиннее 3 символа", }, { ns: "Test.GtStringSecond", @@ -524,7 +591,7 @@ func TestTranslations(t *testing.T) { }, { ns: "Test.GtMultiple", - expected: "GtMultiple должен содержать более 2 элементов", + expected: "GtMultiple должен содержать более 2 элемента", }, { ns: "Test.GtMultipleSecond", @@ -556,7 +623,7 @@ func TestTranslations(t *testing.T) { }, { ns: "Test.LteTime", - expected: "LteTime must be less than or equal to the current Date & Time", + expected: "LteTime должна быть раньше или равна текущему моменту", }, { ns: "Test.LtString", @@ -580,7 +647,7 @@ func TestTranslations(t *testing.T) { }, { ns: "Test.LtTime", - expected: "LtTime must be less than the current Date & Time", + expected: "LtTime должна быть раньше текущего момента", }, { ns: "Test.NeString", @@ -720,7 +787,7 @@ func TestTranslations(t *testing.T) { }, { ns: "Test.StrPtrGtSecond", - expected: "StrPtrGtSecond должен быть длиннее 2 символов", + expected: "StrPtrGtSecond должен быть длиннее 2 символа", }, { ns: "Test.StrPtrGte", @@ -754,6 +821,70 @@ func TestTranslations(t *testing.T) { ns: "Test.Image", expected: "Image должно быть допустимым изображением", }, + { + ns: "Test.RequiredIf", + expected: "RequiredIf обязательное поле", + }, + { + ns: "Test.RequiredUnless", + expected: "RequiredUnless обязательное поле", + }, + { + ns: "Test.RequiredWith", + expected: "RequiredWith обязательное поле", + }, + { + ns: "Test.RequiredWithAll", + expected: "RequiredWithAll обязательное поле", + }, + { + ns: "Test.RequiredWithout", + expected: "RequiredWithout обязательное поле", + }, + { + ns: "Test.RequiredWithoutAll", + expected: "RequiredWithoutAll обязательное поле", + }, + { + ns: "Test.ExcludedIf", + expected: "ExcludedIf должно быть исключено", + }, + { + ns: "Test.ExcludedUnless", + expected: "ExcludedUnless должно быть исключено", + }, + { + ns: "Test.ExcludedWith", + expected: "ExcludedWith должно быть исключено", + }, + { + ns: "Test.ExcludedWithout", + expected: "ExcludedWithout должно быть исключено", + }, + { + ns: "Test.ExcludedWithAll", + expected: "ExcludedWithAll должно быть исключено", + }, + { + ns: "Test.ExcludedWithoutAll", + expected: "ExcludedWithoutAll должно быть исключено", + }, + { + ns: "Test.IsDefault", + expected: "IsDefault должно быть значением по умолчанию", + }, + { + ns: "Test.URN", + expected: "URN должен быть корректным URN согласно RFC 2141", + }, + { + ns: "Test.FQDN", + expected: "FQDN должен быть корректным полным доменным именем (FQDN)", + }, + { + ns: "Test.DateTime", + expected: "DateTime не соответствует формату 2006-01-02", + }, } for _, tt := range tests { From de85568dc88a56158a2f45d429dbd944237d6987 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 1 Mar 2026 23:39:35 +0300 Subject: [PATCH 2/3] style(ru): remove unnecessary newlines in customTransFunc --- translations/ru/ru.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/translations/ru/ru.go b/translations/ru/ru.go index 21e3e82e..cd63636e 100644 --- a/translations/ru/ru.go +++ b/translations/ru/ru.go @@ -748,7 +748,6 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er translation: "{0} должен содержать минимум {1}", override: false, customTransFunc: func(ut ut.Translator, fe validator.FieldError) string { - kind := fe.Kind() typ := fe.Type() @@ -807,7 +806,6 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er translation: "{0} должен быть больше {1}", override: false, customTransFunc: func(ut ut.Translator, fe validator.FieldError) string { - kind := fe.Kind() typ := fe.Type() From 4c6f219ad379a5e980c0c3450eba2a56350ba0e4 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 5 Mar 2026 15:18:28 +0300 Subject: [PATCH 3/3] fix(ru): address review comments - fix plurals and cleanup --- translations/ru/ru.go | 225 +++++++++++++++++++++++++++---------- translations/ru/ru_test.go | 14 +-- 2 files changed, 166 insertions(+), 73 deletions(-) diff --git a/translations/ru/ru.go b/translations/ru/ru.go index cd63636e..15a1c7a6 100644 --- a/translations/ru/ru.go +++ b/translations/ru/ru.go @@ -315,7 +315,7 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er t, err = ut.T("min-items", fe.Field(), c) default: - // Обработка для time.Duration + // Processing for time.Duration if fe.Type() == reflect.TypeOf(time.Duration(0)) { t, err = ut.T("min-number", fe.Field(), fe.Param()) goto END @@ -429,7 +429,7 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er t, err = ut.T("max-items", fe.Field(), c) default: - // Обработка для time.Duration + // Processing for time.Duration if fe.Type() == reflect.TypeOf(time.Duration(0)) { t, err = ut.T("max-number", fe.Field(), fe.Param()) goto END @@ -744,9 +744,57 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er }, }, { - tag: "gte", - translation: "{0} должен содержать минимум {1}", - override: false, + tag: "gte", + customRegisFunc: func(ut ut.Translator) (err error) { + if err = ut.Add("gte-string", "{0} должен содержать минимум {1}", false); err != nil { + return + } + + if err = ut.AddCardinal("gte-string-character", "{0} символ", locales.PluralRuleOne, false); err != nil { + return + } + + if err = ut.AddCardinal("gte-string-character", "{0} символа", locales.PluralRuleFew, false); err != nil { + return + } + + if err = ut.AddCardinal("gte-string-character", "{0} символов", locales.PluralRuleMany, false); err != nil { + return + } + + if err = ut.AddCardinal("gte-string-character", "{0} символы", locales.PluralRuleOther, false); err != nil { + return + } + + if err = ut.Add("gte-number", "{0} должен быть больше или равно {1}", false); err != nil { + return + } + + if err = ut.Add("gte-items", "{0} должен содержать минимум {1}", false); err != nil { + return + } + if err = ut.AddCardinal("gte-items-item", "{0} элемент", locales.PluralRuleOne, false); err != nil { + return + } + + if err = ut.AddCardinal("gte-items-item", "{0} элемента", locales.PluralRuleFew, false); err != nil { + return + } + + if err = ut.AddCardinal("gte-items-item", "{0} элементов", locales.PluralRuleMany, false); err != nil { + return + } + + if err = ut.AddCardinal("gte-items-item", "{0} элементы", locales.PluralRuleOther, false); err != nil { + return + } + + if err = ut.Add("gte-datetime", "{0} должна быть позже или равна текущему моменту", false); err != nil { + return + } + + return + }, customTransFunc: func(ut ut.Translator, fe validator.FieldError) string { kind := fe.Kind() typ := fe.Type() @@ -758,34 +806,40 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er switch kind { case reflect.String: - numStr := fe.Param() - num, _ := strconv.Atoi(numStr) - - var word string - if num == 1 { - word = "символ" - } else if num >= 2 && num <= 4 { - word = "символа" - } else { - word = "символов" + num, _ := strconv.ParseFloat(fe.Param(), 64) + digits := uint64(0) + if idx := strings.Index(fe.Param(), "."); idx != -1 { + digits = uint64(len(fe.Param()[idx+1:])) + } + + c, err := ut.C("gte-string-character", num, digits, ut.FmtNumber(num, digits)) + if err != nil { + return fe.(error).Error() } - return fmt.Sprintf("%s должен содержать минимум %s %s", - fe.Field(), numStr, word) + + t, err := ut.T("gte-string", fe.Field(), c) + if err != nil { + return fe.(error).Error() + } + return t case reflect.Slice, reflect.Map, reflect.Array: - numStr := fe.Param() - num, _ := strconv.Atoi(numStr) - - var word string - if num == 1 { - word = "элемент" - } else if num >= 2 && num <= 4 { - word = "элемента" - } else { - word = "элементов" + num, _ := strconv.ParseFloat(fe.Param(), 64) + digits := uint64(0) + if idx := strings.Index(fe.Param(), "."); idx != -1 { + digits = uint64(len(fe.Param()[idx+1:])) } - return fmt.Sprintf("%s должен содержать минимум %s %s", - fe.Field(), numStr, word) + + c, err := ut.C("gte-items-item", num, digits, ut.FmtNumber(num, digits)) + if err != nil { + return fe.(error).Error() + } + + t, err := ut.T("gte-items", fe.Field(), c) + if err != nil { + return fe.(error).Error() + } + return t case reflect.Struct: if typ == reflect.TypeOf(time.Time{}) { @@ -802,16 +856,62 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er }, }, { - tag: "gt", - translation: "{0} должен быть больше {1}", - override: false, + tag: "gt", + customRegisFunc: func(ut ut.Translator) (err error) { + if err = ut.Add("gt-string", "{0} должен быть длиннее {1}", false); err != nil { + return + } + + if err = ut.AddCardinal("gt-string-character", "{0} символ", locales.PluralRuleOne, false); err != nil { + return + } + + if err = ut.AddCardinal("gt-string-character", "{0} символа", locales.PluralRuleFew, false); err != nil { + return + } + + if err = ut.AddCardinal("gt-string-character", "{0} символов", locales.PluralRuleMany, false); err != nil { + return + } + + if err = ut.AddCardinal("gt-string-character", "{0} символы", locales.PluralRuleOther, false); err != nil { + return + } + + if err = ut.Add("gt-number", "{0} должен быть больше {1}", false); err != nil { + return + } + + if err = ut.Add("gt-items", "{0} должен содержать более {1}", false); err != nil { + return + } + + if err = ut.AddCardinal("gt-items-item", "{0} элемент", locales.PluralRuleOne, false); err != nil { + return + } + + if err = ut.AddCardinal("gt-items-item", "{0} элемента", locales.PluralRuleFew, false); err != nil { + return + } + + if err = ut.AddCardinal("gt-items-item", "{0} элементов", locales.PluralRuleMany, false); err != nil { + return + } + + if err = ut.AddCardinal("gt-items-item", "{0} элементы", locales.PluralRuleOther, false); err != nil { + return + } + + if err = ut.Add("gt-datetime", "{0} должна быть позже текущего момента", false); err != nil { + return + } + + return + }, customTransFunc: func(ut ut.Translator, fe validator.FieldError) string { kind := fe.Kind() typ := fe.Type() - log.Printf("DEBUG - gt tag: Field=%s, Kind=%v, Type=%v, Param=%q", - fe.Field(), kind, typ, fe.Param()) - if kind == reflect.Ptr { kind = typ.Elem().Kind() typ = typ.Elem() @@ -819,35 +919,40 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er switch kind { case reflect.String: + num, _ := strconv.ParseFloat(fe.Param(), 64) + digits := uint64(0) + if idx := strings.Index(fe.Param(), "."); idx != -1 { + digits = uint64(len(fe.Param()[idx+1:])) + } - numStr := fe.Param() - num, _ := strconv.Atoi(numStr) + c, err := ut.C("gt-string-character", num, digits, ut.FmtNumber(num, digits)) + if err != nil { + return fe.(error).Error() + } - var word string - if num == 1 { - word = "символ" - } else if num >= 2 && num <= 4 { - word = "символа" - } else { - word = "символов" + t, err := ut.T("gt-string", fe.Field(), c) + if err != nil { + return fe.(error).Error() } - return fmt.Sprintf("%s должен быть длиннее %s %s", - fe.Field(), numStr, word) + return t case reflect.Slice, reflect.Map, reflect.Array: - numStr := fe.Param() - num, _ := strconv.Atoi(numStr) - - var word string - if num == 1 { - word = "элемент" - } else if num >= 2 && num <= 4 { - word = "элемента" - } else { - word = "элементов" + num, _ := strconv.ParseFloat(fe.Param(), 64) + digits := uint64(0) + if idx := strings.Index(fe.Param(), "."); idx != -1 { + digits = uint64(len(fe.Param()[idx+1:])) + } + + c, err := ut.C("gt-items-item", num, digits, ut.FmtNumber(num, digits)) + if err != nil { + return fe.(error).Error() + } + + t, err := ut.T("gt-items", fe.Field(), c) + if err != nil { + return fe.(error).Error() } - return fmt.Sprintf("%s должен содержать более %s %s", - fe.Field(), numStr, word) + return t case reflect.Struct: if typ == reflect.TypeOf(time.Time{}) { @@ -1039,7 +1144,7 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er translation: "{0} должен содержать только буквы и цифры", override: false, }, - // Добавленные строковые unicode/space теги + // Added unicode/space tags { tag: "alphaspace", translation: "{0} может содержать только буквы и пробелы", @@ -1389,7 +1494,7 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er override: false, }, - // Новые/добавленные простые теги (из en) + // New tags (from en) { tag: "lowercase", translation: "{0} должен быть в нижнем регистре", diff --git a/translations/ru/ru_test.go b/translations/ru/ru_test.go index cc181501..8a40cb6f 100644 --- a/translations/ru/ru_test.go +++ b/translations/ru/ru_test.go @@ -257,7 +257,7 @@ func TestTranslations(t *testing.T) { test.UniqueSlice = []string{"1234", "1234"} test.UniqueMap = map[string]string{"key1": "1234", "key2": "1234"} - // Инициализация для новых полей + // Initialization for new fields test.RequiredIf = "" test.RequiredUnless = "" test.RequiredWith = "" @@ -275,18 +275,6 @@ func TestTranslations(t *testing.T) { test.FQDN = "invalid" test.DateTime = "2008-Feb-01" - test.ExcludedIf = "1234" - test.ExcludedUnless = "1234" - test.ExcludedWith = "1234" - test.ExcludedWithAll = "1234" - test.ExcludedWithout = "1234" - test.ExcludedWithoutAll = "1234" - - test.IsDefault = "not default" - test.URN = "invalid" - test.FQDN = "invalid" - test.DateTime = "2008-Feb-01" - err = validate.Struct(test) NotEqual(t, err, nil)