diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef26ff0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +locales/ +po/*.pot diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8ac381b --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +INPUT_FILES := $(shell find web -name *.html) +DEFAULT_DOMAIN = numerus +POT_FILE = po/$(DEFAULT_DOMAIN).pot +LINGUAS = ca es +MO_FILES = $(patsubst %,locales/%/LC_MESSAGES/$(DEFAULT_DOMAIN).mo,$(LINGUAS)) + +locales: $(MO_FILES) + +locales/%/LC_MESSAGES/numerus.mo: po/%.po + mkdir -p $(@D) + msgfmt -o $@ $< + +po/%.po: $(POT_FILE) + msgmerge --no-wrap --update --backup=off $@ $< + +$(POT_FILE): $(INPUT_FILES) + xgettext --no-wrap --language=Scheme --from-code=UTF-8 --output=$@ --keyword=pgettext:1,2c --package-name=numerus --msgid-bugs-address=jordi@tandem.blog $^ diff --git a/debian/control b/debian/control index 474b265..4299ef5 100644 --- a/debian/control +++ b/debian/control @@ -5,8 +5,11 @@ Maintainer: jordi fita mas Build-Depends: debhelper-compat (= 13), dh-golang, + gettext, golang-any, golang-github-jackc-pgx-v4-dev, + golang-github-leonelquinteros-gotext-dev, + golang-golang-x-text-dev, Standards-Version: 4.6.0 XS-Go-Import-Path: dev.tandem.ws/tandem/numerus Vcs-Browser: https://dev.tandem.ws/tandem/numerus @@ -56,4 +59,4 @@ Description: Simple invoicing and accounting web application A simple web application to keep invoice and accouting records, intended for contractors working in Spain. . - This is the demo SQL script. + This is the demo package. diff --git a/debian/numerus.install b/debian/numerus.install index 80df8e6..0873623 100644 --- a/debian/numerus.install +++ b/debian/numerus.install @@ -1,2 +1,3 @@ usr/bin/numerus usr/bin +locales usr/share/numerus web usr/share/numerus diff --git a/debian/rules b/debian/rules index 44584c3..0663c0a 100755 --- a/debian/rules +++ b/debian/rules @@ -2,3 +2,6 @@ %: dh $@ --builddirectory=_build --buildsystem=golang --with=golang + +execute_before_dh_auto_build: + make diff --git a/go.mod b/go.mod index b4bfc70..755b0c4 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,11 @@ module dev.tandem.ws/tandem/numerus go 1.18 -require github.com/jackc/pgx/v4 v4.17.2 +require ( + github.com/jackc/pgx/v4 v4.17.2 + github.com/leonelquinteros/gotext v1.5.1 + golang.org/x/text v0.3.8 +) require ( github.com/jackc/chunkreader/v2 v2.0.1 // indirect @@ -14,5 +18,4 @@ require ( github.com/jackc/pgtype v1.12.0 // indirect github.com/jackc/puddle v1.3.0 // indirect golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect - golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index 5470b32..eaef7b4 100644 --- a/go.sum +++ b/go.sum @@ -69,6 +69,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leonelquinteros/gotext v1.5.1 h1:vmddRn3gHp67YFjZLZE2AZsgYMT4IBTJhua4yfe7/4Q= +github.com/leonelquinteros/gotext v1.5.1/go.mod h1:/A4Y7BvIsf5JHO60E43ZQDVkV3qO+7eP8HjeqD6ChIA= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -105,6 +107,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -126,19 +129,23 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -152,6 +159,7 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -169,7 +177,9 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/locale.go b/pkg/locale.go new file mode 100644 index 0000000..777f88e --- /dev/null +++ b/pkg/locale.go @@ -0,0 +1,50 @@ +package pkg + +import ( + "context" + "net/http" + + "github.com/leonelquinteros/gotext" + "golang.org/x/text/language" +) + +const contextLocaleKey = "numerus-locale" + +func Locale(next http.Handler) http.Handler { + supportedLanguages := []language.Tag{ + language.Catalan, + language.Spanish, + } + var matcher = language.NewMatcher(supportedLanguages) + + locales := map[language.Tag]*gotext.Locale{} + for _, lang := range supportedLanguages { + locale := gotext.NewLocale("locales", lang.String()) + locale.AddDomain("numerus") + locales[lang] = locale + } + defaultLocale := locales[language.Catalan] + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var locale *gotext.Locale + t, _, err := language.ParseAcceptLanguage(r.Header.Get("Accept-Language")) + if err == nil { + tag, _, _ := matcher.Match(t...) + var ok bool + locale, ok = locales[tag] + for !ok && !tag.IsRoot() { + tag = tag.Parent() + locale, ok = locales[tag] + } + } + if locale == nil { + locale = defaultLocale + } + ctx := context.WithValue(r.Context(), contextLocaleKey, locale) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func getLocale(r *http.Request) *gotext.Locale { + return r.Context().Value(contextLocaleKey).(*gotext.Locale) +} diff --git a/pkg/login.go b/pkg/login.go index 48792fc..0f627a0 100644 --- a/pkg/login.go +++ b/pkg/login.go @@ -2,7 +2,6 @@ package pkg import ( "context" - "html/template" "net" "net/http" "time" @@ -17,7 +16,7 @@ const ( ) type LoginPage struct { - LoginError string + LoginError bool Email string Password string } @@ -33,30 +32,26 @@ func LoginHandler(db *Db) http.Handler { user := getUser(r) if user.LoggedIn { http.Redirect(w, r, "/", http.StatusSeeOther) - } else { - r.ParseForm() - - page := LoginPage{ - Email: r.FormValue("email"), - Password: r.FormValue("password"), - } + return + } + r.ParseForm() + page := LoginPage{ + Email: r.FormValue("email"), + Password: r.FormValue("password"), + } + if r.Method == "POST" { cookie := db.Text(r, "", "select login($1, $2, $3)", page.Email, page.Password, remoteAddr(r)) - if cookie == "" { - page.LoginError = "Invalid user or password" - w.WriteHeader(http.StatusUnauthorized) - t, err := template.ParseFiles("web/template/login.html") - if err != nil { - panic(err) - } - err = t.Execute(w, page) - if err != nil { - panic(err) - } - } else { + if cookie != "" { http.SetCookie(w, createSessionCookie(cookie, 8766*24*time.Hour)) http.Redirect(w, r, "/", http.StatusSeeOther) + return } + w.WriteHeader(http.StatusUnauthorized) + page.LoginError = true + } else { + w.WriteHeader(http.StatusOK) } + renderTemplate(w, r, "login.html", page) }) } @@ -67,7 +62,7 @@ func LogoutHandler(db *Db) http.Handler { db.Exec(r, "select logout()") http.SetCookie(w, createSessionCookie("", -24*time.Hour)) } - http.Redirect(w, r, "/", http.StatusSeeOther) + http.Redirect(w, r, "/login", http.StatusSeeOther) }) } diff --git a/pkg/router.go b/pkg/router.go index 7f24559..996ed7e 100644 --- a/pkg/router.go +++ b/pkg/router.go @@ -1,7 +1,6 @@ package pkg import ( - "html/template" "net/http" ) @@ -13,27 +12,13 @@ func NewRouter(db *Db) http.Handler { router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { user := getUser(r) if user.LoggedIn { - t, err := template.ParseFiles("web/template/index.html") - if err != nil { - panic(err) - } - err = t.Execute(w, nil) - if err != nil { - panic(err) - } + renderTemplate(w, r, "index.html", nil) } else { - var page LoginPage; - t, err := template.ParseFiles("web/template/login.html") - if err != nil { - panic(err) + http.Redirect(w, r, "/login", http.StatusSeeOther) } - err = t.Execute(w, page) - if err != nil { - panic(err) - } - } }) var handler http.Handler = router + handler = Locale(handler) handler = CheckLogin(db, handler) handler = Recoverer(handler) handler = Logger(handler) diff --git a/pkg/template.go b/pkg/template.go new file mode 100644 index 0000000..8fb086c --- /dev/null +++ b/pkg/template.go @@ -0,0 +1,22 @@ +package pkg + +import ( + "html/template" + "io" + "net/http" +) + +func renderTemplate(wr io.Writer, r *http.Request, filename string, data interface{}) { + locale := getLocale(r) + t := template.New(filename) + t.Funcs(template.FuncMap{ + "gettext": locale.Get, + "pgettext": locale.GetC, + }) + if _, err := t.ParseFiles("web/template/" + filename); err != nil { + panic(err) + } + if err := t.Execute(wr, data); err != nil { + panic(err) + } +} diff --git a/po/ca.po b/po/ca.po new file mode 100644 index 0000000..79fd101 --- /dev/null +++ b/po/ca.po @@ -0,0 +1,47 @@ +# Catalan translations for numerus package +# Traduccions al català del paquet «numerus». +# Copyright (C) 2023 jordi fita mas +# This file is distributed under the same license as the numerus package. +# jordi fita mas , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: numerus\n" +"Report-Msgid-Bugs-To: jordi@tandem.blog\n" +"POT-Creation-Date: 2023-01-18 17:54+0100\n" +"PO-Revision-Date: 2023-01-18 17:08+0100\n" +"Last-Translator: jordi fita mas \n" +"Language-Team: Catalan \n" +"Language: ca\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: web/template/login.html:6 web/template/login.html:19 +msgctxt "title" +msgid "Login" +msgstr "Entrada" + +#: web/template/login.html:14 +msgid "Invalid user or password" +msgstr "Nom d’usuari o contrasenya incorrectes" + +#: web/template/login.html:21 +msgctxt "input" +msgid "Email" +msgstr "Correu electrònic" + +#: web/template/login.html:24 +msgctxt "input" +msgid "Password" +msgstr "Contrasenya" + +#: web/template/login.html:27 +msgctxt "action" +msgid "Login" +msgstr "Entra" + +#: web/template/index.html:16 +msgctxt "action" +msgid "Logout" +msgstr "Surt" diff --git a/po/es.po b/po/es.po new file mode 100644 index 0000000..b7b2586 --- /dev/null +++ b/po/es.po @@ -0,0 +1,47 @@ +# Spanish translations for numerus package. +# Copyright (C) 2023 jordi fita mas +# This file is distributed under the same license as the numerus package. +# jordi fita mas , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: numerus\n" +"Report-Msgid-Bugs-To: jordi@tandem.blog\n" +"POT-Creation-Date: 2023-01-18 17:54+0100\n" +"PO-Revision-Date: 2023-01-18 17:45+0100\n" +"Last-Translator: jordi fita mas \n" +"Language-Team: Spanish \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: web/template/login.html:6 web/template/login.html:19 +msgctxt "title" +msgid "Login" +msgstr "Entrada" + +#: web/template/login.html:14 +msgid "Invalid user or password" +msgstr "Nombre de usuario o contraseña inválido" + +#: web/template/login.html:21 +msgctxt "input" +msgid "Email" +msgstr "Correo electrónico" + +#: web/template/login.html:24 +msgctxt "input" +msgid "Password" +msgstr "Contraseña" + +#: web/template/login.html:27 +msgctxt "action" +msgid "Login" +msgstr "Entrar" + +#: web/template/index.html:16 +msgctxt "action" +msgid "Logout" +msgstr "Salir" diff --git a/web/template/index.html b/web/template/index.html index a6c8e29..813b755 100644 --- a/web/template/index.html +++ b/web/template/index.html @@ -13,12 +13,11 @@
    -
  • +
-

Welcome

diff --git a/web/template/login.html b/web/template/login.html index f8ea6a3..1ac895a 100644 --- a/web/template/login.html +++ b/web/template/login.html @@ -3,7 +3,7 @@ - Login — Numerus + {{( pgettext "Login" "title" )}} — Numerus @@ -11,20 +11,20 @@ {{ if .LoginError }} {{ end }}
-

Login

+

{{( pgettext "Login" "title" )}}

- + - + - +