Skip to content

Commit

Permalink
Sessions: locale-aware csv formatting (#5136)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig committed Nov 15, 2022
1 parent ea2c3d5 commit 3f8eba0
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 16 deletions.
2 changes: 1 addition & 1 deletion api/api.go
Expand Up @@ -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
}
57 changes: 47 additions & 10 deletions core/db/session.go
Expand Up @@ -5,14 +5,16 @@ import (
"encoding/csv"
"fmt"
"io"
"strconv"
"strings"
"time"

"github.com/evcc-io/evcc/api"
"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
Expand All @@ -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"`
Expand All @@ -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)
Expand All @@ -70,21 +72,28 @@ 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") == "-" {
continue
}

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")
Expand All @@ -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) {
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)
t.writeHeader(ctx, ww)

// 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()
}
12 changes: 9 additions & 3 deletions server/http_handler.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions util/locale/locale.go
Expand Up @@ -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)
Expand Down

0 comments on commit 3f8eba0

Please sign in to comment.