Skip to content

Commit d2bb8f8

Browse files
committed
chore: upgrade dependencies, clean warnings, and add POEditor translation guidelines
1 parent d0dd948 commit d2bb8f8

20 files changed

Lines changed: 342 additions & 189 deletions

.github/FUNDING.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#file: noinspection YAMLSchemaValidation,YAMLSchemaValidation,YAMLSchemaValidation,YAMLSchemaValidation,YAMLSchemaValidation,YAMLSchemaValidation,YAMLSchemaValidation,YAMLSchemaValidation,YAMLSchemaValidation,YAMLSchemaValidation
12
# These are supported funding model platforms
23

34
github: nichu42

CONTRIBUTING.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,18 @@ If you want to add a real linter (ktlint is the most popular choice) as a separa
146146

147147
## Translations / Localization
148148

149-
BoxViewer currently ships English (default) and German (`values-de/`) translations. New languages are welcome and much appreciated.
149+
BoxViewer uses **POEditor** to collaboratively manage app translations. We welcome contributions for correcting existing strings or translating the app into new languages.
150150

151-
To add or improve a translation:
151+
### ✍️ How to Contribute Translations
152+
1. **Join the Translation Project**: Visit our public join portal on POEditor:
153+
👉 **[Translate BoxViewer on POEditor](https://poeditor.com/join/project/3BO0G8m3BZ)**.
154+
2. **Submit Translations**: You can suggest corrections or translate untranslated strings directly on the web interface.
155+
3. **Adding a New Language**: You can suggest/request a new target language within the POEditor project portal. Once translations are complete, a developer will update the app's locale configuration and sync workflows to enable it.
156+
4. **Moderation**: All submitted translations go through a moderation queue to ensure accuracy and prevent spam before they are merged.
152157

158+
*Alternatively, if you prefer editing XML resources directly, you can submit a Pull Request following the guidelines below:*
159+
160+
### 🛠️ Manual Translation Guidelines (via XML)
153161
1. **Adding a new language:** copy `app/src/main/res/values/strings.xml` to `app/src/main/res/values-<lang>/strings.xml` (e.g., `values-fr/`, `values-es/`, `values-nl/`) and translate every string. The `<lang>` code follows Android's standard ISO 639-1 conventions. Do **not** add a new language by editing the existing `values-de/` directory.
154162
2. **Improving an existing translation:** edit the file directly in the appropriate `values-<lang>/` directory.
155163
3. **Do not translate keys, only values.** String keys (`R.string.dashboard_title`, etc.) are referenced from Kotlin code and must stay identical.

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Crafted with **Kotlin** and **Jetpack Compose** following Material Design 3 guid
3333
* **📈 Rich Telemetry Analysis**: Deep telemetry streams visualization including units, last updated timestamps, coordinates, station type, and exposure type (indoor vs. outdoor).
3434
* **🔍 Smart Discovery Engine**: Locate public senseBoxes from the openSenseMap community using direct search by name/ID, location address auto-complete, or on-demand GPS discovery.
3535
* **🌡️ Local Unit Conversion**: Per-sensor unit switching for temperature (°C/°F/K), pressure (hPa/mbar/Pa/inHg/mmHg), and wind (m/s/km/h/mph/kn) performed entirely on-device.
36-
* **🌐 Multi-Language Support**: BoxViewer currently ships in English and German and is prepared for additional languages.
36+
* **🌐 Multi-Language Support**: BoxViewer currently ships in English and German and is prepared for additional languages (see below)
3737
* **🔗 Quick Sharing & Deep Linking**: Generate local QR codes to share senseBox stations. Recipients scan the code or open a sharing web link to view the station directly inside the BoxViewer app.
3838
* **🔋 Battery & API-Friendly**: Seamless local SQLite caching (`SensorCacheEntity`) and awake-on-unlock widget refresh logic ensure you get fresh data without draining your battery or hammering openSenseMap servers.
3939
* **🔒 Privacy-First & De-Googled Friendly**: 100% free of Google Play Services (GMS) dependencies in the app code. Core functionality relies on the native Android `LocationManager` and direct openSenseMap APIs. Address search and location labels may use the device’s native geocoder (ROM-dependent backend) or an OpenStreetMap-based fallback; see the Privacy Policy for details. Zero telemetry, analytics, or third-party trackers.
@@ -129,6 +129,16 @@ BoxViewer features a built-in Air Quality Index engine that supports six major i
129129

130130
---
131131

132+
## 🌐 Translation & Localization
133+
134+
Help make BoxViewer accessible to everyone! We collaboratively translate the app using **POEditor**. You can help translate the app, submit corrections, or suggest new languages on our POEditor project page:
135+
136+
👉 **[Help Translate BoxViewer on POEditor](https://poeditor.com/join/project/3BO0G8m3BZ)**
137+
138+
For more details on translating and how the sync workflow works, check our [CONTRIBUTING.md](./CONTRIBUTING.md#translations--localization).
139+
140+
---
141+
132142
## 📜 Copyright & License Info
133143

134144
**Copyright (C) 2026 nichu42 and contributors**

app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools">
34

45
<uses-permission android:name="android.permission.INTERNET" />
56
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
@@ -13,6 +14,7 @@
1314
android:icon="@mipmap/ic_launcher"
1415
android:label="@string/app_name"
1516
android:localeConfig="@xml/locale_config"
17+
tools:ignore="UnusedAttribute"
1618
android:roundIcon="@mipmap/ic_launcher_round"
1719
android:supportsRtl="true"
1820
android:theme="@style/Theme.MyApplication">

app/src/main/java/de/nichu42/boxviewer/MainActivity.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ import de.nichu42.boxviewer.util.ApiLogger
4949
import de.nichu42.boxviewer.util.FontScaleHelper
5050
import de.nichu42.boxviewer.util.LocaleHelper
5151
import androidx.compose.ui.res.stringResource
52-
import de.nichu42.boxviewer.R
5352
import kotlinx.coroutines.flow.MutableStateFlow
5453

5554
class MainActivity : AppCompatActivity() {

app/src/main/java/de/nichu42/boxviewer/ui/AboutScreen.kt

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,63 @@ fun AboutScreen(
352352
}
353353
}
354354

355+
// Translation Collaboration Card
356+
Card(
357+
modifier = Modifier
358+
.fillMaxWidth()
359+
.testTag("about_translation_card"),
360+
shape = RoundedCornerShape(12.dp),
361+
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
362+
) {
363+
Column(
364+
modifier = Modifier
365+
.fillMaxWidth()
366+
.padding(16.dp)
367+
) {
368+
Text(
369+
text = stringResource(R.string.about_section_translation),
370+
style = MaterialTheme.typography.labelSmall,
371+
fontWeight = FontWeight.Bold,
372+
color = MaterialTheme.colorScheme.primary
373+
)
374+
Spacer(modifier = Modifier.height(10.dp))
375+
Text(
376+
text = stringResource(R.string.about_translation_description),
377+
style = MaterialTheme.typography.bodyMedium,
378+
color = MaterialTheme.colorScheme.onSurface,
379+
lineHeight = 20.sp
380+
)
381+
Spacer(modifier = Modifier.height(14.dp))
382+
Button(
383+
onClick = {
384+
try {
385+
uriHandler.openUri("https://poeditor.com/join/project/3BO0G8m3BZ")
386+
} catch (e: Exception) {
387+
e.printStackTrace()
388+
}
389+
},
390+
colors = ButtonDefaults.buttonColors(
391+
containerColor = MaterialTheme.colorScheme.primaryContainer,
392+
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
393+
),
394+
shape = RoundedCornerShape(8.dp),
395+
modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp).testTag("link_poeditor")
396+
) {
397+
Icon(
398+
imageVector = Icons.Default.Language,
399+
contentDescription = stringResource(R.string.cd_contribute_translations),
400+
modifier = Modifier.size(18.dp)
401+
)
402+
Spacer(modifier = Modifier.width(8.dp))
403+
Text(
404+
text = stringResource(R.string.about_link_translate),
405+
fontSize = 13.sp,
406+
fontWeight = FontWeight.SemiBold
407+
)
408+
}
409+
}
410+
}
411+
355412
// 3. Data & Attribution Card (openSenseMap platform info)
356413
Card(
357414
modifier = Modifier
@@ -699,7 +756,8 @@ fun LicenseScreen(
699756
onBack: () -> Unit
700757
) {
701758
val context = LocalContext.current
702-
var licenseText by remember { mutableStateOf(context.getString(R.string.about_loading_license)) }
759+
val loadingLicenseText = stringResource(R.string.about_loading_license)
760+
var licenseText by remember { mutableStateOf(loadingLicenseText) }
703761
LaunchedEffect(context, LocalConfiguration.current) {
704762
licenseText = loadLicenseText(context)
705763
}
@@ -775,7 +833,7 @@ private fun loadLicenseText(context: android.content.Context): String {
775833
reader.close()
776834
normalizeLicenseText(text)
777835
} catch (_: Exception) {
778-
context.getString(R.string.about_license_load_error)
836+
context.resources.getString(R.string.about_license_load_error)
779837
}
780838
}
781839

app/src/main/java/de/nichu42/boxviewer/ui/AddBoxConfirmScreen.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import androidx.compose.ui.unit.dp
1717
import androidx.compose.ui.unit.sp
1818
import androidx.lifecycle.compose.collectAsStateWithLifecycle
1919
import androidx.compose.ui.res.stringResource
20+
import androidx.compose.ui.platform.LocalLocale
2021
import de.nichu42.boxviewer.R
2122
import de.nichu42.boxviewer.data.api.SenseBox
2223

@@ -271,8 +272,9 @@ private fun BoxPreviewCard(
271272
Spacer(modifier = Modifier.height(4.dp))
272273
} else {
273274
box.currentLocation?.let { loc ->
274-
val latStr = String.format(java.util.Locale.getDefault(), "%.4f", loc.latitude)
275-
val lngStr = String.format(java.util.Locale.getDefault(), "%.4f", loc.longitude)
275+
val locale = LocalLocale.current.platformLocale
276+
val latStr = String.format(locale, "%.4f", loc.latitude)
277+
val lngStr = String.format(locale, "%.4f", loc.longitude)
276278
Text(
277279
text = "Coords: $latStr, $lngStr",
278280
style = MaterialTheme.typography.bodySmall,

app/src/main/java/de/nichu42/boxviewer/ui/ApiLogViewerScreen.kt

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ fun ApiLogViewerScreen(
5353
val clipboard = LocalClipboard.current
5454
val scope = rememberCoroutineScope()
5555

56+
val logsClearedMsg = stringResource(R.string.api_log_logs_cleared)
57+
val urlCopiedMsg = stringResource(R.string.api_log_url_copied)
58+
val responseCopiedMsg = stringResource(R.string.api_log_response_copied)
59+
val entryCopiedMsg = stringResource(R.string.api_log_entry_copied)
60+
5661
var diagnostics by remember { mutableStateOf<Map<String, Any>?>(null) }
5762
var logEntries by remember { mutableStateOf<List<ApiLogger.ApiLogEntry>>(emptyList()) }
5863
var isLoading by remember { mutableStateOf(true) }
@@ -137,7 +142,7 @@ fun ApiLogViewerScreen(
137142
ApiLogger.clearLogs()
138143
logEntries = emptyList()
139144
diagnostics = null
140-
Toast.makeText(ctx, ctx.getString(R.string.api_log_logs_cleared), Toast.LENGTH_SHORT).show()
145+
Toast.makeText(ctx, logsClearedMsg, Toast.LENGTH_SHORT).show()
141146
},
142147
modifier = Modifier.testTag("api_log_viewer_clear")
143148
) {
@@ -249,19 +254,19 @@ fun ApiLogViewerScreen(
249254
onCopyUrl = {
250255
scope.launch {
251256
clipboard.setClipEntry(ClipData.newPlainText("API Request URL", it).toClipEntry())
252-
Toast.makeText(ctx, ctx.getString(R.string.api_log_url_copied), Toast.LENGTH_SHORT).show()
257+
Toast.makeText(ctx, urlCopiedMsg, Toast.LENGTH_SHORT).show()
253258
}
254259
},
255260
onCopyResponse = {
256261
scope.launch {
257262
clipboard.setClipEntry(ClipData.newPlainText("API Response", it).toClipEntry())
258-
Toast.makeText(ctx, ctx.getString(R.string.api_log_response_copied), Toast.LENGTH_SHORT).show()
263+
Toast.makeText(ctx, responseCopiedMsg, Toast.LENGTH_SHORT).show()
259264
}
260265
},
261266
onCopyAll = {
262267
scope.launch {
263268
clipboard.setClipEntry(ClipData.newPlainText("API Log Item", it).toClipEntry())
264-
Toast.makeText(ctx, ctx.getString(R.string.api_log_entry_copied), Toast.LENGTH_SHORT).show()
269+
Toast.makeText(ctx, entryCopiedMsg, Toast.LENGTH_SHORT).show()
265270
}
266271
}
267272
)
@@ -277,7 +282,6 @@ fun DiagnosticsCollapsiblePanel(
277282
isExpanded: Boolean,
278283
onToggle: () -> Unit
279284
) {
280-
val ctx = LocalContext.current
281285
Card(
282286
modifier = Modifier.fillMaxWidth(),
283287
shape = RoundedCornerShape(12.dp),
@@ -324,10 +328,11 @@ fun DiagnosticsCollapsiblePanel(
324328
.fillMaxWidth(),
325329
verticalArrangement = Arrangement.spacedBy(4.dp)
326330
) {
327-
val appVer = diagnostics["appVersion"] as? String ?: ctx.getString(R.string.time_unknown)
328-
val device = diagnostics["device"] as? String ?: ctx.getString(R.string.time_unknown)
329-
val sdk = diagnostics["androidSdk"]?.toString() ?: ctx.getString(R.string.time_unknown)
330-
val date = diagnostics["date"] as? String ?: ctx.getString(R.string.time_unknown)
331+
val timeUnknown = stringResource(R.string.time_unknown)
332+
val appVer = diagnostics["appVersion"] as? String ?: timeUnknown
333+
val device = diagnostics["device"] as? String ?: timeUnknown
334+
val sdk = diagnostics["androidSdk"]?.toString() ?: timeUnknown
335+
val date = diagnostics["date"] as? String ?: timeUnknown
331336

332337
DiagnosticsRow(label = stringResource(R.string.api_log_label_app_version), value = appVer)
333338
DiagnosticsRow(label = stringResource(R.string.api_log_label_device_info), value = device)
@@ -751,7 +756,6 @@ fun ApiLogEntryRow(
751756
entry: ApiLogger.ApiLogEntry,
752757
onClick: () -> Unit
753758
) {
754-
val ctx = LocalContext.current
755759
val methodColor = when (entry.method.uppercase()) {
756760
"GET" -> Color(0xFF4CAF50)
757761
"POST" -> Color(0xFF2196F3)

0 commit comments

Comments
 (0)