diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ef79e97 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + name: Lint, Build & Test + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Run Android Lint + run: ./gradlew lintDebug + + - name: Upload lint results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: lint-results + path: app/build/reports/lint-results-debug.html + + - name: Build Debug APK + run: ./gradlew assembleDebug + + - name: Run unit tests + run: ./gradlew test diff --git a/CLAUDE.md b/CLAUDE.md index ad39b30..6124be3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,6 +32,19 @@ * When releasing a new version, create a new changelog file matching the versionCode in `app/build.gradle.kts`. * The F-Droid build recipe is stored in `fdroid/com.matedroid.yml` (reference copy; actual recipe lives in fdroiddata repo). +## Localization + +* NEVER hardcode user-visible strings in Kotlin code. Always use `stringResource(R.string.xxx)`. +* When adding new UI text, add the string to all 4 locale files: + - `res/values/strings.xml` (English, default) + - `res/values-it/strings.xml` (Italian) + - `res/values-es/strings.xml` (Spanish) + - `res/values-ca/strings.xml` (Catalan) +* Use snake_case for string names (e.g., `settings_title`, `drive_history`). +* Add XML comments above strings to provide context for translators. +* Technical terms like AC, DC, kW, kWh should NOT be translated. +* Always run `./gradlew lintDebug` before committing to catch hardcoded strings. + ## Gotchas and notes * Always check the local instance of Teslamate API and its documentation to know what's the returned JSON format. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0b090fe..1e10ae1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -83,6 +83,25 @@ android { includeInApk = false includeInBundle = false } + + lint { + // Treat hardcoded text as an error to enforce localization + error += "HardcodedText" + error += "SetTextI18n" + + // Fail the build on errors + abortOnError = true + + // Generate reports + htmlReport = true + xmlReport = true + } + + testOptions { + unitTests.all { + it.jvmArgs("-Xmx1024m") + } + } } dependencies { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3d6cf22..cd4b647 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:localeConfig="@xml/locale_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.MateDroid" diff --git a/app/src/main/java/com/matedroid/ui/components/FullscreenLineChart.kt b/app/src/main/java/com/matedroid/ui/components/FullscreenLineChart.kt index 04aeb41..e5fb78f 100644 --- a/app/src/main/java/com/matedroid/ui/components/FullscreenLineChart.kt +++ b/app/src/main/java/com/matedroid/ui/components/FullscreenLineChart.kt @@ -32,7 +32,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.matedroid.R import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.SecureFlagPolicy @@ -94,7 +96,7 @@ fun FullscreenLineChart( ) { Icon( imageVector = Icons.Default.Fullscreen, - contentDescription = "Fullscreen", + contentDescription = stringResource(R.string.fullscreen), modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -234,7 +236,7 @@ private fun FullscreenChartOverlay( ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Exit fullscreen", + contentDescription = stringResource(R.string.exit_fullscreen), tint = MaterialTheme.colorScheme.onSurfaceVariant ) } diff --git a/app/src/main/java/com/matedroid/ui/screens/battery/BatteryScreen.kt b/app/src/main/java/com/matedroid/ui/screens/battery/BatteryScreen.kt index 22ec523..8e3662f 100644 --- a/app/src/main/java/com/matedroid/ui/screens/battery/BatteryScreen.kt +++ b/app/src/main/java/com/matedroid/ui/screens/battery/BatteryScreen.kt @@ -57,11 +57,13 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.foundation.isSystemInDarkTheme import androidx.hilt.navigation.compose.hiltViewModel +import com.matedroid.R import com.matedroid.ui.theme.CarColorPalette import com.matedroid.ui.theme.CarColorPalettes import com.matedroid.ui.theme.StatusError @@ -107,12 +109,12 @@ fun BatteryScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Battery Health") }, + title = { Text(stringResource(R.string.battery_health_title)) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" + contentDescription = stringResource(R.string.back) ) } }, @@ -151,7 +153,7 @@ fun BatteryScreen( contentAlignment = Alignment.Center ) { Text( - text = "No battery data available", + text = stringResource(R.string.no_battery_data), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -205,14 +207,20 @@ private fun BatteryHealthContent( @Composable private fun CapacityCard(stats: BatteryStats, palette: CarColorPalette, onClick: () -> Unit) { var showTooltip by remember { mutableStateOf(false) } + val capacityTitle = stringResource(R.string.battery_capacity_title) + val capacityMessage = stringResource(R.string.battery_capacity_message) + val capacityLabel = stringResource(R.string.capacity) + val usableNewLabel = stringResource(R.string.usable_new) + val usableNowLabel = stringResource(R.string.usable_now) + val ratedLabel = stringResource(R.string.rated) + val infoLabel = stringResource(R.string.info) + val gotItLabel = stringResource(R.string.got_it) if (showTooltip) { InfoDialog( - title = "Battery Capacity", - message = "Shows your battery's energy storage capacity.\n\n" + - "• Usable (new): The original usable capacity when the car was new\n" + - "• Usable (now): Current usable capacity after degradation\n" + - "• Rated efficiency: Energy consumption per km under standard test conditions (Wh/km)", + title = capacityTitle, + message = capacityMessage, + confirmText = gotItLabel, onDismiss = { showTooltip = false } ) } @@ -240,7 +248,7 @@ private fun CapacityCard(stats: BatteryStats, palette: CarColorPalette, onClick: ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Capacity", + text = capacityLabel, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = palette.onSurface @@ -248,7 +256,7 @@ private fun CapacityCard(stats: BatteryStats, palette: CarColorPalette, onClick: Spacer(modifier = Modifier.width(4.dp)) Icon( imageVector = Icons.Filled.Info, - contentDescription = "Info", + contentDescription = infoLabel, tint = palette.onSurfaceVariant, modifier = Modifier .size(16.dp) @@ -265,13 +273,13 @@ private fun CapacityCard(stats: BatteryStats, palette: CarColorPalette, onClick: ) { CapacityValueCard( value = "%.1f kWh".format(stats.originalCapacity), - label = "Usable (new)", + label = usableNewLabel, iconColor = CapacityGreen, modifier = Modifier.weight(1f) ) CapacityValueCard( value = "%.1f kWh".format(stats.currentCapacity), - label = "Usable (now)", + label = usableNowLabel, iconColor = CapacityYellow, modifier = Modifier.weight(1f) ) @@ -300,7 +308,7 @@ private fun CapacityCard(stats: BatteryStats, palette: CarColorPalette, onClick: color = palette.onSurface ) Text( - text = "Rated", + text = ratedLabel, style = MaterialTheme.typography.labelSmall, color = palette.onSurfaceVariant ) @@ -353,15 +361,20 @@ private fun CapacityValueCard( @Composable private fun DegradationCard(stats: BatteryStats, palette: CarColorPalette, onClick: () -> Unit) { var showTooltip by remember { mutableStateOf(false) } + val degradationTitle = stringResource(R.string.estimated_degradation_title) + val degradationMessage = stringResource(R.string.estimated_degradation_message) + val degradationLabel = stringResource(R.string.estimated_degradation) + val capacityLabel = stringResource(R.string.capacity) + val lossKwhLabel = stringResource(R.string.loss_kwh) + val lossPercentLabel = stringResource(R.string.loss_percent) + val infoLabel = stringResource(R.string.info) + val gotItLabel = stringResource(R.string.got_it) if (showTooltip) { InfoDialog( - title = "Estimated Degradation", - message = "Battery degradation is estimated based on Teslamate data.\n\n" + - "• Capacity %: Remaining battery health compared to new\n" + - "• Loss (kWh): Energy capacity lost due to degradation\n" + - "• Loss (%): Percentage of original capacity lost\n\n" + - "Note: This is an estimate. Tesla does not provide official degradation data.", + title = degradationTitle, + message = degradationMessage, + confirmText = gotItLabel, onDismiss = { showTooltip = false } ) } @@ -388,7 +401,7 @@ private fun DegradationCard(stats: BatteryStats, palette: CarColorPalette, onCli ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Estimated degradation", + text = degradationLabel, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = palette.onSurface @@ -396,7 +409,7 @@ private fun DegradationCard(stats: BatteryStats, palette: CarColorPalette, onCli Spacer(modifier = Modifier.width(4.dp)) Icon( imageVector = Icons.Filled.Info, - contentDescription = "Info", + contentDescription = infoLabel, tint = palette.onSurfaceVariant, modifier = Modifier .size(16.dp) @@ -413,7 +426,7 @@ private fun DegradationCard(stats: BatteryStats, palette: CarColorPalette, onCli verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Capacity", + text = capacityLabel, style = MaterialTheme.typography.bodyMedium, color = palette.onSurfaceVariant ) @@ -448,13 +461,13 @@ private fun DegradationCard(stats: BatteryStats, palette: CarColorPalette, onCli ) { LossValueCard( value = "%.1f kWh".format(stats.lossKwh), - label = "Loss (kWh)", + label = lossKwhLabel, iconColor = LossYellow, modifier = Modifier.weight(1f) ) LossValueCard( value = "%.1f%%".format(stats.lossPercent), - label = "Loss (%)", + label = lossPercentLabel, iconColor = LossRed, modifier = Modifier.weight(1f) ) @@ -505,6 +518,11 @@ private fun LossValueCard( @Composable private fun RangeCard(stats: BatteryStats, palette: CarColorPalette, onClick: () -> Unit) { + val rangeLabel = stringResource(R.string.range) + val maxRangeNewLabel = stringResource(R.string.max_range_new) + val maxRangeNowLabel = stringResource(R.string.max_range_now) + val rangeLossLabel = stringResource(R.string.range_loss) + Card( modifier = Modifier .fillMaxWidth() @@ -518,7 +536,7 @@ private fun RangeCard(stats: BatteryStats, palette: CarColorPalette, onClick: () ) { // Header Text( - text = "Range", + text = rangeLabel, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = palette.onSurface @@ -533,13 +551,13 @@ private fun RangeCard(stats: BatteryStats, palette: CarColorPalette, onClick: () ) { RangeValueCard( value = "%.1f km".format(stats.maxRangeNew), - label = "Max Range (new)", + label = maxRangeNewLabel, iconColor = CapacityGreen, modifier = Modifier.weight(1f) ) RangeValueCard( value = "%.1f km".format(stats.maxRangeNow), - label = "Max Range (now)", + label = maxRangeNowLabel, iconColor = CapacityYellow, modifier = Modifier.weight(1f) ) @@ -568,7 +586,7 @@ private fun RangeCard(stats: BatteryStats, palette: CarColorPalette, onClick: () color = palette.onSurface ) Text( - text = "Range loss", + text = rangeLossLabel, style = MaterialTheme.typography.labelSmall, color = palette.onSurfaceVariant ) @@ -628,12 +646,12 @@ private fun BatteryDetailScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Battery Detail") }, + title = { Text(stringResource(R.string.battery_detail_title)) }, navigationIcon = { IconButton(onClick = onClose) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" + contentDescription = stringResource(R.string.back) ) } }, @@ -666,14 +684,18 @@ private fun BatteryDetailScreen( @Composable private fun BatteryStatusCard(stats: BatteryStats) { var showTooltip by remember { mutableStateOf(false) } + val currentChargeTitle = stringResource(R.string.current_charge_title) + val currentChargeMessage = stringResource(R.string.current_charge_message) + val currentChargeLabel = stringResource(R.string.current_charge) + val usablePercentLabel = stringResource(R.string.usable_percent, stats.usableBatteryLevel) + val infoLabel = stringResource(R.string.info) + val gotItLabel = stringResource(R.string.got_it) if (showTooltip) { InfoDialog( - title = "Current Charge", - message = "Shows your battery's current state of charge.\n\n" + - "• Battery %: The charge level displayed on your dashboard\n" + - "• Usable %: Actual usable charge (Tesla reserves a small buffer for battery protection)\n\n" + - "This is the current charge, not the battery's long-term health.", + title = currentChargeTitle, + message = currentChargeMessage, + confirmText = gotItLabel, onDismiss = { showTooltip = false } ) } @@ -699,14 +721,14 @@ private fun BatteryStatusCard(stats: BatteryStats) { ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Current Charge", + text = currentChargeLabel, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.width(4.dp)) Icon( imageVector = Icons.Filled.Info, - contentDescription = "Info", + contentDescription = infoLabel, tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier .size(16.dp) @@ -751,7 +773,7 @@ private fun BatteryStatusCard(stats: BatteryStats) { horizontalArrangement = Arrangement.End ) { Text( - text = "Usable: ${stats.usableBatteryLevel}%", + text = usablePercentLabel, style = MaterialTheme.typography.bodyMedium, color = DetailCyan ) @@ -763,14 +785,23 @@ private fun BatteryStatusCard(stats: BatteryStats) { @Composable private fun RangeInformationCard(stats: BatteryStats) { var showTooltip by remember { mutableStateOf(false) } + val rangeInfoTitle = stringResource(R.string.range_information_title) + val rangeInfoMessage = stringResource(R.string.range_information_message) + val rangeInfoLabel = stringResource(R.string.range_information) + val estimatedRangeLabel = stringResource(R.string.estimated_range) + val estimatedRangeSubtitle = stringResource(R.string.estimated_range_subtitle) + val ratedRangeLabel = stringResource(R.string.rated_range) + val ratedRangeSubtitle = stringResource(R.string.rated_range_subtitle) + val idealRangeLabel = stringResource(R.string.ideal_range) + val idealRangeSubtitle = stringResource(R.string.ideal_range_subtitle) + val infoLabel = stringResource(R.string.info) + val gotItLabel = stringResource(R.string.got_it) if (showTooltip) { InfoDialog( - title = "Range Information", - message = "Different ways to estimate how far you can drive.\n\n" + - "• Estimated Range: Based on your recent driving efficiency and current conditions\n" + - "• Rated Range: Official EPA/WLTP test range (what Tesla advertises)\n" + - "• Ideal Range: Maximum theoretical range under perfect conditions (no climate, flat road, moderate speed)", + title = rangeInfoTitle, + message = rangeInfoMessage, + confirmText = gotItLabel, onDismiss = { showTooltip = false } ) } @@ -789,14 +820,14 @@ private fun RangeInformationCard(stats: BatteryStats) { verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Range Information", + text = rangeInfoLabel, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.width(4.dp)) Icon( imageVector = Icons.Filled.Info, - contentDescription = "Info", + contentDescription = infoLabel, tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier .size(16.dp) @@ -808,8 +839,8 @@ private fun RangeInformationCard(stats: BatteryStats) { // Range rows RangeInfoRow( - title = "Estimated Range", - subtitle = "Based on current driving conditions", + title = estimatedRangeLabel, + subtitle = estimatedRangeSubtitle, value = "%.1f km".format(stats.estimatedRange), valueColor = RangeBlue ) @@ -817,8 +848,8 @@ private fun RangeInformationCard(stats: BatteryStats) { Spacer(modifier = Modifier.height(12.dp)) RangeInfoRow( - title = "Rated Range", - subtitle = "Official EPA/WLTP range", + title = ratedRangeLabel, + subtitle = ratedRangeSubtitle, value = "%.1f km".format(stats.ratedRange), valueColor = RangeBlue ) @@ -826,8 +857,8 @@ private fun RangeInformationCard(stats: BatteryStats) { Spacer(modifier = Modifier.height(12.dp)) RangeInfoRow( - title = "Ideal Range", - subtitle = "Ideal driving conditions", + title = idealRangeLabel, + subtitle = idealRangeSubtitle, value = "%.1f km".format(stats.idealRange), valueColor = RangeBlue ) @@ -887,13 +918,19 @@ private fun RangeInfoRow( @Composable private fun EstimatedCapacityCard(stats: BatteryStats) { var showTooltip by remember { mutableStateOf(false) } + val estimatedCapacityTitle = stringResource(R.string.estimated_total_capacity_title) + val estimatedCapacityMessage = stringResource(R.string.estimated_total_capacity_message, stats.batteryLevel) + val estimatedCapacityLabel = stringResource(R.string.estimated_total_capacity) + val rangeAt100Label = stringResource(R.string.range_at_100) + val estimatedRangeDescription = stringResource(R.string.estimated_range_description, stats.batteryLevel, stats.ratedRange) + val infoLabel = stringResource(R.string.info) + val gotItLabel = stringResource(R.string.got_it) if (showTooltip) { InfoDialog( - title = "Estimated Total Capacity", - message = "Projects your maximum range if charged to 100%.\n\n" + - "Calculated by extrapolating your current rated range at ${stats.batteryLevel}% to a full charge.\n\n" + - "This helps you understand your battery's current full capacity without needing to charge to 100% (which isn't recommended for daily use).", + title = estimatedCapacityTitle, + message = estimatedCapacityMessage, + confirmText = gotItLabel, onDismiss = { showTooltip = false } ) } @@ -912,14 +949,14 @@ private fun EstimatedCapacityCard(stats: BatteryStats) { verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Estimated Total Capacity", + text = estimatedCapacityLabel, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.width(4.dp)) Icon( imageVector = Icons.Filled.Info, - contentDescription = "Info", + contentDescription = infoLabel, tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier .size(16.dp) @@ -944,12 +981,12 @@ private fun EstimatedCapacityCard(stats: BatteryStats) { Column(modifier = Modifier.weight(1f)) { Text( - text = "Range at 100%", + text = rangeAt100Label, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium ) Text( - text = "Estimate based on current ${stats.batteryLevel}%% and rated range of %.1f km".format(stats.ratedRange), + text = estimatedRangeDescription, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -970,6 +1007,7 @@ private fun EstimatedCapacityCard(stats: BatteryStats) { private fun InfoDialog( title: String, message: String, + confirmText: String, onDismiss: () -> Unit ) { AlertDialog( @@ -989,7 +1027,7 @@ private fun InfoDialog( }, confirmButton = { TextButton(onClick = onDismiss) { - Text("Got it") + Text(confirmText) } } ) diff --git a/app/src/main/java/com/matedroid/ui/screens/charges/ChargeDetailScreen.kt b/app/src/main/java/com/matedroid/ui/screens/charges/ChargeDetailScreen.kt index 70889f1..ed3f256 100644 --- a/app/src/main/java/com/matedroid/ui/screens/charges/ChargeDetailScreen.kt +++ b/app/src/main/java/com/matedroid/ui/screens/charges/ChargeDetailScreen.kt @@ -57,10 +57,12 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.hilt.navigation.compose.hiltViewModel +import com.matedroid.R import com.matedroid.data.api.models.ChargeDetail import com.matedroid.data.api.models.ChargePoint import com.matedroid.data.api.models.Units @@ -102,12 +104,12 @@ fun ChargeDetailScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Charge Details") }, + title = { Text(stringResource(R.string.charge_details_title)) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" + contentDescription = stringResource(R.string.back) ) } }, @@ -170,38 +172,63 @@ private fun ChargeDetailContent( // Stats grid stats?.let { s -> + // Localized labels + val energyLabel = stringResource(R.string.energy) + val batteryLabel = stringResource(R.string.battery) + val powerLabel = stringResource(R.string.power) + val chargerLabel = stringResource(R.string.charger) + val temperatureLabel = stringResource(R.string.temperature) + val costLabel = stringResource(R.string.cost) + val addedLabel = stringResource(R.string.energy_added) + val usedLabel = stringResource(R.string.used) + val efficiencyLabel = stringResource(R.string.efficiency) + val startLabel = stringResource(R.string.start) + val endLabel = stringResource(R.string.end) + val durationLabel = stringResource(R.string.duration) + val maximumLabel = stringResource(R.string.maximum) + val minimumLabel = stringResource(R.string.minimum) + val averageLabel = stringResource(R.string.average) + val voltageMaxLabel = stringResource(R.string.voltage_max) + val voltageMinLabel = stringResource(R.string.voltage_min) + val voltageAvgLabel = stringResource(R.string.voltage_avg) + val currentMaxLabel = stringResource(R.string.current_max) + val currentMinLabel = stringResource(R.string.current_min) + val currentAvgLabel = stringResource(R.string.current_avg) + val totalLabel = stringResource(R.string.total) + val perKwhLabel = stringResource(R.string.per_kwh) + // Energy section StatsSectionCard( - title = "Energy", + title = energyLabel, icon = Icons.Default.EnergySavingsLeaf, stats = listOf( - StatItem("Added", "%.2f kWh".format(s.energyAdded)), - StatItem("Used", "%.2f kWh".format(s.energyUsed)), - StatItem("Efficiency", "%.1f%%".format(s.efficiency)) + StatItem(addedLabel, "%.2f kWh".format(s.energyAdded)), + StatItem(usedLabel, "%.2f kWh".format(s.energyUsed)), + StatItem(efficiencyLabel, "%.1f%%".format(s.efficiency)) ) ) // Battery section StatsSectionCard( - title = "Battery", + title = batteryLabel, icon = Icons.Default.BatteryChargingFull, stats = listOf( - StatItem("Start", "${s.batteryStart}%"), - StatItem("End", "${s.batteryEnd}%"), - StatItem("Added", "+${s.batteryAdded}%"), - StatItem("Duration", formatDuration(s.durationMin)) + StatItem(startLabel, "${s.batteryStart}%"), + StatItem(endLabel, "${s.batteryEnd}%"), + StatItem(addedLabel, "+${s.batteryAdded}%"), + StatItem(durationLabel, formatDuration(s.durationMin)) ) ) // Power section if (s.powerMax > 0) { StatsSectionCard( - title = "Power", + title = powerLabel, icon = Icons.Default.Bolt, stats = listOf( - StatItem("Maximum", "${s.powerMax} kW"), - StatItem("Minimum", "${s.powerMin} kW"), - StatItem("Average", "%.1f kW".format(s.powerAvg)) + StatItem(maximumLabel, "${s.powerMax} kW"), + StatItem(minimumLabel, "${s.powerMin} kW"), + StatItem(averageLabel, "%.1f kW".format(s.powerAvg)) ) ) } @@ -209,15 +236,15 @@ private fun ChargeDetailContent( // Voltage & Current section if (s.voltageMax > 0) { StatsSectionCard( - title = "Charger", + title = chargerLabel, icon = Icons.Default.ElectricalServices, stats = listOf( - StatItem("Voltage (max)", "${s.voltageMax} V"), - StatItem("Voltage (min)", "${s.voltageMin} V"), - StatItem("Voltage (avg)", "%.0f V".format(s.voltageAvg)), - StatItem("Current (max)", "${s.currentMax} A"), - StatItem("Current (min)", "${s.currentMin} A"), - StatItem("Current (avg)", "%.1f A".format(s.currentAvg)) + StatItem(voltageMaxLabel, "${s.voltageMax} V"), + StatItem(voltageMinLabel, "${s.voltageMin} V"), + StatItem(voltageAvgLabel, "%.0f V".format(s.voltageAvg)), + StatItem(currentMaxLabel, "${s.currentMax} A"), + StatItem(currentMinLabel, "${s.currentMin} A"), + StatItem(currentAvgLabel, "%.1f A".format(s.currentAvg)) ) ) } @@ -225,12 +252,12 @@ private fun ChargeDetailContent( // Temperature section if (s.tempMax > -100) { StatsSectionCard( - title = "Temperature", + title = temperatureLabel, icon = Icons.Default.DeviceThermostat, stats = listOf( - StatItem("Maximum", UnitFormatter.formatTemperature(s.tempMax, units)), - StatItem("Minimum", UnitFormatter.formatTemperature(s.tempMin, units)), - StatItem("Average", UnitFormatter.formatTemperature(s.tempAvg, units)) + StatItem(maximumLabel, UnitFormatter.formatTemperature(s.tempMax, units)), + StatItem(minimumLabel, UnitFormatter.formatTemperature(s.tempMin, units)), + StatItem(averageLabel, UnitFormatter.formatTemperature(s.tempAvg, units)) ) ) } @@ -239,11 +266,11 @@ private fun ChargeDetailContent( s.cost?.let { cost -> if (cost > 0) { StatsSectionCard( - title = "Cost", + title = costLabel, icon = Icons.Default.Paid, stats = listOf( - StatItem("Total", "$currencySymbol%.2f".format(cost)), - StatItem("Per kWh", "$currencySymbol%.3f".format(cost / s.energyAdded.coerceAtLeast(0.001))) + StatItem(totalLabel, "$currencySymbol%.2f".format(cost)), + StatItem(perKwhLabel, "$currencySymbol%.3f".format(cost / s.energyAdded.coerceAtLeast(0.001))) ) ) } @@ -255,21 +282,27 @@ private fun ChargeDetailContent( // Extract time range for labels val timeLabels = extractTimeLabels(chargePoints) + // Localized chart titles + val powerProfileTitle = stringResource(R.string.power_profile) + val voltageProfileTitle = stringResource(R.string.voltage_profile) + val currentProfileTitle = stringResource(R.string.current_profile) + val batteryLevelTitle = stringResource(R.string.battery_level) + if (chargePoints.any { (it.chargerPower ?: 0) > 0 }) { - PowerChartCard(chargePoints = chargePoints, timeLabels = timeLabels) + PowerChartCard(chargePoints = chargePoints, timeLabels = timeLabels, title = powerProfileTitle) } // Only show voltage chart for AC charges if (!isDcCharge && chargePoints.any { (it.chargerVoltage ?: 0) > 0 }) { - VoltageChartCard(chargePoints = chargePoints, timeLabels = timeLabels) + VoltageChartCard(chargePoints = chargePoints, timeLabels = timeLabels, title = voltageProfileTitle) } if (chargePoints.any { (it.chargerCurrent ?: 0) > 0 }) { - CurrentChartCard(chargePoints = chargePoints, timeLabels = timeLabels) + CurrentChartCard(chargePoints = chargePoints, timeLabels = timeLabels, title = currentProfileTitle) } if (chargePoints.any { it.outsideTemp != null }) { - TemperatureChartCard(chargePoints = chargePoints, units = units, timeLabels = timeLabels) + TemperatureChartCard(chargePoints = chargePoints, units = units, timeLabels = timeLabels, title = temperatureLabel) } if (chargePoints.any { it.batteryLevel != null }) { - BatteryChartCard(chargePoints = chargePoints, timeLabels = timeLabels) + BatteryChartCard(chargePoints = chargePoints, timeLabels = timeLabels, title = batteryLevelTitle) } } } @@ -280,6 +313,14 @@ private fun ChargeDetailContent( @Composable private fun LocationHeaderCard(detail: ChargeDetail, currencySymbol: String, isDcCharge: Boolean) { + val locationLabel = stringResource(R.string.location) + val unknownLocationLabel = stringResource(R.string.unknown_location) + val startedLabel = stringResource(R.string.started) + val endedLabel = stringResource(R.string.ended) + val energyAddedLabel = stringResource(R.string.energy_added_header) + val costLabel = stringResource(R.string.cost) + val unknownLabel = stringResource(R.string.unknown) + Card( modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors( @@ -301,12 +342,12 @@ private fun LocationHeaderCard(detail: ChargeDetail, currencySymbol: String, isD Spacer(modifier = Modifier.width(12.dp)) Column { Text( - text = "Location", + text = locationLabel, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) ) Text( - text = detail.address ?: "Unknown location", + text = detail.address ?: unknownLocationLabel, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onPrimaryContainer @@ -330,12 +371,12 @@ private fun LocationHeaderCard(detail: ChargeDetail, currencySymbol: String, isD Spacer(modifier = Modifier.width(12.dp)) Column { Text( - text = "Started", + text = startedLabel, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) ) Text( - text = formatDateTime(detail.startDate), + text = formatDateTime(detail.startDate, unknownLabel), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onPrimaryContainer ) @@ -353,18 +394,18 @@ private fun LocationHeaderCard(detail: ChargeDetail, currencySymbol: String, isD Spacer(modifier = Modifier.width(12.dp)) Column { Text( - text = "Ended", + text = endedLabel, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) ) Text( - text = formatDateTime(detail.endDate), + text = formatDateTime(detail.endDate, unknownLabel), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onPrimaryContainer ) detail.durationStr?.let { duration -> Text( - text = "Duration: $duration", + text = stringResource(R.string.duration_label, duration), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) ) @@ -395,7 +436,7 @@ private fun LocationHeaderCard(detail: ChargeDetail, currencySymbol: String, isD Spacer(modifier = Modifier.width(12.dp)) Column { Text( - text = "Energy Added", + text = energyAddedLabel, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) ) @@ -418,7 +459,7 @@ private fun LocationHeaderCard(detail: ChargeDetail, currencySymbol: String, isD Row(verticalAlignment = Alignment.CenterVertically) { Column(horizontalAlignment = Alignment.End) { Text( - text = "Cost", + text = costLabel, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) ) @@ -448,6 +489,8 @@ private fun LocationHeaderCard(detail: ChargeDetail, currencySymbol: String, isD @Composable private fun ChargeMapCard(latitude: Double, longitude: Double) { val context = LocalContext.current + val locationTitle = stringResource(R.string.location) + val chargeLocationMarker = stringResource(R.string.charge_location) fun openInMaps() { val geoUri = Uri.parse("geo:$latitude,$longitude?q=$latitude,$longitude") @@ -467,7 +510,7 @@ private fun ChargeMapCard(latitude: Double, longitude: Double) { modifier = Modifier.padding(16.dp) ) { Text( - text = "Location", + text = locationTitle, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 12.dp) @@ -498,7 +541,7 @@ private fun ChargeMapCard(latitude: Double, longitude: Double) { val marker = Marker(this).apply { position = geoPoint setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) - title = "Charge Location" + title = chargeLocationMarker } overlays.add(marker) @@ -614,12 +657,12 @@ private fun StatItemView( } @Composable -private fun PowerChartCard(chargePoints: List, timeLabels: List) { +private fun PowerChartCard(chargePoints: List, timeLabels: List, title: String) { val powers = chargePoints.mapNotNull { it.chargerPower?.toFloat() } if (powers.size < 2) return ChartCard( - title = "Power Profile", + title = title, icon = Icons.Default.Bolt, data = powers, color = Color(0xFF4CAF50), @@ -629,12 +672,12 @@ private fun PowerChartCard(chargePoints: List, timeLabels: List, timeLabels: List) { +private fun VoltageChartCard(chargePoints: List, timeLabels: List, title: String) { val voltages = chargePoints.mapNotNull { it.chargerVoltage?.toFloat() } if (voltages.size < 2) return ChartCard( - title = "Voltage Profile", + title = title, icon = Icons.Default.ElectricalServices, data = voltages, color = MaterialTheme.colorScheme.tertiary, @@ -644,12 +687,12 @@ private fun VoltageChartCard(chargePoints: List, timeLabels: List, timeLabels: List) { +private fun CurrentChartCard(chargePoints: List, timeLabels: List, title: String) { val currents = chargePoints.mapNotNull { it.chargerCurrent?.toFloat() } if (currents.size < 2) return ChartCard( - title = "Current Profile", + title = title, icon = Icons.Default.Power, data = currents, color = MaterialTheme.colorScheme.secondary, @@ -659,12 +702,12 @@ private fun CurrentChartCard(chargePoints: List, timeLabels: List, units: Units?, timeLabels: List) { +private fun TemperatureChartCard(chargePoints: List, units: Units?, timeLabels: List, title: String) { val temps = chargePoints.mapNotNull { it.outsideTemp?.toFloat() } if (temps.size < 2) return ChartCard( - title = "Temperature", + title = title, icon = Icons.Default.DeviceThermostat, data = temps, color = Color(0xFFFF9800), @@ -677,12 +720,12 @@ private fun TemperatureChartCard(chargePoints: List, units: Units?, } @Composable -private fun BatteryChartCard(chargePoints: List, timeLabels: List) { +private fun BatteryChartCard(chargePoints: List, timeLabels: List, title: String) { val batteryLevels = chargePoints.mapNotNull { it.batteryLevel?.toFloat() } if (batteryLevels.size < 2) return ChartCard( - title = "Battery Level", + title = title, icon = Icons.Default.BatteryChargingFull, data = batteryLevels, color = MaterialTheme.colorScheme.primary, @@ -748,7 +791,7 @@ private fun ChartCard( @Composable private fun ChargeTypeBadge(isDcCharge: Boolean) { val backgroundColor = if (isDcCharge) Color(0xFFFF9800) else Color(0xFF4CAF50) - val text = if (isDcCharge) "DC" else "AC" + val text = if (isDcCharge) stringResource(R.string.charging_dc) else stringResource(R.string.charging_ac) Box( modifier = Modifier @@ -799,8 +842,8 @@ private fun extractTimeLabels(chargePoints: List): List { } } -private fun formatDateTime(dateStr: String?): String { - if (dateStr == null) return "Unknown" +private fun formatDateTime(dateStr: String?, unknownLabel: String = "Unknown"): String { + if (dateStr == null) return unknownLabel return try { val dateTime = try { OffsetDateTime.parse(dateStr).toLocalDateTime() diff --git a/app/src/main/java/com/matedroid/ui/screens/charges/ChargesScreen.kt b/app/src/main/java/com/matedroid/ui/screens/charges/ChargesScreen.kt index f7bbdbe..63b23d4 100644 --- a/app/src/main/java/com/matedroid/ui/screens/charges/ChargesScreen.kt +++ b/app/src/main/java/com/matedroid/ui/screens/charges/ChargesScreen.kt @@ -63,9 +63,11 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import com.matedroid.R import com.matedroid.data.api.models.ChargeData import com.matedroid.ui.components.BarChartData import com.matedroid.ui.components.InteractiveBarChart @@ -102,12 +104,12 @@ fun ChargesScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Charges") }, + title = { Text(stringResource(R.string.charges_title)) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" + contentDescription = stringResource(R.string.back) ) } }, @@ -224,7 +226,7 @@ private fun ChargesContent( item { Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Charge History", + text = stringResource(R.string.charge_history), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) @@ -245,7 +247,7 @@ private fun ChargesContent( contentAlignment = Alignment.Center ) { Text( - text = "No charges found for selected period", + text = stringResource(R.string.no_charges_found), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -294,7 +296,7 @@ private fun DateFilterChips( FilterChip( selected = filter == selectedFilter, onClick = { onFilterSelected(filter) }, - label = { Text(filter.label) }, + label = { Text(getDateFilterLabel(filter)) }, colors = FilterChipDefaults.filterChipColors( selectedContainerColor = palette.surface, selectedLabelColor = palette.onSurface @@ -304,6 +306,17 @@ private fun DateFilterChips( } } +@Composable +private fun getDateFilterLabel(filter: DateFilter): String { + return when (filter) { + DateFilter.LAST_7_DAYS -> stringResource(R.string.filter_last_7_days) + DateFilter.LAST_30_DAYS -> stringResource(R.string.filter_last_30_days) + DateFilter.LAST_90_DAYS -> stringResource(R.string.filter_last_90_days) + DateFilter.LAST_YEAR -> stringResource(R.string.filter_last_year) + DateFilter.ALL_TIME -> stringResource(R.string.filter_all_time) + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ChargeTypeFilterChips( @@ -333,13 +346,22 @@ private fun ChargeTypeFilterChips( FilterChip( selected = isSelected, onClick = { onFilterSelected(filter) }, - label = { Text(filter.label) }, + label = { Text(getChargeTypeFilterLabel(filter)) }, colors = chipColors ) } } } +@Composable +private fun getChargeTypeFilterLabel(filter: ChargeTypeFilter): String { + return when (filter) { + ChargeTypeFilter.ALL -> stringResource(R.string.filter_all) + ChargeTypeFilter.AC -> stringResource(R.string.charging_ac) + ChargeTypeFilter.DC -> stringResource(R.string.charging_dc) + } +} + @Composable private fun SummaryCard(summary: ChargesSummary, currencySymbol: String, palette: CarColorPalette) { Card( @@ -352,7 +374,7 @@ private fun SummaryCard(summary: ChargesSummary, currencySymbol: String, palette modifier = Modifier.padding(16.dp) ) { Text( - text = "Summary", + text = stringResource(R.string.summary), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = palette.onSurface @@ -365,14 +387,14 @@ private fun SummaryCard(summary: ChargesSummary, currencySymbol: String, palette ) { SummaryItem( icon = Icons.Default.ElectricBolt, - label = "Total Sessions", + label = stringResource(R.string.total_sessions), value = summary.totalCharges.toString(), palette = palette, modifier = Modifier.weight(1.2f) ) SummaryItem( icon = Icons.Default.BatteryChargingFull, - label = "Total Energy", + label = stringResource(R.string.total_energy), value = "%.1f kWh".format(summary.totalEnergyAdded), palette = palette, modifier = Modifier.weight(0.8f) @@ -386,14 +408,14 @@ private fun SummaryCard(summary: ChargesSummary, currencySymbol: String, palette ) { SummaryItem( icon = Icons.Default.Paid, - label = "Total Cost", + label = stringResource(R.string.total_cost), value = "$currencySymbol%.2f".format(summary.totalCost), palette = palette, modifier = Modifier.weight(1.2f) ) SummaryItem( icon = Icons.Default.Paid, - label = "Avg Cost/Session", + label = stringResource(R.string.avg_cost_per_session), value = "$currencySymbol%.2f".format(summary.avgCostPerCharge), palette = palette, modifier = Modifier.weight(0.8f) @@ -446,6 +468,13 @@ private fun ChargeItem( onEditCost: (() -> Unit)? = null, onClick: () -> Unit ) { + val unknownLocation = stringResource(R.string.unknown_location) + val energyAddedLabel = stringResource(R.string.energy_added) + val durationLabel = stringResource(R.string.duration) + val costLabel = stringResource(R.string.cost) + val batteryLabel = stringResource(R.string.battery) + val editLabel = stringResource(R.string.edit) + Card( modifier = Modifier .fillMaxWidth() @@ -480,7 +509,7 @@ private fun ChargeItem( Spacer(modifier = Modifier.width(12.dp)) Column(modifier = Modifier.weight(1f)) { Text( - text = charge.address ?: "Unknown location", + text = charge.address ?: unknownLocation, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold, maxLines = 1 @@ -508,7 +537,7 @@ private fun ChargeItem( icon = Icons.Default.BatteryChargingFull, value = "%.1f".format(charge.chargeEnergyAdded ?: 0.0), unit = "kWh", - label = "Added", + label = energyAddedLabel, modifier = Modifier.weight(1f) ) @@ -517,7 +546,7 @@ private fun ChargeItem( icon = Icons.Default.Schedule, value = charge.durationStr ?: "${charge.durationMin ?: 0}m", unit = "", - label = "Duration", + label = durationLabel, modifier = Modifier.weight(1f) ) } @@ -531,9 +560,10 @@ private fun ChargeItem( icon = Icons.Default.Paid, value = "$currencySymbol%.2f".format(charge.cost ?: 0.0), unit = "", - label = "Cost", + label = costLabel, modifier = Modifier.weight(1f), trailingIcon = if (onEditCost != null) Icons.AutoMirrored.Filled.OpenInNew else null, + trailingContentDescription = editLabel, onClick = onEditCost ) @@ -544,7 +574,7 @@ private fun ChargeItem( icon = Icons.Default.ElectricBolt, value = if (startLevel != null && endLevel != null) "$startLevel% → $endLevel%" else "--", unit = "", - label = "Battery", + label = batteryLabel, modifier = Modifier.weight(1f) ) } @@ -560,6 +590,7 @@ private fun ChargeStatCard( label: String, modifier: Modifier = Modifier, trailingIcon: ImageVector? = null, + trailingContentDescription: String? = null, onClick: (() -> Unit)? = null ) { Card( @@ -611,7 +642,7 @@ private fun ChargeStatCard( if (trailingIcon != null) { Icon( imageVector = trailingIcon, - contentDescription = "Edit", + contentDescription = trailingContentDescription, modifier = Modifier .align(Alignment.TopEnd) .padding(6.dp) @@ -626,7 +657,7 @@ private fun ChargeStatCard( @Composable private fun ChargeTypeBadge(isDcCharge: Boolean) { val backgroundColor = if (isDcCharge) Color(0xFFFF9800) else Color(0xFF4CAF50) - val text = if (isDcCharge) "DC" else "AC" + val text = if (isDcCharge) stringResource(R.string.charging_dc) else stringResource(R.string.charging_ac) Box( modifier = Modifier @@ -734,22 +765,27 @@ private fun ChargesChartPage( currencySymbol: String, palette: CarColorPalette ) { - val (title, icon) = when (chartType) { + val title = when (chartType) { ChargesChartType.ENERGY -> when (granularity) { - ChartGranularity.DAILY -> "Energy per Day" - ChartGranularity.WEEKLY -> "Energy per Week" - ChartGranularity.MONTHLY -> "Energy per Month" - } to Icons.Default.BatteryChargingFull + ChartGranularity.DAILY -> stringResource(R.string.chart_energy_per_day) + ChartGranularity.WEEKLY -> stringResource(R.string.chart_energy_per_week) + ChartGranularity.MONTHLY -> stringResource(R.string.chart_energy_per_month) + } ChargesChartType.COST -> when (granularity) { - ChartGranularity.DAILY -> "Cost per Day" - ChartGranularity.WEEKLY -> "Cost per Week" - ChartGranularity.MONTHLY -> "Cost per Month" - } to Icons.Default.Paid + ChartGranularity.DAILY -> stringResource(R.string.chart_cost_per_day) + ChartGranularity.WEEKLY -> stringResource(R.string.chart_cost_per_week) + ChartGranularity.MONTHLY -> stringResource(R.string.chart_cost_per_month) + } ChargesChartType.COUNT -> when (granularity) { - ChartGranularity.DAILY -> "Charges per Day" - ChartGranularity.WEEKLY -> "Charges per Week" - ChartGranularity.MONTHLY -> "Charges per Month" - } to Icons.Default.ElectricBolt + ChartGranularity.DAILY -> stringResource(R.string.chart_charges_per_day) + ChartGranularity.WEEKLY -> stringResource(R.string.chart_charges_per_week) + ChartGranularity.MONTHLY -> stringResource(R.string.chart_charges_per_month) + } + } + val icon = when (chartType) { + ChargesChartType.ENERGY -> Icons.Default.BatteryChargingFull + ChargesChartType.COST -> Icons.Default.Paid + ChargesChartType.COUNT -> Icons.Default.ElectricBolt } Column { diff --git a/app/src/main/java/com/matedroid/ui/screens/dashboard/DashboardScreen.kt b/app/src/main/java/com/matedroid/ui/screens/dashboard/DashboardScreen.kt index dc567f7..e993bdd 100644 --- a/app/src/main/java/com/matedroid/ui/screens/dashboard/DashboardScreen.kt +++ b/app/src/main/java/com/matedroid/ui/screens/dashboard/DashboardScreen.kt @@ -97,10 +97,12 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.hilt.navigation.compose.hiltViewModel +import com.matedroid.R import org.osmdroid.config.Configuration import org.osmdroid.tileprovider.tilesource.TileSourceFactory import org.osmdroid.util.GeoPoint @@ -156,7 +158,7 @@ fun DashboardScreen( if (showCarSelector && uiState.hasMultipleCars) { AlertDialog( onDismissRequest = { showCarSelector = false }, - title = { Text("Select Vehicle") }, + title = { Text(stringResource(R.string.select_vehicle)) }, text = { Column { uiState.cars.forEach { car -> @@ -182,7 +184,7 @@ fun DashboardScreen( ) Spacer(modifier = Modifier.width(12.dp)) Text( - text = car.displayName ?: "Car ${car.carId}", + text = car.displayName ?: stringResource(R.string.car_fallback_name, car.carId), style = MaterialTheme.typography.bodyLarge, fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, modifier = Modifier.weight(1f) @@ -190,7 +192,7 @@ fun DashboardScreen( if (isSelected) { Icon( imageVector = Icons.Filled.Check, - contentDescription = "Selected", + contentDescription = stringResource(R.string.selected), tint = MaterialTheme.colorScheme.primary ) } @@ -200,7 +202,7 @@ fun DashboardScreen( }, confirmButton = { TextButton(onClick = { showCarSelector = false }) { - Text("Cancel") + Text(stringResource(R.string.cancel)) } } ) @@ -218,7 +220,7 @@ fun DashboardScreen( Text(uiState.selectedCarName ?: "MateDroid") Icon( imageVector = Icons.Filled.ArrowDropDown, - contentDescription = "Select car", + contentDescription = stringResource(R.string.select_car), modifier = Modifier.size(24.dp) ) } @@ -228,7 +230,7 @@ fun DashboardScreen( }, actions = { IconButton(onClick = onNavigateToSettings) { - Icon(Icons.Filled.Settings, contentDescription = "Settings") + Icon(Icons.Filled.Settings, contentDescription = stringResource(R.string.settings)) } } ) @@ -310,7 +312,7 @@ private fun LoadingContent() { Column(horizontalAlignment = Alignment.CenterHorizontally) { CircularProgressIndicator() Spacer(modifier = Modifier.height(16.dp)) - Text("Loading vehicle data...") + Text(stringResource(R.string.loading_vehicle_data)) } } } @@ -330,11 +332,11 @@ private fun EmptyContent() { ) Spacer(modifier = Modifier.height(16.dp)) Text( - text = "No vehicles found", + text = stringResource(R.string.no_vehicles_found), style = MaterialTheme.typography.titleMedium ) Text( - text = "Make sure TeslamateApi is properly configured", + text = stringResource(R.string.no_vehicles_hint), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -350,7 +352,7 @@ private fun ErrorContent(message: String) { ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( - text = "Error loading data", + text = stringResource(R.string.error_loading_data), style = MaterialTheme.typography.titleMedium, color = StatusError ) @@ -592,7 +594,7 @@ private fun CarImage( } Image( bitmap = bitmap.asImageBitmap(), - contentDescription = "Car image - tap for stats", + contentDescription = stringResource(R.string.car_image_tap_for_stats), modifier = Modifier .fillMaxSize() .graphicsLayer { @@ -613,7 +615,7 @@ private fun CarImage( ) { Icon( imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = "View stats", + contentDescription = stringResource(R.string.view_stats), tint = MaterialTheme.colorScheme.onPrimaryContainer, modifier = Modifier.size(20.dp) ) @@ -658,7 +660,7 @@ private fun StatusIndicatorsRow( ) Spacer(modifier = Modifier.width(4.dp)) Text( - text = status.state?.replaceFirstChar { it.uppercase() } ?: "Unknown", + text = status.state?.replaceFirstChar { it.uppercase() } ?: stringResource(R.string.unknown), style = MaterialTheme.typography.labelMedium, color = stateColor ) @@ -675,7 +677,7 @@ private fun StatusIndicatorsRow( ) Spacer(modifier = Modifier.width(4.dp)) Text( - text = if (isLocked) "Locked" else "Unlocked", + text = stringResource(if (isLocked) R.string.locked else R.string.unlocked), style = MaterialTheme.typography.labelMedium, color = if (isLocked) StatusSuccess else StatusWarning ) @@ -685,7 +687,7 @@ private fun StatusIndicatorsRow( Spacer(modifier = Modifier.width(8.dp)) Icon( imageVector = Icons.Filled.Power, - contentDescription = "Plugged in", + contentDescription = stringResource(R.string.plugged_in), modifier = Modifier.size(16.dp), tint = palette.accent ) @@ -797,7 +799,7 @@ private fun BatteryCard( ) { Icon( imageVector = Icons.Filled.BatteryChargingFull, - contentDescription = "Tap for battery health", + contentDescription = stringResource(R.string.tap_for_battery_health), modifier = Modifier.size(28.dp), tint = batteryColor ) @@ -820,7 +822,7 @@ private fun BatteryCard( Spacer(modifier = Modifier.width(6.dp)) Icon( imageVector = Icons.Filled.Warning, - contentDescription = "High charge level", + contentDescription = stringResource(R.string.high_charge_level), modifier = Modifier.size(20.dp), tint = StatusWarning ) @@ -836,7 +838,8 @@ private fun BatteryCard( color = palette.onSurface ) Text( - text = "Limit: ${status.chargeLimitSoc ?: "--"}%", + text = status.chargeLimitSoc?.let { stringResource(R.string.charge_limit_format, it) } + ?: stringResource(R.string.charge_limit_unknown), style = MaterialTheme.typography.labelSmall, color = palette.onSurfaceVariant ) @@ -1021,7 +1024,7 @@ private fun ChargingPowerGaugeCompact( lineHeight = 10.sp ) Text( - text = "kW", + text = stringResource(R.string.unit_kw), style = MaterialTheme.typography.labelSmall.copy(fontSize = 8.sp), color = gaugeColor, lineHeight = 8.sp @@ -1040,7 +1043,7 @@ private fun ChargingPowerGaugeCompact( contentAlignment = Alignment.Center ) { Text( - text = if (isDcCharging) "DC" else "AC", + text = stringResource(if (isDcCharging) R.string.charging_dc else R.string.charging_ac), style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.Bold, color = androidx.compose.ui.graphics.Color.White @@ -1203,7 +1206,7 @@ private fun LocationCard(status: CarStatus, units: Units?, resolvedAddress: Stri Spacer(modifier = Modifier.width(12.dp)) Column(modifier = Modifier.weight(1f)) { Text( - text = "Location", + text = stringResource(R.string.location), style = MaterialTheme.typography.labelSmall, color = palette.onSurfaceVariant ) @@ -1225,7 +1228,7 @@ private fun LocationCard(status: CarStatus, units: Units?, resolvedAddress: Stri ) Spacer(modifier = Modifier.width(4.dp)) Text( - text = "Elevation", + text = stringResource(R.string.elevation), style = MaterialTheme.typography.labelSmall, color = palette.onSurfaceVariant ) @@ -1338,7 +1341,7 @@ private fun VehicleInfoCard( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Vehicle Info", + text = stringResource(R.string.vehicle_info), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = palette.onSurface @@ -1353,7 +1356,7 @@ private fun VehicleInfoCard( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { NavButton( - title = "Charges", + title = stringResource(R.string.nav_charges), value = totalCharges?.let { "%,d".format(it) } ?: "--", icon = Icons.Filled.ElectricBolt, palette = palette, @@ -1361,7 +1364,7 @@ private fun VehicleInfoCard( modifier = Modifier.weight(1f) ) NavButton( - title = "Drives", + title = stringResource(R.string.nav_drives), value = totalDrives?.let { "%,d".format(it) } ?: "--", icon = CustomIcons.SteeringWheel, palette = palette, @@ -1377,7 +1380,7 @@ private fun VehicleInfoCard( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { NavButton( - title = "Mileage", + title = stringResource(R.string.nav_mileage), value = status.odometer?.let { val value = UnitFormatter.formatDistanceValue(it, units, 0) "%,.0f %s".format(value, UnitFormatter.getDistanceUnit(units)) @@ -1388,7 +1391,7 @@ private fun VehicleInfoCard( modifier = Modifier.weight(1f) ) NavButton( - title = "Software", + title = stringResource(R.string.nav_software), value = status.version ?: "--", icon = Icons.Filled.Settings, palette = palette, @@ -1490,14 +1493,14 @@ private fun TirePressureDisplay( verticalArrangement = Arrangement.spacedBy(8.dp) ) { TirePressureItem( - label = "FL", + label = stringResource(R.string.tire_fl), pressure = tpms.pressureFl, color = flColor, units = units, alignEnd = true ) TirePressureItem( - label = "RL", + label = stringResource(R.string.tire_rl), pressure = tpms.pressureRl, color = rlColor, units = units, @@ -1583,14 +1586,14 @@ private fun TirePressureDisplay( verticalArrangement = Arrangement.spacedBy(8.dp) ) { TirePressureItem( - label = "FR", + label = stringResource(R.string.tire_fr), pressure = tpms.pressureFr, color = frColor, units = units, alignEnd = false ) TirePressureItem( - label = "RR", + label = stringResource(R.string.tire_rr), pressure = tpms.pressureRr, color = rrColor, units = units, diff --git a/app/src/main/java/com/matedroid/ui/screens/drives/DriveDetailScreen.kt b/app/src/main/java/com/matedroid/ui/screens/drives/DriveDetailScreen.kt index 52d5783..b35af4b 100644 --- a/app/src/main/java/com/matedroid/ui/screens/drives/DriveDetailScreen.kt +++ b/app/src/main/java/com/matedroid/ui/screens/drives/DriveDetailScreen.kt @@ -60,10 +60,12 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.hilt.navigation.compose.hiltViewModel +import com.matedroid.R import com.matedroid.data.api.models.DriveDetail import com.matedroid.data.api.models.DrivePosition import com.matedroid.data.api.models.Units @@ -110,12 +112,12 @@ fun DriveDetailScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Drive Details") }, + title = { Text(stringResource(R.string.drive_details_title)) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" + contentDescription = stringResource(R.string.back) ) } }, @@ -182,59 +184,59 @@ private fun DriveDetailContent( stats?.let { s -> // Speed section StatsSectionCard( - title = "Speed", + title = stringResource(R.string.speed), icon = Icons.Default.Speed, stats = listOf( - StatItem("Maximum", UnitFormatter.formatSpeed(s.speedMax.toDouble(), units)), - StatItem("Average", UnitFormatter.formatSpeed(s.speedAvg, units)), - StatItem("Avg (distance)", UnitFormatter.formatSpeed(s.avgSpeedFromDistance, units)) + StatItem(stringResource(R.string.maximum), UnitFormatter.formatSpeed(s.speedMax.toDouble(), units)), + StatItem(stringResource(R.string.average), UnitFormatter.formatSpeed(s.speedAvg, units)), + StatItem(stringResource(R.string.avg_distance), UnitFormatter.formatSpeed(s.avgSpeedFromDistance, units)) ) ) // Distance & Duration section StatsSectionCard( - title = "Trip", + title = stringResource(R.string.trip), icon = CustomIcons.SteeringWheel, stats = listOf( - StatItem("Distance", UnitFormatter.formatDistance(s.distance, units)), - StatItem("Duration", formatDuration(s.durationMin)), - StatItem("Efficiency", UnitFormatter.formatEfficiency(s.efficiency, units)) + StatItem(stringResource(R.string.distance), UnitFormatter.formatDistance(s.distance, units)), + StatItem(stringResource(R.string.duration), formatDuration(s.durationMin)), + StatItem(stringResource(R.string.efficiency), UnitFormatter.formatEfficiency(s.efficiency, units)) ) ) // Battery section StatsSectionCard( - title = "Battery", + title = stringResource(R.string.battery), icon = Icons.Default.BatteryChargingFull, stats = listOf( - StatItem("Start", "${s.batteryStart}%"), - StatItem("End", "${s.batteryEnd}%"), - StatItem("Used", "${s.batteryUsed}%"), - StatItem("Energy", "%.2f kWh".format(s.energyUsed)) + StatItem(stringResource(R.string.start), "${s.batteryStart}%"), + StatItem(stringResource(R.string.end), "${s.batteryEnd}%"), + StatItem(stringResource(R.string.used), "${s.batteryUsed}%"), + StatItem(stringResource(R.string.energy), "%.2f kWh".format(s.energyUsed)) ) ) // Power section StatsSectionCard( - title = "Power", + title = stringResource(R.string.power), icon = Icons.Default.Bolt, stats = listOf( - StatItem("Max (accel)", "${s.powerMax} kW"), - StatItem("Min (regen)", "${s.powerMin} kW"), - StatItem("Average", "%.1f kW".format(s.powerAvg)) + StatItem(stringResource(R.string.max_accel), "${s.powerMax} kW"), + StatItem(stringResource(R.string.min_regen), "${s.powerMin} kW"), + StatItem(stringResource(R.string.average), "%.1f kW".format(s.powerAvg)) ) ) // Elevation section if (s.elevationMax > 0 || s.elevationMin > 0) { StatsSectionCard( - title = "Elevation", + title = stringResource(R.string.elevation), icon = Icons.Default.Landscape, stats = listOf( - StatItem("Maximum", "${s.elevationMax} m"), - StatItem("Minimum", "${s.elevationMin} m"), - StatItem("Gain", "+${s.elevationGain} m"), - StatItem("Loss", "-${s.elevationLoss} m") + StatItem(stringResource(R.string.maximum), "${s.elevationMax} m"), + StatItem(stringResource(R.string.minimum), "${s.elevationMin} m"), + StatItem(stringResource(R.string.gain), "+${s.elevationGain} m"), + StatItem(stringResource(R.string.loss), "-${s.elevationLoss} m") ) ) } @@ -242,11 +244,11 @@ private fun DriveDetailContent( // Temperature section if (s.outsideTempAvg != null || s.insideTempAvg != null) { StatsSectionCard( - title = "Temperature", + title = stringResource(R.string.temperature), icon = Icons.Default.DeviceThermostat, stats = listOfNotNull( - s.outsideTempAvg?.let { StatItem("Outside", UnitFormatter.formatTemperature(it, units)) }, - s.insideTempAvg?.let { StatItem("Inside", UnitFormatter.formatTemperature(it, units)) } + s.outsideTempAvg?.let { StatItem(stringResource(R.string.outside), UnitFormatter.formatTemperature(it, units)) }, + s.insideTempAvg?.let { StatItem(stringResource(R.string.inside), UnitFormatter.formatTemperature(it, units)) } ) ) } @@ -301,12 +303,12 @@ private fun RouteHeaderCard(detail: DriveDetail) { Spacer(modifier = Modifier.width(12.dp)) Column { Text( - text = "From", + text = stringResource(R.string.from), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) ) Text( - text = detail.startAddress ?: "Unknown location", + text = detail.startAddress ?: stringResource(R.string.unknown_location), style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onPrimaryContainer @@ -330,12 +332,12 @@ private fun RouteHeaderCard(detail: DriveDetail) { Spacer(modifier = Modifier.width(12.dp)) Column { Text( - text = "To", + text = stringResource(R.string.to), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) ) Text( - text = detail.endAddress ?: "Unknown location", + text = detail.endAddress ?: stringResource(R.string.unknown_location), style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onPrimaryContainer @@ -359,7 +361,7 @@ private fun RouteHeaderCard(detail: DriveDetail) { Spacer(modifier = Modifier.width(12.dp)) Column { Text( - text = "Started", + text = stringResource(R.string.started), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) ) @@ -382,7 +384,7 @@ private fun RouteHeaderCard(detail: DriveDetail) { Spacer(modifier = Modifier.width(12.dp)) Column { Text( - text = "Ended", + text = stringResource(R.string.ended), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) ) @@ -393,7 +395,7 @@ private fun RouteHeaderCard(detail: DriveDetail) { ) detail.durationStr?.let { duration -> Text( - text = "Duration: $duration", + text = stringResource(R.string.duration_label, duration), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) ) @@ -441,7 +443,7 @@ private fun DriveMapCard(positions: List, routeColor: Color) { modifier = Modifier.padding(16.dp) ) { Text( - text = "Route Map", + text = stringResource(R.string.route_map), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 12.dp) @@ -614,7 +616,7 @@ private fun SpeedChartCard(positions: List, units: Units?, timeLa if (speeds.size < 2) return ChartCard( - title = "Speed Profile", + title = stringResource(R.string.speed_profile), icon = Icons.Default.Speed, data = speeds, color = MaterialTheme.colorScheme.primary, @@ -632,7 +634,7 @@ private fun PowerChartCard(positions: List, timeLabels: List, timeLabels: List, timeLabels: List< if (elevations.size < 2) return ChartCard( - title = "Elevation Profile", + title = stringResource(R.string.elevation_profile), icon = Icons.Default.Landscape, data = elevations, color = Color(0xFF8B4513), // Brown color for terrain diff --git a/app/src/main/java/com/matedroid/ui/screens/drives/DrivesScreen.kt b/app/src/main/java/com/matedroid/ui/screens/drives/DrivesScreen.kt index 971f1ca..131ac93 100644 --- a/app/src/main/java/com/matedroid/ui/screens/drives/DrivesScreen.kt +++ b/app/src/main/java/com/matedroid/ui/screens/drives/DrivesScreen.kt @@ -60,9 +60,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import com.matedroid.R import com.matedroid.data.api.models.DriveData import com.matedroid.data.api.models.Units import com.matedroid.ui.components.BarChartData @@ -115,12 +117,12 @@ fun DrivesScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Drives") }, + title = { Text(stringResource(R.string.drives_title)) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" + contentDescription = stringResource(R.string.back) ) } }, @@ -223,7 +225,7 @@ private fun DrivesContent( item { Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Drive History", + text = stringResource(R.string.drive_history), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) @@ -244,7 +246,7 @@ private fun DrivesContent( contentAlignment = Alignment.Center ) { Text( - text = "No drives found for selected period", + text = stringResource(R.string.no_drives_found), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -262,6 +264,17 @@ private fun DrivesContent( } } +@Composable +private fun getDateFilterLabel(filter: DriveDateFilter): String { + return when (filter) { + DriveDateFilter.LAST_7_DAYS -> stringResource(R.string.filter_last_7_days) + DriveDateFilter.LAST_30_DAYS -> stringResource(R.string.filter_last_30_days) + DriveDateFilter.LAST_90_DAYS -> stringResource(R.string.filter_last_90_days) + DriveDateFilter.LAST_YEAR -> stringResource(R.string.filter_last_year) + DriveDateFilter.ALL_TIME -> stringResource(R.string.filter_all_time) + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun DateFilterChips( @@ -276,7 +289,7 @@ private fun DateFilterChips( FilterChip( selected = filter == selectedFilter, onClick = { onFilterSelected(filter) }, - label = { Text(filter.label) }, + label = { Text(getDateFilterLabel(filter)) }, colors = FilterChipDefaults.filterChipColors( selectedContainerColor = palette.surface, selectedLabelColor = palette.onSurface @@ -286,6 +299,17 @@ private fun DateFilterChips( } } +@Composable +private fun getDistanceFilterLabel(filter: DriveDistanceFilter, units: Units?): String { + val isImperial = units?.isImperial == true + return when (filter) { + DriveDistanceFilter.ALL -> stringResource(R.string.filter_all) + DriveDistanceFilter.COMMUTE -> stringResource(if (isImperial) R.string.filter_commute_mi else R.string.filter_commute_km) + DriveDistanceFilter.DAY_TRIP -> stringResource(if (isImperial) R.string.filter_day_trip_mi else R.string.filter_day_trip_km) + DriveDistanceFilter.ROAD_TRIP -> stringResource(if (isImperial) R.string.filter_road_trip_mi else R.string.filter_road_trip_km) + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun DistanceFilterChips( @@ -301,7 +325,7 @@ private fun DistanceFilterChips( FilterChip( selected = filter == selectedFilter, onClick = { onFilterSelected(filter) }, - label = { Text(filter.getLabel(units)) }, + label = { Text(getDistanceFilterLabel(filter, units)) }, colors = FilterChipDefaults.filterChipColors( selectedContainerColor = palette.surface, selectedLabelColor = palette.onSurface @@ -323,7 +347,7 @@ private fun SummaryCard(summary: DrivesSummary, palette: CarColorPalette) { modifier = Modifier.padding(16.dp) ) { Text( - text = "Summary", + text = stringResource(R.string.summary), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = palette.onSurface @@ -336,14 +360,14 @@ private fun SummaryCard(summary: DrivesSummary, palette: CarColorPalette) { ) { SummaryItem( icon = Icons.Default.DirectionsCar, - label = "Total Trips", + label = stringResource(R.string.total_trips), value = summary.totalDrives.toString(), palette = palette, modifier = Modifier.weight(1.2f) ) SummaryItem( icon = CustomIcons.SteeringWheel, - label = "Total Distance", + label = stringResource(R.string.total_distance), value = "%.1f km".format(summary.totalDistanceKm), palette = palette, modifier = Modifier.weight(0.8f) @@ -357,14 +381,14 @@ private fun SummaryCard(summary: DrivesSummary, palette: CarColorPalette) { ) { SummaryItem( icon = Icons.Default.Timer, - label = "Total Time", + label = stringResource(R.string.total_time), value = formatDuration(summary.totalDurationMin), palette = palette, modifier = Modifier.weight(1.2f) ) SummaryItem( icon = Icons.Default.Speed, - label = "Max Speed", + label = stringResource(R.string.max_speed), value = "${summary.maxSpeedKmh} km/h", palette = palette, modifier = Modifier.weight(0.8f) @@ -449,7 +473,7 @@ private fun DriveItem( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = drive.startAddress ?: "Unknown", + text = drive.startAddress ?: stringResource(R.string.unknown), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f) @@ -466,7 +490,7 @@ private fun DriveItem( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = drive.endAddress ?: "Unknown", + text = drive.endAddress ?: stringResource(R.string.unknown), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold ) @@ -492,7 +516,7 @@ private fun DriveItem( icon = CustomIcons.SteeringWheel, value = "%.1f".format(drive.distance ?: 0.0), unit = "km", - label = "Distance", + label = stringResource(R.string.distance), modifier = Modifier.weight(1f) ) @@ -501,7 +525,7 @@ private fun DriveItem( icon = Icons.Default.Schedule, value = formatDuration(drive.durationMin ?: 0), unit = "", - label = "Duration", + label = stringResource(R.string.duration), modifier = Modifier.weight(1f) ) } @@ -515,7 +539,7 @@ private fun DriveItem( icon = Icons.Default.Speed, value = "${drive.speedMax ?: 0}", unit = "km/h", - label = "Max Speed", + label = stringResource(R.string.max_speed), modifier = Modifier.weight(1f) ) @@ -526,7 +550,7 @@ private fun DriveItem( icon = Icons.Default.BatteryChargingFull, value = if (startLevel != null && endLevel != null) "$startLevel% → $endLevel%" else "--", unit = "", - label = "Battery", + label = stringResource(R.string.battery), modifier = Modifier.weight(1f) ) } @@ -689,24 +713,24 @@ private fun DrivesChartPage( val (title, icon) = when (chartType) { DrivesChartType.COUNT -> when (granularity) { - DriveChartGranularity.DAILY -> "Drives per Day" - DriveChartGranularity.WEEKLY -> "Drives per Week" - DriveChartGranularity.MONTHLY -> "Drives per Month" + DriveChartGranularity.DAILY -> stringResource(R.string.chart_drives_per_day) + DriveChartGranularity.WEEKLY -> stringResource(R.string.chart_drives_per_week) + DriveChartGranularity.MONTHLY -> stringResource(R.string.chart_drives_per_month) } to Icons.Default.DirectionsCar DrivesChartType.TIME -> when (granularity) { - DriveChartGranularity.DAILY -> "Driving time per Day" - DriveChartGranularity.WEEKLY -> "Driving time per Week" - DriveChartGranularity.MONTHLY -> "Driving time per Month" + DriveChartGranularity.DAILY -> stringResource(R.string.chart_time_per_day) + DriveChartGranularity.WEEKLY -> stringResource(R.string.chart_time_per_week) + DriveChartGranularity.MONTHLY -> stringResource(R.string.chart_time_per_month) } to Icons.Default.Timer DrivesChartType.DISTANCE -> when (granularity) { - DriveChartGranularity.DAILY -> "Distance per Day" - DriveChartGranularity.WEEKLY -> "Distance per Week" - DriveChartGranularity.MONTHLY -> "Distance per Month" + DriveChartGranularity.DAILY -> stringResource(R.string.chart_distance_per_day) + DriveChartGranularity.WEEKLY -> stringResource(R.string.chart_distance_per_week) + DriveChartGranularity.MONTHLY -> stringResource(R.string.chart_distance_per_month) } to CustomIcons.SteeringWheel DrivesChartType.TOP_SPEED -> when (granularity) { - DriveChartGranularity.DAILY -> "Top speed per Day" - DriveChartGranularity.WEEKLY -> "Top speed per Week" - DriveChartGranularity.MONTHLY -> "Top speed per Month" + DriveChartGranularity.DAILY -> stringResource(R.string.chart_speed_per_day) + DriveChartGranularity.WEEKLY -> stringResource(R.string.chart_speed_per_week) + DriveChartGranularity.MONTHLY -> stringResource(R.string.chart_speed_per_month) } to Icons.Default.Speed } diff --git a/app/src/main/java/com/matedroid/ui/screens/drives/WeatherAlongTheWayCard.kt b/app/src/main/java/com/matedroid/ui/screens/drives/WeatherAlongTheWayCard.kt index f7169ae..8e71984 100644 --- a/app/src/main/java/com/matedroid/ui/screens/drives/WeatherAlongTheWayCard.kt +++ b/app/src/main/java/com/matedroid/ui/screens/drives/WeatherAlongTheWayCard.kt @@ -22,9 +22,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.matedroid.R import com.matedroid.data.api.models.Units import com.matedroid.data.repository.WeatherCondition import com.matedroid.data.repository.WeatherPoint @@ -72,7 +74,7 @@ fun WeatherAlongTheWayCard( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Weather Along the Way", + text = stringResource(R.string.weather_card_title), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) @@ -96,7 +98,7 @@ fun WeatherAlongTheWayCard( ) Spacer(modifier = Modifier.width(12.dp)) Text( - text = "Loading weather data...", + text = stringResource(R.string.weather_loading), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) ) @@ -111,7 +113,7 @@ fun WeatherAlongTheWayCard( contentAlignment = Alignment.Center ) { Text( - text = "Weather data unavailable", + text = stringResource(R.string.weather_unavailable), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) ) @@ -153,14 +155,14 @@ private fun WeatherTableHeader() { horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = "Time", + text = stringResource(R.string.weather_time), style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), modifier = Modifier.weight(1f) ) Text( - text = "Distance", + text = stringResource(R.string.weather_distance), style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), @@ -168,7 +170,7 @@ private fun WeatherTableHeader() { textAlign = TextAlign.Center ) Text( - text = "Weather", + text = stringResource(R.string.weather_weather), style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), @@ -261,15 +263,16 @@ private fun getWeatherIconColor(condition: WeatherCondition): Color { /** * Returns a human-readable description for a weather condition. */ +@Composable private fun getWeatherDescription(condition: WeatherCondition): String { return when (condition) { - WeatherCondition.CLEAR -> "Clear sky" - WeatherCondition.PARTLY_CLOUDY -> "Partly cloudy" - WeatherCondition.FOG -> "Foggy" - WeatherCondition.DRIZZLE -> "Light rain" - WeatherCondition.RAIN -> "Rain" - WeatherCondition.SNOW -> "Snow" - WeatherCondition.THUNDERSTORM -> "Thunderstorm" + WeatherCondition.CLEAR -> stringResource(R.string.weather_clear) + WeatherCondition.PARTLY_CLOUDY -> stringResource(R.string.weather_partly_cloudy) + WeatherCondition.FOG -> stringResource(R.string.weather_fog) + WeatherCondition.DRIZZLE -> stringResource(R.string.weather_drizzle) + WeatherCondition.RAIN -> stringResource(R.string.weather_rain) + WeatherCondition.SNOW -> stringResource(R.string.weather_snow) + WeatherCondition.THUNDERSTORM -> stringResource(R.string.weather_thunderstorm) } } @@ -277,13 +280,14 @@ private fun getWeatherDescription(condition: WeatherCondition): String { * Formats distance for the weather table. * Shows "Start" for 0km, "End" for the last point, and formats with appropriate units otherwise. */ +@Composable private fun formatWeatherDistance(distanceKm: Double, units: Units?, isLastPoint: Boolean): String { if (isLastPoint) { - return "End" + return stringResource(R.string.weather_end) } if (distanceKm < 0.1) { - return "Start" + return stringResource(R.string.weather_start) } val isImperial = units?.isImperial == true diff --git a/app/src/main/java/com/matedroid/ui/screens/mileage/MileageScreen.kt b/app/src/main/java/com/matedroid/ui/screens/mileage/MileageScreen.kt index 13c8dd8..8cd5212 100644 --- a/app/src/main/java/com/matedroid/ui/screens/mileage/MileageScreen.kt +++ b/app/src/main/java/com/matedroid/ui/screens/mileage/MileageScreen.kt @@ -57,10 +57,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.foundation.isSystemInDarkTheme import androidx.hilt.navigation.compose.hiltViewModel +import com.matedroid.R import com.matedroid.ui.components.BarChartData import com.matedroid.ui.components.InteractiveBarChart import com.matedroid.ui.theme.CarColorPalette @@ -116,10 +118,10 @@ fun MileageScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Mileage") }, + title = { Text(stringResource(R.string.mileage_title)) }, navigationIcon = { IconButton(onClick = onNavigateBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back)) } }, colors = TopAppBarDefaults.topAppBarColors( @@ -232,7 +234,7 @@ private fun YearOverviewContent( SummaryRow( totalDistance = uiState.totalLifetimeDistance, avgDistance = uiState.avgYearlyDistance, - avgLabel = "Avg/Year", + avgLabel = stringResource(R.string.mileage_avg_year), driveCount = uiState.totalLifetimeDriveCount, palette = palette, firstDriveDate = uiState.firstDriveDate @@ -264,7 +266,7 @@ private fun YearOverviewContent( contentAlignment = Alignment.Center ) { Text( - text = "No drive data available", + text = stringResource(R.string.mileage_no_data), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -292,7 +294,7 @@ private fun YearlyChartCard(chartData: List>, palette: CarColo ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Mileage by Year", + text = stringResource(R.string.mileage_by_year), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = palette.onSurface @@ -381,7 +383,7 @@ private fun YearRow( Icon( imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = "View details", + contentDescription = stringResource(R.string.view_details), tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(24.dp) ) @@ -410,7 +412,7 @@ private fun YearDetailScreen( title = { Text(year.toString()) }, navigationIcon = { IconButton(onClick = onClose) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back)) } }, colors = TopAppBarDefaults.topAppBarColors( @@ -431,7 +433,7 @@ private fun YearDetailScreen( SummaryRow( totalDistance = uiState.yearTotalDistance, avgDistance = uiState.avgMonthlyDistance, - avgLabel = "Avg/Month", + avgLabel = stringResource(R.string.mileage_avg_month), driveCount = uiState.yearDriveCount, palette = palette ) @@ -460,7 +462,7 @@ private fun YearDetailScreen( contentAlignment = Alignment.Center ) { Text( - text = "No drive data for $year", + text = stringResource(R.string.mileage_no_data_year, year), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -489,7 +491,7 @@ private fun MonthlyChartCard(chartData: List>, palette: CarCol ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Mileage by Month", + text = stringResource(R.string.mileage_by_month), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = palette.onSurface @@ -585,7 +587,7 @@ private fun MonthRow( Icon( imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = "View details", + contentDescription = stringResource(R.string.view_details), tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(24.dp) ) @@ -617,7 +619,7 @@ private fun MonthDetailScreen( }, navigationIcon = { IconButton(onClick = onClose) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back)) } }, colors = TopAppBarDefaults.topAppBarColors( @@ -657,7 +659,7 @@ private fun MonthDetailScreen( if (dailyData.isNotEmpty()) { item { Text( - text = "Recent trips", + text = stringResource(R.string.mileage_recent_trips), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) @@ -793,24 +795,26 @@ private fun SummaryRow( var showAvgInfoDialog by remember { mutableStateOf(false) } + // Pre-compute localized strings for dialog + val avgYearTitle = stringResource(R.string.mileage_avg_year_title) + val okText = stringResource(R.string.ok) + // Info dialog explaining the avg/year calculation if (showAvgInfoDialog && firstDriveDate != null) { val dateFormatter = DateTimeFormatter.ofPattern("d MMM yyyy", Locale.getDefault()) val formattedDate = firstDriveDate.format(dateFormatter) - val daysSinceFirst = java.time.temporal.ChronoUnit.DAYS.between(firstDriveDate, LocalDate.now()) + val daysSinceFirst = java.time.temporal.ChronoUnit.DAYS.between(firstDriveDate, LocalDate.now()).toInt() + val dialogMessage = stringResource(R.string.mileage_avg_year_message, formattedDate, daysSinceFirst) AlertDialog( onDismissRequest = { showAvgInfoDialog = false }, - title = { Text("Average per Year") }, + title = { Text(avgYearTitle) }, text = { - Text( - "Based on your average daily driving since your first recorded drive on $formattedDate ($daysSinceFirst days ago).\n\n" + - "Calculation: total distance ÷ days × 365" - ) + Text(dialogMessage) }, confirmButton = { TextButton(onClick = { showAvgInfoDialog = false }) { - Text("OK") + Text(okText) } } ) @@ -829,7 +833,7 @@ private fun SummaryRow( SummaryItem( icon = Icons.Outlined.AllInclusive, value = "%.0f km".format(totalDistance), - label = "Total", + label = stringResource(R.string.mileage_total), iconColor = iconColor, valueColor = valueColor, labelColor = labelColor @@ -847,7 +851,7 @@ private fun SummaryRow( SummaryItem( icon = Icons.Filled.DirectionsCar, value = driveCount.toString(), - label = "# of drives", + label = stringResource(R.string.mileage_drive_count), iconColor = iconColor, valueColor = valueColor, labelColor = labelColor @@ -925,7 +929,7 @@ private fun SummaryItemWithInfo( Spacer(modifier = Modifier.width(2.dp)) Icon( imageVector = Icons.Outlined.Info, - contentDescription = "Info about calculation", + contentDescription = stringResource(R.string.info_about_calculation), tint = labelColor, modifier = Modifier.size(12.dp) ) @@ -1016,14 +1020,14 @@ private fun DailyChartCard( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Mileage by Day", + text = stringResource(R.string.mileage_by_day), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = palette.onSurface ) } Text( - text = "$daysWithData days", + text = stringResource(R.string.format_days_count, daysWithData), style = MaterialTheme.typography.labelMedium, color = palette.onSurfaceVariant ) @@ -1160,7 +1164,7 @@ private fun DayTripRow( // Arrow indicator Icon( imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = "View details", + contentDescription = stringResource(R.string.view_details), tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(24.dp) ) @@ -1193,7 +1197,7 @@ private fun DayDetailScreen( title = { Text(dayOfWeek) }, navigationIcon = { IconButton(onClick = onClose) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back)) } }, colors = TopAppBarDefaults.topAppBarColors( @@ -1222,7 +1226,7 @@ private fun DayDetailScreen( if (dayData.drives.isNotEmpty()) { item { Text( - text = "Drives", + text = stringResource(R.string.mileage_drives), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) @@ -1423,7 +1427,7 @@ private fun DriveRow( // Arrow indicator Icon( imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = "View details", + contentDescription = stringResource(R.string.view_details), tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(24.dp) ) diff --git a/app/src/main/java/com/matedroid/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/matedroid/ui/screens/settings/SettingsScreen.kt index 7a1b3f8..e069c9d 100644 --- a/app/src/main/java/com/matedroid/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/matedroid/ui/screens/settings/SettingsScreen.kt @@ -67,9 +67,11 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.withStyle +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import com.matedroid.R import com.matedroid.data.model.Currency import com.matedroid.ui.theme.MateDroidTheme import com.matedroid.ui.theme.StatusWarning @@ -103,7 +105,7 @@ fun SettingsScreen( Scaffold( topBar = { TopAppBar( - title = { Text("MateDroid Settings") } + title = { Text(stringResource(R.string.settings_title)) } ) }, snackbarHost = { SnackbarHost(snackbarHostState) } @@ -139,7 +141,7 @@ private fun LoadingContent(modifier: Modifier = Modifier) { ) { CircularProgressIndicator() Spacer(modifier = Modifier.height(16.dp)) - Text("Loading settings...") + Text(stringResource(R.string.loading_settings)) } } @@ -167,7 +169,7 @@ private fun CollapsibleSection( ) Icon( imageVector = if (expanded) Icons.Filled.KeyboardArrowUp else Icons.Filled.KeyboardArrowDown, - contentDescription = if (expanded) "Collapse" else "Expand", + contentDescription = stringResource(if (expanded) R.string.collapse else R.string.expand), tint = MaterialTheme.colorScheme.primary ) } @@ -215,14 +217,14 @@ private fun SettingsContent( ) { // === Server Connection Section === Text( - text = "Connect to TeslamateApi", + text = stringResource(R.string.settings_connect_title), style = MaterialTheme.typography.titleMedium ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Enter your TeslamateApi server URL to connect.", + text = stringResource(R.string.settings_connect_description), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -232,8 +234,8 @@ private fun SettingsContent( OutlinedTextField( value = uiState.serverUrl, onValueChange = onServerUrlChange, - label = { Text("Server URL") }, - placeholder = { Text("https://teslamate-api.example.com") }, + label = { Text(stringResource(R.string.settings_server_url_label)) }, + placeholder = { Text(stringResource(R.string.settings_server_url_placeholder)) }, modifier = Modifier .fillMaxWidth() .testTag("urlInput"), @@ -248,7 +250,7 @@ private fun SettingsContent( HorizontalDivider() CollapsibleSection( - title = "Advanced network settings", + title = stringResource(R.string.settings_advanced_network), expanded = advancedNetworkExpanded, onToggle = { advancedNetworkExpanded = !advancedNetworkExpanded } ) { @@ -258,8 +260,8 @@ private fun SettingsContent( OutlinedTextField( value = uiState.secondaryServerUrl, onValueChange = onSecondaryServerUrlChange, - label = { Text("Secondary Server URL (optional)") }, - placeholder = { Text("https://teslamate-api-internal.local") }, + label = { Text(stringResource(R.string.settings_secondary_url_label)) }, + placeholder = { Text(stringResource(R.string.settings_secondary_url_placeholder)) }, modifier = Modifier.fillMaxWidth(), singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), @@ -267,7 +269,7 @@ private fun SettingsContent( ) Text( - text = "Fallback URL used when the primary server is unreachable. Useful for LAN/VPN access.", + text = stringResource(R.string.settings_secondary_url_hint), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 4.dp) @@ -279,8 +281,8 @@ private fun SettingsContent( OutlinedTextField( value = uiState.apiToken, onValueChange = onApiTokenChange, - label = { Text("API Token (optional)") }, - placeholder = { Text("Your API token") }, + label = { Text(stringResource(R.string.settings_api_token_label)) }, + placeholder = { Text(stringResource(R.string.settings_api_token_placeholder)) }, modifier = Modifier .fillMaxWidth() .testTag("tokenInput"), @@ -299,11 +301,9 @@ private fun SettingsContent( } else { Icons.Filled.Visibility }, - contentDescription = if (passwordVisible) { - "Hide token" - } else { - "Show token" - } + contentDescription = stringResource( + if (passwordVisible) R.string.hide_token else R.string.show_token + ) ) } }, @@ -311,7 +311,7 @@ private fun SettingsContent( ) Text( - text = "Leave empty if your TeslamateApi doesn't require authentication.", + text = stringResource(R.string.settings_api_token_hint), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 4.dp) @@ -328,11 +328,11 @@ private fun SettingsContent( ) { Column(modifier = Modifier.weight(1f)) { Text( - text = "Accept invalid certificates", + text = stringResource(R.string.settings_accept_invalid_certs), style = MaterialTheme.typography.bodyMedium ) Text( - text = "Enable for self-signed certificates", + text = stringResource(R.string.settings_accept_invalid_certs_hint), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -365,7 +365,7 @@ private fun SettingsContent( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Disabling certificate validation makes connections vulnerable to man-in-the-middle attacks. Only use on trusted networks.", + text = stringResource(R.string.settings_accept_invalid_certs_warning), style = MaterialTheme.typography.bodySmall, color = StatusWarning ) @@ -380,7 +380,7 @@ private fun SettingsContent( // === Display Settings Section === Text( - text = "Display Settings", + text = stringResource(R.string.settings_display_title), style = MaterialTheme.typography.titleMedium ) @@ -393,14 +393,14 @@ private fun SettingsContent( OutlinedTextField( value = "${selectedCurrency.symbol} ${selectedCurrency.code} - ${selectedCurrency.name}", onValueChange = {}, - label = { Text("Currency for costs") }, + label = { Text(stringResource(R.string.settings_currency_label)) }, modifier = Modifier.fillMaxWidth(), readOnly = true, trailingIcon = { IconButton(onClick = { currencyDropdownExpanded = true }) { Icon( imageVector = Icons.Filled.ArrowDropDown, - contentDescription = "Select currency" + contentDescription = stringResource(R.string.settings_currency_select) ) } } @@ -431,15 +431,15 @@ private fun SettingsContent( OutlinedTextField( value = uiState.teslamateBaseUrl, onValueChange = onTeslamateBaseUrlChange, - label = { Text("Teslamate Base URL (optional)") }, - placeholder = { Text("https://teslamate.example.com") }, + label = { Text(stringResource(R.string.settings_teslamate_url_label)) }, + placeholder = { Text(stringResource(R.string.settings_teslamate_url_placeholder)) }, modifier = Modifier.fillMaxWidth(), singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri) ) Text( - text = "Used for editing charge costs directly in Teslamate.", + text = stringResource(R.string.settings_teslamate_url_hint), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 4.dp) @@ -451,7 +451,7 @@ private fun SettingsContent( HorizontalDivider() CollapsibleSection( - title = "Extra settings", + title = stringResource(R.string.settings_extra), expanded = extraSettingsExpanded, onToggle = { extraSettingsExpanded = !extraSettingsExpanded } ) { @@ -467,7 +467,7 @@ private fun SettingsContent( Column(modifier = Modifier.weight(1f)) { Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = "Show short drives / charges", + text = stringResource(R.string.settings_show_short_label), style = MaterialTheme.typography.bodyMedium ) IconButton( @@ -476,14 +476,14 @@ private fun SettingsContent( ) { Icon( imageVector = Icons.Filled.Info, - contentDescription = "More information", + contentDescription = stringResource(R.string.more_information), tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(18.dp) ) } } Text( - text = "Display very short drives and minimal charges in lists", + text = stringResource(R.string.settings_show_short_hint), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -498,19 +498,13 @@ private fun SettingsContent( if (showShortDrivesChargesInfoDialog) { AlertDialog( onDismissRequest = { showShortDrivesChargesInfoDialog = false }, - title = { Text("Short drives & charges") }, + title = { Text(stringResource(R.string.settings_short_dialog_title)) }, text = { - Text( - "When disabled (default), the following are hidden from the lists:\n\n" + - "• Drives under 1 minute or less than 0.1 km\n" + - "• Charges of 0.1 kWh or less\n\n" + - "These entries are still included in totals, averages, and statistics. " + - "Enable this setting to see all entries in the lists." - ) + Text(stringResource(R.string.settings_short_dialog_text)) }, confirmButton = { TextButton(onClick = { showShortDrivesChargesInfoDialog = false }) { - Text("OK") + Text(stringResource(R.string.ok)) } } ) @@ -531,11 +525,11 @@ private fun SettingsContent( ) Spacer(modifier = Modifier.width(8.dp)) } - Text(if (uiState.isResyncing) "Resyncing..." else "Force Full Resync") + Text(stringResource(if (uiState.isResyncing) R.string.settings_resyncing else R.string.settings_resync_button)) } Text( - text = "Re-downloads all drive and charge details. Use if stats seem incorrect.", + text = stringResource(R.string.settings_resync_hint), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 4.dp) @@ -553,16 +547,15 @@ private fun SettingsContent( modifier = Modifier.size(32.dp) ) }, - title = { Text("Force Full Resync?") }, + title = { Text(stringResource(R.string.settings_resync_dialog_title)) }, text = { Text( buildAnnotatedString { - append("This will ") + append(stringResource(R.string.settings_resync_dialog_text_prefix)) withStyle(SpanStyle(color = StatusError, fontWeight = FontWeight.Bold)) { - append("DELETE") + append(stringResource(R.string.settings_resync_dialog_text_delete)) } - append(" all cached drives, charges, and statistics, then re-download everything from the server.\n\n") - append("The process may take several minutes depending on how much data you have.") + append(stringResource(R.string.settings_resync_dialog_text_suffix)) } ) }, @@ -573,12 +566,12 @@ private fun SettingsContent( onForceResync() } ) { - Text("Resync") + Text(stringResource(R.string.settings_resync_confirm)) } }, dismissButton = { TextButton(onClick = { showResyncConfirmDialog = false }) { - Text("Cancel") + Text(stringResource(R.string.cancel)) } } ) @@ -612,7 +605,7 @@ private fun SettingsContent( ) Spacer(modifier = Modifier.width(8.dp)) } - Text("Test Connection") + Text(stringResource(R.string.settings_test_connection)) } Button( @@ -630,7 +623,7 @@ private fun SettingsContent( ) Spacer(modifier = Modifier.width(8.dp)) } - Text("Save & Continue") + Text(stringResource(R.string.settings_save)) } } @@ -641,7 +634,7 @@ private fun SettingsContent( onClick = onPalettePreview, modifier = Modifier.fillMaxWidth() ) { - Text("Preview Color Palettes (Debug)") + Text(stringResource(R.string.settings_palette_preview)) } } @@ -673,7 +666,7 @@ private fun TestResultCard(result: TestResult) { ) { // Primary server result ServerTestResultRow( - label = "Primary server", + label = stringResource(R.string.settings_primary_server), result = result.primaryResult ) @@ -681,7 +674,7 @@ private fun TestResultCard(result: TestResult) { result.secondaryResult?.let { secondaryResult -> HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) ServerTestResultRow( - label = "Secondary server", + label = stringResource(R.string.settings_secondary_server), result = secondaryResult ) } @@ -694,11 +687,12 @@ private fun ServerTestResultRow( label: String, result: ServerTestResult ) { + val connectedText = stringResource(R.string.settings_connected) val (icon, color, statusText) = when (result) { is ServerTestResult.Success -> Triple( Icons.Filled.CheckCircle, StatusSuccess, - "Connected" + connectedText ) is ServerTestResult.Failure -> Triple( Icons.Filled.Error, diff --git a/app/src/main/java/com/matedroid/ui/screens/stats/StatsScreen.kt b/app/src/main/java/com/matedroid/ui/screens/stats/StatsScreen.kt index 6bd4f08..0c181c2 100644 --- a/app/src/main/java/com/matedroid/ui/screens/stats/StatsScreen.kt +++ b/app/src/main/java/com/matedroid/ui/screens/stats/StatsScreen.kt @@ -75,8 +75,10 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import com.matedroid.R import com.matedroid.data.local.entity.DriveSummary import com.matedroid.domain.model.CarStats import com.matedroid.domain.model.DeepStats @@ -181,12 +183,12 @@ fun StatsScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Stats for Nerds") }, + title = { Text(stringResource(R.string.stats_title)) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" + contentDescription = stringResource(R.string.back) ) } }, @@ -212,8 +214,13 @@ fun StatsScreen( CircularProgressIndicator() } } else if (uiState.carStats == null) { + val emptyMessage = if (uiState.isSyncing) { + stringResource(R.string.stats_syncing) + } else { + stringResource(R.string.stats_empty) + } EmptyState( - message = if (uiState.isSyncing) "Syncing data..." else "No stats available yet.\nPull down to sync.", + message = emptyMessage, syncProgress = uiState.deepSyncProgress, syncPhase = uiState.syncProgress?.phase, isSyncing = uiState.isSyncing @@ -279,17 +286,20 @@ private fun EmptyState( ) // Show sync phase info if (isSyncing && syncPhase != null) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = when (syncPhase) { - SyncPhase.SYNCING_SUMMARIES -> "Fetching drives and charges..." - SyncPhase.SYNCING_DRIVE_DETAILS -> "Processing drive details..." - SyncPhase.SYNCING_CHARGE_DETAILS -> "Processing charge details..." - else -> "" - }, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + val phaseText = when (syncPhase) { + SyncPhase.SYNCING_SUMMARIES -> stringResource(R.string.sync_phase_summaries) + SyncPhase.SYNCING_DRIVE_DETAILS -> stringResource(R.string.sync_phase_drives) + SyncPhase.SYNCING_CHARGE_DETAILS -> stringResource(R.string.sync_phase_charges) + else -> "" + } + if (phaseText.isNotEmpty()) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = phaseText, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } // Show progress bar if we have progress if (syncProgress > 0 || isSyncing) { @@ -300,7 +310,7 @@ private fun EmptyState( modifier = Modifier.fillMaxWidth(0.6f) ) Text( - text = "${(syncProgress * 100).toInt()}% synced", + text = stringResource(R.string.stats_sync_percent, (syncProgress * 100).toInt()), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -416,7 +426,7 @@ private fun YearFilterChips( FilterChip( selected = selectedFilter is YearFilter.AllTime, onClick = { onFilterSelected(YearFilter.AllTime) }, - label = { Text("All Time") }, + label = { Text(stringResource(R.string.filter_all_time)) }, colors = FilterChipDefaults.filterChipColors( selectedContainerColor = palette.surface, selectedLabelColor = palette.onSurface @@ -474,7 +484,7 @@ private fun SyncProgressCard( Spacer(modifier = Modifier.width(12.dp)) Column(modifier = Modifier.weight(1f)) { Text( - text = "Deep Stats Sync in Progress", + text = stringResource(R.string.stats_deep_sync_title), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold ) @@ -484,7 +494,7 @@ private fun SyncProgressCard( modifier = Modifier.fillMaxWidth() ) Text( - text = "${(progress * 100).toInt()}% complete", + text = stringResource(R.string.stats_sync_complete, (progress * 100).toInt()), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSecondaryContainer ) @@ -498,18 +508,18 @@ private fun SyncProgressCard( @Composable private fun QuickStatsDrivesCard(quickStats: QuickStats, palette: CarColorPalette) { StatsCard( - title = "Drives Overview", + title = stringResource(R.string.stats_drives_overview), icon = Icons.Default.DirectionsCar, palette = palette ) { Row(modifier = Modifier.fillMaxWidth()) { StatItem( - label = "Total Drives", + label = stringResource(R.string.stats_total_drives), value = quickStats.totalDrives.toString(), modifier = Modifier.weight(1f) ) StatItem( - label = "Driving Days", + label = stringResource(R.string.stats_driving_days), value = quickStats.totalDrivingDays?.toString() ?: "-", modifier = Modifier.weight(1f) ) @@ -517,12 +527,12 @@ private fun QuickStatsDrivesCard(quickStats: QuickStats, palette: CarColorPalett Spacer(modifier = Modifier.height(12.dp)) Row(modifier = Modifier.fillMaxWidth()) { StatItem( - label = "Total Distance", + label = stringResource(R.string.total_distance), value = "%.0f km".format(quickStats.totalDistanceKm), modifier = Modifier.weight(1f) ) StatItem( - label = "Energy Used", + label = stringResource(R.string.stats_energy_used), value = formatEnergy(quickStats.totalEnergyConsumedKwh), modifier = Modifier.weight(1f) ) @@ -530,12 +540,12 @@ private fun QuickStatsDrivesCard(quickStats: QuickStats, palette: CarColorPalett Spacer(modifier = Modifier.height(12.dp)) Row(modifier = Modifier.fillMaxWidth()) { StatItem( - label = "Avg Efficiency", + label = stringResource(R.string.stats_avg_efficiency), value = "%.0f Wh/km".format(quickStats.avgEfficiencyWhKm), modifier = Modifier.weight(1f) ) StatItem( - label = "Top Speed", + label = stringResource(R.string.stats_top_speed), value = quickStats.maxSpeedKmh?.let { "$it km/h" } ?: "N/A", modifier = Modifier.weight(1f) ) @@ -546,18 +556,18 @@ private fun QuickStatsDrivesCard(quickStats: QuickStats, palette: CarColorPalett @Composable private fun QuickStatsChargesCard(quickStats: QuickStats, palette: CarColorPalette, currencySymbol: String) { StatsCard( - title = "Charges Overview", + title = stringResource(R.string.stats_charges_overview), icon = Icons.Default.BatteryChargingFull, palette = palette ) { Row(modifier = Modifier.fillMaxWidth()) { StatItem( - label = "Total Charges", + label = stringResource(R.string.stats_total_charges), value = quickStats.totalCharges.toString(), modifier = Modifier.weight(1f) ) StatItem( - label = "Energy Added", + label = stringResource(R.string.energy_added), value = formatEnergy(quickStats.totalEnergyAddedKwh), modifier = Modifier.weight(1f) ) @@ -566,12 +576,12 @@ private fun QuickStatsChargesCard(quickStats: QuickStats, palette: CarColorPalet Spacer(modifier = Modifier.height(12.dp)) Row(modifier = Modifier.fillMaxWidth()) { StatItem( - label = "Total Cost", + label = stringResource(R.string.total_cost), value = "%.2f %s".format(quickStats.totalCost, currencySymbol), modifier = Modifier.weight(1f) ) StatItem( - label = "Avg Cost/kWh", + label = stringResource(R.string.stats_avg_cost_kwh), value = quickStats.avgCostPerKwh?.let { "%.3f %s".format(it, currencySymbol) } ?: "N/A", modifier = Modifier.weight(1f) ) @@ -617,47 +627,76 @@ private fun RecordsCard( onRangeRecordClick: (MaxDistanceBetweenChargesRecord) -> Unit, onGapRecordClick: (Double, String, String, String) -> Unit // gapDays, fromDate, toDate, title ) { + // Pre-compute localized strings for use in lambdas + val labelLongestDrive = stringResource(R.string.record_longest_drive) + val labelTopSpeed = stringResource(R.string.record_top_speed) + val labelMostEfficient = stringResource(R.string.record_most_efficient) + val labelLongestStreak = stringResource(R.string.record_longest_streak) + val labelBusiestDay = stringResource(R.string.record_busiest_day) + val labelBiggestGain = stringResource(R.string.record_biggest_gain) + val labelBiggestDrain = stringResource(R.string.record_biggest_drain) + val labelBiggestCharge = stringResource(R.string.record_biggest_charge) + val labelPeakPower = stringResource(R.string.record_peak_power) + val labelMostExpensive = stringResource(R.string.record_most_expensive) + val labelPriciestKwh = stringResource(R.string.record_priciest_kwh) + val labelHighestPoint = stringResource(R.string.record_highest_point) + val labelMostClimbing = stringResource(R.string.record_most_climbing) + val labelHottestDrive = stringResource(R.string.record_hottest_drive) + val labelColdestDrive = stringResource(R.string.record_coldest_drive) + val labelHottestCharge = stringResource(R.string.record_hottest_charge) + val labelColdestCharge = stringResource(R.string.record_coldest_charge) + val labelLongestRange = stringResource(R.string.record_longest_range) + val labelNoCharging = stringResource(R.string.record_longest_no_charging) + val labelNoDriving = stringResource(R.string.record_longest_no_driving) + val labelMostDistanceDay = stringResource(R.string.record_most_distance_day) + val categoryDrives = stringResource(R.string.stats_category_drives) + val categoryBattery = stringResource(R.string.stats_category_battery) + val categoryWeather = stringResource(R.string.stats_category_weather) + val categoryDistances = stringResource(R.string.stats_category_distances) + val gapTypeCharging = stringResource(R.string.gap_type_charging) + val gapTypeDriving = stringResource(R.string.gap_type_driving) + // Category 1: Drives val driveRecords = mutableListOf() quickStats.longestDrive?.let { drive -> - driveRecords.add(RecordData("📏", "Longest Drive", "%.1f km".format(drive.distance), drive.startDate.take(10)) { onDriveClick(drive.driveId) }) + driveRecords.add(RecordData("📏", labelLongestDrive, "%.1f km".format(drive.distance), drive.startDate.take(10)) { onDriveClick(drive.driveId) }) } quickStats.fastestDrive?.let { drive -> - driveRecords.add(RecordData("🏎️", "Top Speed", "${drive.speedMax} km/h", drive.startDate.take(10)) { onDriveClick(drive.driveId) }) + driveRecords.add(RecordData("🏎️", labelTopSpeed, "${drive.speedMax} km/h", drive.startDate.take(10)) { onDriveClick(drive.driveId) }) } quickStats.mostEfficientDrive?.let { drive -> - driveRecords.add(RecordData("🌱", "Most Efficient", "%.0f Wh/km".format(drive.efficiency ?: 0.0), drive.startDate.take(10)) { onDriveClick(drive.driveId) }) + driveRecords.add(RecordData("🌱", labelMostEfficient, "%.0f Wh/km".format(drive.efficiency ?: 0.0), drive.startDate.take(10)) { onDriveClick(drive.driveId) }) } quickStats.longestDrivingStreak?.let { streak -> - driveRecords.add(RecordData("🔥", "Longest Streak", "${streak.streakDays} days", "${streak.startDate} → ${streak.endDate}", null)) + driveRecords.add(RecordData("🔥", labelLongestStreak, stringResource(R.string.format_days, streak.streakDays), "${streak.startDate} → ${streak.endDate}", null)) } quickStats.busiestDay?.let { day -> - driveRecords.add(RecordData("📅", "Busiest Day", "${day.count} drives", day.day) { onDayClick(day.day) }) + driveRecords.add(RecordData("📅", labelBusiestDay, stringResource(R.string.format_drives_count, day.count), day.day) { onDayClick(day.day) }) } // Category 2: Battery val batteryRecords = mutableListOf() quickStats.biggestBatteryGainCharge?.let { record -> - batteryRecords.add(RecordData("🔋", "Biggest Gain", "+${record.percentChange}%", "${record.startLevel}% → ${record.endLevel}%") { onChargeClick(record.recordId) }) + batteryRecords.add(RecordData("🔋", labelBiggestGain, "+${record.percentChange}%", "${record.startLevel}% → ${record.endLevel}%") { onChargeClick(record.recordId) }) } quickStats.biggestBatteryDrainDrive?.let { record -> - batteryRecords.add(RecordData("📉", "Biggest Drain", "-${record.percentChange}%", "${record.startLevel}% → ${record.endLevel}%") { onDriveClick(record.recordId) }) + batteryRecords.add(RecordData("📉", labelBiggestDrain, "-${record.percentChange}%", "${record.startLevel}% → ${record.endLevel}%") { onDriveClick(record.recordId) }) } quickStats.biggestCharge?.let { charge -> - batteryRecords.add(RecordData("⚡", "Biggest Charge", "%.0f kWh".format(charge.energyAdded), charge.startDate.take(10)) { onChargeClick(charge.chargeId) }) + batteryRecords.add(RecordData("⚡", labelBiggestCharge, "%.0f kWh".format(charge.energyAdded), charge.startDate.take(10)) { onChargeClick(charge.chargeId) }) } deepStats?.chargeWithMaxPower?.let { record -> - batteryRecords.add(RecordData("⚡", "Peak Power", "${record.powerKw} kW", record.date?.take(10) ?: "") { onChargeClick(record.chargeId) }) + batteryRecords.add(RecordData("⚡", labelPeakPower, "${record.powerKw} kW", record.date?.take(10) ?: "") { onChargeClick(record.chargeId) }) } quickStats.mostExpensiveCharge?.let { charge -> charge.cost?.let { cost -> - batteryRecords.add(RecordData("💸", "Most Expensive", "%.2f %s".format(cost, currencySymbol), charge.startDate.take(10)) { onChargeClick(charge.chargeId) }) + batteryRecords.add(RecordData("💸", labelMostExpensive, "%.2f %s".format(cost, currencySymbol), charge.startDate.take(10)) { onChargeClick(charge.chargeId) }) } } quickStats.mostExpensivePerKwhCharge?.let { charge -> charge.cost?.let { cost -> if (charge.energyAdded > 0) { - batteryRecords.add(RecordData("📈", "Priciest/kWh", "%.3f %s".format(cost / charge.energyAdded, currencySymbol), charge.startDate.take(10)) { onChargeClick(charge.chargeId) }) + batteryRecords.add(RecordData("📈", labelPriciestKwh, "%.3f %s".format(cost / charge.energyAdded, currencySymbol), charge.startDate.take(10)) { onChargeClick(charge.chargeId) }) } } } @@ -665,46 +704,46 @@ private fun RecordsCard( // Category 3: Weather & Altitude val weatherRecords = mutableListOf() deepStats?.driveWithMaxElevation?.let { record -> - weatherRecords.add(RecordData("🏔️", "Highest Point", "${record.elevationM} m", record.date?.take(10) ?: "") { onDriveClick(record.driveId) }) + weatherRecords.add(RecordData("🏔️", labelHighestPoint, "${record.elevationM} m", record.date?.take(10) ?: "") { onDriveClick(record.driveId) }) } deepStats?.driveWithMostClimbing?.let { record -> - weatherRecords.add(RecordData("⛰️", "Most Climbing", record.elevationGainM?.let { "+$it m" } ?: "N/A", record.date?.take(10) ?: "") { onDriveClick(record.driveId) }) + weatherRecords.add(RecordData("⛰️", labelMostClimbing, record.elevationGainM?.let { "+$it m" } ?: "N/A", record.date?.take(10) ?: "") { onDriveClick(record.driveId) }) } deepStats?.hottestDrive?.let { record -> - weatherRecords.add(RecordData("🌡️", "Hottest Drive", "%.1f°C".format(record.tempC), record.date?.take(10) ?: "") { onDriveClick(record.driveId) }) + weatherRecords.add(RecordData("🌡️", labelHottestDrive, "%.1f°C".format(record.tempC), record.date?.take(10) ?: "") { onDriveClick(record.driveId) }) } deepStats?.coldestDrive?.let { record -> - weatherRecords.add(RecordData("🧊", "Coldest Drive", "%.1f°C".format(record.tempC), record.date?.take(10) ?: "") { onDriveClick(record.driveId) }) + weatherRecords.add(RecordData("🧊", labelColdestDrive, "%.1f°C".format(record.tempC), record.date?.take(10) ?: "") { onDriveClick(record.driveId) }) } deepStats?.hottestCharge?.let { record -> - weatherRecords.add(RecordData("☀️", "Hottest Charge", "%.1f°C".format(record.tempC), record.date?.take(10) ?: "") { onChargeClick(record.chargeId) }) + weatherRecords.add(RecordData("☀️", labelHottestCharge, "%.1f°C".format(record.tempC), record.date?.take(10) ?: "") { onChargeClick(record.chargeId) }) } deepStats?.coldestCharge?.let { record -> - weatherRecords.add(RecordData("❄️", "Coldest Charge", "%.1f°C".format(record.tempC), record.date?.take(10) ?: "") { onChargeClick(record.chargeId) }) + weatherRecords.add(RecordData("❄️", labelColdestCharge, "%.1f°C".format(record.tempC), record.date?.take(10) ?: "") { onChargeClick(record.chargeId) }) } // Category 4: Distances & Gaps val distanceRecords = mutableListOf() quickStats.maxDistanceBetweenCharges?.let { record -> - distanceRecords.add(RecordData("🔋", "Longest Range", "%.1f km".format(record.distance), "${record.fromDate.take(10)} → ${record.toDate.take(10)}") { onRangeRecordClick(record) }) + distanceRecords.add(RecordData("🔋", labelLongestRange, "%.1f km".format(record.distance), "${record.fromDate.take(10)} → ${record.toDate.take(10)}") { onRangeRecordClick(record) }) } quickStats.longestGapWithoutCharging?.let { gap -> - distanceRecords.add(RecordData("⏰", "Longest w/o Charging", "%.1f days".format(gap.gapDays), "${gap.fromDate.take(10)} → ${gap.toDate.take(10)}") { onGapRecordClick(gap.gapDays, gap.fromDate, gap.toDate, "Without Charging") }) + distanceRecords.add(RecordData("⏰", labelNoCharging, "%.1f days".format(gap.gapDays), "${gap.fromDate.take(10)} → ${gap.toDate.take(10)}") { onGapRecordClick(gap.gapDays, gap.fromDate, gap.toDate, gapTypeCharging) }) } quickStats.longestGapWithoutDriving?.let { gap -> - distanceRecords.add(RecordData("🅿️", "Longest w/o Driving", "%.1f days".format(gap.gapDays), "${gap.fromDate.take(10)} → ${gap.toDate.take(10)}") { onGapRecordClick(gap.gapDays, gap.fromDate, gap.toDate, "Without Driving") }) + distanceRecords.add(RecordData("🅿️", labelNoDriving, "%.1f days".format(gap.gapDays), "${gap.fromDate.take(10)} → ${gap.toDate.take(10)}") { onGapRecordClick(gap.gapDays, gap.fromDate, gap.toDate, gapTypeDriving) }) } quickStats.mostDistanceDay?.let { day -> - distanceRecords.add(RecordData("🛣️", "Most Distance Day", "%.1f km".format(day.totalDistance), day.day) { onDayClick(day.day) }) + distanceRecords.add(RecordData("🛣️", labelMostDistanceDay, "%.1f km".format(day.totalDistance), day.day) { onDayClick(day.day) }) } // Build list of all categories with their records data class CategoryData(val title: String, val emoji: String, val records: List) val allCategories = mutableListOf() - if (driveRecords.isNotEmpty()) allCategories.add(CategoryData("Drives", "🚗", driveRecords)) - if (batteryRecords.isNotEmpty()) allCategories.add(CategoryData("Battery", "🔋", batteryRecords)) - if (weatherRecords.isNotEmpty()) allCategories.add(CategoryData("Weather & Altitude", "🌡️", weatherRecords)) - if (distanceRecords.isNotEmpty()) allCategories.add(CategoryData("Distances", "📍", distanceRecords)) + if (driveRecords.isNotEmpty()) allCategories.add(CategoryData(categoryDrives, "🚗", driveRecords)) + if (batteryRecords.isNotEmpty()) allCategories.add(CategoryData(categoryBattery, "🔋", batteryRecords)) + if (weatherRecords.isNotEmpty()) allCategories.add(CategoryData(categoryWeather, "🌡️", weatherRecords)) + if (distanceRecords.isNotEmpty()) allCategories.add(CategoryData(categoryDistances, "📍", distanceRecords)) // Don't render anything if no categories if (allCategories.isEmpty()) return @@ -740,7 +779,7 @@ private fun RecordsCard( ) Spacer(modifier = Modifier.width(12.dp)) Text( - text = "Records", + text = stringResource(R.string.stats_records), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = palette.onSurface @@ -897,12 +936,12 @@ private fun TemperatureStatsCard(deepStats: DeepStats, palette: CarColorPalette) } StatsCard( - title = "Temperature Extremes", + title = stringResource(R.string.stats_temperature_extremes), icon = Icons.Default.Thermostat, palette = palette ) { Text( - text = "While Driving", + text = stringResource(R.string.stats_while_driving), style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold, color = palette.onSurfaceVariant @@ -910,12 +949,12 @@ private fun TemperatureStatsCard(deepStats: DeepStats, palette: CarColorPalette) Spacer(modifier = Modifier.height(8.dp)) Row(modifier = Modifier.fillMaxWidth()) { StatItem( - label = "Hottest", + label = stringResource(R.string.stats_hottest), value = deepStats.maxOutsideTempDrivingC?.let { "%.1f°C".format(it) } ?: "N/A", modifier = Modifier.weight(1f) ) StatItem( - label = "Coldest", + label = stringResource(R.string.stats_coldest), value = deepStats.minOutsideTempDrivingC?.let { "%.1f°C".format(it) } ?: "N/A", modifier = Modifier.weight(1f) ) @@ -924,7 +963,7 @@ private fun TemperatureStatsCard(deepStats: DeepStats, palette: CarColorPalette) if (deepStats.maxCabinTempC != null || deepStats.minCabinTempC != null) { Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Cabin Temperature", + text = stringResource(R.string.stats_cabin_temperature), style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold, color = palette.onSurfaceVariant @@ -932,12 +971,12 @@ private fun TemperatureStatsCard(deepStats: DeepStats, palette: CarColorPalette) Spacer(modifier = Modifier.height(8.dp)) Row(modifier = Modifier.fillMaxWidth()) { StatItem( - label = "Hottest", + label = stringResource(R.string.stats_hottest), value = deepStats.maxCabinTempC?.let { "%.1f°C".format(it) } ?: "N/A", modifier = Modifier.weight(1f) ) StatItem( - label = "Coldest", + label = stringResource(R.string.stats_coldest), value = deepStats.minCabinTempC?.let { "%.1f°C".format(it) } ?: "N/A", modifier = Modifier.weight(1f) ) @@ -966,19 +1005,19 @@ private fun AcDcRatioCard(deepStats: DeepStats, palette: CarColorPalette) { val dcColor = Color(0xFFFFC107) // Yellow/Amber StatsCard( - title = "AC/DC Charging Ratio", + title = stringResource(R.string.stats_ac_dc_ratio), icon = Icons.Default.BatteryChargingFull, palette = palette ) { // Energy stats row Row(modifier = Modifier.fillMaxWidth()) { StatItem( - label = "AC Energy", + label = stringResource(R.string.stats_ac_energy), value = formatEnergy(deepStats.acChargeEnergyKwh), modifier = Modifier.weight(1f) ) StatItem( - label = "DC Energy", + label = stringResource(R.string.stats_dc_energy), value = formatEnergy(deepStats.dcChargeEnergyKwh), modifier = Modifier.weight(1f) ) @@ -1011,26 +1050,26 @@ private fun AcDcRatioCard(deepStats: DeepStats, palette: CarColorPalette) { ) { Column { Text( - text = "AC", + text = stringResource(R.string.charging_ac), style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.Bold, color = acColor ) Text( - text = "${deepStats.acChargeCount} charges", + text = stringResource(R.string.format_charges_count, deepStats.acChargeCount), style = MaterialTheme.typography.labelSmall, color = palette.onSurfaceVariant ) } Column(horizontalAlignment = Alignment.End) { Text( - text = "DC", + text = stringResource(R.string.charging_dc), style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.Bold, color = dcColor ) Text( - text = "${deepStats.dcChargeCount} charges", + text = stringResource(R.string.format_charges_count, deepStats.dcChargeCount), style = MaterialTheme.typography.labelSmall, color = palette.onSurfaceVariant ) @@ -1161,7 +1200,7 @@ private fun RecordCard( if (onClick != null) { Icon( imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = "View details", + contentDescription = stringResource(R.string.view_details), modifier = Modifier.size(18.dp), tint = palette.onSurfaceVariant ) @@ -1221,7 +1260,7 @@ private fun SyncLogsDialog( AlertDialog( onDismissRequest = onDismiss, title = { - Text("Sync Logs") + Text(stringResource(R.string.stats_sync_logs_title)) }, text = { Box( @@ -1246,7 +1285,7 @@ private fun SyncLogsDialog( }, confirmButton = { TextButton(onClick = onDismiss) { - Text("Close") + Text(stringResource(R.string.close)) } } ) @@ -1264,14 +1303,16 @@ private fun GapRecordDialog( palette: CarColorPalette, onDismiss: () -> Unit ) { - val emoji = if (title.contains("Charging")) "⏰" else "🅿️" + // title is now the gap type (Charging/Driving), used for determining emoji + val isCharging = title == stringResource(R.string.gap_type_charging) + val emoji = if (isCharging) "⏰" else "🅿️" AlertDialog( onDismissRequest = onDismiss, title = { Row(verticalAlignment = Alignment.CenterVertically) { Text(emoji, style = MaterialTheme.typography.titleLarge) Spacer(modifier = Modifier.width(8.dp)) - Text("Longest $title") + Text(stringResource(R.string.stats_gap_dialog_title, title)) } }, text = { @@ -1304,7 +1345,7 @@ private fun GapRecordDialog( ) { Column { Text( - text = "Started", + text = stringResource(R.string.started), style = MaterialTheme.typography.labelSmall, color = palette.onSurfaceVariant ) @@ -1317,7 +1358,7 @@ private fun GapRecordDialog( } Column(horizontalAlignment = Alignment.End) { Text( - text = "Ended", + text = stringResource(R.string.ended), style = MaterialTheme.typography.labelSmall, color = palette.onSurfaceVariant ) @@ -1334,7 +1375,7 @@ private fun GapRecordDialog( }, confirmButton = { TextButton(onClick = onDismiss) { - Text("Close") + Text(stringResource(R.string.close)) } } ) @@ -1358,7 +1399,7 @@ private fun RangeRecordDialog( Row(verticalAlignment = Alignment.CenterVertically) { Text("🔋", style = MaterialTheme.typography.titleLarge) Spacer(modifier = Modifier.width(8.dp)) - Text("Longest Range") + Text(stringResource(R.string.stats_range_record_title)) } }, text = { @@ -1378,7 +1419,7 @@ private fun RangeRecordDialog( horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = "Total Distance", + text = stringResource(R.string.total_distance), style = MaterialTheme.typography.labelMedium, color = palette.onSurfaceVariant ) @@ -1396,7 +1437,7 @@ private fun RangeRecordDialog( ) { Column { Text( - text = "From", + text = stringResource(R.string.from), style = MaterialTheme.typography.labelSmall, color = palette.onSurfaceVariant ) @@ -1408,7 +1449,7 @@ private fun RangeRecordDialog( } Column(horizontalAlignment = Alignment.End) { Text( - text = "To", + text = stringResource(R.string.to), style = MaterialTheme.typography.labelSmall, color = palette.onSurfaceVariant ) @@ -1426,7 +1467,7 @@ private fun RangeRecordDialog( // Drives header Text( - text = "Drives (${drives.size})", + text = stringResource(R.string.stats_drives_count, drives.size), style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold, color = palette.onSurfaceVariant @@ -1453,7 +1494,7 @@ private fun RangeRecordDialog( contentAlignment = Alignment.Center ) { Text( - text = "No drives found", + text = stringResource(R.string.stats_no_drives_found), style = MaterialTheme.typography.bodyMedium, color = palette.onSurfaceVariant ) @@ -1477,7 +1518,7 @@ private fun RangeRecordDialog( }, confirmButton = { TextButton(onClick = onDismiss) { - Text("Close") + Text(stringResource(R.string.close)) } } ) @@ -1542,7 +1583,7 @@ private fun DriveListItem( Spacer(modifier = Modifier.width(4.dp)) Icon( imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = "View drive", + contentDescription = stringResource(R.string.view_drive), modifier = Modifier.size(18.dp), tint = palette.onSurfaceVariant ) diff --git a/app/src/main/java/com/matedroid/ui/screens/updates/SoftwareVersionsScreen.kt b/app/src/main/java/com/matedroid/ui/screens/updates/SoftwareVersionsScreen.kt index d1534c2..35a50c3 100644 --- a/app/src/main/java/com/matedroid/ui/screens/updates/SoftwareVersionsScreen.kt +++ b/app/src/main/java/com/matedroid/ui/screens/updates/SoftwareVersionsScreen.kt @@ -51,9 +51,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import com.matedroid.R import com.matedroid.ui.components.BarChartData import com.matedroid.ui.components.InteractiveBarChart import com.matedroid.ui.theme.CarColorPalette @@ -61,10 +63,19 @@ import com.matedroid.ui.theme.CarColorPalettes import java.time.format.DateTimeFormatter import kotlin.math.roundToInt -enum class UpdatesDateFilter(val label: String, val months: Int?) { - LAST_6_MONTHS("Last 6 months", 6), - LAST_YEAR("Last year", 12), - ALL_TIME("All time", null) +enum class UpdatesDateFilter(val months: Int?) { + LAST_6_MONTHS(6), + LAST_YEAR(12), + ALL_TIME(null) +} + +@Composable +private fun getFilterLabel(filter: UpdatesDateFilter): String { + return when (filter) { + UpdatesDateFilter.LAST_6_MONTHS -> stringResource(R.string.filter_last_6_months) + UpdatesDateFilter.LAST_YEAR -> stringResource(R.string.filter_last_year) + UpdatesDateFilter.ALL_TIME -> stringResource(R.string.filter_all_time) + } } @OptIn(ExperimentalMaterial3Api::class) @@ -100,12 +111,12 @@ fun SoftwareVersionsScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Software Versions") }, + title = { Text(stringResource(R.string.software_title)) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" + contentDescription = stringResource(R.string.back) ) } }, @@ -183,7 +194,7 @@ private fun SoftwareVersionsContent( item { Spacer(modifier = Modifier.height(4.dp)) Text( - text = "Update History", + text = stringResource(R.string.software_update_history), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) @@ -204,7 +215,7 @@ private fun SoftwareVersionsContent( contentAlignment = Alignment.Center ) { Text( - text = "No software updates found", + text = stringResource(R.string.software_no_updates), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -247,7 +258,7 @@ private fun OverviewCard( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Overview", + text = stringResource(R.string.software_overview), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = palette.onSurface @@ -261,13 +272,13 @@ private fun OverviewCard( horizontalArrangement = Arrangement.SpaceEvenly ) { StatItem( - label = "Total Updates", + label = stringResource(R.string.software_total_updates), value = stats.totalUpdates.toString(), palette = palette ) StatItem( - label = "Avg. Interval", - value = "${stats.meanDaysBetweenUpdates.roundToInt()} days", + label = stringResource(R.string.software_avg_interval), + value = stringResource(R.string.format_interval_days, stats.meanDaysBetweenUpdates.roundToInt()), palette = palette ) } @@ -282,7 +293,7 @@ private fun OverviewCard( ) { Column { Text( - text = "Newest", + text = stringResource(R.string.software_newest), style = MaterialTheme.typography.labelSmall, color = palette.onSurfaceVariant ) @@ -295,7 +306,7 @@ private fun OverviewCard( } Column(horizontalAlignment = Alignment.End) { Text( - text = "Oldest", + text = stringResource(R.string.software_oldest), style = MaterialTheme.typography.labelSmall, color = palette.onSurfaceVariant ) @@ -337,6 +348,10 @@ private fun MonthlyUpdatesChart( monthlyData: List, palette: CarColorPalette ) { + // Pre-compute localized format strings + val updatesFormat = stringResource(R.string.format_updates_count) + val updatesDecimalFormat = stringResource(R.string.format_updates_decimal) + Card( modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors( @@ -357,7 +372,7 @@ private fun MonthlyUpdatesChart( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Updates per Month", + text = stringResource(R.string.software_updates_per_month), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold ) @@ -369,7 +384,7 @@ private fun MonthlyUpdatesChart( BarChartData( label = data.yearMonth.format(DateTimeFormatter.ofPattern("MMM")), value = data.count.toDouble(), - displayValue = "${data.count} updates" + displayValue = updatesFormat.format(data.count) ) } @@ -379,7 +394,7 @@ private fun MonthlyUpdatesChart( barColor = palette.accent, labelColor = palette.onSurfaceVariant, showEveryNthLabel = if (chartData.size > 6) 3 else 1, - valueFormatter = { "%.0f updates".format(it) } + valueFormatter = { updatesDecimalFormat.format(it) } ) } } @@ -398,7 +413,7 @@ private fun DateFilterChips( FilterChip( selected = filter == selectedFilter, onClick = { onFilterSelected(filter) }, - label = { Text(filter.label) }, + label = { Text(getFilterLabel(filter)) }, colors = FilterChipDefaults.filterChipColors( selectedContainerColor = palette.surface, selectedLabelColor = palette.onSurface @@ -417,6 +432,7 @@ private fun SoftwareVersionCard( val dateFormatter = DateTimeFormatter.ofPattern("MMM d, yyyy") val uriHandler = LocalUriHandler.current val releaseNotesUrl = "https://www.notateslaapp.com/software-updates/version/${update.version}/release-notes" + val unknownLabel = stringResource(R.string.unknown) Card( modifier = Modifier.fillMaxWidth(), @@ -464,7 +480,7 @@ private fun SoftwareVersionCard( ) { Icon( imageVector = Icons.AutoMirrored.Filled.OpenInNew, - contentDescription = "View release notes", + contentDescription = stringResource(R.string.software_view_release_notes), modifier = Modifier.size(16.dp), tint = if (update.isCurrent) palette.accent.copy(alpha = 0.7f) else MaterialTheme.colorScheme.primary.copy(alpha = 0.7f) ) @@ -472,7 +488,7 @@ private fun SoftwareVersionCard( } if (update.isCurrent) { Text( - text = "Current version", + text = stringResource(R.string.software_current_version), style = MaterialTheme.typography.labelSmall, color = palette.accent ) @@ -492,13 +508,13 @@ private fun SoftwareVersionCard( ) { Icon( imageVector = Icons.Default.EmojiEvents, - contentDescription = "Longest installed", + contentDescription = stringResource(R.string.software_longest_installed), modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onTertiaryContainer ) Spacer(modifier = Modifier.width(4.dp)) Text( - text = "Longest", + text = stringResource(R.string.software_longest), style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onTertiaryContainer @@ -517,12 +533,12 @@ private fun SoftwareVersionCard( // Install date Column { Text( - text = "Installed", + text = stringResource(R.string.software_installed), style = MaterialTheme.typography.labelSmall, color = if (update.isCurrent) palette.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant ) Text( - text = update.installDate?.format(dateFormatter) ?: "Unknown", + text = update.installDate?.format(dateFormatter) ?: unknownLabel, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, color = if (update.isCurrent) palette.onSurface else MaterialTheme.colorScheme.onSurface @@ -540,7 +556,7 @@ private fun SoftwareVersionCard( ) Spacer(modifier = Modifier.width(4.dp)) Text( - text = "Days", + text = stringResource(R.string.software_days), style = MaterialTheme.typography.labelSmall, color = if (update.isCurrent) palette.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant ) @@ -564,7 +580,7 @@ private fun SoftwareVersionCard( ) Spacer(modifier = Modifier.width(4.dp)) Text( - text = "Duration", + text = stringResource(R.string.software_duration), style = MaterialTheme.typography.labelSmall, color = if (update.isCurrent) palette.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml new file mode 100644 index 0000000..5380a6f --- /dev/null +++ b/app/src/main/res/values-ca/strings.xml @@ -0,0 +1,803 @@ + + + + MateDroid + + + + Cancel·la + D\'acord + + + Desconegut + + + + Selecciona vehicle + + + Cotxe %d + + + Seleccionat + + + Selecciona cotxe + + + Configuració + + + Carregant dades del vehicle… + + + No s\'han trobat vehicles + + + Assegura\'t que TeslaMate API estigui configurat correctament + + + Error en carregar les dades + + + Bloquejat + + + Desbloquejat + + + Connectat + + + Imatge del cotxe - toca per estadístiques + + + Veure estadístiques + + + Toca per estat de la bateria + + + Límit: %d%% + + + Límit: -- + + + kW + + + DC + + + AC + + + Nivell de càrrega alt + + + Ubicació + + + Altitud + + + Info del vehicle + + + Càrregues + + + Viatges + + + Quilometratge + + + Programari + + + DE + DD + TE + TD + + + + Configuració de MateDroid + + + Carregant configuració… + + + Contraure + Expandir + + + Connecta a TeslamateApi + Introdueix l\'URL del teu servidor TeslamateApi per connectar. + + + URL del servidor + https://teslamate-api.example.com + + + Configuració de xarxa avançada + + + URL del servidor secundari (opcional) + https://teslamate-api-internal.local + URL de reserva quan el servidor principal no està disponible. Útil per accés LAN/VPN. + + + Token API (opcional) + El teu token API + Deixa buit si TeslamateApi no requereix autenticació. + + + Amaga el token + Mostra el token + + + Accepta certificats no vàlids + Activa per a certificats autofirmats + Desactivar la validació de certificats fa les connexions vulnerables a atacs man-in-the-middle. Usa només en xarxes de confiança. + + + Configuració de pantalla + + + Moneda per als costos + Selecciona moneda + + + URL base de Teslamate (opcional) + https://teslamate.example.com + Usat per editar costos de càrrega directament a Teslamate. + + + Configuració extra + + + Mostra viatges / càrregues curts + Mostra viatges molt curts i càrregues mínimes a les llistes + Més informació + + + Viatges i càrregues curts + Quan està desactivat (per defecte), els següents elements estan ocults de les llistes:\n\n• Viatges de menys d\'1 minut o menys de 0,1 km\n• Càrregues de 0,1 kWh o menys\n\nAquests elements segueixen inclosos als totals, mitjanes i estadístiques. Activa aquesta configuració per veure tots els elements a les llistes. + + + Força resincronització completa + Resincronitzant… + Torna a descarregar tots els detalls de viatges i càrregues. Usa si les estadístiques semblen incorrectes. + Forçar resincronització completa? + Això + ELIMINARÀ + tots els viatges, càrregues i estadístiques emmagatzemats, després tornarà a descarregar-ho tot del servidor.\n\nEl procés pot trigar diversos minuts depenent de la quantitat de dades. + Resincronitza + + + Prova la connexió + Desa i continua + + + Vista prèvia de paletes de colors (Debug) + + + Servidor principal + Servidor secundari + Connectat + + + + Viatges + + + Enrere + + + Historial de viatges + + + No s\'han trobat viatges per al període seleccionat + + + Resum + + + Viatges totals + Distància total + Temps total + Velocitat màx + + + Distància + Durada + Bateria + + + Últims 7 dies + Últims 30 dies + Últims 90 dies + Últim any + Tot + + + Tots + Curts (< 10 km) + Excursió (10–100 km) + Viatge llarg (> 100 km) + + + Curts (< 6 mi) + Excursió (6–60 mi) + Viatge llarg (> 60 mi) + + + Viatges per dia + Viatges per setmana + Viatges per mes + + + Temps de conducció per dia + Temps de conducció per setmana + Temps de conducció per mes + + + Distància per dia + Distància per setmana + Distància per mes + + + Velocitat màx per dia + Velocitat màx per setmana + Velocitat màx per mes + + + + Detalls del viatge + + + Des de + Fins a + Ubicació desconeguda + Inici + Fi + + Durada: %s + + + Mapa de ruta + + + Velocitat + Viatge + Potència + Temperatura + + + Màxima + Mínima + Mitjana + Mitjana (distància) + Eficiència + Inici + Fi + Usada + Energia + Màx (accel) + Mín (regen) + Guany + Pèrdua + Exterior + Interior + + + Perfil de velocitat + Perfil de potència + Nivell de bateria + Perfil d\'altitud + + + Temps al llarg del trajecte + + + + Càrregues + + + Historial de càrregues + + + No s\'han trobat càrregues per al període seleccionat + + + Sessions totals + Energia total + Cost total + Cost mitjà/sessió + + + Afegida + Cost + + + Edita + + + Energia per dia + Energia per setmana + Energia per mes + + + Cost per dia + Cost per setmana + Cost per mes + + + Càrregues per dia + Càrregues per setmana + Càrregues per mes + + + + Detalls de la càrrega + + + Energia afegida + + + Lloc de càrrega + + + Carregador + + + Voltatge (màx) + Voltatge (mín) + Voltatge (mitjà) + Corrent (màx) + Corrent (mín) + Corrent (mitjà) + + + Total + Per kWh + + + Perfil de voltatge + Perfil de corrent + + + + Estat de la Bateria + + + No hi ha dades de bateria disponibles + + + Capacitat + Utilitzable (nova) + Utilitzable (ara) + Nominal + + + Capacitat de la Bateria + Mostra la capacitat d\'emmagatzematge energètic de la bateria.\n\n• Utilitzable (nova): La capacitat utilitzable original quan el cotxe era nou\n• Utilitzable (ara): Capacitat utilitzable actual després de la degradació\n• Eficiència nominal: Consum energètic per km en condicions de prova estàndard (Wh/km) + + + Degradació estimada + Pèrdua (kWh) + Pèrdua (%%) + + + Degradació Estimada + La degradació de la bateria s\'estima basant-se en dades de Teslamate.\n\n• Capacitat %%: Salut restant de la bateria comparada amb nova\n• Pèrdua (kWh): Capacitat energètica perduda per degradació\n• Pèrdua (%%): Percentatge de la capacitat original perduda\n\nNota: Això és una estimació. Tesla no proporciona dades oficials de degradació. + + + Autonomia + Autonomia màx (nova) + Autonomia màx (ara) + Pèrdua d\'autonomia + + + Detall de la Bateria + + + Càrrega Actual + + Utilitzable: %d%% + + + Càrrega Actual + Mostra l\'estat de càrrega actual de la bateria.\n\n• Bateria %%: El nivell de càrrega mostrat al tauler\n• Utilitzable %%: Càrrega realment utilitzable (Tesla reserva un petit búfer per protecció)\n\nAquesta és la càrrega actual, no la salut a llarg termini de la bateria. + + + Informació d\'Autonomia + Autonomia Estimada + Basada en les condicions de conducció actuals + Autonomia Nominal + Autonomia oficial EPA/WLTP + Autonomia Ideal + Condicions de conducció ideals + + + Informació d\'Autonomia + Diferents maneres d\'estimar quant pots conduir.\n\n• Autonomia Estimada: Basada en la teva eficiència de conducció recent i condicions actuals\n• Autonomia Nominal: Autonomia oficial de proves EPA/WLTP (la que anuncia Tesla)\n• Autonomia Ideal: Autonomia teòrica màxima en condicions perfectes (sense climatització, carretera plana, velocitat moderada) + + + Capacitat Total Estimada + Autonomia al 100%% + + Estimació basada en el %1$d%% actual i autonomia nominal de %2$.1f km + + + Capacitat Total Estimada + Projecta la teva autonomia màxima si es carrega al 100%%.\n\nCalculada extrapolant la teva autonomia nominal actual al %d%% a una càrrega completa.\n\nAixò t\'ajuda a entendre la capacitat actual de la bateria sense necessitat de carregar al 100%% (cosa no recomanada per a ús diari). + + + Entès + + + Info + + + Tanca + + + + Estadístiques Avançades + + + Sincronitzant dades… + + + No hi ha estadístiques disponibles.\nLlisca cap avall per sincronitzar. + + + Obtenint viatges i càrregues… + + + Processant detalls de viatges… + + + Processant detalls de càrregues… + + + %d%% sincronitzat + + + Sincronització profunda en curs + + + %d%% completat + + + Resum de Viatges + + + Viatges Totals + + + Dies de Conducció + + + Energia Usada + + + Eficiència Mitjana + + + Velocitat Màx + + + Resum de Càrregues + + + Càrregues Totals + + + Cost Mitjà/kWh + + + Rècords + + + Viatges + + + Bateria + + + Temps i Altitud + + + Distàncies + + + Viatge més llarg + + + Velocitat Màx + + + Més Eficient + + + Ratxa més llarga + + + Dia més actiu + + + Major guany + + + Major consum + + + Major càrrega + + + Potència màxima + + + Més car + + + Preu màx/kWh + + + Punt més alt + + + Major desnivell + + + Viatge més calorós + + + Viatge més fred + + + Càrrega més calorosa + + + Càrrega més freda + + + Autonomia màx + + + Màx sense carregar + + + Màx sense conduir + + + Dia amb més km + + + %d dies + + + %d viatges + + + Temperatures Extremes + + + Durant la Conducció + + + Més calent + + + Més fred + + + Temperatura de l\'Habitacle + + + Ràtio CA/CC + + + Energia AC + + + Energia DC + + + %d càrregues + + + Registres de Sincronització + + + Màx Sense %s + + + Carregar + + + Conduir + + + Autonomia Màxima + + + Viatges (%d) + + + No s\'han trobat viatges + + + Veure detalls + + + Veure viatge + + + + Quilometratge + + + No hi ha dades de viatges disponibles + + + No hi ha dades de viatges per al %d + + + Quilometratge per Any + + + Quilometratge per Mes + + + Quilometratge per Dia + + + %d dies + + + Viatges recents + + + Viatges + + + Total + + + Mitjana/Any + + + Mitjana/Mes + + + N° de viatges + + + Mitjana Anual + + + Basat en la teva mitjana diària des del primer viatge registrat el %1$s (fa %2$d dies).\n\nCàlcul: distància total ÷ dies × 365 + + + Info sobre el càlcul + + + + Versions de Programari + + + Últims 6 mesos + + + Historial d\'Actualitzacions + + + No s\'han trobat actualitzacions de programari + + + Resum + + + Actualitzacions Totals + + + Interval Mitjà + + + %d dies + + + Més Recent + + + Més Antic + + + Actualitzacions per Mes + + + %d actualitzacions + + + %.0f actualitzacions + + + Veure notes de la versió + + + Versió actual + + + Instal·lat més temps + + + Més llarg + + + Instal·lat + + + Dies + + + Durada + + + + Pantalla completa + + + Sortir de pantalla completa + + + + Temps al Llarg del Trajecte + + + Carregant dades meteorològiques… + + + Dades meteorològiques no disponibles + + + Hora + + + Distància + + + Temps + + + Inici + + + Final + + + Cel serè + + + Parcialment ennuvolat + + + Boirós + + + Plugim + + + Pluja + + + Neu + + + Tempesta + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml new file mode 100644 index 0000000..27dd599 --- /dev/null +++ b/app/src/main/res/values-es/strings.xml @@ -0,0 +1,803 @@ + + + + MateDroid + + + + Cancelar + OK + + + Desconocido + + + + Seleccionar vehículo + + + Coche %d + + + Seleccionado + + + Seleccionar coche + + + Ajustes + + + Cargando datos del vehículo… + + + No se encontraron vehículos + + + Asegúrate de que TeslaMate API esté configurado correctamente + + + Error al cargar datos + + + Bloqueado + + + Desbloqueado + + + Conectado + + + Imagen del coche - toca para estadísticas + + + Ver estadísticas + + + Toca para estado de batería + + + Límite: %d%% + + + Límite: -- + + + kW + + + DC + + + AC + + + Nivel de carga alto + + + Ubicación + + + Altitud + + + Info del vehículo + + + Cargas + + + Viajes + + + Kilometraje + + + Software + + + DI + DD + TI + TD + + + + Ajustes de MateDroid + + + Cargando ajustes… + + + Contraer + Expandir + + + Conectar a TeslamateApi + Introduce la URL de tu servidor TeslamateApi para conectar. + + + URL del servidor + https://teslamate-api.example.com + + + Ajustes de red avanzados + + + URL del servidor secundario (opcional) + https://teslamate-api-internal.local + URL de respaldo cuando el servidor principal no está disponible. Útil para acceso LAN/VPN. + + + Token API (opcional) + Tu token API + Deja vacío si tu TeslamateApi no requiere autenticación. + + + Ocultar token + Mostrar token + + + Aceptar certificados inválidos + Activar para certificados autofirmados + Desactivar la validación de certificados hace las conexiones vulnerables a ataques man-in-the-middle. Usa solo en redes de confianza. + + + Ajustes de pantalla + + + Moneda para costes + Seleccionar moneda + + + URL base de Teslamate (opcional) + https://teslamate.example.com + Usado para editar costes de carga directamente en Teslamate. + + + Ajustes extra + + + Mostrar viajes / cargas cortos + Mostrar viajes muy cortos y cargas mínimas en las listas + Más información + + + Viajes y cargas cortos + Cuando está desactivado (por defecto), los siguientes elementos están ocultos de las listas:\n\n• Viajes de menos de 1 minuto o menos de 0,1 km\n• Cargas de 0,1 kWh o menos\n\nEstos elementos siguen incluidos en totales, promedios y estadísticas. Activa este ajuste para ver todos los elementos en las listas. + + + Forzar resincronización completa + Resincronizando… + Vuelve a descargar todos los detalles de viajes y cargas. Usa si las estadísticas parecen incorrectas. + ¿Forzar resincronización completa? + Esto + ELIMINARÁ + todos los viajes, cargas y estadísticas en caché, luego volverá a descargar todo del servidor.\n\nEl proceso puede tardar varios minutos dependiendo de la cantidad de datos. + Resincronizar + + + Probar conexión + Guardar y continuar + + + Vista previa de paletas de colores (Debug) + + + Servidor principal + Servidor secundario + Conectado + + + + Viajes + + + Atrás + + + Historial de viajes + + + No se encontraron viajes para el período seleccionado + + + Resumen + + + Viajes totales + Distancia total + Tiempo total + Velocidad máx + + + Distancia + Duración + Batería + + + Últimos 7 días + Últimos 30 días + Últimos 90 días + Último año + Todo + + + Todos + Cortos (< 10 km) + Excursión (10–100 km) + Viaje largo (> 100 km) + + + Cortos (< 6 mi) + Excursión (6–60 mi) + Viaje largo (> 60 mi) + + + Viajes por día + Viajes por semana + Viajes por mes + + + Tiempo de conducción por día + Tiempo de conducción por semana + Tiempo de conducción por mes + + + Distancia por día + Distancia por semana + Distancia por mes + + + Velocidad máx por día + Velocidad máx por semana + Velocidad máx por mes + + + + Detalles del viaje + + + Desde + Hasta + Ubicación desconocida + Inicio + Fin + + Duración: %s + + + Mapa de ruta + + + Velocidad + Viaje + Potencia + Temperatura + + + Máxima + Mínima + Media + Media (distancia) + Eficiencia + Inicio + Fin + Usada + Energía + Máx (accel) + Mín (regen) + Ganancia + Pérdida + Exterior + Interior + + + Perfil de velocidad + Perfil de potencia + Nivel de batería + Perfil de altitud + + + Clima en el trayecto + + + + Cargas + + + Historial de cargas + + + No se encontraron cargas para el período seleccionado + + + Sesiones totales + Energía total + Coste total + Coste medio/sesión + + + Añadida + Coste + + + Editar + + + Energía por día + Energía por semana + Energía por mes + + + Coste por día + Coste por semana + Coste por mes + + + Cargas por día + Cargas por semana + Cargas por mes + + + + Detalles de carga + + + Energía añadida + + + Lugar de carga + + + Cargador + + + Voltaje (máx) + Voltaje (mín) + Voltaje (media) + Corriente (máx) + Corriente (mín) + Corriente (media) + + + Total + Por kWh + + + Perfil de voltaje + Perfil de corriente + + + + Estado de Batería + + + No hay datos de batería disponibles + + + Capacidad + Utilizable (nueva) + Utilizable (ahora) + Nominal + + + Capacidad de Batería + Muestra la capacidad de almacenamiento energético de tu batería.\n\n• Utilizable (nueva): La capacidad utilizable original cuando el coche era nuevo\n• Utilizable (ahora): Capacidad utilizable actual tras la degradación\n• Eficiencia nominal: Consumo energético por km en condiciones de prueba estándar (Wh/km) + + + Degradación estimada + Pérdida (kWh) + Pérdida (%%) + + + Degradación Estimada + La degradación de la batería se estima basándose en datos de Teslamate.\n\n• Capacidad %%: Salud restante de la batería comparada con nueva\n• Pérdida (kWh): Capacidad energética perdida por degradación\n• Pérdida (%%): Porcentaje de la capacidad original perdida\n\nNota: Esta es una estimación. Tesla no proporciona datos oficiales de degradación. + + + Autonomía + Autonomía máx (nueva) + Autonomía máx (ahora) + Pérdida de autonomía + + + Detalle de Batería + + + Carga Actual + + Utilizable: %d%% + + + Carga Actual + Muestra el estado de carga actual de tu batería.\n\n• Batería %%: El nivel de carga mostrado en el salpicadero\n• Utilizable %%: Carga realmente utilizable (Tesla reserva un pequeño búfer para protección)\n\nEsta es la carga actual, no la salud a largo plazo de la batería. + + + Información de Autonomía + Autonomía Estimada + Basada en las condiciones de conducción actuales + Autonomía Nominal + Autonomía oficial EPA/WLTP + Autonomía Ideal + Condiciones de conducción ideales + + + Información de Autonomía + Diferentes formas de estimar cuánto puedes conducir.\n\n• Autonomía Estimada: Basada en tu eficiencia de conducción reciente y condiciones actuales\n• Autonomía Nominal: Autonomía oficial de pruebas EPA/WLTP (la que anuncia Tesla)\n• Autonomía Ideal: Autonomía teórica máxima en condiciones perfectas (sin climatización, carretera llana, velocidad moderada) + + + Capacidad Total Estimada + Autonomía al 100%% + + Estimación basada en el %1$d%% actual y autonomía nominal de %2$.1f km + + + Capacidad Total Estimada + Proyecta tu autonomía máxima si se carga al 100%%.\n\nCalculada extrapolando tu autonomía nominal actual al %d%% a una carga completa.\n\nEsto te ayuda a entender la capacidad actual de tu batería sin necesidad de cargar al 100%% (lo cual no se recomienda para uso diario). + + + Entendido + + + Info + + + Cerrar + + + + Estadísticas Avanzadas + + + Sincronizando datos… + + + No hay estadísticas disponibles.\nDesliza hacia abajo para sincronizar. + + + Obteniendo viajes y cargas… + + + Procesando detalles de viajes… + + + Procesando detalles de cargas… + + + %d%% sincronizado + + + Sincronización profunda en curso + + + %d%% completado + + + Resumen de Viajes + + + Viajes Totales + + + Días de Conducción + + + Energía Usada + + + Eficiencia Media + + + Velocidad Máx + + + Resumen de Cargas + + + Cargas Totales + + + Coste Medio/kWh + + + Récords + + + Viajes + + + Batería + + + Clima y Altitud + + + Distancias + + + Viaje más largo + + + Velocidad Máx + + + Más Eficiente + + + Racha más larga + + + Día más activo + + + Mayor ganancia + + + Mayor consumo + + + Mayor carga + + + Potencia máxima + + + Más caro + + + Precio máx/kWh + + + Punto más alto + + + Mayor desnivel + + + Viaje más caluroso + + + Viaje más frío + + + Carga más calurosa + + + Carga más fría + + + Autonomía máx + + + Máx sin cargar + + + Máx sin conducir + + + Día con más km + + + %d días + + + %d viajes + + + Temperaturas Extremas + + + Durante la Conducción + + + Más caliente + + + Más frío + + + Temperatura del Habitáculo + + + Ratio CA/CC + + + Energía AC + + + Energía DC + + + %d cargas + + + Registros de Sincronización + + + Máx Sin %s + + + Cargar + + + Conducir + + + Autonomía Máxima + + + Viajes (%d) + + + No se encontraron viajes + + + Ver detalles + + + Ver viaje + + + + Kilometraje + + + No hay datos de viajes disponibles + + + No hay datos de viajes para %d + + + Kilometraje por Año + + + Kilometraje por Mes + + + Kilometraje por Día + + + %d días + + + Viajes recientes + + + Viajes + + + Total + + + Media/Año + + + Media/Mes + + + N° de viajes + + + Media Anual + + + Basado en tu media diaria desde tu primer viaje registrado el %1$s (hace %2$d días).\n\nCálculo: distancia total ÷ días × 365 + + + Info sobre el cálculo + + + + Versiones de Software + + + Últimos 6 meses + + + Historial de Actualizaciones + + + No se encontraron actualizaciones de software + + + Resumen + + + Actualizaciones Totales + + + Intervalo Medio + + + %d días + + + Más Reciente + + + Más Antiguo + + + Actualizaciones por Mes + + + %d actualizaciones + + + %.0f actualizaciones + + + Ver notas de la versión + + + Versión actual + + + Instalado más tiempo + + + Más largo + + + Instalado + + + Días + + + Duración + + + + Pantalla completa + + + Salir de pantalla completa + + + + Clima en el Trayecto + + + Cargando datos del clima… + + + Datos del clima no disponibles + + + Hora + + + Distancia + + + Clima + + + Inicio + + + Fin + + + Cielo despejado + + + Parcialmente nublado + + + Niebla + + + Llovizna + + + Lluvia + + + Nieve + + + Tormenta + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml new file mode 100644 index 0000000..dfc251b --- /dev/null +++ b/app/src/main/res/values-it/strings.xml @@ -0,0 +1,803 @@ + + + + MateDroid + + + + Annulla + OK + + + Sconosciuto + + + + Seleziona veicolo + + + Auto %d + + + Selezionato + + + Seleziona auto + + + Impostazioni + + + Caricamento dati veicolo… + + + Nessun veicolo trovato + + + Assicurati che TeslaMate API sia configurato correttamente + + + Errore nel caricamento dati + + + Bloccato + + + Sbloccato + + + Collegato + + + Immagine auto - tocca per statistiche + + + Vedi statistiche + + + Tocca per stato batteria + + + Limite: %d%% + + + Limite: -- + + + kW + + + DC + + + AC + + + Livello carica elevato + + + Posizione + + + Altitudine + + + Info veicolo + + + Ricariche + + + Viaggi + + + Chilometraggio + + + Software + + + AS + AD + PS + PD + + + + Impostazioni MateDroid + + + Caricamento impostazioni… + + + Comprimi + Espandi + + + Connetti a TeslamateApi + Inserisci l\'URL del server TeslamateApi per connetterti. + + + URL del server + https://teslamate-api.example.com + + + Impostazioni di rete avanzate + + + URL server secondario (opzionale) + https://teslamate-api-internal.local + URL di riserva quando il server primario non è raggiungibile. Utile per accesso LAN/VPN. + + + Token API (opzionale) + Il tuo token API + Lascia vuoto se TeslamateApi non richiede autenticazione. + + + Nascondi token + Mostra token + + + Accetta certificati non validi + Abilita per certificati autofirmati + Disabilitare la validazione dei certificati rende le connessioni vulnerabili ad attacchi man-in-the-middle. Usa solo su reti fidate. + + + Impostazioni schermo + + + Valuta per i costi + Seleziona valuta + + + URL base Teslamate (opzionale) + https://teslamate.example.com + Usato per modificare i costi di ricarica direttamente in Teslamate. + + + Impostazioni extra + + + Mostra viaggi / ricariche brevi + Visualizza viaggi molto brevi e ricariche minime nelle liste + Maggiori informazioni + + + Viaggi e ricariche brevi + Quando disabilitato (predefinito), i seguenti elementi sono nascosti dalle liste:\n\n• Viaggi sotto 1 minuto o meno di 0,1 km\n• Ricariche di 0,1 kWh o meno\n\nQuesti elementi sono comunque inclusi nei totali, medie e statistiche. Abilita questa impostazione per vedere tutti gli elementi nelle liste. + + + Forza risincronizzazione completa + Risincronizzazione… + Riscarica tutti i dettagli di viaggi e ricariche. Usa se le statistiche sembrano errate. + Forzare risincronizzazione completa? + Questo + ELIMINERÀ + tutti i viaggi, ricariche e statistiche memorizzati, poi riscaricherà tutto dal server.\n\nIl processo potrebbe richiedere diversi minuti a seconda della quantità di dati. + Risincronizza + + + Prova connessione + Salva e continua + + + Anteprima palette colori (Debug) + + + Server primario + Server secondario + Connesso + + + + Viaggi + + + Indietro + + + Cronologia viaggi + + + Nessun viaggio trovato per il periodo selezionato + + + Riepilogo + + + Viaggi totali + Distanza totale + Tempo totale + Velocità max + + + Distanza + Durata + Batteria + + + Ultimi 7 giorni + Ultimi 30 giorni + Ultimi 90 giorni + Ultimo anno + Tutto + + + Tutti + Brevi (< 10 km) + Gita giornaliera (10–100 km) + Viaggio lungo (> 100 km) + + + Brevi (< 6 mi) + Gita giornaliera (6–60 mi) + Viaggio lungo (> 60 mi) + + + Viaggi al giorno + Viaggi a settimana + Viaggi al mese + + + Tempo di guida al giorno + Tempo di guida a settimana + Tempo di guida al mese + + + Distanza al giorno + Distanza a settimana + Distanza al mese + + + Velocità max al giorno + Velocità max a settimana + Velocità max al mese + + + + Dettagli viaggio + + + Da + A + Posizione sconosciuta + Inizio + Fine + + Durata: %s + + + Mappa percorso + + + Velocità + Viaggio + Potenza + Temperatura + + + Massima + Minima + Media + Media (distanza) + Efficienza + Inizio + Fine + Usata + Energia + Max (accel) + Min (regen) + Guadagno + Perdita + Esterna + Interna + + + Profilo velocità + Profilo potenza + Livello batteria + Profilo altitudine + + + Meteo lungo il percorso + + + + Ricariche + + + Cronologia ricariche + + + Nessuna ricarica trovata per il periodo selezionato + + + Sessioni totali + Energia totale + Costo totale + Costo medio/sessione + + + Aggiunta + Costo + + + Modifica + + + Energia al giorno + Energia a settimana + Energia al mese + + + Costo al giorno + Costo a settimana + Costo al mese + + + Ricariche al giorno + Ricariche a settimana + Ricariche al mese + + + + Dettagli ricarica + + + Energia aggiunta + + + Luogo di ricarica + + + Caricatore + + + Tensione (max) + Tensione (min) + Tensione (media) + Corrente (max) + Corrente (min) + Corrente (media) + + + Totale + Per kWh + + + Profilo tensione + Profilo corrente + + + + Stato Batteria + + + Nessun dato batteria disponibile + + + Capacità + Utilizzabile (nuova) + Utilizzabile (ora) + Nominale + + + Capacità Batteria + Mostra la capacità di accumulo energetico della batteria.\n\n• Utilizzabile (nuova): La capacità utilizzabile originale quando l\'auto era nuova\n• Utilizzabile (ora): Capacità utilizzabile attuale dopo il degrado\n• Efficienza nominale: Consumo energetico per km in condizioni di test standard (Wh/km) + + + Degrado stimato + Perdita (kWh) + Perdita (%%) + + + Degrado Stimato + Il degrado della batteria è stimato in base ai dati di Teslamate.\n\n• Capacità %%: Salute residua della batteria rispetto al nuovo\n• Perdita (kWh): Capacità energetica persa per degrado\n• Perdita (%%): Percentuale della capacità originale persa\n\nNota: Questa è una stima. Tesla non fornisce dati ufficiali sul degrado. + + + Autonomia + Autonomia max (nuova) + Autonomia max (ora) + Perdita autonomia + + + Dettagli Batteria + + + Carica Attuale + + Utilizzabile: %d%% + + + Carica Attuale + Mostra lo stato di carica attuale della batteria.\n\n• Batteria %%: Il livello di carica mostrato sul cruscotto\n• Utilizzabile %%: Carica effettivamente utilizzabile (Tesla riserva un piccolo buffer per protezione)\n\nQuesta è la carica attuale, non la salute a lungo termine della batteria. + + + Informazioni Autonomia + Autonomia Stimata + Basata sulle condizioni di guida attuali + Autonomia Nominale + Autonomia ufficiale EPA/WLTP + Autonomia Ideale + Condizioni di guida ideali + + + Informazioni Autonomia + Diversi modi per stimare quanto puoi guidare.\n\n• Autonomia Stimata: Basata sulla tua efficienza di guida recente e condizioni attuali\n• Autonomia Nominale: Autonomia ufficiale dei test EPA/WLTP (quella pubblicizzata da Tesla)\n• Autonomia Ideale: Autonomia teorica massima in condizioni perfette (no clima, strada pianeggiante, velocità moderata) + + + Capacità Totale Stimata + Autonomia al 100%% + + Stima basata sul %1$d%% attuale e autonomia nominale di %2$.1f km + + + Capacità Totale Stimata + Proietta la tua autonomia massima se caricata al 100%%.\n\nCalcolata estrapolando la tua autonomia nominale attuale al %d%% a una carica completa.\n\nQuesto ti aiuta a capire la capacità attuale della batteria senza doverla caricare al 100%% (cosa non raccomandata per l\'uso quotidiano). + + + Capito + + + Info + + + Chiudi + + + + Statistiche Avanzate + + + Sincronizzazione dati… + + + Nessuna statistica disponibile.\nScorri verso il basso per sincronizzare. + + + Recupero viaggi e ricariche… + + + Elaborazione dettagli viaggi… + + + Elaborazione dettagli ricariche… + + + %d%% sincronizzato + + + Sincronizzazione approfondita in corso + + + %d%% completato + + + Panoramica Viaggi + + + Viaggi Totali + + + Giorni di Guida + + + Energia Usata + + + Efficienza Media + + + Velocità Max + + + Panoramica Ricariche + + + Ricariche Totali + + + Costo Medio/kWh + + + Record + + + Viaggi + + + Batteria + + + Meteo e Altitudine + + + Distanze + + + Viaggio più lungo + + + Velocità Max + + + Più Efficiente + + + Serie più lunga + + + Giorno più intenso + + + Guadagno maggiore + + + Consumo maggiore + + + Ricarica maggiore + + + Potenza massima + + + Più costoso + + + Prezzo max/kWh + + + Punto più alto + + + Maggior dislivello + + + Viaggio più caldo + + + Viaggio più freddo + + + Ricarica più calda + + + Ricarica più fredda + + + Autonomia max + + + Max senza ricaricare + + + Max senza guidare + + + Giorno più km + + + %d giorni + + + %d viaggi + + + Temperature Estreme + + + Durante la Guida + + + Più caldo + + + Più freddo + + + Temperatura Abitacolo + + + Rapporto CA/CC + + + Energia AC + + + Energia DC + + + %d ricariche + + + Log Sincronizzazione + + + Max Senza %s + + + Ricaricare + + + Guidare + + + Autonomia Massima + + + Viaggi (%d) + + + Nessun viaggio trovato + + + Vedi dettagli + + + Vedi viaggio + + + + Chilometraggio + + + Nessun dato di viaggio disponibile + + + Nessun dato di viaggio per il %d + + + Chilometraggio per Anno + + + Chilometraggio per Mese + + + Chilometraggio per Giorno + + + %d giorni + + + Viaggi recenti + + + Viaggi + + + Totale + + + Media/Anno + + + Media/Mese + + + N° di viaggi + + + Media Annuale + + + Basato sulla tua media giornaliera dal primo viaggio registrato il %1$s (%2$d giorni fa).\n\nCalcolo: distanza totale ÷ giorni × 365 + + + Info sul calcolo + + + + Versioni Software + + + Ultimi 6 mesi + + + Cronologia Aggiornamenti + + + Nessun aggiornamento software trovato + + + Panoramica + + + Aggiornamenti Totali + + + Intervallo Medio + + + %d giorni + + + Più Recente + + + Più Vecchio + + + Aggiornamenti per Mese + + + %d aggiornamenti + + + %.0f aggiornamenti + + + Vedi note di rilascio + + + Versione attuale + + + Installato più a lungo + + + Più lungo + + + Installato + + + Giorni + + + Durata + + + + Schermo intero + + + Esci da schermo intero + + + + Meteo Lungo il Percorso + + + Caricamento dati meteo… + + + Dati meteo non disponibili + + + Ora + + + Distanza + + + Meteo + + + Partenza + + + Arrivo + + + Cielo sereno + + + Parzialmente nuvoloso + + + Nebbioso + + + Pioggia leggera + + + Pioggia + + + Neve + + + Temporale + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a8f99b8..fdf0e97 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,803 @@ + MateDroid + + + + Cancel + OK + + + Unknown + + + + Select Vehicle + + + Car %d + + + Selected + + + Select car + + + Settings + + + Loading vehicle data… + + + No vehicles found + + + Make sure TeslaMate API is properly configured + + + Error loading data + + + Locked + + + Unlocked + + + Plugged in + + + Car image - tap for stats + + + View stats + + + Tap for battery health + + + Limit: %d%% + + + Limit: -- + + + kW + + + DC + + + AC + + + High charge level + + + Location + + + Elevation + + + Vehicle Info + + + Charges + + + Drives + + + Mileage + + + Software + + + FL + FR + RL + RR + + + + MateDroid Settings + + + Loading settings… + + + Collapse + Expand + + + Connect to TeslamateApi + Enter your TeslamateApi server URL to connect. + + + Server URL + https://teslamate-api.example.com + + + Advanced network settings + + + Secondary Server URL (optional) + https://teslamate-api-internal.local + Fallback URL used when the primary server is unreachable. Useful for LAN/VPN access. + + + API Token (optional) + Your API token + Leave empty if your TeslamateApi doesn\'t require authentication. + + + Hide token + Show token + + + Accept invalid certificates + Enable for self-signed certificates + Disabling certificate validation makes connections vulnerable to man-in-the-middle attacks. Only use on trusted networks. + + + Display Settings + + + Currency for costs + Select currency + + + Teslamate Base URL (optional) + https://teslamate.example.com + Used for editing charge costs directly in Teslamate. + + + Extra settings + + + Show short drives / charges + Display very short drives and minimal charges in lists + More information + + + Short drives & charges + When disabled (default), the following are hidden from the lists:\n\n• Drives under 1 minute or less than 0.1 km\n• Charges of 0.1 kWh or less\n\nThese entries are still included in totals, averages, and statistics. Enable this setting to see all entries in the lists. + + + Force Full Resync + Resyncing… + Re-downloads all drive and charge details. Use if stats seem incorrect. + Force Full Resync? + This will + DELETE + all cached drives, charges, and statistics, then re-download everything from the server.\n\nThe process may take several minutes depending on how much data you have. + Resync + + + Test Connection + Save & Continue + + + Preview Color Palettes (Debug) + + + Primary server + Secondary server + Connected + + + + Drives + + + Back + + + Drive History + + + No drives found for selected period + + + Summary + + + Total Trips + Total Distance + Total Time + Max Speed + + + Distance + Duration + Battery + + + Last 7 days + Last 30 days + Last 90 days + Last year + All time + + + All + Commute (< 10 km) + Day trip (10–100 km) + Road trip (> 100 km) + + + Commute (< 6 mi) + Day trip (6–60 mi) + Road trip (> 60 mi) + + + Drives per Day + Drives per Week + Drives per Month + + + Driving time per Day + Driving time per Week + Driving time per Month + + + Distance per Day + Distance per Week + Distance per Month + + + Top speed per Day + Top speed per Week + Top speed per Month + + + + Drive Details + + + From + To + Unknown location + Started + Ended + + Duration: %s + + + Route Map + + + Speed + Trip + Power + Temperature + + + Maximum + Minimum + Average + Avg (distance) + Efficiency + Start + End + Used + Energy + Max (accel) + Min (regen) + Gain + Loss + Outside + Inside + + + Speed Profile + Power Profile + Battery Level + Elevation Profile + + + Weather along the way + + + + Charges + + + Charge History + + + No charges found for selected period + + + Total Sessions + Total Energy + Total Cost + Avg Cost/Session + + + Added + Cost + + + Edit + + + Energy per Day + Energy per Week + Energy per Month + + + Cost per Day + Cost per Week + Cost per Month + + + Charges per Day + Charges per Week + Charges per Month + + + + Charge Details + + + Energy Added + + + Charge Location + + + Charger + + + Voltage (max) + Voltage (min) + Voltage (avg) + Current (max) + Current (min) + Current (avg) + + + Total + Per kWh + + + Voltage Profile + Current Profile + + + + Battery Health + + + No battery data available + + + Capacity + Usable (new) + Usable (now) + Rated + + + Battery Capacity + Shows your battery\'s energy storage capacity.\n\n• Usable (new): The original usable capacity when the car was new\n• Usable (now): Current usable capacity after degradation\n• Rated efficiency: Energy consumption per km under standard test conditions (Wh/km) + + + Estimated degradation + Loss (kWh) + Loss (%) + + + Estimated Degradation + Battery degradation is estimated based on Teslamate data.\n\n• Capacity %%: Remaining battery health compared to new\n• Loss (kWh): Energy capacity lost due to degradation\n• Loss (%%): Percentage of original capacity lost\n\nNote: This is an estimate. Tesla does not provide official degradation data. + + + Range + Max Range (new) + Max Range (now) + Range loss + + + Battery Detail + + + Current Charge + + Usable: %d%% + + + Current Charge + Shows your battery\'s current state of charge.\n\n• Battery %%: The charge level displayed on your dashboard\n• Usable %%: Actual usable charge (Tesla reserves a small buffer for battery protection)\n\nThis is the current charge, not the battery\'s long-term health. + + + Range Information + Estimated Range + Based on current driving conditions + Rated Range + Official EPA/WLTP range + Ideal Range + Ideal driving conditions + + + Range Information + Different ways to estimate how far you can drive.\n\n• Estimated Range: Based on your recent driving efficiency and current conditions\n• Rated Range: Official EPA/WLTP test range (what Tesla advertises)\n• Ideal Range: Maximum theoretical range under perfect conditions (no climate, flat road, moderate speed) + + + Estimated Total Capacity + Range at 100%% + + Estimate based on current %1$d%% and rated range of %2$.1f km + + + Estimated Total Capacity + Projects your maximum range if charged to 100%%.\n\nCalculated by extrapolating your current rated range at %d%% to a full charge.\n\nThis helps you understand your battery\'s current full capacity without needing to charge to 100%% (which isn\'t recommended for daily use). + + + Got it + + + Info + + + Close + + + + Stats for Nerds + + + Syncing data… + + + No stats available yet.\nPull down to sync. + + + Fetching drives and charges… + + + Processing drive details… + + + Processing charge details… + + + %d%% synced + + + Deep Stats Sync in Progress + + + %d%% complete + + + Drives Overview + + + Total Drives + + + Driving Days + + + Energy Used + + + Avg Efficiency + + + Top Speed + + + Charges Overview + + + Total Charges + + + Avg Cost/kWh + + + Records + + + Drives + + + Battery + + + Weather & Altitude + + + Distances + + + Longest Drive + + + Top Speed + + + Most Efficient + + + Longest Streak + + + Busiest Day + + + Biggest Gain + + + Biggest Drain + + + Biggest Charge + + + Peak Power + + + Most Expensive + + + Priciest/kWh + + + Highest Point + + + Most Climbing + + + Hottest Drive + + + Coldest Drive + + + Hottest Charge + + + Coldest Charge + + + Longest Range + + + Longest w/o Charging + + + Longest w/o Driving + + + Most Distance Day + + + %d days + + + %d drives + + + Temperature Extremes + + + While Driving + + + Hottest + + + Coldest + + + Cabin Temperature + + + AC/DC Charging Ratio + + + AC Energy + + + DC Energy + + + %d charges + + + Sync Logs + + + Longest Without %s + + + Charging + + + Driving + + + Longest Range + + + Drives (%d) + + + No drives found + + + View details + + + View drive + + + + Mileage + + + No drive data available + + + No drive data for %d + + + Mileage by Year + + + Mileage by Month + + + Mileage by Day + + + %d days + + + Recent trips + + + Drives + + + Total + + + Avg/Year + + + Avg/Month + + + # of drives + + + Average per Year + + + Based on your average daily driving since your first recorded drive on %1$s (%2$d days ago).\n\nCalculation: total distance ÷ days × 365 + + + Info about calculation + + + + Software Versions + + + Last 6 months + + + Update History + + + No software updates found + + + Overview + + + Total Updates + + + Avg. Interval + + + %d days + + + Newest + + + Oldest + + + Updates per Month + + + %d updates + + + %.0f updates + + + View release notes + + + Current version + + + Longest installed + + + Longest + + + Installed + + + Days + + + Duration + + + + Fullscreen + + + Exit fullscreen + + + + Weather Along the Way + + + Loading weather data… + + + Weather data unavailable + + + Time + + + Distance + + + Weather + + + Start + + + End + + + Clear sky + + + Partly cloudy + + + Foggy + + + Light rain + + + Rain + + + Snow + + + Thunderstorm diff --git a/app/src/main/res/xml/locale_config.xml b/app/src/main/res/xml/locale_config.xml new file mode 100644 index 0000000..8abddc5 --- /dev/null +++ b/app/src/main/res/xml/locale_config.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index b8b07cb..9e37d39 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -37,6 +37,74 @@ matedroid/ - **Charts**: Vico - **Maps**: osmdroid (OpenStreetMap) +### Localization (i18n) + +The app supports multiple languages using Android's standard resource-based localization system. Currently supported languages: + +- **English** (default) - `res/values/strings.xml` +- **Italian** - `res/values-it/strings.xml` +- **Spanish** - `res/values-es/strings.xml` +- **Catalan** - `res/values-ca/strings.xml` + +#### Adding/Modifying Translations + +1. **All user-visible strings must be in string resources** - never hardcode text in Kotlin files +2. **String naming convention**: Use `snake_case` (e.g., `settings_title`, `drive_history`) +3. **Add context comments** for translators above each string: + ```xml + + Select Vehicle + ``` + +#### Adding a New String + +1. Add the English string to `res/values/strings.xml`: + ```xml + + Feature Name + ``` + +2. Add translations to all locale files: + - `res/values-it/strings.xml` + - `res/values-es/strings.xml` + - `res/values-ca/strings.xml` + +3. Use in Kotlin code: + ```kotlin + import androidx.compose.ui.res.stringResource + import com.matedroid.R + + Text(text = stringResource(R.string.new_feature_label)) + ``` + +#### Format Strings + +For strings with dynamic values, use placeholders: +```xml + +Limit: %d%% + + +Since %1$s (%2$d days ago) +``` + +Usage: +```kotlin +stringResource(R.string.charge_limit_format, chargeLimit) +stringResource(R.string.avg_year_message, formattedDate, dayCount) +``` + +#### Adding a New Language + +1. Create a new folder: `res/values-{language_code}/` +2. Copy `res/values/strings.xml` to the new folder +3. Translate all strings, keeping the same `name` attributes +4. Android will automatically use the correct language based on device settings + +#### Testing Translations + +Change your device/emulator language in Settings > System > Languages to test different locales. + ### Utility Scripts #### `util/fetch_tesla_assets.py` diff --git a/gradle.properties b/gradle.properties index 3e72f62..739aa06 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ # any settings specified in this file. # Specifies the JVM arguments used for the daemon process. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. org.gradle.parallel=true