featL add localization for various UI strings and error messages

This commit is contained in:
2026-03-14 17:46:00 +07:00
parent 758f5ec05f
commit 8a15572fb9
15 changed files with 1079 additions and 69 deletions
+5 -5
View File
@@ -16,7 +16,7 @@ struct LoginView: View {
.foregroundStyle(.red)
Text("Mayday")
.font(.largeTitle.bold())
Text("Мониторинг и уведомления")
Text("login_subtitle")
.font(.subheadline)
.foregroundStyle(.secondary)
}
@@ -29,7 +29,7 @@ struct LoginView: View {
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
SecureField("Пароль", text: $password)
SecureField("password", text: $password)
.textFieldStyle(.roundedBorder)
.textContentType(.password)
}
@@ -48,14 +48,14 @@ struct LoginView: View {
ProgressView()
.frame(maxWidth: .infinity)
} else {
Text("Войти")
Text("login_button")
.frame(maxWidth: .infinity)
}
}
.buttonStyle(.borderedProminent)
.disabled(email.isEmpty || password.isEmpty || authViewModel.isLoading)
Button("Нет аккаунта? Зарегистрироваться") {
Button("login_no_account") {
showRegister = true
}
.font(.footnote)
@@ -64,7 +64,7 @@ struct LoginView: View {
Button {
Task { await authViewModel.enterPreviewMode() }
} label: {
Label("Демо-режим", systemImage: "play.circle.fill")
Label("demo_mode", systemImage: "play.circle.fill")
.font(.footnote)
.foregroundStyle(.secondary)
}
+8 -8
View File
@@ -14,7 +14,7 @@ struct RegisterView: View {
VStack(spacing: 24) {
Spacer()
Text("Регистрация")
Text("register_title")
.font(.largeTitle.bold())
VStack(spacing: 16) {
@@ -25,23 +25,23 @@ struct RegisterView: View {
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
SecureField("Пароль", text: $password)
SecureField("password", text: $password)
.textFieldStyle(.roundedBorder)
.textContentType(.newPassword)
SecureField("Подтвердите пароль", text: $confirmPassword)
SecureField("confirm_password", text: $confirmPassword)
.textFieldStyle(.roundedBorder)
.textContentType(.newPassword)
}
if password.count > 0 && password.count < 8 {
Text("Пароль должен содержать не менее 8 символов")
Text("password_min_length")
.foregroundStyle(.red)
.font(.footnote)
}
if confirmPassword.count > 0 && password != confirmPassword {
Text("Пароли не совпадают")
Text("passwords_mismatch")
.foregroundStyle(.red)
.font(.footnote)
}
@@ -65,13 +65,13 @@ struct RegisterView: View {
if authViewModel.isLoading {
ProgressView().frame(maxWidth: .infinity)
} else {
Text("Создать аккаунт").frame(maxWidth: .infinity)
Text("register_button").frame(maxWidth: .infinity)
}
}
.buttonStyle(.borderedProminent)
.disabled(!isFormValid || authViewModel.isLoading)
Button("Уже есть аккаунт?") { dismiss() }
Button("register_has_account") { dismiss() }
.font(.footnote)
Spacer()
@@ -80,7 +80,7 @@ struct RegisterView: View {
.navigationDestination(isPresented: $showVerify) {
VerifyEmailView(email: registeredEmail)
}
.navigationTitle("Регистрация")
.navigationTitle("register_title")
.navigationBarTitleDisplayMode(.inline)
}
+5 -5
View File
@@ -14,9 +14,9 @@ struct VerifyEmailView: View {
Spacer()
VStack(spacing: 8) {
Text("Подтвердите email")
Text("verify_email_title")
.font(.largeTitle.bold())
Text("Код отправлен на")
Text("verify_code_sent_to")
.foregroundStyle(.secondary)
Text(email)
.fontWeight(.semibold)
@@ -51,9 +51,9 @@ struct VerifyEmailView: View {
Task { await resendCode() }
} label: {
if resendCooldown > 0 {
Text("Отправить повторно (\(resendCooldown) сек)")
Text("verify_resend_cooldown \(resendCooldown)")
} else {
Text("Отправить повторно")
Text("verify_resend")
}
}
.disabled(resendCooldown > 0)
@@ -61,7 +61,7 @@ struct VerifyEmailView: View {
Spacer()
}
.padding()
.navigationTitle("Подтверждение")
.navigationTitle("verify_nav_title")
.navigationBarTitleDisplayMode(.inline)
.onAppear { focusedIndex = 0 }
.onDisappear { cooldownTask?.cancel() }
@@ -29,7 +29,7 @@ struct NotificationDetailView: View {
Button {
Task { await viewModel.markAsRead(notification) }
} label: {
Text("Отметить прочитанным")
Text("mark_as_read")
.font(.headline)
.foregroundStyle(.red)
.frame(maxWidth: .infinity)
@@ -45,7 +45,7 @@ struct NotificationDetailView: View {
}
}
.background(Color(.systemGroupedBackground))
.navigationTitle("Подробности")
.navigationTitle("details_section")
.navigationBarTitleDisplayMode(.inline)
.task {
await viewModel.markAsRead(notification)
@@ -89,8 +89,8 @@ struct NotificationDetailView: View {
private var statusBadge: some View {
let (text, color): (String, Color) = notification.isRead
? ("Прочитано", .green)
: ("Новое", .red)
? (String(localized: "status_read"), .green)
: (String(localized: "status_new"), .red)
return Text(text)
.font(.caption.bold())
.foregroundStyle(color)
@@ -104,7 +104,7 @@ struct NotificationDetailView: View {
private var detailsCard: some View {
VStack(alignment: .leading, spacing: 12) {
Label("Подробности", systemImage: "doc.text.fill")
Label("details_section", systemImage: "doc.text.fill")
.font(.subheadline.bold())
.foregroundStyle(.primary)
@@ -124,7 +124,7 @@ struct NotificationDetailView: View {
private func metadataCard(_ metadata: [String: String]) -> some View {
VStack(alignment: .leading, spacing: 12) {
Label("Информация", systemImage: "info.circle.fill")
Label("info_section", systemImage: "info.circle.fill")
.font(.subheadline.bold())
.foregroundStyle(.primary)
@@ -170,17 +170,17 @@ struct NotificationDetailView: View {
private var statusCard: some View {
VStack(alignment: .leading, spacing: 12) {
Label("Статус", systemImage: "clock.fill")
Label("status_section", systemImage: "clock.fill")
.font(.subheadline.bold())
.foregroundStyle(.primary)
VStack(spacing: 8) {
infoRow(icon: "paperplane.fill", label: "Канал", value: channelLabel)
infoRow(icon: "paperplane.fill", label: String(localized: "channel_label"), value: channelLabel)
Divider()
infoRow(icon: "clock", label: "Получено", value: notification.createdAt.formatted(date: .abbreviated, time: .shortened))
infoRow(icon: "clock", label: String(localized: "received_label"), value: notification.createdAt.formatted(date: .abbreviated, time: .shortened))
if let readAt = notification.readAt {
Divider()
infoRow(icon: "checkmark.circle.fill", label: "Прочитано", value: readAt.formatted(date: .abbreviated, time: .shortened))
infoRow(icon: "checkmark.circle.fill", label: String(localized: "read_at_label"), value: readAt.formatted(date: .abbreviated, time: .shortened))
}
}
}
@@ -211,7 +211,7 @@ struct NotificationDetailView: View {
private var channelLabel: String {
switch notification.channel {
case .inApp: return "В приложении"
case .inApp: return String(localized: "channel_in_app")
case .push: return "Push"
case .email: return "Email"
}
@@ -20,27 +20,27 @@ struct NotificationsView: View {
ProgressView()
} else if let error = viewModel.error, viewModel.notifications.isEmpty {
ContentUnavailableView(
"Ошибка загрузки",
"loading_error",
systemImage: "exclamationmark.triangle",
description: Text(error)
)
} else if viewModel.notifications.isEmpty {
ContentUnavailableView(
"Нет уведомлений",
"no_notifications",
systemImage: "bell.slash",
description: Text("Новые уведомления появятся здесь")
description: Text("no_notifications_description")
)
} else {
notificationsList
}
}
.background(Color(.systemGroupedBackground))
.navigationTitle("Уведомления")
.navigationTitle("notifications_title")
.toolbar {
#if DEBUG
if PreviewData.isPreviewMode {
ToolbarItem(placement: .topBarLeading) {
Text("ДЕМО")
Text("demo_badge")
.font(.caption2.bold())
.foregroundStyle(.white)
.padding(.horizontal, 8)
@@ -80,7 +80,7 @@ struct NotificationsView: View {
ScrollView {
LazyVStack(spacing: 0) {
if !unreadNotifications.isEmpty {
sectionHeader("Активные")
sectionHeader(String(localized: "notifications_active"))
ForEach(unreadNotifications) { notification in
NavigationLink(destination: NotificationDetailView(notification: notification, viewModel: viewModel)) {
ActiveNotificationCard(notification: notification)
@@ -97,7 +97,7 @@ struct NotificationsView: View {
}
if !readNotifications.isEmpty {
sectionHeader("Завершённые")
sectionHeader(String(localized: "notifications_completed"))
ForEach(readNotifications) { notification in
NavigationLink(destination: NotificationDetailView(notification: notification, viewModel: viewModel)) {
ResolvedNotificationCard(notification: notification)
@@ -172,7 +172,7 @@ struct ActiveNotificationCard: View {
HStack {
Spacer()
Text("Открыть")
Text("open_button")
.font(.subheadline.bold())
.foregroundStyle(Color.red)
.padding(.horizontal, 32)
@@ -238,7 +238,7 @@ struct ResolvedNotificationCard: View {
Image(systemName: "checkmark")
.font(.caption2)
.foregroundStyle(.green)
Text("прочитано \(readAt.formatted(date: .abbreviated, time: .shortened))")
Text("notification_read_at \(readAt.formatted(date: .abbreviated, time: .shortened))")
.font(.caption)
.foregroundStyle(.secondary)
}
@@ -12,11 +12,11 @@ struct ChangePasswordView: View {
NavigationStack {
Form {
Section {
SecureField("Текущий пароль", text: $currentPassword)
SecureField("current_password", text: $currentPassword)
.textContentType(.password)
SecureField("Новый пароль", text: $newPassword)
SecureField("new_password", text: $newPassword)
.textContentType(.newPassword)
SecureField("Подтвердите новый пароль", text: $confirmPassword)
SecureField("confirm_new_password", text: $confirmPassword)
.textContentType(.newPassword)
}
@@ -33,7 +33,7 @@ struct ChangePasswordView: View {
}
Section {
Button("Сохранить") {
Button("save_button") {
Task {
let success = await viewModel.changePassword(current: currentPassword, new: newPassword)
if success { dismiss() }
@@ -43,11 +43,11 @@ struct ChangePasswordView: View {
.frame(maxWidth: .infinity)
}
}
.navigationTitle("Сменить пароль")
.navigationTitle("change_password_title")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Отмена") { dismiss() }
Button("cancel") { dismiss() }
}
}
}
+5 -5
View File
@@ -14,7 +14,7 @@ struct SessionsView: View {
.font(.body)
.lineLimit(1)
if session.isCurrent {
Text("Текущая")
Text("current_session")
.font(.caption)
.padding(.horizontal, 6)
.padding(.vertical, 2)
@@ -26,7 +26,7 @@ struct SessionsView: View {
Text(session.ipAddress)
.font(.caption)
.foregroundStyle(.secondary)
Text("Создана: \(session.createdAt.formatted(date: .abbreviated, time: .shortened))")
Text("session_created \(session.createdAt.formatted(date: .abbreviated, time: .shortened))")
.font(.caption2)
.foregroundStyle(.secondary)
}
@@ -35,17 +35,17 @@ struct SessionsView: View {
Button(role: .destructive) {
Task { await viewModel.deleteSession(session) }
} label: {
Label("Удалить", systemImage: "trash")
Label("delete_button", systemImage: "trash")
}
}
}
}
}
.navigationTitle("Активные сессии")
.navigationTitle("active_sessions_title")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Готово") { dismiss() }
Button("done_button") { dismiss() }
}
}
}
+12 -12
View File
@@ -12,14 +12,14 @@ struct SettingsView: View {
var body: some View {
NavigationStack {
Form {
Section("Аккаунт") {
Section("account_section") {
if let user = authViewModel.currentUser {
LabeledContent("Email", value: user.email)
}
}
Section {
Button("Сменить пароль") {
Button("change_password") {
showChangePassword = true
}
@@ -28,7 +28,7 @@ struct SettingsView: View {
UIApplication.shared.open(url)
}
} label: {
Label("Push-уведомления", systemImage: "bell.badge")
Label("push_notifications", systemImage: "bell.badge")
.foregroundStyle(.primary)
}
}
@@ -38,7 +38,7 @@ struct SettingsView: View {
showSessions = true
} label: {
HStack {
Text("Активные сессии")
Text("active_sessions")
Spacer()
if !viewModel.sessions.isEmpty {
Text("(\(viewModel.sessions.count))")
@@ -52,20 +52,20 @@ struct SettingsView: View {
}
Section {
Button("Выйти из аккаунта", role: .destructive) {
Button("logout_button", role: .destructive) {
Task { await authViewModel.logout() }
}
Button("Выйти на всех устройствах", role: .destructive) {
Button("logout_all_button", role: .destructive) {
showLogoutAllConfirm = true
}
}
}
.navigationTitle("Настройки")
.navigationTitle("settings_title")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Готово") { dismiss() }
Button("done_button") { dismiss() }
}
}
.sheet(isPresented: $showChangePassword) {
@@ -76,11 +76,11 @@ struct SettingsView: View {
.environmentObject(viewModel)
}
.confirmationDialog(
"Выйти на всех устройствах?",
"logout_all_confirm",
isPresented: $showLogoutAllConfirm,
titleVisibility: .visible
) {
Button("Выйти везде", role: .destructive) {
Button("logout_all_action", role: .destructive) {
Task {
do {
_ = try await NotificationsAPIService.shared.logoutAll()
@@ -90,10 +90,10 @@ struct SettingsView: View {
}
}
}
Button("Отмена", role: .cancel) {}
Button("cancel", role: .cancel) {}
}
.alert(
"Ошибка",
"error_title",
isPresented: Binding(
get: { logoutAllError != nil },
set: { if !$0 { logoutAllError = nil } }