Add currency_pattern to language relation

The design calls for rendering all amounts with their currency symbol,
but golang.org/x/text’s currency package always render the symbol in
front, which is wrong in Catalan and Spanish, and a lot of other
languages.

Consulting the Internet, the most popular package for that is
accounting[0], which is almost as useless because they confuse locale
with the currency’s country of origin’s “usual locale” (e.g., en-US for
USD), which is also wrong: in Catalan i need to write USD prices as
"1.234,56 $" regardless of what Americans do.

With accounting i have the recourse of initializing the struct that
holds all the “locale” information, which is also wrong because i have
to define the decimal and thousands separators, something that depends
only on the locale, next to the currency’s precision, that is
locale-independent.  But, since all CLDR data from golang.org/x/text
is inside an internal package, i can not access it and would need to
define all that information myself, which defeats the purpose of using
an external package.

Since for now i only need the format pattern for currency, i just saved
it into the database of available languages, that i do not expect to
grow too much.

[0]: https://github.com/leekchan/accounting
This commit is contained in:
jordi fita mas 2023-02-23 12:12:33 +01:00
parent 97ef02b0f9
commit 8dbf8ef2d0
9 changed files with 43 additions and 23 deletions

View File

@ -4,10 +4,10 @@
begin; begin;
insert into public.language (lang_tag, name, endonym, selectable) insert into public.language (lang_tag, name, endonym, selectable, currency_pattern)
values ('und', 'Undefined', 'Undefined', false) values ('und', 'Undefined', 'Undefined', false, '%[3]s%.[1]*[2]f')
, ('ca', 'Catalan', 'català', true) , ('ca', 'Catalan', 'català', true, '%.[1]*[2]f %[3]s')
, ('es', 'Spanish', 'español', true) , ('es', 'Spanish', 'español', true, '%.[1]*[2]f %[3]s')
; ;
commit; commit;

View File

@ -9,7 +9,8 @@ create table language (
lang_tag text primary key check (length(lang_tag) < 36), -- RFC5646 recommends 35 at least lang_tag text primary key check (length(lang_tag) < 36), -- RFC5646 recommends 35 at least
name text not null, name text not null,
endonym text not null, endonym text not null,
selectable boolean not null selectable boolean not null,
currency_pattern text not null
); );
grant select on table language to guest; grant select on table language to guest;

View File

