feat(i18n): localise hardcoded English across calls, lobby, device verification, moderation, editor and email stage, syncing en/ru keys

This commit is contained in:
heaven 2026-06-15 02:57:32 +03:00
parent 99391d9f28
commit c7dc1d51de
22 changed files with 619 additions and 252 deletions

View file

@ -59,16 +59,30 @@ The developer reviews Russian translations as a native speaker would see them in
- `Organisms` — Complex UI pieces
- `Boot` — Boot/splash error screens (`ConfigConfigError`, `SpecVersions` error, `ClientRoot` init/start error)
**Still to localise** (snapshot taken 2026-04-14 — verify before acting):
**Still to localise** (snapshot taken 2026-06-12 — verify before acting):
- Room features: `MessageEditor`, `MembersDrawer`, `RoomTombstone`
- Lobby: `Lobby.tsx`, `RoomItem.tsx`
- Room features: `MembersDrawer`, `RoomTombstone`
- `AddExisting` feature
- System pages: `FeatureCheck`, `WelcomePage`
- Auth: `OrDivider`
- Dialogs: `LogoutDialog`, `ManualVerification`, `BackupRestore`
- UIA stages: `ReCaptchaStage`, `EmailStage`, `RegistrationTokenStage`
- Components: `TimePicker`, `DatePicker`, `ImageViewer`, `PdfViewer`, `UploadCard`, `UserModeration`
- UIA stages: `ReCaptchaStage`, `RegistrationTokenStage`
- Components: `TimePicker`, `DatePicker`, `ImageViewer`, `PdfViewer`
- Various `aria-label` attributes throughout
Localised 2026-06-12 (the hardcoded-English sweep): `UserModeration`,
call screens (`CallControls`/`PrescreenControls`/`CallView`/`CallMemberCard`),
lobby (`Lobby`/`RoomItem`/`SpaceItem`/`LobbyHeader`), `MessageSearch`
(aria-label), `LeaveSpacePrompt`, the device-verification cluster
(`DeviceVerification`/`DeviceVerificationSetup`/`ManualVerification`/
`BackupRestore`/settings `Verification`), `ComposerAttachments`
(spoiler/retry/cancel/size-error — the attachment strip that replaced the
UploadBoard window), editor `Toolbar` tooltips, `EmailStage`,
`MediaViewerBody` aria-labels (`MediaViewer.*`/`Common.*`). `MessageEditor`
was deleted (editing moved into the composer); `LogoutDialog` was rewritten
as a localized self-contained card. Dead `Settings.email_*notification*`
strings removed from both locale files (the section is never rendered), as
were the keys orphaned by the dropped room-permissions page
(`RoomSettings.perm_*` room-only set), the user emoji-pack settings page,
and the old upload-size-limit concatenation fragments.
When the developer asks to localise a new area, check this list first and update it after finishing.

View file

