diff --git a/SPECS.md b/SPECS.md
index e2155c8..a642041 100644
--- a/SPECS.md
+++ b/SPECS.md
@@ -87,7 +87,6 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
- [ ] **EVHardware's comments are partly French.** About 120 comment lines in the vendored
`EVHardware/` library (mostly `EVHardware.kt`, some tests), half-translated upstream. The
app, the docs and the build files are English.
-- [ ] **Move hardcoded UI strings into `strings.xml`.** Many are inline in Java and layouts.
## Done
@@ -117,6 +116,9 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
INGEST_TOKEN) into git and pushed it. Commit amended and force-pushed; both values regenerated in
the now gitignored `k8s/secrets.yaml` (since moved to `k8s/secrets/secrets.yaml`, outside
`kubectl apply -f k8s/`). Filled-in secrets live only there from now on.
+- 2026-10-18 — App 2.14.4: every UI text is in `strings.xml` (Charging, Consumption and Settings
+ labels, toasts, the trip-reset dialog, charging and Wi-Fi states, the daily summary, the "—"
+ placeholder); lint reports no hard-coded text. Upload-log entries stay in code as log text.
- 2026-10-18 — Translated the rest: `mise.toml`, the comments in `app/build.gradle`, `OTA.md`'s
message table (now the updater's current English messages), the Turkish messages quoted in
`CHANGELOG.md` and `OTA.md` (glossed), two test messages, the ignition-state comment in
diff --git a/app/build.gradle b/app/build.gradle
index 3122615..53d4044 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -34,8 +34,8 @@ android {
applicationId "com.stinnesbeck.nilsmg4"
minSdk 27
targetSdk 36
- versionCode 81
- versionName "2.14.3"
+ versionCode 82
+ versionName "2.14.4"
// Nothing about the server ships in the APK: its URL and ingest token are entered in
// the app's Server tab (ServerConfig), and the database is only reachable by the server.
diff --git a/app/src/main/java/com/evsuite/abrp/MainActivity.java b/app/src/main/java/com/evsuite/abrp/MainActivity.java
index 2e21606..e84325a 100644
--- a/app/src/main/java/com/evsuite/abrp/MainActivity.java
+++ b/app/src/main/java/com/evsuite/abrp/MainActivity.java
@@ -1218,9 +1218,9 @@ public class MainActivity extends AppCompatActivity {
String ssid = wifi.currentSsid();
wifiSsid.setText(ssid == null ? getString(R.string.wifi_not_connected) : ssid);
if (ssid != null) {
- wifiState.setText("Connected ♥");
+ wifiState.setText(R.string.wifi_connected);
} else if (!wifi.isWifiEnabled()) {
- wifiState.setText("Wi-Fi off");
+ wifiState.setText(R.string.wifi_off);
}
// Left alone otherwise: a message the button just set says more about what is
// happening than "not connected" would.
@@ -1378,11 +1378,12 @@ public class MainActivity extends AppCompatActivity {
selectPeriodButton(R.id.consumption_lifetime);
consumptionReset.setOnClickListener(v -> {
if (consumptionPeriod != ConsumptionTracker.Period.TRIP_A && consumptionPeriod != ConsumptionTracker.Period.TRIP_B) return;
- String name = consumptionPeriod == ConsumptionTracker.Period.TRIP_A ? "Trip A" : "Trip B";
- new MaterialAlertDialogBuilder(this).setTitle("Reset " + name + "?")
- .setMessage("This counter's recorded consumption history will be permanently deleted.")
- .setNegativeButton("Cancel", null)
- .setPositiveButton("Reset", (d, w) -> {
+ String name = getString(consumptionPeriod == ConsumptionTracker.Period.TRIP_A
+ ? R.string.consumption_trip_a : R.string.consumption_trip_b);
+ new MaterialAlertDialogBuilder(this).setTitle(getString(R.string.trip_reset_title, name))
+ .setMessage(R.string.trip_reset_message)
+ .setNegativeButton(R.string.pair_cancel, null)
+ .setPositiveButton(R.string.trip_reset_confirm, (d, w) -> {
consumptionTracker.reset(consumptionPeriod);
refreshConsumption();
}).show();
@@ -1409,9 +1410,9 @@ public class MainActivity extends AppCompatActivity {
if (!consumptionTracker.setSohPercent(value)) throw new NumberFormatException();
consumptionSohLayout.setError(null);
consumptionSohInput.setText(String.format(java.util.Locale.US, "%.1f", value));
- Toast.makeText(this, "SoH saved", Toast.LENGTH_SHORT).show();
+ Toast.makeText(this, R.string.soh_saved, Toast.LENGTH_SHORT).show();
} catch (NumberFormatException e) {
- consumptionSohLayout.setError("Enter a value between 1 and 100");
+ consumptionSohLayout.setError(getString(R.string.soh_invalid));
}
}
@@ -1469,9 +1470,9 @@ public class MainActivity extends AppCompatActivity {
if (!chargeSessionTracker.setPricePerKwh(value)) throw new NumberFormatException();
chargePriceLayout.setError(null);
chargePriceInput.setText(String.format(java.util.Locale.US, "%.2f", value));
- Toast.makeText(this, "Charging price saved", Toast.LENGTH_SHORT).show();
+ Toast.makeText(this, R.string.price_saved, Toast.LENGTH_SHORT).show();
} catch (NumberFormatException e) {
- chargePriceLayout.setError("Enter a valid €/kWh value");
+ chargePriceLayout.setError(getString(R.string.price_invalid));
}
}
@@ -1486,9 +1487,9 @@ public class MainActivity extends AppCompatActivity {
// below — which then describe the previous DC session — be read as this charge.
boolean dc = ChargingState.isDcCharging(
code, s.getChargePortConnected(), s.getBatteryPowerKw(), s.getSpeedKmh());
- chargeStatus.setText(dc ? "DC fast charging"
- : charging ? "AC charging (not metered)"
- : (session.startedAtMs > 0 ? "Last charge" : "Not charging"));
+ chargeStatus.setText(dc ? R.string.charge_dc
+ : charging ? R.string.charge_ac_not_metered
+ : (session.startedAtMs > 0 ? R.string.charge_last : R.string.charging_not_charging));
chargeSoc.setText(fmt(s.getSocPercent(), "%.1f %%"));
chargeStartSoc.setText(fmt(session.startSocPercent, "%.1f %%"));
chargeVoltage.setText(fmt(s.getBatteryVoltageV(), "%.1f V"));
@@ -1529,13 +1530,12 @@ public class MainActivity extends AppCompatActivity {
ConsumptionTracker.Totals day = cloudDay(selectedConsumptionDate);
if (day == null) day = consumptionTracker.totalsForDay(selectedConsumptionDate);
if (day.km == 0 && day.kwh == 0 && day.hours == 0 && day.soc == 0) {
- consumptionDailyHistory.setText("No records for this date");
+ consumptionDailyHistory.setText(R.string.consumption_no_records);
} else {
String average = day.km < 0.1 ? "—" : String.format(java.util.Locale.getDefault(),
"%.1f kWh/100 km", day.kwh * 100d / day.km);
long dayMinutes = Math.round(day.hours * 60d);
- consumptionDailyHistory.setText(String.format(java.util.Locale.getDefault(),
- "Distance %.1f km\nEnergy %.2f kWh\nAverage %s\nDuration %d h %02d min\nSOC change %.1f %%",
+ consumptionDailyHistory.setText(getString(R.string.consumption_day_summary,
day.km, day.kwh, average, dayMinutes / 60, dayMinutes % 60, day.soc));
}
}
@@ -1598,21 +1598,21 @@ public class MainActivity extends AppCompatActivity {
* The codes are the vehicle's own — see EVHardware.PROP_VENDOR_CHARGE_STATUS. An
* unrecognised code is shown as itself rather than guessed at.
*/
- private static String chargingLabel(EnergySnapshot s) {
+ private String chargingLabel(EnergySnapshot s) {
Integer status = s.getChargingStatus();
if (status != null) {
switch (status) {
- case 1: return "AC charging ♥";
- case 10: return "DC fast charging ♥";
- case 5: return "Connecting…";
- case 7: return "Plugged in, not charging";
- case 8: return "Stopped";
- case 0: return "Not charging";
- default: return "Status " + status;
+ case 1: return getString(R.string.charge_status_ac);
+ case 10: return getString(R.string.charge_status_dc);
+ case 5: return getString(R.string.charge_status_connecting);
+ case 7: return getString(R.string.charge_status_plugged);
+ case 8: return getString(R.string.charge_status_stopped);
+ case 0: return getString(R.string.charging_not_charging);
+ default: return getString(R.string.charge_status_code, status);
}
}
Boolean plugged = s.getChargePortConnected();
- if (plugged != null) return plugged ? "Connected ♥" : "Not connected";
+ if (plugged != null) return getString(plugged ? R.string.charge_status_connected : R.string.charge_status_unplugged);
return "—";
}
@@ -1652,7 +1652,7 @@ public class MainActivity extends AppCompatActivity {
consumptionCloudClient.invalidateHistory();
runOnUiThread(() -> {
refreshConsumption();
- Toast.makeText(this, "Consumption refreshed", Toast.LENGTH_SHORT).show();
+ Toast.makeText(this, R.string.consumption_refreshed, Toast.LENGTH_SHORT).show();
});
}, "cloud-refresh").start();
}
diff --git a/app/src/main/res/layout-w1000dp/pane_settings.xml b/app/src/main/res/layout-w1000dp/pane_settings.xml
index 12005cd..6e9df62 100644
--- a/app/src/main/res/layout-w1000dp/pane_settings.xml
+++ b/app/src/main/res/layout-w1000dp/pane_settings.xml
@@ -15,12 +15,12 @@
-
+
-
-
-
+
+
+
@@ -28,10 +28,10 @@
-
+
-
+
@@ -39,8 +39,8 @@
-
-
+
+
diff --git a/app/src/main/res/layout-w1000dp/pane_vehicle.xml b/app/src/main/res/layout-w1000dp/pane_vehicle.xml
index 6357b56..24acbed 100644
--- a/app/src/main/res/layout-w1000dp/pane_vehicle.xml
+++ b/app/src/main/res/layout-w1000dp/pane_vehicle.xml
@@ -4,52 +4,52 @@
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -74,18 +74,18 @@
-
+
-
+
-
+
-
+
diff --git a/app/src/main/res/layout/pane_charging.xml b/app/src/main/res/layout/pane_charging.xml
index 43c34d0..02e6819 100644
--- a/app/src/main/res/layout/pane_charging.xml
+++ b/app/src/main/res/layout/pane_charging.xml
@@ -2,23 +2,23 @@
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
-
+
diff --git a/app/src/main/res/layout/pane_consumption.xml b/app/src/main/res/layout/pane_consumption.xml
index 3f19a5a..1d59919 100644
--- a/app/src/main/res/layout/pane_consumption.xml
+++ b/app/src/main/res/layout/pane_consumption.xml
@@ -3,33 +3,33 @@
-
-
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
+
-
+
-
-
-
+
+
+
diff --git a/app/src/main/res/layout/pane_settings.xml b/app/src/main/res/layout/pane_settings.xml
index 943732d..978fbd8 100644
--- a/app/src/main/res/layout/pane_settings.xml
+++ b/app/src/main/res/layout/pane_settings.xml
@@ -14,12 +14,12 @@
-
+
-
-
-
+
+
+
@@ -27,17 +27,17 @@
-
+
-
+
-
-
+
+
diff --git a/app/src/main/res/layout/pane_vehicle.xml b/app/src/main/res/layout/pane_vehicle.xml
index 5bc5918..009564c 100644
--- a/app/src/main/res/layout/pane_vehicle.xml
+++ b/app/src/main/res/layout/pane_vehicle.xml
@@ -4,50 +4,50 @@
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -69,18 +69,18 @@
-
+
-
+
-
+
-
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index dfeaeef..288bf29 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -214,4 +214,69 @@
QR code for the project repository
unknown
Close
+
+
+ Not charging
+ SOC
+ Start SOC
+ Battery voltage
+ Battery current
+ Charging power
+ Net energy into battery
+ Drawn from grid (+10%)
+ Charging time
+ Total cost
+ CHARGING CURVE
+ No charging measurements yet
+ CONSUMPTION PERIOD
+ Lifetime
+ Last 7 days
+ Last 30 days
+ Trip A
+ Trip B
+ Distance
+ Net energy
+ Average consumption
+ Average speed
+ Duration
+ SOC change
+ Reset selected trip
+ Last 7/30 days, lifetime and Trip A/B are stored permanently on the device; app and OTA updates do not erase them.
+ DAILY LOG · LAST 4 MONTHS
+ Pick a date
+ No daily data yet
+ Manual SoH (%)
+ %
+ Save SoH value
+ This manual value is stored on the device and sent to your server with the daily totals.
+ kWh
+ Unit price
+ €/kWh
+ Save price
+ Connected ♥
+ Wi-Fi off
+ Reset %1$s?
+ This counter\'s recorded consumption history will be permanently deleted.
+ Reset
+ SoH saved
+ Enter a value between 1 and 100
+ Charging price saved
+ Enter a valid €/kWh value
+ DC fast charging
+ AC charging (not metered)
+ Last charge
+ No records for this date
+ Distance %1$.1f km\nEnergy %2$.2f kWh\nAverage %3$s\nDuration %4$d h %5$02d min\nSOC change %6$.1f %%
+ Consumption refreshed
+ AC charging ♥
+ DC fast charging ♥
+ Connecting…
+ Plugged in, not charging
+ Stopped
+ Status %1$d
+ Connected ♥
+ Not connected
+
+ —
+ Refresh