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
|
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
|
||||||
|
|
||||||
|
|
2
TODO.md
2
TODO.md
|
@ -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
|
|
@ -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)
|
||||||
|
|
|
@ -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
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"}}
|
{{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}}
|
|
@ -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}}
|
45
routes.go
45
routes.go
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue