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:
@@ -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") }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user