From e7d06fa2089052f857742cc5fb4d2c83f5f15c7f Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 22 Apr 2022 09:37:16 +0300 Subject: [PATCH] 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 --- .github/workflows/ci.yml | 5 +-- core/engine.go | 2 +- core/localizer.go | 77 ++++++++++++++++++++++++++++++++-------- core/localizer_test.go | 12 +++---- core/sentry.go | 2 +- 5 files changed, 74 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea56b65..97a31f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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) \ No newline at end of file diff --git a/core/engine.go b/core/engine.go index 3334d4f..531f0a1 100644 --- a/core/engine.go +++ b/core/engine.go @@ -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 } diff --git a/core/localizer.go b/core/localizer.go index 2c1cd9f..5dbfcf7 100644 --- a/core/localizer.go +++ b/core/localizer.go @@ -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 } } diff --git a/core/localizer_test.go b/core/localizer_test.go index ee627a1..57c5cc8 100644 --- a/core/localizer_test.go +++ b/core/localizer_test.go @@ -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")) } diff --git a/core/sentry.go b/core/sentry.go index c4c0540..3511c1c 100644 --- a/core/sentry.go +++ b/core/sentry.go @@ -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