From 3f8eba029b83e30268a8c4eab0fddb68a2d05e2c Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 15 Nov 2022 10:15:24 +0100 Subject: [PATCH] Sessions: locale-aware csv formatting (#5136) --- api/api.go | 2 +- core/db/session.go | 59 ++++++++++++++++++++++++++++++++++-------- server/http_handler.go | 12 ++++++--- util/locale/locale.go | 4 +-- 4 files changed, 60 insertions(+), 17 deletions(-) diff --git a/api/api.go b/api/api.go index 031c3ec6f..271d0f9f8 100644 --- a/api/api.go +++ b/api/api.go @@ -232,5 +232,5 @@ type FeatureDescriber interface { // CsvWriter converts to csv type CsvWriter interface { - WriteCsv(context.Context, io.Writer) + WriteCsv(context.Context, io.Writer) error } diff --git a/core/db/session.go b/core/db/session.go index 98f3d5da7..f28283112 100644 --- a/core/db/session.go +++ b/core/db/session.go @@ -5,7 +5,6 @@ import ( "encoding/csv" "fmt" "io" - "strconv" "strings" "time" @@ -13,6 +12,9 @@ import ( "github.com/evcc-io/evcc/util/locale" "github.com/fatih/structs" "github.com/nicksnyder/go-i18n/v2/i18n" + "golang.org/x/text/language" + "golang.org/x/text/message" + "golang.org/x/text/number" ) // Session is a single charging session @@ -23,7 +25,7 @@ type Session struct { Loadpoint string `json:"loadpoint"` Identifier string `json:"identifier"` Vehicle string `json:"vehicle"` - Odometer float64 `json:"odometer"` + Odometer float64 `json:"odometer" format:"int"` MeterStart float64 `json:"meterStart" csv:"Meter Start (kWh)" gorm:"column:meter_start_kwh"` MeterStop float64 `json:"meterStop" csv:"Meter Stop (kWh)" gorm:"column:meter_end_kwh"` ChargedEnergy float64 `json:"chargedEnergy" csv:"Charged Energy (kWh)" gorm:"column:charged_kwh"` @@ -43,7 +45,7 @@ type Sessions []Session var _ api.CsvWriter = (*Sessions)(nil) -func (t *Sessions) writeHeader(ctx context.Context, ww *csv.Writer) { +func (t *Sessions) writeHeader(ctx context.Context, ww *csv.Writer) error { localizer := locale.Localizer if val := ctx.Value(locale.Locale).(string); val != "" { localizer = i18n.NewLocalizer(locale.Bundle, val, locale.Language) @@ -70,10 +72,11 @@ func (t *Sessions) writeHeader(ctx context.Context, ww *csv.Writer) { row = append(row, caption) } - _ = ww.Write(row) + + return ww.Write(row) } -func (t *Sessions) writeRow(ww *csv.Writer, r Session) { +func (t *Sessions) writeRow(ww *csv.Writer, mp *message.Printer, r Session) error { var row []string for _, f := range structs.Fields(r) { if f.Tag("csv") == "-" { @@ -81,10 +84,16 @@ func (t *Sessions) writeRow(ww *csv.Writer, r Session) { } var val string + format := f.Tag("format") switch v := f.Value().(type) { case float64: - val = strconv.FormatFloat(v, 'f', 3, 64) + switch format { + case "int": + val = mp.Sprint(number.Decimal(v, number.MaxFractionDigits(0))) + default: + val = mp.Sprint(number.Decimal(v, number.MaxFractionDigits(3))) + } case time.Time: if !v.IsZero() { val = v.Local().Format("2006-01-02 15:04:05") @@ -96,17 +105,45 @@ func (t *Sessions) writeRow(ww *csv.Writer, r Session) { row = append(row, val) } - _ = ww.Write(row) + return ww.Write(row) } // WriteCsv implements the api.CsvWriter interface -func (t *Sessions) WriteCsv(ctx context.Context, w io.Writer) { - ww := csv.NewWriter(w) - t.writeHeader(ctx, ww) +func (t *Sessions) WriteCsv(ctx context.Context, w io.Writer) error { + if _, err := w.Write([]byte{0xEF, 0xBB, 0xBF}); err != nil { + return err + } + // get context language + lang := locale.Language + if language, ok := ctx.Value(locale.Locale).(string); ok && language != "" { + lang = language + } + + tag, err := language.Parse(lang) + if err != nil { + return err + } + + ww := csv.NewWriter(w) + + // set separator according to locale + if b, _ := tag.Base(); b.String() == language.German.String() { + ww.Comma = ';' + } + + if err := t.writeHeader(ctx, ww); err != nil { + return err + } + + mp := message.NewPrinter(tag) for _, r := range *t { - t.writeRow(ww, r) + if err := t.writeRow(ww, mp, r); err != nil { + return err + } } ww.Flush() + + return ww.Error() } diff --git a/server/http_handler.go b/server/http_handler.go index a3362a5ae..c5aaf8398 100644 --- a/server/http_handler.go +++ b/server/http_handler.go @@ -19,6 +19,7 @@ import ( "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/locale" "github.com/gorilla/mux" + "golang.org/x/text/language" ) func indexHandler() http.HandlerFunc { @@ -75,7 +76,7 @@ func csvResult(ctx context.Context, w http.ResponseWriter, res any) { w.Header().Set("Content-Disposition", `attachment; filename="sessions.csv"`) if ww, ok := res.(api.CsvWriter); ok { - ww.WriteCsv(ctx, w) + _ = ww.WriteCsv(ctx, w) } else { w.WriteHeader(http.StatusInternalServerError) } @@ -193,8 +194,13 @@ func sessionHandler(w http.ResponseWriter, r *http.Request) { } if r.URL.Query().Get("format") == "csv" { - accept := r.Header.Get("Accept-Language") - ctx := context.WithValue(context.Background(), locale.Locale, accept) + // get request language + lang := r.Header.Get("Accept-Language") + if tags, _, err := language.ParseAcceptLanguage(lang); err == nil && len(tags) > 0 { + lang = tags[0].String() + } + + ctx := context.WithValue(context.Background(), locale.Locale, lang) csvResult(ctx, w, &res) return } diff --git a/util/locale/locale.go b/util/locale/locale.go index 479b79a80..cb965de09 100644 --- a/util/locale/locale.go +++ b/util/locale/locale.go @@ -36,9 +36,9 @@ func Init() error { } } - Language, err := jibber_jabber.DetectLanguage() + Language, err = jibber_jabber.DetectLanguage() if err != nil { - Language = "de" + Language = language.German.String() } Localizer = i18n.NewLocalizer(Bundle, Language)