feat(i18n): 주요 UI 하드코딩 문구를 I18n 키로 통일한다

This commit is contained in:
Yu Sung
2026-03-31 17:09:01 +09:00
parent 47085dc1ca
commit b2f66cf408
17 changed files with 448 additions and 196 deletions

View File

@@ -437,6 +437,7 @@
} }
}, },
"(채팅 12개) 바로 대화 시작" : { "(채팅 12개) 바로 대화 시작" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -2304,6 +2305,7 @@
} }
}, },
"결제 후 입장" : { "결제 후 입장" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "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" : { "localizations" : {
"en" : { "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", "extractionState" : "stale",
"localizations" : { "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" : { "localizations" : {
"en" : { "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", "extractionState" : "stale",
"localizations" : { "localizations" : {
@@ -8331,6 +8333,7 @@
} }
}, },
"최근 대화한 캐릭터 " : { "최근 대화한 캐릭터 " : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {

View File

@@ -43,7 +43,7 @@ struct ExpandableTextView: View {
Spacer() Spacer()
Image(isExpanded ? "ic_live_detail_top" : "ic_live_detail_bottom") 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) .appFont(size: 12, weight: .medium)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
Spacer() Spacer()

View File

@@ -20,12 +20,12 @@ struct ApplyAuditionCompleteDialog: View {
.opacity(0.5) .opacity(0.5)
VStack(spacing: 0) { VStack(spacing: 0) {
Text("오디션 지원") Text(I18n.Audition.Apply.title)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.padding(.top, 26.7) .padding(.top, 26.7)
Text("보이스온 오디션에 지원해 주셔서 감사합니다.") Text(I18n.Dialog.ApplyAuditionComplete.thankYouDescription)
.appFont(size: 15, weight: .medium) .appFont(size: 15, weight: .medium)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
.padding(.top, 15) .padding(.top, 15)
@@ -40,7 +40,7 @@ struct ApplyAuditionCompleteDialog: View {
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
.padding(.top, 10) .padding(.top, 10)
Text("확인") Text(I18n.Common.confirm)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.padding(.vertical, 16) .padding(.vertical, 16)

View File

@@ -22,12 +22,12 @@ struct CommunityPostPurchaseDialog: View {
.frame(width: geo.size.width, height: geo.size.height) .frame(width: geo.size.width, height: geo.size.height)
VStack(spacing: 0) { VStack(spacing: 0) {
Text("게시글 보기") Text(I18n.Dialog.CommunityPostPurchase.title)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
.padding(.top, 40) .padding(.top, 40)
Text("게시글을\n확인하시겠습니까?") Text(I18n.Dialog.CommunityPostPurchase.description)
.appFont(size: 15, weight: .medium) .appFont(size: 15, weight: .medium)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@@ -35,7 +35,7 @@ struct CommunityPostPurchaseDialog: View {
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
HStack(spacing: 13.3) { HStack(spacing: 13.3) {
Text("취소") Text(I18n.Common.cancel)
.appFont(size: 15.3, weight: .bold) .appFont(size: 15.3, weight: .bold)
.foregroundColor(Color.button) .foregroundColor(Color.button)
.padding(.vertical, 16) .padding(.vertical, 16)
@@ -50,7 +50,7 @@ struct CommunityPostPurchaseDialog: View {
isShowing = false isShowing = false
} }
Text("\(can)캔으로 보기") Text(I18n.Dialog.CommunityPostPurchase.viewWithCans(can))
.appFont(size: 15.3, weight: .bold) .appFont(size: 15.3, weight: .bold)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.padding(.vertical, 16) .padding(.vertical, 16)

View File

@@ -28,7 +28,7 @@ struct CreatorFollowNotifyDialog: View {
if isShow { if isShow {
VStack(alignment: .leading, spacing: 24) { VStack(alignment: .leading, spacing: 24) {
Text("알림") Text(I18n.Common.alertTitle)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)

View File

@@ -36,7 +36,7 @@ struct LivePaymentDialog: View {
if let startDateTime = startDateTime, let nowDateTime = nowDateTime, let desc = desc2 { if let startDateTime = startDateTime, let nowDateTime = nowDateTime, let desc = desc2 {
VStack(spacing: 13.3) { VStack(spacing: 13.3) {
HStack(spacing: 6.7) { HStack(spacing: 6.7) {
Text("- 시작 시각 : ") Text(I18n.Dialog.LivePayment.startTimePrefix)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
@@ -46,7 +46,7 @@ struct LivePaymentDialog: View {
} }
HStack(spacing: 6.7) { HStack(spacing: 6.7) {
Text("- 현재 시각 :") Text(I18n.Dialog.LivePayment.currentTimePrefix)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
@@ -70,7 +70,7 @@ struct LivePaymentDialog: View {
} }
HStack(spacing: 13.3) { HStack(spacing: 13.3) {
Text("취소") Text(cancelButtonTitle)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.button) .foregroundColor(Color.button)
.padding(.vertical, 16) .padding(.vertical, 16)
@@ -83,7 +83,7 @@ struct LivePaymentDialog: View {
) )
.onTapGesture { cancelButtonAction() } .onTapGesture { cancelButtonAction() }
Text("결제 후 입장") Text(confirmButtonTitle)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.padding(.vertical, 16) .padding(.vertical, 16)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@@ -106,9 +106,9 @@ struct LivePaymentDialog: View {
title: "유료 라이브 입장", title: "유료 라이브 입장",
desc: "OO캔을 차감하고\n라이브에 입장 하시겠습니까?", desc: "OO캔을 차감하고\n라이브에 입장 하시겠습니까?",
desc2: "라이브가 시작한 지 1시간 10분이 지났습니다. 라이브에 입장 후 30분 이내에 라이브가 종료될 수도 있습니다.", desc2: "라이브가 시작한 지 1시간 10분이 지났습니다. 라이브에 입장 후 30분 이내에 라이브가 종료될 수도 있습니다.",
confirmButtonTitle: "", confirmButtonTitle: I18n.MemberChannel.paidLiveConfirmTitle,
confirmButtonAction: {}, confirmButtonAction: {},
cancelButtonTitle: "", cancelButtonTitle: I18n.Common.cancel,
cancelButtonAction: {}, cancelButtonAction: {},
startDateTime: "2024-01-01 15:00", startDateTime: "2024-01-01 15:00",
nowDateTime: "2024-01-02 15:00" nowDateTime: "2024-01-02 15:00"

View File

@@ -25,12 +25,12 @@ struct LiveRoomPasswordDialog: View {
.frame(width: geo.size.width, height: geo.size.height) .frame(width: geo.size.width, height: geo.size.height)
VStack(spacing: 0) { VStack(spacing: 0) {
Text("비밀번호 입력") Text(I18n.Dialog.LiveRoomPassword.title)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color(hex: "bbbbbb"))
.padding(.top, 40) .padding(.top, 40)
Text("비공개 라이브의 입장 비밀번호를\n입력해 주세요.") Text(I18n.Dialog.LiveRoomPassword.description)
.appFont(size: 13, weight: .medium) .appFont(size: 13, weight: .medium)
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color(hex: "bbbbbb"))
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@@ -38,8 +38,8 @@ struct LiveRoomPasswordDialog: View {
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
UserTextField( UserTextField(
title: "비밀번호", title: I18n.Dialog.LiveRoomPassword.passwordFieldTitle,
hint: "비밀번호를 입력해 주세요", hint: I18n.Dialog.LiveRoomPassword.passwordFieldPlaceholder,
isSecure: false, isSecure: false,
variable: $password, variable: $password,
keyboardType: .numberPad keyboardType: .numberPad
@@ -48,7 +48,7 @@ struct LiveRoomPasswordDialog: View {
.padding(.top, 13.3) .padding(.top, 13.3)
HStack(spacing: 13.3) { HStack(spacing: 13.3) {
Text("취소") Text(I18n.Common.cancel)
.appFont(size: 15.3, weight: .bold) .appFont(size: 15.3, weight: .bold)
.foregroundColor(Color(hex: "3bb9f1")) .foregroundColor(Color(hex: "3bb9f1"))
.padding(.vertical, 16) .padding(.vertical, 16)
@@ -73,7 +73,7 @@ struct LiveRoomPasswordDialog: View {
.resizable() .resizable()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
Text("으로 입장") Text(I18n.Dialog.LiveRoomPassword.enterSuffix)
.appFont(size: 15.3, weight: .bold) .appFont(size: 15.3, weight: .bold)
.foregroundColor(Color(hex: "ffffff")) .foregroundColor(Color(hex: "ffffff"))
} }
@@ -90,7 +90,7 @@ struct LiveRoomPasswordDialog: View {
isShowing = false isShowing = false
} }
} else { } else {
Text("입장하기") Text(I18n.Dialog.LiveRoomPassword.enter)
.appFont(size: 15.3, weight: .bold) .appFont(size: 15.3, weight: .bold)
.foregroundColor(Color(hex: "ffffff")) .foregroundColor(Color(hex: "ffffff"))
.padding(.vertical, 16) .padding(.vertical, 16)

View File

@@ -25,7 +25,7 @@ struct MemberProfileDialog: View {
VStack(alignment: .leading, spacing: 21) { VStack(alignment: .leading, spacing: 21) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("프로필") Text(I18n.Dialog.MemberProfile.title)
.appFont(size: 15, weight: .bold) .appFont(size: 15, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
@@ -56,7 +56,7 @@ struct MemberProfileDialog: View {
.cornerRadius(8) .cornerRadius(8)
HStack(spacing: 8) { HStack(spacing: 8) {
Text(profile.isBlocked ? "차단 해제" : "차단") Text(profile.isBlocked ? I18n.Dialog.MemberProfile.unblock : I18n.MemberChannel.blockAction)
.appFont(size: 15, weight: .bold) .appFont(size: 15, weight: .bold)
.foregroundColor(Color.button) .foregroundColor(Color.button)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@@ -76,7 +76,7 @@ struct MemberProfileDialog: View {
} }
} }
Text("사용자 신고") Text(I18n.Dialog.MemberProfile.reportUser)
.appFont(size: 15, weight: .bold) .appFont(size: 15, weight: .bold)
.foregroundColor(Color.button) .foregroundColor(Color.button)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@@ -90,7 +90,7 @@ struct MemberProfileDialog: View {
) )
.onTapGesture { viewModel.isShowUesrReportView = true } .onTapGesture { viewModel.isShowUesrReportView = true }
Text("프로필 신고") Text(I18n.Dialog.MemberProfile.reportProfile)
.appFont(size: 15, weight: .bold) .appFont(size: 15, weight: .bold)
.foregroundColor(Color.button) .foregroundColor(Color.button)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@@ -115,7 +115,7 @@ struct MemberProfileDialog: View {
.frame(maxWidth: screenSize().width - 33.3) .frame(maxWidth: screenSize().width - 33.3)
.onAppear { .onAppear {
if memberId <= 1 { if memberId <= 1 {
viewModel.errorMessage = "잘못된 요청입니다." viewModel.errorMessage = I18n.Dialog.MemberProfile.invalidRequest
viewModel.isShowPopup = true viewModel.isShowPopup = true
} else { } else {
viewModel.getMemberProfile(memberId: memberId) viewModel.getMemberProfile(memberId: memberId)

View File

@@ -18,10 +18,10 @@ struct FollowCreatorView: View {
var body: some View { var body: some View {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) { VStack(spacing: 0) {
DetailNavigationBar(title: String(localized: "팔로잉 리스트")) DetailNavigationBar(title: I18n.Follow.followingListTitle)
HStack(spacing: 0) { HStack(spacing: 0) {
Text("") Text(I18n.Follow.totalPrefix)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
@@ -29,7 +29,7 @@ struct FollowCreatorView: View {
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.mainRed3) .foregroundColor(Color.mainRed3)
Text("") Text(I18n.Follow.personUnit)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
@@ -70,7 +70,7 @@ struct FollowCreatorView: View {
.padding(.top, 13.3) .padding(.top, 13.3)
} }
} else { } else {
Text("팔로우 중인 채널이 없습니다.") Text(I18n.Follow.emptyFollowingChannels)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.frame(maxHeight: .infinity) .frame(maxHeight: .infinity)

View File

@@ -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 { enum Series {
static var new: String { pick(ko: "신작", en: "New", ja: "新作") } static var new: String { pick(ko: "신작", en: "New", ja: "新作") }
static var complete: String { pick(ko: "완결", en: "Completed", ja: "完結") } static var complete: String { pick(ko: "완결", en: "Completed", ja: "完結") }

View File

@@ -79,7 +79,7 @@ extension StoreManager: SKProductsRequestDelegate {
DEBUG_LOG("상품불러오기 실패: \(error)") DEBUG_LOG("상품불러오기 실패: \(error)")
DispatchQueue.main.async { [unowned self] in DispatchQueue.main.async { [unowned self] in
self.isLoading = false self.isLoading = false
errorMessage = "상품을 불러오지 못했습니다.\n다시 시도해 주세요." errorMessage = I18n.IAP.loadProductsFailed
self.isShowPopup = true self.isShowPopup = true
} }
} }
@@ -108,7 +108,7 @@ extension StoreManager: SKPaymentTransactionObserver {
case .deferred: case .deferred:
isLoading = false isLoading = false
DEBUG_LOG("아이폰이 잠김 등의 이유로 결제를 진행하지 못했습니다.") DEBUG_LOG("아이폰이 잠김 등의 이유로 결제를 진행하지 못했습니다.")
errorMessage = "아이폰이 잠김 등의 이유로 결제를 진행하지 못했습니다." errorMessage = I18n.IAP.deferredPaymentFailed
isShowPopup = true isShowPopup = true
SKPaymentQueue.default().finishTransaction(transaction) SKPaymentQueue.default().finishTransaction(transaction)
@@ -118,7 +118,7 @@ extension StoreManager: SKPaymentTransactionObserver {
case .restored: case .restored:
isLoading = false isLoading = false
DEBUG_LOG("상품 검증을 하였습니다.") DEBUG_LOG("상품 검증을 하였습니다.")
errorMessage = "상품 검증을 하였습니다." errorMessage = I18n.IAP.productValidationCompleted
isShowPopup = true isShowPopup = true
SKPaymentQueue.default().finishTransaction(transaction) SKPaymentQueue.default().finishTransaction(transaction)
@@ -128,7 +128,7 @@ extension StoreManager: SKPaymentTransactionObserver {
@unknown default: @unknown default:
isLoading = false isLoading = false
DEBUG_LOG("알 수 없는 오류가 발생했습니다.") DEBUG_LOG("알 수 없는 오류가 발생했습니다.")
errorMessage = "알 수 없는 오류가 발생했습니다." errorMessage = I18n.IAP.unknownError
isShowPopup = true isShowPopup = true
SKPaymentQueue.default().finishTransaction(transaction) SKPaymentQueue.default().finishTransaction(transaction)
@@ -173,7 +173,7 @@ extension StoreManager: SKPaymentTransactionObserver {
} }
DispatchQueue.main.async { [unowned self] in DispatchQueue.main.async { [unowned self] in
errorMessage = "결제를 진행하지 못했습니다.\n다시 시도해 주세요." errorMessage = I18n.IAP.paymentFailed
isShowPopup = true isShowPopup = true
} }

View File

@@ -112,7 +112,7 @@ struct ImageCropEditorView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack { HStack {
Button(action: onCancel) { Button(action: onCancel) {
Text("취소") Text(I18n.Common.cancel)
.appFont(size: 16.7, weight: .bold) .appFont(size: 16.7, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
} }
@@ -126,7 +126,7 @@ struct ImageCropEditorView: View {
onCancel() onCancel()
} }
}) { }) {
Text("적용") Text(I18n.Common.apply)
.appFont(size: 16.7, weight: .bold) .appFont(size: 16.7, weight: .bold)
.foregroundColor(Color.button) .foregroundColor(Color.button)
} }
@@ -233,7 +233,7 @@ struct ImageCropEditorView: View {
} }
if aspectPolicy == .free { if aspectPolicy == .free {
Text("모서리 원을 드래그해서 크롭 영역 크기를 조정하세요") Text(I18n.ImagePicker.cropResizeGuide)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.padding(.top, 8) .padding(.top, 8)

View File

@@ -38,7 +38,7 @@ struct EventPopupDialogView: View {
} }
HStack(spacing: 0) { HStack(spacing: 0) {
Text("다시보지 않기") Text(I18n.Main.EventPopup.doNotShowAgain)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
.onTapGesture { .onTapGesture {
@@ -48,7 +48,7 @@ struct EventPopupDialogView: View {
Spacer() Spacer()
Text("닫기") Text(I18n.Main.EventPopup.close)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
.onTapGesture { AppState.shared.eventPopup = nil } .onTapGesture { AppState.shared.eventPopup = nil }

View File

@@ -16,7 +16,7 @@ struct BottomTabView: View {
let tabWidth = width / 4 let tabWidth = width / 4
TabButton( TabButton(
title: "", title: I18n.Main.Tab.home,
action: { action: {
if currentTab != .home { if currentTab != .home {
currentTab = .home currentTab = .home
@@ -37,7 +37,7 @@ struct BottomTabView: View {
) )
TabButton( TabButton(
title: "라이브", title: I18n.Main.Tab.live,
action: { action: {
if currentTab != .live { if currentTab != .live {
currentTab = .live currentTab = .live
@@ -58,7 +58,7 @@ struct BottomTabView: View {
) )
TabButton( TabButton(
title: "채팅", title: I18n.Main.Tab.chat,
action: { action: {
if currentTab != .chat { if currentTab != .chat {
currentTab = .chat currentTab = .chat
@@ -79,7 +79,7 @@ struct BottomTabView: View {
) )
TabButton( TabButton(
title: "마이", title: I18n.Main.Tab.my,
action: { action: {
if currentTab != .mypage { if currentTab != .mypage {
currentTab = .mypage currentTab = .mypage

View File

@@ -216,15 +216,14 @@ struct HomeView: View {
if isShowAuthConfirmView { if isShowAuthConfirmView {
SodaDialog( SodaDialog(
title: "본인인증", title: I18n.Main.Auth.dialogTitle,
desc: "청소년 보호를 위해\n본인인증을 완료한\n성인만 라이브 입장이 가능합니다.\n" + desc: I18n.Main.Auth.liveEntryVerificationDescription,
"라이브 입장을 위해\n본인인증을 진행해 주세요.", confirmButtonTitle: I18n.Main.Auth.goToVerification,
confirmButtonTitle: "본인인증 하러가기",
confirmButtonAction: { confirmButtonAction: {
isShowAuthConfirmView = false isShowAuthConfirmView = false
isShowAuthView = true isShowAuthView = true
}, },
cancelButtonTitle: "취소", cancelButtonTitle: I18n.Common.cancel,
cancelButtonAction: { cancelButtonAction: {
isShowAuthConfirmView = false isShowAuthConfirmView = false
pendingAction = nil pendingAction = nil
@@ -306,7 +305,7 @@ struct HomeView: View {
isShowAuthView = false isShowAuthView = false
} }
.onError { _ in .onError { _ in
AppState.shared.errorMessage = "본인인증 중 오류가 발생했습니다." AppState.shared.errorMessage = I18n.Main.Auth.authenticationError
AppState.shared.isShowErrorPopup = true AppState.shared.isShowErrorPopup = true
isShowAuthView = false isShowAuthView = false
} }

View File

@@ -9,7 +9,7 @@ import SwiftUI
struct TabButton: View { struct TabButton: View {
let title: LocalizedStringResource let title: String
let action: () -> Void let action: () -> Void
let image: () -> String let image: () -> String
let fontWeight: () -> SwiftUI.Font.Weight let fontWeight: () -> SwiftUI.Font.Weight

View File

@@ -220,17 +220,17 @@
- [ ] `SodaLive/Sources/Content/Series/SeriesListAllViewModel.swift` - [ ] `SodaLive/Sources/Content/Series/SeriesListAllViewModel.swift`
### CustomView (3) ### CustomView (3)
- [ ] `SodaLive/Sources/CustomView/ChatTextFieldView.swift` - [x] `SodaLive/Sources/CustomView/ChatTextFieldView.swift`
- [ ] `SodaLive/Sources/CustomView/ExpandableTextView.swift` - [x] `SodaLive/Sources/CustomView/ExpandableTextView.swift`
- [ ] `SodaLive/Sources/CustomView/IconAndTitleToggleButton.swift` - [x] `SodaLive/Sources/CustomView/IconAndTitleToggleButton.swift`
### Dialog (6) ### Dialog (6)
- [ ] `SodaLive/Sources/Dialog/ApplyAuditionCompleteDialog.swift` - [x] `SodaLive/Sources/Dialog/ApplyAuditionCompleteDialog.swift`
- [ ] `SodaLive/Sources/Dialog/CommunityPostPurchaseDialog.swift` - [x] `SodaLive/Sources/Dialog/CommunityPostPurchaseDialog.swift`
- [ ] `SodaLive/Sources/Dialog/CreatorFollowNotifyDialog.swift` - [x] `SodaLive/Sources/Dialog/CreatorFollowNotifyDialog.swift`
- [ ] `SodaLive/Sources/Dialog/LivePaymentDialog.swift` - [x] `SodaLive/Sources/Dialog/LivePaymentDialog.swift`
- [ ] `SodaLive/Sources/Dialog/LiveRoomPasswordDialog.swift` - [x] `SodaLive/Sources/Dialog/LiveRoomPasswordDialog.swift`
- [ ] `SodaLive/Sources/Dialog/MemberProfileDialog.swift` - [x] `SodaLive/Sources/Dialog/MemberProfileDialog.swift`
### Explorer (40) ### Explorer (40)
- [ ] `SodaLive/Sources/Explorer/ExplorerSectionView.swift` - [ ] `SodaLive/Sources/Explorer/ExplorerSectionView.swift`
@@ -275,7 +275,7 @@
- [ ] `SodaLive/Sources/Explorer/Profile/UserProfileViewModel.swift` - [ ] `SodaLive/Sources/Explorer/Profile/UserProfileViewModel.swift`
### Follow (1) ### Follow (1)
- [ ] `SodaLive/Sources/Follow/FollowCreatorView.swift` - [x] `SodaLive/Sources/Follow/FollowCreatorView.swift`
### Home (9) ### Home (9)
- [ ] `SodaLive/Sources/Home/HomeAuditionView.swift` - [ ] `SodaLive/Sources/Home/HomeAuditionView.swift`
@@ -289,10 +289,10 @@
- [ ] `SodaLive/Sources/Home/RecommendChannel/RecommendChannelItemView.swift` - [ ] `SodaLive/Sources/Home/RecommendChannel/RecommendChannelItemView.swift`
### IAP (1) ### IAP (1)
- [ ] `SodaLive/Sources/IAP/StoreManager.swift` - [x] `SodaLive/Sources/IAP/StoreManager.swift`
### ImagePicker (1) ### ImagePicker (1)
- [ ] `SodaLive/Sources/ImagePicker/ImagePicker.swift` - [x] `SodaLive/Sources/ImagePicker/ImagePicker.swift`
### Live (56) ### Live (56)
- [ ] `SodaLive/Sources/Live/Cancel/LiveCancelDialog.swift` - [ ] `SodaLive/Sources/Live/Cancel/LiveCancelDialog.swift`
@@ -353,9 +353,9 @@
- [ ] `SodaLive/Sources/Live/SectionLatestFinishedLiveView.swift` - [ ] `SodaLive/Sources/Live/SectionLatestFinishedLiveView.swift`
### Main (3) ### Main (3)
- [ ] `SodaLive/Sources/Main/EventPopupDialogView.swift` - [x] `SodaLive/Sources/Main/EventPopupDialogView.swift`
- [ ] `SodaLive/Sources/Main/Home/BottomTabView.swift` - [x] `SodaLive/Sources/Main/Home/BottomTabView.swift`
- [ ] `SodaLive/Sources/Main/Home/HomeView.swift` - [x] `SodaLive/Sources/Main/Home/HomeView.swift`
### Message (13) ### Message (13)
- [ ] `SodaLive/Sources/Message/MessageFilterTabView.swift` - [ ] `SodaLive/Sources/Message/MessageFilterTabView.swift`
@@ -587,3 +587,28 @@
- Chat 모듈 하드코딩 한글 재검증 결과, 남은 문자열은 Preview 샘플/SDK 입력값/비노출 분기 로직만 존재. - Chat 모듈 하드코딩 한글 재검증 결과, 남은 문자열은 Preview 샘플/SDK 입력값/비노출 분기 로직만 존재.
- 빌드 검증: `SodaLive`, `SodaLive-dev` Debug 빌드 모두 성공(`** BUILD SUCCEEDED **`). - 빌드 검증: `SodaLive`, `SodaLive-dev` Debug 빌드 모두 성공(`** BUILD SUCCEEDED **`).
- 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 테스트 액션 미구성 확인(코드 실패 아님, 스킴 제약). - 테스트 검증: 두 스킴 모두 `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.`로 테스트 액션 미구성 확인(코드 실패 아님, 스킴 제약).