Add i18n and optimise template parsing

This commit is contained in:
Andrew-71 2024-05-03 15:40:40 +03:00
parent a92e6b04b7
commit 6ff3d2e0ee
10 changed files with 100 additions and 35 deletions

View file

@ -47,6 +47,7 @@ username=admin # Your username
password=admin # Your password password=admin # Your password
port=7101 # What port to run on (probably leave on 7101 if using docker) 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. 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 <config-dir>/log.txt) log_to_file=false # Whether to write logs to a file (located in <config-dir>/log.txt)
enable_scram=false # Whether the app should shut down if there are 3 or more failed login attempts within 100 seconds enable_scram=false # Whether the app should shut down if there are 3 or more failed login attempts within 100 seconds

View file

@ -2,6 +2,7 @@
List of things to add to this project List of things to add to this project
* Fix the weird issue with compose and mounts (absolute path something) when updating!!! * 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 * Better docs in case others want to use ths for some reason
* Github/Codeberg/whatever mirror for when `faye` (my server) is offline * Github/Codeberg/whatever mirror for when `faye` (my server) is offline
* Improve config and use reflection when loading it * Improve config and use reflection when loading it
@ -10,5 +11,4 @@ List of things to add to this project
* Customise log file * Customise log file
* More slog.Debug and a debug flag? * More slog.Debug and a debug flag?
* Consider less clunky auth method * Consider less clunky auth method
* Consider localisation
* *Go* dependency-less? <-- this is a terrible idea * *Go* dependency-less? <-- this is a terrible idea

View file

@ -19,6 +19,7 @@ type Config struct {
Password string `config:"password"` Password string `config:"password"`
Port int `config:"port"` Port int `config:"port"`
Timezone *time.Location `config:"timezone"` Timezone *time.Location `config:"timezone"`
Language string `config:"language"`
LogToFile bool `config:"log_to_file"` LogToFile bool `config:"log_to_file"`
Scram bool `config:"enable_scram"` Scram bool `config:"enable_scram"`
@ -87,6 +88,8 @@ func (c *Config) Reload() error {
} else { } else {
c.Timezone = loc c.Timezone = loc
} }
} else if key == "language" {
c.Language = value
} else if key == "tg_token" { } else if key == "tg_token" {
c.TelegramToken = value c.TelegramToken = value
} else if key == "tg_chat" { } else if key == "tg_chat" {
@ -109,12 +112,12 @@ func (c *Config) Reload() error {
return err return err
} }
return nil return LoadLanguage(c.Language) // (Load selected language
} }
// ConfigInit loads config on startup // ConfigInit loads config on startup
func ConfigInit() Config { 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() err := cfg.Reload()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View file

@ -2,5 +2,6 @@ username=admin
password=admin password=admin
port=7101 port=7101
timezone=Local timezone=Local
language=en
log_to_file=false log_to_file=false
enable_scram=false enable_scram=false

37
i18n.go Normal file
View file

@ -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
}
}

15
i18n/en.json Normal file
View file

@ -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/<name> for a new note",
"misc.date": "Today is",
"button.save": "Save"
}

15
i18n/ru.json Normal file
View file

@ -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": "Сохранить"
}

View file

@ -1,13 +1,13 @@
{{define "header"}} {{define "header"}}
<header> <header>
<h1>🌺 Hibiscus.txt</h1> <h1>🌺 Hibiscus.txt</h1>
<p>Today is <span id="today-date">a place</span></p> <p>{{translatableText "misc.date"}} <span id="today-date">a place</span></p>
</header> </header>
{{end}} {{end}}
{{define "base"}} {{define "base"}}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="{{ translatableText "lang" }}">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@ -29,6 +29,6 @@
{{define "footer"}} {{define "footer"}}
<footer> <footer>
<p><a href="/">today</a> | <a href="/day">previous days</a> | <a href="/notes">notes</a></p> <p><a href="/">{{ translatableText "link.today" }}</a> | <a href="/day">{{ translatableText "link.days" }}</a> | <a href="/notes">{{ translatableText "link.notes" }}</a></p>
</footer> </footer>
{{end}} {{end}}

View file

@ -2,6 +2,6 @@
<form method="POST"> <form method="POST">
<h2><label for="text">{{ .Title }}:</label></h2> <h2><label for="text">{{ .Title }}:</label></h2>
<textarea id="text" cols="40" rows="15" name="text">{{ .Content }}</textarea> <textarea id="text" cols="40" rows="15" name="text">{{ .Content }}</textarea>
<button type="submit">Save</button> <button type="submit">{{ translatableText "button.save" }}</button>
</form> </form>
{{end}} {{end}}

View file

@ -25,6 +25,11 @@ type Entry struct {
type formatEntries func([]string) []Entry 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 // NotFound returns a user-friendly 404 error page
func NotFound(w http.ResponseWriter, r *http.Request) { func NotFound(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404) w.WriteHeader(404)
@ -50,15 +55,9 @@ func GetToday(w http.ResponseWriter, r *http.Request) {
} }
} }
files := []string{"./pages/base.html", "./pages/edit.html"} err = editTemplate.ExecuteTemplate(w, "base", Entry{Title: TranslatableText("title.today"), Content: string(day)})
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)})
if err != nil { if err != nil {
slog.Error("error executing template", "error", err)
InternalError(w, r) InternalError(w, r)
return return
} }
@ -83,15 +82,7 @@ func GetEntries(w http.ResponseWriter, r *http.Request, title string, descriptio
} }
var filesFormatted = format(filesList) var filesFormatted = format(filesList)
files := []string{"./pages/base.html", "./pages/list.html"} err = listTemplate.ExecuteTemplate(w, "base", EntryList{Title: title, Description: description, Entries: filesFormatted})
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})
if err != nil { if err != nil {
slog.Error("error executing template", "error", err) slog.Error("error executing template", "error", err)
InternalError(w, r) 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 // GetDays renders HTML list of previous days' entries
func GetDays(w http.ResponseWriter, r *http.Request) { 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 var filesFormatted []Entry
for i := range files { for i := range files {
v := files[len(files)-1-i] // This is suboptimal, but reverse order is better here 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") dayString = t.Format("02 Jan 2006")
} }
if v == time.Now().Format(time.DateOnly) { 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}) 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 // GetNotes renders HTML list of all notes
func GetNotes(w http.ResponseWriter, r *http.Request) { func GetNotes(w http.ResponseWriter, r *http.Request) {
GetEntries(w, r, "Notes", "/notes/<name> 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 var filesFormatted []Entry
for _, v := range files { for _, v := range files {
titleString := strings.Replace(v, "-", " ", -1) // FIXME: what if I need a hyphen? 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 { } else {
files = append(files, "./pages/entry.html") 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 { if err != nil {
InternalError(w, r) InternalError(w, r)
return return