From c7dc1d51dece71572bf302222ec1b5f310f0d79d Mon Sep 17 00:00:00 2001 From: heaven Date: Mon, 15 Jun 2026 02:57:32 +0300 Subject: [PATCH] feat(i18n): localise hardcoded English across calls, lobby, device verification, moderation, editor and email stage, syncing en/ru keys --- docs/ai/i18n.md | 26 ++- public/locales/en.json | 195 +++++++++++++---- public/locales/ru.json | 206 ++++++++++++++---- src/app/components/BackupRestore.tsx | 47 ++-- src/app/components/DeviceVerification.tsx | 47 ++-- .../components/DeviceVerificationSetup.tsx | 54 ++--- src/app/components/ManualVerification.tsx | 23 +- src/app/components/editor/Toolbar.tsx | 76 +++++-- .../leave-space-prompt/LeaveSpacePrompt.tsx | 10 +- src/app/components/uia-stages/EmailStage.tsx | 29 +-- .../user-profile/UserModeration.tsx | 43 ++-- src/app/features/call/CallControls.tsx | 10 +- src/app/features/call/CallMemberCard.tsx | 6 +- src/app/features/call/CallView.tsx | 18 +- src/app/features/call/PrescreenControls.tsx | 4 +- src/app/features/lobby/Lobby.tsx | 6 +- src/app/features/lobby/LobbyHeader.tsx | 13 +- src/app/features/lobby/RoomItem.tsx | 24 +- src/app/features/lobby/SpaceItem.tsx | 20 +- .../features/message-search/MessageSearch.tsx | 2 +- src/app/features/room/RoomView.tsx | 4 +- .../settings/devices/Verification.tsx | 8 +- 22 files changed, 619 insertions(+), 252 deletions(-) diff --git a/docs/ai/i18n.md b/docs/ai/i18n.md index a01f3939..3247b534 100644 --- a/docs/ai/i18n.md +++ b/docs/ai/i18n.md @@ -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. diff --git a/public/locales/en.json b/public/locales/en.json index 74c4dbfc..d8921242 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -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 chat’s 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" } } diff --git a/public/locales/ru.json b/public/locales/ru.json index e29567c1..8e59809f 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -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": "Вперёд" } } diff --git a/src/app/components/BackupRestore.tsx b/src/app/components/BackupRestore.tsx index 068f437b..033f89cf 100644 --- a/src/app/components/BackupRestore.tsx +++ b/src/app/components/BackupRestore.tsx @@ -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 ( @@ -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')} ); @@ -52,21 +54,23 @@ type BackupSyncingProps = { count: number; }; function BackupSyncing({ count }: BackupSyncingProps) { + const { t } = useTranslation(); return ( - Syncing ({count}) + {t('Settings.backup_syncing', { amount: count })} ); } function BackupProgressFetching() { + const { t } = useTranslation(); return ( - Restoring: 0% + {t('Settings.backup_restoring_percent', { percent: 0 })} @@ -81,10 +85,15 @@ type BackupProgressProps = { downloaded: number; }; function BackupProgress({ total, downloaded }: BackupProgressProps) { + const { t } = useTranslation(); return ( - Restoring: {`${Math.round(percent(0, total, downloaded))}%`} + + {t('Settings.backup_restoring_percent', { + percent: Math.round(percent(0, total, 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) { {trust.matchesDecryptionKey ? ( - Backup has trusted decryption key. + {t('Settings.backup_trusted_decryption_key')} ) : ( - Backup does not have trusted decryption key! + {t('Settings.backup_untrusted_decryption_key')} )} {trust.trusted ? ( - Backup has trusted by signature. + {t('Settings.backup_trusted_signature')} ) : ( - Backup does not have trusted signature! + {t('Settings.backup_untrusted_signature')} )} @@ -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 ( {remainingSession === 0 ? ( @@ -212,12 +223,20 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) { - Version: {backupInfo?.version ?? 'NIL'} + + {t('Settings.backup_version', { + version: backupInfo?.version ?? t('Settings.backup_none_value'), + })} +
- Keys: {backupInfo?.count ?? 'NIL'} + + {t('Settings.backup_keys_count', { + amount: backupInfo?.count ?? t('Settings.backup_none_value'), + })} + } /> @@ -234,7 +253,7 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) { } before={} > - Restore Backup + {t('Settings.backup_restore')}
@@ -251,7 +270,7 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) { )} {!backupEnabled && backupInfo === null && ( - No backup present on server! + {t('Settings.backup_none_on_server')} )} {!syncFailure && !backupEnabled && backupInfo && ( diff --git a/src/app/components/DeviceVerification.tsx b/src/app/components/DeviceVerification.tsx index d2502ccf..dfd70aa0 100644 --- a/src/app/components/DeviceVerification.tsx +++ b/src/app/components/DeviceVerification.tsx @@ -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 ( {message} ); } function VerificationWaitAccept() { + const { t } = useTranslation(); return ( - Please accept the request from other device. - + {t('Settings.verification_accept_prompt')} + ); } @@ -73,12 +76,13 @@ type VerificationAcceptProps = { onAccept: () => Promise; }; function VerificationAccept({ onAccept }: VerificationAcceptProps) { + const { t } = useTranslation(); const [acceptState, accept] = useAsyncCallback(onAccept); const accepting = acceptState.status === AsyncStatus.Loading; return ( - Click accept to start the verification process. + {t('Settings.verification_click_accept')} ); } function VerificationWaitStart() { + const { t } = useTranslation(); return ( - Verification request has been accepted. - + {t('Settings.verification_request_accepted')} + ); } @@ -105,18 +110,20 @@ type VerificationStartProps = { onStart: () => Promise; }; function AutoVerificationStart({ onStart }: VerificationStartProps) { + const { t } = useTranslation(); useEffect(() => { onStart(); }, [onStart]); return ( - + ); } 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 ( - Confirm the emoji below are displayed on both devices, in the same order: + {t('Settings.verification_compare_emoji_prompt')} } > - They Match + {t('Settings.verification_match')} @@ -177,6 +184,7 @@ type SasVerificationProps = { onCancel: () => void; }; function SasVerification({ verifier, onCancel }: SasVerificationProps) { + const { t } = useTranslation(); const [sasData, setSasData] = useState(); useVerifierShowSas(verifier, setSasData); @@ -192,7 +200,7 @@ function SasVerification({ verifier, onCancel }: SasVerificationProps) { return ( - + ); } @@ -201,13 +209,14 @@ type VerificationDoneProps = { onExit: () => void; }; function VerificationDone({ onExit }: VerificationDoneProps) { + const { t } = useTranslation(); return (
- Your device is verified. + {t('Settings.verification_device_verified')}
); @@ -217,11 +226,12 @@ type VerificationCanceledProps = { onClose: () => void; }; function VerificationCanceled({ onClose }: VerificationCanceledProps) { + const { t } = useTranslation(); return ( - Verification has been canceled. + {t('Settings.verification_canceled')} ); @@ -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)
- Device Verification + {t('Settings.device_verification')} @@ -283,7 +294,7 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps) ) : ( ))} diff --git a/src/app/components/DeviceVerificationSetup.tsx b/src/app/components/DeviceVerificationSetup.tsx index 0769402e..35353311 100644 --- a/src/app/components/DeviceVerificationSetup.tsx +++ b/src/app/components/DeviceVerificationSetup.tsx @@ -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 ( - - Generate a Recovery Key for verifying identity if you do not have access to other - devices. Additionally, setup a passphrase as a memorable alternative. - + {t('Settings.verification_setup_desc')} - Passphrase (Optional) + {t('Settings.verification_passphrase_optional')} {setupState.status === AsyncStatus.Error && ( - {setupState.error ? setupState.error.message : 'Unexpected Error!'} + + {setupState.error ? setupState.error.message : t('Settings.verification_error_generic')} + )} {nextAuthData !== null && uiaAction && ( ( - - Authentication steps to perform this action are not supported by client. - - )} + unsupported={() => {t('Settings.verification_uia_unsupported')}} > {(ongoingFlow) => ( { @@ -245,12 +243,9 @@ function RecoveryKeyDisplay({ recoveryKey }: RecoveryKeyDisplayProps) { return ( - - 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. - + {t('Settings.verification_recovery_key_store_desc')} - Recovery Key + {t('Settings.verification_recovery_key')} setShow(!show)} variant="Secondary" radii="Pill"> - {show ? 'Hide' : 'Show'} + + {show ? t('Settings.verification_hide') : t('Settings.verification_show')} + @@ -286,6 +283,7 @@ type DeviceVerificationSetupProps = { }; export const DeviceVerificationSetup = forwardRef( ({ onCancel }, ref) => { + const { t } = useTranslation(); const [recoveryKey, setRecoveryKey] = useState(); return ( @@ -299,7 +297,7 @@ export const DeviceVerificationSetup = forwardRef - Setup Device Verification + {t('Settings.verification_setup_title')} @@ -321,6 +319,7 @@ type DeviceVerificationResetProps = { }; export const DeviceVerificationReset = forwardRef( ({ onCancel }, ref) => { + const { t } = useTranslation(); const [reset, setReset] = useState(false); return ( @@ -334,7 +333,7 @@ export const DeviceVerificationReset = forwardRef - Reset Device Verification + {t('Settings.verification_reset_title')} @@ -356,16 +355,11 @@ export const DeviceVerificationReset = forwardRef ✋🧑‍🚒🤚 - Resetting device verification is permanent. - - 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. - + {t('Settings.verification_reset_permanent')} + {t('Settings.verification_reset_warning')} )} diff --git a/src/app/components/ManualVerification.tsx b/src/app/components/ManualVerification.tsx index f7cde92b..91ad9631 100644 --- a/src/app/components/ManualVerification.tsx +++ b/src/app/components/ManualVerification.tsx @@ -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(); const handleMenu: MouseEventHandler = (evt) => { @@ -55,8 +57,10 @@ export function ManualVerificationMethodSwitcher({ onClick={handleMenu} > - {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')} handleSelect(ManualVerificationMethod.RecoveryPassphrase)} > - Recovery Passphrase + {t('Settings.verification_recovery_passphrase')} handleSelect(ManualVerificationMethod.RecoveryKey)} > - Recovery Key + {t('Settings.verification_recovery_key')} @@ -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 ( {hasPassphrase && ( @@ -167,7 +176,7 @@ export function ManualVerificationTile({ /> {verifyState.status === AsyncStatus.Success ? ( - Device verified! + {t('Settings.verification_verified_success')} ) : ( diff --git a/src/app/components/editor/Toolbar.tsx b/src/app/components/editor/Toolbar.tsx index 7d701c42..b96703a2 100644 --- a/src/app/components/editor/Toolbar.tsx +++ b/src/app/components/editor/Toolbar.tsx @@ -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(); @@ -158,7 +160,12 @@ export function HeadingBlockButton() { } + tooltip={ + + } delay={500} > {(triggerRef) => ( @@ -173,7 +180,12 @@ export function HeadingBlockButton() { )} } + tooltip={ + + } delay={500} > {(triggerRef) => ( @@ -188,7 +200,12 @@ export function HeadingBlockButton() { )} } + tooltip={ + + } 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" > - {`Exit ${KeySymbol.Hyper}`} + {`${t('Room.format_exit')} ${KeySymbol.Hyper}`} )} @@ -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() { } + tooltip={} /> } + tooltip={} /> } + tooltip={ + + } /> } + tooltip={ + + } /> } + tooltip={ + + } /> } + tooltip={} /> @@ -305,22 +330,30 @@ export function Toolbar() { } + tooltip={ + + } /> } + tooltip={ + + } /> } + tooltip={ + + } /> } + tooltip={ + + } /> @@ -330,7 +363,10 @@ export function Toolbar() { + } /> @@ -339,7 +375,15 @@ export function Toolbar() { } + tooltip={ + + } delay={500} > {(triggerRef) => ( diff --git a/src/app/components/leave-space-prompt/LeaveSpacePrompt.tsx b/src/app/components/leave-space-prompt/LeaveSpacePrompt.tsx index 8709b942..62762a74 100644 --- a/src/app/components/leave-space-prompt/LeaveSpacePrompt.tsx +++ b/src/app/components/leave-space-prompt/LeaveSpacePrompt.tsx @@ -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( @@ -66,7 +68,7 @@ export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptP size="500" > - Leave Space + {t('Room.leave_space_title')} @@ -74,10 +76,10 @@ export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptP
- Are you sure you want to leave this space? + {t('Room.leave_space_confirm')} {leaveState.status === AsyncStatus.Error && ( - Failed to leave space! {leaveState.error.message} + {t('Room.leave_space_error', { error: leaveState.error.message })} )} @@ -96,7 +98,7 @@ export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptP } > - {leaveState.status === AsyncStatus.Loading ? 'Leaving...' : 'Leave'} + {leaveState.status === AsyncStatus.Loading ? t('Room.leaving') : t('Room.leave')} diff --git a/src/app/components/uia-stages/EmailStage.tsx b/src/app/components/uia-stages/EmailStage.tsx index fdc2b61a..ada84958 100644 --- a/src/app/components/uia-stages/EmailStage.tsx +++ b/src/app/components/uia-stages/EmailStage.tsx @@ -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 = (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({ {title} {message} - Email + {t('Auth.uia_email_label')}
@@ -80,6 +82,7 @@ export function EmailStageDialog({ emailTokenState: AsyncState; requestEmailToken: RequestEmailTokenCallback; }) { + const { t } = useTranslation(); const { errorCode, error, session } = stageData; const handleSubmit = useCallback( @@ -115,7 +118,7 @@ export function EmailStageDialog({ return ( - Sending verification email... + {t('Auth.uia_email_sending')} ); } @@ -123,11 +126,11 @@ export function EmailStageDialog({ if (emailTokenState.status === AsyncStatus.Error) { return ( - Verification Request Sent - {`Please check your email "${emailTokenState.data.email}" and validate before continuing further.`} + {t('Auth.uia_email_sent_title')} + {t('Auth.uia_email_sent_message', { email: emailTokenState.data.email })} {errorCode && ( {`${errorCode}: ${error}`} @@ -149,7 +152,7 @@ export function EmailStageDialog({ @@ -160,8 +163,8 @@ export function EmailStageDialog({ if (!email) { return ( diff --git a/src/app/components/user-profile/UserModeration.tsx b/src/app/components/user-profile/UserModeration.tsx index 160ecead..4dfbe93a 100644 --- a/src/app/components/user-profile/UserModeration.tsx +++ b/src/app/components/user-profile/UserModeration.tsx @@ -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) { - Kicked User + {t('User.moderation_kicked_user')} {time && date && ( {date} {time} @@ -32,16 +34,16 @@ export function UserKickAlert({ reason, kickedBy, ts }: UserKickAlertProps) { {kickedBy && ( - Kicked by: {kickedBy} + {t('User.moderation_kicked_by')} {kickedBy} )} {reason ? ( <> - Reason: {reason} + {t('User.moderation_reason_label')} {reason} ) : ( - No Reason Provided. + {t('User.moderation_no_reason')} )} @@ -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 - Banned User + {t('User.moderation_banned_user')} {time && date && ( {date} {time} @@ -87,16 +90,16 @@ export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBan {bannedBy && ( - Banned by: {bannedBy} + {t('User.moderation_banned_by')} {bannedBy} )} {reason ? ( <> - Reason: {reason} + {t('User.moderation_reason_label')} {reason} ) : ( - No Reason Provided. + {t('User.moderation_no_reason')} )} @@ -114,7 +117,7 @@ export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBan before={banning && } disabled={banning} > - Unban + {t('User.moderation_unban')} )} @@ -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 - Invited User + {t('User.moderation_invited_user')} {time && date && ( {date} {time} @@ -159,16 +163,16 @@ export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: User {invitedBy && ( - Invited by: {invitedBy} + {t('User.moderation_invited_by')} {invitedBy} )} {reason ? ( <> - Reason: {reason} + {t('User.moderation_reason_label')} {reason} ) : ( - No Reason Provided. + {t('User.moderation_no_reason')} )} @@ -188,7 +192,7 @@ export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: User before={kicking && } disabled={kicking} > - Cancel Invite + {t('User.moderation_cancel_invite')} )} @@ -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(null); @@ -245,10 +250,10 @@ export function UserModeration({ userId, canKick, canBan, canInvite }: UserModer - Moderation + {t('User.moderation_title')} - Invite + {t('User.moderation_invite')} )} {canKick && ( @@ -308,7 +313,7 @@ export function UserModeration({ userId, canKick, canBan, canInvite }: UserModer onClick={kick} disabled={disabled} > - Kick + {t('User.moderation_kick')} )} {canBan && ( @@ -328,7 +333,7 @@ export function UserModeration({ userId, canKick, canBan, canInvite }: UserModer onClick={ban} disabled={disabled} > - Ban + {t('User.moderation_ban')} )} diff --git a/src/app/features/call/CallControls.tsx b/src/app/features/call/CallControls.tsx index f52b86f3..13ccf931 100644 --- a/src/app/features/call/CallControls.tsx +++ b/src/app/features/call/CallControls.tsx @@ -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(null); const [compact, setCompact] = useState(document.body.clientWidth < 500); @@ -140,7 +142,7 @@ export function CallControls({ callEmbed }: CallControlsProps) { onClick={handleSpotlightClick} > - {spotlight ? 'Grid View' : 'Spotlight View'} + {spotlight ? t('Call.view_grid') : t('Call.view_spotlight')} - Reactions + {t('Call.reactions')} @@ -162,7 +164,7 @@ export function CallControls({ callEmbed }: CallControlsProps) { onClick={handleSettingsClick} > - Settings + {t('Call.settings')} @@ -198,7 +200,7 @@ export function CallControls({ callEmbed }: CallControlsProps) { } disabled={exiting} > - End + {t('Call.end_call')} diff --git a/src/app/features/call/CallMemberCard.tsx b/src/app/features/call/CallMemberCard.tsx index 1c3680f5..4930bedf 100644 --- a/src/app/features/call/CallMemberCard.tsx +++ b/src/app/features/call/CallMemberCard.tsx @@ -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({ {viewMore ? ( - Collapse + {t('Call.collapse')} ) : ( - {remaining === 0 ? `+${remaining} Other` : `+${remaining} Others`} + {t('Call.others_count', { count: remaining })} )} diff --git a/src/app/features/call/CallView.tsx b/src/app/features/call/CallView.tsx index 7c7bec6c..34d269ad 100644 --- a/src/app/features/call/CallView.tsx +++ b/src/app/features/call/CallView.tsx @@ -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 ( - Your homeserver does not support calling. But you can still join call started by others. + {t('Call.homeserver_no_calls')} ); } @@ -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 ( - Voice chat’s empty — Be the first to hop in! + {t('Call.empty_be_first')} ); } function NoPermissionMessage() { + const { t } = useTranslation(); return ( - You don't have permission to join! + {t('Call.no_permission_to_join')} ); } function AlreadyInCallMessage() { + const { t } = useTranslation(); return ( - Already in another call — End the current call to join! + {t('Call.already_in_other_call')} ); } function CallPrescreen() { + const { t } = useTranslation(); const mx = useMatrixClient(); const room = useRoom(); const livekitSupported = useLivekitSupport(); @@ -86,11 +92,11 @@ function CallPrescreen() { {hasParticipant && (
- Participant + {t('Call.participant')} - {callMembers.length} Live + {t('Call.live_count', { count: callMembers.length })}
diff --git a/src/app/features/call/PrescreenControls.tsx b/src/app/features/call/PrescreenControls.tsx index 200d0af0..74d90d93 100644 --- a/src/app/features/call/PrescreenControls.tsx +++ b/src/app/features/call/PrescreenControls.tsx @@ -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) { ) } > - Join + {t('Call.join')}
diff --git a/src/app/features/lobby/Lobby.tsx b/src/app/features/lobby/Lobby.tsx index 1ea6f542..144cc0b0 100644 --- a/src/app/features/lobby/Lobby.tsx +++ b/src/app/features/lobby/Lobby.tsx @@ -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')} > @@ -535,7 +537,7 @@ export function Lobby() { radii="Pill" before={} > - Reordering + {t('Room.lobby_reordering')}
)} diff --git a/src/app/features/lobby/LobbyHeader.tsx b/src/app/features/lobby/LobbyHeader.tsx index ed526c68..35dc0303 100644 --- a/src/app/features/lobby/LobbyHeader.tsx +++ b/src/app/features/lobby/LobbyHeader.tsx @@ -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( ({ powerLevels, requestClose }, ref) => { + const { t } = useTranslation(); const mx = useMatrixClient(); const space = useSpace(); const creators = useRoomCreators(space); @@ -87,7 +89,7 @@ const LobbyMenu = forwardRef( disabled={!canInvite} > - Invite + {t('Room.invite')} ( radii="300" > - Space Settings + {t('Room.lobby_space_settings')}
@@ -116,7 +118,7 @@ const LobbyMenu = forwardRef( aria-pressed={promptLeave} > - Leave Space + {t('Room.lobby_leave_space')} {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={ - Members + {t('Room.members')} } > @@ -234,7 +237,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) { offset={4} tooltip={ - More Options + {t('Room.more_options')} } > diff --git a/src/app/features/lobby/RoomItem.tsx b/src/app/features/lobby/RoomItem.tsx index 7de59acd..1dd7aa4a 100644 --- a/src/app/features/lobby/RoomItem.tsx +++ b/src/app/features/lobby/RoomItem.tsx @@ -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( @@ -91,7 +93,7 @@ function RoomJoinButton({ roomId, via }: RoomJoinButtonProps) { onClick={join} disabled={!canJoin} > - Join + {t('Room.lobby_join')}
); @@ -127,6 +129,7 @@ type RoomProfileErrorProps = { via?: string[]; }; function RoomProfileError({ roomId, suggested, inaccessibleRoom, via }: RoomProfileErrorProps) { + const { t } = useTranslation(); return ( @@ -146,12 +149,12 @@ function RoomProfileError({ roomId, suggested, inaccessibleRoom, via }: RoomProf - Unknown + {t('Room.lobby_unknown')} {suggested && ( - Suggested + {t('Room.lobby_suggested')} )} @@ -159,7 +162,7 @@ function RoomProfileError({ roomId, suggested, inaccessibleRoom, via }: RoomProf {inaccessibleRoom ? ( - Inaccessible + {t('Room.lobby_inaccessible')} ) : ( @@ -195,6 +198,7 @@ function RoomProfile({ joinRule, options, }: RoomProfileProps) { + const { t } = useTranslation(); return ( @@ -213,7 +217,7 @@ function RoomProfile({ {suggested && ( - Suggested + {t('Room.lobby_suggested')} )} @@ -221,7 +225,12 @@ function RoomProfile({ {memberCount && ( - {`${millify(memberCount)} Members`} + + {t('Room.lobby_members_count', { + count: memberCount, + formattedCount: millify(memberCount), + })} + )} {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')} > diff --git a/src/app/features/lobby/SpaceItem.tsx b/src/app/features/lobby/SpaceItem.tsx index a2114b78..35954c50 100644 --- a/src/app/features/lobby/SpaceItem.tsx +++ b/src/app/features/lobby/SpaceItem.tsx @@ -60,6 +60,7 @@ type InaccessibleSpaceProfileProps = { suggested?: boolean; }; function InaccessibleSpaceProfile({ roomId, suggested }: InaccessibleSpaceProfileProps) { + const { t } = useTranslation(); return ( ( - U + {nameInitials(t('Room.lobby_unknown'))} )} /> @@ -81,15 +82,15 @@ function InaccessibleSpaceProfile({ roomId, suggested }: InaccessibleSpaceProfil > - Unknown + {t('Room.lobby_unknown')} - Inaccessible + {t('Room.lobby_inaccessible')} {suggested && ( - Suggested + {t('Room.lobby_suggested')} )} @@ -111,6 +112,7 @@ function UnjoinedSpaceProfile({ avatarUrl, suggested, }: UnjoinedSpaceProfileProps) { + const { t } = useTranslation(); const mx = useMatrixClient(); const [joinState, join] = useAsyncCallback( @@ -145,11 +147,11 @@ function UnjoinedSpaceProfile({ > - {name || 'Unknown'} + {name || t('Room.lobby_unknown')} {suggested && ( - Suggested + {t('Room.lobby_suggested')} )} {joinState.status === AsyncStatus.Error && ( @@ -182,6 +184,7 @@ function SpaceProfile({ categoryId, handleClose, }: SpaceProfileProps) { + const { t } = useTranslation(); return ( {suggested && ( - Suggested + {t('Room.lobby_suggested')} )} @@ -225,6 +228,7 @@ type RootSpaceProfileProps = { handleClose?: MouseEventHandler; }; function RootSpaceProfile({ closed, categoryId, handleClose }: RootSpaceProfileProps) { + const { t } = useTranslation(); return ( - Rooms + {t('Room.lobby_rooms')} diff --git a/src/app/features/message-search/MessageSearch.tsx b/src/app/features/message-search/MessageSearch.tsx index 0a3131ba..de49607a 100644 --- a/src/app/features/message-search/MessageSearch.tsx +++ b/src/app/features/message-search/MessageSearch.tsx @@ -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')} > diff --git a/src/app/features/room/RoomView.tsx b/src/app/features/room/RoomView.tsx index a9672bc0..0698fff9 100644 --- a/src/app/features/room/RoomView.tsx +++ b/src/app/features/room/RoomView.tsx @@ -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(null); const roomViewRef = useRef(null); const composerWrapRef = useRef(null); @@ -200,7 +202,7 @@ export function RoomView({ eventId }: { eventId?: string }) { alignItems="Center" justifyContent="Center" > - You do not have permission to post in this room + {t('Room.no_post_permission')} )} diff --git a/src/app/features/settings/devices/Verification.tsx b/src/app/features/settings/devices/Verification.tsx index 06b76383..66756060 100644 --- a/src/app/features/settings/devices/Verification.tsx +++ b/src/app/features/settings/devices/Verification.tsx @@ -85,10 +85,14 @@ function LearnStartVerificationFromOtherDevice() {
  • {t('Settings.verify_step_1')}
  • - Open {t('Settings.verify_step_2_settings')}. + {t('Settings.verification_step_open', { + settings: t('Settings.verify_step_2_settings'), + })}
  • - Find this device in {t('Settings.verify_step_3_section')} section. + {t('Settings.verification_step_find', { + section: t('Settings.verify_step_3_section'), + })}
  • {t('Settings.verify_step_4')}