Dmitriy Gnatenko преди 2 дни
родител
ревизия
ed238c3f7a

+ 1 - 1
Makefile

@@ -3,7 +3,7 @@ include .env
 GOOSE_DB_STRING = ${DB_USER}:${DB_PASSWORD}@tcp\(${DB_HOST}:${DB_PORT}\)/${DB_NAME}
 
 run:
-	cd cmd/app && go run main.go
+	cd cmd/app && go run main.go --config=./../../.env
 
 test:
 	go test ./...

+ 2 - 65
internal/fiber/fiber.go

@@ -1,20 +1,13 @@
 package fiber
 
 import (
-	"errors"
-	"html/template"
-	"strconv"
-	"time"
-
 	cache "git.dmitriygnatenko.ru/dima/go-common/cache/ttl_memory_cache"
 	"git.dmitriygnatenko.ru/dima/go-common/db"
 	"github.com/gofiber/fiber/v2"
-	"github.com/gofiber/template/html/v2"
 
 	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/repositories"
 	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/auth"
 	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/config"
-	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/i18n"
 )
 
 const (
@@ -40,10 +33,10 @@ func Init(sp ServiceProvider) (*fiber.App, error) {
 
 	initStatic(fiberApp)
 
-	initMiddleware(fiberApp, sp)
-
 	initMetrics(fiberApp, sp)
 
+	initMiddleware(fiberApp, sp)
+
 	initPublicHandlers(fiberApp, sp)
 
 	initAdminHandlers(fiberApp, sp)
@@ -59,59 +52,3 @@ func getConfig(sp ServiceProvider) fiber.Config {
 		ErrorHandler:          getErrorHandler(),
 	}
 }
-
-func getViewsEngine(sp ServiceProvider) *html.Engine {
-	engine := html.New(templatesPath, ".html")
-
-	// nolint:gocritic
-	engine.AddFunc("now", func() time.Time {
-		return time.Now()
-	})
-
-	// nolint:gosec
-	engine.AddFunc("noescape", func(str string) template.HTML {
-		return template.HTML(str)
-	})
-
-	engine.AddFunc("gridsep", func(i, l int) bool {
-		i++
-		return i%3 == 0 && i != l
-	})
-
-	engine.AddFunc("version", func() string {
-		return strconv.FormatUint(uint64(sp.ConfigService().StaticVersion()), 10)
-	})
-
-	return engine
-}
-
-func getErrorHandler() fiber.ErrorHandler {
-	return func(fctx *fiber.Ctx, err error) error {
-		lang := i18n.Language(fctx)
-		errCode := fiber.StatusInternalServerError
-		var e *fiber.Error
-		if errors.As(err, &e) {
-			errCode = e.Code
-		}
-
-		var renderData fiber.Map
-
-		if errCode == fiber.StatusNotFound {
-			renderData = fiber.Map{
-				"pageTitle": i18n.T(lang, "page_not_found_err_title"),
-				"code":      fiber.StatusNotFound,
-				"text":      i18n.T(lang, "page_not_found_err_desc"),
-			}
-		} else {
-			renderData = fiber.Map{
-				"pageTitle": i18n.T(lang, "internal_err_title"),
-				"code":      fiber.StatusInternalServerError,
-				"text":      i18n.T(lang, "internal_err_desc"),
-			}
-		}
-
-		renderData["headTitle"] = i18n.T(lang, "head_title")
-
-		return fctx.Render("error", renderData, "_layout")
-	}
-}

+ 0 - 4
internal/fiber/middleware.go

@@ -4,16 +4,12 @@ import (
 	"github.com/gofiber/fiber/v2"
 	"github.com/gofiber/fiber/v2/middleware/cors"
 	"github.com/gofiber/fiber/v2/middleware/recover"
-
-	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/middleware/language"
 )
 
 func initMiddleware(app *fiber.App, sp ServiceProvider) {
 	app.Use(cors.New(getCORSConfig(sp)))
 
 	app.Use(recover.New())
-
-	app.Use(language.New())
 }
 
 func getCORSConfig(sp ServiceProvider) cors.Config {

+ 20 - 19
internal/fiber/public_handlers.go

@@ -7,28 +7,29 @@ import (
 )
 
 func initPublicHandlers(app *fiber.App, sp ServiceProvider) {
-	app.Get(
-		"/",
-		handler.MainPageHandler(
-			sp.CacheService(),
-			sp.ArticleRepository(),
-		),
+	mainPageHandler := handler.MainPageHandler(
+		sp.CacheService(),
+		sp.ArticleRepository(),
 	)
 
-	app.Get(
-		"/tag/:tag", handler.TagHandler(
-			sp.CacheService(),
-			sp.ArticleRepository(),
-			sp.TagRepository(),
-		),
+	app.Get("/:lang<regex(^en$)>", mainPageHandler)
+	app.Get("/", mainPageHandler)
+
+	tagPageHandler := handler.TagHandler(
+		sp.CacheService(),
+		sp.ArticleRepository(),
+		sp.TagRepository(),
 	)
 
-	app.Get(
-		"/article/:article",
-		handler.ArticleHandler(
-			sp.CacheService(),
-			sp.ArticleRepository(),
-			sp.TagRepository(),
-		),
+	app.Get("/:lang<regex(^en$)/tag/:tag>", tagPageHandler)
+	app.Get("/tag/:tag", tagPageHandler)
+
+	articlePageHandler := handler.ArticleHandler(
+		sp.CacheService(),
+		sp.ArticleRepository(),
+		sp.TagRepository(),
 	)
+
+	app.Get("/:lang<regex(^en$)/article/:article", articlePageHandler)
+	app.Get("/article/:article", articlePageHandler)
 }

+ 95 - 0
internal/fiber/templates.go

@@ -0,0 +1,95 @@
+package fiber
+
+import (
+	"errors"
+	"html/template"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/gofiber/fiber/v2"
+	"github.com/gofiber/template/html/v2"
+
+	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/mapper"
+	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/i18n"
+)
+
+func Now() time.Time {
+	return time.Now()
+}
+
+func NoEscape(str string) template.HTML {
+	return template.HTML(str)
+}
+
+func Concat(parts ...string) string {
+	return strings.Join(parts, "")
+}
+
+func GridSep(i, l int) bool {
+	i++
+	return i%3 == 0 && i != l
+}
+
+func Version(sp ServiceProvider) func() string {
+	return func() string {
+		return strconv.FormatUint(uint64(sp.ConfigService().StaticVersion()), 10)
+	}
+}
+
+func Trans(lang i18n.Language, key string) string {
+	return i18n.T(lang, key)
+}
+
+func Link(lang i18n.Language, link string) string {
+	if lang == i18n.Default {
+		return link
+	}
+
+	return "/" + string(lang) + link
+}
+
+func getViewsEngine(sp ServiceProvider) *html.Engine {
+	engine := html.New(templatesPath, ".html")
+
+	engine.AddFunc("now", Now)
+	engine.AddFunc("noescape", NoEscape)
+	engine.AddFunc("concat", Concat)
+	engine.AddFunc("gridsep", GridSep)
+	engine.AddFunc("version", Version(sp))
+	engine.AddFunc("trans", Trans)
+	engine.AddFunc("link", Link)
+
+	return engine
+}
+
+func getErrorHandler() fiber.ErrorHandler {
+	return func(fctx *fiber.Ctx, err error) error {
+		lang := mapper.LanguageFromContext(fctx)
+		errCode := fiber.StatusInternalServerError
+		var e *fiber.Error
+		if errors.As(err, &e) {
+			errCode = e.Code
+		}
+
+		var renderData fiber.Map
+
+		if errCode == fiber.StatusNotFound {
+			renderData = fiber.Map{
+				"pageTitle": i18n.T(lang, "page_not_found_err_title"),
+				"code":      fiber.StatusNotFound,
+				"text":      i18n.T(lang, "page_not_found_err_desc"),
+			}
+		} else {
+			renderData = fiber.Map{
+				"pageTitle": i18n.T(lang, "internal_err_title"),
+				"code":      fiber.StatusInternalServerError,
+				"text":      i18n.T(lang, "internal_err_desc"),
+			}
+		}
+
+		renderData["headTitle"] = i18n.T(lang, "head_title")
+
+		return fctx.Render("error", renderData, "_layout")
+	}
+}

+ 15 - 5
internal/helpers/datetime/datetime.go

@@ -11,7 +11,7 @@ const (
 	dateTimeFormat = "2006-01-02"
 	yearFormat     = "2006"
 	monthFormat    = "01"
-	dateFormat     = "02"
+	formDayFormat  = "02"
 	dayFormat      = "2"
 )
 
@@ -19,14 +19,24 @@ func ParseDateTime(dateTime string) (time.Time, error) {
 	return time.Parse(dateTimeFormat, dateTime)
 }
 
-func FormatDateStr(lang i18n.Lang, date time.Time) string {
-	return date.Format(dayFormat) + " " + getMonthStr(lang, date.Format(monthFormat)) + " " + date.Format(yearFormat)
+func FormatDateStr(lang i18n.Language, date time.Time) string {
+
+	switch lang {
+	case i18n.En:
+		return getMonthStr(lang, date.Format(monthFormat)) + " " +
+			date.Format(dayFormat) + " " +
+			date.Format(yearFormat)
+	default:
+		return date.Format(dayFormat) + " " +
+			getMonthStr(lang, date.Format(monthFormat)) + " " +
+			date.Format(yearFormat)
+	}
 }
 
 func FormatDateForm(date time.Time) string {
-	return date.Format(yearFormat) + "-" + date.Format(monthFormat) + "-" + date.Format(dateFormat)
+	return date.Format(yearFormat) + "-" + date.Format(monthFormat) + "-" + date.Format(formDayFormat)
 }
 
-func getMonthStr(lang i18n.Lang, month string) string {
+func getMonthStr(lang i18n.Language, month string) string {
 	return i18n.T(lang, fmt.Sprintf("m%s", month))
 }

+ 9 - 23
internal/helpers/test/fiber.go

@@ -1,36 +1,22 @@
 package test
 
 import (
-	"html/template"
-	"strconv"
-	"time"
-
-	"github.com/brianvoe/gofakeit/v6"
 	"github.com/gofiber/fiber/v2"
 	"github.com/gofiber/template/html/v2"
+
+	app "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/fiber"
 )
 
 func GetFiberTestConfig() fiber.Config {
 	engine := html.New("./../../templates", ".html")
 
-	// nolint:gocritic
-	engine.AddFunc("now", func() time.Time {
-		return time.Now()
-	})
-
-	// nolint:gosec
-	engine.AddFunc("noescape", func(str string) template.HTML {
-		return template.HTML(str)
-	})
-
-	engine.AddFunc("gridsep", func(i, l int) bool {
-		i++
-		return i%3 == 0 && i != l
-	})
-
-	engine.AddFunc("version", func() string {
-		return strconv.Itoa(gofakeit.Number(1, 1000))
-	})
+	engine.AddFunc("now", app.Now)
+	engine.AddFunc("noescape", app.NoEscape)
+	engine.AddFunc("concat", app.Concat)
+	engine.AddFunc("gridsep", app.GridSep)
+	engine.AddFunc("version", 1)
+	engine.AddFunc("trans", app.Trans)
+	engine.AddFunc("link", app.Link)
 
 	return fiber.Config{
 		Views: engine,

+ 5 - 9
internal/mapper/article.go

@@ -3,17 +3,14 @@ package mapper
 import (
 	"database/sql"
 
-	"github.com/gofiber/fiber/v2"
-
 	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/dto"
 	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/helpers/datetime"
 	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/models"
 	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/i18n"
 )
 
-func ToArticleDTOList(fctx *fiber.Ctx, m []models.Article) []dto.Article {
+func ToArticlesList(lang i18n.Language, m []models.Article) []dto.Article {
 	res := make([]dto.Article, 0, len(m))
-	lang := i18n.Language(fctx)
 
 	for i := range m {
 		res = append(res, dto.Article{
@@ -31,7 +28,7 @@ func ToArticleDTOList(fctx *fiber.Ctx, m []models.Article) []dto.Article {
 	return res
 }
 
-func ToArticleDTO(fctx *fiber.Ctx, m models.Article) *dto.Article {
+func ToArticle(lang i18n.Language, m models.Article) *dto.Article {
 	return &dto.Article{
 		ID:              m.ID,
 		URL:             m.URL,
@@ -40,13 +37,12 @@ func ToArticleDTO(fctx *fiber.Ctx, m models.Article) *dto.Article {
 		Image:           m.Image.String,
 		MetaKeywords:    m.MetaKeywords.String,
 		MetaDescription: m.MetaDescription.String,
-		PublishTime:     datetime.FormatDateStr(i18n.Language(fctx), m.PublishTime),
+		PublishTime:     datetime.FormatDateStr(lang, m.PublishTime),
 	}
 }
 
-func ToArticlePreviewDTOList(fctx *fiber.Ctx, m []models.ArticlePreview) []dto.ArticlePreview {
+func ToArticlesPreview(lang i18n.Language, m []models.ArticlePreview) []dto.ArticlePreview {
 	res := make([]dto.ArticlePreview, 0, len(m))
-	lang := i18n.Language(fctx)
 
 	for i := range m {
 		res = append(res, dto.ArticlePreview{
@@ -83,7 +79,7 @@ func ToArticleForm(a models.Article, tags []models.Tag) *models.ArticleForm {
 	}
 }
 
-func ToArticle(f models.ArticleForm) (*models.Article, error) {
+func ToArticleModel(f models.ArticleForm) (*models.Article, error) {
 	var previewText, image, metaKeywords, metaDesc sql.NullString
 
 	if f.PreviewText != "" {

+ 50 - 0
internal/mapper/language.go

@@ -0,0 +1,50 @@
+package mapper
+
+import (
+	"github.com/gofiber/fiber/v2"
+
+	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/models"
+	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/i18n"
+)
+
+func ToLanguage(lang models.Language) i18n.Language {
+	switch lang {
+	case models.LangRu:
+		return i18n.Ru
+	case models.LangEn:
+		return i18n.En
+	default:
+		return ""
+	}
+}
+
+func ToLanguageModel(lang i18n.Language) models.Language {
+	switch lang {
+	case i18n.Ru:
+		return models.LangRu
+	case i18n.En:
+		return models.LangEn
+	default:
+		return 0
+	}
+}
+
+func ToLanguageFromString(lang string) i18n.Language {
+	switch lang {
+	case "en", "EN":
+		return i18n.En
+	case "ru", "RU":
+		return i18n.Ru
+	default:
+		return ""
+	}
+}
+
+func LanguageFromContext(c *fiber.Ctx) i18n.Language {
+	language := ToLanguageFromString(c.Params(i18n.LangParam))
+	if len(language) == 0 {
+		language = i18n.Default
+	}
+
+	return language
+}

+ 2 - 2
internal/mapper/tag.go

@@ -5,7 +5,7 @@ import (
 	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/models"
 )
 
-func ToTagDTOList(m []models.Tag) []dto.Tag {
+func ToTagsList(m []models.Tag) []dto.Tag {
 	res := make([]dto.Tag, 0, len(m))
 
 	for i := range m {
@@ -19,6 +19,6 @@ func ToTagDTOList(m []models.Tag) []dto.Tag {
 	return res
 }
 
-func ToTag(form models.TagForm) models.Tag {
+func ToTagModel(form models.TagForm) models.Tag {
 	return models.Tag(form)
 }

+ 0 - 15
internal/middleware/language/language.go

@@ -1,15 +0,0 @@
-package language
-
-import (
-	"github.com/gofiber/fiber/v2"
-
-	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/i18n"
-)
-
-func New() fiber.Handler {
-	return func(c *fiber.Ctx) (err error) {
-		c.Locals(i18n.CtxLanguageKey, i18n.Ru) // now only RU language
-
-		return c.Next()
-	}
-}

+ 13 - 5
internal/models/article.go

@@ -5,26 +5,34 @@ import (
 	"time"
 )
 
+type Language uint8
+
+const (
+	LangRu Language = 1
+	LangEn Language = 2
+)
+
 type ArticlePreview struct {
 	ID          uint64         `db:"id"`
 	URL         string         `db:"url"`
 	Title       string         `db:"title"`
-	PublishTime time.Time      `db:"publish_time"`
-	PreviewText sql.NullString `db:"preview_text"`
 	Image       sql.NullString `db:"image"`
+	PreviewText sql.NullString `db:"preview_text"`
+	PublishTime time.Time      `db:"publish_time"`
 }
 
 type Article struct {
 	ID              uint64         `db:"id"`
 	URL             string         `db:"url"`
 	Title           string         `db:"title"`
-	PublishTime     time.Time      `db:"publish_time"`
+	Image           sql.NullString `db:"image"`
 	Text            string         `db:"text"`
 	PreviewText     sql.NullString `db:"preview_text"`
-	IsActive        bool           `db:"is_active"`
-	Image           sql.NullString `db:"image"`
+	PublishTime     time.Time      `db:"publish_time"`
 	MetaKeywords    sql.NullString `db:"meta_keywords"`
 	MetaDescription sql.NullString `db:"meta_desc"`
+	IsActive        bool           `db:"is_active"`
+	Language        Language       `db:"language"`
 }
 
 type ArticleForm struct {

+ 6 - 3
internal/repositories/tag.go

@@ -19,16 +19,19 @@ func InitTagRepository(db DB) *TagRepository {
 	return &TagRepository{db: db}
 }
 
-func (t TagRepository) GetAllUsed(ctx context.Context) ([]models.Tag, error) {
+func (t TagRepository) GetAllUsed(
+	ctx context.Context,
+	lang models.Language,
+) ([]models.Tag, error) {
 	var res []models.Tag
 
 	q := "SELECT t.id, t.url, t.tag " +
 		"FROM " + articleTagTableName + " at, " + tagTableName + " t " +
 		"WHERE t.id = at.tag_id AND at.article_id IN " +
-		"(SELECT id FROM " + articleTableName + " " + "WHERE is_active = true) " +
+		"(SELECT id FROM " + articleTableName + " " + "WHERE is_active = true AND language = ?) " +
 		"GROUP BY t.id"
 
-	err := t.db.SelectContext(ctx, &res, q)
+	err := t.db.SelectContext(ctx, &res, q, lang)
 	if err != nil {
 		return nil, fmt.Errorf("select: %w", err)
 	}

+ 2 - 2
internal/services/handler/admin/article.go

@@ -128,7 +128,7 @@ func AddArticleHandler(
 			}
 
 			if res, _ := articleRepository.GetByURL(ctx, form.URL); res != nil {
-				validateErrors["ArticleForm.URL"] = i18n.T(lang, "err_article_exists")
+				validateErrors["ArticleForm.URL"] = i18n.T(lang, "admin_err_article_exists")
 			}
 
 			tagIDs := make([]uint64, 0, len(form.Tags))
@@ -257,7 +257,7 @@ func EditArticleHandler(
 
 			if res, _ := articleRepository.GetByURL(ctx, form.URL); res != nil {
 				if res.ID != id {
-					validateErrors["ArticleForm.URL"] = i18n.T(lang, "err_article_exists")
+					validateErrors["ArticleForm.URL"] = i18n.T(lang, "admin_err_article_exists")
 				}
 			}
 

+ 61 - 26
internal/services/handler/article.go

@@ -7,14 +7,17 @@ import (
 	"git.dmitriygnatenko.ru/dima/go-common/logger"
 	"github.com/gofiber/fiber/v2"
 
+	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/dto"
 	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/mapper"
 	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/i18n"
 )
 
 const (
-	ArticleCacheKey  = "article"
-	maxArticlesCount = 3
-	articleParam     = "article"
+	ArticleCacheKey        = "article"
+	RecentArticlesCacheKey = "recent-articles"
+	UsedTagsCacheKey       = "used-tags"
+	previewArticlesCount   = 3
+	articleParam           = "article"
 )
 
 func ArticleHandler(
@@ -23,21 +26,27 @@ func ArticleHandler(
 	tagRepository TagRepository,
 ) fiber.Handler {
 	return func(fctx *fiber.Ctx) error {
+		var renderData fiber.Map
 		ctx := fctx.Context()
 		url := fctx.Params(articleParam)
 		lang := i18n.Language(fctx)
 
-		renderData, found := cacheService.Get(ArticleCacheKey + string(lang) + url)
+		// article
+		cacheData, found := cacheService.Get(ArticleCacheKey + url)
+		if found {
+			if articleData, ok := cacheData.(fiber.Map); ok {
+				renderData = articleData
+			}
+		}
 
-		if !found {
-			article, err := articleRepository.GetByURL(ctx, url)
+		if renderData == nil {
+			article, err := articleRepository.GetByURL(ctx, url, mapper.ToLanguageModel(lang))
 			if err != nil {
 				if errors.Is(err, sql.ErrNoRows) {
 					return fiber.ErrNotFound
 				}
 
 				logger.Error(ctx, err.Error())
-
 				return err
 			}
 
@@ -45,38 +54,64 @@ func ArticleHandler(
 				return fiber.ErrNotFound
 			}
 
-			articleDTO := mapper.ToArticleDTO(fctx, *article)
+			articleDTO := mapper.ToArticle(fctx, *article)
+
+			renderData = fiber.Map{
+				"lang":            string(lang),
+				"headTitle":       i18n.T(lang, "head_title"),
+				"headDescription": articleDTO.MetaDescription,
+				"headKeywords":    articleDTO.MetaKeywords,
+				"pageTitle":       i18n.T(lang, "article_page_title", articleDTO.Title),
+				"article":         articleDTO,
+			}
+
+			cacheService.Set(ArticleCacheKey+url, renderData, nil)
+		}
+
+		// recent articles
+		cacheData, found = cacheService.Get(RecentArticlesCacheKey + string(lang))
+		if found {
+			if articlesData, ok := cacheData.([]dto.ArticlePreview); ok {
+				renderData["sidebarArticles"] = articlesData
+			}
+		}
+
+		if _, ok := renderData["sidebarArticles"]; !ok {
+			articles, err := articleRepository.GetAllPreview(ctx, mapper.ToLanguageModel(lang))
 
-			// All used tags
-			tags, err := tagRepository.GetAllUsed(ctx)
 			if err != nil {
 				logger.Error(ctx, err.Error())
 				return err
 			}
 
-			tagsDTO := mapper.ToTagDTOList(tags)
+			if len(articles) > previewArticlesCount {
+				articles = articles[:previewArticlesCount]
+			}
+
+			sidebarArticles := mapper.ToArticlesPreview(lang, articles)
+			cacheService.Set(RecentArticlesCacheKey+string(lang), sidebarArticles, nil)
+			renderData["sidebarArticles"] = sidebarArticles
+		}
+
+		// tags
+		cacheData, found = cacheService.Get(UsedTagsCacheKey + string(lang))
+		if found {
+			if tagsData, ok := cacheData.([]dto.Tag); ok {
+				renderData["sidebarTags"] = tagsData
+			}
+		}
 
-			// Last articles
-			articles, err := articleRepository.GetAllPreview(ctx)
+		if _, ok := renderData["sidebarTags"]; !ok {
+			tags, err := tagRepository.GetAllUsed(ctx, mapper.ToLanguageModel(lang))
 			if err != nil {
 				logger.Error(ctx, err.Error())
 				return err
 			}
-			if len(articles) > maxArticlesCount {
-				articles = articles[:maxArticlesCount]
-			}
 
-			renderData = fiber.Map{
-				"headTitle":       i18n.T(lang, "head_title"),
-				"headDescription": articleDTO.MetaDescription,
-				"headKeywords":    articleDTO.MetaKeywords,
-				"pageTitle":       i18n.T(lang, "article_page_title", articleDTO.Title),
-				"article":         articleDTO,
-				"sidebarArticles": mapper.ToArticlePreviewDTOList(fctx, articles),
-				"sidebarTags":     tagsDTO,
-			}
+			sidebarTags := mapper.ToTagsList(tags)
 
-			cacheService.Set(ArticleCacheKey+string(lang)+url, renderData, nil)
+			cacheService.Set(UsedTagsCacheKey+string(lang), sidebarTags, nil)
+			renderData["sidebarTags"] = sidebarTags
 		}
 
 		return fctx.Render("article", renderData, "_layout")

+ 17 - 20
internal/services/handler/article_test.go

@@ -39,6 +39,7 @@ func TestArticleHandler(t *testing.T) {
 			PreviewText: sql.NullString{Valid: true, String: gofakeit.Phrase()},
 			Image:       sql.NullString{Valid: true, String: gofakeit.URL()},
 			IsActive:    true,
+			Language:    models.LangRu,
 		}
 
 		notActiveArticle = models.Article{
@@ -49,6 +50,7 @@ func TestArticleHandler(t *testing.T) {
 			PublishTime: publishTime,
 			PreviewText: sql.NullString{Valid: true, String: gofakeit.Phrase()},
 			Image:       sql.NullString{Valid: true, String: gofakeit.URL()},
+			Language:    models.LangRu,
 		}
 
 		tags = []models.Tag{
@@ -121,20 +123,19 @@ func TestArticleHandler(t *testing.T) {
 				mock := mocks.NewCacheServiceMock(mc)
 				mock.GetMock.Return(nil, false)
 				mock.SetMock.Return()
-
 				return mock
 			},
 			tagMock: func(mc *minimock.Controller) TagRepository {
 				mock := mocks.NewTagRepositoryMock(mc)
 				mock.GetAllUsedMock.Return(tags, nil)
-
 				return mock
 			},
 			articleMock: func(mc *minimock.Controller) ArticleRepository {
 				mock := mocks.NewArticleRepositoryMock(mc)
 
-				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string, lang models.Language) {
 					assert.Equal(mc, articleURL, url)
+					assert.Equal(mc, models.LangRu, lang)
 				}).Return(&article, nil)
 
 				mock.GetAllPreviewMock.Return(previewArticles, nil)
@@ -153,18 +154,17 @@ func TestArticleHandler(t *testing.T) {
 			cacheMock: func(mc *minimock.Controller) CacheService {
 				mock := mocks.NewCacheServiceMock(mc)
 				mock.GetMock.Return(nil, false)
-
 				return mock
 			},
 			tagMock: func(mc *minimock.Controller) TagRepository {
 				mock := mocks.NewTagRepositoryMock(mc)
-
 				return mock
 			},
 			articleMock: func(mc *minimock.Controller) ArticleRepository {
 				mock := mocks.NewArticleRepositoryMock(mc)
-				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string, lang models.Language) {
 					assert.Equal(mc, articleURL, url)
+					assert.Equal(mc, models.LangRu, lang)
 				}).Return(nil, sql.ErrNoRows)
 
 				return mock
@@ -181,18 +181,16 @@ func TestArticleHandler(t *testing.T) {
 			cacheMock: func(mc *minimock.Controller) CacheService {
 				mock := mocks.NewCacheServiceMock(mc)
 				mock.GetMock.Return(nil, false)
-
 				return mock
 			},
 			tagMock: func(mc *minimock.Controller) TagRepository {
-				mock := mocks.NewTagRepositoryMock(mc)
-
-				return mock
+				return mocks.NewTagRepositoryMock(mc)
 			},
 			articleMock: func(mc *minimock.Controller) ArticleRepository {
 				mock := mocks.NewArticleRepositoryMock(mc)
-				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string, lang models.Language) {
 					assert.Equal(mc, articleURL, url)
+					assert.Equal(mc, models.LangRu, lang)
 				}).Return(nil, internalErr)
 
 				return mock
@@ -209,18 +207,16 @@ func TestArticleHandler(t *testing.T) {
 			cacheMock: func(mc *minimock.Controller) CacheService {
 				mock := mocks.NewCacheServiceMock(mc)
 				mock.GetMock.Return(nil, false)
-
 				return mock
 			},
 			tagMock: func(mc *minimock.Controller) TagRepository {
-				mock := mocks.NewTagRepositoryMock(mc)
-
-				return mock
+				return mocks.NewTagRepositoryMock(mc)
 			},
 			articleMock: func(mc *minimock.Controller) ArticleRepository {
 				mock := mocks.NewArticleRepositoryMock(mc)
-				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string, lang models.Language) {
 					assert.Equal(mc, articleURL, url)
+					assert.Equal(mc, models.LangRu, lang)
 				}).Return(&notActiveArticle, nil)
 
 				return mock
@@ -236,21 +232,21 @@ func TestArticleHandler(t *testing.T) {
 			err: nil,
 			cacheMock: func(mc *minimock.Controller) CacheService {
 				mock := mocks.NewCacheServiceMock(mc)
-				mock.GetMock.Return(nil, false)
+				//	mock.GetMock.Return(nil, false)
 
 				return mock
 			},
 			tagMock: func(mc *minimock.Controller) TagRepository {
 				mock := mocks.NewTagRepositoryMock(mc)
 				mock.GetAllUsedMock.Return(nil, internalErr)
-
 				return mock
 			},
 			articleMock: func(mc *minimock.Controller) ArticleRepository {
 				mock := mocks.NewArticleRepositoryMock(mc)
 
-				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string, lang models.Language) {
 					assert.Equal(mc, articleURL, url)
+					assert.Equal(mc, models.LangRu, lang)
 				}).Return(&article, nil)
 
 				return mock
@@ -279,8 +275,9 @@ func TestArticleHandler(t *testing.T) {
 			articleMock: func(mc *minimock.Controller) ArticleRepository {
 				mock := mocks.NewArticleRepositoryMock(mc)
 
-				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string, lang models.Language) {
 					assert.Equal(mc, articleURL, url)
+					assert.Equal(mc, models.LangRu, lang)
 				}).Return(&article, nil)
 
 				mock.GetAllPreviewMock.Return(nil, internalErr)

+ 11 - 10
internal/services/handler/main_page.go

@@ -23,13 +23,13 @@ type (
 	}
 
 	ArticleRepository interface {
-		GetByURL(ctx context.Context, url string) (*models.Article, error)
-		GetAllPreview(ctx context.Context) ([]models.ArticlePreview, error)
-		GetPreviewByTagID(ctx context.Context, tagID uint64) ([]models.ArticlePreview, error)
+		GetByURL(ctx context.Context, url string, lang models.Language) (*models.Article, error)
+		GetAllPreview(ctx context.Context, lang models.Language) ([]models.ArticlePreview, error)
+		GetPreviewByTagID(ctx context.Context, tagID uint64, lang models.Language) ([]models.ArticlePreview, error)
 	}
 
 	TagRepository interface {
-		GetAllUsed(ctx context.Context) ([]models.Tag, error)
+		GetAllUsed(ctx context.Context, lang models.Language) ([]models.Tag, error)
 		GetByURL(ctx context.Context, tag string) (*models.Tag, error)
 	}
 )
@@ -42,26 +42,27 @@ func MainPageHandler(
 ) fiber.Handler {
 	return func(fctx *fiber.Ctx) error {
 		ctx := fctx.Context()
-		lang := i18n.Language(fctx)
-
-		renderData, found := cacheService.Get(allPreviewArticlesCacheKey + string(lang))
+		lang := mapper.LanguageFromContext(fctx)
+		cacheKey := allPreviewArticlesCacheKey + string(lang)
 
+		renderData, found := cacheService.Get(cacheKey)
 		if !found {
-			articles, err := articleRepository.GetAllPreview(fctx.Context())
+			articles, err := articleRepository.GetAllPreview(ctx, mapper.ToLanguageModel(lang))
 			if err != nil {
 				logger.Error(ctx, err.Error())
 				return err
 			}
 
 			renderData = fiber.Map{
+				"lang":            lang,
 				"headTitle":       i18n.T(lang, "head_title"),
 				"pageTitle":       i18n.T(lang, "main_page_title"),
 				"headDescription": i18n.T(lang, "main_page_desc"),
 				"headKeywords":    i18n.T(lang, "main_page_keywords"),
-				"articles":        mapper.ToArticlePreviewDTOList(fctx, articles),
+				"articles":        mapper.ToArticlesPreview(lang, articles),
 			}
 
-			cacheService.Set(allPreviewArticlesCacheKey+string(lang), renderData, nil)
+			cacheService.Set(cacheKey, renderData, nil)
 		}
 
 		return fctx.Render("index", renderData, "_layout")

+ 0 - 4
internal/services/handler/main_page_test.go

@@ -76,13 +76,11 @@ func TestMainPageHandler(t *testing.T) {
 				mock := mocks.NewCacheServiceMock(mc)
 				mock.GetMock.Return(nil, false)
 				mock.SetMock.Return()
-
 				return mock
 			},
 			articleMock: func(mc *minimock.Controller) ArticleRepository {
 				mock := mocks.NewArticleRepositoryMock(mc)
 				mock.GetAllPreviewMock.Return(previewArticles, nil)
-
 				return mock
 			},
 		},
@@ -97,13 +95,11 @@ func TestMainPageHandler(t *testing.T) {
 			cacheMock: func(mc *minimock.Controller) CacheService {
 				mock := mocks.NewCacheServiceMock(mc)
 				mock.GetMock.Return(nil, false)
-
 				return mock
 			},
 			articleMock: func(mc *minimock.Controller) ArticleRepository {
 				mock := mocks.NewArticleRepositoryMock(mc)
 				mock.GetAllPreviewMock.Return(nil, internalErr)
-
 				return mock
 			},
 		},

+ 132 - 48
internal/services/handler/mocks/article_repository_minimock.go

@@ -19,20 +19,20 @@ type ArticleRepositoryMock struct {
 	t          minimock.Tester
 	finishOnce sync.Once
 
-	funcGetAllPreview          func(ctx context.Context) (aa1 []models.ArticlePreview, err error)
-	inspectFuncGetAllPreview   func(ctx context.Context)
+	funcGetAllPreview          func(ctx context.Context, lang models.Language) (aa1 []models.ArticlePreview, err error)
+	inspectFuncGetAllPreview   func(ctx context.Context, lang models.Language)
 	afterGetAllPreviewCounter  uint64
 	beforeGetAllPreviewCounter uint64
 	GetAllPreviewMock          mArticleRepositoryMockGetAllPreview
 
-	funcGetByURL          func(ctx context.Context, url string) (ap1 *models.Article, err error)
-	inspectFuncGetByURL   func(ctx context.Context, url string)
+	funcGetByURL          func(ctx context.Context, url string, lang models.Language) (ap1 *models.Article, err error)
+	inspectFuncGetByURL   func(ctx context.Context, url string, lang models.Language)
 	afterGetByURLCounter  uint64
 	beforeGetByURLCounter uint64
 	GetByURLMock          mArticleRepositoryMockGetByURL
 
-	funcGetPreviewByTagID          func(ctx context.Context, tagID uint64) (aa1 []models.ArticlePreview, err error)
-	inspectFuncGetPreviewByTagID   func(ctx context.Context, tagID uint64)
+	funcGetPreviewByTagID          func(ctx context.Context, tagID uint64, lang models.Language) (aa1 []models.ArticlePreview, err error)
+	inspectFuncGetPreviewByTagID   func(ctx context.Context, tagID uint64, lang models.Language)
 	afterGetPreviewByTagIDCounter  uint64
 	beforeGetPreviewByTagIDCounter uint64
 	GetPreviewByTagIDMock          mArticleRepositoryMockGetPreviewByTagID
@@ -83,12 +83,14 @@ type ArticleRepositoryMockGetAllPreviewExpectation struct {
 
 // ArticleRepositoryMockGetAllPreviewParams contains parameters of the ArticleRepository.GetAllPreview
 type ArticleRepositoryMockGetAllPreviewParams struct {
-	ctx context.Context
+	ctx  context.Context
+	lang models.Language
 }
 
 // ArticleRepositoryMockGetAllPreviewParamPtrs contains pointers to parameters of the ArticleRepository.GetAllPreview
 type ArticleRepositoryMockGetAllPreviewParamPtrs struct {
-	ctx *context.Context
+	ctx  *context.Context
+	lang *models.Language
 }
 
 // ArticleRepositoryMockGetAllPreviewResults contains results of the ArticleRepository.GetAllPreview
@@ -108,7 +110,7 @@ func (mmGetAllPreview *mArticleRepositoryMockGetAllPreview) Optional() *mArticle
 }
 
 // Expect sets up expected params for ArticleRepository.GetAllPreview
-func (mmGetAllPreview *mArticleRepositoryMockGetAllPreview) Expect(ctx context.Context) *mArticleRepositoryMockGetAllPreview {
+func (mmGetAllPreview *mArticleRepositoryMockGetAllPreview) Expect(ctx context.Context, lang models.Language) *mArticleRepositoryMockGetAllPreview {
 	if mmGetAllPreview.mock.funcGetAllPreview != nil {
 		mmGetAllPreview.mock.t.Fatalf("ArticleRepositoryMock.GetAllPreview mock is already set by Set")
 	}
@@ -121,7 +123,7 @@ func (mmGetAllPreview *mArticleRepositoryMockGetAllPreview) Expect(ctx context.C
 		mmGetAllPreview.mock.t.Fatalf("ArticleRepositoryMock.GetAllPreview mock is already set by ExpectParams functions")
 	}
 
-	mmGetAllPreview.defaultExpectation.params = &ArticleRepositoryMockGetAllPreviewParams{ctx}
+	mmGetAllPreview.defaultExpectation.params = &ArticleRepositoryMockGetAllPreviewParams{ctx, lang}
 	for _, e := range mmGetAllPreview.expectations {
 		if minimock.Equal(e.params, mmGetAllPreview.defaultExpectation.params) {
 			mmGetAllPreview.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmGetAllPreview.defaultExpectation.params)
@@ -153,8 +155,30 @@ func (mmGetAllPreview *mArticleRepositoryMockGetAllPreview) ExpectCtxParam1(ctx
 	return mmGetAllPreview
 }
 
+// ExpectLangParam2 sets up expected param lang for ArticleRepository.GetAllPreview
+func (mmGetAllPreview *mArticleRepositoryMockGetAllPreview) ExpectLangParam2(lang models.Language) *mArticleRepositoryMockGetAllPreview {
+	if mmGetAllPreview.mock.funcGetAllPreview != nil {
+		mmGetAllPreview.mock.t.Fatalf("ArticleRepositoryMock.GetAllPreview mock is already set by Set")
+	}
+
+	if mmGetAllPreview.defaultExpectation == nil {
+		mmGetAllPreview.defaultExpectation = &ArticleRepositoryMockGetAllPreviewExpectation{}
+	}
+
+	if mmGetAllPreview.defaultExpectation.params != nil {
+		mmGetAllPreview.mock.t.Fatalf("ArticleRepositoryMock.GetAllPreview mock is already set by Expect")
+	}
+
+	if mmGetAllPreview.defaultExpectation.paramPtrs == nil {
+		mmGetAllPreview.defaultExpectation.paramPtrs = &ArticleRepositoryMockGetAllPreviewParamPtrs{}
+	}
+	mmGetAllPreview.defaultExpectation.paramPtrs.lang = &lang
+
+	return mmGetAllPreview
+}
+
 // Inspect accepts an inspector function that has same arguments as the ArticleRepository.GetAllPreview
-func (mmGetAllPreview *mArticleRepositoryMockGetAllPreview) Inspect(f func(ctx context.Context)) *mArticleRepositoryMockGetAllPreview {
+func (mmGetAllPreview *mArticleRepositoryMockGetAllPreview) Inspect(f func(ctx context.Context, lang models.Language)) *mArticleRepositoryMockGetAllPreview {
 	if mmGetAllPreview.mock.inspectFuncGetAllPreview != nil {
 		mmGetAllPreview.mock.t.Fatalf("Inspect function is already set for ArticleRepositoryMock.GetAllPreview")
 	}
@@ -178,7 +202,7 @@ func (mmGetAllPreview *mArticleRepositoryMockGetAllPreview) Return(aa1 []models.
 }
 
 // Set uses given function f to mock the ArticleRepository.GetAllPreview method
-func (mmGetAllPreview *mArticleRepositoryMockGetAllPreview) Set(f func(ctx context.Context) (aa1 []models.ArticlePreview, err error)) *ArticleRepositoryMock {
+func (mmGetAllPreview *mArticleRepositoryMockGetAllPreview) Set(f func(ctx context.Context, lang models.Language) (aa1 []models.ArticlePreview, err error)) *ArticleRepositoryMock {
 	if mmGetAllPreview.defaultExpectation != nil {
 		mmGetAllPreview.mock.t.Fatalf("Default expectation is already set for the ArticleRepository.GetAllPreview method")
 	}
@@ -193,14 +217,14 @@ func (mmGetAllPreview *mArticleRepositoryMockGetAllPreview) Set(f func(ctx conte
 
 // When sets expectation for the ArticleRepository.GetAllPreview which will trigger the result defined by the following
 // Then helper
-func (mmGetAllPreview *mArticleRepositoryMockGetAllPreview) When(ctx context.Context) *ArticleRepositoryMockGetAllPreviewExpectation {
+func (mmGetAllPreview *mArticleRepositoryMockGetAllPreview) When(ctx context.Context, lang models.Language) *ArticleRepositoryMockGetAllPreviewExpectation {
 	if mmGetAllPreview.mock.funcGetAllPreview != nil {
 		mmGetAllPreview.mock.t.Fatalf("ArticleRepositoryMock.GetAllPreview mock is already set by Set")
 	}
 
 	expectation := &ArticleRepositoryMockGetAllPreviewExpectation{
 		mock:   mmGetAllPreview.mock,
-		params: &ArticleRepositoryMockGetAllPreviewParams{ctx},
+		params: &ArticleRepositoryMockGetAllPreviewParams{ctx, lang},
 	}
 	mmGetAllPreview.expectations = append(mmGetAllPreview.expectations, expectation)
 	return expectation
@@ -233,15 +257,15 @@ func (mmGetAllPreview *mArticleRepositoryMockGetAllPreview) invocationsDone() bo
 }
 
 // GetAllPreview implements handler.ArticleRepository
-func (mmGetAllPreview *ArticleRepositoryMock) GetAllPreview(ctx context.Context) (aa1 []models.ArticlePreview, err error) {
+func (mmGetAllPreview *ArticleRepositoryMock) GetAllPreview(ctx context.Context, lang models.Language) (aa1 []models.ArticlePreview, err error) {
 	mm_atomic.AddUint64(&mmGetAllPreview.beforeGetAllPreviewCounter, 1)
 	defer mm_atomic.AddUint64(&mmGetAllPreview.afterGetAllPreviewCounter, 1)
 
 	if mmGetAllPreview.inspectFuncGetAllPreview != nil {
-		mmGetAllPreview.inspectFuncGetAllPreview(ctx)
+		mmGetAllPreview.inspectFuncGetAllPreview(ctx, lang)
 	}
 
-	mm_params := ArticleRepositoryMockGetAllPreviewParams{ctx}
+	mm_params := ArticleRepositoryMockGetAllPreviewParams{ctx, lang}
 
 	// Record call args
 	mmGetAllPreview.GetAllPreviewMock.mutex.Lock()
@@ -260,7 +284,7 @@ func (mmGetAllPreview *ArticleRepositoryMock) GetAllPreview(ctx context.Context)
 		mm_want := mmGetAllPreview.GetAllPreviewMock.defaultExpectation.params
 		mm_want_ptrs := mmGetAllPreview.GetAllPreviewMock.defaultExpectation.paramPtrs
 
-		mm_got := ArticleRepositoryMockGetAllPreviewParams{ctx}
+		mm_got := ArticleRepositoryMockGetAllPreviewParams{ctx, lang}
 
 		if mm_want_ptrs != nil {
 
@@ -268,6 +292,10 @@ func (mmGetAllPreview *ArticleRepositoryMock) GetAllPreview(ctx context.Context)
 				mmGetAllPreview.t.Errorf("ArticleRepositoryMock.GetAllPreview got unexpected parameter ctx, want: %#v, got: %#v%s\n", *mm_want_ptrs.ctx, mm_got.ctx, minimock.Diff(*mm_want_ptrs.ctx, mm_got.ctx))
 			}
 
+			if mm_want_ptrs.lang != nil && !minimock.Equal(*mm_want_ptrs.lang, mm_got.lang) {
+				mmGetAllPreview.t.Errorf("ArticleRepositoryMock.GetAllPreview got unexpected parameter lang, want: %#v, got: %#v%s\n", *mm_want_ptrs.lang, mm_got.lang, minimock.Diff(*mm_want_ptrs.lang, mm_got.lang))
+			}
+
 		} else if mm_want != nil && !minimock.Equal(*mm_want, mm_got) {
 			mmGetAllPreview.t.Errorf("ArticleRepositoryMock.GetAllPreview got unexpected parameters, want: %#v, got: %#v%s\n", *mm_want, mm_got, minimock.Diff(*mm_want, mm_got))
 		}
@@ -279,9 +307,9 @@ func (mmGetAllPreview *ArticleRepositoryMock) GetAllPreview(ctx context.Context)
 		return (*mm_results).aa1, (*mm_results).err
 	}
 	if mmGetAllPreview.funcGetAllPreview != nil {
-		return mmGetAllPreview.funcGetAllPreview(ctx)
+		return mmGetAllPreview.funcGetAllPreview(ctx, lang)
 	}
-	mmGetAllPreview.t.Fatalf("Unexpected call to ArticleRepositoryMock.GetAllPreview. %v", ctx)
+	mmGetAllPreview.t.Fatalf("Unexpected call to ArticleRepositoryMock.GetAllPreview. %v %v", ctx, lang)
 	return
 }
 
@@ -376,14 +404,16 @@ type ArticleRepositoryMockGetByURLExpectation struct {
 
 // ArticleRepositoryMockGetByURLParams contains parameters of the ArticleRepository.GetByURL
 type ArticleRepositoryMockGetByURLParams struct {
-	ctx context.Context
-	url string
+	ctx  context.Context
+	url  string
+	lang models.Language
 }
 
 // ArticleRepositoryMockGetByURLParamPtrs contains pointers to parameters of the ArticleRepository.GetByURL
 type ArticleRepositoryMockGetByURLParamPtrs struct {
-	ctx *context.Context
-	url *string
+	ctx  *context.Context
+	url  *string
+	lang *models.Language
 }
 
 // ArticleRepositoryMockGetByURLResults contains results of the ArticleRepository.GetByURL
@@ -403,7 +433,7 @@ func (mmGetByURL *mArticleRepositoryMockGetByURL) Optional() *mArticleRepository
 }
 
 // Expect sets up expected params for ArticleRepository.GetByURL
-func (mmGetByURL *mArticleRepositoryMockGetByURL) Expect(ctx context.Context, url string) *mArticleRepositoryMockGetByURL {
+func (mmGetByURL *mArticleRepositoryMockGetByURL) Expect(ctx context.Context, url string, lang models.Language) *mArticleRepositoryMockGetByURL {
 	if mmGetByURL.mock.funcGetByURL != nil {
 		mmGetByURL.mock.t.Fatalf("ArticleRepositoryMock.GetByURL mock is already set by Set")
 	}
@@ -416,7 +446,7 @@ func (mmGetByURL *mArticleRepositoryMockGetByURL) Expect(ctx context.Context, ur
 		mmGetByURL.mock.t.Fatalf("ArticleRepositoryMock.GetByURL mock is already set by ExpectParams functions")
 	}
 
-	mmGetByURL.defaultExpectation.params = &ArticleRepositoryMockGetByURLParams{ctx, url}
+	mmGetByURL.defaultExpectation.params = &ArticleRepositoryMockGetByURLParams{ctx, url, lang}
 	for _, e := range mmGetByURL.expectations {
 		if minimock.Equal(e.params, mmGetByURL.defaultExpectation.params) {
 			mmGetByURL.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmGetByURL.defaultExpectation.params)
@@ -470,8 +500,30 @@ func (mmGetByURL *mArticleRepositoryMockGetByURL) ExpectUrlParam2(url string) *m
 	return mmGetByURL
 }
 
+// ExpectLangParam3 sets up expected param lang for ArticleRepository.GetByURL
+func (mmGetByURL *mArticleRepositoryMockGetByURL) ExpectLangParam3(lang models.Language) *mArticleRepositoryMockGetByURL {
+	if mmGetByURL.mock.funcGetByURL != nil {
+		mmGetByURL.mock.t.Fatalf("ArticleRepositoryMock.GetByURL mock is already set by Set")
+	}
+
+	if mmGetByURL.defaultExpectation == nil {
+		mmGetByURL.defaultExpectation = &ArticleRepositoryMockGetByURLExpectation{}
+	}
+
+	if mmGetByURL.defaultExpectation.params != nil {
+		mmGetByURL.mock.t.Fatalf("ArticleRepositoryMock.GetByURL mock is already set by Expect")
+	}
+
+	if mmGetByURL.defaultExpectation.paramPtrs == nil {
+		mmGetByURL.defaultExpectation.paramPtrs = &ArticleRepositoryMockGetByURLParamPtrs{}
+	}
+	mmGetByURL.defaultExpectation.paramPtrs.lang = &lang
+
+	return mmGetByURL
+}
+
 // Inspect accepts an inspector function that has same arguments as the ArticleRepository.GetByURL
-func (mmGetByURL *mArticleRepositoryMockGetByURL) Inspect(f func(ctx context.Context, url string)) *mArticleRepositoryMockGetByURL {
+func (mmGetByURL *mArticleRepositoryMockGetByURL) Inspect(f func(ctx context.Context, url string, lang models.Language)) *mArticleRepositoryMockGetByURL {
 	if mmGetByURL.mock.inspectFuncGetByURL != nil {
 		mmGetByURL.mock.t.Fatalf("Inspect function is already set for ArticleRepositoryMock.GetByURL")
 	}
@@ -495,7 +547,7 @@ func (mmGetByURL *mArticleRepositoryMockGetByURL) Return(ap1 *models.Article, er
 }
 
 // Set uses given function f to mock the ArticleRepository.GetByURL method
-func (mmGetByURL *mArticleRepositoryMockGetByURL) Set(f func(ctx context.Context, url string) (ap1 *models.Article, err error)) *ArticleRepositoryMock {
+func (mmGetByURL *mArticleRepositoryMockGetByURL) Set(f func(ctx context.Context, url string, lang models.Language) (ap1 *models.Article, err error)) *ArticleRepositoryMock {
 	if mmGetByURL.defaultExpectation != nil {
 		mmGetByURL.mock.t.Fatalf("Default expectation is already set for the ArticleRepository.GetByURL method")
 	}
@@ -510,14 +562,14 @@ func (mmGetByURL *mArticleRepositoryMockGetByURL) Set(f func(ctx context.Context
 
 // When sets expectation for the ArticleRepository.GetByURL which will trigger the result defined by the following
 // Then helper
-func (mmGetByURL *mArticleRepositoryMockGetByURL) When(ctx context.Context, url string) *ArticleRepositoryMockGetByURLExpectation {
+func (mmGetByURL *mArticleRepositoryMockGetByURL) When(ctx context.Context, url string, lang models.Language) *ArticleRepositoryMockGetByURLExpectation {
 	if mmGetByURL.mock.funcGetByURL != nil {
 		mmGetByURL.mock.t.Fatalf("ArticleRepositoryMock.GetByURL mock is already set by Set")
 	}
 
 	expectation := &ArticleRepositoryMockGetByURLExpectation{
 		mock:   mmGetByURL.mock,
-		params: &ArticleRepositoryMockGetByURLParams{ctx, url},
+		params: &ArticleRepositoryMockGetByURLParams{ctx, url, lang},
 	}
 	mmGetByURL.expectations = append(mmGetByURL.expectations, expectation)
 	return expectation
@@ -550,15 +602,15 @@ func (mmGetByURL *mArticleRepositoryMockGetByURL) invocationsDone() bool {
 }
 
 // GetByURL implements handler.ArticleRepository
-func (mmGetByURL *ArticleRepositoryMock) GetByURL(ctx context.Context, url string) (ap1 *models.Article, err error) {
+func (mmGetByURL *ArticleRepositoryMock) GetByURL(ctx context.Context, url string, lang models.Language) (ap1 *models.Article, err error) {
 	mm_atomic.AddUint64(&mmGetByURL.beforeGetByURLCounter, 1)
 	defer mm_atomic.AddUint64(&mmGetByURL.afterGetByURLCounter, 1)
 
 	if mmGetByURL.inspectFuncGetByURL != nil {
-		mmGetByURL.inspectFuncGetByURL(ctx, url)
+		mmGetByURL.inspectFuncGetByURL(ctx, url, lang)
 	}
 
-	mm_params := ArticleRepositoryMockGetByURLParams{ctx, url}
+	mm_params := ArticleRepositoryMockGetByURLParams{ctx, url, lang}
 
 	// Record call args
 	mmGetByURL.GetByURLMock.mutex.Lock()
@@ -577,7 +629,7 @@ func (mmGetByURL *ArticleRepositoryMock) GetByURL(ctx context.Context, url strin
 		mm_want := mmGetByURL.GetByURLMock.defaultExpectation.params
 		mm_want_ptrs := mmGetByURL.GetByURLMock.defaultExpectation.paramPtrs
 
-		mm_got := ArticleRepositoryMockGetByURLParams{ctx, url}
+		mm_got := ArticleRepositoryMockGetByURLParams{ctx, url, lang}
 
 		if mm_want_ptrs != nil {
 
@@ -589,6 +641,10 @@ func (mmGetByURL *ArticleRepositoryMock) GetByURL(ctx context.Context, url strin
 				mmGetByURL.t.Errorf("ArticleRepositoryMock.GetByURL got unexpected parameter url, want: %#v, got: %#v%s\n", *mm_want_ptrs.url, mm_got.url, minimock.Diff(*mm_want_ptrs.url, mm_got.url))
 			}
 
+			if mm_want_ptrs.lang != nil && !minimock.Equal(*mm_want_ptrs.lang, mm_got.lang) {
+				mmGetByURL.t.Errorf("ArticleRepositoryMock.GetByURL got unexpected parameter lang, want: %#v, got: %#v%s\n", *mm_want_ptrs.lang, mm_got.lang, minimock.Diff(*mm_want_ptrs.lang, mm_got.lang))
+			}
+
 		} else if mm_want != nil && !minimock.Equal(*mm_want, mm_got) {
 			mmGetByURL.t.Errorf("ArticleRepositoryMock.GetByURL got unexpected parameters, want: %#v, got: %#v%s\n", *mm_want, mm_got, minimock.Diff(*mm_want, mm_got))
 		}
@@ -600,9 +656,9 @@ func (mmGetByURL *ArticleRepositoryMock) GetByURL(ctx context.Context, url strin
 		return (*mm_results).ap1, (*mm_results).err
 	}
 	if mmGetByURL.funcGetByURL != nil {
-		return mmGetByURL.funcGetByURL(ctx, url)
+		return mmGetByURL.funcGetByURL(ctx, url, lang)
 	}
-	mmGetByURL.t.Fatalf("Unexpected call to ArticleRepositoryMock.GetByURL. %v %v", ctx, url)
+	mmGetByURL.t.Fatalf("Unexpected call to ArticleRepositoryMock.GetByURL. %v %v %v", ctx, url, lang)
 	return
 }
 
@@ -699,12 +755,14 @@ type ArticleRepositoryMockGetPreviewByTagIDExpectation struct {
 type ArticleRepositoryMockGetPreviewByTagIDParams struct {
 	ctx   context.Context
 	tagID uint64
+	lang  models.Language
 }
 
 // ArticleRepositoryMockGetPreviewByTagIDParamPtrs contains pointers to parameters of the ArticleRepository.GetPreviewByTagID
 type ArticleRepositoryMockGetPreviewByTagIDParamPtrs struct {
 	ctx   *context.Context
 	tagID *uint64
+	lang  *models.Language
 }
 
 // ArticleRepositoryMockGetPreviewByTagIDResults contains results of the ArticleRepository.GetPreviewByTagID
@@ -724,7 +782,7 @@ func (mmGetPreviewByTagID *mArticleRepositoryMockGetPreviewByTagID) Optional() *
 }
 
 // Expect sets up expected params for ArticleRepository.GetPreviewByTagID
-func (mmGetPreviewByTagID *mArticleRepositoryMockGetPreviewByTagID) Expect(ctx context.Context, tagID uint64) *mArticleRepositoryMockGetPreviewByTagID {
+func (mmGetPreviewByTagID *mArticleRepositoryMockGetPreviewByTagID) Expect(ctx context.Context, tagID uint64, lang models.Language) *mArticleRepositoryMockGetPreviewByTagID {
 	if mmGetPreviewByTagID.mock.funcGetPreviewByTagID != nil {
 		mmGetPreviewByTagID.mock.t.Fatalf("ArticleRepositoryMock.GetPreviewByTagID mock is already set by Set")
 	}
@@ -737,7 +795,7 @@ func (mmGetPreviewByTagID *mArticleRepositoryMockGetPreviewByTagID) Expect(ctx c
 		mmGetPreviewByTagID.mock.t.Fatalf("ArticleRepositoryMock.GetPreviewByTagID mock is already set by ExpectParams functions")
 	}
 
-	mmGetPreviewByTagID.defaultExpectation.params = &ArticleRepositoryMockGetPreviewByTagIDParams{ctx, tagID}
+	mmGetPreviewByTagID.defaultExpectation.params = &ArticleRepositoryMockGetPreviewByTagIDParams{ctx, tagID, lang}
 	for _, e := range mmGetPreviewByTagID.expectations {
 		if minimock.Equal(e.params, mmGetPreviewByTagID.defaultExpectation.params) {
 			mmGetPreviewByTagID.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmGetPreviewByTagID.defaultExpectation.params)
@@ -791,8 +849,30 @@ func (mmGetPreviewByTagID *mArticleRepositoryMockGetPreviewByTagID) ExpectTagIDP
 	return mmGetPreviewByTagID
 }
 
+// ExpectLangParam3 sets up expected param lang for ArticleRepository.GetPreviewByTagID
+func (mmGetPreviewByTagID *mArticleRepositoryMockGetPreviewByTagID) ExpectLangParam3(lang models.Language) *mArticleRepositoryMockGetPreviewByTagID {
+	if mmGetPreviewByTagID.mock.funcGetPreviewByTagID != nil {
+		mmGetPreviewByTagID.mock.t.Fatalf("ArticleRepositoryMock.GetPreviewByTagID mock is already set by Set")
+	}
+
+	if mmGetPreviewByTagID.defaultExpectation == nil {
+		mmGetPreviewByTagID.defaultExpectation = &ArticleRepositoryMockGetPreviewByTagIDExpectation{}
+	}
+
+	if mmGetPreviewByTagID.defaultExpectation.params != nil {
+		mmGetPreviewByTagID.mock.t.Fatalf("ArticleRepositoryMock.GetPreviewByTagID mock is already set by Expect")
+	}
+
+	if mmGetPreviewByTagID.defaultExpectation.paramPtrs == nil {
+		mmGetPreviewByTagID.defaultExpectation.paramPtrs = &ArticleRepositoryMockGetPreviewByTagIDParamPtrs{}
+	}
+	mmGetPreviewByTagID.defaultExpectation.paramPtrs.lang = &lang
+
+	return mmGetPreviewByTagID
+}
+
 // Inspect accepts an inspector function that has same arguments as the ArticleRepository.GetPreviewByTagID
-func (mmGetPreviewByTagID *mArticleRepositoryMockGetPreviewByTagID) Inspect(f func(ctx context.Context, tagID uint64)) *mArticleRepositoryMockGetPreviewByTagID {
+func (mmGetPreviewByTagID *mArticleRepositoryMockGetPreviewByTagID) Inspect(f func(ctx context.Context, tagID uint64, lang models.Language)) *mArticleRepositoryMockGetPreviewByTagID {
 	if mmGetPreviewByTagID.mock.inspectFuncGetPreviewByTagID != nil {
 		mmGetPreviewByTagID.mock.t.Fatalf("Inspect function is already set for ArticleRepositoryMock.GetPreviewByTagID")
 	}
@@ -816,7 +896,7 @@ func (mmGetPreviewByTagID *mArticleRepositoryMockGetPreviewByTagID) Return(aa1 [
 }
 
 // Set uses given function f to mock the ArticleRepository.GetPreviewByTagID method
-func (mmGetPreviewByTagID *mArticleRepositoryMockGetPreviewByTagID) Set(f func(ctx context.Context, tagID uint64) (aa1 []models.ArticlePreview, err error)) *ArticleRepositoryMock {
+func (mmGetPreviewByTagID *mArticleRepositoryMockGetPreviewByTagID) Set(f func(ctx context.Context, tagID uint64, lang models.Language) (aa1 []models.ArticlePreview, err error)) *ArticleRepositoryMock {
 	if mmGetPreviewByTagID.defaultExpectation != nil {
 		mmGetPreviewByTagID.mock.t.Fatalf("Default expectation is already set for the ArticleRepository.GetPreviewByTagID method")
 	}
@@ -831,14 +911,14 @@ func (mmGetPreviewByTagID *mArticleRepositoryMockGetPreviewByTagID) Set(f func(c
 
 // When sets expectation for the ArticleRepository.GetPreviewByTagID which will trigger the result defined by the following
 // Then helper
-func (mmGetPreviewByTagID *mArticleRepositoryMockGetPreviewByTagID) When(ctx context.Context, tagID uint64) *ArticleRepositoryMockGetPreviewByTagIDExpectation {
+func (mmGetPreviewByTagID *mArticleRepositoryMockGetPreviewByTagID) When(ctx context.Context, tagID uint64, lang models.Language) *ArticleRepositoryMockGetPreviewByTagIDExpectation {
 	if mmGetPreviewByTagID.mock.funcGetPreviewByTagID != nil {
 		mmGetPreviewByTagID.mock.t.Fatalf("ArticleRepositoryMock.GetPreviewByTagID mock is already set by Set")
 	}
 
 	expectation := &ArticleRepositoryMockGetPreviewByTagIDExpectation{
 		mock:   mmGetPreviewByTagID.mock,
-		params: &ArticleRepositoryMockGetPreviewByTagIDParams{ctx, tagID},
+		params: &ArticleRepositoryMockGetPreviewByTagIDParams{ctx, tagID, lang},
 	}
 	mmGetPreviewByTagID.expectations = append(mmGetPreviewByTagID.expectations, expectation)
 	return expectation
@@ -871,15 +951,15 @@ func (mmGetPreviewByTagID *mArticleRepositoryMockGetPreviewByTagID) invocationsD
 }
 
 // GetPreviewByTagID implements handler.ArticleRepository
-func (mmGetPreviewByTagID *ArticleRepositoryMock) GetPreviewByTagID(ctx context.Context, tagID uint64) (aa1 []models.ArticlePreview, err error) {
+func (mmGetPreviewByTagID *ArticleRepositoryMock) GetPreviewByTagID(ctx context.Context, tagID uint64, lang models.Language) (aa1 []models.ArticlePreview, err error) {
 	mm_atomic.AddUint64(&mmGetPreviewByTagID.beforeGetPreviewByTagIDCounter, 1)
 	defer mm_atomic.AddUint64(&mmGetPreviewByTagID.afterGetPreviewByTagIDCounter, 1)
 
 	if mmGetPreviewByTagID.inspectFuncGetPreviewByTagID != nil {
-		mmGetPreviewByTagID.inspectFuncGetPreviewByTagID(ctx, tagID)
+		mmGetPreviewByTagID.inspectFuncGetPreviewByTagID(ctx, tagID, lang)
 	}
 
-	mm_params := ArticleRepositoryMockGetPreviewByTagIDParams{ctx, tagID}
+	mm_params := ArticleRepositoryMockGetPreviewByTagIDParams{ctx, tagID, lang}
 
 	// Record call args
 	mmGetPreviewByTagID.GetPreviewByTagIDMock.mutex.Lock()
@@ -898,7 +978,7 @@ func (mmGetPreviewByTagID *ArticleRepositoryMock) GetPreviewByTagID(ctx context.
 		mm_want := mmGetPreviewByTagID.GetPreviewByTagIDMock.defaultExpectation.params
 		mm_want_ptrs := mmGetPreviewByTagID.GetPreviewByTagIDMock.defaultExpectation.paramPtrs
 
-		mm_got := ArticleRepositoryMockGetPreviewByTagIDParams{ctx, tagID}
+		mm_got := ArticleRepositoryMockGetPreviewByTagIDParams{ctx, tagID, lang}
 
 		if mm_want_ptrs != nil {
 
@@ -910,6 +990,10 @@ func (mmGetPreviewByTagID *ArticleRepositoryMock) GetPreviewByTagID(ctx context.
 				mmGetPreviewByTagID.t.Errorf("ArticleRepositoryMock.GetPreviewByTagID got unexpected parameter tagID, want: %#v, got: %#v%s\n", *mm_want_ptrs.tagID, mm_got.tagID, minimock.Diff(*mm_want_ptrs.tagID, mm_got.tagID))
 			}
 
+			if mm_want_ptrs.lang != nil && !minimock.Equal(*mm_want_ptrs.lang, mm_got.lang) {
+				mmGetPreviewByTagID.t.Errorf("ArticleRepositoryMock.GetPreviewByTagID got unexpected parameter lang, want: %#v, got: %#v%s\n", *mm_want_ptrs.lang, mm_got.lang, minimock.Diff(*mm_want_ptrs.lang, mm_got.lang))
+			}
+
 		} else if mm_want != nil && !minimock.Equal(*mm_want, mm_got) {
 			mmGetPreviewByTagID.t.Errorf("ArticleRepositoryMock.GetPreviewByTagID got unexpected parameters, want: %#v, got: %#v%s\n", *mm_want, mm_got, minimock.Diff(*mm_want, mm_got))
 		}
@@ -921,9 +1005,9 @@ func (mmGetPreviewByTagID *ArticleRepositoryMock) GetPreviewByTagID(ctx context.
 		return (*mm_results).aa1, (*mm_results).err
 	}
 	if mmGetPreviewByTagID.funcGetPreviewByTagID != nil {
-		return mmGetPreviewByTagID.funcGetPreviewByTagID(ctx, tagID)
+		return mmGetPreviewByTagID.funcGetPreviewByTagID(ctx, tagID, lang)
 	}
-	mmGetPreviewByTagID.t.Fatalf("Unexpected call to ArticleRepositoryMock.GetPreviewByTagID. %v %v", ctx, tagID)
+	mmGetPreviewByTagID.t.Fatalf("Unexpected call to ArticleRepositoryMock.GetPreviewByTagID. %v %v %v", ctx, tagID, lang)
 	return
 }
 

+ 44 - 16
internal/services/handler/mocks/tag_repository_minimock.go

@@ -19,8 +19,8 @@ type TagRepositoryMock struct {
 	t          minimock.Tester
 	finishOnce sync.Once
 
-	funcGetAllUsed          func(ctx context.Context) (ta1 []models.Tag, err error)
-	inspectFuncGetAllUsed   func(ctx context.Context)
+	funcGetAllUsed          func(ctx context.Context, lang models.Language) (ta1 []models.Tag, err error)
+	inspectFuncGetAllUsed   func(ctx context.Context, lang models.Language)
 	afterGetAllUsedCounter  uint64
 	beforeGetAllUsedCounter uint64
 	GetAllUsedMock          mTagRepositoryMockGetAllUsed
@@ -74,12 +74,14 @@ type TagRepositoryMockGetAllUsedExpectation struct {
 
 // TagRepositoryMockGetAllUsedParams contains parameters of the TagRepository.GetAllUsed
 type TagRepositoryMockGetAllUsedParams struct {
-	ctx context.Context
+	ctx  context.Context
+	lang models.Language
 }
 
 // TagRepositoryMockGetAllUsedParamPtrs contains pointers to parameters of the TagRepository.GetAllUsed
 type TagRepositoryMockGetAllUsedParamPtrs struct {
-	ctx *context.Context
+	ctx  *context.Context
+	lang *models.Language
 }
 
 // TagRepositoryMockGetAllUsedResults contains results of the TagRepository.GetAllUsed
@@ -99,7 +101,7 @@ func (mmGetAllUsed *mTagRepositoryMockGetAllUsed) Optional() *mTagRepositoryMock
 }
 
 // Expect sets up expected params for TagRepository.GetAllUsed
-func (mmGetAllUsed *mTagRepositoryMockGetAllUsed) Expect(ctx context.Context) *mTagRepositoryMockGetAllUsed {
+func (mmGetAllUsed *mTagRepositoryMockGetAllUsed) Expect(ctx context.Context, lang models.Language) *mTagRepositoryMockGetAllUsed {
 	if mmGetAllUsed.mock.funcGetAllUsed != nil {
 		mmGetAllUsed.mock.t.Fatalf("TagRepositoryMock.GetAllUsed mock is already set by Set")
 	}
@@ -112,7 +114,7 @@ func (mmGetAllUsed *mTagRepositoryMockGetAllUsed) Expect(ctx context.Context) *m
 		mmGetAllUsed.mock.t.Fatalf("TagRepositoryMock.GetAllUsed mock is already set by ExpectParams functions")
 	}
 
-	mmGetAllUsed.defaultExpectation.params = &TagRepositoryMockGetAllUsedParams{ctx}
+	mmGetAllUsed.defaultExpectation.params = &TagRepositoryMockGetAllUsedParams{ctx, lang}
 	for _, e := range mmGetAllUsed.expectations {
 		if minimock.Equal(e.params, mmGetAllUsed.defaultExpectation.params) {
 			mmGetAllUsed.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmGetAllUsed.defaultExpectation.params)
@@ -144,8 +146,30 @@ func (mmGetAllUsed *mTagRepositoryMockGetAllUsed) ExpectCtxParam1(ctx context.Co
 	return mmGetAllUsed
 }
 
+// ExpectLangParam2 sets up expected param lang for TagRepository.GetAllUsed
+func (mmGetAllUsed *mTagRepositoryMockGetAllUsed) ExpectLangParam2(lang models.Language) *mTagRepositoryMockGetAllUsed {
+	if mmGetAllUsed.mock.funcGetAllUsed != nil {
+		mmGetAllUsed.mock.t.Fatalf("TagRepositoryMock.GetAllUsed mock is already set by Set")
+	}
+
+	if mmGetAllUsed.defaultExpectation == nil {
+		mmGetAllUsed.defaultExpectation = &TagRepositoryMockGetAllUsedExpectation{}
+	}
+
+	if mmGetAllUsed.defaultExpectation.params != nil {
+		mmGetAllUsed.mock.t.Fatalf("TagRepositoryMock.GetAllUsed mock is already set by Expect")
+	}
+
+	if mmGetAllUsed.defaultExpectation.paramPtrs == nil {
+		mmGetAllUsed.defaultExpectation.paramPtrs = &TagRepositoryMockGetAllUsedParamPtrs{}
+	}
+	mmGetAllUsed.defaultExpectation.paramPtrs.lang = &lang
+
+	return mmGetAllUsed
+}
+
 // Inspect accepts an inspector function that has same arguments as the TagRepository.GetAllUsed
-func (mmGetAllUsed *mTagRepositoryMockGetAllUsed) Inspect(f func(ctx context.Context)) *mTagRepositoryMockGetAllUsed {
+func (mmGetAllUsed *mTagRepositoryMockGetAllUsed) Inspect(f func(ctx context.Context, lang models.Language)) *mTagRepositoryMockGetAllUsed {
 	if mmGetAllUsed.mock.inspectFuncGetAllUsed != nil {
 		mmGetAllUsed.mock.t.Fatalf("Inspect function is already set for TagRepositoryMock.GetAllUsed")
 	}
@@ -169,7 +193,7 @@ func (mmGetAllUsed *mTagRepositoryMockGetAllUsed) Return(ta1 []models.Tag, err e
 }
 
 // Set uses given function f to mock the TagRepository.GetAllUsed method
-func (mmGetAllUsed *mTagRepositoryMockGetAllUsed) Set(f func(ctx context.Context) (ta1 []models.Tag, err error)) *TagRepositoryMock {
+func (mmGetAllUsed *mTagRepositoryMockGetAllUsed) Set(f func(ctx context.Context, lang models.Language) (ta1 []models.Tag, err error)) *TagRepositoryMock {
 	if mmGetAllUsed.defaultExpectation != nil {
 		mmGetAllUsed.mock.t.Fatalf("Default expectation is already set for the TagRepository.GetAllUsed method")
 	}
@@ -184,14 +208,14 @@ func (mmGetAllUsed *mTagRepositoryMockGetAllUsed) Set(f func(ctx context.Context
 
 // When sets expectation for the TagRepository.GetAllUsed which will trigger the result defined by the following
 // Then helper
-func (mmGetAllUsed *mTagRepositoryMockGetAllUsed) When(ctx context.Context) *TagRepositoryMockGetAllUsedExpectation {
+func (mmGetAllUsed *mTagRepositoryMockGetAllUsed) When(ctx context.Context, lang models.Language) *TagRepositoryMockGetAllUsedExpectation {
 	if mmGetAllUsed.mock.funcGetAllUsed != nil {
 		mmGetAllUsed.mock.t.Fatalf("TagRepositoryMock.GetAllUsed mock is already set by Set")
 	}
 
 	expectation := &TagRepositoryMockGetAllUsedExpectation{
 		mock:   mmGetAllUsed.mock,
-		params: &TagRepositoryMockGetAllUsedParams{ctx},
+		params: &TagRepositoryMockGetAllUsedParams{ctx, lang},
 	}
 	mmGetAllUsed.expectations = append(mmGetAllUsed.expectations, expectation)
 	return expectation
@@ -224,15 +248,15 @@ func (mmGetAllUsed *mTagRepositoryMockGetAllUsed) invocationsDone() bool {
 }
 
 // GetAllUsed implements handler.TagRepository
-func (mmGetAllUsed *TagRepositoryMock) GetAllUsed(ctx context.Context) (ta1 []models.Tag, err error) {
+func (mmGetAllUsed *TagRepositoryMock) GetAllUsed(ctx context.Context, lang models.Language) (ta1 []models.Tag, err error) {
 	mm_atomic.AddUint64(&mmGetAllUsed.beforeGetAllUsedCounter, 1)
 	defer mm_atomic.AddUint64(&mmGetAllUsed.afterGetAllUsedCounter, 1)
 
 	if mmGetAllUsed.inspectFuncGetAllUsed != nil {
-		mmGetAllUsed.inspectFuncGetAllUsed(ctx)
+		mmGetAllUsed.inspectFuncGetAllUsed(ctx, lang)
 	}
 
-	mm_params := TagRepositoryMockGetAllUsedParams{ctx}
+	mm_params := TagRepositoryMockGetAllUsedParams{ctx, lang}
 
 	// Record call args
 	mmGetAllUsed.GetAllUsedMock.mutex.Lock()
@@ -251,7 +275,7 @@ func (mmGetAllUsed *TagRepositoryMock) GetAllUsed(ctx context.Context) (ta1 []mo
 		mm_want := mmGetAllUsed.GetAllUsedMock.defaultExpectation.params
 		mm_want_ptrs := mmGetAllUsed.GetAllUsedMock.defaultExpectation.paramPtrs
 
-		mm_got := TagRepositoryMockGetAllUsedParams{ctx}
+		mm_got := TagRepositoryMockGetAllUsedParams{ctx, lang}
 
 		if mm_want_ptrs != nil {
 
@@ -259,6 +283,10 @@ func (mmGetAllUsed *TagRepositoryMock) GetAllUsed(ctx context.Context) (ta1 []mo
 				mmGetAllUsed.t.Errorf("TagRepositoryMock.GetAllUsed got unexpected parameter ctx, want: %#v, got: %#v%s\n", *mm_want_ptrs.ctx, mm_got.ctx, minimock.Diff(*mm_want_ptrs.ctx, mm_got.ctx))
 			}
 
+			if mm_want_ptrs.lang != nil && !minimock.Equal(*mm_want_ptrs.lang, mm_got.lang) {
+				mmGetAllUsed.t.Errorf("TagRepositoryMock.GetAllUsed got unexpected parameter lang, want: %#v, got: %#v%s\n", *mm_want_ptrs.lang, mm_got.lang, minimock.Diff(*mm_want_ptrs.lang, mm_got.lang))
+			}
+
 		} else if mm_want != nil && !minimock.Equal(*mm_want, mm_got) {
 			mmGetAllUsed.t.Errorf("TagRepositoryMock.GetAllUsed got unexpected parameters, want: %#v, got: %#v%s\n", *mm_want, mm_got, minimock.Diff(*mm_want, mm_got))
 		}
@@ -270,9 +298,9 @@ func (mmGetAllUsed *TagRepositoryMock) GetAllUsed(ctx context.Context) (ta1 []mo
 		return (*mm_results).ta1, (*mm_results).err
 	}
 	if mmGetAllUsed.funcGetAllUsed != nil {
-		return mmGetAllUsed.funcGetAllUsed(ctx)
+		return mmGetAllUsed.funcGetAllUsed(ctx, lang)
 	}
-	mmGetAllUsed.t.Fatalf("Unexpected call to TagRepositoryMock.GetAllUsed. %v", ctx)
+	mmGetAllUsed.t.Fatalf("Unexpected call to TagRepositoryMock.GetAllUsed. %v %v", ctx, lang)
 	return
 }
 

+ 5 - 6
internal/services/handler/tag.go

@@ -26,8 +26,7 @@ func TagHandler(
 		url := fctx.Params(tagParam)
 		lang := i18n.Language(fctx)
 
-		renderData, found := cacheService.Get(TagCacheKey + string(lang) + url)
-
+		renderData, found := cacheService.Get(TagCacheKey + url + string(lang))
 		if !found {
 			tag, err := tagRepository.GetByURL(ctx, url)
 			if err != nil {
@@ -36,25 +35,25 @@ func TagHandler(
 				}
 
 				logger.Error(ctx, err.Error())
-
 				return err
 			}
 
-			articles, err := articleRepository.GetPreviewByTagID(ctx, tag.ID)
+			articles, err := articleRepository.GetPreviewByTagID(ctx, tag.ID, mapper.ToLanguageModel(lang))
 			if err != nil {
 				logger.Error(ctx, err.Error())
 				return err
 			}
 
 			renderData = fiber.Map{
+				"lang":            lang,
 				"headTitle":       i18n.T(lang, "head_title"),
 				"pageTitle":       i18n.T(lang, "tag_page_title", tag.Tag),
 				"headDescription": i18n.T(lang, "tag_page_desc", tag.Tag),
 				"headKeywords":    i18n.T(lang, "tag_page_keywords", tag.Tag),
-				"articles":        mapper.ToArticlePreviewDTOList(fctx, articles),
+				"articles":        mapper.ToArticlesPreview(lang, articles),
 			}
 
-			cacheService.Set(TagCacheKey+string(lang)+url, renderData, nil)
+			cacheService.Set(TagCacheKey+url+string(lang), renderData, nil)
 		}
 
 		return fctx.Render("tag", renderData, "_layout")

+ 4 - 5
internal/services/handler/tag_test.go

@@ -80,8 +80,9 @@ func TestTagHandler(t *testing.T) {
 			},
 			articleMock: func(mc *minimock.Controller) ArticleRepository {
 				mock := mocks.NewArticleRepositoryMock(mc)
-				mock.GetPreviewByTagIDMock.Inspect(func(ctx context.Context, id uint64) {
+				mock.GetPreviewByTagIDMock.Inspect(func(ctx context.Context, id uint64, lang models.Language) {
 					assert.Equal(mc, tagID, id)
+					assert.Equal(mc, models.LangRu, lang)
 				}).Return([]models.ArticlePreview{article}, nil)
 
 				return mock
@@ -98,7 +99,6 @@ func TestTagHandler(t *testing.T) {
 			cacheMock: func(mc *minimock.Controller) CacheService {
 				mock := mocks.NewCacheServiceMock(mc)
 				mock.GetMock.Return(nil, false)
-
 				return mock
 			},
 			tagMock: func(mc *minimock.Controller) TagRepository {
@@ -125,7 +125,6 @@ func TestTagHandler(t *testing.T) {
 			cacheMock: func(mc *minimock.Controller) CacheService {
 				mock := mocks.NewCacheServiceMock(mc)
 				mock.GetMock.Return(nil, false)
-
 				return mock
 			},
 			tagMock: func(mc *minimock.Controller) TagRepository {
@@ -151,7 +150,6 @@ func TestTagHandler(t *testing.T) {
 			cacheMock: func(mc *minimock.Controller) CacheService {
 				mock := mocks.NewCacheServiceMock(mc)
 				mock.GetMock.Return(nil, false)
-
 				return mock
 			},
 			tagMock: func(mc *minimock.Controller) TagRepository {
@@ -165,8 +163,9 @@ func TestTagHandler(t *testing.T) {
 			},
 			articleMock: func(mc *minimock.Controller) ArticleRepository {
 				mock := mocks.NewArticleRepositoryMock(mc)
-				mock.GetPreviewByTagIDMock.Inspect(func(ctx context.Context, id uint64) {
+				mock.GetPreviewByTagIDMock.Inspect(func(ctx context.Context, id uint64, lang models.Language) {
 					assert.Equal(mc, tagID, id)
+					assert.Equal(mc, models.LangRu, lang)
 				}).Return(nil, internalErr)
 
 				return mock

+ 24 - 0
internal/services/i18n/en.json

@@ -0,0 +1,24 @@
+{
+  "head_title": "From Elephant to Gopher - articles about PHP, Go, algorithms",
+  "recent_articles": "Recent articles",
+  "tags": "Tags",
+  "to_main_page": "to main page",
+  "read": "Read more",
+  "main_page_title": "Articles list",
+  "main_page_desc": "articles list",
+  "main_page_keywords": "PHP, Go, Golang, programming, articles, blog",
+  "footer_text": "If you have any questions, suggestions or requests, please contact",
+  "footer_author": "Dmitriy Gnatenko",
+  "m01": "January",
+  "m02": "February",
+  "m03": "March",
+  "m04": "April",
+  "m05": "May",
+  "m06": "June",
+  "m07": "July",
+  "m08": "August",
+  "m09": "September",
+  "m10": "October",
+  "m11": "November",
+  "m12": "December"
+}

+ 18 - 16
internal/services/i18n/i18n.go

@@ -8,18 +8,21 @@ import (
 	"sync"
 
 	"git.dmitriygnatenko.ru/dima/go-common/logger"
-	"github.com/gofiber/fiber/v2"
 )
 
 //go:embed ru.json
 var ruJsonContent []byte
 
-type Lang string
+//go:embed en.json
+var enJsonContent []byte
+
+type Language string
 
 const (
-	CtxLanguageKey      = "Language"
-	Ru             Lang = "ru"
-	Default        Lang = Ru
+	Ru        Language = "ru"
+	En        Language = "en"
+	Default   Language = Ru
+	LangParam          = "lang"
 )
 
 var (
@@ -28,7 +31,7 @@ var (
 )
 
 type I18n struct {
-	translations map[Lang]map[string]string
+	translations map[Language]map[string]string
 }
 
 func Init() error {
@@ -36,21 +39,28 @@ func Init() error {
 
 	once.Do(func() {
 		ruTranslations := make(map[string]string)
+		enTranslations := make(map[string]string)
 
 		err = json.Unmarshal(ruJsonContent, &ruTranslations)
 		if err != nil {
 			return
 		}
 
-		i18n = &I18n{translations: map[Lang]map[string]string{
+		err = json.Unmarshal(enJsonContent, &enTranslations)
+		if err != nil {
+			return
+		}
+
+		i18n = &I18n{translations: map[Language]map[string]string{
 			Ru: ruTranslations,
+			En: enTranslations,
 		}}
 	})
 
 	return err
 }
 
-func T(lang Lang, key string, args ...any) string {
+func T(lang Language, key string, args ...any) string {
 	if i18n == nil {
 		logger.Error(context.Background(), "i18n not initialized")
 		return ""
@@ -67,11 +77,3 @@ func T(lang Lang, key string, args ...any) string {
 
 	return fmt.Sprintf(i18n.translations[lang][key], args...)
 }
-
-func Language(c *fiber.Ctx) Lang {
-	if lang, ok := c.Locals(CtxLanguageKey).(Lang); ok {
-		return lang
-	}
-
-	return Default
-}

+ 7 - 1
internal/services/i18n/ru.json

@@ -1,5 +1,9 @@
 {
   "head_title": "От слона к суслику - статьи про PHP, Go, алгоритмы",
+  "recent_articles": "Последние статьи",
+  "tags": "Теги",
+  "to_main_page": "на главную",
+  "read": "Читать дальше",
   "main_page_title": "Список статей",
   "main_page_desc": "список статей",
   "main_page_keywords": "PHP, Go, Golang, программирование, статьи, блог",
@@ -18,7 +22,9 @@
   "err_tag_exists": "Тег с данным URL уже существует",
   "admin_add_article_title": "Добавление статьи",
   "admin_edit_article_title": "Редактирование статьи",
-  "err_article_exists": "Статья с данным URL уже существует",
+  "admin_err_article_exists": "Статья с данным URL уже существует",
+  "footer_text": "По всем вопросам, предложениям и пожеланиям обращаться по адресу электронной почты",
+  "footer_author": "Дмитрий Гнатенко",
   "m01": "января",
   "m02": "февраля",
   "m03": "марта",

+ 2 - 2
internal/templates/_layout.html

@@ -25,11 +25,11 @@
     <div class="container text-center">
         <div class="copyright-text">
             <p>
-                По всем вопросам, предложениям и пожеланиям обращаться по адресу электронной почты<br>
+                {{ trans .lang "footer_text"}}<br>
                 <a href="mailto:info@dmitriygnatenko.ru">info@dmitriygnatenko.ru</a>
             </p>
             <hr>
-            <p>&copy; {{ now.UTC.Year }} Дмитрий Гнатенко</p>
+            <p>&copy; {{ now.UTC.Year }} {{ trans .lang "footer_author"}}</p>
         </div>
     </div>
 </footer>

+ 2 - 2
internal/templates/_sidebar.html

@@ -3,7 +3,7 @@
         {{ if .sidebarArticles }}
         <div class="col-md-12 widget-posts top_45">
             <div class="widget-title">
-                <h2 class="upper">Последние статьи</h2>
+                <h2 class="upper">{{ trans .lang "recent_articles"}}</h2>
             </div>
             <ul class="sidebar-posts">
                 {{ range .sidebarArticles }}
@@ -26,7 +26,7 @@
         {{ if .sidebarTags }}
         <div class="col-md-12 widget-tags top_45">
             <div class="widget-title">
-                <h2 class="upper">Теги</h2>
+                <h2 class="upper">{{ trans .lang "tags"}}</h2>
             </div>
             <ul class="top_15">
                 {{ range .sidebarTags }}

+ 1 - 1
internal/templates/article.html

@@ -17,7 +17,7 @@
     <div class="container">
         <div class="row">
             <div class="page-content col-md-12">
-                <a href="/" class="blog-link-to-main">&larr; на главную</a>
+                <a href="/" class="blog-link-to-main">&larr; {{ trans .lang "to_main_page"}}</a>
             </div>
         </div>
         <div class="row">

+ 1 - 1
internal/templates/error.html

@@ -3,7 +3,7 @@
         <div class="row">
             <div class="page-content col-md-12">
                 <div class="not_found">
-                    <a href="/" class="blog-link-to-main">&larr; на главную</a>
+                    <a href="/" class="blog-link-to-main">&larr; {{ trans .lang "to_main_page"}}</a>
                     <br><br>
                     <h1>{{ .code }}</h1>
                     <h2>{{ .text }}</h2>

+ 3 - 1
internal/templates/index.html

@@ -1,4 +1,5 @@
 {{ $len := len .articles }}
+{{ $lang := .lang }}
 <section class="page">
     <div class="container">
         <div class="row">
@@ -10,7 +11,8 @@
                             <span class="data">{{ $article.PublishTime }}</span>
                             <h4>{{ $article.Title }}</h4>
                             {{ if $article.PreviewText }}<p>{{ noescape $article.PreviewText }}</p>{{ end }}
-                            <a href="/article/{{ $article.URL }}" class="blog-link">Читать дальше</a>
+                            {{ $link := concat "/articles/" $article.URL }}
+                            <a href="{{ link $lang $link }}" class="blog-link">{{ trans $lang "read"}}</a>
                         </div>
                     </div>
                 </div>

+ 2 - 2
internal/templates/tag.html

@@ -4,7 +4,7 @@
         <div class="row">
             <div class="page-content col-md-12">
                 <div class="col-md-12 bottom_30">
-                    <a href="/" class="blog-link-to-main">&larr; на главную</a>
+                    <a href="/" class="blog-link-to-main">&larr; {{ trans .lang "to_main_page"}}</a>
                 </div>
             </div>
         </div>
@@ -17,7 +17,7 @@
                             <span class="data">{{ $article.PublishTime }}</span>
                             <h4>{{ $article.Title }}</h4>
                             {{ if $article.PreviewText }}<p>{{ noescape $article.PreviewText }}</p>{{ end }}
-                            <a href="/article/{{ $article.URL }}" class="blog-link">Читать дальше</a>
+                            <a href="/article/{{ $article.URL }}" class="blog-link">{{ trans .lang "read"}}</a>
                         </div>
                     </div>
                 </div>