Browse Source

Add full web examples

- Added and example with and one without files, how to integrate into a web application and
use in templates.
joeybloggs 8 năm trước cách đây
mục cha
commit
c8d146629c

+ 35 - 0
examples/full-no-files/home.tmpl

@@ -0,0 +1,35 @@
+{{ define "home" }}
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>Home</title>
+	</head>
+	<body>
+		<p>Locale: {{ .Trans.Locale }}</p>
+        <p>Trans1: {{ .Trans.C "days-left" 1 0 "1" }}</p>
+        <p>Trans2: {{ .Trans.C "days-left" 2 0 "2" }}</p>
+        <p>FmtNumber Positive: {{ .Trans.FmtNumber .PositiveNum 2 }}</p>
+        <p>FmtNumber Negative: {{ .Trans.FmtNumber .NegativeNum 2 }}</p>
+        <p>FmtPercent Negative: {{ .Trans.FmtPercent .Percent 2 }}</p>
+        <p>FmtCurrency Negative: {{ .Trans.FmtCurrency .PositiveNum 2 .Trans.Currency }}</p>
+        <p>FmtCurrency Negative: {{ .Trans.FmtCurrency .NegativeNum 2 .Trans.Currency }}</p>
+        <p>FmtAccounting Negative: {{ .Trans.FmtAccounting .PositiveNum 2 .Trans.Currency }}</p>
+        <p>FmtAccounting Negative: {{ .Trans.FmtAccounting .NegativeNum 2 .Trans.Currency }}</p>
+        <p>FmtDateShort: {{ .Trans.FmtDateShort .Now }}</p>
+        <p>FmtDateMedium: {{ .Trans.FmtDateMedium .Now }}</p>
+        <p>FmtDateLong: {{ .Trans.FmtDateLong .Now }}</p>
+        <p>FmtDateFull: {{ .Trans.FmtDateFull .Now }}</p>
+        <p>FmtTimeShort: {{ .Trans.FmtTimeShort .Now }}</p>
+        <p>FmtTimeMedium: {{ .Trans.FmtTimeMedium .Now }}</p>
+        <p>FmtTimeLong: {{ .Trans.FmtTimeLong .Now }}</p>
+        <p>FmtTimeFull: {{ .Trans.FmtTimeFull .Now }}</p>
+        <p>MonthsAbbreviated: {{ .Trans.MonthsAbbreviated }}</p>
+        <p>MonthsNarrow: {{ .Trans.MonthsNarrow }}</p>
+        <p>MonthsWide: {{ .Trans.MonthsWide }}</p>
+        <p>WeekdaysAbbreviated: {{ .Trans.WeekdaysAbbreviated }}</p>
+        <p>WeekdaysNarrow: {{ .Trans.WeekdaysNarrow }}</p>
+        <p>WeekdaysShort: {{ .Trans.WeekdaysShort }}</p>
+        <p>WeekdaysWide: {{ .Trans.WeekdaysWide }}</p>
+	</body>
+</html>
+{{ end }}

+ 210 - 0
examples/full-no-files/main.go

