diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index 2872c5b..450a02e 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -1767,17 +1767,59 @@ enum I18n { static var chatDeleteTitle: String { pick(ko: "채팅 삭제", en: "Delete chat", ja: "チャット削除") } } + enum LiveMain { + static var createLiveButton: String { + pick(ko: "라이브 만들기", en: "Create live", ja: "ライブを作成") + } + + static var replaySectionTitle: String { + pick(ko: "라이브 다시 듣기", en: "Live replay", ja: "ライブ再生") + } + } + enum LiveNow { static var allTitle: String { pick(ko: "지금 라이브 중 전체보기", en: "Live Now - All", ja: "ライブ配信中(全て)") } + static var sectionTitle: String { + pick(ko: "지금 라이브중", en: "Live now", ja: "ライブ配信中") + } + static var remaining: String { pick(ko: "잔여", en: "Remaining", ja: "残り") } + + static var emptyStateMessage: String { + pick( + ko: "마이페이지에서 본인인증을 하거나\n라이브를 예약하고 참여해보세요.", + en: "Verify your identity in My Page\nor reserve and join a live.", + ja: "マイページで本人認証を行うか、\nライブを予約して参加してください。" + ) + } + + static var refreshButton: String { + pick(ko: "새로고침", en: "Refresh", ja: "更新") + } + + static var followingChannelsTitle: String { + pick(ko: "팔로잉 채널", en: "Following channels", ja: "フォロー中のチャンネル") + } + + static var liveBadge: String { + pick(ko: "Live", en: "Live", ja: "Live") + } + + static var moreButton: String { + pick(ko: "더보기", en: "More", ja: "もっと見る") + } } enum LiveCancel { + static var title: String { + pick(ko: "예약취소", en: "Cancel reservation", ja: "予約キャンセル") + } + static var reasonPlaceholder: String { pick( ko: "취소사유를 입력하세요", @@ -1786,6 +1828,14 @@ enum I18n { ) } + static var cancelButton: String { + pick(ko: "취소", en: "Cancel", ja: "キャンセル") + } + + static var confirmButton: String { + pick(ko: "확인", en: "Confirm", ja: "確認") + } + static var reservationCanceled: String { pick( ko: "예약이 취소되었습니다.", @@ -1795,6 +1845,154 @@ enum I18n { } } + enum LiveReservation { + enum Section { + static var title: String { + pick(ko: "라이브 예약중", en: "Live reservations", ja: "ライブ予約中") + } + + static var emptyStateMessage: String { + pick( + ko: "지금 예약중인 라이브가 없습니다.\n채널을 팔로잉 하고 라이브 알림을 받아 보세요.", + en: "There are no live reservations right now.\nFollow channels and receive live notifications.", + ja: "現在予約中のライブはありません。\nチャンネルをフォローしてライブ通知を受け取りましょう。" + ) + } + } + + enum All { + static var title: String { + pick(ko: "라이브, 예약 캘린더", en: "Live reservation calendar", ja: "ライブ予約カレンダー") + } + + static var emptyStateMessage: String { + pick( + ko: "지금 예약중인 라이브가 없습니다.\n다른 날짜의 라이브를 예약하고 참여해 보세요.", + en: "There are no live reservations right now.\nReserve a live on another date and join.", + ja: "現在予約中のライブはありません。\n別の日のライブを予約して参加してみてください。" + ) + } + } + + enum Item { + static var reservationCompleted: String { + pick(ko: "예약완료", en: "Reserved", ja: "予約完了") + } + + static var ownCreatedLive: String { + pick(ko: "내가 개설한 라이브", en: "Live I created", ja: "自分が開設したライブ") + } + + static var free: String { + CreateContent.free + } + + static func month(_ value: String) -> String { + pick( + ko: "\(value)월", + en: "\(value)M", + ja: "\(value)月" + ) + } + + static func priceWithCan(_ can: Int) -> String { + pick( + ko: "\(can)캔", + en: "\(can) cans", + ja: "\(can)can" + ) + } + } + + enum Complete { + static var title: String { + pick(ko: "라이브 예약 완료", en: "Live reservation complete", ja: "ライブ予約完了") + } + + static var completedMessage: String { + pick(ko: "예약이 완료되었습니다.", en: "Your reservation is complete.", ja: "予約が完了しました。") + } + + static var reservationInfoTitle: String { + pick(ko: "라이브 예약정보", en: "Reservation details", ja: "ライブ予約情報") + } + + static var channelLabel: String { + pick(ko: "채널", en: "Channel", ja: "チャンネル") + } + + static var purchaseDetailLabel: String { + pick(ko: "구매내역", en: "Purchase", ja: "購入内容") + } + + static var reservationDateLabel: String { + pick(ko: "예약일자", en: "Reservation date", ja: "予約日時") + } + + static var liveCostLabel: String { + pick(ko: "라이브 비용", en: "Live price", ja: "ライブ料金") + } + + static var paymentInfoTitle: String { + pick(ko: "결제정보", en: "Payment info", ja: "決済情報") + } + + static var ownedCanLabel: String { + pick(ko: "보유캔", en: "Owned cans", ja: "保有can") + } + + static var paymentCanLabel: String { + pick(ko: "결제캔", en: "Paid cans", ja: "決済can") + } + + static var remainingCanLabel: String { + pick(ko: "잔여캔", en: "Remaining cans", ja: "残りcan") + } + + static var canSuffix: String { + pick(ko: " 캔", en: " cans", ja: " can") + } + + static var goHome: String { + pick(ko: "홈으로 이동", en: "Go to Home", ja: "ホームへ移動") + } + + static var goReservationList: String { + pick(ko: "예약 내역 이동", en: "View reservations", ja: "予約履歴へ移動") + } + } + } + + enum LiveChat { + static var staffBadge: String { + pick(ko: "스탭", en: "Staff", ja: "スタッフ") + } + + static var donationMemberSuffix: String { + pick(ko: "님이", en: "", ja: "さんが") + } + + static func canWithUnit(_ can: Int) -> String { + pick(ko: "\(can)캔", en: "\(can) cans", ja: "\(can)can") + } + + static var secretMissionDonationSuffix: String { + pick(ko: "으로 비밀미션을 보냈습니다.🤫", en: " sent a secret mission.🤫", ja: "で秘密ミッションを送りました。🤫") + } + + static var donationSuffix: String { + pick(ko: "을 후원하셨습니다.💰🪙", en: " donated.💰🪙", ja: "を後援しました。💰🪙") + } + + static var heartDonationSuffix: String { + pick(ko: "'님이 마음을 전했습니다 : 💕", en: "' sent a heart : 💕", ja: "'さんがハートを送りました : 💕") + } + + static var joinSuffix: String { + pick(ko: "'님이 입장하셨습니다.", en: "' joined.", ja: "'さんが入場しました。") + } + } + enum CreateContent { static var selectFile: String { pick(ko: "파일 선택", en: "Select file", ja: "ファイル選択") } static var selectTheme: String { pick(ko: "테마 선택", en: "Select theme", ja: "テーマ選択") } @@ -1899,6 +2097,7 @@ enum I18n { static var cannotReserveOwnLive: String { pick(ko: "내가 만든 라이브는 예약할 수 없습니다.", en: "reserve a live you created is required.", ja: "自分が作ったライブは予約できません。") } static var enterLiveFailed: String { pick(ko: "라이브에 입장하지 못했습니다.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다.", en: "Could not enter the live.\nIf the problem persists, please contact customer support.", ja: "ライブに入室できませんでした。\n問題が続く場合はカスタマーサポートにお問い合わせください。") } static var fetchLiveInfoFailed: String { pick(ko: "라이브 정보를 가져오지 못했습니다.\n다시 시도해 주세요.", en: "Failed to fetch live information.\nPlease try again.", ja: "ライブ情報を取得できませんでした。\nもう一度お試しください。") } + static var alreadyEndedLive: String { pick(ko: "이미 종료된 라이브 입니다.", en: "This live has already ended.", ja: "このライブはすでに終了しています。") } static var userBlocked: String { pick(ko: "차단하였습니다.", en: "User has been blocked.", ja: "ブロックしました。") } static var userUnblocked: String { pick(ko: "차단이 해제 되었습니다.", en: "User has been unblocked.", ja: "ブロックを解除しました。") } static var blockDialogTitle: String { pick(ko: "사용자 차단", en: "Block User", ja: "ユーザーブロック") } diff --git a/SodaLive/Sources/Live/Cancel/LiveCancelDialog.swift b/SodaLive/Sources/Live/Cancel/LiveCancelDialog.swift index 814c48c..dc3b5a2 100644 --- a/SodaLive/Sources/Live/Cancel/LiveCancelDialog.swift +++ b/SodaLive/Sources/Live/Cancel/LiveCancelDialog.swift @@ -19,7 +19,7 @@ struct LiveCancelDialog: View { var body: some View { VStack(spacing: 0) { - Text("예약취소") + Text(I18n.LiveCancel.title) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color(hex: "bbbbbb")) .padding(.top, 40) @@ -39,7 +39,7 @@ struct LiveCancelDialog: View { .padding(.top, 13.3) HStack(spacing: 13.3) { - Text("취소") + Text(I18n.LiveCancel.cancelButton) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color(hex: "3bb9f1")) .padding(.vertical, 16) @@ -54,7 +54,7 @@ struct LiveCancelDialog: View { isShowCancelPopup = false } - Text("확인") + Text(I18n.LiveCancel.confirmButton) .appFont(size: 18.3, weight: .bold) .foregroundColor(.white) .padding(.vertical, 16) diff --git a/SodaLive/Sources/Live/LiveReplayListView.swift b/SodaLive/Sources/Live/LiveReplayListView.swift index cd5349f..8832897 100644 --- a/SodaLive/Sources/Live/LiveReplayListView.swift +++ b/SodaLive/Sources/Live/LiveReplayListView.swift @@ -19,7 +19,7 @@ struct LiveReplayListView: View { var body: some View { VStack(spacing: 14) { HStack(spacing: 0) { - Text("라이브 다시 듣기") + Text(I18n.LiveMain.replaySectionTitle) .appFont(size: 24, weight: .bold) .foregroundColor(.white) diff --git a/SodaLive/Sources/Live/LiveView.swift b/SodaLive/Sources/Live/LiveView.swift index 945a118..18eca29 100644 --- a/SodaLive/Sources/Live/LiveView.swift +++ b/SodaLive/Sources/Live/LiveView.swift @@ -141,7 +141,7 @@ struct LiveView: View { .resizable() .frame(width: 20, height: 20) - Text("라이브 만들기") + Text(I18n.LiveMain.createLiveButton) .appFont(size: 13.3, weight: .bold) .foregroundColor(.white) } diff --git a/SodaLive/Sources/Live/LiveViewModel.swift b/SodaLive/Sources/Live/LiveViewModel.swift index 4a7b354..cb950eb 100644 --- a/SodaLive/Sources/Live/LiveViewModel.swift +++ b/SodaLive/Sources/Live/LiveViewModel.swift @@ -42,7 +42,7 @@ final class LiveViewModel: ObservableObject { @Published var liveStartDate: String? = nil @Published var nowDate: String? = nil - let paymentDialogCancelTitle = "취소" + let paymentDialogCancelTitle = I18n.Common.cancel var page = 1 var isLast = false @@ -104,13 +104,13 @@ final class LiveViewModel: ObservableObject { if let message = decoded.message { self.errorMessage = message } else { - self.errorMessage = "라이브에 입장하지 못했습니다.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.MemberChannel.enterLiveFailed } self.isShowPopup = true } } catch { - self.errorMessage = "라이브에 입장하지 못했습니다.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.MemberChannel.enterLiveFailed self.isShowPopup = true } } @@ -151,13 +151,13 @@ final class LiveViewModel: ObservableObject { if let message = decoded.message { self.errorMessage = message } else { - self.errorMessage = "라이브에 입장하지 못했습니다.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.MemberChannel.enterLiveFailed } self.isShowPopup = true } } catch { - self.errorMessage = "라이브에 입장하지 못했습니다.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.MemberChannel.enterLiveFailed self.isShowPopup = true } } @@ -189,13 +189,13 @@ final class LiveViewModel: 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 } } @@ -245,13 +245,13 @@ final class LiveViewModel: 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 } } @@ -297,13 +297,13 @@ final class LiveViewModel: 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 } } @@ -314,7 +314,7 @@ final class LiveViewModel: ObservableObject { func reservationLiveRoom(roomId: Int) { getRoomDetail(roomId: roomId) { [unowned self] in if ($0.manager.id == UserDefaults.int(forKey: .userId)) { - self.errorMessage = "내가 만든 라이브는 예약할 수 없습니다." + self.errorMessage = I18n.MemberChannel.cannotReserveOwnLive self.isShowPopup = true } else { if $0.isPrivateRoom { @@ -326,9 +326,9 @@ final class LiveViewModel: ObservableObject { if ($0.price == 0 || $0.isPaid) { self.reservation(roomId: roomId) } else { - self.paymentDialogTitle = "\($0.price)캔으로 예약" - self.paymentDialogDesc = "'\($0.title)' 라이브에 참여하기 위해 결제합니다." - self.paymentDialogConfirmTitle = "결제 후 예약하기" + self.paymentDialogTitle = I18n.MemberChannel.reserveWithCansTitle($0.price) + self.paymentDialogDesc = I18n.MemberChannel.reservePaymentDesc($0.title) + self.paymentDialogConfirmTitle = I18n.MemberChannel.reservePaymentConfirmTitle self.paymentDialogConfirmAction = { [unowned self] in hidePopup() reservation(roomId: roomId) @@ -375,13 +375,13 @@ final class LiveViewModel: ObservableObject { if hours >= 1 { self.liveStartDate = beginDate.convertDateFormat(dateFormat: "yyyy-MM-dd, HH:mm") self.nowDate = now.convertDateFormat(dateFormat: "yyyy-MM-dd, HH:mm") - self.paymentDialogDesc2 = "라이브를 시작한 지 \(hours)시간 \(minutes)분이 지났습니다. 라이브에 입장 후 30분 이내에 라이브가 종료될 수도 있습니다." + self.paymentDialogDesc2 = I18n.MemberChannel.elapsedLiveWarning(hours: hours, minutes: minutes) } } - self.paymentDialogTitle = "유료 라이브 입장" - self.paymentDialogDesc = "\($0.price)캔을 차감하고\n라이브에 입장 하시겠습니까?" - self.paymentDialogConfirmTitle = "결제 후 참여하기" + self.paymentDialogTitle = I18n.MemberChannel.paidLiveEnterTitle + self.paymentDialogDesc = I18n.MemberChannel.paidLiveEnterDesc($0.price) + self.paymentDialogConfirmTitle = I18n.MemberChannel.paidLiveConfirmTitle self.paymentDialogConfirmAction = { [unowned self] in hidePopup() self.enterRoom(roomId: roomId) @@ -425,18 +425,18 @@ final class LiveViewModel: ObservableObject { } else { if let message = decoded.message { if message.contains("종료") { - self.errorMessage = "이미 종료된 라이브 입니다." + self.errorMessage = I18n.MemberChannel.alreadyEndedLive } else { self.errorMessage = message } } else { - self.errorMessage = "라이브 정보를 가져오지 못했습니다.\n다시 시도해 주세요." + self.errorMessage = I18n.MemberChannel.fetchLiveInfoFailed } self.isShowPopup = true } } catch { - self.errorMessage = "라이브 정보를 가져오지 못했습니다.\n다시 시도해 주세요." + self.errorMessage = I18n.MemberChannel.fetchLiveInfoFailed self.isShowPopup = true } @@ -471,13 +471,13 @@ final class LiveViewModel: 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/Live/Now/All/LiveNowAllView.swift b/SodaLive/Sources/Live/Now/All/LiveNowAllView.swift index 2ac9751..7c360a5 100644 --- a/SodaLive/Sources/Live/Now/All/LiveNowAllView.swift +++ b/SodaLive/Sources/Live/Now/All/LiveNowAllView.swift @@ -105,15 +105,14 @@ struct LiveNowAllView: View { if isShowAuthConfirmView { SodaDialog( - title: "본인인증", - desc: "청소년 보호를 위해\n본인인증을 완료한\n성인만 라이브 입장이 가능합니다.\n" + - "라이브 입장을 위해\n본인인증을 진행해 주세요.", - confirmButtonTitle: "본인인증 하러가기", + title: I18n.Main.Auth.dialogTitle, + desc: I18n.Main.Auth.liveEntryVerificationDescription, + confirmButtonTitle: I18n.Main.Auth.goToVerification, confirmButtonAction: { isShowAuthConfirmView = false isShowAuthView = true }, - cancelButtonTitle: "취소", + cancelButtonTitle: I18n.Common.cancel, cancelButtonAction: { isShowAuthConfirmView = false pendingAction = nil @@ -142,7 +141,7 @@ struct LiveNowAllView: View { isShowAuthView = false } .onError { _ in - AppState.shared.errorMessage = "본인인증 중 오류가 발생했습니다." + AppState.shared.errorMessage = I18n.Main.Auth.authenticationError AppState.shared.isShowErrorPopup = true isShowAuthView = false } diff --git a/SodaLive/Sources/Live/Now/LiveNowItemView.swift b/SodaLive/Sources/Live/Now/LiveNowItemView.swift index 3b2d92f..39ad21c 100644 --- a/SodaLive/Sources/Live/Now/LiveNowItemView.swift +++ b/SodaLive/Sources/Live/Now/LiveNowItemView.swift @@ -118,7 +118,7 @@ struct LiveNowItemView: View { .padding(.horizontal, 2) .padding(.bottom, 2) } else { - Text("무료") + Text(I18n.LiveReservation.Item.free) .appFont(size: 14, weight: .regular) .foregroundColor(Color(hex: "#263238")) .padding(.vertical, 4) diff --git a/SodaLive/Sources/Live/Now/SectionLiveNowView.swift b/SodaLive/Sources/Live/Now/SectionLiveNowView.swift index a9998a9..ef5afb2 100644 --- a/SodaLive/Sources/Live/Now/SectionLiveNowView.swift +++ b/SodaLive/Sources/Live/Now/SectionLiveNowView.swift @@ -19,14 +19,14 @@ struct SectionLiveNowView: View { var body: some View { LazyVStack(spacing: 13.3) { HStack(spacing: 0) { - Text("지금 라이브중") + Text(I18n.LiveNow.sectionTitle) .appFont(size: 24, weight: .bold) .foregroundColor(.white) Spacer() if items.count > 0 { - Text("전체보기") + Text(I18n.Common.viewAll) .appFont(size: 14, weight: .regular) .foregroundColor(Color(hex: "78909C")) .onTapGesture { AppState.shared.setAppStep(step: .liveNowAll(onClickParticipant: onClickParticipant)) } @@ -54,7 +54,7 @@ struct SectionLiveNowView: View { .resizable() .frame(width: 60, height: 60) - Text("마이페이지에서 본인인증을 하거나\n라이브를 예약하고 참여해보세요.") + Text(I18n.LiveNow.emptyStateMessage) .appFont(size: 13, weight: .medium) .foregroundColor(Color(hex: "bbbbbb")) .fixedSize(horizontal: false, vertical: true) @@ -72,7 +72,7 @@ struct SectionLiveNowView: View { HStack(spacing: 8) { Image("ic_refresh") - Text("새로고침") + Text(I18n.LiveNow.refreshButton) .appFont(size: 14.7, weight: .medium) .foregroundColor(Color.grayd2) } diff --git a/SodaLive/Sources/Live/RecommendChannel/SectionRecommendChannelView.swift b/SodaLive/Sources/Live/RecommendChannel/SectionRecommendChannelView.swift index 16f7f96..8cca76b 100644 --- a/SodaLive/Sources/Live/RecommendChannel/SectionRecommendChannelView.swift +++ b/SodaLive/Sources/Live/RecommendChannel/SectionRecommendChannelView.swift @@ -17,7 +17,7 @@ struct SectionRecommendChannelView: View { var body: some View { VStack(spacing: 16) { HStack(spacing: 0) { - Text("팔로잉 채널") + Text(I18n.LiveNow.followingChannelsTitle) .appFont(size: 24, weight: .bold) .foregroundColor(.white) @@ -55,7 +55,7 @@ struct SectionRecommendChannelView: View { ) if item.isOnAir { - Text("Live") + Text(I18n.LiveNow.liveBadge) .appFont(size: 8.7, weight: .bold) .foregroundColor(.white) .padding(.vertical, 2.7) @@ -87,7 +87,7 @@ struct SectionRecommendChannelView: View { .resizable() .frame(width: screenSize().width * 0.18, height: screenSize().width * 0.18, alignment: .center) - Text("더보기") + Text(I18n.LiveNow.moreButton) .appFont(size: 14, weight: .regular) .foregroundColor(.white) .frame(width: screenSize().width * 0.18) diff --git a/SodaLive/Sources/Live/Reservation/All/LiveReservationAllItemView.swift b/SodaLive/Sources/Live/Reservation/All/LiveReservationAllItemView.swift index 1ac9abd..b5d1e33 100644 --- a/SodaLive/Sources/Live/Reservation/All/LiveReservationAllItemView.swift +++ b/SodaLive/Sources/Live/Reservation/All/LiveReservationAllItemView.swift @@ -55,7 +55,7 @@ struct LiveReservationAllItemView: View { Spacer() if item.isReservation { - Text("예약완료") + Text(I18n.LiveReservation.Item.reservationCompleted) .appFont(size: 11.3, weight: .medium) .foregroundColor(Color(hex: "d2d2d2")) .padding(.horizontal, 7) @@ -63,7 +63,7 @@ struct LiveReservationAllItemView: View { .background(Color(hex: "533d89")) .cornerRadius(10) } else { - Text(item.price > 0 ? "\(item.price)캔" : "무료") + Text(item.price > 0 ? I18n.LiveReservation.Item.priceWithCan(item.price) : I18n.LiveReservation.Item.free) .appFont(size: 12, weight: .medium) .foregroundColor(Color(hex: "e2e2e2").opacity(0.49)) .padding(.bottom, 6.7) diff --git a/SodaLive/Sources/Live/Reservation/All/LiveReservationAllView.swift b/SodaLive/Sources/Live/Reservation/All/LiveReservationAllView.swift index bdd8507..3b5ce58 100644 --- a/SodaLive/Sources/Live/Reservation/All/LiveReservationAllView.swift +++ b/SodaLive/Sources/Live/Reservation/All/LiveReservationAllView.swift @@ -21,7 +21,7 @@ struct LiveReservationAllView: View { BaseView(isLoading: $viewModel.isLoading) { GeometryReader { proxy in VStack(spacing: 0) { - DetailNavigationBar(title: "라이브, 예약 캘린더") + DetailNavigationBar(title: I18n.LiveReservation.All.title) WeekCalendarView { date in viewModel.selectedDateString = date @@ -57,7 +57,7 @@ struct LiveReservationAllView: View { .resizable() .frame(width: 60, height: 60) - Text("지금 예약중인 라이브가 없습니다.\n다른 날짜의 라이브를 예약하고 참여해 보세요.") + Text(I18n.LiveReservation.All.emptyStateMessage) .appFont(size: 13, weight: .medium) .foregroundColor(Color(hex: "bbbbbb")) .multilineTextAlignment(.center) diff --git a/SodaLive/Sources/Live/Reservation/Complete/LiveReservationCompleteView.swift b/SodaLive/Sources/Live/Reservation/Complete/LiveReservationCompleteView.swift index ae3bca6..d8657db 100644 --- a/SodaLive/Sources/Live/Reservation/Complete/LiveReservationCompleteView.swift +++ b/SodaLive/Sources/Live/Reservation/Complete/LiveReservationCompleteView.swift @@ -14,13 +14,13 @@ struct LiveReservationCompleteView: View { var body: some View { BaseView { VStack(spacing: 0) { - DetailNavigationBar(title: "라이브 예약 완료") { + DetailNavigationBar(title: I18n.LiveReservation.Complete.title) { AppState.shared.setAppStep(step: .main) } ScrollView(.vertical, showsIndicators: false) { VStack(spacing: 0) { - Text("예약이 완료되었습니다.") + Text(I18n.LiveReservation.Complete.completedMessage) .appFont(size: 20, weight: .bold) .foregroundColor(Color(hex: "a285eb")) .frame(width: screenSize().width - 26.7, alignment: .leading) @@ -33,14 +33,14 @@ struct LiveReservationCompleteView: View { .padding(.top, 16.7) .padding(.bottom, 26.7) - Text("라이브 예약정보") + Text(I18n.LiveReservation.Complete.reservationInfoTitle) .appFont(size: 16.7, weight: .bold) .foregroundColor(Color(hex: "eeeeee")) .frame(width: screenSize().width - 53.4, alignment: .leading) VStack(spacing: 6.7) { HStack(spacing: 26.7) { - Text("채널") + Text(I18n.LiveReservation.Complete.channelLabel) .appFont(size: 14.7, weight: .medium) .foregroundColor(Color(hex: "777777")) @@ -51,7 +51,7 @@ struct LiveReservationCompleteView: View { .frame(width: screenSize().width - 53.4, alignment: .leading) HStack(spacing: 26.7) { - Text("구매내역") + Text(I18n.LiveReservation.Complete.purchaseDetailLabel) .appFont(size: 14.7, weight: .medium) .foregroundColor(Color(hex: "777777")) @@ -62,7 +62,7 @@ struct LiveReservationCompleteView: View { .frame(width: screenSize().width - 53.4, alignment: .leading) HStack(spacing: 26.7) { - Text("예약일자") + Text(I18n.LiveReservation.Complete.reservationDateLabel) .appFont(size: 14.7, weight: .medium) .foregroundColor(Color(hex: "777777")) @@ -77,7 +77,7 @@ struct LiveReservationCompleteView: View { .frame(width: screenSize().width - 53.4, alignment: .leading) HStack(spacing: 26.7) { - Text("라이브 비용") + Text(I18n.LiveReservation.Complete.liveCostLabel) .appFont(size: 14.7, weight: .medium) .foregroundColor(Color(hex: "777777")) @@ -94,14 +94,14 @@ struct LiveReservationCompleteView: View { .foregroundColor(Color(hex: "232323")) .padding(.vertical, 20) - Text("결제정보") + Text(I18n.LiveReservation.Complete.paymentInfoTitle) .appFont(size: 16.7, weight: .bold) .foregroundColor(Color(hex: "eeeeee")) .frame(width: screenSize().width - 53.4, alignment: .leading) VStack(spacing: 13.3) { HStack(spacing: 0) { - Text("보유캔") + Text(I18n.LiveReservation.Complete.ownedCanLabel) .appFont(size: 14.7, weight: .medium) .foregroundColor(Color(hex: "777777")) @@ -111,14 +111,14 @@ struct LiveReservationCompleteView: View { .appFont(size: 15.3, weight: .bold) .foregroundColor(Color(hex: "eeeeee")) - Text(" 캔") + Text(I18n.LiveReservation.Complete.canSuffix) .appFont(size: 14.7, weight: .medium) .foregroundColor(Color(hex: "eeeeee")) } .frame(width: screenSize().width - 53.4, alignment: .leading) HStack(spacing: 0) { - Text("결제캔") + Text(I18n.LiveReservation.Complete.paymentCanLabel) .appFont(size: 14.7, weight: .medium) .foregroundColor(Color(hex: "777777")) @@ -128,14 +128,14 @@ struct LiveReservationCompleteView: View { .appFont(size: 15.3, weight: .bold) .foregroundColor(Color(hex: "eeeeee")) - Text(" 캔") + Text(I18n.LiveReservation.Complete.canSuffix) .appFont(size: 14.7, weight: .medium) .foregroundColor(Color(hex: "eeeeee")) } .frame(width: screenSize().width - 53.4, alignment: .leading) HStack(spacing: 0) { - Text("잔여캔") + Text(I18n.LiveReservation.Complete.remainingCanLabel) .appFont(size: 14.7, weight: .medium) .foregroundColor(Color(hex: "777777")) @@ -145,7 +145,7 @@ struct LiveReservationCompleteView: View { .appFont(size: 15.3, weight: .bold) .foregroundColor(Color(hex: "eeeeee")) - Text(" 캔") + Text(I18n.LiveReservation.Complete.canSuffix) .appFont(size: 14.7, weight: .medium) .foregroundColor(Color(hex: "eeeeee")) } @@ -154,7 +154,7 @@ struct LiveReservationCompleteView: View { .padding(.top, 20) HStack(spacing: 13.3) { - Text("홈으로 이동") + Text(I18n.LiveReservation.Complete.goHome) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color(hex: "9970ff")) .padding(.vertical, 16) @@ -170,7 +170,7 @@ struct LiveReservationCompleteView: View { AppState.shared.setAppStep(step: .main) } - Text("예약 내역 이동") + Text(I18n.LiveReservation.Complete.goReservationList) .appFont(size: 18.3, weight: .bold) .foregroundColor(.white) .padding(.vertical, 16) diff --git a/SodaLive/Sources/Live/Reservation/LiveReservationItemView.swift b/SodaLive/Sources/Live/Reservation/LiveReservationItemView.swift index 45a902f..cfac7a8 100644 --- a/SodaLive/Sources/Live/Reservation/LiveReservationItemView.swift +++ b/SodaLive/Sources/Live/Reservation/LiveReservationItemView.swift @@ -67,7 +67,7 @@ struct LiveReservationItemView: View { VStack(alignment: .trailing, spacing: 8) { VStack(spacing: 0) { - Text("\(dateDic["month"] ?? "")월") + Text(I18n.LiveReservation.Item.month(dateDic["month"] ?? "")) .appFont(size: 14, weight: .bold) .foregroundColor(.white) .padding(.vertical, 6) @@ -100,7 +100,7 @@ struct LiveReservationItemView: View { .background(Color(hex: "3b5ff1")) .cornerRadius(4) } else { - Text("무료") + Text(I18n.LiveReservation.Item.free) .appFont(size: 14, weight: .regular) .foregroundColor(Color(hex: "#263238")) .padding(4) diff --git a/SodaLive/Sources/Live/Reservation/MyLiveReservationItemView.swift b/SodaLive/Sources/Live/Reservation/MyLiveReservationItemView.swift index 891f69f..3f3ef86 100644 --- a/SodaLive/Sources/Live/Reservation/MyLiveReservationItemView.swift +++ b/SodaLive/Sources/Live/Reservation/MyLiveReservationItemView.swift @@ -21,7 +21,7 @@ struct MyLiveReservationItemView: View { HStack(spacing: 8) { Image("ic_mic_colored") - Text("내가 개설한 라이브") + Text(I18n.LiveReservation.Item.ownCreatedLive) .appFont(size: 18, weight: .bold) .foregroundColor(Color(hex: "80D8FF")) } @@ -79,7 +79,7 @@ struct MyLiveReservationItemView: View { VStack(alignment: .trailing, spacing: 8) { VStack(spacing: 4) { - Text("\(dateDic["month"] ?? "")월") + Text(I18n.LiveReservation.Item.month(dateDic["month"] ?? "")) .appFont(size: 14, weight: .bold) .foregroundColor(.white) .padding(.vertical, 6) @@ -112,7 +112,7 @@ struct MyLiveReservationItemView: View { .background(Color(hex: "3b5ff1")) .cornerRadius(4) } else { - Text("무료") + Text(I18n.LiveReservation.Item.free) .appFont(size: 14, weight: .regular) .foregroundColor(Color(hex: "#263238")) .padding(4) diff --git a/SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift b/SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift index ce39156..4e72f97 100644 --- a/SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift +++ b/SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift @@ -20,14 +20,14 @@ struct SectionLiveReservationView: View { var body: some View { VStack(spacing: 13.3) { HStack(spacing: 0) { - Text("라이브 예약중") + Text(I18n.LiveReservation.Section.title) .appFont(size: 24, weight: .bold) .foregroundColor(.white) Spacer() if items.count > 0 { - Text("전체보기") + Text(I18n.Common.viewAll) .appFont(size: 14, weight: .regular) .foregroundColor(Color(hex: "78909C")) .onTapGesture { @@ -101,7 +101,7 @@ struct SectionLiveReservationView: View { .resizable() .frame(width: 60, height: 60) - Text("지금 예약중인 라이브가 없습니다.\n채널을 팔로잉 하고 라이브 알림을 받아 보세요.") + Text(I18n.LiveReservation.Section.emptyStateMessage) .appFont(size: 13, weight: .medium) .foregroundColor(Color(hex: "bbbbbb")) .fixedSize(horizontal: false, vertical: true) diff --git a/SodaLive/Sources/Live/Room/Chat/LiveRoomChatItemView.swift b/SodaLive/Sources/Live/Room/Chat/LiveRoomChatItemView.swift index 4572f03..ea45081 100644 --- a/SodaLive/Sources/Live/Room/Chat/LiveRoomChatItemView.swift +++ b/SodaLive/Sources/Live/Room/Chat/LiveRoomChatItemView.swift @@ -104,7 +104,7 @@ struct LiveRoomChatItemView: View { VStack(alignment: .leading, spacing: 6.7) { HStack(spacing: 5) { if chatMessage.rank == -3 { - Text("스탭") + Text(I18n.LiveChat.staffBadge) .appFont(size: 10) .foregroundColor(.white) .padding(2) diff --git a/SodaLive/Sources/Live/Room/Chat/LiveRoomDonationChatItemView.swift b/SodaLive/Sources/Live/Room/Chat/LiveRoomDonationChatItemView.swift index 9117c99..fc23ce1 100644 --- a/SodaLive/Sources/Live/Room/Chat/LiveRoomDonationChatItemView.swift +++ b/SodaLive/Sources/Live/Room/Chat/LiveRoomDonationChatItemView.swift @@ -40,17 +40,17 @@ struct LiveRoomDonationChatItemView: View { .appFont(size: 12) .foregroundColor(.white) - Text("님이") + Text(I18n.LiveChat.donationMemberSuffix) .appFont(size: 12, weight: .light) .foregroundColor(.white) } HStack(spacing: 0) { - Text("\(chatMessage.can)캔") + Text(I18n.LiveChat.canWithUnit(chatMessage.can)) .appFont(size: 15) .foregroundColor(Color(hex: "fdca2f")) - Text(chatMessage.chat.contains("비밀") ? "으로 비밀미션을 보냈습니다.🤫" : "을 후원하셨습니다.💰🪙") + Text(chatMessage.chat.contains("비밀") ? I18n.LiveChat.secretMissionDonationSuffix : I18n.LiveChat.donationSuffix) .appFont(size: 15) .foregroundColor(.white) } diff --git a/SodaLive/Sources/Live/Room/Chat/LiveRoomHeartDonationChatItemView.swift b/SodaLive/Sources/Live/Room/Chat/LiveRoomHeartDonationChatItemView.swift index d00bf11..a01a5fd 100644 --- a/SodaLive/Sources/Live/Room/Chat/LiveRoomHeartDonationChatItemView.swift +++ b/SodaLive/Sources/Live/Room/Chat/LiveRoomHeartDonationChatItemView.swift @@ -21,7 +21,7 @@ struct LiveRoomHeartDonationChatItemView: View { .appFont(size: 12, weight: .bold) .foregroundColor(Color(hex: "ec3aa6")) - Text("'님이 마음을 전했습니다 : 💕") + Text(I18n.LiveChat.heartDonationSuffix) .appFont(size: 12) .foregroundColor(Color.gray11) } diff --git a/SodaLive/Sources/Live/Room/Chat/LiveRoomJoinChatItemView.swift b/SodaLive/Sources/Live/Room/Chat/LiveRoomJoinChatItemView.swift index cab2dbf..9d8f83a 100644 --- a/SodaLive/Sources/Live/Room/Chat/LiveRoomJoinChatItemView.swift +++ b/SodaLive/Sources/Live/Room/Chat/LiveRoomJoinChatItemView.swift @@ -28,7 +28,7 @@ struct LiveRoomJoinChatItemView: View { .appFont(size: 12, weight: .bold) .foregroundColor(Color.mainYellow) - Text("'님이 입장하셨습니다.") + Text(I18n.LiveChat.joinSuffix) .appFont(size: 12) .foregroundColor(Color.grayee) } diff --git a/docs/20260331_하드코딩텍스트_I18n통일계획.md b/docs/20260331_하드코딩텍스트_I18n통일계획.md index c2111a8..dfa5c31 100644 --- a/docs/20260331_하드코딩텍스트_I18n통일계획.md +++ b/docs/20260331_하드코딩텍스트_I18n통일계획.md @@ -319,28 +319,28 @@ ### Live (56) #### Group 1 (1-10) -- [ ] `SodaLive/Sources/Live/Cancel/LiveCancelDialog.swift` -- [ ] `SodaLive/Sources/Live/LatestFinishedLiveItemView.swift` -- [ ] `SodaLive/Sources/Live/LiveReplayListView.swift` -- [ ] `SodaLive/Sources/Live/LiveView.swift` -- [ ] `SodaLive/Sources/Live/LiveViewModel.swift` -- [ ] `SodaLive/Sources/Live/Now/All/LiveNowAllItemView.swift` -- [ ] `SodaLive/Sources/Live/Now/All/LiveNowAllView.swift` -- [ ] `SodaLive/Sources/Live/Now/LiveNowItemView.swift` -- [ ] `SodaLive/Sources/Live/Now/SectionLiveNowView.swift` -- [ ] `SodaLive/Sources/Live/RecommendChannel/SectionRecommendChannelView.swift` +- [x] `SodaLive/Sources/Live/Cancel/LiveCancelDialog.swift` +- [x] `SodaLive/Sources/Live/LatestFinishedLiveItemView.swift` +- [x] `SodaLive/Sources/Live/LiveReplayListView.swift` +- [x] `SodaLive/Sources/Live/LiveView.swift` +- [x] `SodaLive/Sources/Live/LiveViewModel.swift` +- [x] `SodaLive/Sources/Live/Now/All/LiveNowAllItemView.swift` +- [x] `SodaLive/Sources/Live/Now/All/LiveNowAllView.swift` +- [x] `SodaLive/Sources/Live/Now/LiveNowItemView.swift` +- [x] `SodaLive/Sources/Live/Now/SectionLiveNowView.swift` +- [x] `SodaLive/Sources/Live/RecommendChannel/SectionRecommendChannelView.swift` #### Group 2 (11-20) -- [ ] `SodaLive/Sources/Live/Reservation/All/LiveReservationAllItemView.swift` -- [ ] `SodaLive/Sources/Live/Reservation/All/LiveReservationAllView.swift` -- [ ] `SodaLive/Sources/Live/Reservation/Complete/LiveReservationCompleteView.swift` -- [ ] `SodaLive/Sources/Live/Reservation/LiveReservationItemView.swift` -- [ ] `SodaLive/Sources/Live/Reservation/MyLiveReservationItemView.swift` -- [ ] `SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift` -- [ ] `SodaLive/Sources/Live/Room/Chat/LiveRoomChatItemView.swift` -- [ ] `SodaLive/Sources/Live/Room/Chat/LiveRoomDonationChatItemView.swift` -- [ ] `SodaLive/Sources/Live/Room/Chat/LiveRoomHeartDonationChatItemView.swift` -- [ ] `SodaLive/Sources/Live/Room/Chat/LiveRoomJoinChatItemView.swift` +- [x] `SodaLive/Sources/Live/Reservation/All/LiveReservationAllItemView.swift` +- [x] `SodaLive/Sources/Live/Reservation/All/LiveReservationAllView.swift` +- [x] `SodaLive/Sources/Live/Reservation/Complete/LiveReservationCompleteView.swift` +- [x] `SodaLive/Sources/Live/Reservation/LiveReservationItemView.swift` +- [x] `SodaLive/Sources/Live/Reservation/MyLiveReservationItemView.swift` +- [x] `SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift` +- [x] `SodaLive/Sources/Live/Room/Chat/LiveRoomChatItemView.swift` +- [x] `SodaLive/Sources/Live/Room/Chat/LiveRoomDonationChatItemView.swift` +- [x] `SodaLive/Sources/Live/Room/Chat/LiveRoomHeartDonationChatItemView.swift` +- [x] `SodaLive/Sources/Live/Room/Chat/LiveRoomJoinChatItemView.swift` #### Group 3 (21-30) - [ ] `SodaLive/Sources/Live/Room/Chat/LiveRoomRouletteDonationChatItemView.swift` @@ -892,3 +892,41 @@ - 공통 키 재사용 정리: `I18n.Common.viewAll`, `I18n.Common.latestContent`, `I18n.Settings.companyInfo`, `I18n.Chat.Auth.*`, `I18n.LiveRoom.follow/following` 적용. - Oracle 후속 보정: 홈 FAB 버튼 문구를 제목형 키(`uploadTitle`)에서 CTA 전용 키(`I18n.CreateContent.uploadAction`)로 분리해 영문/일문 의미를 버튼 행동과 일치시킴. - Home Group 1 체크박스 9개 `- [x]` 완료 반영. + +### 18차 구현 (Live 모듈 Group 1~2, 20개 파일 처리, 2026-04-01) +- 무엇/왜/어떻게: + - 무엇: `변경 대상 파일 전체 목록`의 `Live` Group 1~2(20개 파일)를 전수 점검하고, 런타임 사용자 노출 하드코딩 문구를 `I18n.*`로 전환했다. + - 왜: Live 메인/실시간 목록/예약/채팅 아이템에 하드코딩 문구가 남아 있어 모듈 간 i18n 접근이 불일치했고, 동일 의미 문구가 ViewModel에 중복되어 유지보수 비용이 높았기 때문이다. + - 어떻게: explore/librarian 병렬 탐색 + `grep`/`ast_grep_search` 직접 점검으로 대상 문자열을 분류한 뒤, `I18n.swift`에 Live 전용 키셋(`LiveMain`, `LiveNow`, `LiveReservation`, `LiveChat`)을 추가하고 호출부를 치환했다. 기존 공통 키(`I18n.Common`, `I18n.MemberChannel`, `I18n.Main.Auth`)는 재사용했다. +- 실행 명령/도구: + - `task(subagent_type="explore", ...)` x2 (`bg_d093725e`, `bg_d4acf3b2`) + - `task(subagent_type="librarian", ...)` x2 (`bg_cfe29077`, `bg_b4c29632`) + - `background_output(task_id=...)` x4 (위 4개 task 결과 수집) + - `grep("\"[^\"]*[가-힣][^\"]*\"", include=대상파일, path=SodaLive/Sources/Live/**)` + - `grep("String\\(localized:|LocalizedStringKey\\(|NSLocalizedString\\(", include=*.swift, path=SodaLive/Sources/Live)` + - `ast_grep_search(pattern="Text(\"$TEXT\")", lang=swift, paths=[SodaLive/Sources/Live])` + - `bash: rg -n ...` (`command not found` 확인) + - `lsp_diagnostics(filePath=변경 파일 전체)` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test` +- 결과: + - `I18n.swift` 추가/확장 키셋: + - 신규: `I18n.LiveMain`, `I18n.LiveReservation(Section/All/Item/Complete)`, `I18n.LiveChat` + - 확장: `I18n.LiveNow(sectionTitle/emptyStateMessage/refreshButton/followingChannelsTitle/liveBadge/moreButton)`, `I18n.LiveCancel(title/cancelButton/confirmButton)`, `I18n.MemberChannel.alreadyEndedLive` + - 치환 완료 파일(실치환 18개): + - `LiveCancelDialog.swift`, `LiveReplayListView.swift`, `LiveView.swift`, `LiveViewModel.swift` + - `LiveNowAllView.swift`, `LiveNowItemView.swift`, `SectionLiveNowView.swift`, `SectionRecommendChannelView.swift` + - `LiveReservationAllItemView.swift`, `LiveReservationAllView.swift`, `LiveReservationCompleteView.swift`, `LiveReservationItemView.swift`, `MyLiveReservationItemView.swift`, `SectionLiveReservationView.swift` + - `LiveRoomChatItemView.swift`, `LiveRoomDonationChatItemView.swift`, `LiveRoomHeartDonationChatItemView.swift`, `LiveRoomJoinChatItemView.swift` + - 점검만 수행(실치환 없음, 체크 완료 2개): + - `LatestFinishedLiveItemView.swift` (런타임 고정 문구 없음, 표시값은 API 기반) + - `LiveNowAllItemView.swift` (런타임 문구가 기존 `I18n` 참조 또는 데이터 바인딩) + - Group 1~2 체크박스 20개 `- [x]` 반영 완료. + - 대상 재탐지 결과, 잔여 한글 리터럴은 Preview 샘플/SDK 입력값(`payload.pg`, `payload.method`, `payload.orderName`)/서버 메시지 분기 비교(`message.contains("종료")`)만 존재. + - 빌드 검증: + - `SodaLive` Debug 빌드 성공(`** BUILD SUCCEEDED **`). + - `SodaLive-dev` Debug 빌드는 병렬 실행 시 `build.db` lock으로 1회 실패 후, 단독 재실행에서 성공(`** BUILD SUCCEEDED **`). + - 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 테스트 액션 미구성 확인(코드 실패 아님, 스킴 제약). + - LSP 진단: SourceKit 단독 해석 환경에서 외부 모듈/프로젝트 심볼 미해결 오류(`RefreshableScrollView`, `Kingfisher`, `AppState` 등)가 보고되나, 동일 변경셋은 `xcodebuild` 실컴파일 통과로 검증 완료.