feat(i18n): 사용자 화면 문구를 I18n 키로 통일한다

This commit is contained in:
Yu Sung
2026-03-31 17:37:29 +09:00
parent b2f66cf408
commit 8e4fe7a534
18 changed files with 551 additions and 256 deletions

View File

@@ -16,9 +16,6 @@
} }
} }
} }
},
" · %@" : {
}, },
" (" : { " (" : {
"localizations" : { "localizations" : {
@@ -828,6 +825,16 @@
} }
} }
}, },
"%@%@" : {
"localizations" : {
"ko" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@%2$@"
}
}
}
},
"%@님을 차단하시겠습니까?" : { "%@님을 차단하시겠습니까?" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -4136,22 +4143,6 @@
} }
} }
}, },
"목" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Thu"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "木"
}
}
}
},
"모든 기기에서 로그아웃" : { "모든 기기에서 로그아웃" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -4203,6 +4194,22 @@
} }
} }
}, },
"목" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Thu"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "木"
}
}
}
},
"무료" : { "무료" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -6933,134 +6940,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" : "人気"
}
}
}
},
"인기 캐릭터 채팅" : {
"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" : {
@@ -7141,6 +7020,54 @@
} }
} }
}, },
"이용약관" : {
"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" : {
@@ -7174,6 +7101,38 @@
} }
} }
}, },
"인기 캐릭터 채팅" : {
"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" : {
@@ -7190,6 +7149,54 @@
} }
} }
}, },
"인기순" : {
"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" : {

View File

@@ -355,6 +355,23 @@ enum I18n {
pick(ko: "더보기 >", en: "More >", ja: "もっと見る >") pick(ko: "더보기 >", en: "More >", ja: "もっと見る >")
} }
} }
enum SearchChannel {
static var title: String {
pick(ko: "채널 탐색", en: "Search channels", ja: "チャンネル探索")
}
}
enum NotificationList {
static var emptyMessage: String {
pick(ko: "알림이 없습니다.", en: "No notifications.", ja: "通知がありません。")
}
static var timestampSeparator: String {
pick(ko: " · ", en: " · ", ja: " · ")
}
}
enum ContentDetail { enum ContentDetail {
static var creatorOtherContents: String { static var creatorOtherContents: String {
pick( pick(
@@ -835,6 +852,10 @@ enum I18n {
} }
enum Report { enum Report {
static var cheersReportTitle: String {
pick(ko: "응원글 신고", en: "Report cheer post", ja: "応援投稿を通報")
}
static var postReportTitle: String { static var postReportTitle: String {
pick(ko: "게시물 신고", en: "Report post", ja: "投稿通報") pick(ko: "게시물 신고", en: "Report post", ja: "投稿通報")
} }
@@ -843,6 +864,135 @@ enum I18n {
pick(ko: "신고", en: "Report", ja: "報告する") pick(ko: "신고", en: "Report", ja: "報告する")
} }
static var cheersReasons: [String] {
[
cheersReasonSpam,
cheersReasonChildAbuse,
cheersReasonHateOrViolence,
cheersReasonTerrorism,
cheersReasonHarassment,
cheersReasonSelfHarm,
cheersReasonMisinformation
]
}
static var userReasons: [String] {
[
userReasonHarassment,
userReasonPrivacy,
userReasonImpersonation,
userReasonThreat,
userReasonChildAbuse,
userReasonHate,
userReasonSpamFraud,
userReasonNone
]
}
static var cheersReasonSpam: String {
pick(
ko: "원치 않는 상업성 콘텐츠 또는 스팸",
en: "Unwanted commercial content or spam",
ja: "望まない商業コンテンツまたはスパム"
)
}
static var cheersReasonChildAbuse: String {
pick(ko: "아동 학대", en: "Child abuse", ja: "児童虐待")
}
static var cheersReasonHateOrViolence: String {
pick(
ko: "증오심 표현 또는 노골적인 폭력",
en: "Hate speech or graphic violence",
ja: "憎悪表現または過度な暴力表現"
)
}
static var cheersReasonTerrorism: String {
pick(ko: "테러 조장", en: "Promotion of terrorism", ja: "テロ助長")
}
static var cheersReasonHarassment: String {
pick(ko: "희롱 또는 괴롭힘", en: "Harassment or bullying", ja: "嫌がらせまたはいじめ")
}
static var cheersReasonSelfHarm: String {
pick(ko: "자살 또는 자해", en: "Suicide or self-harm", ja: "自殺または自傷行為")
}
static var cheersReasonMisinformation: String {
pick(ko: "잘못된 정보", en: "Misinformation", ja: "誤情報")
}
static var userReasonHarassment: String {
pick(ko: "괴롭힘 및 사이버 폭력", en: "Harassment and cyberbullying", ja: "嫌がらせとサイバー暴力")
}
static var userReasonPrivacy: String {
pick(ko: "개인정보 침해", en: "Privacy violation", ja: "プライバシー侵害")
}
static var userReasonImpersonation: String {
pick(ko: "명의 도용", en: "Impersonation", ja: "なりすまし")
}
static var userReasonThreat: String {
pick(ko: "폭력적 위협", en: "Violent threats", ja: "暴力的な脅迫")
}
static var userReasonChildAbuse: String {
pick(ko: "아동 학대", en: "Child abuse", ja: "児童虐待")
}
static var userReasonHate: String {
pick(ko: "보호 대상 집단에 대한 증오심 표현", en: "Hate speech toward protected groups", ja: "保護対象グループへの憎悪表現")
}
static var userReasonSpamFraud: String {
pick(ko: "스팸 및 사기", en: "Spam and fraud", ja: "スパムと詐欺")
}
static var userReasonNone: String {
pick(ko: "나에게 해당하는 문제 없음", en: "None of these apply", ja: "該当する問題なし")
}
static var profilePhotoReportTitle: String {
pick(ko: "프로필 사진 신고", en: "Report profile photo", ja: "プロフィール写真を通報")
}
static var profilePhotoReportDescription: String {
pick(
ko: "신고제도를 남용할 경우, 계정에 제약이 있을 수 있습니다.\n프로필 사진을 신고하시겠습니까?",
en: "Abusing the reporting system may restrict your account.\nDo you want to report this profile photo?",
ja: "通報制度を乱用した場合、アカウントに制約がかかる可能性があります。\nプロフィール写真を通報しますか?"
)
}
static var userReportTitle: String {
pick(ko: "사용자 신고", en: "Report user", ja: "ユーザーを通報")
}
static var userReportAction: String {
pick(ko: "사용자 신고하기", en: "Report user", ja: "ユーザーを通報")
}
static var profileReportAction: String {
pick(ko: "프로필 신고하기", en: "Report profile", ja: "プロフィールを通報")
}
static var blockUserAction: String {
pick(ko: "사용자 차단하기", en: "Block user", ja: "ユーザーをブロック")
}
static var unblockUserAction: String {
pick(ko: "사용자 차단해제", en: "Unblock user", ja: "ユーザーのブロックを解除")
}
static var profileReportReason: String {
pick(ko: "프로필 신고", en: "Report profile", ja: "プロフィールを通報")
}
static var reasons: [String] { static var reasons: [String] {
[ [
reasonSpam, reasonSpam,
@@ -2080,7 +2230,149 @@ If you block this user, the following features will be restricted.
} }
} }
enum User {
static var emailTitle: String {
pick(ko: "이메일", en: "Email", ja: "メール")
}
static var emailPlaceholder: String {
pick(ko: "이메일", en: "Email", ja: "メール")
}
static var passwordTitle: String {
pick(ko: "비밀번호", en: "Password", ja: "パスワード")
}
static var passwordPlaceholder: String {
pick(ko: "비밀번호", en: "Password", ja: "パスワード")
}
static var showPassword: String {
pick(ko: "비밀번호 표시", en: "Show password", ja: "パスワードを表示")
}
static var emailRequired: String {
pick(ko: "이메일을 입력해 주세요.", en: "Please enter your email.", ja: "メールアドレスを入力してください。")
}
static var emailInvalid: String {
pick(ko: "올바른 이메일을 입력하세요", en: "Enter a valid email address.", ja: "正しいメールアドレスを入力してください。")
}
static var passwordRequired: String {
pick(ko: "비밀번호를 입력해 주세요.", en: "Please enter your password.", ja: "パスワードを入力してください。")
}
static var blockUserAction: String {
pick(ko: "사용자 차단하기", en: "Block user", ja: "ユーザーをブロック")
}
static var unblockUserAction: String {
pick(ko: "사용자 차단해제", en: "Unblock user", ja: "ユーザーのブロックを解除")
}
static var reportUserAction: String {
pick(ko: "사용자 신고하기", en: "Report user", ja: "ユーザーを通報")
}
static var reportProfileAction: String {
pick(ko: "프로필 신고하기", en: "Report profile", ja: "プロフィールを通報")
}
}
enum SignUp {
static var title: String {
pick(ko: "회원가입", en: "Sign up", ja: "会員登録")
}
static var terms: String {
pick(ko: "이용약관", en: "Terms of service", ja: "利用規約")
}
static var privacyPolicy: String {
pick(ko: "개인정보수집 및 이용동의", en: "Consent to collect and use personal information", ja: "個人情報の収集および利用への同意")
}
static var required: String {
pick(ko: "(필수)", en: "(Required)", ja: "(必須)")
}
static var submit: String {
pick(ko: "회원가입", en: "Sign up", ja: "会員登録")
}
static var agreementRequired: String {
pick(ko: "약관에 동의하셔야 회원가입이 가능합니다.", en: "You must agree to the terms to sign up.", ja: "利用規約に同意する必要があります。")
}
}
enum FindPassword {
static var title: String {
pick(ko: "비밀번호 재설정", en: "Reset password", ja: "パスワード再設定")
}
static var description1: String {
pick(ko: "회원가입한 이메일 주소로\n임시 비밀번호를 보내드립니다.", en: "We will send a temporary password to the email address you used to sign up.", ja: "登録したメールアドレスに仮パスワードを送信します。")
}
static var description2: String {
pick(ko: "임시 비밀번호로 로그인 후\n마이페이지 > 프로필 설정에서\n비밀번호를 변경하고 이용하세요.", en: "Log in with the temporary password, then change it in My Page > Profile Settings.", ja: "仮パスワードでログイン後、マイページ > プロフィール設定でパスワードを変更してください。")
}
static var emailPlaceholder: String {
pick(ko: "이메일을 입력하세요", en: "Enter your email", ja: "メールアドレスを入力してください")
}
static var submit: String {
pick(ko: "임시 비밀번호 받기", en: "Get temporary password", ja: "仮パスワードを受け取る")
}
static var contactSupport: String {
pick(ko: "고객센터로 문의하기", en: "Contact support", ja: "カスタマーセンターに問い合わせる")
}
static var emailRequired: String {
pick(ko: "이메일을 입력하세요.", en: "Please enter your email.", ja: "メールアドレスを入力してください。")
}
static var successMessage: String {
pick(ko: "임시 비밀번호가 입력하신 이메일로 발송되었습니다.\n이메일을 확인해 주세요.", en: "A temporary password has been sent to your email.\nPlease check your inbox.", ja: "仮パスワードを入力したメールアドレスに送信しました。\nメールをご確認ください。")
}
}
enum Login { enum Login {
static var title: String {
pick(ko: "로그인", en: "Log in", ja: "ログイン")
}
static var login: String {
pick(ko: "로그인", en: "Log in", ja: "ログイン")
}
static var forgotPassword: String {
pick(ko: "비밀번호를 잊으셨나요?", en: "Forgot your password?", ja: "パスワードを忘れましたか?")
}
static var signUpPrompt: String {
pick(ko: "보이스온 회원이 아닌가요? 지금 가입하세요.", en: "Not a VoiceOn member? Sign up now.", ja: "VoiceOnの会員ではありませんか今すぐ登録してください。")
}
static var appleAuthorizationFailed: String {
pick(ko: "애플 로그인 정보를 가져오지 못했습니다.", en: "Failed to retrieve Apple sign-in information.", ja: "Appleログイン情報を取得できませんでした。")
}
static var appleTokenMissing: String {
pick(ko: "애플 인증 토큰을 가져오지 못했습니다.", en: "Failed to retrieve Apple identity token.", ja: "Apple認証トークンを取得できませんでした。")
}
static var appleRetry: String {
pick(ko: "다시 시도해 주세요.", en: "Please try again.", ja: "もう一度お試しください。")
}
static var appleSignInFailed: String {
pick(ko: "애플 로그인에 실패했습니다.\n다시 시도해 주세요.", en: "Apple sign-in failed.\nPlease try again.", ja: "Appleログインに失敗しました。\nもう一度お試しください。")
}
enum Google { enum Google {
static var openFailed: String { static var openFailed: String {
pick( pick(

View File

@@ -25,7 +25,7 @@ struct PushNotificationListItemView: View {
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color(hex: "bbbbbb"))
Text(" · \(item.relativeSentAtText())") Text("\(I18n.NotificationList.timestampSeparator)\(item.relativeSentAtText())")
.appFont(size: 10, weight: .medium) .appFont(size: 10, weight: .medium)
.foregroundColor(Color(hex: "909090")) .foregroundColor(Color(hex: "909090"))
} }

View File

@@ -39,7 +39,7 @@ struct PushNotificationListView: View {
.resizable() .resizable()
.frame(width: 60, height: 60) .frame(width: 60, height: 60)
Text("알림이 없습니다.") Text(I18n.NotificationList.emptyMessage)
.appFont(size: 10.7, weight: .medium) .appFont(size: 10.7, weight: .medium)
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color(hex: "bbbbbb"))
} }
@@ -61,7 +61,7 @@ struct PushNotificationListView: View {
private var titleBar: some View { private var titleBar: some View {
ZStack { ZStack {
DetailNavigationBar(title: "알림") DetailNavigationBar(title: I18n.Common.alertTitle)
HStack(spacing: 0) { HStack(spacing: 0) {
Spacer() Spacer()

View File

@@ -13,15 +13,7 @@ struct CheersReportDialogView: View {
let confirmAction: (String) -> Void let confirmAction: (String) -> Void
@State private var selectedIndex: Int? = nil @State private var selectedIndex: Int? = nil
let reasons = [ let reasons = I18n.Report.cheersReasons
"원치 않는 상업성 콘텐츠 또는 스팸",
"아동 학대",
"증오심 표현 또는 노골적인 폭력",
"테러 조장",
"희롱 또는 괴롭힘",
"자살 또는 자해",
"잘못된 정보"
]
var body: some View { var body: some View {
ZStack { ZStack {
@@ -31,7 +23,7 @@ struct CheersReportDialogView: View {
.onTapGesture { isShowing = false } .onTapGesture { isShowing = false }
VStack(spacing: 13.3) { VStack(spacing: 13.3) {
Text("응원글 신고") Text(I18n.Report.cheersReportTitle)
.appFont(size: 16.7, weight: .medium) .appFont(size: 16.7, weight: .medium)
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
@@ -59,14 +51,14 @@ struct CheersReportDialogView: View {
HStack(spacing: 26.7) { HStack(spacing: 26.7) {
Spacer() Spacer()
Text("취소") Text(I18n.Common.cancel)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color(hex: "3bb9f1")) .foregroundColor(Color(hex: "3bb9f1"))
.onTapGesture { .onTapGesture {
isShowing = false isShowing = false
} }
Text("신고") Text(I18n.Report.reportAction)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color(hex: "3bb9f1")) .foregroundColor(Color(hex: "3bb9f1"))
.onTapGesture { .onTapGesture {

View File

@@ -20,25 +20,25 @@ struct ProfileReportDialogView: View {
.onTapGesture { isShowing = false } .onTapGesture { isShowing = false }
VStack(spacing: 13.3) { VStack(spacing: 13.3) {
Text("프로필 사진 신고") Text(I18n.Report.profilePhotoReportTitle)
.appFont(size: 16.7, weight: .medium) .appFont(size: 16.7, weight: .medium)
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
Text("신고제도를 남용할 경우, 계정에 제약이 있을 수 있습니다.\n프로필 사진을 신고하시겠습니까?") Text(I18n.Report.profilePhotoReportDescription)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color(hex: "909090")) .foregroundColor(Color(hex: "909090"))
HStack(spacing: 26.7) { HStack(spacing: 26.7) {
Spacer() Spacer()
Text("취소") Text(I18n.Common.cancel)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color(hex: "3bb9f1")) .foregroundColor(Color(hex: "3bb9f1"))
.onTapGesture { .onTapGesture {
isShowing = false isShowing = false
} }
Text("신고") Text(I18n.Report.reportAction)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color(hex: "3bb9f1")) .foregroundColor(Color(hex: "3bb9f1"))
.onTapGesture { .onTapGesture {

View File

@@ -29,7 +29,7 @@ struct ProfileReportMenuView: View {
VStack(spacing: 13.3) { VStack(spacing: 13.3) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text(isBlockedUser ? "사용자 차단해제" : "사용자 차단하기") Text(isBlockedUser ? I18n.User.unblockUserAction : I18n.User.blockUserAction)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(.white) .foregroundColor(.white)
@@ -48,7 +48,7 @@ struct ProfileReportMenuView: View {
} }
HStack(spacing: 0) { HStack(spacing: 0) {
Text("사용자 신고하기") Text(I18n.User.reportUserAction)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(.white) .foregroundColor(.white)
@@ -63,7 +63,7 @@ struct ProfileReportMenuView: View {
} }
HStack(spacing: 0) { HStack(spacing: 0) {
Text("프로필 신고하기") Text(I18n.User.reportProfileAction)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(.white) .foregroundColor(.white)

View File

@@ -13,16 +13,7 @@ struct UserReportDialogView: View {
let confirmAction: (String) -> Void let confirmAction: (String) -> Void
@State private var selectedIndex: Int? = nil @State private var selectedIndex: Int? = nil
let reasons = [ let reasons = I18n.Report.userReasons
"괴롭힘 및 사이버 폭력",
"개인정보 침해",
"명의 도용",
"폭력적 위협",
"아동 학대",
"보호 대상 집단에 대한 증오심 표현",
"스팸 및 사기",
"나에게 해당하는 문제 없음"
]
var body: some View { var body: some View {
ZStack { ZStack {
@@ -32,7 +23,7 @@ struct UserReportDialogView: View {
.onTapGesture { isShowing = false } .onTapGesture { isShowing = false }
VStack(spacing: 13.3) { VStack(spacing: 13.3) {
Text("사용자 신고") Text(I18n.Report.userReportTitle)
.appFont(size: 16.7, weight: .medium) .appFont(size: 16.7, weight: .medium)
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
@@ -60,14 +51,14 @@ struct UserReportDialogView: View {
HStack(spacing: 26.7) { HStack(spacing: 26.7) {
Spacer() Spacer()
Text("취소") Text(I18n.Common.cancel)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color(hex: "3bb9f1")) .foregroundColor(Color(hex: "3bb9f1"))
.onTapGesture { .onTapGesture {
isShowing = false isShowing = false
} }
Text("신고") Text(I18n.Report.reportAction)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color(hex: "3bb9f1")) .foregroundColor(Color(hex: "3bb9f1"))
.onTapGesture { .onTapGesture {

View File

@@ -16,7 +16,7 @@ struct SearchChannelView: 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: "채널 탐색") DetailNavigationBar(title: I18n.SearchChannel.title)
HStack(spacing: 0) { HStack(spacing: 0) {
Image("ic_title_search_black") Image("ic_title_search_black")
@@ -77,7 +77,7 @@ struct SearchChannelView: View {
} }
} }
} else { } else {
Text("검색 결과가 없습니다.") Text(I18n.Search.noResults)
.appFont(size: 18.3, weight: .medium) .appFont(size: 18.3, weight: .medium)
.foregroundColor(.white) .foregroundColor(.white)
.padding(.top, 20) .padding(.top, 20)

View File

@@ -17,11 +17,11 @@ struct FindPasswordView: View {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
GeometryReader { proxy in GeometryReader { proxy in
VStack(spacing: 0) { VStack(spacing: 0) {
DetailNavigationBar(title: String(localized: "비밀번호 재설정")) DetailNavigationBar(title: I18n.FindPassword.title)
ScrollView(.vertical, showsIndicators: false) { ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) { VStack(spacing: 0) {
Text("회원가입한 이메일 주소로\n임시 비밀번호를 보내드립니다.") Text(I18n.FindPassword.description1)
.appFont(size: 16, weight: .bold) .appFont(size: 16, weight: .bold)
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@@ -29,7 +29,7 @@ struct FindPasswordView: View {
.padding(.top, 40) .padding(.top, 40)
.padding(.horizontal, 26.7) .padding(.horizontal, 26.7)
Text("임시 비밀번호로 로그인 후\n마이페이지 > 프로필 설정에서\n비밀번호를 변경하고 이용하세요.") Text(I18n.FindPassword.description2)
.appFont(size: 12, weight: .medium) .appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "909090")) .foregroundColor(Color(hex: "909090"))
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@@ -37,7 +37,7 @@ struct FindPasswordView: View {
.padding(.top, 40) .padding(.top, 40)
.padding(.horizontal, 26.7) .padding(.horizontal, 26.7)
TextField("이메일을 입력하세요", text: $viewModel.email) TextField(I18n.FindPassword.emailPlaceholder, text: $viewModel.email)
.focused($isFocused) .focused($isFocused)
.autocapitalization(.none) .autocapitalization(.none)
.disableAutocorrection(true) .disableAutocorrection(true)
@@ -54,7 +54,7 @@ struct FindPasswordView: View {
isFocused = true isFocused = true
} }
Text("임시 비밀번호 받기") Text(I18n.FindPassword.submit)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.frame(maxWidth: proxy.size.width - 26.7) .frame(maxWidth: proxy.size.width - 26.7)
@@ -67,7 +67,7 @@ struct FindPasswordView: View {
HStack(spacing: 13.3) { HStack(spacing: 13.3) {
Image("ic_headphones_blue") Image("ic_headphones_blue")
Text("고객센터로 문의하기") Text(I18n.FindPassword.contactSupport)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(.button) .foregroundColor(.button)
} }

View File

@@ -22,7 +22,7 @@ final class FindPasswordViewModel: ObservableObject {
func findPassword() { func findPassword() {
if email.trimmingCharacters(in: .whitespaces).isEmpty { if email.trimmingCharacters(in: .whitespaces).isEmpty {
errorMessage = "이메일을 입력하세요." errorMessage = I18n.FindPassword.emailRequired
isShowPopup = true isShowPopup = true
return return
} }
@@ -45,7 +45,7 @@ final class FindPasswordViewModel: ObservableObject {
if decoded.success { if decoded.success {
self.email = "" self.email = ""
self.errorMessage = "임시 비밀번호가 입력하신 이메일로 발송되었습니다.\n이메일을 확인해 주세요." self.errorMessage = I18n.FindPassword.successMessage
self.isShowPopup = true self.isShowPopup = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
AppState.shared.back() AppState.shared.back()
@@ -54,13 +54,13 @@ final class FindPasswordViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }
} }

View File

@@ -29,11 +29,11 @@ struct LoginView: View {
} }
VStack(spacing: 0) { VStack(spacing: 0) {
DetailNavigationBar(title: "로그인") DetailNavigationBar(title: I18n.Login.title)
Spacer() Spacer()
TextField("이메일", text: $viewModel.email) TextField(I18n.User.emailPlaceholder, text: $viewModel.email)
.submitLabel(.next) .submitLabel(.next)
.focused($focusedField, equals: .email) .focused($focusedField, equals: .email)
.autocapitalization(.none) .autocapitalization(.none)
@@ -56,9 +56,9 @@ struct LoginView: View {
HStack { HStack {
Group { Group {
if isPasswordVisible { if isPasswordVisible {
TextField("비밀번호", text: $viewModel.password) TextField(I18n.User.passwordPlaceholder, text: $viewModel.password)
} else { } else {
SecureField("비밀번호", text: $viewModel.password) SecureField(I18n.User.passwordPlaceholder, text: $viewModel.password)
} }
} }
.submitLabel(.done) .submitLabel(.done)
@@ -92,7 +92,7 @@ struct LoginView: View {
viewModel.login() viewModel.login()
} }
) { ) {
Text("로그인") Text(I18n.Login.login)
.appFont(size: 15, weight: .bold) .appFont(size: 15, weight: .bold)
.frame(width: screenSize().width - 26.6, height: 46.7) .frame(width: screenSize().width - 26.6, height: 46.7)
.foregroundColor(.white) .foregroundColor(.white)
@@ -101,7 +101,7 @@ struct LoginView: View {
} }
.padding(.top, 40) .padding(.top, 40)
Text("비밀번호를 잊으셨나요?") Text(I18n.Login.forgotPassword)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
.padding(.vertical, 10) .padding(.vertical, 10)
@@ -111,7 +111,7 @@ struct LoginView: View {
} }
.padding(.top, 30) .padding(.top, 30)
Text("보이스온 회원이 아닌가요? 지금 가입하세요.") Text(I18n.Login.signUpPrompt)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
.padding(.vertical, 10) .padding(.vertical, 10)

View File

@@ -33,13 +33,13 @@ final class LoginViewModel: NSObject, ObservableObject {
func login() { func login() {
if email.isEmpty { if email.isEmpty {
self.errorMessage = "이메일을 입력해 주세요." self.errorMessage = I18n.User.emailRequired
self.isShowPopup = true self.isShowPopup = true
return return
} }
if password.isEmpty { if password.isEmpty {
self.errorMessage = "비밀번호를 입력해 주세요." self.errorMessage = I18n.User.passwordRequired
self.isShowPopup = true self.isShowPopup = true
return return
} }
@@ -309,13 +309,13 @@ final class LoginViewModel: NSObject, ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }
} }
@@ -365,20 +365,20 @@ final class LoginViewModel: NSObject, ObservableObject {
extension LoginViewModel: ASAuthorizationControllerDelegate { extension LoginViewModel: ASAuthorizationControllerDelegate {
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential else { guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential else {
self.errorMessage = "애플 로그인 정보를 가져오지 못했습니다." self.errorMessage = I18n.Login.appleAuthorizationFailed
self.isShowPopup = true self.isShowPopup = true
return return
} }
guard let identityTokenData = appleIDCredential.identityToken, guard let identityTokenData = appleIDCredential.identityToken,
let identityToken = String(data: identityTokenData, encoding: .utf8) else { let identityToken = String(data: identityTokenData, encoding: .utf8) else {
self.errorMessage = "애플 인증 토큰을 가져오지 못했습니다." self.errorMessage = I18n.Login.appleTokenMissing
self.isShowPopup = true self.isShowPopup = true
return return
} }
guard let nonce = currentNonce else { guard let nonce = currentNonce else {
self.errorMessage = "다시 시도해 주세요." self.errorMessage = I18n.Login.appleRetry
self.isShowPopup = true self.isShowPopup = true
return return
} }
@@ -389,7 +389,7 @@ extension LoginViewModel: ASAuthorizationControllerDelegate {
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
ERROR_LOG(error.localizedDescription) ERROR_LOG(error.localizedDescription)
self.errorMessage = "애플 로그인에 실패했습니다.\n다시 시도해 주세요." self.errorMessage = I18n.Login.appleSignInFailed
self.isShowPopup = true self.isShowPopup = true
} }
} }

View File

@@ -23,11 +23,11 @@ struct SignUpView: View {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
GeometryReader { proxy in GeometryReader { proxy in
VStack(spacing: 0) { VStack(spacing: 0) {
DetailNavigationBar(title: "회원가입") DetailNavigationBar(title: I18n.SignUp.title)
ScrollView(.vertical, showsIndicators: false) { ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) { VStack(spacing: 0) {
TextField("이메일", text: $viewModel.email) TextField(I18n.User.emailPlaceholder, text: $viewModel.email)
.submitLabel(.next) .submitLabel(.next)
.focused($focusedField, equals: .email) .focused($focusedField, equals: .email)
.autocapitalization(.none) .autocapitalization(.none)
@@ -51,9 +51,9 @@ struct SignUpView: View {
HStack { HStack {
Group { Group {
if isPasswordVisible { if isPasswordVisible {
TextField("비밀번호", text: $viewModel.password) TextField(I18n.User.passwordPlaceholder, text: $viewModel.password)
} else { } else {
SecureField("비밀번호", text: $viewModel.password) SecureField(I18n.User.passwordPlaceholder, text: $viewModel.password)
} }
} }
.submitLabel(.done) .submitLabel(.done)
@@ -96,11 +96,11 @@ struct SignUpView: View {
} }
HStack(spacing: 5) { HStack(spacing: 5) {
Text("이용약관") Text(I18n.SignUp.terms)
.appFont(size: 12, weight: .medium) .appFont(size: 12, weight: .medium)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
Text("(필수)") Text(I18n.SignUp.required)
.appFont(size: 12, weight: .medium) .appFont(size: 12, weight: .medium)
.foregroundColor(Color.button) .foregroundColor(Color.button)
} }
@@ -129,11 +129,11 @@ struct SignUpView: View {
} }
HStack(spacing: 5) { HStack(spacing: 5) {
Text("개인정보수집 및 이용동의") Text(I18n.SignUp.privacyPolicy)
.appFont(size: 12, weight: .medium) .appFont(size: 12, weight: .medium)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
Text("(필수)") Text(I18n.SignUp.required)
.appFont(size: 12, weight: .medium) .appFont(size: 12, weight: .medium)
.foregroundColor(Color.button) .foregroundColor(Color.button)
} }
@@ -152,7 +152,7 @@ struct SignUpView: View {
.padding(.top, 20) .padding(.top, 20)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
Text("회원가입") Text(I18n.SignUp.submit)
.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

@@ -67,13 +67,13 @@ final class SignUpViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }
} }
@@ -83,25 +83,25 @@ final class SignUpViewModel: ObservableObject {
private func validate() -> Bool { private func validate() -> Bool {
if email.trimmingCharacters(in: .whitespaces).isEmpty || !validateEmail() { if email.trimmingCharacters(in: .whitespaces).isEmpty || !validateEmail() {
errorMessage = "올바른 이메일을 입력하세요" errorMessage = I18n.User.emailInvalid
isShowPopup = true isShowPopup = true
return false return false
} }
if password.trimmingCharacters(in: .whitespaces).isEmpty { if password.trimmingCharacters(in: .whitespaces).isEmpty {
errorMessage = "비밀번호를 입력하세요" errorMessage = I18n.User.passwordRequired
isShowPopup = true isShowPopup = true
return false return false
} }
if !validatePassword() { if !validatePassword() {
errorMessage = "영문, 숫자 포함 8자 이상의 비밀번호를 입력해 주세요." errorMessage = I18n.ProfileUpdate.passwordRuleHint
isShowPopup = true isShowPopup = true
return false return false
} }
if !isAgreeTerms || !isAgreePrivacyPolicy { if !isAgreeTerms || !isAgreePrivacyPolicy {
errorMessage = "약관에 동의하셔야 회원가입이 가능합니다." errorMessage = I18n.SignUp.agreementRequired
isShowPopup = true isShowPopup = true
return false return false
} }

View File

@@ -59,7 +59,7 @@ struct UserTextField: View {
Image("btn_select_normal") Image("btn_select_normal")
} }
Text("비밀번호 표시") Text(I18n.User.showPassword)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
} }
@@ -74,8 +74,8 @@ struct UserTextField: View {
struct UserTextField_Previews: PreviewProvider { struct UserTextField_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
UserTextField( UserTextField(
title: "이메일", title: I18n.User.passwordTitle,
hint: "user_id@email.com", hint: I18n.User.passwordPlaceholder,
isSecure: true, isSecure: true,
variable: .constant("test"), variable: .constant("test"),
isPasswordVisibleButton: true isPasswordVisibleButton: true

View File

@@ -54,13 +54,13 @@ final class UserViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }
@@ -88,7 +88,7 @@ final class UserViewModel: ObservableObject {
let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
if decoded.success { if decoded.success {
self.errorMessage = "차단하였습니다." self.errorMessage = I18n.MemberChannel.userBlocked
self.dismissDialog = true self.dismissDialog = true
self.memberId = 0 self.memberId = 0
@@ -97,13 +97,13 @@ final class UserViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
} }
self.isShowPopup = true self.isShowPopup = true
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }
} }
@@ -129,7 +129,7 @@ final class UserViewModel: ObservableObject {
let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
if decoded.success { if decoded.success {
self.errorMessage = "차단이 해제 되었습니다." self.errorMessage = I18n.MemberChannel.userUnblocked
self.dismissDialog = true self.dismissDialog = true
self.memberId = 0 self.memberId = 0
@@ -138,20 +138,20 @@ final class UserViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
} }
self.isShowPopup = true self.isShowPopup = true
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }
} }
.store(in: &subscription) .store(in: &subscription)
} }
func report(type: ReportType, reason: String = "프로필 신고") { func report(type: ReportType, reason: String = I18n.Report.profileReportReason) {
isLoading = true isLoading = true
let request = ReportRequest(type: type, reason: reason, reportedMemberId: memberId, cheersId: nil, audioContentId: nil) let request = ReportRequest(type: type, reason: reason, reportedMemberId: memberId, cheersId: nil, audioContentId: nil)
@@ -178,12 +178,12 @@ final class UserViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }
} }

View File

@@ -416,20 +416,17 @@
- [ ] `SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterViewModel.swift` - [ ] `SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterViewModel.swift`
### Notification (2) ### Notification (2)
- [ ] `SodaLive/Sources/Notification/List/PushNotificationListItemView.swift` - [x] `SodaLive/Sources/Notification/List/PushNotificationListItemView.swift`
- [ ] `SodaLive/Sources/Notification/List/PushNotificationListView.swift` - [x] `SodaLive/Sources/Notification/List/PushNotificationListView.swift`
### Onboarding (1)
- [ ] `SodaLive/Sources/Onboarding/OnboardingView.swift`
### Report (4) ### Report (4)
- [ ] `SodaLive/Sources/Report/CheersReportDialogView.swift` - [x] `SodaLive/Sources/Report/CheersReportDialogView.swift`
- [ ] `SodaLive/Sources/Report/ProfileReportDialogView.swift` - [x] `SodaLive/Sources/Report/ProfileReportDialogView.swift`
- [ ] `SodaLive/Sources/Report/ProfileReportMenuView.swift` - [x] `SodaLive/Sources/Report/ProfileReportMenuView.swift`
- [ ] `SodaLive/Sources/Report/UserReportDialogView.swift` - [x] `SodaLive/Sources/Report/UserReportDialogView.swift`
### SearchChannel (1) ### SearchChannel (1)
- [ ] `SodaLive/Sources/SearchChannel/SearchChannelView.swift` - [x] `SodaLive/Sources/SearchChannel/SearchChannelView.swift`
### Settings (15) ### Settings (15)
- [ ] `SodaLive/Sources/Settings/Content/ContentSettingsView.swift` - [ ] `SodaLive/Sources/Settings/Content/ContentSettingsView.swift`
@@ -457,14 +454,14 @@
- [ ] `SodaLive/Sources/UI/Component/SeriesListItemView.swift` - [ ] `SodaLive/Sources/UI/Component/SeriesListItemView.swift`
### User (8) ### User (8)
- [ ] `SodaLive/Sources/User/FindPassword/FindPasswordView.swift` - [x] `SodaLive/Sources/User/FindPassword/FindPasswordView.swift`
- [ ] `SodaLive/Sources/User/FindPassword/FindPasswordViewModel.swift` - [x] `SodaLive/Sources/User/FindPassword/FindPasswordViewModel.swift`
- [ ] `SodaLive/Sources/User/Login/LoginView.swift` - [x] `SodaLive/Sources/User/Login/LoginView.swift`
- [ ] `SodaLive/Sources/User/Login/LoginViewModel.swift` - [x] `SodaLive/Sources/User/Login/LoginViewModel.swift`
- [ ] `SodaLive/Sources/User/SignUp/SignUpView.swift` - [x] `SodaLive/Sources/User/SignUp/SignUpView.swift`
- [ ] `SodaLive/Sources/User/SignUp/SignUpViewModel.swift` - [x] `SodaLive/Sources/User/SignUp/SignUpViewModel.swift`
- [ ] `SodaLive/Sources/User/UserTextField.swift` - [x] `SodaLive/Sources/User/UserTextField.swift`
- [ ] `SodaLive/Sources/User/UserViewModel.swift` - [x] `SodaLive/Sources/User/UserViewModel.swift`
## 검증 기록 ## 검증 기록
### 1차 계획 수립 (2026-03-31) ### 1차 계획 수립 (2026-03-31)
@@ -612,3 +609,19 @@
- 모듈 재검증 결과, 남은 한글 문자열은 Preview 샘플/`DEBUG_LOG`/SDK 입력값(비노출)만 존재. - 모듈 재검증 결과, 남은 한글 문자열은 Preview 샘플/`DEBUG_LOG`/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.`로 테스트 액션 미구성 확인(코드 실패 아님, 스킴 제약).
### 9차 구현 (User/SearchChannel/Report/Notification 15개 파일 i18n 전환, 2026-03-31)
- 무엇/왜/어떻게:
- 무엇: 변경 대상 목록 중 `User`, `SearchChannel`, `Report`, `Notification` 모듈의 15개 파일을 처리해 화면 문자열과 사용자 노출 에러 메시지를 `I18n.*`로 통일했다.
- 왜: 로그인/회원가입/비밀번호 재설정/채널 탐색/신고/알림 화면에 하드코딩 문구가 남아 있어 다국어 접근이 일관되지 않았기 때문이다.
- 어떻게: 관련 뷰와 뷰모델의 문자열을 교체하고, `I18n.swift``User`, `SignUp`, `FindPassword`, `SearchChannel`, `NotificationList`, `Report` 키를 보강했다.
- 실행 명령/도구:
- `rg -n 'Text\\(\"|SecureField\\(\"|TextField\\(\"|DetailNavigationBar\\(title: \\\"|String\\(localized: \\\"|errorMessage = \\\"|let reasons = \\[' SodaLive/Sources/User SodaLive/Sources/SearchChannel SodaLive/Sources/Report SodaLive/Sources/Notification -g '!**/generated/**'`
- `rg -n 'I18n\\.(User|SignUp|FindPassword|Login|SearchChannel|NotificationList|Report|Common)' SodaLive/Sources/User SodaLive/Sources/SearchChannel SodaLive/Sources/Report SodaLive/Sources/Notification -g '!**/generated/**'`
- `xcodebuild -project "SodaLive.xcodeproj" -scheme "SodaLive" -configuration Debug build`
- `HOME=/tmp/codexhome xcodebuild -project "SodaLive.xcodeproj" -scheme "SodaLive" -configuration Debug -derivedDataPath /tmp/SodaLiveDerivedData -clonedSourcePackagesDirPath /tmp/SodaLiveSPM build`
- 결과:
- `User` 8개 파일, `SearchChannel` 1개 파일, `Report` 4개 파일, `Notification` 2개 파일 체크박스를 완료 처리했다.
- `PushNotificationListItemView.swift`의 시간 구분자도 `I18n.NotificationList.timestampSeparator`로 이관했다.
- `xcodebuild`는 샌드박스 내 캐시/시뮬레이터 접근 제약과 이후 네트워크 차단으로 실패했다. 첫 시도는 workspace 인식 문제와 CoreSimulator 환경 오류가 섞여 있었고, 프로젝트 빌드로 전환한 뒤에는 Swift Package 의존성(`objectbox-swift-spm`)을 GitHub에서 가져오지 못해 중단되었다.
- 따라서 이번 턴에서는 정적 치환과 문서 동기화까지 완료했고, 실제 컴파일 성공 여부는 네트워크가 허용되는 환경에서 추가 확인이 필요하다.