diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 434432fea2..f3e7809fb7 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["webpro.vscode-knip", "oxc.oxc-vscode"] + "recommendations": ["webpro.vscode-knip", "oxc.oxc-vscode", "lokalise.i18n-ally"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index d8071aa979..f8e70cf7a1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,6 +15,17 @@ }, "nixEnvSelector.nixFile": "${workspaceFolder}/flake.nix", "nixEnvSelector.useFlakes": true, + + "i18n-ally.localesPaths": ["public/locales"], + "i18n-ally.namespace": true, + "i18n-ally.pathMatcher": "{locale}/{namespaces}.json", + "i18n-ally.enabledFrameworks": ["react", "react-i18next"], + "i18n-ally.keystyle": "nested", + "i18n-ally.extract.keyMaxLength": 75, + "i18n-ally.extract.targetPickingStrategy": "most-similar", + "i18n-ally.extract.keygenStyle": "snake_case", + "i18n-ally.extract.ignored": ["@"], + "explorer.fileNesting.enabled": true, "explorer.fileNesting.patterns": { "package.json": "pnpm-*.yaml, yarn.lock, package-lock.json, .npmrc, .nvmrc, .node-version", diff --git a/i18next.config.ts b/i18next.config.ts new file mode 100644 index 0000000000..fc2fbae4f2 --- /dev/null +++ b/i18next.config.ts @@ -0,0 +1,7 @@ +export default { + locales: ['en', 'ro'], + extract: { + input: 'src/**/*.tsx', + output: 'public/locales/{{language}}/{{namespace}}.json', + }, +}; diff --git a/knip.json b/knip.json index 1912dfad95..a542824788 100644 --- a/knip.json +++ b/knip.json @@ -1,7 +1,7 @@ { "$schema": "https://unpkg.com/knip@6/schema.json", "entry": ["src/sw.ts", "scripts/normalize-imports.js"], - "ignore": ["oxlint.config.ts", "oxfmt.config.ts"], + "ignore": ["oxlint.config.ts", "oxfmt.config.ts", "i18next.config.ts"], "ignoreExportsUsedInFile": { "interface": true, "type": true diff --git a/public/locales/de.json b/public/locales/de.json deleted file mode 100644 index 43a37160ef..0000000000 --- a/public/locales/de.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "Organisms": { - "RoomCommon": { - "changed_room_name": " hat den Raum Name geändert" - } - } -} diff --git a/public/locales/en.json b/public/locales/en.json deleted file mode 100644 index 7a2534b8f7..0000000000 --- a/public/locales/en.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "Organisms": { - "RoomCommon": { - "changed_room_name": " changed room name" - } - } -} diff --git a/public/locales/en/general.json b/public/locales/en/general.json new file mode 100644 index 0000000000..85d4cca31e --- /dev/null +++ b/public/locales/en/general.json @@ -0,0 +1,169 @@ +{ + "continue": "Continue", + "passphrase": "Passphrase", + "optional": "optional", + "unexpected_error": "Unexpected Error!", + "recovery_key": "Recovery Key", + "download": "Download", + "copy": "Copy", + "hide": "Hide", + "show": "Show", + "recovery_passphrase": "Recovery Passphrase", + "reset": "Reset", + "close": "Close", + "accept": "Accept", + "okay": "Okay", + "retry": "Retry", + "user": "User", + "delete": "Delete", + "dismiss": "Dismiss", + "nickname": "Nickname", + "save": "Save", + "clear": "Clear", + "edit_nickname": "Edit Nickname", + "set_nickname": "Set Nickname", + "cancel": "Cancel", + "upload": "Upload", + "upload_area": "Upload area", + "display_name": "Display name", + "pronouns": "Pronouns", + "unknown": "Unknown", + "edit": "Edit", + "Organisms": { + "RoomCommon": { + "changed_room_name": " changed room name" + } + }, + "Settings": { + "Cosmetics": { + "light_and_dark": "Light & dark", + "light_only": "Light only", + "dark_only": "Dark only", + "off": "Off", + "jumbo_emoji": "Jumbo Emoji", + "jumbo_emoji_size": "Jumbo Emoji Size", + "adjust_the_size_of_emojis_sent_without_text": "Adjust the size of emojis sent without text.", + "privacy_and_security": "Privacy & Security", + "blur_media": "Blur Media", + "blurs_images_and_videos_in_the_timeline": "Blurs images and videos in the timeline.", + "blur_avatars": "Blur Avatars", + "blurs_user_profile_pictures_and_room_icons": "Blurs user profile pictures and room icons.", + "blur_emotes": "Blur Emotes", + "blurs_emoticons_within_messages": "Blurs emoticons within messages.", + "identity": "Identity", + "colorful_names": "Colorful Names", + "assign_unique_colors_to_users_based_on_their_id_does_not_override_room_spac": "Assign unique colors to users based on their ID. Does not override room/space custom colors. Will override default role colors.", + "show_pronoun_pills": "Show Pronoun Pills", + "display_user_pronouns_in_the_message_timeline": "Display user pronouns in the message timeline.", + "max_pronoun_pills": "Max Pronoun Pills", + "maximum_number_of_pronoun_pills": "Maximum number of pronoun pills shown per user in the timeline. Additional pronouns appear behind the ... pill.", + "max_pronoun_pill_length": "Max Pronoun Pill Length", + "maximum_pronoun_pill_length": "Maximum characters shown in each pronoun pill before truncation.", + "pronoun_pills_for_all": "Pronoun Pills for All", + "attempts_to_convert_pronouns_in_names_into_pills_e_g_they_them_or_it_its_tu": "Attempts to convert pronouns in names into pills (e.g. [they/them] or (it/its) turns into a pill).", + "render_custom_profile_cards": "Render Custom Profile Cards", + "choose_whose_profile_card_colors_to_show_everyone_with_a_scheme_only_light": "Choose whose profile card colors to show: everyone with a scheme, only light or dark schemes, or hide them.", + "render_global_username_colors": "Render Global Username Colors", + "display_the_username_colors_anyone_can_set_in_their_account_settings": "Display the username colors anyone can set in their account settings.", + "render_space_room_username_colors": "Render Space/Room Username Colors", + "display_the_username_colors_that_can_be_set_with_color": "Display the username colors that can be set with /color.", + "render_space_room_fonts": "Render Space/Room Fonts", + "display_the_username_fonts_that_can_be_set_with_font": "Display the username fonts that can be set with /font.", + "consistent_icon_style": "Consistent Icon Style", + "harmonize_icon_appearance_with_background_fill": "Harmonize icon appearance with background fill", + "extra_small": "Extra Small", + "none_same_size_as_text": "None (Same size as text)", + "small": "Small", + "normal": "Normal", + "large": "Large", + "extra_large": "Extra Large", + "language_specific_pronouns": "Language Specific Pronouns", + "show_pronouns_only_in_selected_language": "Show pronouns only in selected language", + "if_enabled_pronouns_are_only_shown_when_they_match_your_selected_language_t": "If enabled, pronouns are only shown when they match your selected language. This helps if your contacts set pronouns in different languages. It doesn't affect how your pronouns are shared with others.", + "selected_language_for_pronouns": "Selected language for pronouns", + "the_language_to_show_pronouns_for_when_the_above_setting_is_enabled": "The language to show pronouns for when the above setting is enabled.", + "language_code_e_g_en_de_en_de": "Language code (e.g. 'en', 'de', 'en,de')" + }, + "General": { + "formatting": "Formatting", + "month": "Month", + "day_of_the_week": "Day of the Week", + "day_of_the_week_sunday_0": "Day of the week (Sunday = 0)", + "two_letter_day_name": "Two-letter day name", + "short_day_name": "Short day name", + "full_day_name": "Full day name", + "day_of_the_month": "Day of the Month", + "full_month_name": "Full month name", + "short_month_name": "Short month name", + "the_month": "The month", + "two_digit_month": "Two-digit month", + "four_digit_year": "Four-digit year", + "two_digit_year": "Two-digit year" + } + }, + "RoomView": { + "failed_to_load_history": "Failed to load history.", + "failed_to_load_messages": "Failed to load messages.", + "jump_to_unread": "Jump to Unread", + "mark_as_read": "Mark as Read", + "new_messages": "New Messages", + "jump_to_latest": "Jump to Latest", + "call_started_by": "Call started by", + "start_voice_call": "Start Voice Call", + "typing": { + "are_typing": " are typing...", + "typing_sep_word": " and " + }, + "is_typing": "is typing...", + "close_threads": "Close threads", + "search_threads": "Search threads...", + "clear_search": "Clear search", + "Threads": { + "no_threads_match_your_search": "No threads match your search.", + "no_threads_yet": "No threads yet.", + "jump": "Jump", + "threads": "Threads", + "thread": "Thread", + "no_replies_yet_start_the_thread_below": "No replies yet. Start the thread below!" + }, + "join_new_room": "Join New Room", + "this_room_has_been_replaced_and_is_no_longer_active": "This room has been replaced and is no longer active.", + "failed_to_join_replacement_room": "Failed to join replacement room!", + "open_new_room": "Open New Room", + "scroll_to_top": "Scroll to Top", + "Message": { + "pin_message": "Pin Message", + "unpin_message": "Unpin Message", + "forwarded_private_message": "Forwarded private message", + "forwarded_from_earlier_in_this_room": "Forwarded from earlier in this room", + "forwarded_from_another_room": "Forwarded from another room", + "jump_to_original": "jump to original", + "failed_to_send": "Failed to send.", + "only_you_can_see_this": "Only you can see this.", + "edit_message": "Edit Message", + "Editor": { + "edit_message": "Edit message..." + } + }, + "Reactions": { + "reacted_with": "Reacted with" + } + }, + "RoomInput": { + "recording_duration": "Recording duration:", + "EmojiBoard": { + "no_results_found": "No Results found", + "search_results": "Search Results", + "personal_pack": "Personal Pack", + "unknown_pack": "Unknown Pack" + } + }, + "Room": { + "DirectInvite": {} + }, + "DevTools": { + "json_content": "JSON Content", + "account_data": "Account Data", + "developer_tools": "Developer Tools" + } +} diff --git a/public/locales/en/room/create.json b/public/locales/en/room/create.json new file mode 100644 index 0000000000..ba437d4bf5 --- /dev/null +++ b/public/locales/en/room/create.json @@ -0,0 +1,19 @@ +{ + "versions": "Versions", + "founders": "Founders", + "special_privileged_users_can_be_assigned_during_creation_these_users_have_e": "Special privileged users can be assigned during creation. These users have elevated control and can only be modified during a upgrade.", + "no_suggestions": "No Suggestions", + "please_provide_the_user_id_and_hit_enter": "Please provide the user ID and hit Enter.", + "only_member_of_parent_space_can_join": "Only member of parent space can join.", + "private": "Private", + "only_people_with_invite_can_join": "Only people with invite can join.", + "anyone_with_the_address_can_join": "Anyone with the address can join.", + "public": "Public", + "address_optional": "Address (Optional)", + "pick_an_unique_address_to_make_it_discoverable": "Pick an unique address to make it discoverable.", + "this_address_is_already_taken_please_select_a_different_one": "This address is already taken. Please select a different one.", + "voice_room": "Voice Room", + "live_audio_and_video_conversations": "- Live audio and video conversations.", + "chat_room": "Chat Room", + "messages_photos_and_videos": "- Messages, photos, and videos." +} diff --git a/public/locales/en/room/direct/invite.json b/public/locales/en/room/direct/invite.json new file mode 100644 index 0000000000..fc40e8983a --- /dev/null +++ b/public/locales/en/room/direct/invite.json @@ -0,0 +1,7 @@ +{ + "direct_message_invite_prompt": "This is a Direct Message room, intended for a conversation between two persons. Would you like to convert it into a group chat before continuing?", + "failed_to_convert_direct_message_to_room": "Failed to convert direct message to room!", + "convert_to_group_chat_and_invite": "Convert to Group Chat and Invite", + "invite_to_direct_message_anyway": "Invite to Direct Message anyway", + "invite_another_member": "Invite another Member" +} diff --git a/public/locales/en/settings/device_verification.json b/public/locales/en/settings/device_verification.json new file mode 100644 index 0000000000..2a69592bac --- /dev/null +++ b/public/locales/en/settings/device_verification.json @@ -0,0 +1,28 @@ +{ + "error_uia_action_without_data": "Unexpected Error! UIA action is perform without data.", + "authentication_failed_failed_to_setup_device_verification": "Authentication failed! Failed to setup device verification.", + "unexpected_error_crypto_module_not_found": "Unexpected Error! Crypto module not found!", + "unexpected_error_failed_to_create_recovery_key": "Unexpected Error! Failed to create recovery key.", + "generate_a_recovery_key": "Generate a recovery key for verifying identity if you do not have access to other devices. Additionally, setup a passphrase as a memorable alternative", + "optional_passphrase": "Passphrase (optional)", + "authentication_steps_to_perform_this_action_are_not_supported_by_client": "Authentication steps to perform this action are not supported by client.", + "store_the_recovery_key_in_a_safe_place_for_future_use_as_you_will_need_it_t": "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.", + "setup_device_verification": "Setup Device Verification", + "reset_device_verification": "Reset Device Verification", + "resetting_device_verification_is_permanent": "Resetting device verification is permanent.", + "anyone_you_have_verified_with_will_see_security_alerts_and_your_encryption": "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", + "and_every_device_you_can_verify_from": "and every device you can verify from.", + "please_accept_the_request_from_other_device": "Please accept the request from other device.", + "click_accept_to_start_the_verification_process": "Click accept to start the verification process.", + "waiting_for_request_to_be_accepted": "Waiting for request to be accepted...", + "confirm_the_emoji_below_are_displayed_on_both_devices_in_the_same_order": "Confirm the emoji below are displayed on both devices, in the same order:", + "they_match": "They Match", + "do_not_match": "Do not Match", + "starting_verification_using_emoji_comparison": "Starting verification using emoji comparison...", + "your_device_is_verified": "Your device is verified.", + "verification_has_been_canceled": "Verification has been canceled.", + "device_verification": "Device Verification", + "unexpected_error_verification_is_started_but_verifier_is_missing": "Unexpected Error! Verification is started but verifier is missing.", + "verification_request_has_been_accepted": "Verification request has been accepted.", + "waiting_for_the_response_from_other_device": "Waiting for the response from other device..." +} diff --git a/public/locales/en/settings/experimental.json b/public/locales/en/settings/experimental.json new file mode 100644 index 0000000000..1a580e6551 --- /dev/null +++ b/public/locales/en/settings/experimental.json @@ -0,0 +1,22 @@ +{ + "save_bandwidth_for_sticker_and_emoji_images": "Save Bandwidth for Sticker and Emoji Images", + "enable_bandwidth_saving_for_stickers_and_emojis": "Enable bandwidth saving for stickers and emojis", + "if_enabled_sticker_and_emoji_images_will_be_optimized_to_save_bandwidth_thi": "If enabled, sticker and emoji images will be optimized to save bandwidth. This helps reduce data usage when viewing these images. But will increase server computation load.", + "personas_per_message_profiles": "Personas (Per-Message Profiles)", + "show_personas_tab": "Show Personas Tab", + "enables_the_personas_tab_in_the_settings_menu_for_per_message_profiles": "Enables the personas tab in the settings menu for per-message profiles", + "experimental": "Experimental", + "the_features_listed_below_may_be_unstable_or_incomplete": "The features listed below may be unstable or incomplete,", + "use_at_your_own_risk": "use at your own risk", + "please_report_any_new_issues_potentially_caused_by_these_features": "Please report any new issues potentially caused by these features!", + "enable_sharing_of_encrypted_history": "Enable Sharing of Encrypted History", + "enable_the_sharehistory_command": "Enable the /sharehistory command", + "if_enabled_this_command_will_allow_users_to_share_encrypted_history_with_ot": "If enabled, this command will allow users to share encrypted history with other newly joined users, as per MSC4268.", + "disable_sharehistory_command": "Disable /sharehistory command", + "enable_sharehistory_command": "Enable /sharehistory command", + "enable_media_galleries_support": "Enable Media Galleries Support", + "enable_media_galleries_title": "Enable Media Galleries", + "enable_media_galleries_description": "If enabled, multiple attachments will be sent in one message, as per MSC4274. Incompatible with clients that don't implement it.", + "disable_media_galleries": "Disable Media Galleries", + "enable_media_galleries": "Enable Media Gallleries" +} diff --git a/public/locales/en/settings/keyboard_shortcuts.json b/public/locales/en/settings/keyboard_shortcuts.json new file mode 100644 index 0000000000..1cd7cfa7eb --- /dev/null +++ b/public/locales/en/settings/keyboard_shortcuts.json @@ -0,0 +1,18 @@ +{ + "search_for_messages": "Search for messages", + "jump_to_the_highest_priority_unread_room": "Jump to the highest-priority unread room", + "go_to_next_unread_room_cycle": "Go to next unread room (cycle)", + "go_to_previous_unread_room_cycle": "Go to previous unread room (cycle)", + "undo_in_message_editor": "Undo in message editor", + "redo_in_message_editor": "Redo in message editor", + "seach_and_go_to_room": "Search and go to Room", + "open_sticker_picker": "Open Sticker Picker", + "bold": "Bold", + "italic": "Italic", + "underline": "Underline", + "keyboard_shortcuts": "Keyboard Shortcuts", + "close_keyboard_shortcuts": "Close keyboard shortcuts", + "general": "General", + "navigation": "Navigation", + "messages": "Messages" +} diff --git a/public/locales/en/settings/persona.json b/public/locales/en/settings/persona.json new file mode 100644 index 0000000000..36d53633e7 --- /dev/null +++ b/public/locales/en/settings/persona.json @@ -0,0 +1,21 @@ +{ + "limited_compatibility_with_pluralkit_like_functions": "Limited Compatibility with PluralKit-like functions", + "enable_pk_commands": "Enable PK commands", + "if_enabled_it_will_enable_a_few_pk_style_commands_currently_verry_limited": "If enabled, it will enable a few pk style commands, currently verry limited", + "disable_pk_commands": "disable pk; commands", + "enable_pks_commands": "enable pk; commands", + "enable_shorthands": "Enable Shorthands", + "if_enabled_you_can_use_shorthands_to_use_a_persona_for_one_message_only_eg": "If enabled, you can use shorthands to use a Persona for one message only (eg. '✨:test')", + "disable_checking_typed_messages_for_shorthands": "disable checking typed messages for shorthands", + "enable_checking_typed_messages_for_shorthands": "enable checking typed messages for shorthands", + "profile_id": "Profile ID:", + "pronouns": "Pronouns:", + "reset_pronouns": "Reset Pronouns", + "display_name": "Display Name:", + "delete_profile": "Delete profile {{profileId}}", + "save_profile": "Save profile changes for {{profileId}}", + "save_profile_button_area": "Save button area for {{profileId}}", + "pronouns_for": "Pronouns for {{profileId}}", + "avatar_for": "Avatar for {{profileId}}", + "display_name_for": "Display name for {{profileId}}" +} diff --git a/public/locales/en/settings/profile.json b/public/locales/en/settings/profile.json new file mode 100644 index 0000000000..1a4a955f52 --- /dev/null +++ b/public/locales/en/settings/profile.json @@ -0,0 +1,8 @@ +{ + "avatar_and_upload": "Avatar and upload", + "profile_avatar": "Profile avatar", + "avatar_fallback": "Avatar fallback", + "upload_avatar_image": "Upload avatar image", + "display_name_input": "Display name input", + "reset_display_name": "Reset display name" +} diff --git a/public/locales/ro/general.json b/public/locales/ro/general.json new file mode 100644 index 0000000000..d4d6fbca93 --- /dev/null +++ b/public/locales/ro/general.json @@ -0,0 +1,166 @@ +{ + "continue": "Continuă", + "passphrase": "Parolă", + "optional": "opțional", + "unexpected_error": "Eroare Neașteptată!", + "recovery_key": "Cheie de Recuperare", + "download": "Descarcă", + "copy": "Copiază", + "hide": "Ascunde", + "show": "Arată", + "recovery_passphrase": "Parolă de recuperare", + "reset": "Resetează", + "close": "Închide", + "accept": "Accept", + "okay": "Okay", + "retry": "Reîncearcă", + "user": "utilizator", + "delete": "Șterge", + "dismiss": "Omite", + "nickname": "Poreclă", + "save": "Salvează", + "clear": "Curăță", + "edit_nickname": "Editează Poreclă", + "set_nickname": "Setează Poreclă", + "cancel": "Anulează", + "upload": "Încarcă", + "upload_area": "Zonă încărcare", + "display_name": "Nume Afișat", + "pronouns": "Pronume", + "unknown": "Necunoscut", + "edit": "Editează", + "Organisms": { + "RoomCommon": { + "changed_room_name": " a schimbat numele camerei" + } + }, + "Settings": { + "Cosmetics": { + "light_and_dark": "Luminos & Întunecat", + "light_only": "Doar luminos", + "dark_only": "Doar întunecat", + "off": "Niciodată", + "jumbo_emoji": "Emojiuri Jumbo", + "jumbo_emoji_size": "Mărime Emojiuri Jumbo", + "adjust_the_size_of_emojis_sent_without_text": "Schimbă mărimea emojiurilor trimise fâră text.", + "privacy_and_security": "Confidențialitate și Securitate", + "blur_media": "Estompază media.", + "blurs_images_and_videos_in_the_timeline": "Estompează imaginile si videourile din mesaje.", + "blur_avatars": "Estompează avatar-uri", + "blurs_user_profile_pictures_and_room_icons": "Estompează imaginile de profil si iconițele camerelor.", + "blur_emotes": "Estompează emoticoane", + "blurs_emoticons_within_messages": "Estompează emoticoanele din mesaje.", + "identity": "Identitate", + "colorful_names": "Nume Colorate Aleatoriu", + "assign_unique_colors_to_users_based_on_their_id_does_not_override_room_spac": "Atribuie culori unici utilizatorilor pe baza ID-ului lor. Această setare nu va inlocuii culorile atribuite de cameră/spațiu, dar va inlocuii culoarea rolului de bază", + "show_pronoun_pills": "Arată pilule de pronume", + "display_user_pronouns_in_the_message_timeline": "Arata pronumele utilizatorilor in mesaje", + "max_pronoun_pills": "Număr maxim de pilule de pronume", + "maximum_number_of_pronoun_pills": "Numărul maxim de pilule de pronume prezente de persoană in conversatie. Alte pronume apar după pilula intitulată '...'", + "max_pronoun_pill_length": "Lungimea maximă a pronumelor de profil", + "maximum_pronoun_pill_length": "Numărul maxim de litere in fiecare pronume înainte de a fi trunchiată.", + "pronoun_pills_for_all": "Pilule de pronume pentru toată lumea", + "attempts_to_convert_pronouns_in_names_into_pills_e_g_they_them_or_it_its_tu": "Încearcă sa transformi pronumele din nume in pilule (ex. [el/lui] sau [ea/iei] este transformat in pilulă).", + "render_custom_profile_cards": "Redă carduri de profil personalizate.", + "choose_whose_profile_card_colors_to_show_everyone_with_a_scheme_only_light": "Alege când sa se redea cardul de profil: pentru toată lumea, doar teme întunecate, doar teme luminoase, sau nu le reda deloc.", + "render_global_username_colors": "Redă culorile generale numelor persoanelor", + "display_the_username_colors_anyone_can_set_in_their_account_settings": "Redă culorile numelor tuturor persoanelor care au fost setate de acestea.", + "render_space_room_username_colors": "Redă culorile numelor setate de Cameră/Spațiu.", + "display_the_username_colors_that_can_be_set_with_color": "Redă culorile numelor care pot fi setate cu /color.", + "render_space_room_fonts": "Redă fonturile Camerei/Spațiului.", + "display_the_username_fonts_that_can_be_set_with_font": "Redă fontul numelor care pot fi setate cu /font.", + "consistent_icon_style": "Stil al icoanelor consistent.", + "harmonize_icon_appearance_with_background_fill": "Armonizează forma icoanelor cu o culoare de fundal.", + "extra_small": "Foarte mic", + "none_same_size_as_text": "Bază (aceași mărime ca textul)", + "small": "Mic", + "normal": "Normal", + "large": "Mari", + "extra_large": "Foarte mari", + "language_specific_pronouns": "Pronume specifice limbii", + "show_pronouns_only_in_selected_language": "Redă doar pronumele in limba selectată", + "if_enabled_pronouns_are_only_shown_when_they_match_your_selected_language_t": "Dacă activat, doar pronumele in limba selectată de tine vor apărea. Asta ajuta dacă contactele tale au setata pronume in limbi diferite. Nu afecteaza cum pronumele setate de tine sunt împărtășite.", + "selected_language_for_pronouns": "Limbi selectate pentru pronume.", + "the_language_to_show_pronouns_for_when_the_above_setting_is_enabled": "Limbile in care pronumele vor fi redate pentru setarea de asupra.", + "language_code_e_g_en_de_en_de": "Codul limbii (ex. 'ro', 'de', 'ro,de')" + }, + "General": { + "formatting": "Formatare", + "month": "Luna", + "day_of_the_week": "Ziua săptămănii", + "day_of_the_week_sunday_0": "Ziua săptămănii (Duminică = 0)", + "two_letter_day_name": "Numele zilei cu 2 litere", + "short_day_name": "Numele zilei scurt", + "full_day_name": "Numele zilei întreg", + "day_of_the_month": "Ziua din lună", + "full_month_name": "Numele întreg al lunii", + "short_month_name": "Numele scurt al lunii", + "the_month": "Luna", + "two_digit_month": "Luna cu 2 cifre", + "four_digit_year": "Anul cu 4 cifre", + "two_digit_year": "Anul cu 2 cifre" + } + }, + "RoomView": { + "failed_to_load_history": "Încărcarea istoriei a eșuat.", + "failed_to_load_messages": "Încărcarea mesajelor a eșuat.", + "jump_to_unread": "Sari la necitit", + "mark_as_read": "Notează ca Citit", + "new_messages": "Mesaje Noi", + "jump_to_latest": "Sari la ultimele mesaje", + "call_started_by": "Apel început de", + "start_voice_call": "Începe apel audio", + "typing": { + "are_typing": " scriu...", + "typing_sep_word": " și " + }, + "is_typing": "scrie...", + "close_threads": "Închide fire", + "search_threads": "Caută fire...", + "clear_search": "Curăță căutarea", + "Threads": { + "no_threads_match_your_search": "Nici un fir nu se potrivește cautării.", + "no_threads_yet": "Înca nu sunt fire.", + "jump": "Sari", + "threads": "Fire", + "thread": "Fir", + "no_replies_yet_start_the_thread_below": "Încă nu are răspunsuri. Începe un fir de mesaje!" + }, + "join_new_room": "Alăturăte unei camere noi", + "this_room_has_been_replaced_and_is_no_longer_active": "Aceasă camera a fost schimbată și nu mai este activă.", + "failed_to_join_replacement_room": "Încercarea de a te alătura camerei noi a eșuat!", + "open_new_room": "Deschide Cameră nouă", + "scroll_to_top": "Derulează în Sus", + "Message": { + "pin_message": "Fixează Mesajul", + "unpin_message": "Scoata fixarea Mesajului", + "forwarded_private_message": "Mesaj privat redirecționat", + "forwarded_from_earlier_in_this_room": "Redirecționat dintr-un mesaj mai vechi din această cameră", + "forwarded_from_another_room": "Redirecționat din altă cameră", + "jump_to_original": "du-te la original", + "failed_to_send": "Trimitere eșuată.", + "only_you_can_see_this": "Doar tu poți vedea asta.", + "edit_message": "Editează mesajul", + "Editor": { + "edit_message": "Editează mesajul..." + } + }, + "Reactions": { + "reacted_with": "Reacționat cu" + } + }, + "RoomInput": { + "recording_duration": "Lungime înregistrare:", + "EmojiBoard": { + "no_results_found": "Nu un rezultat", + "search_results": "Rezultate căutare", + "personal_pack": "Pachet Personal", + "unknown_pack": "Pachet Necunoscut" + } + }, + "DevTools": { + "json_content": "Conținut JSON", + "account_data": "Datele contului", + "developer_tools": "Unelete Dezvoltatori" + } +} diff --git a/public/locales/ro/room/create.json b/public/locales/ro/room/create.json new file mode 100644 index 0000000000..5f0c32455d --- /dev/null +++ b/public/locales/ro/room/create.json @@ -0,0 +1,19 @@ +{ + "versions": "Versiuni", + "founders": "Fondatori", + "special_privileged_users_can_be_assigned_during_creation_these_users_have_e": "In durata creări, anumite persoane pot fi privilegiate. Aceste persoana au un control ridicat si pot fi modificate doar cu o modernizare a camerei.", + "no_suggestions": "Nici o sugestie", + "please_provide_the_user_id_and_hit_enter": "Oferiti ID-ul de utilizator si apăsați Enter.", + "only_member_of_parent_space_can_join": "Doar membrii a spațiului părinte se pot alătura.", + "private": "Privată", + "only_people_with_invite_can_join": "Doar persoanele cu o invitație se pot alătura.", + "anyone_with_the_address_can_join": "Oricine are adresa se poate alătura.", + "public": "Publică", + "address_optional": "Adresă (Opțională)", + "pick_an_unique_address_to_make_it_discoverable": "Alege o adresă unică să poată fi comunitatea descoperită.", + "this_address_is_already_taken_please_select_a_different_one": "Această adresă este deja luată. Selectați una diferită.", + "voice_room": "Cameră vocală", + "live_audio_and_video_conversations": "; conversații audio-video live.", + "chat_room": "Cameră discuții", + "messages_photos_and_videos": "; Mesaje, fotografii, and videoclipuri." +} diff --git a/public/locales/ro/room/direct/invite.json b/public/locales/ro/room/direct/invite.json new file mode 100644 index 0000000000..cf7ade5019 --- /dev/null +++ b/public/locales/ro/room/direct/invite.json @@ -0,0 +1,7 @@ +{ + "direct_message_invite_prompt": "Aceasa este o conversație directă, menită pentru discuții între două persoane, Doriți sa o tranformați intr-un group de conversație înainte de a continua?", + "failed_to_convert_direct_message_to_room": "Transformarea convesației directe in conversație de grup a eșuat!", + "convert_to_group_chat_and_invite": "Transformă in grup și invită", + "invite_to_direct_message_anyway": "Invită la mesaje directe oricum", + "invite_another_member": "Invită altă persoana" +} diff --git a/public/locales/ro/settings/device_verification.json b/public/locales/ro/settings/device_verification.json new file mode 100644 index 0000000000..1d377212cc --- /dev/null +++ b/public/locales/ro/settings/device_verification.json @@ -0,0 +1,28 @@ +{ + "error_uia_action_without_data": "Eroare neașteptată! Actiunea UIA este facută fară date.", + "authentication_failed_failed_to_setup_device_verification": "Autentificare eșuată! Verificarea dispozitivului a eșuat.", + "unexpected_error_crypto_module_not_found": "Eroare neașteptată! Modulul Crypto nu poate fi accessat!", + "unexpected_error_failed_to_create_recovery_key": "Eroare neașteptată! Incercarea de a crea o cheie de rezervă a eșuat.", + "generate_a_recovery_key": "Generați o cheie de recuperare pentru a îți verifica identitatea in cazul in care nu ai access la alte dispozitive. Adițional, setează o parolă ca o alternativă mai ușoară de amintit.", + "optional_passphrase": "Parolă (optională)", + "authentication_steps_to_perform_this_action_are_not_supported_by_client": "Pași pentru autentificare nu sunt acceptați de această aplicație.", + "store_the_recovery_key_in_a_safe_place_for_future_use_as_you_will_need_it_t": "Salvează cheia de recuperare intr-un loc sigur pentru a fi accesată in viitor. Vei avea nevoie de ea sa îți verifici identitatea in cazul in care nu ai acces la alte dispozitive.", + "setup_device_verification": "Configurează verificare dispozitivelor.", + "reset_device_verification": "Reseteaza Identitea de verificare a dispozitivelor", + "resetting_device_verification_is_permanent": "Resetarea verificării dispozitivelor este permanentă!", + "anyone_you_have_verified_with_will_see_security_alerts_and_your_encryption": "Toată lumea cu care ai vorbit va primii o alertă de securitate si encriptia de rezervă va fi pierdută. Aproape sigur nu vrei să faci asta, singura situație în care ai vrea să faci asta este daca ai pierdut", + "and_every_device_you_can_verify_from": "și orice alt dispozitiv cu care ai putea verifica.", + "please_accept_the_request_from_other_device": "Acceptați cererea dintr-un alt dispozitiv.", + "click_accept_to_start_the_verification_process": "Apasa pe 'Acceptă' pentru a începe procesul de verificare.", + "waiting_for_request_to_be_accepted": "Asteptăm ca cererea să fie acceptată...", + "confirm_the_emoji_below_are_displayed_on_both_devices_in_the_same_order": "Confirmă ca emoticoanele prezentate ulterior sunt aceleași pe ambele dispozitive, și în aceași ordine:", + "they_match": "Se potrivesc", + "do_not_match": "Sunt diferite", + "starting_verification_using_emoji_comparison": "Începe verificare prin comparare de emoticoane...", + "your_device_is_verified": "Dispozitivul acesta e verificat.", + "verification_has_been_canceled": "Verificarea a forst oprită.", + "device_verification": "Verificare dispozitiv", + "unexpected_error_verification_is_started_but_verifier_is_missing": "Eroare neașteptată! Verificarea a început dar verificatorul a dispărut.", + "verification_request_has_been_accepted": "Cererea de verificare a fost acceptată.", + "waiting_for_the_response_from_other_device": "Așteptăm pentru un răspuns de la celălalt dispozitiv..." +} diff --git a/public/locales/ro/settings/experimental.json b/public/locales/ro/settings/experimental.json new file mode 100644 index 0000000000..4b2a8c2df5 --- /dev/null +++ b/public/locales/ro/settings/experimental.json @@ -0,0 +1,22 @@ +{ + "save_bandwidth_for_sticker_and_emoji_images": "Reduceți Consumul rețelei pentru stickere si emoticoane", + "enable_bandwidth_saving_for_stickers_and_emojis": "Porneste salvare de 'bandwidth' pentru stickere si emoticoane", + "if_enabled_sticker_and_emoji_images_will_be_optimized_to_save_bandwidth_thi": "Dacă pornit, stickerele si emoticoanele vor fi optimizate pentru a consuma mai puțin internet. Asta ajută consumul de date când văzănd aceste imagini, dar crește costul de computare a server-ului.", + "personas_per_message_profiles": "Personalități. (Profiluri per-mesaj)", + "show_personas_tab": "Prezintă meniul de Personalități", + "enables_the_personas_tab_in_the_settings_menu_for_per_message_profiles": "Adauga meniul de personalități in bara de setări pentru profile specifice fiecărui mesaj.", + "experimental": "Experimental", + "the_features_listed_below_may_be_unstable_or_incomplete": "Opțiunile listate ulterior pot fi instabile sau incomplete,", + "use_at_your_own_risk": "folosește la riscul vostru", + "please_report_any_new_issues_potentially_caused_by_these_features": "Vă rugăm sa raportati orice probleme noi cauzate potențial de aceste opțiuni! Mersi!", + "enable_sharing_of_encrypted_history": "Activează împărtășirea de Istorie Criptată.", + "enable_the_sharehistory_command": "Activează comanda de /sharehistory", + "if_enabled_this_command_will_allow_users_to_share_encrypted_history_with_ot": "Dacă activată, această optiune va permite utilizatorilor să impărtașească istoria criptată cu alți utilizatori noi, per MSC4268.", + "disable_sharehistory_command": "Dezactivează comanda /sharehistory", + "enable_sharehistory_command": "Activează comanda /sharehistory", + "enable_media_galleries_support": "Activează suportul pentru galerii de media", + "enable_media_galleries_title": "Activează galeriile media", + "enable_media_galleries_description": "Dacă activat, mai multe atașamente vor fi trimise intr-un singur mesaj, pe baza MSC4272. Incompatibil cu alte aplicatii care nu mermit galerii", + "disable_media_galleries": "Dezactivează galeriile media", + "enable_media_galleries": "Activează galeriile media" +} diff --git a/public/locales/ro/settings/keyboard_shortcuts.json b/public/locales/ro/settings/keyboard_shortcuts.json new file mode 100644 index 0000000000..1ee207d241 --- /dev/null +++ b/public/locales/ro/settings/keyboard_shortcuts.json @@ -0,0 +1,16 @@ +{ + "search_for_messages": "Caută mesaje", + "jump_to_the_highest_priority_unread_room": "Du-te la camera cu cea mai ridicata prioritate de necitire", + "go_to_next_unread_room_cycle": "Du-te la următoarea camera necitită (urmează un ciclu)", + "go_to_previous_unread_room_cycle": "Go to previous unread room (urmează un ciclu)", + "undo_in_message_editor": "Șterge în editorul de mesaje", + "redo_in_message_editor": "Reface în editorul de mesaje", + "seach_and_go_to_room": "Caută și du-te la cameră", + "open_sticker_picker": "Deschide pachet de stickere", + "bold": "Îngroșat", + "italic": "Italic", + "underline": "Subliniat", + "general": "General", + "navigation": "Navigație", + "messages": "Mesaje" +} diff --git a/public/locales/ro/settings/persona.json b/public/locales/ro/settings/persona.json new file mode 100644 index 0000000000..93dd8e48c7 --- /dev/null +++ b/public/locales/ro/settings/persona.json @@ -0,0 +1,21 @@ +{ + "limited_compatibility_with_pluralkit_like_functions": "Compatibilitate limitată cu functii similare cu PluralKit", + "enable_pk_commands": "Activează comenzi PK", + "if_enabled_it_will_enable_a_few_pk_style_commands_currently_verry_limited": "Dacă activat, va permite folosirea de comenzi in stil PK, deocamdată foarte limitate", + "disable_pk_commands": "dezactivează comenzile pk;", + "enable_pks_commands": "activează comenzile pk;", + "enable_shorthands": "Activează scurtături", + "if_enabled_you_can_use_shorthands_to_use_a_persona_for_one_message_only_eg": "Dacă activat, vei putea folosii scuraturi pentru a folosi personalități pentru un mesaj (ex. '✨:test')", + "disable_checking_typed_messages_for_shorthands": "dezactivează verificarea mesajelor scrise pentru scurataturi", + "enable_checking_typed_messages_for_shorthands": "activeaza verificarea mesajelor scrise pentru scurataturi", + "profile_id": "ID profil:", + "pronouns": "Pronume:", + "reset_pronouns": "Resetează Pronumele", + "display_name": "Nume Afișat:", + "delete_profile": "Șterge profilul {{profileId}}", + "save_profile": "Salvează schimbările pentru {{profileId}}", + "save_profile_button_area": "Zona butonului de salvare a profilului {{profileId}}", + "pronouns_for": "Pronumele profilului {{profileId}}", + "avatar_for": "Avatarul profilului {{profileId}}", + "display_name_for": "Numele afișat pentru {{profileId}}" +} diff --git a/public/locales/ro/settings/profile.json b/public/locales/ro/settings/profile.json new file mode 100644 index 0000000000..d85dda76aa --- /dev/null +++ b/public/locales/ro/settings/profile.json @@ -0,0 +1,8 @@ +{ + "avatar_and_upload": "Imagine de profil si încărcare", + "profile_avatar": "Imagine de profil", + "avatar_fallback": "Imagine de rezervă", + "upload_avatar_image": "Încarcă imagine de profil", + "display_name_input": "Afiș introducere nume de profil", + "reset_display_name": "Resetează numele afișat" +} diff --git a/src/app/components/AccountDataEditor.tsx b/src/app/components/AccountDataEditor.tsx index 90ed7907c2..f67baaba38 100644 --- a/src/app/components/AccountDataEditor.tsx +++ b/src/app/components/AccountDataEditor.tsx @@ -23,6 +23,7 @@ import { useTextAreaCodeEditor } from '$hooks/useTextAreaCodeEditor'; import { Page, PageHeader } from './page'; import { SequenceCard } from './sequence-card'; import { TextViewerContent } from './text-viewer'; +import { t } from 'i18next'; const EDITOR_INTENT_SPACE_COUNT = 2; @@ -121,7 +122,7 @@ function AccountDataEdit({ aria-disabled={submitting} > - Account Data + {t('DevTools.account_data')} } > - Save + {t('General.save')} @@ -165,7 +166,7 @@ function AccountDataEdit({ - JSON Content + {t('DevTools.json_content')} - Account Data + {t('DevTools.account_data')} - JSON Content + {t('DevTools.json_content')} - Developer Tools + {t('DevTools.developer_tools')} diff --git a/src/app/components/DeviceVerification.tsx b/src/app/components/DeviceVerification.tsx index 4979c54e7a..04c432e3e7 100644 --- a/src/app/components/DeviceVerification.tsx +++ b/src/app/components/DeviceVerification.tsx @@ -26,6 +26,7 @@ import { } from '$hooks/useVerificationRequest'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import { ContainerColor } from '$styles/ContainerColor.css'; +import { t } from 'i18next'; const DialogHeaderStyles: CSSProperties = { padding: `0 ${config.space.S200} 0 ${config.space.S400}`, @@ -50,7 +51,7 @@ function VerificationUnexpected({ message, onClose }: VerificationUnexpectedProp {message} ); @@ -59,8 +60,10 @@ function VerificationUnexpected({ message, onClose }: VerificationUnexpectedProp function VerificationWaitAccept() { return ( - Please accept the request from other device. - + {t('Settings.device_verification.please_accept_the_request_from_other_device')} + ); } @@ -74,7 +77,9 @@ function VerificationAccept({ onAccept }: VerificationAcceptProps) { const accepting = acceptState.status === AsyncStatus.Loading; return ( - Click accept to start the verification process. + + {t('Settings.device_verification.click_accept_to_start_the_verification_process')} + ); @@ -91,8 +96,10 @@ function VerificationAccept({ onAccept }: VerificationAcceptProps) { function VerificationWaitStart() { return ( - Verification request has been accepted. - + {t('Settings.device_verification.verification_request_has_been_accepted')} + ); } @@ -107,7 +114,9 @@ function AutoVerificationStart({ onStart }: VerificationStartProps) { return ( - + ); } @@ -129,7 +138,11 @@ function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) { return ( - Confirm the emoji below are displayed on both devices, in the same order: + + {t( + 'Settings.device_verification.confirm_the_emoji_below_are_displayed_on_both_devices_in_the_same_order' + )} + } > - They Match + {t('Settings.device_verification.they_match')} @@ -190,7 +203,9 @@ function SasVerification({ verifier, onCancel }: SasVerificationProps) { return ( - + ); } @@ -202,10 +217,10 @@ function VerificationDone({ onExit }: VerificationDoneProps) { return (
- Your device is verified. + {t('Settings.device_verification.your_device_is_verified')}
); @@ -217,9 +232,9 @@ type VerificationCanceledProps = { function VerificationCanceled({ onClose }: VerificationCanceledProps) { return ( - Verification has been canceled. + {t('Settings.device_verification.verification_has_been_canceled')} ); @@ -269,7 +284,7 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps)
- Device Verification + {t('Settings.device_verification.device_verification')} {composerIcon(X)} @@ -293,7 +308,9 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps) ) : ( ))} diff --git a/src/app/components/DeviceVerificationSetup.tsx b/src/app/components/DeviceVerificationSetup.tsx index cf4e21c41d..2320dbb357 100644 --- a/src/app/components/DeviceVerificationSetup.tsx +++ b/src/app/components/DeviceVerificationSetup.tsx @@ -15,6 +15,7 @@ import { useAlive } from '$hooks/useAlive'; import { PasswordInput } from './password-input'; import { ActionUIA, ActionUIAFlowsLoader } from './ActionUIA'; import { UseStateProvider } from './UseStateProvider'; +import { useTranslation } from 'react-i18next'; type UIACallback = ( authDict: AuthDict | null @@ -57,12 +58,8 @@ function makeUIAAction( return action; } -function renderUnsupportedUIAFlow() { - return ( - - Authentication steps to perform this action are not supported by client. - - ); +function renderUnsupportedUIAFlow(unsupportedFlow: string) { + return {unsupportedFlow}; } type SetupVerificationUIAProps = { @@ -89,6 +86,7 @@ type SetupVerificationProps = { function SetupVerification({ onComplete }: Readonly) { const mx = useMatrixClient(); const alive = useAlive(); + const { t } = useTranslation(['settings/device_verification', 'general']); const [uiaAction, setUIAAction] = useState>(); const [nextAuthData, setNextAuthData] = useState(); // null means no next action. @@ -96,7 +94,7 @@ function SetupVerification({ onComplete }: Readonly) { const handleAction = useCallback( async (authDict: AuthDict) => { if (!uiaAction) { - throw new Error('Unexpected Error! UIA action is perform without data.'); + throw new Error(t('error_uia_action_without_data')); } if (alive()) { setNextAuthData(null); @@ -107,7 +105,7 @@ function SetupVerification({ onComplete }: Readonly) { setNextAuthData(authData); } }, - [uiaAction, alive] + [uiaAction, alive, t] ); const resetUIA = useCallback(() => { @@ -139,25 +137,25 @@ function SetupVerification({ onComplete }: Readonly) { if (alive()) { setUIAAction(action); } else { - reject(new Error('Authentication failed! Failed to setup device verification.')); + reject(new Error(t('authentication_failed_failed_to_setup_device_verification'))); } return; } reject(error); }); }), - [alive, resetUIA] + [alive, resetUIA, t] ); const [setupState, setup] = useAsyncCallback( useCallback( async (passphrase) => { const crypto = mx.getCrypto(); - if (!crypto) throw new Error('Unexpected Error! Crypto module not found!'); + if (!crypto) throw new Error(t('unexpected_error_crypto_module_not_found')); const recoveryKeyData = await crypto.createRecoveryKeyFromPassphrase(passphrase); if (!recoveryKeyData.encodedPrivateKey) { - throw new Error('Unexpected Error! Failed to create recovery key.'); + throw new Error(t('unexpected_error_failed_to_create_recovery_key')); } clearSecretStorageKeys(); @@ -175,7 +173,7 @@ function SetupVerification({ onComplete }: Readonly) { onComplete(recoveryKeyData.encodedPrivateKey); }, - [mx, onComplete, authUploadDeviceSigningKeys] + [mx, onComplete, authUploadDeviceSigningKeys, t] ) ); @@ -212,12 +210,9 @@ function SetupVerification({ onComplete }: Readonly) { 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('generate_a_recovery_key')} - Passphrase (Optional) + {t('optional_passphrase')} {setupState.status === AsyncStatus.Error && ( - {setupState.error ? setupState.error.message : 'Unexpected Error!'} + + {setupState.error ? setupState.error.message : t('unexpected_error', { ns: 'general' })} + )} {nextAuthData !== null && uiaAction && ( + renderUnsupportedUIAFlow( + t('authentication_steps_to_perform_this_action_are_not_supported_by_client') + ) + } > {renderSetupVerificationUIA} @@ -249,6 +250,7 @@ type RecoveryKeyDisplayProps = { }; function RecoveryKeyDisplay({ recoveryKey }: Readonly) { const [show, setShow] = useState(false); + const { t } = useTranslation(['settings/device_verification', 'general']); const handleCopy = () => { copyToClipboard(recoveryKey); @@ -266,11 +268,10 @@ function RecoveryKeyDisplay({ recoveryKey }: Readonly) 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('store_the_recovery_key_in_a_safe_place_for_future_use_as_you_will_need_it_t')} - Recovery Key + {t('recovery_key', { ns: 'general' })} ) {safeToDisplayKey} setShow(!show)} variant="Secondary" radii="Pill"> - {show ? 'Hide' : 'Show'} + + {show ? t('hide', { ns: 'general' }) : t('show', { ns: 'general' })} + @@ -307,6 +310,7 @@ type DeviceVerificationSetupProps = { export const DeviceVerificationSetup = forwardRef( ({ onCancel }, ref) => { const [recoveryKey, setRecoveryKey] = useState(); + const { t } = useTranslation(['settings/device_verification']); return ( @@ -319,7 +323,7 @@ export const DeviceVerificationSetup = forwardRef - Setup Device Verification + {t('setup_device_verification')} {composerIcon(X)} @@ -342,6 +346,7 @@ type DeviceVerificationResetProps = { export const DeviceVerificationReset = forwardRef( ({ onCancel }, ref) => { const [reset, setReset] = useState(false); + const { t } = useTranslation(['settings/device_verification', 'general']); return ( @@ -354,7 +359,7 @@ export const DeviceVerificationReset = forwardRef - Reset Device Verification + {t('reset_device_verification')} {composerIcon(X)} @@ -376,16 +381,16 @@ export const DeviceVerificationReset = forwardRef ✋🧑‍🚒🤚 - Resetting device verification is permanent. + {t('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('anyone_you_have_verified_with_will_see_security_alerts_and_your_encryption')}{' '} + {t('recovery_key', { ns: 'general' })} /{' '} + {t('recovery_passphrase', { ns: 'general' })}{' '} + {t('and_every_device_you_can_verify_from')} )} diff --git a/src/app/components/create-room/AdditionalCreatorInput.tsx b/src/app/components/create-room/AdditionalCreatorInput.tsx index c7b7710d3e..4f82f26d07 100644 --- a/src/app/components/create-room/AdditionalCreatorInput.tsx +++ b/src/app/components/create-room/AdditionalCreatorInput.tsx @@ -27,6 +27,7 @@ import type { UseAsyncSearchOptions } from '$hooks/useAsyncSearch'; import { useAsyncSearch } from '$hooks/useAsyncSearch'; import { highlightText, makeHighlightRegex } from '$plugins/react-custom-html-parser'; import { SettingTile } from '$components/setting-tile'; +import { useTranslation } from 'react-i18next'; export const useAdditionalCreators = (defaultCreators?: string[]) => { const mx = useMatrixClient(); @@ -82,6 +83,7 @@ export function AdditionalCreatorInput({ const mx = useMatrixClient(); const [menuCords, setMenuCords] = useState(); const directUsers = useDirectUsers(); + const { t } = useTranslation('room/create'); const [validUserId, setValidUserId] = useState(); const filteredUsers = useMemo( @@ -146,8 +148,8 @@ export function AdditionalCreatorInput({ return ( @@ -262,10 +264,10 @@ export function AdditionalCreatorInput({ gap="100" > - No Suggestions + {t('no_suggestions')} - Please provide the user ID and hit Enter. + {t('please_provide_the_user_id_and_hit_enter')} )} diff --git a/src/app/components/create-room/CreateRoomAccessSelector.tsx b/src/app/components/create-room/CreateRoomAccessSelector.tsx index ba115055a5..61faa66e2c 100644 --- a/src/app/components/create-room/CreateRoomAccessSelector.tsx +++ b/src/app/components/create-room/CreateRoomAccessSelector.tsx @@ -4,6 +4,7 @@ import { Check, sizedIcon } from '$components/icons/phosphor'; import { SequenceCard } from '$components/sequence-card'; import { SettingTile } from '$components/setting-tile'; import { CreateRoomAccess } from './types'; +import { useTranslation } from 'react-i18next'; type CreateRoomAccessSelectorProps = { value?: CreateRoomAccess; @@ -19,6 +20,7 @@ export function CreateRoomAccessSelector({ disabled, getIcon, }: CreateRoomAccessSelectorProps) { + const { t } = useTranslation('room/create'); return ( {canRestrict && ( @@ -39,7 +41,7 @@ export function CreateRoomAccessSelector({ > Restricted - Only member of parent space can join. + {t('only_member_of_parent_space_can_join')} @@ -59,9 +61,9 @@ export function CreateRoomAccessSelector({ before={getIcon(CreateRoomAccess.Private)} after={value === CreateRoomAccess.Private && sizedIcon(Check)} > - Private + {t('private')} - Only people with invite can join. + {t('only_people_with_invite_can_join')} @@ -80,9 +82,9 @@ export function CreateRoomAccessSelector({ before={getIcon(CreateRoomAccess.Public)} after={value === CreateRoomAccess.Public && sizedIcon(Check)} > - Public + {t('public')} - Anyone with the address can join. + {t('anyone_with_the_address_can_join')} diff --git a/src/app/components/create-room/CreateRoomAliasInput.tsx b/src/app/components/create-room/CreateRoomAliasInput.tsx index bc5d12e2fd..97e68ae768 100644 --- a/src/app/components/create-room/CreateRoomAliasInput.tsx +++ b/src/app/components/create-room/CreateRoomAliasInput.tsx @@ -10,6 +10,7 @@ import type { AsyncState } from '$hooks/useAsyncCallback'; import { AsyncStatus, useAsync } from '$hooks/useAsyncCallback'; import { useDebounce } from '$hooks/useDebounce'; import { getMxIdServer } from '$utils/mxIdHelper'; +import { t } from 'i18next'; export function CreateRoomAliasInput({ disabled }: { disabled?: boolean }) { const mx = useMatrixClient(); @@ -74,9 +75,9 @@ export function CreateRoomAliasInput({ disabled }: { disabled?: boolean }) { return ( - Address (Optional) + {t('RoomCreate.address_optional')} - Pick an unique address to make it discoverable. + {t('RoomCreate.pick_an_unique_address_to_make_it_discoverable')} {sizedIcon(Warning, '50', { filled: true })} - This address is already taken. Please select a different one. + {t('RoomCreate.this_address_is_already_taken_please_select_a_different_one')} )} diff --git a/src/app/components/create-room/CreateRoomTypeSelector.tsx b/src/app/components/create-room/CreateRoomTypeSelector.tsx index 6ab26d7c17..7ff9159f30 100644 --- a/src/app/components/create-room/CreateRoomTypeSelector.tsx +++ b/src/app/components/create-room/CreateRoomTypeSelector.tsx @@ -5,6 +5,7 @@ import { SequenceCard } from '$components/sequence-card'; import { SettingTile } from '$components/setting-tile'; import { BetaNoticeBadge } from '$components/BetaNoticeBadge'; import { CreateRoomType } from './types'; +import { t } from 'i18next'; type CreateRoomTypeSelectorProps = { value?: CreateRoomType; @@ -37,10 +38,10 @@ export function CreateRoomTypeSelector({ > - Chat Room + {t('RoomCreate.chat_room')} - - Messages, photos, and videos. + {t('RoomCreate.messages_photos_and_videos')} @@ -62,10 +63,10 @@ export function CreateRoomTypeSelector({ > - Voice Room + {t('RoomCreate.voice_room')} - - Live audio and video conversations. + {t('RoomCreate.live_audio_and_video_conversations')} diff --git a/src/app/components/create-room/RoomVersionSelector.tsx b/src/app/components/create-room/RoomVersionSelector.tsx index 1f7179d880..ac41343ec4 100644 --- a/src/app/components/create-room/RoomVersionSelector.tsx +++ b/src/app/components/create-room/RoomVersionSelector.tsx @@ -7,6 +7,7 @@ import FocusTrap from 'focus-trap-react'; import { stopPropagation } from '$utils/keyboard'; import { SettingTile } from '$components/setting-tile'; import { SequenceCard } from '$components/sequence-card'; +import { t } from 'i18next'; export function RoomVersionSelector({ versions, @@ -64,7 +65,7 @@ export function RoomVersionSelector({ gap="200" style={{ padding: config.space.S200, maxWidth: toRem(300) }} > - Versions + {t('RoomCreate.versions')} {versions.map((version) => ( void; @@ -31,6 +32,7 @@ export function DirectInvitePrompt({ converting, convertError, }: DirectInvitePromptProps) { + const { t } = useTranslation(['room/direct/invite', 'general']); return ( }> @@ -52,7 +54,7 @@ export function DirectInvitePrompt({ size="500" > - Invite another Member + {t('invite_another_member')} {composerIcon(X)} @@ -60,13 +62,10 @@ export function DirectInvitePrompt({
- - This is a Direct Message room, intended for a conversation between two - persons. Would you like to convert it into a group chat before continuing? - + {t('direct_message_invite_prompt')} {convertError && ( - Failed to convert direct message to room! {convertError} + {t('failed_to_convert_direct_message_to_room')} {convertError} )} @@ -81,7 +80,7 @@ export function DirectInvitePrompt({ aria-disabled={converting} > - {converting ? 'Converting...' : 'Convert to Group Chat and Invite'} + {converting ? 'Converting...' : t('convert_to_group_chat_and_invite')}
diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index 2aae96460e..82cd4cf179 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -59,6 +59,7 @@ import { } from './components'; import type { GifData } from './types'; import { EmojiBoardTab, EmojiType } from './types'; +import { t } from 'i18next'; import { useClientConfig } from '$hooks/useClientConfig'; import { useFavoriteGifs } from '$hooks/useFavoriteGifs'; @@ -109,7 +110,10 @@ const useGroups = ( imagePacks.forEach((pack) => { let label = pack.meta.name; - if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name; + if (!label) + label = isUserId(pack.id) + ? t('RoomInput.EmojiBoard.personal_pack') + : mx.getRoom(pack.id)?.name; g.push({ id: pack.id, @@ -137,7 +141,10 @@ const useGroups = ( imagePacks.forEach((pack) => { let label = pack.meta.name; - if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name; + if (!label) + label = isUserId(pack.id) + ? t('RoomInput.EmojiBoard.personal_pack') + : mx.getRoom(pack.id)?.name; g.push({ id: pack.id, @@ -275,7 +282,10 @@ function EmojiSidebar({ {packs.map((pack) => { let label = pack.meta.name; - if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name; + if (!label) + label = isUserId(pack.id) + ? t('RoomInput.EmojiBoard.personal_pack') + : mx.getRoom(pack.id)?.name; // limit width and height to 36 to prevent very large icons from breaking the layout, since custom emoji pack icons can be of any size // trying to get close to the render target size of the icons in the sidebar, which is around 24px @@ -288,7 +298,7 @@ function EmojiSidebar({ key={pack.id} active={activeGroupId === pack.id} id={pack.id} - label={label ?? 'Unknown Pack'} + label={label ?? t('RoomInput.EmojiBoard.unknown_pack')} url={url ?? undefined} onClick={handleScrollToGroup} /> @@ -360,7 +370,7 @@ function StickerSidebar({ key={pack.id} active={activeGroupId === pack.id} id={pack.id} - label={label ?? 'Unknown Pack'} + label={label ?? t('RoomInput.EmojiBoard.unknown_pack')} url={url ?? undefined} onClick={handleScrollToGroup} /> @@ -822,7 +832,11 @@ export function EmojiBoard({ {tab !== EmojiBoardTab.Gif && searchedItems && ( {searchedItems.map((element, index) => renderItem(element, index))} diff --git a/src/app/components/message/Reply.tsx b/src/app/components/message/Reply.tsx index 1178b81b63..f19eff5243 100644 --- a/src/app/components/message/Reply.tsx +++ b/src/app/components/message/Reply.tsx @@ -277,7 +277,7 @@ export const Reply = as<'div', ReplyProps>( const ignoredUsers = useIgnoredUsers(); const isBlockedSender = !!sender && ignoredUsers.includes(sender); - const { t } = useTranslation(); + const { t } = useTranslation('general'); const parseMemberEvent = useMemberEventParser(); diff --git a/src/app/features/room/AudioMessageRecorder.tsx b/src/app/features/room/AudioMessageRecorder.tsx index f0db57b3dd..a5518ca76a 100644 --- a/src/app/features/room/AudioMessageRecorder.tsx +++ b/src/app/features/room/AudioMessageRecorder.tsx @@ -12,6 +12,7 @@ import { useVoiceRecorder } from '$plugins/voice-recorder-kit'; import type { VoiceRecorderStopPayload } from '$plugins/voice-recorder-kit'; import { Box, Text } from 'folds'; import * as css from './AudioMessageRecorder.css'; +import { t } from 'i18next'; export type AudioRecordingCompletePayload = { audioBlob: Blob; @@ -196,7 +197,7 @@ export const AudioMessageRecorder = forwardRef< {announcedTime > 0 && announcedTime === seconds && ( - Recording duration: {formatTime(announcedTime)} + {t('RoomInput.recording_duration')} {formatTime(announcedTime)} )}
diff --git a/src/app/features/room/MembersDrawer.tsx b/src/app/features/room/MembersDrawer.tsx index fec83a0588..4bc8e289e5 100644 --- a/src/app/features/room/MembersDrawer.tsx +++ b/src/app/features/room/MembersDrawer.tsx @@ -66,6 +66,7 @@ import { formatCompactNumber } from '$utils/formatCompactNumber'; import * as css from './MembersDrawer.css'; import { SidebarResizer } from '$pages/client/sidebar/SidebarResizer'; import { useScreenSizeContext, ScreenSize } from '$hooks/useScreenSize'; +import { t } from 'i18next'; type MemberDrawerHeaderProps = { room: Room; @@ -473,7 +474,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) { radii="Pill" outlined size="300" - aria-label="Scroll to Top" + aria-label={t('RoomView.scroll_to_top')} > {composerIcon(CaretUp)} diff --git a/src/app/features/room/RoomCallButton.test.tsx b/src/app/features/room/RoomCallButton.test.tsx index 7f4ce6ae6c..6fc10e3b19 100644 --- a/src/app/features/room/RoomCallButton.test.tsx +++ b/src/app/features/room/RoomCallButton.test.tsx @@ -3,6 +3,7 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import type * as JotaiModule from 'jotai'; import type { Room } from '$types/matrix-sdk'; import { RoomCallButton } from './RoomCallButton'; +import { t } from 'i18next'; const { startCallMock, useCallJoinedMock } = vi.hoisted(() => ({ startCallMock: vi.fn<(...args: unknown[]) => void>(), @@ -40,7 +41,7 @@ describe('RoomCallButton', () => { /> ); - fireEvent.click(screen.getByRole('button', { name: /start voice call/i })); + fireEvent.click(screen.getByRole('button', { name: t('RoomView.start_voice_call') })); await waitFor(() => { expect(startCallMock).toHaveBeenCalledWith(room, { @@ -61,7 +62,7 @@ describe('RoomCallButton', () => { /> ); - fireEvent.click(screen.getByRole('button', { name: /start video call/i })); + fireEvent.click(screen.getByRole('button', { name: t('RoomView.start_voice_call') })); await waitFor(() => { expect(startCallMock).toHaveBeenCalledWith(room, { diff --git a/src/app/features/room/RoomCallButton.tsx b/src/app/features/room/RoomCallButton.tsx index b87ecd8390..16075e3537 100644 --- a/src/app/features/room/RoomCallButton.tsx +++ b/src/app/features/room/RoomCallButton.tsx @@ -5,6 +5,7 @@ import type { Room } from '$types/matrix-sdk'; import { useCallStart, useCallJoined } from '$hooks/useCallEmbed'; import type { CallPreferences } from '$state/callPreferences'; import { callEmbedAtom } from '$state/callEmbed'; +import { t } from 'i18next'; interface RoomCallButtonProps { room: Room; @@ -43,7 +44,7 @@ export function RoomCallButton({ }; const readyCopy = startingVideoCall ? 'Start Video Call' : 'Start Voice Call'; - const ariaLabel = startingVideoCall ? 'Start Video Call' : 'Start Voice Call'; + // const ariaLabel = startingVideoCall ? 'Start Video Call' : 'Start Voice Call'; const icon = startingVideoCall ? VideoCamera : Phone; return ( @@ -66,9 +67,9 @@ export function RoomCallButton({ {composerIcon(icon)} diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 6a48ff9898..1c4549af33 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -74,6 +74,7 @@ import { } from '$hooks/timeline/useProcessedTimeline'; import { useTimelineEventRenderer } from '$hooks/timeline/useTimelineEventRenderer'; import * as css from './RoomTimeline.css'; +import { t } from 'i18next'; const TimelineFloat = as<'div', css.TimelineFloatVariants>( ({ position, className, ...props }, ref) => ( @@ -886,7 +887,7 @@ export function RoomTimeline({ style={{ padding: config.space.S300 }} > - Failed to load history. + {t('RoomView.failed_to_load_history')} timelineSync.handleTimelinePagination(true)} > - Retry + {t('General.retry')}
); @@ -912,7 +913,7 @@ export function RoomTimeline({ style={{ padding: config.space.S300 }} > - Failed to load messages. + {t('RoomView.failed_to_load_messages')} timelineSync.handleTimelinePagination(false)} > - Retry + {t('General.retry')} ); @@ -1050,7 +1051,7 @@ export function RoomTimeline({ before={chipIcon(ChatTeardropDots)} onClick={() => timelineSync.loadEventTimeline(unreadInfo.readUptoEventId)} > - Jump to Unread + {t('RoomView.jump_to_unread')} markAsRead(mx, room.roomId, hideReads)} > - Mark as Read + {t('RoomView.mark_as_read')} )} @@ -1148,7 +1149,7 @@ export function RoomTimeline({ MozUserSelect: 'none', }} > - Jump to Latest + {t('RoomView.jump_to_latest')} )} diff --git a/src/app/features/room/RoomTombstone.tsx b/src/app/features/room/RoomTombstone.tsx index 0979b8acfc..421904e091 100644 --- a/src/app/features/room/RoomTombstone.tsx +++ b/src/app/features/room/RoomTombstone.tsx @@ -9,6 +9,7 @@ import { getViaServers } from '$plugins/via-servers'; import { RoomInputPlaceholder } from './RoomInputPlaceholder'; import * as css from './RoomTombstone.css'; import { KnownMembership } from '$types/matrix-sdk'; +import { t } from 'i18next'; type RoomTombstoneProps = { roomId: string; body?: string; replacementRoomId: string }; export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstoneProps) { @@ -34,10 +35,12 @@ export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstone return ( - {body || 'This room has been replaced and is no longer active.'} + + {body || t('RoomView.this_room_has_been_replaced_and_is_no_longer_active')} + {joinState.status === AsyncStatus.Error && ( - {(joinState.error as Error)?.message ?? 'Failed to join replacement room!'} + {(joinState.error as Error)?.message ?? t('RoomView.failed_to_join_replacement_room')} )} @@ -45,7 +48,7 @@ export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstone {replacementRoom?.getMyMembership() === KnownMembership.Join || joinState.status === AsyncStatus.Success ? ( ) : ( )} diff --git a/src/app/features/room/RoomViewTyping.tsx b/src/app/features/room/RoomViewTyping.tsx index 98267fdd85..7716325746 100644 --- a/src/app/features/room/RoomViewTyping.tsx +++ b/src/app/features/room/RoomViewTyping.tsx @@ -11,6 +11,7 @@ import { useMatrixClient } from '$hooks/useMatrixClient'; import { useRoomTypingMember } from '$hooks/useRoomTypingMembers'; import { nicknamesAtom } from '$state/nicknames'; import * as css from './RoomViewTyping.css'; +import { t } from 'i18next'; export type RoomViewTypingProps = { room: Room; @@ -62,7 +63,7 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>( <> {typingNames[0]} - {' is typing...'} + {` ${t('RoomView.is_typing')}`} )} @@ -70,11 +71,11 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>( <> {typingNames[0]} - {' and '} + {t('RoomView.typing.typing_sep_word')} {typingNames[1]} - {' are typing...'} + {t('RoomView.typing.are_typing')} )} @@ -86,11 +87,11 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>( {typingNames[1]} - {' and '} + {t('RoomView.typing.typing_sep_word')} {typingNames[2]} - {' are typing...'} + {t('RoomView.typing.are_typing')} )} @@ -106,11 +107,11 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>( {typingNames[2]} - {' and '} + {t('RoomView.typing.typing_sep_word')} {typingNames.length - 3} others - {' are typing...'} + {t('RoomView.typing.are_typing')} )} diff --git a/src/app/features/room/ThreadBrowser.tsx b/src/app/features/room/ThreadBrowser.tsx index cb3cd904b7..a18fe73b31 100644 --- a/src/app/features/room/ThreadBrowser.tsx +++ b/src/app/features/room/ThreadBrowser.tsx @@ -62,6 +62,7 @@ import { EncryptedContent } from './message'; import * as css from './ThreadDrawer.css'; import { SidebarResizer } from '$pages/client/sidebar/SidebarResizer'; import { mobileOrTablet } from '$utils/user-agent'; +import { t } from 'i18next'; type ThreadPreviewProps = { room: Room; @@ -222,7 +223,7 @@ function ThreadPreview({ room, thread, onClick, onJump }: ThreadPreviewProps) { )} - Jump + {t('RoomView.Threads.jump')} @@ -448,8 +449,8 @@ export function ThreadBrowser({ room, onOpenThread, onClose, overlay }: ThreadBr const lowerQuery = query.trim().toLowerCase(); const threads = lowerQuery - ? allThreads.filter((t: Thread) => { - const body = t.rootEvent?.getContent()?.body ?? ''; + ? allThreads.filter((fullThread: Thread) => { + const body = fullThread.rootEvent?.getContent()?.body ?? ''; return typeof body === 'string' && body.toLowerCase().includes(lowerQuery); }) : allThreads; @@ -490,7 +491,7 @@ export function ThreadBrowser({ room, onOpenThread, onClose, overlay }: ThreadBr {composerIcon(Chats)} - Threads + {t('RoomView.Threads.threads')} @@ -499,7 +500,7 @@ export function ThreadBrowser({ room, onOpenThread, onClose, overlay }: ThreadBr variant="SurfaceVariant" size="300" radii="300" - aria-label="Close threads" + aria-label={t('RoomView.close_threads')} > {composerIcon(X)} @@ -516,7 +517,7 @@ export function ThreadBrowser({ room, onOpenThread, onClose, overlay }: ThreadBr ref={searchRef} value={query} onChange={handleSearchChange} - placeholder="Search threads..." + placeholder={t('RoomView.search_threads')} variant="Surface" size="400" radii="400" @@ -531,7 +532,7 @@ export function ThreadBrowser({ room, onOpenThread, onClose, overlay }: ThreadBr setQuery(''); searchRef.current?.focus(); }} - aria-label="Clear search" + aria-label={t('RoomView.clear_search')} > {chipIcon(X)} @@ -571,7 +572,9 @@ export function ThreadBrowser({ room, onOpenThread, onClose, overlay }: ThreadBr > {composerIcon(Chats, { style: { opacity: 0.6 } })} - {lowerQuery ? 'No threads match your search.' : 'No threads yet.'} + {lowerQuery + ? t('RoomView.Threads.no_threads_match_your_search') + : t('RoomView.Threads.no_threads_yet')} ); diff --git a/src/app/features/room/ThreadDrawer.tsx b/src/app/features/room/ThreadDrawer.tsx index fdf59c221c..86ab4255c2 100644 --- a/src/app/features/room/ThreadDrawer.tsx +++ b/src/app/features/room/ThreadDrawer.tsx @@ -71,6 +71,7 @@ import { RoomViewFollowing, RoomViewFollowingPlaceholder } from './RoomViewFollo import * as css from './ThreadDrawer.css'; import { SidebarResizer } from '$pages/client/sidebar/SidebarResizer'; import { mobileOrTablet } from '$utils/user-agent'; +import { t } from 'i18next'; /** * Resolve the list of reply events to show in the thread drawer. @@ -505,11 +506,11 @@ export function ThreadDrawer({ room, threadRootId, onClose, overlay }: ThreadDra hasOlderRepliesRef.current = hasOlderReplies; const loadOlderReplies = useCallback(() => { - const t = room.getThread(threadRootId); - if (!t || !t.initialEventsFetched || paginatingOlderRef.current) return; + const fullThread = room.getThread(threadRootId); + if (!fullThread || !fullThread.initialEventsFetched || paginatingOlderRef.current) return; paginatingOlderRef.current = true; setLoadingOlderReplies(true); - mx.paginateEventTimeline(t.timelineSet.getLiveTimeline(), { backwards: true }) + mx.paginateEventTimeline(fullThread.timelineSet.getLiveTimeline(), { backwards: true }) .then((hasMore) => { paginatingOlderRef.current = false; if (!hasMore) setCanPageBack(false); @@ -836,7 +837,7 @@ export function ThreadDrawer({ room, threadRootId, onClose, overlay }: ThreadDra {composerIcon(Chats)} - Thread + {t('RoomView.Threads.thread')} @@ -929,7 +930,7 @@ export function ThreadDrawer({ room, threadRootId, onClose, overlay }: ThreadDra > {composerIcon(Chats, { style: { opacity: 0.6 } })} - No replies yet. Start the thread below! + {t('RoomView.Threads.no_replies_yet_start_the_thread_below')} ); diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index 81293f9a19..eb7add5418 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -56,6 +56,7 @@ import type { PerMessageProfileBeeperFormat } from '$hooks/usePerMessageProfile' import { convertBeeperFormatToOurPerMessageProfile } from '$hooks/usePerMessageProfile'; import { MessageEditor } from './MessageEditor'; import * as css from './styles.css'; +import { t } from 'i18next'; import { modalAtom, ModalType } from '$state/modal'; import { OptionQuickMenu } from '$components/message/modals/Options'; @@ -660,10 +661,10 @@ function MessageInternal( const originalRoomId = messageForwardedProps.originalRoomId; return { label: messageForwardedProps.originalEventPrivate - ? 'Forwarded private message' + ? t('RoomView.Message.forwarded_private_message') : isSameRoomForward(originalRoomId) - ? 'Forwarded from earlier in this room' - : 'Forwarded from another room', + ? t('RoomView.Message.forwarded_from_earlier_in_this_room') + : t('RoomView.Message.forwarded_from_another_room'), roomId: originalRoomId, eventId: messageForwardedProps.originalEventId, ts: messageForwardedProps.originalTimestamp ?? 0, @@ -675,8 +676,8 @@ function MessageInternal( const originalRoomId = msc2723ForwardedMessageProps.room_id; return { label: isSameRoomForward(originalRoomId) - ? 'Forwarded from earlier in this room' - : 'Forwarded from another room', + ? t('RoomView.Message.forwarded_from_earlier_in_this_room') + : t('RoomView.Message.forwarded_from_another_room'), roomId: originalRoomId, eventId: msc2723ForwardedMessageProps.event_id, ts: msc2723ForwardedMessageProps.origin_server_ts ?? 0, @@ -732,7 +733,7 @@ function MessageInternal( data-mention-event-id={forwardedNotice.eventId} onClick={mentionClickHandler} > - jump to original + {t('RoomView.Message.jump_to_original')} )} @@ -766,11 +767,11 @@ function MessageInternal( {isFailedSend && ( - Failed to send. + {t('RoomView.Message.failed_to_send')} {canResend && ( - Retry + {t('General.retry')} )} {canDeleteFailedSend && ( @@ -780,7 +781,7 @@ function MessageInternal( radii="Pill" onClick={handleDeleteFailedSendClick} > - Delete + {t('General.delete')} )} @@ -789,7 +790,7 @@ function MessageInternal( {menuIcon(Info)} - Only you can see this. + {t('RoomView.Message.only_you_can_see_this')} - Dismiss + {t('General.dismiss')} )} diff --git a/src/app/features/room/message/MessageEditor.tsx b/src/app/features/room/message/MessageEditor.tsx index 3f063906e5..1ee24aa087 100644 --- a/src/app/features/room/message/MessageEditor.tsx +++ b/src/app/features/room/message/MessageEditor.tsx @@ -69,6 +69,8 @@ import { readdAngleBracketsForHiddenPreviews, stripMarkdownEscapesForHiddenPreviews, } from './hiddenLinkPreviews'; +import { t } from 'i18next'; +import * as prefixes from '$unstable/prefixes'; type MessageEditorProps = { roomId: string; @@ -231,8 +233,9 @@ export const MessageEditor = as<'div', MessageEditorProps>( : undefined; const rawPmp = - editedEvent?.getContent()?.['m.new_content']?.['com.beeper.per_message_profile'] ?? - mEvent.getContent()?.['com.beeper.per_message_profile']; + editedEvent?.getContent()?.['m.new_content']?.[ + prefixes.MATRIX_UNSTABLE_PER_MESSAGE_PROFILE_PROPERTY_NAME + ] ?? mEvent.getContent()?.[prefixes.MATRIX_UNSTABLE_PER_MESSAGE_PROFILE_PROPERTY_NAME]; const pmpDisplayname = rawPmp !== null && @@ -255,7 +258,7 @@ export const MessageEditor = as<'div', MessageEditorProps>( customHtml = htmlPrefix + customHtml; } - newContent['com.beeper.per_message_profile'] = rawPmp; + newContent[prefixes.MATRIX_UNSTABLE_PER_MESSAGE_PROFILE_PROPERTY_NAME] = rawPmp; } const contentBody: IContent & Omit, 'm.relates_to'> = { @@ -304,11 +307,11 @@ export const MessageEditor = as<'div', MessageEditorProps>( if (oldContent.file !== undefined) content['m.new_content'].file = oldContent.file; if (oldContent.url !== undefined) content['m.new_content'].url = oldContent.url; - if (oldContent['page.codeberg.everypizza.msc4193.spoiler'] !== undefined) { - content['page.codeberg.everypizza.msc4193.spoiler'] = - oldContent['page.codeberg.everypizza.msc4193.spoiler']; - content['m.new_content']['page.codeberg.everypizza.msc4193.spoiler'] = + if (oldContent[prefixes.MATRIX_UNSTABLE_SPOILER_PROPERTY_NAME] !== undefined) { + content[prefixes.MATRIX_UNSTABLE_SPOILER_PROPERTY_NAME] = oldContent['page.codeberg.everypizza.msc4193.spoiler']; + content['m.new_content'][prefixes.MATRIX_UNSTABLE_SPOILER_PROPERTY_NAME] = + oldContent[prefixes.MATRIX_UNSTABLE_SPOILER_PROPERTY_NAME]; } } content['com.beeper.linkpreviews'] = []; @@ -526,7 +529,7 @@ export const MessageEditor = as<'div', MessageEditorProps>( > ( ) : undefined } > - Save + {t('General.save')} - Cancel + {t('General.cancel')} diff --git a/src/app/features/room/reaction-viewer/ReactionViewer.tsx b/src/app/features/room/reaction-viewer/ReactionViewer.tsx index f2c59aae35..be34200261 100644 --- a/src/app/features/room/reaction-viewer/ReactionViewer.tsx +++ b/src/app/features/room/reaction-viewer/ReactionViewer.tsx @@ -17,6 +17,7 @@ import { useOpenUserRoomProfile } from '$state/hooks/userRoomProfile'; import { useSpaceOptionally } from '$hooks/useSpace'; import { getMouseEventCords } from '$utils/dom'; import * as css from './ReactionViewer.css'; +import { t } from 'i18next'; export type ReactionViewerProps = { room: Room; @@ -87,7 +88,9 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
- {`Reacted with :${selectedShortcode}:`} + + {t('RoomView.Reactions.reacted_with')} {`:${selectedShortcode}:`} + {composerIcon(X)} diff --git a/src/app/features/settings/Persona/PKCompat.tsx b/src/app/features/settings/Persona/PKCompat.tsx index 8d55eaec1b..aee5d23abb 100644 --- a/src/app/features/settings/Persona/PKCompat.tsx +++ b/src/app/features/settings/Persona/PKCompat.tsx @@ -4,14 +4,16 @@ import { useSetting } from '$state/hooks/settings'; import { settingsAtom } from '$state/settings'; import { Box, Switch, Text } from 'folds'; import { SequenceCardStyle } from '../styles.css'; +import { useTranslation } from 'react-i18next'; export function PKCompatSettings() { const [usePKCompat, setUsePKCompat] = useSetting(settingsAtom, 'pkCompat'); const [usePmpProxying, setUsePmpProxying] = useSetting(settingsAtom, 'pmpProxying'); + const { t } = useTranslation(['settings/persona']); return ( - Limited Compatibility with PluralKit-like functions + {t('limited_compatibility_with_pluralkit_like_functions')} } /> } diff --git a/src/app/features/settings/Persona/PerMessageProfileEditor.tsx b/src/app/features/settings/Persona/PerMessageProfileEditor.tsx index ea3dece4d6..b5df38b394 100644 --- a/src/app/features/settings/Persona/PerMessageProfileEditor.tsx +++ b/src/app/features/settings/Persona/PerMessageProfileEditor.tsx @@ -18,6 +18,7 @@ import { import type { PronounSet } from '$utils/pronouns'; import { parsePronounsStringToPronounsSetArray } from '$utils/pronouns'; import { SequenceCardStyle } from '../styles.css'; +import { useTranslation } from 'react-i18next'; /** * the props we use for the per-message profile editor, which is used to edit a per-message profile. This is used in the settings page when the user wants to edit a profile. @@ -45,6 +46,7 @@ export function PerMessageProfileEditor({ const [currentDisplayName, setCurrentDisplayName] = useState(displayName ?? ''); const [currentId, setCurrentId] = useState(profileId); const [newId, setNewId] = useState(profileId); + const { t } = useTranslation(['settings/persona', 'settings/profile', 'general']); console.warn(pronouns); @@ -227,7 +229,7 @@ export function PerMessageProfileEditor({ style={{ width: '100%', marginBottom: config.space.S200 }} > - Profile ID: + {t('profile_id')} @@ -264,7 +266,7 @@ export function PerMessageProfileEditor({ overflow: 'visible', marginTop: 20, }} - aria-label="Avatar and upload" + aria-label={t('settings/profile:avatar_and_upload')} > ( - + p )} - alt={`Avatar for profile ${profileId}`} + alt={t('avatar_for', { profileId })} /> {uploadAtom && ( - Display Name: + {t('display_name')} {menuIcon(X)} @@ -390,7 +392,7 @@ export function PerMessageProfileEditor({ alignSelf: 'flex-start', }} > - Pronouns: + {t('pronouns')} {menuIcon(X)} @@ -441,7 +443,7 @@ export function PerMessageProfileEditor({ flexShrink: 0, height: '100%', }} - aria-label={`Save button area for ${profileId}`} + aria-label={t('save_profile_button_area', { profileId })} > diff --git a/src/app/features/settings/cosmetics/Cosmetics.tsx b/src/app/features/settings/cosmetics/Cosmetics.tsx index 0667c6c287..8dda0d7372 100644 --- a/src/app/features/settings/cosmetics/Cosmetics.tsx +++ b/src/app/features/settings/cosmetics/Cosmetics.tsx @@ -28,6 +28,7 @@ import { SequenceCardStyle } from '$features/settings/styles.css'; import { SettingsSectionPage } from '../SettingsSectionPage'; import { Appearance } from './Themes'; import { LanguageSpecificPronouns } from './LanguageSpecificPronouns'; +import { useTranslation } from 'react-i18next'; function PronounPillMaxCountInput({ disabled }: { disabled: boolean }) { const [maxCount, setMaxCount] = useSetting(settingsAtom, 'pronounPillMaxCount'); @@ -206,16 +207,18 @@ function IconSizeSettings() { ); } -const emojiSizeItems = [ - { id: 'none', name: 'None (Same size as text)' }, - { id: 'extraSmall', name: 'Extra Small' }, - { id: 'small', name: 'Small' }, - { id: 'normal', name: 'Normal' }, - { id: 'large', name: 'Large' }, - { id: 'extraLarge', name: 'Extra Large' }, -]; - function SelectJumboEmojiSize() { + const { t } = useTranslation('general'); + + const emojiSizeItems = [ + { id: 'none', name: t('Settings.Cosmetics.none_same_size_as_text') }, + { id: 'extraSmall', name: t('Settings.Cosmetics.extra_small') }, + { id: 'small', name: t('Settings.Cosmetics.small') }, + { id: 'normal', name: t('Settings.Cosmetics.normal') }, + { id: 'large', name: t('Settings.Cosmetics.large') }, + { id: 'extraLarge', name: t('Settings.Cosmetics.extra_large') }, + ]; + const [menuCords, setMenuCords] = useState(); const [jumboEmojiSize, setJumboEmojiSize] = useSetting(settingsAtom, 'jumboEmojiSize'); @@ -283,14 +286,15 @@ function SelectJumboEmojiSize() { ); } -const profileCardRenderItems: { id: RenderUserCardsMode; name: string }[] = [ - { id: 'both', name: 'Light & dark' }, - { id: 'light', name: 'Light only' }, - { id: 'dark', name: 'Dark only' }, - { id: 'none', name: 'Off' }, -]; - function SelectRenderCustomProfileCards() { + const { t } = useTranslation('general'); + const profileCardRenderItems: { id: RenderUserCardsMode; name: string }[] = [ + { id: 'both', name: t('Settings.Cosmetics.light_and_dark') }, + { id: 'light', name: t('Settings.Cosmetics.light_only') }, + { id: 'dark', name: t('Settings.Cosmetics.dark_only') }, + { id: 'none', name: t('Settings.Cosmetics.off') }, + ]; + const [menuCords, setMenuCords] = useState(); const [renderUserCardsMode, setRenderUserCardsMode] = useSetting(settingsAtom, 'renderUserCards'); @@ -304,7 +308,8 @@ function SelectRenderCustomProfileCards() { }; const currentLabel = - profileCardRenderItems.find((i) => i.id === renderUserCardsMode)?.name ?? 'Light & dark'; + profileCardRenderItems.find((i) => i.id === renderUserCardsMode)?.name ?? + t('Settings.Cosmetics.light_and_dark'); return ( <> @@ -360,14 +365,15 @@ function SelectRenderCustomProfileCards() { } function JumboEmoji() { + const { t } = useTranslation('general'); return ( - Jumbo Emoji + {t('Settings.Cosmetics.jumbo_emoji')} } /> @@ -376,6 +382,7 @@ function JumboEmoji() { } function Privacy() { + const { t } = useTranslation('general'); const [privacyBlur, setPrivacyBlur] = useSetting(settingsAtom, 'privacyBlur'); const [privacyBlurAvatars, setPrivacyBlurAvatars] = useSetting( settingsAtom, @@ -385,22 +392,22 @@ function Privacy() { return ( - Privacy & Security + {t('Settings.Cosmetics.privacy_and_security')} } /> } @@ -409,9 +416,9 @@ function Privacy() { } @@ -422,6 +429,7 @@ function Privacy() { } function IdentityCosmetics() { + const { t } = useTranslation('general'); const [legacyUsernameColor, setLegacyUsernameColor] = useSetting( settingsAtom, 'legacyUsernameColor' @@ -438,12 +446,14 @@ function IdentityCosmetics() { return ( - Identity + {t('Settings.Cosmetics.identity')} } /> @@ -468,9 +478,9 @@ function IdentityCosmetics() { style={{ opacity: showPronouns ? 1 : 0.5 }} > } /> @@ -481,33 +491,39 @@ function IdentityCosmetics() { style={{ opacity: showPronouns ? 1 : 0.5 }} > } /> } /> } /> } @@ -515,9 +531,11 @@ function IdentityCosmetics() { } @@ -525,17 +543,17 @@ function IdentityCosmetics() { } /> } /> diff --git a/src/app/features/settings/cosmetics/LanguageSpecificPronouns.tsx b/src/app/features/settings/cosmetics/LanguageSpecificPronouns.tsx index dbab076a8b..bfc6457f5a 100644 --- a/src/app/features/settings/cosmetics/LanguageSpecificPronouns.tsx +++ b/src/app/features/settings/cosmetics/LanguageSpecificPronouns.tsx @@ -4,6 +4,7 @@ import { SequenceCard } from '$components/sequence-card'; import { useEffect, useState } from 'react'; import { getSettings, setSettings } from '$state/settings'; import { SequenceCardStyle } from '../styles.css'; +import { t } from 'i18next'; export type LanguageSpecificPronounsConfig = { enabled?: boolean | string; @@ -69,7 +70,7 @@ export function LanguageSpecificPronouns() { return ( - Language Specific Pronouns + {t('Settings.Cosmetics.language_specific_pronouns')} {useLanguageSpecificPronouns && ( - Save Bandwidth for Sticker and Emoji Images + {t('save_bandwidth_for_sticker_and_emoji_images')} } diff --git a/src/app/features/settings/experimental/Experimental.tsx b/src/app/features/settings/experimental/Experimental.tsx index 085355f40f..2ea22c2a4c 100644 --- a/src/app/features/settings/experimental/Experimental.tsx +++ b/src/app/features/settings/experimental/Experimental.tsx @@ -12,21 +12,23 @@ import { SettingsSectionPage } from '../SettingsSectionPage'; import { BandwidthSavingEmojis } from './BandwithSavingEmojis'; import { MSC4268HistoryShare } from './MSC4268HistoryShare'; import { MSC4274MediaGalleries } from './MSC4274MediaGalleries'; +import { useTranslation } from 'react-i18next'; function PersonaToggle() { const [showPersonaSetting, setShowPersonaSetting] = useSetting( settingsAtom, 'showPersonaSetting' ); + const { t } = useTranslation(['settings/experimental']); return ( - Personas (Per-Message Profiles) + {t('personas_per_message_profiles')} } @@ -41,8 +43,13 @@ type ExperimentalProps = { requestClose: () => void; }; export function Experimental({ requestBack, requestClose }: Readonly) { + const { t } = useTranslation(['settings/experimental']); return ( - + @@ -51,10 +58,10 @@ export function Experimental({ requestBack, requestClose }: Readonly - The features listed below may be unstable or incomplete,{' '} - use at your own risk. + {t('the_features_listed_below_may_be_unstable_or_incomplete')}{' '} + {t('use_at_your_own_risk')}.
- Please report any new issues potentially caused by these features! + {t('please_report_any_new_issues_potentially_caused_by_these_features')} } /> diff --git a/src/app/features/settings/experimental/MSC4268HistoryShare.tsx b/src/app/features/settings/experimental/MSC4268HistoryShare.tsx index 27c868d19b..94e4df345b 100644 --- a/src/app/features/settings/experimental/MSC4268HistoryShare.tsx +++ b/src/app/features/settings/experimental/MSC4268HistoryShare.tsx @@ -4,16 +4,18 @@ import { useSetting } from '$state/hooks/settings'; import { settingsAtom } from '$state/settings'; import { Box, Switch, Text } from 'folds'; import { SequenceCardStyle } from '../styles.css'; +import { useTranslation } from 'react-i18next'; export function MSC4268HistoryShare() { const [enabledMSC4268Command, setEnabledMSC4268Command] = useSetting( settingsAtom, 'enableMSC4268CMD' ); + const { t } = useTranslation(['settings/experimental']); return ( - Enable Sharing of Encrypted History + {t('enable_sharing_of_encrypted_history')} } diff --git a/src/app/features/settings/experimental/MSC4274MediaGalleries.tsx b/src/app/features/settings/experimental/MSC4274MediaGalleries.tsx index 397f9306c4..0128405b31 100644 --- a/src/app/features/settings/experimental/MSC4274MediaGalleries.tsx +++ b/src/app/features/settings/experimental/MSC4274MediaGalleries.tsx @@ -4,16 +4,18 @@ import { useSetting } from '$state/hooks/settings'; import { settingsAtom } from '$state/settings'; import { Box, Switch, Text } from 'folds'; import { SequenceCardStyle } from '../styles.css'; +import { useTranslation } from 'react-i18next'; export function MSC4274MediaGalleries() { const [enabledMediaGalleries, setEnabledMediaGalleries] = useSetting( settingsAtom, 'enableMediaGalleries' ); + const { t } = useTranslation(['settings/experimental']); return ( - Enable Media Galleries Support + {t('enable_media_galleries_support')} } /> diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx index a3fef67ac7..510a4bfcf8 100644 --- a/src/app/features/settings/general/General.tsx +++ b/src/app/features/settings/general/General.tsx @@ -55,7 +55,11 @@ import { isKeyHotkey } from 'is-hotkey'; import { settingsSyncLastSyncedAtom, settingsSyncStatusAtom } from '$hooks/useSettingsSync'; import { exportSettingsAsJson, importSettingsFromJson } from '$utils/settingsSync'; import { SettingsSectionPage } from '../SettingsSectionPage'; +import { t } from 'i18next'; import { CallSoundSettings } from './CallSoundSettings'; +import { useTranslation } from 'react-i18next'; +import type { SettingMenuOption } from '$components/setting-menu-selector'; +import { SettingMenuSelector } from '$components/setting-menu-selector'; type DateHintProps = { hasChanges: boolean; @@ -84,26 +88,27 @@ function DateHint({ hasChanges, handleReset }: Readonly) { >
- Formatting + {t('Settings.General.formatting')}
- Year + {t('Settings.General.year')}
YY {': '} - Two-digit year + {t('Settings.General.two_digit_year')} {' '} YYYY - {': '}Four-digit year + {': '} + {t('Settings.General.four_digit_year')} @@ -111,31 +116,35 @@ function DateHint({ hasChanges, handleReset }: Readonly) {
- Month + {t('Settings.General.month')}
M - {': '}The month + {': '} + {t('Settings.General.the_month')} MM - {': '}Two-digit month + {': '} + {t('Settings.General.two_digit_month')} {' '} MMM - {': '}Short month name + {': '} + {t('Settings.General.short_month_name')} MMMM - {': '}Full month name + {': '} + {t('Settings.General.full_month_name')} @@ -143,13 +152,14 @@ function DateHint({ hasChanges, handleReset }: Readonly) {
- Day of the Month + {t('Settings.General.day_of_the_month')}
D - {': '}Day of the month + {': '} + {t('Settings.General.day_of_the_month')} @@ -162,31 +172,35 @@ function DateHint({ hasChanges, handleReset }: Readonly) {
- Day of the Week + {t('Settings.General.day_of_the_week')}
d - {': '}Day of the week (Sunday = 0) + {': '} + {t('Settings.General.day_of_the_week_sunday_0')} dd - {': '}Two-letter day name + {': '} + {t('Settings.General.two_letter_day_name')} ddd - {': '}Short day name + {': '} + {t('Settings.General.short_day_name')} dddd - {': '}Full day name + {': '} + {t('Settings.General.full_day_name')} @@ -420,6 +434,50 @@ function DateAndTime() { ); } +function LanguageChange() { + const { i18n } = useTranslation('general'); + + const languageOptions: SettingMenuOption[] = [ + { value: '', label: 'System' }, + { value: 'en', label: 'English' }, + { value: 'ro', label: 'Română' }, + ]; + const [curLanguage, setCurLanguage] = useState(localStorage.getItem('i18nextLng') ?? ''); + + const handleLanguageChange = (language: string) => { + if (language) { + setCurLanguage(language); + i18n.changeLanguage(language); + } else { + localStorage.removeItem('i18nextLng'); + setCurLanguage(''); + + const detected = i18n.services.languageDetector?.detect(); + i18n.changeLanguage(Array.isArray(detected) ? detected[0] : (detected ?? 'en')); + } + window.location.reload(); + }; + + return ( + + Language + + + } + /> + + + ); +} + function Editor({ isMobile }: Readonly<{ isMobile: boolean }>) { const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline'); const [editorToolbar, setEditorToolbar] = useSetting(settingsAtom, 'editorToolbar'); @@ -1664,6 +1722,7 @@ export function General({ requestBack, requestClose }: Readonly) { + diff --git a/src/app/features/settings/keyboard-shortcuts/KeyboardShortcuts.tsx b/src/app/features/settings/keyboard-shortcuts/KeyboardShortcuts.tsx index ff1ea36caa..ac0bba256a 100644 --- a/src/app/features/settings/keyboard-shortcuts/KeyboardShortcuts.tsx +++ b/src/app/features/settings/keyboard-shortcuts/KeyboardShortcuts.tsx @@ -7,6 +7,7 @@ import { Box, Scroll, Text, config } from 'folds'; import { PageContent } from '$components/page'; import { SettingsSectionPage } from '../SettingsSectionPage'; +import { useTranslation } from 'react-i18next'; type ShortcutEntry = { keys: string; @@ -18,41 +19,53 @@ type ShortcutCategory = { shortcuts: ShortcutEntry[]; }; -function formatKey(key: string): string { - const isMac = - typeof navigator !== 'undefined' && navigator.platform.toUpperCase().indexOf('MAC') >= 0; - return key - .replace(/\bmod\b/g, isMac ? '⌘' : 'Ctrl') - .replace(/\balt\b/gi, isMac ? '⌥' : 'Alt') - .replace(/\bshift\b/gi, '⇧'); -} - +// uses translation keys to avoid initializing it too early const SHORTCUT_CATEGORIES: ShortcutCategory[] = [ { - name: 'General', - shortcuts: [{ keys: 'Ctrl+F / ⌘+F', description: 'Search for messages' }], + name: 'general', + shortcuts: [{ keys: 'Ctrl+F / ⌘+F', description: 'search_for_messages' }], }, { - name: 'Navigation', + name: 'navigation', shortcuts: [ - { keys: 'Alt+N', description: 'Jump to the highest-priority unread room' }, - { keys: 'Alt+Shift+Down', description: 'Go to next unread room (cycle)' }, - { keys: 'Alt+Shift+Up', description: 'Go to previous unread room (cycle)' }, - { keys: 'Ctrl+K / ⌘+K', description: 'Search and go to Room' }, + { + keys: 'Alt+N', + description: 'jump_to_the_highest_priority_unread_room', + }, + { + keys: 'Alt+Shift+Down', + description: 'go_to_next_unread_room_cycle', + }, + { + keys: 'Alt+Shift+Up', + description: 'go_to_previous_unread_room_cycle', + }, + { keys: 'Ctrl+K / ⌘+K', description: 'seach_and_go_to_room' }, ], }, { - name: 'Messages', + name: 'messages', shortcuts: [ - { keys: 'Ctrl+Z / ⌘+Z', description: 'Undo in message editor' }, - { keys: 'Ctrl+Shift+Z / ⌘+Shift+Z', description: 'Redo in message editor' }, - { keys: 'Ctrl+B / ⌘+B', description: 'Bold' }, - { keys: 'Ctrl+I / ⌘+I', description: 'Italic' }, - { keys: 'Ctrl+U / ⌘+U', description: 'Underline' }, - { keys: 'Ctrl+E / ⌘+E', description: 'Open Sticker Picker' }, + { keys: 'Ctrl+Z / ⌘+Z', description: 'undo_in_message_editor' }, + { + keys: 'Ctrl+Shift+Z / ⌘+Shift+Z', + description: 'redo_in_message_editor', + }, + { keys: 'Ctrl+B / ⌘+B', description: 'bold' }, + { keys: 'Ctrl+I / ⌘+I', description: 'italic' }, + { keys: 'Ctrl+U / ⌘+U', description: 'underline' }, + { keys: 'Ctrl+E / ⌘+E', description: 'open_sticker_picker' }, ], }, ]; +function formatKey(key: string): string { + const isMac = + typeof navigator !== 'undefined' && navigator.platform.toUpperCase().indexOf('MAC') >= 0; + return key + .replace(/\bmod\b/g, isMac ? '⌘' : 'Ctrl') + .replace(/\balt\b/gi, isMac ? '⌥' : 'Alt') + .replace(/\bshift\b/gi, '⇧'); +} function ShortcutRow({ keys, description }: ShortcutEntry) { const parts = keys.split('/').map((k) => k.trim()); @@ -116,11 +129,13 @@ type KeyboardShortcutsProps = { requestClose: () => void; }; export function KeyboardShortcuts({ requestBack, requestClose }: KeyboardShortcutsProps) { + const { t } = useTranslation(['settings/keyboard_shortcuts']); + return ( @@ -131,14 +146,14 @@ export function KeyboardShortcuts({ requestBack, requestClose }: KeyboardShortcu {SHORTCUT_CATEGORIES.map((category) => ( - {category.name} + {t(category.name)}
{category.shortcuts.map((entry) => (
{entry.keys}
- +
))} diff --git a/src/app/features/settings/settingsLink.ts b/src/app/features/settings/settingsLink.ts index e39fc9b33b..3d6dfc228b 100644 --- a/src/app/features/settings/settingsLink.ts +++ b/src/app/features/settings/settingsLink.ts @@ -61,6 +61,7 @@ const settingsLinkFocusIdsBySection: Record({ - debug: false, + defaultNS: 'general', fallbackLng: 'en', + load: 'languageOnly', interpolation: { - escapeValue: false, // not needed for react as it escapes by default + escapeValue: false, + }, + detection: { + order: ['querystring', 'localStorage', 'navigator', 'htmlTag', 'path', 'subdomain'], + lookupQuerystring: 'lng', + caches: ['localStorage'], }, - load: 'languageOnly', backend: { - loadPath: `${trimTrailingSlash(import.meta.env.BASE_URL)}/public/locales/{{lng}}.json`, + loadPath: `${trimTrailingSlash(import.meta.env.BASE_URL)}/public/locales/{{lng}}/{{ns}}.json`, + }, + react: { + useSuspense: false, }, });