Skip to content

Commit

Permalink
Merge pull request #723 from Skyenought/i18n/bug
Browse files Browse the repository at this point in the history
🩹 (fiberi18n): Fix request concurrency errors
  • Loading branch information
ReneWerner87 authored Sep 5, 2023
2 parents f355ca0 + 3f4576a commit 64e52f3
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 60 deletions.
1 change: 0 additions & 1 deletion .github/workflows/test-fiberi18n.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ jobs:
strategy:
matrix:
go-version:
- 1.18.x
- 1.19.x
- 1.20.x
- 1.21.x
Expand Down
44 changes: 26 additions & 18 deletions fiberi18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,35 +20,39 @@ This middleware supports Fiber v2.

```
go get -u github.com/gofiber/fiber/v2
go get -u github.com/gofiber/contrib/fiberi18n
go get -u github.com/gofiber/contrib/fiberi18n/v2
```

## Signature

```
fiberi18n.New(config ...*fiberi18n.Config) fiber.Handler
```
| Name | Signature | Description |
|--------------|----------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|
| New | `New(config ...*fiberi18n.Config) fiber.Handler` | Create a new fiberi18n middleware handler |
| Localize | `Localize(ctx *fiber.Ctx, params interface{}) (string, error)` | Localize returns a localized message. param is one of these type: messageID, *i18n.LocalizeConfig |
| MustLocalize | `MustLocalize(ctx *fiber.Ctx, params interface{}) string` | MustLocalize is similar to Localize, except it panics if an error happens. param is one of these type: messageID, *i18n.LocalizeConfig |

## Config

| Property | Type | Description | Default |
| ---------------- | ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| Next | `func(c *fiber.Ctx) bool` | A function to skip this middleware when returned `true`. | `nil` |
| RootPath | `string` | The i18n template folder path. | `"./example/localize"` |
| AcceptLanguages | `[]language.Tag` | A collection of languages that can be processed. | `[]language.Tag{language.Chinese, language.English}` |
| FormatBundleFile | `string` | The type of the template file. | `"yaml"` |
| DefaultLanguage | `language.Tag` | The default returned language type. | `language.English` |
| Loader | `Loader` | The implementation of the Loader interface, which defines how to read the file. We provide both os.ReadFile and embed.FS.ReadFile. | `LoaderFunc(os.ReadFile)` |
| UnmarshalFunc | `i18n.UnmarshalFunc` | The function used for decoding template files. | `yaml.Unmarshal` |
| LangHandler | `func(ctx *fiber.Ctx, defaultLang string) string` | Used to get the kind of language handled by *fiber.Ctx and defaultLang. | Retrieved from the request header `Accept-Language` or query parameter `lang`. |
| Property | Type | Description | Default |
|------------------|---------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| Next | `func(c *fiber.Ctx) bool` | A function to skip this middleware when returned `true`. | `nil` |
| RootPath | `string` | The i18n template folder path. | `"./example/localize"` |
| AcceptLanguages | `[]language.Tag` | A collection of languages that can be processed. | `[]language.Tag{language.Chinese, language.English}` |
| FormatBundleFile | `string` | The type of the template file. | `"yaml"` |
| DefaultLanguage | `language.Tag` | The default returned language type. | `language.English` |
| Loader | `Loader` | The implementation of the Loader interface, which defines how to read the file. We provide both os.ReadFile and embed.FS.ReadFile. | `LoaderFunc(os.ReadFile)` |
| UnmarshalFunc | `i18n.UnmarshalFunc` | The function used for decoding template files. | `yaml.Unmarshal` |
| LangHandler | `func(ctx *fiber.Ctx, defaultLang string) string` | Used to get the kind of language handled by *fiber.Ctx and defaultLang. | Retrieved from the request header `Accept-Language` or query parameter `lang`. |

## Example

```go
package main

import (
"github.com/gofiber/contrib/fiberi18n"
"log"

"github.com/gofiber/contrib/fiberi18n/v2"
"github.com/gofiber/fiber/v2"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
Expand All @@ -64,17 +68,21 @@ func main() {
}),
)
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString(fiberi18n.MustGetMessage("welcome"))
localize, err := fiberi18n.Localize(c, "welcome")
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return c.SendString(localize)
})
app.Get("/:name", func(ctx *fiber.Ctx) error {
return ctx.SendString(fiberi18n.MustGetMessage(&i18n.LocalizeConfig{
return ctx.SendString(fiberi18n.MustLocalize(ctx, &i18n.LocalizeConfig{
MessageID: "welcomeWithName",
TemplateData: map[string]string{
"name": ctx.Params("name"),
},
}))
})
app.Listen("127.0.0.1:3000")
log.Fatal(app.Listen(":3000"))
}
```

6 changes: 5 additions & 1 deletion fiberi18n/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ type Config struct {
// Optional. Default: The language type is retrieved from the request header: `Accept-Language` or query param : `lang`
LangHandler func(ctx *fiber.Ctx, defaultLang string) string

ctx *fiber.Ctx
bundle *i18n.Bundle
localizerMap *sync.Map
mu sync.Mutex
}

