From 6ff3d2e0eea5b886aed96133c6d90948f8464eae Mon Sep 17 00:00:00 2001 From: Andrew-71 Date: Fri, 3 May 2024 15:40:40 +0300 Subject: [PATCH] Add i18n and optimise template parsing --- README.md | 5 +++-- TODO.md | 2 +- config.go | 7 +++++-- config/config.txt | 1 + i18n.go | 37 +++++++++++++++++++++++++++++++++++++ i18n/en.json | 15 +++++++++++++++ i18n/ru.json | 15 +++++++++++++++ pages/base.html | 6 +++--- pages/edit.html | 2 +- routes.go | 45 +++++++++++++++++++-------------------------- 10 files changed, 100 insertions(+), 35 deletions(-) create mode 100644 i18n.go create mode 100644 i18n/en.json create mode 100644 i18n/ru.json diff --git a/README.md b/README.md index 6bc55a3..d10186f 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,9 @@ they are provided purely for demonstration only and **will break the config if p username=admin # Your username password=admin # Your password port=7101 # What port to run on (probably leave on 7101 if using docker) -timezone=Local # IANA Time zone database identifier ("UTC", Local", "Europe/Moscow" etc.), Defaults to Local if can't parse. -log_to_file=false # Whether to write logs to a file (located in /log.txt) +timezone=Local # IANA Time zone database identifier ("UTC", Local", "Europe/Moscow" etc.), Defaults to Local if can't parse. +language=en # ISO-639 language code (currently supported - en, ru) +log_to_file=false # Whether to write logs to a file (located in /log.txt) enable_scram=false # Whether the app should shut down if there are 3 or more failed login attempts within 100 seconds # Not present by default, set only if you want to be notified of any failed login attempts over telegram diff --git a/TODO.md b/TODO.md index 02ce6ce..30a09ac 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,7 @@ List of things to add to this project * Fix the weird issue with compose and mounts (absolute path something) when updating!!! +* CI/CD pipeline * Better docs in case others want to use ths for some reason * Github/Codeberg/whatever mirror for when `faye` (my server) is offline * Improve config and use reflection when loading it @@ -10,5 +11,4 @@ List of things to add to this project * Customise log file * More slog.Debug and a debug flag? * Consider less clunky auth method -* Consider localisation * *Go* dependency-less? <-- this is a terrible idea \ No newline at end of file diff --git a/config.go b/config.go index 60d0b81..305f72b 100644 --- a/config.go +++ b/config.go @@ -19,6 +19,7 @@ type Config struct { Password string `config:"password"` Port int `config:"port"` Timezone *time.Location `config:"timezone"` + Language string `config:"language"` LogToFile bool `config:"log_to_file"` Scram bool `config:"enable_scram"` @@ -87,6 +88,8 @@ func (c *Config) Reload() error { } else { c.Timezone = loc } + } else if key == "language" { + c.Language = value } else if key == "tg_token" { c.TelegramToken = value } else if key == "tg_chat" { @@ -109,12 +112,12 @@ func (c *Config) Reload() error { return err } - return nil + return LoadLanguage(c.Language) // (Load selected language } // ConfigInit loads config on startup func ConfigInit() Config { - cfg := Config{Port: 7101, Username: "admin", Password: "admin", Timezone: time.Local} // Default values are declared here, I guess + cfg := Config{Port: 7101, Username: "admin", Password: "admin", Timezone: time.Local, Language: "en"} // Default values are declared here, I guess err := cfg.Reload() if err != nil { log.Fatal(err) diff --git a/config/config.txt b/config/config.txt index bde8ece..f9398f4 100644 --- a/config/config.txt +++ b/config/config.txt @@ -2,5 +2,6 @@ username=admin password=admin port=7101 timezone=Local +language=en log_to_file=false enable_scram=false diff --git a/i18n.go b/i18n.go new file mode 100644 index 0000000..03d4f12 --- /dev/null +++ b/i18n.go @@ -0,0 +1,37 @@ +package main + +import ( + "encoding/json" + "errors" + "log/slog" + "os" +) + +var Translations = map[string]string{} + +func LoadLanguage(language string) error { + filename := "i18n/" + language + ".json" + + if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) { + return err + } + + fileContents, err := os.ReadFile(filename) + if err != nil { + slog.Error("error reading file", + "error", err, + "file", filename) + return err + } + + err = json.Unmarshal(fileContents, &Translations) + return err +} + +func TranslatableText(id string) string { + if v, ok := Translations[id]; !ok { + return id + } else { + return v + } +} diff --git a/i18n/en.json b/i18n/en.json new file mode 100644 index 0000000..5d7a4ba --- /dev/null +++ b/i18n/en.json @@ -0,0 +1,15 @@ +{ + "lang": "en", + + "title.today": "Your day so far", + "title.days": "Previous days", + "title.notes": "Notes", + + "link.today": "today", + "link.days": "previous days", + "link.notes": "notes", + + "description.notes": "/notes/ for a new note", + "misc.date": "Today is", + "button.save": "Save" +} \ No newline at end of file diff --git a/i18n/ru.json b/i18n/ru.json new file mode 100644 index 0000000..b883cad --- /dev/null +++ b/i18n/ru.json @@ -0,0 +1,15 @@ +{ + "lang": "ru", + + "title.today": "Сегодняшний день", + "title.days": "Предыдущие дни", + "title.notes": "Заметки", + + "link.today": "сегодня", + "link.days": "раньше", + "link.notes": "заметки", + + "description.notes": "/notes/<название> для новой заметки", + "misc.date": "Сегодня", + "button.save": "Сохранить" +} \ No newline at end of file diff --git a/pages/base.html b/pages/base.html index 44ab90b..d457a49 100644 --- a/pages/base.html +++ b/pages/base.html @@ -1,13 +1,13 @@ {{define "header"}}

