Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Project Focus? #7

Open
jayd3e opened this issue Jan 19, 2017 · 5 comments
Open

Project Focus? #7

jayd3e opened this issue Jan 19, 2017 · 5 comments
Assignees
Labels
Milestone

Comments

@jayd3e
Copy link

jayd3e commented Jan 19, 2017

Hey! So I was checking out the different options in the golang ecosystem for translation, and it looks like it really comes down to go-i18n and this project. I just wanted to better understand the tradeoffs and benefits of the two projects. It appears as though go-i18n may be easier to get started with, but the thing that keeps me coming back to universal-translator is its integration with validator(which I'm a big fan of). The thing that's hardest for me to understand with validator, is how to go from a list of FieldErrors to an actual list of messages that can be displayed in a template. Any thoughts on this would be very helpful.

In the end, my biggest outstanding questions would be, what are the major benefits of going with this project as opposed to others over the long run? Is this project still actively maintained?

@deankarn
Copy link
Contributor

Hey @jayd3e

Glad you're enjoying validator and interested in this project also :)

I'll try to answer your questions and give a little history, sorry if this is long-winded.

Yes this is still actively maintained 😃

I created the universal-translator and locales package which it uses as I could not find any translation library which fully handled plural translations properly; I also wanted to separate the l10n information into it's own package so that the whole community could benefit and implement i18n however they saw fit.

A comparison is a little hard because I think our two projects may have different end goals but right now I think the biggest differences are:

  • go-i18n loads from files and this one directly in code, but it is planned to be able to import and export files
  • this project includes l10n e.g. formatting dates, time, numbers, currency.....

I'll answer the validator integration at the end with some code samples; I'd say the benefits are that my end goal should be a complete and easy to use full translation system; so here are my future goals:

  • Create a frontend/GUI in HTML for managing translations and storing them in BoltDB and allow exporting them or perhaps just helper methods to take those messages and add them into universal-translator. This way I will have a nice and easy way to manage translations and locales. I will most likely do this in another project just so I can keep component separate and everyone can still continue to use the individual components as they see fit.

  • Add middleware for the new project so that it integrates easily into any web application, storing the derived locale into the requests context.

  • Minor update to universal-translator to allow for live updating or translations while the site/code is running (haven't exactly decided the best way to do this yet)

My big issues I've been having are:

  • I've had very little time to work on this recently
  • I need some help with the frontend development, I'm primarily a backend or systems programmer, not a UI/UX and would really appreciate someone's help ( I still need to work out the backend storage before this could start though.

So for now, until I reach my end goals, there's just a little bit of boilerplate needed to get this all set up; but really not that much.

So for validator, I'll give you an example of an html site that implements translations and integrates into validator.

// home.tmpl
{{ define "home" }}
<!DOCTYPE html>
<html>
	<head>
		<title>Home</title>
	</head>
	<body>
		<p>My Locale is "{{ .Trans.Locale }}"</p>
		<p>Error from validator: "{{ .ErrName }}"</p>
	</body>
</html>
{{ end }}

small web app: note: most of this would be split out..

package main

import (
	"context"
	"html/template"
	"log"
	"net/http"
	"time"

	"github.com/go-playground/locales"
	"github.com/go-playground/locales/en"
	"github.com/go-playground/locales/fr"
	"github.com/go-playground/pure"
	"github.com/go-playground/pure/examples/middleware/logging-recovery"
	"github.com/go-playground/universal-translator"
	"github.com/go-playground/validator"
)

var (
	tmpls    *template.Template
	utrans   *ut.UniversalTranslator
	validate *validator.Validate
	transKey = struct {
		name string
	}{
		name: "transKey",
	}
)

func main() {

	validate = validator.New()

	en := en.New()
	utrans = ut.New(en, en, fr.New())
	setup()

	tmpls, _ = template.ParseFiles("home.tmpl")

	r := pure.New()
	r.Use(middleware.LoggingAndRecovery(true), translatorMiddleware)
	r.Get("/", home)

	http.ListenAndServe(":8080", r.Serve())
}

func home(w http.ResponseWriter, r *http.Request) {

	// get locale translator ( could be wrapped into a helper function )
	t := r.Context().Value(transKey).(ut.Translator)

	// validator example
	type User struct {
		Name string `validate:"required"`
	}

	var user User

	s := struct {
		Trans   ut.Translator
		Now     time.Time
		ErrName string
	}{
		Trans: t,
		Now:   time.Now(),
	}

	errs := validate.Struct(user)
	if errs != nil {
		ve := errs.(validator.ValidationErrors)
		translatedErrs := ve.Translate(t)
		s.ErrName = translatedErrs["User.Name"]
	}

	if err := tmpls.ExecuteTemplate(w, "home", s); err != nil {
		log.Fatal(err)
	}
}

func translatorMiddleware(next http.HandlerFunc) http.HandlerFunc {

	return func(w http.ResponseWriter, r *http.Request) {

		params := r.URL.Query()

		locale := params.Get("locale")
		var t ut.Translator

		if len(locale) > 0 {

			var found bool

			if t, found = utrans.GetTranslator(locale); found {
				goto END
			}
		}

		// get and parse the "Accept-Language" http header and return an array
		t, _ = utrans.FindTranslator(pure.AcceptedLanguages(r)...)
	END:
		// I would normally wrap ut.Translator with one with my own functions in order
		// to handle errors and be able to use all functions from translator within the templates.
		r = r.WithContext(context.WithValue(r.Context(), transKey, t))

		next(w, r)
	}
}

func setup() {

	en, _ := utrans.FindTranslator("en")
	en.AddCardinal("days-left", "There is {0} day left", locales.PluralRuleOne, false)
	en.AddCardinal("days-left", "There are {0} days left", locales.PluralRuleOther, false)

	// add english translation for 'required' validation
	validate.RegisterTranslation("required", en, func(ut ut.Translator) (err error) {

		if err = ut.Add("required", "{0} is a required field", false); err != nil {
			return
		}

		return

	},
		func(ut ut.Translator, fe validator.FieldError) string {

			t, err := ut.T(fe.Tag(), fe.Field())
			if err != nil {
				log.Printf("warning: error translating FieldError: %#v", fe)
				return fe.(error).Error()
			}

			return t
		},
	)

	fr, _ := utrans.FindTranslator("fr")
	fr.AddCardinal("days-left", "Il reste {0} jour", locales.PluralRuleOne, false)
	fr.AddCardinal("days-left", "Il reste {0} jours", locales.PluralRuleOther, false)

	// add french translation for 'required' validation
	validate.RegisterTranslation("required", fr, func(ut ut.Translator) (err error) {

		if err = ut.Add("required", "{0} est un champ obligatoire", false); err != nil {
			return
		}

		return
	},
		func(ut ut.Translator, fe validator.FieldError) string {

			t, err := ut.T(fe.Tag(), fe.Field())
			if err != nil {
				log.Printf("warning: error translating FieldError: %#v", fe)
				return fe.(error).Error()
			}

			return t
		},
	)

}

just hit the site with http://localhost:8080/?locale=en and then http://localhost:8080/?locale=fr

It is kind of a pain right to register validator translations, however this is usually a one time thing and the you don't even think about them anymore. I have english translations in the validator package already, looking for help to add more locales and it's allot easier to register them:

en_translations.RegisterDefaultTranslations(validate, en)

see here

I know it seems like allot and wrapping the ut.Translator once more would be a few more lines of code, but this example is an i18 aware application and integrated validator with translations in under 200 lines of code; I'd say that's a bargain 😄

I hope this wasn't too long and answered some questions, please feel free to ask anything else.

@deankarn deankarn added this to the v0.9 milestone Jan 19, 2017
@deankarn deankarn self-assigned this Jan 19, 2017
@jayd3e
Copy link
Author

jayd3e commented Jan 21, 2017

Wow I'm blown away by this response! Thank you so much for your attention to detail and the complete response. You've definitely won a user over. Excited to integrate universal-translator into my app.

So I don't have a ton in response, because I mostly just absorbed all of that and can now go use everything you mentioned. The one piece of feedback I can give you is on your roadmap(the initial three bullets you outlined). As it stands now, universal translator is very focused and I think that's the way it should stay. As I was going through everything, the one major feature that I felt like it was lacking was reading from a file. I think every modern i18n library these days should be able to very easily read from a file to populate the translation rules. This is a huge need, especially when you start farming out translation work to other people. I would say this is the biggest thing that is lacking.

As for the webapp and live reload, I don't think they are needed and I wouldn't spend my time on them. There are already live reload solutions for Go programs that people can use and creating an admin interface provides VERY negligible benefit to just using a file. In fact I would prefer a file over a web interface, b/c a file is easier to share with a translator than a local web interface.

If I were you, I would b-line it to supporting files. Just my two cents.

@deankarn
Copy link
Contributor

Thanks @jayd3e for the feedback, perhaps I will focus on the file import and export 1st.

@deankarn
Copy link
Contributor

deankarn commented Jan 24, 2017

Hey @jayd3e

thanks for all your feedback, so I've done a preliminary file import/export in this branch and would love your input if you have some time :)

if it looks good then I'll add some more format's other than just JSON and allow importing from an io.Reader option as well for embedded or in memory files.

Thanks in advance!

update: here are some samples, they can be combined or organised however the user desires in one or many files and the different types do not have to be separated, everything can be in same array.

Normal Translations/Text Substitutions

[
    {
        "locale": "en",
        "key": "test_trans",
        "trans": "Welcome {0} to the {1}."
    },
    {
        "locale": "en",
        "key": -1,
        "trans": "Welcome {0}"
    }
]

Cardinal Translations

[
    {
        "locale": "en",
        "key": "cardinal_test",
        "trans": "You have {0} day left.",
        "type": "Cardinal",
        "rule": "One"
    },
    {
        "locale": "en",
        "key": "cardinal_test",
        "trans": "You have {0} days left.",
        "type": "Cardinal",
        "rule": "Other"
    }
]

Ordinal Translations

[
    {
        "locale": "en",
        "key": "day",
        "trans": "{0}st",
        "type": "Ordinal",
        "rule": "One"
    },
    {
        "locale": "en",
        "key": "day",
        "trans": "{0}nd",
        "type": "Ordinal",
        "rule": "Two"
    },
    {
        "locale": "en",
        "key": "day",
        "trans": "{0}rd",
        "type": "Ordinal",
        "rule": "Few"
    },
    {
        "locale": "en",
        "key": "day",
        "trans": "{0}th",
        "type": "Ordinal",
        "rule": "Other"
    }
]

Range Translations

[
    {
        "locale": "nl",
        "key": "day",
        "trans": "er {0}-{1} dag vertrokken",
        "type": "Range",
        "rule": "One"
    },
    {
        "locale": "nl",
        "key": "day",
        "trans": "er zijn {0}-{1} dagen over",
        "type": "Range",
        "rule": "Other"
    }
]

@deankarn deankarn modified the milestones: v0.14.0, v0.9 Jan 24, 2017
@deankarn
Copy link
Contributor

deankarn commented Feb 8, 2017

Update:

I have added a file Import & Export logic in release 0.15.0

see README for more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants