From 2cf4f4db2f8343f2e69feea43d52e9fac32dd8f5 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Fri, 26 Jun 2026 17:27:05 +0900 Subject: [PATCH] =?UTF-8?q?feat(locale):=20Phase=202=20=E2=80=94=20?= =?UTF-8?q?=EB=AA=A8=EB=93=A0=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EB=85=B8?= =?UTF-8?q?=EC=B6=9C=20Korean=20=EB=AC=B8=EC=9E=90=EC=97=B4=20=E2=86=92=20?= =?UTF-8?q?strings.xml=20=EC=B6=94=EC=B6=9C=20+=20=EC=98=81=EB=AC=B8=20?= =?UTF-8?q?=EB=B2=88=EC=97=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sub-agent 작업 결과: - values/strings.xml + values-ko/strings.xml 각각 71 → 194 줄 (약 85 신규 string) - 17 파일 수정 (UI views 9개, components 3개, managers 1개, services 2개) 상세: - ui/views/{home, consent, registration, reminder, monitoring (3), personalization, connection} - components/{BatteryWarningBanner, FirmwareWarningBanner, LockableSlider} - managers/AlignmentAdvisorV2 — Out/AdvisorState 에 msgKey + msgArgs 추가 (UI 가 context.getString 으로 localize) - services/{ReminderService, BleForegroundService} 의도적으로 skip: - ClinicalHomeView / ClinicalLiveView (R&D-only dev mode) - PiezoMonitoring dev panel labels (dev 도구) - AlignmentAdvice / 2-phase AlignGuide (UI 미사용) - 이미 영어인 파일들 (Onboarding, Pin, SensorSelect, Calibration 등) Co-Authored-By: Claude Opus 4.7 --- .../managers/AlignmentAdvisorV2.kt | 47 +++++-- .../services/BleForegroundService.kt | 4 +- .../services/ReminderService.kt | 21 ++- .../ui/components/BatteryWarningBanner.kt | 8 +- .../ui/components/FirmwareWarningBanner.kt | 6 +- .../ui/components/LockableSlider.kt | 4 +- .../ui/views/connection/DeviceScanView.kt | 10 +- .../ui/views/consent/ConsentView.kt | 22 ++-- .../ui/views/home/HomeView.kt | 14 +- .../views/monitoring/PiezoMonitoringView.kt | 24 ++-- .../ui/views/monitoring/PlacementGuideView.kt | 23 ++-- .../ui/views/monitoring/VoidingDiaryView.kt | 20 +-- .../PiezoPersonalizationView.kt | 8 +- .../ui/views/registration/RegistrationView.kt | 4 +- .../ui/views/reminder/ReminderSettingsView.kt | 58 +++++---- app/src/main/res/values-ko/strings.xml | 123 ++++++++++++++++++ app/src/main/res/values/strings.xml | 123 ++++++++++++++++++ 17 files changed, 409 insertions(+), 110 deletions(-) diff --git a/app/src/main/java/com/example/medilightv2android/managers/AlignmentAdvisorV2.kt b/app/src/main/java/com/example/medilightv2android/managers/AlignmentAdvisorV2.kt index b616090..6c86676 100644 --- a/app/src/main/java/com/example/medilightv2android/managers/AlignmentAdvisorV2.kt +++ b/app/src/main/java/com/example/medilightv2android/managers/AlignmentAdvisorV2.kt @@ -18,6 +18,7 @@ */ package com.example.medilightv2android.managers +import com.example.medilightv2android.R import com.example.medilightv2android.walldetect.MethodDResult import com.example.medilightv2android.walldetect.MethodDRunner import com.example.medilightv2android.walldetect.algo.methodd.MethodDParams @@ -32,6 +33,10 @@ data class AdvisorState( val state: String, // 'accum' 또는 'commit' val action: AlignAction?, val msg: String, + /** Localized message string resource id (null if no localized message). */ + val msgKey: Int? = null, + /** Args for msgKey (formatted into string). */ + val msgArgs: List = emptyList(), val dets: List?, val summary: AlignSummary?, val accumProgress: Int = 0, // 'accum' 일 때 (현재/총) @@ -161,7 +166,16 @@ class AlignGuide4Stage( ) { enum class Phase4 { INITIAL_ACCUM, VERTICAL_CLIMB, CH3_STABILIZE, LR_BALANCE } - data class Out(val action: AlignAction?, val msg: String, val phase: Phase4, val progress: Int = 0) + data class Out( + val action: AlignAction?, + val msg: String, + val phase: Phase4, + val progress: Int = 0, + /** Localized string resource id for user-facing UI (msg is dev/debug). */ + val msgKey: Int? = null, + /** Args for msgKey formatting. */ + val msgArgs: List = emptyList(), + ) var phase: Phase4 = Phase4.INITIAL_ACCUM private set @@ -191,7 +205,9 @@ class AlignGuide4Stage( initialCount++ if (initialCount < initialTarget) { return Out(null, "잠시 그대로 두세요 (${initialCount}/${initialTarget})", - phase, initialCount) + phase, initialCount, + msgKey = R.string.align_hold_still, + msgArgs = listOf(initialCount, initialTarget)) } phase = Phase4.VERTICAL_CLIMB } @@ -200,7 +216,8 @@ class AlignGuide4Stage( if (phase == Phase4.VERTICAL_CLIMB) { if (!s.ch3) { return Out(AlignAction.MOVE_UP, "↑ 위로 조금씩 올리세요", - phase, 0) + phase, 0, + msgKey = R.string.align_move_up) } phase = Phase4.CH3_STABILIZE stabilizeCount = 0 @@ -216,12 +233,15 @@ class AlignGuide4Stage( phase = Phase4.VERTICAL_CLIMB stabilizeCount = 0 return Out(AlignAction.MOVE_UP, "↑ 살짝 놓침 — 다시 위로", - phase, 0) + phase, 0, + msgKey = R.string.align_recover_up) } if (stabilizeCount < stabilizeTarget) { return Out(AlignAction.STOP, "✓ 좋아요! 그 위치 유지 (${stabilizeCount}/${stabilizeTarget})", - phase, stabilizeCount) + phase, stabilizeCount, + msgKey = R.string.align_hold_position, + msgArgs = listOf(stabilizeCount, stabilizeTarget)) } phase = Phase4.LR_BALANCE } @@ -235,7 +255,10 @@ class AlignGuide4Stage( val recoveryDir = if (3 in lost) AlignAction.MOVE_UP else AlignAction.MOVE_DOWN val arrow = if (recoveryDir == AlignAction.MOVE_UP) "↑" else "↓" val dirWord = if (recoveryDir == AlignAction.MOVE_UP) "위" else "아래" - return Out(recoveryDir, "$arrow 위치 살짝 벗어남 — ${dirWord}로 약간", phase, 0) + val recoveryKey = if (recoveryDir == AlignAction.MOVE_UP) + R.string.align_recover_up_short else R.string.align_recover_down_short + return Out(recoveryDir, "$arrow 위치 살짝 벗어남 — ${dirWord}로 약간", phase, 0, + msgKey = recoveryKey) } // full set 유지 중 — 좌우 균형 (deadband + consecutive 악화 로 oscillation 차단) @@ -245,7 +268,8 @@ class AlignGuide4Stage( val imbal = kotlin.math.abs(u4 - u5) if (imbal <= latTol) { return Out(AlignAction.STOP, - "✓ 정렬 완료! 그대로 측정 가능", phase, 0) + "✓ 정렬 완료! 그대로 측정 가능", phase, 0, + msgKey = R.string.align_complete) } if (lateralDir == null) { // 첫 진입 — u4/u5 비교로 방향 추정 @@ -270,10 +294,14 @@ class AlignGuide4Stage( lateralImbal = imbal val arrow = if (lateralDir == AlignAction.MOVE_LEFT) "←" else "→" val sideWord = if (lateralDir == AlignAction.MOVE_LEFT) "왼쪽" else "오른쪽" - return Out(lateralDir!!, "$arrow ${sideWord}으로 살짝", phase, 0) + val sideKey = if (lateralDir == AlignAction.MOVE_LEFT) + R.string.align_left else R.string.align_right + return Out(lateralDir!!, "$arrow ${sideWord}으로 살짝", phase, 0, + msgKey = sideKey) } // u4/u5 미검출 → probe - return Out(AlignAction.PROBE_LR, "↔ 좌우로 천천히 움직여 보세요", phase, 0) + return Out(AlignAction.PROBE_LR, "↔ 좌우로 천천히 움직여 보세요", phase, 0, + msgKey = R.string.align_probe_lr) } } @@ -329,6 +357,7 @@ class RollingAligner( val state = if (out.phase == AlignGuide4Stage.Phase4.INITIAL_ACCUM) "accum" else "commit" val st = AdvisorState( state = state, action = out.action, msg = out.msg, + msgKey = out.msgKey, msgArgs = out.msgArgs, dets = dets, summary = summary, accumProgress = if (state == "accum") out.progress else accumK, accumTarget = accumK, diff --git a/app/src/main/java/com/example/medilightv2android/services/BleForegroundService.kt b/app/src/main/java/com/example/medilightv2android/services/BleForegroundService.kt index f0cccde..9177723 100644 --- a/app/src/main/java/com/example/medilightv2android/services/BleForegroundService.kt +++ b/app/src/main/java/com/example/medilightv2android/services/BleForegroundService.kt @@ -108,7 +108,7 @@ class BleForegroundService : Service() { "VesiScan BLE Connection", NotificationManager.IMPORTANCE_LOW ).apply { - description = "BLE 기기 연결 유지" + description = getString(R.string.ble_service_channel_desc) setShowBadge(false) } val nm = getSystemService(NotificationManager::class.java) @@ -127,7 +127,7 @@ class BleForegroundService : Service() { return NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("VesiScan") - .setContentText("BLE 기기 연결 중...") + .setContentText(getString(R.string.ble_service_connecting)) .setSmallIcon(R.drawable.ic_launcher_foreground) .setOngoing(true) .setContentIntent(pendingIntent) diff --git a/app/src/main/java/com/example/medilightv2android/services/ReminderService.kt b/app/src/main/java/com/example/medilightv2android/services/ReminderService.kt index 92949d2..9dc403f 100644 --- a/app/src/main/java/com/example/medilightv2android/services/ReminderService.kt +++ b/app/src/main/java/com/example/medilightv2android/services/ReminderService.kt @@ -29,7 +29,6 @@ class ReminderService private constructor(private val context: Context) { } private const val CHANNEL_ID = "catheter_reminder" - private const val CHANNEL_NAME = "배뇨 알림" private const val NOTIFICATION_ID = 2001 private const val REQUEST_CODE = 3001 private const val PREFS_NAME = "reminder_prefs" @@ -148,19 +147,19 @@ class ReminderService private constructor(private val context: Context) { alarmManager.cancel(pendingIntent) } - private val reminderMessages = listOf( - "배뇨 시간입니다! 도뇨 준비를 해주세요.", - "규칙적인 배뇨는 건강의 기본입니다!", - "화장실에 다녀오세요! 💧", - "배뇨 시간이에요. 건강한 습관을 유지하세요!", - "잊지 마세요! 화장실 갈 시간입니다." + private val reminderMessageIds = listOf( + R.string.reminder_msg_1, + R.string.reminder_msg_2, + R.string.reminder_msg_3, + R.string.reminder_msg_4, + R.string.reminder_msg_5, ) fun showNotification() { val builder = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(R.drawable.ic_launcher_foreground) - .setContentTitle("VesiScan 알림") - .setContentText(reminderMessages.random()) + .setContentTitle(context.getString(R.string.notification_title)) + .setContentText(context.getString(reminderMessageIds.random())) .setPriority(NotificationCompat.PRIORITY_HIGH) .setAutoCancel(true) @@ -198,9 +197,9 @@ class ReminderService private constructor(private val context: Context) { private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( - CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH + CHANNEL_ID, context.getString(R.string.notification_channel_name), NotificationManager.IMPORTANCE_HIGH ).apply { - description = "배뇨 알리미 알림" + description = context.getString(R.string.notification_channel_desc) enableVibration(true) } val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager diff --git a/app/src/main/java/com/example/medilightv2android/ui/components/BatteryWarningBanner.kt b/app/src/main/java/com/example/medilightv2android/ui/components/BatteryWarningBanner.kt index 50d9678..8ad175f 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/components/BatteryWarningBanner.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/components/BatteryWarningBanner.kt @@ -18,9 +18,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip 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.ble.BleManager /** @@ -41,11 +43,11 @@ fun BatteryWarningBanner(bleManager: BleManager, modifier: Modifier = Modifier) val critical = batt <= CRITICAL_BATTERY_THRESHOLD val bgColor = if (critical) Color(0xFFD32F2F) else Color(0xFFFF9500) - val title = if (critical) "기기 배터리 위급" else "기기 배터리 부족" + val title = if (critical) stringResource(R.string.battery_critical_title) else stringResource(R.string.battery_low_title) val subtitle = if (critical) - "현재 ${batt}% · 측정이 갑자기 중단될 수 있습니다. 즉시 충전하세요." + stringResource(R.string.battery_critical_desc, batt) else - "현재 ${batt}% · 곧 충전이 필요합니다." + stringResource(R.string.battery_low_desc, batt) Row( modifier = modifier diff --git a/app/src/main/java/com/example/medilightv2android/ui/components/FirmwareWarningBanner.kt b/app/src/main/java/com/example/medilightv2android/ui/components/FirmwareWarningBanner.kt index 3f286db..2dd35fc 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/components/FirmwareWarningBanner.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/components/FirmwareWarningBanner.kt @@ -22,9 +22,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip 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.ble.BleManager import kotlinx.coroutines.delay @@ -75,11 +77,11 @@ fun FirmwareWarningBanner(bleManager: BleManager, modifier: Modifier = Modifier) if (!outdated && !showTimedOut) return val bgColor = if (outdated) Color(0xFFFF9500) else Color(0xFFD32F2F) - val title = if (outdated) "펌웨어 업데이트 필요" else "기기 응답 없음" + val title = if (outdated) stringResource(R.string.firmware_outdated_title) else stringResource(R.string.firmware_no_response_title) // timedOut(mid? 미수신) 케이스는 사용자에게 mid?/펌웨어 호환 같은 기술 용어가 무의미. // outdated 일 때만 상세 안내, timedOut 은 타이틀만 노출. val subtitle: String? = if (outdated) { - "현재: $fw · 필요: VBTFW${"%04d".format(BleManager.MIN_FIRMWARE_BUILD)} 이상. 측정이 정상 동작하지 않을 수 있습니다." + stringResource(R.string.firmware_outdated_desc, fw, BleManager.MIN_FIRMWARE_BUILD) } else { null } diff --git a/app/src/main/java/com/example/medilightv2android/ui/components/LockableSlider.kt b/app/src/main/java/com/example/medilightv2android/ui/components/LockableSlider.kt index f552806..81c31d5 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/components/LockableSlider.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/components/LockableSlider.kt @@ -37,7 +37,9 @@ 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.unit.dp +import com.example.medilightv2android.R @Composable fun LockableSlider( @@ -57,7 +59,7 @@ fun LockableSlider( ) { Icon( if (unlocked) Icons.Default.LockOpen else Icons.Default.Lock, - contentDescription = if (unlocked) "잠금 해제됨 — 다시 탭하여 잠금" else "잠김 — 탭하여 해제 후 조정", + contentDescription = if (unlocked) stringResource(R.string.slider_unlocked_desc) else stringResource(R.string.slider_locked_desc), tint = if (unlocked) Color(0xFF4CAF50) else Color.Gray.copy(alpha = 0.5f), modifier = Modifier.size(18.dp) ) diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/connection/DeviceScanView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/connection/DeviceScanView.kt index 57dfdfc..5a7ec5e 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/connection/DeviceScanView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/connection/DeviceScanView.kt @@ -29,11 +29,13 @@ import androidx.compose.ui.draw.shadow 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.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat import com.example.medilightv2android.AppState +import com.example.medilightv2android.R import com.example.medilightv2android.ble.BleDevice import com.example.medilightv2android.ble.BleManager import com.example.medilightv2android.models.SensorMode @@ -342,17 +344,17 @@ fun DeviceScanView(appState: AppState) { pendingDelete?.let { target -> AlertDialog( onDismissRequest = { pendingDelete = null }, - title = { Text("페어링 해제") }, - text = { Text("${target.name} 의 페어링을 해제합니다. 다시 사용하려면 PIN 재입력이 필요합니다.") }, + title = { Text(stringResource(R.string.unpair_title)) }, + text = { Text(stringResource(R.string.unpair_message, target.name)) }, confirmButton = { TextButton(onClick = { bleManager.unbondAndRemoveAddress(target.address) knownDevices = KnownDeviceStore.getAll(context) pendingDelete = null - }) { Text("해제", color = Color.Red) } + }) { Text(stringResource(R.string.unpair_confirm), color = Color.Red) } }, dismissButton = { - TextButton(onClick = { pendingDelete = null }) { Text("취소") } + TextButton(onClick = { pendingDelete = null }) { Text(stringResource(R.string.cancel)) } } ) } diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/consent/ConsentView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/consent/ConsentView.kt index 3e0c61f..f70a2c4 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/consent/ConsentView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/consent/ConsentView.kt @@ -13,7 +13,9 @@ import androidx.compose.ui.Modifier 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 com.example.medilightv2android.R import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.medilightv2android.AppState @@ -47,13 +49,13 @@ fun ConsentView(appState: AppState) { Spacer(modifier = Modifier.height(16.dp)) Text( - "이용 동의", + stringResource(R.string.consent_title), fontSize = 24.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(horizontal = 32.dp) ) Text( - "서비스 이용을 위해 아래 항목에 동의해 주세요.", + stringResource(R.string.consent_intro), fontSize = 14.sp, color = MlSecondaryText, modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp) @@ -91,28 +93,28 @@ fun ConsentView(appState: AppState) { colors = CheckboxDefaults.colors(checkedColor = MlPrimary) ) Spacer(modifier = Modifier.width(8.dp)) - Text("전체 동의", fontSize = 16.sp, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.consent_agree_all), fontSize = 16.sp, fontWeight = FontWeight.Bold) } HorizontalDivider(color = MlSecondaryText.copy(alpha = 0.2f)) ConsentItem( - title = "[필수] 개인정보 수집 및 이용 동의", - description = "이름, 나이, 성별, 신체정보(키·체중)를 앱 내 기능 제공 목적으로 수집합니다. 수집된 정보는 기기에만 저장되며 외부로 전송되지 않습니다.", + title = stringResource(R.string.consent_personal_title), + description = stringResource(R.string.consent_personal_desc), checked = consentPersonal, onCheckedChange = { consentPersonal = it } ) ConsentItem( - title = "[필수] 의료정보 활용 동의", - description = "방광 측정 데이터, 배뇨일지, NIRS 산소포화도 등 건강 관련 데이터를 모니터링 및 기록 목적으로 활용합니다.", + title = stringResource(R.string.consent_medical_title), + description = stringResource(R.string.consent_medical_desc), checked = consentMedical, onCheckedChange = { consentMedical = it } ) ConsentItem( - title = "[필수] 데이터 저장 동의", - description = "측정 기록 및 배뇨일지 데이터가 기기 내부 저장소에 저장됩니다. 앱 삭제 시 모든 데이터가 삭제됩니다.", + title = stringResource(R.string.consent_data_title), + description = stringResource(R.string.consent_data_desc), checked = consentData, onCheckedChange = { consentData = it } ) @@ -149,7 +151,7 @@ fun ConsentView(appState: AppState) { contentAlignment = Alignment.Center ) { Text( - "동의하고 계속하기", + stringResource(R.string.consent_continue), fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = Color.White 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 d385290..91ee70b 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 @@ -26,7 +26,9 @@ 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 com.example.medilightv2android.R import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.medilightv2android.AppState @@ -167,13 +169,13 @@ fun HomeView(appState: AppState) { var selected by remember { mutableStateOf(curMode) } AlertDialog( onDismissRequest = { showLanguageDialog = false }, - title = { Text("Language / 언어") }, + title = { Text(stringResource(R.string.language_dialog_title)) }, text = { Column { listOf( - com.example.medilightv2android.services.LocalePref.Mode.SYSTEM to "시스템 기본 / System default", - com.example.medilightv2android.services.LocalePref.Mode.KO to "한국어", - com.example.medilightv2android.services.LocalePref.Mode.EN to "English", + com.example.medilightv2android.services.LocalePref.Mode.SYSTEM to stringResource(R.string.language_system_default), + com.example.medilightv2android.services.LocalePref.Mode.KO to stringResource(R.string.language_korean), + com.example.medilightv2android.services.LocalePref.Mode.EN to stringResource(R.string.language_english), ).forEach { (mode, label) -> Row( verticalAlignment = Alignment.CenterVertically, @@ -198,10 +200,10 @@ fun HomeView(appState: AppState) { showLanguageDialog = false // Activity recreate → attachBaseContext 다시 호출 → 새 locale 적용 (context as? android.app.Activity)?.recreate() - }) { Text("OK") } + }) { Text(stringResource(R.string.confirm)) } }, dismissButton = { - TextButton(onClick = { showLanguageDialog = false }) { Text("Cancel") } + TextButton(onClick = { showLanguageDialog = false }) { Text(stringResource(R.string.cancel)) } } ) } 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 e997a7e..c78f62d 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 @@ -175,17 +175,17 @@ fun PiezoMonitoringView(appState: AppState) { Icon(Icons.Default.Warning, contentDescription = null, tint = Color.White, modifier = Modifier.size(56.dp)) Spacer(modifier = Modifier.height(12.dp)) - Text("환자가 일어났습니다", fontSize = 28.sp, + Text(stringResource(R.string.alarm_patient_woke_up), fontSize = 28.sp, fontWeight = FontWeight.ExtraBold, color = Color.White, textAlign = TextAlign.Center) Spacer(modifier = Modifier.height(6.dp)) - Text("Patient is getting up", fontSize = 16.sp, + Text(stringResource(R.string.alarm_patient_woke_up_en), fontSize = 16.sp, fontWeight = FontWeight.Medium, color = Color.White.copy(alpha = 0.9f), textAlign = TextAlign.Center) Spacer(modifier = Modifier.height(8.dp)) val mins = wakeupAlarmElapsedSec / 60 val secs = wakeupAlarmElapsedSec % 60 - Text("누운 시간 ${mins}분 ${secs}초", + Text(stringResource(R.string.alarm_lying_duration, mins, secs), fontSize = 14.sp, color = Color.White.copy(alpha = 0.85f)) Spacer(modifier = Modifier.height(24.dp)) Button( @@ -196,7 +196,7 @@ fun PiezoMonitoringView(appState: AppState) { modifier = Modifier.fillMaxWidth().height(56.dp), shape = RoundedCornerShape(14.dp) ) { - Text("확인", fontSize = 20.sp, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.confirm), fontSize = 20.sp, fontWeight = FontWeight.Bold) } } } @@ -223,16 +223,16 @@ fun PiezoMonitoringView(appState: AppState) { Icon(Icons.Default.DirectionsWalk, contentDescription = null, tint = Color.White, modifier = Modifier.size(64.dp)) Spacer(modifier = Modifier.height(12.dp)) - Text("환자가 걷고 있습니다", fontSize = 30.sp, + Text(stringResource(R.string.alarm_patient_walking), fontSize = 30.sp, fontWeight = FontWeight.ExtraBold, color = Color.White, textAlign = TextAlign.Center) Spacer(modifier = Modifier.height(6.dp)) - Text("Patient is walking — fall risk", + Text(stringResource(R.string.alarm_patient_walking_en), fontSize = 16.sp, fontWeight = FontWeight.Medium, color = Color.White.copy(alpha = 0.92f), textAlign = TextAlign.Center) Spacer(modifier = Modifier.height(8.dp)) - Text("즉시 환자 상태 확인 필요", + Text(stringResource(R.string.alarm_immediate_check), fontSize = 14.sp, color = Color.White.copy(alpha = 0.9f), textAlign = TextAlign.Center) Spacer(modifier = Modifier.height(24.dp)) @@ -244,7 +244,7 @@ fun PiezoMonitoringView(appState: AppState) { modifier = Modifier.fillMaxWidth().height(56.dp), shape = RoundedCornerShape(14.dp) ) { - Text("확인", fontSize = 20.sp, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.confirm), fontSize = 20.sp, fontWeight = FontWeight.Bold) } } } @@ -856,7 +856,7 @@ fun PiezoMonitoringView(appState: AppState) { ) } - // ��뇨 기록 입력 방식 선택 (iOS DiaryRecordSheet 스타일) + // 배뇨 기록 입력 방식 선택 (iOS DiaryRecordSheet 스타일) if (showCatheterizeSheet) { AlertDialog( onDismissRequest = { showCatheterizeSheet = false }, @@ -868,7 +868,7 @@ fun PiezoMonitoringView(appState: AppState) { "${appState.estimatedVolume} mL", fontSize = 28.sp, fontWeight = FontWeight.Bold, color = MlPrimary ) - Text("추정 용량", fontSize = 12.sp, color = MlSecondaryText) + Text(stringResource(R.string.estimated_volume), fontSize = 12.sp, color = MlSecondaryText) } }, text = { @@ -1926,8 +1926,8 @@ fun PiezoMonitoringView(appState: AppState) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.weight(1f)) { - Text("기상 알람", fontSize = labelSize, fontWeight = FontWeight.SemiBold) - Text("일어남 감지 시 빨간 팝업", + Text(stringResource(R.string.wakeup_alarm_title), fontSize = labelSize, fontWeight = FontWeight.SemiBold) + Text(stringResource(R.string.wakeup_alarm_desc), fontSize = 11.sp, color = MlSecondaryText) } Switch( 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 8a3aaea..cd578c1 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 @@ -35,6 +35,7 @@ import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString @@ -45,6 +46,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.medilightv2android.AppScreen import com.example.medilightv2android.AppState +import com.example.medilightv2android.R import com.example.medilightv2android.ble.BleManager import com.example.medilightv2android.ble.PiezoChannelData import com.example.medilightv2android.managers.DetachmentDetection @@ -202,15 +204,15 @@ fun PlacementGuideView(appState: AppState) { Icon(Icons.Default.Warning, contentDescription = null, tint = Color.White, modifier = Modifier.size(56.dp)) Spacer(modifier = Modifier.height(12.dp)) - Text("센서가 분리되었습니다", fontSize = 26.sp, + Text(stringResource(R.string.sensor_detached_title), fontSize = 26.sp, fontWeight = FontWeight.ExtraBold, color = Color.White, textAlign = TextAlign.Center) Spacer(modifier = Modifier.height(6.dp)) - Text("Sensor detached", fontSize = 16.sp, + Text(stringResource(R.string.sensor_detached_subtitle), fontSize = 16.sp, fontWeight = FontWeight.Medium, color = Color.White.copy(alpha = 0.9f), textAlign = TextAlign.Center) Spacer(modifier = Modifier.height(8.dp)) - Text("치골 위쪽에 다시 위치시켜 주세요", + Text(stringResource(R.string.sensor_detached_desc), fontSize = 14.sp, color = Color.White.copy(alpha = 0.85f), textAlign = TextAlign.Center) Spacer(modifier = Modifier.height(24.dp)) @@ -222,7 +224,7 @@ fun PlacementGuideView(appState: AppState) { modifier = Modifier.fillMaxWidth().height(56.dp), shape = RoundedCornerShape(14.dp) ) { - Text("확인", fontSize = 20.sp, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.confirm), fontSize = 20.sp, fontWeight = FontWeight.Bold) } } } @@ -353,7 +355,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("Hold") || hint.contains("slowly") || hint.contains("Slightly")) { append(dots) } if (appState.isDevMode && !lastCv.isNaN()) { @@ -860,7 +863,7 @@ fun PlacementGuideView(appState: AppState) { // Clinical alignment session + waitingForStart 일 땐 알고리즘 skip — // baseline cycles 만 누적 (Start 누르기 전 자동 phase 진행 차단). if (waitingForStart) { - directionHint = "센서를 치골 위에 두고 Start 를 누르세요" + directionHint = context.getString(R.string.press_start_hint) directionIcon = "" placementScore = 0 } else { @@ -880,12 +883,16 @@ fun PlacementGuideView(appState: AppState) { } } bleManager.debugLogger.info("ALIGN_V2 phase=${v2Aligner.phase} state=${st.state} action=${st.action} u4=${st.summary?.u4} u5=${st.summary?.u5} ch3=${st.summary?.ch3} bestSet=${v2Aligner.bestCenterSet}") + // Localized message via string resource (falls back to st.msg for dev builds) + val localizedMsg = st.msgKey?.let { key -> + context.getString(key, *st.msgArgs.toTypedArray()) + } ?: st.msg if (st.state == "accum") { - directionHint = st.msg + directionHint = localizedMsg directionIcon = "" placementScore = 5 } else { - directionHint = st.msg + directionHint = localizedMsg // STOP 액션은 두 경우 — CH3_STABILIZE 의 "위치 유지" (still in progress) vs // LR_BALANCE 의 "정렬 완료" (truly done). 후자만 finalStop 으로 GREEN lock. val finalStop = st.action == com.example.medilightv2android.managers.AlignAction.STOP && diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/VoidingDiaryView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/VoidingDiaryView.kt index 23e428f..ab318f3 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/VoidingDiaryView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/VoidingDiaryView.kt @@ -235,16 +235,16 @@ private fun SummaryBar(records: List) { ) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { Icon(Icons.Default.WaterDrop, contentDescription = null, Modifier.size(14.dp), Color(0xFF2196F3)) - Text("${count}회", fontSize = 12.sp, color = Color(0xFF2196F3)) + Text(stringResource(R.string.count_times, count), fontSize = 12.sp, color = Color(0xFF2196F3)) } Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { Icon(Icons.Default.Science, contentDescription = null, Modifier.size(14.dp), Color(0xFF00BCD4)) - Text("${total} ml", fontSize = 12.sp, color = Color(0xFF00BCD4)) + Text(stringResource(R.string.volume_ml_short, total), fontSize = 12.sp, color = Color(0xFF00BCD4)) } if (count > 0) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { Icon(Icons.Default.BarChart, contentDescription = null, Modifier.size(14.dp), Color(0xFF4CAF50)) - Text("평균 ${avg} ml", fontSize = 12.sp, color = Color(0xFF4CAF50)) + Text(stringResource(R.string.average_ml, avg), fontSize = 12.sp, color = Color(0xFF4CAF50)) } } } @@ -258,20 +258,20 @@ private fun MedicalMetricsCard(metrics: DiaryMetrics) { modifier = Modifier.fillMaxWidth() ) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text("24시간 의료 지표", fontSize = 14.sp, fontWeight = FontWeight.Bold, color = MlPrimary) + Text(stringResource(R.string.medical_metrics_24h), fontSize = 14.sp, fontWeight = FontWeight.Bold, color = MlPrimary) HorizontalDivider(color = MlSecondaryText.copy(alpha = 0.15f)) Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { - MetricCell("24hUV", "${metrics.totalVolume24h} ml", Modifier.weight(1f)) - MetricCell("NUV", "${metrics.nocturnalVolume} ml", Modifier.weight(1f)) + MetricCell("24hUV", stringResource(R.string.volume_ml_short, metrics.totalVolume24h), Modifier.weight(1f)) + MetricCell("NUV", stringResource(R.string.volume_ml_short, metrics.nocturnalVolume), Modifier.weight(1f)) MetricCell("NPI", String.format("%.0f%%", metrics.npi * 100), Modifier.weight(1f), highlight = metrics.npi > 0.33) - MetricCell("Max", "${metrics.maxCapacity} ml", Modifier.weight(1f)) + MetricCell("Max", stringResource(R.string.volume_ml_short, metrics.maxCapacity), Modifier.weight(1f)) } Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { - MetricCell("총 횟수", "${metrics.totalCount}회", Modifier.weight(1f)) - MetricCell("주간", "${metrics.daytimeCount}회", Modifier.weight(1f)) - MetricCell("야간", "${metrics.nighttimeCount}회", Modifier.weight(1f), + MetricCell(stringResource(R.string.metric_total_count), stringResource(R.string.count_times, metrics.totalCount), Modifier.weight(1f)) + MetricCell(stringResource(R.string.metric_daytime), stringResource(R.string.count_times, metrics.daytimeCount), Modifier.weight(1f)) + MetricCell(stringResource(R.string.metric_nighttime), stringResource(R.string.count_times, metrics.nighttimeCount), Modifier.weight(1f), highlight = metrics.nighttimeCount >= 2) } } diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/personalization/PiezoPersonalizationView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/personalization/PiezoPersonalizationView.kt index 28e2d1d..2204543 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/personalization/PiezoPersonalizationView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/personalization/PiezoPersonalizationView.kt @@ -21,10 +21,12 @@ 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.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.ui.components.BladdyIdleRiveView import com.example.medilightv2android.ui.theme.* @@ -172,7 +174,7 @@ private fun CatheterThresholdCard(appState: AppState) { horizontalAlignment = Alignment.CenterHorizontally ) { Text("Voiding Reminder", fontSize = 28.sp, fontWeight = FontWeight.ExtraBold) - Text("방광이 이 정도 차오르면 알림", + Text(stringResource(R.string.voiding_reminder_threshold_desc), fontSize = 12.sp, color = MlSecondaryText, modifier = Modifier.padding(top = 2.dp)) Spacer(modifier = Modifier.height(8.dp)) @@ -354,13 +356,13 @@ private fun WakeupAlarmCard() { modifier = androidx.compose.ui.Modifier.weight(1f) ) { androidx.compose.material3.Text( - "기상 알람", fontSize = 24.sp, fontWeight = FontWeight.ExtraBold + stringResource(R.string.wakeup_alarm_title), fontSize = 24.sp, fontWeight = FontWeight.ExtraBold ) androidx.compose.foundation.layout.Spacer( modifier = androidx.compose.ui.Modifier.height(4.dp) ) androidx.compose.material3.Text( - "환자가 일어나면 빨간 팝업으로 알림 (낙상 예방)", + stringResource(R.string.wakeup_alarm_desc), fontSize = 12.sp, color = MlSecondaryText ) } diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/registration/RegistrationView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/registration/RegistrationView.kt index 52171a0..fb41334 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/registration/RegistrationView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/registration/RegistrationView.kt @@ -13,11 +13,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.input.KeyboardType 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.Sex import com.example.medilightv2android.ui.components.BladdyIdleRiveView import com.example.medilightv2android.ui.theme.* @@ -72,7 +74,7 @@ fun RegistrationView(appState: AppState) { Box(modifier = Modifier.weight(1f)) { RegistrationTextField("Age", ageText, { ageText = it }, KeyboardType.Number) } - listOf(Sex.MALE to "남성", Sex.FEMALE to "여성").forEach { (sex, label) -> + listOf(Sex.MALE to stringResource(R.string.sex_male), Sex.FEMALE to stringResource(R.string.sex_female)).forEach { (sex, label) -> val selected = selectedSex == sex OutlinedButton( onClick = { selectedSex = sex }, diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/reminder/ReminderSettingsView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/reminder/ReminderSettingsView.kt index e7dfbe9..81ab45f 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/reminder/ReminderSettingsView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/reminder/ReminderSettingsView.kt @@ -16,9 +16,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.unit.dp import androidx.compose.ui.unit.sp +import com.example.medilightv2android.R import com.example.medilightv2android.services.ReminderService import com.example.medilightv2android.ui.components.LockableSlider import com.example.medilightv2android.ui.theme.* @@ -43,10 +45,10 @@ fun ReminderSettingsView(onBack: () -> Unit) { Scaffold( topBar = { TopAppBar( - title = { Text("배뇨 알리미", fontWeight = FontWeight.SemiBold) }, + title = { Text(stringResource(R.string.reminder_title), fontWeight = FontWeight.SemiBold) }, navigationIcon = { IconButton(onClick = onBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "뒤로") + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back)) } } ) @@ -70,10 +72,10 @@ fun ReminderSettingsView(onBack: () -> Unit) { verticalAlignment = Alignment.CenterVertically ) { Column(modifier = Modifier.weight(1f)) { - Text("알림 활성화", fontSize = 16.sp, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.reminder_enable), fontSize = 16.sp, fontWeight = FontWeight.Bold) Text( - if (isEnabled) "정해진 간격으로 배뇨 알림을 받습니다" - else "알림이 꺼져 있습니다", + if (isEnabled) stringResource(R.string.reminder_enabled_desc) + else stringResource(R.string.reminder_disabled_desc), fontSize = 12.sp, color = MlSecondaryText ) } @@ -95,7 +97,7 @@ fun ReminderSettingsView(onBack: () -> Unit) { colors = CardDefaults.cardColors(containerColor = MlCardBackground) ) { Column(modifier = Modifier.padding(16.dp)) { - Text("알림 간격", fontSize = 14.sp, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.reminder_interval_label), fontSize = 14.sp, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.height(8.dp)) intervalOptions.forEach { minutes -> Row( @@ -110,7 +112,7 @@ fun ReminderSettingsView(onBack: () -> Unit) { verticalAlignment = Alignment.CenterVertically ) { Text( - formatInterval(minutes), + formatInterval(minutes, context), fontSize = 15.sp, modifier = Modifier.weight(1f) ) @@ -131,12 +133,12 @@ fun ReminderSettingsView(onBack: () -> Unit) { colors = CardDefaults.cardColors(containerColor = MlCardBackground) ) { Column(modifier = Modifier.padding(16.dp)) { - Text("알림 시간대", fontSize = 14.sp, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.reminder_active_hours), fontSize = 14.sp, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.height(12.dp)) Row(verticalAlignment = Alignment.CenterVertically) { - Text("시작", fontSize = 13.sp, color = MlSecondaryText, modifier = Modifier.width(40.dp)) - Text("${startHour}시", fontSize = 18.sp, fontWeight = FontWeight.Bold, color = MlPrimary) + Text(stringResource(R.string.reminder_start), fontSize = 13.sp, color = MlSecondaryText, modifier = Modifier.width(40.dp)) + Text(stringResource(R.string.reminder_hour_format, startHour), fontSize = 18.sp, fontWeight = FontWeight.Bold, color = MlPrimary) } LockableSlider( value = startHour.toFloat(), @@ -157,8 +159,8 @@ fun ReminderSettingsView(onBack: () -> Unit) { Spacer(modifier = Modifier.height(8.dp)) Row(verticalAlignment = Alignment.CenterVertically) { - Text("종료", fontSize = 13.sp, color = MlSecondaryText, modifier = Modifier.width(40.dp)) - Text("${endHour}시", fontSize = 18.sp, fontWeight = FontWeight.Bold, color = MlPrimary) + Text(stringResource(R.string.reminder_end), fontSize = 13.sp, color = MlSecondaryText, modifier = Modifier.width(40.dp)) + Text(stringResource(R.string.reminder_hour_format, endHour), fontSize = 18.sp, fontWeight = FontWeight.Bold, color = MlPrimary) } LockableSlider( value = endHour.toFloat(), @@ -184,8 +186,8 @@ fun ReminderSettingsView(onBack: () -> Unit) { verticalAlignment = Alignment.CenterVertically ) { Column(modifier = Modifier.weight(1f)) { - Text("유동 스케줄링", fontSize = 14.sp, fontWeight = FontWeight.Bold) - Text("마지막 기록 기준으로 다음 알림을 조정합니다", fontSize = 12.sp, color = MlSecondaryText) + Text(stringResource(R.string.reminder_flexible_title), fontSize = 14.sp, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.reminder_flexible_desc), fontSize = 12.sp, color = MlSecondaryText) } Switch( checked = isFlexible, @@ -205,13 +207,13 @@ fun ReminderSettingsView(onBack: () -> Unit) { colors = CardDefaults.cardColors(containerColor = MlCardBackground) ) { Column(modifier = Modifier.padding(16.dp)) { - Text("알림 방식", fontSize = 14.sp, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.reminder_method_label), fontSize = 14.sp, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.height(8.dp)) Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { - Text("소리", modifier = Modifier.weight(1f)) + Text(stringResource(R.string.reminder_sound), modifier = Modifier.weight(1f)) Switch( checked = useSound, onCheckedChange = { @@ -226,7 +228,7 @@ fun ReminderSettingsView(onBack: () -> Unit) { modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { - Text("진동", modifier = Modifier.weight(1f)) + Text(stringResource(R.string.reminder_vibration), modifier = Modifier.weight(1f)) Switch( checked = useVibration, onCheckedChange = { @@ -241,7 +243,7 @@ fun ReminderSettingsView(onBack: () -> Unit) { modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { - Text("팝업", modifier = Modifier.weight(1f)) + Text(stringResource(R.string.reminder_popup), modifier = Modifier.weight(1f)) Switch( checked = usePopup, onCheckedChange = { @@ -261,14 +263,14 @@ fun ReminderSettingsView(onBack: () -> Unit) { colors = CardDefaults.cardColors(containerColor = MlCardBackground) ) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text("요약", fontSize = 14.sp, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.reminder_summary), fontSize = 14.sp, fontWeight = FontWeight.Bold) Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Text("🕐", fontSize = 14.sp) - Text("활성 시간: ${startHour}시 ~ ${endHour}시", fontSize = 14.sp, color = MlPrimary) + Text(stringResource(R.string.reminder_active_hours_summary, startHour, endHour), fontSize = 14.sp, color = MlPrimary) } Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Text("🔁", fontSize = 14.sp) - Text("매 ${formatInterval(intervalMinutes)} 간격", fontSize = 14.sp, color = Color(0xFFF5A623)) + Text(stringResource(R.string.reminder_interval_summary, formatInterval(intervalMinutes, context)), fontSize = 14.sp, color = Color(0xFFF5A623)) } } } @@ -287,8 +289,8 @@ fun ReminderSettingsView(onBack: () -> Unit) { Text("⚠️", fontSize = 20.sp) Spacer(modifier = Modifier.width(12.dp)) Column(modifier = Modifier.weight(1f)) { - Text("알림 권한 필요", fontSize = 14.sp, fontWeight = FontWeight.Medium) - Text("설정에서 알림 권한을 허용해주세요", fontSize = 12.sp, color = MlSecondaryText) + Text(stringResource(R.string.reminder_permission_needed), fontSize = 14.sp, fontWeight = FontWeight.Medium) + Text(stringResource(R.string.reminder_permission_desc), fontSize = 12.sp, color = MlSecondaryText) } OutlinedButton(onClick = { val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { @@ -296,7 +298,7 @@ fun ReminderSettingsView(onBack: () -> Unit) { } context.startActivity(intent) }) { - Text("설정") + Text(stringResource(R.string.settings)) } } } @@ -307,12 +309,12 @@ fun ReminderSettingsView(onBack: () -> Unit) { } } -private fun formatInterval(minutes: Int): String { +private fun formatInterval(minutes: Int, context: android.content.Context): String { val h = minutes / 60 val m = minutes % 60 return when { - h > 0 && m > 0 -> "${h}시간 ${m}분" - h > 0 -> "${h}시간" - else -> "${m}분" + h > 0 && m > 0 -> context.getString(R.string.format_hours_minutes, h, m) + h > 0 -> context.getString(R.string.format_hours, h) + else -> context.getString(R.string.format_minutes, m) } } \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index d531c94..c665f5e 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -68,4 +68,127 @@ 이미지가 너무 어둡습니다 눈금 부족 감지 실패 + + + 언어 + 시스템 기본 + 한국어 + English + + + 이용 동의 + 서비스 이용을 위해 아래 항목에 동의해 주세요. + 전체 동의 + [필수] 개인정보 수집 및 이용 동의 + 이름, 나이, 성별, 신체정보(키·체중)를 앱 내 기능 제공 목적으로 수집합니다. 수집된 정보는 기기에만 저장되며 외부로 전송되지 않습니다. + [필수] 의료정보 활용 동의 + 방광 측정 데이터, 배뇨일지, NIRS 산소포화도 등 건강 관련 데이터를 모니터링 및 기록 목적으로 활용합니다. + [필수] 데이터 저장 동의 + 측정 기록 및 배뇨일지 데이터가 기기 내부 저장소에 저장됩니다. 앱 삭제 시 모든 데이터가 삭제됩니다. + 동의하고 계속하기 + + + 남성 + 여성 + + + 배뇨 알리미 + 알림 활성화 + 정해진 간격으로 배뇨 알림을 받습니다 + 알림이 꺼져 있습니다 + 알림 간격 + 알림 시간대 + 시작 + 종료 + %d시 + 유동 스케줄링 + 마지막 기록 기준으로 다음 알림을 조정합니다 + 알림 방식 + 소리 + 진동 + 팝업 + 요약 + 활성 시간: %1$d시 ~ %2$d시 + 매 %s 간격 + 알림 권한 필요 + 설정에서 알림 권한을 허용해주세요 + 설정 + %1$d시간 %2$d분 + %d시간 + %d분 + + + 평균 %d ml + %d회 + %d ml + 24시간 의료 지표 + 총 횟수 + 주간 + 야간 + + + 센서가 분리되었습니다 + Sensor detached + 치골 위쪽에 다시 위치시켜 주세요 + 센서를 치골 위에 두고 Start 를 누르세요 + + + 환자가 일어났습니다 + Patient is getting up + 누운 시간 %1$d분 %2$d초 + 환자가 걷고 있습니다 + Patient is walking — fall risk + 즉시 환자 상태 확인 필요 + + + 추정 용량 + + + 페어링 해제 + %s 의 페어링을 해제합니다. 다시 사용하려면 PIN 재입력이 필요합니다. + 해제 + + + 배뇨 알림 + 배뇨 알리미 알림 + VesiScan 알림 + 배뇨 시간입니다! 도뇨 준비를 해주세요. + 규칙적인 배뇨는 건강의 기본입니다! + 화장실에 다녀오세요! 💧 + 배뇨 시간이에요. 건강한 습관을 유지하세요! + 잊지 마세요! 화장실 갈 시간입니다. + + + BLE 기기 연결 유지 + BLE 기기 연결 중... + + + 기기 배터리 위급 + 기기 배터리 부족 + 현재 %d%% · 측정이 갑자기 중단될 수 있습니다. 즉시 충전하세요. + 현재 %d%% · 곧 충전이 필요합니다. + 펌웨어 업데이트 필요 + 기기 응답 없음 + 현재: %1$s · 필요: VBTFW%2$04d 이상. 측정이 정상 동작하지 않을 수 있습니다. + + + 잠금 해제됨 — 다시 탭하여 잠금 + 잠김 — 탭하여 해제 후 조정 + + + 기상 알람 + 환자가 일어나면 빨간 팝업으로 알림 (낙상 예방) + 방광이 이 정도 차오르면 알림 + + + 잠시 그대로 두세요 (%1$d/%2$d) + ↑ 위로 조금씩 올리세요 + ↑ 살짝 놓침 — 다시 위로 + ✓ 좋아요! 그 위치 유지 (%1$d/%2$d) + ↑ 위치 살짝 벗어남 — 위로 약간 + ↓ 위치 살짝 벗어남 — 아래로 약간 + ✓ 정렬 완료! 그대로 측정 가능 + ← 왼쪽으로 살짝 + → 오른쪽으로 살짝 + ↔ 좌우로 천천히 움직여 보세요 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0251b34..0de9b36 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -68,4 +68,127 @@ Image is too dark Not enough marks Detection failed + + + Language + System default + Korean + English + + + User Agreement + Please agree to the items below to use this service. + Agree to all + [Required] Personal information collection + We collect name, age, sex, and body information (height, weight) for in-app functionality. The collected data is stored only on the device and is not transmitted externally. + [Required] Medical data use + Bladder measurement data, voiding diary, NIRS oxygen saturation, and other health-related data are used for monitoring and record purposes. + [Required] Data storage + Measurement records and voiding diary data are stored in the device internal storage. All data is deleted when the app is uninstalled. + Agree and continue + + + Male + Female + + + Voiding Reminder + Enable reminders + You will receive voiding reminders at the set interval + Reminders are turned off + Reminder interval + Active hours + Start + End + %d:00 + Flexible scheduling + Adjusts the next reminder based on the most recent record + Notification method + Sound + Vibration + Popup + Summary + Active: %1$d:00 – %2$d:00 + Every %s + Notification permission required + Please allow notifications in Settings + Settings + %1$dh %2$dm + %dh + %dm + + + Avg %d ml + %d times + %d ml + 24-hour Medical Metrics + Total + Day + Night + + + Sensor detached + Sensor detached + Please reposition above the pubic bone + Place the sensor above the pubic bone and press Start + + + Patient is getting up + Patient is getting up + Lying for %1$d min %2$d sec + Patient is walking + Patient is walking — fall risk + Immediate patient check required + + + Estimated volume + + + Unpair Device + Unpair %s. PIN re-entry will be required to use it again. + Unpair + + + Voiding reminder + Voiding reminder notifications + VesiScan reminder + Time to void! Please prepare for catheterization. + Regular voiding is fundamental to health! + Time for the bathroom! 💧 + It is time to void. Keep up healthy habits! + Don\'t forget! It is time to go to the bathroom. + + + Maintains BLE device connection + Connecting to BLE device... + + + Critical battery + Low battery + %d%% · Measurement may stop suddenly. Charge immediately. + %d%% · Please charge soon. + Firmware update required + No response from device + Current: %1$s · Required: VBTFW%2$04d or higher. Measurement may not work properly. + + + Unlocked — tap again to lock + Locked — tap to unlock and adjust + + + Wake-up alarm + Red popup when patient gets up (fall prevention) + Alert when bladder fills to this level + + + Hold still… (%1$d/%2$d) + ↑ Slide up slowly + ↑ Lost it — slide back up + ✓ Good! Keep this position (%1$d/%2$d) + ↑ Slightly off — move up a bit + ↓ Slightly off — move down a bit + ✓ Aligned! Ready to measure + ← Slightly to the left + → Slightly to the right + ↔ Move slowly side to side