Skip to content

Conversation

@PeraSite
Copy link
Member

@PeraSite PeraSite commented Dec 29, 2025

Summary

하드코딩된 문자열을 대체하는 리팩토링을 진행했습니다.

Describe your changes

1. UiText 도입

ViewModel에서 ApplicationContext를 주입받아 getString하는 코드를 완전히 개선했습니다.

UiText는 문자열 리소스를 감싸는 StringResource, 문자열을 감싸는 DynamicString, Empty로 나뉘는 sealed class입니다.

sealed class UiText {
    data class StringResource(
        @StringRes val resId: Int,
        val args: List<Any> = emptyList()
    ) : UiText()

    data class DynamicString(val value: String) : UiText()

    data object Empty : UiText()
}

Toast를 보낼 때를 예시로 들면,
ViewModel에서는

_uiEvent.emit(UiEvent.ShowToast(UiText.StringResource(R.string.toast_review_write_success), ToastType.SUCCESS))

처럼 UiText.StringResource로 ShowToast event를 만들어 emit하고

Activity에서는

showToast(event.message.asString(this), event.type)

이렇게 현재 context에서 UiText를 문자열로 변환(asString)해서 보여주는 방식입니다.

이러한 수정에 맞게 UiEvent.ShowToast의 정의도 다음과 같이 변경되었습니다.

data class ShowToast(
    val message: UiText, // Before - val message: String
    val type: ToastType
) : UiEvent

2. 하드 코딩된 한국어 문자열을 리소스를 사용하게 수정

대부분 String Literal로 쓰인 한국어를 getString로, Compose의 경우 stringResource로 대체하는 Presentation 레이어의 리팩토링이지만, 이외에 특별히 확인해야 할 부분은 다음과 같습니다.

  • CollegeResponse, DepartmentResponse, UserCollegeDepartmentResponse의 toDomain 함수가 fallback 시 하드 코딩된 문자열을 사용하지 않게, fallback을 null로 변경했습니다. 이들을 호출하는 UserRepositoryImpl에서 null인 경우 무시하게끔 mapNotNull을 사용합니다.

  • 메인 식단 화면 상단의 캘린더 중 weekday를 표시하는 CalendarAdapter.kt가 현재 언어에 대응하게 수정했습니다.

image
  • 상단의 캘린더 중 식사 종류를 표시하는 CafeteriaFragment.kt 에서 하드코딩되어있던 ("아침", "점심", "저녁") 을 개선했습니다.
image
  • common enum인 ReportType과 Restaurant가 String Resource를 쓰게 정의를 바꾸었습니다. 이에 Restaurant.korean 처럼 하드 코딩된 문자열을 참조하는 모든 코드도 변경했어요.

  • strings.xml에서 레거시인 것 같은 수많은 entry를 삭제했습니다. 명확한 목적을 가진 경우 string name의 접두사로 toast_, notification_ 등을 붙여서 naming inconsistency를 해결했어요. 이외에도 사용 목적에 맞지 않거나 애매한 이름을 가진 entry를 수정했습니다.

  • ValidateNicknameLocalUseCase.kt 과 UserInfoViewModel.kt 에서 하드코딩되어있던 닉네임 규칙 미준수 오류 메세지를 대체했습니다.

Issue

@PeraSite PeraSite marked this pull request as ready for review December 29, 2025 10:56
@PeraSite PeraSite marked this pull request as draft December 29, 2025 10:57
@PeraSite PeraSite self-assigned this Dec 29, 2025
@PeraSite PeraSite changed the title feat/i18n [Feat] 다국어 지원 Dec 29, 2025
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @PeraSite, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 Pull Request는 앱에 다국어(i18n) 지원을 추가하고, 사용자 인터페이스 텍스트를 효율적으로 관리하기 위해 기존의 하드코딩된 문자열들을 리소스 파일로 외부화하는 것을 목표로 합니다. 또한, ViewModel에서 Context 의존성을 줄이고 데이터 변환 로직의 안정성을 향상시키는 리팩토링도 포함되어 있습니다.

