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
+8
View File
@@ -33,6 +33,8 @@
AA000001000024 /* SessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000002000024 /* SessionsView.swift */; }; AA000001000024 /* SessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000002000024 /* SessionsView.swift */; };
AA000001000025 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA000002000025 /* Assets.xcassets */; }; AA000001000025 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA000002000025 /* Assets.xcassets */; };
AA000001000026 /* PreviewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000002000026A /* PreviewData.swift */; }; 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 */; }; AA000001000030 /* MaydayLiveActivityBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000002000030 /* MaydayLiveActivityBundle.swift */; };
AA000001000031 /* MaydayLiveActivityLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000002000031 /* MaydayLiveActivityLiveActivity.swift */; }; AA000001000031 /* MaydayLiveActivityLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000002000031 /* MaydayLiveActivityLiveActivity.swift */; };
AA000001000032 /* AlertAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000002000008 /* AlertAttributes.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 = "<group>"; }; AA000002000025 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
AA000002000026 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; AA000002000026 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
AA000002000026A /* PreviewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewData.swift; sourceTree = "<group>"; }; AA000002000026A /* PreviewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewData.swift; sourceTree = "<group>"; };
AA000002000027 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
AA000002000028 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
AA000002000030 /* MaydayLiveActivityBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaydayLiveActivityBundle.swift; sourceTree = "<group>"; }; AA000002000030 /* MaydayLiveActivityBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaydayLiveActivityBundle.swift; sourceTree = "<group>"; };
AA000002000031 /* MaydayLiveActivityLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaydayLiveActivityLiveActivity.swift; sourceTree = "<group>"; }; AA000002000031 /* MaydayLiveActivityLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaydayLiveActivityLiveActivity.swift; sourceTree = "<group>"; };
AA000002000033 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; AA000002000033 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -133,6 +137,8 @@
AA000002000003 /* AppDelegate.swift */, AA000002000003 /* AppDelegate.swift */,
AA000002000025 /* Assets.xcassets */, AA000002000025 /* Assets.xcassets */,
AA000002000026 /* Info.plist */, AA000002000026 /* Info.plist */,
AA000002000027 /* Localizable.xcstrings */,
AA000002000028 /* InfoPlist.xcstrings */,
AA000011000003 /* Models */, AA000011000003 /* Models */,
AA000011000004 /* Services */, AA000011000004 /* Services */,
AA000011000005 /* ViewModels */, AA000011000005 /* ViewModels */,
@@ -317,6 +323,8 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
AA000001000025 /* Assets.xcassets in Resources */, AA000001000025 /* Assets.xcassets in Resources */,
AA000001000027 /* Localizable.xcstrings in Resources */,
AA000001000028 /* InfoPlist.xcstrings in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
-2
View File
@@ -52,7 +52,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>NSUserNotificationsUsageDescription</key>
<string>Mayday использует уведомления для оповещения о критических событиях.</string>
</dict> </dict>
</plist> </plist>
+22
View File
@@ -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"
}
+982
View File
@@ -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"
}
+2 -2
View File
@@ -10,8 +10,8 @@ enum APIError: Error, LocalizedError {
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case .invalidURL: return "Invalid URL" case .invalidURL: return String(localized: "error_invalid_url")
case .unauthorized: return "Неверный email или пароль" case .unauthorized: return String(localized: "error_invalid_credentials")
case .validationError(let errors): case .validationError(let errors):
return errors.values.flatMap { $0 }.joined(separator: ", ") return errors.values.flatMap { $0 }.joined(separator: ", ")
case .serverError(let message): return message case .serverError(let message): return message
+2 -2
View File
@@ -44,7 +44,7 @@ class SettingsViewModel: ObservableObject {
func changePassword(current: String, new: String) async -> Bool { func changePassword(current: String, new: String) async -> Bool {
#if DEBUG #if DEBUG
if PreviewData.isPreviewMode { if PreviewData.isPreviewMode {
successMessage = "Пароль успешно изменён" successMessage = String(localized: "password_changed_success")
return true return true
} }
#endif #endif
@@ -53,7 +53,7 @@ class SettingsViewModel: ObservableObject {
defer { isLoading = false } defer { isLoading = false }
do { do {
_ = try await service.changePassword(current: current, new: new) _ = try await service.changePassword(current: current, new: new)
successMessage = "Пароль успешно изменён" successMessage = String(localized: "password_changed_success")
return true return true
} catch { } catch {
self.error = error.localizedDescription self.error = error.localizedDescription
+5 -5
View File
@@ -16,7 +16,7 @@ struct LoginView: View {
.foregroundStyle(.red) .foregroundStyle(.red)
Text("Mayday") Text("Mayday")
.font(.largeTitle.bold()) .font(.largeTitle.bold())
Text("Мониторинг и уведомления") Text("login_subtitle")
.font(.subheadline) .font(.subheadline)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
@@ -29,7 +29,7 @@ struct LoginView: View {
.autocorrectionDisabled() .autocorrectionDisabled()
.textInputAutocapitalization(.never) .textInputAutocapitalization(.never)
SecureField("Пароль", text: $password) SecureField("password", text: $password)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.textContentType(.password) .textContentType(.password)
} }
@@ -48,14 +48,14 @@ struct LoginView: View {
ProgressView() ProgressView()
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} else { } else {
Text("Войти") Text("login_button")
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.disabled(email.isEmpty || password.isEmpty || authViewModel.isLoading) .disabled(email.isEmpty || password.isEmpty || authViewModel.isLoading)
Button("Нет аккаунта? Зарегистрироваться") { Button("login_no_account") {
showRegister = true showRegister = true
} }
.font(.footnote) .font(.footnote)
@@ -64,7 +64,7 @@ struct LoginView: View {
Button { Button {
Task { await authViewModel.enterPreviewMode() } Task { await authViewModel.enterPreviewMode() }
} label: { } label: {
Label("Демо-режим", systemImage: "play.circle.fill") Label("demo_mode", systemImage: "play.circle.fill")
.font(.footnote) .font(.footnote)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
+8 -8
View File
@@ -14,7 +14,7 @@ struct RegisterView: View {
VStack(spacing: 24) { VStack(spacing: 24) {
Spacer() Spacer()
Text("Регистрация") Text("register_title")
.font(.largeTitle.bold()) .font(.largeTitle.bold())
VStack(spacing: 16) { VStack(spacing: 16) {
@@ -25,23 +25,23 @@ struct RegisterView: View {
.autocorrectionDisabled() .autocorrectionDisabled()
.textInputAutocapitalization(.never) .textInputAutocapitalization(.never)
SecureField("Пароль", text: $password) SecureField("password", text: $password)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.textContentType(.newPassword) .textContentType(.newPassword)
SecureField("Подтвердите пароль", text: $confirmPassword) SecureField("confirm_password", text: $confirmPassword)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.textContentType(.newPassword) .textContentType(.newPassword)
} }
if password.count > 0 && password.count < 8 { if password.count > 0 && password.count < 8 {
Text("Пароль должен содержать не менее 8 символов") Text("password_min_length")
.foregroundStyle(.red) .foregroundStyle(.red)
.font(.footnote) .font(.footnote)
} }
if confirmPassword.count > 0 && password != confirmPassword { if confirmPassword.count > 0 && password != confirmPassword {
Text("Пароли не совпадают") Text("passwords_mismatch")
.foregroundStyle(.red) .foregroundStyle(.red)
.font(.footnote) .font(.footnote)
} }
@@ -65,13 +65,13 @@ struct RegisterView: View {
if authViewModel.isLoading { if authViewModel.isLoading {
ProgressView().frame(maxWidth: .infinity) ProgressView().frame(maxWidth: .infinity)
} else { } else {
Text("Создать аккаунт").frame(maxWidth: .infinity) Text("register_button").frame(maxWidth: .infinity)
} }
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.disabled(!isFormValid || authViewModel.isLoading) .disabled(!isFormValid || authViewModel.isLoading)
Button("Уже есть аккаунт?") { dismiss() } Button("register_has_account") { dismiss() }
.font(.footnote) .font(.footnote)
Spacer() Spacer()
@@ -80,7 +80,7 @@ struct RegisterView: View {
.navigationDestination(isPresented: $showVerify) { .navigationDestination(isPresented: $showVerify) {
VerifyEmailView(email: registeredEmail) VerifyEmailView(email: registeredEmail)
} }
.navigationTitle("Регистрация") .navigationTitle("register_title")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
+5 -5
View File
@@ -14,9 +14,9 @@ struct VerifyEmailView: View {
Spacer() Spacer()
VStack(spacing: 8) { VStack(spacing: 8) {
Text("Подтвердите email") Text("verify_email_title")
.font(.largeTitle.bold()) .font(.largeTitle.bold())
Text("Код отправлен на") Text("verify_code_sent_to")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Text(email) Text(email)
.fontWeight(.semibold) .fontWeight(.semibold)
@@ -51,9 +51,9 @@ struct VerifyEmailView: View {
Task { await resendCode() } Task { await resendCode() }
} label: { } label: {
if resendCooldown > 0 { if resendCooldown > 0 {
Text("Отправить повторно (\(resendCooldown) сек)") Text("verify_resend_cooldown \(resendCooldown)")
} else { } else {
Text("Отправить повторно") Text("verify_resend")
} }
} }
.disabled(resendCooldown > 0) .disabled(resendCooldown > 0)
@@ -61,7 +61,7 @@ struct VerifyEmailView: View {
Spacer() Spacer()
} }
.padding() .padding()
.navigationTitle("Подтверждение") .navigationTitle("verify_nav_title")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.onAppear { focusedIndex = 0 } .onAppear { focusedIndex = 0 }
.onDisappear { cooldownTask?.cancel() } .onDisappear { cooldownTask?.cancel() }
@@ -29,7 +29,7 @@ struct NotificationDetailView: View {
Button { Button {
Task { await viewModel.markAsRead(notification) } Task { await viewModel.markAsRead(notification) }
} label: { } label: {
Text("Отметить прочитанным") Text("mark_as_read")
.font(.headline) .font(.headline)
.foregroundStyle(.red) .foregroundStyle(.red)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@@ -45,7 +45,7 @@ struct NotificationDetailView: View {
} }
} }
.background(Color(.systemGroupedBackground)) .background(Color(.systemGroupedBackground))
.navigationTitle("Подробности") .navigationTitle("details_section")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.task { .task {
await viewModel.markAsRead(notification) await viewModel.markAsRead(notification)
@@ -89,8 +89,8 @@ struct NotificationDetailView: View {
private var statusBadge: some View { private var statusBadge: some View {
let (text, color): (String, Color) = notification.isRead let (text, color): (String, Color) = notification.isRead
? ("Прочитано", .green) ? (String(localized: "status_read"), .green)
: ("Новое", .red) : (String(localized: "status_new"), .red)
return Text(text) return Text(text)
.font(.caption.bold()) .font(.caption.bold())
.foregroundStyle(color) .foregroundStyle(color)
@@ -104,7 +104,7 @@ struct NotificationDetailView: View {
private var detailsCard: some View { private var detailsCard: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
Label("Подробности", systemImage: "doc.text.fill") Label("details_section", systemImage: "doc.text.fill")
.font(.subheadline.bold()) .font(.subheadline.bold())
.foregroundStyle(.primary) .foregroundStyle(.primary)
@@ -124,7 +124,7 @@ struct NotificationDetailView: View {
private func metadataCard(_ metadata: [String: String]) -> some View { private func metadataCard(_ metadata: [String: String]) -> some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
Label("Информация", systemImage: "info.circle.fill") Label("info_section", systemImage: "info.circle.fill")
.font(.subheadline.bold()) .font(.subheadline.bold())
.foregroundStyle(.primary) .foregroundStyle(.primary)
@@ -170,17 +170,17 @@ struct NotificationDetailView: View {
private var statusCard: some View { private var statusCard: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
Label("Статус", systemImage: "clock.fill") Label("status_section", systemImage: "clock.fill")
.font(.subheadline.bold()) .font(.subheadline.bold())
.foregroundStyle(.primary) .foregroundStyle(.primary)
VStack(spacing: 8) { VStack(spacing: 8) {
infoRow(icon: "paperplane.fill", label: "Канал", value: channelLabel) infoRow(icon: "paperplane.fill", label: String(localized: "channel_label"), value: channelLabel)
Divider() 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 { if let readAt = notification.readAt {
Divider() 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 { private var channelLabel: String {
switch notification.channel { switch notification.channel {
case .inApp: return "В приложении" case .inApp: return String(localized: "channel_in_app")
case .push: return "Push" case .push: return "Push"
case .email: return "Email" case .email: return "Email"
} }
@@ -20,27 +20,27 @@ struct NotificationsView: View {
ProgressView() ProgressView()
} else if let error = viewModel.error, viewModel.notifications.isEmpty { } else if let error = viewModel.error, viewModel.notifications.isEmpty {
ContentUnavailableView( ContentUnavailableView(
"Ошибка загрузки", "loading_error",
systemImage: "exclamationmark.triangle", systemImage: "exclamationmark.triangle",
description: Text(error) description: Text(error)
) )
} else if viewModel.notifications.isEmpty { } else if viewModel.notifications.isEmpty {
ContentUnavailableView( ContentUnavailableView(
"Нет уведомлений", "no_notifications",
systemImage: "bell.slash", systemImage: "bell.slash",
description: Text("Новые уведомления появятся здесь") description: Text("no_notifications_description")
) )
} else { } else {
notificationsList notificationsList
} }
} }
.background(Color(.systemGroupedBackground)) .background(Color(.systemGroupedBackground))
.navigationTitle("Уведомления") .navigationTitle("notifications_title")
.toolbar { .toolbar {
#if DEBUG #if DEBUG
if PreviewData.isPreviewMode { if PreviewData.isPreviewMode {
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .topBarLeading) {
Text("ДЕМО") Text("demo_badge")
.font(.caption2.bold()) .font(.caption2.bold())
.foregroundStyle(.white) .foregroundStyle(.white)
.padding(.horizontal, 8) .padding(.horizontal, 8)
@@ -80,7 +80,7 @@ struct NotificationsView: View {
ScrollView { ScrollView {
LazyVStack(spacing: 0) { LazyVStack(spacing: 0) {
if !unreadNotifications.isEmpty { if !unreadNotifications.isEmpty {
sectionHeader("Активные") sectionHeader(String(localized: "notifications_active"))
ForEach(unreadNotifications) { notification in ForEach(unreadNotifications) { notification in
NavigationLink(destination: NotificationDetailView(notification: notification, viewModel: viewModel)) { NavigationLink(destination: NotificationDetailView(notification: notification, viewModel: viewModel)) {
ActiveNotificationCard(notification: notification) ActiveNotificationCard(notification: notification)
@@ -97,7 +97,7 @@ struct NotificationsView: View {
} }
if !readNotifications.isEmpty { if !readNotifications.isEmpty {
sectionHeader("Завершённые") sectionHeader(String(localized: "notifications_completed"))
ForEach(readNotifications) { notification in ForEach(readNotifications) { notification in
NavigationLink(destination: NotificationDetailView(notification: notification, viewModel: viewModel)) { NavigationLink(destination: NotificationDetailView(notification: notification, viewModel: viewModel)) {
ResolvedNotificationCard(notification: notification) ResolvedNotificationCard(notification: notification)
@@ -172,7 +172,7 @@ struct ActiveNotificationCard: View {
HStack { HStack {
Spacer() Spacer()
Text("Открыть") Text("open_button")
.font(.subheadline.bold()) .font(.subheadline.bold())
.foregroundStyle(Color.red) .foregroundStyle(Color.red)
.padding(.horizontal, 32) .padding(.horizontal, 32)
@@ -238,7 +238,7 @@ struct ResolvedNotificationCard: View {
Image(systemName: "checkmark") Image(systemName: "checkmark")
.font(.caption2) .font(.caption2)
.foregroundStyle(.green) .foregroundStyle(.green)
Text("прочитано \(readAt.formatted(date: .abbreviated, time: .shortened))") Text("notification_read_at \(readAt.formatted(date: .abbreviated, time: .shortened))")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
@@ -12,11 +12,11 @@ struct ChangePasswordView: View {
NavigationStack { NavigationStack {
Form { Form {
Section { Section {
SecureField("Текущий пароль", text: $currentPassword) SecureField("current_password", text: $currentPassword)
.textContentType(.password) .textContentType(.password)
SecureField("Новый пароль", text: $newPassword) SecureField("new_password", text: $newPassword)
.textContentType(.newPassword) .textContentType(.newPassword)
SecureField("Подтвердите новый пароль", text: $confirmPassword) SecureField("confirm_new_password", text: $confirmPassword)
.textContentType(.newPassword) .textContentType(.newPassword)
} }
@@ -33,7 +33,7 @@ struct ChangePasswordView: View {
} }
Section { Section {
Button("Сохранить") { Button("save_button") {
Task { Task {
let success = await viewModel.changePassword(current: currentPassword, new: newPassword) let success = await viewModel.changePassword(current: currentPassword, new: newPassword)
if success { dismiss() } if success { dismiss() }
@@ -43,11 +43,11 @@ struct ChangePasswordView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
} }
.navigationTitle("Сменить пароль") .navigationTitle("change_password_title")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .topBarLeading) {
Button("Отмена") { dismiss() } Button("cancel") { dismiss() }
} }
} }
} }
+5 -5
View File
@@ -14,7 +14,7 @@ struct SessionsView: View {
.font(.body) .font(.body)
.lineLimit(1) .lineLimit(1)
if session.isCurrent { if session.isCurrent {
Text("Текущая") Text("current_session")
.font(.caption) .font(.caption)
.padding(.horizontal, 6) .padding(.horizontal, 6)
.padding(.vertical, 2) .padding(.vertical, 2)
@@ -26,7 +26,7 @@ struct SessionsView: View {
Text(session.ipAddress) Text(session.ipAddress)
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Text("Создана: \(session.createdAt.formatted(date: .abbreviated, time: .shortened))") Text("session_created \(session.createdAt.formatted(date: .abbreviated, time: .shortened))")
.font(.caption2) .font(.caption2)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
@@ -35,17 +35,17 @@ struct SessionsView: View {
Button(role: .destructive) { Button(role: .destructive) {
Task { await viewModel.deleteSession(session) } Task { await viewModel.deleteSession(session) }
} label: { } label: {
Label("Удалить", systemImage: "trash") Label("delete_button", systemImage: "trash")
} }
} }
} }
} }
} }
.navigationTitle("Активные сессии") .navigationTitle("active_sessions_title")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
Button("Готово") { dismiss() } Button("done_button") { dismiss() }
} }
} }
} }
+12 -12
View File
@@ -12,14 +12,14 @@ struct SettingsView: View {
var body: some View { var body: some View {
NavigationStack { NavigationStack {
Form { Form {
Section("Аккаунт") { Section("account_section") {
if let user = authViewModel.currentUser { if let user = authViewModel.currentUser {
LabeledContent("Email", value: user.email) LabeledContent("Email", value: user.email)
} }
} }
Section { Section {
Button("Сменить пароль") { Button("change_password") {
showChangePassword = true showChangePassword = true
} }
@@ -28,7 +28,7 @@ struct SettingsView: View {
UIApplication.shared.open(url) UIApplication.shared.open(url)
} }
} label: { } label: {
Label("Push-уведомления", systemImage: "bell.badge") Label("push_notifications", systemImage: "bell.badge")
.foregroundStyle(.primary) .foregroundStyle(.primary)
} }
} }
@@ -38,7 +38,7 @@ struct SettingsView: View {
showSessions = true showSessions = true
} label: { } label: {
HStack { HStack {
Text("Активные сессии") Text("active_sessions")
Spacer() Spacer()
if !viewModel.sessions.isEmpty { if !viewModel.sessions.isEmpty {
Text("(\(viewModel.sessions.count))") Text("(\(viewModel.sessions.count))")
@@ -52,20 +52,20 @@ struct SettingsView: View {
} }
Section { Section {
Button("Выйти из аккаунта", role: .destructive) { Button("logout_button", role: .destructive) {
Task { await authViewModel.logout() } Task { await authViewModel.logout() }
} }
Button("Выйти на всех устройствах", role: .destructive) { Button("logout_all_button", role: .destructive) {
showLogoutAllConfirm = true showLogoutAllConfirm = true
} }
} }
} }
.navigationTitle("Настройки") .navigationTitle("settings_title")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
Button("Готово") { dismiss() } Button("done_button") { dismiss() }
} }
} }
.sheet(isPresented: $showChangePassword) { .sheet(isPresented: $showChangePassword) {
@@ -76,11 +76,11 @@ struct SettingsView: View {
.environmentObject(viewModel) .environmentObject(viewModel)
} }
.confirmationDialog( .confirmationDialog(
"Выйти на всех устройствах?", "logout_all_confirm",
isPresented: $showLogoutAllConfirm, isPresented: $showLogoutAllConfirm,
titleVisibility: .visible titleVisibility: .visible
) { ) {
Button("Выйти везде", role: .destructive) { Button("logout_all_action", role: .destructive) {
Task { Task {
do { do {
_ = try await NotificationsAPIService.shared.logoutAll() _ = try await NotificationsAPIService.shared.logoutAll()
@@ -90,10 +90,10 @@ struct SettingsView: View {
} }
} }
} }
Button("Отмена", role: .cancel) {} Button("cancel", role: .cancel) {}
} }
.alert( .alert(
"Ошибка", "error_title",
isPresented: Binding( isPresented: Binding(
get: { logoutAllError != nil }, get: { logoutAllError != nil },
set: { if !$0 { logoutAllError = nil } } set: { if !$0 { logoutAllError = nil } }
@@ -123,8 +123,8 @@ struct MaydayLiveActivityLiveActivity: Widget {
@ViewBuilder @ViewBuilder
func statusBadge(_ status: AlertStatus) -> some View { func statusBadge(_ status: AlertStatus) -> some View {
let (text, color): (String, Color) = status == .active let (text, color): (String, Color) = status == .active
? ("активен", .red) ? (String(localized: "alert_status_active"), .red)
: ("завершён", .green) : (String(localized: "alert_status_resolved"), .green)
Text(text) Text(text)
.font(.caption2.bold()) .font(.caption2.bold())
.textCase(.uppercase) .textCase(.uppercase)