Browse Source

FEATURE: i18n system!; Russian language added

master
parent
commit
8948fdbdcc
  1. 6
      Makefile
  2. 6
      pages/about.html
  3. 43
      pages/base.html
  4. 74
      pages/category.html
  5. 6
      pages/error.html
  6. 10
      pages/index.html
  7. 7
      pages/login.html
  8. 16
      pages/register.html
  9. 55
      src/i18n/i18n.go
  10. 16
      src/i18n/page.go
  11. 33
      src/server/page.go
  12. 47
      src/server/server.go
  13. 23
      src/server/validation.go
  14. 3
      static/images/globe.svg
  15. 3
      static/images/paint-bucket.svg
  16. 20
      translations/eng/about.json
  17. 25
      translations/eng/base.json
  18. 135
      translations/eng/category.json
  19. 20
      translations/eng/error.json
  20. 20
      translations/eng/index.json
  21. 20
      translations/eng/login.json
  22. 1
      translations/eng/paint.json
  23. 50
      translations/eng/register.json
  24. 20
      translations/ru/about.json
  25. 25
      translations/ru/base.json
  26. 135
      translations/ru/category.json
  27. 20
      translations/ru/error.json
  28. 20
      translations/ru/index.json
  29. 15
      translations/ru/login.json
  30. 1
      translations/ru/paint.json
  31. 50
      translations/ru/register.json

6
Makefile

@ -4,6 +4,7 @@ all: savedb clean
cd .. && \
cp -r pages bin && \
cp -r scripts bin && \
cp -r translations bin && \
cp -r static bin
-mv dela.db bin/
@ -19,6 +20,7 @@ cross: clean
mkdir -p bin/dela_linux_x64
cp -r pages bin/dela_linux_x64
cp -r scripts bin/dela_linux_x64
cp -r translations bin/dela_linux_x64
cp -r static bin/dela_linux_x64
cp COPYING bin/dela_linux_x64
cp README.md bin/dela_linux_x64
@ -26,6 +28,7 @@ cross: clean
mkdir -p bin/dela_windows_x64
cp -r pages bin/dela_windows_x64
cp -r scripts bin/dela_windows_x64
cp -r translations bin/dela_windows_x64
cp -r static bin/dela_windows_x64
cp COPYING bin/dela_windows_x64
cp README.md bin/dela_windows_x64
@ -33,6 +36,7 @@ cross: clean
mkdir -p bin/dela_darwin_x64
cp -r pages bin/dela_darwin_x64
cp -r scripts bin/dela_darwin_x64
cp -r translations bin/dela_darwin_x64
cp -r static bin/dela_darwin_x64
cp COPYING bin/dela_darwin_x64
cp README.md bin/dela_darwin_x64
@ -40,6 +44,7 @@ cross: clean
mkdir -p bin/dela_darwin_arm64
cp -r pages bin/dela_darwin_arm64
cp -r scripts bin/dela_darwin_arm64
cp -r translations bin/dela_darwin_arm64
cp -r static bin/dela_darwin_arm64
cp COPYING bin/dela_darwin_arm64
cp README.md bin/dela_darwin_arm64
@ -47,6 +52,7 @@ cross: clean
mkdir -p bin/dela_freebsd_x64
cp -r pages bin/dela_freebsd_x64
cp -r scripts bin/dela_freebsd_x64
cp -r translations bin/dela_freebsd_x64
cp -r static bin/dela_freebsd_x64
cp COPYING bin/dela_freebsd_x64
cp README.md bin/dela_freebsd_x64

6
pages/about.html

@ -6,10 +6,10 @@
<div class="px-4 pt-5 my-5 text-center border shadow-lg">
<h1 class="display-4 fw-bold text-body-emphasis">Dela</h1>
<div class="col-lg-6 mx-auto">
<p class="lead mb-4">a dead simple and minimalistic web TODO list</p>
<p class="lead mb-4">{{index .Translation "about info"}}</p>
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center mb-5">
<a href="/register" class="btn btn-primary btn-lg px-4 me-md-2 fw-bold">Register</a>
<a href="/login" class="btn btn-outline-secondary btn-lg px-4">Log in</a>
<a href="/register" class="btn btn-primary btn-lg px-4 me-md-2 fw-bold">{{index .Translation "about register"}}</a>
<a href="/login" class="btn btn-outline-secondary btn-lg px-4">{{index .Translation "about log in"}}</a>
</div>
</div>
<div class="overflow-hidden" style="max-height: 30vh;">

43
pages/base.html

