Skip to content

Commit

Permalink
Implement gobuffalo#723 localized views
Browse files Browse the repository at this point in the history
  • Loading branch information
stanislas-m committed Nov 23, 2017
1 parent bdf85c6 commit 4b0ea93
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 10 deletions.
29 changes: 26 additions & 3 deletions middleware/i18n/i18n.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package i18n

import (
"log"
"strings"

"github.com/gobuffalo/buffalo"
"github.com/gobuffalo/packr"
Expand All @@ -21,7 +22,7 @@ type LanguageFinder func(*Translator, buffalo.Context) []string
type Translator struct {
// Box - where are the files?
Box packr.Box
// DefaultLanguage - default is "en-US"
// DefaultLanguage - default is passed as a parameter on New.
DefaultLanguage string
// CookieName - name of the cookie to find the desired language.
// default is "lang"
Expand All @@ -32,6 +33,8 @@ type Translator struct {
// HelperName - name of the view helper. default is "t"
HelperName string
LanguageFinder LanguageFinder
// LocalizedViews - enable localized views feature
LocalizedViews bool
}

// Load translations from the t.Box.
Expand Down Expand Up @@ -63,6 +66,7 @@ func New(box packr.Box, language string) (*Translator, error) {
SessionName: "lang",
HelperName: "t",
LanguageFinder: defaultLanguageFinder,
LocalizedViews: false,
}
return t, t.Load()
}
Expand Down Expand Up @@ -92,6 +96,9 @@ func (t *Translator) Middleware() buffalo.MiddlewareFunc {
// set languages in context, if not set yet
if langs := c.Value("languages"); langs == nil {
c.Set("languages", t.LanguageFinder(t, c))
if t.LocalizedViews {
c.Set("templateSuffixes", c.Value("languages"))
}
}

// set translator
Expand Down Expand Up @@ -157,10 +164,26 @@ func defaultLanguageFinder(t *Translator, c buffalo.Context) []string {
// try to get the language from a header:
acceptLang := r.Header.Get("Accept-Language")
if acceptLang != "" {
langs = append(langs, acceptLang)
langs = append(langs, parseAcceptLanguage(acceptLang)...)
}

// try to get the language from the session:
// finally set the default app language as fallback
langs = append(langs, t.DefaultLanguage)
return langs
}

// Inspired from https://siongui.github.io/2015/02/22/go-parse-accept-language/
// Parse an Accept-Language string to get usable lang values for i18n system
func parseAcceptLanguage(acptLang string) []string {
var lqs []string

langQStrs := strings.Split(acptLang, ",")
for _, langQStr := range langQStrs {
trimedLangQStr := strings.Trim(langQStr, " ")

langQ := strings.Split(trimedLangQStr, ";")
lq := langQ[0]
lqs = append(lqs, lq)
}
return lqs
}
41 changes: 34 additions & 7 deletions middleware/i18n/i18n_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type User struct {
LastName string
}

func app() *buffalo.App {
func app(localizedViews bool) *buffalo.App {
app := buffalo.New(buffalo.Options{})

r := render.New(render.Options{
Expand All @@ -28,6 +28,9 @@ func app() *buffalo.App {
if err != nil {
log.Fatal(err)
}
if localizedViews {
t.LocalizedViews = true
}
app.Use(t.Middleware())
app.GET("/", func(c buffalo.Context) error {
return c.Render(200, r.HTML("index.html"))
Expand All @@ -42,21 +45,24 @@ func app() *buffalo.App {
c.Set("Users", usersList)
return c.Render(200, r.HTML("format.html"))
})
app.GET("/localized", func(c buffalo.Context) error {
return c.Render(200, r.HTML("localized_view.html"))
})
return app
}

func Test_i18n(t *testing.T) {
r := require.New(t)

w := willie.New(app())
w := willie.New(app(false))
res := w.Request("/").Get()
r.Equal("Hello, World!\n", res.Body.String())
}

func Test_i18n_fr(t *testing.T) {
r := require.New(t)

w := willie.New(app())
w := willie.New(app(false))
req := w.Request("/")
// Set language as "french"
req.Headers["Accept-Language"] = "fr-fr"
Expand All @@ -67,15 +73,15 @@ func Test_i18n_fr(t *testing.T) {
func Test_i18n_plural(t *testing.T) {
r := require.New(t)

w := willie.New(app())
w := willie.New(app(false))
res := w.Request("/plural").Get()
r.Equal("Hello, alone!\nHello, 5 people!\n", res.Body.String())
}

func Test_i18n_plural_fr(t *testing.T) {
r := require.New(t)

w := willie.New(app())
w := willie.New(app(false))
req := w.Request("/plural")
// Set language as "french"
req.Headers["Accept-Language"] = "fr-fr"
Expand All @@ -86,18 +92,39 @@ func Test_i18n_plural_fr(t *testing.T) {
func Test_i18n_format(t *testing.T) {
r := require.New(t)

w := willie.New(app())
w := willie.New(app(false))
res := w.Request("/format").Get()
r.Equal("Hello Mark!\n\n\t* Mr. Mark Bates\n\n\t* Mr. Chuck Berry\n", res.Body.String())
}

func Test_i18n_format_fr(t *testing.T) {
r := require.New(t)

w := willie.New(app())
w := willie.New(app(false))
req := w.Request("/format")
// Set language as "french"
req.Headers["Accept-Language"] = "fr-fr"
res := req.Get()
r.Equal("Bonjour Mark !\n\n\t* M. Mark Bates\n\n\t* M. Chuck Berry\n", res.Body.String())
}

func Test_i18n_Localized_View(t *testing.T) {
r := require.New(t)

w := willie.New(app(true))
// Test with complex Accept-Language
req := w.Request("/localized")
req.Headers["Accept-Language"] = "en-US,en;q=0.5"
res := req.Get()
r.Equal("Hello!\n", res.Body.String())

// Test priority
req.Headers["Accept-Language"] = "fr,en"
res = req.Get()
r.Equal("Bonjour !\n", res.Body.String())

// Test fallback
req.Headers["Accept-Language"] = "ru"
res = req.Get()
r.Equal("Default\n", res.Body.String())
}
1 change: 1 addition & 0 deletions middleware/i18n/templates/localized_view.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Default
1 change: 1 addition & 0 deletions middleware/i18n/templates/localized_view_en.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello!
1 change: 1 addition & 0 deletions middleware/i18n/templates/localized_view_fr.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Bonjour !
15 changes: 15 additions & 0 deletions render/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ func (s templateRenderer) exec(name string, data Data) (template.HTML, error) {
}
}

// Try to use a suffixed version first (if provided)
if suffixes, ok := data["templateSuffixes"].([]string); ok {
ext := filepath.Ext(name)
rawName := strings.TrimSuffix(name, ext)

for _, suffix := range suffixes {
candidateName := rawName + "_" + suffix + ext
if s.TemplatesBox.Has(candidateName) {
// Replace name with the existing suffixed version
name = candidateName
break
}
}
}

source, err := s.TemplatesBox.MustBytes(name)
if err != nil {
return "", err
Expand Down
43 changes: 43 additions & 0 deletions render/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,46 @@ func Test_AssetPathManifestCorrupt(t *testing.T) {
os.Remove(tmpFile.Name())
}
}

func Test_Suffixes(t *testing.T) {
r := require.New(t)

tPath, err := ioutil.TempDir("", "test")
r.NoError(err)

tmpFile, err := os.Create(filepath.Join(tPath, "test"))
r.NoError(err)
defer os.Remove(tmpFile.Name())

tmpFileSuffixed, err := os.Create(tmpFile.Name() + "_alt")
r.NoError(err)
defer os.Remove(tmpFileSuffixed.Name())

_, err = tmpFile.Write([]byte("normal"))
r.NoError(err)

_, err = tmpFileSuffixed.Write([]byte("suffixed"))
r.NoError(err)

type ji func(string, ...string) render.Renderer

table := []ji{
render.New(render.Options{
TemplatesBox: packr.NewBox(tPath),
}).Template,
}

for _, j := range table {
re := j("foo/bar", filepath.Base(tmpFile.Name()))
r.Equal("foo/bar", re.ContentType())
bb1 := &bytes.Buffer{}
err = re.Render(bb1, render.Data{})
r.NoError(err)
r.Equal("normal", strings.TrimSpace(bb1.String()))

bb2 := &bytes.Buffer{}
err = re.Render(bb2, render.Data{"templateSuffixes": []string{"alt"}})
r.NoError(err)
r.Equal("suffixed", strings.TrimSpace(bb2.String()))
}
}

0 comments on commit 4b0ea93

Please sign in to comment.