Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 25 additions & 16 deletions v3/i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,18 @@ go get -u github.com/gofiber/fiber/v3
go get -u github.com/gofiber/contrib/v3/i18n
```

## Signature
## API

| Name | Signature | Description |
|--------------|----------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|
| New | `New(config ...*i18n.Config) fiber.Handler` | Create a new i18n 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 |
| Name | Signature | Description |
|----------------------|--------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| New | `New(config ...*i18n.Config) *i18n.I18n` | Create a reusable, thread-safe localization container. |
| (*I18n).Localize | `Localize(ctx fiber.Ctx, params interface{}) (string, error)` | Returns a localized message. `params` may be a message ID or `*i18n.LocalizeConfig`. |
| (*I18n).MustLocalize | `MustLocalize(ctx fiber.Ctx, params interface{}) string` | Like `Localize` but panics when localization fails. |

## 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"` |
Expand All @@ -60,23 +59,22 @@ import (
)

func main() {
translator := contribi18n.New(&contribi18n.Config{
RootPath: "./example/localize",
AcceptLanguages: []language.Tag{language.Chinese, language.English},
DefaultLanguage: language.Chinese,
})

app := fiber.New()
app.Use(
contribi18n.New(&contribi18n.Config{
RootPath: "./example/localize",
AcceptLanguages: []language.Tag{language.Chinese, language.English},
DefaultLanguage: language.Chinese,
}),
)
app.Get("/", func(c fiber.Ctx) error {
localize, err := contribi18n.Localize(c, "welcome")
localize, err := translator.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(contribi18n.MustLocalize(ctx, &goi18n.LocalizeConfig{
return ctx.SendString(translator.MustLocalize(ctx, &goi18n.LocalizeConfig{
MessageID: "welcomeWithName",
TemplateData: map[string]string{
"name": ctx.Params("name"),
Expand All @@ -86,3 +84,14 @@ func main() {
log.Fatal(app.Listen(":3000"))
}
```

## Migration from middleware usage

The package now exposes a global, thread-safe container instead of middleware. To migrate existing code:

1. Remove any `app.Use(i18n.New(...))` calls—the translator no longer registers middleware.
2. Instantiate a shared translator during application startup with `translator := i18n.New(...)`.
3. Replace package-level calls such as `i18n.Localize`/`i18n.MustLocalize` with the respective methods on your translator (`translator.Localize`, `translator.MustLocalize`).
4. Drop any manual interaction with `ctx.Locals("i18n")`; all state is managed inside the translator instance.

The translator instance is safe for concurrent use across handlers and reduces per-request allocations by reusing the same bundle and localizer map.
54 changes: 21 additions & 33 deletions v3/i18n/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ import (
)

type Config struct {
// Next defines a function to skip this middleware when returned true.
//
// Optional. Default: nil
Next func(c fiber.Ctx) bool

// RootPath is i18n template folder path
//
// Default: ./example/localize
Expand Down Expand Up @@ -54,7 +49,6 @@ type Config struct {

bundle *i18n.Bundle
localizerMap *sync.Map
mu sync.Mutex
}

type Loader interface {
Expand All @@ -68,7 +62,6 @@ 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 @@ -82,30 +75,29 @@ 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 != "" {
return lang
if lang := c.Query("lang"); lang != "" {
return utils.CopyString(lang)
}
lang = utils.CopyString(c.Get("Accept-Language"))
if lang != "" {
return lang
if lang := c.Get("Accept-Language"); lang != "" {
return utils.CopyString(lang)
}

return defaultLang
}

func configDefault(config ...*Config) *Config {
// Return default config if nothing provided
if len(config) == 0 {
return ConfigDefault
}

// Override default config
cfg := config[0]

if cfg.Next == nil {
cfg.Next = ConfigDefault.Next
var cfg *Config

switch {
case len(config) == 0 || config[0] == nil:
copyCfg := *ConfigDefault
// ensure mutable fields are not shared with defaults
if copyCfg.AcceptLanguages != nil {
copyCfg.AcceptLanguages = append([]language.Tag(nil), copyCfg.AcceptLanguages...)
}
cfg = &copyCfg
default:
cfg = config[0]
}

if cfg.RootPath == "" {
Expand All @@ -116,26 +108,22 @@ func configDefault(config ...*Config) *Config {
cfg.DefaultLanguage = ConfigDefault.DefaultLanguage
}

if cfg.UnmarshalFunc == nil {
cfg.UnmarshalFunc = ConfigDefault.UnmarshalFunc
}

if cfg.FormatBundleFile == "" {
cfg.FormatBundleFile = ConfigDefault.FormatBundleFile
}

if cfg.UnmarshalFunc == nil {
cfg.UnmarshalFunc = ConfigDefault.UnmarshalFunc
}

if cfg.AcceptLanguages == nil {
cfg.AcceptLanguages = ConfigDefault.AcceptLanguages
cfg.AcceptLanguages = append([]language.Tag(nil), ConfigDefault.AcceptLanguages...)
}

if cfg.Loader == nil {
cfg.Loader = ConfigDefault.Loader
}

if cfg.UnmarshalFunc == nil {
cfg.UnmarshalFunc = ConfigDefault.UnmarshalFunc
}

if cfg.LangHandler == nil {
cfg.LangHandler = ConfigDefault.LangHandler
}
Expand Down
11 changes: 6 additions & 5 deletions v3/i18n/embed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ import (
var fs embed.FS

func newEmbedServer() *fiber.App {
app := fiber.New()
app.Use(New(&Config{
translator := New(&Config{
Loader: &EmbedLoader{fs},
UnmarshalFunc: json.Unmarshal,
RootPath: "./example/localizeJSON/",
FormatBundleFile: "json",
}))
})
app := fiber.New()
app.Get("/", func(ctx fiber.Ctx) error {
return ctx.SendString(MustLocalize(ctx, "welcome"))
return ctx.SendString(translator.MustLocalize(ctx, "welcome"))
})
app.Get("/:name", func(ctx fiber.Ctx) error {
return ctx.SendString(MustLocalize(ctx, &i18n.LocalizeConfig{
return ctx.SendString(translator.MustLocalize(ctx, &i18n.LocalizeConfig{
MessageID: "welcomeWithName",
TemplateData: map[string]string{
"name": ctx.Params("name"),
Expand Down Expand Up @@ -93,6 +93,7 @@ func TestEmbedLoader_LoadMessage(t *testing.T) {
got, err := request(tt.args.lang, tt.args.name)
assert.Equal(t, err, nil)
body, err := io.ReadAll(got.Body)
got.Body.Close()
assert.Equal(t, err, nil)
assert.Equal(t, tt.want, string(body))
})
Expand Down
17 changes: 8 additions & 9 deletions v3/i18n/example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,22 @@ import (
)

func main() {
translator := contribi18n.New(&contribi18n.Config{
RootPath: "./localize",
AcceptLanguages: []language.Tag{language.Chinese, language.English},
DefaultLanguage: language.Chinese,
})

app := fiber.New()
app.Use(
contribi18n.New(&contribi18n.Config{
RootPath: "./localize",
AcceptLanguages: []language.Tag{language.Chinese, language.English},
DefaultLanguage: language.Chinese,
}),
)
app.Get("/", func(c fiber.Ctx) error {
localize, err := contribi18n.Localize(c, "welcome")
localize, err := translator.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(contribi18n.MustLocalize(ctx, &goi18n.LocalizeConfig{
return ctx.SendString(translator.MustLocalize(ctx, &goi18n.LocalizeConfig{
MessageID: "welcomeWithName",
TemplateData: map[string]string{
"name": ctx.Params("name"),
Expand Down
64 changes: 37 additions & 27 deletions v3/i18n/i18n.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,43 @@ import (
"path"
"sync"

"github.com/gofiber/fiber/v3/log"

"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/log"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)

const localsKey = "i18n"
// I18n exposes thread-safe localization helpers backed by a shared bundle
// and localizer map. Use New to construct an instance during application start
// and reuse it across handlers.
type I18n struct {
cfg *Config
}

// New prepares a thread-safe i18n container instance.
func New(config ...*Config) *I18n {
cfg := prepareConfig(config...)

return &I18n{cfg: cfg}
}

func prepareConfig(config ...*Config) *Config {
source := configDefault(config...)

cfg := *source

if source.AcceptLanguages != nil {
cfg.AcceptLanguages = append([]language.Tag(nil), source.AcceptLanguages...)
}

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

cfg.loadMessages()
cfg.initLocalizerMap()

return func(c fiber.Ctx) error {
if cfg.Next != nil && cfg.Next(c) {
return c.Next()
}
c.Locals(localsKey, cfg)
return c.Next()
}
return &cfg
}

func (c *Config) loadMessage(filepath string) {
Expand Down Expand Up @@ -64,9 +75,7 @@ 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()
}

/*
Expand All @@ -82,8 +91,8 @@ MustLocalize get the i18n message without error handling
},
})
*/
func MustLocalize(ctx fiber.Ctx, params interface{}) string {
message, err := Localize(ctx, params)
func (i *I18n) MustLocalize(ctx fiber.Ctx, params interface{}) string {
message, err := i.Localize(ctx, params)
if err != nil {
panic(err)
}
Expand All @@ -103,17 +112,12 @@ Localize get the i18n message
},
})
*/
func Localize(ctx fiber.Ctx, params interface{}) (string, error) {
local := ctx.Locals(localsKey)
if local == nil {
return "", fmt.Errorf("i18n.Localize error: %v", "Config is nil")
}

appCfg, ok := local.(*Config)
if !ok {
return "", fmt.Errorf("i18n.Localize error: %v", "Config is not *Config type")
func (i *I18n) Localize(ctx fiber.Ctx, params interface{}) (string, error) {
if i == nil || i.cfg == nil {
return "", fmt.Errorf("i18n.Localize error: %v", "translator is nil")
}

appCfg := i.cfg
lang := appCfg.LangHandler(ctx, appCfg.DefaultLanguage.String())
localizer, _ := appCfg.localizerMap.Load(lang)

Expand All @@ -128,6 +132,12 @@ func Localize(ctx fiber.Ctx, params interface{}) (string, error) {
localizeConfig = &i18n.LocalizeConfig{MessageID: paramValue}
case *i18n.LocalizeConfig:
localizeConfig = paramValue
default:
return "", fmt.Errorf("i18n.Localize error: %v", "unsupported params type")
}

if localizer == nil {
return "", fmt.Errorf("i18n.Localize error: %v", "localizer is nil")
}

message, err := localizer.(*i18n.Localizer).Localize(localizeConfig)
Expand Down
Loading
Loading