@ -30,18 +30,22 @@
</a>
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
<li><a href="/" class="nav-link px-2 text-white">Main</a></li>
<li><a href="/about" class="nav-link px-2 text-white">About</a></li>
<li><a href="/" class="nav-link px-2 text-white">{{index .Translation "base link main"}}</a></li>
<li><a href="/about" class="nav-link px-2 text-white">{{index .Translation "base link about"}}</a></li>
</ul>
<div class="text-end p-3">
<button id="theme-switch-btn" class="btn btn-secondary" onclick="toggleTheme();">
<img id="theme-svg" src="/static/images/brightness-high.svg" alt="Change theme">
</button>
<button id="locale-switch-btn" onclick="switchLocale();" class="btn btn-secondary">
<img src="/static/images/globe.svg" alt="Change Locale">
<small id="locale">ENG</small>
</button>
</div>
<div class="text-end" id="bar-auth">
<a href="/login" class="btn btn-outline-light me-2">Login</a>
<a href="/register" class="btn btn-warning">Sign-up</a>
<a href="/login" class="btn btn-outline-light me-2">{{index .Translation "base link log in"}}</a>
<a href="/register" class="btn btn-warning">{{index .Translation "base link sign up"}}</a>
</div>
</div>
</div>
@ -58,6 +62,32 @@
<script src="/scripts/auth.js"></script>
<script src="/scripts/api.js"></script>
<script>
const locales = ["ENG", "RU"];
function switchLocale() {
let currentLocale = localStorage.getItem("locale");
if (!currentLocale) {
currentLocale = locales[0];
}
// Switch to the next locale
let index = locales.indexOf(currentLocale);
let newLocale;
if (index + 1 >= locales.length) {
newLocale = locales[0];
} else {
newLocale = locales[index+1];
}
// Set locale cookie
document.cookie = "locale="+newLocale+";path=/";
localStorage.setItem("locale", newLocale);
// Refresh page
window.location.reload();
}
function toggleTheme() {
if (document.documentElement.getAttribute('data-bs-theme') == 'dark') {
document.documentElement.setAttribute('data-bs-theme','light');
@ -71,6 +101,11 @@ function toggleTheme() {
}
document.addEventListener('DOMContentLoaded', async function() {
// Locale
let currentLocale = localStorage.getItem("locale");
document.getElementById("locale").innerText = currentLocale;
// Theme
let theme = localStorage.getItem("theme");
if (theme) {

74
pages/category.html

@ -2,7 +2,7 @@
{{ define "content" }}
<h1 style="display: none;" id="categoryId">{{.CurrentGroupId}}</h1>
<h1 style="display: none;" id="categoryId">{{.Data.CurrentGroupId}}</h1>
<!-- Main -->
<main class="d-flex flex-wrap">
@ -13,15 +13,15 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">Confirm Deletion</h5>
<h5 class="modal-title" id="deleteModalLabel">{{index .Translation "category modal confirm deletion"}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Are you sure you want to delete this ToDo?
{{index .Translation "category modal deletion confirmation"}}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" id="confirmDeleteButton">Delete</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDeleteButton">{{index .Translation "category modal delete button"}}</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{index .Translation "category modal cancel button"}}</button>
</div>
</div>
</div>
@ -33,15 +33,15 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="paintModal">Draw Note</h5>
<h5 class="modal-title" id="paintModal">{{index .Translation "category modal draw note"}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="clearCanvas();"></button>
</div>
<div class="modal-body w-100 d-flex flex-column justify-content-center align-items-center">
{{ template "paint" . }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="saveCanvasButton" data-bs-dismiss="modal">Save</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="clearCanvas();">Cancel</button>
<button type="button" class="btn btn-primary" id="saveCanvasButton" data-bs-dismiss="modal">{{index .Translation "category modal save button"}}</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="clearCanvas();">{{index .Translation "category modal cancel button"}}</button>
</div>
</div>
</div>
@ -53,32 +53,32 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="todoModalLabel">TODO Details</h5>
<h5 class="modal-title" id="todoModalLabel">{{index .Translation "category modal todo details"}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div>
<strong>Text:</strong>
<strong>{{index .Translation "category modal todo text"}}</strong>
<span id="modalTodoTextDisplay"></span>
<input type="text" id="modalTodoTextInput" class="form-control" style="display: none;">
</div>
<div>
<strong>Created:</strong> <span id="modalTodoCreated"></span>
<strong>{{index .Translation "category modal todo created"}}</strong> <span id="modalTodoCreated"></span>
</div>
<div>
<strong>Due:</strong>
<strong>{{index .Translation "category modal todo due"}}</strong>
<span id="modalTodoDueDisplay"></span>
<input type="date" id="modalTodoDueInput" class="form-control" style="display: none;">
</div>
<div>
<strong>Completion time:</strong> <span id="modalTodoCompletionTime"></span>
<strong>{{index .Translation "category modal todo completion"}}</strong> <span id="modalTodoCompletionTime"></span>
</div>
<img id="modalTodoImage" class="img-fluid" style="display: none;">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="editButton" style="display: none;" onclick="toggleEditMode(true)">Edit</button>
<button type="button" class="btn btn-success" id="saveButton" style="display: none;" onclick="saveEditedTodo()">Save</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{index .Translation "category modal close button"}}</button>
<button type="button" class="btn btn-primary" id="editButton" style="display: none;" onclick="toggleEditMode(true)">{{index .Translation "category modal edit button"}}</button>
<button type="button" class="btn btn-success" id="saveButton" style="display: none;" onclick="saveEditedTodo()">{{index .Translation "category modal save button"}}</button>
</div>
</div>
</div>
@ -88,18 +88,18 @@
<div id="sidebar" class="col border-right shadow-lg flex-shrink-1 p-2 d-flex flex-column align-items-stretch bg-body-tertiary" style="width: 380px;">
<a href="/" class="d-flex align-items-center flex-shrink-0 p-3 link-body-emphasis text-decoration-none border-bottom">
<img class="bi pe-none me-2" width="30" height="24" src="/static/images/arrows-fullscreen.svg">
<span class="fs-5 fw-semibold">Categories</span>
<span class="fs-5 fw-semibold">{{index .Translation "category categories"}}</span>
</a>
<div class="list-group list-group-flush border-bottom scrollarea">
{{ range .Groups }}
<a id="group-{{.ID}}" href="/group/{{.ID}}" class="list-group-item list-group-item-action py-3 lh-sm {{if eq .ID $.CurrentGroupId}} active {{end}}" aria-current="true" ondragover="allowDrop(event);" ondrop="drop(event);">
{{ range .Data.Groups }}
<a id="group-{{.ID}}" href="/group/{{.ID}}" class="list-group-item list-group-item-action py-3 lh-sm {{if eq .ID $.Data.CurrentGroupId}} active {{end}}" aria-current="true" ondragover="allowDrop(event);" ondrop="drop(event);">
<div id="group-{{.ID}}" class="d-flex w-100 align-items-center justify-content-between">
<strong id="group-{{.ID}}" class="mb-1">{{ .Name }}</strong>
<small id="group-{{.ID}}">{{ .TimeCreated }}</small>
</div>
{{ if not .Removable }}
<div id="group-{{.ID}}" class="col-10 mb-1 small">Not removable</div>
<div id="group-{{.ID}}" class="col-10 mb-1 small">{{index $.Translation "category not removable"}}</div>
{{ end }}
</a>
{{ end }}
@ -113,19 +113,19 @@
<form action="javascript:void(0);" id="todoForm">
<div class="row g-3 align-items-center">
<div class="col-md">
<label for="newTodoText" class="form-label">TODO Text</label>
<input type="text" class="form-control" id="newTodoText" placeholder="Enter TODO text" required>
<label for="newTodoText" class="form-label">{{index .Translation "category todo text"}}</label>
<input type="text" class="form-control" id="newTodoText" placeholder='{{index .Translation "category enter todo text"}}' required>
</div>
<div class="col-md">
<label for="newTodoDue" class="form-label">Due Date</label>
<label for="newTodoDue" class="form-label">{{index .Translation "category due date"}}</label>
<input type="date" class="form-control" name="newTodoDue" id="newTodoDue" required>
</div>
<div class="col-auto">
<button type="button" class="btn btn-primary" id="newTodoPaint" onclick="openPaintModal();">Paint</button>
<button type="button" class="btn btn-primary" id="newTodoPaint" onclick="openPaintModal();"><img src="/static/images/paint-bucket.svg"></button>
</div>
<div class="col-auto">
<button type="submit" id="newTodoSubmit" class="btn btn-primary">Add</button>
<button type="button" id="show-done" class="btn btn-secondary">Show Done</button>
<button type="submit" id="newTodoSubmit" class="btn btn-primary">{{index .Translation "category add"}}</button>
<button type="button" id="show-done" class="btn btn-secondary">{{index .Translation "category show done"}}</button>
</div>
</div>
</form>
@ -134,13 +134,13 @@
<!-- Due -->
<table class="table table-hover" id="due-todos">
<thead>
<th>Image</th>
<th>ToDo</th>
<th>Created</th>
<th>Due</th>
<th>{{index .Translation "category image"}}</th>
<th>{{index .Translation "category todo"}}</th>
<th>{{index .Translation "category created"}}</th>
<th>{{index .Translation "category due"}}</th>
</thead>
<tbody class="text-break">
{{ range .Todos }}
{{ range .Data.Todos }}
{{ if not .IsDone }}
<tr onclick="openTodoModal('{{.ID}}', '{{.Text}}', '{{.TimeCreated}}', '{{.Due}}', null, '{{ printf "%s" .Image }}', true);" draggable="true" id="todo-{{.ID}}" ondragstart="dragStart(event);">
{{ if not .Image }}
@ -170,13 +170,13 @@
<!-- Completed -->
<table class="table table-hover" style="display: none;" id="completed-todos">
<thead>
<th>Image</th>
<th>ToDo</th>
<th>Created</th>
<th>Completed</th>
<th>{{index .Translation "category image"}}</th>
<th>{{index .Translation "category todo"}}</th>
<th>{{index .Translation "category created"}}</th>
<th>{{index .Translation "category completed"}}</th>
</thead>
<tbody class="text-break">
{{ range .Todos }}
{{ range .Data.Todos }}
{{ if .IsDone }}
<tr onclick="openTodoModal('{{.ID}}', '{{.Text}}', '{{.TimeCreated}}', '{{.Due}}', '{{.CompletionTime}}', '{{ printf "%s" .Image }}', false);">
{{ if not .Image }}
@ -344,7 +344,7 @@ document.addEventListener('DOMContentLoaded', async function() {
let showDoneButton = document.getElementById("show-done");
showDoneButton.addEventListener("click", (event) => {
// Rename the button
showDoneButton.innerText = "Show To Do";
showDoneButton.innerText = '{{index .Translation "category js button show todo"}}';
showDoneButton.className = "btn btn-success";
// Show done
showDone();

6
pages/error.html

@ -3,10 +3,10 @@
{{ define "content" }}
<main class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<div class="p-2 flex-fill text-wrap text-center">
<h1 class="text-danger display-2">Error!</h1>
<h1 class="text-danger display-2">{{ index .Translation "error error big" }}</h1>
<img src="/static/images/emoji-frown.svg" alt="Sad face" width="128px" >
<p>Sorry! Something went wrong somewhere!</p>
<p><u>Try to reload the faulty page or try again later</u></p>
<p>{{ index .Translation "error something went wrong" }}</p>
<p><u>{{ index .Translation "error try to reload" }}</u></p>
</div>
</main>

10
pages/index.html

@ -11,18 +11,18 @@
<div id="sidebar" class="col border-right shadow-lg flex-shrink-1 p-2 d-flex flex-column align-items-stretch bg-body-tertiary">
<a href="/" class="d-flex align-items-center flex-shrink-0 p-3 link-body-emphasis text-decoration-none border-bottom">
<img class="bi pe-none me-2" width="30" height="24" src="/static/images/arrows-fullscreen.svg">
<span class="fs-5 fw-semibold">Categories</span>
<span class="fs-5 fw-semibold">{{index .Translation "index categories" }}</span>
</a>
<div class="list-group list-group-flush border-bottom scrollarea">
{{ range .Groups }}
{{ range .Data.Groups }}
<a href="/group/{{.ID}}" class="list-group-item list-group-item-action py-3 lh-sm" aria-current="true">
<div class="d-flex w-100 align-items-center justify-content-between">
<strong class="mb-1">{{ .Name }}</strong>
<small>{{ .TimeCreated }}</small>
</div>
{{ if not .Removable }}
<div class="col-10 mb-1 small">Not removable</div>
<div class="col-10 mb-1 small">{{index $.Translation "index not removable" }}</div>
{{ end }}
</a>
{{ end }}
@ -38,14 +38,14 @@
<!-- Groups -->
<div class="d-flex flex-column flex-grow-1 flex-md-row p-4 gap-4 py-md-5">
<div class="list-group flex-grow-1">
{{ range .Groups }}
{{ range .Data.Groups }}
<div class="list-group-item list-group-item-action d-flex gap-3 py-3" aria-current="true">
<a href="/group/{{.ID}}" class="list-group-item list-group-item-action d-flex gap-3 py-3">
<img src="/static/images/box-arrow-up-right.svg" alt="Go to this category" width="32" height="32">
<div class="d-flex gap-2 w-100 justify-content-between">
<div>
<h6 class="mb-0">{{ .Name }}</h6>
<p class="mb-0 opacity-75">Jump here</p>
<p class="mb-0 opacity-75">{{ index $.Translation "index jump here"}}</p>
</div>
<small class="opacity-50 text-nowrap">{{ .TimeCreated }}</small>
</div>

7
pages/login.html

@ -2,10 +2,9 @@
{{ define "content" }}
<main class="d-flex flex-wrap align-content-center align-items-center container my-5 flex-column">
<div class="p-2 flex-fill text-wrap text-center border shadow-lg">
<h3 class="h3 mb-3 fw-normal">Log in</h3>
<h3 class="h3 mb-3 fw-normal">{{index .Translation "login main"}}</h3>
<form onsubmit="return false;">
<div class="mb-3 input-group">
<img src="/static/images/envelope-at.svg" alt="Email" class="input-group-text">
@ -28,13 +27,13 @@
id="input-password"
aria-describedby="Password"
aria-label="Password"
placeholder="Password"
placeholder='{{index .Translation "login placeholder password"}}'
required
minlength="3">
</div>
<p><span id="error_message" class="text-danger"></span></p>
<input type="submit" value="Log in" class="btn btn-primary" onclick="logIn()">
<input type="submit" value='{{index .Translation "login main"}}' class="btn btn-primary" onclick="logIn()">
</form>
</div>

16
pages/register.html

@ -4,7 +4,7 @@
<main class="d-flex flex-wrap align-content-center container my-5 flex-column">
<div class="p-2 flex-fill text-wrap text-center border shadow-lg">
<h3 class="h3 mb-3 fw-normal">Register <span title="Passwords are hashed client-side, your information is protected">
<h3 class="h3 mb-3 fw-normal">{{index .Translation "register main"}} <span title='{{index .Translation "register passwords info"}}'>
<img src="/static/images/info-circle.svg" alt="Information"></span>
</h3>
<form onsubmit="return false;">
@ -29,13 +29,13 @@
id="input-password"
aria-describedby="Password"
aria-label="Password"
placeholder="Password"
placeholder='{{index .Translation "register placeholder password"}}'
required
minlength="3">
</div>
<p><span id="error_message" class="text-danger"></span></p>
<input type="submit" value="Register" class="btn btn-primary" onclick="register();">
<input type="submit" value='{{index .Translation "register button"}}' class="btn btn-primary" onclick="register();">
</form>
</div>
</main>
@ -44,18 +44,18 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="verificationModalLabel">Email Verification</h5>
<h5 class="modal-title" id="verificationModalLabel">{{index .Translation "register email verification"}}</h5>
</div>
<div class="modal-body">
<p>Enter the verification code sent to your email address</p>
<p>{{index .Translation "register email verification info"}}</p>
<div class="mb-3">
<label for="verificationCode" class="form-label">Verification Code</label>
<input type="text" id="input-code" class="form-control" id="verificationCode" placeholder="Enter your code" required>
<label for="verificationCode" class="form-label">{{index .Translation "register email verification code"}}</label>
<input type="text" id="input-code" class="form-control" id="verificationCode" placeholder='{{index .Translation "register enter code"}}' required>
</div>
<p class="text-danger"><span id="error-message-modal"></span></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="verifyButton" onclick="verify();">Verify</button>
<button type="button" class="btn btn-primary" id="verifyButton" onclick="verify();">{{index .Translation "register verify"}}</button>
</div>
</div>
</div>

55
src/i18n/i18n.go

@ -0,0 +1,55 @@
package i18n
import (
"encoding/json"
"io"
"os"
)
type Language string
func (l *Language) String() string {
return string(*l)
}
const (
Ru Language = "ru"
Eng Language = "eng"
)
type Translations []*Translation
func (ts *Translations) Add(translation *Translation) {
*ts = append(*ts, translation)
}
type Translation struct {
Language Language `json:"language"`
Messages []Message `json:"messages"`
}
type Message struct {
ID string `json:"id"`
Translation string `json:"translation"`
}
func FromFile(filePath string) (*Translation, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
contents, err := io.ReadAll(file)
if err != nil {
return nil, err
}
var translation Translation
err = json.Unmarshal(contents, &translation)
if err != nil {
return nil, err
}
return &translation, nil
}

16
src/i18n/page.go

@ -0,0 +1,16 @@
package i18n
import (
"path/filepath"
)
func GetPageTranslation(pageName string, language Language, translationsDirPath string) (*Translation, error) {
translation, err := FromFile(
filepath.Join(translationsDirPath, language.String(), pageName+".json"),
)
if err != nil {
return nil, err
}
return translation, nil
}

33
src/server/page.go

@ -20,8 +20,41 @@ package server
import (
"Unbewohnte/dela/db"
"Unbewohnte/dela/i18n"
"path/filepath"
)
type PageData struct {
Translation map[string]string
Data interface{}
}
func (s *Server) GetPageData(templateNames []string, language i18n.Language) (*PageData, error) {
translation := make(map[string]string)
translationsDirPath := filepath.Join(s.config.BaseContentDir, TranslationsDirName)
for _, page := range templateNames {
pageTranslation, err := i18n.GetPageTranslation(page, language, translationsDirPath)
if err != nil {
// Try ENG
pageTranslation, err = i18n.GetPageTranslation(page, i18n.Eng, translationsDirPath)
if err != nil {
return nil, err
}
}
// Merge translations
for _, message := range pageTranslation.Messages {
translation[message.ID] = message.Translation
}
}
return &PageData{
Translation: translation,
Data: nil,
}, nil
}
type IndexPageData struct {
Groups []*db.TodoGroup `json:"groups"`
}

47
src/server/server.go

@ -36,9 +36,10 @@ import (
)
const (
PagesDirName string = "pages"
StaticDirName string = "static"
ScriptsDirName string = "scripts"
PagesDirName string = "pages"
StaticDirName string = "static"
ScriptsDirName string = "scripts"
TranslationsDirName string = "translations"
)
type Server struct {
@ -73,6 +74,12 @@ func New(config conf.Conf) (*Server, error) {
return nil, err
}
_, err = os.Stat(filepath.Join(config.BaseContentDir, StaticDirName))
if err != nil {
logger.Error("[Server] A directory with page translations is not available: %s", err)
return nil, err
}
// get database working
serverDB, err := db.FromFile(filepath.Join(config.BaseContentDir, config.ProdDBName))
if err != nil {
@ -132,12 +139,20 @@ func New(config conf.Conf) (*Server, error) {
return
}
pageData, err := GetIndexPageData(server.db, GetLoginFromReq(req))
pageData, err := server.GetPageData([]string{"base", "index"}, LanguageFromReq(req))
if err != nil {
http.Redirect(w, req, "/error", http.StatusTemporaryRedirect)
logger.Error("[Server][/] Failed to get page data: %s", err)
return
}
indexPageData, err := GetIndexPageData(server.db, GetLoginFromReq(req))
if err != nil {
http.Redirect(w, req, "/error", http.StatusTemporaryRedirect)
logger.Error("[Server][/] Failed to get index page data: %s", err)
return
}
pageData.Data = indexPageData
err = requestedPage.ExecuteTemplate(w, "index.html", &pageData)
if err != nil {
@ -183,13 +198,21 @@ func New(config conf.Conf) (*Server, error) {
}
// Get page data
pageData, err := GetCategoryPageData(server.db, GetLoginFromReq(req), groupId)
pageData, err := server.GetPageData([]string{"base", "paint", "category"}, LanguageFromReq(req))
if err != nil {
http.Redirect(w, req, "/error", http.StatusTemporaryRedirect)
logger.Error("[Server][/category/] Failed to get category (%d) page data: %s", groupId, err)
return
}
categoriesData, err := GetCategoryPageData(server.db, GetLoginFromReq(req), groupId)
if err != nil {
http.Redirect(w, req, "/error", http.StatusTemporaryRedirect)
logger.Error("[Server][/category/] Failed to get category (%d) page data: %s", groupId, err)
return
}
pageData.Data = categoriesData
err = requestedPage.ExecuteTemplate(w, "category.html", &pageData)
if err != nil {
http.Redirect(w, req, "/error", http.StatusTemporaryRedirect)
@ -204,13 +227,25 @@ func New(config conf.Conf) (*Server, error) {
filepath.Join(pagesDirPath, req.URL.Path[1:]+".html"),
)
if err == nil {
err = requestedPage.ExecuteTemplate(w, req.URL.Path[1:]+".html", nil)
pageData, err := server.GetPageData(
[]string{"base", req.URL.Path[1:]},
LanguageFromReq(req),
)
if err != nil {
http.Redirect(w, req, "/error", http.StatusTemporaryRedirect)
logger.Error("[Server][/default] Failed to GetPageData for %s: %s", req.URL.Path[1:], err)
return
}
pageData.Data = nil
err = requestedPage.ExecuteTemplate(w, req.URL.Path[1:]+".html", &pageData)
if err != nil {
http.Redirect(w, req, "/error", http.StatusTemporaryRedirect)
logger.Error("[Server][/default] Template error: %s", err)
return
}
} else {
logger.Error("[Server][/default] Error on %s: %s", req.URL.Path[1:], err)
http.Redirect(w, req, "/error", http.StatusTemporaryRedirect)
}
}

23
src/server/validation.go

@ -20,6 +20,7 @@ package server
import (
"Unbewohnte/dela/db"
"Unbewohnte/dela/i18n"
"Unbewohnte/dela/misc"
"fmt"
"net/http"
@ -142,3 +143,25 @@ func GenerateVerificationCode(dbase *db.DB, email string, length uint, lifeTimeS
return verification, nil
}
func LocaleFromReq(req *http.Request) string {
cookie, err := req.Cookie("locale")
if err != nil {
return ""
}
return cookie.Value
}
func LanguageFromReq(req *http.Request) i18n.Language {
switch LocaleFromReq(req) {
case "ENG":
return i18n.Eng
case "RU":
return i18n.Ru
default:
return i18n.Eng
}
}

3
static/images/globe.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-globe" viewBox="0 0 16 16">
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m7.5-6.923c-.67.204-1.335.82-1.887 1.855A8 8 0 0 0 5.145 4H7.5zM4.09 4a9.3 9.3 0 0 1 .64-1.539 7 7 0 0 1 .597-.933A7.03 7.03 0 0 0 2.255 4zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a7 7 0 0 0-.656 2.5zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5zM8.5 5v2.5h2.99a12.5 12.5 0 0 0-.337-2.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5zM5.145 12q.208.58.468 1.068c.552 1.035 1.218 1.65 1.887 1.855V12zm.182 2.472a7 7 0 0 1-.597-.933A9.3 9.3 0 0 1 4.09 12H2.255a7 7 0 0 0 3.072 2.472M3.82 11a13.7 13.7 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5zm6.853 3.472A7 7 0 0 0 13.745 12H11.91a9.3 9.3 0 0 1-.64 1.539 7 7 0 0 1-.597.933M8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855q.26-.487.468-1.068zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.7 13.7 0 0 1-.312 2.5m2.802-3.5a7 7 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7 7 0 0 0-3.072-2.472c.218.284.418.598.597.933M10.855 4a8 8 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

3
static/images/paint-bucket.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-paint-bucket" viewBox="0 0 16 16">
<path d="M6.192 2.78c-.458-.677-.927-1.248-1.35-1.643a3 3 0 0 0-.71-.515c-.217-.104-.56-.205-.882-.02-.367.213-.427.63-.43.896-.003.304.064.664.173 1.044.196.687.556 1.528 1.035 2.402L.752 8.22c-.277.277-.269.656-.218.918.055.283.187.593.36.903.348.627.92 1.361 1.626 2.068.707.707 1.441 1.278 2.068 1.626.31.173.62.305.903.36.262.05.64.059.918-.218l5.615-5.615c.118.257.092.512.05.939-.03.292-.068.665-.073 1.176v.123h.003a1 1 0 0 0 1.993 0H14v-.057a1 1 0 0 0-.004-.117c-.055-1.25-.7-2.738-1.86-3.494a4 4 0 0 0-.211-.434c-.349-.626-.92-1.36-1.627-2.067S8.857 3.052 8.23 2.704c-.31-.172-.62-.304-.903-.36-.262-.05-.64-.058-.918.219zM4.16 1.867c.381.356.844.922 1.311 1.632l-.704.705c-.382-.727-.66-1.402-.813-1.938a3.3 3.3 0 0 1-.131-.673q.137.09.337.274m.394 3.965c.54.852 1.107 1.567 1.607 2.033a.5.5 0 1 0 .682-.732c-.453-.422-1.017-1.136-1.564-2.027l1.088-1.088q.081.181.183.365c.349.627.92 1.361 1.627 2.068.706.707 1.44 1.278 2.068 1.626q.183.103.365.183l-4.861 4.862-.068-.01c-.137-.027-.342-.104-.608-.252-.524-.292-1.186-.8-1.846-1.46s-1.168-1.32-1.46-1.846c-.147-.265-.225-.47-.251-.607l-.01-.068zm2.87-1.935a2.4 2.4 0 0 1-.241-.561c.135.033.324.11.562.241.524.292 1.186.8 1.846 1.46.45.45.83.901 1.118 1.31a3.5 3.5 0 0 0-1.066.091 11 11 0 0 1-.76-.694c-.66-.66-1.167-1.322-1.458-1.847z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

20
translations/eng/about.json

@ -0,0 +1,20 @@
{
"language": "ENG",
"messages": [
{
"id": "about info",
"message": "a dead simple and minimalistic web TODO list",
"translation": "a dead simple and minimalistic web TODO list"
},
{
"id": "about register",
"message": "Register",
"translation": "Register"
},
{
"id": "about log in",
"message": "Log In",
"translation": "Log In"
}
]
}

25
translations/eng/base.json

@ -0,0 +1,25 @@
{
"language": "ENG",
"messages": [
{
"id": "base link main",
"message": "Main",
"translation": "Main"
},
{
"id": "base link about",
"message": "About",
"translation": "About"
},
{
"id": "base link log in",
"message": "Log in",
"translation": "Log In"
},
{
"id": "base link sign up",
"message": "Sign-Up",
"translation": "Sign-Up"
}
]
}

135
translations/eng/category.json

@ -0,0 +1,135 @@
{
"language": "ENG",
"messages": [
{
"id": "category modal confirm deletion",
"message": "Confirm Deletion",
"translation": "Confirm Deletion"
},
{
"id": "category modal deletion confirmation",
"message": "Are you sure you want to delete this ToDo?",
"translation": "Are you sure you want to delete this ToDo?"
},
{
"id": "category modal delete button",
"message": "Delete",
"translation": "Delete"
},
{
"id": "category modal cancel button",
"message": "Cancel",
"translation": "Cancel"
},
{
"id": "category modal draw note",
"message": "Draw Note",
"translation": "Draw Note"
},
{
"id": "category modal save button",
"message": "Save",
"translation": "Save"
},
{
"id": "category modal todo details",
"message": "TODO Details",
"translation": "TODO Details"
},
{
"id": "category modal todo text",
"message": "Text:",
"translation": "Text:"
},
{
"id": "category modal todo created",
"message": "Created:",
"translation": "Created:"
},
{
"id": "category modal todo due",
"message": "Due:",
"translation": "Due:"
},
{
"id": "category modal todo completion",
"message": "Completion Time:",
"translation": "Completion Time:"
},
{
"id": "category modal close button",
"message": "Close",
"translation": "Close"
},
{
"id": "category modal edit button",
"message": "Edit",
"translation": "Edit"
},
{
"id": "category categories",
"message": "Categories",
"translation": "Categories"
},
{
"id": "category not removable",
"message": "Not Removable",
"translation": "Not Removable"
},
{
"id": "category todo text",
"message": "TODO Text",
"translation": "TODO Text"
},
{
"id": "category due date",
"message": "Due Date",
"translation": "Due Date"
},
{
"id": "category add",
"message": "Add",
"translation": "Add"
},
{
"id": "category show done",
"message": "Show Done",
"translation": "Show Done"
},
{
"id": "category image",
"message": "Image",
"translation": "Image"
},
{
"id": "category todo",
"message": "ToDo",
"translation": "Todo"
},
{
"id": "category created",
"message": "Created",
"translation": "Created"
},
{
"id": "category due",
"message": "Due",
"translation": "Due"
},
{
"id": "category completed",
"message": "Completed",
"translation": "Completed"
},
{
"id": "category enter todo text",
"message": "Enter ToDo Text",
"translation": "Enter ToDo Text"
},
{
"id": "category js button show todo",
"message": "Show To Do",
"translation": "Show To Do"
}
]
}

20
translations/eng/error.json

@ -0,0 +1,20 @@
{
"language": "ENG",
"messages": [
{
"id": "error error big",
"message": "Error!",
"translation": "Error!"
},
{
"id": "error something went wrong",
"message": "Sorry! Something went wrong somewhere!",
"translation": "Sorry! Something went wrong somewhere!"
},
{
"id": "error try to reload",
"message": "Try to reload the faulty page or try again later",
"translation": "Try to reload the faulty page or try again later"
}
]
}

20
translations/eng/index.json

@ -0,0 +1,20 @@
{
"language": "ENG",
"messages": [
{
"id": "index categories",
"message": "Categories",
"translation": "Categories"
},
{
"id": "index not removable",
"message": "Not removable",
"translation": "Not removable"
},
{
"id": "index jump here",
"message": "Jump here",
"translation": "Jump here"
}
]
}

20
translations/eng/login.json

@ -0,0 +1,20 @@
{
"language": "ENG",
"messages": [
{
"id": "login main",
"message": "Log In",
"translation": "Log In"
},
{
"id": "login placeholder password",
"message": "Password",
"translation": "Password"
},
{
"id": "index jump here",
"message": "Jump here",
"translation": "Jump here"
}
]
}

1
translations/eng/paint.json

@ -0,0 +1 @@
{}

50
translations/eng/register.json

@ -0,0 +1,50 @@
{
"language": "ENG",
"messages": [
{
"id": "register main",
"message": "Register",
"translation": "Register"
},
{
"id": "register passwords info",
"message": "Passwords are hashed client-side, your information is protected",
"translation": "Passwords are hashed client-side, your information is protected"
},
{
"id": "register placeholder password",
"message": "Password",
"translation": "Password"
},
{
"id": "register email verification",
"message": "Email Verification",
"translation": "Email Verification"
},
{
"id": "register email verification info",
"message": "Enter the verification code sent to your email address",
"translation": "Enter the verification code sent to your email address"
},
{
"id": "register email verification code",
"message": "Verification Code",
"translation": "Verification Code"
},
{
"id": "register enter code",
"message": "Enter your code",
"translation": "Enter your code"
},
{
"id": "register verify",
"message": "Verify",
"translation": "Verify"
},
{
"id": "register button",
"message": "Register",
"translation": "Register"
}
]
}

20
translations/ru/about.json

@ -0,0 +1,20 @@
{
"language": "ENG",
"messages": [
{
"id": "about info",
"message": "a dead simple and minimalistic web TODO list",
"translation": "простой и минималистичный сервис по ведению текстовых заметок"
},
{
"id": "about register",
"message": "Register",
"translation": "Зарегистрироваться"
},
{
"id": "about log in",
"message": "Log In",
"translation": "Войти"
}
]
}

25
translations/ru/base.json

@ -0,0 +1,25 @@
{
"language": "ENG",
"messages": [
{
"id": "base link main",
"message": "Main",
"translation": "Главная"
},
{
"id": "base link about",
"message": "About",
"translation": "О нас"
},
{
"id": "base link log in",
"message": "Log in",
"translation": "Войти"
},
{
"id": "base link sign up",
"message": "Sign-Up",
"translation": "Зарегистрироваться"
}
]
}

135
translations/ru/category.json

@ -0,0 +1,135 @@
{
"language": "ENG",
"messages": [
{
"id": "category modal confirm deletion",
"message": "Confirm Deletion",
"translation": "Подтвердите удаление"
},
{
"id": "category modal deletion confirmation",
"message": "Are you sure you want to delete this ToDo?",
"translation": "Вы действительно хотите удалить эту заметку?"
},
{
"id": "category modal delete button",
"message": "Delete",
"translation": "Удалить"
},
{
"id": "category modal cancel button",
"message": "Cancel",
"translation": "Отменить"
},
{
"id": "category modal draw note",
"message": "Draw Note",
"translation": "Рисование заметки"
},
{
"id": "category modal save button",
"message": "Save",
"translation": "Сохранить"
},
{
"id": "category modal todo details",
"message": "TODO Details",
"translation": "Описание заметки"
},
{
"id": "category modal todo text",
"message": "Text:",
"translation": "Текст:"
},
{
"id": "category modal todo created",
"message": "Created:",
"translation": "Создано:"
},
{
"id": "category modal todo due",
"message": "Due:",
"translation": "Окончание:"
},
{
"id": "category modal todo completion",
"message": "Completion Time:",
"translation": "Выполнено:"
},
{
"id": "category modal close button",
"message": "Close",
"translation": "Закрыть"
},
{
"id": "category modal edit button",
"message": "Edit",
"translation": "Изменить"
},
{
"id": "category categories",
"message": "Categories",
"translation": "Категории"
},
{
"id": "category not removable",
"message": "Not Removable",
"translation": "Неудаляемое"
},
{
"id": "category todo text",
"message": "TODO Text",
"translation": "Текст заметки"
},
{
"id": "category due date",
"message": "Due Date",
"translation": "Дата выполнения"
},
{
"id": "category add",
"message": "Add",
"translation": "Добавить"
},
{
"id": "category show done",
"message": "Show Done",
"translation": "Показать выполненные"
},
{
"id": "category image",
"message": "Image",
"translation": "Изображение"
},
{
"id": "category todo",
"message": "ToDo",
"translation": "Заметка"
},
{
"id": "category created",
"message": "Created",
"translation": "Создано"
},
{
"id": "category due",
"message": "Due",
"translation": "Окончание"
},
{
"id": "category completed",
"message": "Completed",
"translation": "Выполнено"
},
{
"id": "category enter todo text",
"message": "Enter ToDo Text",
"translation": "Введите текст заметки"
},
{
"id": "category js button show todo",
"message": "Show To Do",
"translation": "К невыполненным"
}
]
}

20
translations/ru/error.json

@ -0,0 +1,20 @@
{
"language": "ENG",
"messages": [
{
"id": "error error big",
"message": "Error!",
"translation": "Ошибка!"
},
{
"id": "error something went wrong",
"message": "Sorry! Something went wrong somewhere!",
"translation": "Приносим свои извинения! Что-то пошло не так!"
},
{
"id": "error try to reload",
"message": "Try to reload the faulty page or try again later",
"translation": "Попробуйте перезагрузить страницу, или попробовать позже"
}
]
}

20
translations/ru/index.json

@ -0,0 +1,20 @@
{
"language": "ENG",
"messages": [
{
"id": "index categories",
"message": "Categories",
"translation": "Категории"
},
{
"id": "index not removable",
"message": "Not removable",
"translation": "Неудаляемое"
},
{
"id": "index jump here",
"message": "Jump here",
"translation": "Перейти"
}
]
}

15
translations/ru/login.json

@ -0,0 +1,15 @@
{
"language": "ENG",
"messages": [
{
"id": "login main",
"message": "Log In",
"translation": "Войти"
},
{
"id": "login placeholder password",
"message": "Password",
"translation": "Пароль"
}
]
}

1
translations/ru/paint.json

@ -0,0 +1 @@
{}

50
translations/ru/register.json

@ -0,0 +1,50 @@
{
"language": "ENG",
"messages": [
{
"id": "register main",
"message": "Register",
"translation": "Регистрация"
},
{
"id": "register passwords info",
"message": "Passwords are hashed client-side, your information is protected",
"translation": "Пароли хешируются на стороне клиента, Ваша информация в безопасности"
},
{
"id": "register placeholder password",
"message": "Password",
"translation": "Пароль"
},
{
"id": "register email verification",
"message": "Email Verification",
"translation": "Подтверждение почты"
},
{
"id": "register email verification info",
"message": "Enter the verification code sent to your email address",
"translation": "Введите код подтверждения, отправленный на вашу почту"
},
{
"id": "register email verification code",
"message": "Verification Code",
"translation": "Код подтверждения"
},
{
"id": "register enter code",
"message": "Enter your code",
"translation": "Введите ваш код"
},
{
"id": "register verify",
"message": "Verify",
"translation": "Подтвердить"
},
{
"id": "register button",
"message": "Register",
"translation": "Зарегистрироваться"
}
]
}
Loading…
Cancel
Save