2 コミット ed238c3f7a ... 555eacf23d

作者 SHA1 メッセージ 日付
  Dmitriy Gnatenko 555eacf23d Update translations 5 日 前
  Dmitriy Gnatenko ed238c3f7a Update translations 5 日 前

+ 1 - 1
go.mod

@@ -3,7 +3,7 @@ module git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2
 go 1.22.2
 
 require (
-	git.dmitriygnatenko.ru/dima/go-common v1.6.3
+	git.dmitriygnatenko.ru/dima/go-common v1.6.4
 	github.com/Masterminds/squirrel v1.5.3
 	github.com/brianvoe/gofakeit/v6 v6.28.0
 	github.com/go-playground/locales v0.14.0

+ 2 - 2
go.sum

@@ -38,8 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
-git.dmitriygnatenko.ru/dima/go-common v1.6.3 h1:TcRZOyV3SG6yGRG5b/aNJ3sySF9ti3w0eHMvImMup4s=
-git.dmitriygnatenko.ru/dima/go-common v1.6.3/go.mod h1:/7VcyxInOlvAGedhH8YONNpWWETaXFU8gnTxLDLcing=
+git.dmitriygnatenko.ru/dima/go-common v1.6.4 h1:Cfmy2zPli+tUQCUstC3DddNrzCzcZ48hdSMF4/js4yg=
+git.dmitriygnatenko.ru/dima/go-common v1.6.4/go.mod h1:/7VcyxInOlvAGedhH8YONNpWWETaXFU8gnTxLDLcing=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc=

+ 2 - 2
internal/fiber/public_handlers.go

@@ -21,7 +21,7 @@ func initPublicHandlers(app *fiber.App, sp ServiceProvider) {
 		sp.TagRepository(),
 	)
 
-	app.Get("/:lang<regex(^en$)/tag/:tag>", tagPageHandler)
+	app.Get("/:lang<regex(^en$)>/tag/:tag>", tagPageHandler)
 	app.Get("/tag/:tag", tagPageHandler)
 
 	articlePageHandler := handler.ArticleHandler(
@@ -30,6 +30,6 @@ func initPublicHandlers(app *fiber.App, sp ServiceProvider) {
 		sp.TagRepository(),
 	)
 
-	app.Get("/:lang<regex(^en$)/article/:article", articlePageHandler)
+	app.Get("/:lang<regex(^en$)>/article/:article", articlePageHandler)
 	app.Get("/article/:article", articlePageHandler)
 }

+ 8 - 46
internal/fiber/templates.go

@@ -2,63 +2,25 @@ 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"
+	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/templates/functions"
 )
 
-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)
+	engine.AddFunc("now", functions.Now)
+	engine.AddFunc("noescape", functions.NoEscape)
+	engine.AddFunc("concat", functions.Concat)
+	engine.AddFunc("gridsep", functions.GridSep)
+	engine.AddFunc("version", functions.Version(sp))
+	engine.AddFunc("trans", functions.Trans)
+	engine.AddFunc("link", functions.Link)
 
 	return engine
 }

+ 11 - 8
internal/helpers/test/fiber.go

