Sessions: translate csv column names (#4761)
This commit is contained in:
parent
d0df52305b
commit
3353178160
12 changed files with 152 additions and 18 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
|
@ -231,5 +232,5 @@ type FeatureDescriber interface {
|
|||
|
||||
// CsvWriter converts to csv
|
||||
type CsvWriter interface {
|
||||
WriteCsv(io.Writer)
|
||||
WriteCsv(context.Context, io.Writer)
|
||||
}
|
||||
|
|
|
|||
8
assets/i18n/assets.go
Normal file
8
assets/i18n/assets.go
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package i18n
|
||||
|
||||
import (
|
||||
"embed"
|
||||
)
|
||||
|
||||
//go:embed *.toml
|
||||
var LocaleFS embed.FS
|
||||
|
|
@ -183,6 +183,16 @@ vehicle = "Fahrzeug"
|
|||
energy = "Geladen"
|
||||
date = "Beendet"
|
||||
|
||||
[sessions.csv]
|
||||
loadpoint = "Ladepunkt"
|
||||
vehicle = "Fahrzeug"
|
||||
identifier = "Kennung"
|
||||
chargedenergy = "Energie (kWh)"
|
||||
meterstart = "Anfangszählerstand (kWh)"
|
||||
meterstop = "Endzählerstand (kWh)"
|
||||
created = "Startzeit"
|
||||
finished = "Endzeit"
|
||||
|
||||
[offline]
|
||||
message = "Keine Verbindung zum Server."
|
||||
reload = "Reload?"
|
||||
|
|
|
|||
|
|
@ -183,6 +183,16 @@ vehicle = "Vehicle"
|
|||
energy = "Charged"
|
||||
date = "Finished"
|
||||
|
||||
[sessions.csv]
|
||||
loadpoint = "Loadpoint"
|
||||
vehicle = "Vehicle"
|
||||
identifier = "Identifier"
|
||||
chargedenergy = "Energy (kWh)"
|
||||
meterstart = "Meter Start (kWh)"
|
||||
meterstop = "Meter Stop (kWh)"
|
||||
created = "Created"
|
||||
finished = "Finished"
|
||||
|
||||
[offline]
|
||||
message = "No connection to server."
|
||||
reload = "Reload?"
|
||||
|
|
|
|||
|
|
@ -183,6 +183,16 @@ vehicle = "Automobilis"
|
|||
energy = "Įkrauta"
|
||||
date = "Baigta"
|
||||
|
||||
[sessions.csv]
|
||||
loadpoint = "Įkroviklis"
|
||||
vehicle = "Automobilis"
|
||||
identifier = "Identifikatorius"
|
||||
chargedenergy = "Energija (kWh)"
|
||||
meterstart = "Skaitiklis pradžioje (kWh)"
|
||||
meterstop = "Skaitiklis pabaigoje (kWh)"
|
||||
created = "Pradėta"
|
||||
finished = "Pabaigta"
|
||||
|
||||
[offline]
|
||||
message = "Nėra ryšio su serveriu."
|
||||
reload = "Perkrauti?"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/evcc-io/evcc/server/db"
|
||||
"github.com/evcc-io/evcc/tariff"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/locale"
|
||||
"github.com/evcc-io/evcc/util/machine"
|
||||
"github.com/evcc-io/evcc/util/pipe"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
|
|
@ -73,6 +74,11 @@ func configureEnvironment(cmd *cobra.Command, conf config) (err error) {
|
|||
err = sponsor.ConfigureSponsorship(conf.SponsorToken)
|
||||
}
|
||||
|
||||
// setup translations
|
||||
if err == nil {
|
||||
err = locale.Init()
|
||||
}
|
||||
|
||||
// setup persistence
|
||||
if err == nil && conf.Database.Dsn != "" {
|
||||
err = configureDatabase(conf.Database)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Session is a single charging session
|
||||
|
|
@ -36,15 +40,29 @@ type Sessions []Session
|
|||
|
||||
var _ api.CsvWriter = (*Sessions)(nil)
|
||||
|
||||
func (t *Sessions) writeHeader(ww *csv.Writer) {
|
||||
func (t *Sessions) writeHeader(ctx context.Context, ww *csv.Writer) {
|
||||
localizer := locale.Localizer
|
||||
if val := ctx.Value(locale.Locale).(string); val != "" {
|
||||
localizer = i18n.NewLocalizer(locale.Bundle, val, locale.Language)
|
||||
}
|
||||
|
||||
var row []string
|
||||
for _, f := range structs.Fields(Session{}) {
|
||||
caption := f.Tag("csv")
|
||||
switch {
|
||||
case caption == "-":
|
||||
csv := f.Tag("csv")
|
||||
if csv == "-" {
|
||||
continue
|
||||
case caption == "":
|
||||
caption = f.Name()
|
||||
}
|
||||
|
||||
caption, err := localizer.Localize(&locale.Config{
|
||||
MessageID: "sessions.csv." + strings.ToLower(f.Name()),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if csv != "" {
|
||||
caption = csv
|
||||
} else {
|
||||
caption = f.Name()
|
||||
}
|
||||
}
|
||||
|
||||
row = append(row, caption)
|
||||
|
|
@ -59,17 +77,17 @@ func (t *Sessions) writeRow(ww *csv.Writer, r Session) {
|
|||
continue
|
||||
}
|
||||
|
||||
val := fmt.Sprintf("%v", f.Value())
|
||||
var val string
|
||||
|
||||
switch v := f.Value().(type) {
|
||||
case float64:
|
||||
val = strconv.FormatFloat(v, 'f', 3, 64)
|
||||
case time.Time:
|
||||
if v.IsZero() {
|
||||
val = ""
|
||||
} else {
|
||||
if !v.IsZero() {
|
||||
val = v.Local().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
default:
|
||||
val = fmt.Sprintf("%v", f.Value())
|
||||
}
|
||||
|
||||
row = append(row, val)
|
||||
|
|
@ -79,9 +97,9 @@ func (t *Sessions) writeRow(ww *csv.Writer, r Session) {
|
|||
}
|
||||
|
||||
// WriteCsv implements the api.CsvWriter interface
|
||||
func (t *Sessions) WriteCsv(w io.Writer) {
|
||||
func (t *Sessions) WriteCsv(ctx context.Context, w io.Writer) {
|
||||
ww := csv.NewWriter(w)
|
||||
t.writeHeader(ww)
|
||||
t.writeHeader(ctx, ww)
|
||||
|
||||
for _, r := range *t {
|
||||
t.writeRow(ww, r)
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
2
dist/index.html
vendored
2
dist/index.html
vendored
|
|
@ -20,7 +20,7 @@
|
|||
<meta name="theme-color" content="#020318" />
|
||||
|
||||
<title>evcc</title>
|
||||
<script type="module" crossorigin src="./assets/index.15ca0e2e.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index.34c57bb3.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.0f7f1beb.css">
|
||||
</head>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -16,6 +17,7 @@ import (
|
|||
"github.com/evcc-io/evcc/core/site"
|
||||
dbserver "github.com/evcc-io/evcc/server/db"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/locale"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
|
|
@ -68,12 +70,12 @@ func jsonError(w http.ResponseWriter, status int, err error) {
|
|||
jsonWrite(w, map[string]interface{}{"error": err.Error()})
|
||||
}
|
||||
|
||||
func csvResult(w http.ResponseWriter, res any) {
|
||||
func csvResult(ctx context.Context, w http.ResponseWriter, res any) {
|
||||
w.Header().Set("Content-Type", "text/csv")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="sessions.csv"`)
|
||||
|
||||
if ww, ok := res.(api.CsvWriter); ok {
|
||||
ww.WriteCsv(w)
|
||||
ww.WriteCsv(ctx, w)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -163,7 +165,9 @@ func sessionHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
if r.URL.Query().Get("format") == "csv" {
|
||||
csvResult(w, &res)
|
||||
accept := r.Header.Get("Accept-Language")
|
||||
ctx := context.WithValue(context.Background(), locale.Locale, accept)
|
||||
csvResult(ctx, w, &res)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
6
util/locale/internal/types.go
Normal file
6
util/locale/internal/types.go
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
package internal
|
||||
|
||||
// ContextKey is just an empty struct. It exists so context values can be
|
||||
// an immutable public variable with a unique type. It's immutable
|
||||
// because nobody else can create a ContextKey, being unexported.
|
||||
type ContextKey struct{}
|
||||
61
util/locale/locale.go
Normal file
61
util/locale/locale.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package locale
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/cloudfoundry/jibber_jabber"
|
||||
assets "github.com/evcc-io/evcc/assets/i18n"
|
||||
"github.com/evcc-io/evcc/util/locale/internal"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type Config = i18n.LocalizeConfig
|
||||
|
||||
var (
|
||||
Locale internal.ContextKey
|
||||
|
||||
Bundle *i18n.Bundle
|
||||
Language string
|
||||
Localizer *i18n.Localizer
|
||||
)
|
||||
|
||||
func Init() error {
|
||||
Bundle = i18n.NewBundle(language.English)
|
||||
Bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
||||
|
||||
dir, err := assets.LocaleFS.ReadDir(".")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, d := range dir {
|
||||
if _, err := Bundle.LoadMessageFileFS(assets.LocaleFS, d.Name()); err != nil {
|
||||
return fmt.Errorf("loading locales failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
Language, err := jibber_jabber.DetectLanguage()
|
||||
if err != nil {
|
||||
Language = "de"
|
||||
}
|
||||
|
||||
Localizer = i18n.NewLocalizer(Bundle, Language)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Localize(lc *Config) string {
|
||||
msg, _, err := Localizer.LocalizeWithTag(lc)
|
||||
if err != nil {
|
||||
msg = lc.MessageID
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func LocalizeID(id string) string {
|
||||
return Localize(&Config{
|
||||
MessageID: id,
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue