Bimba.git

commit c1db5ac8fc4a50eb6c9f311c14b04c65a2654a24

Author: Adam Evyčędo <git@apiote.xyz>

merge develop into master for version 3.5.0

%!v(PANIC=String method: strings: negative Repeat count)


diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index 2414a71faf01bde622bc638aa8c23247cee9e6ae..29474db0c178e8e7fe407d7c4101c311eed2dece 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -14,6 +14,20 @@
 * Travel planning
 * Offline timetable
 
+== [3.4] – 2024-07-03
+
+=== Added
+
+* geocoding short OLCs
+* arrows directing to nearest stops
+* unit systems
+
+== [3.4] – 2024-07-03
+
+=== Added
+
+* favourites
+
 == [3.3.2] – 2024-05-22
 
 === Changed




diff --git a/README.adoc b/README.adoc
index f89c19fe5a0c108193f3166443f30c00901f5f57..b98ef0e65cef40da413c6bf0b9a81ea1a0c955c3 100644
--- a/README.adoc
+++ b/README.adoc
@@ -4,7 +4,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later
 
 = Bimba
 Adam Evyčędo <me@apiote.xyz>
-v3.4.0 2024-07-03
+v3.5.0 2024-07-24
 :toc:
 
 Bimba is a FLOSS public transport passenger companion; a timetable in your pocket.




diff --git a/app/build.gradle b/app/build.gradle
index 54d50b5a32b00c98bde44d662ce7e94bfbe0df97..3a3a2355b4f3fd3ae0f7a7f826c94e101701ae5f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -16,17 +16,17 @@     id "com.google.protobuf" version "0.9.4"
 }
 
 android {
-    compileSdk 34
+    compileSdk 34  // https://gitlab.com/fdroid/fdroiddata/-/issues/3299#note_1989808414
 
     defaultConfig {
         applicationId "xyz.apiote.bimba.czwek"
         minSdk 21
         targetSdk 35
-        versionCode 28
-        versionName "3.4.0"
+        versionCode 29
+        versionName "3.5.0"
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
-        resourceConfigurations += ["en", "pl", "it", "de", "fr"]
+        resourceConfigurations += ["en", "pl", "it", "de", "fr", "en-rUS"]
     }
 
     applicationVariants.configureEach { variant ->
@@ -49,11 +49,7 @@     buildFeatures {
         viewBinding true
     }
     namespace 'xyz.apiote.bimba.czwek'
-    buildToolsVersion = '34.0.0'
-
-    sourceSets {
-        main.java.srcDirs += 'src/main/proto'
-    }
+    buildToolsVersion = '34.0.0'  // https://gitlab.com/fdroid/fdroiddata/-/issues/3299#note_1989808414
 }
 
 dependencies {
@@ -76,6 +72,11 @@     implementation 'com.otaliastudios:zoomlayout:1.9.0'
     implementation 'dev.bandb.graphview:graphview:0.8.1'
     implementation 'org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3'
     implementation 'com.github.jershell:kbson:0.5.0'
+    implementation 'androidx.preference:preference-ktx:1.2.1'
+    implementation 'androidx.work:work-runtime-ktx:2.9.0'
+    implementation 'com.github.doyaaaaaken:kotlin-csv-jvm:1.9.3'
+    implementation 'commons-io:commons-io:2.16.1'
+
 
     implementation project(path: ':fruchtfleisch')
 




diff --git a/app/src/androidTest/java/xyz/apiote/bimba/czwek/units/TGMKtTest.kt b/app/src/androidTest/java/xyz/apiote/bimba/czwek/units/TGMKtTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8699c205595e80d6dca7922994585004ad8f3a31
--- /dev/null
+++ b/app/src/androidTest/java/xyz/apiote/bimba/czwek/units/TGMKtTest.kt
@@ -0,0 +1,55 @@
+package xyz.apiote.bimba.czwek.units
+
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Test
+
+class TGMKtTest {
+	@Test
+	fun toDozenalString0(){
+		val i = 0
+		val context = InstrumentationRegistry.getInstrumentation().targetContext
+		assert(i.toDozenalString(context) == "zero") { "got ${i.toDozenalString(context)}, wanted zero" }
+	}
+
+	@Test
+	fun toDozenalString1(){
+		val i = 1
+		val context = InstrumentationRegistry.getInstrumentation().targetContext
+		assert(i.toDozenalString(context) == "one") { "got ${i.toDozenalString(context)}, wanted one" }
+	}
+
+	@Test
+	fun toDozenalString10(){
+		val i = 12
+		val context = InstrumentationRegistry.getInstrumentation().targetContext
+		assert(i.toDozenalString(context) == "one zen") { "got ${i.toDozenalString(context)}, wanted one zen" }
+	}
+
+	@Test
+	fun toDozenalString100(){
+		val i = 144
+		val context = InstrumentationRegistry.getInstrumentation().targetContext
+		assert(i.toDozenalString(context) == "one duna") { "got ${i.toDozenalString(context)}, wanted one duna" }
+	}
+
+	@Test
+	fun toDozenalString23(){
+		val i = 27
+		val context = InstrumentationRegistry.getInstrumentation().targetContext
+		assert(i.toDozenalString(context) == "two zen three") { "got ${i.toDozenalString(context)}, wanted two zen three" }
+	}
+
+	@Test
+	fun toDozenalString234(){
+		val i = 328
+		val context = InstrumentationRegistry.getInstrumentation().targetContext
+		assert(i.toDozenalString(context) == "two duna three zen four") { "got ${i.toDozenalString(context)}, wanted two duna three zen four" }
+	}
+
+	@Test
+	fun toDozenalString204(){
+		val i = 292
+		val context = InstrumentationRegistry.getInstrumentation().targetContext
+		assert(i.toDozenalString(context) == "two duna four") { "got ${i.toDozenalString(context)}, wanted two duna four" }
+	}
+}
\ No newline at end of file




diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f7c0314f0b78f5e5a72bccc33f8d7b940088a0d9..df18dfb966f5fd8c37c0893fa264b05f1a37ef6b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,11 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
-<!--
-SPDX-FileCopyrightText: Adam Evyčędo
-
-SPDX-License-Identifier: GPL-3.0-or-later
--->
-
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:tool="http://schemas.android.com/tools">
 
@@ -13,6 +6,7 @@ 	
 	<uses-permission android:name="android.permission.INTERNET" />
 	<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
 	<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+	<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
 
 	<application
 		android:name=".Bimba"
@@ -27,6 +21,10 @@ 		android:roundIcon="@mipmap/ic_launcher_round"
 		android:supportsRtl="true"
 		android:theme="@style/Theme.Bimba.Style"
 		tool:targetApi="33">
+		<activity
+			android:name=".settings.SettingsActivity"
+			android:exported="false"
+			android:label="@string/title_settings" />
 		<activity
 			android:name=".AboutActivity"
 			android:exported="false" />




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt
index b2a20c6ca37251d8b153b0789dbfa920fa015eb3..5b7701049711808d7606ecf398e3d60a65f685aa 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt
@@ -7,15 +7,16 @@
 import android.Manifest
 import android.content.Intent
 import android.content.pm.PackageManager
+import android.os.Build
 import android.os.Bundle
 import android.view.View
-import android.widget.Toast
 import androidx.activity.OnBackPressedCallback
 import androidx.activity.enableEdgeToEdge
 import androidx.activity.result.ActivityResultLauncher
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.appcompat.app.AppCompatActivity
 import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.app.ActivityCompat
 import androidx.core.content.ContextCompat
 import androidx.core.content.edit
 import androidx.core.view.ViewCompat
@@ -28,6 +29,7 @@ import androidx.fragment.app.FragmentManager
 import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks
 import androidx.navigation.fragment.NavHostFragment
 import androidx.navigation.ui.setupWithNavController
+import androidx.preference.PreferenceManager
 import com.google.android.material.bottomnavigation.BottomNavigationView
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.google.openlocationcode.OpenLocationCode
@@ -39,6 +41,7 @@ import xyz.apiote.bimba.czwek.dashboard.ui.voyage.VoyageFragment
 import xyz.apiote.bimba.czwek.databinding.ActivityMainBinding
 import xyz.apiote.bimba.czwek.search.ResultsActivity
 import xyz.apiote.bimba.czwek.settings.ServerChooserActivity
+import xyz.apiote.bimba.czwek.settings.SettingsActivity
 import xyz.apiote.bimba.czwek.settings.feeds.FeedChooserActivity
 
 
@@ -109,6 +112,10 @@ 				R.id.drawer_cities -> {
 					startActivity(Intent(this, FeedChooserActivity::class.java))
 				}
 
+				R.id.drawer_settings -> {
+					startActivity(Intent(this, SettingsActivity::class.java))
+				}
+
 				R.id.drawer_about -> {
 					startActivity(Intent(this, AboutActivity::class.java))
 				}
@@ -151,12 +158,22 @@ 						)
 					)
 						.setTitle(getString(R.string.no_location_access))
 						.setMessage(getString(R.string.no_location_message))
-						.setPositiveButton(R.string.ok) { _, _ ->}
+						.setPositiveButton(R.string.ok) { _, _ -> }
 						.show()
 					locationPermissionDialogShown = true
 				}
 			}
 		}
