Add Catalan and Spanish translation with gotext[3]
I had to choose between [1], [2], and [3]. As far as i could find, [1] is not easy to work with templates[4] and at the moment is not maintained[5]. Both [2] and [3] use the same approach to be used from within templates: you have to define a FuncMap with template functions that call the message catalog. Also, both libraries seems to be reasonably maintained, and have packages in Debian’s repository. However, [2] repeats the same mistakes that POSIX did with its catalogs—using identifiers that are not the strings in the source language—, however this time the catalogs are written in JSON or YAML! This, somehow, makes things worse…. [3], the one i settled with, is fine and decently maintained. There are some surprising things, such as to be able to use directly the PO file, and that it has higher priority over the corresponding MO, or that the order of parameters is reversed in respect to gettext. However, it uses a saner format, and is a lot easier to work with than [3]. The problem, of course, is that xgettext does not know how to find translatable strings inside the template. [3] includes a CLI tool similar to xgettext, but is not a drop-in replacement[6] and does not process templates. The proper way to handle this would be to add a parser to xgettext, but for now i found out that if i surround the call to the translation functions from within the template with parentheses, i can trick xgettext into believing it is parsing Scheme code, and extracts the strings successfully—at least, for what i have tried. Had to add the keyword for pgettext, because Schemed does not have it, but at least i can do that with command line parameters. For now i left only Spanish and Catalan as the two available languages, even though the source text is written in English, because that way i can make sure i do not leave strings untranslated. [1]: https://golang.org/x/text [2]: https://github.com/nicksnyder/go-i18n [3]: https://github.com/leonelquinteros/gotext [4]: https://github.com/golang/go/issues/39954 [5]: https://github.com/golang/go/issues/12750 [6]: https://github.com/leonelquinteros/gotext/issues/38
This commit is contained in:
parent
afd4bc16b7
commit
e38420697b
|
@ -0,0 +1,2 @@
|
|||
locales/
|
||||
po/*.pot
|
|
@ -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 $^
|
|
@ -5,8 +5,11 @@ Maintainer: jordi fita mas <jordi@tandem.blog>
|
|||
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.
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
usr/bin/numerus usr/bin
|
||||
locales usr/share/numerus
|
||||
web usr/share/numerus
|
||||
|
|
|
@ -2,3 +2,6 @@
|
|||
|
||||
%:
|
||||
dh $@ --builddirectory=_build --buildsystem=golang --with=golang
|
||||
|
||||
execute_before_dh_auto_build:
|
||||
make
|
||||
|
|
7
go.mod
7
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
|
||||
)
|
||||
|
|
10
go.sum
10
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=
|
||||
|
|
|
@ -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)
|
||||
}
|
39
pkg/login.go
39
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)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 <jordi@tandem.blog>, 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 <jordi@tandem.blog>\n"
|
||||
"Language-Team: Catalan <ca@dodds.net>\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"
|
|
@ -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 <jordi@tandem.blog>, 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 <jordi@tandem.blog>\n"
|
||||
"Language-Team: Spanish <es@tp.org.es>\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"
|
|
@ -13,12 +13,11 @@
|
|||
<button aria-haspopup="true">
|
||||
<svg role="image" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="50" height="50"><path fill="none" d="M0 0h24v24H0z"/><path d="M9.342 18.782l-1.931-.518.787-2.939a10.988 10.988 0 0 1-3.237-1.872l-2.153 2.154-1.415-1.415 2.154-2.153a10.957 10.957 0 0 1-2.371-5.07l1.968-.359C3.903 10.812 7.579 14 12 14c4.42 0 8.097-3.188 8.856-7.39l1.968.358a10.957 10.957 0 0 1-2.37 5.071l2.153 2.153-1.415 1.415-2.153-2.154a10.988 10.988 0 0 1-3.237 1.872l.787 2.94-1.931.517-.788-2.94a11.072 11.072 0 0 1-3.74 0l-.788 2.94z"/></svg></button>
|
||||
<ul>
|
||||
<li><form method="POST" action="/logout"><button type="submit">Logout</button></form></li>
|
||||
<li><form method="POST" action="/logout"><button type="submit">{{( pgettext "Logout" "action" )}}</button></form></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<h2>Welcome</h2>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Login — Numerus</title>
|
||||
<title>{{( pgettext "Login" "title" )}} — Numerus</title>
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="/static/numerus.css">
|
||||
</head>
|
||||
<body class="web">
|
||||
|
@ -11,20 +11,20 @@
|
|||
|
||||
{{ if .LoginError }}
|
||||
<div class="error" role="alert">
|
||||
<p>{{ .LoginError }}</p>
|
||||
<p>{{( gettext "Invalid user or password" )}}</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<section id="login">
|
||||
<h2>Login</h2>
|
||||
<h2>{{( pgettext "Login" "title" )}}</h2>
|
||||
<form method="POST" action="/login">
|
||||
<label for="user_email">Email</label>
|
||||
<label for="user_email">{{( pgettext "Email" "input" )}}</label>
|
||||
<input id="user_email" type="email" required autofocus name="email" autocapitalize="none" value="{{ .Email }}">
|
||||
|
||||
<label for="user_password">Password</label>
|
||||
<label for="user_password">{{( pgettext "Password" "input" )}}</label>
|
||||
<input id="user_password" type="password" required name="password" autocomplete="current-password" value="{{ .Password }}">
|
||||
|
||||
<button type="submit">Login</button>
|
||||
<button type="submit">{{( pgettext "Login" "action" )}}</button>
|
||||
</form>
|
||||
</section>
|
||||
</body>
|
||||
|
|
Loading…
Reference in New Issue