영어 데일리

컴포즈에서 다국어하는 법 (25.01.05)

현욱 정리장 2025. 1. 5. 22:56

https://proandroiddev.com/localization-in-compose-the-pragmatic-way-012cc4e167d2

 

compose에서의 로컬라이제이션 접근 방식에 대해 나는 고민했었습니다

현재는 jetpack compose 또는 compose multiplatform에서 우리는 주로 string resource file에서 현지화 적용을 자주 사용합니다. 

하지만 나는 이 접근방식이 몇가지 한계점이 있다는걸 느꼈습니다.

 

  • string resource 파일들은 일반 문자열이나, 플레이스홀더 표기법이 포함된 문자열을 저장할수 있지만, 주석같은 문자열은 한계가 있습니다. 
  • 플레이스홀더 값에 따라 조건부로 문자열을 재구성하는데에 한계가 있습니다 (복수형 표현 과같은) 
  • xml을 리소스파일로 쓴 이래로,  특수 문자를 이스케이프 해야 하는 경우, 일부상황에서는 <![CDATA[]]>를 사용해야할수도 있습니다.

 

동기부여

나는 컴포즈 코드에서 Material Theme 속성에 접근하는 방법을 좋아합니다. 이것은 단순하며 독립적이고, 간결합니다

Column {
    Text(
        text = "Hello, World!",
        style = MaterialTheme.typography.bodyMedium,
        color = MaterialTheme.colorScheme.primary,
    )
}

 

Material Theme 속성은 MaterialTheme 컴포저블을 제공합니다. 일반적으로 컴포저블 최상단 트리에 배치되어있습니다. 

이는 내부적으로 CompositionLocalProvider를 사용하여 테마 속성을 계층 구조를 따라 하위로 전달합니다. 

그리고 제공된 객체가 변경될때,  속성이 사용되는 특정한 부분만 recompose하는데에 매우 효율적입니다.

 

컴포즈의 MaterialTheme을 보고 영감을 받았습니다. 우리는 다국어를 동일한 방식으로 접근해보려고 합니다. 

Localized 객체의 데이터 구조를 Composable 트리에 최상단에 배치된 COmpositionLocalProvider를 통해 전달합니다. 그리고 

이를 계층 구조에 깊은 부분에서도 매끄럽게 사용할 수 있습니다.

유저가 어플리케이션 위치를 업데이트를 하려고할때, 업데이트 된 localizaed 객체를 COmpositionLocalProvider에 제공하고, 로케일 변경에 영향을 주는 계층 구조의 부분을 다시 구성하도록 합니다.

 

구현 

별도의  문자열 리소스파일을 유지하는 대신, 우리는 코틀린 코드에 보관할수있습니다. 우리는 인터페이스를 정의하고, 속성 또는 메소드를 통해 다국어 문자열을 리턴하는 형태로 제공할 수 있습니다. 

interface DefaultStrings {
    companion object : DefaultStrings
    val greeting: String
        get() = "Hello, World!"

    fun apples(count: Int): String {
        return when (count) {
            1 -> "1 Apple"
            else -> "$count Apples"
        }
    }
}

 

코틀린에서는 companion object를 생성할 수 있고, 이를 기본 인스턴스로 설계하여 동일한 인터페이스를 구현하고 해당 인터페이스 이름으로 접근할 수 있습니다. (이는 Compose의 Modifier에서 영감을 받았습니다.)

다국어를 제공함으로써, 우리는 DefaultString 인터페이스에 다국어버전을 오버라이드 할 수 있습니다. 

interface CzechStrings : DefaultStrings {
    companion object : CzechStrings

    override val greeting: String
        get() = "Ahoj světe!"

    override fun apples(count: Int): String {
        return when (count) {
            in 1..4 -> "$count jablka"
            else -> "$count jablek"
        }
    }
}

 

체코어는 복수형 표현이 영어와 다르기 때문에, 복수형 문제를 해결하는 방법을 잘 보여주는 샘플코드에서 사용되어있습니다. (저는 체코어에 익숙하지 않고, 번역은 구글 번역기를 통해 제공되었습니다.)

 

