Add the skeleton of the web application
It does nothing more than to server a single page that does nothing
interesting.
This time i do not use a router. Instead, i am trying out a technique
i have seen in an article[0] that i have tried in other, smaller,
projects and seems to work surprisingly well: it just “cuts off” the
URI path by path, passing the request from handler to handler until
it finds its way to a handler that actually serves the request.
That helps to loosen the coupling between the application and lower
handlers, and makes dependencies explicit, because i need to pass the
locale, company, etc. down instead of storing them in contexts. Let’s
see if i do not regret it on a later date.
I also made a lot more packages that in Numerus. In Numerus i actually
only have the single pkg package, and it works, kind of, but i notice
how i name my methods to avoid clashing instead of using packages for
that. That is, instead of pkg.NewApp i now have app.New.
Initially i thought that Locale should be inside app, but then there was
a circular dependency between app and template. That is why i created a
separate package, but now i am wondering if template should be inside
app too, but then i would have app.MustRenderTemplate instead of
template.MustRender.
The CSS is the most bare-bones file i could write because i am focusing
in markup right now; Oriol will fill in the file once the application is
working.
[0]: https://blog.merovius.de/posts/2017-06-18-how-not-to-use-an-http-router/
2023-07-22 22:11:00 +00:00
|
|
|
/*
|
|
|
|
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
|
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
*/
|
|
|
|
|
|
|
|
package locale
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"github.com/leonelquinteros/gotext"
|
|
|
|
"golang.org/x/text/language"
|
|
|
|
|
|
|
|
"dev.tandem.ws/tandem/camper/pkg/database"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Locale struct {
|
|
|
|
*gotext.Locale
|
|
|
|
CurrencyPattern string
|
|
|
|
Language language.Tag
|
|
|
|
}
|
|
|
|
|
|
|
|
type Locales map[language.Tag]*Locale
|
|
|
|
|
|
|
|
func (m Locales) Tags() []language.Tag {
|
|
|
|
keys := make([]language.Tag, len(m))
|
|
|
|
i := 0
|
|
|
|
for k := range m {
|
|
|
|
keys[i] = k
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
return keys
|
|
|
|
}
|
|
|
|
|
|
|
|
func MustGetAll(db *database.DB) Locales {
|
|
|
|
availableLanguages := mustGetAvailableLanguages(db)
|
|
|
|
locales := map[language.Tag]*Locale{}
|
|
|
|
for _, lang := range availableLanguages {
|
|
|
|
locale := newLocale(lang)
|
|
|
|
locale.AddDomain("camper")
|
|
|
|
locales[lang.tag] = locale
|
|
|
|
}
|
|
|
|
return locales
|
|
|
|
}
|
|
|
|
|
|
|
|
func newLocale(lang availableLanguage) *Locale {
|
|
|
|
return &Locale{
|
|
|
|
gotext.NewLocale("locale", lang.tag.String()),
|
|
|
|
lang.currencyPattern,
|
|
|
|
lang.tag,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-24 15:09:43 +00:00
|
|
|
func (l *Locale) Gettext(str string) string {
|
|
|
|
return l.GetD(l.GetDomain(), str)
|
|
|
|
}
|
|
|
|
|
2023-07-26 18:46:09 +00:00
|
|
|
func (l *Locale) Pgettext(str string, ctx string) string {
|
|
|
|
return l.GetDC(l.GetDomain(), str, ctx)
|
|
|
|
}
|
|
|
|
|
2023-07-24 15:09:43 +00:00
|
|
|
func (l *Locale) GettextNoop(str string) string {
|
|
|
|
return str
|
|
|
|
}
|
|
|
|
|
Get user from database based on cookie and serve login if not logged in
To get the user from the database i have to set the cookie first, that
was already done in database.MustAcquire, but i thought they were too
far apart, even thought they are so related. So, the cookie, and thus
the role, is set when getting the user, that is actually the first thing
to do once the connection is acquired. However, that way the database
package has no knowledge of cookies, and the code that sets the cookie
and retrieves the user are next to each other.
I applied the same logic to the changes of locale.Match: it has not
business knowing that the accept language string comes from a request;
it only needs the actual string. Also, the TODO comment about getting
the user’s locale made no sense, now, because app already knows that
locale, so there is no need to pass the user to the locale package.
Getting the locale is done after retrieving the user from the database,
for the same reason the connection is Acquired as far up as possible:
almost every request will need this value, together with the user and
the database connection.
I am a bit affraid that i will end up with functions that always expect
these three values. Maybe i can put the locale inside user, as it is
the user’s locale, after all, no matter if it came from the database or
the user agent, but connection and user must be separate, i think.
We’ll see.
2023-07-25 23:50:39 +00:00
|
|
|
func Match(acceptLanguage string, locales Locales, matcher language.Matcher) *Locale {
|
|
|
|
t, _, err := language.ParseAcceptLanguage(acceptLanguage)
|
|
|
|
if err != nil {
|
|
|
|
return nil
|
Add the skeleton of the web application
It does nothing more than to server a single page that does nothing
interesting.
This time i do not use a router. Instead, i am trying out a technique
i have seen in an article[0] that i have tried in other, smaller,
projects and seems to work surprisingly well: it just “cuts off” the
URI path by path, passing the request from handler to handler until
it finds its way to a handler that actually serves the request.
That helps to loosen the coupling between the application and lower
handlers, and makes dependencies explicit, because i need to pass the
locale, company, etc. down instead of storing them in contexts. Let’s
see if i do not regret it on a later date.
I also made a lot more packages that in Numerus. In Numerus i actually
only have the single pkg package, and it works, kind of, but i notice
how i name my methods to avoid clashing instead of using packages for
that. That is, instead of pkg.NewApp i now have app.New.
Initially i thought that Locale should be inside app, but then there was
a circular dependency between app and template. That is why i created a
separate package, but now i am wondering if template should be inside
app too, but then i would have app.MustRenderTemplate instead of
template.MustRender.
The CSS is the most bare-bones file i could write because i am focusing
in markup right now; Oriol will fill in the file once the application is
working.
[0]: https://blog.merovius.de/posts/2017-06-18-how-not-to-use-an-http-router/
2023-07-22 22:11:00 +00:00
|
|
|
}
|
Get user from database based on cookie and serve login if not logged in
To get the user from the database i have to set the cookie first, that
was already done in database.MustAcquire, but i thought they were too
far apart, even thought they are so related. So, the cookie, and thus
the role, is set when getting the user, that is actually the first thing
to do once the connection is acquired. However, that way the database
package has no knowledge of cookies, and the code that sets the cookie
and retrieves the user are next to each other.
I applied the same logic to the changes of locale.Match: it has not
business knowing that the accept language string comes from a request;
it only needs the actual string. Also, the TODO comment about getting
the user’s locale made no sense, now, because app already knows that
locale, so there is no need to pass the user to the locale package.
Getting the locale is done after retrieving the user from the database,
for the same reason the connection is Acquired as far up as possible:
almost every request will need this value, together with the user and
the database connection.
I am a bit affraid that i will end up with functions that always expect
these three values. Maybe i can put the locale inside user, as it is
the user’s locale, after all, no matter if it came from the database or
the user agent, but connection and user must be separate, i think.
We’ll see.
2023-07-25 23:50:39 +00:00
|
|
|
var locale *Locale
|
|
|
|
tag, _, _ := matcher.Match(t...)
|
|
|
|
var ok bool
|
|
|
|
locale, ok = locales[tag]
|
|
|
|
for !ok && !tag.IsRoot() {
|
|
|
|
tag = tag.Parent()
|
|
|
|
locale, ok = locales[tag]
|
Add the skeleton of the web application
It does nothing more than to server a single page that does nothing
interesting.
This time i do not use a router. Instead, i am trying out a technique
i have seen in an article[0] that i have tried in other, smaller,
projects and seems to work surprisingly well: it just “cuts off” the
URI path by path, passing the request from handler to handler until
it finds its way to a handler that actually serves the request.
That helps to loosen the coupling between the application and lower
handlers, and makes dependencies explicit, because i need to pass the
locale, company, etc. down instead of storing them in contexts. Let’s
see if i do not regret it on a later date.
I also made a lot more packages that in Numerus. In Numerus i actually
only have the single pkg package, and it works, kind of, but i notice
how i name my methods to avoid clashing instead of using packages for
that. That is, instead of pkg.NewApp i now have app.New.
Initially i thought that Locale should be inside app, but then there was
a circular dependency between app and template. That is why i created a
separate package, but now i am wondering if template should be inside
app too, but then i would have app.MustRenderTemplate instead of
template.MustRender.
The CSS is the most bare-bones file i could write because i am focusing
in markup right now; Oriol will fill in the file once the application is
working.
[0]: https://blog.merovius.de/posts/2017-06-18-how-not-to-use-an-http-router/
2023-07-22 22:11:00 +00:00
|
|
|
}
|
|
|
|
return locale
|
|
|
|
}
|
|
|
|
|
|
|
|
type availableLanguage struct {
|
|
|
|
tag language.Tag
|
|
|
|
currencyPattern string
|
|
|
|
}
|
|
|
|
|
|
|
|
func mustGetAvailableLanguages(db *database.DB) []availableLanguage {
|
|
|
|
rows, err := db.Query(context.Background(), "select lang_tag, currency_pattern from language where selectable")
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
var languages []availableLanguage
|
|
|
|
for rows.Next() {
|
|
|
|
var langTag string
|
|
|
|
var currencyPattern string
|
|
|
|
err = rows.Scan(&langTag, ¤cyPattern)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
languages = append(languages, availableLanguage{language.MustParse(langTag), currencyPattern})
|
|
|
|
}
|
|
|
|
if rows.Err() != nil {
|
|
|
|
panic(rows.Err())
|
|
|
|
}
|
|
|
|
|
|
|
|
return languages
|
|
|
|
}
|