App language change (#1370)

* Add back option to change app langauge

Courtesy of AndroidX/Appcompat
On Android 13, it uses the native methods (at least for now) Also adding locales config file to only list supported languages in android app info's setting

A bit about this for A12 and under, an app restart is needed right now when switching from LTR to RTL or vice versa, not sure if later versions of androidx app compat will change this

* stop using preference context for strings (when possible)

which fixes some of the language issues on a12 and under

* Fixes to timespanfromnow method

now respect set language

* Dont show langauge selector on android 6

it doesn't work so rip

* Filter out unsupported locales in language selector

* appcompat -> 1.6.0-beta01

* Set notifications to use app language

* Add beta tag to language

* Update ExtensionHolder.kt
This commit is contained in:
Jays2Kings 2022-08-17 14:57:47 -04:00 committed by GitHub
parent 0cf6393ad6
commit 30b4b589e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 347 additions and 124 deletions

View file

@ -126,10 +126,10 @@ dependencies {
implementation("tachiyomi.sourceapi:source-api:1.1")
// Android X libraries
implementation("androidx.appcompat:appcompat:1.6.0-alpha03")
implementation("androidx.appcompat:appcompat:1.6.0-beta01")
implementation("androidx.cardview:cardview:1.0.0")
implementation("com.google.android.material:material:1.7.0-alpha02")
implementation("androidx.webkit:webkit:1.4.0")
implementation("androidx.webkit:webkit:1.5.0-rc01")
implementation("androidx.recyclerview:recyclerview:1.2.1")
implementation("androidx.preference:preference:1.2.0")
implementation("androidx.annotation:annotation:1.4.0")

View file

@ -36,6 +36,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:largeHeap="true"
android:localeConfig="@xml/locales_config"
android:theme="@style/Theme.Tachiyomi"
android:networkSecurityConfig="@xml/network_security_config">
<activity
@ -239,6 +240,15 @@
android:name=".data.backup.BackupRestoreService"
android:exported="false"/>
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"

View file

@ -26,6 +26,7 @@ import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.ui.source.SourcePresenter
import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.notification
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -90,10 +91,11 @@ open class App : Application(), DefaultLifecycleObserver {
val notificationManager = NotificationManagerCompat.from(this)
if (enabled) {
disableIncognitoReceiver.register()
val notification = notification(Notifications.CHANNEL_INCOGNITO_MODE) {
val incogText = getString(R.string.incognito_mode)
val nContext = localeContext
val notification = nContext.notification(Notifications.CHANNEL_INCOGNITO_MODE) {
val incogText = nContext.getString(R.string.incognito_mode)
setContentTitle(incogText)
setContentText(getString(R.string.turn_off_, incogText))
setContentText(nContext.getString(R.string.turn_off_, incogText))
setSmallIcon(R.drawable.ic_incognito_24dp)
setOngoing(true)

View file

@ -16,6 +16,7 @@ import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.notificationManager
import timber.log.Timber
import uy.kohesive.injekt.Injekt
@ -27,7 +28,7 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
override fun doWork(): Result {
val preferences = Injekt.get<PreferencesHelper>()
val notifier = BackupNotifier(context)
val notifier = BackupNotifier(context.localeContext)
val uri = inputData.getString(LOCATION_URI_KEY)?.let { Uri.parse(it) }
?: preferences.backupsDirectory().get().toUri()
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL)

View file

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupRestore
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning
import eu.kanade.tachiyomi.util.system.localeContext
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -61,7 +62,7 @@ class BackupRestoreService : Service() {
fun stop(context: Context) {
context.stopService(Intent(context, BackupRestoreService::class.java))
BackupNotifier(context).showRestoreError(context.getString(R.string.restoring_backup_canceled))
BackupNotifier(context.localeContext).showRestoreError(context.getString(R.string.restoring_backup_canceled))
}
}
@ -78,7 +79,7 @@ class BackupRestoreService : Service() {
super.onCreate()
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
notifier = BackupNotifier(this)
notifier = BackupNotifier(this.localeContext)
wakeLock = acquireWakeLock()
startForeground(Notifications.ID_RESTORE_PROGRESS, notifier.showRestoreProgress().build())

View file

@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notificationManager
import uy.kohesive.injekt.injectLazy
@ -69,6 +70,7 @@ internal class DownloadNotifier(private val context: Context) {
}
fun setPlaceholder(download: Download?) {
val context = context.localeContext
with(notification) {
// Check if first call.
if (!isDownloading) {
@ -140,7 +142,7 @@ internal class DownloadNotifier(private val context: Context) {
}
val downloadingProgressText =
context.getString(R.string.downloading_progress)
context.localeContext.getString(R.string.downloading_progress)
.format(download.downloadedImages, download.pages!!.size)
if (preferences.hideNotificationContent()) {
@ -166,6 +168,7 @@ internal class DownloadNotifier(private val context: Context) {
* Show notification when download is paused.
*/
fun onDownloadPaused() {
val context = context.localeContext
with(notification) {
setContentTitle(context.getString(R.string.paused))
setContentText(context.getString(R.string.download_paused))
@ -203,6 +206,7 @@ internal class DownloadNotifier(private val context: Context) {
* @param reason the text to show.
*/
fun onWarning(reason: String) {
val context = context.localeContext
with(notification) {
setContentTitle(context.getString(R.string.downloads))
setContentText(reason)
@ -223,6 +227,7 @@ internal class DownloadNotifier(private val context: Context) {
* Called when the downloader has too many downloads from one source.
*/
fun massDownloadWarning() {
val context = context.localeContext
val notification = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER) {
setContentTitle(context.getString(R.string.warning))
setSmallIcon(R.drawable.ic_warning_white_24dp)
@ -260,6 +265,7 @@ internal class DownloadNotifier(private val context: Context) {
customIntent: Intent? = null,
) {
// Create notification
val context = context.localeContext
with(notification) {
setContentTitle(
mangaTitle?.plus(": $chapter") ?: context.getString(R.string.download_error),

View file

@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.util.system.connectivityManager
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
import eu.kanade.tachiyomi.util.system.isOnline
import eu.kanade.tachiyomi.util.system.isServiceRunning
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.powerManager
import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.injectLazy
@ -241,7 +242,7 @@ class DownloadService : Service() {
private fun getPlaceholderNotification(): Notification {
return NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER)
.setContentTitle(getString(R.string.downloading))
.setContentTitle(localeContext.getString(R.string.downloading))
.build()
}
}

View file

@ -42,6 +42,7 @@ import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import eu.kanade.tachiyomi.util.system.localeContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
@ -170,7 +171,7 @@ class LibraryUpdateService(
*/
override fun onCreate() {
super.onCreate()
notifier = LibraryUpdateNotifier(this)
notifier = LibraryUpdateNotifier(this.localeContext)
wakeLock = acquireWakeLock(timeout = TimeUnit.MINUTES.toMillis(30))
startForeground(Notifications.ID_LIBRARY_PROGRESS, notifier.progressNotificationBuilder.build())
}
@ -380,7 +381,7 @@ class LibraryUpdateService(
val errorFile = writeErrorFile(failedUpdates).getUriCompat(this)
notifier.showUpdateErrorNotification(failedUpdates.map { it.key.title }, errorFile)
}
mangaShortcutManager.updateShortcuts()
mangaShortcutManager.updateShortcuts(this)
failedUpdates.clear()
notifier.cancelProgressNotification()
if (runExtensionUpdatesAfter && !DownloadService.isRunning(this)) {

View file

@ -237,6 +237,8 @@ class PreferencesHelper(val context: Context) {
else -> SimpleDateFormat(format, Locale.getDefault())
}
fun appLanguage() = flowPrefs.getString("app_language", "")
fun downloadsDirectory() = flowPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString())
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)

View file

@ -8,6 +8,7 @@ import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.toast
class AppUpdateBroadcast : BroadcastReceiver() {
@ -27,7 +28,7 @@ class AppUpdateBroadcast : BroadcastReceiver() {
val notifyOnInstall = extras.getBoolean(AppUpdateService.EXTRA_NOTIFY_ON_INSTALL, false)
try {
if (notifyOnInstall) {
AppUpdateNotifier(context).onInstallFinished()
AppUpdateNotifier(context.localeContext).onInstallFinished()
}
} finally {
AppUpdateService.stop(context)
@ -37,7 +38,7 @@ class AppUpdateBroadcast : BroadcastReceiver() {
if (status != PackageInstaller.STATUS_FAILURE_ABORTED) {
context.toast(R.string.could_not_install_update)
val uri = intent.getStringExtra(AppUpdateService.EXTRA_FILE_URI) ?: return
AppUpdateNotifier(context).onInstallError(uri.toUri())
AppUpdateNotifier(context.localeContext).onInstallError(uri.toUri())
}
}
}
@ -48,7 +49,7 @@ class AppUpdateBroadcast : BroadcastReceiver() {
remove(AppUpdateService.NOTIFY_ON_INSTALL_KEY)
}
if (notifyOnInstall) {
AppUpdateNotifier(context).onInstallFinished()
AppUpdateNotifier(context.localeContext).onInstallFinished()
}
}
}

View file

@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.withIOContext
import uy.kohesive.injekt.injectLazy
import java.util.Date
@ -63,7 +64,7 @@ class AppUpdateChecker {
) {
AutoAppUpdaterJob.setupTask(context)
}
AppUpdateNotifier(context).promptUpdate(result.release)
AppUpdateNotifier(context.localeContext).promptUpdate(result.release)
}
result

View file

@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.network.newCallWithProgress
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
@ -52,7 +53,7 @@ class AppUpdateService : Service() {
override fun onCreate() {
super.onCreate()
notifier = AppUpdateNotifier(this)
notifier = AppUpdateNotifier(this.localeContext)
startForeground(Notifications.ID_UPDATER, notifier.onDownloadStarted(getString(R.string.app_name)).build())

View file

@ -11,6 +11,7 @@ import androidx.work.WorkManager
import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
import eu.kanade.tachiyomi.util.system.localeContext
import kotlinx.coroutines.coroutineScope
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -32,7 +33,7 @@ class AutoAppUpdaterJob(private val context: Context, workerParams: WorkerParame
}
val result = AppUpdateChecker().checkForUpdate(context, true, doExtrasAfterNewUpdate = false)
if (result is AppUpdateResult.NewUpdate && !AppUpdateService.isRunning()) {
AppUpdateNotifier(context).cancel()
AppUpdateNotifier(context.localeContext).cancel()
AppUpdateNotifier.releasePageUrl = result.release.releaseLink
AppUpdateService.start(context, result.release.downloadLink, false)
}

View file

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.extension.ExtensionManager.ExtensionInfo
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineScope
@ -140,7 +141,7 @@ class ExtensionInstallService(
override fun onCreate() {
super.onCreate()
notificationManager.cancel(Notifications.ID_UPDATES_TO_EXTS)
notifier = ExtensionInstallNotifier(this)
notifier = ExtensionInstallNotifier(this.localeContext)
wakeLock = acquireWakeLock(timeout = TimeUnit.MINUTES.toMillis(30))
startForeground(Notifications.ID_EXTENSION_PROGRESS, notifier.progressNotificationBuilder.build())
}

View file

@ -26,6 +26,7 @@ import eu.kanade.tachiyomi.data.updater.AutoAppUpdaterJob
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.util.system.connectivityManager
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.notification
import kotlinx.coroutines.coroutineScope
import rikka.shizuku.Shizuku
@ -110,6 +111,7 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
notify(
Notifications.ID_UPDATES_TO_EXTS,
context.notification(Notifications.CHANNEL_UPDATES_TO_EXTS) {
val context = context.localeContext
setContentTitle(
context.resources.getQuantityString(
R.plurals.extension_updates_available,

View file

@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.main.SearchActivity
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.getThemeWithExtras
import eu.kanade.tachiyomi.util.system.setLocaleByAppCompat
import eu.kanade.tachiyomi.util.system.setThemeByPref
import uy.kohesive.injekt.injectLazy
@ -20,6 +21,7 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
private var updatedTheme: Resources.Theme? = null
override fun onCreate(savedInstanceState: Bundle?) {
setLocaleByAppCompat()
updatedTheme = null
setThemeByPref(preferences)
super.onCreate(savedInstanceState)

View file

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.getThemeWithExtras
import eu.kanade.tachiyomi.util.system.setLocaleByAppCompat
import eu.kanade.tachiyomi.util.system.setThemeByPref
import nucleus.view.NucleusAppCompatActivity
import uy.kohesive.injekt.injectLazy
@ -18,6 +19,7 @@ abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P
private var updatedTheme: Resources.Theme? = null
override fun onCreate(savedInstanceState: Bundle?) {
setLocaleByAppCompat()
updatedTheme = null
setThemeByPref(preferences)
super.onCreate(savedInstanceState)

View file

@ -5,6 +5,7 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.system.getThemeWithExtras
import eu.kanade.tachiyomi.util.system.setLocaleByAppCompat
import eu.kanade.tachiyomi.util.system.setThemeByPref
import uy.kohesive.injekt.injectLazy
@ -14,6 +15,7 @@ abstract class BaseThemedActivity : AppCompatActivity() {
private var updatedTheme: Resources.Theme? = null
override fun onCreate(savedInstanceState: Bundle?) {
setLocaleByAppCompat()
updatedTheme = null
setThemeByPref(preferences)
super.onCreate(savedInstanceState)

View file

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.category
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.library.LibrarySort
import eu.kanade.tachiyomi.util.system.executeOnIO
import kotlinx.coroutines.CoroutineScope
@ -20,11 +19,8 @@ import uy.kohesive.injekt.api.get
class CategoryPresenter(
private val controller: CategoryController,
private val db: DatabaseHelper = Injekt.get(),
preferences: PreferencesHelper = Injekt.get(),
) {
private val context = preferences.context
private var scope = CoroutineScope(Job() + Dispatchers.Default)
/**
@ -51,7 +47,8 @@ class CategoryPresenter(
}
private fun newCategory(): Category {
val default = Category.create(context.getString(R.string.create_new_category))
val default =
Category.create(controller.view?.context?.getString(R.string.create_new_category) ?: "")
default.order = CREATE_CATEGORY_ORDER
default.id = Int.MIN_VALUE
return default

View file

@ -49,20 +49,15 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
InstalledExtensionsOrder.RecentlyUpdated -> {
extensionUpdateDate(extension.pkgName)?.let {
binding.date.isVisible = true
binding.date.text = itemView.context.getString(
R.string.updated_,
it.timeSpanFromNow,
)
binding.date.text = itemView.context.timeSpanFromNow(R.string.updated_, it)
infoText.add("")
}
}
InstalledExtensionsOrder.RecentlyInstalled -> {
extensionInstallDate(extension.pkgName)?.let {
binding.date.isVisible = true
binding.date.text = itemView.context.getString(
R.string.installed_,
it.timeSpanFromNow,
)
binding.date.text =
itemView.context.timeSpanFromNow(R.string.installed_, it)
infoText.add("")
}
}

View file

@ -176,7 +176,8 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
override fun onCreateBubbleText(position: Int): String {
val preferences: PreferencesHelper by injectLazy()
val db: DatabaseHelper by injectLazy()
if (position == itemCount - 1) return recyclerView.context.getString(R.string.bottom)
val context = recyclerView.context
if (position == itemCount - 1) return context.getString(R.string.bottom)
return when (val item: IFlexible<*>? = getItem(position)) {
is LibraryHeaderItem -> {
vibrateOnCategoryChange(item.category.name)
@ -188,7 +189,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
LibrarySort.DragAndDrop -> {
if (item.header.category.isDynamic) {
val category = db.getCategoriesForManga(item.manga).executeAsBlocking().firstOrNull()?.name
category ?: recyclerView.context.getString(R.string.default_value)
category ?: context.getString(R.string.default_value)
} else {
val title = item.manga.title
if (preferences.removeArticles().get()) title.removeArticles().chop(15)
@ -200,10 +201,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
val history = db.getChapters(id).executeAsBlocking()
val last = history.maxOfOrNull { it.date_fetch }
if (last != null && last > 100) {
recyclerView.context.getString(
R.string.fetched_,
last.timeSpanFromNow(preferences.context),
)
context.timeSpanFromNow(R.string.fetched_, last)
} else {
"N/A"
}
@ -213,18 +211,15 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
val history = db.getHistoryByMangaId(id).executeAsBlocking()
val last = history.maxOfOrNull { it.last_read }
if (last != null && last > 100) {
recyclerView.context.getString(
R.string.read_,
last.timeSpanFromNow(preferences.context),
)
context.timeSpanFromNow(R.string.read_, last)
} else {
"N/A"
}
}
LibrarySort.Unread -> {
val unread = item.manga.unread
if (unread > 0) recyclerView.context.getString(R.string._unread, unread)
else recyclerView.context.getString(R.string.read)
if (unread > 0) context.getString(R.string._unread, unread)
else context.getString(R.string.read)
}
LibrarySort.TotalChapters -> {
val total = item.manga.totalChapters
@ -240,10 +235,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
LibrarySort.LatestChapter -> {
val lastUpdate = item.manga.last_update
if (lastUpdate > 0) {
recyclerView.context.getString(
R.string.updated_,
lastUpdate.timeSpanFromNow(preferences.context),
)
context.timeSpanFromNow(R.string.updated_, lastUpdate)
} else {
"N/A"
}
@ -251,7 +243,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
LibrarySort.DateAdded -> {
val added = item.manga.date_added
if (added > 0) {
recyclerView.context.getString(R.string.added_, added.timeSpanFromNow(preferences.context))
context.timeSpanFromNow(R.string.added_, added)
} else {
"N/A"
}

View file

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.library
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
@ -29,9 +30,9 @@ import uy.kohesive.injekt.injectLazy
class LibraryItem(
val manga: LibraryManga,
header: LibraryHeaderItem,
private val context: Context?,
private val preferences: PreferencesHelper = Injekt.get(),
) :
AbstractSectionableItem<LibraryHolder, LibraryHeaderItem>(header), IFilterable<String> {
) : AbstractSectionableItem<LibraryHolder, LibraryHeaderItem>(header), IFilterable<String> {
var downloadCount = -1
var unreadType = 2
@ -172,7 +173,8 @@ class LibraryItem(
private fun containsGenre(tag: String, genres: List<String>?): Boolean {
if (tag.trim().isEmpty()) return true
val seriesType by lazy { manga.seriesType(preferences.context, sourceManager) }
context ?: return false
val seriesType by lazy { manga.seriesType(context, sourceManager) }
return if (tag.startsWith("-")) {
val realTag = tag.substringAfter("-")
genres?.find {

View file

@ -66,6 +66,8 @@ class LibraryPresenter(
) : BaseCoroutinePresenter<LibraryController>() {
private val context = preferences.context
private val viewContext
get() = controller?.view?.context
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
@ -198,6 +200,7 @@ class LibraryPresenter(
LibraryItem(
LibraryManga.createBlank(id),
LibraryHeaderItem({ getCategory(id) }, id),
viewContext,
),
)
}
@ -581,7 +584,7 @@ class LibraryPresenter(
else headerItems[it.category]
) ?: return@mapNotNull null
categorySet.add(it.category)
LibraryItem(it, headerItem)
LibraryItem(it, headerItem, viewContext)
}.toMutableList()
val categoriesHidden = if (forceShowAllCategories) {
@ -599,7 +602,7 @@ class LibraryPresenter(
) {
val headerItem = headerItems[catId]
if (headerItem != null) items.add(
LibraryItem(LibraryManga.createBlank(catId), headerItem),
LibraryItem(LibraryManga.createBlank(catId), headerItem, viewContext),
)
} else if (catId in categoriesHidden && showAll && categories.size > 1) {
val mangaToRemove = items.filter { it.manga.category == catId }
@ -611,7 +614,14 @@ class LibraryPresenter(
items.removeAll(mangaToRemove)
val headerItem = headerItems[catId]
if (headerItem != null) items.add(
LibraryItem(LibraryManga.createHide(catId, mergedTitle, mangaToRemove.size), headerItem),
LibraryItem(
LibraryManga.createHide(
catId,
mergedTitle,
mangaToRemove.size,
),
headerItem, viewContext,
),
)
}
}
@ -672,7 +682,7 @@ class LibraryPresenter(
} ?: listOf(unknown)
}
tags.map {
LibraryItem(manga, makeOrGetHeader(it))
LibraryItem(manga, makeOrGetHeader(it), viewContext)
}
}
BY_TRACK_STATUS -> {
@ -688,9 +698,9 @@ class LibraryPresenter(
service.getStatus(track.status)
}
} else {
context.getString(R.string.not_tracked)
controller?.view?.context?.getString(R.string.not_tracked) ?: ""
}
listOf(LibraryItem(manga, makeOrGetHeader(status)))
listOf(LibraryItem(manga, makeOrGetHeader(status), viewContext))
}
BY_SOURCE -> {
val source = sourceManager.getOrStub(manga.source)
@ -698,12 +708,13 @@ class LibraryPresenter(
LibraryItem(
manga,
makeOrGetHeader("${source.name}$sourceSplitter${source.id}"),
viewContext,
),
)
}
BY_AUTHOR -> {
if (manga.artist.isNullOrBlank() && manga.author.isNullOrBlank()) {
listOf(LibraryItem(manga, makeOrGetHeader(unknown)))
listOf(LibraryItem(manga, makeOrGetHeader(unknown), viewContext))
} else {
listOfNotNull(
manga.author.takeUnless { it.isNullOrBlank() },
@ -714,11 +725,11 @@ class LibraryPresenter(
author.ifBlank { null }
}
}.flatten().distinct().map {
LibraryItem(manga, makeOrGetHeader(it, true))
LibraryItem(manga, makeOrGetHeader(it, true), viewContext)
}
}
}
else -> listOf(LibraryItem(manga, makeOrGetHeader(mapStatus(manga.status))))
else -> listOf(LibraryItem(manga, makeOrGetHeader(mapStatus(manga.status)), viewContext))
}
}.flatten().toMutableList()
@ -761,7 +772,11 @@ class LibraryPresenter(
sectionedLibraryItems[catId] = mangaToRemove
items.removeAll { it.header.catId == catId }
if (headerItem != null) items.add(
LibraryItem(LibraryManga.createHide(catId, mergedTitle, mangaToRemove.size), headerItem),
LibraryItem(
LibraryManga.createHide(catId, mergedTitle, mangaToRemove.size),
headerItem,
viewContext,
),
)
}
}

View file

@ -747,7 +747,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>(), DownloadServiceLi
}
fun saveExtras() {
mangaShortcutManager.updateShortcuts()
mangaShortcutManager.updateShortcuts(this)
MangaCoverMetadata.savePrefs()
}

View file

@ -374,7 +374,7 @@ class MangaDetailsPresenter(
.map { it.toModel() },
)
}
mangaShortcutManager.updateShortcuts()
controller?.view?.context?.let { mangaShortcutManager.updateShortcuts(it) }
}
if (newChapters.second.isNotEmpty()) {
val removedChaptersId = newChapters.second.map { it.id }
@ -447,7 +447,7 @@ class MangaDetailsPresenter(
) e.message?.split(": ")?.drop(1)
?.joinToString(": ")
else e.message
) ?: preferences.context.getString(R.string.unknown_error)
) ?: controller?.view?.context?.getString(R.string.unknown_error) ?: ""
}
/**
@ -636,7 +636,8 @@ class MangaDetailsPresenter(
filtersId.add(if (manga.bookmarkedFilter(preferences) == Manga.CHAPTER_SHOW_BOOKMARKED) R.string.bookmarked else null)
filtersId.add(if (manga.bookmarkedFilter(preferences) == Manga.CHAPTER_SHOW_NOT_BOOKMARKED) R.string.not_bookmarked else null)
filtersId.add(if (manga.filtered_scanlators?.isNotEmpty() == true) R.string.scanlators else null)
return filtersId.filterNotNull().joinToString(", ") { preferences.context.getString(it) }
return filtersId.filterNotNull()
.joinToString(", ") { controller?.view?.context?.getString(it) ?: "" }
}
fun setScanlatorFilter(filteredScanlators: Set<String>) {

View file

@ -30,6 +30,7 @@ import eu.kanade.tachiyomi.ui.setting.titleRes
import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.lang.toTimestampString
import eu.kanade.tachiyomi.util.system.isOnline
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.materialAlertDialog
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.openInBrowser
@ -97,7 +98,7 @@ class AboutController : SettingsController() {
onClick {
activity?.let {
val deviceInfo = CrashLogUtil(it).getDebugInfo()
val deviceInfo = CrashLogUtil(it.localeContext).getDebugInfo()
val clipboard = it.getSystemService<ClipboardManager>()!!
val appInfo = it.getString(R.string.app_info)
clipboard.setPrimaryClip(ClipData.newPlainText(appInfo, deviceInfo))

View file

@ -44,6 +44,7 @@ import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.executeOnIO
import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.toInt
import eu.kanade.tachiyomi.util.system.withUIContext
import kotlinx.coroutines.CoroutineScope
@ -343,14 +344,16 @@ class ReaderPresenter(
return delegatedSource.pageNumber(url)?.minus(1)
}
@Suppress("DEPRECATION")
suspend fun loadChapterURL(url: Uri) {
val host = url.host ?: return
val context = view ?: preferences.context
val delegatedSource = sourceManager.getDelegatedSource(host) ?: error(
preferences.context.getString(R.string.source_not_installed),
context.getString(R.string.source_not_installed),
)
val chapterUrl = delegatedSource.chapterUrl(url)
val sourceId = delegatedSource.delegate?.id ?: error(
preferences.context.getString(R.string.source_not_installed),
context.getString(R.string.source_not_installed),
)
if (chapterUrl != null) {
val dbChapter = db.getChapters(chapterUrl).executeOnIO().find {
@ -395,18 +398,18 @@ class ReaderPresenter(
delegatedSource.delegate!!,
).first
chapterId = newChapters.find { it.url == chapter.url }?.id
?: error(preferences.context.getString(R.string.chapter_not_found))
?: error(context.getString(R.string.chapter_not_found))
} else {
chapter.date_fetch = Date().time
chapterId = db.insertChapter(chapter).executeOnIO().insertedId() ?: error(
preferences.context.getString(R.string.unknown_error),
context.getString(R.string.unknown_error),
)
}
withContext(Dispatchers.Main) {
init(manga, chapterId)
}
}
} else error(preferences.context.getString(R.string.unknown_error))
} else error(context.getString(R.string.unknown_error))
}
/**
@ -838,7 +841,7 @@ class ReaderPresenter(
val manga = manga ?: return
val context = Injekt.get<Application>()
val notifier = SaveImageNotifier(context)
val notifier = SaveImageNotifier(context.localeContext)
notifier.onClear()
// Pictures directory.
@ -873,7 +876,7 @@ class ReaderPresenter(
val manga = manga ?: return@launch
val context = Injekt.get<Application>()
val notifier = SaveImageNotifier(context)
val notifier = SaveImageNotifier(context.localeContext)
notifier.onClear()
// Pictures directory.

View file

@ -118,58 +118,36 @@ class RecentMangaHolder(
}
val notValidNum = item.mch.chapter.chapter_number <= 0
binding.body.isVisible = !isSmallUpdates
val context = itemView.context
binding.body.text = when {
item.mch.chapter.id == null -> binding.body.context.getString(
R.string.added_,
item.mch.manga.date_added.timeSpanFromNow(itemView.context),
)
item.mch.chapter.id == null -> context.timeSpanFromNow(R.string.added_, item.mch.manga.date_added)
isSmallUpdates -> ""
item.mch.history.id == null -> {
if (adapter.viewType == RecentsPresenter.VIEW_TYPE_ONLY_UPDATES) {
if (adapter.sortByFetched) {
binding.body.context.getString(
R.string.fetched_,
item.chapter.date_fetch.timeSpanFromNow(itemView.context),
)
context.timeSpanFromNow(R.string.fetched_, item.chapter.date_fetch)
} else {
binding.body.context.getString(
R.string.updated_,
item.chapter.date_upload.timeSpanFromNow(itemView.context),
)
context.timeSpanFromNow(R.string.updated_, item.chapter.date_upload)
}
} else {
binding.body.context.getString(
R.string.fetched_,
item.chapter.date_fetch.timeSpanFromNow(itemView.context),
) + "\n" + binding.body.context.getString(
R.string.updated_,
item.chapter.date_upload.timeSpanFromNow(itemView.context),
)
context.timeSpanFromNow(R.string.fetched_, item.chapter.date_fetch) + "\n" +
context.timeSpanFromNow(R.string.updated_, item.chapter.date_upload)
}
}
item.chapter.id != item.mch.chapter.id ->
binding.body.context.getString(
R.string.read_,
item.mch.history.last_read.timeSpanFromNow,
) + "\n" + binding.body.context.getString(
if (notValidNum) R.string.last_read_ else R.string.last_read_chapter_,
if (notValidNum) item.mch.chapter.name else adapter.decimalFormat.format(item.mch.chapter.chapter_number),
)
item.chapter.pages_left > 0 && !item.chapter.read ->
binding.body.context.getString(
R.string.read_,
item.mch.history.last_read.timeSpanFromNow(itemView.context),
) + "\n" + itemView.resources.getQuantityString(
R.plurals.pages_left,
item.chapter.pages_left,
item.chapter.pages_left,
)
else -> binding.body.context.getString(
R.string.read_,
item.mch.history.last_read.timeSpanFromNow(itemView.context),
item.chapter.id != item.mch.chapter.id -> context.timeSpanFromNow(R.string.read_, item.mch.history.last_read) +
"\n" + binding.body.context.getString(
if (notValidNum) R.string.last_read_ else R.string.last_read_chapter_,
if (notValidNum) item.mch.chapter.name else adapter.decimalFormat.format(item.mch.chapter.chapter_number),
)
item.chapter.pages_left > 0 && !item.chapter.read -> context.timeSpanFromNow(R.string.read_, item.mch.history.last_read) +
"\n" + itemView.resources.getQuantityString(
R.plurals.pages_left,
item.chapter.pages_left,
item.chapter.pages_left,
)
else -> context.timeSpanFromNow(R.string.read_, item.mch.history.last_read)
}
if ((itemView.context as? Activity)?.isDestroyed != true) {
if ((context as? Activity)?.isDestroyed != true) {
binding.coverThumbnail.loadManga(item.mch.manga)
}
if (!item.mch.manga.isLocal()) {

View file

@ -39,6 +39,7 @@ import eu.kanade.tachiyomi.util.system.disableItems
import eu.kanade.tachiyomi.util.system.isPackageInstalled
import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.materialAlertDialog
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast
@ -90,7 +91,7 @@ class SettingsAdvancedController : SettingsController() {
summaryRes = R.string.saves_error_logs
onClick {
CrashLogUtil(context).dumpLogs()
CrashLogUtil(context.localeContext).dumpLogs()
}
}

View file

@ -1,14 +1,22 @@
package eu.kanade.tachiyomi.ui.setting
import android.content.Intent
import android.content.res.XmlResourceParser
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.View
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.BuildCompat
import androidx.core.os.LocaleListCompat
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.updater.AutoAppUpdaterJob
import eu.kanade.tachiyomi.util.lang.addBetaTag
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.system.systemLangContext
import java.util.Locale
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class SettingsGeneralController : SettingsController() {
@ -18,6 +26,8 @@ class SettingsGeneralController : SettingsController() {
var lastThemeXLight: Int? = null
var lastThemeXDark: Int? = null
var themePreference: ThemePreference? = null
@BuildCompat.PrereleaseSdkCheck
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.general
@ -131,6 +141,83 @@ class SettingsGeneralController : SettingsController() {
}
defaultValue = ""
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
listPreference(activity) {
key = "language"
isPersistent = false
title = context.getString(R.string.language).let {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
it.addBetaTag(context)
} else {
it
}
}
dialogTitleRes = R.string.language
val locales = mutableListOf<String>()
val availLocales = Locale.getAvailableLocales()
resources?.getXml(R.xml.locales_config).use { parser ->
parser ?: return@use
while (parser.next() != XmlResourceParser.END_DOCUMENT) {
if (parser.eventType == XmlResourceParser.START_TAG && parser.name == "locale") {
val locale = parser.getAttributeValue(
"http://schemas.android.com/apk/res/android",
"name",
) ?: continue
if (availLocales.contains(Locale.forLanguageTag(locale))) {
locales.add(locale)
}
}
}
}
val localesMap = locales.associateBy { Locale.forLanguageTag(it) }
.toSortedMap { locale1, locale2 ->
val l1 = locale1.getDisplayName(locale1)
.replaceFirstChar { it.uppercase(locale1) }
val l2 = locale2.getDisplayName(locale2)
.replaceFirstChar { it.uppercase(locale2) }
l1.compareToCaseInsensitiveNaturalOrder(l2)
}
val localArray = localesMap.keys.filterNotNull().toTypedArray()
val localeList = LocaleListCompat.create(*localArray)
val sysDef = context.systemLangContext.getString(R.string.system_default)
entries = listOf(sysDef) + localesMap.keys.map { locale ->
locale.getDisplayName(locale).replaceFirstChar { it.uppercase(locale) }
}
entryValues = listOf("") + localesMap.values
defaultValue = ""
val locale = AppCompatDelegate.getApplicationLocales()
.getFirstMatch(locales.toTypedArray())
if (locale != null) {
tempValue = localArray.indexOf(
if (locales.contains(locale.toLanguageTag())) {
locale
} else {
localeList.getFirstMatch(arrayOf(locale.toLanguageTag()))
},
) + 1
tempEntry =
locale.getDisplayName(locale).replaceFirstChar { it.uppercase(locale) }
}
onChange {
val value = it as String
val appLocale: LocaleListCompat = if (value.isBlank()) {
preferences.appLanguage().delete()
LocaleListCompat.getEmptyLocaleList()
} else {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
preferences.appLanguage().set(value)
}
LocaleListCompat.forLanguageTags(value)
}
AppCompatDelegate.setApplicationLocales(appLocale)
true
}
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
infoPreference(R.string.language_requires_app_restart)
}
}
}
}

View file

@ -106,7 +106,7 @@ class SettingsSearchController :
binding.recycler.adapter = adapter
// load all search results
SettingsSearchHelper.initPreferenceSearchResultCollection(presenter.preferences.context)
SettingsSearchHelper.initPreferenceSearchResultCollection(view.context)
}
override fun onDestroyView(view: View) {

View file

@ -22,7 +22,6 @@ import eu.kanade.tachiyomi.ui.main.SearchActivity
import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
import eu.kanade.tachiyomi.ui.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.util.system.launchIO
import kotlinx.coroutines.GlobalScope
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -35,15 +34,14 @@ class MangaShortcutManager(
val sourceManager: SourceManager = Injekt.get(),
) {
val context: Context = preferences.context
fun updateShortcuts() {
fun updateShortcuts(context: Context) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) {
if (!preferences.showSeriesInShortcuts() && !preferences.showSourcesInShortcuts()) {
val shortcutManager = context.getSystemService(ShortcutManager::class.java)
shortcutManager.removeAllDynamicShortcuts()
return
}
GlobalScope.launchIO {
launchIO {
val shortcutManager = context.getSystemService(ShortcutManager::class.java)
val recentManga = if (preferences.showSeriesInShortcuts()) {

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.util.system
import android.app.ActivityManager
import android.app.LocaleManager
import android.app.Notification
import android.app.NotificationManager
import android.content.BroadcastReceiver
@ -44,6 +45,7 @@ import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.util.Locale
import kotlin.math.max
private const val TABLET_UI_MIN_SCREEN_WIDTH_DP = 720
@ -483,3 +485,41 @@ fun Context.getApplicationIcon(pkgName: String): Drawable? {
null
}
}
/** Context used for notifications as Appcompat app lang does not support notifications */
val Context.localeContext: Context
get() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) return this
val pref = Injekt.get<PreferencesHelper>()
val prefsLang = if (pref.appLanguage().isSet()) {
Locale.forLanguageTag(pref.appLanguage().get())
} else null
val configuration = Configuration(resources.configuration)
configuration.setLocale(
prefsLang
?: AppCompatDelegate.getApplicationLocales()[0]
?: Locale.getDefault(),
)
return createConfigurationContext(configuration)
}
fun setLocaleByAppCompat() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
AppCompatDelegate.getApplicationLocales().get(0)?.let { Locale.setDefault(it) }
}
}
val Context.systemLangContext: Context
get() {
val configuration = Configuration(resources.configuration)
val systemLocale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getSystemService<LocaleManager>()?.systemLocales?.get(0)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Resources.getSystem().configuration.locales.get(0)
} else {
return this
} ?: Locale.getDefault()
configuration.setLocale(systemLocale)
return createConfigurationContext(configuration)
}

View file

@ -18,6 +18,8 @@ fun Long.timeSpanFromNow(context: Context): String {
}
}
fun Context.timeSpanFromNow(res: Int, time: Long) = getString(res, time.timeSpanFromNow(this))
/**
* Convert local time millisecond value to Calendar instance in UTC
*

View file

@ -20,6 +20,8 @@ open class ListMatPreference @JvmOverloads constructor(
get() = emptyArray()
set(value) { entries = value.map { context.getString(it) } }
private var defValue: String = ""
var tempEntry: String? = null
var tempValue: Int? = null
var entries: List<String> = emptyList()
override fun onSetInitialValue(defaultValue: Any?) {
@ -27,10 +29,19 @@ open class ListMatPreference @JvmOverloads constructor(
defValue = defaultValue as? String ?: defValue
}
private val indexOfPref: Int
get() = tempValue ?: entryValues.indexOf(
if (isPersistent) {
sharedPreferences?.getString(key, defValue)
} else {
tempEntry
} ?: defValue,
)
override var customSummaryProvider: SummaryProvider<MatPreference>? = SummaryProvider<MatPreference> {
val index = entryValues.indexOf(sharedPreferences?.getString(key, defValue))
val index = indexOfPref
if (entries.isEmpty() || index == -1) ""
else entries[index]
else tempEntry ?: entries.getOrNull(index) ?: ""
}
override fun dialog(): MaterialAlertDialogBuilder {
@ -41,10 +52,16 @@ open class ListMatPreference @JvmOverloads constructor(
@SuppressLint("CheckResult")
open fun MaterialAlertDialogBuilder.setListItems() {
val default = entryValues.indexOf(sharedPreferences?.getString(key, defValue) ?: defValue)
val default = indexOfPref
setSingleChoiceItems(entries.toTypedArray(), default) { dialog, pos ->
val value = entryValues[pos]
sharedPreferences?.edit { putString(key, value) }
if (isPersistent) {
sharedPreferences?.edit { putString(key, value) }
} else {
tempValue = pos
tempEntry = entries.getOrNull(pos)
notifyChanged()
}
this@ListMatPreference.summary = this@ListMatPreference.summary
callChangeListener(value)
dialog.dismiss()

View file

@ -739,6 +739,7 @@
<string name="over_wifi_only">Over Wi-Fi only</string>
<string name="over_any_network">Over any network</string>
<string name="dont_auto_update">Don\'t auto-update</string>
<string name="language_requires_app_restart">Some languages may require an app relaunch to display correctly</string>
<string name="app_shortcuts">App shortcuts</string>
<string name="show_recent_sources">Show recently used sources</string>

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="ar"/>
<locale android:name="bg"/>
<locale android:name="bn"/>
<locale android:name="ca"/>
<locale android:name="ceb"/>
<locale android:name="cs"/>
<locale android:name="cv"/>
<locale android:name="de"/>
<locale android:name="el"/>
<locale android:name="en"/>
<locale android:name="eo"/>
<locale android:name="es"/>
<locale android:name="eu"/>
<locale android:name="fa"/>
<locale android:name="fi"/>
<locale android:name="fil"/>
<locale android:name="fr"/>
<locale android:name="gl"/>
<locale android:name="hi"/>
<locale android:name="hr"/>
<locale android:name="hu"/>
<locale android:name="in"/>
<locale android:name="it"/>
<locale android:name="ja"/>
<locale android:name="ka"/>
<locale android:name="km"/>
<locale android:name="ko"/>
<locale android:name="lv"/>
<locale android:name="mn"/>
<locale android:name="ms"/>
<locale android:name="my"/>
<locale android:name="nb-NO"/>
<locale android:name="nl"/>
<locale android:name="nn"/>
<locale android:name="or"/>
<locale android:name="pl"/>
<locale android:name="pt"/>
<locale android:name="pt-BR"/>
<locale android:name="ro"/>
<locale android:name="ru"/>
<locale android:name="sc"/>
<locale android:name="sk"/>
<locale android:name="sr"/>
<locale android:name="sv"/>
<locale android:name="te"/>
<locale android:name="th"/>
<locale android:name="ti"/>
<locale android:name="tl"/>
<locale android:name="tr"/>
<locale android:name="uk"/>
<locale android:name="vi"/>
<locale android:name="zh-CN"/>
<locale android:name="zh-TW"/>
</locale-config>