Skip to content

Commit

Permalink
I18n support with statically generated class (#145)
Browse files Browse the repository at this point in the history
* Add i18n support using java property files.
Not working yet on ios.
Processing of the file content need more work (manual linebreaks not detected yet) and testing.

* Add 2 example usages

* Add type check

* Move resources to common main

* Adjust gradle file to include resources in ios

* Use Properties instead of loading resource as text

* Dont throw if resources not found (for dev)

* Add resources from commonMain to android.
FOr ios its still not working. several attempts are kept in commented out form...

* - fix crash when some mappings are not found/defined & defaulting to english

* cleanups

* Generate class with all resources based on resource files.

Build fails as it seems it does not access the src dir with the generated class correctly.

* Use source dir instead of build dir as on iOS there are issues.
Split files per language.
Add UTF8 support

* - removed dead code

---------

Co-authored-by: Rodrigo Varela <[email protected]>
  • Loading branch information
HenrikJannsen and rodvar authored Jan 7, 2025
1 parent 22d3cc6 commit be6bc51
Show file tree
Hide file tree
Showing 147 changed files with 38,545 additions and 10 deletions.
110 changes: 109 additions & 1 deletion shared/domain/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import java.io.InputStreamReader
import java.util.Properties
import kotlin.io.path.Path

plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinCocoapods)
Expand Down Expand Up @@ -111,7 +115,7 @@ kotlin {
}
androidMain.dependencies {
implementation(libs.androidx.core)

implementation(libs.koin.core)
implementation(libs.koin.android)
}
Expand Down Expand Up @@ -149,4 +153,108 @@ android {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

sourceSets["main"].resources.srcDirs("src/commonMain/resources")
}

tasks.withType<Copy> {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

// Create a task class to ensure proper serialization for configuration cache compatibility
abstract class GenerateResourceBundlesTask : DefaultTask() {
@get:InputDirectory
abstract val inputDir: DirectoryProperty

@get:OutputDirectory
abstract val outputDir: DirectoryProperty

@TaskAction
fun generate() {
val resourceDir = inputDir.asFile.get()

val bundleNames: List<String> = listOf(
"default",
"application",
"bisq_easy",
"reputation",
"chat",
"support",
"user",
"network",
"settings",
"payment_method",
"offer",
"mobile" // custom for mobile client
)

val languageCodes = listOf("en", "af_ZA", "cs", "de", "es", "it", "pcm", "pt_BR", "ru")

val bundlesByCode: Map<String, List<ResourceBundle>> = languageCodes.associateWith { languageCode ->
bundleNames.mapNotNull { bundleName ->
val code = if (languageCode.lowercase() == "en") "" else "_$languageCode"
val fileName = "$bundleName$code.properties"
var file = Path(resourceDir.path, fileName).toFile()

if (!file.exists()) {
// Fall back to English default properties if no translation file
file = Path(resourceDir.path, "$bundleName.properties").toFile()
if (!file.exists()) {
logger.warn("File not found: ${file.absolutePath}")
return@mapNotNull null // Skip missing files
}
}

val properties = Properties()

// Use InputStreamReader to ensure UTF-8 encoding
file.inputStream().use { inputStream ->
InputStreamReader(inputStream, Charsets.UTF_8).use { reader ->
properties.load(reader)
}
}

val map = properties.entries.associate { it.key.toString() to it.value.toString() }
ResourceBundle(map, bundleName, languageCode)
}
}

bundlesByCode.forEach { (languageCode, bundles) ->
val outputFile: File = outputDir.get().file("GeneratedResourceBundles_$languageCode.kt").asFile
val generatedCode = StringBuilder().apply {
appendLine("package network.bisq.mobile.i18n")
appendLine()
appendLine("// Auto-generated file. Do not modify manually.")
appendLine("object GeneratedResourceBundles_$languageCode {")
appendLine(" val bundles = mapOf(")
bundles.forEach { bundle ->
appendLine(" \"${bundle.bundleName}\" to mapOf(")
bundle.map.forEach { (key, value) ->
val escapedValue = value
.replace("\\", "\\\\") // Escape backslashes
.replace("\"", "\\\"") // Escape double quotes
.replace("\n", "\\n") // Escape newlines
appendLine(" \"$key\" to \"$escapedValue\",")
}
appendLine(" ),")
}
appendLine(" )")
appendLine("}")
}

outputFile.parentFile.mkdirs()
outputFile.writeText(generatedCode.toString(), Charsets.UTF_8)
}
}

data class ResourceBundle(val map: Map<String, String>, val bundleName: String, val languageCode: String)
}

tasks.register<GenerateResourceBundlesTask>("generateResourceBundles") {
group = "build"
description = "Generate a Kotlin file with hardcoded ResourceBundle data"
inputDir.set(layout.projectDirectory.dir("src/commonMain/resources/mobile"))
// Using build dir still not working on iOS
// Thus we use the source dir as target
outputDir.set(layout.projectDirectory.dir("src/commonMain/kotlin/network/bisq/mobile/i18n"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import kotlinx.serialization.Serializable
import java.io.ByteArrayOutputStream
import java.util.Locale
import java.text.DecimalFormat
import java.io.IOException
import java.io.InputStream
import java.util.Scanner
import java.util.Properties

actual fun getPlatformSettings(): Settings {
return Settings()
Expand All @@ -39,6 +43,17 @@ class AndroidPlatformInfo : PlatformInfo {

actual fun getPlatformInfo(): PlatformInfo = AndroidPlatformInfo()


actual fun loadProperties(fileName: String): Map<String, String> {
val properties = Properties()
val classLoader = Thread.currentThread().contextClassLoader
val resource = classLoader?.getResourceAsStream(fileName)
?: throw IllegalArgumentException("Resource not found: $fileName")
properties.load(resource)

return properties.entries.associate { it.key.toString() to it.value.toString() }
}

@Serializable(with = PlatformImageSerializer::class)
actual class PlatformImage(val bitmap: ImageBitmap) {
actual companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import network.bisq.mobile.domain.data.BackgroundDispatcher
import network.bisq.mobile.domain.data.repository.SettingsRepository
import network.bisq.mobile.domain.service.TrustedNodeService
import network.bisq.mobile.domain.service.bootstrap.ApplicationBootstrapFacade
import network.bisq.mobile.i18n.i18n

class ClientApplicationBootstrapFacade(
private val settingsRepository: SettingsRepository,
Expand Down Expand Up @@ -36,15 +37,15 @@ class ClientApplicationBootstrapFacade(
if (!trustedNodeService.isConnected()) {
try {
trustedNodeService.connect()
setState("Connected to Trusted Node")
setState("bootstrap.connectedToTrustedNode".i18n())
setProgress(1.0f)
} catch (e: Exception) {
log.e(e) { "Failed to connect to trusted node" }
setState("No connectivity")
setProgress(1.0f)
}
} else {
setState("Connected to Trusted Node")
setState("bootstrap.connectedToTrustedNode".i18n())
setProgress(1.0f)
}
// }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ expect fun getDeviceLanguageCode(): String

expect fun getPlatformInfo(): PlatformInfo

expect fun loadProperties(fileName: String): Map<String, String>

@Serializable(with = PlatformImageSerializer::class)
expect class PlatformImage {
companion object {
Expand Down
Loading

0 comments on commit be6bc51

Please sign in to comment.