diff --git a/SodaLive/Sources/Audition/Applicant/ApplyMethodView.swift b/SodaLive/Sources/Audition/Applicant/ApplyMethodView.swift index 7735e9e..6746ab5 100644 --- a/SodaLive/Sources/Audition/Applicant/ApplyMethodView.swift +++ b/SodaLive/Sources/Audition/Applicant/ApplyMethodView.swift @@ -30,7 +30,7 @@ struct ApplyMethodView: View { } } - Text("오디션 지원방식") + Text(I18n.Audition.ApplyMethod.title) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color.graybb) .padding(.top, 33.3) @@ -39,7 +39,7 @@ struct ApplyMethodView: View { HStack(spacing: 3) { Image("ic_upload") - Text("파일 업로드") + Text(I18n.Audition.ApplyMethod.fileUpload) .appFont(size: 14.7, weight: .medium) .foregroundColor(Color.button) } @@ -60,7 +60,7 @@ struct ApplyMethodView: View { HStack(spacing: 3) { Image("ic_mic_color_button") - Text("바로 녹음") + Text(I18n.Audition.ApplyMethod.recordNow) .appFont(size: 14.7, weight: .medium) .foregroundColor(Color.button) } @@ -81,7 +81,7 @@ struct ApplyMethodView: View { .padding(.top, 21.3) HStack(spacing: 0) { - Text("※ 파일은 mp3, aac만 업로드 가능") + Text(I18n.Audition.ApplyMethod.fileFormatNotice) .appFont(size: 12, weight: .medium) .foregroundColor(Color.gray77) .padding(.top, 13.3) diff --git a/SodaLive/Sources/Audition/Applicant/AuditionApplicantRecordingView.swift b/SodaLive/Sources/Audition/Applicant/AuditionApplicantRecordingView.swift index 1fd714c..34c00b6 100644 --- a/SodaLive/Sources/Audition/Applicant/AuditionApplicantRecordingView.swift +++ b/SodaLive/Sources/Audition/Applicant/AuditionApplicantRecordingView.swift @@ -30,7 +30,7 @@ struct AuditionApplicantRecordingView: View { VStack { VStack(spacing: 0) { HStack(spacing: 0) { - Text("오디션 녹음") + Text(I18n.Audition.Recording.title) .appFont(size: 18.3, weight: .bold) .foregroundColor(.white) @@ -76,7 +76,7 @@ struct AuditionApplicantRecordingView: View { HStack(spacing: 0) { Spacer() - Text("삭제") + Text(I18n.Common.delete) .appFont(size: 15.3, weight: .medium) .foregroundColor(Color.graybb.opacity(0)) @@ -99,7 +99,7 @@ struct AuditionApplicantRecordingView: View { Spacer() - Text("삭제") + Text(I18n.Common.delete) .appFont(size: 15.3, weight: .medium) .foregroundColor(Color.graybb) .onTapGesture { @@ -113,7 +113,7 @@ struct AuditionApplicantRecordingView: View { .padding(.vertical, 52.3) HStack(spacing: 13.3) { - Text("다시 녹음") + Text(I18n.Audition.Recording.recordAgain) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color.button) .frame(width: (proxy.size.width - 40) / 3, height: 50) @@ -129,7 +129,7 @@ struct AuditionApplicantRecordingView: View { soundManager.recordMode = .RECORD } - Text("녹음완료") + Text(I18n.Audition.Recording.recordComplete) .appFont(size: 18.3, weight: .bold) .foregroundColor(.white) .frame(width: (proxy.size.width - 40) * 2 / 3, height: 50) @@ -140,7 +140,7 @@ struct AuditionApplicantRecordingView: View { let soundData = try Data(contentsOf: soundManager.getAudioFileURL()) onClickCompleteRecording(tempFileName, soundData) } catch { - errorMessage = "녹음파일을 생성하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + errorMessage = I18n.Audition.Recording.createFileFailed isShowPopup = true } } diff --git a/SodaLive/Sources/Audition/Applicant/AuditionApplyView.swift b/SodaLive/Sources/Audition/Applicant/AuditionApplyView.swift index a733b2a..9b3fe81 100644 --- a/SodaLive/Sources/Audition/Applicant/AuditionApplyView.swift +++ b/SodaLive/Sources/Audition/Applicant/AuditionApplyView.swift @@ -33,7 +33,7 @@ struct AuditionApplyView: View { if isShow { VStack(alignment: .leading, spacing: 0) { HStack(spacing: 0) { - Text("오디션 지원") + Text(I18n.Audition.Apply.title) .appFont(size: 18.3, weight: .medium) .foregroundColor(.white) @@ -45,7 +45,7 @@ struct AuditionApplyView: View { } } - Text("녹음파일") + Text(I18n.Audition.Apply.recordingFile) .appFont(size: 16.7, weight: .bold) .foregroundColor(.grayee) .padding(.top, 20) @@ -53,7 +53,7 @@ struct AuditionApplyView: View { HStack(spacing: 4) { Image("ic_note_square") - Text(filename) + Text(displayFileName) .appFont(size: 13.3, weight: .medium) .foregroundColor(.grayd2) @@ -66,12 +66,12 @@ struct AuditionApplyView: View { .cornerRadius(5.3) .padding(.top, 10) - Text("연락처") + Text(I18n.Audition.Apply.contact) .appFont(size: 16.7, weight: .bold) .foregroundColor(.grayee) .padding(.top, 15) - TextField("합격시 받을 연락처를 남겨주세요", text: $phoneNumber) + TextField(I18n.Audition.Apply.contactPlaceholder, text: $phoneNumber) .autocapitalization(.none) .disableAutocorrection(true) .keyboardType(.decimalPad) @@ -89,7 +89,7 @@ struct AuditionApplyView: View { .resizable() .frame(width: 20, height: 20) - Text("보이스온 오디오 드라마 오디션 합격시 개인 연락을 위한 개인 정보(연락처) 수집 및 활용에 동의합니다.\n오디션 지원자는 개인정보 수집 및 활용 동의에 거부할 권리가 있으며 비동의시 오디션 지원은 취소 됩니다.") + Text(I18n.Audition.Apply.privacyAgreement) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color.grayee) .lineSpacing(3) @@ -100,7 +100,7 @@ struct AuditionApplyView: View { isAgree.toggle() } - Text("오디션 지원하기") + Text(I18n.Audition.Apply.submit) .appFont(size: 13.3, weight: .bold) .foregroundColor(Color.grayee) .padding(.vertical, 13.3) @@ -110,7 +110,7 @@ struct AuditionApplyView: View { .padding(.top, 35) .onTapGesture { if !isAgree { - errorMessage = "연락처 수집 및 활용에 동의하셔야 오디션 지원이 가능합니다." + errorMessage = I18n.Audition.Apply.requireAgreement isShowPopup = true return } @@ -137,6 +137,14 @@ struct AuditionApplyView: View { } } } + + private var displayFileName: String { + if filename.hasPrefix("voiceon_now_voice_") { + return I18n.Audition.Apply.recordedVoiceFileName + } + + return filename + } } #Preview { diff --git a/SodaLive/Sources/Audition/AuditionView.swift b/SodaLive/Sources/Audition/AuditionView.swift index 583a944..1978691 100644 --- a/SodaLive/Sources/Audition/AuditionView.swift +++ b/SodaLive/Sources/Audition/AuditionView.swift @@ -24,7 +24,7 @@ struct AuditionView: View { .resizable() .frame(width: 20, height: 20) - Text("오디션") + Text(I18n.Audition.title) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color(hex: "eeeeee")) } @@ -43,13 +43,13 @@ struct AuditionView: View { .background(Color.black) HStack(spacing: 0) { - Text("보이스온 오디션 이용방법") + Text(I18n.Audition.List.usageGuide) .appFont(size: 13.3, weight: .medium) .foregroundColor(.white) Spacer() - Text("자세히>") + Text(I18n.Audition.List.detail) .appFont(size: 13.3, weight: .medium) .foregroundColor(.white) } @@ -73,17 +73,17 @@ struct AuditionView: View { if $0 == 0 && !item.isOff { VStack(alignment: .leading, spacing: 25) { HStack(spacing: 0) { - Text("오디션") + Text(I18n.Audition.title) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color.grayee) - Text(" ON") + Text(I18n.Audition.List.onStatus) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color.mainRed) Spacer() - Text("총 \(viewModel.inProgressCount)개") + Text(I18n.Audition.List.totalCount(viewModel.inProgressCount)) .appFont(size: 11.3, weight: .medium) .foregroundColor(Color.graybb) } @@ -111,17 +111,17 @@ struct AuditionView: View { .padding(.top, 5) HStack(spacing: 0) { - Text("오디션") + Text(I18n.Audition.title) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color.grayee) - Text(" OFF") + Text(I18n.Audition.List.offStatus) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color.graybb) Spacer() - Text("총 \(viewModel.completedCount)개") + Text(I18n.Audition.List.totalCount(viewModel.completedCount)) .appFont(size: 11.3, weight: .medium) .foregroundColor(Color.graybb) } diff --git a/SodaLive/Sources/Audition/AuditionViewModel.swift b/SodaLive/Sources/Audition/AuditionViewModel.swift index a259f20..0542d17 100644 --- a/SodaLive/Sources/Audition/AuditionViewModel.swift +++ b/SodaLive/Sources/Audition/AuditionViewModel.swift @@ -66,13 +66,13 @@ final class AuditionViewModel: 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/Audition/Detail/AuditionDetailView.swift b/SodaLive/Sources/Audition/Detail/AuditionDetailView.swift index ccc9c51..48e5c6f 100644 --- a/SodaLive/Sources/Audition/Detail/AuditionDetailView.swift +++ b/SodaLive/Sources/Audition/Detail/AuditionDetailView.swift @@ -30,7 +30,7 @@ struct AuditionDetailView: View { .frame(maxWidth: .infinity) .cornerRadius(6.7) - Text("오디션 정보") + Text(I18n.Audition.Detail.informationTitle) .appFont(size: 14.7, weight: .bold) .foregroundColor(Color.grayee) .padding(.top, 15) @@ -38,7 +38,7 @@ struct AuditionDetailView: View { ExpandableTextView(text: response.information) .padding(.top, 13.3) - Text("오디션 캐릭터") + Text(I18n.Audition.Detail.characterTitle) .appFont(size: 14.7, weight: .bold) .foregroundColor(Color.grayee) .padding(.top, 15) diff --git a/SodaLive/Sources/Audition/Detail/AuditionDetailViewModel.swift b/SodaLive/Sources/Audition/Detail/AuditionDetailViewModel.swift index 984ade5..ffb554c 100644 --- a/SodaLive/Sources/Audition/Detail/AuditionDetailViewModel.swift +++ b/SodaLive/Sources/Audition/Detail/AuditionDetailViewModel.swift @@ -18,7 +18,7 @@ final class AuditionDetailViewModel: ObservableObject { @Published var isLoading = false @Published var response: GetAuditionDetailResponse? = nil - @Published var title: String = "보이스온" + @Published var title: String = I18n.Audition.defaultTitle func getAuditionDetail(auditionId: Int, onFailure: @escaping () -> Void) { isLoading = true @@ -45,7 +45,7 @@ final class AuditionDetailViewModel: ObservableObject { if let message = decoded.message { self.errorMessage = message } else { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError } self.isShowPopup = true @@ -54,7 +54,7 @@ final class AuditionDetailViewModel: ObservableObject { } } } catch { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError self.isShowPopup = true DispatchQueue.main.asyncAfter(deadline: .now() + 2) { onFailure() diff --git a/SodaLive/Sources/Audition/Detail/AuditionSoundManager.swift b/SodaLive/Sources/Audition/Detail/AuditionSoundManager.swift index ab21697..ee86df9 100644 --- a/SodaLive/Sources/Audition/Detail/AuditionSoundManager.swift +++ b/SodaLive/Sources/Audition/Detail/AuditionSoundManager.swift @@ -64,7 +64,7 @@ class AuditionSoundManager: NSObject, ObservableObject { private func setupPlayer(with url: String) { guard let url = URL(string: url) else { - self.errorMessage = "오류가 발생했습니다. 다시 시도해 주세요." + self.errorMessage = I18n.Audition.Sound.playbackFailed self.isShowPopup = true return } @@ -92,7 +92,7 @@ class AuditionSoundManager: NSObject, ObservableObject { } } catch { DispatchQueue.main.async { - self.errorMessage = "오류가 발생했습니다. 다시 시도해 주세요." + self.errorMessage = I18n.Audition.Sound.playbackFailed self.isShowPopup = true self.isLoading = false } diff --git a/SodaLive/Sources/Audition/Role/AuditionDetailRoleItemView.swift b/SodaLive/Sources/Audition/Role/AuditionDetailRoleItemView.swift index 582d34d..9c2ee2a 100644 --- a/SodaLive/Sources/Audition/Role/AuditionDetailRoleItemView.swift +++ b/SodaLive/Sources/Audition/Role/AuditionDetailRoleItemView.swift @@ -28,7 +28,7 @@ struct AuditionDetailRoleItemView: View { .opacity(item.isComplete ? 0.7 : 0.0) ) - Text(item.isComplete ? "모집완료" : "모집중") + Text(item.isComplete ? I18n.Audition.Detail.recruitmentClosed : I18n.Audition.Detail.recruiting) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color.white) .padding(.horizontal, 9) diff --git a/SodaLive/Sources/Audition/Role/AuditionRoleDetailView.swift b/SodaLive/Sources/Audition/Role/AuditionRoleDetailView.swift index fd0bf50..9aa0a62 100644 --- a/SodaLive/Sources/Audition/Role/AuditionRoleDetailView.swift +++ b/SodaLive/Sources/Audition/Role/AuditionRoleDetailView.swift @@ -45,7 +45,7 @@ struct AuditionRoleDetailView: View { HStack(spacing: 14) { if let url = URL(string: roleDetail.originalWorkUrl), UIApplication.shared.canOpenURL(url) { - Text("원작 보러가기") + Text(I18n.Audition.Detail.viewOriginalWork) .appFont(size: 16, weight: .bold) .foregroundColor(Color.button) .padding(.vertical, 12) @@ -59,7 +59,7 @@ struct AuditionRoleDetailView: View { } if let url = URL(string: roleDetail.auditionScriptUrl), UIApplication.shared.canOpenURL(url) { - Text("오디션 대본 확인") + Text(I18n.Audition.Detail.checkScript) .appFont(size: 16, weight: .bold) .foregroundColor(Color.button) .padding(.vertical, 12) @@ -74,7 +74,7 @@ struct AuditionRoleDetailView: View { } VStack(alignment: .leading, spacing: 13.3) { - Text("오디션 캐릭터 정보") + Text(I18n.Audition.Detail.characterInfo) .appFont(size: 14.7, weight: .bold) .foregroundColor(Color.grayee) @@ -83,13 +83,13 @@ struct AuditionRoleDetailView: View { } if viewModel.applicantList.isEmpty { - Text("지원자가 없습니다.") + Text(I18n.Audition.Detail.noApplicants) .appFont(size: 13, weight: .medium) .foregroundColor(Color.grayee) .padding(.top, 15) } else { HStack(spacing: 0) { - Text("참여자") + Text(I18n.Audition.Detail.participants) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color.graybb) @@ -98,13 +98,13 @@ struct AuditionRoleDetailView: View { .foregroundColor(Color.button) .padding(.leading, 2.3) - Text("명") + Text(I18n.Audition.Detail.personUnit) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color.graybb) Spacer() - Text("최신순") + Text(I18n.Audition.Detail.sortNewest) .appFont(size: 13.3, weight: .medium) .foregroundColor( viewModel.sortType == .NEWEST ? Color.button : Color.graybb @@ -113,7 +113,7 @@ struct AuditionRoleDetailView: View { viewModel.setSortType(sortType: .NEWEST) } - Text("좋아요순") + Text(I18n.Audition.Detail.sortLikes) .appFont(size: 13.3, weight: .medium) .foregroundColor( viewModel.sortType == .LIKES ? Color.button : Color.graybb @@ -161,7 +161,7 @@ struct AuditionRoleDetailView: View { } if let roleDetail = viewModel.auditionRoleDetail { - Text(roleDetail.isAlreadyApplicant ? "오디션 재지원" : "오디션 지원") + Text(roleDetail.isAlreadyApplicant ? I18n.Audition.Apply.reapply : I18n.Audition.Apply.apply) .appFont(size: 15.3, weight: .bold) .foregroundColor(Color.white) .padding(14) @@ -241,9 +241,9 @@ struct AuditionRoleDetailView: View { if isShowNoticeReapply { SodaDialog( - title: "재지원 안내", - desc: "재지원 시 이전 지원 내역은 삭제되며 받은 투표수는 무효 처리됩니다.", - confirmButtonTitle: "확인" + title: I18n.Audition.Apply.reapplyNoticeTitle, + desc: I18n.Audition.Apply.reapplyNoticeDesc, + confirmButtonTitle: I18n.Common.confirm ) { isShowNoticeReapply = false isShowApplyMethodView = true @@ -252,9 +252,9 @@ struct AuditionRoleDetailView: View { if isShowNoticeAuthView { SodaDialog( - title: "- 본인인증 -", - desc: "마이페이지에서 '본인인증'을 하고 다시 오디션에 지원해 주세요.", - confirmButtonTitle: "확인" + title: I18n.Audition.Apply.authRequiredTitle, + desc: I18n.Audition.Apply.authRequiredDesc, + confirmButtonTitle: I18n.Common.confirm ) { isShowNoticeAuthView = false } @@ -264,7 +264,7 @@ struct AuditionRoleDetailView: View { SodaDialog( title: viewModel.dialogTitle, desc: viewModel.dialogDesc, - confirmButtonTitle: "확인" + confirmButtonTitle: I18n.Common.confirm ) { viewModel.isShowVoteCompleteView = false viewModel.isShowNotifyVote = false @@ -300,17 +300,17 @@ struct AuditionRoleDetailView: View { viewModel.fileName = fileUrl.lastPathComponent isShowApplyView = true } else { - viewModel.errorMessage = "콘텐츠 파일을 불러오지 못했습니다.\n다시 선택해 주세요" + viewModel.errorMessage = I18n.Audition.Apply.fileLoadFailed viewModel.isShowPopup = true } } else { - viewModel.errorMessage = "콘텐츠 파일을 불러오지 못했습니다.\n다시 선택해 주세요" + viewModel.errorMessage = I18n.Audition.Apply.fileLoadFailed viewModel.isShowPopup = true } case .failure(let error): DEBUG_LOG("error: \(error.localizedDescription)") - viewModel.errorMessage = "콘텐츠 파일을 불러오지 못했습니다.\n다시 선택해 주세요" + viewModel.errorMessage = I18n.Audition.Apply.fileLoadFailed viewModel.isShowPopup = true } } diff --git a/SodaLive/Sources/Audition/Role/AuditionRoleDetailViewModel.swift b/SodaLive/Sources/Audition/Role/AuditionRoleDetailViewModel.swift index 991b2bd..ba490aa 100644 --- a/SodaLive/Sources/Audition/Role/AuditionRoleDetailViewModel.swift +++ b/SodaLive/Sources/Audition/Role/AuditionRoleDetailViewModel.swift @@ -21,7 +21,7 @@ final class AuditionRoleDetailViewModel: ObservableObject { @Published var totalCount = 0 @Published var applicantList = [GetAuditionRoleApplicantItem]() - @Published var name = "보이스온" + @Published var name = I18n.Audition.defaultTitle @Published var auditionRoleDetail: GetAuditionRoleDetailResponse? = nil @Published private(set) var sortType = AuditionApplicantSortType.NEWEST { @@ -93,13 +93,13 @@ final class AuditionRoleDetailViewModel: ObservableObject { if let message = roleDetailDecoded.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 } @@ -118,7 +118,7 @@ final class AuditionRoleDetailViewModel: ObservableObject { if let message = applicantListDecoded.message { self.errorMessage = message } else { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError } self.isShowPopup = true @@ -127,7 +127,7 @@ final class AuditionRoleDetailViewModel: ObservableObject { } } } catch { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError self.isShowPopup = true DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.onFailure() @@ -172,13 +172,13 @@ final class AuditionRoleDetailViewModel: 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 } } @@ -188,13 +188,13 @@ final class AuditionRoleDetailViewModel: ObservableObject { func applyAudition(onSuccess: @escaping () -> Void) { if phoneNumber.count != 11 { - errorMessage = "잘못된 연락처 입니다.\n다시 입력해 주세요." + errorMessage = I18n.Audition.Apply.invalidContact isShowPopup = true return } guard let soundData = soundData else { - errorMessage = "잘못된 녹음 파일 입니다.\n다시 선택해 주세요." + errorMessage = I18n.Audition.Apply.invalidRecordingFile isShowPopup = true return } @@ -248,19 +248,19 @@ final class AuditionRoleDetailViewModel: ObservableObject { if let message = decoded.message { self.errorMessage = message } else { - self.errorMessage = "오디션 지원을 완료하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Audition.Apply.applyFailed } self.isShowPopup = true } } catch { - self.errorMessage = "오디션 지원을 완료하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Audition.Apply.applyFailed self.isShowPopup = true } } .store(in: &subscription) } else { - self.errorMessage = "오디션 지원을 완료하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Audition.Apply.applyFailed self.isShowPopup = true self.isLoading = false } @@ -287,8 +287,8 @@ final class AuditionRoleDetailViewModel: ObservableObject { if decoded.success { if self.isShowNotifyVote { - self.dialogTitle = "[오디션 응원]" - self.dialogDesc = "오디션을 응원하셨습니다\n(무료응원 : 1계정당 1일 1회)\n1캔으로 추가 응원을 해보세요." + self.dialogTitle = I18n.Audition.Vote.cheerTitle + self.dialogDesc = I18n.Audition.Vote.cheerDescription self.isShowVoteCompleteView = true } @@ -302,20 +302,20 @@ final class AuditionRoleDetailViewModel: ObservableObject { } else { if let message = decoded.message { if message.contains("오늘 응원은 여기까지") { - self.dialogTitle = "[오늘 응원 제한]" - self.dialogDesc = "오늘 응원은 여기까지!\n하루 최대 10회까지 이용이 가능합니다.\n내일 다시 이용해주세요." + self.dialogTitle = I18n.Audition.Vote.limitTitle + self.dialogDesc = I18n.Audition.Vote.limitDescription self.isShowVoteCompleteView = true } else { self.errorMessage = message self.isShowPopup = true } } else { - self.errorMessage = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + self.errorMessage = I18n.Audition.Vote.unknownError self.isShowPopup = true } } } catch { - self.errorMessage = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + self.errorMessage = I18n.Audition.Vote.unknownError self.isShowPopup = true } } diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index 2774cdd..ef59f96 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -281,6 +281,294 @@ enum I18n { static var points: String { pick(ko: "포인트", en: "Points", ja: "ポイント") } } + enum Audition { + static var title: String { + pick(ko: "오디션", en: "Audition", ja: "オーディション") + } + + static var defaultTitle: String { + pick(ko: "보이스온", en: "VoiceOn", ja: "ボイスオン") + } + + enum List { + static var usageGuide: String { + pick( + ko: "보이스온 오디션 이용방법", + en: "How to use VoiceOn Audition", + ja: "ボイスオンオーディション利用方法" + ) + } + + static var detail: String { + pick(ko: "자세히>", en: "Details >", ja: "詳しく >") + } + + static var onStatus: String { + pick(ko: " ON", en: " ON", ja: " ON") + } + + static var offStatus: String { + pick(ko: " OFF", en: " OFF", ja: " OFF") + } + + static func totalCount(_ count: Int) -> String { + pick( + ko: "총 \(count)개", + en: "Total \(count)", + ja: "全\(count)件" + ) + } + } + + enum ApplyMethod { + static var title: String { + pick(ko: "오디션 지원방식", en: "Audition application method", ja: "オーディション応募方法") + } + + static var fileUpload: String { + pick(ko: "파일 업로드", en: "Upload file", ja: "ファイルアップロード") + } + + static var recordNow: String { + pick(ko: "바로 녹음", en: "Record now", ja: "今すぐ録音") + } + + static var fileFormatNotice: String { + pick( + ko: "※ 파일은 mp3, aac만 업로드 가능", + en: "※ Only mp3 and aac files can be uploaded", + ja: "※ ファイルはmp3、aacのみアップロード可能" + ) + } + } + + enum Apply { + static var title: String { + pick(ko: "오디션 지원", en: "Apply for audition", ja: "オーディション応募") + } + + static var recordingFile: String { + pick(ko: "녹음파일", en: "Recorded file", ja: "録音ファイル") + } + + static var contact: String { + pick(ko: "연락처", en: "Contact", ja: "連絡先") + } + + static var contactPlaceholder: String { + pick( + ko: "합격시 받을 연락처를 남겨주세요", + en: "Enter contact information to receive if selected", + ja: "合格時に受け取る連絡先を入力してください" + ) + } + + static var recordedVoiceFileName: String { + pick(ko: "내 녹음파일", en: "My recording", ja: "自分の録音ファイル") + } + + static var privacyAgreement: String { + pick( + ko: "보이스온 오디오 드라마 오디션 합격시 개인 연락을 위한 개인 정보(연락처) 수집 및 활용에 동의합니다.\n오디션 지원자는 개인정보 수집 및 활용 동의에 거부할 권리가 있으며 비동의시 오디션 지원은 취소 됩니다.", + en: "I agree to the collection and use of personal information (contact) for personal contact if selected for the VoiceOn audio drama audition.\nApplicants have the right to refuse consent to the collection and use of personal information, and if they do not consent, the audition application will be canceled.", + ja: "ボイスオンオーディオドラマオーディション合格時の個別連絡のため、個人情報(連絡先)の収集および利用に同意します。\n応募者は個人情報の収集および利用への同意を拒否する権利があり、同意しない場合はオーディション応募が取り消されます。" + ) + } + + static var submit: String { + pick(ko: "오디션 지원하기", en: "Submit audition application", ja: "オーディションに応募する") + } + + static var apply: String { + pick(ko: "오디션 지원", en: "Apply for audition", ja: "オーディション応募") + } + + static var reapply: String { + pick(ko: "오디션 재지원", en: "Reapply for audition", ja: "オーディション再応募") + } + + static var requireAgreement: String { + pick( + ko: "연락처 수집 및 활용에 동의하셔야 오디션 지원이 가능합니다.", + en: "You must agree to the collection and use of contact information to apply for the audition.", + ja: "連絡先の収集および利用に同意すると、オーディションに応募できます。" + ) + } + + static var reapplyNoticeTitle: String { + pick(ko: "재지원 안내", en: "Reapply notice", ja: "再応募の案内") + } + + static var reapplyNoticeDesc: String { + pick( + ko: "재지원 시 이전 지원 내역은 삭제되며 받은 투표수는 무효 처리됩니다.", + en: "When you reapply, your previous application history is deleted and received votes are invalidated.", + ja: "再応募すると、以前の応募履歴は削除され、受け取った投票数は無効になります。" + ) + } + + static var authRequiredTitle: String { + pick(ko: "- 본인인증 -", en: "- Identity verification -", ja: "- 本人認証 -") + } + + static var authRequiredDesc: String { + pick( + ko: "마이페이지에서 '본인인증'을 하고 다시 오디션에 지원해 주세요.", + en: "Please complete 'Identity Verification' in My Page and apply for the audition again.", + ja: "マイページで「本人認証」を行ってから、もう一度オーディションに応募してください。" + ) + } + + static var invalidContact: String { + pick( + ko: "잘못된 연락처 입니다.\n다시 입력해 주세요.", + en: "Invalid contact information.\nPlease enter it again.", + ja: "連絡先が正しくありません。\nもう一度入力してください。" + ) + } + + static var invalidRecordingFile: String { + pick( + ko: "잘못된 녹음 파일 입니다.\n다시 선택해 주세요.", + en: "Invalid recording file.\nPlease select it again.", + ja: "録音ファイルが正しくありません。\nもう一度選択してください。" + ) + } + + static var fileLoadFailed: String { + pick( + ko: "오디오 파일을 불러오지 못했습니다.\n다시 선택해 주세요", + en: "Could not load the audio file.\nPlease select it again.", + ja: "オーディオファイルを読み込めませんでした。\nもう一度選択してください。" + ) + } + + static var applyFailed: String { + pick( + ko: "오디션 지원을 완료하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다.", + en: "Could not complete the audition application.\nPlease try again.\nIf the problem persists, please contact customer support.", + ja: "オーディション応募を完了できませんでした。\nもう一度お試しください。\n問題が続く場合はカスタマーサポートにお問い合わせください。" + ) + } + } + + enum Recording { + static var title: String { + pick(ko: "오디션 녹음", en: "Audition recording", ja: "オーディション録音") + } + + static var recordAgain: String { + pick(ko: "다시 녹음", en: "Record again", ja: "再録音") + } + + static var recordComplete: String { + pick(ko: "녹음완료", en: "Recording complete", ja: "録音完了") + } + + static var createFileFailed: String { + pick( + ko: "녹음파일을 생성하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다.", + en: "Could not create the recording file.\nPlease try again.\nIf the problem persists, please contact customer support.", + ja: "録音ファイルを作成できませんでした。\nもう一度お試しください。\n問題が続く場合はカスタマーサポートにお問い合わせください。" + ) + } + } + + enum Detail { + static var informationTitle: String { + pick(ko: "오디션 정보", en: "Audition information", ja: "オーディション情報") + } + + static var characterTitle: String { + pick(ko: "오디션 캐릭터", en: "Audition character", ja: "オーディションキャラクター") + } + + static var viewOriginalWork: String { + pick(ko: "원작 보러가기", en: "View original work", ja: "原作を見る") + } + + static var checkScript: String { + pick(ko: "오디션 대본 확인", en: "Check audition script", ja: "オーディション台本を確認") + } + + static var characterInfo: String { + pick(ko: "오디션 캐릭터 정보", en: "Audition character info", ja: "オーディションキャラクター情報") + } + + static var noApplicants: String { + pick(ko: "지원자가 없습니다.", en: "No applicants.", ja: "応募者がいません。") + } + + static var participants: String { + pick(ko: "참여자", en: "Participants", ja: "参加者") + } + + static var personUnit: String { + pick(ko: "명", en: "people", ja: "名") + } + + static var sortNewest: String { + pick(ko: "최신순", en: "Newest", ja: "新着順") + } + + static var sortLikes: String { + pick(ko: "좋아요순", en: "Most liked", ja: "いいね順") + } + + static var recruiting: String { + pick(ko: "모집중", en: "Open", ja: "募集中") + } + + static var recruitmentClosed: String { + pick(ko: "모집완료", en: "Closed", ja: "募集終了") + } + } + + enum Vote { + static var cheerTitle: String { + pick(ko: "[오디션 응원]", en: "[Audition cheer]", ja: "[オーディション応援]") + } + + static var cheerDescription: String { + pick( + ko: "오디션을 응원하셨습니다\n(무료응원 : 1계정당 1일 1회)\n1캔으로 추가 응원을 해보세요.", + en: "You cheered for this audition.\n(Free cheer: once per account per day)\nTry sending an additional cheer with 1 can.", + ja: "オーディションを応援しました。\n(無料応援:1アカウント1日1回)\n1canで追加応援してみましょう。" + ) + } + + static var limitTitle: String { + pick(ko: "[오늘 응원 제한]", en: "[Today's cheer limit]", ja: "[本日の応援制限]") + } + + static var limitDescription: String { + pick( + ko: "오늘 응원은 여기까지!\n하루 최대 10회까지 이용이 가능합니다.\n내일 다시 이용해주세요.", + en: "That's all for today's cheers!\nYou can use it up to 10 times a day.\nPlease try again tomorrow.", + ja: "本日の応援はここまでです!\n1日最大10回まで利用できます。\n明日またご利用ください。" + ) + } + + static var unknownError: String { + pick( + ko: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.", + en: "An unknown error occurred. Please try again.", + ja: "不明なエラーが発生しました。もう一度お試しください。" + ) + } + } + + enum Sound { + static var playbackFailed: String { + pick( + ko: "오류가 발생했습니다. 다시 시도해 주세요.", + en: "An error occurred. Please try again.", + ja: "エラーが発生しました。もう一度お試しください。" + ) + } + } + } + enum Report { static var postReportTitle: String { pick(ko: "게시물 신고", en: "Report post", ja: "投稿通報") diff --git a/docs/20260331_하드코딩텍스트_I18n통일계획.md b/docs/20260331_하드코딩텍스트_I18n통일계획.md index d4fa0d5..b3f3486 100644 --- a/docs/20260331_하드코딩텍스트_I18n통일계획.md +++ b/docs/20260331_하드코딩텍스트_I18n통일계획.md @@ -95,19 +95,19 @@ ## 변경 대상 파일 전체 목록 ### Audition (13) -- [ ] `SodaLive/Sources/Audition/Applicant/ApplyMethodView.swift` -- [ ] `SodaLive/Sources/Audition/Applicant/AuditionApplicantItemView.swift` -- [ ] `SodaLive/Sources/Audition/Applicant/AuditionApplicantRecordingView.swift` -- [ ] `SodaLive/Sources/Audition/Applicant/AuditionApplyView.swift` -- [ ] `SodaLive/Sources/Audition/AuditionItemView.swift` -- [ ] `SodaLive/Sources/Audition/AuditionView.swift` -- [ ] `SodaLive/Sources/Audition/AuditionViewModel.swift` -- [ ] `SodaLive/Sources/Audition/Detail/AuditionDetailView.swift` -- [ ] `SodaLive/Sources/Audition/Detail/AuditionDetailViewModel.swift` -- [ ] `SodaLive/Sources/Audition/Detail/AuditionSoundManager.swift` -- [ ] `SodaLive/Sources/Audition/Role/AuditionDetailRoleItemView.swift` -- [ ] `SodaLive/Sources/Audition/Role/AuditionRoleDetailView.swift` -- [ ] `SodaLive/Sources/Audition/Role/AuditionRoleDetailViewModel.swift` +- [x] `SodaLive/Sources/Audition/Applicant/ApplyMethodView.swift` +- [x] `SodaLive/Sources/Audition/Applicant/AuditionApplicantItemView.swift` +- [x] `SodaLive/Sources/Audition/Applicant/AuditionApplicantRecordingView.swift` +- [x] `SodaLive/Sources/Audition/Applicant/AuditionApplyView.swift` +- [x] `SodaLive/Sources/Audition/AuditionItemView.swift` +- [x] `SodaLive/Sources/Audition/AuditionView.swift` +- [x] `SodaLive/Sources/Audition/AuditionViewModel.swift` +- [x] `SodaLive/Sources/Audition/Detail/AuditionDetailView.swift` +- [x] `SodaLive/Sources/Audition/Detail/AuditionDetailViewModel.swift` +- [x] `SodaLive/Sources/Audition/Detail/AuditionSoundManager.swift` +- [x] `SodaLive/Sources/Audition/Role/AuditionDetailRoleItemView.swift` +- [x] `SodaLive/Sources/Audition/Role/AuditionRoleDetailView.swift` +- [x] `SodaLive/Sources/Audition/Role/AuditionRoleDetailViewModel.swift` ### Chat (28) - [ ] `SodaLive/Sources/Chat/Character/CharacterItemView.swift` @@ -536,3 +536,26 @@ - 내부 로그/채널 prefix/API path/아이콘명/색상코드/Preview 샘플만 가진 29개 파일 추가 제외 완료. - 변경 대상 파일 수 `359 → 330`, 상위 모듈 수 `24 → 21`으로 갱신. - 예외 유지: `Content/Detail/ContentDetailPurchaseButton.swift`는 `"원으로"/"캔으로"` 사용자 노출 텍스트가 있어 유지. + +### 6차 구현 (Audition 모듈 13개 i18n 전환, 2026-03-31) +- 무엇/왜/어떻게: + - 무엇: 변경 대상 목록의 `Audition` 모듈 13개 파일을 전수 처리해 사용자 노출 하드코딩 문구를 `I18n.*` 참조로 교체. + - 왜: Audition 영역의 UI/토스트/다이얼로그/오류 메시지가 하드코딩 상태여서 다국어 일관성이 깨지고 유지보수 비용이 높았기 때문. + - 어떻게: explore/librarian/Oracle 병렬 분석 + `grep`/`ast_grep_search` 직접 검증으로 누락 지점을 수집한 뒤, `I18n.swift`에 `I18n.Audition` 네임스페이스를 추가하고 호출부를 모듈 단위로 일괄 치환. +- 실행 명령/도구: + - `task(subagent_type="explore", ...)` x2 (`bg_f58a087c`, `bg_02b03b28`) + - `task(subagent_type="librarian", ...)` x2 (`bg_f62866ac`, `bg_5cd8656b`) + - `task(subagent_type="oracle", ...)` x1 (`bg_81c2c04e`) + - `grep("\"[^\"]*[가-힣][^\"]*\"", include=*.swift, path=SodaLive/Sources/Audition)` + - `ast_grep_search(pattern="Text(\"$TEXT\")", lang=swift, paths=[SodaLive/Sources/Audition])` + - `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` +- 결과: + - Audition 호출부 치환 완료 파일: `ApplyMethodView`, `AuditionApplicantRecordingView`, `AuditionApplyView`, `AuditionView`, `AuditionViewModel`, `AuditionDetailView`, `AuditionDetailViewModel`, `AuditionSoundManager`, `AuditionDetailRoleItemView`, `AuditionRoleDetailView`, `AuditionRoleDetailViewModel`. + - `I18n.swift`에 `I18n.Audition`(List/ApplyMethod/Apply/Recording/Detail/Vote/Sound) 키셋 추가 및 공통 오류는 `I18n.Common.commonError`로 통합. + - 녹음 자동 파일명(`voiceon_now_voice_*`)이 사용자에게 그대로 보이던 문제를 `displayFileName` 처리(`I18n.Audition.Apply.recordedVoiceFileName`)로 보정. + - Audition 모듈 하드코딩 한글 재검증 결과, 남은 문자열은 Preview 샘플/DEBUG_LOG/서버 메시지 분기 비교(비노출 로직)만 존재. + - 빌드 검증: `SodaLive`, `SodaLive-dev` Debug 빌드 모두 성공(`** BUILD SUCCEEDED **`). + - 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 테스트 액션 미구성 확인(코드 실패 아님, 스킴 제약).