Conversation
opficdev
commented
Mar 31, 2026
- closed 영어 로컬라이제이션을 구현한다 #341
There was a problem hiding this comment.
Code Review
이번 pull request는 프로젝트 전반에 걸쳐 하드코딩된 한국어 문자열을 String(localized:) 및 String.localizedStringWithFormat을 사용하는 로컬라이제이션 방식으로 전환하는 대규모 작업을 포함하고 있습니다. 주요 피드백으로는 TodayViewModel에서 로컬라이즈된 문자열을 직접 비교하여 섹션을 필터링하는 로직을 열거형 기반으로 개선하여 유지보수성을 높일 것과, RelativeTimeText 및 PushNotificationSettingsView에서 문자열 결합 대신 표준 포맷팅 API를 사용하여 언어별 어순 및 시간 표기법 차이에 대응할 것을 권장했습니다.
| let allSections: [SectionContent] = [ | ||
| SectionContent(title: "집중할 일", items: groupedItems.focused), | ||
| SectionContent(title: "지난 마감", items: groupedItems.overdue), | ||
| SectionContent(title: "\(upcomingWindowDays)일 내 일정", items: groupedItems.dueSoon), | ||
| SectionContent(title: "나중 일정", items: groupedItems.later), | ||
| SectionContent(title: "일정 미정", items: groupedItems.unscheduled) | ||
| SectionContent(title: String(localized: "today_section_focused"), items: groupedItems.focused), | ||
| SectionContent(title: String(localized: "today_section_overdue"), items: groupedItems.overdue), | ||
| SectionContent( | ||
| title: String.localizedStringWithFormat( | ||
| String(localized: "today_section_due_soon_format"), | ||
| Int64(upcomingWindowDays) | ||
| ), | ||
| items: groupedItems.dueSoon | ||
| ), | ||
| SectionContent(title: String(localized: "today_section_later"), items: groupedItems.later), | ||
| SectionContent(title: String(localized: "today_section_unscheduled"), items: groupedItems.unscheduled) | ||
| ] | ||
|
|
||
| switch state.selectedSummaryScope { | ||
| case .all: | ||
| return allSections.filter { !$0.items.isEmpty } | ||
| case .focused: | ||
| return allSections.filter { $0.title == "집중할 일" && !$0.items.isEmpty } | ||
| return allSections.filter { $0.title == String(localized: "today_section_focused") && !$0.items.isEmpty } | ||
| case .overdue: | ||
| return allSections.filter { $0.title == "지난 마감" && !$0.items.isEmpty } | ||
| return allSections.filter { $0.title == String(localized: "today_section_overdue") && !$0.items.isEmpty } | ||
| case .dueSoon: | ||
| return allSections.filter { $0.title == "\(upcomingWindowDays)일 내 일정" && !$0.items.isEmpty } | ||
| return allSections.filter { | ||
| $0.title == String.localizedStringWithFormat( | ||
| String(localized: "today_section_due_soon_format"), | ||
| Int64(upcomingWindowDays) | ||
| ) && !$0.items.isEmpty | ||
| } | ||
| } |
There was a problem hiding this comment.
로컬라이즈된 문자열을 비교하여 섹션을 필터링하는 방식은 유지보수가 어렵고 비효율적입니다. SummaryScope 열거형을 활용하여 필요한 섹션만 직접 생성하여 반환하도록 개선할 수 있습니다.
switch state.selectedSummaryScope {
case .all:
return [
SectionContent(title: String(localized: "today_section_focused"), items: groupedItems.focused),
SectionContent(title: String(localized: "today_section_overdue"), items: groupedItems.overdue),
SectionContent(
title: String.localizedStringWithFormat(
String(localized: "today_section_due_soon_format"),
Int64(upcomingWindowDays)
),
items: groupedItems.dueSoon
),
SectionContent(title: String(localized: "today_section_later"), items: groupedItems.later),
SectionContent(title: String(localized: "today_section_unscheduled"), items: groupedItems.unscheduled)
].filter { !$0.items.isEmpty }
case .focused:
let items = groupedItems.focused
return items.isEmpty ? [] : [SectionContent(title: String(localized: "today_section_focused"), items: items)]
case .overdue:
let items = groupedItems.overdue
return items.isEmpty ? [] : [SectionContent(title: String(localized: "today_section_overdue"), items: items)]
case .dueSoon:
let items = groupedItems.dueSoon
let title = String.localizedStringWithFormat(String(localized: "today_section_due_soon_format"), Int64(upcomingWindowDays))
return items.isEmpty ? [] : [SectionContent(title: title, items: items)]
}| var body: some View { | ||
| TimelineView(.periodic(from: .now, by: 1.0)) { context in | ||
| Text(relativeTimeText(from: date, now: context.date) + " 업데이트") | ||
| Text(relativeTimeText(from: date, now: context.date) + String(localized: "relative_time_updated_suffix")) |
There was a problem hiding this comment.
문자열 결합(+)을 통한 로컬라이제이션은 언어별 어순 차이를 처리하기 어렵습니다. 포맷 스트링(예: "%@ 업데이트")을 사용하도록 개선이 필요합니다. (참고: relative_time_updated_format 키를 스트링 카탈로그에 추가해야 합니다.)
| Text(relativeTimeText(from: date, now: context.date) + String(localized: "relative_time_updated_suffix")) | |
| Text(String.localizedStringWithFormat(String(localized: "relative_time_updated_format"), relativeTimeText(from: date, now: context.date))) |
| let hourText = date.formatted(formatStyle) | ||
| let minuteText = date.formatted(.dateTime.minute(.twoDigits)) | ||
| return "\(hourText) \(minuteText)분" | ||
| return "\(hourText) \(minuteText)\(String(localized: "push_settings_minute_suffix"))" |
There was a problem hiding this comment.
|
/gemini review |
|
|
||
| struct SectionContent: Identifiable, Equatable { | ||
| var id: String { title } | ||
| var id: SummaryScope { scope } |
There was a problem hiding this comment.
SectionContent의 id가 SummaryScope 타입의 scope 프로퍼티를 반환하도록 변경되었습니다. 하지만 sections 프로퍼티의 구현을 보면, .all 스코프일 때 "나중 일정"(later)과 "일정 미정"(unscheduled) 섹션 모두 scope 값으로 .all을 사용하고 있습니다. 만약 두 섹션 모두 아이템이 존재하여 화면에 표시될 경우, List 내에서 중복된 ID가 발생하게 됩니다. 이는 SwiftUI에서 렌더링 오류나 예기치 않은 동작을 유발할 수 있으므로, 각 섹션이 고유한 ID를 가질 수 있도록 SummaryScope에 새로운 케이스를 추가하거나 id 생성 로직을 수정해야 합니다.
| var id: SummaryScope { scope } | |
| var id: String { title } |
| scope: .all, | ||
| title: String(localized: "today_section_later"), | ||
| items: items.later | ||
| ) | ||
| + makeSection( | ||
| scope: .all, | ||
| title: String(localized: "today_section_unscheduled"), | ||
| items: items.unscheduled | ||
| ) |