type Loader interface {
Expand All @@ -68,6 +68,7 @@ func (f LoaderFunc) LoadMessage(path string) ([]byte, error) {
}

var ConfigDefault = &Config{
Next: nil,
RootPath: "./example/localize",
DefaultLanguage: language.English,
AcceptLanguages: []language.Tag{language.Chinese, language.English},
Expand All @@ -78,6 +79,9 @@ var ConfigDefault = &Config{
}

func defaultLangHandler(c *fiber.Ctx, defaultLang string) string {
if c == nil || c.Request() == nil {
return defaultLang
}
var lang string
lang = utils.CopyString(c.Query("lang"))
if lang != "" {
Expand Down
5 changes: 3 additions & 2 deletions fiberi18n/embed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ func newEmbedServer() *fiber.App {
FormatBundleFile: "json",
}))
app.Get("/", func(ctx *fiber.Ctx) error {
return ctx.SendString(MustGetMessage("welcome"))
return ctx.SendString(MustLocalize(ctx, "welcome"))
})
app.Get("/:name", func(ctx *fiber.Ctx) error {
return ctx.SendString(MustGetMessage(&i18n.LocalizeConfig{
return ctx.SendString(MustLocalize(ctx, &i18n.LocalizeConfig{
MessageID: "welcomeWithName",
TemplateData: map[string]string{
"name": ctx.Params("name"),
Expand All @@ -52,6 +52,7 @@ func request(lang language.Tag, name string) (*http.Response, error) {
}

func TestEmbedLoader_LoadMessage(t *testing.T) {
t.Parallel()
type args struct {
lang language.Tag
name string
Expand Down
10 changes: 7 additions & 3 deletions fiberi18n/example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package main
import (
"log"

"github.com/gofiber/contrib/fiberi18n"
"github.com/gofiber/contrib/fiberi18n/v2"
"github.com/gofiber/fiber/v2"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
Expand All @@ -19,10 +19,14 @@ func main() {
}),
)
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString(fiberi18n.MustGetMessage("welcome"))
localize, err := fiberi18n.Localize(c, "welcome")
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return c.SendString(localize)
})
app.Get("/:name", func(ctx *fiber.Ctx) error {
return ctx.SendString(fiberi18n.MustGetMessage(&i18n.LocalizeConfig{
return ctx.SendString(fiberi18n.MustLocalize(ctx, &i18n.LocalizeConfig{
MessageID: "welcomeWithName",
TemplateData: map[string]string{
"name": ctx.Params("name"),
Expand Down
8 changes: 4 additions & 4 deletions fiberi18n/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/gofiber/contrib/fiberi18n
module github.com/gofiber/contrib/fiberi18n/v2

go 1.18
go 1.19

require (
github.com/gofiber/fiber/v2 v2.49.1
Expand All @@ -16,9 +16,9 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.49.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/sys v0.12.0 // indirect
)
7 changes: 4 additions & 3 deletions fiberi18n/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.49.0 h1:9FdvCpmxB74LH4dPb7IJ1cOSsluR07XG3I1txXWwJpE=
Expand All @@ -41,8 +42,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand Down
47 changes: 27 additions & 20 deletions fiberi18n/i18n.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,32 @@ package fiberi18n

import (
"fmt"
"github.com/gofiber/fiber/v2/log"
"path"
"sync"

"github.com/gofiber/fiber/v2"
"github.com/nicksnyder/go-i18n/v2/i18n"
)

var appCfg *Config
const localsKey = "fiberi18n"

// New creates a new middleware handler
func New(config ...*Config) fiber.Handler {
appCfg = configDefault(config...)
cfg := configDefault(config...)
// init bundle
bundle := i18n.NewBundle(appCfg.DefaultLanguage)
bundle.RegisterUnmarshalFunc(appCfg.FormatBundleFile, appCfg.UnmarshalFunc)
appCfg.bundle = bundle
bundle := i18n.NewBundle(cfg.DefaultLanguage)
bundle.RegisterUnmarshalFunc(cfg.FormatBundleFile, cfg.UnmarshalFunc)
cfg.bundle = bundle

appCfg.loadMessages().initLocalizerMap()
cfg.loadMessages()
cfg.initLocalizerMap()

return func(c *fiber.Ctx) error {
if appCfg.Next != nil && appCfg.Next(c) {
if cfg.Next != nil && cfg.Next(c) {
return c.Next()
}

appCfg.ctx = c

c.Locals(localsKey, cfg)
return c.Next()
}
}
Expand Down Expand Up @@ -63,42 +63,48 @@ func (c *Config) initLocalizerMap() {
if _, ok := localizerMap.Load(lang); !ok {
localizerMap.Store(lang, i18n.NewLocalizer(c.bundle, lang))
}
c.mu.Lock()
c.localizerMap = localizerMap
c.mu.Unlock()
}

/*
MustGetMessage get the i18n message without error handling
MustLocalize get the i18n message without error handling
param is one of these type: messageID, *i18n.LocalizeConfig
Example:
MustGetMessage("hello") // messageID is hello
MustGetMessage(&i18n.LocalizeConfig{
MustLocalize(ctx, "hello") // messageID is hello
MustLocalize(ctx, &i18n.LocalizeConfig{
MessageID: "welcomeWithName",
TemplateData: map[string]string{
"name": context.Param("name"),
},
})
*/
func MustGetMessage(params interface{}) string {
message, _ := GetMessage(params)
func MustLocalize(ctx *fiber.Ctx, params interface{}) string {
message, err := Localize(ctx, params)
if err != nil {
panic(err)
}
return message
}

/*
GetMessage get the i18n message
Localize get the i18n message
param is one of these type: messageID, *i18n.LocalizeConfig
Example:
GetMessage("hello") // messageID is hello
GetMessage(&i18n.LocalizeConfig{
Localize(ctx, "hello") // messageID is hello
Localize(ctx, &i18n.LocalizeConfig{
MessageID: "welcomeWithName",
TemplateData: map[string]string{
"name": context.Param("name"),
},
})
*/
func GetMessage(params interface{}) (string, error) {
lang := appCfg.LangHandler(appCfg.ctx, appCfg.DefaultLanguage.String())
func Localize(ctx *fiber.Ctx, params interface{}) (string, error) {
appCfg := ctx.Locals(localsKey).(*Config)
lang := appCfg.LangHandler(ctx, appCfg.DefaultLanguage.String())

localizer, _ := appCfg.localizerMap.Load(lang)
if localizer == nil {
Expand All @@ -116,6 +122,7 @@ func GetMessage(params interface{}) (string, error) {

message, err := localizer.(*i18n.Localizer).Localize(localizeConfig)
if err != nil {
log.Errorf("i18n.Localize error: %v", err)
return "", fmt.Errorf("i18n.Localize error: %v", err)
}
return message, nil
Expand Down
Loading

0 comments on commit 64e52f3

Please sign in to comment.