feat(locale): Phase 1 — 언어 전환 인프라 (시스템/한국어/English)

신규:
- LocalePref.kt — Mode {SYSTEM, KO, EN} 영속화 + Context wrap
- MainActivity.attachBaseContext — 사용자 선택 locale 으로 Configuration override
- HomeView 우상단 🌐 Language 아이콘 → 선택 다이얼로그 → recreate

Phase 2: 모든 hardcoded Korean text → strings.xml + values-ko/strings.xml 추출 (대량 작업, 별도 commit)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 16:53:02 +09:00
parent 22ef6576f8
commit bc7843809e
3 changed files with 146 additions and 2 deletions
@@ -43,6 +43,11 @@ import com.example.medilightv2android.ui.views.clinical.ClinicalHomeView
import com.example.medilightv2android.ui.views.clinical.ClinicalLiveView import com.example.medilightv2android.ui.views.clinical.ClinicalLiveView
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
/** 사용자가 선택한 앱 언어 적용 — Activity 생성 전에 Locale wrap. */
override fun attachBaseContext(newBase: android.content.Context) {
super.attachBaseContext(com.example.medilightv2android.services.LocalePref.wrap(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -0,0 +1,73 @@
/*
* LocalePref — 앱 언어 설정 영속화 + 컨텍스트 wrapping.
*
* 모드:
* - SYSTEM (default): 폰 시스템 언어 따라감 (Korean → 한국어, 그 외 → English)
* - KO : 강제 한국어
* - EN : 강제 영어
*
* 적용:
* 1. MainActivity.attachBaseContext(base) → wrap(base) 호출
* 2. 언어 변경 시 setMode(...) 호출 후 activity recreate (또는 process restart)
*
* Android 13+ 의 per-app locale API (AppCompatDelegate.setApplicationLocales) 와
* 무관하게 Configuration override 방식으로 모든 SDK 에서 동작.
*/
package com.example.medilightv2android.services
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import java.util.Locale
object LocalePref {
enum class Mode { SYSTEM, KO, EN }
private const val PREF = "locale_pref"
private const val KEY_MODE = "mode"
fun getMode(context: Context): Mode {
val s = context.applicationContext
.getSharedPreferences(PREF, Context.MODE_PRIVATE)
.getString(KEY_MODE, Mode.SYSTEM.name) ?: Mode.SYSTEM.name
return runCatching { Mode.valueOf(s) }.getOrDefault(Mode.SYSTEM)
}
fun setMode(context: Context, mode: Mode) {
context.applicationContext
.getSharedPreferences(PREF, Context.MODE_PRIVATE)
.edit().putString(KEY_MODE, mode.name).apply()
}
/** 선택된 모드에 맞춰 Locale 적용된 새 Context 반환. attachBaseContext 에서 사용. */
fun wrap(base: Context): Context {
val mode = getMode(base)
if (mode == Mode.SYSTEM) return base // 시스템 기본 — 손대지 않음
val locale = when (mode) {
Mode.KO -> Locale.KOREAN
Mode.EN -> Locale.ENGLISH
else -> return base
}
Locale.setDefault(locale)
val config = Configuration(base.resources.configuration)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val list = android.os.LocaleList(locale)
android.os.LocaleList.setDefault(list)
config.setLocales(list)
} else {
@Suppress("DEPRECATION")
config.locale = locale
}
return base.createConfigurationContext(config)
}
/** 현재 적용된 locale 의 사용자 표시용 라벨. */
fun currentLabel(context: Context): String = when (getMode(context)) {
Mode.SYSTEM -> "시스템 기본 / System"
Mode.KO -> "한국어"
Mode.EN -> "English"
}
}
@@ -8,9 +8,17 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.unit.min import androidx.compose.ui.unit.min
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.RadioButton
import androidx.compose.material3.RadioButtonDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Language
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -30,11 +38,23 @@ fun HomeView(appState: AppState) {
val context = LocalContext.current val context = LocalContext.current
var tapCount by remember { mutableIntStateOf(0) } var tapCount by remember { mutableIntStateOf(0) }
var lastTapTime by remember { mutableLongStateOf(0L) } var lastTapTime by remember { mutableLongStateOf(0L) }
var showLanguageDialog by remember { mutableStateOf(false) }
Box(modifier = Modifier.fillMaxSize().background(MlBackground)) {
// 우상단 — 언어 설정 진입 아이콘
IconButton(
onClick = { showLanguageDialog = true },
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp)
) {
Icon(Icons.Default.Language, contentDescription = "Language",
tint = MlSecondaryText.copy(alpha = 0.7f))
}
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize(),
.background(MlBackground),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
@@ -139,4 +159,50 @@ fun HomeView(appState: AppState) {
Spacer(modifier = Modifier.height(48.dp)) Spacer(modifier = Modifier.height(48.dp))
} }
} }
} // Box (top-bar gear overlay) close
// 언어 선택 다이얼로그 — 시스템 / 한국어 / English. 변경 시 Activity recreate.
if (showLanguageDialog) {
val curMode = com.example.medilightv2android.services.LocalePref.getMode(context)
var selected by remember { mutableStateOf(curMode) }
AlertDialog(
onDismissRequest = { showLanguageDialog = false },
title = { Text("Language / 언어") },
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",
).forEach { (mode, label) ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable { selected = mode }
.padding(vertical = 6.dp)
) {
RadioButton(
selected = selected == mode,
onClick = { selected = mode }
)
Spacer(Modifier.width(4.dp))
Text(label)
}
}
}
},
confirmButton = {
TextButton(onClick = {
com.example.medilightv2android.services.LocalePref.setMode(context, selected)
showLanguageDialog = false
// Activity recreate → attachBaseContext 다시 호출 → 새 locale 적용
(context as? android.app.Activity)?.recreate()
}) { Text("OK") }
},
dismissButton = {
TextButton(onClick = { showLanguageDialog = false }) { Text("Cancel") }
}
)
}
} }