Skip to content

[#341] 영어 로컬라이제이션을 구현한다#342

Merged
opficdev merged 14 commits intodevelopfrom
feat/#341-Localization
Mar 31, 2026
Merged

[#341] 영어 로컬라이제이션을 구현한다#342
opficdev merged 14 commits intodevelopfrom
feat/#341-Localization

Conversation

@opficdev
Copy link
Copy Markdown
Owner

@opficdev opficdev self-assigned this Mar 31, 2026
Copy link
Copy Markdown
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

이번 pull request는 프로젝트 전반에 걸쳐 하드코딩된 한국어 문자열을 String(localized:) 및 String.localizedStringWithFormat을 사용하는 로컬라이제이션 방식으로 전환하는 대규모 작업을 포함하고 있습니다. 주요 피드백으로는 TodayViewModel에서 로컬라이즈된 문자열을 직접 비교하여 섹션을 필터링하는 로직을 열거형 기반으로 개선하여 유지보수성을 높일 것과, RelativeTimeText 및 PushNotificationSettingsView에서 문자열 결합 대신 표준 포맷팅 API를 사용하여 언어별 어순 및 시간 표기법 차이에 대응할 것을 권장했습니다.

Comment on lines 91 to 119
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
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

로컬라이즈된 문자열을 비교하여 섹션을 필터링하는 방식은 유지보수가 어렵고 비효율적입니다. 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"))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

문자열 결합(+)을 통한 로컬라이제이션은 언어별 어순 차이를 처리하기 어렵습니다. 포맷 스트링(예: "%@ 업데이트")을 사용하도록 개선이 필요합니다. (참고: relative_time_updated_format 키를 스트링 카탈로그에 추가해야 합니다.)

Suggested change
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"))"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

시간 형식을 수동으로 조합하는 대신 date.formatted(.dateTime.hour().minute())와 같은 표준 API를 사용하면 각 국가별 시간 표기법(AM/PM 위치 등)을 자동으로 준수할 수 있습니다.

Suggested change
return "\(hourText) \(minuteText)\(String(localized: "push_settings_minute_suffix"))"
return date.formatted(.dateTime.hour().minute())

@opficdev
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown
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

이 풀 요청은 앱 전반의 하드코딩된 문자열을 지역화된 문자열로 교체하고 프로젝트 설정을 업데이트하여 다국어 지원을 구현합니다. TodayViewModel.swift에서 SectionContent의 식별자를 SummaryScope로 변경하면서 '나중 일정'과 '일정 미정' 섹션이 동일한 ID를 갖게 되어 SwiftUI 렌더링 오류가 발생할 수 있는 문제가 발견되었습니다.


struct SectionContent: Identifiable, Equatable {
var id: String { title }
var id: SummaryScope { scope }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

SectionContentidSummaryScope 타입의 scope 프로퍼티를 반환하도록 변경되었습니다. 하지만 sections 프로퍼티의 구현을 보면, .all 스코프일 때 "나중 일정"(later)과 "일정 미정"(unscheduled) 섹션 모두 scope 값으로 .all을 사용하고 있습니다. 만약 두 섹션 모두 아이템이 존재하여 화면에 표시될 경우, List 내에서 중복된 ID가 발생하게 됩니다. 이는 SwiftUI에서 렌더링 오류나 예기치 않은 동작을 유발할 수 있으므로, 각 섹션이 고유한 ID를 가질 수 있도록 SummaryScope에 새로운 케이스를 추가하거나 id 생성 로직을 수정해야 합니다.

Suggested change
var id: SummaryScope { scope }
var id: String { title }

Comment on lines +115 to +123
scope: .all,
title: String(localized: "today_section_later"),
items: items.later
)
+ makeSection(
scope: .all,
title: String(localized: "today_section_unscheduled"),
items: items.unscheduled
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

위에서 언급한 바와 같이, 여기서 생성되는 두 섹션이 모두 scope: .all을 사용하고 있어 ID 충돌이 발생합니다. SectionContent 구조체 정의에서 id를 다시 title로 변경하거나, 각 섹션을 구분할 수 있는 고유한 값을 scope 인자로 전달해야 합니다.

@opficdev opficdev merged commit dc1d1f7 into develop Mar 31, 2026
1 check passed
@opficdev opficdev deleted the feat/#341-Localization branch March 31, 2026 14:22
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.

영어 로컬라이제이션을 구현한다

1 participant