diff --git a/Mayday.xcodeproj/project.pbxproj b/Mayday.xcodeproj/project.pbxproj index d69fe5d..71139e8 100644 --- a/Mayday.xcodeproj/project.pbxproj +++ b/Mayday.xcodeproj/project.pbxproj @@ -33,6 +33,8 @@ AA000001000024 /* SessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000002000024 /* SessionsView.swift */; }; AA000001000025 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA000002000025 /* Assets.xcassets */; }; AA000001000026 /* PreviewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000002000026A /* PreviewData.swift */; }; + AA000001000027 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = AA000002000027 /* Localizable.xcstrings */; }; + AA000001000028 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = AA000002000028 /* InfoPlist.xcstrings */; }; AA000001000030 /* MaydayLiveActivityBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000002000030 /* MaydayLiveActivityBundle.swift */; }; AA000001000031 /* MaydayLiveActivityLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000002000031 /* MaydayLiveActivityLiveActivity.swift */; }; AA000001000032 /* AlertAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000002000008 /* AlertAttributes.swift */; }; @@ -91,6 +93,8 @@ AA000002000025 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; AA000002000026 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AA000002000026A /* PreviewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewData.swift; sourceTree = ""; }; + AA000002000027 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; + AA000002000028 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; AA000002000030 /* MaydayLiveActivityBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaydayLiveActivityBundle.swift; sourceTree = ""; }; AA000002000031 /* MaydayLiveActivityLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaydayLiveActivityLiveActivity.swift; sourceTree = ""; }; AA000002000033 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -133,6 +137,8 @@ AA000002000003 /* AppDelegate.swift */, AA000002000025 /* Assets.xcassets */, AA000002000026 /* Info.plist */, + AA000002000027 /* Localizable.xcstrings */, + AA000002000028 /* InfoPlist.xcstrings */, AA000011000003 /* Models */, AA000011000004 /* Services */, AA000011000005 /* ViewModels */, @@ -317,6 +323,8 @@ buildActionMask = 2147483647; files = ( AA000001000025 /* Assets.xcassets in Resources */, + AA000001000027 /* Localizable.xcstrings in Resources */, + AA000001000028 /* InfoPlist.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Mayday/Info.plist b/Mayday/Info.plist index edda67b..a56f332 100644 --- a/Mayday/Info.plist +++ b/Mayday/Info.plist @@ -52,7 +52,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - NSUserNotificationsUsageDescription - Mayday использует уведомления для оповещения о критических событиях. diff --git a/Mayday/InfoPlist.xcstrings b/Mayday/InfoPlist.xcstrings new file mode 100644 index 0000000..60e994a --- /dev/null +++ b/Mayday/InfoPlist.xcstrings @@ -0,0 +1,22 @@ +{ + "sourceLanguage" : "ru", + "strings" : { + "NSUserNotificationsUsageDescription" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mayday использует уведомления для оповещения о критических событиях." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mayday uses notifications to alert you about critical events." + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/Mayday/Localizable.xcstrings b/Mayday/Localizable.xcstrings new file mode 100644 index 0000000..e9aa136 --- /dev/null +++ b/Mayday/Localizable.xcstrings @@ -0,0 +1,982 @@ +{ + "sourceLanguage" : "ru", + "strings" : { + "login_subtitle" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мониторинг и уведомления" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Monitoring and notifications" + } + } + } + }, + "password" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пароль" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Password" + } + } + } + }, + "login_button" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Войти" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign in" + } + } + } + }, + "login_no_account" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нет аккаунта? Зарегистрироваться" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No account? Sign up" + } + } + } + }, + "demo_mode" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Демо-режим" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Demo mode" + } + } + } + }, + "register_title" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Регистрация" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registration" + } + } + } + }, + "confirm_password" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подтвердите пароль" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirm password" + } + } + } + }, + "password_min_length" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пароль должен содержать не менее 8 символов" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Password must be at least 8 characters" + } + } + } + }, + "passwords_mismatch" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пароли не совпадают" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwords don't match" + } + } + } + }, + "register_button" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Создать аккаунт" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Create account" + } + } + } + }, + "register_has_account" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Уже есть аккаунт?" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Already have an account?" + } + } + } + }, + "verify_email_title" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подтвердите email" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verify email" + } + } + } + }, + "verify_code_sent_to" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Код отправлен на" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Code sent to" + } + } + } + }, + "verify_resend_cooldown %lld" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправить повторно (%lld сек)" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Resend (%lld sec)" + } + } + } + }, + "verify_resend" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправить повторно" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Resend" + } + } + } + }, + "verify_nav_title" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подтверждение" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verification" + } + } + } + }, + "loading_error" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ошибка загрузки" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loading error" + } + } + } + }, + "no_notifications" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нет уведомлений" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No notifications" + } + } + } + }, + "no_notifications_description" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Новые уведомления появятся здесь" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "New notifications will appear here" + } + } + } + }, + "notifications_title" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Уведомления" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notifications" + } + } + } + }, + "demo_badge" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "ДЕМО" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "DEMO" + } + } + } + }, + "notifications_active" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Активные" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Active" + } + } + } + }, + "notifications_completed" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Завершённые" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Completed" + } + } + } + }, + "open_button" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Открыть" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open" + } + } + } + }, + "mark_as_read" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отметить прочитанным" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mark as read" + } + } + } + }, + "details_section" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подробности" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Details" + } + } + } + }, + "info_section" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Информация" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Information" + } + } + } + }, + "status_section" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Статус" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Status" + } + } + } + }, + "status_read" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прочитано" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Read" + } + } + } + }, + "status_new" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Новое" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "New" + } + } + } + }, + "channel_in_app" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "В приложении" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "In-app" + } + } + } + }, + "channel_label" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Канал" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Channel" + } + } + } + }, + "received_label" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Получено" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Received" + } + } + } + }, + "read_at_label" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прочитано" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Read" + } + } + } + }, + "notification_read_at %@" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "прочитано %@" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "read %@" + } + } + } + }, + "account_section" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Аккаунт" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Account" + } + } + } + }, + "change_password" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сменить пароль" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change password" + } + } + } + }, + "push_notifications" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Push-уведомления" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Push notifications" + } + } + } + }, + "active_sessions" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Активные сессии" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Active sessions" + } + } + } + }, + "logout_button" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выйти из аккаунта" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign out" + } + } + } + }, + "logout_all_button" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выйти на всех устройствах" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign out on all devices" + } + } + } + }, + "settings_title" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Настройки" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Settings" + } + } + } + }, + "done_button" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Готово" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Done" + } + } + } + }, + "logout_all_confirm" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выйти на всех устройствах?" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign out on all devices?" + } + } + } + }, + "logout_all_action" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выйти везде" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign out everywhere" + } + } + } + }, + "cancel" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отмена" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel" + } + } + } + }, + "error_title" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ошибка" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error" + } + } + } + }, + "current_password" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Текущий пароль" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Current password" + } + } + } + }, + "new_password" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Новый пароль" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "New password" + } + } + } + }, + "confirm_new_password" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подтвердите новый пароль" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirm new password" + } + } + } + }, + "save_button" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сохранить" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Save" + } + } + } + }, + "change_password_title" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сменить пароль" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change password" + } + } + } + }, + "current_session" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Текущая" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Current" + } + } + } + }, + "session_created %@" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Создана: %@" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Created: %@" + } + } + } + }, + "delete_button" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удалить" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete" + } + } + } + }, + "active_sessions_title" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Активные сессии" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Active sessions" + } + } + } + }, + "password_changed_success" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пароль успешно изменён" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Password changed successfully" + } + } + } + }, + "error_invalid_url" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Неверный URL" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invalid URL" + } + } + } + }, + "error_invalid_credentials" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Неверный email или пароль" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invalid email or password" + } + } + } + }, + "alert_status_active" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "активен" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "active" + } + } + } + }, + "alert_status_resolved" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "завершён" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "resolved" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/Mayday/Services/HTTPClient.swift b/Mayday/Services/HTTPClient.swift index d31d70f..6739e5f 100644 --- a/Mayday/Services/HTTPClient.swift +++ b/Mayday/Services/HTTPClient.swift @@ -10,8 +10,8 @@ enum APIError: Error, LocalizedError { var errorDescription: String? { switch self { - case .invalidURL: return "Invalid URL" - case .unauthorized: return "Неверный email или пароль" + case .invalidURL: return String(localized: "error_invalid_url") + case .unauthorized: return String(localized: "error_invalid_credentials") case .validationError(let errors): return errors.values.flatMap { $0 }.joined(separator: ", ") case .serverError(let message): return message diff --git a/Mayday/ViewModels/SettingsViewModel.swift b/Mayday/ViewModels/SettingsViewModel.swift index 273aef3..d604382 100644 --- a/Mayday/ViewModels/SettingsViewModel.swift +++ b/Mayday/ViewModels/SettingsViewModel.swift @@ -44,7 +44,7 @@ class SettingsViewModel: ObservableObject { func changePassword(current: String, new: String) async -> Bool { #if DEBUG if PreviewData.isPreviewMode { - successMessage = "Пароль успешно изменён" + successMessage = String(localized: "password_changed_success") return true } #endif @@ -53,7 +53,7 @@ class SettingsViewModel: ObservableObject { defer { isLoading = false } do { _ = try await service.changePassword(current: current, new: new) - successMessage = "Пароль успешно изменён" + successMessage = String(localized: "password_changed_success") return true } catch { self.error = error.localizedDescription diff --git a/Mayday/Views/Auth/LoginView.swift b/Mayday/Views/Auth/LoginView.swift index 926b125..c8ca19a 100644 --- a/Mayday/Views/Auth/LoginView.swift +++ b/Mayday/Views/Auth/LoginView.swift @@ -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) } diff --git a/Mayday/Views/Auth/RegisterView.swift b/Mayday/Views/Auth/RegisterView.swift index f2ad233..e4dffa0 100644 --- a/Mayday/Views/Auth/RegisterView.swift +++ b/Mayday/Views/Auth/RegisterView.swift @@ -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) } diff --git a/Mayday/Views/Auth/VerifyEmailView.swift b/Mayday/Views/Auth/VerifyEmailView.swift index 9056925..c3636cd 100644 --- a/Mayday/Views/Auth/VerifyEmailView.swift +++ b/Mayday/Views/Auth/VerifyEmailView.swift @@ -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() } diff --git a/Mayday/Views/Notifications/NotificationDetailView.swift b/Mayday/Views/Notifications/NotificationDetailView.swift index b3b3ece..7bf1274 100644 --- a/Mayday/Views/Notifications/NotificationDetailView.swift +++ b/Mayday/Views/Notifications/NotificationDetailView.swift @@ -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" } diff --git a/Mayday/Views/Notifications/NotificationsView.swift b/Mayday/Views/Notifications/NotificationsView.swift index 7fc927f..b298201 100644 --- a/Mayday/Views/Notifications/NotificationsView.swift +++ b/Mayday/Views/Notifications/NotificationsView.swift @@ -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) } diff --git a/Mayday/Views/Settings/ChangePasswordView.swift b/Mayday/Views/Settings/ChangePasswordView.swift index 8a91345..7cfd4b9 100644 --- a/Mayday/Views/Settings/ChangePasswordView.swift +++ b/Mayday/Views/Settings/ChangePasswordView.swift @@ -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() } } } } diff --git a/Mayday/Views/Settings/SessionsView.swift b/Mayday/Views/Settings/SessionsView.swift index 68fdd3c..ece179b 100644 --- a/Mayday/Views/Settings/SessionsView.swift +++ b/Mayday/Views/Settings/SessionsView.swift @@ -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() } } } } diff --git a/Mayday/Views/Settings/SettingsView.swift b/Mayday/Views/Settings/SettingsView.swift index eec09cc..0237473 100644 --- a/Mayday/Views/Settings/SettingsView.swift +++ b/Mayday/Views/Settings/SettingsView.swift @@ -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 } } diff --git a/MaydayLiveActivity/MaydayLiveActivityLiveActivity.swift b/MaydayLiveActivity/MaydayLiveActivityLiveActivity.swift index 39d4164..1f2138a 100644 --- a/MaydayLiveActivity/MaydayLiveActivityLiveActivity.swift +++ b/MaydayLiveActivity/MaydayLiveActivityLiveActivity.swift @@ -123,8 +123,8 @@ struct MaydayLiveActivityLiveActivity: Widget { @ViewBuilder func statusBadge(_ status: AlertStatus) -> some View { let (text, color): (String, Color) = status == .active - ? ("активен", .red) - : ("завершён", .green) + ? (String(localized: "alert_status_active"), .red) + : (String(localized: "alert_status_resolved"), .green) Text(text) .font(.caption2.bold()) .textCase(.uppercase)