모든 로컬라이즈된 객체를 준비하고, 계층 구조에 따라 로컬라이션 객체를 전파하기 위해 CompositionLocal을 정의할 수 있으며, 로케일 필요에 따라 업데이트하기위해 전역 MutableStateFlow를 사용할 수 있습니다. 또한 로직을 캡슐화하기 위해 LocaleProvider라는 컴포저블을 정의하고, 이를 계층 구조의 최상단에 배치합니다. 

 

val LocalStrings = compositionLocalOf<DefaultStrings> { DefaultStrings }
val AppLocale = MutableStateFlow("en")

@Composable
fun LocaleProvider(
    localeOverride: String? = null,
    content: @Composable () -> Unit,
) {
    val localeState: State<String>? = if (localeOverride == null) {
        AppLocale.collectAsState()
    } else {
        null
    }
    val locale = localeOverride ?: localeState?.value
    val strings = when (locale) {
        "cz" -> CzechStrings
        "ar" -> ArabicStrings
        "es" -> SpanishStrings
        "de" -> GermanStrings
        else -> DefaultStrings
    }
    val layoutDirection = when (locale) {
        "ar" -> LayoutDirection.Rtl
        else -> LayoutDirection.Ltr
    }
    CompositionLocalProvider(
        LocalStrings provides strings,
        LocalLayoutDirection provides layoutDirection,
        content = content
    )
}

 

위에 예제는 로컬라이제이션과 함께 레이아웃의 방향성도 고려합니다. 이는 아랍어와 같은 RTL (오른쪽에서 왼쪽) 언어로 앱을 로컬라이즈할때 좋습니다. 또한 LocalProvider 컴포저블의 localeOverride 매개변수는 다른 로케일에 대한 미리보기를 제공하는대 도움을 줍니다.

 

데모

데모를 위해, 텍스트와 버튼이 포함된 단순한 컴포저블 UI를 만들어보겠습니다. 이 예제는 체코어의 UI 로컬라이제이션을 시연할것입니다. 체코어는 영어와 다르게 복수형 표현을 가지고 있기에 적합합니다. 

 

@Composable
fun AppleCounter(
    modifier: Modifier = Modifier,
) {
    Column(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
    ) {
        Text(text = LocalStrings.current.greeting)
        var count by rememberSaveable { mutableIntStateOf(0) }
        Text(text = LocalStrings.current.apples(count))
        Row {
            Button(onClick = { count++ }) {
                Text(text = "+")
            }
            Button(onClick = { count-- }) {
                Text(text = "-")
            }
        }
        Row {
            Button(onClick = { AppLocale.value = "en" }) {
                Text(text = "en")
            }
            Button(onClick = { AppLocale.value = "cz" }) {
                Text(text = "cz")
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
private fun AppleCounterEnglishPreview() {
    MaterialTheme {
        LocaleProvider {
            AppleCounter(modifier = Modifier.size(300.dp))
        }
    }
}

@Preview(showBackground = true)
@Composable
private fun AppleCounterCzechPreview() {
    MaterialTheme {
        LocaleProvider(localeOverride = "cz") {
            AppleCounter(modifier = Modifier.size(300.dp))
        }
    }
}

 

localoverride 매개변수를 사용하면, 우리는 안드로이드스튜디오에서 미리보기를 생성할수있습니다.

또한 안드로이드 스튜디오의 인터렉티브 프리뷰 기능을 사용하면 버튼을 클릭하며 로컬라이제이션과 복수형 표현이 완벽하게 동작하는 걸 볼 수 있습니다. 

 

결론

데모 코드에서는 다음과 같은 일을 할 수 있습니다

  • 로컬라이제이션 문자열
  • 향상된 플레이스홀더 및 조건부대체
  • 향상된 복수형표현
  • 주석이 포험된 문자열 지원

그러나 이 접근법은 문자열에만 국한되지 않습니다. 이미지 심지어 컴포저블까지 로컬라이제이션 할 수 있습니다. 이 구조를 여러분의 맞게 조정해보세요. 

 

기타

rephrase: 재 구성

concies : 간결하다

to propogate: 전파하다 

inspired by : 영감을 받다

seamlessly : 매끄럽게 

separate : 별도의