diff --git a/SodaLive/Resources/Localizable.xcstrings b/SodaLive/Resources/Localizable.xcstrings index 96d0b6f..39e0bb6 100644 --- a/SodaLive/Resources/Localizable.xcstrings +++ b/SodaLive/Resources/Localizable.xcstrings @@ -80,6 +80,9 @@ } } } + }, + " %lld" : { + }, " OFF" : { "localizations" : { @@ -4065,18 +4068,18 @@ } } }, - "모집중" : { + "모집완료" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Recruiting" + "value" : "Recruitment closed" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "募集中" + "value" : "募集終了" } } } @@ -4097,18 +4100,18 @@ } } }, - "모집완료" : { + "모집중" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Recruitment closed" + "value" : "Recruiting" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "募集終了" + "value" : "募集中" } } } @@ -6721,6 +6724,102 @@ } } }, + "이벤트 참여하기" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join the event" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "イベントに参加する" + } + } + } + }, + "이용약관" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terms of service" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "利用規約" + } + } + } + }, + "인기" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Popular" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "人気" + } + } + } + }, + "인기순" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "By popularity" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "人気順" + } + } + } + }, + "인증완료" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verification completed" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "認証完了" + } + } + } + }, + "일" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sun" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "日" + } + } + } + }, "이메일" : { "localizations" : { "en" : { @@ -6801,38 +6900,6 @@ } } }, - "이벤트 참여하기" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Join the event" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "イベントに参加する" - } - } - } - }, - "이용약관" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Terms of service" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "利用規約" - } - } - } - }, "이전화" : { "localizations" : { "en" : { @@ -6849,22 +6916,6 @@ } } }, - "인기" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Popular" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "人気" - } - } - } - }, "인기 시리즈" : { "localizations" : { "en" : { @@ -6945,54 +6996,6 @@ } } }, - "인기순" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "By popularity" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "人気順" - } - } - } - }, - "인증완료" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verification completed" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "認証完了" - } - } - } - }, - "일" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sun" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "日" - } - } - } - }, "일간 랭킹" : { "localizations" : { "en" : { @@ -9635,4 +9638,4 @@ } }, "version" : "1.1" -} +} \ No newline at end of file diff --git a/SodaLive/Sources/Home/HomeLatestContentView.swift b/SodaLive/Sources/Home/HomeLatestContentView.swift index 73acb16..12367f1 100644 --- a/SodaLive/Sources/Home/HomeLatestContentView.swift +++ b/SodaLive/Sources/Home/HomeLatestContentView.swift @@ -14,7 +14,7 @@ struct HomeLatestContentView: View { let contentList: [AudioContentMainItem] let selectTheme: (String) -> Void - @State private var selectedTheme = "전체" + @State private var selectedTheme = I18n.Category.all let rows = [ GridItem(.flexible(), alignment: .leading), diff --git a/SodaLive/Sources/Home/HomeTabViewModel.swift b/SodaLive/Sources/Home/HomeTabViewModel.swift index bca1319..98384d2 100644 --- a/SodaLive/Sources/Home/HomeTabViewModel.swift +++ b/SodaLive/Sources/Home/HomeTabViewModel.swift @@ -40,10 +40,10 @@ final class HomeTabViewModel: ObservableObject { @Published var recommendContentList: [AudioContentMainItem] = [] private let sortType = [ - "매출": ContentRankingSortType.REVENUE, - "판매량": ContentRankingSortType.SALES_COUNT, - "댓글": ContentRankingSortType.COMMENT_COUNT, - "좋아요": ContentRankingSortType.LIKE_COUNT + I18n.RankingSort.revenue: ContentRankingSortType.REVENUE, + I18n.RankingSort.salesVolume: ContentRankingSortType.SALES_COUNT, + I18n.RankingSort.comments: ContentRankingSortType.COMMENT_COUNT, + I18n.RankingSort.likes: ContentRankingSortType.LIKE_COUNT ] func fetchData() { @@ -67,7 +67,7 @@ final class HomeTabViewModel: ObservableObject { if let data = decoded.data, decoded.success { self.liveList = data.liveList self.creatorRanking = data.creatorRanking - self.latestContentThemeList = ["전체"] + data.latestContentThemeList + self.latestContentThemeList = [I18n.Category.all] + data.latestContentThemeList self.latestContentList = data.latestContentList self.eventBannerList = data.bannerList self.originalAudioDramaList = data.originalAudioDramaList @@ -82,13 +82,13 @@ final class HomeTabViewModel: ObservableObject { if let message = decoded.message { self.errorMessage = message } else { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError } self.isShowPopup = true } } catch { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError self.isShowPopup = true } @@ -100,7 +100,7 @@ final class HomeTabViewModel: ObservableObject { func getLatestContentByTheme(theme: String) { isLoading = true - repository.getLatestContentByTheme(theme: theme == "전체" ? "" : theme) + repository.getLatestContentByTheme(theme: theme == I18n.Category.all ? "" : theme) .sink { result in switch result { case .finished: @@ -121,13 +121,13 @@ final class HomeTabViewModel: ObservableObject { if let message = decoded.message { self.errorMessage = message } else { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError } self.isShowPopup = true } } catch { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError self.isShowPopup = true } @@ -160,13 +160,13 @@ final class HomeTabViewModel: ObservableObject { if let message = decoded.message { self.errorMessage = message } else { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError } self.isShowPopup = true } } catch { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError self.isShowPopup = true } @@ -198,13 +198,13 @@ final class HomeTabViewModel: ObservableObject { if let message = decoded.message { self.errorMessage = message } else { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError } self.isShowPopup = true } } catch { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError self.isShowPopup = true } } @@ -236,13 +236,13 @@ final class HomeTabViewModel: ObservableObject { if let message = decoded.message { self.errorMessage = message } else { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError } self.isShowPopup = true } } catch { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError self.isShowPopup = true } } @@ -274,13 +274,13 @@ final class HomeTabViewModel: ObservableObject { if let message = decoded.message { self.errorMessage = message } else { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError } self.isShowPopup = true } } catch { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError self.isShowPopup = true } } diff --git a/SodaLive/Sources/Home/HomeWeeklyChartView.swift b/SodaLive/Sources/Home/HomeWeeklyChartView.swift index f68f9c3..0c2f7b7 100644 --- a/SodaLive/Sources/Home/HomeWeeklyChartView.swift +++ b/SodaLive/Sources/Home/HomeWeeklyChartView.swift @@ -20,8 +20,13 @@ struct HomeWeeklyChartView: View { let contentList: [GetAudioContentRankingItem] let onTapSort: (String) -> Void - let sortList = ["매출", "판매량", "댓글", "좋아요"] - @State private var selectedSort = "매출" + let sortList = [ + I18n.RankingSort.revenue, + I18n.RankingSort.salesVolume, + I18n.RankingSort.comments, + I18n.RankingSort.likes + ] + @State private var selectedSort = I18n.RankingSort.revenue var body: some View { VStack(spacing: 16) { diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift new file mode 100644 index 0000000..ae788aa --- /dev/null +++ b/SodaLive/Sources/I18n/I18n.swift @@ -0,0 +1,66 @@ +// +// I18n.swift +// SodaLive +// +// Created by Junie (AI) on 2025/12/17. +// + +import Foundation + +// MARK: - I18n 네임스페이스 +// String Catalog를 사용하지 않는 컨텍스트에서 사용할 하드코딩 맵 기반 i18n. +// 기준 언어 선택은 LanguageHeaderProvider.current("ko"|"en"|"ja"). +enum I18n { + enum Common { + // 기본 샘플들 + static var apply: String { pick(ko: "적용", en: "Apply", ja: "適用") } + static var confirm: String { pick(ko: "확인", en: "Confirm", ja: "確認") } + static var cancel: String { pick(ko: "취소", en: "Cancel", ja: "キャンセル") } + + // 설정 + static var settings: String { pick(ko: "설정", en: "Settings", ja: "設定") } + + static var commonError: String { pick(ko: "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다.", en: "Settings", ja: "設定") } + } + + enum Category { + static var all: String { pick(ko: "전체", en: "All", ja: "すべて") } + } + + enum RankingSort { + // 분석/지표 등 + static var revenue: String { pick(ko: "매출", en: "Revenue", ja: "売上高") } + static var salesVolume: String { pick(ko: "판매량", en: "Sales", ja: "販売量") } + static var comments: String { pick(ko: "댓글", en: "Comments", ja: "コメント") } + static var likes: String { pick(ko: "좋아요", en: "Likes", ja: "いいね") } + } + + enum Tab { + // 탭/도메인 + static var character: String { pick(ko: "캐릭터", en: "Character", ja: "キャラクター") } + static var work: String { pick(ko: "작품", en: "Work", ja: "作品") } + static var talk: String { pick(ko: "톡", en: "Talk", ja: "トーク") } + } +} + +// MARK: - 내부 헬퍼 +@inline(__always) +private func pick(ko: String, en: String, ja: String) -> String { + switch LanguageHeaderProvider.current { + case "ko": return ko + case "ja": return ja + default: return en + } +} + +/* + 사용 예시 (ViewModel, Service 등 Text 이외 컨텍스트): + + // ViewModel 내부 + let title = I18n.Common.all + + // View 내부(Text 대신 다른 UI 요소 라벨 등) + let menuLabel = I18n.Common.settings + + 주의: 기존에 Text("...")와 같은 String Catalog 사용부는 그대로 유지합니다. +*/