Highlights

  • 다국어(i18n) 지원 구현: 앱에 다국어 지원을 추가하기 위해 AppLanguage enum을 도입하고, 사용자가 언어를 선택할 수 있는 LanguageSelectorActivityLanguageSelectorScreen을 구현했습니다. 사용자 선택은 SettingDataStore에 저장되며, AppCompatDelegate.setApplicationLocales를 통해 앱의 로케일을 동적으로 변경합니다.
  • 문자열 리소스 외부화 및 번역 추가: 앱 전반에 걸쳐 하드코딩된 수많은 문자열을 strings.xml 리소스 파일로 이동시켰습니다. 또한, 영어, 일본어, 베트남어, 중국어 번역 파일을 새로 추가하여 다국어 환경을 지원합니다. ReportTypeRestaurant enum도 문자열 리소스 ID를 사용하도록 업데이트되었습니다.
  • ViewModel Context 의존성 제거 및 UiText 도입: MainViewModel, IntroViewModel, LoginViewModel, MyPageViewModel, SignOutViewModel 등 여러 ViewModel에서 Context에 대한 직접적인 의존성을 제거했습니다. 대신 UiText sealed class를 도입하여 ViewModel이 UI 텍스트를 Context 없이 처리할 수 있도록 추상화했습니다.
  • 데이터 변환 로직 개선: CollegeResponse, DepartmentResponse, UserCollegeDepartmentResponsetoDomain() 확장 함수를 nullable 타입으로 변경하여 null 안전성을 강화했습니다. UserRepositoryImpl에서는 mapNotNull을 사용하여 유효한 도메인 객체만 처리하도록 개선했습니다.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

이 PR은 앱에 다국어 지원을 추가하기 위한 대규모 리팩토링을 포함하고 있습니다. 하드코딩된 문자열을 문자열 리소스로 추출하고, 영어, 일본어, 중국어, 베트남어 번역을 추가한 점이 인상적입니다. 특히 UiText sealed class를 도입하여 ViewModel에서 Context 의존성을 제거하고 문자열을 안전하게 처리하도록 개선한 점은 매우 훌륭한 아키텍처 개선입니다. 또한 언어 선택 화면을 추가하고 사용자의 언어 설정을 저장 및 적용하는 기능 구현도 잘 되었습니다. 전반적으로 코드 품질을 크게 향상시키는 매우 중요한 변경이라고 생각합니다. 몇 가지 추가 개선을 위한 제안 사항을 아래 주석으로 남겼습니다. 훌륭한 작업입니다!

I am having trouble creating individual review comments. Click here to see my feedback.

app/src/main/res/values/strings.xml (13-42)

medium

문자열 리소스를 리팩토링하는 훌륭한 작업입니다. 다만, 리팩토링 과정에서 더 이상 사용되지 않는 이전 문자열들(login_done, set_nickname_unable, recomented 등)이 일부 남아있는 것으로 보입니다. 유지보수성을 높이고 APK 크기를 줄이기 위해 사용하지 않는 리소스를 제거하는 것이 좋습니다. Android Studio의 "Refactor > Remove Unused Resources..." 기능을 사용하면 쉽게 찾아 삭제할 수 있습니다.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements comprehensive multi-language support for the EAT-SSU Android application, supporting Korean, English, Chinese, Japanese, and Vietnamese. The implementation involves significant architectural refactoring to eliminate hardcoded strings and introduce a clean separation between ViewModels and UI layer for string resource handling.

Key Changes:

  • Introduced UiText sealed class to handle string resources without Context in ViewModels
  • Migrated all hardcoded Korean strings to string resource files with translations
  • Added language selection feature in MyPage settings
  • Refactored enums (Restaurant, ReportType) to use string resource IDs instead of hardcoded strings

Reviewed changes