@@ -0,0 +1,210 @@
+package main
+
+import (
+	"context"
+	"html/template"
+	"log"
+	"net/http"
+	"time"
+
+	"github.com/go-playground/locales"
+	"github.com/go-playground/locales/currency"
+	"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"
+)
+
+var (
+	tmpls    *template.Template
+	utrans   *ut.UniversalTranslator
+	transKey = struct {
+		name string
+	}{
+		name: "transKey",
+	}
+)
+
+// Translator wraps ut.Translator in order to handle errors transparently
+// it is totally optional but recommended as it can now be used directly in
+// templates and nobody can add translations where they're not supposed to.
+type Translator interface {
+	locales.Translator
+
+	// creates the translation for the locale given the 'key' and params passed in.
+	// wraps ut.Translator.T to handle errors
+	T(key interface{}, params ...string) string
+
+	// creates the cardinal translation for the locale given the 'key', 'num' and 'digit' arguments
+	//  and param passed in.
+	// wraps ut.Translator.C to handle errors
+	C(key interface{}, num float64, digits uint64, param string) string
+
+	// creates the ordinal translation for the locale given the 'key', 'num' and 'digit' arguments
+	// and param passed in.
+	// wraps ut.Translator.O to handle errors
+	O(key interface{}, num float64, digits uint64, param string) string
+
+	//  creates the range translation for the locale given the 'key', 'num1', 'digit1', 'num2' and
+	//  'digit2' arguments and 'param1' and 'param2' passed in
+	// wraps ut.Translator.R to handle errors
+	R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) string
+
+	// Currency returns the type used by the given locale.
+	Currency() currency.Type
+}
+
+// implements Translator interface definition above.
+type translator struct {
+	locales.Translator
+	trans ut.Translator
+}
+
+var _ Translator = (*translator)(nil)
+
+func (t *translator) T(key interface{}, params ...string) string {
+
+	s, err := t.trans.T(key, params...)
+	if err != nil {
+		log.Printf("issue translating key: '%v' error: '%s'", key, err)
+	}
+
+	return s
+}
+
+func (t *translator) C(key interface{}, num float64, digits uint64, param string) string {
+
+	s, err := t.trans.C(key, num, digits, param)
+	if err != nil {
+		log.Printf("issue translating cardinal key: '%v' error: '%s'", key, err)
+	}
+
+	return s
+}
+
+func (t *translator) O(key interface{}, num float64, digits uint64, param string) string {
+
+	s, err := t.trans.C(key, num, digits, param)
+	if err != nil {
+		log.Printf("issue translating ordinal key: '%v' error: '%s'", key, err)
+	}
+
+	return s
+}
+
+func (t *translator) R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) string {
+
+	s, err := t.trans.R(key, num1, digits1, num2, digits2, param1, param2)
+	if err != nil {
+		log.Printf("issue translating range key: '%v' error: '%s'", key, err)
+	}
+
+	return s
+}
+
+func (t *translator) Currency() currency.Type {
+
+	// choose your own locale. The reason it isn't mapped for you is because many
+	// countries have multiple currencies; it's up to you and you're application how
+	// and which currencies to use. I recommend adding a function it to to your custon translator
+	// interface like defined above.
+	switch t.Locale() {
+	case "en":
+		return currency.USD
+	case "fr":
+		return currency.EUR
+	default:
+		return currency.USD
+	}
+}
+
+func main() {
+
+	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)
+
+	log.Println("Running on Port :8080")
+	log.Println("Try me with URL http://localhost:8080/?locale=en")
+	log.Println("and then http://localhost:8080/?locale=fr")
+	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).(Translator)
+
+	s := struct {
+		Trans       Translator
+		Now         time.Time
+		PositiveNum float64
+		NegativeNum float64
+		Percent     float64
+	}{
+		Trans:       t,
+		Now:         time.Now(),
+		PositiveNum: 1234576.45,
+		NegativeNum: -35900394.34,
+		Percent:     96.76,
+	}
+
+	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) {
+
+		// there are many ways to check, this is just checking for query param &
+		// Accept-Language header but can be expanded to Cookie's etc....
+
+		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, &translator{trans: t, Translator: t.(locales.Translator)}))
+
+		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)
+
+	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)
+
+	err = utrans.VerifyTranslations()
+	if err != nil {
+		log.Fatal(err)
+	}
+}

+ 35 - 0
examples/full-with-files/home.tmpl

@@ -0,0 +1,35 @@
+{{ define "home" }}
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>Home</title>
+	</head>
+	<body>
+		<p>Locale: {{ .Trans.Locale }}</p>
+        <p>Trans1: {{ .Trans.C "days-left" 1 0 "1" }}</p>
+        <p>Trans2: {{ .Trans.C "days-left" 2 0 "2" }}</p>
+        <p>FmtNumber Positive: {{ .Trans.FmtNumber .PositiveNum 2 }}</p>
+        <p>FmtNumber Negative: {{ .Trans.FmtNumber .NegativeNum 2 }}</p>
+        <p>FmtPercent Negative: {{ .Trans.FmtPercent .Percent 2 }}</p>
+        <p>FmtCurrency Negative: {{ .Trans.FmtCurrency .PositiveNum 2 .Trans.Currency }}</p>
+        <p>FmtCurrency Negative: {{ .Trans.FmtCurrency .NegativeNum 2 .Trans.Currency }}</p>
+        <p>FmtAccounting Negative: {{ .Trans.FmtAccounting .PositiveNum 2 .Trans.Currency }}</p>
+        <p>FmtAccounting Negative: {{ .Trans.FmtAccounting .NegativeNum 2 .Trans.Currency }}</p>
+        <p>FmtDateShort: {{ .Trans.FmtDateShort .Now }}</p>
+        <p>FmtDateMedium: {{ .Trans.FmtDateMedium .Now }}</p>
+        <p>FmtDateLong: {{ .Trans.FmtDateLong .Now }}</p>
+        <p>FmtDateFull: {{ .Trans.FmtDateFull .Now }}</p>
+        <p>FmtTimeShort: {{ .Trans.FmtTimeShort .Now }}</p>
+        <p>FmtTimeMedium: {{ .Trans.FmtTimeMedium .Now }}</p>
+        <p>FmtTimeLong: {{ .Trans.FmtTimeLong .Now }}</p>
+        <p>FmtTimeFull: {{ .Trans.FmtTimeFull .Now }}</p>
+        <p>MonthsAbbreviated: {{ .Trans.MonthsAbbreviated }}</p>
+        <p>MonthsNarrow: {{ .Trans.MonthsNarrow }}</p>
+        <p>MonthsWide: {{ .Trans.MonthsWide }}</p>
+        <p>WeekdaysAbbreviated: {{ .Trans.WeekdaysAbbreviated }}</p>
+        <p>WeekdaysNarrow: {{ .Trans.WeekdaysNarrow }}</p>
+        <p>WeekdaysShort: {{ .Trans.WeekdaysShort }}</p>
+        <p>WeekdaysWide: {{ .Trans.WeekdaysWide }}</p>
+	</body>
+</html>
+{{ end }}

