From 24d63f4c9832538a8b4d119f54df2507ffe46578 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Wed, 31 Oct 2018 23:10:04 +0000 Subject: [PATCH] add translations --- cmd/site/html.go | 46 +++++++++++-------- cmd/site/i18n.go | 46 +++++++++++++++++++ cmd/site/locale.go | 97 ++++++++++++++++++++++++++++++++++++++++ cmd/site/main.go | 41 +++++++++++------ go.mod | 7 ++- go.sum | 10 ++--- locales/en.json | 43 ++++++++++++++++++ locales/tp.json | 43 ++++++++++++++++++ templates/base.html | 4 +- templates/blogindex.html | 8 ++-- templates/blogpost.html | 10 ++--- templates/contact.html | 16 +++---- templates/error.html | 2 +- templates/index.html | 19 +++++--- templates/resume.html | 4 +- 15 files changed, 327 insertions(+), 69 deletions(-) create mode 100644 cmd/site/i18n.go create mode 100644 cmd/site/locale.go create mode 100644 locales/en.json create mode 100644 locales/tp.json diff --git a/cmd/site/html.go b/cmd/site/html.go index f42b947..1037baf 100644 --- a/cmd/site/html.go +++ b/cmd/site/html.go @@ -19,29 +19,39 @@ func logTemplateTime(name string, from time.Time) { func (s *Site) renderTemplatePage(templateFname string, data interface{}) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer logTemplateTime(templateFname, time.Now()) - s.tlock.RLock() - defer s.tlock.RUnlock() - var t *template.Template - var err error - - if s.templates[templateFname] == nil { - t, err = template.ParseFiles("templates/base.html", "templates/"+templateFname) + getTranslation := func(group, key string, vals ...interface{}) string { + var lc locale + var ok bool + locale, err := GetPreferredLocale(r) if err != nil { - w.WriteHeader(http.StatusInternalServerError) - ln.Error(context.Background(), err, ln.F{"action": "renderTemplatePage", "page": templateFname}) - fmt.Fprintf(w, "error: %v", err) + ln.Error(r.Context(), err) + lc = s.t.locales["en"] + goto overwith } - ln.Log(context.Background(), ln.F{"action": "loaded_new_template", "fname": templateFname}) + lc, ok = s.t.Get(locale.Lang) + if !ok { + ln.Log(r.Context(), ln.F{"msg": "unknown language requested", "lang": locale.Lang, "header": r.Header.Get("Accept-Language")}) + lc = s.t.locales["en"] + goto overwith + } - s.tlock.RUnlock() - s.tlock.Lock() - s.templates[templateFname] = t - s.tlock.Unlock() - s.tlock.RLock() - } else { - t = s.templates[templateFname] + overwith: + return lc.Value(group, key, vals...) + } + funcMap := template.FuncMap{ + "trans": getTranslation, + } + + var t *template.Template = template.New(templateFname).Funcs(funcMap) + var err error + + t, err = t.ParseFiles("templates/base.html", "templates/"+templateFname) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + ln.Error(context.Background(), err, ln.F{"action": "renderTemplatePage", "page": templateFname}) + fmt.Fprintf(w, "error: %v", err) } err = t.Execute(w, data) diff --git a/cmd/site/i18n.go b/cmd/site/i18n.go new file mode 100644 index 0000000..6c8fc92 --- /dev/null +++ b/cmd/site/i18n.go @@ -0,0 +1,46 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" +) + +type locale map[string]map[string]string + +func (l locale) Value(group, key string, args ...interface{}) string { + sg, ok := l[group] + if !ok { + return "no group " + group + } + + result, ok := sg[key] + if !ok { + return fmt.Sprintf("in group %s, no key %s", group, key) + } + + return fmt.Sprintf(result, args...) +} + +type translations struct { + locales map[string]locale +} + +func (t *translations) LoadLocale(name string, r io.Reader) error { + l := locale{} + d := json.NewDecoder(r) + err := d.Decode(&l) + if err == nil { + t.locales[name] = l + } + return err +} + +func (t *translations) Get(name string) (locale, bool) { + l, ok := t.locales[name] + if !ok { + return nil, false + } + + return l, ok +} diff --git a/cmd/site/locale.go b/cmd/site/locale.go new file mode 100644 index 0000000..a65034a --- /dev/null +++ b/cmd/site/locale.go @@ -0,0 +1,97 @@ +package main + +import ( + "errors" + "net/http" + "strconv" + "strings" +) + +// Locale is locale value from the Accept-Language header in request +type Locale struct { + Lang, Country string + Qual float64 +} + +// Name returns the locale value in 'lang' or 'lang_country' format +// eg: de_DE, en_US, gb +func (l *Locale) Name() string { + if len(l.Country) > 0 { + return l.Lang + "_" + l.Country + } + return l.Lang +} + +// ParseLocale creates a Locale from a locale string +func ParseLocale(locale string) Locale { + locsplt := strings.Split(locale, "_") + resp := Locale{} + resp.Lang = locsplt[0] + if len(locsplt) > 1 { + resp.Country = locsplt[1] + } + return resp +} + +const ( + acceptLanguage = "Accept-Language" +) + +func supportedLocales(alstr string) []Locale { + locales := make([]Locale, 0) + alstr = strings.Replace(alstr, " ", "", -1) + if alstr == "" { + return locales + } + al := strings.Split(alstr, ",") + for _, lstr := range al { + locales = append(locales, Locale{ + Lang: parseLang(lstr), + Country: parseCountry(lstr), + Qual: parseQual(lstr), + }) + } + return locales +} + +// GetLocales returns supported locales for the given requet +func GetLocales(r *http.Request) []Locale { + return supportedLocales(r.Header.Get(acceptLanguage)) +} + +// GetPreferredLocale return preferred locale for the given reuqest +// returns error if there is no preferred locale +func GetPreferredLocale(r *http.Request) (*Locale, error) { + locales := GetLocales(r) + if len(locales) == 0 { + return &Locale{}, errors.New("No locale found") + } + return &locales[0], nil +} + +func parseLang(val string) string { + locale := strings.Split(val, ";")[0] + lang := strings.Split(locale, "-")[0] + return lang +} + +func parseCountry(val string) string { + locale := strings.Split(val, ";")[0] + spl := strings.Split(locale, "-") + if len(spl) > 1 { + return spl[1] + } + return "" +} + +func parseQual(val string) float64 { + spl := strings.Split(val, ";") + if len(spl) > 1 { + qual, err := strconv.ParseFloat(strings.Split(spl[1], "=")[1], 64) + if err != nil { + return 1 + } + return qual + } + return 1 +} diff --git a/cmd/site/main.go b/cmd/site/main.go index e9b4d21..83cbeb2 100644 --- a/cmd/site/main.go +++ b/cmd/site/main.go @@ -9,7 +9,6 @@ import ( "path/filepath" "sort" "strings" - "sync" "time" "github.com/Xe/jsonfeed" @@ -46,10 +45,8 @@ type Site struct { mux *http.ServeMux - templates map[string]*template.Template - tlock sync.RWMutex - segment analytics.Client + t *translations } func (s *Site) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -78,31 +75,49 @@ func Build() (*Site, error) { Date string } + t := &translations{ + locales: map[string]locale{}, + } + + for _, lang := range []string{"en", "tp"} { + fin, err := os.Open(filepath.Join("locales", lang+".json")) + if err != nil { + return nil, err + } + defer fin.Close() + + err = t.LoadLocale(lang, fin) + if err != nil { + return nil, err + } + } + + l := t.locales["en"] + s := &Site{ rssFeed: &feeds.Feed{ - Title: "Christine Dodrill's Blog", + Title: l.Value("blog", "title"), Link: &feeds.Link{Href: "https://christine.website/blog"}, - Description: "My blog posts and rants about various technology things.", + Description: l.Value("blog", "description"), Author: &feeds.Author{Name: "Christine Dodrill", Email: "me@christine.website"}, Created: bootTime, - Copyright: "This work is copyright Christine Dodrill. My viewpoints are my own and not the view of any employer past, current or future.", + Copyright: l.Value("meta", "rss_copyright"), }, jsonFeed: &jsonfeed.Feed{ Version: jsonfeed.CurrentVersion, - Title: "Christine Dodrill's Blog", + Title: l.Value("blog", "title"), HomePageURL: "https://christine.website", FeedURL: "https://christine.website/blog.json", - Description: "My blog posts and rants about various technology things.", - UserComment: "This is a JSON feed of my blogposts. For more information read: https://jsonfeed.org/version/1", + Description: l.Value("blog", "description"), + UserComment: l.Value("meta", "json_feed"), Icon: icon, Favicon: icon, Author: jsonfeed.Author{ - Name: "Christine Dodrill", + Name: l.Value("header", "name"), Avatar: icon, }, }, - mux: http.NewServeMux(), - templates: map[string]*template.Template{}, + mux: http.NewServeMux(), } if wk := os.Getenv("SEGMENT_WRITE_KEY"); wk != "" { diff --git a/go.mod b/go.mod index ce5e6d5..39b8f84 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,17 @@ module github.com/Xe/site require ( - github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da // indirect github.com/Xe/gopreload v0.0.0-20170326043426-a00a8beb369c github.com/Xe/jsonfeed v0.0.0-20170520170432-e21591505612 github.com/Xe/ln v0.0.0-20170921000907-466e05b2ef3e - github.com/daaku/go.zipexe v0.0.0-20150329023125-a5fe2436ffcb // indirect + github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/gops v0.3.2 github.com/gorilla/feeds v1.0.0 - github.com/jtolds/qod v0.0.0-20170925014538-3abb44dfc7ba // indirect github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect github.com/kr/pretty v0.1.0 // indirect - github.com/magefile/mage v0.0.0-20171025021237-ab3ca2f6f855 // indirect github.com/pkg/errors v0.8.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday v0.0.0-20170806171014-cadec560ec52 github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c // indirect github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect diff --git a/go.sum b/go.sum index dc390b8..15b9853 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,17 @@ -github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da/go.mod h1:DgrzXonpdQbfN3uYaGz1EG4Sbhyum/MMIn6Cphlh2bw= github.com/Xe/gopreload v0.0.0-20170326043426-a00a8beb369c h1:lqTJqaoonxgJMvvfl1ukr/3qCEGWC0nQxzPezbJrhHs= github.com/Xe/gopreload v0.0.0-20170326043426-a00a8beb369c/go.mod h1:0aSWHJguPNHo6zlU7A4Ktua1A/VUr5Jdr1QZ2amOkAQ= github.com/Xe/jsonfeed v0.0.0-20170520170432-e21591505612 h1:5cPld6YTMozzm3lK9VCnOErgoFbADM2hZc4KDu0YNKs= github.com/Xe/jsonfeed v0.0.0-20170520170432-e21591505612/go.mod h1:UKXAbYA/G9yE+cPbpfiP7FlT6Kr3N2mbEjQEJ9hT004= github.com/Xe/ln v0.0.0-20170921000907-466e05b2ef3e h1:uMC/C/zBov+PItx2c6Vax4lt37X+2V5X7NWRcXsxuzE= github.com/Xe/ln v0.0.0-20170921000907-466e05b2ef3e/go.mod h1:hDOCtu3XM9SVMB0UHBilUvRbZ0JKMI7IDbvBzwclIb0= -github.com/daaku/go.zipexe v0.0.0-20150329023125-a5fe2436ffcb/go.mod h1:U0vRfAucUOohvdCxt5MWLF+TePIL0xbCkbKIiV8TQCE= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/gops v0.3.2 h1:n9jMkrye8dh3WQ0IxG5dzLRIhQeZDZoGaj0D7T7x7hQ= github.com/google/gops v0.3.2/go.mod h1:pMQgrscwEK/aUSW1IFSaBPbJX82FPHWaSoJw1axQfD0= github.com/gorilla/feeds v1.0.0 h1:EbkEvaYf+PXhYNHS20heBG7Rl2X6Zy8l11ZBWAHkWqE= github.com/gorilla/feeds v1.0.0/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA= -github.com/jtolds/qod v0.0.0-20170925014538-3abb44dfc7ba/go.mod h1:CC0XsLGDIrizqHTsUEJh89QHzv2u2mix/O9xl4+LqSw= github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro= github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -19,10 +19,10 @@ 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/magefile/mage v0.0.0-20171025021237-ab3ca2f6f855/go.mod h1:IUDi13rsHje59lecXokTfGX0QIzO45uVPlXnJYsXepA= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday v0.0.0-20170806171014-cadec560ec52 h1:xZ0R4UuR0EDx7m0OmvsqZaomfqCeYJ/PyLm94WLhnIM= github.com/russross/blackfriday v0.0.0-20170806171014-cadec560ec52/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c h1:rsRTAcCR5CeNLkvgBVSjQoDGRRt6kggsE6XYBqCv2KQ= diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..4e64a91 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,43 @@ +{ + "header": { + "name": "Christine Dodrill", + "blog": "Blog", + "contact": "Contact", + "resume": "Resume" + }, + "meta": { + "copyright": "Copyright %s Christine Dodrill. Any and all opinions listed here are my own, not my employer's.", + "rss_copyright": "This work is copyright Christine Dodrill. My viewpoints are my own and not the view of any employer past, current or future.", + "json_feed": "This is a JSON feed of my blogposts. For more information, read: https://jsonfeed.org/version/1" + }, + "index": { + "contact_me": "Contact Me", + "title": "Web and Backend Services Devops Specialist", + "skills": "Skills", + "highlighted_projects": "Highlighted Projects" + }, + "blog": { + "index_title": "Blogposts", + "blogroll": "Other Blogs I Find Interesting", + "description": "My blog posts and rants about various technology things.", + "title": "Christine Dodrill's Blog", + "posted_on": "Posted on %s", + "read_this": "Read Post", + "disclaimer": "Content posted on %s, opinions and preferences of the author may have changed since then." + }, + "contact": { + "header": "Contact Information", + "email": "Email", + "social_media": "Social Media", + "other_info": "Other Information", + "donations": "To send me donations, my bitcoin address is: ", + "irc_comment": "I am on many IRC networks. On Freenode I am using the nick Xe but elsewhere I will use the nick Xena or Cadey." + }, + "error": { + "title": "Error", + "post_not_found": "no such post found: %s" + }, + "resume": { + "plaintext": "Plain-text version of this resume here" + } +} diff --git a/locales/tp.json b/locales/tp.json new file mode 100644 index 0000000..e1ef9b9 --- /dev/null +++ b/locales/tp.json @@ -0,0 +1,43 @@ +{ + "header": { + "name": "jan Wisi Totiwi", + "blog": "lipu sona", + "contact": "lipu toki", + "resume": "lipu pali mi" + }, + "meta": { + "copyright": "lipu ni li pali e jan Wisi Totiwi pi suli %s. sina pali ala e ni. tenpo ale la nasin sona mi li nasin sona ala pi kulupu lawa mi.", + "rss_copyright": "lipu ni li pali e jan Wisi Totiwi. sina pali ala e ni. tenpo ale la nasin sona mi li nasin sona ala pi kulupu lawa mi.", + "json_feed": "lipu ni li lipu Sason Pe. sina wile sona la sina lukin e lipu: https://jsonfeed.org/version/1" + }, + "index": { + "contact_me": "toki e mi", + "title": "jan pali e ilo suli", + "skills": "kepeken pali e mi", + "highlighted_projects": "pali musi e mi" + }, + "blog": { + "index_title": "lipu sona", + "blogroll": "lipu sona pi jan ante", + "description": "lipu ni li jan Wisi Totiwi pi lipu sona. lipu sona ni li ilo pona li ilo sona.", + "title": "jan Wisi Totiwi pi lipu sona", + "posted_on": "tenpo pi %s", + "read_this": "lukin e ni", + "disclaimer": "tenpo %s la lipu ni li pali. tenpo ni la jan Wisi Totiwi li ken nasin sona ante." + }, + "contact": { + "header": "lipu toki e mi", + "email": "ilo toki", + "social_media": "kalama ilo toki", + "other_info": "lipu ante", + "donations": "ken la sina pana mani e mi. sina wile kepeken lipu: ", + "irc_comment": "mi kepeken e kulupu mute pi ilo toki. nimi mi li 'Xe' anu 'Xena' anu 'Cadey'." + }, + "error": { + "title": "pakala", + "post_not_found": "lipu %s li pona ala. ona li lukin ala." + }, + "resume": { + "plaintext": "sina wile lipu pona la sina lukin e ni" + } +} diff --git a/templates/base.html b/templates/base.html index c00bc1e..8c23220 100644 --- a/templates/base.html +++ b/templates/base.html @@ -56,11 +56,11 @@ {{ template "scripts" . }}
-

Christine Dodrill - Blog - Contact - Resume

+

{{ trans "header" "name" }} - {{ trans "header" "blog" }} - {{ trans "header" "contact" }} - {{ trans "header" "resume" }}

{{ template "content" . }}
-
Copyright 2018 Christine Dodrill. Any and all opinions listed here are my own and not representative of my employer.
+
{{ trans "meta" "copyright" "2018" }}