Copilot reviewed 72 out of 72 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
core/common/src/main/java/com/eatssu/common/UiText.kt New sealed class for Context-independent string handling in ViewModels
core/common/src/main/java/com/eatssu/common/UiEvent.kt Updated ShowToast to use UiText instead of String
core/common/src/main/java/com/eatssu/common/enums/Restaurant.kt Refactored to use displayNameResId instead of hardcoded Korean strings
core/common/src/main/java/com/eatssu/common/enums/ReportType.kt Refactored to use descriptionResId for multi-language support
app/src/main/res/values*/strings.xml Added comprehensive string resources for 5 languages (ko, en, ja, zh, vi)
app/src/main/java/com/eatssu/android/presentation/util/UiTextUtil.kt Composable extension function for UiText resolution
app/src/main/java/com/eatssu/android/presentation/mypage/language/* New language selector feature with ViewModel, Screen, and Activity
app/src/main/java/com/eatssu/android/domain/model/AppLanguage.kt Enum defining supported languages with locale codes
app/src/main/java/com/eatssu/android/data/local/SettingDataStore.kt Added language preference persistence
app/src/main/java/com/eatssu/android/presentation/cafeteria/calendar/CalendarAdapter.kt Localized weekday display using string arrays
Multiple ViewModels Removed ApplicationContext injections, migrated to UiText for messages
Multiple Activities/Fragments Updated to use getString() and stringResource() for UI text
app/src/main/java/com/eatssu/android/data/remote/dto/response/*Response.kt Changed toDomain() to return nullable types for proper fallback handling

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +36 to +38
// onClick 람다에서 LocalContext 접근이 불가하므로 Composable 레벨에서 미리 매핑 생성
val restaurantDisplayNameMap = Restaurant.getVariableRestaurantList()
.associateBy { it.toUiText().asString() }
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the WidgetSettingScreen, the restaurantDisplayNameMap is created by calling asString() on each restaurant's UiText within the Composable. However, this assumes the locale won't change during the screen's lifetime. If the user changes language while this screen is open, the map will contain outdated strings. Consider regenerating this map when the language changes or using a different approach that resolves the strings lazily.

Copilot uses AI. Check for mistakes.
@Composable
fun MealWidgetPreviewError() {
MealWidgetError("저녁", Restaurant.DODAM.korean, "에러임")
MealWidgetError("저녁", Restaurant.DODAM.toUiText().asString(), "에러임")
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded string "에러임" (meaning "This is an error") in the preview function should be replaced with a string resource or a more descriptive English placeholder for consistency with the internationalization effort.

Copilot uses AI. Check for mistakes.
Comment on lines +300 to +304
MealWidgetContent(
"저녁",
listOf(listOf("밥", "국", "반찬", "음료")),
Restaurant.DODAM.toUiText().asString()
)
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the preview function, calling asString() without a Context will fail since it's not a Composable function. The preview should use stringResource(R.string.restaurant_dodam) instead, or provide a proper context for the UiText resolution.

Copilot uses AI. Check for mistakes.
@PeraSite PeraSite marked this pull request as ready for review December 31, 2025 07:17
   2
   3 develop의 주요 변경사항 통합:
   4 - Paging3 무한 스크롤 적용
   5 - EventLogger 리뷰 작성 추적
   6 - Authenticator 패턴 도입
   7 - 레거시 리소스 파일 삭제
   8
   9 feat/i18n-safe의 변경사항 유지:
  10 - UiText 기반 문자열 리소스화
  11 - 구조화된 strings.xml 체계
  12 - 다국어 지원 강화
@PeraSite PeraSite requested a review from HI-JIN2 January 31, 2026 15:55
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 66 out of 70 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +136 to +143
tvCollege.text = data.selectedCollege?.collegeName ?: "단과대"
tvCollege.setTextColor(
getColor(
if (data.selectedCollege.collegeId != -1) R.color.gray700 else R.color.gray400
if (data.selectedCollege != null) R.color.gray700 else R.color.gray400
)
)

tvDepartment.text = data.selectedDepartment.departmentName
tvDepartment.text = data.selectedDepartment?.departmentName ?: "학과"
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded Korean strings should use string resources for consistency with multi-language support. The fallback values "단과대" and "학과" should reference the existing string resources R.string.college_placeholder and R.string.department_placeholder.

Copilot uses AI. Check for mistakes.
Comment on lines +300 to +311
MealWidgetContent(
"저녁",
listOf(listOf("밥", "국", "반찬", "음료")),
Restaurant.DODAM.toUiText().asString()
)
}

@OptIn(ExperimentalGlancePreviewApi::class)
@Preview
@Composable
fun MealWidgetPreviewError() {
MealWidgetError("저녁", Restaurant.DODAM.korean, "에러임")
MealWidgetError("저녁", Restaurant.DODAM.toUiText().asString(), "에러임")
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preview functions contain hardcoded Korean strings ("저녁", "밥", "국", "반찬", "음료", "에러임") which should be replaced with mock English text or removed, as they won't work correctly in multi-language environments. Preview functions execute at design time when context is not available.

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +43
if (departmentName.isBlank() || departmentName == placeholderDepartment) {
stringResource(labelResId)
} else {
departmentName
}
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comparison departmentName == placeholderDepartment compares the actual department name string with the localized placeholder text. This is fragile and will break when the language changes. Consider using a null or empty check instead, or using a sentinel value from the data layer rather than comparing UI strings.

Copilot uses AI. Check for mistakes.
val nativeDisplayName: String
) {
// SYSTEM("", "System Default", "시스템 언어"), // 다국어 재활성화 시 주석 해제
KOREAN("ko", "Korean", "한국어");
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semicolon on line 14 should be removed since KOREAN is currently the only active enum entry (the commented-out entries don't count). In Kotlin, a semicolon after the last enum entry is only needed when enum body members (like companion object) follow multiple entries.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

다국어 지원

2 participants