Sessions: locale-aware csv formatting (#5136)
This commit is contained in:
parent
ea2c3d53fe
commit
3f8eba029b
4 changed files with 60 additions and 17 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue