diff --git a/SodaLive/Resources/Localizable.xcstrings b/SodaLive/Resources/Localizable.xcstrings index 595a5da..90ea4b2 100644 --- a/SodaLive/Resources/Localizable.xcstrings +++ b/SodaLive/Resources/Localizable.xcstrings @@ -437,6 +437,7 @@ } }, "(채팅 12개) 바로 대화 시작" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2304,6 +2305,7 @@ } }, "결제 후 입장" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -6931,6 +6933,134 @@ } } }, + "이용약관" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terms of service" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "利用規約" + } + } + } + }, + "이전화" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Previous episode" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "前の話" + } + } + } + }, + "인기" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Popular" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "人気" + } + } + } + }, + "인기 캐릭터 채팅" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Top character" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "人気キャラチャット" + } + } + } + }, + "인기 콘텐츠" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Popular content" + } + }, + "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" : { @@ -7011,54 +7141,6 @@ } } }, - "이용약관" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Terms of service" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "利用規約" - } - } - } - }, - "이전화" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Previous episode" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "前の話" - } - } - } - }, - "인기" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Popular" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "人気" - } - } - } - }, "인기 시리즈" : { "extractionState" : "stale", "localizations" : { @@ -7092,38 +7174,6 @@ } } }, - "인기 캐릭터 채팅" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Top character" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "人気キャラチャット" - } - } - } - }, - "인기 콘텐츠" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Popular content" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "人気コンテンツ" - } - } - } - }, "인기 크리에이터" : { "localizations" : { "en" : { @@ -7140,54 +7190,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" : "日" - } - } - } - }, "일간 랭킹" : { "extractionState" : "stale", "localizations" : { @@ -8331,6 +8333,7 @@ } }, "최근 대화한 캐릭터 " : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { diff --git a/SodaLive/Sources/CustomView/ExpandableTextView.swift b/SodaLive/Sources/CustomView/ExpandableTextView.swift index 140cdd9..dd197f5 100644 --- a/SodaLive/Sources/CustomView/ExpandableTextView.swift +++ b/SodaLive/Sources/CustomView/ExpandableTextView.swift @@ -43,7 +43,7 @@ struct ExpandableTextView: View { Spacer() Image(isExpanded ? "ic_live_detail_top" : "ic_live_detail_bottom") - Text(isExpanded ? "접기" : "펼치기") + Text(isExpanded ? I18n.CustomView.collapse : I18n.CustomView.expand) .appFont(size: 12, weight: .medium) .foregroundColor(Color.graybb) Spacer() diff --git a/SodaLive/Sources/Dialog/ApplyAuditionCompleteDialog.swift b/SodaLive/Sources/Dialog/ApplyAuditionCompleteDialog.swift index f442e57..1969326 100644 --- a/SodaLive/Sources/Dialog/ApplyAuditionCompleteDialog.swift +++ b/SodaLive/Sources/Dialog/ApplyAuditionCompleteDialog.swift @@ -20,12 +20,12 @@ struct ApplyAuditionCompleteDialog: View { .opacity(0.5) VStack(spacing: 0) { - Text("오디션 지원") + Text(I18n.Audition.Apply.title) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color.grayee) .padding(.top, 26.7) - - Text("보이스온 오디션에 지원해 주셔서 감사합니다.") + + Text(I18n.Dialog.ApplyAuditionComplete.thankYouDescription) .appFont(size: 15, weight: .medium) .foregroundColor(Color.graybb) .padding(.top, 15) @@ -40,7 +40,7 @@ struct ApplyAuditionCompleteDialog: View { .foregroundColor(Color.graybb) .padding(.top, 10) - Text("확인") + Text(I18n.Common.confirm) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color.white) .padding(.vertical, 16) diff --git a/SodaLive/Sources/Dialog/CommunityPostPurchaseDialog.swift b/SodaLive/Sources/Dialog/CommunityPostPurchaseDialog.swift index 2b783a9..5032f56 100644 --- a/SodaLive/Sources/Dialog/CommunityPostPurchaseDialog.swift +++ b/SodaLive/Sources/Dialog/CommunityPostPurchaseDialog.swift @@ -22,12 +22,12 @@ struct CommunityPostPurchaseDialog: View { .frame(width: geo.size.width, height: geo.size.height) VStack(spacing: 0) { - Text("게시글 보기") + Text(I18n.Dialog.CommunityPostPurchase.title) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color.graybb) .padding(.top, 40) - Text("게시글을\n확인하시겠습니까?") + Text(I18n.Dialog.CommunityPostPurchase.description) .appFont(size: 15, weight: .medium) .foregroundColor(Color.graybb) .multilineTextAlignment(.center) @@ -35,7 +35,7 @@ struct CommunityPostPurchaseDialog: View { .padding(.horizontal, 13.3) HStack(spacing: 13.3) { - Text("취소") + Text(I18n.Common.cancel) .appFont(size: 15.3, weight: .bold) .foregroundColor(Color.button) .padding(.vertical, 16) @@ -50,7 +50,7 @@ struct CommunityPostPurchaseDialog: View { isShowing = false } - Text("\(can)캔으로 보기") + Text(I18n.Dialog.CommunityPostPurchase.viewWithCans(can)) .appFont(size: 15.3, weight: .bold) .foregroundColor(Color.white) .padding(.vertical, 16) diff --git a/SodaLive/Sources/Dialog/CreatorFollowNotifyDialog.swift b/SodaLive/Sources/Dialog/CreatorFollowNotifyDialog.swift index 1b3e78a..b0b197e 100644 --- a/SodaLive/Sources/Dialog/CreatorFollowNotifyDialog.swift +++ b/SodaLive/Sources/Dialog/CreatorFollowNotifyDialog.swift @@ -28,7 +28,7 @@ struct CreatorFollowNotifyDialog: View { if isShow { VStack(alignment: .leading, spacing: 24) { - Text("알림") + Text(I18n.Common.alertTitle) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color.grayee) diff --git a/SodaLive/Sources/Dialog/LivePaymentDialog.swift b/SodaLive/Sources/Dialog/LivePaymentDialog.swift index 92c0ced..c2f461f 100644 --- a/SodaLive/Sources/Dialog/LivePaymentDialog.swift +++ b/SodaLive/Sources/Dialog/LivePaymentDialog.swift @@ -36,7 +36,7 @@ struct LivePaymentDialog: View { if let startDateTime = startDateTime, let nowDateTime = nowDateTime, let desc = desc2 { VStack(spacing: 13.3) { HStack(spacing: 6.7) { - Text("- 시작 시각 : ") + Text(I18n.Dialog.LivePayment.startTimePrefix) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color.graybb) @@ -46,7 +46,7 @@ struct LivePaymentDialog: View { } HStack(spacing: 6.7) { - Text("- 현재 시각 :") + Text(I18n.Dialog.LivePayment.currentTimePrefix) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color.graybb) .multilineTextAlignment(.leading) @@ -70,7 +70,7 @@ struct LivePaymentDialog: View { } HStack(spacing: 13.3) { - Text("취소") + Text(cancelButtonTitle) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color.button) .padding(.vertical, 16) @@ -83,7 +83,7 @@ struct LivePaymentDialog: View { ) .onTapGesture { cancelButtonAction() } - Text("결제 후 입장") + Text(confirmButtonTitle) .appFont(size: 18.3, weight: .bold) .padding(.vertical, 16) .frame(maxWidth: .infinity) @@ -106,9 +106,9 @@ struct LivePaymentDialog: View { title: "유료 라이브 입장", desc: "OO캔을 차감하고\n라이브에 입장 하시겠습니까?", desc2: "라이브가 시작한 지 1시간 10분이 지났습니다. 라이브에 입장 후 30분 이내에 라이브가 종료될 수도 있습니다.", - confirmButtonTitle: "", + confirmButtonTitle: I18n.MemberChannel.paidLiveConfirmTitle, confirmButtonAction: {}, - cancelButtonTitle: "", + cancelButtonTitle: I18n.Common.cancel, cancelButtonAction: {}, startDateTime: "2024-01-01 15:00", nowDateTime: "2024-01-02 15:00" diff --git a/SodaLive/Sources/Dialog/LiveRoomPasswordDialog.swift b/SodaLive/Sources/Dialog/LiveRoomPasswordDialog.swift index 1d6cd01..60c010b 100644 --- a/SodaLive/Sources/Dialog/LiveRoomPasswordDialog.swift +++ b/SodaLive/Sources/Dialog/LiveRoomPasswordDialog.swift @@ -25,12 +25,12 @@ struct LiveRoomPasswordDialog: View { .frame(width: geo.size.width, height: geo.size.height) VStack(spacing: 0) { - Text("비밀번호 입력") + Text(I18n.Dialog.LiveRoomPassword.title) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color(hex: "bbbbbb")) .padding(.top, 40) - Text("비공개 라이브의 입장 비밀번호를\n입력해 주세요.") + Text(I18n.Dialog.LiveRoomPassword.description) .appFont(size: 13, weight: .medium) .foregroundColor(Color(hex: "bbbbbb")) .multilineTextAlignment(.center) @@ -38,8 +38,8 @@ struct LiveRoomPasswordDialog: View { .padding(.horizontal, 13.3) UserTextField( - title: "비밀번호", - hint: "비밀번호를 입력해 주세요", + title: I18n.Dialog.LiveRoomPassword.passwordFieldTitle, + hint: I18n.Dialog.LiveRoomPassword.passwordFieldPlaceholder, isSecure: false, variable: $password, keyboardType: .numberPad @@ -48,7 +48,7 @@ struct LiveRoomPasswordDialog: View { .padding(.top, 13.3) HStack(spacing: 13.3) { - Text("취소") + Text(I18n.Common.cancel) .appFont(size: 15.3, weight: .bold) .foregroundColor(Color(hex: "3bb9f1")) .padding(.vertical, 16) @@ -73,7 +73,7 @@ struct LiveRoomPasswordDialog: View { .resizable() .frame(width: 20, height: 20) - Text("으로 입장") + Text(I18n.Dialog.LiveRoomPassword.enterSuffix) .appFont(size: 15.3, weight: .bold) .foregroundColor(Color(hex: "ffffff")) } @@ -90,7 +90,7 @@ struct LiveRoomPasswordDialog: View { isShowing = false } } else { - Text("입장하기") + Text(I18n.Dialog.LiveRoomPassword.enter) .appFont(size: 15.3, weight: .bold) .foregroundColor(Color(hex: "ffffff")) .padding(.vertical, 16) diff --git a/SodaLive/Sources/Dialog/MemberProfileDialog.swift b/SodaLive/Sources/Dialog/MemberProfileDialog.swift index 24a0e58..4564581 100644 --- a/SodaLive/Sources/Dialog/MemberProfileDialog.swift +++ b/SodaLive/Sources/Dialog/MemberProfileDialog.swift @@ -25,7 +25,7 @@ struct MemberProfileDialog: View { VStack(alignment: .leading, spacing: 21) { HStack(spacing: 0) { - Text("프로필") + Text(I18n.Dialog.MemberProfile.title) .appFont(size: 15, weight: .bold) .foregroundColor(Color.grayee) @@ -56,7 +56,7 @@ struct MemberProfileDialog: View { .cornerRadius(8) HStack(spacing: 8) { - Text(profile.isBlocked ? "차단 해제" : "차단") + Text(profile.isBlocked ? I18n.Dialog.MemberProfile.unblock : I18n.MemberChannel.blockAction) .appFont(size: 15, weight: .bold) .foregroundColor(Color.button) .frame(maxWidth: .infinity) @@ -76,7 +76,7 @@ struct MemberProfileDialog: View { } } - Text("사용자 신고") + Text(I18n.Dialog.MemberProfile.reportUser) .appFont(size: 15, weight: .bold) .foregroundColor(Color.button) .frame(maxWidth: .infinity) @@ -90,7 +90,7 @@ struct MemberProfileDialog: View { ) .onTapGesture { viewModel.isShowUesrReportView = true } - Text("프로필 신고") + Text(I18n.Dialog.MemberProfile.reportProfile) .appFont(size: 15, weight: .bold) .foregroundColor(Color.button) .frame(maxWidth: .infinity) @@ -115,7 +115,7 @@ struct MemberProfileDialog: View { .frame(maxWidth: screenSize().width - 33.3) .onAppear { if memberId <= 1 { - viewModel.errorMessage = "잘못된 요청입니다." + viewModel.errorMessage = I18n.Dialog.MemberProfile.invalidRequest viewModel.isShowPopup = true } else { viewModel.getMemberProfile(memberId: memberId) diff --git a/SodaLive/Sources/Follow/FollowCreatorView.swift b/SodaLive/Sources/Follow/FollowCreatorView.swift index dd24ad8..ad3f0ef 100644 --- a/SodaLive/Sources/Follow/FollowCreatorView.swift +++ b/SodaLive/Sources/Follow/FollowCreatorView.swift @@ -18,10 +18,10 @@ struct FollowCreatorView: View { var body: some View { BaseView(isLoading: $viewModel.isLoading) { VStack(spacing: 0) { - DetailNavigationBar(title: String(localized: "팔로잉 리스트")) + DetailNavigationBar(title: I18n.Follow.followingListTitle) HStack(spacing: 0) { - Text("총") + Text(I18n.Follow.totalPrefix) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color.grayee) @@ -29,7 +29,7 @@ struct FollowCreatorView: View { .appFont(size: 13.3, weight: .medium) .foregroundColor(Color.mainRed3) - Text("명") + Text(I18n.Follow.personUnit) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color.grayee) @@ -70,7 +70,7 @@ struct FollowCreatorView: View { .padding(.top, 13.3) } } else { - Text("팔로우 중인 채널이 없습니다.") + Text(I18n.Follow.emptyFollowingChannels) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color.grayee) .frame(maxHeight: .infinity) diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index ef3463a..6eb925a 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -1654,6 +1654,231 @@ If you block this user, the following features will be restricted. } } + enum ImagePicker { + static var cropResizeGuide: String { + pick( + ko: "모서리 원을 드래그해서 크롭 영역 크기를 조정하세요", + en: "Drag the corner handles to resize the crop area.", + ja: "角のハンドルをドラッグしてクロップ範囲のサイズを調整してください。" + ) + } + } + + enum CustomView { + static var collapse: String { + pick(ko: "접기", en: "Collapse", ja: "閉じる") + } + + static var expand: String { + pick(ko: "펼치기", en: "Expand", ja: "展開") + } + } + + enum IAP { + static var loadProductsFailed: String { + pick( + ko: "상품을 불러오지 못했습니다.\n다시 시도해 주세요.", + en: "Could not load products.\nPlease try again.", + ja: "商品を読み込めませんでした。\nもう一度お試しください。" + ) + } + + static var deferredPaymentFailed: String { + pick( + ko: "아이폰이 잠김 등의 이유로 결제를 진행하지 못했습니다.", + en: "Could not proceed with payment because the iPhone is locked.", + ja: "iPhoneがロックされているなどの理由で決済を進められませんでした。" + ) + } + + static var productValidationCompleted: String { + pick( + ko: "상품 검증을 하였습니다.", + en: "Product validation is complete.", + ja: "商品検証が完了しました。" + ) + } + + static var unknownError: String { + pick( + ko: "알 수 없는 오류가 발생했습니다.", + en: "An unknown error occurred.", + ja: "不明なエラーが発生しました。" + ) + } + + static var paymentFailed: String { + pick( + ko: "결제를 진행하지 못했습니다.\n다시 시도해 주세요.", + en: "Could not complete payment.\nPlease try again.", + ja: "決済を完了できませんでした。\nもう一度お試しください。" + ) + } + } + + enum Follow { + static var followingListTitle: String { + pick(ko: "팔로잉 리스트", en: "Following list", ja: "フォロー中リスト") + } + + static var totalPrefix: String { + pick(ko: "총", en: "Total", ja: "合計") + } + + static var personUnit: String { + pick(ko: "명", en: "people", ja: "人") + } + + static var emptyFollowingChannels: String { + pick( + ko: "팔로우 중인 채널이 없습니다.", + en: "There are no channels you follow.", + ja: "フォロー中のチャンネルがありません。" + ) + } + } + + enum Main { + enum EventPopup { + static var doNotShowAgain: String { + pick(ko: "다시보지 않기", en: "Don't show again", ja: "今後表示しない") + } + + static var close: String { + pick(ko: "닫기", en: "Close", ja: "閉じる") + } + } + + enum Tab { + static var home: String { pick(ko: "홈", en: "Home", ja: "ホーム") } + static var live: String { pick(ko: "라이브", en: "Live", ja: "ライブ") } + static var chat: String { pick(ko: "채팅", en: "Chat", ja: "チャット") } + static var my: String { pick(ko: "마이", en: "My", ja: "マイ") } + } + + enum Auth { + static var dialogTitle: String { + pick(ko: "본인인증", en: "Identity verification", ja: "本人認証") + } + + static var liveEntryVerificationDescription: String { + pick( + ko: "청소년 보호를 위해\n본인인증을 완료한\n성인만 라이브 입장이 가능합니다.\n라이브 입장을 위해\n본인인증을 진행해 주세요.", + en: "Only adults who completed identity verification can enter live rooms for youth protection.\nPlease complete identity verification to enter live.", + ja: "青少年保護のため、本人認証を完了した成人のみライブに入場できます。\nライブ入場のために本人認証を行ってください。" + ) + } + + static var goToVerification: String { + pick(ko: "본인인증 하러가기", en: "Verify identity", ja: "本人認証へ") + } + + static var authenticationError: String { + pick( + ko: "본인인증 중 오류가 발생했습니다.", + en: "An error occurred during identity verification.", + ja: "本人認証中にエラーが発生しました。" + ) + } + } + } + + enum Dialog { + enum ApplyAuditionComplete { + static var thankYouDescription: String { + pick( + ko: "보이스온 오디션에 지원해 주셔서 감사합니다.", + en: "Thank you for applying to the VoiceOn audition.", + ja: "VoiceOnオーディションにご応募いただきありがとうございます。" + ) + } + } + + enum CommunityPostPurchase { + static var title: String { + pick(ko: "게시글 보기", en: "View post", ja: "投稿を見る") + } + + static var description: String { + pick( + ko: "게시글을\n확인하시겠습니까?", + en: "Do you want to\nview this post?", + ja: "投稿を\n確認しますか?" + ) + } + + static func viewWithCans(_ can: Int) -> String { + pick( + ko: "\(can)캔으로 보기", + en: "View with \(can) cans", + ja: "\(can)canで見る" + ) + } + } + + enum LivePayment { + static var startTimePrefix: String { + pick(ko: "- 시작 시각 : ", en: "- Start time : ", ja: "- 開始時刻 : ") + } + + static var currentTimePrefix: String { + pick(ko: "- 현재 시각 :", en: "- Current time :", ja: "- 現在時刻 :") + } + } + + enum LiveRoomPassword { + static var title: String { + pick(ko: "비밀번호 입력", en: "Enter password", ja: "パスワード入力") + } + + static var description: String { + pick( + ko: "비공개 라이브의 입장 비밀번호를\n입력해 주세요.", + en: "Please enter the entry password\nfor this private live.", + ja: "非公開ライブの入場パスワードを\n入力してください。" + ) + } + + static var passwordFieldTitle: String { + pick(ko: "비밀번호", en: "Password", ja: "パスワード") + } + + static var passwordFieldPlaceholder: String { + pick(ko: "비밀번호를 입력해 주세요", en: "Please enter your password", ja: "パスワードを入力してください") + } + + static var enterSuffix: String { + pick(ko: "으로 입장", en: " to enter", ja: "で入場") + } + + static var enter: String { + pick(ko: "입장하기", en: "Enter", ja: "入場する") + } + } + + enum MemberProfile { + static var title: String { + pick(ko: "프로필", en: "Profile", ja: "プロフィール") + } + + static var unblock: String { + pick(ko: "차단 해제", en: "Unblock", ja: "ブロック解除") + } + + static var reportUser: String { + pick(ko: "사용자 신고", en: "Report user", ja: "ユーザーを通報") + } + + static var reportProfile: String { + pick(ko: "프로필 신고", en: "Report profile", ja: "プロフィールを通報") + } + + static var invalidRequest: String { + pick(ko: "잘못된 요청입니다.", en: "Invalid request.", ja: "不正なリクエストです。") + } + } + } + enum Series { static var new: String { pick(ko: "신작", en: "New", ja: "新作") } static var complete: String { pick(ko: "완결", en: "Completed", ja: "完結") } diff --git a/SodaLive/Sources/IAP/StoreManager.swift b/SodaLive/Sources/IAP/StoreManager.swift index 9ce36d5..50a6202 100644 --- a/SodaLive/Sources/IAP/StoreManager.swift +++ b/SodaLive/Sources/IAP/StoreManager.swift @@ -79,7 +79,7 @@ extension StoreManager: SKProductsRequestDelegate { DEBUG_LOG("상품불러오기 실패: \(error)") DispatchQueue.main.async { [unowned self] in self.isLoading = false - errorMessage = "상품을 불러오지 못했습니다.\n다시 시도해 주세요." + errorMessage = I18n.IAP.loadProductsFailed self.isShowPopup = true } } @@ -108,7 +108,7 @@ extension StoreManager: SKPaymentTransactionObserver { case .deferred: isLoading = false DEBUG_LOG("아이폰이 잠김 등의 이유로 결제를 진행하지 못했습니다.") - errorMessage = "아이폰이 잠김 등의 이유로 결제를 진행하지 못했습니다." + errorMessage = I18n.IAP.deferredPaymentFailed isShowPopup = true SKPaymentQueue.default().finishTransaction(transaction) @@ -118,7 +118,7 @@ extension StoreManager: SKPaymentTransactionObserver { case .restored: isLoading = false DEBUG_LOG("상품 검증을 하였습니다.") - errorMessage = "상품 검증을 하였습니다." + errorMessage = I18n.IAP.productValidationCompleted isShowPopup = true SKPaymentQueue.default().finishTransaction(transaction) @@ -128,7 +128,7 @@ extension StoreManager: SKPaymentTransactionObserver { @unknown default: isLoading = false DEBUG_LOG("알 수 없는 오류가 발생했습니다.") - errorMessage = "알 수 없는 오류가 발생했습니다." + errorMessage = I18n.IAP.unknownError isShowPopup = true SKPaymentQueue.default().finishTransaction(transaction) @@ -173,7 +173,7 @@ extension StoreManager: SKPaymentTransactionObserver { } DispatchQueue.main.async { [unowned self] in - errorMessage = "결제를 진행하지 못했습니다.\n다시 시도해 주세요." + errorMessage = I18n.IAP.paymentFailed isShowPopup = true } diff --git a/SodaLive/Sources/ImagePicker/ImagePicker.swift b/SodaLive/Sources/ImagePicker/ImagePicker.swift index 5319697..a8b1af2 100644 --- a/SodaLive/Sources/ImagePicker/ImagePicker.swift +++ b/SodaLive/Sources/ImagePicker/ImagePicker.swift @@ -112,7 +112,7 @@ struct ImageCropEditorView: View { VStack(spacing: 0) { HStack { Button(action: onCancel) { - Text("취소") + Text(I18n.Common.cancel) .appFont(size: 16.7, weight: .bold) .foregroundColor(Color.grayee) } @@ -126,7 +126,7 @@ struct ImageCropEditorView: View { onCancel() } }) { - Text("적용") + Text(I18n.Common.apply) .appFont(size: 16.7, weight: .bold) .foregroundColor(Color.button) } @@ -233,7 +233,7 @@ struct ImageCropEditorView: View { } if aspectPolicy == .free { - Text("모서리 원을 드래그해서 크롭 영역 크기를 조정하세요") + Text(I18n.ImagePicker.cropResizeGuide) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color.grayee) .padding(.top, 8) diff --git a/SodaLive/Sources/Main/EventPopupDialogView.swift b/SodaLive/Sources/Main/EventPopupDialogView.swift index ae038bf..85b11fe 100644 --- a/SodaLive/Sources/Main/EventPopupDialogView.swift +++ b/SodaLive/Sources/Main/EventPopupDialogView.swift @@ -38,7 +38,7 @@ struct EventPopupDialogView: View { } HStack(spacing: 0) { - Text("다시보지 않기") + Text(I18n.Main.EventPopup.doNotShowAgain) .appFont(size: 14.7, weight: .medium) .foregroundColor(Color(hex: "eeeeee")) .onTapGesture { @@ -48,7 +48,7 @@ struct EventPopupDialogView: View { Spacer() - Text("닫기") + Text(I18n.Main.EventPopup.close) .appFont(size: 14.7, weight: .medium) .foregroundColor(Color(hex: "eeeeee")) .onTapGesture { AppState.shared.eventPopup = nil } diff --git a/SodaLive/Sources/Main/Home/BottomTabView.swift b/SodaLive/Sources/Main/Home/BottomTabView.swift index 442121c..29d5b7f 100644 --- a/SodaLive/Sources/Main/Home/BottomTabView.swift +++ b/SodaLive/Sources/Main/Home/BottomTabView.swift @@ -16,7 +16,7 @@ struct BottomTabView: View { let tabWidth = width / 4 TabButton( - title: "홈", + title: I18n.Main.Tab.home, action: { if currentTab != .home { currentTab = .home @@ -37,7 +37,7 @@ struct BottomTabView: View { ) TabButton( - title: "라이브", + title: I18n.Main.Tab.live, action: { if currentTab != .live { currentTab = .live @@ -58,7 +58,7 @@ struct BottomTabView: View { ) TabButton( - title: "채팅", + title: I18n.Main.Tab.chat, action: { if currentTab != .chat { currentTab = .chat @@ -79,7 +79,7 @@ struct BottomTabView: View { ) TabButton( - title: "마이", + title: I18n.Main.Tab.my, action: { if currentTab != .mypage { currentTab = .mypage diff --git a/SodaLive/Sources/Main/Home/HomeView.swift b/SodaLive/Sources/Main/Home/HomeView.swift index dab6c12..9469ad6 100644 --- a/SodaLive/Sources/Main/Home/HomeView.swift +++ b/SodaLive/Sources/Main/Home/HomeView.swift @@ -216,15 +216,14 @@ struct HomeView: 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 @@ -306,7 +305,7 @@ struct HomeView: 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/Main/Home/TabButton.swift b/SodaLive/Sources/Main/Home/TabButton.swift index e989dcd..bacafbf 100644 --- a/SodaLive/Sources/Main/Home/TabButton.swift +++ b/SodaLive/Sources/Main/Home/TabButton.swift @@ -9,7 +9,7 @@ import SwiftUI struct TabButton: View { - let title: LocalizedStringResource + let title: String let action: () -> Void let image: () -> String let fontWeight: () -> SwiftUI.Font.Weight diff --git a/docs/20260331_하드코딩텍스트_I18n통일계획.md b/docs/20260331_하드코딩텍스트_I18n통일계획.md index 6283107..3ebea4f 100644 --- a/docs/20260331_하드코딩텍스트_I18n통일계획.md +++ b/docs/20260331_하드코딩텍스트_I18n통일계획.md @@ -220,17 +220,17 @@ - [ ] `SodaLive/Sources/Content/Series/SeriesListAllViewModel.swift` ### CustomView (3) -- [ ] `SodaLive/Sources/CustomView/ChatTextFieldView.swift` -- [ ] `SodaLive/Sources/CustomView/ExpandableTextView.swift` -- [ ] `SodaLive/Sources/CustomView/IconAndTitleToggleButton.swift` +- [x] `SodaLive/Sources/CustomView/ChatTextFieldView.swift` +- [x] `SodaLive/Sources/CustomView/ExpandableTextView.swift` +- [x] `SodaLive/Sources/CustomView/IconAndTitleToggleButton.swift` ### Dialog (6) -- [ ] `SodaLive/Sources/Dialog/ApplyAuditionCompleteDialog.swift` -- [ ] `SodaLive/Sources/Dialog/CommunityPostPurchaseDialog.swift` -- [ ] `SodaLive/Sources/Dialog/CreatorFollowNotifyDialog.swift` -- [ ] `SodaLive/Sources/Dialog/LivePaymentDialog.swift` -- [ ] `SodaLive/Sources/Dialog/LiveRoomPasswordDialog.swift` -- [ ] `SodaLive/Sources/Dialog/MemberProfileDialog.swift` +- [x] `SodaLive/Sources/Dialog/ApplyAuditionCompleteDialog.swift` +- [x] `SodaLive/Sources/Dialog/CommunityPostPurchaseDialog.swift` +- [x] `SodaLive/Sources/Dialog/CreatorFollowNotifyDialog.swift` +- [x] `SodaLive/Sources/Dialog/LivePaymentDialog.swift` +- [x] `SodaLive/Sources/Dialog/LiveRoomPasswordDialog.swift` +- [x] `SodaLive/Sources/Dialog/MemberProfileDialog.swift` ### Explorer (40) - [ ] `SodaLive/Sources/Explorer/ExplorerSectionView.swift` @@ -275,7 +275,7 @@ - [ ] `SodaLive/Sources/Explorer/Profile/UserProfileViewModel.swift` ### Follow (1) -- [ ] `SodaLive/Sources/Follow/FollowCreatorView.swift` +- [x] `SodaLive/Sources/Follow/FollowCreatorView.swift` ### Home (9) - [ ] `SodaLive/Sources/Home/HomeAuditionView.swift` @@ -289,10 +289,10 @@ - [ ] `SodaLive/Sources/Home/RecommendChannel/RecommendChannelItemView.swift` ### IAP (1) -- [ ] `SodaLive/Sources/IAP/StoreManager.swift` +- [x] `SodaLive/Sources/IAP/StoreManager.swift` ### ImagePicker (1) -- [ ] `SodaLive/Sources/ImagePicker/ImagePicker.swift` +- [x] `SodaLive/Sources/ImagePicker/ImagePicker.swift` ### Live (56) - [ ] `SodaLive/Sources/Live/Cancel/LiveCancelDialog.swift` @@ -353,9 +353,9 @@ - [ ] `SodaLive/Sources/Live/SectionLatestFinishedLiveView.swift` ### Main (3) -- [ ] `SodaLive/Sources/Main/EventPopupDialogView.swift` -- [ ] `SodaLive/Sources/Main/Home/BottomTabView.swift` -- [ ] `SodaLive/Sources/Main/Home/HomeView.swift` +- [x] `SodaLive/Sources/Main/EventPopupDialogView.swift` +- [x] `SodaLive/Sources/Main/Home/BottomTabView.swift` +- [x] `SodaLive/Sources/Main/Home/HomeView.swift` ### Message (13) - [ ] `SodaLive/Sources/Message/MessageFilterTabView.swift` @@ -587,3 +587,28 @@ - Chat 모듈 하드코딩 한글 재검증 결과, 남은 문자열은 Preview 샘플/SDK 입력값/비노출 분기 로직만 존재. - 빌드 검증: `SodaLive`, `SodaLive-dev` Debug 빌드 모두 성공(`** BUILD SUCCEEDED **`). - 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 테스트 액션 미구성 확인(코드 실패 아님, 스킴 제약). + +### 8차 구현 (ImagePicker/CustomView/IAP/Follow/Main/Dialog 15개 파일 i18n 전환, 2026-03-31) +- 무엇/왜/어떻게: + - 무엇: 변경 대상 목록 중 `ImagePicker`, `CustomView`, `IAP`, `Follow`, `Main`, `Dialog` 모듈의 15개 파일을 처리해 사용자 노출 하드코딩 문구를 `I18n.*` 참조로 정리. + - 왜: 주요 공통 UI(버튼/다이얼로그/토스트/탭 라벨/인증 안내)에 하드코딩 문자열이 남아 있어 모듈 간 다국어 일관성이 깨지는 상태였기 때문. + - 어떻게: explore/librarian 병렬 탐색 + `grep`/`ast_grep_search` 직접 검증으로 치환 대상을 확정하고, `I18n.swift`에 모듈별 네임스페이스(`ImagePicker`, `CustomView`, `IAP`, `Follow`, `Main`, `Dialog`)를 추가한 뒤 호출부를 교체. +- 실행 명령/도구: + - `task(subagent_type="explore", ...)` x2 (`bg_da05186f`, `bg_81c85d58`) + - `task(subagent_type="librarian", ...)` x2 (`bg_4b24d2ad`, `bg_d8b1253f`) + - `grep("\"[^\"]*[가-힣][^\"]*\"", include=*.swift, path=SodaLive/Sources/{ImagePicker,CustomView,IAP,Follow,Main,Dialog})` (모듈별 개별 실행) + - `grep("String\\(localized:|LocalizedStringKey\\(|NSLocalizedString\\(", include=*.swift, path=...)` (모듈별 개별 실행) + - `ast_grep_search(pattern="Text(\"$TEXT\")", lang=swift, paths=[...])` + - `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` +- 결과: + - 호출부 치환 완료 파일: `ImagePicker.swift`, `ExpandableTextView.swift`, `StoreManager.swift`, `FollowCreatorView.swift`, `EventPopupDialogView.swift`, `BottomTabView.swift`, `HomeView.swift`, `ApplyAuditionCompleteDialog.swift`, `CommunityPostPurchaseDialog.swift`, `CreatorFollowNotifyDialog.swift`, `LivePaymentDialog.swift`, `LiveRoomPasswordDialog.swift`, `MemberProfileDialog.swift`. + - 점검만 수행(실치환 없음) 파일: `ChatTextFieldView.swift`, `IconAndTitleToggleButton.swift` (Preview 샘플 문자열만 존재, 런타임 노출 문자열 없음). + - `I18n.swift` 추가 키셋: `I18n.ImagePicker`, `I18n.CustomView`, `I18n.IAP`, `I18n.Follow`, `I18n.Main`(EventPopup/Tab/Auth), `I18n.Dialog`(ApplyAuditionComplete/CommunityPostPurchase/LivePayment/LiveRoomPassword/MemberProfile). + - `Main/Home/HomeView.swift`의 Bootpay 입력값(`payload.pg`, `payload.method`, `payload.orderName`)은 SDK 입력값 유지 원칙에 따라 비노출 고정값으로 유지. + - 모듈 재검증 결과, 남은 한글 문자열은 Preview 샘플/`DEBUG_LOG`/SDK 입력값(비노출)만 존재. + - 빌드 검증: `SodaLive`, `SodaLive-dev` Debug 빌드 모두 성공(`** BUILD SUCCEEDED **`). + - 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 테스트 액션 미구성 확인(코드 실패 아님, 스킴 제약).