Translate the dashboard to English; euro instead of lira

Dashboard: every label, empty state, chart legend and the login page
are English; numbers and dates use en-GB (24-hour clock, day-month
dates), money is EUR, percentages read 80% instead of %80, durations
"2 h 05 min". html lang is en. Server version 1.1.2; manifests pin it.

Car app: the charging tab's price suffix, validation message and total
cost use € instead of ₺.

Checked by rendering all three pages from the 1.1.2 image against
InfluxDB 2.7.12 in podman.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
2026-10-17 14:39:59 +02:00
co-authored by Claude Opus 5.5
parent 818442db08
commit 538d03361d
14 changed files with 119 additions and 119 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "nilsmg4-web",
"version": "1.1.1",
"version": "1.1.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nilsmg4-web",
"version": "1.1.1",
"version": "1.1.2",
"dependencies": {
"@influxdata/influxdb-client": "^1.35.0",
"next": "^15.5.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "nilsmg4-web",
"version": "1.1.1",
"version": "1.1.2",
"private": true,
"scripts": {
"dev": "next dev",
+21 -21
View File
@@ -12,10 +12,10 @@ export default async function ChargingPage() {
if (!installationId || sessions.length === 0) {
return (
<Shell active="/charging" title="Şarj">
<Shell active="/charging" title="Charging">
<div className="card">
<p className="empty">
Şarj kaydı yok. Yalnızca 10 kW üstü DC şarjlar ölçülür; AC şarj kaydedilmez.
No charging sessions yet. Only DC charges above 10 kW are metered; AC charging is not recorded.
</p>
</div>
</Shell>
@@ -25,22 +25,22 @@ export default async function ChargingPage() {
const total = sumCharging(sessions);
return (
<Shell active="/charging" title="Şarj" subtitle={`${sessions.length} DC seans`}>
<Shell active="/charging" title="Charging" subtitle={`${sessions.length} DC session${sessions.length === 1 ? "" : "s"}`}>
<div className="tiles">
<Tile label="Bataryaya giren net" value={precise(total.energyKwh)} unit="kWh" tone="gold" />
<Tile label="Şebekeden alınan (+%10)" value={precise(total.gridEnergyKwh)} unit="kWh" tone="gold" />
<Tile label="Toplam ödenen" value={money(total.cost)} tone="gold" />
<Tile label="Net into battery" value={precise(total.energyKwh)} unit="kWh" tone="gold" />
<Tile label="Drawn from grid (+10%)" value={precise(total.gridEnergyKwh)} unit="kWh" tone="gold" />
<Tile label="Total paid" value={money(total.cost)} tone="gold" />
<Tile
label="Ortalama birim"
label="Average price"
value={total.gridEnergyKwh > 0 ? money(total.cost / total.gridEnergyKwh) : "—"}
note="kWh başına"
note="per kWh"
tone="teal"
/>
<Tile label="En yüksek güç" value={num(total.peakKw)} unit="kW" tone="teal" />
<Tile label="Peak power" value={num(total.peakKw)} unit="kW" tone="teal" />
</div>
<section className="card">
<h2 className="card-title">Seanslar</h2>
<h2 className="card-title">Sessions</h2>
<ul className="list">
{sessions.map((session) => {
const gained =
@@ -59,7 +59,7 @@ export default async function ChargingPage() {
<div className="session-meta">
{session.isDemo && "Demo · "}
{duration(session.durationSeconds)}
{gained !== null && ` · %${num(session.startSocPercent as number)} → %${num(session.endSocPercent as number)}`}
{gained !== null && ` · ${num(session.startSocPercent as number)}% → ${num(session.endSocPercent as number)}%`}
</div>
</div>
<div className="session-energy">
@@ -77,16 +77,16 @@ export default async function ChargingPage() {
}))}
/>
<div className="rows" style={{ marginTop: 14 }}>
<Row label="Başlangıç" value={dateTime(session.startedAt)} />
<Row label="Bitiş" value={dateTime(session.endedAt)} />
<Row label="Ölçülen süre" value={duration(session.durationSeconds)} />
<Row label="Bataryaya giren net" value={`${precise(session.energyKwh)} kWh`} />
<Row label="Şebekeden alınan (+%10)" value={`${precise(session.gridEnergyKwh ?? session.energyKwh * 1.1)} kWh`} />
<Row label="Kazanılan doluluk" value={gained === null ? "—" : `%${num(gained)}`} />
<Row label="Tepe güç" value={peak > 0 ? `${num(peak)} kW` : "—"} />
<Row label="Ortalama güç" value={average > 0 ? `${num(average)} kW` : "—"} />
<Row label="Birim fiyat" value={`${money(session.pricePerKwh)}/kWh`} />
<Row label="Tutar" value={money(session.totalCost)} />
<Row label="Start" value={dateTime(session.startedAt)} />
<Row label="End" value={dateTime(session.endedAt)} />
<Row label="Metered duration" value={duration(session.durationSeconds)} />
<Row label="Net into battery" value={`${precise(session.energyKwh)} kWh`} />
<Row label="Drawn from grid (+10%)" value={`${precise(session.gridEnergyKwh ?? session.energyKwh * 1.1)} kWh`} />
<Row label="Charge added" value={gained === null ? "—" : `${num(gained)}%`} />
<Row label="Peak power" value={peak > 0 ? `${num(peak)} kW` : "—"} />
<Row label="Average power" value={average > 0 ? `${num(average)} kW` : "—"} />
<Row label="Unit price" value={`${money(session.pricePerKwh)}/kWh`} />
<Row label="Amount" value={money(session.totalCost)} />
</div>
</div>
</details>
+31 -31
View File
@@ -18,9 +18,9 @@ export default async function ConsumptionPage({
if (!installationId || days.length === 0) {
return (
<Shell active="/consumption" title="Tüketim">
<Shell active="/consumption" title="Consumption">
<div className="card">
<p className="empty">Günlük tüketim kaydı yok. Araç günü kapattığında yüklenir.</p>
<p className="empty">No daily consumption records yet. The car uploads them as it drives.</p>
</div>
</Shell>
);
@@ -46,14 +46,14 @@ export default async function ConsumptionPage({
return (
<Shell
active="/consumption"
title="Tüketim"
subtitle={hasFilter ? (validFilter ? isoDate(selectedDate!) : "Seçili gün kaydı yok") : `${days.length} günlük kayıt`}
title="Consumption"
subtitle={hasFilter ? (validFilter ? isoDate(selectedDate!) : "No record for the selected day") : `${days.length} day${days.length === 1 ? "" : "s"} recorded`}
>
{/* Date filter */}
<section className="card">
<form className="filter-form" method="get" action="/consumption">
<label className="filter-label" htmlFor="date-picker">
Tarihe göre filtrele
Filter by date
</label>
<div className="filter-row">
<input
@@ -65,9 +65,9 @@ export default async function ConsumptionPage({
max={maxDate}
className="field date-input"
/>
<button className="button filter-button" type="submit">Filtrele</button>
<button className="button filter-button" type="submit">Filter</button>
{hasFilter && (
<Link className="filter-clear" href="/consumption">Temizle</Link>
<Link className="filter-clear" href="/consumption">Clear</Link>
)}
</div>
</form>
@@ -80,60 +80,60 @@ export default async function ConsumptionPage({
{validFilter && selected ? (
<>
<div className="tiles">
<Tile label="Mesafe" value={num(selected.day.km)} unit="km" tone="teal" />
<Tile label="Enerji" value={precise(selected.day.kwh)} unit="kWh" tone="gold" />
<Tile label="Ortalama" value={efficiency(selected.day.kwh, selected.day.km)} tone="teal" />
<Tile label="Distance" value={num(selected.day.km)} unit="km" tone="teal" />
<Tile label="Energy" value={precise(selected.day.kwh)} unit="kWh" tone="gold" />
<Tile label="Average" value={efficiency(selected.day.kwh, selected.day.km)} tone="teal" />
<Tile
label="Ortalama hız"
label="Average speed"
value={selected.day.hours > 0 ? num(selected.day.km / selected.day.hours) : "—"}
unit={selected.day.hours > 0 ? "km/h" : undefined}
/>
</div>
<div className="rows">
<Row label="Sürüş süresi" value={duration(selected.day.hours * 3600)} />
<Row label="Doluluk düşüşü" value={`%${num(selected.day.socDrop)}`} />
<Row label="O gün sonu ömür boyu" value={selected.lifetime ? `${num(selected.lifetime.km)} km` : "—"} />
<Row label="Driving time" value={duration(selected.day.hours * 3600)} />
<Row label="SoC used" value={`${num(selected.day.socDrop)}%`} />
<Row label="Lifetime at end of day" value={selected.lifetime ? `${num(selected.lifetime.km)} km` : "—"} />
</div>
</>
) : (
<p className="empty">Bu tarih için tüketim kaydı yok.</p>
<p className="empty">No consumption recorded for this date.</p>
)}
</section>
)}
<div className="tiles">
<Tile label="Toplam mesafe" value={num(total.km)} unit="km" tone="teal" />
<Tile label="Toplam enerji" value={precise(total.kwh)} unit="kWh" tone="gold" />
<Tile label="Ortalama" value={efficiency(total.kwh, total.km)} tone="teal" />
<Tile label="Total distance" value={num(total.km)} unit="km" tone="teal" />
<Tile label="Total energy" value={precise(total.kwh)} unit="kWh" tone="gold" />
<Tile label="Average" value={efficiency(total.kwh, total.km)} tone="teal" />
<Tile
label="Ortalama hız"
label="Average speed"
value={total.hours > 0 ? num(total.km / total.hours) : "—"}
unit={total.hours > 0 ? "km/h" : undefined}
/>
</div>
<section className="card">
<h2 className="card-title">Günlük enerji</h2>
<h2 className="card-title">Daily energy</h2>
<DailyEnergyChart days={chartDays} />
</section>
<section className="card">
<h2 className="card-title">Verimlilik eğilimi</h2>
<h2 className="card-title">Efficiency trend</h2>
<EfficiencyChart days={chartDays} />
</section>
<section className="card">
<h2 className="card-title">Ömür boyu</h2>
<h2 className="card-title">Lifetime</h2>
<div className="rows">
<Row label="Mesafe" value={`${num(lifetime.km)} km`} />
<Row label="Enerji" value={`${precise(lifetime.kwh)} kWh`} />
<Row label="Ortalama" value={efficiency(lifetime.kwh, lifetime.km)} />
<Row label="Sürüş süresi" value={duration(lifetime.hours * 3600)} />
<Row label="Distance" value={`${num(lifetime.km)} km`} />
<Row label="Energy" value={`${precise(lifetime.kwh)} kWh`} />
<Row label="Average" value={efficiency(lifetime.kwh, lifetime.km)} />
<Row label="Driving time" value={duration(lifetime.hours * 3600)} />
</div>
</section>
<section className="card">
<h2 className="card-title">Gün gün</h2>
<h2 className="card-title">Day by day</h2>
<ul className="list">
{table.map((day) => {
const isSelected = hasFilter && day.date === selectedDate;
@@ -158,13 +158,13 @@ export default async function ConsumptionPage({
</summary>
<div className="session-body">
<div className="rows">
<Row label="Sürüş süresi" value={duration(day.day.hours * 3600)} />
<Row label="Driving time" value={duration(day.day.hours * 3600)} />
<Row
label="Ortalama hız"
label="Average speed"
value={day.day.hours > 0 ? `${num(day.day.km / day.day.hours)} km/h` : "—"}
/>
<Row label="Doluluk düşüşü" value={`%${num(day.day.socDrop)}`} />
<Row label="O gün sonu ömür boyu" value={day.lifetime ? `${num(day.lifetime.km)} km` : "—"} />
<Row label="SoC used" value={`${num(day.day.socDrop)}%`} />
<Row label="Lifetime at end of day" value={day.lifetime ? `${num(day.lifetime.km)} km` : "—"} />
</div>
</div>
</details>
+2 -2
View File
@@ -3,7 +3,7 @@ import "./globals.css";
export const metadata: Metadata = {
title: "Nils' MG4",
description: "MG4 tüketim ve şarj geçmişi",
description: "MG4 consumption and charging history",
};
export const viewport: Viewport = {
@@ -16,7 +16,7 @@ export const viewport: Viewport = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="tr">
<html lang="en">
<body>{children}</body>
</html>
);
+4 -4
View File
@@ -14,7 +14,7 @@ export default async function LoginPage({
<div className="login">
<form className="login-card" action="/api/session" method="post">
<h1 className="title">Nils&apos; MG4</h1>
<p className="subtitle">Tüketim ve şarj geçmişi</p>
<p className="subtitle">Consumption and charging history</p>
{/*
A hidden username so password managers will offer to save this.
Most of them ignore a lone password field — they key a saved entry on
@@ -33,15 +33,15 @@ export default async function LoginPage({
className="field"
type="password"
name="password"
placeholder="Parola"
placeholder="Password"
autoComplete="current-password"
autoFocus
required
/>
<button className="button" type="submit">
Giriş
Sign in
</button>
{error && <p className="error">Parola hatalı.</p>}
{error && <p className="error">Wrong password.</p>}
</form>
</div>
);
+25 -25
View File
@@ -12,10 +12,10 @@ export default async function OverviewPage() {
if (!installationId) {
return (
<Shell active="/" title="Özet">
<Shell active="/" title="Overview">
<div className="card">
<p className="empty">
Henüz veri yok. Araç ilk günlük kaydı gönderdiğinde burada görünecek.
No data yet. It appears here once the car sends its first daily record.
</p>
</div>
</Shell>
@@ -31,26 +31,26 @@ export default async function OverviewPage() {
return (
<Shell
active="/"
title="Özet"
subtitle={installationCount > 1 ? `${installationCount} araçtan en günceli` : "MG4"}
title="Overview"
subtitle={installationCount > 1 ? `Most recent of ${installationCount} cars` : "MG4"}
>
<div className="tiles">
<Tile
label="Şebekeden alınan"
label="Drawn from grid"
value={precise(charge.gridEnergyKwh)}
unit="kWh"
note={`${charge.sessions} DC şarj`}
note={`${charge.sessions} DC charge${charge.sessions === 1 ? "" : "s"}`}
tone="gold"
/>
<Tile
label="Ödenen"
label="Paid"
value={money(charge.cost)}
note={charge.gridEnergyKwh > 0 ? `${money(charge.cost / charge.gridEnergyKwh)}/kWh ort.` : undefined}
note={charge.gridEnergyKwh > 0 ? `${money(charge.cost / charge.gridEnergyKwh)}/kWh avg.` : undefined}
tone="gold"
/>
<Tile label="Son 30 gün" value={num(recent.km)} unit="km" note={efficiency(recent.kwh, recent.km)} tone="teal" />
<Tile label="Last 30 days" value={num(recent.km)} unit="km" note={efficiency(recent.kwh, recent.km)} tone="teal" />
<Tile
label="Ömür boyu"
label="Lifetime"
value={lifetime ? num(lifetime.km) : "—"}
unit="km"
note={lifetime ? efficiency(lifetime.kwh, lifetime.km) : undefined}
@@ -59,39 +59,39 @@ export default async function OverviewPage() {
</div>
<section className="card">
<h2 className="card-title">Son 30 gün</h2>
<h2 className="card-title">Last 30 days</h2>
<DailyEnergyChart days={days.map((day) => ({ date: day.date, kwh: day.day.kwh, km: day.day.km }))} />
</section>
{lastSession && (
<section className="card">
<h2 className="card-title">Son şarj</h2>
<h2 className="card-title">Last charge</h2>
<div className="rows">
<Row label="Tarih" value={dateTime(lastSession.startedAt)} />
<Row label="Bataryaya giren net" value={`${precise(lastSession.energyKwh)} kWh`} />
<Row label="Şebekeden alınan (+%10)" value={`${precise(lastSession.gridEnergyKwh ?? lastSession.energyKwh * 1.1)} kWh`} />
<Row label="Date" value={dateTime(lastSession.startedAt)} />
<Row label="Net into battery" value={`${precise(lastSession.energyKwh)} kWh`} />
<Row label="Drawn from grid (+10%)" value={`${precise(lastSession.gridEnergyKwh ?? lastSession.energyKwh * 1.1)} kWh`} />
<Row
label="Doluluk"
label="State of charge"
value={
lastSession.startSocPercent !== undefined && lastSession.endSocPercent !== undefined
? `%${num(lastSession.startSocPercent)} → %${num(lastSession.endSocPercent)}`
? `${num(lastSession.startSocPercent)}% → ${num(lastSession.endSocPercent)}%`
: "—"
}
/>
<Row label="Süre" value={duration(lastSession.durationSeconds)} />
<Row label="Ücret" value={money(lastSession.totalCost)} />
<Row label="Duration" value={duration(lastSession.durationSeconds)} />
<Row label="Cost" value={money(lastSession.totalCost)} />
</div>
</section>
)}
<section className="card">
<h2 className="card-title">Batarya ve toplamlar</h2>
<h2 className="card-title">Battery and totals</h2>
<div className="rows">
<Row label="Sağlık (SOH)" value={soh === undefined ? "—" : `%${num(soh)}`} />
<Row label="Toplam şarj süresi" value={duration(charge.seconds)} />
<Row label="Görülen en yüksek güç" value={charge.peakKw > 0 ? `${num(charge.peakKw)} kW` : "—"} />
<Row label="Şarjla kazanılan doluluk" value={charge.socGained > 0 ? `%${num(charge.socGained)}` : "—"} />
<Row label="Kayıtlı gün" value={`${days.length}`} />
<Row label="Health (SoH)" value={soh === undefined ? "—" : `${num(soh)}%`} />
<Row label="Total charging time" value={duration(charge.seconds)} />
<Row label="Highest power seen" value={charge.peakKw > 0 ? `${num(charge.peakKw)} kW` : "—"} />
<Row label="Charge added" value={charge.socGained > 0 ? `${num(charge.socGained)}%` : "—"} />
<Row label="Days recorded" value={`${days.length}`} />
</div>
</section>
</Shell>
+12 -12
View File
@@ -78,7 +78,7 @@ export interface DayDatum {
* distinction the telemetry side makes between a failed read and a zero.
*/
export function DailyEnergyChart({ days }: { days: DayDatum[] }) {
if (days.length === 0) return <p className="empty">Henüz günlük kayıt yok</p>;
if (days.length === 0) return <p className="empty">No daily records yet</p>;
const plot = PLOT;
const max = niceMax(Math.max(...days.map((day) => day.kwh)));
const slot = innerWidth(plot) / days.length;
@@ -86,7 +86,7 @@ export function DailyEnergyChart({ days }: { days: DayDatum[] }) {
return (
<>
<svg className="chart" viewBox={`0 0 ${plot.width} ${plot.height}`} role="img"
aria-label="Gün gün tüketilen enerji">
aria-label="Energy used per day">
<Grid plot={plot} max={max} format={(value) => value.toFixed(0)} />
{days.map((day, index) => {
const height = (day.kwh / max) * innerHeight(plot);
@@ -116,7 +116,7 @@ export function DailyEnergyChart({ days }: { days: DayDatum[] }) {
<div className="legend">
<span>
<span className="swatch" style={{ background: GOLD }} />
Günlük enerji (kWh)
Daily energy (kWh)
</span>
</div>
</>
@@ -128,7 +128,7 @@ export function EfficiencyChart({ days }: { days: DayDatum[] }) {
const points = days
.filter((day) => day.km >= 1 && day.kwh > 0)
.map((day) => ({ date: day.date, value: (day.kwh * 100) / day.km }));
if (points.length < 2) return <p className="empty">Grafik için yeterli gün yok</p>;
if (points.length < 2) return <p className="empty">Not enough days for a chart</p>;
const plot = PLOT;
const max = niceMax(Math.max(...points.map((point) => point.value)));
const step = innerWidth(plot) / Math.max(1, points.length - 1);
@@ -144,7 +144,7 @@ export function EfficiencyChart({ days }: { days: DayDatum[] }) {
return (
<>
<svg className="chart" viewBox={`0 0 ${plot.width} ${plot.height}`} role="img"
aria-label="Gün gün ortalama tüketim">
aria-label="Average consumption per day">
<Grid plot={plot} max={max} format={(value) => value.toFixed(0)} />
<line
x1={plot.padLeft}
@@ -176,7 +176,7 @@ export function EfficiencyChart({ days }: { days: DayDatum[] }) {
</span>
<span>
<span className="swatch" style={{ background: GOLD }} />
Ortalama {average.toFixed(1)}
Average {average.toFixed(1)}
</span>
</div>
</>
@@ -196,7 +196,7 @@ export interface CurveDatum {
* them — the taper is only legible next to the percentage that caused it.
*/
export function ChargeCurveChart({ curve }: { curve: CurveDatum[] }) {
if (curve.length < 2) return <p className="empty">Bu seans için eğri kaydı yok</p>;
if (curve.length < 2) return <p className="empty">No curve recorded for this session</p>;
const plot = { ...PLOT, padRight: 26 };
const times = curve.map((point) => new Date(point.timestamp).getTime());
const start = times[0];
@@ -223,7 +223,7 @@ export function ChargeCurveChart({ curve }: { curve: CurveDatum[] }) {
return (
<>
<svg className="chart" viewBox={`0 0 ${plot.width} ${plot.height}`} role="img"
aria-label="Şarj gücü ve doluluk eğrisi">
aria-label="Charging power and state-of-charge curve">
<Grid plot={plot} max={maxPower} format={(value) => value.toFixed(0)} />
<defs>
<linearGradient id="powerFill" x1="0" x2="0" y1="0" y2="1">
@@ -241,20 +241,20 @@ export function ChargeCurveChart({ curve }: { curve: CurveDatum[] }) {
0%
</text>
<text x={plot.padLeft} y={plot.height - 6} fill={MUTED} fontSize={9}>
0 dk
0 min
</text>
<text x={plot.width - plot.padRight} y={plot.height - 6} fill={MUTED} fontSize={9} textAnchor="end">
{Math.round(span / 60000)} dk
{Math.round(span / 60000)} min
</text>
</svg>
<div className="legend">
<span>
<span className="swatch" style={{ background: GOLD }} />
Güç (kW)
Power (kW)
</span>
<span>
<span className="swatch" style={{ background: CYAN }} />
Doluluk (%)
State of charge (%)
</span>
</div>
</>
+4 -4
View File
@@ -2,9 +2,9 @@ import Link from "next/link";
import type { ReactNode } from "react";
const TABS = [
{ href: "/", label: "Özet", icon: "◈" },
{ href: "/consumption", label: "Tüketim", icon: "▤" },
{ href: "/charging", label: "Şarj", icon: "⚡" },
{ href: "/", label: "Overview", icon: "◈" },
{ href: "/consumption", label: "Consumption", icon: "▤" },
{ href: "/charging", label: "Charging", icon: "⚡" },
] as const;
export type Tab = (typeof TABS)[number]["href"];
@@ -42,7 +42,7 @@ export function Shell({
*/}
<form action="/logout" method="post">
<button className="signout" type="submit">
Çıkış
Sign out
</button>
</form>
</header>
+9 -9
View File
@@ -1,10 +1,10 @@
/** Turkish formatting for every user-facing number and date. */
/** English formatting (en-GB: 24-hour clock, day-month dates) with euro for every number and date. */
const NUMBER = new Intl.NumberFormat("tr-TR", { maximumFractionDigits: 1 });
const PRECISE = new Intl.NumberFormat("tr-TR", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const CURRENCY = new Intl.NumberFormat("tr-TR", { style: "currency", currency: "TRY", maximumFractionDigits: 2 });
const DATE = new Intl.DateTimeFormat("tr-TR", { day: "numeric", month: "long", year: "numeric" });
const DATE_TIME = new Intl.DateTimeFormat("tr-TR", {
const NUMBER = new Intl.NumberFormat("en-GB", { maximumFractionDigits: 1 });
const PRECISE = new Intl.NumberFormat("en-GB", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const CURRENCY = new Intl.NumberFormat("en-GB", { style: "currency", currency: "EUR", maximumFractionDigits: 2 });
const DATE = new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "long", year: "numeric" });
const DATE_TIME = new Intl.DateTimeFormat("en-GB", {
day: "numeric",
month: "short",
hour: "2-digit",
@@ -32,13 +32,13 @@ export function dateTime(value: Date | string): string {
return DATE_TIME.format(new Date(value));
}
/** `2 sa 05 dk` — the same shape the car's charging pane uses. */
/** `2 h 05 min` — the same shape the car's charging pane uses. */
export function duration(seconds: number): string {
const total = Math.max(0, Math.round(seconds));
const hours = Math.floor(total / 3600);
const minutes = Math.floor((total % 3600) / 60);
if (hours === 0) return `${minutes} dk`;
return `${hours} sa ${String(minutes).padStart(2, "0")} dk`;
if (hours === 0) return `${minutes} min`;
return `${hours} h ${String(minutes).padStart(2, "0")} min`;
}
/** Energy per 100 km, or a dash — never a ratio computed from a distance of nothing. */