diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 873b469260..7aed59ddcb 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -155,6 +155,7 @@ android {
dependencies {
implementation(projects.core)
+ implementation(projects.i18n)
implementation(projects.sourceApi)
// Compose
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
index 25187628be..b6f85365cb 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
@@ -17,7 +17,6 @@ import android.net.wifi.WifiManager
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
-import android.util.TypedValue
import android.view.View
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
@@ -103,30 +102,6 @@ fun Context.contextCompatDrawable(@DrawableRes resource: Int): Drawable? {
return ContextCompat.getDrawable(this, resource)
}
-/**
- * Converts to dp.
- */
-val Int.pxToDp: Int
- get() = (this / Resources.getSystem().displayMetrics.density).toInt()
-
-val Float.pxToDp: Float
- get() = (this / Resources.getSystem().displayMetrics.density)
-
-/**
- * Converts to px.
- */
-val Int.dpToPx: Int
- get() = this.toFloat().dpToPx.toInt()
-
-val Int.spToPx: Int
- get() = this.toFloat().spToPx.toInt()
-
-val Float.spToPx: Float
- get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics)
-
-val Float.dpToPx: Float
- get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics)
-
/** Converts to px and takes into account LTR/RTL layout */
fun Float.dpToPxEnd(resources: Resources): Float {
return this * resources.displayMetrics.density * if (resources.isLTR) 1 else -1
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 060454eb11..c05e49c91b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1116,12 +1116,6 @@
No file picker app found
Failed to acquire persistent folder access. The app may behave unexpectedly.
-
- Failed to bypass Cloudflare
- Please update the WebView app for better compatibility
-
- WebView is required for Tachiyomi
-
See your recently updated library entries
Widget not available when app lock is enabled
diff --git a/build.gradle.kts b/build.gradle.kts
index 38914a27f1..7cbeb0930d 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -19,6 +19,7 @@ buildscript {
classpath(kotlinx.serialization.gradle)
classpath(libs.firebase.crashlytics.gradle)
classpath(libs.sqldelight.gradle)
+ classpath(libs.moko.generator)
}
repositories {
gradlePluginPortal()
diff --git a/buildSrc/src/main/kotlin/LocalesConfigPlugin.kt b/buildSrc/src/main/kotlin/LocalesConfigPlugin.kt
new file mode 100644
index 0000000000..c6a13f7c42
--- /dev/null
+++ b/buildSrc/src/main/kotlin/LocalesConfigPlugin.kt
@@ -0,0 +1,41 @@
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.kotlin.dsl.TaskContainerScope
+
+fun TaskContainerScope.registerLocalesConfigTask(project: Project): TaskProvider {
+ return with(project) {
+ register("generateLocalesConfig") {
+ val emptyResourcesElement = "\\s*|".toRegex()
+ val valuesPrefix = "values-?".toRegex()
+
+ val languages = fileTree("$projectDir/src/main/res/")
+ .matching {
+ include("**/strings.xml")
+ }
+ .filterNot {
+ it.readText().contains(emptyResourcesElement)
+ }
+ .map { it.parentFile.name }
+ .sorted()
+ .joinToString(separator = "\n") {
+ val language = it
+ .replace(valuesPrefix, "")
+ .replace("-r", "-")
+ .takeIf(String::isNotBlank) ?: "en"
+ " "
+ }
+
+ val content = """
+
+
+$languages
+
+ """.trimIndent()
+
+ val localeFile = file("$projectDir/src/main/res/xml/locales_config.xml")
+ localeFile.parentFile.mkdirs()
+ localeFile.writeText(content)
+ }
+ }
+}
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 02a4370ef1..cbfc6566c0 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -9,16 +9,18 @@ kotlin {
sourceSets {
val commonMain by getting {
dependencies {
- api(libs.okhttp)
- api(libs.okhttp.logging.interceptor)
- api(libs.okhttp.dnsoverhttps)
- api(libs.okhttp.brotli)
- api(libs.okio)
+ implementation(projects.i18n)
api(libs.bundles.logging)
}
}
val androidMain by getting {
dependencies {
+ api(libs.okhttp)
+ api(libs.okhttp.logging.interceptor)
+ api(libs.okhttp.dnsoverhttps)
+ api(libs.okhttp.brotli)
+ api(libs.okio)
+
api(androidx.core)
api(androidx.annotation)
api(libs.rxjava)
diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt
index 2b7ea26e02..ca3d155395 100644
--- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt
+++ b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt
@@ -5,7 +5,6 @@ import android.content.Context
import android.webkit.WebView
import android.widget.Toast
import androidx.core.content.ContextCompat
-import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.AndroidCookieJar
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
import eu.kanade.tachiyomi.util.system.isOutdated
@@ -15,6 +14,8 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
+import yokai.i18n.MR
+import yokai.util.lang.getMString
import java.io.IOException
import java.util.concurrent.*
@@ -48,7 +49,7 @@ class CloudflareInterceptor(
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
// we don't crash the entire app
catch (e: CloudflareBypassException) {
- throw IOException(context.getString(R.string.failed_to_bypass_cloudflare))
+ throw IOException(context.getMString(MR.strings.failed_to_bypass_cloudflare))
} catch (e: Exception) {
throw IOException(e)
}
@@ -130,7 +131,7 @@ class CloudflareInterceptor(
if (!cloudflareBypassed) {
// Prompt user to update WebView if it seems too outdated
if (isWebViewOutdated) {
- context.toast(R.string.please_update_webview, Toast.LENGTH_LONG)
+ context.toast(MR.strings.please_update_webview, Toast.LENGTH_LONG)
}
throw CloudflareBypassException()
diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt
index 1f366a189c..47018c7443 100644
--- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt
+++ b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt
@@ -5,7 +5,6 @@ import android.os.Build
import android.webkit.WebSettings
import android.webkit.WebView
import android.widget.Toast
-import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.launchUI
@@ -15,6 +14,7 @@ import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
+import yokai.i18n.MR
import java.util.*
import java.util.concurrent.*
@@ -56,7 +56,7 @@ abstract class WebViewInterceptor(
if (!WebViewUtil.supportsWebView(context)) {
launchUI {
- context.toast(R.string.webview_is_required, Toast.LENGTH_LONG)
+ context.toast(MR.strings.webview_is_required, Toast.LENGTH_LONG)
}
return response
}
diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DensityExtensions.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DensityExtensions.kt
new file mode 100644
index 0000000000..2fc2ef21d4
--- /dev/null
+++ b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DensityExtensions.kt
@@ -0,0 +1,28 @@
+package eu.kanade.tachiyomi.util.system
+
+import android.content.res.Resources
+import android.util.TypedValue
+
+/**
+ * Converts to dp.
+ */
+val Int.pxToDp: Int
+ get() = (this / Resources.getSystem().displayMetrics.density).toInt()
+
+val Float.pxToDp: Float
+ get() = (this / Resources.getSystem().displayMetrics.density)
+
+/**
+ * Converts to px.
+ */
+val Int.dpToPx: Int
+ get() = this.toFloat().dpToPx.toInt()
+
+val Int.spToPx: Int
+ get() = this.toFloat().spToPx.toInt()
+
+val Float.spToPx: Float
+ get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics)
+
+val Float.dpToPx: Float
+ get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/DeviceUtil.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DeviceUtil.kt
similarity index 100%
rename from app/src/main/java/eu/kanade/tachiyomi/util/system/DeviceUtil.kt
rename to core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DeviceUtil.kt
diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt
index 6c04883652..1930e6eae7 100644
--- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt
+++ b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt
@@ -3,6 +3,8 @@ package eu.kanade.tachiyomi.util.system
import android.content.Context
import android.widget.Toast
import androidx.annotation.StringRes
+import dev.icerock.moko.resources.StringResource
+import yokai.util.lang.getMString
/**
* Display a toast in this context.
@@ -14,6 +16,16 @@ fun Context.toast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT)
Toast.makeText(this, resource, duration).show()
}
+/**
+ * Display a toast in this context.
+ *
+ * @param resource the text resource.
+ * @param duration the duration of the toast. Defaults to short.
+ */
+fun Context.toast(resource: StringResource, duration: Int = Toast.LENGTH_SHORT) {
+ toast(getMString(resource), duration)
+}
+
/**
* Display a toast in this context.
*
diff --git a/core/src/androidMain/kotlin/yokai/util/lang/MokoExtensions.kt b/core/src/androidMain/kotlin/yokai/util/lang/MokoExtensions.kt
new file mode 100644
index 0000000000..4d431d9f6c
--- /dev/null
+++ b/core/src/androidMain/kotlin/yokai/util/lang/MokoExtensions.kt
@@ -0,0 +1,8 @@
+package yokai.util.lang
+
+import android.content.Context
+import dev.icerock.moko.resources.StringResource
+import dev.icerock.moko.resources.desc.Resource
+import dev.icerock.moko.resources.desc.StringDesc
+
+fun Context.getMString(stringRes: StringResource): String = StringDesc.Resource(stringRes).toString(this)
diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt b/core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt
similarity index 75%
rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt
rename to core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt
index fc122c9538..8a1023b082 100644
--- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt
+++ b/core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt
@@ -1,13 +1,10 @@
package eu.kanade.tachiyomi.network
-import android.os.Build
-import androidx.annotation.RequiresApi
import eu.kanade.tachiyomi.core.preference.PreferenceStore
class NetworkPreferences(private val preferenceStore: PreferenceStore) {
fun dohProvider() = preferenceStore.getInt("doh_provider", -1)
- @RequiresApi(Build.VERSION_CODES.GINGERBREAD)
fun defaultUserAgent() = preferenceStore.getString("default_user_agent", NetworkHelper.DEFAULT_USER_AGENT)
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6ef3370c25..9335bac840 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,6 +3,7 @@ chucker = "3.5.2"
coil3 = "3.0.0-alpha06"
flexible-adapter = "c8013533"
fast_adapter = "5.6.0"
+moko = "0.24.0"
nucleus = "3.0.0"
okhttp = "5.0.0-alpha.14"
shizuku = "12.1.0"
@@ -59,6 +60,10 @@ junit-android = { module = "androidx.test.ext:junit", version = "1.1.5" }
junrar = { module = "com.github.junrar:junrar", version = "7.5.5" }
loading-button = { module = "br.com.simplepass:loading-button-android", version = "2.2.0" }
mockk = { module = "io.mockk:mockk", version = "1.13.11" }
+
+moko-resources = { module = "dev.icerock.moko:resources", version.ref = "moko" }
+moko-generator = { module = "dev.icerock.moko:resources-generator", version.ref = "moko" }
+
okio = { module = "com.squareup.okio:okio", version = "3.9.0" }
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" }
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" }
diff --git a/i18n/.gitignore b/i18n/.gitignore
new file mode 100644
index 0000000000..4d99b14242
--- /dev/null
+++ b/i18n/.gitignore
@@ -0,0 +1,2 @@
+# Generated
+locales_config.xml
diff --git a/i18n/build.gradle.kts b/i18n/build.gradle.kts
new file mode 100644
index 0000000000..69893be26c
--- /dev/null
+++ b/i18n/build.gradle.kts
@@ -0,0 +1,49 @@
+plugins {
+ kotlin("multiplatform")
+ id("com.android.library")
+ id("dev.icerock.mobile.multiplatform-resources")
+}
+
+kotlin {
+ androidTarget()
+
+ applyDefaultHierarchyTemplate()
+
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ api(libs.moko.resources)
+ }
+ }
+ val androidMain by getting {
+ dependsOn(commonMain)
+ }
+ }
+}
+
+android {
+ namespace = "yokai.i18n"
+
+ sourceSets {
+ named("main") {
+ res.srcDir("src/commonMain/moko-resources")
+ }
+ }
+}
+
+multiplatformResources {
+ resourcesPackage.set("yokai.i18n")
+}
+
+tasks {
+ val localesConfigTask = registerLocalesConfigTask(project)
+ preBuild {
+ dependsOn(localesConfigTask)
+ }
+
+ withType {
+ kotlinOptions.freeCompilerArgs += listOf(
+ "-Xexpect-actual-classes",
+ )
+ }
+}
diff --git a/i18n/src/androidMain/AndroidManifest.xml b/i18n/src/androidMain/AndroidManifest.xml
new file mode 100644
index 0000000000..8072ee00db
--- /dev/null
+++ b/i18n/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml
new file mode 100644
index 0000000000..7b09b1a533
--- /dev/null
+++ b/i18n/src/commonMain/moko-resources/base/strings.xml
@@ -0,0 +1,8 @@
+
+
+
+ Failed to bypass Cloudflare
+ Please update the WebView app for better compatibility
+
+ WebView is required for Tachiyomi
+
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 72192d1c8b..0be46720cc 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -33,6 +33,7 @@ rootProject.name = "Yokai"
include(":app")
include(":core")
include(":domain")
+include(":i18n")
include(":presentation-core")
include(":presentation-widget")
include(":source-api")