diff --git a/go.mod b/go.mod index e38a3449d6..f3882f1414 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/sprig v2.22.0+incompatible github.com/caos/logging v0.0.0-20191210002624-b3260f690a6a - github.com/ghodss/yaml v1.0.0 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b github.com/golang/mock v1.4.3 github.com/golang/protobuf v1.3.5 @@ -21,16 +20,16 @@ require ( github.com/huandu/xstrings v1.3.0 // indirect github.com/imdario/mergo v0.3.8 // indirect github.com/kr/pretty v0.1.0 // indirect - github.com/magiconair/properties v1.8.1 github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/nicksnyder/go-i18n/v2 v2.0.3 github.com/stretchr/testify v1.5.1 golang.org/x/crypto v0.0.0-20200320181102-891825fb96df golang.org/x/net v0.0.0-20200320220750-118fecf932d8 // indirect golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae // indirect - golang.org/x/text v0.3.2 // indirect + golang.org/x/text v0.3.2 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 google.golang.org/genproto v0.0.0-20200319113533-08878b785e9c // indirect google.golang.org/grpc v1.28.0 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect - gopkg.in/yaml.v2 v2.2.8 // indirect + gopkg.in/yaml.v2 v2.2.7 ) diff --git a/go.sum b/go.sum index 8b4fb19b9b..e5db0f7feb 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= @@ -64,12 +65,12 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/nicksnyder/go-i18n/v2 v2.0.3 h1:ks/JkQiOEhhuF6jpNvx+Wih1NIiXzUnZeZVnJuI8R8M= +github.com/nicksnyder/go-i18n/v2 v2.0.3/go.mod h1:oDab7q8XCYMRlcrBnaY/7B1eOectbvj6B1UPBT+p5jo= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -89,6 +90,7 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200320181102-891825fb96df h1:lDWgvUvNnaTnNBc/dwOty86cFeKoKWbwy2wQj0gIxbU= golang.org/x/crypto v0.0.0-20200320181102-891825fb96df/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -100,6 +102,7 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200320220750-118fecf932d8 h1:1+zQlQqEEhUeStBTi653GZAnAuivZq/2hz+Iz+OP7rg= golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -111,6 +114,7 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab h1:FvshnhkKW+LO3HWHodML8kuVX8rnJTxKm9dFPuI68UM= golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae h1:3tcmuaB7wwSZtelmiv479UjUB+vviwABz7a133ZwOKQ= @@ -125,6 +129,7 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= @@ -146,12 +151,11 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= diff --git a/internal/api/grpc/auth_interceptor.go b/internal/api/grpc/auth_interceptor.go index 4e91d11714..6ade793b1e 100644 --- a/internal/api/grpc/auth_interceptor.go +++ b/internal/api/grpc/auth_interceptor.go @@ -7,6 +7,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "github.com/caos/zitadel/internal/api" "github.com/caos/zitadel/internal/api/auth" ) @@ -22,7 +23,7 @@ func AuthorizationInterceptor(verifier auth.TokenVerifier, authConfig *auth.Conf return nil, status.Error(codes.Unauthenticated, "auth header missing") } - orgID := GetHeader(ctx, ZitadelOrgID) + orgID := GetHeader(ctx, api.ZitadelOrgID) ctx, err := auth.CheckUserAuthorization(ctx, req, authToken, orgID, verifier, authConfig, authOpt) if err != nil { diff --git a/internal/api/grpc/header.go b/internal/api/grpc/header.go index 5d217b7518..f1498c1562 100644 --- a/internal/api/grpc/header.go +++ b/internal/api/grpc/header.go @@ -4,12 +4,8 @@ import ( "context" "github.com/grpc-ecosystem/go-grpc-middleware/util/metautils" -) -const ( - Authorization = "authorization" - - ZitadelOrgID = "x-zitadel-orgid" + "github.com/caos/zitadel/internal/api" ) func GetHeader(ctx context.Context, headername string) string { @@ -17,5 +13,5 @@ func GetHeader(ctx context.Context, headername string) string { } func GetAuthorizationHeader(ctx context.Context) string { - return GetHeader(ctx, Authorization) + return GetHeader(ctx, api.Authorization) } diff --git a/internal/api/header.go b/internal/api/header.go new file mode 100644 index 0000000000..fa8efc0282 --- /dev/null +++ b/internal/api/header.go @@ -0,0 +1,8 @@ +package api + +const ( + Authorization = "authorization" + AcceptLanguage = "Accept-Language" + + ZitadelOrgID = "x-zitadel-orgid" +) diff --git a/internal/api/html/i18n.go b/internal/api/html/i18n.go new file mode 100644 index 0000000000..dd5d272668 --- /dev/null +++ b/internal/api/html/i18n.go @@ -0,0 +1,108 @@ +package html + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "path" + + "github.com/BurntSushi/toml" + "github.com/caos/logging" + "github.com/nicksnyder/go-i18n/v2/i18n" + "golang.org/x/text/language" + "gopkg.in/yaml.v2" + + "github.com/caos/zitadel/internal/api" + http_util "github.com/caos/zitadel/internal/api/http" + "github.com/caos/zitadel/internal/errors" +) + +type Translator struct { + bundle *i18n.Bundle + cookieName string + cookieHandler *http_util.CookieHandler +} + +type TranslatorConfig struct { + Path string + DefaultLanguage language.Tag + CookieName string +} + +func NewTranslator(config TranslatorConfig) (*Translator, error) { + t := new(Translator) + var err error + t.bundle, err = newBundle(config.Path, config.DefaultLanguage) + if err != nil { + return nil, err + } + t.cookieHandler = http_util.NewCookieHandler() + t.cookieName = config.CookieName + return t, nil +} + +func newBundle(i18nDir string, defaultLanguage language.Tag) (*i18n.Bundle, error) { + bundle := i18n.NewBundle(defaultLanguage) + bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal) + bundle.RegisterUnmarshalFunc("yml", yaml.Unmarshal) + bundle.RegisterUnmarshalFunc("json", json.Unmarshal) + bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) + files, err := ioutil.ReadDir(i18nDir) + if err != nil { + return nil, errors.ThrowNotFound(err, "HTML-MnXRie", "path not found") + } + for _, file := range files { + bundle.MustLoadMessageFile(path.Join(i18nDir, file.Name())) + } + return bundle, nil +} + +func (t *Translator) LocalizeFromRequest(r *http.Request, id string, args map[string]interface{}) string { + s, err := t.localizerFromRequest(r).Localize(&i18n.LocalizeConfig{ + MessageID: id, + TemplateData: args, + }) + if err != nil { + logging.Log("HTML-MsF5sx").WithError(err).Warnf("missing translation") + return id + } + return s +} + +func (t *Translator) Localize(id string, args map[string]interface{}) string { + s, _ := t.localizer().Localize(&i18n.LocalizeConfig{ + MessageID: id, + TemplateData: args, + }) + return s +} + +func (t *Translator) Lang(r *http.Request) language.Tag { + matcher := language.NewMatcher(t.bundle.LanguageTags()) + tag, _ := language.MatchStrings(matcher, t.langsFromRequest(r)...) + return tag +} + +func (t *Translator) SetLangCookie(w http.ResponseWriter, lang language.Tag) { + t.cookieHandler.SetCookie(w, t.cookieName, lang.String()) +} + +func (t *Translator) localizerFromRequest(r *http.Request) *i18n.Localizer { + return t.localizer(t.langsFromRequest(r)...) +} + +func (t *Translator) localizer(langs ...string) *i18n.Localizer { + return i18n.NewLocalizer(t.bundle, langs...) +} + +func (t *Translator) langsFromRequest(r *http.Request) []string { + langs := make([]string, 0) + if r != nil { + lang, err := t.cookieHandler.GetCookieValue(r, t.cookieName) + if err == nil { + langs = append(langs, lang) + } + langs = append(langs, r.Header.Get(api.AcceptLanguage)) + } + return langs +} diff --git a/internal/api/html/renderer.go b/internal/api/html/renderer.go new file mode 100644 index 0000000000..182087e31d --- /dev/null +++ b/internal/api/html/renderer.go @@ -0,0 +1,82 @@ +package html + +import ( + "net/http" + "path" + "text/template" + + "github.com/caos/logging" + "golang.org/x/text/language" +) + +const ( + TranslateFn = "t" +) + +type Renderer struct { + Templates map[string]*template.Template + i18n *Translator +} + +func NewRenderer(templatesDir string, tmplMapping map[string]string, funcs map[string]interface{}, translatorConfig TranslatorConfig) (*Renderer, error) { + var err error + r := new(Renderer) + r.i18n, err = NewTranslator(translatorConfig) + if err != nil { + return nil, err + } + r.loadTemplates(templatesDir, tmplMapping, funcs) + return r, nil +} + +func (r *Renderer) RenderTemplate(w http.ResponseWriter, req *http.Request, tmpl *template.Template, data interface{}, reqFuncs map[string]interface{}) { + reqFuncs = r.registerTranslateFn(req, reqFuncs) + if err := tmpl.Funcs(reqFuncs).Execute(w, data); err != nil { + logging.Log("HTML-lF8F6w").WithError(err).WithField("template", tmpl.Name).Error("error rendering template") + } +} + +func (r *Renderer) Localize(id string, args map[string]interface{}) string { + return r.i18n.Localize(id, args) +} + +func (r *Renderer) LocalizeFromRequest(req *http.Request, id string, args map[string]interface{}) string { + return r.i18n.LocalizeFromRequest(req, id, args) +} +func (r *Renderer) Lang(req *http.Request) language.Tag { + return r.i18n.Lang(req) +} + +func (r *Renderer) loadTemplates(templatesDir string, tmplMapping map[string]string, funcs map[string]interface{}) { + funcs = r.registerTranslateFn(nil, funcs) + funcs[TranslateFn] = func(id string, args ...interface{}) string { + return id + } + tmpls := template.Must(template.New("").Funcs(funcs).ParseGlob(path.Join(templatesDir, "*.html"))) + r.Templates = make(map[string]*template.Template, len(tmplMapping)) + for name, file := range tmplMapping { + r.Templates[name] = tmpls.Lookup(file) + } +} + +func (r *Renderer) registerTranslateFn(req *http.Request, funcs map[string]interface{}) map[string]interface{} { + if funcs == nil { + funcs = make(map[string]interface{}) + } + funcs[TranslateFn] = func(id string, args ...interface{}) string { + m := map[string]interface{}{} + var key string + for i, arg := range args { + if i%2 == 0 { + key = arg.(string) + continue + } + m[key] = arg + } + if r == nil { + return r.Localize(id, m) + } + return r.LocalizeFromRequest(req, id, m) + } + return funcs +}