@ -103,15 +103,24 @@
"reset_button": "Reset Password",
"reset_error_fallback": "Failed to reset password.",
"reset_success_message": "Password has been reset successfully. Please login with your new password.",
"reset_success_login": "Login"
"reset_success_login": "Login",
"uia_email_label": "Email",
"uia_email_send_button": "Send Verification Email",
"uia_email_cancel": "Cancel",
"uia_email_sending": "Sending verification email...",
"uia_email_verify_title": "Verify Email",
"uia_email_send_error": "Failed to send verification email request.",
"uia_email_sent_title": "Verification Request Sent",
"uia_email_sent_message": "Please check your email \"{{email}}\" and validate before continuing further.",
"uia_email_continue": "Continue",
"uia_email_provide_title": "Provide Email",
"uia_email_provide_message": "Please provide email to send verification request."
},
"Settings": {
"title": "Settings",
"menu_general": "General",
"menu_account": "Account",
"menu_notifications": "Notifications",
"menu_devices": "Devices",
"menu_emojis_stickers": "Emojis & Stickers",
"menu_about": "About",
"network": {
"menu": "Connection",
@ -224,10 +233,6 @@
"fsi_prompt_body": "Let Vojo show full-screen notifications so incoming calls wake the screen and appear over the lockscreen, like WhatsApp or Telegram. Open \"Full-screen notifications\" and turn Vojo on.",
"fsi_prompt_later": "Not now",
"fsi_prompt_open": "Open settings",
"email_notification": "Email Notification",
"email_no_email": "Your account does not have any email attached.",
"email_send_notif": "Send notification to your email.",
"email_send_notif_to": "Send notification to your email. (\"{{email}}\")",
"unexpected_error": "Unexpected Error!",
"all_messages": "All Messages",
"one_to_one": "1-to-1 Chats",
@ -302,21 +307,11 @@
"import_desc": "Load password protected copy of encryption data from device to decrypt your messages.",
"import": "Import",
"decrypt": "Decrypt",
"emojis_stickers_title": "Emojis & Stickers",
"default_pack": "Default Pack",
"unknown": "Unknown",
"view": "View",
"favorite_packs": "Favorite Packs",
"select_pack": "Select Pack",
"select_pack_desc": "Pick emoji and sticker packs from rooms to use globally.",
"select": "Select",
"room_packs": "Room Packs",
"select_all": "Select All",
"unselect_all": "Unselect All",
"no_packs": "No Packs",
"no_packs_desc": "Packs from rooms will appear here. You do not have any rooms with packs yet.",
"apply_error": "Failed to apply changes! Please try again.",
"apply_ready": "Changes saved! Apply when ready.",
"apply_changes": "Apply Changes",
"about_title": "About",
"about_tagline": "A messenger for everyone.",
@ -333,7 +328,58 @@
"notify_on_mention": "Mentions",
"notify_on_mention_desc": "Notify me when someone mentions my name or username.",
"room_announcements": "@room announcements",
"room_announcements_desc": "Notify me about @room messages."
"room_announcements_desc": "Notify me about @room messages.",
"verification_close": "Close",
"verification_accept_prompt": "Please accept the request from other device.",
"verification_waiting_accept": "Waiting for request to be accepted...",
"verification_click_accept": "Click accept to start the verification process.",
"verification_accept": "Accept",
"verification_request_accepted": "Verification request has been accepted.",
"verification_waiting_response": "Waiting for the response from other device...",
"verification_starting_emoji": "Starting verification using emoji comparison...",
"verification_compare_emoji_prompt": "Confirm the emoji below are displayed on both devices, in the same order:",
"verification_match": "They Match",
"verification_no_match": "Do not Match",
"verification_device_verified": "Your device is verified.",
"verification_okay": "Okay",
"verification_canceled": "Verification has been canceled.",
"verification_unexpected_error": "Unexpected Error! Verification is started but verifier is missing.",
"verification_setup_desc": "Generate a Recovery Key for verifying identity if you do not have access to other devices. Additionally, setup a passphrase as a memorable alternative.",
"verification_passphrase_optional": "Passphrase (Optional)",
"verification_continue": "Continue",
"verification_error_generic": "Unexpected Error!",
"verification_uia_unsupported": "Authentication steps to perform this action are not supported by client.",
"verification_recovery_key_store_desc": "Store the Recovery Key in a safe place for future use, as you will need it to verify your identity if you do not have access to other devices.",
"verification_recovery_key": "Recovery Key",
"verification_show": "Show",
"verification_hide": "Hide",
"verification_copy": "Copy",
"verification_download": "Download",
"verification_setup_title": "Setup Device Verification",
"verification_reset_title": "Reset Device Verification",
"verification_reset_permanent": "Resetting device verification is permanent.",
"verification_reset_warning": "Anyone you have verified with will see security alerts and your encryption backup will be lost. You almost certainly do not want to do this, unless you have lost Recovery Key or Recovery Passphrase and every device you can verify from.",
"verification_recovery_passphrase": "Recovery Passphrase",
"verification_select_method": "Select a verification method.",
"verification_provide_key": "Provide recovery key.",
"verification_verified_success": "Device verified!",
"verification_step_open": "Open {{settings}}.",
"verification_step_find": "Find this device in {{section}} section.",
"backup_connected": "Connected",
"backup_disconnected": "Disconnected",
"backup_syncing": "Syncing ({{amount}})",
"backup_restoring_percent": "Restoring: {{percent}}%",
"backup_trusted_decryption_key": "Backup has trusted decryption key.",
"backup_untrusted_decryption_key": "Backup does not have trusted decryption key!",
"backup_trusted_signature": "Backup is trusted by signature.",
"backup_untrusted_signature": "Backup does not have trusted signature!",
"backup_encryption_title": "Encryption Backup",
"backup_details": "Backup Details",
"backup_version": "Version: {{version}}",
"backup_keys_count": "Keys: {{amount}}",
"backup_none_value": "NIL",
"backup_restore": "Restore Backup",
"backup_none_on_server": "No backup present on server!"
},
"Search": {
"search": "Search",
@ -521,7 +567,20 @@
"bubble_cancelled_count_one": "{{count}} cancelled call",
"bubble_cancelled_count_other": "{{count}} cancelled calls",
"duration_minutes_seconds": "{{minutes}} min {{seconds}} sec",
"duration_seconds": "{{seconds}} sec"
"duration_seconds": "{{seconds}} sec",
"view_grid": "Grid View",
"view_spotlight": "Spotlight View",
"reactions": "Reactions",
"settings": "Settings",
"homeserver_no_calls": "Your homeserver does not support calling. But you can still join call started by others.",
"empty_be_first": "Voice chats empty — be the first to hop in!",
"no_permission_to_join": "You don't have permission to join!",
"already_in_other_call": "Already in another call — end the current call to join!",
"participant": "Participants",
"live_count": "{{count}} Live",
"collapse": "Collapse",
"others_count_one": "+{{count}} Other",
"others_count_other": "+{{count}} Others"
},
"Room": {
"delivery": {
@ -534,7 +593,6 @@
"collapse_avatar": "Collapse avatar",
"expand_avatar": "Open avatar",
"new_messages": "New Messages",
"jump_to_unread": "Jump to Unread",
"mark_as_read": "Mark as Read",
"jump_to_latest": "Jump to Latest",
"message_render_error": "This message could not be displayed.",
@ -542,6 +600,13 @@
"yesterday": "Yesterday",
"view_reactions": "View Reactions",
"read_receipts": "Read Receipts",
"seen_by": "Seen by",
"seen_by_count_one": "{{count}} person",
"seen_by_count_other": "{{count}} people",
"seen_by_empty": "No one has seen this message yet.",
"editing_message": "Editing message",
"editing_cancel": "Cancel editing",
"no_post_permission": "You do not have permission to post in this room",
"view_source": "View Source",
"source_code": "Source Code",
"copy_link": "Copy Link",
@ -652,7 +717,6 @@
"thread_summary_unread_other": "{{count}} unread",
"thread_summary_highlight_one": "{{count}} mention",
"thread_summary_highlight_other": "{{count}} mentions",
"no_post_permission": "You do not have permission to post in this room",
"empty_dm": "The hardest part is the first message.",
"empty_dm_alt_1": "You have to start somewhere.",
"empty_dm_alt_2": "Someone has to go first.",
@ -693,7 +757,43 @@
"autocomplete_rooms": "Rooms",
"autocomplete_emojis": "Emojis",
"autocomplete_commands": "Commands",
"autocomplete_unknown_room": "Unknown Room"
"autocomplete_unknown_room": "Unknown Room",
"lobby_join": "Join",
"lobby_unknown": "Unknown",
"lobby_suggested": "Suggested",
"lobby_inaccessible": "Inaccessible",
"lobby_open_room": "Open Room",
"lobby_rooms": "Rooms",
"lobby_scroll_to_top": "Scroll to Top",
"lobby_reordering": "Reordering",
"lobby_space_settings": "Space Settings",
"lobby_leave_space": "Leave Space",
"lobby_members_count_one": "{{formattedCount}} Member",
"lobby_members_count_other": "{{formattedCount}} Members",
"msg_search_scroll_to_top": "Scroll to Top",
"leave_space_title": "Leave Space",
"leave_space_confirm": "Are you sure you want to leave this space?",
"leave_space_error": "Failed to leave space! {{error}}",
"upload_spoiler": "Spoiler",
"upload_retry": "Retry",
"upload_retry_label": "Retry Upload",
"upload_cancel_label": "Cancel Upload",
"upload_too_large": "File too large",
"format_bold": "Bold",
"format_italic": "Italic",
"format_underline": "Underline",
"format_strikethrough": "Strike Through",
"format_inline_code": "Inline Code",
"format_spoiler": "Spoiler",
"format_block_quote": "Block Quote",
"format_block_code": "Block Code",
"format_ordered_list": "Ordered List",
"format_unordered_list": "Unordered List",
"format_heading": "Heading {{level}}",
"format_exit_formatting": "Exit Formatting",
"format_exit": "Exit",
"format_markdown_disable": "Disable Markdown",
"format_markdown_enable": "Enable Markdown"
},
"Inbox": {
"invite_title": "Invite",
@ -890,38 +990,19 @@
"sort_z_to_a": "Z to A",
"sort_newest": "Newest",
"sort_oldest": "Oldest",
"perm_messages": "Messages",
"perm_send_messages": "Send Messages",
"perm_send_stickers": "Send Stickers",
"perm_send_reactions": "Send Reactions",
"perm_ping_room": "Ping @room",
"perm_pin_messages": "Pin Messages",
"perm_other_message_events": "Other Message Events",
"perm_calls": "Calls",
"perm_join_call": "Join Call",
"perm_moderation": "Moderation",
"perm_invite": "Invite",
"perm_kick": "Kick",
"perm_ban": "Ban",
"perm_delete_others_messages": "Delete Others' Messages",
"perm_delete_self_messages": "Delete Self Messages",
"perm_room_overview": "Room Overview",
"perm_room_avatar": "Room Avatar",
"perm_room_name": "Room Name",
"perm_room_topic": "Room Topic",
"perm_settings": "Settings",
"perm_change_room_access": "Change Room Access",
"perm_publish_address": "Publish Address",
"perm_change_all_permission": "Change All Permissions",
"perm_edit_power_levels": "Edit Power Levels",
"perm_enable_encryption": "Enable Encryption",
"perm_history_visibility": "History Visibility",
"perm_upgrade_room": "Upgrade Room",
"perm_other_settings": "Other Settings",
"perm_other": "Other",
"perm_manage_emojis_stickers": "Manage Emojis & Stickers",
"perm_change_server_acls": "Change Server ACLs",
"perm_modify_widgets": "Modify Widgets",
"founders": "Founders",
"founders_desc": "Founding members have all permissions and can only be changed during a room upgrade.",
"power_levels": "Power Levels",
@ -1113,6 +1194,8 @@
"blocked_title": "Blocked User",
"blocked_description": "You do not receive any messages or invites from this user.",
"profile_title": "Profile",
"back": "Back",
"manage_powers": "Manage roles",
"presence_online": "Online",
"presence_unavailable": "Idle",
"presence_offline": "Offline",
@ -1123,7 +1206,7 @@
"last_seen_hours_other": "Last seen {{count}} hours ago",
"last_seen_yesterday": "Last seen yesterday at {{time}}",
"last_seen_date": "Last seen {{date}}",
"row_id": "id",
"row_id": "nick",
"row_server": "server",
"row_role": "role",
"row_mutual": "shared",
@ -1137,7 +1220,22 @@
"copy_user_link": "Copy user link",
"copy_server": "Copy server",
"explore_community": "Explore community",
"open_in_browser": "Open in browser"
"open_in_browser": "Open in browser",
"moderation_title": "Moderation",
"moderation_reason": "Reason",
"moderation_reason_label": "Reason:",
"moderation_no_reason": "No reason provided.",
"moderation_kicked_user": "Kicked User",
"moderation_banned_user": "Banned User",
"moderation_invited_user": "Invited User",
"moderation_kicked_by": "Kicked by:",
"moderation_banned_by": "Banned by:",
"moderation_invited_by": "Invited by:",
"moderation_unban": "Unban",
"moderation_cancel_invite": "Cancel Invite",
"moderation_invite": "Invite",
"moderation_kick": "Kick",
"moderation_ban": "Ban"
},
"Share": {
"share_text": "Ready to share text",
@ -1148,5 +1246,16 @@
"share_files": "Ready to share {{count}} files",
"tap_chat_to_send": "Open a chat to drop it in",
"cancel": "Cancel share"
},
"Common": {
"close": "Close",
"retry": "Retry"
},
"MediaViewer": {
"zoom_out": "Zoom out",
"zoom_in": "Zoom in",
"download": "Download",
"previous": "Previous",
"next": "Next"
}
}

View file

@ -103,15 +103,24 @@
"reset_button": "Сбросить пароль",
"reset_error_fallback": "Не удалось сбросить пароль.",
"reset_success_message": "Пароль успешно сброшен. Войдите с новым паролем.",
"reset_success_login": "Войти"
"reset_success_login": "Войти",
"uia_email_label": "Эл. почта",
"uia_email_send_button": "Отправить письмо для подтверждения",
"uia_email_cancel": "Отмена",
"uia_email_sending": "Отправка письма для подтверждения...",
"uia_email_verify_title": "Подтверждение почты",
"uia_email_send_error": "Не удалось отправить письмо для подтверждения.",
"uia_email_sent_title": "Письмо для подтверждения отправлено",
"uia_email_sent_message": "Проверьте почту \"{{email}}\" и подтвердите адрес, прежде чем продолжить.",
"uia_email_continue": "Продолжить",
"uia_email_provide_title": "Укажите эл. почту",
"uia_email_provide_message": "Укажите адрес эл. почты, чтобы отправить письмо для подтверждения."
},
"Settings": {
"title": "Настройки",
"menu_general": "Общие",
"menu_account": "Аккаунт",
"menu_notifications": "Уведомления",
"menu_devices": "Устройства",
"menu_emojis_stickers": "Эмодзи и стикеры",
"menu_about": "О приложении",
"network": {
"menu": "Соединение",
@ -224,10 +233,6 @@
"fsi_prompt_body": "Разрешите Vojo показывать полноэкранные уведомления — тогда входящие звонки будут будить экран и появляться поверх блокировки, как в WhatsApp или Telegram. Откройте «Уведомления поверх экрана блокировки» и включите переключатель для Vojo.",
"fsi_prompt_later": "Позже",
"fsi_prompt_open": "Открыть настройки",
"email_notification": "Уведомления по почте",
"email_no_email": "К вашему аккаунту не привязана электронная почта.",
"email_send_notif": "Отправлять уведомления на вашу почту.",
"email_send_notif_to": "Отправлять уведомления на вашу почту. (\"{{email}}\")",
"unexpected_error": "Непредвиденная ошибка!",
"all_messages": "Все сообщения",
"one_to_one": "Личные чаты",
@ -302,21 +307,11 @@
"import_desc": "Загрузите защищённую паролем копию ключей шифрования с устройства для расшифровки сообщений.",
"import": "Импорт",
"decrypt": "Расшифровать",
"emojis_stickers_title": "Эмодзи и стикеры",
"default_pack": "Пакет по умолчанию",
"unknown": "Неизвестно",
"view": "Открыть",
"favorite_packs": "Избранные пакеты",
"select_pack": "Выбрать пакет",
"select_pack_desc": "Выберите пакеты эмодзи и стикеров из комнат для использования во всех комнатах.",
"select": "Выбрать",
"room_packs": "Пакеты комнат",
"select_all": "Выбрать все",
"unselect_all": "Снять выделение",
"no_packs": "Нет пакетов",
"no_packs_desc": "Здесь появятся пакеты из комнат. У вас пока нет комнат с пакетами.",
"apply_error": "Не удалось применить изменения! Попробуйте снова.",
"apply_ready": "Изменения сохранены! Примените, когда будете готовы.",
"apply_changes": "Применить изменения",
"about_title": "О приложении",
"about_tagline": "Вседоступный мессенджер.",
@ -333,7 +328,58 @@
"notify_on_mention": "Упоминания",
"notify_on_mention_desc": "Уведомлять, когда упоминают моё имя или ник.",
"room_announcements": "Объявления @room",
"room_announcements_desc": "Уведомлять о сообщениях с @room."
"room_announcements_desc": "Уведомлять о сообщениях с @room.",
"verification_close": "Закрыть",
"verification_accept_prompt": "Примите запрос на другом устройстве.",
"verification_waiting_accept": "Ожидание подтверждения запроса...",
"verification_click_accept": "Нажмите «Принять», чтобы начать верификацию.",
"verification_accept": "Принять",
"verification_request_accepted": "Запрос на верификацию принят.",
"verification_waiting_response": "Ожидание ответа от другого устройства...",
"verification_starting_emoji": "Начинаем верификацию сравнением эмодзи...",
"verification_compare_emoji_prompt": "Убедитесь, что на обоих устройствах показаны одни и те же эмодзи в одинаковом порядке:",
"verification_match": "Совпадают",
"verification_no_match": "Не совпадают",
"verification_device_verified": "Ваше устройство верифицировано.",
"verification_okay": "Понятно",
"verification_canceled": "Верификация отменена.",
"verification_unexpected_error": "Непредвиденная ошибка! Верификация началась, но обработчик проверки не найден.",
"verification_setup_desc": "Создайте ключ восстановления — он подтвердит вашу личность, если другие устройства недоступны. Дополнительно можно задать парольную фразу, которую проще запомнить.",
"verification_passphrase_optional": "Парольная фраза (необязательно)",
"verification_continue": "Продолжить",
"verification_error_generic": "Непредвиденная ошибка!",
"verification_uia_unsupported": "Клиент не поддерживает шаги аутентификации, необходимые для этого действия.",
"verification_recovery_key_store_desc": "Сохраните ключ восстановления в надёжном месте: он понадобится для подтверждения личности, если другие устройства будут недоступны.",
"verification_recovery_key": "Ключ восстановления",
"verification_show": "Показать",
"verification_hide": "Скрыть",
"verification_copy": "Скопировать",
"verification_download": "Скачать",
"verification_setup_title": "Настройка верификации устройства",
"verification_reset_title": "Сброс верификации устройства",
"verification_reset_permanent": "Сброс верификации устройства необратим.",
"verification_reset_warning": "Все, с кем вы проходили верификацию, увидят предупреждения безопасности, а резервная копия шифрования будет потеряна. Делайте это, только если вы потеряли ключ восстановления или парольную фразу и все устройства, с которых можно пройти верификацию.",
"verification_recovery_passphrase": "Парольная фраза восстановления",
"verification_select_method": "Выберите способ верификации.",
"verification_provide_key": "Введите ключ восстановления.",
"verification_verified_success": "Устройство верифицировано!",
"verification_step_open": "Откройте «{{settings}}».",
"verification_step_find": "Найдите это устройство в разделе «{{section}}».",
"backup_connected": "Подключено",
"backup_disconnected": "Отключено",
"backup_syncing": "Синхронизация ({{amount}})",
"backup_restoring_percent": "Восстановление: {{percent}}%",
"backup_trusted_decryption_key": "У резервной копии есть доверенный ключ расшифровки.",
"backup_untrusted_decryption_key": "У резервной копии нет доверенного ключа расшифровки!",
"backup_trusted_signature": "Резервная копия заверена доверенной подписью.",
"backup_untrusted_signature": "У резервной копии нет доверенной подписи!",
"backup_encryption_title": "Резервная копия шифрования",
"backup_details": "Сведения о резервной копии",
"backup_version": "Версия: {{version}}",
"backup_keys_count": "Ключей: {{amount}}",
"backup_none_value": "нет",
"backup_restore": "Восстановить резервную копию",
"backup_none_on_server": "На сервере нет резервной копии!"
},
"Search": {
"search": "Поиск",
@ -523,11 +569,28 @@
"bubble_missed_count_one": "{{count}} пропущенный звонок",
"bubble_missed_count_few": "{{count}} пропущенных звонка",
"bubble_missed_count_many": "{{count}} пропущенных звонков",
"bubble_missed_count_other": "{{count}} пропущенных звонков",
"bubble_cancelled_count_one": "{{count}} отменённый звонок",
"bubble_cancelled_count_few": "{{count}} отменённых звонка",
"bubble_cancelled_count_many": "{{count}} отменённых звонков",
"bubble_cancelled_count_other": "{{count}} отменённых звонков",
"duration_minutes_seconds": "{{minutes}} мин {{seconds}} сек",
"duration_seconds": "{{seconds}} сек"
"duration_seconds": "{{seconds}} сек",
"view_grid": "Вид галереи",
"view_spotlight": "Вид докладчика",
"reactions": "Реакции",
"settings": "Настройки",
"homeserver_no_calls": "Ваш сервер не поддерживает звонки, но вы можете присоединиться к звонку, который начал кто-то другой.",
"empty_be_first": "В голосовом чате пусто — присоединяйтесь первым!",
"no_permission_to_join": "У вас нет прав, чтобы присоединиться!",
"already_in_other_call": "Вы уже в другом звонке — завершите его, чтобы присоединиться!",
"participant": "Участники",
"live_count": "{{count}} в эфире",
"collapse": "Свернуть",
"others_count_one": "Ещё {{count}} участник",
"others_count_few": "Ещё {{count}} участника",
"others_count_many": "Ещё {{count}} участников",
"others_count_other": "Ещё {{count}} участников"
},
"Room": {
"delivery": {
@ -540,7 +603,6 @@
"collapse_avatar": "Свернуть аватар",
"expand_avatar": "Развернуть аватар",
"new_messages": "Новые сообщения",
"jump_to_unread": "К непрочитанным",
"mark_as_read": "Отметить прочитанным",
"jump_to_latest": "К последним",
"message_render_error": "Не удалось показать это сообщение.",
@ -548,6 +610,15 @@
"yesterday": "Вчера",
"view_reactions": "Реакции",
"read_receipts": "Подтверждения прочтения",
"seen_by": "Прочитали",
"seen_by_count_one": "{{count}} человек",
"seen_by_count_few": "{{count}} человека",
"seen_by_count_many": "{{count}} человек",
"seen_by_count_other": "{{count}} человек",
"seen_by_empty": "Это сообщение пока никто не прочитал.",
"editing_message": "Редактирование",
"editing_cancel": "Отменить редактирование",
"no_post_permission": "У вас нет разрешения на отправку сообщений в этой комнате",
"view_source": "Исходный код",
"source_code": "Исходный код",
"copy_link": "Копировать ссылку",
@ -668,7 +739,6 @@
"thread_summary_highlight_few": "{{count}} упоминания",
"thread_summary_highlight_many": "{{count}} упоминаний",
"thread_summary_highlight_other": "{{count}} упоминания",
"no_post_permission": "У вас нет разрешения на отправку сообщений в этой комнате",
"empty_dm": "Самое сложное — первое сообщение.",
"empty_dm_alt_1": "С чего-то надо начать.",
"empty_dm_alt_2": "Кто-то должен написать первым.",
@ -709,7 +779,45 @@
"autocomplete_rooms": "Комнаты",
"autocomplete_emojis": "Эмодзи",
"autocomplete_commands": "Команды",
"autocomplete_unknown_room": "Неизвестная комната"
"autocomplete_unknown_room": "Неизвестная комната",
"lobby_join": "Присоединиться",
"lobby_unknown": "Неизвестно",
"lobby_suggested": "Рекомендуется",
"lobby_inaccessible": "Недоступно",
"lobby_open_room": "Открыть комнату",
"lobby_rooms": "Комнаты",
"lobby_scroll_to_top": "Наверх",
"lobby_reordering": "Перемещение",
"lobby_space_settings": "Настройки сообщества",
"lobby_leave_space": "Покинуть сообщество",
"lobby_members_count_one": "{{formattedCount}} участник",
"lobby_members_count_few": "{{formattedCount}} участника",
"lobby_members_count_many": "{{formattedCount}} участников",
"lobby_members_count_other": "{{formattedCount}} участников",
"msg_search_scroll_to_top": "Наверх",
"leave_space_title": "Покинуть сообщество",
"leave_space_confirm": "Покинуть это сообщество?",
"leave_space_error": "Не удалось покинуть сообщество! {{error}}",
"upload_spoiler": "Спойлер",
"upload_retry": "Повторить",
"upload_retry_label": "Повторить загрузку",
"upload_cancel_label": "Отменить загрузку",
"upload_too_large": "Файл слишком большой",
"format_bold": "Жирный",
"format_italic": "Курсив",
"format_underline": "Подчёркнутый",
"format_strikethrough": "Зачёркнутый",
"format_inline_code": "Код",
"format_spoiler": "Спойлер",
"format_block_quote": "Цитата",
"format_block_code": "Блок кода",
"format_ordered_list": "Нумерованный список",
"format_unordered_list": "Маркированный список",
"format_heading": "Заголовок {{level}}",
"format_exit_formatting": "Выйти из форматирования",
"format_exit": "Выйти",
"format_markdown_disable": "Отключить Markdown",
"format_markdown_enable": "Включить Markdown"
},
"Inbox": {
"invite_title": "Пригласить",
@ -908,38 +1016,19 @@
"sort_z_to_a": "Я — А",
"sort_newest": "Новые",
"sort_oldest": "Старые",
"perm_messages": "Сообщения",
"perm_send_messages": "Отправка сообщений",
"perm_send_stickers": "Отправка стикеров",
"perm_send_reactions": "Отправка реакций",
"perm_ping_room": "Упоминание @room",
"perm_pin_messages": "Закрепление сообщений",
"perm_other_message_events": "Прочие события сообщений",
"perm_calls": "Звонки",
"perm_join_call": "Присоединиться к звонку",
"perm_moderation": "Модерация",
"perm_invite": "Приглашение",
"perm_kick": "Исключение",
"perm_ban": "Бан",
"perm_delete_others_messages": "Удаление чужих сообщений",
"perm_delete_self_messages": "Удаление своих сообщений",
"perm_room_overview": "Обзор комнаты",
"perm_room_avatar": "Аватар комнаты",
"perm_room_name": "Название комнаты",
"perm_room_topic": "Тема комнаты",
"perm_settings": "Настройки",
"perm_change_room_access": "Изменение доступа к комнате",
"perm_publish_address": "Публикация адреса",
"perm_change_all_permission": "Изменение всех прав",
"perm_edit_power_levels": "Редактирование уровней власти",
"perm_enable_encryption": "Включение шифрования",
"perm_history_visibility": "Видимость истории",
"perm_upgrade_room": "Обновление комнаты",
"perm_other_settings": "Прочие настройки",
"perm_other": "Прочее",
"perm_manage_emojis_stickers": "Управление эмодзи и стикерами",
"perm_change_server_acls": "Изменение ACL серверов",
"perm_modify_widgets": "Изменение виджетов",
"founders": "Основатели",
"founders_desc": "Основатели имеют все права. Изменить их состав можно только при обновлении комнаты.",
"power_levels": "Уровни власти",
@ -1131,6 +1220,8 @@
"blocked_title": "Заблокирован",
"blocked_description": "Сообщения и приглашения от этого пользователя не приходят.",
"profile_title": "Профиль",
"back": "Назад",
"manage_powers": "Управление ролями",
"presence_online": "В сети",
"presence_unavailable": "Не активен",
"presence_offline": "Не в сети",
@ -1138,12 +1229,14 @@
"last_seen_minutes_one": "Был в сети {{count}} минуту назад",
"last_seen_minutes_few": "Был в сети {{count}} минуты назад",
"last_seen_minutes_many": "Был в сети {{count}} минут назад",
"last_seen_minutes_other": "Был в сети {{count}} минут назад",
"last_seen_hours_one": "Был в сети {{count}} час назад",
"last_seen_hours_few": "Был в сети {{count}} часа назад",
"last_seen_hours_many": "Был в сети {{count}} часов назад",
"last_seen_hours_other": "Был в сети {{count}} часов назад",
"last_seen_yesterday": "Был в сети вчера в {{time}}",
"last_seen_date": "Был в сети {{date}}",
"row_id": "id",
"row_id": "ник",
"row_server": "сервер",
"row_role": "роль",
"row_mutual": "общие",
@ -1154,11 +1247,27 @@
"row_mutual_count_one": "{{count}} чат",
"row_mutual_count_few": "{{count}} чата",
"row_mutual_count_many": "{{count}} чатов",
"row_mutual_count_other": "{{count}} чатов",
"copy_user_id": "Скопировать ID",
"copy_user_link": "Скопировать ссылку",
"copy_server": "Скопировать сервер",
"explore_community": "Открыть сервер",
"open_in_browser": "Открыть в браузере"
"open_in_browser": "Открыть в браузере",
"moderation_title": "Модерация",
"moderation_reason": "Причина",
"moderation_reason_label": "Причина:",
"moderation_no_reason": "Причина не указана.",
"moderation_kicked_user": "Пользователь исключён",
"moderation_banned_user": "Пользователь забанен",
"moderation_invited_user": "Пользователь приглашён",
"moderation_kicked_by": "Исключил(а):",
"moderation_banned_by": "Забанил(а):",
"moderation_invited_by": "Пригласил(а):",
"moderation_unban": "Разбанить",
"moderation_cancel_invite": "Отменить приглашение",
"moderation_invite": "Пригласить",
"moderation_kick": "Исключить",
"moderation_ban": "Забанить"
},
"Share": {
"share_text": "Готово к пересылке: текст",
@ -1169,5 +1278,16 @@
"share_files": "Готово к пересылке: {{count}} файлов",
"tap_chat_to_send": "Откройте чат, чтобы отправить",
"cancel": "Отменить"
},
"Common": {
"close": "Закрыть",
"retry": "Повторить"
},
"MediaViewer": {
"zoom_out": "Уменьшить",
"zoom_in": "Увеличить",
"download": "Скачать",
"previous": "Назад",
"next": "Вперёд"
}
}

View file

@ -19,6 +19,7 @@ import {
Text,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import { BackupProgressStatus, backupRestoreProgressAtom } from '../state/backupRestore';
import { InfoCard } from './info-card';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
@ -35,6 +36,7 @@ type BackupStatusProps = {
enabled: boolean;
};
function BackupStatus({ enabled }: BackupStatusProps) {
const { t } = useTranslation();
return (
<Box as="span" gap="100" alignItems="Center">
<Badge variant={enabled ? 'Success' : 'Critical'} fill="Solid" size="200" radii="Pill" />
@ -43,7 +45,7 @@ function BackupStatus({ enabled }: BackupStatusProps) {
size="L400"
style={{ color: enabled ? color.Success.Main : color.Critical.Main }}
>
{enabled ? 'Connected' : 'Disconnected'}
{enabled ? t('Settings.backup_connected') : t('Settings.backup_disconnected')}
</Text>
</Box>
);
@ -52,21 +54,23 @@ type BackupSyncingProps = {
count: number;
};
function BackupSyncing({ count }: BackupSyncingProps) {
const { t } = useTranslation();
return (
<Box as="span" gap="100" alignItems="Center">
<Spinner size="50" variant="Primary" fill="Soft" />
<Text as="span" size="L400" style={{ color: color.Primary.Main }}>
Syncing ({count})
{t('Settings.backup_syncing', { amount: count })}
</Text>
</Box>
);
}
function BackupProgressFetching() {
const { t } = useTranslation();
return (
<Box grow="Yes" gap="200" alignItems="Center">
<Badge variant="Secondary" fill="Solid" radii="300">
<Text size="L400">Restoring: 0%</Text>
<Text size="L400">{t('Settings.backup_restoring_percent', { percent: 0 })}</Text>
</Badge>
<Box grow="Yes" direction="Column">
<ProgressBar variant="Secondary" size="300" min={0} max={1} value={0} />
@ -81,10 +85,15 @@ type BackupProgressProps = {
downloaded: number;
};
function BackupProgress({ total, downloaded }: BackupProgressProps) {
const { t } = useTranslation();
return (
<Box grow="Yes" gap="200" alignItems="Center">
<Badge variant="Secondary" fill="Solid" radii="300">
<Text size="L400">Restoring: {`${Math.round(percent(0, total, downloaded))}%`}</Text>
<Text size="L400">
{t('Settings.backup_restoring_percent', {
percent: Math.round(percent(0, total, downloaded)),
})}
</Text>
</Badge>
<Box grow="Yes" direction="Column">
<ProgressBar variant="Secondary" size="300" min={0} max={total} value={downloaded} />
@ -103,6 +112,7 @@ type BackupTrustInfoProps = {
backupInfo: KeyBackupInfo;
};
function BackupTrustInfo({ crypto, backupInfo }: BackupTrustInfoProps) {
const { t } = useTranslation();
const trust = useKeyBackupTrust(crypto, backupInfo);
if (!trust) return null;
@ -111,20 +121,20 @@ function BackupTrustInfo({ crypto, backupInfo }: BackupTrustInfoProps) {
<Box direction="Column">
{trust.matchesDecryptionKey ? (
<Text size="T200" style={{ color: color.Success.Main }}>
<b>Backup has trusted decryption key.</b>
<b>{t('Settings.backup_trusted_decryption_key')}</b>
</Text>
) : (
<Text size="T200" style={{ color: color.Critical.Main }}>
<b>Backup does not have trusted decryption key!</b>
<b>{t('Settings.backup_untrusted_decryption_key')}</b>
</Text>
)}
{trust.trusted ? (
<Text size="T200" style={{ color: color.Success.Main }}>
<b>Backup has trusted by signature.</b>
<b>{t('Settings.backup_trusted_signature')}</b>
</Text>
) : (
<Text size="T200" style={{ color: color.Critical.Main }}>
<b>Backup does not have trusted signature!</b>
<b>{t('Settings.backup_untrusted_signature')}</b>
</Text>
)}
</Box>
@ -135,6 +145,7 @@ type BackupRestoreTileProps = {
crypto: CryptoApi;
};
export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) {
const { t } = useTranslation();
const [restoreProgress, setRestoreProgress] = useAtom(backupRestoreProgressAtom);
const restoring =
restoreProgress.status === BackupProgressStatus.Fetching ||
@ -168,7 +179,7 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) {
return (
<InfoCard
variant="Surface"
title="Encryption Backup"
title={t('Settings.backup_encryption_title')}
after={
<Box alignItems="Center" gap="200">
{remainingSession === 0 ? (
@ -212,12 +223,20 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) {
<Box direction="Column" gap="200">
<InfoCard
variant="SurfaceVariant"
title="Backup Details"
title={t('Settings.backup_details')}
description={
<>
<span>Version: {backupInfo?.version ?? 'NIL'}</span>
<span>
{t('Settings.backup_version', {
version: backupInfo?.version ?? t('Settings.backup_none_value'),
})}
</span>
<br />
<span>Keys: {backupInfo?.count ?? 'NIL'}</span>
<span>
{t('Settings.backup_keys_count', {
amount: backupInfo?.count ?? t('Settings.backup_none_value'),
})}
</span>
</>
}
/>
@ -234,7 +253,7 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) {
}
before={<Icon size="100" src={Icons.Download} />}
>
<Text size="B300">Restore Backup</Text>
<Text size="B300">{t('Settings.backup_restore')}</Text>
</Button>
</Box>
</Menu>
@ -251,7 +270,7 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) {
)}
{!backupEnabled && backupInfo === null && (
<Text size="T200" style={{ color: color.Critical.Main }}>
<b>No backup present on server!</b>
<b>{t('Settings.backup_none_on_server')}</b>
</Text>
)}
{!syncFailure && !backupEnabled && backupInfo && (

View file

@ -22,6 +22,7 @@ import {
Text,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import {
useVerificationRequestPhase,
useVerificationRequestReceived,
@ -50,21 +51,23 @@ function WaitingMessage({ message }: WaitingMessageProps) {
type VerificationUnexpectedProps = { message: string; onClose: () => void };
function VerificationUnexpected({ message, onClose }: VerificationUnexpectedProps) {
const { t } = useTranslation();
return (
<Box direction="Column" gap="400">
<Text>{message}</Text>
<Button variant="Secondary" fill="Soft" onClick={onClose}>
<Text size="B400">Close</Text>
<Text size="B400">{t('Settings.verification_close')}</Text>
</Button>
</Box>
);
}
function VerificationWaitAccept() {
const { t } = useTranslation();
return (
<Box direction="Column" gap="400">
<Text>Please accept the request from other device.</Text>
<WaitingMessage message="Waiting for request to be accepted..." />
<Text>{t('Settings.verification_accept_prompt')}</Text>
<WaitingMessage message={t('Settings.verification_waiting_accept')} />
</Box>
);
}
@ -73,12 +76,13 @@ type VerificationAcceptProps = {
onAccept: () => Promise<void>;
};
function VerificationAccept({ onAccept }: VerificationAcceptProps) {
const { t } = useTranslation();
const [acceptState, accept] = useAsyncCallback(onAccept);
const accepting = acceptState.status === AsyncStatus.Loading;
return (
<Box direction="Column" gap="400">
<Text>Click accept to start the verification process.</Text>
<Text>{t('Settings.verification_click_accept')}</Text>
<Button
variant="Primary"
fill="Solid"
@ -86,17 +90,18 @@ function VerificationAccept({ onAccept }: VerificationAcceptProps) {
before={accepting && <Spinner size="100" variant="Primary" fill="Solid" />}
disabled={accepting}
>
<Text size="B400">Accept</Text>
<Text size="B400">{t('Settings.verification_accept')}</Text>
</Button>
</Box>
);
}
function VerificationWaitStart() {
const { t } = useTranslation();
return (
<Box direction="Column" gap="400">
<Text>Verification request has been accepted.</Text>
<WaitingMessage message="Waiting for the response from other device..." />
<Text>{t('Settings.verification_request_accepted')}</Text>
<WaitingMessage message={t('Settings.verification_waiting_response')} />
</Box>
);
}
@ -105,18 +110,20 @@ type VerificationStartProps = {
onStart: () => Promise<void>;
};
function AutoVerificationStart({ onStart }: VerificationStartProps) {
const { t } = useTranslation();
useEffect(() => {
onStart();
}, [onStart]);
return (
<Box direction="Column" gap="400">
<WaitingMessage message="Starting verification using emoji comparison..." />
<WaitingMessage message={t('Settings.verification_starting_emoji')} />
</Box>
);
}
function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
const { t } = useTranslation();
const [confirmState, confirm] = useAsyncCallback(useCallback(() => sasData.confirm(), [sasData]));
const confirming =
@ -124,7 +131,7 @@ function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
return (
<Box direction="Column" gap="400">
<Text>Confirm the emoji below are displayed on both devices, in the same order:</Text>
<Text>{t('Settings.verification_compare_emoji_prompt')}</Text>
<Box
className={ContainerColor({ variant: 'SurfaceVariant' })}
style={{
@ -157,7 +164,7 @@ function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
disabled={confirming}
before={confirming && <Spinner size="100" variant="Primary" />}
>
<Text size="B400">They Match</Text>
<Text size="B400">{t('Settings.verification_match')}</Text>
</Button>
<Button
variant="Primary"
@ -165,7 +172,7 @@ function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
onClick={() => sasData.mismatch()}
disabled={confirming}
>
<Text size="B400">Do not Match</Text>
<Text size="B400">{t('Settings.verification_no_match')}</Text>
</Button>
</Box>
</Box>
@ -177,6 +184,7 @@ type SasVerificationProps = {
onCancel: () => void;
};
function SasVerification({ verifier, onCancel }: SasVerificationProps) {
const { t } = useTranslation();
const [sasData, setSasData] = useState<ShowSasCallbacks>();
useVerifierShowSas(verifier, setSasData);
@ -192,7 +200,7 @@ function SasVerification({ verifier, onCancel }: SasVerificationProps) {
return (
<Box direction="Column" gap="400">
<WaitingMessage message="Starting verification using emoji comparison..." />
<WaitingMessage message={t('Settings.verification_starting_emoji')} />
</Box>
);
}
@ -201,13 +209,14 @@ type VerificationDoneProps = {
onExit: () => void;
};
function VerificationDone({ onExit }: VerificationDoneProps) {
const { t } = useTranslation();
return (
<Box direction="Column" gap="400">
<div>
<Text>Your device is verified.</Text>
<Text>{t('Settings.verification_device_verified')}</Text>
</div>
<Button variant="Primary" fill="Solid" onClick={onExit}>
<Text size="B400">Okay</Text>
<Text size="B400">{t('Settings.verification_okay')}</Text>
</Button>
</Box>
);
@ -217,11 +226,12 @@ type VerificationCanceledProps = {
onClose: () => void;
};
function VerificationCanceled({ onClose }: VerificationCanceledProps) {
const { t } = useTranslation();
return (
<Box direction="Column" gap="400">
<Text>Verification has been canceled.</Text>
<Text>{t('Settings.verification_canceled')}</Text>
<Button variant="Secondary" fill="Soft" onClick={onClose}>
<Text size="B400">Close</Text>
<Text size="B400">{t('Settings.verification_close')}</Text>
</Button>
</Box>
);
@ -232,6 +242,7 @@ type DeviceVerificationProps = {
onExit: () => void;
};
export function DeviceVerification({ request, onExit }: DeviceVerificationProps) {
const { t } = useTranslation();
const phase = useVerificationRequestPhase(request);
const handleCancel = useCallback(() => {
@ -259,7 +270,7 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps)
<Dialog variant="Surface">
<Header style={DialogHeaderStyles} variant="Surface" size="500">
<Box grow="Yes">
<Text size="H4">Device Verification</Text>
<Text size="H4">{t('Settings.device_verification')}</Text>
</Box>
<IconButton size="300" radii="300" onClick={handleCancel}>
<Icon src={Icons.Cross} />
@ -283,7 +294,7 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps)
<SasVerification verifier={request.verifier} onCancel={handleCancel} />
) : (
<VerificationUnexpected
message="Unexpected Error! Verification is started but verifier is missing."
message={t('Settings.verification_unexpected_error')}
onClose={handleCancel}
/>
))}

View file

@ -16,6 +16,7 @@ import {
import FileSaver from 'file-saver';
import to from 'await-to-js';
import { AuthDict, IAuthData, MatrixError, UIAuthCallback } from 'matrix-js-sdk';
import { useTranslation } from 'react-i18next';
import { PasswordInput } from './password-input';
import { ContainerColor } from '../styles/ContainerColor.css';
import { copyToClipboard } from '../utils/dom';
@ -71,6 +72,7 @@ type SetupVerificationProps = {
onComplete: (recoveryKey: string) => void;
};
function SetupVerification({ onComplete }: SetupVerificationProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const alive = useAlive();
@ -181,12 +183,9 @@ function SetupVerification({ onComplete }: SetupVerificationProps) {
return (
<Box as="form" onSubmit={handleSubmit} direction="Column" gap="400">
<Text size="T300">
Generate a <b>Recovery Key</b> for verifying identity if you do not have access to other
devices. Additionally, setup a passphrase as a memorable alternative.
</Text>
<Text size="T300">{t('Settings.verification_setup_desc')}</Text>
<Box direction="Column" gap="100">
<Text size="L400">Passphrase (Optional)</Text>
<Text size="L400">{t('Settings.verification_passphrase_optional')}</Text>
<PasswordInput name="passphraseInput" size="400" readOnly={loading} />
</Box>
<Button
@ -194,21 +193,19 @@ function SetupVerification({ onComplete }: SetupVerificationProps) {
disabled={loading}
before={loading && <Spinner size="200" variant="Primary" fill="Solid" />}
>
<Text size="B400">Continue</Text>
<Text size="B400">{t('Settings.verification_continue')}</Text>
</Button>
{setupState.status === AsyncStatus.Error && (
<Text size="T200" style={{ color: color.Critical.Main }}>
<b>{setupState.error ? setupState.error.message : 'Unexpected Error!'}</b>
<b>
{setupState.error ? setupState.error.message : t('Settings.verification_error_generic')}
</b>
</Text>
)}
{nextAuthData !== null && uiaAction && (
<ActionUIAFlowsLoader
authData={nextAuthData ?? uiaAction.authData}
unsupported={() => (
<Text size="T200">
Authentication steps to perform this action are not supported by client.
</Text>
)}
unsupported={() => <Text size="T200">{t('Settings.verification_uia_unsupported')}</Text>}
>
{(ongoingFlow) => (
<ActionUIA
@ -228,6 +225,7 @@ type RecoveryKeyDisplayProps = {
recoveryKey: string;
};
function RecoveryKeyDisplay({ recoveryKey }: RecoveryKeyDisplayProps) {
const { t } = useTranslation();
const [show, setShow] = useState(false);
const handleCopy = () => {
@ -245,12 +243,9 @@ function RecoveryKeyDisplay({ recoveryKey }: RecoveryKeyDisplayProps) {
return (
<Box direction="Column" gap="400">
<Text size="T300">
Store the Recovery Key in a safe place for future use, as you will need it to verify your
identity if you do not have access to other devices.
</Text>
<Text size="T300">{t('Settings.verification_recovery_key_store_desc')}</Text>
<Box direction="Column" gap="100">
<Text size="L400">Recovery Key</Text>
<Text size="L400">{t('Settings.verification_recovery_key')}</Text>
<Box
className={ContainerColor({ variant: 'SurfaceVariant' })}
style={{
@ -265,16 +260,18 @@ function RecoveryKeyDisplay({ recoveryKey }: RecoveryKeyDisplayProps) {
{safeToDisplayKey}
</Text>
<Chip onClick={() => setShow(!show)} variant="Secondary" radii="Pill">
<Text size="B300">{show ? 'Hide' : 'Show'}</Text>
<Text size="B300">
{show ? t('Settings.verification_hide') : t('Settings.verification_show')}
</Text>
</Chip>
</Box>
</Box>
<Box direction="Column" gap="200">
<Button onClick={handleCopy}>
<Text size="B400">Copy</Text>
<Text size="B400">{t('Settings.verification_copy')}</Text>
</Button>
<Button onClick={handleDownload} fill="Soft">
<Text size="B400">Download</Text>
<Text size="B400">{t('Settings.verification_download')}</Text>
</Button>
</Box>
</Box>
@ -286,6 +283,7 @@ type DeviceVerificationSetupProps = {
};
export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerificationSetupProps>(
({ onCancel }, ref) => {
const { t } = useTranslation();
const [recoveryKey, setRecoveryKey] = useState<string>();
return (
@ -299,7 +297,7 @@ export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerifica
size="500"
>
<Box grow="Yes">
<Text size="H4">Setup Device Verification</Text>
<Text size="H4">{t('Settings.verification_setup_title')}</Text>
</Box>
<IconButton size="300" radii="300" onClick={onCancel}>
<Icon src={Icons.Cross} />
@ -321,6 +319,7 @@ type DeviceVerificationResetProps = {
};
export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerificationResetProps>(
({ onCancel }, ref) => {
const { t } = useTranslation();
const [reset, setReset] = useState(false);
return (
@ -334,7 +333,7 @@ export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerifica
size="500"
>
<Box grow="Yes">
<Text size="H4">Reset Device Verification</Text>
<Text size="H4">{t('Settings.verification_reset_title')}</Text>
</Box>
<IconButton size="300" radii="300" onClick={onCancel}>
<Icon src={Icons.Cross} />
@ -356,16 +355,11 @@ export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerifica
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Box direction="Column" gap="200">
<Text size="H1">🧑🚒🤚</Text>
<Text size="T300">Resetting device verification is permanent.</Text>
<Text size="T300">
Anyone you have verified with will see security alerts and your encryption backup
will be lost. You almost certainly do not want to do this, unless you have lost{' '}
<b>Recovery Key</b> or <b>Recovery Passphrase</b> and every device you can verify
from.
</Text>
<Text size="T300">{t('Settings.verification_reset_permanent')}</Text>
<Text size="T300">{t('Settings.verification_reset_warning')}</Text>
</Box>
<Button variant="Critical" onClick={() => setReset(true)}>
<Text size="B400">Reset</Text>
<Text size="B400">{t('Settings.reset')}</Text>
</Button>
</Box>
)}

View file

@ -13,6 +13,7 @@ import {
color,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import { stopPropagation } from '../utils/keyboard';
import { SettingTile } from './setting-tile';
import { SecretStorageKeyContent } from '../../types/matrix/accountData';
@ -33,6 +34,7 @@ export function ManualVerificationMethodSwitcher({
value,
onChange,
}: ManualVerificationMethodSwitcherProps) {
const { t } = useTranslation();
const [menuCords, setMenuCords] = useState<RectCords>();
const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
@ -55,8 +57,10 @@ export function ManualVerificationMethodSwitcher({
onClick={handleMenu}
>
<Text as="span" size="B300">
{value === ManualVerificationMethod.RecoveryPassphrase && 'Recovery Passphrase'}
{value === ManualVerificationMethod.RecoveryKey && 'Recovery Key'}
{value === ManualVerificationMethod.RecoveryPassphrase &&
t('Settings.verification_recovery_passphrase')}
{value === ManualVerificationMethod.RecoveryKey &&
t('Settings.verification_recovery_key')}
</Text>
</Chip>
<PopOut
@ -87,7 +91,7 @@ export function ManualVerificationMethodSwitcher({
onClick={() => handleSelect(ManualVerificationMethod.RecoveryPassphrase)}
>
<Box grow="Yes">
<Text size="T300">Recovery Passphrase</Text>
<Text size="T300">{t('Settings.verification_recovery_passphrase')}</Text>
</Box>
</MenuItem>
<MenuItem
@ -98,7 +102,7 @@ export function ManualVerificationMethodSwitcher({
onClick={() => handleSelect(ManualVerificationMethod.RecoveryKey)}
>
<Box grow="Yes">
<Text size="T300">Recovery Key</Text>
<Text size="T300">{t('Settings.verification_recovery_key')}</Text>
</Box>
</MenuItem>
</Box>
@ -120,6 +124,7 @@ export function ManualVerificationTile({
secretStorageKeyContent,
options,
}: ManualVerificationTileProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const hasPassphrase = !!secretStorageKeyContent.passphrase;
@ -154,8 +159,12 @@ export function ManualVerificationTile({
return (
<Box direction="Column" gap="200">
<SettingTile
title="Verify Manually"
description={hasPassphrase ? 'Select a verification method.' : 'Provide recovery key.'}
title={t('Settings.verify_manually')}
description={
hasPassphrase
? t('Settings.verification_select_method')
: t('Settings.verification_provide_key')
}
after={
<Box alignItems="Center" gap="200">
{hasPassphrase && (
@ -167,7 +176,7 @@ export function ManualVerificationTile({
/>
{verifyState.status === AsyncStatus.Success ? (
<Text size="T200" style={{ color: color.Success.Main }}>
<b>Device verified!</b>
<b>{t('Settings.verification_verified_success')}</b>
</Text>
) : (
<Box direction="Column" gap="100">

View file

@ -18,6 +18,7 @@ import {
toRem,
} from 'folds';
import React, { MouseEventHandler, ReactNode, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactEditor, useSlate } from 'slate-react';
import {
headingLevel,
@ -119,6 +120,7 @@ export function BlockButton({ format, icon, tooltip }: BlockButtonProps) {
}
export function HeadingBlockButton() {
const { t } = useTranslation();
const editor = useSlate();
const level = headingLevel(editor);
const [anchor, setAnchor] = useState<RectCords>();
@ -158,7 +160,12 @@ export function HeadingBlockButton() {
<Menu style={{ padding: config.space.S100 }}>
<Box gap="100">
<TooltipProvider
tooltip={<BtnTooltip text="Heading 1" shortCode={`${modKey} + 1`} />}
tooltip={
<BtnTooltip
text={t('Room.format_heading', { level: 1 })}
shortCode={`${modKey} + 1`}
/>
}
delay={500}
>
{(triggerRef) => (
@ -173,7 +180,12 @@ export function HeadingBlockButton() {
)}
</TooltipProvider>
<TooltipProvider
tooltip={<BtnTooltip text="Heading 2" shortCode={`${modKey} + 2`} />}
tooltip={
<BtnTooltip
text={t('Room.format_heading', { level: 2 })}
shortCode={`${modKey} + 2`}
/>
}
delay={500}
>
{(triggerRef) => (
@ -188,7 +200,12 @@ export function HeadingBlockButton() {
)}
</TooltipProvider>
<TooltipProvider
tooltip={<BtnTooltip text="Heading 3" shortCode={`${modKey} + 3`} />}
tooltip={
<BtnTooltip
text={t('Room.format_heading', { level: 3 })}
shortCode={`${modKey} + 3`}
/>
}
delay={500}
>
{(triggerRef) => (
@ -224,6 +241,7 @@ export function HeadingBlockButton() {
type ExitFormattingProps = { tooltip: ReactNode };
export function ExitFormatting({ tooltip }: ExitFormattingProps) {
const { t } = useTranslation();
const editor = useSlate();
const handleClick = () => {
@ -245,7 +263,7 @@ export function ExitFormatting({ tooltip }: ExitFormattingProps) {
size="400"
radii="300"
>
<Text size="B400">{`Exit ${KeySymbol.Hyper}`}</Text>
<Text size="B400">{`${t('Room.format_exit')} ${KeySymbol.Hyper}`}</Text>
</IconButton>
)}
</TooltipProvider>
@ -253,6 +271,7 @@ export function ExitFormatting({ tooltip }: ExitFormattingProps) {
}
export function Toolbar() {
const { t } = useTranslation();
const editor = useSlate();
const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
const disableInline = isBlockActive(editor, BlockType.CodeBlock);
@ -271,32 +290,38 @@ export function Toolbar() {
<MarkButton
format={MarkType.Bold}
icon={Icons.Bold}
tooltip={<BtnTooltip text="Bold" shortCode={`${modKey} + B`} />}
tooltip={<BtnTooltip text={t('Room.format_bold')} shortCode={`${modKey} + B`} />}
/>
<MarkButton
format={MarkType.Italic}
icon={Icons.Italic}
tooltip={<BtnTooltip text="Italic" shortCode={`${modKey} + I`} />}
tooltip={<BtnTooltip text={t('Room.format_italic')} shortCode={`${modKey} + I`} />}
/>
<MarkButton
format={MarkType.Underline}
icon={Icons.Underline}
tooltip={<BtnTooltip text="Underline" shortCode={`${modKey} + U`} />}
tooltip={
<BtnTooltip text={t('Room.format_underline')} shortCode={`${modKey} + U`} />
}
/>
<MarkButton
format={MarkType.StrikeThrough}
icon={Icons.Strike}
tooltip={<BtnTooltip text="Strike Through" shortCode={`${modKey} + S`} />}
tooltip={
<BtnTooltip text={t('Room.format_strikethrough')} shortCode={`${modKey} + S`} />
}
/>
<MarkButton
format={MarkType.Code}
icon={Icons.Code}
tooltip={<BtnTooltip text="Inline Code" shortCode={`${modKey} + [`} />}
tooltip={
<BtnTooltip text={t('Room.format_inline_code')} shortCode={`${modKey} + [`} />
}
/>
<MarkButton
format={MarkType.Spoiler}
icon={Icons.EyeBlind}
tooltip={<BtnTooltip text="Spoiler" shortCode={`${modKey} + H`} />}
tooltip={<BtnTooltip text={t('Room.format_spoiler')} shortCode={`${modKey} + H`} />}
/>
</Box>
<Line variant="SurfaceVariant" direction="Vertical" style={{ height: toRem(12) }} />
@ -305,22 +330,30 @@ export function Toolbar() {
<BlockButton
format={BlockType.BlockQuote}
icon={Icons.BlockQuote}
tooltip={<BtnTooltip text="Block Quote" shortCode={`${modKey} + '`} />}
tooltip={
<BtnTooltip text={t('Room.format_block_quote')} shortCode={`${modKey} + '`} />
}
/>
<BlockButton
format={BlockType.CodeBlock}
icon={Icons.BlockCode}
tooltip={<BtnTooltip text="Block Code" shortCode={`${modKey} + ;`} />}
tooltip={
<BtnTooltip text={t('Room.format_block_code')} shortCode={`${modKey} + ;`} />
}
/>
<BlockButton
format={BlockType.OrderedList}
icon={Icons.OrderList}
tooltip={<BtnTooltip text="Ordered List" shortCode={`${modKey} + 7`} />}
tooltip={
<BtnTooltip text={t('Room.format_ordered_list')} shortCode={`${modKey} + 7`} />
}
/>
<BlockButton
format={BlockType.UnorderedList}
icon={Icons.UnorderList}
tooltip={<BtnTooltip text="Unordered List" shortCode={`${modKey} + 8`} />}
tooltip={
<BtnTooltip text={t('Room.format_unordered_list')} shortCode={`${modKey} + 8`} />
}
/>
<HeadingBlockButton />
</Box>
@ -330,7 +363,10 @@ export function Toolbar() {
<Box shrink="No" gap="100">
<ExitFormatting
tooltip={
<BtnTooltip text="Exit Formatting" shortCode={`Escape, ${modKey} + E`} />
<BtnTooltip
text={t('Room.format_exit_formatting')}
shortCode={`Escape, ${modKey} + E`}
/>
}
/>
</Box>
@ -339,7 +375,15 @@ export function Toolbar() {
<Box className={css.MarkdownBtnBox} shrink="No" grow="Yes" justifyContent="End">
<TooltipProvider
align="End"
tooltip={<BtnTooltip text={isMarkdown ? 'Disable Markdown' : 'Enable Markdown'} />}
tooltip={
<BtnTooltip
text={
isMarkdown
? t('Room.format_markdown_disable')
: t('Room.format_markdown_enable')
}
/>
}
delay={500}
>
{(triggerRef) => (

View file

@ -17,6 +17,7 @@ import {
Spinner,
} from 'folds';
import { MatrixError } from 'matrix-js-sdk';
import { useTranslation } from 'react-i18next';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { stopPropagation } from '../../utils/keyboard';
@ -27,6 +28,7 @@ type LeaveSpacePromptProps = {
onCancel: () => void;
};
export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const [leaveState, leaveRoom] = useAsyncCallback<undefined, MatrixError, []>(
@ -66,7 +68,7 @@ export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptP
size="500"
>
<Box grow="Yes">
<Text size="H4">Leave Space</Text>
<Text size="H4">{t('Room.leave_space_title')}</Text>
</Box>
<IconButton size="300" onClick={onCancel} radii="300">
<Icon src={Icons.Cross} />
@ -74,10 +76,10 @@ export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptP
</Header>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Box direction="Column" gap="200">
<Text priority="400">Are you sure you want to leave this space?</Text>
<Text priority="400">{t('Room.leave_space_confirm')}</Text>
{leaveState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} size="T300">
Failed to leave space! {leaveState.error.message}
{t('Room.leave_space_error', { error: leaveState.error.message })}
</Text>
)}
</Box>
@ -96,7 +98,7 @@ export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptP
}
>
<Text size="B400">
{leaveState.status === AsyncStatus.Loading ? 'Leaving...' : 'Leave'}
{leaveState.status === AsyncStatus.Loading ? t('Room.leaving') : t('Room.leave')}
</Text>
</Button>
</Box>

View file

@ -1,6 +1,7 @@
import React, { useEffect, useCallback, FormEventHandler } from 'react';
import { Dialog, Text, Box, Button, config, Input, color, Spinner } from 'folds';
import { AuthType, MatrixError } from 'matrix-js-sdk';
import { useTranslation } from 'react-i18next';
import { StageComponentProps } from './types';
import { AsyncState, AsyncStatus } from '../../hooks/useAsyncCallback';
import { RequestEmailTokenCallback, RequestEmailTokenResponse } from '../../hooks/types';
@ -18,13 +19,14 @@ function EmailErrorDialog({
onRetry: (email: string) => void;
onCancel: () => void;
}) {
const { t } = useTranslation();
const handleFormSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
evt.preventDefault();
const { retryEmailInput } = evt.target as HTMLFormElement & {
retryEmailInput: HTMLInputElement;
};
const t = retryEmailInput.value;
onRetry(t);
onRetry(retryEmailInput.value);
};
return (
@ -40,7 +42,7 @@ function EmailErrorDialog({
<Text size="H4">{title}</Text>
<Text>{message}</Text>
<Text as="label" size="L400" style={{ paddingTop: config.space.S400 }}>
Email
{t('Auth.uia_email_label')}
</Text>
<Input
name="retryEmailInput"
@ -53,12 +55,12 @@ function EmailErrorDialog({
</Box>
<Button variant="Primary" type="submit">
<Text as="span" size="B400">
Send Verification Email
{t('Auth.uia_email_send_button')}
</Text>
</Button>
<Button variant="Critical" fill="None" outlined type="button" onClick={onCancel}>
<Text as="span" size="B400">
Cancel
{t('Auth.uia_email_cancel')}
</Text>
</Button>
</Box>
@ -80,6 +82,7 @@ export function EmailStageDialog({
emailTokenState: AsyncState<RequestEmailTokenResponse, MatrixError>;
requestEmailToken: RequestEmailTokenCallback;
}) {
const { t } = useTranslation();
const { errorCode, error, session } = stageData;
const handleSubmit = useCallback(
@ -115,7 +118,7 @@ export function EmailStageDialog({
return (
<Box direction="Column" alignItems="Center" gap="400">
<Spinner variant="Secondary" size="600" />
<Text style={{ color: color.Secondary.Main }}>Sending verification email...</Text>
<Text style={{ color: color.Secondary.Main }}>{t('Auth.uia_email_sending')}</Text>
</Box>
);
}
@ -123,11 +126,11 @@ export function EmailStageDialog({
if (emailTokenState.status === AsyncStatus.Error) {
return (
<EmailErrorDialog
title={emailTokenState.error.errcode ?? 'Verify Email'}
title={emailTokenState.error.errcode ?? t('Auth.uia_email_verify_title')}
message={
emailTokenState.error?.data?.error ??
emailTokenState.error.message ??
'Failed to send verification Email request.'
t('Auth.uia_email_send_error')
}
onRetry={handleEmailSubmit}
onCancel={onCancel}
@ -140,8 +143,8 @@ export function EmailStageDialog({
<Dialog>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Box direction="Column" gap="100">
<Text size="H4">Verification Request Sent</Text>
<Text>{`Please check your email "${emailTokenState.data.email}" and validate before continuing further.`}</Text>
<Text size="H4">{t('Auth.uia_email_sent_title')}</Text>
<Text>{t('Auth.uia_email_sent_message', { email: emailTokenState.data.email })}</Text>
{errorCode && (
<Text style={{ color: color.Critical.Main }}>{`${errorCode}: ${error}`}</Text>
@ -149,7 +152,7 @@ export function EmailStageDialog({
</Box>
<Button variant="Primary" onClick={() => handleSubmit(emailTokenState.data.result.sid)}>
<Text as="span" size="B400">
Continue
{t('Auth.uia_email_continue')}
</Text>
</Button>
</Box>
@ -160,8 +163,8 @@ export function EmailStageDialog({
if (!email) {
return (
<EmailErrorDialog
title="Provide Email"
message="Please provide email to send verification request."
title={t('Auth.uia_email_provide_title')}
message={t('Auth.uia_email_provide_message')}
onRetry={handleEmailSubmit}
onCancel={onCancel}
/>

View file

@ -1,5 +1,6 @@
import { Box, Button, color, config, Icon, Icons, Spinner, Text, Input } from 'folds';
import React, { useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useRoom } from '../../hooks/useRoom';
import { CutoutCard } from '../cutout-card';
import { SettingTile } from '../setting-tile';
@ -14,6 +15,7 @@ type UserKickAlertProps = {
ts?: number;
};
export function UserKickAlert({ reason, kickedBy, ts }: UserKickAlertProps) {
const { t } = useTranslation();
const time = ts ? timeHourMinute(ts) : undefined;
const date = ts ? timeDayMonYear(ts) : undefined;
@ -22,7 +24,7 @@ export function UserKickAlert({ reason, kickedBy, ts }: UserKickAlertProps) {
<SettingTile>
<Box direction="Column" gap="200">
<Box gap="200" justifyContent="SpaceBetween">
<Text size="L400">Kicked User</Text>
<Text size="L400">{t('User.moderation_kicked_user')}</Text>
{time && date && (
<Text size="T200">
{date} {time}
@ -32,16 +34,16 @@ export function UserKickAlert({ reason, kickedBy, ts }: UserKickAlertProps) {
<Box direction="Column">
{kickedBy && (
<Text size="T200">
Kicked by: <b>{kickedBy}</b>
{t('User.moderation_kicked_by')} <b>{kickedBy}</b>
</Text>
)}
<Text size="T200">
{reason ? (
<>
Reason: <b>{reason}</b>
{t('User.moderation_reason_label')} <b>{reason}</b>
</>
) : (
<i>No Reason Provided.</i>
<i>{t('User.moderation_no_reason')}</i>
)}
</Text>
</Box>
@ -59,6 +61,7 @@ type UserBanAlertProps = {
ts?: number;
};
export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBanAlertProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const room = useRoom();
const time = ts ? timeHourMinute(ts) : undefined;
@ -77,7 +80,7 @@ export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBan
<SettingTile>
<Box direction="Column" gap="200">
<Box gap="200" justifyContent="SpaceBetween">
<Text size="L400">Banned User</Text>
<Text size="L400">{t('User.moderation_banned_user')}</Text>
{time && date && (
<Text size="T200">
{date} {time}
@ -87,16 +90,16 @@ export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBan
<Box direction="Column">
{bannedBy && (
<Text size="T200">
Banned by: <b>{bannedBy}</b>
{t('User.moderation_banned_by')} <b>{bannedBy}</b>
</Text>
)}
<Text size="T200">
{reason ? (
<>
Reason: <b>{reason}</b>
{t('User.moderation_reason_label')} <b>{reason}</b>
</>
) : (
<i>No Reason Provided.</i>
<i>{t('User.moderation_no_reason')}</i>
)}
</Text>
</Box>
@ -114,7 +117,7 @@ export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBan
before={banning && <Spinner size="100" variant="Critical" fill="Solid" />}
disabled={banning}
>
<Text size="B300">Unban</Text>
<Text size="B300">{t('User.moderation_unban')}</Text>
</Button>
)}
</Box>
@ -131,6 +134,7 @@ type UserInviteAlertProps = {
ts?: number;
};
export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: UserInviteAlertProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const room = useRoom();
const time = ts ? timeHourMinute(ts) : undefined;
@ -149,7 +153,7 @@ export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: User
<SettingTile>
<Box direction="Column" gap="200">
<Box gap="200" justifyContent="SpaceBetween">
<Text size="L400">Invited User</Text>
<Text size="L400">{t('User.moderation_invited_user')}</Text>
{time && date && (
<Text size="T200">
{date} {time}
@ -159,16 +163,16 @@ export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: User
<Box direction="Column">
{invitedBy && (
<Text size="T200">
Invited by: <b>{invitedBy}</b>
{t('User.moderation_invited_by')} <b>{invitedBy}</b>
</Text>
)}
<Text size="T200">
{reason ? (
<>
Reason: <b>{reason}</b>
{t('User.moderation_reason_label')} <b>{reason}</b>
</>
) : (
<i>No Reason Provided.</i>
<i>{t('User.moderation_no_reason')}</i>
)}
</Text>
</Box>
@ -188,7 +192,7 @@ export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: User
before={kicking && <Spinner size="100" variant="Success" fill="Soft" />}
disabled={kicking}
>
<Text size="B300">Cancel Invite</Text>
<Text size="B300">{t('User.moderation_cancel_invite')}</Text>
</Button>
)}
</Box>
@ -204,6 +208,7 @@ type UserModerationProps = {
canInvite: boolean;
};
export function UserModeration({ userId, canKick, canBan, canInvite }: UserModerationProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const room = useRoom();
const reasonInputRef = useRef<HTMLInputElement>(null);
@ -245,10 +250,10 @@ export function UserModeration({ userId, canKick, canBan, canInvite }: UserModer
<Box direction="Column" gap="400">
<Box direction="Column" gap="200">
<Box grow="Yes" direction="Column" gap="100">
<Text size="L400">Moderation</Text>
<Text size="L400">{t('User.moderation_title')}</Text>
<Input
ref={reasonInputRef}
placeholder="Reason"
placeholder={t('User.moderation_reason')}
size="300"
variant="Background"
radii="300"
@ -288,7 +293,7 @@ export function UserModeration({ userId, canKick, canBan, canInvite }: UserModer
onClick={invite}
disabled={disabled}
>
<Text size="B300">Invite</Text>
<Text size="B300">{t('User.moderation_invite')}</Text>
</Button>
)}
{canKick && (
@ -308,7 +313,7 @@ export function UserModeration({ userId, canKick, canBan, canInvite }: UserModer
onClick={kick}
disabled={disabled}
>
<Text size="B300">Kick</Text>
<Text size="B300">{t('User.moderation_kick')}</Text>
</Button>
)}
{canBan && (
@ -328,7 +333,7 @@ export function UserModeration({ userId, canKick, canBan, canInvite }: UserModer
onClick={ban}
disabled={disabled}
>
<Text size="B300">Ban</Text>
<Text size="B300">{t('User.moderation_ban')}</Text>
</Button>
)}
</Box>

View file

@ -15,6 +15,7 @@ import {
toRem,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import { SequenceCard } from '../../components/sequence-card';
import * as css from './styles.css';
import {
@ -33,6 +34,7 @@ type CallControlsProps = {
callEmbed: CallEmbed;
};
export function CallControls({ callEmbed }: CallControlsProps) {
const { t } = useTranslation();
const controlRef = useRef<HTMLDivElement>(null);
const [compact, setCompact] = useState(document.body.clientWidth < 500);
@ -140,7 +142,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
onClick={handleSpotlightClick}
>
<Text size="B300" truncate>
{spotlight ? 'Grid View' : 'Spotlight View'}
{spotlight ? t('Call.view_grid') : t('Call.view_spotlight')}
</Text>
</MenuItem>
<MenuItem
@ -150,7 +152,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
onClick={handleReactionsClick}
>
<Text size="B300" truncate>
Reactions
{t('Call.reactions')}
</Text>
</MenuItem>
</>
@ -162,7 +164,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
onClick={handleSettingsClick}
>
<Text size="B300" truncate>
Settings
{t('Call.settings')}
</Text>
</MenuItem>
</Box>
@ -198,7 +200,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
}
disabled={exiting}
>
<Text size="B400">End</Text>
<Text size="B400">{t('Call.end_call')}</Text>
</Button>
</Box>
</Box>

View file

@ -1,6 +1,7 @@
import { CallMembership, SessionMembershipData } from 'matrix-js-sdk/lib/matrixrtc';
import React, { useState } from 'react';
import { Avatar, Box, Icon, Icons, Text } from 'folds';
import { useTranslation } from 'react-i18next';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
@ -84,6 +85,7 @@ export function CallMemberRenderer({
members: CallMembership[];
max?: number;
}) {
const { t } = useTranslation();
const [viewMore, setViewMore] = useState(false);
const truncatedMembers = viewMore ? members : members.slice(0, 4);
@ -105,11 +107,11 @@ export function CallMemberRenderer({
<Box grow="Yes" gap="300" alignItems="Center">
{viewMore ? (
<Text size="L400" truncate>
Collapse
{t('Call.collapse')}
</Text>
) : (
<Text size="L400" truncate>
{remaining === 0 ? `+${remaining} Other` : `+${remaining} Others`}
{t('Call.others_count', { count: remaining })}
</Text>
)}
</Box>

View file

@ -1,5 +1,6 @@
import React, { RefObject, useRef } from 'react';
import { Badge, Box, color, Header, Scroll, Text, toRem } from 'folds';
import { useTranslation } from 'react-i18next';
import { useCallEmbed, useCallJoined, useCallEmbedPlacementSync } from '../../hooks/useCallEmbed';
import { ContainerColor } from '../../styles/ContainerColor.css';
import { PrescreenControls } from './PrescreenControls';
@ -16,9 +17,10 @@ import { CallControls } from './CallControls';
import { useLivekitSupport } from '../../hooks/useLivekitSupport';
function LivekitServerMissingMessage() {
const { t } = useTranslation();
return (
<Text style={{ margin: 'auto', color: color.Critical.Main }} size="L400" align="Center">
Your homeserver does not support calling. But you can still join call started by others.
{t('Call.homeserver_no_calls')}
</Text>
);
}
@ -30,6 +32,7 @@ function JoinMessage({
hasParticipant?: boolean;
livekitSupported?: boolean;
}) {
const { t } = useTranslation();
if (hasParticipant) return null;
if (livekitSupported === false) {
@ -38,28 +41,31 @@ function JoinMessage({
return (
<Text style={{ margin: 'auto' }} size="L400" align="Center">
Voice chats empty Be the first to hop in!
{t('Call.empty_be_first')}
</Text>
);
}
function NoPermissionMessage() {
const { t } = useTranslation();
return (
<Text style={{ margin: 'auto' }} size="L400" align="Center">
You don&#39;t have permission to join!
{t('Call.no_permission_to_join')}
</Text>
);
}
function AlreadyInCallMessage() {
const { t } = useTranslation();
return (
<Text style={{ margin: 'auto', color: color.Warning.Main }} size="L400" align="Center">
Already in another call End the current call to join!
{t('Call.already_in_other_call')}
</Text>
);
}
function CallPrescreen() {
const { t } = useTranslation();
const mx = useMatrixClient();
const room = useRoom();
const livekitSupported = useLivekitSupport();
@ -86,11 +92,11 @@ function CallPrescreen() {
{hasParticipant && (
<Header size="300">
<Box grow="Yes" alignItems="Center">
<Text size="L400">Participant</Text>
<Text size="L400">{t('Call.participant')}</Text>
</Box>
<Badge variant="Critical" fill="Solid" size="400">
<Text as="span" size="L400" truncate>
{callMembers.length} Live
{t('Call.live_count', { count: callMembers.length })}
</Text>
</Badge>
</Header>

View file

@ -1,5 +1,6 @@
import React from 'react';
import { Box, Button, Icon, Icons, Spinner, Text } from 'folds';
import { useTranslation } from 'react-i18next';
import { SequenceCard } from '../../components/sequence-card';
import * as css from './styles.css';
import { ChatButton, ControlDivider, MicrophoneButton, VideoButton } from './Controls';
@ -11,6 +12,7 @@ type PrescreenControlsProps = {
canJoin?: boolean;
};
export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
const { t } = useTranslation();
const room = useRoom();
const callEmbed = useCallEmbed();
const callJoined = useCallJoined(callEmbed);
@ -57,7 +59,7 @@ export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
)
}
>
<Text size="B400">Join</Text>
<Text size="B400">{t('Call.join')}</Text>
</Button>
</Box>
</SequenceCard>

View file

@ -3,6 +3,7 @@ import { Box, Chip, Icon, IconButton, Icons, Line, Scroll, Spinner, Text, config
import { useVirtualizer } from '@tanstack/react-virtual';
import { useAtom, useAtomValue } from 'jotai';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { JoinRule, RestrictedAllowType, Room } from 'matrix-js-sdk';
import type { AccountDataEvents, StateEvents } from 'matrix-js-sdk';
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
@ -150,6 +151,7 @@ const useCanDropLobbyItem = (
};
export function Lobby() {
const { t } = useTranslation();
const navigate = useNavigate();
const mx = useMatrixClient();
const mDirects = useAtomValue(mDirectAtom);
@ -459,7 +461,7 @@ export function Lobby() {
radii="Pill"
outlined
size="300"
aria-label="Scroll to Top"
aria-label={t('Room.lobby_scroll_to_top')}
>
<Icon src={Icons.ChevronTop} size="300" />
</IconButton>
@ -535,7 +537,7 @@ export function Lobby() {
radii="Pill"
before={<Spinner variant="Secondary" fill="Soft" size="100" />}
>
<Text size="L400">Reordering</Text>
<Text size="L400">{t('Room.lobby_reordering')}</Text>
</Chip>
</Box>
)}

View file

@ -17,6 +17,7 @@ import {
toRem,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import { PageHeader } from '../../components/page';
import { useSetSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
@ -45,6 +46,7 @@ type LobbyMenuProps = {
};
const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
({ powerLevels, requestClose }, ref) => {
const { t } = useTranslation();
const mx = useMatrixClient();
const space = useSpace();
const creators = useRoomCreators(space);
@ -87,7 +89,7 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
disabled={!canInvite}
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Invite
{t('Room.invite')}
</Text>
</MenuItem>
<MenuItem
@ -97,7 +99,7 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
radii="300"
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Space Settings
{t('Room.lobby_space_settings')}
</Text>
</MenuItem>
</Box>
@ -116,7 +118,7 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
aria-pressed={promptLeave}
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Leave Space
{t('Room.lobby_leave_space')}
</Text>
</MenuItem>
{promptLeave && (
@ -140,6 +142,7 @@ type LobbyHeaderProps = {
powerLevels: IPowerLevels;
};
export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const space = useSpace();
@ -213,7 +216,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
offset={4}
tooltip={
<Tooltip>
<Text>Members</Text>
<Text>{t('Room.members')}</Text>
</Tooltip>
}
>
@ -234,7 +237,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
offset={4}
tooltip={
<Tooltip>
<Text>More Options</Text>
<Text>{t('Room.more_options')}</Text>
</Tooltip>
}
>

View file

@ -19,6 +19,7 @@ import {
toRem,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import { JoinRule, MatrixError, Room } from 'matrix-js-sdk';
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
@ -44,6 +45,7 @@ type RoomJoinButtonProps = {
via?: string[];
};
function RoomJoinButton({ roomId, via }: RoomJoinButtonProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
@ -91,7 +93,7 @@ function RoomJoinButton({ roomId, via }: RoomJoinButtonProps) {
onClick={join}
disabled={!canJoin}
>
<Text size="B300">Join</Text>
<Text size="B300">{t('Room.lobby_join')}</Text>
</Chip>
</Box>
);
@ -127,6 +129,7 @@ type RoomProfileErrorProps = {
via?: string[];
};
function RoomProfileError({ roomId, suggested, inaccessibleRoom, via }: RoomProfileErrorProps) {
const { t } = useTranslation();
return (
<Box grow="Yes" gap="300">
<Avatar>
@ -146,12 +149,12 @@ function RoomProfileError({ roomId, suggested, inaccessibleRoom, via }: RoomProf
<Box grow="Yes" direction="Column" className={css.ErrorNameContainer}>
<Box gap="200" alignItems="Center">
<Text size="H5" truncate>
Unknown
{t('Room.lobby_unknown')}
</Text>
{suggested && (
<Box shrink="No" alignItems="Center">
<Badge variant="Success" fill="Soft" radii="Pill" outlined>
<Text size="L400">Suggested</Text>
<Text size="L400">{t('Room.lobby_suggested')}</Text>
</Badge>
</Box>
)}
@ -159,7 +162,7 @@ function RoomProfileError({ roomId, suggested, inaccessibleRoom, via }: RoomProf
<Box gap="200" alignItems="Center">
{inaccessibleRoom ? (
<Badge variant="Secondary" fill="Soft" radii="300" size="500">
<Text size="L400">Inaccessible</Text>
<Text size="L400">{t('Room.lobby_inaccessible')}</Text>
</Badge>
) : (
<Text size="T200" truncate>
@ -195,6 +198,7 @@ function RoomProfile({
joinRule,
options,
}: RoomProfileProps) {
const { t } = useTranslation();
return (
<Box grow="Yes" gap="300">
<Avatar>
@ -213,7 +217,7 @@ function RoomProfile({
{suggested && (
<Box shrink="No" alignItems="Center">
<Badge variant="Success" fill="Soft" radii="Pill" outlined>
<Text size="L400">Suggested</Text>
<Text size="L400">{t('Room.lobby_suggested')}</Text>
</Badge>
</Box>
)}
@ -221,7 +225,12 @@ function RoomProfile({
<Box gap="200" alignItems="Center">
{memberCount && (
<Box shrink="No" gap="200">
<Text size="T200" priority="300">{`${millify(memberCount)} Members`}</Text>
<Text size="T200" priority="300">
{t('Room.lobby_members_count', {
count: memberCount,
formattedCount: millify(memberCount),
})}
</Text>
</Box>
)}
{memberCount && topic && (
@ -311,6 +320,7 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
},
ref
) => {
const { t } = useTranslation();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const { roomId, content } = item;
@ -359,7 +369,7 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
fill="None"
size="400"
radii="Pill"
aria-label="Open Room"
aria-label={t('Room.lobby_open_room')}
>
<Icon size="50" src={Icons.ArrowRight} />
</Chip>

View file

@ -60,6 +60,7 @@ type InaccessibleSpaceProfileProps = {
suggested?: boolean;
};
function InaccessibleSpaceProfile({ roomId, suggested }: InaccessibleSpaceProfileProps) {
const { t } = useTranslation();
return (
<Chip
as="span"
@ -72,7 +73,7 @@ function InaccessibleSpaceProfile({ roomId, suggested }: InaccessibleSpaceProfil
roomId={roomId}
renderFallback={() => (
<Text as="span" size="H6">
U
{nameInitials(t('Room.lobby_unknown'))}
</Text>
)}
/>
@ -81,15 +82,15 @@ function InaccessibleSpaceProfile({ roomId, suggested }: InaccessibleSpaceProfil
>
<Box alignItems="Center" gap="200">
<Text size="H4" truncate>
Unknown
{t('Room.lobby_unknown')}
</Text>
<Badge variant="Secondary" fill="Soft" radii="Pill" outlined>
<Text size="L400">Inaccessible</Text>
<Text size="L400">{t('Room.lobby_inaccessible')}</Text>
</Badge>
{suggested && (
<Badge variant="Success" fill="Soft" radii="Pill" outlined>
<Text size="L400">Suggested</Text>
<Text size="L400">{t('Room.lobby_suggested')}</Text>
</Badge>
)}
</Box>
@ -111,6 +112,7 @@ function UnjoinedSpaceProfile({
avatarUrl,
suggested,
}: UnjoinedSpaceProfileProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
@ -145,11 +147,11 @@ function UnjoinedSpaceProfile({
>
<Box alignItems="Center" gap="200">
<Text size="H4" truncate>
{name || 'Unknown'}
{name || t('Room.lobby_unknown')}
</Text>
{suggested && (
<Badge variant="Success" fill="Soft" radii="Pill" outlined>
<Text size="L400">Suggested</Text>
<Text size="L400">{t('Room.lobby_suggested')}</Text>
</Badge>
)}
{joinState.status === AsyncStatus.Error && (
@ -182,6 +184,7 @@ function SpaceProfile({
categoryId,
handleClose,
}: SpaceProfileProps) {
const { t } = useTranslation();
return (
<Chip
data-category-id={categoryId}
@ -211,7 +214,7 @@ function SpaceProfile({
</Text>
{suggested && (
<Badge variant="Success" fill="Soft" radii="Pill" outlined>
<Text size="L400">Suggested</Text>
<Text size="L400">{t('Room.lobby_suggested')}</Text>
</Badge>
)}
</Box>
@ -225,6 +228,7 @@ type RootSpaceProfileProps = {
handleClose?: MouseEventHandler<HTMLButtonElement>;
};
function RootSpaceProfile({ closed, categoryId, handleClose }: RootSpaceProfileProps) {
const { t } = useTranslation();
return (
<Chip
data-category-id={categoryId}
@ -236,7 +240,7 @@ function RootSpaceProfile({ closed, categoryId, handleClose }: RootSpaceProfileP
>
<Box alignItems="Center" gap="200">
<Text size="H4" truncate>
Rooms
{t('Room.lobby_rooms')}
</Text>
</Box>
</Chip>

View file

@ -198,7 +198,7 @@ export function MessageSearch({
radii="Pill"
outlined
size="300"
aria-label="Scroll to Top"
aria-label={t('Room.msg_search_scroll_to_top')}
>
<Icon src={Icons.ChevronTop} size="300" />
</IconButton>

View file

@ -4,6 +4,7 @@ import { EventType } from 'matrix-js-sdk';
import { ReactEditor } from 'slate-react';
import { isKeyHotkey } from 'is-hotkey';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { useStateEvent } from '../../hooks/useStateEvent';
import { StateEvent } from '../../../types/matrix/room';
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
@ -54,6 +55,7 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
};
export function RoomView({ eventId }: { eventId?: string }) {
const { t } = useTranslation();
const roomInputRef = useRef<HTMLDivElement>(null);
const roomViewRef = useRef<HTMLDivElement>(null);
const composerWrapRef = useRef<HTMLDivElement>(null);
@ -200,7 +202,7 @@ export function RoomView({ eventId }: { eventId?: string }) {
alignItems="Center"
justifyContent="Center"
>
<Text align="Center">You do not have permission to post in this room</Text>
<Text align="Center">{t('Room.no_post_permission')}</Text>
</RoomInputPlaceholder>
)}
</>

View file

@ -85,10 +85,14 @@ function LearnStartVerificationFromOtherDevice() {
<ul style={{ margin: `${config.space.S100} 0` }}>
<li>{t('Settings.verify_step_1')}</li>
<li>
Open <i>{t('Settings.verify_step_2_settings')}</i>.
{t('Settings.verification_step_open', {
settings: t('Settings.verify_step_2_settings'),
})}
</li>
<li>
Find this device in <i>{t('Settings.verify_step_3_section')}</i> section.
{t('Settings.verification_step_find', {
section: t('Settings.verify_step_3_section'),
})}
</li>
<li>{t('Settings.verify_step_4')}</li>
</ul>