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

@ -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 <config-dir>/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 <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
# Not present by default, set only if you want to be notified of any failed login attempts over telegram

View file

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

View file

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

View file

@ -2,5 +2,6 @@ username=admin
password=admin
port=7101
timezone=Local
language=en
log_to_file=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"}}
<header>
<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>
{{end}}
{{define "base"}}
<!DOCTYPE html>
<html lang="en">
<html lang="{{ translatableText "lang" }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@ -29,6 +29,6 @@
{{define "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>
{{end}}

View file

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

View file

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