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/events.json b/public/locales/en/events.json new file mode 100644 index 0000000000..9ca7331d97 --- /dev/null +++ b/public/locales/en/events.json @@ -0,0 +1,12 @@ +{ + "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..." + } +} diff --git a/public/locales/en/general.json b/public/locales/en/general.json new file mode 100644 index 0000000000..2936ad8602 --- /dev/null +++ b/public/locales/en/general.json @@ -0,0 +1,41 @@ +{ + "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", + "result_one": " result", + "result_other": " results", + "jump": "Jump", + "import": "Import", + "Organisms": { + "RoomCommon": { + "changed_room_name": " changed room name" + } + } +} 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/room/drawers/members.json b/public/locales/en/room/drawers/members.json new file mode 100644 index 0000000000..5930eddb32 --- /dev/null +++ b/public/locales/en/room/drawers/members.json @@ -0,0 +1,5 @@ +{ + "scroll_to_top": "Scroll to Top", + "no_members": "No '{{filter}}' Members", + "type_name": "Type name..." +} diff --git a/public/locales/en/room/drawers/reactions.json b/public/locales/en/room/drawers/reactions.json new file mode 100644 index 0000000000..1e4fe329b1 --- /dev/null +++ b/public/locales/en/room/drawers/reactions.json @@ -0,0 +1,3 @@ +{ + "reacted_with": "Reacted with" +} diff --git a/public/locales/en/room/drawers/threads.json b/public/locales/en/room/drawers/threads.json new file mode 100644 index 0000000000..030e333a3a --- /dev/null +++ b/public/locales/en/room/drawers/threads.json @@ -0,0 +1,13 @@ +{ + "close_threads": "Close threads", + "search_threads": "Search threads...", + "clear_search": "Clear search", + "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!", + "reply_one": " reply", + "reply_other": " replies" +} diff --git a/public/locales/en/room/input.json b/public/locales/en/room/input.json new file mode 100644 index 0000000000..e65f0f05ff --- /dev/null +++ b/public/locales/en/room/input.json @@ -0,0 +1,9 @@ +{ + "recording_duration": "Recording duration:", + "EmojiBoard": { + "no_results_found": "No Results found", + "search_results": "Search Results", + "personal_pack": "Personal Pack", + "unknown_pack": "Unknown Pack" + } +} diff --git a/public/locales/en/room/room-view/replaced-room.json b/public/locales/en/room/room-view/replaced-room.json new file mode 100644 index 0000000000..a74d59460e --- /dev/null +++ b/public/locales/en/room/room-view/replaced-room.json @@ -0,0 +1,6 @@ +{ + "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" +} diff --git a/public/locales/en/room/room-view/room-buttons.json b/public/locales/en/room/room-view/room-buttons.json new file mode 100644 index 0000000000..cee6c2ef64 --- /dev/null +++ b/public/locales/en/room/room-view/room-buttons.json @@ -0,0 +1,3 @@ +{ + "start_voice_call": "Start Voice Call" +} diff --git a/public/locales/en/room/room-view/timeline-pills.json b/public/locales/en/room/room-view/timeline-pills.json new file mode 100644 index 0000000000..66ebb5c201 --- /dev/null +++ b/public/locales/en/room/room-view/timeline-pills.json @@ -0,0 +1,7 @@ +{ + "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", + "jump_to_latest": "Jump to Latest" +} diff --git a/public/locales/en/room/room-view/typing.json b/public/locales/en/room/room-view/typing.json new file mode 100644 index 0000000000..b0ef34c9a6 --- /dev/null +++ b/public/locales/en/room/room-view/typing.json @@ -0,0 +1,7 @@ +{ + "are_typing": " are typing...", + "typing_comma": ", ", + "typing_sep_word": " and ", + "is_typing": " is typing...", + "typing_others": " others" +} diff --git a/public/locales/en/settings/appearance.json b/public/locales/en/settings/appearance.json new file mode 100644 index 0000000000..d3da4e010b --- /dev/null +++ b/public/locales/en/settings/appearance.json @@ -0,0 +1,49 @@ +{ + "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')" +} diff --git a/public/locales/en/settings/dev_tools.json b/public/locales/en/settings/dev_tools.json new file mode 100644 index 0000000000..bbd680faf8 --- /dev/null +++ b/public/locales/en/settings/dev_tools.json @@ -0,0 +1,5 @@ +{ + "json_content": "JSON Content", + "account_data": "Account Data", + "developer_tools": "Developer Tools" +} 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/general.json b/public/locales/en/settings/general.json new file mode 100644 index 0000000000..59a8c6034f --- /dev/null +++ b/public/locales/en/settings/general.json @@ -0,0 +1,187 @@ +{ + "formatting": "Formatting", + "year": "Year", + "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", + "two_digit_day_of_the_month": "Two-digit day of the month", + + "date_format": "Date Format", + "twenty_four_hour_time_format": "24-Hour Time Format", + "current_language": "Current Language", + + "Editor": { + "editor": "Editor", + "enter_for_newline_title": "ENTER for Newline", + "enter_for_newline_description": "Use {{keycombo}} + ENTER to send message.", + "composer_formatting_toolbar_title": "Message Formatting Toolbar", + "composer_formatting_toolbar_description": "Enable the formatting toolbar in the message composer.", + "hide_add_menu_title": "Hide Add Menu in the Editor", + "hide_add_menu_description": "Make the Plus button in the editor only add files. You may still send the special items using commands such as /poll and /location", + "hide_typing_indicators_title": "Hide Typing Indicators", + "hide_typing_indicators_description": "Turn off typing status.", + "hide_read_receipts_title": "Hide Read Receipts", + "hide_read_receipts_description": "Turn off read receipts.", + "presence_status_title": "Presence Status", + "presence_status_description": "Show and receive online status from other users.", + "reply_notifications_title": "Send notifications for replies", + "reply_notifications_description": "Disable to use silent replies by default. You can still toggle reply notifications for each reply.", + "individual_attachments_title": "Send text with individual attachment as caption", + "individual_attachments_description": "Send the contents of the message field as the attachment caption if present." + }, + "Gestures": { + "gestures": "Gestures", + "mobile_only": " (Mobile only)", + "enable_swiping_title": "Enable Swiping", + "enable_swiping_description": "Swipe left for rooms, swipe right for actions.", + "right_swipe_action_title": "Right Swipe Action", + "right_swipe_action_description": "What happens when you swipe right on a message." + }, + "Calls": { + "calls": "Calls", + "large_room_call_button_title": "Show Call Button for Large Rooms", + "join_on_click_voicecalls_title": "Join voice calls by just clicking the room's icon", + "incoming_call_sound_title": "Incoming Call Sound", + "incoming_call_sound_description": "Play ringtone audio for incoming calls.", + "notify_voice_rooms_title": "Notify For Voice Rooms", + "notify_voice_rooms_description": "Play ringtone audio when someone starts or joins a voice room.", + "outgoing_ringback_sound_title": "Outgoing Ringback Sound", + "outgoing_ringback_sound_description": "Play ringback while waiting for someone to join.", + "call_ringtone_title": "Ringtone", + "call_ringtone_description": "Choose the incoming call ringtone.", + "call_ringback_tone_title": "Ringback Tone", + "call_ringback_tone_description": "Choose what plays while your outgoing call is waiting.", + "call_ringtone_volume_title": "Ringtone Volume", + "always_play_call_sound_title": "Always Play Call Sound", + "always_play_call_sound_description": "Play call sounds even when message notification sounds are turned off.", + "custom_call_ringtone_title": "Custom Ringtone", + "custom_call_ringtone_description": "Import an audio file for your ringtone.", + "custom_call_ringtone_empty": "No custom ringtone imported.", + "custom_call_ringtone_preview": "Preview Ringtone", + "custom_call_ringback_title": "Custom Ringback", + "custom_call_ringback_description": "Import an audio file for outgoing ringback.", + "custom_call_ringback_empty": "No custom ringback imported.", + "custom_call_ringback_preview": "Preview Ringback", + "custom_ringtone_not_available": "Custom ringtone is not available on this device. Falling back to default.", + "custom_ringback_not_available": "Custom ringback is not available on this device. Falling back to default.", + "custom_file_imported": "Custom File (Imported)", + "custom_file": "Custom File", + "unable_to_preview_this_ringtone": "Unable to preview this ringtone in your browser.", + "could_not_import_file_format": "Could not import this file. Try a different audio format.", + "import_custom_ringtone_file": "Import a custom ringtone file first.", + "import_custom_ringback_file": "Import a custom ringback file first.", + "max_file_size": "Max file size: {{maxSize}}. ", + "max_file_duration": "Max duration: {{maxDuration}}.", + "only_audio_files_supported": "Only audio files are supported.", + "file_too_large": "File is too large. Max {{maxSize}} allowed", + "file_too_long": "{{label}} must be between 1s and {{maxDuration}}" + }, + "Messages": { + "messages": "Messages", + "message_spacing_title": "Message Spacing", + "file_description_placement_title": "File description placement", + + "disable_to_hide_redacted_messages_entirely": "Disable to hide redacted messages entirely instead of showing a tombstone.", + "enable_to_show_tombstone_events_for_redacted": "Enable to show tombstone events for redacted messages instead of hiding them entirely.", + "emoji_selector_threshold_title": "Emoji Selector Character Threshold", + "right_aligned_bubbles_title": "Right Aligned Bubbles", + "right_aligned_bubbles_description": "While using bubble layout, have your bubbles right aligned.", + "disable_media_auto_load_title": "Disable Media Auto Load", + "hide_membership_change_title": "Hide Membership Change", + "hide_profile_change_title": "Hide Profile Change", + "hide_member_events_read_only_rooms_title": "Hide Member Events in Read-Only Rooms", + "hide_member_events_read_only_rooms_description": "Hide membership changes, reactions, and reaction redactions in read-only rooms such as announcement channels.", + "show_hidden_events_title": "Show Hidden Events", + "show_hidden_events_description": "Reveal additional timeline events that are normally filtered out.", + "show_hidden_events_disable": "Disable to hide hidden events", + "show_hidden_events_enable": "Enable to show hidden events, this will cause visual clutter in busy rooms.", + "hidden_event_edits_title": "Show Edit Events", + "hidden_event_edits_description": "Show message edits as separate timeline events with a link to the previous version.", + "show_redacted_message_tombstones_title": "Show Tombstones for Redacted Messages", + "show_redacted_message_tombstones_description": "Show a tombstone in place of redacted messages instead of hiding them entirely.", + "hidden_event_redaction_timeline_title": "Show Redaction Events", + "hidden_event_redaction_timeline_description": "Show when a message was redacted as a timeline event with a link to the original message.", + "hidden_event_reactions_title": "Show Reaction Events", + "hidden_event_reactions_description": "Show reactions as separate timeline events with a link to the target message.", + "hidden_event_reaction_tombstones_title": "Show Tombstones for Redacted Reactions", + "hidden_event_reaction_tombstones_description": "Show a tombstone in place of redacted reactions instead of hiding them entirely.", + "hidden_event_reaction_redaction_timeline_title": "Show Reaction Redaction Events", + "hidden_event_reaction_redaction_timeline_description": "Show when a reaction was removed as a timeline event with a link to the target message.", + "hidden_event_other_title": "Show Other Hidden Events", + "hidden_event_other_description": "Show generic state events and other unrecognized timeline events." + }, + "Embeds": { + "embeds": "Embeds", + "display_multiple_embeds_title": "Display Multiple Embeds", + "display_multiple_embeds_description": "Display the embeds of all the links. Turning it off makes it only show the embed of the 1st item", + "display_bundled_embeds_title": "Display Bundled Embeds", + "display_bundled_embeds_description": "Show embeds when provided by the message itself. The embeds may be fabricated or incorrect.", + "url_preview_title": "Server-side Embeds", + "url_preview_description": "Send the links from inside the messages to your homeserver to generate previews of the linked pages.", + "encrypted_room_url_preview_title": "Server-side Embeds in Encrypted Room", + "encrypted_room_url_preview_description": "Request server-side embeds in E2EE chats. This partially decreases secrecy by revealing sent links to your homeserver", + "client_side_embeds_title": "Client-side Embeds", + "client_side_embeds_description": "Attempt to preview supported urls (e.g. YouTube) on the client, without involving the homeserver. This will expose your IP Address to third party services.", + "client_side_embeds_disable": "Disable client-side embeds", + "client_side_embeds_enable": "Enable client-side embeds", + "encrypted_room_embeds_title": "Client-side Embeds in Encrypted Rooms", + "encrypted_room_embeds_disable": "Disable client-side embeds in encrypted rooms", + "encrypted_room_embeds_enable": "Enable client-side embeds in encrypted rooms", + "embed_youtube_links_title": "Embed YouTube Links", + "embed_youtube_links_disable": "Disable client-side Youtube video embeds", + "embed_youtube_links_enable": "Enable client-side Youtube video embeds", + "enable_gif_picker_title": "Enable Gif Picker", + "enable_gif_picker_description": "Enables the gif picker in the emoji board. This reduces Privacy because it makes requests to klipy.com whenever you search for a gif.", + "enable_gif_picker_disable": "Disable GIF picker", + "enable_gif_picker_enable": "Enable GIF picker", + "show_interactive_map_title": "Show Interactive maps", + "show_interactive_map_description": "Show an interactive map in messages. This reduces Privacy because it requests map data from OpenStreetMap.org whenever you need to load a uncached part of the maps.", + "show_interactive_map_disable": "Disable Interactive Map", + "show_interactive_map_enable": "Enable Interactive Map", + "show_interactive_map_enc_title": "Show Interactive maps in Encrypted Rooms", + "show_interactive_map_enc_description": "Show an interactive map in Encrypted rooms. This reduces Privacy because it requests map data from OpenStreetMap.org whenever you need to load a uncached part of the maps.", + "show_interactive_map_enc_disable": "Disable Interactive Map", + "show_interactive_map_enc_enable": "Enable Interactive Map" + }, + "Sync": { + "sync": "Sync", + "use_sliding_sync_title": "Use Sliding Sync", + "use_sliding_sync_enable": "Enable Sliding Sync for this current login/session. Requires server support and admin configuration.", + "use_sliding_sync_disable": "Unavailable: the server has disabled Sliding Sync in its config.", + "use_sliding_sync_documentation": " More info/Documentation.", + "use_sliding_sync_issues": " Known issues (Sable GitHub)." + }, + "SettingsSync": { + "settings_sync": "Settings Sync & Backup", + "sync_across_devices_title": "Sync across devices", + "sync_across_devices_description": "Store your settings in your Matrix account so they follow you to any Sable instance. Notification and zoom preferences are kept per-device.", + "sync_status_title": "Sync status", + "sync_across_devices_export": "Export Settings", + "sync_across_devices_import": "Import Settings", + "sync_across_devices_error": "Sync failed, will retry on next change", + "sync_across_devices_syncing": "Syncing…", + "sync_across_devices_uninitialized": "Not yet synced this session", + "sync_across_devices_existing": "Last synced at {{time}}", + "sync_across_devices_invalid": "Could not import, file was invalid or you cancelled." + }, + "DiagnosticsAndPriivacy": { + "diagnostics_and_priivacy": "Diagnostics & Privacy", + "error_reporting_title": "Error Reporting", + "error_reporting_send": "Send anonymous crash reports to help improve Sable. No messages, room names, or personal data are included.", + "error_reporting_unimplemented": "Error reporting is not configured for this build.", + "session_replay_title": "Session Replay", + "session_replay_description": "Allow recording of UI interactions to help debug errors. All text, media, and inputs are fully masked before sending.", + "privacy_policy": "Privacy Policy", + "please_refresh": "Please refresh the page for these settings to take effect." + } +} 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/events.json b/public/locales/ro/events.json new file mode 100644 index 0000000000..b2d906d11e --- /dev/null +++ b/public/locales/ro/events.json @@ -0,0 +1,12 @@ +{ + "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..." + } +} diff --git a/public/locales/ro/general.json b/public/locales/ro/general.json new file mode 100644 index 0000000000..391394f869 --- /dev/null +++ b/public/locales/ro/general.json @@ -0,0 +1,41 @@ +{ + "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ă", + "result_one": " rezultat", + "result_other": " rezultate", + "jump": "Sari", + "import": "Importă", + "Organisms": { + "RoomCommon": { + "changed_room_name": " a schimbat numele camerei" + } + } +} diff --git a/public/locales/ro/room/create.json b/public/locales/ro/room/create.json new file mode 100644 index 0000000000..2c0095dbe4 --- /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": "Poți tribuii permissiuni speciale pe durata creației. Aceste persoane vor avea un control ridicat. Lista de persoane speciale poate fi schimbată doar într-un 'upgrade'", + "no_suggestions": "Nici o sugestie", + "please_provide_the_user_id_and_hit_enter": "Oferiți ID-ul persoanel și apăsați Enter.", + "only_member_of_parent_space_can_join": "Doar membrii a spatiului 'părinte' se pot alătura.", + "private": "Privat", + "only_people_with_invite_can_join": "Doar persoanele invitate se pot alătura.", + "anyone_with_the_address_can_join": "Oricine se poate alătura cu adresa.", + "public": "Public", + "address_optional": "Addresă (Optională)", + "pick_an_unique_address_to_make_it_discoverable": "Alege o adresă unicat să fie ușor de descoperit de alte persoane.", + "this_address_is_already_taken_please_select_a_different_one": "Această adresă este deja luată. Alege altă adresă.", + "voice_room": "Cameră vocală", + "live_audio_and_video_conversations": "- Conversatii audio-video live.", + "chat_room": "Cameră discuții", + "messages_photos_and_videos": "- Mesaje, fotografii și 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..afd0ed9d24 --- /dev/null +++ b/public/locales/ro/room/direct/invite.json @@ -0,0 +1,7 @@ +{ + "direct_message_invite_prompt": "Aceasta este o cameră directă, mentită pentru o conversație între două persoane. Dorești să o transformi într-o conversație de grup înainte să continui?", + "failed_to_convert_direct_message_to_room": "Transformarea a eșuat!", + "convert_to_group_chat_and_invite": "Transformă in conversație de grup și invită", + "invite_to_direct_message_anyway": "Invită în cameră directă", + "invite_another_member": "Invită altă persoană" +} diff --git a/public/locales/ro/room/drawers/members.json b/public/locales/ro/room/drawers/members.json new file mode 100644 index 0000000000..a31bef4076 --- /dev/null +++ b/public/locales/ro/room/drawers/members.json @@ -0,0 +1,5 @@ +{ + "scroll_to_top": "Rulează spre început", + "no_members": "Nici un membru '{{filter}}'", + "type_name": "Scrie nume..." +} diff --git a/public/locales/ro/room/drawers/reactions.json b/public/locales/ro/room/drawers/reactions.json new file mode 100644 index 0000000000..8bd704d83e --- /dev/null +++ b/public/locales/ro/room/drawers/reactions.json @@ -0,0 +1,3 @@ +{ + "reacted_with": "A reacționat cu" +} diff --git a/public/locales/ro/room/drawers/threads.json b/public/locales/ro/room/drawers/threads.json new file mode 100644 index 0000000000..b2d6a1aca3 --- /dev/null +++ b/public/locales/ro/room/drawers/threads.json @@ -0,0 +1,14 @@ +{ + "close_threads": "Închide fire", + "search_threads": "Caută fir...", + "clear_search": "Curăță căutarea", + "no_threads_match_your_search": "Nu se potrivește nici un fir.", + "no_threads_yet": "Încă nu există nici un fir.", + "jump": "Sari", + "threads": "Fire", + "thread": "Fir", + "no_replies_yet_start_the_thread_below": "Nici un răspuns încă. Începe un fir!", + "reply_one": " răspuns", + "reply_few": " răspunsuri", + "reply_other": " de răspunsuri" +} diff --git a/public/locales/ro/room/input.json b/public/locales/ro/room/input.json new file mode 100644 index 0000000000..254b580345 --- /dev/null +++ b/public/locales/ro/room/input.json @@ -0,0 +1,9 @@ +{ + "recording_duration": "Durata înregistrării:", + "EmojiBoard": { + "no_results_found": "Nici un rezultat", + "search_results": "Caută rezultate", + "personal_pack": "Pachet personal", + "unknown_pack": "Pachet necunoscut" + } +} diff --git a/public/locales/ro/room/room-view/replaced-room.json b/public/locales/ro/room/room-view/replaced-room.json new file mode 100644 index 0000000000..8b49732092 --- /dev/null +++ b/public/locales/ro/room/room-view/replaced-room.json @@ -0,0 +1,6 @@ +{ + "join_new_room": "Alăturate camerei noi", + "this_room_has_been_replaced_and_is_no_longer_active": "Această cameră a fost schimbata și nu mai este activă.", + "failed_to_join_replacement_room": "Alăturarea la noua cameră a eșuat!", + "open_new_room": "Deschide camera nouă" +} diff --git a/public/locales/ro/room/room-view/room-buttons.json b/public/locales/ro/room/room-view/room-buttons.json new file mode 100644 index 0000000000..4ba0d8ff1b --- /dev/null +++ b/public/locales/ro/room/room-view/room-buttons.json @@ -0,0 +1,3 @@ +{ + "start_voice_call": "Începe apel vocal" +} diff --git a/public/locales/ro/room/room-view/timeline-pills.json b/public/locales/ro/room/room-view/timeline-pills.json new file mode 100644 index 0000000000..5add633e53 --- /dev/null +++ b/public/locales/ro/room/room-view/timeline-pills.json @@ -0,0 +1,7 @@ +{ + "failed_to_load_history": "Încărcarea istoriei a eșuat.", + "failed_to_load_messages": "Încărcarea mesajelor a eșuat.", + "jump_to_unread": "Rulează la Necitite", + "mark_as_read": "Notează ca citite", + "jump_to_latest": "Rulează la Noi" +} diff --git a/public/locales/ro/room/room-view/typing.json b/public/locales/ro/room/room-view/typing.json new file mode 100644 index 0000000000..b33b29fc00 --- /dev/null +++ b/public/locales/ro/room/room-view/typing.json @@ -0,0 +1,7 @@ +{ + "are_typing": " sciu...", + "typing_comma": ", ", + "typing_sep_word": " și ", + "is_typing": " scrie...", + "typing_others": " alții" +} diff --git a/public/locales/ro/settings/appearance.json b/public/locales/ro/settings/appearance.json new file mode 100644 index 0000000000..33917ed0c6 --- /dev/null +++ b/public/locales/ro/settings/appearance.json @@ -0,0 +1,49 @@ +{ + "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": "Blurează imaginile si videourile din mesaje.", + "blur_avatars": "Blurează avatar-uri", + "blurs_user_profile_pictures_and_room_icons": "Blurează imaginile de profil si iconițele camerelor.", + "blur_emotes": "Blurează emoticoane", + "blurs_emoticons_within_messages": "Blurează 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')" +} diff --git a/public/locales/ro/settings/dev_tools.json b/public/locales/ro/settings/dev_tools.json new file mode 100644 index 0000000000..9a9d6caae2 --- /dev/null +++ b/public/locales/ro/settings/dev_tools.json @@ -0,0 +1,5 @@ +{ + "json_content": "Conținut JSON", + "account_data": "Datele contului", + "developer_tools": "Unelete Dezvoltatori" +} 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/general.json b/public/locales/ro/settings/general.json new file mode 100644 index 0000000000..027477abe6 --- /dev/null +++ b/public/locales/ro/settings/general.json @@ -0,0 +1,186 @@ +{ + "formatting": "Formatare", + "year": "An", + "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", + "two_digit_day_of_the_month": "Ziua din lună cu 2 cifre month", + + "date_format": "Formatul zilei", + "twenty_four_hour_time_format": "Format in 24 de ore", + "current_language": "Limba folosită", + + "Editor": { + "editor": "Editor", + "enter_for_newline_title": "ENTER adaugă linie nouă", + "enter_for_newline_description": "Folosește {{keycombo}} + ENTER pentru a trimite un mesaj.", + "composer_formatting_toolbar_title": "Bară pentru modelarea mesajelor", + "composer_formatting_toolbar_description": "Activează bara de modelare in zona de scriere a mesajului.", + "hide_add_menu_title": "Ascunde Meniul de adăugare din editor", + "hide_add_menu_description": "Butonul de plus va putea doar să adauge file. Veți putea trimite mesaje speciale folosind comenzi precum /poll și /location.", + "hide_typing_indicators_title": "Ascunde indicatorul de scriere", + "hide_typing_indicators_description": "Ascunde textul care descrie cine scrie activ.", + "hide_read_receipts_title": "Ascunde lista de citite", + "hide_read_receipts_description": "Ascunde lista de persoane care urmăresc conversația activ.", + "presence_status_title": "Prezența activă", + "presence_status_description": "Timite si primeste status-ul de activiate de la alte persoane.", + "reply_notifications_title": "Trimite notificări in răspunsuri", + "reply_notifications_description": "Dezactivează pentru a trimite răspunsuri silențioase automat. Veți putea trimite notificări ne-silențioase prin apăsarea clopoțelului înainte de a trimite răspunsul.", + "individual_attachments_title": "Trimite text care conține un atașament ca pe o descriere.", + "individual_attachments_description": "Trimite continutul mesajului ca descriere dacă exista atașamente." + }, + "Gestures": { + "gestures": "Gesturi", + "mobile_only": " (Doar pe mobil)", + "enable_swiping_title": "Activează glisare", + "enable_swiping_description": "Glisează spre dreapta pentru camere, și spre stânga pentru acțiuni", + "right_swipe_action_title": "Acțiunea spre Stânga", + "right_swipe_action_description": "Ce se întămplă când glisați spre stânga." + }, + "Calls": { + "calls": "Apeluri", + "large_room_call_button_title": "Afișează butonul de apeluri în camerele largi", + "join_on_click_voicecalls_title": "Participă în apeluri de voce prin simpla apăsare a iconiței camerei", + "incoming_call_sound_title": "Tonul de apel primit", + "incoming_call_sound_description": "Ascultă tonul de apel de intrare.", + "notify_voice_rooms_title": "Notificări de apel", + "notify_voice_rooms_description": "Pornește sunetul de apel când cineva începe sau se alătura unui apel.", + "outgoing_ringback_sound_title": "Tonul de apel trimis", + "outgoing_ringback_sound_description": "Pornește tonul de apel de ieșire când așteptând ca cineva să se alăture.", + "call_ringtone_title": "Ton de intrare", + "call_ringtone_description": "Alege tonul de apel de intrare.", + "call_ringback_tone_title": "Ton de ieșire", + "call_ringback_tone_description": "Alege tonul de apel de ieșire.", + "call_ringtone_volume_title": "Volumul tonului", + "always_play_call_sound_title": "Pornește sunetul de apel orice ar fii", + "always_play_call_sound_description": "Pornește tonul de apel chiar dacă suneul de notificări sunt oprite.", + "custom_call_ringtone_title": "Ton de intrare personalizat", + "custom_call_ringtone_description": "Adaugă o fișă audio pentru tonul tău.", + "custom_call_ringtone_empty": "Nici un ton personal nu a fost adăugat.", + "custom_call_ringtone_preview": "Previzualizare Ton", + "custom_call_ringback_title": "Ton de ieșire personal", + "custom_call_ringback_description": "Adaugă o fișă audio pentru tonul de ieșire.", + "custom_call_ringback_empty": "Nici un ton de ieșire personal nu a fost adăugat.", + "custom_call_ringback_preview": "Previzualizare Ton de ieșire", + "custom_ringtone_not_available": "Adăugarea de tonuri de intrare pestonalizate nu este posibilă pe acest dispozitiv. Revenim la setarea implicită.", + "custom_ringback_not_available": "Adăugarea de tonuri de ieșire pestonalizate nu este posibilă pe acest dispozitiv. Revenim la setarea implicită.", + "custom_file_imported": "Fișă personală (adăugată)", + "custom_file": "Fișă personală", + "unable_to_preview_this_ringtone": "Previzualizarea acestui ton nu este posibilă pe acest dispozitiv.", + "could_not_import_file_format": "Adăugarea acestei fișe a eșuat. Încearca un alt format audio.", + "import_custom_ringtone_file": "Adaugă un ton de apel de intrare mai întăi.", + "import_custom_ringback_file": "Adaugă un ton de apel de ieșire mai întăi.", + "max_file_size": "Mărimea maximă: {{maxSize}}. ", + "max_file_duration": "Durata maximă: {{maxDuration}}.", + "only_audio_files_supported": "Sunt permise doar formate audio.", + "file_too_large": "Fișa este prea mare. Maximul permis este de {{maxSize}}", + "file_too_long": "{{label}} trebuie să se încadreze între 1s și {{maxDuration}}" + }, + "Messages": { + "messages": "Mesaje", + "message_spacing_title": "Spațierea mesajelor", + "file_description_placement_title": "Poziția descrierii fișelor", + "disable_to_hide_redacted_messages_entirely": "Dezactivează pentru a ascunde mesajele redactate cu totul în loc de a arăta o notiță.", + "enable_to_show_tombstone_events_for_redacted": "Activează pentru a afișa o notiță mentionănd mesajul șters în loc ca acesta să fie ascuns complet.", + "emoji_selector_threshold_title": "Prag de caracter pentru slector de Emoji", + "right_aligned_bubbles_title": "Bule aliniate la dreapta", + "right_aligned_bubbles_description": "Când folosești afișajul de bule, mesajele tale vor fi aliniate la dreapta.", + "disable_media_auto_load_title": "Dezactivează încărcarea automată a fișelor media", + "hide_membership_change_title": "Ascunde schimbările de participare", + "hide_profile_change_title": "Ascunde schimbările in profil", + "hide_member_events_read_only_rooms_title": "Ascunde evenimentele de membru in camerele 'doar citire'", + "hide_member_events_read_only_rooms_description": "Ascunde schimbările de participare, reacțiile, si redacționarea reacțiilor in camere 'doar citire' precum camerele de anunțuri.", + "show_hidden_events_title": "Afișează evenimente ascunse", + "show_hidden_events_description": "Descoperă evenimente din conversație care sunt de obicei asunse.", + "show_hidden_events_disable": "Dezactivează pentru a nu afișa evenimentele ascunse", + "show_hidden_events_enable": "Activează pentru a afișa evenimentele ascunse, asta va crea un sense in dezordine in camerele foarte active.", + "hidden_event_edits_title": "Afișează evenimentele de editaj", + "hidden_event_edits_description": "Afișează evenimentele de editare ca evenimente separate cu 'link' spre versiunea trecută.", + "show_redacted_message_tombstones_title": "Afișează notițe pentru mesajele redactate", + "show_redacted_message_tombstones_description": "Afișează o notița in locul mesajului redactat în loc de a fi complet ascuns complet.", + "hidden_event_redaction_timeline_title": "Afișează evenimentele de redactare", + "hidden_event_redaction_timeline_description": "Afișează redactarea unui mesaj cu un 'link' la mesajul care a fost redactat.", + "hidden_event_reactions_title": "Afișează evenimentele de reacție", + "hidden_event_reactions_description": "Afișează reactiile ca evenimente separate cu un 'link' spre mesajul spre care a fost directionată reacția.", + "hidden_event_reaction_tombstones_title": "Afișează notițe pentru reacții redactate", + "hidden_event_reaction_tombstones_description": "Afișează o notiță pentru redactarea reacției în loc de a le ascunde complet.", + "hidden_event_reaction_redaction_timeline_title": "Afișează evenimente de redacție reacțiilor", + "hidden_event_reaction_redaction_timeline_description": "Afișează evenimentul de redactare a unei reacții cu un 'link' spre mesasj în loc de a ascunde asta complet.", + "hidden_event_other_title": "Afișează alte evenimete ascunse", + "hidden_event_other_description": "Afișează evenimente generice de statut si alte evenimente necunoscute." + }, + "Embeds": { + "embeds": "Încorporări", + "display_multiple_embeds_title": "Afișează încorporări multiple", + "display_multiple_embeds_description": "Afișează încorporări pentru fiecare link în parte. Dezactivarea acestei setări va face aplicația sa afișeze încorporare pentru primul link", + "display_bundled_embeds_title": "Afișează încorprari 'Bundled'", + "display_bundled_embeds_description": "Afișează încorporari furnizate de mesaj. Aceste încorporari ar putea fi fabricate sau incorecte.", + "url_preview_title": "Încorporări de la server", + "url_preview_description": "Trimite link-urile din mesaje spre serverul tău pentru a genera încorporări pentru link-uri.", + "encrypted_room_url_preview_title": "Încorporări de la server în camere criptate", + "encrypted_room_url_preview_description": "Cere încorporări de la server pentru conversatii criptate end-to-end. Asta reduce confiențialitatea parțial întrucăt necesită trimiterea link-ului care este parte din mesaj spre server-ul tău", + "client_side_embeds_title": "Încorporări de la aplicație", + "client_side_embeds_description": "Încearcă să creezi încrporari pentru link-uri permise (ex. YouTube) de aplicatie, fâră să trimită link-ul spre server. Asta îți va expune adresa de IP spre servere 'third-party'.", + "client_side_embeds_disable": "Dezactivează încorporari de la aplicație", + "client_side_embeds_enable": "Activează încorporari de la aplicație", + "encrypted_room_embeds_title": "Încorporări de la apricație în camere criptate", + "encrypted_room_embeds_disable": "Dezactivează încorporari de la aplicație in camerele criptate", + "encrypted_room_embeds_enable": "Activează încorporari de la aplicație in camerele criptate", + "embed_youtube_links_title": "Încorporări pentru link-uri Youtube", + "embed_youtube_links_disable": "Dezactiveaza încorporarile create de aplicație pentru videoclipuri youtube", + "embed_youtube_links_enable": "Activeaza încorporarile create de aplicație pentru videoclipuri youtube", + "enable_gif_picker_title": "Activează selectorul de GIF-uri", + "enable_gif_picker_description": "Activează selectorul de GIF-uri. Asta reduce confidențialitatea deoarece crează request-uri spre klipy.com când cauți un gif.", + "enable_gif_picker_disable": "Dezactivează selectorul de GIF-uri", + "enable_gif_picker_enable": "Activează selectorul de GIF-uri", + "show_interactive_map_title": "Afișează hărți interactive", + "show_interactive_map_description": "Afișează hărți interactive în mesaje. Asta reduce confidențialitatea deoarece crează request-uri spre OpenStreetMap.org pentru părți din hartă care nu sunt salvate local.", + "show_interactive_map_disable": "Dezactivează hărți interactive", + "show_interactive_map_enable": "Activează hărți interactive", + "show_interactive_map_enc_title": "Afișează hărți interactive în camerele criptate", + "show_interactive_map_enc_description": "Afișează hărți interactive în camerle criptate. Asta reduce confidențialitatea deoarece crează request-uri spre OpenStreetMap.org pentru părți din hartă care nu sunt salvate local.", + "show_interactive_map_enc_disable": "Dezactivează hărți interactive", + "show_interactive_map_enc_enable": "Activează hărți interactive" + }, + "Sync": { + "sync": "Sincronizare", + "use_sliding_sync_title": "Folosește sincronizare 'Sliding'", + "use_sliding_sync_enable": "Activează sincronizarea 'sliding' pentru sesiunea aceasta. Necesită suport de la server.", + "use_sliding_sync_disable": "Neaccesibil: Server-ul a dezactivat sincronizarea 'sliding' in configurare.", + "use_sliding_sync_documentation": " Mai multă informație/documentație.", + "use_sliding_sync_issues": " Probleme Cunsocute (GitHub Sable)." + }, + "SettingsSync": { + "settings_sync": "Sincronizare setări si backup", + "sync_across_devices_title": "Sincronizează între dispozitive", + "sync_across_devices_description": "Salvează setările in contul tău matrix pentru ca ele să fie folosite pentru fiecare dispozitiv. Notificările si zoom-ul sunt pastrate individualizate dispozitivului respectiv.", + "sync_status_title": "Situația sincronizării", + "sync_across_devices_export": "Exportă Setări", + "sync_across_devices_import": "Importă Setări", + "sync_across_devices_error": "Sincronizarea a eșuat, vom încerca din nou la următoarea schimbare", + "sync_across_devices_syncing": "Sincronizând…", + "sync_across_devices_uninitialized": "Aceasta sesiune nu a fost înca sincronizată", + "sync_across_devices_existing": "Ultima sincronizare a fost la {{time}}", + "sync_across_devices_invalid": "Importarea a eșuat, fișa a fost incorectă sau procesul a fost anulat." + }, + "DiagnosticsAndPriivacy": { + "diagnostics_and_priivacy": "Diagnostice si Confidențialitate", + "error_reporting_title": "Raporare Erori", + "error_reporting_send": "Trimite raporturi anonime dacă ți se blochează aplicația. Nici un mesaj, nume al camerei, sau informație personala este inclusă.", + "error_reporting_unimplemented": "Raportarea de erori nu este configurata pe acest build.", + "session_replay_title": "Reluare a sesiunii", + "session_replay_description": "Permite înregistrarea interacțiunilor UI pentru a ajuta în rezolvarea problemelor. Tot textul, media, si scrisul este mascat inainte de a fi trimis.", + "privacy_policy": "Politică de Confidențialitate", + "please_refresh": "Reîmprospatează aplicația pentru ca aceste schimbări să își facă efectul." + } +} 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..083c695a1c 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 { useTranslation } from 'react-i18next'; const EDITOR_INTENT_SPACE_COUNT = 2; @@ -48,6 +49,7 @@ function AccountDataEdit({ onSave, }: AccountDataEditProps) { const alive = useAlive(); + const { t } = useTranslation(['settings/dev_tools', 'general']); const textAreaRef = useRef(null); const [jsonError, setJSONError] = useState(); @@ -121,7 +123,7 @@ function AccountDataEdit({ aria-disabled={submitting} > - Account Data + {t('account_data')} } > - Save + {t('save', { ns: 'general' })} @@ -165,7 +167,7 @@ function AccountDataEdit({ - JSON Content + {t('json_content')} void; }; function AccountDataView({ type, defaultContent, onEdit }: AccountDataViewProps) { + const { t } = useTranslation(['settings/dev_tools', 'general']); return ( - Account Data + {t('account_data')} - JSON Content + {t('json_content')} ({ type: type ?? '', content: content ?? {}, @@ -290,7 +294,7 @@ export function AccountDataEditor({ onClick={requestClose} before={sizedIcon(ArrowLeft, '100')} > - Developer Tools + {t('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..cb208b44d6 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 { useTranslation } from 'react-i18next'; export function CreateRoomAliasInput({ disabled }: { disabled?: boolean }) { const mx = useMatrixClient(); @@ -17,6 +18,7 @@ export function CreateRoomAliasInput({ disabled }: { disabled?: boolean }) { const [aliasAvail, setAliasAvail] = useState>({ status: AsyncStatus.Idle, }); + const { t } = useTranslation('room/create'); useEffect(() => { if (aliasAvail.status === AsyncStatus.Success && aliasInputRef.current?.value === '') { @@ -74,9 +76,9 @@ export function CreateRoomAliasInput({ disabled }: { disabled?: boolean }) { return ( - Address (Optional) + {t('address_optional')} - Pick an unique address to make it discoverable. + {t('pick_an_unique_address_to_make_it_discoverable')} {sizedIcon(Warning, '50', { filled: true })} - This address is already taken. Please select a different one. + {t('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..fe2407088e 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 { useTranslation } from 'react-i18next'; type CreateRoomTypeSelectorProps = { value?: CreateRoomType; @@ -18,6 +19,7 @@ export function CreateRoomTypeSelector({ disabled, getIcon, }: CreateRoomTypeSelectorProps) { + const { t } = useTranslation('room/create'); return ( - Chat Room + {t('chat_room')} - - Messages, photos, and videos. + {t('messages_photos_and_videos')} @@ -62,10 +64,10 @@ export function CreateRoomTypeSelector({ > - Voice Room + {t('voice_room')} - - Live audio and video conversations. + {t('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..de466250c0 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -61,6 +61,7 @@ import type { GifData } from './types'; import { EmojiBoardTab, EmojiType } from './types'; import { useClientConfig } from '$hooks/useClientConfig'; import { useFavoriteGifs } from '$hooks/useFavoriteGifs'; +import { useTranslation } from 'react-i18next'; /* oxlint-disable typescript/no-explicit-any */ // TODO: type klipy api properly @@ -96,6 +97,7 @@ const useGroups = ( const recentEmojis = useRecentEmoji(mx, 21); const labels = useEmojiGroupLabels(); + const { t } = useTranslation(['room/input']); const emojiGroupItems = useMemo(() => { const g: EmojiGroupItem[] = []; @@ -109,7 +111,8 @@ 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('EmojiBoard.personal_pack') : mx.getRoom(pack.id)?.name; g.push({ id: pack.id, @@ -129,7 +132,7 @@ const useGroups = ( }); return g; - }, [mx, recentEmojis, labels, imagePacks, tab]); + }, [mx, recentEmojis, labels, imagePacks, tab, t]); const stickerGroupItems = useMemo(() => { const g: StickerGroupItem[] = []; @@ -137,7 +140,8 @@ 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('EmojiBoard.personal_pack') : mx.getRoom(pack.id)?.name; g.push({ id: pack.id, @@ -149,7 +153,7 @@ const useGroups = ( }); return g; - }, [mx, imagePacks, tab]); + }, [mx, imagePacks, tab, t]); const gifGroupItems = useMemo(() => { if (tab !== EmojiBoardTab.Gif) return []; @@ -248,6 +252,7 @@ function EmojiSidebar({ }: Readonly) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); + const { t } = useTranslation(['room/input']); const [activeGroupId, setActiveGroupId] = useAtom(activeGroupAtom); const usage = ImageUsage.Emoticon; @@ -275,7 +280,8 @@ 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('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 +294,7 @@ function EmojiSidebar({ key={pack.id} active={activeGroupId === pack.id} id={pack.id} - label={label ?? 'Unknown Pack'} + label={label ?? t('EmojiBoard.unknown_pack')} url={url ?? undefined} onClick={handleScrollToGroup} /> @@ -331,6 +337,7 @@ function StickerSidebar({ saveStickerEmojiBandwidth, onScrollToGroup, }: Readonly) { + const { t } = useTranslation(['room/input']); const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); @@ -360,7 +367,7 @@ function StickerSidebar({ key={pack.id} active={activeGroupId === pack.id} id={pack.id} - label={label ?? 'Unknown Pack'} + label={label ?? t('EmojiBoard.unknown_pack')} url={url ?? undefined} onClick={handleScrollToGroup} /> @@ -473,6 +480,7 @@ export function EmojiBoard({ const mx = useMatrixClient(); const [saveStickerEmojiBandwidth] = useSetting(settingsAtom, 'saveStickerEmojiBandwidth'); const [showGifPicker] = useSetting(settingsAtom, 'enableGifPicker'); + const { t } = useTranslation(['room/input']); const emojiTab = tab === EmojiBoardTab.Emoji; const gifTab = tab === EmojiBoardTab.Gif; @@ -822,7 +830,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..f896252625 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 { useTranslation } from 'react-i18next'; export type AudioRecordingCompletePayload = { audioBlob: Blob; @@ -54,6 +55,7 @@ export const AudioMessageRecorder = forwardRef< const [isCanceling, setIsCanceling] = useState(false); const [announcedTime, setAnnouncedTime] = useState(0); const [barCount, setBarCount] = useState(MAX_BAR_COUNT); + const { t } = useTranslation(['room/input']); const onRecordingCompleteRef = useRef(onRecordingComplete); onRecordingCompleteRef.current = onRecordingComplete; @@ -196,7 +198,7 @@ export const AudioMessageRecorder = forwardRef< {announcedTime > 0 && announcedTime === seconds && ( - Recording duration: {formatTime(announcedTime)} + {t('recording_duration')} {formatTime(announcedTime)} )}
diff --git a/src/app/features/room/MembersDrawer.tsx b/src/app/features/room/MembersDrawer.tsx index fec83a0588..027eadd7e0 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 { useTranslation } from 'react-i18next'; type MemberDrawerHeaderProps = { room: Room; @@ -73,6 +74,7 @@ type MemberDrawerHeaderProps = { }; function MemberDrawerHeader({ room, hideText }: MemberDrawerHeaderProps) { const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer'); + const { t } = useTranslation(['general']); return (
@@ -91,7 +93,7 @@ function MemberDrawerHeader({ room, hideText }: MemberDrawerHeaderProps) { offset={4} tooltip={ - Close + {t('close')} } > @@ -249,6 +251,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) { const creators = useRoomCreators(room); const getPowerTag = useGetMemberPowerTag(room, creators, powerLevels); const getPowerLevel = useGetMemberPowerLevel(powerLevels); + const { t } = useTranslation(['room/drawers/members', 'general']); const fetchingMembers = members.length < room.getJoinedMemberCount(); const openUserRoomProfile = useOpenUserRoomProfile(); @@ -435,7 +438,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) { ref={searchInputRef} onChange={handleSearchChange} style={{ paddingRight: config.space.S200 }} - placeholder="Type name..." + placeholder={t('type_name')} variant="Surface" size="400" radii="400" @@ -456,9 +459,13 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) { }} after={chipIcon(X)} > - {`${result.items.length || 'No'} ${ - result.items.length === 1 ? 'Result' : 'Results' - }`} + + {result.items.length} + {t('result', { + ns: 'general', + count: result.items.length, + })} + ) } @@ -473,7 +480,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) { radii="Pill" outlined size="300" - aria-label="Scroll to Top" + aria-label={t('scroll_to_top')} > {composerIcon(CaretUp)} @@ -481,7 +488,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) { {!fetchingMembers && !result && processMembers.length === 0 && ( - {`No "${membershipFilter.name}" Members`} + {t('no_members', { filter: membershipFilter.name })} )} diff --git a/src/app/features/room/RoomCallButton.test.tsx b/src/app/features/room/RoomCallButton.test.tsx index 7f4ce6ae6c..4c2334d35f 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 { useTranslation } from 'react-i18next'; const { startCallMock, useCallJoinedMock } = vi.hoisted(() => ({ startCallMock: vi.fn<(...args: unknown[]) => void>(), @@ -23,6 +24,7 @@ vi.mock('jotai', async (importOriginal: () => Promise) => { }); describe('RoomCallButton', () => { + const { t } = useTranslation(['room/room-view/room-buttons']); const room = { roomId: '!room:example.org' } as Room; beforeEach(() => { @@ -40,7 +42,7 @@ describe('RoomCallButton', () => { /> ); - fireEvent.click(screen.getByRole('button', { name: /start voice call/i })); + fireEvent.click(screen.getByRole('button', { name: t('start_voice_call') })); await waitFor(() => { expect(startCallMock).toHaveBeenCalledWith(room, { @@ -61,7 +63,7 @@ describe('RoomCallButton', () => { /> ); - fireEvent.click(screen.getByRole('button', { name: /start video call/i })); + fireEvent.click(screen.getByRole('button', { name: t('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..cb2752b81a 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 { useTranslation } from 'react-i18next'; interface RoomCallButtonProps { room: Room; @@ -21,6 +22,7 @@ export function RoomCallButton({ kind, allowVideoStart = true, }: RoomCallButtonProps) { + const { t } = useTranslation(['room/room-view/room-buttons']); const startCall = useCallStart(direct); const callEmbed = useAtomValue(callEmbedAtom); const joined = useCallJoined(callEmbed); @@ -43,7 +45,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 +68,9 @@ export function RoomCallButton({ {composerIcon(icon)} diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 6a48ff9898..d3f3479f82 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 { useTranslation } from 'react-i18next'; const TimelineFloat = as<'div', css.TimelineFloatVariants>( ({ position, className, ...props }, ref) => ( @@ -310,6 +311,7 @@ export function RoomTimeline({ ); const [incomingInlineImagesMaxHeight] = useSetting(settingsAtom, 'incomingInlineImagesMaxHeight'); const [hideMemberInReadOnly] = useSetting(settingsAtom, 'hideMembershipInReadOnly'); + const { t } = useTranslation(['room/room-view/timeline-pills', 'general']); const [showInteractiveMap] = useSetting(settingsAtom, 'showInteractiveMap'); const [showEncInteractiveMap] = useSetting(settingsAtom, 'showEncInteractiveMap'); @@ -886,7 +888,7 @@ export function RoomTimeline({ style={{ padding: config.space.S300 }} > - Failed to load history. + {t('failed_to_load_history')} timelineSync.handleTimelinePagination(true)} > - Retry + {t('retry', { ns: 'general' })} ); @@ -912,7 +914,7 @@ export function RoomTimeline({ style={{ padding: config.space.S300 }} > - Failed to load messages. + {t('failed_to_load_messages')} timelineSync.handleTimelinePagination(false)} > - Retry + {t('retry', { ns: 'general' })} ); @@ -1050,7 +1052,7 @@ export function RoomTimeline({ before={chipIcon(ChatTeardropDots)} onClick={() => timelineSync.loadEventTimeline(unreadInfo.readUptoEventId)} > - Jump to Unread + {t('jump_to_unread')} markAsRead(mx, room.roomId, hideReads)} > - Mark as Read + {t('mark_as_read')} )} @@ -1148,7 +1150,7 @@ export function RoomTimeline({ MozUserSelect: 'none', }} > - Jump to Latest + {t('jump_to_latest')} )} diff --git a/src/app/features/room/RoomTombstone.tsx b/src/app/features/room/RoomTombstone.tsx index 0979b8acfc..3534aabf1a 100644 --- a/src/app/features/room/RoomTombstone.tsx +++ b/src/app/features/room/RoomTombstone.tsx @@ -9,9 +9,11 @@ import { getViaServers } from '$plugins/via-servers'; import { RoomInputPlaceholder } from './RoomInputPlaceholder'; import * as css from './RoomTombstone.css'; import { KnownMembership } from '$types/matrix-sdk'; +import { useTranslation } from 'react-i18next'; type RoomTombstoneProps = { roomId: string; body?: string; replacementRoomId: string }; export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstoneProps) { + const { t } = useTranslation(['room/room-view/replaced-room']); const mx = useMatrixClient(); const { navigateRoom } = useRoomNavigate(); @@ -34,10 +36,10 @@ export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstone return ( - {body || 'This room has been replaced and is no longer active.'} + {body || t('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('failed_to_join_replacement_room')} )} @@ -45,7 +47,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..26f2b0a3d9 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 { useTranslation } from 'react-i18next'; export type RoomViewTypingProps = { room: Room; @@ -21,6 +22,7 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>( const mx = useMatrixClient(); const typingMembers = useRoomTypingMember(room.roomId); const nicknames = useAtomValue(nicknamesAtom); + const { t } = useTranslation(['room/room-view/typing']); const typingNames = typingMembers .filter((receipt) => receipt.userId !== mx.getUserId()) @@ -62,7 +64,7 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>( <> {typingNames[0]} - {' is typing...'} + {t('is_typing')} )} @@ -70,11 +72,11 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>( <> {typingNames[0]} - {' and '} + {t('typing_sep_word')} {typingNames[1]} - {' are typing...'} + {t('are_typing')} )} @@ -82,15 +84,15 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>( <> {typingNames[0]} - {', '} + {t('typing_comma')} {typingNames[1]} - {' and '} + {t('typing_sep_word')} {typingNames[2]} - {' are typing...'} + {t('are_typing')} )} @@ -98,19 +100,22 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>( <> {typingNames[0]} - {', '} + {t('typing_comma')} {typingNames[1]} - {', '} + {t('typing_comma')} {typingNames[2]} - {' and '} + {t('typing_sep_word')} - {typingNames.length - 3} others + + {typingNames.length - 3} + {t('typing_others')} + - {' are typing...'} + {t('are_typing')} )} diff --git a/src/app/features/room/ThreadBrowser.tsx b/src/app/features/room/ThreadBrowser.tsx index cb3cd904b7..9c75d27d1b 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 { useTranslation } from 'react-i18next'; type ThreadPreviewProps = { room: Room; @@ -82,6 +83,7 @@ function ThreadPreview({ room, thread, onClick, onJump }: ThreadPreviewProps) { const [urlPreview] = useSetting(settingsAtom, 'urlPreview'); const mentionClickHandler = useMentionClickHandler(room.roomId); const spoilerClickHandler = useSpoilerClickHandler(); + const { t } = useTranslation(['room/room-view/drawer.threads', 'general']); const linkifyOpts = useMemo( () => ({ @@ -222,7 +224,7 @@ function ThreadPreview({ room, thread, onClick, onJump }: ThreadPreviewProps) { )} - Jump + {t('jump', { ns: 'general' })} @@ -315,6 +317,7 @@ export function ThreadBrowser({ room, onOpenThread, onClose, overlay }: ThreadBr const loadingMoreRef = useRef(false); const canLoadMoreRef = useRef(false); canLoadMoreRef.current = canLoadMore; + const { t } = useTranslation(['room/drawers/threads']); // On mount, set up thread event listeners, create the server-side thread // timeline sets, then fetch page 1 via paginate. The two operations are @@ -448,8 +451,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 +493,7 @@ export function ThreadBrowser({ room, onOpenThread, onClose, overlay }: ThreadBr {composerIcon(Chats)} - Threads + {t('threads')} @@ -499,7 +502,7 @@ export function ThreadBrowser({ room, onOpenThread, onClose, overlay }: ThreadBr variant="SurfaceVariant" size="300" radii="300" - aria-label="Close threads" + aria-label={t('close_threads')} > {composerIcon(X)} @@ -516,7 +519,7 @@ export function ThreadBrowser({ room, onOpenThread, onClose, overlay }: ThreadBr ref={searchRef} value={query} onChange={handleSearchChange} - placeholder="Search threads..." + placeholder={t('search_threads')} variant="Surface" size="400" radii="400" @@ -531,7 +534,7 @@ export function ThreadBrowser({ room, onOpenThread, onClose, overlay }: ThreadBr setQuery(''); searchRef.current?.focus(); }} - aria-label="Clear search" + aria-label={t('clear_search')} > {chipIcon(X)} @@ -571,7 +574,7 @@ 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('no_threads_match_your_search') : t('no_threads_yet')} ); diff --git a/src/app/features/room/ThreadDrawer.tsx b/src/app/features/room/ThreadDrawer.tsx index fdf59c221c..8a8edd790e 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 { useTranslation } from 'react-i18next'; /** * Resolve the list of reply events to show in the thread drawer. @@ -160,6 +161,7 @@ export function ThreadDrawer({ room, threadRootId, onClose, overlay }: ThreadDra const [showInteractiveMap] = useSetting(settingsAtom, 'showInteractiveMap'); const [showEncInteractiveMap] = useSetting(settingsAtom, 'showEncInteractiveMap'); const showMaps = room.hasEncryptionStateEvent() ? showEncInteractiveMap : showInteractiveMap; + const { t } = useTranslation(['room/drawers/threads']); // Memoized parsing options const linkifyOpts = useMemo( @@ -505,11 +507,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 +838,7 @@ export function ThreadDrawer({ room, threadRootId, onClose, overlay }: ThreadDra {composerIcon(Chats)} - Thread + {t('thread')} @@ -929,7 +931,7 @@ export function ThreadDrawer({ room, threadRootId, onClose, overlay }: ThreadDra > {composerIcon(Chats, { style: { opacity: 0.6 } })} - No replies yet. Start the thread below! + {t('no_replies_yet_start_the_thread_below')} ); @@ -951,7 +953,10 @@ export function ThreadDrawer({ room, threadRootId, onClose, overlay }: ThreadDra }} > - {processedReplies.length} {processedReplies.length === 1 ? 'reply' : 'replies'} + {processedReplies.length} + {t('reply', { + count: processedReplies.length, + })} void; @@ -375,6 +376,7 @@ function MessageInternal( ) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); + const { t } = useTranslation(['events', 'general']); const [isEmoji, setIsEmoji] = useState(false); @@ -660,10 +662,10 @@ function MessageInternal( const originalRoomId = messageForwardedProps.originalRoomId; return { label: messageForwardedProps.originalEventPrivate - ? 'Forwarded private message' + ? t('forwarded_private_message') : isSameRoomForward(originalRoomId) - ? 'Forwarded from earlier in this room' - : 'Forwarded from another room', + ? t('forwarded_from_earlier_in_this_room') + : t('forwarded_from_another_room'), roomId: originalRoomId, eventId: messageForwardedProps.originalEventId, ts: messageForwardedProps.originalTimestamp ?? 0, @@ -675,8 +677,8 @@ function MessageInternal( const originalRoomId = msc2723ForwardedMessageProps.room_id; return { label: isSameRoomForward(originalRoomId) - ? 'Forwarded from earlier in this room' - : 'Forwarded from another room', + ? t('forwarded_from_earlier_in_this_room') + : t('forwarded_from_another_room'), roomId: originalRoomId, eventId: msc2723ForwardedMessageProps.event_id, ts: msc2723ForwardedMessageProps.origin_server_ts ?? 0, @@ -685,7 +687,7 @@ function MessageInternal( } return null; - }, [messageForwardedProps, msc2723ForwardedMessageProps, room.roomId]); + }, [messageForwardedProps, msc2723ForwardedMessageProps, room.roomId, t]); const handleResendClick: MouseEventHandler = useCallback( (evt) => { @@ -732,7 +734,7 @@ function MessageInternal( data-mention-event-id={forwardedNotice.eventId} onClick={mentionClickHandler} > - jump to original + {t('jump_to_original')} )} @@ -766,11 +768,11 @@ function MessageInternal( {isFailedSend && ( - Failed to send. + {t('failed_to_send')} {canResend && ( - Retry + {t('retry', { ns: 'general' })} )} {canDeleteFailedSend && ( @@ -780,7 +782,7 @@ function MessageInternal( radii="Pill" onClick={handleDeleteFailedSendClick} > - Delete + {t('delete', { ns: 'general' })} )} @@ -789,7 +791,7 @@ function MessageInternal( {menuIcon(Info)} - Only you can see this. + {t('only_you_can_see_this')} - Dismiss + {t('dismiss', { ns: 'general' })} )} 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..e7f491b7ed 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 { useTranslation } from 'react-i18next'; export type ReactionViewerProps = { room: Room; @@ -35,6 +36,7 @@ export const ReactionViewer = as<'div', ReactionViewerProps>( const space = useSpaceOptionally(); const openProfile = useOpenUserRoomProfile(); const nicknames = useAtomValue(nicknamesAtom); + const { t } = useTranslation(['room/drawers/reactions']); const [selectedKey, setSelectedKey] = useState(() => { if (initialKey) return initialKey; @@ -87,7 +89,9 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
- {`Reacted with :${selectedShortcode}:`} + + {t('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..a9341274c1 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('settings/appearance'); + + const emojiSizeItems = [ + { id: 'none', name: t('none_same_size_as_text') }, + { id: 'extraSmall', name: t('extra_small') }, + { id: 'small', name: t('small') }, + { id: 'normal', name: t('normal') }, + { id: 'large', name: t('large') }, + { id: 'extraLarge', name: t('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('settings/appearance'); + const profileCardRenderItems: { id: RenderUserCardsMode; name: string }[] = [ + { id: 'both', name: t('light_and_dark') }, + { id: 'light', name: t('light_only') }, + { id: 'dark', name: t('dark_only') }, + { id: 'none', name: t('off') }, + ]; + const [menuCords, setMenuCords] = useState(); const [renderUserCardsMode, setRenderUserCardsMode] = useSetting(settingsAtom, 'renderUserCards'); @@ -304,7 +308,7 @@ function SelectRenderCustomProfileCards() { }; const currentLabel = - profileCardRenderItems.find((i) => i.id === renderUserCardsMode)?.name ?? 'Light & dark'; + profileCardRenderItems.find((i) => i.id === renderUserCardsMode)?.name ?? t('light_and_dark'); return ( <> @@ -360,14 +364,15 @@ function SelectRenderCustomProfileCards() { } function JumboEmoji() { + const { t } = useTranslation('settings/appearance'); return ( - Jumbo Emoji + {t('jumbo_emoji')} } /> @@ -376,6 +381,7 @@ function JumboEmoji() { } function Privacy() { + const { t } = useTranslation('settings/appearance'); const [privacyBlur, setPrivacyBlur] = useSetting(settingsAtom, 'privacyBlur'); const [privacyBlurAvatars, setPrivacyBlurAvatars] = useSetting( settingsAtom, @@ -385,22 +391,22 @@ function Privacy() { return ( - Privacy & Security + {t('privacy_and_security')} } /> } @@ -409,9 +415,9 @@ function Privacy() { } @@ -422,6 +428,7 @@ function Privacy() { } function IdentityCosmetics() { + const { t } = useTranslation('settings/appearance'); const [legacyUsernameColor, setLegacyUsernameColor] = useSetting( settingsAtom, 'legacyUsernameColor' @@ -438,12 +445,14 @@ function IdentityCosmetics() { return ( - Identity + {t('identity')} } /> @@ -468,9 +477,9 @@ function IdentityCosmetics() { style={{ opacity: showPronouns ? 1 : 0.5 }} > } /> @@ -481,33 +490,37 @@ function IdentityCosmetics() { style={{ opacity: showPronouns ? 1 : 0.5 }} > } /> } /> } /> } @@ -515,9 +528,9 @@ function IdentityCosmetics() { } @@ -525,17 +538,17 @@ function IdentityCosmetics() { } /> } /> diff --git a/src/app/features/settings/cosmetics/LanguageSpecificPronouns.tsx b/src/app/features/settings/cosmetics/LanguageSpecificPronouns.tsx index dbab076a8b..63034cefb0 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 { useTranslation } from 'react-i18next'; export type LanguageSpecificPronounsConfig = { enabled?: boolean | string; @@ -35,6 +36,7 @@ function splitAndTrimLanguages(languages: string): string[] { export function LanguageSpecificPronouns() { const [useLanguageSpecificPronouns, setEnabled] = useState(false); const [languageList, setLanguageList] = useState(''); + const { t } = useTranslation(['settings/appearance']); // common handler for saving changes to the language specific pronouns settings const handleSave = (enabled: boolean, languages: string) => { @@ -69,7 +71,7 @@ export function LanguageSpecificPronouns() { return ( - Language Specific Pronouns + {t('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/CallSoundSettings.tsx b/src/app/features/settings/general/CallSoundSettings.tsx index ac43c64417..5b4d1ccbb7 100644 --- a/src/app/features/settings/general/CallSoundSettings.tsx +++ b/src/app/features/settings/general/CallSoundSettings.tsx @@ -29,6 +29,7 @@ import { type CustomToneMetadata, type PreviewTone, } from './CallSoundSettingsCards'; +import { useTranslation } from 'react-i18next'; const toCustomToneMetadata = (stored: StoredCallRingtone): CustomToneMetadata => ({ fileName: stored.fileName, @@ -37,6 +38,7 @@ const toCustomToneMetadata = (stored: StoredCallRingtone): CustomToneMetadata => }); export function CallSoundSettings() { + const { t } = useTranslation(['settings/general']); const [incomingCallSoundEnabled, setIncomingCallSoundEnabled] = useSetting( settingsAtom, 'incomingCallSoundEnabled' @@ -89,11 +91,11 @@ export function CallSoundSettings() { useEffect(() => { if (!loadingCustomState && !hasCustomRingtone && callRingtoneId === 'custom') { setCallRingtoneId('sable-default'); - setCustomError('Custom ringtone is not available on this device. Falling back to default.'); + setCustomError(t('Calls.custom_ringtone_not_available')); } if (!loadingCustomState && !hasCustomRingback && callRingbackTone === 'custom') { setCallRingbackTone('sable-default'); - setCustomError('Custom ringback is not available on this device. Falling back to default.'); + setCustomError(t('Calls.custom_ringback_not_available')); } }, [ callRingtoneId, @@ -103,6 +105,7 @@ export function CallSoundSettings() { loadingCustomState, setCallRingtoneId, setCallRingbackTone, + t, ]); const ringtoneOptions = useMemo( @@ -111,12 +114,12 @@ export function CallSoundSettings() { option.value === 'custom' ? { ...option, - label: customRingtoneMeta ? 'Custom File (Imported)' : 'Custom File', + label: customRingtoneMeta ? t('Calls.custom_file_imported') : t('Calls.custom_file'), disabled: loadingCustomState, } : option ), - [customRingtoneMeta, loadingCustomState] + [customRingtoneMeta, loadingCustomState, t] ); const ringbackOptions = useMemo( () => @@ -124,12 +127,12 @@ export function CallSoundSettings() { option.value === 'custom' ? { ...option, - label: customRingbackMeta ? 'Custom File (Imported)' : 'Custom File', + label: customRingbackMeta ? t('Calls.custom_file_imported') : t('Calls.custom_file'), disabled: loadingCustomState, } : option ), - [customRingbackMeta, loadingCustomState] + [customRingbackMeta, loadingCustomState, t] ); const playPreviewTone = useCallback( @@ -148,12 +151,12 @@ export function CallSoundSettings() { }, 2500); } catch (err) { if (err instanceof Error && err.name === 'AbortError') return; - setCustomError('Unable to preview this ringtone in your browser.'); + setCustomError(t('Calls.unable_to_preview_this_ringtone')); } finally { setPreviewing(false); } }, - [callRingtoneId, callRingbackTone, callRingtoneVolume] + [callRingtoneId, callRingbackTone, callRingtoneVolume, t] ); const importCustomTone = useCallback( @@ -179,20 +182,20 @@ export function CallSoundSettings() { durationMs, }); if (!validation.valid) { - setCustomError(customToneValidationError(validation.reason, label)); + setCustomError(customToneValidationError(validation.reason, label, t)); return; } const stored = await putTone(file, durationMs); onImported(stored); } catch { - setCustomError('Could not import this file. Try a different audio format.'); + setCustomError(t('Calls.could_not_import_file_format')); } }); input.click(); }, - [] + [t] ); const handleImportCustomRingtone = useCallback(() => { @@ -233,7 +236,7 @@ export function CallSoundSettings() { const handleRingtoneSelection = (next: CallRingtoneId) => { if (next === 'custom' && !hasCustomRingtone) { - setCustomError('Import a custom ringtone file first.'); + setCustomError(t('Calls.import_custom_ringtone_file')); return; } setCustomError(null); @@ -242,7 +245,7 @@ export function CallSoundSettings() { const handleRingbackSelection = (next: CallRingtoneId) => { if (next === 'custom' && !hasCustomRingback) { - setCustomError('Import a custom ringback file first.'); + setCustomError(t('Calls.import_custom_ringback_file')); return; } setCustomError(null); @@ -259,9 +262,9 @@ export function CallSoundSettings() { ); } @@ -71,6 +73,7 @@ export function CustomToneSettingsCard({ onPreview: (tone: PreviewTone) => void; onReset: () => void; }) { + const { t } = useTranslation(['settings/general', 'general']); return ( } onClick={onImport} > - Import + {t('import', { ns: 'general' })} {previewActions.map(({ label, tone, icon }) => ( - Max file size: {bytesToSize(CUSTOM_CALL_RINGTONE_MAX_BYTES)}. Max duration:{' '} - {millisecondsToMinutesAndSeconds(CUSTOM_CALL_RINGTONE_MAX_DURATION_MS)}. + {t('Calls.max_file_size', { maxSize: bytesToSize(CUSTOM_CALL_RINGTONE_MAX_BYTES) })} + {t('Calls.max_file_duration', { + maxDuration: millisecondsToMinutesAndSeconds(CUSTOM_CALL_RINGTONE_MAX_DURATION_MS), + })} @@ -136,14 +141,16 @@ export function CustomToneSettingsCard({ export const customToneValidationError = ( reason: 'type' | 'size' | 'duration', - label: 'Ringtone' | 'Ringback' + label: 'Ringtone' | 'Ringback', + t: TFunction ): string => { - if (reason === 'type') return 'Only audio files are supported.'; + if (reason === 'type') return t('Calls.only_audio_files_supported'); if (reason === 'size') { - return `File is too large. Max ${bytesToSize(CUSTOM_CALL_RINGTONE_MAX_BYTES)} allowed.`; + return t('Calls.file_too_large', { maxSize: bytesToSize(CUSTOM_CALL_RINGTONE_MAX_BYTES) }); } - return `${label} must be between 1s and ${millisecondsToMinutesAndSeconds( - CUSTOM_CALL_RINGTONE_MAX_DURATION_MS - )}.`; + return t('Calls.file_too_long', { + label: label, + maxDuration: millisecondsToMinutesAndSeconds(CUSTOM_CALL_RINGTONE_MAX_DURATION_MS), + }); }; diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx index a3fef67ac7..16dd78659f 100644 --- a/src/app/features/settings/general/General.tsx +++ b/src/app/features/settings/general/General.tsx @@ -56,12 +56,16 @@ import { settingsSyncLastSyncedAtom, settingsSyncStatusAtom } from '$hooks/useSe import { exportSettingsAsJson, importSettingsFromJson } from '$utils/settingsSync'; import { SettingsSectionPage } from '../SettingsSectionPage'; 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; handleReset: () => void; }; function DateHint({ hasChanges, handleReset }: Readonly) { + const { t } = useTranslation(['settings/general', 'general']); const [anchor, setAnchor] = useState(); const categoryPadding = { padding: config.space.S200, paddingTop: 0 }; @@ -84,26 +88,27 @@ function DateHint({ hasChanges, handleReset }: Readonly) { >
- Formatting + {t('formatting')}
- Year + {t('year')}
YY {': '} - Two-digit year + {t('two_digit_year')} {' '} YYYY - {': '}Four-digit year + {': '} + {t('four_digit_year')} @@ -111,31 +116,35 @@ function DateHint({ hasChanges, handleReset }: Readonly) {
- Month + {t('month')}
M - {': '}The month + {': '} + {t('the_month')} MM - {': '}Two-digit month + {': '} + {t('two_digit_month')} {' '} MMM - {': '}Short month name + {': '} + {t('short_month_name')} MMMM - {': '}Full month name + {': '} + {t('full_month_name')} @@ -143,50 +152,56 @@ function DateHint({ hasChanges, handleReset }: Readonly) {
- Day of the Month + {t('day_of_the_month')}
D - {': '}Day of the month + {': '} + {t('day_of_the_month')} DD - {': '}Two-digit day of the month + {': '} + {t('two_digit_day_of_the_month')}
- Day of the Week + {t('day_of_the_week')}
d - {': '}Day of the week (Sunday = 0) + {': '} + {t('day_of_the_week_sunday_0')} dd - {': '}Two-letter day name + {': '} + {t('two_letter_day_name')} ddd - {': '}Short day name + {': '} + {t('short_day_name')} dddd - {': '}Full day name + {': '} + {t('full_day_name')} @@ -229,6 +244,7 @@ type CustomDateFormatProps = { onChange: (format: string) => void; }; function CustomDateFormat({ value, onChange }: Readonly) { + const { t } = useTranslation(['settings/general', 'general']); const [dateFormatCustom, setDateFormatCustom] = useState(value); useEffect(() => { @@ -282,7 +298,7 @@ function CustomDateFormat({ value, onChange }: Readonly) disabled={!hasChanges} type="submit" > - Save + {t('save', { ns: 'general' })}
@@ -369,6 +385,7 @@ function SelectDateFormat() { const [dateFormatString, setDateFormatString] = useSetting(settingsAtom, 'dateFormatString'); const [selectedDateFormat, setSelectedDateFormat] = useState(dateFormatString); const customDateFormat = selectedDateFormat === ''; + const { t } = useTranslation(['settings/general']); const handlePresetChange = (format: string) => { setSelectedDateFormat(format); @@ -380,7 +397,7 @@ function SelectDateFormat() { return ( <> } @@ -394,20 +411,21 @@ function SelectDateFormat() { function getTombstoneSettingToggleTitle(showTombstone: boolean): string { if (showTombstone) { - return 'Disable to hide redacted messages entirely instead of showing a tombstone.'; + return 'disable_to_hide_redacted_messages_entirely'; } - return 'Enable to show tombstone events for redacted messages instead of hiding them entirely.'; + return 'enable_to_show_tombstone_events_for_redacted'; } function DateAndTime() { const [hour24Clock, setHour24Clock] = useSetting(settingsAtom, 'hour24Clock'); + const { t } = useTranslation(['settings/general']); return ( Date & Time } /> @@ -420,6 +438,51 @@ function DateAndTime() { ); } +function LanguageChange() { + const { i18n } = useTranslation('general'); + const { t } = useTranslation(['settings/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'); @@ -432,43 +495,38 @@ function Editor({ isMobile }: Readonly<{ isMobile: boolean }>) { settingsAtom, 'sendIndividualAttachmentAsCaption' ); + const { t } = useTranslation(['settings/general']); return ( - Editor - - - } - /> - + {t('Editor.editor')} + {!isMobile && ( + + + } + /> + + )} } /> } @@ -476,33 +534,33 @@ function Editor({ isMobile }: Readonly<{ isMobile: boolean }>) { } /> } /> } /> } @@ -510,9 +568,9 @@ function Editor({ isMobile }: Readonly<{ isMobile: boolean }>) { ) function Gestures({ isMobile }: Readonly<{ isMobile: boolean }>) { const [mobileGestures, setMobileGestures] = useSetting(settingsAtom, 'mobileGestures'); + const { t } = useTranslation(['settings/general']); return ( - Gestures {!isMobile && '(Mobile Only)'} + + {t('Gestures.gestures')} + {!isMobile && t('Gestures.mobile_only')} + ) { } /> @@ -877,6 +939,7 @@ function EmojiSelectorThresholdInput() { } function Calls() { + const { t } = useTranslation(['settings/general']); const [alwaysShowCallButton, setAlwaysShowCallButton] = useSetting( settingsAtom, 'alwaysShowCallButton' @@ -888,10 +951,10 @@ function Calls() { return ( - Calls + {t('Calls.calls')} - Messages + {t('Messages.messages')} } /> } /> } /> @@ -992,16 +1057,16 @@ function Messages() { {messageLayout === MessageLayout.Bubble && ( } /> )}