diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e11aaf7..c8dc305 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,6 +50,10 @@ android { compose = true } + androidResources { + generateLocaleConfig = true + } + sourceSets { getByName("main") { java.srcDir("../gm-java/src/main/java") @@ -61,16 +65,13 @@ dependencies { implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material3.adaptive.navigation.suite) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.graphics) - implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.biometric) implementation(libs.play.services.code.scanner) implementation(libs.androidx.lifecycle.process) - implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.kotlinx.serialization.json) testImplementation(libs.junit) androidTestImplementation(libs.androidx.compose.ui.test.junit4) diff --git a/app/src/main/java/swiss/qpq/gajumobile/MainActivity.kt b/app/src/main/java/swiss/qpq/gajumobile/MainActivity.kt index 257c4b8..e340189 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/MainActivity.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/MainActivity.kt @@ -7,7 +7,6 @@ import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -28,7 +27,10 @@ import kotlinx.coroutines.withContext import swiss.qpq.gajumobile.data.AppStateManager import swiss.qpq.gajumobile.data.WalletRepository import swiss.qpq.gajumobile.data.models.Account +import swiss.qpq.gajumobile.data.models.GridsSignRequest +import swiss.qpq.gajumobile.data.models.GridsSignType import swiss.qpq.gajumobile.data.models.Wallet +import swiss.qpq.gajumobile.data.models.toGridsSignRequest import swiss.qpq.gajumobile.security.AppLifecycleObserver import swiss.qpq.gajumobile.security.BiometricHelper import swiss.qpq.gajumobile.security.SecurityManager @@ -97,9 +99,9 @@ class MainActivity : FragmentActivity() { } } else { val routerError: (String) -> Unit = { message -> - Toast.makeText(context, message, Toast.LENGTH_LONG).show() + Toast.makeText(this@MainActivity, message, Toast.LENGTH_LONG).show() } - GajuRouter(walletRepo, appStateManager, routerError) + GajuRouter(this@MainActivity, walletRepo, appStateManager, routerError) } } } @@ -130,11 +132,11 @@ enum class Screen { @Composable fun GajuRouter( + context: android.content.Context, walletRepo: WalletRepository, appStateManager: AppStateManager, onError: (String) -> Unit, ) { - val context = LocalContext.current val appState by appStateManager.state.collectAsState() val scope = rememberCoroutineScope() @@ -192,7 +194,7 @@ fun GajuRouter( var pendingGasPrice by rememberSaveable { mutableStateOf("") } var pendingTTL by rememberSaveable { mutableStateOf("") } - var pendingSignRequest by remember { mutableStateOf?>(null) } + var pendingSignRequest by remember { mutableStateOf(null) } var pendingSignUrl by rememberSaveable { mutableStateOf("") } var activeFetchJob by remember { mutableStateOf(null) } @@ -222,7 +224,7 @@ fun GajuRouter( navigateTo(Screen.ACCOUNT_NAMING) } catch (e: Throwable) { val msg = e.message ?: e.toString() - onError("Failed to generate account: $msg") + onError(context.getString(R.string.err_generate_account, msg)) android.util.Log.e("GajuRouter", "Account generation failed", e) } }, @@ -244,7 +246,7 @@ fun GajuRouter( pendingAccount = account navigateTo(Screen.ACCOUNT_NAMING) } catch (e: Exception) { - onError("Failed to recover account: ${e.message}") + onError(context.getString(R.string.err_recover_account, e.message ?: "")) } }, onBack = { navigateBack() }, @@ -330,8 +332,10 @@ fun GajuRouter( walletRepo.deleteAccount(selectedWalletId, selectedAccountId) } appStateManager.notifyWalletsChanged() - screenStack.removeAt(screenStack.size - 1) - screenStack.removeAt(screenStack.size - 1) + // Pop back to Dashboard by removing the confirmation and the management screen + repeat(2) { + if (screenStack.size > 1) screenStack.removeAt(screenStack.size - 1) + } }, onCancel = { navigateBack() } ) @@ -367,7 +371,7 @@ fun GajuRouter( if (updated != null) { appStateManager.notifyWalletsChanged() } else { - onError("Refresh failed. Check network endpoints.") + onError(context.getString(R.string.err_refresh_failed)) } } }, @@ -441,15 +445,15 @@ fun GajuRouter( val targetEndpoints = targetWallet.endpoints + endpoint walletRepo.saveWallet(targetWallet.copy(endpoints = targetEndpoints)) appStateManager.notifyWalletsChanged() - onError("Network mismatch: Node reports '$reportedNetworkId'. Added to wallet '${targetWallet.name}'.") + onError(context.getString(R.string.err_network_mismatch_added, reportedNetworkId, targetWallet.name)) } else { - nodeStatusError = "CRITICAL MISMATCH: Node reports '$reportedNetworkId' but wallet is '${wallet.networkId}'. Please create a wallet for '$reportedNetworkId' first." + nodeStatusError = context.getString(R.string.err_critical_mismatch_create, reportedNetworkId, wallet.networkId) } } else { val newList = wallet.endpoints + endpoint walletRepo.saveWallet(wallet.copy(endpoints = newList)) appStateManager.notifyWalletsChanged() - onError("Endpoint verified and added.") + onError(context.getString(R.string.msg_endpoint_verified)) } } catch (e: Exception) { // Even if status check fails, we might still want to add it? @@ -483,10 +487,10 @@ fun GajuRouter( walletRepo.saveWallet(targetWallet.copy(endpoints = targetEndpoints)) appStateManager.notifyWalletsChanged() - onError("Network mismatch: Node reports '$reportedNetworkId'. Moved to wallet '${targetWallet.name}'.") + onError(context.getString(R.string.err_network_mismatch_moved, reportedNetworkId, targetWallet.name)) } else { // Warn for discard - nodeStatusError = "CRITICAL MISMATCH: Node reports '$reportedNetworkId' but wallet is configured for '${wallet.networkId}'. Fix this endpoint or it will be discarded from this wallet." + nodeStatusError = context.getString(R.string.err_critical_mismatch_fix, reportedNetworkId, wallet.networkId) } } else { nodeStatusResult = status @@ -526,7 +530,7 @@ fun GajuRouter( viewMnemonicPhrase = phrase navigateTo(Screen.VIEW_MNEMONIC) } catch (e: Exception) { - onError("Authentication failed: ${e.message}") + onError(context.getString(R.string.err_auth_failed, e.message ?: "")) } }, onVerifyMnemonic = { @@ -537,7 +541,7 @@ fun GajuRouter( verifyMnemonicPhrase = phrase navigateTo(Screen.VERIFY_MNEMONIC) } catch (e: Exception) { - onError("Authentication failed: ${e.message}") + onError(context.getString(R.string.err_auth_failed, e.message ?: "")) } }, onRename = { newName -> @@ -605,21 +609,22 @@ fun GajuRouter( activeFetchJob = scope.launch { try { android.util.Log.i("GajuRouter", "Fetching GRIDS request from: ${result.url}") - val request = withContext(Dispatchers.IO) { + @Suppress("UNCHECKED_CAST") + val response = withContext(Dispatchers.IO) { Http.get(result.url) as? Map } - if (request != null) { - pendingSignRequest = request + if (response != null) { + pendingSignRequest = response.toGridsSignRequest() // Remove fetching screen and add request screen screenStack.removeAt(screenStack.size - 1) navigateTo(Screen.SIGN_REQUEST) } else { - onError("Failed to fetch signature request.") + onError(context.getString(R.string.err_fetch_sig_failed)) navigateBack() } } catch (e: Exception) { if (activeFetchJob?.isCancelled == false) { - onError("Fetch failed: ${e.message}") + onError(context.getString(R.string.err_fetch_failed, e.message ?: "")) navigateBack() } } finally { @@ -631,7 +636,7 @@ fun GajuRouter( navigateTo(Screen.SEND_FORM) } } catch (e: Exception) { - onError("Invalid GRIDS URL: ${e.message}") + onError(context.getString(R.string.err_invalid_grids_url, e.message ?: "")) } }, onGenerate = { navigateTo(Screen.QR_RECEIVE) }, @@ -648,7 +653,7 @@ fun GajuRouter( val activeAccount = activeWallet?.accounts?.getOrNull(activeAccountIndex) if (activeAccount == null) { - onError("No active account selected.") + onError(context.getString(R.string.err_no_active_account)) navigateBack() return } @@ -687,7 +692,7 @@ fun GajuRouter( val activeAccount = activeWallet?.accounts?.getOrNull(activeAccountIndex) if (activeAccount == null) { - onError("No active account selected.") + onError(context.getString(R.string.err_no_active_account)) navigateBack() return } @@ -735,14 +740,14 @@ fun GajuRouter( } val hash = result["hash"] as? String ?: "Unknown" - onError("Transaction posted! Hash: $hash") + onError(context.getString(R.string.msg_tx_posted, hash)) // Success! Go back to dashboard screenStack.clear() screenStack.add(Screen.DASHBOARD) } catch (e: Throwable) { val msg = e.message ?: e.toString() - onError("Send failed: $msg") + onError(context.getString(R.string.err_send_failed, msg)) android.util.Log.e("GajuRouter", "Send failed", e) } } @@ -756,29 +761,25 @@ fun GajuRouter( val activeAccount = activeWallet?.accounts?.getOrNull(activeAccountIndex) val request = pendingSignRequest ?: return - val type = request["type"] as? String ?: "" - val payload = request["payload"] as? String ?: "" - val publicId = request["public_id"] as? String // If request specifies an ID, try to find that account - val signAccount = if (publicId != null && publicId != "false") { - wallets.flatMap { it.accounts }.find { it.gajuId == publicId } + val signAccount = if (request.publicId != null && request.publicId != "false") { + wallets.flatMap { it.accounts }.find { it.gajuId == request.publicId } } else { activeAccount } if (signAccount == null) { - onError("No matching account found for signature.") + onError(context.getString(R.string.err_no_matching_account)) navigateBack() return } SignRequestScreen( - type = type, + request = request, originUrl = pendingSignUrl, accountLabel = signAccount.label, accountId = signAccount.gajuId, - payload = payload, onSign = { scope.launch { try { @@ -786,30 +787,33 @@ fun GajuRouter( val signedRequest = mutableMapOf() // Essential keys present in all GRIDS responses - signedRequest["grids"] = request["grids"] - signedRequest["chain"] = request["chain"] - signedRequest["network_id"] = request["network_id"] - signedRequest["type"] = type + signedRequest["grids"] = request.grids + signedRequest["chain"] = request.chain + signedRequest["network_id"] = request.networkId + signedRequest["type"] = request.type.toProtocolString() signedRequest["public_id"] = signAccount.gajuId // Must be the actual ID string, never 'false' - when (type) { - "message" -> { - val sig = AccountAirlock.signMessage(signAccount.privateKeyEnvelope, masterKey, payload) - signedRequest["payload"] = payload + when (request.type) { + GridsSignType.MESSAGE -> { + val sig = AccountAirlock.signMessage(signAccount.privateKeyEnvelope, masterKey, request.payload) + signedRequest["payload"] = request.payload signedRequest["signature"] = android.util.Base64.encodeToString(sig, android.util.Base64.NO_WRAP) } - "binary" -> { - val binary = android.util.Base64.decode(payload, android.util.Base64.DEFAULT) + GridsSignType.BINARY -> { + val binary = android.util.Base64.decode(request.payload, android.util.Base64.DEFAULT) val sig = AccountAirlock.signBinary(signAccount.privateKeyEnvelope, masterKey, binary) - signedRequest["payload"] = payload + signedRequest["payload"] = request.payload signedRequest["signature"] = android.util.Base64.encodeToString(sig, android.util.Base64.NO_WRAP) } - "tx" -> { - val nid = request["network_id"] as? String ?: activeWallet?.networkId ?: "groot.mainnet" - val txBinary = android.util.Base64.decode(payload, android.util.Base64.DEFAULT) + GridsSignType.TX -> { + val nid = request.networkId ?: activeWallet?.networkId ?: "groot.mainnet" + val txBinary = android.util.Base64.decode(request.payload, android.util.Base64.DEFAULT) signedRequest["payload"] = AccountAirlock.signTx(signAccount.privateKeyEnvelope, masterKey, txBinary, nid) signedRequest["signed"] = true } + else -> { + throw Exception("Cannot sign unknown request type") + } } android.util.Log.i("GajuRouter", "POSTing GRIDS response to $pendingSignUrl") @@ -817,12 +821,12 @@ fun GajuRouter( Http.post(pendingSignUrl, signedRequest) } - onError("Signature submitted successfully.") + onError(context.getString(R.string.msg_sig_submitted)) screenStack.clear() screenStack.add(Screen.DASHBOARD) } catch (e: Throwable) { val msg = e.message ?: e.toString() - onError("Signing failed: $msg") + onError(context.getString(R.string.err_sig_failed, msg)) android.util.Log.e("GajuRouter", "Signing failed", e) } } @@ -836,7 +840,7 @@ fun GajuRouter( val activeAccount = activeWallet?.accounts?.getOrNull(activeAccountIndex) if (activeAccount == null) { - onError("No active account selected.") + onError(context.getString(R.string.err_no_active_account)) navigateBack() return } diff --git a/app/src/main/java/swiss/qpq/gajumobile/data/models/GridsModels.kt b/app/src/main/java/swiss/qpq/gajumobile/data/models/GridsModels.kt new file mode 100644 index 0000000..302baa8 --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/data/models/GridsModels.kt @@ -0,0 +1,51 @@ +package swiss.qpq.gajumobile.data.models + +import kotlinx.serialization.Serializable + +@Serializable +enum class GridsSignType { + MESSAGE, + BINARY, + TX, + UNKNOWN; + + companion object { + fun fromString(type: String?): GridsSignType = when (type) { + "message" -> MESSAGE + "binary" -> BINARY + "tx" -> TX + else -> UNKNOWN + } + } + + fun toProtocolString(): String = when (this) { + MESSAGE -> "message" + BINARY -> "binary" + TX -> "tx" + UNKNOWN -> "unknown" + } +} + +@Serializable +data class GridsSignRequest( + val type: GridsSignType, + val payload: String, + val publicId: String?, + val networkId: String?, + val grids: String?, + val chain: String? +) + +/** + * Safely maps a raw Map (typically from ZJ.decode) to a typed GridsSignRequest. + */ +fun Map.toGridsSignRequest(): GridsSignRequest { + return GridsSignRequest( + type = GridsSignType.fromString(this["type"] as? String), + payload = this["payload"] as? String ?: "", + publicId = this["public_id"] as? String, + networkId = this["network_id"] as? String, + grids = this["grids"] as? String, + chain = this["chain"] as? String + ) +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/security/AccountAirlock.java b/app/src/main/java/swiss/qpq/gajumobile/security/AccountAirlock.java index 0f6fb5d..6eae6d7 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/security/AccountAirlock.java +++ b/app/src/main/java/swiss/qpq/gajumobile/security/AccountAirlock.java @@ -4,7 +4,6 @@ import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.UUID; import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; import swiss.qpq.gajumaru.core.crypto.Vault; import swiss.qpq.gajumaru.core.encoding.Mnemonic; import swiss.qpq.gajumaru.core.tools.CryptoUtils; @@ -52,7 +51,6 @@ public final class AccountAirlock { ) throws Exception { byte[] seed = null; - byte[] dekBytes = null; try { // 1. Decode mnemonic to seed @@ -72,30 +70,15 @@ public final class AccountAirlock { GajuNative.memzero(keypair); // 3. Generate random Data Encryption Key (DEK) - SecureRandom random = new SecureRandom(); - dekBytes = new byte[32]; - random.nextBytes(dekBytes); - SecretKey dek = new SecretKeySpec(dekBytes, "AES"); - // 4. Encrypt Private Key (seed) with DEK - Vault.Ciphertext dataCiphertext = Vault.encrypt(dek, seed); - // 5. Encrypt DEK with Master Key - Vault.Ciphertext dekCiphertext = Vault.encrypt(masterKey, dekBytes); - - EncryptedEnvelope envelope = new EncryptedEnvelope( - dataCiphertext.getData(), - dekCiphertext.getData(), - dataCiphertext.getIv(), - dekCiphertext.getIv() - ); + EncryptedEnvelope envelope = EncryptionService.encrypt(masterKey, seed); String id = UUID.randomUUID().toString(); return new Account(id, label, publicKey, envelope, null); } finally { if (seed != null) GajuNative.memzero(seed); - if (dekBytes != null) CryptoUtils.wipe(dekBytes); } } @@ -183,23 +166,7 @@ public final class AccountAirlock { EncryptedEnvelope envelope, SecretKey masterKey ) throws Exception { - byte[] plaintextDek = null; - try { - plaintextDek = Vault.decrypt( - masterKey, - envelope.getDekIv(), - envelope.getEncryptedDek() - ); - SecretKey dek = new SecretKeySpec(plaintextDek, "AES"); - - return Vault.decrypt( - dek, - envelope.getDataIv(), - envelope.getEncryptedData() - ); - } finally { - if (plaintextDek != null) CryptoUtils.wipe(plaintextDek); - } + return EncryptionService.decrypt(masterKey, envelope); } /** diff --git a/app/src/main/java/swiss/qpq/gajumobile/security/EncryptionService.kt b/app/src/main/java/swiss/qpq/gajumobile/security/EncryptionService.kt index f024662..ea7ea3c 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/security/EncryptionService.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/security/EncryptionService.kt @@ -8,6 +8,7 @@ import javax.crypto.KeyGenerator import javax.crypto.SecretKey import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.SecretKeySpec +import swiss.qpq.gajumaru.core.tools.CryptoUtils @Serializable data class EncryptedEnvelope( @@ -42,47 +43,87 @@ object EncryptionService { private const val GCM_TAG_LENGTH = 128 private const val IV_LENGTH = 12 + @JvmStatic fun encrypt(context: Context, data: ByteArray): EncryptedEnvelope { + val masterKey = KeyManager.getMasterKey(context) + return encrypt(masterKey, data) + } + + @JvmStatic + fun encrypt(masterKey: SecretKey, data: ByteArray): EncryptedEnvelope { // 1. Generate a random Data Encryption Key (DEK) in software val dek = generateDek() + val dekBytes = dek.encoded - // 2. Encrypt the data with the DEK - val dataCipher = Cipher.getInstance(AES_GCM) - val dataIv = ByteArray(IV_LENGTH).apply { SecureRandom().nextBytes(this) } - dataCipher.init(Cipher.ENCRYPT_MODE, dek, GCMParameterSpec(GCM_TAG_LENGTH, dataIv)) - val encryptedData = dataCipher.doFinal(data) + try { + // 2. Encrypt the data with the DEK + val (encryptedData, dataIv) = encryptRaw(dek, data) - // 3. Encrypt the DEK with the Master Key (Hardware-backed) - val masterKey = KeyManager.getMasterKey(context) - val dekCipher = Cipher.getInstance(AES_GCM) - val dekIv = ByteArray(IV_LENGTH).apply { SecureRandom().nextBytes(this) } - // Now using manual IV for hardware key to ensure tag length consistency - dekCipher.init(Cipher.ENCRYPT_MODE, masterKey, GCMParameterSpec(GCM_TAG_LENGTH, dekIv)) - val encryptedDek = dekCipher.doFinal(dek.encoded) + // 3. Encrypt the DEK with the Master Key (Hardware-backed) + val (encryptedDek, dekIv) = encryptRaw(masterKey, dekBytes) - return EncryptedEnvelope( - encryptedData = encryptedData, - encryptedDek = encryptedDek, - dataIv = dataIv, - dekIv = dekIv - ) + return EncryptedEnvelope( + encryptedData = encryptedData, + encryptedDek = encryptedDek, + dataIv = dataIv, + dekIv = dekIv + ) + } finally { + CryptoUtils.wipe(dekBytes) + } } + @JvmStatic fun decrypt(context: Context, envelope: EncryptedEnvelope): ByteArray { - // 1. Decrypt the DEK using the Master Key val masterKey = KeyManager.getMasterKey(context) - val dekCipher = Cipher.getInstance(AES_GCM) - dekCipher.init(Cipher.DECRYPT_MODE, masterKey, GCMParameterSpec(GCM_TAG_LENGTH, envelope.dekIv)) - val decryptedDekBytes = dekCipher.doFinal(envelope.encryptedDek) - val dek = SecretKeySpec(decryptedDekBytes, "AES") - - // 2. Decrypt the data using the DEK - val dataCipher = Cipher.getInstance(AES_GCM) - dataCipher.init(Cipher.DECRYPT_MODE, dek, GCMParameterSpec(GCM_TAG_LENGTH, envelope.dataIv)) - return dataCipher.doFinal(envelope.encryptedData) + return decrypt(masterKey, envelope) } - private fun generateDek(): SecretKey { + @JvmStatic + fun decrypt(masterKey: SecretKey, envelope: EncryptedEnvelope): ByteArray { + var decryptedDekBytes: ByteArray? = null + try { + // 1. Decrypt the DEK using the Master Key + decryptedDekBytes = decryptRaw(masterKey, envelope.dekIv, envelope.encryptedDek) + val dek = SecretKeySpec(decryptedDekBytes, "AES") + + // 2. Decrypt the data using the DEK + return decryptRaw(dek, envelope.dataIv, envelope.encryptedData) + } catch (e: Exception) { + val msg = e.message ?: e.toString() + throw Exception("EncryptionService.decrypt failed: $msg", e) + } finally { + if (decryptedDekBytes != null) CryptoUtils.wipe(decryptedDekBytes) + } + } + + @JvmStatic + fun encryptRaw(key: SecretKey, data: ByteArray): Pair { + try { + val cipher = Cipher.getInstance(AES_GCM) + val iv = ByteArray(IV_LENGTH).apply { SecureRandom().nextBytes(this) } + cipher.init(Cipher.ENCRYPT_MODE, key, GCMParameterSpec(GCM_TAG_LENGTH, iv)) + val ciphertext = cipher.doFinal(data) + return ciphertext to iv + } catch (e: Exception) { + throw Exception("encryptRaw failed: ${e.message}", e) + } + } + + @JvmStatic + fun decryptRaw(key: SecretKey, iv: ByteArray, encryptedData: ByteArray): ByteArray { + try { + val cipher = Cipher.getInstance(AES_GCM) + cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(GCM_TAG_LENGTH, iv)) + return cipher.doFinal(encryptedData) + } catch (e: Exception) { + val msg = e.message ?: e.toString() + throw Exception("decryptRaw failed [${e.javaClass.simpleName}]: $msg", e) + } + } + + @JvmStatic + fun generateDek(): SecretKey { val keyGen = KeyGenerator.getInstance("AES") keyGen.init(256) return keyGen.generateKey() diff --git a/app/src/main/java/swiss/qpq/gajumobile/security/GajuNative.kt b/app/src/main/java/swiss/qpq/gajumobile/security/GajuNative.kt index 6c7ea39..0cfcdba 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/security/GajuNative.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/security/GajuNative.kt @@ -7,10 +7,8 @@ object GajuNative { init { try { System.loadLibrary("gaju-native") - android.util.Log.i("GajuNative", "Native library loaded successfully.") } catch (e: Throwable) { loadError = e.message ?: e.toString() - android.util.Log.e("GajuNative", "Failed to load library: $loadError", e) } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/components/GajuComponents.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/components/GajuComponents.kt index c614957..4474644 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/components/GajuComponents.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/components/GajuComponents.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color 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.TextAlign import androidx.compose.ui.unit.dp @@ -130,7 +131,7 @@ fun GajuTopBar( onDismissRequest = { menuExpanded = false } ) { DropdownMenuItem( - text = { Text("Manage Wallets") }, + text = { Text(stringResource(R.string.menu_manage_wallets)) }, onClick = { menuExpanded = false onNavigate("manage_wallets") @@ -138,7 +139,7 @@ fun GajuTopBar( leadingIcon = { Icon(painterResource(R.drawable.ic_account_box), null, modifier = Modifier.size(18.dp)) } ) DropdownMenuItem( - text = { Text("Transactions") }, + text = { Text(stringResource(R.string.menu_transactions)) }, onClick = { menuExpanded = false onNavigate("transactions") @@ -146,7 +147,7 @@ fun GajuTopBar( leadingIcon = { Icon(painterResource(R.drawable.gajumobile_icon), null, modifier = Modifier.size(18.dp), tint = Color.Unspecified) } ) DropdownMenuItem( - text = { Text("Settings") }, + text = { Text(stringResource(R.string.menu_settings)) }, onClick = { menuExpanded = false onNavigate("settings") @@ -154,7 +155,7 @@ fun GajuTopBar( leadingIcon = { Icon(painterResource(R.drawable.settings), null, modifier = Modifier.size(18.dp)) } ) DropdownMenuItem( - text = { Text("Environment Info") }, + text = { Text(stringResource(R.string.menu_environment_info)) }, onClick = { menuExpanded = false onNavigate("environment_info") @@ -243,7 +244,7 @@ fun MnemonicReveal( tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), ) Text( - text = "TAP TO REVEAL", + text = stringResource(R.string.tap_to_reveal), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), fontWeight = FontWeight.Bold, ) @@ -294,7 +295,7 @@ fun SuccessLayout( title: String, content: @Composable ColumnScope.() -> Unit, onProceed: () -> Unit, - proceedText: String = "Proceed to Dashboard", + proceedText: String = stringResource(R.string.proceed_to_dashboard), ) { var checked by remember { mutableStateOf(value = false) } @@ -322,7 +323,7 @@ fun SuccessLayout( ), ) Text( - text = "I understand and fully assume the risks.", + text = stringResource(R.string.understand_risks_success_label), color = MaterialTheme.colorScheme.onBackground, fontSize = 14.sp, modifier = Modifier.padding(start = 8.dp), diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/AccountNamingScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/AccountNamingScreen.kt index 641bf31..9145d19 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/AccountNamingScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/AccountNamingScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -61,14 +62,14 @@ fun AccountNamingScreen( ) { GajuHeader() Text( - "Name your account", + stringResource(R.string.name_account_title), style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.onBackground ) Spacer(modifier = Modifier.height(24.dp)) Text( - "Account ID:", + stringResource(R.string.account_id_label), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurface ) @@ -101,7 +102,7 @@ fun AccountNamingScreen( ) { Icon( painter = painterResource(id = R.drawable.ic_content_copy), - contentDescription = "Copy ID", + contentDescription = stringResource(R.string.copy_id_content_desc), tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp) ) @@ -113,8 +114,8 @@ fun AccountNamingScreen( GajuTextField( value = name, onValueChange = { name = it }, - label = "Account Name", - placeholder = "Optional (defaults to ID)" + label = stringResource(R.string.account_name_label), + placeholder = stringResource(R.string.account_name_placeholder) ) Spacer(modifier = Modifier.weight(1f)) @@ -122,7 +123,7 @@ fun AccountNamingScreen( Row(modifier = Modifier.fillMaxWidth()) { if (onBack != null) { GajuButton( - text = "Back", + text = stringResource(R.string.back_button), onClick = onBack, modifier = Modifier.weight(1f), containerColor = MaterialTheme.colorScheme.secondary, @@ -131,7 +132,7 @@ fun AccountNamingScreen( Spacer(modifier = Modifier.width(16.dp)) } GajuButton( - text = "Finish", + text = stringResource(R.string.finish_button), onClick = { onAccountNamed(name.ifBlank { publicKey }) }, modifier = Modifier.weight(1f) ) diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/CreateWalletScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/CreateWalletScreen.kt index b6670bf..6fd3ca8 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/CreateWalletScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/CreateWalletScreen.kt @@ -22,8 +22,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import swiss.qpq.gajumobile.R import swiss.qpq.gajumobile.ui.components.GajuButton import swiss.qpq.gajumobile.ui.components.GajuHeader import swiss.qpq.gajumobile.ui.components.GajuTextField @@ -47,13 +49,13 @@ fun CreateWalletScreen( GajuHeader() Spacer(modifier = Modifier.height(32.dp)) Text( - "Create Wallet", + stringResource(R.string.create_wallet_title), style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.onBackground ) Spacer(modifier = Modifier.height(16.dp)) Text( - "A wallet is a collection of accounts, like a folder for your identities.", + stringResource(R.string.create_wallet_description), style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant @@ -63,15 +65,15 @@ fun CreateWalletScreen( GajuTextField( value = walletName, onValueChange = { walletName = it }, - label = "Wallet Name", - placeholder = "e.g. Personal, Savings", + label = stringResource(R.string.wallet_name_label), + placeholder = stringResource(R.string.wallet_name_placeholder), singleLine = true ) Spacer(modifier = Modifier.height(24.dp)) Text( - "NETWORK", + stringResource(R.string.network_label), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.align(Alignment.Start) @@ -81,7 +83,10 @@ fun CreateWalletScreen( modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { - listOf("groot.mainnet" to "Mainnet", "groot.testnet" to "Testnet").forEach { (id, label) -> + listOf( + "groot.mainnet" to stringResource(R.string.network_mainnet), + "groot.testnet" to stringResource(R.string.network_testnet) + ).forEach { (id, label) -> val isSelected = selectedNetwork == id AssistChip( onClick = { selectedNetwork = id }, @@ -99,7 +104,7 @@ fun CreateWalletScreen( Row(modifier = Modifier.fillMaxWidth()) { if (onBack != null) { GajuButton( - text = "Back", + text = stringResource(R.string.back_button), onClick = onBack, modifier = Modifier.weight(1f), containerColor = MaterialTheme.colorScheme.secondary, @@ -108,7 +113,7 @@ fun CreateWalletScreen( Spacer(modifier = Modifier.width(16.dp)) } GajuButton( - text = "Continue", + text = stringResource(R.string.continue_button), enabled = walletName.isNotBlank(), onClick = { onWalletCreated(walletName, selectedNetwork) }, modifier = Modifier.weight(1f) diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DashboardScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DashboardScreen.kt index 69d040d..7b361d8 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DashboardScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DashboardScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard 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.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -67,17 +68,17 @@ fun DashboardScreen( if (wallets.isEmpty()) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("No wallets found. Please create one.") + Text(stringResource(R.string.no_wallets_message)) Spacer(modifier = Modifier.height(16.dp)) ActionCircle( - text = "Create", + text = stringResource(R.string.create_button), containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary, onClick = { onNavigate("CREATE_WALLET") } ) Spacer(modifier = Modifier.height(32.dp)) Text( - text = "If you created a wallet but don't see it, it may be corrupted.", + text = stringResource(R.string.wallets_corrupted_message), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.error, textAlign = TextAlign.Center, @@ -86,7 +87,7 @@ fun DashboardScreen( androidx.compose.material3.TextButton( onClick = { onNavigate("WIPE_DATA") } ) { - Text("Wipe Broken Wallets", color = MaterialTheme.colorScheme.error) + Text(stringResource(R.string.wipe_broken_wallets_button), color = MaterialTheme.colorScheme.error) } } } @@ -95,6 +96,7 @@ fun DashboardScreen( val clipboard = LocalClipboard.current val scope = rememberCoroutineScope() + val accountIdLabel = stringResource(R.string.account_id_label_clipboard) val walletPagerState = rememberPagerState(initialPage = selectedWalletIndex.coerceIn(0, wallets.size - 1)) { wallets.size } // Sync pager state back to router @@ -203,7 +205,7 @@ fun DashboardScreen( text = { Icon( painter = painterResource(id = R.drawable.ic_add), - contentDescription = "Add Account", + contentDescription = stringResource(R.string.add_account_content_desc), tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(20.dp) ) @@ -234,14 +236,14 @@ fun DashboardScreen( IconButton( onClick = { scope.launch { - clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("Account ID", account.gajuId))) + clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(accountIdLabel, account.gajuId))) } }, modifier = Modifier.size(24.dp) ) { Icon( painter = painterResource(id = R.drawable.ic_content_copy), - contentDescription = "Copy ID", + contentDescription = stringResource(R.string.copy_id_content_desc), tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp) ) @@ -264,9 +266,9 @@ fun DashboardScreen( verticalAlignment = Alignment.CenterVertically, ) { Column { - Text("BALANCE", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) + Text(stringResource(R.string.balance_label), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) Text( - text = "ON ${currentWallet.networkId.uppercase()}", + text = stringResource(R.string.on_network_label, currentWallet.networkId.uppercase()), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f), fontWeight = FontWeight.Bold @@ -281,7 +283,7 @@ fun DashboardScreen( ) { Icon( painter = painterResource(id = R.drawable.ic_refresh), - contentDescription = "Refresh Balance", + contentDescription = stringResource(R.string.refresh_balance_content_desc), tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(20.dp), ) @@ -295,7 +297,7 @@ fun DashboardScreen( ) { Icon( painter = painterResource(id = R.drawable.settings), - contentDescription = "Account Settings", + contentDescription = stringResource(R.string.account_settings_content_desc), tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(24.dp), ) @@ -326,19 +328,19 @@ fun DashboardScreen( horizontalArrangement = Arrangement.SpaceEvenly, ) { ActionCircle( - text = "Send", + text = stringResource(R.string.send_action), containerColor = MaterialTheme.colorScheme.secondary, contentColor = MaterialTheme.colorScheme.onSecondary, onClick = onSend, ) ActionCircle( - text = "GRIDS", + text = stringResource(R.string.grids_action), containerColor = MaterialTheme.colorScheme.tertiary, contentColor = MaterialTheme.colorScheme.onTertiary, onClick = onGrids, ) ActionCircle( - text = "Receive", + text = stringResource(R.string.receive_action), containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary, onClick = onReceive, diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DeleteConfirmationScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DeleteConfirmationScreen.kt index def179d..18190c3 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DeleteConfirmationScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DeleteConfirmationScreen.kt @@ -28,10 +28,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import swiss.qpq.gajumobile.R import swiss.qpq.gajumobile.ui.components.GajuButton import swiss.qpq.gajumobile.ui.components.GajuHeader @@ -42,6 +44,11 @@ fun DeleteConfirmationScreen( onConfirm: () -> Unit, onCancel: () -> Unit, ) { + val localizedType = when (type) { + "WALLET" -> stringResource(R.string.wallet) + "ACCOUNT" -> stringResource(R.string.account) + else -> type + } Scaffold( modifier = Modifier.fillMaxSize(), bottomBar = { @@ -52,7 +59,7 @@ fun DeleteConfirmationScreen( .padding(24.dp), ) { GajuButton( - text = "CANCEL", + text = stringResource(R.string.cancel), onClick = onCancel, modifier = Modifier.fillMaxWidth(), containerColor = MaterialTheme.colorScheme.secondary, @@ -60,7 +67,7 @@ fun DeleteConfirmationScreen( ) Spacer(modifier = Modifier.height(16.dp)) GajuButton( - text = "CONFIRM\nDELETE", + text = stringResource(R.string.confirm_delete_button), onClick = onConfirm, modifier = Modifier.fillMaxWidth(), containerColor = Color(0xFFF44336), @@ -80,7 +87,7 @@ fun DeleteConfirmationScreen( GajuHeader() Text( - text = "DELETE $type", + text = stringResource(R.string.delete_type_title, localizedType), color = MaterialTheme.colorScheme.onBackground, fontSize = 20.sp, fontWeight = FontWeight.Bold, @@ -104,14 +111,14 @@ fun DeleteConfirmationScreen( Spacer(modifier = Modifier.height(32.dp)) - Text("Accounts within this wallet", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) + Text(stringResource(R.string.accounts_within_this_wallet_label), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) // TODO: Use real account/balance if possible - Text("Daily account", color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.padding(top = 16.dp)) + Text(stringResource(R.string.daily_account_placeholder), color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.padding(top = 16.dp)) Spacer(modifier = Modifier.height(32.dp)) Text( - text = "WARNING: Deleting a $type will delete all accounts within it.", + text = stringResource(R.string.delete_warning_message, localizedType), color = MaterialTheme.colorScheme.error, fontWeight = FontWeight.Bold, fontSize = 12.sp, @@ -119,15 +126,7 @@ fun DeleteConfirmationScreen( ) Text( - text = """ - To recover a deleted account, you will need your mnemonic phrase. Lost mnemonic phrases cannot be retrieved. - - All funds within a lost account are also lost. - - Back up your mnemonic phrases for every account before making any changes here to be safe. - - This action cannot be undone. - """.trimIndent(), + text = stringResource(R.string.delete_risks_disclaimer), color = MaterialTheme.colorScheme.onBackground, fontSize = 12.sp, textAlign = TextAlign.Center, @@ -138,7 +137,7 @@ fun DeleteConfirmationScreen( Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { checked = !checked }) { Checkbox(checked = checked, onCheckedChange = { checked = it }) Text( - "I understand and assume all risks from this action. I would like to proceed.", + stringResource(R.string.understand_risks_checkbox), fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground, ) diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/EnvironmentInfoScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/EnvironmentInfoScreen.kt index 63bd154..8acc0de 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/EnvironmentInfoScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/EnvironmentInfoScreen.kt @@ -14,7 +14,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import swiss.qpq.gajumobile.R import swiss.qpq.gajumobile.ui.components.GajuButton import swiss.qpq.gajumobile.ui.components.GajuHeader import swiss.qpq.gajumobile.ui.components.SecurityDataSection @@ -49,7 +51,7 @@ fun EnvironmentInfoScreen( ) { GajuHeader() Text( - "Environment Info", + stringResource(R.string.menu_environment_info), style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.onBackground ) @@ -57,43 +59,43 @@ fun EnvironmentInfoScreen( Spacer(modifier = Modifier.height(24.dp)) SecurityDataSection( - title = "Security Hardware", + title = stringResource(R.string.security_hardware_title), data = listOf( - "Keystore" to if (hasStrongBox) "StrongBox (Secure Element)" else "Hardware TEE", - "Auth Enforcement" to "OS Lock Required", - "Encryption" to "Hardware-Generated IVs" + stringResource(R.string.keystore_label) to if (hasStrongBox) stringResource(R.string.strongbox_label) else stringResource(R.string.hardware_tee_label), + stringResource(R.string.auth_enforcement_label) to stringResource(R.string.os_lock_required_label), + stringResource(R.string.encryption_label) to stringResource(R.string.hw_generated_ivs_label) ) ) SecurityDataSection( - title = "Device Identity", + title = stringResource(R.string.device_identity_title), data = listOf( - "Manufacturer" to android.os.Build.MANUFACTURER, - "Model" to android.os.Build.MODEL, - "Board" to android.os.Build.BOARD, - "Hardware" to android.os.Build.HARDWARE + stringResource(R.string.manufacturer_label) to android.os.Build.MANUFACTURER, + stringResource(R.string.model_label) to android.os.Build.MODEL, + stringResource(R.string.board_label) to android.os.Build.BOARD, + stringResource(R.string.hardware_label) to android.os.Build.HARDWARE ) ) SecurityDataSection( - title = "System Build", + title = stringResource(R.string.system_build_title), data = listOf( - "Android Version" to "${android.os.Build.VERSION.RELEASE} (API ${android.os.Build.VERSION.SDK_INT})", - "Security Patch" to android.os.Build.VERSION.SECURITY_PATCH, - "Fingerprint" to android.os.Build.FINGERPRINT + stringResource(R.string.android_version_label) to "${android.os.Build.VERSION.RELEASE} (API ${android.os.Build.VERSION.SDK_INT})", + stringResource(R.string.security_patch_label) to android.os.Build.VERSION.SECURITY_PATCH, + stringResource(R.string.fingerprint_label) to android.os.Build.FINGERPRINT ) ) SecurityDataSection( title = "GajuMobile", data = listOf( - "App Version" to appVersion, - "Theme" to "Arboreal (Gajumaru)" + stringResource(R.string.app_version_label) to appVersion, + stringResource(R.string.theme_label) to stringResource(R.string.theme_name) ) ) Spacer(modifier = Modifier.weight(1f)) - GajuButton(text = "Done", onClick = onBack) + GajuButton(text = stringResource(R.string.done_button), onClick = onBack) } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ExplorerScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ExplorerScreen.kt index 0fe51f0..60439e2 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ExplorerScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ExplorerScreen.kt @@ -16,10 +16,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import swiss.qpq.gajumobile.R import swiss.qpq.gajumobile.ui.components.GajuTopBar @Composable @@ -37,13 +36,13 @@ fun ExplorerScreen( horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = "Transactions", + text = stringResource(R.string.explorer_title), color = MaterialTheme.colorScheme.onBackground, style = MaterialTheme.typography.headlineMedium, modifier = Modifier.padding(vertical = 16.dp), ) - Text("Loading Groot", color = MaterialTheme.colorScheme.primary, fontStyle = androidx.compose.ui.text.font.FontStyle.Italic) + Text(stringResource(R.string.loading_groot), color = MaterialTheme.colorScheme.primary, fontStyle = androidx.compose.ui.text.font.FontStyle.Italic) Spacer(modifier = Modifier.height(16.dp)) @@ -53,7 +52,7 @@ fun ExplorerScreen( shape = RoundedCornerShape(4.dp), ) { Box(contentAlignment = Alignment.Center) { - Text("Gaju Explorer Content", color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(stringResource(R.string.explorer_content_placeholder), color = MaterialTheme.colorScheme.onSurfaceVariant) } } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/FetchingScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/FetchingScreen.kt index e371310..6d6e73f 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/FetchingScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/FetchingScreen.kt @@ -1,15 +1,26 @@ package swiss.qpq.gajumobile.ui.screens import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import swiss.qpq.gajumobile.R import swiss.qpq.gajumobile.ui.components.GajuButton import swiss.qpq.gajumobile.ui.components.GajuHeader @@ -43,7 +54,7 @@ fun FetchingScreen( Spacer(modifier = Modifier.height(32.dp)) Text( - text = "RETRIEVING REQUEST", + text = stringResource(R.string.retrieving_request_title), style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onBackground, fontWeight = FontWeight.Bold @@ -52,7 +63,7 @@ fun FetchingScreen( Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Connecting to:", + text = stringResource(R.string.connecting_to_label), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -68,7 +79,7 @@ fun FetchingScreen( Spacer(modifier = Modifier.weight(1f)) GajuButton( - text = "CANCEL", + text = stringResource(R.string.cancel), onClick = onCancel, modifier = Modifier.fillMaxWidth(), containerColor = MaterialTheme.colorScheme.secondary, diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/LockScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/LockScreen.kt index 721796b..2359872 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/LockScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/LockScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -47,14 +48,14 @@ fun LockScreen( ) Spacer(modifier = Modifier.height(48.dp)) Text( - text = "App Locked", + text = stringResource(R.string.app_locked_title), color = MaterialTheme.colorScheme.onBackground, fontSize = 24.sp, fontWeight = FontWeight.Bold, ) Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Please authenticate to continue.", + text = stringResource(R.string.auth_request_message), color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f), fontSize = 16.sp, ) @@ -72,7 +73,7 @@ fun LockScreen( Spacer(modifier = Modifier.height(48.dp)) Button(onClick = onUnlockRequest) { - Text("Unlock") + Text(stringResource(R.string.unlock_button)) } } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageAccountScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageAccountScreen.kt index 1cb32cf..55f4cb4 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageAccountScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageAccountScreen.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard 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.TextAlign import androidx.compose.ui.unit.dp @@ -62,6 +63,7 @@ fun ManageAccountScreen( ) { val clipboard = LocalClipboard.current val scope = rememberCoroutineScope() + val accountIdLabel = stringResource(R.string.account_id_label_clipboard) var showRenameDialog by remember { mutableStateOf(value = false) } var newName by remember(account.label) { mutableStateOf(value = account.label) } @@ -73,21 +75,21 @@ fun ManageAccountScreen( tonalElevation = 8.dp ) { Column(modifier = Modifier.padding(24.dp)) { - Text("Rename Account", style = MaterialTheme.typography.headlineSmall) + Text(stringResource(R.string.rename_account_title), style = MaterialTheme.typography.headlineSmall) Spacer(modifier = Modifier.height(16.dp)) GajuTextField( value = newName, onValueChange = { newName = it }, - label = "New Name" + label = stringResource(R.string.new_name_label) ) Spacer(modifier = Modifier.height(24.dp)) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { - TextButton(onClick = { showRenameDialog = false }) { Text("CANCEL") } + TextButton(onClick = { showRenameDialog = false }) { Text(stringResource(R.string.cancel)) } Spacer(modifier = Modifier.width(8.dp)) TextButton(onClick = { onRename(newName) showRenameDialog = false - }) { Text("SAVE") } + }) { Text(stringResource(R.string.save_button_caps)) } } } } @@ -105,12 +107,12 @@ fun ManageAccountScreen( horizontalAlignment = Alignment.CenterHorizontally, ) { if (isUnverified) { - ManageAction(label = "Verify mnemonic backup", icon = R.drawable.ic_check_circle, onClick = onVerifyMnemonic) + ManageAction(label = stringResource(R.string.verify_mnemonic_backup_action), icon = R.drawable.ic_check_circle, onClick = onVerifyMnemonic) Spacer(modifier = Modifier.height(8.dp)) } - ManageAction(label = "View mnemonic phrase", icon = R.drawable.ic_visibility, onClick = onViewMnemonic) + ManageAction(label = stringResource(R.string.view_mnemonic_phrase_action), icon = R.drawable.ic_visibility, onClick = onViewMnemonic) Spacer(modifier = Modifier.height(8.dp)) - ManageAction(label = "Delete this account", icon = R.drawable.ic_delete, isDestructive = true, onClick = onDelete) + ManageAction(label = stringResource(R.string.delete_this_account_action), icon = R.drawable.ic_delete, isDestructive = true, onClick = onDelete) } }, ) { innerPadding -> @@ -125,7 +127,7 @@ fun ManageAccountScreen( GajuHeader() Text( - text = "Manage Account", + text = stringResource(R.string.manage_account_title), color = MaterialTheme.colorScheme.onBackground, style = MaterialTheme.typography.headlineMedium, modifier = Modifier.padding(vertical = 16.dp), @@ -175,7 +177,7 @@ fun ManageAccountScreen( Spacer(modifier = Modifier.height(24.dp)) Text( - text = "Account ID:", + text = stringResource(R.string.account_id_label), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -197,14 +199,14 @@ fun ManageAccountScreen( IconButton( onClick = { scope.launch { - clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("Account ID", account.gajuId))) + clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(accountIdLabel, account.gajuId))) } }, modifier = Modifier.size(24.dp) ) { Icon( painter = painterResource(id = R.drawable.ic_content_copy), - contentDescription = "Copy ID", + contentDescription = stringResource(R.string.copy_id_content_desc), tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp) ) diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageWalletsScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageWalletsScreen.kt index 99e4ccb..12e4e94 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageWalletsScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageWalletsScreen.kt @@ -32,6 +32,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.unit.dp import androidx.compose.ui.unit.sp @@ -74,10 +75,10 @@ fun ManageWalletsScreen( tonalElevation = 8.dp, ) { Column(modifier = Modifier.padding(24.dp)) { - Text("Node Status", style = MaterialTheme.typography.headlineSmall) + Text(stringResource(R.string.node_status_title), style = MaterialTheme.typography.headlineSmall) Spacer(modifier = Modifier.height(16.dp)) if (statusError != null) { - Text("Error: $statusError", color = MaterialTheme.colorScheme.error) + Text(stringResource(R.string.error_with_value, statusError), color = MaterialTheme.colorScheme.error) } else if (statusResult != null) { val scrollState = rememberScrollState() Column(modifier = Modifier.height(300.dp).verticalScroll(scrollState)) { @@ -91,7 +92,7 @@ fun ManageWalletsScreen( } Spacer(modifier = Modifier.height(24.dp)) TextButton(onClick = onClearStatus, modifier = Modifier.align(Alignment.End)) { - Text("CLOSE") + Text(stringResource(R.string.close_button_caps)) } } } @@ -106,23 +107,23 @@ fun ManageWalletsScreen( tonalElevation = 8.dp, ) { Column(modifier = Modifier.padding(24.dp)) { - Text("Rename Wallet", style = MaterialTheme.typography.headlineSmall) + Text(stringResource(R.string.rename_wallet_title), style = MaterialTheme.typography.headlineSmall) Spacer(modifier = Modifier.height(16.dp)) GajuTextField( value = newName, onValueChange = { newName = it }, - label = "New Name", + label = stringResource(R.string.new_name_label), ) Spacer(modifier = Modifier.height(24.dp)) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { - TextButton(onClick = { walletToRename = null }) { Text("CANCEL") } + TextButton(onClick = { walletToRename = null }) { Text(stringResource(R.string.cancel)) } Spacer(modifier = Modifier.width(8.dp)) TextButton( onClick = { walletToRename?.let { onRenameWallet(it.id, newName) } walletToRename = null } - ) { Text("SAVE") } + ) { Text(stringResource(R.string.save_button_caps)) } } } } @@ -138,12 +139,12 @@ fun ManageWalletsScreen( tonalElevation = 8.dp, ) { Column(modifier = Modifier.padding(24.dp)) { - Text("Add Endpoint", style = MaterialTheme.typography.headlineSmall) + Text(stringResource(R.string.add_endpoint_title), style = MaterialTheme.typography.headlineSmall) Spacer(modifier = Modifier.height(16.dp)) GajuTextField( value = newHost, onValueChange = { newHost = it }, - label = "Host (IP or Domain)", + label = stringResource(R.string.host_label), singleLine = true, placeholder = placeholderHost, ) @@ -151,7 +152,7 @@ fun ManageWalletsScreen( GajuTextField( value = newPort, onValueChange = { newPort = it }, - label = "Port", + label = stringResource(R.string.port_label), singleLine = true, placeholder = "3013" ) @@ -162,11 +163,11 @@ fun ManageWalletsScreen( onCheckedChange = { newUseTls = it } ) Spacer(Modifier.width(12.dp)) - Text("Use TLS (HTTPS)", style = MaterialTheme.typography.bodyMedium) + Text(stringResource(R.string.use_tls_label), style = MaterialTheme.typography.bodyMedium) } Spacer(modifier = Modifier.height(24.dp)) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { - TextButton(onClick = { walletForNewEndpoint = null }) { Text("CANCEL") } + TextButton(onClick = { walletForNewEndpoint = null }) { Text(stringResource(R.string.cancel)) } Spacer(modifier = Modifier.width(8.dp)) TextButton( enabled = (newHost.isNotBlank() && newPort.toIntOrNull() != null), @@ -180,7 +181,7 @@ fun ManageWalletsScreen( newPort = "3013" newUseTls = false } - ) { Text("ADD") } + ) { Text(stringResource(R.string.add_button_caps)) } } } } @@ -199,7 +200,7 @@ fun ManageWalletsScreen( ) { GajuHeader() Text( - "Manage Wallets", + stringResource(R.string.manage_wallets_title), style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.onBackground ) @@ -240,7 +241,7 @@ fun ManageWalletsScreen( Spacer(modifier = Modifier.height(24.dp)) ManageAction( - label = "Add new wallet", + label = stringResource(R.string.add_new_wallet_action), icon = R.drawable.ic_add, onClick = onAddWallet ) @@ -248,7 +249,7 @@ fun ManageWalletsScreen( Spacer(modifier = Modifier.height(32.dp)) TextButton(onClick = onBack) { - Text("Done") + Text(stringResource(R.string.done_button)) } } } @@ -292,7 +293,7 @@ private fun WalletManagementCard( IconButton(onClick = onDelete) { Icon( painter = painterResource(id = R.drawable.ic_delete), - contentDescription = "Delete Wallet", + contentDescription = stringResource(R.string.delete_wallet_content_desc), tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(20.dp), ) @@ -302,7 +303,7 @@ private fun WalletManagementCard( Spacer(modifier = Modifier.height(16.dp)) Text( - text = "NETWORK ENDPOINTS", + text = stringResource(R.string.network_endpoints_label), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f) ) @@ -332,7 +333,7 @@ private fun WalletManagementCard( if (wallet.endpoints.size > 1) { Icon( painter = painterResource(id = R.drawable.ic_delete), - contentDescription = "Remove Endpoint", + contentDescription = stringResource(R.string.remove_endpoint_content_desc), modifier = Modifier.size(16.dp).clickable { onDeleteEndpoint(index) }, tint = MaterialTheme.colorScheme.error.copy(alpha = 0.6f) ) @@ -347,7 +348,7 @@ private fun WalletManagementCard( ) { Icon(painterResource(R.drawable.ic_check_circle), null, modifier = Modifier.size(14.dp)) Spacer(Modifier.width(6.dp)) - Text("CHECK STATUS", fontSize = 10.sp, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.check_status_button_caps), fontSize = 10.sp, fontWeight = FontWeight.Bold) } } } @@ -359,13 +360,13 @@ private fun WalletManagementCard( ) { Icon(painterResource(R.drawable.ic_add), null, modifier = Modifier.size(14.dp)) Spacer(Modifier.width(4.dp)) - Text("ADD ENDPOINT", fontSize = 11.sp) + Text(stringResource(R.string.add_endpoint_button_caps), fontSize = 11.sp) } HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp), color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) Text( - text = "${wallet.accounts.size} account(s)", + text = stringResource(R.string.accounts_count_label, wallet.accounts.size), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), ) diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/MnemonicRecoveryScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/MnemonicRecoveryScreen.kt index d23a9d2..2b55585 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/MnemonicRecoveryScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/MnemonicRecoveryScreen.kt @@ -34,7 +34,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import swiss.qpq.gajumobile.R import swiss.qpq.gajumobile.security.AccountAirlock import swiss.qpq.gajumobile.ui.components.CommittedWordChip import swiss.qpq.gajumobile.ui.components.GajuButton @@ -98,7 +100,7 @@ fun MnemonicRecoveryScreen( horizontalArrangement = Arrangement.SpaceBetween ) { GajuButton( - text = "BACK", + text = stringResource(R.string.back_button_caps), onClick = onBack, modifier = Modifier.weight(1f), containerColor = MaterialTheme.colorScheme.secondary, @@ -106,7 +108,7 @@ fun MnemonicRecoveryScreen( ) Spacer(modifier = Modifier.width(16.dp)) GajuButton( - text = "RECOVER", + text = stringResource(R.string.recover_button_caps), enabled = committedWords.size >= 21 && !isAddingWord, onClick = { val phrase = committedWords.map { it.toByteArray(Charsets.UTF_8) }.toTypedArray() @@ -127,14 +129,14 @@ fun MnemonicRecoveryScreen( ) { GajuHeader() Text( - "RECOVER ACCOUNT", + stringResource(R.string.recover_account_title), style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.onBackground ) Spacer(modifier = Modifier.height(16.dp)) Text( - "Mnemonic Phrase:", + stringResource(R.string.mnemonic_phrase_label), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -171,7 +173,7 @@ fun MnemonicRecoveryScreen( } else if (committedWords.size < 23) { AssistChip( onClick = { isAddingWord = true }, - label = { Text("+ NEXT WORD") }, + label = { Text(stringResource(R.string.add_next_word_button)) }, colors = AssistChipDefaults.assistChipColors( containerColor = MaterialTheme.colorScheme.primaryContainer, labelColor = MaterialTheme.colorScheme.onPrimaryContainer diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/MnemonicVerifyScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/MnemonicVerifyScreen.kt index 615cad0..325d724 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/MnemonicVerifyScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/MnemonicVerifyScreen.kt @@ -34,7 +34,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import swiss.qpq.gajumobile.R import swiss.qpq.gajumobile.security.AccountAirlock import swiss.qpq.gajumobile.ui.components.CommittedWordChip import swiss.qpq.gajumobile.ui.components.GajuButton @@ -102,7 +104,7 @@ fun MnemonicVerifyScreen( horizontalArrangement = Arrangement.SpaceBetween ) { GajuButton( - text = "BACK", + text = stringResource(R.string.back_button_caps), onClick = onBack, modifier = Modifier.weight(1f), containerColor = MaterialTheme.colorScheme.secondary, @@ -110,7 +112,7 @@ fun MnemonicVerifyScreen( ) Spacer(modifier = Modifier.width(16.dp)) GajuButton( - text = "VERIFY", + text = stringResource(R.string.verify_button_caps), enabled = committedWords.size == expectedPhrase.size && !isAddingWord, onClick = { val inputPhrase = committedWords.map { it.toByteArray(Charsets.UTF_8) } @@ -143,7 +145,7 @@ fun MnemonicVerifyScreen( ) { GajuHeader() Text( - "VERIFY BACKUP", + stringResource(R.string.verify_backup_title), style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.onBackground ) @@ -185,7 +187,7 @@ fun MnemonicVerifyScreen( } else if (committedWords.size < expectedPhrase.size) { AssistChip( onClick = { isAddingWord = true }, - label = { Text("+ NEXT WORD") }, + label = { Text(stringResource(R.string.add_next_word_button)) }, colors = AssistChipDefaults.assistChipColors( containerColor = MaterialTheme.colorScheme.primaryContainer, labelColor = MaterialTheme.colorScheme.onPrimaryContainer @@ -197,7 +199,7 @@ fun MnemonicVerifyScreen( if (verificationError) { Spacer(modifier = Modifier.height(16.dp)) Text( - "Mnemonic phrase does not match. Please check again.", + stringResource(R.string.err_mnemonic_mismatch), color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall ) diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRReceiveScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRReceiveScreen.kt index 1707ade..58e5dd0 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRReceiveScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRReceiveScreen.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.launch +import androidx.compose.ui.res.stringResource import swiss.qpq.gajumobile.R import swiss.qpq.gajumobile.ui.components.GajuButton import swiss.qpq.gajumobile.ui.components.GajuHeader @@ -65,6 +66,7 @@ fun QRReceiveScreen( var isGenerated by remember { mutableStateOf(value = false) } val clipboard = LocalClipboard.current val scope = rememberCoroutineScope() + val gridsUrlLabel = stringResource(R.string.grids_url_label_clipboard) val amountPucks = remember(amountStr) { try { @@ -110,7 +112,7 @@ fun QRReceiveScreen( .padding(24.dp), ) { GajuButton( - text = if (isGenerated) "EDIT" else "BACK", + text = if (isGenerated) stringResource(id = R.string.edit_button_caps) else stringResource(id = R.string.back_button_caps), onClick = { if (isGenerated) isGenerated = false else onDone() }, modifier = Modifier.weight(1f), containerColor = MaterialTheme.colorScheme.secondary, @@ -118,7 +120,7 @@ fun QRReceiveScreen( ) Spacer(modifier = Modifier.width(16.dp)) GajuButton( - text = if (isGenerated) "DONE" else "GENERATE", + text = if (isGenerated) stringResource(id = R.string.done_button_caps) else stringResource(id = R.string.generate_button_caps), onClick = { if (isGenerated) onDone() else isGenerated = true }, modifier = Modifier.weight(1f), ) @@ -137,7 +139,7 @@ fun QRReceiveScreen( GajuHeader() Text( - text = if (isGenerated) "TRANSFER REQUEST" else "RECEIVE", + text = if (isGenerated) stringResource(id = R.string.transfer_request_title) else stringResource(id = R.string.receive_title), color = MaterialTheme.colorScheme.onBackground, fontSize = 24.sp, fontWeight = FontWeight.Bold, @@ -147,7 +149,7 @@ fun QRReceiveScreen( if (!isGenerated) { // Input Phase Column(modifier = Modifier.fillMaxWidth()) { - Text("RECIPIENT", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) + Text(stringResource(id = R.string.recipient_label), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) Spacer(modifier = Modifier.height(4.dp)) Surface( color = MaterialTheme.colorScheme.surfaceVariant, @@ -178,8 +180,8 @@ fun QRReceiveScreen( GajuTextField( value = amountStr, onValueChange = { amountStr = it }, - label = "REQUESTED AMOUNT (OPTIONAL)", - placeholder = "0.0", + label = stringResource(id = R.string.requested_amount_label), + placeholder = stringResource(id = R.string.requested_amount_placeholder), modifier = Modifier.fillMaxWidth(), trailingIcon = { Text(unitLabel, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, fontSize = 12.sp) }, singleLine = true, @@ -190,8 +192,8 @@ fun QRReceiveScreen( GajuTextField( value = payload, onValueChange = { payload = it }, - label = "PAYLOAD / MESSAGE (OPTIONAL)", - placeholder = "Add a note...", + label = stringResource(id = R.string.payload_optional_label), + placeholder = stringResource(id = R.string.payload_optional_placeholder), modifier = Modifier.fillMaxWidth(), ) } else { @@ -202,7 +204,7 @@ fun QRReceiveScreen( .background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(12.dp)) .padding(16.dp), ) { - DetailRow(label = "RECIPIENT", value = account.label) + DetailRow(label = stringResource(id = R.string.recipient_label), value = account.label) Text( text = account.gajuId, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), @@ -213,13 +215,13 @@ fun QRReceiveScreen( modifier = Modifier.padding(vertical = 8.dp), color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), ) - DetailRow(label = "AMOUNT", value = formattedAmount) + DetailRow(label = stringResource(id = R.string.amount_label), value = formattedAmount) if (payload.isNotBlank()) { HorizontalDivider( modifier = Modifier.padding(vertical = 8.dp), color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), ) - DetailRow(label = "MESSAGE", value = payload) + DetailRow(label = stringResource(id = R.string.message_label), value = payload) } } @@ -236,7 +238,7 @@ fun QRReceiveScreen( } else { Icon( painter = painterResource(id = R.drawable.ic_qr_code), - contentDescription = "QR Code", + contentDescription = stringResource(id = R.string.qr_code_content_desc), modifier = Modifier.size(180.dp), tint = Color.Black.copy(alpha = 0.1f), ) @@ -252,18 +254,18 @@ fun QRReceiveScreen( horizontalArrangement = Arrangement.SpaceBetween, ) { Text( - text = "GRIDS URL", + text = stringResource(id = R.string.grids_url_label), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, ) Icon( painter = painterResource(id = R.drawable.ic_content_copy), - contentDescription = "Copy URL", + contentDescription = stringResource(id = R.string.copy_url_content_desc), modifier = Modifier .size(20.dp) .clickable { scope.launch { - clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("GRIDS URL", generatedUrl))) + clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(gridsUrlLabel, generatedUrl))) } }, tint = MaterialTheme.colorScheme.primary, @@ -286,7 +288,7 @@ fun QRReceiveScreen( Spacer(modifier = Modifier.height(24.dp)) Text( - text = "Scan to send to this account", + text = stringResource(id = R.string.scan_to_send_hint), color = MaterialTheme.colorScheme.onBackground, fontSize = 14.sp, fontWeight = FontWeight.Light, diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRScannerScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRScannerScreen.kt index 244b7c6..2a498ed 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRScannerScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRScannerScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.unit.sp import com.google.mlkit.vision.barcode.common.Barcode import com.google.mlkit.vision.codescanner.GmsBarcodeScanning import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions +import androidx.compose.ui.res.stringResource import swiss.qpq.gajumobile.R import swiss.qpq.gajumobile.ui.components.GajuHeader import swiss.qpq.gajumobile.ui.components.ScannerAction @@ -75,7 +76,7 @@ fun QRScannerScreen( GajuHeader() Text( - text = "SCAN QR", + text = stringResource(id = R.string.scan_qr_title), color = MaterialTheme.colorScheme.onBackground, fontSize = 20.sp, fontWeight = FontWeight.Light, @@ -103,7 +104,7 @@ fun QRScannerScreen( ) Spacer(modifier = Modifier.height(16.dp)) Text( - text = "TAP TO SCAN", + text = stringResource(id = R.string.tap_to_scan), color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, fontSize = 16.sp, @@ -114,7 +115,7 @@ fun QRScannerScreen( Spacer(modifier = Modifier.weight(0.5f)) GajuButton( - text = "MANUAL ENTRY", + text = stringResource(id = R.string.manual_entry_button), onClick = onManualEntry, modifier = Modifier.padding(horizontal = 48.dp), ) @@ -127,9 +128,9 @@ fun QRScannerScreen( .padding(bottom = 48.dp), horizontalArrangement = Arrangement.SpaceEvenly, ) { - ScannerAction(label = "Back", icon = R.drawable.ic_home, onClick = onBack) - ScannerAction(label = "Generate QR", icon = R.drawable.ic_qr_code, onClick = onGenerate) - ScannerAction(label = "Upload QR", icon = R.drawable.ic_qr_code, onClick = onUpload) + ScannerAction(label = stringResource(id = R.string.back_action), icon = R.drawable.ic_home, onClick = onBack) + ScannerAction(label = stringResource(id = R.string.generate_qr_action), icon = R.drawable.ic_qr_code, onClick = onGenerate) + ScannerAction(label = stringResource(id = R.string.upload_qr_action), icon = R.drawable.ic_qr_code, onClick = onUpload) } } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendFormScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendFormScreen.kt index 6980ef5..a4e6915 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendFormScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendFormScreen.kt @@ -32,6 +32,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.res.stringResource +import swiss.qpq.gajumobile.R import swiss.qpq.gajumobile.ui.components.GajuButton import swiss.qpq.gajumobile.ui.components.GajuHeader import swiss.qpq.gajumobile.ui.components.GajuTextField @@ -66,7 +68,7 @@ fun SendFormScreen( .padding(24.dp), ) { GajuButton( - text = "BACK", + text = stringResource(id = R.string.back_button_caps), onClick = onBack, modifier = Modifier.weight(1f), containerColor = MaterialTheme.colorScheme.secondary, @@ -74,7 +76,7 @@ fun SendFormScreen( ) Spacer(modifier = Modifier.width(16.dp)) GajuButton( - text = "REVIEW", + text = stringResource(id = R.string.review_button_caps), onClick = { onReview(toAddress, amount, payload, gas, gasPrice, ttl) }, modifier = Modifier.weight(1f), enabled = toAddress.isNotBlank() && amount.isNotBlank(), @@ -94,7 +96,7 @@ fun SendFormScreen( GajuHeader() Text( - text = "SEND", + text = stringResource(id = R.string.send_title), color = MaterialTheme.colorScheme.onBackground, fontSize = 24.sp, fontWeight = FontWeight.Bold, @@ -103,7 +105,7 @@ fun SendFormScreen( // 0. Sender Account (Read-only) Column(modifier = Modifier.fillMaxWidth()) { - Text("SENDING FROM", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) + Text(stringResource(id = R.string.sending_from_label), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) Spacer(modifier = Modifier.height(4.dp)) Surface( color = MaterialTheme.colorScheme.surfaceVariant, @@ -118,7 +120,7 @@ fun SendFormScreen( fontSize = 16.sp, ) Text( - text = "Balance: ${fromAccount.formattedBalance(balanceFormat, balanceUnit)}", + text = stringResource(id = R.string.balance_with_value, fromAccount.formattedBalance(balanceFormat, balanceUnit)), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, fontWeight = FontWeight.Medium, @@ -139,8 +141,8 @@ fun SendFormScreen( GajuTextField( value = toAddress, onValueChange = { toAddress = it }, - label = "RECIPIENT ACCOUNT", - placeholder = "ak_...", + label = stringResource(id = R.string.recipient_account_label), + placeholder = stringResource(id = R.string.recipient_account_placeholder), modifier = Modifier.fillMaxWidth(), singleLine = true, ) @@ -152,8 +154,8 @@ fun SendFormScreen( GajuTextField( value = amount, onValueChange = { amount = it }, - label = "AMOUNT", - placeholder = "0.0", + label = stringResource(id = R.string.amount_label), + placeholder = stringResource(id = R.string.amount_placeholder), modifier = Modifier.fillMaxWidth(), trailingIcon = { Text(unitLabel, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, fontSize = 12.sp) }, singleLine = true, @@ -165,15 +167,15 @@ fun SendFormScreen( GajuTextField( value = payload, onValueChange = { payload = it }, - label = "PAYLOAD / MESSAGE", - placeholder = "Optional message...", + label = stringResource(id = R.string.payload_label), + placeholder = stringResource(id = R.string.payload_placeholder), modifier = Modifier.fillMaxWidth(), ) Spacer(modifier = Modifier.height(24.dp)) Text( - text = if (showAdvanced) "HIDE DETAILS" else "SHOW DETAILS", + text = if (showAdvanced) stringResource(id = R.string.hide_details) else stringResource(id = R.string.show_details), color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, fontSize = 14.sp, @@ -191,7 +193,7 @@ fun SendFormScreen( GajuTextField( value = gas, onValueChange = { gas = it }, - label = "GAS", + label = stringResource(id = R.string.gas_label), modifier = Modifier.fillMaxWidth(), singleLine = true, ) @@ -203,7 +205,7 @@ fun SendFormScreen( GajuTextField( value = gasPrice, onValueChange = { gasPrice = it }, - label = "GAS PRICE", + label = stringResource(id = R.string.gas_price_label), modifier = Modifier.weight(1f), singleLine = true, ) @@ -212,7 +214,7 @@ fun SendFormScreen( GajuTextField( value = ttl, onValueChange = { ttl = it }, - label = "TTL", + label = stringResource(id = R.string.ttl_label), modifier = Modifier.weight(1f), singleLine = true, ) diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendReviewScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendReviewScreen.kt index 9afe5d0..a13fe38 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendReviewScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendReviewScreen.kt @@ -32,6 +32,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.res.stringResource +import swiss.qpq.gajumobile.R import swiss.qpq.gajumobile.ui.components.GajuButton import swiss.qpq.gajumobile.ui.components.GajuHeader @@ -61,7 +63,7 @@ fun SendReviewScreen( ) { Checkbox(checked = confirmed, onCheckedChange = { confirmed = it }) Text( - "I confirm that the above details are correct and I would like to proceed.", + stringResource(id = R.string.confirm_details_checkbox), fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground, ) @@ -69,7 +71,7 @@ fun SendReviewScreen( Row(modifier = Modifier.fillMaxWidth()) { GajuButton( - text = "BACK", + text = stringResource(id = R.string.back_button_caps), onClick = onBack, modifier = Modifier.weight(1f), containerColor = MaterialTheme.colorScheme.secondary, @@ -77,7 +79,7 @@ fun SendReviewScreen( ) Spacer(modifier = Modifier.width(16.dp)) GajuButton( - text = "SEND", + text = stringResource(id = R.string.send_button_caps), onClick = onSend, modifier = Modifier.weight(1f), enabled = confirmed, @@ -98,19 +100,19 @@ fun SendReviewScreen( GajuHeader() Text( - text = "REVIEW SEND", + text = stringResource(id = R.string.review_send_title), color = MaterialTheme.colorScheme.onBackground, fontSize = 24.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(vertical = 16.dp), ) - Text("SENDING FROM", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) + Text(stringResource(id = R.string.sending_from_label), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) Text(senderLabel, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.height(16.dp)) - Text("SENDING TO", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) + Text(stringResource(id = R.string.sending_to_label), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) Text(recipientLabel, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onBackground) Text( text = recipientId, @@ -128,14 +130,14 @@ fun SendReviewScreen( shape = RoundedCornerShape(16.dp), ) { Column(modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) { - Text("You are sending a payment of", fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(stringResource(id = R.string.payment_summary_prefix), fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(amountStr, fontSize = 24.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) - Text("to", fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(stringResource(id = R.string.payment_summary_to), fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(recipientId.take(12) + "...", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) Spacer(modifier = Modifier.height(16.dp)) - Text("THIS ACTION CANNOT BE UNDONE.", fontWeight = FontWeight.ExtraBold, color = MaterialTheme.colorScheme.error) - Text("Are you sure you want to proceed?", fontStyle = androidx.compose.ui.text.font.FontStyle.Italic, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(stringResource(id = R.string.cannot_be_undone_warning), fontWeight = FontWeight.ExtraBold, color = MaterialTheme.colorScheme.error) + Text(stringResource(id = R.string.proceed_confirmation_question), fontStyle = androidx.compose.ui.text.font.FontStyle.Italic, color = MaterialTheme.colorScheme.onSurfaceVariant) } } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SettingsScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SettingsScreen.kt index 6e6b6fc..dbd7881 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SettingsScreen.kt @@ -1,8 +1,12 @@ package swiss.qpq.gajumobile.ui.screens +import android.app.LocaleManager +import android.os.LocaleList import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -30,7 +34,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext 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.TextAlign import androidx.compose.ui.unit.dp @@ -39,12 +45,18 @@ import swiss.qpq.gajumobile.R import swiss.qpq.gajumobile.ui.components.GajuTopBar import swiss.qpq.gajumaru.core.formatting.GajuFormat +@OptIn(ExperimentalLayoutApi::class) @Composable fun SettingsScreen( balanceFormat: GajuFormat.Type, onFormatChange: (GajuFormat.Type) -> Unit, onNavigate: (String) -> Unit ) { + val context = LocalContext.current + val localeManager = remember { context.getSystemService(LocaleManager::class.java) } + val currentLocales = localeManager.applicationLocales + val currentLanguage = if (currentLocales.isEmpty) "en" else currentLocales.get(0).language + Scaffold( topBar = { GajuTopBar(onNavigate = onNavigate) }, ) { innerPadding -> @@ -57,7 +69,7 @@ fun SettingsScreen( horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = "Settings", + text = stringResource(R.string.settings_title), color = MaterialTheme.colorScheme.onBackground, style = MaterialTheme.typography.headlineMedium, modifier = Modifier.padding(vertical = 16.dp), @@ -78,7 +90,7 @@ fun SettingsScreen( shape = RoundedCornerShape(8.dp), ) { Text( - text = "DISPLAY PREFERENCES", + text = stringResource(R.string.display_preferences), textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(16.dp), @@ -88,7 +100,7 @@ fun SettingsScreen( Spacer(modifier = Modifier.height(24.dp)) Text( - text = "BALANCE FORMAT", + text = stringResource(R.string.balance_format), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.align(Alignment.Start) @@ -111,6 +123,42 @@ fun SettingsScreen( } } + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.select_language), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.align(Alignment.Start) + ) + + FlowRow( + modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + val languages = listOf( + "en" to R.string.lang_en, + "ja" to R.string.lang_ja, + "de" to R.string.lang_de, + "fr" to R.string.lang_fr, + "it" to R.string.lang_it + ) + languages.forEach { (tag, nameRes) -> + val isSelected = currentLanguage == tag + AssistChip( + onClick = { + localeManager.applicationLocales = LocaleList.forLanguageTags(tag) + }, + label = { Text(stringResource(nameRes)) }, + colors = AssistChipDefaults.assistChipColors( + containerColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant, + labelColor = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + } + } + Spacer(modifier = Modifier.weight(1f)) var showWipeConfirm by remember { mutableStateOf(false) } @@ -126,12 +174,12 @@ fun SettingsScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - "WIPE ALL DATA?", + stringResource(R.string.wipe_data_confirm_title), fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onErrorContainer ) Text( - "This will permanently delete all wallets, accounts, and keys. This action cannot be undone.", + stringResource(R.string.wipe_data_confirm_message), style = MaterialTheme.typography.bodySmall, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onErrorContainer, @@ -145,13 +193,13 @@ fun SettingsScreen( onClick = { showWipeConfirm = false }, colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.onErrorContainer) ) { - Text("CANCEL") + Text(stringResource(R.string.cancel)) } androidx.compose.material3.Button( onClick = { onNavigate("WIPE_DATA") }, colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error) ) { - Text("WIPE EVERYTHING") + Text(stringResource(R.string.wipe_everything)) } } } @@ -169,7 +217,7 @@ fun SettingsScreen( modifier = Modifier.size(18.dp) ) Spacer(modifier = Modifier.width(8.dp)) - Text("WIPE ALL DATA") + Text(stringResource(R.string.wipe_all_data)) } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SetupChoiceScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SetupChoiceScreen.kt index 2006674..bac5d56 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SetupChoiceScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SetupChoiceScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -50,7 +51,7 @@ fun SetupChoiceScreen( Spacer(modifier = Modifier.height(48.dp)) GajuButton( - text = "Create Account", + text = stringResource(R.string.create_account_button), onClick = onCreate, containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary, @@ -59,7 +60,7 @@ fun SetupChoiceScreen( Spacer(modifier = Modifier.height(16.dp)) GajuButton( - text = "Recover Account", + text = stringResource(R.string.recover_account_button), onClick = onRecover, containerColor = MaterialTheme.colorScheme.secondary, contentColor = MaterialTheme.colorScheme.onSecondary, @@ -68,7 +69,7 @@ fun SetupChoiceScreen( Spacer(modifier = Modifier.height(24.dp)) Text( - text = "To recover an existing account, you will need your mnemonic phrase.", + text = stringResource(R.string.recover_mnemonic_hint), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp, textAlign = TextAlign.Center, @@ -78,7 +79,7 @@ fun SetupChoiceScreen( if (onBack != null) { Spacer(modifier = Modifier.height(32.dp)) TextButton(onClick = onBack) { - Text("Cancel") + Text(stringResource(R.string.cancel)) } } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SignRequestScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SignRequestScreen.kt index deeaadc..1cedacf 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SignRequestScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SignRequestScreen.kt @@ -1,28 +1,45 @@ package swiss.qpq.gajumobile.ui.screens import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import swiss.qpq.gajumobile.R +import swiss.qpq.gajumobile.data.models.GridsSignRequest +import swiss.qpq.gajumobile.data.models.GridsSignType import swiss.qpq.gajumobile.ui.components.GajuButton import swiss.qpq.gajumobile.ui.components.GajuHeader @Composable fun SignRequestScreen( - type: String, + request: GridsSignRequest, originUrl: String, accountLabel: String, accountId: String, - payload: String, onSign: () -> Unit, onCancel: () -> Unit, ) { @@ -36,7 +53,7 @@ fun SignRequestScreen( .padding(24.dp) ) { GajuButton( - text = "CANCEL", + text = stringResource(R.string.cancel), onClick = onCancel, modifier = Modifier.weight(1f), containerColor = MaterialTheme.colorScheme.secondary, @@ -44,7 +61,7 @@ fun SignRequestScreen( ) Spacer(modifier = Modifier.width(16.dp)) GajuButton( - text = "SIGN", + text = stringResource(R.string.sign_button_caps), onClick = onSign, modifier = Modifier.weight(1f), ) @@ -70,11 +87,11 @@ fun SignRequestScreen( // function name, and args. This would be significantly more robust to abuse than // the current "TX signature request" where the TX internals are opaque. Text( - text = when (type) { - "message" -> "MESSAGE SIGNATURE REQUEST" - "binary" -> "BINARY DATA SIGNATURE REQUEST" - "tx" -> "TRANSACTION SIGNATURE REQUEST" - else -> "SIGNATURE REQUEST" + text = when (request.type) { + GridsSignType.MESSAGE -> stringResource(R.string.msg_sig_req) + GridsSignType.BINARY -> stringResource(R.string.bin_sig_req) + GridsSignType.TX -> stringResource(R.string.tx_sig_req) + else -> stringResource(R.string.generic_sig_req) }, color = MaterialTheme.colorScheme.onBackground, fontSize = 20.sp, @@ -84,7 +101,7 @@ fun SignRequestScreen( ) Text( - "The server at the URL below is requesting you sign the following.", + stringResource(R.string.sig_req_description), fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center @@ -92,22 +109,22 @@ fun SignRequestScreen( Spacer(modifier = Modifier.height(24.dp)) - SectionLabel("SIGNATURE ACCOUNT") + SectionLabel(stringResource(R.string.sig_account_label)) Text(accountLabel, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold) Text(accountId, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace) Spacer(modifier = Modifier.height(16.dp)) - SectionLabel("ORIGINATING URL") + SectionLabel(stringResource(R.string.originating_url_label)) Text(originUrl, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center) Spacer(modifier = Modifier.height(24.dp)) - SectionLabel(when (type) { - "message" -> "MESSAGE" - "binary" -> "BASE-64 DATA" - "tx" -> "TRANSACTION DATA" - else -> "PAYLOAD" + SectionLabel(when (request.type) { + GridsSignType.MESSAGE -> stringResource(R.string.payload_type_message) + GridsSignType.BINARY -> stringResource(R.string.payload_type_base64) + GridsSignType.TX -> stringResource(R.string.payload_type_tx) + else -> stringResource(R.string.payload_type_generic) }) Surface( @@ -116,7 +133,7 @@ fun SignRequestScreen( shape = RoundedCornerShape(8.dp), ) { Text( - text = payload, + text = request.payload, modifier = Modifier.padding(16.dp).verticalScroll(rememberScrollState()), fontSize = 12.sp, fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace, diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/TransactionSuccessScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/TransactionSuccessScreen.kt index 9552a46..518c551 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/TransactionSuccessScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/TransactionSuccessScreen.kt @@ -5,12 +5,13 @@ import androidx.compose.foundation.layout.height import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import swiss.qpq.gajumobile.R import swiss.qpq.gajumobile.ui.components.SuccessLayout @Composable @@ -18,23 +19,23 @@ fun TransactionSuccessScreen( onExit: () -> Unit, ) { SuccessLayout( - title = "SEND", - proceedText = "EXIT", + title = stringResource(R.string.send_title), + proceedText = stringResource(R.string.exit_button_caps), onProceed = onExit, content = { - Text("Transaction Successful!", color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.transaction_success_title), color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.height(32.dp)) - Text("You have successfully sent a payment of", fontSize = 14.sp, color = MaterialTheme.colorScheme.onBackground) + Text(stringResource(R.string.transaction_success_payment_prefix), fontSize = 14.sp, color = MaterialTheme.colorScheme.onBackground) Text("木 22,980.78991", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) - Text("to", fontSize = 14.sp, color = MaterialTheme.colorScheme.onBackground) + Text(stringResource(R.string.transaction_success_to), fontSize = 14.sp, color = MaterialTheme.colorScheme.onBackground) Text("ak_9jzm...", fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground) Spacer(modifier = Modifier.height(32.dp)) - Text("The amount has been subtracted from the DAILY account in your PRIMARY WALLET.", textAlign = TextAlign.Center, fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground) + Text(stringResource(R.string.transaction_success_subtracted_msg), textAlign = TextAlign.Center, fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground) Spacer(modifier = Modifier.height(32.dp)) - Text("You can track this transaction on-chain here.", fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground) - Text("VIEW TRANSACTION ON-CHAIN", color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, fontSize = 12.sp) + Text(stringResource(R.string.transaction_success_track_hint), fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground) + Text(stringResource(R.string.view_transaction_on_chain), color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, fontSize = 12.sp) }, ) } diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties new file mode 100644 index 0000000..92481bb --- /dev/null +++ b/app/src/main/res/resources.properties @@ -0,0 +1 @@ +unqualifiedResLocale=en diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 0000000..5207932 --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,249 @@ + + GajuMobile + Einstellungen + ANZEIGEEINSTELLUNGEN + SALDOFORMAT + ALLE DATEN LÖSCHEN? + Dies wird alle Wallets, Konten und Schlüssel dauerhaft löschen. Diese Aktion kann nicht rückgängig gemacht werden. + ABBRECHEN + ALLES LÖSCHEN + ALLE DATEN LÖSCHEN + SPRACHE WÄHLEN + + English + 日本語 (Japanisch) + Deutsch + Français (Französisch) + Italiano (Italienisch) + + + Wallets verwalten + Transaktionen + Einstellungen + Umgebungsinformationen + ZUM ANZEIGEN TIPPEN + + NÄCHSTES WORT + BEENDEN + Zum Dashboard weiterleiten + + Konto konnte nicht erstellt werden: %1$s + Konto konnte nicht wiederhergestellt werden: %1$s + Aktualisierung fehlgeschlagen. Prüfen Sie die Netzwerk-Endpunkte. + Endpunkt verifiziert und hinzugefügt. + Authentifizierung fehlgeschlagen: %1$s + Transaktion gepostet! Hash: %1$s + Senden fehlgeschlagen: %1$s + Signatur erfolgreich übermittelt. + Signatur fehlgeschlagen: %1$s + Kein aktives Konto ausgewählt. + Kein passendes Konto für Signatur gefunden. + Signaturanfrage konnte nicht abgerufen werden. + Abruf fehlgeschlagen: %1$s + Ungültige GRIDS-URL: %1$s + Netzwerk-Abweichung: Knoten meldet \'%1$s\'. Zu Wallet \'%2$s\' hinzugefügt. + KRITISCHE ABWEICHUNG: Knoten meldet \'%1$s\', aber Wallet ist \'%2$s\'. Bitte erstellen Sie zuerst eine Wallet für \'%1$s\'. + Netzwerk-Abweichung: Knoten meldet \'%1$s\'. Zu Wallet \'%2$s\' verschoben. + KRITISCHE ABWEICHUNG: Knoten meldet \'%1$s\', aber Wallet ist für \'%2$s\' konfiguriert. Korrigieren Sie diesen Endpunkt, sonst wird er aus dieser Wallet entfernt. + + + NACHRICHTEN-SIGNATURANFRAGE + BINÄRDATEN-SIGNATURANFRAGE + TRANSAKTIONS-SIGNATURANFRAGE + SIGNATURANFRAGE + Der Server unter der folgenden URL bittet Sie, Folgendes zu signieren. + SIGNATUR-KONTO + URSPRÜNGLICHE URL + SIGNIEREN + NACHRICHT + BASE-64 DATEN + TRANSAKTIONSDATEN + PAYLOAD + + + Sicherheits-Hardware + Keystore + StrongBox (Sicheres Element) + Hardware-TEE + Authentifizierungs-Erzwingung + OS-Sperre erforderlich + Verschlüsselung + Hardware-generierte IVs + Geräteidentität + Hersteller + Modell + Board + Hardware + System-Build + Android-Version + Sicherheitspatch + Fingerabdruck + App-Version + Thema + Arboreal (Gajumaru) + + + Keine Wallets gefunden. Bitte erstelle eine. + Erstellen + Wenn du eine Wallet erstellt hast, sie aber nicht siehst, ist sie möglicherweise beschädigt. + Beschädigte Wallets löschen + SALDO + AUF %1$s + Saldo aktualisieren + Kontoeinstellungen + Senden + GRIDS + Empfangen + Konto hinzufügen + ID kopieren + + + Wallet erstellen + Eine Wallet ist eine Sammlung von Konten, wie ein Ordner für deine Identitäten. + Wallet-Name + z.B. Privat, Ersparnisse + NETZWERK + Zurück + Weiter + Mainnet + Testnet + + + Transaktionen + Groot wird geladen + Gaju Explorer Inhalt + + + Konto erstellen + Konto wiederherstellen + Um ein bestehendes Konto wiederherzustellen, benötigst du deine mnemonische Phrase. + + + WIEDERHERSTELLEN + KONTO WIEDERHERSTELLEN + Mnemonic-Phrase: + VERIFIZIEREN + BACKUP VERIFIZIEREN + Mnemonic-Phrase stimmt nicht überein. Bitte erneut prüfen. + + + Benenne dein Konto + Konto-ID: + Kontoname + Optional (Standard ist die ID) + Fertigstellen + + + App gesperrt + Bitte authentifiziere dich, um fortzufahren. + Entsperren + + + ANFRAGE WIRD ABRUFEN + Verbindung zu: + + + SENDEN + SENDEN VON + Saldo: %1$s + EMPFÄNGERKONTO + ak_... + BETRAG + 0.0 + PAYLOAD / NACHRICHT + Optionale Nachricht... + DETAILS ANZEIGEN + DETAILS AUSBLENDEN + GAS + GASPREIS + TTL + ZURÜCK + PRÜFEN + + + SENDEN PRÜFEN + SENDEN AN + Ich bestätige, dass die oben genannten Details korrekt sind und ich fortfahren möchte. + SENDEN + Sie senden eine Zahlung von + an + DIESE AKTION KANN NICHT RÜCKGÄNGIG GEMACHT WERDEN. + Sind Sie sicher, dass Sie fortfahren möchten? + + + Transaktion erfolgreich! + Sie haben erfolgreich eine Zahlung gesendet von + an + Der Betrag wurde vom TÄGLICHEN Konto in Ihrer PRIMÄREN WALLET abgezogen. + Sie können diese Transaktion hier on-chain verfolgen. + TRANSAKTION ON-CHAIN ANZEIGEN + Ich verstehe und übernehme alle Risiken. + + + QR SCANNEN + ZUM SCANNEN TIPPEN + MANUELLE EINGABE + QR-Code generieren + QR-Code hochladen + Zurück + + + TRANSFERANFRAGE + EMPFANGEN + EMPFÄNGER + ANGEFORDERTER BETRAG (OPTIONAL) + 0.0 + PAYLOAD / NACHRICHT (OPTIONAL) + Notiz hinzufügen... + BEARBEITEN + FERTIG + GENERIEREN + NACHRICHT + QR-Code + GRIDS URL + URL kopieren + Scannen, um an dieses Konto zu senden + + + Konto-ID + GRIDS-URL + + + Konto verwalten + Konto umbenennen + Neuer Name + SPEICHERN + Mnemonic-Backup verifizieren + Mnemonic-Phrase anzeigen + Dieses Konto löschen + + + Wallets verwalten + Knotenstatus + Fehler: %1$s + SCHLIESSEN + Wallet umbenennen + Endpunkt hinzufügen + Host (IP oder Domain) + Port + TLS verwenden (HTTPS) + HINZUFÜGEN + Wallet löschen + NETZWERK-ENDPUNKTE + Endpunkt entfernen + STATUS PRÜFEN + ENDPUNKT HINZUFÜGEN + %1$d Konto/Konten + Neue Wallet hinzufügen + Fertig + + + %1$s LÖSCHEN + Konten in dieser Wallet + Tägliches Konto + WARNUNG: Das Löschen eines %1$s löscht alle darin enthaltenen Konten. + Um ein gelöschtes Konto wiederherzustellen, benötigen Sie Ihre Mnemonic-Phrase. Verlorene Mnemonic-Phrasen können nicht wiederhergestellt werden.\n\nAlle Gelder in einem verlorenen Konto gehen ebenfalls verloren.\n\nSichern Sie Ihre Mnemonic-Phrasen für jedes Konto, bevor Sie hier Änderungen vornehmen, um sicherzugehen.\n\nDiese Aktion kann nicht rückgängig gemacht werden. + Ich verstehe und übernehme alle Risiken dieser Aktion. Ich möchte fortfahren. + LÖSCHEN\nBESTÄTIGEN + WALLET + KONTO + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000..f0b9f0d --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,249 @@ + + GajuMobile + Paramètres + PRÉFÉRENCES D\'AFFICHAGE + FORMAT DU SOLDE + SUPPRIMER TOUTES LES DONNÉES ? + Cela supprimera définitivement tous les portefeuilles, comptes et clés. Cette action est irréversible. + ANNULER + TOUT SUPPRIMER + SUPPRIMER TOUTES LES DONNÉES + CHOISIR LA LANGUE + + English (Anglais) + 日本語 (Japonais) + Deutsch (Allemand) + Français + Italiano (Italien) + + + Gérer les portefeuilles + Transactions + Paramètres + Informations sur l\'environnement + APPUYER POUR RÉVÉLER + + MOT SUIVANT + QUITTER + Accéder au tableau de bord + + Échec de la génération du compte : %1$s + Échec de la récupération du compte : %1$s + Échec de l\'actualisation. Vérifiez les points de terminaison du réseau. + Point de terminaison vérifié et ajouté. + Échec de l\'authentification : %1$s + Transaction publiée ! Hash : %1$s + Échec de l\'envoi : %1$s + Signature soumise avec succès. + Échec de la signature : %1$s + Aucun compte actif sélectionné. + Aucun compte correspondant trouvé pour la signature. + Échec de la récupération de la demande de signature. + Échec de la récupération : %1$s + URL GRIDS invalide : %1$s + Incohérence du réseau : le nœud signale \'%1$s\'. Ajouté au portefeuille \'%2$s\'. + INCOHÉRENCE CRITIQUE : le nœud signale \'%1$s\' mais le portefeuille est \'%2$s\'. Veuillez d\'abord créer un portefeuille pour \'%1$s\'. + Incohérence du réseau : le nœud signale \'%1$s\'. Déplacé vers le portefeuille \'%2$s\'. + INCOHÉRENCE CRITIQUE : le nœud signale \'%1$s\' mais le portefeuille est configuré pour \'%2$s\'. Corrigez ce point de terminaison ou il sera supprimé de ce portefeuille. + + + DEMANDE DE SIGNATURE DE MESSAGE + DEMANDE DE SIGNATURE DE DONNÉES BINAIRES + DEMANDE DE SIGNATURE DE TRANSACTION + DEMANDE DE SIGNATURE + Le serveur à l\'URL ci-dessous demande que vous signiez ce qui suit. + COMPTE DE SIGNATURE + URL D\'ORIGINE + SIGNER + MESSAGE + DONNÉES BASE-64 + DONNÉES DE TRANSACTION + PAYLOAD + + + Matériel de sécurité + Keystore + StrongBox (élément sécurisé) + Hardware TEE + Application de l\'authentification + Verrouillage de l\'OS requis + Chiffrement + IV générés par le matériel + Identité de l\'appareil + Fabricant + Modèle + Carte + Matériel + Build du système + Version Android + Correctif de sécurité + Empreinte digitale + Version de l\'application + Thème + Arboreal (Gajumaru) + + + Aucun portefeuille trouvé. Veuillez en créer un. + Créer + Si vous avez créé un portefeuille mais qu\'il n\'apparaît pas, il est peut-être corrompu. + Effacer les portefeuilles corrompus + SOLDE + SUR %1$s + Actualiser le solde + Paramètres du compte + Envoyer + GRIDS + Recevoir + Ajouter un compte + Copier l\'ID + + + Créer un portefeuille + Un portefeuille est une collection de comptes, comme un dossier pour vos identités. + Nom du portefeuille + ex. Personnel, Épargne + RÉSEAU + Retour + Continuer + Mainnet + Testnet + + + Transactions + Chargement de Groot + Contenu de l\'explorateur Gaju + + + Créer un compte + Récupérer un compte + Pour récupérer un compte existant, vous aurez besoin de votre phrase mnémonique. + + + RÉCUPÉRER + RÉCUPÉRER LE COMPTE + Phrase mnémonique : + VÉRIFIER + VÉRIFIER LA SAUVEGARDE + La phrase mnémonique ne correspond pas. Veuillez vérifier à nouveau. + + + Nommez votre compte + ID du compte : + Nom du compte + Optionnel (par défaut l\'ID) + Terminer + + + Application verrouillée + Veuillez vous authentifier pour continuer. + Déverrouiller + + + RÉCUPÉRATION DE LA REQUÊTE + Connexion à : + + + ENVOYER + ENVOI DEPUIS + Solde : %1$s + COMPTE DESTINATAIRE + ak_... + MONTANT + 0.0 + PAYLOAD / MESSAGE + Message optionnel... + AFFICHER LES DÉTAILS + MASQUER LES DÉTAILS + GAZ + PRIX DU GAZ + TTL + RETOUR + VÉRIFIER + + + VÉRIFIER L\'ENVOI + ENVOI À + Je confirme que les informations ci-dessus sont correctes et je souhaite continuer. + ENVOYER + Vous envoyez un paiement de + à + CETTE ACTION EST IRRÉVERSIBLE. + Êtes-vous sûr de vouloir continuer ? + + + Transaction réussie ! + Vous avez envoyé avec succès un paiement de + à + Le montant a été déduit du compte QUOTIDIEN de votre PORTEFEUILLE PRINCIPAL. + Vous pouvez suivre cette transaction sur la chaîne ici. + VOIR LA TRANSACTION SUR LA CHAÎNE + Je comprends et assume pleinement les risques. + + + SCANNER QR + APPUYER POUR SCANNER + SAISIE MANUELLE + Générer QR + Télécharger QR + Retour + + + DEMANDE DE TRANSFERT + RECEVOIR + DESTINATAIRE + MONTANT DEMANDÉ (OPTIONNEL) + 0.0 + PAYLOAD / MESSAGE (OPTIONNEL) + Ajouter une note... + MODIFIER + TERMINÉ + GÉNÉRER + MESSAGE + Code QR + URL GRIDS + Copier l\'URL + Scannez pour envoyer à ce compte + + + ID du compte + URL GRIDS + + + Gérer le compte + Renommer le compte + Nouveau nom + ENREGISTRER + Vérifier la sauvegarde mnémonique + Voir la phrase mnémonique + Supprimer ce compte + + + Gérer les portefeuilles + État du nœud + Erreur : %1$s + FERMER + Renommer le portefeuille + Ajouter un point de terminaison + Hôte (IP ou domaine) + Port + Utiliser TLS (HTTPS) + AJOUTER + Supprimer le portefeuille + POINTS DE TERMINAISON RÉSEAU + Supprimer le point de terminaison + VÉRIFIER L\'ÉTAT + AJOUTER UN POINT DE TERMINAISON + %1$d compte(s) + Ajouter un nouveau portefeuille + Terminé + + + SUPPRIMER %1$s + Comptes dans ce portefeuille + Compte quotidien + AVERTISSEMENT : La suppression d\'un %1$s supprimera tous les comptes qu\'il contient. + Pour récupérer un compte supprimé, vous aurez besoin de votre phrase mnémonique. Les phrases mnémoniques perdues ne peuvent pas être récupérées.\n\nTous les fonds dans un compte perdu sont également perdus.\n\nSauvegardez vos phrases mnémoniques pour chaque compte avant d\'apporter des modifications ici pour plus de sécurité.\n\nCette action ne peut pas être annulée. + Je comprends et assume tous les risques de cette action. Je souhaite continuer. + CONFIRMER LA\nSUPPRESSION + PORTEFEUILLE + COMPTE + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml new file mode 100644 index 0000000..2192e29 --- /dev/null +++ b/app/src/main/res/values-it/strings.xml @@ -0,0 +1,249 @@ + + GajuMobile + Impostazioni + PREFERENZE DI VISUALIZZAZIONE + FORMATO DEL SALDO + CANCELLARE TUTTI I DATI? + Questa azione eliminerà permanentemente tutti i portafogli, account e chiavi. L\'operazione non può essere annullata. + ANNULLA + CANCELLA TUTTO + CANCELLA TUTTI I DATI + SELEZIONA LINGUA + + English (Inglese) + 日本語 (Giapponese) + Deutsch (Tedesco) + Français (Francese) + Italiano + + + Gestisci portafogli + Transazioni + Impostazioni + Informazioni sull\'ambiente + TOCCA PER RIVELARE + + PROSSIMA PAROLA + ESCI + Procedi alla dashboard + + Impossibile generare l\'account: %1$s + Impossibile recuperare l\'account: %1$s + Aggiornamento fallito. Controlla gli endpoint di rete. + Endpoint verificato e aggiunto. + Autenticazione fallita: %1$s + Transazione inviata! Hash: %1$s + Invio fallito: %1$s + Firma inviata con successo. + Firma fallita: %1$s + Nessun account attivo selezionato. + Nessun account corrispondente trovato per la firma. + Impossibile recuperare la richiesta di firma. + Recupero fallito: %1$s + URL GRIDS non valido: %1$s + Discrepanza di rete: il nodo segnala \'%1$s\'. Aggiunto al portafoglio \'%2$s\'. + DISCREPANZA CRITICA: il nodo segnala \'%1$s\' ma il portafoglio è \'%2$s\'. Crea prima un portafoglio per \'%1$s\'. + Discrepanza di rete: il nodo segnala \'%1$s\'. Spostato nel portafoglio \'%2$s\'. + DISCREPANZA CRITICA: il nodo segnala \'%1$s\' ma il portafoglio è configurato per \'%2$s\'. Correggi questo endpoint o verrà rimosso da questo portafoglio. + + + RICHIESTA DI FIRMA DEL MESSAGGIO + RICHIESTA DI FIRMA DATI BINARI + RICHIESTA DI FIRMA TRANSAZIONE + RICHIESTA DI FIRMA + Il server all\'URL sottostante ti sta chiedendo di firmare quanto segue. + ACCOUNT DI FIRMA + URL DI ORIGINE + FIRMA + MESSAGGIO + DATI BASE-64 + DATI TRANSAZIONE + PAYLOAD + + + Hardware di sicurezza + Keystore + StrongBox (elemento sicuro) + TEE hardware + Applicazione autenticazione + Blocco OS richiesto + Crittografia + IV generati dall\'hardware + Identità dispositivo + Produttore + Modello + Scheda + Hardware + Build di sistema + Versione Android + Patch di sicurezza + Impronta digitale + Versione app + Tema + Arboreal (Gajumaru) + + + Nessun portafoglio trovato. Creane uno. + Crea + Se hai creato un portafoglio ma non lo vedi, potrebbe essere danneggiato. + Cancella i portafogli danneggiati + SALDO + SU %1$s + Aggiorna saldo + Impostazioni account + Invia + GRIDS + Ricevi + Aggiungi account + Copia ID + + + Crea portafoglio + Un portafoglio è una raccolta di account, come una cartella per le tue identità. + Nome portafoglio + es. Personale, Risparmi + RETE + Indietro + Continua + Mainnet + Testnet + + + Transazioni + Caricamento di Groot + Contenuto dell\'explorer Gaju + + + Crea account + Recupera account + Per recuperare un account esistente, avrai bisogno della tua frase mnemonica. + + + RECUPERA + RECUPERA ACCOUNT + Frase mnemonica: + VERIFICA + VERIFICA BACKUP + La frase mnemonica non corrisponde. Controlla di nuovo. + + + Dai un nome al tuo account + ID account: + Nome account + Opzionale (predefinito è ID) + Fine + + + App bloccata + Autenticati per continuare. + Sblocca + + + RECUPERO DELLA RICHIESTA + Connessione a: + + + INVIA + INVIO DA + Saldo: %1$s + ACCOUNT DESTINATARIO + ak_... + IMPORTO + 0.0 + PAYLOAD / MESSAGGIO + Messaggio opzionale... + MOSTRA DETTAGLI + NASCONDI DETTAGLI + GAS + PREZZO GAS + TTL + INDIETRO + RIVEDI + + + RIVEDI INVIO + INVIO A + Confermo che i dettagli sopra riportati sono corretti e desidero procedere. + INVIA + Stai inviando un pagamento di + a + QUESTA AZIONE NON PUÒ ESSERE ANNULLATA. + Sei sicuro di voler procedere? + + + Transazione riuscita! + Hai inviato con successo un pagamento di + a + L\'importo è stato detratto dal conto GIORNALIERO nel tuo PORTAFOGLIO PRINCIPALE. + Puoi tracciare questa transazione on-chain qui. + VISUALIZZA TRANSAZIONE ON-CHAIN + Comprendo e mi assumo pienamente i rischi. + + + SCANSIONA QR + TOCCA PER SCANSIONARE + INSERIMENTO MANUALE + Genera QR + Carica QR + Indietro + + + RICHIESTA DI TRASFERIMENTO + RICEVI + DESTINATARIO + IMPORTO RICHIESTO (OPZIONALE) + 0.0 + PAYLOAD / MESSAGGIO (OPZIONALE) + Aggiungi una nota... + MODIFICA + FATTO + GENERA + MESSAGGIO + Codice QR + URL GRIDS + Copia URL + Scansiona per inviare a questo account + + + ID account + URL GRIDS + + + Gestisci account + Rinomina account + Nuovo nome + SALVA + Verifica backup mnemonico + Visualizza frase mnemonica + Elimina questo account + + + Gestisci portafogli + Stato del nodo + Errore: %1$s + CHIUDI + Rinomina portafoglio + Aggiungi endpoint + Host (IP o dominio) + Porta + Usa TLS (HTTPS) + AGGIUNGI + Elimina portafoglio + ENDPOINT DI RETE + Rimuovi endpoint + CONTROLLA STATO + AGGIUNGI ENDPOINT + %1$d account + Aggiungi nuovo portafoglio + Fatto + + + ELIMINA %1$s + Account in questo portafoglio + Account giornaliero + ATTENZIONE: L\'eliminazione di un %1$s eliminerà tutti gli account in esso contenuti. + Per recuperare un account eliminato, avrai bisogno della tua frase mnemonica. Le frasi mnemoniche perse non possono essere recuperate.\n\nTutti i fondi in un account perso vanno anch\'essi persi.\n\nEsegui il backup delle tue frasi mnemoniche per ogni account prima di apportare modifiche qui per sicurezza.\n\nQuesta azione non può essere annullata. + Comprendo e mi assumo tutti i rischi di questa azione. Desidero procedere. + CONFERMA\nELIMINA + PORTAFOGLIO + ACCOUNT + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000..eda7b7a --- /dev/null +++ b/app/src/main/res/values-ja/strings.xml @@ -0,0 +1,249 @@ + + GajuMobile + 設定 + 表示設定 + 残高表示形式 + 全データを消去しますか? + すべてのウォレット、アカウント、およびキーが永久に削除されます。この操作は取り消せません。 + キャンセル + すべて消去 + 全データを消去 + 言語を選択 + + English (英語) + 日本語 + Deutsch (ドイツ語) + Français (フランス語) + Italiano (イタリア語) + + + ウォレットを管理 + トランザクション + 設定 + 環境情報 + タップして表示 + + 次の単語 + 終了 + ダッシュボードへ進む + + アカウントの生成に失敗しました: %1$s + アカウントの復元に失敗しました: %1$s + 更新に失敗しました。ネットワークエンドポイントを確認してください。 + エンドポイントが確認され、追加されました。 + 認証に失敗しました: %1$s + トランザクションが送信されました! ハッシュ: %1$s + 送信に失敗しました: %1$s + 署名が正常に送信されました。 + 署名に失敗しました: %1$s + 有効なアカウントが選択されていません。 + 署名に一致するアカウントが見つかりません。 + 署名リクエストの取得に失敗しました。 + 取得に失敗しました: %1$s + 無効なGRIDS URLです: %1$s + ネットワークの不一致: ノードが \'%1$s\' を報告しています。ウォレット \'%2$s\' に追加されました。 + 致命的な不一致: ノードが \'%1$s\' を報告していますが、ウォレットは \'%2$s\' です。まず \'%1$s\' のウォレットを作成してください。 + ネットワークの不一致: ノードが \'%1$s\' を報告しています。ウォレット \'%2$s\' に移動されました。 + 致命的な不一致: ノードが \'%1$s\' を報告していますが、ウォレットは \'%2$s\' 用に設定されています。このエンドポイントを修正してください。修正されない場合は、このウォレットから破棄されます。 + + + メッセージ署名リクエスト + バイナリデータ署名リクエスト + トランザクション署名リクエスト + 署名リクエスト + 以下のURLのサーバーが署名を要求しています。 + 署名アカウント + リクエスト元URL + 署名 + メッセージ + BASE-64 データ + トランザクションデータ + ペイロード + + + セキュリティハードウェア + キーストア + StrongBox (セキュアエレメント) + ハードウェア TEE + 認証の強制 + OSロック必須 + 暗号化 + ハードウェア生成のIV + デバイスID + メーカー + モデル + ボード + ハードウェア + システムビルド + Android バージョン + セキュリティパッチ + フィンガープリント + アプリバージョン + テーマ + Arboreal (Gajumaru) + + + ウォレットが見つかりません。作成してください。 + 作成 + ウォレットを作成したのに表示されない場合は、データが破損している可能性があります。 + 破損したウォレットを消去 + 残高 + %1$s ネットワーク上 + 残高を更新 + アカウント設定 + 送信 + GRIDS + 受信 + アカウントを追加 + IDをコピー + + + ウォレットを作成 + ウォレットは、IDを整理するためのフォルダのような、アカウントの集合体です。 + ウォレット名 + 例:個人用、貯蓄用 + ネットワーク + 戻る + 次へ + メインネット + テストネット + + + トランザクション + Grootを読み込み中 + Gajuエクスプローラーのコンテンツ + + + アカウントを作成 + アカウントを復元 + 既存のアカウントを復元するには、ニーモニックフレーズが必要です。 + + + 復元 + アカウントを復元 + ニーモニックフレーズ: + 確認 + バックアップを確認 + ニーモニックフレーズが一致しません。もう一度確認してください。 + + + アカウントに名前を付ける + アカウントID: + アカウント名 + オプション(デフォルトはID) + 完了 + + + アプリがロックされています + 続行するには認証してください。 + ロック解除 + + + リクエストを取得中 + 接続先: + + + 送信 + 送信元 + 残高: %1$s + 送信先アカウント + ak_... + 金額 + 0.0 + ペイロード / メッセージ + メッセージ(任意)... + 詳細を表示 + 詳細を隠す + ガス + ガス価格 + TTL + 戻る + 確認 + + + 送信内容を確認 + 送信先 + 上記の詳細が正しいことを確認し、続行します。 + 送信 + 送金額 + 送信先 + この操作は取り消せません。 + 本当に続行しますか? + + + トランザクション成功! + 以下の送金に成功しました + 送信先 + 金額はプライマリウォレットの日常アカウントから差し引かれました。 + ここでチェーン上のトランザクションを追跡できます。 + チェーン上でトランザクションを表示 + 私はリスクを理解し、完全に引き受けます。 + + + QRコードをスキャン + タップしてスキャン + 手動入力 + QRコードを生成 + QRコードをアップロード + 戻る + + + 送金リクエスト + 受信 + 受信者 + リクエスト金額(任意) + 0.0 + ペイロード / メッセージ(任意) + メモを追加... + 編集 + 完了 + 生成 + メッセージ + QRコード + GRIDS URL + URLをコピー + スキャンしてこのアカウントに送信します + + + アカウントID + GRIDS URL + + + アカウントを管理 + アカウントの名前を変更 + 新しい名前 + 保存 + ニーモニックのバックアップを確認 + ニーモニックフレーズを表示 + このアカウントを削除 + + + ウォレットを管理 + ノードの状態 + エラー: %1$s + 閉じる + ウォレットの名前を変更 + エンドポイントを追加 + ホスト (IP またはドメイン) + ポート + TLS (HTTPS) を使用 + 追加 + ウォレットを削除 + ネットワークエンドポイント + エンドポイントを削除 + 状態を確認 + エンドポイントを追加 + %1$d 個のアカウント + 新しいウォレットを追加 + 完了 + + + %1$s を削除 + このウォレット内のアカウント + 日常アカウント + 警告: %1$s を削除すると、その中のすべてのアカウントが削除されます。 + 削除されたアカウントを復元するには、ニーモニックフレーズが必要です。失われたニーモニックフレーズは取得できません。\n\n失われたアカウント内のすべての資金も失われます。\n\n安全のため、変更を加える前にすべてのアカウントのニーモニックフレーズをバックアップしてください。\n\nこの操作は取り消せません。 + 私はこの操作によるすべてのリスクを理解し、引き受けます。続行します。 + 削除を確定 + ウォレット + アカウント + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 109edc7..9c2267c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,249 @@ GajuMobile - \ No newline at end of file + Settings + DISPLAY PREFERENCES + BALANCE FORMAT + WIPE ALL DATA? + This will permanently delete all wallets, accounts, and keys. This action cannot be undone. + CANCEL + WIPE EVERYTHING + WIPE ALL DATA + SELECT LANGUAGE + + Failed to generate account: %1$s + Failed to recover account: %1$s + Refresh failed. Check network endpoints. + Endpoint verified and added. + Authentication failed: %1$s + Transaction posted! Hash: %1$s + Send failed: %1$s + Signature submitted successfully. + Signing failed: %1$s + No active account selected. + No matching account found for signature. + Failed to fetch signature request. + Fetch failed: %1$s + Invalid GRIDS URL: %1$s + Network mismatch: Node reports \'%1$s\'. Added to wallet \'%2$s\'. + CRITICAL MISMATCH: Node reports \'%1$s\' but wallet is \'%2$s\'. Please create a wallet for \'%1$s\' first. + Network mismatch: Node reports \'%1$s\'. Moved to wallet \'%2$s\'. + CRITICAL MISMATCH: Node reports \'%1$s\' but wallet is configured for \'%2$s\'. Fix this endpoint or it will be discarded from this wallet. + + English + 日本語 (Japanese) + Deutsch (German) + Français (French) + Italiano (Italian) + + + Manage Wallets + Transactions + Settings + Environment Info + TAP TO REVEAL + + NEXT WORD + EXIT + Proceed to Dashboard + + + MESSAGE SIGNATURE REQUEST + BINARY DATA SIGNATURE REQUEST + TRANSACTION SIGNATURE REQUEST + SIGNATURE REQUEST + The server at the URL below is requesting you sign the following. + SIGNATURE ACCOUNT + ORIGINATING URL + SIGN + MESSAGE + BASE-64 DATA + TRANSACTION DATA + PAYLOAD + + + Security Hardware + Keystore + StrongBox (Secure Element) + Hardware TEE + Auth Enforcement + OS Lock Required + Encryption + Hardware-Generated IVs + Device Identity + Manufacturer + Model + Board + Hardware + System Build + Android Version + Security Patch + Fingerprint + App Version + Theme + Arboreal (Gajumaru) + + + No wallets found. Please create one. + Create + If you created a wallet but don\'t see it, it may be corrupted. + Wipe Broken Wallets + BALANCE + ON %1$s + Refresh Balance + Account Settings + Send + GRIDS + Receive + Add Account + Copy ID + + + Create Wallet + A wallet is a collection of accounts, like a folder for your identities. + Wallet Name + e.g. Personal, Savings + NETWORK + Back + Continue + Mainnet + Testnet + + + Transactions + Loading Groot + Gaju Explorer Content + + + Create Account + Recover Account + To recover an existing account, you will need your mnemonic phrase. + + + RECOVER + RECOVER ACCOUNT + Mnemonic Phrase: + VERIFY + VERIFY BACKUP + Mnemonic phrase does not match. Please check again. + + + Name your account + Account ID: + Account Name + Optional (defaults to ID) + Finish + + + App Locked + Please authenticate to continue. + Unlock + + + RETRIEVING REQUEST + Connecting to: + + + SEND + SENDING FROM + Balance: %1$s + RECIPIENT ACCOUNT + ak_... + AMOUNT + 0.0 + PAYLOAD / MESSAGE + Optional message... + SHOW DETAILS + HIDE DETAILS + GAS + GAS PRICE + TTL + BACK + REVIEW + + + REVIEW SEND + SENDING TO + I confirm that the above details are correct and I would like to proceed. + SEND + You are sending a payment of + to + THIS ACTION CANNOT BE UNDONE. + Are you sure you want to proceed? + + + Transaction Successful! + You have successfully sent a payment of + to + The amount has been subtracted from the DAILY account in your PRIMARY WALLET. + You can track this transaction on-chain here. + VIEW TRANSACTION ON-CHAIN + I understand and fully assume the risks. + + + SCAN QR + TAP TO SCAN + MANUAL ENTRY + Generate QR + Upload QR + Back + + + TRANSFER REQUEST + RECEIVE + RECIPIENT + REQUESTED AMOUNT (OPTIONAL) + 0.0 + PAYLOAD / MESSAGE (OPTIONAL) + Add a note... + EDIT + DONE + GENERATE + MESSAGE + QR Code + GRIDS URL + Copy URL + Scan to send to this account + + + Account ID + GRIDS URL + + + Manage Account + Rename Account + New Name + SAVE + Verify mnemonic backup + View mnemonic phrase + Delete this account + + + Manage Wallets + Node Status + Error: %1$s + CLOSE + Rename Wallet + Add Endpoint + Host (IP or Domain) + Port + Use TLS (HTTPS) + ADD + Delete Wallet + NETWORK ENDPOINTS + Remove Endpoint + CHECK STATUS + ADD ENDPOINT + %1$d account(s) + Add new wallet + Done + + + DELETE %1$s + Accounts within this wallet + Daily account + WARNING: Deleting a %1$s will delete all accounts within it. + To recover a deleted account, you will need your mnemonic phrase. Lost mnemonic phrases cannot be retrieved.\n\nAll funds within a lost account are also lost.\n\nBack up your mnemonic phrases for every account before making any changes here to be safe.\n\nThis action cannot be undone. + I understand and assume all risks from this action. I would like to proceed. + CONFIRM\nDELETE + WALLET + ACCOUNT + diff --git a/app/src/test/java/swiss/qpq/gajumobile/data/models/GridsModelsTest.kt b/app/src/test/java/swiss/qpq/gajumobile/data/models/GridsModelsTest.kt new file mode 100644 index 0000000..84597e3 --- /dev/null +++ b/app/src/test/java/swiss/qpq/gajumobile/data/models/GridsModelsTest.kt @@ -0,0 +1,49 @@ +package swiss.qpq.gajumobile.data.models + +import org.junit.Assert.assertEquals +import org.junit.Test + +class GridsModelsTest { + + @Test + fun testMapToGridsSignRequest() { + val rawMap = mapOf( + "type" to "message", + "payload" to "Hello world", + "public_id" to "ak_123", + "network_id" to "groot.mainnet", + "grids" to "v1", + "chain" to "groot" + ) + + val request = rawMap.toGridsSignRequest() + + assertEquals(GridsSignType.MESSAGE, request.type) + assertEquals("Hello world", request.payload) + assertEquals("ak_123", request.publicId) + assertEquals("groot.mainnet", request.networkId) + assertEquals("v1", request.grids) + assertEquals("groot", request.chain) + } + + @Test + fun testMapWithUnknownType() { + val rawMap = mapOf( + "type" to "something_new", + "payload" to "secret" + ) + + val request = rawMap.toGridsSignRequest() + + assertEquals(GridsSignType.UNKNOWN, request.type) + assertEquals("secret", request.payload) + } + + @Test + fun testToProtocolString() { + assertEquals("message", GridsSignType.MESSAGE.toProtocolString()) + assertEquals("binary", GridsSignType.BINARY.toProtocolString()) + assertEquals("tx", GridsSignType.TX.toProtocolString()) + assertEquals("unknown", GridsSignType.UNKNOWN.toProtocolString()) + } +} diff --git a/app/src/test/java/swiss/qpq/gajumobile/security/AccountAirlockTest.kt b/app/src/test/java/swiss/qpq/gajumobile/security/AccountAirlockTest.kt index dcc7a15..f2f28e0 100644 --- a/app/src/test/java/swiss/qpq/gajumobile/security/AccountAirlockTest.kt +++ b/app/src/test/java/swiss/qpq/gajumobile/security/AccountAirlockTest.kt @@ -60,7 +60,7 @@ class AccountAirlockTest { // Verify we can sign with this account val message = "Test sign".toByteArray() - val signature = AccountAirlock.sign(account.privateKeyEnvelope, masterKey, message) + val signature = AccountAirlock.signBinary(account.privateKeyEnvelope, masterKey, message) assertNotNull(signature) assertTrue(Ed25519.verify(account.publicKey, message, signature))