@ -12,27 +12,31 @@ const contextLocaleKey = "numerus-locale"
type Locale struct { type Locale struct {
*gotext.Locale *gotext.Locale
CurrencyPattern string
Language language.Tag Language language.Tag
} }
func NewLocale(lang language.Tag) *Locale { func NewLocale(lang availableLanguage) *Locale {
return &Locale{ return &Locale{
gotext.NewLocale("locales", lang.String()), gotext.NewLocale("locales", lang.tag.String()),
lang, lang.currencyPattern,
lang.tag,
} }
} }
func LocaleSetter(db *Db, next http.Handler) http.Handler { func LocaleSetter(db *Db, next http.Handler) http.Handler {
availableLanguages := mustGetAvailableLanguages(db) availableLanguages := mustGetAvailableLanguages(db)
var matcher = language.NewMatcher(availableLanguages)
locales := map[language.Tag]*Locale{} locales := map[language.Tag]*Locale{}
var tags []language.Tag
for _, lang := range availableLanguages { for _, lang := range availableLanguages {
locale := NewLocale(lang) locale := NewLocale(lang)
locale.AddDomain("numerus") locale.AddDomain("numerus")
locales[lang] = locale locales[lang.tag] = locale
tags = append(tags, lang.tag)
} }
defaultLocale := locales[language.Catalan] defaultLocale := locales[language.Catalan]
var matcher = language.NewMatcher(tags)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var locale *Locale var locale *Locale
@ -70,21 +74,27 @@ func gettext(str string, locale *Locale) string {
return locale.Get(str) return locale.Get(str)
} }
func mustGetAvailableLanguages(db *Db) []language.Tag { type availableLanguage struct {
rows, err := db.Query(context.Background(), "select lang_tag from language where selectable") tag language.Tag
currencyPattern string
}
func mustGetAvailableLanguages(db *Db) []availableLanguage {
rows, err := db.Query(context.Background(), "select lang_tag, currency_pattern from language where selectable")
if err != nil { if err != nil {
panic(err) panic(err)
} }
defer rows.Close() defer rows.Close()
var langs []language.Tag var langs []availableLanguage
for rows.Next() { for rows.Next() {
var langTag string var langTag string
err = rows.Scan(&langTag) var currencyPattern string
err = rows.Scan(&langTag, &currencyPattern)
if err != nil { if err != nil {
panic(err) panic(err)
} }
langs = append(langs, language.MustParse(langTag)) langs = append(langs, availableLanguage{language.MustParse(langTag), currencyPattern})
} }
if rows.Err() != nil { if rows.Err() != nil {
panic(rows.Err()) panic(rows.Err())

View File

@ -38,7 +38,7 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s
if err != nil { if err != nil {
f = math.NaN() f = math.NaN()
} }
return p.Sprintf("%.*f", company.DecimalDigits, number.Decimal(f)) return p.Sprintf(locale.CurrencyPattern, company.DecimalDigits, number.Decimal(f), company.CurrencySymbol)
}, },
"formatDate": func(time time.Time) string { "formatDate": func(time time.Time) string {
return time.Format("02/01/2006") return time.Format("02/01/2006")

View File

@ -7,7 +7,7 @@ begin;
set search_path to public; set search_path to public;
select plan(23); select plan(27);
select has_table('language'); select has_table('language');
select has_pk('language'); select has_pk('language');
@ -37,6 +37,11 @@ select col_type_is('language', 'selectable', 'boolean');
select col_not_null('language', 'selectable'); select col_not_null('language', 'selectable');
select col_hasnt_default('language', 'selectable'); select col_hasnt_default('language', 'selectable');
select has_column('language', 'currency_pattern');
select col_type_is('language', 'currency_pattern', 'text');
select col_not_null('language', 'currency_pattern');
select col_hasnt_default('language', 'currency_pattern');
select * select *
from finish(); from finish();

View File

@ -9,20 +9,23 @@ from language
where lang_tag = 'und' where lang_tag = 'und'
and name = 'Undefined' and name = 'Undefined'
and endonym = 'Undefined' and endonym = 'Undefined'
and not selectable; and not selectable
and currency_pattern = '%[3]s%.[1]*[2]f';
select 1 / count(*) select 1 / count(*)
from language from language
where lang_tag = 'ca' where lang_tag = 'ca'
and name = 'Catalan' and name = 'Catalan'
and endonym = 'català' and endonym = 'català'
and selectable; and selectable
and currency_pattern = '%.[1]*[2]f %[3]s';
select 1 / count(*) select 1 / count(*)
from language from language
where lang_tag = 'es' where lang_tag = 'es'
and name = 'Spanish' and name = 'Spanish'
and endonym = 'español' and endonym = 'español'
and selectable; and selectable
and currency_pattern = '%.[1]*[2]f %[3]s';
rollback; rollback;

View File

@ -6,6 +6,7 @@ select lang_tag
, name , name
, endonym , endonym
, selectable , selectable
, currency_pattern
from public.language from public.language
where false; where false;

View File

@ -49,7 +49,7 @@
<tr> <tr>
<td><input type="checkbox" name="id" id="new-product-id-{{$key}}" value="{{.Id}}"></td> <td><input type="checkbox" name="id" id="new-product-id-{{$key}}" value="{{.Id}}"></td>
<td><label for="new-product-id-{{$key}}">{{ .Name }}</label></td> <td><label for="new-product-id-{{$key}}">{{ .Name }}</label></td>
<td>{{ .Price | formatPrice }}</td> <td class="numeric">{{ .Price | formatPrice }}</td>
</tr> </tr>
{{- end }} {{- end }}
{{ else }} {{ else }}

View File

@ -29,7 +29,7 @@
<tr> <tr>
<td></td> <td></td>
<td><a href="{{ companyURI "/products/"}}{{ .Slug }}">{{ .Name }}</a></td> <td><a href="{{ companyURI "/products/"}}{{ .Slug }}">{{ .Name }}</a></td>
<td>{{ .Price | formatPrice }}</td> <td class="numeric">{{ .Price | formatPrice }}</td>
</tr> </tr>
{{- end }} {{- end }}
{{ else }} {{ else }}