From 59d4ac0e81e44b8304f95c31a9c7006d460a8b3b Mon Sep 17 00:00:00 2001 From: jjangddu Date: Mon, 29 Jun 2026 09:17:23 +0900 Subject: [PATCH] =?UTF-8?q?feat(locale):=20Phase=203=20=E2=80=94=20?= =?UTF-8?q?=EC=A0=84=EC=88=98=20sweep,=20=EB=85=B8=EC=9D=B8=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EA=B8=B0=EC=A4=80=20=EC=98=81=EC=96=B4=20?= =?UTF-8?q?=EC=9E=94=EC=97=AC=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=ED=95=9C?= =?UTF-8?q?=EA=B8=80=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이전 Phase 2 에서 'already English-only' 라며 skip 했던 화면들 — 노인 사용자에게는 영어로 보이면 안 됨. 전체 다시 훑어 모든 user-visible 영어 hardcoded text 를 strings.xml 로 추출. 11 파일 수정: - MainActivity (Exit dialog) - HomeView (Smart Bladder Monitoring / Start / Connected / Clinical Mode / dev toast) - OnboardingView (Welcome, Get Started 등) - CalibrationView (모든 status + Retry/Skip/Continue) - SensorSelectView (title, descriptions) - ShuffledKeypad (Delete contentDescription) - MeasurementHistoryView (title, tabs, empty state, Today/Yesterday) - VivamyoMonitoringView (전체 status) - PiezoMonitoringView (사용자 영역 — Auto/Single Scan, Voiding/Catheterization, 측정 카드, dialog 등) - PlacementGuideView (V1 hints, Step indicator, Start/Skip/Complete Alignment, In position/Position lost) 추출 strings: ~170 (각 언어). strings.xml 194 → 365 라인. skip 유지: - ClinicalHomeView / ClinicalLiveView (R&D dev mode) - PiezoMonitoring dev panel sliders (3-tap 활성화 dev 도구) - PlacementGuide dev settings overlay + Echo Waveforms (isDevMode gated) - BLE Debug Log / Scan Log dialog (dev 전용) - 브랜드명 (VesiScan-Basic, Vivamyo, Bladdy) - DEMO 배지, IMU debug 문자열, Log.d / debugLogger 출력 Korean coverage: ~95% (일반 사용자가 절대 보지 않는 dev surface 만 영어 유지). Co-Authored-By: Claude Opus 4.7 --- .../medilightv2android/MainActivity.kt | 10 +- .../ui/views/calibration/CalibrationView.kt | 35 ++-- .../views/history/MeasurementHistoryView.kt | 55 +++--- .../ui/views/home/HomeView.kt | 13 +- .../views/monitoring/PiezoMonitoringView.kt | 109 +++++------ .../ui/views/monitoring/PlacementGuideView.kt | 161 +++++++++-------- .../views/monitoring/VivamyoMonitoringView.kt | 40 ++-- .../ui/views/onboarding/OnboardingView.kt | 8 +- .../ui/views/pin/ShuffledKeypad.kt | 4 +- .../ui/views/sensorselect/SensorSelectView.kt | 10 +- app/src/main/res/values-ko/strings.xml | 171 ++++++++++++++++++ app/src/main/res/values/strings.xml | 171 ++++++++++++++++++ 12 files changed, 587 insertions(+), 200 deletions(-) diff --git a/app/src/main/java/com/example/medilightv2android/MainActivity.kt b/app/src/main/java/com/example/medilightv2android/MainActivity.kt index 6f9bb0d..8ef93be 100644 --- a/app/src/main/java/com/example/medilightv2android/MainActivity.kt +++ b/app/src/main/java/com/example/medilightv2android/MainActivity.kt @@ -18,9 +18,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import app.rive.runtime.kotlin.core.Rive import com.example.medilightv2android.services.KeychainService import com.example.medilightv2android.services.NotificationService +import com.example.medilightv2android.R import com.example.medilightv2android.ui.theme.Medilightv2androidTheme import com.example.medilightv2android.ui.views.calibration.CalibrationView import com.example.medilightv2android.ui.views.connection.DeviceScanView @@ -81,16 +83,16 @@ fun MedilightApp(modifier: Modifier = Modifier) { if (showExitDialog) { androidx.compose.material3.AlertDialog( onDismissRequest = { showExitDialog = false }, - title = { androidx.compose.material3.Text("Exit App") }, - text = { androidx.compose.material3.Text("Are you sure you want to exit?") }, + title = { androidx.compose.material3.Text(stringResource(R.string.exit_app_title)) }, + text = { androidx.compose.material3.Text(stringResource(R.string.exit_app_message)) }, confirmButton = { androidx.compose.material3.TextButton(onClick = { (context as? android.app.Activity)?.finish() }) { - androidx.compose.material3.Text("Exit", color = Color.Red) + androidx.compose.material3.Text(stringResource(R.string.exit_app_confirm), color = Color.Red) } }, dismissButton = { androidx.compose.material3.TextButton(onClick = { showExitDialog = false }) { - androidx.compose.material3.Text("Cancel") + androidx.compose.material3.Text(stringResource(R.string.cancel)) } } ) diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/calibration/CalibrationView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/calibration/CalibrationView.kt index 916fddc..8825d13 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/calibration/CalibrationView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/calibration/CalibrationView.kt @@ -17,11 +17,13 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +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 com.example.medilightv2android.AppState +import com.example.medilightv2android.R import com.example.medilightv2android.ble.BleManager import com.example.medilightv2android.managers.NirsManager import com.example.medilightv2android.models.NirsMagResponse @@ -35,7 +37,14 @@ fun CalibrationView(appState: AppState) { val nirsManager = remember { NirsManager.instance } val handler = remember { Handler(Looper.getMainLooper()) } - var statusMessage by remember { mutableStateOf("Initializing...") } + val initMsg = stringResource(R.string.calibration_initializing) + val completeMsg = stringResource(R.string.calibration_complete_status) + val noResponseMsg = stringResource(R.string.calibration_no_response) + val poweringMsg = stringResource(R.string.calibration_powering_on) + val activatingMsg = stringResource(R.string.calibration_activating) + val readingGainMsg = stringResource(R.string.calibration_reading_gain) + + var statusMessage by remember { mutableStateOf(initMsg) } var isComplete by remember { mutableStateOf(false) } var isFailed by remember { mutableStateOf(false) } var didReceiveResponse by remember { mutableStateOf(false) } @@ -44,27 +53,27 @@ fun CalibrationView(appState: AppState) { fun completeCalibration() { if (isComplete) return - statusMessage = "NIRS Calibrated" + statusMessage = completeMsg isComplete = true } fun failCalibration() { if (isComplete || isFailed) return - statusMessage = "No response from device. Check the device is powered on and nearby." + statusMessage = noResponseMsg isFailed = true } fun startCalibration() { - statusMessage = "Powering on NIRS transducer..." + statusMessage = poweringMsg bleManager.onNirsPowerOnReceived = { didReceiveResponse = true - statusMessage = "Activating sensor..." + statusMessage = activatingMsg handler.postDelayed({ bleManager.sendMqqQuery() }, 2000) } bleManager.onNirsSensorActivated = { - statusMessage = "Reading gain calibration..." + statusMessage = readingGainMsg handler.postDelayed({ bleManager.sendMagQuery() }, 300) } @@ -87,7 +96,7 @@ fun CalibrationView(appState: AppState) { isFailed = false isComplete = false didReceiveResponse = false - statusMessage = "Initializing..." + statusMessage = initMsg startCalibration() } @@ -124,9 +133,9 @@ fun CalibrationView(appState: AppState) { Text( text = when { - isFailed -> "Calibration Failed" - isComplete -> "Calibration Complete" - else -> "Calibrating NIRS..." + isFailed -> stringResource(R.string.calibration_failed) + isComplete -> stringResource(R.string.calibration_complete) + else -> stringResource(R.string.calibration_in_progress) }, fontSize = 24.sp, fontWeight = FontWeight.Bold, @@ -167,7 +176,7 @@ fun CalibrationView(appState: AppState) { Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Default.Refresh, null, tint = Color.White) Spacer(Modifier.width(8.dp)) - Text("Retry", fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = Color.White) + Text(stringResource(R.string.retry), fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = Color.White) } } } @@ -176,7 +185,7 @@ fun CalibrationView(appState: AppState) { onClick = { appState.calibrationComplete() }, modifier = Modifier.align(Alignment.CenterHorizontally) ) { - Text("Skip", fontSize = 16.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText) + Text(stringResource(R.string.calibration_skip), fontSize = 16.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText) } } } else if (isComplete) { @@ -197,7 +206,7 @@ fun CalibrationView(appState: AppState) { ), contentAlignment = Alignment.Center ) { - Text("Continue", fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = Color.White) + Text(stringResource(R.string.calibration_continue), fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = Color.White) } } } diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/history/MeasurementHistoryView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/history/MeasurementHistoryView.kt index ba32c8d..1852f03 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/history/MeasurementHistoryView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/history/MeasurementHistoryView.kt @@ -27,10 +27,12 @@ import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +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 import com.example.medilightv2android.AppState +import com.example.medilightv2android.R import com.example.medilightv2android.models.* import com.example.medilightv2android.services.MeasurementHistoryStore import com.example.medilightv2android.ui.theme.* @@ -54,17 +56,17 @@ fun MeasurementHistoryView(appState: AppState) { verticalAlignment = Alignment.CenterVertically ) { IconButton(onClick = { appState.backToMonitoring() }) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back", tint = MlPrimary) + Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back), tint = MlPrimary) } - Text("History", fontSize = 20.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(start = 8.dp)) + Text(stringResource(R.string.history_title), fontSize = 20.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(start = 8.dp)) Spacer(modifier = Modifier.weight(1f)) - Text("${records.size} records", fontSize = 13.sp, color = MlSecondaryText) + Text(stringResource(R.string.history_records_count, records.size), fontSize = 13.sp, color = MlSecondaryText) } // Tab picker TabRow(selectedTabIndex = selectedTab, modifier = Modifier.padding(horizontal = 24.dp)) { - Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }) { Text("History", modifier = Modifier.padding(12.dp)) } - Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }) { Text("Trends", modifier = Modifier.padding(12.dp)) } + Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }) { Text(stringResource(R.string.history_tab_history), modifier = Modifier.padding(12.dp)) } + Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }) { Text(stringResource(R.string.history_tab_trends), modifier = Modifier.padding(12.dp)) } } if (records.isEmpty()) { @@ -73,9 +75,9 @@ fun MeasurementHistoryView(appState: AppState) { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - Text("No measurements yet", fontSize = 16.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText) + Text(stringResource(R.string.history_no_measurements), fontSize = 16.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText) Spacer(modifier = Modifier.height(8.dp)) - Text("Tap Spot to start measuring", fontSize = 14.sp, color = MlSecondaryText.copy(alpha = 0.7f)) + Text(stringResource(R.string.history_tap_spot_hint), fontSize = 14.sp, color = MlSecondaryText.copy(alpha = 0.7f)) } } else if (selectedTab == 0) { LazyColumn( @@ -98,7 +100,7 @@ fun MeasurementHistoryView(appState: AppState) { // Level chart item { InteractiveChartCard( - title = "Bladder Level", + title = stringResource(R.string.history_chart_bladder_level), measurements = measurements, valueExtractor = { it.level.toFloat() }, maxValue = BladderLevel.MAX_LEVEL.toFloat(), @@ -113,7 +115,7 @@ fun MeasurementHistoryView(appState: AppState) { if (measurements.isNotEmpty()) { item { InteractiveChartCard( - title = "Estimated Volume", + title = stringResource(R.string.history_chart_estimated_volume), measurements = measurements, valueExtractor = { it.estimatedVolume.toFloat() }, maxValue = measurements.maxOf { it.estimatedVolume }.toFloat().coerceAtLeast(1f), @@ -133,13 +135,14 @@ fun MeasurementHistoryView(appState: AppState) { modifier = Modifier.shadow(4.dp, RoundedCornerShape(16.dp)) ) { Column(modifier = Modifier.padding(20.dp)) { - Text("Catheterization Log", fontSize = 14.sp, fontWeight = FontWeight.SemiBold) + Text(stringResource(R.string.history_catheterization_log), fontSize = 14.sp, fontWeight = FontWeight.SemiBold) Spacer(modifier = Modifier.height(8.dp)) cathEvents.forEach { event -> + val yesterdayLabel = stringResource(R.string.history_yesterday) Row(modifier = Modifier.padding(vertical = 4.dp)) { - Text(formatDate(event.timestamp), fontSize = 13.sp) + Text(formatDate(event.timestamp, yesterdayLabel), fontSize = 13.sp) Spacer(modifier = Modifier.weight(1f)) - Text("at Lv.${event.level}", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText) + Text(stringResource(R.string.history_at_level, event.level), fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText) } } } @@ -191,6 +194,8 @@ private fun InteractiveChartCard( if (selectedIndex in measurements.indices) { val record = measurements[selectedIndex] val value = valueExtractor(record) + val todayLabel = stringResource(R.string.history_today) + val yesterdayLabel = stringResource(R.string.history_yesterday) Row( modifier = Modifier .fillMaxWidth() @@ -200,7 +205,7 @@ private fun InteractiveChartCard( verticalAlignment = Alignment.CenterVertically ) { Text( - formatDateTime(record.timestamp), + formatDateTime(record.timestamp, todayLabel, yesterdayLabel), fontSize = 11.sp, fontWeight = FontWeight.Medium, color = MlPrimary @@ -224,7 +229,7 @@ private fun InteractiveChartCard( } if (measurements.isEmpty()) { - Text("No measurement data", fontSize = 12.sp, color = MlSecondaryText) + Text(stringResource(R.string.history_no_data), fontSize = 12.sp, color = MlSecondaryText) } else { val density = LocalDensity.current val textPaint = remember { @@ -346,7 +351,7 @@ private fun InteractiveChartCard( // Hint text if (measurements.size > 3 && scale < 1.05f && selectedIndex < 0) { Text( - "Pinch to zoom \u2022 Tap point for details", + stringResource(R.string.history_pinch_hint), fontSize = 10.sp, color = MlSecondaryText.copy(alpha = 0.5f), modifier = Modifier.padding(top = 4.dp) @@ -390,10 +395,10 @@ private fun RecordCard(record: MeasurementRecord) { Column(modifier = Modifier.weight(1f)) { Text( - if (isCath) "Catheterization" else "Measurement", + if (isCath) stringResource(R.string.history_catheterization) else stringResource(R.string.history_measurement), fontSize = 14.sp, fontWeight = FontWeight.SemiBold ) - Text(formatDate(record.timestamp), fontSize = 12.sp, color = MlSecondaryText) + Text(formatDate(record.timestamp, stringResource(R.string.history_yesterday)), fontSize = 12.sp, color = MlSecondaryText) } Column(horizontalAlignment = Alignment.End) { @@ -408,7 +413,7 @@ private fun RecordCard(record: MeasurementRecord) { } } -private fun formatDate(timestamp: Long): String { +private fun formatDate(timestamp: Long, yesterdayLabel: String = "Yesterday"): String { val cal = Calendar.getInstance() val now = Calendar.getInstance() cal.timeInMillis = timestamp @@ -416,18 +421,18 @@ private fun formatDate(timestamp: Long): String { return when { cal.get(Calendar.DAY_OF_YEAR) == now.get(Calendar.DAY_OF_YEAR) && cal.get(Calendar.YEAR) == now.get(Calendar.YEAR) -> { - SimpleDateFormat("h:mm a", Locale.US).format(Date(timestamp)) + SimpleDateFormat("h:mm a", Locale.getDefault()).format(Date(timestamp)) } now.timeInMillis - timestamp < 2 * 24 * 60 * 60 * 1000 -> { - "Yesterday " + SimpleDateFormat("h:mm a", Locale.US).format(Date(timestamp)) + "$yesterdayLabel " + SimpleDateFormat("h:mm a", Locale.getDefault()).format(Date(timestamp)) } else -> { - SimpleDateFormat("MMM d, h:mm a", Locale.US).format(Date(timestamp)) + SimpleDateFormat("MMM d, h:mm a", Locale.getDefault()).format(Date(timestamp)) } } } -private fun formatDateTime(timestamp: Long): String { +private fun formatDateTime(timestamp: Long, todayLabel: String = "Today", yesterdayLabel: String = "Yesterday"): String { val cal = Calendar.getInstance() val now = Calendar.getInstance() cal.timeInMillis = timestamp @@ -435,13 +440,13 @@ private fun formatDateTime(timestamp: Long): String { return when { cal.get(Calendar.DAY_OF_YEAR) == now.get(Calendar.DAY_OF_YEAR) && cal.get(Calendar.YEAR) == now.get(Calendar.YEAR) -> { - "Today " + SimpleDateFormat("h:mm:ss a", Locale.US).format(Date(timestamp)) + "$todayLabel " + SimpleDateFormat("h:mm:ss a", Locale.getDefault()).format(Date(timestamp)) } now.timeInMillis - timestamp < 2 * 24 * 60 * 60 * 1000 -> { - "Yesterday " + SimpleDateFormat("h:mm:ss a", Locale.US).format(Date(timestamp)) + "$yesterdayLabel " + SimpleDateFormat("h:mm:ss a", Locale.getDefault()).format(Date(timestamp)) } else -> { - SimpleDateFormat("MMM d, h:mm:ss a", Locale.US).format(Date(timestamp)) + SimpleDateFormat("MMM d, h:mm:ss a", Locale.getDefault()).format(Date(timestamp)) } } } diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/home/HomeView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/home/HomeView.kt index 91ee70b..f8c22c7 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/home/HomeView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/home/HomeView.kt @@ -50,7 +50,7 @@ fun HomeView(appState: AppState) { .align(Alignment.TopEnd) .padding(8.dp) ) { - Icon(Icons.Default.Language, contentDescription = "Language", + Icon(Icons.Default.Language, contentDescription = stringResource(R.string.home_language_icon_desc), tint = MlSecondaryText.copy(alpha = 0.7f)) } @@ -82,7 +82,8 @@ fun HomeView(appState: AppState) { if (tapCount >= 3) { tapCount = 0 appState.isDevMode = !appState.isDevMode - val msg = if (appState.isDevMode) "Developer mode ON" else "Developer mode OFF" + val msg = if (appState.isDevMode) context.getString(R.string.home_dev_mode_on) + else context.getString(R.string.home_dev_mode_off) Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() } } @@ -95,7 +96,7 @@ fun HomeView(appState: AppState) { Spacer(modifier = Modifier.height(4.dp)) - Text("Smart Bladder Monitoring", fontSize = 16.sp, color = MlSecondaryText) + Text(stringResource(R.string.home_subtitle), fontSize = 16.sp, color = MlSecondaryText) Spacer(modifier = Modifier.height(4.dp)) @@ -107,7 +108,7 @@ fun HomeView(appState: AppState) { // BLE 연결 상태 chip — 연결 유지 정책에서 사용자가 인지 가능하도록 if (appState.isDeviceConnected) { Text( - "● Connected — Start to resume", + stringResource(R.string.home_connected_resume), fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = Color(0xFF4CAF50), @@ -139,7 +140,7 @@ fun HomeView(appState: AppState) { ), contentAlignment = Alignment.Center ) { - Text("Start", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = Color.White) + Text(stringResource(R.string.home_start), fontSize = 20.sp, fontWeight = FontWeight.Bold, color = Color.White) } } @@ -155,7 +156,7 @@ fun HomeView(appState: AppState) { shape = RoundedCornerShape(14.dp), colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF7C3AED)) ) { - Text("Clinical Mode (Dev)", color = Color.White, fontWeight = FontWeight.SemiBold) + Text(stringResource(R.string.home_clinical_mode_dev), color = Color.White, fontWeight = FontWeight.SemiBold) } } else { Spacer(modifier = Modifier.height(48.dp)) diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt index c78f62d..a825a66 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt @@ -108,7 +108,7 @@ fun PiezoMonitoringView(appState: AppState) { .padding(horizontal = 32.dp, vertical = 64.dp), contentAlignment = Alignment.Center ) { - Text("Recorded", fontSize = 48.sp, fontWeight = FontWeight.ExtraBold, color = Color.White) + Text(stringResource(R.string.piezo_recorded), fontSize = 48.sp, fontWeight = FontWeight.ExtraBold, color = Color.White) } } } @@ -812,10 +812,10 @@ fun PiezoMonitoringView(appState: AppState) { var addAmount by remember { mutableStateOf("1") } AlertDialog( onDismissRequest = { showAddCatheterDialog = false }, - title = { Text("Add Catheters") }, + title = { Text(stringResource(R.string.piezo_add_catheters_title)) }, text = { Column { - Text("Enter the number of catheters to add:") + Text(stringResource(R.string.piezo_add_catheters_message)) Spacer(modifier = Modifier.height(8.dp)) OutlinedTextField( value = addAmount, @@ -829,10 +829,10 @@ fun PiezoMonitoringView(appState: AppState) { val amount = addAmount.toIntOrNull() ?: 0 if (amount > 0) appState.addCatheterCount(amount) showAddCatheterDialog = false - }) { Text("Add", color = MlPrimary) } + }) { Text(stringResource(R.string.piezo_add), color = MlPrimary) } }, dismissButton = { - TextButton(onClick = { showAddCatheterDialog = false }) { Text("Cancel") } + TextButton(onClick = { showAddCatheterDialog = false }) { Text(stringResource(R.string.cancel)) } } ) } @@ -841,17 +841,17 @@ fun PiezoMonitoringView(appState: AppState) { if (showCatheterizeConfirm) { AlertDialog( onDismissRequest = { showCatheterizeConfirm = false }, - title = { Text("Reset bladder level?") }, - text = { Text("This will reset the bladder level to 0. This action cannot be undone.") }, + title = { Text(stringResource(R.string.piezo_reset_level_title)) }, + text = { Text(stringResource(R.string.piezo_reset_level_message)) }, confirmButton = { TextButton(onClick = { appState.resetLevel() resetMeasurement() showCatheterizeConfirm = false - }) { Text("Catheterize", color = Color.Red) } + }) { Text(stringResource(R.string.piezo_catheterize), color = Color.Red) } }, dismissButton = { - TextButton(onClick = { showCatheterizeConfirm = false }) { Text("Cancel") } + TextButton(onClick = { showCatheterizeConfirm = false }) { Text(stringResource(R.string.cancel)) } } ) } @@ -946,7 +946,7 @@ fun PiezoMonitoringView(appState: AppState) { ) { Icon(Icons.Default.FlashOn, contentDescription = null, tint = Color(0xFF4CAF50)) Spacer(modifier = Modifier.width(12.dp)) - Text("Quick Save", fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f)) + Text(stringResource(R.string.piezo_quick_save), fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f)) Text("$quickVol mL", color = MlSecondaryText, fontSize = 14.sp) Spacer(modifier = Modifier.width(4.dp)) Icon(Icons.Default.ChevronRight, contentDescription = null, tint = MlSecondaryText) @@ -1075,10 +1075,10 @@ fun PiezoMonitoringView(appState: AppState) { val iconSize = if (isCompact) 28.dp else 32.dp // 빌드 122: Home 아이콘 추가 (Placement 옆) IconButton(onClick = { appState.goHome() }, modifier = Modifier.size(btnSize)) { - Icon(Icons.Default.Home, "Home", tint = MlPrimary, modifier = Modifier.size(iconSize)) + Icon(Icons.Default.Home, stringResource(R.string.piezo_home_desc), tint = MlPrimary, modifier = Modifier.size(iconSize)) } IconButton(onClick = { appState.enterPlacementFromMonitoring() }, modifier = Modifier.size(btnSize)) { - Icon(Icons.Default.MyLocation, "Placement", tint = MlPrimary, modifier = Modifier.size(iconSize)) + Icon(Icons.Default.MyLocation, stringResource(R.string.piezo_placement_desc), tint = MlPrimary, modifier = Modifier.size(iconSize)) } Spacer(modifier = Modifier.weight(1f)) @@ -1088,13 +1088,13 @@ fun PiezoMonitoringView(appState: AppState) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( Icons.Default.BluetoothDisabled, - contentDescription = "Disconnected", + contentDescription = stringResource(R.string.piezo_disconnect_desc), tint = Color.Red, modifier = Modifier.size(16.dp) ) Spacer(modifier = Modifier.width(4.dp)) Text( - "Disconnect", + stringResource(R.string.piezo_disconnect), fontSize = (12 * fontScale).sp, fontWeight = FontWeight.Bold, color = Color.Red @@ -1175,11 +1175,11 @@ fun PiezoMonitoringView(appState: AppState) { modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically ) { - Icon(Icons.Default.LocalHospital, "Catheter", tint = MlPrimary, modifier = Modifier.size(16.dp)) + Icon(Icons.Default.LocalHospital, stringResource(R.string.piezo_catheter_desc), tint = MlPrimary, modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.width(4.dp)) Text("$cathCount", fontSize = (13 * fontScale).sp, fontWeight = FontWeight.Bold, color = MlPrimary) Spacer(modifier = Modifier.width(6.dp)) - Text("left", fontSize = (10 * fontScale).sp, fontWeight = FontWeight.SemiBold, + Text(stringResource(R.string.piezo_catheter_left), fontSize = (10 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = MlPrimary.copy(alpha = 0.7f)) } } @@ -1187,12 +1187,12 @@ fun PiezoMonitoringView(appState: AppState) { var addCount by remember { mutableStateOf("1") } AlertDialog( onDismissRequest = { showAddCatheter = false }, - title = { Text("Add Catheters") }, + title = { Text(stringResource(R.string.piezo_add_catheters_title)) }, text = { OutlinedTextField( value = addCount, onValueChange = { addCount = it.filter { c -> c.isDigit() } }, - label = { Text("Count") }, + label = { Text(stringResource(R.string.piezo_add_catheters_count_label)) }, singleLine = true ) }, @@ -1201,10 +1201,10 @@ fun PiezoMonitoringView(appState: AppState) { val n = addCount.toIntOrNull() ?: 0 if (n > 0) appState.addCatheterCount(n) showAddCatheter = false - }) { Text("Add") } + }) { Text(stringResource(R.string.piezo_add)) } }, dismissButton = { - TextButton(onClick = { showAddCatheter = false }) { Text("Cancel") } + TextButton(onClick = { showAddCatheter = false }) { Text(stringResource(R.string.cancel)) } } ) } @@ -1213,7 +1213,7 @@ fun PiezoMonitoringView(appState: AppState) { // Settings button IconButton(onClick = { showSettings = !showSettings }, modifier = Modifier.size(btnSize)) { - Icon(Icons.Default.Settings, "Settings", tint = if (showSettings) MlPrimary else MlSecondaryText, modifier = Modifier.size(iconSize)) + Icon(Icons.Default.Settings, stringResource(R.string.piezo_settings_desc), tint = if (showSettings) MlPrimary else MlSecondaryText, modifier = Modifier.size(iconSize)) } } @@ -1282,10 +1282,10 @@ fun PiezoMonitoringView(appState: AppState) { com.example.medilightv2android.managers.Posture.UNKNOWN -> Color(0xFF616161) // 진한 회색 — 가시성 확보 } val label = when (posture) { - com.example.medilightv2android.managers.Posture.LYING -> "Lying" - com.example.medilightv2android.managers.Posture.NON_LYING -> "Upright" - com.example.medilightv2android.managers.Posture.WALKING -> "Walking" - com.example.medilightv2android.managers.Posture.UNKNOWN -> "Detecting" + com.example.medilightv2android.managers.Posture.LYING -> stringResource(R.string.piezo_posture_lying) + com.example.medilightv2android.managers.Posture.NON_LYING -> stringResource(R.string.piezo_posture_upright) + com.example.medilightv2android.managers.Posture.WALKING -> stringResource(R.string.piezo_posture_walking) + com.example.medilightv2android.managers.Posture.UNKNOWN -> stringResource(R.string.piezo_posture_detecting) } Row( verticalAlignment = Alignment.CenterVertically, @@ -1371,7 +1371,8 @@ fun PiezoMonitoringView(appState: AppState) { if (devTapCount >= 3) { devTapCount = 0 appState.isDevMode = !appState.isDevMode - val msg = if (appState.isDevMode) "Developer mode ON" else "Developer mode OFF" + val msg = if (appState.isDevMode) context.getString(R.string.home_dev_mode_on) + else context.getString(R.string.home_dev_mode_off) android.widget.Toast.makeText(context, msg, android.widget.Toast.LENGTH_SHORT).show() } } @@ -1401,7 +1402,7 @@ fun PiezoMonitoringView(appState: AppState) { // 좌측: Current Measurement (회의 #7) val showMeasurement = if (isAutoMeasuring) autoVolumeWindow.size >= 5 && lastVolumeMl > 0 else lastVolumeMl > 0 val currentMeasurement = if (showMeasurement) "%.0f".format(lastVolumeMl) else "—" - InfoCard("Current Measurement", currentMeasurement, "mL", urgency.accentColor, Modifier.weight(1f)) + InfoCard(stringResource(R.string.piezo_current_measurement), currentMeasurement, "mL", urgency.accentColor, Modifier.weight(1f)) // 우측: 측정 max / max capacity 비율 (회의 #6) — 위에 XX%, 아래 raw 분수 val maxMeasured = if (displayMaxVolumeMl > 0) "%.0f".format(displayMaxVolumeMl) else "—" val capacityVal = appState.piezoSettings.maxVolume @@ -1447,7 +1448,7 @@ fun PiezoMonitoringView(appState: AppState) { disabledContainerColor = Color.Gray.copy(alpha = 0.3f) ) ) { - Text("Voiding / Catheterization", fontSize = (12 * fontScale).sp, + Text(stringResource(R.string.piezo_voiding_catheterization), fontSize = (12 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White, textAlign = TextAlign.Center, maxLines = 2) } @@ -1466,9 +1467,9 @@ fun PiezoMonitoringView(appState: AppState) { ) ) { if (isAutoMeasuring) { - Text("Stop Scan", fontSize = (14 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White) + Text(stringResource(R.string.piezo_stop_scan), fontSize = (14 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White) } else { - Text("Auto Scan", fontSize = (14 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White) + Text(stringResource(R.string.piezo_auto_scan), fontSize = (14 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White) } } @@ -1532,7 +1533,7 @@ fun PiezoMonitoringView(appState: AppState) { if (isSpotInProgress) { CircularProgressIndicator(color = Color.White, modifier = Modifier.size(18.dp), strokeWidth = 2.dp) } else { - Text("Single Scan", fontSize = (14 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White) + Text(stringResource(R.string.piezo_single_scan), fontSize = (14 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White) } } } @@ -1550,7 +1551,7 @@ fun PiezoMonitoringView(appState: AppState) { ) { Icon(Icons.Default.Build, "Logs", tint = Color.White, modifier = Modifier.size(14.dp)) Text( - if (appState.isDevMode) "Developer Tools" else "Logs & Charts", + if (appState.isDevMode) stringResource(R.string.piezo_developer_tools) else stringResource(R.string.piezo_logs_charts), fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = Color.White ) Spacer(modifier = Modifier.weight(1f)) @@ -1648,7 +1649,7 @@ fun PiezoMonitoringView(appState: AppState) { ) { // Max Volume Row(verticalAlignment = Alignment.CenterVertically) { - Text("Bladder Capacity", fontSize = labelSize, fontWeight = FontWeight.SemiBold) + Text(stringResource(R.string.piezo_settings_bladder_capacity), fontSize = labelSize, fontWeight = FontWeight.SemiBold) Spacer(modifier = Modifier.weight(1f)) Text("${appState.piezoSettings.maxVolume} mL", fontSize = labelSize, fontWeight = FontWeight.Bold, color = MlPrimary) } @@ -1693,9 +1694,9 @@ fun PiezoMonitoringView(appState: AppState) { // Catheterization Threshold Row(verticalAlignment = Alignment.CenterVertically) { - Text("Catheter Threshold", fontSize = labelSize, fontWeight = FontWeight.SemiBold) + Text(stringResource(R.string.piezo_settings_catheter_threshold), fontSize = labelSize, fontWeight = FontWeight.SemiBold) Spacer(modifier = Modifier.weight(1f)) - Text("Level ${appState.bladderLevel.catheterizationThreshold}", fontSize = labelSize, fontWeight = FontWeight.Bold, color = MlPrimary) + Text(stringResource(R.string.piezo_settings_level_format, appState.bladderLevel.catheterizationThreshold), fontSize = labelSize, fontWeight = FontWeight.Bold, color = MlPrimary) } LockableSlider( value = appState.bladderLevel.catheterizationThreshold.toFloat(), @@ -1715,14 +1716,16 @@ fun PiezoMonitoringView(appState: AppState) { val displayValue = if (unitMode == "sec") currentMs / 1000.0 else currentMs / 60_000.0 val valueClamped = displayValue.coerceIn(0.5, 10.0) Row(verticalAlignment = Alignment.CenterVertically) { - Text("Auto Scan Interval", fontSize = labelSize, fontWeight = FontWeight.SemiBold) + Text(stringResource(R.string.piezo_settings_auto_scan_interval), fontSize = labelSize, fontWeight = FontWeight.SemiBold) Spacer(modifier = Modifier.weight(1f)) Text("%.1f".format(valueClamped), fontSize = labelSize, fontWeight = FontWeight.Bold, color = MlPrimary) Spacer(modifier = Modifier.width(6.dp)) // sec/min 토글 (우측) Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - listOf("sec", "min").forEach { mode -> + val secLabel = stringResource(R.string.piezo_settings_unit_sec) + val minLabel = stringResource(R.string.piezo_settings_unit_min) + listOf("sec" to secLabel, "min" to minLabel).forEach { (mode, label) -> val sel = unitMode == mode Box( modifier = Modifier @@ -1738,7 +1741,7 @@ fun PiezoMonitoringView(appState: AppState) { } .padding(horizontal = 10.dp, vertical = 2.dp) ) { - Text(mode, fontSize = 11.sp, fontWeight = FontWeight.Bold, + Text(label, fontSize = 11.sp, fontWeight = FontWeight.Bold, color = if (sel) Color.White else Color.Black) } } @@ -2174,7 +2177,7 @@ fun PiezoMonitoringView(appState: AppState) { ) { Icon(Icons.Default.LinkOff, null, tint = Color(0xFF333333), modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.width(6.dp)) - Text("Disconnect", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = Color(0xFF333333)) + Text(stringResource(R.string.piezo_disconnect), fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = Color(0xFF333333)) } Spacer(modifier = Modifier.height(6.dp)) @@ -2189,13 +2192,13 @@ fun PiezoMonitoringView(appState: AppState) { ) { Icon(Icons.Default.LinkOff, null, tint = Color.Red, modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.width(6.dp)) - Text("Delete Pairing", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = Color.Red) + Text(stringResource(R.string.piezo_delete_pairing_title), fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = Color.Red) } if (showDisconnectConfirm) { AlertDialog( onDismissRequest = { showDisconnectConfirm = false }, - title = { Text("Delete Pairing") }, - text = { Text("Disconnect and remove pairing.") }, + title = { Text(stringResource(R.string.piezo_delete_pairing_title)) }, + text = { Text(stringResource(R.string.piezo_delete_pairing_message)) }, confirmButton = { TextButton(onClick = { showDisconnectConfirm = false; showSettings = false @@ -2204,9 +2207,9 @@ fun PiezoMonitoringView(appState: AppState) { // 명시적 Unpair는 끊은 게 맞으므로 여기서 직접 false 처리. appState.isDeviceConnected = false appState.goHome() - }) { Text("Delete", color = Color.Red) } + }) { Text(stringResource(R.string.delete), color = Color.Red) } }, - dismissButton = { TextButton(onClick = { showDisconnectConfirm = false }) { Text("Cancel") } } + dismissButton = { TextButton(onClick = { showDisconnectConfirm = false }) { Text(stringResource(R.string.cancel)) } } ) } @@ -2219,7 +2222,7 @@ fun PiezoMonitoringView(appState: AppState) { modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { - Text("App", fontSize = 13.sp, color = MlSecondaryText) + Text(stringResource(R.string.piezo_settings_app), fontSize = 13.sp, color = MlSecondaryText) Spacer(modifier = Modifier.weight(1f)) Text( com.example.medilightv2android.BuildConfig.VERSION_NAME, @@ -2235,7 +2238,7 @@ fun PiezoMonitoringView(appState: AppState) { modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { - Text("Device", fontSize = 13.sp, color = MlSecondaryText) + Text(stringResource(R.string.piezo_settings_device), fontSize = 13.sp, color = MlSecondaryText) Spacer(modifier = Modifier.weight(1f)) Text(devName, fontSize = 13.sp, @@ -2250,7 +2253,7 @@ fun PiezoMonitoringView(appState: AppState) { modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { - Text("Firmware", fontSize = 13.sp, color = MlSecondaryText) + Text(stringResource(R.string.piezo_settings_firmware), fontSize = 13.sp, color = MlSecondaryText) Spacer(modifier = Modifier.weight(1f)) Text( if (fwVersion.isNotEmpty()) fwVersion else "—", @@ -2290,7 +2293,7 @@ fun PiezoMonitoringView(appState: AppState) { verticalAlignment = Alignment.CenterVertically ) { TextButton(onClick = { showSettings = false }) { - Text("Cancel", color = MlSecondaryText) + Text(stringResource(R.string.cancel), color = MlSecondaryText) } Spacer(modifier = Modifier.width(8.dp)) Button( @@ -2301,7 +2304,7 @@ fun PiezoMonitoringView(appState: AppState) { shape = RoundedCornerShape(10.dp), colors = ButtonDefaults.buttonColors(containerColor = MlPrimary) ) { - Text("Save", color = Color.White, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.save), color = Color.White, fontWeight = FontWeight.Bold) } } } @@ -2313,18 +2316,18 @@ fun PiezoMonitoringView(appState: AppState) { if (showRealignPrompt) { AlertDialog( onDismissRequest = { showRealignPrompt = false }, - title = { Text("Position Lost or Insufficient Gel") }, - text = { Text("Position lost or gel may be insufficient. Please check.") }, + title = { Text(stringResource(R.string.piezo_realign_title)) }, + text = { Text(stringResource(R.string.piezo_realign_message)) }, confirmButton = { Button(onClick = { showRealignPrompt = false autoFailCount = 0 appState.currentScreen = com.example.medilightv2android.AppScreen.PLACEMENT_GUIDE - }) { Text("Realign") } + }) { Text(stringResource(R.string.piezo_realign_button)) } }, dismissButton = { TextButton(onClick = { showRealignPrompt = false; autoFailCount = 0 }) { - Text("Dismiss") + Text(stringResource(R.string.piezo_dismiss)) } } ) diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt index cd578c1..68c953b 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt @@ -83,8 +83,8 @@ fun PlacementGuideView(appState: AppState) { val isSweepMode = com.example.medilightv2android.managers.GreenZoneConstants.placementGuideMode == com.example.medilightv2android.managers.PlacementGuideMode.SWEEP var directionHint by remember { mutableStateOf( - if (isSweepMode) "Place sensor near navel, slide down ↓" - else "Place the sensor just above the pubic bone" + if (isSweepMode) context.getString(R.string.placement_sweep_initial) + else context.getString(R.string.placement_initial_hint) ) } var directionIcon by remember { mutableStateOf("") } var placementScore by remember { mutableIntStateOf(0) } @@ -148,7 +148,7 @@ fun PlacementGuideView(appState: AppState) { lastHintChangeMs = 0L lastGuidePhase = PlacementPhase.VERTICAL phaseHoldScans = 0 - directionHint = "Place VesiScan above the pubic bone, then press Start" + directionHint = context.getString(R.string.placement_press_start) directionIcon = "" lastCv = Double.NaN for (i in 0 until 6) { channelDetected[i] = false; channelUrineLen[i] = 0 } @@ -265,24 +265,24 @@ fun PlacementGuideView(appState: AppState) { }) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(R.string.placement_back_desc), tint = MlPrimary ) } Spacer(modifier = Modifier.weight(1f)) if (isLocked) { - Icon(Icons.Filled.CheckCircle, "Ready", tint = Color(0xFF4CAF50), modifier = Modifier.size(24.dp)) + Icon(Icons.Filled.CheckCircle, stringResource(R.string.placement_ready_desc), tint = Color(0xFF4CAF50), modifier = Modifier.size(24.dp)) Spacer(modifier = Modifier.width(4.dp)) } if (appState.isDevMode) { IconButton(onClick = { showPlacementSettings = !showPlacementSettings }) { - Icon(Icons.Default.Settings, "Settings", tint = if (showPlacementSettings) MlPrimary else MlSecondaryText) + Icon(Icons.Default.Settings, stringResource(R.string.piezo_settings_desc), tint = if (showPlacementSettings) MlPrimary else MlSecondaryText) } } } // (B) 타이틀 — 태블릿 기준 화면폭 ≈ 60% 차지 (32 → 56sp). Text( - text = "Sensor Alignment", + text = stringResource(R.string.placement_title), fontSize = 56.sp, fontWeight = FontWeight.ExtraBold, color = Color.Black @@ -307,11 +307,18 @@ fun PlacementGuideView(appState: AppState) { // ─── (1) Hint 1 — 박스는 항상 자리(높이 1.5×), 텍스트는 waitingForStart 시만 ─── val hint1Bg = Color(0xFFFFF3E0) - val hint1Text = remember { + val hint1Full = stringResource(R.string.placement_press_start) + val hint1KeyEn = "pubic bone" + val hint1KeyKo = "치골" + val hint1Text = remember(hint1Full) { buildAnnotatedString { - val full = "Place VesiScan above the pubic bone, then press Start" - val key = "pubic bone" - val idx = full.indexOf(key, ignoreCase = true) + val full = hint1Full + val key = when { + full.contains(hint1KeyEn, ignoreCase = true) -> hint1KeyEn + full.contains(hint1KeyKo) -> hint1KeyKo + else -> "" + } + val idx = if (key.isNotEmpty()) full.indexOf(key, ignoreCase = true) else -1 if (idx >= 0) { append(full.substring(0, idx)) withStyle(SpanStyle(color = Color.Red, fontWeight = FontWeight.ExtraBold)) { @@ -356,6 +363,8 @@ fun PlacementGuideView(appState: AppState) { if (hint.startsWith("Slide") || hint.startsWith("Optimizing") || hint.startsWith("Checking") || hint.startsWith("Adjust") || hint.contains("이동") || hint.contains("천천히") || hint.contains("그대로") || + hint.contains("내려") || hint.contains("올려") || hint.contains("확인") || + hint.contains("조정") || hint.contains("최적화") || hint.contains("Hold") || hint.contains("slowly") || hint.contains("Slightly")) { append(dots) } @@ -364,14 +373,20 @@ fun PlacementGuideView(appState: AppState) { } } val displayHint = buildAnnotatedString { - val key = "pubic bone" - val idx = rawHint.indexOf(key, ignoreCase = true) + val keyEn = "pubic bone" + val keyKo = "치골" + val matchedKey = when { + rawHint.contains(keyEn, ignoreCase = true) -> keyEn + rawHint.contains(keyKo) -> keyKo + else -> "" + } + val idx = if (matchedKey.isNotEmpty()) rawHint.indexOf(matchedKey, ignoreCase = true) else -1 if (idx >= 0) { append(rawHint.substring(0, idx)) withStyle(SpanStyle(color = Color.Red, fontWeight = FontWeight.ExtraBold)) { - append(rawHint.substring(idx, idx + key.length)) + append(rawHint.substring(idx, idx + matchedKey.length)) } - append(rawHint.substring(idx + key.length)) + append(rawHint.substring(idx + matchedKey.length)) } else { append(rawHint) } @@ -400,7 +415,7 @@ fun PlacementGuideView(appState: AppState) { // 1) 하복부 일러스트 (배경 — 박스 가득) Image( painter = painterResource(id = com.example.medilightv2android.R.drawable.body_silhouette), - contentDescription = "Body silhouette", + contentDescription = stringResource(R.string.placement_body_desc), modifier = Modifier.matchParentSize(), contentScale = ContentScale.FillBounds ) @@ -559,7 +574,7 @@ fun PlacementGuideView(appState: AppState) { // waitingForStart 일 때는 "Step 0/N" + 모든 점 숨김 (체크포인트 포함). val displayStepIdx = if (waitingForStart) -1 else currentStepIdx Text( - text = "Step ${displayStepIdx + 1}/$totalSteps", + text = stringResource(R.string.placement_step_format, displayStepIdx + 1, totalSteps), fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.Black ) if (!waitingForStart) { @@ -846,7 +861,7 @@ fun PlacementGuideView(appState: AppState) { prevDetached = nowDetached if (detachResult != null && detachResult.isDetached) { - directionHint = "Sensor detached. Restart above pubic bone." + directionHint = context.getString(R.string.placement_detached_restart) directionIcon = "" // 화살표 없음 placementScore = 0 isContinuousMode = false @@ -938,7 +953,7 @@ fun PlacementGuideView(appState: AppState) { val vs = VerticalState(maxCenterSeen, stableAtMax, boundaryHit, lastDetectedCount = sweepLastDetected, everSawAllFour = sweepEverSawAll) val guideResult = computePlacementGuide( - detected, urineLens, allWalls, scanCount, placementPhase, vs, + context, detected, urineLens, allWalls, scanCount, placementPhase, vs, currentRMm = currentRMm, bestRMm = bestRMm, sagittalArgMaxCh = sagittalArgMaxCh, @@ -1016,7 +1031,7 @@ fun PlacementGuideView(appState: AppState) { // GREEN hold 중이면 힌트 강제 유지 if (greenHeld) { - directionHint = "In position!" + directionHint = context.getString(R.string.placement_in_position) directionIcon = "checkmark.circle" placementPhase = PlacementPhase.GREEN if (!guideResult.cv.isNaN()) lastCv = guideResult.cv @@ -1033,7 +1048,7 @@ fun PlacementGuideView(appState: AppState) { greenFailCount++ if (greenFailCount < 3) { // 아직 이탈 안 함 — GREEN 유지 - directionHint = "In position!" + directionHint = context.getString(R.string.placement_in_position) directionIcon = "checkmark.circle" placementPhase = PlacementPhase.GREEN } else { @@ -1042,7 +1057,7 @@ fun PlacementGuideView(appState: AppState) { greenLockedAtMs = 0L greenFailCount = 0 if (lastLedMode != 5) { bleManager.sendLedMode(5); lastLedMode = 5 } - directionHint = "Position lost. Restart above pubic bone." + directionHint = context.getString(R.string.placement_position_lost) directionIcon = "" // 화살표 없음 placementScore = 10 placementPhase = PlacementPhase.VERTICAL @@ -1190,7 +1205,7 @@ fun PlacementGuideView(appState: AppState) { shape = RoundedCornerShape(18.dp), colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFF9800)) ) { - Text("Start Alignment", fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.Black) + Text(stringResource(R.string.placement_start_alignment), fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.Black) } } @@ -1228,7 +1243,7 @@ fun PlacementGuideView(appState: AppState) { Icon(Icons.Filled.CheckCircle, contentDescription = null, tint = Color.White, modifier = Modifier.size(32.dp)) Spacer(modifier = Modifier.width(10.dp)) Text( - if (isClinicalAlign) "Complete Alignment" else "Start Scanning", + if (isClinicalAlign) stringResource(R.string.placement_complete_alignment) else stringResource(R.string.placement_start_scanning), fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.White ) } @@ -1249,7 +1264,7 @@ fun PlacementGuideView(appState: AppState) { } }) { Text( - text = "Skip", + text = stringResource(R.string.placement_skip), fontSize = 20.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText @@ -1675,6 +1690,7 @@ data class VerticalState( ) private fun computePlacementGuide( + context: android.content.Context, detected: List, urineLens: List, walls: List?> = emptyList(), @@ -1688,9 +1704,10 @@ private fun computePlacementGuide( lateralOffsetMm: Double? = null, ): PlacementGuideResult { val detectedCount = detected.count { it } + fun s(id: Int) = context.getString(id) if (detectedCount == 0) { - return PlacementGuideResult("Slide up ↑", "arrow.up", 0, + return PlacementGuideResult(s(R.string.placement_slide_up), "arrow.up", 0, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) } @@ -1715,9 +1732,9 @@ private fun computePlacementGuide( // Phase 1: nothing detected — start instruction if (centerDetected == 0) { val hint = if (vs.everSawAllFour) - "Signal lost — slide up ↑" + s(R.string.placement_signal_lost) else - "Start near navel, slide down slowly ↓" + s(R.string.placement_sweep_start) val icon = if (vs.everSawAllFour) "arrow.up" else "arrow.down" return PlacementGuideResult(hint, icon, 5, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) @@ -1726,7 +1743,7 @@ private fun computePlacementGuide( // Phase 4: was at all-4, now lost a channel → overshoot (boundary) if (vs.everSawAllFour && centerDetected < prev && centerDetected < 4) { vs.boundaryHit = true - return PlacementGuideResult("Too far — slide up ↑", "arrow.up", 50, + return PlacementGuideResult(s(R.string.placement_too_far), "arrow.up", 50, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) } @@ -1741,14 +1758,14 @@ private fun computePlacementGuide( val ch5ok = detected.getOrElse(5) { false } // 좌우도 이미 균등하면 바로 GREEN if (ch4ok && ch5ok && kotlin.math.abs(l4 - l5) <= 5) { - PlacementGuideResult("Position OK! Final check...", + PlacementGuideResult(s(R.string.placement_position_ok), "checkmark.circle", 90, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.GREEN) } else { val lrHint = when { - l4 > 0 && l5 > 0 && l4 > l5 -> "Slide left ←" - l4 > 0 && l5 > 0 && l5 > l4 -> "Slide right →" - else -> "Checking" + l4 > 0 && l5 > 0 && l4 > l5 -> s(R.string.placement_slide_left) + l4 > 0 && l5 > 0 && l5 > l4 -> s(R.string.placement_slide_right) + else -> s(R.string.placement_checking) } val lrIcon = when { l4 > 0 && l5 > 0 && l4 > l5 -> "arrow.left" @@ -1759,19 +1776,19 @@ private fun computePlacementGuide( phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.LATERAL) } } - 1 -> PlacementGuideResult("Slide down slightly ↓", "arrow.down", 75, + 1 -> PlacementGuideResult(s(R.string.placement_slide_down_slight), "arrow.down", 75, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) - 2 -> PlacementGuideResult("Slide down ↓", "arrow.down", 70, + 2 -> PlacementGuideResult(s(R.string.placement_slide_down), "arrow.down", 70, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) - 3 -> PlacementGuideResult("Slide further down ↓", "arrow.down", 65, + 3 -> PlacementGuideResult(s(R.string.placement_slide_further_down), "arrow.down", 65, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) - else -> PlacementGuideResult("Hold still", "checkmark.circle", 80, + else -> PlacementGuideResult(s(R.string.placement_hold_still), "checkmark.circle", 80, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) } } // Phase 2: channels coming online (1..3) - return PlacementGuideResult("Slide down ↓", "arrow.down", + return PlacementGuideResult(s(R.string.placement_slide_down), "arrow.down", 20 + centerDetected * 10, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) } @@ -1784,13 +1801,13 @@ private fun computePlacementGuide( val hint: String; val icon: String when { (ch0det || ch1det) && !ch2det && !ch3det -> { - hint = "Slide up ↑"; icon = "arrow.up" + hint = s(R.string.placement_slide_up); icon = "arrow.up" } (ch3det || ch2det) && !ch0det && !ch1det -> { - hint = "Slide down ↓"; icon = "arrow.down" + hint = s(R.string.placement_slide_down); icon = "arrow.down" } else -> { - hint = "Slide up ↑"; icon = "arrow.up" + hint = s(R.string.placement_slide_up); icon = "arrow.up" } } return PlacementGuideResult(hint, icon, centerDetected * 10, @@ -1835,7 +1852,7 @@ private fun computePlacementGuide( val ch5ok = detected.getOrElse(5) { false } // 항상 LATERAL 단계를 거침 — 전환 시 항상 Checking 표시 - val lrHint = "Checking" + val lrHint = s(R.string.placement_checking) val lrIcon = "checkmark.circle" val score = (40 + ((1.0 - kotlin.math.abs(gradient) / gradientThr) * 20).toInt()) .coerceIn(40, 60) @@ -1852,16 +1869,16 @@ private fun computePlacementGuide( val ch4ok = detected.getOrElse(4) { false } val ch5ok = detected.getOrElse(5) { false } // 항상 LATERAL 단계를 거침 - return PlacementGuideResult("Checking", + return PlacementGuideResult(s(R.string.placement_checking), "checkmark.circle", 50, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.LATERAL) } else if (gradient > 0) { - val intensity = if (gradient > 0.8) "Slide down ↓" else "Slide down slightly ↓" + val intensity = if (gradient > 0.8) s(R.string.placement_slide_down) else s(R.string.placement_slide_down_slight) return PlacementGuideResult(intensity, "arrow.down", (20 + centerDetected * 5).coerceAtMost(35), phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) } else { - val intensity = if (gradient < -0.8) "Slide up ↑" else "Slide up slightly ↑" + val intensity = if (gradient < -0.8) s(R.string.placement_slide_up) else s(R.string.placement_slide_up_slight) return PlacementGuideResult(intensity, "arrow.up", (20 + centerDetected * 5).coerceAtMost(35), phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) @@ -1877,14 +1894,14 @@ private fun computePlacementGuide( // 상하 유지 확인 — 좌우 이동 중 center 채널 소실 if (centerDetected < 2) { - return PlacementGuideResult("Start over from above the pubic bone", "arrow.up", 10, + return PlacementGuideResult(s(R.string.placement_restart_pubic), "arrow.up", 10, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.VERTICAL) } // 절대 차이 10 sample 이하 → 좌우 충분히 균등, 바로 GREEN if (ch4det && ch5det && kotlin.math.abs(len4 - len5) <= 10) { return PlacementGuideResult( - "Final check...", + s(R.string.placement_final_check), "checkmark.circle", 60, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.GREEN) } @@ -1894,15 +1911,15 @@ private fun computePlacementGuide( val absOff = kotlin.math.abs(lateralOffsetMm) if (absOff <= 8.0) { return PlacementGuideResult( - "Final check...", + s(R.string.placement_final_check), "checkmark.circle", 60, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.GREEN) } return if (lateralOffsetMm > 0) { - PlacementGuideResult("Slide right →", "arrow.right", 45, + PlacementGuideResult(s(R.string.placement_slide_right), "arrow.right", 45, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL) } else { - PlacementGuideResult("Slide left ←", "arrow.left", 45, + PlacementGuideResult(s(R.string.placement_slide_left), "arrow.left", 45, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL) } } @@ -1910,16 +1927,16 @@ private fun computePlacementGuide( // 한쪽만 detect → 반대쪽으로 이동 (잡히는 쪽의 반대로) // ch4(왼쪽)만 잡힘 = 기기가 오른쪽 치우침 = 왼쪽으로 if (ch4det && !ch5det) { - return PlacementGuideResult("Slide left ←", "arrow.left", 40, + return PlacementGuideResult(s(R.string.placement_slide_left), "arrow.left", 40, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL) } // ch5(오른쪽)만 잡힘 = 기기가 왼쪽 치우침 = 오른쪽으로 if (ch5det && !ch4det) { - return PlacementGuideResult("Slide right →", "arrow.right", 40, + return PlacementGuideResult(s(R.string.placement_slide_right), "arrow.right", 40, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL) } if (!ch4det && !ch5det) { - return PlacementGuideResult("Try moving right →", "arrow.right", 35, + return PlacementGuideResult(s(R.string.placement_try_right), "arrow.right", 35, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL) } @@ -1934,14 +1951,14 @@ private fun computePlacementGuide( if (lrSymmetry >= 0.5) { return PlacementGuideResult( - "Symmetry OK! Final check...", + s(R.string.placement_symmetry_ok), "checkmark.circle", 60, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.GREEN) } // ch4 > ch5 → 방광이 오른쪽에 있음 → 오른쪽으로 이동 // lrDiff > 0 = ch4 길다 = 방광이 왼쪽 → 왼쪽으로 - val hint = if (lrDiff > 0) "Move sensor left ←" else "Move sensor right →" + val hint = if (lrDiff > 0) s(R.string.placement_move_left) else s(R.string.placement_move_right) val icon = if (lrDiff > 0) "arrow.left" else "arrow.right" return PlacementGuideResult(hint, icon, 45, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL) @@ -1976,13 +1993,13 @@ private fun computePlacementGuide( val navelMissing = 0 in missingCenters // ch0 = 배꼽 쪽 (top of sensor) val (hint, icon) = when { missingCenters.size >= 2 -> - Pair("Adjust position — multiple channels missing", "arrow.up") + Pair(s(R.string.placement_adjust_multiple), "arrow.up") pubicMissing -> - Pair("Slide up slightly ↑ (ch3 missing)", "arrow.up") + Pair(s(R.string.placement_slide_up_ch3), "arrow.up") navelMissing -> - Pair("Slide down slightly ↓ (ch0 missing)", "arrow.down") + Pair(s(R.string.placement_slide_down_ch0), "arrow.down") else -> - Pair("Adjust position slightly — center channel missing", "arrow.up") + Pair(s(R.string.placement_adjust_center), "arrow.up") } return PlacementGuideResult(hint, icon, 30, phase = PlacementPhase.GREEN, nextPhase = PlacementPhase.VERTICAL) @@ -2041,7 +2058,7 @@ private fun computePlacementGuide( if (isPass) { val score = ((1.0 - cv / cvThr).coerceIn(0.0, 1.0) * 100).toInt().coerceAtLeast(70) return PlacementGuideResult( - "In position!", + s(R.string.placement_in_position), "checkmark.circle", score, cv, true, lrDev, lrPass, phase = PlacementPhase.GREEN, nextPhase = PlacementPhase.GREEN) } else { @@ -2059,33 +2076,33 @@ private fun computePlacementGuide( when { // ch4/ch5 중 하나만 빠짐 → 반대 방향 안내 !lrPass && !ch4det && ch5det -> { - adjustHint = "Slide left ←"; adjustIcon = "arrow.left" + adjustHint = s(R.string.placement_slide_left); adjustIcon = "arrow.left" } !lrPass && ch4det && !ch5det -> { - adjustHint = "Slide right →"; adjustIcon = "arrow.right" + adjustHint = s(R.string.placement_slide_right); adjustIcon = "arrow.right" } !lrPass && !ch4det && !ch5det -> { - adjustHint = "Adjust left or right"; adjustIcon = "arrow.right" + adjustHint = s(R.string.placement_adjust_lr); adjustIcon = "arrow.right" } !lrPass && lrDev > DEFAULT_LR_DEV_THR -> { - adjustHint = "Slide left ←"; adjustIcon = "arrow.left" + adjustHint = s(R.string.placement_slide_left); adjustIcon = "arrow.left" } !lrPass && lrDev < -DEFAULT_LR_DEV_THR -> { - adjustHint = "Slide right →"; adjustIcon = "arrow.right" + adjustHint = s(R.string.placement_slide_right); adjustIcon = "arrow.right" } // Edge 비대칭 — ch3 짧음 (센서 너무 아래) → 위로 미세 조정 !edgePass && edgeAsym > 0 -> { - adjustHint = "Slide up slightly ↑"; adjustIcon = "arrow.up" + adjustHint = s(R.string.placement_slide_up_slight); adjustIcon = "arrow.up" } // Edge 비대칭 — ch0 짧음 (센서 너무 위) → 아래로 미세 조정 !edgePass && edgeAsym < 0 -> { - adjustHint = "Slide down slightly ↓"; adjustIcon = "arrow.down" + adjustHint = s(R.string.placement_slide_down_slight); adjustIcon = "arrow.down" } gradient > 0.3 -> { - adjustHint = "Slide down ↓"; adjustIcon = "arrow.down" + adjustHint = s(R.string.placement_slide_down); adjustIcon = "arrow.down" } gradient < -0.3 -> { - adjustHint = "Slide up ↑"; adjustIcon = "arrow.up" + adjustHint = s(R.string.placement_slide_up); adjustIcon = "arrow.up" } else -> { // 방향 애매 + CV 미달 → ch별 len 분석으로 방향 제시 @@ -2096,13 +2113,13 @@ private fun computePlacementGuide( // 최소 채널이 최대의 60% 미만 → 약한 쪽으로 안내 if (minCh <= 1) { // ch0/ch1 약함 → 위로 - adjustHint = "Slide up slightly ↑"; adjustIcon = "arrow.up" + adjustHint = s(R.string.placement_slide_up_slight); adjustIcon = "arrow.up" } else { // ch2/ch3 약함 → 아래로 - adjustHint = "Slide down slightly ↓"; adjustIcon = "arrow.down" + adjustHint = s(R.string.placement_slide_down_slight); adjustIcon = "arrow.down" } } else { - adjustHint = "Optimizing"; adjustIcon = "checkmark.circle" + adjustHint = s(R.string.placement_optimizing); adjustIcon = "checkmark.circle" } } } diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/VivamyoMonitoringView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/VivamyoMonitoringView.kt index 349d91b..f3f2f02 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/VivamyoMonitoringView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/VivamyoMonitoringView.kt @@ -25,10 +25,12 @@ import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext +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 import com.example.medilightv2android.AppState +import com.example.medilightv2android.R import com.example.medilightv2android.ble.BleManager import com.example.medilightv2android.managers.NirsManager import com.example.medilightv2android.models.NirsMcjPacket @@ -195,16 +197,16 @@ fun VivamyoMonitoringView(appState: AppState) { if (showClearConfirm) { AlertDialog( onDismissRequest = { showClearConfirm = false }, - title = { Text("Clear all data?") }, - text = { Text("All collected NIRS data will be permanently deleted.") }, + title = { Text(stringResource(R.string.vivamyo_clear_title)) }, + text = { Text(stringResource(R.string.vivamyo_clear_message)) }, confirmButton = { TextButton(onClick = { nirsManager.clearData() showClearConfirm = false - }) { Text("Clear Data", color = Color.Red) } + }) { Text(stringResource(R.string.vivamyo_clear_button), color = Color.Red) } }, dismissButton = { - TextButton(onClick = { showClearConfirm = false }) { Text("Cancel") } + TextButton(onClick = { showClearConfirm = false }) { Text(stringResource(R.string.cancel)) } } ) } @@ -215,10 +217,10 @@ fun VivamyoMonitoringView(appState: AppState) { else -> Color.Green } val stO2StatusText = when { - currentOxy.stO2 < 50 -> "Low" - currentOxy.stO2 < 60 -> "Caution" - currentOxy.stO2 <= 85 -> "Normal" - else -> "Check" + currentOxy.stO2 < 50 -> context.getString(R.string.vivamyo_status_low) + currentOxy.stO2 < 60 -> context.getString(R.string.vivamyo_status_caution) + currentOxy.stO2 <= 85 -> context.getString(R.string.vivamyo_status_normal) + else -> context.getString(R.string.vivamyo_status_check) } Column( @@ -236,11 +238,11 @@ fun VivamyoMonitoringView(appState: AppState) { nirsManager.reset() appState.goHome() }, modifier = Modifier.size(48.dp)) { - Icon(Icons.Default.Home, "Home", tint = MlPrimary, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp)) + Icon(Icons.Default.Home, stringResource(R.string.vivamyo_home_desc), tint = MlPrimary, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp)) } Column(modifier = Modifier.padding(start = if (isCompact) 4.dp else 8.dp)) { - Text("Vivamyo", fontSize = if (isCompact) 16.sp else 20.sp, fontWeight = FontWeight.Bold) - if (!isCompact) Text("NIRS Monitoring", fontSize = 13.sp, color = MlSecondaryText) + Text(stringResource(R.string.vivamyo_title), fontSize = if (isCompact) 16.sp else 20.sp, fontWeight = FontWeight.Bold) + if (!isCompact) Text(stringResource(R.string.vivamyo_subtitle), fontSize = 13.sp, color = MlSecondaryText) } Spacer(modifier = Modifier.weight(1f)) if (isDemoMode) { @@ -283,9 +285,9 @@ fun VivamyoMonitoringView(appState: AppState) { NirsMetricsCard(nirsManager) Row(modifier = Modifier.fillMaxWidth()) { - Text("Samples: $sampleCount", fontSize = 11.sp, color = MlSecondaryText) + Text(stringResource(R.string.vivamyo_samples_label, sampleCount), fontSize = 11.sp, color = MlSecondaryText) Spacer(modifier = Modifier.weight(1f)) - Text("4Hz | 60s window", fontSize = 11.sp, color = MlSecondaryText) + Text(stringResource(R.string.vivamyo_window_label), fontSize = 11.sp, color = MlSecondaryText) } } else { // Oxy/Deoxy tab @@ -293,14 +295,14 @@ fun VivamyoMonitoringView(appState: AppState) { LineChartCard("HHb (DeOxy)", nirsManager.oxyHbHistory, Color.Blue, "uM") Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Text("Samples: $sampleCount", fontSize = 11.sp, color = MlSecondaryText) + Text(stringResource(R.string.vivamyo_samples_label, sampleCount), fontSize = 11.sp, color = MlSecondaryText) Spacer(modifier = Modifier.weight(1f)) if (isFilterEnabled) { - Text("Filtered", fontSize = 10.sp, fontWeight = FontWeight.Bold, color = Color.White, + Text(stringResource(R.string.vivamyo_filtered), fontSize = 10.sp, fontWeight = FontWeight.Bold, color = Color.White, modifier = Modifier.clip(RoundedCornerShape(4.dp)).background(Color(0xFF9966FF)).padding(horizontal = 6.dp, vertical = 2.dp)) Spacer(modifier = Modifier.width(4.dp)) } - Text("4Hz | 60s window", fontSize = 11.sp, color = MlSecondaryText) + Text(stringResource(R.string.vivamyo_window_label), fontSize = 11.sp, color = MlSecondaryText) } NirsMetricsCard(nirsManager) @@ -321,7 +323,7 @@ fun VivamyoMonitoringView(appState: AppState) { Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Default.Delete, null, tint = Color.Red) Spacer(modifier = Modifier.width(4.dp)) - Text("Clear", fontWeight = FontWeight.SemiBold, color = Color.Red) + Text(stringResource(R.string.vivamyo_clear), fontWeight = FontWeight.SemiBold, color = Color.Red) } } @@ -346,7 +348,7 @@ fun VivamyoMonitoringView(appState: AppState) { null, tint = Color.White ) Spacer(modifier = Modifier.width(4.dp)) - Text(if (isStreaming) "Stop" else "Start", fontWeight = FontWeight.SemiBold, color = Color.White) + Text(if (isStreaming) stringResource(R.string.vivamyo_stop) else stringResource(R.string.vivamyo_start), fontWeight = FontWeight.SemiBold, color = Color.White) } } } @@ -424,7 +426,7 @@ private fun LineChartCard(label: String, data: List, color: Color, unit: Box( modifier = Modifier.fillMaxWidth().height(140.dp), contentAlignment = Alignment.Center - ) { Text("No data", fontSize = 11.sp, color = MlSecondaryText) } + ) { Text(stringResource(R.string.vivamyo_no_data), fontSize = 11.sp, color = MlSecondaryText) } } else { val minVal = data.min() val maxVal = data.max() diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/onboarding/OnboardingView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/onboarding/OnboardingView.kt index bcbd7f4..e0bca0a 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/onboarding/OnboardingView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/onboarding/OnboardingView.kt @@ -11,11 +11,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush 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 com.example.medilightv2android.AppState +import com.example.medilightv2android.R import com.example.medilightv2android.ui.components.BladdyIdleRiveView import com.example.medilightv2android.ui.theme.* @@ -34,7 +36,7 @@ fun OnboardingView(appState: AppState) { Spacer(modifier = Modifier.height(24.dp)) Text( - text = "Welcome to VesiScan-Basic", + text = stringResource(R.string.onboarding_welcome), fontSize = 24.sp, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center @@ -43,7 +45,7 @@ fun OnboardingView(appState: AppState) { Spacer(modifier = Modifier.height(12.dp)) Text( - text = "Smart bladder monitoring\nfor better quality of life", + text = stringResource(R.string.onboarding_subtitle), fontSize = 16.sp, color = MlSecondaryText, textAlign = TextAlign.Center @@ -71,7 +73,7 @@ fun OnboardingView(appState: AppState) { ), contentAlignment = Alignment.Center ) { - Text("Get Started", fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = Color.White) + Text(stringResource(R.string.onboarding_get_started), fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = Color.White) } } } diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/pin/ShuffledKeypad.kt b/app/src/main/java/com/example/medilightv2android/ui/views/pin/ShuffledKeypad.kt index f79a609..3902299 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/pin/ShuffledKeypad.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/pin/ShuffledKeypad.kt @@ -16,9 +16,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color +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 +import com.example.medilightv2android.R import com.example.medilightv2android.ui.theme.MlCardBackground @Composable @@ -55,7 +57,7 @@ fun ShuffledKeypad( ) { Icon( Icons.AutoMirrored.Filled.Backspace, - contentDescription = "Delete", + contentDescription = stringResource(R.string.pin_delete), modifier = Modifier.size(24.dp) ) } diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/sensorselect/SensorSelectView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/sensorselect/SensorSelectView.kt index 53b5191..b935c29 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/sensorselect/SensorSelectView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/sensorselect/SensorSelectView.kt @@ -18,10 +18,12 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +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 import com.example.medilightv2android.AppState +import com.example.medilightv2android.R import com.example.medilightv2android.models.SensorMode import com.example.medilightv2android.ui.components.BladdyIdleRiveView import com.example.medilightv2android.ui.theme.* @@ -40,11 +42,11 @@ fun SensorSelectView(appState: AppState) { Spacer(modifier = Modifier.height(32.dp)) - Text("Select Sensor", fontSize = 28.sp, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.sensor_select_title), fontSize = 28.sp, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.height(8.dp)) - Text("Choose your measurement mode", fontSize = 16.sp, color = MlSecondaryText) + Text(stringResource(R.string.sensor_select_subtitle), fontSize = 16.sp, color = MlSecondaryText) Spacer(modifier = Modifier.height(32.dp)) @@ -55,7 +57,7 @@ fun SensorSelectView(appState: AppState) { SensorCard( mode = SensorMode.NIRS, title = "NIRS", - description = "Near-Infrared Spectroscopy\nTissue oxygenation monitoring", + description = stringResource(R.string.sensor_nirs_desc), gradientColors = listOf(Color(0xFF00DBA1), Color(0xFF008FFF)), iconText = "NIRS", onClick = { appState.selectSensor(SensorMode.NIRS) } @@ -64,7 +66,7 @@ fun SensorSelectView(appState: AppState) { SensorCard( mode = SensorMode.PIEZO, title = "Piezo", - description = "Ultrasound Echo\nBladder volume measurement", + description = stringResource(R.string.sensor_piezo_desc), gradientColors = listOf(Color(0xFF8F44FC), Color(0xFFF04A8F)), iconText = "Piezo", onClick = { appState.selectSensor(SensorMode.PIEZO) } diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index c665f5e..46c3960 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -191,4 +191,175 @@ ← 왼쪽으로 살짝 → 오른쪽으로 살짝 ↔ 좌우로 천천히 움직여 보세요 + + + 앱 종료 + 앱을 종료하시겠습니까? + 종료 + + + 스마트 방광 모니터링 + 시작 + ● 연결됨 — 시작을 눌러 이어서 진행하세요 + 임상 모드 (개발자) + 개발자 모드 켜짐 + 개발자 모드 꺼짐 + 언어 + + + VesiScan-Basic 에 오신 것을 환영합니다 + 더 나은 삶의 질을 위한\n스마트 방광 모니터링 + 시작하기 + + + PIN 설정 + PIN 재입력 + PIN 입력 + PIN 이 일치하지 않습니다. 다시 시도해주세요. + 잘못된 PIN 입니다 + 지우기 + + + 센서 선택 + 측정 방식을 선택하세요 + 근적외선 분광법\n조직 산소화 모니터링 + 초음파 에코\n방광 용량 측정 + + + 초기화 중… + NIRS 보정 완료 + 기기 응답이 없습니다. 기기가 켜져 있고 가까이 있는지 확인해주세요. + NIRS 센서 켜는 중… + 센서 활성화 중… + 보정값 읽는 중… + 보정 실패 + 보정 완료 + NIRS 보정 중… + 건너뛰기 + 계속 + + + 기록 + 기록 %d건 + 기록 + 추이 + 측정 기록이 없습니다 + 단일 측정을 눌러 시작하세요 + 방광 레벨 + 추정 용량 + 도뇨 기록 + 레벨 %d 에서 + 측정 데이터가 없습니다 + 두 손가락으로 확대 • 점을 누르면 상세 정보 + 도뇨 + 측정 + 어제 + 오늘 + + + Vivamyo + NIRS 모니터링 + 모든 데이터를 지우시겠습니까? + 수집된 NIRS 데이터가 영구적으로 삭제됩니다. + 데이터 지우기 + 낮음 + 주의 + 정상 + 확인 필요 + 샘플: %d + 4Hz | 60초 윈도우 + 필터 적용 + 데이터 없음 + 지우기 + 중지 + 시작 + 홈 + + + 기록됨 + 카테터 추가 + 추가할 카테터 개수를 입력해주세요: + 개수 + 추가 + 방광 레벨을 초기화하시겠습니까? + 방광 레벨이 0 으로 초기화됩니다. 이 작업은 되돌릴 수 없습니다. + 도뇨 + 바로 저장 + 개 남음 + 연결 해제 + 연결 끊김 + 배뇨 / 도뇨 + 측정 중지 + 자동 측정 + 단일 측정 + 현재 측정값 + 위치를 놓쳤거나 젤이 부족합니다 + 위치가 어긋났거나 젤이 부족할 수 있습니다. 확인해주세요. + 다시 정렬 + 닫기 + 페어링 삭제 + 연결을 끊고 페어링을 삭제합니다. + 누움 + 서있음 + 걷는 중 + 감지 중 + 홈 + 위치 잡기 + 카테터 + 설정 + + + 센서 정렬 + VesiScan 을 치골 위에 놓고 시작 버튼을 누르세요 + 치골 바로 위에 센서를 위치시켜 주세요 + 배꼽 근처에 센서를 놓고 아래로 내려주세요 ↓ + 단계 %1$d/%2$d + 정렬 시작 + 정렬 완료 + 측정 시작 + 건너뛰기 + 제 위치입니다! + 위치를 놓쳤습니다. 치골 위에서 다시 시작해주세요. + 센서가 분리되었습니다. 치골 위에서 다시 시작해주세요. + 신호를 놓쳤습니다 — 위로 올려주세요 ↑ + 배꼽 근처에서 시작해 천천히 아래로 내려주세요 ↓ + 너무 멀어요 — 위로 올려주세요 ↑ + 위치 좋습니다! 최종 확인 중… + 왼쪽으로 이동 ← + 오른쪽으로 이동 → + 확인 중 + 아래로 살짝 내려주세요 ↓ + 아래로 내려주세요 ↓ + 더 아래로 내려주세요 ↓ + 잠시 그대로 두세요 + 위로 올려주세요 ↑ + 위로 살짝 올려주세요 ↑ + 최종 확인 중… + 센서를 왼쪽으로 ← + 센서를 오른쪽으로 → + 오른쪽으로 이동해보세요 → + 대칭이 맞습니다! 최종 확인 중… + 치골 위에서 다시 시작해주세요 + 위치를 조정해주세요 — 여러 채널 감지 안 됨 + 위로 살짝 올려주세요 ↑ (ch3 감지 안 됨) + 아래로 살짝 내려주세요 ↓ (ch0 감지 안 됨) + 위치를 살짝 조정해주세요 — 중앙 채널 감지 안 됨 + 좌우로 조정해주세요 + 최적화 중 + 뒤로 + 준비 완료 + 신체 실루엣 + + + 방광 용량 + 도뇨 알림 기준 + 자동 측정 간격 + 레벨 %d + 초 + 분 + 앱 + 기기 + 펌웨어 + 기록 및 그래프 + 개발자 도구 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0de9b36..6c576ab 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -191,4 +191,175 @@ ← Slightly to the left → Slightly to the right ↔ Move slowly side to side + + + Exit App + Are you sure you want to exit? + Exit + + + Smart Bladder Monitoring + Start + ● Connected — Start to resume + Clinical Mode (Dev) + Developer mode ON + Developer mode OFF + Language + + + Welcome to VesiScan-Basic + Smart bladder monitoring\nfor better quality of life + Get Started + + + Set PIN + Re-enter PIN + Enter PIN + PINs don\'t match. Try again. + Incorrect PIN + Delete + + + Select Sensor + Choose your measurement mode + Near-Infrared Spectroscopy\nTissue oxygenation monitoring + Ultrasound Echo\nBladder volume measurement + + + Initializing… + NIRS Calibrated + No response from device. Check the device is powered on and nearby. + Powering on NIRS transducer… + Activating sensor… + Reading gain calibration… + Calibration Failed + Calibration Complete + Calibrating NIRS… + Skip + Continue + + + History + %d records + History + Trends + No measurements yet + Tap Spot to start measuring + Bladder Level + Estimated Volume + Catheterization Log + at Lv.%d + No measurement data + Pinch to zoom • Tap point for details + Catheterization + Measurement + Yesterday + Today + + + Vivamyo + NIRS Monitoring + Clear all data? + All collected NIRS data will be permanently deleted. + Clear Data + Low + Caution + Normal + Check + Samples: %d + 4Hz | 60s window + Filtered + No data + Clear + Stop + Start + Home + + + Recorded + Add Catheters + Enter the number of catheters to add: + Count + Add + Reset bladder level? + This will reset the bladder level to 0. This action cannot be undone. + Catheterize + Quick Save + left + Disconnect + Disconnected + Voiding / Catheterization + Stop Scan + Auto Scan + Single Scan + Current Measurement + Position Lost or Insufficient Gel + Position lost or gel may be insufficient. Please check. + Realign + Dismiss + Delete Pairing + Disconnect and remove pairing. + Lying + Upright + Walking + Detecting + Home + Placement + Catheter + Settings + + + Sensor Alignment + Place VesiScan above the pubic bone, then press Start + Place the sensor just above the pubic bone + Place sensor near navel, slide down ↓ + Step %1$d/%2$d + Start Alignment + Complete Alignment + Start Scanning + Skip + In position! + Position lost. Restart above pubic bone. + Sensor detached. Restart above pubic bone. + Signal lost — slide up ↑ + Start near navel, slide down slowly ↓ + Too far — slide up ↑ + Position OK! Final check… + Slide left ← + Slide right → + Checking + Slide down slightly ↓ + Slide down ↓ + Slide further down ↓ + Hold still + Slide up ↑ + Slide up slightly ↑ + Final check… + Move sensor left ← + Move sensor right → + Try moving right → + Symmetry OK! Final check… + Start over from above the pubic bone + Adjust position — multiple channels missing + Slide up slightly ↑ (ch3 missing) + Slide down slightly ↓ (ch0 missing) + Adjust position slightly — center channel missing + Adjust left or right + Optimizing + Back + Ready + Body silhouette + + + Bladder Capacity + Catheter Threshold + Auto Scan Interval + Level %d + sec + min + App + Device + Firmware + Logs & Charts + Developer Tools