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
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?) {
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.RoundedCornerShape
import androidx.compose.ui.unit.min
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
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.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Language
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -30,11 +38,23 @@ fun HomeView(appState: AppState) {
val context = LocalContext.current
var tapCount by remember { mutableIntStateOf(0) }
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(
modifier = Modifier
.fillMaxSize()
.background(MlBackground),
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.weight(1f))
@@ -139,4 +159,50 @@ fun HomeView(appState: AppState) {
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") }
}
)
}
}