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
uses: actions/setup-go@v2
with:
# TODO: Should migrate to 1.18 later
go-version: '1.17'
- name: Get dependencies
run: go mod tidy
@ -35,7 +36,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.16', '1.17']
go-version: ['1.16', '1.17', '1.18']
steps:
- name: Set up Go ${{ matrix.go-version }}
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
- name: Coverage
run: |
go get -v -u github.com/axw/gocov/gocov
go install github.com/axw/gocov/gocov@latest
gocov convert ./coverage.txt | gocov report
bash <(curl -s https://codecov.io/bash)

View File

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

View File

@ -38,16 +38,60 @@ type Localizer struct {
TranslationsPath string
}
// LocalizerInterface contains entire public interface of the localizer component.
type LocalizerInterface interface {
GetLocalizedMessage(messageID string) string
GetLocalizedTemplateMessage(messageID string, templateData map[string]interface{}) string
Localize(messageID string) (string, error)
MessageLocalizer
LocalizerWithMiddleware
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.
// Usage:
// 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{
i18nStorage: &sync.Map{},
LocaleMatcher: matcher,
@ -66,7 +110,7 @@ func NewLocalizer(locale language.Tag, matcher language.Matcher, translationsPat
// TODO This code should be covered with tests.
func NewLocalizerFS(
locale language.Tag, matcher language.Matcher, translationsFS fs.FS,
) *Localizer {
) LocalizerInterface {
localizer := &Localizer{
i18nStorage: &sync.Map{},
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.
// 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).
func (l *Localizer) Clone() *Localizer {
func (l *Localizer) Clone() CloneableLocalizer {
clone := &Localizer{
i18nStorage: l.i18nStorage,
TranslationsFS: l.TranslationsFS,
@ -122,7 +166,7 @@ func (l *Localizer) Clone() *Localizer {
// engine.Use(localizer.LocalizationMiddleware())
func (l *Localizer) LocalizationMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
clone := l.Clone()
clone := l.Clone().(LocaleControls)
clone.SetLocale(c.GetHeader("Accept-Language"))
c.Set(LocalizerContextKey, clone)
}
@ -262,7 +306,7 @@ func (l *Localizer) isUnd(tag language.Tag) bool {
// getCurrentLocalizer returns *i18n.Localizer with current language tag.
func (l *Localizer) getCurrentLocalizer() *i18n.Localizer {
return l.getLocalizer(l.LanguageTag)
return l.getLocalizer(l.Language())
}
// SetLocale will change language for current localizer.
@ -287,6 +331,11 @@ func (l *Localizer) SetLanguage(tag language.Tag) {
l.LoadTranslations()
}
// Language returns current language tag.
func (l *Localizer) Language() language.Tag {
return l.LanguageTag
}
// FetchLanguage will load language from tag
//
// 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.
// 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)
if loc != nil {
loc.SetLocale(c.GetHeader("Accept-Language"))
lang := GetRootLanguageTag(loc.LanguageTag)
if lang != loc.LanguageTag {
lang := GetRootLanguageTag(loc.Language())
if lang != loc.Language() {
loc.SetLanguage(lang)
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.
func MustGetContextLocalizer(c *gin.Context) *Localizer {
func MustGetContextLocalizer(c *gin.Context) LocalizerInterface {
if localizer, ok := GetContextLocalizer(c); ok {
return localizer
}
@ -367,13 +416,13 @@ func MustGetContextLocalizer(c *gin.Context) *Localizer {
}
// 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 {
return nil, false
}
if item, ok := c.Get(LocalizerContextKey); ok {
if localizer, ok := item.(*Localizer); ok {
if localizer, ok := item.(LocalizerInterface); ok {
return localizer, true
}
}

View File

@ -48,7 +48,7 @@ func createTestLangFiles(t *testing.T) {
type LocalizerTest struct {
suite.Suite
localizer *Localizer
localizer LocalizerInterface
}
func (l *LocalizerTest) SetupSuite() {
@ -92,9 +92,9 @@ func (l *LocalizerTest) Test_LocalizationMiddleware_Context() {
assert.NotNil(l.T(), esLocalizer)
assert.NotNil(l.T(), ruLocalizer)
assert.Equal(l.T(), language.English, enLocalizer.LanguageTag)
assert.Equal(l.T(), language.Spanish, esLocalizer.LanguageTag)
assert.Equal(l.T(), language.Russian, ruLocalizer.LanguageTag)
assert.Equal(l.T(), language.English, enLocalizer.Language())
assert.Equal(l.T(), language.Spanish, esLocalizer.Language())
assert.Equal(l.T(), language.Russian, ruLocalizer.Language())
assert.Equal(l.T(), "Test message", enLocalizer.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())
}()
localizer := l.localizer.Clone()
localizer := l.localizer.Clone().(LocalizerInterface)
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(), "Тестовое сообщение", localizer.GetLocalizedMessage("message"))
}

View File

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