+ 207 - 0
examples/full-with-files/main.go

@@ -0,0 +1,207 @@
+package main
+
+import (
+	"context"
+	"html/template"
+	"log"
+	"net/http"
+	"time"
+
+	"github.com/go-playground/locales"
+	"github.com/go-playground/locales/currency"
+	"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"
+)
+
+var (
+	tmpls    *template.Template
+	utrans   *ut.UniversalTranslator
+	transKey = struct {
+		name string
+	}{
+		name: "transKey",
+	}
+)
+
+// Translator wraps ut.Translator in order to handle errors transparently
+// it is totally optional but recommended as it can now be used directly in
+// templates and nobody can add translations where they're not supposed to.
+type Translator interface {
+	locales.Translator
+
+	// creates the translation for the locale given the 'key' and params passed in.
+	// wraps ut.Translator.T to handle errors
+	T(key interface{}, params ...string) string
+
+	// creates the cardinal translation for the locale given the 'key', 'num' and 'digit' arguments
+	//  and param passed in.
+	// wraps ut.Translator.C to handle errors
+	C(key interface{}, num float64, digits uint64, param string) string
+
+	// creates the ordinal translation for the locale given the 'key', 'num' and 'digit' arguments
+	// and param passed in.
+	// wraps ut.Translator.O to handle errors
+	O(key interface{}, num float64, digits uint64, param string) string
+
+	//  creates the range translation for the locale given the 'key', 'num1', 'digit1', 'num2' and
+	//  'digit2' arguments and 'param1' and 'param2' passed in
+	// wraps ut.Translator.R to handle errors
+	R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) string
+
+	// Currency returns the type used by the given locale.
+	Currency() currency.Type
+}
+
+// implements Translator interface definition above.
+type translator struct {
+	locales.Translator
+	trans ut.Translator
+}
+
+var _ Translator = (*translator)(nil)
+
+func (t *translator) T(key interface{}, params ...string) string {
+
+	s, err := t.trans.T(key, params...)
+	if err != nil {
+		log.Printf("issue translating key: '%v' error: '%s'", key, err)
+	}
+
+	return s
+}
+
+func (t *translator) C(key interface{}, num float64, digits uint64, param string) string {
+
+	s, err := t.trans.C(key, num, digits, param)
+	if err != nil {
+		log.Printf("issue translating cardinal key: '%v' error: '%s'", key, err)
+	}
+
+	return s
+}
+
+func (t *translator) O(key interface{}, num float64, digits uint64, param string) string {
+
+	s, err := t.trans.C(key, num, digits, param)
+	if err != nil {
+		log.Printf("issue translating ordinal key: '%v' error: '%s'", key, err)
+	}
+
+	return s
+}
+
+func (t *translator) R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) string {
+
+	s, err := t.trans.R(key, num1, digits1, num2, digits2, param1, param2)
+	if err != nil {
+		log.Printf("issue translating range key: '%v' error: '%s'", key, err)
+	}
+
+	return s
+}
+
+func (t *translator) Currency() currency.Type {
+
+	// choose your own locale. The reason it isn't mapped for you is because many
+	// countries have multiple currencies; it's up to you and you're application how
+	// and which currencies to use. I recommend adding a function it to to your custon translator
+	// interface like defined above.
+	switch t.Locale() {
+	case "en":
+		return currency.USD
+	case "fr":
+		return currency.EUR
+	default:
+		return currency.USD
+	}
+}
+
+func main() {
+
+	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)
+
+	log.Println("Running on Port :8080")
+	log.Println("Try me with URL http://localhost:8080/?locale=en")
+	log.Println("and then http://localhost:8080/?locale=fr")
+	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).(Translator)
+
+	s := struct {
+		Trans       Translator
+		Now         time.Time
+		PositiveNum float64
+		NegativeNum float64
+		Percent     float64
+	}{
+		Trans:       t,
+		Now:         time.Now(),
+		PositiveNum: 1234576.45,
+		NegativeNum: -35900394.34,
+		Percent:     96.76,
+	}
+
+	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) {
+
+		// there are many ways to check, this is just checking for query param &
+		// Accept-Language header but can be expanded to Cookie's etc....
+
+		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, &translator{trans: t, Translator: t.(locales.Translator)}))
+
+		next(w, r)
+	}
+}
+
+func setup() {
+
+	err := utrans.Import(ut.JSON, "translations")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	err = utrans.VerifyTranslations()
+	if err != nil {
+		log.Fatal(err)
+	}
+}

+ 16 - 0
examples/full-with-files/translations/en/home.json

@@ -0,0 +1,16 @@
+[
+    {
+        "locale": "en",
+        "key": "days-left",
+        "trans": "There is {0} day left",
+        "type": "Cardinal",
+        "rule": "One"
+    },
+    {
+        "locale": "en",
+        "key": "days-left",
+        "trans": "There are {0} days left",
+        "type": "Cardinal",
+        "rule": "Other"
+    }
+]

+ 16 - 0
examples/full-with-files/translations/fr/home.json

@@ -0,0 +1,16 @@
+[
+    {
+        "locale": "fr",
+        "key": "days-left",
+        "trans": "Il reste {0} jour",
+        "type": "Cardinal",
+        "rule": "One"
+    },
+    {
+        "locale": "fr",
+        "key": "days-left",
+        "trans": "Il reste {0} jours",
+        "type": "Cardinal",
+        "rule": "Other"
+    }
+]

+ 72 - 1
translator_test.go

@@ -288,7 +288,7 @@ func TestOrdinalTranslation(t *testing.T) {
 		},
 		{
 			key:      "day",
-			trans:    "{0}sfefewt",
+			trans:    "{0}st",
 			rule:     locales.PluralRuleOne,
 			expected: nil,
 		},
@@ -785,3 +785,74 @@ func TestGetFallback(t *testing.T) {
 		t.Errorf("Expected '%s' Got '%s'", expected, trans.Locale())
 	}
 }
+
+func TestVerifyUTTranslations(t *testing.T) {
+
+	e := en.New()
+	uni := New(e, e)
+	en, found := uni.GetTranslator("en")
+	if !found {
+		t.Fatalf("Expected '%t' Got '%t'", true, found)
+	}
+
+	translations := []struct {
+		key           interface{}
+		trans         string
+		rule          locales.PluralRule
+		expected      error
+		expectedError bool
+		override      bool
+	}{
+		{
+			key:      "day",
+			trans:    "{0}st",
+			rule:     locales.PluralRuleOne,
+			expected: nil,
+		},
+		{
+			key:      "day",
+			trans:    "{0}nd",
+			rule:     locales.PluralRuleTwo,
+			expected: nil,
+		},
+		{
+			key:      "day",
+			trans:    "{0}rd",
+			rule:     locales.PluralRuleFew,
+			expected: nil,
+		},
+		// intentionally leaving out plural other
+		// {
+		// 	key:      "day",
+		// 	trans:    "{0}th",
+		// 	rule:     locales.PluralRuleOther,
+		// 	expected: nil,
+		// },
+	}
+
+	for _, tt := range translations {
+
+		err := en.AddOrdinal(tt.key, tt.trans, tt.rule, tt.override)
+		if err != tt.expected {
+			if !tt.expectedError || err.Error() != tt.expected.Error() {
+				t.Errorf("Expected '<nil>' Got '%s'", err)
+			}
+		}
+	}
+
+	expected := "error: missing 'ordinal' plural rule 'Other' for translation with key 'day' and locale 'en'"
+	err := uni.VerifyTranslations()
+	if err == nil || err.Error() != expected {
+		t.Fatalf("Expected '%s' Got '%s'", expected, err)
+	}
+
+	err = en.AddOrdinal("day", "{0}th", locales.PluralRuleOther, false)
+	if err != nil {
+		t.Fatalf("Expected '%v' Got '%s'", nil, err)
+	}
+
+	err = uni.VerifyTranslations()
+	if err != nil {
+		t.Fatalf("Expected '%v' Got '%s'", nil, err)
+	}
+}

+ 14 - 0
universal_translator.go

@@ -97,3 +97,17 @@ func (t *UniversalTranslator) AddTranslator(translator locales.Translator, overr
 
 	return nil
 }
+
+// VerifyTranslations runs through all locales and identifies any issues
+// eg. missing plural rules for a locale
+func (t *UniversalTranslator) VerifyTranslations() (err error) {
+
+	for _, trans := range t.translators {
+		err = trans.VerifyTranslations()
+		if err != nil {
+			return
+		}
+	}
+
+	return
+}