+
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+			if (ActivityCompat.checkSelfPermission(
+					this,
+					Manifest.permission.POST_NOTIFICATIONS
+				) != PackageManager.PERMISSION_GRANTED && shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
+			) {
+				requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1)
+			}
+		}
 	}
 
 	fun onNavigationClicked() {
@@ -200,11 +217,36 @@ 	fun onSearchClicked(text: CharSequence?) {
 		if (OpenLocationCode.isValidCode(text.toString())) {
 			val olc = OpenLocationCode(text.toString())
 			if (!olc.isFull) {
-				Toast.makeText(this, getString(R.string.code_is_not_full), Toast.LENGTH_LONG).show()
-				return
+				showResults(ResultsActivity.Mode.MODE_SHORT_CODE_LOCATION, text.toString())
+			} else {
+				val area = olc.decode()
+				showResults(olc.code, area.centerLatitude, area.centerLongitude)
 			}
-			val area = olc.decode()
-			showResults(olc.code, area.centerLatitude, area.centerLongitude)
+		} else if (OpenLocationCode.isValidCode(
+				text.toString().trim().split(" ").first().trim(',').trim()
+			)
+		) {
+			if (PreferenceManager.getDefaultSharedPreferences(applicationContext)
+					.getLong("cities_last_update", -1) < 0
+			) {
+				if (!PreferenceManager.getDefaultSharedPreferences(applicationContext)
+						.getBoolean("no_geocoding_data_shown", false)
+				) {
+					MaterialAlertDialogBuilder(this)
+						.setIcon(R.drawable.geocoding)
+						.setTitle(R.string.no_geocoding_data)
+						.setMessage(R.string.no_geocoding_data_description)
+						.setPositiveButton(R.string.ok) { _, _ ->
+							showResults(
+								ResultsActivity.Mode.MODE_SEARCH,
+								text.toString()
+							)
+						}
+						.show()
+				}
+			} else {
+				showResults(ResultsActivity.Mode.MODE_SHORT_CODE, text.toString())
+			}
 		} else {
 			showResults(ResultsActivity.Mode.MODE_SEARCH, text.toString())
 		}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/Favourites.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/Favourites.kt
index b3cb8722547c815be4076bc24615a101aba0a8f6..711016241320500c160eac7dd14196cd1b974411 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/Favourites.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/Favourites.kt
@@ -177,7 +177,7 @@ 			} else if (departure.isEmpty) {
 				holder.feedName.text = favourite.feedName
 				holder.stopHeadline.text = favourite.stopName
 				holder.lineIcon.setImageDrawable(null)
-				holder.lineName.text = context.getString(R.string.no_next_departures)
+				holder.lineName.text = context.getString(R.string.no_departures).lowercase()
 				holder.departureTime.text = ""
 				holder.departureTimeFull.text = ""
 				holder.headsign.text = ""




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt
index d3d3c1afc89244763f309441eb5c24275862d30d..3d6730f9a369a48fef475076e0746fb39a9b5dcc 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt
@@ -29,7 +29,6 @@ import xyz.apiote.bimba.czwek.databinding.FragmentHomeBinding
 import xyz.apiote.bimba.czwek.dpToPixelI
 import xyz.apiote.bimba.czwek.repo.Favourite
 import xyz.apiote.bimba.czwek.search.BimbaResultsAdapter
-import xyz.apiote.bimba.czwek.units.Millisecond
 import xyz.apiote.bimba.czwek.units.Second
 
 class HomeFragment : Fragment() {
@@ -41,7 +40,7 @@ 	private lateinit var favouritesAdapter: BimbaFavouritesAdapter
 	private lateinit var viewModel: HomeViewModel
 
 	private val countdown =
-		object : CountDownTimer(Millisecond(Second(30)).millis, Millisecond(Second(10)).millis) {
+		object : CountDownTimer(Second(30).milliseconds(), Second(10).milliseconds()) {
 			override fun onTick(millisUntilFinished: Long) {
 			}
 
@@ -62,7 +61,7 @@ 			ViewModelProvider(this)[HomeViewModel::class.java]
 		viewModel.queryables.observe(viewLifecycleOwner) {
 			adapter.feedsSettings = viewModel.feedsSettings
 			adapter.feeds = viewModel.feeds
-			adapter.update(it)
+			adapter.update(it, null, false)
 		}
 		viewModel.favourites.observe(viewLifecycleOwner) {
 			favouritesAdapter.updateFavourites(it)
@@ -90,7 +89,7 @@ 				TransitionState.HIDDEN -> false
 				else -> false
 			}
 			if (newState === TransitionState.HIDING) {
-				adapter.update(listOf())
+				adapter.update(listOf(), null, false)
 			}
 		}
 
@@ -98,7 +97,7 @@ 		binding.searchBar.setNavigationOnClickListener {
 			(context as MainActivity).onNavigationClicked()
 		}
 		binding.suggestionsRecycler.layoutManager = LinearLayoutManager(activity)
-		adapter = BimbaResultsAdapter(layoutInflater, activity, listOf())
+		adapter = BimbaResultsAdapter(layoutInflater, activity, listOf(), null, null, false)
 		binding.suggestionsRecycler.adapter = adapter
 
 		binding.searchView.editText.addTextChangedListener(




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt
index f53541d8f75292860a260fd389f57fe047e1bf32..324169dea1b496d9daa3162b5c04dc6281fd1909 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt
@@ -35,6 +35,8 @@ import xyz.apiote.bimba.czwek.repo.Position
 import xyz.apiote.bimba.czwek.repo.Stop
 import xyz.apiote.bimba.czwek.repo.TrafficResponseException
 import xyz.apiote.bimba.czwek.repo.Vehicle
+import xyz.apiote.bimba.czwek.units.UnitSystem
+
 
 class MapViewModel : ViewModel() {
 
@@ -79,9 +81,15 @@ 			content.findViewById(R.id.rt_icon).visibility = View.GONE
 			// TODO vehicle accessible
 			content.findViewById<ImageView>(R.id.wheelchair_icon).visibility = View.GONE
 
-			// todo units -- [3.2] settings or system-based
-			content.findViewById<TextView>(R.id.speed_text).text =
-				ctx.getString(R.string.speed_in_km_per_h, vehicle.Speed * 3.6)
+			Log.i("unit", "${vehicle.Speed.mps}")
+			UnitSystem.getSelected(requireContext()).let { us ->
+				content.findViewById<TextView>(R.id.speed_text).apply {
+					text =
+						us.toString(requireContext(), us.speedUnit(vehicle.Speed))
+					contentDescription =
+						us.speedUnit(vehicle.Speed).contentDescription(requireContext(), us.base)
+				}
+			}
 
 			content.findViewById<LinearLayout>(R.id.congestion).visibility =
 				if (vehicle.congestionLevel == CongestionLevel.UNKNOWN) View.GONE else View.VISIBLE




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt
index b9b2871dd953aab86b88c501729cc02801e8f705..bd20eaf7e474954a96ea993491dddebf413e1fb9 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt
@@ -41,6 +41,7 @@ import xyz.apiote.bimba.czwek.repo.Departure
 import xyz.apiote.bimba.czwek.repo.DepartureItem
 import xyz.apiote.bimba.czwek.repo.OccupancyStatus
 import xyz.apiote.bimba.czwek.repo.Vehicle
+import xyz.apiote.bimba.czwek.units.UnitSystem
 import java.time.ZoneId
 import java.time.ZonedDateTime
 
@@ -91,7 +92,7 @@ 			context: Context?
 		) {
 			val alertDescriptions = alerts.map { it.description }.filter { it != "" }
 				.joinToString(separator = "\n")
-			holder?.moreButton?.setOnClickListener{
+			holder?.moreButton?.setOnClickListener {
 				MaterialAlertDialogBuilder(context!!)
 					.setTitle("Alerts")
 					.setPositiveButton(R.string.ok) { _, _ -> }
@@ -303,9 +304,14 @@ 				)
 			}
 
 			findViewById<TextView>(R.id.boarding_text).text = departure.boardingText(ctx)
-			// todo units -- [3.2] settings or system-based
-			findViewById<TextView>(R.id.speed_text).text =
-				getString(R.string.speed_in_km_per_h, departure.vehicle.Speed * 3.6)
+			UnitSystem.getSelected(requireContext()).let { us ->
+				findViewById<TextView>(R.id.speed_text).apply {
+					text =
+						us.toString(context, us.speedUnit(departure.vehicle.Speed))
+					contentDescription =
+						us.speedUnit(departure.vehicle.Speed).contentDescription(requireContext(), us.base)
+				}
+			}
 
 			findViewById<LinearLayout>(R.id.congestion).visibility =
 				if (departure.vehicle.congestionLevel == CongestionLevel.UNKNOWN) View.GONE else View.VISIBLE




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt
index 783606925186535e8a5ba30761e3b1ae932b6b2d..1164836fb36f512ff578e26a75e9c1ac04013cac 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt
@@ -44,7 +44,6 @@ import xyz.apiote.bimba.czwek.repo.DepartureItem
 import xyz.apiote.bimba.czwek.repo.Favourite
 import xyz.apiote.bimba.czwek.repo.OfflineRepository
 import xyz.apiote.bimba.czwek.repo.Stop
-import xyz.apiote.bimba.czwek.units.Millisecond
 import xyz.apiote.bimba.czwek.units.Second
 import xyz.apiote.bimba.czwek.units.Tim
 import java.time.Instant
@@ -73,10 +72,10 @@ 	private val linesFilterTemporary = mutableMapOf()
 
 	// TODO [elizabeth] millisInFuture from header Cache-Control max-age
 	private val countdown =
-		object : CountDownTimer(Millisecond(Second(30)).millis, Millisecond(Tim(1)).millis) {
+		object : CountDownTimer(Second(30).milliseconds(), Tim(1).milliseconds()) {
 			override fun onTick(millisUntilFinished: Long) {
-				val timsUntillFinished = Tim(Millisecond(millisUntilFinished))
-				binding.departuresUpdatesProgress.progress = timsUntillFinished.tims
+				val timsUntillFinished = Tim(Second(millisUntilFinished.toDouble()/1000))
+				binding.departuresUpdatesProgress.progress = timsUntillFinished.tims.toInt()
 			}
 
 			override fun onFinish() {
@@ -233,7 +232,7 @@ 								R.drawable.filter
 							)
 						)
 							.setTitle("Filtered departures")
-							.setMessage("Do you want to save a favourite filtered with selected lines?")
+							.setMessage(R.string.filtered_stop_question)
 							.setPositiveButton(R.string.filtered) { _, _ ->
 								saveFavourite(viewModel.linesFilter.value!!.keys)
 							}
@@ -405,7 +404,7 @@ 		val lastUpdateAgo = ZonedDateTime.now().toEpochSecond() - adapter.lastUpdate.toEpochSecond()
 		if (lastUpdateAgo > 59 && adapter.lastUpdate.year != 0) {
 			snackbar.setText(
 				getString(
-					R.string.departures_snackbar,
+					R.string.last_update,
 					DateUtils.getRelativeTimeSpanString(
 						adapter.lastUpdate.toEpochSecond() * 1000,
 						ZonedDateTime.now().toEpochSecond() * 1000,
@@ -454,8 +453,8 @@ 		// TODO [elizabeth] max, progress from header Cache-Control max-age
 		binding.departuresUpdatesProgress.apply {
 			visibility = View.VISIBLE
 			isIndeterminate = false
-			max = Tim(Second(30)).tims
-			progress = Tim(Second(30)).tims
+			max = Tim(Second(30)).tims.toInt()
+			progress = Tim(Second(30)).tims.toInt()
 		}
 		countdown.cancel()
 		countdown.start()




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/FirstRunActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/FirstRunActivity.kt
index 8f29cb95067119e2ee24b1688138d347ad55b74b..117278a8722bd5277e13416aeb8abdeea2ea8c24 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/FirstRunActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/FirstRunActivity.kt
@@ -4,13 +4,23 @@ // SPDX-License-Identifier: GPL-3.0-or-later
 
 package xyz.apiote.bimba.czwek.onboarding
 
+import android.app.NotificationChannel
+import android.app.NotificationManager
 import android.content.Intent
+import android.os.Build
 import android.os.Bundle
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.preference.PreferenceManager
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkManager
+import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.dashboard.MainActivity
 import xyz.apiote.bimba.czwek.repo.migrateDB
+import xyz.apiote.bimba.czwek.settings.DownloadCitiesWorker
 import xyz.apiote.bimba.czwek.settings.feeds.migrateFeedsSettings
+import java.time.Instant
+import java.time.temporal.ChronoUnit
 
 class FirstRunActivity : AppCompatActivity() {
 	override fun onCreate(savedInstanceState: Bundle?) {
@@ -21,6 +31,20 @@ 		val preferences = getSharedPreferences("shp", MODE_PRIVATE)
 
 		migrateFeedsSettings(this)
 		migrateDB(this)
+		createNotificationChannels()
+
+		val (updatesEnabled, weekPassed) = PreferenceManager.getDefaultSharedPreferences(this).let {
+			arrayOf(
+				it.getBoolean("autoupdate_cities_list", false),
+				Instant.ofEpochSecond(it.getLong("cities_last_update", 0)).plus(7, ChronoUnit.DAYS)
+					.isBefore(Instant.now())
+			)
+		}
+
+		if (updatesEnabled && weekPassed) {
+			WorkManager.getInstance(this)
+				.enqueue(OneTimeWorkRequest.from(DownloadCitiesWorker::class.java))
+		}
 
 		val intent = if (preferences.getBoolean("firstRun", true)) {
 			Intent(this, OnboardingActivity::class.java)
@@ -31,4 +55,17 @@ 		startActivity(intent)
 		finish()
 	}
 
+	private fun createNotificationChannels() {
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+			val name = getString(R.string.cities_channel_name)
+			val descriptionText = getString(R.string.cities_channel_description)
+			val importance = NotificationManager.IMPORTANCE_LOW
+			val channel = NotificationChannel("cities_channel", name, importance).apply {
+				description = descriptionText
+			}
+			val notificationManager: NotificationManager =
+				getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+			notificationManager.createNotificationChannel(channel)
+		}
+	}
 }
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt
index e90ce512e3b7eba9e635c9514268ee246ddc9db8..b3459fda1568096322caab7e16e7dace0d9b87a6 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt
@@ -16,6 +16,9 @@ import xyz.apiote.bimba.czwek.api.DepartureV3
 import xyz.apiote.bimba.czwek.api.DepartureV4
 import xyz.apiote.bimba.czwek.api.Time
 import xyz.apiote.bimba.czwek.api.UnknownResourceVersionException
+import xyz.apiote.bimba.czwek.units.Second
+import xyz.apiote.bimba.czwek.units.TGM
+import xyz.apiote.bimba.czwek.units.UnitSystem
 import java.time.Instant
 import java.time.ZoneId
 import java.time.ZonedDateTime
@@ -169,12 +172,20 @@ 		if (departureTime.isBefore(now) && r < 3u) {
 			r = 0u
 		}
 		return when (r) {
-			0u -> DateUtils.getRelativeTimeSpanString(
-				departureTime.toEpochSecond() * 1000,
-				now.toEpochSecond() * 1000,
-				DateUtils.MINUTE_IN_MILLIS,
-				DateUtils.FORMAT_ABBREV_RELATIVE
-			).toString()
+			0u -> if (context != null && UnitSystem.getSelected(context) is TGM) {
+				val us = UnitSystem.getSelected(context)
+				us.toString(
+					context,
+					us.timeUnit(Second((departureTime.toEpochSecond() - now.toEpochSecond()).toInt()))
+				)
+			} else {
+				DateUtils.getRelativeTimeSpanString(
+					departureTime.toEpochSecond() * 1000,
+					now.toEpochSecond() * 1000,
+					DateUtils.MINUTE_IN_MILLIS,
+					DateUtils.FORMAT_ABBREV_RELATIVE
+				).toString()
+			}
 
 			1u -> context?.getString(R.string.departure_momentarily) ?: "momentarily"
 			2u -> context?.getString(R.string.departure_now) ?: "now"




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Vehicle.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Vehicle.kt
index b4ae2e175d1ed72cf2b38980d9da95693764bd80..44aa528a021dd99af6f62a3f5f09bff65dbe9897 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Vehicle.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Vehicle.kt
@@ -11,6 +11,7 @@ import xyz.apiote.bimba.czwek.api.OccupancyStatusV1
 import xyz.apiote.bimba.czwek.api.VehicleV1
 import xyz.apiote.bimba.czwek.api.VehicleV2
 import xyz.apiote.bimba.czwek.api.VehicleV3
+import xyz.apiote.bimba.czwek.units.Mps
 
 enum class CongestionLevel {
 	UNKNOWN, SMOOTH, STOP_AND_GO, SIGNIFICANT, SEVERE;
@@ -73,7 +74,7 @@ data class Vehicle(
 	val ID: String,
 	val Position: Position,
 	val Capabilities: UShort,
-	val Speed: Float,
+	val Speed: Mps,
 	val Line: LineStub,
 	val Headsign: String,
 	val congestionLevel: CongestionLevel,
@@ -83,7 +84,7 @@ 	constructor(v: VehicleV1) : this(
 		v.ID,
 		Position(v.Position),
 		v.Capabilities,
-		v.Speed,
+		Mps(v.Speed.toDouble()),
 		LineStub(v.Line),
 		v.Headsign,
 		CongestionLevel.of(v.CongestionLevel),
@@ -94,7 +95,7 @@ 	constructor(v: VehicleV2) : this(
 		v.ID,
 		Position(v.Position),
 		v.Capabilities,
-		v.Speed,
+		Mps(v.Speed.toDouble()),
 		LineStub(v.Line),
 		v.Headsign,
 		CongestionLevel.of(v.CongestionLevel),
@@ -105,7 +106,7 @@ 	constructor(v: VehicleV3) : this(
 		v.ID,
 		Position(v.Position),
 		v.Capabilities,
-		v.Speed,
+		Mps(v.Speed.toDouble()),
 		LineStub(v.Line),
 		v.Headsign,
 		CongestionLevel.of(v.CongestionLevel),




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt
index 5575f8547ed6f8abfeacabc82767927c814a74ac..7ce6dc062432c68e176a752e609cc2b966dbef52 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt
@@ -6,6 +6,7 @@ package xyz.apiote.bimba.czwek.search
 
 import android.content.Context
 import android.content.Intent
+import android.location.Location
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
@@ -21,6 +22,9 @@ import xyz.apiote.bimba.czwek.repo.Queryable
 import xyz.apiote.bimba.czwek.repo.Stop
 import xyz.apiote.bimba.czwek.repo.StopStub
 import xyz.apiote.bimba.czwek.settings.feeds.FeedsSettings
+import xyz.apiote.bimba.czwek.units.Metre
+import xyz.apiote.bimba.czwek.units.UnitSystem
+import kotlin.math.abs
 
 class BimbaViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
 	val root: View = itemView.findViewById(R.id.suggestion)
@@ -28,6 +32,8 @@ 	val icon: ImageView = itemView.findViewById(R.id.suggestion_image)
 	val title: TextView = itemView.findViewById(R.id.suggestion_title)
 	val description: TextView = itemView.findViewById(R.id.suggestion_description)
 	val feedName: TextView = itemView.findViewById(R.id.feed_name)
+	val distance: TextView = itemView.findViewById(R.id.distance)
+	val arrow: ImageView = itemView.findViewById(R.id.arrow)
 
 	companion object {
 		fun bind(
@@ -36,10 +42,23 @@ 			holder: BimbaViewHolder?,
 			context: Context?,
 			feeds: Map<String, FeedInfo>?,
 			feedsSettings: FeedsSettings?,
-			onClickListener: (Queryable) -> Unit
+			onClickListener: (Queryable) -> Unit,
+			position: Location?,
+			heading: Float?,
+			showArrow: Boolean
 		) {
 			when (queryable) {
-				is Stop -> bindStop(queryable, holder, context, feeds, feedsSettings)
+				is Stop -> bindStop(
+					queryable,
+					holder,
+					context,
+					feeds,
+					feedsSettings,
+					position,
+					heading,
+					showArrow
+				)
+
 				is Line -> bindLine(queryable, holder, context, feeds, feedsSettings)
 			}
 			holder?.root?.setOnClickListener {
@@ -82,8 +101,39 @@ 			stop: Stop,
 			holder: BimbaViewHolder?,
 			context: Context?,
 			feeds: Map<String, FeedInfo>?,
-			feedsSettings: FeedsSettings?
+			feedsSettings: FeedsSettings?,
+			position: Location?,
+			heading: Float?,
+			showArrow: Boolean
 		) {
+
+			if (showArrow && position != null && heading != null) {
+				Location(null).apply {
+					latitude = stop.position.latitude
+					longitude = stop.position.longitude
+				}.let {
+					val angle =
+						(360 + ((position.bearingTo(it) + 360).mod(360f)) - heading).mod(360f)
+					val distance = position.distanceTo(it)
+					holder?.arrow?.apply {
+						setImageResource(R.drawable.arrow)
+						rotation = angle
+						visibility = View.VISIBLE
+						contentDescription = "Arrow" // TODO
+					}
+					holder?.distance?.apply {
+						val us = UnitSystem.getSelected(context!!)
+						text = us.toString(context, us.distanceUnit(Metre(distance.toDouble())))
+						contentDescription =
+							us.distanceUnit(Metre(distance.toDouble())).contentDescription(context, us.base)
+						visibility = View.VISIBLE
+					}
+				}
+			} else {
+				holder?.arrow?.visibility = View.GONE
+				holder?.distance?.visibility = View.GONE
+			}
+
 			holder?.icon?.apply {
 				setImageDrawable(stop.icon(context!!))
 				contentDescription = context.getString(R.string.stop_content_description)
@@ -149,11 +199,20 @@ class BimbaResultsAdapter(
 	private val inflater: LayoutInflater,
 	private val context: Context?,
 	private var queryables: List<Queryable>,
+	private var position: Location?,
+	private var heading: Float?,
+	private var showArrow: Boolean
 ) :
 	RecyclerView.Adapter<BimbaViewHolder>() {
 	class DiffUtilCallback(
 		private val oldQueryables: List<Queryable>,
-		private val newQueryables: List<Queryable>
+		private val newQueryables: List<Queryable>,
+		private val oldPosition: Location?,
+		private val newPosition: Location?,
+		private val oldHeading: Float?,
+		private val newHeading: Float?,
+		private val oldShowArrow: Boolean,
+		private val newShowArrow: Boolean
 	) : DiffUtil.Callback() {
 		override fun getOldListSize() = oldQueryables.size
 
@@ -202,7 +261,11 @@ 					val oldChangeOptions =
 						oldQueryable.changeOptions.joinToString { "${it.line}->${it.headsign}" }
 					val newChangeOptions =
 						(newQueryable as Stop).changeOptions.joinToString { "${it.line}->${it.headsign}" }
-					oldQueryable.name == newQueryable.name && oldChangeOptions == newChangeOptions
+					oldQueryable.name == newQueryable.name && oldChangeOptions == newChangeOptions &&
+						oldPosition?.latitude == newPosition?.latitude &&
+						oldPosition?.longitude == newPosition?.longitude &&
+						oldHeading == newHeading &&
+						oldShowArrow == newShowArrow
 				}
 
 				else -> false // XXX unreachable
@@ -247,16 +310,82 @@ 			holder,
 			context,
 			feeds,
 			feedsSettings,
-			onClickListener
+			onClickListener,
+			this.position,
+			heading,
+			showArrow
 		)
 	}
 
 	override fun getItemCount(): Int = queryables.size
 
-	fun update(queryables: List<Queryable>?) {
-		val newQueryables = queryables ?: emptyList()
-		val diff = DiffUtil.calculateDiff(DiffUtilCallback(this.queryables, newQueryables))
-		this.queryables = newQueryables
+	fun update(
+		queryables: List<Queryable>?,
+		position: Location?,
+		heading: Float?,
+		showArrow: Boolean
+	) {
+		val diff = DiffUtil.calculateDiff(
+			DiffUtilCallback(
+				this.queryables,
+				queryables ?: emptyList(),
+				this.position,
+				position,
+				this.heading,
+				heading,
+				this.showArrow,
+				showArrow
+			)
+		)
+		this.position = position
+		this.heading = heading
+		this.showArrow = showArrow
+		this.queryables = queryables ?: emptyList()
+		diff.dispatchUpdatesTo(this)
+	}
+
+	fun update(
+		heading: Float?,
+	) {
+		if (abs((heading ?: 0f) - (this.heading ?: 0f)) < 15) {
+			return
+		}
+		val diff = DiffUtil.calculateDiff(
+			DiffUtilCallback(
+				queryables,
+				queryables,
+				position,
+				position,
+				this.heading,
+				heading,
+				showArrow,
+				showArrow
+			)
+		)
+		this.heading = heading
+		diff.dispatchUpdatesTo(this)
+	}
+
+	fun update(
+		queryables: List<Queryable>?,
+		position: Location?,
+		showArrow: Boolean
+	) {
+		val diff = DiffUtil.calculateDiff(
+			DiffUtilCallback(
+				this.queryables,
+				queryables ?: emptyList(),
+				this.position,
+				position,
+				heading,
+				heading,
+				this.showArrow,
+				showArrow
+			)
+		)
+		this.position = position
+		this.showArrow = showArrow
+		this.queryables = queryables ?: emptyList()
 		diff.dispatchUpdatesTo(this)
 	}
 




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt
index f8729039653a32172c5b25a51668ff9ddfb15a6e..9a778552b653033536f19fbbb2deb113457e765b 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt
@@ -5,6 +5,10 @@
 package xyz.apiote.bimba.czwek.search
 
 import android.content.Context
+import android.hardware.Sensor
+import android.hardware.SensorEvent
+import android.hardware.SensorEventListener
+import android.hardware.SensorManager
 import android.location.Location
 import android.location.LocationListener
 import android.location.LocationManager
@@ -23,6 +27,7 @@ import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.updateLayoutParams
 import androidx.core.view.updatePadding
 import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.openlocationcode.OpenLocationCode
 import kotlinx.coroutines.MainScope
 import kotlinx.coroutines.Runnable
 import kotlinx.coroutines.launch
@@ -36,9 +41,9 @@ import xyz.apiote.bimba.czwek.repo.Queryable
 import xyz.apiote.bimba.czwek.repo.TrafficResponseException
 import xyz.apiote.bimba.czwek.settings.feeds.FeedsSettings
 
-class ResultsActivity : AppCompatActivity(), LocationListener {
+class ResultsActivity : AppCompatActivity(), LocationListener, SensorEventListener {
 	enum class Mode {
-		MODE_LOCATION, MODE_SEARCH, MODE_POSITION
+		MODE_LOCATION, MODE_SEARCH, MODE_POSITION, MODE_SHORT_CODE_LOCATION, MODE_SHORT_CODE
 	}
 
 	private var _binding: ActivityResultsBinding? = null
@@ -48,6 +53,9 @@ 	private lateinit var adapter: BimbaResultsAdapter
 
 	private val handler = Handler(Looper.getMainLooper())
 	private var runnable = Runnable {}
+	private var gravity: FloatArray? = null
+	private var geomagnetic: FloatArray? = null
+	private var shortOLC: OpenLocationCode? = null
 
 	override fun onCreate(savedInstanceState: Bundle?) {
 		enableEdgeToEdge()
@@ -73,7 +81,7 @@ 			windowInsets
 		}
 
 		binding.resultsRecycler.layoutManager = LinearLayoutManager(this)
-		adapter = BimbaResultsAdapter(layoutInflater, this, listOf())
+		adapter = BimbaResultsAdapter(layoutInflater, this, listOf(), null, null, false)
 		binding.resultsRecycler.adapter = adapter
 
 		when (getMode()) {
@@ -82,12 +90,39 @@ 				supportActionBar?.title = getString(R.string.stops_nearby)
 				locate()
 			}
 
+			Mode.MODE_SHORT_CODE_LOCATION -> {
+				val query = intent.extras?.getString("query")
+				getString(R.string.stops_near_code, query)
+				shortOLC = OpenLocationCode(query)
+				locate()
+			}
+
+			Mode.MODE_SHORT_CODE -> {
+				val query = intent.extras?.getString("query")
+				val split = query!!.trim().split(" ")
+				val code = split.first().trim(',').trim()
+				val freePart = split.drop(1).joinToString(" ")
+				val location = findPlace(this, freePart)
+				if (location == null) {
+					showError(Error(0, R.string.error_geocoding, R.drawable.geocoding))
+				} else {
+					val area = OpenLocationCode(code).recover(location.latitude, location.longitude).decode()
+					getQueryablesByLocation(Location(null).apply {
+						latitude = area.centerLatitude
+						longitude = area.centerLongitude
+					}, this)
+				}
+			}
+
 			Mode.MODE_POSITION -> {
 				val query = intent.extras?.getString("query")
 				val lat = intent.extras?.getDouble("lat")
 				val lon = intent.extras?.getDouble("lon")
 				supportActionBar?.title = getString(R.string.stops_near_code, query)
-				getQueryablesByLocation(Position(lat!!, lon!!), this)
+				getQueryablesByLocation(Location(null).apply {
+					latitude = lat!!
+					longitude = lon!!
+				}, this)
 			}
 
 			Mode.MODE_SEARCH -> {
@@ -108,6 +143,12 @@ 		}
 	}
 
 	private fun locate() {
+		val sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
+		val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
+		val magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
+		sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL)
+		sensorManager.registerListener(this, magnetometer, SensorManager.SENSOR_DELAY_NORMAL)
+
 		try {
 			val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
 			locationManager.requestLocationUpdates(
@@ -127,7 +168,30 @@ 	}
 
 	override fun onLocationChanged(location: Location) {
 		handler.removeCallbacks(runnable)
-		getQueryablesByLocation(Position(location.latitude, location.longitude), this)
+		val area = shortOLC?.recover(location.latitude, location.longitude)?.decode()
+		if (area != null) {
+			getQueryablesByLocation(Location(null).apply {
+				latitude = area.centerLatitude
+				longitude = area.centerLongitude
+			}, this, false)
+		} else {
+			getQueryablesByLocation(location, this, true)
+		}
+	}
+
+	override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
+	override fun onSensorChanged(event: SensorEvent?) {
+		if (event?.sensor?.type == Sensor.TYPE_ACCELEROMETER) gravity = event.values
+		if (event?.sensor?.type == Sensor.TYPE_MAGNETIC_FIELD) geomagnetic = event.values
+		if (gravity != null && geomagnetic != null) {
+			val r = FloatArray(9)
+			val success = SensorManager.getRotationMatrix(r, FloatArray(9), gravity, geomagnetic)
+			if (success) {
+				val orientation = FloatArray(3)
+				SensorManager.getOrientation(r, orientation)
+				adapter.update((orientation[0] * 180 / Math.PI).toFloat())
+			}
+		}
 	}
 
 	override fun onResume() {
@@ -142,6 +206,8 @@ 		super.onPause()
 		val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
 		locationManager.removeUpdates(this)
 		handler.removeCallbacks(runnable)
+		val sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
+		sensorManager.unregisterListener(this)
 	}
 
 	override fun onDestroy() {
@@ -168,7 +234,7 @@ 			try {
 				val repository = OnlineRepository()
 				val result = repository.queryQueryables(query, context)
 				getFeeds()
-				updateItems(result)
+				updateItems(result, null, false)
 			} catch (e: TrafficResponseException) {
 				Log.w("Suggestion", "$e")
 				showError(e.error)
@@ -176,13 +242,18 @@ 			}
 		}
 	}
 
-	private fun getQueryablesByLocation(position: Position, context: Context) {
+	private fun getQueryablesByLocation(
+		position: Location,
+		context: Context,
+		showArrow: Boolean = false
+	) {
 		MainScope().launch {
 			try {
 				val repository = OnlineRepository()
-				val result = repository.locateQueryables(position, context)
+				val result =
+					repository.locateQueryables(Position(position.latitude, position.longitude), context)
 				getFeeds()
-				updateItems(result)
+				updateItems(result, position, showArrow)
 			} catch (e: TrafficResponseException) {
 				Log.w("Suggestion", "$e")
 				showError(e.error)
@@ -200,9 +271,9 @@ 		binding.errorText.text = getString(error.stringResource)
 		binding.errorImage.setImageDrawable(AppCompatResources.getDrawable(this, error.imageResource))
 	}
 
-	private fun updateItems(queryables: List<Queryable>?) {
+	private fun updateItems(queryables: List<Queryable>?, position: Location?, showArrow: Boolean) {
 		binding.resultsProgress.visibility = View.GONE
-		adapter.update(queryables)
+		adapter.update(queryables, position, showArrow)
 		if (queryables.isNullOrEmpty()) {
 			binding.errorImage.visibility = View.VISIBLE
 			binding.errorText.visibility = View.VISIBLE




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/geocoder.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/geocoder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7fd75e381c417517d76ecd5d8b2f68ac68718d0a
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/geocoder.kt
@@ -0,0 +1,27 @@
+package xyz.apiote.bimba.czwek.search
+
+import android.content.Context
+import android.database.sqlite.SQLiteDatabase
+import android.location.Location
+
+fun findPlace(context: Context, name: String): Location? {
+	val db = SQLiteDatabase.openOrCreateDatabase(context.getDatabasePath("geocoding").path, null)
+	val cursor = db.rawQuery(
+		"select lat, lon from place_names join places using(id) where name = ?",
+		arrayOf(name)
+	)
+
+	if (!cursor.moveToNext()) {
+		cursor.close()
+		db.close()
+		return null
+	}
+
+	val location = Location(null).apply {
+		latitude = cursor.getDouble(0)
+		longitude = cursor.getDouble(1)
+	}
+	cursor.close()
+	db.close()
+	return location
+}
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/DownloadCitiesWorker.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/DownloadCitiesWorker.kt
new file mode 100644
index 0000000000000000000000000000000000000000..02de1d344e0666614ee02207fe90531cd3cb5626
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/DownloadCitiesWorker.kt
@@ -0,0 +1,221 @@
+package xyz.apiote.bimba.czwek.settings
+
+import android.Manifest
+import android.app.NotificationManager
+import android.content.Context
+import android.content.pm.PackageManager
+import android.database.sqlite.SQLiteDatabase
+import android.util.Log
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.edit
+import androidx.core.database.sqlite.transaction
+import androidx.preference.PreferenceManager
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import com.github.doyaaaaaken.kotlincsv.dsl.csvReader
+import org.apache.commons.io.input.BoundedInputStream
+import xyz.apiote.bimba.czwek.R
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.io.File
+import java.net.URL
+import java.time.ZonedDateTime
+import java.util.UUID
+import java.util.zip.ZipEntry
+import java.util.zip.ZipInputStream
+
+class DownloadCitiesWorker(appContext: Context, workerParams: WorkerParameters) :
+	Worker(appContext, workerParams) {
+
+	override fun doWork(): Result {
+		val notificationBuilder = NotificationCompat.Builder(applicationContext, "cities_channel")
+			.setSmallIcon(R.drawable.geocoding)
+			.setContentTitle(applicationContext.getString(R.string.updating_geocoding_data))
+			.setContentText(applicationContext.getString(R.string.downloading_cities_list))
+			.setPriority(NotificationCompat.PRIORITY_LOW)
+			.setProgress(100, 0, true)
+		try {
+			if (ActivityCompat.checkSelfPermission(
+					applicationContext,
+					Manifest.permission.POST_NOTIFICATIONS
+				) == PackageManager.PERMISSION_GRANTED
+			) {
+				NotificationManagerCompat.from(applicationContext).notify(0, notificationBuilder.build())
+			}
+
+			val db = SQLiteDatabase.openOrCreateDatabase(
+				applicationContext.getDatabasePath("geocoding").path,
+				null
+			)
+			val url = URL("https://download.geonames.org/export/dump/cities15000.zip")
+			val connection = url.openConnection()
+			var length = connection.contentLength.toLong()
+			val connectionEtag = connection.getHeaderField("ETag")
+			val savedEtag = PreferenceManager.getDefaultSharedPreferences(applicationContext)
+				.getString("cities_etag", null)
+			if (savedEtag != null && savedEtag == connectionEtag) {
+				if (ActivityCompat.checkSelfPermission(
+						applicationContext,
+						Manifest.permission.POST_NOTIFICATIONS
+					) == PackageManager.PERMISSION_GRANTED
+				) {
+					NotificationManagerCompat.from(applicationContext).cancel(0)
+				}
+				return Result.success()
+			}
+
+			db.execSQL("drop table if exists place_names2")
+			db.execSQL("drop table if exists places2")
+			db.execSQL("create table places2(id text primary key, lat real, lon real)")
+			db.execSQL("create table place_names2(id text references places(id), name text primary key)")
+
+			var countingStream =
+				BoundedInputStream.Builder()
+					.setInputStream(BufferedInputStream(connection.getInputStream())).get()
+			val zipFileStream = BufferedOutputStream(
+				File(
+					applicationContext.noBackupFilesDir.path,
+					"cities.zip"
+				).outputStream()
+			)
+
+			val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
+			var bytes = countingStream.read(buffer)
+			while (bytes >= 0) {
+				zipFileStream.write(buffer, 0, bytes)
+				Log.i(
+					"geocoding",
+					"zip_download: downloaded ${countingStream.count}/$length: ${countingStream.count.toFloat() / length * 100}%"
+				)
+				if (ActivityCompat.checkSelfPermission(
+						applicationContext,
+						Manifest.permission.POST_NOTIFICATIONS
+					) == PackageManager.PERMISSION_GRANTED
+				) {
+					notificationBuilder
+						.setProgress(100, (countingStream.count.toFloat() / length * 100).toInt(), false)
+					NotificationManagerCompat.from(applicationContext).notify(0, notificationBuilder.build())
+				}
+				bytes = countingStream.read(buffer)
+			}
+			countingStream.close()
+			zipFileStream.close()
+
+			notificationBuilder
+				.setProgress(100, 0, true)
+				.setContentText(applicationContext.getString(R.string.saving_cities_list))
+			if (ActivityCompat.checkSelfPermission(
+					applicationContext,
+					Manifest.permission.POST_NOTIFICATIONS
+				) == PackageManager.PERMISSION_GRANTED
+			) {
+				NotificationManagerCompat.from(applicationContext).notify(0, notificationBuilder.build())
+			}
+			val zipFile = File(applicationContext.noBackupFilesDir.path, "cities.zip")
+			length = zipFile.length()
+			countingStream =
+				BoundedInputStream.Builder().setInputStream(BufferedInputStream(zipFile.inputStream()))
+					.get()
+			val stream = ZipInputStream(countingStream)
+			var entry: ZipEntry? = stream.nextEntry
+			while (entry != null) {
+				if (entry.name != "cities15000.txt") {
+					entry = stream.nextEntry
+					continue
+				}
+				var count = 0
+				db.transaction {
+					csvReader { delimiter = '\t' }.open(stream) {
+						readAllAsSequence().forEach { row ->
+							val names = if (row[3] == "") {
+								"${row[1]},${row[2]}"
+							} else {
+								row[3]
+							}
+							if (count % 1000 == 0) {
+								Log.i(
+									"geocoding",
+									"${countingStream.count}/$length=${countingStream.count.toFloat() / length * 100}% $names"
+								)
+								if (ActivityCompat.checkSelfPermission(
+										applicationContext,
+										Manifest.permission.POST_NOTIFICATIONS
+									) == PackageManager.PERMISSION_GRANTED
+								) {
+									notificationBuilder
+										.setProgress(
+											100,
+											(countingStream.count.toFloat() / length * 100).toInt(),
+											false
+										)
+									NotificationManagerCompat.from(applicationContext)
+										.notify(0, notificationBuilder.build())
+								}
+							}
+							count++
+
+							val id = UUID.randomUUID()
+							db.execSQL("insert into places2 values(?, ?, ?)", arrayOf(id, row[4], row[5]))
+							names.split(",").toSet().forEach { name ->
+								db.execSQL(
+									"insert into place_names2 values(?, ?) on conflict(name) do nothing",
+									arrayOf(id, name)
+								)
+								db.execSQL(
+									"insert into place_names2 values(?, ?) on conflict(name) do nothing",
+									arrayOf(id, "$name, ${row[8]}")
+								)
+							}
+						}
+					}
+				}
+				Log.i("geocoding", "COMPLETE")
+				break
+			}
+			stream.close()
+			zipFile.delete()
+
+			db.execSQL("drop index if exists place_names__name")
+			db.execSQL("drop table if exists place_names")
+			db.execSQL("drop table if exists places")
+			db.execSQL("alter table places2 rename to places")
+			db.execSQL("alter table place_names2 rename to place_names")
+			db.execSQL("create unique index place_names__name on place_names(name)")
+
+			PreferenceManager.getDefaultSharedPreferences(applicationContext).edit {
+				putLong("cities_last_update", ZonedDateTime.now().toEpochSecond())
+				putString("cities_etag", connectionEtag)
+			}
+
+			db.close()
+			if (ActivityCompat.checkSelfPermission(
+					applicationContext,
+					Manifest.permission.POST_NOTIFICATIONS
+				) == PackageManager.PERMISSION_GRANTED
+			) {
+				notificationBuilder
+					.setContentText("")
+					.setContentTitle(applicationContext.getString(R.string.finished_updating_geocoding_data))
+					.setProgress(100, 100, false)
+				NotificationManagerCompat.from(applicationContext).notify(0, notificationBuilder.build())
+			}
+			return Result.success()
+		} catch (e: Exception) {
+			e.printStackTrace()
+			if (ActivityCompat.checkSelfPermission(
+					applicationContext,
+					Manifest.permission.POST_NOTIFICATIONS
+				) == PackageManager.PERMISSION_GRANTED
+			) {
+				notificationBuilder
+					.setContentText("")
+					.setContentTitle(applicationContext.getString(R.string.updating_geocoding_data_failed))
+					.setProgress(100, 100, false)
+				NotificationManagerCompat.from(applicationContext).notify(0, notificationBuilder.build())
+			}
+			return Result.failure()
+		}
+	}
+}
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/ServerChooserActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/ServerChooserActivity.kt
index 223dfeee4be34ef5e501c4760e382eb3342bdf24..88165f880c011dfc8b55b0e6d32ac1363d8b0f55 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/ServerChooserActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/ServerChooserActivity.kt
@@ -134,7 +134,7 @@ 		MaterialAlertDialogBuilder(this).setIcon(AppCompatResources.getDrawable(this, icon))
 			.setTitle(getString(title)).setMessage(getString(description))
 			.setNegativeButton(R.string.cancel) { _, _ -> }.apply {
 				if (onPositive != null) {
-					setPositiveButton(R.string.cont) { _, _ ->
+					setPositiveButton(R.string.continue_) { _, _ ->
 						onPositive()
 					}
 				}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/SettingsActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/SettingsActivity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4a76cd5ef120f9419ff479301845c81d8240ceae
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/SettingsActivity.kt
@@ -0,0 +1,108 @@
+package xyz.apiote.bimba.czwek.settings
+
+import android.content.Context
+import android.os.Bundle
+import android.text.format.DateUtils
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updateLayoutParams
+import androidx.core.view.updatePadding
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceManager
+import androidx.preference.SwitchPreferenceCompat
+import androidx.work.ExistingWorkPolicy
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.OutOfQuotaPolicy
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import xyz.apiote.bimba.czwek.R
+import java.util.concurrent.ExecutionException
+
+
+class SettingsActivity : AppCompatActivity() {
+
+	override fun onCreate(savedInstanceState: Bundle?) {
+		enableEdgeToEdge()
+		super.onCreate(savedInstanceState)
+		setContentView(R.layout.settings_activity)
+		if (savedInstanceState == null) {
+			supportFragmentManager
+				.beginTransaction()
+				.replace(R.id.settings, SettingsFragment())
+				.commit()
+		}
+		supportActionBar?.setDisplayHomeAsUpEnabled(true)
+
+		val root = findViewById<View>(R.id.settings)
+
+		ViewCompat.setOnApplyWindowInsetsListener(root) { v, windowInsets ->
+			val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+			v.updatePadding(left = windowInsets.displayCutout?.safeInsetLeft?.takeIf { it > 0 } ?: insets.left)
+			v.updatePadding(right = windowInsets.displayCutout?.safeInsetRight?.takeIf { it > 0 } ?: insets.right)
+			v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
+				topMargin = insets.top
+			}
+			windowInsets
+		}
+	}
+
+	class SettingsFragment : PreferenceFragmentCompat() {
+		override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+			setPreferencesFromResource(R.xml.root_preferences, rootKey)
+
+			findPreference<Preference>("download_cities_list")?.setOnPreferenceClickListener {
+				val request = OneTimeWorkRequestBuilder<DownloadCitiesWorker>()
+					.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+					.build()
+				WorkManager.getInstance(requireContext())
+					.enqueueUniqueWork("download_cities", ExistingWorkPolicy.KEEP, request)
+				findPreference<Preference>("download_cities_list")?.isEnabled = false
+				true
+			}
+
+			if (isWorkScheduled(requireContext(), "download_cities")) {
+				findPreference<Preference>("download_cities_list")?.isEnabled = false
+			}
+
+			val citiesLastUpdate = PreferenceManager.getDefaultSharedPreferences(requireContext())
+				.getLong("cities_last_update", -1)
+			if (citiesLastUpdate > 0) {
+				val lastUpdateTime = DateUtils.getRelativeDateTimeString(
+					context,
+					citiesLastUpdate * DateUtils.SECOND_IN_MILLIS,
+					DateUtils.DAY_IN_MILLIS,
+					DateUtils.WEEK_IN_MILLIS,
+					0
+				)
+
+				findPreference<SwitchPreferenceCompat>("autoupdate_cities_list")?.summary =
+					getString(R.string.last_update, lastUpdateTime)
+			}
+		}
+
+		private fun isWorkScheduled(context: Context, name: String): Boolean {
+			val instance = WorkManager.getInstance(context)
+			val statuses = instance.getWorkInfosForUniqueWork(name)
+			try {
+				var running = false
+				val workInfoList = statuses.get()
+				for (workInfo in workInfoList) {
+					val state: WorkInfo.State = workInfo.state
+					running = (state == WorkInfo.State.RUNNING) or (state == WorkInfo.State.ENQUEUED)
+				}
+				return running
+			} catch (e: ExecutionException) {
+				e.printStackTrace()
+				return false
+			} catch (e: InterruptedException) {
+				e.printStackTrace()
+				return false
+			}
+		}
+	}
+}
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/units/TGM.kt b/app/src/main/java/xyz/apiote/bimba/czwek/units/TGM.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d99b72fac627db1e05c134ee36112e0a1d2ac798
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/units/TGM.kt
@@ -0,0 +1,201 @@
+package xyz.apiote.bimba.czwek.units
+
+import android.content.Context
+import xyz.apiote.bimba.czwek.R
+import java.text.NumberFormat
+import kotlin.math.abs
+import kotlin.math.pow
+
+
+class TGM(base: Int) : UnitSystem(base) {
+	override fun timeUnit(count: Long): TimeUnit = Tim(count)
+	override fun timeUnit(other: TimeUnit): TimeUnit = Tim(other)
+
+	override fun speedUnit(count: Double): SpeedUnit = Vlos(count)
+	override fun speedUnit(other: SpeedUnit): SpeedUnit = Vlos(other)
+
+	override fun distanceUnit(count: Double): DistanceUnit = Grafut(count)
+	override fun distanceUnit(other: DistanceUnit): DistanceUnit = Grafut(other)
+
+	override fun toString(context: Context, s: SpeedUnit): String = s.toString(context, base)
+	override fun toString(context: Context, t: TimeUnit): String = t.toString(context, base)
+	override fun toString(context: Context, d: DistanceUnit): String = d.toString(context, base)
+}
+
+class Tim(val tims: Long) : TimeUnit {
+	constructor(other: TimeUnit) : this((other.milliseconds() * 144 / 25 / 1000))
+
+	override fun milliseconds(): Long = tims * 25 * 1000 / 144
+	override fun toString(context: Context, base: Int): String {
+		val res = if (tims < 0) {
+			R.string.time_in_tm_past
+		} else {
+			R.string.time_in_tm
+		}
+		val (t, m) = if (tims > base.toDouble().pow(4)) {
+			Pair(tims / base.toDouble().pow(4).toInt(), "⁴")
+		} else {
+			Pair(tims / base.toDouble().pow(2).toInt(), "²")
+		}
+		return if (base == 10) {
+			context.getString(
+				res,
+				NumberFormat.getInstance().apply { maximumFractionDigits = 2 }.format(abs(t)),
+				m
+			)
+		} else {
+			context.getString(
+				res,
+				abs(t).toString(12).lowercase().replace("a", "↊").replace("b", "↋"),
+				m
+			)
+		}
+	}
+
+	override fun contentDescription(context: Context, base: Int): String {
+		return if (base == 10) {
+			context.resources.getQuantityString(
+				R.plurals.time_in_tm_cd,
+				tims.toInt(),
+				tims.toInt()
+			)
+		} else {
+			if (tims > base.toDouble().pow(4)) {
+				val t = tims / base.toDouble().pow(4)
+				context.resources.getQuantityString(
+					R.plurals.time_in_4tm_12_cd,
+					t.toInt(),
+					t.toInt().toDozenalString(context)
+				)
+			} else {
+				val t = tims / base.toDouble().pow(2)
+				context.resources.getQuantityString(
+					R.plurals.time_in_2tm_12_cd,
+					t.toInt(),
+					t.toInt().toDozenalString(context)
+				)
+			}
+		}
+	}
+}
+
+class Grafut(val grafut: Double) : DistanceUnit {
+	constructor(other: DistanceUnit) : this(other.meters() / .295682912)
+
+	override fun meters(): Double = grafut * .295682912
+
+	override fun toString(context: Context, base: Int): String {
+		val (g, m) = if (grafut > base.toDouble().pow(3)) {
+			Pair(grafut / base.toDouble().pow(3), "³")
+		} else {
+			Pair(grafut, "")
+		}
+
+		return if (base == 10) {
+			context.getString(
+				R.string.distance_in_gf,
+				NumberFormat.getInstance().apply { maximumFractionDigits = 2 }.format(g),
+				m
+			)
+		} else {
+			context.getString(
+				R.string.distance_in_gf,
+				g.toString(12, 2).lowercase().replace("a", "↊").replace("b", "↋"),
+				m
+			)
+		}
+	}
+
+	override fun contentDescription(context: Context, base: Int): String {
+		return if (base == 10) {
+			context.resources.getQuantityString(
+				R.plurals.distance_in_gf_cd,
+				grafut.toInt(),
+				grafut.toInt()
+			)
+		} else {
+			if (grafut > base.toDouble().pow(3)) {
+				val g = grafut / base.toDouble().pow(3)
+				context.resources.getQuantityString(
+					R.plurals.distance_in_3gf_12_cd,
+					g.toInt(),
+					g.toInt().toDozenalString(context)
+				)
+			} else {
+				context.resources.getQuantityString(
+					R.plurals.distance_in_gf_12_cd,
+					grafut.toInt(),
+					grafut.toInt().toDozenalString(context)
+				)
+			}
+		}
+	}
+}
+
+class Vlos(val vlos: Double) : SpeedUnit {
+	constructor(other: SpeedUnit) : this(other.mps() / 1.703133986928105)
+
+	override fun mps(): Double = vlos * 1.703133986928105
+	override fun contentDescription(context: Context, base: Int): String {
+		return if (base == 10) {
+			context.resources.getQuantityString(
+				R.plurals.speed_in_vl_cd,
+				vlos.toInt(),
+				vlos.toInt()
+			)
+		} else {
+			context.resources.getQuantityString(
+				R.plurals.speed_in_vl_12_cd,
+				vlos.toInt(),
+				vlos.toInt().toDozenalString(context)
+			)
+		}
+	}
+
+	override fun toString(context: Context, base: Int): String {
+		return context.getString(R.string.speed_in_vl, vlos.toString(base, 3).lowercase())
+	}
+}
+
+fun Double.toString(radix: Int, precision: Int): String {
+	var x = this
+	var result = ""
+	if (x < 0) {
+		result = "-"
+		x = -x
+	}
+	result += x.toInt().toString(radix)
+	var frac = (x - x.toInt())
+	var digits = 0
+	while (frac > 0 && digits < precision) {
+		if (digits == 0) result += if (radix == 12) "·" else NumberFormat.getInstance().apply { maximumFractionDigits = 1 }.format(0.5).replace(Regex("[0-9]"), "")
+		frac *= radix
+		result += frac.toInt().toString(radix)
+		frac -= frac.toInt()
+		digits++
+	}
+	return result.lowercase().replace("a", "↊").replace("b", "↋")
+}
+
+fun Int.toDozenalString(context: Context): String {
+	if (this == 0) {
+		return context.resources.getStringArray(R.array.dozenal_digits)[0]
+	}
+	val r = StringBuilder()
+	var n = this
+	val digits = context.resources.getStringArray(R.array.dozenal_digits)
+	val multipliers = context.resources.getStringArray(R.array.dozenal_multipliers)
+	var i = 0
+	while (n > 0) {
+		val u = n % 12
+		if (u != 0) {
+			r.insert(0, " ")
+			r.insert(0, multipliers[i])
+			r.insert(0, " ")
+			r.insert(0, digits[u])
+		}
+		n /= 12
+		i++
+	}
+	return r.toString().trim()
+}
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/units/Time.kt b/app/src/main/java/xyz/apiote/bimba/czwek/units/Time.kt
deleted file mode 100644
index 9456eae31f27301d561789f4e07ca44dd9519bef..0000000000000000000000000000000000000000
--- a/app/src/main/java/xyz/apiote/bimba/czwek/units/Time.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package xyz.apiote.bimba.czwek.units
-
-interface TimeUnit
-
-data class Second(val secs: Long) {
-	constructor(t: Tim) : this(t.tims.toLong() * 25 / 144)
-}
-
-data class Millisecond(val millis: Long) {
-	constructor(t: Tim) : this(t.tims.toLong() * 25 * 1000 / 144)
-	constructor(s: Second) : this(s.secs * 1000)
-}
-
-data class Tim(val tims: Int) {
-	constructor(s: Second) : this((s.secs * 144 / 25).toInt())
-	constructor(m: Millisecond) : this((m.millis * 144 / 25 / 1000).toInt())
-}
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/units/UnitSystem.kt b/app/src/main/java/xyz/apiote/bimba/czwek/units/UnitSystem.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9d0e88c75fc8e29c60f42e938836d8aea33f5b09
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/units/UnitSystem.kt
@@ -0,0 +1,91 @@
+package xyz.apiote.bimba.czwek.units
+
+import android.content.Context
+import android.icu.util.LocaleData
+import android.icu.util.LocaleData.MeasurementSystem
+import android.icu.util.ULocale
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.preference.PreferenceManager
+import java.util.Locale
+
+abstract class UnitSystem(val base: Int) {
+	companion object {
+		@RequiresApi(Build.VERSION_CODES.P)
+		private fun forMeasureSystem(ms: MeasurementSystem) =
+			when (ms) {
+				MeasurementSystem.SI -> {
+					Metric
+				}
+
+				MeasurementSystem.UK -> {
+					Imperial
+				}
+
+				MeasurementSystem.US -> {
+					USCustomary
+				}
+
+				else -> {
+					Metric
+				}
+			}
+
+		private fun forLocale(country: String): UnitSystem = if (setOf(
+				"AG", "BS", "BZ", "DM", "GD", "MH", "FM", "PW", "KN", "LC", "VC", "GB", "AI", "VG",
+				"IO", "KY", "FK", "MS", "SH", "TC", "GG", "IM", "JE", "US", "AS", "GU", "MP", "PR", "VI"
+			).contains(country)
+		) {
+			Imperial
+		} else {
+			Metric
+		}
+
+		private fun getDefault() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+			forMeasureSystem(LocaleData.getMeasurementSystem(ULocale.forLocale(Locale.getDefault())))
+		} else {
+			forLocale(Locale.getDefault().country)
+		}
+
+		fun getSelected(context: Context): UnitSystem =
+			when (PreferenceManager.getDefaultSharedPreferences(context)
+				.getString("unit_system", "default")) {
+				"default" -> getDefault()
+				"metric" -> Metric
+				"imperial" -> Imperial
+				"customary" -> USCustomary
+				"tgm10" -> TGM(10)
+				"tgm12" -> TGM(12)
+				else -> getDefault()
+			}
+	}
+
+	abstract fun timeUnit(count: Long): TimeUnit
+	abstract fun timeUnit(other: TimeUnit): TimeUnit
+	abstract fun speedUnit(count: Double): SpeedUnit
+	abstract fun speedUnit(other: SpeedUnit): SpeedUnit
+	abstract fun distanceUnit(count: Double): DistanceUnit
+	abstract fun distanceUnit(other: DistanceUnit): DistanceUnit
+
+	abstract fun toString(context: Context, s: SpeedUnit): String
+	abstract fun toString(context: Context, t: TimeUnit): String
+	abstract fun toString(context: Context, d: DistanceUnit): String
+}
+
+interface TimeUnit {
+	fun milliseconds(): Long
+	fun toString(context: Context, base: Int): String
+	fun contentDescription(context: Context, base: Int): String
+}
+
+interface SpeedUnit {
+	fun mps(): Double
+	fun toString(context: Context, base: Int): String
+	fun contentDescription(context: Context, base: Int): String
+}
+
+interface DistanceUnit {
+	fun meters(): Double
+	fun toString(context: Context, base: Int): String
+	fun contentDescription(context: Context, base: Int): String
+}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/units/imperial.kt b/app/src/main/java/xyz/apiote/bimba/czwek/units/imperial.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f06ce367fd44750b68a9f8eb3e1a60835d232898
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/units/imperial.kt
@@ -0,0 +1,75 @@
+package xyz.apiote.bimba.czwek.units
+
+import android.content.Context
+import xyz.apiote.bimba.czwek.R
+import java.text.NumberFormat
+
+object Imperial : UnitSystem(10) {
+	override fun timeUnit(count: Long): TimeUnit = Second(count.toDouble())
+	override fun timeUnit(other: TimeUnit): TimeUnit = Second(other)
+
+	override fun speedUnit(count: Double): SpeedUnit = Miph(count)
+	override fun speedUnit(other: SpeedUnit): SpeedUnit = Miph(other)
+
+	override fun distanceUnit(count: Double): DistanceUnit = MiYd(count)
+	override fun distanceUnit(other: DistanceUnit): DistanceUnit = MiYd(other)
+
+	override fun toString(context: Context, s: SpeedUnit): String = s.toString(context, base)
+	override fun toString(context: Context, t: TimeUnit): String = t.toString(context, base)
+	override fun toString(context: Context, d: DistanceUnit): String = d.toString(context, base)
+}
+
+class Miph(val miph: Double) : SpeedUnit {
+	constructor(s: Int) : this(s.toDouble())
+	constructor(other: SpeedUnit) : this(other.mps() / 0.44704)
+
+	override fun mps(): Double = miph * 0.44704
+	override fun toString(context: Context, base: Int): String =
+		context.getString(
+			R.string.speed_in_mi_per_h,
+			NumberFormat.getInstance().apply { maximumFractionDigits = 2 }.format(miph)
+		)
+
+	override fun contentDescription(context: Context, base: Int): String =
+		context.resources.getQuantityString(
+			R.plurals.speed_in_mi_per_h_cd, miph.toInt(), miph.toInt()
+		)
+}
+
+class MiYd(val mi: Double) : DistanceUnit {
+	constructor(other: DistanceUnit) : this(other.meters() / 1609.344)
+
+	override fun meters(): Double = mi * 1609.344
+
+	override fun toString(context: Context, base: Int): String =
+		if (mi < 1) {
+			context.getString(
+				R.string.distance_in_yd,
+				NumberFormat.getIntegerInstance().format((mi * 1760).toInt())
+			)
+		} else {
+			context.getString(
+				R.string.distance_in_mi,
+				NumberFormat.getInstance().apply { maximumFractionDigits = 2 }.format(mi)
+			)
+		}
+
+	override fun contentDescription(context: Context, base: Int): String =
+		if (mi < 1) {
+			context.resources.getQuantityString(
+				R.plurals.distance_in_yd_cd,
+				(mi / 1760).toInt(),
+				(mi / 1760).toInt()
+			)
+		} else {
+			val miles = mi.toInt()
+			val yards = ((mi - miles) * 1760).toInt()
+			val miString = context.resources.getQuantityString(R.plurals.distance_in_mi_cd, miles)
+			val ydString = context.resources.getQuantityString(R.plurals.distance_in_yd_cd, yards)
+			if (yards > 0) {
+				context.getString(R.string.distance_in_two_units_cd, miString, ydString)
+			} else {
+				miString
+			}
+		}
+}
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/units/metric.kt b/app/src/main/java/xyz/apiote/bimba/czwek/units/metric.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2f410632b40b89c76a02ad9d6e1f33097c15970e
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/units/metric.kt
@@ -0,0 +1,109 @@
+package xyz.apiote.bimba.czwek.units
+
+import android.content.Context
+import xyz.apiote.bimba.czwek.R
+import java.text.NumberFormat
+
+object Metric : UnitSystem(10) {
+
+	override fun timeUnit(count: Long): TimeUnit = Second(count.toDouble())
+	override fun timeUnit(other: TimeUnit): TimeUnit = Second(other)
+
+	override fun speedUnit(count: Double): SpeedUnit = Kmph(count)
+	override fun speedUnit(other: SpeedUnit): SpeedUnit = Kmph(other)
+
+	override fun distanceUnit(count: Double): DistanceUnit = Km(count)
+	override fun distanceUnit(other: DistanceUnit): DistanceUnit = Km(other)
+
+	override fun toString(context: Context, s: SpeedUnit): String = s.toString(context, base)
+	override fun toString(context: Context, t: TimeUnit): String = t.toString(context, base)
+	override fun toString(context: Context, d: DistanceUnit): String = d.toString(context, base)
+}
+
+class Km(val km: Double) : DistanceUnit {
+	constructor(other: DistanceUnit) : this(other.meters() / 1000)
+
+	override fun meters(): Double = km * 1000
+	override fun toString(context: Context, base: Int): String =
+		if (km < 1) {
+			context.getString(
+				R.string.distance_in_m,
+				NumberFormat.getIntegerInstance().format((km * 1000).toInt())
+			)
+		} else {
+			context.getString(
+				R.string.distance_in_km,
+				NumberFormat.getInstance().apply { maximumFractionDigits = 2 }.format(km)
+			)
+		}
+
+	override fun contentDescription(context: Context, base: Int): String =
+		if (km < 1) {
+			context.resources.getQuantityString(R.plurals.distance_in_m_cd, (km * 1000).toInt())
+		} else {
+			val kilometres = km.toInt()
+			val metres = ((km - kilometres) * 1000).toInt()
+			val kmString = context.resources.getQuantityString(R.plurals.distance_in_km_cd, kilometres)
+			val mString = context.resources.getQuantityString(R.plurals.distance_in_m_cd, metres)
+			if (metres > 0) {
+				context.getString(R.string.distance_in_two_units_cd, kmString, mString)
+			} else {
+				kmString
+			}
+		}
+}
+
+class Metre(val m: Double) : DistanceUnit {
+	override fun meters(): Double = m
+
+	override fun toString(context: Context, base: Int): String =
+		context.getString(R.string.distance_in_m, NumberFormat.getIntegerInstance().format(m.toInt()))
+
+	override fun contentDescription(context: Context, base: Int): String =
+		context.resources.getQuantityString(R.plurals.distance_in_m_cd, m.toInt(), m.toInt())
+}
+
+class Second(val seconds: Double) : TimeUnit {
+	constructor(s: Int) : this(s.toDouble())
+	constructor(other: TimeUnit) : this((other.milliseconds().toDouble() / 1000))
+
+	override fun milliseconds(): Long = (seconds * 1000).toLong()
+	override fun toString(context: Context, base: Int): String =
+		context.getString(
+			R.string.time_in_s,
+			NumberFormat.getInstance().apply { maximumFractionDigits = 2 }.format(seconds)
+		)
+
+	override fun contentDescription(context: Context, base: Int): String =
+		context.resources.getQuantityString(R.plurals.time_in_s_cd, seconds.toInt(), seconds)
+}
+
+class Mps(val mps: Double) : SpeedUnit {
+	constructor(s: Int) : this(s.toDouble())
+	constructor(other: SpeedUnit) : this(other.mps())
+
+	override fun mps(): Double = mps
+	override fun toString(context: Context, base: Int): String =
+		context.getString(
+			R.string.speed_in_m_per_s,
+			NumberFormat.getInstance().apply { maximumFractionDigits = 2 }.format(mps)
+		)
+
+	override fun contentDescription(context: Context, base: Int): String =
+		context.resources.getQuantityString(R.plurals.speed_in_m_per_s_cd, mps.toInt(), mps)
+}
+
+class Kmph(val kmph: Double) : SpeedUnit {
+	constructor(s: Int) : this(s.toDouble())
+	constructor(other: SpeedUnit) : this(other.mps() * 3.6)
+
+	override fun mps(): Double = kmph / 3.6
+	override fun toString(context: Context, base: Int): String =
+		context.getString(
+			R.string.speed_in_km_per_h,
+			NumberFormat.getInstance().apply { maximumFractionDigits = 2 }.format(kmph)
+		)
+
+	override fun contentDescription(context: Context, base: Int): String =
+		context.resources.getQuantityString(R.plurals.speed_in_km_per_h_cd, kmph.toInt(), kmph.toInt())
+}
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/units/usCustomary.kt b/app/src/main/java/xyz/apiote/bimba/czwek/units/usCustomary.kt
new file mode 100644
index 0000000000000000000000000000000000000000..eb5013c9735a7166f5f822311799e00b4beb072d
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/units/usCustomary.kt
@@ -0,0 +1,53 @@
+package xyz.apiote.bimba.czwek.units
+
+import android.content.Context
+import xyz.apiote.bimba.czwek.R
+import java.text.NumberFormat
+
+object USCustomary : UnitSystem(10) {
+	override fun timeUnit(count: Long): TimeUnit = Second(count.toDouble())
+	override fun timeUnit(other: TimeUnit): TimeUnit = Second(other)
+
+	override fun speedUnit(count: Double): SpeedUnit = Miph(count)
+	override fun speedUnit(other: SpeedUnit): SpeedUnit = Miph(other)
+
+	override fun distanceUnit(count: Double): DistanceUnit = MiFt(count)
+	override fun distanceUnit(other: DistanceUnit): DistanceUnit = MiFt(other)
+
+	override fun toString(context: Context, s: SpeedUnit): String = s.toString(context, base)
+	override fun toString(context: Context, t: TimeUnit): String = t.toString(context, base)
+	override fun toString(context: Context, d: DistanceUnit): String = d.toString(context, base)
+}
+
+class MiFt(val mi: Double) : DistanceUnit {
+	constructor(other: DistanceUnit) : this(other.meters() / 1609.344)
+
+	override fun meters(): Double = mi * 1609.344
+
+	override fun toString(context: Context, base: Int): String = if (mi < 1) {
+		context.getString(
+			R.string.distance_in_ft,
+			NumberFormat.getIntegerInstance().format((mi * 5280).toInt())
+		)
+	} else {
+		context.getString(
+			R.string.distance_in_mi,
+			NumberFormat.getInstance().apply { maximumFractionDigits = 2 }.format(mi)
+		)
+	}
+
+	override fun contentDescription(context: Context, base: Int): String =
+		if (mi < 1) {
+			context.resources.getQuantityString(R.plurals.distance_in_ft_cd, (mi * 5280).toInt())
+		} else {
+			val miles = mi.toInt()
+			val feet = ((mi - miles) * 5280).toInt()
+			val miString = context.resources.getQuantityString(R.plurals.distance_in_mi_cd, miles)
+			val ftString = context.resources.getQuantityString(R.plurals.distance_in_ft_cd, feet)
+			if (feet > 0) {
+				context.getString(R.string.distance_in_two_units_cd, miString, ftString)
+			} else {
+				miString
+			}
+		}
+}




diff --git a/app/src/main/res/drawable/arrow.xml b/app/src/main/res/drawable/arrow.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d985176695cd4a54bd596b14ad6b3595028fdff6
--- /dev/null
+++ b/app/src/main/res/drawable/arrow.xml
@@ -0,0 +1,15 @@
+<!--
+SPDX-FileCopyrightText: Google
+
+SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:tint="?attr/colorOnSurface"
+	android:viewportWidth="960"
+	android:viewportHeight="960">
+	<path
+		android:fillColor="@android:color/white"
+		android:pathData="M440,880L440,233L256,416L200,360L480,80L760,360L704,417L520,233L520,880L440,880Z" />
+</vector>
\ No newline at end of file




diff --git a/app/src/main/res/drawable/download.xml b/app/src/main/res/drawable/download.xml
new file mode 100644
index 0000000000000000000000000000000000000000..287b023df7d8e7aceb629b78d32c3048d63636aa
--- /dev/null
+++ b/app/src/main/res/drawable/download.xml
@@ -0,0 +1,16 @@
+<!--
+SPDX-FileCopyrightText: Google
+
+SPDX-License-Identifier: Apache-2.0
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:tint="?attr/colorOnSurface"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+	<path
+		android:fillColor="@android:color/white"
+		android:pathData="M5,20h14v-2H5V20zM19,9h-4V3H9v6H5l7,7L19,9z" />
+</vector>




diff --git a/app/src/main/res/drawable/download_black.xml b/app/src/main/res/drawable/download_black.xml
deleted file mode 100644
index 460eafe98b28c5f4230ff2d599cd098d373acb67..0000000000000000000000000000000000000000
--- a/app/src/main/res/drawable/download_black.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<!--
-SPDX-FileCopyrightText: Google
-
-SPDX-License-Identifier: Apache-2.0
--->
-
-<vector android:height="24dp" android:tint="#000000"
-    android:viewportHeight="24" android:viewportWidth="24"
-    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
-    <path android:fillColor="@android:color/white" android:pathData="M5,20h14v-2H5V20zM19,9h-4V3H9v6H5l7,7L19,9z"/>
-</vector>




diff --git a/app/src/main/res/drawable/geocoding.xml b/app/src/main/res/drawable/geocoding.xml
new file mode 100644
index 0000000000000000000000000000000000000000..79eaee48d8d2f42d73cb6bdb8e0ae88ff8183512
--- /dev/null
+++ b/app/src/main/res/drawable/geocoding.xml
@@ -0,0 +1,15 @@
+<!--
+SPDX-FileCopyrightText: Google
+
+SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:tint="?attr/colorOnSurface"
+	android:viewportWidth="960"
+	android:viewportHeight="960">
+	<path
+		android:fillColor="@android:color/white"
+		android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q626,80 735.5,171.5Q845,263 872,401L790,401Q771,328 721.5,270.5Q672,213 600,184L600,200Q600,233 576.5,256.5Q553,280 520,280L440,280L440,360Q440,377 428.5,388.5Q417,400 400,400L320,400L320,480L400,480L400,600L360,600L168,408Q165,426 162.5,444Q160,462 160,480Q160,611 252,705Q344,799 480,800L480,880ZM844,860L716,732Q695,744 671,752Q647,760 620,760Q545,760 492.5,707.5Q440,655 440,580Q440,505 492.5,452.5Q545,400 620,400Q695,400 747.5,452.5Q800,505 800,580Q800,607 792,631Q784,655 772,676L900,804L844,860ZM620,680Q662,680 691,651Q720,622 720,580Q720,538 691,509Q662,480 620,480Q578,480 549,509Q520,538 520,580Q520,622 549,651Q578,680 620,680Z" />
+</vector>
\ No newline at end of file




diff --git a/app/src/main/res/drawable/sync.xml b/app/src/main/res/drawable/sync.xml
new file mode 100644
index 0000000000000000000000000000000000000000..81dbed5fa3d03152beba444b8f9712edee421b84
--- /dev/null
+++ b/app/src/main/res/drawable/sync.xml
@@ -0,0 +1,17 @@
+<!--
+SPDX-FileCopyrightText: Google
+
+SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:tint="?attr/colorOnSurface"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+
+	<path
+		android:fillColor="@android:color/white"
+		android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z" />
+
+</vector>




diff --git a/app/src/main/res/drawable/units.xml b/app/src/main/res/drawable/units.xml
new file mode 100644
index 0000000000000000000000000000000000000000..65250dd0441defb5c597d591f4d9a9416857b4c6
--- /dev/null
+++ b/app/src/main/res/drawable/units.xml
@@ -0,0 +1,10 @@
+<!--
+SPDX-FileCopyrightText: Google
+
+SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorOnSurface" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M7,15H5.5v-4.5H4V9h3V15zM13.5,13.5h-3v-1h2c0.55,0 1,-0.45 1,-1V10c0,-0.55 -0.45,-1 -1,-1H9v1.5h3v1h-2c-0.55,0 -1,0.45 -1,1V15h4.5V13.5zM19.5,14v-4c0,-0.55 -0.45,-1 -1,-1H15v1.5h3v1h-2v1h2v1h-3V15h3.5C19.05,15 19.5,14.55 19.5,14z"/>
+    
+</vector>




diff --git a/app/src/main/res/font/yellowcircle8.otf b/app/src/main/res/font/yellowcircle8.otf
index 31e8706da286d9575e653938bfd72b7fdba1676d..bfd91f587ddefb3c1f81d7750fd41162c882ae6f 100644
Binary files a/app/src/main/res/font/yellowcircle8.otf and b/app/src/main/res/font/yellowcircle8.otf differ




diff --git a/app/src/main/res/layout/activity_server_chooser.xml b/app/src/main/res/layout/activity_server_chooser.xml
index dd8faff12d484f93ecc8905613b0fa90f86b2cf3..3fc4e19a1648219fa679015ac743c8fbd6b03030 100644
--- a/app/src/main/res/layout/activity_server_chooser.xml
+++ b/app/src/main/res/layout/activity_server_chooser.xml
@@ -55,7 +55,7 @@ 		android:id="@+id/button"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		android:layout_marginTop="16dp"
-		android:text="@string/bimba_server_continue_button"
+		android:text="@string/continue_"
 		app:layout_constraintEnd_toEndOf="@+id/token_field"
 		app:layout_constraintStart_toStartOf="@+id/token_field"
 		app:layout_constraintTop_toBottomOf="@+id/token_field" />




diff --git a/app/src/main/res/layout/preferences_switch_material.xml b/app/src/main/res/layout/preferences_switch_material.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9bd2f24892abc6f705c6ce92d4392c206e2a9855
--- /dev/null
+++ b/app/src/main/res/layout/preferences_switch_material.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+SPDX-FileCopyrightText: Adam Evyčędo
+
+SPDX-License-Identifier: GPL-3.0-or-later
+-->
+
+<!-- Derived from https://github.com/androidx/androidx/blob/005e9694795cee9a42375d80b0d813af9e700ac1/preference/preference/res/layout/preference_widget_switch_compat.xml -->
+<com.google.android.material.materialswitch.MaterialSwitch xmlns:android="http://schemas.android.com/apk/res/android"
+	android:id="@+id/switchWidget"
+	android:layout_width="wrap_content"
+	android:layout_height="wrap_content"
+	android:background="@null"
+	android:clickable="false"
+	android:focusable="false" />
\ No newline at end of file




diff --git a/app/src/main/res/layout/result.xml b/app/src/main/res/layout/result.xml
index ca09e81118eadcdf7ab34fed534d7e84c4169680..e7dd45553043a5ffad0cfceb1ed5fc71ace8b0b6 100644
--- a/app/src/main/res/layout/result.xml
+++ b/app/src/main/res/layout/result.xml
@@ -24,7 +24,32 @@ 		app:layout_constraintTop_toTopOf="@+id/suggestion_title"
 		tool:ignore="ContentDescription"
 		tool:src="@drawable/vehicle_black" />
 
-	<!-- todo maxWidth or separate layout for graphView -->
+	<ImageView
+		android:id="@+id/arrow"
+		android:layout_width="24dp"
+		android:layout_height="24dp"
+		android:visibility="gone"
+		android:layout_marginStart="11dp"
+		android:layout_marginTop="8dp"
+		android:layout_marginEnd="8dp"
+		android:importantForAccessibility="no"
+		app:layout_constraintEnd_toEndOf="@+id/distance"
+		app:layout_constraintStart_toStartOf="@+id/distance"
+		app:layout_constraintTop_toTopOf="parent"
+		tool:src="@drawable/arrow" />
+
+	<com.google.android.material.textview.MaterialTextView
+		android:id="@+id/distance"
+		android:visibility="gone"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_marginTop="4dp"
+		android:layout_marginEnd="8dp"
+		android:textAppearance="@style/TextAppearance.Material3.BodySmall"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintTop_toBottomOf="@+id/arrow"
+		tool:text="650m" />
+
 	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/suggestion_title"
 		android:maxWidth="320dp"
@@ -47,14 +72,17 @@ 		app:layout_constraintStart_toStartOf="@+id/suggestion_title"
 		app:layout_constraintTop_toBottomOf="@+id/suggestion_title"
 		tool:text="TfL London" />
 
+	<!-- todo maxWidth or separate layout for graphView -->
 	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/suggestion_description"
 		style="@style/Theme.Bimba.SearchResult.Description"
-		android:layout_width="wrap_content"
+		android:layout_width="0dp"
 		android:layout_height="wrap_content"
 		android:maxWidth="360dp"
 		android:layout_marginTop="4dp"
+		android:layout_marginEnd="4dp"
 		app:layout_constraintBottom_toBottomOf="parent"
+		app:layout_constraintEnd_toStartOf="@+id/distance"
 		app:layout_constraintStart_toStartOf="@+id/suggestion_title"
 		app:layout_constraintTop_toBottomOf="@+id/feed_name"
 		tool:text="Metropolitan » Baker Street, Tower Hill The Monument, Westminster, Piccadilly Circus, Oxford Street" />




diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b5b5089cad3cc4480dae8e346bc72ab4d18096c9
--- /dev/null
+++ b/app/src/main/res/layout/settings_activity.xml
@@ -0,0 +1,9 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent">
+
+	<FrameLayout
+		android:id="@+id/settings"
+		android:layout_width="match_parent"
+		android:layout_height="match_parent" />
+</LinearLayout>
\ No newline at end of file




diff --git a/app/src/main/res/menu/drawer.xml b/app/src/main/res/menu/drawer.xml
index 6aa4cd40617cbc62655fc57af593549f6d4016a1..1d876c0cf374716191449dcfaf664152687c4623 100644
--- a/app/src/main/res/menu/drawer.xml
+++ b/app/src/main/res/menu/drawer.xml
@@ -25,6 +25,9 @@ 		
 	</item>
 	<!-- other settings -->
 	<item
+	android:id="@+id/drawer_settings"
+	android:title="@string/title_settings"/>
+	<item
 		android:id="@+id/drawer_about"
 		android:title="@string/title_about"/>
 </menu>
\ No newline at end of file




diff --git a/app/src/main/res/navigation/front_navigation.xml b/app/src/main/res/navigation/front_navigation.xml
index 1c383d702f4123fb51c68712cce1ca3139ace99a..1049aa1728a3a4a9591aad523c1527025415d860 100644
--- a/app/src/main/res/navigation/front_navigation.xml
+++ b/app/src/main/res/navigation/front_navigation.xml
@@ -27,6 +27,6 @@
 	<fragment
 		android:id="@+id/navigation_voyage"
 		android:name="xyz.apiote.bimba.czwek.dashboard.ui.voyage.VoyageFragment"
-		android:label="@string/title_voyage"
+		android:label="@string/title_journey"
 		tool:layout="@layout/fragment_voyage" />
 </navigation>
\ No newline at end of file




diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3b5e70a97a94517eebe0bf7f8d1b7273fd45f931
--- /dev/null
+++ b/app/src/main/res/values/arrays.xml
@@ -0,0 +1,49 @@
+<resources>
+	<string-array name="unit_entries">
+		<item>@string/units_locale_based</item>
+		<item>@string/units_metric</item>
+		<item>@string/units_imperial</item>
+		<item>@string/units_customary</item>
+		<item>@string/units_tgm10</item>
+		<item>@string/units_tgm12</item>
+	</string-array>
+
+	<string-array name="unit_values">
+		<item>locale</item>
+		<item>metric</item>
+		<item>imperial</item>
+		<item>customary</item>
+		<item>tgm10</item>
+		<item>tgm12</item>
+	</string-array>
+
+	<string-array name="dozenal_digits">
+		<item>@string/zero</item>
+		<item>@string/one</item>
+		<item>@string/two</item>
+		<item>@string/three</item>
+		<item>@string/four</item>
+		<item>@string/five</item>
+		<item>@string/six</item>
+		<item>@string/seven</item>
+		<item>@string/eight</item>
+		<item>@string/nine</item>
+		<item>@string/ten</item>
+		<item>@string/elv</item>
+	</string-array>
+
+	<string-array name="dozenal_multipliers">
+		<item/>
+		<item>@string/zen</item>
+		<item>@string/duna</item>
+		<item>@string/trin</item>
+		<item>@string/quedra</item>
+		<item>@string/quen</item>
+		<item>@string/hes</item>
+		<item>@string/sev</item>
+		<item>@string/ak</item>
+		<item>@string/neen</item>
+		<item>@string/dex</item>
+		<item>@string/lef</item>
+	</string-array>
+</resources>
\ No newline at end of file




diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 19afd0e9e16147ad1463025d7342a239b206452a..8e5ca43fe47402d84cefdf881ac378ccd664757d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,18 +1,20 @@
 <!--
-SPDX-FileCopyrightText: Adam Evyčędo
+SPDX-FileCopyrightText: Adam Evyčędo and contributors using Weblate
 
 SPDX-License-Identifier: GPL-3.0-or-later
 -->
 
-<resources>
+<!-- NOTE base strings are in en-GB -->
+
+<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
 	<string name="app_name">Bimba</string>
 	<string name="title_home">Home</string>
 	<string name="title_map">Map</string>
-	<string name="title_voyage">Voyage</string>
+	<string name="title_journey">Journey</string>
 	<string name="home_fab_description">GPS icon</string>
 	<string name="search_placeholder">stops, lines, or plus codes</string>
 	<string name="title_activity_results">Results</string>
-	<string name="cont">Continue</string>
+	<string name="continue_">Continue</string>
 	<string name="save">Save</string>
 	<string name="error_400">The application made a malformed request</string>
 	<string name="error_401">A token is needed to use this server</string>
@@ -21,13 +23,95 @@ 	Not found
 	<string name="error_429">Rate limit exceeded. Try again later</string>
 	<string name="error_50x">There was an error on the sever. Try again later</string>
 	<string name="error_unknown">Unknown error happened</string>
-	<string name="error_connecting">Error connecting to the server. Try again later</string> <!-- send a bug report to bimba@git.apiote.xyz, details are: url=$URL, response=$response -->
+	<!-- send a bug report to bimba@git.apiote.xyz, details are: url=$URL, response=$response -->
+	<string name="error_connecting">Error connecting to the server. Try again later</string>
 	<string name="error_offline">You are offline. Connect to the Internet</string>
 	<string name="error_gps">Cannot obtain current location</string>
 	<string name="no_departures">No departures</string>
 	<string name="waiting_position">waiting for position</string>
 	<string name="vehicle_headsign">%1$s » %2$s</string>
-	<string name="speed_in_km_per_h">%1$.3f km/h</string>
+	<string name="distance_in_m">%1$s m</string>
+	<plurals name="distance_in_m_cd">
+		<item quantity="one">%1$d metre</item>
+		<item quantity="other">%1$d metres</item>
+	</plurals>
+	<string name="distance_in_km">%1$s km</string>
+	<plurals name="distance_in_km_cd">
+		<item quantity="one">%1$d kilometre</item>
+		<item quantity="other">%1$d kilometres</item>
+	</plurals>
+	<string name="distance_in_two_units_cd">%1$s and %2$s</string>
+	<string name="distance_in_yd">%1$s yd</string>
+	<plurals name="distance_in_yd_cd">
+		<item quantity="one">%1$d yard</item>
+		<item quantity="other">%1$d yards</item>
+	</plurals>
+	<string name="distance_in_ft">%1$s ft</string>
+	<plurals name="distance_in_ft_cd">
+		<item quantity="one">%1$d foot</item>
+		<item quantity="other">%1$d feet</item>
+	</plurals>
+	<string name="distance_in_mi">%1$s mi</string>
+	<plurals name="distance_in_mi_cd">
+		<item quantity="one">%1$d mile</item>
+		<item quantity="other">%1$d miles</item>
+	</plurals>
+	<string name="distance_in_gf">%1$s %2$sGf</string>
+	<plurals name="distance_in_gf_cd">
+		<item quantity="one">%1$d grafut</item>
+		<item quantity="other">%1$d grafuts</item>
+	</plurals>
+	<plurals name="distance_in_3gf_12_cd">
+		<item quantity="one">%1$s trinagrafut</item>
+		<item quantity="other">%1$s trinagrafuts</item>
+	</plurals>
+	<plurals name="distance_in_gf_12_cd">
+		<item quantity="one">%1$s grafut</item>
+		<item quantity="other">%1$s grafuts</item>
+	</plurals>
+	<string name="time_in_s">%1$s s</string>
+	<plurals name="time_in_s_cd">
+		<item quantity="one">%1$d second</item>
+		<item quantity="other">%1$d seconds</item>
+	</plurals>
+	<string name="time_in_tm">%1$s %2$sTm</string>
+	<string name="time_in_tm_past">%1$s %2$sTm ago</string>
+	<plurals name="time_in_tm_cd">
+		<item quantity="one">%1$d tim</item>
+		<item quantity="other">%1$d tims</item>
+	</plurals>
+	<plurals name="time_in_4tm_12_cd">
+		<item quantity="one">%1$s quedratim</item>
+		<item quantity="other">%1$s quedratims</item>
+	</plurals>
+	<plurals name="time_in_2tm_12_cd">
+		<item quantity="one">%1$s dunatim</item>
+		<item quantity="other">%1$s dunatims</item>
+	</plurals>
+	<string name="speed_in_km_per_h">%1$s km/h</string>
+	<string name="speed_in_m_per_s">%1$s m/s</string>
+	<string name="speed_in_mi_per_h">%1$s mph</string>
+	<string name="speed_in_vl">%1$s Vl</string>
+	<plurals name="speed_in_m_per_s_cd">
+		<item quantity="one">%1$d meter per second</item>
+		<item quantity="other">%1$d meters per second</item>
+	</plurals>
+	<plurals name="speed_in_km_per_h_cd">
+		<item quantity="one">%1$d kilometer per hour</item>
+		<item quantity="other">%1$d kilometers per hour</item>
+	</plurals>
+	<plurals name="speed_in_mi_per_h_cd">
+		<item quantity="one">%1$d mile per hour</item>
+		<item quantity="other">%1$d mile per hour</item>
+	</plurals>
+	<plurals name="speed_in_vl_cd">
+		<item quantity="one">%1$d vlos</item>
+		<item quantity="other">%1$d vlos</item>
+	</plurals>
+	<plurals name="speed_in_vl_12_cd">
+		<item quantity="one">%1$s vlos</item>
+		<item quantity="other">%1$s vlos</item>
+	</plurals>
 	<string name="congestion_unknown">unknown</string>
 	<string name="congestion_smooth">smooth</string>
 	<string name="congestion_stop_and_go">stop and go</string>
@@ -61,7 +145,6 @@ 	Stops nearby
 	<string name="results_for">Results for ‘%1$s’</string>
 	<string name="bimba_server_address_hint">Server</string>
 	<string name="bimba_server_token_hint">Token</string>
-	<string name="bimba_server_continue_button">Continue</string>
 	<string name="realtime_content_description">departure is realtime</string>
 	<string name="wheelchair_content_description">vehicle is wheelchair accessible</string>
 	<string name="air_condition_content_description">air conditioning</string>
@@ -72,7 +155,8 @@ 	USB charging
 	<string name="show_departures">Show departures</string>
 	<string name="open_in_maps_app">Open in maps app</string>
 	<string name="stop_content_description">stop</string>
-	<string name="seatbelts_everyone">Seatbelts, everyone!</string> <!-- taken from ‘Magic School Bus’. Should be translated like in the series. It’s the first words of the intro song -->
+	<!-- taken from ‘Magic School Bus’. Should be translated like in the series. It’s the first words of the intro song -->
+	<string name="seatbelts_everyone">Seatbelts, everyone!</string>
 	<string name="onboarding_question">How would you like to start?</string>
 	<string name="onboarding_simple">Simple</string>
 	<string name="onboarding_simple_action">choose localities</string>
@@ -90,7 +174,6 @@ 	Localities
 	<string name="error_url">Malformed URL provided</string>
 	<string name="error_traffic_spec">Cannot verify server</string>
 	<string name="stops_near_code">Stops near %1$s</string>
-	<string name="code_is_not_full">Code is not full</string>
 	<string name="choose_server">Choose server flavour</string>
 	<string name="ok">OK</string>
 	<string name="no_location_access">Location access not given</string>
@@ -100,10 +183,10 @@ 	Stop on demand
 	<string name="stop_stub_in_zone">Stop in zone %1$s</string>
 	<string name="vehicle_headsign_content_description">%1$s towards %2$s</string>
 	<string name="departure_headsign">» %1$s</string>
-	<string name="credits">Font yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) based on Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans\n\n Mastodon icon (https://github.com/mastodon/joinmastodon) © Mastodon contributors, AGPL-3.0-or-later\n\n Bimba logo created by https://github.com/tebriz159\n\n Material icons © Google, Apache-2.0\n\n Map data © OpenStreetMap contributors (https://www.openstreetmap.org/copyright), ODbL-1.0</string>
+	<string name="credits">Font yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) based on Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans\n\n Mastodon icon (https://github.com/mastodon/joinmastodon) © Mastodon contributors, AGPL-3.0-or-later\n\n Bimba logo created by https://github.com/tebriz159\n\n Material icons © Google, Apache-2.0\n\n Map data © OpenStreetMap contributors (https://www.openstreetmap.org/copyright), ODbL-1.0\n\n Cities list used for geocoding short plus codes © Geonames (https://geonames.org), CC BY</string>
 	<string name="title_about">About</string>
 	<string name="translation_button_description">link to translations service</string>
-	<string name="app_description">FLOSS public transport passenger companion; a timetable in your pocket.</string>
+	<string name="app_description">FLOSS public transport passenger companion; a timetable in your pocket.</string>
 	<string name="website_button_description">link to website</string>
 	<string name="code_button_description">link to source code</string>
 	<string name="mastodon_button_description">link to Mastodon</string>
@@ -114,7 +197,6 @@ 	App version is not compatible with the server
 	<string name="filter_localities">filter localities</string>
 	<string name="error_41">This locality is not supported by the server</string>
 	<string name="stop_from_qr_code">QR code stop</string>
-	<string name="departures_snackbar">Last update: %1$s</string>
 	<string name="title_select_date">Select day of departures</string>
 	<string name="title_select_line">Select line</string>
 	<string name="clear_date_selection">Clear</string>
@@ -131,10 +213,50 @@ 	Favourite
 	<string name="filtered">Filtered</string>
 	<string name="unfiltered">Unfiltered</string>
 	<string name="cannot_save_favourite">Couldn’t save the favourite</string>
-	<string name="no_next_departures">no more departures</string>
 	<string name="error_44">No more departures</string>
 	<string name="loading">loading…</string>
 	<string name="favourite_deleted">Favourite deleted</string>
 	<string name="undo">Undo</string>
-
+	<string name="units_title">Unit system</string>
+	<string name="units_locale_based">Locale based</string>
+	<string name="units_metric">Metric (SI)</string>
+	<string name="units_imperial">Imperial (UK)</string>
+	<string name="units_customary">US Customary</string>
+	<string name="units_tgm10">TGM (base 10)</string>
+	<string name="units_tgm12">TGM (base 12)</string>
+	<string name="title_settings">Settings</string>
+	<string name="no_geocoding_data">No geocoding data</string>
+	<string name="no_geocoding_data_description">The query contains a short plus code but there is no geocoding data present. Download geocoding data or enable auto updating in settings.</string>
+	<string name="error_geocoding">City not found</string>
+	<string name="cities_channel_name">Cities update channel</string>
+	<string name="cities_channel_description">Notifications showing progress of updating geocoding local data</string>
+	<string name="saving_cities_list">saving cities list</string>
+	<string name="updating_geocoding_data">Updating geocoding data</string>
+	<string name="downloading_cities_list">downloading cities list</string>
+	<string name="finished_updating_geocoding_data">Finished updating geocoding data</string>
+	<string name="updating_geocoding_data_failed">Updating geocoding data failed</string>
+	<string name="zero">zero</string>
+	<string name="one">one</string>
+	<string name="two">two</string>
+	<string name="three">three</string>
+	<string name="four">four</string>
+	<string name="five">five</string>
+	<string name="six">six</string>
+	<string name="seven">seven</string>
+	<string name="eight">eight</string>
+	<string name="nine">nine</string>
+	<string name="ten">ten</string>
+	<string name="elv">elv</string>
+	<string name="zen">zen</string>
+	<string name="duna">duna</string>
+	<string name="quedra">quedra</string>
+	<string name="trin">trin</string>
+	<string name="quen">quen</string>
+	<string name="hes">hes</string>
+	<string name="sev">sev</string>
+	<string name="ak">ak</string>
+	<string name="neen">neen</string>
+	<string name="dex">dex</string>
+	<string name="lef">lef</string>
+	<string name="filtered_stop_question">Do you want to save a favourite filtered with selected lines?</string>
 </resources>




diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 2ed4f97f64d1cafcc78e7558ca4486830dc8c3ea..042048b10e8ca72c5956032013ac943358cfae06 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -43,7 +43,7 @@ 		.2
 	</style>
 
 	<declare-styleable name="Theme.Bimba">
-		<attr name="randomColourLightness" format="float"/>
+		<attr name="randomColourLightness" format="float" />
 		<attr name="lightStatusBar" format="boolean" />
 	</declare-styleable>
 
@@ -73,5 +73,9 @@
 	<style name="roundedImageView" parent="">
 		<item name="cornerFamily">rounded</item>
 		<item name="cornerSize">24dp</item>
+	</style>
+
+	<style name="Preference.SwitchPreferenceCompat" parent="@style/Preference.SwitchPreferenceCompat.Material" tool:ignore="ResourceCycle">
+		<item name="widgetLayout">@layout/preferences_switch_material</item>
 	</style>
 </resources>
\ No newline at end of file




diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 3b2cafc7373811872ce3e4dc2a3e795908a0e2a9..0cd1a0dacf54562ef83c5f01f304f305c18a6ec6 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -8,9 +8,9 @@     Örtlichkeiten filtern
     <string name="title_map">Karte</string>
     <string name="home_fab_description">GPS-Symbol</string>
     <string name="title_activity_results">Ergebnisse</string>
-    <string name="cont">Fortsetzen</string>
+    <string name="continue_">Fortsetzen</string>
     <string name="vehicle_headsign">%1$s » %2$s</string>
-    <string name="speed_in_km_per_h">%1$.3f km/h</string>
+    <string name="speed_in_km_per_h">%1$s km/h</string>
     <string name="congestion_unknown">unbekannt</string>
     <string name="congestion_congestion">Stau</string>
     <string name="occupancy_unknown">unbekannt</string>
@@ -28,7 +28,6 @@     %1$s «» %2$s
     <string name="line_headsigns_content_description">zwischen %1$s und %2$s</string>
     <string name="bimba_server_address_hint">Server</string>
     <string name="bimba_server_token_hint">Token</string>
-    <string name="bimba_server_continue_button">Fortsetzen</string>
     <string name="bicycles_allowed_content_description">Fahrräder erlaubt</string>
     <string name="voice_announcements_content_description">Sprachansagen</string>
     <string name="usb_charging_content_description">USB-Aufladen</string>
@@ -47,11 +46,144 @@     Link zu Übersetzungsdienst
     <string name="code_button_description">Link zu Quellcode</string>
     <string name="mastodon_button_description">Link zu Mastodon</string>
     <string name="app_name">Bimba</string>
-    <string name="title_voyage">Reise</string>
+    <string name="title_journey">Reise</string>
     <string name="save">Speichern</string>
     <string name="error_404">Nicht gefunden</string>
     <string name="at_time_realtime">um %1$02d:%2$02d:%3$02d</string>
     <string name="air_condition_content_description">Klimatisierung</string>
     <string name="error">Fehler</string>
     <string name="title_servers">Server</string>
+    <string name="time_in_s">%1$s s</string>
+    <string name="time_in_tm">%1$s %2$sTm</string>
+    <string name="time_in_tm_past">vor %1$s %2$sTm</string>
+    <string name="speed_in_m_per_s">%1$s m/s</string>
+    <string name="speed_in_mi_per_h">%1$s mph</string>
+    <string name="speed_in_vl">%1$s Vl</string>
+    <plurals name="speed_in_mi_per_h_cd">
+        <item quantity="one">%1$d meile pro stunde</item>
+        <item quantity="other">%1$d meilen pro stunde</item>
+    </plurals>
+    <plurals name="distance_in_3gf_12_cd">
+        <item quantity="one">%1$s trinagrafuß</item>
+        <item quantity="other">%1$s trinagrafüße</item>
+    </plurals>
+    <plurals name="distance_in_gf_12_cd">
+        <item quantity="one">%1$s grafuß</item>
+        <item quantity="other">%1$s grafüße</item>
+    </plurals>
+    <plurals name="time_in_s_cd">
+        <item quantity="one">%1$d sekunde</item>
+        <item quantity="other">%1$d sekunden</item>
+    </plurals>
+    <plurals name="time_in_tm_cd">
+        <item quantity="one">%1$d tim</item>
+        <item quantity="other">%1$d tims</item>
+    </plurals>
+    <plurals name="time_in_4tm_12_cd">
+        <item quantity="one">%1$s quedratim</item>
+        <item quantity="other">%1$s quedratims</item>
+    </plurals>
+    <plurals name="time_in_2tm_12_cd">
+        <item quantity="one">%1$s dunatim</item>
+        <item quantity="other">%1$s dunatims</item>
+    </plurals>
+    <plurals name="speed_in_m_per_s_cd">
+        <item quantity="one">%1$d meter pro sekunde</item>
+        <item quantity="other">%1$d meter pro sekunde</item>
+    </plurals>
+    <plurals name="speed_in_km_per_h_cd">
+        <item quantity="one">%1$d kilometer pro stunde</item>
+        <item quantity="other">%1$d kilometer pro stunde</item>
+    </plurals>
+    <plurals name="speed_in_vl_cd">
+        <item quantity="one">%1$d vlos</item>
+        <item quantity="other">%1$d vlos</item>
+    </plurals>
+    <plurals name="speed_in_vl_12_cd">
+        <item quantity="one">%1$s vlos</item>
+        <item quantity="other">%1$s vlos</item>
+    </plurals>
+    <string name="departure_momentarily">gleich</string>
+    <string name="departure_departed">abgefahren</string>
+    <string name="title_home">Start</string>
+    <string name="search_placeholder">Haltestellen, Linien oder Plus Codes</string>
+    <string name="occupancy_wont_let">wird nicht reinlassen</string>
+    <string name="stops_nearby">Haltestellen in der Nähe</string>
+    <string name="results_for">Ergebnisse für „%1$s“</string>
+    <string name="stop_content_description">Haltestelle</string>
+    <string name="stops_near_code">Haltestellen in der Nähe von %1$s</string>
+    <plurals name="distance_in_gf_cd">
+        <item quantity="one">%1$d grafuß</item>
+        <item quantity="other">%1$d grafüße</item>
+    </plurals>
+    <string name="distance_in_m">%1$s m</string>
+    <plurals name="distance_in_m_cd">
+        <item quantity="one">%1$d meter</item>
+        <item quantity="other">%1$d meter</item>
+    </plurals>
+    <string name="distance_in_km">%1$s km</string>
+    <plurals name="distance_in_km_cd">
+        <item quantity="one">%1$d kilometer</item>
+        <item quantity="other">%1$d kilometer</item>
+    </plurals>
+    <string name="distance_in_two_units_cd">%1$s und %2$s</string>
+    <string name="distance_in_yd">%1$s yd</string>
+    <plurals name="distance_in_yd_cd">
+        <item quantity="one">%1$d yard</item>
+        <item quantity="other">%1$d yard</item>
+    </plurals>
+    <string name="distance_in_ft">%1$s ft</string>
+    <plurals name="distance_in_ft_cd">
+        <item quantity="one">%1$d fuß</item>
+        <item quantity="other">%1$d füße</item>
+    </plurals>
+    <string name="distance_in_mi">%1$s mi</string>
+    <plurals name="distance_in_mi_cd">
+        <item quantity="one">%1$d meile</item>
+        <item quantity="other">%1$d meilen</item>
+    </plurals>
+    <string name="distance_in_gf">%1$s %2$sGf</string>
+    <string name="app_description">Fahrgastbegleiter FLOSS für öffentlichen Verkehr: ein Fahrplan in deine Tasche.</string>
+    <string name="error_44">keine Abfahrten</string>
+    <string name="loading">wird geladen…</string>
+    <string name="units_metric">Metrisches (SI)</string>
+    <string name="units_customary">US-amerikanisches</string>
+    <string name="units_imperial">Kaiserliches (VK)</string>
+    <string name="units_tgm10">TGM (basis 10)</string>
+    <string name="title_settings">Einstellungen</string>
+    <string name="zero">null</string>
+    <string name="six">sechs</string>
+    <string name="eight">acht</string>
+    <string name="nine">neun</string>
+    <string name="ten">zehn</string>
+    <string name="elv">elf</string>
+    <string name="zen">zend</string>
+    <string name="duna">duna</string>
+    <string name="quedra">quedra</string>
+    <string name="trin">trin</string>
+    <string name="quen">quen</string>
+    <string name="hes">hes</string>
+    <string name="sev">sev</string>
+    <string name="ak">ak</string>
+    <string name="neen">neen</string>
+    <string name="dex">dechs</string>
+    <string name="lef">lef</string>
+    <string name="more">Mehr</string>
+    <string name="error_429">Ratenlimit überschritten. Versuch später</string>
+    <string name="error_offline">Du bist offline. Mit dem Internet verbind</string>
+    <string name="no_departures">Keine Abfahrten</string>
+    <string name="departure_headsign_content_description">zu %1$s</string>
+    <string name="show_departures">Abfahrten anzeigen</string>
+    <string name="rate_limit">Ratenlimit</string>
+    <string name="seatbelts_everyone">Legt die Gurte an!</string>
+    <string name="onboarding_question">Wie möchtest du beginnen?</string>
+    <string name="units_locale_based">Systembasiert</string>
+    <string name="units_title">Einheitensystem</string>
+    <string name="units_tgm12">TGM (basis 12)</string>
+    <string name="one">ein</string>
+    <string name="two">zwei</string>
+    <string name="three">drei</string>
+    <string name="four">vier</string>
+    <string name="five">fünf</string>
+    <string name="seven">sieben</string>
 </resources>
\ No newline at end of file




diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml
deleted file mode 100644
index 68de08ea5bcbaf161327f182bc53ee5445d4beb6..0000000000000000000000000000000000000000
--- a/app/src/main/res/values-en-rGB/strings.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-SPDX-FileCopyrightText: Adam Evyčędo and contributors using Weblate
-
-SPDX-License-Identifier: GPL-3.0-or-later
---><resources>
-    <string name="app_name">Bimba</string>
-    <string name="title_home">Home</string>
-    <string name="title_map">Map</string>
-    <string name="title_voyage">Journey</string>
-    <string name="title_activity_results">Results</string>
-    <string name="cont">Continue</string>
-    <string name="save">Save</string>
-</resources>
\ No newline at end of file




diff --git a/app/src/main/res/values-en-rUS/strings.xml b/app/src/main/res/values-en-rUS/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8afbdd1697a0c2c92063444971b7bb220891b8e1
--- /dev/null
+++ b/app/src/main/res/values-en-rUS/strings.xml
@@ -0,0 +1,269 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+SPDX-FileCopyrightText: Adam Evyčędo and contributors using Weblate
+
+SPDX-License-Identifier: GPL-3.0-or-later
+--><resources>
+    <string name="app_name">Bimba</string>
+    <string name="title_home">Home</string>
+    <string name="title_map">Map</string>
+    <string name="title_journey">Journey</string>
+    <string name="home_fab_description">GPS icon</string>
+    <string name="search_placeholder">stops, lines, or plus codes</string>
+    <string name="title_activity_results">Results</string>
+    <string name="continue_">Continue</string>
+    <string name="save">Save</string>
+    <string name="error_400">The application made a malformed request</string>
+    <string name="error_401">A token is needed to use this server</string>
+    <string name="error_403">The token you provided is incorrect</string>
+    <string name="error_404">Not found</string>
+    <string name="error_429">Rate limit exceeded. Try again later</string>
+    <string name="error_50x">There was an error on the sever. Try again later</string>
+    <string name="error_unknown">Unknown error happened</string>
+    <string name="error_connecting">Error connecting to the server. Try again later</string>
+    <!-- send a bug report to bimba@git.apiote.xyz, details are: url=$URL, response=$response -->
+	<string name="error_offline">You are offline. Connect to the Internet</string>
+    <string name="error_gps">Cannot obtain current location</string>
+    <string name="no_departures">No departures</string>
+    <string name="waiting_position">waiting for position</string>
+    <string name="vehicle_headsign">%1$s » %2$s</string>
+    <string name="distance_in_m">%1$s m</string>
+    <plurals name="distance_in_m_cd">
+        <item quantity="one">%1$d meter</item>
+        <item quantity="other">%1$d meters</item>
+    </plurals>
+    <string name="distance_in_km">%1$s km</string>
+    <plurals name="distance_in_km_cd">
+        <item quantity="one">%1$d kilometer</item>
+        <item quantity="other">%1$d kilometers</item>
+    </plurals>
+    <string name="time_in_s">%1$s s</string>
+    <plurals name="time_in_s_cd">
+        <item quantity="one">%1$d second</item>
+        <item quantity="other">%1$d seconds</item>
+    </plurals>
+    <string name="time_in_tm">%1$s %2$sTm</string>
+    <string name="time_in_tm_past">%1$s %2$sTm ago</string>
+    <plurals name="time_in_tm_cd">
+        <item quantity="one">%1$d tim</item>
+        <item quantity="other">%1$d tims</item>
+    </plurals>
+    <string name="speed_in_km_per_h">%1$s km/h</string>
+    <string name="speed_in_m_per_s">%1$s m/s</string>
+    <string name="speed_in_mi_per_h">%1$s mph</string>
+    <string name="speed_in_vl">%1$s Vl</string>
+    <plurals name="speed_in_m_per_s_cd">
+        <item quantity="one">%1$d meter per second</item>
+        <item quantity="other">%1$d meters per second</item>
+    </plurals>
+    <plurals name="speed_in_km_per_h_cd">
+        <item quantity="one">%1$d kilometer per hour</item>
+        <item quantity="other">%1$d kilometers per hour</item>
+    </plurals>
+    <plurals name="speed_in_mi_per_h_cd">
+        <item quantity="one">%1$d mile per hour</item>
+        <item quantity="other">%1$d mile per hour</item>
+    </plurals>
+    <plurals name="speed_in_vl_cd">
+        <item quantity="one">%1$d vlos</item>
+        <item quantity="other">%1$d vlos</item>
+    </plurals>
+    <string name="congestion_unknown">unknown</string>
+    <string name="congestion_smooth">smooth</string>
+    <string name="congestion_stop_and_go">stop and go</string>
+    <string name="congestion_congestion">congestion</string>
+    <string name="congestion_jams">jams</string>
+    <string name="occupancy_unknown">unknown</string>
+    <string name="occupancy_empty">empty</string>
+    <string name="occupancy_many_seats">many seats</string>
+    <string name="occupancy_few_seats">few seats</string>
+    <string name="occupancy_standing_only">standing only</string>
+    <string name="occupancy_crowded">crowded</string>
+    <string name="occupancy_full">full</string>
+    <string name="occupancy_wont_let">won’t let in</string>
+    <string name="no_map_app">No maps app installed</string>
+    <string name="departure_headsign_content_description">towards %1$s</string>
+    <string name="departure_momentarily">momentarily</string>
+    <string name="departure_departed">departed</string>
+    <string name="departure_now">now</string>
+    <string name="at_time">at %1$02d:%2$02d</string>
+    <string name="at_time_realtime">at %1$02d:%2$02d:%3$02d</string>
+    <string name="on_demand">on demand</string>
+    <string name="no_boarding">no boarding</string>
+    <string name="on_boarding">on-boarding</string>
+    <string name="off_boarding">off-boarding</string>
+    <string name="boarding">can board</string>
+    <string name="line_headsign">» %1$s</string>
+    <string name="line_headsign_content_description">towards %1$s</string>
+    <string name="line_headsigns">%1$s «» %2$s</string>
+    <string name="line_headsigns_content_description">between %1$s and %2$s</string>
+    <string name="stops_nearby">Stops nearby</string>
+    <string name="results_for">Results for ‘%1$s’</string>
+    <string name="bimba_server_address_hint">Server</string>
+    <string name="bimba_server_token_hint">Token</string>
+    <string name="realtime_content_description">departure is realtime</string>
+    <string name="wheelchair_content_description">vehicle is wheelchair accessible</string>
+    <string name="air_condition_content_description">air conditioning</string>
+    <string name="bicycles_allowed_content_description">bicycles allowed</string>
+    <string name="voice_announcements_content_description">voice announcements</string>
+    <string name="tickets_sold_content_description">tickets sold on board</string>
+    <string name="usb_charging_content_description">USB charging</string>
+    <string name="show_departures">Show departures</string>
+    <string name="open_in_maps_app">Open in maps app</string>
+    <string name="stop_content_description">stop</string>
+    <string name="seatbelts_everyone">Seatbelts, everyone!</string>
+    <!-- taken from ‘Magic School Bus’. Should be translated like in the series. It’s the first words of the intro song -->
+	<string name="onboarding_question">How would you like to start?</string>
+    <string name="onboarding_simple">Simple</string>
+    <string name="onboarding_simple_action">choose localities</string>
+    <string name="onboarding_advanced">Advanced</string>
+    <string name="onboarding_advanced_action">choose server</string>
+    <string name="cancel">Cancel</string>
+    <string name="error">Error</string>
+    <string name="rate_limit">Rate limit</string>
+    <string name="server_rate_limited_question">This server is rate-limited and no token was given. Do you want to continue?</string>
+    <string name="server_private_question">This server is private and no token was given</string>
+    <string name="last_update">Last update: %1$s</string>
+    <string name="title_feeds">Timetables</string>
+    <string name="title_servers">Servers</string>
+    <string name="title_cities">Localities</string>
+    <string name="error_url">Malformed URL provided</string>
+    <string name="error_traffic_spec">Cannot verify server</string>
+    <string name="stops_near_code">Stops near %1$s</string>
+    <string name="choose_server">Choose server flavour</string>
+    <string name="ok">OK</string>
+    <string name="no_location_access">Location access not given</string>
+    <string name="no_location_message">Permission to use location is needed to find nearby stops and show current position on map. Other features will work without it. It can be enabled and disabled in system settings any time.</string>
+    <string name="stop_stub_on_demand_in_zone">Stop on demand in zone %1$s</string>
+    <string name="stop_stub_on_demand">Stop on demand</string>
+    <string name="stop_stub_in_zone">Stop in zone %1$s</string>
+    <string name="vehicle_headsign_content_description">%1$s towards %2$s</string>
+    <string name="departure_headsign">» %1$s</string>
+    <string name="credits">Font yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) based on Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans
+\n
+\n Mastodon icon (https://github.com/mastodon/joinmastodon) © Mastodon contributors, AGPL-3.0-or-later
+\n
+\n Bimba logo created by https://github.com/tebriz159
+\n
+\n Material icons © Google, Apache-2.0
+\n
+\n Map data © OpenStreetMap contributors (https://www.openstreetmap.org/copyright), ODbL-1.0
+\n
+\n Cities list used for geocoding short plus codes © Geonames (https://geonames.org), CC BY</string>
+    <string name="title_about">About</string>
+    <string name="translation_button_description">link to translations service</string>
+    <string name="app_description">FLOSS public transport passenger companion; a timetable in your pocket.</string>
+    <string name="website_button_description">link to website</string>
+    <string name="code_button_description">link to source code</string>
+    <string name="mastodon_button_description">link to Mastodon</string>
+    <string name="use_online_feed">Use online feed</string>
+    <string name="information_may_be_outdated">Information may be outdated</string>
+    <string name="current_timetable_validity">Current timetable valid: %1$s to %2$s</string>
+    <string name="error_406">App version is not compatible with the server</string>
+    <string name="filter_localities">filter localities</string>
+    <string name="error_41">This locality is not supported by the server</string>
+    <string name="stop_from_qr_code">QR code stop</string>
+    <string name="title_select_date">Select day of departures</string>
+    <string name="title_select_line">Select line</string>
+    <string name="clear_date_selection">Clear</string>
+    <string name="title_filter">Filter</string>
+    <string name="title_filter_byline">Filter by line</string>
+    <string name="title_filter_bytime">Filter by time</string>
+    <string name="title_select_time_start">Select start time</string>
+    <string name="title_select_time_end">Select end time</string>
+    <string name="more">More</string>
+    <string name="alert_header">Status updates</string>
+    <string name="map_attribution"><![CDATA[© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors]]></string>
+    <string name="favourite_content_description">Save as favorite</string>
+    <string name="favourite">Favorite</string>
+    <string name="filtered">Filtered</string>
+    <string name="unfiltered">Unfiltered</string>
+    <string name="cannot_save_favourite">Couldn’t save the favorite</string>
+    <string name="error_44">No more departures</string>
+    <string name="loading">loading…</string>
+    <string name="favourite_deleted">Favorite deleted</string>
+    <string name="undo">Undo</string>
+    <string name="units_title">Unit system</string>
+    <string name="units_locale_based">Locale based</string>
+    <string name="units_metric">Metric (SI)</string>
+    <string name="units_imperial">Imperial (UK)</string>
+    <string name="units_customary">US Customary</string>
+    <string name="units_tgm10">TGM (base 10)</string>
+    <string name="units_tgm12">TGM (base 12)</string>
+    <string name="title_settings">Settings</string>
+    <string name="no_geocoding_data">No geocoding data</string>
+    <string name="no_geocoding_data_description">The query contains a short plus code but there is no geocoding data present. Download geocoding data or enable auto updating in settings.</string>
+    <string name="error_geocoding">City not found</string>
+    <string name="cities_channel_name">Cities update channel</string>
+    <string name="cities_channel_description">Notifications showing progress of updating geocoding local data</string>
+    <string name="saving_cities_list">saving cities list</string>
+    <string name="updating_geocoding_data">Updating geocoding data</string>
+    <string name="downloading_cities_list">downloading cities list</string>
+    <string name="finished_updating_geocoding_data">Finished updating geocoding data</string>
+    <string name="updating_geocoding_data_failed">Updating geocoding data failed</string>
+    <string name="zero">zero</string>
+    <string name="one">one</string>
+    <string name="two">two</string>
+    <string name="three">three</string>
+    <string name="four">four</string>
+    <string name="five">five</string>
+    <string name="six">six</string>
+    <string name="seven">seven</string>
+    <string name="eight">eight</string>
+    <string name="nine">nine</string>
+    <string name="ten">ten</string>
+    <string name="elv">elv</string>
+    <string name="zen">zen</string>
+    <string name="duna">duna</string>
+    <string name="quedra">quedra</string>
+    <string name="trin">trin</string>
+    <string name="quen">quen</string>
+    <string name="hes">hes</string>
+    <string name="sev">sev</string>
+    <string name="ak">ak</string>
+    <string name="neen">neen</string>
+    <string name="dex">dex</string>
+    <string name="lef">lef</string>
+    <string name="distance_in_two_units_cd">%1$s and %2$s</string>
+    <string name="distance_in_yd">%1$s yd</string>
+    <string name="distance_in_ft">%1$s ft</string>
+    <string name="distance_in_mi">%1$s mi</string>
+    <string name="distance_in_gf">%1$s %2$sGf</string>
+    <string name="filtered_stop_question">Do you want to save a favorite filtered with selected lines?</string>
+    <plurals name="distance_in_yd_cd">
+        <item quantity="one">%1$d yard</item>
+        <item quantity="other">%1$d yards</item>
+    </plurals>
+    <plurals name="distance_in_ft_cd">
+        <item quantity="one">%1$d foot</item>
+        <item quantity="other">%1$d feet</item>
+    </plurals>
+    <plurals name="distance_in_mi_cd">
+        <item quantity="one">%1$d mile</item>
+        <item quantity="other">%1$d miles</item>
+    </plurals>
+    <plurals name="distance_in_gf_cd">
+        <item quantity="one">%1$d grafut</item>
+        <item quantity="other">%1$d grafuts</item>
+    </plurals>
+    <plurals name="distance_in_3gf_12_cd">
+        <item quantity="one">%1$s trinagrafut</item>
+        <item quantity="other">%1$s trinagrafuts</item>
+    </plurals>
+    <plurals name="distance_in_gf_12_cd">
+        <item quantity="one">%1$s grafut</item>
+        <item quantity="other">%1$s grafuts</item>
+    </plurals>
+    <plurals name="time_in_4tm_12_cd">
+        <item quantity="one">%1$s quedratim</item>
+        <item quantity="other">%1$s quedratims</item>
+    </plurals>
+    <plurals name="time_in_2tm_12_cd">
+        <item quantity="one">%1$s dunatim</item>
+        <item quantity="other">%1$s dunatims</item>
+    </plurals>
+    <plurals name="speed_in_vl_12_cd">
+        <item quantity="one">%1$s vlos</item>
+        <item quantity="other">%1$s vlos</item>
+    </plurals>
+</resources>
\ No newline at end of file




diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index df879f01ea8436856a0eed72136b1a4b85951084..681fc303bc0dd98da77ad08144d901815fb93f97 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -1,10 +1,14 @@
 <?xml version="1.0" encoding="utf-8"?>
-<resources>
+<!--
+SPDX-FileCopyrightText: Adam Evyčędo and contributors using Weblate
+
+SPDX-License-Identifier: GPL-3.0-or-later
+--><resources>
     <string name="app_name">Bimba</string>
     <string name="title_home">Accueil</string>
     <string name="home_fab_description">Icône GPS</string>
     <string name="save">Enregistrer</string>
-    <string name="cont">Continuer</string>
+    <string name="continue_">Continuer</string>
     <string name="search_placeholder">Arrêts, lignes, ou Plus Codes</string>
     <string name="error_401">Un jeton est nécessaire pour utiliser ce serveur</string>
     <string name="error_403">Le jeton fourni est incorrect</string>
@@ -45,11 +49,10 @@     A propos
     <string name="website_button_description">Lien vers le site</string>
     <string name="use_online_feed">Utiliser le flux en ligne</string>
     <string name="error_406">La version de l\'application est incompatible avec le serveur</string>
-    <string name="departures_snackbar">Dernière mise à jour : %1$s</string>
     <string name="title_filter">Filtrer</string>
     <string name="cancel">Annuler</string>
     <string name="more">Plus</string>
-    <string name="title_voyage">Voyage</string>
+    <string name="title_journey">Voyage</string>
     <string name="title_activity_results">Résultats</string>
     <string name="title_map">Carte</string>
     <string name="no_departures">Aucun départ</string>
@@ -57,7 +60,6 @@     Serveur
     <string name="air_condition_content_description">Air conditionné</string>
     <string name="bicycles_allowed_content_description">Vélos autorisés</string>
     <string name="show_departures">Montrer les départs</string>
-    <string name="bimba_server_continue_button">Continuer</string>
     <string name="realtime_content_description">Départ en temps réel</string>
     <string name="open_in_maps_app">Ouvrir dans une application de cartes</string>
     <string name="departure_now">Maintenant</string>
@@ -68,7 +70,7 @@     Prochainement
     <string name="error_offline">Vous êtes hors ligne. Connectez-vous à Internet</string>
     <string name="off_boarding">Débarquement</string>
     <string name="error_gps">Impossible d\'obtenir la position actuelle</string>
-    <string name="speed_in_km_per_h">%1$.3f km/h</string>
+    <string name="speed_in_km_per_h">%1$s km/h</string>
     <string name="occupancy_unknown">Inconnu</string>
     <string name="occupancy_few_seats">Peu de sièges</string>
     <string name="occupancy_crowded">Encombré</string>
@@ -114,6 +116,8 @@     Pas trouvé
     <string name="error_429">Limite dépassée. Réessayez plus tard</string>
     <string name="onboarding_simple_action">Choisir la localité</string>
     <string name="no_location_access">Accès à la position non autorisé</string>
-    <string name="seatbelts_everyone">En voiture tout le monde!</string>
+    <string name="seatbelts_everyone">En voiture tout le monde !</string>
     <string name="onboarding_simple">Simple</string>
+    <string name="occupancy_wont_let">embarquement refusé</string>
+    <string name="choose_server">Choix du type de serveur</string>
 </resources>
\ No newline at end of file




diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 4b8162ad24cae62e539542f5747503263a3424a7..69ecbb6d8a8a72b7e4f4af3003ecdcb28aebaf0a 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -1,17 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-SPDX-FileCopyrightText: Adam Evyčędo
+SPDX-FileCopyrightText: Adam Evyčędo and contributors using Weblate
 
 SPDX-License-Identifier: GPL-3.0-or-later
 --><resources>
     <string name="app_name">Bimba</string>
     <string name="title_home">Casa</string>
     <string name="title_map">Cartina</string>
-    <string name="title_voyage">Viaggio</string>
+    <string name="title_journey">Viaggio</string>
     <string name="home_fab_description">icona GPS</string>
     <string name="search_placeholder">fermate, linee o codici OLC</string>
     <string name="title_activity_results">Risultati</string>
-    <string name="cont">Continua</string>
+    <string name="continue_">Continua</string>
     <string name="save">Salva</string>
     <string name="error_400">L’app ha fatto una richiesta malformata</string>
     <string name="error_401">Un gettone è necessario per usare questo server</string>
@@ -27,7 +27,7 @@     Nessune partenze
     <string name="waiting_position">In attesa della posizione</string>
     <string name="vehicle_headsign">%1$s » %2$s</string>
     <string name="vehicle_headsign_content_description">%1$s verso %2$s</string>
-    <string name="speed_in_km_per_h">%1$.3f km/h</string>
+    <string name="speed_in_km_per_h">%1$s km/h</string>
     <string name="congestion_unknown">sconosciuta</string>
     <string name="congestion_smooth">fluido</string>
     <string name="congestion_stop_and_go">fermarsi e andare</string>
@@ -62,7 +62,6 @@     Fermate vicine
     <string name="results_for">Risultati per «%1$s»</string>
     <string name="bimba_server_address_hint">Server</string>
     <string name="bimba_server_token_hint">Gettone</string>
-    <string name="bimba_server_continue_button">Continua</string>
     <string name="realtime_content_description">la partenza è in tempo reale</string>
     <string name="wheelchair_content_description">il veicolo è accessibile alle sedie a rotelle</string>
     <string name="air_condition_content_description">climatizzazione</string>
@@ -91,7 +90,6 @@     Località
     <string name="error_url">URL malformato fornito</string>
     <string name="error_traffic_spec">Impossibile verificare il server</string>
     <string name="stops_near_code">Fermate vicino a %1$s</string>
-    <string name="code_is_not_full">Il codice non è pieno</string>
     <string name="choose_server">Scegli la varietà del server</string>
     <string name="ok">OK</string>
     <string name="no_location_access">Accesso alla posizione non fornito</string>
@@ -101,7 +99,7 @@     Fermata su richiesta
     <string name="stop_stub_in_zone">Fermata nella zona %1$s</string>
     <string name="title_about">Che cos\'è</string>
     <string name="translation_button_description">link al servizio di traduzioni</string>
-    <string name="app_description">Compagno FLOSS di passeggero di trasport pubblico; un orario nella tasca</string>
+    <string name="app_description">Compagno FLOSS di passeggero di trasport pubblico; un orario nella tasca.</string>
     <string name="website_button_description">link al sito web</string>
     <string name="code_button_description">link al codice sorgente</string>
     <string name="mastodon_button_description">link a Mastodon</string>
@@ -111,9 +109,18 @@     Orario attuale valido: %1$s fino alla %2$s
     <string name="error_406">La versione dell’app non è compatibile con il server</string>
     <string name="filter_localities">filtra le località</string>
     <string name="error_41">Questa località non è supportata dal server</string>
-    <string name="credits">Font yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) basato su Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans\n\n L’icona di Mastodon (https://github.com/mastodon/joinmastodon) © collaboratori Mastodon, AGPL-3.0-or-later\n\n L’icona di Bimba creata da https://github.com/tebriz159\n\n Material icons © Google, Apache-2.0\n\n Dati della carta © collaboratori OpenStreetMap (https://www.openstreetmap.org/copyright), ODbL-1.0</string>
+    <string name="credits">Font yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) basato su Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans
+\n
+\n L’icona di Mastodon (https://github.com/mastodon/joinmastodon) © collaboratori Mastodon, AGPL-3.0-or-later
+\n
+\n L’icona di Bimba creata da https://github.com/tebriz159
+\n
+\n Material icons © Google, Apache-2.0
+\n
+\n Dati della carta © collaboratori OpenStreetMap (https://www.openstreetmap.org/copyright), ODbL-1.0
+\n
+\nElenco delle città utilizzato per la geocodificazione dei brevi codici OLC © Geonames (https://geonames.org), CC BY</string>
     <string name="stop_from_qr_code">Stop del codice QR</string>
-    <string name="departures_snackbar">L’ultimo aggiornamento: %1$s</string>
     <string name="title_select_date">Scegli il giorno della partenza</string>
     <string name="title_select_line">Scegli la linea</string>
     <string name="clear_date_selection">Azzera</string>
@@ -125,4 +132,153 @@     Scegli il fine
     <string name="more">Più</string>
     <string name="alert_header">Informazioni tempestive</string>
     <string name="map_attribution"><![CDATA[© collaboratori <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>]]></string>
-</resources>
+    <string name="time_in_tm">%1$s %2$sTm</string>
+    <string name="time_in_tm_past">%1$s %2$sTm fa</string>
+    <string name="speed_in_m_per_s">%1$s m/s</string>
+    <string name="speed_in_mi_per_h">%1$s mph</string>
+    <string name="speed_in_vl">%1$s Vl</string>
+    <string name="filtered">Filtrato</string>
+    <string name="unfiltered">Non filtrato</string>
+    <string name="error_geocoding">Città non trovata</string>
+    <string name="cities_channel_name">Canale di aggiornamento delle città</string>
+    <string name="cities_channel_description">Notifiche che mostrano l\'avanzamento dell\'aggiornamento dei dati locali di geocodifica</string>
+    <string name="saving_cities_list">salvando l\'elenco delle città</string>
+    <string name="updating_geocoding_data">Aggiornando i dati di geocodifica</string>
+    <string name="downloading_cities_list">scaricando l\'elenco delle città</string>
+    <string name="finished_updating_geocoding_data">Aggiornamento dei dati di geocodificazione finito</string>
+    <string name="updating_geocoding_data_failed">Aggiornamento dei dati di geocodifica non riuscito</string>
+    <string name="four">quattro</string>
+    <string name="five">cinque</string>
+    <string name="six">sei</string>
+    <string name="seven">sette</string>
+    <string name="eight">otto</string>
+    <string name="nine">nove</string>
+    <string name="ten">dieci</string>
+    <string name="elv">el</string>
+    <string name="duna">duna</string>
+    <string name="quedra">quedra</string>
+    <string name="zen">dozzi</string>
+    <string name="trin">trino</string>
+    <string name="quen">queno</string>
+    <string name="hes">heso</string>
+    <string name="sev">sevo</string>
+    <string name="ak">aco</string>
+    <string name="neen">nino</string>
+    <string name="dex">deso</string>
+    <string name="lef">lefo</string>
+    <string name="filtered_stop_question">Vuoi salvare un preferito filtrato con le linee selezionate?</string>
+    <plurals name="distance_in_km_cd">
+        <item quantity="one">%1$d chilometro</item>
+        <item quantity="many">%1$d di chilometri</item>
+        <item quantity="other">%1$d chilometri</item>
+    </plurals>
+    <plurals name="distance_in_yd_cd">
+        <item quantity="one">%1$d iarda</item>
+        <item quantity="many">%1$d di iarde</item>
+        <item quantity="other">%1$d iarde</item>
+    </plurals>
+    <plurals name="distance_in_ft_cd">
+        <item quantity="one">%1$d piede</item>
+        <item quantity="many">%1$d di piedi</item>
+        <item quantity="other">%1$d piedi</item>
+    </plurals>
+    <plurals name="distance_in_gf_cd">
+        <item quantity="one">%1$d grapiede</item>
+        <item quantity="many">%1$d di grapiedi</item>
+        <item quantity="other">%1$d grapiedi</item>
+    </plurals>
+    <plurals name="distance_in_3gf_12_cd">
+        <item quantity="one">%1$s trinagrapiede</item>
+        <item quantity="many">%1$s di trinagrapiedi</item>
+        <item quantity="other">%1$s trinagrapiedi</item>
+    </plurals>
+    <plurals name="time_in_s_cd">
+        <item quantity="one">%1$d secondo</item>
+        <item quantity="many">%1$d di secondi</item>
+        <item quantity="other">%1$d secondi</item>
+    </plurals>
+    <plurals name="time_in_tm_cd">
+        <item quantity="one">%1$d timo</item>
+        <item quantity="many">%1$d di timi</item>
+        <item quantity="other">%1$d timi</item>
+    </plurals>
+    <plurals name="time_in_4tm_12_cd">
+        <item quantity="one">%1$s quedratimo</item>
+        <item quantity="many">%1$s di quedratimi</item>
+        <item quantity="other">%1$s quedratimi</item>
+    </plurals>
+    <plurals name="time_in_2tm_12_cd">
+        <item quantity="one">%1$s dunatimo</item>
+        <item quantity="many">%1$s di dunatimi</item>
+        <item quantity="other">%1$s dunatimi</item>
+    </plurals>
+    <plurals name="speed_in_km_per_h_cd">
+        <item quantity="one">%1$d chilometro orario</item>
+        <item quantity="many">%1$d di chilometri orari</item>
+        <item quantity="other">%1$d chilometri orari</item>
+    </plurals>
+    <plurals name="speed_in_mi_per_h_cd">
+        <item quantity="one">%1$d miglio orario</item>
+        <item quantity="many">%1$d di miglia orarie</item>
+        <item quantity="other">%1$d miglia orarie</item>
+    </plurals>
+    <plurals name="speed_in_vl_cd">
+        <item quantity="one">%1$d vloso</item>
+        <item quantity="many">%1$d di vlosi</item>
+        <item quantity="other">%1$d vlosi</item>
+    </plurals>
+    <plurals name="speed_in_vl_12_cd">
+        <item quantity="one">%1$s vloso</item>
+        <item quantity="many">%1$s di vlosi</item>
+        <item quantity="other">%1$s vlosi</item>
+    </plurals>
+    <plurals name="distance_in_mi_cd">
+        <item quantity="one">%1$d miglio</item>
+        <item quantity="many">%1$d di miglia</item>
+        <item quantity="other">%1$d miglia</item>
+    </plurals>
+    <plurals name="distance_in_m_cd">
+        <item quantity="one">%1$d metro</item>
+        <item quantity="many">%1$d di metri</item>
+        <item quantity="other">%1$d metri</item>
+    </plurals>
+    <plurals name="distance_in_gf_12_cd">
+        <item quantity="one">%1$s grapiede</item>
+        <item quantity="many">%1$s di grapiedi</item>
+        <item quantity="other">%1$s grapiedi</item>
+    </plurals>
+    <plurals name="speed_in_m_per_s_cd">
+        <item quantity="one">%1$d metro al secondo</item>
+        <item quantity="many">%1$d di metri al secondo</item>
+        <item quantity="other">%1$d metri al secondo</item>
+    </plurals>
+    <string name="favourite">Preferito</string>
+    <string name="no_geocoding_data">Nessun dato di geocodifica</string>
+    <string name="no_geocoding_data_description">La domanda contiene un codice breve OLC ma non sono presenti dati di geocodifica. Scarica i dati di geocodifica o abilita l\'aggiornamento automatico nelle impostazioni.</string>
+    <string name="zero">zero</string>
+    <string name="one">un</string>
+    <string name="two">due</string>
+    <string name="three">tre</string>
+    <string name="distance_in_m">%1$s m</string>
+    <string name="distance_in_km">%1$s km</string>
+    <string name="distance_in_two_units_cd">%1$s e %2$s</string>
+    <string name="distance_in_yd">%1$s yd</string>
+    <string name="distance_in_ft">%1$s ft</string>
+    <string name="distance_in_mi">%1$s mi</string>
+    <string name="distance_in_gf">%1$s %2$sGf</string>
+    <string name="time_in_s">%1$s s</string>
+    <string name="favourite_content_description">Salva nei preferiti</string>
+    <string name="cannot_save_favourite">Impossibile salvare nei preferiti</string>
+    <string name="error_44">nessune più partenze</string>
+    <string name="loading">in caricamento…</string>
+    <string name="favourite_deleted">Il preferito cancellato</string>
+    <string name="undo">Annulla</string>
+    <string name="units_title">Sistema di unità</string>
+    <string name="units_locale_based">Basato sul sistema</string>
+    <string name="units_metric">Metrico (SI)</string>
+    <string name="units_imperial">Imperiale (RU)</string>
+    <string name="units_customary">Consuetudinario statunitense</string>
+    <string name="units_tgm10">TGM (base 10)</string>
+    <string name="units_tgm12">TGM (base 12)</string>
+    <string name="title_settings">Impostazioni</string>
+</resources>
\ No newline at end of file




diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index 4c87604b47f664e0663bbc07cdf7d3db1016c94b..a29ad3303af3a290263427ff3950ee82602572bf 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-SPDX-FileCopyrightText: Adam Evyčędo
+SPDX-FileCopyrightText: Adam Evyčędo and contributors using Weblate
 
 SPDX-License-Identifier: GPL-3.0-or-later
 -->




diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index e97857fb100d4f924cc140f36f4fb2c5e36955e1..18cc0e701931c44646e7e0c5b028a021e85a355c 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -51,4 +51,9 @@