danielgtaylor / huma

Huma REST/HTTP API Framework for Golang with OpenAPI 3.1
https://huma.rocks/
MIT License
1.87k stars 138 forks source link

Validation errors localization #414

Open lordspinach opened 4 months ago

lordspinach commented 4 months ago

Hello!

I'm facing a problem, I want to localize my application based on HUMA and for that I have to implement Registry, Schema and PathBuffer with almost identical logic in the validation part. Another solution is to disable HUMA validation altogether and implement something else. So my question is. Do you plan to implement internationalization in HUMA? Maybe you already have some thoughts on this and would be willing to share them so I can help you implement this feature. Or, conversely, why you don't want to do it. I think this feature would be awesome and am open to discussing it.

Best regards

danielgtaylor commented 4 months ago

@lordspinach I have zero experience doing i18n in Go. I've done it in Python programs in the past using gettext, but am not entirely sure where to start, so I'm open to discussion on this one. I think it would be nice:

Thanks!

lordspinach commented 4 months ago

@danielgtaylor As an example solution i can suggest something like this from my current app:

//go:embed locales/*/data.yaml
var f embed.FS

// langMap is a map that contains supported locales in [en][validation][unexpected property] format
var langMap = make(map[string]map[string]map[string]string)

// LoadLocalesMap loads the language locales into the map variable by parsing the YAML data files.
func LoadLocalesMap() error {
    err := parseLocalesToMap()
    if err != nil {
        return errors.Wrap(err, "failed to parse locales")
    }
    return nil
}

func T(ctx context.Context, path string, args ...any) string {
    localeMap := ctx.Value("locale").(map[string]map[string]string)
    p := strings.Split(path, ":")
    category, key := p[0], p[1]
    translation, ok := localeMap[category][key]
    if !ok {
        return ""
    }
    return fmt.Sprintf(translation, args...)
}

func GetSupportedLanguages() []string {
    supportedLanguages := make([]string, 0, len(langMap))
    for lang := range langMap {
        supportedLanguages = append(supportedLanguages, lang)
    }
    return supportedLanguages
}

func GetLocaleMap(lang string) map[string]map[string]string {
    return langMap[lang]
}

func parseLocalesToMap() error {
    rootDir, err := f.ReadDir("locales")
    if err != nil {
        return gerro.Internal.Wrap(err, "can't read locales")
    }
    for _, locale := range rootDir {
        file, err := f.ReadFile(fmt.Sprintf("locales/%s/data.yaml", locale.Name()))
        if err != nil {
            return errors.Wrap(err, fmt.Sprintf("can't read locales/%s/data.yaml", locale.Name()))
        }
        localeData := make(map[string]map[string]string)
        err = yaml.Unmarshal(file, &localeData)
        if err != nil {
            return errors.Wrap(err, fmt.Sprintf("can't parse locales/%s/data.yaml", locale.Name()))
        }
        langMap[locale.Name()] = localeData
    }
    return nil
}

Here is an example of usage:

return huma.Error400BadRequest(i18n.T(ctx, "validation:Several validation errors occurred"), details...)

This implementation pretend to use ctx for load any of locale. As we load all locales into map at app start we keeps allocations on request at the same level. It allows us to get locale based on request's Accept-Language and send corresponding Content-Language. Also i think we should choose something else then embed fs for locales reading as it not support dynamic path change and it will make impossible to reassign locales in peoples apps. I'm not in full dive into huma and not sure is it ok to send context to such a lot of a places and hope to get your thoughts on my suggestion