Skip to content

Commit a321369

Browse files
committed
Add support location translations
1 parent cc9878d commit a321369

File tree

11 files changed

+192
-9
lines changed

11 files changed

+192
-9
lines changed

android/app/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ dependencies {
339339
implementation(Dependencies.AndroidX.lifecycleViewmodelKtx)
340340
implementation(Dependencies.AndroidX.lifecycleRuntimeCompose)
341341
implementation(Dependencies.Arrow.core)
342+
implementation(Dependencies.Arrow.optics)
342343
implementation(Dependencies.Arrow.resilience)
343344
implementation(Dependencies.Compose.constrainLayout)
344345
implementation(Dependencies.Compose.foundation)

android/app/src/main/AndroidManifest.xml

+6
Original file line numberDiff line numberDiff line change
@@ -106,5 +106,11 @@
106106
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
107107
android:resource="@xml/provider_paths" />
108108
</provider>
109+
<receiver android:name=".broadcastreceiver.LocaleChangedBroadcastReceiver"
110+
android:exported="false">
111+
<intent-filter>
112+
<action android:name="android.intent.action.LOCALE_CHANGED" />
113+
</intent-filter>
114+
</receiver>
109115
</application>
110116
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package net.mullvad.mullvadvpn.broadcastreceiver
2+
3+
import android.content.BroadcastReceiver
4+
import android.content.Context
5+
import android.content.Intent
6+
import net.mullvad.mullvadvpn.lib.shared.LocaleRepository
7+
import org.koin.core.component.KoinComponent
8+
import org.koin.core.component.inject
9+
10+
class LocaleChangedBroadcastReceiver : BroadcastReceiver(), KoinComponent {
11+
private val localeRepository by inject<LocaleRepository>()
12+
13+
override fun onReceive(context: Context?, intent: Intent?) {
14+
if (intent?.action == Intent.ACTION_LOCALE_CHANGED) {
15+
localeRepository.refreshLocale()
16+
}
17+
}
18+
}

android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/AppModule.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,5 @@ val appModule = module {
3232
single { AccountRepository(get(), get(), MainScope()) }
3333
single { DeviceRepository(get()) }
3434
single { VpnPermissionRepository(androidContext()) }
35-
single { ConnectionProxy(get(), get()) }
35+
single { ConnectionProxy(get(), get(), get()) }
3636
}

android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import net.mullvad.mullvadvpn.applist.ApplicationsProvider
1010
import net.mullvad.mullvadvpn.constant.IS_PLAY_BUILD
1111
import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport
1212
import net.mullvad.mullvadvpn.lib.payment.PaymentProvider
13+
import net.mullvad.mullvadvpn.lib.shared.LocaleRepository
14+
import net.mullvad.mullvadvpn.lib.shared.RelayLocationTranslationRepository
1315
import net.mullvad.mullvadvpn.lib.shared.VoucherRepository
1416
import net.mullvad.mullvadvpn.repository.ApiAccessRepository
1517
import net.mullvad.mullvadvpn.repository.ChangelogRepository
@@ -117,13 +119,15 @@ val uiModule = module {
117119
single { MullvadProblemReport(get()) }
118120
single { RelayOverridesRepository(get()) }
119121
single { CustomListsRepository(get()) }
120-
single { RelayListRepository(get()) }
122+
single { RelayListRepository(get(), get()) }
121123
single { RelayListFilterRepository(get()) }
122124
single { VoucherRepository(get(), get()) }
123125
single { SplitTunnelingRepository(get()) }
124126
single { ApiAccessRepository(get()) }
125127
single { NewDeviceRepository() }
126128
single { SplashCompleteRepository() }
129+
single { LocaleRepository(get()) }
130+
single { RelayLocationTranslationRepository(get(), get(), MainScope()) }
127131

128132
single { AccountExpiryNotificationUseCase(get()) }
129133
single { TunnelStateNotificationUseCase(get()) }

android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayListRepository.kt

+32-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package net.mullvad.mullvadvpn.repository
22

3+
import arrow.optics.Every
4+
import arrow.optics.copy
5+
import arrow.optics.dsl.every
36
import kotlinx.coroutines.CoroutineDispatcher
47
import kotlinx.coroutines.CoroutineScope
58
import kotlinx.coroutines.Dispatchers
69
import kotlinx.coroutines.flow.Flow
710
import kotlinx.coroutines.flow.SharingStarted
811
import kotlinx.coroutines.flow.StateFlow
12+
import kotlinx.coroutines.flow.combine
913
import kotlinx.coroutines.flow.distinctUntilChanged
1014
import kotlinx.coroutines.flow.map
1115
import kotlinx.coroutines.flow.stateIn
@@ -17,18 +21,41 @@ import net.mullvad.mullvadvpn.lib.model.RelayItem
1721
import net.mullvad.mullvadvpn.lib.model.RelayItemId
1822
import net.mullvad.mullvadvpn.lib.model.WireguardConstraints
1923
import net.mullvad.mullvadvpn.lib.model.WireguardEndpointData
24+
import net.mullvad.mullvadvpn.lib.model.cities
25+
import net.mullvad.mullvadvpn.lib.model.name
26+
import net.mullvad.mullvadvpn.lib.shared.RelayLocationTranslationRepository
2027
import net.mullvad.mullvadvpn.relaylist.findByGeoLocationId
2128

2229
class RelayListRepository(
2330
private val managementService: ManagementService,
31+
private val translationRepository: RelayLocationTranslationRepository,
2432
dispatcher: CoroutineDispatcher = Dispatchers.IO
2533
) {
2634
val relayList: StateFlow<List<RelayItem.Location.Country>> =
27-
managementService.relayCountries.stateIn(
28-
CoroutineScope(dispatcher),
29-
SharingStarted.WhileSubscribed(),
30-
emptyList()
31-
)
35+
combine(managementService.relayCountries, translationRepository.translations) {
36+
countries,
37+
translations ->
38+
countries.translateRelay(translations)
39+
}
40+
.stateIn(CoroutineScope(dispatcher), SharingStarted.WhileSubscribed(), emptyList())
41+
42+
private fun List<RelayItem.Location.Country>.translateRelay(
43+
translations: Map<String, String>
44+
): List<RelayItem.Location.Country> {
45+
if (translations.isEmpty()) {
46+
return this
47+
}
48+
49+
return Every.list<RelayItem.Location.Country>().modify(this) {
50+
it.copy<RelayItem.Location.Country> {
51+
RelayItem.Location.Country.name set translations.getOrDefault(it.name, it.name)
52+
RelayItem.Location.Country.cities.every(Every.list()).name transform
53+
{ cityName ->
54+
translations.getOrDefault(cityName, cityName)
55+
}
56+
}
57+
}
58+
}
3259

3360
val wireguardEndpointData: StateFlow<WireguardEndpointData> =
3461
managementService.wireguardEndpointData.stateIn(
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package net.mullvad.mullvadvpn.lib.model
22

3+
import arrow.optics.optics
34
import java.net.InetAddress
45

6+
@optics
57
data class GeoIpLocation(
68
val ipv4: InetAddress?,
79
val ipv6: InetAddress?,
@@ -10,4 +12,6 @@ data class GeoIpLocation(
1012
val latitude: Double,
1113
val longitude: Double,
1214
val hostname: String?,
13-
)
15+
) {
16+
companion object
17+
}

android/lib/shared/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ android {
2727
}
2828

2929
dependencies {
30+
implementation(project(Dependencies.Mullvad.resourceLib))
3031
implementation(project(Dependencies.Mullvad.commonLib))
3132
implementation(project(Dependencies.Mullvad.daemonGrpc))
3233
implementation(project(Dependencies.Mullvad.modelLib))

android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxy.kt

+25-1
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,38 @@ package net.mullvad.mullvadvpn.lib.shared
33
import arrow.core.Either
44
import arrow.core.raise.either
55
import arrow.core.raise.ensure
6+
import kotlinx.coroutines.flow.combine
7+
import mullvad_daemon.management_interface.location
68
import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
79
import net.mullvad.mullvadvpn.lib.model.ConnectError
10+
import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
11+
import net.mullvad.mullvadvpn.lib.model.TunnelState
12+
import net.mullvad.mullvadvpn.lib.model.location
813

914
class ConnectionProxy(
1015
private val managementService: ManagementService,
16+
private val translationRepository: RelayLocationTranslationRepository,
1117
private val vpnPermissionRepository: VpnPermissionRepository
1218
) {
13-
val tunnelState = managementService.tunnelState
19+
val tunnelState =
20+
combine(managementService.tunnelState, translationRepository.translations) {
21+
tunnelState,
22+
translations ->
23+
tunnelState.translateLocations(translations)
24+
}
25+
26+
private fun TunnelState.translateLocations(translations: Map<String, String>): TunnelState {
27+
return when (this) {
28+
is TunnelState.Connecting -> copy(location = location?.translate(translations))
29+
is TunnelState.Disconnected -> copy(location = location?.translate(translations))
30+
is TunnelState.Disconnecting -> this
31+
is TunnelState.Error -> this
32+
is TunnelState.Connected -> copy(location = location?.translate(translations))
33+
}
34+
}
35+
36+
private fun GeoIpLocation.translate(translations: Map<String, String>): GeoIpLocation =
37+
copy(city = translations[city] ?: city, country = translations[country] ?: country)
1438

1539
suspend fun connect(): Either<ConnectError, Boolean> = either {
1640
ensure(vpnPermissionRepository.hasVpnPermission()) { ConnectError.NoVpnPermission }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package net.mullvad.mullvadvpn.lib.shared
2+
3+
import android.content.res.Resources
4+
import co.touchlab.kermit.Logger
5+
import java.util.Locale
6+
import kotlin.also
7+
import kotlinx.coroutines.flow.MutableStateFlow
8+
import kotlinx.coroutines.flow.StateFlow
9+
10+
class LocaleRepository(val resources: Resources) {
11+
private val _currentLocale = MutableStateFlow(getLocale())
12+
val currentLocale: StateFlow<Locale?> = _currentLocale
13+
14+
private fun getLocale(): Locale? = resources.configuration.locales.get(0)
15+
16+
fun refreshLocale() {
17+
_currentLocale.value = getLocale().also { Logger.d("New locale: $it") }
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package net.mullvad.mullvadvpn.lib.shared
2+
3+
import android.content.Context
4+
import android.content.res.Configuration
5+
import android.content.res.XmlResourceParser
6+
import co.touchlab.kermit.Logger
7+
import java.util.Locale
8+
import kotlin.collections.associate
9+
import kotlin.collections.set
10+
import kotlin.collections.toMap
11+
import kotlin.to
12+
import kotlinx.coroutines.CoroutineDispatcher
13+
import kotlinx.coroutines.CoroutineScope
14+
import kotlinx.coroutines.Dispatchers
15+
import kotlinx.coroutines.MainScope
16+
import kotlinx.coroutines.flow.SharingStarted
17+
import kotlinx.coroutines.flow.StateFlow
18+
import kotlinx.coroutines.flow.map
19+
import kotlinx.coroutines.flow.stateIn
20+
import kotlinx.coroutines.withContext
21+
22+
typealias Translations = Map<String, String>
23+
24+
class RelayLocationTranslationRepository(
25+
val context: Context,
26+
val localeRepository: LocaleRepository,
27+
externalScope: CoroutineScope = MainScope(),
28+
val dispatcher: CoroutineDispatcher = Dispatchers.IO
29+
) {
30+
val translations: StateFlow<Translations> =
31+
localeRepository.currentLocale
32+
.map { loadTranslations(it) }
33+
.stateIn(externalScope, SharingStarted.Eagerly, emptyMap())
34+
35+
private val defaultTranslation: Map<String, String>
36+
37+
init {
38+
val defaultConfiguration = defaultConfiguration()
39+
val confContext = context.createConfigurationContext(defaultConfiguration)
40+
val defaultTranslationXml = confContext.resources.getXml(R.xml.relay_locations)
41+
defaultTranslation = loadRelayTranslation(defaultTranslationXml)
42+
}
43+
44+
private suspend fun loadTranslations(locale: Locale?): Translations =
45+
withContext(dispatcher) {
46+
Logger.d("Updating translations based $locale")
47+
if (locale == null || locale.language == DEFAULT_LANGUAGE) emptyMap()
48+
else {
49+
// Load current translations
50+
val xml = context.resources.getXml(R.xml.relay_locations)
51+
val translation = loadRelayTranslation(xml)
52+
53+
translation.entries.associate { (id, name) -> defaultTranslation[id]!! to name }
54+
}
55+
}
56+
57+
private fun loadRelayTranslation(xml: XmlResourceParser): Map<String, String> {
58+
val translation = mutableMapOf<String, String>()
59+
while (xml.eventType != XmlResourceParser.END_DOCUMENT) {
60+
if (xml.eventType == XmlResourceParser.START_TAG && xml.name == "string") {
61+
val key = xml.getAttributeValue(null, "name")
62+
val value = xml.nextText()
63+
translation[key] = value
64+
}
65+
xml.next()
66+
}
67+
return translation.toMap()
68+
}
69+
70+
private fun defaultConfiguration(): Configuration {
71+
val configuration = context.resources.configuration
72+
configuration.setLocale(Locale(DEFAULT_LANGUAGE))
73+
return configuration
74+
}
75+
76+
companion object {
77+
private const val DEFAULT_LANGUAGE = "en"
78+
}
79+
}

0 commit comments

Comments
 (0)