Add i18n and optimise template parsing
This commit is contained in:
parent
a92e6b04b7
commit
6ff3d2e0ee
10 changed files with 100 additions and 35 deletions
|
@ -47,6 +47,7 @@ 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.
|
||||
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
|
||||
|
||||
|
|
2
TODO.md
2
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
|
|
@ -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)
|
||||
|
|
|
@ -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
37
i18n.go
Normal 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
15
i18n/en.json
Normal 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
15
i18n/ru.json
Normal 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": "Сохранить"
|
||||
}
|
|
@ -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}}
|
|
@ -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}}
|
45
routes.go
45
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/<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
|
||||
|
|
Loading…
Reference in a new issue