Add localizer interfaces (#46)

* add localizer interfaces
* add 1.18 to the supported versions
* use go 1.17 for lint for now
* use localizer interfaces in the library
* fix incorrect naming of the HTTPResponseLocalizer interface
* fix coverage collection
This commit is contained in:
Pavel 2022-04-22 09:37:16 +03:00 committed by GitHub
parent b260c8fb50
commit e7d06fa208
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 74 additions and 24 deletions

View File

@ -22,6 +22,7 @@ jobs:
- name: Set up Go 1.17 - name: Set up Go 1.17
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
# TODO: Should migrate to 1.18 later
go-version: '1.17' go-version: '1.17'
- name: Get dependencies - name: Get dependencies
run: go mod tidy run: go mod tidy
@ -35,7 +36,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
go-version: ['1.16', '1.17'] go-version: ['1.16', '1.17', '1.18']
steps: steps:
- name: Set up Go ${{ matrix.go-version }} - name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2 uses: actions/setup-go@v2
@ -49,6 +50,6 @@ jobs:
run: go test ./... -v -cpu 2 -timeout 10s -race -cover -coverprofile=coverage.txt -covermode=atomic run: go test ./... -v -cpu 2 -timeout 10s -race -cover -coverprofile=coverage.txt -covermode=atomic
- name: Coverage - name: Coverage
run: | run: |
go get -v -u github.com/axw/gocov/gocov go install github.com/axw/gocov/gocov@latest
gocov convert ./coverage.txt | gocov report gocov convert ./coverage.txt | gocov report
bash <(curl -s https://codecov.io/bash) bash <(curl -s https://codecov.io/bash)

View File

@ -137,7 +137,7 @@ func (e *Engine) Prepare() *Engine {
e.LocaleMatcher = DefaultLocalizerMatcher() e.LocaleMatcher = DefaultLocalizerMatcher()
} }
if e.isUnd(e.Localizer.LanguageTag) { if e.isUnd(e.Localizer.Language()) {
e.Localizer.LanguageTag = DefaultLanguage e.Localizer.LanguageTag = DefaultLanguage
} }

View File

@ -38,16 +38,60 @@ type Localizer struct {
TranslationsPath string TranslationsPath string
} }
// LocalizerInterface contains entire public interface of the localizer component.
type LocalizerInterface interface { type LocalizerInterface interface {
GetLocalizedMessage(messageID string) string MessageLocalizer
GetLocalizedTemplateMessage(messageID string, templateData map[string]interface{}) string LocalizerWithMiddleware
Localize(messageID string) (string, error) LocalizerWithFuncs
LocaleControls
HTTPResponseLocalizer
CloneableLocalizer
}
// MessageLocalizer can localize regular strings and strings with template parameters.
type MessageLocalizer interface {
GetLocalizedMessage(string) string
GetLocalizedTemplateMessage(string, map[string]interface{}) string
Localize(string) (string, error)
LocalizeTemplateMessage(string, map[string]interface{}) (string, error)
}
// LocalizerWithMiddleware can provide middlewares for usage in the app.
type LocalizerWithMiddleware interface {
LocalizationMiddleware() gin.HandlerFunc
}
// LocalizerWithFuncs can provide template functions.
type LocalizerWithFuncs interface {
LocalizationFuncMap() template.FuncMap
}
// LocaleControls is an instance of localizer with exposed locale controls.
type LocaleControls interface {
Preload([]language.Tag)
SetLocale(string)
SetLanguage(language.Tag)
Language() language.Tag
LoadTranslations()
}
// HTTPResponseLocalizer can localize strings and return them with HTTP error codes.
type HTTPResponseLocalizer interface {
BadRequestLocalized(string) (int, interface{})
UnauthorizedLocalized(string) (int, interface{})
ForbiddenLocalized(string) (int, interface{})
InternalServerErrorLocalized(string) (int, interface{})
}
// CloneableLocalizer is a localizer which can clone itself.
type CloneableLocalizer interface {
Clone() CloneableLocalizer
} }
// NewLocalizer returns localizer instance with specified parameters. // NewLocalizer returns localizer instance with specified parameters.
// Usage: // Usage:
// NewLocalizer(language.English, DefaultLocalizerMatcher(), "translations") // NewLocalizer(language.English, DefaultLocalizerMatcher(), "translations")
func NewLocalizer(locale language.Tag, matcher language.Matcher, translationsPath string) *Localizer { func NewLocalizer(locale language.Tag, matcher language.Matcher, translationsPath string) LocalizerInterface {
localizer := &Localizer{ localizer := &Localizer{
i18nStorage: &sync.Map{}, i18nStorage: &sync.Map{},
LocaleMatcher: matcher, LocaleMatcher: matcher,
@ -66,7 +110,7 @@ func NewLocalizer(locale language.Tag, matcher language.Matcher, translationsPat
// TODO This code should be covered with tests. // TODO This code should be covered with tests.
func NewLocalizerFS( func NewLocalizerFS(
locale language.Tag, matcher language.Matcher, translationsFS fs.FS, locale language.Tag, matcher language.Matcher, translationsFS fs.FS,
) *Localizer { ) LocalizerInterface {
localizer := &Localizer{ localizer := &Localizer{
i18nStorage: &sync.Map{}, i18nStorage: &sync.Map{},
LocaleMatcher: matcher, LocaleMatcher: matcher,
@ -97,7 +141,7 @@ func DefaultLocalizerMatcher() language.Matcher {
// Clone *core.Localizer. Clone shares it's translations with the parent localizer. Language tag will not be shared. // Clone *core.Localizer. Clone shares it's translations with the parent localizer. Language tag will not be shared.
// Because of that you can change clone's language without affecting parent localizer. // Because of that you can change clone's language without affecting parent localizer.
// This method should be used when LocalizationMiddleware is not feasible (outside of *gin.HandlerFunc). // This method should be used when LocalizationMiddleware is not feasible (outside of *gin.HandlerFunc).
func (l *Localizer) Clone() *Localizer { func (l *Localizer) Clone() CloneableLocalizer {
clone := &Localizer{ clone := &Localizer{
i18nStorage: l.i18nStorage, i18nStorage: l.i18nStorage,
TranslationsFS: l.TranslationsFS, TranslationsFS: l.TranslationsFS,
@ -122,7 +166,7 @@ func (l *Localizer) Clone() *Localizer {
// engine.Use(localizer.LocalizationMiddleware()) // engine.Use(localizer.LocalizationMiddleware())
func (l *Localizer) LocalizationMiddleware() gin.HandlerFunc { func (l *Localizer) LocalizationMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
clone := l.Clone() clone := l.Clone().(LocaleControls)
clone.SetLocale(c.GetHeader("Accept-Language")) clone.SetLocale(c.GetHeader("Accept-Language"))
c.Set(LocalizerContextKey, clone) c.Set(LocalizerContextKey, clone)
} }
@ -262,7 +306,7 @@ func (l *Localizer) isUnd(tag language.Tag) bool {
// getCurrentLocalizer returns *i18n.Localizer with current language tag. // getCurrentLocalizer returns *i18n.Localizer with current language tag.
func (l *Localizer) getCurrentLocalizer() *i18n.Localizer { func (l *Localizer) getCurrentLocalizer() *i18n.Localizer {
return l.getLocalizer(l.LanguageTag) return l.getLocalizer(l.Language())
} }
// SetLocale will change language for current localizer. // SetLocale will change language for current localizer.
@ -287,6 +331,11 @@ func (l *Localizer) SetLanguage(tag language.Tag) {
l.LoadTranslations() l.LoadTranslations()
} }
// Language returns current language tag.
func (l *Localizer) Language() language.Tag {
return l.LanguageTag
}
// FetchLanguage will load language from tag // FetchLanguage will load language from tag
// //
// Deprecated: Use `(*core.Localizer).LoadTranslations()` instead. // Deprecated: Use `(*core.Localizer).LoadTranslations()` instead.
@ -344,13 +393,13 @@ func (l *Localizer) InternalServerErrorLocalized(err string) (int, interface{})
// GetContextLocalizer returns localizer from context if it is present there. // GetContextLocalizer returns localizer from context if it is present there.
// Language will be set using Accept-Language header and root language tag. // Language will be set using Accept-Language header and root language tag.
func GetContextLocalizer(c *gin.Context) (loc *Localizer, ok bool) { func GetContextLocalizer(c *gin.Context) (loc LocalizerInterface, ok bool) {
loc, ok = extractLocalizerFromContext(c) loc, ok = extractLocalizerFromContext(c)
if loc != nil { if loc != nil {
loc.SetLocale(c.GetHeader("Accept-Language")) loc.SetLocale(c.GetHeader("Accept-Language"))
lang := GetRootLanguageTag(loc.LanguageTag) lang := GetRootLanguageTag(loc.Language())
if lang != loc.LanguageTag { if lang != loc.Language() {
loc.SetLanguage(lang) loc.SetLanguage(lang)
loc.LoadTranslations() loc.LoadTranslations()
} }
@ -359,7 +408,7 @@ func GetContextLocalizer(c *gin.Context) (loc *Localizer, ok bool) {
} }
// MustGetContextLocalizer returns Localizer instance if it exists in provided context. Panics otherwise. // MustGetContextLocalizer returns Localizer instance if it exists in provided context. Panics otherwise.
func MustGetContextLocalizer(c *gin.Context) *Localizer { func MustGetContextLocalizer(c *gin.Context) LocalizerInterface {
if localizer, ok := GetContextLocalizer(c); ok { if localizer, ok := GetContextLocalizer(c); ok {
return localizer return localizer
} }
@ -367,13 +416,13 @@ func MustGetContextLocalizer(c *gin.Context) *Localizer {
} }
// extractLocalizerFromContext returns localizer from context if it exist there. // extractLocalizerFromContext returns localizer from context if it exist there.
func extractLocalizerFromContext(c *gin.Context) (*Localizer, bool) { func extractLocalizerFromContext(c *gin.Context) (LocalizerInterface, bool) {
if c == nil { if c == nil {
return nil, false return nil, false
} }
if item, ok := c.Get(LocalizerContextKey); ok { if item, ok := c.Get(LocalizerContextKey); ok {
if localizer, ok := item.(*Localizer); ok { if localizer, ok := item.(LocalizerInterface); ok {
return localizer, true return localizer, true
} }
} }

View File

@ -48,7 +48,7 @@ func createTestLangFiles(t *testing.T) {
type LocalizerTest struct { type LocalizerTest struct {
suite.Suite suite.Suite
localizer *Localizer localizer LocalizerInterface
} }
func (l *LocalizerTest) SetupSuite() { func (l *LocalizerTest) SetupSuite() {
@ -92,9 +92,9 @@ func (l *LocalizerTest) Test_LocalizationMiddleware_Context() {
assert.NotNil(l.T(), esLocalizer) assert.NotNil(l.T(), esLocalizer)
assert.NotNil(l.T(), ruLocalizer) assert.NotNil(l.T(), ruLocalizer)
assert.Equal(l.T(), language.English, enLocalizer.LanguageTag) assert.Equal(l.T(), language.English, enLocalizer.Language())
assert.Equal(l.T(), language.Spanish, esLocalizer.LanguageTag) assert.Equal(l.T(), language.Spanish, esLocalizer.Language())
assert.Equal(l.T(), language.Russian, ruLocalizer.LanguageTag) assert.Equal(l.T(), language.Russian, ruLocalizer.Language())
assert.Equal(l.T(), "Test message", enLocalizer.GetLocalizedMessage("message")) assert.Equal(l.T(), "Test message", enLocalizer.GetLocalizedMessage("message"))
assert.Equal(l.T(), "Mensaje de prueba", esLocalizer.GetLocalizedMessage("message")) assert.Equal(l.T(), "Mensaje de prueba", esLocalizer.GetLocalizedMessage("message"))
@ -164,10 +164,10 @@ func (l *LocalizerTest) Test_Clone() {
require.Nil(l.T(), recover()) require.Nil(l.T(), recover())
}() }()
localizer := l.localizer.Clone() localizer := l.localizer.Clone().(LocalizerInterface)
localizer.SetLanguage(language.Russian) localizer.SetLanguage(language.Russian)
assert.NotEqual(l.T(), l.localizer.LanguageTag, localizer.LanguageTag) assert.NotEqual(l.T(), l.localizer.Language(), localizer.Language())
assert.Equal(l.T(), "Test message", l.localizer.GetLocalizedMessage("message")) assert.Equal(l.T(), "Test message", l.localizer.GetLocalizedMessage("message"))
assert.Equal(l.T(), "Тестовое сообщение", localizer.GetLocalizedMessage("message")) assert.Equal(l.T(), "Тестовое сообщение", localizer.GetLocalizedMessage("message"))
} }

View File

@ -46,7 +46,7 @@ type SentryTagged interface {
type Sentry struct { type Sentry struct {
SentryConfig sentry.ClientOptions SentryConfig sentry.ClientOptions
Logger logger.Logger Logger logger.Logger
Localizer *Localizer Localizer MessageLocalizer
AppInfo AppInfo AppInfo AppInfo
SentryLoggerConfig SentryLoggerConfig SentryLoggerConfig SentryLoggerConfig
ServerName string ServerName string