diff options
30 files changed, 374 insertions, 276 deletions
@@ -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 ) @@ -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(`<a style="color: #273de1 !important;" href="https://%s/%s/user/unsubscribe?code=%X">`, m.clientHost, post.Lang, unsubscribeCode), 1) + unsubscribeFooter := strings.Replace(l10n.T.GetPath(post.Lang, "Mail", "UnsubscribeFooter").(string), "{}", fmt.Sprintf(`<a style="color: #273de1 !important;" href="https://%s/%s/user/unsubscribe?code=%X">`, m.clientHost, post.Lang, unsubscribeCode), 1) unsubscribeFooter = strings.Replace(unsubscribeFooter, "{/}", "</a>", 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/locale/localization.en.yaml b/l10n/en.yaml index 9571aca..9571aca 100644 --- a/locale/localization.en.yaml +++ b/l10n/en.yaml diff --git a/locale/localization.ru.yaml b/l10n/ru.yaml index 1818998..1818998 100644 --- a/locale/localization.ru.yaml +++ b/l10n/ru.yaml 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.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/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" }} -<p>{{ .L.Mail.NewPost.Intro }}</p> +<p>{{ l $.Lang "Mail" "NewPost" "Intro" }}</p> <table> <tr> <td rowspan="3"><a href="https://{{ .ClientHost }}/{{ .Lang }}/blog/{{ .Post.FileName }}"><img src="{{ printf .ThumbnailBaseUrl .Post.Metadata.Thumbnail }}"></a></td> @@ -9,7 +9,7 @@ <td class="darkened" style="font-size: 16px">{{ .Post.Metadata.ShortDescription }}</td> </tr> <tr> - <td class="darkened" style="font-size: 16px">{{ .L.Mail.NewPost.CapturedOn }} {{ .Post.Metadata.ActionDate }}</td> + <td class="darkened" style="font-size: 16px">{{ l $.Lang "Mail" "NewPost" "CapturedOn" }} {{ .Post.Metadata.ActionDate }}</td> </tr> </table> {{ 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" }} -<p>{{ .L.Mail.VerifyEmail.Welcome }}</p> -<p>{{ .L.Mail.VerifyEmail.Intro }}</p> +<p>{{ l $.Lang "Mail" "VerifyEmail" "Welcome" }}</p> +<p>{{ l $.Lang "Mail" "VerifyEmail" "Intro" }}</p> <ol> - <li>{{ .L.Mail.VerifyEmail.GotoLink }} <a href="https://{{ .ClientHost }}/{{ .Lang }}/user?email_code={{ .VerificationCode }}" class="outlier">>>></a></li> - <li>{{ .L.Mail.VerifyEmail.InputCode }} <span class="outlier">{{ .VerificationCode }}</span><span></li> + <li>{{ l $.Lang "Mail" "VerifyEmail" "GotoLink" }} <a href="https://{{ .ClientHost }}/{{ .Lang }}/user?email_code={{ .VerificationCode }}" class="outlier">>>></a></li> + <li>{{ l $.Lang "Mail" "VerifyEmail" "InputCode" }} <span class="outlier">{{ .VerificationCode }}</span><span></li> </ol> -<p>{{ .L.Mail.VerifyEmail.IfRandom }}</p> +<p>{{ l $.Lang "Mail" "VerifyEmail" "IfRandom" }}</p> <a href="https://{{ .ClientHost }}" class="outlier">{{ .ClientHost }}</a> {{ 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"> <div class="flex flex-col overflow-y-auto"> <fieldset> - <legend class="font-bold w-full text-center">{{ .L.BlogSearch.OrderByHeader }}</legend> + <legend class="font-bold w-full text-center">{{ l $.Lang "BlogSearch" "OrderByHeader" }}</legend> <div class="md:flex flex-col grid grid-cols-3 grid-rows-2 grid-flow-col"> <div> <label class="flex justify-start gap-1 items-center"> <input type="radio" id="sortTitleAsc" name="sort" value="titleAsc" class="bg-accent-light border-transparent text-accent-deep focus:border-transparent focus:bg-accent-light focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep" {{ if eq .QuerySort "titleAsc" }}checked{{ end }}> - {{ .L.BlogSearch.TitleOrdered }} + {{ l $.Lang "BlogSearch" "TitleOrdered" }} <svg viewBox="0 0 24 24" class="h-8"><use href="#icon-arrange-letter-asc"/></svg> </label> </div> <div> <label class="flex justify-start gap-1 items-center"> <input type="radio" id="sortTitleDesc" name="sort" value="titleDesc" class="bg-accent-light border-transparent text-accent-deep focus:border-transparent focus:bg-accent-light focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep" {{ if eq .QuerySort "titleDesc" }}checked{{ end }}> - {{ .L.BlogSearch.TitleOrdered }} + {{ l $.Lang "BlogSearch" "TitleOrdered" }} <svg viewBox="0 0 24 24" class="h-8"><use href="#icon-arrange-letter-desc"/></svg> </label> </div> <div> <label class="flex justify-start gap-1 items-center"> <input type="radio" id="sortActionDateAsc" name="sort" value="actionDateAsc" class="bg-accent-light border-transparent text-accent-deep focus:border-transparent focus:bg-accent-light focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep" {{ if eq .QuerySort "actionDateAsc" }}checked{{ end }}> - {{ .L.BlogSearch.ActionDateOrdered }} + {{ l $.Lang "BlogSearch" "ActionDateOrdered" }} <svg viewBox="0 0 24 24" class="h-8"><use href="#icon-arrange-number-asc"/></svg> </label> </div> <div> <label class="flex justify-start gap-1 items-center"> <input type="radio" id="sortActionDateDesc" name="sort" value="actionDateDesc" class="bg-accent-light border-transparent text-accent-deep focus:border-transparent focus:bg-accent-light focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep" {{ if eq .QuerySort "actionDateDesc" }}checked{{ end }}> - {{ .L.BlogSearch.ActionDateOrdered }} + {{ l $.Lang "BlogSearch" "ActionDateOrdered" }} <svg viewBox="0 0 24 24" class="h-8"><use href="#icon-arrange-number-desc"/></svg> </label> </div> <div> <label class="flex justify-start gap-1 items-center"> <input type="radio" id="sortPublicationDateAsc" name="sort" value="publicationDateAsc" class="bg-accent-light border-transparent text-accent-deep focus:border-transparent focus:bg-accent-light focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep" {{ if eq .QuerySort "publicationDateAsc" }}checked{{ end }}> - {{ .L.BlogSearch.PublicationDateOrdered }} + {{ l $.Lang "BlogSearch" "PublicationDateOrdered" }} <svg viewBox="0 0 24 24" class="h-8"><use href="#icon-arrange-number-asc"/></svg> </label> </div> <div> <label class="flex justify-start gap-1 items-center"> <input type="radio" id="sortPublicationDateDesc" name="sort" value="publicationDateDesc" class="bg-accent-light border-transparent text-accent-deep focus:border-transparent focus:bg-accent-light focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep" {{ if eq .QuerySort "publicationDateDesc" }}checked{{ end }}> - {{ .L.BlogSearch.PublicationDateOrdered }} + {{ l $.Lang "BlogSearch" "PublicationDateOrdered" }} <svg viewBox="0 0 24 24" class="h-8"><use href="#icon-arrange-number-desc"/></svg> </label> </div> </div> </fieldset> <fieldset class="mt-1"> - <legend class="font-bold w-full text-center">{{ .L.BlogSearch.TagsHeader }}</legend> + <legend class="font-bold w-full text-center">{{ l $.Lang "BlogSearch" "TagsHeader" }}</legend> <div class="md:flex flex-col grid grid-cols-3 xs:grid-cols-4 sm:grid-cols-5 grid-flow-row-dense"> <div class="m-1"> - <label class="font-bold"><input type="checkbox" id="tagsAllCheckbox" onclick="selectAll();" class="bg-accent-light rounded border-transparent text-accent-deep focus:border-transparent focus:bg-background-dark focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep"> {{ .L.BlogSearch.ChooseAllTags }}</label> + <label class="font-bold"><input type="checkbox" id="tagsAllCheckbox" onclick="selectAll();" class="bg-accent-light rounded border-transparent text-accent-deep focus:border-transparent focus:bg-background-dark focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep"> {{ l $.Lang "BlogSearch" "ChooseAllTags" }}</label> </div> {{- range .Tags }} <div class="m-1"> 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 }} </h1> <p class="block grow text-lg font-gentium text-secondary"> - <b>{{ .L.Metadata.Published }}</b>: + <b>{{ l $.Lang "Metadata" "Published" }}</b>: <time datetime="{{ .PublishedDate }}" hx-get="/api/v1/tz" hx-vals='js:{timestamp: "{{ .PublishedDate }}", tz: clientTimeZone}' hx-target="this" hx-swap="innerHTML" hx-trigger="load"> {{ .PublishedDate }} </time> </p> - <p class="block grow text-lg font-gentium text-secondary"><b>{{ .L.Metadata.Action }}</b>: {{ .ActionDate }}</p> + <p class="block grow text-lg font-gentium text-secondary"><b>{{ l $.Lang "Metadata" "Action" }}</b>: {{ .ActionDate }}</p> <p class="block grow text-md font-gentium font-thin text-main-hard">{{ .ShortDescription }}</p> </div> </div> @@ -25,7 +25,7 @@ {{- if .Medley }} <div class="overflow-y-auto grow-2 shrink-0 flex-1 bg-paper bg-background-dark shadow-elevation-2 -mr-8 sm:-mt-4 max-sm:-ml-8 z-2"> <h2 class="text-2xl font-gentium font-extrabold text-main-hard tracking-[.0125rem] ml-2"> - {{ .Medley }} + {{ l .Lang "Medleys" .Medley }} </h2> <hr class="block border-t-2 my-1 border-dotted border-main-hard w-full"> <div hx-get="/api/v1/blog-search" hx-vals='js:{lang: "{{ .Lang }}", tz: clientTimeZone, medley: "{{ .Medley }}", sort: "medley", hideTags: true, hidePublishedTime: true, highlight: "{{ .Codename }}"}' hx-target="this" hx-swap="innerHTML" hx-trigger="load" diff --git a/views/pages/global-map.html b/views/pages/global-map.html index 788eac9..0a7b941 100644 --- a/views/pages/global-map.html +++ b/views/pages/global-map.html @@ -3,19 +3,19 @@ <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <meta name="description" content="{{ .L.GlobalMap.Description }}" /> - <title>{{ .L.GlobalMap.Header }} // SAYA.UZ</title> - <meta property="og:title" content="{{ .L.GlobalMap.Header }}" /> + <meta name="description" content="{{ l $.Lang "GlobalMap" "Description" }}" /> + <title>{{ l $.Lang "GlobalMap" "Header" }} // SAYA.UZ</title> + <meta property="og:title" content="{{ l $.Lang "GlobalMap" "Header" }}" /> <meta property="og:description" - content="{{ .L.GlobalMap.Description }}" + content="{{ l $.Lang "GlobalMap" "Description" }}" /> <meta property="og:url" content="{{ .CanonicalEndpoint }}/{{ .Lang }}/map" /> <meta property="og:type" content="site" /> - <title>{{ .L.GlobalMap.Header }} // SAYA.UZ</title> + <title>{{ l $.Lang "GlobalMap" "Header" }} // SAYA.UZ</title> <link rel="canonical" href="{{ .CanonicalEndpoint }}/{{ .Lang }}/map" /> <link rel="stylesheet" diff --git a/views/pages/home-page.html b/views/pages/home-page.html index 6e026aa..e89be39 100644 --- a/views/pages/home-page.html +++ b/views/pages/home-page.html @@ -23,17 +23,17 @@ <div class="flex flex-col justify-start items-start bg-paper bg-background-dark mb-8 shadow-[0.4rem_0.4rem_0.4rem_black]"> <p class="text-3xl/loose sm:text-4xl/loose lg:text-5xl/loose tracking-widest uppercase w-full font-m-plus font-thin italic text-left border-l-4 border-solid border-background-dark - bg-linear-to-r from-background-dark to-background-light">{{ .L.HomePage.Hymn1 }}</p> + bg-linear-to-r from-background-dark to-background-light">{{ l $.Lang "HomePage" "Hymn1" }}</p> <hr class="w-full border-t-2 border-dotted border-main-hard mb-1"> - <p class="text-xl/loose sm:text-2xl/loose lg:text-3xl/loose -indent-8 ml-12 p-1">{{ .L.HomePage.Hymn2 }}</p> - <p class="text-xl/loose sm:text-2xl/loose lg:text-3xl/loose -indent-8 ml-12 p-1">{{ .L.HomePage.Hymn3 }}</p> - <p class="text-xl/loose sm:text-2xl/loose lg:text-3xl/loose -indent-8 ml-12 p-1">{{ .L.HomePage.Hymn4 }}</p> + <p class="text-xl/loose sm:text-2xl/loose lg:text-3xl/loose -indent-8 ml-12 p-1">{{ l $.Lang "HomePage" "Hymn2" }}</p> + <p class="text-xl/loose sm:text-2xl/loose lg:text-3xl/loose -indent-8 ml-12 p-1">{{ l $.Lang "HomePage" "Hymn3" }}</p> + <p class="text-xl/loose sm:text-2xl/loose lg:text-3xl/loose -indent-8 ml-12 p-1">{{ l $.Lang "HomePage" "Hymn4" }}</p> </div> <div class="flex flex-col justify-center items-center"> <p class="text-xl sm:text-2xl lg:text-3xl p-1 w-full font-m-plus font-thin italic text-left border-l-1.5 border-solid border-background-dark - bg-linear-to-r from-background-dark to-background-light">{{ .L.HomePage.DidYouKnowThat }}</p> + bg-linear-to-r from-background-dark to-background-light">{{ l $.Lang "HomePage" "DidYouKnowThat" }}</p> <hr class="w-full border-t-2 border-dotted border-main-hard mb-1"> <ul class="w-full text-left list-[circle]"> <li class="ml-4 mb-1">{{ index .FunFacts 0 }}</li> @@ -47,7 +47,7 @@ <div class="flex flex-col justify-center items-start mb-4"> <p class="text-xl sm:text-2xl lg:text-3xl p-1 w-full font-m-plus font-thin italic text-left border-l-1.5 border-solid border-background-dark - bg-linear-to-r from-background-dark to-background-light">{{ .L.HomePage.Contacts }}</p> + bg-linear-to-r from-background-dark to-background-light">{{ l $.Lang "HomePage" "Contacts" }}</p> <hr class="w-full border-t-2 border-dotted border-main-hard mb-1"> <a href="https://t.me/EarlInisMona"> <svg class="w-4 h-4 mr-1 inline" viewBox="0 0 24 24"><use href="#icon-telegram"/></svg> @@ -122,22 +122,22 @@ <div class="flex flex-col justify-center items-center"> <p class="text-xl sm:text-2xl lg:text-3xl p-1 w-full font-m-plus font-thin italic text-left border-l-1.5 border-solid border-color-background-dark - bg-linear-to-r from-background-dark to-background-light">{{ .L.HomePage.SidebarDescription }}</p> + bg-linear-to-r from-background-dark to-background-light">{{ l $.Lang "HomePage" "SidebarDescription" }}</p> <hr class="w-full border-t-2 border-dotted border-main-hard mb-1"> <div class="w-full grid grid-cols-[1fr_4fr] content-center items-center [&>*:nth-child(4n+2)]:bg-background-medium/50 [&>*:nth-child(4n+3)]:bg-background-medium/50" style="container-type: inline-size;"> <svg viewBox="0 0 24 24" class="h-full w-[15cqi] context-center text-main-hard"><use href="#icon-back"/></svg> - <p class="p-1 w-[85cqi]">{{ .L.HomePage.BackDescription }}</p> + <p class="p-1 w-[85cqi]">{{ l $.Lang "HomePage" "BackDescription" }}</p> <svg viewBox="0 0 24 24" class="h-full w-[15cqi] context-center text-main-hard"><use href="#icon-logo"/></svg> - <p class="p-1 w-[85cqi]">{{ .L.HomePage.HomePageDescription }}</p> + <p class="p-1 w-[85cqi]">{{ l $.Lang "HomePage" "HomePageDescription" }}</p> <svg viewBox="0 0 24 24" class="h-full w-[15cqi] context-center text-main-hard"><use href="#icon-newspaper"/></svg> - <p class="p-1 w-[85cqi]">{{ .L.HomePage.BlogSearchDescription }}</p> + <p class="p-1 w-[85cqi]">{{ l $.Lang "HomePage" "BlogSearchDescription" }}</p> <svg viewBox="0 0 24 24" class="h-full w-[15cqi] context-center text-main-hard"><use href="#icon-map"/></svg> - <p class="p-1 w-[85cqi]">{{ .L.HomePage.MarkerMapDescription }}</p> + <p class="p-1 w-[85cqi]">{{ l $.Lang "HomePage" "MarkerMapDescription" }}</p> <svg viewBox="0 0 24 24" class="h-full w-[15cqi] context-center text-main-hard"><use href="#icon-palette"/></svg> - <p class="p-1 w-[85cqi]">{{ .L.HomePage.ThemeSwitchDescription }}</p> + <p class="p-1 w-[85cqi]">{{ l $.Lang "HomePage" "ThemeSwitchDescription" }}</p> <svg viewBox="0 0 24 24" class="h-full w-[15cqi] context-center text-main-hard"><use href="#icon-account"/></svg> - <p class="p-1 w-[85cqi]">{{ .L.HomePage.AccountDescription }}</p> + <p class="p-1 w-[85cqi]">{{ l $.Lang "HomePage" "AccountDescription" }}</p> </div> </div> </div> diff --git a/views/pages/unsubscribe-page.html b/views/pages/unsubscribe-page.html index 9a78488..12ff1f0 100644 --- a/views/pages/unsubscribe-page.html +++ b/views/pages/unsubscribe-page.html @@ -21,7 +21,7 @@ href="/favicon.ico" type="image/x-icon" /> - <title>{{ .L.UnsubscribePage.Header }} // SAYA.UZ</title> + <title>{{ l $.Lang "UnsubscribePage" "Header" }} // SAYA.UZ</title> <style> .bg-paper { background-image: url(data:image/webp;base64,UklGRi4BAABXRUJQVlA4ICIBAABQCQCdASqAAIAAPm02lUmkIqIkIImIgA2JaW3rMFmGDAOKLAoJtP591ByFR74Tun2DP+Lygpfsz5FH7+cKgxi2Islfd03i5AJdXUfU0SDc8SXYAAD++hlzeYAJWQEF8ZdkJdlcA+5btZg0FMGaso8SHoBph0qAOQysElJ5C5SJFNoKIV1J550UyL7Y9Gh0/c/uzLnBmr8lvz7k1WeA81H4S5ViU/wBqa7aM1m8D9rDtrk8cSTqx+xWF6MS8UfZGSsks6CT/igtgv6syFN9uNA3LjnjiCJKYOvFcMkX+jB2578GWVo3nyLaM1QNPYCxn3TiUbnf7U0MdD2NsBpg1+Jd1wu6yW/U+goN+IQS6Nt4RMWltVcZHlxrneKJi0SobO0AAA==); diff --git a/views/pages/user-page.html b/views/pages/user-page.html index 0dd006a..dc4966b 100644 --- a/views/pages/user-page.html +++ b/views/pages/user-page.html @@ -6,16 +6,16 @@ <div class="flex flex-row w-full mb-4 z-21"> <div class="flex flex-col w-full sm:flex-3/5 grow-0"> <label class="block text-main-medium font-bold mb-1 z-22"> - {{ .L.UserProfile.EmailHeader }} + {{ l $.Lang "UserProfile" "EmailHeader" }} </label> <input class="bg-background-medium appearance-none border-background-medium rounded ml-6 mr-4 py-2 px-4 text-main-medium leading-tight inset-shadow-[0_0_0.4rem_black] focus:outline-none focus:bg-background-light focus:text-main-hard z-22" - id="email-input" name="email" type="text" value="{{ .Email }}" placeholder="{{ .L.UserProfile.TypeEmail }}" maxlength="64" + id="email-input" name="email" type="text" value="{{ .Email }}" placeholder="{{ l $.Lang "UserProfile" "TypeEmail" }}" maxlength="64" data-validated-email="{{ .Email }}" onchange="checkIfValidated(this);"> </div> </div> <button class="w-max mx-auto bg-main-hard hover:bg-main-medium active:bg-main-soft active:inset-shadow-[0.2em_0.2em_0.2rem_black] transition-colors transition-300 text-background-light font-bold py-2 px-4 rounded z-22" hx-post="/api/v1/email/send-verification-code" hx-target="#email-message" hx-swap="outerHTML" hx-indicator="#email-form"> - {{ .L.UserProfile.SendCodeButton }} + {{ l $.Lang "UserProfile" "SendCodeButton" }} </button> </form> <div id="email-message" class="bg-paper bg-background-dark text-main-soft z-26 w-full inset-shadow-[0_0_0.4rem_black] py-2 px-4 text-center"></div> @@ -27,7 +27,7 @@ <div class="flex flex-row w-full mb-4 z-21"> <div class="flex flex-col w-full sm:flex-2/5 grow-0"> <label class="block text-main-medium font-bold ml-1 z-22"> - {{ .L.UserProfile.VerificationCodeHeader }} + {{ l $.Lang "UserProfile" "VerificationCodeHeader" }} </label> <input class="bg-background-medium appearance-none border-background-medium rounded ml-6 mr-4 py-2 px-4 text-main-medium leading-tight inset-shadow-[0_0_0.4rem_black] focus:outline-none focus:bg-background-light focus:text-main-hard z-22" name="email_code" type="text" value="{{ .EmailCode }}" placeholder="0123456789ABCDEF" maxlength="16" autocapitalize="characters"> @@ -35,7 +35,7 @@ </div> <button class="w-max mx-auto bg-main-hard hover:bg-main-medium active:bg-main-soft active:inset-shadow-[0.2em_0.2em_0.2rem_black] transition-colors transition-300 text-background-light font-bold py-2 px-4 rounded z-22" hx-post="/api/v1/email/verify" hx-target="#verification-message" hx-swap="outerHTML" hx-indicator="#email-verification-form"> - {{ .L.UserProfile.VerifyButton }} + {{ l $.Lang "UserProfile" "VerifyButton" }} </button> </form> <div id="verification-message" class="bg-paper bg-background-dark text-main-soft z-26 w-full inset-shadow-[0_0_0.4rem_black] py-2 px-4 text-center"></div> @@ -47,17 +47,17 @@ <form id="subs-form" class="w-full mb-2 flex flex-col relative crossable"> <div class="flex flex-col w-full mb-4 z-21"> <label class="block text-main-medium font-bold mb-1 z-22"> - {{ .L.UserProfile.SubscriptionHeader }} + {{ l $.Lang "UserProfile" "SubscriptionHeader" }} </label> <div class="flex flex-col sm:flex-row w-full"> <div class="m-1 w-1/3"> - <label><input class="mr-1" type="radio" id="tagsNone" name="tags" value="none" {{ if eq .TagsPicked "none" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ .L.UserProfile.TagsNone }}</label> + <label><input class="mr-1" type="radio" id="tagsNone" name="tags" value="none" {{ if eq .TagsPicked "none" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ l $.Lang "UserProfile" "TagsNone" }}</label> </div> <div class="m-1 w-1/3"> - <label><input class="mr-1" type="radio" id="tagsAll" name="tags" value="all" {{ if eq .TagsPicked "all" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ .L.UserProfile.TagsAll }}</label> + <label><input class="mr-1" type="radio" id="tagsAll" name="tags" value="all" {{ if eq .TagsPicked "all" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ l $.Lang "UserProfile" "TagsAll" }}</label> </div> <div class="m-1 w-1/3"> - <label><input class="mr-1" type="radio" id="tagsSpecific" name="tags" value="specific" {{ if eq .TagsPicked "specific" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ .L.UserProfile.TagsSpecific }}</label> + <label><input class="mr-1" type="radio" id="tagsSpecific" name="tags" value="specific" {{ if eq .TagsPicked "specific" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ l $.Lang "UserProfile" "TagsSpecific" }}</label> </div> </div> <div id="pick-tags-form" class="flex flex-col w-full relative crossable"> @@ -80,7 +80,7 @@ </div> <button class="w-max mx-auto bg-color-main-hard hover:bg-main-medium active:bg-color-main-soft active:inset-shadow-[0.2em_0.2em_0.2rem_black] transition-colors transition-300 text-background-light font-bold py-2 px-4 rounded z-22" hx-put="/api/v1/subs" hx-target="#subs-message" hx-swap="outerHTML" hx-indicator="#subs-form"> - {{ .L.UserProfile.SaveButton }} + {{ l $.Lang "UserProfile" "SaveButton" }} </button> </div> </form> @@ -168,7 +168,7 @@ function addTag(name) { found = [...existingTags].find(opt => opt.value===name); if (!found) { subsMessage.classList.remove("hidden"); - subsMessage.innerHTML = "{{ .L.UserProfile.TagDoesNotExist }}".replace("{}", name); + subsMessage.innerHTML = "{{ l $.Lang "UserProfile" "TagDoesNotExist" }}".replace("{}", name); return; } @@ -177,7 +177,7 @@ function addTag(name) { found = [...subscribedTags].find(opt => opt.value===name); if (found) { subsMessage.classList.remove("hidden"); - subsMessage.innerHTML = "{{ .L.UserProfile.TagAlreadyAdded }}".replace("{}", name); + subsMessage.innerHTML = "{{ l $.Lang "UserProfile" "TagAlreadyAdded" }}".replace("{}", name); return; } |