🌺 Hibiscus.txt

-

Today is a place

+

{{translatableText "misc.date"}} a place

{{end}} {{define "base"}} - + @@ -29,6 +29,6 @@ {{define "footer"}} {{end}} \ No newline at end of file diff --git a/pages/edit.html b/pages/edit.html index 732f47e..dca9637 100644 --- a/pages/edit.html +++ b/pages/edit.html @@ -2,6 +2,6 @@

- +
{{end}} \ No newline at end of file diff --git a/routes.go b/routes.go index 3af232b..32e8cf7 100644 --- a/routes.go +++ b/routes.go @@ -25,6 +25,11 @@ type Entry struct { type formatEntries func([]string) []Entry +var templateFuncs = map[string]interface{}{"translatableText": TranslatableText} +var editTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/edit.html")) +var viewTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/entry.html")) +var listTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/list.html")) + // NotFound returns a user-friendly 404 error page func NotFound(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) @@ -50,15 +55,9 @@ func GetToday(w http.ResponseWriter, r *http.Request) { } } - files := []string{"./pages/base.html", "./pages/edit.html"} - ts, err := template.ParseFiles(files...) - if err != nil { - InternalError(w, r) - return - } - - err = ts.ExecuteTemplate(w, "base", Entry{Title: "Your day so far", Content: string(day)}) + err = editTemplate.ExecuteTemplate(w, "base", Entry{Title: TranslatableText("title.today"), Content: string(day)}) if err != nil { + slog.Error("error executing template", "error", err) InternalError(w, r) return } @@ -83,15 +82,7 @@ func GetEntries(w http.ResponseWriter, r *http.Request, title string, descriptio } var filesFormatted = format(filesList) - files := []string{"./pages/base.html", "./pages/list.html"} - ts, err := template.ParseFiles(files...) - if err != nil { - slog.Error("error parsing template files", "error", err) - InternalError(w, r) - return - } - - err = ts.ExecuteTemplate(w, "base", EntryList{Title: title, Description: description, Entries: filesFormatted}) + err = listTemplate.ExecuteTemplate(w, "base", EntryList{Title: title, Description: description, Entries: filesFormatted}) if err != nil { slog.Error("error executing template", "error", err) InternalError(w, r) @@ -101,7 +92,7 @@ func GetEntries(w http.ResponseWriter, r *http.Request, title string, descriptio // GetDays renders HTML list of previous days' entries func GetDays(w http.ResponseWriter, r *http.Request) { - GetEntries(w, r, "Previous days", "", "day", func(files []string) []Entry { + GetEntries(w, r, TranslatableText("title.days"), "", "day", func(files []string) []Entry { var filesFormatted []Entry for i := range files { v := files[len(files)-1-i] // This is suboptimal, but reverse order is better here @@ -111,7 +102,10 @@ func GetDays(w http.ResponseWriter, r *http.Request) { dayString = t.Format("02 Jan 2006") } if v == time.Now().Format(time.DateOnly) { - dayString = "Today" + // Fancy text for today + // This looks bad, but strings.Title is deprecated, and I'm not importing a golang.org/x package for this... + dayString = TranslatableText("link.today") + dayString = strings.ToTitle(string([]rune(dayString)[0])) + string([]rune(dayString)[1:]) } filesFormatted = append(filesFormatted, Entry{Title: dayString, Link: "day/" + v}) } @@ -121,7 +115,7 @@ func GetDays(w http.ResponseWriter, r *http.Request) { // GetNotes renders HTML list of all notes func GetNotes(w http.ResponseWriter, r *http.Request) { - GetEntries(w, r, "Notes", "/notes/ for a new note", "notes", func(files []string) []Entry { + GetEntries(w, r, TranslatableText("title.notes"), TranslatableText("description.notes"), "notes", func(files []string) []Entry { var filesFormatted []Entry for _, v := range files { titleString := strings.Replace(v, "-", " ", -1) // FIXME: what if I need a hyphen? @@ -149,13 +143,12 @@ func GetEntry(w http.ResponseWriter, r *http.Request, title string, filename str } else { files = append(files, "./pages/entry.html") } - ts, err := template.ParseFiles(files...) - if err != nil { - InternalError(w, r) - return - } - err = ts.ExecuteTemplate(w, "base", Entry{Title: title, Content: string(entry)}) + if editable { + err = editTemplate.ExecuteTemplate(w, "base", Entry{Title: title, Content: string(entry)}) + } else { + err = viewTemplate.ExecuteTemplate(w, "base", Entry{Title: title, Content: string(entry)}) + } if err != nil { InternalError(w, r) return