@@ -4,19 +4,22 @@ import (
 	"github.com/gofiber/fiber/v2"
 	"github.com/gofiber/template/html/v2"
 
-	app "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/fiber"
+	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/templates/functions"
 )
 
 func GetFiberTestConfig() fiber.Config {
 	engine := html.New("./../../templates", ".html")
 
-	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)
+	engine.AddFunc("now", functions.Now)
+	engine.AddFunc("noescape", functions.NoEscape)
+	engine.AddFunc("concat", functions.Concat)
+	engine.AddFunc("gridsep", functions.GridSep)
+	engine.AddFunc("trans", functions.Trans)
+	engine.AddFunc("link", functions.Link)
+
+	engine.AddFunc("version", func() string {
+		return "1"
+	})
 
 	return fiber.Config{
 		Views: engine,

+ 38 - 15
internal/repositories/article.go

@@ -22,6 +22,7 @@ var articleTableColumns = []string{
 	"meta_keywords",
 	"meta_desc",
 	"is_active",
+	"language",
 }
 
 type ArticleRepository struct {
@@ -32,13 +33,16 @@ func InitArticleRepository(db DB) *ArticleRepository {
 	return &ArticleRepository{db: db}
 }
 
-func (a ArticleRepository) GetAllPreview(ctx context.Context) ([]models.ArticlePreview, error) {
+func (a ArticleRepository) GetAllPreview(
+	ctx context.Context,
+	lang models.Language,
+) ([]models.ArticlePreview, error) {
 	var res []models.ArticlePreview
 
 	q, v, err := sq.Select("id", "url", "publish_time", "title", "preview_text", "image").
 		From(articleTableName).
-		PlaceholderFormat(sq.Dollar).
-		Where(sq.Eq{"is_active": true}).
+		PlaceholderFormat(sq.Question).
+		Where(sq.Eq{"is_active": true, "language": uint64(lang)}).
 		OrderBy("publish_time DESC").
 		ToSql()
 
@@ -74,15 +78,19 @@ func (a ArticleRepository) GetAll(ctx context.Context) ([]models.Article, error)
 	return res, nil
 }
 
-func (a ArticleRepository) GetPreviewByTagID(ctx context.Context, tagID uint64) ([]models.ArticlePreview, error) {
+func (a ArticleRepository) GetPreviewByTagID(
+	ctx context.Context,
+	tagID uint64,
+	lang models.Language,
+) ([]models.ArticlePreview, error) {
 	var res []models.ArticlePreview
 
 	q := "SELECT a.id, a.url, a.publish_time, a.title, a.preview_text, a.image " +
 		"FROM " + articleTableName + " a, " + articleTagTableName + " at " +
-		"WHERE a.is_active = true AND at.article_id = a.id AND at.tag_id = $1 " +
+		"WHERE a.is_active = true AND a.language = ? AND at.article_id = a.id AND at.tag_id = ? " +
 		"ORDER BY a.publish_time DESC"
 
-	err := a.db.SelectContext(ctx, &res, q, tagID)
+	err := a.db.SelectContext(ctx, &res, q, uint64(lang), tagID)
 	if err != nil {
 		return nil, fmt.Errorf("select: %w", err)
 	}
@@ -90,13 +98,20 @@ func (a ArticleRepository) GetPreviewByTagID(ctx context.Context, tagID uint64)
 	return res, nil
 }
 
-func (a ArticleRepository) GetByURL(ctx context.Context, url string) (*models.Article, error) {
+func (a ArticleRepository) GetByURL(
+	ctx context.Context,
+	url string,
+	lang models.Language,
+) (*models.Article, error) {
 	var res models.Article
 
 	q, v, err := sq.Select(articleTableColumns...).
 		From(articleTableName).
-		PlaceholderFormat(sq.Dollar).
-		Where(sq.Eq{"url": url}).
+		PlaceholderFormat(sq.Question).
+		Where(sq.Eq{
+			"url":      url,
+			"language": uint64(lang),
+		}).
 		Limit(1).
 		ToSql()
 
@@ -112,12 +127,15 @@ func (a ArticleRepository) GetByURL(ctx context.Context, url string) (*models.Ar
 	return &res, nil
 }
 
-func (a ArticleRepository) GetByID(ctx context.Context, id uint64) (*models.Article, error) {
+func (a ArticleRepository) GetByID(
+	ctx context.Context,
+	id uint64,
+) (*models.Article, error) {
 	var res models.Article
 
 	q, v, err := sq.Select(articleTableColumns...).
 		From(articleTableName).
-		PlaceholderFormat(sq.Dollar).
+		PlaceholderFormat(sq.Question).
 		Where(sq.Eq{"id": id}).
 		Limit(1).
 		ToSql()
@@ -134,9 +152,12 @@ func (a ArticleRepository) GetByID(ctx context.Context, id uint64) (*models.Arti
 	return &res, nil
 }
 
-func (a ArticleRepository) Add(ctx context.Context, m models.Article) (uint64, error) {
+func (a ArticleRepository) Add(
+	ctx context.Context,
+	m models.Article,
+) (uint64, error) {
 	q, v, err := sq.Insert(articleTableName).
-		PlaceholderFormat(sq.Dollar).
+		PlaceholderFormat(sq.Question).
 		Columns(
 			"url",
 			"publish_time",
@@ -147,6 +168,7 @@ func (a ArticleRepository) Add(ctx context.Context, m models.Article) (uint64, e
 			"meta_keywords",
 			"meta_desc",
 			"is_active",
+			"language",
 		).Values(
 		m.URL,
 		m.PublishTime,
@@ -157,6 +179,7 @@ func (a ArticleRepository) Add(ctx context.Context, m models.Article) (uint64, e
 		m.MetaKeywords,
 		m.MetaDescription,
 		m.IsActive,
+		m.Language,
 	).
 		Suffix("RETURNING id").
 		ToSql()
@@ -176,7 +199,7 @@ func (a ArticleRepository) Add(ctx context.Context, m models.Article) (uint64, e
 
 func (a ArticleRepository) Update(ctx context.Context, req models.Article) error {
 	q, v, err := sq.Update(articleTableName).
-		PlaceholderFormat(sq.Dollar).
+		PlaceholderFormat(sq.Question).
 		Set("url", req.URL).
 		Set("publish_time", req.PublishTime).
 		Set("title", req.Title).
@@ -203,7 +226,7 @@ func (a ArticleRepository) Update(ctx context.Context, req models.Article) error
 
 func (a ArticleRepository) Delete(ctx context.Context, id uint64) error {
 	q, v, err := sq.Delete(articleTableName).
-		PlaceholderFormat(sq.Dollar).
+		PlaceholderFormat(sq.Question).
 		Where(sq.Eq{"id": id}).
 		ToSql()
 

+ 0 - 2
internal/service_provider/sp.go

@@ -80,10 +80,8 @@ func Init() (*ServiceProvider, error) {
 		logger.WithEmailLogLevel(configService.LoggerEmailLevel()),
 		logger.WithEmailSubject(configService.LoggerEmailSubject()),
 		logger.WithEmailRecipient(configService.LoggerEmailRecipient()),
-		logger.WithEmailLogAddSource(configService.LoggerEmailAddSource()),
 		logger.WithStdoutLogEnabled(configService.LoggerStdoutEnabled()),
 		logger.WithStdoutLogLevel(configService.LoggerStdoutLevel()),
-		logger.WithStdoutLogAddSource(configService.LoggerStdoutAddSource()),
 	}
 
 	if len(configService.SMTPPassword()) > 0 &&

+ 0 - 14
internal/services/config/config.go

@@ -37,10 +37,8 @@ type Service struct {
 	staticVersion               uint16
 	loggerStdoutEnabled         bool
 	loggerStdoutLevel           string
-	loggerStdoutAddSource       bool
 	loggerEmailEnabled          bool
 	loggerEmailLevel            string
-	loggerEmailAddSource        bool
 	loggerEmailSubject          string
 	loggerEmailRecipient        string
 	loginRateLimiterMaxRequests uint16
@@ -92,10 +90,8 @@ func Init() (*Service, error) {
 		StaticVersion               uint16        `mapstructure:"STATIC_VERSION"`
 		LoggerStdoutEnabled         bool          `mapstructure:"LOGGER_STDOUT_ENABLED"`
 		LoggerStdoutLevel           string        `mapstructure:"LOGGER_STDOUT_LEVEL"`
-		LoggerStdoutAddSource       bool          `mapstructure:"LOGGER_STDOUT_ADD_SOURCE"`
 		LoggerEmailEnabled          bool          `mapstructure:"LOGGER_EMAIL_ENABLED"`
 		LoggerEmailLevel            string        `mapstructure:"LOGGER_EMAIL_LEVEL"`
-		LoggerEmailAddSource        bool          `mapstructure:"LOGGER_EMAIL_ADD_SOURCE"`
 		LoggerEmailSubject          string        `mapstructure:"LOGGER_EMAIL_SUBJECT"`
 		LoggerEmailRecipient        string        `mapstructure:"LOGGER_EMAIL_RECIPIENT"`
 		LoginRateLimiterMaxRequests uint16        `mapstructure:"LOGIN_RATE_LIMITER_MAX_REQUESTS"`
@@ -134,10 +130,8 @@ func Init() (*Service, error) {
 		staticVersion:               s.StaticVersion,
 		loggerStdoutEnabled:         s.LoggerStdoutEnabled,
 		loggerStdoutLevel:           s.LoggerStdoutLevel,
-		loggerStdoutAddSource:       s.LoggerStdoutAddSource,
 		loggerEmailEnabled:          s.LoggerEmailEnabled,
 		loggerEmailLevel:            s.LoggerEmailLevel,
-		loggerEmailAddSource:        s.LoggerEmailAddSource,
 		loggerEmailSubject:          s.LoggerEmailSubject,
 		loggerEmailRecipient:        s.LoggerEmailRecipient,
 		loginRateLimiterMaxRequests: s.LoginRateLimiterMaxRequests,
@@ -253,10 +247,6 @@ func (s Service) LoggerStdoutLevel() string {
 	return s.loggerStdoutLevel
 }
 
-func (s Service) LoggerStdoutAddSource() bool {
-	return s.loggerStdoutAddSource
-}
-
 func (s Service) LoggerEmailEnabled() bool {
 	return s.loggerEmailEnabled
 }
@@ -265,10 +255,6 @@ func (s Service) LoggerEmailLevel() string {
 	return s.loggerEmailLevel
 }
 
-func (s Service) LoggerEmailAddSource() bool {
-	return s.loggerEmailAddSource
-}
-
 func (s Service) LoggerEmailSubject() string {
 	return s.loggerEmailSubject
 }

+ 13 - 10
internal/services/handler/article.go

@@ -29,10 +29,11 @@ func ArticleHandler(
 		var renderData fiber.Map
 		ctx := fctx.Context()
 		url := fctx.Params(articleParam)
-		lang := i18n.Language(fctx)
+		lang := mapper.LanguageFromContext(fctx)
 
 		// article
-		cacheData, found := cacheService.Get(ArticleCacheKey + url)
+		articleCacheKey := ArticleCacheKey + url
+		cacheData, found := cacheService.Get(articleCacheKey)
 		if found {
 			if articleData, ok := cacheData.(fiber.Map); ok {
 				renderData = articleData
@@ -42,6 +43,7 @@ func ArticleHandler(
 		if renderData == nil {
 			article, err := articleRepository.GetByURL(ctx, url, mapper.ToLanguageModel(lang))
 			if err != nil {
+				logger.Error(ctx, err.Error())
 				if errors.Is(err, sql.ErrNoRows) {
 					return fiber.ErrNotFound
 				}
@@ -54,10 +56,10 @@ func ArticleHandler(
 				return fiber.ErrNotFound
 			}
 
-			articleDTO := mapper.ToArticle(fctx, *article)
+			articleDTO := mapper.ToArticle(lang, *article)
 
 			renderData = fiber.Map{
-				"lang":            string(lang),
+				"lang":            lang,
 				"headTitle":       i18n.T(lang, "head_title"),
 				"headDescription": articleDTO.MetaDescription,
 				"headKeywords":    articleDTO.MetaKeywords,
@@ -65,11 +67,12 @@ func ArticleHandler(
 				"article":         articleDTO,
 			}
 
-			cacheService.Set(ArticleCacheKey+url, renderData, nil)
+			cacheService.Set(articleCacheKey, renderData, nil)
 		}
 
 		// recent articles
-		cacheData, found = cacheService.Get(RecentArticlesCacheKey + string(lang))
+		recentArticlesCacheKey := RecentArticlesCacheKey + string(lang)
+		cacheData, found = cacheService.Get(recentArticlesCacheKey)
 		if found {
 			if articlesData, ok := cacheData.([]dto.ArticlePreview); ok {
 				renderData["sidebarArticles"] = articlesData
@@ -89,12 +92,13 @@ func ArticleHandler(
 			}
 
 			sidebarArticles := mapper.ToArticlesPreview(lang, articles)
-			cacheService.Set(RecentArticlesCacheKey+string(lang), sidebarArticles, nil)
 			renderData["sidebarArticles"] = sidebarArticles
+			cacheService.Set(recentArticlesCacheKey, sidebarArticles, nil)
 		}
 
 		// tags
-		cacheData, found = cacheService.Get(UsedTagsCacheKey + string(lang))
+		usedTagsCacheKey := UsedTagsCacheKey + string(lang)
+		cacheData, found = cacheService.Get(usedTagsCacheKey)
 		if found {
 			if tagsData, ok := cacheData.([]dto.Tag); ok {
 				renderData["sidebarTags"] = tagsData
@@ -109,9 +113,8 @@ func ArticleHandler(
 			}
 
 			sidebarTags := mapper.ToTagsList(tags)
-
-			cacheService.Set(UsedTagsCacheKey+string(lang), sidebarTags, nil)
 			renderData["sidebarTags"] = sidebarTags
+			cacheService.Set(usedTagsCacheKey, sidebarTags, nil)
 		}
 
 		return fctx.Render("article", renderData, "_layout")

+ 2 - 2
internal/services/handler/main_page.go

@@ -34,7 +34,7 @@ type (
 	}
 )
 
-const allPreviewArticlesCacheKey = "all-preview-articles"
+const AllPreviewArticlesCacheKey = "all-preview-articles"
 
 func MainPageHandler(
 	cacheService CacheService,
@@ -43,7 +43,7 @@ func MainPageHandler(
 	return func(fctx *fiber.Ctx) error {
 		ctx := fctx.Context()
 		lang := mapper.LanguageFromContext(fctx)
-		cacheKey := allPreviewArticlesCacheKey + string(lang)
+		cacheKey := AllPreviewArticlesCacheKey + string(lang)
 
 		renderData, found := cacheService.Get(cacheKey)
 		if !found {

+ 4 - 3
internal/services/handler/tag.go

@@ -24,9 +24,10 @@ func TagHandler(
 	return func(fctx *fiber.Ctx) error {
 		ctx := fctx.Context()
 		url := fctx.Params(tagParam)
-		lang := i18n.Language(fctx)
+		lang := mapper.LanguageFromContext(fctx)
+		cacheKey := TagCacheKey + url + string(lang)
 
-		renderData, found := cacheService.Get(TagCacheKey + url + string(lang))
+		renderData, found := cacheService.Get(cacheKey)
 		if !found {
 			tag, err := tagRepository.GetByURL(ctx, url)
 			if err != nil {
@@ -53,7 +54,7 @@ func TagHandler(
 				"articles":        mapper.ToArticlesPreview(lang, articles),
 			}
 
-			cacheService.Set(TagCacheKey+url+string(lang), renderData, nil)
+			cacheService.Set(cacheKey, renderData, nil)
 		}
 
 		return fctx.Render("tag", renderData, "_layout")

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

@@ -7,6 +7,7 @@
   "main_page_title": "Articles list",
   "main_page_desc": "articles list",
   "main_page_keywords": "PHP, Go, Golang, programming, articles, blog",
+  "article_page_title": "Article<br>%s",
   "footer_text": "If you have any questions, suggestions or requests, please contact",
   "footer_author": "Dmitriy Gnatenko",
   "m01": "January",

+ 13 - 2
internal/services/i18n/i18n.go

@@ -71,9 +71,20 @@ func T(lang Language, key string, args ...any) string {
 		return ""
 	}
 
+	var translated string
+
 	if len(args) == 0 {
-		return i18n.translations[lang][key]
+		translated = i18n.translations[lang][key]
+	} else {
+		translated = fmt.Sprintf(i18n.translations[lang][key], args...)
+	}
+
+	if len(translated) == 0 {
+		logger.Warnf(
+			context.Background(),
+			"i18n: language %s translation %s is not found", lang, key,
+		)
 	}
 
-	return fmt.Sprintf(i18n.translations[lang][key], args...)
+	return translated
 }

+ 4 - 2
internal/templates/_sidebar.html

@@ -1,3 +1,4 @@
+{{ $lang := .lang }}
 <div class="sidebar col-md-3">
     <div class="row">
         {{ if .sidebarArticles }}
@@ -8,10 +9,11 @@
             <ul class="sidebar-posts">
                 {{ range .sidebarArticles }}
                 <li>
+                    {{ $link := concat "/article/" .URL }}
                     <div class="title">
-                        <a href="/article/{{ .URL }}">{{ .Title }}</a>
+                        <a href={{ link $lang $link }}>{{ .Title }}</a>
                     </div>
-                    <a class="post-content" href="/article/{{ .URL }}">
+                    <a class="post-content" href={{ link $lang $link }}>
                         {{ if .PreviewText }}
                             <p>{{ noescape .PreviewText }}</p>
                         {{ end }}

+ 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; {{ trans .lang "to_main_page"}}</a>
+                <a href={{ link .lang "/" }} class="blog-link-to-main">&larr; {{ trans .lang "to_main_page"}}</a>
             </div>
         </div>
         <div class="row">

+ 54 - 0
internal/templates/functions/functions.go

@@ -0,0 +1,54 @@
+package functions
+
+import (
+	"html/template"
+	"strconv"
+	"strings"
+	"time"
+
+	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/config"
+	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/i18n"
+)
+
+type ServiceProvider interface {
+	ConfigService() *config.Service
+}
+
+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
+	}
+
+	if link == "/" {
+		return "/" + string(lang)
+	}
+
+	return "/" + string(lang) + link
+}

+ 1 - 1
internal/templates/index.html

@@ -11,7 +11,7 @@
                             <span class="data">{{ $article.PublishTime }}</span>
                             <h4>{{ $article.Title }}</h4>
                             {{ if $article.PreviewText }}<p>{{ noescape $article.PreviewText }}</p>{{ end }}
-                            {{ $link := concat "/articles/" $article.URL }}
+                            {{ $link := concat "/article/" $article.URL }}
                             <a href="{{ link $lang $link }}" class="blog-link">{{ trans $lang "read"}}</a>
                         </div>
                     </div>

+ 0 - 2
readme.md

@@ -51,10 +51,8 @@ STATIC_VERSION=1
 # Logger
 LOGGER_STDOUT_ENABLED=true
 LOGGER_STDOUT_LEVEL=info
-LOGGER_STDOUT_ADD_SOURCE=false
 LOGGER_EMAIL_ENABLED=true
 LOGGER_EMAIL_LEVEL=error
-LOGGER_EMAIL_ADD_SOURCE=true
 LOGGER_EMAIL_RECIPIENT=info@dmitriygnatenko.ru
 LOGGER_EMAIL_SUBJECT=Error from dmitriygnatenko.ru
 ```