diff --git a/V2rayNG/app/build.gradle.kts b/V2rayNG/app/build.gradle.kts index eeb3016ed5..8e80b0cce6 100644 --- a/V2rayNG/app/build.gradle.kts +++ b/V2rayNG/app/build.gradle.kts @@ -128,6 +128,21 @@ android { compose = true } + androidResources { + generateLocaleConfig = true + localeFilters += listOf( + "en", + "zh-rCN", + "zh-rTW", + "vi", + "ru", + "fa", + "ar", + "bn", + "bqi-rIR" + ) + } + packaging { jniLibs { useLegacyPackaging = true @@ -142,6 +157,7 @@ dependencies { // AndroidX Core Libraries implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) // Compose Libraries implementation(platform(libs.androidx.compose.bom)) diff --git a/V2rayNG/app/src/main/AndroidManifest.xml b/V2rayNG/app/src/main/AndroidManifest.xml index 7fbb251348..0238a58610 100644 --- a/V2rayNG/app/src/main/AndroidManifest.xml +++ b/V2rayNG/app/src/main/AndroidManifest.xml @@ -52,6 +52,15 @@ android:theme="@style/AppThemeDayNight.NoActionBar" android:usesCleartextTraffic="true"> + + + + = 0) { - service.getString(R.string.connection_test_available, time) - } else { - service.getString(R.string.connection_test_error, errorStr) - } - MessageHelper.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result) + val result = ConnectionTestResult( + delayMillis = time, + errorMessage = errorStr, + ) + MessageHelper.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_RESULT, result) // Only fetch IP info if the delay test was successful if (time >= 0) { SpeedtestManager.getRemoteIPInfo()?.let { ip -> - MessageHelper.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result\n$ip") + MessageHelper.sendMsg2UI( + service, + AppConfig.MSG_MEASURE_DELAY_RESULT, + result.copy( + country = ip.country, + ipAddress = ip.ipAddress, + ), + ) } } } @@ -492,4 +498,4 @@ object CoreServiceManager { } } } -} \ No newline at end of file +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConnectionTestResult.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConnectionTestResult.kt new file mode 100644 index 0000000000..a30ad156f1 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConnectionTestResult.kt @@ -0,0 +1,11 @@ +package com.v2ray.ang.dto + +import java.io.Serializable + +/** Locale-neutral result sent from the daemon for presentation by the UI process. */ +data class ConnectionTestResult( + val delayMillis: Long, + val errorMessage: String = "", + val country: String? = null, + val ipAddress: String? = null, +) : Serializable diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/entities/ServerAffiliationInfo.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/entities/ServerAffiliationInfo.kt index ce21a3d288..2f78334458 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/entities/ServerAffiliationInfo.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/entities/ServerAffiliationInfo.kt @@ -1,10 +1,3 @@ package com.v2ray.ang.dto.entities -data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) { - fun getTestDelayString(): String { - if (testDelayMillis == 0L) { - return "" - } - return testDelayMillis.toString() + "ms" - } -} \ No newline at end of file +data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/entities/ServersCache.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/entities/ServersCache.kt index 05df615189..d25e087da4 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/entities/ServersCache.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/entities/ServersCache.kt @@ -4,5 +4,4 @@ data class ServersCache( val guid: String, val profile: ProfileItem, val testDelayMillis: Long = 0L, - val testDelayString: String = "", ) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/enums/Language.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/enums/Language.kt index cc89c9781e..aae48f9d70 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/enums/Language.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/enums/Language.kt @@ -1,20 +1,56 @@ package com.v2ray.ang.enums -enum class Language(val code: String) { - AUTO("auto"), - ENGLISH("en"), - CHINA("zh-rCN"), - TRADITIONAL_CHINESE("zh-rTW"), - VIETNAMESE("vi"), - RUSSIAN("ru"), - PERSIAN("fa"), - ARABIC("ar"), - BANGLA("bn"), - BAKHTIARI("bqi-rIR"); +import androidx.core.os.LocaleListCompat +import java.util.Locale + +enum class Language(val code: String, val languageTag: String) { + AUTO("auto", ""), + ENGLISH("en", "en"), + CHINA("zh-rCN", "zh-CN"), + TRADITIONAL_CHINESE("zh-rTW", "zh-TW"), + VIETNAMESE("vi", "vi"), + RUSSIAN("ru", "ru"), + PERSIAN("fa", "fa"), + ARABIC("ar", "ar"), + BANGLA("bn", "bn"), + BAKHTIARI("bqi-rIR", "bqi-IR"); + + val locale: Locale? + get() = languageTag.takeIf { it.isNotEmpty() }?.let(Locale::forLanguageTag) + + fun toLocaleList(): LocaleListCompat = + if (languageTag.isEmpty()) { + LocaleListCompat.getEmptyLocaleList() + } else { + LocaleListCompat.forLanguageTags(languageTag) + } companion object { fun fromCode(code: String): Language { return entries.find { it.code == code } ?: AUTO } + + fun fromLanguageTag(languageTag: String?): Language { + if (languageTag.isNullOrBlank()) return AUTO + + val locale = Locale.forLanguageTag(languageTag) + return when (locale.language) { + "en" -> ENGLISH + "zh" -> if ( + locale.script == "Hant" || locale.country in setOf("TW", "HK", "MO") + ) { + TRADITIONAL_CHINESE + } else { + CHINA + } + "vi" -> VIETNAMESE + "ru" -> RUSSIAN + "fa" -> PERSIAN + "ar" -> ARABIC + "bn" -> BANGLA + "bqi" -> BAKHTIARI + else -> AUTO + } + } } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AppLocaleManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AppLocaleManager.kt new file mode 100644 index 0000000000..0edb99c080 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AppLocaleManager.kt @@ -0,0 +1,140 @@ +package com.v2ray.ang.handler + +import android.app.LocaleManager +import android.content.Context +import android.content.res.Configuration +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.app.LocaleManagerCompat +import androidx.core.content.ContextCompat +import androidx.core.os.ConfigurationCompat +import com.v2ray.ang.AppConfig +import com.v2ray.ang.enums.Language + +/** + * Keeps the legacy in-app language preference synchronized with Android's per-app locale APIs. + */ +object AppLocaleManager { + + /** + * Migrates the existing MMKV language preference and restores it before the first activity. + */ + fun initialize(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Api33.prepareMigration(context) + } else { + initializeCompat(context) + } + } + + /** + * Completes the one-time handoff after AppCompat has attached the activity context. + */ + fun onActivityCreated(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Api33.completeMigration(context) + } else { + syncLegacyPreference(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag()) + } + } + + /** + * Applies a language selected in the app and lets AppCompat recreate the current activity. + */ + fun setApplicationLanguage(languageCode: String) { + val language = Language.fromCode(languageCode) + persistLegacyPreference(language) + AppCompatDelegate.setApplicationLocales(language.toLocaleList()) + } + + /** + * Returns a context suitable for resource access outside AppCompatActivity. + */ + fun localizedContext(context: Context): Context { + val localizedContext = ContextCompat.getContextForLanguage(context) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU || localizedContext !== context) { + return localizedContext + } + + // During the one-time upgrade from the old picker, AppCompat has not written its locale + // record yet. Use the still-current MMKV value for non-activity contexts on that launch. + val language = storedLanguage() + language.locale ?: return context + val configuration = Configuration(context.resources.configuration) + ConfigurationCompat.setLocales(configuration, language.toLocaleList()) + return context.createConfigurationContext(configuration) + } + + private fun initializeCompat(context: Context) { + val storedLocales = LocaleManagerCompat.getApplicationLocales(context) + if (storedLocales.isEmpty) { + val legacyLanguage = storedLanguage() + if (legacyLanguage != Language.AUTO) { + AppCompatDelegate.setApplicationLocales(legacyLanguage.toLocaleList()) + } + } else { + syncLegacyPreference(storedLocales.get(0)?.toLanguageTag()) + } + } + + private fun storedLanguage(): Language = Language.fromCode( + MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code + ) + + private fun syncLegacyPreference(languageTag: String?) { + persistLegacyPreference(Language.fromLanguageTag(languageTag)) + } + + private fun persistLegacyPreference(language: Language) { + val current = MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) + if (current != language.code) { + MmkvManager.encodeSettings(AppConfig.PREF_LANGUAGE, language.code) + SettingsChangeManager.notifySettingChanged(AppConfig.PREF_LANGUAGE) + } + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private object Api33 { + fun prepareMigration(context: Context) { + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_APP_LOCALE_MIGRATED, false)) { + return + } + + val localeManager = context.getSystemService(LocaleManager::class.java) + val applicationLocales = localeManager.applicationLocales + if (!applicationLocales.isEmpty) { + syncLegacyPreference(applicationLocales.get(0)?.toLanguageTag()) + return + } + + val legacyLanguage = storedLanguage() + if (legacyLanguage != Language.AUTO) { + // Make application and service contexts correct even before the first activity. + localeManager.applicationLocales = android.os.LocaleList( + requireNotNull(legacyLanguage.locale) + ) + } + } + + fun completeMigration(context: Context) { + val localeManager = context.getSystemService(LocaleManager::class.java) + if (!MmkvManager.decodeSettingsBool(AppConfig.PREF_APP_LOCALE_MIGRATED, false)) { + val frameworkLanguage = Language.fromLanguageTag( + localeManager.applicationLocales.get(0)?.toLanguageTag() + ) + val language = frameworkLanguage.takeUnless { it == Language.AUTO } + ?: storedLanguage() + + // Android's migration guidance requires this one-time AppCompat handoff after + // Activity.onCreate(), including on Android 13 and newer. + AppCompatDelegate.setApplicationLocales(language.toLocaleList()) + persistLegacyPreference(language) + MmkvManager.encodeSettings(AppConfig.PREF_APP_LOCALE_MIGRATED, true) + return + } + + syncLegacyPreference(localeManager.applicationLocales.get(0)?.toLanguageTag()) + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt index c09d0573b3..f4cd34a68f 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt @@ -16,7 +16,6 @@ import com.v2ray.ang.dto.entities.ProfileItem import com.v2ray.ang.dto.entities.RulesetItem import com.v2ray.ang.dto.entities.SubscriptionItem import com.v2ray.ang.enums.EConfigType -import com.v2ray.ang.enums.Language import com.v2ray.ang.enums.RoutingType import com.v2ray.ang.enums.VpnInterfaceAddressConfig import com.v2ray.ang.extension.moveItem @@ -31,7 +30,6 @@ import com.v2ray.ang.util.LogUtil import com.v2ray.ang.util.Utils import java.io.File import java.io.FileOutputStream -import java.util.Locale import kotlin.random.Random object SettingsManager { @@ -388,29 +386,6 @@ object SettingsManager { return value.coerceIn(1, 128) } - /** - * Get the locale. - * @return The locale. - */ - fun getLocale(): Locale { - val langCode = - MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code - val language = Language.fromCode(langCode) - - return when (language) { - Language.AUTO -> Utils.getSysLocale() - Language.ENGLISH -> Locale.ENGLISH - Language.CHINA -> Locale.CHINA - Language.TRADITIONAL_CHINESE -> Locale.TRADITIONAL_CHINESE - Language.VIETNAMESE -> Locale.forLanguageTag("vi") - Language.RUSSIAN -> Locale.forLanguageTag("ru") - Language.PERSIAN -> Locale.forLanguageTag("fa") - Language.ARABIC -> Locale.forLanguageTag("ar") - Language.BANGLA -> Locale.forLanguageTag("bn") - Language.BAKHTIARI -> Locale.forLanguageTag("bqi-IR") - } - } - /** * Retrieves the currently selected VPN interface address configuration. * This method reads the user's preference for VPN interface addressing and returns diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt index b1e6dda743..524ee97df6 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt @@ -13,6 +13,11 @@ import java.net.UnknownHostException object SpeedtestManager { + data class RemoteEndpointInfo( + val country: String?, + val ipAddress: String?, + ) + /** * Measures the time taken to establish a TCP connection to a given URL and port. * @@ -48,7 +53,7 @@ object SpeedtestManager { return -1 } - fun getRemoteIPInfo(): String? { + fun getRemoteIPInfo(): RemoteEndpointInfo? { val url = MmkvManager.decodeSettingsString(AppConfig.PREF_IP_API_URL) .takeIf { !it.isNullOrBlank() } ?: AppConfig.IP_API_URL @@ -81,6 +86,9 @@ object SpeedtestManager { ipInfo.location?.country_code ).firstOrNull { !it.isNullOrBlank() } - return "(${country ?: "unknown"}) ${ip ?: "unknown"}" + return RemoteEndpointInfo( + country = country, + ipAddress = ip, + ) } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/CoreProxyOnlyService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/CoreProxyOnlyService.kt index 44de0720c0..7e5640196f 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/CoreProxyOnlyService.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/CoreProxyOnlyService.kt @@ -7,10 +7,10 @@ import android.os.IBinder import com.v2ray.ang.AppConfig import com.v2ray.ang.contracts.ServiceControl import com.v2ray.ang.core.CoreServiceManager +import com.v2ray.ang.handler.AppLocaleManager import com.v2ray.ang.handler.NotificationManager import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.util.LogUtil -import com.v2ray.ang.util.MyContextWrapper import java.lang.ref.SoftReference class CoreProxyOnlyService : Service(), ServiceControl { @@ -96,9 +96,7 @@ class CoreProxyOnlyService : Service(), ServiceControl { * @param newBase The new base context. */ override fun attachBaseContext(newBase: Context?) { - val context = newBase?.let { - MyContextWrapper.wrap(newBase, SettingsManager.getLocale()) - } + val context = newBase?.let(AppLocaleManager::localizedContext) super.attachBaseContext(context) } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/CoreRootService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/CoreRootService.kt index ab59915a32..d5265fb88c 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/CoreRootService.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/CoreRootService.kt @@ -7,11 +7,11 @@ import android.os.IBinder import com.v2ray.ang.AppConfig import com.v2ray.ang.contracts.ServiceControl import com.v2ray.ang.core.CoreServiceManager +import com.v2ray.ang.handler.AppLocaleManager import com.v2ray.ang.handler.NotificationManager import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.root.RootProxyManager import com.v2ray.ang.util.LogUtil -import com.v2ray.ang.util.MyContextWrapper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -90,9 +90,7 @@ class CoreRootService : Service(), ServiceControl { override fun onBind(intent: Intent?): IBinder? = null override fun attachBaseContext(newBase: Context?) { - val context = newBase?.let { - MyContextWrapper.wrap(newBase, SettingsManager.getLocale()) - } + val context = newBase?.let(AppLocaleManager::localizedContext) super.attachBaseContext(context) } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/CoreTestService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/CoreTestService.kt index ddcd8117c4..1ef04d8adc 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/CoreTestService.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/CoreTestService.kt @@ -2,6 +2,7 @@ package com.v2ray.ang.service import android.app.PendingIntent import android.app.Service +import android.content.Context import android.content.Intent import android.os.IBinder import androidx.core.app.NotificationCompat @@ -13,6 +14,7 @@ import com.v2ray.ang.dto.TestServiceMessage import com.v2ray.ang.enums.NotificationChannelType import com.v2ray.ang.extension.serializable import com.v2ray.ang.handler.AngConfigManager +import com.v2ray.ang.handler.AppLocaleManager import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.helper.MessageHelper import com.v2ray.ang.helper.NotificationHelper @@ -21,6 +23,10 @@ import java.util.Collections class CoreTestService : Service() { + override fun attachBaseContext(newBase: Context?) { + super.attachBaseContext(newBase?.let(AppLocaleManager::localizedContext)) + } + // manage active batch workers so each batch is independent and cancellable private val activeWorkers = Collections.synchronizedList(mutableListOf()) private val cancelAction by lazy { @@ -36,7 +42,7 @@ class CoreTestService : Service() { ) NotificationCompat.Action.Builder( R.drawable.ic_stop_24dp, - getString(android.R.string.cancel), + getString(R.string.action_cancel), pendingIntent ).build() } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/CoreVpnService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/CoreVpnService.kt index 59653d2a31..9d0d8461bc 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/CoreVpnService.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/CoreVpnService.kt @@ -17,12 +17,12 @@ import com.v2ray.ang.BuildConfig import com.v2ray.ang.contracts.ServiceControl import com.v2ray.ang.contracts.Tun2SocksControl import com.v2ray.ang.core.CoreServiceManager +import com.v2ray.ang.handler.AppLocaleManager import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.NotificationManager import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.root.RootLanSharing import com.v2ray.ang.util.LogUtil -import com.v2ray.ang.util.MyContextWrapper import com.v2ray.ang.util.Utils import java.lang.ref.SoftReference import java.util.concurrent.atomic.AtomicBoolean @@ -129,9 +129,7 @@ class CoreVpnService : VpnService(), ServiceControl { } override fun attachBaseContext(newBase: Context?) { - val context = newBase?.let { - MyContextWrapper.wrap(newBase, SettingsManager.getLocale()) - } + val context = newBase?.let(AppLocaleManager::localizedContext) super.attachBaseContext(context) } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt index 70bfdcfe7e..fb26101a07 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt @@ -12,6 +12,7 @@ import com.v2ray.ang.AppConfig import com.v2ray.ang.R import com.v2ray.ang.core.CoreServiceManager import com.v2ray.ang.core.LauncherManager +import com.v2ray.ang.handler.AppLocaleManager import com.v2ray.ang.helper.MessageHelper import com.v2ray.ang.util.LogUtil import com.v2ray.ang.util.Utils @@ -19,6 +20,10 @@ import java.lang.ref.SoftReference class QSTileService : TileService() { + override fun attachBaseContext(newBase: Context?) { + super.attachBaseContext(newBase?.let(AppLocaleManager::localizedContext)) + } + /** * Sets the state of the tile. * @param state The state to set. diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/SubscriptionUpdateService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/SubscriptionUpdateService.kt index 5263f13cc4..88604bf2a4 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/SubscriptionUpdateService.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/SubscriptionUpdateService.kt @@ -13,6 +13,7 @@ import com.v2ray.ang.dto.entities.SubscriptionCache import com.v2ray.ang.enums.NotificationChannelType import com.v2ray.ang.extension.serializable import com.v2ray.ang.handler.AngConfigManager +import com.v2ray.ang.handler.AppLocaleManager import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.helper.NotificationHelper import com.v2ray.ang.util.LogUtil @@ -28,6 +29,10 @@ import java.util.concurrent.atomic.AtomicInteger class SubscriptionUpdateService : Service() { + override fun attachBaseContext(newBase: Context?) { + super.attachBaseContext(newBase?.let(AppLocaleManager::localizedContext)) + } + private val serviceJob = Job() private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt index b698c29273..9b82c693fe 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt @@ -53,7 +53,7 @@ fun AboutScreen(onBackClick: () -> Unit) { val context = LocalContext.current var showOssDialog by remember { mutableStateOf(false) } - val libVersion = CoreNativeManager.getLibVersion() + val libVersion = CoreNativeManager.getLibVersion() ?: stringResource(R.string.value_unknown) val versionText = "v${BuildConfig.VERSION_NAME} ($libVersion)" val appIdText = BuildConfig.APPLICATION_ID @@ -122,7 +122,7 @@ fun AboutScreen(onBackClick: () -> Unit) { }, confirmButton = { TextButton(onClick = { showOssDialog = false }) { - Text(stringResource(android.R.string.ok)) + Text(stringResource(R.string.action_ok)) } }, containerColor = MaterialTheme.colorScheme.surface, diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/apppicker/AppPickerViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/apppicker/AppPickerViewModel.kt index 9df05f0ec2..3d6c624dbd 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/apppicker/AppPickerViewModel.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/apppicker/AppPickerViewModel.kt @@ -41,16 +41,30 @@ class AppPickerViewModel(application: Application) : BaseViewModel(application) } fun loadApps(context: Context) { - if (allApps != null || isAppListLoading) return + allApps?.let { apps -> + val localizedUnknownApp = getString(R.string.app_picker_unknown_app) + val updatedApps = apps.map { appInfo -> + if (appInfo.packageName == AppConfig.UNIDENTIFIED_PACKAGE) { + appInfo.copy(appName = localizedUnknownApp) + } else { + appInfo + } + } + allApps = sortApps(updatedApps) + _displayedApps.value = applyFilter(currentQuery) + return + } + if (isAppListLoading) return val applicationContext = context.applicationContext + val localizedUnknownApp = getString(R.string.app_picker_unknown_app) isAppListLoading = true launchLoading { try { selectedSnapshot = _selectedPackages.value val apps = withContext(Dispatchers.IO) { val list = AppManagerUtil.loadNetworkAppList(applicationContext) - val special = createSpecialItemUnidentified(applicationContext) + val special = createSpecialItemUnidentified(localizedUnknownApp) sortApps(list + special) } allApps = apps @@ -122,9 +136,9 @@ class AppPickerViewModel(application: Application) : BaseViewModel(application) } } - private fun createSpecialItemUnidentified(context: Context): AppInfo { + private fun createSpecialItemUnidentified(appName: String): AppInfo { return AppInfo( - appName = context.getString(R.string.app_picker_unknown_app), + appName = appName, packageName = AppConfig.UNIDENTIFIED_PACKAGE, isSystemApp = false, isSelected = 0 diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/backup/BackupActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/backup/BackupActivity.kt index c2e6dbb8dd..1466c28c99 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/backup/BackupActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/backup/BackupActivity.kt @@ -317,7 +317,7 @@ private fun WebDavInputDialog( } }, confirmText = stringResource(R.string.menu_item_save_config), - dismissText = stringResource(android.R.string.cancel), + dismissText = stringResource(R.string.action_cancel), onConfirm = { onSave( WebDavConfig( diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/base/BaseComponentActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/base/BaseComponentActivity.kt index fc0325ee97..12e8c50737 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/base/BaseComponentActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/base/BaseComponentActivity.kt @@ -1,23 +1,18 @@ package com.v2ray.ang.ui.base -import android.content.Context import android.os.Bundle -import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.Composable -import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.handler.AppLocaleManager import com.v2ray.ang.ui.compose.AppTheme -import com.v2ray.ang.util.MyContextWrapper -abstract class BaseComponentActivity : ComponentActivity() { - - override fun attachBaseContext(newBase: Context?) { - super.attachBaseContext(MyContextWrapper.wrap(newBase ?: return, SettingsManager.getLocale())) - } +abstract class BaseComponentActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + AppLocaleManager.onActivityCreated(this) enableEdgeToEdge() setContent { AppTheme { diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/base/BaseViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/base/BaseViewModel.kt index d4c9952aee..c346c3f72e 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/base/BaseViewModel.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/base/BaseViewModel.kt @@ -1,12 +1,14 @@ package com.v2ray.ang.ui.base import android.app.Application +import android.content.Context import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.v2ray.ang.AngApplication import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toastError import com.v2ray.ang.extension.toastSuccess +import com.v2ray.ang.handler.AppLocaleManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -27,6 +29,9 @@ abstract class BaseViewModel(application: Application) : AndroidViewModel(applic application as AngApplication } + protected val localizedContext: Context + get() = AppLocaleManager.localizedContext(app) + @Suppress("PropertyName") protected val _isLoading = MutableStateFlow(false) val isLoading: StateFlow = _isLoading.asStateFlow() @@ -39,56 +44,56 @@ abstract class BaseViewModel(application: Application) : AndroidViewModel(applic * Send neutral toast event (Resource ID). */ fun toast(resId: Int) { - app.toast(resId) + localizedContext.toast(resId) } /** * Send neutral toast event (String). */ fun toast(message: String) { - app.toast(message) + localizedContext.toast(message) } /** * Send success toast event (Resource ID). */ fun toastSuccess(resId: Int) { - app.toastSuccess(resId) + localizedContext.toastSuccess(resId) } /** * Send success toast event (String). */ fun toastSuccess(message: String) { - app.toastSuccess(message) + localizedContext.toastSuccess(message) } /** * Send error toast event (Resource ID). */ fun toastError(resId: Int) { - app.toastError(resId) + localizedContext.toastError(resId) } /** * Send error toast event (String). */ fun toastError(message: String) { - app.toastError(message) + localizedContext.toastError(message) } /** * Get string from resource ID. */ fun getString(resId: Int): String { - return app.getString(resId) + return localizedContext.getString(resId) } /** * Get formatted string from resource ID. */ fun getString(resId: Int, vararg formatArgs: Any?): String { - return app.getString(resId, *formatArgs) + return localizedContext.getString(resId, *formatArgs) } /** diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/checkupdate/CheckUpdateActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/checkupdate/CheckUpdateActivity.kt index e6f8dcec41..565796c785 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/checkupdate/CheckUpdateActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/checkupdate/CheckUpdateActivity.kt @@ -59,7 +59,7 @@ fun CheckUpdateScreen( val showUpdateDialog by viewModel.showUpdateDialog.collectAsStateWithLifecycle() val updateResult by viewModel.updateResult.collectAsStateWithLifecycle() - val libVersion = CoreNativeManager.getLibVersion() + val libVersion = CoreNativeManager.getLibVersion() ?: stringResource(R.string.value_unknown) val versionText = "v${BuildConfig.VERSION_NAME} ($libVersion)" Scaffold( @@ -109,7 +109,7 @@ fun CheckUpdateScreen( }, dismissButton = { TextButton(onClick = { viewModel.dismissUpdateDialog() }) { - Text(stringResource(android.R.string.cancel)) + Text(stringResource(R.string.action_cancel)) } }, containerColor = MaterialTheme.colorScheme.surface diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/checkupdate/CheckUpdateViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/checkupdate/CheckUpdateViewModel.kt index 8d598ed449..524993be01 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/checkupdate/CheckUpdateViewModel.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/checkupdate/CheckUpdateViewModel.kt @@ -42,12 +42,8 @@ class CheckUpdateViewModel(application: Application) : BaseViewModel(application toastSuccess(R.string.update_already_latest_version) } } catch (e: Exception) { - LogUtil.e(AppConfig.TAG, "Failed to check for updates: ${e.message}") - if (e.message == null) { - toastError(R.string.toast_failure) - } else { - toastError(e.message.orEmpty()) - } + LogUtil.e(AppConfig.TAG, "Failed to check for updates", e) + toastError(R.string.toast_failure) } } } @@ -55,4 +51,4 @@ class CheckUpdateViewModel(application: Application) : BaseViewModel(application fun dismissUpdateDialog() { _showUpdateDialog.value = false } -} \ No newline at end of file +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/compose/Dialog.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/compose/Dialog.kt index 5d3bd08543..d389817f4e 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/compose/Dialog.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/compose/Dialog.kt @@ -44,8 +44,8 @@ import com.v2ray.ang.R fun ConfirmDialog( title: String? = null, message: String, - confirmText: String = stringResource(android.R.string.ok), - dismissText: String? = stringResource(android.R.string.cancel), + confirmText: String = stringResource(R.string.action_ok), + dismissText: String? = stringResource(R.string.action_cancel), confirmIcon: @Composable (() -> Unit)? = null, onConfirm: () -> Unit, onDismiss: () -> Unit @@ -242,7 +242,7 @@ fun SelectListDialog( confirmButton = {}, dismissButton = { TextButton(onClick = onDismiss) { - Text(stringResource(android.R.string.cancel)) + Text(stringResource(R.string.action_cancel)) } }, containerColor = MaterialTheme.colorScheme.surface diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/compose/FormFields.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/compose/FormFields.kt index 7ab3f1e9a0..ea74eeafd5 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/compose/FormFields.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/compose/FormFields.kt @@ -78,6 +78,7 @@ fun FormDropdownField( editable: Boolean = false, enabled: Boolean = true, placeholder: String? = null, + supportingText: String? = null, ) { var expanded by rememberSaveable { mutableStateOf(false) } val menuScrollState = rememberScrollState() @@ -104,6 +105,7 @@ fun FormDropdownField( enabled = enabled, label = { Text(label) }, placeholder = { if (placeholder != null) Text(placeholder) }, + supportingText = supportingText?.let { { Text(it) } }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, colors = OutlinedTextFieldDefaults.colors( focusedContainerColor = Color.Transparent, diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/compose/SettingsItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/compose/SettingsItem.kt index 71945d4b4a..cb77d28321 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/compose/SettingsItem.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/compose/SettingsItem.kt @@ -164,8 +164,8 @@ fun SettingsEditItem( ) ), onFieldChange = { _, v -> text = v }, - confirmText = stringResource(android.R.string.ok), - dismissText = stringResource(android.R.string.cancel), + confirmText = stringResource(R.string.action_ok), + dismissText = stringResource(R.string.action_cancel), onConfirm = { showDialog = false; onValueChanged(text) }, onDismiss = { showDialog = false } ) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/logcat/LogcatActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/logcat/LogcatActivity.kt index dbf1694821..32e971edcb 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/logcat/LogcatActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/logcat/LogcatActivity.kt @@ -37,12 +37,14 @@ import androidx.compose.ui.unit.dp import androidx.core.content.FileProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope +import com.v2ray.ang.AppConfig import com.v2ray.ang.R -import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastError import com.v2ray.ang.ui.base.BaseComponentActivity import com.v2ray.ang.ui.compose.AppTopBar import com.v2ray.ang.ui.compose.ItemDivider import com.v2ray.ang.ui.compose.verticalScrollbar +import com.v2ray.ang.util.LogUtil import com.v2ray.ang.util.Utils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -91,8 +93,9 @@ class LogcatActivity : BaseComponentActivity() { uri to logFile.name } catch (e: Exception) { + LogUtil.e(AppConfig.TAG, "Failed to share Logcat", e) withContext(Dispatchers.Main) { - toast(e.localizedMessage ?: e.toString()) + toastError(R.string.toast_failure) } return@launch } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainContract.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainContract.kt index 103f9bf261..8df9ece89b 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainContract.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainContract.kt @@ -1,8 +1,18 @@ package com.v2ray.ang.ui.main +import com.v2ray.ang.dto.ConnectionTestResult import com.v2ray.ang.dto.GroupMapItem import com.v2ray.ang.dto.LocateTarget +/** Locale-neutral state formatted only when it reaches the main UI. */ +sealed interface MainStatus { + data object Disconnected : MainStatus + data object Connected : MainStatus + data object Testing : MainStatus + data class TestProgress(val progress: String) : MainStatus + data class ConnectionTest(val result: ConnectionTestResult) : MainStatus +} + /** * Main UI state */ @@ -12,7 +22,7 @@ data class MainUiState( val selectedGuid: String? = null, val isRunning: Boolean = false, val isTesting: Boolean = false, - val statusText: String = "", + val status: MainStatus = MainStatus.Disconnected, val locateTarget: LocateTarget? = null, val confirmRemove: Boolean = false, val doubleColumnDisplay: Boolean = false, diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainRepository.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainRepository.kt index 11359a2d5a..e272f747b8 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainRepository.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainRepository.kt @@ -8,13 +8,16 @@ import androidx.core.content.ContextCompat import com.v2ray.ang.AngApplication import com.v2ray.ang.AppConfig import com.v2ray.ang.R +import com.v2ray.ang.dto.ConnectionTestResult import com.v2ray.ang.dto.SubscriptionUpdateResult import com.v2ray.ang.dto.TestServiceMessage import com.v2ray.ang.dto.entities.ProfileItem import com.v2ray.ang.dto.entities.ServerAffiliationInfo import com.v2ray.ang.dto.entities.SubscriptionCache import com.v2ray.ang.dto.entities.SubscriptionItem +import com.v2ray.ang.extension.serializable import com.v2ray.ang.handler.AngConfigManager +import com.v2ray.ang.handler.AppLocaleManager import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SubscriptionUpdater @@ -31,6 +34,9 @@ class MainRepository( private val app: AngApplication ) : MainDataSource { + private val localizedContext: Context + get() = AppLocaleManager.localizedContext(app) + private val closed = AtomicBoolean(false) private val _mainServiceEvent = MutableSharedFlow( @@ -48,14 +54,12 @@ class MainRepository( AppConfig.MSG_STATE_RUNNING -> MainServiceEvent.StateRunning AppConfig.MSG_STATE_NOT_RUNNING -> MainServiceEvent.StateNotRunning AppConfig.MSG_STATE_START_SUCCESS -> MainServiceEvent.StateStartSuccess - AppConfig.MSG_STATE_START_FAILURE -> MainServiceEvent.StateStartFailure( - safeIntent.getStringExtra("content").orEmpty() - ) + AppConfig.MSG_STATE_START_FAILURE -> MainServiceEvent.StateStartFailure AppConfig.MSG_STATE_STOP_SUCCESS -> MainServiceEvent.StateStopSuccess - AppConfig.MSG_MEASURE_DELAY_SUCCESS -> MainServiceEvent.MeasureDelaySuccess( - safeIntent.getStringExtra("content").orEmpty() - ) + AppConfig.MSG_MEASURE_DELAY_RESULT -> safeIntent + .serializable("content") + ?.let { MainServiceEvent.MeasureDelayResult(it) } AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> MainServiceEvent.MeasureConfigSuccess AppConfig.MSG_MEASURE_CONFIG_NOTIFY -> MainServiceEvent.MeasureConfigNotify( @@ -116,9 +120,10 @@ class MainRepository( override fun isGroupAllDisplayEnabled(): Boolean = MmkvManager.decodeSettingsBool(AppConfig.PREF_GROUP_ALL_DISPLAY) - override fun getString(resId: Int): String = app.getString(resId) + override fun getString(resId: Int): String = localizedContext.getString(resId) - override fun getString(resId: Int, vararg formatArgs: Any): String = app.getString(resId, *formatArgs) + override fun getString(resId: Int, vararg formatArgs: Any): String = + localizedContext.getString(resId, *formatArgs) override fun getSubscriptions(): List { val result = mutableListOf() @@ -126,7 +131,7 @@ class MainRepository( result += SubscriptionCache( guid = "", subscription = SubscriptionItem().apply { - remarks = app.getString(R.string.filter_config_all) + remarks = localizedContext.getString(R.string.filter_config_all) } ) } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainScreen.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainScreen.kt index 35b2cbe8bb..ee2b149d50 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainScreen.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainScreen.kt @@ -44,7 +44,7 @@ fun MainScreen( val groups = uiState.groups val isLoading by mainViewModel.isLoading.collectAsStateWithLifecycle() val isRunning = uiState.isRunning - val displayText = uiState.statusText + val displayText = mainViewModel.formatStatus(uiState.status) val selectedGuid = uiState.selectedGuid val doubleColumnDisplay = uiState.doubleColumnDisplay val confirmRemove = uiState.confirmRemove diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainServerPager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainServerPager.kt index dad6ef8364..bf36175418 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainServerPager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainServerPager.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.LineBreak import androidx.compose.ui.text.style.TextOverflow @@ -249,7 +250,6 @@ private fun ServerItemRow( statistics = profile.description.nullIfBlank() ?: AngConfigManager.generateDescription(profile), typeDescription = getProtocolDescription(profile), - testResult = serverCache.testDelayString, testDelayMillis = serverCache.testDelayMillis, isSelected = serverCache.guid == selectedGuid, subscriptionRemarks = subRemarks, @@ -283,7 +283,6 @@ private fun ServerItemColumn( remarks = profile.remarks, statistics = profile.description.nullIfBlank() ?: AngConfigManager.generateDescription(profile), typeDescription = getProtocolDescription(profile), - testResult = serverCache.testDelayString, testDelayMillis = serverCache.testDelayMillis, isSelected = serverCache.guid == selectedGuid, subscriptionRemarks = subRemarks, @@ -303,7 +302,6 @@ fun ServerListItem( remarks: String, statistics: String, typeDescription: String, - testResult: String, testDelayMillis: Long, isSelected: Boolean, subscriptionRemarks: String, @@ -316,6 +314,12 @@ fun ServerListItem( modifier: Modifier = Modifier, dragModifier: Modifier = Modifier ) { + val testResult = if (testDelayMillis == 0L) { + "" + } else { + stringResource(R.string.server_test_delay_value, testDelayMillis) + } + Row( modifier = modifier .fillMaxWidth() diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainServiceEvent.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainServiceEvent.kt index 105092c7ac..9da9eff1a8 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainServiceEvent.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainServiceEvent.kt @@ -1,12 +1,14 @@ package com.v2ray.ang.ui.main +import com.v2ray.ang.dto.ConnectionTestResult + sealed class MainServiceEvent { data object StateRunning : MainServiceEvent() data object StateNotRunning : MainServiceEvent() data object StateStartSuccess : MainServiceEvent() - data class StateStartFailure(val errorMessage: String) : MainServiceEvent() + data object StateStartFailure : MainServiceEvent() data object StateStopSuccess : MainServiceEvent() - data class MeasureDelaySuccess(val content: String) : MainServiceEvent() + data class MeasureDelayResult(val result: ConnectionTestResult) : MainServiceEvent() data object MeasureConfigSuccess : MainServiceEvent() data class MeasureConfigNotify(val progress: String) : MainServiceEvent() data class MeasureConfigFinish(val finishedCount: String?) : MainServiceEvent() diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainViewModel.kt index c1114af800..83f0814d0c 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainViewModel.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.v2ray.ang.AppConfig import com.v2ray.ang.R +import com.v2ray.ang.dto.ConnectionTestResult import com.v2ray.ang.dto.GroupMapItem import com.v2ray.ang.dto.LocateTarget import com.v2ray.ang.dto.TestServiceMessage @@ -45,15 +46,11 @@ class MainViewModel( private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default private val preloadDispatcher: CoroutineDispatcher = Dispatchers.IO.limitedParallelism(1) - private val disconnectedText: String = dataSource.getString(R.string.connection_not_connected) - private val connectedText: String = dataSource.getString(R.string.connection_connected) - // ---------- UI state ---------- private val _uiState = MutableStateFlow( MainUiState( selectedGroupId = dataSource.getSelectedSubscriptionId(), selectedGuid = dataSource.getSelectServer(), - statusText = disconnectedText, confirmRemove = dataSource.getConfirmRemove(), doubleColumnDisplay = dataSource.getDoubleColumnDisplay() ) @@ -105,19 +102,14 @@ class MainViewModel( updateRunningState(true) } - is MainServiceEvent.StateStartFailure -> { - val error = event.errorMessage - if (error.isNotBlank()) { - toastError(error) - } else { - toastError(R.string.toast_services_failure) - } + MainServiceEvent.StateStartFailure -> { + toastError(R.string.toast_services_failure) updateRunningState(false) } MainServiceEvent.StateStopSuccess -> updateRunningState(false) - is MainServiceEvent.MeasureDelaySuccess -> { - _uiState.update { it.copy(statusText = event.content) } + is MainServiceEvent.MeasureDelayResult -> { + _uiState.update { it.copy(status = MainStatus.ConnectionTest(event.result)) } } MainServiceEvent.MeasureConfigSuccess -> { @@ -129,14 +121,7 @@ class MainViewModel( } is MainServiceEvent.MeasureConfigNotify -> { - _uiState.update { - it.copy( - statusText = dataSource.getString( - R.string.connection_running_task_left, - event.progress - ) - ) - } + _uiState.update { it.copy(status = MainStatus.TestProgress(event.progress)) } } is MainServiceEvent.MeasureConfigFinish -> { @@ -145,6 +130,37 @@ class MainViewModel( } } + internal fun formatStatus(status: MainStatus): String = when (status) { + MainStatus.Disconnected -> dataSource.getString(R.string.connection_not_connected) + MainStatus.Connected -> dataSource.getString(R.string.connection_connected) + MainStatus.Testing -> dataSource.getString(R.string.connection_test_testing) + is MainStatus.TestProgress -> dataSource.getString( + R.string.connection_running_task_left, + status.progress + ) + + is MainStatus.ConnectionTest -> formatConnectionTestResult(status.result) + } + + private fun formatConnectionTestResult(result: ConnectionTestResult): String { + val status = if (result.delayMillis >= 0) { + val delay = dataSource.getString(R.string.server_test_delay_value, result.delayMillis) + dataSource.getString(R.string.connection_test_available, delay) + } else { + val detail = result.errorMessage.ifBlank { + dataSource.getString(R.string.connection_test_empty_message) + } + dataSource.getString(R.string.connection_test_error, detail) + } + + if (result.delayMillis < 0 || (result.country == null && result.ipAddress == null)) { + return status + } + + val unknown = dataSource.getString(R.string.value_unknown) + return "$status\n(${result.country ?: unknown}) ${result.ipAddress ?: unknown}" + } + // ---------- Public state accessors ---------- fun serversForGroup(groupId: String): StateFlow> = groupPageFlows.computeIfAbsent(groupId) { MutableStateFlow(emptyList()) } @@ -235,8 +251,7 @@ class MainViewModel( ServersCache( guid = guid, profile = profile.copy(), - testDelayMillis = affiliation?.testDelayMillis ?: 0L, - testDelayString = affiliation?.getTestDelayString().orEmpty() + testDelayMillis = affiliation?.testDelayMillis ?: 0L ) } @@ -665,7 +680,7 @@ class MainViewModel( _uiState.update { it.copy( isTesting = false, - statusText = if (it.isRunning) connectedText else disconnectedText + status = if (it.isRunning) MainStatus.Connected else MainStatus.Disconnected ) } } @@ -683,7 +698,7 @@ class MainViewModel( _uiState.update { it.copy( isTesting = true, - statusText = dataSource.getString(R.string.connection_test_testing) + status = MainStatus.Testing ) } viewModelScope.launch(ioDispatcher) { @@ -700,11 +715,7 @@ class MainViewModel( } fun testCurrentServerRealPing() { - _uiState.update { - it.copy( - statusText = dataSource.getString(R.string.connection_test_testing) - ) - } + _uiState.update { it.copy(status = MainStatus.Testing) } dataSource.testCurrentServerRealPing() } @@ -715,7 +726,7 @@ class MainViewModel( _uiState.update { it.copy( isTesting = false, - statusText = if (it.isRunning) connectedText else disconnectedText + status = if (it.isRunning) MainStatus.Connected else MainStatus.Disconnected ) } reloadAllGroups(_uiState.value.groups.map { it.id }) @@ -751,8 +762,8 @@ class MainViewModel( _uiState.update { state -> state.copy( isRunning = running, - statusText = if (!clearTestingText && state.isTesting) state.statusText - else if (running) connectedText else disconnectedText + status = if (!clearTestingText && state.isTesting) state.status + else if (running) MainStatus.Connected else MainStatus.Disconnected ) } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/routing/RoutingEditActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/routing/RoutingEditActivity.kt index 45d7e2ba30..6513569949 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/routing/RoutingEditActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/routing/RoutingEditActivity.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.Text @@ -220,21 +221,27 @@ fun RoutingEditScreen( checked = locked, onCheckedChange = { locked = it } ) + Text( + text = stringResource(R.string.routing_settings_tips), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) FormTextField( label = stringResource(R.string.routing_settings_domain), - placeholder = stringResource(R.string.routing_settings_tips), + placeholder = stringResource(R.string.routing_settings_comma_tip), value = domain, onValueChange = { domain = it } ) FormTextField( label = stringResource(R.string.routing_settings_ip), - placeholder = stringResource(R.string.routing_settings_tips), + placeholder = stringResource(R.string.routing_settings_comma_tip), value = ip, onValueChange = { ip = it } ) FormTextField( label = stringResource(R.string.routing_settings_process), - placeholder = stringResource(R.string.routing_settings_tips), + placeholder = stringResource(R.string.routing_settings_comma_tip), value = processText, onValueChange = { processText = it }, enabled = canUseProcess diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/settings/SettingsActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/settings/SettingsActivity.kt index de3ed0a910..04b120a5be 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/settings/SettingsActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/settings/SettingsActivity.kt @@ -15,6 +15,7 @@ import androidx.compose.material3.ScaffoldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -25,6 +26,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig.VPN import com.v2ray.ang.R +import com.v2ray.ang.handler.AppLocaleManager +import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager.rememberMmkvBool import com.v2ray.ang.handler.MmkvManager.rememberMmkvString import com.v2ray.ang.handler.SettingsChangeManager @@ -120,7 +123,11 @@ fun SettingsScreen( var confirmRemove by rememberMmkvBool(AppConfig.PREF_CONFIRM_REMOVE, false) var doubleColumnDisplay by rememberMmkvBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false) var groupAllDisplay by rememberMmkvBool(AppConfig.PREF_GROUP_ALL_DISPLAY, false) - var language by rememberMmkvString(AppConfig.PREF_LANGUAGE, "auto") + var language by remember { + mutableStateOf( + MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE, "auto") ?: "auto" + ) + } var uiModeNight by rememberMmkvString(AppConfig.PREF_UI_MODE_NIGHT, "0") var ipv6Enabled by rememberMmkvBool(AppConfig.PREF_IPV6_ENABLED, false) @@ -225,7 +232,10 @@ fun SettingsScreen( entries = languageEntries, values = languageValues, selectedValue = language, - onSelected = { language = it } + onSelected = { + language = it + AppLocaleManager.setApplicationLanguage(it) + } ) SettingsListItem( title = stringResource(R.string.title_pref_ui_mode_night), diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/subscription/SubEditActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/subscription/SubEditActivity.kt index 977b754fea..545fb3c7a4 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/subscription/SubEditActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/subscription/SubEditActivity.kt @@ -227,7 +227,8 @@ fun SubEditScreen( value = prevProfile, options = profileSuggestions, onValueChange = { prevProfile = it }, - editable = true + editable = true, + supportingText = stringResource(R.string.sub_setting_entry_proxy_tip) ) FormDropdownField( label = stringResource(R.string.sub_setting_next_profile), @@ -235,7 +236,8 @@ fun SubEditScreen( value = nextProfile, options = profileSuggestions, onValueChange = { nextProfile = it }, - editable = true + editable = true, + supportingText = stringResource(R.string.sub_setting_exit_proxy_tip) ) } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/subscription/SubSettingActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/subscription/SubSettingActivity.kt index c217b37940..82d9065d6b 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/subscription/SubSettingActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/subscription/SubSettingActivity.kt @@ -323,12 +323,12 @@ fun SubSettingScreen( showUpdateDialog = false onSubUpdate() }) { - Text(text = stringResource(android.R.string.ok)) + Text(text = stringResource(R.string.action_ok)) } }, dismissButton = { TextButton(onClick = { showUpdateDialog = false }) { - Text(text = stringResource(android.R.string.cancel)) + Text(text = stringResource(R.string.action_cancel)) } } ) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/MyContextWrapper.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/MyContextWrapper.kt deleted file mode 100644 index 34352f05ba..0000000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/MyContextWrapper.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.v2ray.ang.util - -import android.content.Context -import android.content.ContextWrapper -import android.content.res.Configuration -import android.content.res.Resources -import android.os.LocaleList -import java.util.Locale - -open class MyContextWrapper(base: Context?) : ContextWrapper(base) { - companion object { - /** - * Wraps the context with a new locale. - * - * @param context The original context. - * @param newLocale The new locale to set. - * @return A ContextWrapper with the new locale. - */ - fun wrap(context: Context, newLocale: Locale?): ContextWrapper { - var mContext = context - val res: Resources = mContext.resources - val configuration: Configuration = res.configuration - - val locale = newLocale ?: Locale.getDefault() - configuration.setLocale(locale) - val localeList = LocaleList(locale) - LocaleList.setDefault(localeList) - configuration.setLocales(localeList) - - mContext = mContext.createConfigurationContext(configuration) - return ContextWrapper(mContext) - } - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/resources.properties b/V2rayNG/app/src/main/res/resources.properties new file mode 100644 index 0000000000..92481bb0b1 --- /dev/null +++ b/V2rayNG/app/src/main/res/resources.properties @@ -0,0 +1 @@ +unqualifiedResLocale=en diff --git a/V2rayNG/app/src/main/res/values-ar/strings.xml b/V2rayNG/app/src/main/res/values-ar/strings.xml index 65f6ab61d2..4cd7b04b7a 100644 --- a/V2rayNG/app/src/main/res/values-ar/strings.xml +++ b/V2rayNG/app/src/main/res/values-ar/strings.xml @@ -8,6 +8,8 @@ إغلاق درج التنقل نجحت عملية ترحيل البيانات! إيقاف الخدمة + موافق + إلغاء فشلت عملية ترحيل البيانات! اسحب لأسفل للتحديث! @@ -27,19 +29,19 @@ حفظ التكوين تعديل التكوين حذف التكوين - استيراد التكوين من رمز الاستجابة السريعة (QRcode) + استيراد التكوين من رمز QR استيراد التكوين من الحافظة استيراد التكوين من الجهاز إضافة [مجموعة سياسات] إضافة [سلسلة وكلاء] - الكتابة يدويًا [VMess] - الكتابة يدويًا [VLESS] - الكتابة يدويًا [Shadowsocks] - الكتابة يدويًا [SOCKS] - إدخال يدوي [HTTP] - الكتابة يدويًا [Trojan] - الكتابة يدويًا [Wireguard] - إدخال يدوي [Hysteria2] + إضافة [VMess] + إضافة [VLESS] + إضافة [Shadowsocks] + إضافة [SOCKS] + إضافة [HTTP] + إضافة [Trojan] + إضافة [WireGuard] + إضافة [Hysteria2] هل أنت متأكد من أنك تريد حذف جميع ملفات التعريف المعروضة حاليًا؟ هل أنت متأكد من أنك تريد حذف جميع ملفات التعريف المكررة المعروضة حاليًا؟ يرجى اختبار ملفات التعريف أولًا. هل أنت متأكد من أنك تريد حذف جميع ملفات التعريف غير الصالحة المعروضة حاليًا؟ @@ -71,7 +73,7 @@ path ws path httpupgrade path - xhttp path + xhttp path h2 path QUIC key kcp seed @@ -90,14 +92,14 @@ التشفير التدفق (flow) المفتاح العام - PreSharedKey(optional) + PreSharedKey (اختياري) المعرّف القصير SpiderX mldsa65Verify المفتاح السري - محجوز (اختياري) + Reserved (اختياري، مفصول بفواصل) العنوان المحلي (اختياري IPv4/IPv6، مفصولة بفواصل) - Mtu (اختياري، الافتراضي 1420) + MTU (اختياري، الافتراضي 1420) mKCP MTU (اختياري، الافتراضي في النواة 1350) mKCP TTI (اختياري، الافتراضي في النواة 50) نجاح @@ -118,13 +120,13 @@ مضيف (SNI) (اختياري) الإجراء غير مسموح به كلمة مرور Obfs - التنقل بين المنافذ - فاصل تبديل المنفذ + التنقل بين المنافذ (يتجاوز قيمة المنفذ) + فاصل تبديل المنفذ (بالثواني) عرض النطاق للتنزيل (الوحدات المدعومة: k/m/g/t) عرض النطاق للرفع (الوحدات المدعومة: k/m/g/t) - XHTTP Mode - XHTTP Extra raw JSON, format: { XHTTPObject } - finalMask raw JSON, format: { FinalMaskObject } + وضع XHTTP + JSON خام لحقل XHTTP Extra، بالتنسيق: { XHTTPObject } + JSON خام لحقل finalMask، بالتنسيق: { FinalMaskObject } echConfigList التحقق من شهادة النظير بالاسم بصمة الشهادة (SHA-256) @@ -140,7 +142,7 @@ إضافة أصل إضافة ملفات إضافة URL - مسح رمز الاستجابة السريعة (QRcode) + مسح رمز QR URL تنزيل الملفات إضافة عنوان URL للأصل @@ -155,7 +157,7 @@ عكس الاختيار أدخل الكلمات الرئيسية وضع التجاوز - تحديد تطبيق الوكيل تلقائيًا + تحديد التطبيقات التي ستستخدم الوكيل تلقائيًا جارٍ تنزيل المحتوى تصدير إلى الحافظة استيراد من الحافظة @@ -169,7 +171,7 @@ إعدادات النواة إعدادات VPN الوكيل لكل تطبيق - عام: التطبيق المحدد هو وكيل، غير المحدد اتصال مباشر؛ \nوضع التجاوز: التطبيق المحدد متصل مباشرة، غير المحدد وكيل. \nخيار تحديد تطبيق الوكيل تلقائيًا في القائمة + عام: تستخدم التطبيقات المحددة الوكيل، وتتصل التطبيقات غير المحددة مباشرة؛\nوضع التجاوز: تتصل التطبيقات المحددة مباشرة، وتستخدم التطبيقات غير المحددة الوكيل.\nيتوفر في القائمة خيار لتحديد التطبيقات التي ستستخدم الوكيل تلقائيًا. الاتصال تلقائياً عند بدء التشغيل الاتصال تلقائياً بالخادم المحدد عند بدء التشغيل؛ قد لا تنجح المحاولة @@ -214,10 +216,10 @@ تفضيل IPv6 تفضيل عناوين IPv6 عند حل أسماء النطاقات - DNS البعيد (udp/tcp/https/quic) (اختياري) + DNS البعيد (UDP/TCP/HTTPS/QUIC، اختياري) DNS - VPN DNS (IPv4/v6 فقط) + VPN DNS (IPv4/IPv6 فقط) هل تتجاوز VPN شبكة LAN؟ عنوان واجهة VPN @@ -225,10 +227,10 @@ قيمة MTU لشبكة VPN (الافتراضي 1500) - DNS المحلي (اختياري) + DNS المباشر (اختياري) DNS - مضيفو DNS (التنسيق: domain:address,…) + تعيينات مضيفي DNS (التنسيق: domain:address,…) domain:address,… عنوان URL لاختبار التأخير الحقيقي @@ -246,7 +248,7 @@ تفعيل الوكيل المحلي عند التعطيل، لن يتوفر وكيل SOCKS، ولذلك لن تتمكن تحديثات الاشتراك والإجراءات المشابهة من استخدام الوكيل SOCKS5 UDP - مصادقة socks5 ليست آمنة تمامًا عند استخدام UDP + مصادقة SOCKS5 ليست آمنة تمامًا عند استخدام UDP. منفذ البروكسي المحلي تمكين منفذ وكيل محلي ديناميكي إنشاء منفذ وكيل محلي عشوائي في كل مرة يُنشأ فيها inbound @@ -278,7 +280,7 @@ لم يتم العثور على تطبيق Telegram سياسة الخصوصية رمز QR - حول\nترجمة م. ابراهيم قاسم + حول التطبيق الكود المصدري تراخيص المصادر المفتوحة قناة Telegram @@ -299,7 +301,7 @@ تمكين ميزة Hev TUN عند التمكين، سيستخدم TUN ‏hev-socks5-tunnel؛ وإلا فسيستخدم xray-core. مستوى سجل Hev TUN - مهلة القراءة/الكتابة لـ Hev TUN بالثواني (القيم الافتراضية tcp,udp هي 300,60) + مهلة القراءة/الكتابة لـ Hev TUN (بالثواني؛ القيم الافتراضية لـ TCP/UDP: 300/60) إعدادات الوضع تمكين وضع Root (لمستخدمي Root) استخدام صلاحيات Root للوكيل الشفاف منخفض المستوى. يدير وضع Root حركة مرور الشبكة مباشرةً بدلاً من استخدام خدمة VPN في Android. أثناء تفعيله، لا يعمل الوضع العادي ولا توجيه التطبيقات ولا إعدادات VPN المرتبطة. @@ -312,11 +314,11 @@ مسح مشاركة السجل إعادة تشغيل الخدمة - حذف جميع الإعدادات (قبل أول خطوة) - حذف الإعدادات المكررة (2) - حذف الإعدادات غير الصالحة (بعد الاختبار؛ 4) - تصدير الإعدادات غير المخصصة إلى الحافظة - إعداد مجموعة الاشتراك + حذف جميع الإعدادات + حذف الإعدادات المكررة + حذف الإعدادات غير الصالحة + تصدير التكوينات إلى الحافظة + الاشتراكات ملاحظات عنوان URL اختياري User Agent @@ -325,15 +327,17 @@ تفعيل التحديث تفعيل التحديث التلقائي السماح بعنوان HTTP غير آمن - ملاحظات تكوين الوكيل السابق - ملاحظات تكوين الوكيل التالي + وكيل الدخول + وكيل الخروج يجب أن تكون ملاحظات التكوين موجودة وفريدة + يُضاف الوكيل المحدد قبل كل ملف تعريف في هذا الاشتراك كحلقة الدخول في سلسلة الوكلاء لذلك الملف. + يُضاف الوكيل المحدد بعد كل ملف تعريف في هذا الاشتراك كحلقة الخروج في سلسلة الوكلاء لذلك الملف. فاصل التحديث غير صالح. الحد الأدنى هو 15 دقيقة. - تحديث الاشتراك (أول خطوة) - Tcping لجميع الإعدادات - اختبر جميع الإعدادات (3) + تحديث الاشتراك + اختبار تأخير TCP (TCPing) + اختبار زمن الاستجابة الفعلي ملفات الأصول - الفرز حسب نتائج الاختبار (5) + الفرز حسب نتائج الاختبار تصفية ملف التكوين الكل انقر على الشاشة لفتح الكاميرا والمسح @@ -353,7 +357,8 @@ استراتيجية النطاق إعدادات التوجيه - افصل بعلامة الفاصلة (,) واختر أحد: domain أو ip أو process + لا تُطبَّق القاعدة إلا إذا تحققت جميع شروط المطابقة المحددة معًا (AND). تُفحص القواعد من الأعلى إلى الأسفل، وتُطبَّق أول قاعدة متطابقة. + افصل بين الإدخالات المتعددة بفواصل (,). حفظ مسح إعدادات قاعدة التوجيه @@ -378,22 +383,23 @@ التحقق من الاتصال يجري الاختبار… جارٍ اختبار %d من التكوينات… - نجاح: استغرق اتصال HTTP %dms - فشل اكتشاف اتصال الإنترنت: %s + تم الاتصال بنجاح خلال %1$s + %1$d ملي ثانية + فشل اختبار الاتصال: %s الإنترنت غير متاح رمز الخطأ: #%d - متصل، انقر للتحقق من الاتصال + متصل. اضغط للتحقق من الاتصال. غير متصل الاختبارات قيد التشغيل: %s تم استيراد الاشتراك بنجاح فشل استيراد الاشتراك إعدادات تجزئة الحزم (Fragment) - حزم الجزء + الحزم المراد تجزئتها طول الجزء (الحد الأدنى - الحد الأقصى) فاصل الجزء (الحد الأدنى - الحد الأقصى) - تفعيل الجزء - ماكس سبليت الجزء + تفعيل تجزئة الحزم + الحد الأقصى لعدد الأجزاء إعدادات مراقبة الاتصال (Observatory) فاصل leastPing فاصل leastLoad @@ -416,12 +422,12 @@ اختبار الاتصالات الصادرة (outbounds) الاتصال الصادر الاحتياطي (outbound، اختياري) - اعضاء سلسلة الوكيل + أعضاء سلسلة الوكيل اضغط هنا لاختيار عضو - اختر عضوا - يرجى اختيار remark لكل صف في السلسلة - عدد الاعضاء غير كاف - اعضاء سلسلة غير صالحين: %1$s + اختر عضوًا + يرجى اختيار عضو لكل صف في السلسلة + عدد الأعضاء غير كافٍ + أعضاء سلسلة غير صالحين: %1$s النسخ الاحتياطي والاستعادة @@ -435,14 +441,14 @@ كلمة المرور الدليل البعيد (اختياري) - رمز استجابة سريعة (QRcode) + رمز QR تصدير إلى الحافظة تصدير التكوين الكامل إلى الحافظة تعديل حذف إغلاق - رمز استجابة سريعة (QRcode) + رمز QR تصدير إلى الحافظة @@ -500,14 +506,6 @@ نسخ السجل مسح السجل إضافة قاعدة توجيه - لم يتم تحديد تكوين - تعذرت قراءة التكوين المحدد - تعذر إعداد التكوين المحدد - تعذر إنشاء تكوين النواة - تعذر إنشاء تكوين النواة: %1$s - تعذر إنشاء تكوين النواة لاختبار السرعة: %1$s - التكوين المخصص فارغ - تعذر تشغيل النواة بدء الخدمة إيقاف الخدمة رجوع diff --git a/V2rayNG/app/src/main/res/values-bn/strings.xml b/V2rayNG/app/src/main/res/values-bn/strings.xml index 052e6aac23..8e31819610 100644 --- a/V2rayNG/app/src/main/res/values-bn/strings.xml +++ b/V2rayNG/app/src/main/res/values-bn/strings.xml @@ -8,6 +8,8 @@ নেভিগেশন ড্রয়ার বন্ধ করুন ডেটা স্থানান্তর সফল! সেবা বন্ধ করুন + ঠিক আছে + বাতিল ডেটা স্থানান্তর ব্যর্থ! রিফ্রেশ করতে নিচে টানুন! @@ -32,14 +34,14 @@ ডিভাইস থেকে কনফিগ আমদানি করুন [নীতি গ্রুপ] যোগ করুন [প্রক্সি চেইন] যোগ করুন - ম্যানুয়ালি টাইপ করুন [VMess] - ম্যানুয়ালি টাইপ করুন [VLESS] - ম্যানুয়ালি টাইপ করুন [Shadowsocks] - ম্যানুয়ালি টাইপ করুন [SOCKS] - ম্যানুয়ালি লিখুন [HTTP] - ম্যানুয়ালি টাইপ করুন [Trojan] - ম্যানুয়ালি টাইপ করুন [Wireguard] - ম্যানুয়ালি লিখুন [Hysteria2] + [VMess] যোগ করুন + [VLESS] যোগ করুন + [Shadowsocks] যোগ করুন + [SOCKS] যোগ করুন + [HTTP] যোগ করুন + [Trojan] যোগ করুন + [WireGuard] যোগ করুন + [Hysteria2] যোগ করুন আপনি কি বর্তমানে প্রদর্শিত সব প্রোফাইল মুছে ফেলতে চান? আপনি কি বর্তমানে প্রদর্শিত সব সদৃশ প্রোফাইল মুছে ফেলতে চান? প্রথমে প্রোফাইলগুলো পরীক্ষা করুন। আপনি কি বর্তমানে প্রদর্শিত সব অবৈধ প্রোফাইল মুছে ফেলতে চান? @@ -67,7 +69,7 @@ xhttp হোস্ট h2 হোস্ট QUIC নিরাপত্তা - gRPC কর্তৃপক্ষ + gRPC Authority পথ ws পথ httpupgrade পথ @@ -90,12 +92,12 @@ এনক্রিপশন প্রবাহ (flow) পাবলিক কী - PreSharedKey(optional) + PreSharedKey (ঐচ্ছিক) শর্ট আইডি SpiderX mldsa65Verify সিক্রেট কী - সংরক্ষিত (ঐচ্ছিক) + Reserved (ঐচ্ছিক, কমা দিয়ে পৃথক) স্থানীয় ঠিকানা (ঐচ্ছিক IPv4/IPv6, কমা দ্বারা পৃথক করা) MTU (ঐচ্ছিক, ডিফল্ট 1420) mKCP MTU (ঐচ্ছিক, কোর ডিফল্ট 1350) @@ -118,13 +120,13 @@ হোস্ট (SNI) (ঐচ্ছিক) অ্যাকশন অনুমোদিত নয় Obfs পাসওয়ার্ড - পোর্ট হপিং - পোর্ট পরিবর্তনের বিরতি + পোর্ট হপিং (পোর্টের মান প্রতিস্থাপন করবে) + পোর্ট পরিবর্তনের বিরতি (সেকেন্ড) ডাউনলোড ব্যান্ডউইডথ (সমর্থিত একক: k/m/g/t) আপলোড ব্যান্ডউইডথ (সমর্থিত একক: k/m/g/t) - XHTTP Mode - XHTTP Extra raw JSON, format: { XHTTPObject } - finalMask raw JSON, format: { FinalMaskObject } + XHTTP মোড + XHTTP Extra-এর কাঁচা JSON, ফরম্যাট: { XHTTPObject } + finalMask-এর কাঁচা JSON, ফরম্যাট: { FinalMaskObject } echConfigList নাম দিয়ে পিয়ার সার্টিফিকেট যাচাই করুন সার্টিফিকেট ফিঙ্গারপ্রিন্ট (SHA-256) @@ -155,7 +157,7 @@ নির্বাচন উল্টে দিন কিওয়ার্ড লিখুন বাইপাস মোড - অটো সিলেক্ট প্রক্সি অ্যাপ + প্রক্সি ব্যবহার করবে এমন অ্যাপ স্বয়ংক্রিয়ভাবে নির্বাচন করুন বিষয়বস্তু ডাউনলোড হচ্ছে ক্লিপবোর্ডে রপ্তানি করুন ক্লিপবোর্ড থেকে আমদানি করুন @@ -169,7 +171,7 @@ কোর সেটিংস VPN সেটিংস প্রতি-অ্যাপ প্রক্সি - সাধারণ: চেকড অ্যাপ প্রক্সি, আনচেকড সরাসরি সংযোগ; \nবাইপাস মোড: চেকড অ্যাপ সরাসরি সংযুক্ত, আনচেকড প্রক্সি। \nমেনুতে প্রক্সি অ্যাপ্লিকেশন স্বয়ংক্রিয়ভাবে নির্বাচন করার বিকল্প + সাধারণ: নির্বাচিত অ্যাপ প্রক্সি ব্যবহার করে, অনির্বাচিত অ্যাপ সরাসরি সংযুক্ত হয়;\nবাইপাস মোড: নির্বাচিত অ্যাপ সরাসরি সংযুক্ত হয়, অনির্বাচিত অ্যাপ প্রক্সি ব্যবহার করে।\nযেসব অ্যাপ প্রক্সি ব্যবহার করবে, সেগুলি স্বয়ংক্রিয়ভাবে নির্বাচনের বিকল্পটি মেনুতে আছে। চালুর সময় স্বয়ংক্রিয় সংযোগ চালুর সময় নির্বাচিত সার্ভারে স্বয়ংক্রিয়ভাবে সংযোগ করে; সংযোগ ব্যর্থ হতে পারে @@ -214,10 +216,10 @@ IPv6 অগ্রাধিকার দিন ডোমেইন নাম রেজোলিউশনে IPv6 ঠিকানাকে অগ্রাধিকার দিন - রিমোট DNS (udp/tcp/https/quic)(ঐচ্ছিক) + রিমোট DNS (UDP/TCP/HTTPS/QUIC, ঐচ্ছিক) DNS - VPN DNS (শুধুমাত্র IPv4/v6) + VPN DNS (শুধুমাত্র IPv4/IPv6) VPN কি LAN এড়িয়ে যাবে? VPN ইন্টারফেসের ঠিকানা @@ -225,13 +227,13 @@ VPN MTU (ডিফল্ট 1500) - ঘরোয়া DNS (ঐচ্ছিক) + সরাসরি DNS (ঐচ্ছিক) DNS - DNS হোস্ট (ফরম্যাট: domain:address,…) + DNS হোস্ট ম্যাপিং (বিন্যাস: domain:address,…) domain:address,… - সঠিক বিলম্ব পরীক্ষা ইউআরএল + প্রকৃত বিলম্ব পরীক্ষার URL URL একযোগে প্রকৃত বিলম্ব পরীক্ষা @@ -246,7 +248,7 @@ লোকাল প্রক্সি সক্ষম করুন বন্ধ থাকলে কোনো SOCKS প্রক্সি পাওয়া যাবে না, তাই সাবস্ক্রিপশন আপডেট ও অনুরূপ কাজ প্রক্সি ব্যবহার করতে পারবে না SOCKS5 UDP - UDP ব্যবহার করার সময় socks5 প্রমাণীকরণ সম্পূর্ণরূপে নিরাপদ নয় + UDP ব্যবহার করার সময় SOCKS5 প্রমাণীকরণ সম্পূর্ণরূপে নিরাপদ নয়। স্থানীয় প্রক্সি পোর্ট গতিশীল স্থানীয় প্রক্সি পোর্ট চালু করুন প্রতিবার inbound তৈরি হলে একটি এলোমেলো স্থানীয় প্রক্সি পোর্ট তৈরি করুন @@ -287,7 +289,7 @@ স্বয়ংক্রিয় আপডেট সাবস্ক্রিপশন পটভূমিতে একটি নির্দিষ্ট সময় পর পর আপনার সাবস্ক্রিপশন স্বয়ংক্রিয়ভাবে আপডেট করুন। ডিভাইসের উপর নির্ভর করে, এই বৈশিষ্ট্যটি সবসময় কাজ নাও করতে পারে - অটো আপডেট ইন্টারভ্যাল (মিনিট, সর্বনিম্ন মান ১৫) + স্বয়ংক্রিয় আপডেটের বিরতি (মিনিট; সর্বনিম্ন ১৫) লগ স্তর বহির্গামী সংযোগের ডোমেইন রেজলভ পদ্ধতি (outbound) @@ -299,7 +301,7 @@ Hev TUN সুবিধা চালু করুন চালু থাকলে TUN hev-socks5-tunnel ব্যবহার করবে; অন্যথায় xray-core ব্যবহার করবে। Hev TUN লগ স্তর - Hev TUN পড়া/লেখার সময়সীমা, সেকেন্ডে (tcp,udp ডিফল্ট 300,60) + Hev TUN পড়া/লেখার সময়সীমা (সেকেন্ড; TCP/UDP-এর ডিফল্ট: 300/60) মোড সেটিংস Root মোড চালু (Root ব্যবহারকারীদের জন্য) Root সুবিধা ব্যবহার করে নিম্ন-স্তরের স্বচ্ছ প্রক্সি চালু করুন। এই মোডটি Android-এর মানক সিস্টেম VPN এড়িয়ে সরাসরি অন্তর্নিহিত সব নেটওয়ার্ক ট্রাফিক নিয়ন্ত্রণ করে। চালু করার পর আগের সাধারণ মোড, অ্যাপভিত্তিক রাউটিং এবং সংশ্লিষ্ট VPN সেটিংস আর কার্যকর থাকবে না। @@ -309,14 +311,14 @@ Logcat কপি করুন - স্পষ্ট করুন + পরিষ্কার করুন লগ শেয়ার করুন সার্ভিস পুনরায় চালু করুন সব কনফিগারেশন মুছে ফেলুন ডুপ্লিকেট কনফিগারেশন মুছে ফেলুন অবৈধ কনফিগারেশন মুছে ফেলুন (প্রথমে পরীক্ষা করুন) - কাস্টম না করা কনফিগারেশনগুলি ক্লিপবোর্ডে রপ্তানি করুন - সাবস্ক্রিপশন গ্রুপ সেটিং + কনফিগারেশন ক্লিপবোর্ডে রপ্তানি করুন + সাবস্ক্রিপশন মন্তব্য ঐচ্ছিক URL User Agent @@ -325,13 +327,15 @@ আপডেট সক্রিয় করুন স্বয়ংক্রিয় আপডেট সক্রিয় করুন অনিরাপদ HTTP ঠিকানা অনুমোদন করুন - আগের প্রক্সি কনফিগারেশনের মন্তব্য - পরবর্তী প্রক্সি কনফিগারেশনের মন্তব্য + প্রবেশ প্রক্সি + প্রস্থান প্রক্সি কনফিগারেশনের মন্তব্যটি বিদ্যমান ও অনন্য হতে হবে + নির্বাচিত প্রক্সিটি এই সাবস্ক্রিপশনের প্রতিটি প্রোফাইলের আগে সেই প্রোফাইলের প্রক্সি চেইনের প্রবেশ লিঙ্ক হিসেবে যোগ করা হবে। + নির্বাচিত প্রক্সিটি এই সাবস্ক্রিপশনের প্রতিটি প্রোফাইলের পরে সেই প্রোফাইলের প্রক্সি চেইনের প্রস্থান লিঙ্ক হিসেবে যোগ করা হবে। আপডেটের বিরতি সঠিক নয়। সর্বনিম্ন ১৫ মিনিট। সাবস্ক্রিপশন আপডেট - সব কনফিগারেশন TCPing - সব কনফিগারেশন প্রকৃত বিলম্ব + TCP বিলম্ব পরীক্ষা করুন (TCPing) + প্রকৃত বিলম্ব পরীক্ষা করুন অ্যাসেট ফাইল টেস্ট ফলাফল দ্বারা সাজানো কনফিগারেশন ফাইল ফিল্টার করুন @@ -353,7 +357,8 @@ ডোমেইন কৌশল রাউটিং সেটিংস - কমা (,) দিয়ে আলাদা করুন। domain, ip বা process-এ একটি নির্বাচন করুন + সব নির্ধারিত মিলের শর্ত একসঙ্গে পূরণ হলেই কেবল নিয়মটি প্রয়োগ হবে (AND)। নিয়মগুলি ওপর থেকে নিচে পরীক্ষা করা হয়; প্রথম মিলে যাওয়া নিয়মটি প্রয়োগ করা হয়। + একাধিক এন্ট্রি কমা (,) দিয়ে আলাদা করুন। সেভ করুন মুছে ফেলুন রাউটিং নিয়মের সেটিংস @@ -361,7 +366,7 @@ পূর্বনির্ধারিত নিয়মাবলী আমদানি করুন বিদ্যমান নিয়মসমূহ মুছে যাবে। চালিয়ে যেতে চান? ক্লিপবোর্ড থেকে নিয়মসমূহ আমদানি করুন - QRcode থেকে রুলসেট আমদানি করুন + QR কোড থেকে রুলসেট আমদানি করুন নিয়মসমূহ ক্লিপবোর্ডে রপ্তানি করুন লক করা; প্রিসেট আমদানির সময় এই নিয়মটি রাখুন domain @@ -378,11 +383,12 @@ সংযোগ পরীক্ষা করুন পরীক্ষা চলছে… %dটি কনফিগারেশন পরীক্ষা করা হচ্ছে… - সফল: HTTP সংযোগ নিয়েছে %dms - ইন্টারনেট সংযোগ সনাক্ত করতে ব্যর্থ: %s + সংযোগ সফল হয়েছে; সময় লেগেছে %1$s + %1$d মিলিসেকেন্ড + সংযোগ পরীক্ষা ব্যর্থ: %s ইন্টারনেট উপলব্ধ নয় ত্রুটি কোড: #%d - সংযুক্ত, সংযোগ পরীক্ষা করতে ট্যাপ করুন + সংযুক্ত।\nসংযোগ পরীক্ষা করতে ট্যাপ করুন। সংযুক্ত নয় চলমান পরীক্ষা: %s @@ -500,14 +506,6 @@ লগ কপি করুন লগ মুছুন রাউটিং নিয়ম যোগ করুন - কোনো কনফিগারেশন নির্বাচন করা হয়নি - নির্বাচিত কনফিগারেশন পড়তে ব্যর্থ - নির্বাচিত কনফিগারেশন প্রস্তুত করতে ব্যর্থ - কোর কনফিগারেশন তৈরি করতে ব্যর্থ - কোর কনফিগারেশন তৈরি করতে ব্যর্থ: %1$s - স্পিড টেস্টের জন্য কোর কনফিগারেশন তৈরি করতে ব্যর্থ: %1$s - কাস্টম কনফিগারেশন খালি - কোর চালু করতে ব্যর্থ সার্ভিস শুরু করুন সার্ভিস বন্ধ করুন ফিরে যান diff --git a/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml b/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml index 2b9bcb20f7..518a051dad 100644 --- a/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml +++ b/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml @@ -8,6 +8,8 @@ بستن نومگه کشاری جاگورویی داڌه یل وا مووفقیت ٱنجوم وابی! واڌاشتن سرویس + قوۊل + لغو جاگورویی داڌه یل شکست خرد! سی وانۊ کردن، بلگه ن بلم بکشین! @@ -27,7 +29,7 @@ زفت کردن کانفیگ آلشتکاری کانفیگ پاک کردن کانفیگ - و من ٱووردن کانفیگ ز QRcode + و من ٱووردن کانفیگ ز کود QR و من ٱووردن کانفیگ ز کلیپ بورد و من ٱووردن کانفیگ ز محلی ٱووردن [بونکۊ سیاست] @@ -38,7 +40,7 @@ ٱووردن [SOCKS] ٱووردن [HTTP] ٱووردن [Trojan] - ٱووردن [Wireguard] + ٱووردن [WireGuard] ٱووردن [Hysteria2] پوی کانفیگا ک هیم سکو نشووݩ داڌه ابۊن پاک ابۊن، هنی هم اخۊی پاکسووݩ کۊنی؟ پوی کانفیگا تکراری ک هیم سکو نشووݩ داڌه ابۊن پاک ابۊن، هنی هم اخۊی پاکسووݩ کۊنی؟ @@ -140,7 +142,7 @@ ٱووردن دارایی ٱووردن فایل ٱووردن لینگ - اسکن QRcode + اسکن کود QR URL دانلود فایلا نشۊوی اینترنتی بونچک ن اضاف کۊنین @@ -149,14 +151,14 @@ بونچک فایلا جغرافیایی (اختیاری) - هونی بار ونی ابۊ... + هونی بار ونی ابۊ… پیتینیڌن پسند پوی چواسه کردن پسندا کلمه یل کلیڌی ن بزنین حالت دور زیڌن پسند خوتکار برنومه یلی ک پروکسی اخون - محتوا هونی دانلود ابۊن... + محتوا هونی دانلود ابۊن… و در کشیڌن من کلیپ بورد و من ٱووردن ز کلیپ بورد سامووا ب تفکیک برنومه @@ -169,7 +171,7 @@ سامووا هسته سامووا VPN پروکسی و ری برنومه - پوی وولاتی: برنومه پسند وابیڌه وا ی پروکسی منپیز ابۊ، برنومه پسند نوابیڌه موستقیمن منپیز ابۊ.\nحالت دور زیڌن: برنومه پسند وابیڌه موستقیمن منپیز ابۊ، برنومه پسند نوابیڌه وا ی پروکسی منپیز ابۊ.\nپسند خوتکار برنومه یل پروکسی من نومگه امکووݩ پذیر هڌ. + پوی وولاتی: برنومه پسند وابیڌه وا ی پروکسی منپیز ابۊ، برنومه پسند نوابیڌه موستقیمن منپیز ابۊ.\nحالت دور زیڌن: برنومه پسند وابیڌه موستقیمن منپیز ابۊ، برنومه پسند نوابیڌه وا ی پروکسی منپیز ابۊ.\nپسند خوتکار برنومه یلی ک وا ی پروکسی منپیز ابۊن من نومگه امکووݩ پذیر هڌ. منپیز خوتکار مجال ره ونی مجال ره وندن، خوساخوس و سرور پسند بیڌه منپیز ابۊ که گاشڌ نا مووفق بۊ. @@ -214,10 +216,10 @@ ترجی داڌن IPv6 مجال هل وو فسل نوم دامنه، نشۊویا IPv6 ن ترجی بڌین - DNS ز ره دیر (udp/tcp/https/quic، اختیاری) + DNS ز ره دیر (UDP/TCP/HTTPS/QUIC، اختیاری) DNS - VPN DNS (تینا IPv4/v6) + VPN DNS (تینا IPv4/IPv6) VPN ز شبکه محلی اگوڌرته؟ نشۊوی رابط VPN @@ -225,11 +227,11 @@ VPN MTU (پؽش فرض 1500) - DNS منی (اختیاری) + DNS موستقیم (اختیاری) DNS - DNS هاست موستقیم (قالوو: دامنه: نشۊوی،...) - دامنه:نشۊوی،... + هاستا DNS (قالوو: domain:address,…) + دامنه:نشۊوی،… نشۊوی اینترنتی آزمایش تئخیر واقعی URL @@ -246,7 +248,7 @@ ره وندن پروکسی محلی هرسا ره نوسته بۊ، هیچ پروکسی SOCKS من دسرس نؽ، سی دل هیمو کارایی جۊر ورۊ رسۊوی یل اشتراک نترن پروکسی ن و کار گرن. SOCKS5 UDP - ائراز هۊویت socks5 مجال استفاڌه ز UDP کاملن ایمن نؽ + ائراز هۊویت SOCKS5 مجال استفاڌه ز UDP کاملن ایمن نؽ. پورت پروکسی محلی وورکل پورت موتغیر سی پروکسی محلی هر کرت ک و من ٱووڌنی وورکل ابۊ، ی پورت پروکسی محلی تساڌۊفی وورکل اکونه. @@ -273,7 +275,7 @@ فشناڌن منشڌ - فشناڌن منشڌ یا گوزارش موشکلا من Github + فشناڌن منشڌ یا گوزارش موشکلا من GitHub ٱووڌن من بونکۊ تلگرام برنومه تلگرامن نجوست حریم سیخومی @@ -298,8 +300,8 @@ سامووا حالت رابط منتوری و کار گرؽڌن ویژیی Hev TUN مجالی ک ره ونده بۊ، TUN ایا hev-socks5-tunnel ن و کار اگره؛ ٱر ره نوۊفته بۊ، همو xray-core ن و کار اگره. - Hev Tun سئت گوزارشا - زمووݩ مندیر بیڌن خوندن/هؽل کردن Hev Tun (ثانیه) (پؽش فرض tcp، udp 300،60) + Hev TUN سئت گوزارشا + زمووݩ مندیر بیڌن خوندن/هؽل کردن Hev TUN (ثانیه؛ پؽش‌فرض TCP/UDP: 300/60) سامووا حالت حالت رۊت ره وست (منتورووݩ رۊت) ره وستن پروکسی شفاف سئت بلم وا امتیازا ریشه. ای حالت، VPN استاندارد سیستوم ٱندروید ن دور ازنه تا موستقیمن پوی ترافیک شبکه لمی ن وا دست بگره. بئڌ ره وندن، حالت معمۊلی ٱسلی، تور جۊری هر برنومه وو سامووا VPN مربۊته کاملن نا موئتبر ابۊن. @@ -315,8 +317,8 @@ پاک کردن پوی کانفیگا بونکۊ سکویی پاک کردن کانفیگا تکراری بونکۊ سکویی پاک کردن کانفیگا نا موئتبر بونکۊ سکویی - و در کشیڌن کانفیگا غیر سفارشی بونکۊ سکویی من کلیپ بورد - سامووا بونکۊ اشتراک + و در کشیڌن کانفیگا من کلیپ بورد + اشتراکا نیشتنا نشۊوی اینترنتی اختیاری User Agent @@ -325,13 +327,15 @@ ره وندن ورۊ کردن ره وندن ورۊ کردن خوتکار موجاز کردن نشۊوی HTTP نا ٱمن - نیشتنا کانفیگ پروکسی دیندایی - نیشتنا کانفیگ پروکسی نیایی + پروکسی وۊرۊی + پروکسی دری موطمئن بۊ ک نیشتنا هڌس وو جۊرس نی + ای پروکسی پؽش ز هر کانفیگ من ای اشتراک، وری ٱول عوزو زنجیره پروکسی اضاف ابۊ. + ای پروکسی بئڌ ز هر کانفیگ من ای اشتراک، وری آخر عوزو زنجیره پروکسی اضاف ابۊ. فاصله ورۊ کردن نا موئتبر هڌ. حداقل مقدار 15 دؽقه هڌ. ورۊ کردن اشتراک بونکۊ سکویی - Tcping کانفیگا بونکۊ سکویی - تئخیر واقعی کانفیگا بونکۊ سکویی + آزمایش تئخیر TCP (TCPing) + آزمایش تئخیر واقعی فایلا بونچک جغرافیایی ترتیب و ری نتیجه یل پینگ فیلتر کردن کانفیگا @@ -353,7 +357,8 @@ نشقه دامنه سامووا تور جوستن - وا کاما (,) ز یک جوڌا ابۊن؛ یکی ن بزنین: domain، ip یا process + پوی شرطا سئت بیڌه بایڌ هوم زمووی جور ابۊن (AND). قانووݩ یل ز سر و ته وارس ابۊن؛ ٱولی قانووݩ جور وابیڌه و کار ابسته ابۊ. + چنتا مقدار ن وا کاما (,) ز یک جوڌا کۊنین. زفت کردن روفتن سامووا قانووݩ تور جوستن @@ -361,7 +366,7 @@ و من ٱووردن قانووا قانووایی ک هیم سکو هڌسووݩ پاک ابۊن، هنی هم اخۊی پاکسووݩ کۊنی؟ و من ٱووردن قانووا ز کلیپ بورد - و من ٱووردن قانووا ز QRcode + و من ٱووردن قانووا ز کود QR و در کشیڌن قانووا وو زفت من کلیپ بورد چفت هڌ، ای قانووݩ ن مجال و من ٱووردن ز پؽش سامووا زفت کۊنین domain @@ -377,17 +382,18 @@ منپیزن واجۊری کوݩ هونی آزمایش ابۊ… - %d کانفیگ هونی آزمایش ابۊ... - مووفق بی: منپیز %dms طۊل کشی + %d کانفیگ هونی آزمایش ابۊ… + مووفق بی: منپیز %1$s طۊل کشی + %1$d میلی‌ثانیه منپیز و اینترنتن نجوست: %s اینترنت من دسرس نؽ کود خطا: #%d - منپیز هڌ، سی واجۊری کیلیک کوݩ + منپیز هڌ. سی واجۊری کیلیک کوݩ. منپیز نؽڌ آزمایشا هونی ٱنجوم ابۊن: %s - اشتراک وا مووفقیت زفت وابی - اشتراک زفت نوابی + اشتراک وا مووفقیت و من ٱووڌه وابی + و من ٱووردن اشتراک مووفق نبی سامووا فرگمنت (Fragment) کتنا فرگمنت طۊل کتنا فرگمنت (حداقل-حداکثر) @@ -408,7 +414,7 @@ نوسخه نۊ ن جوست: %s سکو ورۊ رسۊوی کۊنین واجۊری نوسخه یل پؽش ز تیجنیڌن - ورۊ رسۊوی ن هونی واجۊری اکونه... + ورۊ رسۊوی ن هونی واجۊری اکونه… نوع بونکۊ سیاست ز بونکۊ اشتراک @@ -435,14 +441,14 @@ رزم دایرکتوری ره دیر (اختیاری) - QRcode + کود QR و در کشیڌن من کلیپ بورد و در کشیڌن پوی کانفیگ من کلیپ بورد آلشتکاری پاک کردن بستن - QRcode + کود QR و در کشیڌن من کلیپ بورد @@ -500,14 +506,6 @@ لف گیری گوزارش روفتن گوزارش ٱووردن قانووݩ - هیچ کانفیگ پسند وابیڌه نؽ - خوندن کانفیگ پسند وابیڌه مووفق نبی - پردازشت کانفیگ پسند وابیڌه مووفق نبی - ساڌن کانفیگ هسته مووفق نبی - ساڌن کانفیگ هسته مووفق نبی: %1$s - ساڌن کانفیگ هسته سی آزمایش سورعت مووفق نبی: %1$s - کانفیگ سفارشی نؽ - ره وستن هسته مووفق نبی ره وستن سرویس واڌاشتن سرویس برگشت diff --git a/V2rayNG/app/src/main/res/values-fa/strings.xml b/V2rayNG/app/src/main/res/values-fa/strings.xml index 9944a53c45..28bbc36825 100644 --- a/V2rayNG/app/src/main/res/values-fa/strings.xml +++ b/V2rayNG/app/src/main/res/values-fa/strings.xml @@ -8,6 +8,8 @@ بستن منو کشویی انتقال داده‌ها با موفقیت انجام شد! توقف سرویس + تأیید + لغو انتقال داده‌ها با شکست مواجه شد! برای بروزرسانی، صفحه را به پایین بکشید! @@ -27,7 +29,7 @@ ذخیره کانفیگ ویرایش کردن کانفیگ حذف کانفیگ - کانفیگ را از QRcode وارد کنید + کانفیگ را از کد QR وارد کنید کانفیگ را از کلیپ ‌بورد وارد کنید کانفیگ را از محلی وارد کنید افزودن [گروه خط‌‌ مشی] @@ -38,7 +40,7 @@ افزودن [SOCKS] افزودن [HTTP] افزودن [Trojan] - افزودن [Wireguard] + افزودن [WireGuard] افزودن [Hysteria2] آیا مطمئن هستید که می‌خواهید همه پروفایل‌های نمایش‌داده‌شده را حذف کنید؟ آیا مطمئن هستید که می‌خواهید همه پروفایل‌های تکراری نمایش‌داده‌شده را حذف کنید؟ @@ -73,7 +75,7 @@ مسیر HTTPUpgrade مسیر XHTTP مسیر H2 - مسیر QUIC + کلید QUIC KCP سید gRPC نام سرویس TLS @@ -118,13 +120,13 @@ هاست (SNI) (اختیاری) این عمل ممنوع است رمز عبور obfs - پورت پرش (درگاه سرور را بازنویسی می کند) + پرش پورت (پورت سرور را بازنویسی می‌کند) فاصله پورت پرش (ثانیه) - کاهش پهنای باند (واحد) - افزایش پهنای باند (واحد) + پهنای باند دانلود (واحدهای پشتیبانی‌شده: k/m/g/t) + پهنای باند آپلود (واحدهای پشتیبانی‌شده: k/m/g/t) حالت XHTTP خام JSON XHTTP Extra، قالب: { XHTTPObject } - finalMask raw JSON, format: { FinalMaskObject } + JSON خام finalMask، قالب: { FinalMaskObject } echConfigList تأیید گواهی همتا بر اساس نام اثر انگشت گواهی (SHA-256) @@ -139,24 +141,24 @@ کپی فایل شکست خورد، لطفا از برنامه مدیریت فایل استفاده کنید افزودن منبع افزودن فایل ‌ها - افزودن لینک - اسکن QRcode + افزودن URL + اسکن کد QR URL دانلود فایل‌ ها - آدرس اینترنتی را اضافه کنید + افزودن URL منبع فایل پیدا نشد این نام از قبل وجود دارد منبع فایل های جغرافیایی (اختیاری) - در حال بارگذاری... + در حال بارگذاری… جستجو انتخاب همه معکوس کردن انتخاب‌ها کلمات کلیدی را وارد کنید حالت دور زدن انتخاب خودکار برنامه‌های نیازمند پروکسی - در حال دانلود محتوا... + در حال دانلود محتوا… خروجی گرفتن در کلیپ‌ بورد وارد کردن از کلیپ‌ بورد تنظیمات به تفکیک برنامه @@ -169,7 +171,7 @@ تنظیمات هسته تنظیمات VPN پروکسی به تفکیک برنامه - عمومی: برنامه انتخاب شده از طریق یک پروکسی متصل می شود، برنامه انتخاب نشده مستقیماً متصل می شود. \nحالت دور زدن: برنامه انتخاب شده مستقیماً متصل می شود، برنامه انتخاب نشده از طریق یک پروکسی متصل می شود. \nانتخاب خودکار برنامه های پراکسی در منو امکان پذیر است. + حالت عادی: برنامه‌های انتخاب‌شده از پروکسی استفاده می‌کنند و برنامه‌های انتخاب‌نشده مستقیماً متصل می‌شوند؛\nحالت دور زدن: برنامه‌های انتخاب‌شده مستقیماً متصل می‌شوند و برنامه‌های انتخاب‌نشده از پروکسی استفاده می‌کنند.\nگزینهٔ انتخاب خودکار برنامه‌هایی که باید از پروکسی استفاده کنند در منو قرار دارد. اتصال خودکار هنگام راه اندازی هنگام راه اندازی به طور خودکار به سرور انتخابی متصل می شود که ممکن است ناموفق باشد. @@ -184,7 +186,7 @@ تنظیمات Mux فعال کردن Mux - سریعتر است، اما ممکن است باعث اتصال ناپایدار شود\nمخزن ترافیک TCP با 8 اتصال پیش‌فرض، نحوه مدیریت UDP و QUIC را در زیر سفارشی کنید. + سریع‌تر است، اما ممکن است اتصال را ناپایدار کند.\nدر ادامه می‌توانید نحوهٔ پردازش TCP، UDP و QUIC را تنظیم کنید. اتصالات TCP (محدوده -1 تا 1024) اتصالات XUDP (محدوده -1 تا 1024) مدیریت QUIC در تونل MUX @@ -200,7 +202,7 @@ فعال‌سازی تشخیص دامنهٔ مقصد در ترافیک (Sniffing) شناسایی دامنه‌های مقصد از ترافیک (به‌طور پیش‌فرض فعال است). فعال‌سازی routeOnly - از نام دامنه (Snnifed) فقط برای مسیریابی استفاده کنید و آدرس مقصد را به عنوان IP ذخیره کنید. + از نام دامنهٔ شناسایی‌شده فقط برای مسیریابی استفاده کنید و آدرس مقصد را به‌صورت IP نگه دارید. فعال کردن DNS محلی درخواست های DNS به هسته وارد شده و توسط ماژول DNS پردازش می شوند (توصیه می شود در صورت نیاز به مسیریابی برای دور زدن آدرس های LAN و سرزمین اصلی فعال شود) @@ -211,13 +213,13 @@ فعال کردن IPv6 آدرس و مسیرهای IPv6 را به رابط VPN اضافه کنید - ترجیح دادن IPV6 + ترجیح دادن IPv6 هنگام بازیابی نام دامنه، آدرس‌های IPv6 را ترجیح دهید - DNS از راه دور (اختیاری) (udp/tcp/https/quic) + DNS راه دور (UDP/TCP/HTTPS/QUIC، اختیاری) DNS - VPN DNS (فقط IPv4/v6) + VPN DNS (فقط IPv4/IPv6) آیا VPN از شبکه محلی عبور می کند؟ آدرس واسط VPN @@ -225,17 +227,17 @@ VPN MTU (پیش‌فرض 1500) - DNS داخلی (اختیاری) + DNS مستقیم (اختیاری) DNS - DNS مستقیم هاست (فرمت: دامنه:آدرس،…) + نگاشت‌های میزبان DNS (قالب: domain:address,…) دامنه:آدرس،… - آدرس اینترنتی آزمایش تاخیر واقعی کانفیگ ها + URL آزمایش تأخیر واقعی URL آزمایش‌های هم‌زمان تأخیر واقعی - تست اطلاعات اتصال فعلی URL + URL آزمایش اطلاعات اتصال فعلی URL اشتراک‌گذاری پروکسی در شبکه محلی (LAN) @@ -246,7 +248,7 @@ فعال‌سازی پروکسی محلی اگر غیرفعال باشد، هیچ پروکسی SOCKS در دسترس نخواهد بود و کارهایی مثل آپدیت اشتراک‌ها نمی‌توانند از پروکسی استفاده کنند. SOCKS5 UDP - احراز هویت socks5 هنگام استفاده از UDP کاملاً ایمن نیست + احراز هویت SOCKS5 هنگام استفاده از UDP کاملاً ایمن نیست. پورت پروکسی محلی تولید پورت متغیر برای پروکسی محلی با هر بار ایجاد یک کانکشن ورودی، یک پورت تصادفی تولید می‌کند. @@ -287,7 +289,7 @@ به‌ روزرسانی خودکار اشتراک ها اشتراک های خود را به طور خودکار با فاصله زمانی در پس زمینه به روز کنید. بسته به دستگاه، این ویژگی ممکن است همیشه کار نکند. - فاصله به‌ روزرسانی خودکار ( حداقل مقدار ، 15 دقیقه ) + فاصلهٔ به‌روزرسانی خودکار (دقیقه؛ حداقل 15) سطح گزارشات روش نام‌گشایی دامنهٔ خروجی (outbound) @@ -296,10 +298,10 @@ زبان تنظیمات رابط کاربری تنظیمات حالت رابط کاربری - استفاده از قابلیت Hev TUN + استفاده از Hev TUN اگر فعال باشد، تونل VPN از hev-socks5-tunnel استفاده می‌کند؛ در غیر این صورت از xray-core استفاده خواهد شد. - سطح گزارشات Hev Tun - زمان انتظار خواندن/نوشتن Hev Tun (ثانیه) (پیش‌فرض tcp,udp 300،60) + سطح گزارش‌های Hev TUN + مهلت خواندن/نوشتن Hev TUN (ثانیه؛ پیش‌فرض TCP/UDP: 300/60) تنظیمات حالت فعال‌سازی حالت Root (کاربران Root) برای پروکسی شفاف سطح پایین از دسترسی Root استفاده می‌کند. حالت Root به‌جای سرویس VPN در Android، ترافیک شبکه را مستقیماً مدیریت می‌کند. هنگام فعال بودن این حالت، حالت عادی، مسیریابی جداگانهٔ برنامه‌ها و تنظیمات مرتبط با VPN اعمال نمی‌شوند. @@ -315,25 +317,27 @@ حذف تمام کانفیگ های گروه فعلی حذف کانفیگ های تکراری گروه فعلی حذف کانفیگ های نامعتبر گروه فعلی (ابتدا آزمایش کنید) - خروجی گرفتن کانفیگ های غیرسفارشی گروه فعلی در کلیپ ‌بورد - تنظیمات گروه‌ اشتراک + صادر کردن کانفیگ‌ها به کلیپ‌بورد + اشتراک‌ها ملاحظات - نشانی اینترنتی اختیاری + URL اختیاری User Agent سرآیندهای درخواست (قالب JSON) - نام مستعار فیلتر + فیلتر عبارت منظم برای نام‌ها فعال کردن به‌روزرسانی فعال سازی به‌روزرسانی خودکار مجاز کردن آدرس HTTP ناامن - نام مستعار پروکسی قبلی - نام مستعار پروکسی بعدی + پروکسی ورودی + پروکسی خروجی لطفاً مطمئن شوید که نام مستعار وجود دارد و منحصر به فرد است + پروکسی انتخاب‌شده پیش از هر پروفایل این اشتراک، به‌عنوان حلقهٔ ورودی زنجیرهٔ پروکسی آن پروفایل اضافه می‌شود. + پروکسی انتخاب‌شده پس از هر پروفایل این اشتراک، به‌عنوان حلقهٔ خروجی زنجیرهٔ پروکسی آن پروفایل اضافه می‌شود. بازه بروزرسانی نامعتبر است. حداقل مقدار 15 دقیقه است. به‌روزرسانی گروه فعلی اشتراک - TCPING کانفیگ های گروه فعلی - تاخیر واقعی کانفیگ های گروه فعلی + آزمایش تأخیر TCP (TCPing) + آزمایش تأخیر واقعی فایل های منبع جغرافیایی - مرتب‌ سازی بر اساس نتایج آزمایش + مرتب‌سازی بر اساس نتایج آزمایش فیلتر کردن کانفیگ‌ها همه برای باز کردن دوربین و اسکن، روی صفحه بزنید @@ -353,7 +357,8 @@ استراتژی دامنه تنظیمات مسیریابی - با کاما (,) جدا کنید. یکی از موارد domain، ip یا process را وارد کنید + یک قانون فقط زمانی اعمال می‌شود که همهٔ شرط‌های تطبیق واردشده هم‌زمان برقرار باشند (AND). قوانین از بالا به پایین بررسی می‌شوند و نخستین قانون منطبق اعمال می‌شود. + چند ورودی را با کاما (,) جدا کنید. ذخیره حذف تنظیمات قانون مسیریابی @@ -361,7 +366,7 @@ وارد کردن مجموعه قوانین از پیش تعریف شده مجموعه قوانین موجود حذف خواهند شد، آیا مطمئن هستید که ادامه می دهید؟ وارد کردن مجموعه قوانین از کلیپ بورد - وارد کردن مجموعه قوانین از QRcode + وارد کردن مجموعه قوانین از کد QR صادر کردن مجموعه قوانین به کلیپ بورد قفل است، این قانون را هنگام وارد کردن از پیش تنظیم‌ ها حفظ کنید domain @@ -376,24 +381,25 @@ outboundTag اتصال را بررسی کنید - در حال آزمایش... + در حال آزمایش… تست کردن %d کانفیگ… - موفقیت: اتصال %dms طول کشید - اتصال به اینترنت شناسایی نشد: %s + اتصال در %1$s با موفقیت برقرار شد + %1$d میلی‌ثانیه + آزمایش اتصال ناموفق بود: %s اینترنت در دسترس نیست کد خطا: #%d - متصل است، برای بررسی اتصال ضربه بزنید + متصل است.\nبرای بررسی اتصال ضربه بزنید. متصل نیست آزمایش‌های در حال اجرا: %s - اشتراک با موفقیت ذخیره شد - ذخیره اشتراک ناموفق بود + اشتراک با موفقیت وارد شد + وارد کردن اشتراک ناموفق بود تنظیمات قطعه‌بندی بسته‌ها (Fragment) - بسته های فرگمنت - طول بسته های فرگمنت (حداقل-حداکثر) - فاصله بین بسته های فرگمنت (حداقل-حداکثر) - فعال کردن فرگمنت - حداکثر تقسیم فرگمنت + بسته‌های مورد قطعه‌بندی (Fragment) + طول قطعه (حداقل-حداکثر) + فاصلهٔ قطعه‌ها (حداقل-حداکثر) + فعال کردن Fragment + حداکثر تعداد قطعه‌ها تنظیمات پایش اتصال (Observatory) فاصله leastPing فاصله leastLoad @@ -419,7 +425,7 @@ اعضای زنجیره پروکسی برای انتخاب عضو اینجا بزنید یک عضو را انتخاب کنید - لطفا برای هر ردیف زنجیره remark انتخاب کنید + لطفاً برای هر ردیف زنجیره یک عضو انتخاب کنید تعداد اعضا کافی نیست اعضای نامعتبر زنجیره: %1$s @@ -429,20 +435,20 @@ بازیابی پیکربندی اشتراک گذاری پیکربندی تنظیمات WebDAV - لطفا ابتدا WebDAV را پیکربندی کنید. - لینک سرور WebDAV - یوزرنیم - پسورد + لطفاً ابتدا WebDAV را پیکربندی کنید. + URL سرور WebDAV + نام کاربری + رمز عبور دایرکتوری راه دور (اختیاری) - QRcode + کد QR خروجی گرفتن در کلیپ‌ بورد خروجی گرفتن کانفیگ کامل در کلیپ بورد ویرایش حذف بستن - QRcode + کد QR خروجی گرفتن در کلیپ‌ بورد @@ -464,8 +470,8 @@ لیست سفید چین ( دور زدن سرزمین اصلی ) لیست سیاه چین - جهانی(GLOBAL) - ایران + جهانی (Global) + فهرست سفید ایران لیست سفید روسیه @@ -500,14 +506,6 @@ کپی گزارش پاک کردن گزارش افزودن قانون مسیریابی - هیچ پیکربندی‌ای انتخاب نشده است - خواندن پیکربندی انتخاب‌شده ناموفق بود - آماده‌سازی پیکربندی انتخاب‌شده ناموفق بود - ایجاد پیکربندی هسته ناموفق بود - ایجاد پیکربندی هسته ناموفق بود: %1$s - ایجاد پیکربندی هسته برای آزمایش سرعت ناموفق بود: %1$s - پیکربندی سفارشی خالی است - راه‌اندازی هسته ناموفق بود شروع سرویس توقف سرویس بازگشت diff --git a/V2rayNG/app/src/main/res/values-night/themes.xml b/V2rayNG/app/src/main/res/values-night/themes.xml index 5fe5bbee56..672c89ce85 100644 --- a/V2rayNG/app/src/main/res/values-night/themes.xml +++ b/V2rayNG/app/src/main/res/values-night/themes.xml @@ -1,7 +1,7 @@ -