From 44b72cbc80fb89788470bda75eeb9c0bb3dee2f5 Mon Sep 17 00:00:00 2001 From: SayaAndy Date: Sat, 16 May 2026 13:18:08 +0700 Subject: feat: all color themes now have light & dark variations depending on preference feat: color all leaflet elements depending on color theme & scheme feat: change font to m-plus for short description fix: theme wheel position on different dpi screens in global map --- views/pages/global-map.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'views/pages/global-map.html') diff --git a/views/pages/global-map.html b/views/pages/global-map.html index a6dc006..788eac9 100644 --- a/views/pages/global-map.html +++ b/views/pages/global-map.html @@ -229,7 +229,6 @@ const themeWheel = document.getElementById("theme-wheel"); const themeButton = document.getElementById("sidebar-theme-button"); const themeIcons = document.querySelectorAll(".theme-icon"); - const themeWheelRadius = convertRemToPixels(6); const THUMB_BASE = "{{ .ThumbnailBaseUrl }}"; themeIcons.forEach((icon) => { @@ -294,6 +293,7 @@ const buttonRect = themeButton.getBoundingClientRect(); const buttonCenterX = buttonRect.left + buttonRect.width / 2; const buttonCenterY = buttonRect.top + buttonRect.height / 2; + const themeWheelRadius = convertRemToPixels(6); const margin = 0; -- cgit v1.3.1+13 From e9353a66bf51486f702eaa0f591dea54c9bd4cd1 Mon Sep 17 00:00:00 2001 From: SayaAndy Date: Sun, 17 May 2026 17:13:32 +0700 Subject: refactor: rewrite localization for dynamic set and get feat: medley localnames feat: integrate package minor updates fix: on errors with blog client init write correct type --- go.mod | 14 +- go.sum | 28 ++-- internal/blog/b2.go | 21 ++- internal/blog/client.go | 1 + internal/blog/index.go | 5 +- internal/blog/s3.go | 24 ++- internal/mailer/mailer.go | 16 +- .../handlers/api-v1-email-is-in-verification.go | 3 +- .../api-v1-email-send-verification-code.go | 15 +- internal/router/handlers/api-v1-email-verify.go | 7 +- internal/router/handlers/api-v1-subs-put.go | 7 +- internal/router/handlers/lang-blog.go | 9 +- internal/router/handlers/lang-user-unsubscribe.go | 9 +- internal/router/handlers/lang-user.go | 9 +- internal/router/handlers/lang.go | 9 +- internal/router/router.go | 18 +-- internal/templatemanager/templatemanager.go | 5 + l10n/en.yaml | 79 ++++++++++ l10n/ru.yaml | 79 ++++++++++ l10n/translator-hook.go | 39 +++++ l10n/translator.go | 167 +++++++++++++++++++++ locale/localization.en.yaml | 79 ---------- locale/localization.go | 144 ------------------ locale/localization.ru.yaml | 79 ---------- views/messages/new-post.html | 4 +- views/messages/verify-email.html | 10 +- views/pages/blog-catalogue.html | 18 +-- views/pages/blog-page.html | 6 +- views/pages/global-map.html | 10 +- views/pages/home-page.html | 26 ++-- views/pages/unsubscribe-page.html | 2 +- views/pages/user-page.html | 24 +-- 32 files changed, 532 insertions(+), 434 deletions(-) create mode 100644 l10n/en.yaml create mode 100644 l10n/ru.yaml create mode 100644 l10n/translator-hook.go create mode 100644 l10n/translator.go delete mode 100644 locale/localization.en.yaml delete mode 100644 locale/localization.go delete mode 100644 locale/localization.ru.yaml (limited to 'views/pages/global-map.html') diff --git a/go.mod b/go.mod index 779e641..4511ddb 100644 --- a/go.mod +++ b/go.mod @@ -5,15 +5,15 @@ go 1.26 require ( github.com/Backblaze/blazer v0.7.2 github.com/dgraph-io/ristretto/v2 v2.4.0 - github.com/go-co-op/gocron/v2 v2.21.1 + github.com/go-co-op/gocron/v2 v2.21.2 github.com/go-playground/validator/v10 v10.30.2 github.com/gofiber/fiber/v2 v2.52.13 github.com/golang-migrate/migrate/v4 v4.19.1 github.com/mattn/go-sqlite3 v1.14.44 - github.com/valyala/fasthttp v1.70.0 - github.com/wneessen/go-mail v0.7.2 + github.com/valyala/fasthttp v1.71.0 + github.com/wneessen/go-mail v0.7.3 github.com/yuin/goldmark v1.8.2 - golang.org/x/crypto v0.50.0 + golang.org/x/crypto v0.51.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -40,7 +40,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect - github.com/aws/aws-sdk-go-v2/service/s3 v1.100.1 + github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 github.com/aws/smithy-go v1.25.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect @@ -58,6 +58,6 @@ require ( github.com/mattn/go-runewidth v0.0.23 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/text v0.36.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect ) diff --git a/go.sum b/go.sum index d277091..e6c5284 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/ku github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.100.1 h1:mxuT1xE+dI54NW3RkNjP8DUT5HXqbkiAFvfdyDFwE5c= -github.com/aws/aws-sdk-go-v2/service/s3 v1.100.1/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4= +github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 h1:etqBTKY581iwLL/H/S2sVgk3C9lAsTJFeXWFDsDcWOU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4= github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= @@ -53,8 +53,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/go-co-op/gocron/v2 v2.21.1 h1:QYOK6iOQVCut+jDcs4zRdWRTBHRxRCEeeFi1TnAmgbU= -github.com/go-co-op/gocron/v2 v2.21.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U= +github.com/go-co-op/gocron/v2 v2.21.2 h1:bD8/YwkojYHgXFr3iEulL148KBdTbKVxUZzFKpXcdbY= +github.com/go-co-op/gocron/v2 v2.21.2/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -104,22 +104,22 @@ github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= -github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= -github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8= -github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k= +github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k= +github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA= +github.com/wneessen/go-mail v0.7.3 h1:g3DravXC5SMlVdboFrQA8Jx95A8sOzoBeS5F+vzNRK0= +github.com/wneessen/go-mail v0.7.3/go.mod h1:QGhBX0yNbc1J+Mkjcu7z2rpj4B4l+BmDY8gYznPC9sk= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/blog/b2.go b/internal/blog/b2.go index 8a4d363..f2ac890 100644 --- a/internal/blog/b2.go +++ b/internal/blog/b2.go @@ -2,6 +2,7 @@ package blog import ( "context" + "encoding/json" "fmt" "slices" "strings" @@ -37,6 +38,20 @@ func NewB2Client(cfg *config.StorageConfig) (Client, error) { return &B2Client{b2cl: b2cl, bucket: bucket, prefix: b2cfg.Prefix}, nil } +func (c *B2Client) GetMedleys() ([]MedleyEntry, error) { + idxRaw, err := c.readAll(MedleysIndexFileName) + if err != nil { + return nil, fmt.Errorf("read %s: %w", MedleysIndexFileName, err) + } + + var idx []MedleyEntry + if err := json.Unmarshal(idxRaw, &idx); err != nil { + return nil, fmt.Errorf("unmarshal %s: %w", MedleysIndexFileName, err) + } + + return idx, nil +} + func (c *B2Client) Scan(prefix string) ([]*Page, error) { filePaths := []*Page{} @@ -102,7 +117,11 @@ func (c *B2Client) Scan(prefix string) ([]*Page, error) { } func (c *B2Client) ReadAll(path string) ([]byte, error) { - obj := c.bucket.Object(c.prefix + path) + return c.readAll(c.prefix + path) +} + +func (c *B2Client) readAll(path string) ([]byte, error) { + obj := c.bucket.Object(path) if obj == nil { return nil, fmt.Errorf("failed to reference object in B2 bucket") } diff --git a/internal/blog/client.go b/internal/blog/client.go index 56bbee4..53eb0fd 100644 --- a/internal/blog/client.go +++ b/internal/blog/client.go @@ -17,6 +17,7 @@ type Page struct { type Client interface { Scan(prefix string) ([]*Page, error) + GetMedleys() ([]MedleyEntry, error) ReadAll(path string) ([]byte, error) ReadFrontmatter(path string) (metadata *frontmatter.Metadata, markdown []byte, err error) } diff --git a/internal/blog/index.go b/internal/blog/index.go index 6c1a560..a09b59e 100644 --- a/internal/blog/index.go +++ b/internal/blog/index.go @@ -92,8 +92,9 @@ func (idx *Index) UnmarshalJSON(data []byte) error { } type MedleyEntry struct { - Codename string `json:"codename"` - Content []string `json:"content"` + Codename string `json:"codename"` + Localnames map[string]string `json:"localnames"` + Content []string `json:"content"` } type MedleyPageEntry struct { diff --git a/internal/blog/s3.go b/internal/blog/s3.go index 04fad33..3c1a932 100644 --- a/internal/blog/s3.go +++ b/internal/blog/s3.go @@ -10,14 +10,13 @@ import ( "github.com/SayaAndy/saya-today-web/config" "github.com/SayaAndy/saya-today-web/internal/frontmatter" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/aws/aws-sdk-go-v2/aws" awsconfig "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/s3" ) -const s3ScanConcurrency = 32 - type S3Client struct { prefix string bucketName string @@ -60,6 +59,20 @@ func NewS3Client(cfg *config.StorageConfig) (Client, error) { return &S3Client{s3cfg.Prefix, s3cfg.BucketName, s3cl}, nil } +func (c *S3Client) GetMedleys() ([]MedleyEntry, error) { + idxRaw, err := c.readAll(MedleysIndexFileName) + if err != nil { + return nil, fmt.Errorf("read %s: %w", MedleysIndexFileName, err) + } + + var idx []MedleyEntry + if err := json.Unmarshal(idxRaw, &idx); err != nil { + return nil, fmt.Errorf("unmarshal %s: %w", MedleysIndexFileName, err) + } + + return idx, nil +} + func (c *S3Client) Scan(prefix string) ([]*Page, error) { out, err := c.s3cl.GetObject(context.Background(), &s3.GetObjectInput{ Bucket: aws.String(c.bucketName), @@ -156,6 +169,13 @@ func (c *S3Client) Scan(prefix string) ([]*Page, error) { } } + medleys, _ := c.GetMedleys() + for _, medley := range medleys { + for locale, localname := range medley.Localnames { + l10n.T.SetPath(localname, true, locale, "Medleys", medley.Codename) + } + } + return pages, nil } diff --git a/internal/mailer/mailer.go b/internal/mailer/mailer.go index ffa7b30..33db4f7 100644 --- a/internal/mailer/mailer.go +++ b/internal/mailer/mailer.go @@ -16,7 +16,7 @@ import ( "github.com/SayaAndy/saya-today-web/internal/blog" "github.com/SayaAndy/saya-today-web/internal/templatemanager" - "github.com/SayaAndy/saya-today-web/locale" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/dgraph-io/ristretto/v2" "github.com/gofiber/fiber/v2" "github.com/wneessen/go-mail" @@ -43,8 +43,6 @@ type Mailer struct { hashMap map[string][]byte hashMapMutex sync.RWMutex - - l map[string]*locale.LocaleConfig } type SubscriptionType int @@ -55,7 +53,7 @@ const ( Specific ) -func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string, mailAddress string, username string, password string, salt []byte, localization map[string]*locale.LocaleConfig) (*Mailer, error) { +func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string, mailAddress string, username string, password string, salt []byte) (*Mailer, error) { verificationCodes, err := ristretto.NewCache(&ristretto.Config[uint64, string]{ NumCounters: 10000, MaxCost: 1 << 20, // 1 MB @@ -111,7 +109,7 @@ func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string End time.Time CodeExpiry time.Time }, 0), - l: localization}, nil + }, nil } func (m *Mailer) GetHash(id string) []byte { @@ -261,10 +259,9 @@ func (m *Mailer) SendVerificationCode(userId string, address string, lang string verificationInfo := fmt.Sprintf("%s.%s", base64.RawStdEncoding.EncodeToString([]byte(userId)), base64.RawStdEncoding.EncodeToString([]byte(address))) m.verificationCodes.Set(verificationCode, verificationInfo, int64(len(verificationInfo)+8)) - message.Subject(m.l[lang].Mail.VerifyEmail.Subject) + message.Subject(l10n.T.GetPath(lang, "Mail", "VerifyEmail", "Subject").(string)) msg, err := m.tm.Render("verify-email", fiber.Map{ - "L": m.l[lang], "Lang": lang, "VerificationCode": fmt.Sprintf("%X", verificationCode), "ClientHost": m.clientHost, @@ -499,11 +496,10 @@ rowLoop: rand.Read(unsubscribeCodeBytes) unsubscribeCode := binary.LittleEndian.Uint64(unsubscribeCodeBytes) - unsubscribeFooter := strings.Replace(m.l[post.Lang].Mail.UnsubscribeFooter, "{}", fmt.Sprintf(``, m.clientHost, post.Lang, unsubscribeCode), 1) + unsubscribeFooter := strings.Replace(l10n.T.GetPath(post.Lang, "Mail", "UnsubscribeFooter").(string), "{}", fmt.Sprintf(``, m.clientHost, post.Lang, unsubscribeCode), 1) unsubscribeFooter = strings.Replace(unsubscribeFooter, "{/}", "", 1) msgBody, err := m.tm.Render("new-post", fiber.Map{ - "L": m.l[post.Lang], "Lang": post.Lang, "Post": post, "ClientHost": m.clientHost, @@ -529,7 +525,7 @@ rowLoop: message.SetMessageID() message.SetDate() message.SetBulk() - message.Subject(m.l[post.Lang].Mail.NewPost.Subject) + message.Subject(l10n.T.GetPath(post.Lang, "Mail", "NewPost", "Subject").(string)) message.SetBodyString(mail.TypeTextHTML, string(msgBody)) m.unsubscribeCodes.Set(unsubscribeCode, user.userId, 40) diff --git a/internal/router/handlers/api-v1-email-is-in-verification.go b/internal/router/handlers/api-v1-email-is-in-verification.go index 4f05f48..b8ae162 100644 --- a/internal/router/handlers/api-v1-email-is-in-verification.go +++ b/internal/router/handlers/api-v1-email-is-in-verification.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/gofiber/fiber/v2" ) @@ -49,7 +50,7 @@ func (r *OngoingVerificationHandler) Render(c *fiber.Ctx, supplements *router.Su if !isAllowed { sterileDataset["striked-end-time"] = template.HTMLAttr(fmt.Sprintf("data-striked-end-time=\"%d\"", whenAllowed.UnixMilli())) templateMap["Status"] = "Neutral" - templateMap["Message"] = strings.ReplaceAll(supplements.Localization[lang].UserProfile.DelayTilVerification, "{}", whenAllowed.Format("2006-01-02 15:04:05 MST")) + templateMap["Message"] = strings.ReplaceAll(l10n.T.GetPath(lang, "UserProfile", "DelayTilVerification").(string), "{}", whenAllowed.Format("2006-01-02 15:04:05 MST")) } else { templateMap["Status"] = "OK" templateMap["Message"] = "" diff --git a/internal/router/handlers/api-v1-email-send-verification-code.go b/internal/router/handlers/api-v1-email-send-verification-code.go index 0b53efa..ea8df4d 100644 --- a/internal/router/handlers/api-v1-email-send-verification-code.go +++ b/internal/router/handlers/api-v1-email-send-verification-code.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/gofiber/fiber/v2" ) @@ -49,27 +50,27 @@ func (r *SendVerificationCodeHandler) Render(c *fiber.Ctx, supplements *router.S email := c.FormValue("email") if email == "" { templateMap["Status"] = "Failed" - templateMap["Message"] = supplements.Localization[lang].UserProfile.EmailEmpty + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "EmailEmpty").(string) return fiber.StatusUnprocessableEntity, nil } isTaken, err := supplements.Mailer.MailIsTaken(email) if err != nil { templateMap["Status"] = "Failed" - templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationCodeSendingError + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationCodeSendingError").(string) slog.Error("failed to check if address is already taken", slog.String("error", err.Error())) return fiber.StatusUnprocessableEntity, nil } if isTaken { templateMap["Status"] = "Failed" - templateMap["Message"] = supplements.Localization[lang].UserProfile.EmailTaken + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "EmailTaken").(string) return fiber.StatusUnprocessableEntity, nil } if isAllowed, whenAllowed, _ := supplements.Mailer.IsAllowedToRetryVerification(id); !isAllowed { templateMap["Status"] = "Failed" - templateMap["Message"] = strings.ReplaceAll(supplements.Localization[lang].UserProfile.DelayTilVerification, "{}", whenAllowed.Format("2006-01-02 15:04:05 MST")) + templateMap["Message"] = strings.ReplaceAll(l10n.T.GetPath(lang, "UserProfile", "DelayTilVerification").(string), "{}", whenAllowed.Format("2006-01-02 15:04:05 MST")) templateMap["DataAttributes"] = map[string]any{ "striked-end-time": template.HTMLAttr(fmt.Sprintf("data-striked-end-time=\"%d\"", whenAllowed.UnixMilli())), } @@ -78,20 +79,20 @@ func (r *SendVerificationCodeHandler) Render(c *fiber.Ctx, supplements *router.S if previousEmail, _, _ := supplements.Mailer.GetInfo(supplements.Mailer.GetHash(id)); previousEmail == email { templateMap["Status"] = "Failed" - templateMap["Message"] = supplements.Localization[lang].UserProfile.EmailAlreadyValidated + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "EmailAlreadyValidated").(string) return fiber.StatusUnprocessableEntity, nil } if err = supplements.Mailer.SendVerificationCode(id, email, lang); err != nil { templateMap["Status"] = "Failed" - templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationCodeSendingError + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationCodeSendingError").(string) slog.Error("failed to send a verification code", slog.String("error", err.Error())) return fiber.StatusUnprocessableEntity, nil } _, endTime, codeExpiry := supplements.Mailer.IsAllowedToRetryVerification(id) templateMap["Status"] = "OK" - templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationCodeSent + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationCodeSent").(string) templateMap["DataAttributes"] = map[string]any{ "striked-end-time": template.HTMLAttr(fmt.Sprintf("data-striked-end-time=\"%d\"", endTime.UnixMilli())), "code-expiry-time": template.HTMLAttr(fmt.Sprintf("data-code-expiry-time=\"%d\"", codeExpiry.UnixMilli())), diff --git a/internal/router/handlers/api-v1-email-verify.go b/internal/router/handlers/api-v1-email-verify.go index ace1870..65ad6d9 100644 --- a/internal/router/handlers/api-v1-email-verify.go +++ b/internal/router/handlers/api-v1-email-verify.go @@ -5,6 +5,7 @@ import ( "log/slog" "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/gofiber/fiber/v2" ) @@ -46,19 +47,19 @@ func (r *VerifyCodeHandler) Render(c *fiber.Ctx, supplements *router.Supplements if verificationCode == "" { templateMap["Status"] = "Failed" - templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationEmpty + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationEmpty").(string) return fiber.StatusUnprocessableEntity, nil } if err = supplements.Mailer.Verify(verificationCode, lang); err != nil { slog.Warn("verification code is invalid", slog.String("verification_code", verificationCode), slog.String("error", err.Error())) templateMap["Status"] = "Failed" - templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationFailed + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationFailed").(string) return fiber.StatusUnprocessableEntity, nil } templateMap["Status"] = "OK" - templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationSuccess + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationSuccess").(string) templateMap["DataAttributes"] = map[string]any{ "hide-verification-panel": template.HTMLAttr("data-code-expiry-time=\"true\""), } diff --git a/internal/router/handlers/api-v1-subs-put.go b/internal/router/handlers/api-v1-subs-put.go index 32fba7e..12dc5cf 100644 --- a/internal/router/handlers/api-v1-subs-put.go +++ b/internal/router/handlers/api-v1-subs-put.go @@ -3,6 +3,7 @@ package handlers import ( "github.com/SayaAndy/saya-today-web/internal/mailer" "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/gofiber/fiber/v2" ) @@ -52,18 +53,18 @@ func (r *PutSubsHandler) Render(c *fiber.Ctx, supplements *router.Supplements, l subscriptionTypeEnum = mailer.Specific default: templateMap["Status"] = "Failed" - templateMap["Message"] = supplements.Localization[lang].UserProfile.SubscribeInvalidType + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "SubscribeInvalidType").(string) return fiber.StatusUnprocessableEntity, nil } specificTags := c.FormValue("tags_picked") if err = supplements.Mailer.Subscribe(supplements.Mailer.GetHash(c.IP()), subscriptionTypeEnum, specificTags); err != nil { templateMap["Status"] = "Failed" - templateMap["Message"] = supplements.Localization[lang].UserProfile.FailedToSubscribe + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "FailedToSubscribe").(string) return fiber.StatusUnprocessableEntity, nil } templateMap["Status"] = "OK" - templateMap["Message"] = supplements.Localization[lang].UserProfile.SubscribedSuccessfully + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "SubscribedSuccessfully").(string) return fiber.StatusOK, nil } diff --git a/internal/router/handlers/lang-blog.go b/internal/router/handlers/lang-blog.go index 9f7947b..c31c2eb 100644 --- a/internal/router/handlers/lang-blog.go +++ b/internal/router/handlers/lang-blog.go @@ -11,6 +11,7 @@ import ( "github.com/SayaAndy/saya-today-web/internal/blog" "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/gofiber/fiber/v2" ) @@ -57,8 +58,8 @@ func (r *CatalogueHandler) SitemapInfo(supplements *router.Supplements) []router func (r *CatalogueHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (meta []router.MetaField, err error) { return []router.MetaField{ - {Property: "og:title", Content: supplements.Localization[lang].BlogSearch.Header}, - {Property: "og:description", Content: supplements.Localization[lang].BlogSearch.Description}, + {Property: "og:title", Content: l10n.T.GetPath(lang, "BlogSearch", "Header").(string)}, + {Property: "og:description", Content: l10n.T.GetPath(lang, "BlogSearch", "Description").(string)}, {Property: "og:url", Content: fmt.Sprintf("%s/%s/blog", templateMap["CanonicalEndpoint"], lang)}, {Property: "og:type", Content: "website"}, }, nil @@ -85,14 +86,14 @@ func (r *CatalogueHandler) RenderBody(c *fiber.Ctx, supplements *router.Suppleme templateMap["Tags"] = tagsArray templateMap["QuerySort"] = querySort templateMap["QueryTags"] = strings.Join(queryTags, ",") - templateMap["Title"] = supplements.Localization[lang].BlogSearch.Header + templateMap["Title"] = l10n.T.GetPath(lang, "BlogSearch", "Header").(string) templateMap["PreviousBlogPage"] = previousBlogPage return fiber.StatusOK, nil } func (r *CatalogueHandler) RenderHeader(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) { - templateMap["Title"] = supplements.Localization[lang].BlogSearch.Header + templateMap["Title"] = l10n.T.GetPath(lang, "BlogSearch", "Header").(string) return fiber.StatusOK, nil } diff --git a/internal/router/handlers/lang-user-unsubscribe.go b/internal/router/handlers/lang-user-unsubscribe.go index 4fbd244..e22d854 100644 --- a/internal/router/handlers/lang-user-unsubscribe.go +++ b/internal/router/handlers/lang-user-unsubscribe.go @@ -4,6 +4,7 @@ import ( "log/slog" "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/gofiber/fiber/v2" ) @@ -39,24 +40,24 @@ func (r *UnsubscribeHandler) Render(c *fiber.Ctx, supplements *router.Supplement if unsubscribeCode == "" { statusColor = "0, 0, 255" statusEmoji = "(╭ರ_•́)" - statusText = supplements.Localization[lang].UnsubscribePage.UnsetCode + statusText = l10n.T.GetPath(lang, "UnsubscribePage", "UnsetCode").(string) status = fiber.ErrBadRequest.Code } else if clientError, serverError := supplements.Mailer.Unsubscribe(unsubscribeCode); clientError != nil { slog.Info("got a client error when unsubscribing", slog.String("error", clientError.Error())) statusColor = "255, 0, 0" statusEmoji = "(͠≖~≖ ͡ )" - statusText = supplements.Localization[lang].UnsubscribePage.InvalidCode + statusText = l10n.T.GetPath(lang, "UnsubscribePage", "InvalidCode").(string) status = fiber.ErrBadRequest.Code } else if serverError != nil { slog.Error("got a server error when unsubscribing", slog.String("error", serverError.Error())) statusColor = "255, 128, 0" statusEmoji = "( ˶°ㅁ°) !!" - statusText = supplements.Localization[lang].UnsubscribePage.OnServerError + statusText = l10n.T.GetPath(lang, "UnsubscribePage", "OnServerError").(string) status = fiber.ErrInternalServerError.Code } else { statusColor = "0, 255, 0" statusEmoji = "♡⸜(˶˃ ᵕ ˂˶)⸝♡" - statusText = supplements.Localization[lang].UnsubscribePage.Success + statusText = l10n.T.GetPath(lang, "UnsubscribePage", "Success").(string) status = fiber.StatusOK } diff --git a/internal/router/handlers/lang-user.go b/internal/router/handlers/lang-user.go index 0dda529..085a9aa 100644 --- a/internal/router/handlers/lang-user.go +++ b/internal/router/handlers/lang-user.go @@ -6,6 +6,7 @@ import ( "github.com/SayaAndy/saya-today-web/internal/mailer" "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/gofiber/fiber/v2" ) @@ -40,15 +41,15 @@ func (r *UserHandler) ToValidateLang() router.LangSetting { func (r *UserHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (meta []router.MetaField, err error) { return []router.MetaField{ {Name: "robots", Content: "noindex,nofollow"}, - {Property: "og:title", Content: supplements.Localization[lang].UserProfile.Header}, - {Property: "og:description", Content: supplements.Localization[lang].UserProfile.Description}, + {Property: "og:title", Content: l10n.T.GetPath(lang, "UserProfile", "Header").(string)}, + {Property: "og:description", Content: l10n.T.GetPath(lang, "UserProfile", "Description").(string)}, {Property: "og:url", Content: fmt.Sprintf("%s/%s/user", templateMap["CanonicalEndpoint"], lang)}, {Property: "og:type", Content: "website"}, }, nil } func (r *UserHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) { - templateMap["Title"] = supplements.Localization[lang].UserProfile.Header + templateMap["Title"] = l10n.T.GetPath(lang, "UserProfile", "Header").(string) email, _, err := supplements.Mailer.GetInfo(supplements.Mailer.GetHash(c.IP())) if err != nil { @@ -83,6 +84,6 @@ func (r *UserHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements, } func (r *UserHandler) RenderHeader(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) { - templateMap["Title"] = supplements.Localization[lang].UserProfile.Header + templateMap["Title"] = l10n.T.GetPath(lang, "UserProfile", "Header").(string) return fiber.StatusOK, nil } diff --git a/internal/router/handlers/lang.go b/internal/router/handlers/lang.go index 25b5036..cc0b33b 100644 --- a/internal/router/handlers/lang.go +++ b/internal/router/handlers/lang.go @@ -6,6 +6,7 @@ import ( "time" "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/gofiber/fiber/v2" ) @@ -51,8 +52,8 @@ func (r *HomeHandler) SitemapInfo(supplements *router.Supplements) []router.Site func (r *HomeHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (meta []router.MetaField, err error) { return []router.MetaField{ - {Property: "og:title", Content: supplements.Localization[lang].HomePage.Header}, - {Property: "og:description", Content: supplements.Localization[lang].HomePage.HomePageDescription}, + {Property: "og:title", Content: l10n.T.GetPath(lang, "HomePage", "Header").(string)}, + {Property: "og:description", Content: l10n.T.GetPath(lang, "HomePage", "HomePageDescription").(string)}, {Property: "og:image", Content: fmt.Sprintf( supplements.PhotoStorage.HomePageGifs.BaseUrl, supplements.PhotoStorage.HomePageGifs.Indexes[rand.Int()%len(supplements.PhotoStorage.HomePageGifs.Indexes)], @@ -63,7 +64,7 @@ func (r *HomeHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lan } func (r *HomeHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) { - templateMap["Title"] = supplements.Localization[lang].HomePage.Header + templateMap["Title"] = l10n.T.GetPath(lang, "HomePage", "Header").(string) templateMap["FilledHeartCount"] = uint(40) templateMap["OutlineHeartCount"] = uint(40) templateMap["GifUrl"] = fmt.Sprintf( @@ -75,6 +76,6 @@ func (r *HomeHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements, } func (r *HomeHandler) RenderHeader(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) { - templateMap["Title"] = supplements.Localization[lang].HomePage.Header + templateMap["Title"] = l10n.T.GetPath(lang, "HomePage", "Header").(string) return fiber.StatusOK, nil } diff --git a/internal/router/router.go b/internal/router/router.go index a1208e0..43d6138 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -23,7 +23,6 @@ import ( "github.com/SayaAndy/saya-today-web/internal/mailer" "github.com/SayaAndy/saya-today-web/internal/tailwind" "github.com/SayaAndy/saya-today-web/internal/templatemanager" - "github.com/SayaAndy/saya-today-web/locale" "github.com/dgraph-io/ristretto/v2" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/compress" @@ -95,7 +94,6 @@ type Route interface { type Supplements struct { DB *sql.DB BlogClient blog.Client - Localization map[string]*locale.LocaleConfig AvailableLanguages []config.AvailableLanguageConfig ClientCache *ClientCache PageCache *ristretto.Cache[string, []byte] @@ -149,16 +147,7 @@ func NewRouter(cfg *config.Config) (*Router, error) { supplements.BlogClient, err = blog.NewClientMap[cfg.BlogPages.Storage.Type](&cfg.BlogPages.Storage) if err != nil { - return nil, fmt.Errorf("fail to initialize b2 client: %w", err) - } - - supplements.Localization = make(map[string]*locale.LocaleConfig, len(cfg.AvailableLanguages)) - for _, lang := range cfg.AvailableLanguages { - localeCfg, err := locale.InitConfig(cfg.LocalePath + lang.LocFile) - if err != nil { - return nil, fmt.Errorf("fail to initialize a locale: %w", err) - } - supplements.Localization[lang.Name] = localeCfg + return nil, fmt.Errorf("fail to initialize blog client: type %s: %w", cfg.BlogPages.Storage.Type, err) } supplements.MarkdownRenderer = goldmark.New( @@ -200,7 +189,7 @@ func NewRouter(cfg *config.Config) (*Router, error) { } supplements.Mailer, err = mailer.NewMailer(supplements.DB, cfg.Mail.ClientHost, cfg.Mail.MailHost, - cfg.Mail.PublicName, cfg.Mail.MailAddress, cfg.Mail.Username, cfg.Mail.Password, []byte(cfg.Mail.Salt), supplements.Localization) + cfg.Mail.PublicName, cfg.Mail.MailAddress, cfg.Mail.Username, cfg.Mail.Password, []byte(cfg.Mail.Salt)) if err != nil { return nil, fmt.Errorf("fail to initialize mailer: %w", err) } @@ -314,7 +303,6 @@ func (r *Router) InitRoutes() (err error) { } defaultMap := fiber.Map{ - "L": r.supplements.Localization[lang], "Lang": lang, "Path": trimmedPath, "QueryString": queryString, @@ -467,7 +455,6 @@ func (r *Router) generalPage(c *fiber.Ctx, route Route, lang string) error { } valueMap := fiber.Map{ - "L": r.supplements.Localization[lang], "Lang": lang, "Path": trimmedPath, "QueryString": queryString, @@ -593,7 +580,6 @@ func (r *Router) generalPageSegment(c *fiber.Ctx, part string) error { var statusCode int defaultMap := fiber.Map{ - "L": r.supplements.Localization[lang], "Lang": lang, "Path": strings.Trim(path, "/"), "QueryString": queryString, diff --git a/internal/templatemanager/templatemanager.go b/internal/templatemanager/templatemanager.go index 9d14e15..5c84418 100644 --- a/internal/templatemanager/templatemanager.go +++ b/internal/templatemanager/templatemanager.go @@ -8,6 +8,8 @@ import ( "path/filepath" "strings" "time" + + "github.com/SayaAndy/saya-today-web/l10n" ) type TemplateManager struct { @@ -38,6 +40,9 @@ var templateFuncMap = template.FuncMap{ "fdiv": func(a, b int) float64 { return float64(a) / float64(b) }, + "l": func(path ...any) any { + return l10n.T.GetPath(path...) + }, } func NewTemplateManager(templates ...TemplateManagerTemplates) (*TemplateManager, error) { diff --git a/l10n/en.yaml b/l10n/en.yaml new file mode 100644 index 0000000..9571aca --- /dev/null +++ b/l10n/en.yaml @@ -0,0 +1,79 @@ +TagsLabel: "Tags:" +BlogSearch: + Header: "Blog Search" + Description: "The Saya Blog posts' list" + TagsHeader: "Tags" + OrderByHeader: "Order by..." + TitleOrdered: "Title" + ActionDateOrdered: "Action Date" + PublicationDateOrdered: "Publication Date" + ChooseAllTags: "Choose All" +GlobalMap: + Header: "Global Map" + Description: "Global map with all the tags of places I've been to" +HomePage: + Header: "Home Page" + Contacts: "Contacts" + DidYouKnowThat: "Did you know, that..." + SidebarDescription: "Describing the Sidebar" + BackDescription: "This button will lead back to higher page (like Home => Language Pick, Blog Search => Home)" + HomePageDescription: "Home Page -- where we are right now!" + BlogSearchDescription: "Blog Search. You can find my travel notes there and, maybe, something else!" + MarkerMapDescription: "Map with markers of my travel notes -- a convinient way to find an interesting region and find related posts!" + ThemeSwitchDescription: "Theme Palette lets you change the theme of the site -- there are 2 dark and 3 bright themes currently!" + AccountDescription: "This is where you can subscribe to any blog update via E-mail!" + Hymn1: "We welcome you to Sayasite!" + Hymn2: "We've been expecting you!" + Hymn3: "You bring such joy in Sayasite!" + Hymn4: "No matter where you roam, know our love is true!~" +UnsubscribePage: + Header: "Cancelling the Subscription" + UnsetCode: "Your cancel code is not set. You just having a walk around the site?" + InvalidCode: "Your cancel code could not be verified. Maybe, the code expired after a day. Or maybe, you've already unsubscribed." + OnServerError: "We couldn't unsubscribe you, the problem is on our side! We will solve this problem as soon as possible." + Success: "Cancelled your subscription successfully! We do hope we will engage you again someday." +Metadata: + Published: "Published" + Action: "Took place on" +Mail: + UnsubscribeFooter: "If this letter got you in a bad mood, you can unsubscribe from my blog by {}this link{/}." + VerifyEmail: + Subject: "Verification Link for SAYA.UZ is waiting for you!" + Welcome: "Greetings!" + Intro: "There are 2 ways for us to validate your profile:" + GotoLink: "Go by this link:" + InputCode: "Or input this code on the personal page:" + IfRandom: "If you are unsure what this letter this, you can ignore it, and also, if you are interested, you may visit my site :)" + NewPost: + Subject: "A new post arrived at SAYA.UZ!" + Intro: "A new post came!" + CapturedOn: "Captured on" +UserProfile: + Header: "Personal Settings" + Description: "Set and verify your e-mail here, as well as subscribe to favorite blog tags" + EmailHeader: "E-Mail" + VerificationCodeHeader: "Verification Code" + TypeEmail: "Please type your email" + SendCodeButton: "Send Code" + VerifyButton: "Verify" + VerificationCodeSent: "Verification code sent! Please check your e-mail. Your next verification attempt will be possible on {}." + DelayTilVerification: "You have already requested the verification before! Please wait until {} for a new verification request." + FailedEmailRender: "Failed to render the email status message, please ask administrator for a more detailed cause." + VerificationCodeSendingError: "Failed to send a verification code, please ask administrator for a more detailed cause." + EmailAlreadyValidated: "You already have this E-Mail validated." + EmailTaken: "This E-Mail is already taken." + EmailEmpty: "E-Mail is unset or empty." + VerificationSuccess: "Success! You are now able to subscribe with your new e-mail." + VerificationFailed: "Verification code is invalid, please ask administrator for a more detailed cause." + VerificationEmpty: "Verification code is unset or empty." + SubscriptionHeader: "Subscribe to..." + TagsNone: "None" + TagsAll: "All" + TagsSpecific: "Specific" + TagDoesNotExist: 'The tag "{}" does not exist on the site.' + TagAlreadyAdded: 'The tag "{}" is already in your subscribed list.' + SaveButton: "Save" + SubscribeInvalidType: "Invalid subscription type." + FailedToSubscribe: "Failed to subscribe by new settings, please ask administrator for a more detailed cause." + SubscribedSuccessfully: "Successfully subscribed you by new settings!" + RefreshPage: "Please refresh the profile page on the device, where you requested the code." diff --git a/l10n/ru.yaml b/l10n/ru.yaml new file mode 100644 index 0000000..1818998 --- /dev/null +++ b/l10n/ru.yaml @@ -0,0 +1,79 @@ +TagsLabel: "Тэги:" +BlogSearch: + Header: "Поиск по блогу" + Description: "Каталог записей Saya Blog" + TagsHeader: "Тэги" + OrderByHeader: "Упорядочить по..." + TitleOrdered: "названию" + ActionDateOrdered: "дате действия" + PublicationDateOrdered: "времени публикации" + ChooseAllTags: "Выбрать все" +GlobalMap: + Header: "Глобальная карта" + Description: "Глобальная карта с метками мест, где я побывал" +HomePage: + Header: "Домашняя страница" + Contacts: "Контакты" + DidYouKnowThat: "А вы знали, что..." + SidebarDescription: "Поясняем за сайдбар" + BackDescription: "Эта кнопка позволяет идти на страницу вверх (т.е. Домашняя страница => Выбор языка, Поиск по блогу => Домашняя страница)" + HomePageDescription: "Домашняя страница -- где мы сейчас и находимся!" + BlogSearchDescription: "Поиск по блогу. Там размещены мои путевые заметки и, может, что-то ещё!" + MarkerMapDescription: "Карта с маркерами моих путевых заметок, -- удобный способ найти интересный регион и перейти на заинтересовавшие посты!" + ThemeSwitchDescription: "Позволяет поменять тему сайта -- есть 3 светлые и 2 тёмные темы!" + AccountDescription: "Здесь можно подписаться на обновления в блоге, используя электронную почту!" + Hymn1: "Приветствуем Вас на Саясайте!" + Hymn2: "Мы Вас столько ждали!" + Hymn3: "Столько радости от Вас на Саясайте!" + Hymn4: "Где бы Вы не были, знайте, -- наша любовь крепка!" +UnsubscribePage: + Header: "Отписка" + UnsetCode: "Код для отписки не проставлен. Вы случайно не заблудились?" + InvalidCode: "Код для отписки не подтверждён. Может, истекло время в сутки для кода. А может, вы уже успели отписаться." + OnServerError: "Мы не смогли вас отписать, проблема на нашей стороне! В скором времени займёмся проблемой." + Success: "Вы отписались от рассылки! Надеюсь, мы ещё сможем вас заинтересовать." +Metadata: + Published: "Опубликовано" + Action: "Время действия" +Mail: + UnsubscribeFooter: "Если данное письмо пришло вам случайно, либо вы хотите отписаться, можете перейти по {}этой ссылке{/}." + VerifyEmail: + Subject: "Ссылка для верификации для подтверждения вашей почты SAYA.UZ" + Welcome: "Приветствую!" + Intro: "Есть 2 способа, чтобы подтвердить свою электронную почту:" + GotoLink: "Перейти по этой ссылке:" + InputCode: "Либо ввести этот код на странице подписок:" + IfRandom: "Если ты не понимаешь, что это за спам, можешь проигнорировать это письмо, а ещё, если интересно, можешь посетить мой сайт :)" + NewPost: + Subject: "Новый пост на SAYA.UZ!" + Intro: "Вышел новый пост!" + CapturedOn: "Снималось" +UserProfile: + Header: "Личные настройки" + Description: "Здесь можно настроить и верифицировать свою электронную почту, а также подписаться на избранные темы блога" + EmailHeader: "E-Mail" + VerificationCodeHeader: "Код верификации" + TypeEmail: "Пожалуйста, введите свою почту" + SendCodeButton: "Отправить код" + VerifyButton: "Верифицировать" + VerificationCodeSent: "Выслали вам код верификации! Проверьте свою почту. Следующая попытка верификации будет возможна {}." + DelayTilVerification: "Вы уже запрашивали недавно верификацию! Дождитесь {} для новой попытки." + FailedEmailRender: "Неудачная попытка создать электронное письмо, более подробно спрашивайте у администратора." + VerificationCodeSendingError: "Неудачная попытка прислать код для верификации, более подробно спрашивайте у администратора." + EmailAlreadyValidated: "Эту почту вы уже подтверждали." + EmailTaken: "Эта почта уже занята." + EmailEmpty: "Почта не введена." + VerificationSuccess: "Поздравляем! Теперь по этой почте можно подписаться на блог." + VerificationFailed: "Код для верификации невалиден, при вопросах, пожалуйста, обращайтесь к администратору." + VerificationEmpty: "Код для верификации не введён." + SubscriptionHeader: "Подписаться на..." + TagsNone: "Ничего" + TagsAll: "Всё" + TagsSpecific: "Определённое" + TagDoesNotExist: 'Тэга "{}" нет на сайте.' + TagAlreadyAdded: 'Тэг "{}" уже у вас добавлен.' + SaveButton: "Сохранить" + SubscribeInvalidType: "Неизвестный тип подписки." + FailedToSubscribe: "Неудачная попытка записать новую подписку, более подробно спрашивайте у администратора." + SubscribedSuccessfully: "Вы успешно подписались по новым настройкам!" + RefreshPage: "Пожалуйста, обновите страницу с профилем на устройстве, откуда запрашивали код." diff --git a/l10n/translator-hook.go b/l10n/translator-hook.go new file mode 100644 index 0000000..ff5837c --- /dev/null +++ b/l10n/translator-hook.go @@ -0,0 +1,39 @@ +package l10n + +var hooks map[string]*TranslatorHook + +type TranslatorHook struct { + t *Translator + prefix string +} + +func NewTranslatorHook(prefix string) *TranslatorHook { + if _, ok := hooks[prefix]; !ok { + hooks[prefix] = &TranslatorHook{T, prefix} + } + return hooks[prefix] +} + +func (h *TranslatorHook) Set(key string, val any, toOverwrite bool) (alreadySet bool, err error) { + return h.t.Set(h.prefix+key, val, toOverwrite) +} + +func (h *TranslatorHook) SetPath(val any, toOverwrite bool, path ...any) (alreadySet bool, err error) { + return h.t.SetPath(val, toOverwrite, append([]any{h.prefix}, path...)...) +} + +func (h *TranslatorHook) Get(key string) any { + return h.t.Get(h.prefix + "." + key) +} + +func (h *TranslatorHook) GetPath(path ...any) any { + return h.t.GetPath(append([]any{h.prefix}, path...)...) +} + +func (h *TranslatorHook) GetDefault(key string, def any) any { + return h.t.GetDefault(h.prefix+"."+key, def) +} + +func (h *TranslatorHook) GetDefaultPath(def any, path ...any) any { + return h.t.GetDefaultPath(def, append([]any{h.prefix}, path...)...) +} diff --git a/l10n/translator.go b/l10n/translator.go new file mode 100644 index 0000000..4d4b4eb --- /dev/null +++ b/l10n/translator.go @@ -0,0 +1,167 @@ +package l10n + +import ( + "errors" + "fmt" + "log/slog" + "os" + "strings" + "sync" + + "gopkg.in/yaml.v3" +) + +var T *Translator + +func init() { + var err error + if T, err = NewTranslator("ru", "en"); err != nil { + slog.Error("fail to load localization", slog.String("error", err.Error())) + os.Exit(1) + } +} + +type Translator struct { + l map[string]any + mu sync.RWMutex +} + +func NewTranslator(locales ...string) (*Translator, error) { + t := &Translator{l: make(map[string]any)} + errs := make([]error, 0) + + for _, locale := range locales { + contentBytes, err := os.ReadFile(fmt.Sprintf("l10n/%s.yaml", locale)) + if err != nil { + errs = append(errs, fmt.Errorf("opening one of files: l10n/%s.yaml: %w", locale, err)) + continue + } + + var content map[string]any + if err := yaml.Unmarshal(contentBytes, &content); err != nil { + errs = append(errs, fmt.Errorf("parsing one of files: l10n/%s.yaml: %w", locale, err)) + continue + } + + errs = append(errs, t.initMap("."+locale, content)...) + } + + return t, errors.Join(errs...) +} + +func (t *Translator) Set(key string, val any, toOverwrite bool) (alreadySet bool, err error) { + t.mu.RLock() + if _, alreadySet = t.l[key]; !alreadySet || (alreadySet && toOverwrite) { + t.mu.RUnlock() + return alreadySet, errors.Join(t.setSafe(key, val)...) + } + t.mu.RUnlock() + return alreadySet, nil +} + +func (t *Translator) SetPath(val any, toOverwrite bool, path ...any) (alreadySet bool, err error) { + key, err := formatPath(path...) + if err != nil { + return false, err + } + return t.Set(key, val, toOverwrite) +} + +func (t *Translator) Get(key string) any { + return t.GetDefault(key, key[strings.LastIndex(key, ".")+1:]) +} + +func (t *Translator) GetPath(path ...any) any { + return t.GetDefaultPath(path[len(path)-1], path...) +} + +func (t *Translator) GetDefault(key string, def any) any { + t.mu.RLock() + defer t.mu.RUnlock() + if v, ok := t.l[key]; ok { + return v + } + slog.Warn("failed to find localization for a key", slog.String("key", key)) + return def +} + +func (t *Translator) GetDefaultPath(def any, path ...any) any { + t.mu.RLock() + defer t.mu.RUnlock() + key, err := formatPath(path...) + if err != nil { + slog.Error("get localized value by path: path is invalid", slog.String("error", err.Error())) + return def + } + return t.GetDefault(key, def) +} + +func (t *Translator) initSlice(parent string, level []any) []error { + errs := make([]error, 0) + for i, v := range level { + newParent := parent + fmt.Sprintf("[%d]", i) + t.setSafe(newParent, v) + } + return errs +} + +func (t *Translator) initMap(parent string, level map[string]any) []error { + errs := make([]error, 0) + for k, v := range level { + key, err := formatPath(k) + if err != nil { + errs = append(errs, err) + continue + } + + errs = append(errs, t.setSafe(parent+key, v)...) + } + return errs +} + +func (t *Translator) setSafe(k string, v any) []error { + errs := make([]error, 0) + t.mu.Lock() + switch vv := v.(type) { + case int: + t.l[k] = vv + t.mu.Unlock() + case float64: + t.l[k] = vv + t.mu.Unlock() + case string: + t.l[k] = vv + t.mu.Unlock() + case []any: + t.mu.Unlock() + errs = append(errs, t.initSlice(k, vv)...) + case map[string]any: + t.mu.Unlock() + errs = append(errs, t.initMap(k, vv)...) + default: + t.mu.Unlock() + errs = append(errs, fmt.Errorf("invalid value type: %s, %v", k, vv)) + } + return errs +} + +func formatPath(path ...any) (string, error) { + var key string + for _, part := range path { + switch k := part.(type) { + case int: + key += fmt.Sprintf("[%d]", k) + case string: + key += "." + strings.ReplaceAll(k, ".", "\\.") + case []any: + add, err := formatPath(k...) + if err != nil { + return "", fmt.Errorf("while formatting nested slice: %w", err) + } + key += add + default: + return "", fmt.Errorf("invalid key part: after %s follows %v", key, k) + } + } + return key, nil +} diff --git a/locale/localization.en.yaml b/locale/localization.en.yaml deleted file mode 100644 index 9571aca..0000000 --- a/locale/localization.en.yaml +++ /dev/null @@ -1,79 +0,0 @@ -TagsLabel: "Tags:" -BlogSearch: - Header: "Blog Search" - Description: "The Saya Blog posts' list" - TagsHeader: "Tags" - OrderByHeader: "Order by..." - TitleOrdered: "Title" - ActionDateOrdered: "Action Date" - PublicationDateOrdered: "Publication Date" - ChooseAllTags: "Choose All" -GlobalMap: - Header: "Global Map" - Description: "Global map with all the tags of places I've been to" -HomePage: - Header: "Home Page" - Contacts: "Contacts" - DidYouKnowThat: "Did you know, that..." - SidebarDescription: "Describing the Sidebar" - BackDescription: "This button will lead back to higher page (like Home => Language Pick, Blog Search => Home)" - HomePageDescription: "Home Page -- where we are right now!" - BlogSearchDescription: "Blog Search. You can find my travel notes there and, maybe, something else!" - MarkerMapDescription: "Map with markers of my travel notes -- a convinient way to find an interesting region and find related posts!" - ThemeSwitchDescription: "Theme Palette lets you change the theme of the site -- there are 2 dark and 3 bright themes currently!" - AccountDescription: "This is where you can subscribe to any blog update via E-mail!" - Hymn1: "We welcome you to Sayasite!" - Hymn2: "We've been expecting you!" - Hymn3: "You bring such joy in Sayasite!" - Hymn4: "No matter where you roam, know our love is true!~" -UnsubscribePage: - Header: "Cancelling the Subscription" - UnsetCode: "Your cancel code is not set. You just having a walk around the site?" - InvalidCode: "Your cancel code could not be verified. Maybe, the code expired after a day. Or maybe, you've already unsubscribed." - OnServerError: "We couldn't unsubscribe you, the problem is on our side! We will solve this problem as soon as possible." - Success: "Cancelled your subscription successfully! We do hope we will engage you again someday." -Metadata: - Published: "Published" - Action: "Took place on" -Mail: - UnsubscribeFooter: "If this letter got you in a bad mood, you can unsubscribe from my blog by {}this link{/}." - VerifyEmail: - Subject: "Verification Link for SAYA.UZ is waiting for you!" - Welcome: "Greetings!" - Intro: "There are 2 ways for us to validate your profile:" - GotoLink: "Go by this link:" - InputCode: "Or input this code on the personal page:" - IfRandom: "If you are unsure what this letter this, you can ignore it, and also, if you are interested, you may visit my site :)" - NewPost: - Subject: "A new post arrived at SAYA.UZ!" - Intro: "A new post came!" - CapturedOn: "Captured on" -UserProfile: - Header: "Personal Settings" - Description: "Set and verify your e-mail here, as well as subscribe to favorite blog tags" - EmailHeader: "E-Mail" - VerificationCodeHeader: "Verification Code" - TypeEmail: "Please type your email" - SendCodeButton: "Send Code" - VerifyButton: "Verify" - VerificationCodeSent: "Verification code sent! Please check your e-mail. Your next verification attempt will be possible on {}." - DelayTilVerification: "You have already requested the verification before! Please wait until {} for a new verification request." - FailedEmailRender: "Failed to render the email status message, please ask administrator for a more detailed cause." - VerificationCodeSendingError: "Failed to send a verification code, please ask administrator for a more detailed cause." - EmailAlreadyValidated: "You already have this E-Mail validated." - EmailTaken: "This E-Mail is already taken." - EmailEmpty: "E-Mail is unset or empty." - VerificationSuccess: "Success! You are now able to subscribe with your new e-mail." - VerificationFailed: "Verification code is invalid, please ask administrator for a more detailed cause." - VerificationEmpty: "Verification code is unset or empty." - SubscriptionHeader: "Subscribe to..." - TagsNone: "None" - TagsAll: "All" - TagsSpecific: "Specific" - TagDoesNotExist: 'The tag "{}" does not exist on the site.' - TagAlreadyAdded: 'The tag "{}" is already in your subscribed list.' - SaveButton: "Save" - SubscribeInvalidType: "Invalid subscription type." - FailedToSubscribe: "Failed to subscribe by new settings, please ask administrator for a more detailed cause." - SubscribedSuccessfully: "Successfully subscribed you by new settings!" - RefreshPage: "Please refresh the profile page on the device, where you requested the code." diff --git a/locale/localization.go b/locale/localization.go deleted file mode 100644 index 87b8ea1..0000000 --- a/locale/localization.go +++ /dev/null @@ -1,144 +0,0 @@ -package locale - -import ( - "os" - - "github.com/go-playground/validator/v10" - "gopkg.in/yaml.v3" -) - -type LocaleConfig struct { - TagsLabel string `yaml:"TagsLabel" json:"TagsLabel"` - BlogSearch BlogSearchConfig `yaml:"BlogSearch" json:"BlogSearch"` - GlobalMap GlobalMapConfig `yaml:"GlobalMap" json:"GlobalMap"` - HomePage HomePageConfig `yaml:"HomePage" json:"HomePage"` - UnsubscribePage UnsubscribePageConfig `yaml:"UnsubscribePage" json:"UnsubscribePage"` - Metadata MetadataConfig `yaml:"Metadata" json:"Metadata"` - Mail MailConfig `yaml:"Mail" json:"Mail"` - UserProfile UserProfileConfig `yaml:"UserProfile" json:"UserProfile"` -} - -type BlogSearchConfig struct { - Header string `yaml:"Header" json:"Header"` - Description string `yaml:"Description" json:"Description"` - TagsHeader string `yaml:"TagsHeader" json:"TagsHeader"` - OrderByHeader string `yaml:"OrderByHeader" json:"OrderByHeader"` - TitleOrdered string `yaml:"TitleOrdered" json:"TitleOrdered"` - ActionDateOrdered string `yaml:"ActionDateOrdered" json:"ActionDateOrdered"` - PublicationDateOrdered string `yaml:"PublicationDateOrdered" json:"PublicationDateOrdered"` - ChooseAllTags string `yaml:"ChooseAllTags" json:"ChooseAllTags"` -} - -type GlobalMapConfig struct { - Header string `yaml:"Header" json:"Header"` - Description string `yaml:"Description" json:"Description"` -} - -type HomePageConfig struct { - Header string `yaml:"Header" json:"Header"` - Contacts string `yaml:"Contacts" json:"Contacts"` - DidYouKnowThat string `yaml:"DidYouKnowThat" json:"DidYouKnowThat"` - SidebarDescription string `yaml:"SidebarDescription" json:"SidebarDescription"` - BackDescription string `yaml:"BackDescription" json:"BackDescription"` - HomePageDescription string `yaml:"HomePageDescription" json:"HomePageDescription"` - BlogSearchDescription string `yaml:"BlogSearchDescription" json:"BlogSearchDescription"` - MarkerMapDescription string `yaml:"MarkerMapDescription" json:"MarkerMapDescription"` - ThemeSwitchDescription string `yaml:"ThemeSwitchDescription" json:"ThemeSwitchDescription"` - AccountDescription string `yaml:"AccountDescription" json:"AccountDescription"` - Hymn1 string `yaml:"Hymn1" json:"Hymn1"` - Hymn2 string `yaml:"Hymn2" json:"Hymn2"` - Hymn3 string `yaml:"Hymn3" json:"Hymn3"` - Hymn4 string `yaml:"Hymn4" json:"Hymn4"` -} - -type UnsubscribePageConfig struct { - Header string `yaml:"Header" json:"Header"` - UnsetCode string `yaml:"UnsetCode" json:"UnsetCode"` - InvalidCode string `yaml:"InvalidCode" json:"InvalidCode"` - OnServerError string `yaml:"OnServerError" json:"OnServerError"` - Success string `yaml:"Success" json:"Success"` -} - -type MetadataConfig struct { - Action string `yaml:"Action" json:"Action"` - Published string `yaml:"Published" json:"Published"` -} - -type MailConfig struct { - UnsubscribeFooter string `yaml:"UnsubscribeFooter" json:"UnsubscribeFooter"` - VerifyEmail VerifyEmailConfig `yaml:"VerifyEmail" json:"VerifyEmail"` - NewPost NewPostConfig `yaml:"NewPost" json:"NewPost"` -} - -type VerifyEmailConfig struct { - Subject string `yaml:"Subject" json:"Subject"` - Welcome string `yaml:"Welcome" json:"Welcome"` - Intro string `yaml:"Intro" json:"Intro"` - GotoLink string `yaml:"GotoLink" json:"GotoLink"` - InputCode string `yaml:"InputCode" json:"InputCode"` - IfRandom string `yaml:"IfRandom" json:"IfRandom"` -} - -type NewPostConfig struct { - Subject string `yaml:"Subject" json:"Subject"` - CapturedOn string `yaml:"CapturedOn" json:"CapturedOn"` - Intro string `yaml:"Intro" json:"Intro"` -} - -type UserProfileConfig struct { - Header string `yaml:"Header" json:"Header"` - Description string `yaml:"Description" json:"Description"` - EmailHeader string `yaml:"EmailHeader" json:"EmailHeader"` - VerificationCodeHeader string `yaml:"VerificationCodeHeader" json:"VerificationCodeHeader"` - TypeEmail string `yaml:"TypeEmail" json:"TypeEmail"` - SendCodeButton string `yaml:"SendCodeButton" json:"SendCodeButton"` - VerifyButton string `yaml:"VerifyButton" json:"VerifyButton"` - VerificationCodeSent string `yaml:"VerificationCodeSent" json:"VerificationCodeSent"` - DelayTilVerification string `yaml:"DelayTilVerification" json:"DelayTilVerification"` - FailedEmailRender string `yaml:"FailedEmailRender" json:"FailedEmailRender"` - VerificationCodeSendingError string `yaml:"VerificationCodeSendingError" json:"VerificationCodeSendingError"` - EmailAlreadyValidated string `yaml:"EmailAlreadyValidated" json:"EmailAlreadyValidated"` - EmailTaken string `yaml:"EmailTaken" json:"EmailTaken"` - EmailEmpty string `yaml:"EmailEmpty" json:"EmailEmpty"` - VerificationSuccess string `yaml:"VerificationSuccess" json:"VerificationSuccess"` - VerificationFailed string `yaml:"VerificationFailed" json:"VerificationFailed"` - VerificationEmpty string `yaml:"VerificationEmpty" json:"VerificationEmpty"` - SubscriptionHeader string `yaml:"SubscriptionHeader" json:"SubscriptionHeader"` - TagsNone string `yaml:"TagsNone" json:"TagsNone"` - TagsAll string `yaml:"TagsAll" json:"TagsAll"` - TagsSpecific string `yaml:"TagsSpecific" json:"TagsSpecific"` - TagDoesNotExist string `yaml:"TagDoesNotExist" json:"TagDoesNotExist"` - TagAlreadyAdded string `yaml:"TagAlreadyAdded" json:"TagAlreadyAdded"` - SaveButton string `yaml:"SaveButton" json:"SaveButton"` - SubscribeInvalidType string `yaml:"SubscribeInvalidType" json:"SubscribeInvalidType"` - FailedToSubscribe string `yaml:"FailedToSubscribe" json:"FailedToSubscribe"` - SubscribedSuccessfully string `yaml:"SubscribedSuccessfully" json:"SubscribedSuccessfully"` - RefreshPage string `yaml:"RefreshPage" json:"RefreshPage"` -} - -func LoadConfig(path string, config *LocaleConfig) error { - fileBytes, err := os.ReadFile(path) - if err != nil { - return err - } - - if err = yaml.Unmarshal(fileBytes, config); err != nil { - return err - } - - return nil -} - -func InitConfig(path string) (*LocaleConfig, error) { - config := &LocaleConfig{} - if err := LoadConfig(path, config); err != nil { - return nil, err - } - - validate := validator.New(validator.WithRequiredStructEnabled()) - if err := validate.Struct(config); err != nil { - return nil, err - } - - return config, nil -} diff --git a/locale/localization.ru.yaml b/locale/localization.ru.yaml deleted file mode 100644 index 1818998..0000000 --- a/locale/localization.ru.yaml +++ /dev/null @@ -1,79 +0,0 @@ -TagsLabel: "Тэги:" -BlogSearch: - Header: "Поиск по блогу" - Description: "Каталог записей Saya Blog" - TagsHeader: "Тэги" - OrderByHeader: "Упорядочить по..." - TitleOrdered: "названию" - ActionDateOrdered: "дате действия" - PublicationDateOrdered: "времени публикации" - ChooseAllTags: "Выбрать все" -GlobalMap: - Header: "Глобальная карта" - Description: "Глобальная карта с метками мест, где я побывал" -HomePage: - Header: "Домашняя страница" - Contacts: "Контакты" - DidYouKnowThat: "А вы знали, что..." - SidebarDescription: "Поясняем за сайдбар" - BackDescription: "Эта кнопка позволяет идти на страницу вверх (т.е. Домашняя страница => Выбор языка, Поиск по блогу => Домашняя страница)" - HomePageDescription: "Домашняя страница -- где мы сейчас и находимся!" - BlogSearchDescription: "Поиск по блогу. Там размещены мои путевые заметки и, может, что-то ещё!" - MarkerMapDescription: "Карта с маркерами моих путевых заметок, -- удобный способ найти интересный регион и перейти на заинтересовавшие посты!" - ThemeSwitchDescription: "Позволяет поменять тему сайта -- есть 3 светлые и 2 тёмные темы!" - AccountDescription: "Здесь можно подписаться на обновления в блоге, используя электронную почту!" - Hymn1: "Приветствуем Вас на Саясайте!" - Hymn2: "Мы Вас столько ждали!" - Hymn3: "Столько радости от Вас на Саясайте!" - Hymn4: "Где бы Вы не были, знайте, -- наша любовь крепка!" -UnsubscribePage: - Header: "Отписка" - UnsetCode: "Код для отписки не проставлен. Вы случайно не заблудились?" - InvalidCode: "Код для отписки не подтверждён. Может, истекло время в сутки для кода. А может, вы уже успели отписаться." - OnServerError: "Мы не смогли вас отписать, проблема на нашей стороне! В скором времени займёмся проблемой." - Success: "Вы отписались от рассылки! Надеюсь, мы ещё сможем вас заинтересовать." -Metadata: - Published: "Опубликовано" - Action: "Время действия" -Mail: - UnsubscribeFooter: "Если данное письмо пришло вам случайно, либо вы хотите отписаться, можете перейти по {}этой ссылке{/}." - VerifyEmail: - Subject: "Ссылка для верификации для подтверждения вашей почты SAYA.UZ" - Welcome: "Приветствую!" - Intro: "Есть 2 способа, чтобы подтвердить свою электронную почту:" - GotoLink: "Перейти по этой ссылке:" - InputCode: "Либо ввести этот код на странице подписок:" - IfRandom: "Если ты не понимаешь, что это за спам, можешь проигнорировать это письмо, а ещё, если интересно, можешь посетить мой сайт :)" - NewPost: - Subject: "Новый пост на SAYA.UZ!" - Intro: "Вышел новый пост!" - CapturedOn: "Снималось" -UserProfile: - Header: "Личные настройки" - Description: "Здесь можно настроить и верифицировать свою электронную почту, а также подписаться на избранные темы блога" - EmailHeader: "E-Mail" - VerificationCodeHeader: "Код верификации" - TypeEmail: "Пожалуйста, введите свою почту" - SendCodeButton: "Отправить код" - VerifyButton: "Верифицировать" - VerificationCodeSent: "Выслали вам код верификации! Проверьте свою почту. Следующая попытка верификации будет возможна {}." - DelayTilVerification: "Вы уже запрашивали недавно верификацию! Дождитесь {} для новой попытки." - FailedEmailRender: "Неудачная попытка создать электронное письмо, более подробно спрашивайте у администратора." - VerificationCodeSendingError: "Неудачная попытка прислать код для верификации, более подробно спрашивайте у администратора." - EmailAlreadyValidated: "Эту почту вы уже подтверждали." - EmailTaken: "Эта почта уже занята." - EmailEmpty: "Почта не введена." - VerificationSuccess: "Поздравляем! Теперь по этой почте можно подписаться на блог." - VerificationFailed: "Код для верификации невалиден, при вопросах, пожалуйста, обращайтесь к администратору." - VerificationEmpty: "Код для верификации не введён." - SubscriptionHeader: "Подписаться на..." - TagsNone: "Ничего" - TagsAll: "Всё" - TagsSpecific: "Определённое" - TagDoesNotExist: 'Тэга "{}" нет на сайте.' - TagAlreadyAdded: 'Тэг "{}" уже у вас добавлен.' - SaveButton: "Сохранить" - SubscribeInvalidType: "Неизвестный тип подписки." - FailedToSubscribe: "Неудачная попытка записать новую подписку, более подробно спрашивайте у администратора." - SubscribedSuccessfully: "Вы успешно подписались по новым настройкам!" - RefreshPage: "Пожалуйста, обновите страницу с профилем на устройстве, откуда запрашивали код." diff --git a/views/messages/new-post.html b/views/messages/new-post.html index 98f0b52..e956253 100644 --- a/views/messages/new-post.html +++ b/views/messages/new-post.html @@ -1,5 +1,5 @@ {{ define "body" }} -

{{ .L.Mail.NewPost.Intro }}

+

{{ l $.Lang "Mail" "NewPost" "Intro" }}

@@ -9,7 +9,7 @@ - +
{{ .Post.Metadata.ShortDescription }}
{{ .L.Mail.NewPost.CapturedOn }} {{ .Post.Metadata.ActionDate }}{{ l $.Lang "Mail" "NewPost" "CapturedOn" }} {{ .Post.Metadata.ActionDate }}
{{ end }} diff --git a/views/messages/verify-email.html b/views/messages/verify-email.html index dd6ef33..e6fff76 100644 --- a/views/messages/verify-email.html +++ b/views/messages/verify-email.html @@ -1,10 +1,10 @@ {{ define "body" }} -

{{ .L.Mail.VerifyEmail.Welcome }}

-

{{ .L.Mail.VerifyEmail.Intro }}

+

{{ l $.Lang "Mail" "VerifyEmail" "Welcome" }}

+

{{ l $.Lang "Mail" "VerifyEmail" "Intro" }}

    -
  1. {{ .L.Mail.VerifyEmail.GotoLink }} >>>
  2. -
  3. {{ .L.Mail.VerifyEmail.InputCode }} {{ .VerificationCode }}
  4. +
  5. {{ l $.Lang "Mail" "VerifyEmail" "GotoLink" }} >>>
  6. +
  7. {{ l $.Lang "Mail" "VerifyEmail" "InputCode" }} {{ .VerificationCode }}
-

{{ .L.Mail.VerifyEmail.IfRandom }}

+

{{ l $.Lang "Mail" "VerifyEmail" "IfRandom" }}

{{ .ClientHost }} {{ end }} \ No newline at end of file diff --git a/views/pages/blog-catalogue.html b/views/pages/blog-catalogue.html index 97917d6..b2bb0a3 100644 --- a/views/pages/blog-catalogue.html +++ b/views/pages/blog-catalogue.html @@ -51,57 +51,57 @@ class="tags-list relative flex flex-col w-full bg-paper bg-background-light">
- {{ .L.BlogSearch.OrderByHeader }} + {{ l $.Lang "BlogSearch" "OrderByHeader" }}
- {{ .L.BlogSearch.TagsHeader }} + {{ l $.Lang "BlogSearch" "TagsHeader" }}
- +
{{- range .Tags }}
diff --git a/views/pages/blog-page.html b/views/pages/blog-page.html index a14d85c..5f964e6 100644 --- a/views/pages/blog-page.html +++ b/views/pages/blog-page.html @@ -11,12 +11,12 @@ {{ .Title }}

- {{ .L.Metadata.Published }}: + {{ l $.Lang "Metadata" "Published" }}:

-

{{ .L.Metadata.Action }}: {{ .ActionDate }}

+

{{ l $.Lang "Metadata" "Action" }}: {{ .ActionDate }}

{{ .ShortDescription }}

@@ -25,7 +25,7 @@ {{- if .Medley }}

- {{ .Medley }} + {{ l .Lang "Medleys" .Medley }}


- - {{ .L.GlobalMap.Header }} // SAYA.UZ - + + {{ l $.Lang "GlobalMap" "Header" }} // SAYA.UZ + - {{ .L.GlobalMap.Header }} // SAYA.UZ + {{ l $.Lang "GlobalMap" "Header" }} // SAYA.UZ

{{ .L.HomePage.Hymn1 }}

+ bg-linear-to-r from-background-dark to-background-light">{{ l $.Lang "HomePage" "Hymn1" }}


-

{{ .L.HomePage.Hymn2 }}

-

{{ .L.HomePage.Hymn3 }}

-

{{ .L.HomePage.Hymn4 }}

+

{{ l $.Lang "HomePage" "Hymn2" }}

+

{{ l $.Lang "HomePage" "Hymn3" }}

+

{{ l $.Lang "HomePage" "Hymn4" }}

{{ .L.HomePage.DidYouKnowThat }}

+ bg-linear-to-r from-background-dark to-background-light">{{ l $.Lang "HomePage" "DidYouKnowThat" }}