From e38420697bf4c0395059b9f4af85ad3dd75b4df3 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Wed, 18 Jan 2023 19:07:42 +0100 Subject: [PATCH] Add Catalan and Spanish translation with gotext[3] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitignore | 2 ++ Makefile | 17 ++++++++++++++ debian/control | 5 ++++- debian/numerus.install | 1 + debian/rules | 3 +++ go.mod | 7 ++++-- go.sum | 10 +++++++++ pkg/locale.go | 50 +++++++++++++++++++++++++++++++++++++++++ pkg/login.go | 39 ++++++++++++++------------------ pkg/router.go | 21 +++-------------- pkg/template.go | 22 ++++++++++++++++++ po/ca.po | 47 ++++++++++++++++++++++++++++++++++++++ po/es.po | 47 ++++++++++++++++++++++++++++++++++++++ web/template/index.html | 3 +-- web/template/login.html | 12 +++++----- 15 files changed, 235 insertions(+), 51 deletions(-) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 pkg/locale.go create mode 100644 pkg/template.go create mode 100644 po/ca.po create mode 100644 po/es.po 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" )}}

- + - + - +