Skip to content

Commit

Permalink
feat: i18n package with spanish & english translations (#28)
Browse files Browse the repository at this point in the history
* feat: added fallback error message to map

* feat: i18n package

* refactor: move default map to i18n/en package

* feat: Add new es package to i18n (#29) credit @cachesdev 

* Add new zes package which contains spanish translations for validation errors

* Remove articles and change `Coleccion` to `Lista`

* Fix typo

* Change package name zes -> es

* fix: import

* docs: i18n package

* feat: option to choose lang key

* docs: added i18n docs

---------

Co-authored-by: Gustavo Dominguez <59543366+cachesdev@users.noreply.github.com>
  • Loading branch information
Oudwins and cachesdev authored Sep 19, 2024
1 parent 166d881 commit 1120fd6
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 71 deletions.
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ Killer Features:
- Almost no reflection when using primitive types
- **Built-in coercion** support for most types
- Zero dependencies!
- **Two Helper Packages**
- **Three Helper Packages**
- **zenv**: parse environment variables
- **zhttp**: parse http forms & query params
- **i18n**: Opinionated solution to good i18n zog errors

> **API Stability:**
>
Expand Down Expand Up @@ -151,9 +152,9 @@ Most of these things are issues we would like to address in future versions.
- Validations and parsing cannot be run separately
- It is not recommended to use very deeply nested schemas since that requires a lot of reflection and can have a negative impact on performance

## Helper Packages (zenv & zhttp)
## Helper Packages

For convenience zog provides two helper packages:
For convenience zog provides three helper packages:

**zenv: helps validate environment variables**

Expand Down Expand Up @@ -218,6 +219,31 @@ func handlePostRequest(w http.ResponseWriter, r *http.Request) {

```

**zi18n: helps with having error messages in multiple languages**

```go
// Somewhere when you start your app
import (
"github.com/Oudwins/zog/i18n"
"github.com/Oudwins/zog/i18n/es"
"github.com/Oudwins/zog/i18n/en"
)
i18n.SetLanguagesErrsMap(map[string]i18n.LangMap{
"es": es.Map,
"en": en.Map,
},
"es", // default language
i18n.WithLangKey("langKey"), // default lang key is "lang"
)


// Now when we parse
schema.Parse(data, &dest, z.WithCtxValue("langKey", "es")) // get spanish errors
schema.Parse(data, &dest, z.WithCtxValue("langKey", "en")) // get english errors
schema.Parse(data, &dest) // get default lang errors (spanish in this case)

```

## Parsing Context

Zog uses a `ParseCtx` to pass around information related to a specific `schema.Parse()` call. Currently use of the parse context is quite limited but it will be expanded upon in the future.
Expand Down
89 changes: 24 additions & 65 deletions conf/Errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,79 +6,38 @@ import (
"fmt"
"strings"

"github.com/Oudwins/zog/i18n/en"
p "github.com/Oudwins/zog/internals"
zconst "github.com/Oudwins/zog/zconst"
"github.com/Oudwins/zog/zconst"
)

// Default error messages for all schemas. Replace the text with your own messages to customize the error messages for all zog schemas
// As a general rule of thumb, if an error message only has one parameter, the parameter name will be the same as the error code
var DefaultErrMsgMap = map[zconst.ZogType]map[zconst.ZogErrCode]string{
zconst.TypeString: {
zconst.ErrCodeRequired: "is required",
zconst.ErrCodeMin: "string must contain at least {{min}} character(s)",
zconst.ErrCodeMax: "string must contain at most {{min}} character(s)",
zconst.ErrCodeLen: "string must be exactly {{len}} character(s)",
zconst.ErrCodeEmail: "must be a valid email",
zconst.ErrCodeURL: "must be a valid URL",
zconst.ErrCodeHasPrefix: "string must start with {{prefix}}",
zconst.ErrCodeHasSuffix: "string must end with {{suffix}}",
zconst.ErrCodeContains: "string must contain {{contained}}",
zconst.ErrCodeContainsDigit: "string must contain at least one digit",
zconst.ErrCodeContainsUpper: "string must contain at least one uppercase letter",
zconst.ErrCodeContainsLower: "string must contain at least one lowercase letter",
zconst.ErrCodeContainsSpecial: "string must contain at least one special character",
zconst.ErrCodeOneOf: "string must be one of {{one_of_options}}",
},
zconst.TypeBool: {
zconst.ErrCodeRequired: "is required",
zconst.ErrCodeTrue: "must be true",
zconst.ErrCodeFalse: "must be false",
},
zconst.TypeNumber: {
zconst.ErrCodeRequired: "is required",
zconst.ErrCodeLTE: "number must be less than or equal to {{lte}}",
zconst.ErrCodeLT: "number must be less than {{lt}}",
zconst.ErrCodeGTE: "number must be greater than or equal to {{gte}}",
zconst.ErrCodeGT: "number must be greater than {{gt}}",
zconst.ErrCodeEQ: "number must be equal to {{eq}}",
zconst.ErrCodeOneOf: "number must be one of {{options}}",
},
zconst.TypeTime: {
zconst.ErrCodeRequired: "is required",
zconst.ErrCodeAfter: "time must be after {{after}}",
zconst.ErrCodeBefore: "time must be before {{before}}",
zconst.ErrCodeEQ: "time must be equal to {{eq}}",
},
zconst.TypeSlice: {
zconst.ErrCodeRequired: "is required",
zconst.ErrCodeMin: "slice must contain at least {{min}} items",
zconst.ErrCodeMax: "slice must contain at most {{max}} items",
zconst.ErrCodeLen: "slice must contain exactly {{len}} items",
zconst.ErrCodeContains: "slice must contain {{contained}}",
},
zconst.TypeStruct: {
zconst.ErrCodeRequired: "is required",
},
}
var DefaultErrMsgMap zconst.LangMap = en.Map

// Default error formatter it uses the errors above. Please override the `ErrorFormatter` variable instead of this one to customize the error messages for all zog schemas
var DefaultErrorFormatter p.ErrFmtFunc = func(e p.ZogError, p p.ParseCtx) {
if e.Message() != "" {
return
}
// Check if the error msg is defined do nothing if it set
t := e.Dtype()
msg, ok := DefaultErrMsgMap[t][e.Code()]
if !ok {
e.SetMessage(t + " is invalid")
return
}
for k, v := range e.Params() {
// TODO replace this with a string builder
msg = strings.ReplaceAll(msg, "{{"+k+"}}", fmt.Sprintf("%v", v))
func NewDefaultFormatter(m zconst.LangMap) p.ErrFmtFunc {
return func(e p.ZogError, p p.ParseCtx) {
if e.Message() != "" {
return
}
// Check if the error msg is defined do nothing if it set
t := e.Dtype()
msg, ok := m[t][e.Code()]
if !ok {
e.SetMessage(m[t][zconst.ErrCodeFallback])
return
}
for k, v := range e.Params() {
// TODO replace this with a string builder
msg = strings.ReplaceAll(msg, "{{"+k+"}}", fmt.Sprintf("%v", v))
}
e.SetMessage(msg)
}
e.SetMessage(msg)

}

// Default error formatter it uses the errors above. Please override the `ErrorFormatter` variable instead of this one to customize the error messages for all zog schemas
var DefaultErrorFormatter p.ErrFmtFunc = NewDefaultFormatter(DefaultErrMsgMap)

// Override this
var ErrorFormatter = DefaultErrorFormatter
60 changes: 60 additions & 0 deletions i18n/en/en.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package en

import (
"github.com/Oudwins/zog/zconst"
)

var Map zconst.LangMap = map[zconst.ZogType]map[zconst.ZogErrCode]string{
zconst.TypeString: {
zconst.ErrCodeRequired: "is required",
zconst.ErrCodeMin: "string must contain at least {{min}} character(s)",
zconst.ErrCodeMax: "string must contain at most {{max}} character(s)",
zconst.ErrCodeLen: "string must be exactly {{len}} character(s)",
zconst.ErrCodeEmail: "must be a valid email",
zconst.ErrCodeURL: "must be a valid URL",
zconst.ErrCodeHasPrefix: "string must start with {{prefix}}",
zconst.ErrCodeHasSuffix: "string must end with {{suffix}}",
zconst.ErrCodeContains: "string must contain {{contained}}",
zconst.ErrCodeContainsDigit: "string must contain at least one digit",
zconst.ErrCodeContainsUpper: "string must contain at least one uppercase letter",
zconst.ErrCodeContainsLower: "string must contain at least one lowercase letter",
zconst.ErrCodeContainsSpecial: "string must contain at least one special character",
zconst.ErrCodeOneOf: "string must be one of {{one_of_options}}",
zconst.ErrCodeFallback: "string is invalid",
},
zconst.TypeBool: {
zconst.ErrCodeRequired: "is required",
zconst.ErrCodeTrue: "must be true",
zconst.ErrCodeFalse: "must be false",
zconst.ErrCodeFallback: "value is invalid",
},
zconst.TypeNumber: {
zconst.ErrCodeRequired: "is required",
zconst.ErrCodeLTE: "number must be less than or equal to {{lte}}",
zconst.ErrCodeLT: "number must be less than {{lt}}",
zconst.ErrCodeGTE: "number must be greater than or equal to {{gte}}",
zconst.ErrCodeGT: "number must be greater than {{gt}}",
zconst.ErrCodeEQ: "number must be equal to {{eq}}",
zconst.ErrCodeOneOf: "number must be one of {{options}}",
zconst.ErrCodeFallback: "number is invalid",
},
zconst.TypeTime: {
zconst.ErrCodeRequired: "is required",
zconst.ErrCodeAfter: "time must be after {{after}}",
zconst.ErrCodeBefore: "time must be before {{before}}",
zconst.ErrCodeEQ: "time must be equal to {{eq}}",
zconst.ErrCodeFallback: "time is invalid",
},
zconst.TypeSlice: {
zconst.ErrCodeRequired: "is required",
zconst.ErrCodeMin: "slice must contain at least {{min}} items",
zconst.ErrCodeMax: "slice must contain at most {{max}} items",
zconst.ErrCodeLen: "slice must contain exactly {{len}} items",
zconst.ErrCodeContains: "slice must contain {{contained}}",
zconst.ErrCodeFallback: "slice is invalid",
},
zconst.TypeStruct: {
zconst.ErrCodeRequired: "is required",
zconst.ErrCodeFallback: "struct is invalid",
},
}
60 changes: 60 additions & 0 deletions i18n/es/es.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package es

import (
"github.com/Oudwins/zog/zconst"
)

var Map zconst.LangMap = map[zconst.ZogType]map[zconst.ZogErrCode]string{
zconst.TypeString: {
zconst.ErrCodeRequired: "Es obligatorio",
zconst.ErrCodeMin: "Cadena debe contener al menos {{min}} caracter(es)",
zconst.ErrCodeMax: "Cadena debe contener como máximo {{max}} caracter(es)",
zconst.ErrCodeLen: "Cadena debe tener exactamente {{len}} caracter(es)",
zconst.ErrCodeEmail: "Debe ser un correo electrónico válido",
zconst.ErrCodeURL: "Debe ser una URL válida",
zconst.ErrCodeHasPrefix: "Cadena debe comenzar con {{prefix}}",
zconst.ErrCodeHasSuffix: "Cadena debe terminar con {{suffix}}",
zconst.ErrCodeContains: "Cadena debe contener {{contained}}",
zconst.ErrCodeContainsDigit: "Cadena debe contener al menos un dígito",
zconst.ErrCodeContainsUpper: "Cadena debe contener al menos una letra mayúscula",
zconst.ErrCodeContainsLower: "Cadena debe contener al menos una letra minúscula",
zconst.ErrCodeContainsSpecial: "Cadena debe contener al menos un carácter especial",
zconst.ErrCodeOneOf: "Cadena debe ser una de las siguientes: {{one_of_options}}",
zconst.ErrCodeFallback: "Cadena no es válida",
},
zconst.TypeBool: {
zconst.ErrCodeRequired: "Es obligatorio",
zconst.ErrCodeTrue: "Debe ser verdadero",
zconst.ErrCodeFalse: "Debe ser falso",
zconst.ErrCodeFallback: "Valor no es válido",
},
zconst.TypeNumber: {
zconst.ErrCodeRequired: "Es obligatorio",
zconst.ErrCodeLTE: "Número debe ser menor o igual a {{lte}}",
zconst.ErrCodeLT: "Número debe ser menor que {{lt}}",
zconst.ErrCodeGTE: "Número debe ser mayor o igual a {{gte}}",
zconst.ErrCodeGT: "Número debe ser mayor que {{gt}}",
zconst.ErrCodeEQ: "Número debe ser igual a {{eq}}",
zconst.ErrCodeOneOf: "Número debe ser uno de los siguientes: {{options}}",
zconst.ErrCodeFallback: "Número no es válido",
},
zconst.TypeTime: {
zconst.ErrCodeRequired: "Es obligatorio",
zconst.ErrCodeAfter: "Fecha debe ser posterior a {{after}}",
zconst.ErrCodeBefore: "Fecha debe ser anterior a {{before}}",
zconst.ErrCodeEQ: "Fecha debe ser igual a {{eq}}",
zconst.ErrCodeFallback: "Fecha no es válida",
},
zconst.TypeSlice: {
zconst.ErrCodeRequired: "Es obligatorio",
zconst.ErrCodeMin: "Lista debe contener al menos {{min}} elementos",
zconst.ErrCodeMax: "Lista debe contener como máximo {{max}} elementos",
zconst.ErrCodeLen: "Lista debe contener exactamente {{len}} elementos",
zconst.ErrCodeContains: "Lista debe contener {{contained}}",
zconst.ErrCodeFallback: "Lista no es válida",
},
zconst.TypeStruct: {
zconst.ErrCodeRequired: "Es obligatorio",
zconst.ErrCodeFallback: "Estructura no es válida",
},
}
46 changes: 46 additions & 0 deletions i18n/i18n.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package i18n

import (
"github.com/Oudwins/zog/conf"
"github.com/Oudwins/zog/internals"
"github.com/Oudwins/zog/zconst"
)

// Takes a map[langKey]conf.LangMap
// usage is i18n.SetLanguagesErrsMap(map[string]zconst.LangMap{
// "es": es.Map, "en": en.Map,
// }, "en", i18n.WithLangKey("langKey"))
// schema.Parse(data, &dest, z.WithCtxValue("langKey", "es"))
func SetLanguagesErrsMap(m map[string]zconst.LangMap, defaultLang string, opts ...setLanguageOption) {
langKey := "lang"

for _, op := range opts {
op(&langKey)
}

conf.ErrorFormatter = func(e internals.ZogError, ctx internals.ParseCtx) {
lang := ctx.Get(langKey)
if lang != nil {
langM, ok := m[lang.(string)]
if ok {
conf.NewDefaultFormatter(langM)(e, ctx)
return
}
}
// use default lang if failed to get correct language map
conf.NewDefaultFormatter(m[defaultLang])(e, ctx)
}
}

// Override the default lang key used to get the language from the ParseContext
func WithLangKey(key string) setLanguageOption {
return func(lk *string) {
*lk = key
}
}

// Please use the helper function this type may very well change in the future but the helper function's API will stay the same
type setLanguageOption = func(langKey *string)

// Proxy the type for easy use
type LangMap = zconst.LangMap
11 changes: 8 additions & 3 deletions zconst/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ const (
ZogTag = "zog"
)

// Map used to format errors in Zog. Both ZogType & ZogErrCode are just strings
type LangMap = map[ZogType]map[ZogErrCode]string

type ZogType = string

const (
Expand All @@ -18,9 +21,11 @@ const (
type ZogErrCode = string

const (
ErrCodeCustom ZogErrCode = "custom" // all
ErrCodeRequired ZogErrCode = "required" // all
ErrCodeCoerce ZogErrCode = "coerce" // all
ErrCodeCustom ZogErrCode = "custom" // all
ErrCodeRequired ZogErrCode = "required" // all
ErrCodeCoerce ZogErrCode = "coerce" // all
// all. Applied when other errror code is not implemented. Required to be implemented for every zog type!
ErrCodeFallback ZogErrCode = "fallback"
ErrCodeEQ ZogErrCode = "eq" // number, time, string
ErrCodeOneOf ZogErrCode = "one_of_options" // string or number

Expand Down

0 comments on commit 1120fd6

Please sign in to comment.