diff --git a/Cargo.lock b/Cargo.lock index 6cb96f12e6..aadb9f17b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6386,6 +6386,16 @@ dependencies = [ "windows-registry 0.5.3", ] +[[package]] +name = "i18n" +version = "0.0.0" +dependencies = [ + "log", + "once_cell", + "rust-embed 8.7.2", + "serde_json", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -9097,6 +9107,7 @@ dependencies = [ "ai", "anyhow", "cfg-if", + "i18n", "instant", "log", "pathfinder_color", @@ -14462,6 +14473,7 @@ dependencies = [ "http_client", "http_server", "hyper", + "i18n", "image", "indexmap 2.12.0", "infer", @@ -14716,6 +14728,7 @@ dependencies = [ "clap_complete", "color-print", "humantime", + "i18n", "jaq-all", "serde", "serial_test", diff --git a/Cargo.toml b/Cargo.toml index 5e833e9fba..6d9ff138f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ fuzzy_match = { path = "crates/fuzzy_match" } handlebars = { path = "crates/handlebars" } http_client = { path = "crates/http_client" } http_server = { path = "crates/http_server" } +i18n = { path = "crates/i18n" } input_classifier = { path = "crates/input_classifier" } integration = { path = "crates/integration" } ipc = { path = "crates/ipc" } diff --git a/app/Cargo.toml b/app/Cargo.toml index 3aac30962c..e2dd2c4611 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -129,6 +129,7 @@ jemalloc_pprof = { version = "0.8.1", optional = true, features = [ ] } lsp-types = "0.97.0" indexmap = { version = "2.0.2", features = ["serde"] } +i18n.workspace = true input_classifier.workspace = true instant.workspace = true ipc.workspace = true diff --git a/app/src/ai/agent/comment.rs b/app/src/ai/agent/comment.rs index 9147cc5f69..b2a305ee08 100644 --- a/app/src/ai/agent/comment.rs +++ b/app/src/ai/agent/comment.rs @@ -45,7 +45,7 @@ impl ReviewComment { .head_title .as_ref() .cloned() - .unwrap_or_else(|| "Review Comment".to_string()), + .unwrap_or_else(|| i18n::t("code_review.comments.review_comment")), } } } diff --git a/app/src/ai/agent/conversation.rs b/app/src/ai/agent/conversation.rs index a2a9b7cbbd..e85bba89fb 100644 --- a/app/src/ai/agent/conversation.rs +++ b/app/src/ai/agent/conversation.rs @@ -4174,6 +4174,16 @@ impl std::fmt::Display for ConversationStatus { } impl ConversationStatus { + pub fn localized_label(&self) -> String { + match self { + ConversationStatus::InProgress => i18n::t("ai.conversation_status.in_progress"), + ConversationStatus::Success => i18n::t("ai.conversation_status.done"), + ConversationStatus::Error => i18n::t("ai.conversation_status.error"), + ConversationStatus::Cancelled => i18n::t("ai.conversation_status.cancelled"), + ConversationStatus::Blocked { .. } => i18n::t("ai.conversation_status.blocked"), + } + } + pub fn render_icon(&self, appearance: &Appearance) -> warpui::elements::Icon { match self { ConversationStatus::InProgress => in_progress_icon(appearance), diff --git a/app/src/ai/agent/todos/popup.rs b/app/src/ai/agent/todos/popup.rs index d3a42cda41..a89f95c816 100644 --- a/app/src/ai/agent/todos/popup.rs +++ b/app/src/ai/agent/todos/popup.rs @@ -133,7 +133,7 @@ impl AgentTodosPopupView { let mut header_row = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Center); let mut header = Text::new( - "Tasks".to_string(), + i18n::t("ai.todos.tasks"), appearance.header_font_family(), styles.detail_font_size + 2., ) diff --git a/app/src/ai/agent_conversations_model.rs b/app/src/ai/agent_conversations_model.rs index 0d65f4d9a9..2cf5295fd5 100644 --- a/app/src/ai/agent_conversations_model.rs +++ b/app/src/ai/agent_conversations_model.rs @@ -384,7 +384,7 @@ impl AgentRunDisplayStatus { .status_message .as_ref() .map(|m| m.message.clone()) - .unwrap_or_else(|| "Task blocked".to_string()), + .unwrap_or_else(|| i18n::t("ai.agent_conversations.task_blocked")), }, AmbientAgentTaskState::Cancelled => Self::TaskCancelled, AmbientAgentTaskState::Unknown => Self::TaskUnknown, @@ -482,6 +482,34 @@ impl AgentRunDisplayStatus { } } } + + pub fn localized_label(&self) -> String { + match self { + AgentRunDisplayStatus::TaskQueued => i18n::t("ai.conversation_status.queued"), + AgentRunDisplayStatus::TaskPending => i18n::t("ai.conversation_status.pending"), + AgentRunDisplayStatus::TaskClaimed => i18n::t("ai.conversation_status.claimed"), + AgentRunDisplayStatus::TaskInProgress + | AgentRunDisplayStatus::ConversationInProgress => { + i18n::t("ai.conversation_status.in_progress") + } + AgentRunDisplayStatus::TaskSucceeded | AgentRunDisplayStatus::ConversationSucceeded => { + i18n::t("ai.conversation_status.done") + } + AgentRunDisplayStatus::TaskFailed | AgentRunDisplayStatus::TaskUnknown => { + i18n::t("ai.conversation_status.failed") + } + AgentRunDisplayStatus::TaskError | AgentRunDisplayStatus::ConversationError => { + i18n::t("ai.conversation_status.error") + } + AgentRunDisplayStatus::TaskBlocked { .. } + | AgentRunDisplayStatus::ConversationBlocked { .. } => { + i18n::t("ai.conversation_status.blocked") + } + AgentRunDisplayStatus::TaskCancelled | AgentRunDisplayStatus::ConversationCancelled => { + i18n::t("ai.conversation_status.cancelled") + } + } + } } impl std::fmt::Display for AgentRunDisplayStatus { diff --git a/app/src/ai/agent_management/agent_management_model.rs b/app/src/ai/agent_management/agent_management_model.rs index 6c23895dbf..ff9be70250 100644 --- a/app/src/ai/agent_management/agent_management_model.rs +++ b/app/src/ai/agent_management/agent_management_model.rs @@ -157,12 +157,16 @@ impl AgentNotificationsModel { ); } CLIAgentSessionStatus::Success => { - let title = session_context - .display_title() - .unwrap_or_else(|| format!("{} completed", agent.display_name())); + let title = session_context.display_title().unwrap_or_else(|| { + format!( + "{}{}", + agent.display_name(), + i18n::t("agent_management.notifications.agent_completed_suffix") + ) + }); let message = match agent { - CLIAgent::Codex => "Notification from Codex", - _ => "Task completed.", + CLIAgent::Codex => i18n::t("agent_management.notifications.from_codex"), + _ => i18n::t("agent_management.notifications.task_completed"), }; let metadata = TerminalViewMetadata::lookup(*terminal_view_id, ctx); self.add_notification( @@ -181,15 +185,19 @@ impl AgentNotificationsModel { ); } CLIAgentSessionStatus::Blocked { message } => { - let title = session_context - .display_title() - .unwrap_or_else(|| format!("{} needs attention", agent.display_name())); + let title = session_context.display_title().unwrap_or_else(|| { + format!( + "{}{}", + agent.display_name(), + i18n::t("agent_management.notifications.needs_attention_suffix") + ) + }); let metadata = TerminalViewMetadata::lookup(*terminal_view_id, ctx); self.add_notification( title, - message - .clone() - .unwrap_or_else(|| "Waiting for input.".to_owned()), + message.clone().unwrap_or_else(|| { + i18n::t("agent_management.notifications.waiting_for_input") + }), NotificationCategory::Request, NotificationSourceAgent::CLI { agent: *agent, @@ -319,7 +327,8 @@ impl AgentNotificationsModel { return; } - let title = latest_query.unwrap_or_else(|| "Agent task".to_owned()); + let title = + latest_query.unwrap_or_else(|| i18n::t("agent_management.notifications.agent_task")); let metadata = TerminalViewMetadata::lookup(terminal_view_id, ctx); let oz_agent = NotificationSourceAgent::Oz { is_ambient: metadata.is_ambient, @@ -334,7 +343,7 @@ impl AgentNotificationsModel { let artifacts = self.flush_pending_artifacts(conversation_id); self.add_notification( title, - "Task completed.".to_owned(), + i18n::t("agent_management.notifications.task_completed"), NotificationCategory::Complete, oz_agent, origin, @@ -348,7 +357,7 @@ impl AgentNotificationsModel { let artifacts = self.flush_pending_artifacts(conversation_id); self.add_notification( title, - "Task was cancelled.".to_owned(), + i18n::t("agent_management.notifications.task_cancelled"), NotificationCategory::Complete, oz_agent, origin, @@ -375,7 +384,7 @@ impl AgentNotificationsModel { let artifacts = self.flush_pending_artifacts(conversation_id); self.add_notification( title, - "Something went wrong.".to_owned(), + i18n::t("agent_management.notifications.error"), NotificationCategory::Error, oz_agent, origin, diff --git a/app/src/ai/agent_management/agent_type_selector.rs b/app/src/ai/agent_management/agent_type_selector.rs index 95b5a555d7..5dae5fa754 100644 --- a/app/src/ai/agent_management/agent_type_selector.rs +++ b/app/src/ai/agent_management/agent_type_selector.rs @@ -127,7 +127,7 @@ impl AgentTypeSelector { let theme = appearance.theme(); let title = Text::new( - "Choose your agent".to_string(), + i18n::t("agent_management.agent_type.choose_agent"), appearance.ui_font_family(), TITLE_FONT_SIZE, ) @@ -186,8 +186,8 @@ impl AgentTypeSelector { &self, index: usize, icon: Icon, - title: &'static str, - description: &'static str, + title: String, + description: String, is_suggested: bool, mouse_state: MouseStateHandle, action: AgentTypeSelectorAction, @@ -258,11 +258,14 @@ impl AgentTypeSelector { .with_child(title_text); if is_suggested { - let suggested_text = - Text::new("Suggested".to_string(), font_family, OPTION_DESC_FONT_SIZE) - .with_style(Properties::default().weight(Weight::Medium)) - .with_color(badge_text_color) - .finish(); + let suggested_text = Text::new( + i18n::t("agent_management.agent_type.suggested"), + font_family, + OPTION_DESC_FONT_SIZE, + ) + .with_style(Properties::default().weight(Weight::Medium)) + .with_color(badge_text_color) + .finish(); let suggested = Container::new(suggested_text) .with_horizontal_padding(8.) @@ -334,8 +337,8 @@ impl AgentTypeSelector { let cloud_agent_option = self.render_option( 0, Icon::OzCloud, - "Cloud agent", - "Runs autonomously in a cloud environment you choose. Best for parallel or long-running work.", + i18n::t("agent_management.agent_type.cloud_agent"), + i18n::t("agent_management.agent_type.cloud_agent.desc"), true, self.cloud_agent_mouse_state.clone(), AgentTypeSelectorAction::SelectCloudAgent, @@ -345,8 +348,8 @@ impl AgentTypeSelector { let local_agent_option = self.render_option( 1, Icon::Oz, - "Local agent", - "Runs on your machine and requires supervision. Best for quick, interactive tasks.", + i18n::t("agent_management.agent_type.local_agent"), + i18n::t("agent_management.agent_type.local_agent.desc"), false, self.local_agent_mouse_state.clone(), AgentTypeSelectorAction::SelectLocalAgent, diff --git a/app/src/ai/agent_management/cloud_setup_guide_view.rs b/app/src/ai/agent_management/cloud_setup_guide_view.rs index e0fcd0d9bc..85bfb9c30b 100644 --- a/app/src/ai/agent_management/cloud_setup_guide_view.rs +++ b/app/src/ai/agent_management/cloud_setup_guide_view.rs @@ -118,8 +118,11 @@ impl CloudSetupGuideView { ); let visit_oz_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Visit Oz", SecondaryTheme) - .on_click(|ctx| ctx.dispatch_typed_action(CloudSetupGuideAction::VisitOz)) + ActionButton::new( + i18n::t("agent_management.cloud_setup.visit_oz"), + SecondaryTheme, + ) + .on_click(|ctx| ctx.dispatch_typed_action(CloudSetupGuideAction::VisitOz)) }); Self { @@ -146,7 +149,7 @@ impl CloudSetupGuideView { let mut header_container = Flex::column().with_spacing(8.); let title = Text::new( - "Getting started with Oz cloud agents", + i18n::t("agent_management.cloud_setup.title"), appearance.ui_font_family(), title_font_size, ) @@ -156,7 +159,7 @@ impl CloudSetupGuideView { header_container.add_child(title); let subtitle = Text::new( - "Start Oz cloud agents directly in Warp from an integration (Linear, Slack), with an event (GitHub, built-in schedule), or programmatically with the Oz SDK or CLI.", + i18n::t("agent_management.cloud_setup.subtitle"), appearance.ui_font_family(), subtitle_font_size, ) @@ -168,7 +171,7 @@ impl CloudSetupGuideView { let docs_line = Flex::row() .with_child( Text::new_inline( - "Check out the ", + i18n::t("agent_management.cloud_setup.docs_prefix"), appearance.ui_font_family(), subtitle_font_size, ) @@ -179,7 +182,7 @@ impl CloudSetupGuideView { appearance .ui_builder() .link( - "Oz documentation".to_string(), + i18n::t("agent_management.cloud_setup.docs_link"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(CloudSetupGuideAction::OpenDocs { @@ -197,7 +200,7 @@ impl CloudSetupGuideView { ) .with_child( Text::new_inline( - " to learn more.", + i18n::t("agent_management.cloud_setup.docs_suffix"), appearance.ui_font_family(), subtitle_font_size, ) @@ -215,7 +218,7 @@ impl CloudSetupGuideView { let font_size = 16.; let text = Text::new_inline( - "Quick start: Visit oz.warp.dev for a UI-based setup experience.", + i18n::t("agent_management.cloud_setup.quick_start"), appearance.ui_font_family(), font_size, ) @@ -249,7 +252,7 @@ impl CloudSetupGuideView { let font_size = 16.; Text::new( - "Manual setup: Create a Slack or Linear integration with the Oz CLI", + i18n::t("agent_management.cloud_setup.manual_setup"), appearance.ui_font_family(), font_size, ) @@ -293,8 +296,8 @@ impl CloudSetupGuideView { /// Render a description that includes a link at the end /// (e.g. "Use warp's environment setup command to have an agent help you through it. LINK[Visit docs]") fn render_description_with_link( - prefix: &'static str, - link_text: &'static str, + prefix: String, + link_text: String, link_mouse_state: MouseStateHandle, telemetry_url: SetupGuideDocs, appearance: &Appearance, @@ -347,39 +350,66 @@ impl CloudSetupGuideView { let Some((workflow, setup_step)) = (match code { CREATE_ENV_SLASH_CMD => Some(( WorkflowType::Local( - Workflow::new("Create Environment", CREATE_ENV_SLASH_CMD).with_arguments(vec![ - Argument::new("github link or local filepath", ArgumentType::Text) - .with_description("GitHub link or local filepath to the repository"), - ]), + Workflow::new( + i18n::t("agent_management.cloud_setup.workflow.create_environment"), + CREATE_ENV_SLASH_CMD, + ) + .with_arguments(vec![Argument::new( + "github link or local filepath", + ArgumentType::Text, + ) + .with_description(i18n::t( + "agent_management.cloud_setup.workflow.arg.github_link_or_path", + ))]), ), SetupGuideStep::CreateEnvironment, )), CREATE_ENV_CLI_CMD => Some(( WorkflowType::Local( - Workflow::new("Create Environment (CLI)", CREATE_ENV_CLI_CMD).with_arguments( - vec![ - Argument::new("NAME", ArgumentType::Text) - .with_description("Name for the environment"), - Argument::new("DOCKER_IMAGE", ArgumentType::Text) - .with_description("Docker image to use for the environment"), - ], - ), + Workflow::new( + i18n::t("agent_management.cloud_setup.workflow.create_environment_cli"), + CREATE_ENV_CLI_CMD, + ) + .with_arguments(vec![ + Argument::new("NAME", ArgumentType::Text).with_description(i18n::t( + "agent_management.cloud_setup.workflow.arg.environment_name", + )), + Argument::new("DOCKER_IMAGE", ArgumentType::Text).with_description( + i18n::t("agent_management.cloud_setup.workflow.arg.docker_image"), + ), + ]), ), SetupGuideStep::CreateEnvironmentCli, )), CREATE_SLACK_INTEGRATION_CMD => Some(( WorkflowType::Local( - Workflow::new("Create Slack Integration", CREATE_SLACK_INTEGRATION_CMD) - .with_arguments(vec![Argument::new("environment_id", ArgumentType::Text) - .with_description("ID of the environment to integrate with")]), + Workflow::new( + i18n::t("agent_management.cloud_setup.workflow.create_slack_integration"), + CREATE_SLACK_INTEGRATION_CMD, + ) + .with_arguments(vec![Argument::new( + "environment_id", + ArgumentType::Text, + ) + .with_description(i18n::t( + "agent_management.cloud_setup.workflow.arg.environment_id", + ))]), ), SetupGuideStep::CreateSlackIntegration, )), CREATE_LINEAR_INTEGRATION_CMD => Some(( WorkflowType::Local( - Workflow::new("Create Linear Integration", CREATE_LINEAR_INTEGRATION_CMD) - .with_arguments(vec![Argument::new("environment_id", ArgumentType::Text) - .with_description("ID of the environment to integrate with")]), + Workflow::new( + i18n::t("agent_management.cloud_setup.workflow.create_linear_integration"), + CREATE_LINEAR_INTEGRATION_CMD, + ) + .with_arguments(vec![Argument::new( + "environment_id", + ArgumentType::Text, + ) + .with_description(i18n::t( + "agent_management.cloud_setup.workflow.arg.environment_id", + ))]), ), SetupGuideStep::CreateLinearIntegration, )), @@ -432,7 +462,7 @@ impl CloudSetupGuideView { .with_child(Self::render_step_number(1, appearance)) .with_child( Text::new( - "Create an environment", + i18n::t("agent_management.cloud_setup.step.create_environment"), appearance.ui_font_family(), step_title_font_size, ) @@ -444,7 +474,7 @@ impl CloudSetupGuideView { let description = Container::new( Text::new( - "First, set up an environment to create an integration.", + i18n::t("agent_management.cloud_setup.step.create_environment.desc"), appearance.ui_font_family(), step_desc_font_size, ) @@ -455,8 +485,8 @@ impl CloudSetupGuideView { .finish(); let sub_description = Container::new(Self::render_description_with_link( - "Use Warp's environment setup command to have an agent help you through it. ", - "Visit docs", + i18n::t("agent_management.cloud_setup.step.create_environment.docs_prefix"), + i18n::t("agent_management.cloud_setup.visit_docs"), self.env_docs_link_mouse_state.clone(), SetupGuideDocs::Environment, appearance, @@ -475,7 +505,7 @@ impl CloudSetupGuideView { let or_text = Container::new( Text::new( - "Or, supply your own existing docker image.", + i18n::t("agent_management.cloud_setup.step.create_environment.or_text"), appearance.ui_font_family(), step_desc_font_size, ) @@ -517,7 +547,7 @@ impl CloudSetupGuideView { .with_child(Self::render_step_number(2, appearance)) .with_child( Text::new( - "Create an integration", + i18n::t("agent_management.cloud_setup.step.create_integration"), appearance.ui_font_family(), step_title_font_size, ) @@ -528,8 +558,8 @@ impl CloudSetupGuideView { .finish(); let sub_description = Container::new(Self::render_description_with_link( - "Integrate Slack or Linear to assign Warp's Agent tasks with @Warp. ", - "Visit docs", + i18n::t("agent_management.cloud_setup.step.create_integration.docs_prefix"), + i18n::t("agent_management.cloud_setup.visit_docs"), self.integration_docs_link_mouse_state.clone(), SetupGuideDocs::Integration, appearance, diff --git a/app/src/ai/agent_management/details_action_buttons.rs b/app/src/ai/agent_management/details_action_buttons.rs index 2af397b6b8..4fd9e1713f 100644 --- a/app/src/ai/agent_management/details_action_buttons.rs +++ b/app/src/ai/agent_management/details_action_buttons.rs @@ -114,7 +114,7 @@ impl ConversationActionButtonsRow { let open_button = ctx.add_typed_action_view(|_| { Self::make_action_button( Icon::LinkExternal, - "Open conversation", + i18n::t("agent_management.details.open_conversation"), None, AgentDetailsAction::Open, ) @@ -123,7 +123,7 @@ impl ConversationActionButtonsRow { let cancel_task_button = ctx.add_typed_action_view(|_| { Self::make_action_button( Icon::StopFilled, - "Cancel task", + i18n::t("agent_management.details.cancel_task"), Some(AnsiColorIdentifier::Red), AgentDetailsAction::CancelTask, ) @@ -132,7 +132,7 @@ impl ConversationActionButtonsRow { let fork_conversation_button = ctx.add_typed_action_view(|_| { Self::make_action_button( Icon::ArrowSplit, - "Fork conversation", + i18n::t("agent_management.details.fork_conversation"), None, AgentDetailsAction::ForkConversation, ) @@ -141,7 +141,7 @@ impl ConversationActionButtonsRow { let view_details_button = ctx.add_typed_action_view(|_| { Self::make_action_button( Icon::Info, - "View details", + i18n::t("agent_management.details.view_details"), None, AgentDetailsAction::ViewDetails, ) @@ -150,7 +150,7 @@ impl ConversationActionButtonsRow { let copy_link_button = ctx.add_typed_action_view(|_| { Self::make_action_button( Icon::Link, - "Copy link to run", + i18n::t("agent_management.details.copy_link_to_run"), None, AgentDetailsAction::CopyLink, ) @@ -179,7 +179,7 @@ impl ConversationActionButtonsRow { fn make_action_button( icon: Icon, - tooltip: &str, + tooltip: String, icon_color: Option, action: AgentDetailsAction, ) -> ActionButton { diff --git a/app/src/ai/agent_management/notifications/item.rs b/app/src/ai/agent_management/notifications/item.rs index 182c87535b..a220e31d8c 100644 --- a/app/src/ai/agent_management/notifications/item.rs +++ b/app/src/ai/agent_management/notifications/item.rs @@ -33,11 +33,11 @@ pub enum NotificationFilter { } impl NotificationFilter { - pub(crate) fn label(&self) -> &'static str { + pub(crate) fn label(&self) -> String { match self { - NotificationFilter::All => "All tabs", - NotificationFilter::Unread => "Unread", - NotificationFilter::Errors => "Errors", + NotificationFilter::All => i18n::t("agent_management.notifications.filter.all_tabs"), + NotificationFilter::Unread => i18n::t("agent_management.notifications.filter.unread"), + NotificationFilter::Errors => i18n::t("agent_management.notifications.filter.errors"), } } } diff --git a/app/src/ai/agent_management/notifications/toast_stack.rs b/app/src/ai/agent_management/notifications/toast_stack.rs index 1efc923ac0..01ef61719d 100644 --- a/app/src/ai/agent_management/notifications/toast_stack.rs +++ b/app/src/ai/agent_management/notifications/toast_stack.rs @@ -459,7 +459,10 @@ fn render_keybinding_hint(keystroke: Keystroke, appearance: &Appearance) -> Box< let hint_text = appearance .ui_builder() - .wrappable_text("Open conversation".to_string(), false) + .wrappable_text( + i18n::t("agent_management.notifications.open_conversation"), + false, + ) .with_style(UiComponentStyles { font_size: Some(12.), font_color: Some(theme.disabled_text_color(theme.surface_2()).into()), diff --git a/app/src/ai/agent_management/notifications/view.rs b/app/src/ai/agent_management/notifications/view.rs index 4680e8cff1..f0493c7aa5 100644 --- a/app/src/ai/agent_management/notifications/view.rs +++ b/app/src/ai/agent_management/notifications/view.rs @@ -120,7 +120,7 @@ impl NotificationMailboxView { ActionButton::new("", NakedTheme) .with_icon(Icon::X) .with_size(ButtonSize::XSmall) - .with_tooltip("Close") + .with_tooltip(i18n::t("agent_management.notifications.close")) .with_tooltip_sublabel("Esc") .on_click(|ctx| { ctx.dispatch_typed_action(NotificationMailboxViewAction::Dismiss); @@ -128,11 +128,14 @@ impl NotificationMailboxView { }); let mark_all_read_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Mark all as read", NakedTheme) - .with_size(ButtonSize::Small) - .on_click(|ctx| { - ctx.dispatch_typed_action(NotificationMailboxViewAction::MarkAllRead); - }) + ActionButton::new( + i18n::t("agent_management.notifications.mark_all_as_read"), + NakedTheme, + ) + .with_size(ButtonSize::Small) + .on_click(|ctx| { + ctx.dispatch_typed_action(NotificationMailboxViewAction::MarkAllRead); + }) }); Self { @@ -409,7 +412,7 @@ impl NotificationMailboxView { let label = appearance .ui_builder() - .wrappable_text("Notifications".to_string(), false) + .wrappable_text(i18n::t("agent_management.notifications.title"), false) .with_style(UiComponentStyles { font_size: Some(14.), font_color: Some(theme.main_text_color(theme.surface_2()).into()), @@ -460,9 +463,11 @@ impl NotificationMailboxView { let is_active = self.active_filter == filter; let count = notifications.filtered_count(filter); let label = if count == 0 { - filter.label().to_string() + filter.label() } else { - format!("{} ({count})", filter.label()) + i18n::t("agent_management.notifications.filter_with_count") + .replace("{label}", &filter.label()) + .replace("{count}", &count.to_string()) }; let text_color = if is_active { theme.main_text_color(theme.surface_2()) @@ -546,7 +551,7 @@ impl NotificationMailboxView { Container::new( appearance .ui_builder() - .wrappable_text("No notifications".to_string(), false) + .wrappable_text(i18n::t("agent_management.notifications.empty"), false) .with_style(UiComponentStyles { font_size: Some(14.), font_color: Some(theme.sub_text_color(theme.surface_2()).into()), diff --git a/app/src/ai/agent_management/view.rs b/app/src/ai/agent_management/view.rs index 5c7a5003d6..9203713d96 100644 --- a/app/src/ai/agent_management/view.rs +++ b/app/src/ai/agent_management/view.rs @@ -113,8 +113,6 @@ const BUTTON_SIZE: f32 = 20.; const CARD_AGENT_ICON_SIZE: f32 = 24.; const CREATOR_AVATAR_FONT_SIZE: f32 = 10.; -const SESSION_EXPIRED_TEXT: &str = "Sessions expire after one week and cannot be opened."; - pub fn init(app: &mut AppContext) { use crate::util::bindings::cmd_or_ctrl_shift; @@ -129,6 +127,20 @@ fn should_show_artifacts(artifacts: &[Artifact]) -> bool { !artifacts.is_empty() && FeatureFlag::ConversationArtifacts.is_enabled() } +fn source_display_name(source: &AgentSource) -> String { + match source { + AgentSource::ScheduledAgent => i18n::t("agent_management.source.scheduled"), + AgentSource::Interactive | AgentSource::CloudMode => { + i18n::t("agent_management.source.warp_app") + } + AgentSource::WebApp => i18n::t("agent_management.source.oz_web"), + AgentSource::GitHubAction => i18n::t("agent_management.source.github_action"), + AgentSource::Linear | AgentSource::AgentWebhook | AgentSource::Slack | AgentSource::Cli => { + source.display_name().to_string() + } + } +} + pub type ManagementCardItemId = AgentConversationEntryId; /// Store state for a given task row @@ -222,9 +234,9 @@ impl AgentManagementView { let list_state = Self::construct_fresh_list_state(ctx.handle()); let all_filter_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("All", NakedTheme) + ActionButton::new(i18n::t("agent_management.owner_filter.all"), NakedTheme) .with_size(ButtonSize::Small) - .with_tooltip("View your agent tasks plus all shared team tasks") + .with_tooltip(i18n::t("agent_management.owner_filter.all.tooltip")) .on_click(|ctx| { ctx.dispatch_typed_action(AgentManagementViewAction::SetOwnerFilter( OwnerFilter::All, @@ -233,18 +245,21 @@ impl AgentManagementView { }); let personal_filter_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Personal", NakedTheme) - .with_size(ButtonSize::Small) - .with_tooltip("View agent tasks you created") - .on_click(|ctx| { - ctx.dispatch_typed_action(AgentManagementViewAction::SetOwnerFilter( - OwnerFilter::PersonalOnly, - )) - }) + ActionButton::new( + i18n::t("agent_management.owner_filter.personal"), + NakedTheme, + ) + .with_size(ButtonSize::Small) + .with_tooltip(i18n::t("agent_management.owner_filter.personal.tooltip")) + .on_click(|ctx| { + ctx.dispatch_typed_action(AgentManagementViewAction::SetOwnerFilter( + OwnerFilter::PersonalOnly, + )) + }) }); let setup_guide_button = CompactibleActionButton::new( - "Get started".to_string(), + i18n::t("agent_management.get_started"), None, ButtonSize::Small, AgentManagementViewAction::ToggleSetupGuide, @@ -254,7 +269,7 @@ impl AgentManagementView { ); let view_agents_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("View Agents", NakedTheme) + ActionButton::new(i18n::t("agent_management.view_agents"), NakedTheme) .with_size(ButtonSize::Small) .with_icon(Icon::ArrowLeft) .on_click(|ctx| { @@ -272,7 +287,7 @@ impl AgentManagementView { let creator_dropdown = ctx.add_typed_action_view(Self::create_creator_dropdown); let no_filter_results_button = ctx.add_typed_action_view(move |_ctx| { - ActionButton::new("Clear filters", SecondaryTheme) + ActionButton::new(i18n::t("agent_management.clear_filters"), SecondaryTheme) .with_size(ButtonSize::Small) .on_click(move |ctx| { ctx.dispatch_typed_action(AgentManagementViewAction::ClearFilters) @@ -280,7 +295,7 @@ impl AgentManagementView { }); let clear_all_filters_button = ctx.add_typed_action_view(move |_ctx| { - ActionButton::new("Clear all", NakedTheme) + ActionButton::new(i18n::t("agent_management.clear_all"), NakedTheme) .with_icon(Icon::X) .with_size(ButtonSize::Small) .on_click(move |ctx| { @@ -311,7 +326,7 @@ impl AgentManagementView { }, ctx, ); - editor.set_placeholder_text("Search", ctx); + editor.set_placeholder_text(i18n::t("agent_management.search_placeholder"), ctx); editor }); ctx.subscribe_to_view(&search_editor, |me, _handle, event, ctx| { @@ -319,7 +334,7 @@ impl AgentManagementView { }); let new_agent_button = CompactibleActionButton::new( - "New agent".to_string(), + i18n::t("agent_management.new_agent"), None, ButtonSize::Small, AgentManagementViewAction::ShowAgentTypeSelector, @@ -487,11 +502,15 @@ impl AgentManagementView { }; let mut dropdown = Dropdown::new(ctx); - Self::setup_filter_menu(&mut dropdown, "Status", ctx); + Self::setup_filter_menu( + &mut dropdown, + i18n::t("agent_management.filter.status"), + ctx, + ); // Use this helper to make dropdown items with status icons let make_status_option = - |label: &str, action: AgentManagementViewAction, icon_data: Option<(Icon, Fill)>| { + |label: String, action: AgentManagementViewAction, icon_data: Option<(Icon, Fill)>| { let mut fields = MenuItemFields::new(label) .with_on_select_action(DropdownAction::select_action_and_close(action)); if let Some((icon, color)) = icon_data { @@ -502,22 +521,22 @@ impl AgentManagementView { let items = vec![ make_status_option( - "All", + i18n::t("agent_management.filter.all"), AgentManagementViewAction::SetStatusFilter(StatusFilter::All), None, ), make_status_option( - "Working", + i18n::t("agent_management.status.working"), AgentManagementViewAction::SetStatusFilter(StatusFilter::Working), Some((Icon::ClockLoader, Fill::from(magenta))), ), make_status_option( - "Done", + i18n::t("agent_management.status.done"), AgentManagementViewAction::SetStatusFilter(StatusFilter::Done), Some((Icon::Check, Fill::from(green))), ), make_status_option( - "Failed", + i18n::t("agent_management.status.failed"), AgentManagementViewAction::SetStatusFilter(StatusFilter::Failed), Some((Icon::X, Fill::from(red))), ), @@ -532,7 +551,11 @@ impl AgentManagementView { ctx: &mut ViewContext>, ) -> Dropdown { let mut dropdown = Dropdown::new(ctx); - Self::setup_filter_menu(&mut dropdown, "Source", ctx); + Self::setup_filter_menu( + &mut dropdown, + i18n::t("agent_management.filter.source"), + ctx, + ); // Set a max height so we can fit all of the source options without scrolling dropdown.set_menu_max_height(200., ctx); @@ -561,7 +584,7 @@ impl AgentManagementView { } let mut items = vec![MenuItem::Item( - MenuItemFields::new("All").with_on_select_action( + MenuItemFields::new(i18n::t("agent_management.filter.all")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetSourceFilter(SourceFilter::All), ), @@ -569,7 +592,7 @@ impl AgentManagementView { )]; for source in sources { items.push(MenuItem::Item( - MenuItemFields::new(source.display_name()).with_on_select_action( + MenuItemFields::new(source_display_name(&source)).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetSourceFilter(SourceFilter::Specific(source)), ), @@ -596,29 +619,38 @@ impl AgentManagementView { ctx: &mut ViewContext>, ) -> Dropdown { let mut dropdown = Dropdown::new(ctx); - Self::setup_filter_menu(&mut dropdown, "Created on", ctx); + Self::setup_filter_menu( + &mut dropdown, + i18n::t("agent_management.filter.created_on"), + ctx, + ); let items = vec![ - MenuItem::Item(MenuItemFields::new("All").with_on_select_action( - DropdownAction::select_action_and_close( - AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::All), - ), - )), - MenuItem::Item(MenuItemFields::new("Last 24 hours").with_on_select_action( - DropdownAction::select_action_and_close( - AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::Last24Hours), - ), - )), - MenuItem::Item(MenuItemFields::new("Past 3 days").with_on_select_action( - DropdownAction::select_action_and_close( - AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::Past3Days), - ), - )), - MenuItem::Item(MenuItemFields::new("Last week").with_on_select_action( - DropdownAction::select_action_and_close( - AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::LastWeek), + MenuItem::Item( + MenuItemFields::new(i18n::t("agent_management.filter.all")).with_on_select_action( + DropdownAction::select_action_and_close( + AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::All), + ), ), - )), + ), + MenuItem::Item( + MenuItemFields::new(i18n::t("agent_management.created_on.last_24_hours")) + .with_on_select_action(DropdownAction::select_action_and_close( + AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::Last24Hours), + )), + ), + MenuItem::Item( + MenuItemFields::new(i18n::t("agent_management.created_on.past_3_days")) + .with_on_select_action(DropdownAction::select_action_and_close( + AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::Past3Days), + )), + ), + MenuItem::Item( + MenuItemFields::new(i18n::t("agent_management.created_on.last_week")) + .with_on_select_action(DropdownAction::select_action_and_close( + AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::LastWeek), + )), + ), ]; dropdown.set_rich_items(items, ctx); @@ -630,34 +662,44 @@ impl AgentManagementView { ctx: &mut ViewContext>, ) -> Dropdown { let mut dropdown = Dropdown::new(ctx); - Self::setup_filter_menu(&mut dropdown, "Has artifact", ctx); + Self::setup_filter_menu( + &mut dropdown, + i18n::t("agent_management.filter.has_artifact"), + ctx, + ); let items = vec![ - MenuItem::Item(MenuItemFields::new("All").with_on_select_action( - DropdownAction::select_action_and_close( - AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::All), - ), - )), - MenuItem::Item(MenuItemFields::new("Pull Request").with_on_select_action( - DropdownAction::select_action_and_close( - AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::PullRequest), - ), - )), - MenuItem::Item(MenuItemFields::new("Plan").with_on_select_action( - DropdownAction::select_action_and_close( - AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::Plan), - ), - )), - MenuItem::Item(MenuItemFields::new("Screenshot").with_on_select_action( - DropdownAction::select_action_and_close( - AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::Screenshot), - ), - )), - MenuItem::Item(MenuItemFields::new("File").with_on_select_action( - DropdownAction::select_action_and_close( - AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::File), + MenuItem::Item( + MenuItemFields::new(i18n::t("agent_management.filter.all")).with_on_select_action( + DropdownAction::select_action_and_close( + AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::All), + ), ), - )), + ), + MenuItem::Item( + MenuItemFields::new(i18n::t("agent_management.artifact.pull_request")) + .with_on_select_action(DropdownAction::select_action_and_close( + AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::PullRequest), + )), + ), + MenuItem::Item( + MenuItemFields::new(i18n::t("agent_management.artifact.plan")) + .with_on_select_action(DropdownAction::select_action_and_close( + AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::Plan), + )), + ), + MenuItem::Item( + MenuItemFields::new(i18n::t("agent_management.artifact.screenshot")) + .with_on_select_action(DropdownAction::select_action_and_close( + AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::Screenshot), + )), + ), + MenuItem::Item( + MenuItemFields::new(i18n::t("agent_management.artifact.file")) + .with_on_select_action(DropdownAction::select_action_and_close( + AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::File), + )), + ), ]; dropdown.set_rich_items(items, ctx); @@ -669,7 +711,11 @@ impl AgentManagementView { ctx: &mut ViewContext>, ) -> Dropdown { let mut dropdown = Dropdown::new(ctx); - Self::setup_filter_menu(&mut dropdown, "Harness", ctx); + Self::setup_filter_menu( + &mut dropdown, + i18n::t("agent_management.filter.harness"), + ctx, + ); let items = Self::build_harness_dropdown_items(ctx); dropdown.set_rich_items(items, ctx); @@ -679,7 +725,7 @@ impl AgentManagementView { fn build_harness_dropdown_items(app: &AppContext) -> Vec> { let mut items = vec![MenuItem::Item( - MenuItemFields::new("All").with_on_select_action( + MenuItemFields::new(i18n::t("agent_management.filter.all")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetHarnessFilter(HarnessFilter::All), ), @@ -707,20 +753,27 @@ impl AgentManagementView { ctx: &mut ViewContext>, ) -> FilterableDropdown { let mut dropdown = FilterableDropdown::new(ctx); - Self::setup_searchable_filter_menu(&mut dropdown, "Environment", ctx); + Self::setup_searchable_filter_menu( + &mut dropdown, + i18n::t("agent_management.filter.environment"), + ctx, + ); // Keep the button compact when a specific environment ID is selected by abbreviating the // displayed ID. (The dropdown menu still shows the full ID.) - dropdown.set_menu_header_text_override(|text| { - if matches!(text, "All" | "None") { - return format!("Environment: {text}"); + let environment_label = i18n::t("agent_management.filter.environment"); + let all_label = i18n::t("agent_management.filter.all"); + let none_label = i18n::t("agent_management.filter.none"); + dropdown.set_menu_header_text_override(move |text| { + if text == all_label || text == none_label { + return format!("{environment_label}: {text}"); } let abbreviated = text.chars().take(6).collect::(); if abbreviated == text { - format!("Environment: {text}") + format!("{environment_label}: {text}") } else { - format!("Environment: {abbreviated}…") + format!("{environment_label}: {abbreviated}…") } }); @@ -734,16 +787,21 @@ impl AgentManagementView { ctx: &mut ViewContext>, ) -> FilterableDropdown { let mut dropdown = FilterableDropdown::new(ctx); - Self::setup_searchable_filter_menu(&mut dropdown, "Created by", ctx); + Self::setup_searchable_filter_menu( + &mut dropdown, + i18n::t("agent_management.filter.created_by"), + ctx, + ); dropdown } // Initialize the dropdown menu for the filter dropdowns (status, source) fn setup_filter_menu( dropdown: &mut Dropdown, - label_prefix: &'static str, + label_prefix: impl Into, ctx: &mut ViewContext>, ) { + let label_prefix = label_prefix.into(); dropdown.set_menu_width(160., ctx); dropdown.set_main_axis_size(MainAxisSize::Min, ctx); dropdown.set_menu_header_text_override(move |text| format!("{}: {}", label_prefix, text)); @@ -753,9 +811,10 @@ impl AgentManagementView { // Initialize the dropdown menu for the searchable filter dropdowns (creator) fn setup_searchable_filter_menu( dropdown: &mut FilterableDropdown, - label_prefix: &'static str, + label_prefix: impl Into, ctx: &mut ViewContext>, ) { + let label_prefix = label_prefix.into(); dropdown.set_menu_width(320., ctx); dropdown.set_main_axis_size(MainAxisSize::Min, ctx); dropdown.set_menu_header_text_override(move |text| format!("{}: {}", label_prefix, text)); @@ -776,15 +835,9 @@ impl AgentManagementView { let model = AgentConversationsModel::as_ref(ctx); let envs = model.get_all_environment_ids_and_names(ctx); - let selected_name = match &self.filters.environment { - EnvironmentFilter::All => Some("All".to_string()), - EnvironmentFilter::NoEnvironment => Some("None".to_string()), - EnvironmentFilter::Specific(id) => envs.get(id).cloned(), - }; - self.environment_dropdown.update(ctx, |dropdown, ctx| { let mut items = vec![MenuItem::Item( - MenuItemFields::new("All").with_on_select_action( + MenuItemFields::new(i18n::t("agent_management.filter.all")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetEnvironmentFilter(EnvironmentFilter::All), ), @@ -792,7 +845,7 @@ impl AgentManagementView { )]; items.push(MenuItem::Item( - MenuItemFields::new("None").with_on_select_action( + MenuItemFields::new(i18n::t("agent_management.filter.none")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetEnvironmentFilter( EnvironmentFilter::NoEnvironment, @@ -817,21 +870,18 @@ impl AgentManagementView { } dropdown.set_rich_items(items, ctx); - if let Some(selected_name) = selected_name { - dropdown.set_selected_by_name(&selected_name, ctx); - } + dropdown.set_selected_by_action( + AgentManagementViewAction::SetEnvironmentFilter(self.filters.environment.clone()), + ctx, + ); }); } fn update_creator_dropdown(&mut self, ctx: &mut ViewContext) { let creators = AgentConversationsModel::as_ref(ctx).get_all_creators(ctx); - let creator_filter_name = match &self.filters.creator { - CreatorFilter::All => "All", - CreatorFilter::Specific { name, .. } => name, - }; self.creator_dropdown.update(ctx, |dropdown, ctx| { let mut items = vec![MenuItem::Item( - MenuItemFields::new("All").with_on_select_action( + MenuItemFields::new(i18n::t("agent_management.filter.all")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetCreatorFilter(CreatorFilter::All), ), @@ -850,7 +900,10 @@ impl AgentManagementView { )); } dropdown.set_rich_items(items, ctx); - dropdown.set_selected_by_name(creator_filter_name, ctx); + dropdown.set_selected_by_action( + AgentManagementViewAction::SetCreatorFilter(self.filters.creator.clone()), + ctx, + ); }); } @@ -1215,7 +1268,9 @@ impl AgentManagementView { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::default("Copied branch name".to_string()); + let toast = DismissibleToast::default(i18n::t( + "agent_management.toast.copied_branch_name", + )); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); } @@ -1506,13 +1561,16 @@ impl AgentManagementView { // Early return if session is available - no status label rendered let (label_text, tooltip_text_opt) = match session_status { - SessionStatus::Expired => ("Session expired", Some(SESSION_EXPIRED_TEXT)), - SessionStatus::Unavailable => ("No session available", None), + SessionStatus::Expired => ( + i18n::t("agent_management.session.expired"), + Some(i18n::t("agent_management.session.expired_tooltip")), + ), + SessionStatus::Unavailable => (i18n::t("agent_management.session.unavailable"), None), SessionStatus::Available => return Empty::new().finish(), }; Hoverable::new(mouse_state, move |state| { - let label = Text::new_inline(label_text, font_family, font_size) + let label = Text::new_inline(label_text.clone(), font_family, font_size) .with_color(theme.nonactive_ui_text_color().into()); let container = Container::new(label.finish()) @@ -1522,11 +1580,8 @@ impl AgentManagementView { let mut stack = Stack::new().with_child(container.finish()); if state.is_hovered() { - if let Some(tooltip_text) = tooltip_text_opt { - let tooltip = ui_builder - .tool_tip(tooltip_text.to_string()) - .build() - .finish(); + if let Some(tooltip_text) = tooltip_text_opt.as_ref() { + let tooltip = ui_builder.tool_tip(tooltip_text.clone()).build().finish(); stack.add_positioned_overlay_child( tooltip, OffsetPositioning::offset_from_parent( @@ -1740,7 +1795,7 @@ impl AgentManagementView { .creator .name .clone() - .unwrap_or_else(|| "Unknown".to_string()); + .unwrap_or_else(|| i18n::t("agent_management.unknown")); let avatar = Self::render_avatar_with_tooltip( &creator_name, appearance, @@ -1790,14 +1845,19 @@ impl AgentManagementView { let mut metadata_parts = Vec::new(); if let Some(source) = &entry.display.source { - metadata_parts.push(format!("Source: {}", source.display_name())); + metadata_parts.push(format!( + "{}: {}", + i18n::t("agent_management.metadata.source"), + source_display_name(source) + )); } let availability = HarnessAvailabilityModel::as_ref(app); if availability.should_show_harness_selector() { if let Some(harness) = entry.display.harness { metadata_parts.push(format!( - "Harness: {}", + "{}: {}", + i18n::t("agent_management.metadata.harness"), availability.display_name_for(harness) )); } @@ -1812,9 +1872,9 @@ impl AgentManagementView { .principal_type .is_some_and(|pt| pt.is_service_account()) { - "Agent" + i18n::t("agent_management.metadata.agent") } else { - "Executor" + i18n::t("agent_management.metadata.executor") }; metadata_parts.push(format!("{label}: {name}")); } @@ -1822,11 +1882,17 @@ impl AgentManagementView { } if let Some(run_time) = &entry.display.run_time { - metadata_parts.push(format!("Run time: {run_time}")); + metadata_parts.push(format!( + "{}: {run_time}", + i18n::t("agent_management.metadata.run_time") + )); } if let Some(usage) = entry.display.request_usage.map(format_credits) { - metadata_parts.push(format!("Credits used: {usage}")); + metadata_parts.push(format!( + "{}: {usage}", + i18n::t("agent_management.metadata.credits_used") + )); } Text::new(metadata_parts.join(" • "), font_family, font_size) @@ -1896,7 +1962,7 @@ impl AgentManagementView { let build_header = |use_expanded: bool| { let title = Text::new_inline( - "Runs", + i18n::t("agent_management.runs_title"), appearance.ui_font_family(), appearance.ui_font_size() + 4., ) @@ -2006,7 +2072,7 @@ impl AgentManagementView { let mut stack = Stack::new().with_child(loading_icon); if mouse_state.is_hovered() { let tooltip = ui_builder - .tool_tip(String::from("Loading cloud agent runs")) + .tool_tip(i18n::t("agent_management.loading_cloud_runs")) .build() .finish(); stack.add_positioned_overlay_child( @@ -2029,7 +2095,7 @@ impl AgentManagementView { let theme = appearance.theme(); let title = Text::new_inline( - "Runs", + i18n::t("agent_management.runs_title"), appearance.ui_font_family(), appearance.ui_font_size() + 4., ) @@ -2051,7 +2117,7 @@ impl AgentManagementView { .with_child(Container::new(loading_icon).with_margin_right(10.).finish()) .with_child( Text::new_inline( - "Loading agents...", + i18n::t("agent_management.loading_agents"), appearance.ui_font_family(), appearance.ui_font_size() + 2., ) @@ -2107,7 +2173,7 @@ impl AgentManagementView { .finish(); let text = Text::new_inline( - "No results matched your filters", + i18n::t("agent_management.no_filter_results"), appearance.ui_font_family(), appearance.ui_font_size(), ) diff --git a/app/src/ai/agent_sdk/admin.rs b/app/src/ai/agent_sdk/admin.rs index 8d7e85a31a..4aaae393fd 100644 --- a/app/src/ai/agent_sdk/admin.rs +++ b/app/src/ai/agent_sdk/admin.rs @@ -33,19 +33,28 @@ pub fn login(ctx: &mut AppContext) -> Result<()> { let auth_state = AuthStateProvider::as_ref(ctx).get(); match (auth_state.username_for_display(), auth_state.user_email()) { (Some(username), Some(email)) if username != email => { - println!("You are already logged in as {username} ({email}).") + println!( + "{}", + i18n::t("ai.agent_sdk.admin.already_logged_in_as_username_email") + .replace("{username}", &username) + .replace("{email}", &email) + ) } (Some(name), _) | (None, Some(name)) => { - println!("You are already logged in as {name}.") + println!( + "{}", + i18n::t("ai.agent_sdk.admin.already_logged_in_as_name") + .replace("{name}", &name) + ) } (None, None) => { - println!("You are already logged in.") + println!("{}", i18n::t("ai.agent_sdk.admin.already_logged_in")) } } ctx.terminate_app(TerminationMode::ForceTerminate, None); } else { // Device auth succeeded. - println!("Logged in successfully"); + println!("{}", i18n::t("ai.agent_sdk.admin.logged_in_successfully")); ctx.terminate_app(TerminationMode::ForceTerminate, None); } } @@ -60,9 +69,10 @@ pub fn login(ctx: &mut AppContext) -> Result<()> { // Device auth failed. let err_msg = match event { AuthManagerEvent::AuthFailed(err) => { - format!("Authentication failed: {err:#}") + i18n::t("ai.agent_sdk.admin.authentication_failed_with_error") + .replace("{error}", &format!("{err:#}")) } - _ => "Authentication failed".to_string(), + _ => i18n::t("ai.agent_sdk.admin.authentication_failed"), }; ctx.terminate_app( TerminationMode::ForceTerminate, @@ -76,10 +86,16 @@ pub fn login(ctx: &mut AppContext) -> Result<()> { user_code, } => { if let Some(url) = verification_url_complete { - println!("To log in, open this URL in your browser:\n{url}"); + println!( + "{}", + i18n::t("ai.agent_sdk.admin.login_open_url").replace("{url}", &url) + ); } else { println!( - "To log in, visit {verification_url} and enter this code: {user_code}" + "{}", + i18n::t("ai.agent_sdk.admin.login_visit_and_enter_code") + .replace("{url}", &verification_url) + .replace("{code}", &user_code) ); } } @@ -136,7 +152,7 @@ pub fn whoami(ctx: &mut AppContext, output_format: OutputFormat) -> Result<()> { .map(String::from) .unwrap_or(s) }) - .ok_or_else(|| anyhow::anyhow!("Could not determine user ID. Are you logged in?"))?; + .ok_or_else(|| anyhow::anyhow!(i18n::t("ai.agent_sdk.admin.user_id_missing")))?; let mut info = WhoamiOutput { uid, @@ -182,22 +198,44 @@ pub fn whoami(ctx: &mut AppContext, output_format: OutputFormat) -> Result<()> { } OutputFormat::Pretty => { match principal_type { - PrincipalType::User => println!("User ID: {}", info.uid), + PrincipalType::User => { + println!( + "{}", + i18n::t("ai.agent_sdk.admin.user_id").replace("{uid}", &info.uid) + ) + } PrincipalType::ServiceAccount => { - println!("Service account ID: {}", info.uid) + println!( + "{}", + i18n::t("ai.agent_sdk.admin.service_account_id") + .replace("{uid}", &info.uid) + ) } } if let Some(name) = &info.display_name { - println!("Display Name: {name}"); + println!( + "{}", + i18n::t("ai.agent_sdk.admin.display_name").replace("{name}", name) + ); } if let Some(email) = &info.email { - println!("Email: {email}"); + println!( + "{}", + i18n::t("ai.agent_sdk.admin.email").replace("{email}", email) + ); } if let Some(team_uid) = &info.team_uid { - println!("Team ID: {team_uid}"); + println!( + "{}", + i18n::t("ai.agent_sdk.admin.team_id").replace("{team_uid}", team_uid) + ); } if let Some(team_name) = &info.team_name { - println!("Team Name: {team_name}"); + println!( + "{}", + i18n::t("ai.agent_sdk.admin.team_name") + .replace("{team_name}", team_name) + ); } } OutputFormat::Text => { @@ -206,9 +244,9 @@ pub fn whoami(ctx: &mut AppContext, output_format: OutputFormat) -> Result<()> { OutputFormat::Ndjson => { ctx.terminate_app( TerminationMode::ForceTerminate, - Some(Err(anyhow::anyhow!( - "`whoami` does not support `--output-format ndjson`" - ))), + Some(Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.admin.whoami_ndjson_unsupported" + )))), ); return; } @@ -225,13 +263,13 @@ pub fn whoami(ctx: &mut AppContext, output_format: OutputFormat) -> Result<()> { pub fn logout(ctx: &mut AppContext) -> Result<()> { let auth_state = AuthStateProvider::as_ref(ctx).get(); if !auth_state.is_logged_in() { - println!("You are not logged in."); + println!("{}", i18n::t("ai.agent_sdk.admin.not_logged_in")); ctx.terminate_app(TerminationMode::ForceTerminate, None); return Ok(()); } crate::auth::log_out(ctx); - println!("Logged out successfully."); + println!("{}", i18n::t("ai.agent_sdk.admin.logged_out_successfully")); ctx.terminate_app(TerminationMode::ForceTerminate, None); Ok(()) } diff --git a/app/src/ai/agent_sdk/agent_config.rs b/app/src/ai/agent_sdk/agent_config.rs index 47f51e36be..98347fc63e 100644 --- a/app/src/ai/agent_sdk/agent_config.rs +++ b/app/src/ai/agent_sdk/agent_config.rs @@ -47,10 +47,10 @@ fn parse_repo_spec(spec: &str) -> anyhow::Result { return Ok(GithubRepo::new(parts[0].to_string(), parts[1].to_string())); } - Err(anyhow::anyhow!( - "Invalid repo format: '{}'. Expected 'owner/repo' or 'https://github.com/owner/repo'", - spec - )) + Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.agent_config.invalid_repo_format" + ) + .replace("{repo}", spec))) } impl AgentConfigRunner { @@ -77,10 +77,10 @@ impl AgentConfigRunner { if attempt > MAX_AUTH_ATTEMPTS { ctx.terminate_app( TerminationMode::ForceTerminate, - Some(Err(anyhow::anyhow!( - "Exceeded maximum number of authorization attempts ({}). Please try again later.", - MAX_AUTH_ATTEMPTS - ))), + Some(Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.agent_config.max_authorization_attempts_exceeded" + ) + .replace("{count}", &MAX_AUTH_ATTEMPTS.to_string())))), ); return; } @@ -111,15 +111,24 @@ impl AgentConfigRunner { UserRepoAuthStatusEnum::NoInstallationOrAccessForRepo => { if !status.is_public { eprintln!( - "Cannot access private repo {}/{}", - status.owner, status.repo, + "{}", + i18n::t( + "ai.agent_sdk.agent_config.cannot_access_private_repo" + ) + .replace("{owner}", &status.owner) + .replace("{repo}", &status.repo), ); has_blocking_private_issues = true; } // Public repos without auth are fine - no warning needed } UserRepoAuthStatusEnum::UserNotConnectedToGithub => { - eprintln!("User not connected to GitHub"); + eprintln!( + "{}", + i18n::t( + "ai.agent_sdk.agent_config.user_not_connected_to_github" + ) + ); has_blocking_private_issues = true; break; } @@ -135,8 +144,19 @@ impl AgentConfigRunner { // Handle OAuth flow if server provides auth_url + tx_id match (response.auth_url, response.tx_id) { (Some(auth_url), Some(tx_id)) => { - println!("\nAuthorization required for private repository access."); - println!("Opening browser for GitHub authorization: {auth_url}\n"); + println!( + "\n{}", + i18n::t( + "ai.agent_sdk.agent_config.private_repo_authorization_required" + ) + ); + println!( + "{}\n", + i18n::t( + "ai.agent_sdk.agent_config.opening_github_authorization" + ) + .replace("{url}", &auth_url) + ); ctx.open_url(&auth_url); let integrations_client = ServerApiProvider::handle(ctx) @@ -157,7 +177,9 @@ impl AgentConfigRunner { ctx.terminate_app( TerminationMode::ForceTerminate, Some(Err(anyhow::anyhow!( - "GitHub authorization failed. Please try again." + i18n::t( + "ai.agent_sdk.agent_config.github_authorization_failed" + ) ))), ); } @@ -165,7 +187,9 @@ impl AgentConfigRunner { ctx.terminate_app( TerminationMode::ForceTerminate, Some(Err(anyhow::anyhow!( - "GitHub authorization expired. Please try again." + i18n::t( + "ai.agent_sdk.agent_config.github_authorization_expired" + ) ))), ); } @@ -173,7 +197,9 @@ impl AgentConfigRunner { ctx.terminate_app( TerminationMode::ForceTerminate, Some(Err(anyhow::anyhow!( - "Unexpected OAuth status" + i18n::t( + "ai.agent_sdk.agent_config.unexpected_oauth_status" + ) ))), ); } @@ -181,7 +207,10 @@ impl AgentConfigRunner { ctx.terminate_app( TerminationMode::ForceTerminate, Some(Err(anyhow::anyhow!( - "Error polling OAuth status: {err}" + i18n::t( + "ai.agent_sdk.agent_config.poll_oauth_status_error" + ) + .replace("{error}", &err.to_string()) ))), ); } @@ -189,15 +218,22 @@ impl AgentConfigRunner { }); } (Some(auth_url), None) => { - println!("\nAuthorize access here: {auth_url}\n"); - println!("After authorizing, please re-run this command."); + println!( + "\n{}\n", + i18n::t("ai.agent_sdk.agent_config.authorize_access_here") + .replace("{url}", &auth_url) + ); + println!( + "{}", + i18n::t("ai.agent_sdk.agent_config.rerun_after_authorizing") + ); ctx.terminate_app(TerminationMode::ForceTerminate, None); } _ => { ctx.terminate_app( TerminationMode::ForceTerminate, Some(Err(anyhow::anyhow!( - "Cannot list agents: authorization required but no auth flow provided" + i18n::t("ai.agent_sdk.agent_config.no_auth_flow_provided") ))), ); } @@ -206,7 +242,9 @@ impl AgentConfigRunner { Err(e) => { ctx.terminate_app( TerminationMode::ForceTerminate, - Some(Err(e.context("Failed to check GitHub auth status"))), + Some(Err(e.context(i18n::t( + "ai.agent_sdk.agent_config.check_github_auth_status_failed", + )))), ); } } @@ -217,9 +255,15 @@ impl AgentConfigRunner { let ai_client = ServerApiProvider::handle(ctx).as_ref(ctx).get_ai_client(); if repo.is_some() { - println!("Fetching agent skills from the specified repository..."); + println!( + "{}", + i18n::t("ai.agent_sdk.agent_config.fetching_from_repository") + ); } else { - println!("Fetching agent skills from your Warp environments..."); + println!( + "{}", + i18n::t("ai.agent_sdk.agent_config.fetching_from_environments") + ); } let list_future = async move { ai_client.list_skills(repo).await }; @@ -238,14 +282,18 @@ impl AgentConfigRunner { /// Print a list of agents in a card-style format. fn print_agents_table(agents: &[AgentSkillItem]) { if agents.is_empty() { - println!("No skills found."); + println!("{}", i18n::t("ai.agent_sdk.agent_config.no_skills_found")); return; } if agents.len() == 1 { - println!("\nAgent:"); + println!("\n{}", i18n::t("ai.agent_sdk.agent_config.heading.agent")); } else { - println!("\nAgents ({}):", agents.len()); + println!( + "\n{}", + i18n::t("ai.agent_sdk.agent_config.heading.agents") + .replace("{count}", &agents.len().to_string()) + ); } for agent in agents { @@ -255,12 +303,15 @@ impl AgentConfigRunner { let mut table = super::output::standard_table(); // ID - table.add_row(vec![format!("ID: {}", variant.id)]); + table.add_row(vec![ + i18n::t("ai.agent_sdk.agent_config.field.id").replace("{id}", &variant.id) + ]); // Description if !variant.description.is_empty() { + let description_label = i18n::t("ai.agent_sdk.agent_config.label.description"); let description_cell = super::text_layout::render_labeled_wrapped_field( - "Description", + &description_label, &variant.description, MAX_LINE_WIDTH, ); @@ -276,8 +327,9 @@ impl AgentConfigRunner { } else { truncated }; + let base_prompt_label = i18n::t("ai.agent_sdk.agent_config.label.base_prompt"); let prompt_cell = super::text_layout::render_labeled_wrapped_field( - "Base Prompt", + &base_prompt_label, &truncated_prompt, MAX_LINE_WIDTH, ); @@ -286,8 +338,10 @@ impl AgentConfigRunner { // Source table.add_row(vec![format!( - "Source: {}/{}", - variant.source.owner, variant.source.name + "{}", + i18n::t("ai.agent_sdk.agent_config.field.source") + .replace("{owner}", &variant.source.owner) + .replace("{name}", &variant.source.name) )]); // Environments @@ -297,7 +351,10 @@ impl AgentConfigRunner { .iter() .map(|e| format!("{} ({})", e.name, e.uid)) .collect(); - table.add_row(vec![format!("Environments: {}", env_entries.join(", "))]); + table.add_row(vec![i18n::t( + "ai.agent_sdk.agent_config.field.environments", + ) + .replace("{environments}", &env_entries.join(", "))]); } println!("{table}"); diff --git a/app/src/ai/agent_sdk/agent_management.rs b/app/src/ai/agent_sdk/agent_management.rs index 5d60e798f4..5c942c3b9e 100644 --- a/app/src/ai/agent_sdk/agent_management.rs +++ b/app/src/ai/agent_sdk/agent_management.rs @@ -225,7 +225,9 @@ impl AgentManagementRunner { }, }; if request_is_empty(&request) { - return Err(anyhow!("No updates requested")); + return Err(anyhow!(i18n::t( + "ai.agent_sdk.agent_management.no_updates_requested" + ))); } if matches!(output_format, OutputFormat::Json) || json_output.force_json_output() { @@ -315,9 +317,9 @@ fn ensure_json_sort_is_not_requested( if (matches!(output_format, OutputFormat::Json) || json_output.force_json_output()) && (args.sort_by.is_some() || args.sort_order.is_some()) { - return Err(anyhow!( - "--sort-by and --sort-order are not supported with JSON output" - )); + return Err(anyhow!(i18n::t( + "ai.agent_sdk.agent_management.json_sort_unsupported" + ))); } Ok(()) @@ -362,14 +364,14 @@ fn sort_agents( impl TableFormat for AgentResponse { fn header() -> Vec { vec![ - Cell::new("UID"), - Cell::new("Name"), - Cell::new("Created"), - Cell::new("Description"), - Cell::new("Secrets"), - Cell::new("Skills"), - Cell::new("Base model"), - Cell::new("Environment"), + Cell::new(i18n::t("ai.agent_sdk.agent_management.table.uid")), + Cell::new(i18n::t("ai.agent_sdk.agent_management.table.name")), + Cell::new(i18n::t("ai.agent_sdk.agent_management.table.created")), + Cell::new(i18n::t("ai.agent_sdk.agent_management.table.description")), + Cell::new(i18n::t("ai.agent_sdk.agent_management.table.secrets")), + Cell::new(i18n::t("ai.agent_sdk.agent_management.table.skills")), + Cell::new(i18n::t("ai.agent_sdk.agent_management.table.base_model")), + Cell::new(i18n::t("ai.agent_sdk.agent_management.table.environment")), ] } @@ -395,7 +397,10 @@ fn print_agents(agents: &[AgentResponse], output_format: OutputFormat) -> anyhow let (visible_agents, hidden_count) = visible_agents_and_hidden_count(agents); match output_format { OutputFormat::Pretty if visible_agents.is_empty() => { - println!("No agents found."); + println!( + "{}", + i18n::t("ai.agent_sdk.agent_management.no_agents_found") + ); print_skills_hint(); } OutputFormat::Pretty => { @@ -433,7 +438,11 @@ fn visible_agents_and_hidden_count(agents: &[AgentResponse]) -> (Vec 0 { - eprintln!("{hidden_count} disabled agents hidden"); + eprintln!( + "{}", + i18n::t("ai.agent_sdk.agent_management.disabled_agents_hidden") + .replace("{count}", &hidden_count.to_string()) + ); } } fn print_single_agent(agent: &AgentResponse, output_format: OutputFormat) -> anyhow::Result<()> { @@ -451,7 +460,10 @@ fn print_single_agent(agent: &AgentResponse, output_format: OutputFormat) -> any fn print_skills_hint() { let binary_name = warp_cli::binary_name().unwrap_or_else(|| "warp".to_string()); - println!("\n\nLooking for your agent skills? Use `{binary_name} agent skills` instead."); + println!( + "\n\n{}", + i18n::t("ai.agent_sdk.agent_management.skills_hint").replace("{binary_name}", &binary_name) + ); } #[derive(Serialize)] @@ -463,7 +475,10 @@ struct DeleteAgentResult<'a> { fn print_delete_result(uid: &str, output_format: OutputFormat) -> anyhow::Result<()> { match output_format { OutputFormat::Pretty => { - println!("Deleted agent {uid}."); + println!( + "{}", + i18n::t("ai.agent_sdk.agent_management.deleted").replace("{uid}", uid) + ); } OutputFormat::Text => { let mut stdout = std::io::stdout(); diff --git a/app/src/ai/agent_sdk/ambient.rs b/app/src/ai/agent_sdk/ambient.rs index b7fe198096..8032550c21 100644 --- a/app/src/ai/agent_sdk/ambient.rs +++ b/app/src/ai/agent_sdk/ambient.rs @@ -232,11 +232,15 @@ impl AmbientAgentRunner { } fn run_agent(&self, args: RunCloudArgs, ctx: &mut ModelContext) -> anyhow::Result<()> { if !FeatureFlag::AmbientAgentsCommandLine.is_enabled() { - return Err(anyhow::anyhow!("Unsupported feature")); + return Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.ambient.unsupported_feature" + ))); } let skill_enabled = FeatureFlag::OzPlatformSkills.is_enabled(); if args.skill.is_some() && !skill_enabled { - return Err(anyhow::anyhow!("unexpected argument '--skill' found")); + return Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.ambient.unexpected_skill_arg" + ))); } let refresh_future = super::common::refresh_workspace_metadata(ctx); @@ -257,7 +261,7 @@ impl AmbientAgentRunner { || args.conversation.is_some(); if !has_prompt_source { super::report_fatal_error( - anyhow::anyhow!("Either --prompt, --skill, or --conversation must be provided"), + anyhow::anyhow!(i18n::t("ai.agent_sdk.ambient.missing_prompt_source")), ctx, ); return; @@ -275,7 +279,11 @@ impl AmbientAgentRunner { Ok(server_id) => server_id.into(), Err(err) => { super::report_fatal_error( - anyhow::anyhow!("Failed to parse saved prompt ID '{id}': {err}"), + anyhow::anyhow!(i18n::t( + "ai.agent_sdk.ambient.saved_prompt_parse_failed" + ) + .replace("{id}", &id) + .replace("{error}", &err.to_string())), ctx, ); return; @@ -290,7 +298,10 @@ impl AmbientAgentRunner { Some(prompt_text) => Some(prompt_text.to_string()), None => { super::report_fatal_error( - anyhow::anyhow!("'{id}' is not a saved prompt"), + anyhow::anyhow!(i18n::t( + "ai.agent_sdk.ambient.saved_prompt_not_prompt" + ) + .replace("{id}", &id)), ctx, ); return; @@ -298,7 +309,10 @@ impl AmbientAgentRunner { }, None => { super::report_fatal_error( - anyhow::anyhow!("Saved prompt with ID '{id}' not found"), + anyhow::anyhow!(i18n::t( + "ai.agent_sdk.ambient.saved_prompt_not_found" + ) + .replace("{id}", &id)), ctx, ); return; @@ -323,11 +337,9 @@ impl AmbientAgentRunner { // This ensures users don't have to go through env selection if attachment validation fails if args.attachment_paths.len() > MAX_ATTACHMENT_COUNT_FOR_CLOUD_QUERY { super::report_fatal_error( - anyhow::anyhow!( - "Too many attachments. Maximum {} attachments allowed, but {} were provided.", - MAX_ATTACHMENT_COUNT_FOR_CLOUD_QUERY, - args.attachment_paths.len() - ), + anyhow::anyhow!(i18n::t("ai.agent_sdk.ambient.too_many_attachments") + .replace("{max}", &MAX_ATTACHMENT_COUNT_FOR_CLOUD_QUERY.to_string()) + .replace("{count}", &args.attachment_paths.len().to_string())), ctx, ); return; @@ -354,7 +366,9 @@ impl AmbientAgentRunner { } else { if !args.attachment_paths.is_empty() { super::report_fatal_error( - anyhow::anyhow!("Attachment upload is not enabled"), + anyhow::anyhow!(i18n::t( + "ai.agent_sdk.ambient.attachment_upload_not_enabled" + )), ctx, ); return; @@ -375,9 +389,9 @@ impl AmbientAgentRunner { let environment_id = match EnvironmentChoice::resolve_for_create(environment_args, ctx) { Ok(EnvironmentChoice::None) => { - eprintln!("Agent will run without an environment."); + eprintln!("{}", i18n::t("ai.agent_sdk.ambient.without_environment")); None - }, + } Ok(EnvironmentChoice::Environment { id, .. }) => Some(id), Err(ResolveConfigurationError::Canceled) => { ctx.terminate_app(TerminationMode::ForceTerminate, None); @@ -499,7 +513,11 @@ impl AmbientAgentRunner { let oz_root_url = ChannelState::oz_root_url(); let ai_client_clone = ai_client.clone(); let spawn_future = async move { - let mut stream = Box::pin(spawn_task(request, ai_client_clone, Some(TASK_STATUS_POLLING_DURATION))); + let mut stream = Box::pin(spawn_task( + request, + ai_client_clone, + Some(TASK_STATUS_POLLING_DURATION), + )); let mut session_join_info = None; let mut spawned_task_id = None; @@ -507,14 +525,29 @@ impl AmbientAgentRunner { match event_result { Ok(event) => match event { AmbientAgentEvent::TaskSpawned { task_id, .. } => { - println!("Spawned ambient agent with run ID: {task_id}"); - println!("View run: {oz_root_url}/runs/{task_id}"); + println!( + "{}", + i18n::t("ai.agent_sdk.ambient.spawned") + .replace("{id}", &task_id.to_string()) + ); + println!( + "{}", + i18n::t("ai.agent_sdk.ambient.view_run") + .replace("{url}", &format!("{oz_root_url}/runs/{task_id}")) + ); spawned_task_id = Some(task_id); } AmbientAgentEvent::AtCapacity => { - println!("Concurrent cloud agent limit reached. This agent run will begin when one of your current cloud runs completes."); + println!( + "{}", + i18n::t("ai.agent_sdk.ambient.concurrent_limit_reached") + ); if let Some(url) = &upgrade_link { - println!("To increase your concurrent agent limit, upgrade your plan: {}", url); + println!( + "{}", + i18n::t("ai.agent_sdk.ambient.upgrade_plan") + .replace("{url}", url) + ); } } AmbientAgentEvent::StateChanged { @@ -527,25 +560,51 @@ impl AmbientAgentRunner { | AmbientAgentTaskState::Succeeded ) || state.is_failure_like() { - println!("Agent state: {:?}", state); + println!( + "{}", + i18n::t("ai.agent_sdk.ambient.agent_state") + .replace("{state}", &format!("{state:?}")) + ); } if state.is_failure_like() { if let Some(msg) = status_message { - println!("Error: {}", msg.message); + println!( + "{}", + i18n::t("ai.agent_sdk.ambient.error") + .replace("{message}", &msg.message) + ); } else { - println!("Run failed with no error message"); + println!( + "{}", + i18n::t("ai.agent_sdk.ambient.failed_no_error_message") + ); } } } AmbientAgentEvent::SessionStarted { session_join_info: info, } => { - println!("View agent session: {}", info.session_link); + println!( + "{}", + i18n::t("ai.agent_sdk.ambient.view_session") + .replace("{url}", &info.session_link) + ); session_join_info = Some(info); } AmbientAgentEvent::TimedOut => { - let task_id_str = spawned_task_id.as_ref().map_or_else(|| "unknown".to_string(), |id| id.to_string()); - println!("Agent session with run ID {task_id_str} is not ready after {}s. Check for a sharing link in the ambient agent management panel. See https://docs.warp.dev/agent-platform/cloud-agents/managing-cloud-agents for details.", TASK_STATUS_POLLING_DURATION.as_secs()); + let task_id_str = spawned_task_id.as_ref().map_or_else( + || i18n::t("ai.agent_sdk.ambient.unknown"), + |id| id.to_string(), + ); + println!( + "{}", + i18n::t("ai.agent_sdk.ambient.session_not_ready") + .replace("{id}", &task_id_str) + .replace( + "{seconds}", + &TASK_STATUS_POLLING_DURATION.as_secs().to_string(), + ) + ); } }, Err(err) => { @@ -814,14 +873,18 @@ impl AmbientAgentRunner { /// Print runs in a beautifully formatted ASCII table with card-style layout. fn print_tasks_table(tasks: &[AmbientAgentTask]) { if tasks.is_empty() { - println!("No runs found."); + println!("{}", i18n::t("ai.agent_sdk.ambient.no_runs_found")); return; } if tasks.len() == 1 { - println!("\nAgent Run:"); + println!("\n{}", i18n::t("ai.agent_sdk.ambient.heading.run")); } else { - println!("\nAgent Runs ({}):", tasks.len()); + println!( + "\n{}", + i18n::t("ai.agent_sdk.ambient.heading.runs") + .replace("{count}", &tasks.len().to_string()) + ); } let oz_root_url = ChannelState::oz_root_url(); @@ -840,8 +903,9 @@ impl AmbientAgentRunner { // Title (wrapped, single cell) if !task.title.is_empty() { + let title_label = i18n::t("ai.agent_sdk.ambient.label.title"); let title_cell = crate::ai::agent_sdk::text_layout::render_labeled_wrapped_field( - "Title", + &title_label, &task.title, MAX_LINE_WIDTH, ); @@ -849,24 +913,31 @@ impl AmbientAgentRunner { } if let Some(executor) = task.executor_display_name() { - table.add_row(vec![format!("Executed as: {executor}")]); + table + .add_row(vec![i18n::t("ai.agent_sdk.ambient.executed_as") + .replace("{executor}", &executor)]); } // Agent config snapshot (if available) if let Some(config) = task.agent_config_snapshot.as_ref() { let config_str = serde_json::to_string_pretty(config).unwrap_or_else(|_| format!("{config:?}")); - table.add_row(vec![format!("Config:\n{config_str}")]); + table.add_row(vec![ + i18n::t("ai.agent_sdk.ambient.config").replace("{config}", &config_str) + ]); } // Created time let created_formatted = format_approx_duration_from_now_utc(task.created_at); - table.add_row(vec![format!("Created: {}", created_formatted)]); + table.add_row(vec![ + i18n::t("ai.agent_sdk.ambient.created").replace("{time}", &created_formatted) + ]); // Status message (if available) - single multi-line cell if let Some(status_msg) = &task.status_message { + let status_label = i18n::t("ai.agent_sdk.ambient.label.status"); let status_cell = crate::ai::agent_sdk::text_layout::render_labeled_wrapped_field( - "Status", + &status_label, &status_msg.message, MAX_LINE_WIDTH, ); @@ -881,7 +952,8 @@ impl AmbientAgentRunner { // Session link (if available) if let Some(session_join_info) = SessionJoinInfo::from_task(task) { - table.add_row(vec![format!("Session: {}", session_join_info.session_link)]); + table.add_row(vec![i18n::t("ai.agent_sdk.ambient.session") + .replace("{url}", &session_join_info.session_link)]); } println!("{table}"); @@ -890,7 +962,7 @@ impl AmbientAgentRunner { /// Format artifacts for display. fn format_artifacts(artifacts: &[Artifact]) -> String { - let mut lines = vec!["Artifacts:".to_string()]; + let mut lines = vec![i18n::t("ai.agent_sdk.ambient.artifacts")]; for artifact in artifacts { match artifact { @@ -902,25 +974,51 @@ impl AmbientAgentRunner { .. } => { let pr_display = match (repo, number) { - (Some(repo), Some(num)) => format!(" PR: {} #{}", repo, num), - _ => " PR:".to_string(), + (Some(repo), Some(num)) => { + format!( + " {}", + i18n::t("ai.agent_sdk.ambient.artifact.pr_with_repo") + .replace("{repo}", repo) + .replace("{number}", &num.to_string()) + ) + } + _ => format!(" {}", i18n::t("ai.agent_sdk.ambient.artifact.pr")), }; lines.push(pr_display); - lines.push(format!(" Branch: {}", branch)); - lines.push(format!(" Link: {}", url)); + lines.push(format!( + " {}", + i18n::t("ai.agent_sdk.ambient.artifact.branch").replace("{branch}", branch) + )); + lines.push(format!( + " {}", + i18n::t("ai.agent_sdk.ambient.artifact.link").replace("{url}", url) + )); } Artifact::Plan { notebook_uid, title, .. } => { - let plan_title = title.as_deref().unwrap_or("Untitled Plan"); - lines.push(format!(" Plan: {}", plan_title)); + let plan_title = title + .as_deref() + .map(ToString::to_string) + .unwrap_or_else(|| i18n::t("ai.agent_sdk.ambient.artifact.untitled_plan")); + lines.push(format!( + " {}", + i18n::t("ai.agent_sdk.ambient.artifact.plan") + .replace("{title}", &plan_title) + )); if let Some(id) = notebook_uid { lines.push(format!( - " Link: {}/drive/notebook/{}", - ChannelState::server_root_url(), - id + " {}", + i18n::t("ai.agent_sdk.ambient.artifact.link").replace( + "{url}", + &format!( + "{}/drive/notebook/{}", + ChannelState::server_root_url(), + id + ), + ) )); } } @@ -929,8 +1027,16 @@ impl AmbientAgentRunner { description, .. } => { - let desc = description.as_deref().unwrap_or("No description"); - lines.push(format!(" Screenshot: {} ({})", artifact_uid, desc)); + let desc = description + .as_deref() + .map(ToString::to_string) + .unwrap_or_else(|| i18n::t("ai.agent_sdk.ambient.artifact.no_description")); + lines.push(format!( + " {}", + i18n::t("ai.agent_sdk.ambient.artifact.screenshot") + .replace("{uid}", artifact_uid) + .replace("{description}", &desc) + )); } Artifact::File { filename, @@ -939,10 +1045,20 @@ impl AmbientAgentRunner { .. } => { let label = super::super::artifacts::file_button_label(filename, filepath); - lines.push(format!(" File: {}", label)); - lines.push(format!(" Path: {}", filepath)); + lines.push(format!( + " {}", + i18n::t("ai.agent_sdk.ambient.artifact.file").replace("{label}", &label) + )); + lines.push(format!( + " {}", + i18n::t("ai.agent_sdk.ambient.artifact.path").replace("{path}", filepath) + )); if let Some(description) = description { - lines.push(format!(" Description: {}", description)); + lines.push(format!( + " {}", + i18n::t("ai.agent_sdk.ambient.artifact.description") + .replace("{description}", description) + )); } } } @@ -977,9 +1093,9 @@ fn ensure_stream_output_format(output_format: OutputFormat) -> anyhow::Result<() return Ok(()); } - Err(anyhow!( - "Streaming commands require `--output-format ndjson`" - )) + Err(anyhow!(i18n::t( + "ai.agent_sdk.ambient.streaming_requires_ndjson" + ))) } fn stream_retry_backoff(failures: usize) -> Duration { @@ -992,7 +1108,9 @@ fn stream_retry_backoff(failures: usize) -> Duration { fn write_stream_record(record: &T) -> anyhow::Result<()> { let mut stdout = std::io::stdout(); super::output::write_json_line(record, &mut stdout)?; - stdout.flush().context("unable to flush stdout")?; + stdout + .flush() + .context(i18n::t("ai.agent_sdk.ambient.flush_stdout_failed"))?; Ok(()) } @@ -1002,12 +1120,15 @@ fn task_id_from_run_id(run_id: &str) -> Option { fn task_id_from_oz_run_id_env() -> anyhow::Result> { match std::env::var(warp_cli::OZ_RUN_ID_ENV) { - Ok(run_id) => parse_ambient_task_id(&run_id, "Invalid OZ_RUN_ID").map(Some), + Ok(run_id) => { + let prefix = i18n::t("ai.agent_sdk.ambient.invalid_oz_run_id"); + parse_ambient_task_id(&run_id, &prefix).map(Some) + } Err(std::env::VarError::NotPresent) => Ok(None), - Err(std::env::VarError::NotUnicode(_)) => Err(anyhow!( - "{} is set but is not valid Unicode", - warp_cli::OZ_RUN_ID_ENV - )), + Err(std::env::VarError::NotUnicode(_)) => { + Err(anyhow!(i18n::t("ai.agent_sdk.ambient.env_not_unicode") + .replace("{env}", warp_cli::OZ_RUN_ID_ENV))) + } } } @@ -1039,10 +1160,13 @@ impl SendAgentMessageLogContext { } fn error_context(&self) -> String { - format!( - "Failed to send agent message (sender_run_id={:?}, task_id={:?}, target_agent_ids={:?})", - self.sender_run_id, self.task_id, self.target_agent_ids - ) + let sender_run_id = format!("{:?}", self.sender_run_id); + let task_id = format!("{:?}", self.task_id); + let target_agent_ids = format!("{:?}", self.target_agent_ids); + i18n::t("ai.agent_sdk.ambient.message.send_failed_context") + .replace("{sender_run_id}", &sender_run_id) + .replace("{task_id}", &task_id) + .replace("{target_agent_ids}", &target_agent_ids) } fn log_start(&self) { @@ -1109,7 +1233,10 @@ async fn watch_messages_forever( Ok(stream) => { if !initial_connect { eprintln!( - "Reconnected message watch for run {run_id} at sequence {last_seen_sequence}." + "{}", + i18n::t("ai.agent_sdk.ambient.message.watch.reconnected") + .replace("{run_id}", &run_id) + .replace("{sequence}", &last_seen_sequence.to_string()) ); } initial_connect = false; @@ -1118,14 +1245,18 @@ async fn watch_messages_forever( } Err(err) => { if initial_connect { - return Err(err.context("Failed to open agent event stream")); + return Err( + err.context(i18n::t("ai.agent_sdk.ambient.message.watch.open_failed")) + ); } failures += 1; let backoff = stream_retry_backoff(failures); eprintln!( - "Message watch reconnect failed: {err:#}. Retrying in {}s.", - backoff.as_secs() + "{}", + i18n::t("ai.agent_sdk.ambient.message.watch.reconnect_failed") + .replace("{error}", &format!("{err:#}")) + .replace("{seconds}", &backoff.as_secs().to_string()) ); Timer::after(backoff).await; continue; @@ -1139,7 +1270,13 @@ async fn watch_messages_forever( let event = match serde_json::from_str::(&message.data) { Ok(event) => event, Err(err) => { - eprintln!("Skipping malformed agent event payload: {err}"); + eprintln!( + "{}", + i18n::t( + "ai.agent_sdk.ambient.message.watch.malformed_event_payload" + ) + .replace("{error}", &err.to_string()) + ); continue; } }; @@ -1155,8 +1292,9 @@ async fn watch_messages_forever( let Some(message_id) = event.ref_id.clone() else { eprintln!( - "Skipping new_message event without ref_id at sequence {}.", - event.sequence + "{}", + i18n::t("ai.agent_sdk.ambient.message.watch.missing_ref_id") + .replace("{sequence}", &event.sequence.to_string()) ); last_seen_sequence = event.sequence; continue; @@ -1175,8 +1313,11 @@ async fn watch_messages_forever( failures += 1; let backoff = stream_retry_backoff(failures); eprintln!( - "Failed to hydrate message {message_id}: {err:#}. Retrying in {}s.", - backoff.as_secs() + "{}", + i18n::t("ai.agent_sdk.ambient.message.watch.hydrate_failed") + .replace("{message_id}", &message_id) + .replace("{error}", &format!("{err:#}")) + .replace("{seconds}", &backoff.as_secs().to_string()) ); Timer::after(backoff).await; break; @@ -1198,8 +1339,10 @@ async fn watch_messages_forever( failures += 1; let backoff = stream_retry_backoff(failures); eprintln!( - "Message watch disconnected: {err}. Retrying in {}s.", - backoff.as_secs() + "{}", + i18n::t("ai.agent_sdk.ambient.message.watch.disconnected") + .replace("{error}", &err.to_string()) + .replace("{seconds}", &backoff.as_secs().to_string()) ); Timer::after(backoff).await; break; @@ -1208,8 +1351,9 @@ async fn watch_messages_forever( failures += 1; let backoff = stream_retry_backoff(failures); eprintln!( - "Message watch stream closed. Reconnecting in {}s.", - backoff.as_secs() + "{}", + i18n::t("ai.agent_sdk.ambient.message.watch.closed") + .replace("{seconds}", &backoff.as_secs().to_string()) ); Timer::after(backoff).await; break; @@ -1241,11 +1385,16 @@ where OutputFormat::Pretty | OutputFormat::Text => { writeln!( &mut output, - "Sent {} message(s).", - response.message_ids.len() + "{}", + i18n::t("ai.agent_sdk.ambient.message.sent_count") + .replace("{count}", &response.message_ids.len().to_string()) )?; if !response.message_ids.is_empty() { - writeln!(&mut output, "Message IDs:")?; + writeln!( + &mut output, + "{}", + i18n::t("ai.agent_sdk.ambient.message.ids") + )?; for message_id in &response.message_ids { writeln!(&mut output, "- {message_id}")?; } @@ -1275,22 +1424,52 @@ where OutputFormat::Json => super::output::write_json(response, &mut output), OutputFormat::Ndjson => super::output::write_json_line(response, &mut output), OutputFormat::Pretty | OutputFormat::Text => { - writeln!(&mut output, "Message ID: {}", response.message_id)?; - writeln!(&mut output, "From: {}", response.sender_run_id)?; - writeln!(&mut output, "Subject: {}", response.subject)?; - writeln!(&mut output, "Sent At: {}", response.sent_at)?; writeln!( &mut output, - "Delivered At: {}", - format_optional_timestamp(response.delivered_at.as_deref()) + "{}", + i18n::t("ai.agent_sdk.ambient.message.message_id") + .replace("{id}", &response.message_id) )?; writeln!( &mut output, - "Read At: {}", - format_optional_timestamp(response.read_at.as_deref()) + "{}", + i18n::t("ai.agent_sdk.ambient.message.from") + .replace("{from}", &response.sender_run_id) + )?; + writeln!( + &mut output, + "{}", + i18n::t("ai.agent_sdk.ambient.message.subject") + .replace("{subject}", &response.subject) + )?; + writeln!( + &mut output, + "{}", + i18n::t("ai.agent_sdk.ambient.message.sent_at") + .replace("{sent_at}", &response.sent_at) + )?; + writeln!( + &mut output, + "{}", + i18n::t("ai.agent_sdk.ambient.message.delivered_at").replace( + "{delivered_at}", + format_optional_timestamp(response.delivered_at.as_deref()) + ) + )?; + writeln!( + &mut output, + "{}", + i18n::t("ai.agent_sdk.ambient.message.read_at").replace( + "{read_at}", + format_optional_timestamp(response.read_at.as_deref()) + ) )?; writeln!(&mut output)?; - writeln!(&mut output, "Body:")?; + writeln!( + &mut output, + "{}", + i18n::t("ai.agent_sdk.ambient.message.body") + )?; writeln!(&mut output, "{}", response.body)?; Ok(()) } @@ -1322,7 +1501,12 @@ where OutputFormat::Json => super::output::write_json(&result, &mut output), OutputFormat::Ndjson => super::output::write_json_line(&result, &mut output), OutputFormat::Pretty | OutputFormat::Text => { - writeln!(&mut output, "Marked message delivered: {message_id}")?; + writeln!( + &mut output, + "{}", + i18n::t("ai.agent_sdk.ambient.message.marked_delivered") + .replace("{id}", message_id) + )?; Ok(()) } } @@ -1331,12 +1515,12 @@ where impl super::output::TableFormat for AgentMessageHeader { fn header() -> Vec { vec![ - Cell::new("MESSAGE ID"), - Cell::new("FROM"), - Cell::new("SUBJECT"), - Cell::new("SENT AT"), - Cell::new("DELIVERED AT"), - Cell::new("READ AT"), + Cell::new(i18n::t("ai.agent_sdk.ambient.message.table.message_id")), + Cell::new(i18n::t("ai.agent_sdk.ambient.message.table.from")), + Cell::new(i18n::t("ai.agent_sdk.ambient.message.table.subject")), + Cell::new(i18n::t("ai.agent_sdk.ambient.message.table.sent_at")), + Cell::new(i18n::t("ai.agent_sdk.ambient.message.table.delivered_at")), + Cell::new(i18n::t("ai.agent_sdk.ambient.message.table.read_at")), ] } diff --git a/app/src/ai/agent_sdk/api_key.rs b/app/src/ai/agent_sdk/api_key.rs index ed392724a9..921ee4890d 100644 --- a/app/src/ai/agent_sdk/api_key.rs +++ b/app/src/ai/agent_sdk/api_key.rs @@ -113,7 +113,7 @@ impl ApiKeyCommandRunner { )); } GenerateApiKeyResult::Unknown => { - return Err(anyhow!("failed to create API key")) + return Err(anyhow!(i18n::t("ai.agent_sdk.api_key.create_failed"))) } }; print_created_api_key(result, output_format, json_output)?; @@ -168,22 +168,25 @@ impl ApiKeyCommandRunner { if !force { if !io::stdin().is_terminal() { super::report_fatal_error( - anyhow!( - "Refusing to expire API key without confirmation in non-interactive mode (use --force to bypass)" - ), + anyhow!(i18n::t( + "ai.agent_sdk.api_key.expire_refusing_noninteractive" + )), ctx, ); return; } - let prompt = format!("Expire API key '{key}'?"); + let prompt = i18n::t("ai.agent_sdk.api_key.expire_confirm") + .replace("{key}", &key.to_string()); let should_expire = match Confirm::new(&prompt) .with_default(false) - .with_help_message("This action takes effect immediately") + .with_help_message(&i18n::t("ai.agent_sdk.api_key.expire_confirm_help")) .prompt() { Ok(should_expire) => should_expire, - Err(InquireError::OperationCanceled | InquireError::OperationInterrupted) => { + Err( + InquireError::OperationCanceled | InquireError::OperationInterrupted, + ) => { ctx.terminate_app(TerminationMode::ForceTerminate, None); return; } @@ -194,7 +197,7 @@ impl ApiKeyCommandRunner { }; if !should_expire { - println!("Expiration cancelled"); + println!("{}", i18n::t("ai.agent_sdk.api_key.expiration_cancelled")); ctx.terminate_app(TerminationMode::ForceTerminate, None); return; } @@ -213,7 +216,7 @@ impl ApiKeyCommandRunner { )); } ExpireApiKeyResult::Unknown => { - return Err(anyhow!("failed to expire API key")) + return Err(anyhow!(i18n::t("ai.agent_sdk.api_key.expire_failed"))) } }; print_expire_api_key_result( @@ -267,20 +270,27 @@ impl fmt::Display for ApiKeyInfo { let name = &self.name; let uid = &self.uid; let created_at = self.created_at.format("%Y-%m-%d %H:%M:%S UTC"); - write!(f, "{name} ({uid}, created {created_at})") + write!( + f, + "{}", + i18n::t("ai.agent_sdk.api_key.display") + .replace("{name}", name) + .replace("{uid}", uid) + .replace("{created_at}", &created_at.to_string()) + ) } } impl TableFormat for ApiKeyInfo { fn header() -> Vec { vec![ - Cell::new("UID"), - Cell::new("Name"), - Cell::new("Key"), - Cell::new("Scope"), - Cell::new("Created"), - Cell::new("Last Used"), - Cell::new("Expires At"), + Cell::new(i18n::t("ai.agent_sdk.api_key.table.uid")), + Cell::new(i18n::t("ai.agent_sdk.api_key.table.name")), + Cell::new(i18n::t("ai.agent_sdk.api_key.table.key")), + Cell::new(i18n::t("ai.agent_sdk.api_key.table.scope")), + Cell::new(i18n::t("ai.agent_sdk.api_key.table.created")), + Cell::new(i18n::t("ai.agent_sdk.api_key.table.last_used")), + Cell::new(i18n::t("ai.agent_sdk.api_key.table.expires_at")), ] } @@ -294,12 +304,12 @@ impl TableFormat for ApiKeyInfo { Cell::new( self.last_used_at .map(format_approx_duration_from_now_utc) - .unwrap_or_else(|| "Never".to_string()), + .unwrap_or_else(|| i18n::t("ai.agent_sdk.api_key.never")), ), Cell::new( self.expires_at .map(|dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string()) - .unwrap_or_else(|| "Never".to_string()), + .unwrap_or_else(|| i18n::t("ai.agent_sdk.api_key.never")), ), ] } @@ -321,14 +331,16 @@ fn resolve_api_key_identifier( matches.sort_by_key(|key| Reverse(key.created_at)); if matches.is_empty() { - return Err(anyhow!("API key '{key_identifier}' not found")); + return Err(anyhow!( + i18n::t("ai.agent_sdk.api_key.not_found").replace("{key}", key_identifier) + )); } else if matches.len() == 1 { return Ok(Some(matches[0].clone())); } if io::stdin().is_terminal() { return match Select::new( - &format!("Multiple API keys match '{key_identifier}'. Select a key to expire:"), + &i18n::t("ai.agent_sdk.api_key.multiple_select").replace("{key}", key_identifier), matches, ) .prompt() @@ -338,14 +350,18 @@ fn resolve_api_key_identifier( Err(err) => Err(err.into()), }; } - println!("Multiple API keys match '{key_identifier}':"); + println!( + "{}", + i18n::t("ai.agent_sdk.api_key.multiple_matches").replace("{key}", key_identifier) + ); for key in matches { println!(" {key}"); } - Err(anyhow!( - "Multiple API keys match '{key_identifier}'; specify the key by UID" - )) + Err(anyhow!(i18n::t( + "ai.agent_sdk.api_key.multiple_specify_uid" + ) + .replace("{key}", key_identifier))) } #[derive(Debug, Clone, Serialize)] @@ -419,11 +435,13 @@ fn expires_at_from_args(args: ApiKeyExpirationArgs) -> Result> { if let Some(expires_in) = args.expires_in { let duration = chrono::Duration::from_std(expires_in.into()) - .map_err(|_| anyhow!("expiration duration is too large"))?; + .map_err(|_| anyhow!(i18n::t("ai.agent_sdk.api_key.expiration_too_large")))?; return Ok(Some(Time::from(Utc::now() + duration))); } - Err(anyhow!("expiration behavior is required")) + Err(anyhow!(i18n::t( + "ai.agent_sdk.api_key.expiration_behavior_required" + ))) } fn print_created_api_key( @@ -439,10 +457,20 @@ fn print_created_api_key( OutputFormat::Json => output::write_json(&result, std::io::stdout())?, OutputFormat::Ndjson => output::write_json_line(&result, std::io::stdout())?, OutputFormat::Pretty | OutputFormat::Text => { - println!("API key '{}' created.", result.api_key.name); - println!("UID: {}", result.api_key.uid); - println!("Raw API key: {}", result.raw_api_key); - println!("This secret key is shown only once. Store it securely."); + println!( + "{}", + i18n::t("ai.agent_sdk.api_key.created").replace("{name}", &result.api_key.name) + ); + println!( + "{}", + i18n::t("ai.agent_sdk.api_key.uid").replace("{uid}", &result.api_key.uid) + ); + println!( + "{}", + i18n::t("ai.agent_sdk.api_key.raw_api_key") + .replace("{api_key}", &result.raw_api_key) + ); + println!("{}", i18n::t("ai.agent_sdk.api_key.store_securely")); } } Ok(()) @@ -465,9 +493,15 @@ fn print_expire_api_key_result( OutputFormat::Ndjson => output::write_json_line(&result, std::io::stdout())?, OutputFormat::Pretty | OutputFormat::Text => { if expired { - println!("API key '{}' expired.", result.key_uid); + println!( + "{}", + i18n::t("ai.agent_sdk.api_key.expired").replace("{uid}", &result.key_uid) + ); } else { - println!("API key '{}' was not expired.", result.key_uid); + println!( + "{}", + i18n::t("ai.agent_sdk.api_key.not_expired").replace("{uid}", &result.key_uid) + ); } } } diff --git a/app/src/ai/agent_sdk/artifact.rs b/app/src/ai/agent_sdk/artifact.rs index b1e794d5c3..a873c11cb5 100644 --- a/app/src/ai/agent_sdk/artifact.rs +++ b/app/src/ai/agent_sdk/artifact.rs @@ -143,7 +143,7 @@ async fn get_artifact( ai_client .get_artifact_download(artifact_uid) .await - .with_context(|| format!("Failed to get artifact '{artifact_uid}'")) + .with_context(|| i18n::t("ai.agent_sdk.artifact.get_failed").replace("{uid}", artifact_uid)) } async fn download_artifact( @@ -233,37 +233,93 @@ fn write_get_output_to( match output_format { OutputFormat::Json | OutputFormat::Ndjson => { serde_json::to_writer(&mut *output, &output_record) - .context("unable to write JSON output")?; + .context(i18n::t("ai.agent_sdk.output.write_json_failed"))?; writeln!(&mut *output)?; } OutputFormat::Pretty => { - writeln!(&mut *output, "Artifact UID: {}", output_record.artifact_uid)?; writeln!( &mut *output, - "Artifact type: {}", - output_record.artifact_type + "{}", + i18n::t("ai.agent_sdk.artifact.field.artifact_uid") + .replace("{uid}", &output_record.artifact_uid) + )?; + writeln!( + &mut *output, + "{}", + i18n::t("ai.agent_sdk.artifact.field.artifact_type") + .replace("{type}", &output_record.artifact_type) + )?; + writeln!( + &mut *output, + "{}", + i18n::t("ai.agent_sdk.artifact.field.created_at") + .replace("{created_at}", &output_record.created_at) + )?; + writeln!( + &mut *output, + "{}", + i18n::t("ai.agent_sdk.artifact.field.download_url") + .replace("{url}", &output_record.download_url) + )?; + writeln!( + &mut *output, + "{}", + i18n::t("ai.agent_sdk.artifact.field.expires_at") + .replace("{expires_at}", &output_record.expires_at) + )?; + writeln!( + &mut *output, + "{}", + i18n::t("ai.agent_sdk.artifact.field.content_type") + .replace("{content_type}", &output_record.content_type) )?; - writeln!(&mut *output, "Created at: {}", output_record.created_at)?; - writeln!(&mut *output, "Download URL: {}", output_record.download_url)?; - writeln!(&mut *output, "Expires at: {}", output_record.expires_at)?; - writeln!(&mut *output, "Content type: {}", output_record.content_type)?; if let Some(filepath) = output_record.filepath { - writeln!(&mut *output, "Filepath: {filepath}")?; + writeln!( + &mut *output, + "{}", + i18n::t("ai.agent_sdk.artifact.field.filepath") + .replace("{filepath}", &filepath) + )?; } if let Some(filename) = output_record.filename { - writeln!(&mut *output, "Filename: {filename}")?; + writeln!( + &mut *output, + "{}", + i18n::t("ai.agent_sdk.artifact.field.filename") + .replace("{filename}", &filename) + )?; } if let Some(description) = output_record.description { - writeln!(&mut *output, "Description: {description}")?; + writeln!( + &mut *output, + "{}", + i18n::t("ai.agent_sdk.artifact.field.description") + .replace("{description}", &description) + )?; } if let Some(size_bytes) = output_record.size_bytes { - writeln!(&mut *output, "Size bytes: {size_bytes}")?; + writeln!( + &mut *output, + "{}", + i18n::t("ai.agent_sdk.artifact.field.size_bytes") + .replace("{size_bytes}", &size_bytes.to_string()) + )?; } } OutputFormat::Text => { writeln!( &mut *output, - "Artifact UID\tArtifact type\tCreated at\tDownload URL\tExpires at\tContent type\tFilepath\tFilename\tDescription\tSize bytes" + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + i18n::t("ai.agent_sdk.artifact.header.artifact_uid"), + i18n::t("ai.agent_sdk.artifact.header.artifact_type"), + i18n::t("ai.agent_sdk.artifact.header.created_at"), + i18n::t("ai.agent_sdk.artifact.header.download_url"), + i18n::t("ai.agent_sdk.artifact.header.expires_at"), + i18n::t("ai.agent_sdk.artifact.header.content_type"), + i18n::t("ai.agent_sdk.artifact.header.filepath"), + i18n::t("ai.agent_sdk.artifact.header.filename"), + i18n::t("ai.agent_sdk.artifact.header.description"), + i18n::t("ai.agent_sdk.artifact.header.size_bytes") )?; writeln!( &mut *output, @@ -304,21 +360,42 @@ fn write_download_output_to( match output_format { OutputFormat::Json | OutputFormat::Ndjson => { serde_json::to_writer(&mut *output, output_record) - .context("unable to write JSON output")?; + .context(i18n::t("ai.agent_sdk.output.write_json_failed"))?; writeln!(&mut *output)?; } OutputFormat::Pretty => { - writeln!(&mut *output, "Artifact downloaded")?; - writeln!(&mut *output, "Artifact UID: {}", output_record.artifact_uid)?; writeln!( &mut *output, - "Artifact type: {}", - output_record.artifact_type + "{}", + i18n::t("ai.agent_sdk.artifact.downloaded") + )?; + writeln!( + &mut *output, + "{}", + i18n::t("ai.agent_sdk.artifact.field.artifact_uid") + .replace("{uid}", &output_record.artifact_uid) + )?; + writeln!( + &mut *output, + "{}", + i18n::t("ai.agent_sdk.artifact.field.artifact_type") + .replace("{type}", &output_record.artifact_type) + )?; + writeln!( + &mut *output, + "{}", + i18n::t("ai.agent_sdk.artifact.field.path") + .replace("{path}", &output_record.path.display().to_string()) )?; - writeln!(&mut *output, "Path: {}", output_record.path.display())?; } OutputFormat::Text => { - writeln!(&mut *output, "Artifact UID\tArtifact type\tPath")?; + writeln!( + &mut *output, + "{}\t{}\t{}", + i18n::t("ai.agent_sdk.artifact.header.artifact_uid"), + i18n::t("ai.agent_sdk.artifact.header.artifact_type"), + i18n::t("ai.agent_sdk.artifact.header.path") + )?; writeln!( &mut *output, "{}\t{}\t{}", @@ -356,32 +433,61 @@ fn write_upload_output_to( match output_format { OutputFormat::Json | OutputFormat::Ndjson => { serde_json::to_writer(&mut *output, &output_record) - .context("unable to write JSON output")?; + .context(i18n::t("ai.agent_sdk.output.write_json_failed"))?; writeln!(&mut *output)?; } OutputFormat::Pretty => { - writeln!(&mut *output, "Artifact uploaded")?; - writeln!(&mut *output, "Artifact UID: {}", output_record.artifact_uid)?; - writeln!(&mut *output, "Filepath: {}", output_record.filepath)?; writeln!( &mut *output, - "Description: {}", - output_record.description.as_deref().unwrap_or("") + "{}", + i18n::t("ai.agent_sdk.artifact.uploaded") )?; - writeln!(&mut *output, "MIME type: {}", output_record.mime_type)?; writeln!( &mut *output, - "Size bytes: {}", - output_record - .size_bytes - .map(|size| size.to_string()) - .unwrap_or_default() + "{}", + i18n::t("ai.agent_sdk.artifact.field.artifact_uid") + .replace("{uid}", &output_record.artifact_uid) + )?; + writeln!( + &mut *output, + "{}", + i18n::t("ai.agent_sdk.artifact.field.filepath") + .replace("{filepath}", &output_record.filepath) + )?; + writeln!( + &mut *output, + "{}", + i18n::t("ai.agent_sdk.artifact.field.description").replace( + "{description}", + output_record.description.as_deref().unwrap_or("") + ) + )?; + writeln!( + &mut *output, + "{}", + i18n::t("ai.agent_sdk.artifact.field.mime_type") + .replace("{mime_type}", &output_record.mime_type) + )?; + let size_bytes = output_record + .size_bytes + .map(|size| size.to_string()) + .unwrap_or_default(); + writeln!( + &mut *output, + "{}", + i18n::t("ai.agent_sdk.artifact.field.size_bytes") + .replace("{size_bytes}", &size_bytes) )?; } OutputFormat::Text => { writeln!( &mut *output, - "Artifact UID\tFilepath\tDescription\tMIME type\tSize bytes" + "{}\t{}\t{}\t{}\t{}", + i18n::t("ai.agent_sdk.artifact.header.artifact_uid"), + i18n::t("ai.agent_sdk.artifact.header.filepath"), + i18n::t("ai.agent_sdk.artifact.header.description"), + i18n::t("ai.agent_sdk.artifact.header.mime_type"), + i18n::t("ai.agent_sdk.artifact.header.size_bytes") )?; writeln!( &mut *output, diff --git a/app/src/ai/agent_sdk/artifact_tests.rs b/app/src/ai/agent_sdk/artifact_tests.rs index f8dc8e7c52..1672da1b78 100644 --- a/app/src/ai/agent_sdk/artifact_tests.rs +++ b/app/src/ai/agent_sdk/artifact_tests.rs @@ -4,6 +4,10 @@ use warp_cli::agent::OutputFormat; use super::*; +fn use_english_locale() { + i18n::set_locale("en"); +} + fn sample_completed_upload() -> CompletedFileArtifactUpload { CompletedFileArtifactUpload { artifact: sample_artifact_record(), @@ -94,6 +98,8 @@ fn write_get_output_to_writes_ndjson_output() { #[test] fn write_get_output_to_writes_pretty_output() { + use_english_locale(); + let mut output = Vec::new(); write_get_output_to( @@ -111,6 +117,8 @@ fn write_get_output_to_writes_pretty_output() { #[test] fn write_get_output_to_writes_text_output() { + use_english_locale(); + let mut output = Vec::new(); write_get_output_to( @@ -128,6 +136,8 @@ fn write_get_output_to_writes_text_output() { #[test] fn write_download_output_to_writes_pretty_output() { + use_english_locale(); + let artifact = sample_file_download_response(); let path = std::path::absolute("report.txt").unwrap(); let output_record = DownloadArtifactOutput::new(&artifact, path.clone()); @@ -228,6 +238,8 @@ fn write_upload_output_to_writes_ndjson_output() { #[test] fn write_upload_output_to_writes_pretty_output() { + use_english_locale(); + let mut output = Vec::new(); write_upload_output_to( @@ -245,6 +257,8 @@ fn write_upload_output_to_writes_pretty_output() { #[test] fn write_upload_output_to_writes_text_output() { + use_english_locale(); + let mut output = Vec::new(); write_upload_output_to(&mut output, &sample_completed_upload(), OutputFormat::Text).unwrap(); diff --git a/app/src/ai/agent_sdk/artifact_upload.rs b/app/src/ai/agent_sdk/artifact_upload.rs index 6fa7cdebbc..8acdc52b05 100644 --- a/app/src/ai/agent_sdk/artifact_upload.rs +++ b/app/src/ai/agent_sdk/artifact_upload.rs @@ -36,7 +36,10 @@ impl TryFrom for FileArtifactUploadRequest { fn try_from(value: UploadArtifactArgs) -> Result { let run_id = match value.run_id { - Some(run_id) => Some(parse_run_id(&run_id, "Invalid run ID")?), + Some(run_id) => Some(parse_run_id( + &run_id, + &i18n::t("ai.agent_sdk.common.invalid_run_id"), + )?), None => None, }; @@ -122,8 +125,9 @@ impl FileArtifactUploader { let uploaded_artifact = self .confirm_upload(create_response.artifact.artifact_uid, checksum) .await?; - let size_bytes = i64::try_from(artifact.file_size) - .context("Artifact file size exceeds supported range")?; + let size_bytes = i64::try_from(artifact.file_size).context(i18n::t( + "ai.agent_sdk.artifact_upload.file_size_out_of_range", + ))?; Ok(CompletedFileArtifactUpload { artifact: uploaded_artifact, @@ -154,7 +158,7 @@ impl FileArtifactUploader { size_bytes: artifact.graphql_size_bytes(), }) .await - .context("Failed to create file artifact upload target") + .context(i18n::t("ai.agent_sdk.artifact_upload.create_target_failed")) } async fn upload_artifact_bytes( @@ -178,7 +182,7 @@ impl FileArtifactUploader { self.ai_client .confirm_file_artifact_upload(artifact_uid, checksum) .await - .context("Failed to confirm file artifact upload") + .context(i18n::t("ai.agent_sdk.artifact_upload.confirm_failed")) } pub(crate) async fn resolve_upload_association( @@ -211,18 +215,14 @@ impl FileArtifactUploader { .list_ai_conversation_metadata(Some(vec![conversation_id.as_str().to_string()])) .await .with_context(|| { - format!( - "Failed to load conversation '{}' to resolve artifact upload headers", - conversation_id.as_str() - ) + i18n::t("ai.agent_sdk.artifact_upload.load_conversation_failed") + .replace("{conversation_id}", conversation_id.as_str()) })?; let metadata = single_conversation_metadata(conversation_id.as_str(), metadata) .with_context(|| { - format!( - "Failed to load conversation '{}' to resolve artifact upload headers", - conversation_id.as_str() - ) + i18n::t("ai.agent_sdk.artifact_upload.load_conversation_failed") + .replace("{conversation_id}", conversation_id.as_str()) })?; ambient_task_id_from_conversation_metadata(conversation_id.as_str(), metadata) @@ -234,16 +234,21 @@ fn normalize_artifact_filepath(path: &Path) -> String { } fn file_size_and_prefix_for_path(path: &Path, max_bytes: usize) -> Result<(u64, Vec)> { - let mut file = File::open(path) - .with_context(|| format!("Failed to open artifact file '{}'", path.display()))?; + let path_display = path.display().to_string(); + let mut file = File::open(path).with_context(|| { + i18n::t("ai.agent_sdk.artifact_upload.open_file_failed").replace("{path}", &path_display) + })?; let file_size = file .metadata() - .with_context(|| format!("Failed to stat artifact file '{}'", path.display()))? + .with_context(|| { + i18n::t("ai.agent_sdk.artifact_upload.stat_file_failed") + .replace("{path}", &path_display) + })? .len(); let mut bytes = vec![0; max_bytes]; - let bytes_read = file - .read(&mut bytes) - .with_context(|| format!("Failed to read artifact file '{}'", path.display()))?; + let bytes_read = file.read(&mut bytes).with_context(|| { + i18n::t("ai.agent_sdk.artifact_upload.read_file_failed").replace("{path}", &path_display) + })?; bytes.truncate(bytes_read); Ok((file_size, bytes)) } @@ -269,9 +274,16 @@ fn single_conversation_metadata( mut metadata: Vec, ) -> Result { match metadata.len() { - 0 => bail!("Conversation not found"), + 0 => bail!( + "{}", + i18n::t("ai.agent_sdk.artifact_upload.conversation_not_found") + ), 1 => Ok(metadata.pop().expect("metadata length checked")), - _ => bail!("Multiple conversations found for '{conversation_id}'"), + _ => bail!( + "{}", + i18n::t("ai.agent_sdk.artifact_upload.multiple_conversations_found") + .replace("{conversation_id}", conversation_id) + ), } } @@ -280,7 +292,11 @@ fn ambient_task_id_from_conversation_metadata( metadata: ServerAIConversationMetadata, ) -> Result { metadata.ambient_agent_task_id.ok_or_else(|| { - anyhow!("Conversation '{conversation_id}' is not backed by a cloud agent task") + anyhow!( + "{}", + i18n::t("ai.agent_sdk.artifact_upload.conversation_missing_cloud_task") + .replace("{conversation_id}", conversation_id) + ) }) } @@ -293,17 +309,25 @@ fn load_env_run_id() -> Result> { Ok(run_id) => Ok(Some(run_id)), Err(env::VarError::NotPresent) => Ok(None), Err(env::VarError::NotUnicode(_)) => Err(anyhow!( - "{OZ_RUN_ID_ENV_VAR} is set but is not valid Unicode" + "{}", + i18n::t("ai.agent_sdk.artifact_upload.env_var_not_unicode") + .replace("{env_var}", OZ_RUN_ID_ENV_VAR) )), } } fn resolve_env_run_id(env_run_id: Option) -> Result { let Some(run_id) = env_run_id else { - bail!("{OZ_RUN_ID_ENV_VAR} is not set"); + bail!( + "{}", + i18n::t("ai.agent_sdk.artifact_upload.env_var_not_set") + .replace("{env_var}", OZ_RUN_ID_ENV_VAR) + ); }; - parse_run_id(&run_id, "Invalid OZ_RUN_ID") + let prefix = i18n::t("ai.agent_sdk.artifact_upload.invalid_env_run_id") + .replace("{env_var}", OZ_RUN_ID_ENV_VAR); + parse_run_id(&run_id, &prefix) } fn resolve_upload_association_from_sources( @@ -327,9 +351,12 @@ fn resolve_upload_association_from_sources( } if let Some(conversation_id) = explicit_conversation_id { - match conversation_task_id - .ok_or_else(|| anyhow!("conversation resolution should be provided"))? - { + match conversation_task_id.ok_or_else(|| { + anyhow!( + "{}", + i18n::t("ai.agent_sdk.artifact_upload.conversation_resolution_required") + ) + })? { Ok(ambient_task_id) => { return Ok(ResolvedUploadAssociation { conversation_id: Some(conversation_id), @@ -354,8 +381,14 @@ fn resolve_upload_association_from_sources( }; return Err(anyhow!( - "Failed to resolve artifact upload association for conversation '{}': {conversation_err}; also failed to use {OZ_RUN_ID_ENV_VAR}: {env_err}", - conversation_id.as_str() + "{}", + i18n::t( + "ai.agent_sdk.artifact_upload.resolve_association_for_conversation_failed" + ) + .replace("{conversation_id}", conversation_id.as_str()) + .replace("{conversation_err}", &conversation_err.to_string()) + .replace("{env_var}", OZ_RUN_ID_ENV_VAR) + .replace("{env_err}", &env_err.to_string()) )); } } @@ -363,7 +396,10 @@ fn resolve_upload_association_from_sources( let ambient_task_id = resolve_env_run_id(env_run_id).map_err(|env_err| { anyhow!( - "Failed to resolve artifact upload association: no usable --run-id or --conversation-id was provided, and {OZ_RUN_ID_ENV_VAR}: {env_err}" + "{}", + i18n::t("ai.agent_sdk.artifact_upload.resolve_association_failed") + .replace("{env_var}", OZ_RUN_ID_ENV_VAR) + .replace("{env_err}", &env_err.to_string()) ) })?; diff --git a/app/src/ai/agent_sdk/artifact_upload_tests.rs b/app/src/ai/agent_sdk/artifact_upload_tests.rs index 7e57507195..cd0afabe19 100644 --- a/app/src/ai/agent_sdk/artifact_upload_tests.rs +++ b/app/src/ai/agent_sdk/artifact_upload_tests.rs @@ -12,6 +12,10 @@ use crate::cloud_object::{Revision, ServerMetadata, ServerPermissions}; use crate::persistence::model::ConversationUsageMetadata; use crate::server::ids::ServerId; +fn use_english_locale() { + i18n::set_locale("en"); +} + fn create_mock_server_metadata() -> ServerMetadata { ServerMetadata { uid: ServerId::default(), @@ -117,6 +121,8 @@ fn single_conversation_metadata_returns_the_only_metadata_record() { #[test] fn single_conversation_metadata_errors_when_no_metadata_is_returned() { + use_english_locale(); + let err = single_conversation_metadata("conversation-123", Vec::new()).unwrap_err(); assert!(err.to_string().contains("Conversation not found")); @@ -124,6 +130,8 @@ fn single_conversation_metadata_errors_when_no_metadata_is_returned() { #[test] fn ambient_task_id_from_conversation_metadata_requires_cloud_task_metadata() { + use_english_locale(); + let err = ambient_task_id_from_conversation_metadata( "conversation-123", create_conversation_metadata("conversation-123", None), @@ -157,6 +165,8 @@ fn explicit_run_id_wins_over_env_fallback() { #[test] fn invalid_explicit_run_id_errors_even_if_env_fallback_exists() { + use_english_locale(); + let err = FileArtifactUploadRequest::try_from(UploadArtifactArgs { path: PathBuf::from("outputs/report.txt"), run_id: Some("not-a-run-id".to_string()), @@ -232,6 +242,8 @@ fn missing_args_fall_back_to_env_run_id_for_request_association() { #[test] fn missing_args_and_missing_env_return_clear_error() { + use_english_locale(); + let err = resolve_upload_association_from_sources(None, None, None, None).unwrap_err(); assert!(err @@ -242,6 +254,8 @@ fn missing_args_and_missing_env_return_clear_error() { #[test] fn invalid_env_run_id_returns_clear_error() { + use_english_locale(); + let err = resolve_upload_association_from_sources(None, None, None, Some("not-a-run-id".to_string())) .unwrap_err(); diff --git a/app/src/ai/agent_sdk/common.rs b/app/src/ai/agent_sdk/common.rs index 9cc5fc0dde..49df80828c 100644 --- a/app/src/ai/agent_sdk/common.rs +++ b/app/src/ai/agent_sdk/common.rs @@ -49,9 +49,11 @@ pub fn validate_agent_mode_base_model_id( .map(|id| id.to_string()) .collect::>() .join(", "); - Err(anyhow::anyhow!( - "Unknown model id '{model_id}'. Try one of: {suggestions}" - )) + Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.common.unknown_model_id" + ) + .replace("{model_id}", model_id) + .replace("{suggestions}", &suggestions))) } } @@ -59,16 +61,18 @@ pub(super) fn parse_ambient_task_id( run_id: &str, error_prefix: &str, ) -> anyhow::Result { - run_id - .parse() - .map_err(|err| anyhow::anyhow!("{error_prefix} '{run_id}': {err}")) + run_id.parse().map_err(|err| { + let message = format!("{error_prefix} '{run_id}': {err}"); + anyhow::anyhow!(message) + }) } pub(super) fn set_ambient_task_context_from_run_id( ctx: &AppContext, run_id: &str, ) -> anyhow::Result { - let task_id = parse_ambient_task_id(run_id, "Invalid run ID")?; + let prefix = i18n::t("ai.agent_sdk.common.invalid_run_id"); + let task_id = parse_ambient_task_id(run_id, &prefix)?; ServerApiProvider::handle(ctx) .as_ref(ctx) .get() @@ -85,7 +89,7 @@ pub fn resolve_owner(team_flag: bool, user_flag: bool, ctx: &AppContext) -> anyh if team_flag { let team_id = UserWorkspaces::as_ref(ctx) .current_team_uid() - .ok_or_else(|| anyhow::anyhow!("User is not on a team"))?; + .ok_or_else(|| anyhow::anyhow!(i18n::t("ai.agent_sdk.common.user_not_on_team")))?; return Ok(Owner::Team { team_uid: team_id }); } @@ -93,7 +97,9 @@ pub fn resolve_owner(team_flag: bool, user_flag: bool, ctx: &AppContext) -> anyh let user_id = AuthStateProvider::as_ref(ctx) .get() .user_id() - .ok_or_else(|| anyhow::anyhow!("User should be logged in"))?; + .ok_or_else(|| { + anyhow::anyhow!(i18n::t("ai.agent_sdk.common.user_should_be_logged_in")) + })?; return Ok(Owner::User { user_uid: user_id }); } @@ -106,7 +112,7 @@ pub fn resolve_owner(team_flag: bool, user_flag: bool, ctx: &AppContext) -> anyh let user_id = AuthStateProvider::as_ref(ctx) .get() .user_id() - .ok_or_else(|| anyhow::anyhow!("User should be logged in"))?; + .ok_or_else(|| anyhow::anyhow!(i18n::t("ai.agent_sdk.common.user_should_be_logged_in")))?; Ok(Owner::User { user_uid: user_id }) } @@ -129,7 +135,7 @@ where async move { let _ = refresh_future .await - .map_err(|_| anyhow::anyhow!("Timed out refreshing team metadata"))?; + .map_err(|_| anyhow::anyhow!(i18n::t("ai.agent_sdk.common.team_metadata_timeout")))?; Ok(()) } } @@ -141,7 +147,7 @@ pub fn refresh_warp_drive( UpdateManager::as_ref(ctx) .initial_load_complete() .with_timeout(WARP_DRIVE_SYNC_TIMEOUT) - .map_err(|_| anyhow::anyhow!("Timed out waiting for Warp Drive to sync")) + .map_err(|_| anyhow::anyhow!(i18n::t("ai.agent_sdk.common.warp_drive_sync_timeout"))) } /// Fetch the conversation's server metadata and validate that its harness matches the caller's @@ -164,9 +170,10 @@ pub(super) async fn fetch_and_validate_conversation_harness( .into_iter() .next() .ok_or_else(|| { - AgentDriverError::ConversationLoadFailed(format!( - "conversation {conversation_id} not found or not accessible" - )) + AgentDriverError::ConversationLoadFailed( + i18n::t("ai.agent_sdk.common.conversation_not_found_or_inaccessible") + .replace("{conversation_id}", conversation_id), + ) })?; if metadata.harness != args_harness { @@ -189,20 +196,71 @@ pub fn format_owner(owner: &Owner) -> &'static str { } } +/// Format an object owner for localized display in the CLI. +pub fn localized_format_owner(owner: &Owner) -> String { + match owner { + Owner::User { .. } => i18n::t("ai.agent_sdk.common.owner.personal"), + Owner::Team { .. } => i18n::t("ai.agent_sdk.common.owner.team"), + } +} + /// An error resolving an agent option, which we may have prompted the user for. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] pub enum ResolveConfigurationError { /// The user canceled the operation, and we should exit. - #[error("Operation canceled")] Canceled, - #[error("{id} is not a valid {kind} identifier")] - InvalidId { id: String, kind: &'static str }, - #[error("{kind} {id} not found")] - ObjectNotFound { id: String, kind: &'static str }, - #[error(transparent)] + InvalidId { + id: String, + kind: &'static str, + }, + ObjectNotFound { + id: String, + kind: &'static str, + }, Other(anyhow::Error), } +impl fmt::Display for ResolveConfigurationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ResolveConfigurationError::Canceled => { + write!(f, "{}", i18n::t("ai.agent_sdk.common.operation_cancelled")) + } + ResolveConfigurationError::InvalidId { id, kind } => write!( + f, + "{}", + i18n::t("ai.agent_sdk.common.invalid_id") + .replace("{id}", id) + .replace("{kind}", &localized_object_kind(kind)) + ), + ResolveConfigurationError::ObjectNotFound { id, kind } => write!( + f, + "{}", + i18n::t("ai.agent_sdk.common.object_not_found") + .replace("{kind}", &localized_object_kind(kind)) + .replace("{id}", id) + ), + ResolveConfigurationError::Other(err) => write!(f, "{err}"), + } + } +} + +impl std::error::Error for ResolveConfigurationError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + ResolveConfigurationError::Other(err) => Some(err.as_ref()), + _ => None, + } + } +} + +fn localized_object_kind(kind: &str) -> String { + match kind { + "environment" => i18n::t("ai.agent_sdk.common.object_kind.environment"), + _ => kind.to_string(), + } +} + #[derive(Clone, Debug, PartialEq)] pub enum EnvironmentChoice { /// The user explicitly chose not to use an environment. @@ -253,26 +311,25 @@ impl EnvironmentChoice { // If there are no synced environments, require the user to create one or use --no-environment. if options.len() == 1 { let cli_name = warp_cli::binary_name().unwrap_or_else(|| "warp".to_string()); - return Err(ResolveConfigurationError::Other(anyhow::anyhow!( - "No environments are configured for this account.\n\ -You can create an environment with `{cli_name} environment create`.\n\ -Or, re-run this command with `--no-environment` to not use an environment.\n\ -Without an environment, the agent will not be able to access private repositories or create pull requests.", - ))); + return Err(ResolveConfigurationError::Other(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.common.no_environments_configured" + ) + .replace("{cli_name}", &cli_name)))); } - let prompt = "Select an environment to run the agent in (or 'No environment'):"; + let prompt = i18n::t("ai.agent_sdk.common.select_environment_prompt"); - let choice = Select::new(prompt, options).prompt(); + let choice = Select::new(&prompt, options).prompt(); match choice { Ok(choice) => Ok(choice), Err(InquireError::OperationCanceled | InquireError::OperationInterrupted) => { Err(ResolveConfigurationError::Canceled) } - Err(err) => Err(ResolveConfigurationError::Other(anyhow::anyhow!( - "Error selecting environment: {err}" - ))), + Err(err) => Err(ResolveConfigurationError::Other(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.common.select_environment_error" + ) + .replace("{error}", &err.to_string())))), } } } @@ -321,7 +378,8 @@ impl fmt::Display for EnvironmentChoice { match self { EnvironmentChoice::None => write!( f, - "No environment (agent will not be able to access private repositories or create pull requests)", + "{}", + i18n::t("ai.agent_sdk.common.no_environment_choice"), ), EnvironmentChoice::Environment { id, name } => write!(f, "{name} ({id})"), } diff --git a/app/src/ai/agent_sdk/config_file.rs b/app/src/ai/agent_sdk/config_file.rs index 472d474908..3359993cb0 100644 --- a/app/src/ai/agent_sdk/config_file.rs +++ b/app/src/ai/agent_sdk/config_file.rs @@ -44,8 +44,10 @@ pub struct LoadedAgentConfigSnapshotFile { /// - otherwise: try JSON, then YAML #[cfg(not(target_family = "wasm"))] pub fn load_config_file(path: &Path) -> anyhow::Result { - let contents = std::fs::read_to_string(path) - .with_context(|| format!("Failed to read config file '{}'", path.display()))?; + let path_display = path.display().to_string(); + let contents = std::fs::read_to_string(path).with_context(|| { + i18n::t("ai.agent_sdk.config_file.read_failed").replace("{path}", &path_display) + })?; let ext = path .extension() @@ -53,23 +55,24 @@ pub fn load_config_file(path: &Path) -> anyhow::Result parse_json(&contents) - .with_context(|| format!("Invalid JSON in config file '{}'", path.display()))?, - Some("yml") | Some("yaml") => parse_yaml(&contents) - .with_context(|| format!("Invalid YAML in config file '{}'", path.display()))?, + Some("json") => parse_json(&contents).with_context(|| { + i18n::t("ai.agent_sdk.config_file.invalid_json").replace("{path}", &path_display) + })?, + Some("yml") | Some("yaml") => parse_yaml(&contents).with_context(|| { + i18n::t("ai.agent_sdk.config_file.invalid_yaml").replace("{path}", &path_display) + })?, _ => parse_json(&contents) .or_else(|_| parse_yaml(&contents)) .with_context(|| { - format!( - "Failed to parse config file '{}' as JSON or YAML", - path.display() - ) + i18n::t("ai.agent_sdk.config_file.parse_json_or_yaml_failed") + .replace("{path}", &path_display) })?, }; if let Some(mcp_servers) = &file.mcp_servers { - super::mcp_config::validate_mcp_servers(mcp_servers) - .with_context(|| format!("Invalid mcp_servers in '{}'", path.display()))?; + super::mcp_config::validate_mcp_servers(mcp_servers).with_context(|| { + i18n::t("ai.agent_sdk.config_file.invalid_mcp_servers").replace("{path}", &path_display) + })?; } Ok(LoadedAgentConfigSnapshotFile { file }) @@ -79,7 +82,8 @@ pub fn load_config_file(path: &Path) -> anyhow::Result anyhow::Result { Err(anyhow::anyhow!( - "Config files are not supported in WASM builds" + "{}", + i18n::t("ai.agent_sdk.config_file.wasm_unsupported") )) } @@ -93,7 +97,7 @@ fn parse_yaml(input: &str) -> anyhow::Result { } fn supported_keys_context() -> String { - "Supported keys: name, environment_id, model_id, base_prompt, mcp_servers, host, computer_use_enabled".to_string() + i18n::t("ai.agent_sdk.config_file.supported_keys") } /// Convert an unwrapped `mcp_servers` map into runtime MCP specs for AgentDriver. @@ -108,13 +112,21 @@ pub fn mcp_specs_from_mcp_servers( let mut json_map: Map = Map::new(); for (name, config) in mcp_servers { - let obj = config - .as_object() - .ok_or_else(|| anyhow::anyhow!("MCP server '{name}' config must be a JSON object"))?; + let obj = config.as_object().ok_or_else(|| { + anyhow::anyhow!( + "{}", + i18n::t("ai.agent_sdk.mcp_config.config_must_be_object") + .replace("{server_name}", name) + ) + })?; if let Some(warp_id) = obj.get("warp_id").and_then(Value::as_str) { let uuid = uuid::Uuid::parse_str(warp_id).map_err(|_| { - anyhow::anyhow!("MCP server '{name}' field 'warp_id' must be a UUID") + anyhow::anyhow!( + "{}", + i18n::t("ai.agent_sdk.mcp_config.warp_id_must_be_uuid") + .replace("{server_name}", name) + ) })?; uuids.push(uuid); } else { @@ -128,8 +140,8 @@ pub fn mcp_specs_from_mcp_servers( let mut specs: Vec = uuids.into_iter().map(MCPSpec::Uuid).collect(); if !json_map.is_empty() { - let json = - serde_json::to_string(&json_map).context("Failed to serialize MCP server map")?; + let json = serde_json::to_string(&json_map) + .context(i18n::t("ai.agent_sdk.config_file.serialize_mcp_map_failed"))?; specs.push(MCPSpec::Json(json)); } diff --git a/app/src/ai/agent_sdk/config_file_tests.rs b/app/src/ai/agent_sdk/config_file_tests.rs index 659cfdf114..eacdf3f4d9 100644 --- a/app/src/ai/agent_sdk/config_file_tests.rs +++ b/app/src/ai/agent_sdk/config_file_tests.rs @@ -7,6 +7,10 @@ use warp_cli::mcp::MCPSpec; use crate::ai::ambient_agents::AgentConfigSnapshot; +fn use_english_locale() { + i18n::set_locale("en"); +} + fn write_temp(suffix: &str, contents: &str) -> tempfile::NamedTempFile { let mut file = tempfile::Builder::new().suffix(suffix).tempfile().unwrap(); file.write_all(contents.as_bytes()).unwrap(); @@ -54,6 +58,8 @@ mcp_servers: #[test] fn unknown_keys_are_rejected() { + use_english_locale(); + let contents = json!({ "model_id": "gpt-4o", "typo_model": "oops" @@ -68,6 +74,8 @@ fn unknown_keys_are_rejected() { #[test] fn mcp_must_be_under_mcp_servers_key() { + use_english_locale(); + let contents = json!({ "model_id": "gpt-4o", "mcpServers": { "s": { "command": "npx", "args": [] } } diff --git a/app/src/ai/agent_sdk/driver.rs b/app/src/ai/agent_sdk/driver.rs index d30c74dcc1..186645e5ec 100644 --- a/app/src/ai/agent_sdk/driver.rs +++ b/app/src/ai/agent_sdk/driver.rs @@ -1473,13 +1473,18 @@ impl AgentDriver { }; let error = match repo_metadata.as_ref(ctx).repository_state(&id, ctx) { Some(IndexedRepoState::Indexed(_)) => None, - Some(IndexedRepoState::Pending(_)) => Some(format!( - "Repository indexing is still pending: {repo_id_path}" - )), - Some(IndexedRepoState::Failed(error)) => { - Some(format!("Repository indexing failed: {error}")) - } - None => Some(format!("Repository not found: {repo_id_path}")), + Some(IndexedRepoState::Pending(_)) => Some( + i18n::t("ai.agent_sdk.driver.repository_indexing_pending") + .replace("{repo_path}", &repo_id_path.to_string()), + ), + Some(IndexedRepoState::Failed(error)) => Some( + i18n::t("ai.agent_sdk.driver.repository_indexing_failed") + .replace("{error}", &error.to_string()), + ), + None => Some( + i18n::t("ai.agent_sdk.driver.repository_not_found") + .replace("{repo_path}", &repo_id_path.to_string()), + ), }; Some((repo_path, error)) }) @@ -2284,14 +2289,14 @@ impl AgentDriver { report_if_error!(runner .save_conversation(SavePoint::Periodic, foreground) .await - .context("Failed to save harness conversation (periodic)")); + .context(i18n::t("ai.agent_sdk.driver.save_harness_conversation_periodic_failed"))); } _ = harness_exit_rx => { log::debug!("Requesting harness exit"); report_if_error!(runner .exit(foreground) .await - .context("Failed to exit harness")); + .context(i18n::t("ai.agent_sdk.driver.exit_harness_failed"))); } detected = scanner_fut => { if let Some(error) = detected { @@ -2338,9 +2343,9 @@ impl AgentDriver { report_if_error!(runner .exit(foreground) .await - .context( - "Failed to exit harness after runtime failure detection", - )); + .context(i18n::t( + "ai.agent_sdk.driver.exit_harness_after_runtime_failure_failed", + ))); detected_runtime_failure = Some(error); } } @@ -2356,8 +2361,9 @@ impl AgentDriver { let final_save_succeeded = match runner .save_conversation(SavePoint::Final, foreground) .await - .context("Failed to save harness conversation (final)") - { + .context(i18n::t( + "ai.agent_sdk.driver.save_harness_conversation_final_failed", + )) { Ok(()) => true, Err(err) => { report_error!(err); @@ -2375,7 +2381,9 @@ impl AgentDriver { if let Err(err) = runner .cleanup(cleanup_disposition, foreground) .await - .context("Failed to clean up harness runtime state") + .context(i18n::t( + "ai.agent_sdk.driver.cleanup_harness_runtime_state_failed", + )) { report_error!(err); } @@ -2516,7 +2524,7 @@ impl AgentDriver { // When a new exchange is appended, we should already have its inputs available. report_if_error!(me .write_exchange_inputs(exchange) - .context("Failed to write exchange inputs")); + .context(i18n::t("ai.agent_sdk.driver.write_exchange_inputs_failed"))); // Forward any successful file-edit paths from this exchange's inputs to the // snapshot declarations writer so the end-of-run upload covers files written @@ -2569,7 +2577,7 @@ impl AgentDriver { report_if_error!(output::with_stdout_buffered(|buf| match me.output_format { OutputFormat::Json | OutputFormat::Ndjson => output::json::conversation_started(&token, buf), OutputFormat::Text | OutputFormat::Pretty => output::text::conversation_started(&token, buf), - }).context("Failed to write conversation ID")); + }).context(i18n::t("ai.agent_sdk.driver.write_conversation_id_failed"))); written_conversation_id = true; // Store the server conversation token and record that we should update the task @@ -2587,7 +2595,7 @@ impl AgentDriver { if exchange.output_status.is_finished() { report_if_error!(me .write_exchange_output(exchange) - .context("Failed to write exchange output")); + .context(i18n::t("ai.agent_sdk.driver.write_exchange_output_failed"))); } // Perform task update after all immutable borrows end @@ -2774,7 +2782,7 @@ impl AgentDriver { } } }) - .context("Failed to write artifact_created")); + .context(i18n::t("ai.agent_sdk.driver.write_artifact_created_failed"))); }); // Submit the AI query. @@ -2950,12 +2958,16 @@ impl AgentDriver { report_if_error!(runner .handle_session_update(&spawner) .await - .context("Failed to update harness state from CLI session event")); + .context(i18n::t( + "ai.agent_sdk.driver.update_harness_state_from_cli_session_event_failed", + ))); log::debug!("Triggering post-turn save of harness conversation data"); report_if_error!(runner .save_conversation(SavePoint::PostTurn, &spawner) .await - .context("Failed to save harness conversation (post-turn)")); + .context(i18n::t( + "ai.agent_sdk.driver.save_harness_conversation_post_turn_failed", + ))); }, |_, _, _| {}, ); @@ -2975,9 +2987,7 @@ impl AgentDriver { ) { match event { TerminalDriverEvent::SlowBootstrap => { - eprintln!( - "Warning: Terminal session is slow to bootstrap. See https://docs.warp.dev/support-and-community/troubleshooting-and-support/known-issues#shells to troubleshoot." - ); + eprintln!("{}", i18n::t("ai.agent_sdk.driver.slow_bootstrap_warning")); } TerminalDriverEvent::EstablishedSharedSession { session_id, @@ -2994,7 +3004,9 @@ impl AgentDriver { report_if_error!(server_api .update_agent_task(task_id, None, Some(session_id), None, None) .await - .context("Error setting ambient agent shared session ID")); + .context(i18n::t( + "ai.agent_sdk.driver.set_ambient_agent_shared_session_id_failed", + ))); }, |_, _, _| {}, ); @@ -3058,7 +3070,8 @@ impl AgentDriver { for provider in providers { if let Err(err) = provider.cleanup().await { - report_error!(anyhow!(err).context("Unable to clean up cloud provider")); + report_error!(anyhow!(err) + .context(i18n::t("ai.agent_sdk.driver.cleanup_cloud_provider_failed"))); } } } @@ -3260,7 +3273,7 @@ pub(super) fn write_run_started(run_id: &str, output_format: OutputFormat) { OutputFormat::Json | OutputFormat::Ndjson => output::json::run_started(run_id, buf), OutputFormat::Text | OutputFormat::Pretty => output::text::run_started(run_id, buf), }) - .context("Failed to write run ID")); + .context(i18n::t("ai.agent_sdk.driver.write_run_id_failed"))); } /// Report a driver-level error to the server for the given task. @@ -3278,9 +3291,10 @@ pub(super) async fn report_driver_error( .update_agent_task(task_id, Some(state), None, None, Some(status_update)) .await { - report_error!( - anyhow!(e).context(format!("Failed to report driver error for task {task_id}")) - ); + report_error!(anyhow!(e).context( + i18n::t("ai.agent_sdk.driver.report_driver_error_failed") + .replace("{task_id}", &task_id.to_string()) + )); } } @@ -3312,7 +3326,9 @@ fn write_session_joined(join_url: &str, output_format: OutputFormat) { output::text::shared_session_established(join_url, buf) } }) - .context("Failed to write shared session event")); + .context(i18n::t( + "ai.agent_sdk.driver.write_shared_session_event_failed" + ))); } #[cfg(test)] diff --git a/app/src/ai/agent_sdk/driver/attachments.rs b/app/src/ai/agent_sdk/driver/attachments.rs index c2f6d617f0..0f1f38a2c1 100644 --- a/app/src/ai/agent_sdk/driver/attachments.rs +++ b/app/src/ai/agent_sdk/driver/attachments.rs @@ -44,7 +44,7 @@ pub(crate) async fn fetch_and_download_attachments( let attachments = ai_client .get_task_attachments(task_id.clone()) .await - .context("Failed to fetch task attachments")?; + .context(i18n::t("ai.agent_sdk.driver.attachments.fetch_task_failed"))?; log::info!("Fetched {} task attachments", attachments.len()); @@ -83,16 +83,18 @@ pub(crate) async fn fetch_and_download_handoff_snapshot_attachments( let attachments = ai_client .get_handoff_snapshot_attachments(&task_id) .await - .context("Failed to fetch handoff snapshot attachments")?; + .context(i18n::t( + "ai.agent_sdk.driver.attachments.fetch_handoff_failed", + ))?; if attachments.is_empty() { return Ok(None); } let handoff_dir = attachments_dir.join("handoff"); - fs::create_dir_all(&handoff_dir) - .await - .context("Failed to create handoff attachments directory")?; + fs::create_dir_all(&handoff_dir).await.context(i18n::t( + "ai.agent_sdk.driver.attachments.create_handoff_dir_failed", + ))?; let attempts = attachments.len(); let download_futures = attachments.into_iter().map(|attachment| { @@ -144,7 +146,7 @@ async fn download_and_write_attachments( ) -> anyhow::Result<()> { fs::create_dir_all(attachment_dir) .await - .context("Failed to create attachments directory")?; + .context(i18n::t("ai.agent_sdk.driver.attachments.create_dir_failed"))?; log::info!( "Created attachments directory at: {}", attachment_dir.display() @@ -181,7 +183,13 @@ async fn download_task_attachment( let safe_filename = Path::new(&attachment.filename) .file_name() .and_then(|n| n.to_str()) - .ok_or_else(|| anyhow::anyhow!("Invalid filename for file_id={}", attachment.file_id))? + .ok_or_else(|| { + anyhow::anyhow!( + "{}", + i18n::t("ai.agent_sdk.driver.attachments.invalid_filename") + .replace("{file_id}", &attachment.file_id) + ) + })? .to_string(); let file_path = attachment_dir.join(&safe_filename); @@ -227,11 +235,9 @@ async fn download_attachment( ) -> anyhow::Result<()> { let operation = format!("download attachment '{}'", file_path.display()); with_bounded_retry(&operation, || async { - let response = http_client - .get(download_url) - .send() - .await - .context("Failed to send download request")?; + let response = http_client.get(download_url).send().await.context(i18n::t( + "ai.agent_sdk.driver.attachments.send_download_request_failed", + ))?; if !response.status().is_success() { let status = response.status(); @@ -240,19 +246,23 @@ async fn download_attachment( status: status.as_u16(), body: body.clone(), }) - .context(format!("Download failed with status {status}: {body}"))); + .context( + i18n::t("ai.agent_sdk.driver.attachments.download_status_failed") + .replace("{status}", &status.to_string()) + .replace("{body}", &body), + )); } // Stream the response body directly to disk instead of buffering the full payload // in memory. - let mut file = fs::File::create(file_path) - .await - .context("Failed to create file")?; + let mut file = fs::File::create(file_path).await.context(i18n::t( + "ai.agent_sdk.driver.attachments.create_file_failed", + ))?; let mut response_stream = StreamReader::new(response.bytes_stream().map_err(std::io::Error::other)); tokio::io::copy(&mut response_stream, &mut file) .await - .context("Failed to write file")?; + .context(i18n::t("ai.agent_sdk.driver.attachments.write_file_failed"))?; Ok(()) }) @@ -268,8 +278,10 @@ pub fn process_attachment( ) -> anyhow::Result { let file_bytes = std::fs::read(attachment_path).map_err(|e| { anyhow::anyhow!( - "Failed to read attachment file '{}': {e}", - attachment_path.display() + "{}", + i18n::t("ai.agent_sdk.driver.attachments.read_attachment_file_failed") + .replace("{path}", &attachment_path.display().to_string()) + .replace("{error}", &e.to_string()) ) })?; @@ -289,8 +301,13 @@ pub fn process_attachment( if file_bytes.len() > MAX_ATTACHMENT_SIZE_BYTES { return Err(anyhow::anyhow!( - "File is too large ({}MB). Maximum size is 10MB.", - file_bytes.len() / (1024 * 1024) + "{}", + i18n::t("ai.agent_sdk.driver.attachments.file_too_large") + .replace("{size_mb}", &(file_bytes.len() / (1024 * 1024)).to_string()) + .replace( + "{max_mb}", + &(MAX_ATTACHMENT_SIZE_BYTES / (1024 * 1024)).to_string() + ) )); } diff --git a/app/src/ai/agent_sdk/driver/attachments_tests.rs b/app/src/ai/agent_sdk/driver/attachments_tests.rs index e5b3c2a82c..a37a97475a 100644 --- a/app/src/ai/agent_sdk/driver/attachments_tests.rs +++ b/app/src/ai/agent_sdk/driver/attachments_tests.rs @@ -10,6 +10,10 @@ use crate::ai::agent_sdk::test_support::build_test_http_client; use crate::ai::ambient_agents::AmbientAgentTaskId; use crate::server::server_api::ai::MockAIClient; +fn use_english_locale() { + i18n::set_locale("en"); +} + #[test] fn process_attachment_text_file() { let mut f = NamedTempFile::with_suffix(".txt").unwrap(); @@ -29,6 +33,8 @@ fn process_attachment_text_file() { #[test] fn process_attachment_too_large() { + use_english_locale(); + let mut f = NamedTempFile::with_suffix(".bin").unwrap(); let data = vec![0u8; MAX_ATTACHMENT_SIZE_BYTES + 1]; f.write_all(&data).unwrap(); @@ -39,6 +45,8 @@ fn process_attachment_too_large() { #[test] fn process_attachment_nonexistent_file() { + use_english_locale(); + let path = std::path::PathBuf::from("/tmp/nonexistent_attachment_test_file.xyz"); let err = process_attachment(&path, 0).unwrap_err(); assert!(err.to_string().contains("Failed to read")); @@ -328,6 +336,8 @@ async fn e2e_empty_attachment_list_returns_none_without_creating_dir() { #[tokio::test] async fn e2e_get_handoff_snapshot_attachments_failure_is_fatal() { + use_english_locale(); + // When the listing call errors, the function must return Err wrapping the underlying // message with a context describing where it happened. let _guard = FeatureFlag::OzHandoff.override_enabled(true); diff --git a/app/src/ai/agent_sdk/driver/cloud_provider.rs b/app/src/ai/agent_sdk/driver/cloud_provider.rs index 0ae837ee1c..7d02665412 100644 --- a/app/src/ai/agent_sdk/driver/cloud_provider.rs +++ b/app/src/ai/agent_sdk/driver/cloud_provider.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::ffi::OsString; +use std::fmt; use std::future::Future; use std::pin::Pin; @@ -14,11 +15,9 @@ mod gcp; pub(crate) type Result = std::result::Result; -#[derive(Debug, thiserror::Error)] -#[error("{provider_name} setup failed")] +#[derive(Debug)] pub(crate) struct CloudProviderSetupError { provider_name: &'static str, - #[source] source: Error, } @@ -31,6 +30,23 @@ impl CloudProviderSetupError { } } +impl fmt::Display for CloudProviderSetupError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + i18n::t("ai.agent_sdk.driver.cloud_provider.setup_failed") + .replace("{provider_name}", self.provider_name) + ) + } +} + +impl std::error::Error for CloudProviderSetupError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(self.source.as_ref()) + } +} + /// A cloud provider that we configure automatic Oz access to. pub(crate) trait CloudProvider: Send { /// Return environment variables that should be injected into the terminal diff --git a/app/src/ai/agent_sdk/driver/cloud_provider/aws.rs b/app/src/ai/agent_sdk/driver/cloud_provider/aws.rs index 1d8151d5cb..ac2b3b70eb 100644 --- a/app/src/ai/agent_sdk/driver/cloud_provider/aws.rs +++ b/app/src/ai/agent_sdk/driver/cloud_provider/aws.rs @@ -42,7 +42,9 @@ impl AwsCloudProvider { .prefix(&format!("oz_aws_oidc_{run_id}_")) .suffix(".token") .tempfile() - .context("Failed to create temporary AWS OIDC token file") + .context(i18n::t( + "ai.agent_sdk.driver.cloud_provider.aws.create_token_file_failed", + )) .map_err(|error| CloudProviderSetupError::new(Self::PROVIDER_NAME, error))?; Ok(Self { @@ -111,6 +113,9 @@ impl CloudProvider for AwsCloudProvider { // 2. Write the token to the pre-created temporary file. async_fs::write(&token_file_path, token.token.as_bytes()) .await + .context(i18n::t( + "ai.agent_sdk.driver.cloud_provider.aws.write_token_file_failed", + )) .map_err(|err| CloudProviderSetupError::new(Self::PROVIDER_NAME, err))?; safe_info!( @@ -126,7 +131,9 @@ impl CloudProvider for AwsCloudProvider { let Self { token_file, .. } = *self; token_file .close() - .context("Failed to remove AWS OIDC token file") + .context(i18n::t( + "ai.agent_sdk.driver.cloud_provider.aws.remove_token_file_failed", + )) .map_err(|err| CloudProviderSetupError::new(Self::PROVIDER_NAME, err)) }) } diff --git a/app/src/ai/agent_sdk/driver/cloud_provider/gcp.rs b/app/src/ai/agent_sdk/driver/cloud_provider/gcp.rs index 4e318c26ca..96fecf8de4 100644 --- a/app/src/ai/agent_sdk/driver/cloud_provider/gcp.rs +++ b/app/src/ai/agent_sdk/driver/cloud_provider/gcp.rs @@ -36,7 +36,9 @@ impl GcpCloudProvider { }; let credentials = GcpCredentials::federated(run_id, &federation_config) - .context("Failed to prepare GCP federation credentials") + .context(i18n::t( + "ai.agent_sdk.driver.cloud_provider.gcp.prepare_credentials_failed", + )) .map_err(|error| CloudProviderSetupError::new(Self::PROVIDER_NAME, error))?; Ok(Self { credentials }) @@ -52,7 +54,9 @@ impl CloudProvider for GcpCloudProvider { Box::pin(async move { self.credentials .cleanup() - .context("Failed to remove GCP credential files") + .context(i18n::t( + "ai.agent_sdk.driver.cloud_provider.gcp.remove_credentials_failed", + )) .map_err(|err| CloudProviderSetupError::new(Self::PROVIDER_NAME, err)) }) } diff --git a/app/src/ai/agent_sdk/driver/error_classification.rs b/app/src/ai/agent_sdk/driver/error_classification.rs index 2871cbc028..2d7052857e 100644 --- a/app/src/ai/agent_sdk/driver/error_classification.rs +++ b/app/src/ai/agent_sdk/driver/error_classification.rs @@ -13,43 +13,38 @@ pub fn classify_driver_error(error: &AgentDriverError) -> (AgentTaskState, TaskS AgentDriverError::TerminalUnavailable | AgentDriverError::InvalidRuntimeState => ( AgentTaskState::Error, TaskStatusUpdate::with_error_code( - "An internal error occurred. Please try running your task again. If the issue persists, contact support.", + i18n::t("ai.agent_sdk.driver.error_classification.internal_error"), PlatformErrorCode::InternalError, ), ), AgentDriverError::BootstrapFailed => ( AgentTaskState::Error, TaskStatusUpdate::with_error_code( - "Terminal session failed to start. Please try running your task again.", + i18n::t("ai.agent_sdk.driver.error_classification.bootstrap_failed"), PlatformErrorCode::InternalError, ), ), AgentDriverError::ShareSessionFailed { error: share_err } => { let message = match share_err { ShareSessionError::Internal(_) => { - "Failed to share agent session due to an internal error. Please try running your task again.".to_string() + i18n::t("ai.agent_sdk.driver.error_classification.share_session_internal") } ShareSessionError::Failed(reason) => { // The reason string comes from the session-sharing layer and is aimed at // interactive users (e.g. "try sharing again"). Provide a cloud-agent- // appropriate message instead of wrapping it, which would produce // repetitive "try again" text. - format!("Failed to share agent session: {reason}") + i18n::t("ai.agent_sdk.driver.error_classification.share_session_failed") + .replace("{reason}", reason) } ShareSessionError::Disabled => { - "Session sharing is not enabled for your account. This is likely because \ - an administrator has disabled session sharing for your team. Please \ - verify that session sharing is enabled in your team settings, or try \ - running without the --share flag." - .to_string() + i18n::t("ai.agent_sdk.driver.error_classification.share_session_disabled") } ShareSessionError::Timeout => { - "Failed to share agent session: timed out waiting for the session sharing \ - server to respond. Please check your network connection and try again." - .to_string() + i18n::t("ai.agent_sdk.driver.error_classification.share_session_timeout") } ShareSessionError::Interrupted => { - "Session sharing was interrupted before it could complete. Please try running your task again.".to_string() + i18n::t("ai.agent_sdk.driver.error_classification.share_session_interrupted") } }; ( @@ -66,7 +61,7 @@ pub fn classify_driver_error(error: &AgentDriverError) -> (AgentTaskState, TaskS AgentDriverError::WarpDriveSyncFailed => ( AgentTaskState::Error, TaskStatusUpdate::with_error_code( - "Warp Drive failed to sync. Please check your network connection and try again.", + i18n::t("ai.agent_sdk.driver.error_classification.warp_drive_sync_failed"), PlatformErrorCode::InternalError, ), ), @@ -75,9 +70,8 @@ pub fn classify_driver_error(error: &AgentDriverError) -> (AgentTaskState, TaskS ( AgentTaskState::Error, TaskStatusUpdate::with_error_code( - format!( - "Authentication required. Log in via '{bin} login', provide an API key via '--api-key', or set the WARP_API_KEY environment variable." - ), + i18n::t("ai.agent_sdk.driver.error_classification.not_logged_in") + .replace("{bin}", &bin), PlatformErrorCode::AuthenticationRequired, ), ) @@ -85,7 +79,8 @@ pub fn classify_driver_error(error: &AgentDriverError) -> (AgentTaskState, TaskS AgentDriverError::CloudProviderSetupFailed(err) => ( AgentTaskState::Error, TaskStatusUpdate::with_error_code( - format!("Error configuring cloud access: {err:#}"), + i18n::t("ai.agent_sdk.driver.error_classification.cloud_access_failed") + .replace("{error}", &format!("{err:#}")), PlatformErrorCode::InternalError, ), ), @@ -94,76 +89,70 @@ pub fn classify_driver_error(error: &AgentDriverError) -> (AgentTaskState, TaskS AgentDriverError::MCPServerNotFound(uuid) => ( AgentTaskState::Failed, TaskStatusUpdate::with_error_code( - format!( - "MCP server {uuid} was not found. Verify the server exists in your Warp Drive and the UUID is correct." - ), + i18n::t("ai.agent_sdk.driver.error_classification.mcp_server_not_found") + .replace("{uuid}", &uuid.to_string()), PlatformErrorCode::EnvironmentSetupFailed, ), ), AgentDriverError::MCPStartupFailed => ( AgentTaskState::Failed, TaskStatusUpdate::with_error_code( - "One or more MCP servers failed to start. Check that your MCP server configuration is valid and the server process is runnable.", + i18n::t("ai.agent_sdk.driver.error_classification.mcp_startup_failed"), PlatformErrorCode::EnvironmentSetupFailed, ), ), AgentDriverError::MCPJsonParseError(msg) => ( AgentTaskState::Failed, TaskStatusUpdate::with_error_code( - format!("Failed to parse MCP server JSON configuration: {msg}"), + i18n::t("ai.agent_sdk.driver.error_classification.mcp_json_parse_failed") + .replace("{message}", msg), PlatformErrorCode::EnvironmentSetupFailed, ), ), AgentDriverError::MCPMissingVariables => ( AgentTaskState::Failed, TaskStatusUpdate::with_error_code( - "MCP server configuration is missing required variables. Provide all required environment variables or template values.", + i18n::t("ai.agent_sdk.driver.error_classification.mcp_missing_variables"), PlatformErrorCode::EnvironmentSetupFailed, ), ), AgentDriverError::ProfileError(name) => ( AgentTaskState::Failed, TaskStatusUpdate::with_error_code( - format!( - "Agent profile \"{name}\" not found. Check the profile ID and ensure it exists in your team's Warp Drive." - ), + i18n::t("ai.agent_sdk.driver.error_classification.profile_not_found") + .replace("{name}", name), PlatformErrorCode::ResourceNotFound, ), ), AgentDriverError::AIWorkflowNotFound(id) => ( AgentTaskState::Failed, TaskStatusUpdate::with_error_code( - format!( - "Saved prompt not found for ID {id}. Verify the prompt exists in your Warp Drive." - ), + i18n::t("ai.agent_sdk.driver.error_classification.saved_prompt_not_found") + .replace("{id}", id), PlatformErrorCode::ResourceNotFound, ), ), AgentDriverError::EnvironmentNotFound(id) => ( AgentTaskState::Failed, TaskStatusUpdate::with_error_code( - format!( - "Environment '{id}' not found. Verify the environment ID and ensure it exists in your team settings." - ), + i18n::t("ai.agent_sdk.driver.error_classification.environment_not_found") + .replace("{id}", id), PlatformErrorCode::ResourceNotFound, ), ), AgentDriverError::EnvironmentSetupFailed(msg) => ( AgentTaskState::Failed, TaskStatusUpdate::with_error_code( - format!( - "Environment setup failed: {msg}. Check your repository URLs and setup commands." - ), + i18n::t("ai.agent_sdk.driver.error_classification.environment_setup_failed") + .replace("{message}", msg), PlatformErrorCode::EnvironmentSetupFailed, ), ), AgentDriverError::InvalidWorkingDirectory { path, .. } => ( AgentTaskState::Failed, TaskStatusUpdate::with_error_code( - format!( - "Working directory '{}' does not exist or is not a directory. Verify the path in your environment configuration.", - path.display() - ), + i18n::t("ai.agent_sdk.driver.error_classification.invalid_working_directory") + .replace("{path}", &path.display().to_string()), PlatformErrorCode::EnvironmentSetupFailed, ), ), @@ -189,62 +178,71 @@ pub fn classify_driver_error(error: &AgentDriverError) -> (AgentTaskState, TaskS // --- Cancellation / Blocked (no error code) --- AgentDriverError::ConversationCancelled { .. } => ( AgentTaskState::Cancelled, - TaskStatusUpdate::message("Task cancelled."), + TaskStatusUpdate::message(i18n::t( + "ai.agent_sdk.driver.error_classification.task_cancelled", + )), ), AgentDriverError::ConversationBlocked { blocked_action } => ( AgentTaskState::Blocked, - TaskStatusUpdate::message(format!( - "The agent got stuck waiting for user confirmation on the action: {blocked_action}" - )), + TaskStatusUpdate::message( + i18n::t("ai.agent_sdk.driver.error_classification.conversation_blocked") + .replace("{blocked_action}", blocked_action), + ), ), // --- Setup errors --- AgentDriverError::TeamMetadataRefreshTimeout => ( AgentTaskState::Error, TaskStatusUpdate::with_error_code( - "Timed out refreshing team metadata. Please check your network connection and try again.", + i18n::t("ai.agent_sdk.driver.error_classification.team_metadata_timeout"), PlatformErrorCode::InternalError, ), ), AgentDriverError::SkillResolutionFailed(msg) => ( AgentTaskState::Failed, TaskStatusUpdate::with_error_code( - format!("Skill resolution failed: {msg}"), + i18n::t("ai.agent_sdk.driver.error_classification.skill_resolution_failed") + .replace("{message}", msg), PlatformErrorCode::ResourceNotFound, ), ), AgentDriverError::ConfigBuildFailed(err) => ( AgentTaskState::Failed, TaskStatusUpdate::with_error_code( - format!("Failed to build agent configuration: {err}"), + i18n::t("ai.agent_sdk.driver.error_classification.config_build_failed") + .replace("{error}", &err.to_string()), PlatformErrorCode::EnvironmentSetupFailed, ), ), AgentDriverError::PromptResolutionFailed(err) => ( AgentTaskState::Error, TaskStatusUpdate::with_error_code( - format!("Failed to resolve prompt for the run: {err}"), + i18n::t("ai.agent_sdk.driver.error_classification.prompt_resolution_failed") + .replace("{error}", &err.to_string()), PlatformErrorCode::InternalError, ), ), AgentDriverError::SecretsFetchFailed(err) => ( AgentTaskState::Error, TaskStatusUpdate::with_error_code( - format!("Failed to fetch task secrets: {err}"), + i18n::t("ai.agent_sdk.driver.error_classification.secrets_fetch_failed") + .replace("{error}", &err.to_string()), PlatformErrorCode::InternalError, ), ), AgentDriverError::AwsBedrockCredentialsFailed(msg) => ( AgentTaskState::Failed, TaskStatusUpdate::with_error_code( - format!("Failed to initialize AWS Bedrock credentials: {msg}"), + i18n::t("ai.agent_sdk.driver.error_classification.aws_bedrock_failed") + .replace("{message}", msg), PlatformErrorCode::EnvironmentSetupFailed, ), ), AgentDriverError::ConversationLoadFailed(msg) => ( AgentTaskState::Error, TaskStatusUpdate::with_error_code( - format!("Failed to load conversation: {msg}"), + i18n::t("ai.agent_sdk.driver.error_classification.conversation_load_failed") + .replace("{message}", msg), PlatformErrorCode::InternalError, ), ), @@ -255,10 +253,10 @@ pub fn classify_driver_error(error: &AgentDriverError) -> (AgentTaskState, TaskS } => ( AgentTaskState::Failed, TaskStatusUpdate::with_error_code( - format!( - "Conversation {conversation_id} was produced by the {expected} harness, but --harness {got} was requested. \ - Re-run with --harness {expected} (or omit --harness) to continue this conversation." - ), + i18n::t("ai.agent_sdk.driver.error_classification.conversation_harness_mismatch") + .replace("{conversation_id}", conversation_id) + .replace("{expected}", expected) + .replace("{got}", got), PlatformErrorCode::EnvironmentSetupFailed, ), ), @@ -269,10 +267,10 @@ pub fn classify_driver_error(error: &AgentDriverError) -> (AgentTaskState, TaskS } => ( AgentTaskState::Failed, TaskStatusUpdate::with_error_code( - format!( - "Task {task_id} was created with the {expected} harness, but --harness {got} was requested. \ - Re-run with --harness {expected} (or omit --harness) to continue this task." - ), + i18n::t("ai.agent_sdk.driver.error_classification.task_harness_mismatch") + .replace("{task_id}", task_id) + .replace("{expected}", expected) + .replace("{got}", got), PlatformErrorCode::EnvironmentSetupFailed, ), ), @@ -282,40 +280,42 @@ pub fn classify_driver_error(error: &AgentDriverError) -> (AgentTaskState, TaskS } => ( AgentTaskState::Failed, TaskStatusUpdate::with_error_code( - format!( - "Conversation {conversation_id} has no stored transcript for the {harness} harness. \ - The prior run may have crashed before saving any state." - ), + i18n::t("ai.agent_sdk.driver.error_classification.resume_state_missing") + .replace("{conversation_id}", conversation_id) + .replace("{harness}", harness), PlatformErrorCode::ResourceNotFound, ), ), AgentDriverError::HarnessCommandFailed { exit_code } => ( AgentTaskState::Failed, TaskStatusUpdate::with_error_code( - format!("Harness command exited with code {exit_code}"), + i18n::t("ai.agent_sdk.driver.error_classification.harness_command_failed") + .replace("{exit_code}", &exit_code.to_string()), PlatformErrorCode::InternalError, ), ), AgentDriverError::HarnessSetupFailed { harness, reason } => ( AgentTaskState::Failed, TaskStatusUpdate::with_error_code( - format!("Harness '{harness}' validation failed: {reason}"), + i18n::t("ai.agent_sdk.driver.error_classification.harness_setup_failed") + .replace("{harness}", harness) + .replace("{reason}", reason), PlatformErrorCode::EnvironmentSetupFailed, ), ), AgentDriverError::HarnessConfigSetupFailed { harness, error } => ( AgentTaskState::Failed, TaskStatusUpdate::with_error_code( - format!("Harness '{harness}' config setup failed: {error}"), + i18n::t("ai.agent_sdk.driver.error_classification.harness_config_failed") + .replace("{harness}", harness) + .replace("{error}", &error.to_string()), PlatformErrorCode::EnvironmentSetupFailed, ), ), AgentDriverError::HarnessAuthCheckFailed { harness, detail } => { - let message = format!( - "Harness '{harness}' authentication check failed: login credentials \ - are invalid or expired. Verify that the authentication secret \ - configured for this harness is correct." - ); + let message = + i18n::t("ai.agent_sdk.driver.error_classification.harness_auth_check_failed") + .replace("{harness}", harness); log::error!("Preflight detail for {harness}: {detail}"); ( AgentTaskState::Failed, @@ -330,12 +330,11 @@ pub fn classify_driver_error(error: &AgentDriverError) -> (AgentTaskState, TaskS pattern, excerpt, } => { - let message = format!( - "Harness '{harness}' could not make a successful API request. \ - Matched failure pattern '{pattern}' in harness output: \"{excerpt}\". \ - This usually means the API key is invalid, out of credits, or the \ - account is misconfigured." - ); + let message = + i18n::t("ai.agent_sdk.driver.error_classification.harness_runtime_failure") + .replace("{harness}", harness) + .replace("{pattern}", pattern) + .replace("{excerpt}", excerpt); log::error!("Runtime failure for {harness}: pattern={pattern}, excerpt={excerpt}"); ( AgentTaskState::Failed, diff --git a/app/src/ai/agent_sdk/driver/error_classification_tests.rs b/app/src/ai/agent_sdk/driver/error_classification_tests.rs index 7a71082c8c..d4a8f4734e 100644 --- a/app/src/ai/agent_sdk/driver/error_classification_tests.rs +++ b/app/src/ai/agent_sdk/driver/error_classification_tests.rs @@ -4,12 +4,22 @@ use super::classify_driver_error; use crate::ai::agent_sdk::driver::terminal::ShareSessionError; use crate::ai::agent_sdk::driver::AgentDriverError; +fn classify_driver_error_en( + error: &AgentDriverError, +) -> ( + AgentTaskState, + crate::server::server_api::ai::TaskStatusUpdate, +) { + i18n::set_locale("en"); + classify_driver_error(error) +} + fn assert_state_and_code( error: AgentDriverError, expected_state: AgentTaskState, expected_code: Option, ) { - let (state, update) = classify_driver_error(&error); + let (state, update) = classify_driver_error_en(&error); assert_eq!(state, expected_state, "unexpected state for {error}"); assert_eq!( update.error_code, expected_code, @@ -39,7 +49,7 @@ fn terminal_unavailable_is_error_with_internal() { #[test] fn not_logged_in_is_error_with_auth_required() { - let (state, update) = classify_driver_error(&AgentDriverError::NotLoggedIn); + let (state, update) = classify_driver_error_en(&AgentDriverError::NotLoggedIn); assert_eq!(state, AgentTaskState::Error); assert_eq!( update.error_code, @@ -101,11 +111,12 @@ fn environment_not_found_is_failed_with_resource_not_found() { #[test] fn conversation_harness_mismatch_is_failed_with_env_setup() { - let (state, update) = classify_driver_error(&AgentDriverError::ConversationHarnessMismatch { - conversation_id: "conv-123".into(), - expected: "claude".into(), - got: "oz".into(), - }); + let (state, update) = + classify_driver_error_en(&AgentDriverError::ConversationHarnessMismatch { + conversation_id: "conv-123".into(), + expected: "claude".into(), + got: "oz".into(), + }); assert_eq!(state, AgentTaskState::Failed); assert_eq!( update.error_code, @@ -118,7 +129,7 @@ fn conversation_harness_mismatch_is_failed_with_env_setup() { #[test] fn conversation_resume_state_missing_is_failed_with_resource_not_found() { let (state, update) = - classify_driver_error(&AgentDriverError::ConversationResumeStateMissing { + classify_driver_error_en(&AgentDriverError::ConversationResumeStateMissing { harness: "claude".into(), conversation_id: "conv-123".into(), }); @@ -132,7 +143,7 @@ fn conversation_resume_state_missing_is_failed_with_resource_not_found() { #[test] fn share_session_disabled_gets_feature_not_available() { - let (state, update) = classify_driver_error(&AgentDriverError::ShareSessionFailed { + let (state, update) = classify_driver_error_en(&AgentDriverError::ShareSessionFailed { error: ShareSessionError::Disabled, }); assert_eq!(state, AgentTaskState::Error); @@ -146,7 +157,7 @@ fn share_session_disabled_gets_feature_not_available() { #[test] fn share_session_timeout_gets_internal_error() { - let (state, update) = classify_driver_error(&AgentDriverError::ShareSessionFailed { + let (state, update) = classify_driver_error_en(&AgentDriverError::ShareSessionFailed { error: ShareSessionError::Timeout, }); assert_eq!(state, AgentTaskState::Error); @@ -156,7 +167,7 @@ fn share_session_timeout_gets_internal_error() { #[test] fn share_session_failed_includes_reason() { - let (state, update) = classify_driver_error(&AgentDriverError::ShareSessionFailed { + let (state, update) = classify_driver_error_en(&AgentDriverError::ShareSessionFailed { error: ShareSessionError::Failed("server rejected".into()), }); assert_eq!(state, AgentTaskState::Error); @@ -167,7 +178,7 @@ fn share_session_failed_includes_reason() { #[test] fn conversation_cancelled_is_cancelled() { - let (state, update) = classify_driver_error(&AgentDriverError::ConversationCancelled { + let (state, update) = classify_driver_error_en(&AgentDriverError::ConversationCancelled { reason: crate::ai::agent::CancellationReason::ManuallyCancelled, }); assert_eq!(state, AgentTaskState::Cancelled); @@ -176,7 +187,7 @@ fn conversation_cancelled_is_cancelled() { #[test] fn conversation_blocked_is_blocked() { - let (state, update) = classify_driver_error(&AgentDriverError::ConversationBlocked { + let (state, update) = classify_driver_error_en(&AgentDriverError::ConversationBlocked { blocked_action: "rm -rf /".into(), }); assert_eq!(state, AgentTaskState::Blocked); @@ -187,7 +198,7 @@ fn conversation_blocked_is_blocked() { #[test] fn harness_auth_check_failed_is_failed_with_auth_required() { - let (state, update) = classify_driver_error(&AgentDriverError::HarnessAuthCheckFailed { + let (state, update) = classify_driver_error_en(&AgentDriverError::HarnessAuthCheckFailed { harness: "claude".into(), detail: "exit code 1".into(), }); @@ -204,11 +215,12 @@ fn harness_auth_check_failed_is_failed_with_auth_required() { #[test] fn harness_runtime_failure_detected_is_failed_with_auth_required() { - let (state, update) = classify_driver_error(&AgentDriverError::HarnessRuntimeFailureDetected { - harness: "claude".into(), - pattern: "credit balance is too low".into(), - excerpt: "Error: Your credit balance is too low to make this request.".into(), - }); + let (state, update) = + classify_driver_error_en(&AgentDriverError::HarnessRuntimeFailureDetected { + harness: "claude".into(), + pattern: "credit balance is too low".into(), + excerpt: "Error: Your credit balance is too low to make this request.".into(), + }); assert_eq!(state, AgentTaskState::Failed); assert_eq!( update.error_code, diff --git a/app/src/ai/agent_sdk/driver/git_credentials.rs b/app/src/ai/agent_sdk/driver/git_credentials.rs index bd08952e90..14a15a918b 100644 --- a/app/src/ai/agent_sdk/driver/git_credentials.rs +++ b/app/src/ai/agent_sdk/driver/git_credentials.rs @@ -28,7 +28,11 @@ const DEFAULT_GIT_EMAIL: &str = "oz-agent@warp.dev"; const GH_HOSTS_FILENAME: &str = "hosts.yml"; fn home_dir() -> Result { - dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not determine home directory")) + dirs::home_dir().ok_or_else(|| { + anyhow::anyhow!(i18n::t( + "ai.agent_sdk.driver.git_credentials.home_dir_missing" + )) + }) } /// Write `content` to `path` using owner-only (0600) permissions. @@ -47,16 +51,26 @@ fn write_secret_file(path: &std::path::Path, content: &str) -> Result<()> { .truncate(true) .mode(0o600) .open(path) - .with_context(|| format!("Failed to open {} for writing", path.display()))?; + .with_context(|| { + i18n::t("ai.agent_sdk.driver.git_credentials.open_for_writing_failed") + .replace("{path}", &path.display().to_string()) + })?; file.set_permissions(std::fs::Permissions::from_mode(0o600)) - .with_context(|| format!("Failed to set permissions on {}", path.display()))?; - file.write_all(content.as_bytes()) - .with_context(|| format!("Failed to write {}", path.display()))?; + .with_context(|| { + i18n::t("ai.agent_sdk.driver.git_credentials.set_permissions_failed") + .replace("{path}", &path.display().to_string()) + })?; + file.write_all(content.as_bytes()).with_context(|| { + i18n::t("ai.agent_sdk.driver.git_credentials.write_file_failed") + .replace("{path}", &path.display().to_string()) + })?; } #[cfg(not(unix))] { - std::fs::write(path, content) - .with_context(|| format!("Failed to write {}", path.display()))?; + std::fs::write(path, content).with_context(|| { + i18n::t("ai.agent_sdk.driver.git_credentials.write_file_failed") + .replace("{path}", &path.display().to_string()) + })?; } Ok(()) } @@ -88,11 +102,9 @@ fn write_git_credentials_file(credentials: &[GitCredential]) -> Result<()> { write_secret_file(&tmp_path, &content)?; std::fs::rename(&tmp_path, &path).with_context(|| { - format!( - "Failed to rename {} to {}", - tmp_path.display(), - path.display() - ) + i18n::t("ai.agent_sdk.driver.git_credentials.rename_failed") + .replace("{from}", &tmp_path.display().to_string()) + .replace("{to}", &path.display().to_string()) })?; Ok(()) @@ -114,8 +126,10 @@ fn write_gh_hosts_yml(credentials: &[GitCredential], home: &std::path::Path) -> return Ok(()); } let gh_config_dir = home.join(".config").join("gh"); - std::fs::create_dir_all(&gh_config_dir) - .with_context(|| format!("Failed to create {}", gh_config_dir.display()))?; + std::fs::create_dir_all(&gh_config_dir).with_context(|| { + i18n::t("ai.agent_sdk.driver.git_credentials.create_dir_failed") + .replace("{path}", &gh_config_dir.display().to_string()) + })?; let path = gh_config_dir.join(GH_HOSTS_FILENAME); let tmp_path = gh_config_dir.join(format!("{GH_HOSTS_FILENAME}.tmp")); @@ -131,11 +145,9 @@ fn write_gh_hosts_yml(credentials: &[GitCredential], home: &std::path::Path) -> write_secret_file(&tmp_path, &yaml)?; std::fs::rename(&tmp_path, &path).with_context(|| { - format!( - "Failed to rename {} to {}", - tmp_path.display(), - path.display() - ) + i18n::t("ai.agent_sdk.driver.git_credentials.rename_failed") + .replace("{from}", &tmp_path.display().to_string()) + .replace("{to}", &path.display().to_string()) })?; Ok(()) @@ -243,13 +255,17 @@ async fn try_refresh(task_id: &str, ai_client: &Arc) -> Result<()> let workload_token = warp_isolation_platform::issue_workload_token(Some(Duration::from_secs(5 * 60))) .await - .context("Failed to issue workload token for git credentials refresh")? + .context(i18n::t( + "ai.agent_sdk.driver.git_credentials.issue_workload_token_failed", + ))? .token; let credentials = ai_client .get_task_git_credentials(task_id.to_string(), workload_token) .await - .context("Failed to fetch git credentials from server")?; + .context(i18n::t( + "ai.agent_sdk.driver.git_credentials.fetch_from_server_failed", + ))?; if credentials.is_empty() { log::debug!("No git credentials returned during refresh; skipping file write"); diff --git a/app/src/ai/agent_sdk/driver/harness/claude_code.rs b/app/src/ai/agent_sdk/driver/harness/claude_code.rs index 5b0370e65f..6d5abbb160 100644 --- a/app/src/ai/agent_sdk/driver/harness/claude_code.rs +++ b/app/src/ai/agent_sdk/driver/harness/claude_code.rs @@ -277,14 +277,14 @@ impl ClaudeHarnessRunner { // doesn't exist locally. envelope.cwd = working_dir.to_path_buf(); let config_root = claude_config_dir().map_err(|e| { - AgentDriverError::ConfigBuildFailed( - e.context("Failed to resolve Claude config dir"), - ) + AgentDriverError::ConfigBuildFailed(e.context(i18n::t( + "ai.agent_sdk.driver.harness.claude.resolve_config_dir_failed", + ))) })?; write_envelope(&envelope, &config_root).map_err(|e| { - AgentDriverError::ConfigBuildFailed( - e.context("Failed to rehydrate Claude transcript"), - ) + AgentDriverError::ConfigBuildFailed(e.context(i18n::t( + "ai.agent_sdk.driver.harness.claude.rehydrate_transcript_failed", + ))) })?; // Index write is best-effort: upstream Claude versions vary in how they use // `sessions-index.json`, so losing the index entry shouldn't abort the run. @@ -520,7 +520,12 @@ impl HarnessRunner for ClaudeHarnessRunner { }); }) .await - .map_err(|_| anyhow::anyhow!("Agent driver dropped while sending /exit")) + .map_err(|_| { + anyhow::anyhow!( + i18n::t("ai.agent_sdk.driver.harness.driver_dropped_while_sending") + .replace("{command}", "/exit") + ) + }) } async fn handle_session_update(&self, _foreground: &ModelSpawner) -> Result<()> { @@ -598,20 +603,32 @@ async fn upload_transcript( ) -> Result<()> { log::info!("Uploading Claude Code transcript to conversation {conversation_id}"); - let config_dir = claude_config_dir().context("Failed to resolve Claude config dir")?; + let config_dir = claude_config_dir().context(i18n::t( + "ai.agent_sdk.driver.harness.claude.resolve_config_dir_failed", + ))?; let working_dir = working_dir.to_path_buf(); let body = tokio::task::spawn_blocking(move || { - let mut envelope = read_envelope(session_id, &working_dir, &config_dir) - .with_context(|| format!("Failed to read transcript for session {session_id}"))?; + let mut envelope = + read_envelope(session_id, &working_dir, &config_dir).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.claude.read_transcript_for_session_failed") + .replace("{session_id}", &session_id.to_string()) + })?; envelope.claude_version = claude_version; - serde_json::to_vec(&envelope).context("Failed to serialize transcript envelope") + serde_json::to_vec(&envelope).context(i18n::t( + "ai.agent_sdk.driver.harness.claude.serialize_transcript_envelope_failed", + )) }) .await - .context("read_envelope task panicked")??; + .context(i18n::t( + "ai.agent_sdk.driver.harness.read_envelope_task_panicked", + ))??; let target = client .get_transcript_upload_target(&conversation_id) .await - .with_context(|| format!("Failed to get transcript upload target for {conversation_id}"))?; + .with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.get_transcript_upload_target_failed") + .replace("{conversation_id}", &conversation_id.to_string()) + })?; upload_to_target(client.http_client(), &target, body).await } pub(crate) fn prepare_claude_environment_config( @@ -636,7 +653,7 @@ fn claude_global_config_path() -> Result { home_dir_for_claude_config() .map(|home| home.join(CLAUDE_JSON_FILE_NAME)) - .ok_or_else(|| anyhow::anyhow!("could not determine home directory")) + .ok_or_else(|| anyhow::anyhow!(i18n::t("ai.agent_sdk.driver.harness.home_dir_missing"))) } fn prepare_claude_config( @@ -663,7 +680,7 @@ fn prepare_claude_config( write_json_file( claude_json_path, &claude_config, - "Failed to serialize Claude config", + i18n::t("ai.agent_sdk.driver.harness.claude.serialize_config_failed"), )?; Ok(()) } @@ -674,7 +691,7 @@ fn prepare_claude_settings(claude_settings_path: &Path) -> Result<()> { write_json_file( claude_settings_path, &settings, - "Failed to serialize Claude settings", + i18n::t("ai.agent_sdk.driver.harness.claude.serialize_settings_failed"), )?; Ok(()) } @@ -818,7 +835,9 @@ pub(crate) fn serialize_claude_mcp_config( }) .collect(), }; - serde_json::to_string_pretty(&config).context("Failed to serialize Claude MCP config") + serde_json::to_string_pretty(&config).context(i18n::t( + "ai.agent_sdk.driver.harness.claude.serialize_mcp_config_failed", + )) } #[cfg(test)] diff --git a/app/src/ai/agent_sdk/driver/harness/claude_code/parent_bridge.rs b/app/src/ai/agent_sdk/driver/harness/claude_code/parent_bridge.rs index 855b8c59d7..e980bd0725 100644 --- a/app/src/ai/agent_sdk/driver/harness/claude_code/parent_bridge.rs +++ b/app/src/ai/agent_sdk/driver/harness/claude_code/parent_bridge.rs @@ -302,10 +302,14 @@ pub(super) fn parent_bridge_surfaced_message_path( } pub(super) fn ensure_parent_bridge_state_dir(state_dir: &Path) -> Result<()> { - fs::create_dir_all(parent_bridge_staged_dir(state_dir)) - .with_context(|| format!("Failed to create {}", state_dir.display()))?; - fs::create_dir_all(parent_bridge_surfaced_dir(state_dir)) - .with_context(|| format!("Failed to create {}", state_dir.display()))?; + fs::create_dir_all(parent_bridge_staged_dir(state_dir)).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.create_dir_failed") + .replace("{path}", &state_dir.display().to_string()) + })?; + fs::create_dir_all(parent_bridge_surfaced_dir(state_dir)).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.create_dir_failed") + .replace("{path}", &state_dir.display().to_string()) + })?; Ok(()) } @@ -316,9 +320,15 @@ pub(super) fn read_parent_bridge_event_cursor(state_dir: &Path) -> Result { } let cursor = serde_json::from_slice::( - &fs::read(&path).with_context(|| format!("Failed to read {}", path.display()))?, + &fs::read(&path).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.read_failed") + .replace("{path}", &path.display().to_string()) + })?, ) - .with_context(|| format!("Failed to parse {}", path.display()))?; + .with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.parse_failed") + .replace("{path}", &path.display().to_string()) + })?; Ok(cursor.since_sequence) } @@ -406,7 +416,10 @@ fn parent_bridge_sorted_message_paths(dir: &Path) -> Result> { } let mut paths = fs::read_dir(dir) - .with_context(|| format!("Failed to read {}", dir.display()))? + .with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.read_failed") + .replace("{path}", &dir.display().to_string()) + })? .filter_map(|entry| entry.ok().map(|entry| entry.path())) .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("json")) .collect::>(); @@ -419,9 +432,15 @@ fn parent_bridge_message_records(dir: &Path) -> Result( - &fs::read(&path).with_context(|| format!("Failed to read {}", path.display()))?, + &fs::read(&path).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.read_failed") + .replace("{path}", &path.display().to_string()) + })?, ) - .with_context(|| format!("Failed to parse {}", path.display()))?; + .with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.parse_failed") + .replace("{path}", &path.display().to_string()) + })?; Ok((path, record)) }) .collect() @@ -516,9 +535,10 @@ fn remove_file_if_exists(path: &Path) -> Result<()> { match fs::remove_file(path) { Ok(()) => Ok(()), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(err) => { - Err(anyhow::Error::from(err).context(format!("Failed to remove {}", path.display()))) - } + Err(err) => Err(anyhow::Error::from(err).context( + i18n::t("ai.agent_sdk.driver.harness.parent_bridge.remove_file_failed") + .replace("{path}", &path.display().to_string()), + )), } } @@ -533,7 +553,10 @@ async fn hydrate_parent_bridge_message_record( let message = hydrator .read_message_with_timeout(&record.message_id) .await - .with_context(|| format!("Failed to read lead-agent message {}", record.message_id))?; + .with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.parent_bridge.read_lead_agent_message_failed") + .replace("{message_id}", &record.message_id) + })?; Ok(MessageBridgeMessageRecord { sequence: record.sequence, message_id: message.message_id, @@ -733,28 +756,44 @@ fn write_parent_bridge_json_atomically(path: &Path, value: &T) -> fn write_parent_bridge_bytes_atomically(path: &Path, bytes: &[u8]) -> Result<()> { let Some(parent) = path.parent() else { - return Err(anyhow!("{} has no parent directory", path.display())); + return Err(anyhow!(i18n::t( + "ai.agent_sdk.driver.harness.parent_bridge.no_parent_dir" + ) + .replace("{path}", &path.display().to_string()))); }; - fs::create_dir_all(parent).with_context(|| format!("Failed to create {}", parent.display()))?; + fs::create_dir_all(parent).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.create_dir_failed") + .replace("{path}", &parent.display().to_string()) + })?; let prefix = path .file_name() .and_then(|name| name.to_str()) .unwrap_or("parent-bridge"); - let mut temp_file = NamedTempFile::new_in(parent) - .with_context(|| format!("Failed to create temp file for {}", path.display()))?; - temp_file - .write_all(bytes) - .with_context(|| format!("Failed to write temp file for {}", path.display()))?; - temp_file - .flush() - .with_context(|| format!("Failed to flush temp file for {}", path.display()))?; + let mut temp_file = NamedTempFile::new_in(parent).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.parent_bridge.create_temp_file_failed") + .replace("{path}", &path.display().to_string()) + })?; + temp_file.write_all(bytes).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.parent_bridge.write_temp_file_failed") + .replace("{path}", &path.display().to_string()) + })?; + temp_file.flush().with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.parent_bridge.flush_temp_file_failed") + .replace("{path}", &path.display().to_string()) + })?; temp_file .persist(path) .map(|_| ()) .map_err(|err| { - anyhow::Error::from(err.error).context(format!("Failed to write {}", path.display())) + anyhow::Error::from(err.error).context( + i18n::t("ai.agent_sdk.driver.harness.json.write_failed") + .replace("{path}", &path.display().to_string()), + ) }) - .with_context(|| format!("Failed to persist temporary {prefix} file"))?; + .with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.parent_bridge.persist_temp_file_failed") + .replace("{prefix}", prefix) + })?; Ok(()) } diff --git a/app/src/ai/agent_sdk/driver/harness/claude_code/wake_driver.rs b/app/src/ai/agent_sdk/driver/harness/claude_code/wake_driver.rs index 47e4879bce..9e56bf8b56 100644 --- a/app/src/ai/agent_sdk/driver/harness/claude_code/wake_driver.rs +++ b/app/src/ai/agent_sdk/driver/harness/claude_code/wake_driver.rs @@ -157,14 +157,21 @@ impl ClaudeHarness { }, ) .await - .with_context(|| format!("Failed to resolve Claude wake prompt for task {task_id}"))?; + .with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.claude.wake.resolve_prompt_failed") + .replace("{task_id}", &task_id.to_string()) + })?; let bytes = server_api .fetch_transcript_for_task(&task_id) .await - .with_context(|| format!("Failed to fetch Claude transcript for task {task_id}"))?; + .with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.claude.wake.fetch_transcript_failed") + .replace("{task_id}", &task_id.to_string()) + })?; let envelope: ClaudeTranscriptEnvelope = serde_json::from_slice(&bytes).with_context(|| { - format!("Failed to deserialize Claude transcript for wake task {task_id}") + i18n::t("ai.agent_sdk.driver.harness.claude.wake.deserialize_transcript_failed") + .replace("{task_id}", &task_id.to_string()) })?; let wake_prompt = match resolved.resumption_prompt { Some(resumption_prompt) if !resumption_prompt.is_empty() => { @@ -192,13 +199,17 @@ impl ClaudeHarness { wake_message: Option, ) -> Result { let working_dir = working_dir.unwrap_or_else(|| remote.envelope.cwd.clone()); - prepare_claude_environment_config(&working_dir, &HashMap::new()) - .context("Failed to prepare Claude environment for wake")?; + prepare_claude_environment_config(&working_dir, &HashMap::new()).context(i18n::t( + "ai.agent_sdk.driver.harness.claude.wake.prepare_environment_failed", + ))?; remote.envelope.cwd = working_dir.clone(); - let config_root = claude_config_dir().context("Failed to resolve Claude config dir")?; - write_envelope(&remote.envelope, &config_root) - .context("Failed to rehydrate Claude transcript for wake")?; + let config_root = claude_config_dir().context(i18n::t( + "ai.agent_sdk.driver.harness.claude.resolve_config_dir_failed", + ))?; + write_envelope(&remote.envelope, &config_root).context(i18n::t( + "ai.agent_sdk.driver.harness.claude.wake.rehydrate_transcript_failed", + ))?; if let Err(error) = write_session_index_entry(remote.session_id, &working_dir, &config_root) { log::warn!("Failed to update Claude sessions-index.json for wake: {error:#}"); @@ -214,8 +225,10 @@ impl ClaudeHarness { ) .await?; let prompt_path = state_dir.join(CLAUDE_WAKE_PROMPT_FILE_NAME); - std::fs::write(&prompt_path, remote.wake_prompt.as_bytes()) - .with_context(|| format!("Failed to write {}", prompt_path.display()))?; + std::fs::write(&prompt_path, remote.wake_prompt.as_bytes()).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.write_failed") + .replace("{path}", &prompt_path.display().to_string()) + })?; let command = claude_command( CLIAgent::Claude.command_prefix(), diff --git a/app/src/ai/agent_sdk/driver/harness/claude_transcript.rs b/app/src/ai/agent_sdk/driver/harness/claude_transcript.rs index addc8bbfeb..648f63a38d 100644 --- a/app/src/ai/agent_sdk/driver/harness/claude_transcript.rs +++ b/app/src/ai/agent_sdk/driver/harness/claude_transcript.rs @@ -86,7 +86,7 @@ pub(crate) fn claude_config_dir() -> Result { } home_dir_for_claude_config() .map(|h| h.join(".claude")) - .ok_or_else(|| anyhow::anyhow!("could not determine home directory")) + .ok_or_else(|| anyhow::anyhow!(i18n::t("ai.agent_sdk.driver.harness.home_dir_missing"))) } /// In tests on Windows, `dirs::home_dir()` ignores `HOME`, so we check it @@ -128,9 +128,10 @@ pub(crate) fn read_envelope( .join(session_uuid.to_string()) .join("subagents"); if subagents_dir.is_dir() { - for entry in std::fs::read_dir(&subagents_dir) - .with_context(|| format!("Failed to read subagents dir {}", subagents_dir.display()))? - { + for entry in std::fs::read_dir(&subagents_dir).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.claude.read_subagents_dir_failed") + .replace("{path}", &subagents_dir.display().to_string()) + })? { let entry = entry?; let path = entry.path(); if path.extension().and_then(|e| e.to_str()) != Some("jsonl") { @@ -148,9 +149,10 @@ pub(crate) fn read_envelope( let todos_dir = config_root.join("todos"); let todos_prefix = format!("{session_uuid}-agent-"); if todos_dir.is_dir() { - for entry in std::fs::read_dir(&todos_dir) - .with_context(|| format!("Failed to read todos dir {}", todos_dir.display()))? - { + for entry in std::fs::read_dir(&todos_dir).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.claude.read_todos_dir_failed") + .replace("{path}", &todos_dir.display().to_string()) + })? { let entry = entry?; let path = entry.path(); if path.extension().and_then(|e| e.to_str()) != Some("json") { @@ -197,37 +199,49 @@ pub(crate) fn write_envelope( ) -> Result<()> { let encoded = encode_cwd(&envelope.cwd); let projects_dir = config_root.join("projects").join(&encoded); - std::fs::create_dir_all(&projects_dir) - .with_context(|| format!("Failed to create {}", projects_dir.display()))?; + std::fs::create_dir_all(&projects_dir).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.create_dir_failed") + .replace("{path}", &projects_dir.display().to_string()) + })?; // Main session JSONL. let session_file = projects_dir.join(format!("{}.jsonl", envelope.uuid)); - std::fs::write(&session_file, entries_to_jsonl(&envelope.entries)?) - .with_context(|| format!("Failed to write {}", session_file.display()))?; + std::fs::write(&session_file, entries_to_jsonl(&envelope.entries)?).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.write_failed") + .replace("{path}", &session_file.display().to_string()) + })?; // Subagent JSONLs. if !envelope.subagents.is_empty() { let subagents_dir = projects_dir .join(envelope.uuid.to_string()) .join("subagents"); - std::fs::create_dir_all(&subagents_dir) - .with_context(|| format!("Failed to create {}", subagents_dir.display()))?; + std::fs::create_dir_all(&subagents_dir).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.create_dir_failed") + .replace("{path}", &subagents_dir.display().to_string()) + })?; for (stem, entries) in &envelope.subagents { let path = subagents_dir.join(format!("{stem}.jsonl")); - std::fs::write(&path, entries_to_jsonl(entries)?) - .with_context(|| format!("Failed to write {}", path.display()))?; + std::fs::write(&path, entries_to_jsonl(entries)?).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.write_failed") + .replace("{path}", &path.display().to_string()) + })?; } } // Per-agent todo lists. if !envelope.todos.is_empty() { let todos_dir = config_root.join("todos"); - std::fs::create_dir_all(&todos_dir) - .with_context(|| format!("Failed to create {}", todos_dir.display()))?; + std::fs::create_dir_all(&todos_dir).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.create_dir_failed") + .replace("{path}", &todos_dir.display().to_string()) + })?; for (stem, value) in &envelope.todos { let path = todos_dir.join(format!("{stem}.json")); - std::fs::write(&path, serde_json::to_vec(value)?) - .with_context(|| format!("Failed to write {}", path.display()))?; + std::fs::write(&path, serde_json::to_vec(value)?).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.write_failed") + .replace("{path}", &path.display().to_string()) + })?; } } @@ -277,9 +291,10 @@ pub(crate) fn write_session_index_entry( }, Err(e) if e.kind() == std::io::ErrorKind::NotFound => serde_json::Map::new(), Err(e) => { - return Err( - anyhow::Error::from(e).context(format!("Failed to read {}", index_path.display())) - ); + return Err(anyhow::Error::from(e).context( + i18n::t("ai.agent_sdk.driver.harness.json.read_failed") + .replace("{path}", &index_path.display().to_string()), + )); } }; @@ -294,15 +309,21 @@ pub(crate) fn write_session_index_entry( index.insert(session_uuid.to_string(), entry); if let Some(parent) = index_path.parent() { - std::fs::create_dir_all(parent) - .with_context(|| format!("Failed to create {}", parent.display()))?; + std::fs::create_dir_all(parent).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.create_dir_failed") + .replace("{path}", &parent.display().to_string()) + })?; } std::fs::write( &index_path, - serde_json::to_vec_pretty(&Value::Object(index)) - .context("Failed to serialize sessions-index.json")?, + serde_json::to_vec_pretty(&Value::Object(index)).context(i18n::t( + "ai.agent_sdk.driver.harness.claude.serialize_sessions_index_failed", + ))?, ) - .with_context(|| format!("Failed to write {}", index_path.display()))?; + .with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.write_failed") + .replace("{path}", &index_path.display().to_string()) + })?; Ok(()) } @@ -315,15 +336,19 @@ pub(crate) fn read_jsonl(path: &Path) -> Result> { Ok(f) => f, Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()), Err(e) => { - return Err( - anyhow::Error::from(e).context(format!("Failed to open {}", path.display())) - ); + return Err(anyhow::Error::from(e).context( + i18n::t("ai.agent_sdk.driver.harness.claude.open_jsonl_failed") + .replace("{path}", &path.display().to_string()), + )); } }; let reader = BufReader::new(file); let mut entries = Vec::new(); for line in reader.lines() { - let line = line.with_context(|| format!("Failed to read line from {}", path.display()))?; + let line = line.with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.claude.read_jsonl_line_failed") + .replace("{path}", &path.display().to_string()) + })?; let trimmed = line.trim(); if trimmed.is_empty() { continue; diff --git a/app/src/ai/agent_sdk/driver/harness/codex.rs b/app/src/ai/agent_sdk/driver/harness/codex.rs index daed142ee7..9762824155 100644 --- a/app/src/ai/agent_sdk/driver/harness/codex.rs +++ b/app/src/ai/agent_sdk/driver/harness/codex.rs @@ -231,14 +231,14 @@ impl CodexHarnessRunner { envelope, }) => { let sessions_root = codex_sessions_root().map_err(|e| { - AgentDriverError::ConfigBuildFailed( - e.context("Failed to resolve codex sessions root"), - ) + AgentDriverError::ConfigBuildFailed(e.context(i18n::t( + "ai.agent_sdk.driver.harness.codex.resolve_sessions_root_failed", + ))) })?; let path = write_envelope(&envelope, &sessions_root).map_err(|e| { - AgentDriverError::ConfigBuildFailed( - e.context("Failed to rehydrate codex transcript"), - ) + AgentDriverError::ConfigBuildFailed(e.context(i18n::t( + "ai.agent_sdk.driver.harness.codex.rehydrate_transcript_failed", + ))) })?; (Some(session_id), Some(conversation_id), Some(path)) } @@ -350,7 +350,12 @@ impl HarnessRunner for CodexHarnessRunner { }); }) .await - .map_err(|_| anyhow::anyhow!("Agent driver dropped while sending /exit")) + .map_err(|_| { + anyhow::anyhow!( + i18n::t("ai.agent_sdk.driver.harness.driver_dropped_while_sending") + .replace("{command}", "/exit") + ) + }) } /// Capture the codex session ID from the `SessionStart` event picked up by the `CLIAgentSessionsModel`. @@ -462,15 +467,22 @@ async fn upload_transcript( let entries = read_jsonl(&transcript_path)?; let metadata = parse_session_meta(entries.first()).unwrap_or_default(); let envelope = CodexTranscriptEnvelope::new(session_id, metadata, entries); - serde_json::to_vec(&envelope).context("Failed to serialize codex transcript") + serde_json::to_vec(&envelope).context(i18n::t( + "ai.agent_sdk.driver.harness.codex.serialize_transcript_failed", + )) }) .await - .context("read_envelope task panicked")??; + .context(i18n::t( + "ai.agent_sdk.driver.harness.read_envelope_task_panicked", + ))??; let target = client .get_transcript_upload_target(&conversation_id) .await - .with_context(|| format!("Failed to get transcript upload target for {conversation_id}"))?; + .with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.get_transcript_upload_target_failed") + .replace("{conversation_id}", &conversation_id.to_string()) + })?; upload_to_target(client.http_client(), &target, body).await?; Ok(()) } @@ -541,25 +553,21 @@ fn codex_config_dir() -> Result { } dirs::home_dir() .map(|home| home.join(CODEX_CONFIG_DIR)) - .ok_or_else(|| anyhow::anyhow!("could not determine home directory")) + .ok_or_else(|| anyhow::anyhow!(i18n::t("ai.agent_sdk.driver.harness.home_dir_missing"))) } fn write_codex_agents_override(codex_dir: &Path, system_prompt: &str) -> Result<()> { fs::create_dir_all(codex_dir).with_context(|| { - format!( - "Failed to create Codex config dir at {}", - codex_dir.display() - ) + i18n::t("ai.agent_sdk.driver.harness.codex.create_config_dir_failed") + .replace("{path}", &codex_dir.display().to_string()) })?; // Note: this currently works because we are only doing this for cloud agents; if we enable // this for local runs we'll want to make sure we don't clobber any existing file overrides. let prompt_path = codex_dir.join(CODEX_AGENTS_OVERRIDE_FILE_NAME); fs::write(&prompt_path, system_prompt).with_context(|| { - format!( - "Failed to write Codex system prompt to {}", - prompt_path.display() - ) + i18n::t("ai.agent_sdk.driver.harness.codex.write_system_prompt_failed") + .replace("{path}", &prompt_path.display().to_string()) }) } @@ -593,10 +601,14 @@ fn prepare_codex_auth(auth_path: &Path, api_key: &str) -> Result<()> { /// codex sets up this file itself. fn write_codex_auth_json(path: &Path, auth: &CodexAuthDotJson) -> Result<()> { if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("Failed to create {}", parent.display()))?; + fs::create_dir_all(parent).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.create_dir_failed") + .replace("{path}", &parent.display().to_string()) + })?; } - let bytes = serde_json::to_vec_pretty(auth).context("Failed to serialize Codex auth.json")?; + let bytes = serde_json::to_vec_pretty(auth).context(i18n::t( + "ai.agent_sdk.driver.harness.codex.serialize_auth_json_failed", + ))?; #[cfg(unix)] { @@ -608,14 +620,25 @@ fn write_codex_auth_json(path: &Path, auth: &CodexAuthDotJson) -> Result<()> { .truncate(true) .mode(0o600) .open(path) - .with_context(|| format!("Failed to open {} for writing", path.display()))?; + .with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.codex.open_auth_json_failed") + .replace("{path}", &path.display().to_string()) + })?; file.set_permissions(fs::Permissions::from_mode(0o600)) - .with_context(|| format!("Failed to set permissions on {}", path.display()))?; - file.write_all(&bytes) - .with_context(|| format!("Failed to write {}", path.display()))?; + .with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.codex.set_auth_permissions_failed") + .replace("{path}", &path.display().to_string()) + })?; + file.write_all(&bytes).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.write_failed") + .replace("{path}", &path.display().to_string()) + })?; } #[cfg(not(unix))] - fs::write(path, &bytes).with_context(|| format!("Failed to write {}", path.display()))?; + fs::write(path, &bytes).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.write_failed") + .replace("{path}", &path.display().to_string()) + })?; Ok(()) } @@ -737,14 +760,13 @@ fn prepare_codex_config_toml( if let Some(parent) = config_toml_path.parent() { fs::create_dir_all(parent).with_context(|| { - format!("Failed to create Codex config dir at {}", parent.display()) + i18n::t("ai.agent_sdk.driver.harness.codex.create_config_dir_failed") + .replace("{path}", &parent.display().to_string()) })?; } fs::write(config_toml_path, doc.to_string()).with_context(|| { - format!( - "Failed to write Codex config.toml at {}", - config_toml_path.display() - ) + i18n::t("ai.agent_sdk.driver.harness.codex.write_config_toml_failed") + .replace("{path}", &config_toml_path.display().to_string()) }) } diff --git a/app/src/ai/agent_sdk/driver/harness/codex_transcript.rs b/app/src/ai/agent_sdk/driver/harness/codex_transcript.rs index fa630f6ab7..05921ce56b 100644 --- a/app/src/ai/agent_sdk/driver/harness/codex_transcript.rs +++ b/app/src/ai/agent_sdk/driver/harness/codex_transcript.rs @@ -90,7 +90,9 @@ pub(crate) fn codex_sessions_root() -> anyhow::Result { PathBuf::from(dir) } else { dirs::home_dir() - .ok_or_else(|| anyhow::anyhow!("could not determine home directory"))? + .ok_or_else(|| { + anyhow::anyhow!(i18n::t("ai.agent_sdk.driver.harness.home_dir_missing")) + })? .join(CODEX_HOME_DIRNAME) }; Ok(home.join(CODEX_SESSIONS_SUBDIR)) @@ -174,8 +176,10 @@ pub(crate) fn write_envelope( .join(format!("{:04}", timestamp.year())) .join(format!("{:02}", timestamp.month())) .join(format!("{:02}", timestamp.day())); - fs::create_dir_all(&day_dir) - .with_context(|| format!("Failed to create {}", day_dir.display()))?; + fs::create_dir_all(&day_dir).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.create_dir_failed") + .replace("{path}", &day_dir.display().to_string()) + })?; // Codex's filename format: `[year]-[month]-[day]T[hour]-[minute]-[second]` // (codex `rollout/src/recorder.rs::precompute_log_file_info`). let date_str = timestamp.format("%Y-%m-%dT%H-%M-%S").to_string(); @@ -183,8 +187,10 @@ pub(crate) fn write_envelope( "rollout-{date_str}-{session_id}.jsonl", session_id = envelope.session_id )); - fs::write(&file_path, entries_to_jsonl(&envelope.entries)?) - .with_context(|| format!("Failed to write {}", file_path.display()))?; + fs::write(&file_path, entries_to_jsonl(&envelope.entries)?).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.write_failed") + .replace("{path}", &file_path.display().to_string()) + })?; Ok(file_path) } diff --git a/app/src/ai/agent_sdk/driver/harness/gemini.rs b/app/src/ai/agent_sdk/driver/harness/gemini.rs index 26984c4862..d3ea59f30b 100644 --- a/app/src/ai/agent_sdk/driver/harness/gemini.rs +++ b/app/src/ai/agent_sdk/driver/harness/gemini.rs @@ -199,7 +199,12 @@ impl HarnessRunner for GeminiHarnessRunner { }); }) .await - .map_err(|_| anyhow::anyhow!("Agent driver dropped while sending /quit")) + .map_err(|_| { + anyhow::anyhow!( + i18n::t("ai.agent_sdk.driver.harness.driver_dropped_while_sending") + .replace("{command}", "/quit") + ) + }) } async fn save_conversation( @@ -249,8 +254,8 @@ fn prepare_gemini_environment_config( working_dir: &Path, system_prompt: Option<&str>, ) -> Result<()> { - let home_dir = - dirs::home_dir().ok_or_else(|| anyhow::anyhow!("could not determine home directory"))?; + let home_dir = dirs::home_dir() + .ok_or_else(|| anyhow::anyhow!(i18n::t("ai.agent_sdk.driver.harness.home_dir_missing")))?; let gemini_dir = home_dir.join(GEMINI_CONFIG_DIR); prepare_gemini_settings( &gemini_dir.join(GEMINI_SETTINGS_FILE_NAME), @@ -263,10 +268,8 @@ fn prepare_gemini_environment_config( if let Some(prompt) = system_prompt { let prompt_path = gemini_dir.join(GEMINI_SYSTEM_PROMPT_FILE_NAME); std::fs::write(&prompt_path, prompt).with_context(|| { - format!( - "Failed to write Gemini system prompt to {}", - prompt_path.display() - ) + i18n::t("ai.agent_sdk.driver.harness.gemini.write_system_prompt_failed") + .replace("{path}", &prompt_path.display().to_string()) })?; } Ok(()) @@ -292,7 +295,7 @@ fn prepare_gemini_settings(settings_path: &Path, has_system_prompt: bool) -> Res write_json_file( settings_path, &settings, - "Failed to serialize Gemini settings", + i18n::t("ai.agent_sdk.driver.harness.gemini.serialize_settings_failed"), ) } @@ -305,7 +308,7 @@ fn prepare_gemini_trusted_folders(trusted_path: &Path, working_dir: &Path) -> Re write_json_file( trusted_path, &trusted, - "Failed to serialize Gemini trusted folders", + i18n::t("ai.agent_sdk.driver.harness.gemini.serialize_trusted_folders_failed"), ) } diff --git a/app/src/ai/agent_sdk/driver/harness/json_utils.rs b/app/src/ai/agent_sdk/driver/harness/json_utils.rs index 5385efd8c6..d9cd44358b 100644 --- a/app/src/ai/agent_sdk/driver/harness/json_utils.rs +++ b/app/src/ai/agent_sdk/driver/harness/json_utils.rs @@ -25,12 +25,16 @@ where return Ok(T::default()); } Err(e) => { - return Err( - anyhow::Error::from(e).context(format!("Failed to read {}", path.display())) - ); + return Err(anyhow::Error::from(e).context( + i18n::t("ai.agent_sdk.driver.harness.json.read_failed") + .replace("{path}", &path.display().to_string()), + )); } }; - serde_json::from_str(&content).with_context(|| format!("Failed to parse {}", path.display())) + serde_json::from_str(&content).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.parse_failed") + .replace("{path}", &path.display().to_string()) + }) } /// Serialize `value` as pretty JSON and write it to `path`, creating parent @@ -40,20 +44,26 @@ where pub(super) fn write_json_file( path: &Path, value: &T, - serialize_error: &'static str, + serialize_error: impl Into, ) -> Result<()> where T: Serialize, { + let serialize_error = serialize_error.into(); if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent) - .with_context(|| format!("Failed to create {}", parent.display()))?; + std::fs::create_dir_all(parent).with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.create_dir_failed") + .replace("{path}", &parent.display().to_string()) + })?; } std::fs::write( path, serde_json::to_vec_pretty(value).context(serialize_error)?, ) - .with_context(|| format!("Failed to write {}", path.display())) + .with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.json.write_failed") + .replace("{path}", &path.display().to_string()) + }) } /// Serialize a slice of JSON values as a JSONL byte string (one value per line). diff --git a/app/src/ai/agent_sdk/driver/harness/mod.rs b/app/src/ai/agent_sdk/driver/harness/mod.rs index 60993a9c66..cb7a423556 100644 --- a/app/src/ai/agent_sdk/driver/harness/mod.rs +++ b/app/src/ai/agent_sdk/driver/harness/mod.rs @@ -563,14 +563,18 @@ pub(super) fn write_temp_file( .suffix(suffix) .tempfile() .map_err(|e| { - AgentDriverError::ConfigBuildFailed(anyhow::anyhow!( - "Failed to create temp file '{prefix}': {e}" - )) + AgentDriverError::ConfigBuildFailed(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.driver.harness.create_temp_file_failed" + ) + .replace("{prefix}", prefix) + .replace("{error}", &e.to_string()))) })?; file.write_all(content.as_bytes()).map_err(|e| { - AgentDriverError::ConfigBuildFailed(anyhow::anyhow!( - "Failed to write temp file '{prefix}': {e}" - )) + AgentDriverError::ConfigBuildFailed(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.driver.harness.write_temp_file_failed" + ) + .replace("{prefix}", prefix) + .replace("{error}", &e.to_string()))) })?; Ok(file) } @@ -586,12 +590,14 @@ pub(crate) async fn upload_block_snapshot( .get_block_snapshot_upload_target(&conversation_id) .await .with_context(|| { - format!("Unable to get block upload slot for conversation {conversation_id}") + i18n::t("ai.agent_sdk.driver.harness.get_block_upload_slot_failed") + .replace("{conversation_id}", &conversation_id.to_string()) })?; - let body = block - .to_json() - .with_context(|| format!("Unable to serialize block for conversation {conversation_id}"))?; + let body = block.to_json().with_context(|| { + i18n::t("ai.agent_sdk.driver.harness.serialize_block_failed") + .replace("{conversation_id}", &conversation_id.to_string()) + })?; upload_to_target(client.http_client(), &target, body).await } @@ -610,7 +616,7 @@ pub(super) async fn upload_current_block_snapshot( let snapshot = foreground .spawn(move |_, ctx| td.as_ref(ctx).block_snapshot(&block_id, ctx)) .await - .map_err(|_| anyhow::anyhow!("Agent driver dropped"))?; + .map_err(|_| anyhow::anyhow!(i18n::t("ai.agent_sdk.driver.harness.driver_dropped")))?; match snapshot { Some(block) => upload_block_snapshot(client, conversation_id, block).await, None => { diff --git a/app/src/ai/agent_sdk/driver/output.rs b/app/src/ai/agent_sdk/driver/output.rs index 2f8454ddf2..5f3ab575f4 100644 --- a/app/src/ai/agent_sdk/driver/output.rs +++ b/app/src/ai/agent_sdk/driver/output.rs @@ -3,8 +3,6 @@ pub mod text { use std::fmt; use std::io::{self, Write}; - const CANCELLED_MESSAGE: &str = ""; - use ai::agent::action_result::{FetchConversationResult, ReadSkillResult, UseComputerResult}; use itertools::Itertools; @@ -53,34 +51,61 @@ pub mod text { output, exit_code, .. - } => writeln!(w, "{output}\n\n (`{command}` exited with code {exit_code})"), + } => writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.command.completed") + .replace("{output}", output) + .replace("{command}", command) + .replace("{exit_code}", &exit_code.to_string()) + ), RequestCommandOutputResult::LongRunningCommandSnapshot { command, .. } => { - writeln!(w, "`{command}` is still running...") + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.command.long_running") + .replace("{command}", command) + ) } RequestCommandOutputResult::CancelledBeforeExecution => { - writeln!(w, "{CANCELLED_MESSAGE}") + writeln!(w, "{}", i18n::t("ai.agent_sdk.driver.output.cancelled")) } RequestCommandOutputResult::Denylisted { .. } => { writeln!( w, - "Command was not allowed to run due to presence on denylist" + "{}", + i18n::t("ai.agent_sdk.driver.output.command.denylisted") ) } }, AIAgentActionResultType::WriteToLongRunningShellCommand(result) => match result { WriteToLongRunningShellCommandResult::Snapshot { .. } => { - writeln!(w, "Command is still running...") + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.command.still_running") + ) } WriteToLongRunningShellCommandResult::CommandFinished { output, exit_code, .. - } => writeln!(w, "{output}\n\n (exited with code {exit_code})"), + } => writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.command.finished") + .replace("{output}", output) + .replace("{exit_code}", &exit_code.to_string()) + ), WriteToLongRunningShellCommandResult::Cancelled => { - writeln!(w, "{CANCELLED_MESSAGE}") + writeln!(w, "{}", i18n::t("ai.agent_sdk.driver.output.cancelled")) } WriteToLongRunningShellCommandResult::Error(_) => { - writeln!(w, "Failed to write to command.") + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.command.write_failed") + ) } }, AIAgentActionResultType::RequestFileEdits(result) => match result { @@ -92,22 +117,36 @@ pub mod text { } => { writeln!( w, - "Updated {} files, deleted {} files:\n```diff\n{diff}\n```", - updated_files.len(), - deleted_files.len() + "{}", + i18n::t("ai.agent_sdk.driver.output.file_edits.updated_deleted") + .replace("{updated_count}", &updated_files.len().to_string()) + .replace("{deleted_count}", &deleted_files.len().to_string()) + .replace("{diff}", diff) ) } RequestFileEditsResult::Cancelled => { - writeln!(w, "{CANCELLED_MESSAGE}") + writeln!(w, "{}", i18n::t("ai.agent_sdk.driver.output.cancelled")) } RequestFileEditsResult::DiffApplicationFailed { error } => { - writeln!(w, "Editing files failed: {error}") + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.file_edits.failed") + .replace("{error}", error) + ) } }, AIAgentActionResultType::ReadFiles(result) => match result { ReadFilesResult::Success { .. } => Ok(()), - ReadFilesResult::Error(error) => writeln!(w, "Reading files failed: {error}"), - ReadFilesResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), + ReadFilesResult::Error(error) => writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.read_files.failed") + .replace("{error}", error) + ), + ReadFilesResult::Cancelled => { + writeln!(w, "{}", i18n::t("ai.agent_sdk.driver.output.cancelled")) + } }, AIAgentActionResultType::UploadArtifact(result) => match result { UploadArtifactResult::Success { @@ -116,25 +155,52 @@ pub mod text { .. } => match filepath { Some(filepath) => { - writeln!(w, "Uploaded artifact {artifact_uid} from {filepath}") + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.artifact.uploaded_from") + .replace("{artifact_uid}", artifact_uid) + .replace("{filepath}", filepath) + ) } - None => writeln!(w, "Uploaded artifact {artifact_uid}"), + None => writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.artifact.uploaded") + .replace("{artifact_uid}", artifact_uid) + ), }, UploadArtifactResult::Error(error) => { - writeln!(w, "Uploading artifact failed: {error}") + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.artifact.upload_failed") + .replace("{error}", error) + ) + } + UploadArtifactResult::Cancelled => { + writeln!(w, "{}", i18n::t("ai.agent_sdk.driver.output.cancelled")) } - UploadArtifactResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), }, AIAgentActionResultType::SearchCodebase(result) => match result { SearchCodebaseResult::Success { files } => { - writeln!(w, "Codebase search results:")?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.codebase.search_results") + )?; for file in files { writeln!(w, "- {file}")?; } Ok(()) } SearchCodebaseResult::Failed { message, .. } => { - writeln!(w, "Searching codebase failed: {message}") + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.codebase.search_failed") + .replace("{message}", message) + ) } SearchCodebaseResult::Cancelled => todo!(), }, @@ -145,13 +211,25 @@ pub mod text { } Ok(()) } - GrepResult::Error(error) => writeln!(w, "grep failed: {error}"), - GrepResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), + GrepResult::Error(error) => writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.grep.failed").replace("{error}", error) + ), + GrepResult::Cancelled => { + writeln!(w, "{}", i18n::t("ai.agent_sdk.driver.output.cancelled")) + } }, AIAgentActionResultType::FileGlob(result) => match result { FileGlobResult::Success { matched_files } => writeln!(w, "{matched_files}"), - FileGlobResult::Error(error) => writeln!(w, "find failed: {error}"), - FileGlobResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), + FileGlobResult::Error(error) => writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.find.failed").replace("{error}", error) + ), + FileGlobResult::Cancelled => { + writeln!(w, "{}", i18n::t("ai.agent_sdk.driver.output.cancelled")) + } }, AIAgentActionResultType::FileGlobV2(result) => match result { FileGlobV2Result::Success { matched_files, .. } => { @@ -160,8 +238,14 @@ pub mod text { } Ok(()) } - FileGlobV2Result::Error(error) => writeln!(w, "find failed: {error}"), - FileGlobV2Result::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), + FileGlobV2Result::Error(error) => writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.find.failed").replace("{error}", error) + ), + FileGlobV2Result::Cancelled => { + writeln!(w, "{}", i18n::t("ai.agent_sdk.driver.output.cancelled")) + } }, AIAgentActionResultType::ReadMCPResource(result) => match result { ReadMCPResourceResult::Success { resource_contents } => { @@ -192,9 +276,16 @@ pub mod text { Ok(()) } ReadMCPResourceResult::Error(error) => { - writeln!(w, "Reading MCP resource failed: {error}") + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.mcp.read_resource_failed") + .replace("{error}", error) + ) + } + ReadMCPResourceResult::Cancelled => { + writeln!(w, "{}", i18n::t("ai.agent_sdk.driver.output.cancelled")) } - ReadMCPResourceResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), }, AIAgentActionResultType::CallMCPTool(result) => { match result { @@ -206,7 +297,12 @@ pub mod text { writeln!(w, "{}", text_content.text)?; } rmcp::model::RawContent::Image(image_content) => { - writeln!(w, "{} image", image_content.mime_type)?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.mcp.image") + .replace("{mime_type}", &image_content.mime_type) + )?; } rmcp::model::RawContent::Resource(embedded_resource) => { match &embedded_resource.resource { @@ -228,7 +324,12 @@ pub mod text { }; } rmcp::model::RawContent::Audio(audio_content) => { - writeln!(w, "{} audio", audio_content.mime_type)?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.mcp.audio") + .replace("{mime_type}", &audio_content.mime_type) + )?; } rmcp::model::RawContent::ResourceLink(raw_resource) => { let rmcp::model::RawResource { @@ -248,26 +349,51 @@ pub mod text { Ok(()) } CallMCPToolResult::Error(error) => { - writeln!(w, "Calling MCP tool failed: {error}") + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.mcp.call_tool_failed") + .replace("{error}", error) + ) + } + CallMCPToolResult::Cancelled => { + writeln!(w, "{}", i18n::t("ai.agent_sdk.driver.output.cancelled")) } - CallMCPToolResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), } } AIAgentActionResultType::ReadSkill(result) => match result { ReadSkillResult::Success { content } => { - writeln!(w, "Skill read successfully: {}", content.file_name) + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.skill.read_success") + .replace("{file_name}", &content.file_name) + ) + } + ReadSkillResult::Error(error) => { + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.skill.read_error") + .replace("{error}", error) + ) + } + ReadSkillResult::Cancelled => { + writeln!(w, "{}", i18n::t("ai.agent_sdk.driver.output.cancelled")) } - ReadSkillResult::Error(error) => writeln!(w, "Skill read error: {error}"), - ReadSkillResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), }, AIAgentActionResultType::SuggestNewConversation(result) => match result { SuggestNewConversationResult::Accepted { .. } | SuggestNewConversationResult::Rejected => Ok(()), - SuggestNewConversationResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), + SuggestNewConversationResult::Cancelled => { + writeln!(w, "{}", i18n::t("ai.agent_sdk.driver.output.cancelled")) + } }, AIAgentActionResultType::SuggestPrompt(result) => match result { SuggestPromptResult::Accepted { .. } => Ok(()), - SuggestPromptResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), + SuggestPromptResult::Cancelled => { + writeln!(w, "{}", i18n::t("ai.agent_sdk.driver.output.cancelled")) + } }, AIAgentActionResultType::OpenCodeReview => Ok(()), AIAgentActionResultType::InsertReviewComments(_) => Ok(()), @@ -281,19 +407,38 @@ pub mod text { AIAgentActionResultType::UseComputer(result) => match result { // TODO(AGENT-2281): implement UseComputerResult::Success(_result) => Ok(()), - UseComputerResult::Error(error) => writeln!(w, "Use computer error: {error}"), - UseComputerResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), + UseComputerResult::Error(error) => writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.use_computer.error") + .replace("{error}", error) + ), + UseComputerResult::Cancelled => { + writeln!(w, "{}", i18n::t("ai.agent_sdk.driver.output.cancelled")) + } }, // TODO(AGENT-2281): implement AIAgentActionResultType::RequestComputerUse(_result) => Ok(()), AIAgentActionResultType::FetchConversation(result) => match result { FetchConversationResult::Success { directory_path } => { - writeln!(w, "Fetched conversation to {directory_path}") + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.fetch_conversation.success") + .replace("{directory_path}", directory_path) + ) } FetchConversationResult::Error(error) => { - writeln!(w, "Fetch conversation error: {error}") + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.fetch_conversation.error") + .replace("{error}", error) + ) + } + FetchConversationResult::Cancelled => { + writeln!(w, "{}", i18n::t("ai.agent_sdk.driver.output.cancelled")) } - FetchConversationResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), }, // StartAgent is a client-side orchestration action, not used in SDK AIAgentActionResultType::StartAgent(_) => Ok(()), @@ -316,35 +461,61 @@ pub mod text { } AIAgentOutputMessageType::Action(action) => match &action.action { AIAgentActionType::RequestCommandOutput { command, .. } => { - writeln!(w, "Running `{command}`")?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.action.running_command") + .replace("{command}", command) + )?; } AIAgentActionType::WriteToLongRunningShellCommand { input, .. } => { - writeln!(w, "Write {} bytes to command", input.len())?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.action.write_bytes") + .replace("{bytes}", &input.len().to_string()) + )?; } AIAgentActionType::ReadFiles(request) => { + let files = request + .locations + .iter() + .format_with(", ", |loc, f| f(&format_args!("{}", loc.name))) + .to_string(); writeln!( w, - "Reading {}", - request - .locations - .iter() - .format_with(", ", |loc, f| f(&format_args!("{}", loc.name))) + "{}", + i18n::t("ai.agent_sdk.driver.output.action.reading") + .replace("{files}", &files) )?; // TODO: Better formatting, need shell info. } AIAgentActionType::UploadArtifact(request) => { - writeln!(w, "Uploading artifact {}", request.file_path)?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.action.uploading_artifact") + .replace("{file_path}", &request.file_path) + )?; } AIAgentActionType::SearchCodebase(request) => { writeln!( w, - "Searching {} for {}", - request.codebase_path.as_deref().unwrap_or("codebase"), - request.query + "{}", + i18n::t("ai.agent_sdk.driver.output.action.searching_codebase") + .replace( + "{codebase}", + request.codebase_path.as_deref().unwrap_or("codebase") + ) + .replace("{query}", &request.query) )?; } AIAgentActionType::RequestFileEdits { file_edits, title } => { - write!(w, "Editing files:")?; + write!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.action.editing_files") + )?; if let Some(title) = title { write!(w, " {title}")?; } @@ -356,39 +527,71 @@ pub mod text { } } AIAgentActionType::Grep { queries, path } => { - writeln!(w, "Grepping for {} in {path}", format_queries(queries))?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.action.grepping") + .replace("{queries}", &format_queries(queries)) + .replace("{path}", path) + )?; } AIAgentActionType::FileGlob { patterns, path } => { - write!(w, "Finding files matching {}", format_queries(patterns))?; - if let Some(path) = path { - write!(w, " in {path}")?; - } - writeln!(w)?; + let queries = format_queries(patterns); + let message = if let Some(path) = path { + i18n::t("ai.agent_sdk.driver.output.action.finding_files_in_path") + .replace("{queries}", &queries) + .replace("{path}", path) + } else { + i18n::t("ai.agent_sdk.driver.output.action.finding_files") + .replace("{queries}", &queries) + }; + writeln!(w, "{message}")?; } AIAgentActionType::FileGlobV2 { patterns, search_dir, } => { - write!(w, "Finding files matching {}", format_queries(patterns))?; - if let Some(path) = search_dir { - write!(w, " in {path}")?; - } - writeln!(w)?; + let queries = format_queries(patterns); + let message = if let Some(path) = search_dir { + i18n::t("ai.agent_sdk.driver.output.action.finding_files_in_path") + .replace("{queries}", &queries) + .replace("{path}", path) + } else { + i18n::t("ai.agent_sdk.driver.output.action.finding_files") + .replace("{queries}", &queries) + }; + writeln!(w, "{message}")?; } AIAgentActionType::ReadMCPResource { server_id: _, name, uri, } => match uri { - Some(uri) => writeln!(w, "Reading MCP resource {uri}")?, - None => writeln!(w, "Reading MCP resource {name}")?, + Some(uri) => writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.action.reading_mcp_resource") + .replace("{resource}", uri) + )?, + None => writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.action.reading_mcp_resource") + .replace("{resource}", name) + )?, }, AIAgentActionType::CallMCPTool { server_id: _, name, input, } => { - writeln!(w, "MCP tool call {name}({input:#})")?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.action.mcp_tool_call") + .replace("{name}", name) + .replace("{input}", &format!("{input:#}")) + )?; } AIAgentActionType::SuggestNewConversation { .. } => (), AIAgentActionType::SuggestPrompt { .. } => (), @@ -402,27 +605,54 @@ pub mod text { | AIAgentActionType::ReadShellCommandOutput { .. } | AIAgentActionType::TransferShellCommandControlToUser { .. } => (), AIAgentActionType::UseComputer(request) => { - writeln!(w, "Computer use action: {}", request.action_summary)?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.action.computer_use") + .replace("{summary}", &request.action_summary) + )?; } AIAgentActionType::RequestComputerUse(request) => { - writeln!(w, "Requesting computer use: {}", request.task_summary)?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.action.request_computer_use") + .replace("{summary}", &request.task_summary) + )?; } AIAgentActionType::ReadSkill(request) => { - writeln!(w, "Reading skill: {}", request.skill)?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.action.reading_skill") + .replace("{skill}", &request.skill.to_string()) + )?; } AIAgentActionType::FetchConversation { conversation_id } => { - writeln!(w, "Fetching conversation {conversation_id}")?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.action.fetching_conversation") + .replace("{conversation_id}", conversation_id) + )?; } AIAgentActionType::StartAgent { name, .. } => { - writeln!(w, "Starting agent: {name}")?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.action.starting_agent") + .replace("{name}", name) + )?; } AIAgentActionType::SendMessageToAgent { addresses, subject, .. } => { writeln!( w, - "Sending message to [{}]: {subject}", - addresses.join(", ") + "{}", + i18n::t("ai.agent_sdk.driver.output.action.sending_message") + .replace("{addresses}", &addresses.join(", ")) + .replace("{subject}", subject) )?; } AIAgentActionType::AskUserQuestion { .. } => (), @@ -431,11 +661,15 @@ pub mod text { }, AIAgentOutputMessageType::TodoOperation(operation) => match operation { TodoOperation::UpdateTodos { todos } => { - writeln!(w, "Updated TODO list:")?; + writeln!(w, "{}", i18n::t("ai.agent_sdk.driver.output.todo.updated"))?; format_todos(todos, w)?; } TodoOperation::MarkAsCompleted { completed_todos } => { - writeln!(w, "Completed TODOs:")?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.todo.completed") + )?; format_todos(completed_todos, w)?; } }, @@ -444,41 +678,89 @@ pub mod text { } AIAgentOutputMessageType::WebSearch(status) => match status { WebSearchStatus::Searching { query } => match query { - Some(q) => writeln!(w, "Searching web for: {q}")?, - None => writeln!(w, "Searching web")?, + Some(q) => writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.web.searching_for") + .replace("{query}", q) + )?, + None => { + writeln!(w, "{}", i18n::t("ai.agent_sdk.driver.output.web.searching"))? + } }, WebSearchStatus::Success { query, pages } => { - writeln!(w, "Searched web for: {query} ({} results)", pages.len())?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.web.searched_results") + .replace("{query}", query) + .replace("{count}", &pages.len().to_string()) + )?; } WebSearchStatus::Error { query } => { - writeln!(w, "Web search failed for: {query}")?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.web.search_failed") + .replace("{query}", query) + )?; } }, AIAgentOutputMessageType::WebFetch(status) => match status { WebFetchStatus::Fetching { urls } => { - writeln!(w, "Fetching {} web pages...", urls.len())?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.web.fetching") + .replace("{count}", &urls.len().to_string()) + )?; } WebFetchStatus::Success { pages } => { - writeln!(w, "Fetched {} web pages", pages.len())?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.web.fetched") + .replace("{count}", &pages.len().to_string()) + )?; } WebFetchStatus::Error => { - writeln!(w, "Web fetch failed")?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.web.fetch_failed") + )?; } }, AIAgentOutputMessageType::CommentsAddressed { comments: comment_ids, } => { - writeln!(w, "Addressed {} comments", comment_ids.len())?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.comments.addressed") + .replace("{count}", &comment_ids.len().to_string()) + )?; } AIAgentOutputMessageType::DebugOutput { text } => { writeln!(w, "[DEBUG] {text}")?; } AIAgentOutputMessageType::ArtifactCreated(data) => match data { ArtifactCreatedData::PullRequest { url, branch } => { - writeln!(w, "Created PR: {url} (branch: {branch})")?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.artifact.pr_created") + .replace("{url}", url) + .replace("{branch}", branch) + )?; } ArtifactCreatedData::Screenshot { artifact_uid, .. } => { - writeln!(w, "Screenshot captured (artifact: {artifact_uid})")?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.artifact.screenshot_captured") + .replace("{artifact_uid}", artifact_uid) + )?; } ArtifactCreatedData::File { artifact_uid, @@ -487,18 +769,36 @@ pub mod text { } => { writeln!( w, - "File artifact uploaded: {filepath} (artifact: {artifact_uid})" + "{}", + i18n::t("ai.agent_sdk.driver.output.artifact.file_uploaded") + .replace("{filepath}", filepath) + .replace("{artifact_uid}", artifact_uid) )?; } }, AIAgentOutputMessageType::SkillInvoked(invoked_skill) => { - writeln!(w, "Skill Read: {}", invoked_skill.name)?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.skill.invoked") + .replace("{name}", &invoked_skill.name) + )?; } AIAgentOutputMessageType::MessagesReceivedFromAgents { messages } => { - writeln!(w, "Received {} messages", messages.len())?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.messages.received") + .replace("{count}", &messages.len().to_string()) + )?; } AIAgentOutputMessageType::EventsFromAgents { event_ids } => { - writeln!(w, "Received {} agent events", event_ids.len())?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.events.received") + .replace("{count}", &event_ids.len().to_string()) + )?; } } } @@ -520,20 +820,34 @@ pub mod text { pub fn conversation_started(conversation_id: &str, w: &mut W) -> io::Result<()> { writeln!( w, - "New conversation started with debug ID: {conversation_id}\n" + "{}", + i18n::t("ai.agent_sdk.driver.output.conversation_started") + .replace("{conversation_id}", conversation_id) ) } /// Report the run ID with a link to the Oz dashboard. pub fn run_started(run_id: &str, w: &mut W) -> io::Result<()> { let run_url = super::run_url(run_id); - writeln!(w, "Run ID: {run_id}")?; - writeln!(w, "Open in Oz: {run_url}\n") + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.run_id").replace("{run_id}", run_id) + )?; + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.open_in_oz").replace("{url}", &run_url) + ) } /// Report that a shared session has been established. pub fn shared_session_established(join_url: &str, w: &mut W) -> io::Result<()> { - writeln!(w, "Sharing session at: {join_url}") + writeln!( + w, + "{}", + i18n::t("ai.agent_sdk.driver.output.sharing_session").replace("{join_url}", join_url) + ) } /// Format a list of query patterns. @@ -554,7 +868,11 @@ pub mod text { ) -> io::Result<()> { writeln!( w, - "Created plan (title: {title}, id: {document_id}, notebook: {notebook_link})" + "{}", + i18n::t("ai.agent_sdk.driver.output.plan_created") + .replace("{title}", title) + .replace("{document_id}", document_id) + .replace("{notebook_link}", notebook_link) ) } } diff --git a/app/src/ai/agent_sdk/driver/snapshot.rs b/app/src/ai/agent_sdk/driver/snapshot.rs index 622893ae59..341f63d37e 100644 --- a/app/src/ai/agent_sdk/driver/snapshot.rs +++ b/app/src/ai/agent_sdk/driver/snapshot.rs @@ -537,29 +537,37 @@ async fn path_is_under_existing_repo(path: &Path) -> bool { /// The serialized shape matches the schema the parser expects. async fn append_declaration_line(declarations_path: &Path, path: &str) -> Result<()> { if let Some(parent) = declarations_path.parent() { - tokio_fs::create_dir_all(parent) - .await - .with_context(|| format!("create_dir_all {}", parent.display()))?; + tokio_fs::create_dir_all(parent).await.with_context(|| { + i18n::t("ai.agent_sdk.driver.snapshot.create_declarations_dir_failed") + .replace("{path}", &parent.display().to_string()) + })?; } let mut line = serde_json::to_string(&FileDeclaration { version: DECLARATION_VERSION, kind: "file", path, }) - .context("serialize file declaration")?; + .context(i18n::t( + "ai.agent_sdk.driver.snapshot.serialize_file_declaration_failed", + ))?; line.push('\n'); let mut file = OpenOptions::new() .append(true) .create(true) .open(declarations_path) .await - .with_context(|| format!("open declarations file {}", declarations_path.display()))?; - file.write_all(line.as_bytes()) - .await - .with_context(|| format!("write declarations file {}", declarations_path.display()))?; - file.flush() - .await - .with_context(|| format!("flush declarations file {}", declarations_path.display()))?; + .with_context(|| { + i18n::t("ai.agent_sdk.driver.snapshot.open_declarations_file_failed") + .replace("{path}", &declarations_path.display().to_string()) + })?; + file.write_all(line.as_bytes()).await.with_context(|| { + i18n::t("ai.agent_sdk.driver.snapshot.write_declarations_file_failed") + .replace("{path}", &declarations_path.display().to_string()) + })?; + file.flush().await.with_context(|| { + i18n::t("ai.agent_sdk.driver.snapshot.flush_declarations_file_failed") + .replace("{path}", &declarations_path.display().to_string()) + })?; Ok(()) } @@ -804,7 +812,9 @@ pub(crate) async fn upload_snapshot_for_handoff( let response = client .upload_local_handoff_snapshot(upload_request) .await - .context("failed to allocate initial snapshot token")?; + .context(i18n::t( + "ai.agent_sdk.driver.snapshot.allocate_initial_snapshot_token_failed", + ))?; log::info!( "Initial snapshot token allocated; expires_at={}, uploads={}", response.expires_at, @@ -994,7 +1004,9 @@ async fn upload_gathered_snapshot( Err(e) => { // Pipeline-abort: route through report_error! so Sentry captures the structured // error chain and on-call alerting can fire. - report_error!(e.context("Failed to get snapshot upload targets; skipping upload")); + report_error!(e.context(i18n::t( + "ai.agent_sdk.driver.snapshot.get_upload_targets_failed" + ))); return None; } }; @@ -1048,8 +1060,9 @@ async fn upload_prepared_snapshot_files( Ok(b) => b, Err(e) => { // Pipeline-abort: route through report_error! so Sentry captures it. - report_error!(anyhow::Error::from(e) - .context("Failed to serialize snapshot manifest; skipping upload")); + report_error!(anyhow::Error::from(e).context(i18n::t( + "ai.agent_sdk.driver.snapshot.serialize_manifest_failed" + ))); return None; } }; @@ -1062,7 +1075,10 @@ async fn upload_prepared_snapshot_files( Err(e) => { // Capture the full chain for the manifest's `error` field, then surface it // to Sentry via report_error!. - let e = e.context(format!("Failed to upload manifest '{manifest_filename}'")); + let e = e.context( + i18n::t("ai.agent_sdk.driver.snapshot.upload_manifest_failed") + .replace("{manifest_filename}", &manifest_filename), + ); let msg = format!("{e:#}"); report_error!(e); (false, Some(msg)) @@ -1198,7 +1214,9 @@ async fn gather_file( }); } Err(e) => { - let err_str = format!("Failed to read file '{file_path}': {e:#}"); + let err_str = i18n::t("ai.agent_sdk.driver.snapshot.read_file_failed") + .replace("{file_path}", file_path) + .replace("{error}", &format!("{e:#}")); log::warn!("{err_str}"); files.push(FileManifestEntry { path: file_path.to_string(), @@ -1602,17 +1620,24 @@ where ))); } Err(_) => anyhow::bail!( - "git {:?} timed out after {:?} in {}", - args, - GIT_COMMAND_TIMEOUT, - repo_dir.display() + "{}", + i18n::t("ai.agent_sdk.driver.snapshot.git_command_timed_out") + .replace("{args}", &format!("{args:?}")) + .replace("{timeout}", &format!("{GIT_COMMAND_TIMEOUT:?}")) + .replace("{repo_dir}", &repo_dir.display().to_string()) ), }; let status_code = output.status.code().unwrap_or(-1); if !allowed_exit_codes.contains(&status_code) { let stderr = String::from_utf8_lossy(&output.stderr); - anyhow::bail!("git {:?} failed in {}: {stderr}", args, repo_dir.display()); + anyhow::bail!( + "{}", + i18n::t("ai.agent_sdk.driver.snapshot.git_command_failed") + .replace("{args}", &format!("{args:?}")) + .replace("{repo_dir}", &repo_dir.display().to_string()) + .replace("{stderr}", &stderr) + ); } Ok(output.stdout) } diff --git a/app/src/ai/agent_sdk/environment.rs b/app/src/ai/agent_sdk/environment.rs index 224675d092..e59b856c93 100644 --- a/app/src/ai/agent_sdk/environment.rs +++ b/app/src/ai/agent_sdk/environment.rs @@ -45,16 +45,35 @@ fn parse_repos(repo_strings: Vec) -> anyhow::Result> { .map(|r| { let parts: Vec<&str> = r.split('/').collect(); if parts.len() != 2 { - return Err(anyhow::anyhow!( - "Invalid repo format: '{}'. Expected format: 'owner/repo'", - r - )); + return Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.environment.invalid_repo_format" + ) + .replace("{repo}", &r))); } Ok(GithubRepo::new(parts[0].to_string(), parts[1].to_string())) }) .collect() } +fn timed_out_waiting_for_warp_drive_error() -> anyhow::Error { + anyhow::anyhow!(i18n::t( + "ai.agent_sdk.environment.timed_out_waiting_for_warp_drive" + )) +} + +fn environment_not_found_error(id: &str) -> anyhow::Error { + anyhow::anyhow!(i18n::t("ai.agent_sdk.environment.not_found").replace("{id}", id)) +} + +fn localized_environment_action(action: &str) -> String { + match action { + "create" => i18n::t("ai.agent_sdk.environment.action.create"), + "delete" => i18n::t("ai.agent_sdk.environment.action.delete"), + "update" => i18n::t("ai.agent_sdk.environment.action.update"), + _ => action.to_string(), + } +} + /// Handle environment-related CLI commands. pub fn run( ctx: &mut AppContext, @@ -167,19 +186,29 @@ impl EnvironmentCommandRunner { OutputFormat::Text | OutputFormat::Pretty ) { println!( - "All Warp dev images contain Python and Node. For more information, see: {}\n", - WARP_DEV_ENVIRONMENTS_REPO + "{}\n", + i18n::t("ai.agent_sdk.environment.image_list_info") + .replace("{repo}", WARP_DEV_ENVIRONMENTS_REPO) ); } output::print_list(image_infos, global_options.output_format); ctx.terminate_app(warpui::platform::TerminationMode::ForceTerminate, None); } ListWarpDevImagesResult::UserFacingError(_) | ListWarpDevImagesResult::Unknown => { - super::report_fatal_error(anyhow::anyhow!("Failed to fetch images"), ctx); + super::report_fatal_error( + anyhow::anyhow!(i18n::t("ai.agent_sdk.environment.fetch_images_failed")), + ctx, + ); } }, Err(err) => { - super::report_fatal_error(anyhow::anyhow!("Failed to fetch images: {}", err), ctx); + super::report_fatal_error( + anyhow::anyhow!(i18n::t( + "ai.agent_sdk.environment.fetch_images_failed_with_error" + ) + .replace("{error}", &err.to_string())), + ctx, + ); } }); } @@ -191,10 +220,7 @@ impl EnvironmentCommandRunner { ctx.spawn(initial_sync, move |_, result, ctx| { if result.is_err() { - super::report_fatal_error( - anyhow::anyhow!("Timed out waiting for Warp Drive to sync"), - ctx, - ); + super::report_fatal_error(timed_out_waiting_for_warp_drive_error(), ctx); return; } @@ -262,10 +288,7 @@ impl EnvironmentCommandRunner { ctx.spawn(initial_sync, move |_, result, ctx| { if result.is_err() { - super::report_fatal_error( - anyhow::anyhow!("Timed out waiting for Warp Drive to sync"), - ctx, - ); + super::report_fatal_error(timed_out_waiting_for_warp_drive_error(), ctx); return; } @@ -275,7 +298,7 @@ impl EnvironmentCommandRunner { Err(_) => { ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, - Some(Err(anyhow::anyhow!("Environment {} not found", id))), + Some(Err(environment_not_found_error(&id))), ); return; } @@ -289,34 +312,56 @@ impl EnvironmentCommandRunner { } else { ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, - Some(Err(anyhow::anyhow!("Environment {} not found", id))), + Some(Err(environment_not_found_error(&id))), ); } }); } fn print_environment_details(env: &AmbientAgentEnvironment) { - println!("Name: {}", env.name); + println!( + "{}", + i18n::t("ai.agent_sdk.environment.detail.name").replace("{name}", &env.name) + ); if let Some(desc) = &env.description { - println!("Description: {desc}"); + println!( + "{}", + i18n::t("ai.agent_sdk.environment.detail.description") + .replace("{description}", desc) + ); } match &env.base_image { BaseImage::DockerImage(img) => { - println!("Docker image: {img}"); + println!( + "{}", + i18n::t("ai.agent_sdk.environment.detail.docker_image").replace("{image}", img) + ); } } if env.github_repos.is_empty() { - println!("Repositories: None"); + println!( + "{}", + i18n::t("ai.agent_sdk.environment.detail.repositories_none") + ); } else { - println!("Repositories:"); + println!( + "{}", + i18n::t("ai.agent_sdk.environment.detail.repositories") + ); for repo in &env.github_repos { println!(" - {}/{}", repo.owner, repo.repo); } } if env.setup_commands.is_empty() { - println!("Setup commands: None"); + println!( + "{}", + i18n::t("ai.agent_sdk.environment.detail.setup_commands_none") + ); } else { - println!("Setup commands:"); + println!( + "{}", + i18n::t("ai.agent_sdk.environment.detail.setup_commands") + ); for (i, cmd) in env.setup_commands.iter().enumerate() { println!(" {}. {}", i + 1, cmd); } @@ -327,7 +372,7 @@ impl EnvironmentCommandRunner { fn handle_inquire_error(err: InquireError, ctx: &mut ModelContext) -> bool { match err { InquireError::OperationCanceled | InquireError::OperationInterrupted => { - eprintln!("Environment creation canceled."); + eprintln!("{}", i18n::t("ai.agent_sdk.environment.creation_cancelled")); ctx.terminate_app(warpui::platform::TerminationMode::ForceTerminate, None); true } @@ -340,8 +385,6 @@ impl EnvironmentCommandRunner { where F: FnOnce(String, &mut ModelContext) + Send + 'static, { - const CUSTOM_IMAGE_OPTION: &str = "Custom Docker image"; - let server_api = ServerApiProvider::as_ref(ctx).get(); let operation = ListWarpDevImages::build(ListWarpDevImagesVariables {}); let fetch_images = async move { server_api.send_graphql_request(operation, None).await }; @@ -351,32 +394,36 @@ impl EnvironmentCommandRunner { ListWarpDevImagesResult::ListWarpDevImagesOutput(output) => { if output.images.is_empty() { super::report_fatal_error( - anyhow::anyhow!("No Warp dev images available."), + anyhow::anyhow!(i18n::t( + "ai.agent_sdk.environment.no_warp_dev_images_available" + )), ctx, ); return; } + println!("{}\n", i18n::t("ai.agent_sdk.environment.no_docker_image")); println!( - "No docker image provided, please select a base image.\n" - ); - println!( - "All warpdotdev images contain Python and Node, in addition to language-specific tooling. For more info: {}\n", - WARP_DEV_ENVIRONMENTS_REPO + "{}\n", + i18n::t("ai.agent_sdk.environment.images_info") + .replace("{repo}", WARP_DEV_ENVIRONMENTS_REPO) ); let mut image_choices: Vec = output.images.into_iter().map(|img| img.image).collect(); - image_choices.push(CUSTOM_IMAGE_OPTION.to_string()); + let custom_image_option = + i18n::t("ai.agent_sdk.environment.custom_docker_image"); + image_choices.push(custom_image_option.clone()); - let selected_image = match Select::new("Select a base image:", image_choices) - .prompt() - { + let select_prompt = i18n::t("ai.agent_sdk.environment.select_base_image"); + let selected_image = match Select::new(&select_prompt, image_choices).prompt() { Ok(image) => image, Err(err) => { if !Self::handle_inquire_error(err, ctx) { super::report_fatal_error( - anyhow::anyhow!("Error selecting image"), + anyhow::anyhow!(i18n::t( + "ai.agent_sdk.environment.select_image_error" + )), ctx, ); } @@ -384,13 +431,16 @@ impl EnvironmentCommandRunner { } }; - let final_image = if selected_image == CUSTOM_IMAGE_OPTION { - match inquire::Text::new("Enter custom Docker image name:").prompt() { + let final_image = if selected_image == custom_image_option { + let prompt = i18n::t("ai.agent_sdk.environment.enter_custom_docker_image"); + match inquire::Text::new(&prompt).prompt() { Ok(custom) => custom, Err(err) => { if !Self::handle_inquire_error(err, ctx) { super::report_fatal_error( - anyhow::anyhow!("Error entering custom image"), + anyhow::anyhow!(i18n::t( + "ai.agent_sdk.environment.enter_custom_image_error" + )), ctx, ); } @@ -405,13 +455,21 @@ impl EnvironmentCommandRunner { } ListWarpDevImagesResult::UserFacingError(_) | ListWarpDevImagesResult::Unknown => { super::report_fatal_error( - anyhow::anyhow!("Failed to fetch list of base images"), + anyhow::anyhow!(i18n::t( + "ai.agent_sdk.environment.fetch_base_images_failed" + )), ctx, ); } }, Err(err) => { - super::report_fatal_error(anyhow::anyhow!("Failed to fetch images: {err}"), ctx); + super::report_fatal_error( + anyhow::anyhow!(i18n::t( + "ai.agent_sdk.environment.fetch_images_failed_with_error" + ) + .replace("{error}", &err.to_string())), + ctx, + ); } }); } @@ -471,10 +529,7 @@ impl EnvironmentCommandRunner { ctx.spawn(initial_sync, move |_, result, ctx| { if result.is_err() { - super::report_fatal_error( - anyhow::anyhow!("Timed out waiting for Warp Drive to sync"), - ctx, - ); + super::report_fatal_error(timed_out_waiting_for_warp_drive_error(), ctx); return; } @@ -520,10 +575,10 @@ impl EnvironmentCommandRunner { if attempt > MAX_AUTH_ATTEMPTS { ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, - Some(Err(anyhow::anyhow!( - "Exceeded maximum number of authorization attempts ({}). Please try again later.", - MAX_AUTH_ATTEMPTS - ))), + Some(Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.environment.max_authorization_attempts_exceeded" + ) + .replace("{count}", &MAX_AUTH_ATTEMPTS.to_string())))), ); return; } @@ -559,8 +614,12 @@ impl EnvironmentCommandRunner { UserRepoAuthStatusEnum::NoInstallationOrAccessForRepo => { if !status.is_public { eprintln!( - "Cannot access private repo {}/{}", - status.owner, status.repo, + "{}", + i18n::t( + "ai.agent_sdk.environment.cannot_access_private_repo" + ) + .replace("{owner}", &status.owner) + .replace("{repo}", &status.repo), ); has_blocking_private_issues = true; private_repo_owners.insert(status.owner.clone()); @@ -569,7 +628,10 @@ impl EnvironmentCommandRunner { } } UserRepoAuthStatusEnum::UserNotConnectedToGithub => { - eprintln!("User not connected to GitHub"); + eprintln!( + "{}", + i18n::t("ai.agent_sdk.environment.user_not_connected_to_github") + ); has_blocking_private_issues = true; break; } @@ -582,8 +644,10 @@ impl EnvironmentCommandRunner { ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, Some(Err(anyhow::anyhow!( - "All private repositories in an environment must belong to the same owner. Found multiple owners: {}.\nIf you need support for private repos from multiple owners, please submit a GitHub issue.", - owners_str + i18n::t( + "ai.agent_sdk.environment.private_repos_multiple_owners" + ) + .replace("{owners}", &owners_str) ))), ); return; @@ -601,13 +665,21 @@ impl EnvironmentCommandRunner { ) { eprintln!( - "Warning: using public repo {}/{} without authorization. Read-only access is available, but you need to authorize if you want full access.", - status.owner, status.repo + "{}", + i18n::t( + "ai.agent_sdk.environment.public_repo_auth_warning" + ) + .replace("{owner}", &status.owner) + .replace("{repo}", &status.repo) ); } } if let Some(auth_url) = response.auth_url { - println!("\nAuthorize access here: {auth_url}\n"); + println!( + "\n{}\n", + i18n::t("ai.agent_sdk.environment.authorize_access_here") + .replace("{url}", &auth_url) + ); } } @@ -621,8 +693,19 @@ impl EnvironmentCommandRunner { match (response.auth_url, response.tx_id) { (Some(auth_url), Some(tx_id)) => { // Open URL and poll for OAuth completion. - println!("\nAuthorization required for private repository access."); - println!("Opening browser for GitHub authorization: {auth_url}\n"); + println!( + "\n{}", + i18n::t( + "ai.agent_sdk.environment.private_repo_authorization_required" + ) + ); + println!( + "{}\n", + i18n::t( + "ai.agent_sdk.environment.opening_github_authorization" + ) + .replace("{url}", &auth_url) + ); ctx.open_url(&auth_url); let integrations_client = ServerApiProvider::as_ref(ctx) @@ -644,7 +727,9 @@ impl EnvironmentCommandRunner { ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, Some(Err(anyhow::anyhow!( - "GitHub authorization failed. Please try again." + i18n::t( + "ai.agent_sdk.environment.github_authorization_failed" + ) ))), ); } @@ -652,7 +737,9 @@ impl EnvironmentCommandRunner { ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, Some(Err(anyhow::anyhow!( - "GitHub authorization expired. Please try again." + i18n::t( + "ai.agent_sdk.environment.github_authorization_expired" + ) ))), ); } @@ -662,7 +749,9 @@ impl EnvironmentCommandRunner { ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, Some(Err(anyhow::anyhow!( - "Unexpected non-terminal OAuth status returned" + i18n::t( + "ai.agent_sdk.environment.unexpected_non_terminal_oauth_status" + ) ))), ); } @@ -670,7 +759,10 @@ impl EnvironmentCommandRunner { ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, Some(Err(anyhow::anyhow!( - "Error polling OAuth status: {err}" + i18n::t( + "ai.agent_sdk.environment.poll_oauth_status_error" + ) + .replace("{error}", &err.to_string()) ))), ); } @@ -680,8 +772,15 @@ impl EnvironmentCommandRunner { } (Some(auth_url), None) => { // Legacy flow: no txId, print URL and exit. - println!("\nAuthorize access here: {auth_url}\n"); - println!("After authorizing, please re-run this command."); + println!( + "\n{}\n", + i18n::t("ai.agent_sdk.environment.authorize_access_here") + .replace("{url}", &auth_url) + ); + println!( + "{}", + i18n::t("ai.agent_sdk.environment.rerun_after_authorizing") + ); ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, None, @@ -692,7 +791,9 @@ impl EnvironmentCommandRunner { ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, Some(Err(anyhow::anyhow!( - "Server error: did not receive auth URL for OAuth flow" + i18n::t( + "ai.agent_sdk.environment.missing_oauth_auth_url" + ) ))), ); } @@ -701,8 +802,13 @@ impl EnvironmentCommandRunner { ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, Some(Err(anyhow::anyhow!( - "Cannot {} environment: authorization required but no auth flow provided by server", - operation_name + i18n::t( + "ai.agent_sdk.environment.no_auth_flow_provided" + ) + .replace( + "{action}", + &localized_environment_action(operation_name) + ) ))), ); } @@ -711,7 +817,9 @@ impl EnvironmentCommandRunner { Err(e) => { ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, - Some(Err(e.context("Failed to check GitHub auth status"))), + Some(Err(e.context(i18n::t( + "ai.agent_sdk.environment.check_github_auth_status_failed", + )))), ); } } @@ -761,7 +869,11 @@ impl EnvironmentCommandRunner { && result.client_id == Some(client_id) { let server_id = result.server_id.unwrap(); - println!("Environment created successfully with ID: {server_id}"); + println!( + "{}", + i18n::t("ai.agent_sdk.environment.created_successfully") + .replace("{id}", &server_id.to_string()) + ); ctx.terminate_app(warpui::platform::TerminationMode::ForceTerminate, None); } } @@ -787,23 +899,33 @@ impl EnvironmentCommandRunner { .await }; - ctx.spawn(check_integrations_future, move |_, result, ctx| { - match result { + ctx.spawn( + check_integrations_future, + move |_, result, ctx| match result { Ok(output) => { if !output.provider_names.is_empty() { let integration_list = output.provider_names.join(", "); - let prompt_message = format!( - "This environment is used in the following integration(s): {integration_list}. Are you sure you want to {action} it?" - ); + let action_label = localized_environment_action(action); + let prompt_message = + i18n::t("ai.agent_sdk.environment.integration_usage_confirm") + .replace("{integrations}", &integration_list) + .replace("{action}", &action_label); - let confirmation = Confirm::new(&prompt_message) - .with_default(false) - .prompt(); + let confirmation = + Confirm::new(&prompt_message).with_default(false).prompt(); match confirmation { Ok(true) => on_confirm(ctx), - Ok(false) | Err(InquireError::OperationCanceled | InquireError::OperationInterrupted) => { - println!("Environment {action} canceled."); + Ok(false) + | Err( + InquireError::OperationCanceled + | InquireError::OperationInterrupted, + ) => { + println!( + "{}", + i18n::t("ai.agent_sdk.environment.action_cancelled") + .replace("{action}", &action_label) + ); ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, None, @@ -812,10 +934,13 @@ impl EnvironmentCommandRunner { Err(err) => { ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, - Some(Err(anyhow::anyhow!("Error prompting for confirmation: {err}"))), + Some(Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.environment.confirmation_prompt_error" + ) + .replace("{error}", &err.to_string())))), ); } - } + } } else { on_confirm(ctx); } @@ -823,13 +948,14 @@ impl EnvironmentCommandRunner { Err(_) => { ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, - Some(Err(anyhow::anyhow!( - "Aborting environment {action} because integration usage could not be determined. Re-run with --force to override." - ))), + Some(Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.environment.integration_usage_unknown_abort" + ) + .replace("{action}", &localized_environment_action(action))))), ); } - } - }); + }, + ); } #[allow(clippy::too_many_arguments)] @@ -853,10 +979,7 @@ impl EnvironmentCommandRunner { ctx.spawn(initial_sync, move |_, result, ctx| { if result.is_err() { - super::report_fatal_error( - anyhow::anyhow!("Timed out waiting for Warp Drive to sync"), - ctx, - ); + super::report_fatal_error(timed_out_waiting_for_warp_drive_error(), ctx); return; } @@ -864,7 +987,7 @@ impl EnvironmentCommandRunner { let server_id = match ServerId::try_from(id.as_str()) { Ok(sid) => sid, Err(_) => { - let error = anyhow::anyhow!("Environment {} not found", id); + let error = environment_not_found_error(&id); ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, Some(Err(error)), @@ -875,7 +998,7 @@ impl EnvironmentCommandRunner { let sync_id = SyncId::ServerId(server_id); let environment = CloudAmbientAgentEnvironment::get_by_id(&sync_id, ctx); let Some(environment) = environment else { - let error = anyhow::anyhow!("Environment {} not found", id); + let error = environment_not_found_error(&id); ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, Some(Err(error)), @@ -966,8 +1089,10 @@ impl EnvironmentCommandRunner { updated_env.github_repos.remove(pos); } else { eprintln!( - "Warning: repository {}/{} not found in environment, skipping removal", - repo.owner, repo.repo + "{}", + i18n::t("ai.agent_sdk.environment.repository_removal_warning") + .replace("{owner}", &repo.owner) + .replace("{repo}", &repo.repo) ); } } @@ -981,7 +1106,9 @@ impl EnvironmentCommandRunner { updated_env.setup_commands.remove(pos); } else { eprintln!( - "Warning: setup command '{cmd}' not found in environment, skipping removal" + "{}", + i18n::t("ai.agent_sdk.environment.setup_command_removal_warning") + .replace("{command}", cmd) ); } } @@ -1006,7 +1133,10 @@ impl EnvironmentCommandRunner { { match result.success_type { OperationSuccessType::Success => { - println!("Environment updated successfully!\n"); + println!( + "{}\n", + i18n::t("ai.agent_sdk.environment.updated_successfully") + ); Self::print_environment_details(&updated_env); ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, @@ -1015,7 +1145,7 @@ impl EnvironmentCommandRunner { } _ => { super::report_fatal_error( - anyhow::anyhow!("Failed to update environment"), + anyhow::anyhow!(i18n::t("ai.agent_sdk.environment.update_failed")), ctx, ); } @@ -1032,10 +1162,7 @@ impl EnvironmentCommandRunner { ctx.spawn(initial_sync, move |_, result, ctx| { if result.is_err() { - super::report_fatal_error( - anyhow::anyhow!("Timed out waiting for Warp Drive to sync"), - ctx, - ); + super::report_fatal_error(timed_out_waiting_for_warp_drive_error(), ctx); return; } @@ -1043,7 +1170,7 @@ impl EnvironmentCommandRunner { let server_id = match ServerId::try_from(id.as_str()) { Ok(sid) => sid, Err(_) => { - let error = anyhow::anyhow!("Environment {} not found", id); + let error = environment_not_found_error(&id); ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, Some(Err(error)), @@ -1054,7 +1181,7 @@ impl EnvironmentCommandRunner { let sync_id = SyncId::ServerId(server_id); let environment = CloudAmbientAgentEnvironment::get_by_id(&sync_id, ctx); let Some(environment) = environment else { - let error = anyhow::anyhow!("Environment {} not found", id); + let error = environment_not_found_error(&id); ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, Some(Err(error)), @@ -1090,7 +1217,10 @@ impl EnvironmentCommandRunner { if matches!(result.operation, ObjectOperation::Delete { .. }) { match result.success_type { OperationSuccessType::Success => { - println!("Environment deleted successfully"); + println!( + "{}", + i18n::t("ai.agent_sdk.environment.deleted_successfully") + ); ctx.terminate_app( warpui::platform::TerminationMode::ForceTerminate, None, @@ -1098,7 +1228,7 @@ impl EnvironmentCommandRunner { } _ => { super::report_fatal_error( - anyhow::anyhow!("Failed to delete environment"), + anyhow::anyhow!(i18n::t("ai.agent_sdk.environment.delete_failed")), ctx, ); } @@ -1135,15 +1265,15 @@ struct EnvironmentInfo { impl TableFormat for EnvironmentInfo { fn header() -> Vec { vec![ - Cell::new("ID"), - Cell::new("Name"), - Cell::new("Description"), - Cell::new("Base image"), - Cell::new("Git repos"), - Cell::new("Setup commands"), - Cell::new("Creator"), - Cell::new("Last edited"), - Cell::new("Scope"), + Cell::new(i18n::t("ai.agent_sdk.environment.table.id")), + Cell::new(i18n::t("ai.agent_sdk.environment.table.name")), + Cell::new(i18n::t("ai.agent_sdk.environment.table.description")), + Cell::new(i18n::t("ai.agent_sdk.environment.table.base_image")), + Cell::new(i18n::t("ai.agent_sdk.environment.table.git_repos")), + Cell::new(i18n::t("ai.agent_sdk.environment.table.setup_commands")), + Cell::new(i18n::t("ai.agent_sdk.environment.table.creator")), + Cell::new(i18n::t("ai.agent_sdk.environment.table.last_edited")), + Cell::new(i18n::t("ai.agent_sdk.environment.table.scope")), ] } @@ -1182,9 +1312,9 @@ struct ImageInfo { impl TableFormat for ImageInfo { fn header() -> Vec { vec![ - Cell::new("Image"), - Cell::new("Repository"), - Cell::new("Tag"), + Cell::new(i18n::t("ai.agent_sdk.environment.image_table.image")), + Cell::new(i18n::t("ai.agent_sdk.environment.image_table.repository")), + Cell::new(i18n::t("ai.agent_sdk.environment.image_table.tag")), ] } diff --git a/app/src/ai/agent_sdk/federate.rs b/app/src/ai/agent_sdk/federate.rs index 76e42e5c6d..10d6cf54e6 100644 --- a/app/src/ai/agent_sdk/federate.rs +++ b/app/src/ai/agent_sdk/federate.rs @@ -20,7 +20,9 @@ pub fn run( command: FederateCommand, ) -> Result<()> { if !FeatureFlag::OzIdentityFederation.is_enabled() { - return Err(anyhow::anyhow!("This feature is not enabled")); + return Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.federate.feature_not_enabled" + ))); } match command { FederateCommand::IssueToken(args) => issue_token(ctx, args, global_options.output_format), @@ -39,8 +41,9 @@ fn issue_token( let duration: std::time::Duration = args.duration.into(); let audience = args.audience; let subject_template = match args.subject_template { - Some(template) => vec1::Vec1::try_from_vec(template) - .map_err(|_| anyhow::anyhow!("--subject-template requires at least one value"))?, + Some(template) => vec1::Vec1::try_from_vec(template).map_err(|_| { + anyhow::anyhow!(i18n::t("ai.agent_sdk.federate.subject_template_required")) + })?, None => vec1::vec1!["principal".to_owned()], }; @@ -71,9 +74,19 @@ fn issue_token( println!("{token_value}"); } OutputFormat::Pretty => { - println!("Token: {token_value}"); - println!("Expires at: {expires_at}"); - println!("Issuer: {issuer}"); + println!( + "{}", + i18n::t("ai.agent_sdk.federate.token").replace("{token}", &token_value) + ); + println!( + "{}", + i18n::t("ai.agent_sdk.federate.expires_at") + .replace("{expires_at}", &expires_at) + ); + println!( + "{}", + i18n::t("ai.agent_sdk.federate.issuer").replace("{issuer}", &issuer) + ); } } ctx.terminate_app(TerminationMode::ForceTerminate, None); @@ -106,8 +119,10 @@ fn issue_gcp_token(ctx: &mut AppContext, args: IssueGcpTokenArgs) -> Result<()> // If we can't cache the token, report an error but don't fail the command. if let Some(output_path) = output_file { if let Err(err) = std::fs::write(&output_path, &output) { - report_error!(anyhow!(err) - .context(format!("Error writing GCP token to {output_path}"))); + report_error!(anyhow!(err).context( + i18n::t("ai.agent_sdk.federate.write_gcp_token_failed") + .replace("{path}", &output_path) + )); } } diff --git a/app/src/ai/agent_sdk/harness_support.rs b/app/src/ai/agent_sdk/harness_support.rs index 7bef384d4e..b3e19dbb8a 100644 --- a/app/src/ai/agent_sdk/harness_support.rs +++ b/app/src/ai/agent_sdk/harness_support.rs @@ -26,7 +26,9 @@ pub fn run( args: HarnessSupportArgs, ) -> Result<()> { if !FeatureFlag::AgentHarness.is_enabled() { - return Err(anyhow::anyhow!("This feature is not enabled")); + return Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.harness_support.feature_not_enabled" + ))); } // Store the run ID so that it's included on all server requests, along with a workload token. @@ -121,7 +123,11 @@ fn report_artifact( println!("{json}"); } OutputFormat::Pretty | OutputFormat::Text => { - println!("Artifact reported: {}", response.artifact_uid); + println!( + "{}", + i18n::t("ai.agent_sdk.harness_support.artifact_reported") + .replace("{uid}", &response.artifact_uid) + ); } } ctx.terminate_app(TerminationMode::ForceTerminate, None); @@ -155,7 +161,10 @@ fn notify_user( println!("{{}}"); } OutputFormat::Pretty | OutputFormat::Text => { - println!("Notification sent."); + println!( + "{}", + i18n::t("ai.agent_sdk.harness_support.notification_sent") + ); } } ctx.terminate_app(TerminationMode::ForceTerminate, None); @@ -192,7 +201,7 @@ fn finish_task( println!("{{}}"); } OutputFormat::Pretty | OutputFormat::Text => { - println!("Task finished."); + println!("{}", i18n::t("ai.agent_sdk.harness_support.task_finished")); } } ctx.terminate_app(TerminationMode::ForceTerminate, None); @@ -227,9 +236,9 @@ fn report_shutdown( client.report_error_shutdown(category, message).await } (None, None) => client.report_clean_shutdown().await, - _ => anyhow::bail!( - "--error-category and --error-message must be provided together" - ), + _ => anyhow::bail!(i18n::t( + "ai.agent_sdk.harness_support.shutdown_error_args_required" + )), } }, move |_, result, ctx| match result { @@ -239,7 +248,10 @@ fn report_shutdown( println!("{{}}"); } OutputFormat::Pretty | OutputFormat::Text => { - println!("Shutdown reported."); + println!( + "{}", + i18n::t("ai.agent_sdk.harness_support.shutdown_reported") + ); } } ctx.terminate_app(TerminationMode::ForceTerminate, None); diff --git a/app/src/ai/agent_sdk/integration.rs b/app/src/ai/agent_sdk/integration.rs index c54ed20e1d..652ba16c09 100644 --- a/app/src/ai/agent_sdk/integration.rs +++ b/app/src/ai/agent_sdk/integration.rs @@ -35,6 +35,14 @@ pub fn run( struct IntegrationCommandRunner; +fn localized_integration_action(is_update: bool) -> String { + if is_update { + i18n::t("ai.agent_sdk.integration.action.update") + } else { + i18n::t("ai.agent_sdk.integration.action.creation") + } +} + impl IntegrationCommandRunner { fn list(&self, global_options: GlobalOptions, ctx: &mut ModelContext) { // Hardcoded set of providers that this client knows how to render. @@ -154,15 +162,22 @@ impl IntegrationCommandRunner { let environment_uid = match EnvironmentChoice::resolve_for_create(environment_args, ctx) { Ok(EnvironmentChoice::None) => { - eprintln!("Creating integration without an environment."); + eprintln!( + "{}", + i18n::t("ai.agent_sdk.integration.creating_without_environment") + ); None } Ok(EnvironmentChoice::Environment { id, .. }) => { - eprintln!("Creating integration with environment {id}."); + eprintln!( + "{}", + i18n::t("ai.agent_sdk.integration.creating_with_environment") + .replace("{id}", &id) + ); Some(id) } Err(ResolveConfigurationError::Canceled) => { - eprintln!("Integration creation canceled."); + eprintln!("{}", i18n::t("ai.agent_sdk.integration.creation_cancelled")); ctx.terminate_app(TerminationMode::ForceTerminate, None); return; } @@ -204,15 +219,16 @@ impl IntegrationCommandRunner { attempt: u32, ) { const MAX_CREATE_ATTEMPTS: u32 = 8; - let action = if is_update { "update" } else { "creation" }; + let action = localized_integration_action(is_update); if attempt > MAX_CREATE_ATTEMPTS { ctx.terminate_app( TerminationMode::ForceTerminate, - Some(Err(anyhow::anyhow!( - "Exceeded maximum number of integration creation attempts ({}). Retry.", - MAX_CREATE_ATTEMPTS - ))), + Some(Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.integration.max_attempts_exceeded" + ) + .replace("{action}", &action) + .replace("{count}", &MAX_CREATE_ATTEMPTS.to_string())))), ); return; } @@ -257,7 +273,11 @@ impl IntegrationCommandRunner { match (auth_url, tx_id) { (Some(auth_url), Some(tx_id)) => { // We have another auth step: open URL and poll txId. - println!("Authorize the provider here: {auth_url}\n"); + println!( + "{}\n", + i18n::t("ai.agent_sdk.integration.authorize_provider_here") + .replace("{url}", &auth_url) + ); ctx.open_url(&auth_url); let integrations_client = ServerApiProvider::as_ref(ctx) @@ -302,13 +322,17 @@ impl IntegrationCommandRunner { Ok(OauthConnectTxStatus::Failed) => { ctx.terminate_app( TerminationMode::ForceTerminate, - Some(Err(anyhow::anyhow!("OAuth authorization failed."))), + Some(Err(anyhow::anyhow!( + i18n::t("ai.agent_sdk.integration.oauth_authorization_failed") + ))), ); } Ok(OauthConnectTxStatus::Expired) => { ctx.terminate_app( TerminationMode::ForceTerminate, - Some(Err(anyhow::anyhow!("OAuth authorization expired."))), + Some(Err(anyhow::anyhow!( + i18n::t("ai.agent_sdk.integration.oauth_authorization_expired") + ))), ); } Ok(OauthConnectTxStatus::Pending) @@ -316,13 +340,18 @@ impl IntegrationCommandRunner { // Should not be returned by poll_oauth_until_terminal. ctx.terminate_app( TerminationMode::ForceTerminate, - Some(Err(anyhow::anyhow!("Unexpected non-terminal OAuth status returned"))), + Some(Err(anyhow::anyhow!( + i18n::t("ai.agent_sdk.integration.unexpected_non_terminal_oauth_status") + ))), ); } Err(err) => { ctx.terminate_app( TerminationMode::ForceTerminate, - Some(Err(anyhow::anyhow!("Error polling OAuth status: {err}"))), + Some(Err(anyhow::anyhow!( + i18n::t("ai.agent_sdk.integration.poll_oauth_status_error") + .replace("{error}", &err.to_string()) + ))), ); } } @@ -330,10 +359,16 @@ impl IntegrationCommandRunner { ); } (Some(auth_url), None) => { - println!("Authorize the provider here: {auth_url}\n"); + println!( + "{}\n", + i18n::t("ai.agent_sdk.integration.authorize_provider_here") + .replace("{url}", &auth_url) + ); ctx.open_url(&auth_url); println!( - "After authorizing, re-run the command to continue the integration {action} process.", + "{}", + i18n::t("ai.agent_sdk.integration.rerun_after_authorizing") + .replace("{action}", &action) ); ctx.terminate_app( TerminationMode::ForceTerminate, @@ -343,7 +378,10 @@ impl IntegrationCommandRunner { (None, Some(_)) => { ctx.terminate_app( TerminationMode::ForceTerminate, - Some(Err(anyhow::anyhow!("Server did not return an authURL for the integration creation process."))), + Some(Err(anyhow::anyhow!( + i18n::t("ai.agent_sdk.integration.missing_auth_url") + .replace("{action}", &action) + ))), ); } (None, None) => { @@ -356,7 +394,11 @@ impl IntegrationCommandRunner { } else { ctx.terminate_app( TerminationMode::ForceTerminate, - Some(Err(anyhow::anyhow!("Integration creation reported failure: {}", output.message))), + Some(Err(anyhow::anyhow!( + i18n::t("ai.agent_sdk.integration.reported_failure") + .replace("{action}", &action) + .replace("{message}", &output.message) + ))), ); } } diff --git a/app/src/ai/agent_sdk/integration_output.rs b/app/src/ai/agent_sdk/integration_output.rs index ed02d0c62f..4abc8c310f 100644 --- a/app/src/ai/agent_sdk/integration_output.rs +++ b/app/src/ai/agent_sdk/integration_output.rs @@ -24,7 +24,10 @@ pub fn print_integrations(graphql_output: &SimpleIntegrationsOutput, output_form let integrations = &graphql_output.integrations; if integrations.is_empty() { - println!("No integrations found."); + println!( + "{}", + i18n::t("ai.agent_sdk.integration.no_integrations_found") + ); return; } @@ -40,9 +43,15 @@ pub fn print_integrations(graphql_output: &SimpleIntegrationsOutput, output_form OutputFormat::Pretty | OutputFormat::Text => { // Use the existing card-style layout for pretty/text output if integrations.len() == 1 { - println!("\nIntegration:"); + println!( + "\n{}", + i18n::t("ai.agent_sdk.integration.heading.integration") + ); } else { - println!("\nIntegrations:"); + println!( + "\n{}", + i18n::t("ai.agent_sdk.integration.heading.integrations") + ); } for integration in integrations { @@ -159,10 +168,11 @@ fn print_integration_card(integration: &SimpleIntegration) { // Row 2: Status: Status description let emoji = status_emoji(integration.connection_status); - let explanation = status_explanation(integration.connection_status); + let explanation = localized_status_explanation(integration.connection_status); let status_text = format!("{emoji} {explanation}"); + let status_label = i18n::t("ai.agent_sdk.integration.label.status"); let status_row = crate::ai::agent_sdk::text_layout::render_labeled_wrapped_field( - "Status", + &status_label, &status_text, MAX_LINE_WIDTH, ); @@ -173,10 +183,11 @@ fn print_integration_card(integration: &SimpleIntegration) { Some(ListedSimpleIntegrationConfig { environment_uid, .. }) if !environment_uid.is_empty() => environment_uid.clone(), - _ => "(none)".to_string(), + _ => i18n::t("ai.agent_sdk.integration.none"), }; + let environment_label = i18n::t("ai.agent_sdk.integration.label.environment"); let env_row = crate::ai::agent_sdk::text_layout::render_labeled_wrapped_field( - "Environment", + &environment_label, &env_value, MAX_LINE_WIDTH, ); @@ -185,8 +196,9 @@ fn print_integration_card(integration: &SimpleIntegration) { // Model row (only if present). if let Some(ListedSimpleIntegrationConfig { model_id, .. }) = &integration.integration_config { if !model_id.is_empty() { + let model_label = i18n::t("ai.agent_sdk.integration.label.model"); let model_row = crate::ai::agent_sdk::text_layout::render_labeled_wrapped_field( - "Model", + &model_label, model_id, MAX_LINE_WIDTH, ); @@ -198,8 +210,9 @@ fn print_integration_card(integration: &SimpleIntegration) { if let Some(ListedSimpleIntegrationConfig { base_prompt, .. }) = &integration.integration_config { if !base_prompt.is_empty() { + let base_prompt_label = i18n::t("ai.agent_sdk.integration.label.base_prompt"); let base_prompt_row = crate::ai::agent_sdk::text_layout::render_labeled_wrapped_field( - "Base prompt", + &base_prompt_label, base_prompt, MAX_LINE_WIDTH, ); @@ -211,7 +224,8 @@ fn print_integration_card(integration: &SimpleIntegration) { if let Some(config) = &integration.integration_config { let lines = mcp_server_display_lines(config); if !lines.is_empty() { - let row = render_labeled_wrapped_lines("MCP servers", &lines, MAX_LINE_WIDTH); + let mcp_servers_label = i18n::t("ai.agent_sdk.integration.label.mcp_servers"); + let row = render_labeled_wrapped_lines(&mcp_servers_label, &lines, MAX_LINE_WIDTH); table.add_row(vec![row]); } } @@ -221,7 +235,8 @@ fn print_integration_card(integration: &SimpleIntegration) { if let Some(created) = integration.created_at { let dt = created.utc(); let formatted = format_approx_duration_from_now_utc(dt); - created_updated.push_str(&format!("Created: {formatted}")); + created_updated + .push_str(&i18n::t("ai.agent_sdk.integration.created").replace("{time}", &formatted)); } if let Some(updated) = integration.updated_at { let dt = updated.utc(); @@ -229,7 +244,8 @@ fn print_integration_card(integration: &SimpleIntegration) { if !created_updated.is_empty() { created_updated.push_str(" | "); } - created_updated.push_str(&format!("Updated: {formatted}")); + created_updated + .push_str(&i18n::t("ai.agent_sdk.integration.updated").replace("{time}", &formatted)); } if !created_updated.is_empty() { let wrapped = @@ -268,6 +284,26 @@ fn status_explanation(status: SimpleIntegrationConnectionStatus) -> &'static str } } +fn localized_status_explanation(status: SimpleIntegrationConnectionStatus) -> String { + match status { + SimpleIntegrationConnectionStatus::NotConnected => { + i18n::t("ai.agent_sdk.integration.status.not_connected") + } + SimpleIntegrationConnectionStatus::ConnectionError => { + i18n::t("ai.agent_sdk.integration.status.connection_error") + } + SimpleIntegrationConnectionStatus::IntegrationNotConfigured => { + i18n::t("ai.agent_sdk.integration.status.not_configured") + } + SimpleIntegrationConnectionStatus::NotEnabled => { + i18n::t("ai.agent_sdk.integration.status.not_enabled") + } + SimpleIntegrationConnectionStatus::Active => { + i18n::t("ai.agent_sdk.integration.status.active") + } + } +} + /// Serializable integration info for output. #[derive(Serialize)] struct IntegrationInfo { @@ -334,12 +370,12 @@ impl IntegrationInfo { impl TableFormat for IntegrationInfo { fn header() -> Vec { vec![ - Cell::new("Provider"), - Cell::new("Description"), - Cell::new("Status"), - Cell::new("Environment"), - Cell::new("Created"), - Cell::new("Updated"), + Cell::new(i18n::t("ai.agent_sdk.integration.table.provider")), + Cell::new(i18n::t("ai.agent_sdk.integration.table.description")), + Cell::new(i18n::t("ai.agent_sdk.integration.table.status")), + Cell::new(i18n::t("ai.agent_sdk.integration.table.environment")), + Cell::new(i18n::t("ai.agent_sdk.integration.table.created")), + Cell::new(i18n::t("ai.agent_sdk.integration.table.updated")), ] } @@ -348,7 +384,11 @@ impl TableFormat for IntegrationInfo { Cell::new(&self.provider), Cell::new(&self.description), Cell::new(&self.status), - Cell::new(self.environment_uid.as_deref().unwrap_or("(none)")), + Cell::new( + self.environment_uid + .clone() + .unwrap_or_else(|| i18n::t("ai.agent_sdk.integration.none")), + ), Cell::new(&self.created_at_formatted), Cell::new(&self.updated_at_formatted), ] diff --git a/app/src/ai/agent_sdk/mcp.rs b/app/src/ai/agent_sdk/mcp.rs index e89229bf5f..4473433259 100644 --- a/app/src/ai/agent_sdk/mcp.rs +++ b/app/src/ai/agent_sdk/mcp.rs @@ -60,7 +60,10 @@ struct MCPServerInfo { impl TableFormat for MCPServerInfo { fn header() -> Vec { - vec![Cell::new("UUID"), Cell::new("Name")] + vec![ + Cell::new(i18n::t("ai.agent_sdk.mcp.table.uuid")), + Cell::new(i18n::t("ai.agent_sdk.mcp.table.name")), + ] } fn row(&self) -> Vec { diff --git a/app/src/ai/agent_sdk/mcp_config.rs b/app/src/ai/agent_sdk/mcp_config.rs index 46f6575212..1361dba65a 100644 --- a/app/src/ai/agent_sdk/mcp_config.rs +++ b/app/src/ai/agent_sdk/mcp_config.rs @@ -41,7 +41,7 @@ pub(super) fn build_mcp_servers_from_specs( let value = parse_json_with_optional_braces(&json_str)?; let server_map = TemplatableMCPServer::find_template_map(value) - .context("Failed to parse MCP server map")?; + .context(i18n::t("ai.agent_sdk.mcp_config.parse_server_map_failed"))?; for (name, config) in server_map { insert_unique(&mut merged, name, config)?; @@ -61,7 +61,10 @@ pub(super) fn build_mcp_servers_from_specs( fn insert_unique(map: &mut Map, name: String, config: Value) -> anyhow::Result<()> { if map.contains_key(&name) { - anyhow::bail!("Duplicate MCP server name '{name}' specified multiple times"); + anyhow::bail!( + "{}", + i18n::t("ai.agent_sdk.mcp_config.duplicate_server_name").replace("{name}", &name) + ); } map.insert(name, config); @@ -77,14 +80,14 @@ fn parse_json_with_optional_braces(input: &str) -> anyhow::Result { format!("{{{json}}}") }; - serde_json::from_str(&json).with_context(|| "Invalid MCP JSON".to_string()) + serde_json::from_str(&json).with_context(|| i18n::t("ai.agent_sdk.mcp_config.invalid_json")) } #[cfg(not(target_family = "wasm"))] fn normalize_mcp_json_for_single_server(input: &str) -> anyhow::Result { crate::ai::mcp::parsing::normalize_mcp_json(input) .map_err(|e| anyhow::anyhow!(e)) - .context("Failed to normalize MCP JSON") + .context(i18n::t("ai.agent_sdk.mcp_config.normalize_json_failed")) } // The CLI + ambient-agent API isn’t used in WASM builds, but this module still needs to compile. @@ -98,8 +101,8 @@ fn normalize_mcp_json_for_single_server(input: &str) -> anyhow::Result { format!("{{{json}}}") }; - let value: Value = - serde_json::from_str(&json_for_parsing).with_context(|| "Invalid MCP JSON".to_string())?; + let value: Value = serde_json::from_str(&json_for_parsing) + .with_context(|| i18n::t("ai.agent_sdk.mcp_config.invalid_json"))?; let is_single_server = value.get("command").is_some() || value.get("url").is_some(); if is_single_server { @@ -122,7 +125,11 @@ pub(super) fn validate_mcp_servers(mcp_servers: &Map) -> anyhow:: fn validate_server_config(server_name: &str, config: &Value) -> anyhow::Result<()> { let obj = config.as_object().ok_or_else(|| { - anyhow::anyhow!("MCP server '{server_name}' config must be a JSON object") + anyhow::anyhow!( + "{}", + i18n::t("ai.agent_sdk.mcp_config.config_must_be_object") + .replace("{server_name}", server_name) + ) })?; let has_warp_id = obj.contains_key("warp_id"); @@ -132,38 +139,63 @@ fn validate_server_config(server_name: &str, config: &Value) -> anyhow::Result<( let kind_count = usize::from(has_warp_id) + usize::from(has_command) + usize::from(has_url); if kind_count != 1 { anyhow::bail!( - "MCP server '{server_name}' must have exactly one of: 'warp_id', 'command', or 'url'" + "{}", + i18n::t("ai.agent_sdk.mcp_config.exactly_one_transport") + .replace("{server_name}", server_name) ); } if has_warp_id { let warp_id = obj.get("warp_id").and_then(Value::as_str).ok_or_else(|| { - anyhow::anyhow!("MCP server '{server_name}' field 'warp_id' must be a string") + anyhow::anyhow!( + "{}", + i18n::t("ai.agent_sdk.mcp_config.field_must_be_string") + .replace("{server_name}", server_name) + .replace("{field}", "warp_id") + ) })?; uuid::Uuid::parse_str(warp_id).with_context(|| { - format!("MCP server '{server_name}' field 'warp_id' must be a UUID") + i18n::t("ai.agent_sdk.mcp_config.warp_id_must_be_uuid") + .replace("{server_name}", server_name) })?; } if has_command { let command = obj.get("command").and_then(Value::as_str).ok_or_else(|| { - anyhow::anyhow!("MCP server '{server_name}' field 'command' must be a string") + anyhow::anyhow!( + "{}", + i18n::t("ai.agent_sdk.mcp_config.field_must_be_string") + .replace("{server_name}", server_name) + .replace("{field}", "command") + ) })?; if command.is_empty() { - anyhow::bail!("MCP server '{server_name}' field 'command' must be non-empty"); + anyhow::bail!( + "{}", + i18n::t("ai.agent_sdk.mcp_config.field_must_be_non_empty") + .replace("{server_name}", server_name) + .replace("{field}", "command") + ); } if let Some(args) = obj.get("args") { let args = args.as_array().ok_or_else(|| { - anyhow::anyhow!("MCP server '{server_name}' field 'args' must be an array") + anyhow::anyhow!( + "{}", + i18n::t("ai.agent_sdk.mcp_config.args_must_be_array") + .replace("{server_name}", server_name) + ) })?; for (idx, arg) in args.iter().enumerate() { if !arg.is_string() { anyhow::bail!( - "MCP server '{server_name}' field 'args[{idx}]' must be a string" + "{}", + i18n::t("ai.agent_sdk.mcp_config.args_item_must_be_string") + .replace("{server_name}", server_name) + .replace("{idx}", &idx.to_string()) ); } } @@ -172,11 +204,21 @@ fn validate_server_config(server_name: &str, config: &Value) -> anyhow::Result<( if has_url { let url = obj.get("url").and_then(Value::as_str).ok_or_else(|| { - anyhow::anyhow!("MCP server '{server_name}' field 'url' must be a string") + anyhow::anyhow!( + "{}", + i18n::t("ai.agent_sdk.mcp_config.field_must_be_string") + .replace("{server_name}", server_name) + .replace("{field}", "url") + ) })?; if url.is_empty() { - anyhow::bail!("MCP server '{server_name}' field 'url' must be non-empty"); + anyhow::bail!( + "{}", + i18n::t("ai.agent_sdk.mcp_config.field_must_be_non_empty") + .replace("{server_name}", server_name) + .replace("{field}", "url") + ); } } @@ -196,12 +238,23 @@ fn validate_string_map_field( }; let map = value.as_object().ok_or_else(|| { - anyhow::anyhow!("MCP server '{server_name}' field '{field}' must be an object") + anyhow::anyhow!( + "{}", + i18n::t("ai.agent_sdk.mcp_config.field_must_be_object") + .replace("{server_name}", server_name) + .replace("{field}", field) + ) })?; for (key, value) in map { if !value.is_string() { - anyhow::bail!("MCP server '{server_name}' field '{field}.{key}' must be a string"); + anyhow::bail!( + "{}", + i18n::t("ai.agent_sdk.mcp_config.nested_field_must_be_string") + .replace("{server_name}", server_name) + .replace("{field}", field) + .replace("{key}", key) + ); } } diff --git a/app/src/ai/agent_sdk/mcp_config_tests.rs b/app/src/ai/agent_sdk/mcp_config_tests.rs index edac7a61af..33d3c3261f 100644 --- a/app/src/ai/agent_sdk/mcp_config_tests.rs +++ b/app/src/ai/agent_sdk/mcp_config_tests.rs @@ -3,6 +3,10 @@ use warp_cli::mcp::MCPSpec; use super::build_mcp_servers_from_specs; +fn use_english_locale() { + i18n::set_locale("en"); +} + fn build(specs: Vec) -> Map { build_mcp_servers_from_specs(&specs) .expect("builder should not error") @@ -171,6 +175,8 @@ fn single_server_shorthand_url_is_wrapped() { #[test] fn merge_multiple_specs_and_duplicate_name_errors() { + use_english_locale(); + let s1 = json!({ "mcpServers": { "a": { "command": "npx", "args": [] } } }).to_string(); let s2 = json!({ "mcpServers": { "b": { "url": "https://example.com/mcp" } } }).to_string(); @@ -208,6 +214,8 @@ fn preserves_escaped_strings_in_env_values() { #[test] fn validation_rejects_invalid_entries() { + use_english_locale(); + // Both command and url. let spec = json!({ "mcpServers": { diff --git a/app/src/ai/agent_sdk/mod.rs b/app/src/ai/agent_sdk/mod.rs index 4df462a952..9eacb3fd90 100644 --- a/app/src/ai/agent_sdk/mod.rs +++ b/app/src/ai/agent_sdk/mod.rs @@ -113,8 +113,8 @@ fn maybe_warn_team_api_key(ctx: &AppContext) { } eprintln!( - "\x1b[33mWarning: Free cloud credits apply to personal runs only but this run uses \ - a team API key. If you want to use free cloud credits, consider using a personal API key instead.\x1b[0m" + "\x1b[33m{}\x1b[0m", + i18n::t("ai.agent_sdk.team_api_key_free_credits_warning") ); } @@ -140,7 +140,7 @@ fn dispatch_command( CliCommand::Agent(agent_cmd) => run_agent(ctx, global_options, agent_cmd), CliCommand::Environment(environment_cmd) => { if !FeatureFlag::CloudEnvironments.is_enabled() { - return Err(anyhow::anyhow!("invalid value 'environment'")); + return Err(invalid_value_error("environment")); } environment::run(ctx, global_options, environment_cmd) } @@ -152,72 +152,85 @@ fn dispatch_command( CliCommand::Whoami => admin::whoami(ctx, global_options.output_format), CliCommand::Provider(provider_cmd) => { if !FeatureFlag::ProviderCommand.is_enabled() { - return Err(anyhow::anyhow!("invalid value 'provider'")); + return Err(invalid_value_error("provider")); } provider::run(ctx, global_options, provider_cmd) } #[cfg(not(target_family = "wasm"))] CliCommand::Integration(integration_cmd) => { if !FeatureFlag::IntegrationCommand.is_enabled() { - return Err(anyhow::anyhow!("invalid value 'integration'")); + return Err(invalid_value_error("integration")); } integration::run(ctx, global_options, integration_cmd) } #[cfg(target_family = "wasm")] CliCommand::Integration(_) => { - return Err(anyhow::anyhow!("invalid value 'integration'")); + return Err(invalid_value_error("integration")); } CliCommand::Schedule(schedule_cmd) => { if !FeatureFlag::ScheduledAmbientAgents.is_enabled() { - return Err(anyhow::anyhow!("invalid value 'schedule'")); + return Err(invalid_value_error("schedule")); } schedule::run(ctx, global_options, schedule_cmd) } CliCommand::Secret(secret_cmd) => { if !FeatureFlag::WarpManagedSecrets.is_enabled() { - return Err(anyhow::anyhow!("invalid value 'secret'")); + return Err(invalid_value_error("secret")); } secret::run(ctx, global_options, secret_cmd) } CliCommand::Federate(federate_cmd) => { if !FeatureFlag::OzIdentityFederation.is_enabled() { - return Err(anyhow::anyhow!("invalid value 'federate'")); + return Err(invalid_value_error("federate")); } federate::run(ctx, global_options, federate_cmd) } CliCommand::HarnessSupport(args) => { if !FeatureFlag::AgentHarness.is_enabled() { - return Err(anyhow::anyhow!("invalid value 'harness-support'")); + return Err(invalid_value_error("harness-support")); } harness_support::run(ctx, global_options, args) } CliCommand::Artifact(artifact_cmd) => { if !FeatureFlag::ArtifactCommand.is_enabled() { - return Err(anyhow::anyhow!("invalid value 'artifact'")); + return Err(invalid_value_error("artifact")); } artifact::run(ctx, global_options, artifact_cmd) } CliCommand::ApiKey(api_key_cmd) => { if !FeatureFlag::APIKeyManagement.is_enabled() { - return Err(anyhow::anyhow!("invalid value 'api-key'")); + return Err(invalid_value_error("api-key")); } api_key::run(ctx, global_options, api_key_cmd) } } } +fn invalid_value_error(value: &str) -> anyhow::Error { + anyhow::anyhow!(i18n::t("ai.agent_sdk.invalid_value").replace("{value}", value)) +} + +fn unexpected_argument_error(argument: &str) -> anyhow::Error { + anyhow::anyhow!( + i18n::t("ai.agent_sdk.unexpected_argument_found").replace("{argument}", argument) + ) +} + +fn unsupported_local_child_harness_reason(harness: &str) -> String { + i18n::t("ai.agent_sdk.harness.local_child_only").replace("{harness}", harness) +} + fn format_skill_resolution_error(err: ResolveSkillError) -> String { match err { ResolveSkillError::NotFound { skill } => { - format!("Skill '{skill}' not found") + i18n::t("ai.agent_sdk.skill_resolution.skill_not_found").replace("{skill}", &skill) } ResolveSkillError::RepoNotFound { repo } => { - format!("Repository '{repo}' not found") + i18n::t("ai.agent_sdk.skill_resolution.repository_not_found").replace("{repo}", &repo) } ResolveSkillError::Ambiguous { skill, candidates } => { - let mut msg = format!( - "Skill '{skill}' is ambiguous; specify as repo:skill_name\n\nCandidates:\n" - ); + let mut msg = + i18n::t("ai.agent_sdk.skill_resolution.ambiguous").replace("{skill}", &skill); for path in candidates { msg.push_str(&format!("- {}\n", path.display())); } @@ -227,14 +240,20 @@ fn format_skill_resolution_error(err: ResolveSkillError) -> String { repo, expected, found, - } => { - format!("Repository '{repo}' found but belongs to org '{found}', expected '{expected}'") - } + } => i18n::t("ai.agent_sdk.skill_resolution.org_mismatch") + .replace("{repo}", &repo) + .replace("{found}", &found) + .replace("{expected}", &expected), ResolveSkillError::ParseFailed { path, message } => { - format!("Failed to parse skill file {}: {message}", path.display()) + i18n::t("ai.agent_sdk.skill_resolution.parse_failed") + .replace("{path}", &path.display().to_string()) + .replace("{message}", &message) } ResolveSkillError::CloneFailed { org, repo, message } => { - format!("Failed to clone repository '{org}/{repo}': {message}") + i18n::t("ai.agent_sdk.skill_resolution.clone_failed") + .replace("{org}", &org) + .replace("{repo}", &repo) + .replace("{message}", &message) } } } @@ -248,23 +267,21 @@ fn run_agent( match command { AgentCommand::Run(args) => { if args.environment.is_some() && !FeatureFlag::CloudEnvironments.is_enabled() { - return Err(anyhow::anyhow!("unexpected argument '--environment' found")); + return Err(unexpected_argument_error("--environment")); } if args.conversation.is_some() && !FeatureFlag::CloudConversations.is_enabled() { - return Err(anyhow::anyhow!( - "unexpected argument '--conversation' found" - )); + return Err(unexpected_argument_error("--conversation")); } if args.skill.is_some() && !FeatureFlag::OzPlatformSkills.is_enabled() { - return Err(anyhow::anyhow!("unexpected argument '--skill' found")); + return Err(unexpected_argument_error("--skill")); } if args.harness != Harness::Oz && !FeatureFlag::AgentHarness.is_enabled() { - return Err(anyhow::anyhow!("unexpected argument '--harness' found")); + return Err(unexpected_argument_error("--harness")); } if args.harness == Harness::OpenCode { - return Err(anyhow::anyhow!( - "The opencode harness is only supported for local child agent launches." - )); + return Err(anyhow::anyhow!(unsupported_local_child_harness_reason( + "opencode" + ))); } let server_api = ServerApiProvider::handle(ctx).as_ref(ctx).get_ai_client(); @@ -295,20 +312,18 @@ fn run_agent( if args.environment.environment.is_some() && !FeatureFlag::CloudEnvironments.is_enabled() { - return Err(anyhow::anyhow!("unexpected argument '--environment' found")); + return Err(unexpected_argument_error("--environment")); } if args.conversation.is_some() && !FeatureFlag::CloudConversations.is_enabled() { - return Err(anyhow::anyhow!( - "unexpected argument '--conversation' found" - )); + return Err(unexpected_argument_error("--conversation")); } if args.harness != Harness::Oz && !FeatureFlag::AgentHarness.is_enabled() { - return Err(anyhow::anyhow!("unexpected argument '--harness' found")); + return Err(unexpected_argument_error("--harness")); } if args.claude_auth_secret.is_some() && args.harness != Harness::Claude { - return Err(anyhow::anyhow!( - "--claude-auth-secret is only valid with --harness claude." - )); + return Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.claude_auth_secret_harness_only" + ))); } ambient::run_ambient_agent(ctx, args) } @@ -557,9 +572,9 @@ fn run_task( TaskCommand::Get(args) => { if args.conversation { if !FeatureFlag::ConversationApi.is_enabled() { - return Err(anyhow::anyhow!( - "The --conversation flag is not available in this build" - )); + return Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.conversation_flag_unavailable" + ))); } ambient::get_run_conversation(ctx, args.task_id) } else { @@ -568,9 +583,9 @@ fn run_task( } TaskCommand::Conversation(conv_cmd) => { if !FeatureFlag::ConversationApi.is_enabled() { - return Err(anyhow::anyhow!( - "The 'conversation' subcommand is not available in this build" - )); + return Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.conversation_subcommand_unavailable" + ))); } match conv_cmd { warp_cli::task::ConversationCommand::Get(args) => { @@ -580,9 +595,9 @@ fn run_task( } TaskCommand::Message(message_cmd) => { if !FeatureFlag::OrchestrationV2.is_enabled() { - return Err(anyhow::anyhow!( - "The 'message' subcommand is not available in this build" - )); + return Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.message_subcommand_unavailable" + ))); } ambient::run_message(ctx, global_options, message_cmd) } @@ -699,10 +714,9 @@ impl AgentDriverRunner { // without the flag. Fail loudly so callers don't silently fall back to a // hard-coded STS region. let role_region = bedrock_role_region.ok_or_else(|| { - AgentDriverError::AwsBedrockCredentialsFailed( - "--bedrock-role-region is required when --bedrock-inference-role is set" - .to_string(), - ) + AgentDriverError::AwsBedrockCredentialsFailed(i18n::t( + "ai.agent_sdk.bedrock_role_region_required", + )) })?; // Set the OIDC strategy on the UI thread and kick off the refresh; the // returned future resolves when credentials are committed to the model. @@ -731,9 +745,7 @@ impl AgentDriverRunner { HarnessKind::Unsupported(harness) => { return Err(AgentDriverError::HarnessSetupFailed { harness: harness.to_string(), - reason: format!( - "The {harness} harness is only supported for local child agent launches." - ), + reason: unsupported_local_child_harness_reason(&harness.to_string()), }); } HarnessKind::Oz | HarnessKind::ThirdParty(_) => {} @@ -882,9 +894,12 @@ impl AgentDriverRunner { ) -> Result<(AgentDriverOptions, Task, Option), AgentDriverError> { // Get the working directory let working_dir = match args.cwd.as_ref() { - Some(dir) => dunce::canonicalize(dir) - .with_context(|| format!("Unable to resolve {}", dir.display())), - None => std::env::current_dir().context("Unable to determine working directory"), + Some(dir) => dunce::canonicalize(dir).with_context(|| { + i18n::t("ai.agent_sdk.unable_to_resolve_path") + .replace("{path}", &dir.display().to_string()) + }), + None => std::env::current_dir() + .context(i18n::t("ai.agent_sdk.working_directory_unavailable")), } .map_err(AgentDriverError::ConfigBuildFailed)?; @@ -961,7 +976,9 @@ impl AgentDriverRunner { // Extract the prompt text that we'll pass up to the server when we create the task. let prompt_for_task_creation = match &prompt { Some(Prompt::PlainText(text)) => text.clone(), - Some(Prompt::SavedPrompt(id)) => format!("Saved prompt ({id})"), + Some(Prompt::SavedPrompt(id)) => { + i18n::t("ai.agent_sdk.saved_prompt_source").replace("{id}", id) + } None => skill .as_ref() .map(|s| format!("/{}", s.skill_identifier)) @@ -1311,9 +1328,9 @@ impl AgentDriverRunner { RestorationMode::Continue, ) .ok_or_else(|| { - AgentDriverError::ConversationLoadFailed( - "Failed to convert conversation data to AIConversation".into(), - ) + AgentDriverError::ConversationLoadFailed(i18n::t( + "ai.agent_sdk.conversation_conversion_failed", + )) })?; Ok(Some(driver::ResumeOptions::Oz(Box::new( ConversationRestorationInNewPaneType::Historical { @@ -1337,9 +1354,7 @@ impl AgentDriverRunner { } HarnessKind::Unsupported(harness) => Err(AgentDriverError::HarnessSetupFailed { harness: harness.to_string(), - reason: format!( - "The {harness} harness is only supported for local child agent launches." - ), + reason: unsupported_local_child_harness_reason(&harness.to_string()), }), } } @@ -1493,7 +1508,7 @@ fn launch_command( let auth_state = AuthStateProvider::handle(ctx).as_ref(ctx).get(); if !auth_state.is_logged_in() { return Err(anyhow::anyhow!( - "You are not logged in - please log in with `{cli_name} login` to continue." + i18n::t("ai.agent_sdk.login_required").replace("{cli_name}", &cli_name) )); } @@ -1515,15 +1530,19 @@ fn launch_command( dispatched = true; let auth_state = AuthStateProvider::handle(ctx).as_ref(ctx).get(); let message = if auth_state.is_api_key_authenticated() { - "Your API key is invalid. Please provide a valid key via '--api-key' or the WARP_API_KEY environment variable.".to_string() + i18n::t("ai.agent_sdk.invalid_api_key") } else { - format!("Your credentials are invalid. Please log in again with `{cli_name} login`.") + i18n::t("ai.agent_sdk.invalid_credentials").replace("{cli_name}", &cli_name) }; report_fatal_error(anyhow::anyhow!(message), ctx); } AuthManagerEvent::AuthFailed(err) => { dispatched = true; - report_fatal_error(anyhow::anyhow!("Authentication failed: {err:#}"), ctx); + report_fatal_error( + anyhow::anyhow!(i18n::t("ai.agent_sdk.authentication_failed_with_error") + .replace("{error}", &format!("{err:#}"))), + ctx, + ); } _ => {} } @@ -1557,8 +1576,9 @@ fn report_fatal_error(err: anyhow::Error, ctx: &mut AppContext) { if let Ok(path) = log_file_path() { let _ = write!( message, - "\n\nFor more information, check Warp logs at {}", - path.display() + "\n\n{}", + i18n::t("ai.agent_sdk.check_warp_logs") + .replace("{path}", &path.display().to_string()) ); } } diff --git a/app/src/ai/agent_sdk/model.rs b/app/src/ai/agent_sdk/model.rs index eca520ae9a..097566c5c9 100644 --- a/app/src/ai/agent_sdk/model.rs +++ b/app/src/ai/agent_sdk/model.rs @@ -38,7 +38,9 @@ impl ModelCommandRunner { ctx.spawn(refresh_future, move |_, refresh_result, ctx| { if refresh_result.is_err() { super::report_fatal_error( - anyhow::anyhow!("Timed out refreshing workspace metadata"), + anyhow::anyhow!(i18n::t( + "ai.agent_sdk.model.refresh_workspace_metadata_timeout" + )), ctx, ); return; @@ -76,7 +78,7 @@ struct ModelListItem { impl TableFormat for ModelListItem { fn header() -> Vec { - vec![Cell::new("MODEL ID")] + vec![Cell::new(i18n::t("ai.agent_sdk.model.table.model_id"))] } fn row(&self) -> Vec { diff --git a/app/src/ai/agent_sdk/oauth_flow.rs b/app/src/ai/agent_sdk/oauth_flow.rs index c1e2d346f0..bab922e334 100644 --- a/app/src/ai/agent_sdk/oauth_flow.rs +++ b/app/src/ai/agent_sdk/oauth_flow.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use std::time::Duration; -use anyhow::{anyhow, Result}; +use anyhow::Result; use warp_graphql::queries::get_oauth_connect_tx_status::OauthConnectTxStatus; use warpui::r#async::Timer; @@ -19,7 +19,8 @@ pub async fn poll_oauth_until_terminal( const MAX_ATTEMPTS: u32 = 120; // 10 minutes total // TODO(bens): render some kind of spinner here println!( - "Waiting for authorization to complete... If this doesn't update after authorizing, please restart the command and try again.\n" + "{}\n", + i18n::t("ai.agent_sdk.oauth.waiting_for_authorization") ); for attempt in 1..=MAX_ATTEMPTS { @@ -43,5 +44,7 @@ pub async fn poll_oauth_until_terminal( } } - Err(anyhow!("Timed out waiting for OAuth authorization")) + Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.oauth.authorization_timeout" + ))) } diff --git a/app/src/ai/agent_sdk/output.rs b/app/src/ai/agent_sdk/output.rs index a0349951ae..40a66d83e6 100644 --- a/app/src/ai/agent_sdk/output.rs +++ b/app/src/ai/agent_sdk/output.rs @@ -48,7 +48,8 @@ where T: Serialize, W: std::io::Write, { - serde_json::to_writer_pretty(&mut output, value).context("unable to write JSON output")?; + serde_json::to_writer_pretty(&mut output, value) + .context(i18n::t("ai.agent_sdk.output.write_json_failed"))?; writeln!(&mut output)?; Ok(()) } @@ -59,7 +60,8 @@ where T: Serialize, W: std::io::Write, { - serde_json::to_writer(&mut output, value).context("unable to write JSON output")?; + serde_json::to_writer(&mut output, value) + .context(i18n::t("ai.agent_sdk.output.write_json_failed"))?; writeln!(&mut output)?; Ok(()) } @@ -176,7 +178,7 @@ pub fn print_raw_json(value: serde_json::Value, json_output: &JsonOutput) -> any match json_output.filter.as_ref() { None => { serde_json::to_writer_pretty(&mut out, &value) - .context("unable to write JSON output")?; + .context(i18n::t("ai.agent_sdk.output.write_json_failed"))?; writeln!(&mut out)?; } Some(filter) => run_jq_filter(value, filter, &mut out)?, @@ -213,11 +215,19 @@ fn run_jq_filter( Default::default(), [input_result].into_iter(), // Callback to format invalid input errors. - |err| anyhow::anyhow!("Invalid data: {err}"), + |err| { + anyhow::anyhow!( + "{}", + i18n::t("ai.agent_sdk.output.invalid_data").replace("{error}", &err.to_string()) + ) + }, // Callback to handle filter outputs. |result| match result { Ok(val) => write_filter_output(&val, out), - Err(err) => anyhow::bail!("jq filter error: {err}"), + Err(err) => anyhow::bail!( + "{}", + i18n::t("ai.agent_sdk.output.jq_filter_error").replace("{error}", &err.to_string()) + ), }, )?; @@ -256,7 +266,7 @@ fn write_filter_output(val: &Val, out: &mut W) -> anyhow::Res } Val::Arr(_) | Val::Obj(_) => { jaq_write::write(&mut *out, &pretty_pp(), 0, val) - .context("unable to write jq output as JSON")?; + .context(i18n::t("ai.agent_sdk.output.write_jq_json_failed"))?; writeln!(out)?; } } @@ -277,7 +287,8 @@ where match output_format { OutputFormat::Json => { let items = items.into_iter().collect::>(); - serde_json::to_writer(&mut output, &items).context("unable to write JSON output") + serde_json::to_writer(&mut output, &items) + .context(i18n::t("ai.agent_sdk.output.write_json_failed")) } OutputFormat::Ndjson => { for item in items { diff --git a/app/src/ai/agent_sdk/profiles.rs b/app/src/ai/agent_sdk/profiles.rs index 0330e02353..4e5c963a0b 100644 --- a/app/src/ai/agent_sdk/profiles.rs +++ b/app/src/ai/agent_sdk/profiles.rs @@ -45,7 +45,7 @@ impl ProfilesCommandRunner { let name = profile.data().display_name().to_string(); let id = match profile.sync_id() { Some(SyncId::ServerId(server_id)) => server_id.to_string(), - _ => "Unsynced".to_string(), + _ => i18n::t("ai.agent_sdk.profiles.unsynced"), }; ProfileInfo { id, name } }) @@ -72,7 +72,10 @@ struct ProfileInfo { impl TableFormat for ProfileInfo { fn header() -> Vec { - vec![Cell::new("ID"), Cell::new("Name")] + vec![ + Cell::new(i18n::t("ai.agent_sdk.profiles.table.id")), + Cell::new(i18n::t("ai.agent_sdk.profiles.table.name")), + ] } fn row(&self) -> Vec { diff --git a/app/src/ai/agent_sdk/provider.rs b/app/src/ai/agent_sdk/provider.rs index 268d84c7ca..15a899a078 100644 --- a/app/src/ai/agent_sdk/provider.rs +++ b/app/src/ai/agent_sdk/provider.rs @@ -45,10 +45,10 @@ impl ProviderCommandRunner { if provider_type.allowed_in_team_context() && provider_type.allowed_in_personal_context() { - return Err(anyhow::anyhow!( - "Provider '{}' must be setup for either a team or personal account", - provider_type.slug() - )); + return Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.provider.scope_required" + ) + .replace("{provider}", &provider_type.slug()))); } use_team_auth = provider_type.allowed_in_team_context(); } else if personal { @@ -61,7 +61,9 @@ impl ProviderCommandRunner { let team_uid = match UserWorkspaces::as_ref(ctx).current_team_uid() { Some(uid) => uid, None => { - return Err(anyhow::anyhow!("User is not on a team")); + return Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.provider.user_not_on_team" + ))); } }; format!("{server_url}/oauth/connect/{slug}?principalType=team&principalId={team_uid}") @@ -69,7 +71,12 @@ impl ProviderCommandRunner { format!("{server_url}/oauth/connect/{slug}") }; - println!("To authenticate {slug}, open this URL in your browser: {url}"); + println!( + "{}", + i18n::t("ai.agent_sdk.provider.authenticate_open_url") + .replace("{provider}", &slug) + .replace("{url}", &url) + ); // Open the URL in the default browser ctx.open_url(&url); @@ -103,7 +110,7 @@ impl ProviderCommandRunner { } let allowed_str = allowed_for.join(", "); - let status = "❌ Not Connected".to_string(); // TODO(bens): get this from gql + let status = i18n::t("ai.agent_sdk.provider.status.not_connected"); // TODO(bens): get this from gql ProviderInfo { name, @@ -139,10 +146,10 @@ struct ProviderInfo { impl TableFormat for ProviderInfo { fn header() -> Vec { vec![ - Cell::new("NAME"), - Cell::new("SLUG"), - Cell::new("ALLOWED FOR"), - Cell::new("STATUS"), + Cell::new(i18n::t("ai.agent_sdk.provider.table.name")), + Cell::new(i18n::t("ai.agent_sdk.provider.table.slug")), + Cell::new(i18n::t("ai.agent_sdk.provider.table.allowed_for")), + Cell::new(i18n::t("ai.agent_sdk.provider.table.status")), ] } diff --git a/app/src/ai/agent_sdk/schedule.rs b/app/src/ai/agent_sdk/schedule.rs index a37e83e2c9..93c9a28487 100644 --- a/app/src/ai/agent_sdk/schedule.rs +++ b/app/src/ai/agent_sdk/schedule.rs @@ -76,7 +76,7 @@ fn create(ctx: &mut AppContext, args: CreateScheduleArgs) -> anyhow::Result<()> let environment_id = match EnvironmentChoice::resolve_for_create(environment_args, ctx) { Ok(EnvironmentChoice::None) => { - eprintln!("Scheduling agent to run without an environment."); + eprintln!("{}", i18n::t("ai.agent_sdk.schedule.without_environment")); None } Ok(EnvironmentChoice::Environment { id, .. }) => Some(id), @@ -149,11 +149,18 @@ fn create(ctx: &mut AppContext, args: CreateScheduleArgs) -> anyhow::Result<()> config.agent_config = agent_config; // Print something here because scheduling an agent can take a while. - println!("Scheduling agent {}...", config.name); + println!( + "{}", + i18n::t("ai.agent_sdk.schedule.scheduling_agent").replace("{name}", &config.name) + ); let create_future = manager.create_schedule(config, owner, ctx); ctx.spawn(create_future, |_manager, result, ctx| match result { Ok(sync_id) => { - println!("Scheduled agent: {sync_id}"); + println!( + "{}", + i18n::t("ai.agent_sdk.schedule.scheduled_agent") + .replace("{id}", &sync_id.to_string()) + ); ctx.terminate_app(TerminationMode::ForceTerminate, None); } Err(err) => { @@ -175,6 +182,8 @@ struct ScheduleInfo { last_ran: Option>, next_run: Option>, scope: String, + #[serde(skip_serializing)] + scope_display: String, prompt: String, last_spawn_error: Option, agent_config: AgentConfigSnapshot, @@ -184,6 +193,7 @@ impl ScheduleInfo { fn new( id: String, scope: String, + scope_display: String, config: ScheduledAmbientAgent, history: Option<&ScheduledAgentHistory>, ) -> Self { @@ -197,6 +207,7 @@ impl ScheduleInfo { last_ran, next_run, scope, + scope_display, prompt: config.prompt, last_spawn_error: config.last_spawn_error, agent_config: config.agent_config, @@ -228,18 +239,18 @@ impl ScheduleInfo { impl TableFormat for ScheduleInfo { fn header() -> Vec { vec![ - Cell::new("ID"), - Cell::new("Name"), - Cell::new("Schedule"), - Cell::new("Paused"), - Cell::new("Last ran"), - Cell::new("Next run"), - Cell::new("Scope"), + Cell::new(i18n::t("ai.agent_sdk.schedule.table.id")), + Cell::new(i18n::t("ai.agent_sdk.schedule.table.name")), + Cell::new(i18n::t("ai.agent_sdk.schedule.table.schedule")), + Cell::new(i18n::t("ai.agent_sdk.schedule.table.paused")), + Cell::new(i18n::t("ai.agent_sdk.schedule.table.last_ran")), + Cell::new(i18n::t("ai.agent_sdk.schedule.table.next_run")), + Cell::new(i18n::t("ai.agent_sdk.schedule.table.scope")), ] } fn row(&self) -> Vec { - let paused_display = if self.paused { "Yes" } else { "No" }; + let paused_display = localized_bool(self.paused); vec![ Cell::new(&self.id), Cell::new(&self.name), @@ -247,13 +258,13 @@ impl TableFormat for ScheduleInfo { Cell::new(paused_display), Cell::new(self.last_ran_display()), Cell::new(self.next_run_display()), - Cell::new(&self.scope), + Cell::new(&self.scope_display), ] } } fn print_schedule_info(info: &ScheduleInfo, output_format: OutputFormat) -> anyhow::Result<()> { - let paused_display = if info.paused { "Yes" } else { "No" }; + let paused_display = localized_bool(info.paused); match output_format { OutputFormat::Json => { @@ -262,71 +273,143 @@ fn print_schedule_info(info: &ScheduleInfo, output_format: OutputFormat) -> anyh } OutputFormat::Ndjson => output::write_json_line(info, std::io::stdout()), OutputFormat::Text => { - println!("Name: {}", info.name); - println!("Cron schedule: {}", info.cron_schedule); - println!("Paused: {paused_display}"); + println!( + "{}", + i18n::t("ai.agent_sdk.schedule.detail.name").replace("{name}", &info.name) + ); + println!( + "{}", + i18n::t("ai.agent_sdk.schedule.detail.cron_schedule") + .replace("{schedule}", &info.cron_schedule) + ); + println!( + "{}", + i18n::t("ai.agent_sdk.schedule.detail.paused").replace("{paused}", &paused_display) + ); let last_ran = info.last_ran_display(); let next_run = info.next_run_display(); - println!("Last ran: {last_ran}"); + println!( + "{}", + i18n::t("ai.agent_sdk.schedule.detail.last_ran").replace("{last_ran}", &last_ran) + ); if let Some(error) = &info.last_spawn_error { - println!("Last error: {error}"); + println!( + "{}", + i18n::t("ai.agent_sdk.schedule.detail.last_error").replace("{error}", error) + ); } - println!("Next run: {next_run}"); + println!( + "{}", + i18n::t("ai.agent_sdk.schedule.detail.next_run").replace("{next_run}", &next_run) + ); - println!("Prompt: {}", info.prompt); + println!( + "{}", + i18n::t("ai.agent_sdk.schedule.detail.prompt").replace("{prompt}", &info.prompt) + ); if let Some(environment_id) = &info.agent_config.environment_id { - println!("Environment ID: {environment_id}"); + println!( + "{}", + i18n::t("ai.agent_sdk.schedule.detail.environment_id") + .replace("{id}", environment_id) + ); } if let Some(model_id) = &info.agent_config.model_id { - println!("Model ID: {model_id}"); + println!( + "{}", + i18n::t("ai.agent_sdk.schedule.detail.model_id").replace("{id}", model_id) + ); } if let Some(agent_name) = &info.agent_config.name { - println!("Agent name: {agent_name}"); + println!( + "{}", + i18n::t("ai.agent_sdk.schedule.detail.agent_name") + .replace("{name}", agent_name) + ); } if let Some(skill_spec) = &info.agent_config.skill_spec { - println!("Skill: {skill_spec}"); + println!( + "{}", + i18n::t("ai.agent_sdk.schedule.detail.skill").replace("{skill}", skill_spec) + ); } if let Some(worker_host) = &info.agent_config.worker_host { - println!("Host: {worker_host}"); + println!( + "{}", + i18n::t("ai.agent_sdk.schedule.detail.host").replace("{host}", worker_host) + ); } Ok(()) } OutputFormat::Pretty => { let mut table = output::standard_table(); - table.add_row(vec![Cell::new("Name"), Cell::new(&info.name)]); table.add_row(vec![ - Cell::new("Cron schedule"), + Cell::new(i18n::t("ai.agent_sdk.schedule.table.name")), + Cell::new(&info.name), + ]); + table.add_row(vec![ + Cell::new(i18n::t("ai.agent_sdk.schedule.table.cron_schedule")), Cell::new(&info.cron_schedule), ]); - table.add_row(vec![Cell::new("Paused"), Cell::new(paused_display)]); + table.add_row(vec![ + Cell::new(i18n::t("ai.agent_sdk.schedule.table.paused")), + Cell::new(paused_display), + ]); let last_ran = info.last_ran_display(); let next_run = info.next_run_display(); - table.add_row(vec![Cell::new("Last ran"), Cell::new(last_ran)]); + table.add_row(vec![ + Cell::new(i18n::t("ai.agent_sdk.schedule.table.last_ran")), + Cell::new(last_ran), + ]); if let Some(error) = &info.last_spawn_error { - table.add_row(vec![Cell::new("Last error"), Cell::new(error)]); + table.add_row(vec![ + Cell::new(i18n::t("ai.agent_sdk.schedule.table.last_error")), + Cell::new(error), + ]); } - table.add_row(vec![Cell::new("Next run"), Cell::new(next_run)]); + table.add_row(vec![ + Cell::new(i18n::t("ai.agent_sdk.schedule.table.next_run")), + Cell::new(next_run), + ]); - table.add_row(vec![Cell::new("Prompt"), Cell::new(&info.prompt)]); + table.add_row(vec![ + Cell::new(i18n::t("ai.agent_sdk.schedule.table.prompt")), + Cell::new(&info.prompt), + ]); if let Some(environment_id) = &info.agent_config.environment_id { - table.add_row(vec![Cell::new("Environment ID"), Cell::new(environment_id)]); + table.add_row(vec![ + Cell::new(i18n::t("ai.agent_sdk.schedule.table.environment_id")), + Cell::new(environment_id), + ]); } if let Some(model_id) = &info.agent_config.model_id { - table.add_row(vec![Cell::new("Model ID"), Cell::new(model_id)]); + table.add_row(vec![ + Cell::new(i18n::t("ai.agent_sdk.schedule.table.model_id")), + Cell::new(model_id), + ]); } if let Some(agent_name) = &info.agent_config.name { - table.add_row(vec![Cell::new("Agent name"), Cell::new(agent_name)]); + table.add_row(vec![ + Cell::new(i18n::t("ai.agent_sdk.schedule.table.agent_name")), + Cell::new(agent_name), + ]); } if let Some(skill_spec) = &info.agent_config.skill_spec { - table.add_row(vec![Cell::new("Skill"), Cell::new(skill_spec)]); + table.add_row(vec![ + Cell::new(i18n::t("ai.agent_sdk.schedule.table.skill")), + Cell::new(skill_spec), + ]); } if let Some(worker_host) = &info.agent_config.worker_host { - table.add_row(vec![Cell::new("Host"), Cell::new(worker_host)]); + table.add_row(vec![ + Cell::new(i18n::t("ai.agent_sdk.schedule.table.host")), + Cell::new(worker_host), + ]); } println!("{table}"); @@ -335,6 +418,14 @@ fn print_schedule_info(info: &ScheduleInfo, output_format: OutputFormat) -> anyh } } +fn localized_bool(value: bool) -> String { + if value { + i18n::t("common.yes") + } else { + i18n::t("common.no") + } +} + fn pause(ctx: &mut AppContext, args: PauseScheduleArgs) -> anyhow::Result<()> { let schedule_id = SyncId::ServerId(ServerId::try_from(args.schedule_id)?); @@ -346,11 +437,11 @@ fn pause(ctx: &mut AppContext, args: PauseScheduleArgs) -> anyhow::Result<()> { return; } - println!("Pausing agent..."); + println!("{}", i18n::t("ai.agent_sdk.schedule.pausing_agent")); let pause_future = manager.pause_schedule(schedule_id, ctx); ctx.spawn(pause_future, |_manager, result, ctx| match result { Ok(()) => { - println!("Schedule paused"); + println!("{}", i18n::t("ai.agent_sdk.schedule.paused")); ctx.terminate_app(TerminationMode::ForceTerminate, None); } Err(err) => { @@ -374,11 +465,11 @@ fn unpause(ctx: &mut AppContext, args: UnpauseScheduleArgs) -> anyhow::Result<() return; } - println!("Resuming agent..."); + println!("{}", i18n::t("ai.agent_sdk.schedule.resuming_agent")); let unpause_future = manager.unpause_schedule(schedule_id, ctx); ctx.spawn(unpause_future, |_manager, result, ctx| match result { Ok(()) => { - println!("Schedule unpaused"); + println!("{}", i18n::t("ai.agent_sdk.schedule.unpaused")); ctx.terminate_app(TerminationMode::ForceTerminate, None); } Err(err) => { @@ -499,7 +590,7 @@ fn update(ctx: &mut AppContext, args: UpdateScheduleArgs) -> anyhow::Result<()> args.skill.map(|s| Some(s.to_string())) }; - println!("Updating agent..."); + println!("{}", i18n::t("ai.agent_sdk.schedule.updating_agent")); let update_future = manager.update_schedule( schedule_id, UpdateScheduleParams { @@ -518,7 +609,7 @@ fn update(ctx: &mut AppContext, args: UpdateScheduleArgs) -> anyhow::Result<()> ); ctx.spawn(update_future, |_manager, result, ctx| match result { Ok(()) => { - println!("Schedule updated"); + println!("{}", i18n::t("ai.agent_sdk.schedule.updated")); ctx.terminate_app(TerminationMode::ForceTerminate, None); } Err(err) => { @@ -548,6 +639,8 @@ fn list(ctx: &mut AppContext, output_format: OutputFormat) -> anyhow::Result<()> let config = schedule.model().string_model.clone(); let sync_id = schedule.sync_id(); let scope = super::common::format_owner(&schedule.permissions().owner).to_string(); + let scope_display = + super::common::localized_format_owner(&schedule.permissions().owner); // TODO(ben): Consider a bulk lookup API for scheduled agent history. let history_future = manager.fetch_schedule_history(sync_id, ctx); @@ -567,7 +660,7 @@ fn list(ctx: &mut AppContext, output_format: OutputFormat) -> anyhow::Result<()> SyncId::ClientId(_) => "Unsynced".to_string(), }; - ScheduleInfo::new(id, scope, config, history.as_ref()) + ScheduleInfo::new(id, scope, scope_display, config, history.as_ref()) } }); @@ -602,7 +695,10 @@ fn get( } let Some(schedule) = CloudScheduledAmbientAgent::get_by_id(&schedule_id, ctx) else { - super::report_fatal_error(anyhow::anyhow!("Schedule not found"), ctx); + super::report_fatal_error( + anyhow::anyhow!(i18n::t("ai.agent_sdk.schedule.not_found")), + ctx, + ); return; }; @@ -611,6 +707,8 @@ fn get( SyncId::ClientId(_) => "Unsynced".to_string(), }; let scope = super::common::format_owner(&schedule.permissions().owner).to_string(); + let scope_display = + super::common::localized_format_owner(&schedule.permissions().owner); let config = schedule.model().string_model.clone(); // Don't hold references into the CloudObject store across an async spawn. @@ -625,7 +723,7 @@ fn get( } }; - let info = ScheduleInfo::new(id, scope, config, history.as_ref()); + let info = ScheduleInfo::new(id, scope, scope_display, config, history.as_ref()); if let Err(err) = print_schedule_info(&info, output_format) { super::report_fatal_error(err, ctx); return; @@ -650,11 +748,11 @@ fn delete(ctx: &mut AppContext, args: DeleteScheduleArgs) -> anyhow::Result<()> return; } - println!("Deleting agent..."); + println!("{}", i18n::t("ai.agent_sdk.schedule.deleting_agent")); let delete_future = manager.delete_schedule(schedule_id, ctx); ctx.spawn(delete_future, |_manager, result, ctx| match result { Ok(()) => { - println!("Schedule deleted"); + println!("{}", i18n::t("ai.agent_sdk.schedule.deleted")); ctx.terminate_app(TerminationMode::ForceTerminate, None); } Err(err) => { diff --git a/app/src/ai/agent_sdk/secret.rs b/app/src/ai/agent_sdk/secret.rs index fabcac9bd2..87967c1561 100644 --- a/app/src/ai/agent_sdk/secret.rs +++ b/app/src/ai/agent_sdk/secret.rs @@ -42,11 +42,11 @@ struct SecretInfo { impl TableFormat for SecretInfo { fn header() -> Vec { vec![ - Cell::new("Name"), - Cell::new("Scope"), - Cell::new("Type"), - Cell::new("Created"), - Cell::new("Updated"), + Cell::new(i18n::t("ai.agent_sdk.secret.table.name")), + Cell::new(i18n::t("ai.agent_sdk.secret.table.scope")), + Cell::new(i18n::t("ai.agent_sdk.secret.table.type")), + Cell::new(i18n::t("ai.agent_sdk.secret.table.created")), + Cell::new(i18n::t("ai.agent_sdk.secret.table.updated")), ] } @@ -68,7 +68,9 @@ pub fn run( command: SecretCommand, ) -> Result<()> { if !FeatureFlag::WarpManagedSecrets.is_enabled() { - return Err(anyhow::anyhow!("This feature is not enabled")); + return Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.secret.feature_not_enabled" + ))); } match command { @@ -192,9 +194,9 @@ fn create_secret(ctx: &mut AppContext, args: CreateSecretArgs) -> Result<()> { ), }, None => { - let name = args.name.ok_or_else(|| { - anyhow::anyhow!("Secret name is required. Usage: oz secret create ") - })?; + let name = args + .name + .ok_or_else(|| anyhow::anyhow!(i18n::t("ai.agent_sdk.secret.name_required")))?; ( name, SecretInput::Simple { @@ -266,7 +268,10 @@ fn create_secret_with_input( ); ctx.spawn(create_future, move |_, result, ctx| match result { Ok(secret) => { - println!("Secret '{}' created", secret.name); + println!( + "{}", + i18n::t("ai.agent_sdk.secret.created").replace("{name}", &secret.name) + ); ctx.terminate_app(TerminationMode::ForceTerminate, None); } Err(err) => { @@ -313,28 +318,30 @@ fn delete_secret(ctx: &mut AppContext, args: DeleteSecretArgs) -> Result<()> { if !force { if !io::stdin().is_terminal() { super::report_fatal_error( - anyhow::anyhow!( - "Refusing to delete secret without confirmation in non-interactive mode (use --force to bypass)" - ), + anyhow::anyhow!(i18n::t( + "ai.agent_sdk.secret.delete_refusing_noninteractive" + )), ctx, ); return; } let scope = match owner { - Owner::User { .. } => "personal", - Owner::Team { .. } => "team", + Owner::User { .. } => i18n::t("ai.agent_sdk.secret.scope.personal"), + Owner::Team { .. } => i18n::t("ai.agent_sdk.secret.scope.team"), }; - let should_delete = match Confirm::new(&format!("Delete {scope} secret '{name}'?")) + let prompt = i18n::t("ai.agent_sdk.secret.delete_confirm") + .replace("{scope}", &scope) + .replace("{name}", &name); + let should_delete = match Confirm::new(&prompt) .with_default(false) - .with_help_message("This action cannot be undone") + .with_help_message(&i18n::t("ai.agent_sdk.secret.delete_confirm_help")) .prompt() { Ok(should_delete) => should_delete, Err(InquireError::OperationCanceled | InquireError::OperationInterrupted) => { - ctx - .terminate_app(TerminationMode::ForceTerminate, None); + ctx.terminate_app(TerminationMode::ForceTerminate, None); return; } Err(err) => { @@ -344,9 +351,8 @@ fn delete_secret(ctx: &mut AppContext, args: DeleteSecretArgs) -> Result<()> { }; if !should_delete { - println!("Deletion cancelled"); - ctx - .terminate_app(TerminationMode::ForceTerminate, None); + println!("{}", i18n::t("ai.agent_sdk.secret.deletion_cancelled")); + ctx.terminate_app(TerminationMode::ForceTerminate, None); return; } } @@ -354,9 +360,11 @@ fn delete_secret(ctx: &mut AppContext, args: DeleteSecretArgs) -> Result<()> { let delete_future = manager.delete_secret(secret_owner, name.clone()); ctx.spawn(delete_future, move |_, result, ctx| match result { Ok(()) => { - println!("Secret '{name}' deleted"); - ctx - .terminate_app(TerminationMode::ForceTerminate, None); + println!( + "{}", + i18n::t("ai.agent_sdk.secret.deleted").replace("{name}", &name) + ); + ctx.terminate_app(TerminationMode::ForceTerminate, None); } Err(err) => { super::report_fatal_error(err, ctx); @@ -430,7 +438,8 @@ fn update_secret(ctx: &mut AppContext, args: UpdateSecretArgs) -> Result<()> { Some(t) => t, None => { super::report_fatal_error( - anyhow::anyhow!("Secret '{}' not found", args.name), + anyhow::anyhow!(i18n::t("ai.agent_sdk.secret.not_found") + .replace("{name}", &args.name)), ctx, ); return; @@ -453,7 +462,11 @@ fn update_secret(ctx: &mut AppContext, args: UpdateSecretArgs) -> Result<()> { ); ctx.spawn(update_future, move |_, result, ctx| match result { Ok(secret) => { - println!("Secret '{}' updated", secret.name); + println!( + "{}", + i18n::t("ai.agent_sdk.secret.updated") + .replace("{name}", &secret.name) + ); ctx.terminate_app(TerminationMode::ForceTerminate, None); } Err(err) => { @@ -471,7 +484,10 @@ fn update_secret(ctx: &mut AppContext, args: UpdateSecretArgs) -> Result<()> { ); ctx.spawn(update_future, move |_, result, ctx| match result { Ok(secret) => { - println!("Secret '{}' updated", secret.name); + println!( + "{}", + i18n::t("ai.agent_sdk.secret.updated").replace("{name}", &secret.name) + ); ctx.terminate_app(TerminationMode::ForceTerminate, None); } Err(err) => { @@ -528,7 +544,8 @@ fn list_secrets( fn read_simple_secret_value(args: &ValueArgs) -> Result> { if let Some(value_file) = args.value_file.as_ref() { let value = fs::read_to_string(value_file).with_context(|| { - format!("Failed to read secret value from: {}", value_file.display()) + i18n::t("ai.agent_sdk.secret.read_value_file_failed") + .replace("{path}", &value_file.display().to_string()) })?; if value.is_empty() { Ok(None) @@ -536,7 +553,8 @@ fn read_simple_secret_value(args: &ValueArgs) -> Result> { Ok(Some(value)) } } else if io::stdin().is_terminal() { - let result = Password::new("Secret value:") + let prompt = i18n::t("ai.agent_sdk.secret.secret_value"); + let result = Password::new(&prompt) .with_display_toggle_enabled() .without_confirmation() .prompt(); @@ -592,16 +610,16 @@ fn make_secret_value_from_gql_type( ManagedSecretType::AnthropicApiKey => Ok(ManagedSecretValue::anthropic_api_key(raw)), ManagedSecretType::AnthropicBedrockAccessKey => { // Bedrock access key secrets cannot be updated through the generic raw-string path. - Err(anyhow::anyhow!( - "Bedrock access key secrets cannot be updated via `--value`; re-create the secret instead" - )) + Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.secret.bedrock_access_key_update_not_supported" + ))) } ManagedSecretType::AnthropicBedrockApiKey => { // Bedrock secrets cannot be updated through the generic raw-string path. // The caller should use the dedicated Bedrock creation flow instead. - Err(anyhow::anyhow!( - "Bedrock API key secrets cannot be updated via `--value`; re-create the secret instead" - )) + Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.secret.bedrock_api_key_update_not_supported" + ))) } ManagedSecretType::OpenaiApiKey => Ok(ManagedSecretValue::openai_api_key(raw, None)), } @@ -642,8 +660,10 @@ fn read_openai_api_key_secret_value( // Non-interactive: leave the base URL unset rather than prompting or failing. None } else { - match inquire::Text::new("OpenAI base URL (optional, press Enter to skip):") - .with_help_message("e.g. https://us.api.openai.com/v1 for a regional endpoint") + let prompt = i18n::t("ai.agent_sdk.secret.openai_base_url"); + let help = i18n::t("ai.agent_sdk.secret.openai_base_url_help"); + match inquire::Text::new(&prompt) + .with_help_message(&help) .prompt() { Ok(value) => { @@ -675,11 +695,12 @@ fn read_bedrock_secret_value( Some(k) if !k.is_empty() => k, _ => { if !io::stdin().is_terminal() { - return Err(anyhow::anyhow!( - "Bedrock secrets require --bedrock-api-key and --region in non-interactive mode" - )); + return Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.secret.bedrock_noninteractive_required" + ))); } - let result = Password::new("Bedrock API key:") + let prompt = i18n::t("ai.agent_sdk.secret.bedrock_api_key"); + let result = Password::new(&prompt) .with_display_toggle_enabled() .without_confirmation() .prompt(); @@ -698,11 +719,12 @@ fn read_bedrock_secret_value( Some(r) if !r.is_empty() => r, _ => { if !io::stdin().is_terminal() { - return Err(anyhow::anyhow!( - "Bedrock secrets require --bedrock-api-key and --region in non-interactive mode" - )); + return Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.secret.bedrock_noninteractive_required" + ))); } - let result = inquire::Text::new("AWS Region:").prompt(); + let prompt = i18n::t("ai.agent_sdk.secret.aws_region"); + let result = inquire::Text::new(&prompt).prompt(); match result { Ok(value) if !value.is_empty() => value, Ok(_) => return Ok(None), @@ -730,17 +752,16 @@ fn read_bedrock_access_key_secret_value( session_token: Option, region: Option, ) -> Result> { - // Error message used for all three required fields when running non-interactively. - // --session-token is intentionally omitted because it is optional. - const NON_INTERACTIVE_REQUIRED_MSG: &str = "Bedrock access key secrets require --access-key-id, --secret-access-key, and --region in non-interactive mode"; - let access_key_id = match access_key_id { Some(v) if !v.is_empty() => v, _ => { if !io::stdin().is_terminal() { - return Err(anyhow::anyhow!(NON_INTERACTIVE_REQUIRED_MSG)); + return Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.secret.bedrock_access_key_noninteractive_required" + ))); } - match inquire::Text::new("AWS Access Key ID:").prompt() { + let prompt = i18n::t("ai.agent_sdk.secret.aws_access_key_id"); + match inquire::Text::new(&prompt).prompt() { Ok(value) if !value.is_empty() => value, Ok(_) => return Ok(None), Err(InquireError::OperationCanceled | InquireError::OperationInterrupted) => { @@ -755,9 +776,12 @@ fn read_bedrock_access_key_secret_value( Some(v) if !v.is_empty() => v, _ => { if !io::stdin().is_terminal() { - return Err(anyhow::anyhow!(NON_INTERACTIVE_REQUIRED_MSG)); + return Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.secret.bedrock_access_key_noninteractive_required" + ))); } - match Password::new("AWS Secret Access Key:") + let prompt = i18n::t("ai.agent_sdk.secret.aws_secret_access_key"); + match Password::new(&prompt) .with_display_toggle_enabled() .without_confirmation() .prompt() @@ -783,7 +807,8 @@ fn read_bedrock_access_key_secret_value( // persistent IAM credentials do not need a session token. None } else { - match Password::new("AWS Session Token (optional, press Enter to skip):") + let prompt = i18n::t("ai.agent_sdk.secret.aws_session_token"); + match Password::new(&prompt) .with_display_toggle_enabled() .without_confirmation() .prompt() @@ -804,9 +829,12 @@ fn read_bedrock_access_key_secret_value( Some(r) if !r.is_empty() => r, _ => { if !io::stdin().is_terminal() { - return Err(anyhow::anyhow!(NON_INTERACTIVE_REQUIRED_MSG)); + return Err(anyhow::anyhow!(i18n::t( + "ai.agent_sdk.secret.bedrock_access_key_noninteractive_required" + ))); } - match inquire::Text::new("AWS Region:").prompt() { + let prompt = i18n::t("ai.agent_sdk.secret.aws_region"); + match inquire::Text::new(&prompt).prompt() { Ok(value) if !value.is_empty() => value, Ok(_) => return Ok(None), Err(InquireError::OperationCanceled | InquireError::OperationInterrupted) => { @@ -847,11 +875,15 @@ fn find_secret_type( fn format_secret_type(type_: &ManagedSecretType) -> String { match type_ { - ManagedSecretType::RawValue => "Raw Value".to_string(), + ManagedSecretType::RawValue => i18n::t("ai.agent_sdk.secret.type.raw_value"), ManagedSecretType::Dotenvx => "dotenvx".to_string(), - ManagedSecretType::AnthropicApiKey => "Anthropic API Key".to_string(), - ManagedSecretType::AnthropicBedrockAccessKey => "Anthropic Bedrock Access Key".to_string(), - ManagedSecretType::AnthropicBedrockApiKey => "Anthropic Bedrock API Key".to_string(), - ManagedSecretType::OpenaiApiKey => "OpenAI API Key".to_string(), + ManagedSecretType::AnthropicApiKey => i18n::t("ai.agent_sdk.secret.type.anthropic_api_key"), + ManagedSecretType::AnthropicBedrockAccessKey => { + i18n::t("ai.agent_sdk.secret.type.anthropic_bedrock_access_key") + } + ManagedSecretType::AnthropicBedrockApiKey => { + i18n::t("ai.agent_sdk.secret.type.anthropic_bedrock_api_key") + } + ManagedSecretType::OpenaiApiKey => i18n::t("ai.agent_sdk.secret.type.openai_api_key"), } } diff --git a/app/src/ai/agent_tips.rs b/app/src/ai/agent_tips.rs index 2bc8baeb1b..de0c2a0b6e 100644 --- a/app/src/ai/agent_tips.rs +++ b/app/src/ai/agent_tips.rs @@ -37,9 +37,9 @@ pub trait AITip: Clone { fn description(&self) -> &str; /// Converts the tip to formatted text fragments for rendering. - /// Default implementation adds "Tip: " prefix and parses backtick-wrapped text as inline code. + /// Default implementation adds a localized tip prefix and parses backtick-wrapped text as inline code. fn to_formatted_text(&self, _app: &AppContext) -> Vec { - let text = format!("Tip: {}", self.description()); + let text = i18n::t("ai.agent_tips.tip_prefix").replace("{description}", self.description()); // Style backtick-wrapped text as inline code let parts: Vec<&str> = text.split('`').collect(); @@ -87,28 +87,28 @@ pub enum AgentTipKind { static DEFAULT_TIPS: LazyLock> = LazyLock::new(|| { vec![ AgentTip { - description: "`/` to open the slash-command menu and access quick agent actions.".to_string(), + description_key: "ai.agent_tips.slash_command_menu", link: Some("https://docs.warp.dev/agent-platform/capabilities/slash-commands".to_string()), binding_name: None, action: None, kind: AgentTipKind::SlashCommands, }, AgentTip { - description: " to toggle natural language detection and switch between agent and terminal input.".to_string(), + description_key: "ai.agent_tips.toggle_natural_language_detection", link: Some("https://docs.warp.dev/terminal/input/universal-input#input-modes".to_string()), binding_name: Some(SET_INPUT_MODE_AGENT_ACTION_NAME), action: None, kind: AgentTipKind::General, }, AgentTip { - description: "`/plan` to create a plan for the agent before executing.".to_string(), + description_key: "ai.agent_tips.plan_prompt", link: Some("https://docs.warp.dev/agent-platform/capabilities/planning".to_string()), binding_name: None, action: None, kind: AgentTipKind::SlashCommands, }, AgentTip { - description: " to open the Command Palette and access Warp actions and shortcuts.".to_string(), + description_key: "ai.agent_tips.open_command_palette", link: Some("https://docs.warp.dev/terminal/command-palette".to_string()), binding_name: Some(TOGGLE_COMMAND_PALETTE_KEYBINDING_NAME), action: Some(WorkspaceAction::OpenPalette { @@ -119,224 +119,224 @@ static DEFAULT_TIPS: LazyLock> = LazyLock::new(|| { kind: AgentTipKind::General, }, AgentTip { - description: "Store reusable workflows, notebooks, and prompts in your".to_string(), + description_key: "ai.agent_tips.store_reusable_objects", link: Some("https://docs.warp.dev/knowledge-and-collaboration/warp-drive".to_string()), binding_name: None, action: Some(WorkspaceAction::OpenWarpDrive), kind: AgentTipKind::WarpDrive, }, AgentTip { - description: "Enter a new prompt to redirect the agent while it's running.".to_string(), + description_key: "ai.agent_tips.redirect_running_agent", link: None, binding_name: None, action: None, kind: AgentTipKind::General, }, AgentTip { - description: "`@` to add context from files, blocks, or Warp Drive objects to your prompt.".to_string(), + description_key: "ai.agent_tips.at_add_context", link: Some("https://docs.warp.dev/agent-platform/local-agents/agent-context/using-to-add-context".to_string()), binding_name: None, action: None, kind: AgentTipKind::Context, }, AgentTip { - description: " to attach the prior command output as agent context.".to_string(), + description_key: "ai.agent_tips.attach_prior_command_output", link: Some("https://docs.warp.dev/agent-platform/local-agents/agent-context/blocks-as-context#attaching-blocks-as-context".to_string()), binding_name: Some(SELECT_PREVIOUS_BLOCK_ACTION_NAME), action: None, kind: AgentTipKind::Context, }, AgentTip { - description: "`/init` to index the repo so the agent can understand your codebase.".to_string(), + description_key: "ai.agent_tips.init_index_repo", link: Some("https://docs.warp.dev/agent-platform/capabilities/codebase-context".to_string()), binding_name: None, action: None, kind: AgentTipKind::CodebaseContext, }, AgentTip { - description: "Add agent profiles to customize permissions and models per session.".to_string(), + description_key: "ai.agent_tips.agent_profiles", link: Some("https://docs.warp.dev/agent-platform/capabilities/agent-profiles-permissions".to_string()), binding_name: None, action: None, kind: AgentTipKind::General, }, AgentTip { - description: "Right-click a block to fork the conversation from that point.".to_string(), + description_key: "ai.agent_tips.right_click_fork", link: Some("https://docs.warp.dev/agent-platform/local-agents/interacting-with-agents/conversation-forking".to_string()), binding_name: None, action: None, kind: AgentTipKind::General, }, AgentTip { - description: "Right-click a block to copy a conversation's output.".to_string(), + description_key: "ai.agent_tips.right_click_copy_output", link: Some("https://docs.warp.dev/terminal/blocks/block-actions#copy-input-output-of-block".to_string()), binding_name: None, action: None, kind: AgentTipKind::General, }, AgentTip { - description: "Drag an image into the pane to attach it as agent context.".to_string(), + description_key: "ai.agent_tips.drag_image_context", link: Some("https://docs.warp.dev/agent-platform/local-agents/agent-context/images-as-context".to_string()), binding_name: None, action: None, kind: AgentTipKind::Context, }, AgentTip { - description: "Prompt the agent to control interactive tools like node, python, postgres, gdb, or vim.".to_string(), + description_key: "ai.agent_tips.control_interactive_tools", link: Some("https://docs.warp.dev/agent-platform/capabilities/full-terminal-use".to_string()), binding_name: None, action: None, kind: AgentTipKind::General, }, AgentTip { - description: " to open the code review panel and review the agent's changes.".to_string(), + description_key: "ai.agent_tips.open_code_review_panel", link: Some("https://docs.warp.dev/code/code-review".to_string()), binding_name: Some(TOGGLE_RIGHT_PANEL_BINDING_NAME), action: None, kind: AgentTipKind::Code, }, AgentTip { - description: "`/add-mcp` to add an MCP server to your workspace.".to_string(), + description_key: "ai.agent_tips.add_mcp", link: Some("https://docs.warp.dev/agent-platform/capabilities/mcp".to_string()), binding_name: None, action: None, kind: AgentTipKind::Mcp, }, AgentTip { - description: "`/open-mcp-servers` to view and share MCP servers with your team.".to_string(), + description_key: "ai.agent_tips.open_mcp_servers", link: None, binding_name: None, action: None, kind: AgentTipKind::Mcp, }, AgentTip { - description: "`/create-environment` to turn a repo into a remote docker environment an agent can run in.".to_string(), + description_key: "ai.agent_tips.create_environment", link: Some("https://docs.warp.dev/reference/cli/integration-setup".to_string()), binding_name: None, action: None, kind: AgentTipKind::General, }, AgentTip { - description: "`/add-prompt` to create a reusable prompt for repeatable workflows.".to_string(), + description_key: "ai.agent_tips.add_prompt", link: None, binding_name: None, action: None, kind: AgentTipKind::WarpDrive, }, AgentTip { - description: "`/add-rule` to create a global agent rule.".to_string(), + description_key: "ai.agent_tips.add_rule", link: Some("https://docs.warp.dev/agent-platform/capabilities/rules".to_string()), binding_name: None, action: None, kind: AgentTipKind::Context, }, AgentTip { - description: "`/fork` to create a fresh copy of the current conversation, optionally with a new prompt.".to_string(), + description_key: "ai.agent_tips.fork_conversation", link: Some("https://docs.warp.dev/agent-platform/local-agents/interacting-with-agents/conversation-forking".to_string()), binding_name: None, action: None, kind: AgentTipKind::SlashCommands, }, AgentTip { - description: "`/open-code-review` to open the code review panel and inspect agent-generated diffs.".to_string(), + description_key: "ai.agent_tips.open_code_review_command", link: None, binding_name: None, action: Some(WorkspaceAction::ToggleRightPanel), kind: AgentTipKind::Code, }, AgentTip { - description: "`/new` to start a new agent conversation with clean context.".to_string(), + description_key: "ai.agent_tips.new_conversation", link: Some("https://docs.warp.dev/agent-platform/local-agents/interacting-with-agents".to_string()), binding_name: None, action: None, kind: AgentTipKind::SlashCommands, }, AgentTip { - description: "`/compact` to summarize the current conversation and free up space in the context window.".to_string(), + description_key: "ai.agent_tips.compact_conversation", link: None, binding_name: None, action: None, kind: AgentTipKind::SlashCommands, }, AgentTip { - description: "`/usage` to show your current AI credits usage.".to_string(), + description_key: "ai.agent_tips.usage", link: None, binding_name: None, action: None, kind: AgentTipKind::General, }, AgentTip { - description: "Use the `oz` command to run an Oz agent in headless mode, useful for remote machines.".to_string(), + description_key: "ai.agent_tips.oz_headless", link: Some("https://docs.warp.dev/reference/cli".to_string()), binding_name: None, action: None, kind: AgentTipKind::General, }, AgentTip { - description: "Right-click selected text to attach it as agent context.".to_string(), + description_key: "ai.agent_tips.right_click_selected_text", link: Some("https://docs.warp.dev/agent-platform/local-agents/agent-context/blocks-as-context#attaching-blocks-as-context".to_string()), binding_name: None, action: None, kind: AgentTipKind::Context, }, AgentTip { - description: "Use `AGENTS.md` or `CLAUDE.md` to apply project-scoped rules.".to_string(), + description_key: "ai.agent_tips.project_rules_files", link: Some("https://docs.warp.dev/agent-platform/capabilities/rules#project-rules-1".to_string()), binding_name: None, action: None, kind: AgentTipKind::Context, }, AgentTip { - description: "Paste a URL to attach that webpage as context for the agent.".to_string(), + description_key: "ai.agent_tips.paste_url_context", link: Some("https://docs.warp.dev/agent-platform/local-agents/agent-context/urls-as-context".to_string()), binding_name: None, action: None, kind: AgentTipKind::Context, }, AgentTip { - description: "Warpify a remote SSH session to enable Oz inside that environment.".to_string(), + description_key: "ai.agent_tips.warpify_ssh", link: Some("https://docs.warp.dev/terminal/warpify".to_string()), binding_name: None, action: None, kind: AgentTipKind::General, }, AgentTip { - description: "Switch agent profiles to quickly change models and agent permissions.".to_string(), + description_key: "ai.agent_tips.switch_agent_profiles", link: Some("https://docs.warp.dev/agent-platform/capabilities/agent-profiles-permissions".to_string()), binding_name: None, action: None, kind: AgentTipKind::General, }, AgentTip { - description: "`/init` to generate a `WARP.md` file and define project rules for the agent.".to_string(), + description_key: "ai.agent_tips.init_generate_warp_md", link: Some("https://docs.warp.dev/agent-platform/capabilities/rules".to_string()), binding_name: None, action: None, kind: AgentTipKind::SlashCommands, }, AgentTip { - description: " to auto-approve the agent's commands and diffs for the rest of the session.".to_string(), + description_key: "ai.agent_tips.auto_approve_session", link: Some("https://docs.warp.dev/agent-platform/capabilities/full-terminal-use#session-level-approvals".to_string()), binding_name: Some(TOGGLE_AUTOEXECUTE_MODE_KEYBINDING), action: None, kind: AgentTipKind::General, }, AgentTip { - description: "Type `&` or use the handoff chip to move a local conversation to the cloud.".to_string(), + description_key: "ai.agent_tips.handoff_to_cloud", link: None, binding_name: None, action: None, kind: AgentTipKind::Handoff, }, AgentTip { - description: "Enable desktop notifications to get an alert when an agent needs your attention.".to_string(), + description_key: "ai.agent_tips.enable_desktop_notifications", link: Some("https://docs.warp.dev/agent-platform/cloud-agents/managing-cloud-agents#in-app-agent-notifications".to_string()), binding_name: None, action: None, kind: AgentTipKind::General, }, AgentTip { - description: " to cancel the current agent task.".to_string(), + description_key: "ai.agent_tips.cancel_agent_task", link: None, binding_name: Some(CANCEL_COMMAND_KEYBINDING), action: None, @@ -347,11 +347,11 @@ static DEFAULT_TIPS: LazyLock> = LazyLock::new(|| { #[derive(Clone, Debug)] pub struct AgentTip { - /// The text that will be displayed to the user. This is parsed such that: - /// "Tip: " is added as a prefix, + /// The locale key for the text that will be displayed to the user. This is parsed such that: + /// a localized tip prefix is added, /// "" is replaced with user-defined and platform-specific keybinding referenced by binding_name, /// `text` that is wrapped in backticks is formatted as inline code - pub description: String, + pub description_key: &'static str, pub link: Option, pub binding_name: Option<&'static str>, pub action: Option, @@ -379,11 +379,12 @@ impl AITip for AgentTip { } fn description(&self) -> &str { - &self.description + self.description_key } fn to_formatted_text(&self, app: &AppContext) -> Vec { - let mut text = format!("Tip: {}", self.description); + let mut text = i18n::t("ai.agent_tips.tip_prefix") + .replace("{description}", &i18n::t(self.description_key)); // Replace with the actual keybinding string if let Some(keystroke) = self.keystroke(app) { @@ -428,7 +429,7 @@ impl AITip for AgentTip { // Tips whose description references a keybinding placeholder should only be shown // when the keybinding is actually configured, so we never display the raw // "" string to users. - if self.description.contains("") && self.keystroke(app).is_none() { + if i18n::t(self.description_key).contains("") && self.keystroke(app).is_none() { return false; } true @@ -438,9 +439,13 @@ impl AITip for AgentTip { impl WorkspaceAction { pub fn display_text(&self) -> Option { match self { - WorkspaceAction::OpenPalette { .. } => Some("Open palette".to_string()), - WorkspaceAction::OpenWarpDrive => Some("Warp Drive.".to_string()), - WorkspaceAction::ToggleRightPanel => Some("Show diff view".to_string()), + WorkspaceAction::OpenPalette { .. } => { + Some(i18n::t("ai.agent_tips.action.open_palette")) + } + WorkspaceAction::OpenWarpDrive => Some(i18n::t("ai.agent_tips.action.warp_drive")), + WorkspaceAction::ToggleRightPanel => { + Some(i18n::t("ai.agent_tips.action.show_diff_view")) + } _ => None, } } @@ -455,8 +460,7 @@ pub fn get_agent_tips(ctx: &AppContext) -> Vec { && AISettings::as_ref(ctx).is_voice_input_enabled(ctx) { tips.push(AgentTip { - description: "Hold to speak your prompt directly to the agent." - .to_string(), + description_key: "ai.agent_tips.voice_input", link: Some( "https://docs.warp.dev/agent-platform/local-agents/interacting-with-agents/voice" .to_string(), @@ -538,7 +542,7 @@ impl AITipModel { let still_in_pool = self .tips .iter() - .any(|tip| tip.description == current_tip.description); + .any(|tip| tip.description_key == current_tip.description_key); !still_in_pool || !current_tip.is_tip_applicable(None, ctx) }) diff --git a/app/src/ai/ai_document_view.rs b/app/src/ai/ai_document_view.rs index 8dced4759c..a5c967d313 100644 --- a/app/src/ai/ai_document_view.rs +++ b/app/src/ai/ai_document_view.rs @@ -133,7 +133,9 @@ impl From for AIDocumentEvent { } } -pub const DEFAULT_PLANNING_DOCUMENT_TITLE: &str = "Planning document"; +pub fn default_planning_document_title() -> String { + i18n::t("ai_document.default_planning_document_title") +} /// Entry for the version history dropdown menu. struct VersionMenuEntry { @@ -359,7 +361,7 @@ impl AIDocumentView { let document_title = AIDocumentModel::as_ref(ctx) .get_document(&document_id, document_version) .map(|doc| doc.get_title()) - .unwrap_or_else(|| DEFAULT_PLANNING_DOCUMENT_TITLE.to_string()); + .unwrap_or_else(default_planning_document_title); let pane_configuration = ctx.add_model(|_ctx| PaneConfiguration::new(document_title)); // Create version menu view and subscribe to close events to hide overlay @@ -378,7 +380,7 @@ impl AIDocumentView { ActionButton::new("", NakedTheme) .with_icon(icons::Icon::History) .with_size(ButtonSize::Small) - .with_tooltip("Show version history") + .with_tooltip(i18n::t("ai_document.show_version_history")) .on_click(|ctx| { ctx.dispatch_typed_action( PaneHeaderAction::::CustomAction( @@ -401,10 +403,11 @@ impl AIDocumentView { // Read the actual configured keybinding for the save action let save_action = keybinding_name_to_keystroke(SAVE_FILE_BINDING_NAME, ctx) .map(|k| k.displayed()) - .unwrap_or("Click".to_string()); - let tooltip_text = format!("This plan has changes the agent isn't aware of. {save_action} to stop the agent's current task and send the updated plan"); + .unwrap_or_else(|| i18n::t("common.click")); + let tooltip_text = + i18n::t("ai_document.update_agent_tooltip").replace("{save_action}", &save_action); let update_plan_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Update Agent", PrimaryTheme) + ActionButton::new(i18n::t("ai_document.update_agent"), PrimaryTheme) .with_size(ButtonSize::Small) .with_tooltip(tooltip_text) .with_tooltip_alignment(TooltipAlignment::Right) @@ -420,7 +423,7 @@ impl AIDocumentView { // Create restore button let restore_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Restore", SecondaryTheme) + ActionButton::new(i18n::t("common.restore"), SecondaryTheme) .with_size(ButtonSize::Small) .on_click(|ctx| { ctx.dispatch_typed_action( @@ -660,7 +663,7 @@ impl AIDocumentView { let appearance = Appearance::as_ref(app); let ui_builder = appearance.ui_builder().clone(); let tooltip = ui_builder - .tool_tip("Save and auto-sync this plan to your Warp Drive".to_string()) + .tool_tip(i18n::t("ai_document.save_and_auto_sync_tooltip")) .build() .finish(); let sync_button_mouse_state = self.sync_button_mouse_state.clone(); @@ -712,9 +715,7 @@ impl AIDocumentView { let theme = appearance.theme(); let color = theme.nonactive_ui_detail().into_solid(); let ui_builder = appearance.ui_builder().clone(); - let tooltip_text = - "This plan is synced to your Warp Drive and will auto save any edits you make." - .to_string(); + let tooltip_text = i18n::t("ai_document.auto_save_synced_tooltip"); let synced_status_mouse_state = self.synced_status_mouse_state.clone(); Container::new( ConstrainedBox::new( @@ -771,7 +772,7 @@ impl AIDocumentView { let title = AIDocumentModel::as_ref(app) .get_current_document(&self.document_id) .map(|doc| doc.title.clone()) - .unwrap_or_else(|| DEFAULT_PLANNING_DOCUMENT_TITLE.to_string()); + .unwrap_or_else(default_planning_document_title); let version_button = SavePosition::new( ChildView::new(&self.version_button).finish(), @@ -905,7 +906,9 @@ impl AIDocumentView { .iter() .map(|entry| { let label = if let Some(from_version) = entry.restored_from { - format!("{} (restored from {})", entry.version, from_version) + i18n::t("ai_document.version_restored_from") + .replace("{version}", &entry.version.to_string()) + .replace("{from_version}", &from_version.to_string()) } else { entry.version.to_string() }; @@ -1017,12 +1020,12 @@ impl AIDocumentView { let title = AIDocumentModel::as_ref(ctx) .get_current_document(&self.document_id) .map(|doc| doc.title.clone()) - .unwrap_or_else(|| "Untitled".to_string()); + .unwrap_or_else(|| i18n::t("common.untitled")); // Sanitize the title for use as a filename let sanitized_title = safe_filename(&title); let filename = if sanitized_title.is_empty() { - "Untitled.md".to_string() + format!("{}.md", i18n::t("common.untitled")) } else { format!("{sanitized_title}.md") }; @@ -1144,7 +1147,7 @@ impl TypedActionView for AIDocumentView { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::success("Link copied to clipboard".to_string()), + DismissibleToast::success(i18n::t("ai_document.toast.link_copied")), window_id, ctx, ); @@ -1157,7 +1160,7 @@ impl TypedActionView for AIDocumentView { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::success("Plan ID copied to clipboard".to_string()), + DismissibleToast::success(i18n::t("ai_document.toast.plan_id_copied")), window_id, ctx, ); @@ -1307,13 +1310,13 @@ impl BackingView for AIDocumentView { AIDocumentModel::as_ref(ctx).get_document_warp_drive_object_link(&self.document_id, ctx) { menu_items.push( - MenuItemFields::new("Copy link") + MenuItemFields::new(i18n::t("common.copy_link")) .with_on_select_action(AIDocumentAction::CopyLink(link)) .with_icon(Icon::Link) .into_item(), ); menu_items.push( - MenuItemFields::new("Show in Warp Drive") + MenuItemFields::new(i18n::t("ai_document.show_in_warp_drive")) .with_on_select_action(AIDocumentAction::ShowInWarpDrive) .with_icon(Icon::WarpDrive) .into_item(), @@ -1323,7 +1326,7 @@ impl BackingView for AIDocumentView { #[cfg(feature = "local_fs")] { menu_items.push( - crate::menu::MenuItemFields::new("Save as markdown file") + crate::menu::MenuItemFields::new(i18n::t("ai_document.save_as_markdown_file")) .with_on_select_action(AIDocumentAction::Export) .with_icon(Icon::Download) .into_item(), @@ -1332,7 +1335,7 @@ impl BackingView for AIDocumentView { // Add "Attach to active session" menu item menu_items.push( - MenuItemFields::new("Attach to active session") + MenuItemFields::new(i18n::t("ai_document.attach_to_active_session")) .with_on_select_action(AIDocumentAction::AttachToActiveSession) .with_icon(Icon::Paperclip) .into_item(), @@ -1340,7 +1343,7 @@ impl BackingView for AIDocumentView { // Add "Copy plan ID" menu item menu_items.push( - MenuItemFields::new("Copy plan ID") + MenuItemFields::new(i18n::t("ai_document.copy_plan_id")) .with_on_select_action(AIDocumentAction::CopyPlanId) .with_icon(Icon::Copy) .into_item(), diff --git a/app/src/ai/artifacts/buttons.rs b/app/src/ai/artifacts/buttons.rs index 00fabd2878..7237c7f58a 100644 --- a/app/src/ai/artifacts/buttons.rs +++ b/app/src/ai/artifacts/buttons.rs @@ -141,7 +141,9 @@ fn collect_buttons( } => { // Only show plan button if synced to Warp Drive (has notebook_uid) if let Some(notebook_uid) = notebook_uid { - let button_text = title.clone().unwrap_or("Untitled Plan".to_string()); + let button_text = title + .clone() + .unwrap_or_else(|| i18n::t("ai.artifacts.untitled_plan")); let theme = theme.clone(); buttons.push(ctx.add_typed_action_view(move |_| { make_plan_button(button_text, *notebook_uid, theme) @@ -195,7 +197,7 @@ fn collect_buttons( if !screenshot_uids.is_empty() { let theme = theme.clone(); buttons.push(ctx.add_typed_action_view(move |_| { - make_screenshot_button("Screenshots".to_string(), screenshot_uids, theme) + make_screenshot_button(i18n::t("ai.artifacts.screenshots"), screenshot_uids, theme) })); } @@ -221,7 +223,7 @@ fn make_branch_button(branch: String, theme: Arc) -> Acti make_artifact_button( branch.clone(), Icon::GitBranch, - "Copy branch name", + &i18n::t("ai.artifact.button.copy_branch_name"), Some(AnsiColorIdentifier::Green), ArtifactButtonAction::CopyBranch { branch }, theme, @@ -243,7 +245,7 @@ fn make_pr_button( make_artifact_button( display_text, Icon::Github, - "Open pull request", + &i18n::t("ai.artifact.button.open_pull_request"), None, ArtifactButtonAction::OpenPullRequest { url }, theme, diff --git a/app/src/ai/artifacts/mod.rs b/app/src/ai/artifacts/mod.rs index bc1992cfc1..9dda2e3d68 100644 --- a/app/src/ai/artifacts/mod.rs +++ b/app/src/ai/artifacts/mod.rs @@ -292,7 +292,7 @@ pub fn file_button_label(filename: &str, filepath: &str) -> String { { return filepath_basename.to_string(); } - "File".to_string() + i18n::t("ai.artifacts.file") } pub fn open_screenshot_lightbox( @@ -360,7 +360,7 @@ fn screenshot_lightbox_image_from_download_result( log::warn!("Failed to load screenshot artifact {index}: {e}"); Some(LightboxImage { source: LightboxImageSource::Loading, - description: Some("Failed to load".to_string()), + description: Some(i18n::t("ai.artifacts.failed_to_load")), }) } } @@ -386,7 +386,7 @@ pub fn download_file_artifact( log::warn!("Failed to load file artifact {artifact_uid}: {error}"); show_file_download_toast( &artifact_uid, - DismissibleToast::error("Failed to prepare file download.".to_string()), + DismissibleToast::error(i18n::t("ai.artifacts.download_prepare_failed")), ctx, ); } @@ -447,7 +447,10 @@ fn open_file_download_picker( move |_me, result, ctx| match result { Ok(()) => show_file_download_toast( &artifact_uid, - DismissibleToast::success(format!("Downloaded {toast_filename}.")), + DismissibleToast::success( + i18n::t("ai.artifacts.downloaded") + .replace("{filename}", &toast_filename), + ), ctx, ), Err(error) => { diff --git a/app/src/ai/blocklist/action_model.rs b/app/src/ai/blocklist/action_model.rs index a5f228a51b..c8ac2ca6f7 100644 --- a/app/src/ai/blocklist/action_model.rs +++ b/app/src/ai/blocklist/action_model.rs @@ -52,7 +52,7 @@ use crate::ai::agent::{ AIAgentActionType, AIAgentActionTypeDiscriminants, AIAgentExchange, AIAgentInput, CancellationReason, CreateDocumentsResult, EditDocumentsResult, RequestCommandOutputResult, }; -use crate::ai::ai_document_view::DEFAULT_PLANNING_DOCUMENT_TITLE; +use crate::ai::ai_document_view::default_planning_document_title; use crate::ai::blocklist::action_model::execute::suggest_new_conversation::SuggestNewConversationExecutor; use crate::ai::document::ai_document_model::AIDocumentModel; use crate::ai::get_relevant_files::controller::GetRelevantFilesController; @@ -1325,7 +1325,7 @@ impl BlocklistAIActionModel { .as_ref() .and_then(|t| t.get(index)) .cloned() - .unwrap_or_else(|| DEFAULT_PLANNING_DOCUMENT_TITLE.to_string()); + .unwrap_or_else(default_planning_document_title); doc_model.restore_document( doc_context.document_id, diff --git a/app/src/ai/blocklist/agent_view/agent_input_footer/editor.rs b/app/src/ai/blocklist/agent_view/agent_input_footer/editor.rs index 90ac959e52..0fe545c09e 100644 --- a/app/src/ai/blocklist/agent_view/agent_input_footer/editor.rs +++ b/app/src/ai/blocklist/agent_view/agent_input_footer/editor.rs @@ -19,9 +19,6 @@ use crate::terminal::session_settings::{ }; use crate::{report_if_error, Appearance}; -const AGENT_MODAL_TITLE: &str = "Edit agent toolbelt"; -const CLI_MODAL_TITLE: &str = "Edit CLI agent toolbelt"; - /// Controls which set of items and settings the editor modal operates on. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum AgentToolbarEditorMode { @@ -233,10 +230,11 @@ impl View for AgentToolbarInlineEditor { fn render(&self, app: &AppContext) -> Box { let appearance = Appearance::as_ref(app); + let available_section_label = i18n::t("agent_input_footer.editor.available_chips"); render_chip_editor_sections( &self.chip_configurator, ChipEditorSectionsConfig { - available_section_label: "Available chips", + available_section_label: &available_section_label, is_at_defaults: self.is_at_defaults(), reset_action: AgentToolbarInlineEditorAction::ResetDefault, activate_action: AgentToolbarInlineEditorAction::Activate, @@ -325,10 +323,14 @@ impl AgentToolbarEditorModal { self.is_dirty = false; } - fn modal_title(&self) -> &'static str { + fn modal_title(&self) -> String { match self.mode { - AgentToolbarEditorMode::AgentView => AGENT_MODAL_TITLE, - AgentToolbarEditorMode::CLIAgent => CLI_MODAL_TITLE, + AgentToolbarEditorMode::AgentView => { + i18n::t("agent_input_footer.editor.edit_agent_toolbelt") + } + AgentToolbarEditorMode::CLIAgent => { + i18n::t("agent_input_footer.editor.edit_cli_agent_toolbelt") + } } } } @@ -382,11 +384,13 @@ impl View for AgentToolbarEditorModal { fn render(&self, app: &AppContext) -> Box { let appearance = Appearance::as_ref(app); + let modal_title = self.modal_title(); + let available_section_label = i18n::t("agent_input_footer.editor.available_chips"); render_chip_editor_modal( &self.chip_configurator, ChipEditorModalConfig { - title: self.modal_title(), - available_section_label: "Available chips", + title: &modal_title, + available_section_label: &available_section_label, is_at_defaults: self.is_at_defaults(), is_dirty: self.is_dirty, cancel_action: AgentToolbarEditorAction::Cancel, diff --git a/app/src/ai/blocklist/agent_view/agent_input_footer/environment_selector.rs b/app/src/ai/blocklist/agent_view/agent_input_footer/environment_selector.rs index 7b6aa33f1b..90548ba39c 100644 --- a/app/src/ai/blocklist/agent_view/agent_input_footer/environment_selector.rs +++ b/app/src/ai/blocklist/agent_view/agent_input_footer/environment_selector.rs @@ -168,7 +168,7 @@ impl GenericMenuItem for NewEnvironmentMenuItem { } fn name(&self) -> String { - "New environment".to_string() + i18n::t("agent_input_footer.new_environment") } fn icon(&self, _app: &AppContext) -> Option { @@ -205,7 +205,7 @@ impl EnvironmentSelector { let button = ctx.add_typed_action_view(|_ctx| { ActionButton::new("", AgentInputButtonTheme) .with_icon(Icon::Globe4) - .with_tooltip("Choose an environment") + .with_tooltip(i18n::t("agent_input_footer.choose_environment")) .with_size(ButtonSize::AgentInputButton) .with_disabled_theme(DisabledTheme) .on_click(|ctx| { @@ -430,9 +430,9 @@ impl EnvironmentSelector { let label = if let Some(id) = self.target.selected_environment_id(ctx) { CloudAmbientAgentEnvironment::get_by_id(&id, ctx) .map(|env| env.model().string_model.display_name()) - .unwrap_or_else(|| "New environment".to_string()) + .unwrap_or_else(|| i18n::t("agent_input_footer.new_environment")) } else { - "New environment".to_string() + i18n::t("agent_input_footer.new_environment") }; let is_configuring = self.is_configuring(ctx); diff --git a/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs b/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs index d5e4eb113f..ab96867df9 100644 --- a/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs +++ b/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs @@ -117,17 +117,6 @@ use crate::workspace::ToastStack; use crate::workspace::WorkspaceAction; use crate::workspaces::user_workspaces::UserWorkspaces; -const ENABLE_NLD_TOOLTIP: &str = "Enable terminal command autodetection"; -const DISABLE_NLD_TOOLTIP: &str = "Disable terminal command autodetection"; - -const FAST_FORWARD_ON_TOOLTIP: &str = "Turn off auto-approve all agent actions"; -const FAST_FORWARD_OFF_TOOLTIP: &str = "Auto-approve all agent actions for this task"; -const FAST_FORWARD_LOCKED_TOOLTIP: &str = - "Fast forward is always enabled for cloud agent conversations"; - -const START_REMOTE_CONTROL_TOOLTIP: &str = "Start remote control"; -const START_REMOTE_CONTROL_LOGIN_REQUIRED_TOOLTIP: &str = "Log in to use /remote-control"; - const CLOUD_MODE_V2_FOOTER_GAP: f32 = 4.; /// Voice input state for the CLI agent footer. Unlike the editor-based voice @@ -279,9 +268,9 @@ impl AgentInputFooter { button.set_active(is_nld_enabled, ctx); button.set_tooltip( Some(if is_nld_enabled { - DISABLE_NLD_TOOLTIP + i18n::t("agent_input_footer.disable_command_autodetection") } else { - ENABLE_NLD_TOOLTIP + i18n::t("agent_input_footer.enable_command_autodetection") }), ctx, ); @@ -296,9 +285,9 @@ impl AgentInputFooter { button.set_active(is_nld_enabled, ctx); button.set_tooltip( Some(if is_nld_enabled { - DISABLE_NLD_TOOLTIP + i18n::t("agent_input_footer.disable_command_autodetection") } else { - ENABLE_NLD_TOOLTIP + i18n::t("agent_input_footer.enable_command_autodetection") }), ctx, ); @@ -308,7 +297,7 @@ impl AgentInputFooter { let mic_button = ctx.add_typed_action_view(|_ctx| { let button = ActionButton::new("", ActiveMicButtonTheme) .with_icon(Icon::Microphone) - .with_tooltip("Voice input") + .with_tooltip(i18n::t("agent_input_footer.voice_input")) .with_size(button_size) .with_tooltip_alignment(TooltipAlignment::Left); #[cfg(feature = "voice_input")] @@ -344,7 +333,7 @@ impl AgentInputFooter { let file_button = ctx.add_typed_action_view(|_ctx| { ActionButton::new("", AgentInputButtonTheme) .with_icon(Icon::Plus) - .with_tooltip("Attach file") + .with_tooltip(i18n::t("agent_input_footer.attach_file")) .with_size(button_size) .with_tooltip_alignment(TooltipAlignment::Left) .on_click(|ctx| { @@ -358,7 +347,7 @@ impl AgentInputFooter { let fast_forward_button = ctx.add_typed_action_view(|_ctx| { ActionButton::new("", FastForwardButtonTheme) .with_icon(Icon::FastForward) - .with_tooltip(FAST_FORWARD_OFF_TOOLTIP) + .with_tooltip(i18n::t("agent_input_footer.auto_approve_off")) .with_size(button_size) .with_tooltip_alignment(TooltipAlignment::Left) .with_disabled_theme(FastForwardLockedTheme) @@ -373,7 +362,7 @@ impl AgentInputFooter { let handoff_to_cloud_button = ctx.add_typed_action_view(|_ctx| { ActionButton::new("", AgentInputButtonTheme) .with_icon(Icon::UploadCloud) - .with_tooltip("Hand off to cloud (or type &)") + .with_tooltip(i18n::t("agent_input_footer.hand_off_to_cloud")) .with_size(button_size) .with_tooltip_alignment(TooltipAlignment::Left) .on_click(|ctx| { @@ -384,39 +373,45 @@ impl AgentInputFooter { // CLI agent-specific buttons (only rendered when a CLI agent session is active). let cli_button_size = ButtonSize::AgentInputButton; let file_explorer_button = ctx.add_typed_action_view(|ctx| { - ActionButton::new("File explorer", AgentInputButtonTheme) - .with_icon(Icon::FileCopy) - .with_tooltip("Open file explorer") - .with_size(cli_button_size) - .with_tooltip_alignment(TooltipAlignment::Left) - .with_keybinding( - KeystrokeSource::Binding(TOGGLE_PROJECT_EXPLORER_BINDING_NAME), - ctx, - ) - .with_compact_keybinding(true) - .on_click(|ctx| { - ctx.dispatch_typed_action(AgentInputFooterAction::ToggleFileExplorer); - }) + ActionButton::new( + i18n::t("agent_input_footer.file_explorer"), + AgentInputButtonTheme, + ) + .with_icon(Icon::FileCopy) + .with_tooltip(i18n::t("agent_input_footer.open_file_explorer")) + .with_size(cli_button_size) + .with_tooltip_alignment(TooltipAlignment::Left) + .with_keybinding( + KeystrokeSource::Binding(TOGGLE_PROJECT_EXPLORER_BINDING_NAME), + ctx, + ) + .with_compact_keybinding(true) + .on_click(|ctx| { + ctx.dispatch_typed_action(AgentInputFooterAction::ToggleFileExplorer); + }) }); let rich_input_button = ctx.add_typed_action_view(|ctx| { - ActionButton::new("Rich Input", AgentInputButtonTheme) - .with_icon(Icon::TextInput) - .with_tooltip("Open Rich Input") - .with_size(cli_button_size) - .with_tooltip_alignment(TooltipAlignment::Left) - .with_keybinding( - KeystrokeSource::Binding(OPEN_CLI_AGENT_RICH_INPUT_KEYBINDING), - ctx, - ) - .with_compact_keybinding(true) - .on_click(|ctx| { - ctx.dispatch_typed_action(AgentInputFooterAction::ToggleRichInput); - }) + ActionButton::new( + i18n::t("agent_input_footer.rich_input"), + AgentInputButtonTheme, + ) + .with_icon(Icon::TextInput) + .with_tooltip(i18n::t("agent_input_footer.open_rich_input")) + .with_size(cli_button_size) + .with_tooltip_alignment(TooltipAlignment::Left) + .with_keybinding( + KeystrokeSource::Binding(OPEN_CLI_AGENT_RICH_INPUT_KEYBINDING), + ctx, + ) + .with_compact_keybinding(true) + .on_click(|ctx| { + ctx.dispatch_typed_action(AgentInputFooterAction::ToggleRichInput); + }) }); let settings_button = ctx.add_typed_action_view(|_ctx| { ActionButton::new("", AgentInputButtonTheme) .with_icon(Icon::Settings) - .with_tooltip("Open coding agent settings") + .with_tooltip(i18n::t("agent_input_footer.open_coding_agent_settings")) .with_size(cli_button_size) .with_tooltip_alignment(TooltipAlignment::Left) .on_click(|ctx| { @@ -425,64 +420,76 @@ impl AgentInputFooter { }); let install_plugin_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Enable notifications", InstallPluginButtonTheme) - .with_icon(Icon::Download) - .with_tooltip( - "Install the Warp plugin to enable rich agent notifications within Warp", - ) - .with_size(cli_button_size) - .with_tooltip_alignment(TooltipAlignment::Left) - .with_adjoined_side(AdjoinedSide::Right) - .on_click(|ctx| { - ctx.dispatch_typed_action(AgentInputFooterAction::InstallPlugin); - }) + ActionButton::new( + i18n::t("terminal.plugin_instructions.enable_notifications"), + InstallPluginButtonTheme, + ) + .with_icon(Icon::Download) + .with_tooltip(i18n::t("terminal.plugin_instructions.enable_tooltip")) + .with_size(cli_button_size) + .with_tooltip_alignment(TooltipAlignment::Left) + .with_adjoined_side(AdjoinedSide::Right) + .on_click(|ctx| { + ctx.dispatch_typed_action(AgentInputFooterAction::InstallPlugin); + }) }); let plugin_instructions_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Notifications setup instructions", InstallPluginButtonTheme) - .with_icon(Icon::Info) - .with_tooltip("View instructions to install the Warp plugin") - .with_size(cli_button_size) - .with_tooltip_alignment(TooltipAlignment::Left) - .with_adjoined_side(AdjoinedSide::Right) - .on_click(|ctx| { - ctx.dispatch_typed_action( - AgentInputFooterAction::OpenPluginInstallInstructionsPane, - ); - }) + ActionButton::new( + i18n::t("terminal.plugin_instructions.install_instructions_button"), + InstallPluginButtonTheme, + ) + .with_icon(Icon::Info) + .with_tooltip(i18n::t( + "terminal.plugin_instructions.install_instructions_tooltip", + )) + .with_size(cli_button_size) + .with_tooltip_alignment(TooltipAlignment::Left) + .with_adjoined_side(AdjoinedSide::Right) + .on_click(|ctx| { + ctx.dispatch_typed_action( + AgentInputFooterAction::OpenPluginInstallInstructionsPane, + ); + }) }); let update_plugin_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Update Warp plugin", InstallPluginButtonTheme) - .with_icon(Icon::Download) - .with_tooltip("A new version of the Warp plugin is available") - .with_size(cli_button_size) - .with_tooltip_alignment(TooltipAlignment::Left) - .with_adjoined_side(AdjoinedSide::Right) - .on_click(|ctx| { - ctx.dispatch_typed_action(AgentInputFooterAction::UpdatePlugin); - }) + ActionButton::new( + i18n::t("terminal.plugin_instructions.update_button"), + InstallPluginButtonTheme, + ) + .with_icon(Icon::Download) + .with_tooltip(i18n::t("terminal.plugin_instructions.update_tooltip")) + .with_size(cli_button_size) + .with_tooltip_alignment(TooltipAlignment::Left) + .with_adjoined_side(AdjoinedSide::Right) + .on_click(|ctx| { + ctx.dispatch_typed_action(AgentInputFooterAction::UpdatePlugin); + }) }); let update_instructions_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Plugin update instructions", InstallPluginButtonTheme) - .with_icon(Icon::Info) - .with_tooltip("View instructions to update the Warp plugin") - .with_size(cli_button_size) - .with_tooltip_alignment(TooltipAlignment::Left) - .with_adjoined_side(AdjoinedSide::Right) - .on_click(|ctx| { - ctx.dispatch_typed_action( - AgentInputFooterAction::OpenPluginUpdateInstructionsPane, - ); - }) + ActionButton::new( + i18n::t("terminal.plugin_instructions.update_instructions_button"), + InstallPluginButtonTheme, + ) + .with_icon(Icon::Info) + .with_tooltip(i18n::t( + "terminal.plugin_instructions.update_instructions_tooltip", + )) + .with_size(cli_button_size) + .with_tooltip_alignment(TooltipAlignment::Left) + .with_adjoined_side(AdjoinedSide::Right) + .on_click(|ctx| { + ctx.dispatch_typed_action(AgentInputFooterAction::OpenPluginUpdateInstructionsPane); + }) }); let dismiss_plugin_chip_button = ctx.add_typed_action_view(|_ctx| { ActionButton::new("", InstallPluginButtonTheme) .with_icon(Icon::X) .with_size(cli_button_size) - .with_tooltip("Dismiss") + .with_tooltip(i18n::t("common.dismiss")) .with_tooltip_alignment(TooltipAlignment::Left) .with_adjoined_side(AdjoinedSide::Left) .on_click(|ctx| { @@ -522,7 +529,9 @@ impl AgentInputFooter { #[cfg(not(target_family = "wasm"))] if let CLIAgentSessionsModelEvent::Started { .. } = event { if let Some(agent) = me.cli_agent(ctx) { - let label = format!("Enable {} notifications", agent.display_name()); + let label = + i18n::t("terminal.plugin_instructions.enable_agent_notifications") + .replace("{agent}", agent.display_name()); me.install_plugin_button.update(ctx, |button, ctx| { button.set_label(label, ctx); }); @@ -558,8 +567,9 @@ impl AgentInputFooter { let is_open = matches!(new_input_state, CLIAgentInputState::Open { .. }); me.rich_input_button.update(ctx, |button, ctx| { if is_open { - button.set_label("Hide Rich Input", ctx); - button.set_tooltip(Some("Hide Rich Input"), ctx); + button.set_label(i18n::t("agent_input_footer.hide_rich_input"), ctx); + button + .set_tooltip(Some(i18n::t("agent_input_footer.hide_rich_input")), ctx); button.set_keybinding( Some(KeystrokeSource::Binding( OPEN_CLI_AGENT_RICH_INPUT_KEYBINDING, @@ -567,8 +577,9 @@ impl AgentInputFooter { ctx, ); } else { - button.set_label("Rich Input", ctx); - button.set_tooltip(Some("Open Rich Input"), ctx); + button.set_label(i18n::t("agent_input_footer.rich_input"), ctx); + button + .set_tooltip(Some(i18n::t("agent_input_footer.open_rich_input")), ctx); button.set_keybinding( Some(KeystrokeSource::Binding( OPEN_CLI_AGENT_RICH_INPUT_KEYBINDING, @@ -584,7 +595,7 @@ impl AgentInputFooter { let start_remote_control_button = ctx.add_typed_action_view(|_ctx| { ActionButton::new("/remote-control", RemoteControlButtonTheme) .with_icon(Icon::Phone01) - .with_tooltip(START_REMOTE_CONTROL_TOOLTIP) + .with_tooltip(i18n::t("agent_input_footer.start_remote_control")) .with_size(cli_button_size) .with_tooltip_alignment(TooltipAlignment::Left) .on_click(|ctx| { @@ -593,21 +604,24 @@ impl AgentInputFooter { }); let stop_remote_control_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Stop sharing", RemoteControlButtonTheme) - .with_icon(Icon::StopFilled) - .with_icon_ansi_color(AnsiColorIdentifier::Red) - .with_tooltip("Stop sharing") - .with_size(cli_button_size) - .with_tooltip_alignment(TooltipAlignment::Left) - .on_click(|ctx| { - ctx.dispatch_typed_action(AgentInputFooterAction::StopRemoteControl); - }) + ActionButton::new( + i18n::t("agent_input_footer.stop_sharing"), + RemoteControlButtonTheme, + ) + .with_icon(Icon::StopFilled) + .with_icon_ansi_color(AnsiColorIdentifier::Red) + .with_tooltip(i18n::t("agent_input_footer.stop_sharing")) + .with_size(cli_button_size) + .with_tooltip_alignment(TooltipAlignment::Left) + .on_click(|ctx| { + ctx.dispatch_typed_action(AgentInputFooterAction::StopRemoteControl); + }) }); let context_window_button = ctx.add_typed_action_view(|_ctx| { ActionButton::new("", AgentInputButtonTheme) .with_icon(Icon::ConversationContext0) - .with_tooltip("Context window usage") + .with_tooltip(i18n::t("agent_input_footer.context_window_usage")) .with_size(button_size) .with_tooltip_alignment(TooltipAlignment::Left) }); @@ -1200,11 +1214,7 @@ impl AgentInputFooter { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "Could not automatically install plugin. \ - Please click the chip again for manual installation steps." - .to_owned(), - ), + DismissibleToast::error(i18n::t("agent_input_footer.plugin_auto_install_failed")), window_id, ctx, ); @@ -1217,9 +1227,9 @@ impl AgentInputFooter { #[cfg(not(target_family = "wasm"))] fn handle_plugin_operation( &mut self, - progress_toast: &str, - error_label: &str, - success_toast: &str, + progress_toast: String, + error_label: String, + success_toast: String, operation_kind: PluginChipTelemetryKind, operation: F, ctx: &mut ViewContext, @@ -1278,16 +1288,15 @@ impl AgentInputFooter { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_persistent_toast( - DismissibleToast::default(progress_toast.to_owned()) - .with_object_id(toast_id.clone()), + DismissibleToast::default(progress_toast).with_object_id(toast_id.clone()), window_id, ctx, ); }); let toast_id_for_callback = toast_id.clone(); - let error_label = error_label.to_owned(); - let success_toast = success_toast.to_owned(); + let no_plugin_manager_message = + i18n::t("terminal.plugin_instructions.error.no_plugin_manager"); ctx.spawn( async move { let path_env_var = path_future.await; @@ -1296,7 +1305,7 @@ impl AgentInputFooter { else { return Err(( PluginInstallError { - message: "No plugin manager available".to_owned(), + message: no_plugin_manager_message, log: String::new(), }, None, @@ -1351,10 +1360,12 @@ impl AgentInputFooter { DismissibleToast::error(format!("{error_label}: {err}")); if let Some(log_path) = log_path { toast = toast.with_link( - ToastLink::new("See logs for details".to_owned()) - .with_onclick_action(WorkspaceAction::OpenFilePath { - path: log_path, - }), + ToastLink::new(i18n::t( + "terminal.plugin_instructions.see_logs", + )) + .with_onclick_action( + WorkspaceAction::OpenFilePath { path: log_path }, + ), ); } toast @@ -1378,10 +1389,12 @@ impl AgentInputFooter { .cli_agent(ctx) .and_then(plugin_manager_for) .map(|m| m.install_success_message()) - .unwrap_or("Warp plugin installed. Please restart the session to activate."); + .unwrap_or_else(|| { + i18n::t("terminal.plugin_instructions.success.installed_restart_session") + }); self.handle_plugin_operation( - "Installing Warp plugin...", - "Failed to install Warp plugin", + i18n::t("terminal.plugin_instructions.installing"), + i18n::t("terminal.plugin_instructions.install_failed"), success_msg, PluginChipTelemetryKind::Install, |manager| async move { manager.install().await }, @@ -1395,10 +1408,12 @@ impl AgentInputFooter { .cli_agent(ctx) .and_then(plugin_manager_for) .map(|m| m.update_success_message()) - .unwrap_or("Warp plugin updated. Please restart the session to activate."); + .unwrap_or_else(|| { + i18n::t("terminal.plugin_instructions.success.updated_restart_session") + }); self.handle_plugin_operation( - "Updating Warp plugin...", - "Failed to update Warp plugin", + i18n::t("terminal.plugin_instructions.updating"), + i18n::t("terminal.plugin_instructions.update_failed"), success_msg, PluginChipTelemetryKind::Update, |manager| async move { manager.update().await }, @@ -1772,7 +1787,10 @@ impl AgentInputFooter { match &self.cli_voice_input_state { CLIVoiceInputState::Stopped => { if !crate::ai::AIRequestUsageModel::as_ref(ctx).can_request_voice() { - self.show_cli_voice_error_toast("Voice input limit reached", ctx); + self.show_cli_voice_error_toast( + i18n::t("agent_input_footer.voice_limit_reached"), + ctx, + ); return; } @@ -1888,11 +1906,17 @@ impl AgentInputFooter { } Err(e) => match e { TranscribeError::QuotaLimit => { - self.show_cli_voice_error_toast("Voice input limit reached", ctx); + self.show_cli_voice_error_toast( + i18n::t("agent_input_footer.voice_limit_reached"), + ctx, + ); } _ => { log::error!("Failed to transcribe CLI voice input: {e:?}"); - self.show_cli_voice_error_toast("Failed to transcribe voice input", ctx); + self.show_cli_voice_error_toast( + i18n::t("agent_input_footer.voice_transcribe_failed"), + ctx, + ); } }, } @@ -1920,10 +1944,10 @@ impl AgentInputFooter { } #[cfg(feature = "voice_input")] - fn show_cli_voice_error_toast(&self, message: &str, ctx: &mut ViewContext) { + fn show_cli_voice_error_toast(&self, message: String, ctx: &mut ViewContext) { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::error(message.to_string()); + let toast = DismissibleToast::error(message); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); } @@ -1932,8 +1956,8 @@ impl AgentInputFooter { fn show_cli_microphone_access_toast(&self, ctx: &mut ViewContext) { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::error(String::from( - "Failed to start voice input (you may need to enable Microphone access)", + let toast = DismissibleToast::error(i18n::t( + "agent_input_footer.voice_microphone_access_failed", )); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); @@ -1945,10 +1969,10 @@ impl AgentInputFooter { AISettings::handle(ctx).update(ctx, |settings, ctx| { if let Some(toggle_key) = settings.maybe_setup_first_time_voice(ctx) { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::success(format!( - "Voice input is enabled. You can also press and hold the `{}` key to activate voice input (configure in Settings > AI > Voice)", - toggle_key.display_name() - )); + let toast = DismissibleToast::success( + i18n::t("agent_input_footer.voice_first_time_enabled") + .replace("{key}", &toggle_key.display_name()), + ); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); } @@ -1978,11 +2002,11 @@ impl AgentInputFooter { Icon::FastForward }; let tooltip = if is_force_enabled { - FAST_FORWARD_LOCKED_TOOLTIP + i18n::t("agent_input_footer.auto_approve_locked") } else if is_active { - FAST_FORWARD_ON_TOOLTIP + i18n::t("agent_input_footer.auto_approve_on") } else { - FAST_FORWARD_OFF_TOOLTIP + i18n::t("agent_input_footer.auto_approve_off") }; self.fast_forward_button.update(ctx, |button, ctx| { @@ -2001,9 +2025,9 @@ impl AgentInputFooter { .get() .is_anonymous_or_logged_out(); let tooltip = if login_required { - START_REMOTE_CONTROL_LOGIN_REQUIRED_TOOLTIP + i18n::t("agent_input_footer.remote_control_login_required") } else { - START_REMOTE_CONTROL_TOOLTIP + i18n::t("agent_input_footer.start_remote_control") }; self.start_remote_control_button.update(ctx, |button, ctx| { button.set_disabled(login_required, ctx); @@ -2018,7 +2042,8 @@ impl AgentInputFooter { let usage = conversation.context_window_usage(); let icon = icon_for_context_window_usage(usage); let remaining_pct = ((1.0 - usage) * 100.0).round() as i32; - let tooltip = format!("{remaining_pct}% context remaining"); + let tooltip = i18n::t("agent_input_footer.context_remaining") + .replace("{percent}", &remaining_pct.to_string()); self.context_window_button.update(ctx, |button, ctx| { button.set_icon(Some(icon), ctx); @@ -2323,7 +2348,7 @@ fn render_ftu_callout( Expanded::new( 1., Text::new( - "Now using Full Terminal Agent's default model.", + i18n::t("agent_input_footer.full_terminal_agent_default_model"), appearance.ui_font_family(), appearance.monospace_font_size() - 2., ) diff --git a/app/src/ai/blocklist/agent_view/agent_message_bar.rs b/app/src/ai/blocklist/agent_view/agent_message_bar.rs index ac1c765833..1aa0f6a967 100644 --- a/app/src/ai/blocklist/agent_view/agent_message_bar.rs +++ b/app/src/ai/blocklist/agent_view/agent_message_bar.rs @@ -370,14 +370,14 @@ impl View for AgentMessageBar { Some(FigmaMcpStatus::NotInstalled) => { message.items.push(figma_chip( self.mouse_states.figma_install_button.clone(), - "Get Figma MCP", + i18n::t("ai.agent.figma.get_mcp"), Some(InputAction::FigmaAddButtonClicked), )); } Some(FigmaMcpStatus::Installed) => { message.items.push(figma_chip( self.mouse_states.figma_enable_button.clone(), - "Enable Figma MCP", + i18n::t("ai.agent.figma.enable_mcp"), Some(InputAction::FigmaEnableButtonClicked), )); } @@ -385,7 +385,7 @@ impl View for AgentMessageBar { message.items.push( figma_chip( self.mouse_states.figma_enable_button.clone(), - "Enabling...", + i18n::t("ai.agent.figma.enabling"), None, ) .with_is_disabled(true), @@ -460,7 +460,9 @@ impl MessageProvider> for BootstrappingMessageProducer { { None } else { - Some(Message::from_text("Starting shell...")) + Some(Message::from_text(i18n::t( + "terminal.agent_message_bar.starting_shell", + ))) } } } @@ -511,7 +513,7 @@ impl MessageProvider> for ZeroStateMessageProducer { items.push(MessageItem::clickable( vec![ MessageItem::keystroke(resume_keystroke), - MessageItem::text("to resume conversation"), + MessageItem::text(i18n::t("terminal.message_bar.to_resume_conversation")), ], |ctx| { ctx.dispatch_typed_action(TerminalAction::ResumeConversation); @@ -624,7 +626,7 @@ impl MessageProvider> for ZeroStateMessageProducer { items.push(MessageItem::clickable( vec![ MessageItem::keystroke(conversations_keystroke), - MessageItem::text("open conversation"), + MessageItem::text(i18n::t("terminal.message_bar.open_conversation")), ], |ctx| { ctx.dispatch_typed_action(InputAction::ToggleConversationsMenu); @@ -649,7 +651,7 @@ impl MessageProvider> for ZeroStateMessageProducer { items.push(MessageItem::clickable( vec![ MessageItem::keystroke(code_review_keystroke), - MessageItem::text("for code review"), + MessageItem::text(i18n::t("terminal.message_bar.for_code_review")), ], |ctx| { ctx.dispatch_typed_action(WorkspaceAction::ToggleRightPanel); @@ -675,11 +677,11 @@ impl MessageProvider> for ZeroStateMessageProducer { Keystroke::parse("cmdorctrl-alt-p").expect("keystroke should parse"), ), MessageItem::text(if is_plan_for_this_conversation_open { - "to hide plan" + i18n::t("terminal.message_bar.to_hide_plan") } else if plan_count > 1 { - "to view plans" + i18n::t("terminal.message_bar.to_view_plans") } else { - "to view plan" + i18n::t("terminal.message_bar.to_view_plan") }), ], |ctx| { @@ -699,7 +701,7 @@ impl MessageProvider> for ZeroStateMessageProducer { items.push(MessageItem::clickable( vec![ MessageItem::keystroke(fork_keystroke), - MessageItem::text("to fork and continue"), + MessageItem::text(i18n::t("terminal.message_bar.to_fork_and_continue")), ], |ctx| { ctx.dispatch_typed_action( @@ -839,7 +841,7 @@ impl MessageProvider> for HideShortcutsMessageProducer { key: "?".to_owned(), ..Default::default() }), - MessageItem::text("to hide help"), + MessageItem::text(i18n::t("terminal.message_bar.to_hide_help")), ], |ctx| { ctx.dispatch_typed_action(InputAction::ToggleAgentViewShortcuts); @@ -871,12 +873,16 @@ impl MessageProvider> for AutodetectedBashModeMessageProduc let message = match keybinding_name_to_keystroke(SET_INPUT_MODE_AGENT_ACTION_NAME, app) { Some(keystroke) => Message::new(vec![ - MessageItem::text("autodetected shell command, "), + MessageItem::text(i18n::t( + "terminal.agent_message_bar.autodetected_shell_command_prefix", + )), MessageItem::keystroke(keystroke), - MessageItem::text(" to override"), + MessageItem::text(i18n::t("terminal.message_bar.to_override")), ]) .with_text_color(appearance.theme().ansi_fg_blue()), - None => Message::from_text("autodetected shell command"), + None => Message::from_text(i18n::t( + "terminal.agent_message_bar.autodetected_shell_command", + )), }; Some(message) @@ -968,7 +974,7 @@ impl MessageProvider> for ExitBashModeMessageProducer { color: None, background_color: None, }, - MessageItem::text("to exit shell mode"), + MessageItem::text(i18n::t("terminal.agent_message_bar.to_exit_shell_mode")), ]) .with_text_color(text_color), ) @@ -980,7 +986,7 @@ impl MessageProvider> for ExitBashModeMessageProducer { /// When `action` is `None`, the chip is returned without an action (caller should disable it). fn figma_chip( mouse_state: MouseStateHandle, - label: &'static str, + label: String, action: Option, ) -> MessageItem { let items = vec![ diff --git a/app/src/ai/blocklist/agent_view/agent_view_block.rs b/app/src/ai/blocklist/agent_view/agent_view_block.rs index 3f0bb0390b..dfd7f6127e 100644 --- a/app/src/ai/blocklist/agent_view/agent_view_block.rs +++ b/app/src/ai/blocklist/agent_view/agent_view_block.rs @@ -216,7 +216,7 @@ fn render_deleted_state( .with_main_axis_size(MainAxisSize::Max) .with_child( Text::new( - cached_title.unwrap_or_else(|| "Deleted conversation".to_string()), + cached_title.unwrap_or_else(|| i18n::t("ai.agent_view.deleted_conversation")), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -227,7 +227,7 @@ fn render_deleted_state( }) .finish(), ) - .with_child(render_subtext("Deleted".to_string(), appearance)) + .with_child(render_subtext(i18n::t("ai.agent_view.deleted"), appearance)) .finish(); render_block_container( @@ -292,9 +292,9 @@ impl View for AgentViewEntryBlock { let is_open_elsewhere = is_active && !is_active_in_this_pane; let subtext = if is_open_elsewhere { - Some("Open in different pane") + Some(i18n::t("ai.agent_view.open_in_different_pane")) } else if self.is_restored { - Some("Restored") + Some(i18n::t("ai.agent_view.restored")) } else if !self.is_new && !matches!( self.origin, @@ -302,7 +302,7 @@ impl View for AgentViewEntryBlock { | AgentViewEntryOrigin::AgentRequestedNewConversation ) { - Some("Continued") + Some(i18n::t("ai.agent_view.continued")) } else { None }; @@ -316,7 +316,7 @@ impl View for AgentViewEntryBlock { Text::new( conversation .title() - .unwrap_or("Untitled conversation".to_string()), + .unwrap_or_else(|| i18n::t("ai.agent_view.untitled_conversation")), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -335,7 +335,7 @@ impl View for AgentViewEntryBlock { .finish(), ); if let Some(subtext) = subtext { - title_section.add_child(render_subtext(subtext.to_string(), appearance)); + title_section.add_child(render_subtext(subtext, appearance)); } let conversation_id = self.conversation_id; @@ -485,9 +485,7 @@ impl TypedActionView for AgentViewEntryBlock { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "Couldn't navigate to conversation.".to_string(), - ), + DismissibleToast::error(i18n::t("ai.conversation.navigate_failed")), window_id, ctx, ); diff --git a/app/src/ai/blocklist/agent_view/child_agent_status_card.rs b/app/src/ai/blocklist/agent_view/child_agent_status_card.rs index 9aaf3f9a66..c9db9592ca 100644 --- a/app/src/ai/blocklist/agent_view/child_agent_status_card.rs +++ b/app/src/ai/blocklist/agent_view/child_agent_status_card.rs @@ -194,8 +194,11 @@ impl View for ChildAgentStatusCard { continue; } - let agent_name = child.agent_name().unwrap_or("Agent").to_string(); - let title = child.title().unwrap_or_else(|| "Untitled".to_string()); + let agent_name = child + .agent_name() + .map(|agent_name| agent_name.to_string()) + .unwrap_or_else(|| i18n::t("common.agent")); + let title = child.title().unwrap_or_else(|| i18n::t("common.untitled")); let status_icon = child .status() .status_icon_and_color(appearance.theme(), StatusColorStyle::Standard); diff --git a/app/src/ai/blocklist/agent_view/controller.rs b/app/src/ai/blocklist/agent_view/controller.rs index acb8f056ae..611c17746c 100644 --- a/app/src/ai/blocklist/agent_view/controller.rs +++ b/app/src/ai/blocklist/agent_view/controller.rs @@ -994,9 +994,9 @@ fn exit_confirmation_message( ..Default::default() }, if should_stop_and_exit { - "again to stop and exit" + i18n::t("ai.agent_view.exit_confirmation.stop_and_exit") } else { - "again to exit" + i18n::t("ai.agent_view.exit_confirmation.exit") }, ), ExitConfirmationTrigger::CtrlC => ( @@ -1005,7 +1005,7 @@ fn exit_confirmation_message( ctrl: true, ..Default::default() }, - "again to exit", + i18n::t("ai.agent_view.exit_confirmation.exit"), ), }; @@ -1023,7 +1023,9 @@ fn new_conversation_keybinding_confirmation_message( let appearance = Appearance::handle(app).as_ref(app); Message::new(vec![ MessageItem::keystroke(keystroke), - MessageItem::text("again to start new conversation"), + MessageItem::text(i18n::t( + "ai.agent_view.exit_confirmation.start_new_conversation", + )), ]) .with_text_color(appearance.theme().ansi_fg_magenta()) } diff --git a/app/src/ai/blocklist/agent_view/inline_agent_view_header.rs b/app/src/ai/blocklist/agent_view/inline_agent_view_header.rs index 9f62b9c188..b4032146a0 100644 --- a/app/src/ai/blocklist/agent_view/inline_agent_view_header.rs +++ b/app/src/ai/blocklist/agent_view/inline_agent_view_header.rs @@ -19,13 +19,6 @@ use crate::terminal::TerminalModel; use crate::ui_components::blended_colors; use crate::ui_components::icons::Icon; -const AGENT_PROMPT_TO_INTERACT_MESSAGE: &str = "Prompt agent to interact with"; -const AGENT_WAITING_ON_INSTRUCTIONS_MESSAGE: &str = "Agent is waiting on instructions"; -const AGENT_WAITING_FOR_COMMAND_TO_EXIT_MESSAGE: &str = "Agent is waiting for command to exit"; -const AGENT_BLOCKED_MESSAGE: &str = "Agent needs your permission to continue"; -const AGENT_IN_CONTROL_MESSAGE: &str = "Agent is in control"; -const USER_IN_CONTROL_MESSAGE: &str = "User is in control"; - /// A header rendered as rich content above the active block when Agent View is in inline mode. pub struct InlineAgentViewHeader { terminal_view_id: EntityId, @@ -123,9 +116,10 @@ impl View for InlineAgentViewHeader { blended_colors::text_main(appearance.theme(), header_background).into(), ); let message = if let Some(command) = top_level_command.as_deref() { - format!("{AGENT_PROMPT_TO_INTERACT_MESSAGE} `{command}`") + i18n::t("ai.inline_agent.prompt_interact_with_command") + .replace("{command}", command) } else { - format!("{AGENT_PROMPT_TO_INTERACT_MESSAGE} the running command") + i18n::t("ai.inline_agent.prompt_interact_with_running_command") }; return HeaderConfig::new(message, app) .with_icon(icon) @@ -148,15 +142,15 @@ impl View for InlineAgentViewHeader { let is_waiting_on_instructions = action.is_none() && !is_streaming && is_agent_in_control && !is_action_blocked; let message = if is_user_in_control { - USER_IN_CONTROL_MESSAGE.to_owned() + i18n::t("ai.inline_agent.user_in_control") } else if is_action_blocked { - AGENT_BLOCKED_MESSAGE.to_owned() + i18n::t("ai.inline_agent.agent_blocked") } else if is_waiting_for_command_to_exit { - AGENT_WAITING_FOR_COMMAND_TO_EXIT_MESSAGE.to_owned() + i18n::t("ai.inline_agent.waiting_for_command_exit") } else if is_waiting_on_instructions { - AGENT_WAITING_ON_INSTRUCTIONS_MESSAGE.to_owned() + i18n::t("ai.inline_agent.waiting_on_instructions") } else { - AGENT_IN_CONTROL_MESSAGE.to_owned() + i18n::t("ai.inline_agent.agent_in_control") }; let icon = if is_user_in_control || is_waiting_on_instructions { diff --git a/app/src/ai/blocklist/agent_view/orchestration_conversation_links.rs b/app/src/ai/blocklist/agent_view/orchestration_conversation_links.rs index 1957a3549c..15e1fcacb4 100644 --- a/app/src/ai/blocklist/agent_view/orchestration_conversation_links.rs +++ b/app/src/ai/blocklist/agent_view/orchestration_conversation_links.rs @@ -144,11 +144,11 @@ pub(crate) fn parent_conversation_navigation_card( let parent_title = BlocklistAIHistoryModel::as_ref(app) .conversation(&parent_conversation_id) .and_then(|conversation| conversation.title()) - .unwrap_or_else(|| "Parent conversation".to_string()); + .unwrap_or_else(|| i18n::t("ai.orchestration.parent_conversation")); let action = conversation_navigation_action(parent_conversation_id, app)?; Some(conversation_navigation_card( parent_title, - Some("Back to parent conversation".to_string()), + Some(i18n::t("ai.orchestration.back_to_parent_conversation")), move |ctx, _, _| { ctx.dispatch_typed_action(action.clone()); }, diff --git a/app/src/ai/blocklist/agent_view/orchestration_pill_bar.rs b/app/src/ai/blocklist/agent_view/orchestration_pill_bar.rs index ffcb47cc0d..3284c0a217 100644 --- a/app/src/ai/blocklist/agent_view/orchestration_pill_bar.rs +++ b/app/src/ai/blocklist/agent_view/orchestration_pill_bar.rs @@ -2,6 +2,7 @@ //! orchestrator and its child agents. Clicking a pill switches the //! active pane to that agent's conversation. +use std::borrow::Cow; use std::cell::RefCell; use std::collections::hash_map::DefaultHasher; use std::collections::{HashMap, HashSet}; @@ -359,7 +360,7 @@ impl Entity for OrchestrationPillBar { impl OrchestrationPillBar { fn overflow_menu_item( - label: &'static str, + label: String, icon: Icon, action: OrchestrationPillBarAction, hover_background: Fill, @@ -488,19 +489,19 @@ impl OrchestrationPillBar { let mut items = if is_open_elsewhere { vec![item( - "Focus pane", + i18n::t("ai.orchestration.focus_pane"), Icon::ArrowSplit, OrchestrationPillBarAction::FocusOpenedConversation(conversation_id), )] } else { vec![ item( - "Open in new pane", + i18n::t("ai.orchestration.open_in_new_pane"), Icon::ArrowSplit, OrchestrationPillBarAction::OpenInNewPane(conversation_id), ), item( - "Open in new tab", + i18n::t("ai.orchestration.open_in_new_tab"), Icon::Plus, OrchestrationPillBarAction::OpenInNewTab(conversation_id), ), @@ -508,7 +509,7 @@ impl OrchestrationPillBar { }; if Self::oz_run_url_for_conversation(conversation_id, ctx).is_some() { items.push(item( - "View in Oz", + i18n::t("ai.orchestration.view_in_oz"), Icon::Oz, OrchestrationPillBarAction::ViewInOz(conversation_id), )); @@ -529,15 +530,15 @@ impl OrchestrationPillBar { items.push(MenuItem::Separator); if is_in_progress { items.push(destructive_item( - "Stop agent", + i18n::t("ai.orchestration.stop_agent"), Icon::StopFilled, OrchestrationPillBarAction::Stop(conversation_id), )); } let (kill_label, kill_icon) = if is_in_finished_state { - ("Delete agent", Icon::Trash) + (i18n::t("ai.orchestration.delete_agent"), Icon::Trash) } else { - ("Kill agent", Icon::X) + (i18n::t("ai.orchestration.kill_agent"), Icon::X) }; items.push(destructive_item( kill_label, @@ -678,10 +679,11 @@ impl OrchestrationPillBar { // Stamp each child's current pin state; partitioning happens at render. let pill_bar_model = OrchestrationPillBarModel::as_ref(app); for child in children { - let name = child + let name: Cow<'_, str> = child .agent_name() .filter(|n| !n.is_empty()) - .unwrap_or("Agent"); + .map(Cow::Borrowed) + .unwrap_or_else(|| Cow::Owned(i18n::t("ai.orchestration.agent"))); let pin_state = if pill_bar_model.is_pinned(&child.id()) { PillPinState::Pinned } else { @@ -690,8 +692,8 @@ impl OrchestrationPillBar { specs.push(PillSpec { conversation_id: child.id(), label: name.to_string(), - avatar_color: pill_avatar_color(name, theme), - avatar_glyph: AvatarGlyph::Letter(pill_initial(name)), + avatar_color: pill_avatar_color(name.as_ref(), theme), + avatar_glyph: AvatarGlyph::Letter(pill_initial(name.as_ref())), status: Some(child.status().clone()), is_selected: child.id() == active_id, kind: PillKind::Child, @@ -751,7 +753,7 @@ fn orchestrator_label(orchestrator: &AIConversation) -> String { .agent_name() .filter(|n| !n.is_empty()) .map(|n| n.to_string()) - .unwrap_or_else(|| "Orchestrator".to_string()) + .unwrap_or_else(|| i18n::t("ai.orchestration.orchestrator")) } impl OrchestrationPillBar { @@ -1360,7 +1362,7 @@ fn render_hover_card( .filter(|n| !n.is_empty()) .map(|n| n.to_string()) .or_else(|| conversation.title()) - .unwrap_or_else(|| "Agent".to_string()); + .unwrap_or_else(|| i18n::t("ai.orchestration.agent")); // Header: small avatar disc + bold agent name on the left, status // badge right-aligned. We use the conversation's `ConversationStatus` @@ -1598,7 +1600,7 @@ fn render_status_badge( .with_height(12.) .finish(); let label = Text::new( - status.to_string(), + status.localized_label(), appearance.ui_font_family(), appearance.monospace_font_size() - 2., ) @@ -2303,7 +2305,7 @@ pub fn render_orchestration_breadcrumbs( .filter(|t| !t.is_empty()) .or_else(|| p.agent_name().map(str::to_string)) }) - .unwrap_or_else(|| "Orchestrator".to_string()); + .unwrap_or_else(|| i18n::t("ai.orchestration.orchestrator")); // Treat empty `agent_name` as missing so the label, avatar color, and // initial all consistently fall back to "Agent". Without the @@ -2313,8 +2315,9 @@ pub fn render_orchestration_breadcrumbs( let child_name = active .agent_name() .filter(|n| !n.is_empty()) - .unwrap_or("Agent"); - let child_label = child_name.to_string(); + .map(str::to_string) + .unwrap_or_else(|| i18n::t("ai.orchestration.agent")); + let child_label = child_name.clone(); // Parent crumb uses the Oz glyph on a neutral disc to match the // orchestrator pill in the pill bar. @@ -2331,8 +2334,8 @@ pub fn render_orchestration_breadcrumbs( let child_spec = CrumbSpec { conversation_id: active_id, label: child_label, - avatar_color: pill_avatar_color(child_name, theme), - avatar_glyph: AvatarGlyph::Letter(pill_initial(child_name)), + avatar_color: pill_avatar_color(&child_name, theme), + avatar_glyph: AvatarGlyph::Letter(pill_initial(&child_name)), is_active: true, }; diff --git a/app/src/ai/blocklist/agent_view/shortcuts/mod.rs b/app/src/ai/blocklist/agent_view/shortcuts/mod.rs index c0e57a62a4..d961229478 100644 --- a/app/src/ai/blocklist/agent_view/shortcuts/mod.rs +++ b/app/src/ai/blocklist/agent_view/shortcuts/mod.rs @@ -122,7 +122,7 @@ pub fn render_agent_shortcuts_view( key: "!".to_owned(), ..Default::default() }, - text: "input shell command".into(), + text: i18n::t("ai.agent_shortcuts.input_shell_command").into(), ..Default::default() }, app, @@ -135,7 +135,7 @@ pub fn render_agent_shortcuts_view( key: "/".to_owned(), ..Default::default() }, - text: "for slash commands".into(), + text: i18n::t("ai.agent_shortcuts.slash_commands").into(), ..Default::default() }, app, @@ -147,7 +147,7 @@ pub fn render_agent_shortcuts_view( key: "@".to_owned(), ..Default::default() }, - text: "for file paths and attaching other context".into(), + text: i18n::t("ai.agent_shortcuts.file_paths_context").into(), ..Default::default() }, app, @@ -160,7 +160,7 @@ pub fn render_agent_shortcuts_view( shortcuts.push(render_shortcut( ShortcutProps { keystroke, - text: "open code review".into(), + text: i18n::t("ai.agent_shortcuts.open_code_review").into(), ..Default::default() }, app, @@ -175,7 +175,7 @@ pub fn render_agent_shortcuts_view( shortcuts.push(render_shortcut( ShortcutProps { keystroke, - text: "toggle conversation list".into(), + text: i18n::t("ai.agent_shortcuts.toggle_conversation_list").into(), ..Default::default() }, app, @@ -186,7 +186,7 @@ pub fn render_agent_shortcuts_view( shortcuts.push(render_shortcut( ShortcutProps { keystroke: Keystroke::parse(cmd_or_ctrl_shift("y")).expect("is valid keystroke"), - text: "search and continue conversations".into(), + text: i18n::t("ai.agent_shortcuts.search_continue_conversations").into(), ..Default::default() }, app, @@ -202,7 +202,7 @@ pub fn render_agent_shortcuts_view( shortcuts.push(render_shortcut( ShortcutProps { keystroke: new_conversation_keystroke.clone(), - text: "start a new conversation".into(), + text: i18n::t("ai.agent_shortcuts.start_new_conversation").into(), ..Default::default() }, app, @@ -215,7 +215,7 @@ pub fn render_agent_shortcuts_view( shortcuts.push(render_shortcut( ShortcutProps { keystroke, - text: "toggle auto-accept".into(), + text: i18n::t("ai.agent_shortcuts.toggle_auto_accept").into(), ..Default::default() }, app, @@ -230,7 +230,7 @@ pub fn render_agent_shortcuts_view( ctrl: true, ..Default::default() }, - text: "pause agent".into(), + text: i18n::t("ai.agent_shortcuts.pause_agent").into(), ..Default::default() }, app, @@ -242,7 +242,7 @@ pub fn render_agent_shortcuts_view( key: "escape".to_owned(), ..Default::default() }, - text: "go back to terminal".into(), + text: i18n::t("ai.agent_shortcuts.go_back_to_terminal").into(), ..Default::default() }, app, diff --git a/app/src/ai/blocklist/agent_view/zero_state_block.rs b/app/src/ai/blocklist/agent_view/zero_state_block.rs index 7d67115caf..f07df89d25 100644 --- a/app/src/ai/blocklist/agent_view/zero_state_block.rs +++ b/app/src/ai/blocklist/agent_view/zero_state_block.rs @@ -47,7 +47,6 @@ use crate::terminal::{self, prompt, TerminalModel}; use crate::util::time_format::format_approx_duration_from_now_utc; const CLOUD_AGENT_DOCS_URL: &str = "https://docs.warp.dev/agent-platform/cloud-agents/overview"; -const OZ_UPDATES_SECTION_HEADER: &str = "What's new in Oz"; // The maximum number of Oz updates from the changelog rendered in-line in the 'What's new in Oz section'. const MAX_OZ_UPDATE_COUNT: usize = 4; @@ -399,23 +398,25 @@ impl View for AgentViewZeroStateBlock { let header_props = if self.origin.is_cloud_agent() { HeaderProps { - title: "New Oz cloud agent conversation".into(), + title: i18n::t("ai.zero_state.cloud_agent.title").into(), description: AgentViewDescription::CloudModeWithDocsLink, icon: Icon::OzCloud, } } else { - let mut local_description = - "Send a prompt below to start a new conversation".to_owned(); let active_session = self.active_session(app); let location_label = active_session.as_deref().and_then(|session| { format_session_location(session, self.current_working_directory.as_deref()) }); - if let Some(location_label) = location_label { - local_description += &format!(" in `{location_label}`"); - } + let local_description = match location_label { + Some(location_label) => { + i18n::t("ai.zero_state.local_mode.description_with_location") + .replace("{location}", &location_label) + } + None => i18n::t("ai.zero_state.local_mode.description"), + }; HeaderProps { - title: "New Oz agent conversation".into(), + title: i18n::t("ai.zero_state.local_agent.title").into(), description: AgentViewDescription::PlainText(vec![local_description.into()]), icon: Icon::Oz, } @@ -642,7 +643,7 @@ fn render_title_and_description(props: HeaderProps, app: &AppContext) -> Vec Vec, app: &AppContext) -> Vec, app: &AppContext) -> Vec, app: &AppContext) -> Vec, app: &AppContext) -> Vec, app: &AppContext) -> Vec, app: &AppContext) -> Option, app: &AppContext) -> Option other.clone(), }; let command_text = if display_input.is_null() { - format!("MCP Tool: {name}") + i18n::t("ai.block.mcp_tool").replace("{name}", name) } else { - format!("MCP Tool: {name} ({display_input})") + i18n::t("ai.block.mcp_tool_with_input") + .replace("{name}", name) + .replace("{input}", &display_input.to_string()) }; self.handle_mcp_tool_stream_update(action_id, &command_text, ctx); } @@ -1975,9 +1980,10 @@ impl AIBlock { action: AIAgentActionType::SuggestNewConversation { .. }, .. } => { - let start_new_conversation_button_text = "Start a new conversation".to_owned(); + let start_new_conversation_button_text = + i18n::t("ai.output.start_new_conversation"); let continue_current_conversation_button_text = - "Continue current conversation".to_owned(); + i18n::t("ai.output.continue_current_conversation"); let server_output_id = self.model.server_output_id(ctx); let accept_action = AIBlockAction::StartNewConversationButtonClicked { @@ -3578,7 +3584,7 @@ impl AIBlock { for (index, document) in documents.iter().enumerate() { let title = if document.title.is_empty() { - DEFAULT_PLANNING_DOCUMENT_TITLE.to_string() + default_planning_document_title() } else { document.title.clone() }; @@ -5610,8 +5616,10 @@ fn set_imported_comment_button_disabled( handle.update(ctx, |button, ctx| { button.set_disabled(should_disable, ctx); if should_disable { - let tooltip = repo_path - .map(|path| format!("Navigate to {} to open these comments", path.display_path())); + let tooltip = repo_path.map(|path| { + i18n::t("ai.block.navigate_to_repo_to_open_comments") + .replace("{path}", &path.display_path()) + }); button.set_tooltip(tooltip, ctx); } else { button.set_tooltip(None::, ctx); @@ -6030,7 +6038,7 @@ impl TypedActionView for AIBlock { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::success(String::from("Copied to clipboard")), + DismissibleToast::success(i18n::t("common.copied_to_clipboard")), window_id, ctx, ); @@ -6337,7 +6345,7 @@ impl TypedActionView for AIBlock { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { let toast = - DismissibleToast::default(String::from("Thank you for the feedback!")); + DismissibleToast::default(i18n::t("ai.block.thank_you_for_feedback")); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); diff --git a/app/src/ai/blocklist/block/cli.rs b/app/src/ai/blocklist/block/cli.rs index 9cc3058e01..a058123a45 100644 --- a/app/src/ai/blocklist/block/cli.rs +++ b/app/src/ai/blocklist/block/cli.rs @@ -52,14 +52,7 @@ use crate::ai::agent::{ AIAgentActionType, AIAgentInput, AIAgentOutput, AIAgentOutputMessageType, AIAgentPtyWriteMode, AIAgentText, AIAgentTextSection, CancellationReason, ProgrammingLanguage, WebSearchStatus, }; -use crate::ai::blocklist::block::view_impl::common::{ - render_query_text, UserQueryProps, BLOCKED_ACTION_MESSAGE_FOR_GREP_OR_FILE_GLOB, - BLOCKED_ACTION_MESSAGE_FOR_READING_FILES, BLOCKED_ACTION_MESSAGE_FOR_SEARCHING_CODEBASE, - BLOCKED_ACTION_MESSAGE_FOR_WRITE_TO_LONG_RUNNING_SHELL_COMMAND, - LOAD_OUTPUT_MESSAGE_FOR_FILE_GLOB, LOAD_OUTPUT_MESSAGE_FOR_GREP, - LOAD_OUTPUT_MESSAGE_FOR_READING_FILES, LOAD_OUTPUT_MESSAGE_FOR_SEARCH_CODEBASE, - LOAD_OUTPUT_MESSAGE_FOR_WEB_SEARCH, -}; +use crate::ai::blocklist::block::view_impl::common::{render_query_text, UserQueryProps}; use crate::ai::blocklist::block::TextLocation; use crate::ai::blocklist::code_block::CodeSnippetButtonHandles; use crate::ai::blocklist::inline_action::inline_action_icons::icon_size; @@ -122,7 +115,6 @@ lazy_static! { const HAS_PENDING_CLI_ACTION_CONTEXT_KEY: &str = "HasPendingCLIAgentAction"; const HAS_PENDING_NON_TRANSFER_CONTROL_ACTION_CONTEXT_KEY: &str = "HasPendingNonTransferControlCLIAgentAction"; -const BLOCKED_ACTION_MESSAGE_FOR_TRANSFER_CONTROL: &str = "Agent is asking you to take control."; pub fn init(app: &mut AppContext) { use warpui::keymap::macros::*; @@ -234,7 +226,7 @@ impl CLISubagentView { ctx: &mut ViewContext, ) -> Self { let allow_button = CompactibleSplitActionButton::new( - "Allow".to_string(), + i18n::t("common.allow"), Some(KeystrokeSource::Fixed(ACCEPT_KEYSTROKE.clone())), ButtonSize::Small, CLISubagentAction::ExecuteBlockedAction, @@ -246,7 +238,7 @@ impl CLISubagentView { ); let reject_button = CompactibleActionButton::new( - "Refine".to_string(), + i18n::t("common.refine"), Some(KeystrokeSource::Fixed(REJECT_KEYSTROKE.clone())), ButtonSize::Small, CLISubagentAction::RejectBlockedAction { @@ -258,7 +250,7 @@ impl CLISubagentView { ); let take_over_button = CompactibleActionButton::new( - "Take over".to_string(), + i18n::t("ai.command.take_over"), Some(KeystrokeSource::Binding( SET_INPUT_MODE_TERMINAL_ACTION_NAME, )), @@ -271,7 +263,7 @@ impl CLISubagentView { ctx, ); let transfer_control_button = CompactibleActionButton::new( - "Take control".to_string(), + i18n::t("ai.command.take_control"), Some(KeystrokeSource::Binding( SET_INPUT_MODE_TERMINAL_ACTION_NAME, )), @@ -293,11 +285,11 @@ impl CLISubagentView { allow_menu.update(ctx, |menu, ctx| { menu.set_items( vec![ - MenuItemFields::new("Accept".to_string()) + MenuItemFields::new(i18n::t("ai.command.accept")) .with_key_shortcut_label(Some(ACCEPT_KEYSTROKE.displayed())) .with_on_select_action(CLISubagentAction::ExecuteBlockedAction) .into_item(), - MenuItemFields::new("Auto-approve".to_string()) + MenuItemFields::new(i18n::t("ai.command.auto_approve")) .with_key_shortcut_label(Some(AUTO_APPROVE_KEYSTROKE.displayed())) .with_on_select_action(CLISubagentAction::ExecuteAndAutoApprove) .into_item(), @@ -1269,8 +1261,7 @@ impl View for CLISubagentView { AIAgentActionType::WriteToLongRunningShellCommand { input, mode, .. } => { Some(render_blocked_action( BlockedActionProps { - header: BLOCKED_ACTION_MESSAGE_FOR_WRITE_TO_LONG_RUNNING_SHELL_COMMAND - .to_string(), + header: i18n::t("ai.blocked.write_to_running_command"), description: Some(render_write_to_pty_input( WriteToPtyInputProps { input: input.clone(), @@ -1304,7 +1295,7 @@ impl View for CLISubagentView { AIAgentActionType::TransferShellCommandControlToUser { ref reason } => { Some(render_blocked_action( BlockedActionProps { - header: BLOCKED_ACTION_MESSAGE_FOR_TRANSFER_CONTROL.to_string(), + header: i18n::t("ai.cli.agent_asking_take_control"), description: Some(render_transfer_control_reason(reason, app)), is_allow_menu_open: false, allow_menu: None, @@ -1424,7 +1415,7 @@ impl TypedActionView for CLISubagentView { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::success(String::from("Copied to clipboard")), + DismissibleToast::success(i18n::t("common.copied_to_clipboard")), window_id, ctx, ); @@ -1520,12 +1511,10 @@ fn should_show_read_files_speedbump(app: &AppContext) -> bool { fn get_action_loading_text(action: AIAgentActionType) -> Option { match action { - AIAgentActionType::SearchCodebase(_) => { - Some(LOAD_OUTPUT_MESSAGE_FOR_SEARCH_CODEBASE.to_string()) - } - AIAgentActionType::ReadFiles(_) => Some(LOAD_OUTPUT_MESSAGE_FOR_READING_FILES.to_string()), - AIAgentActionType::Grep { .. } => Some(LOAD_OUTPUT_MESSAGE_FOR_GREP.to_string()), - AIAgentActionType::FileGlobV2 { .. } => Some(LOAD_OUTPUT_MESSAGE_FOR_FILE_GLOB.to_string()), + AIAgentActionType::SearchCodebase(_) => Some(i18n::t("ai.loading.searching_codebase")), + AIAgentActionType::ReadFiles(_) => Some(i18n::t("ai.loading.reading_files")), + AIAgentActionType::Grep { .. } => Some(i18n::t("ai.loading.grepping")), + AIAgentActionType::FileGlobV2 { .. } => Some(i18n::t("ai.loading.finding_files")), _ => None, } } @@ -1584,9 +1573,9 @@ fn render_web_search(query: Option, app: &AppContext) -> Box AI"), + FormattedTextFragment::hyperlink( + i18n::t("ai.command.manage_agent_permissions"), + "Settings > AI", + ), ])]), font_size, font_family, @@ -1876,16 +1868,12 @@ fn render_transfer_control_reason(reason: &str, app: &AppContext) -> Box Option { match action { AIAgentActionType::WriteToLongRunningShellCommand { .. } => { - Some(BLOCKED_ACTION_MESSAGE_FOR_WRITE_TO_LONG_RUNNING_SHELL_COMMAND.to_string()) - } - AIAgentActionType::ReadFiles(..) => { - Some(BLOCKED_ACTION_MESSAGE_FOR_READING_FILES.to_string()) - } - AIAgentActionType::SearchCodebase(..) => { - Some(BLOCKED_ACTION_MESSAGE_FOR_SEARCHING_CODEBASE.to_string()) + Some(i18n::t("ai.blocked.write_to_running_command")) } + AIAgentActionType::ReadFiles(..) => Some(i18n::t("ai.blocked.reading_files")), + AIAgentActionType::SearchCodebase(..) => Some(i18n::t("ai.blocked.searching_codebase")), AIAgentActionType::Grep { .. } | AIAgentActionType::FileGlobV2 { .. } => { - Some(BLOCKED_ACTION_MESSAGE_FOR_GREP_OR_FILE_GLOB.to_string()) + Some(i18n::t("ai.blocked.grep_or_file_glob")) } _ => None, } diff --git a/app/src/ai/blocklist/block/numbered_button.rs b/app/src/ai/blocklist/block/numbered_button.rs index 12a95cf0e2..9c0333cd4e 100644 --- a/app/src/ai/blocklist/block/numbered_button.rs +++ b/app/src/ai/blocklist/block/numbered_button.rs @@ -45,7 +45,7 @@ pub(super) fn render_recommended_badge(appearance: &Appearance) -> Box Self { let close_button = show_close_button.then(|| { ctx.add_typed_action_view(|_| { - ActionButton::new("Remove queued prompt", NakedTheme) + ActionButton::new(i18n::t("ai.pending_prompt.remove"), NakedTheme) .with_icon(Icon::X) .with_size(ButtonSize::XSmall) .on_click(|ctx| { @@ -62,7 +62,7 @@ impl PendingUserQueryBlock { }); let send_now_button = show_send_now_button.then(|| { ctx.add_typed_action_view(|_| { - ActionButton::new("Send now", NakedTheme) + ActionButton::new(i18n::t("ai.pending_prompt.send_now"), NakedTheme) .with_icon(Icon::Play) .with_size(ButtonSize::XSmall) .on_click(|ctx| { @@ -169,7 +169,7 @@ impl View for PendingUserQueryBlock { .finish(); let queued_badge = Text::new( - "Queued", + i18n::t("ai.pending_prompt.queued"), appearance.ui_font_family(), appearance.monospace_font_size().max(4.) - 2., ) diff --git a/app/src/ai/blocklist/block/status_bar.rs b/app/src/ai/blocklist/block/status_bar.rs index c9f411ed67..acf4260b12 100644 --- a/app/src/ai/blocklist/block/status_bar.rs +++ b/app/src/ai/blocklist/block/status_bar.rs @@ -26,7 +26,7 @@ use super::model::{AIBlockModel, AIBlockModelImpl, AIBlockOutputStatus}; use super::view_impl::common::{ render_switch_control_to_user_button, render_warping_indicator, render_warping_indicator_base, AutoExecuteButtonProps, ButtonProps, ForceRefreshButtonProps, MaybeShimmeringText, - WarpingIndicatorProps, WarpingProps, LOAD_OUTPUT_MESSAGE, WAITING_FOR_USER_INPUT_MESSAGE, + WarpingIndicatorProps, WarpingProps, }; use crate::ai::agent::conversation::AIConversationId; use crate::ai::agent::{ @@ -725,7 +725,7 @@ impl BlocklistAIStatusBar { if let Some(tip) = self.current_tip.as_ref() { send_telemetry_from_app_ctx!( TelemetryEvent::AgentTipShown { - tip: tip.description.clone() + tip: tip.description_key.to_owned() }, ctx ); @@ -807,9 +807,8 @@ impl BlocklistAIStatusBar { app, ); let default_warping_text = fallback_warping_text - .as_deref() - .unwrap_or(LOAD_OUTPUT_MESSAGE) - .to_owned(); + .clone() + .unwrap_or_else(|| i18n::t("ai.loading.warping")); let secondary_element = if fallback_warping_text.is_some() { Some(render_fallback_explanation(model.as_ref(), app)) } else { @@ -927,14 +926,18 @@ impl BlocklistAIStatusBar { if let Some(auth_url) = ambient_agent_model.github_auth_url() { let error_message = ambient_agent_model .github_auth_error_message() - .unwrap_or("Missing GitHub authentication."); + .map(ToOwned::to_owned) + .unwrap_or_else(|| i18n::t("ai.status.missing_github_authentication")); return Some(render_wrapping_standard_message_bar( CoreIcon::Triangle, error_color, error_color, vec![ FormattedTextFragment::plain_text(format!("{error_message} ")), - FormattedTextFragment::hyperlink("Authenticate GitHub", auth_url.to_owned()), + FormattedTextFragment::hyperlink( + i18n::t("ai.status.authenticate_github"), + auth_url.to_owned(), + ), ], app, )); @@ -946,9 +949,9 @@ impl BlocklistAIStatusBar { CoreIcon::StopFilled, color, color, - vec![FormattedTextFragment::plain_text( - "Cloud agent run cancelled", - )], + vec![FormattedTextFragment::plain_text(i18n::t( + "ai.status.cloud_agent_run_cancelled", + ))], app, )); } @@ -999,7 +1002,7 @@ fn render_agent_tip(tip: &AgentTip, app: &AppContext) -> Box { let appearance = Appearance::as_ref(app); let theme = appearance.theme(); - let tip_description = tip.description.clone(); + let tip_description = tip.description_key.to_owned(); let action_text = tip.action.clone().and_then(|action| action.display_text()); let mut fragments = tip.to_formatted_text(app); @@ -1009,7 +1012,10 @@ fn render_agent_tip(tip: &AgentTip, app: &AppContext) -> Box { fragments.push(FormattedTextFragment::hyperlink_action(text, action)); } else if let Some(link_target) = tip.link.clone() { fragments.push(FormattedTextFragment::plain_text(" ")); - fragments.push(FormattedTextFragment::hyperlink("Learn more", link_target)); + fragments.push(FormattedTextFragment::hyperlink( + i18n::t("common.learn_more"), + link_target, + )); } let formatted_text = @@ -1069,9 +1075,9 @@ fn render_fallback_explanation( .map(|info| info.base_model_name.as_str()); let text = match primary_name { Some(primary) => { - format!("The primary model ({primary}) failed. Retrying with the fallback model.") + i18n::t("ai.status.primary_model_failed_with_name").replace("{model}", primary) } - None => "The primary model failed. Retrying with the fallback model.".to_owned(), + None => i18n::t("ai.status.primary_model_failed"), }; let appearance = Appearance::as_ref(app); Text::new_inline( @@ -1124,8 +1130,8 @@ fn resolve_fallback_warping_message( return None; } Some(match display_name.as_deref() { - Some(name) => format!("Warping with {name}."), - None => "Warping with another model.".to_owned(), + Some(name) => i18n::t("ai.status.warping_with").replace("{model}", name), + None => i18n::t("ai.status.warping_with_another_model"), }) } @@ -1163,7 +1169,7 @@ impl View for BlocklistAIStatusBar { WarpingIndicatorProps { icon: None, warping_indicator_text: MaybeShimmeringText::Shimmering { - text: "Setting up environment".into(), + text: i18n::t("ai.status.setting_up_environment").into(), shimmering_text_handle: self.shimmering_text_handle.clone(), }, non_shimmering_text: None, @@ -1190,13 +1196,13 @@ impl View for BlocklistAIStatusBar { WarpingIndicatorProps { icon: Some(icons::gray_clock_icon(appearance).finish()), warping_indicator_text: MaybeShimmeringText::Static( - WAITING_FOR_USER_INPUT_MESSAGE.into(), + i18n::t("ai.common.agent_waiting_for_instructions").into(), ), non_shimmering_text: None, non_shimmering_suffix: None, buttons: Some(render_switch_control_to_user_button( - "Exit", - "Exit agent input", + i18n::t("common.exit"), + i18n::t("ai.common.exit_agent_input_tooltip"), ButtonProps { button_handle: &self.state_handles.take_over_button, keystroke: self.set_terminal_input_keystroke.as_ref(), diff --git a/app/src/ai/blocklist/block/toggleable_items.rs b/app/src/ai/blocklist/block/toggleable_items.rs index 8f60ba1979..af51b0be0e 100644 --- a/app/src/ai/blocklist/block/toggleable_items.rs +++ b/app/src/ai/blocklist/block/toggleable_items.rs @@ -175,7 +175,7 @@ impl View for ToggleableItemsView { .finish(); let hint_text = Span::new( - "to toggle selection", + i18n::t("common.to_toggle_selection"), UiComponentStyles { margin: Some(Coords::default().left(6.)), ..hint_styles diff --git a/app/src/ai/blocklist/block/view_impl.rs b/app/src/ai/blocklist/block/view_impl.rs index 0716883f98..0dec25a2ed 100644 --- a/app/src/ai/blocklist/block/view_impl.rs +++ b/app/src/ai/blocklist/block/view_impl.rs @@ -725,7 +725,7 @@ pub fn render_citation( /// "Manage AI Autonomy permissions" link. Matches the visual rhythm of /// [`render_autonomy_checkbox_setting_speedbump_footer`]. pub fn render_autonomy_dropdown_setting_speedbump_footer( - description: &'static str, + description: String, dropdown: &warpui::ViewHandle>, settings_link_handle: MouseStateHandle, app: &AppContext, @@ -760,7 +760,7 @@ where appearance .ui_builder() .link( - "Manage AI Autonomy permissions".into(), + i18n::t("ai.output.manage_ai_autonomy_permissions").into(), None, Some(Box::new(move |ctx| { ctx.dispatch_typed_action( @@ -787,7 +787,7 @@ where /// This function is needed both above (i.e. `block.rs`) and below (i.e. `output.rs`), and as such /// cannot reside in `output.rs` because we don't want to make `mod output` public. pub fn render_autonomy_checkbox_setting_speedbump_footer( - description: &'static str, + description: String, checked: bool, on_toggled_action: AIBlockAction, checkbox_handle: MouseStateHandle, @@ -833,7 +833,7 @@ pub fn render_autonomy_checkbox_setting_speedbump_footer( appearance .ui_builder() .link( - "Manage AI Autonomy permissions".into(), + i18n::t("ai.output.manage_ai_autonomy_permissions").into(), None, Some(Box::new(move |ctx| { ctx.dispatch_typed_action( diff --git a/app/src/ai/blocklist/block/view_impl/common.rs b/app/src/ai/blocklist/block/view_impl/common.rs index 3ce176541b..07983f31e6 100644 --- a/app/src/ai/blocklist/block/view_impl/common.rs +++ b/app/src/ai/blocklist/block/view_impl/common.rs @@ -102,44 +102,11 @@ use crate::workspaces::workspace::CustomerType; pub const STATUS_ICON_SIZE_DELTA: f32 = 4.; pub const STATUS_FOOTER_VERTICAL_PADDING: f32 = 4.; -pub const WAITING_FOR_USER_INPUT_MESSAGE: &str = "Agent waiting for instructions..."; const IMAGE_SOURCE_LINK_LINE_INDEX: usize = 1; -const ERROR_APOLOGY_TEXT: &str = "I'm sorry, I couldn't complete that request."; -const INTERNAL_WARP_ERROR: &str = "Internal Warp error."; - -pub const LOAD_OUTPUT_MESSAGE: &str = "Warping..."; -pub const LOAD_OUTPUT_MESSAGE_FOR_ADJUSTING: &str = "Adjusting tasks..."; -pub const LOAD_OUTPUT_MESSAGE_FOR_PASSIVE_CODE_GEN: &str = "Generating fix..."; -pub const LOAD_OUTPUT_MESSAGE_FOR_CREATING_DIFF: &str = "Creating diff..."; -pub const LOAD_OUTPUT_MESSAGE_FOR_PREPARING_QUESTION: &str = "Preparing question..."; -pub const LOAD_OUTPUT_MESSAGE_FOR_GENERATING_PLAN: &str = "Generating plan..."; -pub const LOAD_OUTPUT_MESSAGE_FOR_UPDATING_PLAN: &str = "Updating plan..."; -pub const LOAD_OUTPUT_MESSAGE_FOR_SUMMARIZING_CONVERSATION: &str = "Summarizing conversation..."; -pub const LOAD_OUTPUT_MESSAGE_FOR_SUMMARIZING_TOOL_CALL_RESULT: &str = - "Summarizing command output..."; -pub const LOAD_OUTPUT_MESSAGE_FOR_SEARCH_CODEBASE: &str = "Searching codebase..."; -pub const LOAD_OUTPUT_MESSAGE_FOR_READING_FILES: &str = "Reading files..."; -pub const LOAD_OUTPUT_MESSAGE_FOR_GREP: &str = "Grepping..."; -pub const LOAD_OUTPUT_MESSAGE_FOR_FILE_GLOB: &str = "Finding files..."; -pub const LOAD_OUTPUT_MESSAGE_FOR_RUNNING_COMMAND: &str = "Executing command..."; -pub const LOAD_OUTPUT_MESSAGE_FOR_WRITING_TO_COMMAND: &str = "Writing command input..."; -pub const LOAD_OUTPUT_MESSAGE_FOR_WAITING_FOR_COMMAND_COMPLETION: &str = - "Waiting for command to exit..."; -pub const LOAD_OUTPUT_MESSAGE_FOR_WEB_SEARCH: &str = "Searching the web..."; -pub const LOAD_OUTPUT_MESSAGE_FOR_FETCHING_REVIEW_COMMENTS: &str = "Fetching PR comments..."; - #[cfg(feature = "local_fs")] pub(crate) type ResolvedBlocklistImageSources = HashMap>; -pub const BLOCKED_ACTION_MESSAGE_FOR_WRITE_TO_LONG_RUNNING_SHELL_COMMAND: &str = - "Can I write the following to this running command?"; -pub const BLOCKED_ACTION_MESSAGE_FOR_READING_FILES: &str = "Grant access to the following files?"; -pub const BLOCKED_ACTION_MESSAGE_FOR_SEARCHING_CODEBASE: &str = - "Grant access to the following repository?"; -pub const BLOCKED_ACTION_MESSAGE_FOR_GREP_OR_FILE_GLOB: &str = - "OK if I search the files in this directory?"; - const BLOCKLIST_VISUAL_SECTION_HEIGHT_LINE_MULTIPLIER: f32 = 10.0; const INLINE_IMAGE_HEIGHT: f32 = 164.; const INLINE_IMAGE_MAX_WIDTH: f32 = 218.; @@ -290,10 +257,10 @@ pub fn render_warping_indicator( // Choose the appropriate message based on summarization type let base_message = match summarization_type { SummarizationType::ConversationSummary => { - LOAD_OUTPUT_MESSAGE_FOR_SUMMARIZING_CONVERSATION + i18n::t("ai.loading.summarizing_conversation") } SummarizationType::ToolCallResultSummary => { - LOAD_OUTPUT_MESSAGE_FOR_SUMMARIZING_TOOL_CALL_RESULT + i18n::t("ai.loading.summarizing_command_output") } }; @@ -309,53 +276,49 @@ pub fn render_warping_indicator( // Move the timer / token text outside of the base message, we don't want it to shimmer // since that would cause the animation to reset every time the tokens or time changes. non_shimmering_text = Some(timer_text.to_string()); - base_message.into() + base_message } else { - base_message.to_string() + base_message } } else if props.model.contains_update_document_action(app) { - LOAD_OUTPUT_MESSAGE_FOR_UPDATING_PLAN.to_string() + i18n::t("ai.loading.updating_plan") } else if props.model.contains_create_document_action(app) { - LOAD_OUTPUT_MESSAGE_FOR_GENERATING_PLAN.to_string() + i18n::t("ai.loading.generating_plan") } else if props.model.request_type(app).is_passive_code_diff() { - LOAD_OUTPUT_MESSAGE_FOR_PASSIVE_CODE_GEN.to_string() + i18n::t("ai.loading.generating_fix") } else if is_last_message_requesting_file_edits { - LOAD_OUTPUT_MESSAGE_FOR_CREATING_DIFF.to_string() + i18n::t("ai.loading.creating_diff") } else if is_last_message_asking_user_question { - LOAD_OUTPUT_MESSAGE_FOR_PREPARING_QUESTION.to_string() + i18n::t("ai.loading.preparing_question") } else if is_searching_web { - LOAD_OUTPUT_MESSAGE_FOR_WEB_SEARCH.to_string() + i18n::t("ai.loading.searching_web") } else if is_fetching_review_comments { - LOAD_OUTPUT_MESSAGE_FOR_FETCHING_REVIEW_COMMENTS.to_string() + i18n::t("ai.loading.fetching_pr_comments") } else if is_interrupt_query_for_same_conversation && output_to_render .as_ref() .is_none_or(|output| output.get().messages.is_empty()) { // Only "Adjusting..." if nothing from the current exchange has streamed yet. - LOAD_OUTPUT_MESSAGE_FOR_ADJUSTING.to_string() + i18n::t("ai.loading.adjusting_tasks") } else { match props .action_model .get_async_running_action(app) .map(|action| &action.action) { - Some(AIAgentActionType::SearchCodebase(..)) => { - LOAD_OUTPUT_MESSAGE_FOR_SEARCH_CODEBASE.to_owned() - } - Some(AIAgentActionType::Grep { .. }) => LOAD_OUTPUT_MESSAGE_FOR_GREP.to_owned(), + Some(AIAgentActionType::SearchCodebase(..)) => i18n::t("ai.loading.searching_codebase"), + Some(AIAgentActionType::Grep { .. }) => i18n::t("ai.loading.grepping"), Some(AIAgentActionType::CallMCPTool { name, .. }) => { - format!("Calling \"{name}\" MCP tool...") + i18n::t("ai.loading.calling_mcp_tool").replace("{name}", name) } Some(AIAgentActionType::ReadMCPResource { name, .. }) => { - format!("Reading \"{name}\" MCP resource...") + i18n::t("ai.loading.reading_mcp_resource").replace("{name}", name) } Some(AIAgentActionType::FileGlob { .. }) - | Some(AIAgentActionType::FileGlobV2 { .. }) => { - LOAD_OUTPUT_MESSAGE_FOR_FILE_GLOB.to_owned() - } + | Some(AIAgentActionType::FileGlobV2 { .. }) => i18n::t("ai.loading.finding_files"), Some(AIAgentActionType::WriteToLongRunningShellCommand { .. }) => { - LOAD_OUTPUT_MESSAGE_FOR_WRITING_TO_COMMAND.to_owned() + i18n::t("ai.loading.writing_command_input") } action => { let active_block = props.terminal_model.block_list().active_block(); @@ -365,7 +328,7 @@ pub fn render_warping_indicator( { if action.is_none() { should_render_waiting_icon = true; - WAITING_FOR_USER_INPUT_MESSAGE.to_owned() + i18n::t("ai.common.agent_waiting_for_instructions") } else { // Choose the base message depending on whether the agent is waiting // for the command to exit or polling at a fixed interval. @@ -373,8 +336,8 @@ pub fn render_warping_indicator( Some(AIAgentActionType::ReadShellCommandOutput { delay: Some(ShellCommandDelay::OnCompletion), .. - }) => LOAD_OUTPUT_MESSAGE_FOR_WAITING_FOR_COMMAND_COMPLETION, - _ => LOAD_OUTPUT_MESSAGE_FOR_RUNNING_COMMAND, + }) => i18n::t("ai.loading.waiting_for_command_exit"), + _ => i18n::t("ai.loading.executing_command"), }; // Compute "Next check in {time}" for fixed-interval polls. Only // `ReadShellCommandOutput { delay: Duration(_) }` has a meaningful @@ -401,16 +364,19 @@ pub fn render_warping_indicator( } else { format!("{}m", secs / 60) }; - let suffix = format!(" · Next check in {formatted}"); + let suffix = format!( + " · {}", + i18n::t("ai.loading.next_check_in").replace("{time}", &formatted) + ); // Keep the base message constant so the shimmering animation // isn't interrupted every time the countdown ticks. The // suffix is rendered as a separate non-shimmering element, // matching the same pattern used by the summarization timer. non_shimmering_text = Some(suffix); - base.to_owned() + base } else { - base.to_owned() + base } } } else { @@ -437,8 +403,8 @@ pub fn render_warping_indicator( if let Some(take_over_button_props) = props.take_over_lrc_control_button { has_buttons = true; buttons_row.add_child(render_switch_control_to_user_button( - "Take over", - "Take over control of the command", + i18n::t("ai.command.take_over"), + i18n::t("ai.common.take_over_command_tooltip"), take_over_button_props, appearance, )); @@ -665,9 +631,9 @@ pub fn render_warping_indicator_base( pub fn format_elapsed_seconds(elapsed: std::time::Duration) -> String { let total_seconds = elapsed.as_secs(); if total_seconds == 1 { - "1 second".to_string() + i18n::t("ai.common.elapsed_second_one") } else { - format!("{total_seconds} seconds") + i18n::t("ai.common.elapsed_second_other").replace("{seconds}", &total_seconds.to_string()) } } @@ -748,13 +714,13 @@ fn render_hide_responses_button( ) -> Box { let theme = appearance.theme(); let button_text = if should_hide_responses { - "Show responses" + i18n::t("ai.common.show_responses") } else { - "Hide responses" + i18n::t("ai.common.hide_responses") }; let text = Container::new( Text::new( - button_text, + button_text.clone(), appearance.ui_font_family(), get_keybinding_font_size(appearance), ) @@ -764,9 +730,9 @@ fn render_hide_responses_button( .finish(); let tooltip_text = if should_hide_responses { - "Show agent responses" + i18n::t("ai.common.show_agent_responses") } else { - "Hide agent responses" + i18n::t("ai.common.hide_agent_responses") }; render_warping_indicator_button( @@ -774,7 +740,7 @@ fn render_hide_responses_button( appearance, text, props.keystroke, - tooltip_text.to_string(), + tooltip_text, props.is_active, false, |ctx| { @@ -784,8 +750,8 @@ fn render_hide_responses_button( } pub fn render_switch_control_to_user_button( - text: &'static str, - tooltip: &'static str, + text: String, + tooltip: String, props: ButtonProps, appearance: &Appearance, ) -> Box { @@ -806,7 +772,7 @@ pub fn render_switch_control_to_user_button( appearance, text, props.keystroke, - tooltip.to_string(), + tooltip, props.is_active, false, |ctx| { @@ -830,7 +796,7 @@ fn render_stop_button(props: ButtonProps, appearance: &Appearance) -> Box| { @@ -858,9 +824,9 @@ fn render_queue_next_prompt_button( .finish(); let tooltip_text = if props.is_active { - "Auto-queue is on: your next prompt will be queued" + i18n::t("ai.common.auto_queue_on_tooltip") } else { - "Auto-queue next prompt while agent is responding" + i18n::t("ai.common.auto_queue_next_prompt_tooltip") }; render_warping_indicator_button( @@ -868,7 +834,7 @@ fn render_queue_next_prompt_button( appearance, icon, props.keystroke, - tooltip_text.to_string(), + tooltip_text, props.is_active, false, |ctx| { @@ -902,11 +868,11 @@ fn render_auto_approve_button( .finish(); let tooltip_text = if props.is_locked { - "Fast forward is always enabled for cloud agent conversations" + i18n::t("ai.common.fast_forward_cloud_always_enabled") } else if is_active { - "Turn off auto-approve all agent actions" + i18n::t("ai.common.turn_off_auto_approve_actions") } else { - "Auto-approve all agent actions for this task" + i18n::t("ai.common.auto_approve_actions_for_task") }; render_warping_indicator_button( @@ -960,7 +926,7 @@ fn render_force_refresh_inline( // Mirror `render_output_status_text` exactly: same `Text` configuration plus // the `Container::with_margin_top(1.)` wrapper so this sits on the same // baseline as the adjacent `Last seen by agent ...` text. - let text = Text::new(" · Check now".to_string(), font_family, font_size) + let text = Text::new(i18n::t("ai.output.check_now"), font_family, font_size) .with_color(color) .with_style(Properties::default()) .with_clip(ClipConfig::end()) @@ -974,7 +940,7 @@ fn render_force_refresh_inline( let mut stack = Stack::new().with_child(text_with_margin); if state.is_hovered() { let tool_tip = ui_builder - .tool_tip("Ask the agent to check this command now, skipping its timer.".to_owned()) + .tool_tip(i18n::t("ai.output.check_now_tooltip")) .build() .finish(); stack.add_positioned_overlay_child( @@ -2148,7 +2114,7 @@ fn render_mermaid_diagram_section( .finish(); render_visual_card( - "Mermaid diagram".to_string(), + i18n::t("ai.common.mermaid_diagram"), Icon::Dataflow, Container::new(mermaid_canvas) .with_background(theme.background()) @@ -2985,13 +2951,14 @@ pub struct FailedOutputProps<'a> { pub fn render_failed_output(props: FailedOutputProps, app: &AppContext) -> Box { let appearance = Appearance::as_ref(app); + let error_apology = i18n::t("ai.common.error_apology"); let error_text = match props.error { RenderableAIError::QuotaLimit { user_display_message, } => { if let Some(message) = user_display_message { - format!("{ERROR_APOLOGY_TEXT}\n\n{message}") + format!("{error_apology}\n\n{message}") } else { let ai_request_usage_model = AIRequestUsageModel::as_ref(app); let formatted_next_refresh_time = ai_request_usage_model @@ -3000,15 +2967,20 @@ pub fn render_failed_output(props: FailedOutputProps, app: &AppContext) -> Box { - "Warp is currently overloaded. Please try again later.".to_string() - } + RenderableAIError::ServerOverloaded => i18n::t("ai.common.server_overloaded"), RenderableAIError::InternalWarpError => { - format!("{ERROR_APOLOGY_TEXT}\n\n{INTERNAL_WARP_ERROR}") + format!( + "{}\n\n{}", + error_apology, + i18n::t("ai.common.internal_warp_error") + ) } RenderableAIError::Other { error_message, @@ -3018,13 +2990,19 @@ pub fn render_failed_output(props: FailedOutputProps, app: &AppContext) -> Box Box( warpui::ui_components::button::ButtonVariant::Text, props.submit_issue_button_handle, ) - .with_centered_text_label("Send Feedback".to_string()) + .with_centered_text_label(i18n::t("common.send_feedback")) .with_style(submit_button_style) .with_hovered_styles(submit_button_hover_style) .with_clicked_styles(submit_button_hover_style) @@ -3311,7 +3291,7 @@ pub(crate) fn render_debug_footer( // render the conversation's debug id so screenshots automatically show the debug id let debug_text = Text::new( - format!("Debug information: {debug_info}"), + i18n::t("ai.common.debug_information").replace("{debug_info}", &debug_info), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -3355,7 +3335,7 @@ pub(crate) fn render_debug_footer( }) .finish(); let copy_button_with_tooltip = appearance.ui_builder().tool_tip_on_element( - "Copy debug ID".to_string(), + i18n::t("ai.common.copy_debug_id"), props.debug_copy_button_handle, copy_button, warpui::elements::ParentAnchor::TopRight, diff --git a/app/src/ai/blocklist/block/view_impl/orchestration.rs b/app/src/ai/blocklist/block/view_impl/orchestration.rs index e1deeaa294..539a6b13d4 100644 --- a/app/src/ai/blocklist/block/view_impl/orchestration.rs +++ b/app/src/ai/blocklist/block/view_impl/orchestration.rs @@ -42,7 +42,6 @@ use crate::terminal::view::TerminalAction; use crate::ui_components::blended_colors; use crate::ui_components::icons::Icon; -const GENERATING_TITLE_PLACEHOLDER: &str = "Generating title..."; const ORCHESTRATION_COLLAPSED_MAX_HEIGHT: f32 = 200.; #[derive(Clone, Debug, PartialEq, Eq)] struct OrchestrationParticipant { @@ -56,16 +55,17 @@ struct OrchestrationParticipant { impl OrchestrationParticipant { fn orchestrator() -> Self { Self { - display_name: "Orchestrator".to_string(), + display_name: i18n::t("ai.orchestration.orchestrator"), avatar: OrchestrationAvatar::Orchestrator, conversation_id: None, } } fn unknown_child() -> Self { + let display_name = i18n::t("ai.orchestration.unknown_agent"); Self { - display_name: "Unknown agent".to_string(), - avatar: OrchestrationAvatar::agent("Unknown agent".to_string()), + display_name: display_name.clone(), + avatar: OrchestrationAvatar::agent(display_name), conversation_id: None, } } @@ -458,7 +458,9 @@ pub(super) fn render_send_message( ); } SendMessageToAgentResult::Error(error) => { - let label = format!("Failed to send message to {recipients}: {error}"); + let label = i18n::t("ai.orchestration.send_message_failed") + .replace("{recipients}", &recipients) + .replace("{error}", error); let status_icon = inline_action_icons::red_x_icon(appearance).finish(); return render_requested_action_row_for_text( label.into(), @@ -475,7 +477,8 @@ pub(super) fn render_send_message( .finish(); } SendMessageToAgentResult::Cancelled => { - let label = format!("Send message to {recipients} cancelled."); + let label = i18n::t("ai.orchestration.send_message_cancelled") + .replace("{recipients}", &recipients); let status_icon = inline_action_icons::cancelled_icon(appearance).finish(); return render_requested_action_row_for_text( label.into(), @@ -501,7 +504,7 @@ pub(super) fn render_send_message( || status.as_ref().is_some_and(|s| s.is_queued()); let label_fragments = vec![ - FormattedTextFragment::plain_text("Sending message to "), + FormattedTextFragment::plain_text(i18n::t("ai.orchestration.sending_message_to")), FormattedTextFragment::bold(&recipients), FormattedTextFragment::plain_text(format!(": {subject}")), ]; @@ -578,7 +581,7 @@ pub(super) fn render_start_agent( let (label_fragments, status_icon) = match result { StartAgentResult::Success { .. } => ( vec![ - FormattedTextFragment::plain_text("Started agent "), + FormattedTextFragment::plain_text(i18n::t("ai.orchestration.started_agent")), FormattedTextFragment::bold(name), FormattedTextFragment::plain_text(start_agent_success_suffix(execution_mode)), ], @@ -596,7 +599,9 @@ pub(super) fn render_start_agent( vec![ FormattedTextFragment::plain_text(start_agent_cancelled_prefix(execution_mode)), FormattedTextFragment::bold(name), - FormattedTextFragment::plain_text(" cancelled."), + FormattedTextFragment::plain_text(i18n::t( + "ai.orchestration.agent_cancelled_suffix", + )), ], inline_action_icons::cancelled_icon(appearance).finish(), ), @@ -725,31 +730,35 @@ pub(super) fn render_start_agent( .finish() } -fn start_agent_success_suffix(execution_mode: &StartAgentExecutionMode) -> &'static str { +fn start_agent_success_suffix(execution_mode: &StartAgentExecutionMode) -> String { match execution_mode { - StartAgentExecutionMode::Local { .. } => " locally.", - StartAgentExecutionMode::Remote { .. } => " remotely.", + StartAgentExecutionMode::Local { .. } => i18n::t("ai.orchestration.started_locally_suffix"), + StartAgentExecutionMode::Remote { .. } => { + i18n::t("ai.orchestration.started_remotely_suffix") + } } } -fn start_agent_error_prefix(execution_mode: &StartAgentExecutionMode) -> &'static str { +fn start_agent_error_prefix(execution_mode: &StartAgentExecutionMode) -> String { match execution_mode { - StartAgentExecutionMode::Local { .. } => "Failed to start agent ", - StartAgentExecutionMode::Remote { .. } => "Failed to start remote agent ", + StartAgentExecutionMode::Local { .. } => i18n::t("ai.orchestration.failed_to_start_agent"), + StartAgentExecutionMode::Remote { .. } => { + i18n::t("ai.orchestration.failed_to_start_remote_agent") + } } } -fn start_agent_cancelled_prefix(execution_mode: &StartAgentExecutionMode) -> &'static str { +fn start_agent_cancelled_prefix(execution_mode: &StartAgentExecutionMode) -> String { match execution_mode { - StartAgentExecutionMode::Local { .. } => "Start agent ", - StartAgentExecutionMode::Remote { .. } => "Start remote agent ", + StartAgentExecutionMode::Local { .. } => i18n::t("ai.orchestration.start_agent"), + StartAgentExecutionMode::Remote { .. } => i18n::t("ai.orchestration.start_remote_agent"), } } -fn start_agent_in_progress_prefix(execution_mode: &StartAgentExecutionMode) -> &'static str { +fn start_agent_in_progress_prefix(execution_mode: &StartAgentExecutionMode) -> String { match execution_mode { - StartAgentExecutionMode::Local { .. } => "Starting agent ", - StartAgentExecutionMode::Remote { .. } => "Starting remote agent ", + StartAgentExecutionMode::Local { .. } => i18n::t("ai.orchestration.starting_agent"), + StartAgentExecutionMode::Remote { .. } => i18n::t("ai.orchestration.starting_remote_agent"), } } @@ -826,7 +835,7 @@ fn available_conversation_title_for_id( Some(title) if conversation.initial_query().as_deref() != Some(title.as_str()) => { Some(title) } - _ => Some(GENERATING_TITLE_PLACEHOLDER.to_string()), + _ => Some(i18n::t("ai.orchestration.generating_title")), } } diff --git a/app/src/ai/blocklist/block/view_impl/output.rs b/app/src/ai/blocklist/block/view_impl/output.rs index 59c5e88876..81497f6f15 100644 --- a/app/src/ai/blocklist/block/view_impl/output.rs +++ b/app/src/ai/blocklist/block/view_impl/output.rs @@ -67,10 +67,7 @@ use crate::ai::agent_conversations_model::AgentConversationsModel; use crate::ai::ambient_agents::AmbientAgentTaskId; use crate::ai::blocklist::action_model::AIActionStatus; use crate::ai::blocklist::block::model::{AIBlockModel, AIBlockModelHelper, AIBlockOutputStatus}; -use crate::ai::blocklist::block::view_impl::common::{ - MaybeShimmeringText, BLOCKED_ACTION_MESSAGE_FOR_GREP_OR_FILE_GLOB, - BLOCKED_ACTION_MESSAGE_FOR_READING_FILES, BLOCKED_ACTION_MESSAGE_FOR_SEARCHING_CODEBASE, -}; +use crate::ai::blocklist::block::view_impl::common::MaybeShimmeringText; use crate::ai::blocklist::block::{ AIBlock, AIBlockAction, AIBlockStateHandles, ActionButtons, AutonomySettingSpeedbump, CollapsibleElementState, CollapsibleExpansionState, EmbeddedCodeEditorView, FinishReason, @@ -123,8 +120,6 @@ use crate::view_components::compactible_action_button::{ use crate::workspace::WorkspaceAction; use crate::{AIAgentTodoList, FeatureFlag}; -const BLOCKED_ACTION_MESSAGE_FOR_UPLOADING_ARTIFACT: &str = "Grant access to upload this artifact?"; - /// Data required to render the AI block output component. #[derive(Copy, Clone)] pub(crate) struct Props<'a> { @@ -352,9 +347,10 @@ pub(super) fn render(props: Props, app: &AppContext) -> Box { && props.thinking_display_mode.should_render() => { let header_text = if let Some(dur) = finished_duration { - format!("Thought for {}", format_elapsed_seconds(*dur)) + i18n::t("ai.output.thought_for") + .replace("{duration}", &format_elapsed_seconds(*dur)) } else { - "Thinking".to_string() + i18n::t("ai.output.thinking") }; if let Some(element) = render_collapsible_block( output_message, @@ -452,7 +448,7 @@ pub(super) fn render(props: Props, app: &AppContext) -> Box { // action so the user sees the error instead // of an empty box. let formatted_text = render_requested_action_body_text( - "Failed to read files".into(), + i18n::t("ai.output.failed_to_read_files").into(), appearance.ui_font_family(), app, ); @@ -855,7 +851,7 @@ pub(super) fn render(props: Props, app: &AppContext) -> Box { SummarizationType::ConversationSummary ) && !are_all_text_sections_empty(&text.sections) => { - let header_text = "Conversation summarized".to_string(); + let header_text = i18n::t("ai.output.conversation_summarized"); if let Some(element) = render_collapsible_block( output_message, header_text, @@ -1021,9 +1017,9 @@ pub(super) fn render(props: Props, app: &AppContext) -> Box { )); } None => { - fragments.push(FormattedTextFragment::plain_text( - "this conversation", - )); + fragments.push(FormattedTextFragment::plain_text(i18n::t( + "ai.output.this_conversation", + ))); } }; match query { @@ -1119,8 +1115,7 @@ pub(super) fn render(props: Props, app: &AppContext) -> Box { output_items.add_child( render_informational_footer( app, - "Sorry you had a bad experience with this interaction. We've refunded you 1 credit. We appreciate your feedback!" - .to_string(), + i18n::t("ai.output.refunded_one_credit"), ) .with_agent_output_item_spacing(app) .finish(), @@ -1130,9 +1125,8 @@ pub(super) fn render(props: Props, app: &AppContext) -> Box { output_items.add_child( render_informational_footer( app, - format!( - "Sorry you had a bad experience with this interaction. We've refunded you {request_refunded_count} credits. We appreciate your feedback!" - ), + i18n::t("ai.output.refunded_credits") + .replace("{count}", &request_refunded_count.to_string()), ) .with_agent_output_item_spacing(app) .finish(), @@ -1172,7 +1166,7 @@ pub(super) fn render(props: Props, app: &AppContext) -> Box { output_items.add_child( render_informational_footer( app, - "This response won't count towards your usage.".to_string(), + i18n::t("ai.output.response_wont_count_usage"), ) .with_agent_output_item_spacing(app) .finish(), @@ -1325,10 +1319,12 @@ fn render_search_codebase( .codebase_search_speedbump_option_handles .clone(), vec![ - RadioButtonItem::text( - "Always allow file access for coding tasks", - ), - RadioButtonItem::text("Always allow file access for this repo"), + RadioButtonItem::text(i18n::t( + "ai.output.always_allow_file_access_for_coding_tasks", + )), + RadioButtonItem::text(i18n::t( + "ai.output.always_allow_file_access_for_this_repo", + )), ], props .state_handles @@ -1370,7 +1366,7 @@ fn render_search_codebase( appearance .ui_builder() .link( - "Manage AI Autonomy permissions".into(), + i18n::t("ai.output.manage_ai_autonomy_permissions").into(), None, Some(Box::new(move |ctx| { ctx.dispatch_typed_action( @@ -1415,17 +1411,11 @@ fn render_search_codebase( } _ => { let root_repo_path = root_repo_path?; - renderable_action( - props, - id, - format!("Search in {}", root_repo_path.to_string_lossy()).as_str(), - app, - footer, - appearance, - Some(status), - ) - .render(app) - .finish() + let label = i18n::t("ai.output.search_in_path") + .replace("{path}", &root_repo_path.to_string_lossy()); + renderable_action(props, id, &label, app, footer, appearance, Some(status)) + .render(app) + .finish() } } } @@ -1448,7 +1438,7 @@ fn render_search_codebase( ) .with_header(blocked_action_header( id.clone(), - BLOCKED_ACTION_MESSAGE_FOR_SEARCHING_CODEBASE, + &i18n::t("ai.blocked.searching_codebase"), buttons.run_button.clone(), buttons.cancel_button.clone(), props.action_model, @@ -1465,17 +1455,11 @@ fn render_search_codebase( } _ => { let root_repo_path = root_repo_path?; - renderable_action( - props, - id, - format!("Searching in {}", root_repo_path.to_string_lossy()).as_str(), - app, - footer, - appearance, - Some(status), - ) - .render(app) - .finish() + let label = i18n::t("ai.output.searching_in_path") + .replace("{path}", &root_repo_path.to_string_lossy()); + renderable_action(props, id, &label, app, footer, appearance, Some(status)) + .render(app) + .finish() } }, AIActionStatus::Finished(result) => match props.search_codebase_view.get(id) { @@ -1494,7 +1478,7 @@ fn render_search_codebase( renderable_action( props, id, - "No relevant files found.", + &i18n::t("ai.output.no_relevant_files_found"), app, footer, appearance, @@ -1529,13 +1513,12 @@ fn render_search_codebase( SearchCodebaseResult::Failed { reason, .. } => { let root_repo_path = root_repo_path?; let message = match reason { - SearchCodebaseFailureReason::CodebaseNotIndexed => format!( - "Search in {} failed because the codebase isn't indexed", - root_repo_path.to_string_lossy(), - ), - _ => { - format!("Search in {} failed", root_repo_path.to_string_lossy()) + SearchCodebaseFailureReason::CodebaseNotIndexed => { + i18n::t("ai.output.search_in_path_failed_not_indexed") + .replace("{path}", &root_repo_path.to_string_lossy()) } + _ => i18n::t("ai.output.search_in_path_failed") + .replace("{path}", &root_repo_path.to_string_lossy()), }; renderable_action( props, @@ -1551,11 +1534,12 @@ fn render_search_codebase( } SearchCodebaseResult::Cancelled => { let root_repo_path = root_repo_path?; + let message = i18n::t("ai.output.search_in_path_cancelled") + .replace("{path}", &root_repo_path.to_string_lossy()); renderable_action( props, id, - format!("Search in {} cancelled", root_repo_path.to_string_lossy()) - .as_str(), + &message, app, footer, appearance, @@ -1570,17 +1554,11 @@ fn render_search_codebase( }, None => { let root_repo_path = root_repo_path?; - renderable_action( - props, - id, - format!("Search in {}", root_repo_path.to_string_lossy()).as_str(), - app, - footer, - appearance, - None, - ) - .render(app) - .finish() + let label = i18n::t("ai.output.search_in_path") + .replace("{path}", &root_repo_path.to_string_lossy()); + renderable_action(props, id, &label, app, footer, appearance, None) + .render(app) + .finish() } }; Some(requested_action) @@ -1762,7 +1740,7 @@ fn render_read_skill( let skill_icon_override = icon_override_for_skill_name(&skill.name); let open_button = render_skill_button( - "Open skill", + &i18n::t("ai.output.open_skill"), props.state_handles.open_skill_button_handle.clone(), appearance, skill.provider, @@ -1805,7 +1783,7 @@ fn render_read_files( renderable_action = renderable_action .with_header(blocked_action_header( id.clone(), - BLOCKED_ACTION_MESSAGE_FOR_READING_FILES, + &i18n::t("ai.blocked.reading_files"), buttons.run_button.clone(), buttons.cancel_button.clone(), props.action_model, @@ -1837,7 +1815,7 @@ fn render_read_files( *shown.lock() = true; renderable_action = renderable_action.with_footer(render_autonomy_checkbox_setting_speedbump_footer( - "Always allow file access for coding tasks", + i18n::t("ai.output.always_allow_file_access_for_coding_tasks"), *checked, AIBlockAction::ToggleAutoreadFilesSpeedbumpCheckbox, props @@ -1994,20 +1972,20 @@ fn render_stopped_output(props: Props, app: &AppContext) -> Box { .get_item_index(&item.id) .map(|index| (item, index)) }) { - return Some(format!( - "Stopped task {}/{}: \"{}\"", - item_index + 1, - todo_list.len(), - item.title - )); + return Some( + i18n::t("ai.output.stopped_task_indexed") + .replace("{current}", &(item_index + 1).to_string()) + .replace("{total}", &todo_list.len().to_string()) + .replace("{title}", &item.title), + ); } } - conversation - .initial_query() - .map(|task_name| format!("Stopped task: \"{task_name}\"")) + conversation.initial_query().map(|task_name| { + i18n::t("ai.output.stopped_task_named").replace("{title}", &task_name) + }) }) - .unwrap_or_else(|| "Stopped task".to_string()); + .unwrap_or_else(|| i18n::t("ai.output.stopped_task")); let stop_icon = Container::new( ConstrainedBox::new(gray_stop_icon(appearance).finish()) @@ -2102,7 +2080,7 @@ fn render_stopped_output(props: Props, app: &AppContext) -> Box { .with_custom_label(button_content) .with_tooltip(move || { ui_builder - .tool_tip("Resume conversation".to_string()) + .tool_tip(i18n::t("ai.output.resume_conversation")) .build() .finish() }) @@ -2151,11 +2129,12 @@ fn render_requested_edits_output_message( .is_some_and(|status| status.is_failed()) && !is_passive_code_gen_block { + let fallback_title = i18n::t("ai.output.could_not_apply_changes_to_file"); let title = requested_edit .view .as_ref(app) .title() - .unwrap_or("Could not apply changes to file."); + .unwrap_or(&fallback_title); RenderableAction::new(title, app) .with_icon(inline_action_icons::cancelled_icon(appearance).finish()) .render(app) @@ -2164,7 +2143,7 @@ fn render_requested_edits_output_message( match requested_edit.view.as_ref(app).display_mode() { DisplayMode::FullPane => Align::new( Text::new_inline( - "This suggestion is being edited in another tab.", + i18n::t("ai.output.suggestion_edited_in_another_tab"), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -2274,11 +2253,11 @@ fn render_suggest_new_conversation( }; let (label, status_icon) = match result { SuggestNewConversationResult::Accepted { .. } => ( - "New conversation started", + i18n::t("ai.output.new_conversation_started"), inline_action_icons::green_check_icon(appearance).finish(), ), SuggestNewConversationResult::Rejected => ( - "Continuing current conversation", + i18n::t("ai.output.continuing_current_conversation"), warpui::elements::Icon::new( Icon::FlipForward.into(), internal_colors::neutral_6(theme), @@ -2286,7 +2265,7 @@ fn render_suggest_new_conversation( .finish(), ), SuggestNewConversationResult::Cancelled => ( - "New conversation suggestion cancelled", + i18n::t("ai.output.new_conversation_suggestion_cancelled"), inline_action_icons::cancelled_icon(appearance).finish(), ), }; @@ -2308,7 +2287,7 @@ fn render_suggest_new_conversation( } if props.shared_session_status.is_viewer() { - let header_element = HeaderConfig::new("Start a new conversation", app) + let header_element = HeaderConfig::new(i18n::t("ai.output.start_new_conversation"), app) .with_icon(gray_stop_icon(appearance)) .render(app); @@ -2323,8 +2302,7 @@ fn render_suggest_new_conversation( let mut content = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch); - let new_conversation_header_text = - "It seems like the topic changed. Would you like to make a new conversation?"; + let new_conversation_header_text = i18n::t("ai.output.new_conversation_prompt"); let new_conversation_header_element = HeaderConfig::new(new_conversation_header_text, app) .with_icon(yellow_stop_icon(appearance)) .with_corner_radius_override(CornerRadius::with_top(Radius::Pixels(8.))) @@ -2370,8 +2348,9 @@ fn create_formatted_text_for_grep( .as_ref() .is_some_and(|status| status.is_queued()); + let current_directory = i18n::t("ai.output.current_directory"); let display_path = if path == "." { - "the current directory" + current_directory.as_str() } else { path }; @@ -2382,19 +2361,23 @@ fn create_formatted_text_for_grep( .expect("Queries slice should have an element"); let mut fragments = if is_cancelled || is_queued { vec![ - FormattedTextFragment::plain_text("Grep for "), + FormattedTextFragment::plain_text(i18n::t("ai.output.grep_for")), FormattedTextFragment::inline_code(query), ] } else { vec![ - FormattedTextFragment::plain_text("Grepping for "), + FormattedTextFragment::plain_text(i18n::t("ai.output.grepping_for")), FormattedTextFragment::inline_code(query), ] }; fragments.push(if is_cancelled { - FormattedTextFragment::plain_text(format!(" in {display_path} cancelled")) + FormattedTextFragment::plain_text( + i18n::t("ai.output.in_path_cancelled").replace("{path}", display_path), + ) } else { - FormattedTextFragment::plain_text(format!(" in {display_path}")) + FormattedTextFragment::plain_text( + i18n::t("ai.output.in_path").replace("{path}", display_path), + ) }); FormattedText::new([FormattedTextLine::Line(fragments)]) } else { @@ -2403,17 +2386,21 @@ fn create_formatted_text_for_grep( if is_cancelled { lines.push(FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text(format!( - "Cancelled grep for the following patterns in {display_path}" + "{}", + i18n::t("ai.output.cancelled_grep_patterns_in_path") + .replace("{path}", display_path) )), ])); } else { lines.push(FormattedTextLine::Line(vec![if is_queued { FormattedTextFragment::plain_text(format!( - "Grep for the following patterns in {display_path}" + "{}", + i18n::t("ai.output.grep_patterns_in_path").replace("{path}", display_path) )) } else { FormattedTextFragment::plain_text(format!( - "Grepping for the following patterns in {display_path}" + "{}", + i18n::t("ai.output.grepping_patterns_in_path").replace("{path}", display_path) )) }])); } @@ -2470,7 +2457,8 @@ fn create_formatted_text_for_file_glob( .as_ref() .is_some_and(|status| status.is_queued()); - let path = path.unwrap_or("the current directory"); + let current_directory = i18n::t("ai.output.current_directory"); + let path = path.unwrap_or(¤t_directory); let formatted_text = if patterns.len() == 1 { let pattern = patterns @@ -2479,19 +2467,21 @@ fn create_formatted_text_for_file_glob( let mut fragments = if is_cancelled || is_queued { vec![ - FormattedTextFragment::plain_text("Search for files that match "), + FormattedTextFragment::plain_text(i18n::t("ai.output.search_for_files_matching")), FormattedTextFragment::inline_code(pattern), ] } else { vec![ - FormattedTextFragment::plain_text("Finding files that match "), + FormattedTextFragment::plain_text(i18n::t("ai.output.finding_files_matching")), FormattedTextFragment::inline_code(pattern), ] }; fragments.push(if is_cancelled { - FormattedTextFragment::plain_text(format!(" in {path} cancelled")) + FormattedTextFragment::plain_text( + i18n::t("ai.output.in_path_cancelled").replace("{path}", path), + ) } else { - FormattedTextFragment::plain_text(format!(" in {path}")) + FormattedTextFragment::plain_text(i18n::t("ai.output.in_path").replace("{path}", path)) }); FormattedText::new([FormattedTextLine::Line(fragments)]) } else { @@ -2500,17 +2490,21 @@ fn create_formatted_text_for_file_glob( if is_cancelled { lines.push(FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text(format!( - "Cancelled search for files that match the following patterns in {path}" + "{}", + i18n::t("ai.output.cancelled_file_search_patterns_in_path") + .replace("{path}", path) )), ])); } else { lines.push(FormattedTextLine::Line(vec![if is_queued { FormattedTextFragment::plain_text(format!( - "Find files that match the following patterns in {path}" + "{}", + i18n::t("ai.output.find_file_patterns_in_path").replace("{path}", path) )) } else { FormattedTextFragment::plain_text(format!( - "Finding files that match the following patterns in {path}" + "{}", + i18n::t("ai.output.finding_file_patterns_in_path").replace("{path}", path) )) }])); } @@ -2568,7 +2562,7 @@ fn render_file_retrieval_tool( config = config .with_header(blocked_action_header( action_id.clone(), - BLOCKED_ACTION_MESSAGE_FOR_GREP_OR_FILE_GLOB, + &i18n::t("ai.blocked.grep_or_file_glob"), buttons.run_button.clone(), buttons.cancel_button.clone(), props.action_model, @@ -2599,7 +2593,7 @@ fn render_file_retrieval_tool( } if show_for_action_id == action_id => { *shown.lock() = true; config = config.with_footer(render_autonomy_checkbox_setting_speedbump_footer( - "Always allow file access for coding tasks", + i18n::t("ai.output.always_allow_file_access_for_coding_tasks"), *checked, AIBlockAction::ToggleAutoreadFilesSpeedbumpCheckbox, props @@ -2639,7 +2633,7 @@ fn render_comment_addressed_header(comment: &ReviewComment, app: &AppContext) -> Shrinkable::new( 1., Text::new_inline( - format!("Comment addressed: \"{content}\""), + i18n::t("ai.output.comment_addressed").replace("{content}", &content), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -2685,7 +2679,7 @@ fn render_read_mcp_resource( renderable_action = renderable_action .with_header(blocked_action_header( action_id.clone(), - "OK if I read this MCP resource?", + &i18n::t("ai.output.ok_read_mcp_resource"), buttons.run_button.clone(), buttons.cancel_button.clone(), props.action_model, @@ -2715,10 +2709,11 @@ fn format_upload_artifact_text( request: &UploadArtifactRequest, result: Option<&UploadArtifactResult>, ) -> String { - let mut lines = vec![format!("Upload artifact: {}", request.file_path)]; + let mut lines = + vec![i18n::t("ai.output.upload_artifact").replace("{file_path}", &request.file_path)]; if let Some(description) = request.description.as_deref() { - lines.push(format!("Description: {description}")); + lines.push(i18n::t("ai.output.artifact_description").replace("{description}", description)); } match result { @@ -2727,13 +2722,19 @@ fn format_upload_artifact_text( filepath, .. }) => { - lines.push(format!("Status: uploaded artifact {artifact_uid}")); + lines.push( + i18n::t("ai.output.artifact_status_uploaded") + .replace("{artifact_uid}", artifact_uid), + ); if let Some(filepath) = filepath.as_deref() { - lines.push(format!("Uploaded file: {filepath}")); + lines.push( + i18n::t("ai.output.artifact_uploaded_file").replace("{filepath}", filepath), + ); } } Some(UploadArtifactResult::Error(error)) => { - lines.push(format!("Status: upload failed: {error}")); + lines + .push(i18n::t("ai.output.artifact_status_upload_failed").replace("{error}", error)); } Some(UploadArtifactResult::Cancelled) => {} None => {} @@ -2771,7 +2772,7 @@ fn render_upload_artifact( renderable_action = renderable_action .with_header(blocked_action_header( action_id.clone(), - BLOCKED_ACTION_MESSAGE_FOR_UPLOADING_ARTIFACT, + &i18n::t("ai.output.grant_access_upload_artifact"), buttons.run_button.clone(), buttons.cancel_button.clone(), props.action_model, @@ -2828,7 +2829,7 @@ fn render_use_computer( btn.render( appearance, button::Params { - content: button::Content::Label("View screenshot".into()), + content: button::Content::Label(i18n::t("ai.block.view_screenshot").into()), theme: &button::themes::Secondary, options: button::Options { size: button::Size::Small, @@ -2871,7 +2872,7 @@ fn render_request_computer_use( renderable_action = renderable_action .with_header(blocked_action_header( action_id.clone(), - "OK if I use computer control for this task?", + &i18n::t("ai.output.ok_use_computer_control"), buttons.run_button.clone(), buttons.cancel_button.clone(), props.action_model, @@ -2917,7 +2918,7 @@ fn render_references_footer( )?; let title = Text::new_inline( - "References", + i18n::t("ai.output.references"), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -3002,7 +3003,7 @@ fn render_suggested_rules_and_prompts_footer( let theme = appearance.theme(); let title_row_color = theme.sub_text_color(theme.background()); let title_text = Text::new_inline( - "Suggestions:", + i18n::t("ai.output.suggestions"), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -3107,7 +3108,7 @@ fn render_response_footer(props: Props, app: &AppContext) -> Option Option Option Option Box { // Show tooltip on hover or while clicked let mut stack = Stack::new().with_child(content.finish()); let tooltip = ui_builder - .tool_tip("Show credit usage details".to_string()) + .tool_tip(i18n::t("ai.output.show_credit_usage_details")) .build() .finish(); stack.add_positioned_overlay_child( @@ -3734,7 +3735,7 @@ fn render_collapsible_debug_output( // "Debug output" label row.add_child( Text::new( - "Debug output".to_string(), + i18n::t("ai.output.debug_output"), appearance.ai_font_family(), appearance.monospace_font_size(), ) @@ -3877,16 +3878,20 @@ fn conversation_search_phase(task: &crate::ai::agent::task::Task) -> Conversatio fn format_conversation_search_phase(phase: &ConversationSearchPhase) -> String { match phase { - ConversationSearchPhase::ListingMessages => "Listing messages".to_string(), + ConversationSearchPhase::ListingMessages => { + i18n::t("ai.output.conversation_search_listing_messages") + } ConversationSearchPhase::Grepping { patterns } => { if patterns.is_empty() { - return "Grepping for patterns".to_string(); + return i18n::t("ai.output.conversation_search_grepping_patterns"); } let joined = truncate_from_end(&patterns.join(", "), 60); - format!("Grepping for patterns: {joined}") + i18n::t("ai.output.conversation_search_grepping_patterns_with_patterns") + .replace("{patterns}", &joined) } ConversationSearchPhase::ReadingMessages { count } => { - format!("Reading {count} messages") + i18n::t("ai.output.conversation_search_reading_messages") + .replace("{count}", &count.to_string()) } } } diff --git a/app/src/ai/blocklist/block/view_impl/todos.rs b/app/src/ai/blocklist/block/view_impl/todos.rs index fc5fee017e..d5d592bb15 100644 --- a/app/src/ai/blocklist/block/view_impl/todos.rs +++ b/app/src/ai/blocklist/block/view_impl/todos.rs @@ -36,7 +36,7 @@ pub(super) fn render_todos( // Add collapsible header. let id = id.clone(); - let mut header_config = HeaderConfig::new("Tasks", app) + let mut header_config = HeaderConfig::new(i18n::t("ai.todos.tasks"), app) .with_interaction_mode(InteractionMode::ManuallyExpandable( ExpandedConfig::new(state.is_expanded, state.header_toggle_mouse_state.clone()) .with_toggle_callback(move |ctx| { @@ -60,7 +60,7 @@ pub(super) fn render_todos( let is_list_outdated = has_cancelled_todo || todos.len() != conversation.active_todo_list().map_or(0, |list| list.len()); if is_list_outdated { - header_config = header_config.with_badge("Outdated".to_string()); + header_config = header_config.with_badge(i18n::t("ai.todos.outdated")); } let header_element = header_config.render(app); @@ -186,21 +186,26 @@ pub(super) fn render_completed_todo_items( if i == 0 { if let Some((index, list_len)) = index_and_len { - completed_text += format!( - "Completed {} ({}/{})", - completed_item.title, - index + 1, - list_len - ) - .as_str() + completed_text += i18n::t("ai.todos.completed_indexed") + .replace("{title}", &completed_item.title) + .replace("{current}", &(index + 1).to_string()) + .replace("{total}", &list_len.to_string()) + .as_str() } else { - completed_text += format!("Completed {}", completed_item.title).as_str() + completed_text += i18n::t("ai.todos.completed") + .replace("{title}", &completed_item.title) + .as_str() } } else if let Some((index, list_len)) = index_and_len { - completed_text += - format!(", {} ({}/{})", completed_item.title, index + 1, list_len).as_str() + completed_text += i18n::t("ai.todos.completed_item_separator_indexed") + .replace("{title}", &completed_item.title) + .replace("{current}", &(index + 1).to_string()) + .replace("{total}", &list_len.to_string()) + .as_str() } else { - completed_text += format!(", {}", completed_item.title).as_str() + completed_text += i18n::t("ai.todos.completed_item_separator") + .replace("{title}", &completed_item.title) + .as_str() } } if completed_text.is_empty() { diff --git a/app/src/ai/blocklist/code_block.rs b/app/src/ai/blocklist/code_block.rs index ea4c1d4f59..39870a43bd 100644 --- a/app/src/ai/blocklist/code_block.rs +++ b/app/src/ai/blocklist/code_block.rs @@ -62,7 +62,7 @@ fn render_file_icon(path: &Path, appearance: &Appearance, app: &AppContext) -> B fn render_button( appearance: &Appearance, icon: Icon, - tooltip_text: &str, + tooltip_text: impl Into, mouse_handle: MouseStateHandle, formatted_text: String, on_click: F, @@ -72,7 +72,7 @@ where F: FnMut(String, &mut EventContext) + 'static, { let ui_builder = appearance.ui_builder().clone(); - let tooltip_text = tooltip_text.to_owned(); + let tooltip_text = tooltip_text.into(); let mut on_click = on_click; let button_element = if let Some(color) = color { icon_button_with_color(appearance, icon, false, mouse_handle, color) @@ -217,7 +217,7 @@ fn render_linked_code_block_internal( let insert_button = render_button( appearance, Icon::AtSign, - "Add as Context", + i18n::t("ai.code_block.add_as_context"), mouse_handles.insert_button, insert_text, on_insert, @@ -236,7 +236,7 @@ fn render_linked_code_block_internal( let copy_button = render_button( appearance, Icon::Copy, - "Copy", + i18n::t("common.copy"), mouse_handles.copy_button, code_clone.clone(), on_copy, @@ -255,7 +255,7 @@ fn render_linked_code_block_internal( let open_button = render_button( appearance, Icon::LinkExternal, - "Open in Warp", + i18n::t("common.open_in_warp"), mouse_handles.open_button, code_clone.clone(), on_open, @@ -324,7 +324,7 @@ fn render_plain_code_block_internal( let copy_button = render_button( appearance, Icon::Copy, - "Copy", + i18n::t("common.copy"), mouse_handles.copy_button, code_clone.clone(), on_copy, @@ -339,7 +339,7 @@ fn render_plain_code_block_internal( let insert_button = render_button( appearance, Icon::TerminalInput, - "Run in terminal", + i18n::t("ai.code_block.run_in_terminal"), mouse_handles.insert_button, code_clone.clone(), on_execute, diff --git a/app/src/ai/blocklist/codebase_index_speedbump_banner.rs b/app/src/ai/blocklist/codebase_index_speedbump_banner.rs index 3d58cf8673..cf7a315742 100644 --- a/app/src/ai/blocklist/codebase_index_speedbump_banner.rs +++ b/app/src/ai/blocklist/codebase_index_speedbump_banner.rs @@ -14,17 +14,8 @@ use crate::terminal::view::{InlineBannerId, TerminalAction}; use crate::ui_components::blended_colors; use crate::ui_components::icons::Icon; -const SPEEDBUMP_HEADER: &str = "Index Codebase?"; -const SPEEDBUMP_TEXT: &str = "Indexing helps agents quickly understand context and provide targeted solutions. Code is never stored on the server."; /// Uniform padding around the banner const PADDING: f32 = 12.; -/// Text for the button that allows execution -const ALLOW_BUTTON_TEXT: &str = "Index codebase"; -const ALLOW_SETTINGS_TEXT: &str = "Allow automatic indexing"; -const DISMISS_FOREVER_BUTTON_TEXT: &str = "Don't show again"; - -const INDEXING_HEADER: &str = "Indexing codebase"; -const VIEW_STATUS_BUTTON_TEXT: &str = "View status"; #[derive(PartialEq, Clone)] pub enum VisibilityState { @@ -99,9 +90,9 @@ impl CodebaseIndexSpeedbumpBannerState { let title = ui_builder .span(if self.visibility_state == VisibilityState::Speedbump { - SPEEDBUMP_HEADER + i18n::t("ai.codebase_index.speedbump_header") } else { - INDEXING_HEADER + i18n::t("ai.codebase_index.indexing_header") }) .with_style(UiComponentStyles { font_color: Some(appearance.theme().foreground().into_solid()), @@ -114,7 +105,7 @@ impl CodebaseIndexSpeedbumpBannerState { col.add_child(title); let body = ui_builder - .span(SPEEDBUMP_TEXT) + .span(i18n::t("ai.codebase_index.speedbump_text")) .with_style(UiComponentStyles { font_color: Some(blended_colors::text_sub(theme, theme.surface_1())), font_size: Some(appearance.ui_font_size()), @@ -191,7 +182,7 @@ impl CodebaseIndexSpeedbumpBannerState { .finish(); let checkbox_text = ui_builder - .span(ALLOW_SETTINGS_TEXT) + .span(i18n::t("ai.codebase_index.allow_automatic_indexing")) .with_style(UiComponentStyles { font_color: Some(blended_colors::text_disabled(theme, theme.surface_1())), font_size: Some(appearance.ui_font_size()), @@ -226,7 +217,7 @@ impl CodebaseIndexSpeedbumpBannerState { ButtonVariant::Outlined, self.dont_show_again_mouse_state.clone(), ) - .with_text_label(DISMISS_FOREVER_BUTTON_TEXT.to_string()) + .with_text_label(i18n::t("common.do_not_show_again")) .with_style(UiComponentStyles { font_color: Some(appearance.theme().foreground().into_solid()), font_size: Some(appearance.ui_font_size()), @@ -261,7 +252,7 @@ impl CodebaseIndexSpeedbumpBannerState { ButtonVariant::Outlined, self.allow_button_mouse_state.clone(), ) - .with_text_label(ALLOW_BUTTON_TEXT.to_string()) + .with_text_label(i18n::t("ai.codebase_index.index_codebase")) .with_style(UiComponentStyles { font_color: Some(appearance.theme().foreground().into_solid()), font_size: Some(appearance.ui_font_size()), @@ -298,7 +289,7 @@ impl CodebaseIndexSpeedbumpBannerState { ButtonVariant::Outlined, self.view_status_button_mouse_state.clone(), ) - .with_text_label(VIEW_STATUS_BUTTON_TEXT.to_string()) + .with_text_label(i18n::t("ai.codebase_index.view_status")) .with_style(UiComponentStyles { font_color: Some(appearance.theme().foreground().into_solid()), font_size: Some(appearance.ui_font_size()), diff --git a/app/src/ai/blocklist/inline_action/ask_user_question_view.rs b/app/src/ai/blocklist/inline_action/ask_user_question_view.rs index 46012be4a3..f570a6fbb9 100644 --- a/app/src/ai/blocklist/inline_action/ask_user_question_view.rs +++ b/app/src/ai/blocklist/inline_action/ask_user_question_view.rs @@ -764,7 +764,7 @@ impl AskUserQuestionView { ctx, ); let skip_button = CompactibleActionButton::new( - "Skip all".to_string(), + i18n::t("ai.ask_user_question.skip_all"), Some(KeystrokeSource::Fixed(CTRL_C_KEYSTROKE.clone())), ButtonSize::InlineActionHeader, AskUserQuestionViewAction::SkipAll, @@ -773,7 +773,7 @@ impl AskUserQuestionView { ctx, ); let next_button = CompactibleActionButton::new( - "Next".to_string(), + i18n::t("common.next"), Some(KeystrokeSource::Fixed( Keystroke::parse("enter").expect("keystroke should parse"), )), @@ -1067,7 +1067,7 @@ impl AskUserQuestionView { number, accepted_text .clone() - .unwrap_or_else(|| "Other...".to_string()), + .unwrap_or_else(|| i18n::t("ai.ask_user_question.other")), accepted_text.is_some(), false, true, @@ -1083,7 +1083,7 @@ impl AskUserQuestionView { let initial_text = initial_text.map(String::from); let input = ctx.add_view(move |ctx| { let input = compact_agent_input::CompactAgentInput::new(ctx); - input.set_placeholder_text("Type your answer and press Enter", ctx); + input.set_placeholder_text(i18n::t("ai.ask_user_question.placeholder"), ctx); if let Some(initial_text) = initial_text.as_deref() { input.set_text(initial_text, ctx); } @@ -1264,7 +1264,7 @@ impl AskUserQuestionView { let current = self.session.current()?; let mut question_text = current.question.question.clone(); if current.question.is_multiselect() { - question_text.push_str(" (select all that apply)"); + question_text.push_str(&i18n::t("ai.ask_user_question.select_all_suffix")); } let has_nav_footer = self.session.has_multiple_questions(); let container_height = ask_user_question_container_height( @@ -1286,7 +1286,7 @@ impl AskUserQuestionView { .finish(), ); content.add_child( - HeaderConfig::new("Agent questions", app) + HeaderConfig::new(i18n::t("ai.ask_user_question.agent_questions"), app) .with_icon(yellow_stop_icon(appearance)) .with_corner_radius_override(CornerRadius::with_top(Radius::Pixels(8.))) .render_header(app, Some(header_right.finish())), @@ -1319,7 +1319,7 @@ impl AskUserQuestionView { fn render_unavailable(&self, appearance: &Appearance, app: &AppContext) -> Box { wrap_with_agent_output_item_spacing( - HeaderConfig::new("Questions unavailable".to_string(), app) + HeaderConfig::new(i18n::t("ai.ask_user_question.questions_unavailable"), app) .with_icon(inline_action_icons::reverted_icon(appearance)) .render(app), app, @@ -1359,12 +1359,12 @@ impl AskUserQuestionView { } AskUserQuestionResult::Error(_) | AskUserQuestionResult::Cancelled => ( None, - "Questions skipped".to_string(), + i18n::t("ai.ask_user_question.questions_skipped"), inline_action_icons::reverted_icon(appearance), ), AskUserQuestionResult::SkippedByAutoApprove { .. } => ( None, - "Questions skipped due to auto-approve".to_string(), + i18n::t("ai.ask_user_question.questions_skipped_auto_approve"), inline_action_icons::reverted_icon(appearance), ), }; @@ -1449,7 +1449,7 @@ impl AskUserQuestionView { let theme = appearance.theme(); let dropdown = self.speedbump_dropdown.as_ref()?; let row = render_autonomy_dropdown_setting_speedbump_footer( - "Allow the agent to ask questions:", + i18n::t("ai.ask_user_question.allow_agent_to_ask_questions"), dropdown, settings_link_handle, app, @@ -1536,7 +1536,10 @@ impl AskUserQuestionView { let nav_message = Message::new(vec![ MessageItem::clickable( - vec![MessageItem::keystroke(left_key), MessageItem::text("prev")], + vec![ + MessageItem::keystroke(left_key), + MessageItem::text(i18n::t("common.previous")), + ], |ctx| { ctx.dispatch_typed_action(AskUserQuestionViewAction::NavigatePrev); }, @@ -1544,7 +1547,10 @@ impl AskUserQuestionView { ), MessageItem::text(" / "), MessageItem::clickable( - vec![MessageItem::keystroke(right_key), MessageItem::text("next")], + vec![ + MessageItem::keystroke(right_key), + MessageItem::text(i18n::t("common.next")), + ], |ctx| { ctx.dispatch_typed_action(AskUserQuestionViewAction::NavigateNext); }, @@ -1730,21 +1736,21 @@ fn ask_user_question_completion_state( if answered_count == 0 { AskUserQuestionCompletionState { - label: "Questions skipped".to_string(), + label: i18n::t("ai.ask_user_question.questions_skipped"), status_icon: inline_action_icons::reverted_icon(appearance), } } else { let label = if answered_count == total { if total == 1 { - "Answered question".to_string() + i18n::t("ai.ask_user_question.answered_question") } else { - format!("Answered all {total} questions") + i18n::t("ai.ask_user_question.answered_all_questions") + .replace("{total}", &total.to_string()) } } else { - format!( - "Answered {answered_count} of {total} question{}", - if total == 1 { "" } else { "s" } - ) + i18n::t("ai.ask_user_question.answered_partial") + .replace("{answered_count}", &answered_count.to_string()) + .replace("{total}", &total.to_string()) }; AskUserQuestionCompletionState { label, @@ -1767,15 +1773,15 @@ fn render_answers( let mut content = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch); for (index, question) in questions.iter().enumerate() { let answer = answers.and_then(|answers| answers.get(index)); - let question_text = format!("Q: {}", question.question); + let question_text = i18n::t("ai.ask_user_question.question_prefix") + .replace("{question}", &question.question); let question_label = render_text_with_markdown_support(&question_text, font_size, text_color, appearance); - let answer_text = format!( - "A: {}", - answer - .map(AskUserQuestionAnswerItem::display_text) - .unwrap_or_else(|| "Skipped".to_string()) - ); + let answer_display = answer + .map(AskUserQuestionAnswerItem::display_text) + .unwrap_or_else(|| i18n::t("ai.ask_user_question.skipped")); + let answer_text = + i18n::t("ai.ask_user_question.answer_prefix").replace("{answer}", &answer_display); let answer_label = render_text_with_markdown_support(&answer_text, font_size, muted_color, appearance); diff --git a/app/src/ai/blocklist/inline_action/aws_bedrock_credentials_error.rs b/app/src/ai/blocklist/inline_action/aws_bedrock_credentials_error.rs index 15e5928c99..91d95cd95d 100644 --- a/app/src/ai/blocklist/inline_action/aws_bedrock_credentials_error.rs +++ b/app/src/ai/blocklist/inline_action/aws_bedrock_credentials_error.rs @@ -57,7 +57,7 @@ impl AwsBedrockCredentialsErrorView { ) -> Self { // Run button let run_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Refresh AWS Credentials", PrimaryTheme) + ActionButton::new(i18n::t("ai.aws_bedrock.refresh_credentials"), PrimaryTheme) .with_size(ButtonSize::InlineActionHeader) .on_click(|ctx| { ctx.dispatch_typed_action(AwsBedrockCredentialsErrorAction::RunLoginCommand) @@ -66,7 +66,7 @@ impl AwsBedrockCredentialsErrorView { // Configure button let configure_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Configure", NakedTheme) + ActionButton::new(i18n::t("common.configure"), NakedTheme) .with_size(ButtonSize::InlineActionHeader) .on_click(|ctx| { ctx.dispatch_typed_action(AwsBedrockCredentialsErrorAction::Configure) @@ -138,7 +138,7 @@ impl View for AwsBedrockCredentialsErrorView { let make_alert_text = || { Text::new( - "AWS credentials expired or missing", + i18n::t("ai.aws_bedrock.credentials_expired_or_missing"), appearance.ui_font_family(), 14., ) @@ -187,7 +187,7 @@ impl View for AwsBedrockCredentialsErrorView { .finish(); let checkbox_label = Text::new( - "Always run automatically", + i18n::t("ai.aws_bedrock.always_run_automatically"), appearance.ui_font_family(), appearance.monospace_font_size() - 1., ) diff --git a/app/src/ai/blocklist/inline_action/code_diff_view.rs b/app/src/ai/blocklist/inline_action/code_diff_view.rs index 735778e76b..04d33234c3 100644 --- a/app/src/ai/blocklist/inline_action/code_diff_view.rs +++ b/app/src/ai/blocklist/inline_action/code_diff_view.rs @@ -100,16 +100,6 @@ use crate::view_components::DismissibleToast; use crate::workspace::ToastStack; use crate::{cmd_or_ctrl_shift, send_telemetry_from_ctx, TelemetryEvent}; -const REQUESTED_EDIT_CANCEL_LABEL: &str = "Cancel"; -const REQUESTED_EDIT_REFINE_LABEL: &str = "Refine"; -const REQUESTED_EDIT_ACCEPT_LABEL: &str = "Accept"; -const REQUESTED_EDIT_ACCEPT_AND_AUTOEXECUTE_LABEL: &str = "Auto-approve"; -const REQUESTED_EDIT_EDIT_LABEL: &str = "Edit"; -const REQUESTED_EDIT_MINIMIZE_LABEL: &str = "Done"; -const SUGGESTED_EDIT_ACCEPT_LABEL: &str = "Accept"; -const SUGGESTED_EDIT_ACCEPT_AND_CONTINUE_LABEL: &str = "Accept and continue with agent"; -const SUGGESTED_EDIT_ITERATE_WITH_AGENT_LABEL: &str = "Iterate with agent"; -const SUGGESTED_EDIT_DISMISS_LABEL: &str = "Dismiss"; const MAX_EDITOR_HEIGHT: f32 = 500.; const INLINE_EDITOR_HEIGHT: f32 = 94.; const INLINE_EDITOR_HEIGHT_EXPANDED: f32 = 400.; @@ -189,7 +179,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( EDIT_REQUESTED_EDIT_NAME, - "Edit Code Diff", + i18n::t("ai.code_diff.edit_code_diff"), CodeDiffViewAction::Edit, ) .with_context_predicate(id!(CodeDiffView::ui_name()) & !id!(DISPATCHED_REQUESTED_EDIT_EXPANDED)) @@ -508,7 +498,7 @@ impl CodeDiffView { self.accept_split_button_menu.update(ctx, |menu, ctx| { menu.set_items( vec![MenuItemFields::new_multiline( - SUGGESTED_EDIT_ACCEPT_AND_CONTINUE_LABEL, + i18n::t("ai.code_diff.accept_and_continue_with_agent"), 2, ) .with_on_select_action( @@ -529,19 +519,15 @@ impl CodeDiffView { .map(|k| k.displayed()) .unwrap_or_default(); - let accept_item = MenuItemFields::new_with_label( - REQUESTED_EDIT_ACCEPT_LABEL, - accept_keystroke.as_str(), - ) - .with_on_select_action(CodeDiffViewAction::TryAccept) - .into_item(); + let accept_item = + MenuItemFields::new_with_label(i18n::t("ai.command.accept"), accept_keystroke) + .with_on_select_action(CodeDiffViewAction::TryAccept) + .into_item(); - let auto_item = MenuItemFields::new_with_label( - REQUESTED_EDIT_ACCEPT_AND_AUTOEXECUTE_LABEL, - auto_keystroke.as_str(), - ) - .with_on_select_action(CodeDiffViewAction::AcceptAndAutoExecute) - .into_item(); + let auto_item = + MenuItemFields::new_with_label(i18n::t("ai.command.auto_approve"), auto_keystroke) + .with_on_select_action(CodeDiffViewAction::AcceptAndAutoExecute) + .into_item(); self.accept_split_button_menu.update(ctx, |menu, ctx| { menu.set_items(vec![accept_item, auto_item], ctx); @@ -634,7 +620,9 @@ impl CodeDiffView { full: ("Failed to save file for accepted AgentMode diffs for {}: {}", file_path_clone, error) ); let toast = DismissibleToast::error(format!( - "Failed to save file {file_path_clone}" + "{}", + i18n::t("ai.code_diff.failed_to_save_file") + .replace("{file_path}", &file_path_clone) )); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast(toast, window_id, ctx); @@ -783,12 +771,12 @@ impl CodeDiffView { .collect(); let cancel_button_label = if is_passive { - SUGGESTED_EDIT_DISMISS_LABEL + i18n::t("common.dismiss") } else { - REQUESTED_EDIT_REFINE_LABEL + i18n::t("common.refine") }; let cancel_button = CompactibleActionButton::new( - cancel_button_label.to_string(), + cancel_button_label, Some(KeystrokeSource::Fixed( CANCEL_REQUESTED_EDIT_KEYSTROKE.clone(), )), @@ -800,7 +788,7 @@ impl CodeDiffView { ); let edit_button = CompactibleActionButton::new( - REQUESTED_EDIT_EDIT_LABEL.to_string(), + i18n::t("common.edit"), Some(KeystrokeSource::Binding(EDIT_REQUESTED_EDIT_NAME)), ButtonSize::Small, CodeDiffViewAction::Edit, @@ -810,7 +798,7 @@ impl CodeDiffView { ); let minimize_button = CompactibleActionButton::new( - REQUESTED_EDIT_MINIMIZE_LABEL.to_string(), + i18n::t("common.done"), Some(KeystrokeSource::Fixed( MINIMIZE_REQUESTED_EDIT_KEYSTROKE.clone(), )), @@ -822,7 +810,7 @@ impl CodeDiffView { ); let iterate_with_agent_button = CompactibleActionButton::new( - SUGGESTED_EDIT_ITERATE_WITH_AGENT_LABEL.to_string(), + i18n::t("ai.code_diff.iterate_with_agent"), Some(KeystrokeSource::Binding(SET_INPUT_MODE_AGENT_ACTION_NAME)), ButtonSize::Small, CodeDiffViewAction::IterateOnPassiveDiffWithAgent, @@ -832,11 +820,7 @@ impl CodeDiffView { ); let accept_and_autoexecute_split_button = CompactibleSplitActionButton::new( - if is_passive { - SUGGESTED_EDIT_ACCEPT_LABEL.to_string() - } else { - REQUESTED_EDIT_ACCEPT_LABEL.to_string() - }, + i18n::t("ai.command.accept"), Some(accept_keystroke_source(is_passive)), ButtonSize::Small, CodeDiffViewAction::TryAccept, @@ -867,7 +851,7 @@ impl CodeDiffView { let code_review_button = ctx.add_typed_action_view(|ctx| { ActionButton::new("", NakedTheme) .with_icon(Icon::Diff) - .with_tooltip("Review changes") + .with_tooltip(i18n::t("ai.block.review_changes")) .with_width(icon_size(ctx)) .with_height(icon_size(ctx)) .on_click(|ctx| { @@ -879,7 +863,7 @@ impl CodeDiffView { let expansion_button_collapsed = ctx.add_typed_action_view(|ctx| { ActionButton::new("", NakedTheme) .with_icon(Icon::ChevronRight) - .with_tooltip("Expand") + .with_tooltip(i18n::t("common.expand")) .with_width(icon_size(ctx)) .with_height(icon_size(ctx)) .on_click(|ctx| { @@ -890,7 +874,7 @@ impl CodeDiffView { let expansion_button_expanded = ctx.add_typed_action_view(|ctx| { ActionButton::new("", NakedTheme) .with_icon(Icon::ChevronDown) - .with_tooltip("Collapse") + .with_tooltip(i18n::t("common.collapse")) .with_width(icon_size(ctx)) .with_height(icon_size(ctx)) .on_click(|ctx| { @@ -1137,7 +1121,10 @@ impl CodeDiffView { .unwrap_or_else(|| "file".to_string()); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error(format!("Failed to revert changes to {file_name}")), + DismissibleToast::error( + i18n::t("ai.code_diff.revert_failed") + .replace("{file_name}", &file_name), + ), window_id, ctx, ); @@ -1653,7 +1640,7 @@ impl CodeDiffView { fg_overlay_6(appearance.theme()) }; let mcp_config_button = render_provider_icon_button( - "Open config", + &i18n::t("ai.code_diff.open_config"), mcp_button_handle.clone(), appearance, icon, @@ -1851,10 +1838,10 @@ impl CodeDiffView { let diff_type = diff.diff_view.as_ref(app).diff(); let file_name = match diff.diff_view.as_ref(app).file_name() { Some(file_name) if matches!(diff_type, Some(DiffType::Create { .. })) => { - format!("{file_name} (new)") + i18n::t("ai.code_diff.file_name_new").replace("{file_name}", &file_name) } Some(file_name) if matches!(diff_type, Some(DiffType::Delete { .. })) => { - format!("{file_name} (deleted)") + i18n::t("ai.code_diff.file_name_deleted").replace("{file_name}", &file_name) } Some(file_name) => { // Check if this is a rename @@ -1869,7 +1856,7 @@ impl CodeDiffView { file_name } } - None => "No file name".to_string(), + None => i18n::t("ai.code_diff.no_file_name"), }; // Get the full path for the tooltip @@ -1989,7 +1976,7 @@ impl CodeDiffView { if Self::is_rename_without_changes(diff_type) { let placeholder = Container::new( Text::new( - "File renamed without changes", + i18n::t("ai.code_diff.file_renamed_without_changes"), appearance.monospace_font_family(), appearance.monospace_font_size(), ) @@ -2170,11 +2157,11 @@ impl CodeDiffView { if self.display_mode.is_embedded() { let label = if self.is_passive { - SUGGESTED_EDIT_DISMISS_LABEL + i18n::t("common.dismiss") } else { - REQUESTED_EDIT_CANCEL_LABEL + i18n::t("common.cancel") }; - self.cancel_button.set_label(label.to_string(), ctx); + self.cancel_button.set_label(label, ctx); } for diff in &self.pending_diffs { @@ -2529,7 +2516,7 @@ impl CodeDiffView { let checkbox_text = appearance .ui_builder() - .span("Don't show me suggested code banners again") + .span(i18n::t("ai.code_diff.hide_suggested_code_banners")) .with_style(UiComponentStyles { font_color: Some(font_color), font_size: Some(font_size), @@ -2542,7 +2529,7 @@ impl CodeDiffView { let formatted_text = FormattedTextElement::new( FormattedText::new([FormattedTextLine::Line(vec![ FormattedTextFragment::hyperlink( - "Manage suggested code banner settings", + i18n::t("ai.code_diff.manage_suggested_code_banner_settings"), "Settings > AI", ), ])]), @@ -3155,7 +3142,7 @@ impl BackingView for CodeDiffView { // Code diffs should show "Requested Edit" as the title and hide the close button // since they are closed via accept/reject actions. view::HeaderContent::Standard(view::StandardHeader { - title: "Requested Edit".to_string(), + title: i18n::t("ai.code_diff.requested_edit"), title_secondary: None, title_style: None, title_clip_config: warpui::text_layout::ClipConfig::start(), diff --git a/app/src/ai/blocklist/inline_action/host_picker.rs b/app/src/ai/blocklist/inline_action/host_picker.rs index b310a1602f..f81549c874 100644 --- a/app/src/ai/blocklist/inline_action/host_picker.rs +++ b/app/src/ai/blocklist/inline_action/host_picker.rs @@ -49,8 +49,6 @@ pub enum HostPickerEvent { Closed, } -const CUSTOM_HOST_LABEL: &str = "Custom host…"; -const DEFAULT_BADGE: &str = "Default"; const EDITOR_PLACEHOLDER: &str = "my-worker-host"; // ── Internal action plumbing ──────────────────────────────────────── @@ -429,7 +427,7 @@ pub(crate) fn build_menu_items( if let Some(slug) = default_host { items.push(menu_item_for_known( slug, - Some(DEFAULT_BADGE), + Some(&i18n::t("common.default")), InternalAction::SelectKnown(slug.to_string()), )); known_slugs.push(slug.to_string()); @@ -471,7 +469,7 @@ pub(crate) fn build_menu_items( } } items.push(MenuItem::Item( - MenuItemFields::new(CUSTOM_HOST_LABEL).with_on_select_action( + MenuItemFields::new(i18n::t("ai.host_picker.custom_host")).with_on_select_action( DropdownAction::select_action_and_close(InternalAction::EnterCustomMode), ), )); @@ -483,7 +481,7 @@ pub(crate) fn build_menu_items( /// badge when it matches the workspace default. pub(crate) fn menu_label_for(slug: &str, default_host: Option<&str>) -> String { if default_host == Some(slug) { - format_known_label(slug, Some(DEFAULT_BADGE)) + format_known_label(slug, Some(&i18n::t("common.default"))) } else { format_known_label(slug, None) } diff --git a/app/src/ai/blocklist/inline_action/orchestration_controls.rs b/app/src/ai/blocklist/inline_action/orchestration_controls.rs index 12a87163ef..204cb49b1f 100644 --- a/app/src/ai/blocklist/inline_action/orchestration_controls.rs +++ b/app/src/ai/blocklist/inline_action/orchestration_controls.rs @@ -61,7 +61,6 @@ const DEFAULT_HOST_ENV_VAR: &str = "WARP_CLOUD_MODE_DEFAULT_HOST"; // ── Shared constants ──────────────────────────────────────────────── pub const ORCHESTRATION_WARP_WORKER_HOST: &str = WARP_WORKER_HOST; -pub const ORCHESTRATION_ENV_NONE_LABEL: &str = "Empty environment"; pub const ORCHESTRATION_PICKER_HEIGHT: f32 = 36.; pub const ORCHESTRATION_PICKER_BORDER_WIDTH: f32 = 1.; @@ -69,17 +68,9 @@ pub const ORCHESTRATION_PICKER_FONT_SIZE: f32 = 14.; pub const ORCHESTRATION_PICKER_RADIUS: f32 = 4.; pub const ORCHESTRATION_PICKER_MAX_WIDTH: f32 = 205.; -const DEFAULT_MODEL_LABEL: &str = "Default model"; const ORCHESTRATION_SEGMENTED_CONTROL_PADDING: f32 = 4.; const ORCHESTRATION_SEGMENT_VERTICAL_PADDING: f32 = 4.; -/// Label shown in the auth secret picker when no secret is selected -/// (the child agent will inherit credentials from its environment). -const AUTH_SECRET_INHERIT_LABEL: &str = "Skip (advanced)"; -/// Label for the auth secret column. -pub const AUTH_SECRET_COLUMN_LABEL: &str = "API key"; -const AUTH_SECRET_CREATE_NEW_LABEL: &str = "New API key…"; - // ── Action trait ──────────────────────────────────────────────────── /// Trait that both `RunAgentsCardViewAction` and @@ -533,7 +524,8 @@ pub fn populate_model_picker_for_harness // Local Codex: only "Default model" entry. let items = vec![default_model_menu_item::()]; dropdown.set_rich_items(items, ctx_dropdown); - dropdown.set_selected_by_name(DEFAULT_MODEL_LABEL, ctx_dropdown); + dropdown + .set_selected_by_name(i18n::t("ai.orchestration.default_model"), ctx_dropdown); } Some(harness) => { // Non-Oz harness: "Default model" at top, then server-provided @@ -552,7 +544,7 @@ pub fn populate_model_picker_for_harness } // Find display name before set_rich_items borrows ctx_dropdown mutably. let selected_display_name = if initial_model_id.is_empty() { - Some(DEFAULT_MODEL_LABEL.to_string()) + Some(i18n::t("ai.orchestration.default_model")) } else { availability .models_for(harness) @@ -562,7 +554,7 @@ pub fn populate_model_picker_for_harness .find(|m| m.id == initial_model_id) .map(|m| m.display_name.clone()) }) - .or_else(|| Some(DEFAULT_MODEL_LABEL.to_string())) + .or_else(|| Some(i18n::t("ai.orchestration.default_model"))) }; dropdown.set_rich_items(items, ctx_dropdown); if let Some(name) = &selected_display_name { @@ -576,7 +568,7 @@ pub fn populate_model_picker_for_harness /// Creates a "Default model" menu item that emits an empty model_id. fn default_model_menu_item() -> MenuItem { MenuItem::Item( - MenuItemFields::new(DEFAULT_MODEL_LABEL).with_on_select_action( + MenuItemFields::new(i18n::t("ai.orchestration.default_model")).with_on_select_action( DropdownAction::select_action_and_close(A::model_changed(String::new())), ), ) @@ -716,9 +708,13 @@ pub fn populate_harness_picker( } else { fields = fields.with_disabled(true); let tooltip = match local_setup_state { - Some(LocalHarnessSetupState::MissingHarness { tooltip }) => tooltip, - Some(LocalHarnessSetupState::ProductDisabled { message }) => message, - Some(LocalHarnessSetupState::Ready) | None => "Disabled by your administrator", + Some(LocalHarnessSetupState::MissingHarness { tooltip }) => tooltip.to_string(), + Some(LocalHarnessSetupState::ProductDisabled { message }) => { + message.to_string() + } + Some(LocalHarnessSetupState::Ready) | None => { + i18n::t("ai.orchestration.disabled_by_admin") + } }; fields = fields.with_tooltip(tooltip); } @@ -779,12 +775,13 @@ pub fn create_environment_picker( let mut items: Vec> = Vec::new(); let mut selected_name: Option = None; items.push(MenuItem::Item( - MenuItemFields::new(ORCHESTRATION_ENV_NONE_LABEL).with_on_select_action( - DropdownAction::select_action_and_close(A::environment_changed(String::new())), - ), + MenuItemFields::new(i18n::t("ai.orchestration.empty_environment")) + .with_on_select_action(DropdownAction::select_action_and_close( + A::environment_changed(String::new()), + )), )); if initial_env.is_empty() { - selected_name = Some(ORCHESTRATION_ENV_NONE_LABEL.to_string()); + selected_name = Some(i18n::t("ai.orchestration.empty_environment")); } for (env_id, env_name) in &sorted_envs { if env_id == &initial_env { @@ -824,12 +821,13 @@ pub fn populate_environment_picker( let mut items: Vec> = Vec::new(); let mut selected_name: Option = None; items.push(MenuItem::Item( - MenuItemFields::new(ORCHESTRATION_ENV_NONE_LABEL).with_on_select_action( - DropdownAction::select_action_and_close(A::environment_changed(String::new())), - ), + MenuItemFields::new(i18n::t("ai.orchestration.empty_environment")) + .with_on_select_action(DropdownAction::select_action_and_close( + A::environment_changed(String::new()), + )), )); if initial_env.is_empty() { - selected_name = Some(ORCHESTRATION_ENV_NONE_LABEL.to_string()); + selected_name = Some(i18n::t("ai.orchestration.empty_environment")); } for (env_id, env_name) in &sorted_envs { if env_id == &initial_env { @@ -882,9 +880,13 @@ fn render_new_environment_footer( .finish(), ) .with_child( - Text::new_inline("New environment", font_family, font_size) - .with_color(text_color.into()) - .finish(), + Text::new_inline( + i18n::t("agent_input_footer.new_environment"), + font_family, + font_size, + ) + .with_color(text_color.into()) + .finish(), ) .finish(), ) @@ -1162,7 +1164,7 @@ pub fn accept_disabled_reason_with_auth( } } if auth_secret_selection_required(state, ctx) { - return Some("Select an API key for this harness to continue.".to_string()); + return Some(i18n::t("ai.orchestration.select_api_key_for_harness")); } None } @@ -1199,7 +1201,7 @@ pub fn populate_auth_secret_picker_for_harness> = Vec::new(); items.push(MenuItem::Item( - MenuItemFields::new(AUTH_SECRET_INHERIT_LABEL).with_on_select_action( + MenuItemFields::new(i18n::t("ai.orchestration.skip_advanced")).with_on_select_action( DropdownAction::select_action_and_close(A::auth_secret_changed(None)), ), )); @@ -1223,12 +1225,13 @@ pub fn populate_auth_secret_picker_for_harness { items.push(MenuItem::Item( - MenuItemFields::new("Loading…").with_disabled(true), + MenuItemFields::new(i18n::t("common.loading")).with_disabled(true), )); } AuthSecretFetchState::Failed(_) => { items.push(MenuItem::Item( - MenuItemFields::new("Unable to load secrets").with_disabled(true), + MenuItemFields::new(i18n::t("ai.auth_secret.unable_to_load_secrets")) + .with_disabled(true), )); } } @@ -1236,7 +1239,7 @@ pub fn populate_auth_secret_picker_for_harness name.clone(), - AuthSecretSelection::Inherit => AUTH_SECRET_INHERIT_LABEL.to_string(), + AuthSecretSelection::Inherit => i18n::t("ai.orchestration.skip_advanced"), AuthSecretSelection::Unset if supports_create_new => { - AUTH_SECRET_CREATE_NEW_LABEL.to_string() + i18n::t("ai.orchestration.new_api_key") } - AuthSecretSelection::Unset => AUTH_SECRET_INHERIT_LABEL.to_string(), + AuthSecretSelection::Unset => i18n::t("ai.orchestration.skip_advanced"), }; let _ = selected_display_name; let _ = &availability; @@ -1572,7 +1575,7 @@ pub fn sync_picker_selections( } Some(harness) => { if target_model_id.is_empty() { - Some(DEFAULT_MODEL_LABEL.to_string()) + Some(i18n::t("ai.orchestration.default_model")) } else { let availability = HarnessAvailabilityModel::as_ref(ctx_dropdown); availability.models_for(harness).and_then(|models| { @@ -1614,7 +1617,10 @@ pub fn sync_picker_selections( }; environment_picker.update(ctx, |dropdown, ctx_dropdown| { if env_id.is_empty() { - dropdown.set_selected_by_name(ORCHESTRATION_ENV_NONE_LABEL, ctx_dropdown); + dropdown.set_selected_by_name( + i18n::t("ai.orchestration.empty_environment"), + ctx_dropdown, + ); return; } let all_envs = CloudAmbientAgentEnvironment::get_all(ctx_dropdown); @@ -1641,11 +1647,11 @@ pub fn sync_picker_selections( auth_secret_picker.update(ctx, |dropdown, ctx_dropdown| { let label = match &selection { AuthSecretSelection::Named(name) => name.clone(), - AuthSecretSelection::Inherit => AUTH_SECRET_INHERIT_LABEL.to_string(), + AuthSecretSelection::Inherit => i18n::t("ai.orchestration.skip_advanced"), AuthSecretSelection::Unset if supports_create_new => { - AUTH_SECRET_CREATE_NEW_LABEL.to_string() + i18n::t("ai.orchestration.new_api_key") } - AuthSecretSelection::Unset => AUTH_SECRET_INHERIT_LABEL.to_string(), + AuthSecretSelection::Unset => i18n::t("ai.orchestration.skip_advanced"), }; dropdown.set_selected_by_name(&label, ctx_dropdown); }); @@ -1806,7 +1812,7 @@ pub fn render_mode_toggle( ) -> Box { let theme = appearance.theme(); let label = Text::new( - "Agent location".to_string(), + i18n::t("ai.orchestration.agent_location"), appearance.ui_font_family(), appearance.monospace_font_size() - 1., ) @@ -1814,7 +1820,7 @@ pub fn render_mode_toggle( .finish(); let local_segment = render_segment_button::( - "Local", + &i18n::t("ai.orchestration.local"), !is_remote, A::execution_mode_toggled(false), handles.local_toggle.clone(), @@ -1822,7 +1828,7 @@ pub fn render_mode_toggle( active_segment_bg, ); let cloud_segment = render_segment_button::( - "Cloud", + &i18n::t("ai.orchestration.cloud"), is_remote, A::execution_mode_toggled(true), handles.cloud_toggle.clone(), @@ -1945,7 +1951,7 @@ pub fn render_picker_row_with_layout( if show_harness_picker { add( &mut column, - "Agent harness", + &i18n::t("ai.orchestration.agent_harness"), handles .harness_picker .as_ref() @@ -1955,7 +1961,7 @@ pub fn render_picker_row_with_layout( if show_auth_picker { add( &mut column, - AUTH_SECRET_COLUMN_LABEL, + &i18n::t("ai.orchestration.api_key"), handles .auth_secret_picker .as_ref() @@ -1965,7 +1971,7 @@ pub fn render_picker_row_with_layout( if is_remote { add( &mut column, - "Host", + &i18n::t("ai.orchestration.host"), handles .host_picker .as_ref() @@ -1973,7 +1979,7 @@ pub fn render_picker_row_with_layout( ); add( &mut column, - "Environment", + &i18n::t("ai.orchestration.environment"), handles .environment_picker .as_ref() @@ -1982,7 +1988,7 @@ pub fn render_picker_row_with_layout( } add( &mut column, - "Base model", + &i18n::t("ai.orchestration.base_model"), handles .model_picker .as_ref() @@ -2004,7 +2010,7 @@ pub fn render_picker_row_with_layout( if show_harness_picker { add_picker( &mut row, - "Agent harness", + &i18n::t("ai.orchestration.agent_harness"), handles .harness_picker .as_ref() @@ -2014,7 +2020,7 @@ pub fn render_picker_row_with_layout( if is_remote { add_picker( &mut row, - "Host", + &i18n::t("ai.orchestration.host"), handles .host_picker .as_ref() @@ -2022,7 +2028,7 @@ pub fn render_picker_row_with_layout( ); add_picker( &mut row, - "Environment", + &i18n::t("ai.orchestration.environment"), handles .environment_picker .as_ref() @@ -2031,7 +2037,7 @@ pub fn render_picker_row_with_layout( } add_picker( &mut row, - "Base model", + &i18n::t("ai.orchestration.base_model"), handles .model_picker .as_ref() @@ -2040,7 +2046,7 @@ pub fn render_picker_row_with_layout( if show_auth_picker { add_picker( &mut row, - AUTH_SECRET_COLUMN_LABEL, + &i18n::t("ai.orchestration.api_key"), handles .auth_secret_picker .as_ref() @@ -2112,8 +2118,8 @@ pub fn empty_env_recommendation_message( } let env_count = CloudAmbientAgentEnvironment::get_all(app).len(); Some(if env_count > 0 { - "We recommend selecting an environment for cloud agents.".to_string() + i18n::t("ai.orchestration.recommend_select_environment") } else { - "We recommend creating an environment for cloud agents.".to_string() + i18n::t("ai.orchestration.recommend_create_environment") }) } diff --git a/app/src/ai/blocklist/inline_action/requested_action.rs b/app/src/ai/blocklist/inline_action/requested_action.rs index 938d537bc0..4d7eb65e51 100644 --- a/app/src/ai/blocklist/inline_action/requested_action.rs +++ b/app/src/ai/blocklist/inline_action/requested_action.rs @@ -38,9 +38,6 @@ use crate::ai::blocklist::inline_action::inline_action_header::{ use crate::ai::blocklist::inline_action::inline_action_icons::icon_size; use crate::ui_components::blended_colors; -const REQUESTED_ACTION_CANCEL_LABEL: &str = "Cancel"; -const REQUESTED_ACTION_RUN_LABEL: &str = "Run"; - const KEYBOARD_SHORTCUT_MARGIN_RIGHT: f32 = 8.; lazy_static! { @@ -242,16 +239,18 @@ pub(super) fn render_header_buttons( const BUTTON_MARGIN_RIGHT: f32 = 16.; let appearance = Appearance::as_ref(app); + let cancel_label = i18n::t("common.cancel"); + let run_label = i18n::t("common.run"); let width_required_for_full_size_layout = approx_keystroke_button_width( - REQUESTED_ACTION_CANCEL_LABEL, + &cancel_label, appearance.monospace_font_size(), cancel_keystroke, None, app, ) + BUTTON_MARGIN_RIGHT + approx_keystroke_button_width( - REQUESTED_ACTION_RUN_LABEL, + &run_label, appearance.monospace_font_size(), run_keystroke, None, @@ -265,14 +264,14 @@ pub(super) fn render_header_buttons( ..Default::default() }; let width_required_for_compact_layout = approx_keystroke_button_width( - REQUESTED_ACTION_CANCEL_LABEL, + &cancel_label, compact_button_font_size, cancel_keystroke, Some(compact_button_styles), app, ) .max(approx_keystroke_button_width( - REQUESTED_ACTION_RUN_LABEL, + &run_label, compact_button_font_size, run_keystroke, Some(compact_button_styles), @@ -286,7 +285,7 @@ pub(super) fn render_header_buttons( let mut default_row = Flex::row().with_child( Container::new(render_keyboard_shortcut_button( - REQUESTED_ACTION_CANCEL_LABEL, + cancel_label.clone(), Some(cancel_keystroke.clone()), cancel_button.clone(), cancel_callback, @@ -298,7 +297,7 @@ pub(super) fn render_header_buttons( ); let mut size_constrained_column = Flex::column().with_child(render_keyboard_shortcut_button( - REQUESTED_ACTION_CANCEL_LABEL, + cancel_label, Some(cancel_keystroke.clone()), cancel_button.clone(), cancel_clone, @@ -308,7 +307,7 @@ pub(super) fn render_header_buttons( if should_show_accept_button { default_row.add_child(render_keyboard_shortcut_button( - REQUESTED_ACTION_RUN_LABEL, + run_label.clone(), Some(run_keystroke.clone()), run_button.clone(), accept_callback, @@ -318,7 +317,7 @@ pub(super) fn render_header_buttons( size_constrained_column.add_child( Container::new(render_keyboard_shortcut_button( - REQUESTED_ACTION_RUN_LABEL, + run_label, Some(run_keystroke.clone()), run_button.clone(), accept_clone, @@ -484,7 +483,7 @@ fn render_requested_action_row_for_element( } pub fn render_keyboard_shortcut_button( - label: &'static str, + label: String, keystroke: Option, mouse_state: MouseStateHandle, on_click: Rc, @@ -493,7 +492,7 @@ pub fn render_keyboard_shortcut_button( ) -> Box { let appearance = Appearance::as_ref(app); let theme = appearance.theme(); - Hoverable::new(mouse_state, |mouse_state| { + Hoverable::new(mouse_state, move |mouse_state| { let text_color = if mouse_state.is_hovered() { blended_colors::accent(theme).into_solid() } else { @@ -527,7 +526,7 @@ pub fn render_keyboard_shortcut_button( } row.with_child( Text::new_inline( - label, + label.clone(), appearance.ui_font_family(), shortcut_styles .font_size diff --git a/app/src/ai/blocklist/inline_action/requested_command.rs b/app/src/ai/blocklist/inline_action/requested_command.rs index ca1e3ad661..b20f1d2b19 100644 --- a/app/src/ai/blocklist/inline_action/requested_command.rs +++ b/app/src/ai/blocklist/inline_action/requested_command.rs @@ -69,23 +69,6 @@ use crate::view_components::compactible_split_action_button::CompactibleSplitAct /// For horizontal padding, use [`INLINE_ACTION_HORIZONTAL_PADDING`] for consistency. pub const REQUESTED_COMMAND_BODY_VERTICAL_PADDING: f32 = 16.; -const REQUESTED_COMMAND_REJECT_LABEL: &str = "Reject"; -const REQUESTED_COMMAND_ACCEPT_LABEL: &str = "Run"; -const REQUESTED_COMMAND_EDIT_LABEL: &str = "Edit"; -const REQUESTED_COMMAND_MINIMIZE_LABEL: &str = "Done"; - -const LOADING_MESSAGE: &str = "Generating command..."; -const COMMAND_WAITING_FOR_USER_MESSAGE: &str = "OK if I run this command and read the output?"; -const MCP_TOOL_WAITING_FOR_USER_MESSAGE: &str = "OK if I call this MCP tool?"; -const MONITORING_COMMAND_MESSAGE: &str = "Agent is monitoring command..."; -const AGENT_NEEDS_INPUT_MESSAGE: &str = "Agent needs your input to continue"; -const USER_TOOK_CONTROL_COMMAND_MESSAGE: &str = "User is in control."; -const USER_STOPPED_CLI_SUBAGENT_COMMAND_MESSAGE: &str = "Paused agent. User is in control."; -const AGENT_REQUESTED_USER_TAKE_CONTROL_COMMAND_MESSAGE: &str = "User in control"; -const AGENT_ERRORED_COMMAND_MESSAGE: &str = "Agent ran into an issue. Take over control."; -pub const VIEWING_COMMAND_DETAIL_MESSAGE: &str = "Viewing command detail"; -const VIEWING_MCP_TOOL_DETAIL_MESSAGE: &str = "Viewing MCP tool call detail"; - const EDIT_COMMAND_ACTION_NAME: &str = "requested_command:edit"; const EDIT_MODE_OPEN_KEYMAP_CONTEXT: &str = "RequestedCommandViewEditModeOpen"; @@ -150,7 +133,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( EDIT_COMMAND_ACTION_NAME, - "Edit requested command", + i18n::t("ai.requested_command.edit_requested_command"), RequestedCommandViewAction::OpenEditMode, ) .with_key_binding(cmd_or_ctrl_shift("e")) @@ -261,7 +244,7 @@ impl RequestedCommandView { ctx: &mut ViewContext, ) -> Self { let cancel_button = CompactibleActionButton::new( - REQUESTED_COMMAND_REJECT_LABEL.to_string(), + i18n::t("common.reject"), Some(KeystrokeSource::Fixed( CANCEL_REQUESTED_COMMAND_KEYSTROKE.clone(), )), @@ -274,7 +257,7 @@ impl RequestedCommandView { let position_id_prefix = format!("{action_id:?}"); let accept_and_autoexecute_split_button = CompactibleSplitActionButton::new( - REQUESTED_COMMAND_ACCEPT_LABEL.to_string(), + i18n::t("common.run"), Some(KeystrokeSource::Fixed( ENTER_ACCEPT_REQUESTED_COMMAND_KEYSTROKE.clone(), )), @@ -290,7 +273,7 @@ impl RequestedCommandView { ); let edit_button = CompactibleActionButton::new( - REQUESTED_COMMAND_EDIT_LABEL.to_string(), + i18n::t("common.edit"), Some(KeystrokeSource::Binding(EDIT_COMMAND_ACTION_NAME)), ButtonSize::InlineActionHeader, RequestedCommandViewAction::OpenEditMode, @@ -300,7 +283,7 @@ impl RequestedCommandView { ); let minimize_button = CompactibleActionButton::new( - REQUESTED_COMMAND_MINIMIZE_LABEL.to_string(), + i18n::t("common.done"), Some(KeystrokeSource::Fixed( MINIMIZE_REQUESTED_COMMAND_KEYSTROKE.clone(), )), @@ -594,16 +577,15 @@ impl RequestedCommandView { .map(|k| k.displayed()) .unwrap_or_default(); - let accept_item = MenuItemFields::new_with_label( - REQUESTED_COMMAND_ACCEPT_LABEL, - accept_keystroke.as_str(), - ) - .with_on_select_action(RequestedCommandViewAction::Accept) - .into_item(); + let accept_item = + MenuItemFields::new_with_label(i18n::t("common.run"), accept_keystroke) + .with_on_select_action(RequestedCommandViewAction::Accept) + .into_item(); - let auto_item = MenuItemFields::new_with_label("Auto-approve", auto_keystroke.as_str()) - .with_on_select_action(RequestedCommandViewAction::AcceptAndAutoExecute) - .into_item(); + let auto_item = + MenuItemFields::new_with_label(i18n::t("ai.command.auto_approve"), auto_keystroke) + .with_on_select_action(RequestedCommandViewAction::AcceptAndAutoExecute) + .into_item(); self.accept_split_button_menu.update(ctx, |menu, ctx| { menu.set_items(vec![accept_item, auto_item], ctx); @@ -651,7 +633,7 @@ impl RequestedCommandView { citations_padding, app, ) - .map(|citation| ("Copied from", citation)) + .map(|citation| (i18n::t("ai.requested_command.copied_from"), citation)) } else { // Otherwise, we render all the citations (if any) and mention that the command was derived from them. render_citation_chips( @@ -661,7 +643,7 @@ impl RequestedCommandView { citations_padding, app, ) - .map(|citations| ("Derived from", citations)) + .map(|citations| (i18n::t("ai.requested_command.derived_from"), citations)) }; let citations_footer = citations_footer_props.map(|(prefix, suffix)| { @@ -702,7 +684,7 @@ impl RequestedCommandView { ) if show_for_action_id == &self.action_id => { *shown.lock() = true; Some(render_autonomy_checkbox_setting_speedbump_footer( - "Always allow Oz to execute read-only commands (relies on model)", + i18n::t("ai.output.always_allow_oz_read_only_commands"), *checked, AIBlockAction::ToggleAutoexecuteReadonlyCommandsSpeedbumpCheckbox, self.autoexecute_readonly_commands_speedbump_checkbox_handle @@ -759,7 +741,7 @@ impl RequestedCommandView { ) .with_child( Text::new( - "Your profile is set to always ask for permission to execute commands.", + i18n::t("ai.requested_command.always_ask_permission"), appearance.ui_font_family(), font_size, ) @@ -774,7 +756,8 @@ impl RequestedCommandView { appearance .ui_builder() .link( - "Manage command execution setting".into(), + i18n::t("ai.requested_command.manage_command_execution_setting") + .into(), None, Some(Box::new(move |ctx| { ctx.dispatch_typed_action( @@ -1029,8 +1012,12 @@ impl RequestedCommandView { } Some(AIActionStatus::Blocked) => { title = match &self.action_type { - RequestedActionViewType::Command => COMMAND_WAITING_FOR_USER_MESSAGE.into(), - RequestedActionViewType::McpTool => MCP_TOOL_WAITING_FOR_USER_MESSAGE.into(), + RequestedActionViewType::Command => { + i18n::t("ai.requested_command.ok_run_command").into() + } + RequestedActionViewType::McpTool => { + i18n::t("ai.requested_command.ok_call_mcp_tool").into() + } }; } Some(AIActionStatus::RunningAsync) | Some(AIActionStatus::Finished(..)) @@ -1050,11 +1037,12 @@ impl RequestedCommandView { ); if is_errored { - AGENT_ERRORED_COMMAND_MESSAGE.into() + i18n::t("ai.requested_command.agent_error_take_over").into() } else if *is_blocked { - AGENT_NEEDS_INPUT_MESSAGE.into() + i18n::t("ai.requested_command.agent_needs_input").into() } else { - MONITORING_COMMAND_MESSAGE.into() + i18n::t("ai.requested_command.agent_monitoring_command") + .into() } } LongRunningCommandControlState::User { reason } => { @@ -1062,15 +1050,17 @@ impl RequestedCommandView { } } } else { - VIEWING_COMMAND_DETAIL_MESSAGE.into() + i18n::t("ai.requested_command.viewing_command_detail").into() } } - RequestedActionViewType::McpTool => VIEWING_MCP_TOOL_DETAIL_MESSAGE.into(), + RequestedActionViewType::McpTool => { + i18n::t("ai.requested_command.viewing_mcp_tool_detail").into() + } }; } None => { if self.block_model.status(app).is_streaming() { - title = LOADING_MESSAGE.into(); + title = i18n::t("ai.requested_command.generating_command").into(); if !self .block_model @@ -1091,7 +1081,7 @@ impl RequestedCommandView { // mid-flight. let title_str = self.get_header_title_text(); title = if title_str.trim().is_empty() { - LOADING_MESSAGE.into() + i18n::t("ai.requested_command.generating_command").into() } else { title_str.into() }; @@ -1110,7 +1100,7 @@ impl RequestedCommandView { // Show cancelled command loading message when the command was cancelled during generation, // and then restored with an empty title as a result. if title.is_empty() { - title = LOADING_MESSAGE.into(); + title = i18n::t("ai.requested_command.generating_command").into(); font_color_override = Some(blended_colors::text_disabled( appearance.theme(), appearance.theme().surface_2(), @@ -1313,14 +1303,12 @@ impl RequestedCommandView { } } -pub(crate) fn header_message_for_user_take_over_reason( - reason: &UserTakeOverReason, -) -> &'static str { +pub(crate) fn header_message_for_user_take_over_reason(reason: &UserTakeOverReason) -> String { match reason { - UserTakeOverReason::Manual => USER_TOOK_CONTROL_COMMAND_MESSAGE, - UserTakeOverReason::Stop => USER_STOPPED_CLI_SUBAGENT_COMMAND_MESSAGE, + UserTakeOverReason::Manual => i18n::t("ai.requested_command.user_in_control"), + UserTakeOverReason::Stop => i18n::t("ai.requested_command.paused_agent_user_in_control"), UserTakeOverReason::TransferFromAgent { .. } => { - AGENT_REQUESTED_USER_TAKE_CONTROL_COMMAND_MESSAGE + i18n::t("ai.requested_command.user_in_control_short") } } } @@ -1437,13 +1425,17 @@ impl View for RequestedCommandView { // If we have a result, show the JSON response. let result_text = match result { CallMCPToolResult::Success { result } => serde_json::to_string_pretty(result) - .unwrap_or_else(|_| "Error formatting JSON".to_string()), + .unwrap_or_else(|_| i18n::t("ai.requested_command.error_formatting_json")), CallMCPToolResult::Error(error) => { - format!("Error: {error}") + i18n::t("ai.requested_command.error_prefix").replace("{error}", error) + } + CallMCPToolResult::Cancelled => { + i18n::t("ai.requested_command.tool_call_cancelled") } - CallMCPToolResult::Cancelled => "Tool call was cancelled".to_string(), }; - format!("{command_text}\n\nResponse: {result_text}") + i18n::t("ai.requested_command.response_with_result") + .replace("{command}", command_text) + .replace("{result}", &result_text) } else if self.is_header_expanded { command_text.to_string() } else { diff --git a/app/src/ai/blocklist/inline_action/run_agents_card_view.rs b/app/src/ai/blocklist/inline_action/run_agents_card_view.rs index 48e51fa78c..bbf440df44 100644 --- a/app/src/ai/blocklist/inline_action/run_agents_card_view.rs +++ b/app/src/ai/blocklist/inline_action/run_agents_card_view.rs @@ -70,8 +70,6 @@ use crate::view_components::compactible_split_action_button::CompactibleSplitAct use crate::view_components::dropdown::DropdownEvent; use crate::view_components::{FilterableDropdownEvent, FilterableDropdownOrientation}; -const RUN_AGENTS_CARD_TITLE: &str = "Can I start additional agents for this task?"; - pub fn init(app: &mut AppContext) { use warpui::keymap::macros::*; @@ -312,7 +310,7 @@ impl RunAgentsCardView { let accept_keystroke = ENTER_KEYSTROKE.clone(); let reject_button = CompactibleActionButton::new( - "Reject".to_string(), + i18n::t("common.reject"), Some(KeystrokeSource::Fixed(reject_keystroke)), ButtonSize::Small, RunAgentsCardViewAction::Reject, @@ -322,7 +320,7 @@ impl RunAgentsCardView { ); let position_id_prefix = format!("{action_id:?}"); let accept_button = CompactibleSplitActionButton::new( - "Accept".to_string(), + i18n::t("ai.command.accept"), Some(KeystrokeSource::Fixed(accept_keystroke)), ButtonSize::Small, RunAgentsCardViewAction::Accept, @@ -711,7 +709,7 @@ impl RunAgentsCardView { }; accept.set_disabled(reason.is_some(), ctx); // Tooltip explains why the button is disabled; falls back to "Accept". - accept.set_tooltip(reason.or_else(|| Some("Accept".to_string())), ctx); + accept.set_tooltip(reason.or_else(|| Some(i18n::t("common.accept"))), ctx); self.handles.accept_button = Some(accept); } @@ -922,9 +920,12 @@ impl RunAgentsCardView { fn toggle_accept_menu(&mut self, ctx: &mut ViewContext) { self.is_accept_menu_open = !self.is_accept_menu_open; if self.is_accept_menu_open { - let item = MenuItemFields::new_with_label("Accept w/o orchestration", "") - .with_on_select_action(RunAgentsCardViewAction::AcceptWithoutOrchestration) - .into_item(); + let item = MenuItemFields::new_with_label( + i18n::t("ai.run_agents.accept_without_orchestration"), + String::new(), + ) + .with_on_select_action(RunAgentsCardViewAction::AcceptWithoutOrchestration) + .into_item(); self.accept_menu.update(ctx, |menu, ctx| { menu.set_items(vec![item], ctx); }); @@ -984,7 +985,7 @@ impl View for RunAgentsCardView { // because restored blocks have no pending action status. if self.block_model.is_restored() { return render_status_only_card( - "Spawn agents cancelled".to_string(), + i18n::t("ai.run_agents.cancelled"), appearance, StatusKind::Cancelled, app, @@ -996,7 +997,7 @@ impl View for RunAgentsCardView { // and the action is queued for user confirmation). if !matches!(status, Some(AIActionStatus::Blocked)) { return render_status_only_card( - "Configuring agents\u{2026}".to_string(), + i18n::t("ai.run_agents.configuring"), appearance, StatusKind::Spawning, app, @@ -1262,7 +1263,7 @@ fn render_confirmation_card( fn render_header(handles: &RunAgentsCardHandles, app: &AppContext) -> Box { let appearance = Appearance::as_ref(app); - let mut config = HeaderConfig::new(RUN_AGENTS_CARD_TITLE, app) + let mut config = HeaderConfig::new(i18n::t("ai.run_agents_card.title"), app) .with_icon(icons::yellow_stop_icon(appearance)) .with_corner_radius_override(CornerRadius::with_top(Radius::Pixels(8.))); @@ -1325,7 +1326,8 @@ fn render_agents_section(state: &RunAgentsEditState, app: &AppContext) -> Box (String, Status .count(); let label = if launched == total { if total == 1 { - "Spawned 1 agent".to_string() + i18n::t("ai.run_agents.spawned_one") } else { - format!("Spawned {total} agents") + i18n::t("ai.run_agents.spawned_other").replace("{count}", &total.to_string()) } } else { - format!("Spawned {launched} of {total} agents") + i18n::t("ai.run_agents.spawned_partial") + .replace("{launched}", &launched.to_string()) + .replace("{total}", &total.to_string()) }; let kind = if launched == total { StatusKind::Success @@ -1382,24 +1386,22 @@ pub(crate) fn format_terminal_state(result: &RunAgentsResult) -> (String, Status } RunAgentsResult::Denied { reason } => { let body = if reason.is_empty() { - "Orchestration is currently disabled. Re-enable on the plan card to launch." - .to_string() + i18n::t("ai.run_agents.orchestration_disabled") } else { - format!( - "Orchestration is currently disabled. Re-enable on the plan card to launch. ({reason})" - ) + i18n::t("ai.run_agents.orchestration_disabled_reason").replace("{reason}", reason) }; (body, StatusKind::Cancelled) } RunAgentsResult::Failure { error } => { let label = if error.is_empty() { - "Failed to start orchestration".to_string() + i18n::t("ai.run_agents.failed_to_start_orchestration") } else { - format!("Failed to start orchestration: {error}") + i18n::t("ai.run_agents.failed_to_start_orchestration_with_error") + .replace("{error}", error) }; (label, StatusKind::Failure) } - RunAgentsResult::Cancelled => ("Spawn agents cancelled".to_string(), StatusKind::Cancelled), + RunAgentsResult::Cancelled => (i18n::t("ai.run_agents.cancelled"), StatusKind::Cancelled), } } @@ -1419,9 +1421,9 @@ fn render_spawning_card( ) -> Box { let total = snapshot.agent_count; let label = if total == 1 { - "Spawning 1 agent\u{2026}".to_string() + i18n::t("ai.run_agents.spawning_one") } else { - format!("Spawning {total} agents\u{2026}") + i18n::t("ai.run_agents.spawning_other").replace("{count}", &total.to_string()) }; render_status_only_card(label, appearance, StatusKind::Spawning, app) } diff --git a/app/src/ai/blocklist/inline_action/search_codebase.rs b/app/src/ai/blocklist/inline_action/search_codebase.rs index a9baab31aa..33ca85f365 100644 --- a/app/src/ai/blocklist/inline_action/search_codebase.rs +++ b/app/src/ai/blocklist/inline_action/search_codebase.rs @@ -155,9 +155,11 @@ impl SearchCodebaseView { app: &AppContext, ) -> Box { let title_text = if let Some(repo_name) = &self.repo_name { - format!("Searched for \"{}\" in {}", self.search_query, repo_name) + i18n::t("ai.search_codebase.searched_for_in_repo") + .replace("{query}", &self.search_query) + .replace("{repo}", repo_name) } else { - format!("Searched for \"{}\"", self.search_query) + i18n::t("ai.search_codebase.searched_for").replace("{query}", &self.search_query) }; let body = if self.collapsible.is_expanded { @@ -229,7 +231,11 @@ impl SearchCodebaseView { font_size: Some(appearance.monospace_font_size()), ..Default::default() }; - self.render_formatted_text("No results found".to_string(), no_results_style, appearance) + self.render_formatted_text( + i18n::t("search.no_results_found"), + no_results_style, + appearance, + ) } else { render_read_files_text( render_read_file_args, @@ -461,9 +467,12 @@ impl View for SearchCodebaseView { | AIActionStatus::RunningAsync, ) => { let loading_text = if let Some(repo_name) = &self.repo_name { - format!("Searching for \"{}\" in {}", self.search_query, repo_name) + i18n::t("ai.search_codebase.searching_for_in_repo") + .replace("{query}", &self.search_query) + .replace("{repo}", repo_name) } else { - format!("Searching codebase for \"{}\"", self.search_query) + i18n::t("ai.search_codebase.searching_for") + .replace("{query}", &self.search_query) }; let loading_icon = yellow_running_icon(appearance); self.render_header(appearance, loading_text, loading_icon, app) @@ -472,12 +481,11 @@ impl View for SearchCodebaseView { } Some(AIActionStatus::Finished(result)) if result.result.is_cancelled() => { let cancelled_text = if let Some(repo_name) = &self.repo_name { - format!( - "Search for \"{}\" in {} cancelled", - self.search_query, repo_name - ) + i18n::t("ai.search_codebase.cancelled_in_repo") + .replace("{query}", &self.search_query) + .replace("{repo}", repo_name) } else { - format!("Search for \"{}\" cancelled", self.search_query) + i18n::t("ai.search_codebase.cancelled").replace("{query}", &self.search_query) }; let cancelled_icon = cancelled_icon(appearance); self.render_header(appearance, cancelled_text, cancelled_icon, app) @@ -490,12 +498,12 @@ impl View for SearchCodebaseView { .finish(), _ => { let text = if let Some(repo_name) = &self.repo_name { - format!( - "Searched codebase for \"{}\" in {}", - self.search_query, repo_name - ) + i18n::t("ai.search_codebase.searched_codebase_for_in_repo") + .replace("{query}", &self.search_query) + .replace("{repo}", repo_name) } else { - format!("Searched codebase for \"{}\"", self.search_query) + i18n::t("ai.search_codebase.searched_codebase_for") + .replace("{query}", &self.search_query) }; self.render_simple_header(text, app) .with_agent_output_item_spacing(app) diff --git a/app/src/ai/blocklist/inline_action/web_fetch.rs b/app/src/ai/blocklist/inline_action/web_fetch.rs index 00eb873360..3f14e1da78 100644 --- a/app/src/ai/blocklist/inline_action/web_fetch.rs +++ b/app/src/ai/blocklist/inline_action/web_fetch.rs @@ -37,7 +37,7 @@ impl WebFetchView { let appearance = Appearance::as_ref(app); let loading_icon = yellow_running_icon(appearance); - let text = format!("Fetching {} web pages...", urls.len()); + let text = i18n::t("ai.web_fetch.fetching").replace("{count}", &urls.len().to_string()); super::search_results_common::render_loading_header(text, loading_icon, app) } @@ -49,9 +49,11 @@ impl WebFetchView { ) -> Box { let successful_count = pages.iter().filter(|(_, _, success)| *success).count(); let title_text = if successful_count == pages.len() { - format!("Fetched {} web pages", pages.len()) + i18n::t("ai.web_fetch.fetched_all").replace("{count}", &pages.len().to_string()) } else { - format!("Fetched {} of {} web pages", successful_count, pages.len()) + i18n::t("ai.web_fetch.fetched_partial") + .replace("{successful}", &successful_count.to_string()) + .replace("{total}", &pages.len().to_string()) }; let body = if self.collapsible.is_expanded { @@ -63,7 +65,7 @@ impl WebFetchView { render_collapsible_search_results( title_text, pages.len(), - "URLs", + &i18n::t("ai.search_results.urls"), &self.collapsible, body, |ctx| { @@ -118,7 +120,7 @@ impl WebFetchView { if pages.is_empty() { let no_results = Text::new_inline( - "No URLs fetched".to_string(), + i18n::t("ai.web_fetch.no_urls_fetched"), appearance.ui_font_family(), appearance.monospace_font_size(), ) diff --git a/app/src/ai/blocklist/inline_action/web_search.rs b/app/src/ai/blocklist/inline_action/web_search.rs index 2d6723426e..0420d914cf 100644 --- a/app/src/ai/blocklist/inline_action/web_search.rs +++ b/app/src/ai/blocklist/inline_action/web_search.rs @@ -40,9 +40,9 @@ impl WebSearchView { let loading_icon = yellow_running_icon(appearance); let text = if let Some(q) = query { - format!("Searching the web for \"{q}\"") + i18n::t("ai.web_search.searching_for").replace("{query}", q) } else { - "Searching the web".to_string() + i18n::t("ai.web_search.searching") }; super::search_results_common::render_loading_header(text, loading_icon, app) @@ -55,9 +55,9 @@ impl WebSearchView { app: &AppContext, ) -> Box { let title_text = if query.is_empty() { - "Searched the web".to_string() + i18n::t("ai.web_search.searched") } else { - format!("Searched the web for \"{query}\"") + i18n::t("ai.web_search.searched_for").replace("{query}", query) }; let body = if self.collapsible.is_expanded { @@ -69,7 +69,7 @@ impl WebSearchView { render_collapsible_search_results( title_text, pages.len(), - "URLs", + &i18n::t("ai.search_results.urls"), &self.collapsible, body, |ctx| { @@ -108,7 +108,7 @@ impl WebSearchView { if pages.is_empty() { let no_results = Text::new_inline( - "No URLs found".to_string(), + i18n::t("ai.web_search.no_urls_found"), appearance.ui_font_family(), appearance.monospace_font_size(), ) diff --git a/app/src/ai/blocklist/local_agent_task_sync_model.rs b/app/src/ai/blocklist/local_agent_task_sync_model.rs index 67083f5615..4c048b51e8 100644 --- a/app/src/ai/blocklist/local_agent_task_sync_model.rs +++ b/app/src/ai/blocklist/local_agent_task_sync_model.rs @@ -337,19 +337,24 @@ fn map_conversation_status( Some(error) => classify_renderable_error(error), None => ( AgentTaskState::Error, - Some(TaskStatusUpdate::message("Agent encountered an error")), + Some(TaskStatusUpdate::message(i18n::t( + "ai.blocklist.local_agent_task_sync_model.agent_error", + ))), ), } } ConversationStatus::Cancelled => ( AgentTaskState::Cancelled, - Some(TaskStatusUpdate::message("Cancelled by user")), + Some(TaskStatusUpdate::message(i18n::t( + "ai.blocklist.local_agent_task_sync_model.cancelled_by_user", + ))), ), ConversationStatus::Blocked { blocked_action } => ( AgentTaskState::Blocked, - Some(TaskStatusUpdate::message(format!( - "The agent got stuck waiting for user confirmation on the action: {blocked_action}" - ))), + Some(TaskStatusUpdate::message( + i18n::t("ai.blocklist.local_agent_task_sync_model.conversation_blocked") + .replace("{blocked_action}", blocked_action), + )), ), } } @@ -365,44 +370,47 @@ pub(crate) fn classify_renderable_error( } => ( AgentTaskState::Failed, Some(TaskStatusUpdate::with_error_code( - user_display_message.as_deref().unwrap_or( - "Your team has run out of credits. Purchase more credits to continue.", - ), + user_display_message.clone().unwrap_or_else(|| { + i18n::t("ai.blocklist.local_agent_task_sync_model.quota_limit") + }), PlatformErrorCode::InsufficientCredits, )), ), RenderableAIError::ServerOverloaded => ( AgentTaskState::Error, Some(TaskStatusUpdate::with_error_code( - "Warp is temporarily overloaded. Please try again shortly.", + i18n::t("ai.blocklist.local_agent_task_sync_model.server_overloaded"), PlatformErrorCode::ResourceUnavailable, )), ), RenderableAIError::InternalWarpError => ( AgentTaskState::Error, Some(TaskStatusUpdate::with_error_code( - "An internal error occurred during the conversation. Please try again.", + i18n::t("ai.blocklist.local_agent_task_sync_model.internal_warp_error"), PlatformErrorCode::InternalError, )), ), RenderableAIError::ContextWindowExceeded(msg) => ( AgentTaskState::Failed, Some(TaskStatusUpdate::with_error_code( - format!("Context window exceeded: {msg}"), + i18n::t("ai.blocklist.local_agent_task_sync_model.context_window_exceeded") + .replace("{message}", msg), PlatformErrorCode::InternalError, )), ), RenderableAIError::InvalidApiKey { provider, .. } => ( AgentTaskState::Failed, Some(TaskStatusUpdate::with_error_code( - format!("Invalid API key for {provider}. Update your API key in settings."), + i18n::t("ai.blocklist.local_agent_task_sync_model.invalid_api_key") + .replace("{provider}", provider), PlatformErrorCode::AuthenticationRequired, )), ), RenderableAIError::AwsBedrockCredentialsExpiredOrInvalid { model_name } => ( AgentTaskState::Failed, Some(TaskStatusUpdate::with_error_code( - format!("AWS Bedrock credentials expired or invalid for {model_name}."), + i18n::t("ai.blocklist.local_agent_task_sync_model.aws_bedrock_invalid") + .replace("{model_name}", model_name), PlatformErrorCode::AuthenticationRequired, )), ), diff --git a/app/src/ai/blocklist/local_agent_task_sync_model_tests.rs b/app/src/ai/blocklist/local_agent_task_sync_model_tests.rs index a7980cc53a..2cb8fd43f4 100644 --- a/app/src/ai/blocklist/local_agent_task_sync_model_tests.rs +++ b/app/src/ai/blocklist/local_agent_task_sync_model_tests.rs @@ -14,6 +14,13 @@ use crate::ai::ambient_agents::AmbientAgentTaskId; use crate::server::server_api::ai::{AIClient, MockAIClient, TaskStatusUpdate}; use crate::terminal::cli_agent_sessions::{CLIAgentSessionStatus, CLIAgentSessionsModel}; +fn classify_renderable_error_en( + error: &RenderableAIError, +) -> (AgentTaskState, Option) { + i18n::set_locale("en"); + classify_renderable_error(error) +} + /// Helper to assert a (state, Option) tuple. fn assert_update( (state, update): (AgentTaskState, Option), @@ -44,7 +51,7 @@ fn assert_update( #[test] fn quota_limit_is_failed_with_insufficient_credits() { assert_update( - classify_renderable_error(&RenderableAIError::QuotaLimit { + classify_renderable_error_en(&RenderableAIError::QuotaLimit { user_display_message: None, }), AgentTaskState::Failed, @@ -56,7 +63,7 @@ fn quota_limit_is_failed_with_insufficient_credits() { #[test] fn server_overloaded_is_error_with_resource_unavailable() { assert_update( - classify_renderable_error(&RenderableAIError::ServerOverloaded), + classify_renderable_error_en(&RenderableAIError::ServerOverloaded), AgentTaskState::Error, Some(PlatformErrorCode::ResourceUnavailable), Some("overloaded"), @@ -66,7 +73,7 @@ fn server_overloaded_is_error_with_resource_unavailable() { #[test] fn internal_warp_error_is_error() { assert_update( - classify_renderable_error(&RenderableAIError::InternalWarpError), + classify_renderable_error_en(&RenderableAIError::InternalWarpError), AgentTaskState::Error, Some(PlatformErrorCode::InternalError), Some("internal error"), @@ -76,7 +83,7 @@ fn internal_warp_error_is_error() { #[test] fn context_window_exceeded_is_failed() { assert_update( - classify_renderable_error(&RenderableAIError::ContextWindowExceeded("too big".into())), + classify_renderable_error_en(&RenderableAIError::ContextWindowExceeded("too big".into())), AgentTaskState::Failed, Some(PlatformErrorCode::InternalError), Some("Context window exceeded"), @@ -86,7 +93,7 @@ fn context_window_exceeded_is_failed() { #[test] fn invalid_api_key_is_failed_with_auth_required() { assert_update( - classify_renderable_error(&RenderableAIError::InvalidApiKey { + classify_renderable_error_en(&RenderableAIError::InvalidApiKey { provider: "OpenAI".into(), model_name: "gpt-4".into(), }), @@ -99,7 +106,7 @@ fn invalid_api_key_is_failed_with_auth_required() { #[test] fn aws_bedrock_credentials_is_failed_with_auth_required() { assert_update( - classify_renderable_error(&RenderableAIError::AwsBedrockCredentialsExpiredOrInvalid { + classify_renderable_error_en(&RenderableAIError::AwsBedrockCredentialsExpiredOrInvalid { model_name: "claude-v2".into(), }), AgentTaskState::Failed, @@ -111,7 +118,7 @@ fn aws_bedrock_credentials_is_failed_with_auth_required() { #[test] fn other_error_is_error_with_internal() { assert_update( - classify_renderable_error(&RenderableAIError::Other { + classify_renderable_error_en(&RenderableAIError::Other { error_message: "something broke".into(), will_attempt_resume: false, waiting_for_network: false, diff --git a/app/src/ai/blocklist/mod.rs b/app/src/ai/blocklist/mod.rs index 968dd77ae2..c70f7dcafc 100644 --- a/app/src/ai/blocklist/mod.rs +++ b/app/src/ai/blocklist/mod.rs @@ -77,10 +77,9 @@ pub(crate) use queued_query::{ pub use suggestion_chip_view::*; pub use view_util::error_color; pub(crate) use view_util::{ - ai_brand_color, ai_indicator_height, format_credits, + ai_brand_color, ai_indicator_height, attach_as_agent_mode_context_text, format_credits, get_ai_block_overflow_menu_element_position_id, get_attached_blocks_chip_element_position_id, - render_ai_agent_mode_icon, render_ai_follow_up_icon, ATTACH_AS_AGENT_MODE_CONTEXT_TEXT, - CLAUDE_ORANGE, NEW_AGENT_PANE_LABEL, + render_ai_agent_mode_icon, render_ai_follow_up_icon, CLAUDE_ORANGE, NEW_AGENT_PANE_LABEL, }; pub use crate::ai::blocklist::block::{secret_redaction, AIBlockResponseRating, TextLocation}; diff --git a/app/src/ai/blocklist/prompt/plan_and_todo_list.rs b/app/src/ai/blocklist/prompt/plan_and_todo_list.rs index 63f6ac2b43..43a05661b5 100644 --- a/app/src/ai/blocklist/prompt/plan_and_todo_list.rs +++ b/app/src/ai/blocklist/prompt/plan_and_todo_list.rs @@ -254,9 +254,9 @@ impl PlanAndTodoListView { chip_content.finish(), self.plan_button_mouse_state.clone(), if is_agent_unaware_of_plan_edits { - "Agent is unaware of recent plan edits".to_string() + i18n::t("context_chips.plan.agent_unaware_of_recent_edits") } else { - "View plan".to_string() + i18n::t("context_chips.plan.view_plan") }, corner_radius, appearance, @@ -411,7 +411,7 @@ impl PlanAndTodoListView { .render_chip_button( content, self.todo_button_mouse_state.clone(), - "View todo list".to_string(), + i18n::t("context_chips.todo.view_todo_list"), corner_radius, appearance, ) diff --git a/app/src/ai/blocklist/prompt/prompt_alert.rs b/app/src/ai/blocklist/prompt/prompt_alert.rs index a3cb6e84dd..2d693cad25 100644 --- a/app/src/ai/blocklist/prompt/prompt_alert.rs +++ b/app/src/ai/blocklist/prompt/prompt_alert.rs @@ -20,28 +20,6 @@ use crate::workspaces::user_workspaces::UserWorkspaces; const ANONYMOUS_USER_REQUEST_LIMIT_SOFT_GATE_PERCENTAGE: f32 = 0.5; -const TELEMETRY_DISABLED_PRIMARY_TEXT: &str = "To use AI features,"; -const ENABLE_ANALYTICS_ACTION_TEXT: &str = "enable analytics"; -const UPGRADE_TO_BUILD_ACTION_TEXT: &str = "upgrade"; - -const NO_CONNECTION_PRIMARY_TEXT: &str = "No internet connection"; -const ANONYMOUS_USER_REQUEST_LIMIT_SOFT_GATE_PRIMARY_TEXT: &str = ""; -const ANONYMOUS_USER_REQUEST_LIMIT_HARD_GATE_PRIMARY_TEXT: &str = "At Limit -"; -const DELINQUENT_DUE_TO_PAYMENT_ISSUE_PRIMARY_TEXT: &str = "Restricted due to payment issue"; -const OUT_OF_REQUESTS_PRIMARY_TEXT: &str = "Out of credits"; - -const ANONYMOUS_USER_REQUEST_LIMIT_ACTION_TEXT: &str = "Sign up for more AI credits"; -const DELINQUENT_DUE_TO_PAYMENT_ISSUE_ACTION_TEXT: &str = "Manage billing"; -const OVERAGES_TOGGLEABLE_BUT_NOT_ENABLED_ACTION_TEXT: &str = "Enable premium overages"; -const MONTHLY_OVERAGES_SPEND_LIMIT_REACHED_ACTION_TEXT: &str = "Increase monthly spend limit"; -const UPGRADE_TEXT: &str = "Upgrade"; -const COMPARE_PLANS_TEXT: &str = "Compare plans"; -const CONTACT_SUPPORT_TEXT: &str = "Contact support"; -const NON_ADMIN_CONTACT_ADMIN_TEXT: &str = ", contact a team admin"; -const NON_ADMIN_ASK_ADMIN_TO_ENABLE_OVERAGES_TEXT: &str = ", ask a team admin to enable overages"; -const NON_ADMIN_ASK_ADMIN_TO_INCREASE_OVERAGES_TEXT: &str = - ", ask a team admin to increase overages"; - #[derive(Debug, Clone, PartialEq, Eq)] pub enum PromptAlertAction { SignUpClickedForAnonymousUser, @@ -222,36 +200,34 @@ impl PromptAlertView { text_fragments.push(FormattedTextFragment::plain_text(" ")); match state { PromptAlertState::NoConnection => { - text_fragments.push(FormattedTextFragment::plain_text( - NO_CONNECTION_PRIMARY_TEXT, - )); + text_fragments.push(FormattedTextFragment::plain_text(i18n::t( + "ai.prompt_alert.no_connection", + ))); } PromptAlertState::TelemetryDisabledOnFreeTier => { - text_fragments.push(FormattedTextFragment::plain_text( - TELEMETRY_DISABLED_PRIMARY_TEXT, - )); + text_fragments.push(FormattedTextFragment::plain_text(i18n::t( + "ai.prompt_alert.telemetry_disabled_primary", + ))); } PromptAlertState::AnonymousUserRequestLimitSoftGate => { - text_fragments.push(FormattedTextFragment::plain_text( - ANONYMOUS_USER_REQUEST_LIMIT_SOFT_GATE_PRIMARY_TEXT, - )); + text_fragments.push(FormattedTextFragment::plain_text(String::new())); } PromptAlertState::AnonymousUserRequestLimitHardGate => { - text_fragments.push(FormattedTextFragment::plain_text( - ANONYMOUS_USER_REQUEST_LIMIT_HARD_GATE_PRIMARY_TEXT, - )); + text_fragments.push(FormattedTextFragment::plain_text(i18n::t( + "ai.prompt_alert.anonymous_limit_hard_gate_primary", + ))); } PromptAlertState::DelinquentDueToPaymentIssue => { - text_fragments.push(FormattedTextFragment::plain_text( - DELINQUENT_DUE_TO_PAYMENT_ISSUE_PRIMARY_TEXT, - )); + text_fragments.push(FormattedTextFragment::plain_text(i18n::t( + "ai.prompt_alert.delinquent_primary", + ))); } PromptAlertState::OveragesToggleableButNotEnabled | PromptAlertState::MonthlyOveragesSpendLimitReached | PromptAlertState::RequestLimitReached => { - text_fragments.push(FormattedTextFragment::plain_text( - OUT_OF_REQUESTS_PRIMARY_TEXT, - )); + text_fragments.push(FormattedTextFragment::plain_text(i18n::t( + "ai.prompt_alert.out_of_credits", + ))); } PromptAlertState::NoAlert => {} } @@ -275,12 +251,12 @@ impl PromptAlertView { // Show "enable analytics" action link text_fragments.push(FormattedTextFragment::plain_text(" ")); text_fragments.push(FormattedTextFragment::hyperlink_action( - ENABLE_ANALYTICS_ACTION_TEXT, + i18n::t("ai.prompt_alert.enable_analytics"), PromptAlertAction::OpenPrivacySettingsClicked, )); // Show "or upgrade to Build" link - text_fragments.push(FormattedTextFragment::plain_text(" or ")); + text_fragments.push(FormattedTextFragment::plain_text(i18n::t("common.or"))); let upgrade_url = if let Some(team) = UserWorkspaces::as_ref(app).current_team() { UserWorkspaces::upgrade_link_for_team(team.uid) } else { @@ -288,7 +264,7 @@ impl PromptAlertView { UserWorkspaces::upgrade_link(user_id) }; text_fragments.push(FormattedTextFragment::hyperlink( - UPGRADE_TO_BUILD_ACTION_TEXT, + i18n::t("ai.prompt_alert.upgrade"), upgrade_url, )); text_fragments.push(FormattedTextFragment::plain_text(".")); @@ -297,7 +273,7 @@ impl PromptAlertView { | PromptAlertState::AnonymousUserRequestLimitHardGate => { text_fragments.push(FormattedTextFragment::plain_text(" ")); text_fragments.push(FormattedTextFragment::hyperlink_action( - ANONYMOUS_USER_REQUEST_LIMIT_ACTION_TEXT, + i18n::t("ai.prompt_alert.sign_up_for_more_credits"), PromptAlertAction::SignUpClickedForAnonymousUser, )); } @@ -309,41 +285,41 @@ impl PromptAlertView { if has_admin_permissions && has_billing_history { text_fragments.push(FormattedTextFragment::plain_text(" ")); text_fragments.push(FormattedTextFragment::hyperlink_action( - DELINQUENT_DUE_TO_PAYMENT_ISSUE_ACTION_TEXT, + i18n::t("ai.prompt_alert.manage_billing"), PromptAlertAction::ManageBillingClicked { team_uid: current_team.map(|team| team.uid).unwrap_or_default(), }, )); } else { - text_fragments.push(FormattedTextFragment::plain_text( - NON_ADMIN_CONTACT_ADMIN_TEXT, - )); + text_fragments.push(FormattedTextFragment::plain_text(i18n::t( + "ai.prompt_alert.contact_team_admin", + ))); } } PromptAlertState::OveragesToggleableButNotEnabled => { if has_admin_permissions { text_fragments.push(FormattedTextFragment::plain_text(" ")); text_fragments.push(FormattedTextFragment::hyperlink_action( - OVERAGES_TOGGLEABLE_BUT_NOT_ENABLED_ACTION_TEXT, + i18n::t("ai.prompt_alert.enable_premium_overages"), PromptAlertAction::OpenSettingsClicked, )); } else { - text_fragments.push(FormattedTextFragment::plain_text( - NON_ADMIN_ASK_ADMIN_TO_ENABLE_OVERAGES_TEXT, - )); + text_fragments.push(FormattedTextFragment::plain_text(i18n::t( + "ai.prompt_alert.ask_admin_enable_overages", + ))); } } PromptAlertState::MonthlyOveragesSpendLimitReached => { if has_admin_permissions { text_fragments.push(FormattedTextFragment::plain_text(" ")); text_fragments.push(FormattedTextFragment::hyperlink_action( - MONTHLY_OVERAGES_SPEND_LIMIT_REACHED_ACTION_TEXT, + i18n::t("ai.prompt_alert.increase_monthly_spend_limit"), PromptAlertAction::OpenSettingsClicked, )); } else { - text_fragments.push(FormattedTextFragment::plain_text( - NON_ADMIN_ASK_ADMIN_TO_INCREASE_OVERAGES_TEXT, - )); + text_fragments.push(FormattedTextFragment::plain_text(i18n::t( + "ai.prompt_alert.ask_admin_increase_overages", + ))); } } PromptAlertState::RequestLimitReached => { @@ -352,18 +328,18 @@ impl PromptAlertView { if team.billing_metadata.can_upgrade_to_higher_tier_plan() { let upgrade_url = UserWorkspaces::upgrade_link_for_team(team.uid); let upgrade_text = if !has_admin_permissions { - COMPARE_PLANS_TEXT + i18n::t("ai.prompt_alert.compare_plans") } else if team.billing_metadata.can_upgrade_to_build_plan() { - "Upgrade to Build" + i18n::t("ai.prompt_alert.upgrade_to_build") } else { - UPGRADE_TEXT + i18n::t("ai.prompt_alert.upgrade") }; text_fragments .push(FormattedTextFragment::hyperlink(upgrade_text, upgrade_url)); } else { text_fragments.push(FormattedTextFragment::hyperlink( - CONTACT_SUPPORT_TEXT, + i18n::t("ai.prompt_alert.contact_support"), "mailto:support@warp.dev".to_owned(), )); } @@ -373,19 +349,19 @@ impl PromptAlertView { let label = if let Some(workspace) = UserWorkspaces::as_ref(app).current_workspace() { if workspace.billing_metadata.can_upgrade_to_build_plan() { - "Upgrade to Build" + i18n::t("ai.prompt_alert.upgrade_to_build") } else { - UPGRADE_TEXT + i18n::t("ai.prompt_alert.upgrade") } } else { - UPGRADE_TEXT + i18n::t("ai.prompt_alert.upgrade") }; text_fragments.push(FormattedTextFragment::hyperlink(label, upgrade_url)); } if UserWorkspaces::as_ref(app).is_byo_api_key_enabled(app) { - text_fragments.push(FormattedTextFragment::plain_text(" or ")); + text_fragments.push(FormattedTextFragment::plain_text(i18n::t("common.or"))); text_fragments.push(FormattedTextFragment::hyperlink_action( - "use your own API keys", + i18n::t("ai.prompt_alert.use_own_api_keys"), WorkspaceAction::ShowSettingsPageWithSearch { search_query: "api".to_string(), section: Some(SettingsSection::WarpAgent), @@ -450,7 +426,7 @@ impl View for PromptAlertView { if suggest_buy_credits { text_fragments.push(FormattedTextFragment::plain_text(" ")); text_fragments.push(FormattedTextFragment::hyperlink_action( - "Add credits", + i18n::t("ai.prompt_alert.add_credits"), WorkspaceAction::ShowSettingsPage(SettingsSection::BillingAndUsage), )); } else { diff --git a/app/src/ai/blocklist/suggested_agent_mode_workflow_modal.rs b/app/src/ai/blocklist/suggested_agent_mode_workflow_modal.rs index c2b270d907..33e2868cb9 100644 --- a/app/src/ai/blocklist/suggested_agent_mode_workflow_modal.rs +++ b/app/src/ai/blocklist/suggested_agent_mode_workflow_modal.rs @@ -26,8 +26,6 @@ use crate::workflows::{WorkflowSelectionSource, WorkflowSource, WorkflowType}; use crate::workspaces::user_workspaces::UserWorkspaces; use crate::TelemetryEvent; -const SUGGESTED_PROMPT_MODAL_HEADER: &str = "Prompt"; - /// A modal component for displaying and managing suggested agent mode workflows. /// This component wraps a WorkflowView in a modal dialog with proper styling and /// event handling. @@ -113,7 +111,7 @@ impl SuggestedAgentModeWorkflowModal { let modal = ctx.add_typed_action_view(|ctx| { let mut modal = Modal::new( - Some(SUGGESTED_PROMPT_MODAL_HEADER.to_string()), + Some(i18n::t("ai.suggested_workflow.prompt")), workflow_view_handle, ctx, ) diff --git a/app/src/ai/blocklist/suggested_rule_modal.rs b/app/src/ai/blocklist/suggested_rule_modal.rs index 3f50180789..11d69692ed 100644 --- a/app/src/ai/blocklist/suggested_rule_modal.rs +++ b/app/src/ai/blocklist/suggested_rule_modal.rs @@ -36,7 +36,6 @@ use crate::ui_components::blended_colors; use crate::view_components::action_button::{ActionButton, PrimaryTheme}; use crate::workspaces::user_workspaces::UserWorkspaces; -const HEADER_TEXT: &str = "Suggested rule"; const MAX_EDITOR_HEIGHT: f32 = 240.; pub fn init(app: &mut AppContext) { @@ -94,7 +93,7 @@ impl SuggestedRuleModal { let view_handle = view.clone(); let modal = ctx.add_typed_action_view(|ctx| { - Modal::new(Some(HEADER_TEXT.to_string()), view, ctx) + Modal::new(Some(i18n::t("ai.rules.suggested_rule")), view, ctx) .with_modal_style(UiComponentStyles { width: Some(510.), background: Some(background.into()), @@ -246,7 +245,7 @@ impl SuggestedRuleView { ctx.subscribe_to_model(&network_status, |me, _, _event, ctx| { let is_edit_allowed = me.is_edit_allowed(ctx); let tooltip = if !is_edit_allowed { - Some("Editing is disabled while offline.".to_string()) + Some(i18n::t("ai.rules.editing_disabled_offline")) } else { None }; @@ -308,12 +307,12 @@ impl SuggestedRuleView { }); let add_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Add rule", PrimaryTheme) + ActionButton::new(i18n::t("ai.rules.add_rule"), PrimaryTheme) .on_click(|ctx| ctx.dispatch_typed_action(SuggestedRuleDialogAction::Add)) }); let edit_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Edit rule", PrimaryTheme) + ActionButton::new(i18n::t("ai.rules.edit_rule"), PrimaryTheme) .on_click(|ctx| ctx.dispatch_typed_action(SuggestedRuleDialogAction::Edit)) }); @@ -494,7 +493,8 @@ impl SuggestedRuleView { { let AIFact::Memory(AIMemory { name, content, .. }) = rule.model().string_model.clone(); self.name_editor.update(ctx, |name_editor, ctx| { - name_editor.set_buffer_text(&name.unwrap_or("Untitled".to_string()), ctx); + name_editor + .set_buffer_text(&name.unwrap_or_else(|| i18n::t("common.untitled")), ctx); }); self.content_editor.update(ctx, |content_editor, ctx| { content_editor.set_buffer_text(&content, ctx); @@ -562,7 +562,7 @@ impl SuggestedRuleView { let editor_margin = 16.; Flex::column() - .with_child(self.render_label("Name".to_string(), appearance)) + .with_child(self.render_label(i18n::t("common.name"), appearance)) .with_child( Container::new(ChildView::new(&self.name_editor).finish()) .with_background(editor_bg) @@ -573,7 +573,7 @@ impl SuggestedRuleView { .with_margin_bottom(editor_margin) .finish(), ) - .with_child(self.render_label("Rule".to_string(), appearance)) + .with_child(self.render_label(i18n::t("ai.rules.rule"), appearance)) .with_child( ConstrainedBox::new( Container::new( diff --git a/app/src/ai/blocklist/suggestion_chip_view.rs b/app/src/ai/blocklist/suggestion_chip_view.rs index 01bad86d65..f03d1b0b9b 100644 --- a/app/src/ai/blocklist/suggestion_chip_view.rs +++ b/app/src/ai/blocklist/suggestion_chip_view.rs @@ -145,7 +145,7 @@ impl Suggestion { pub fn tooltip(&self) -> String { match self { Suggestion::Rule { rule, .. } => { - format!("Add rule: {}", rule.content.clone()) + i18n::t("ai.suggestion.add_rule").replace("{rule}", &rule.content) } Suggestion::AgentModeWorkflow { workflow, .. } => { let prompt = if workflow.prompt.chars().count() > MAX_PROMPT_TOOLTIP_LENGTH { @@ -158,7 +158,7 @@ impl Suggestion { } else { workflow.prompt.clone() }; - format!("Suggested prompt:\n{prompt}") + i18n::t("ai.suggestion.suggested_prompt").replace("{prompt}", &prompt) } } } diff --git a/app/src/ai/blocklist/summarization_cancel_dialog.rs b/app/src/ai/blocklist/summarization_cancel_dialog.rs index d0261d65da..bf519033cf 100644 --- a/app/src/ai/blocklist/summarization_cancel_dialog.rs +++ b/app/src/ai/blocklist/summarization_cancel_dialog.rs @@ -87,7 +87,7 @@ impl View for SummarizationCancelDialog { appearance .ui_builder() .button(ButtonVariant::Secondary, self.cancel_mouse.clone()) - .with_centered_text_label("Cancel summarization".into()) + .with_centered_text_label(i18n::t("ai.summarization.cancel")) .with_style(UiComponentStyles { width: Some(CANCEL_BUTTON_WIDTH), ..button_style @@ -105,7 +105,7 @@ impl View for SummarizationCancelDialog { let continue_button = appearance .ui_builder() .button(ButtonVariant::Accent, self.continue_mouse.clone()) - .with_centered_text_label("Continue summarization".into()) + .with_centered_text_label(i18n::t("ai.summarization.continue")) .with_style(UiComponentStyles { width: Some(CONTINUE_BUTTON_WIDTH), ..button_style @@ -163,8 +163,8 @@ impl View for SummarizationCancelDialog { // Build dialog content let dialog_core = Dialog::new( - "Cancel summarization?".to_string(), - Some("Summarization is already running. If you cancel now, the request may still incur cost, any progress so far will be lost, and restarting will take longer.\n\nAre you sure you want to cancel?".to_string()), + i18n::t("ai.summarization.cancel_dialog.title"), + Some(i18n::t("ai.summarization.cancel_dialog.body")), UiComponentStyles { padding: Some(Coords::uniform(24.)), ..dialog_styles diff --git a/app/src/ai/blocklist/telemetry_banner.rs b/app/src/ai/blocklist/telemetry_banner.rs index 9d5cad1117..4665bc4e03 100644 --- a/app/src/ai/blocklist/telemetry_banner.rs +++ b/app/src/ai/blocklist/telemetry_banner.rs @@ -15,9 +15,6 @@ use crate::workspaces::user_workspaces::UserWorkspaces; use crate::workspaces::workspace::UgcCollectionEnablementSetting; use crate::{Appearance, FeatureFlag, WorkspaceAction}; -const TITLE_EXISTING_USERS: &str = "We've updated our telemetry policy."; -const TITLE_NEW_USERS: &str = "Help improve Warp."; -const DESCRIPTION: &str = "We may collect certain console interactions to improve Warp's AI capabilities. You can opt out any time."; const PRIVACY_URL: &str = "https://warp.dev/privacy"; #[derive(Default, Debug, Clone)] @@ -50,9 +47,9 @@ impl View for TelemetryBanner { let ui_builder = appearance.ui_builder(); let title = if self.is_onboarded { - TITLE_EXISTING_USERS + i18n::t("ai.telemetry.policy_updated") } else { - TITLE_NEW_USERS + i18n::t("ai.telemetry.help_improve_warp") }; let left = Flex::row() @@ -82,10 +79,14 @@ impl View for TelemetryBanner { .finish(), ) .with_child( - Text::new(DESCRIPTION, ui_builder.ui_font_family(), 12.) - .with_color(theme.nonactive_ui_text_color().into_solid()) - .soft_wrap(true) - .finish(), + Text::new( + i18n::t("ai.telemetry.description"), + ui_builder.ui_font_family(), + 12., + ) + .with_color(theme.nonactive_ui_text_color().into_solid()) + .soft_wrap(true) + .finish(), ) .finish(), ) @@ -99,7 +100,7 @@ impl View for TelemetryBanner { Container::new( ui_builder .button(ButtonVariant::Text, self.learn_more_mouse_state.clone()) - .with_text_label("Learn more".into()) + .with_text_label(i18n::t("common.learn_more")) .with_style(UiComponentStyles { height: Some(24.), padding: Some(Coords { @@ -130,7 +131,7 @@ impl View for TelemetryBanner { ButtonVariant::Outlined, self.privacy_settings_mouse_state.clone(), ) - .with_text_label("Manage privacy settings".into()) + .with_text_label(i18n::t("ai.telemetry.manage_privacy_settings")) .with_style(UiComponentStyles { ..Default::default() }) diff --git a/app/src/ai/blocklist/usage/conversation_usage_view.rs b/app/src/ai/blocklist/usage/conversation_usage_view.rs index 3b2a6bd5fd..a7916e9664 100644 --- a/app/src/ai/blocklist/usage/conversation_usage_view.rs +++ b/app/src/ai/blocklist/usage/conversation_usage_view.rs @@ -288,7 +288,7 @@ impl ConversationUsageView { // Usage summary labels.push(render_section_header( - "USAGE SUMMARY".to_string(), + i18n::t("ai.usage.summary"), appearance, )); values.push(render_section_header("".to_string(), appearance)); @@ -306,7 +306,7 @@ impl ConversationUsageView { { let last_block_credits = self.usage_info.credits_spent_for_last_block.unwrap(); labels.push(render_label_text( - "Credits spent (last response)", + &i18n::t("ai.usage.credits_spent_last_response"), appearance, )); values.push(render_value_text( @@ -314,14 +314,20 @@ impl ConversationUsageView { appearance, )); - labels.push(render_label_text("Credits spent (total)", appearance)); + labels.push(render_label_text( + &i18n::t("ai.usage.credits_spent_total"), + appearance, + )); values.push(self.render_total_credits_value_row( total_credits_value, rollup.as_ref(), appearance, )); } else { - labels.push(render_label_text("Credits spent", appearance)); + labels.push(render_label_text( + &i18n::t("ai.usage.credits_spent"), + appearance, + )); values.push(self.render_total_credits_value_row( total_credits_value, rollup.as_ref(), @@ -337,9 +343,16 @@ impl ConversationUsageView { // existing flex spacing handles indentation. self.append_per_agent_rows(&mut labels, &mut values, rollup.as_ref(), appearance); - labels.push(render_label_text("Tool calls", appearance)); + labels.push(render_label_text( + &i18n::t("ai.usage.tool_calls"), + appearance, + )); values.push(render_value_text( - format_value_text(self.usage_info.tool_calls, "call"), + format_count_text( + self.usage_info.tool_calls, + "ai.usage.call_one", + "ai.usage.call_other", + ), appearance, )); @@ -359,9 +372,10 @@ impl ConversationUsageView { let label_text = if category == PRIMARY_AGENT_CATEGORY && entries_by_category.len() == 1 { - "Models".to_string() + i18n::t("ai.usage.models") } else { - format!("Models ({})", token_usage_category_display_name(&category)) + let category_name = token_usage_category_display_name(&category); + i18n::t("ai.usage.models_with_category").replace("{category}", &category_name) }; // For FULL_TERMINAL_USE_CATEGORY, add an info icon with tooltip @@ -372,7 +386,7 @@ impl ConversationUsageView { .ui_builder() .info_button_with_tooltip( font_size * 0.85, - "You can change which model is used for full terminal use in the AI settings page", + &i18n::t("ai.usage.full_terminal_model_tooltip"), self.full_terminal_use_tooltip_mouse_state.clone(), ) .finish(); @@ -435,7 +449,10 @@ impl ConversationUsageView { ); } - labels.push(render_label_text("Context window used", appearance)); + labels.push(render_label_text( + &i18n::t("ai.usage.context_window_used"), + appearance, + )); let context_usage_str = format!("{}%", (self.usage_info.context_window_usage * 100.).round()); let context_window_element = Flex::row() @@ -473,18 +490,28 @@ impl ConversationUsageView { // Tool call summary labels.push(render_section_header( - "TOOL CALL SUMMARY".to_string(), + i18n::t("ai.usage.tool_call_summary"), appearance, )); values.push(render_section_header("".to_string(), appearance)); - labels.push(render_label_text("Files changed", appearance)); + labels.push(render_label_text( + &i18n::t("ai.usage.files_changed"), + appearance, + )); values.push(render_value_text( - format_value_text(self.usage_info.files_changed, "file"), + format_count_text( + self.usage_info.files_changed, + "ai.usage.file_one", + "ai.usage.file_other", + ), appearance, )); - labels.push(render_label_text("Diffs applied", appearance)); + labels.push(render_label_text( + &i18n::t("ai.usage.diffs_applied"), + appearance, + )); let diffs_element = Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( @@ -521,9 +548,16 @@ impl ConversationUsageView { .finish(); values.push(diffs_element); - labels.push(render_label_text("Commands executed", appearance)); + labels.push(render_label_text( + &i18n::t("ai.usage.commands_executed"), + appearance, + )); values.push(render_value_text( - format_value_text(self.usage_info.commands_executed, "command"), + format_count_text( + self.usage_info.commands_executed, + "ai.usage.command_one", + "ai.usage.command_other", + ), appearance, )); @@ -548,25 +582,31 @@ impl ConversationUsageView { // Section header labels.push(render_section_header( - "LAST RESPONSE TIME".to_string(), + i18n::t("ai.usage.last_response_time"), appearance, )); values.push(render_section_header("".to_string(), appearance)); - labels.push(render_label_text("Time to first token", appearance)); + labels.push(render_label_text( + &i18n::t("ai.usage.time_to_first_token"), + appearance, + )); values.push(render_value_text( - format!( - "{:.1} seconds", - timing.time_to_first_token_ms as f64 / 1000.0 + i18n::t("ai.usage.seconds").replace( + "{seconds}", + &format!("{:.1}", timing.time_to_first_token_ms as f64 / 1000.0), ), appearance, )); - labels.push(render_label_text("Total agent response time", appearance)); + labels.push(render_label_text( + &i18n::t("ai.usage.total_agent_response_time"), + appearance, + )); values.push(render_value_text( - format!( - "{:.1} seconds", - timing.total_agent_response_time_ms as f64 / 1000.0 + i18n::t("ai.usage.seconds").replace( + "{seconds}", + &format!("{:.1}", timing.total_agent_response_time_ms as f64 / 1000.0), ), appearance, )); @@ -574,11 +614,14 @@ impl ConversationUsageView { if let Some(wall_ms) = timing.wall_to_wall_response_time_ms { if wall_ms != 0 { labels.push(render_label_text( - "Total time (including tool calls)", + &i18n::t("ai.usage.total_time_including_tool_calls"), appearance, )); values.push(render_value_text( - format!("{:.1} seconds", wall_ms as f64 / 1000.0), + i18n::t("ai.usage.seconds").replace( + "{seconds}", + &format!("{:.1}", wall_ms as f64 / 1000.0), + ), appearance, )); } @@ -681,18 +724,17 @@ impl ConversationUsageView { let link_color = theme.ansi_fg_blue(); let icon_size = font_size; let (label, icon) = if self.details_expanded { - ("Hide details", Icon::ChevronUp) + (i18n::t("ai.usage.hide_details"), Icon::ChevronUp) } else { - ("View details", Icon::ChevronDown) + (i18n::t("ai.usage.view_details"), Icon::ChevronDown) }; Hoverable::new( self.details_toggle_mouse_state.clone(), move |_hover_state| { - let text_element = - Text::new(label.to_string(), appearance.ui_font_family(), font_size) - .with_color(link_color) - .with_selectable(false) - .finish(); + let text_element = Text::new(label.clone(), appearance.ui_font_family(), font_size) + .with_color(link_color) + .with_selectable(false) + .finish(); let icon_element = ConstrainedBox::new(icon.to_warpui_icon(link_color.into()).finish()) .with_width(icon_size) @@ -791,7 +833,7 @@ impl ConversationUsageView { let theme = appearance.theme(); let font_size = appearance.ui_font_size() + 2.; let link_color = theme.ansi_fg_blue(); - let label = format!("Show {hidden_count} more"); + let label = i18n::t("ai.usage.show_more").replace("{count}", &hidden_count.to_string()); Hoverable::new(self.show_more_mouse_state.clone(), move |_hover_state| { Text::new(label.clone(), appearance.ui_font_family(), font_size) .with_color(link_color) @@ -903,10 +945,10 @@ fn render_section_header(header_label: String, appearance: &Appearance) -> Box String { - format!("{} {}{}", value, label, if value == 1 { "" } else { "s" }) +/// Format a localized count value using singular/plural keys. +fn format_count_text(value: i32, singular_key: &str, plural_key: &str) -> String { + let key = if value == 1 { singular_key } else { plural_key }; + i18n::t(key).replace("{count}", &value.to_string()) } /// Helper to build a text element with consistent styling for labels. diff --git a/app/src/ai/blocklist/view_util.rs b/app/src/ai/blocklist/view_util.rs index e7a95c1906..8e56e9d2bc 100644 --- a/app/src/ai/blocklist/view_util.rs +++ b/app/src/ai/blocklist/view_util.rs @@ -22,11 +22,15 @@ const PROVIDER_BUTTON_ICON_SIZE: f32 = 14.; const PROVIDER_BUTTON_ICON_TEXT_GAP: f32 = 8.; /// Text to use as a label throughout the app for user interactions that will attach selected -/// block(s) or text selections to a new AI query. -pub static ATTACH_AS_AGENT_MODE_CONTEXT_TEXT: LazyLock<&'static str> = - LazyLock::new(|| "Attach as agent context"); +/// block(s) or text selections to a new AI query. Resolved at call time so it tracks the active +/// locale when the language is switched live. +pub fn attach_as_agent_mode_context_text() -> String { + i18n::t("ai.agent_mode.attach_as_context") +} /// Label we use for the the command palette action to create a new local Oz agent pane. +/// Kept as the English source string: it is consumed as an `EditableBinding` description, which is +/// localized centrally by slugifying the description (`keybinding.description.new_agent_pane`). pub static NEW_AGENT_PANE_LABEL: LazyLock<&'static str> = LazyLock::new(|| "New Agent Pane"); /// Claude/Anthropic brand color (official brand orange #D97757). @@ -78,7 +82,7 @@ pub fn render_ai_follow_up_icon( let tooltip_background = appearance.theme().tooltip_background(); let tool_tip = appearance .ui_builder() - .tool_tip("Follow up with existing conversation".to_owned()) + .tool_tip(i18n::t("ai.conversation.follow_up_existing_tooltip")) .with_style(UiComponentStyles { font_size: Some(12.), background: Some(warpui::elements::Fill::Solid(tooltip_background)), @@ -147,12 +151,12 @@ pub fn format_credits(credits: f32) -> String { if credits.fract() < 0.1 { let whole = credits.trunc() as i32; if whole == 1 { - format!("{whole} credit") + i18n::t("ai.usage.credit_one").replace("{count}", &whole.to_string()) } else { - format!("{whole} credits") + i18n::t("ai.usage.credit_other").replace("{count}", &whole.to_string()) } } else { - format!("{credits:.1} credits") + i18n::t("ai.usage.credit_other").replace("{count}", &format!("{credits:.1}")) } } diff --git a/app/src/ai/conversation_details_panel.rs b/app/src/ai/conversation_details_panel.rs index f6b9ffdde8..14e3995434 100644 --- a/app/src/ai/conversation_details_panel.rs +++ b/app/src/ai/conversation_details_panel.rs @@ -79,9 +79,6 @@ const HARNESS_CIRCLE_SIZE: f32 = 16.0; const HARNESS_ICON_IN_CIRCLE: f32 = 9.0; const LABEL_VALUE_GAP: f32 = 4.0; const SECTION_HEADER_GAP: f32 = 8.0; -const RUN_METADATA_ACCESS_DENIED_TITLE: &str = "Run metadata is not available"; -const RUN_METADATA_ACCESS_DENIED_DESCRIPTION: &str = - "You can view this shared session, but run metadata is only visible to users with access to this run."; /// Panel rendering mode. #[derive(Debug, Clone, PartialEq)] @@ -517,7 +514,7 @@ impl ConversationDetailsData { environment_id: None, conversation_id: None, }, - title: "Cloud agent run".to_string(), + title: i18n::t("ai.conversation.cloud_agent_run"), creator: None, executor: None, created_at: None, @@ -608,7 +605,7 @@ pub fn init(app: &mut AppContext) { app.register_fixed_bindings([FixedBinding::custom( CustomAction::Copy, ConversationDetailsPanelAction::CopySelectedText, - "Copy", + i18n::t("common.copy"), id!(ConversationDetailsPanel::ui_name()) & !id!("IMEOpen"), )]); } @@ -651,16 +648,16 @@ impl ConversationDetailsPanel { #[cfg(not(target_family = "wasm"))] let continue_locally_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Continue locally", PrimaryTheme) - .with_tooltip("Fork this conversation locally") + ActionButton::new(i18n::t("ai.conversation.continue_locally"), PrimaryTheme) + .with_tooltip(i18n::t("ai.conversation.continue_locally_tooltip")) .with_size(ButtonSize::Small) .on_click(|ctx| { ctx.dispatch_typed_action(ConversationDetailsPanelAction::ContinueLocally); }) }); let open_in_oz_button = ctx.add_typed_action_view(|_| { - ActionButton::new("View in Oz", SecondaryTheme) - .with_tooltip("View this run in the Oz web app") + ActionButton::new(i18n::t("ai.conversation.view_in_oz"), SecondaryTheme) + .with_tooltip(i18n::t("ai.conversation.view_in_oz_tooltip")) .with_size(ButtonSize::Small) .on_click(|ctx| { ctx.dispatch_typed_action(ConversationDetailsPanelAction::OpenInOz); @@ -772,7 +769,9 @@ impl ConversationDetailsPanel { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::default("Copied branch name".to_string()); + let toast = DismissibleToast::default(i18n::t( + "agent_management.toast.copied_branch_name", + )); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); } @@ -989,11 +988,9 @@ impl ConversationDetailsPanel { .finish(); let created_text = Text::new( - format!( - "Created by {} • {}", - creator.display_name, - format_approx_duration_from_now(created_at) - ), + i18n::t("ai.conversation.created_by") + .replace("{name}", &creator.display_name) + .replace("{time}", &format_approx_duration_from_now(created_at)), appearance.ui_font_family(), ui_font_size, ) @@ -1035,7 +1032,7 @@ impl ConversationDetailsPanel { let ui_font_size = appearance.ui_font_size(); let label_text = Text::new( - "Agent".to_string(), + i18n::t("common.agent"), appearance.ui_font_family(), ui_font_size, ) @@ -1092,7 +1089,7 @@ impl ConversationDetailsPanel { let ui_font_size = appearance.ui_font_size(); let label_text = Text::new( - "Error".to_string(), + i18n::t("common.error"), appearance.ui_font_family(), ui_font_size, ) @@ -1143,7 +1140,7 @@ impl ConversationDetailsPanel { .finish(); let title = Text::new( - RUN_METADATA_ACCESS_DENIED_TITLE, + i18n::t("ai.conversation.run_metadata_access_denied_title"), appearance.ui_font_family(), ui_font_size, ) @@ -1152,7 +1149,7 @@ impl ConversationDetailsPanel { .with_selectable(true) .finish(); let description = Text::new( - RUN_METADATA_ACCESS_DENIED_DESCRIPTION, + i18n::t("ai.conversation.run_metadata_access_denied_description"), appearance.ui_font_family(), ui_font_size - 1., ) @@ -1223,7 +1220,7 @@ impl ConversationDetailsPanel { // Section header let header = Text::new( - "Status".to_string(), + i18n::t("common.status"), appearance.ui_font_family(), ui_font_size, ) @@ -1235,12 +1232,12 @@ impl ConversationDetailsPanel { PanelMode::Task { display_status, .. } => { let status = display_status.as_ref()?; let (icon, color) = status.status_icon_and_color(theme); - (icon, color, status.to_string()) + (icon, color, status.localized_label()) } PanelMode::Conversation { status, .. } => { let status = status.as_ref()?; let (icon, color) = status.status_icon_and_color(theme, StatusColorStyle::Standard); - (icon, color, status.to_string()) + (icon, color, status.localized_label()) } }; @@ -1293,7 +1290,7 @@ impl ConversationDetailsPanel { let ui_font_size = appearance.ui_font_size(); let label_text = Text::new( - "Harness".to_string(), + i18n::t("agent_management.metadata.harness"), appearance.ui_font_family(), ui_font_size, ) @@ -1376,7 +1373,7 @@ impl ConversationDetailsPanel { let oz_link = appearance .ui_builder() .link( - "Open in Oz".to_string(), + i18n::t("ai.conversation.open_in_oz"), Some(skill_url), None, self.mouse_states.skill_link.clone(), @@ -1412,7 +1409,7 @@ impl ConversationDetailsPanel { let source_link = appearance .ui_builder() .link( - "Open in GitHub".to_string(), + i18n::t("ai.conversation.open_in_github"), Some(github_url), None, self.mouse_states.skill_source_link.clone(), @@ -1433,7 +1430,11 @@ impl ConversationDetailsPanel { if trimmed.is_empty() { return None; } - Some(self.render_simple_field("Initial query", trimmed, appearance)) + Some(self.render_simple_field( + &i18n::t("ai.conversation.initial_query"), + trimmed, + appearance, + )) } fn render_artifacts_section(&self, appearance: &Appearance) -> Option> { @@ -1444,7 +1445,7 @@ impl ConversationDetailsPanel { let ui_font_size = appearance.ui_font_size(); let label_text = Text::new( - "Artifacts".to_string(), + i18n::t("ai.conversation.artifacts"), appearance.ui_font_family(), ui_font_size, ) @@ -1483,7 +1484,7 @@ impl ConversationDetailsPanel { let ui_font_size = appearance.ui_font_size(); let header_text = Text::new( - "Environment setup commands".to_string(), + i18n::t("ai.conversation.environment_setup_commands"), appearance.ui_font_family(), ui_font_size, ) @@ -1548,7 +1549,7 @@ impl ConversationDetailsPanel { // Section header let header = Text::new( - "Environment details".to_string(), + i18n::t("ai.conversation.environment_details"), appearance.ui_font_family(), ui_font_size, ) @@ -1583,7 +1584,7 @@ impl ConversationDetailsPanel { }; let name_text = Text::new( - format!("Name: {environment_name}"), + i18n::t("ai.conversation.environment_name").replace("{name}", environment_name), appearance.ui_font_family(), ui_font_size, ) @@ -1599,7 +1600,7 @@ impl ConversationDetailsPanel { section.add_child( Container::new(render_copyable_field( - "ID", + &i18n::t("ai.conversation.id"), environment_id, CopyButtonKind::EnvironmentId, ConversationDetailsPanelAction::CopyEnvironmentId, @@ -1610,7 +1611,7 @@ impl ConversationDetailsPanel { section.add_child( Container::new(render_copyable_field( - "Image", + &i18n::t("ai.conversation.image"), &docker_image, CopyButtonKind::DockerImage, ConversationDetailsPanelAction::CopyDockerImage, @@ -1920,7 +1921,7 @@ impl View for ConversationDetailsPanel { if let Some(directory) = directory { content.add_child( Container::new(self.render_field_with_copy( - "Directory", + &i18n::t("ai.conversation.directory"), directory, ConversationDetailsPanelAction::CopyDirectory, CopyButtonKind::Directory, @@ -1935,7 +1936,7 @@ impl View for ConversationDetailsPanel { if let Some(id) = conversation_id { content.add_child( Container::new(self.render_field_with_copy( - "Conversation ID", + &i18n::t("ai.conversation.conversation_id"), id, ConversationDetailsPanelAction::CopyConversationId, CopyButtonKind::ConversationId, @@ -1953,7 +1954,7 @@ impl View for ConversationDetailsPanel { if let Some(directory) = directory { content.add_child( Container::new(self.render_field_with_copy( - "Directory", + &i18n::t("ai.conversation.directory"), directory, ConversationDetailsPanelAction::CopyDirectory, CopyButtonKind::Directory, @@ -1967,7 +1968,7 @@ impl View for ConversationDetailsPanel { if let Some(task_id) = task_id { content.add_child( Container::new(self.render_field_with_copy( - "Run ID", + &i18n::t("ai.conversation.run_id"), &task_id.to_string(), ConversationDetailsPanelAction::CopyRunId, CopyButtonKind::RunId, @@ -1984,27 +1985,39 @@ impl View for ConversationDetailsPanel { if let Some(credits) = self.data.credits { let formatted = format!("{credits:.1}"); content.add_child( - Container::new(self.render_simple_field("Credits used", &formatted, appearance)) - .with_margin_bottom(FIELD_SPACING) - .finish(), + Container::new(self.render_simple_field( + &i18n::t("ai.conversation.credits_used"), + &formatted, + appearance, + )) + .with_margin_bottom(FIELD_SPACING) + .finish(), ); } if let Some(duration) = self.data.run_time { let formatted = human_readable_precise_duration(duration); content.add_child( - Container::new(self.render_simple_field("Run time", &formatted, appearance)) - .with_margin_bottom(FIELD_SPACING) - .finish(), + Container::new(self.render_simple_field( + &i18n::t("ai.conversation.run_time"), + &formatted, + appearance, + )) + .with_margin_bottom(FIELD_SPACING) + .finish(), ); } if let Some(created_at) = self.data.created_at { let formatted = created_at.format("%I:%M %p on %-m/%-d/%Y").to_string(); content.add_child( - Container::new(self.render_simple_field("Created on", &formatted, appearance)) - .with_margin_bottom(FIELD_SPACING) - .finish(), + Container::new(self.render_simple_field( + &i18n::t("ai.conversation.created_on"), + &formatted, + appearance, + )) + .with_margin_bottom(FIELD_SPACING) + .finish(), ); } diff --git a/app/src/ai/document/ai_document_model.rs b/app/src/ai/document/ai_document_model.rs index 9928949e92..ea186611cc 100644 --- a/app/src/ai/document/ai_document_model.rs +++ b/app/src/ai/document/ai_document_model.rs @@ -20,7 +20,7 @@ use {anyhow, warp_multi_agent_api as maa_api}; use crate::ai::agent::conversation::AIConversationId; use crate::ai::agent::AIAgentActionId; -use crate::ai::ai_document_view::DEFAULT_PLANNING_DOCUMENT_TITLE; +use crate::ai::ai_document_view::default_planning_document_title; use crate::ai::blocklist::{BlocklistAIHistoryEvent, BlocklistAIHistoryModel}; use crate::ai::execution_profiles::profiles::AIExecutionProfilesModel; use crate::appearance::Appearance; @@ -738,10 +738,12 @@ impl AIDocumentModel { log::info!( "Creating document {id} from persisted SQLite content (conversation not restored)" ); - let title = persisted_title.unwrap_or(DEFAULT_PLANNING_DOCUMENT_TITLE); + let title = persisted_title + .map(str::to_string) + .unwrap_or_else(default_planning_document_title); self.create_document_internal( id, - title, + &title, persisted_content, AIDocumentUpdateSource::Restoration, // We don't have the conversation ID this is for - this is free floating and not connected to any conversation diff --git a/app/src/ai/document/orchestration_config_block.rs b/app/src/ai/document/orchestration_config_block.rs index d5f39cdce8..c2ed0cabab 100644 --- a/app/src/ai/document/orchestration_config_block.rs +++ b/app/src/ai/document/orchestration_config_block.rs @@ -105,11 +105,6 @@ fn render_pill_toggle(is_on: bool, theme: &WarpTheme) -> Box { .finish() } -const CONFIG_BLOCK_HEADER: &str = "Use orchestration"; -const CONFIG_BLOCK_DESCRIPTION: &str = - "Break this work into coordinated streams with multiple agents."; -const BASE_MODEL_HELPER: &str = "The primary model all agents will use."; - // ── Action type ───────────────────────────────────────────────────── #[derive(Clone, Debug, PartialEq, Eq)] @@ -606,7 +601,7 @@ impl View for OrchestrationConfigBlockView { // Header row: "Use orchestration" + pill toggle switch let header_label = Text::new( - CONFIG_BLOCK_HEADER.to_string(), + i18n::t("ai.orchestration_config.header"), appearance.ui_font_family(), 16., ) @@ -634,7 +629,7 @@ impl View for OrchestrationConfigBlockView { // Description let description = Text::new( - CONFIG_BLOCK_DESCRIPTION.to_string(), + i18n::t("ai.orchestration_config.description"), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -662,7 +657,7 @@ impl View for OrchestrationConfigBlockView { }; let disabled_text_color = blended_colors::text_disabled(theme, theme.background()); let details_text = Text::new( - "View details".to_string(), + i18n::t("common.view_details"), appearance.ui_font_family(), appearance.monospace_font_size() + 1., ) @@ -722,7 +717,7 @@ impl View for OrchestrationConfigBlockView { // Helper text let helper = Text::new( - BASE_MODEL_HELPER.to_string(), + i18n::t("ai.orchestration_config.base_model_helper"), appearance.ui_font_family(), appearance.monospace_font_size() - 1., ) diff --git a/app/src/ai/execution_profiles/editor/mod.rs b/app/src/ai/execution_profiles/editor/mod.rs index 047d6c70bd..74e4af4485 100644 --- a/app/src/ai/execution_profiles/editor/mod.rs +++ b/app/src/ai/execution_profiles/editor/mod.rs @@ -71,8 +71,11 @@ fn render_upgrade_footer( .with_height(16.) .finish(); - let label = "Frontier models are unavailable on free plans. Upgrade"; - let upgrade_start = label.len() - "Upgrade".len(); + let label_prefix = i18n::t("ai.execution_profiles.editor.upgrade_footer.prefix"); + let upgrade_label = i18n::t("ai.execution_profiles.editor.upgrade_footer.upgrade"); + let label = format!("{label_prefix}{upgrade_label}"); + let upgrade_start = label_prefix.chars().count(); + let upgrade_end = upgrade_start + upgrade_label.chars().count(); let info_text = Text::new( label, appearance.ui_font_family(), @@ -83,15 +86,15 @@ fn render_upgrade_footer( Highlight::new() .with_properties(Properties::default()) .with_foreground_color(internal_colors::accent_fg(theme).into()), - (upgrade_start..label.len()).collect(), + (upgrade_start..upgrade_end).collect(), ) .with_hoverable_char_range( - upgrade_start..label.len(), + upgrade_start..upgrade_end, upgrade_mouse_state, Some(Cursor::PointingHand), |_is_hovered, _ctx, _app| {}, ) - .with_clickable_char_range(upgrade_start..label.len(), move |_modifiers, ctx, _app| { + .with_clickable_char_range(upgrade_start..upgrade_end, move |_modifiers, ctx, _app| { ctx.dispatch_typed_action(WorkspaceAction::ShowUpgrade); }) .finish(); @@ -138,8 +141,6 @@ struct TooltipMouseStateHandles { pub mod manager; pub use manager::*; -pub const HEADER_TEXT: &str = "Profile Editor"; - #[derive(Debug, Clone)] pub enum ExecutionProfileEditorViewEvent { Pane(PaneEvent), @@ -277,26 +278,28 @@ pub struct ExecutionProfileEditorView { impl ExecutionProfileEditorView { pub fn new(profile_id: ClientProfileId, ctx: &mut ViewContext) -> Self { - let pane_configuration = ctx.add_model(|_ctx| PaneConfiguration::new(HEADER_TEXT)); + let pane_configuration = ctx.add_model(|_ctx| { + PaneConfiguration::new(i18n::t("ai.execution_profiles.editor.header")) + }); let apply_code_diffs_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = Dropdown::new(ctx); dropdown.set_items( vec![ DropdownItem::new( - "Agent decides", + i18n::t("ai.execution_profiles.editor.permission.agent_decides"), ExecutionProfileEditorViewAction::SetApplyCodeDiffs { permission: ActionPermission::AgentDecides, }, ), DropdownItem::new( - "Always allow", + i18n::t("ai.execution_profiles.editor.permission.always_allow"), ExecutionProfileEditorViewAction::SetApplyCodeDiffs { permission: ActionPermission::AlwaysAllow, }, ), DropdownItem::new( - "Always ask", + i18n::t("ai.execution_profiles.editor.permission.always_ask"), ExecutionProfileEditorViewAction::SetApplyCodeDiffs { permission: ActionPermission::AlwaysAsk, }, @@ -312,19 +315,19 @@ impl ExecutionProfileEditorView { dropdown.set_items( vec![ DropdownItem::new( - "Agent decides", + i18n::t("ai.execution_profiles.editor.permission.agent_decides"), ExecutionProfileEditorViewAction::SetReadFiles { permission: ActionPermission::AgentDecides, }, ), DropdownItem::new( - "Always allow", + i18n::t("ai.execution_profiles.editor.permission.always_allow"), ExecutionProfileEditorViewAction::SetReadFiles { permission: ActionPermission::AlwaysAllow, }, ), DropdownItem::new( - "Always ask", + i18n::t("ai.execution_profiles.editor.permission.always_ask"), ExecutionProfileEditorViewAction::SetReadFiles { permission: ActionPermission::AlwaysAsk, }, @@ -340,19 +343,19 @@ impl ExecutionProfileEditorView { dropdown.set_items( vec![ DropdownItem::new( - "Agent decides", + i18n::t("ai.execution_profiles.editor.permission.agent_decides"), ExecutionProfileEditorViewAction::SetExecuteCommands { permission: ActionPermission::AgentDecides, }, ), DropdownItem::new( - "Always allow", + i18n::t("ai.execution_profiles.editor.permission.always_allow"), ExecutionProfileEditorViewAction::SetExecuteCommands { permission: ActionPermission::AlwaysAllow, }, ), DropdownItem::new( - "Always ask", + i18n::t("ai.execution_profiles.editor.permission.always_ask"), ExecutionProfileEditorViewAction::SetExecuteCommands { permission: ActionPermission::AlwaysAsk, }, @@ -368,19 +371,19 @@ impl ExecutionProfileEditorView { dropdown.set_items( vec![ DropdownItem::new( - "Always allow", + i18n::t("ai.execution_profiles.editor.permission.always_allow"), ExecutionProfileEditorViewAction::SetWriteToPty { permission: WriteToPtyPermission::AlwaysAllow, }, ), DropdownItem::new( - "Always ask", + i18n::t("ai.execution_profiles.editor.permission.always_ask"), ExecutionProfileEditorViewAction::SetWriteToPty { permission: WriteToPtyPermission::AlwaysAsk, }, ), DropdownItem::new( - "Ask on first write", + i18n::t("ai.execution_profiles.editor.permission.ask_on_first_write"), ExecutionProfileEditorViewAction::SetWriteToPty { permission: WriteToPtyPermission::AskOnFirstWrite, }, @@ -396,19 +399,19 @@ impl ExecutionProfileEditorView { dropdown.set_items( vec![ DropdownItem::new( - "Agent decides", + i18n::t("ai.execution_profiles.editor.permission.agent_decides"), ExecutionProfileEditorViewAction::SetCallMcpServers { permission: ActionPermission::AgentDecides, }, ), DropdownItem::new( - "Always allow", + i18n::t("ai.execution_profiles.editor.permission.always_allow"), ExecutionProfileEditorViewAction::SetCallMcpServers { permission: ActionPermission::AlwaysAllow, }, ), DropdownItem::new( - "Always ask", + i18n::t("ai.execution_profiles.editor.permission.always_ask"), ExecutionProfileEditorViewAction::SetCallMcpServers { permission: ActionPermission::AlwaysAsk, }, @@ -424,19 +427,19 @@ impl ExecutionProfileEditorView { dropdown.set_items( vec![ DropdownItem::new( - "Never", + i18n::t("ai.execution_profiles.editor.permission.never"), ExecutionProfileEditorViewAction::SetComputerUse { permission: super::ComputerUsePermission::Never, }, ), DropdownItem::new( - "Always ask", + i18n::t("ai.execution_profiles.editor.permission.always_ask"), ExecutionProfileEditorViewAction::SetComputerUse { permission: super::ComputerUsePermission::AlwaysAsk, }, ), DropdownItem::new( - "Always allow", + i18n::t("ai.execution_profiles.editor.permission.always_allow"), ExecutionProfileEditorViewAction::SetComputerUse { permission: super::ComputerUsePermission::AlwaysAllow, }, @@ -452,19 +455,19 @@ impl ExecutionProfileEditorView { dropdown.set_items( vec![ DropdownItem::new( - "Never ask", + i18n::t("ai.execution_profiles.editor.permission.never_ask"), ExecutionProfileEditorViewAction::SetAskUserQuestion { permission: super::AskUserQuestionPermission::Never, }, ), DropdownItem::new( - "Ask unless auto-approve", + i18n::t("ai.execution_profiles.editor.permission.ask_unless_auto_approve"), ExecutionProfileEditorViewAction::SetAskUserQuestion { permission: super::AskUserQuestionPermission::AskExceptInAutoApprove, }, ), DropdownItem::new( - "Always ask", + i18n::t("ai.execution_profiles.editor.permission.always_ask"), ExecutionProfileEditorViewAction::SetAskUserQuestion { permission: super::AskUserQuestionPermission::AlwaysAsk, }, @@ -480,19 +483,19 @@ impl ExecutionProfileEditorView { dropdown.set_items( vec![ DropdownItem::new( - "Never", + i18n::t("ai.execution_profiles.editor.permission.never"), ExecutionProfileEditorViewAction::SetRunAgents { permission: RunAgentsPermission::NeverAllow, }, ), DropdownItem::new( - "Always allow", + i18n::t("ai.execution_profiles.editor.permission.always_allow"), ExecutionProfileEditorViewAction::SetRunAgents { permission: RunAgentsPermission::AlwaysAllow, }, ), DropdownItem::new( - "Always ask", + i18n::t("ai.execution_profiles.editor.permission.always_ask"), ExecutionProfileEditorViewAction::SetRunAgents { permission: RunAgentsPermission::AlwaysAsk, }, @@ -505,13 +508,17 @@ impl ExecutionProfileEditorView { let mcp_allowlist_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = FilterableDropdown::new(ctx); - dropdown.set_menu_header_to_static("Select MCP servers"); + dropdown.set_menu_header_to_static(i18n::t( + "ai.execution_profiles.editor.select_mcp_servers", + )); dropdown }); let mcp_denylist_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = FilterableDropdown::new(ctx); - dropdown.set_menu_header_to_static("Select MCP servers"); + dropdown.set_menu_header_to_static(i18n::t( + "ai.execution_profiles.editor.select_mcp_servers", + )); dropdown }); @@ -574,7 +581,10 @@ impl ExecutionProfileEditorView { let command_allowlist_editor = ctx.add_typed_action_view(|ctx| { let mut input = SubmittableTextInput::new(ctx).validate_on_edit(|s| Regex::new(s).is_ok()); - input.set_placeholder_text("e.g. ls .*", ctx); + input.set_placeholder_text( + i18n::t("ai.execution_profiles.editor.command_allowlist_placeholder"), + ctx, + ); input }); @@ -587,7 +597,10 @@ impl ExecutionProfileEditorView { let command_denylist_editor = ctx.add_typed_action_view(|ctx| { let mut input = SubmittableTextInput::new(ctx).validate_on_edit(|s| Regex::new(s).is_ok()); - input.set_placeholder_text("e.g. rm .*", ctx); + input.set_placeholder_text( + i18n::t("ai.execution_profiles.editor.command_denylist_placeholder"), + ctx, + ); input }); @@ -602,7 +615,10 @@ impl ExecutionProfileEditorView { let expanded = host_native_absolute_path(s, &None, &None); Path::new(&expanded).is_dir() }); - input.set_placeholder_text("e.g. ~/code-repos/repo", ctx); + input.set_placeholder_text( + i18n::t("ai.execution_profiles.editor.directory_allowlist_placeholder"), + ctx, + ); input }); @@ -620,7 +636,10 @@ impl ExecutionProfileEditorView { }, ctx, ); - editor.set_placeholder_text("e.g. \"YOLO code\"", ctx); + editor.set_placeholder_text( + i18n::t("ai.execution_profiles.editor.profile_name_placeholder"), + ctx, + ); editor }); @@ -634,11 +653,14 @@ impl ExecutionProfileEditorView { Self::update_profile_name_editor(&profile_name_editor, &profile_data, ctx); let delete_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Delete profile", DangerSecondaryTheme) - .with_icon(Icon::Trash) - .on_click(|ctx| { - ctx.dispatch_typed_action(ExecutionProfileEditorViewAction::DeleteProfile); - }) + ActionButton::new( + i18n::t("ai.execution_profiles.editor.delete_profile"), + DangerSecondaryTheme, + ) + .with_icon(Icon::Trash) + .on_click(|ctx| { + ctx.dispatch_typed_action(ExecutionProfileEditorViewAction::DeleteProfile); + }) }); let mut view = Self { @@ -1332,7 +1354,7 @@ impl ExecutionProfileEditorView { ) { profile_name_editor.update(ctx, |editor, ctx| { let display_name = if profile_data.is_default_profile { - "Default".to_string() + i18n::t("ai.execution_profiles.editor.default_profile") } else { profile_data.name.clone() }; @@ -1777,7 +1799,7 @@ impl BackingView for ExecutionProfileEditorView { _app: &AppContext, ) -> view::HeaderContent { view::HeaderContent::Standard(view::StandardHeader { - title: HEADER_TEXT.into(), + title: i18n::t("ai.execution_profiles.editor.header"), title_secondary: None, title_style: None, title_clip_config: warpui::text_layout::ClipConfig::start(), diff --git a/app/src/ai/execution_profiles/editor/ui_helpers.rs b/app/src/ai/execution_profiles/editor/ui_helpers.rs index dba1ee21e2..af91a654ab 100644 --- a/app/src/ai/execution_profiles/editor/ui_helpers.rs +++ b/app/src/ai/execution_profiles/editor/ui_helpers.rs @@ -12,7 +12,10 @@ use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles}; use warpui::{AppContext, Element, SingletonEntity, ViewHandle}; use super::{ExecutionProfileEditorView, ExecutionProfileEditorViewAction}; -use crate::ai::execution_profiles::{AIExecutionProfile, ActionPermission}; +use crate::ai::execution_profiles::{ + AIExecutionProfile, ActionPermission, AskUserQuestionPermission, ComputerUsePermission, + RunAgentsPermission, WriteToPtyPermission, +}; use crate::editor::EditorView; use crate::settings::AISettings; use crate::ui_components::icons::Icon; @@ -62,8 +65,81 @@ fn nice_step(raw: f64) -> f64 { use crate::settings_view::{render_input_list, render_separator, InputListItem}; -pub const WORKSPACE_OVERRIDE_TOOLTIP_MESSAGE: &str = - "This option is enforced by your organization's settings and cannot be customized."; +fn action_permission_description(permission: ActionPermission) -> String { + match permission { + ActionPermission::AgentDecides | ActionPermission::Unknown => { + i18n::t("ai.execution_profiles.editor.permission_desc.agent_decides") + } + ActionPermission::AlwaysAllow => { + i18n::t("ai.execution_profiles.editor.permission_desc.always_allow") + } + ActionPermission::AlwaysAsk => { + i18n::t("ai.execution_profiles.editor.permission_desc.always_ask") + } + } +} + +fn write_to_pty_permission_description(permission: WriteToPtyPermission) -> String { + match permission { + WriteToPtyPermission::AlwaysAllow => { + action_permission_description(ActionPermission::AlwaysAllow) + } + WriteToPtyPermission::AskOnFirstWrite => { + i18n::t("ai.execution_profiles.editor.permission_desc.write_to_pty.ask_on_first_write") + } + WriteToPtyPermission::AlwaysAsk => { + i18n::t("ai.execution_profiles.editor.permission_desc.write_to_pty.always_ask") + } + WriteToPtyPermission::Unknown => action_permission_description(ActionPermission::Unknown), + } +} + +fn computer_use_permission_description(permission: ComputerUsePermission) -> String { + match permission { + ComputerUsePermission::Never => { + i18n::t("ai.execution_profiles.editor.permission_desc.computer_use.never") + } + ComputerUsePermission::AlwaysAsk => { + i18n::t("ai.execution_profiles.editor.permission_desc.computer_use.always_ask") + } + ComputerUsePermission::AlwaysAllow => { + i18n::t("ai.execution_profiles.editor.permission_desc.computer_use.always_allow") + } + ComputerUsePermission::Unknown => { + i18n::t("ai.execution_profiles.editor.permission_desc.unknown") + } + } +} + +fn ask_user_question_permission_description(permission: AskUserQuestionPermission) -> String { + match permission { + AskUserQuestionPermission::AskExceptInAutoApprove + | AskUserQuestionPermission::Unknown => i18n::t( + "ai.execution_profiles.editor.permission_desc.ask_user_question.ask_unless_auto_approve", + ), + AskUserQuestionPermission::Never => { + i18n::t("ai.execution_profiles.editor.permission_desc.ask_user_question.never") + } + AskUserQuestionPermission::AlwaysAsk => { + i18n::t("ai.execution_profiles.editor.permission_desc.ask_user_question.always_ask") + } + } +} + +fn run_agents_permission_description(permission: RunAgentsPermission) -> String { + match permission { + RunAgentsPermission::NeverAllow | RunAgentsPermission::Unknown => { + i18n::t("ai.execution_profiles.editor.permission_desc.run_agents.never") + } + RunAgentsPermission::AlwaysAllow => { + i18n::t("ai.execution_profiles.editor.permission_desc.run_agents.always_allow") + } + RunAgentsPermission::AlwaysAsk => { + i18n::t("ai.execution_profiles.editor.permission_desc.run_agents.always_ask") + } + } +} + pub fn render_header_section( appearance: &Appearance, profile_name_editor: &ViewHandle, @@ -87,7 +163,7 @@ pub fn render_header_section( if is_default_profile { column.add_child(render_info_section( - "Default profile name cannot be changed.", + &i18n::t("ai.execution_profiles.editor.default_profile_name_locked"), None, appearance, )); @@ -99,17 +175,25 @@ pub fn render_header_section( } fn render_header_title(appearance: &Appearance) -> Box { - Text::new_inline("Edit Profile", appearance.ui_font_family(), 16.) - .with_style(Properties::default().weight(Weight::Bold)) - .with_color(appearance.theme().active_ui_text_color().into()) - .finish() + Text::new_inline( + i18n::t("ai.execution_profiles.editor.edit_profile"), + appearance.ui_font_family(), + 16., + ) + .with_style(Properties::default().weight(Weight::Bold)) + .with_color(appearance.theme().active_ui_text_color().into()) + .finish() } fn render_header_name_label(appearance: &Appearance) -> Box { Container::new( - Text::new("Name", appearance.ui_font_family(), 13.) - .with_color(appearance.theme().active_ui_text_color().into()) - .finish(), + Text::new( + i18n::t("ai.execution_profiles.editor.name_label"), + appearance.ui_font_family(), + 13., + ) + .with_color(appearance.theme().active_ui_text_color().into()) + .finish(), ) .with_margin_top(16.) .finish() @@ -252,11 +336,14 @@ pub fn render_models_section( ) -> Box { let mut column = Flex::column() .with_child(render_separator(appearance)) - .with_child(render_section_label("MODELS", appearance)) + .with_child(render_section_label( + &i18n::t("ai.execution_profiles.editor.models_section"), + appearance, + )) .with_child(render_filterable_dropdown_row( appearance, - "Base model", - "This model serves as the primary engine behind the agent. It powers most interactions and invokes other models for tasks like planning or code generation when necessary. Warp may automatically switch to alternate models based on model availability or for auxiliary tasks such as conversation summarization.", + &i18n::t("ai.execution_profiles.editor.base_model.label"), + &i18n::t("ai.execution_profiles.editor.base_model.desc"), &view.base_model_dropdown, )); @@ -266,16 +353,16 @@ pub fn render_models_section( column = column.with_child(render_filterable_dropdown_row( appearance, - "Full terminal use model", - "The model used when the agent operates inside interactive terminal applications like database shells, debuggers, REPLs, or dev servers—reading live output and writing commands to the PTY.", + &i18n::t("ai.execution_profiles.editor.full_terminal_use_model.label"), + &i18n::t("ai.execution_profiles.editor.full_terminal_use_model.desc"), &view.full_terminal_use_model_dropdown, )); if FeatureFlag::LocalComputerUse.is_enabled() { column.add_child(render_filterable_dropdown_row( appearance, - "Computer use model", - "The model used when the agent takes control of your computer to interact with graphical applications through mouse movements, clicks, and keyboard input.", + &i18n::t("ai.execution_profiles.editor.computer_use_model.label"), + &i18n::t("ai.execution_profiles.editor.computer_use_model.desc"), &view.computer_use_model_dropdown, )); } @@ -305,7 +392,7 @@ fn render_context_window_row( let max = cw.max; let label = Text::new( - "Context window".to_string(), + i18n::t("ai.execution_profiles.editor.context_window.label"), appearance.ui_font_family(), 13., ) @@ -314,7 +401,7 @@ fn render_context_window_row( let min_label_text = min.separate_with_commas(); let max_label_text = max.separate_with_commas(); let desc = Text::new( - "The base model's working memory — how many tokens of your conversation, code, and documents it can consider at once. Larger windows enable longer conversations and more coherent responses over bigger codebases, at the cost of higher latency and compute usage.".to_string(), + i18n::t("ai.execution_profiles.editor.context_window.desc"), appearance.ui_font_family(), 11., ) @@ -437,13 +524,16 @@ pub fn render_permissions_section( let ai_settings = AISettings::as_ref(app); let mut column = Flex::column().with_children([ render_separator(appearance), - render_section_label("PERMISSIONS", appearance), + render_section_label( + &i18n::t("ai.execution_profiles.editor.permissions_section"), + appearance, + ), render_permission_row( appearance, Icon::Code2, - "Apply code diffs", + &i18n::t("ai.execution_profiles.editor.permission.apply_code_diffs"), &view.apply_code_diffs_dropdown, - profile_data.apply_code_diffs.description(), + &action_permission_description(profile_data.apply_code_diffs), !ai_settings.is_code_diffs_permissions_editable(app), view.tooltip_mouse_state_handles .apply_code_diffs_tooltip_mouse_state @@ -452,9 +542,9 @@ pub fn render_permissions_section( render_permission_row( appearance, Icon::Notebook, - "Read files", + &i18n::t("ai.execution_profiles.editor.permission.read_files"), &view.read_files_dropdown, - profile_data.read_files.description(), + &action_permission_description(profile_data.read_files), !ai_settings.is_read_files_permissions_editable(app), view.tooltip_mouse_state_handles .read_files_tooltip_mouse_state @@ -476,9 +566,9 @@ pub fn render_permissions_section( column.add_child(render_permission_row( appearance, Icon::Terminal, - "Execute commands", + &i18n::t("ai.execution_profiles.editor.permission.execute_commands"), &view.execute_commands_dropdown, - profile_data.execute_commands.description(), + &action_permission_description(profile_data.execute_commands), !ai_settings.is_execute_commands_permissions_editable(app), view.tooltip_mouse_state_handles .execute_commands_tooltip_mouse_state @@ -513,9 +603,9 @@ pub fn render_permissions_section( column.add_child(render_permission_row( appearance, Icon::Workflow, - "Interact with running commands", + &i18n::t("ai.execution_profiles.editor.permission.interact_with_running_commands"), &view.write_to_pty_dropdown, - profile_data.write_to_pty.description(), + &write_to_pty_permission_description(profile_data.write_to_pty), !ai_settings.is_write_to_pty_permissions_editable(app), view.tooltip_mouse_state_handles .write_to_pty_tooltip_mouse_state @@ -526,9 +616,9 @@ pub fn render_permissions_section( column.add_child(render_permission_row( appearance, Icon::Laptop, - "Computer use", + &i18n::t("ai.execution_profiles.editor.permission.computer_use"), &view.computer_use_dropdown, - profile_data.computer_use.description(), + &computer_use_permission_description(profile_data.computer_use), !ai_settings.is_computer_use_permissions_editable(app), view.tooltip_mouse_state_handles .computer_use_tooltip_mouse_state @@ -539,9 +629,9 @@ pub fn render_permissions_section( column.add_child(render_permission_row( appearance, Icon::MessageText, - "Ask questions", + &i18n::t("ai.execution_profiles.editor.permission.ask_questions"), &view.ask_user_question_dropdown, - profile_data.ask_user_question.description(), + &ask_user_question_permission_description(profile_data.ask_user_question), !ai_settings.is_ask_user_question_permissions_editable(app), view.tooltip_mouse_state_handles .ask_user_question_tooltip_mouse_state @@ -550,9 +640,9 @@ pub fn render_permissions_section( column.add_child(render_permission_row( appearance, Icon::Atom, - "Run orchestrated agents", + &i18n::t("ai.execution_profiles.editor.permission.run_orchestrated_agents"), &view.run_agents_dropdown, - profile_data.run_agents.description(), + &run_agents_permission_description(profile_data.run_agents), !ai_settings.is_run_agents_permissions_editable(app), view.tooltip_mouse_state_handles .run_agents_tooltip_mouse_state @@ -562,9 +652,9 @@ pub fn render_permissions_section( column.add_child(render_permission_row( appearance, Icon::Dataflow, - "Call MCP servers", + &i18n::t("ai.execution_profiles.editor.permission.call_mcp_servers"), &view.call_mcp_servers_dropdown, - profile_data.mcp_permissions.description(), + &action_permission_description(profile_data.mcp_permissions), !ai_settings.is_mcp_permission_editable(app), // Use MCP override for this permission view.tooltip_mouse_state_handles .call_mcp_servers_tooltip_mouse_state @@ -709,8 +799,8 @@ fn render_directory_allowlist_section( let is_editable = ai_settings.is_directory_allowlist_editable(app); render_list_section( - "Directory allowlist", - "Give the agent file access to certain directories.", + &i18n::t("ai.execution_profiles.editor.directory_allowlist.label"), + &i18n::t("ai.execution_profiles.editor.directory_allowlist.desc"), &profile_data.directory_allowlist, &view.directory_allowlist_mouse_state_handles, Some(&view.directory_allowlist_editor), @@ -734,8 +824,8 @@ fn render_command_allowlist_section( let is_editable = ai_settings.is_command_allowlist_editable(app); render_list_section( - "Command allowlist", - "Regular expressions to match commands that can be automatically executed by Oz.", + &i18n::t("ai.execution_profiles.editor.command_allowlist.label"), + &i18n::t("ai.execution_profiles.editor.command_allowlist.desc"), &profile_data.command_allowlist, &view.command_allowlist_mouse_state_handles, Some(&view.command_allowlist_editor), @@ -800,8 +890,8 @@ fn render_command_denylist_section( ); let mut column = Flex::column().with_child(create_section_header( - "Command denylist", - "Regular expressions to match commands that Oz should always ask permission to execute.", + &i18n::t("ai.execution_profiles.editor.command_denylist.label"), + &i18n::t("ai.execution_profiles.editor.command_denylist.desc"), appearance, )); column = column.with_child(list); @@ -814,7 +904,10 @@ fn render_command_denylist_section( fn display_mcp_name(uuid: &Uuid, app: &AppContext) -> String { TemplatableMCPServerManager::get_mcp_name(uuid, app).unwrap_or({ log::warn!("Expected a name for MCP server {uuid} but could not find one."); - format!("MCP Server {uuid}") + format!( + "{} {uuid}", + i18n::t("ai.execution_profiles.editor.mcp_server_fallback") + ) }) } @@ -828,8 +921,8 @@ fn render_mcp_allowlist_section( let is_editable = ai_settings.is_mcp_permission_editable(app); render_list_section( - "MCP allowlist", - "MCP servers that are allowed to be called by Oz.", + &i18n::t("ai.execution_profiles.editor.mcp_allowlist.label"), + &i18n::t("ai.execution_profiles.editor.mcp_allowlist.desc"), &profile_data.mcp_allowlist, &view.mcp_allowlist_mouse_state_handles, None, @@ -854,8 +947,8 @@ fn render_mcp_denylist_section( let is_editable = ai_settings.is_mcp_permission_editable(app); render_list_section( - "MCP denylist", - "MCP servers that are not allowed to be called by Oz.", + &i18n::t("ai.execution_profiles.editor.mcp_denylist.label"), + &i18n::t("ai.execution_profiles.editor.mcp_denylist.desc"), &profile_data.mcp_denylist, &view.mcp_denylist_mouse_state_handles, None, @@ -889,7 +982,7 @@ pub fn render_plan_auto_sync_toggle( .finish(); let label_elem = Text::new( - "Plan auto-sync".to_string(), + i18n::t("ai.execution_profiles.editor.plan_auto_sync.label"), appearance.ui_font_family(), 13., ) @@ -897,8 +990,7 @@ pub fn render_plan_auto_sync_toggle( .finish(); let desc_elem = Text::new( - "The plans this agent creates will be automatically added and synced to Warp Drive." - .to_string(), + i18n::t("ai.execution_profiles.editor.plan_auto_sync.desc"), appearance.ui_font_family(), 11., ) @@ -963,7 +1055,7 @@ pub fn render_web_search_toggle( .finish(); let label_elem = Text::new( - "Call web tools".to_string(), + i18n::t("ai.execution_profiles.editor.web_search.label"), appearance.ui_font_family(), 13., ) @@ -971,7 +1063,7 @@ pub fn render_web_search_toggle( .finish(); let desc_elem = Text::new( - "The agent may use web search when helpful for completing tasks.".to_string(), + i18n::t("ai.execution_profiles.editor.web_search.desc"), appearance.ui_font_family(), 11., ) @@ -1027,7 +1119,9 @@ pub fn wrap_disabled_with_workspace_override_tooltip( if state.is_hovered() { let tooltip = appearance .ui_builder() - .tool_tip(WORKSPACE_OVERRIDE_TOOLTIP_MESSAGE.to_string()) + .tool_tip(i18n::t( + "ai.execution_profiles.editor.workspace_override_tooltip", + )) .build() .finish(); diff --git a/app/src/ai/facts/view/mod.rs b/app/src/ai/facts/view/mod.rs index 6017e1895c..9f2ab762b3 100644 --- a/app/src/ai/facts/view/mod.rs +++ b/app/src/ai/facts/view/mod.rs @@ -31,8 +31,6 @@ mod style; use rule::*; use rule_editor::*; -const OFFLINE_TEXT: &str = "You are offline. Some rules will be read only."; - #[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] pub enum AIFactPage { #[default] @@ -76,7 +74,8 @@ pub struct AIFactView { impl AIFactView { pub fn new(ctx: &mut ViewContext) -> Self { - let pane_configuration = ctx.add_model(|_ctx| PaneConfiguration::new(HEADER_TEXT)); + let pane_configuration = + ctx.add_model(|_ctx| PaneConfiguration::new(i18n::t("ai.rules.header"))); let rule_view = ctx.add_typed_action_view(RuleView::new); ctx.subscribe_to_view(&rule_view, |me, _, event, ctx| { @@ -209,7 +208,7 @@ impl AIFactView { Container::new( appearance .ui_builder() - .wrappable_text(OFFLINE_TEXT, true) + .wrappable_text(i18n::t("ai.rules.offline_banner"), true) .build() .finish(), ) @@ -325,7 +324,7 @@ impl BackingView for AIFactView { _ctx: &view::HeaderRenderContext<'_>, _app: &AppContext, ) -> view::HeaderContent { - view::HeaderContent::simple(HEADER_TEXT) + view::HeaderContent::simple(i18n::t("ai.rules.header")) } fn set_focus_handle(&mut self, focus_handle: PaneFocusHandle, _ctx: &mut ViewContext) { diff --git a/app/src/ai/facts/view/rule.rs b/app/src/ai/facts/view/rule.rs index 4f959c2984..dd18ec8749 100644 --- a/app/src/ai/facts/view/rule.rs +++ b/app/src/ai/facts/view/rule.rs @@ -43,20 +43,6 @@ use crate::view_components::DismissibleToast; use crate::workspace::ToastStack; use crate::workspaces::user_workspaces::UserWorkspaces; -pub const HEADER_TEXT: &str = "Rules"; -const DESCRIPTION_TEXT: &str = "Rules enhance the agent by providing structured guidelines that help maintain consistency, enforce best practices, and adapt to specific workflows, including codebases or broader tasks."; - -const SEARCH_PLACEHOLDER_TEXT: &str = "Search rules"; -const ZERO_STATE_TEXT: &str = - "Add a rule above, or drop one at ~/.agents/AGENTS.md to apply it across every project."; -const ZERO_STATE_TEXT_PROJECT: &str = - "Once you generate a WARP.md rules file for a project, it will appear here."; - -const DISABLED_BANNER_TEXT: &str = - "Your rules are disabled and won't be used as context in sessions. You can "; -const DISABLED_BANNER_LINK_TEXT: &str = "turn it back on"; -const DISABLED_BANNER_TEXT_2: &str = " anytime."; - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RuleScope { Global, @@ -284,18 +270,18 @@ impl RuleView { search_editor.update(ctx, |editor, ctx| { editor.clear_buffer_and_reset_undo_stack(ctx); - editor.set_placeholder_text(SEARCH_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text(i18n::t("ai.rules.search_placeholder"), ctx); }); let search_bar = ctx.add_typed_action_view(|_| SearchBar::new(search_editor.clone())); let add_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Add", NakedTheme) + ActionButton::new(i18n::t("common.add"), NakedTheme) .with_icon(Icon::Plus) .on_click(|ctx| ctx.dispatch_typed_action(RuleViewAction::AddRule)) }); let initialize_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Initialize Project", NakedTheme) + ActionButton::new(i18n::t("ai.rules.initialize_project"), NakedTheme) .with_icon(Icon::Plus) .on_click(|ctx| ctx.dispatch_typed_action(RuleViewAction::InitializeProject)) }); @@ -478,7 +464,7 @@ impl RuleView { .with_child( appearance .ui_builder() - .wrappable_text(HEADER_TEXT, true) + .wrappable_text(i18n::t("ai.rules.header"), true) .with_style(style::header_text()) .build() .finish(), @@ -490,7 +476,7 @@ impl RuleView { Container::new( appearance .ui_builder() - .wrappable_text(DESCRIPTION_TEXT, true) + .wrappable_text(i18n::t("ai.rules.description"), true) .with_style(style::description_text(appearance)) .build() .finish(), @@ -500,16 +486,18 @@ impl RuleView { } fn render_scope_tabs(&self, appearance: &Appearance) -> Box { + let global_label = i18n::t("ai.rules.scope.global"); let global_tab = Container::new(self.render_scope_tab( - "Global", + &global_label, RuleScope::Global, appearance, self.global_tab_mouse_state.clone(), )) .with_padding_right(4.) .finish(); + let project_based_label = i18n::t("ai.rules.scope.project_based"); let project_tab = self.render_scope_tab( - "Project based", + &project_based_label, RuleScope::ProjectBased, appearance, self.project_tab_mouse_state.clone(), @@ -597,14 +585,17 @@ impl RuleView { } fn render_disabled_banner(&self, appearance: &Appearance) -> Box { - let mut link = FormattedTextFragment::hyperlink(DISABLED_BANNER_LINK_TEXT, "Settings > AI"); + let mut link = FormattedTextFragment::hyperlink( + i18n::t("ai.rules.disabled_banner_link"), + "Settings > AI", + ); link.styles.weight = Some(CustomWeight::Bold); let formatted_text = FormattedTextElement::new( FormattedText::new([FormattedTextLine::Line(vec![ - FormattedTextFragment::bold(DISABLED_BANNER_TEXT), + FormattedTextFragment::bold(i18n::t("ai.rules.disabled_banner_prefix")), link, - FormattedTextFragment::bold(DISABLED_BANNER_TEXT_2), + FormattedTextFragment::bold(i18n::t("ai.rules.disabled_banner_suffix")), ])]), style::SUBTEXT_FONT_SIZE, appearance.ui_font_family(), @@ -729,7 +720,7 @@ impl RuleView { appearance .ui_builder() .button(ButtonVariant::Outlined, project_row.mouse_state.clone()) - .with_text_label("Open file".to_string()) + .with_text_label(i18n::t("common.open_file")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(RuleViewAction::OpenFile(file_path.clone())); @@ -763,12 +754,12 @@ impl RuleView { let formatted_name = match name { Some(name) => { if name.is_empty() { - "Untitled".to_string() + i18n::t("common.untitled") } else { name } } - None => "Untitled".to_string(), + None => i18n::t("common.untitled"), }; // Truncate content to 3 lines let formatted_content = if content.split("\n").count() > 3 { @@ -880,8 +871,8 @@ impl RuleView { fn render_zero_state(&self, appearance: &Appearance) -> Box { let text = match self.current_scope { - RuleScope::Global => ZERO_STATE_TEXT, - RuleScope::ProjectBased => ZERO_STATE_TEXT_PROJECT, + RuleScope::Global => i18n::t("ai.rules.zero_state.global"), + RuleScope::ProjectBased => i18n::t("ai.rules.zero_state.project_based"), }; let centered_text = appearance diff --git a/app/src/ai/facts/view/rule_editor.rs b/app/src/ai/facts/view/rule_editor.rs index 60bbc7a436..2e3cfc3f91 100644 --- a/app/src/ai/facts/view/rule_editor.rs +++ b/app/src/ai/facts/view/rule_editor.rs @@ -28,9 +28,6 @@ use crate::ui_components::buttons::icon_button; use crate::ui_components::icons::Icon; use crate::view_components::action_button::{ActionButton, DangerSecondaryTheme, PrimaryTheme}; -const RULE_NAME_PLACEHOLDER_TEXT: &str = "e.g. Rust rules"; -const RULE_DESCRIPTION_PLACEHOLDER_TEXT: &str = "e.g. Never use unwrap in Rust"; - #[derive(Debug, Clone, Copy)] enum EditorType { Name, @@ -99,7 +96,7 @@ impl RuleEditorView { }, ctx, ); - editor.set_placeholder_text(RULE_NAME_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text(i18n::t("ai.rules.name_placeholder"), ctx); editor }); ctx.subscribe_to_view(&name_editor, |me, _editor, event, ctx| { @@ -126,7 +123,7 @@ impl RuleEditorView { }, ctx, ); - editor.set_placeholder_text(RULE_DESCRIPTION_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text(i18n::t("ai.rules.description_placeholder"), ctx); editor }); ctx.subscribe_to_view(&content_editor, |me, _editor, event, ctx| { @@ -134,7 +131,7 @@ impl RuleEditorView { }); let save_button = ctx.add_typed_action_view(|ctx| { - let mut button = ActionButton::new("Save", PrimaryTheme) + let mut button = ActionButton::new(i18n::t("common.save"), PrimaryTheme) .with_icon(Icon::Check) .on_click(|ctx| { ctx.dispatch_typed_action(RuleEditorViewAction::Save); @@ -145,7 +142,7 @@ impl RuleEditorView { }); let delete_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Delete rule", DangerSecondaryTheme) + ActionButton::new(i18n::t("ai.rules.delete_rule"), DangerSecondaryTheme) .with_icon(Icon::Trash) .on_click(|ctx| { ctx.dispatch_typed_action(RuleEditorViewAction::Delete); @@ -258,9 +255,9 @@ impl RuleEditorView { fn render_header(&self, appearance: &Appearance) -> Box { let title = if self.ai_fact.is_none() { - "Add Rule" + i18n::t("ai.rules.add_rule_title") } else { - "Edit Rule" + i18n::t("ai.rules.edit_rule_title") }; Container::new( Flex::row() @@ -333,15 +330,27 @@ impl RuleEditorView { fn render_form(&self, appearance: &Appearance) -> Box { Flex::column() .with_child( - Container::new(appearance.ui_builder().span("Name").build().finish()) - .with_margin_bottom(style::ITEM_BOTTOM_MARGIN) - .finish(), + Container::new( + appearance + .ui_builder() + .span(i18n::t("common.name")) + .build() + .finish(), + ) + .with_margin_bottom(style::ITEM_BOTTOM_MARGIN) + .finish(), ) .with_child(self.render_name_editor(appearance)) .with_child( - Container::new(appearance.ui_builder().span("Rule").build().finish()) - .with_margin_bottom(style::ITEM_BOTTOM_MARGIN) - .finish(), + Container::new( + appearance + .ui_builder() + .span(i18n::t("ai.rules.rule")) + .build() + .finish(), + ) + .with_margin_bottom(style::ITEM_BOTTOM_MARGIN) + .finish(), ) .with_child(self.render_content_editor(appearance)) .finish() diff --git a/app/src/ai/llms.rs b/app/src/ai/llms.rs index be3f8967bb..167afbe535 100644 --- a/app/src/ai/llms.rs +++ b/app/src/ai/llms.rs @@ -46,7 +46,10 @@ pub fn should_show_bedrock_icon_for_model(llm: &LLMInfo, app: &AppContext) -> bo /// Note: this key used to store a single [`AvailableLLMs`] /// but was migrated to store a full [`ModelsByFeature`]. pub const MODELS_BY_FEATURE_CACHE_KEY: &str = "AvailableLLMs"; -const CUSTOM_ENDPOINT_USAGE_FALLBACK_LABEL: &str = "Custom endpoint"; + +fn custom_endpoint_usage_fallback_label() -> String { + i18n::t("ai.llms.custom_endpoint") +} #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct LLMUsageMetadata { @@ -65,15 +68,13 @@ pub enum DisableReason { impl DisableReason { /// Returns a user-facing tooltip explaining why the model is disabled. - pub fn tooltip_text(&self) -> &'static str { + pub fn tooltip_text(&self) -> String { match self { - DisableReason::AdminDisabled => "This model has been disabled by your team admin.", - DisableReason::OutOfRequests => "Please upgrade your plan to make more requests.", - DisableReason::ProviderOutage => { - "This model is temporarily unavailable due to a provider outage." - } - DisableReason::RequiresUpgrade => "Please upgrade your plan to access this model.", - DisableReason::Unavailable => "This model is unavailable.", + DisableReason::AdminDisabled => i18n::t("ai.model.disable_reason.admin_disabled"), + DisableReason::OutOfRequests => i18n::t("ai.model.disable_reason.out_of_requests"), + DisableReason::ProviderOutage => i18n::t("ai.model.disable_reason.provider_outage"), + DisableReason::RequiresUpgrade => i18n::t("ai.model.disable_reason.requires_upgrade"), + DisableReason::Unavailable => i18n::t("ai.model.disable_reason.unavailable"), } } @@ -827,7 +828,7 @@ impl LLMPreferences { self.custom_llm_info_for_id(&config_key) .map(|info| info.display_name.as_str()) .map(str::to_string) - .unwrap_or_else(|| CUSTOM_ENDPOINT_USAGE_FALLBACK_LABEL.to_string()) + .unwrap_or_else(custom_endpoint_usage_fallback_label) } fn custom_llm_info_for_id_if_enabled(&self, id: &LLMId, app: &AppContext) -> Option<&LLMInfo> { diff --git a/app/src/ai/llms_tests.rs b/app/src/ai/llms_tests.rs index 18b2a4b90e..f16c2b8b4e 100644 --- a/app/src/ai/llms_tests.rs +++ b/app/src/ai/llms_tests.rs @@ -254,7 +254,7 @@ fn custom_endpoint_usage_display_label_resolves_alias_name_and_generic_fallback( ); assert_eq!( preferences.custom_endpoint_usage_display_label("unknown"), - CUSTOM_ENDPOINT_USAGE_FALLBACK_LABEL + i18n::t("ai.llms.custom_endpoint") ); } diff --git a/app/src/ai/mcp/templatable_manager/native.rs b/app/src/ai/mcp/templatable_manager/native.rs index 64e50a67f8..c47ae2f52e 100644 --- a/app/src/ai/mcp/templatable_manager/native.rs +++ b/app/src/ai/mcp/templatable_manager/native.rs @@ -117,46 +117,51 @@ impl fmt::Display for LegacyToTemplatableMCPConversionError { fn error_to_user_message(error: &rmcp::RmcpError) -> String { match error { rmcp::RmcpError::ClientInitialize(err) => { - format!("Failed to initialize client: {}", err) + i18n::t("ai.mcp.templatable_manager.error.client_initialize") + .replace("{error}", &err.to_string()) } rmcp::RmcpError::ServerInitialize(err) => { - format!("Failed to initialize server: {}", err) + i18n::t("ai.mcp.templatable_manager.error.server_initialize") + .replace("{error}", &err.to_string()) } rmcp::RmcpError::TransportCreation { error, .. } => { - format!("Failed to establish connection: {}", error) + i18n::t("ai.mcp.templatable_manager.error.transport_creation") + .replace("{error}", &error.to_string()) } rmcp::RmcpError::Runtime(err) => { - format!("Runtime error: {}", err) + i18n::t("ai.mcp.templatable_manager.error.runtime").replace("{error}", &err.to_string()) } rmcp::RmcpError::Service(err) => match err { rmcp::ServiceError::McpError(_) => { - "Server returned an error. Please check server logs for details.".to_string() + i18n::t("ai.mcp.templatable_manager.error.server_returned_error") } rmcp::ServiceError::TransportSend(_) => { - "Failed to send data to server. Connection may have been lost.".to_string() + i18n::t("ai.mcp.templatable_manager.error.transport_send") } rmcp::ServiceError::TransportClosed => { - "Connection closed unexpectedly. The server may have crashed.".to_string() + i18n::t("ai.mcp.templatable_manager.error.transport_closed") } rmcp::ServiceError::UnexpectedResponse => { - "Server sent an unexpected response. The server may be incompatible.".to_string() + i18n::t("ai.mcp.templatable_manager.error.unexpected_response") } - rmcp::ServiceError::Cancelled { reason } => format!( - "Operation was cancelled with reason: {}", - reason.clone().unwrap_or("Unknown reason".to_string()) - ), - rmcp::ServiceError::Timeout { timeout } => { - format!( - "Connection timed out after {} seconds. The server may be unresponsive.", - timeout.as_secs() + rmcp::ServiceError::Cancelled { reason } => { + i18n::t("ai.mcp.templatable_manager.error.cancelled").replace( + "{reason}", + &reason.clone().unwrap_or_else(|| { + i18n::t("ai.mcp.templatable_manager.error.unknown_reason") + }), ) } - _ => format!("Service error: {}", err), + rmcp::ServiceError::Timeout { timeout } => { + i18n::t("ai.mcp.templatable_manager.error.timeout") + .replace("{seconds}", &timeout.as_secs().to_string()) + } + _ => i18n::t("ai.mcp.templatable_manager.error.service") + .replace("{error}", &err.to_string()), }, // The enum is marked as non-exhaustive, so we need a catch-all. - _ => { - format!("Error: {error}") - } + _ => i18n::t("ai.mcp.templatable_manager.error.generic") + .replace("{error}", &error.to_string()), } } @@ -717,7 +722,9 @@ impl TemplatableMCPServerManager { if mode.is_reconnect() { self.notify_reconnect_waiters( installation_uuid, - Err("Template contains no servers".to_string()), + Err(i18n::t( + "ai.mcp.templatable_manager.error.template_contains_no_servers", + )), ); } return; @@ -731,7 +738,10 @@ impl TemplatableMCPServerManager { if mode.is_reconnect() { self.notify_reconnect_waiters( installation_uuid, - Err(format!("Failed to parse MCP server: {err:#}")), + Err( + i18n::t("ai.mcp.templatable_manager.error.parse_server_failed") + .replace("{error}", &format!("{err:#}")), + ), ); } return; @@ -753,10 +763,9 @@ impl TemplatableMCPServerManager { if let Some(window_id) = WindowManager::as_ref(ctx).active_window() { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "PATH required to launch MCP server. Please open a new terminal session to autopopulate PATH." - .to_string(), - ), + DismissibleToast::error(i18n::t( + "ai.mcp.path_required_to_launch_server", + )), window_id, ctx, ); @@ -766,7 +775,9 @@ impl TemplatableMCPServerManager { if mode.is_reconnect() { self.notify_reconnect_waiters( installation_uuid, - Err("PATH not available".to_string()), + Err(i18n::t( + "ai.mcp.templatable_manager.error.path_not_available", + )), ); } return; @@ -1600,7 +1611,9 @@ impl TemplatableMCPServerManager { else { self.notify_reconnect_waiters( installation_uuid, - Err("Installation not found".to_string()), + Err(i18n::t( + "ai.mcp.templatable_manager.error.installation_not_found", + )), ); return; }; @@ -1990,7 +2003,9 @@ async fn determine_transport( fn unexpected_error(status: reqwest::StatusCode) -> rmcp::RmcpError { rmcp::RmcpError::transport_creation::(format!( - "Unexpected status code: {status}" + "{}", + i18n::t("ai.mcp.templatable_manager.error.unexpected_status_code") + .replace("{status}", &status.to_string()) )) } match send_initialize_request(url, headers, None).await? { @@ -1999,7 +2014,7 @@ async fn determine_transport( StatusCode::UNAUTHORIZED => { if !FeatureFlag::McpOauth.is_enabled() { return Err(rmcp::RmcpError::transport_creation::( - "Server requires authentication, which is not yet supported.".to_string(), + i18n::t("ai.mcp.templatable_manager.error.auth_not_supported"), )); } @@ -2023,9 +2038,10 @@ async fn determine_transport( if let Some(active_window_id) = ctx.windows().active_window() { ToastStack::handle(ctx).update(ctx, |stack, ctx| { stack.add_ephemeral_toast( - DismissibleToast::default(format!( - "Successfully authenticated {server_name} MCP server" - )), + DismissibleToast::default( + i18n::t("ai.mcp.templatable_manager.auth_success_toast") + .replace("{server_name}", &server_name), + ), active_window_id, ctx, ); diff --git a/app/src/ai_assistant/mod.rs b/app/src/ai_assistant/mod.rs index b2cf1e70bc..cb9f0d8d53 100644 --- a/app/src/ai_assistant/mod.rs +++ b/app/src/ai_assistant/mod.rs @@ -33,7 +33,6 @@ mod test_util; pub const PROMPT_CHARACTER_LIMIT: usize = 1000; pub const AI_ASSISTANT_FEATURE_NAME: &str = "Warp AI"; -pub const ASK_AI_ASSISTANT_TEXT: &str = "Ask Warp AI"; pub const AI_ASSISTANT_SVG_PATH: &str = "bundled/svg/ai-assistant.svg"; diff --git a/app/src/ai_assistant/panel.rs b/app/src/ai_assistant/panel.rs index 8b40907555..66996deaed 100644 --- a/app/src/ai_assistant/panel.rs +++ b/app/src/ai_assistant/panel.rs @@ -30,7 +30,7 @@ use super::transcript::{Transcript, TranscriptEvent}; use super::utils::{render_prepared_response_button, render_request_limit_info, TranscriptPart}; use super::{ AskAIType, AI_ASSISTANT_FEATURE_NAME, AI_ASSISTANT_LOGO_COLOR, AI_ASSISTANT_SVG_PATH, - ASK_AI_ASSISTANT_TEXT, PROMPT_CHARACTER_LIMIT, + PROMPT_CHARACTER_LIMIT, }; use crate::appearance::Appearance; use crate::editor::{ @@ -68,16 +68,6 @@ const BODY_FONT_SIZE: f32 = 13.; const TITLE_FONT_SIZE: f32 = 16.; const ZERO_STATE_HELP_TEXT_FONT_SIZE: f32 = 12.; -const ZERO_STATE_HELP_TEXT: &str = "Shift + ctrl + space a block or text selection to ask Warp AI."; -const SCRIPT_ZERO_STATE_PROMPT: &str = "Write a script to connect to an AWS EC2 instance."; -const GIT_ZERO_STATE_PROMPT: &str = "How do I undo the most recent commits in git?"; -const FILES_ZERO_STATE_PROMPT: &str = "How do I find all files containing specific text?"; - -// The placeholder texts are prepended with a space to give them cushion from the cursor. -const INIT_PLACEHOLDER_TEXT: &str = " Ask a question..."; -const FOLLOWUP_PLACEHOLDER_TEXT: &str = " Type a response or click one above..."; -const RESTART_BUTTON_TEXT: &str = "Restart"; - const ASK_AI_BLOCK_INPUT_LIMIT: usize = 100; #[derive(Default)] @@ -131,7 +121,10 @@ pub enum AIAssistantAction { ClosePanel, ResetContext, CopyTranscript, - PreparedPrompt(&'static str), + PreparedPrompt { + prompt: String, + telemetry_prompt: &'static str, + }, ClickedUrl(HyperlinkUrl), CopyAnswerToClipboard(Arc), FocusTerminalInput, @@ -196,7 +189,10 @@ impl AIAssistantPanelView { }) }; editor.update(ctx, |editor, ctx| { - editor.set_placeholder_text(INIT_PLACEHOLDER_TEXT, ctx) + editor.set_placeholder_text( + format!(" {}", i18n::t("ai_assistant.placeholder.ask_question")), + ctx, + ) }); ctx.subscribe_to_view(&editor, |me, _, event, ctx| { me.handle_editor_event(event, ctx); @@ -563,7 +559,10 @@ impl AIAssistantPanelView { RequestsEvent::RequestFinished { .. } => { self.editor.update(ctx, |editor, ctx| { editor.clear_buffer_and_reset_undo_stack(ctx); - editor.set_placeholder_text(FOLLOWUP_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text( + format!(" {}", i18n::t("ai_assistant.placeholder.followup")), + ctx, + ); }); self.transcript_view.update(ctx, |transcript_view, ctx| { transcript_view.scroll_to_bottom_of_transcript(ctx); @@ -631,7 +630,10 @@ impl AIAssistantPanelView { } self.editor.update(ctx, |editor, ctx| { - editor.set_placeholder_text(INIT_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text( + format!(" {}", i18n::t("ai_assistant.placeholder.ask_question")), + ctx, + ); }); self.requests_model.update(ctx, |requests_model, ctx| { @@ -653,17 +655,20 @@ impl AIAssistantPanelView { let mut result = String::new(); let time_now = Local::now(); - result.push_str(&format!( - "## Warp AI Transcript ({})\n\n", - time_now.format("%x %l:%M %p") - )); + result.push_str( + &i18n::t("ai_assistant.transcript.title") + .replace("{time}", &time_now.format("%x %l:%M %p").to_string()), + ); for part in transcript { - result.push_str(&format!("Prompt: {}\n\n", part.raw_user_prompt().trim())); - result.push_str(&format!( - "Warp AI: {}\n\n", - part.raw_assistant_answer().trim() - )); + result.push_str( + &i18n::t("ai_assistant.transcript.prompt") + .replace("{text}", part.raw_user_prompt().trim()), + ); + result.push_str( + &i18n::t("ai_assistant.transcript.answer") + .replace("{text}", part.raw_assistant_answer().trim()), + ); } ctx.clipboard() @@ -781,7 +786,7 @@ impl AIAssistantPanelView { ..Default::default() }; ui_builder - .tool_tip("Copy transcript to clipboard".to_owned()) + .tool_tip(i18n::t("ai_assistant.copy_transcript_tooltip")) .with_style(tool_tip_style) .build() .finish() @@ -823,7 +828,7 @@ impl AIAssistantPanelView { Some(hover_style), Some(hover_style), ) - .with_text_label(RESTART_BUTTON_TEXT.to_owned()) + .with_text_label(i18n::t("common.restart")) .build() .on_click(move |ctx, _, _| ctx.dispatch_typed_action(AIAssistantAction::ResetContext)) .with_cursor(Cursor::PointingHand) @@ -839,7 +844,7 @@ impl AIAssistantPanelView { .with_children([ Container::new( Text::new_inline( - "Character limit exceeded.", + i18n::t("ai_assistant.character_limit_exceeded"), appearance.ui_font_family(), BODY_FONT_SIZE, ) @@ -903,9 +908,13 @@ impl AIAssistantPanelView { ) .with_child( Container::new( - Text::new_inline(ASK_AI_ASSISTANT_TEXT, appearance.ui_font_family(), 14.) - .with_color(sub_text_color) - .finish(), + Text::new_inline( + i18n::t("ai_assistant.ask_warp_ai"), + appearance.ui_font_family(), + 14., + ) + .with_color(sub_text_color) + .finish(), ) .with_margin_top(8.) .finish(), @@ -918,7 +927,8 @@ impl AIAssistantPanelView { self.mouse_state_handles.git_zero_state_prompt.clone(), Some(300.), None, - GIT_ZERO_STATE_PROMPT, + i18n::t("ai_assistant.prompt.undo_recent_commits"), + "How do I undo the most recent commits in git?", )) .with_margin_top(20.) .with_margin_bottom(10.) @@ -928,7 +938,8 @@ impl AIAssistantPanelView { self.mouse_state_handles.files_zero_state_prompt.clone(), Some(300.), None, - FILES_ZERO_STATE_PROMPT, + i18n::t("ai_assistant.prompt.find_files_containing_text"), + "How do I find all files containing specific text?", )) .with_margin_bottom(10.) .finish(), @@ -937,7 +948,8 @@ impl AIAssistantPanelView { self.mouse_state_handles.script_zero_state_prompt.clone(), Some(300.), None, - SCRIPT_ZERO_STATE_PROMPT, + i18n::t("ai_assistant.prompt.connect_aws_ec2"), + "Write a script to connect to an AWS EC2 instance.", )) .finish(), ]); @@ -966,7 +978,7 @@ impl AIAssistantPanelView { 1., appearance .ui_builder() - .wrappable_text(ZERO_STATE_HELP_TEXT.to_string(), true) + .wrappable_text(i18n::t("ai_assistant.zero_state_help"), true) .with_style(UiComponentStyles { font_family_id: Some(appearance.ui_font_family()), font_size: Some(ZERO_STATE_HELP_TEXT_FONT_SIZE), @@ -1040,9 +1052,17 @@ impl TypedActionView for AIAssistantPanelView { ClosePanel => { ctx.emit(AIAssistantPanelEvent::ClosePanel); } - PreparedPrompt(prompt) => { - self.issue_request(prompt.to_string(), ctx); - send_telemetry_from_ctx!(TelemetryEvent::UsedWarpAIPreparedPrompt { prompt }, ctx); + PreparedPrompt { + prompt, + telemetry_prompt, + } => { + self.issue_request(prompt.clone(), ctx); + send_telemetry_from_ctx!( + TelemetryEvent::UsedWarpAIPreparedPrompt { + prompt: telemetry_prompt + }, + ctx + ); } ClickedUrl(url) => { ctx.open_url(&url.url); diff --git a/app/src/ai_assistant/requests.rs b/app/src/ai_assistant/requests.rs index 084e9dc092..e00a577e85 100644 --- a/app/src/ai_assistant/requests.rs +++ b/app/src/ai_assistant/requests.rs @@ -240,9 +240,10 @@ impl Requests { cache_request_limit_info(request_limit_info, ctx); model.request_limit_info = request_limit_info; let next_time = if let Some(next_refresh_time) = model.serialized_time_until_refresh() { - format!("after {next_refresh_time}") + i18n::t("ai_assistant.requests.retry_after") + .replace("{time}", &next_refresh_time) } else { - String::from("later") + i18n::t("ai_assistant.requests.retry_later") }; let auth_state = AuthStateProvider::as_ref(ctx).get(); @@ -252,17 +253,23 @@ impl Requests { if team.billing_metadata.can_upgrade_to_higher_tier_plan() { if has_admin_permissions { let upgrade_url = UserWorkspaces::upgrade_link_for_team(team.uid); - format!("It seems you're out of credits. Please try again {next_time}.\n\n[Upgrade]({upgrade_url}) for more credits.") + i18n::t("ai_assistant.requests.out_of_credits_upgrade") + .replace("{next_time}", &next_time) + .replace("{upgrade_url}", &upgrade_url) } else { - format!("It seems you're out of credits. Please try again {next_time}.\n\nContact a team admin to upgrade for more credits.") + i18n::t("ai_assistant.requests.out_of_credits_contact_admin") + .replace("{next_time}", &next_time) } } else { - format!("It seems you're out of credits. Please try again {next_time}.") + i18n::t("ai_assistant.requests.out_of_credits") + .replace("{next_time}", &next_time) } } else { let user_id = auth_state.user_id().unwrap_or_default(); let upgrade_url = UserWorkspaces::upgrade_link(user_id); - format!("It seems you're out of credits. Please try again {next_time}.\n\n[Upgrade]({upgrade_url}) for more credits.") + i18n::t("ai_assistant.requests.out_of_credits_upgrade") + .replace("{next_time}", &next_time) + .replace("{upgrade_url}", &upgrade_url) }; let response_in_markdown = markdown_segments_from_text( transcript_part_index, @@ -400,11 +407,26 @@ impl Requests { let num_hours = num_minutes / 60; let num_days = num_hours / 24; let remaining_text = if num_days > 0 { - format!("{num_days} days") + if num_days == 1 { + i18n::t("ai_assistant.requests.duration.day") + } else { + i18n::t("ai_assistant.requests.duration.days") + .replace("{count}", &num_days.to_string()) + } } else if num_hours > 0 { - format!("{num_hours} hours") + if num_hours == 1 { + i18n::t("ai_assistant.requests.duration.hour") + } else { + i18n::t("ai_assistant.requests.duration.hours") + .replace("{count}", &num_hours.to_string()) + } } else { - format!("{num_minutes} minutes") + if num_minutes == 1 { + i18n::t("ai_assistant.requests.duration.minute") + } else { + i18n::t("ai_assistant.requests.duration.minutes") + .replace("{count}", &num_minutes.to_string()) + } }; Some(remaining_text) } diff --git a/app/src/ai_assistant/transcript.rs b/app/src/ai_assistant/transcript.rs index 064ccecd89..b27a91d99f 100644 --- a/app/src/ai_assistant/transcript.rs +++ b/app/src/ai_assistant/transcript.rs @@ -51,14 +51,6 @@ const COPY_BUTTON_SIZE: f32 = 14.; const TERMINAL_INPUT_BUTTON_SIZE: f32 = 20.; const SAVE_AS_WORKFLOW_BUTTON_SIZE: f32 = 20.; -const HOW_DO_I_FIX_PROMPT: &str = "How do I fix this?"; -const SHOW_EXAMPLES_PROMPT: &str = "Show examples."; -const WHAT_TO_DO_NEXT_PROMPT: &str = "What should I do next?"; -const IN_FLIGHT_REQUEST_TEXT: &str = "Generating answer..."; -const ACCURACY_NOTICE_TEXT: &str = "AI responses can be inaccurate."; -const MISSING_CONTEXT_NOTICE_TEXT: &str = - "Warp AI might forget earlier answers as conversations get long."; - lazy_static::lazy_static! { static ref SCROLL_BUFFER_OFFSET_PX: Pixels = (10.).into_pixels(); } @@ -428,7 +420,7 @@ impl Transcript { .finish(); buttons.add_child(appearance.ui_builder().tool_tip_on_element( - "Copy code to clipboard [Cmd + C]".to_string(), + i18n::t("ai_assistant.transcript.copy_code_tooltip"), mouse_state_handles.copy_button_tooltip.clone(), copy_button, ParentAnchor::TopRight, @@ -463,7 +455,7 @@ impl Transcript { buttons.add_child( Container::new(appearance.ui_builder().tool_tip_on_element( - "Insert code into terminal input [Cmd + Enter]".to_string(), + i18n::t("ai_assistant.transcript.insert_code_tooltip"), mouse_state_handles.play_button_tooltip.clone(), insert_button, ParentAnchor::TopRight, @@ -498,7 +490,7 @@ impl Transcript { buttons.add_child( SavePosition::new( Container::new(appearance.ui_builder().tool_tip_on_element( - "Save as workflow [Cmd + S]".to_string(), + i18n::t("ai_assistant.transcript.save_as_workflow_tooltip"), mouse_state_handles.save_as_workflow_button_tooltip.clone(), save_as_workflow_button, ParentAnchor::TopRight, @@ -560,7 +552,7 @@ impl Transcript { .finish(); appearance.ui_builder().tool_tip_on_element( - "Copy answer to clipboard".to_string(), + i18n::t("ai_assistant.transcript.copy_answer_tooltip"), tooltip_handle, copy_button, ParentAnchor::TopRight, @@ -762,7 +754,8 @@ impl Transcript { self.mouse_state_handles.what_to_do_next_button.clone(), None, Some(8.), - WHAT_TO_DO_NEXT_PROMPT, + i18n::t("ai_assistant.prompt.what_to_do_next"), + "What should I do next?", )) .with_child( Container::new(render_prepared_response_button( @@ -770,7 +763,8 @@ impl Transcript { self.mouse_state_handles.show_examples_button.clone(), None, Some(8.), - SHOW_EXAMPLES_PROMPT, + i18n::t("ai_assistant.prompt.show_examples"), + "Show examples.", )) .with_margin_left(10.) .with_margin_right(10.) @@ -781,7 +775,8 @@ impl Transcript { self.mouse_state_handles.how_do_i_fix_button.clone(), None, Some(8.), - HOW_DO_I_FIX_PROMPT, + i18n::t("ai_assistant.prompt.how_do_i_fix_this"), + "How do I fix this?", )) .finish() } @@ -828,10 +823,11 @@ impl View for Transcript { blocks.add_child(self.render_user_prompt(request, appearance)); let transcript_part_index = transcript.len(); + let in_flight_request_text = i18n::t("ai_assistant.generating_answer"); let in_flight_request_markdown = markdown_segments_from_text( transcript_part_index, TranscriptPartSubType::Answer, - IN_FLIGHT_REQUEST_TEXT, + &in_flight_request_text, ); blocks.add_child(self.render_assistant_answer( transcript_part_index, @@ -840,7 +836,7 @@ impl View for Transcript { copy_all_tooltip_and_button_mouse_handles: None, formatted_message: FormattedTranscriptMessage { markdown: in_flight_request_markdown, - raw: IN_FLIGHT_REQUEST_TEXT.to_owned(), + raw: in_flight_request_text, }, }, appearance, @@ -881,7 +877,10 @@ impl View for Transcript { blocks.add_child( Container::new( - self.render_warning_message(ACCURACY_NOTICE_TEXT.to_string(), appearance), + self.render_warning_message( + i18n::t("ai_assistant.accuracy_notice"), + appearance, + ), ) .with_margin_top(DETAILS_BOTTOM_MARGIN) .with_margin_bottom(if current_transcript_summarized { @@ -895,7 +894,7 @@ impl View for Transcript { if current_transcript_summarized { blocks.add_child( Container::new(self.render_warning_message( - MISSING_CONTEXT_NOTICE_TEXT.to_string(), + i18n::t("ai_assistant.missing_context_notice"), appearance, )) .with_margin_bottom(DETAILS_BOTTOM_MARGIN) diff --git a/app/src/ai_assistant/utils.rs b/app/src/ai_assistant/utils.rs index df53a5b5c7..7e2f7d1d19 100644 --- a/app/src/ai_assistant/utils.rs +++ b/app/src/ai_assistant/utils.rs @@ -260,7 +260,8 @@ pub fn render_prepared_response_button( mouse_state_handle: MouseStateHandle, width: Option, right_left_padding: Option, - prompt: &'static str, + prompt: String, + telemetry_prompt: &'static str, ) -> Box { let theme = appearance.theme(); let default_button_styles = UiComponentStyles { @@ -299,11 +300,14 @@ pub fn render_prepared_response_button( Some(hovered_and_clicked_styles), Some(hovered_and_clicked_styles), ) - .with_centered_text_label(prompt.to_string()) + .with_centered_text_label(prompt.clone()) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { - ctx.dispatch_typed_action(AIAssistantAction::PreparedPrompt(prompt)) + ctx.dispatch_typed_action(AIAssistantAction::PreparedPrompt { + prompt: prompt.clone(), + telemetry_prompt, + }) }) .finish() } @@ -328,7 +332,9 @@ pub fn render_request_limit_info( .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Text::new_inline( - format!("Credits used: {num_requests_used} / {request_limit}.",), + i18n::t("ai_assistant.credits.used") + .replace("{used}", &num_requests_used.to_string()) + .replace("{limit}", &request_limit.to_string()), appearance.ui_font_family(), REQUEST_LIMIT_INFO_FONT_SIZE, ) @@ -369,7 +375,8 @@ pub fn render_request_limit_info( row.add_child( Container::new( Text::new_inline( - format!("{next_refresh_time} until refresh."), + i18n::t("ai_assistant.credits.until_refresh") + .replace("{time}", &next_refresh_time), appearance.ui_font_family(), REQUEST_LIMIT_INFO_FONT_SIZE, ) diff --git a/app/src/app_menus.rs b/app/src/app_menus.rs index d96ccbe843..0aaf311e81 100644 --- a/app/src/app_menus.rs +++ b/app/src/app_menus.rs @@ -39,27 +39,13 @@ use crate::{auth, report_if_error}; type CheckmarkStatusGetter = dyn 'static + Fn(&mut AppContext) -> bool; -const ENABLE_SHELL_DEBUG_MODE_MENU_ITEM_NAME: &str = - "Enable Shell Debug Mode (-x) for New Sessions"; -const DISABLE_SHELL_DEBUG_MODE_MENU_ITEM_NAME: &str = - "Disable Shell Debug Mode (-x) for New Sessions"; -const ENABLE_IN_BAND_GENERATORS_MENU_ITEM_NAME: &str = "Enable In-band Generators for New Sessions"; -const DISABLE_IN_BAND_GENERATORS_MENU_ITEM_NAME: &str = - "Disable in-band generators for new sessions"; -const ENABLE_PTY_RECORDING: &str = "Enable PTY Recording Mode (warp.pty.recording)"; -const DISABLE_PTY_RECORDING: &str = "Disable PTY Recording Mode (warp.pty.recording)"; -const SHOW_BOOTSTRAP_BLOCK_MENU_ITEM_NAME: &str = "Show Initialization Block"; -const HIDE_BOOTSTRAP_BLOCK_MENU_ITEM_NAME: &str = "Hide Initialization Block"; -const SHOW_IN_BAND_COMMAND_BLOCKS_MENU_ITEM_NAME: &str = "Show In-band Command Blocks"; -const HIDE_IN_BAND_COMMAND_BLOCKS_MENU_ITEM_NAME: &str = "Hide In-band Command Blocks"; -const SHOW_SSH_COMMAND_BLOCKS_MENU_ITEM_NAME: &str = "Show Warpified SSH Blocks"; -const HIDE_SSH_COMMAND_BLOCKS_MENU_ITEM_NAME: &str = "Hide Warpified SSH Blocks"; -const EXPORT_DEFAULT_SETTINGS_CSV_MENU_ITEM_NAME: &str = - "Export Default Settings as CSV to home dir"; - const SETTINGS_CSV_FILE_NAME: &str = "warp_default_settings.csv"; const MAX_RECENT_REPOS_IN_MENU: usize = 10; +fn menu_label(key: &str) -> String { + i18n::t(key) +} + /// Creates the root app menu bar pub fn menu_bar(ctx: &mut AppContext) -> MenuBar { MenuBar::new(vec![ @@ -81,9 +67,9 @@ pub fn menu_bar(ctx: &mut AppContext) -> MenuBar { // To create submenus, we could use MenuItem::Custom(CustomMenuItem::new_with_submenu(...)) pub fn dock_menu() -> Menu { Menu::new( - "New Window", + menu_label("app_menu.dock.new_window"), vec![MenuItem::Custom(CustomMenuItem::new( - "New Window", + &menu_label("app_menu.dock.new_window"), move |ctx| { ctx.dispatch_global_action("root_view:open_new", &()); ctx.dispatch_global_action("workspace:save_app", &()); @@ -99,11 +85,126 @@ fn custom_shortcut(action: CustomAction) -> Option { } fn default_name(action: CustomAction, ctx: &AppContext) -> String { - ctx.description_for_custom_action(action.into(), bindings::MAC_MENUS_CONTEXT) + let fallback = ctx + .description_for_custom_action(action.into(), bindings::MAC_MENUS_CONTEXT) .unwrap_or_else(|| { debug_assert!(false, "action should have a name: {action:?}"); "".into() - }) + }); + custom_action_menu_label(action, fallback) +} + +fn custom_action_menu_label(action: CustomAction, fallback: String) -> String { + match action { + CustomAction::AddCursorAbove => i18n::t("app_menu.action.add_cursor_above"), + CustomAction::AddCursorBelow => i18n::t("app_menu.action.add_cursor_below"), + CustomAction::AddNextOccurrence => i18n::t("app_menu.action.add_next_occurrence"), + CustomAction::AISearch => i18n::t("app_menu.action.ai_search"), + CustomAction::AttachSelectionAsAgentModeContext => { + i18n::t("app_menu.action.attach_selection_as_agent_context") + } + CustomAction::ActivateNextPane => i18n::t("app_menu.action.activate_next_pane"), + CustomAction::ActivatePreviousPane => i18n::t("app_menu.action.activate_previous_pane"), + CustomAction::ClearBlocks => i18n::t("app_menu.action.clear_blocks"), + CustomAction::ClearEditor => i18n::t("app_menu.action.clear_editor"), + CustomAction::CloseCurrentSession => i18n::t("app_menu.action.close_current_session"), + CustomAction::CloseOtherTabs => i18n::t("app_menu.action.close_other_tabs"), + CustomAction::CloseTab => i18n::t("app_menu.action.close_tab"), + CustomAction::CloseTabsRight => i18n::t("app_menu.action.close_tabs_right"), + CustomAction::CloseWindow => i18n::t("app_menu.action.close_window"), + CustomAction::CommandPalette => i18n::t("app_menu.action.command_palette"), + CustomAction::CommandSearch => i18n::t("app_menu.action.command_search"), + CustomAction::ConfigureKeybindings => i18n::t("app_menu.action.configure_keybindings"), + CustomAction::Copy => i18n::t("app_menu.action.copy"), + CustomAction::CopyBlock => i18n::t("app_menu.action.copy_block"), + CustomAction::CopyBlockCommand => i18n::t("app_menu.action.copy_block_command"), + CustomAction::CopyBlockOutput => i18n::t("app_menu.action.copy_block_output"), + CustomAction::CreateBlockPermalink => i18n::t("app_menu.action.create_block_permalink"), + CustomAction::Cut => i18n::t("app_menu.action.cut"), + CustomAction::CycleNextSession => i18n::t("app_menu.action.cycle_next_session"), + CustomAction::CyclePrevSession => i18n::t("app_menu.action.cycle_previous_session"), + CustomAction::DecreaseFontSize => i18n::t("app_menu.action.decrease_font_size"), + CustomAction::DecreaseZoom => i18n::t("app_menu.action.decrease_zoom"), + CustomAction::DisableSyncTerminalInputs => { + i18n::t("app_menu.action.disable_sync_terminal_inputs") + } + CustomAction::FilesPalette => i18n::t("app_menu.action.files_palette"), + CustomAction::Find => i18n::t("app_menu.action.find"), + CustomAction::FindWithinBlock => i18n::t("app_menu.action.find_within_block"), + CustomAction::FocusInput => i18n::t("app_menu.action.focus_input"), + CustomAction::GoToLine => i18n::t("app_menu.action.go_to_line"), + CustomAction::History => i18n::t("app_menu.action.history"), + CustomAction::IncreaseFontSize => i18n::t("app_menu.action.increase_font_size"), + CustomAction::IncreaseZoom => i18n::t("app_menu.action.increase_zoom"), + CustomAction::LaunchConfigPalette => i18n::t("app_menu.action.launch_config_palette"), + CustomAction::MoveTabLeft => i18n::t("app_menu.action.move_tab_left"), + CustomAction::MoveTabRight => i18n::t("app_menu.action.move_tab_right"), + CustomAction::NavigationPalette => i18n::t("app_menu.action.navigation_palette"), + CustomAction::NewAgentModePane => i18n::t("app_menu.action.new_agent_mode_pane"), + CustomAction::NewFile => i18n::t("app_menu.action.new_file"), + CustomAction::NewPersonalAIPrompt => i18n::t("app_menu.action.new_personal_ai_prompt"), + CustomAction::NewPersonalEnvVars => i18n::t("app_menu.action.new_personal_env_vars"), + CustomAction::NewPersonalNotebook => i18n::t("app_menu.action.new_personal_notebook"), + CustomAction::NewPersonalWorkflow => i18n::t("app_menu.action.new_personal_workflow"), + CustomAction::NewTeamAIPrompt => i18n::t("app_menu.action.new_team_ai_prompt"), + CustomAction::NewTeamEnvVars => i18n::t("app_menu.action.new_team_env_vars"), + CustomAction::NewTeamNotebook => i18n::t("app_menu.action.new_team_notebook"), + CustomAction::NewTeamWorkflow => i18n::t("app_menu.action.new_team_workflow"), + CustomAction::OpenAIFactCollection => i18n::t("app_menu.action.open_ai_fact_collection"), + CustomAction::OpenMCPServerCollection => { + i18n::t("app_menu.action.open_mcp_server_collection") + } + CustomAction::OpenRepository => i18n::t("app_menu.action.open_repository"), + CustomAction::OpenTeamSettings => i18n::t("app_menu.action.open_team_settings"), + CustomAction::Paste => i18n::t("app_menu.action.paste"), + CustomAction::Redo => i18n::t("app_menu.action.redo"), + CustomAction::ReferAFriend => i18n::t("app_menu.action.refer_a_friend"), + CustomAction::RenameTab => i18n::t("app_menu.action.rename_tab"), + CustomAction::ResetFontSize => i18n::t("app_menu.action.reset_font_size"), + CustomAction::ResetZoom => i18n::t("app_menu.action.reset_zoom"), + CustomAction::SaveCurrentConfig => i18n::t("app_menu.action.save_current_config"), + CustomAction::ScrollToBottomOfSelectedBlocks => { + i18n::t("app_menu.action.scroll_to_bottom_of_selected_blocks") + } + CustomAction::ScrollToTopOfSelectedBlocks => { + i18n::t("app_menu.action.scroll_to_top_of_selected_blocks") + } + CustomAction::SearchDrive => i18n::t("app_menu.action.search_drive"), + CustomAction::SelectAll => i18n::t("app_menu.action.select_all"), + CustomAction::SelectAllBlocks => i18n::t("app_menu.action.select_all_blocks"), + CustomAction::SelectBlockAbove => i18n::t("app_menu.action.select_block_above"), + CustomAction::SelectBlockBelow => i18n::t("app_menu.action.select_block_below"), + CustomAction::ShareCurrentSession => i18n::t("app_menu.action.share_current_session"), + CustomAction::SharePaneContents => i18n::t("app_menu.action.share_pane_contents"), + CustomAction::ShowAboutWarp => i18n::t("app_menu.action.show_about_warp"), + CustomAction::ShowAppearance => i18n::t("app_menu.action.show_appearance"), + CustomAction::ShowSettings => i18n::t("app_menu.action.show_settings"), + CustomAction::SplitPaneDown => i18n::t("app_menu.action.split_pane_down"), + CustomAction::SplitPaneLeft => i18n::t("app_menu.action.split_pane_left"), + CustomAction::SplitPaneRight => i18n::t("app_menu.action.split_pane_right"), + CustomAction::SplitPaneUp => i18n::t("app_menu.action.split_pane_up"), + CustomAction::ToggleBookmarkBlock => i18n::t("app_menu.action.toggle_bookmark_block"), + CustomAction::ToggleConversationListView => { + i18n::t("app_menu.action.toggle_conversation_list_view") + } + CustomAction::ToggleGlobalSearch => i18n::t("app_menu.action.toggle_global_search"), + CustomAction::ToggleKeybindingsPage => i18n::t("app_menu.action.toggle_keybindings_page"), + CustomAction::ToggleMaximizePane => i18n::t("app_menu.action.toggle_maximize_pane"), + CustomAction::ToggleProjectExplorer => i18n::t("app_menu.action.toggle_project_explorer"), + CustomAction::ToggleResourceCenter => i18n::t("app_menu.action.toggle_resource_center"), + CustomAction::ToggleSyncAllTerminalInputsInAllTabs => { + i18n::t("app_menu.action.toggle_sync_all_terminal_inputs") + } + CustomAction::ToggleSyncTerminalInputsInCurrentTab => { + i18n::t("app_menu.action.toggle_sync_current_tab_terminal_inputs") + } + CustomAction::ToggleWarpDrive => i18n::t("app_menu.action.toggle_warp_drive"), + CustomAction::Undo => i18n::t("app_menu.action.undo"), + CustomAction::ViewChangelog => i18n::t("app_menu.action.view_changelog"), + CustomAction::ViewSharedBlocks => i18n::t("app_menu.action.view_shared_blocks"), + CustomAction::Workflows => i18n::t("app_menu.action.workflows"), + _ => fallback, + } } fn non_updateable_custom_item(action: CustomAction, ctx: &AppContext) -> MenuItem { @@ -167,7 +268,7 @@ fn make_new_app_menu(ctx: &AppContext) -> Menu { ]; menu_items.push(MenuItem::Custom(CustomMenuItem::new_with_submenu( - "Preferences", + &menu_label("app_menu.app.preferences"), |_| (), no_updates, None, @@ -188,14 +289,14 @@ fn make_new_app_menu(ctx: &AppContext) -> Menu { menu_items.push(MenuItem::Separator); menu_items.push(link_menu_item( - "Privacy Policy...", + "app_menu.app.privacy_policy", links::PRIVACY_POLICY_URL.into(), )); let debug_menu_items = debug_menu_items(); if !debug_menu_items.is_empty() { menu_items.push(MenuItem::Custom(CustomMenuItem::new_with_submenu( - "Debug", + &menu_label("app_menu.app.debug"), |_| (), no_updates, None, @@ -209,7 +310,7 @@ fn make_new_app_menu(ctx: &AppContext) -> Menu { menu_items.push(MenuItem::Standard(StandardAction::ShowAllApps)); menu_items.push(MenuItem::Separator); menu_items.push(MenuItem::Custom(CustomMenuItem::new( - "Set Warp as Default Terminal", + &menu_label("app_menu.app.set_default_terminal"), move |ctx| { DefaultTerminal::handle(ctx).update(ctx, |default_terminal, ctx| { default_terminal.make_warp_default(ctx) @@ -229,7 +330,7 @@ fn make_new_app_menu(ctx: &AppContext) -> Menu { ))); menu_items.push(MenuItem::Separator); menu_items.push(MenuItem::Custom(CustomMenuItem::new( - "Log out", + &menu_label("app_menu.app.log_out"), auth::maybe_log_out, move |_, ctx| { let is_anonymous = AuthStateProvider::handle(ctx) @@ -253,7 +354,7 @@ fn make_new_file_menu(ctx: &AppContext) -> Menu { MenuItem::Separator, updateable_custom_item_without_checkmark(CustomAction::OpenRepository, ctx), MenuItem::Custom(CustomMenuItem::new_with_submenu( - "Open Recent", + &menu_label("app_menu.file.open_recent"), |_| (), |_props, ctx| { let recent_repos = generate_recent_repos_for_menu(ctx); @@ -271,7 +372,7 @@ fn make_new_file_menu(ctx: &AppContext) -> Menu { updateable_custom_item_without_checkmark(CustomAction::CloseWindow, ctx), ]); - Menu::new("File", file_menu_options) + Menu::new(menu_label("app_menu.top.file"), file_menu_options) } fn make_new_edit_menu(ctx: &AppContext) -> Menu { @@ -300,7 +401,7 @@ fn make_new_edit_menu(ctx: &AppContext) -> Menu { ]; let group_5 = vec![ MenuItem::Custom(CustomMenuItem::new( - "Use Warp's Prompt", + &menu_label("app_menu.edit.use_warp_prompt"), move |ctx| ctx.dispatch_global_action("app:toggle_user_ps1", &()), move |_props, ctx| MenuItemPropertyChanges { checked: Some( @@ -313,7 +414,7 @@ fn make_new_edit_menu(ctx: &AppContext) -> Menu { None, )), MenuItem::Custom(CustomMenuItem::new( - "Copy on Select within the Terminal", + &menu_label("app_menu.edit.copy_on_select"), move |ctx| { ctx.dispatch_global_action("app:toggle_copy_on_select", &()); }, @@ -339,7 +440,7 @@ fn make_new_edit_menu(ctx: &AppContext) -> Menu { edit_menu_items.push(MenuItem::Separator); edit_menu_items.push(MenuItem::Custom(CustomMenuItem::new_with_submenu( - "Synchronize Inputs", + &menu_label("app_menu.edit.synchronize_inputs"), |_| (), no_updates, None, @@ -371,7 +472,7 @@ fn make_new_edit_menu(ctx: &AppContext) -> Menu { edit_menu_items.extend(group_5); - Menu::new("Edit", edit_menu_items) + Menu::new(menu_label("app_menu.top.edit"), edit_menu_items) } fn make_new_view_menu(ctx: &AppContext) -> Menu { @@ -391,7 +492,7 @@ fn make_new_view_menu(ctx: &AppContext) -> Menu { updateable_custom_item_without_checkmark(CustomAction::Workflows, ctx), MenuItem::Separator, MenuItem::Custom(CustomMenuItem::new( - "Toggle Mouse Reporting", + &menu_label("app_menu.view.toggle_mouse_reporting"), move |ctx| { ctx.dispatch_global_action("workspace:toggle_mouse_reporting", &()); }, @@ -408,7 +509,7 @@ fn make_new_view_menu(ctx: &AppContext) -> Menu { None, )), MenuItem::Custom(CustomMenuItem::new( - "Toggle Scroll Reporting", + &menu_label("app_menu.view.toggle_scroll_reporting"), move |ctx| { ctx.dispatch_global_action("workspace:toggle_scroll_reporting", &()); }, @@ -423,7 +524,7 @@ fn make_new_view_menu(ctx: &AppContext) -> Menu { None, )), MenuItem::Custom(CustomMenuItem::new( - "Toggle Focus Reporting", + &menu_label("app_menu.view.toggle_focus_reporting"), move |ctx| { ctx.dispatch_global_action("workspace:toggle_focus_reporting", &()); }, @@ -449,7 +550,7 @@ fn make_new_view_menu(ctx: &AppContext) -> Menu { items.extend([ MenuItem::Separator, MenuItem::Custom(CustomMenuItem::new( - "Compact Mode", + &menu_label("app_menu.view.compact_mode"), move |ctx| { TerminalSettings::handle(ctx).update(ctx, |terminal_settings, ctx| { let current_value = *terminal_settings.spacing_mode; @@ -483,7 +584,7 @@ fn make_new_view_menu(ctx: &AppContext) -> Menu { ]); } - Menu::new("View", items) + Menu::new(menu_label("app_menu.top.view"), items) } fn make_new_tab_menu(ctx: &AppContext) -> Menu { @@ -510,7 +611,7 @@ fn make_new_tab_menu(ctx: &AppContext) -> Menu { updateable_custom_item_without_checkmark(CustomAction::CloseOtherTabs, ctx), updateable_custom_item_without_checkmark(CustomAction::CloseTabsRight, ctx), ]; - Menu::new("Tab", items) + Menu::new(menu_label("app_menu.top.tab"), items) } fn make_new_ai_menu(ctx: &AppContext) -> Menu { @@ -543,7 +644,7 @@ fn make_new_ai_menu(ctx: &AppContext) -> Menu { )); } - Menu::new("AI", items) + Menu::new(menu_label("app_menu.top.ai"), items) } fn make_new_blocks_menu(ctx: &AppContext) -> Menu { @@ -581,7 +682,7 @@ fn make_new_blocks_menu(ctx: &AppContext) -> Menu { items.extend(debug_items); } - Menu::new("Blocks", items) + Menu::new(menu_label("app_menu.top.blocks"), items) } fn make_new_drive_menu(ctx: &AppContext) -> Menu { @@ -625,7 +726,7 @@ fn make_new_drive_menu(ctx: &AppContext) -> Menu { ]) } - Menu::new("Drive", items) + Menu::new(menu_label("app_menu.top.drive"), items) } /// Returns [`MenuItem`]s that aid debugging to be included in the Block menu. @@ -636,7 +737,7 @@ fn block_menu_debug_items() -> Vec { } items.push(MenuItem::Custom(CustomMenuItem::new( - SHOW_IN_BAND_COMMAND_BLOCKS_MENU_ITEM_NAME, + &menu_label("app_menu.blocks.show_in_band_command_blocks"), move |ctx| { let handle = BlockVisibilitySettings::handle(ctx); handle.update(ctx, |block_visibility_settings, ctx| { @@ -655,9 +756,9 @@ fn block_menu_debug_items() -> Vec { let name = if BlockVisibilitySettings::handle(ctx).read(ctx, |settings, _ctx| { *settings.should_show_in_band_command_blocks.value() }) { - HIDE_IN_BAND_COMMAND_BLOCKS_MENU_ITEM_NAME.to_owned() + menu_label("app_menu.blocks.hide_in_band_command_blocks") } else { - SHOW_IN_BAND_COMMAND_BLOCKS_MENU_ITEM_NAME.to_owned() + menu_label("app_menu.blocks.show_in_band_command_blocks") }; MenuItemPropertyChanges { @@ -669,7 +770,7 @@ fn block_menu_debug_items() -> Vec { ))); items.push(MenuItem::Custom(CustomMenuItem::new( - SHOW_SSH_COMMAND_BLOCKS_MENU_ITEM_NAME, + &menu_label("app_menu.blocks.show_ssh_command_blocks"), move |ctx| { let handle = BlockVisibilitySettings::handle(ctx); handle.update(ctx, |block_visibility_settings, ctx| { @@ -686,9 +787,9 @@ fn block_menu_debug_items() -> Vec { let name = if BlockVisibilitySettings::handle(ctx).read(ctx, |settings, _ctx| { *settings.should_show_ssh_block.value() }) { - HIDE_SSH_COMMAND_BLOCKS_MENU_ITEM_NAME.to_owned() + menu_label("app_menu.blocks.hide_ssh_command_blocks") } else { - SHOW_SSH_COMMAND_BLOCKS_MENU_ITEM_NAME.to_owned() + menu_label("app_menu.blocks.show_ssh_command_blocks") }; MenuItemPropertyChanges { @@ -704,7 +805,7 @@ fn block_menu_debug_items() -> Vec { fn toggle_bootstrap_block_menu_item() -> MenuItem { MenuItem::Custom(CustomMenuItem::new( - SHOW_BOOTSTRAP_BLOCK_MENU_ITEM_NAME, + &menu_label("app_menu.blocks.show_initialization_block"), move |ctx| { BlockVisibilitySettings::handle(ctx).update(ctx, |block_visibility_settings, ctx| { let new_value = !block_visibility_settings @@ -722,9 +823,9 @@ fn toggle_bootstrap_block_menu_item() -> MenuItem { let name = if BlockVisibilitySettings::handle(ctx).read(ctx, |settings, _ctx| { *settings.should_show_bootstrap_block.value() }) { - HIDE_BOOTSTRAP_BLOCK_MENU_ITEM_NAME.to_owned() + menu_label("app_menu.blocks.hide_initialization_block") } else { - SHOW_BOOTSTRAP_BLOCK_MENU_ITEM_NAME.to_owned() + menu_label("app_menu.blocks.show_initialization_block") }; MenuItemPropertyChanges { @@ -737,8 +838,8 @@ fn toggle_bootstrap_block_menu_item() -> MenuItem { } fn make_new_window_menu() -> Menu { - Menu::new( - "Window", + Menu::new_window( + menu_label("app_menu.top.window"), vec![ MenuItem::Standard(StandardAction::Minimize), MenuItem::Standard(StandardAction::Zoom), @@ -754,7 +855,7 @@ fn debug_menu_items() -> Vec { if FeatureFlag::DebugMode.is_enabled() { debug_menu_items.push(MenuItem::Custom(CustomMenuItem::new( - ENABLE_SHELL_DEBUG_MODE_MENU_ITEM_NAME, + &menu_label("app_menu.debug.enable_shell_debug_mode"), move |ctx| { DebugSettings::handle(ctx).update(ctx, |debug_settings, ctx| { let new_value = !debug_settings.is_shell_debug_mode_enabled.value(); @@ -770,9 +871,9 @@ fn debug_menu_items() -> Vec { let name = if DebugSettings::handle(ctx).read(ctx, |settings, _ctx| { *settings.is_shell_debug_mode_enabled.value() }) { - DISABLE_SHELL_DEBUG_MODE_MENU_ITEM_NAME.to_owned() + menu_label("app_menu.debug.disable_shell_debug_mode") } else { - ENABLE_SHELL_DEBUG_MODE_MENU_ITEM_NAME.to_owned() + menu_label("app_menu.debug.enable_shell_debug_mode") }; MenuItemPropertyChanges { @@ -784,7 +885,7 @@ fn debug_menu_items() -> Vec { ))); debug_menu_items.push(MenuItem::Custom(CustomMenuItem::new( - ENABLE_PTY_RECORDING, + &menu_label("app_menu.debug.enable_pty_recording"), move |ctx| { DebugSettings::handle(ctx).update(ctx, |debug_settings, ctx| { let new_value = !debug_settings.recording_mode.value(); @@ -795,9 +896,9 @@ fn debug_menu_items() -> Vec { let name = if DebugSettings::handle(ctx) .read(ctx, |settings, _ctx| *settings.recording_mode.value()) { - DISABLE_PTY_RECORDING.to_owned() + menu_label("app_menu.debug.disable_pty_recording") } else { - ENABLE_PTY_RECORDING.to_owned() + menu_label("app_menu.debug.enable_pty_recording") }; MenuItemPropertyChanges { @@ -809,7 +910,7 @@ fn debug_menu_items() -> Vec { ))); debug_menu_items.push(MenuItem::Custom(CustomMenuItem::new( - ENABLE_IN_BAND_GENERATORS_MENU_ITEM_NAME, + &menu_label("app_menu.debug.enable_in_band_generators"), move |ctx| { DebugSettings::handle(ctx).update(ctx, |debug_settings, ctx| { let new_value = !debug_settings @@ -829,9 +930,9 @@ fn debug_menu_items() -> Vec { .are_in_band_generators_for_all_sessions_enabled .value() }) { - DISABLE_IN_BAND_GENERATORS_MENU_ITEM_NAME.to_owned() + menu_label("app_menu.debug.disable_in_band_generators") } else { - ENABLE_IN_BAND_GENERATORS_MENU_ITEM_NAME.to_owned() + menu_label("app_menu.debug.enable_in_band_generators") }; MenuItemPropertyChanges { @@ -847,14 +948,14 @@ fn debug_menu_items() -> Vec { } debug_menu_items.push(MenuItem::Custom(CustomMenuItem::new( - "Manually Toggle Network Status", + &menu_label("app_menu.debug.manually_toggle_network_status"), move |ctx| ctx.dispatch_global_action("workspace:toggle_debug_network_status", &()), no_updates, None, ))); debug_menu_items.push(MenuItem::Custom(CustomMenuItem::new( - EXPORT_DEFAULT_SETTINGS_CSV_MENU_ITEM_NAME, + &menu_label("app_menu.debug.export_default_settings_csv"), move |ctx| { let default_settings = SettingsManager::handle(ctx).as_ref(ctx).default_values(); let mut writer = Writer::from_writer( @@ -881,7 +982,7 @@ fn debug_menu_items() -> Vec { ))); debug_menu_items.push(MenuItem::Custom(CustomMenuItem::new( - "Create anonymous user", + &menu_label("app_menu.debug.create_anonymous_user"), move |ctx| ctx.dispatch_global_action("workspace:debug_create_anonymous_user", &()), no_updates, None, @@ -895,9 +996,9 @@ fn debug_menu_items() -> Vec { debug_menu_items } -fn link_menu_item(title: &'static str, link: Cow<'static, str>) -> MenuItem { +fn link_menu_item(title_key: &'static str, link: Cow<'static, str>) -> MenuItem { MenuItem::Custom(CustomMenuItem::new( - title, + &menu_label(title_key), move |ctx| { ctx.open_url(&link); }, @@ -908,7 +1009,7 @@ fn link_menu_item(title: &'static str, link: Cow<'static, str>) -> MenuItem { fn feedback_menu_item() -> MenuItem { MenuItem::Custom(CustomMenuItem::new( - "Send Feedback...", + &menu_label("app_menu.help.send_feedback"), move |ctx| { // Route through the root-view action so workspace windows can open the // guided AI flow, while non-workspace windows still fall back to the @@ -922,12 +1023,18 @@ fn feedback_menu_item() -> MenuItem { fn make_new_help_menu() -> Menu { Menu::new( - "Help", + menu_label("app_menu.top.help"), vec![ feedback_menu_item(), - link_menu_item("Warp Documentation...", links::USER_DOCS_URL.into()), - link_menu_item("GitHub Issues...", links::GITHUB_ISSUES_URL.into()), - link_menu_item("Warp Slack Community...", links::SLACK_URL.into()), + link_menu_item( + "app_menu.help.warp_documentation", + links::USER_DOCS_URL.into(), + ), + link_menu_item( + "app_menu.help.github_issues", + links::GITHUB_ISSUES_URL.into(), + ), + link_menu_item("app_menu.help.slack_community", links::SLACK_URL.into()), ], ) } @@ -961,7 +1068,7 @@ fn make_launch_config_menu_items(ctx: &mut AppContext) -> Vec { // TODO(vorporeal): use non_updateable_custom_item() here instead launch_config_menu_items.push(MenuItem::Custom(CustomMenuItem::new( - "Save New...", + &menu_label("app_menu.launch_config.save_new"), custom_action_dispatcher(CustomAction::SaveCurrentConfig), no_updates, custom_shortcut(CustomAction::SaveCurrentConfig), @@ -976,13 +1083,13 @@ fn make_new_elements_menu_items(ctx: &AppContext) -> Vec { // shows its dedicated keystroke instead. let mut new_elements_menu = vec![ MenuItem::Custom(CustomMenuItem::new( - "New Window", + &menu_label("app_menu.file.new_window"), open_new_window, no_updates, Some(Keystroke::parse("cmd-n").expect("Valid keystroke")), )), MenuItem::Custom(CustomMenuItem::new( - "New Terminal Tab", + &menu_label("app_menu.file.new_terminal_tab"), open_new_default_tab_or_window, move |_props: &MenuItemProperties, ctx: &mut AppContext| { let mut changes = MenuItemPropertyChanges::default(); @@ -1007,7 +1114,7 @@ fn make_new_elements_menu_items(ctx: &AppContext) -> Vec { Some(Keystroke::parse("cmd-t").expect("Valid keystroke")), )), MenuItem::Custom(CustomMenuItem::new( - "New Agent Tab", + &menu_label("app_menu.file.new_agent_tab"), open_new_agent_tab_or_window, move |_props: &MenuItemProperties, ctx: &mut AppContext| { let mut changes = MenuItemPropertyChanges::default(); @@ -1043,7 +1150,7 @@ fn make_new_elements_menu_items(ctx: &AppContext) -> Vec { let reopen_session_action_updater = custom_action_updater(CustomAction::ReopenClosedSession, Box::new(|_| false)); new_elements_menu.push(MenuItem::Custom(CustomMenuItem::new( - "Reopen closed session", + &menu_label("app_menu.file.reopen_closed_session"), |ctx| { UndoCloseStack::handle(ctx).update(ctx, |stack, ctx| { stack.undo_close(ctx); @@ -1058,7 +1165,7 @@ fn make_new_elements_menu_items(ctx: &AppContext) -> Vec { ))); new_elements_menu.push(MenuItem::Custom(CustomMenuItem::new_with_submenu( - "Launch Configurations", + &menu_label("app_menu.file.launch_configurations"), |_| (), |_props, ctx| MenuItemPropertyChanges { submenu: Some(Some(make_launch_config_menu_items(ctx))), @@ -1171,11 +1278,12 @@ fn custom_action_updater( changes.disabled = Some(binding.is_none()); if let Some(binding) = binding { if let Some(description) = binding.description { - changes.name = Some( + changes.name = Some(custom_action_menu_label( + action, description .resolve(ctx, bindings::MAC_MENUS_CONTEXT) .into_owned(), - ); + )); } changes.keystroke = Some(bindings::trigger_to_keystroke(binding.trigger)); } diff --git a/app/src/auth/auth_manager.rs b/app/src/auth/auth_manager.rs index 0417091dd8..001727fd16 100644 --- a/app/src/auth/auth_manager.rs +++ b/app/src/auth/auth_manager.rs @@ -620,7 +620,7 @@ impl AuthManager { Err(err) => { report_error!( - anyhow!(err).context("Encountered an error trying to create anonymous users") + anyhow!(err).context(i18n::t("auth.errors.create_anonymous_user_failed")) ); ctx.emit(AuthManagerEvent::CreateAnonymousUserFailed); } diff --git a/app/src/auth/auth_override_warning_body.rs b/app/src/auth/auth_override_warning_body.rs index a8a92f9661..ac152ef45d 100644 --- a/app/src/auth/auth_override_warning_body.rs +++ b/app/src/auth/auth_override_warning_body.rs @@ -28,16 +28,6 @@ const ACTION_BUTTON_BORDER_WIDTH: f32 = 2.; const ACTION_BUTTON_HORIZONTAL_PADDING: f32 = 8.; const ACTION_BUTTON_FONT_SIZE: f32 = 14.; -const AUTH_OVERRIDE_DESCRIPTION: &str = "It looks like you logged into a Warp account through a web browser. If you continue, any personal Warp drive objects and preferences from this anonymous session with be permanently deleted."; -const AUTH_OVERRIDE_CONFIRMATION_WARNING: &str = "This cannot be undone."; -const AUTH_OVERRIDE_INITIAL_STEP_HEADER: &str = "New login detected"; -const AUTH_OVERRIDE_CONFIRM_CONFIRMATION_STEP_HEADER: &str = - "Delete personal Warp Drive objects and preferences?"; -const AUTH_OVERRIDE_BULK_EXPORT_BUTTON_LABEL: &str = "Export your data"; -const AUTH_OVERRIDE_BULK_EXPORT_DESCRIPTION: &str = " to import later."; -const AUTH_OVERRIDE_CANCEL_BUTTON_LABEL: &str = "Cancel"; -const AUTH_OVERRIDE_CONTINUE_BUTTON_LABEL: &str = "Continue"; - #[derive(Clone, Copy, Debug)] pub enum AuthOverrideWarningBodyAction { Close, @@ -100,9 +90,9 @@ impl AuthOverrideWarningBody { }; let text = match self.confirmation_step { - AuthOverrideConfirmationStep::Initial => AUTH_OVERRIDE_INITIAL_STEP_HEADER, + AuthOverrideConfirmationStep::Initial => i18n::t("auth.override.initial_header"), AuthOverrideConfirmationStep::ConfirmChangeUser => { - AUTH_OVERRIDE_CONFIRM_CONFIRMATION_STEP_HEADER + i18n::t("auth.override.confirm_header") } }; @@ -154,7 +144,7 @@ impl AuthOverrideWarningBody { AuthOverrideConfirmationStep::Initial => { let description = Container::new( ui_builder - .paragraph(AUTH_OVERRIDE_DESCRIPTION) + .paragraph(i18n::t("auth.override.description")) .with_style(muted_styles) .build() .finish(), @@ -167,7 +157,7 @@ impl AuthOverrideWarningBody { .with_child( ui_builder .link( - AUTH_OVERRIDE_BULK_EXPORT_BUTTON_LABEL.into(), + i18n::t("auth.override.export_data"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action( @@ -184,7 +174,7 @@ impl AuthOverrideWarningBody { ) .with_child( ui_builder - .span(AUTH_OVERRIDE_BULK_EXPORT_DESCRIPTION) + .span(i18n::t("auth.override.export_suffix")) .with_style(muted_styles) .build() .finish(), @@ -200,7 +190,7 @@ impl AuthOverrideWarningBody { AuthOverrideConfirmationStep::ConfirmChangeUser => { let confirmation = Container::new( ui_builder - .paragraph(AUTH_OVERRIDE_CONFIRMATION_WARNING) + .paragraph(i18n::t("auth.override.confirm_warning")) .with_style(muted_styles) .build() .finish(), @@ -285,7 +275,7 @@ impl AuthOverrideWarningBody { Some(click_button_style), None, ) - .with_centered_text_label(AUTH_OVERRIDE_CANCEL_BUTTON_LABEL.into()) + .with_centered_text_label(i18n::t("common.cancel")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(AuthOverrideWarningBodyAction::Close); @@ -311,7 +301,7 @@ impl AuthOverrideWarningBody { Some(outline_click_button_style), None, ) - .with_centered_text_label(AUTH_OVERRIDE_CONTINUE_BUTTON_LABEL.into()) + .with_centered_text_label(i18n::t("common.continue")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(continue_action); @@ -376,8 +366,8 @@ impl View for AuthOverrideWarningBody { fn accessibility_contents(&self, _: &AppContext) -> Option { Some(AccessibilityContent::new( - "New login detected", - "Warp has detected a new login from a web browser. Press escape to cancel and continue using Warp without login.", + i18n::t("auth.override.initial_header"), + i18n::t("auth.override.accessibility_description"), WarpA11yRole::HelpRole, )) } diff --git a/app/src/auth/auth_view_body.rs b/app/src/auth/auth_view_body.rs index 2a49c920a5..2d9718de7f 100644 --- a/app/src/auth/auth_view_body.rs +++ b/app/src/auth/auth_view_body.rs @@ -45,9 +45,6 @@ const TOS_URL: &str = "https://www.warp.dev/terms-of-service"; const COMMON_BODY_UI_FONT_SIZE: f32 = 12.; const AUTH_MODAL_GAP: f32 = 16.; -const AUTH_TOKEN_INPUT_PLACEHOLDER_TEXT: &str = "Auth Token"; -const AUTH_TOKEN_INPUT_PLACEHOLDER_TEXT_EXPERIMENTAL: &str = "Browser auth token"; - const AUTH_TOKEN_INPUT_BORDER_RADIUS: Radius = Radius::Pixels(4.); lazy_static! { @@ -163,9 +160,9 @@ impl AuthViewBody { let placeholder_text = if matches!(experiment_group, Some(AuthFlowInstructions::Experiment)) { - AUTH_TOKEN_INPUT_PLACEHOLDER_TEXT_EXPERIMENTAL + i18n::t("auth.browser_auth_token") } else { - AUTH_TOKEN_INPUT_PLACEHOLDER_TEXT + i18n::t("auth.auth_token") }; editor.set_placeholder_text(placeholder_text, ctx); @@ -255,7 +252,7 @@ impl AuthViewBody { .with_child( ui_builder .link( - "Click here to paste your token from the browser".into(), + i18n::t("auth.paste_token_link"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(AuthViewBodyAction::EnterToken); @@ -328,7 +325,7 @@ impl AuthViewBody { Flex::row() .with_child( ui_builder - .span("By continuing, you agree to Warp's ") + .span(i18n::t("auth.terms_prefix")) .with_style(disclaimer_styles) .build() .finish(), @@ -336,7 +333,7 @@ impl AuthViewBody { .with_child( ui_builder .link( - "Terms of Service".into(), + i18n::t("auth.terms_of_service"), Some(TOS_URL.into()), None, self.mouse_state_handles.tos_mouse_state_handle.clone(), @@ -356,7 +353,7 @@ impl AuthViewBody { Align::new( ui_builder .link( - "Privacy Settings".into(), + i18n::t("auth.privacy_settings"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(AuthViewBodyAction::ShowOverlay( @@ -377,7 +374,7 @@ impl AuthViewBody { Flex::column() .with_child( ui_builder - .paragraph("If you'd like to opt out of analytics and AI features,") + .paragraph(i18n::t("auth.opt_out_analytics_ai")) .with_style(disclaimer_styles) .build() .finish(), @@ -386,7 +383,7 @@ impl AuthViewBody { Flex::row() .with_child( ui_builder - .paragraph("you can adjust your ") + .paragraph(i18n::t("auth.adjust_your")) .with_style(disclaimer_styles) .build() .finish(), @@ -394,7 +391,7 @@ impl AuthViewBody { .with_child( ui_builder .link( - "Privacy Settings".into(), + i18n::t("auth.privacy_settings"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(AuthViewBodyAction::ShowOverlay( @@ -474,7 +471,7 @@ impl AuthViewBody { Some(click_button_style), None, ) - .with_centered_text_label("Sign up".into()) + .with_centered_text_label(i18n::t("auth.sign_up")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(on_click_action); @@ -486,14 +483,14 @@ impl AuthViewBody { Flex::row() .with_child( ui_builder - .span("Already have an account? ") + .span(i18n::t("auth.already_have_account")) .build() .finish(), ) .with_child( ui_builder .link( - "Sign in".into(), + i18n::t("auth.sign_in"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(AuthViewBodyAction::Login); @@ -514,14 +511,14 @@ impl AuthViewBody { Flex::row() .with_child( ui_builder - .span("Don't want to sign in right now? ") + .span(i18n::t("auth.no_sign_in_now")) .build() .finish(), ) .with_child( ui_builder .link( - "Skip for now".into(), + i18n::t("auth.skip_for_now"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(AuthViewBodyAction::InitiateLoginLater); @@ -545,13 +542,13 @@ impl AuthViewBody { Flex::column() .with_child( ui_builder - .paragraph("Are you sure you want to skip login?") + .paragraph(i18n::t("auth.skip_login_confirm")) .build() .finish(), ) .with_child( ui_builder - .paragraph("You can sign up later, but some features, such as AI,") + .paragraph(i18n::t("auth.skip_login_details_1")) .build() .finish(), ) @@ -559,14 +556,14 @@ impl AuthViewBody { Flex::row() .with_child( ui_builder - .span("are only available to logged-in users. ") + .span(i18n::t("auth.skip_login_details_2")) .build() .finish(), ) .with_child( ui_builder .link( - "Yes, skip login".into(), + i18n::t("auth.yes_skip_login"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(AuthViewBodyAction::LoginLater); @@ -603,16 +600,12 @@ impl AuthViewBody { }; let text = match self.variant { - AuthViewVariant::RequireLoginCloseable => { - "In order to use Warp’s AI features or collaborate with others, please create an account." - } + AuthViewVariant::RequireLoginCloseable => i18n::t("auth.require_login_ai"), AuthViewVariant::HitDriveObjectLimitCloseable => { - "In order to create more objects in Warp Drive, please create an account." - } - AuthViewVariant::ShareRequirementCloseable => { - "In order to share, please create an account." + i18n::t("auth.require_login_drive_limit") } - _ => "", + AuthViewVariant::ShareRequirementCloseable => i18n::t("auth.require_login_share"), + _ => String::new(), }; Container::new( @@ -636,10 +629,10 @@ impl AuthViewBody { }; let text = match self.variant { - AuthViewVariant::Initial => "Welcome to Warp!", + AuthViewVariant::Initial => i18n::t("auth.welcome_to_warp"), AuthViewVariant::RequireLoginCloseable | AuthViewVariant::HitDriveObjectLimitCloseable - | AuthViewVariant::ShareRequirementCloseable => "Sign up for Warp", + | AuthViewVariant::ShareRequirementCloseable => i18n::t("auth.sign_up_for_warp"), }; ui_builder @@ -755,7 +748,7 @@ impl AuthViewBody { let header = Container::new( ui_builder - .paragraph("Sign in on your browser \nto continue") + .paragraph(i18n::t("auth.browser_sign_in_title_multiline")) .with_style(header_styles) .build() .finish(), @@ -769,14 +762,14 @@ impl AuthViewBody { Flex::row() .with_child( ui_builder - .span("If your browser hasn't launched, ") + .span(i18n::t("auth.browser_not_launched_prefix")) .build() .finish(), ) .with_child( ui_builder .link( - "copy the URL".into(), + i18n::t("auth.copy_url"), None, Some(Box::new(|event_ctx| { event_ctx.dispatch_typed_action( @@ -795,7 +788,7 @@ impl AuthViewBody { ) .with_child( ui_builder - .span("and open the page manually.") + .span(i18n::t("auth.open_page_manually")) .build() .finish(), ) @@ -994,8 +987,8 @@ impl View for AuthViewBody { fn accessibility_contents(&self, _: &AppContext) -> Option { Some(AccessibilityContent::new( - "Welcome to Warp!", - "Press enter to open your browser to Sign Up or Sign In.", + i18n::t("auth.welcome_to_warp"), + i18n::t("auth.a11y_open_browser"), WarpA11yRole::HelpRole, )) } diff --git a/app/src/auth/auth_view_modal.rs b/app/src/auth/auth_view_modal.rs index 60cea5b5fe..2ee06582d2 100644 --- a/app/src/auth/auth_view_modal.rs +++ b/app/src/auth/auth_view_modal.rs @@ -100,7 +100,9 @@ impl AuthRedirectPayload { /// must be of format {scheme}://auth/desktop_redirect?refresh_token={token}. pub fn from_url(url: Url) -> Result { if url.host_str() != Some(AUTH_URL_HOST) { - return Err(anyhow!("Received URL with unexpected host: {} ", url)); + return Err(anyhow!( + i18n::t("auth.errors.redirect_url_unexpected_host").replace("{url}", url.as_str()) + )); } let query_params: HashMap<_, _> = url.query_pairs().into_owned().collect(); if let Some(token) = query_params.get(AUTH_URL_REFRESH_TOKEN_QUERY_PARAM) { @@ -117,10 +119,10 @@ impl AuthRedirectPayload { state: query_params.get(AUTH_URL_STATE_QUERY_PARAM).cloned(), }) } else { - Err(anyhow!( - "Received URL without refresh token query param: {}", - url - )) + Err(anyhow!(i18n::t( + "auth.errors.redirect_url_missing_refresh_token" + ) + .replace("{url}", url.as_str()))) } } diff --git a/app/src/auth/auth_view_shared_helpers.rs b/app/src/auth/auth_view_shared_helpers.rs index eb77efb3b0..a833678e04 100644 --- a/app/src/auth/auth_view_shared_helpers.rs +++ b/app/src/auth/auth_view_shared_helpers.rs @@ -54,7 +54,7 @@ where ..Default::default() }; - let text = "You are currently offline. An internet connection is required to use Warp for the first time."; + let text = i18n::t("auth.offline_first_time"); let (button_color, button_variant) = action_button_color_and_variant(appearance); let button_styles = UiComponentStyles { @@ -94,7 +94,7 @@ where Some(click_button_style), None, ) - .with_centered_text_label("Learn more".into()) + .with_centered_text_label(i18n::t("common.learn_more")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(action.clone()); @@ -168,9 +168,9 @@ where ..Default::default() }; - let paragraph_1 = "All of Warp’s non-cloud features work offline."; - let paragraph_2 = "However, we require users to be online when using Warp for the first time in order to enable Warp's AI and cloud features."; - let paragraph_3 = "We offer cloud features to all users, and so we need an internet connection to meter AI usage, prevent abuse, and associate cloud objects with users. If you opt to use Warp logged-out, a unique ID will be attached to an anonymous user account in order to support these features."; + let paragraph_1 = i18n::t("auth.offline_info.paragraph_1"); + let paragraph_2 = i18n::t("auth.offline_info.paragraph_2"); + let paragraph_3 = i18n::t("auth.offline_info.paragraph_3"); Container::new( Flex::column() @@ -184,7 +184,7 @@ where Container::new( appearance .ui_builder() - .span("Using Warp Offline") + .span(i18n::t("auth.offline_info.title")) .with_style(header_styles) .build() .finish(), @@ -231,7 +231,7 @@ where .with_child(render_close_overlay_button( appearance, appearance.ui_builder(), - "Dismiss".into(), + i18n::t("common.dismiss"), mouse_state_handle, action, )) @@ -362,7 +362,7 @@ pub fn render_privacy_settings_overlay_body( .with_child( Container::new( ui_builder - .span("Privacy Settings") + .span(i18n::t("auth.privacy_settings")) .with_style(header_styles) .build() .finish(), @@ -380,7 +380,7 @@ pub fn render_privacy_settings_overlay_body( .with_child(render_close_overlay_button( appearance, ui_builder, - "Done".into(), + i18n::t("common.done"), handles.close_button_mouse.clone(), actions.hide_overlay.clone(), )) @@ -449,7 +449,11 @@ pub fn render_privacy_settings_toggles( .with_child( Shrinkable::new( 1., - render_privacy_settings_section_header("Help improve Warp", appearance).finish(), + render_privacy_settings_section_header( + i18n::t("auth.privacy.help_improve_warp"), + appearance, + ) + .finish(), ) .finish(), ) @@ -466,17 +470,15 @@ pub fn render_privacy_settings_toggles( ) .finish(); - let telemetry_description = render_description( - appearance, - "High-level feature usage data helps Warp's product team prioritize the roadmap.".into(), - ); + let telemetry_description = + render_description(appearance, i18n::t("auth.privacy.telemetry_description")); let telemetry_link = Flex::row() .with_child( appearance .ui_builder() .link( - "Learn more".into(), + i18n::t("common.learn_more"), Some(PRIVACY_URL.into()), None, handles.telemetry_docs_mouse.clone(), @@ -494,7 +496,11 @@ pub fn render_privacy_settings_toggles( .with_child( Shrinkable::new( 1., - render_privacy_settings_section_header("Send crash reports", appearance).finish(), + render_privacy_settings_section_header( + i18n::t("auth.privacy.send_crash_reports"), + appearance, + ) + .finish(), ) .finish(), ) @@ -513,7 +519,7 @@ pub fn render_privacy_settings_toggles( let crash_reporting_description = render_description( appearance, - "Crash reporting helps Warp's engineering team understand stability and improve performance.".into(), + i18n::t("auth.privacy.crash_reporting_description"), ); let toggle_cloud = actions.toggle_cloud_conversation_storage.clone(); @@ -524,7 +530,7 @@ pub fn render_privacy_settings_toggles( Shrinkable::new( 1., render_privacy_settings_section_header( - "Store AI conversations in the cloud", + i18n::t("auth.privacy.store_ai_conversations"), appearance, ) .finish(), @@ -547,11 +553,10 @@ pub fn render_privacy_settings_toggles( let cloud_conversation_storage_description = render_description( appearance, if PrivacySettings::as_ref(app).is_cloud_conversation_storage_enabled { - "Agent conversations can be shared with others and are retained when you log in on different devices. This data is only stored for product functionality, and Warp will not use it for analytics." + i18n::t("auth.privacy.cloud_conversation_on_description") } else { - "Agent conversations are only stored locally on your machine, are lost upon logout, and cannot be shared. Note: conversation data for ambient agents are still stored in the cloud." - } - .into(), + i18n::t("auth.privacy.cloud_conversation_off_description") + }, ); let mut col = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch); diff --git a/app/src/auth/login_failure_notification.rs b/app/src/auth/login_failure_notification.rs index 4bd1f66610..831a5a5bee 100644 --- a/app/src/auth/login_failure_notification.rs +++ b/app/src/auth/login_failure_notification.rs @@ -27,9 +27,11 @@ impl LoginFailureReason { mut fragments: Vec, ) -> Vec { fragments.extend([ - FormattedTextFragment::plain_text(" Not the first time? See our "), + FormattedTextFragment::plain_text(i18n::t( + "auth.login_failure.troubleshooting_prefix", + )), FormattedTextFragment::hyperlink( - "troubleshooting docs", + i18n::t("auth.login_failure.troubleshooting_docs"), LOGIN_TROUBLESHOOTING_DOCS_URL, ), FormattedTextFragment::plain_text("."), @@ -39,27 +41,27 @@ impl LoginFailureReason { let fragments = match self { LoginFailureReason::InvalidRedirectUrl { was_pasted } => { let text = if *was_pasted { - "An invalid auth token was entered into the modal." + i18n::t("auth.login_failure.invalid_auth_token") } else { - "Failed to log in. Try manually copying the auth token from the \ - authentication web page and pasting into the modal." + i18n::t("auth.login_failure.copy_token_manually") }; with_troubleshooting_text(vec![FormattedTextFragment::plain_text(text)]) } LoginFailureReason::FailedUserAuthentication => { - with_troubleshooting_text(vec![FormattedTextFragment::plain_text( - "Request to log in failed.", - )]) + with_troubleshooting_text(vec![FormattedTextFragment::plain_text(i18n::t( + "auth.login_failure.login_failed", + ))]) } LoginFailureReason::FailedMintCustomToken => { - with_troubleshooting_text(vec![FormattedTextFragment::plain_text( - "Request to sign up failed.", - )]) + with_troubleshooting_text(vec![FormattedTextFragment::plain_text(i18n::t( + "auth.login_failure.signup_failed", + ))]) } - LoginFailureReason::InvalidStateParameter | LoginFailureReason::MissingStateParameter => { - with_troubleshooting_text(vec![FormattedTextFragment::plain_text( - "The redirect URL pasted did not originate from this app. Please click the button below to try again.", - )]) + LoginFailureReason::InvalidStateParameter + | LoginFailureReason::MissingStateParameter => { + with_troubleshooting_text(vec![FormattedTextFragment::plain_text(i18n::t( + "auth.login_failure.invalid_redirect_url", + ))]) } }; FormattedText::new([FormattedTextLine::Line(fragments)]) diff --git a/app/src/auth/login_slide.rs b/app/src/auth/login_slide.rs index 669eaa7012..7f0a6503ae 100644 --- a/app/src/auth/login_slide.rs +++ b/app/src/auth/login_slide.rs @@ -1,7 +1,7 @@ use std::cell::Cell; use onboarding::slides::{layout, slide_content}; -use onboarding::{OnboardingIntention, AI_FEATURES, WARP_DRIVE_FEATURES}; +use onboarding::{ai_features, warp_drive_features, OnboardingIntention}; use pathfinder_color::ColorU; use pathfinder_geometry::vector::vec2f; use ui_components::{button, Component as _, Options as _}; @@ -292,7 +292,7 @@ impl LoginSlideView { }, ctx, ); - editor.set_placeholder_text("Auth Token", ctx); + editor.set_placeholder_text(i18n::t("auth.auth_token"), ctx); editor }); @@ -461,11 +461,11 @@ impl LoginSlideView { /// Disclaimer prefix shown before the "Privacy Settings" link. AI is /// dropped from the wording on paths that don't enable AI (e.g. /// Terminal+Drive), since there are no AI features to opt out of there. - fn privacy_disclaimer_prefix(&self) -> &'static str { + fn privacy_disclaimer_prefix(&self) -> String { if self.ai_enabled { - "If you'd like to opt out of analytics and AI features, you can adjust your " + i18n::t("auth.privacy_disclaimer_ai_prefix") } else { - "If you'd like to opt out of analytics, you can adjust your " + i18n::t("auth.privacy_disclaimer_prefix") } } @@ -476,9 +476,9 @@ impl LoginSlideView { let is_terminal = matches!(self.intention, OnboardingIntention::Terminal); let title_text = if is_terminal { - "Get started with Warp Drive" + i18n::t("auth.get_started_warp_drive") } else { - "Get started with AI" + i18n::t("auth.get_started_ai") }; let title = FormattedTextElement::from_str(title_text, appearance.ui_font_family(), 36.) .with_color(internal_colors::text_main( @@ -490,9 +490,9 @@ impl LoginSlideView { .finish(); let subtitle_text = if is_terminal { - "Connect your account to save and share notebooks, workflows, and more across devices." + i18n::t("auth.connect_account_drive") } else { - "Connect your account to enable AI-powered planning, coding, and automation." + i18n::t("auth.connect_account_ai") }; let subtitle = FormattedTextElement::from_str(subtitle_text, appearance.ui_font_family(), 16.) @@ -512,7 +512,7 @@ impl LoginSlideView { let tos_line = Flex::row() .with_child( ui_builder - .span("By continuing, you agree to Warp's ") + .span(i18n::t("auth.terms_prefix")) .with_style(disclaimer_styles) .build() .finish(), @@ -520,7 +520,7 @@ impl LoginSlideView { .with_child( ui_builder .link( - "Terms of Service".into(), + i18n::t("auth.terms_of_service"), Some(TOS_URL.into()), None, self.tos_mouse_state.clone(), @@ -546,7 +546,7 @@ impl LoginSlideView { .with_child( ui_builder .link( - "Privacy Settings".into(), + i18n::t("auth.privacy_settings"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(LoginSlideAction::ShowPrivacySettings); @@ -587,7 +587,7 @@ impl LoginSlideView { let back_button = self.back_button.render( appearance, button::Params { - content: button::Content::Label("Back".into()), + content: button::Content::Label(i18n::t("common.back").into()), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -600,9 +600,9 @@ impl LoginSlideView { let cmd_enter = Keystroke::parse("cmdorctrl-enter").unwrap_or_default(); let skip_label = if matches!(self.intention, OnboardingIntention::Terminal) { - "Disable Warp Drive" + i18n::t("auth.disable_warp_drive") } else { - "Disable AI features" + i18n::t("auth.disable_ai_features") }; let skip_button = self.skip_button.render( appearance, @@ -623,7 +623,7 @@ impl LoginSlideView { let login_button = self.login_button.render( appearance, button::Params { - content: button::Content::Label("Continue".into()), + content: button::Content::Label(i18n::t("common.continue").into()), theme: &button::themes::Primary, options: button::Options { keystroke: Some(enter), @@ -669,7 +669,7 @@ impl LoginSlideView { }; let title = FormattedTextElement::from_str( - "Sign in on your browser to continue", + i18n::t("auth.browser_sign_in_title"), appearance.ui_font_family(), 36., ) @@ -686,7 +686,7 @@ impl LoginSlideView { Flex::row() .with_child( ui_builder - .span("If your browser hasn't launched, ") + .span(i18n::t("auth.browser_not_launched_prefix")) .with_style(sub_text_styles) .build() .finish(), @@ -694,7 +694,7 @@ impl LoginSlideView { .with_child( ui_builder .link( - "copy the URL".into(), + i18n::t("auth.copy_url"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(LoginSlideAction::CopyLoginUrl); @@ -707,7 +707,7 @@ impl LoginSlideView { ) .with_child( ui_builder - .span(" and open") + .span(i18n::t("auth.and_open")) .with_style(sub_text_styles) .build() .finish(), @@ -716,7 +716,7 @@ impl LoginSlideView { ) .with_child( ui_builder - .span("the page manually.") + .span(i18n::t("auth.page_manually")) .with_style(sub_text_styles) .build() .finish(), @@ -770,7 +770,7 @@ impl LoginSlideView { .with_child( ui_builder .link( - "Click here to paste your token from the browser".into(), + i18n::t("auth.paste_token_link"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(LoginSlideAction::EnterToken); @@ -799,7 +799,7 @@ impl LoginSlideView { let back_button = self.browser_back_button.render( appearance, button::Params { - content: button::Content::Label("Back".into()), + content: button::Content::Label(i18n::t("common.back").into()), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -827,15 +827,18 @@ impl LoginSlideView { ) -> Vec> { let theme = appearance.theme(); - let title = - FormattedTextElement::from_str("Privacy Settings", appearance.ui_font_family(), 36.) - .with_color(internal_colors::text_main( - theme, - theme.background().into_solid(), - )) - .with_weight(Weight::Medium) - .with_alignment(TextAlignment::Left) - .finish(); + let title = FormattedTextElement::from_str( + i18n::t("auth.privacy_settings"), + appearance.ui_font_family(), + 36., + ) + .with_color(internal_colors::text_main( + theme, + theme.background().into_solid(), + )) + .with_weight(Weight::Medium) + .with_alignment(TextAlignment::Left) + .finish(); let actions = PrivacySettingsActions { toggle_telemetry: LoginSlideAction::ToggleTelemetry, @@ -859,7 +862,7 @@ impl LoginSlideView { let back_button = self.done_button.render( appearance, button::Params { - content: button::Content::Label("Back".into()), + content: button::Content::Label(i18n::t("common.back").into()), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -897,9 +900,9 @@ impl LoginSlideView { let is_terminal = matches!(self.intention, OnboardingIntention::Terminal); let title_text = if is_terminal { - "Are you sure you want to disable Warp Drive?" + i18n::t("auth.disable_warp_drive_confirm") } else { - "Are you sure you want to disable AI features?" + i18n::t("auth.disable_ai_confirm") }; let title = FormattedTextElement::from_str(title_text, appearance.ui_font_family(), 16.) .with_color(internal_colors::text_main(theme, dialog_surface_solid)) @@ -933,9 +936,9 @@ impl LoginSlideView { .finish(); let body_text_str = if is_terminal { - "Warp Drive lets you save workflows and knowledge across devices and share them with your team. By continuing, you won't have access to the following features:" + i18n::t("auth.disable_warp_drive_body") } else { - "Warp is better with AI. By continuing, you won't have access to any of the following features:" + i18n::t("auth.disable_ai_body") }; let body_text = FormattedTextElement::from_str(body_text_str, appearance.ui_font_family(), 14.) @@ -948,12 +951,12 @@ impl LoginSlideView { let feature_x_fill: ThemeFill = ThemeFill::Solid(theme.ansi_fg_red()); let mut feature_list = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch); - let feature_items: &[&str] = if is_terminal { - WARP_DRIVE_FEATURES + let feature_items = if is_terminal { + warp_drive_features() } else { - AI_FEATURES + ai_features() }; - for &item in feature_items { + for item in feature_items { let icon_el = ConstrainedBox::new(Icon::X.to_warpui_icon(feature_x_fill).finish()) .with_width(16.) .with_height(16.) @@ -988,9 +991,9 @@ impl LoginSlideView { .finish(); let cancel_label = if is_terminal { - "Enable Warp Drive" + i18n::t("auth.enable_warp_drive") } else { - "Enable AI features" + i18n::t("auth.enable_ai_features") }; let login_button = self.dialog_login_button.render( appearance, @@ -1010,7 +1013,7 @@ impl LoginSlideView { let skip_confirm_button = self.dialog_skip_button.render( appearance, button::Params { - content: button::Content::Label("Skip for now".into()), + content: button::Content::Label(i18n::t("auth.skip_for_now").into()), theme: &button::themes::Primary, options: button::Options { keystroke: Some(dialog_enter), diff --git a/app/src/auth/mod.rs b/app/src/auth/mod.rs index 5780f07479..5e5f5926c0 100644 --- a/app/src/auth/mod.rs +++ b/app/src/auth/mod.rs @@ -96,87 +96,87 @@ pub fn maybe_log_out(app: &mut AppContext) { || num_unsaved_files > 0) { send_telemetry_sync_from_app_ctx!(TelemetryEvent::LogOutModalShown, app); - let mut button_data = vec![ModalButton::for_app("Yes, log out", |ctx| { - log_out(ctx); - })]; + let mut button_data = vec![ModalButton::for_app( + i18n::t("auth.logout.confirm"), + |ctx| { + log_out(ctx); + }, + )]; let mut info_text_vec: Vec = vec![]; if num_long_running_commands > 0 { - let plural = if num_long_running_commands > 1 { - "processes" + let key = if num_long_running_commands > 1 { + "auth.logout.running_processes" } else { - "process" + "auth.logout.running_process" }; - info_text_vec.push(format!( - "You have {num_long_running_commands} {plural} running." - )); - - button_data.push(ModalButton::for_app("Show running processes", move |ctx| { - send_telemetry_sync_from_app_ctx!( - TelemetryEvent::LogOutModalCancel { nav_palette: true }, - ctx - ); - let windowing_model = ctx.windows(); - let window_id = if let Some(active_window_id) = windowing_model.active_window() { - active_window_id - } else if let Some(window_id) = ctx.window_ids().collect_vec().first() { - let window_id = *window_id; - windowing_model.show_window_and_focus_app(window_id); - window_id - } else { - return; - }; - - if let Some(workspaces) = ctx.views_of_type::(window_id) { - if let Some(handle) = workspaces.first() { - ctx.dispatch_typed_action_for_view( - window_id, - handle.id(), - &WorkspaceAction::OpenPalette { - mode: PaletteMode::Navigation, - source: PaletteSource::LogOutModal, - query: Some("running".to_owned()), - }, - ); + info_text_vec + .push(i18n::t(key).replace("{count}", &num_long_running_commands.to_string())); + + button_data.push(ModalButton::for_app( + i18n::t("auth.logout.show_running_processes"), + move |ctx| { + send_telemetry_sync_from_app_ctx!( + TelemetryEvent::LogOutModalCancel { nav_palette: true }, + ctx + ); + let windowing_model = ctx.windows(); + let window_id = if let Some(active_window_id) = windowing_model.active_window() + { + active_window_id + } else if let Some(window_id) = ctx.window_ids().collect_vec().first() { + let window_id = *window_id; + windowing_model.show_window_and_focus_app(window_id); + window_id + } else { + return; + }; + + if let Some(workspaces) = ctx.views_of_type::(window_id) { + if let Some(handle) = workspaces.first() { + ctx.dispatch_typed_action_for_view( + window_id, + handle.id(), + &WorkspaceAction::OpenPalette { + mode: PaletteMode::Navigation, + source: PaletteSource::LogOutModal, + query: Some("running".to_owned()), + }, + ); + } } - } - })) + }, + )) } if num_shared_sessions > 0 { - let plural = if num_shared_sessions > 1 { - "sessions" + let key = if num_shared_sessions > 1 { + "auth.logout.shared_sessions" } else { - "session" + "auth.logout.shared_session" }; - info_text_vec.push(format!("You have {num_shared_sessions} shared {plural}.")); + info_text_vec.push(i18n::t(key).replace("{count}", &num_shared_sessions.to_string())); } if num_unsaved_objects > 0 { - let plural = if num_unsaved_objects > 1 { - "objects" + let key = if num_unsaved_objects > 1 { + "auth.logout.unsynced_objects" } else { - "object" + "auth.logout.unsynced_object" }; - info_text_vec.push(format!( - "You have {num_unsaved_objects} unsynced Warp Drive {plural}. \ - Logging out will cause you to lose the {plural}." - )); + info_text_vec.push(i18n::t(key).replace("{count}", &num_unsaved_objects.to_string())); } if num_unsaved_files > 0 { - let plural = if num_unsaved_files > 1 { - "files" + let key = if num_unsaved_files > 1 { + "auth.logout.unsaved_files" } else { - "file" + "auth.logout.unsaved_file" }; - info_text_vec.push(format!( - "You have {num_unsaved_files} unsaved {plural}. \ - Logging out will cause you to lose the {plural}." - )); + info_text_vec.push(i18n::t(key).replace("{count}", &num_unsaved_files.to_string())); } - button_data.push(ModalButton::for_app("Cancel", move |ctx| { + button_data.push(ModalButton::for_app(i18n::t("common.cancel"), move |ctx| { send_telemetry_sync_from_app_ctx!( TelemetryEvent::LogOutModalCancel { nav_palette: false }, ctx @@ -184,7 +184,7 @@ pub fn maybe_log_out(app: &mut AppContext) { })); let alert_data = AlertDialogWithCallbacks::for_app( - "Log out?", + i18n::t("auth.logout.title"), info_text_vec.join("\n"), button_data, move |ctx| { diff --git a/app/src/auth/needs_sso_link_view.rs b/app/src/auth/needs_sso_link_view.rs index 40e65eb1b0..82eb3cb69b 100644 --- a/app/src/auth/needs_sso_link_view.rs +++ b/app/src/auth/needs_sso_link_view.rs @@ -56,7 +56,7 @@ impl View for NeedsSsoLinkView { ButtonVariant::Accent, self.mouse_state_handles.link_sso_handle.clone(), ) - .with_text_label("Link SSO".to_string()) + .with_text_label(i18n::t("auth.link_sso")) .with_style(UiComponentStyles { padding: Some(Coords { top: 10., @@ -77,8 +77,8 @@ impl View for NeedsSsoLinkView { .finish(); LoginErrorModal::new(app) - .with_header("Your organization has enabled SSO for your account") - .with_detail("Click the button below to link your Warp account to your SSO provider.") + .with_header(i18n::t("auth.sso_enabled_header")) + .with_detail(i18n::t("auth.sso_link_detail")) .with_action(link_sso_button) .build() .finish() diff --git a/app/src/auth/paste_auth_token_modal.rs b/app/src/auth/paste_auth_token_modal.rs index 5ddffd83fe..414a50ed68 100644 --- a/app/src/auth/paste_auth_token_modal.rs +++ b/app/src/auth/paste_auth_token_modal.rs @@ -121,7 +121,7 @@ impl PasteAuthTokenModalView { }, ctx, ); - editor.set_placeholder_text("Enter auth token", ctx); + editor.set_placeholder_text(i18n::t("auth.enter_auth_token"), ctx); editor }); @@ -239,7 +239,7 @@ impl View for PasteAuthTokenModalView { let ui_builder = appearance.ui_builder(); let title = FormattedTextElement::from_str( - "Paste your auth token below", + i18n::t("auth.paste_token_modal.title"), appearance.ui_font_family(), 16., ) @@ -266,7 +266,7 @@ impl View for PasteAuthTokenModalView { let subtitle_color = internal_colors::text_sub(theme, dialog_surface_solid); let subtitle = FormattedTextElement::from_str( - "Paste your auth token from the browser to get complete login.", + i18n::t("auth.paste_token_modal.subtitle"), appearance.ui_font_family(), 14., ) @@ -326,7 +326,7 @@ impl View for PasteAuthTokenModalView { let cancel_button = self.cancel_button.render( appearance, button::Params { - content: button::Content::Label("Cancel".into()), + content: button::Content::Label(i18n::t("common.cancel").into()), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -341,7 +341,7 @@ impl View for PasteAuthTokenModalView { let continue_button = self.continue_button.render( appearance, button::Params { - content: button::Content::Label("Continue".into()), + content: button::Content::Label(i18n::t("common.continue").into()), theme: &button::themes::Primary, options: button::Options { keystroke: Some(enter), diff --git a/app/src/auth/user.rs b/app/src/auth/user.rs index a653276776..f44a33f801 100644 --- a/app/src/auth/user.rs +++ b/app/src/auth/user.rs @@ -48,7 +48,9 @@ impl TryFrom warp_graphql::mutations::create_anonymous_user::AnonymousUserType::NativeClientAnonymousUserFeatureGated => Ok(AnonymousUserType::NativeClientAnonymousUserFeatureGated), warp_graphql::mutations::create_anonymous_user::AnonymousUserType::WebClientAnonymousUser => Ok(AnonymousUserType::WebClientAnonymousUser), warp_graphql::mutations::create_anonymous_user::AnonymousUserType::Other(_) => { - Err(anyhow!("could not convert unknown anonymous user type")) + Err(anyhow!(i18n::t( + "auth.errors.unknown_anonymous_user_type" + ))) }, } } diff --git a/app/src/auth/web_handoff.rs b/app/src/auth/web_handoff.rs index e295c51493..9794c6d76a 100644 --- a/app/src/auth/web_handoff.rs +++ b/app/src/auth/web_handoff.rs @@ -79,7 +79,8 @@ impl WebHandoffView { self.import_user_from_session_cookie(ctx); } Err(AuthHandoffError::Unexpected(err)) => { - report_error!(anyhow!("Web user handoff failed: {err:?}")); + report_error!(anyhow!(i18n::t("auth.errors.web_user_handoff_failed") + .replace("{error}", &format!("{err:?}")))); self.state = HandoffState::Failed; ctx.notify(); } @@ -119,8 +120,10 @@ impl View for WebHandoffView { fn render(&self, app: &AppContext) -> Box { let label = match &self.state { - HandoffState::LoadingFromHost | HandoffState::LoadingFromSessionCookie => "Loading...", - HandoffState::Failed => "Error authenticating - please refresh the page", + HandoffState::LoadingFromHost | HandoffState::LoadingFromSessionCookie => { + i18n::t("auth.web_handoff.loading") + } + HandoffState::Failed => i18n::t("auth.web_handoff.failed"), }; LoginErrorModal::new(app) diff --git a/app/src/autoupdate/linux.rs b/app/src/autoupdate/linux.rs index ee92bc8c27..c4f0463aca 100644 --- a/app/src/autoupdate/linux.rs +++ b/app/src/autoupdate/linux.rs @@ -200,24 +200,27 @@ mod package_manager { FormattedTextLine::Heading(FormattedTextHeader { // Make this an

heading_size: 3, - text: vec![FormattedTextFragment::bold(format!( - "Run {package_manager_name} to update" - ))], + text: vec![FormattedTextFragment::bold( + i18n::t("autoupdate.linux.run_package_manager_to_update") + .replace("{package_manager}", &package_manager_name), + )], }), FormattedTextLine::Line(vec![ - FormattedTextFragment::plain_text("If you installed Warp using "), + FormattedTextFragment::plain_text(i18n::t( + "autoupdate.linux.installed_using_prefix", + )), FormattedTextFragment::bold(package_manager_name), - FormattedTextFragment::plain_text( - " or a compatible tool, the pre-filled command will update Warp for you.", - ), + FormattedTextFragment::plain_text(i18n::t( + "autoupdate.linux.compatible_tool_suffix", + )), ]), ]; if self.package_manager.needs_repository_configuration() { lines.push(FormattedTextLine::Line(vec![ - FormattedTextFragment::plain_text( - "\nThe command below includes a one-time configuration of the Warp package repository and PGP signing key.", - ), + FormattedTextFragment::plain_text(i18n::t( + "autoupdate.linux.one_time_repo_setup", + )), ])); } @@ -226,22 +229,22 @@ mod package_manager { .distribution_update_disabled_repository() { lines.push(FormattedTextLine::Line(vec![ - FormattedTextFragment::plain_text( - "\nThe ", - ), + FormattedTextFragment::plain_text(i18n::t("autoupdate.linux.the_prefix")), FormattedTextFragment::inline_code("warp_handle_dist_upgrade"), - FormattedTextFragment::plain_text( - " function ensures the Warp package repository is enabled, as we've detected you recently upgraded your distribution.", - ), + FormattedTextFragment::plain_text(i18n::t( + "autoupdate.linux.ensure_repo_function_suffix", + )), ])); } lines.push(FormattedTextLine::Line(vec![ - FormattedTextFragment::plain_text("\nReview the command below, then "), - FormattedTextFragment::bold("press enter"), - FormattedTextFragment::plain_text(" to install the update and re-launch Warp. "), + FormattedTextFragment::plain_text(i18n::t("autoupdate.linux.review_command_then")), + FormattedTextFragment::bold(i18n::t("autoupdate.linux.press_enter")), + FormattedTextFragment::plain_text(i18n::t( + "autoupdate.linux.install_and_relaunch_suffix", + )), FormattedTextFragment::hyperlink( - "Please report any issues", + i18n::t("autoupdate.linux.report_issues"), "https://github.com/warpdotdev/Warp/issues/new/choose", ), ])); diff --git a/app/src/autoupdate/mod.rs b/app/src/autoupdate/mod.rs index 726fd7e66c..0c76cb26c2 100644 --- a/app/src/autoupdate/mod.rs +++ b/app/src/autoupdate/mod.rs @@ -736,13 +736,13 @@ pub fn accessibility_content( match (request_type, update_available) { // Found autoupdate (RequestType::ManualCheck, Ok(UpdateReady::Yes { .. })) => Some(AccessibilityContent::new( - "Update available.", - "Use the command palette to install and relaunch Warp", + i18n::t("autoupdate.accessibility.update_available"), + i18n::t("autoupdate.accessibility.install_relaunch_help"), WarpA11yRole::HelpRole, )), // Any non-successful autoupdate check (RequestType::ManualCheck, _) => Some(AccessibilityContent::new_without_help( - "No updates available", + i18n::t("autoupdate.accessibility.no_updates_available"), WarpA11yRole::HelpRole, )), _ => None, diff --git a/app/src/billing/shared_objects_creation_denied_body.rs b/app/src/billing/shared_objects_creation_denied_body.rs index 505c917407..ac0315cb56 100644 --- a/app/src/billing/shared_objects_creation_denied_body.rs +++ b/app/src/billing/shared_objects_creation_denied_body.rs @@ -17,21 +17,29 @@ const BUTTON_PADDING: f32 = 12.; const BUTTON_FONT_SIZE: f32 = 14.; const BUTTON_BORDER_RADIUS: f32 = 4.; -const DEFAULT_DELINQUENT_ADMIN_MODAL_SUBHEADER: &str = "Shared drive objects have been restricted due to a subscription payment issue.\n\nPlease update your payment information to restore access."; -const DEFAULT_DELINQUENT_ADMIN_ENTERPRISE_MODAL_SUBHEADER: &str = "Shared drive objects have been restricted due to a subscription payment issue.\n\nPlease contact support@warp.dev to restore access."; -const DEFAULT_DELINQUENT_MODAL_SUBHEADER: &str = "Shared drive objects have been restricted due to a subscription payment issue.\n\nPlease contact a team admin to restore access."; -const DEFAULT_ADMIN_PROSUMER_MODAL_SUBHEADER: &str = "Warp's Pro plan comes with a limited number of shared drive objects.\n\nFor access to unlimited shared drive objects, upgrade to the Turbo plan."; -const DEFAULT_PROSUMER_MODAL_SUBHEADER: &str = "Warp's Pro plan comes with a limited number of shared drive objects.\n\nFor access to unlimited shared drive objects, contact a team admin to upgrade to the Turbo plan."; -const DEFAULT_ADMIN_MODAL_SUBHEADER: &str = "Warp's free plan comes with a limited number of shared drive objects.\n\nFor access to unlimited shared drive objects, upgrade to a paid plan."; -const DEFAULT_MODAL_SUBHEADER: &str = "Warp's free plan comes with a limited number of shared drive objects.\n\nFor access to unlimited shared drive objects, contact a team admin to upgrade to a paid plan."; -const VIEW_PLANS_TEXT: &str = "Compare plans"; -const MANAGE_BILLING_BUTTON_TEXT: &str = "Manage billing"; - #[derive(Default)] struct MouseStateHandles { button_mouse_state: MouseStateHandle, } +pub(super) fn shared_object_type_label(object_type: DriveObjectType) -> String { + match object_type { + DriveObjectType::Notebook { .. } => i18n::t("drive.object.notebook"), + DriveObjectType::Workflow => i18n::t("drive.object.workflow"), + DriveObjectType::Folder => i18n::t("drive.object.folder"), + DriveObjectType::EnvVarCollection => i18n::t("drive.object.env_var_collection"), + DriveObjectType::AgentModeWorkflow => i18n::t("drive.object.prompt"), + DriveObjectType::AIFact => i18n::t("drive.object.ai_fact"), + DriveObjectType::AIFactCollection => i18n::t("drive.object.ai_fact_collection"), + DriveObjectType::MCPServer => i18n::t("drive.object.mcp_server"), + DriveObjectType::MCPServerCollection => i18n::t("drive.object.mcp_server_collection"), + } +} + +fn shared_object_message(key: &str, object_type: DriveObjectType) -> String { + i18n::t(key).replace("{object_type}", &shared_object_type_label(object_type)) +} + pub struct SharedObjectsCreationDeniedBody { object_type: Option, has_admin_permissions: bool, @@ -93,23 +101,41 @@ impl View for SharedObjectsCreationDeniedBody { let sub_header = match self.object_type { Some(object_type) => { - match (self.is_delinquent_due_to_payment_issue, self.has_admin_permissions, self.customer_type) { + match ( + self.is_delinquent_due_to_payment_issue, + self.has_admin_permissions, + self.customer_type, + ) { (true, true, _) => { if is_stripe_paid_plan { - format!("Shared {object_type}s have been restricted due to a subscription payment issue.\n\nPlease update your payment information to restore access.") + shared_object_message( + "billing.shared_objects.delinquent_admin_stripe", + object_type, + ) } else { - format!("Shared {object_type}s have been restricted due to a subscription payment issue.\n\nPlease contact support@warp.dev to restore access.") + shared_object_message( + "billing.shared_objects.delinquent_admin_enterprise", + object_type, + ) } - }, - (true, false, _) => format!("Shared {object_type}s have been restricted due to a subscription payment issue.\n\nPlease contact a team admin to restore access."), + } + (true, false, _) => shared_object_message( + "billing.shared_objects.delinquent_non_admin", + object_type, + ), (false, true, CustomerType::Prosumer) => { - format!("Warp's Pro plan comes with a limited number of shared {object_type}s.\n\nFor access to unlimited shared {object_type}s, upgrade to the Build plan.") + shared_object_message("billing.shared_objects.prosumer_admin", object_type) } - (false, false, CustomerType::Prosumer) => { - format!("Warp's Pro plan comes with a limited number of shared {object_type}s.\n\nFor access to unlimited shared {object_type}s, contact a team admin to upgrade to the Build plan.") + (false, false, CustomerType::Prosumer) => shared_object_message( + "billing.shared_objects.prosumer_non_admin", + object_type, + ), + (false, true, _) => { + shared_object_message("billing.shared_objects.free_admin", object_type) + } + (false, false, _) => { + shared_object_message("billing.shared_objects.free_non_admin", object_type) } - (false, true, _) => format!("Warp's free plan comes with a limited number of shared {object_type}s.\n\nFor access to unlimited shared {object_type}s, upgrade to a paid plan."), - (false, false, _) => format!("Warp's free plan comes with a limited number of shared {object_type}s.\n\nFor access to unlimited shared {object_type}s, contact a team admin to upgrade to a paid plan."), } } _ => match ( @@ -119,18 +145,20 @@ impl View for SharedObjectsCreationDeniedBody { ) { (true, true, _) => { if is_stripe_paid_plan { - DEFAULT_DELINQUENT_ADMIN_MODAL_SUBHEADER.into() + i18n::t("billing.shared_objects.default_delinquent_admin_stripe") } else { - DEFAULT_DELINQUENT_ADMIN_ENTERPRISE_MODAL_SUBHEADER.into() + i18n::t("billing.shared_objects.default_delinquent_admin_enterprise") } } - (true, false, _) => DEFAULT_DELINQUENT_MODAL_SUBHEADER.into(), + (true, false, _) => i18n::t("billing.shared_objects.default_delinquent_non_admin"), (false, true, CustomerType::Prosumer) => { - DEFAULT_ADMIN_PROSUMER_MODAL_SUBHEADER.into() + i18n::t("billing.shared_objects.default_prosumer_admin") + } + (false, false, CustomerType::Prosumer) => { + i18n::t("billing.shared_objects.default_prosumer_non_admin") } - (false, false, CustomerType::Prosumer) => DEFAULT_PROSUMER_MODAL_SUBHEADER.into(), - (false, true, _) => DEFAULT_ADMIN_MODAL_SUBHEADER.into(), - (false, false, _) => DEFAULT_MODAL_SUBHEADER.into(), + (false, true, _) => i18n::t("billing.shared_objects.default_free_admin"), + (false, false, _) => i18n::t("billing.shared_objects.default_free_non_admin"), }, }; @@ -167,7 +195,7 @@ impl View for SharedObjectsCreationDeniedBody { 0.5, self.render_button( appearance, - MANAGE_BILLING_BUTTON_TEXT.into(), + i18n::t("billing.shared_objects.manage_billing"), self.button_mouse_states.button_mouse_state.clone(), SharedObjectsCreationDeniedBodyAction::ManageBilling, ), @@ -189,7 +217,7 @@ impl View for SharedObjectsCreationDeniedBody { 0.5, self.render_button( appearance, - VIEW_PLANS_TEXT.into(), + i18n::t("billing.shared_objects.compare_plans"), self.button_mouse_states.button_mouse_state.clone(), SharedObjectsCreationDeniedBodyAction::Upgrade, ), diff --git a/app/src/billing/shared_objects_creation_denied_modal.rs b/app/src/billing/shared_objects_creation_denied_modal.rs index 5449a010b8..bc5cc53b94 100644 --- a/app/src/billing/shared_objects_creation_denied_modal.rs +++ b/app/src/billing/shared_objects_creation_denied_modal.rs @@ -10,7 +10,7 @@ use warpui::{ }; use super::shared_objects_creation_denied_body::{ - SharedObjectsCreationDeniedBody, SharedObjectsCreationDeniedBodyEvent, + shared_object_type_label, SharedObjectsCreationDeniedBody, SharedObjectsCreationDeniedBodyEvent, }; use crate::drive::cloud_object_styling::warp_drive_icon_color; use crate::drive::DriveObjectType; @@ -21,8 +21,6 @@ use crate::ui_components::icons::Icon; use crate::workspaces::user_workspaces::UserWorkspaces; use crate::workspaces::workspace::CustomerType; -const DEFAULT_LIMIT_REACHED_MODAL_HEADER: &str = "Shared object limit reached"; - pub struct SharedObjectsCreationDeniedModal { shared_objects_creation_denied_modal: ViewHandle>, team_uid: Option, @@ -65,7 +63,9 @@ impl SharedObjectsCreationDeniedModal { let shared_objects_creation_denied_modal = ctx.add_typed_action_view(|ctx| { Modal::new( - Some(DEFAULT_LIMIT_REACHED_MODAL_HEADER.into()), + Some(i18n::t( + "billing.shared_objects.default_limit_reached_header", + )), shared_objects_creation_denied_body, ctx, ) @@ -124,10 +124,17 @@ impl SharedObjectsCreationDeniedModal { ) { let appearance = Appearance::as_ref(ctx); self.team_uid = Some(team_uid); + let object_type_label = shared_object_type_label(object_type); let title: Option = if is_delinquent_due_to_payment_issue { - Some(format!("Shared {object_type}s restricted")) + Some( + i18n::t("billing.shared_objects.restricted_title") + .replace("{object_type}", &object_type_label), + ) } else { - Some(format!("Shared {object_type}s limit reached")) + Some( + i18n::t("billing.shared_objects.limit_reached_title") + .replace("{object_type}", &object_type_label), + ) }; let (icon, icon_color) = match object_type { DriveObjectType::Notebook { is_ai_document } => ( diff --git a/app/src/bin/generate_settings_schema.rs b/app/src/bin/generate_settings_schema.rs index 9cf09e5cf2..a5fa5c1442 100644 --- a/app/src/bin/generate_settings_schema.rs +++ b/app/src/bin/generate_settings_schema.rs @@ -2,7 +2,7 @@ //! //! Usage: //! ``` -//! cargo run --bin generate_settings_schema -- [--channel dev|preview|stable] [output_path] +//! cargo run --bin generate_settings_schema -- [--channel dev|preview|stable] [--locale en|zh-CN] [output_path] //! ``` use std::collections::HashSet; @@ -106,6 +106,29 @@ fn active_flags_for_channel(channel: &str) -> HashSet { flags } +fn localized_catalog_string(key: &str, fallback: &str) -> Option { + let value = i18n::t(key); + if value == key { + if fallback.is_empty() { + None + } else { + Some(fallback.to_string()) + } + } else { + Some(value) + } +} + +fn localized_schema_description(entry: &SettingSchemaEntry) -> Option { + if let Some(key) = entry.description_key { + localized_catalog_string(key, entry.description) + } else if entry.description.is_empty() { + None + } else { + Some(entry.description.to_string()) + } +} + /// Creates intermediate hierarchy objects so that a setting at e.g. /// `appearance.text` is nested under `properties.appearance.properties.text.properties`. fn ensure_hierarchy<'a>( @@ -145,6 +168,7 @@ fn main() { let args: Vec = std::env::args().collect(); let mut channel = "dev"; + let mut locale = i18n::FALLBACK_LOCALE; let mut output_path: Option<&str> = None; let mut i = 1; while i < args.len() { @@ -155,6 +179,12 @@ fn main() { channel = &args[i]; } } + "--locale" => { + i += 1; + if i < args.len() { + locale = &args[i]; + } + } arg if !arg.starts_with('-') => { output_path = Some(arg); } @@ -166,6 +196,8 @@ fn main() { i += 1; } + i18n::set_locale(locale); + let active_flags = active_flags_for_channel(channel); let mut generator = SchemaGenerator::default(); let mut root_properties = Map::new(); @@ -197,13 +229,10 @@ fn main() { } } - // Always overwrite description with the macro-provided one - if !entry.description.is_empty() { + // Always overwrite description with the macro-provided/localized one. + if let Some(description) = localized_schema_description(entry) { if let Some(obj) = schema_value.as_object_mut() { - obj.insert( - "description".to_string(), - Value::String(entry.description.to_string()), - ); + obj.insert("description".to_string(), Value::String(description)); } } @@ -227,16 +256,17 @@ fn main() { "$schema".to_string(), Value::String("https://json-schema.org/draft/2020-12/schema".to_string()), ); - root.insert( - "title".to_string(), - Value::String("Warp Settings".to_string()), - ); - root.insert( - "description".to_string(), - Value::String(format!( - "JSON Schema for Warp settings ({channel} channel, {entry_count} settings)" - )), - ); + let title = + localized_catalog_string("settings.schema.title", "Warp Settings").unwrap_or_default(); + root.insert("title".to_string(), Value::String(title)); + let description = localized_catalog_string( + "settings.schema.description", + "JSON Schema for Warp settings ({channel} channel, {entry_count} settings)", + ) + .unwrap_or_default() + .replace("{channel}", channel) + .replace("{entry_count}", &entry_count.to_string()); + root.insert("description".to_string(), Value::String(description)); root.insert("type".to_string(), Value::String("object".to_string())); root.insert("properties".to_string(), Value::Object(root_properties)); diff --git a/app/src/chip_configurator/mod.rs b/app/src/chip_configurator/mod.rs index 6afa277d11..b06e5d7006 100644 --- a/app/src/chip_configurator/mod.rs +++ b/app/src/chip_configurator/mod.rs @@ -196,13 +196,13 @@ impl ControlItemRenderer { .finish() } - pub(crate) fn display_label(&self) -> &str { + pub(crate) fn display_label(&self) -> String { if let Some(label) = &self.custom_label { - label + label.clone() } else if let Some(kind) = &self.kind { - kind.display_label() + kind.display_label().to_string() } else { - "Unknown" + i18n::t("chip_configurator.unknown") } } @@ -221,7 +221,7 @@ impl ControlItemRenderer { appearance: &Appearance, ) -> Box { let font_size = udi_font_size(appearance); - let label = self.display_label().to_string(); + let label = self.display_label(); let icon = self.display_icon(); let is_dragging = matches!(drag_state, ChipDragState::Draggable { is_dragging: true }); let mut hoverable = Hoverable::new(self.tooltip_state_handle.clone(), move |mouse_state| { diff --git a/app/src/chip_configurator/modal_shell.rs b/app/src/chip_configurator/modal_shell.rs index f8266f8ae3..ba414501f7 100644 --- a/app/src/chip_configurator/modal_shell.rs +++ b/app/src/chip_configurator/modal_shell.rs @@ -29,7 +29,6 @@ const PRIMARY_BUTTON_HEIGHT: f32 = 40.; const SECTION_UNIFORM_PADDING: f32 = 16.; const MARGIN_BETWEEN_MODAL_SECTIONS: f32 = 16.; const MODAL_CONTENT_FONT_SIZE: f32 = 14.; -const RESTORE_DEFAULT_LABEL: &str = "Restore default"; /// Mouse state handles for interactive controls in chip editor sections and modals. #[derive(Default)] @@ -146,7 +145,7 @@ fn render_restore_default_button( let button = Hoverable::new(mouse_handle.clone(), |_state| { appearance .ui_builder() - .span(RESTORE_DEFAULT_LABEL.to_string()) + .span(i18n::t("chip_configurator.restore_default")) .with_style(UiComponentStyles { font_size: Some(MODAL_CONTENT_FONT_SIZE), ..Default::default() @@ -205,7 +204,10 @@ pub fn render_chip_editor_sections( ); let left_section = Flex::column() - .with_child(render_section_label("Left side", appearance)) + .with_child(render_section_label( + &i18n::t("chip_configurator.left_side"), + appearance, + )) .with_child( Container::new(chip_configurator.render_left_drop_zone( config.activate_action, @@ -218,7 +220,10 @@ pub fn render_chip_editor_sections( .finish(); let right_section = Flex::column() - .with_child(render_section_label("Right side", appearance)) + .with_child(render_section_label( + &i18n::t("chip_configurator.right_side"), + appearance, + )) .with_child( Container::new(chip_configurator.render_right_drop_zone( config.activate_action, @@ -293,7 +298,7 @@ fn render_buttons( appearance: &Appearance, ) -> Box { let cancel_button = render_primary_button( - "Cancel".to_string(), + i18n::t("common.cancel"), ButtonVariant::Outlined, false, &config.mouse_handles.cancel, @@ -302,7 +307,7 @@ fn render_buttons( ); let save_button = render_primary_button( - "Save changes".to_string(), + i18n::t("common.save_changes"), ButtonVariant::Accent, !config.is_dirty, &config.mouse_handles.save, diff --git a/app/src/cloud_object/grab_edit_access_modal.rs b/app/src/cloud_object/grab_edit_access_modal.rs index 836bd13511..f1f4f33d49 100644 --- a/app/src/cloud_object/grab_edit_access_modal.rs +++ b/app/src/cloud_object/grab_edit_access_modal.rs @@ -9,12 +9,6 @@ use crate::appearance::Appearance; use crate::ui_components::buttons::close_button; use crate::ui_components::dialog::{dialog_styles, Dialog}; -const EDIT_ANYWAY_CTA_LABEL: &str = "Edit anyway"; -const CANCEL_CTA_LABEL: &str = "Cancel"; -const EDIT_ANYWAY_TEXT: &str = - "If you take edit controls, the current editor will be forced into view mode"; -const CURRENTLY_EDITED_LABEL: &str = "This notebook is currently being edited"; - #[derive(Default)] struct MouseStateHandles { close_button: MouseStateHandle, @@ -52,13 +46,17 @@ impl GrabEditAccessModal { let theme = appearance.theme(); let ui_builder = appearance.ui_builder(); - let description = Text::new(EDIT_ANYWAY_TEXT, appearance.ui_font_family(), 13.) - .with_style(Properties { - style: Style::Normal, - weight: Weight::Bold, - }) - .with_color(theme.active_ui_text_color().into()) - .finish(); + let description = Text::new( + i18n::t("cloud_object.grab_edit_access.description"), + appearance.ui_font_family(), + 13., + ) + .with_style(Properties { + style: Style::Normal, + weight: Weight::Bold, + }) + .with_color(theme.active_ui_text_color().into()) + .finish(); let close_button = close_button(appearance, self.mouse_state_handles.close_button.clone()) .build() @@ -67,7 +65,7 @@ impl GrabEditAccessModal { .finish(); Dialog::new( - CURRENTLY_EDITED_LABEL.to_string(), + i18n::t("cloud_object.grab_edit_access.title"), None, dialog_styles(appearance), ) @@ -80,7 +78,7 @@ impl GrabEditAccessModal { ButtonVariant::Basic, self.mouse_state_handles.cancel_button.clone(), ) - .with_text_label(CANCEL_CTA_LABEL.to_string()) + .with_text_label(i18n::t("common.cancel")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(GrabEditAccessModalAction::Close) @@ -97,7 +95,7 @@ impl GrabEditAccessModal { ButtonVariant::Warn, self.mouse_state_handles.edit_anyway_button.clone(), ) - .with_text_label(EDIT_ANYWAY_CTA_LABEL.to_string()) + .with_text_label(i18n::t("cloud_object.grab_edit_access.edit_anyway")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(GrabEditAccessModalAction::GrabEditAccess) diff --git a/app/src/cloud_object/mod.rs b/app/src/cloud_object/mod.rs index ed3dac9caf..6de3e660c8 100644 --- a/app/src/cloud_object/mod.rs +++ b/app/src/cloud_object/mod.rs @@ -916,10 +916,18 @@ impl CloudObjectMetadataExt for CloudObjectMetadata { .map(|r| format_approx_duration_from_now_utc(r.utc())); let full_string = match (editor_string, time_ago_string) { - (Some(name), Some(time_ago)) if name.is_empty() => format!("Edited {time_ago}"), - (Some(name), Some(time_ago)) => format!("{name} edited {time_ago}"), - (None, Some(time_ago)) => format!("Edited {time_ago}"), - (Some(name), None) => format!("Last edited by {name}"), + (Some(name), Some(time_ago)) if name.is_empty() => { + i18n::t("cloud_object.history.edited").replace("{time_ago}", &time_ago) + } + (Some(name), Some(time_ago)) => i18n::t("cloud_object.history.edited_by") + .replace("{name}", &name) + .replace("{time_ago}", &time_ago), + (None, Some(time_ago)) => { + i18n::t("cloud_object.history.edited").replace("{time_ago}", &time_ago) + } + (Some(name), None) => { + i18n::t("cloud_object.history.last_edited_by").replace("{name}", &name) + } _ => return None, }; @@ -947,8 +955,9 @@ impl CloudObjectMetadataExt for CloudObjectMetadata { let days_left = deletion_time.signed_duration_since(current_time).num_days(); let full_string = match days_left { - 0 | 1 => "1 day until permanent deletion".to_string(), - _ => format!("{days_left} days until permanent deletion"), + 0 | 1 => i18n::t("cloud_object.permadeletion.one_day"), + _ => i18n::t("cloud_object.permadeletion.days") + .replace("{days_left}", &days_left.to_string()), }; Some(full_string) } else { @@ -1002,16 +1011,16 @@ pub enum Space { impl Space { pub fn name(&self, app: &AppContext) -> String { match self { - Space::Personal => "Personal".to_string(), + Space::Personal => i18n::t("cloud_object.space.personal"), Space::Team { team_uid, .. } => { let user_workspaces = UserWorkspaces::as_ref(app); if let Some(team) = user_workspaces.team_from_uid(*team_uid) { team.name.clone() } else { - "Team".to_string() + i18n::t("cloud_object.space.team") } } - Space::Shared => "Shared with me".to_string(), + Space::Shared => i18n::t("cloud_object.space.shared_with_me"), } } } diff --git a/app/src/cloud_object/model/actions.rs b/app/src/cloud_object/model/actions.rs index 80ef21cca1..d5a36e11a4 100644 --- a/app/src/cloud_object/model/actions.rs +++ b/app/src/cloud_object/model/actions.rs @@ -10,6 +10,23 @@ use crate::server::ids::{HashedSqliteId, ObjectUid}; pub enum ObjectActionsEvent {} +fn localized_action_name(action_type: &ObjectActionType, count: i64) -> String { + match action_type { + ObjectActionType::Execute if count == 1 => i18n::t("cloud_object.action.run"), + ObjectActionType::Execute => i18n::t("cloud_object.action.runs"), + } +} + +fn localized_action_history_summary( + count: i64, + action_type: &ObjectActionType, + period_key: &str, +) -> String { + i18n::t(period_key) + .replace("{count}", &count.to_string()) + .replace("{action}", &localized_action_name(action_type, count)) +} + #[cfg(not(target_family = "wasm"))] pub fn object_action_from_persisted( other: crate::persistence::model::PersistedObjectAction, @@ -250,7 +267,11 @@ impl ObjectActions { // If the object is not in the model, return 0. let all_actions_on_this_object = self.object_actions_by_id.get(uid); if all_actions_on_this_object.is_none() { - return Some("0 runs in the last year".to_string()); + return Some(localized_action_history_summary( + 0, + &action_type, + "cloud_object.action_history.last_year", + )); } // If the object doesn't have any of these action types recorded, return 0. @@ -258,21 +279,21 @@ impl ObjectActions { .iter() .filter(|a| a.action_type == action_type); if all_relevant_actions.clone().count() == 0 { - return Some("0 runs in the last year".to_string()); + return Some(localized_action_history_summary( + 0, + &action_type, + "cloud_object.action_history.last_year", + )); } // If the action has occurred in the last day, return Day as the time unit. let one_day_ago = Utc::now() - Duration::days(1); let in_the_last_day = all_relevant_actions.clone().filter(|a| matches!(a.action_subtype, ObjectActionSubtype::SingleAction { timestamp, .. } if timestamp > one_day_ago)).count(); if in_the_last_day > 0 { - return Some(format!( - "{} {} in the last day", - in_the_last_day, - if in_the_last_day == 1 { - action_type.singular() - } else { - action_type.plural() - } + return Some(localized_action_history_summary( + in_the_last_day as i64, + &action_type, + "cloud_object.action_history.last_day", )); } @@ -280,14 +301,10 @@ impl ObjectActions { let one_week_ago = Utc::now() - Duration::days(7); let in_the_last_week = all_relevant_actions.clone().filter(|a| matches!(a.action_subtype, ObjectActionSubtype::SingleAction { timestamp, .. } if timestamp > one_week_ago)).count(); if in_the_last_week > 0 { - return Some(format!( - "{} {} in the last week", - in_the_last_week, - if in_the_last_week == 1 { - action_type.singular() - } else { - action_type.plural() - } + return Some(localized_action_history_summary( + in_the_last_week as i64, + &action_type, + "cloud_object.action_history.last_week", )); } @@ -295,14 +312,10 @@ impl ObjectActions { let one_month_ago = Utc::now() - Duration::days(30); let in_the_last_month = all_relevant_actions.clone().filter(|a| matches!(a.action_subtype, ObjectActionSubtype::SingleAction { timestamp, .. } if timestamp > one_month_ago)).count(); if in_the_last_month > 0 { - return Some(format!( - "{} {} in the last month", - in_the_last_month, - if in_the_last_month == 1 { - action_type.singular() - } else { - action_type.plural() - } + return Some(localized_action_history_summary( + in_the_last_month as i64, + &action_type, + "cloud_object.action_history.last_month", )); } @@ -323,14 +336,10 @@ impl ObjectActions { }) .sum(); - Some(format!( - "{} {} in the last year", - in_the_last_year, - if in_the_last_year == 1 { - action_type.singular() - } else { - action_type.plural() - } + Some(localized_action_history_summary( + i64::from(in_the_last_year), + &action_type, + "cloud_object.action_history.last_year", )) } diff --git a/app/src/cloud_object/model/actions_tests.rs b/app/src/cloud_object/model/actions_tests.rs index 62dc410c1f..83a7687c04 100644 --- a/app/src/cloud_object/model/actions_tests.rs +++ b/app/src/cloud_object/model/actions_tests.rs @@ -3,8 +3,13 @@ use warpui::App; use super::{ObjectAction, ObjectActionSubtype, ObjectActionType, ObjectActions}; +fn use_english_locale() { + i18n::set_locale("en"); +} + #[test] fn test_object_actions_daily() { + use_english_locale(); App::test((), |mut app| async move { let actions: Vec = vec![ ObjectAction { @@ -91,6 +96,7 @@ fn test_object_actions_daily() { #[test] fn test_object_actions_rollup_weekly() { + use_english_locale(); App::test((), |mut app| async move { let actions: Vec = vec![ ObjectAction { @@ -177,6 +183,7 @@ fn test_object_actions_rollup_weekly() { #[test] fn test_object_actions_rollup_monthly() { + use_english_locale(); App::test((), |mut app| async move { let actions: Vec = vec![ ObjectAction { @@ -274,6 +281,7 @@ fn test_object_actions_rollup_monthly() { #[test] fn test_object_actions_rollup_yearly() { + use_english_locale(); App::test((), |mut app| async move { let actions: Vec = vec![ ObjectAction { @@ -371,6 +379,7 @@ fn test_object_actions_rollup_yearly() { #[test] fn test_object_actions_rollup_out_of_date_bundle() { + use_english_locale(); App::test((), |mut app| async move { let actions: Vec = vec![ ObjectAction { @@ -468,6 +477,7 @@ fn test_object_actions_rollup_out_of_date_bundle() { #[test] fn test_object_actions_rollup_none() { + use_english_locale(); App::test((), |mut app| async move { let actions: Vec = vec![ ObjectAction { diff --git a/app/src/cloud_object/toast_message.rs b/app/src/cloud_object/toast_message.rs index 44f21f2dd5..b1ffb4e034 100644 --- a/app/src/cloud_object/toast_message.rs +++ b/app/src/cloud_object/toast_message.rs @@ -19,88 +19,121 @@ impl CloudObjectToastMessage { match (object.object_type(), operation, success_type) { // We should only show toasts for creates initiated by the user, not by the system - (_, ObjectOperation::Create { initiated_by: InitiatedBy::User }, OperationSuccessType::Success) => { + ( + _, + ObjectOperation::Create { + initiated_by: InitiatedBy::User, + }, + OperationSuccessType::Success, + ) => { let containing_object_name = object.containing_object_name(app); - Some(format!("{object_name} saved to {containing_object_name}")) + Some( + i18n::t("cloud_object.toast.saved_to") + .replace("{object_name}", &object_name) + .replace("{containing_object_name}", &containing_object_name), + ) } // notebooks intentionally do not have an update message, as they are updated // as the user types and so toasts would be VERY noisy - ( - ObjectType::Notebook, - ObjectOperation::Update, - OperationSuccessType::Success, - ) => None, + (ObjectType::Notebook, ObjectOperation::Update, OperationSuccessType::Success) => None, (_, ObjectOperation::Update, OperationSuccessType::Success) => { - Some(format!("{object_name} updated")) + Some(i18n::t("cloud_object.toast.updated").replace("{object_name}", &object_name)) } - (_, ObjectOperation::MoveToFolder, OperationSuccessType::Success) | (_, ObjectOperation::MoveToDrive, OperationSuccessType::Success) => { + (_, ObjectOperation::MoveToFolder, OperationSuccessType::Success) + | (_, ObjectOperation::MoveToDrive, OperationSuccessType::Success) => { let containing_object_name = object.containing_object_name(app); - Some(format!("{object_name} moved to {containing_object_name}")) + Some( + i18n::t("cloud_object.toast.moved_to") + .replace("{object_name}", &object_name) + .replace("{containing_object_name}", &containing_object_name), + ) } (_, ObjectOperation::Trash, OperationSuccessType::Success) => { - Some(format!("{object_name} trashed")) + Some(i18n::t("cloud_object.toast.trashed").replace("{object_name}", &object_name)) } (_, ObjectOperation::Untrash, OperationSuccessType::Success) => { - Some(format!("{object_name} restored")) + Some(i18n::t("cloud_object.toast.restored").replace("{object_name}", &object_name)) } (_, ObjectOperation::Leave, OperationSuccessType::Success) => { - Some(format!("Left {object_name}")) - } - (_, ObjectOperation::Create { initiated_by: InitiatedBy::User }, OperationSuccessType::Failure) => { - Some(format!("Failed to create {object_name_lowercase}")) - } - (_, ObjectOperation::Create { initiated_by: InitiatedBy::User }, OperationSuccessType::Denied(message)) => { - Some(message.to_string()) - } - (_, ObjectOperation::Update, OperationSuccessType::Failure) => { - Some(format!("Failed to update {object_name_lowercase}")) - } - (_, ObjectOperation::MoveToFolder, OperationSuccessType::Failure) | (_, ObjectOperation::MoveToDrive, OperationSuccessType::Failure) => { - Some(format!("Failed to move {object_name_lowercase}")) - } - (_, ObjectOperation::Trash, OperationSuccessType::Failure) => { - Some(format!("Failed to trash {object_name_lowercase}")) - } - (_, ObjectOperation::Untrash, OperationSuccessType::Failure) => { - Some(format!("Failed to restore {object_name_lowercase}")) + Some(i18n::t("cloud_object.toast.left").replace("{object_name}", &object_name)) } + ( + _, + ObjectOperation::Create { + initiated_by: InitiatedBy::User, + }, + OperationSuccessType::Failure, + ) => Some( + i18n::t("cloud_object.toast.failed_create") + .replace("{object_name_lowercase}", &object_name_lowercase), + ), + ( + _, + ObjectOperation::Create { + initiated_by: InitiatedBy::User, + }, + OperationSuccessType::Denied(message), + ) => Some(message.to_string()), + (_, ObjectOperation::Update, OperationSuccessType::Failure) => Some( + i18n::t("cloud_object.toast.failed_update") + .replace("{object_name_lowercase}", &object_name_lowercase), + ), + (_, ObjectOperation::MoveToFolder, OperationSuccessType::Failure) + | (_, ObjectOperation::MoveToDrive, OperationSuccessType::Failure) => Some( + i18n::t("cloud_object.toast.failed_move") + .replace("{object_name_lowercase}", &object_name_lowercase), + ), + (_, ObjectOperation::Trash, OperationSuccessType::Failure) => Some( + i18n::t("cloud_object.toast.failed_trash") + .replace("{object_name_lowercase}", &object_name_lowercase), + ), + (_, ObjectOperation::Untrash, OperationSuccessType::Failure) => Some( + i18n::t("cloud_object.toast.failed_restore") + .replace("{object_name_lowercase}", &object_name_lowercase), + ), // We should only show deletion failure toasts for user-initiated deletions. - (_, ObjectOperation::Delete { initiated_by: InitiatedBy::User }, OperationSuccessType::Failure) => { - Some(format!("Failed to delete {object_name_lowercase}")) - } - (_, ObjectOperation::Leave, OperationSuccessType::Failure) => { - Some(format!("Failed to leave {object_name}")) - } ( - ObjectType::Workflow, - ObjectOperation::Update, - OperationSuccessType::Rejection, - ) => { - Some("This workflow could not be saved because changes were made while you were editing.".to_string()) + _, + ObjectOperation::Delete { + initiated_by: InitiatedBy::User, + }, + OperationSuccessType::Failure, + ) => Some( + i18n::t("cloud_object.toast.failed_delete") + .replace("{object_name_lowercase}", &object_name_lowercase), + ), + (_, ObjectOperation::Leave, OperationSuccessType::Failure) => Some( + i18n::t("cloud_object.toast.failed_leave").replace("{object_name}", &object_name), + ), + (ObjectType::Workflow, ObjectOperation::Update, OperationSuccessType::Rejection) => { + Some(i18n::t("cloud_object.toast.workflow_conflict")) } ( - ObjectType::GenericStringObject(GenericStringObjectFormat::Json(JsonObjectType::EnvVarCollection)), + ObjectType::GenericStringObject(GenericStringObjectFormat::Json( + JsonObjectType::EnvVarCollection, + )), ObjectOperation::Update, OperationSuccessType::Rejection, - ) => { - Some("Environment variables could not be saved because changes were made while you were editing.".to_string()) - } + ) => Some(i18n::t("cloud_object.toast.env_vars_conflict")), ( - ObjectType::GenericStringObject(GenericStringObjectFormat::Json(JsonObjectType::AIFact)), + ObjectType::GenericStringObject(GenericStringObjectFormat::Json( + JsonObjectType::AIFact, + )), ObjectOperation::Update, OperationSuccessType::Rejection, - ) => { - Some("Rule could not be saved because changes were made while you were editing.".to_string()) - } - (_, ObjectOperation::TakeEditAccess, OperationSuccessType::Failure) => { - Some(format!("Failed to start editing {object_name_lowercase}")) - } - (_, ObjectOperation::UpdatePermissions, OperationSuccessType::Success) => { - Some(format!("Successfully updated permissions for {object_name_lowercase}")) - } - (_, ObjectOperation::UpdatePermissions, OperationSuccessType::Failure) => { - Some(format!("Failed to update permissions for {object_name_lowercase}")) - } + ) => Some(i18n::t("cloud_object.toast.rule_conflict")), + (_, ObjectOperation::TakeEditAccess, OperationSuccessType::Failure) => Some( + i18n::t("cloud_object.toast.failed_start_editing") + .replace("{object_name_lowercase}", &object_name_lowercase), + ), + (_, ObjectOperation::UpdatePermissions, OperationSuccessType::Success) => Some( + i18n::t("cloud_object.toast.permissions_updated") + .replace("{object_name_lowercase}", &object_name_lowercase), + ), + (_, ObjectOperation::UpdatePermissions, OperationSuccessType::Failure) => Some( + i18n::t("cloud_object.toast.permissions_update_failed") + .replace("{object_name_lowercase}", &object_name_lowercase), + ), _ => None, } } @@ -111,10 +144,8 @@ impl CloudObjectToastMessage { success_type: &OperationSuccessType, ) -> Option { let count_objects_message = match num_objects { - 1 => "1 object".to_string(), - n => { - format!("{n} objects") - } + 1 => i18n::t("cloud_object.toast.object_count_one"), + n => i18n::t("cloud_object.toast.object_count_many").replace("{count}", &n.to_string()), }; match (operation, success_type) { // We should only show deletion failure toasts for user-initiated deletions. @@ -123,15 +154,19 @@ impl CloudObjectToastMessage { initiated_by: InitiatedBy::User, }, OperationSuccessType::Success, - ) => Some(format!("{count_objects_message} deleted forever")), - (ObjectOperation::EmptyTrash, OperationSuccessType::Success) => Some(format!( - "Trash emptied: {count_objects_message} deleted forever" - )), + ) => Some( + i18n::t("cloud_object.toast.deleted_forever") + .replace("{count_objects_message}", &count_objects_message), + ), + (ObjectOperation::EmptyTrash, OperationSuccessType::Success) => Some( + i18n::t("cloud_object.toast.trash_emptied") + .replace("{count_objects_message}", &count_objects_message), + ), (ObjectOperation::EmptyTrash, OperationSuccessType::Failure) => { - Some("Failed to empty trash".to_string()) + Some(i18n::t("cloud_object.toast.empty_trash_failed")) } (ObjectOperation::EmptyTrash, OperationSuccessType::Rejection) => { - Some("No objects in trash to empty".to_string()) + Some(i18n::t("cloud_object.toast.no_objects_to_empty")) } _ => None, } diff --git a/app/src/code/editor/comment_editor.rs b/app/src/code/editor/comment_editor.rs index 9016609f1f..bd0488056d 100644 --- a/app/src/code/editor/comment_editor.rs +++ b/app/src/code/editor/comment_editor.rs @@ -164,7 +164,7 @@ impl CommentEditor { ViewHandle, ) { let save_button = ctx.add_typed_action_view(|ctx| { - ActionButton::new("Comment", PrimaryTheme) + ActionButton::new(i18n::t("code.editor.comment.comment"), PrimaryTheme) .with_keybinding( KeystrokeSource::Fixed(Keystroke::parse("cmdorctrl-enter").unwrap_or_default()), ctx, @@ -180,7 +180,7 @@ impl CommentEditor { }); let close_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Cancel", NakedTheme) + ActionButton::new(i18n::t("common.cancel"), NakedTheme) .on_click(|ctx| { ctx.dispatch_typed_action(CommentEditorAction::CloseEditor); }) @@ -188,7 +188,7 @@ impl CommentEditor { }); let remove_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Remove", DangerNakedTheme) + ActionButton::new(i18n::t("common.remove"), DangerNakedTheme) .on_click(|ctx| { ctx.dispatch_typed_action(CommentEditorAction::RemoveComment); }) @@ -276,7 +276,7 @@ impl CommentEditor { self.is_imported_comment = origin.is_imported_from_github(); self.save_button.update(ctx, |button, ctx| { - button.set_label("Update", ctx); + button.set_label(i18n::t("common.update"), ctx); }); ctx.notify(); @@ -295,7 +295,7 @@ impl CommentEditor { self.is_imported_comment = false; self.save_button.update(ctx, |button, ctx| { - button.set_label("Comment", ctx); + button.set_label(i18n::t("code.editor.comment.comment"), ctx); }); ctx.notify(); @@ -331,7 +331,7 @@ impl CommentEditor { .finish(); let label = Text::new( - "Comment imported from GitHub".to_string(), + i18n::t("code.editor.comment.imported_from_github"), appearance.ui_font_family(), appearance.ui_font_size(), ) diff --git a/app/src/code/editor/element/gutter_button.rs b/app/src/code/editor/element/gutter_button.rs index 7005e4c097..3fd8aaf90a 100644 --- a/app/src/code/editor/element/gutter_button.rs +++ b/app/src/code/editor/element/gutter_button.rs @@ -48,7 +48,7 @@ pub(super) trait GutterButton { fn is_enabled(&self) -> bool; /// The tooltip text displayed when the button is hovered. - fn tooltip_text(&self) -> Option<&'static str>; + fn tooltip_text(&self) -> Option; /// The icon of the button. fn icon(&self) -> Icon; @@ -70,11 +70,11 @@ impl GutterButton for AddAsContextButton { self.is_enabled } - fn tooltip_text(&self) -> Option<&'static str> { + fn tooltip_text(&self) -> Option { if self.is_enabled { - Some("Add diff hunk as context") + Some(i18n::t("code.editor.gutter.add_diff_hunk_as_context")) } else { - Some("Save changes to attach as context.") + Some(i18n::t("code.editor.gutter.save_changes_attach_context")) } } @@ -99,11 +99,11 @@ impl GutterButton for RevertHunkButton { self.is_enabled } - fn tooltip_text(&self) -> Option<&'static str> { + fn tooltip_text(&self) -> Option { if self.is_enabled { - Some("Revert diff hunk") + Some(i18n::t("code.editor.gutter.revert_diff_hunk")) } else { - Some("Save changes to revert") + Some(i18n::t("code.editor.gutter.save_changes_revert")) } } @@ -152,11 +152,13 @@ impl GutterButton for CommentButton { ) } - fn tooltip_text(&self) -> Option<&'static str> { + fn tooltip_text(&self) -> Option { match self { - CommentButton::CreateNewComment => Some("Add comment on line"), - CommentButton::Disabled => Some("Save changes to add comment"), - CommentButton::AddedComment => Some("Show saved comment"), + CommentButton::CreateNewComment => { + Some(i18n::t("code.editor.gutter.add_comment_on_line")) + } + CommentButton::Disabled => Some(i18n::t("code.editor.gutter.save_changes_add_comment")), + CommentButton::AddedComment => Some(i18n::t("code.editor.gutter.show_saved_comment")), CommentButton::EditorOpenedToCreateNewComment | CommentButton::EditorOpenedToUpdateComment => None, } diff --git a/app/src/code/editor/find/view.rs b/app/src/code/editor/find/view.rs index 2cc73faf0d..6007440e70 100644 --- a/app/src/code/editor/find/view.rs +++ b/app/src/code/editor/find/view.rs @@ -48,12 +48,6 @@ const FIND_EDITOR_BORDER_WIDTH: f32 = 1.; const FIND_EDITOR_FONT_SIZE: f32 = 12.; const FIND_EDITOR_ROW_SPACING: f32 = 4.; -pub const REGEX_TOGGLE_TOOLTIP: &str = "Regex toggle"; -pub const CASE_SENSITIVE_TOOLTIP: &str = "Case sensitive search"; -pub const PRESERVE_CASE_TOOLTIP: &str = "Preserve case"; -pub const FIND_PLACEHOLDER_TEXT: &str = "Find"; -pub const REPLACE_PLACEHOLDER_TEXT: &str = "Replace"; - #[derive(Default)] struct ButtonMouseStates { match_up: MouseStateHandle, @@ -142,7 +136,7 @@ impl CodeEditorFind { }, ctx, ); - editor.set_placeholder_text(FIND_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text(i18n::t("code.editor.find.placeholder"), ctx); editor }); @@ -159,7 +153,8 @@ impl CodeEditorFind { }, ctx, ); - replace_editor.set_placeholder_text(REPLACE_PLACEHOLDER_TEXT, ctx); + replace_editor + .set_placeholder_text(i18n::t("code.editor.find.replace_placeholder"), ctx); replace_editor }); @@ -174,7 +169,7 @@ impl CodeEditorFind { let editor_height = line_height + (2. * FIND_EDITOR_PADDING) + 5.; let select_all_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Select all", SecondaryTheme) + ActionButton::new(i18n::t("code.editor.find.select_all"), SecondaryTheme) .on_click(|ctx| { ctx.dispatch_typed_action(FindAction::SelectAll); }) @@ -184,13 +179,14 @@ impl CodeEditorFind { }); let replace_all_button = ctx.add_typed_action_view(|ctx| { - let mut button = ActionButton::new("Replace all", SecondaryTheme) - .on_click(|ctx| { - ctx.dispatch_typed_action(FindAction::ReplaceAll); - }) - .with_width(72.) - .with_height(editor_height) - .with_disabled_theme(DisabledSecondaryTheme); + let mut button = + ActionButton::new(i18n::t("code.editor.find.replace_all"), SecondaryTheme) + .on_click(|ctx| { + ctx.dispatch_typed_action(FindAction::ReplaceAll); + }) + .with_width(72.) + .with_height(editor_height) + .with_disabled_theme(DisabledSecondaryTheme); button.set_disabled(true, ctx); button }); @@ -372,16 +368,20 @@ impl CodeEditorFind { pub fn emit_result_a11y_content(&mut self, ctx: &mut ViewContext) { let content = if let Some(match_index) = self.searcher.as_ref(ctx).selected_match() { AccessibilityContent::new( - format!( - "Result {} of {}.", - match_index + 1, - self.searcher.as_ref(ctx).match_count() - ), - "Use enter and shift-enter to navigate between matches. Escape to quit.", + i18n::t("code.editor.find.a11y.result") + .replace("{current}", &(match_index + 1).to_string()) + .replace( + "{total}", + &self.searcher.as_ref(ctx).match_count().to_string(), + ), + i18n::t("code.editor.find.a11y.navigate_help"), WarpA11yRole::UserAction, ) } else { - AccessibilityContent::new_without_help("No results.", WarpA11yRole::UserAction) + AccessibilityContent::new_without_help( + i18n::t("code.editor.find.a11y.no_results"), + WarpA11yRole::UserAction, + ) }; ctx.emit_a11y_content(content); } @@ -392,15 +392,15 @@ impl CodeEditorFind { let content = if let Some(match_index) = self.searcher.as_ref(ctx).selected_match() { let remaining_matches = self.searcher.as_ref(ctx).match_count(); AccessibilityContent::new( - format!( - "Successfully replaced match. Selected match is {match_index} of {remaining_matches}" - ), - "Continue pressing Enter to replace more matches, or use up/down arrows to navigate.", + i18n::t("code.editor.find.a11y.replaced_match") + .replace("{current}", &match_index.to_string()) + .replace("{total}", &remaining_matches.to_string()), + i18n::t("code.editor.find.a11y.replace_more_help"), WarpA11yRole::UserAction, ) } else { AccessibilityContent::new_without_help( - "Successfully replaced the last match.", + i18n::t("code.editor.find.a11y.replaced_last_match"), WarpA11yRole::UserAction, ) }; @@ -472,7 +472,7 @@ impl CodeEditorFind { mouse_state_handle: MouseStateHandle, on_click_action: FindAction, size: f32, - tooltip_text: Option<&str>, + tooltip_text: Option, right_margin: f32, ) -> Box { Hoverable::new(mouse_state_handle, |state| { @@ -508,7 +508,7 @@ impl CodeEditorFind { .finish(); let mut stack = Stack::new().with_child(icon); - if let (Some(tooltip_text), true) = (tooltip_text, state.is_hovered()) { + if let (Some(tooltip_text), true) = (tooltip_text.as_ref(), state.is_hovered()) { let tooltip = appearance .ui_builder() .tool_tip(tooltip_text.to_string()) @@ -744,7 +744,7 @@ impl CodeEditorFind { self.button_mouse_states.toggle_regex_search.clone(), FindAction::ToggleRegexSearch, editor_height, - Some(REGEX_TOGGLE_TOOLTIP), + Some(i18n::t("code.editor.find.regex_tooltip")), ICON_PADDING, ); let case_sensitive_icon = Container::new( @@ -756,7 +756,7 @@ impl CodeEditorFind { self.button_mouse_states.toggle_case_sensitivity.clone(), FindAction::ToggleCaseSensitivity, editor_height, - Some(CASE_SENSITIVE_TOOLTIP), + Some(i18n::t("code.editor.find.case_sensitive_tooltip")), ICON_PADDING, ), "case_sensitive_button", @@ -857,7 +857,7 @@ impl CodeEditorFind { self.button_mouse_states.toggle_preserve_case.clone(), FindAction::TogglePreserveCase, editor_height, - Some(PRESERVE_CASE_TOOLTIP), + Some(i18n::t("code.editor.find.preserve_case_tooltip")), ICON_PADDING, ); replace_editor_row.add_child(preserve_case_icon); @@ -927,20 +927,17 @@ impl View for CodeEditorFind { let match_count = self.searcher.as_ref(app).match_count(); let selected_match = self.searcher.as_ref(app).selected_match(); let description = match (match_count, selected_match) { - (0, _) | (_, None) => "Find bar for searching text in the editor.".to_string(), - (count, Some(current)) => format!( - "Find bar with {} matches found. Currently on match {} of {}.", - count, - current + 1, - count - ), + (0, _) | (_, None) => i18n::t("code.editor.find.a11y.find_bar"), + (count, Some(current)) => i18n::t("code.editor.find.a11y.find_bar_with_matches") + .replace("{count}", &count.to_string()) + .replace("{current}", &(current + 1).to_string()), }; let is_replace_focused = self.is_replace_open && self.replace_editor.is_focused(app); let help_text = if is_replace_focused { - "Replace field focused. Type replacement text, press Enter to replace current match, Tab to return to find field. Use up/down arrows to navigate matches, Escape to close." + i18n::t("code.editor.find.a11y.replace_focused") } else { - "Find field focused. Type to search text. Use Enter and Shift-Enter or up/down arrows to navigate between matches. Press Escape to close find bar." + i18n::t("code.editor.find.a11y.find_focused") }; Some(AccessibilityContent::new( diff --git a/app/src/code/editor/goto_line/view.rs b/app/src/code/editor/goto_line/view.rs index a4cedb5b0f..6091564f17 100644 --- a/app/src/code/editor/goto_line/view.rs +++ b/app/src/code/editor/goto_line/view.rs @@ -51,7 +51,7 @@ impl GoToLineView { }, ctx, ); - editor.set_placeholder_text("Line number:Column", ctx); + editor.set_placeholder_text(i18n::t("code.editor.goto_line.placeholder"), ctx); editor }); @@ -136,7 +136,7 @@ impl View for GoToLineView { let theme = appearance.theme(); let label = Text::new_inline( - "Go to line", + i18n::t("code.editor.goto_line.title"), appearance.ui_font_family(), GOTO_LINE_LABEL_FONT_SIZE, ) diff --git a/app/src/code/editor/nav_bar.rs b/app/src/code/editor/nav_bar.rs index fc15bf07e6..394a5f760b 100644 --- a/app/src/code/editor/nav_bar.rs +++ b/app/src/code/editor/nav_bar.rs @@ -72,14 +72,14 @@ impl NavBar { }); let up_label_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Previous", NakedTheme) + ActionButton::new(i18n::t("common.previous"), NakedTheme) .with_size(ButtonSize::InlineActionHeader) .with_icon(Icon::ArrowUp) .on_click(|ctx| ctx.dispatch_typed_action(NavBarAction::NavigateUp)) }); let down_label_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Next", NakedTheme) + ActionButton::new(i18n::t("common.next"), NakedTheme) .with_size(ButtonSize::InlineActionHeader) .with_icon(Icon::ArrowDown) .on_click(|ctx| ctx.dispatch_typed_action(NavBarAction::NavigateDown)) @@ -148,7 +148,7 @@ impl NavBar { ) -> Box { let diff_text = appearance .ui_builder() - .span("Hunk:") + .span(i18n::t("code.editor.nav.hunk")) .with_style(UiComponentStyles { font_color: Some(appearance.theme().sub_text_color(background).into()), ..Default::default() @@ -200,7 +200,7 @@ impl NavBar { ButtonVariant::Outlined, self.mouse_state_handles.revert_mouse_state.clone(), ) - .with_text_label("Reject".to_string()) + .with_text_label(i18n::t("code.editor.nav.reject")) .build() .on_click(|ctx, _, _| ctx.dispatch_typed_action(NavBarAction::Revert)) .finish(), diff --git a/app/src/code/editor/view.rs b/app/src/code/editor/view.rs index 7a0bfef743..4195d31b26 100644 --- a/app/src/code/editor/view.rs +++ b/app/src/code/editor/view.rs @@ -660,7 +660,7 @@ impl CodeEditorView { let trimmed = input.trim().to_string(); if trimmed.is_empty() { self.goto_line_dialog.update(ctx, |dialog, ctx| { - dialog.set_error("Please enter a line number".to_string(), ctx); + dialog.set_error(i18n::t("code.editor.goto_line.enter_line"), ctx); }); return; } @@ -672,7 +672,7 @@ impl CodeEditorView { Ok(n) if n >= 1 => n, _ => { self.goto_line_dialog.update(ctx, |dialog, ctx| { - dialog.set_error("Please enter a valid line number".to_string(), ctx); + dialog.set_error(i18n::t("code.editor.goto_line.valid_line"), ctx); }); return; } @@ -682,10 +682,8 @@ impl CodeEditorView { Ok(n) => Some(n), Err(_) => { self.goto_line_dialog.update(ctx, |dialog, ctx| { - dialog.set_error( - "Please enter a valid column number".to_string(), - ctx, - ); + dialog + .set_error(i18n::t("code.editor.goto_line.valid_column"), ctx); }); return; } diff --git a/app/src/code/file_tree/view.rs b/app/src/code/file_tree/view.rs index 0ec4778407..8a83817a85 100644 --- a/app/src/code/file_tree/view.rs +++ b/app/src/code/file_tree/view.rs @@ -66,10 +66,6 @@ use crate::workspace::ToastStack; mod editing; mod render; -const REMOTE_TEXT: &str = "The Project Explorer requires access to your local workspace, which isn’t supported in remote sessions."; -const DISABLED_TEXT: &str = "The Project Explorer requires access to your local workspace. Open a new session or navigate to an active session to view."; -const WSL_TEXT: &str = "The Project Explorer doesn't currently work in WSL."; - /// Stable identifier for an item in the file tree. /// Includes both the root directory and the index within that root's flattened list. #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -1597,10 +1593,8 @@ impl FileTreeView { fn show_exceeded_file_limit_toast(ctx: &mut ViewContext) { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::error(String::from( - "Folder has too many files to display in the file explorer.", - )) - .with_object_id("file_tree_exceeded_file_limit".to_string()); + let toast = DismissibleToast::error(i18n::t("code.file_tree.too_many_files")) + .with_object_id("file_tree_exceeded_file_limit".to_string()); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); } @@ -2332,12 +2326,12 @@ impl FileTreeView { let path_local = item.path().to_local_path_lossy(); if !is_file_content_binary(&path_local) { items.extend([ - MenuItemFields::new("Open in new pane") + MenuItemFields::new(i18n::t("code.file_tree.open_in_new_pane")) .with_on_select_action(FileTreeAction::OpenInNewPane { id: id.clone(), }) .into_item(), - MenuItemFields::new("Open in new tab") + MenuItemFields::new(i18n::t("code.file_tree.open_in_new_tab")) .with_on_select_action(FileTreeAction::OpenInNewTab { id: id.clone(), }) @@ -2345,7 +2339,7 @@ impl FileTreeView { ]); } else { items.push( - MenuItemFields::new("Open file") + MenuItemFields::new(i18n::t("common.open_file")) .with_on_select_action(FileTreeAction::ItemClicked { id: id.clone(), }) @@ -2355,7 +2349,7 @@ impl FileTreeView { } FileTreeItem::DirectoryHeader { .. } => { items.push( - MenuItemFields::new("New file") + MenuItemFields::new(i18n::t("code.file_tree.new_file")) .with_on_select_action(FileTreeAction::NewFileBelowDirectory { id: id.clone(), }) @@ -2364,7 +2358,7 @@ impl FileTreeView { items.push(MenuItem::Separator); if self.has_terminal_session { items.push( - MenuItemFields::new("cd to directory") + MenuItemFields::new(i18n::t("code.file_tree.cd_to_directory")) .with_on_select_action(FileTreeAction::CDToDirectory { id: id.clone(), }) @@ -2372,7 +2366,7 @@ impl FileTreeView { ); } items.push( - MenuItemFields::new("Open in new tab") + MenuItemFields::new(i18n::t("code.file_tree.open_in_new_tab")) .with_on_select_action(FileTreeAction::OpenInNewTab { id: id.clone() }) .into_item(), ); @@ -2380,11 +2374,11 @@ impl FileTreeView { }; let open_text = if cfg!(target_os = "macos") { - "Reveal in Finder" + i18n::t("code.file_tree.reveal_in_finder") } else if cfg!(target_os = "windows") { - "Reveal in Explorer" + i18n::t("code.file_tree.reveal_in_explorer") } else { - "Reveal in file manager" + i18n::t("code.file_tree.reveal_in_file_manager") }; items.push( MenuItemFields::new(open_text) @@ -2397,12 +2391,12 @@ impl FileTreeView { let is_repo_root_dir = id.index == 0; if !is_repo_root_dir { items.push( - MenuItemFields::new("Rename") + MenuItemFields::new(i18n::t("common.rename")) .with_on_select_action(FileTreeAction::Rename { id: id.clone() }) .into_item(), ); items.push( - MenuItemFields::new("Delete") + MenuItemFields::new(i18n::t("common.delete")) .with_on_select_action(FileTreeAction::Delete { id: id.clone() }) .into_item(), ); @@ -2414,7 +2408,7 @@ impl FileTreeView { items.push(MenuItem::Separator); } items.push( - MenuItemFields::new("Attach as context") + MenuItemFields::new(i18n::t("code.file_tree.attach_as_context")) .with_on_select_action(FileTreeAction::AttachAsContext { id: id.clone() }) .into_item(), ); @@ -2424,10 +2418,10 @@ impl FileTreeView { items.push(MenuItem::Separator); } items.extend([ - MenuItemFields::new("Copy path") + MenuItemFields::new(i18n::t("common.copy_path")) .with_on_select_action(FileTreeAction::CopyPath { id: id.clone() }) .into_item(), - MenuItemFields::new("Copy relative path") + MenuItemFields::new(i18n::t("common.copy_relative_path")) .with_on_select_action(FileTreeAction::CopyRelativePath { id: id.clone() }) .into_item(), ]); @@ -2745,7 +2739,7 @@ impl FileTreeView { ) .with_child( Text::new( - "Project explorer unavailable", + i18n::t("code.file_tree.project_explorer_unavailable"), appearance.ui_font_family(), appearance.ui_font_size() + 2., ) @@ -2932,13 +2926,13 @@ impl View for FileTreeView { #[cfg(not(feature = "local_fs"))] fn render(&self, app: &AppContext) -> Box { - self.render_error_state(REMOTE_TEXT.to_string(), app) + self.render_error_state(i18n::t("code.file_tree.remote_unavailable"), app) } #[cfg(feature = "local_fs")] fn render(&self, app: &AppContext) -> Box { if matches!(self.enablement, CodingPanelEnablementState::Disabled) { - return self.render_error_state(DISABLED_TEXT.to_string(), app); + return self.render_error_state(i18n::t("code.file_tree.disabled_unavailable"), app); } if matches!( @@ -2959,7 +2953,7 @@ impl View for FileTreeView { return if has_remote_server { self.render_loading_state(app) } else { - self.render_error_state(REMOTE_TEXT.to_string(), app) + self.render_error_state(i18n::t("code.file_tree.remote_unavailable"), app) }; } @@ -2967,7 +2961,7 @@ impl View for FileTreeView { self.enablement, CodingPanelEnablementState::UnsupportedSession ) { - return self.render_error_state(WSL_TEXT.to_string(), app); + return self.render_error_state(i18n::t("code.file_tree.wsl_unavailable"), app); } return self.render_loading_state(app); diff --git a/app/src/code/file_tree/view/render.rs b/app/src/code/file_tree/view/render.rs index 7aca6edbbb..dc1252e471 100644 --- a/app/src/code/file_tree/view/render.rs +++ b/app/src/code/file_tree/view/render.rs @@ -23,7 +23,7 @@ impl FileTreeItem { .path .file_name() .map(ToOwned::to_owned) - .unwrap_or_else(|| String::from("File")); + .unwrap_or_else(|| i18n::t("code.file_tree.file")); let icon_from_file_path = icon_from_file_path(metadata.path.as_str(), appearance).map(ImageOrIcon::Image); @@ -48,7 +48,7 @@ impl FileTreeItem { .path .file_name() .map(ToOwned::to_owned) - .unwrap_or_else(|| String::from("Folder")); + .unwrap_or_else(|| i18n::t("code.file_tree.folder")); RenderState { display_name, icon: ImageOrIcon::Icon(Icon::Folder), diff --git a/app/src/code/find_references_view.rs b/app/src/code/find_references_view.rs index 80578fa759..7c58abc004 100644 --- a/app/src/code/find_references_view.rs +++ b/app/src/code/find_references_view.rs @@ -494,11 +494,10 @@ fn render_header( let appearance = Appearance::handle(app).as_ref(app); let theme = appearance.theme(); - // "Showing X references" title let title_text = if total_refs == 1 { - "Showing 1 reference".to_string() + i18n::t("code.find_references.showing_singular") } else { - format!("Showing {total_refs} references") + i18n::t("code.find_references.showing_plural").replace("{count}", &total_refs.to_string()) }; let title = Align::new( @@ -644,7 +643,7 @@ fn render_reference_entry( } else { // Show loading indicator when line_content is None Text::new_inline( - "Loading...", + i18n::t("common.loading"), appearance.monospace_font_family(), appearance.monospace_font_size(), ) diff --git a/app/src/code/footer.rs b/app/src/code/footer.rs index c7c1456563..2ff9048832 100644 --- a/app/src/code/footer.rs +++ b/app/src/code/footer.rs @@ -366,7 +366,8 @@ impl CodeFooterView { // Create a button that dispatches EnableLSP action // The action handler will check lsp_repo_status to decide whether to install first let enable_lsp_button = server_type.map(|st| { - let label = format!("Enable {}", st.binary_name()); + let label = + i18n::t("code.footer.enable_server").replace("{server_name}", st.binary_name()); ctx.add_typed_action_view(|_ctx| { ActionButton::new(label, NakedTheme) .with_size(ButtonSize::Small) @@ -652,12 +653,14 @@ impl CodeFooterView { #[cfg_attr(target_arch = "wasm32", allow(dead_code))] fn button_label_for_status(status: &LspRepoStatus) -> Option { match status { - LspRepoStatus::DisabledAndNotInstalled { server_type } => { - Some(format!("Install {}", server_type.binary_name())) - } - LspRepoStatus::DisabledAndInstalled { server_type } => { - Some(format!("Enable {}", server_type.binary_name())) - } + LspRepoStatus::DisabledAndNotInstalled { server_type } => Some( + i18n::t("code.footer.install_server") + .replace("{server_name}", server_type.binary_name()), + ), + LspRepoStatus::DisabledAndInstalled { server_type } => Some( + i18n::t("code.footer.enable_server") + .replace("{server_name}", server_type.binary_name()), + ), _ => None, } } @@ -675,9 +678,9 @@ impl CodeFooterView { .iter() .any(|s| matches!(s, LspRepoStatus::DisabledAndNotInstalled { .. })); if any_needs_install { - Some("Install servers".to_string()) + Some(i18n::t("code.footer.install_servers")) } else { - Some("Enable servers".to_string()) + Some(i18n::t("code.footer.enable_servers")) } } } @@ -957,7 +960,7 @@ impl CodeFooterView { .root_for_workspace(self.mode.path()) .and_then(|path| path.file_name()) .and_then(|directory_name| directory_name.to_str().map(|s| s.to_string())) - .unwrap_or("unknown workspace".to_string()); + .unwrap_or_else(|| i18n::t("code.footer.unknown_workspace")); let background = appearance.theme().surface_2(); @@ -1024,9 +1027,8 @@ impl CodeFooterView { .mode .path() .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("unknown workspace") - .to_string(); + .and_then(|n| n.to_str().map(ToOwned::to_owned)) + .unwrap_or_else(|| i18n::t("code.footer.unknown_workspace")); let background = appearance.theme().surface_2(); @@ -1097,7 +1099,7 @@ impl CodeFooterView { appearance: &Appearance, mouse_state: MouseStateHandle, icon_creator: F, - label: &'static str, + label: String, action: CodeFooterViewAction, ) -> Box { let theme = appearance.theme().clone(); @@ -1115,7 +1117,7 @@ impl CodeFooterView { .finish(); let label_element = ui_builder - .span(label) + .span(label.clone()) .with_style(UiComponentStyles { font_color: Some(text_color), ..Default::default() @@ -1172,7 +1174,7 @@ impl CodeFooterView { .to_warpui_icon(ThemeFill::Solid(text_color)) .finish() }, - "Open logs", + i18n::t("code.footer.open_logs"), CodeFooterViewAction::OpenLogs, ) } @@ -1194,7 +1196,7 @@ impl CodeFooterView { .to_warpui_icon(ThemeFill::Solid(text_color)) .finish() }, - "Restart server", + i18n::t("code.footer.restart_server"), CodeFooterViewAction::RestartServer, ) } @@ -1217,7 +1219,7 @@ impl CodeFooterView { .with_uniform_padding(2.) .finish() }, - "Stop server", + i18n::t("code.footer.stop_server"), CodeFooterViewAction::StopServer, ) } @@ -1239,7 +1241,7 @@ impl CodeFooterView { .to_warpui_icon(ThemeFill::Solid(text_color)) .finish() }, - "Start server", + i18n::t("code.footer.start_server"), CodeFooterViewAction::StartServer, ) } @@ -1261,7 +1263,7 @@ impl CodeFooterView { .to_warpui_icon(ThemeFill::Solid(text_color)) .finish() }, - "Remove server", + i18n::t("code.footer.remove_server"), CodeFooterViewAction::RemoveServer, ) } @@ -1285,9 +1287,9 @@ impl CodeFooterView { .finish() }, if is_plural { - "Restart all servers" + i18n::t("code.footer.restart_all_servers") } else { - "Restart server" + i18n::t("code.footer.restart_server") }, CodeFooterViewAction::RestartAllServers, ) @@ -1313,9 +1315,9 @@ impl CodeFooterView { .finish() }, if is_plural { - "Stop all servers" + i18n::t("code.footer.stop_all_servers") } else { - "Stop server" + i18n::t("code.footer.stop_server") }, CodeFooterViewAction::StopAllServers, ) @@ -1341,11 +1343,11 @@ impl CodeFooterView { .finish() }, if !is_plural { - "Start server" + i18n::t("code.footer.start_server") } else if has_running { - "Start all stopped servers" + i18n::t("code.footer.start_all_stopped_servers") } else { - "Start all servers" + i18n::t("code.footer.start_all_servers") }, CodeFooterViewAction::StartAllServers, ) @@ -1368,7 +1370,7 @@ impl CodeFooterView { .to_warpui_icon(ThemeFill::Solid(text_color)) .finish() }, - "Manage servers", + i18n::t("code.footer.manage_servers"), CodeFooterViewAction::ManageServers, ) } @@ -1478,11 +1480,18 @@ impl CodeFooterView { .latest_progress_update() .map(|update| update.to_display_message()) .filter(|msg| !msg.trim().is_empty()) - .map(|msg| format!("{}: {msg}", server.server_name())), - LspModelState::Stopped { .. } | LspModelState::Stopping { .. } => { - Some(format!("{}: stopped", server.server_name())) - } - LspModelState::Failed { .. } => Some(format!("{}: error", server.server_name())), + .map(|msg| { + i18n::t("code.footer.server_message") + .replace("{server_name}", &server.server_name()) + .replace("{message}", &msg) + }), + LspModelState::Stopped { .. } | LspModelState::Stopping { .. } => Some( + i18n::t("code.footer.server_stopped") + .replace("{server_name}", &server.server_name()), + ), + LspModelState::Failed { .. } => Some( + i18n::t("code.footer.server_error").replace("{server_name}", &server.server_name()), + ), } } @@ -1515,11 +1524,13 @@ impl CodeFooterView { let root_name = root_path .file_name() .and_then(|s| s.to_str()) - .unwrap_or("this workspace"); + .map(str::to_owned) + .unwrap_or_else(|| i18n::t("code.footer.this_workspace")); Some(( - Some(format!( - "Language support is not currently enabled for {root_name}" - )), + Some( + i18n::t("code.footer.language_support_not_enabled") + .replace("{root_name}", &root_name), + ), true, )) } else { @@ -1540,7 +1551,11 @@ impl CodeFooterView { let server_ref = server.as_ref(app); if let LspModelState::Failed { error } = server_ref.state() { return ( - Some(format!("{}: {error}", server_ref.server_name())), + Some( + i18n::t("code.footer.server_message") + .replace("{server_name}", &server_ref.server_name()) + .replace("{message}", error), + ), false, ); } @@ -1565,7 +1580,10 @@ impl CodeFooterView { LspModelState::Stopped { .. } | LspModelState::Stopping { .. } ) { return ( - Some(format!("{}: stopped", server_ref.server_name())), + Some( + i18n::t("code.footer.server_stopped") + .replace("{server_name}", &server_ref.server_name()), + ), false, ); } @@ -1587,31 +1605,36 @@ impl CodeFooterView { .. } => match PersistedWorkspace::as_ref(app).has_enabled_lsp_server_for_file_path(path) { LSPEnablementResultForFile::UnsupportedLanguage => ( - Some("Language support is unavailable for this file type".to_string()), + Some(i18n::t( + "code.footer.language_support_unavailable_file_type", + )), false, ), LSPEnablementResultForFile::LSPNotEnabled { root_name } => match lsp_repo_status { LspRepoStatus::CheckingForInstallation => ( - Some(format!( - "Language support is not currently enabled for {}", - root_name.unwrap_or("this codebase".to_string()) + Some(i18n::t("code.footer.language_support_not_enabled").replace( + "{root_name}", + &root_name.unwrap_or_else(|| i18n::t("code.footer.this_codebase")), )), false, ), LspRepoStatus::Ready | LspRepoStatus::Enabled => ( - Some("Language server is unavailable for this codebase".to_string()), + Some(i18n::t("code.footer.language_server_unavailable_codebase")), false, ), LspRepoStatus::DisabledAndNotInstalled { .. } | LspRepoStatus::DisabledAndInstalled { .. } => ( - Some(format!( - "Language support is not currently enabled for {}", - root_name.unwrap_or("this codebase".to_string()) + Some(i18n::t("code.footer.language_support_not_enabled").replace( + "{root_name}", + &root_name.unwrap_or_else(|| i18n::t("code.footer.this_codebase")), )), true, ), LspRepoStatus::Installing { server_type } => ( - Some(format!("Installing {}...", server_type.binary_name())), + Some( + i18n::t("code.footer.installing_server") + .replace("{server_name}", server_type.binary_name()), + ), false, ), }, @@ -1630,7 +1653,8 @@ impl CodeFooterView { let root_name = root_path .file_name() .and_then(|s| s.to_str()) - .unwrap_or("this workspace"); + .map(str::to_owned) + .unwrap_or_else(|| i18n::t("code.footer.this_workspace")); // Check if any server has a CTA-worthy status if let Some(cta) = self.workspace_cta_message() { @@ -1641,7 +1665,10 @@ impl CodeFooterView { for status in lsp_repo_statuses.values() { if let LspRepoStatus::Installing { server_type } = status { return ( - Some(format!("Installing {}...", server_type.binary_name())), + Some( + i18n::t("code.footer.installing_server") + .replace("{server_name}", server_type.binary_name()), + ), false, ); } @@ -1657,7 +1684,10 @@ impl CodeFooterView { // All servers are enabled/ready but no live servers — unavailable ( - Some(format!("Language support is unavailable for {root_name}")), + Some( + i18n::t("code.footer.language_support_unavailable") + .replace("{root_name}", &root_name), + ), false, ) } @@ -1729,7 +1759,7 @@ impl View for CodeFooterView { Self::render_status_text( theme, appearance, - "Use Oz to update this config".to_string(), + i18n::t("code.footer.use_oz_to_update_config"), ), ) .finish(), diff --git a/app/src/code/global_buffer_model.rs b/app/src/code/global_buffer_model.rs index 6c424ffb13..c5fa47ed46 100644 --- a/app/src/code/global_buffer_model.rs +++ b/app/src/code/global_buffer_model.rs @@ -818,9 +818,9 @@ impl GlobalBufferModel { safe: ("[remote-buffer] No remote server client at buffer save time"), full: ("[remote-buffer] No remote server client for save: host={host_id:?}") ); - return Err(FileSaveError::RemoteError( - "No remote server client available".to_string(), - )); + return Err(FileSaveError::RemoteError(i18n::t( + "code.global_buffer_model.no_remote_server_client", + ))); }; // Flush any pending edit batch so the server has the latest @@ -1974,10 +1974,14 @@ impl GlobalBufferModel { ctx: &mut ModelContext, ) -> Result<(), FileSaveError> { let Some(state) = self.buffers.get(&file_id) else { - return Err(FileSaveError::RemoteError("Buffer not found".to_string())); + return Err(FileSaveError::RemoteError(i18n::t( + "code.global_buffer_model.buffer_not_found", + ))); }; let Some(buffer) = state.buffer.upgrade(ctx) else { - return Err(FileSaveError::RemoteError("Buffer deallocated".to_string())); + return Err(FileSaveError::RemoteError(i18n::t( + "code.global_buffer_model.buffer_deallocated", + ))); }; let content = buffer.as_ref(ctx).text().into_string(); let version = buffer.as_ref(ctx).version(); @@ -1998,7 +2002,9 @@ impl GlobalBufferModel { ctx: &mut ModelContext, ) -> Result<(), FileSaveError> { let Some(state) = self.buffers.get_mut(&file_id) else { - return Err(FileSaveError::RemoteError("Buffer not found".to_string())); + return Err(FileSaveError::RemoteError(i18n::t( + "code.global_buffer_model.buffer_not_found", + ))); }; if let BufferSource::ServerLocal { sync_clock, .. } = &mut state.source { @@ -2007,7 +2013,9 @@ impl GlobalBufferModel { } let Some(buffer) = state.buffer.upgrade(ctx) else { - return Err(FileSaveError::RemoteError("Buffer deallocated".to_string())); + return Err(FileSaveError::RemoteError(i18n::t( + "code.global_buffer_model.buffer_deallocated", + ))); }; let new_version = ContentVersion::new(); diff --git a/app/src/code/inline_diff.rs b/app/src/code/inline_diff.rs index ce585ebcd6..478975a5da 100644 --- a/app/src/code/inline_diff.rs +++ b/app/src/code/inline_diff.rs @@ -314,7 +314,10 @@ impl DiffViewer for InlineDiffView { .update(_ctx, |file_model, ctx| { file_model.delete(file_id, version, ctx) }) - .map_err(|e| format!("Failed to delete file: {e:?}"))?; + .map_err(|e| { + i18n::t("code.inline_diff.failed_delete_file") + .replace("{error}", &format!("{e:?}")) + })?; return Ok(()); } @@ -327,7 +330,7 @@ impl DiffViewer for InlineDiffView { .diff() .as_ref(_ctx) .base() - .ok_or_else(|| "Missing base content".to_string())? + .ok_or_else(|| i18n::t("code.inline_diff.missing_base_content"))? .to_string(); let version = self.editor.as_ref(_ctx).version(_ctx); @@ -335,7 +338,10 @@ impl DiffViewer for InlineDiffView { .update(_ctx, |file_model, ctx| { file_model.save(file_id, base_content, version, ctx) }) - .map_err(|e| format!("Failed to save file: {e:?}"))?; + .map_err(|e| { + i18n::t("code.inline_diff.failed_save_file") + .replace("{error}", &format!("{e:?}")) + })?; } Ok(()) diff --git a/app/src/code/local_code_editor.rs b/app/src/code/local_code_editor.rs index f0f897b96b..91ccce5469 100644 --- a/app/src/code/local_code_editor.rs +++ b/app/src/code/local_code_editor.rs @@ -1833,7 +1833,7 @@ impl LocalCodeEditorView { Shrinkable::new( 1., Text::new_inline( - "Add as context", + i18n::t("code.editor.add_as_context"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -1950,10 +1950,10 @@ impl LocalCodeEditorView { /// Creates menu items for the context menu fn context_menu_items(&self) -> Vec> { vec![ - MenuItemFields::new("Go to definition") + MenuItemFields::new(i18n::t("code.editor.go_to_definition")) .with_on_select_action(LocalCodeEditorAction::GotoDefinition) .into_item(), - MenuItemFields::new("Find references") + MenuItemFields::new(i18n::t("code.editor.find_references")) .with_on_select_action(LocalCodeEditorAction::FindReferences) .into_item(), ] @@ -2114,12 +2114,12 @@ impl DiffViewer for LocalCodeEditorView { .diff() .as_ref(ctx) .base() - .ok_or_else(|| "Missing base content".to_string())? + .ok_or_else(|| i18n::t("code.inline_diff.missing_base_content"))? .to_string(); let file_id = self .file_id() - .ok_or_else(|| "Missing file_id".to_string())?; + .ok_or_else(|| i18n::t("code.local_code_editor.missing_file_id"))?; let buffer_version = self.editor.as_ref(ctx).version(ctx); @@ -2127,7 +2127,10 @@ impl DiffViewer for LocalCodeEditorView { .update(ctx, |model, ctx| { model.save(file_id, base_content, buffer_version, ctx) }) - .map_err(|e| format!("Failed to save file: {e:?}")) + .map_err(|e| { + i18n::t("code.local_code_editor.failed_save_file") + .replace("{error}", &format!("{e:?}")) + }) } } @@ -2383,7 +2386,7 @@ pub fn render_unsaved_changes_banner( Shrinkable::new( 1., Text::new( - "This file has saved changes that are not reflected here.", + i18n::t("code.editor.saved_changes_not_reflected"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -2401,7 +2404,7 @@ pub fn render_unsaved_changes_banner( appearance .ui_builder() .button(ButtonVariant::Text, discard_mouse_state) - .with_text_label("Discard this version".into()) + .with_text_label(i18n::t("code.editor.discard_this_version")) .with_style(UiComponentStyles { height: Some(24.), padding: Some(Coords { @@ -2423,7 +2426,7 @@ pub fn render_unsaved_changes_banner( appearance .ui_builder() .button(ButtonVariant::Outlined, overwrite_mouse_state) - .with_text_label("Overwrite".into()) + .with_text_label(i18n::t("code.editor.overwrite")) .with_style(UiComponentStyles { font_color: Some(appearance.theme().active_ui_text_color().into()), ..Default::default() @@ -2480,7 +2483,7 @@ pub fn render_remote_disconnected_banner(appearance: &Appearance) -> Box display_path_with_host(location, false, ctx), - None => "Untitled".to_string(), + None => i18n::t("common.untitled"), }; self.pane_configuration.update(ctx, |pane_config, ctx| { @@ -830,7 +830,7 @@ impl CodeView { if self.tab_group.len() > 1 { secondary.push_str(&format!(" (+{})", self.tab_group.len() - 1)); } else if is_new { - secondary.push_str(" (new)"); + secondary.push_str(&i18n::t("code.view.new_suffix")); } pane_config.set_title(title, ctx); @@ -921,7 +921,7 @@ impl CodeView { fn display_load_failure(window_id: WindowId, ctx: &mut ViewContext) { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::error(String::from("Failed to load file.")) + let toast = DismissibleToast::error(i18n::t("code.toast.load_file_failed")) .with_object_id("failed_to_load_file".to_string()); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); @@ -929,7 +929,7 @@ impl CodeView { fn display_save_failure(window_id: WindowId, ctx: &mut ViewContext) { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::error(String::from("Failed to save file.")) + let toast = DismissibleToast::error(i18n::t("code.toast.save_file_failed")) .with_object_id("failed_to_save_file".to_string()); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); @@ -938,7 +938,7 @@ impl CodeView { fn display_remote_disconnected_save_failure(window_id: WindowId, ctx: &mut ViewContext) { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { let toast = - DismissibleToast::error(String::from("Cannot save — remote session disconnected.")) + DismissibleToast::error(i18n::t("code.toast.save_file_remote_disconnected")) .with_object_id("failed_to_save_file_remote_disconnected".to_string()); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); @@ -946,7 +946,7 @@ impl CodeView { fn display_save_success(window_id: WindowId, ctx: &mut ViewContext) { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::success(String::from("File saved.")) + let toast = DismissibleToast::success(i18n::t("code.toast.file_saved")) .with_object_id("file_saved".to_string()); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); @@ -1075,7 +1075,7 @@ impl CodeView { ButtonVariant::Outlined, tab.mouse_state_handles.reject_mouse_state.clone(), ) - .with_text_label("Reject".to_string()) + .with_text_label(i18n::t("common.reject")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(CodeViewAction::RejectPendingDiffs) @@ -1093,7 +1093,7 @@ impl CodeView { ButtonVariant::Outlined, tab.mouse_state_handles.accept_mouse_state.clone(), ) - .with_text_label("Accept and save".to_string()) + .with_text_label(i18n::t("code.accept_and_save")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action( @@ -1509,7 +1509,7 @@ impl CodeView { .as_ref() .map(|loc| display_name_with_host(loc, app)) .filter(|n| !n.is_empty()) - .unwrap_or_else(|| "Untitled".to_string()); + .unwrap_or_else(|| i18n::t("common.untitled")); let language_icon = icon_from_file_path(&file_name, appearance, ItemHighlightState::Default); row.add_child( @@ -1889,7 +1889,7 @@ impl CodeView { .map(|loc| display_name_with_host(loc, app)) .filter(|n| !n.is_empty()) }) - .unwrap_or_else(|| "Untitled".to_string()); + .unwrap_or_else(|| i18n::t("common.untitled")); let appearance = Appearance::as_ref(app); let is_pane_dragging = header_ctx.draggable_state.is_dragging(); @@ -2027,9 +2027,12 @@ impl CodeView { }; let mut items = vec![ - MenuItemFields::new_with_label("Close saved", &format!("{modifier_keys} U")) - .with_on_select_action(CodeViewAction::CloseSaved) - .into_item(), + MenuItemFields::new_with_label( + i18n::t("code.close_saved"), + format!("{modifier_keys} U"), + ) + .with_on_select_action(CodeViewAction::CloseSaved) + .into_item(), MenuItemFields::toggle_pane_action(is_maximized) .with_on_select_action(CodeViewAction::ToggleMaximized) .into_item(), @@ -2045,7 +2048,7 @@ impl CodeView { if active_location.is_some() { items.push(MenuItem::Separator); items.push( - MenuItemFields::new("Copy file path") + MenuItemFields::new(i18n::t("common.copy_file_path")) .with_on_select_action(CodeViewAction::CopyFilePath) .into_item(), ); @@ -2053,11 +2056,11 @@ impl CodeView { if local_path.is_some() { let reveal_label = if cfg!(target_os = "macos") { - "Reveal in Finder" + i18n::t("code.file_tree.reveal_in_finder") } else if cfg!(target_os = "windows") { - "Reveal in Explorer" + i18n::t("code.file_tree.reveal_in_explorer") } else { - "Reveal in file manager" + i18n::t("code.file_tree.reveal_in_file_manager") }; items.push( MenuItemFields::new(reveal_label) @@ -2076,7 +2079,7 @@ impl CodeView { }); if is_md { items.push( - MenuItemFields::new("View Markdown preview") + MenuItemFields::new(i18n::t("code.view_markdown_preview")) .with_on_select_action(CodeViewAction::RenderMarkdown) .into_item(), ); diff --git a/app/src/code_review/code_review_header/mod.rs b/app/src/code_review/code_review_header/mod.rs index 70fbd66d23..f92c93696b 100644 --- a/app/src/code_review/code_review_header/mod.rs +++ b/app/src/code_review/code_review_header/mod.rs @@ -320,7 +320,7 @@ impl CodeReviewHeader { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Discard all".to_string(), + i18n::t("code_review.discard.all"), Icon::ReverseLeft.to_warpui_icon(warp_core::ui::theme::Fill::Solid( sub_text_color.into_solid(), )), @@ -433,7 +433,7 @@ impl CodeReviewHeader { })) .with_tooltip(move || { ui_builder - .tool_tip("Add diff set as context".to_owned()) + .tool_tip(i18n::t("code_review.add_diff_set_context")) .build() .finish() }) @@ -489,6 +489,6 @@ impl CodeReviewHeader { fn get_header_text(diff_state_model: &ModelHandle, app: &AppContext) -> String { let branch_name = diff_state_model.read(app, |model, ctx| model.get_current_branch_name(ctx)); - branch_name.unwrap_or("Reviewing open changes".to_string()) + branch_name.unwrap_or_else(|| i18n::t("code_review.header.reviewing_open_changes")) } } diff --git a/app/src/code_review/code_review_view.rs b/app/src/code_review/code_review_view.rs index 4ee482dff3..7a67235aae 100644 --- a/app/src/code_review/code_review_view.rs +++ b/app/src/code_review/code_review_view.rs @@ -207,9 +207,9 @@ where .with_tooltip(move || { ui_builder .tool_tip(if is_sidebar_expanded { - "Hide file navigation".to_owned() + i18n::t("code_review.file_nav.hide") } else { - "Show file navigation".to_owned() + i18n::t("code_review.file_nav.show") }) .build() .finish() @@ -267,17 +267,15 @@ const CODE_REVIEW_EDITOR_LINE_HEIGHT_RATIO: f32 = 1.4; /// Extra scroll buffer (in pixels) added when scrolling to a line that has a comment editor below it. const COMMENT_EDITOR_SCROLL_BUFFER: f32 = 200.0; -pub const CODE_REVIEW_TOOLTIP_TEXT: &str = "View changes"; -const REMOTE_TEXT: &str = "Diffs only work for local workspaces."; -const DISABLED_TEXT: &str = "Diffs only work for git repositories."; -const WSL_TEXT: &str = "Diffs don't currently work in WSL."; +pub fn code_review_tooltip_text() -> String { + i18n::t("code_review.view_changes") +} pub fn get_discard_button_disabled_tooltip(git_operation_blocked: bool) -> String { if git_operation_blocked { - "Cannot discard changes while a git operation (merge, rebase, etc.) is in progress" - .to_string() + i18n::t("code_review.discard.disabled_git_operation") } else { - "No changes to discard".to_string() + i18n::t("code_review.discard.no_changes") } } @@ -285,13 +283,13 @@ pub fn get_discard_button_disabled_tooltip(git_operation_blocked: bool) -> Strin /// live shortcut for `code_review:toggle_file_navigation` when one is bound. fn file_nav_button_tooltip(is_sidebar_expanded: bool, app: &AppContext) -> String { let label = if is_sidebar_expanded { - "Hide file navigation" + i18n::t("code_review.file_nav.hide") } else { - "Show file navigation" + i18n::t("code_review.file_nav.show") }; match keybinding_name_to_display_string("code_review:toggle_file_navigation", app) { Some(shortcut) => format!("{label} ({shortcut})"), - None => label.to_string(), + None => label, } } @@ -471,26 +469,38 @@ impl DiscardOperationType { pub fn title(&self) -> String { match self { DiscardOperationType::AllUncommittedChanges => { - "Discard uncommitted changes?".to_string() + i18n::t("code_review.discard.title.all_uncommitted") } DiscardOperationType::FileUncommittedChanges => { - "Discard all uncommitted changes to file?".to_string() + i18n::t("code_review.discard.title.file_uncommitted") + } + DiscardOperationType::AllChangesAgainstBranch(_) => { + i18n::t("code_review.discard.title.all_changes") } - DiscardOperationType::AllChangesAgainstBranch(_) => "Discard all changes?".to_string(), DiscardOperationType::FileChangesAgainstBranch(_) => { - "Discard all changes to file?".to_string() + i18n::t("code_review.discard.title.file_changes") } } } pub fn description(&self) -> Option { match self { - DiscardOperationType::AllUncommittedChanges => Some("You're about to discard all local changes that haven't been committed.".to_string()), - DiscardOperationType::FileUncommittedChanges => Some("This will restore this file to the last committed version and discard local edits.".to_string()), - DiscardOperationType::AllChangesAgainstBranch(None) => Some("You're about to discard all committed and uncommitted changes.".to_string()), - DiscardOperationType::FileChangesAgainstBranch(None) => Some("This will restore this file to the main branch version and discard all committed and uncommitted edits.".to_string()), - DiscardOperationType::AllChangesAgainstBranch(Some(_)) => Some("You're about to discard all committed and uncommitted changes.".to_string()), - DiscardOperationType::FileChangesAgainstBranch(Some(branch)) => Some(format!("This will reset this file to the {branch} branch version and discard all committed and uncommitted edits.")), + DiscardOperationType::AllUncommittedChanges => { + Some(i18n::t("code_review.discard.description.all_uncommitted")) + } + DiscardOperationType::FileUncommittedChanges => { + Some(i18n::t("code_review.discard.description.file_uncommitted")) + } + DiscardOperationType::AllChangesAgainstBranch(_) => { + Some(i18n::t("code_review.discard.description.all_changes")) + } + DiscardOperationType::FileChangesAgainstBranch(None) => { + Some(i18n::t("code_review.discard.description.file_changes_main")) + } + DiscardOperationType::FileChangesAgainstBranch(Some(branch)) => Some( + i18n::t("code_review.discard.description.file_changes_branch") + .replace("{branch}", branch), + ), } } @@ -1105,7 +1115,7 @@ impl CodeReviewView { let maximize_button = ctx.add_typed_action_view(move |_| { // Since the view isn't part of a pane group yet, default to not-maximized. The button will be updated //when focus state changes. - let (icon, tooltip_text) = (Icon::Maximize, "Maximize"); + let (icon, tooltip_text) = (Icon::Maximize, i18n::t("code_review.maximize")); ActionButton::new("", NakedTheme) .with_icon(icon) @@ -1134,7 +1144,7 @@ impl CodeReviewView { }); let git_primary_action_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Commit", SecondaryTheme) + ActionButton::new(i18n::t("code_review.git.commit"), SecondaryTheme) .with_size(ButtonSize::Small) .with_icon(Icon::GitCommit) .with_adjoined_side(AdjoinedSide::Right) @@ -1175,7 +1185,7 @@ impl CodeReviewView { let undo_action_button = ctx.add_typed_action_view(move |ctx| { let keybinding = custom_tag_to_keystroke(CustomAction::Undo.into()); - let mut action_button = ActionButton::new("Undo", NakedTheme) + let mut action_button = ActionButton::new(i18n::t("code_review.undo"), NakedTheme) .with_size(ButtonSize::Small) .on_click(move |ctx| { ctx.dispatch_typed_action(WorkspaceAction::UndoRevertInCodeReviewPane { @@ -1192,12 +1202,15 @@ impl CodeReviewView { }); let discard_confirm_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Discard changes", DangerPrimaryTheme) - .on_click(|ctx| ctx.dispatch_typed_action(CodeReviewAction::ConfirmDiscardFile)) + ActionButton::new( + i18n::t("code_review.discard.confirm_button"), + DangerPrimaryTheme, + ) + .on_click(|ctx| ctx.dispatch_typed_action(CodeReviewAction::ConfirmDiscardFile)) }); let discard_cancel_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Cancel", NakedTheme).on_click(|ctx| { + ActionButton::new(i18n::t("common.cancel"), NakedTheme).on_click(|ctx| { ctx.dispatch_typed_action(CodeReviewAction::CancelDiscardFile); }) }); @@ -1265,9 +1278,9 @@ impl CodeReviewView { let header = CodeReviewHeader::new(); let init_project_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Initialize codebase", NakedTheme) + ActionButton::new(i18n::t("code_review.init_codebase"), NakedTheme) .with_size(ButtonSize::Small) - .with_tooltip("Enables codebase indexing and WARP.md") + .with_tooltip(i18n::t("code_review.init_codebase.tooltip")) .with_tooltip_alignment(TooltipAlignment::Center) .on_click(|ctx| { ctx.dispatch_typed_action(CodeReviewAction::InitProjectForCurrentDirectory) @@ -1276,9 +1289,9 @@ impl CodeReviewView { #[cfg(not(target_family = "wasm"))] let open_repository_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Open repository", NakedTheme) + ActionButton::new(i18n::t("code_review.open_repository"), NakedTheme) .with_size(ButtonSize::Small) - .with_tooltip("Navigate to a repo and initialize it for coding") + .with_tooltip(i18n::t("code_review.open_repository.tooltip")) .with_tooltip_alignment(TooltipAlignment::Center) .on_click(|ctx| ctx.dispatch_typed_action(CodeReviewAction::OpenRepository)) }); @@ -1392,9 +1405,9 @@ impl CodeReviewView { let is_maximized = focus_handle.is_maximized(ctx); let (icon, tooltip) = if is_maximized { - (Icon::Minimize, "Restore") + (Icon::Minimize, i18n::t("code_review.restore")) } else { - (Icon::Maximize, "Maximize") + (Icon::Maximize, i18n::t("code_review.maximize")) }; self.maximize_button.update(ctx, |button, ctx| { @@ -1467,7 +1480,7 @@ impl CodeReviewView { // 1. Always add "Uncommitted changes" first. targets.push(DiffTarget::new( - "Uncommitted changes", + i18n::t("code_review.diff_target.uncommitted_changes"), DiffMode::Head, matches!(current_mode, DiffMode::Head), )); @@ -2562,7 +2575,7 @@ impl CodeReviewView { let discard_tooltip_text = if git_operation_blocked { get_discard_button_disabled_tooltip(git_operation_blocked) } else { - "Discard changes".to_string() + i18n::t("code_review.discard.confirm_button") }; let mut file_states = vec![]; @@ -2613,7 +2626,7 @@ impl CodeReviewView { ActionButton::new("", NakedTheme) .with_icon(Icon::LinkExternal) .with_size(ButtonSize::InlineActionHeader) - .with_tooltip("Open file") + .with_tooltip(i18n::t("code_review.open_file")) .on_click(move |ctx| { ctx.dispatch_typed_action(CodeReviewAction::OpenInNewTab { path: open_tab_path.clone(), @@ -2650,7 +2663,7 @@ impl CodeReviewView { ActionButton::new("", NakedTheme) .with_icon(Icon::Paperclip) .with_size(ButtonSize::InlineActionHeader) - .with_tooltip("Add file diff as context") + .with_tooltip(i18n::t("code_review.add_file_diff_context")) .on_click(move |ctx| { ctx.dispatch_typed_action(CodeReviewAction::AddDiffSetAsContext( DiffSetScope::File(context_path.clone()), @@ -2663,7 +2676,7 @@ impl CodeReviewView { ActionButton::new("", NakedTheme) .with_icon(Icon::Copy) .with_size(ButtonSize::InlineActionHeader) - .with_tooltip("Copy file path") + .with_tooltip(i18n::t("code_review.copy_file_path")) .on_click(move |ctx| { ctx.dispatch_typed_action(CodeReviewAction::CopyFilePath(copy_path.clone())) }) @@ -3636,7 +3649,7 @@ impl CodeReviewView { fn render_placeholder_header(appearance: &Appearance) -> Box { let theme = appearance.theme(); - let header_text = "Loading open changes..."; + let header_text = i18n::t("code_review.loading_open_changes"); let loading_icon = Icon::Loading .to_warpui_icon(warp_core::ui::theme::Fill::Solid( internal_colors::neutral_6(theme), @@ -3790,7 +3803,7 @@ impl CodeReviewView { ) .with_child( Text::new( - "Error loading diffs", + i18n::t("code_review.error_loading_diffs"), appearance.ui_font_family(), appearance.ui_font_size() + 2., ) @@ -3833,7 +3846,7 @@ impl CodeReviewView { ) .with_text_and_icon_label(TextAndIcon::new( TextAndIconAlignment::IconFirst, - " Retry".to_string(), + format!(" {}", i18n::t("code_review.retry")), Icon::Refresh.to_warpui_icon(warp_core::ui::theme::Fill::Solid( theme.main_text_color(theme.background()).into(), )), @@ -3871,7 +3884,7 @@ impl CodeReviewView { pub fn render_no_repo_found_state( appearance: &Appearance, - message: &'static str, + message: String, open_repo_button: Option>, ) -> Box { let theme = appearance.theme(); @@ -3898,7 +3911,7 @@ impl CodeReviewView { ) .with_child( Text::new( - "Cannot detect diffs for this folder", + i18n::t("code_review.no_repo.title"), appearance.ui_font_family(), appearance.ui_font_size() + 2., ) @@ -3939,21 +3952,33 @@ impl CodeReviewView { appearance: &Appearance, open_repo_button: Option>, ) -> Box { - Self::render_no_repo_found_state(appearance, REMOTE_TEXT, open_repo_button) + Self::render_no_repo_found_state( + appearance, + i18n::t("code_review.no_repo.remote"), + open_repo_button, + ) } pub fn render_wsl_state( appearance: &Appearance, open_repo_button: Option>, ) -> Box { - Self::render_no_repo_found_state(appearance, WSL_TEXT, open_repo_button) + Self::render_no_repo_found_state( + appearance, + i18n::t("code_review.no_repo.wsl"), + open_repo_button, + ) } pub fn render_not_repo_state( appearance: &Appearance, open_repo_button: Option>, ) -> Box { - Self::render_no_repo_found_state(appearance, DISABLED_TEXT, open_repo_button) + Self::render_no_repo_found_state( + appearance, + i18n::t("code_review.no_repo.disabled"), + open_repo_button, + ) } fn render_loaded_state( @@ -4039,15 +4064,19 @@ impl CodeReviewView { .finish(), ) .with_child( - Text::new("No open changes", appearance.ui_font_family(), 16.) - .with_style(Properties::default().weight(Weight::Semibold)) - .with_color(theme.main_text_color(theme.surface_2()).into()) - .finish(), + Text::new( + i18n::t("code_review.no_changes.title"), + appearance.ui_font_family(), + 16., + ) + .with_style(Properties::default().weight(Weight::Semibold)) + .with_color(theme.main_text_color(theme.surface_2()).into()) + .finish(), ) .with_child( Container::new( Text::new( - "As you or the Agent make changes, you'll be able to track them here.", + i18n::t("code_review.no_changes.description"), appearance.ui_font_family(), 14., ) @@ -4090,7 +4119,8 @@ impl CodeReviewView { zero_state_column.add_child( Container::new( Text::new( - format!("Repo is initialized with a {file_name} file."), + i18n::t("code_review.no_changes.repo_initialized") + .replace("{file_name}", file_name), appearance.ui_font_family(), 12., ) @@ -4237,7 +4267,8 @@ impl CodeReviewView { self.clear_review_comments(ctx); ToastStack::handle(ctx).update(ctx, |stack, ctx| { - let toast = DismissibleToast::default("Comments sent to agent".into()); + let toast = + DismissibleToast::default(i18n::t("code_review.comments.sent_to_agent")); stack.add_ephemeral_toast(toast, self.window_id, ctx); }); ctx.emit(CodeReviewViewEvent::ReviewSubmitted); @@ -4245,7 +4276,7 @@ impl CodeReviewView { } ReviewSubmissionResult::Error => { log::error!("Failed to submit review comments"); - let error_message = "Could not submit comments to the agent".to_string(); + let error_message = i18n::t("code_review.comments.submit_error"); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { let toast = DismissibleToast::error(error_message); toast_stack.add_ephemeral_toast(toast, self.window_id, ctx); @@ -4908,8 +4939,8 @@ impl CodeReviewView { if editor_state.has_unsaved_changes(app) { let save_keystroke = Keystroke::parse("cmdorctrl-s").unwrap_or_default(); let save_shortcut = save_keystroke.displayed(); - let tooltip_text = - format!("This file has unsaved changes. {save_shortcut} to save"); + let tooltip_text = i18n::t("code_review.unsaved_file.tooltip") + .replace("{shortcut}", &save_shortcut); render_unsaved_circle_with_tooltip( editor_state.unsaved_changes_mouse_state(), tooltip_text, @@ -5145,7 +5176,7 @@ impl CodeReviewView { if diff_size == DiffSize::Unrenderable { return Self::styled_file_content_container( Text::new( - "Diff is too large to render", + i18n::t("code_review.file_content.diff_too_large"), appearance.monospace_font_family(), appearance.monospace_font_size(), ) @@ -5158,7 +5189,7 @@ impl CodeReviewView { if file.file_diff.is_binary { Self::styled_file_content_container( Text::new( - "Binary file - no diff available", + i18n::t("code_review.file_content.binary"), appearance.monospace_font_family(), appearance.monospace_font_size(), ) @@ -5169,7 +5200,7 @@ impl CodeReviewView { } else if file.file_diff.status.is_renamed() && file.file_diff.is_empty() { Self::styled_file_content_container( Text::new( - "File renamed without changes", + i18n::t("code_review.file_content.renamed_without_changes"), appearance.monospace_font_family(), appearance.monospace_font_size(), ) @@ -5180,7 +5211,7 @@ impl CodeReviewView { } else if file.file_diff.status.is_new_file() && file.file_diff.is_empty() { Self::styled_file_content_container( Text::new( - "New empty file", + i18n::t("code_review.file_content.new_empty_file"), appearance.monospace_font_family(), appearance.monospace_font_size(), ) @@ -5210,7 +5241,7 @@ impl CodeReviewView { } else { Self::styled_file_content_container( Text::new( - "Unable to load file content", + i18n::t("code_review.file_content.unable_to_load"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -5302,7 +5333,7 @@ impl CodeReviewView { if self.discard_dialog_state.discard_file_paths.is_empty() { return Text::new( - "No file selected", + i18n::t("code_review.discard.no_file_selected"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -5317,7 +5348,7 @@ impl CodeReviewView { let CodeReviewViewState::Loaded(loaded) = self.state() else { return Text::new( - "No files to discard", + i18n::t("code_review.discard.no_files_to_discard"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -5445,8 +5476,10 @@ impl CodeReviewView { ) .check(self.discard_dialog_state.stash_changes_enabled) .with_label( - appearance.ui_builder().span("Stash changes").with_style( - UiComponentStyles { + appearance + .ui_builder() + .span(i18n::t("code_review.discard.stash_changes")) + .with_style(UiComponentStyles { font_size: Some(appearance.ui_font_size()), font_color: Some( appearance @@ -5455,8 +5488,7 @@ impl CodeReviewView { .into(), ), ..Default::default() - }, - ), + }), ) .build() .on_click(|ctx, _, _| { @@ -5586,9 +5618,9 @@ impl CodeReviewView { let toast_id = self.revert_hunk_toast_id(ctx); crate::workspace::ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = crate::view_components::DismissibleToast::default( - "Diff removed".to_string(), - ) + let toast = crate::view_components::DismissibleToast::default(i18n::t( + "code_review.diff_removed", + )) .with_object_id(toast_id) .with_action_button(self.undo_action_button.clone()); toast_stack.add_ephemeral_toast(toast, self.window_id, ctx); @@ -5688,9 +5720,9 @@ impl CodeReviewView { if is_long_running { let toast_id = self.attach_context_not_allowed_toast_id(ctx); crate::workspace::ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = crate::view_components::DismissibleToast::default( - "Cannot attach context when terminal is running".to_string(), - ) + let toast = crate::view_components::DismissibleToast::default(i18n::t( + "code_review.cannot_attach_context_terminal_running", + )) .with_object_id(toast_id); toast_stack.add_ephemeral_toast(toast, self.window_id, ctx); }); @@ -5800,9 +5832,9 @@ impl CodeReviewView { if !is_input_box_visible { let toast_id = self.attach_diff_not_allowed_toast_id(ctx); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::default( - "Cannot attach diff while input is not available".to_string(), - ) + let toast = DismissibleToast::default(i18n::t( + "code_review.add_context.input_unavailable", + )) .with_object_id(toast_id); toast_stack.add_ephemeral_toast(toast, self.window_id, ctx); }); @@ -6514,10 +6546,13 @@ impl CodeReviewView { PrimaryGitActionMode::Commit => { let disabled = !self.has_uncommitted_changes(ctx); self.git_primary_action_button.update(ctx, |button, ctx| { - button.set_label("Commit", ctx); + button.set_label(i18n::t("code_review.git.commit"), ctx); button.set_icon(Some(Icon::GitCommit), ctx); button.set_disabled(disabled, ctx); - button.set_tooltip(disabled.then_some("No changes to commit"), ctx); + button.set_tooltip( + disabled.then(|| i18n::t("code_review.git.no_changes_to_commit")), + ctx, + ); button.set_on_click( |ctx| ctx.dispatch_typed_action(CodeReviewAction::OpenCommitDialog), ctx, @@ -6526,12 +6561,15 @@ impl CodeReviewView { }); self.git_operations_chevron.update(ctx, |button, ctx| { button.set_disabled(disabled, ctx); - button.set_tooltip(disabled.then_some("No git actions available"), ctx); + button.set_tooltip( + disabled.then(|| i18n::t("code_review.git.no_actions_available")), + ctx, + ); }); } PrimaryGitActionMode::Push => { self.git_primary_action_button.update(ctx, |button, ctx| { - button.set_label("Push", ctx); + button.set_label(i18n::t("code_review.git.push"), ctx); button.set_icon(Some(Icon::ArrowUp), ctx); button.set_disabled(false, ctx); button.clear_tooltip(ctx); @@ -6547,7 +6585,7 @@ impl CodeReviewView { } PrimaryGitActionMode::CreatePr => { self.git_primary_action_button.update(ctx, |button, ctx| { - button.set_label("Create PR", ctx); + button.set_label(i18n::t("code_review.git.create_pr"), ctx); button.set_icon(Some(Icon::Github), ctx); button.set_disabled(false, ctx); button.clear_tooltip(ctx); @@ -6570,7 +6608,8 @@ impl CodeReviewView { button.set_icon(Some(Icon::Github), ctx); button.set_disabled(is_pr_info_refreshing, ctx); button.set_tooltip( - is_pr_info_refreshing.then_some("Refreshing PR info"), + is_pr_info_refreshing + .then(|| i18n::t("code_review.git.refreshing_pr_info")), ctx, ); button.set_on_click( @@ -6585,7 +6624,7 @@ impl CodeReviewView { } PrimaryGitActionMode::Publish => { self.git_primary_action_button.update(ctx, |button, ctx| { - button.set_label("Publish", ctx); + button.set_label(i18n::t("code_review.git.publish"), ctx); button.set_icon(Some(Icon::UploadCloud), ctx); button.set_disabled(false, ctx); button.clear_tooltip(ctx); @@ -6605,7 +6644,7 @@ impl CodeReviewView { /// only the disabled state flips across modes (enabled in Commit mode, /// disabled in Push mode where there's nothing to commit). fn commit_menu_item(disabled: bool) -> MenuItem { - MenuItemFields::new("Commit") + MenuItemFields::new(i18n::t("code_review.git.commit")) .with_icon(Icon::GitCommit) .with_on_select_action(CodeReviewAction::OpenCommitDialog) .with_disabled(disabled) @@ -6617,13 +6656,13 @@ impl CodeReviewView { /// sets the upstream). fn push_or_publish_menu_item(has_upstream: bool, disabled: bool) -> MenuItem { if has_upstream { - MenuItemFields::new("Push") + MenuItemFields::new(i18n::t("code_review.git.push")) .with_icon(Icon::ArrowUp) .with_on_select_action(CodeReviewAction::OpenPushDialog) .with_disabled(disabled) .into_item() } else { - MenuItemFields::new("Publish") + MenuItemFields::new(i18n::t("code_review.git.publish")) .with_icon(Icon::UploadCloud) .with_on_select_action(CodeReviewAction::PublishBranch) .with_disabled(disabled) @@ -6648,7 +6687,7 @@ impl CodeReviewView { let is_on_main = diff_state.is_on_main_branch(app); let has_upstream = diff_state.upstream_ref(app).is_some(); let upstream_differs_from_main = diff_state.upstream_differs_from_main(app); - MenuItemFields::new("Create PR") + MenuItemFields::new(i18n::t("code_review.git.create_pr")) .with_icon(Icon::Github) .with_on_select_action(CodeReviewAction::OpenCreatePrDialog) .with_disabled( @@ -6722,7 +6761,7 @@ impl CodeReviewView { if FeatureFlag::DiffSetAsContext.is_enabled() && has_changes { items.push( - MenuItemFields::new("Add diff set as context") + MenuItemFields::new(i18n::t("code_review.add_diff_set_context")) .with_icon(Icon::Paperclip) .with_on_select_action(CodeReviewAction::AddDiffSetAsContext(DiffSetScope::All)) .into_item(), @@ -6730,9 +6769,12 @@ impl CodeReviewView { } let (comment_label, comment_icon) = if self.get_existing_diffset_comment(ctx).is_some() { - ("Show saved comment", Icon::MessageText) + ( + i18n::t("code_review.comments.show_saved"), + Icon::MessageText, + ) } else { - ("Add comment", Icon::MessagePlusSquare) + (i18n::t("code_review.comments.add"), Icon::MessagePlusSquare) }; items.push( @@ -6757,7 +6799,7 @@ impl CodeReviewView { let is_ai_enabled = AISettings::as_ref(ctx).is_any_ai_enabled(ctx); if is_ai_enabled && FeatureFlag::DiffSetAsContext.is_enabled() && has_changes { items.push( - MenuItemFields::new("Add diff set as context") + MenuItemFields::new(i18n::t("code_review.add_diff_set_context")) .with_icon(Icon::Paperclip) .with_on_select_action(CodeReviewAction::AddDiffSetAsContext(DiffSetScope::All)) .into_item(), @@ -6767,9 +6809,12 @@ impl CodeReviewView { if FeatureFlag::FileAndDiffSetComments.is_enabled() && has_changes { let (comment_label, comment_icon) = if self.get_existing_diffset_comment(ctx).is_some() { - ("Show saved comment", Icon::MessageText) + ( + i18n::t("code_review.comments.show_saved"), + Icon::MessageText, + ) } else { - ("Add comment", Icon::MessagePlusSquare) + (i18n::t("code_review.comments.add"), Icon::MessagePlusSquare) }; items.push( @@ -6782,7 +6827,7 @@ impl CodeReviewView { if FeatureFlag::DiscardPerFileAndAllChanges.is_enabled() && has_changes { items.push( - MenuItemFields::new("Discard all") + MenuItemFields::new(i18n::t("code_review.discard.all")) .with_icon(Icon::ReverseLeft) .with_on_select_action(CodeReviewAction::ShowDiscardConfirmDialog(None)) .into_item(), @@ -7608,7 +7653,7 @@ impl BackingView for CodeReviewView { _ctx: &view::HeaderRenderContext<'_>, _app: &AppContext, ) -> view::HeaderContent { - view::HeaderContent::simple("Reviewing code changes") + view::HeaderContent::simple(i18n::t("code_review.header.reviewing_changes")) } fn set_focus_handle(&mut self, focus_handle: PaneFocusHandle, ctx: &mut ViewContext) { diff --git a/app/src/code_review/comment_list_view.rs b/app/src/code_review/comment_list_view.rs index 37a37b5be3..4d8b346fe4 100644 --- a/app/src/code_review/comment_list_view.rs +++ b/app/src/code_review/comment_list_view.rs @@ -1,5 +1,3 @@ -use std::borrow::Cow; - use indexmap::IndexMap; use pathfinder_color::ColorU; use pathfinder_geometry::vector::vec2f; @@ -57,22 +55,26 @@ use crate::view_components::action_button::{ }; use crate::workspace::view::right_panel::ReviewDestination; -/// Header text for the outdated section when there is exactly one outdated comment. -const OUTDATED_SECTION_HEADER_SINGULAR: &str = "1 comment will be omitted because it is outdated."; -/// Header text format for the outdated section when there are multiple outdated comments. -/// Use with `format!` to insert the count. -const OUTDATED_SECTION_HEADER_PLURAL_FMT: &str = - " comments will be omitted because they are outdated."; - /// Returns the header text for the outdated section based on the number of outdated comments. -fn outdated_section_header_text(count: usize) -> Cow<'static, str> { +fn outdated_section_header_text(count: usize) -> String { if count == 1 { - Cow::Borrowed(OUTDATED_SECTION_HEADER_SINGULAR) + i18n::t("code_review.comments.outdated_omitted.singular") } else { - Cow::Owned(format!("{count}{OUTDATED_SECTION_HEADER_PLURAL_FMT}")) + i18n::t("code_review.comments.outdated_omitted.plural") + .replace("{count}", &count.to_string()) } } +fn comment_count_label(count: usize, outdated_only: bool) -> String { + let key = match (outdated_only, count == 1) { + (true, true) => "code_review.comments.button.outdated_singular", + (true, false) => "code_review.comments.button.outdated_plural", + (false, true) => "code_review.comments.button.singular", + (false, false) => "code_review.comments.button.plural", + }; + i18n::t(key).replace("{count}", &count.to_string()) +} + /// Convert markdown text to HTML using the editor's buffer serialization. /// This function takes a comment editor view that has already been created with markdown content /// and extracts the HTML representation from its buffer. @@ -203,7 +205,7 @@ impl CommentListView { let menu = ctx.add_view(|_| Menu::new()); let comments_button = ctx.add_view(|_| { - ActionButton::new("1 Comment", CustomSecondaryActionTheme) + ActionButton::new(comment_count_label(1, false), CustomSecondaryActionTheme) .with_size(ButtonSize::Small) .on_click(|ctx| { ctx.dispatch_typed_action(CommentListAction::ToggleCollapsed); @@ -245,24 +247,12 @@ impl CommentListView { .count(); if non_outdated_count == 0 && total_count > 0 { - format!( - "{} outdated comment{}", - total_count, - if total_count == 1 { "" } else { "s" } - ) + comment_count_label(total_count, true) } else { - format!( - "{} comment{}", - non_outdated_count, - if non_outdated_count == 1 { "" } else { "s" } - ) + comment_count_label(non_outdated_count, false) } } else { - format!( - "{} comment{}", - total_count, - if total_count == 1 { "" } else { "s" } - ) + comment_count_label(total_count, false) }; self.comments_button @@ -297,8 +287,7 @@ impl CommentListView { sendable_comments > 0, ai_available, ai_enabled, - ) - .into_owned(); + ); CommentListDebugState { review_destination: self.review_destination.clone(), @@ -804,7 +793,8 @@ impl CommentListView { .finish(); let outdated_text = Text::new( - format!("{outdated_count} outdated"), + i18n::t("code_review.comments.outdated_count") + .replace("{count}", &outdated_count.to_string()), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -879,7 +869,7 @@ impl CommentListView { ButtonVariant::Text, self.view_state.cancel_button_mouse_state.clone(), ) - .with_text_label("Cancel".to_string()) + .with_text_label(i18n::t("common.cancel")) .build() .finish(), ) @@ -903,25 +893,29 @@ impl CommentListView { has_sendable_comments: bool, ai_available: bool, ai_enabled: bool, - ) -> Cow<'static, str> { + ) -> String { if let ReviewDestination::Cli(agent) = destination { if !has_sendable_comments { - Cow::Borrowed("No non-outdated comments to send") + i18n::t("code_review.comments.no_sendable_comments") } else { let cmd = agent.command_prefix(); - let label = if cmd.is_empty() { "CLI agent" } else { cmd }; - Cow::Owned(format!("Send diff comments to {label}")) + let label = if cmd.is_empty() { + i18n::t("code_review.comments.cli_agent") + } else { + cmd.to_string() + }; + i18n::t("code_review.comments.send_to_destination").replace("{label}", &label) } } else if !ai_enabled { - Cow::Borrowed("AI must be enabled to send comments to Agent") + i18n::t("code_review.comments.ai_must_be_enabled") } else if !ai_available { - Cow::Borrowed("Agent code review requires AI credits") + i18n::t("code_review.comments.requires_ai_credits") } else if matches!(destination, ReviewDestination::None) { - Cow::Borrowed("All terminals are busy") + i18n::t("code_review.comments.all_terminals_busy") } else if !has_sendable_comments { - Cow::Borrowed("No non-outdated comments to send") + i18n::t("code_review.comments.no_sendable_comments") } else { - Cow::Borrowed("Send diff comments to Agent") + i18n::t("code_review.comments.send_to_agent.tooltip") } } @@ -946,7 +940,7 @@ impl CommentListView { let tooltip = appearance .ui_builder() - .tool_tip(tooltip_text.into_owned()) + .tool_tip(tooltip_text) .build() .finish(); @@ -956,7 +950,7 @@ impl CommentListView { ButtonVariant::Accent, self.view_state.submit_button_mouse_state.clone(), ) - .with_text_label("Send to Agent".to_string()) + .with_text_label(i18n::t("code_review.comments.send_to_agent.button")) .with_tooltip(|| tooltip) .with_tooltip_position(ButtonTooltipPosition::AboveLeft); @@ -1065,19 +1059,21 @@ impl CommentListView { html_url: Option<&str>, appearance: &Appearance, ) -> Vec> { - let mut items = vec![MenuItemFields::new("Copy text") - .with_icon(Icon::Copy) - .with_on_select_action(CommentListAction::CopyCommentText) - .into_item()]; + let mut items = vec![ + MenuItemFields::new(i18n::t("code_review.comments.copy_text")) + .with_icon(Icon::Copy) + .with_on_select_action(CommentListAction::CopyCommentText) + .into_item(), + ]; - let mut edit_item = MenuItemFields::new("Edit") + let mut edit_item = MenuItemFields::new(i18n::t("code_review.comments.edit")) .with_icon(Icon::Pencil) .with_on_select_action(CommentListAction::EditComment); if is_file_level || is_outdated { let tooltip_text = if is_file_level { - "File-level comments currently can't be edited." + i18n::t("code_review.comments.file_level_cannot_edit") } else { - "Outdated comments can't be edited." + i18n::t("code_review.comments.outdated_cannot_edit") }; edit_item = edit_item.with_disabled(true).with_tooltip(tooltip_text); } @@ -1085,7 +1081,7 @@ impl CommentListView { if let Some(url) = html_url { items.push( - MenuItemFields::new("View in GitHub") + MenuItemFields::new(i18n::t("code_review.comments.view_in_github")) .with_icon(Icon::Github) .with_on_select_action(CommentListAction::ViewInGitHub { url: url.to_string(), @@ -1095,7 +1091,7 @@ impl CommentListView { } items.push( - MenuItemFields::new("Remove") + MenuItemFields::new(i18n::t("code_review.comments.remove")) .with_icon(Icon::Trash) .with_override_text_color(Fill::Solid(appearance.theme().ansi_fg_red())) .with_override_icon_color(Fill::Solid(appearance.theme().ansi_fg_red())) diff --git a/app/src/code_review/comment_rendering.rs b/app/src/code_review/comment_rendering.rs index 2fb99045e7..727e9dcfdd 100644 --- a/app/src/code_review/comment_rendering.rs +++ b/app/src/code_review/comment_rendering.rs @@ -109,7 +109,7 @@ fn render_comment_file_path_header( let outdated_chip = Container::new( Text::new( - "Outdated", + i18n::t("code_review.comments.outdated_chip"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -172,7 +172,7 @@ fn render_comment_text_section( if is_imported_from_github { left_section.add_child( Text::new( - "From GitHub".to_string(), + i18n::t("code_review.comments.from_github"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -475,7 +475,7 @@ impl CommentViewCard { _ => source .head() .map(|head| head.title()) - .unwrap_or_else(|| "Review Comment".to_string()), + .unwrap_or_else(|| i18n::t("code_review.comments.review_comment")), } } } diff --git a/app/src/code_review/context.rs b/app/src/code_review/context.rs index 46aa31bcab..fa4e5064e5 100644 --- a/app/src/code_review/context.rs +++ b/app/src/code_review/context.rs @@ -76,13 +76,13 @@ pub fn create_attachment_reference_and_key( match scope { DiffSetScope::All => { let diff_set_description = match diff_mode { - DiffMode::Head => "uncommitted changes".to_string(), + DiffMode::Head => i18n::t("code_review.context.uncommitted_changes"), DiffMode::MainBranch => { let main_branch = main_branch_name.unwrap_or("main"); - format!("diffset against {main_branch}") + i18n::t("code_review.context.diffset_against").replace("{branch}", main_branch) } DiffMode::OtherBranch(branch_name) => { - format!("diffset against {branch_name}") + i18n::t("code_review.context.diffset_against").replace("{branch}", branch_name) } }; let key = diff_set_description.clone(); diff --git a/app/src/code_review/diff_menu.rs b/app/src/code_review/diff_menu.rs index b93843c219..076ea720c9 100644 --- a/app/src/code_review/diff_menu.rs +++ b/app/src/code_review/diff_menu.rs @@ -112,7 +112,7 @@ impl CodeReviewDiffMenu { ..Default::default() }; let mut editor = EditorView::new(options, ctx); - editor.set_placeholder_text("Search diff sets or branches to compare…", ctx); + editor.set_placeholder_text(i18n::t("code_review.diff_menu.search_placeholder"), ctx); editor }); @@ -279,7 +279,7 @@ impl CodeReviewDiffMenu { let theme = appearance.theme(); Container::new( Text::new( - "No matches", + i18n::t("common.no_matches"), appearance.ui_font_family(), appearance.ui_font_size(), ) diff --git a/app/src/code_review/diff_selector.rs b/app/src/code_review/diff_selector.rs index c19f5dd8fb..55cb3910d8 100644 --- a/app/src/code_review/diff_selector.rs +++ b/app/src/code_review/diff_selector.rs @@ -162,7 +162,7 @@ impl View for DiffSelector { let font_size = appearance.ui_font_size(); let label = if self.trigger_label.is_empty() { - "Uncommitted changes".to_string() + i18n::t("code_review.diff_target.uncommitted_changes") } else { self.trigger_label.clone() }; diff --git a/app/src/code_review/diff_state/remote.rs b/app/src/code_review/diff_state/remote.rs index 81b63cb6de..0a5e692076 100644 --- a/app/src/code_review/diff_state/remote.rs +++ b/app/src/code_review/diff_state/remote.rs @@ -343,8 +343,7 @@ impl RemoteDiffStateModel { } DiffState::Loaded => { let Some(base_content) = diffs else { - let error = - "Server reported loaded state but no diff data was available".to_string(); + let error = i18n::t("code_review.remote_diff.empty_data"); let load_duration = self .tracked_diff_load_start_time .take() diff --git a/app/src/code_review/git_dialog/commit.rs b/app/src/code_review/git_dialog/commit.rs index dd27d26394..ac1f28ed12 100644 --- a/app/src/code_review/git_dialog/commit.rs +++ b/app/src/code_review/git_dialog/commit.rs @@ -64,18 +64,6 @@ pub enum CommitSubAction { const EDITOR_FONT_SIZE: f32 = 12.; const EDITOR_MIN_HEIGHT: f32 = 72.; -/// Placeholder shown while the open-time AI commit-message autogen is in -/// flight. -const GENERATING_PLACEHOLDER_TEXT: &str = "Generating commit message\u{2026}"; -/// Placeholder shown once the open-time autogen resolves — either as a -/// nudge if the user later clears the generated draft, or as guidance when -/// autogen failed and the editor is blank. Also used when autogen is off. -const FALLBACK_PLACEHOLDER_TEXT: &str = "Type a commit message"; -/// Loading-state label while the commit / chain runs. Static regardless of -/// which chain is in flight — the success toast communicates what actually -/// ran. -const LOADING_LABEL: &str = "Committing\u{2026}"; - pub struct CommitState { pub(super) intent: CommitIntent, include_unstaged: bool, @@ -107,20 +95,23 @@ pub(super) fn new_state( // whether or not the branch already has an upstream — but the label // and icon flip to communicate the user-visible difference. let (push_label, push_icon) = if has_upstream { - ("Commit and push", Icon::ArrowUp) + (i18n::t("code_review.git.commit_and_push"), Icon::ArrowUp) } else { - ("Commit and publish", Icon::UploadCloud) + ( + i18n::t("code_review.git.commit_and_publish"), + Icon::UploadCloud, + ) }; // If AI autogen is on, the dialog opens with "Generating\u{2026}" and a // background request fills the editor when it resolves. Otherwise, we // land on the manual-type prompt immediately. let ai_autogen_enabled = should_send_git_ops_ai_request(ctx); let initial_placeholder = if ai_autogen_enabled { - GENERATING_PLACEHOLDER_TEXT + i18n::t("code_review.git.commit_message.generating_placeholder") } else { - FALLBACK_PLACEHOLDER_TEXT + i18n::t("code_review.git.commit_message.placeholder") }; - let message_editor = ctx.add_typed_action_view(|ctx| { + let message_editor = ctx.add_typed_action_view(move |ctx| { let appearance = Appearance::as_ref(ctx); let options = EditorOptions { text: TextOptions { @@ -137,7 +128,7 @@ pub(super) fn new_state( }; let mut editor = EditorView::new(options, ctx); - editor.set_placeholder_text(initial_placeholder, ctx); + editor.set_placeholder_text(initial_placeholder.clone(), ctx); editor }); @@ -146,7 +137,7 @@ pub(super) fn new_state( }); let commit_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Commit", SecondaryTheme) + ActionButton::new(i18n::t("code_review.git.commit"), SecondaryTheme) .with_size(ButtonSize::XSmall) .with_height(32.) .with_icon(Icon::GitCommit) @@ -157,7 +148,7 @@ pub(super) fn new_state( }) }); let commit_and_push_button = ctx.add_typed_action_view(move |_ctx| { - ActionButton::new(push_label, SecondaryTheme) + ActionButton::new(push_label.clone(), SecondaryTheme) .with_size(ButtonSize::XSmall) .with_height(32.) .with_icon(push_icon) @@ -170,15 +161,18 @@ pub(super) fn new_state( let commit_and_create_pr_button = if allow_create_pr { Some(ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Commit and create PR", SecondaryTheme) - .with_size(ButtonSize::XSmall) - .with_height(32.) - .with_icon(Icon::Github) - .on_click(|ctx| { - ctx.dispatch_typed_action(GitDialogAction::Commit(CommitSubAction::SetIntent( - CommitIntent::CommitAndCreatePr, - ))) - }) + ActionButton::new( + i18n::t("code_review.git.commit_and_create_pr"), + SecondaryTheme, + ) + .with_size(ButtonSize::XSmall) + .with_height(32.) + .with_icon(Icon::Github) + .on_click(|ctx| { + ctx.dispatch_typed_action(GitDialogAction::Commit(CommitSubAction::SetIntent( + CommitIntent::CommitAndCreatePr, + ))) + }) })) } else { None @@ -242,9 +236,9 @@ pub(super) fn is_ready_to_confirm(state: &CommitState, app: &AppContext) -> bool /// Returns a tooltip to show on the disabled Confirm button when the /// user needs to take action, or `None` when no tooltip is needed. -pub(super) fn confirm_tooltip(state: &CommitState, app: &AppContext) -> Option<&'static str> { +pub(super) fn confirm_tooltip(state: &CommitState, app: &AppContext) -> Option { if !state.file_changes.is_empty() && commit_message(state, app).is_none() { - Some("Enter a commit message") + Some(i18n::t("code_review.git.commit_message.enter_tooltip")) } else { None } @@ -294,7 +288,10 @@ fn generate_commit_message( // Swap "Generating\u{2026}" for the manual-type // prompt so it shows if the user later clears the // generated draft. - editor.set_placeholder_text(FALLBACK_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text( + i18n::t("code_review.git.commit_message.placeholder"), + ctx, + ); // User input wins — don't clobber their text. if !user_typed { editor.system_reset_buffer_text(generated.trim(), ctx); @@ -306,7 +303,10 @@ fn generate_commit_message( Err(err) => { log::warn!("Failed to autogenerate commit message: {err}"); editor_handle.update(ctx, |editor, ctx| { - editor.set_placeholder_text(FALLBACK_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text( + i18n::t("code_review.git.commit_message.placeholder"), + ctx, + ); }); me.refresh_confirm_enabled(ctx); ctx.notify(); @@ -368,7 +368,7 @@ pub(super) fn start_confirm(me: &mut GitDialog, ctx: &mut ViewContext let repo_path = me.repo_path().clone(); let branch_name = me.branch_name().to_string(); - me.set_loading(LOADING_LABEL, ctx); + me.set_loading(i18n::t("code_review.git.commit.loading"), ctx); // Lock the commit message editor while the async op is in flight. message_editor.update(ctx, |editor, ctx| { @@ -432,10 +432,10 @@ pub(super) fn start_confirm(me: &mut GitDialog, ctx: &mut ViewContext }; match result { Ok(CommitOutcome::Committed) => { - show_toast("Changes successfully committed.", ctx); + show_toast(i18n::t("code_review.git.commit.success"), ctx); } Ok(CommitOutcome::Pushed) => { - show_toast("Changes committed and pushed.", ctx); + show_toast(i18n::t("code_review.git.commit_and_push.success"), ctx); } Ok(CommitOutcome::PrCreated(pr)) => { show_pr_created_toast(&pr, ctx); @@ -561,7 +561,7 @@ fn render_changes_section(state: &CommitState, appearance: &Appearance) -> Box Box Box { let label = Text::new( - "Commit message", + i18n::t("code_review.git.commit_message.label"), appearance.ui_font_family(), appearance.ui_font_size(), ) diff --git a/app/src/code_review/git_dialog/mod.rs b/app/src/code_review/git_dialog/mod.rs index 713f8910a3..003f81e0b9 100644 --- a/app/src/code_review/git_dialog/mod.rs +++ b/app/src/code_review/git_dialog/mod.rs @@ -144,48 +144,48 @@ fn should_send_git_ops_ai_request(app: &AppContext) -> bool { /// Maps a raw git error string to a user-friendly toast message. Known /// failure modes get dedicated copy; anything else falls back to a generic /// message (the raw error is always logged separately at the call site). -fn user_facing_git_error(raw: &str) -> &'static str { +fn user_facing_git_error(raw: &str) -> String { let lower = raw.to_lowercase(); if lower.contains("nothing to commit") { - "No changes to commit." + i18n::t("code_review.git.error.no_changes_to_commit") } else if lower.contains("please tell me who you are") || lower.contains("author identity unknown") { - "Git identity not configured. Set user.name and user.email." + i18n::t("code_review.git.error.identity_not_configured") } else if lower.contains("updates were rejected") || lower.contains("non-fast-forward") || lower.contains("fetch first") { - "Remote has new changes \u{2014} pull before pushing." + i18n::t("code_review.git.error.remote_has_new_changes") } else if lower.contains("does not appear to be a git repository") || lower.contains("no configured push destination") || lower.contains("no such remote") { - "No remote configured for this branch." + i18n::t("code_review.git.error.no_remote") } else if lower.contains("authentication failed") || lower.contains("permission denied (publickey)") { - "Authentication failed. Check your Git credentials." + i18n::t("code_review.git.error.auth_failed") } else if lower.contains("could not resolve host") || lower.contains("network is unreachable") || lower.contains("connection timed out") { - "Network error. Check your connection." + i18n::t("code_review.git.error.network") } else if lower.contains("repository not found") { - "Remote repository not found." + i18n::t("code_review.git.error.remote_repo_not_found") } else if lower.contains("failed to execute gh command") { // `run_gh_command` wraps spawn failures with this prefix, which is // the reliable "gh binary missing" signal. - "GitHub CLI (gh) not installed. See https://cli.github.com/." + i18n::t("code_review.git.error.gh_not_installed") } else if lower.contains("not logged in") || lower.contains("authentication required") || lower.contains("gh auth login") { // Phrases mirror `context_chips::current_prompt::is_gh_auth_error`, // which has been vetted against real `gh` failure output. - "GitHub CLI not authenticated. Run `gh auth login`." + i18n::t("code_review.git.error.gh_not_authenticated") } else { - "Git operation failed." + i18n::t("code_review.git.error.generic") } } @@ -206,7 +206,7 @@ fn render_branch_section( let sub_color = theme.sub_text_color(theme.surface_1()).into_solid(); let label = Text::new( - "Branch", + i18n::t("code_review.git.branch"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -291,10 +291,12 @@ fn render_file_changes_box( let total_deletions: usize = file_changes.iter().map(|f| f.deletions).sum(); let files_text = Text::new( - format!( - "{total_files} {}", - if total_files == 1 { "file" } else { "files" } - ), + i18n::t(if total_files == 1 { + "code_review.git.files.singular" + } else { + "code_review.git.files.plural" + }) + .replace("{count}", &total_files.to_string()), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -499,7 +501,7 @@ impl GitDialog { // communicates which of commit / commit-and-push / commit-and-create-PR // will actually run on click. let (confirm_button, cancel_button, close_button) = - Self::build_dialog_buttons("Confirm", None, ctx); + Self::build_dialog_buttons(i18n::t("code_review.git.confirm"), None, ctx); let state = commit::new_state(&repo_path, allow_create_pr, has_upstream, ctx); let this = Self { repo_path, @@ -559,7 +561,7 @@ impl GitDialog { } fn build_dialog_buttons( - confirm_label: &'static str, + confirm_label: String, confirm_icon: Option, ctx: &mut ViewContext, ) -> ( @@ -568,7 +570,7 @@ impl GitDialog { ViewHandle, ) { let confirm_button = ctx.add_typed_action_view(move |_ctx| { - let mut button = ActionButton::new(confirm_label, SecondaryTheme) + let mut button = ActionButton::new(confirm_label.clone(), SecondaryTheme) .with_size(ButtonSize::Small) .with_height(32.); if let Some(icon) = confirm_icon { @@ -577,7 +579,7 @@ impl GitDialog { button.on_click(|ctx| ctx.dispatch_typed_action(GitDialogAction::Confirm)) }); let cancel_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Cancel", NakedTheme) + ActionButton::new(i18n::t("common.cancel"), NakedTheme) .with_size(ButtonSize::Small) .with_height(32.) .on_click(|ctx| ctx.dispatch_typed_action(GitDialogAction::Cancel)) @@ -614,7 +616,7 @@ impl GitDialog { /// Disables cancel/confirm/close and swaps the confirm label while the /// async op is running. - fn set_loading(&mut self, loading_label: &'static str, ctx: &mut ViewContext) { + fn set_loading(&mut self, loading_label: String, ctx: &mut ViewContext) { self.loading = true; self.confirm_button.update(ctx, |b, ctx| { b.set_label(loading_label, ctx); @@ -650,17 +652,17 @@ impl GitDialog { }); } - fn title(&self) -> &'static str { + fn title(&self) -> String { match &self.mode { - GitDialogMode::Commit(_) => "Commit your changes", + GitDialogMode::Commit(_) => i18n::t("code_review.git.dialog.title.commit"), GitDialogMode::Push(state) => { if state.publish { - "Publish branch" + i18n::t("code_review.git.dialog.title.publish") } else { - "Push changes" + i18n::t("code_review.git.dialog.title.push") } } - GitDialogMode::CreatePr(_) => "Create pull request", + GitDialogMode::CreatePr(_) => i18n::t("code_review.git.dialog.title.create_pr"), } } diff --git a/app/src/code_review/git_dialog/pr.rs b/app/src/code_review/git_dialog/pr.rs index 178c03f8bf..86d9695cf4 100644 --- a/app/src/code_review/git_dialog/pr.rs +++ b/app/src/code_review/git_dialog/pr.rs @@ -46,16 +46,16 @@ pub struct PrState { changes_scroll_state: ClippedScrollStateHandle, } -pub(super) fn confirm_label_for() -> &'static str { - "Create PR" +pub(super) fn confirm_label_for() -> String { + i18n::t("code_review.git.create_pr") } pub(super) fn confirm_icon_for() -> Icon { Icon::Github } -fn loading_label_for() -> &'static str { - "Creating\u{2026}" +fn loading_label_for() -> String { + i18n::t("code_review.git.create_pr.loading") } /// PR mode has no prerequisites beyond a branch with commits; confirm is @@ -234,9 +234,9 @@ pub(super) fn show_pr_created_toast(pr_info: &PrInfo, ctx: &mut ViewContext Box) -> PushState { } } -pub(super) fn confirm_label(publish: bool) -> &'static str { +pub(super) fn confirm_label(publish: bool) -> String { if publish { - "Publish" + i18n::t("code_review.git.publish") } else { - "Push" + i18n::t("code_review.git.push") } } @@ -74,11 +74,11 @@ pub(super) fn confirm_icon(publish: bool) -> Icon { } } -fn loading_label(publish: bool) -> &'static str { +fn loading_label(publish: bool) -> String { if publish { - "Publishing…" + i18n::t("code_review.git.publish.loading") } else { - "Pushing…" + i18n::t("code_review.git.push.loading") } } @@ -155,9 +155,9 @@ pub(super) fn start_confirm(me: &mut GitDialog, ctx: &mut ViewContext match result { Ok(_) => { let toast_msg = if publish { - "Branch successfully published." + i18n::t("code_review.git.publish.success") } else { - "Changes successfully pushed." + i18n::t("code_review.git.push.success") }; show_toast(toast_msg, ctx); } @@ -209,7 +209,7 @@ fn render_commits_section(state: &PushState, appearance: &Appearance) -> Box Box Box) -> Self { let editor = ctx.add_typed_action_view(|ctx| { - GlowingEditor::new( - "Provide a repository URL e.g. \"git@github.com:username/project.git\"", - ctx, - ) + GlowingEditor::new(i18n::t("coding_entrypoints.clone_repo.placeholder"), ctx) }); ctx.subscribe_to_view(&editor, move |me, _, event, ctx| { diff --git a/app/src/coding_entrypoints/create_project_view.rs b/app/src/coding_entrypoints/create_project_view.rs index e87ffd4c8a..db1e580817 100644 --- a/app/src/coding_entrypoints/create_project_view.rs +++ b/app/src/coding_entrypoints/create_project_view.rs @@ -28,14 +28,18 @@ pub struct CreateProjectView { } struct BuildSuggestion { - prompt: &'static str, + prompt_key: &'static str, mouse_state: MouseStateHandle, } impl CreateProjectView { pub fn new(is_ftux: bool, ctx: &mut ViewContext) -> Self { - let editor = - ctx.add_typed_action_view(|ctx| GlowingEditor::new("What do you want to build?", ctx)); + let editor = ctx.add_typed_action_view(|ctx| { + GlowingEditor::new( + i18n::t("coding_entrypoints.create_project.placeholder"), + ctx, + ) + }); ctx.subscribe_to_view(&editor, move |me, _, event, ctx| { me.handle_editor_event(event, ctx); @@ -43,23 +47,23 @@ impl CreateProjectView { let suggestions = vec![ BuildSuggestion { - prompt: "Build a Minesweeper clone in React", + prompt_key: "coding_entrypoints.create_project.suggestion.minesweeper", mouse_state: Default::default(), }, BuildSuggestion { - prompt: "Code a Node.js server that returns random quotes from a JSON file", + prompt_key: "coding_entrypoints.create_project.suggestion.random_quotes_server", mouse_state: Default::default(), }, BuildSuggestion { - prompt: "Write a CSV to JSON converter CLI", + prompt_key: "coding_entrypoints.create_project.suggestion.csv_to_json_cli", mouse_state: Default::default(), }, BuildSuggestion { - prompt: "Create a starter template for a résumé web page", + prompt_key: "coding_entrypoints.create_project.suggestion.resume_template", mouse_state: Default::default(), }, BuildSuggestion { - prompt: "Make a Conway's Game of Life simulation", + prompt_key: "coding_entrypoints.create_project.suggestion.game_of_life", mouse_state: Default::default(), }, ]; @@ -122,7 +126,7 @@ impl CreateProjectView { let font_color = theme.sub_text_color(theme.background()).into_solid(); let mouse_state = suggestion.mouse_state.clone(); - let prompt = suggestion.prompt; + let prompt = i18n::t(suggestion.prompt_key); let row = Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) @@ -140,7 +144,7 @@ impl CreateProjectView { .finish(), Expanded::new( 1., - Text::new(prompt, font_family, font_size) + Text::new(prompt.clone(), font_family, font_size) .with_color(font_color) .with_style(Properties::default().weight(Weight::Medium)) .soft_wrap(false) @@ -163,7 +167,7 @@ impl CreateProjectView { .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(CreateProjectAction::SuggestionSelected { - prompt: prompt.to_string(), + prompt: prompt.clone(), }); }) .finish() diff --git a/app/src/coding_entrypoints/project_buttons.rs b/app/src/coding_entrypoints/project_buttons.rs index 7f48dd16cd..0d4c1f0cf7 100644 --- a/app/src/coding_entrypoints/project_buttons.rs +++ b/app/src/coding_entrypoints/project_buttons.rs @@ -224,11 +224,13 @@ impl View for ProjectButtons { if FeatureFlag::CreateProjectFlow.is_enabled() { row.add_children([ Container::new(self.glowing_button( - "Create new project", + i18n::t("coding_entrypoints.project_buttons.create_new_project.label"), Icon::Plus, ProjectButtonsAction::CreateProject, TooltipData { - text: "Create and initialize a brand new project".to_string(), + text: i18n::t( + "coding_entrypoints.project_buttons.create_new_project.tooltip", + ), keybinding: keybinding_name_to_display_string( "project_buttons:create_new_project", app, @@ -240,11 +242,11 @@ impl View for ProjectButtons { .with_margin_right(16.) .finish(), Container::new(self.glowing_button( - "Open repository", + i18n::t("coding_entrypoints.project_buttons.open_repository.label"), Icon::Folder, ProjectButtonsAction::OpenRepository, TooltipData { - text: "Open an existing local folder or repository".to_string(), + text: i18n::t("coding_entrypoints.project_buttons.open_repository.tooltip"), keybinding: keybinding_name_to_display_string( "project_buttons:open_repository", app, @@ -256,11 +258,13 @@ impl View for ProjectButtons { .with_margin_right(16.) .finish(), self.glowing_button( - "Clone repository", + i18n::t("coding_entrypoints.project_buttons.clone_repository.label"), Icon::Duplicate, ProjectButtonsAction::CloneRepository, TooltipData { - text: "Clone a repo from GitHub or another source".to_string(), + text: i18n::t( + "coding_entrypoints.project_buttons.clone_repository.tooltip", + ), keybinding: None, }, self.state_handles.clone_repo_button.clone(), @@ -272,11 +276,13 @@ impl View for ProjectButtons { Expanded::new( 1., self.glowing_button( - "Open repository", + i18n::t("coding_entrypoints.project_buttons.open_repository.label"), Icon::Plus, ProjectButtonsAction::CreateProject, TooltipData { - text: "Open an existing local folder or repository".to_string(), + text: i18n::t( + "coding_entrypoints.project_buttons.open_repository.tooltip", + ), keybinding: keybinding_name_to_display_string( "project_buttons:open_repository", app, diff --git a/app/src/context_chips/context_chip.rs b/app/src/context_chips/context_chip.rs index 7fa81e2f4b..384cc6e6c7 100644 --- a/app/src/context_chips/context_chip.rs +++ b/app/src/context_chips/context_chip.rs @@ -168,11 +168,13 @@ pub enum ChipDisabledReason { impl ChipDisabledReason { pub fn tooltip_text(&self) -> String { match self { - Self::RequiresLocalSession => "Requires a local session".to_string(), + Self::RequiresLocalSession => i18n::t("context_chips.requires_local_session"), Self::RequiresExecutable { command } if command == "gh" => { - "Requires the GitHub CLI".to_string() + i18n::t("context_chips.requires_github_cli") + } + Self::RequiresExecutable { command } => { + i18n::t("context_chips.requires_command").replace("{command}", command) } - Self::RequiresExecutable { command } => format!("Requires the `{command}` command"), } } } diff --git a/app/src/context_chips/current_prompt.rs b/app/src/context_chips/current_prompt.rs index 49c0d4296f..350303d3a4 100644 --- a/app/src/context_chips/current_prompt.rs +++ b/app/src/context_chips/current_prompt.rs @@ -1300,14 +1300,16 @@ impl CurrentPrompt { if has_value && chip_kind.is_copyable() { if let Some(chip) = chip_kind.to_chip() { Some( - MenuItemFields::new(format!("Copy {}", chip.title())) - .with_on_select_action(TerminalAction::ContextMenu( - ContextMenuAction::CopyPrompt { - position, - part: PromptPart::ContextChip(chip_kind), - }, - )) - .into_item(), + MenuItemFields::new( + i18n::t("context_chips.copy_chip").replace("{title}", chip.title()), + ) + .with_on_select_action(TerminalAction::ContextMenu( + ContextMenuAction::CopyPrompt { + position, + part: PromptPart::ContextChip(chip_kind), + }, + )) + .into_item(), ) } else { log::error!("Missing definition for chip: {chip_kind:?}"); diff --git a/app/src/context_chips/display_chip.rs b/app/src/context_chips/display_chip.rs index 76de4d44d8..203e79383f 100644 --- a/app/src/context_chips/display_chip.rs +++ b/app/src/context_chips/display_chip.rs @@ -37,7 +37,7 @@ use crate::ai::blocklist::{BlocklistAIContextModel, BlocklistAIInputModel}; use crate::ai::document::ai_document_model::{AIDocumentId, AIDocumentVersion}; use crate::appearance::Appearance; use crate::code::editor::{add_color, remove_color}; -use crate::code_review::code_review_view::CODE_REVIEW_TOOLTIP_TEXT; +use crate::code_review::code_review_view::code_review_tooltip_text; use crate::code_review::diff_state::DiffStats; use crate::completer::SessionContext; use crate::context_chips::git_branch_on_click::{ @@ -505,7 +505,7 @@ impl GenericMenuItem for CreateGitBranch { } fn name(&self) -> String { - format!("Create new branch \"{}\"", self.0) + i18n::t("context_chips.create_new_branch").replace("{branch}", &self.0) } fn icon(&self, _app: &AppContext) -> Option { @@ -662,7 +662,7 @@ impl DisplayChip { DisplayChipMenu::new( Vec::::new(), Some(FixedFooter::new(Arc::new(DirectoryItem { - name: ".. (Parent Directory)".to_string(), + name: i18n::t("context_chips.parent_directory"), directory_type: DirectoryType::NavigateToParent, }))), // Show parent directory option ChipMenuType::Directories, @@ -810,9 +810,9 @@ impl DisplayChip { }; let quota_reset_popup = ctx.add_typed_action_view(|_| { - FeaturePopup::alert_icon(NewFeaturePopupLabel::FromString( - "Monthly AI credits reset!".to_string(), - )) + FeaturePopup::alert_icon(NewFeaturePopupLabel::FromString(i18n::t( + "context_chips.monthly_ai_credits_reset", + ))) }); ctx.subscribe_to_view("a_reset_popup, |_, _, event, ctx| match event { @@ -1092,7 +1092,7 @@ impl DisplayChip { if state.is_hovered() && is_interactive && !menu_open { let tool_tip = appearance .ui_builder() - .tool_tip("Change git branch".to_string()) + .tool_tip(i18n::t("context_chips.change_git_branch")) .build() .finish(); stack.add_positioned_overlay_child(tool_tip, udi_tooltip_positioning()); @@ -1159,7 +1159,7 @@ impl DisplayChip { if state.is_hovered() { let tool_tip = appearance .ui_builder() - .tool_tip("View pull request".to_string()) + .tool_tip(i18n::t("context_chips.view_pull_request")) .build() .finish(); stack.add_positioned_overlay_child(tool_tip, udi_tooltip_positioning()); @@ -1251,7 +1251,7 @@ impl DisplayChip { let tool_tip = appearance .ui_builder() .tool_tip_with_sublabel( - CODE_REVIEW_TOOLTIP_TEXT.to_string(), + code_review_tooltip_text(), code_review_keybinding.clone(), ) .build() @@ -1339,7 +1339,7 @@ impl DisplayChip { if state.is_hovered() { let tool_tip = appearance .ui_builder() - .tool_tip("Change working directory".to_string()) + .tool_tip(i18n::t("context_chips.change_working_directory")) .build() .finish(); @@ -1386,7 +1386,7 @@ impl DisplayChip { if state.is_hovered() && !is_cli_agent_active { let tool_tip = appearance .ui_builder() - .tool_tip("Working directory".to_string()) + .tool_tip(i18n::t("context_chips.working_directory")) .build() .finish(); diff --git a/app/src/context_chips/display_menu.rs b/app/src/context_chips/display_menu.rs index 167e8ceb7a..6a78d04b9a 100644 --- a/app/src/context_chips/display_menu.rs +++ b/app/src/context_chips/display_menu.rs @@ -315,9 +315,13 @@ impl DisplayChipMenu { }; let mut editor = EditorView::new(options, ctx); let placeholder_text = match chip_menu_type { - ChipMenuType::Directories => "Search directories...", - ChipMenuType::Branches => "Search branches...", - ChipMenuType::Environments => "Search environments...", + ChipMenuType::Directories => { + i18n::t("context_chips.menu.search_directories") + } + ChipMenuType::Branches => i18n::t("context_chips.menu.search_branches"), + ChipMenuType::Environments => { + i18n::t("context_chips.menu.search_environments") + } ChipMenuType::CodeReview => { unreachable!("search input should not be constructed") } @@ -650,7 +654,7 @@ impl DisplayChipMenu { .map(|repo| repo.repo.clone()) .collect::>(); let repos_text = if repo_names.is_empty() { - "(none)".to_string() + i18n::t("context_chips.environment.none") } else { repo_names.join(", ") }; @@ -875,15 +879,25 @@ impl DisplayChipMenu { .with_cross_axis_alignment(CrossAxisAlignment::Start) .with_child(row( Icon::Globe4, - "Name:", + &i18n::t("context_chips.environment.name"), value_text(data.name.clone()), false, )) - .with_child(row(Icon::Hash, "ID:", id_value, false)) - .with_child(row(Icon::Docker, "Image:", image_value, false)) + .with_child(row( + Icon::Hash, + &i18n::t("context_chips.environment.id"), + id_value, + false, + )) + .with_child(row( + Icon::Docker, + &i18n::t("context_chips.environment.image"), + image_value, + false, + )) .with_child(row( Icon::Github, - "Repos:", + &i18n::t("context_chips.environment.repos"), value_text(data.repos_text.clone()), true, )) @@ -1069,7 +1083,7 @@ impl DisplayChipMenu { let (label, font_size, horizontal_padding, vertical_padding, text_color) = match self.chip_menu_type { ChipMenuType::Environments => ( - "No results", + i18n::t("context_chips.menu.no_results"), ENV_MENU_ITEM_FONT_SIZE, ENV_MENU_ITEM_HORIZONTAL_PADDING, ENV_MENU_ITEM_VERTICAL_PADDING, @@ -1078,7 +1092,7 @@ impl DisplayChipMenu { ChipMenuType::Directories | ChipMenuType::Branches | ChipMenuType::CodeReview => ( - "No results found", + i18n::t("context_chips.menu.no_results_found"), appearance.ui_font_size(), LABEL_HORIZONTAL_PADDING, LABEL_VERTICAL_PADDING * 2.0, diff --git a/app/src/context_chips/mod.rs b/app/src/context_chips/mod.rs index bc62110974..7e6cd0d0e1 100644 --- a/app/src/context_chips/mod.rs +++ b/app/src/context_chips/mod.rs @@ -195,7 +195,7 @@ impl ContextChipKind { pub fn to_chip(&self) -> Option { match self { Self::WorkingDirectory => Some(ContextChip::builtin_with_runtime_policy( - "Working Directory", + i18n::t("context_chips.title.working_directory"), builtins::working_directory, RefreshConfig::OnDemandOnly, ChipRuntimePolicy::new( @@ -209,7 +209,7 @@ impl ContextChipKind { ), )), Self::Username => Some(ContextChip::builtin_with_runtime_policy( - "User", + i18n::t("context_chips.title.user"), builtins::username, RefreshConfig::OnDemandOnly, ChipRuntimePolicy::new( @@ -220,7 +220,7 @@ impl ContextChipKind { ), )), Self::Hostname => Some(ContextChip::builtin_with_runtime_policy( - "Host", + i18n::t("context_chips.title.host"), builtins::hostname, RefreshConfig::OnDemandOnly, ChipRuntimePolicy::new( @@ -231,7 +231,7 @@ impl ContextChipKind { ), )), Self::VirtualEnvironment => Some(ContextChip::builtin_with_runtime_policy( - "Python Virtualenv", + i18n::t("context_chips.title.python_virtualenv"), builtins::virtual_environment, RefreshConfig::OnDemandOnly, ChipRuntimePolicy::new( @@ -245,7 +245,7 @@ impl ContextChipKind { ), )), Self::CondaEnvironment => Some(ContextChip::builtin_with_runtime_policy( - "Conda Environment", + i18n::t("context_chips.title.conda_environment"), builtins::conda_environment, RefreshConfig::OnDemandOnly, ChipRuntimePolicy::new( @@ -259,7 +259,7 @@ impl ContextChipKind { ), )), Self::NodeVersion => Some(ContextChip::builtin_with_runtime_policy( - "Node.js Version", + i18n::t("context_chips.title.node_js_version"), builtins::node_version, RefreshConfig::OnDemandOnly, ChipRuntimePolicy::new( @@ -273,17 +273,17 @@ impl ContextChipKind { ), )), Self::Date => Some(ContextChip::builtin( - "Date", + i18n::t("context_chips.title.date"), builtins::date, DATE_REFRESH_CONFIG, )), Self::Time12 => Some(ContextChip::builtin( - "Time (12-hour format)", + i18n::t("context_chips.title.time_12"), builtins::time12, TIME_REFRESH_CONFIG, )), Self::Time24 => Some(ContextChip::builtin( - "Time (24-hour format)", + i18n::t("context_chips.title.time_24"), builtins::time24, TIME_REFRESH_CONFIG, )), @@ -292,14 +292,14 @@ impl ContextChipKind { None } Self::ShellGitBranch => Some(ContextChip::shell_builtin( - "Git Branch", + i18n::t("context_chips.title.git_branch"), builtins::shell_git_branch(), Some(builtins::shell_other_git_branches()), GIT_REFRESH_CONFIG, )), Self::GitDiffStats => Some( ContextChip::shell_builtin( - "Git Diff Stats", + i18n::t("context_chips.title.git_diff_stats"), builtins::shell_git_line_changes(), None, GIT_REFRESH_CONFIG, @@ -308,40 +308,40 @@ impl ContextChipKind { ), Self::GithubPullRequest if !FeatureFlag::GithubPrPromptChip.is_enabled() => None, Self::GithubPullRequest => Some(ContextChip::builtin( - "GitHub Pull Request", + i18n::t("context_chips.title.github_pull_request"), |_| None, RefreshConfig::OnDemandOnly, )), Self::KubernetesContext => Some(ContextChip::shell_builtin( - "Kubernetes Context", + i18n::t("context_chips.title.kubernetes_context"), builtins::kubernetes_current_context(), None, RefreshConfig::OnDemandOnly, )), Self::SvnBranch => Some(ContextChip::shell_builtin( - "Svn Branch", + i18n::t("context_chips.title.svn_branch"), builtins::svn_branch_context(), None, RefreshConfig::OnDemandOnly, )), Self::SvnDirtyItems => Some(ContextChip::shell_builtin( - "Svn Uncommitted File Count", + i18n::t("context_chips.title.svn_uncommitted_file_count"), builtins::svn_dirty_items(), None, RefreshConfig::OnDemandOnly, )), Self::Ssh => Some(ContextChip::builtin( - "Remote Login", + i18n::t("context_chips.title.remote_login"), builtins::ssh_session, RefreshConfig::OnDemandOnly, )), Self::Subshell => Some(ContextChip::builtin( - "subshell", + i18n::t("context_chips.title.subshell"), builtins::subshell, RefreshConfig::OnDemandOnly, )), Self::AgentPlanAndTodoList => Some(ContextChip::builtin( - "Agent Plan and Todo List", + i18n::t("context_chips.title.agent_plan_and_todo_list"), |_| Some(ChipValue::Text(String::new())), RefreshConfig::OnDemandOnly, )), diff --git a/app/src/context_chips/node_version_popup.rs b/app/src/context_chips/node_version_popup.rs index c936db1059..d5dec2fd78 100644 --- a/app/src/context_chips/node_version_popup.rs +++ b/app/src/context_chips/node_version_popup.rs @@ -73,7 +73,7 @@ impl NodeVersionPopupView { ctx: &mut ViewContext, ) -> Self { let install_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Install nvm", SecondaryTheme) + ActionButton::new(i18n::t("node_version.install_nvm"), SecondaryTheme) .with_icon(icons::Icon::Terminal) .on_click(|ctx| { ctx.dispatch_typed_action(NodeVersionPopupAction::InstallNvm); @@ -177,7 +177,7 @@ impl NodeVersionPopupView { col.add_child( Text::new( - "Install nvm to enable version switching", + i18n::t("context_chips.node.install_nvm_title"), styles.ui_font_family, styles.detail_font_size + 2., ) @@ -189,7 +189,7 @@ impl NodeVersionPopupView { col.add_child( Container::new( Text::new( - "This menu helps you switch between Node.js versions — but it requires nvm to be installed.", + i18n::t("context_chips.node.install_nvm_description"), styles.ui_font_family, styles.detail_font_size, ) @@ -238,7 +238,7 @@ impl NodeVersionPopupView { // Heading col.add_child( Text::new( - "No node versions installed", + i18n::t("context_chips.node.no_versions_installed"), styles.ui_font_family, styles.detail_font_size + 2., ) @@ -251,7 +251,7 @@ impl NodeVersionPopupView { col.add_child( Container::new( Text::new( - "Try installing versions with nvm", + i18n::t("context_chips.node.try_installing_with_nvm"), styles.ui_font_family, styles.detail_font_size, ) @@ -282,10 +282,14 @@ impl NodeVersionPopupView { col.add_child( Container::new( - Text::new("Installed", styles.ui_font_family, styles.detail_font_size) - .with_style(Properties::default()) - .with_color(styles.secondary_text_color) - .finish(), + Text::new( + i18n::t("common.installed"), + styles.ui_font_family, + styles.detail_font_size, + ) + .with_style(Properties::default()) + .with_color(styles.secondary_text_color) + .finish(), ) .with_horizontal_padding(12.) .finish(), diff --git a/app/src/context_chips/prompt_type.rs b/app/src/context_chips/prompt_type.rs index 2884cc74ad..6266a9d260 100644 --- a/app/src/context_chips/prompt_type.rs +++ b/app/src/context_chips/prompt_type.rs @@ -59,14 +59,16 @@ impl PromptType { if chip_result.value.is_some() && chip_result.kind.is_copyable() { if let Some(chip) = chip_result.kind.to_chip() { Some( - MenuItemFields::new(format!("Copy {}", chip.title())) - .with_on_select_action(TerminalAction::ContextMenu( - ContextMenuAction::CopyPrompt { - position, - part: PromptPart::ContextChip(chip_result.kind), - }, - )) - .into_item(), + MenuItemFields::new( + i18n::t("context_chips.copy_chip").replace("{title}", chip.title()), + ) + .with_on_select_action(TerminalAction::ContextMenu( + ContextMenuAction::CopyPrompt { + position, + part: PromptPart::ContextChip(chip_result.kind), + }, + )) + .into_item(), ) } else { log::error!("Missing definition for chip: {:?}", chip_result.kind); diff --git a/app/src/drive/cloud_action_confirmation_dialog.rs b/app/src/drive/cloud_action_confirmation_dialog.rs index 34a4ee0a1f..34a3ff85a7 100644 --- a/app/src/drive/cloud_action_confirmation_dialog.rs +++ b/app/src/drive/cloud_action_confirmation_dialog.rs @@ -15,21 +15,6 @@ const BUTTON_BORDER_RADIUS: f32 = 4.; const BORDER_WIDTH: f32 = 1.; const DIALOG_WIDTH: f32 = 450.; -const CANCEL_TEXT: &str = "Cancel"; - -const DELETE_TEAM_TITLE_TEXT: &str = "Are you sure you want to delete this team?"; -const LEAVE_TEAM_TITLE_TEXT: &str = "Are you sure you want to leave this team?"; -const REMOVE_TEAM_MEMBER_TITLE_TEXT: &str = "Are you sure you want to remove this member?"; - -const DELETE_TEAM_BODY_TEXT: &str = "Deleting this team will permanently delete it and all of its related content, including billing information or credits. You will not be able to restore them."; -const LEAVE_TEAM_BODY_TEXT: &str = "You will need to be reinvited in order to rejoin."; -const LEAVE_TEAM_RELOAD_CREDITS_BODY_TEXT: &str = "If you leave this team, you’ll lose access to any remaining reload credits tied to it. You’ll regain access to any unused, non-expired credits if you rejoin the same team later."; -const REMOVE_TEAM_MEMBER_RELOAD_CREDITS_BODY_TEXT: &str = "This member will lose access to any remaining reload credits tied to this team. If they rejoin later, they’ll regain access to any unused, non-expired credits."; - -const DELETE_TEAM_CONFIRM_TEXT: &str = "Yes, delete"; -const LEAVE_TEAM_CONFIRM_TEXT: &str = "Yes, leave"; -const LEAVE_TEAM_RELOAD_CREDITS_CONFIRM_TEXT: &str = "Leave Team"; -const REMOVE_TEAM_MEMBER_RELOAD_CREDITS_CONFIRM_TEXT: &str = "Remove Member"; pub enum CloudActionConfirmationDialogEvent { Cancel, @@ -81,11 +66,13 @@ impl CloudActionConfirmationDialog { match self.variant { CloudActionConfirmationDialogVariant::LeaveTeam | CloudActionConfirmationDialogVariant::LeaveTeamReloadCredits => { - LEAVE_TEAM_TITLE_TEXT.to_string() + i18n::t("drive.confirmation.leave_team.title") + } + CloudActionConfirmationDialogVariant::DeleteTeam => { + i18n::t("drive.confirmation.delete_team.title") } - CloudActionConfirmationDialogVariant::DeleteTeam => DELETE_TEAM_TITLE_TEXT.to_string(), CloudActionConfirmationDialogVariant::RemoveTeamMemberReloadCredits => { - REMOVE_TEAM_MEMBER_TITLE_TEXT.to_string() + i18n::t("drive.confirmation.remove_member.title") } CloudActionConfirmationDialogVariant::None => String::new(), } @@ -93,13 +80,17 @@ impl CloudActionConfirmationDialog { fn body_text(&self) -> String { match self.variant { - CloudActionConfirmationDialogVariant::LeaveTeam => LEAVE_TEAM_BODY_TEXT.to_string(), - CloudActionConfirmationDialogVariant::DeleteTeam => DELETE_TEAM_BODY_TEXT.to_string(), + CloudActionConfirmationDialogVariant::LeaveTeam => { + i18n::t("drive.confirmation.leave_team.body") + } + CloudActionConfirmationDialogVariant::DeleteTeam => { + i18n::t("drive.confirmation.delete_team.body") + } CloudActionConfirmationDialogVariant::LeaveTeamReloadCredits => { - LEAVE_TEAM_RELOAD_CREDITS_BODY_TEXT.to_string() + i18n::t("drive.confirmation.leave_team_reload_credits.body") } CloudActionConfirmationDialogVariant::RemoveTeamMemberReloadCredits => { - REMOVE_TEAM_MEMBER_RELOAD_CREDITS_BODY_TEXT.to_string() + i18n::t("drive.confirmation.remove_member_reload_credits.body") } CloudActionConfirmationDialogVariant::None => String::new(), } @@ -107,15 +98,17 @@ impl CloudActionConfirmationDialog { fn confirm_button_text(&self) -> String { match self.variant { - CloudActionConfirmationDialogVariant::LeaveTeam => LEAVE_TEAM_CONFIRM_TEXT.to_string(), + CloudActionConfirmationDialogVariant::LeaveTeam => { + i18n::t("drive.confirmation.leave_team.confirm") + } CloudActionConfirmationDialogVariant::DeleteTeam => { - DELETE_TEAM_CONFIRM_TEXT.to_string() + i18n::t("drive.confirmation.delete_team.confirm") } CloudActionConfirmationDialogVariant::LeaveTeamReloadCredits => { - LEAVE_TEAM_RELOAD_CREDITS_CONFIRM_TEXT.to_string() + i18n::t("drive.confirmation.leave_team_reload_credits.confirm") } CloudActionConfirmationDialogVariant::RemoveTeamMemberReloadCredits => { - REMOVE_TEAM_MEMBER_RELOAD_CREDITS_CONFIRM_TEXT.to_string() + i18n::t("drive.confirmation.remove_member_reload_credits.confirm") } CloudActionConfirmationDialogVariant::None => String::new(), } @@ -173,7 +166,7 @@ impl View for CloudActionConfirmationDialog { padding: Some(Coords::uniform(BUTTON_PADDING)), ..Default::default() }) - .with_text_label(CANCEL_TEXT.into()) + .with_text_label(i18n::t("common.cancel")) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { diff --git a/app/src/drive/cloud_object_naming_dialog.rs b/app/src/drive/cloud_object_naming_dialog.rs index 77eee56eda..a7193b7af8 100644 --- a/app/src/drive/cloud_object_naming_dialog.rs +++ b/app/src/drive/cloud_object_naming_dialog.rs @@ -29,13 +29,6 @@ const BUTTON_FONT_SIZE: f32 = 14.; const BUTTON_PADDING: f32 = 12.; const BUTTON_MARGIN_BETWEEN: f32 = 8.; -const NOTEBOOK_TITLE: &str = "Notebook name"; -const FOLDER_TITLE: &str = "Folder name"; -const ENV_VAR_COLLECTION_TITLE: &str = "Collection name"; -const CREATE_BUTTON_TEXT: &str = "Create"; -const CANCEL_BUTTON_TEXT: &str = "Cancel"; -const RENAME_BUTTON_TEXT: &str = "Rename"; - /// Struct holding necessary information and states for the dialog /// that opens when creating or updating a folder or notebook. /// @@ -139,16 +132,16 @@ impl CloudObjectNamingDialog { appearance: &Appearance, ) -> Box { let title = match object_type { - DriveObjectType::Notebook { .. } => NOTEBOOK_TITLE, - DriveObjectType::Folder => FOLDER_TITLE, - DriveObjectType::EnvVarCollection => ENV_VAR_COLLECTION_TITLE, + DriveObjectType::Notebook { .. } => i18n::t("drive.naming.notebook_name"), + DriveObjectType::Folder => i18n::t("drive.naming.folder_name"), + DriveObjectType::EnvVarCollection => i18n::t("drive.naming.collection_name"), // workflows and ai facts aren't a part of this dialog DriveObjectType::Workflow | DriveObjectType::AgentModeWorkflow | DriveObjectType::AIFact | DriveObjectType::AIFactCollection | DriveObjectType::MCPServer - | DriveObjectType::MCPServerCollection => "", + | DriveObjectType::MCPServerCollection => String::new(), }; Text::new_inline( @@ -223,8 +216,8 @@ impl CloudObjectNamingDialog { }; let primary_button_text = match self.is_rename { - true => RENAME_BUTTON_TEXT, - false => CREATE_BUTTON_TEXT, + true => i18n::t("common.rename"), + false => i18n::t("common.create"), }; let primary_button_action = self.current_primary_action(); @@ -239,7 +232,7 @@ impl CloudObjectNamingDialog { Some(primary_hovered_and_clicked_styles), Some(primary_disabled_styles), ) - .with_text_label(primary_button_text.into()); + .with_text_label(primary_button_text); if let Some(title) = self.title(app) { if title.is_empty() || !self.title_editor.as_ref(app).is_dirty(app) { @@ -261,7 +254,7 @@ impl CloudObjectNamingDialog { padding: Some(Coords::uniform(BUTTON_PADDING)), ..Default::default() }) - .with_text_label(CANCEL_BUTTON_TEXT.into()) + .with_text_label(i18n::t("common.cancel")) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { diff --git a/app/src/drive/empty_trash_confirmation_dialog.rs b/app/src/drive/empty_trash_confirmation_dialog.rs index 8664aea504..dfa79ffed3 100644 --- a/app/src/drive/empty_trash_confirmation_dialog.rs +++ b/app/src/drive/empty_trash_confirmation_dialog.rs @@ -8,12 +8,6 @@ use warpui::{AppContext, Element, Entity, SingletonEntity, TypedActionView, View use crate::appearance::Appearance; use crate::ui_components::dialog::{dialog_styles, Dialog}; -const CANCEL_TEXT: &str = "Cancel"; - -const EMPTY_TRASH_TITLE_TEXT: &str = "Are you sure you want to empty the trash?"; -const EMPTY_TRASH_BODY_TEXT: &str = "This action cannot be undone."; -const EMPTY_TRASH_CONFIRM_TEXT: &str = "Yes, empty trash"; - // This follows our new design standard for confirmation dialogs (e.g. used in the session sharing dialog) // Design team has discouraged us from continuing to use CloudActionConfirmationDialog's current design // TODO: update CloudActionConfirmationDialog to use this design @@ -65,7 +59,7 @@ impl View for EmptyTrashConfirmationDialog { let confirm_button = appearance .ui_builder() .button(ButtonVariant::Accent, self.confirm_mouse_state.clone()) - .with_centered_text_label(EMPTY_TRASH_CONFIRM_TEXT.into()) + .with_centered_text_label(i18n::t("drive.trash.empty_confirm")) .with_style(button_style) .build() .with_cursor(Cursor::PointingHand) @@ -77,7 +71,7 @@ impl View for EmptyTrashConfirmationDialog { let cancel_button = appearance .ui_builder() .button(ButtonVariant::Basic, self.cancel_mouse_state.clone()) - .with_centered_text_label(CANCEL_TEXT.into()) + .with_centered_text_label(i18n::t("common.cancel")) .with_style(button_style) .build() .with_cursor(Cursor::PointingHand) @@ -87,8 +81,8 @@ impl View for EmptyTrashConfirmationDialog { .finish(); Dialog::new( - EMPTY_TRASH_TITLE_TEXT.into(), - Some(EMPTY_TRASH_BODY_TEXT.into()), + i18n::t("drive.trash.empty_confirmation_title"), + Some(i18n::t("drive.trash.empty_confirmation_body")), UiComponentStyles { width: Some(460.), padding: Some(Coords::uniform(24.)), diff --git a/app/src/drive/export.rs b/app/src/drive/export.rs index 492ef1e902..80d4df4920 100644 --- a/app/src/drive/export.rs +++ b/app/src/drive/export.rs @@ -230,12 +230,12 @@ impl ExportManager { if is_bulk && self.exports.is_empty() { ToastStack::handle(ctx).update(ctx, move |toast_stack, ctx| { let link_label = if cfg!(target_os = "macos") { - "Open in Finder" + i18n::t("drive.export.open_in_finder") } else { - "Open in folder" + i18n::t("drive.export.open_in_folder") }; - let mut toast_link = ToastLink::new(link_label.to_string()); + let mut toast_link = ToastLink::new(link_label); if let Ok(path) = path { // The path to open in the bulk case is one level up from the export dir. let root_dir = path.parent().unwrap_or(path.as_path()).to_path_buf(); @@ -243,7 +243,7 @@ impl ExportManager { .with_onclick_action(WorkspaceAction::OpenInExplorer { path: root_dir }); } toast_stack.add_ephemeral_toast( - DismissibleToast::success("Finished exporting objects".to_string()) + DismissibleToast::success(i18n::t("drive.export.finished")) .with_link(toast_link), window_id, ctx, @@ -315,7 +315,7 @@ impl ExportManager { }; let name = if name.is_empty() { - "Untitled".to_string() + i18n::t("common.untitled") } else { safe_filename(&name) }; @@ -372,8 +372,8 @@ impl ExportManager { let window_id = export.remove().window_id; ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { let message = match id.display_name(ctx) { - Some(name) => format!("Failed to export {name}"), - None => "Export failed".to_string(), + Some(name) => i18n::t("drive.export.failed_named").replace("{name}", &name), + None => i18n::t("drive.export.failed"), }; toast_stack.add_persistent_toast(DismissibleToast::error(message), window_id, ctx); }); @@ -393,19 +393,19 @@ impl ExportManager { if !export.get().is_bulk { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { let message = match export.key().display_name(ctx) { - Some(name) => format!("Exported {name}"), - None => "Exported object".to_string(), + Some(name) => i18n::t("drive.export.exported_named").replace("{name}", &name), + None => i18n::t("drive.export.exported_object"), }; let link_label = if cfg!(target_os = "macos") { - "Open in Finder" + i18n::t("drive.export.open_in_finder") } else { - "Open in folder" + i18n::t("drive.export.open_in_folder") }; toast_stack.add_ephemeral_toast( DismissibleToast::success(message).with_link( - ToastLink::new(link_label.to_string()).with_onclick_action( + ToastLink::new(link_label).with_onclick_action( WorkspaceAction::OpenInExplorer { path: root_path }, ), ), @@ -450,7 +450,7 @@ impl ExportId { .map(|object| { let mut name = object.display_name(); if name.is_empty() { - name.push_str("Untitled") + name.push_str(&i18n::t("common.untitled")) } name }) diff --git a/app/src/drive/export_tests.rs b/app/src/drive/export_tests.rs index 61db5a83f5..c66c1fd9dc 100644 --- a/app/src/drive/export_tests.rs +++ b/app/src/drive/export_tests.rs @@ -98,6 +98,7 @@ impl ExportTest { } fn initialize_app(app: &mut App) { + i18n::set_locale("en"); app.add_singleton_model(CloudModel::mock); app.add_singleton_model(ExportManager::new); app.add_singleton_model(UserWorkspaces::default_mock); diff --git a/app/src/drive/import/modal.rs b/app/src/drive/import/modal.rs index eeffc2737d..88d02fcb27 100644 --- a/app/src/drive/import/modal.rs +++ b/app/src/drive/import/modal.rs @@ -228,9 +228,13 @@ impl ImportModal { Shrinkable::new( 1.0, Align::new( - Text::new_inline("Import", appearance.ui_font_family(), HEADER_FONT_SIZE) - .with_color(appearance.theme().active_ui_text_color().into()) - .finish(), + Text::new_inline( + i18n::t("drive.import.title"), + appearance.ui_font_family(), + HEADER_FONT_SIZE, + ) + .with_color(appearance.theme().active_ui_text_color().into()) + .finish(), ) .left() .finish(), @@ -282,9 +286,9 @@ impl ImportModal { fn render_footer(&self, appearance: &Appearance, app: &AppContext) -> Box { let button_text = if !self.import_modal.as_ref(app).upload_in_progress(app) { - "Close".to_string() + i18n::t("common.close") } else { - "Cancel".to_string() + i18n::t("common.cancel") }; Container::new( diff --git a/app/src/drive/import/modal_body.rs b/app/src/drive/import/modal_body.rs index 4434a33e9a..b03bfc3a9e 100644 --- a/app/src/drive/import/modal_body.rs +++ b/app/src/drive/import/modal_body.rs @@ -119,7 +119,7 @@ impl ImportModalBody { ImportQueueEvent::FileCompleted { file_id, server_id } => { let result = match server_id { Some(id) => UploadResult::Success(id.clone()), - None => UploadResult::Error("Failed to upload file to server".to_string()), + None => UploadResult::Error(i18n::t("drive.import.failed_upload_file")), }; // Update the upstream folder status with the upload success state. @@ -133,9 +133,7 @@ impl ImportModalBody { } => { let result = match server_id { Some(id) => UploadResult::Success(id.clone()), - None => { - UploadResult::Error("Failed to upload folder to server".to_string()) - } + None => UploadResult::Error(i18n::t("drive.import.failed_upload_folder")), }; state.mark_folder_synced(result, *folder_id); @@ -380,13 +378,13 @@ impl ImportModalBody { let file_picker_button = if is_loading { base_button - .with_centered_text_label("Preparing...".to_string()) + .with_centered_text_label(i18n::t("drive.import.preparing")) .disabled() } else { base_button.with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::TextFirst, - "Choose files...".to_string(), + i18n::t("drive.import.choose_files"), Icon::Import.to_warpui_icon( appearance .theme() @@ -426,7 +424,7 @@ impl ImportModalBody { let link_to_document = appearance .ui_builder() .link( - "Learn about file support and formatting".to_string(), + i18n::t("drive.import.learn_file_support"), Some(FILE_TYPE_DOCS_URL.to_string()), None, self.link_mouse_state.clone(), diff --git a/app/src/drive/import/nodes.rs b/app/src/drive/import/nodes.rs index 5cd6119fde..1b5afcf56b 100644 --- a/app/src/drive/import/nodes.rs +++ b/app/src/drive/import/nodes.rs @@ -881,7 +881,9 @@ impl FileUploadState { file_node_to_update.status = match result { UploadResult::Success(id) => UploadStatus::Loaded(id), - UploadResult::Error(e) => UploadStatus::Error(format!("Failed to parse file: {e}")), + UploadResult::Error(e) => UploadStatus::Error( + i18n::t("drive.import.failed_parse_file").replace("{error}", &e), + ), }; let parent_id = file_node_to_update.parent_id; diff --git a/app/src/drive/index.rs b/app/src/drive/index.rs index a412d3c648..cb35c6bfc6 100644 --- a/app/src/drive/index.rs +++ b/app/src/drive/index.rs @@ -97,8 +97,6 @@ use crate::workspaces::user_workspaces::UserWorkspaces; use crate::workspaces::workspace::WorkspaceUid; use crate::{report_if_error, send_telemetry_from_ctx, ObjectActions}; -const WARP_DRIVE_TITLE: &str = "Warp Drive"; - // Team zero state consts const HINT_HORIZONTAL_PADDING: f32 = 18.; const ITEM_INTERNAL_PADDING: f32 = 4.; @@ -145,7 +143,6 @@ const HOVER_PREVIEW_Y_OFFSET: f32 = 0.; const CREATE_TEAM_ICON_WIDTH: f32 = 16.; const CREATE_TEAM_ICON_HEIGHT: f32 = 16.; -const CREATE_TEAM_TEXT: &str = "Share commands & knowledge with your teammates."; const LOADING_ICON_WIDTH: f32 = 16.; const LOADING_ICON_HEIGHT: f32 = 16.; @@ -156,21 +153,6 @@ const OFFLINE_BANNER_ICON_SPACING: f32 = 8.; const OFFLINE_BANNER_PADDING_HORIZONTAL: f32 = 16.; const OFFLINE_BANNER_PADDING_VERTICAL: f32 = 4.; -const FOLDER_LABEL: &str = "Folder"; -const NOTEBOOK_LABEL: &str = "Notebook"; -const WORKFLOW_LABEL: &str = "Workflow"; -const AGENT_MODE_WORKFLOW_LABEL: &str = "Prompt"; -const ENV_VAR_COLLECTION_LABEL: &str = "Environment variables"; -const INDEX_FOLDER_LABEL: &str = "New folder"; -const INDEX_NOTEBOOK_LABEL: &str = "New notebook"; -const INDEX_WORKFLOW_LABEL: &str = "New workflow"; -const INDEX_AGENT_MODE_WORKFLOW_LABEL: &str = "New prompt"; -const INDEX_ENV_VAR_COLLECTION_LABEL: &str = "New environment variables"; - -const IMPORT_LABEL: &str = "Import"; -const REMOVE_LABEL: &str = "Remove"; -const OFFLINE_BANNER_TEXT: &str = "You are offline. Some files will be read only."; - pub const DRIVE_INDEX_VIEW_POSITION_ID: &str = "drive_index_view_id"; // Sets the speed of the autoscroll that occurs when you drag an item near the Warp Drive border. @@ -178,26 +160,26 @@ pub const AUTOSCROLL_SPEED_MULTIPLIER: f32 = 10.; // Sets the distance from a border at which scroll events start to occur. pub const AUTOSCROLL_DETECTION_DISTANCE: f32 = 30.0; -const ZERO_STATE_WORKFLOW_LABEL: &str = "Workflow"; -const ZERO_STATE_NOTEBOOK_LABEL: &str = "Notebook"; - -const SORTING_BUTTON_TOOLTIP_LABEL: &str = "Sort by"; - -const RETRY_BUTTON_TOOLTIP_LABEL: &str = "Retry sync"; - -const SHARED_OBJECT_LIMIT_HIT_BANNER_LINE: &str = - "Upgrade for access to more notebooks, workflows, shared sessions, and AI credits."; - -const PAYMENT_ISSUE_BANNER_LINE_1: &str = - "Shared objects have been restricted due to a subscription payment issue."; - -const PAYMENT_ISSUE_BANNER_LINE_2_ADMIN: &str = - "Please update your payment information to restore access."; - -const PAYMENT_ISSUE_BANNER_LINE_2_ADMIN_ENTERPRISE: &str = - "Please contact support@warp.dev to restore access."; +fn shared_limit_object_type_name(object_type: ObjectType) -> String { + match object_type { + ObjectType::Notebook => i18n::t("drive.limit.object.notebooks"), + ObjectType::Workflow => i18n::t("drive.limit.object.workflows"), + ObjectType::Folder => i18n::t("drive.limit.object.folders"), + ObjectType::GenericStringObject(GenericStringObjectFormat::Json( + JsonObjectType::EnvVarCollection, + )) => i18n::t("drive.limit.object.environment_variables"), + ObjectType::GenericStringObject(GenericStringObjectFormat::Json( + JsonObjectType::AIFact, + )) => i18n::t("drive.limit.object.rules"), + ObjectType::GenericStringObject(_) => i18n::t("drive.limit.object.objects"), + } +} -const PAYMENT_ISSUE_BANNER_LINE_2_NONADMIN: &str = "Please contact a team admin to restore access."; +fn banner_text(first: &str, second: &str) -> String { + i18n::t("drive.banner.joined") + .replace("{first}", first) + .replace("{second}", second) +} /// Struct to hold different state-related information on per-space basis. /// Currently, we only have 1 space (1 Team), but as we're working on personal space, and add @@ -924,7 +906,7 @@ impl DriveIndex { ..Default::default() }; let mut editor = EditorView::single_line(options, ctx); - editor.set_placeholder_text("Untitled", ctx); + editor.set_placeholder_text(i18n::t("common.untitled"), ctx); editor }); @@ -1590,7 +1572,7 @@ impl DriveIndex { Some(empty_trash_hover_style), Some(empty_trash_disabled_style), ) - .with_text_label("Empty trash".to_string()); + .with_text_label(i18n::t("drive.empty_trash")); // Only show Empty Trash button when online, do not show for Shared space if self.is_online(app) && space != &Space::Shared { @@ -1712,18 +1694,24 @@ impl DriveIndex { } (DriveIndexVariant::MainIndex, DriveIndexSection::CreateATeam) => { if self.is_online(app) { - Some(self.render_team_section_header(CREATE_TEAM_TEXT.to_owned(), appearance)) + Some( + self.render_team_section_header( + i18n::t("drive.team.create_hint"), + appearance, + ), + ) } else { None } } (DriveIndexVariant::MainIndex, DriveIndexSection::JoinTeam) => { if self.is_online(app) { - let join_teams_text = format!( - "Collaborate with {} of your teammates already on Warp.", - UserWorkspaces::handle(app) + let join_teams_text = i18n::t("drive.team.join_hint").replace( + "{count}", + &UserWorkspaces::handle(app) .as_ref(app) .total_teammates_in_joinable_teams() + .to_string(), ); Some(self.render_team_section_header(join_teams_text, appearance)) } else { @@ -1803,7 +1791,7 @@ impl DriveIndex { let title = Container::new( appearance .ui_builder() - .wrappable_text("TRASH".to_string(), false) + .wrappable_text(i18n::t("drive.trash.uppercase"), false) .with_style(UiComponentStyles { font_family_id: Some(appearance.ui_font_family()), font_size: Some(SECTION_HEADER_FONT_SIZE), @@ -2030,7 +2018,7 @@ impl DriveIndex { fn render_team_zero_state_hint( &self, icon: Icon, - label: &'static str, + label: String, appearance: &Appearance, ) -> Box { let rendered_icon = ConstrainedBox::new( @@ -2074,12 +2062,10 @@ impl DriveIndex { } fn render_team_space_zero_state(&self, appearance: &Appearance) -> Box { - let hint_text = - "Drag or move a personal workflow or notebook here to share it with your team."; let zero_state_info = Container::new( appearance .ui_builder() - .wrappable_text(hint_text, true) + .wrappable_text(i18n::t("drive.team.zero_state_hint"), true) .with_style(UiComponentStyles { font_family_id: Some(appearance.ui_font_family()), font_size: Some(ITEM_FONT_SIZE), @@ -2095,10 +2081,26 @@ impl DriveIndex { let zero_state_contents = Flex::column().with_children([ zero_state_info, - self.render_team_zero_state_hint(Icon::Workflow, ZERO_STATE_WORKFLOW_LABEL, appearance), - self.render_team_zero_state_hint(Icon::Workflow, ZERO_STATE_WORKFLOW_LABEL, appearance), - self.render_team_zero_state_hint(Icon::Notebook, ZERO_STATE_NOTEBOOK_LABEL, appearance), - self.render_team_zero_state_hint(Icon::Notebook, ZERO_STATE_NOTEBOOK_LABEL, appearance), + self.render_team_zero_state_hint( + Icon::Workflow, + i18n::t("drive.object.workflow"), + appearance, + ), + self.render_team_zero_state_hint( + Icon::Workflow, + i18n::t("drive.object.workflow"), + appearance, + ), + self.render_team_zero_state_hint( + Icon::Notebook, + i18n::t("drive.object.notebook"), + appearance, + ), + self.render_team_zero_state_hint( + Icon::Notebook, + i18n::t("drive.object.notebook"), + appearance, + ), ]); Container::new(zero_state_contents.finish()) @@ -2126,7 +2128,7 @@ impl DriveIndex { appearance: &Appearance, app: &AppContext, ) -> Box { - let button_text = "Create team".to_owned(); + let button_text = i18n::t("drive.team.create_team"); let create_button = if UserWorkspaces::as_ref(app).total_teammates_in_joinable_teams() == 0 { appearance @@ -2194,9 +2196,9 @@ impl DriveIndex { app: &AppContext, ) -> Box { let text = if UserWorkspaces::as_ref(app).num_joinable_teams() > 1 { - "View teams to join" + i18n::t("drive.team.view_teams_to_join") } else { - "View team to join" + i18n::t("drive.team.view_team_to_join") }; let join_button = Container::new( @@ -2220,7 +2222,7 @@ impl DriveIndex { font_size: Some(14.), ..Default::default() }) - .with_centered_text_label(text.to_owned()) + .with_centered_text_label(text) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { @@ -2232,10 +2234,14 @@ impl DriveIndex { .finish(); let or_text = Container::new( - Text::new_inline("Or", appearance.ui_font_family(), ITEM_FONT_SIZE) - .with_color(appearance.theme().nonactive_ui_text_color().into()) - .with_style(Properties::default().weight(Weight::Medium)) - .finish(), + Text::new_inline( + i18n::t("common.or_standalone"), + appearance.ui_font_family(), + ITEM_FONT_SIZE, + ) + .with_color(appearance.theme().nonactive_ui_text_color().into()) + .with_style(Properties::default().weight(Weight::Medium)) + .finish(), ) .with_margin_top(14.) .finish(); @@ -2394,7 +2400,7 @@ impl DriveIndex { Shrinkable::new( 1., Text::new_inline( - OFFLINE_BANNER_TEXT, + i18n::t("drive.offline_banner"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -2476,7 +2482,7 @@ impl DriveIndex { let text = Container::new( appearance .ui_builder() - .span(WARP_DRIVE_TITLE.to_string()) + .span(i18n::t("drive.title")) .with_style(UiComponentStyles { font_family_id: Some(appearance.ui_font_family()), font_size: Some(TITLE_FONT_SIZE), @@ -2538,7 +2544,7 @@ impl DriveIndex { let text = Container::new( appearance .ui_builder() - .span("Trash".to_string()) + .span(i18n::t("drive.trash.title")) .with_style(UiComponentStyles { font_family_id: Some(appearance.ui_font_family()), font_size: Some(TITLE_FONT_SIZE), @@ -2607,11 +2613,7 @@ impl DriveIndex { 1., appearance .ui_builder() - .wrappable_text( - "Items in the trash will be deleted forever after 30 days." - .to_string(), - true, - ) + .wrappable_text(i18n::t("drive.trash.deleted_after_30_days"), true) .with_style(UiComponentStyles { font_family_id: Some(appearance.ui_font_family()), font_size: Some(WARNING_FONT_SIZE), @@ -2966,9 +2968,7 @@ impl DriveIndex { |mouse_state| { let mut stack = Stack::new().with_child(loading_icon); if mouse_state.is_hovered() { - let tooltip = appearance - .ui_builder() - .tool_tip(String::from("Syncing Warp Drive")); + let tooltip = appearance.ui_builder().tool_tip(i18n::t("drive.syncing")); stack.add_positioned_overlay_child( tooltip.build().finish(), @@ -3004,9 +3004,7 @@ impl DriveIndex { self.mouse_state_handles.sorting_button_mouse_state.clone(), |mouse_state| { if mouse_state.is_hovered() { - let tooltip = appearance - .ui_builder() - .tool_tip(SORTING_BUTTON_TOOLTIP_LABEL.to_string()); + let tooltip = appearance.ui_builder().tool_tip(i18n::t("drive.sort_by")); button.add_positioned_overlay_child( tooltip.build().finish(), @@ -3036,7 +3034,7 @@ impl DriveIndex { ) .with_tooltip(move || { ui_builder - .tool_tip(RETRY_BUTTON_TOOLTIP_LABEL.to_string()) + .tool_tip(i18n::t("drive.retry_sync")) .build() .finish() }) @@ -3688,7 +3686,7 @@ impl DriveIndex { if is_online { menu_items.push( - MenuItemFields::new(FOLDER_LABEL) + MenuItemFields::new(i18n::t("drive.menu.folder")) .with_on_select_action(DriveIndexAction::create_object( DriveObjectType::Folder, *space, @@ -3700,7 +3698,7 @@ impl DriveIndex { } menu_items.push( - MenuItemFields::new(WORKFLOW_LABEL) + MenuItemFields::new(i18n::t("drive.menu.workflow")) .with_on_select_action(DriveIndexAction::create_object( DriveObjectType::Workflow, *space, @@ -3712,7 +3710,7 @@ impl DriveIndex { if FeatureFlag::AgentModeWorkflows.is_enabled() { menu_items.push( - MenuItemFields::new(AGENT_MODE_WORKFLOW_LABEL) + MenuItemFields::new(i18n::t("drive.menu.prompt")) .with_on_select_action(DriveIndexAction::create_object( DriveObjectType::AgentModeWorkflow, *space, @@ -3724,7 +3722,7 @@ impl DriveIndex { } menu_items.push( - MenuItemFields::new(NOTEBOOK_LABEL) + MenuItemFields::new(i18n::t("drive.menu.notebook")) .with_on_select_action(DriveIndexAction::create_object( DriveObjectType::Notebook { is_ai_document: false, @@ -3737,7 +3735,7 @@ impl DriveIndex { ); menu_items.push( - MenuItemFields::new(ENV_VAR_COLLECTION_LABEL) + MenuItemFields::new(i18n::t("drive.menu.environment_variables")) .with_on_select_action(DriveIndexAction::create_object( DriveObjectType::EnvVarCollection, *space, @@ -3748,7 +3746,7 @@ impl DriveIndex { ); menu_items.push( - MenuItemFields::new(IMPORT_LABEL) + MenuItemFields::new(i18n::t("drive.import.title")) .with_on_select_action(DriveIndexAction::OpenImportModal { space: *space, initial_folder_id: None, @@ -3950,7 +3948,7 @@ impl DriveIndex { .with_cross_axis_alignment(CrossAxisAlignment::Start) .with_main_axis_alignment(MainAxisAlignment::SpaceBetween) .with_child( - Text::new_inline("Warp Drive".to_string(), appearance.ui_font_family(), 14.) + Text::new_inline(i18n::t("drive.title"), appearance.ui_font_family(), 14.) .with_color(theme.main_text_color(background_color).into()) .with_style(Properties { weight: warpui::fonts::Weight::Bold, @@ -3962,8 +3960,7 @@ impl DriveIndex { .with_child(close_icon_button) .finish(); - let personal_object_limit_description = - "Sign up for free to increase your storage limit and unlock more features."; + let personal_object_limit_description = i18n::t("drive.limit.personal_sign_up_description"); let body_text = appearance .ui_builder() @@ -4052,7 +4049,7 @@ impl DriveIndex { .with_style(button_styles) .with_hovered_styles(hovered_and_clicked_styles) .with_active_styles(hovered_and_clicked_styles) - .with_centered_text_label("Sign up".to_string()) + .with_centered_text_label(i18n::t("settings.account.sign_up")) .build() .on_click(|ctx, _, _| ctx.dispatch_typed_action(DriveIndexAction::SignupAnonymousUser)) .with_cursor(Cursor::PointingHand) @@ -4109,15 +4106,15 @@ impl DriveIndex { }; let name = match object_type { - DriveObjectType::Notebook { .. } => "Notebooks", - DriveObjectType::Workflow => "Workflows", - DriveObjectType::EnvVarCollection => "Environment Variables", - DriveObjectType::Folder => "Folders", - DriveObjectType::AgentModeWorkflow => "Agent Workflows", - DriveObjectType::AIFact => "AI Fact", - DriveObjectType::AIFactCollection => "Rules", - DriveObjectType::MCPServer => "MCP Server", - DriveObjectType::MCPServerCollection => "MCP Servers", + DriveObjectType::Notebook { .. } => i18n::t("drive.limit.notebooks"), + DriveObjectType::Workflow => i18n::t("drive.limit.workflows"), + DriveObjectType::EnvVarCollection => i18n::t("drive.limit.environment_variables"), + DriveObjectType::Folder => i18n::t("drive.limit.folders"), + DriveObjectType::AgentModeWorkflow => i18n::t("drive.limit.agent_workflows"), + DriveObjectType::AIFact => i18n::t("drive.limit.ai_fact"), + DriveObjectType::AIFactCollection => i18n::t("drive.limit.rules"), + DriveObjectType::MCPServer => i18n::t("drive.limit.mcp_server"), + DriveObjectType::MCPServerCollection => i18n::t("drive.limit.mcp_servers"), }; let name_styles = UiComponentStyles { font_family_id: Some(appearance.ui_font_family()), @@ -4168,15 +4165,19 @@ impl DriveIndex { let highlight = Highlight::new().with_properties(Properties::default().weight(Weight::Bold)); - let banner_line_1 = format!("You've run out of {object_type}s on your plan."); + let object_type_name = shared_limit_object_type_name(object_type); + let banner_line_1 = + i18n::t("drive.limit.shared_limit_reached").replace("{object_type}", &object_type_name); + let banner_line_2 = i18n::t("drive.limit.shared_limit_upgrade"); + let banner_text = banner_text(&banner_line_1, &banner_line_2); let body = Container::new( appearance .ui_builder() - .wrappable_text( - format!("{banner_line_1} {SHARED_OBJECT_LIMIT_HIT_BANNER_LINE}"), - true, + .wrappable_text(banner_text, true) + .with_highlights( + (0..banner_line_1.chars().count()).collect::>(), + highlight, ) - .with_highlights((0..banner_line_1.len()).collect::>(), highlight) .with_style(UiComponentStyles { font_size: Some(12.), font_color: Some(appearance.theme().main_text_color(background_color).into()), @@ -4196,7 +4197,7 @@ impl DriveIndex { .shared_object_limit_hit_banner_button_mouse_state .clone(), ) - .with_centered_text_label("Compare plans".into()) + .with_centered_text_label(i18n::t("settings.account.compare_plans")) .with_style(UiComponentStyles { font_size: Some(14.), font_weight: Some(Weight::Light), @@ -4252,23 +4253,22 @@ impl DriveIndex { Highlight::new().with_properties(Properties::default().weight(Weight::Bold)); let banner_line_2 = if has_admin_permissions && is_on_stripe_paid_plan { - PAYMENT_ISSUE_BANNER_LINE_2_ADMIN + i18n::t("drive.payment_issue.admin") } else if has_admin_permissions && !is_on_stripe_paid_plan { - PAYMENT_ISSUE_BANNER_LINE_2_ADMIN_ENTERPRISE + i18n::t("drive.payment_issue.admin_enterprise") } else { - PAYMENT_ISSUE_BANNER_LINE_2_NONADMIN + i18n::t("drive.payment_issue.non_admin") }; + let banner_line_1 = i18n::t("drive.payment_issue.restricted"); + let payment_issue_text = banner_text(&banner_line_1, &banner_line_2); body.add_child( Container::new( appearance .ui_builder() - .wrappable_text( - format!("{PAYMENT_ISSUE_BANNER_LINE_1} {banner_line_2}").to_string(), - true, - ) + .wrappable_text(payment_issue_text, true) .with_highlights( - (0..PAYMENT_ISSUE_BANNER_LINE_1.len()).collect::>(), + (0..banner_line_1.chars().count()).collect::>(), highlight, ) .with_style(UiComponentStyles { @@ -4294,7 +4294,7 @@ impl DriveIndex { .payment_issue_banner_button_mouse_state .clone(), ) - .with_centered_text_label("Manage billing".into()) + .with_centered_text_label(i18n::t("settings.account.manage_billing")) .with_style(UiComponentStyles { font_size: Some(14.), font_weight: Some(Weight::Light), @@ -4361,7 +4361,7 @@ impl DriveIndex { if self.is_online(app) { if !FeatureFlag::SharedWithMe.is_enabled() || editability.can_edit() { menu_items.push( - MenuItemFields::new(INDEX_FOLDER_LABEL) + MenuItemFields::new(i18n::t("drive.menu.new_folder")) .with_on_select_action(DriveIndexAction::create_object( DriveObjectType::Folder, *space, @@ -4371,7 +4371,7 @@ impl DriveIndex { .into_item(), ); menu_items.push( - MenuItemFields::new(INDEX_WORKFLOW_LABEL) + MenuItemFields::new(i18n::t("drive.menu.new_workflow")) .with_on_select_action(DriveIndexAction::create_object( DriveObjectType::Workflow, *space, @@ -4383,7 +4383,7 @@ impl DriveIndex { if FeatureFlag::AgentModeWorkflows.is_enabled() { menu_items.push( - MenuItemFields::new(INDEX_AGENT_MODE_WORKFLOW_LABEL) + MenuItemFields::new(i18n::t("drive.menu.new_prompt")) .with_on_select_action(DriveIndexAction::create_object( DriveObjectType::AgentModeWorkflow, *space, @@ -4395,7 +4395,7 @@ impl DriveIndex { } menu_items.push( - MenuItemFields::new(INDEX_NOTEBOOK_LABEL) + MenuItemFields::new(i18n::t("drive.menu.new_notebook")) .with_on_select_action(DriveIndexAction::create_object( DriveObjectType::Notebook { is_ai_document: false, @@ -4408,7 +4408,7 @@ impl DriveIndex { ); menu_items.push( - MenuItemFields::new(INDEX_ENV_VAR_COLLECTION_LABEL) + MenuItemFields::new(i18n::t("drive.menu.new_environment_variables")) .with_on_select_action(DriveIndexAction::create_object( DriveObjectType::EnvVarCollection, *space, @@ -4422,7 +4422,7 @@ impl DriveIndex { } if !FeatureFlag::SharedWithMe.is_enabled() || editability.can_edit() { menu_items.push( - MenuItemFields::new("Rename") + MenuItemFields::new(i18n::t("common.rename")) .with_on_select_action( DriveIndexAction::OpenCloudObjectNamingDialog { space: *space, @@ -4440,7 +4440,7 @@ impl DriveIndex { if let Some(object) = object { if let Some(object_link) = object.object_link() { menu_items.push( - MenuItemFields::new("Copy link") + MenuItemFields::new(i18n::t("common.copy_link")) .with_on_select_action(DriveIndexAction::CopyObjectLinkToClipboard( object_link, )) @@ -4449,7 +4449,7 @@ impl DriveIndex { ); if editability.can_edit() { menu_items.push( - MenuItemFields::new("Share") + MenuItemFields::new(i18n::t("common.share")) .with_on_select_action(DriveIndexAction::ToggleShareDialog { warp_drive_item_id: *warp_drive_item_id, }) @@ -4462,7 +4462,7 @@ impl DriveIndex { if !FeatureFlag::SharedWithMe.is_enabled() || editability.can_edit() { menu_items.push( - MenuItemFields::new(IMPORT_LABEL) + MenuItemFields::new(i18n::t("drive.import.title")) .with_on_select_action(DriveIndexAction::OpenImportModal { space: *space, initial_folder_id: Some(*folder_id), @@ -4472,7 +4472,7 @@ impl DriveIndex { ); } menu_items.push( - MenuItemFields::new("Collapse all") + MenuItemFields::new(i18n::t("common.collapse_all")) .with_on_select_action(DriveIndexAction::CollapseAllInLocation( CloudObjectLocation::Folder(*folder_id), )) @@ -4483,7 +4483,7 @@ impl DriveIndex { if let Some(object) = object { if FeatureFlag::SharedWithMe.is_enabled() && object.can_leave(app) { menu_items.push( - MenuItemFields::new(REMOVE_LABEL) + MenuItemFields::new(i18n::t("common.remove")) .with_on_select_action(DriveIndexAction::LeaveSharedObject { cloud_object_type_and_id: *cloud_object_type_and_id, }) @@ -4497,7 +4497,7 @@ impl DriveIndex { if let Some(object) = object { if self.is_online(app) && object.metadata().is_errored() { menu_items.push( - MenuItemFields::new("Retry") + MenuItemFields::new(i18n::t("common.retry")) .with_on_select_action(DriveIndexAction::RetryFailedObject( *cloud_object_type_and_id, )) @@ -4507,7 +4507,7 @@ impl DriveIndex { if let Some(server_id) = cloud_object_type_and_id.server_id() { menu_items.push( - MenuItemFields::new("Revert to server") + MenuItemFields::new(i18n::t("drive.revert_to_server")) .with_on_select_action(DriveIndexAction::RevertFailedObject( server_id, )) @@ -4529,7 +4529,7 @@ impl DriveIndex { { if let Some(ai_document_id) = notebook.model().ai_document_id { menu_items.push( - MenuItemFields::new("Attach to active session") + MenuItemFields::new(i18n::t("common.attach_to_active_session")) .with_on_select_action(DriveIndexAction::AttachPlanAsContext( ai_document_id, )) @@ -4577,9 +4577,9 @@ impl DriveIndex { let workflow: Option<&CloudWorkflow> = object.into(); let workflow = workflow.expect("Object is workflow"); let label = if workflow.model().data.is_agent_mode_workflow() { - "Copy prompt" + i18n::t("terminal.context_menu.copy_prompt") } else { - "Copy workflow text" + i18n::t("drive.copy_workflow_text") }; menu_items.push( MenuItemFields::new(label) @@ -4591,7 +4591,7 @@ impl DriveIndex { ); if workflow.model().data.is_agent_mode_workflow() { menu_items.push( - MenuItemFields::new("Copy id") + MenuItemFields::new(i18n::t("common.copy_id")) .with_on_select_action(DriveIndexAction::CopyWorkflowId( *cloud_object_type_and_id, )) @@ -4604,7 +4604,7 @@ impl DriveIndex { JsonObjectType::EnvVarCollection, )) => { menu_items.push( - MenuItemFields::new("Copy variables") + MenuItemFields::new(i18n::t("drive.copy_variables")) .with_on_select_action(DriveIndexAction::CopyObjectToClipboard( *cloud_object_type_and_id, )) @@ -4612,7 +4612,7 @@ impl DriveIndex { .into_item(), ); menu_items.push( - MenuItemFields::new("Load in subshell") + MenuItemFields::new(i18n::t("drive.load_in_subshell")) .with_on_select_action( DriveIndexAction::InvokeEnvVarCollectionInSubshell( object.cloud_object_type_and_id(), @@ -4640,13 +4640,16 @@ impl DriveIndex { match space { Space::Personal | Space::Shared => None, Space::Team { .. } => Some( - MenuItemFields::new(format!("Move to {}", space.name(app))) - .with_on_select_action(DriveIndexAction::MoveObject { - cloud_object_type_and_id: *cloud_object_type_and_id, - new_space: *space, - }) - .with_icon(Icon::Move) - .into_item(), + MenuItemFields::new( + i18n::t("drive.menu.move_to_space") + .replace("{space}", &space.name(app)), + ) + .with_on_select_action(DriveIndexAction::MoveObject { + cloud_object_type_and_id: *cloud_object_type_and_id, + new_space: *space, + }) + .with_icon(Icon::Move) + .into_item(), ), } } else { @@ -4668,7 +4671,7 @@ impl DriveIndex { )) => { if let Some(object_link) = object.object_link() { menu_items.push( - MenuItemFields::new("Copy link") + MenuItemFields::new(i18n::t("common.copy_link")) .with_on_select_action( DriveIndexAction::CopyObjectLinkToClipboard(object_link), ) @@ -4678,7 +4681,7 @@ impl DriveIndex { } if editability.can_edit() { menu_items.push( - MenuItemFields::new("Share") + MenuItemFields::new(i18n::t("common.share")) .with_on_select_action(DriveIndexAction::ToggleShareDialog { warp_drive_item_id: *warp_drive_item_id, }) @@ -4696,7 +4699,7 @@ impl DriveIndex { if let Some(object_link) = object.object_link() { if let Ok(url) = Url::parse(&object_link) { menu_items.push( - MenuItemFields::new("Open on Desktop") + MenuItemFields::new(i18n::t("common.open_on_desktop")) .with_on_select_action( DriveIndexAction::OpenObjectLinkOnDesktop(url), ) @@ -4711,7 +4714,7 @@ impl DriveIndex { || (self.is_online(app) && matches!(space, Space::Team { .. })) { menu_items.push( - MenuItemFields::new("Duplicate") + MenuItemFields::new(i18n::t("common.duplicate")) .with_on_select_action(DriveIndexAction::DuplicateObject( *cloud_object_type_and_id, )) @@ -4726,7 +4729,7 @@ impl DriveIndex { #[cfg(feature = "local_fs")] if object.can_export() { menu_items.push( - MenuItemFields::new("Export") + MenuItemFields::new(i18n::t("common.export")) .with_on_select_action(DriveIndexAction::ExportObject( *cloud_object_type_and_id, )) @@ -4737,7 +4740,7 @@ impl DriveIndex { if FeatureFlag::SharedWithMe.is_enabled() && object.can_leave(app) { menu_items.push( - MenuItemFields::new(REMOVE_LABEL) + MenuItemFields::new(i18n::t("common.remove")) .with_on_select_action(DriveIndexAction::LeaveSharedObject { cloud_object_type_and_id: *cloud_object_type_and_id, }) @@ -4752,7 +4755,7 @@ impl DriveIndex { && (!FeatureFlag::SharedWithMe.is_enabled() || access_level.can_trash()) { menu_items.push( - MenuItemFields::new("Trash") + MenuItemFields::new(i18n::t("common.trash")) .with_on_select_action(DriveIndexAction::TrashObject { cloud_object_type_and_id: *cloud_object_type_and_id, }) @@ -4773,9 +4776,9 @@ impl DriveIndex { prefer_open: bool, ) -> MenuItemFields { if (FeatureFlag::SharedWithMe.is_enabled() && !editability.can_edit()) || prefer_open { - MenuItemFields::new("Open").with_icon(Icon::Eye) + MenuItemFields::new(i18n::t("common.open")).with_icon(Icon::Eye) } else { - MenuItemFields::new("Edit").with_icon(Icon::Rename) + MenuItemFields::new(i18n::t("common.edit")).with_icon(Icon::Rename) } } @@ -4798,7 +4801,7 @@ impl DriveIndex { if let Some(object) = object { if self.is_online(app) && object.metadata().is_errored() { menu_items.push( - MenuItemFields::new("Retry") + MenuItemFields::new(i18n::t("common.retry")) .with_on_select_action(DriveIndexAction::RetryFailedObject( *cloud_object_type_and_id, )) @@ -4808,7 +4811,7 @@ impl DriveIndex { if let Some(server_id) = cloud_object_type_and_id.server_id() { menu_items.push( - MenuItemFields::new("Revert to server") + MenuItemFields::new(i18n::t("drive.revert_to_server")) .with_on_select_action(DriveIndexAction::RevertFailedObject(server_id)) .with_icon(Icon::ReverseLeft) .into_item(), @@ -4820,7 +4823,7 @@ impl DriveIndex { if self.online_only_operation_allowed(cloud_object_type_and_id, app) { if !FeatureFlag::SharedWithMe.is_enabled() || access_level.can_trash() { menu_items.push( - MenuItemFields::new("Restore") + MenuItemFields::new(i18n::t("common.restore")) .with_on_select_action(DriveIndexAction::UntrashObject { cloud_object_type_and_id: *cloud_object_type_and_id, }) @@ -4830,7 +4833,7 @@ impl DriveIndex { } if !FeatureFlag::SharedWithMe.is_enabled() || access_level.can_delete() { menu_items.push( - MenuItemFields::new("Delete forever") + MenuItemFields::new(i18n::t("common.delete_forever")) .with_on_select_action(DriveIndexAction::DeleteObject { cloud_object_type_and_id: *cloud_object_type_and_id, }) @@ -4902,7 +4905,7 @@ impl DriveIndex { space: *space, offset, }); - let menu_items = vec![MenuItemFields::new("Collapse all") + let menu_items = vec![MenuItemFields::new(i18n::t("common.collapse_all")) .with_on_select_action(DriveIndexAction::CollapseAllInLocation( CloudObjectLocation::Space(*space), )) diff --git a/app/src/drive/index_tests.rs b/app/src/drive/index_tests.rs index d5ce04002f..8c8729f6ca 100644 --- a/app/src/drive/index_tests.rs +++ b/app/src/drive/index_tests.rs @@ -34,6 +34,7 @@ use crate::workspaces::user_workspaces::UserWorkspaces; use crate::ASSETS; fn initialize_app(app: &mut App) { + i18n::set_locale("en"); initialize_settings_for_tests(app); app.add_singleton_model(CloudModel::mock); diff --git a/app/src/drive/items/ai_fact_collection.rs b/app/src/drive/items/ai_fact_collection.rs index c5d0e917d5..ef6fd3ba79 100644 --- a/app/src/drive/items/ai_fact_collection.rs +++ b/app/src/drive/items/ai_fact_collection.rs @@ -26,7 +26,7 @@ impl WarpDriveAIFactCollection { impl WarpDriveItem for WarpDriveAIFactCollection { fn display_name(&self) -> Option { - Some("Rules".to_string()) + Some(i18n::t("drive.items.rules")) } fn metadata(&self) -> Option<&CloudObjectMetadata> { diff --git a/app/src/drive/items/env_var_collection.rs b/app/src/drive/items/env_var_collection.rs index db09b0b574..211911fc11 100644 --- a/app/src/drive/items/env_var_collection.rs +++ b/app/src/drive/items/env_var_collection.rs @@ -61,7 +61,7 @@ impl WarpDriveItem for WarpDriveEnvVarCollection { let title_to_render = if let Some(title) = title_text { title } else { - "Untitled".to_string() + i18n::t("common.untitled") }; let title = appearance .ui_builder() diff --git a/app/src/drive/items/item.rs b/app/src/drive/items/item.rs index 54295a35c2..39952f6796 100644 --- a/app/src/drive/items/item.rs +++ b/app/src/drive/items/item.rs @@ -428,20 +428,18 @@ impl<'a> WarpDriveRow<'a> { .permissions() .owner; - let mut owner_label = "From ".to_string(); - match owner { - Owner::User { user_uid } => { - match UserProfiles::as_ref(app).displayable_identifier_for_uid(user_uid) { - Some(user) => owner_label.push_str(&user), - None => owner_label.push_str("unknown user"), - } - } - Owner::Team { team_uid, .. } => owner_label.push_str( - UserWorkspaces::as_ref(app) - .team_from_uid(team_uid) - .map_or("unknown team", |team| &team.name), - ), - } + let owner_name = match owner { + Owner::User { user_uid } => UserProfiles::as_ref(app) + .displayable_identifier_for_uid(user_uid) + .unwrap_or_else(|| i18n::t("drive.items.unknown_user")), + Owner::Team { team_uid, .. } => UserWorkspaces::as_ref(app) + .team_from_uid(team_uid) + .map_or_else( + || i18n::t("drive.items.unknown_team"), + |team| team.name.clone(), + ), + }; + let owner_label = i18n::t("drive.items.from_owner").replace("{owner}", &owner_name); let background = appearance.theme().surface_1(); let text_color = appearance.theme().sub_text_color(background); @@ -644,7 +642,7 @@ impl<'a> WarpDriveRow<'a> { Span::new( self.item .display_name() - .unwrap_or_else(|| "Untitled".to_string()), + .unwrap_or_else(|| i18n::t("common.untitled")), style, ) .build() diff --git a/app/src/drive/items/mcp_server_collection.rs b/app/src/drive/items/mcp_server_collection.rs index bcd4f6a98b..2920abdff0 100644 --- a/app/src/drive/items/mcp_server_collection.rs +++ b/app/src/drive/items/mcp_server_collection.rs @@ -26,7 +26,7 @@ impl WarpDriveMCPServerCollection { impl WarpDriveItem for WarpDriveMCPServerCollection { fn display_name(&self) -> Option { - Some("MCP Servers".to_string()) + Some(i18n::t("drive.items.mcp_servers")) } fn metadata(&self) -> Option<&CloudObjectMetadata> { diff --git a/app/src/drive/items/notebook.rs b/app/src/drive/items/notebook.rs index 5fe90c6d67..50fb6c947f 100644 --- a/app/src/drive/items/notebook.rs +++ b/app/src/drive/items/notebook.rs @@ -58,7 +58,7 @@ impl WarpDriveItem for WarpDriveNotebook { fn preview(&self, appearance: &Appearance) -> Option> { let title_text = self.notebook.model().title.clone(); let title_to_render = if title_text.is_empty() { - "Untitled".to_string() + i18n::t("common.untitled") } else { title_text }; diff --git a/app/src/drive/mod.rs b/app/src/drive/mod.rs index 535f4b5f8f..1047d239ac 100644 --- a/app/src/drive/mod.rs +++ b/app/src/drive/mod.rs @@ -213,13 +213,17 @@ impl DriveSortOrder { } /// Returns the text that is used to display the sorting option in the KnowledgeIndex's sorting menu - pub fn menu_text(&self, index_variant: DriveIndexVariant) -> &str { + pub fn menu_text(&self, index_variant: DriveIndexVariant) -> String { match (self, index_variant) { - (DriveSortOrder::ByTimestamp, DriveIndexVariant::MainIndex) => "Last updated", - (DriveSortOrder::ByTimestamp, DriveIndexVariant::Trash) => "Last trashed", - (DriveSortOrder::AlphabeticalDescending, _) => "A to Z", - (DriveSortOrder::AlphabeticalAscending, _) => "Z to A", - (DriveSortOrder::ByObjectType, _) => "Type", + (DriveSortOrder::ByTimestamp, DriveIndexVariant::MainIndex) => { + i18n::t("drive.sort.last_updated") + } + (DriveSortOrder::ByTimestamp, DriveIndexVariant::Trash) => { + i18n::t("drive.sort.last_trashed") + } + (DriveSortOrder::AlphabeticalDescending, _) => i18n::t("drive.sort.a_to_z"), + (DriveSortOrder::AlphabeticalAscending, _) => i18n::t("drive.sort.z_to_a"), + (DriveSortOrder::ByObjectType, _) => i18n::t("drive.sort.type"), } } } diff --git a/app/src/drive/settings.rs b/app/src/drive/settings.rs index 2afbbd6d3e..11402d04ea 100644 --- a/app/src/drive/settings.rs +++ b/app/src/drive/settings.rs @@ -14,7 +14,7 @@ define_settings_group!(WarpDriveSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "warp_drive.sorting_choice", - description: "The sort order for items in Warp Drive.", + description_key: "settings.schema.warp_drive.sorting_choice.description", }, sharing_onboarding_block_shown: WarpDriveSharingOnboardingBlockShown { type: bool, @@ -31,7 +31,7 @@ define_settings_group!(WarpDriveSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "warp_drive.enabled", - description: "Whether Warp Drive is enabled.", + description_key: "settings.schema.warp_drive.enabled.description", }, ]); diff --git a/app/src/drive/sharing/dialog/inheritance.rs b/app/src/drive/sharing/dialog/inheritance.rs index bfdfdf8c25..d921c8dfd4 100644 --- a/app/src/drive/sharing/dialog/inheritance.rs +++ b/app/src/drive/sharing/dialog/inheritance.rs @@ -48,9 +48,10 @@ impl InheritanceState { match folder_name { Some(folder_name) => { - let prefix = style::detail_text("Inherited from ", appearance) - .build() - .finish(); + let prefix = + style::detail_text(i18n::t("drive.sharing.inherited_from_prefix"), appearance) + .build() + .finish(); let source_folder = self.source_folder; let folder_link = appearance .ui_builder() @@ -74,14 +75,17 @@ impl InheritanceState { .with_children([prefix, folder_link]) .with_cross_axis_alignment(CrossAxisAlignment::Center) .finish(), - tooltip_text: "Edit inherited permissions on the parent folder", + tooltip_text: i18n::t("drive.sharing.edit_inherited_permissions_tooltip"), } } None => InheritanceDetails { - source_label: style::detail_text("Inherited permission", appearance) - .build() - .finish(), - tooltip_text: "Cannot edit inherited permissions", + source_label: style::detail_text( + i18n::t("drive.sharing.inherited_permission"), + appearance, + ) + .build() + .finish(), + tooltip_text: i18n::t("drive.sharing.cannot_edit_inherited_permissions_tooltip"), }, } } @@ -93,5 +97,5 @@ pub struct InheritanceDetails { /// permissions directly. pub source_label: Box, /// A tooltip to show on disabled permission-editing controls. - pub tooltip_text: &'static str, + pub tooltip_text: String, } diff --git a/app/src/drive/sharing/dialog/mod.rs b/app/src/drive/sharing/dialog/mod.rs index 44b69da2c7..10147b86a0 100644 --- a/app/src/drive/sharing/dialog/mod.rs +++ b/app/src/drive/sharing/dialog/mod.rs @@ -78,8 +78,6 @@ const QR_VISUAL_SIZE: f32 = 160.; const QR_ICON_BUTTON_SIZE: f32 = 32.; const QR_EXPORT_SIZE: u32 = 1024; -const NO_ACCESS_LABEL: &str = "No access"; - #[derive(Default)] struct UiStateHandles { invite_button: MouseStateHandle, @@ -97,6 +95,22 @@ struct UiStateHandles { guest_scroll_state: ScrollStateHandle, } +fn sharing_access_level_label(access_level: SharingAccessLevel) -> String { + match access_level { + SharingAccessLevel::View => i18n::t("drive.sharing.access.can_view"), + SharingAccessLevel::Edit => i18n::t("drive.sharing.access.can_edit"), + SharingAccessLevel::Full => i18n::t("drive.sharing.access.full"), + } +} + +fn sharing_access_level_name(access_level: SharingAccessLevel) -> String { + match access_level { + SharingAccessLevel::View => i18n::t("drive.sharing.access.view_name"), + SharingAccessLevel::Edit => i18n::t("drive.sharing.access.edit_name"), + SharingAccessLevel::Full => i18n::t("drive.sharing.access.full_name"), + } +} + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] enum SharingDialogMode { #[default] @@ -263,9 +277,10 @@ impl SharingDialog { let invite_form = EmailInviteForm { email_editor: ctx.add_typed_action_view(|ctx| { + let email_placeholder = i18n::t("drive.sharing.emails_placeholder"); let mut view = WordBlockEditorView::new( ctx, - "Emails", + &email_placeholder, 13., vec![' ', ','], EMAIL_CHIP_WIDTH, @@ -954,7 +969,9 @@ impl SharingDialog { let window_id = ctx.window_id(); let object_name = self.targeted_object_name(ctx); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::default(format!("Copied link to {object_name}.")); + let toast = DismissibleToast::default( + i18n::t("drive.sharing.link_copied").replace("{object_name}", &object_name), + ); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); } @@ -985,19 +1002,23 @@ impl SharingDialog { let is_session = matches!(self.target, Some(ShareableObject::Session { .. })); self.guest_menu.update(ctx, |menu, ctx| { - let mut items = vec![MenuItemFields::new(SharingAccessLevel::View.label()) - .with_on_select_action(SharingDialogAction::SetGuestAccessLevel( - SharingAccessLevel::View, - )) - .with_disabled( - inherited_access && current_access_level >= SharingAccessLevel::View, - ) - .into_item()]; + let mut items = + vec![ + MenuItemFields::new(sharing_access_level_label(SharingAccessLevel::View)) + .with_on_select_action(SharingDialogAction::SetGuestAccessLevel( + SharingAccessLevel::View, + )) + .with_disabled( + inherited_access + && current_access_level >= SharingAccessLevel::View, + ) + .into_item(), + ]; // Only add Edit option if not an AI conversation if !is_ai_conversation { items.push( - MenuItemFields::new(SharingAccessLevel::Edit.label()) + MenuItemFields::new(sharing_access_level_label(SharingAccessLevel::Edit)) .with_on_select_action(SharingDialogAction::SetGuestAccessLevel( SharingAccessLevel::Edit, )) @@ -1014,7 +1035,7 @@ impl SharingDialog { if !is_team_guest || !is_session { items.push(MenuItem::Separator); items.push( - MenuItemFields::new("Remove") + MenuItemFields::new(i18n::t("common.remove")) .with_on_select_action(SharingDialogAction::RemoveGuest) .with_disabled(inherited_access) .into_item(), @@ -1341,12 +1362,12 @@ impl SharingDialog { // Note: Items will be updated dynamically in reset_invite_access_level_menu // based on whether the target is an AI conversation menu.add_items([ - MenuItemFields::new(SharingAccessLevel::View.label()) + MenuItemFields::new(sharing_access_level_label(SharingAccessLevel::View)) .with_on_select_action(SharingDialogAction::SetInviteAccessLevel( SharingAccessLevel::View, )) .into_item(), - MenuItemFields::new(SharingAccessLevel::Edit.label()) + MenuItemFields::new(sharing_access_level_label(SharingAccessLevel::Edit)) .with_on_select_action(SharingDialogAction::SetInviteAccessLevel( SharingAccessLevel::Edit, )) @@ -1366,16 +1387,19 @@ impl SharingDialog { let is_ai_conversation = matches!(self.target, Some(ShareableObject::AIConversation(_))); self.invite_form.access_level_menu.update(ctx, |menu, ctx| { - let mut items = vec![MenuItemFields::new(SharingAccessLevel::View.label()) - .with_on_select_action(SharingDialogAction::SetInviteAccessLevel( - SharingAccessLevel::View, - )) - .into_item()]; + let mut items = + vec![ + MenuItemFields::new(sharing_access_level_label(SharingAccessLevel::View)) + .with_on_select_action(SharingDialogAction::SetInviteAccessLevel( + SharingAccessLevel::View, + )) + .into_item(), + ]; // Only add Edit option if not an AI conversation if !is_ai_conversation { items.push( - MenuItemFields::new(SharingAccessLevel::Edit.label()) + MenuItemFields::new(sharing_access_level_label(SharingAccessLevel::Edit)) .with_on_select_action(SharingDialogAction::SetInviteAccessLevel( SharingAccessLevel::Edit, )) @@ -1417,9 +1441,9 @@ impl SharingDialog { ButtonVariant::Text, self.ui_state_handles.invite_access_level_button.clone(), ) - .with_centered_text_label( - self.invite_form.selected_access_level.label().to_string(), - ) + .with_centered_text_label(sharing_access_level_label( + self.invite_form.selected_access_level, + )) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(SharingDialogAction::ToggleInviteAccessLevelMenu); @@ -1451,7 +1475,7 @@ impl SharingDialog { ButtonVariant::Accent, self.ui_state_handles.invite_button.clone(), ) - .with_centered_text_label("Invite".into()) + .with_centered_text_label(i18n::t("settings.teams.button.invite")) .with_style(UiComponentStyles { // Adjust the height to match the email editor's padding. height: Some(style::ACL_ITEM_HEIGHT + 6.), @@ -1493,18 +1517,24 @@ impl SharingDialog { .with_cross_axis_alignment(CrossAxisAlignment::Start); if !validation_state.duplicate_guests.is_empty() { - let error_text = format!( - "Already shared with {}", - validation_state.duplicate_guests.iter().format(", ") - ); + let email_list = validation_state + .duplicate_guests + .iter() + .format(", ") + .to_string(); + let error_text = + i18n::t("drive.sharing.already_shared_with").replace("{emails}", &email_list); contents.add_child(self.render_error_message(error_text, appearance)); } if !validation_state.invalid_emails.is_empty() { - let error_text = format!( - "Invalid address: {}", - validation_state.invalid_emails.iter().format(", ") - ); + let email_list = validation_state + .invalid_emails + .iter() + .format(", ") + .to_string(); + let error_text = + i18n::t("drive.sharing.invalid_address").replace("{emails}", &email_list); contents.add_child(self.render_error_message(error_text, appearance)); } @@ -1789,7 +1819,7 @@ impl SharingDialog { fn render_access_header(&self, appearance: &Appearance) -> Box { appearance .ui_builder() - .span("Who has access") + .span(i18n::t("drive.sharing.who_has_access")) .with_style(UiComponentStyles { font_color: Some(style::label_text(appearance)), font_size: Some(style::PRIMARY_TEXT_SIZE), @@ -1818,11 +1848,9 @@ impl SharingDialog { let text = appearance .ui_builder() .wrappable_text( - format!( - "Live session started at {} on {}", - started_at.format("%l:%M%P"), - started_at.format("%m/%d"), - ), + i18n::t("drive.sharing.live_session_started") + .replace("{time}", &started_at.format("%l:%M%P").to_string()) + .replace("{date}", &started_at.format("%m/%d").to_string()), true, ) .with_style(UiComponentStyles { @@ -1853,14 +1881,16 @@ impl SharingDialog { return None; } - const PREFIX: &str = "You must have full access to manage permissions. You have "; - const SUFFIX: &str = " access."; - let access_level_start = PREFIX.chars().count(); - let access_level_end = access_level_start + access_level.name().chars().count(); + let prefix = i18n::t("drive.sharing.restricted_access_prefix"); + let access_level_name = sharing_access_level_name(access_level); + let suffix = i18n::t("drive.sharing.restricted_access_suffix"); + let label = format!("{prefix}{access_level_name}{suffix}"); + let access_level_start = prefix.chars().count(); + let access_level_end = access_level_start + access_level_name.chars().count(); let text = appearance .ui_builder() - .wrappable_text(format!("{PREFIX}{}{SUFFIX}", access_level.name()), true) + .wrappable_text(label, true) .with_style(UiComponentStyles { font_color: Some(style::label_text(appearance)), ..Default::default() @@ -1884,15 +1914,15 @@ impl SharingDialog { let owner = self.owner(app)?; let tooltip_text = match owner { - Subject::Team(_) => "Team objects automatically grant full permissions to team members", - _ => "Owners always have full permissions on their objects", + Subject::Team(_) => i18n::t("drive.sharing.team_owner_full_permissions_tooltip"), + _ => i18n::t("drive.sharing.owner_full_permissions_tooltip"), }; let owner_access_label = render_with_detail_tooltip( tooltip_text, self.ui_state_handles.owner_tooltip.clone(), appearance .ui_builder() - .span(SharingAccessLevel::Full.label()) + .span(sharing_access_level_label(SharingAccessLevel::Full)) .with_style(UiComponentStyles { font_color: Some( appearance @@ -1970,8 +2000,8 @@ impl SharingDialog { ); let menu_button_label = match self.link_sharing_state.access_level { - Some(access_level) => access_level.label(), - None => NO_ACCESS_LABEL, + Some(access_level) => sharing_access_level_label(access_level), + None => i18n::t("drive.sharing.access.no_access"), }; let mut menu_button = appearance .ui_builder() @@ -1979,7 +2009,7 @@ impl SharingDialog { ButtonVariant::Text, self.ui_state_handles.link_sharing_menu_button.clone(), ) - .with_centered_text_label(menu_button_label.to_string()) + .with_centered_text_label(menu_button_label) .with_style(UiComponentStyles { padding: Some(Coords::default()), ..Default::default() @@ -2027,17 +2057,17 @@ impl SharingDialog { let is_ai_conversation = matches!(self.target, Some(ShareableObject::AIConversation(_))); let mut items = vec![ - MenuItemFields::new("Only people invited") + MenuItemFields::new(i18n::t("drive.sharing.only_people_invited")) .with_on_select_action(SharingDialogAction::SetLinkPermissions(None)) .with_icon(Icon::Lock) .with_disabled(inherited_access) .into_item(), MenuItem::Separator, - MenuItemFields::new("Anyone with the link") + MenuItemFields::new(i18n::t("drive.sharing.anyone_with_link")) .with_no_interaction_on_hover() .with_icon(Icon::Globe) .into_item(), - MenuItemFields::new(SharingAccessLevel::View.label()) + MenuItemFields::new(sharing_access_level_label(SharingAccessLevel::View)) .with_on_select_action(SharingDialogAction::SetLinkPermissions(Some( SharingAccessLevel::View, ))) @@ -2050,7 +2080,7 @@ impl SharingDialog { // Only add Edit option if not an AI conversation if !is_ai_conversation { items.push( - MenuItemFields::new(SharingAccessLevel::Edit.label()) + MenuItemFields::new(sharing_access_level_label(SharingAccessLevel::Edit)) .with_on_select_action(SharingDialogAction::SetLinkPermissions(Some( SharingAccessLevel::Edit, ))) @@ -2149,8 +2179,8 @@ impl SharingDialog { let menu_button = { let label = match self.team_sharing_state.access_level { - Some(access_level) => access_level.label(), - None => NO_ACCESS_LABEL, + Some(access_level) => sharing_access_level_label(access_level), + None => i18n::t("drive.sharing.access.no_access"), }; let button = appearance .ui_builder() @@ -2158,7 +2188,7 @@ impl SharingDialog { ButtonVariant::Text, self.ui_state_handles.team_sharing_menu_button.clone(), ) - .with_centered_text_label(label.to_string()) + .with_centered_text_label(label) .with_style(UiComponentStyles { padding: Some(Coords::default()), ..Default::default() @@ -2208,17 +2238,17 @@ impl SharingDialog { let inherited_access = self.team_sharing_state.inheritance.is_some(); let current_access_level = self.team_sharing_state.access_level; let items = [ - MenuItemFields::new("Only invited teammates") + MenuItemFields::new(i18n::t("drive.sharing.only_invited_teammates")) .with_on_select_action(SharingDialogAction::SetTeamPermissions(None)) .with_icon(Icon::Lock) .with_disabled(inherited_access) .into_item(), MenuItem::Separator, - MenuItemFields::new("Teammates with the link") + MenuItemFields::new(i18n::t("drive.sharing.teammates_with_link")) .with_no_interaction_on_hover() .with_icon(Icon::Users) .into_item(), - MenuItemFields::new(SharingAccessLevel::View.label()) + MenuItemFields::new(sharing_access_level_label(SharingAccessLevel::View)) .with_on_select_action(SharingDialogAction::SetTeamPermissions(Some( SharingAccessLevel::View, ))) @@ -2226,7 +2256,7 @@ impl SharingDialog { inherited_access && current_access_level >= Some(SharingAccessLevel::View), ) .into_item(), - MenuItemFields::new(SharingAccessLevel::Edit.label()) + MenuItemFields::new(sharing_access_level_label(SharingAccessLevel::Edit)) .with_on_select_action(SharingDialogAction::SetTeamPermissions(Some( SharingAccessLevel::Edit, ))) @@ -2274,7 +2304,7 @@ impl SharingDialog { let mut access_level_button = appearance .ui_builder() .button(ButtonVariant::Text, guest.menu_button_handle.clone()) - .with_centered_text_label(guest.current_access_level.label().to_string()) + .with_centered_text_label(sharing_access_level_label(guest.current_access_level)) .with_style(UiComponentStyles { padding: Some(Coords::default()), ..Default::default() @@ -2381,7 +2411,9 @@ impl SharingDialog { .with_padding_right(10.) .finish(); - let name_text = subject.name(app).unwrap_or(Cow::Borrowed("Unknown")); + let name_text = subject + .name(app) + .unwrap_or_else(|| Cow::Owned(i18n::t("drive.sharing.unknown_subject"))); let name_label = appearance .ui_builder() .span(name_text) @@ -2447,7 +2479,7 @@ impl SharingDialog { fn download_qr_code(&self, ctx: &mut ViewContext) { let Some(url) = self.target_link(ctx) else { self.show_ephemeral_toast( - DismissibleToast::error("Unable to download QR code.".to_string()), + DismissibleToast::error(i18n::t("drive.sharing.qr_download_failed")), ctx, ); return; @@ -2457,7 +2489,7 @@ impl SharingDialog { Ok(png) => png, Err(_) => { self.show_ephemeral_toast( - DismissibleToast::error("Unable to download QR code.".to_string()), + DismissibleToast::error(i18n::t("drive.sharing.qr_download_failed")), ctx, ); return; @@ -2485,11 +2517,11 @@ impl SharingDialog { fn handle_qr_write_result(&self, result: std::io::Result<()>, ctx: &mut ViewContext) { match result { Ok(()) => self.show_ephemeral_toast( - DismissibleToast::success("QR code downloaded.".to_string()), + DismissibleToast::success(i18n::t("drive.sharing.qr_downloaded")), ctx, ), Err(_) => self.show_ephemeral_toast( - DismissibleToast::error("Unable to download QR code.".to_string()), + DismissibleToast::error(i18n::t("drive.sharing.qr_download_failed")), ctx, ), } @@ -2499,7 +2531,7 @@ impl SharingDialog { &self, icon: Icon, action: SharingDialogAction, - tooltip: &'static str, + tooltip: String, mouse_state: MouseStateHandle, appearance: &Appearance, ) -> Box { @@ -2604,7 +2636,7 @@ impl SharingDialog { .finish(); let title = appearance .ui_builder() - .span("Share session QR code") + .span(i18n::t("drive.sharing.share_session_qr_code")) .with_style(UiComponentStyles { font_color: Some(foreground), font_size: Some(style::HEADER_TEXT_SIZE), @@ -2673,14 +2705,14 @@ impl SharingDialog { self.render_footer_icon_button( Icon::Copy, SharingDialogAction::CopyLink, - "Copy link", + i18n::t("common.copy_link"), self.ui_state_handles.qr_copy_button.clone(), appearance, ), Container::new(self.render_footer_icon_button( Icon::Download, SharingDialogAction::DownloadQrCode, - "Download QR code", + i18n::t("drive.sharing.download_qr_code"), self.ui_state_handles.qr_download_button.clone(), appearance, )) @@ -2703,7 +2735,7 @@ impl SharingDialog { .unwrap_or_else(|| { appearance .ui_builder() - .paragraph("Unable to create QR code for this session link.") + .paragraph(i18n::t("drive.sharing.qr_create_failed")) .with_style(UiComponentStyles { font_color: Some(style::acl_secondary_text_color(appearance)), ..Default::default() @@ -2757,7 +2789,7 @@ impl SharingDialog { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Copy link", + i18n::t("common.copy_link"), Icon::Link.to_warpui_icon(copy_button_foreground), MainAxisSize::Min, MainAxisAlignment::SpaceBetween, @@ -2788,7 +2820,7 @@ impl SharingDialog { self.render_footer_icon_button( Icon::QrCode, SharingDialogAction::ShowQrCode, - "Show QR code", + i18n::t("drive.sharing.show_qr_code"), self.ui_state_handles.qr_code_button.clone(), appearance, ) diff --git a/app/src/drive/sharing/mod.rs b/app/src/drive/sharing/mod.rs index 6c72f397ba..eea3fb487f 100644 --- a/app/src/drive/sharing/mod.rs +++ b/app/src/drive/sharing/mod.rs @@ -110,7 +110,9 @@ impl SubjectExt for Subject { Subject::User(kind) => kind.name(app), Subject::PendingUser { email } => email.clone().map(Cow::from), Subject::Team(kind) => kind.display_name(app).map(Cow::from), - Subject::AnyoneWithLink(_) => Some(Cow::from("Anyone with the link")), + Subject::AnyoneWithLink(_) => { + Some(Cow::from(i18n::t("drive.sharing.anyone_with_link"))) + } } } diff --git a/app/src/drive/workflows/ai_assist.rs b/app/src/drive/workflows/ai_assist.rs index 3c4c43ef24..2988c92d23 100644 --- a/app/src/drive/workflows/ai_assist.rs +++ b/app/src/drive/workflows/ai_assist.rs @@ -64,14 +64,11 @@ pub enum GeneratedCommandMetadataError { impl GeneratedCommandMetadataError { pub fn user_facing_message(&self) -> String { match self { - Self::BadCommand => { - "Failed to generate metadata. Please try again with a different command." - } - Self::AiProviderError => "Something went wrong. Please try again.", - Self::RateLimited => "Looks like you're out of AI credits. Please try again later.", - Self::Other => "Something went wrong. Please try again.", + Self::BadCommand => i18n::t("drive.workflows.ai_assist.bad_command"), + Self::AiProviderError => i18n::t("common.something_went_wrong_try_again"), + Self::RateLimited => i18n::t("drive.workflows.ai_assist.rate_limited"), + Self::Other => i18n::t("common.something_went_wrong_try_again"), } - .to_string() } } @@ -108,7 +105,7 @@ impl WorkflowModal { name: parameter.name, description: Some(parameter.description), default_value: Some(parameter.default_value), - arg_type: Default::default() + arg_type: Default::default(), }) .collect_vec(); @@ -125,10 +122,7 @@ impl WorkflowModal { environment_variables: None, }; - send_telemetry_from_ctx!( - TelemetryEvent::AutoGenerateMetadataSuccess, - ctx - ); + send_telemetry_from_ctx!(TelemetryEvent::AutoGenerateMetadataSuccess, ctx); modal.populate_missing_field_with_suggestion(workflow, ctx); ctx.notify(); @@ -141,18 +135,27 @@ impl WorkflowModal { if let Some(team) = UserWorkspaces::as_ref(ctx).current_team() { let current_user_email = auth_state.user_email().unwrap_or_default(); - let has_admin_permissions = team.has_admin_permissions(¤t_user_email); + let has_admin_permissions = + team.has_admin_permissions(¤t_user_email); if team.billing_metadata.can_upgrade_to_higher_tier_plan() { if has_admin_permissions { - ctx.emit(WorkflowModalEvent::AiAssistUpgradeError(Some(team.uid), current_user_id)); + ctx.emit(WorkflowModalEvent::AiAssistUpgradeError( + Some(team.uid), + current_user_id, + )); } else { - ctx.emit(WorkflowModalEvent::AiAssistError("Looks like you're out of AI credits. Contact a team admin to upgrade for more credits.".to_string())); + ctx.emit(WorkflowModalEvent::AiAssistError(i18n::t( + "drive.workflows.ai_assist.rate_limited_contact_admin", + ))); } } else { ctx.emit(WorkflowModalEvent::AiAssistError(message.clone())); } } else { - ctx.emit(WorkflowModalEvent::AiAssistUpgradeError(None, current_user_id)); + ctx.emit(WorkflowModalEvent::AiAssistUpgradeError( + None, + current_user_id, + )); } } else { ctx.emit(WorkflowModalEvent::AiAssistError(message.clone())); @@ -173,7 +176,7 @@ impl WorkflowModal { AIRequestUsageModel::handle(ctx).update(ctx, |request_usage_model, ctx| { request_usage_model.refresh_request_usage_async(ctx); }); - } + }, ); self.ai_metadata_assist_state = AiAssistState::RequestInFlight; diff --git a/app/src/drive/workflows/enum_creation_dialog.rs b/app/src/drive/workflows/enum_creation_dialog.rs index 817e19de09..4307f8cd6e 100644 --- a/app/src/drive/workflows/enum_creation_dialog.rs +++ b/app/src/drive/workflows/enum_creation_dialog.rs @@ -49,17 +49,6 @@ const SECTION_FONT_SIZE: f32 = 16.; const SPAN_FONT_SIZE: f32 = 16.; const VARIANT_FONT_SIZE: f32 = 13.; -const CANCEL_BUTTON_LABEL: &str = "Close"; -const NEW_ENUM_SPAN: &str = "New enum"; -const EXISTING_ENUM_SPAN: &str = "Edit enum"; -const NAME_PLACEHOLDER_TEXT: &str = "Name"; -const CREATE_BUTTON_LABEL: &str = "Create"; -const SAVE_BUTTON_LABEL: &str = "Save"; -const VARIANT_PLACEHOLDER_TEXT: &str = "Variant"; -const STATIC_LABEL_TEXT: &str = "Variants"; -const DYNAMIC_PLACEHOLDER_TEXT: &str = - "# Enter a shell command that generates variants, delimited by newlines.\n\ngit branch -a"; - #[derive(Debug, Clone)] pub enum EnumCreationDialogAction { Close, @@ -149,6 +138,15 @@ enum EnumType { Dynamic, } +impl EnumType { + fn label(self) -> String { + match self { + EnumType::Static => i18n::t("workflows.enum.static"), + EnumType::Dynamic => i18n::t("workflows.enum.dynamic"), + } + } +} + impl EnumCreationDialog { pub fn new(ctx: &mut ViewContext) -> Self { let name_editor = { @@ -162,7 +160,7 @@ impl EnumCreationDialog { }; let mut editor = EditorView::single_line(options, ctx); - editor.set_placeholder_text(NAME_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text(&i18n::t("workflows.enum.name_placeholder"), ctx); editor }) }; @@ -195,7 +193,7 @@ impl EnumCreationDialog { }; let mut editor = EditorView::new(options, ctx); - editor.set_placeholder_text(DYNAMIC_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text(&i18n::t("workflows.enum.dynamic_placeholder"), ctx); editor.set_autogrow(true); editor }) @@ -548,7 +546,7 @@ impl EnumCreationDialog { }, ctx, ); - editor.set_placeholder_text(VARIANT_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text(&i18n::t("workflows.enum.variant_placeholder"), ctx); editor }); @@ -579,7 +577,7 @@ impl EnumCreationDialog { appearance: &Appearance, button_mouse_state: MouseStateHandle, action: EnumCreationDialogAction, - label_text: &str, + label_text: String, is_save: bool, is_disabled: bool, ) -> Box { @@ -593,7 +591,7 @@ impl EnumCreationDialog { }, button_mouse_state, ) - .with_centered_text_label(label_text.to_owned()) + .with_centered_text_label(label_text) .with_style(UiComponentStyles { font_size: Some(BUTTON_FONT_SIZE), font_weight: Some(warpui::fonts::Weight::Normal), @@ -627,8 +625,8 @@ impl EnumCreationDialog { fn render_dialog_header(&self, appearance: &Appearance) -> Box { let text = match self.sync_id { - Some(_) => EXISTING_ENUM_SPAN, - None => NEW_ENUM_SPAN, + Some(_) => i18n::t("workflows.enum.edit_title"), + None => i18n::t("workflows.enum.new_title"), }; appearance @@ -651,10 +649,7 @@ impl EnumCreationDialog { self.enum_type_handles.enum_type_mouse_states.clone(), self.enum_type_options .iter() - .map(|arg_type| { - let label: &'static str = arg_type.into(); - ToggleMenuItem::new(label) - }) + .map(|arg_type| ToggleMenuItem::new(arg_type.label())) .collect(), self.enum_type_handles.enum_type_state_handle.clone(), Some(0), @@ -820,7 +815,7 @@ impl EnumCreationDialog { 1., appearance .ui_builder() - .span(STATIC_LABEL_TEXT.to_string()) + .span(i18n::t("workflows.enum.variants")) .with_style(UiComponentStyles { font_size: Some(SECTION_FONT_SIZE), ..Default::default() @@ -861,8 +856,8 @@ impl EnumCreationDialog { fn render_footer_buttons(&self, appearance: &Appearance, app: &AppContext) -> Box { let disable_save = self.should_disable_save(app); let save_button_label = match self.sync_id { - None => CREATE_BUTTON_LABEL, - Some(_) => SAVE_BUTTON_LABEL, + None => i18n::t("common.create"), + Some(_) => i18n::t("common.save"), }; Flex::row() @@ -876,7 +871,7 @@ impl EnumCreationDialog { .cancel_button_mouse_state_handle .clone(), EnumCreationDialogAction::Close, - CANCEL_BUTTON_LABEL, + i18n::t("common.close"), false, false, ), diff --git a/app/src/drive/workflows/modal.rs b/app/src/drive/workflows/modal.rs index 5fc1bfed11..44ea2827f8 100644 --- a/app/src/drive/workflows/modal.rs +++ b/app/src/drive/workflows/modal.rs @@ -83,20 +83,7 @@ const DIALOG_WIDTH: f32 = 460.; const AI_ASSIST_BUTTON_SIZE: f32 = 96.; const SCROLLBAR_WIDTH: ScrollbarWidth = ScrollbarWidth::Auto; -const TITLE_PLACEHOLDER_TEXT: &str = "Untitled workflow"; -const DESCRIPTION_PLACEHOLDER_TEXT: &str = "Add a description"; -const COMMAND_EDITOR_PLACEHOLDER_TEXT: &str = - "echo \"Hello {{your_name}}\" # insert arguments with curly braces\n# enter a single-line command or an entire shell script"; -const ARGUMENT_BUTTON_TEXT: &str = "New argument"; -const ARGUMENT_DESCRIPTION_PLACEHOLDER_TEXT: &str = "Description"; -const ARGUMENT_DEFAULT_VALUE_PLACEHOLDER_TEXT: &str = "Default value (optional)"; -const SAVE_BUTTON_TEXT: &str = "Save workflow"; -const AI_ASSIST_BUTTON_TEXT: &str = "Autofill"; -const AI_ASSIST_LOADING_TEXT: &str = "Loading"; const DEFAULT_ARGUMENT_PREFIX: &str = "argument"; -const UNSAVED_CHANGES_TEXT: &str = "You have unsaved changes."; -const KEEP_EDITING_TEXT: &str = "Keep editing"; -const DISCARD_CHANGES_TEXT: &str = "Discard changes"; #[derive(Default)] struct MouseStateHandles { @@ -212,12 +199,15 @@ impl WorkflowModal { let appearance = Appearance::as_ref(ctx); let header_font_size = appearance.header_font_size(); let ui_font_family = appearance.ui_font_family(); + let title_placeholder = i18n::t("workflows.modal.title_placeholder"); + let description_placeholder = i18n::t("workflows.editor.description_placeholder"); + let command_placeholder = i18n::t("workflows.editor.command_placeholder"); let title_editor: ViewHandle = Self::create_editor_handle( ctx, Some(header_font_size), Some(ui_font_family), - Some(TITLE_PLACEHOLDER_TEXT), + Some(title_placeholder), false, /* vim_keybindings */ true, /* single_line */ ); @@ -230,7 +220,7 @@ impl WorkflowModal { ctx, Some(DESCRIPTION_FONT_SIZE), Some(ui_font_family), - Some(DESCRIPTION_PLACEHOLDER_TEXT), + Some(description_placeholder), false, /* vim_keybindings */ false, /* single_line */ ); @@ -243,7 +233,7 @@ impl WorkflowModal { ctx, Some(CONTENT_EDITOR_FONT_SIZE), None, - Some(COMMAND_EDITOR_PLACEHOLDER_TEXT), + Some(command_placeholder), true, /* vim_keybindings */ false, /* single_line */ ); @@ -357,7 +347,7 @@ impl WorkflowModal { ctx: &mut ViewContext, font_size_override: Option, font_family_override: Option, - placeholder_text: Option<&str>, + placeholder_text: Option, vim_keybindings: bool, single_line: bool, ) -> ViewHandle { @@ -393,7 +383,7 @@ impl WorkflowModal { ); if let Some(text) = placeholder_text { - editor.set_placeholder_text(text, ctx); + editor.set_placeholder_text(&text, ctx); } editor @@ -675,7 +665,7 @@ impl WorkflowModal { // Add "Copy workflow text" to menu menu_items.push( - MenuItemFields::new("Copy workflow text") + MenuItemFields::new(i18n::t("drive.copy_workflow_text")) .with_on_select_action(WorkflowModalAction::CopyObjectToClipboard) .with_icon(Icon::CopyMenuItem) .into_item(), @@ -684,7 +674,7 @@ impl WorkflowModal { // Add "Trash" to menu if self.is_online(app) { menu_items.push( - MenuItemFields::new("Trash") + MenuItemFields::new(i18n::t("common.trash")) .with_on_select_action(WorkflowModalAction::TrashObject) .with_icon(Icon::Trash) .into_item(), @@ -1196,7 +1186,7 @@ impl WorkflowModal { ctx, Some(ARGUMENT_EDITOR_FONT_SIZE), Some(ui_font_family), - Some(ARGUMENT_DESCRIPTION_PLACEHOLDER_TEXT), + Some(i18n::t("workflows.modal.argument_description_placeholder")), false, /* vim_keybindings */ false, ); @@ -1212,7 +1202,9 @@ impl WorkflowModal { ctx, Some(ARGUMENT_EDITOR_FONT_SIZE), Some(ui_font_family), - Some(ARGUMENT_DEFAULT_VALUE_PLACEHOLDER_TEXT), + Some(i18n::t( + "workflows.modal.argument_default_value_placeholder", + )), false, /* vim_keybindings */ false, ); @@ -1639,7 +1631,7 @@ impl WorkflowModal { padding: Some(Coords::uniform(BUTTON_PADDING)), ..Default::default() }) - .with_text_label(ARGUMENT_BUTTON_TEXT.into()); + .with_text_label(i18n::t("workflows.modal.new_argument")); if self.is_new_argument_button_disabled() { new_argument_button = new_argument_button.disabled(); @@ -1655,7 +1647,7 @@ impl WorkflowModal { Some(primary_hovered_and_clicked_styles), Some(primary_disabled_styles), ) - .with_text_label(SAVE_BUTTON_TEXT.into()); + .with_text_label(i18n::t("workflows.modal.save_workflow")); if self.is_save_workflow_button_disabled() { save_button = save_button.disabled(); @@ -1685,15 +1677,19 @@ impl WorkflowModal { .with_main_axis_alignment(MainAxisAlignment::SpaceBetween); let label_and_icon = match self.ai_metadata_assist_state { - AiAssistState::PreRequest => Some((AI_ASSIST_BUTTON_TEXT, Icon::AiAssistant)), - AiAssistState::RequestInFlight => Some((AI_ASSIST_LOADING_TEXT, Icon::Refresh)), + AiAssistState::PreRequest => { + Some((i18n::t("workflows.editor.autofill"), Icon::AiAssistant)) + } + AiAssistState::RequestInFlight => { + Some((i18n::t("workflows.editor.loading"), Icon::Refresh)) + } AiAssistState::Generated => None, }; if let Some((label, icon)) = label_and_icon { let text_and_icon = TextAndIcon::new( TextAndIconAlignment::TextFirst, - label.to_string(), + label, icon.to_warpui_icon(appearance.theme().active_ui_text_color()), MainAxisSize::Min, MainAxisAlignment::Center, @@ -1724,7 +1720,7 @@ impl WorkflowModal { .finish(); let button_with_tool_tip = appearance.ui_builder().tool_tip_on_element( - "Generate a title, descriptions, or parameters with Warp AI".to_string(), + i18n::t("workflows.editor.autofill_tooltip"), self.button_mouse_states.ai_assist_tool_tip.clone(), rendered_button, ParentAnchor::BottomMiddle, @@ -1767,7 +1763,7 @@ impl WorkflowModal { padding: Some(Coords::uniform(BUTTON_PADDING)), ..Default::default() }) - .with_text_label(KEEP_EDITING_TEXT.into()) + .with_text_label(i18n::t("workflows.editor.keep_editing")) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { @@ -1787,7 +1783,7 @@ impl WorkflowModal { padding: Some(Coords::uniform(BUTTON_PADDING)), ..Default::default() }) - .with_text_label(DISCARD_CHANGES_TEXT.into()) + .with_text_label(i18n::t("workflows.editor.discard_changes")) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| ctx.dispatch_typed_action(WorkflowModalAction::ForceClose)) @@ -1795,7 +1791,7 @@ impl WorkflowModal { Container::new( Dialog::new( - UNSAVED_CHANGES_TEXT.to_string(), + i18n::t("workflows.editor.unsaved_changes"), None, dialog_styles(appearance), ) diff --git a/app/src/drive/workflows/workflow_arg_selector.rs b/app/src/drive/workflows/workflow_arg_selector.rs index e3d7464b68..8e5bd57cd1 100644 --- a/app/src/drive/workflows/workflow_arg_selector.rs +++ b/app/src/drive/workflows/workflow_arg_selector.rs @@ -34,7 +34,6 @@ use crate::ui_components::buttons::{highlight, icon_button}; use crate::ui_components::icons::{self, Icon}; use crate::workflows::workflow::ArgumentType; -const ARGUMENT_DEFAULT_VALUE_PLACEHOLDER_TEXT: &str = "Default value (optional)"; const ARGUMENT_EDITOR_FONT_SIZE: f32 = 14.; const DROPDOWN_PADDING: f32 = 8.; const DROPDOWN_BORDER_RADIUS: f32 = 6.; @@ -494,7 +493,7 @@ impl WorkflowArgSelector { let should_show_placeholder = self.text_editor.as_ref(app).is_empty(app); let text_label = match should_show_placeholder { - true => ARGUMENT_DEFAULT_VALUE_PLACEHOLDER_TEXT.to_string(), + true => i18n::t("drive.workflows.argument_default_value_placeholder"), false => self.text_editor.as_ref(app).buffer_text(app), }; @@ -796,7 +795,7 @@ impl WorkflowArgSelector { let mut menu = Hoverable::new(self.enum_menu_mouse_state.clone(), |state| { let button = Text::new_inline( - "New".to_string(), + i18n::t("common.new"), appearance.ui_font_family(), ARGUMENT_EDITOR_FONT_SIZE, ) diff --git a/app/src/editor/accept_autosuggestion_keybinding_view.rs b/app/src/editor/accept_autosuggestion_keybinding_view.rs index f88bbdfc44..d6b9f0a2b2 100644 --- a/app/src/editor/accept_autosuggestion_keybinding_view.rs +++ b/app/src/editor/accept_autosuggestion_keybinding_view.rs @@ -93,7 +93,7 @@ impl AcceptAutosuggestionKeybinding { }, ) .into_item(), - MenuItemFields::new("Custom...") + MenuItemFields::new(i18n::t("common.custom_ellipsis")) .with_on_select_action( AcceptAutosuggestionKeybindingAction::OpenSettingsForCustomKeybinding, ) @@ -338,7 +338,7 @@ impl View for AcceptAutosuggestionKeybinding { if !is_menu_open && state.is_hovered() { let tool_tip = appearance .ui_builder() - .autosuggestion_tool_tip("Change keybinding".into()) + .autosuggestion_tool_tip(i18n::t("editor.autosuggestion.change_keybinding")) .build() .finish(); stack.add_positioned_overlay_child( diff --git a/app/src/editor/autosuggestion_ignore_view.rs b/app/src/editor/autosuggestion_ignore_view.rs index cc6119bb31..0c28fc6ac5 100644 --- a/app/src/editor/autosuggestion_ignore_view.rs +++ b/app/src/editor/autosuggestion_ignore_view.rs @@ -138,7 +138,9 @@ impl View for AutosuggestionIgnore { if state.is_hovered() { let tool_tip = appearance .ui_builder() - .autosuggestion_tool_tip("Ignore this suggestion".into()) + .autosuggestion_tool_tip(i18n::t( + "editor.autosuggestion.ignore_this_suggestion", + )) .build() .finish(); stack.add_positioned_overlay_child( diff --git a/app/src/editor/view/element.rs b/app/src/editor/view/element.rs index 41aeabe02f..cd1bd08b5d 100644 --- a/app/src/editor/view/element.rs +++ b/app/src/editor/view/element.rs @@ -1508,7 +1508,7 @@ impl EditorElement { .with_margin_right(self.view_snapshot.em_width) .finish(), Text::new( - "Cycle suggestions", + i18n::t("editor.autosuggestion.cycle_suggestions"), self.view_snapshot.font_family, font_size, ) diff --git a/app/src/editor/view/mod.rs b/app/src/editor/view/mod.rs index 81c8cb1dbb..0cb0b115be 100644 --- a/app/src/editor/view/mod.rs +++ b/app/src/editor/view/mod.rs @@ -136,8 +136,6 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const DEFAULT_TAB_SIZE: usize = 4; pub const ACCEPT_AUTOSUGGESTION_KEYBINDING_NAME: &str = "editor_view:insert_autosuggestion"; -pub const VOICE_LIMIT_HIT_TOAST_TEXT: &str = "You have hit the limit for Voice requests. Your limit will be refreshed as a part of your next cycle."; -pub const VOICE_ERROR_TOAST_TEXT: &str = "An error occurred while processing your voice input."; pub const MAX_IMAGES_PER_CONVERSATION: usize = 200; @@ -1683,28 +1681,26 @@ impl ImageContextOptions { } = self { if *unsupported_model { - return "Image attachment isn't supported by this model".into(); + return i18n::t("editor.image.attachment_unsupported_model"); } if *is_processing_attached_images { - return "Loading...".into(); + return i18n::t("common.loading"); } if *num_images_attached >= MAX_IMAGE_COUNT_FOR_QUERY { - return format!( - "Image attachment is disabled — limit is {MAX_IMAGE_COUNT_FOR_QUERY} per query" - ); + return i18n::t("editor.image.attachment_disabled_query_limit") + .replace("{count}", &MAX_IMAGE_COUNT_FOR_QUERY.to_string()); } let total_images = *num_images_attached + *num_images_in_conversation; if total_images >= MAX_IMAGES_PER_CONVERSATION { - return format!( - "Image attachment is disabled — limit is {MAX_IMAGES_PER_CONVERSATION} per conversation" - ); + return i18n::t("editor.image.attachment_disabled_conversation_limit") + .replace("{count}", &MAX_IMAGES_PER_CONVERSATION.to_string()); } } - "Attach images".into() + i18n::t("editor.image.attach_images") } pub fn num_images_attached(&self) -> usize { @@ -4968,10 +4964,9 @@ impl EditorView { if !image_paths.is_empty() && is_unsupported_model { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "The selected model does not support images as context." - .to_string(), - ), + DismissibleToast::error(i18n::t( + "editor.model_no_image_context", + )), window_id, ctx, ); @@ -4994,17 +4989,20 @@ impl EditorView { let limit_reason = if num_excess_images == num_excess_images_by_query_limit { - format!("limit is {MAX_IMAGE_COUNT_FOR_QUERY} per query") + i18n::t("editor.image.limit_per_query") + .replace("{count}", &MAX_IMAGE_COUNT_FOR_QUERY.to_string()) } else { - format!("limit is {MAX_IMAGES_PER_CONVERSATION} per conversation") + i18n::t("editor.image.limit_per_conversation") + .replace("{count}", &MAX_IMAGES_PER_CONVERSATION.to_string()) }; let message = if num_excess_images == 1 { - format!("1 image wasn't attached - {limit_reason}.") + i18n::t("editor.image.not_attached.limit_one") + .replace("{limit_reason}", &limit_reason) } else { - format!( - "{num_excess_images} images weren't attached - {limit_reason}." - ) + i18n::t("editor.image.not_attached.limit_many") + .replace("{count}", &num_excess_images.to_string()) + .replace("{limit_reason}", &limit_reason) }; ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { @@ -5074,9 +5072,7 @@ impl EditorView { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "The selected model does not support images as context".to_owned(), - ), + DismissibleToast::error(i18n::t("editor.model_no_image_context_no_period")), window_id, ctx, ); @@ -5133,11 +5129,12 @@ impl EditorView { move |this, (images, num_unsupported_images, num_read_errors), ctx| { if num_unsupported_images > 0 { let message = if num_unsupported_images == 1 && num_images_user_attached == 1 { - "Image cannot be attached - supported types are PNG, JPG, GIF, WEBP.".into() + i18n::t("editor.image.unsupported_single_only") } else if num_unsupported_images == 1 { - "1 image wasn't attached - supported types are PNG, JPG, GIF, WEBP.".into() + i18n::t("editor.image.unsupported_one") } else { - format!("{num_unsupported_images} images weren't attached - supported types are PNG, JPG, GIF, WEBP.") + i18n::t("editor.image.unsupported_many") + .replace("{count}", &num_unsupported_images.to_string()) }; ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { @@ -5151,11 +5148,12 @@ impl EditorView { if num_read_errors > 0 { let message = if num_read_errors == 1 && num_images_user_attached == 1 { - "Image cannot be attached - failed to read file.".into() + i18n::t("editor.image.read_failed_single_only") } else if num_read_errors == 1 { - "1 image wasn't attached - failed to read file.".into() + i18n::t("editor.image.read_failed_one") } else { - format!("{num_read_errors} images weren't attached - failed to read files.") + i18n::t("editor.image.read_failed_many") + .replace("{count}", &num_read_errors.to_string()) }; ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { @@ -5168,7 +5166,11 @@ impl EditorView { } if !images.is_empty() { - this.process_and_attach_images_as_ai_context(num_images_user_attached, images, ctx); + this.process_and_attach_images_as_ai_context( + num_images_user_attached, + images, + ctx, + ); } }, ); @@ -5189,9 +5191,7 @@ impl EditorView { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "The selected model does not support images as context".to_owned(), - ), + DismissibleToast::error(i18n::t("editor.model_no_image_context_no_period")), window_id, ctx, ); @@ -5259,13 +5259,12 @@ impl EditorView { if num_oversized_images > 0 { let message = if num_oversized_images == 1 && num_images_user_attached == 1 { - "Image cannot be attached - file is too large.".into() + i18n::t("editor.image.too_large_single_only") } else if num_oversized_images == 1 { - "1 image wasn't attached — file is too large.".into() + i18n::t("editor.image.too_large_one") } else { - format!( - "{num_oversized_images} images weren't attached — files are too large." - ) + i18n::t("editor.image.too_large_many") + .replace("{count}", &num_oversized_images.to_string()) }; ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { @@ -5279,13 +5278,12 @@ impl EditorView { if num_unprocessed_images > 0 { let message = if num_unprocessed_images == 1 && num_images_user_attached == 1 { - "Image cannot be attached - error processing.".into() + i18n::t("editor.image.processing_error_single_only") } else if num_unprocessed_images == 1 { - "1 image wasn't attached - error processing.".into() + i18n::t("editor.image.processing_error_one") } else { - format!( - "{num_unprocessed_images} images weren't attached - error processing." - ) + i18n::t("editor.image.processing_error_many") + .replace("{count}", &num_unprocessed_images.to_string()) }; ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { @@ -8047,19 +8045,18 @@ impl EditorView { padding: Some(Coords::uniform(icon_size / 10.)), ..Default::default() }); - let button = - button - .with_tooltip_position(ButtonTooltipPosition::Above) - .with_tooltip(self.render_menu_button_tooltip( - "Search files and directories".to_string(), - appearance, - )) - .build() - .with_cursor(Cursor::PointingHand) - .on_click(move |ctx, _, _| { - ctx.dispatch_typed_action(EditorAction::SetAIContextMenuOpen(true)); - }) - .finish(); + let button = button + .with_tooltip_position(ButtonTooltipPosition::Above) + .with_tooltip(self.render_menu_button_tooltip( + i18n::t("editor.context_menu.search_files_and_directories"), + appearance, + )) + .build() + .with_cursor(Cursor::PointingHand) + .on_click(move |ctx, _, _| { + ctx.dispatch_typed_action(EditorAction::SetAIContextMenuOpen(true)); + }) + .finish(); Some(button) } @@ -8398,7 +8395,7 @@ impl TypedActionView for EditorView { | EditorAction::Backspace => ActionAccessibilityContent::Empty, EditorAction::Paste => { ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( - format!("Pasting: {}", self.clipboard_content(ctx)), + i18n::t("editor.a11y.pasting").replace("{text}", &self.clipboard_content(ctx)), WarpA11yRole::UserAction, )) } diff --git a/app/src/editor/view/model/mod.rs b/app/src/editor/view/model/mod.rs index dda625e5b9..f0f32b6a99 100644 --- a/app/src/editor/view/model/mod.rs +++ b/app/src/editor/view/model/mod.rs @@ -507,15 +507,20 @@ impl EditorModel { let action = if inclusive_contains(&selection_after, start) && inclusive_contains(&selection_after, end) { - "selected" + i18n::t("editor.a11y.selected") } else { - "unselected" + i18n::t("editor.a11y.unselected") }; - AccessibilityContent::new(delta, format!(", {action}"), WarpA11yRole::UserAction) - } - (true, false) => { - AccessibilityContent::new_without_help("Unselected", WarpA11yRole::UserAction) + AccessibilityContent::new( + delta, + i18n::t("editor.a11y.selection_action").replace("{action}", &action), + WarpA11yRole::UserAction, + ) } + (true, false) => AccessibilityContent::new_without_help( + i18n::t("editor.a11y.unselected"), + WarpA11yRole::UserAction, + ), } } @@ -2211,7 +2216,7 @@ impl EditorModel { ctx.emit_a11y_content(AccessibilityContent::new( self.selected_text(ctx), - ", deleted", + i18n::t("editor.a11y.deleted"), WarpA11yRole::UserAction, )); self.change_selections(new_selections, ctx); @@ -2235,7 +2240,7 @@ impl EditorModel { ctx.emit_a11y_content(AccessibilityContent::new( self.selected_text(ctx), - ", deleted", + i18n::t("editor.a11y.deleted"), WarpA11yRole::UserAction, )); self.change_selections(new_selections, ctx); diff --git a/app/src/editor/view/voice.rs b/app/src/editor/view/voice.rs index 06cbbb58fe..836dd05290 100644 --- a/app/src/editor/view/voice.rs +++ b/app/src/editor/view/voice.rs @@ -68,9 +68,9 @@ impl EditorView { ctx: &mut ViewContext, ) -> ViewHandle { let voice_new_feature_popup = ctx.add_typed_action_view(|_| { - FeaturePopup::new_feature(NewFeaturePopupLabel::FromString( - "Try Voice Input".to_string(), - )) + FeaturePopup::new_feature(NewFeaturePopupLabel::FromString(i18n::t( + "editor.voice.try_voice_input", + ))) }); ctx.subscribe_to_view(&voice_new_feature_popup, |_me, _, event, ctx| { @@ -263,7 +263,8 @@ impl EditorView { .as_ref(ctx) .can_request_voice() { - self.voice_error_toast(super::VOICE_LIMIT_HIT_TOAST_TEXT, ctx); + let message = i18n::t("editor.voice.limit_hit"); + self.voice_error_toast(&message, ctx); return false; } @@ -325,13 +326,10 @@ impl EditorView { AISettings::handle(ctx).update(ctx, |settings, ctx| { if let Some(toggle_key) = settings.maybe_setup_first_time_voice(ctx) { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = crate::view_components::DismissibleToast::success( - format!( - "Voice input is enabled. You can also press and hold the `{}` key to activate voice input (configure in Settings > AI > Voice)", - toggle_key.display_name() - ) - .to_string(), - ); + let message = i18n::t("editor.voice.enabled_with_key") + .replace("{key}", toggle_key.display_name()); + let toast = + crate::view_components::DismissibleToast::success(message); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); } @@ -356,8 +354,8 @@ impl EditorView { fn show_microphone_access_toast(ctx: &mut ViewContext) { let active_window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, move |toast_stack, ctx| { - let mut toast = crate::view_components::DismissibleToast::error(String::from( - "Failed to start voice input (you may need to enable Microphone access)", + let mut toast = crate::view_components::DismissibleToast::error(i18n::t( + "editor.voice.start_failed_microphone", )); // Set an id so the toast is shown at most once. toast = toast.with_object_id(MICROPHONE_ACCESS_ERROR_ID.to_string()); @@ -497,11 +495,13 @@ impl EditorView { } Err(e) => match e { TranscribeError::QuotaLimit => { - self.voice_error_toast(super::VOICE_LIMIT_HIT_TOAST_TEXT, ctx) + let message = i18n::t("editor.voice.limit_hit"); + self.voice_error_toast(&message, ctx) } _ => { log::error!("Failed to transcribe voice input: {e:?}"); - self.voice_error_toast(super::VOICE_ERROR_TOAST_TEXT, ctx) + let message = i18n::t("editor.voice.error"); + self.voice_error_toast(&message, ctx) } }, } @@ -529,14 +529,12 @@ impl EditorView { let modifier_key = AISettings::handle(app).as_ref(app).voice_input_toggle_key; let tooltip_text = if mic_access_denied { - "Voice transcription is disabled because Microphone access was not granted.".to_string() + i18n::t("editor.voice.tooltip_microphone_denied") } else if modifier_key == VoiceInputToggleKey::None { - "Voice transcription".to_string() + i18n::t("editor.voice.tooltip") } else { - format!( - "Voice transcription (hold `{}` key)", - modifier_key.display_name().to_lowercase() - ) + i18n::t("editor.voice.tooltip_with_key") + .replace("{key}", &modifier_key.display_name().to_lowercase()) }; Box::new(move || { diff --git a/app/src/env_vars/env_var_collection_block.rs b/app/src/env_vars/env_var_collection_block.rs index 04321a7daa..06960ecf15 100644 --- a/app/src/env_vars/env_var_collection_block.rs +++ b/app/src/env_vars/env_var_collection_block.rs @@ -44,9 +44,6 @@ use crate::view_components::compactible_action_button::{ /// For horizontal padding, use [`INLINE_ACTION_HORIZONTAL_PADDING`] for consistency. const ENV_VAR_COLLECTION_BODY_VERTICAL_PADDING: f32 = 16.; -const ENV_VAR_COLLECTION_CANCEL_LABEL: &str = "Cancel"; -const ENV_VAR_COLLECTION_ACCEPT_LABEL: &str = "Run"; - lazy_static! { static ref CANCEL_ENV_VAR_COLLECTION_KEYSTROKE: Keystroke = Keystroke { ctrl: true, @@ -147,7 +144,7 @@ impl EnvVarCollectionBlock { ctx: &mut ViewContext, ) -> Self { let cancel_button = CompactibleActionButton::new( - ENV_VAR_COLLECTION_CANCEL_LABEL.to_string(), + i18n::t("common.cancel"), Some(KeystrokeSource::Fixed( CANCEL_ENV_VAR_COLLECTION_KEYSTROKE.clone(), )), @@ -159,7 +156,7 @@ impl EnvVarCollectionBlock { ); let accept_button = CompactibleActionButton::new( - ENV_VAR_COLLECTION_ACCEPT_LABEL.to_string(), + i18n::t("common.run"), Some(KeystrokeSource::Fixed( ACCEPT_ENV_VAR_COLLECTION_KEYSTROKE.clone(), )), @@ -255,11 +252,8 @@ impl EnvVarCollectionBlock { } fn render_header(&self, app: &AppContext) -> Box { - const COMMAND_WAITING_FOR_USER_MESSAGE: &str = - "OK if I run this command and read the output?"; - let title: Cow<'static, str> = if self.state == EnvVarCollectionState::WaitingForUser { - COMMAND_WAITING_FOR_USER_MESSAGE.into() + i18n::t("env_vars.command_waiting_for_user").into() } else { self.command.clone().into() }; diff --git a/app/src/env_vars/view/command_dialog/command_dialog_view.rs b/app/src/env_vars/view/command_dialog/command_dialog_view.rs index 0a4929d0dd..b0a2f8a826 100644 --- a/app/src/env_vars/view/command_dialog/command_dialog_view.rs +++ b/app/src/env_vars/view/command_dialog/command_dialog_view.rs @@ -27,12 +27,6 @@ const CONTAINER_PADDING: f32 = 25.; const ELEMENT_SPACING: f32 = 10.; const EDITOR_DIVIDE: f32 = 6.; -const SECRET_SPAN: &str = "Secret command"; -const SAVE_BUTTON_LABEL: &str = "Save"; -const CANCEL_BUTTON_LABEL: &str = "Cancel"; -const NAME_PLACEHOLDER_TEXT: &str = "Name"; -const COMMAND_PLACEHOLDER_TEXT: &str = "Command"; - #[derive(Debug, Clone)] pub enum EnvVarCommandDialogAction { Close, @@ -70,7 +64,7 @@ impl EnvVarCommandDialog { }; let mut editor = EditorView::single_line(options, ctx); - editor.set_placeholder_text(NAME_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text(i18n::t("common.name"), ctx); editor }) }; @@ -98,7 +92,7 @@ impl EnvVarCommandDialog { }; let mut editor = EditorView::new(options, ctx); - editor.set_placeholder_text(COMMAND_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text(i18n::t("env_vars.command_placeholder"), ctx); editor }) }; @@ -254,7 +248,7 @@ impl EnvVarCommandDialog { Container::new( appearance .ui_builder() - .span(SECRET_SPAN) + .span(i18n::t("env_vars.secret_command")) .with_style(UiComponentStyles { font_size: Some(SPAN_FONT_SIZE), ..Default::default() @@ -306,7 +300,7 @@ impl View for EnvVarCommandDialog { .cancel_button_mouse_state_handle .clone(), EnvVarCommandDialogAction::Close, - CANCEL_BUTTON_LABEL, + &i18n::t("common.cancel"), false, app, ), @@ -325,7 +319,7 @@ impl View for EnvVarCommandDialog { .save_button_mouse_state_handle .clone(), EnvVarCommandDialogAction::SaveCommand, - SAVE_BUTTON_LABEL, + &i18n::t("common.save"), true, app, ), diff --git a/app/src/env_vars/view/editors.rs b/app/src/env_vars/view/editors.rs index 7910203ede..291bc5e578 100644 --- a/app/src/env_vars/view/editors.rs +++ b/app/src/env_vars/view/editors.rs @@ -21,8 +21,6 @@ use crate::Appearance; const LABEL_FONT_SIZE: f32 = 12.; const METADATA_SPACING: f32 = 8.; const LAST_ROW_ELEMENT_SPACING: f32 = 2.; -const TITLE_LABEL_TEXT: &str = "Title"; -const DESCRIPTION_LABEL_TEXT: &str = "Description"; const VERTICAL_TEXT_INPUT_PADDING: f32 = 5.; const HORIZONTAL_TEXT_INPUT_PADDING: f32 = 10.; @@ -33,7 +31,7 @@ impl EnvVarCollectionView { ctx: &mut ViewContext, font_size_override: Option, font_family_override: Option, - placeholder_text: Option<&str>, + placeholder_text: Option, single_line: bool, ) -> ViewHandle { let text = TextOptions { @@ -68,7 +66,7 @@ impl EnvVarCollectionView { ) }; - if let Some(text) = placeholder_text { + if let Some(text) = placeholder_text.as_ref() { editor.set_placeholder_text(text, ctx); } @@ -363,7 +361,7 @@ impl EnvVarCollectionView { Flex::column() .with_child( - Container::new(self.render_metadata_label(TITLE_LABEL_TEXT, appearance)) + Container::new(self.render_metadata_label(i18n::t("env_vars.title"), appearance)) .with_margin_bottom(METADATA_SPACING) .finish(), ) @@ -378,9 +376,11 @@ impl EnvVarCollectionView { ) .with_child( SavePosition::new( - Container::new(self.render_metadata_label(DESCRIPTION_LABEL_TEXT, appearance)) - .with_margin_bottom(METADATA_SPACING) - .finish(), + Container::new( + self.render_metadata_label(i18n::t("common.description"), appearance), + ) + .with_margin_bottom(METADATA_SPACING) + .finish(), DESCRIPTION_EDITOR_POSITION, ) .finish(), diff --git a/app/src/env_vars/view/env_var_collection.rs b/app/src/env_vars/view/env_var_collection.rs index 3913ac85c8..96d4778a54 100644 --- a/app/src/env_vars/view/env_var_collection.rs +++ b/app/src/env_vars/view/env_var_collection.rs @@ -69,19 +69,11 @@ const SECTION_SPACING: f32 = 16.; // Variable rows pub(super) const ROW_SPACING: f32 = 8.; -pub const EDUCATION_TEXT: &str = "Add secret or command. Warp never stores external secrets"; const VARIABLE_FONT_SIZE: f32 = 13.; const DESCRIPTION_EDITOR_CUTOFF: f32 = 30.; const DESCRIPTION_BOTTOM_MARGIN: f32 = 12.; const DIVIDER_BOTTOM_MARGIN: f32 = 4.; const PLACEHOLDER_FONT_SIZE: f32 = 14.; -const VARIABLE_VALUE_PLACEHOLDER_TEXT: &str = "Value"; -const VARIABLE_DESCRIPTION_PLACEHOLDER_TEXT: &str = "Description"; -const VARIABLE_NAME_PLACEHOLDER_TEXT: &str = "Variable"; - -// Text input fields -const TITLE_PLACEHOLDER_TEXT: &str = "Add a title"; -const DESCRIPTION_PLACEHOLDER_TEXT: &str = "Add a description"; // Button spacing const BUTTON_CONTAINER_HORIZONTAL_MARGIN: f32 = 36.; @@ -348,8 +340,8 @@ impl ValidationError { /// Create validation error from detected secret level fn from_secret_level(secret_level: SecretLevel) -> Self { let message = match secret_level { - SecretLevel::Enterprise => "This environment variable cannot be created due to conflicts with your enterprise's secret redaction settings. Contact a team admin for details.".to_string(), - SecretLevel::User => "This environment variable cannot be created due to conflicts with your secret redaction settings. Save the secret as an environment variable (in your shell config or a .env file), or update your secret redaction settings in Settings > Privacy.".to_string(), + SecretLevel::Enterprise => i18n::t("env_vars.secret_redaction_conflict_enterprise"), + SecretLevel::User => i18n::t("env_vars.secret_redaction_conflict_user"), }; Self { secret_level, @@ -486,7 +478,8 @@ impl EnvVarCollectionView { view.handle_cloud_model_event(event, ctx); }); - let pane_configuration = ctx.add_model(|_ctx| PaneConfiguration::new("Untitled")); + let pane_configuration = + ctx.add_model(|_ctx| PaneConfiguration::new(i18n::t("common.untitled"))); let active_env_var_collection_data = ctx.add_model(ActiveEnvVarCollectionData::new); ctx.subscribe_to_model( @@ -507,14 +500,14 @@ impl EnvVarCollectionView { ctx, Some(PLACEHOLDER_FONT_SIZE), Some(ui_font_family), - Some(TITLE_PLACEHOLDER_TEXT), + Some(i18n::t("env_vars.title_placeholder")), true, ); let description_editor = Self::create_editor_handle( ctx, Some(PLACEHOLDER_FONT_SIZE), Some(ui_font_family), - Some(DESCRIPTION_PLACEHOLDER_TEXT), + Some(i18n::t("env_vars.description_placeholder")), false, ); ctx.subscribe_to_view(&title_editor, |me, _, event, ctx| { @@ -658,7 +651,12 @@ impl EnvVarCollectionView { let title = collection.title.clone().unwrap_or_default(); - self.set_pane_title(if title.is_empty() { "Untitled" } else { &title }, ctx); + let pane_title = if title.is_empty() { + i18n::t("common.untitled") + } else { + title.clone() + }; + self.set_pane_title(&pane_title, ctx); if let Some(server_id) = env_var_collection.id.into_server() { self.pane_configuration.update(ctx, |pane_config, ctx| { pane_config @@ -740,9 +738,7 @@ impl EnvVarCollectionView { let window_id = ctx.window_id(); crate::workspace::ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "An error occurred while trying to invoke the env var".to_owned(), - ), + DismissibleToast::error(i18n::t("env_vars.invoke_failed")), window_id, ctx, ); @@ -873,7 +869,7 @@ impl EnvVarCollectionView { ctx, Some(VARIABLE_FONT_SIZE), Some(ui_font_family), - Some(VARIABLE_NAME_PLACEHOLDER_TEXT), + Some(i18n::t("env_vars.variable_placeholder")), true, ); @@ -885,7 +881,7 @@ impl EnvVarCollectionView { ctx, Some(VARIABLE_FONT_SIZE), Some(ui_font_family), - Some(VARIABLE_VALUE_PLACEHOLDER_TEXT), + Some(i18n::t("env_vars.value_placeholder")), true, ); @@ -897,7 +893,7 @@ impl EnvVarCollectionView { ctx, Some(VARIABLE_FONT_SIZE), Some(ui_font_family), - Some(VARIABLE_DESCRIPTION_PLACEHOLDER_TEXT), + Some(i18n::t("common.description")), true, ); @@ -1130,7 +1126,7 @@ impl EnvVarCollectionView { .finish() } else { appearance.ui_builder().tool_tip_on_element( - EDUCATION_TEXT.to_string(), + i18n::t("env_vars.secret_or_command_tooltip"), self.button_mouse_states.secret_tooltip_state.clone(), icon_button_with_context_menu( Icon::Key, @@ -1584,7 +1580,11 @@ impl BackingView for EnvVarCollectionView { app: &AppContext, ) -> view::HeaderContent { let title = self.title_editor.as_ref(app).buffer_text(app); - let title = if title.is_empty() { "Untitled" } else { &title }; + let title = if title.is_empty() { + i18n::t("common.untitled") + } else { + title + }; view::HeaderContent::simple(title) } diff --git a/app/src/env_vars/view/fixed_view_components.rs b/app/src/env_vars/view/fixed_view_components.rs index d0c3319708..26ce30754b 100644 --- a/app/src/env_vars/view/fixed_view_components.rs +++ b/app/src/env_vars/view/fixed_view_components.rs @@ -22,9 +22,6 @@ const VARIABLE_DIVIDER_HEIGHT: f32 = 2.; const SECTION_FONT_SIZE: f32 = 16.; const BUTTON_HEIGHT: f32 = 32.; -const SAVE_BUTTON_TEXT: &str = "Save"; -const VARIABLES_LABEL_TEXT: &str = "Variables"; - /// This file contains components that fixed in the view, /// i.e. the trash banner, breadcrumbs, and variables section header impl EnvVarCollectionView { @@ -57,9 +54,9 @@ impl EnvVarCollectionView { let mut stack = Stack::new(); let text = if deleted { - "You no longer have access to these environment variables" + i18n::t("env_vars.no_longer_access") } else { - "Environment variables were moved to trash" + i18n::t("env_vars.moved_to_trash") }; stack.add_child( Align::new( @@ -112,13 +109,11 @@ impl EnvVarCollectionView { ) .with_tooltip(move || { ui_builder - .tool_tip( - "Restore environment variables from trash".to_string(), - ) + .tool_tip(i18n::t("env_vars.restore_from_trash")) .build() .finish() }) - .with_text_label("Restore".to_string()) + .with_text_label(i18n::t("common.restore")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(EnvVarCollectionAction::Untrash) @@ -161,7 +156,7 @@ impl EnvVarCollectionView { 2., appearance .ui_builder() - .span(VARIABLES_LABEL_TEXT.to_string()) + .span(i18n::t("env_vars.variables")) .with_style(UiComponentStyles { font_size: Some(SECTION_FONT_SIZE), ..Default::default() @@ -240,7 +235,7 @@ impl EnvVarCollectionView { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::TextFirst, - "Load", + i18n::t("env_vars.load"), Icon::TerminalInput.to_warpui_icon(appearance.theme().active_ui_text_color()), MainAxisSize::Min, MainAxisAlignment::SpaceBetween, @@ -293,7 +288,7 @@ impl EnvVarCollectionView { font_size: Some(14.), ..Default::default() }) - .with_centered_text_label(SAVE_BUTTON_TEXT.to_owned()); + .with_centered_text_label(i18n::t("common.save")); if is_save_disabled { button = button.disabled(); diff --git a/app/src/env_vars/view/menus.rs b/app/src/env_vars/view/menus.rs index 66edc27e5f..929c97094d 100644 --- a/app/src/env_vars/view/menus.rs +++ b/app/src/env_vars/view/menus.rs @@ -31,7 +31,7 @@ pub struct Menus { impl EnvVarCollectionView { pub(super) fn initialize_menus(ctx: &mut ViewContext) -> Menus { let command_item = Self::item( - "Command", + i18n::t("env_vars.command"), EnvVarCollectionAction::DisplayCommandDialog, None, Some(Icon::Terminal), @@ -52,14 +52,14 @@ impl EnvVarCollectionView { ); let edit_item = Self::item( - "Edit", + i18n::t("common.edit"), EnvVarCollectionAction::EditCommand, None, Some(Icon::Terminal), ); let clear_secret_item = Self::item( - "Clear secret", + i18n::t("env_vars.clear_secret"), EnvVarCollectionAction::ClearSecret, None, Some(Icon::Trash), @@ -128,28 +128,28 @@ impl EnvVarCollectionView { ctx: &mut ViewContext, ) -> ViewHandle> { let split_pane_right = Self::item( - "Split pane right", + i18n::t("common.split_pane_right"), EnvVarCollectionAction::EmitPaneEvent(PaneEvent::SplitRight(None)), keybinding_name_to_display_string("pane_group:add_right", ctx), None, ); let split_pane_left = Self::item( - "Split pane left", + i18n::t("common.split_pane_left"), EnvVarCollectionAction::EmitPaneEvent(PaneEvent::SplitLeft(None)), keybinding_name_to_display_string("pane_group:add_left", ctx), None, ); let split_pane_down = Self::item( - "Split pane down", + i18n::t("common.split_pane_down"), EnvVarCollectionAction::EmitPaneEvent(PaneEvent::SplitDown(None)), keybinding_name_to_display_string("pane_group:add_down", ctx), None, ); let split_pane_up = Self::item( - "Split pane up", + i18n::t("common.split_pane_up"), EnvVarCollectionAction::EmitPaneEvent(PaneEvent::SplitUp(None)), keybinding_name_to_display_string("pane_group:add_up", ctx), None, @@ -161,9 +161,9 @@ impl EnvVarCollectionView { .is_some_and(|handle| handle.split_pane_state(ctx).is_maximized()); let toggle_maximize_pane = Self::item( if is_maximized { - "Minimize pane" + i18n::t("env_vars.minimize_pane") } else { - "Maximize pane" + i18n::t("env_vars.maximize_pane") }, EnvVarCollectionAction::EmitPaneEvent(PaneEvent::ToggleMaximized), keybinding_name_to_display_string("pane_group:toggle_maximize_pane", ctx), @@ -171,7 +171,7 @@ impl EnvVarCollectionView { ); let close_pane = Self::item( - "Close pane", + i18n::t("common.close_pane"), EnvVarCollectionAction::EmitPaneEvent(PaneEvent::Close), trigger_to_keystroke(&Trigger::Custom(CustomAction::CloseCurrentSession.into())) .map(|keystroke| keystroke.displayed()), @@ -336,7 +336,7 @@ impl EnvVarCollectionView { } fn item( - name: &str, + name: impl Into, action: EnvVarCollectionAction, key_shortcut: Option, icon: Option, @@ -372,7 +372,7 @@ impl EnvVarCollectionView { // Add "Copy Link" to menu if let Some(link) = self.env_var_collection_link(ctx) { menu_items.push( - MenuItemFields::new("Copy link") + MenuItemFields::new(i18n::t("common.copy_link")) .with_on_select_action(EnvVarCollectionAction::CopyLink(link)) .with_icon(Icon::Link) .into_item(), @@ -382,7 +382,7 @@ impl EnvVarCollectionView { // Add "Duplicate" to menu if space != Some(Space::Shared) { menu_items.push( - MenuItemFields::new("Duplicate") + MenuItemFields::new(i18n::t("common.duplicate")) .with_on_select_action(EnvVarCollectionAction::Duplicate) .with_icon(Icon::Duplicate) .into_item(), @@ -394,7 +394,7 @@ impl EnvVarCollectionView { && (!FeatureFlag::SharedWithMe.is_enabled() || access_level.can_trash()) { menu_items.push( - MenuItemFields::new("Trash") + MenuItemFields::new(i18n::t("common.trash")) .with_on_select_action(EnvVarCollectionAction::Trash) .with_icon(Icon::Trash) .into_item(), @@ -403,7 +403,7 @@ impl EnvVarCollectionView { #[cfg(feature = "local_fs")] menu_items.push( - MenuItemFields::new("Export") + MenuItemFields::new(i18n::t("common.export")) .with_on_select_action(EnvVarCollectionAction::Export) .with_icon(Icon::Download) .into_item(), diff --git a/app/src/env_vars/view/unsaved_changes_dialog.rs b/app/src/env_vars/view/unsaved_changes_dialog.rs index 16a9ef8b8c..10ea1267a5 100644 --- a/app/src/env_vars/view/unsaved_changes_dialog.rs +++ b/app/src/env_vars/view/unsaved_changes_dialog.rs @@ -9,9 +9,6 @@ use warpui::Element; use super::env_var_collection::{EnvVarCollectionAction, EnvVarCollectionView}; use crate::ui_components::dialog::{dialog_styles, Dialog}; -const UNSAVED_CHANGES_TEXT: &str = "You have unsaved changes."; -const KEEP_EDITING_TEXT: &str = "Keep editing"; -const DISCARD_CHANGES_TEXT: &str = "Discard changes"; const BUTTON_FONT_SIZE: f32 = 14.; const BUTTON_PADDING: f32 = 12.; const MODAL_HORIZONTAL_MARGIN: f32 = 28.; @@ -23,7 +20,7 @@ impl EnvVarCollectionView { appearance: &Appearance, button_mouse_state: MouseStateHandle, action: EnvVarCollectionAction, - text: &str, + text: String, ) -> Box { appearance .ui_builder() @@ -34,7 +31,7 @@ impl EnvVarCollectionView { padding: Some(Coords::uniform(BUTTON_PADDING)), ..Default::default() }) - .with_text_label(text.into()) + .with_text_label(text) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| ctx.dispatch_typed_action(action.clone())) @@ -46,19 +43,19 @@ impl EnvVarCollectionView { appearance, self.button_mouse_states.keep_editing_state.clone(), EnvVarCollectionAction::CloseUnsavedChangesDialog, - KEEP_EDITING_TEXT, + i18n::t("env_vars.keep_editing"), ); let discard_changes_button = self.render_unsaved_changes_dialog_button( appearance, self.button_mouse_states.discard_changes_state.clone(), EnvVarCollectionAction::ForceClose, - DISCARD_CHANGES_TEXT, + i18n::t("env_vars.discard_changes"), ); Container::new( Dialog::new( - UNSAVED_CHANGES_TEXT.to_string(), + i18n::t("env_vars.unsaved_changes"), None, dialog_styles(appearance), ) diff --git a/app/src/external_secrets/mod.rs b/app/src/external_secrets/mod.rs index bd2b0a9d15..3b981b890b 100644 --- a/app/src/external_secrets/mod.rs +++ b/app/src/external_secrets/mod.rs @@ -184,14 +184,19 @@ impl SecretManager { ) -> ErrorMessageAndCommand { match error_type { SecretErrorType::NotInstalled => { - let message = format!("{} CLI is not installed", &self); + let manager = self.to_string(); + let message = i18n::t("external_secrets.error.cli_not_installed") + .replace("{manager}", &manager); let (link, link_message) = ( match self { SecretManager::OnePassword => Some(ONEPASSWORD_DOCS_LINK.to_owned()), SecretManager::LastPass => Some(LASTPASS_DOCS_LINK.to_owned()), }, - Some(format!("View {} CLI installation documentation", &self)), + Some( + i18n::t("external_secrets.link.view_cli_installation_docs") + .replace("{manager}", &manager), + ), ); ErrorMessageAndCommand { @@ -204,21 +209,19 @@ impl SecretManager { let (link, link_message) = match self { SecretManager::OnePassword => ( Some(ONEPASSWORD_DOCS_LINK.to_owned()), - Some("Integrate 1Password app with CLI".to_owned()), + Some(i18n::t("external_secrets.link.integrate_1password_cli")), ), SecretManager::LastPass => (None, None), }; ErrorMessageAndCommand { - message: format!( - "{} didn't return secrets (likely not configured or authenticated)", - &self - ), + message: i18n::t("external_secrets.error.fetch_failed") + .replace("{manager}", &self.to_string()), link, link_message, } } SecretErrorType::InvalidPlatform => ErrorMessageAndCommand { - message: "Platform not supported".to_owned(), + message: i18n::t("external_secrets.error.platform_not_supported"), link: None, link_message: None, }, diff --git a/app/src/input_suggestions.rs b/app/src/input_suggestions.rs index 6f22d7280c..2d5a2ea1a7 100644 --- a/app/src/input_suggestions.rs +++ b/app/src/input_suggestions.rs @@ -50,6 +50,18 @@ pub enum DetailContent { AIQueryHistory(Box), } +fn last_ran_a11y(time: impl AsRef) -> String { + i18n::t("input_suggestions.a11y.last_ran").replace("{time}", time.as_ref()) +} + +fn suggestion_a11y(text: &str) -> String { + i18n::t("input_suggestions.a11y.suggestion").replace("{text}", text) +} + +fn selected_a11y(text: &str) -> String { + i18n::t("input_suggestions.a11y.selected").replace("{text}", text) +} + impl From for DetailContent { fn from(entry: HistoryEntry) -> Self { DetailContent::RichHistory(Box::new(entry)) @@ -539,11 +551,10 @@ impl InputSuggestions { .and_then(|details| match details { DetailContent::RichHistory(entry) => entry .start_ts - .map(|ts| format!("Last ran {}", format_approx_duration_from_now(ts))), + .map(|ts| last_ran_a11y(format_approx_duration_from_now(ts))), DetailContent::Description(desc) => Some(desc.clone()), - DetailContent::AIQueryHistory(entry) => Some(format!( - "Last ran {}", - format_approx_duration_from_now(entry.start_time) + DetailContent::AIQueryHistory(entry) => Some(last_ran_a11y( + format_approx_duration_from_now(entry.start_time), )), }) } @@ -588,14 +599,14 @@ impl InputSuggestions { ) { (Some(text), Some(desc)) => { ctx.emit_a11y_content(AccessibilityContent::new( - format!("Suggestion: {text}.\n"), + format!("{}\n", suggestion_a11y(text)), desc, WarpA11yRole::MenuItemRole, )); } (Some(text), None) => { ctx.emit_a11y_content(AccessibilityContent::new_without_help( - format!("Suggestion: {text}.\n"), + format!("{}\n", suggestion_a11y(text)), WarpA11yRole::MenuItemRole, )); } @@ -619,7 +630,7 @@ impl InputSuggestions { if let Some(text) = self.get_selected_item_text() { ctx.emit_a11y_content(AccessibilityContent::new_without_help( - format!("Selected: {text}"), + selected_a11y(text), WarpA11yRole::MenuItemRole, )); } @@ -644,7 +655,7 @@ impl InputSuggestions { ctx: &mut ViewContext, ) { ctx.emit_a11y_content(AccessibilityContent::new_without_help( - "Closed suggestions.", + i18n::t("input_suggestions.a11y.closed"), WarpA11yRole::UserAction, )); ctx.emit(Event::CloseSuggestion { @@ -700,7 +711,7 @@ impl InputSuggestions { Align::new( Container::new( Text::new_inline( - String::from("No suggestions"), + i18n::t("input_suggestions.no_suggestions"), appearance.monospace_font_family(), appearance.monospace_font_size(), ) @@ -884,7 +895,9 @@ impl InputSuggestions { let tooltip_element = appearance .ui_builder() - .tool_tip("Ignore this suggestion".to_string()) + .tool_tip(i18n::t( + "editor.autosuggestion.ignore_this_suggestion", + )) .build() .finish(); @@ -1084,10 +1097,9 @@ impl View for InputSuggestions { fn accessibility_contents(&self, _: &AppContext) -> Option { Some(AccessibilityContent::new( - "Command suggestions.", + i18n::t("input_suggestions.a11y.command_suggestions"), // TODO use bindings from user settings - "Navigate with tab and shift-tab, and confirm with enter. Execute selected command \ - with command + enter. Esc leaves the suggestions menu.", + i18n::t("input_suggestions.a11y.command_suggestions_help"), WarpA11yRole::MenuRole, )) } diff --git a/app/src/launch_configs/save_modal.rs b/app/src/launch_configs/save_modal.rs index 5476bd3373..3b2a53b6c1 100644 --- a/app/src/launch_configs/save_modal.rs +++ b/app/src/launch_configs/save_modal.rs @@ -38,8 +38,6 @@ const MODAL_WIDTH: f32 = 660.; const SIDE_PADDING: f32 = 16.; const BUTTON_SIZE: f32 = 24.; const DOC_LINK_WIDTH: f32 = 120.; -const SAVE_CONFIG_BUTTON_LABEL: &str = "Save Configuration"; -const OPEN_FILE_BUTTON_LABEL: &str = "Open YAML File"; pub fn init(app: &mut AppContext) { use warpui::keymap::macros::*; @@ -343,7 +341,7 @@ impl LaunchConfigSaveModal { ) -> Box { self.save_modal_button( appearance, - SAVE_CONFIG_BUTTON_LABEL.to_owned(), + i18n::t("launch_configs.save_configuration"), self.mouse_states.save_button_state.clone(), disabled, ) @@ -357,7 +355,7 @@ impl LaunchConfigSaveModal { fn render_open_file_button(&self, appearance: &Appearance) -> Box { self.save_modal_button( appearance, - OPEN_FILE_BUTTON_LABEL.to_owned(), + i18n::t("launch_configs.open_yaml_file"), self.mouse_states.open_file_button_state.clone(), false, ) @@ -440,7 +438,7 @@ impl LaunchConfigSaveModal { 1.0, Align::new( Text::new_inline( - "Save Current Configuration", + i18n::t("launch_configs.save_current_configuration"), appearance.header_font_family(), appearance.header_font_size(), ) @@ -529,7 +527,7 @@ impl LaunchConfigSaveModal { appearance .ui_builder() .link( - "Link to Documentation".to_string(), + i18n::t("launch_configs.link_to_documentation"), Some( "https://docs.warp.dev/terminal/sessions/launch-configurations" .to_string(), @@ -550,11 +548,18 @@ impl LaunchConfigSaveModal { let info = match &self.save_state { SaveState::Success => header .with_child( - self.render_formatted_text_line(appearance, vec![ - FormattedTextFragment::plain_text("Saved successfully to "), - FormattedTextFragment::inline_code(self.file_name.clone().unwrap_or_default()), - FormattedTextFragment::plain_text(".") - ]) + self.render_formatted_text_line( + appearance, + vec![ + FormattedTextFragment::plain_text(i18n::t( + "launch_configs.saved_successfully_to", + )), + FormattedTextFragment::inline_code( + self.file_name.clone().unwrap_or_default(), + ), + FormattedTextFragment::plain_text("."), + ], + ) .with_padding_bottom(24.) .finish(), ) @@ -564,34 +569,42 @@ impl LaunchConfigSaveModal { appearance, match failure_type { FailureType::FileAlreadyExists => { - "Failed to save. A launch configuration with the same name already exists.".to_string() + i18n::t("launch_configs.failed_file_already_exists") } - FailureType::Other => "An issue was encountered while saving.".to_string(), + FailureType::Other => i18n::t("launch_configs.failed_saving"), }, ) .with_padding_bottom(24.) .finish(), ), SaveState::NotSaved => { - let mut text = "This will save your current configuration of windows, tabs \ - and panes to a file so you can easily open it again".to_string(); - if self.open_modal_keybinding_str.is_empty() { - text.push('.'); + let text = if self.open_modal_keybinding_str.is_empty() { + i18n::t("launch_configs.description") } else { - text.push_str(&format!(" with {}.", self.open_modal_keybinding_str)); - } + i18n::t("launch_configs.description_with_shortcut") + .replace("{shortcut}", &self.open_modal_keybinding_str) + }; header .with_child( - self.render_formatted_text_line(appearance, vec![ - FormattedTextFragment::plain_text(text) - ]).finish() + self.render_formatted_text_line( + appearance, + vec![FormattedTextFragment::plain_text(text)], + ) + .finish(), ) .with_child( - self.render_formatted_text_line(appearance, vec![ - FormattedTextFragment::plain_text("\nThe YAML file is saved to "), - FormattedTextFragment::inline_code(home_relative_path(&launch_configs_dir())), - FormattedTextFragment::plain_text("."), - ]) + self.render_formatted_text_line( + appearance, + vec![ + FormattedTextFragment::plain_text(i18n::t( + "launch_configs.yaml_saved_to", + )), + FormattedTextFragment::inline_code(home_relative_path( + &launch_configs_dir(), + )), + FormattedTextFragment::plain_text("."), + ], + ) .with_padding_bottom(24.) .finish(), ) @@ -658,10 +671,8 @@ impl View for LaunchConfigSaveModal { fn accessibility_contents(&self, _ctx: &AppContext) -> Option { Some(AccessibilityContent::new( - "Save Config Modal", - "Type the name of the file to which you want to save your - current configuration of windows, tabs, and panes. Use enter to save the - launch configuration, esc to quit the save configuration modal.", + i18n::t("launch_configs.a11y.save_config_modal"), + i18n::t("launch_configs.a11y.save_config_modal_help"), WarpA11yRole::PopoverRole, )) } diff --git a/app/src/menu.rs b/app/src/menu.rs index dbd94d33ee..3b293ba6c5 100644 --- a/app/src/menu.rs +++ b/app/src/menu.rs @@ -52,6 +52,14 @@ const DROP_SHADOW_COLOR: ColorU = ColorU { }; const SECONDARY_TEXT_RATIO: f32 = 0.9; +fn menu_item_selected_a11y(item: &str) -> String { + i18n::t("menu.a11y.item_selected").replace("{item}", item) +} + +fn menu_item_expanded_a11y(item: &str) -> String { + i18n::t("menu.a11y.item_expanded").replace("{item}", item) +} + #[derive(Clone, Debug)] /// At the current time, its not recommended to have more than 1 nested submenu due to /// layout constraints. @@ -715,9 +723,9 @@ impl MenuItemFields { pub fn toggle_pane_action(is_maximized: bool) -> Self { Self::new(if is_maximized { - "Minimize pane" + i18n::t("menu.pane.minimize") } else { - "Maximize pane" + i18n::t("menu.pane.maximize") }) } @@ -774,6 +782,10 @@ impl MenuItemFields { self.on_select_action.as_ref() } + pub fn has_submenu(&self) -> bool { + self.has_submenu + } + pub fn no_highlight_on_hover(mut self) -> Self { self.highlight_on_hover = false; self @@ -2540,19 +2552,19 @@ impl SubMenu { Select(_) => { let menu_item = match self.selected_item() { Some(item) => match item { - MenuItem::Item(fields) => format!("{} Selected", fields.get_a11y_text()), + MenuItem::Item(fields) => menu_item_selected_a11y(fields.get_a11y_text()), MenuItem::ItemsRow { items } => { let selected_item_text = items .get(self.selected_item_index.unwrap_or_default()) .map_or_else(|| "", |item| item.get_a11y_text()); - format!("{selected_item_text} Selected") + menu_item_selected_a11y(selected_item_text) } MenuItem::Separator => String::from(""), MenuItem::Submenu { fields, .. } => { - format!("{} Expanded", fields.get_a11y_text()) + menu_item_expanded_a11y(fields.get_a11y_text()) } MenuItem::Header { fields, .. } => { - format!("{} Selected", fields.get_a11y_text()) + menu_item_selected_a11y(fields.get_a11y_text()) } }, None => String::from(""), @@ -2560,9 +2572,9 @@ impl SubMenu { let instructions = if matches!(self.selected_item(), Some(MenuItem::Submenu { .. })) { - "Press the up key or the down key to select a menu item. Press the right key to open the submenu" + i18n::t("menu.a11y.instructions.select_item_open_submenu") } else { - "Press the up key or the down key to select a menu item" + i18n::t("menu.a11y.instructions.select_item") }; Custom(AccessibilityContent::new( @@ -2572,23 +2584,23 @@ impl SubMenu { )) } OpenSubmenu => Custom(AccessibilityContent::new( - String::from("Submenu Expanded"), - "Press the right key to open the selected submenu", + i18n::t("menu.a11y.submenu_expanded"), + i18n::t("menu.a11y.instructions.open_selected_submenu"), WarpA11yRole::TextRole, )), CloseSubmenu(_) => Custom(AccessibilityContent::new( - String::from("Submenu Closed"), - "Removing focus from a submenu will close the submenu", + i18n::t("menu.a11y.submenu_closed"), + i18n::t("menu.a11y.instructions.close_submenu"), WarpA11yRole::TextRole, )), Close(_) => Custom(AccessibilityContent::new( - String::from("Menu Closed"), - "Press the escape key to close the menu", + i18n::t("menu.a11y.menu_closed"), + i18n::t("menu.a11y.instructions.close_menu"), WarpA11yRole::TextRole, )), Enter => Custom(AccessibilityContent::new( - String::from("Action Selected"), - "Press the enter key to execute the selected menu item action", + i18n::t("menu.a11y.action_selected"), + i18n::t("menu.a11y.instructions.execute_action"), WarpA11yRole::TextRole, )), HoverSubmenuLeafNode { .. } diff --git a/app/src/notebooks/context_menu.rs b/app/src/notebooks/context_menu.rs index 07d0204a61..3779f2b8f6 100644 --- a/app/src/notebooks/context_menu.rs +++ b/app/src/notebooks/context_menu.rs @@ -116,19 +116,19 @@ where }; if has_selection && can_edit { - let item = MenuItemFields::new("Cut") + let item = MenuItemFields::new(i18n::t("common.cut")) .with_on_select_action(V::Action::from(ContextMenuAction::CutSelectedText)) .with_key_shortcut_label(custom_action_to_display(CustomAction::Cut)); items.push(item.into_item()); } if has_selection { - let item = MenuItemFields::new("Copy") + let item = MenuItemFields::new(i18n::t("common.copy")) .with_on_select_action(V::Action::from(ContextMenuAction::CopySelectedText)) .with_key_shortcut_label(custom_action_to_display(CustomAction::Copy)); items.push(item.into_item()); } if can_edit { - let item = MenuItemFields::new("Paste") + let item = MenuItemFields::new(i18n::t("common.paste")) .with_on_select_action(V::Action::from(ContextMenuAction::Paste)) .with_key_shortcut_label(custom_action_to_display(CustomAction::Paste)); items.push(item.into_item()); @@ -155,7 +155,7 @@ where let mut items = vec![]; if ContextFlag::CreateNewSession.is_enabled() { items.extend([ - MenuItemFields::new("Split pane right") + MenuItemFields::new(i18n::t("common.split_pane_right")) .with_on_select_action(V::Action::from(ContextMenuAction::EmitPaneEvent( PaneEvent::SplitRight(None), ))) @@ -164,7 +164,7 @@ where ctx, )) .into_item(), - MenuItemFields::new("Split pane left") + MenuItemFields::new(i18n::t("common.split_pane_left")) .with_on_select_action(V::Action::from(ContextMenuAction::EmitPaneEvent( PaneEvent::SplitLeft(None), ))) @@ -173,7 +173,7 @@ where ctx, )) .into_item(), - MenuItemFields::new("Split pane down") + MenuItemFields::new(i18n::t("common.split_pane_down")) .with_on_select_action(V::Action::from(ContextMenuAction::EmitPaneEvent( PaneEvent::SplitDown(None), ))) @@ -182,7 +182,7 @@ where ctx, )) .into_item(), - MenuItemFields::new("Split pane up") + MenuItemFields::new(i18n::t("common.split_pane_up")) .with_on_select_action(V::Action::from(ContextMenuAction::EmitPaneEvent( PaneEvent::SplitUp(None), ))) @@ -213,7 +213,7 @@ where ); items.push( - MenuItemFields::new("Close pane") + MenuItemFields::new(i18n::t("common.close_pane")) .with_on_select_action(V::Action::from(ContextMenuAction::EmitPaneEvent( PaneEvent::Close, ))) diff --git a/app/src/notebooks/editor/block_insertion_menu.rs b/app/src/notebooks/editor/block_insertion_menu.rs index 1a8b8ddd82..ab1278ced6 100644 --- a/app/src/notebooks/editor/block_insertion_menu.rs +++ b/app/src/notebooks/editor/block_insertion_menu.rs @@ -97,7 +97,7 @@ impl BlockInsertionMenuState { if embedded_objects_enabled { menu.add_item( - MenuItemFields::new("Embed") + MenuItemFields::new(i18n::t("notebooks.insert.embed")) .with_icon(Icon::EmbedBlock) .with_on_select_action(EditorViewAction::OpenEmbeddedObjectSearch) .into_item(), @@ -117,7 +117,7 @@ impl BlockInsertionMenuState { } menu.add_item( - MenuItemFields::new("Divider") + MenuItemFields::new(i18n::t("notebooks.insert.divider")) .with_icon(Icon::HorizontalRuleBlock) .with_on_select_action(EditorViewAction::InsertBlock( warp_editor::content::text::BlockType::Item(BufferBlockItem::HorizontalRule), @@ -237,7 +237,7 @@ impl RichTextEditorView { let title = model .get_notebook(id) .map(|notebook| notebook.model().title.clone()) - .unwrap_or_else(|| "Untitled".to_string()); + .unwrap_or_else(|| i18n::t("common.untitled")); let link = model .get_by_uid(&CloudObjectTypeAndId::Notebook(*id).uid()) .and_then(|object| object.object_link()); @@ -306,7 +306,7 @@ impl RichTextEditorView { }) .with_tooltip(move || { ui_builder - .tool_tip("Insert block".to_string()) + .tool_tip(i18n::t("notebooks.insert.insert_block")) .build() .finish() }) diff --git a/app/src/notebooks/editor/embedding_model.rs b/app/src/notebooks/editor/embedding_model.rs index fe763e0a6d..84d54ea11c 100644 --- a/app/src/notebooks/editor/embedding_model.rs +++ b/app/src/notebooks/editor/embedding_model.rs @@ -227,7 +227,7 @@ impl NotebookEmbed { appearance, Icon::Pencil, self.mouse_state_handles.edit_button_state.clone(), - "Edit", + i18n::t("common.edit"), None, ) .on_click(move |ctx, _, _| { @@ -245,7 +245,7 @@ impl NotebookEmbed { appearance, Icon::Copy, self.mouse_state_handles.copy_button_state.clone(), - "Copy", + i18n::t("common.copy"), custom_action_to_display(CustomAction::Copy), ) .on_click(move |ctx, _, _| { @@ -267,7 +267,7 @@ impl NotebookEmbed { appearance, Icon::TerminalInput, self.mouse_state_handles.insert_button_state.clone(), - "Run in terminal", + i18n::t("notebooks.command.run_in_terminal"), NotebookKeybindings::as_ref(ctx).run_commands_keybinding(), ) .on_click(move |ctx, _, _| { @@ -318,7 +318,7 @@ impl EmbeddedItemModel for NotebookEmbed { .remove_embedding_button_state .clone(), ) - .with_text_label("Remove".to_string()) + .with_text_label(i18n::t("common.remove")) .build() .with_cursor(Cursor::Arrow) .on_click(move |ctx, _, _| { diff --git a/app/src/notebooks/editor/find_bar.rs b/app/src/notebooks/editor/find_bar.rs index 8a0a760237..3263053331 100644 --- a/app/src/notebooks/editor/find_bar.rs +++ b/app/src/notebooks/editor/find_bar.rs @@ -27,10 +27,7 @@ use super::view::{EditorViewEvent, RichTextEditorView}; use crate::appearance::Appearance; use crate::editor::{EditorView, Event as EditorEvent, SingleLineEditorOptions, TextOptions}; use crate::ui_components::icons::Icon; -use crate::view_components::find::{ - CASE_SENSITIVE_LABEL, CASE_SENSITIVE_TOOLTIP, FIND_BAR_WIDTH, REGEX_TOGGLE_LABEL, - REGEX_TOGGLE_TOOLTIP, -}; +use crate::view_components::find::{CASE_SENSITIVE_LABEL, FIND_BAR_WIDTH, REGEX_TOGGLE_LABEL}; /// View for the find bar within a notebook. pub struct FindBar { @@ -187,7 +184,7 @@ impl FindBar { if searcher.has_query() { let match_count = searcher.match_count(); let text = if match_count == 0 { - "No matches".to_string() + i18n::t("common.no_matches") } else { let mut text = String::new(); match searcher.selected_match() { @@ -411,7 +408,7 @@ impl View for FindBar { ), self.render_toggle_button( REGEX_TOGGLE_LABEL, - REGEX_TOGGLE_TOOLTIP, + &i18n::t("find.regex_toggle_tooltip"), FindBarAction::ToggleRegex, searcher.is_regex(), self.button_handles.regex_toggle.clone(), @@ -420,7 +417,7 @@ impl View for FindBar { ), self.render_toggle_button( CASE_SENSITIVE_LABEL, - CASE_SENSITIVE_TOOLTIP, + &i18n::t("find.case_sensitive_tooltip"), FindBarAction::ToggleCaseSensitive, searcher.is_case_sensitive(), self.button_handles.case_sensitive_toggle.clone(), @@ -535,21 +532,21 @@ impl TypedActionView for FindBar { let text = match action { FindBarAction::ToggleRegex => { if self.searcher.as_ref(ctx).is_regex() { - "Enable regex search" + i18n::t("find.a11y.enable_regex_search") } else { - "Disable regex search" + i18n::t("find.a11y.disable_regex_search") } } FindBarAction::ToggleCaseSensitive => { if self.searcher.as_ref(ctx).is_case_sensitive() { - "Enable case-sensitive search" + i18n::t("find.a11y.enable_case_sensitive_search") } else { - "Disable case-sensitive search" + i18n::t("find.a11y.disable_case_sensitive_search") } } - FindBarAction::FocusNextMatch => "Focus next match", - FindBarAction::FocusPreviousMatch => "Focus previous match", - FindBarAction::Close => "Close find bar", + FindBarAction::FocusNextMatch => i18n::t("find.a11y.focus_next_match"), + FindBarAction::FocusPreviousMatch => i18n::t("find.a11y.focus_previous_match"), + FindBarAction::Close => i18n::t("find.a11y.close_find_bar"), }; Some(AccessibilityContent::new_without_help( text, diff --git a/app/src/notebooks/editor/link_editor.rs b/app/src/notebooks/editor/link_editor.rs index 69cce4b740..2bbece669b 100644 --- a/app/src/notebooks/editor/link_editor.rs +++ b/app/src/notebooks/editor/link_editor.rs @@ -52,7 +52,7 @@ impl LinkEditor { let tag_editor = ctx.add_typed_action_view(|ctx| { let mut editor = EditorView::single_line(editor_options.clone(), ctx); - editor.set_placeholder_text("Text", ctx); + editor.set_placeholder_text(i18n::t("common.text"), ctx); editor }); @@ -62,7 +62,7 @@ impl LinkEditor { let url_editor = ctx.add_typed_action_view(|ctx| { let mut editor = EditorView::single_line(editor_options.clone(), ctx); - editor.set_placeholder_text("Link (web or file)", ctx); + editor.set_placeholder_text(i18n::t("notebooks.link_editor.link_placeholder"), ctx); editor }); @@ -251,7 +251,7 @@ impl View for LinkEditor { let mut link_button = appearance .ui_builder() .button(ButtonVariant::Accent, self.apply_link_mouse_state.clone()) - .with_centered_text_label("Apply link".to_string()); + .with_centered_text_label(i18n::t("notebooks.link_editor.apply_link")); // Disable the link button if either of the editors are empty. if !self.is_valid(app) { diff --git a/app/src/notebooks/editor/mod.rs b/app/src/notebooks/editor/mod.rs index dd12e86ae5..475f1c8d2e 100644 --- a/app/src/notebooks/editor/mod.rs +++ b/app/src/notebooks/editor/mod.rs @@ -131,15 +131,16 @@ impl BlockType { } } - fn label(self) -> &'static str { + fn label(self) -> String { match self { - BlockType::Text => "Text", - BlockType::Header(size) => size.label(), - BlockType::RunnableCommand => "Command", - BlockType::UnorderedList => "Bulleted list", - BlockType::OrderedList => "Numbered list", - BlockType::Code => "Code", - BlockType::TaskList => "To-do list", + BlockType::Text => i18n::t("notebooks.block.text"), + BlockType::Header(size) => i18n::t("notebooks.block.heading") + .replace("{level}", &usize::from(size).to_string()), + BlockType::RunnableCommand => i18n::t("notebooks.block.command"), + BlockType::UnorderedList => i18n::t("notebooks.block.bulleted_list"), + BlockType::OrderedList => i18n::t("notebooks.block.numbered_list"), + BlockType::Code => i18n::t("notebooks.block.code"), + BlockType::TaskList => i18n::t("notebooks.block.todo_list"), } } } diff --git a/app/src/notebooks/editor/model.rs b/app/src/notebooks/editor/model.rs index 795959726d..ede858cbb8 100644 --- a/app/src/notebooks/editor/model.rs +++ b/app/src/notebooks/editor/model.rs @@ -1399,7 +1399,7 @@ impl NotebooksEditorModel { if let Some(command) = child_model.executable_command(ctx) { ctx.emit_a11y_content(AccessibilityContent::new_without_help( - format!("Selected workflow: {command}"), + i18n::t("notebooks.a11y.selected_workflow").replace("{command}", command.as_ref()), WarpA11yRole::TextareaRole, )); } @@ -1753,12 +1753,21 @@ impl NotebooksEditorModel { /// Accessibility content for toggling an inline style. pub fn style_toggle_a11y(&self, style: BufferTextStyle) -> ActionAccessibilityContent { - let action = if self.is_style_active(style) { - "off" + let state = if self.is_style_active(style) { + i18n::t("notebooks.a11y.style.off") } else { - "on" + i18n::t("notebooks.a11y.style.on") }; - let text = format!("{style:?} {action}"); + let style = match style { + BufferTextStyle::Weight(_) => i18n::t("notebooks.a11y.style.bold"), + BufferTextStyle::Italic => i18n::t("notebooks.a11y.style.italic"), + BufferTextStyle::Underline => i18n::t("notebooks.a11y.style.underline"), + BufferTextStyle::InlineCode => i18n::t("notebooks.a11y.style.inline_code"), + BufferTextStyle::StrikeThrough => i18n::t("notebooks.a11y.style.strikethrough"), + }; + let text = i18n::t("notebooks.a11y.style_toggle") + .replace("{style}", &style) + .replace("{state}", &state); ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( text, WarpA11yRole::UserAction, diff --git a/app/src/notebooks/editor/notebook_command.rs b/app/src/notebooks/editor/notebook_command.rs index 9351ec7cf2..a197282e68 100644 --- a/app/src/notebooks/editor/notebook_command.rs +++ b/app/src/notebooks/editor/notebook_command.rs @@ -671,7 +671,7 @@ impl RunnableCommandModel for NotebookCommand { .with_active_styles(active_highlight) .with_tooltip(move || { tooltip_builder_raw - .tool_tip("Raw".to_string()) + .tool_tip(i18n::t("notebooks.command.raw")) .build() .finish() }) @@ -696,7 +696,7 @@ impl RunnableCommandModel for NotebookCommand { .with_active_styles(active_highlight) .with_tooltip(move || { tooltip_builder_rendered - .tool_tip("Rendered".to_string()) + .tool_tip(i18n::t("notebooks.command.rendered")) .build() .finish() }) @@ -737,7 +737,7 @@ impl RunnableCommandModel for NotebookCommand { self.mouse_state_handles .mermaid_fullscreen_button_state .clone(), - "Open full screen", + i18n::t("notebooks.command.open_full_screen"), None, ) .on_click(move |ctx, app, _| { @@ -771,7 +771,7 @@ impl RunnableCommandModel for NotebookCommand { appearance, Icon::Copy, self.mouse_state_handles.copy_button_state.clone(), - "Copy", + i18n::t("common.copy"), custom_action_to_display(CustomAction::Copy), ) .on_click(move |ctx, app, _| { @@ -798,7 +798,7 @@ impl RunnableCommandModel for NotebookCommand { appearance, Icon::TerminalInput, self.mouse_state_handles.insert_button_state.clone(), - "Run in terminal", + i18n::t("notebooks.command.run_in_terminal"), NotebookKeybindings::as_ref(ctx).run_commands_keybinding(), ) .on_click(move |ctx, app, _| { diff --git a/app/src/notebooks/editor/omnibar.rs b/app/src/notebooks/editor/omnibar.rs index 021eb2ab38..3910e9e5f7 100644 --- a/app/src/notebooks/editor/omnibar.rs +++ b/app/src/notebooks/editor/omnibar.rs @@ -441,14 +441,18 @@ impl TypedActionView for Omnibar { .style_toggle_a11y(BufferTextStyle::InlineCode), OmnibarAction::ConvertBlock(style) => { ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( - format!("Convert to {}", BlockType::from(style).label()), + i18n::t("notebooks.a11y.convert_to") + .replace("{block_type}", &BlockType::from(style).label()), WarpA11yRole::UserAction, )) } OmnibarAction::OpenLinkEditor => ActionAccessibilityContent::from_debug(), - OmnibarAction::UnstyleLink => ActionAccessibilityContent::Custom( - AccessibilityContent::new_without_help("Remove link", WarpA11yRole::UserAction), - ), + OmnibarAction::UnstyleLink => { + ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( + i18n::t("notebooks.a11y.remove_link"), + WarpA11yRole::UserAction, + )) + } } } } diff --git a/app/src/notebooks/editor/view.rs b/app/src/notebooks/editor/view.rs index 707d0140df..9853b9c9af 100644 --- a/app/src/notebooks/editor/view.rs +++ b/app/src/notebooks/editor/view.rs @@ -246,14 +246,14 @@ pub fn init(app: &mut AppContext) { FixedBinding::custom( CustomAction::Copy, EditorViewAction::Copy, - "Copy", + i18n::t("common.copy"), id!("RichTextEditorView") & !id!("IMEOpen"), ), // Bindings for paste require the StandardAction and CustomAction binding to work on all platforms. FixedBinding::custom( CustomAction::Paste, EditorViewAction::Paste, - "Paste", + i18n::t("common.paste"), id!("RichTextEditorView") & !id!("IMEOpen"), ), FixedBinding::standard( @@ -265,32 +265,32 @@ pub fn init(app: &mut AppContext) { FixedBinding::custom( CustomAction::WindowsPaste, EditorViewAction::Paste, - "Paste", + i18n::t("common.paste"), id!("RichTextEditorView") & !id!("IMEOpen"), ), #[cfg(windows)] FixedBinding::custom( CustomAction::WindowsCopy, EditorViewAction::Copy, - "Copy", + i18n::t("common.copy"), id!("RichTextEditorView") & !id!("IMEOpen"), ), FixedBinding::custom( CustomAction::Cut, EditorViewAction::Cut, - "Cut", + i18n::t("common.cut"), id!("RichTextEditorView") & !id!("IMEOpen"), ), FixedBinding::custom( CustomAction::Undo, EditorViewAction::Undo, - "Undo", + i18n::t("common.undo"), id!("RichTextEditorView") & !id!("IMEOpen"), ), FixedBinding::custom( CustomAction::Redo, EditorViewAction::Redo, - "Redo", + i18n::t("common.redo"), id!("RichTextEditorView") & !id!("IMEOpen"), ), ]); @@ -299,14 +299,14 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "editor_view:deselect_command", - "De-select shell commands", + i18n::t("notebooks.binding.deselect_shell_commands"), EditorViewAction::ExitCommandSelection, ) .with_context_predicate(id!("RichTextEditorView") & id!("HasCommandSelection")) .with_key_binding("escape"), EditableBinding::new( "editor_view:select_command", - "Select shell command at cursor", + i18n::t("notebooks.binding.select_shell_command_at_cursor"), EditorViewAction::SelectCommandAtCursor, ) .with_context_predicate( @@ -317,21 +317,21 @@ pub fn init(app: &mut AppContext) { .with_key_binding("escape"), EditableBinding::new( "editor_view:select_previous_command", - "Select previous command", + i18n::t("notebooks.binding.select_previous_command"), EditorViewAction::CommandUp, ) .with_context_predicate(id!("RichTextEditorView")) .with_key_binding("cmdorctrl-up"), EditableBinding::new( "editor_view:select_next_command", - "Select next command", + i18n::t("notebooks.binding.select_next_command"), EditorViewAction::CommandDown, ) .with_context_predicate(id!("RichTextEditorView")) .with_key_binding("cmdorctrl-down"), EditableBinding::new( "editor_view:run_commands", - "Run selected commands", + i18n::t("notebooks.binding.run_selected_commands"), EditorViewAction::RunSelectedCommands, ) .with_context_predicate(id!("RichTextEditorView") & id!("CanExecuteShellCommands")) @@ -364,28 +364,28 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "editor_view:toggle_debug_mode", - "Toggle rich-text debug mode", + i18n::t("notebooks.binding.toggle_debug_mode"), EditorViewAction::ToggleDebugMode, ) .with_context_predicate(id!("RichTextEditorView")) .with_enabled(debug_notebooks_enabled), EditableBinding::new( "editor_view:debug_copy_buffer", - "Copy rich-text buffer", + i18n::t("notebooks.binding.copy_rich_text_buffer"), EditorViewAction::DebugCopyBuffer, ) .with_context_predicate(id!("RichTextEditorView")) .with_enabled(debug_notebooks_enabled), EditableBinding::new( "editor_view:debug_copy_selection", - "Copy rich-text selection", + i18n::t("notebooks.binding.copy_rich_text_selection"), EditorViewAction::DebugCopySelection, ) .with_context_predicate(id!("RichTextEditorView")) .with_enabled(debug_notebooks_enabled), EditableBinding::new( "editor_view:log_state", - "Log editor state", + i18n::t("notebooks.binding.log_editor_state"), EditorViewAction::DebugLogState, ) .with_context_predicate(id!("RichTextEditorView")) @@ -396,7 +396,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "editor_view:move_backward_one_word", - "Move Backward One Word", + i18n::t("notebooks.binding.move_backward_one_word"), EditorViewAction::MoveBackwardsByWord, ) .with_context_predicate(text_entry.clone()) @@ -404,7 +404,7 @@ pub fn init(app: &mut AppContext) { .with_linux_or_windows_key_binding("ctrl-left"), EditableBinding::new( "editor_view:move_forward_one_word", - "Move Forward One Word", + i18n::t("notebooks.binding.move_forward_one_word"), EditorViewAction::MoveForwardsByWord, ) .with_context_predicate(text_entry.clone()) @@ -412,38 +412,42 @@ pub fn init(app: &mut AppContext) { .with_linux_or_windows_key_binding("ctrl-right"), EditableBinding::new( "editor_view:move_forward_one_word", - "Move forward one word", + i18n::t("notebooks.binding.move_forward_one_word"), EditorViewAction::MoveForwardsByWord, ) .with_context_predicate(text_entry.clone()) .with_key_binding("meta-f"), EditableBinding::new( "editor_view:move_backward_one_word", - "Move backward one word", + i18n::t("notebooks.binding.move_backward_one_word"), EditorViewAction::MoveBackwardsByWord, ) .with_context_predicate(text_entry.clone()) .with_key_binding("meta-b"), - EditableBinding::new("editor_view:up", "Move cursor up", EditorViewAction::MoveUp) - .with_context_predicate(text_entry.clone()) - .with_key_binding("ctrl-p"), + EditableBinding::new( + "editor_view:up", + i18n::t("notebooks.binding.move_cursor_up"), + EditorViewAction::MoveUp, + ) + .with_context_predicate(text_entry.clone()) + .with_key_binding("ctrl-p"), EditableBinding::new( "editor_view:down", - "Move cursor down", + i18n::t("notebooks.binding.move_cursor_down"), EditorViewAction::MoveDown, ) .with_context_predicate(text_entry.clone()) .with_key_binding("ctrl-n"), EditableBinding::new( "editor_view:left", - "Move cursor left", + i18n::t("notebooks.binding.move_cursor_left"), EditorViewAction::MoveLeft, ) .with_context_predicate(text_entry.clone()) .with_key_binding("ctrl-b"), EditableBinding::new( "editor_view:right", - "Move cursor right", + i18n::t("notebooks.binding.move_cursor_right"), EditorViewAction::MoveRight, ) .with_context_predicate(text_entry.clone()) @@ -452,7 +456,7 @@ pub fn init(app: &mut AppContext) { // This doesn't reuse the move_to_line_start naming from the terminal input editor to // distinguish between soft-wrapped line and hard-wrapped line (paragraph) movement. "editor_view:move_to_paragraph_start", - "Move to start of paragraph", + i18n::t("notebooks.binding.move_to_paragraph_start"), EditorViewAction::MoveToParagraphStart, ) .with_context_predicate(text_entry.clone()) @@ -460,7 +464,7 @@ pub fn init(app: &mut AppContext) { .with_mac_key_binding("ctrl-a"), EditableBinding::new( "editor_view:home", - "Home", + i18n::t("notebooks.binding.home"), EditorViewAction::MoveToLineStart, ) .with_context_predicate(text_entry.clone()) @@ -468,50 +472,54 @@ pub fn init(app: &mut AppContext) { .with_linux_or_windows_key_binding("home"), EditableBinding::new( "editor_view:move_to_paragraph_end", - "Move to end of paragraph", + i18n::t("notebooks.binding.move_to_paragraph_end"), EditorViewAction::MoveToParagraphEnd, ) .with_context_predicate(text_entry.clone()) .with_mac_key_binding("ctrl-e"), - EditableBinding::new("editor_view:end", "End", EditorViewAction::MoveToLineEnd) - .with_context_predicate(text_entry.clone()) - .with_mac_key_binding("cmd-right") - .with_linux_or_windows_key_binding("end"), + EditableBinding::new( + "editor_view:end", + i18n::t("notebooks.binding.end"), + EditorViewAction::MoveToLineEnd, + ) + .with_context_predicate(text_entry.clone()) + .with_mac_key_binding("cmd-right") + .with_linux_or_windows_key_binding("end"), ]); // Editable selection keybindings: app.register_editable_bindings([ EditableBinding::new( "editor_view:select_left_by_word", - "Select one word to the left", + i18n::t("notebooks.binding.select_one_word_left"), EditorViewAction::SelectBackwardsByWord, ) .with_context_predicate(text_entry.clone()) .with_key_binding("shift-meta-B"), EditableBinding::new( "editor_view:select_right_by_word", - "Select one word to the right", + i18n::t("notebooks.binding.select_one_word_right"), EditorViewAction::SelectForwardsByWord, ) .with_context_predicate(text_entry.clone()) .with_key_binding("shift-meta-F"), EditableBinding::new( "editor_view:select_left", - "Select one character to the left", + i18n::t("notebooks.binding.select_one_character_left"), EditorViewAction::SelectLeft, ) .with_context_predicate(text_entry.clone()) .with_key_binding("shift-ctrl-B"), EditableBinding::new( "editor_view:select_right", - "Select one character to the right", + i18n::t("notebooks.binding.select_one_character_right"), EditorViewAction::SelectRight, ) .with_context_predicate(text_entry.clone()) .with_key_binding("shift-ctrl-F"), EditableBinding::new( "editor_view:select_up", - "Select up", + i18n::t("notebooks.binding.select_up"), EditorViewAction::SelectUp, ) .with_context_predicate(text_entry.clone()) @@ -520,28 +528,28 @@ pub fn init(app: &mut AppContext) { .with_mac_key_binding("shift-ctrl-P"), EditableBinding::new( "editor_view:select_down", - "Select down", + i18n::t("notebooks.binding.select_down"), EditorViewAction::SelectDown, ) .with_context_predicate(text_entry.clone()) .with_mac_key_binding("shift-ctrl-N"), EditableBinding::new( "editor_view:select_all", - "Select all", + i18n::t("common.select_all"), EditorViewAction::SelectAll, ) .with_context_predicate(text_entry.clone()) .with_custom_action(CustomAction::SelectAll), EditableBinding::new( "editor:select_to_paragraph_start", - "Select to start of paragraph", + i18n::t("notebooks.binding.select_to_paragraph_start"), EditorViewAction::SelectToParagraphStart, ) .with_context_predicate(text_entry.clone()) .with_mac_key_binding("shift-ctrl-A"), EditableBinding::new( "editor:select_to_paragraph_end", - "Select to end of paragraph", + i18n::t("notebooks.binding.select_to_paragraph_end"), EditorViewAction::SelectToParagraphEnd, ) .with_context_predicate(text_entry.clone()) @@ -549,7 +557,7 @@ pub fn init(app: &mut AppContext) { // `shift-end` is registered on all platforms for this action. EditableBinding::new( "editor_view:select_to_line_end", - "Select To Line End", + i18n::t("notebooks.binding.select_to_line_end"), EditorViewAction::SelectToLineEnd, ) .with_context_predicate(text_entry.clone()) @@ -557,7 +565,7 @@ pub fn init(app: &mut AppContext) { // `end` is registered on all platforms for this action. EditableBinding::new( "editor_view:select_to_line_start", - "Select To Line Start", + i18n::t("notebooks.binding.select_to_line_start"), EditorViewAction::SelectToLineStart, ) .with_context_predicate(text_entry.clone()) @@ -581,24 +589,28 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "editor_view:backspace", - "Remove the previous character", + i18n::t("notebooks.binding.remove_previous_character"), EditorViewAction::Backspace, ) .with_context_predicate(text_entry.clone()) .with_key_binding("ctrl-h"), - EditableBinding::new("editor_view:delete", "Delete", EditorViewAction::Delete) - .with_context_predicate(text_entry.clone()) - .with_key_binding("ctrl-d"), + EditableBinding::new( + "editor_view:delete", + i18n::t("common.delete"), + EditorViewAction::Delete, + ) + .with_context_predicate(text_entry.clone()) + .with_key_binding("ctrl-d"), EditableBinding::new( "editor_view:cut_word_left", - "Cut word left", + i18n::t("notebooks.binding.cut_word_left"), EditorViewAction::CutWordLeft, ) .with_context_predicate(text_entry.clone()) .with_key_binding("ctrl-w"), EditableBinding::new( "editor:delete_word_left", - "Delete word left", + i18n::t("notebooks.binding.delete_word_left"), EditorViewAction::DeleteWordLeft, ) .with_context_predicate(text_entry.clone()) @@ -606,14 +618,14 @@ pub fn init(app: &mut AppContext) { .with_linux_or_windows_key_binding("ctrl-backspace"), EditableBinding::new( "editor_view:cut_word_right", - "Cut word right", + i18n::t("notebooks.binding.cut_word_right"), EditorViewAction::CutWordRight, ) .with_context_predicate(text_entry.clone()) .with_key_binding("alt-d"), EditableBinding::new( "editor:delete_word_right", - "Delete word right", + i18n::t("notebooks.binding.delete_word_right"), EditorViewAction::DeleteWordRight, ) .with_context_predicate(text_entry.clone()) @@ -621,13 +633,13 @@ pub fn init(app: &mut AppContext) { .with_linux_or_windows_key_binding("ctrl-delete"), EditableBinding::new( "editor_view:cut_all_left", - "Cut all left", + i18n::t("notebooks.binding.cut_all_left"), EditorViewAction::CutLineLeft, ) .with_context_predicate(text_entry.clone()), EditableBinding::new( "editor_view:delete_all_left", - "Delete all left", + i18n::t("notebooks.binding.delete_all_left"), EditorViewAction::DeleteLineLeft, ) .with_context_predicate(text_entry.clone()) @@ -638,14 +650,14 @@ pub fn init(app: &mut AppContext) { .with_linux_or_windows_key_binding("ctrl-y"), EditableBinding::new( "editor_view:cut_all_right", - "Cut all right", + i18n::t("notebooks.binding.cut_all_right"), EditorViewAction::CutLineRight, ) .with_context_predicate(text_entry.clone()) .with_key_binding("ctrl-k"), EditableBinding::new( "editor_view:delete_all_right", - "Delete all right", + i18n::t("notebooks.binding.delete_all_right"), EditorViewAction::DeleteLineRight, ) .with_context_predicate(text_entry.clone()) @@ -659,14 +671,14 @@ pub fn init(app: &mut AppContext) { // editable for users who are used to something else. EditableBinding::new( "editor:edit_link", - "Create or edit link", + i18n::t("notebooks.binding.create_or_edit_link"), EditorViewAction::CreateOrEditLink, ) .with_context_predicate(text_entry.clone()) .with_key_binding("cmdorctrl-k"), EditableBinding::new( "editor_view:inline_code", - "Toggle inline code styling", + i18n::t("notebooks.binding.toggle_inline_code"), EditorViewAction::InlineCode, ) .with_context_predicate(text_entry.clone()) @@ -677,14 +689,14 @@ pub fn init(app: &mut AppContext) { .with_mac_key_binding("cmd-shift-C"), EditableBinding::new( "editor_view:strikethrough", - "Toggle strikethrough styling", + i18n::t("notebooks.binding.toggle_strikethrough"), EditorViewAction::StrikeThrough, ) .with_context_predicate(text_entry.clone()) .with_key_binding("cmdorctrl-shift-X"), EditableBinding::new( "editor_view:underline", - "Toggle underline styling", + i18n::t("notebooks.binding.toggle_underline"), EditorViewAction::Underline, ) .with_context_predicate(text_entry.clone()) @@ -695,7 +707,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "editor:find", - "Find in Notebook", + i18n::t("notebooks.binding.find_in_notebook"), EditorViewAction::ShowFindBar, ) .with_key_binding(cmd_or_ctrl_shift("f")) @@ -703,25 +715,25 @@ pub fn init(app: &mut AppContext) { .with_context_predicate(id!("RichTextEditorView")), EditableBinding::new( "editor:next_find_match", - "Focus next match", + i18n::t("notebooks.binding.focus_next_match"), FindBarAction::FocusNextMatch, ) .with_context_predicate(id!("FindBar")), EditableBinding::new( "editor:previous_find_match", - "Focus previous match", + i18n::t("notebooks.binding.focus_previous_match"), FindBarAction::FocusPreviousMatch, ) .with_context_predicate(id!("FindBar")), EditableBinding::new( "editor:toggle_regex_find", - "Toggle regular expression search", + i18n::t("notebooks.binding.toggle_regex_search"), FindBarAction::ToggleRegex, ) .with_context_predicate(id!("FindBar")), EditableBinding::new( "editor:toggle_case_sensitive_find", - "Toggle case-sensitive search", + i18n::t("notebooks.binding.toggle_case_sensitive_search"), FindBarAction::ToggleCaseSensitive, ) .with_context_predicate(id!("FindBar")), @@ -2390,7 +2402,10 @@ impl RichTextEditorView { .ui_builder() .copy_button(12., self.mouse_states.copy_link_mouse_handle.clone()) .with_tooltip(move || { - ui_builder.tool_tip("Copy link".to_owned()).build().finish() + ui_builder + .tool_tip(i18n::t("common.copy_link")) + .build() + .finish() }) .build() .on_click(|ctx, _, _| ctx.dispatch_typed_action(EditorViewAction::CopyLink)) @@ -2442,7 +2457,7 @@ impl RichTextEditorView { ButtonVariant::Text, self.mouse_states.edit_link_mouse_handle.clone(), ) - .with_text_label("Edit".to_string()) + .with_text_label(i18n::t("common.edit")) .build() .on_click(|ctx, _, _| ctx.dispatch_typed_action(EditorViewAction::EditLink)) .finish(), @@ -2510,11 +2525,10 @@ impl RichTextEditorView { let path = selected_file_path.path.clone(); let line_and_column_num = selected_file_path.line_and_column_num; let primary_text = if path.is_dir() { - "Open folder" + i18n::t("common.open_folder") } else { - "Open file" - } - .to_string(); + i18n::t("common.open_file") + }; let show_open_in_warp = should_show_open_in_warp_link(&path, ctx); let path_for_primary = path.clone(); let modifier = directly_open_link_keybinding_string(); @@ -2528,14 +2542,14 @@ impl RichTextEditorView { force_open_in_warp: false, }); }), - detail: Some(format!("[{modifier} Click]")), + detail: Some(format!("[{modifier} {}]", i18n::t("common.click"))), mouse_state: self.file_path_mouse_states.open_file_handle.clone(), }]; if show_open_in_warp { let path_for_warp = path.clone(); links.push(TooltipLink { - text: "Open in Warp".to_string(), + text: i18n::t("common.open_in_warp"), on_click: Box::new(move |ctx: &mut EventContext| { ctx.dispatch_typed_action(EditorViewAction::OpenFile { path: path_for_warp.clone(), @@ -2841,7 +2855,7 @@ impl TypedActionView for RichTextEditorView { } let window_id = ctx.window_id(); crate::workspace::ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::default(String::from("Link copied")); + let toast = DismissibleToast::default(i18n::t("common.link_copied")); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); ctx.notify(); @@ -3132,7 +3146,8 @@ impl TypedActionView for RichTextEditorView { } EditorViewAction::Paste | EditorViewAction::MiddleClickPaste => { ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( - format!("Pasting: {}", ctx.clipboard().read().plain_text), + i18n::t("notebooks.a11y.pasting") + .replace("{text}", &ctx.clipboard().read().plain_text), WarpA11yRole::UserAction, )) } @@ -3145,27 +3160,36 @@ impl TypedActionView for RichTextEditorView { | EditorViewAction::Indent | EditorViewAction::Unindent | EditorViewAction::Tab => ActionAccessibilityContent::from_debug(), - EditorViewAction::ShiftTab => ActionAccessibilityContent::Custom( - AccessibilityContent::new_without_help("Shift-tab", WarpA11yRole::UserAction), - ), + EditorViewAction::ShiftTab => { + ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( + i18n::t("notebooks.a11y.shift_tab"), + WarpA11yRole::UserAction, + )) + } EditorViewAction::EditLink | EditorViewAction::CreateOrEditLink => { ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( - "Edit Link", + i18n::t("notebooks.a11y.edit_link"), + WarpA11yRole::UserAction, + )) + } + EditorViewAction::CopyLink => { + ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( + i18n::t("notebooks.a11y.copy_link"), WarpA11yRole::UserAction, )) } - EditorViewAction::CopyLink => ActionAccessibilityContent::Custom( - AccessibilityContent::new_without_help("Copy Link", WarpA11yRole::UserAction), - ), EditorViewAction::OpenTooltipLink(link) => { ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( - format!("Open link: {}", **link), + i18n::t("notebooks.a11y.open_link").replace("{link}", &(**link).to_string()), WarpA11yRole::UserAction, )) } EditorViewAction::SecondaryLinkAction(link) => { let content = link.secondary_action().map_or_else( - || format!("Secondary click on {}", **link), + || { + i18n::t("notebooks.a11y.secondary_click_on") + .replace("{link}", &(**link).to_string()) + }, |action| action.accessibility_content.into_owned(), ); ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( @@ -3175,66 +3199,82 @@ impl TypedActionView for RichTextEditorView { } EditorViewAction::DeleteLineLeft => { ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( - "Delete line left", + i18n::t("notebooks.a11y.delete_line_left"), WarpA11yRole::UserAction, )) } EditorViewAction::DeleteLineRight => { ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( - "Delete line right", + i18n::t("notebooks.a11y.delete_line_right"), WarpA11yRole::UserAction, )) } EditorViewAction::DeleteWordLeft => { ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( - "Delete word left", + i18n::t("notebooks.a11y.delete_word_left"), WarpA11yRole::UserAction, )) } EditorViewAction::DeleteWordRight => { ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( - "Delete word right", + i18n::t("notebooks.a11y.delete_word_right"), WarpA11yRole::UserAction, )) } - EditorViewAction::CutLineLeft => ActionAccessibilityContent::Custom( - AccessibilityContent::new_without_help("Cut line left", WarpA11yRole::UserAction), - ), - EditorViewAction::CutLineRight => ActionAccessibilityContent::Custom( - AccessibilityContent::new_without_help("Cut line right", WarpA11yRole::UserAction), - ), - EditorViewAction::CutWordLeft => ActionAccessibilityContent::Custom( - AccessibilityContent::new_without_help("Cut word left", WarpA11yRole::UserAction), - ), - EditorViewAction::CutWordRight => ActionAccessibilityContent::Custom( - AccessibilityContent::new_without_help("Cut word right", WarpA11yRole::UserAction), - ), + EditorViewAction::CutLineLeft => { + ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( + i18n::t("notebooks.a11y.cut_line_left"), + WarpA11yRole::UserAction, + )) + } + EditorViewAction::CutLineRight => { + ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( + i18n::t("notebooks.a11y.cut_line_right"), + WarpA11yRole::UserAction, + )) + } + EditorViewAction::CutWordLeft => { + ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( + i18n::t("notebooks.a11y.cut_word_left"), + WarpA11yRole::UserAction, + )) + } + EditorViewAction::CutWordRight => { + ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( + i18n::t("notebooks.a11y.cut_word_right"), + WarpA11yRole::UserAction, + )) + } EditorViewAction::ShowCharacterPalette => { ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( - "Show character palette", + i18n::t("notebooks.a11y.show_character_palette"), + WarpA11yRole::UserAction, + )) + } + EditorViewAction::ShowFindBar => { + ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( + i18n::t("notebooks.a11y.show_find_bar"), WarpA11yRole::UserAction, )) } - EditorViewAction::ShowFindBar => ActionAccessibilityContent::Custom( - AccessibilityContent::new_without_help("Show find bar", WarpA11yRole::UserAction), - ), EditorViewAction::OpenBlockInsertionMenu => { ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( - "Open block-insertion menu", + i18n::t("notebooks.a11y.open_block_insertion_menu"), WarpA11yRole::UserAction, )) } EditorViewAction::OpenEmbeddedObjectSearch => { ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( - "Open embedded object search menu", + i18n::t("notebooks.a11y.open_embedded_object_search_menu"), WarpA11yRole::UserAction, )) } EditorViewAction::InsertBlock(block_type) => { ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( - format!("Insert {} block", BlockType::from(block_type).label()), + i18n::t("notebooks.a11y.insert_block") + .replace("{block_type}", &BlockType::from(block_type).label()), WarpA11yRole::UserAction, )) } @@ -3260,24 +3300,28 @@ impl TypedActionView for RichTextEditorView { .style_toggle_a11y(BufferTextStyle::StrikeThrough), EditorViewAction::ExitCommandSelection => { ActionAccessibilityContent::Custom(AccessibilityContent::new( - "De-select command", - "Switch from selecting commands to selecting text", + i18n::t("notebooks.a11y.deselect_command"), + i18n::t("notebooks.a11y.deselect_command_help"), WarpA11yRole::UserAction, )) } EditorViewAction::CodeBlockTypeSelectedAtOffset { code_block_type, .. } => ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( - format!("Change code block language to {code_block_type}"), + i18n::t("notebooks.a11y.change_code_block_language") + .replace("{language}", &code_block_type.to_string()), WarpA11yRole::UserAction, )), - EditorViewAction::CopyTextToClipboard { .. } => ActionAccessibilityContent::Custom( - AccessibilityContent::new_without_help("Copy code block", WarpA11yRole::UserAction), - ), + EditorViewAction::CopyTextToClipboard { .. } => { + ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( + i18n::t("notebooks.a11y.copy_code_block"), + WarpA11yRole::UserAction, + )) + } EditorViewAction::ToggleTaskList(_) => { // TODO(ben): Is it useful to include the text and/or on/off state here? ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( - "Toggle task list", + i18n::t("notebooks.a11y.toggle_task_list"), WarpA11yRole::UserAction, )) } diff --git a/app/src/notebooks/file/mod.rs b/app/src/notebooks/file/mod.rs index f1950eefb7..cdfe7b049b 100644 --- a/app/src/notebooks/file/mod.rs +++ b/app/src/notebooks/file/mod.rs @@ -219,14 +219,14 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "notebookview:focus_terminal_input", - "Focus Terminal Input from File", + i18n::t("notebooks.binding.focus_terminal_input_from_file"), FileNotebookAction::FocusTerminalInput, ) .with_context_predicate(id!("FileNotebookView")) .with_key_binding(cmd_or_ctrl_shift("l")), EditableBinding::new( "notebookview:reload_file", - "Reload file", + i18n::t("notebooks.binding.reload_file"), FileNotebookAction::ReloadFile, ) .with_context_predicate(id!("FileNotebookView")), @@ -314,7 +314,7 @@ impl FileNotebookView { .as_ref() .map(|location| location.name.clone()) .or_else(|| self.file_state.display_name()) - .unwrap_or_else(|| "Untitled".to_string()) + .unwrap_or_else(|| i18n::t("common.untitled")) } pub fn focus(&self, ctx: &mut ViewContext) { @@ -744,9 +744,10 @@ impl FileNotebookView { EditorViewEvent::Focused => ctx.emit(FileNotebookEvent::Pane(PaneEvent::FocusSelf)), EditorViewEvent::RunWorkflow(workflow) => { let workflow_type = workflow.named_workflow(|| { - self.location - .as_ref() - .map(|location| format!("Command from {}", location.name)) + self.location.as_ref().map(|location| { + i18n::t("notebooks.workflow.command_from") + .replace("{source}", &location.name) + }) }); let source = workflow.source.unwrap_or(WorkflowSource::Notebook { notebook_id: None, @@ -870,7 +871,10 @@ impl FileNotebookView { .with_child( appearance .ui_builder() - .paragraph(format!("Could not read {}", source.display_name())) + .paragraph( + i18n::t("notebooks.file.could_not_read") + .replace("{source}", &source.display_name()), + ) .with_style(self.state_style(appearance)) .build() .finish(), @@ -883,7 +887,7 @@ impl FileNotebookView { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::TextFirst, - "Try again".to_string(), + i18n::t("common.try_again"), Icon::Refresh.to_warpui_icon(error_text_color), MainAxisSize::Min, MainAxisAlignment::Center, @@ -909,7 +913,9 @@ impl FileNotebookView { Align::new( appearance .ui_builder() - .paragraph(format!("Loading {}...", source.display_name())) + .paragraph( + i18n::t("notebooks.file.loading").replace("{source}", &source.display_name()), + ) .with_style(self.state_style(appearance)) .build() .finish(), @@ -922,7 +928,7 @@ impl FileNotebookView { Align::new( appearance .ui_builder() - .paragraph("Missing source file".to_string()) + .paragraph(i18n::t("notebooks.file.missing_source_file")) .with_style(self.state_style(appearance)) .build() .finish(), @@ -975,7 +981,7 @@ impl View for FileNotebookView { fn accessibility_contents(&self, _ctx: &AppContext) -> Option { Some(AccessibilityContent::new_without_help( - format!("{} notebook", self.title()), + i18n::t("notebooks.a11y.notebook_with_title").replace("{title}", &self.title()), WarpA11yRole::TextRole, )) } @@ -1129,7 +1135,7 @@ impl BackingView for FileNotebookView { if let Some(SourceFile::FileBased { .. }) = self.file_state.source() { actions.push(MenuItem::Separator); actions.push( - MenuItemFields::new("Refresh file") + MenuItemFields::new(i18n::t("notebooks.file.refresh_file")) .with_on_select_action(FileNotebookAction::ReloadFile) .into_item(), ); @@ -1139,13 +1145,13 @@ impl BackingView for FileNotebookView { // The markdown rendered/raw toggle is always visible in the pane header, so we don't // duplicate it in the overflow menu. Keep "Open in editor" available for local files. actions.push( - MenuItemFields::new("Open in editor") + MenuItemFields::new(i18n::t("notebooks.file.open_in_editor")) .with_on_select_action(FileNotebookAction::OpenInEditor) .into_item(), ); actions.extend([ MenuItem::Separator, - MenuItemFields::new("Copy file path") + MenuItemFields::new(i18n::t("common.copy_file_path")) .with_on_select_action(FileNotebookAction::CopyFilePath) .into_item(), ]); @@ -1301,7 +1307,7 @@ impl FileLocation { let name = path .file_name() .map(|name| name.to_string_lossy().into_owned()) - .unwrap_or_else(|| "Unnamed".to_string()); + .unwrap_or_else(|| i18n::t("common.untitled")); Self { breadcrumbs, name } } diff --git a/app/src/notebooks/link.rs b/app/src/notebooks/link.rs index 4aa2114089..120a40f8c8 100644 --- a/app/src/notebooks/link.rs +++ b/app/src/notebooks/link.rs @@ -49,16 +49,16 @@ impl LinkTarget { pub fn secondary_action(&self) -> Option { match self { LinkTarget::LocalDirectory { .. } => Some(SecondaryAction { - label: "New session".into(), - tooltip: Some("Open a new terminal session in this directory".into()), - accessibility_content: "Open in terminal session".into(), + label: i18n::t("notebooks.link.new_session").into(), + tooltip: Some(i18n::t("notebooks.link.new_session_tooltip").into()), + accessibility_content: i18n::t("notebooks.link.open_in_terminal_session").into(), }), LinkTarget::LocalFile { is_markdown: true, .. } => Some(SecondaryAction { - label: "Open in editor".into(), + label: i18n::t("notebooks.file.open_in_editor").into(), tooltip: None, - accessibility_content: "Edit Markdown file".into(), + accessibility_content: i18n::t("notebooks.link.edit_markdown_file").into(), }), LinkTarget::Url(_) | LinkTarget::LocalFile { .. } => None, } @@ -404,9 +404,13 @@ impl From for ResolveError { impl fmt::Display for ResolveError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - ResolveError::FileNotFound => f.write_str("File not found"), - ResolveError::MissingContext => f.write_str("No base directory"), - ResolveError::Unknown => f.write_str("Broken file link"), + ResolveError::FileNotFound => { + f.write_str(&i18n::t("notebooks.link.error.file_not_found")) + } + ResolveError::MissingContext => { + f.write_str(&i18n::t("notebooks.link.error.no_base_directory")) + } + ResolveError::Unknown => f.write_str(&i18n::t("notebooks.link.error.broken_file_link")), } } } diff --git a/app/src/notebooks/notebook.rs b/app/src/notebooks/notebook.rs index 01f1d46ba2..be8c40c984 100644 --- a/app/src/notebooks/notebook.rs +++ b/app/src/notebooks/notebook.rs @@ -102,12 +102,6 @@ const EDIT_BUTTON_MARGIN: f32 = 6.; const HEADER_MARGIN: f32 = 15.; const BANNER_VERTICAL_MARGIN: f32 = 10.; -const CONFLICT_RESOLUTION_MESSAGE: &str = - "This notebook could not be saved because changes were made while you were editing. Please copy your work and refresh."; -const REFRESH_BUTTON_TEXT: &str = "Refresh"; - -const FEATURE_NOT_AVAILABLE_MESSAGE: &str = "This notebook could not be saved to the server because the feature is temporarily unavailable. The changes are saved locally. Please retry later."; - /// The frequency at which we check for modifications and save the notebook to the server. This /// lets us trade off how quickly edits appear on other clients with the load on the server for RTC /// object updates. @@ -137,7 +131,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "notebookview:increase_font_size", - "Increase notebook font size", + i18n::t("notebooks.binding.increase_font_size"), NotebookAction::IncreaseFontSize, ) .with_context_predicate(id!("NotebookView") & id!("NotMatchNotebookToMonospaceSize")) @@ -145,7 +139,7 @@ pub fn init(app: &mut AppContext) { .with_key_binding("cmdorctrl-="), EditableBinding::new( "notebookview:decrease_font_size", - "Decrease notebook font size", + i18n::t("notebooks.binding.decrease_font_size"), NotebookAction::DecreaseFontSize, ) .with_context_predicate(id!("NotebookView") & id!("NotMatchNotebookToMonospaceSize")) @@ -153,7 +147,7 @@ pub fn init(app: &mut AppContext) { .with_key_binding("cmdorctrl--"), EditableBinding::new( "notebookview:reset_font_size", - "Reset notebook font size", + i18n::t("notebooks.binding.reset_font_size"), NotebookAction::ResetFontSize, ) .with_context_predicate(id!("NotebookView") & id!("NotMatchNotebookToMonospaceSize")) @@ -161,7 +155,7 @@ pub fn init(app: &mut AppContext) { .with_custom_action(CustomAction::ResetFontSize), EditableBinding::new( "notebookview:focus_terminal_input", - "Focus Terminal Input from Notebook", + i18n::t("notebooks.binding.focus_terminal_input_from_notebook"), NotebookAction::FocusTerminalInput, ) .with_context_predicate(id!("NotebookView")) @@ -176,14 +170,14 @@ pub fn init(app: &mut AppContext) { FixedBinding::custom( CustomAction::IncreaseFontSize, NotebookAction::IncreaseFontSize, - "Increase font size", + i18n::t("notebooks.binding.increase_font_size_short"), id!("NotebookView") & id!("NotMatchNotebookToMonospaceSize"), ) .with_group(bindings::BindingGroup::Settings.as_str()), FixedBinding::custom( CustomAction::DecreaseFontSize, NotebookAction::DecreaseFontSize, - "Decrease font size", + i18n::t("notebooks.binding.decrease_font_size_short"), id!("NotebookView") & id!("NotMatchNotebookToMonospaceSize"), ) .with_group(bindings::BindingGroup::Settings.as_str()), @@ -355,7 +349,7 @@ impl NotebookView { ..Default::default() }; let mut editor = EditorView::single_line(options, ctx); - editor.set_placeholder_text("Untitled", ctx); + editor.set_placeholder_text(i18n::t("common.untitled"), ctx); editor }); ctx.subscribe_to_view(&title, |notebook, _, event, ctx| { @@ -475,7 +469,7 @@ impl NotebookView { fn title_from_editor(title_editor: &ViewHandle, app: &AppContext) -> String { let mut title = title_editor.as_ref(app).buffer_text(app); if title.is_empty() { - title.push_str("Untitled"); + title.push_str(&i18n::t("common.untitled")); } title } @@ -798,10 +792,9 @@ impl NotebookView { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "This notebook cannot be saved because its content contains secrets" - .to_string(), - ), + DismissibleToast::error(i18n::t( + "notebooks.error.content_contains_secrets", + )), window_id, ctx, ); @@ -1385,13 +1378,16 @@ impl NotebookView { match space { Space::Personal => { menu_items.extend(team_spaces.iter().map(|space| { - MenuItemFields::new(format!("Move to {}", space.name(ctx))) - .with_on_select_action(NotebookAction::MoveToSpace { - cloud_object_type_and_id: cloud_object_type, - new_space: *space, - }) - .with_icon(Icon::Move) - .into_item() + MenuItemFields::new( + i18n::t("notebooks.move_to_space") + .replace("{space}", &space.name(ctx)), + ) + .with_on_select_action(NotebookAction::MoveToSpace { + cloud_object_type_and_id: cloud_object_type, + new_space: *space, + }) + .with_icon(Icon::Move) + .into_item() })); } Space::Shared => {} // TODO: Revisit these menu items with sharing in mind @@ -1402,7 +1398,7 @@ impl NotebookView { if let Some(ai_document_id) = self.active_notebook_data.as_ref(ctx).ai_document_id(ctx) { menu_items.push( - MenuItemFields::new("Attach to active session") + MenuItemFields::new(i18n::t("common.attach_to_active_session")) .with_on_select_action(NotebookAction::AttachPlanAsContext(ai_document_id)) .with_icon(icons::Icon::Paperclip) .into_item(), @@ -1412,7 +1408,7 @@ impl NotebookView { // Add "Copy Link" to menu if let Some(link) = self.notebook_link(ctx) { menu_items.push( - MenuItemFields::new("Copy link") + MenuItemFields::new(i18n::t("common.copy_link")) .with_on_select_action(NotebookAction::CopyLink(link)) .with_icon(icons::Icon::Link) .into_item(), @@ -1429,7 +1425,7 @@ impl NotebookView { if let Some(link) = self.notebook_link(ctx) { if let Ok(url) = Url::parse(&link) { menu_items.push( - MenuItemFields::new("Open on Desktop") + MenuItemFields::new(i18n::t("common.open_on_desktop")) .with_on_select_action(NotebookAction::OpenLinkOnDesktop(url)) .with_icon(icons::Icon::Laptop) .into_item(), @@ -1441,7 +1437,7 @@ impl NotebookView { // Add "Duplicate" to menu if active_notebook_data.space(ctx) != Some(Space::Shared) { menu_items.push( - MenuItemFields::new("Duplicate") + MenuItemFields::new(i18n::t("common.duplicate")) .with_on_select_action(NotebookAction::Duplicate) .with_icon(icons::Icon::Duplicate) .into_item(), @@ -1451,7 +1447,7 @@ impl NotebookView { #[cfg(feature = "local_fs")] { menu_items.push( - MenuItemFields::new("Export") + MenuItemFields::new(i18n::t("common.export")) .with_on_select_action(NotebookAction::Export) .with_icon(icons::Icon::Download) .into_item(), @@ -1463,7 +1459,7 @@ impl NotebookView { && (!FeatureFlag::SharedWithMe.is_enabled() || access_level.can_trash()) { menu_items.push( - MenuItemFields::new("Trash") + MenuItemFields::new(i18n::t("common.trash")) .with_on_select_action(NotebookAction::Trash) .with_icon(icons::Icon::Trash) .into_item(), @@ -1726,10 +1722,7 @@ impl NotebookView { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "This notebook cannot be saved because its title contains secrets" - .to_string(), - ), + DismissibleToast::error(i18n::t("notebooks.error.title_contains_secrets")), window_id, ctx, ); @@ -1804,8 +1797,9 @@ impl NotebookView { fn run_notebook_workflow(&self, workflow: &NotebookWorkflow, ctx: &mut ViewContext) { // If the notebook workflow was anonymous, synthesize metadata for it. - let workflow_type = - workflow.named_workflow(|| Some(format!("Command from {}", self.title(ctx)))); + let workflow_type = workflow.named_workflow(|| { + Some(i18n::t("notebooks.workflow.command_from").replace("{source}", &self.title(ctx))) + }); let notebook_id = self.server_id(ctx); let source = workflow.source.unwrap_or_else(|| { @@ -1909,9 +1903,9 @@ impl NotebookView { let mut stack = Stack::new(); let text = if deleted { - "You no longer have access to this notebook" + i18n::t("notebooks.no_longer_access") } else { - "Notebook was moved to trash" + i18n::t("notebooks.moved_to_trash") }; stack.add_child( Align::new( @@ -1968,11 +1962,11 @@ impl NotebookView { ) .with_tooltip(move || { ui_builder - .tool_tip("Restore notebook from trash".to_string()) + .tool_tip(i18n::t("notebooks.restore_from_trash")) .build() .finish() }) - .with_text_label("Restore".to_string()) + .with_text_label(i18n::t("common.restore")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(NotebookAction::Untrash) @@ -1998,14 +1992,11 @@ impl NotebookView { ) .with_tooltip(move || { ui_builder - .tool_tip( - "Copy notebook contents into your personal workspace" - .to_string(), - ) + .tool_tip(i18n::t("notebooks.copy_to_personal_tooltip")) .build() .finish() }) - .with_text_label("Copy to Personal".to_string()) + .with_text_label(i18n::t("notebooks.copy_to_personal")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(NotebookAction::CopyToPersonal) @@ -2046,8 +2037,10 @@ impl NotebookView { .ui_builder() .wrappable_text( match sync_error { - NotebookSyncError::FeatureNotAvailable => FEATURE_NOT_AVAILABLE_MESSAGE, - NotebookSyncError::InConflict => CONFLICT_RESOLUTION_MESSAGE, + NotebookSyncError::FeatureNotAvailable => { + i18n::t("notebooks.sync.feature_not_available") + } + NotebookSyncError::InConflict => i18n::t("notebooks.sync.conflict"), }, true, ) @@ -2083,11 +2076,11 @@ impl NotebookView { ) .with_tooltip(move || { ui_builder - .tool_tip("Copy notebook contents to your clipboard".to_string()) + .tool_tip(i18n::t("notebooks.copy_all_tooltip")) .build() .finish() }) - .with_text_label("Copy All".to_string()) + .with_text_label(i18n::t("notebooks.copy_all")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(NotebookAction::CopyToClipboard) @@ -2117,11 +2110,11 @@ impl NotebookView { ) .with_tooltip(move || { ui_builder - .tool_tip("Refresh notebook".to_string()) + .tool_tip(i18n::t("notebooks.refresh")) .build() .finish() }) - .with_text_label(REFRESH_BUTTON_TEXT.to_string()) + .with_text_label(i18n::t("common.refresh")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action( @@ -2160,7 +2153,7 @@ impl View for NotebookView { fn accessibility_contents(&self, ctx: &AppContext) -> Option { Some(AccessibilityContent::new_without_help( - format!("{} notebook", self.title(ctx)), + i18n::t("notebooks.a11y.notebook_with_title").replace("{title}", &self.title(ctx)), WarpA11yRole::TextRole, )) } @@ -2309,7 +2302,7 @@ impl TypedActionView for NotebookView { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::success("Link copied to clipboard".to_string()), + DismissibleToast::success(i18n::t("common.link_copied_to_clipboard")), window_id, ctx, ); diff --git a/app/src/notebooks/notebook/details_bar.rs b/app/src/notebooks/notebook/details_bar.rs index 6a6955cbab..3d920fce81 100644 --- a/app/src/notebooks/notebook/details_bar.rs +++ b/app/src/notebooks/notebook/details_bar.rs @@ -130,7 +130,7 @@ impl DetailsBar { let ui_builder = appearance.ui_builder().clone(); edit_button = edit_button.with_tooltip(move || { ui_builder - .tool_tip("Sign in to edit".to_string()) + .tool_tip(i18n::t("common.sign_in_to_edit")) .build() .finish() }); @@ -167,13 +167,13 @@ impl DetailsBar { match editor.state { EditorState::None => appearance .ui_builder() - .span("Viewing") + .span(i18n::t("common.viewing")) .with_style(base_text_styles) .build() .finish(), EditorState::CurrentUser => appearance .ui_builder() - .span("Editing") + .span(i18n::t("common.editing")) .with_style(base_text_styles) .build() .finish(), @@ -181,7 +181,7 @@ impl DetailsBar { let editor = editor_display_name(editor.email.as_deref(), app); appearance .ui_builder() - .span(format!("{editor} is editing")) + .span(i18n::t("notebooks.editor_is_editing").replace("{editor}", &editor)) .with_style(base_text_styles) .with_highlights( (0..editor.chars().count()).collect(), @@ -202,7 +202,7 @@ fn editor_display_name(email: Option<&str>, app: &AppContext) -> String { Some(email) => UserProfiles::as_ref(app) .displayable_identifier_for_email(email) .unwrap_or_else(|| email.to_string()), - None => "Other user".to_string(), + None => i18n::t("common.other_user"), } } diff --git a/app/src/notebooks/notebook/details_bar_tests.rs b/app/src/notebooks/notebook/details_bar_tests.rs index 841c439c6a..f06ca7d070 100644 --- a/app/src/notebooks/notebook/details_bar_tests.rs +++ b/app/src/notebooks/notebook/details_bar_tests.rs @@ -31,8 +31,11 @@ fn test_editor_display_name() { }); app.read(|ctx| { - // If there's no known editor, default to "Other user"; - assert_eq!(&editor_display_name(None, ctx), "Other user"); + // If there's no known editor, default to the localized generic label. + assert_eq!( + &editor_display_name(None, ctx), + &i18n::t("common.other_user") + ); // If the editor doesn't have a profile, default to their email. assert_eq!( diff --git a/app/src/pane_group/mod.rs b/app/src/pane_group/mod.rs index 1219a8e7c8..1725375da8 100644 --- a/app/src/pane_group/mod.rs +++ b/app/src/pane_group/mod.rs @@ -3010,10 +3010,13 @@ impl PaneGroup { let user_default_shell_changed_banner = ctx.add_typed_action_view(|_| { Banner::::new_permanently_dismissible( BannerTextContent::formatted_text(vec![ - FormattedTextFragment::plain_text( - "Warp doesn't currently support your default shell, falling back to zsh. ", + FormattedTextFragment::plain_text(i18n::t( + "pane_group.default_shell_unsupported", + )), + FormattedTextFragment::hyperlink( + i18n::t("common.learn_more"), + WARP_SHELL_COMPATIBILITY_DOCS, ), - FormattedTextFragment::hyperlink("Learn more", WARP_SHELL_COMPATIBILITY_DOCS), ]), ) }); diff --git a/app/src/pane_group/pane/code_diff_pane.rs b/app/src/pane_group/pane/code_diff_pane.rs index b972e91fa7..de47eb6cdd 100644 --- a/app/src/pane_group/pane/code_diff_pane.rs +++ b/app/src/pane_group/pane/code_diff_pane.rs @@ -22,7 +22,7 @@ impl CodeDiffPane { let pane_configuration = ctx.add_model(|_ctx| { let mut config = PaneConfiguration::new(""); // This title must be set with .set_title and not just ::new() to ensure that the tab renders immediately. - config.set_title("Requested Edit", _ctx); + config.set_title(i18n::t("ai.code_diff.requested_edit"), _ctx); config }); diff --git a/app/src/pane_group/pane/get_started_view.rs b/app/src/pane_group/pane/get_started_view.rs index 9d9c8ace1e..7290625ed1 100644 --- a/app/src/pane_group/pane/get_started_view.rs +++ b/app/src/pane_group/pane/get_started_view.rs @@ -32,7 +32,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( "workspace:new_tab", - "Terminal session", + i18n::t("pane_group.binding.terminal_session"), GetStartedAction::TerminalSession, ) .with_context_predicate(id!("GetStartedView")) @@ -60,7 +60,8 @@ pub struct GetStartedView { impl GetStartedView { pub fn new(ctx: &mut ViewContext) -> Self { - let pane_configuration = ctx.add_model(|_ctx| PaneConfiguration::new("Get started")); + let pane_configuration = + ctx.add_model(|_ctx| PaneConfiguration::new(i18n::t("pane_group.get_started.title"))); let project_buttons = ctx.add_typed_action_view(ProjectButtons::new); ctx.subscribe_to_view(&project_buttons, Self::handle_project_buttons_event); @@ -223,7 +224,7 @@ impl GetStartedView { .finish(), appearance .ui_builder() - .paragraph("Welcome to Warp") + .paragraph(i18n::t("pane_group.get_started.welcome")) .with_style(UiComponentStyles { font_size: Some(20.), ..Default::default() @@ -233,7 +234,7 @@ impl GetStartedView { Container::new( appearance .ui_builder() - .paragraph("The Agentic Development Environment") + .paragraph(i18n::t("pane_group.get_started.tagline")) .with_style(UiComponentStyles { font_size: Some(14.), font_family_id: Some(appearance.monospace_font_family()), @@ -361,7 +362,7 @@ impl BackingView for GetStartedView { _ctx: &view::HeaderRenderContext<'_>, _app: &AppContext, ) -> view::HeaderContent { - view::HeaderContent::simple("Get started") + view::HeaderContent::simple(i18n::t("pane_group.get_started.title")) } fn set_focus_handle(&mut self, focus_handle: PaneFocusHandle, _ctx: &mut ViewContext) { diff --git a/app/src/pane_group/pane/local_harness_launch.rs b/app/src/pane_group/pane/local_harness_launch.rs index 161a8dadc9..122ed9cd1d 100644 --- a/app/src/pane_group/pane/local_harness_launch.rs +++ b/app/src/pane_group/pane/local_harness_launch.rs @@ -72,14 +72,10 @@ pub(super) fn normalize_local_child_harness(harness_type: &str) -> Option) -> Result<(), String> { match shell_type { Some(ShellType::Bash) | Some(ShellType::Zsh) | Some(ShellType::Fish) => Ok(()), - Some(ShellType::PowerShell) => Err( - "Local child harnesses currently require bash, zsh, or fish; PowerShell is not supported." - .to_string(), - ), - None => Err( - "Local child harnesses currently require a detected bash, zsh, or fish session." - .to_string(), - ), + Some(ShellType::PowerShell) => { + Err(i18n::t("pane_group.local_child.powershell_unsupported")) + } + None => Err(i18n::t("pane_group.local_child.no_supported_shell")), } } @@ -175,9 +171,9 @@ pub(super) async fn prepare_local_harness_child_launch( let Some(harness) = normalize_local_child_harness(&harness_type) else { let harness_name = harness_type.trim(); return Err(if harness_name.is_empty() { - "Local child harness type is missing.".to_string() + i18n::t("pane_group.local_child.missing_harness_type") } else { - format!("Unsupported local child harness '{harness_name}'.") + i18n::t("pane_group.local_child.unsupported_harness").replace("{harness}", harness_name) }); }; if let Some(message) = local_harness_product_disabled_message(harness) { @@ -191,10 +187,8 @@ pub(super) async fn prepare_local_harness_child_launch( let working_dir = startup_directory .or_else(|| std::env::current_dir().ok()) .ok_or_else(|| { - format!( - "Could not resolve a working directory for the local {} child.", - harness.display_name() - ) + i18n::t("pane_group.local_child.missing_working_directory") + .replace("{harness}", harness.display_name()) })?; let HarnessKind::ThirdParty(third_party_harness) = harness_kind(harness).map_err(|error: AgentDriverError| error.to_string())? diff --git a/app/src/pane_group/pane/terminal_pane.rs b/app/src/pane_group/pane/terminal_pane.rs index b6a56e0981..c0b8b55a37 100644 --- a/app/src/pane_group/pane/terminal_pane.rs +++ b/app/src/pane_group/pane/terminal_pane.rs @@ -1791,15 +1791,16 @@ fn launch_local_no_harness_child( parent_conversation_id, request_id: Some(request_id), orchestration_harness: Some(Harness::Oz), - error_message: - "Failed to create a hidden pane for the local child agent." - .to_string(), + error_message: i18n::t( + "pane_group.local_child.failed_create_hidden_agent_pane", + ), }, ctx, ); } } Err(error) => { + let error = error.to_string(); let _ = create_error_child_agent_conversation( group, ErrorChildAgentConversationRequest { @@ -1808,7 +1809,8 @@ fn launch_local_no_harness_child( parent_conversation_id, request_id: Some(request_id), orchestration_harness: Some(Harness::Oz), - error_message: format!("Failed to create local child task: {error}"), + error_message: i18n::t("pane_group.local_child.failed_create_task") + .replace("{error}", &error), }, ctx, ); @@ -1940,9 +1942,9 @@ fn launch_local_harness_child( parent_conversation_id, request_id: Some(request_id), orchestration_harness: Some(orchestration_harness), - error_message: - "Failed to create a hidden pane for the local child harness." - .to_string(), + error_message: i18n::t( + "pane_group.local_child.failed_create_hidden_harness_pane", + ), }, ctx, ); diff --git a/app/src/pane_group/pane/view/header/mod.rs b/app/src/pane_group/pane/view/header/mod.rs index 2196087aec..e2a8334729 100644 --- a/app/src/pane_group/pane/view/header/mod.rs +++ b/app/src/pane_group/pane/view/header/mod.rs @@ -145,9 +145,9 @@ impl PaneHeader

{ let shared_content = SharedPaneContent::new(ctx); let toolbelt_feature_popup = ctx.add_view(|_| { - FeaturePopup::new_feature(NewFeaturePopupLabel::FromString( - "Open files and review code diffs".to_string(), - )) + FeaturePopup::new_feature(NewFeaturePopupLabel::FromString(i18n::t( + "pane_group.header.toolbelt_feature_popup", + ))) }); ctx.subscribe_to_view(&toolbelt_feature_popup, move |me, _, event, ctx| { me.handle_toolbelt_feature_popup_event(event, ctx); diff --git a/app/src/pane_group/pane/view/header/sharing.rs b/app/src/pane_group/pane/view/header/sharing.rs index 7b1551e854..eac2ab3fac 100644 --- a/app/src/pane_group/pane/view/header/sharing.rs +++ b/app/src/pane_group/pane/view/header/sharing.rs @@ -19,11 +19,6 @@ use crate::server::telemetry::SharingDialogSource; use crate::ui_components::buttons::{icon_button, icon_button_with_color}; use crate::ui_components::icons::Icon; -const UNSHARABLE_CONVERSATION_TOOLTIP: &str = - "This conversation cannot be shared because it is not \ - stored in the cloud.\nTo sync to cloud and share, enable the setting under Settings > Privacy, \ - and then make another request."; - /// Pane header component for sharing the pane contents. pub struct SharedPaneContent { sharing_dialog: ViewHandle, @@ -196,16 +191,16 @@ impl PaneHeader

{ ( Icon::Share, false, - UNSHARABLE_CONVERSATION_TOOLTIP.to_string(), + i18n::t("pane_group.header.unsharable_conversation_tooltip"), ) } else if editability.can_edit() { ( Icon::Share, self.open_overlay == OpenOverlay::SharingDialog, - "Share".to_string(), + i18n::t("common.share"), ) } else { - (Icon::Link, false, "Copy link".to_string()) + (Icon::Link, false, i18n::t("common.copy_link")) }; let ui_builder = appearance.ui_builder().clone(); @@ -259,9 +254,10 @@ impl PaneHeader

{ element.add_child(primary_button); if !editability.can_edit() { - let mut tooltip_text = String::from("Read-only"); + let mut tooltip_text = i18n::t("pane_group.header.read_only"); if matches!(editability, ContentEditability::RequiresLogin) { - tooltip_text.push_str(". Sign in to edit"); + tooltip_text.push_str(". "); + tooltip_text.push_str(&i18n::t("common.sign_in_to_edit")); } let ui_builder = appearance.ui_builder().clone(); diff --git a/app/src/pane_group/pane/view/mod.rs b/app/src/pane_group/pane/view/mod.rs index 906ed5d44e..6745072cca 100644 --- a/app/src/pane_group/pane/view/mod.rs +++ b/app/src/pane_group/pane/view/mod.rs @@ -36,7 +36,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( "pane:share_pane_contents", - "Share pane", + i18n::t("pane_group.binding.share_pane"), PaneAction::ShareContents, ) .with_custom_action(CustomAction::SharePaneContents) diff --git a/app/src/pane_group/pane/welcome_view.rs b/app/src/pane_group/pane/welcome_view.rs index 0a4e2d4449..7cfe53c61a 100644 --- a/app/src/pane_group/pane/welcome_view.rs +++ b/app/src/pane_group/pane/welcome_view.rs @@ -35,7 +35,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "workspace:new_tab", - "Terminal session", + i18n::t("pane_group.binding.terminal_session"), WelcomeViewAction::CreateTerminalSession, ) .with_context_predicate(id!("WelcomeView")) @@ -44,7 +44,7 @@ pub fn init(app: &mut AppContext) { .with_enabled(|| ContextFlag::CreateNewSession.is_enabled()), EditableBinding::new( "welcome_view:open_project", - "Add repository", + i18n::t("pane_group.binding.add_repository"), WelcomeViewAction::OpenProject, ) .with_context_predicate(id!("WelcomeView")) @@ -71,7 +71,8 @@ pub struct WelcomeView { impl WelcomeView { pub fn new(startup_directory: Option, ctx: &mut ViewContext) -> Self { - let pane_configuration = ctx.add_model(|_ctx| PaneConfiguration::new("New tab")); + let pane_configuration = + ctx.add_model(|_ctx| PaneConfiguration::new(i18n::t("pane_group.welcome.title"))); let window_id = ctx.window_id(); let view_id = ctx.view_id(); let palette = ctx.add_typed_action_view(|ctx| { @@ -290,7 +291,7 @@ impl BackingView for WelcomeView { _ctx: &view::HeaderRenderContext<'_>, _app: &AppContext, ) -> view::HeaderContent { - view::HeaderContent::simple("New tab") + view::HeaderContent::simple(i18n::t("pane_group.welcome.title")) } fn set_focus_handle(&mut self, focus_handle: PaneFocusHandle, _ctx: &mut ViewContext) { diff --git a/app/src/prompt/editor_modal.rs b/app/src/prompt/editor_modal.rs index 33242c1747..9386209ce2 100644 --- a/app/src/prompt/editor_modal.rs +++ b/app/src/prompt/editor_modal.rs @@ -51,11 +51,6 @@ const DROPDOWN_WIDTH: f32 = 72.; const MODAL_CONTENT_FONT_SIZE: f32 = 14.; const CHECKBOX_SIZE: f32 = 16.; -const MODAL_TITLE: &str = "Edit prompt"; -const WARP_PROMPT_SECTION_HEADER: &str = "Warp terminal prompt"; -const SHELL_PROMPT_SECTION_HEADER: &str = "Shell prompt (PS1)"; -const RESTORE_DEFAULT_BUTTON: &str = "Restore default"; - pub fn init(app: &mut AppContext) { use warpui::keymap::macros::*; @@ -479,7 +474,7 @@ impl EditorModal { fn render_header(&self, appearance: &Appearance) -> Box { appearance .ui_builder() - .span(MODAL_TITLE.to_string()) + .span(i18n::t("prompt.editor.title")) .with_style(UiComponentStyles { font_size: Some(MODAL_TITLE_FONT_SIZE), font_weight: Some(warpui::fonts::Weight::Bold), @@ -570,7 +565,7 @@ impl EditorModal { |_state| { appearance .ui_builder() - .span(RESTORE_DEFAULT_BUTTON.to_string()) + .span(i18n::t("common.restore_default")) .with_style(UiComponentStyles { font_size: Some(MODAL_CONTENT_FONT_SIZE), ..Default::default() @@ -594,7 +589,7 @@ impl EditorModal { fn render_same_line_prompt_section(&self, appearance: &Appearance) -> Box { let label = appearance .ui_builder() - .span("Same line prompt".to_string()) + .span(i18n::t("prompt.editor.same_line_prompt")) .with_style(UiComponentStyles { font_size: Some(MODAL_CONTENT_FONT_SIZE), ..Default::default() @@ -632,7 +627,7 @@ impl EditorModal { Container::new( appearance .ui_builder() - .span("Separator".to_string()) + .span(i18n::t("prompt.editor.separator")) .with_style(UiComponentStyles { font_size: Some(MODAL_CONTENT_FONT_SIZE), ..Default::default() @@ -670,7 +665,7 @@ impl EditorModal { .with_child( appearance .ui_builder() - .span(WARP_PROMPT_SECTION_HEADER.to_string()) + .span(i18n::t("prompt.editor.warp_terminal_prompt")) .with_style(UiComponentStyles { font_size: Some(MODAL_CONTENT_FONT_SIZE), font_weight: Some(warpui::fonts::Weight::Semibold), @@ -719,7 +714,7 @@ impl EditorModal { let header = appearance .ui_builder() - .span(SHELL_PROMPT_SECTION_HEADER.to_string()) + .span(i18n::t("prompt.editor.shell_prompt")) .with_style(UiComponentStyles { font_size: Some(MODAL_CONTENT_FONT_SIZE), font_weight: Some(warpui::fonts::Weight::Semibold), @@ -779,7 +774,7 @@ impl EditorModal { fn render_buttons(&self, appearance: &Appearance) -> Box { let cancel_button = self.render_primary_button( - "Cancel".to_string(), + i18n::t("common.cancel"), ButtonVariant::Outlined, false, self.mouse_state_handles.cancel_button_handle.clone(), @@ -794,7 +789,7 @@ impl EditorModal { || (matches!(self.prompt_type, PromptType::Warp) && self.chip_configurator.used_chips.is_empty()); let save_button = self.render_primary_button( - "Save changes".to_string(), + i18n::t("prompt.editor.save_changes"), ButtonVariant::Accent, save_disabled, self.mouse_state_handles.save_button_handle.clone(), diff --git a/app/src/quit_warning/mod.rs b/app/src/quit_warning/mod.rs index 8d1a9f6a60..cea0407c28 100644 --- a/app/src/quit_warning/mod.rs +++ b/app/src/quit_warning/mod.rs @@ -1,5 +1,3 @@ -use std::fmt::Write; - use itertools::Itertools; use settings::ToggleableSetting as _; use warpui::modals::{AlertDialogWithCallbacks, AppModalCallback, ModalButton}; @@ -289,48 +287,62 @@ impl<'a> UnsavedStateSummary<'a> { let mut info_text_lines = Vec::::new(); let scope_suffix = match self.scope { - QuitScope::Tabs(ref tabs) if tabs.len() == 1 => " in this tab.", - QuitScope::Window(_) => " in this window.", - QuitScope::Pane { .. } => " in this pane.", - QuitScope::App | QuitScope::Tabs(_) | QuitScope::EditorTab { .. } => ".", + QuitScope::Tabs(ref tabs) if tabs.len() == 1 => i18n::t("quit_warning.scope.this_tab"), + QuitScope::Window(_) => i18n::t("quit_warning.scope.this_window"), + QuitScope::Pane { .. } => i18n::t("quit_warning.scope.this_pane"), + QuitScope::App | QuitScope::Tabs(_) | QuitScope::EditorTab { .. } => { + i18n::t("quit_warning.scope.default") + } }; if self.total_long_running_commands > 0 { - let mut process_info_text = format!( - "You have {} {} running", - self.total_long_running_commands, - pluralize(self.total_long_running_commands, "process", "processes") - ); + let command_count = self.total_long_running_commands.to_string(); + let mut process_info_text = if self.total_long_running_commands == 1 { + i18n::t("quit_warning.process.running_one") + } else { + i18n::t("quit_warning.process.running_many") + } + .replace("{count}", &command_count); + if self.windows_with_long_running_commands > 1 { - let _ = write!( - &mut process_info_text, - " in {} windows", - self.windows_with_long_running_commands - ); + process_info_text.push_str(&i18n::t("quit_warning.process.in_windows").replace( + "{count}", + &self.windows_with_long_running_commands.to_string(), + )); } else if self.tabs_with_long_running_commands > 1 { - let _ = write!( - &mut process_info_text, - " in {} tabs", - self.tabs_with_long_running_commands + process_info_text.push_str( + &i18n::t("quit_warning.process.in_tabs") + .replace("{count}", &self.tabs_with_long_running_commands.to_string()), ); } - process_info_text.push_str(scope_suffix); + process_info_text.push_str(&scope_suffix); info_text_lines.push(process_info_text); } if self.shared_sessions > 0 { - info_text_lines.push(format!( - "You are sharing {} {}{scope_suffix}", - self.shared_sessions, - pluralize(self.shared_sessions, "session", "sessions") - )); + let shared_session_count = self.shared_sessions.to_string(); + let shared_session_text = if self.shared_sessions == 1 { + i18n::t("quit_warning.shared_session.one") + } else { + i18n::t("quit_warning.shared_session.many") + } + .replace("{count}", &shared_session_count) + .replace("{scope}", &scope_suffix); + info_text_lines.push(shared_session_text); } if self.unsaved_code_changes { if let QuitScope::EditorTab { ref file_name, .. } = self.scope { - info_text_lines.push(format!("Do you want to save the changes you made to {}? Your changes will be discarded if you don't save them.", file_name.clone().unwrap_or("this file".to_string()))); + let file_name = file_name + .clone() + .unwrap_or_else(|| i18n::t("quit_warning.file.this_file")); + info_text_lines.push( + i18n::t("quit_warning.unsaved_file.save_changes").replace("{file}", &file_name), + ); } else { - info_text_lines.push(format!("You have unsaved file changes{scope_suffix}")); + info_text_lines.push( + i18n::t("quit_warning.unsaved_file.changes").replace("{scope}", &scope_suffix), + ); } } @@ -396,25 +408,33 @@ impl<'a> QuitWarningDialog<'a> { if let Some(callback) = on_confirm { let confirm_title = match state.scope { - QuitScope::Window(_) | QuitScope::Tabs(_) | QuitScope::Pane { .. } => "Yes, close", - QuitScope::App => "Yes, quit", - _ => "", + QuitScope::Window(_) | QuitScope::Tabs(_) | QuitScope::Pane { .. } => { + i18n::t("quit_warning.button.yes_close") + } + QuitScope::App => i18n::t("quit_warning.button.yes_quit"), + _ => String::new(), }; - buttons.push(ModalButton::for_app(confirm_title.to_string(), callback)); + buttons.push(ModalButton::for_app(confirm_title, callback)); } if let Some(callback) = on_save_changes { - buttons.push(ModalButton::for_app("Save".to_string(), callback)); + buttons.push(ModalButton::for_app( + i18n::t("quit_warning.button.save"), + callback, + )); } if let Some(callback) = on_discard_changes { - buttons.push(ModalButton::for_app("Don't Save".to_string(), callback)); + buttons.push(ModalButton::for_app( + i18n::t("quit_warning.button.dont_save"), + callback, + )); } if let Some(callback) = on_show_processes { if state.total_long_running_commands > 0 { buttons.push(ModalButton::for_app( - "Show running processes".to_string(), + i18n::t("quit_warning.button.show_running_processes"), move |app| { callback(app); }, @@ -423,16 +443,19 @@ impl<'a> QuitWarningDialog<'a> { } if let Some(callback) = on_cancel { - buttons.push(ModalButton::for_app("Cancel".to_string(), callback)); + buttons.push(ModalButton::for_app( + i18n::t("quit_warning.button.cancel"), + callback, + )); } let title = match &state.scope { - QuitScope::Pane { .. } => "Close pane?", - QuitScope::Tabs(tabs) if tabs.len() == 1 => "Close tab?", - QuitScope::Tabs(_) => "Close tabs?", - QuitScope::Window(_) => "Close window?", - QuitScope::App => "Quit Warp?", - QuitScope::EditorTab { .. } => "Save changes?", + QuitScope::Pane { .. } => i18n::t("quit_warning.title.close_pane"), + QuitScope::Tabs(tabs) if tabs.len() == 1 => i18n::t("quit_warning.title.close_tab"), + QuitScope::Tabs(_) => i18n::t("quit_warning.title.close_tabs"), + QuitScope::Window(_) => i18n::t("quit_warning.title.close_window"), + QuitScope::App => i18n::t("quit_warning.title.quit_warp"), + QuitScope::EditorTab { .. } => i18n::t("quit_warning.title.save_changes"), }; AlertDialogWithCallbacks::for_app( @@ -489,14 +512,6 @@ impl<'a> QuitWarningDialog<'a> { } } -fn pluralize<'a>(count: usize, singular: &'a str, plural: &'a str) -> &'a str { - if count > 1 { - plural - } else { - singular - } -} - /// Callback to disable the quit warning modal. fn on_disable_warning_modal(ctx: &mut AppContext) { GeneralSettings::handle(ctx).update(ctx, |general_settings, ctx| { diff --git a/app/src/remote_server/codebase_index_model.rs b/app/src/remote_server/codebase_index_model.rs index cab031708e..0ff632a6c1 100644 --- a/app/src/remote_server/codebase_index_model.rs +++ b/app/src/remote_server/codebase_index_model.rs @@ -718,7 +718,8 @@ impl RemoteCodebaseIndexModel { let mut updated = false; for (key, status) in &mut self.statuses { if key.host == host_label { - let failure_message = "The remote host is currently disconnected.".to_string(); + let failure_message = + i18n::t("remote_server.codebase_index.remote_host_disconnected"); if status.state != RemoteCodebaseIndexState::Unavailable || status.failure_message.as_ref() != Some(&failure_message) { @@ -915,7 +916,7 @@ fn search_availability_for_status( else { return RemoteCodebaseSearchAvailability::Unavailable { remote_path, - message: "The remote codebase index is missing its root hash.".to_string(), + message: i18n::t("remote_server.codebase_index.missing_root_hash"), }; }; RemoteCodebaseSearchAvailability::Ready(RemoteCodebaseSearchContext { @@ -935,7 +936,7 @@ fn search_availability_for_status( message: status .failure_message .clone() - .unwrap_or_else(|| "Remote codebase search is not available.".to_string()), + .unwrap_or_else(|| i18n::t("remote_server.codebase_index.search_unavailable")), }, } } diff --git a/app/src/remote_server/server_model.rs b/app/src/remote_server/server_model.rs index 34ddbb5da2..77787e8a1d 100644 --- a/app/src/remote_server/server_model.rs +++ b/app/src/remote_server/server_model.rs @@ -914,7 +914,8 @@ impl ServerModel { ); not_enabled_codebase_index_status(repo_path.to_string_lossy().to_string()) } else if !manager.can_create_new_indices() { - let failure_message = "Cannot index remote codebase because the maximum number of codebase indexes has been reached.".to_string(); + let failure_message = + i18n::t("remote_server.codebase_index.max_indices_reached"); log::warn!( "[Remote codebase indexing] Daemon cannot start IndexCodebase: repo_path={} reason={failure_message}", repo_path.display() @@ -927,8 +928,7 @@ impl ServerModel { Self::current_codebase_index_status_or_queued(manager, repo_path, ctx) } else { let failure_message = - "Cannot index remote codebase because indexing did not start." - .to_string(); + i18n::t("remote_server.codebase_index.indexing_not_started"); log::warn!( "[Remote codebase indexing] Daemon cannot start IndexCodebase: repo_path={} reason={failure_message}", repo_path.display() @@ -1007,8 +1007,7 @@ impl ServerModel { |_, repo_path, _| { unavailable_codebase_index_status( repo_path.to_string_lossy().to_string(), - "Cannot resync remote codebase because it has not been indexed." - .to_string(), + i18n::t("remote_server.codebase_index.resync_not_indexed"), ) }, ctx, diff --git a/app/src/resource_center/keybindings_page.rs b/app/src/resource_center/keybindings_page.rs index c50b2319b8..64767b703b 100644 --- a/app/src/resource_center/keybindings_page.rs +++ b/app/src/resource_center/keybindings_page.rs @@ -29,7 +29,6 @@ use crate::editor::{ TextOptions, }; use crate::search_bar::SearchBar; -use crate::settings_view; use crate::settings_view::keybindings::{KeybindingChangedEvent, KeybindingChangedNotifier}; use crate::util::bindings::{filter_bindings_including_keystroke, CommandBinding}; use crate::workspace::tab_settings::TabSettings; @@ -90,7 +89,7 @@ impl KeybindingsView { search_editor.update(ctx, |editor, ctx| { editor.clear_buffer_and_reset_undo_stack(ctx); - editor.set_placeholder_text(settings_view::keybindings::SEARCH_PLACEHOLDER, ctx); + editor.set_placeholder_text(i18n::t("settings.keybindings.search_placeholder"), ctx); }); let search_bar = { @@ -346,7 +345,11 @@ impl KeybindingsView { .build() .finish(), ) - .with_child(self.render_text("To toggle this panel".into(), None, appearance)) + .with_child(self.render_text( + i18n::t("resource_center.keybindings.toggle_hint"), + None, + appearance, + )) .with_cross_axis_alignment(CrossAxisAlignment::Center) .finish(); @@ -361,7 +364,7 @@ impl KeybindingsView { appearance .ui_builder() .link( - "here.".into(), + i18n::t("resource_center.keybindings.settings_link"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(WorkspaceAction::ConfigureKeybindingSettings { @@ -384,7 +387,7 @@ impl KeybindingsView { Container::new( column .with_child(self.render_text( - "Go to settings > keyboard shortcuts to configure custom keybindings".into(), + i18n::t("resource_center.keybindings.settings_hint"), None, appearance, )) @@ -412,15 +415,15 @@ impl KeybindingsView { Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch); let title = match section { - KeybindingSection::Essentials => "Essentials", - KeybindingSection::Blocks => "Blocks", - KeybindingSection::InputEditor => "Input Editor", - KeybindingSection::Terminal => "Terminal", - KeybindingSection::Fundamentals => "Fundamentals", + KeybindingSection::Essentials => i18n::t("resource_center.keybindings.essentials"), + KeybindingSection::Blocks => i18n::t("resource_center.keybindings.blocks"), + KeybindingSection::InputEditor => i18n::t("resource_center.keybindings.input_editor"), + KeybindingSection::Terminal => i18n::t("resource_center.keybindings.terminal"), + KeybindingSection::Fundamentals => i18n::t("resource_center.keybindings.fundamentals"), }; let mut section_header = self.render_text( - title.into(), + title, Some(UiComponentStyles { font_color: Some(appearance.theme().active_ui_text_color().into()), font_size: Some(SECTION_HEADER_FONT_SIZE), diff --git a/app/src/resource_center/main_page.rs b/app/src/resource_center/main_page.rs index 64394e1a49..6ddeb32dda 100644 --- a/app/src/resource_center/main_page.rs +++ b/app/src/resource_center/main_page.rs @@ -390,7 +390,7 @@ impl ResourceCenterMainView { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Invite a friend to Warp", + i18n::t("resource_center.invite_friend"), Icon::new(SEND_SVG_PATH, appearance.theme().accent()), MainAxisSize::Max, MainAxisAlignment::Center, @@ -429,7 +429,7 @@ impl ResourceCenterMainView { appearance .ui_builder() - .wrappable_text("Mark all as read", false) + .wrappable_text(i18n::t("resource_center.mark_all_as_read"), false) .with_style(style) .build() .finish() diff --git a/app/src/resource_center/section_views/changelog_section.rs b/app/src/resource_center/section_views/changelog_section.rs index 09ee72240d..0e3fcc2ec1 100644 --- a/app/src/resource_center/section_views/changelog_section.rs +++ b/app/src/resource_center/section_views/changelog_section.rs @@ -30,9 +30,6 @@ struct ChangelogMouseStateHandles { view_changelogs_mouse_state: MouseStateHandle, } -const CHANGELOG_FETCH_ERROR_MSG: &str = "Unable to fetch the latest changelog."; -const CHANGELOG_LOADING_MSG: &str = "Loading..."; - pub struct ChangelogSectionView { changelog_model_handle: ModelHandle, changelog_button_mouse_states: ChangelogMouseStateHandles, @@ -96,10 +93,10 @@ impl ChangelogSectionView { new_features_highlighted_link: Default::default(), improvements_highlighted_link: Default::default(), bug_fixes_highlighted_link: Default::default(), - changelog_fetch_error: create_formatted_text_from_string( - CHANGELOG_FETCH_ERROR_MSG.to_string(), - ), - changelog_loading: create_formatted_text_from_string(CHANGELOG_LOADING_MSG.to_string()), + changelog_fetch_error: create_formatted_text_from_string(i18n::t( + "resource_center.changelog.fetch_error", + )), + changelog_loading: create_formatted_text_from_string(i18n::t("common.loading")), } } @@ -118,22 +115,23 @@ impl ChangelogSectionView { model: &ChangelogModel, appearance: &Appearance, ) { - let title = ChangelogHeader::NewFeatures.to_string(); + let lookup_title = ChangelogHeader::NewFeatures.to_string(); + let display_title = changelog_header_label(ChangelogHeader::NewFeatures); let icon = icons::Icon::Gift; - let Some(markdown) = model.parsed_changelog.get(&title) else { + let Some(markdown) = model.parsed_changelog.get(&lookup_title) else { return; }; // Section Title if self.show_special_new_features_header { content.add_child(render_special_changelog_header( - &title, + &display_title, render_icon(icon, appearance.theme().terminal_colors().normal.red.into()), appearance, )); } else { content.add_child(render_basic_changelog_header( - &title, + &display_title, render_icon( icon, appearance @@ -199,14 +197,15 @@ impl ChangelogSectionView { ]; for (section, icon, link) in additional_sections { - let title = section.to_string(); - let Some(markdown) = model.parsed_changelog.get(&title) else { + let lookup_title = section.to_string(); + let display_title = changelog_header_label(section); + let Some(markdown) = model.parsed_changelog.get(&lookup_title) else { continue; }; // Title content.add_child(render_basic_changelog_header( - &title, + &display_title, render_icon( icon, appearance @@ -228,6 +227,14 @@ fn render_icon(icon: icons::Icon, color: Fill) -> ConstrainedBox { .with_height(16.) } +fn changelog_header_label(header: ChangelogHeader) -> String { + match header { + ChangelogHeader::NewFeatures => i18n::t("resource_center.changelog.new_features"), + ChangelogHeader::Improvements => i18n::t("resource_center.changelog.improvements"), + ChangelogHeader::BugFixes => i18n::t("resource_center.changelog.bug_fixes"), + } +} + fn render_special_changelog_header( title: &str, icon: ConstrainedBox, @@ -366,7 +373,7 @@ impl SectionView for ChangelogSectionView { appearance .ui_builder() .link( - "Read all changelogs".into(), + i18n::t("resource_center.changelog.read_all"), Some("https://docs.warp.dev/changelog".into()), None, self.changelog_button_mouse_states diff --git a/app/src/resource_center/section_views/content_section.rs b/app/src/resource_center/section_views/content_section.rs index b857136f69..b97394061e 100644 --- a/app/src/resource_center/section_views/content_section.rs +++ b/app/src/resource_center/section_views/content_section.rs @@ -94,7 +94,7 @@ impl ContentSectionView { appearance .ui_builder() .link( - item.button_label.to_string(), + i18n::t(item.button_label), Some(item.url.into()), None, mouse_state_handle, @@ -125,7 +125,7 @@ impl ContentSectionView { Container::new( appearance .ui_builder() - .wrappable_text(item.title.to_string(), true) + .wrappable_text(i18n::t(item.title), true) .with_style(UiComponentStyles { font_size: Some(DESCRIPTION_FONT_SIZE), ..Default::default() @@ -142,7 +142,7 @@ impl ContentSectionView { Container::new( appearance .ui_builder() - .wrappable_text(item.description.to_string(), true) + .wrappable_text(i18n::t(item.description), true) .with_style(UiComponentStyles { font_size: Some(DESCRIPTION_FONT_SIZE), font_color: Some(ColorU::from( diff --git a/app/src/resource_center/section_views/feature_section.rs b/app/src/resource_center/section_views/feature_section.rs index fd34020adc..2cb083fc3f 100644 --- a/app/src/resource_center/section_views/feature_section.rs +++ b/app/src/resource_center/section_views/feature_section.rs @@ -34,12 +34,12 @@ pub enum FeatureSection { } impl FeatureSection { - pub fn section_name_string(&self) -> &'static str { + pub fn section_name_string(&self) -> String { match self { - FeatureSection::WhatsNew => "What's New?", - FeatureSection::GettingStarted => "Getting Started", - FeatureSection::MaximizeWarp => "Maximize Warp", - FeatureSection::AdvancedSetup => "Advanced Setup", + FeatureSection::WhatsNew => i18n::t("resource_center.section.whats_new"), + FeatureSection::GettingStarted => i18n::t("resource_center.section.getting_started"), + FeatureSection::MaximizeWarp => i18n::t("resource_center.section.maximize_warp"), + FeatureSection::AdvancedSetup => i18n::t("resource_center.section.advanced_setup"), } } } @@ -209,7 +209,7 @@ impl FeatureSectionView { Container::new( appearance .ui_builder() - .wrappable_text(item.title.to_string(), true) + .wrappable_text(i18n::t(item.title), true) .with_style(UiComponentStyles { font_size: Some(DESCRIPTION_FONT_SIZE), font_color: (Some(title_color.into())), @@ -233,7 +233,7 @@ impl FeatureSectionView { ) -> Box { appearance .ui_builder() - .wrappable_text(item.description.to_string(), true) + .wrappable_text(i18n::t(item.description), true) .with_style(UiComponentStyles { font_size: Some(DESCRIPTION_FONT_SIZE), font_color: Some(color.into()), diff --git a/app/src/resource_center/section_views/mod.rs b/app/src/resource_center/section_views/mod.rs index 68064bd3a3..9d8d1f31c3 100644 --- a/app/src/resource_center/section_views/mod.rs +++ b/app/src/resource_center/section_views/mod.rs @@ -91,7 +91,7 @@ pub trait SectionView { Align::new( appearance .ui_builder() - .wrappable_text(section_name.section_name_string().to_string(), false) + .wrappable_text(section_name.section_name_string(), false) .with_style(UiComponentStyles { font_family_id: Some(appearance.ui_font_family()), font_size: Some(SECTION_HEADER_FONT_SIZE), diff --git a/app/src/resource_center/sections.rs b/app/src/resource_center/sections.rs index f3ecfae958..c14fcc4261 100644 --- a/app/src/resource_center/sections.rs +++ b/app/src/resource_center/sections.rs @@ -18,32 +18,32 @@ pub fn sections(ctx: &mut ViewContext) -> Vec

{ section_name: FeatureSection::GettingStarted, items: vec![ FeatureItem::new( - "Create your first block", - "Run a command to see your command and output grouped.", + "resource_center.feature.create_first_block.title", + "resource_center.feature.create_first_block.description", Tip::Hint(TipHint::CreateBlock), ctx, ), FeatureItem::new( - "Navigate blocks", - "Click to select a block and navigate with arrow keys.", + "resource_center.feature.navigate_blocks.title", + "resource_center.feature.navigate_blocks.description", Tip::Hint(TipHint::BlockSelect), ctx, ), FeatureItem::new( - "Take an action on block", - "Right click on a block to copy/paste, share, more.", + "resource_center.feature.block_action.title", + "resource_center.feature.block_action.description", Tip::Hint(TipHint::BlockAction), ctx, ), FeatureItem::new( - "Open command palette", - "Access all of Warp via the keyboard.", + "resource_center.feature.command_palette.title", + "resource_center.feature.command_palette.description", Tip::Action(TipAction::CommandPalette), ctx, ), FeatureItem::new( - "Set your theme", - "Make Warp your own by choosing a theme.", + "resource_center.feature.theme_picker.title", + "resource_center.feature.theme_picker.description", Tip::Action(TipAction::ThemePicker), ctx, ), @@ -61,22 +61,22 @@ pub fn sections(ctx: &mut ViewContext) -> Vec
{ section_name: FeatureSection::AdvancedSetup, items: vec![ ContentItem { - title: "Use your custom prompt", - description: "Set up Warp to honor your PS1 setting", + title: "resource_center.content.custom_prompt.title", + description: "resource_center.content.custom_prompt.description", url: "https://docs.warp.dev/terminal/appearance/prompt", - button_label: "View documentation", + button_label: "resource_center.content.view_documentation", }, ContentItem { - title: "Integrate Warp with your IDE", - description: "Configure Warp to launch from your most used development tools", + title: "resource_center.content.ide_integration.title", + description: "resource_center.content.ide_integration.description", url: "https://docs.warp.dev/terminal/integrations-and-plugins", - button_label: "View documentation", + button_label: "resource_center.content.view_documentation", }, ContentItem { - title: "How Warp uses Warp", - description: "Learn how Warp's engineering team uses their favorite features", + title: "resource_center.content.how_warp_uses_warp.title", + description: "resource_center.content.how_warp_uses_warp.description", url: "https://www.warp.dev/blog/how-warp-uses-warp", - button_label: "Read article", + button_label: "resource_center.content.read_article", }, ], }; @@ -89,23 +89,23 @@ fn maximize_warp_items(ctx: &mut ViewContext) -> Vec) -> Vec Vec { vec![ CommandBinding::new( "workspace:new_window".into(), - "Open New Window".into(), + i18n::t("resource_center.keybindings.additional.open_new_window"), Some(Keystroke::parse("cmd-n").expect("Valid keystroke")), ), CommandBinding::new( "workspace:hide_warp".into(), - "Hide Warp".into(), + i18n::t("resource_center.keybindings.additional.hide_warp"), Some(Keystroke::parse("cmd-h").expect("Valid keystroke")), ), CommandBinding::new( "workspace:hide_others".into(), - "Hide Others".into(), + i18n::t("resource_center.keybindings.additional.hide_others"), Some(Keystroke::parse("alt-cmd-h").expect("Valid keystroke")), ), CommandBinding::new( "workspace:quit_warp".into(), - "Quit Warp".into(), + i18n::t("resource_center.keybindings.additional.quit_warp"), Some(Keystroke::parse("cmd-q").expect("Valid keystroke")), ), CommandBinding::new( "workspace:minimize".into(), - "Minimize".into(), + i18n::t("resource_center.keybindings.additional.minimize"), Some(Keystroke::parse("cmd-m").expect("Valid keystroke")), ), ] diff --git a/app/src/resource_center/view.rs b/app/src/resource_center/view.rs index 46c6c2c101..2d2a46814a 100644 --- a/app/src/resource_center/view.rs +++ b/app/src/resource_center/view.rs @@ -43,11 +43,11 @@ pub enum ResourceCenterFooterItem { } impl ResourceCenterFooterItem { - pub fn ui_label(&self) -> &'static str { + pub fn ui_label(&self) -> String { match self { - ResourceCenterFooterItem::Docs => "Docs", - ResourceCenterFooterItem::Slack => "Slack", - ResourceCenterFooterItem::Feedback => "Feedback", + ResourceCenterFooterItem::Docs => i18n::t("resource_center.footer.docs"), + ResourceCenterFooterItem::Slack => i18n::t("resource_center.footer.slack"), + ResourceCenterFooterItem::Feedback => i18n::t("resource_center.footer.feedback"), } } @@ -327,12 +327,14 @@ impl ResourceCenterView { let current_page = self.page_views.get(self.current_view_index).map(|x| x.page); let header_text = match current_page { - Some(ResourceCenterPage::Keybindings) => "Keyboard Shortcuts".to_string(), + Some(ResourceCenterPage::Keybindings) => { + i18n::t("resource_center.header.keyboard_shortcuts") + } _ => { if FeatureFlag::AvatarInTabBar.is_enabled() { String::new() } else { - "Warp Essentials".to_string() + i18n::t("resource_center.header.warp_essentials") } } }; @@ -428,7 +430,7 @@ impl ResourceCenterView { let button = appearance .ui_builder() .button(ButtonVariant::Text, mouse_state) - .with_text_label(item.ui_label().to_string()) + .with_text_label(item.ui_label()) .with_style( UiComponentStyles::default().set_padding(Coords::default().left(SCROLLBAR_OFFSET)), ) diff --git a/app/src/root_view.rs b/app/src/root_view.rs index 37db5d149f..eee9a40bef 100644 --- a/app/src/root_view.rs +++ b/app/src/root_view.rs @@ -424,19 +424,19 @@ pub fn init(app: &mut AppContext) { app.register_fixed_bindings([ FixedBinding::empty( - "Hide All Windows", + i18n::t("root_view.binding.hide_all_windows"), RootViewAction::ShowOrHideNonQuakeModeWindows, id!("RootView") & id!(flags::ACTIVATION_HOTKEY_FLAG), ), FixedBinding::empty( - "Show Dedicated Hotkey Window", + i18n::t("root_view.binding.show_dedicated_hotkey_window"), RootViewAction::ToggleQuakeModeWindow, id!("RootView") & id!(flags::QUAKE_MODE_ENABLED_CONTEXT_FLAG) & !id!(flags::QUAKE_WINDOW_OPEN_FLAG), ), FixedBinding::empty( - "Hide Dedicated Hotkey Window", + i18n::t("root_view.binding.hide_dedicated_hotkey_window"), RootViewAction::ToggleQuakeModeWindow, id!("RootView") & id!(flags::QUAKE_MODE_ENABLED_CONTEXT_FLAG) @@ -448,7 +448,7 @@ pub fn init(app: &mut AppContext) { // Register a binding to toggle fullscreen on Linux and Windows. EditableBinding::new( "root_view:toggle_fullscreen", - "Toggle fullscreen", + i18n::t("root_view.binding.toggle_fullscreen"), RootViewAction::ToggleFullscreen, ) .with_group(bindings::BindingGroup::Navigation.as_str()) @@ -897,7 +897,7 @@ fn create_environment(arg: &CreateEnvironmentArg, ctx: &mut AppContext) { workspace .active_tab_pane_group() .update(ctx, |pane_group, ctx| { - pane_group.set_title("Create Environment", ctx); + pane_group.set_title(&i18n::t("root_view.create_environment"), ctx); if let Some(terminal_view) = pane_group.active_session_view(ctx) { terminal_view.update(ctx, |_, ctx| { @@ -930,7 +930,7 @@ fn create_environment_and_run(arg: &CreateEnvironmentArg, ctx: &mut AppContext) workspace .active_tab_pane_group() .update(ctx, |pane_group, ctx| { - pane_group.set_title("Create Environment", ctx); + pane_group.set_title(&i18n::t("root_view.create_environment"), ctx); if let Some(terminal_view) = pane_group.active_session_view(ctx) { terminal_view.update(ctx, |_, ctx| { @@ -1055,7 +1055,7 @@ fn open_warp_drive_object(arg: &OpenWarpDriveObjectArgs, ctx: &mut AppContext) { fn display_object_missing_error_in_window(window_id: WindowId, ctx: &mut AppContext) { crate::workspace::ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::error(String::from("Resource not found or access denied")); + let toast = DismissibleToast::error(i18n::t("root_view.resource_not_found")); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); } @@ -2675,7 +2675,7 @@ impl RootView { workspace .active_tab_pane_group() .update(ctx, |pane_group, ctx| { - pane_group.set_title("Create Environment", ctx); + pane_group.set_title(&i18n::t("root_view.create_environment"), ctx); if let Some(terminal_view) = pane_group.active_session_view(ctx) { terminal_view.update(ctx, |_, ctx| { @@ -2720,7 +2720,7 @@ impl RootView { workspace .active_tab_pane_group() .update(ctx, |pane_group, ctx| { - pane_group.set_title("Create Environment", ctx); + pane_group.set_title(&i18n::t("root_view.create_environment"), ctx); if let Some(terminal_view) = pane_group.active_session_view(ctx) { terminal_view.update(ctx, |_, ctx| { diff --git a/app/src/search/action/search_item.rs b/app/src/search/action/search_item.rs index 69e5a13ad1..af6c9ceb8d 100644 --- a/app/src/search/action/search_item.rs +++ b/app/src/search/action/search_item.rs @@ -148,7 +148,8 @@ impl SearchItem for MatchedBinding { let trigger = self.binding.trigger.as_ref(); format!( - "Selected {}, {}.", + "{} {}, {}.", + i18n::t("search.a11y.selected_prefix"), &self .binding .description @@ -161,10 +162,13 @@ impl SearchItem for MatchedBinding { self.binding .trigger .as_ref() - .map_or("Press enter to confirm.".into(), |trigger| { + .map_or(i18n::t("search.a11y.press_enter_to_confirm"), |trigger| { format!( - "Press enter to confirm. Use {} binding to run this action in the future.", - trigger.normalized() + "{} {} {} {}", + i18n::t("search.a11y.press_enter_to_confirm"), + i18n::t("search.a11y.use_binding_prefix"), + trigger.normalized(), + i18n::t("search.a11y.use_binding_suffix") ) }) .into() diff --git a/app/src/search/ai_context_menu/blocks/search_item.rs b/app/src/search/ai_context_menu/blocks/search_item.rs index 465855836d..927246cc96 100644 --- a/app/src/search/ai_context_menu/blocks/search_item.rs +++ b/app/src/search/ai_context_menu/blocks/search_item.rs @@ -19,20 +19,32 @@ use crate::util::truncation::truncate_from_end; /// Calculate how long ago a timestamp was fn time_ago_string(timestamp: Option<&DateTime>) -> String { let Some(timestamp) = timestamp else { - return "Just now".to_string(); + return i18n::t("search.ai_context_menu.time.just_now"); }; let now = Local::now(); let duration = now.signed_duration_since(*timestamp); if duration.num_seconds() < 60 { - "Just now".to_string() + i18n::t("search.ai_context_menu.time.just_now") } else if duration.num_minutes() < 60 { - format!("{} minutes ago", duration.num_minutes()) + format!( + "{}{}", + duration.num_minutes(), + i18n::t("search.ai_context_menu.time.minutes_ago_suffix") + ) } else if duration.num_hours() < 24 { - format!("{} hours ago", duration.num_hours()) + format!( + "{}{}", + duration.num_hours(), + i18n::t("search.ai_context_menu.time.hours_ago_suffix") + ) } else { - format!("{} days ago", duration.num_days()) + format!( + "{}{}", + duration.num_days(), + i18n::t("search.ai_context_menu.time.days_ago_suffix") + ) } } @@ -135,7 +147,7 @@ impl SearchItem for BlockSearchItem { // Create sub text: last 3 lines of output let sub_text = if self.output_lines.is_empty() { - "No output".to_string() + i18n::t("search.ai_context_menu.blocks.no_output") } else { let joined = self.output_lines.join("\n").trim().to_string(); // Additional safety truncation for the hover card @@ -206,6 +218,10 @@ impl SearchItem for BlockSearchItem { } fn accessibility_label(&self) -> String { - format!("Block: {}", self.command) + format!( + "{}: {}", + i18n::t("search.ai_context_menu.blocks.prefix"), + self.command + ) } } diff --git a/app/src/search/ai_context_menu/code/data_source.rs b/app/src/search/ai_context_menu/code/data_source.rs index 26dd0e67a2..bbc00aa1e2 100644 --- a/app/src/search/ai_context_menu/code/data_source.rs +++ b/app/src/search/ai_context_menu/code/data_source.rs @@ -232,7 +232,7 @@ struct CodeSearchError; #[cfg(not(target_family = "wasm"))] impl DataSourceRunError for CodeSearchError { fn user_facing_error(&self) -> String { - "Code search failed".to_string() + i18n::t("search.ai_context_menu.code_search_failed") } fn telemetry_payload(&self) -> serde_json::Value { diff --git a/app/src/search/ai_context_menu/code/search_item.rs b/app/src/search/ai_context_menu/code/search_item.rs index 95cf839a20..575e2b3d9d 100644 --- a/app/src/search/ai_context_menu/code/search_item.rs +++ b/app/src/search/ai_context_menu/code/search_item.rs @@ -261,8 +261,10 @@ impl SearchItem for CodeSearchItem { fn accessibility_label(&self) -> String { format!( - "Code symbol: {} in {}:{}", + "{}: {} {} {}:{}", + i18n::t("search.ai_context_menu.code_symbol_prefix"), self.code_symbol.symbol.name, + i18n::t("search.ai_context_menu.code_symbol_in"), self.code_symbol.file_path.to_string_lossy(), self.code_symbol.symbol.line_number ) diff --git a/app/src/search/ai_context_menu/commands/search_item.rs b/app/src/search/ai_context_menu/commands/search_item.rs index 455c646fb8..4ecc609e73 100644 --- a/app/src/search/ai_context_menu/commands/search_item.rs +++ b/app/src/search/ai_context_menu/commands/search_item.rs @@ -81,6 +81,10 @@ impl SearchItem for CommandSearchItem { } fn accessibility_label(&self) -> String { - format!("Command: {}", self.command) + format!( + "{}: {}", + i18n::t("search.ai_context_menu.command_prefix"), + self.command + ) } } diff --git a/app/src/search/ai_context_menu/conversations/search_item.rs b/app/src/search/ai_context_menu/conversations/search_item.rs index fecec8936a..45176eb541 100644 --- a/app/src/search/ai_context_menu/conversations/search_item.rs +++ b/app/src/search/ai_context_menu/conversations/search_item.rs @@ -124,6 +124,10 @@ impl SearchItem for ConversationSearchItem { } fn accessibility_label(&self) -> String { - format!("Conversation: {}", self.item.title) + format!( + "{}: {}", + i18n::t("search.ai_context_menu.conversation_prefix"), + self.item.title + ) } } diff --git a/app/src/search/ai_context_menu/diffset/data_source.rs b/app/src/search/ai_context_menu/diffset/data_source.rs index bbb19b83a1..953c7bcb2a 100644 --- a/app/src/search/ai_context_menu/diffset/data_source.rs +++ b/app/src/search/ai_context_menu/diffset/data_source.rs @@ -6,9 +6,6 @@ use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction; use crate::search::data_source::{Query, QueryResult}; use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource}; -const UNCOMMITTED_CHANGES_NAME: &str = "uncommitted changes"; -const MAIN_BRANCH_CHANGES_NAME: &str = "changes vs. main branch"; - pub struct DiffSetDataSource; impl SyncDataSource for DiffSetDataSource { @@ -24,8 +21,10 @@ impl SyncDataSource for DiffSetDataSource { let mut results: Vec> = vec![]; // Add uncommitted changes option + let uncommitted_changes_name = + i18n::t("search.ai_context_menu.diffset.uncommitted_changes"); if let Some(match_result) = - fuzzy_match::match_indices_case_insensitive(UNCOMMITTED_CHANGES_NAME, query_text) + fuzzy_match::match_indices_case_insensitive(&uncommitted_changes_name, query_text) { results.push( DiffSetSearchItem { @@ -37,8 +36,9 @@ impl SyncDataSource for DiffSetDataSource { } // Add main branch comparison option + let main_branch_changes_name = i18n::t("search.ai_context_menu.diffset.changes_vs_main"); if let Some(match_result) = - fuzzy_match::match_indices_case_insensitive(MAIN_BRANCH_CHANGES_NAME, query_text) + fuzzy_match::match_indices_case_insensitive(&main_branch_changes_name, query_text) { results.push( DiffSetSearchItem { diff --git a/app/src/search/ai_context_menu/diffset/search_item.rs b/app/src/search/ai_context_menu/diffset/search_item.rs index b97f32c861..12279960be 100644 --- a/app/src/search/ai_context_menu/diffset/search_item.rs +++ b/app/src/search/ai_context_menu/diffset/search_item.rs @@ -21,17 +21,23 @@ pub struct DiffSetSearchItem { impl DiffSetSearchItem { pub fn name(&self) -> String { match &self.diff_mode { - DiffMode::Head => "Uncommitted changes".to_string(), - DiffMode::MainBranch => "Changes vs. main branch".to_string(), - DiffMode::OtherBranch(branch) => format!("Changes vs. {branch}"), + DiffMode::Head => i18n::t("search.ai_context_menu.diffset.uncommitted_changes"), + DiffMode::MainBranch => i18n::t("search.ai_context_menu.diffset.changes_vs_main"), + DiffMode::OtherBranch(branch) => format!( + "{}{branch}", + i18n::t("search.ai_context_menu.diffset.changes_vs_prefix") + ), } } pub fn description(&self) -> String { match &self.diff_mode { - DiffMode::Head => "All uncommitted changes in the working directory".to_string(), - DiffMode::MainBranch => "All changes compared to the main branch".to_string(), - DiffMode::OtherBranch(branch) => format!("All changes compared to {branch}"), + DiffMode::Head => i18n::t("search.ai_context_menu.diffset.uncommitted_desc"), + DiffMode::MainBranch => i18n::t("search.ai_context_menu.diffset.main_desc"), + DiffMode::OtherBranch(branch) => format!( + "{}{branch}", + i18n::t("search.ai_context_menu.diffset.compared_to_prefix") + ), } } } diff --git a/app/src/search/ai_context_menu/files/search_item.rs b/app/src/search/ai_context_menu/files/search_item.rs index 145ddb0ee3..2ebe959a25 100644 --- a/app/src/search/ai_context_menu/files/search_item.rs +++ b/app/src/search/ai_context_menu/files/search_item.rs @@ -79,9 +79,17 @@ impl SearchItem for FileSearchItem { fn accessibility_label(&self) -> String { if self.is_directory { - format!("Directory: {}", self.path.display()) + format!( + "{}: {}", + i18n::t("search.ai_context_menu.directory_prefix"), + self.path.display() + ) } else { - format!("File: {}", self.path.display()) + format!( + "{}: {}", + i18n::t("search.ai_context_menu.file_prefix"), + self.path.display() + ) } } } diff --git a/app/src/search/ai_context_menu/notebooks/search_item.rs b/app/src/search/ai_context_menu/notebooks/search_item.rs index 3d493cff74..47257a2c1b 100644 --- a/app/src/search/ai_context_menu/notebooks/search_item.rs +++ b/app/src/search/ai_context_menu/notebooks/search_item.rs @@ -189,9 +189,18 @@ impl SearchItem for NotebookSearchItem { fn accessibility_label(&self) -> String { if let Some(description) = &self.notebook_description { - format!("Notebook: {} - {}", self.notebook_name, description) + format!( + "{}: {} - {}", + i18n::t("search.ai_context_menu.notebook_prefix"), + self.notebook_name, + description + ) } else { - format!("Notebook: {}", self.notebook_name) + format!( + "{}: {}", + i18n::t("search.ai_context_menu.notebook_prefix"), + self.notebook_name + ) } } @@ -200,7 +209,7 @@ impl SearchItem for NotebookSearchItem { // Use notebook name, or "Untitled" if empty let display_name = if self.notebook_name.is_empty() { - "Untitled".to_string() + i18n::t("search.untitled") } else { self.notebook_name.clone() }; diff --git a/app/src/search/ai_context_menu/rules/search_item.rs b/app/src/search/ai_context_menu/rules/search_item.rs index d77d9f54ec..f6a1abe371 100644 --- a/app/src/search/ai_context_menu/rules/search_item.rs +++ b/app/src/search/ai_context_menu/rules/search_item.rs @@ -170,10 +170,10 @@ impl SearchItem for RuleSearchItem { if !name.is_empty() { name.clone() } else { - "Rule".to_string() + i18n::t("search.ai_context_menu.rule_prefix") } } else { - "Rule".to_string() + i18n::t("search.ai_context_menu.rule_prefix") }; // Create title element @@ -226,6 +226,10 @@ impl SearchItem for RuleSearchItem { } fn accessibility_label(&self) -> String { - format!("Rule: {}", self.rule_content) + format!( + "{}: {}", + i18n::t("search.ai_context_menu.rule_prefix"), + self.rule_content + ) } } diff --git a/app/src/search/ai_context_menu/skills/search_item.rs b/app/src/search/ai_context_menu/skills/search_item.rs index a35b3d848a..20075020ea 100644 --- a/app/src/search/ai_context_menu/skills/search_item.rs +++ b/app/src/search/ai_context_menu/skills/search_item.rs @@ -129,6 +129,10 @@ impl SearchItem for SkillSearchItem { } fn accessibility_label(&self) -> String { - format!("Skill: {}", self.name) + format!( + "{}: {}", + i18n::t("search.ai_context_menu.skill_prefix"), + self.name + ) } } diff --git a/app/src/search/ai_context_menu/view.rs b/app/src/search/ai_context_menu/view.rs index 7af7f9d1db..d48c9733fd 100644 --- a/app/src/search/ai_context_menu/view.rs +++ b/app/src/search/ai_context_menu/view.rs @@ -102,28 +102,39 @@ pub enum AIContextMenuCategory { } impl AIContextMenuCategory { - pub fn name(&self) -> &'static str { + pub fn name(&self) -> String { match self { - AIContextMenuCategory::CurrentFolderFiles => "Files and folders", - AIContextMenuCategory::RepoFiles => "Files and folders", - AIContextMenuCategory::Commands => "Commands", - AIContextMenuCategory::Blocks => "Blocks", - AIContextMenuCategory::Workflows => "Workflows", - AIContextMenuCategory::Notebooks => "Notebooks", - AIContextMenuCategory::Plans => "Plans", - AIContextMenuCategory::Diffs => "Diffs", - AIContextMenuCategory::Docs => "Docs", - AIContextMenuCategory::Tasks => "Past tasks", - AIContextMenuCategory::Rules => "Rules", - AIContextMenuCategory::Servers => "Servers and integrations", - AIContextMenuCategory::Terminal => "Terminal", - AIContextMenuCategory::Web => "Web", - AIContextMenuCategory::RecentDiff => "Most recent diff", - AIContextMenuCategory::RecentBlock => "Most recent block", - AIContextMenuCategory::Code => "Code", - AIContextMenuCategory::DiffSet => "Diff sets", - AIContextMenuCategory::Conversations => "Conversations", - AIContextMenuCategory::Skills => "Skills", + AIContextMenuCategory::CurrentFolderFiles | AIContextMenuCategory::RepoFiles => { + i18n::t("search.ai_context_menu.category.files_and_folders") + } + AIContextMenuCategory::Commands => i18n::t("search.ai_context_menu.category.commands"), + AIContextMenuCategory::Blocks => i18n::t("search.ai_context_menu.category.blocks"), + AIContextMenuCategory::Workflows => { + i18n::t("search.ai_context_menu.category.workflows") + } + AIContextMenuCategory::Notebooks => { + i18n::t("search.ai_context_menu.category.notebooks") + } + AIContextMenuCategory::Plans => i18n::t("search.ai_context_menu.category.plans"), + AIContextMenuCategory::Diffs => i18n::t("search.ai_context_menu.category.diffs"), + AIContextMenuCategory::Docs => i18n::t("search.ai_context_menu.category.docs"), + AIContextMenuCategory::Tasks => i18n::t("search.ai_context_menu.category.tasks"), + AIContextMenuCategory::Rules => i18n::t("search.ai_context_menu.category.rules"), + AIContextMenuCategory::Servers => i18n::t("search.ai_context_menu.category.servers"), + AIContextMenuCategory::Terminal => i18n::t("search.ai_context_menu.category.terminal"), + AIContextMenuCategory::Web => i18n::t("search.ai_context_menu.category.web"), + AIContextMenuCategory::RecentDiff => { + i18n::t("search.ai_context_menu.category.recent_diff") + } + AIContextMenuCategory::RecentBlock => { + i18n::t("search.ai_context_menu.category.recent_block") + } + AIContextMenuCategory::Code => i18n::t("search.ai_context_menu.category.code"), + AIContextMenuCategory::DiffSet => i18n::t("search.ai_context_menu.category.diff_set"), + AIContextMenuCategory::Conversations => { + i18n::t("search.ai_context_menu.category.conversations") + } + AIContextMenuCategory::Skills => i18n::t("search.ai_context_menu.category.skills"), } } @@ -1372,7 +1383,7 @@ impl AIContextMenu { let theme = appearance.theme(); Container::new( Text::new( - "No results found", + i18n::t("search.ai_context_menu.no_results"), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -1388,7 +1399,7 @@ impl AIContextMenu { let theme = appearance.theme(); Container::new( Text::new( - "Loading results...", + i18n::t("search.ai_context_menu.loading_results"), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -1405,7 +1416,7 @@ impl AIContextMenu { let theme = appearance.theme(); Container::new( Text::new( - "Code symbols indexing...", + i18n::t("search.ai_context_menu.code_symbols_indexing"), appearance.ui_font_family(), appearance.monospace_font_size(), ) diff --git a/app/src/search/ai_context_menu/workflows/search_item.rs b/app/src/search/ai_context_menu/workflows/search_item.rs index bb879025a9..ac99accecf 100644 --- a/app/src/search/ai_context_menu/workflows/search_item.rs +++ b/app/src/search/ai_context_menu/workflows/search_item.rs @@ -179,9 +179,18 @@ impl SearchItem for WorkflowSearchItem { fn accessibility_label(&self) -> String { if let Some(description) = &self.workflow_description { - format!("Workflow: {} - {}", self.workflow_name, description) + format!( + "{}: {} - {}", + i18n::t("search.ai_context_menu.workflow_prefix"), + self.workflow_name, + description + ) } else { - format!("Workflow: {}", self.workflow_name) + format!( + "{}: {}", + i18n::t("search.ai_context_menu.workflow_prefix"), + self.workflow_name + ) } } diff --git a/app/src/search/command_palette/conversations/data_source.rs b/app/src/search/command_palette/conversations/data_source.rs index cba204cb78..14baedef13 100644 --- a/app/src/search/command_palette/conversations/data_source.rs +++ b/app/src/search/command_palette/conversations/data_source.rs @@ -27,11 +27,13 @@ enum ConversationSection { } impl ConversationSection { - fn title(&self) -> &'static str { + fn title(&self) -> String { match self { - ConversationSection::ActivePane => "Active pane conversations", - ConversationSection::OtherActive => "Other active conversations", - ConversationSection::Past => "Past conversations", + ConversationSection::ActivePane => i18n::t("search.conversations.section.active_pane"), + ConversationSection::OtherActive => { + i18n::t("search.conversations.section.other_active") + } + ConversationSection::Past => i18n::t("search.conversations.section.past"), } } diff --git a/app/src/search/command_palette/conversations/search_item.rs b/app/src/search/command_palette/conversations/search_item.rs index 27b4427b1d..6d57980ebf 100644 --- a/app/src/search/command_palette/conversations/search_item.rs +++ b/app/src/search/command_palette/conversations/search_item.rs @@ -68,7 +68,7 @@ impl ConversationSearchItem { Flex::row() .with_child( Text::new_inline( - "New conversation", + i18n::t("search.conversations.new_conversation"), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -89,7 +89,7 @@ impl ConversationSearchItem { let appearance = Appearance::as_ref(app); let action_title = Text::new_inline( - "Fork current conversation", + i18n::t("search.conversations.fork_current_conversation"), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -243,7 +243,7 @@ impl ConversationSearchItem { let fork_button_tool_tip = appearance .ui_builder() - .tool_tip("Fork conversation".to_string()) + .tool_tip(i18n::t("search.conversations.fork_conversation_tooltip")) .build(); let fork_button_inner = icon_button( @@ -416,27 +416,32 @@ impl SearchItem for ConversationSearchItem { match &self.action_info { ConversationAction::Resume(matched_conversation) => { format!( - "Conversation: {}", + "{}: {}", + i18n::t("search.conversations.conversation"), matched_conversation.as_ref().conversation.title() ) } ConversationAction::Fork { title, .. } => { - format!("Fork current conversation ({title})") + format!( + "{} ({title})", + i18n::t("search.conversations.fork_current_conversation") + ) } - ConversationAction::New => "New conversation".to_string(), + ConversationAction::New => i18n::t("search.conversations.new_conversation"), } } fn accessibility_help_message(&self) -> Option { match &self.action_info { ConversationAction::Resume(matched_conversation) => Some(format!( - "Press enter to navigate to conversation \"{}\".", + "{} \"{}\".", + i18n::t("search.conversations.press_enter_to_navigate"), matched_conversation.as_ref().conversation.title() )), ConversationAction::Fork { .. } => { - Some("Press enter to fork the current conversation into a new conversation.".into()) + Some(i18n::t("search.conversations.press_enter_to_fork")) } - ConversationAction::New => Some("Press enter to create a new conversation.".into()), + ConversationAction::New => Some(i18n::t("search.conversations.press_enter_to_create")), } } } diff --git a/app/src/search/command_palette/files/search_item.rs b/app/src/search/command_palette/files/search_item.rs index 8dccbdacd5..2eb7baca21 100644 --- a/app/src/search/command_palette/files/search_item.rs +++ b/app/src/search/command_palette/files/search_item.rs @@ -97,17 +97,21 @@ impl SearchItem for FileSearchItem { fn accessibility_label(&self) -> String { if self.is_directory { - format!("Directory: {}", self.path.display()) + format!( + "{}: {}", + i18n::t("search.files.directory"), + self.path.display() + ) } else { - format!("File: {}", self.path.display()) + format!("{}: {}", i18n::t("search.files.file"), self.path.display()) } } fn accessibility_help_message(&self) -> Option { Some(if self.is_directory { - "Press Enter to navigate to this directory".to_string() + i18n::t("search.files.press_enter_to_navigate_directory") } else { - "Press Enter to open this file".to_string() + i18n::t("search.files.press_enter_to_open_file") }) } @@ -161,7 +165,11 @@ impl SearchItem for CreateFileSearchItem { let text_color = highlight_state.sub_text_fill(appearance).into_solid(); let label = Text::new_inline( - format!("Create a file named {}…", &self.file_name), + format!( + "{} {}…", + i18n::t("search.files.create_file_named"), + &self.file_name + ), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -195,13 +203,19 @@ impl SearchItem for CreateFileSearchItem { } fn accessibility_label(&self) -> String { - format!("Create file: {}", self.file_name) + format!( + "{}: {}", + i18n::t("search.files.create_file"), + self.file_name + ) } fn accessibility_help_message(&self) -> Option { Some(format!( - "Press Enter to create {} in the current directory", - self.file_name + "{} {} {}", + i18n::t("search.files.press_enter_to_create_prefix"), + self.file_name, + i18n::t("search.files.press_enter_to_create_suffix") )) } diff --git a/app/src/search/command_palette/filter_chip_renderer.rs b/app/src/search/command_palette/filter_chip_renderer.rs index 620fcabcc4..9931d24497 100644 --- a/app/src/search/command_palette/filter_chip_renderer.rs +++ b/app/src/search/command_palette/filter_chip_renderer.rs @@ -9,7 +9,9 @@ use warpui::{Element, EventContext}; use crate::appearance::Appearance; use crate::drive::cloud_object_styling::warp_drive_icon_color; use crate::drive::DriveObjectType; -use crate::search::{FilterChipRenderer as CommonFilterChipRenderer, QueryFilter}; +use crate::search::{ + query_filter_display_name, FilterChipRenderer as CommonFilterChipRenderer, QueryFilter, +}; use crate::util::color::{ContrastingColor, MinimumAllowedContrast}; /// Trait to render filter chips for the command palette. @@ -42,7 +44,7 @@ impl FilterChipRenderer for QueryFilter { .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Text::new_inline( - self.display_name(), + query_filter_display_name(*self), appearance.ui_font_family(), font_size, ) diff --git a/app/src/search/command_palette/launch_config/search_item.rs b/app/src/search/command_palette/launch_config/search_item.rs index c4aa2a5b45..6d29ff18bd 100644 --- a/app/src/search/command_palette/launch_config/search_item.rs +++ b/app/src/search/command_palette/launch_config/search_item.rs @@ -71,10 +71,16 @@ impl crate::search::item::SearchItem for SearchItem { } fn accessibility_label(&self) -> String { - format!("Selected {}.", self.launch_config.name) + format!( + "{} {}.", + i18n::t("search.a11y.selected_prefix"), + self.launch_config.name + ) } fn accessibility_help_message(&self) -> Option { - Some("Press enter to use this launch configuration.".into()) + Some(i18n::t( + "search.launch_config.press_enter_to_use_launch_configuration", + )) } } diff --git a/app/src/search/command_palette/navigation/render.rs b/app/src/search/command_palette/navigation/render.rs index 0de4c430a2..1f52fbc78c 100644 --- a/app/src/search/command_palette/navigation/render.rs +++ b/app/src/search/command_palette/navigation/render.rs @@ -105,7 +105,7 @@ fn render_current_session_pill( ) -> Box { let current_session_pill = appearance .ui_builder() - .span("Current".to_string()) + .span(i18n::t("common.current")) .with_style(UiComponentStyles { font_family_id: Some(appearance.monospace_font_family()), // The font size is scaled down to make sure the pill fits in the row with its padding. @@ -337,7 +337,7 @@ impl CommandRenderInfo { match command_context { CommandContext::RunningCommand { running_command } => CommandRenderInfo { command_text: Some(running_command), - hint_text: "Running...".to_string(), + hint_text: i18n::t("search.navigation.running"), row_spacing: styles::NAVIGATION_PALETTE_COMMAND_ROW_SPACING, hint_margin: styles::NAVIGATION_PALETTE_COMMAND_HINT_MARGIN, }, @@ -355,27 +355,35 @@ impl CommandRenderInfo { }, command_text: Some(last_run_command), hint_text: match mins_since_completion { - Some(mins) if mins >= 60 => "Completed over 1 hour ago".to_string(), - Some(mins) if mins == 1 => format!("Completed {mins} minute ago"), - Some(mins) => format!("Completed {mins} minutes ago"), - None => "No timestamp found".to_string(), + Some(mins) if mins >= 60 => i18n::t("search.navigation.completed_over_hour"), + Some(mins) if mins == 1 => format!( + "{} {mins} {}", + i18n::t("search.navigation.completed_prefix"), + i18n::t("search.navigation.minute_ago") + ), + Some(mins) => format!( + "{} {mins} {}", + i18n::t("search.navigation.completed_prefix"), + i18n::t("search.navigation.minutes_ago") + ), + None => i18n::t("search.navigation.no_timestamp"), }, }, CommandContext::RunningAIBlock { prompt } => CommandRenderInfo { command_text: Some(prompt), - hint_text: "Running...".to_string(), + hint_text: i18n::t("search.navigation.running"), row_spacing: styles::NAVIGATION_PALETTE_COMMAND_ROW_SPACING, hint_margin: styles::NAVIGATION_PALETTE_COMMAND_HINT_MARGIN, }, CommandContext::LastRunAIBlock { prompt } => CommandRenderInfo { command_text: Some(prompt), - hint_text: "Completed".to_string(), + hint_text: i18n::t("search.navigation.completed"), row_spacing: styles::NAVIGATION_PALETTE_COMMAND_ROW_SPACING, hint_margin: styles::NAVIGATION_PALETTE_COMMAND_HINT_MARGIN, }, CommandContext::None => CommandRenderInfo { command_text: Some(String::new()), - hint_text: "Empty Session".to_string(), + hint_text: i18n::t("search.navigation.empty_session"), row_spacing: 0., hint_margin: 0., }, diff --git a/app/src/search/command_palette/navigation/search_item.rs b/app/src/search/command_palette/navigation/search_item.rs index d6a97206f4..aeb2bf59e1 100644 --- a/app/src/search/command_palette/navigation/search_item.rs +++ b/app/src/search/command_palette/navigation/search_item.rs @@ -103,7 +103,8 @@ impl crate::search::item::SearchItem for SearchItem { fn accessibility_label(&self) -> String { format!( - "Selected {}. {}.", + "{} {}. {}.", + i18n::t("search.a11y.selected_prefix"), self.navigation_data().prompt(), self.navigation_data() .command_context() @@ -113,6 +114,6 @@ impl crate::search::item::SearchItem for SearchItem { } fn accessibility_help_message(&self) -> Option { - Some("Press enter to navigate to this session.".into()) + Some(i18n::t("search.a11y.press_enter_to_navigate_session")) } } diff --git a/app/src/search/command_palette/new_session/new_session_option.rs b/app/src/search/command_palette/new_session/new_session_option.rs index 572127d08d..5999f25457 100644 --- a/app/src/search/command_palette/new_session/new_session_option.rs +++ b/app/src/search/command_palette/new_session/new_session_option.rs @@ -1,5 +1,4 @@ use std::borrow::Cow; -use std::fmt; use warpui::Action; @@ -25,21 +24,6 @@ pub(super) enum Direction { Left, } -impl fmt::Display for Direction { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - Direction::Down => "Down", - Direction::Right => "Right", - Direction::Up => "Up", - Direction::Left => "Left", - } - ) - } -} - #[derive(Debug)] pub(super) enum NewSessionConfig { NewTab(AvailableShell), @@ -83,13 +67,28 @@ impl NewSessionOption { impl NewSessionOption { pub(super) fn new(id: NewSessionOptionId, config: NewSessionConfig) -> Self { let description = match &config { - NewSessionConfig::NewTab(shell) => format!("Create New Tab: {}", shell.short_name()), + NewSessionConfig::NewTab(shell) => format!( + "{}: {}", + i18n::t("search.new_session.create_new_tab"), + shell.short_name() + ), NewSessionConfig::NewWindow(shell) => { - format!("Create New Window: {}", shell.short_name()) - } - NewSessionConfig::Split(direction, shell) => { - format!("Split Pane {direction}: {}", shell.short_name()) + format!( + "{}: {}", + i18n::t("search.new_session.create_new_window"), + shell.short_name() + ) } + NewSessionConfig::Split(direction, shell) => format!( + "{}: {}", + match direction { + Direction::Down => i18n::t("search.new_session.split_pane_down"), + Direction::Right => i18n::t("search.new_session.split_pane_right"), + Direction::Up => i18n::t("search.new_session.split_pane_up"), + Direction::Left => i18n::t("search.new_session.split_pane_left"), + }, + shell.short_name() + ), }; Self { id, diff --git a/app/src/search/command_palette/new_session/search_item.rs b/app/src/search/command_palette/new_session/search_item.rs index e1deb8940a..f2e8fef753 100644 --- a/app/src/search/command_palette/new_session/search_item.rs +++ b/app/src/search/command_palette/new_session/search_item.rs @@ -74,10 +74,14 @@ impl crate::search::item::SearchItem for SearchItem { } fn accessibility_label(&self) -> String { - format!("Selected {}.", self.option.description()) + format!( + "{} {}.", + i18n::t("search.a11y.selected_prefix"), + self.option.description() + ) } fn accessibility_help_message(&self) -> Option { - Some("Press enter to launch this session.".into()) + Some(i18n::t("search.a11y.press_enter_to_launch_session")) } } diff --git a/app/src/search/command_palette/repos/repo_search_item.rs b/app/src/search/command_palette/repos/repo_search_item.rs index 7be1a5a84e..e67eb35f1a 100644 --- a/app/src/search/command_palette/repos/repo_search_item.rs +++ b/app/src/search/command_palette/repos/repo_search_item.rs @@ -135,6 +135,10 @@ impl SearchItem for RepoSearchItem { } fn accessibility_label(&self) -> String { - format!("Repo: {}", self.metadata.path.display()) + format!( + "{}: {}", + i18n::t("search.repos.repo"), + self.metadata.path.display() + ) } } diff --git a/app/src/search/command_palette/separator_search_item.rs b/app/src/search/command_palette/separator_search_item.rs index 3a3304fb4c..d5b9ceeced 100644 --- a/app/src/search/command_palette/separator_search_item.rs +++ b/app/src/search/command_palette/separator_search_item.rs @@ -66,7 +66,7 @@ impl SearchItem for SeparatorSearchItem { } fn accessibility_label(&self) -> String { - format!("Section: {}", self.title) + format!("{}: {}", i18n::t("search.separator.section"), self.title) } fn is_static_separator(&self) -> bool { diff --git a/app/src/search/command_palette/tabs/search_item.rs b/app/src/search/command_palette/tabs/search_item.rs index 3c92a2e02f..73c8e2dd94 100644 --- a/app/src/search/command_palette/tabs/search_item.rs +++ b/app/src/search/command_palette/tabs/search_item.rs @@ -54,7 +54,9 @@ impl SearchItemTrait for SearchItem { let appearance = Appearance::as_ref(app); let title_text = Text::new_inline( - format!("[Tab {}] {}", self.tab.tab_index, self.tab.title), + i18n::t("search.command_palette.tabs.title_with_index") + .replace("{index}", &self.tab.tab_index.to_string()) + .replace("{title}", &self.tab.title), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -99,12 +101,18 @@ impl SearchItemTrait for SearchItem { } fn accessibility_label(&self) -> String { - format!("Selected tab: {}.", self.tab.title) + format!( + "{} {}: {}.", + i18n::t("search.a11y.selected_prefix"), + i18n::t("search.tabs.tab"), + self.tab.title + ) } fn accessibility_help_message(&self) -> Option { Some(format!( - "Press enter to navigate to tab: {}.", + "{}: {}.", + i18n::t("search.tabs.press_enter_to_navigate"), self.tab.title )) } diff --git a/app/src/search/command_palette/view.rs b/app/src/search/command_palette/view.rs index 753756c219..7183cac2e9 100644 --- a/app/src/search/command_palette/view.rs +++ b/app/src/search/command_palette/view.rs @@ -279,7 +279,7 @@ impl View { SearchBar::new( mixer.clone(), search_bar_state.clone(), - "Search for a command", + i18n::t("search.command_palette.placeholder"), Self::create_query_result_renderer, ctx, ) @@ -291,7 +291,7 @@ impl View { }); let placeholder_element = QueryResultRenderer::new( - MatchedBinding::placeholder("No results found".into()).into(), + MatchedBinding::placeholder(i18n::t("search.no_results")).into(), "command_palette:no_results".into(), |_, _, _| {}, *styles::QUERY_RESULT_RENDERER_STYLES, @@ -835,10 +835,9 @@ impl View { if let Some(window_id) = window_id { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "Cannot switch conversations while agent is monitoring a command." - .to_string(), - ), + DismissibleToast::error(i18n::t( + "search.command_palette.cannot_switch_conversations", + )), window_id, ctx, ); @@ -976,9 +975,9 @@ impl View { if can_start_new_conversation { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "Cannot start a new conversation while agent is monitoring a command.".to_string(), - ), + DismissibleToast::error(i18n::t( + "search.command_palette.cannot_start_conversation", + )), window_id, ctx, ); diff --git a/app/src/search/command_palette/warp_drive/env_var_collection_search_item.rs b/app/src/search/command_palette/warp_drive/env_var_collection_search_item.rs index ae83606d92..8af38351b8 100644 --- a/app/src/search/command_palette/warp_drive/env_var_collection_search_item.rs +++ b/app/src/search/command_palette/warp_drive/env_var_collection_search_item.rs @@ -63,7 +63,7 @@ impl SearchItem for EnvVarCollectionSearchItem { .string_model .title .clone() - .unwrap_or("Untitled".to_owned()) + .unwrap_or_else(|| i18n::t("search.untitled")) .to_owned(), appearance.ui_font_family(), appearance.monospace_font_size(), @@ -164,13 +164,14 @@ impl SearchItem for EnvVarCollectionSearchItem { fn accessibility_label(&self) -> String { format!( - "Environment Variables: {}", + "{}: {}", + i18n::t("search.warp_drive.environment_variables"), self.cloud_env_var_collection .model() .string_model .title .clone() - .unwrap_or("Untitled".to_owned()) + .unwrap_or_else(|| i18n::t("search.untitled")) ) } } diff --git a/app/src/search/command_palette/warp_drive/notebook_search_item.rs b/app/src/search/command_palette/warp_drive/notebook_search_item.rs index 362ada2a6d..601ffd3405 100644 --- a/app/src/search/command_palette/warp_drive/notebook_search_item.rs +++ b/app/src/search/command_palette/warp_drive/notebook_search_item.rs @@ -62,7 +62,7 @@ impl SearchItem for NotebookSearchItem { ) -> Box { let appearance = Appearance::as_ref(app); let title = if self.cloud_notebook.model().title.is_empty() { - "Untitled".to_string() + i18n::t("search.untitled") } else { self.cloud_notebook.model().title.clone() }; @@ -142,6 +142,14 @@ impl SearchItem for NotebookSearchItem { } fn accessibility_label(&self) -> String { - format!("Notebook: {}", self.cloud_notebook.model().title) + format!( + "{}: {}", + i18n::t("search.warp_drive.notebook"), + if self.cloud_notebook.model().title.is_empty() { + i18n::t("search.untitled") + } else { + self.cloud_notebook.model().title.clone() + } + ) } } diff --git a/app/src/search/command_palette/warp_drive/workflow_search_item.rs b/app/src/search/command_palette/warp_drive/workflow_search_item.rs index 25b9a1938b..1e9fde72fa 100644 --- a/app/src/search/command_palette/warp_drive/workflow_search_item.rs +++ b/app/src/search/command_palette/warp_drive/workflow_search_item.rs @@ -149,6 +149,10 @@ impl SearchItem for WorkflowSearchItem { } fn accessibility_label(&self) -> String { - format!("Workflow: {}", self.cloud_workflow.model().data.name()) + format!( + "{}: {}", + i18n::t("search.warp_drive.workflow"), + self.cloud_workflow.model().data.name() + ) } } diff --git a/app/src/search/command_palette/zero_state/items.rs b/app/src/search/command_palette/zero_state/items.rs index 2fafde4a9b..e123735059 100644 --- a/app/src/search/command_palette/zero_state/items.rs +++ b/app/src/search/command_palette/zero_state/items.rs @@ -170,7 +170,10 @@ impl Items { let mut flex = Flex::column(); if !self.recent.is_empty() { - flex.add_child(Self::render_section_text("Recent", appearance)); + flex.add_child(Self::render_section_text( + i18n::t("search.zero_state.recent"), + appearance, + )); flex.add_children(self.recent.iter().enumerate().map(|(idx, result)| { Self::render_query_result( @@ -183,7 +186,10 @@ impl Items { } if !self.suggested.is_empty() { - flex.add_child(Self::render_section_text("Suggested", appearance)); + flex.add_child(Self::render_section_text( + i18n::t("search.zero_state.suggested"), + appearance, + )); flex.add_children(self.suggested.iter().enumerate().map(|(idx, result)| { Self::render_query_result( diff --git a/app/src/search/command_search/ai_queries/ai_queries_search_item.rs b/app/src/search/command_search/ai_queries/ai_queries_search_item.rs index f908afec57..afbfc5c12f 100644 --- a/app/src/search/command_search/ai_queries/ai_queries_search_item.rs +++ b/app/src/search/command_search/ai_queries/ai_queries_search_item.rs @@ -152,7 +152,8 @@ impl SearchItem for AIQuerySearchResultItem { Container::new( ui_builder .paragraph(format!( - "Ran {}", + "{} {}", + i18n::t("search.command_search.result.ran_prefix"), format_approx_duration_from_now(self.start_time) )) .build() @@ -178,6 +179,10 @@ impl SearchItem for AIQuerySearchResultItem { } fn accessibility_label(&self) -> String { - format!("AI query: {}", self.query_text) + format!( + "{}: {}", + i18n::t("search.command_search.result.ai_query_prefix"), + self.query_text + ) } } diff --git a/app/src/search/command_search/env_var_collections/env_var_collection_search_item.rs b/app/src/search/command_search/env_var_collections/env_var_collection_search_item.rs index 3e34ad9fc2..979843a95d 100644 --- a/app/src/search/command_search/env_var_collections/env_var_collection_search_item.rs +++ b/app/src/search/command_search/env_var_collections/env_var_collection_search_item.rs @@ -34,7 +34,7 @@ impl EnvVarCollectionSearchItem { env_var_collection .title .clone() - .unwrap_or("Untitled".to_owned()), + .unwrap_or_else(|| i18n::t("search.untitled")), true, ) .with_style(UiComponentStyles { @@ -92,7 +92,7 @@ impl SearchItem for EnvVarCollectionSearchItem { env_var_collection .title .clone() - .unwrap_or("Untitled".to_owned()), + .unwrap_or_else(|| i18n::t("search.untitled")), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -216,11 +216,12 @@ impl SearchItem for EnvVarCollectionSearchItem { let env_var_collection = self.env_var_collection.model().string_model.clone(); format!( - "Environment Variables: {}", + "{}: {}", + i18n::t("search.command_search.result.environment_variables_prefix"), env_var_collection .title .clone() - .unwrap_or("Untitled".to_owned()) + .unwrap_or_else(|| i18n::t("search.untitled")) ) } } diff --git a/app/src/search/command_search/history/history_search_item.rs b/app/src/search/command_search/history/history_search_item.rs index 96955c2df3..423db4f12d 100644 --- a/app/src/search/command_search/history/history_search_item.rs +++ b/app/src/search/command_search/history/history_search_item.rs @@ -156,7 +156,11 @@ impl SearchItem for HistorySearchItem { } fn accessibility_label(&self) -> String { - format!("History item: {}", self.entry.command) + format!( + "{}: {}", + i18n::t("search.command_search.result.history_item_prefix"), + self.entry.command + ) } } diff --git a/app/src/search/command_search/notebooks/notebook_search_item.rs b/app/src/search/command_search/notebooks/notebook_search_item.rs index 7b35da6950..689de8325b 100644 --- a/app/src/search/command_search/notebooks/notebook_search_item.rs +++ b/app/src/search/command_search/notebooks/notebook_search_item.rs @@ -136,6 +136,10 @@ impl SearchItem for NotebookSearchItem { } fn accessibility_label(&self) -> String { - format!("Notebook: {}", self.model.title) + format!( + "{}: {}", + i18n::t("search.command_search.result.notebook_prefix"), + self.model.title + ) } } diff --git a/app/src/search/command_search/projects/project_search_item.rs b/app/src/search/command_search/projects/project_search_item.rs index 37a58ada6a..45b0f81a48 100644 --- a/app/src/search/command_search/projects/project_search_item.rs +++ b/app/src/search/command_search/projects/project_search_item.rs @@ -146,7 +146,11 @@ impl SearchItem for ProjectSearchItem { } fn accessibility_label(&self) -> String { - format!("Project: {}", self.name) + format!( + "{}: {}", + i18n::t("search.command_search.result.project_prefix"), + self.name + ) } fn dedup_key(&self) -> Option { diff --git a/app/src/search/command_search/settings.rs b/app/src/search/command_search/settings.rs index 22a6ec2973..4a8f996f0a 100644 --- a/app/src/search/command_search/settings.rs +++ b/app/src/search/command_search/settings.rs @@ -9,6 +9,6 @@ define_settings_group!(CommandSearchSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "workflows.show_global_workflows_in_universal_search", - description: "Whether to show global workflows in universal search results.", + description_key: "settings.schema.workflows.show_global_workflows_in_universal_search.description", }, ]); diff --git a/app/src/search/command_search/view.rs b/app/src/search/command_search/view.rs index fc78baec9e..fea6a615dd 100644 --- a/app/src/search/command_search/view.rs +++ b/app/src/search/command_search/view.rs @@ -56,7 +56,6 @@ use crate::terminal::resizable_data::{ModalType, ResizableData, DEFAULT_UNIVERSA use crate::terminal::{History, HistoryEvent}; use crate::workspaces::user_workspaces::UserWorkspaces; -const DEFAULT_PLACEHOLDER_TEXT: &str = "Search your history, workflows, and more"; const PANEL_POSITION_ID: &str = "CommandSearchViewPanel"; const DETAILS_PANEL_MARGIN: f32 = 4.; const MIN_WIDTH_RATIO: f32 = 0.25; @@ -145,7 +144,7 @@ impl CommandSearchView { SearchBar::new( mixer.clone(), search_bar_state.clone(), - DEFAULT_PLACEHOLDER_TEXT, + i18n::t("search.command_search.placeholder"), |result_index, result| { QueryResultRenderer::new( result, @@ -507,13 +506,13 @@ impl CommandSearchView { let (a11y_content, a11y_help_content) = if was_immediately_executed { ( - "Result executed".to_owned(), - "Press Cmd-Up to navigate to the command's output.".to_owned(), + i18n::t("search.command_search.a11y.result_executed"), + i18n::t("search.command_search.a11y.result_executed_help"), ) } else { ( - "Result accepted.".to_owned(), - "You can edit the command here before pressing Enter to execute it.".to_owned(), + i18n::t("search.command_search.a11y.result_accepted"), + i18n::t("search.command_search.a11y.result_accepted_help"), ) }; ctx.emit_a11y_content(AccessibilityContent::new( @@ -567,7 +566,7 @@ impl CommandSearchView { let muted_color: ColorU = appearance.theme().nonactive_ui_text_color().into(); let text = appearance .ui_builder() - .span("Loading...") + .span(i18n::t("search.command_search.loading")) .with_style(UiComponentStyles { font_size: Some(appearance.monospace_font_size()), font_family_id: Some(appearance.ui_font_family()), @@ -608,7 +607,10 @@ impl CommandSearchView { current_user_id, ) } else { - self.render_error_header_text("Looks like you're out of credits. Contact a team admin to upgrade for more credits.".to_string(), appearance) + self.render_error_header_text( + i18n::t("search.command_search.out_of_credits_contact_admin"), + appearance, + ) } } else { self.render_error_header_text(message, appearance) @@ -673,7 +675,7 @@ impl CommandSearchView { appearance .ui_builder() .link( - "Upgrade".into(), + i18n::t("search.command_search.upgrade"), None, Some(Box::new(move |ctx| { ctx.dispatch_typed_action(CommandSearchAction::AttemptLoginGatedUpgrade); @@ -685,7 +687,7 @@ impl CommandSearchView { appearance .ui_builder() .link( - "Upgrade".into(), + i18n::t("search.command_search.upgrade"), None, Some(Box::new(move |ctx| { ctx.dispatch_typed_action(CommandSearchAction::OpenUpgradeLink( @@ -700,7 +702,7 @@ impl CommandSearchView { row.add_child( appearance .ui_builder() - .span("Looks like you're out of credits. ") + .span(i18n::t("search.command_search.out_of_credits_prefix")) .with_style(UiComponentStyles { font_size: Some(appearance.monospace_font_size()), font_family_id: Some(appearance.ui_font_family()), @@ -722,7 +724,7 @@ impl CommandSearchView { row.add_child( appearance .ui_builder() - .span(" for more credits.") + .span(i18n::t("search.command_search.out_of_credits_suffix")) .with_style(UiComponentStyles { font_size: Some(appearance.monospace_font_size()), font_family_id: Some(appearance.ui_font_family()), @@ -749,7 +751,7 @@ impl CommandSearchView { // There are no results to display, so notify the user of that fact. let text = appearance .ui_builder() - .span("No results found.") + .span(i18n::t("search.command_search.no_results")) .with_style(UiComponentStyles { font_size: Some(appearance.monospace_font_size()), font_family_id: Some(appearance.ui_font_family()), @@ -992,8 +994,8 @@ impl View for CommandSearchView { fn accessibility_contents(&self, _ctx: &AppContext) -> Option { Some(AccessibilityContent::new( - "Command Search".to_owned(), - "Search your history, workflows, and more. Use the Up and Down arrows to browse search results after typing. Press Enter to accept a selected result, inserting it into the terminal input. Press Escape to close.".to_owned(), + i18n::t("search.command_search.a11y.title"), + i18n::t("search.command_search.a11y.help"), WarpA11yRole::MenuRole, )) } diff --git a/app/src/search/command_search/warp_ai.rs b/app/src/search/command_search/warp_ai.rs index 39a71b7363..30e21acd53 100644 --- a/app/src/search/command_search/warp_ai.rs +++ b/app/src/search/command_search/warp_ai.rs @@ -29,9 +29,6 @@ use crate::ui_components::icons::Icon as UIIcon; use crate::util::color::{ContrastingColor, MinimumAllowedContrast}; use crate::workflows::{AIWorkflowOrigin, WorkflowSource, WorkflowType}; -const OPEN_WARP_AI_ITEM_BODY_TEXT: &str = "Ask Warp AI for command suggestions"; -const TRANSLATE_WITH_WARP_AI_ITEM_BODY_TEXT: &str = "Translate into shell command using Warp AI"; - #[derive(Clone, Debug)] pub enum WarpAISearchItem { /// Translates the query within command search. @@ -42,10 +39,10 @@ pub enum WarpAISearchItem { } impl WarpAISearchItem { - fn item_body_text(&self) -> &'static str { + fn item_body_text(&self) -> String { match self { - WarpAISearchItem::Translate => TRANSLATE_WITH_WARP_AI_ITEM_BODY_TEXT, - WarpAISearchItem::Open => OPEN_WARP_AI_ITEM_BODY_TEXT, + WarpAISearchItem::Translate => i18n::t("search.command_search.warp_ai.translate_body"), + WarpAISearchItem::Open => i18n::t("search.command_search.warp_ai.open_body"), } } } @@ -133,7 +130,11 @@ impl SearchItem for WarpAISearchItem { } fn accessibility_label(&self) -> String { - format!("Warp AI: {}", self.item_body_text()) + format!( + "{}: {}", + i18n::t("search.command_search.warp_ai.prefix"), + self.item_body_text() + ) } } @@ -235,12 +236,11 @@ impl AsyncDataSource for WarpAIDataSource { impl DataSourceRunError for GenerateCommandsFromNaturalLanguageError { fn user_facing_error(&self) -> String { match self { - Self::BadPrompt => "No results found. Please try again with a more specific query.", - Self::AiProviderError => "Something went wrong. Please try again.", - Self::RateLimited => "Looks like you're out of AI credits. Please try again later.", - Self::Other => "Something went wrong. Please try again.", + Self::BadPrompt => i18n::t("search.command_search.warp_ai.error.bad_prompt"), + Self::AiProviderError => i18n::t("search.command_search.warp_ai.error.provider_error"), + Self::RateLimited => i18n::t("search.command_search.warp_ai.error.rate_limited"), + Self::Other => i18n::t("search.command_search.warp_ai.error.provider_error"), } - .to_string() } fn telemetry_payload(&self) -> serde_json::Value { diff --git a/app/src/search/command_search/workflows/workflow_search_item.rs b/app/src/search/command_search/workflows/workflow_search_item.rs index 14385433d0..69bc49f599 100644 --- a/app/src/search/command_search/workflows/workflow_search_item.rs +++ b/app/src/search/command_search/workflows/workflow_search_item.rs @@ -258,7 +258,11 @@ impl SearchItem for WorkflowSearchItem { } fn accessibility_label(&self) -> String { - format!("Workflow: {}", self.workflow_data().name()) + format!( + "{}: {}", + i18n::t("search.command_search.result.workflow_prefix"), + self.workflow_data().name() + ) } } diff --git a/app/src/search/command_search/zero_state.rs b/app/src/search/command_search/zero_state.rs index 5a2b6d0c5d..c0a4553df8 100644 --- a/app/src/search/command_search/zero_state.rs +++ b/app/src/search/command_search/zero_state.rs @@ -192,7 +192,7 @@ impl View for CommandSearchZeroStateView { let command_search_text = Container::new( Text::new_inline( - "Command Search", + i18n::t("search.command_search.zero_state.title"), appearance.ui_font_family(), styles::header_text_font_size(appearance), ) @@ -214,7 +214,7 @@ impl View for CommandSearchZeroStateView { .with_child( Container::new( Text::new_inline( - "I'm looking for...", + i18n::t("search.command_search.zero_state.looking_for"), appearance.ui_font_family(), styles::subheader_text_font_size(appearance), ) @@ -233,7 +233,7 @@ impl View for CommandSearchZeroStateView { .with_child( Container::new( Text::new_inline( - "Example queries", + i18n::t("search.command_search.zero_state.example_queries"), appearance.ui_font_family(), styles::subheader_text_font_size(appearance), ) diff --git a/app/src/search/external_secrets/external_secret_search_item.rs b/app/src/search/external_secrets/external_secret_search_item.rs index 99267e6bed..c4836862d3 100644 --- a/app/src/search/external_secrets/external_secret_search_item.rs +++ b/app/src/search/external_secrets/external_secret_search_item.rs @@ -94,6 +94,10 @@ impl SearchItem for ExternalSecretSearchItem { } fn accessibility_label(&self) -> String { - format!("Secret: {}", &self.external_secret.get_display_name()) + format!( + "{}: {}", + i18n::t("search.external_secrets.secret_prefix"), + &self.external_secret.get_display_name() + ) } } diff --git a/app/src/search/external_secrets/view.rs b/app/src/search/external_secrets/view.rs index ff44f39dc5..8ebebcc8d2 100644 --- a/app/src/search/external_secrets/view.rs +++ b/app/src/search/external_secrets/view.rs @@ -38,8 +38,6 @@ lazy_static! { }; } -const DEFAULT_PLACEHOLDER_TEXT: &str = "Search for a secret"; - pub struct ExternalSecretsMenu { scroll_state: ScrollStateHandle, list_state: UniformListState, @@ -85,7 +83,7 @@ impl ExternalSecretsMenu { SearchBar::new( mixer.clone(), search_bar_state.clone(), - DEFAULT_PLACEHOLDER_TEXT, + i18n::t("search.external_secrets.placeholder"), |result_index, result| { QueryResultRenderer::new( result, @@ -180,7 +178,7 @@ impl ExternalSecretsMenu { // There are no results to display, so notify the user of that fact. let text = appearance .ui_builder() - .span("No results found.") + .span(i18n::t("search.no_results_found_period")) .with_style(UiComponentStyles { font_size: Some(appearance.monospace_font_size()), font_family_id: Some(appearance.ui_font_family()), diff --git a/app/src/search/filter_chip_renderer.rs b/app/src/search/filter_chip_renderer.rs index 679a98d5cf..caef37c556 100644 --- a/app/src/search/filter_chip_renderer.rs +++ b/app/src/search/filter_chip_renderer.rs @@ -7,7 +7,7 @@ use warpui::platform::Cursor; use warpui::{Element, EventContext}; use crate::appearance::Appearance; -use crate::search::QueryFilter; +use crate::search::{query_filter_display_name, QueryFilter}; /// Trait to render a filter chip. pub trait FilterChipRenderer { @@ -80,14 +80,18 @@ impl FilterChipRenderer for QueryFilter { } flex.add_child( - Text::new_inline(self.display_name(), appearance.ui_font_family(), font_size) - .with_color( - appearance - .theme() - .main_text_color(appearance.theme().surface_2()) - .into_solid(), - ) - .finish(), + Text::new_inline( + query_filter_display_name(*self), + appearance.ui_font_family(), + font_size, + ) + .with_color( + appearance + .theme() + .main_text_color(appearance.theme().surface_2()) + .into_solid(), + ) + .finish(), ); Container::new(flex.finish()) diff --git a/app/src/search/mod.rs b/app/src/search/mod.rs index 7fa2baac59..3ead8afc6a 100644 --- a/app/src/search/mod.rs +++ b/app/src/search/mod.rs @@ -26,3 +26,71 @@ pub use result_renderer::ItemHighlightState; // Re-export core search types. pub use warp_search_core::*; pub use workflows::fuzzy_match::FuzzyMatchWorkflowResult; + +pub fn query_filter_display_name(filter: QueryFilter) -> String { + match filter { + QueryFilter::History => i18n::t("search.filter.history"), + QueryFilter::Workflows => i18n::t("search.filter.workflows"), + QueryFilter::AgentModeWorkflows => i18n::t("search.filter.agent_mode_workflows"), + QueryFilter::Notebooks => i18n::t("search.filter.notebooks"), + QueryFilter::Plans => i18n::t("search.filter.plans"), + QueryFilter::NaturalLanguage => i18n::t("search.filter.natural_language"), + QueryFilter::Actions => i18n::t("search.filter.actions"), + QueryFilter::Sessions => i18n::t("search.filter.sessions"), + QueryFilter::Tabs => i18n::t("search.filter.tabs"), + QueryFilter::Conversations => i18n::t("search.filter.conversations"), + QueryFilter::LaunchConfigurations => i18n::t("search.filter.launch_configurations"), + QueryFilter::Drive => i18n::t("search.filter.drive"), + QueryFilter::EnvironmentVariables => i18n::t("search.filter.environment_variables"), + QueryFilter::PromptHistory => i18n::t("search.filter.prompt_history"), + QueryFilter::Files => i18n::t("search.filter.files"), + QueryFilter::Commands => i18n::t("search.filter.commands"), + QueryFilter::Blocks => i18n::t("search.filter.blocks"), + QueryFilter::Code => i18n::t("search.filter.code"), + QueryFilter::Rules => i18n::t("search.filter.rules"), + QueryFilter::Repos => i18n::t("search.filter.repos"), + QueryFilter::DiffSets => i18n::t("search.filter.diff_sets"), + QueryFilter::StaticSlashCommands => i18n::t("search.filter.static_slash_commands"), + QueryFilter::Skills => i18n::t("search.filter.skills"), + QueryFilter::BaseModels => i18n::t("search.filter.base_models"), + QueryFilter::FullTerminalUseModels => i18n::t("search.filter.full_terminal_use_models"), + QueryFilter::CurrentDirectoryConversations => { + i18n::t("search.filter.current_directory_conversations") + } + } +} + +pub fn query_filter_placeholder_text(filter: QueryFilter) -> String { + match filter { + QueryFilter::History => i18n::t("search.placeholder.history"), + QueryFilter::Workflows => i18n::t("search.placeholder.workflows"), + QueryFilter::AgentModeWorkflows => i18n::t("search.placeholder.agent_mode_workflows"), + QueryFilter::Notebooks => i18n::t("search.placeholder.notebooks"), + QueryFilter::Plans => i18n::t("search.placeholder.plans"), + QueryFilter::NaturalLanguage => i18n::t("search.placeholder.natural_language"), + QueryFilter::Actions => i18n::t("search.placeholder.actions"), + QueryFilter::Sessions => i18n::t("search.placeholder.sessions"), + QueryFilter::Tabs => i18n::t("search.placeholder.tabs"), + QueryFilter::Conversations => i18n::t("search.placeholder.conversations"), + QueryFilter::LaunchConfigurations => i18n::t("search.placeholder.launch_configurations"), + QueryFilter::Drive => i18n::t("search.placeholder.drive"), + QueryFilter::EnvironmentVariables => i18n::t("search.placeholder.environment_variables"), + QueryFilter::PromptHistory => i18n::t("search.placeholder.prompt_history"), + QueryFilter::Files => i18n::t("search.placeholder.files"), + QueryFilter::Commands => i18n::t("search.placeholder.commands"), + QueryFilter::Blocks => i18n::t("search.placeholder.blocks"), + QueryFilter::Code => i18n::t("search.placeholder.code"), + QueryFilter::Rules => i18n::t("search.placeholder.rules"), + QueryFilter::Repos => i18n::t("search.placeholder.repos"), + QueryFilter::DiffSets => i18n::t("search.placeholder.diff_sets"), + QueryFilter::StaticSlashCommands => i18n::t("search.placeholder.static_slash_commands"), + QueryFilter::Skills => i18n::t("search.placeholder.skills"), + QueryFilter::BaseModels => i18n::t("search.placeholder.base_models"), + QueryFilter::FullTerminalUseModels => { + i18n::t("search.placeholder.full_terminal_use_models") + } + QueryFilter::CurrentDirectoryConversations => { + i18n::t("search.placeholder.current_directory_conversations") + } + } +} diff --git a/app/src/search/notebook_embedding/notebooks/notebook_search_item.rs b/app/src/search/notebook_embedding/notebooks/notebook_search_item.rs index 750d55cb34..c4643ad053 100644 --- a/app/src/search/notebook_embedding/notebooks/notebook_search_item.rs +++ b/app/src/search/notebook_embedding/notebooks/notebook_search_item.rs @@ -101,7 +101,7 @@ impl SearchItem for NotebookSearchItem { let warning_font_size = appearance.ui_font_size() - 4.; let warning_text = appearance .ui_builder() - .span("Not visible to other users") + .span(i18n::t("search.not_visible_to_other_users")) .with_style(UiComponentStyles { font_size: Some(warning_font_size), margin: Some(Coords::uniform(0.).left(4.)), @@ -171,7 +171,11 @@ impl SearchItem for NotebookSearchItem { } fn accessibility_label(&self) -> String { - format!("Notebook: {}", self.cloud_notebook.model().title) + format!( + "{}: {}", + i18n::t("search.command_search.result.notebook_prefix"), + self.cloud_notebook.model().title + ) } } diff --git a/app/src/search/notebook_embedding/view.rs b/app/src/search/notebook_embedding/view.rs index 16ac2e3afe..f1618ea4fb 100644 --- a/app/src/search/notebook_embedding/view.rs +++ b/app/src/search/notebook_embedding/view.rs @@ -23,8 +23,6 @@ use crate::search::notebook_embedding::workflows::CloudWorkflowsDataSource; use crate::search::result_renderer::{QueryResultRenderer, QueryResultRendererStyles}; use crate::search::search_bar::{SearchBar, SearchBarEvent, SearchBarState, SearchResultOrdering}; -const DEFAULT_PLACEHOLDER_TEXT: &str = "Search for a reference"; - lazy_static! { static ref QUERY_RESULT_RENDERER_STYLES: QueryResultRendererStyles = QueryResultRendererStyles { @@ -83,7 +81,7 @@ impl EmbeddingSearchMenu { SearchBar::new( mixer.clone(), search_bar_state.clone(), - DEFAULT_PLACEHOLDER_TEXT, + i18n::t("search.notebook_embedding.placeholder"), |result_index, result| { QueryResultRenderer::new( result, @@ -200,7 +198,7 @@ impl EmbeddingSearchMenu { // There are no results to display, so notify the user of that fact. let text = appearance .ui_builder() - .span("No results found.") + .span(i18n::t("search.no_results_found_period")) .with_style(UiComponentStyles { font_size: Some(appearance.monospace_font_size()), font_family_id: Some(appearance.ui_font_family()), diff --git a/app/src/search/notebook_embedding/workflows/workflow_search_item.rs b/app/src/search/notebook_embedding/workflows/workflow_search_item.rs index 49797ec8a0..60a77731f6 100644 --- a/app/src/search/notebook_embedding/workflows/workflow_search_item.rs +++ b/app/src/search/notebook_embedding/workflows/workflow_search_item.rs @@ -102,7 +102,7 @@ impl SearchItem for WorkflowSearchItem { let warning_font_size = appearance.ui_font_size() - 4.; let warning_text = appearance .ui_builder() - .span("Not visible to other users") + .span(i18n::t("search.not_visible_to_other_users")) .with_style(UiComponentStyles { font_size: Some(warning_font_size), margin: Some(Coords::uniform(0.).left(4.)), @@ -179,7 +179,11 @@ impl SearchItem for WorkflowSearchItem { fn accessibility_label(&self) -> String { let workflow = &self.cloud_workflow.model().data; - format!("Workflow: {}", workflow.name()) + format!( + "{}: {}", + i18n::t("search.command_search.result.workflow_prefix"), + workflow.name() + ) } } diff --git a/app/src/search/search_bar.rs b/app/src/search/search_bar.rs index e0e369919f..a926c7ea5c 100644 --- a/app/src/search/search_bar.rs +++ b/app/src/search/search_bar.rs @@ -23,7 +23,7 @@ use crate::editor::{ use crate::search::data_source::{Query, QueryResult}; use crate::search::mixer::SearchMixer; use crate::search::result_renderer::{QueryResultIndex, QueryResultRenderer}; -use crate::search::QueryFilter; +use crate::search::{query_filter_display_name, query_filter_placeholder_text, QueryFilter}; use crate::ui_components::blended_colors; use crate::ui_components::icons::Icon; @@ -114,7 +114,7 @@ pub struct SearchBar { mixer: ModelHandle>, /// The placeholder text that is rendered in the search bar when no query has been run or /// filters have been applied. - placeholder_text: &'static str, + placeholder_text: String, create_query_result_renderer_fn: CreateQueryResultRendererFn, /// Font family to use when rendering the editor and query filters. If `None` the monospace font /// family is used. @@ -339,7 +339,7 @@ impl SearchBar { pub fn new( mixer: ModelHandle>, state: ModelHandle>, - placeholder_text: &'static str, + placeholder_text: impl Into, create_query_result_renderer_fn: CreateQueryResultRendererFn, ctx: &mut ViewContext, ) -> Self { @@ -366,7 +366,7 @@ impl SearchBar { let me = Self { editor_handle, mixer, - placeholder_text, + placeholder_text: placeholder_text.into(), state, create_query_result_renderer_fn, font_family_override: None, @@ -690,7 +690,11 @@ impl SearchBar { if let Some(loading_filters) = self.mixer.as_ref(ctx).loading_query_filters() { for loading_filter in loading_filters.into_iter() { ctx.emit_a11y_content(AccessibilityContent::new_without_help( - format!("Loading {} suggestions", loading_filter.display_name()), + format!( + "{} {}", + i18n::t("search.a11y.loading_suggestions_prefix"), + query_filter_display_name(loading_filter) + ), WarpA11yRole::MenuItemRole, )); } @@ -700,7 +704,7 @@ impl SearchBar { if let Some((.., data_source_err)) = self.mixer.as_ref(ctx).first_data_source_error() { ctx.emit_a11y_content(AccessibilityContent::new( - "Error finding results", + i18n::t("search.a11y.error_finding_results"), data_source_err.user_facing_error(), WarpA11yRole::MenuItemRole, )); @@ -708,7 +712,11 @@ impl SearchBar { } if let Some(selected_result) = self.state.as_ref(ctx).selected_result() { - let a11y_content_text = format!("Selected {}", selected_result.accessibility_label(),); + let a11y_content_text = format!( + "{} {}", + i18n::t("search.a11y.selected_prefix"), + selected_result.accessibility_label(), + ); let a11y_content = match selected_result.accessibility_help_message() { None => AccessibilityContent::new_without_help( a11y_content_text, @@ -774,10 +782,10 @@ impl SearchBar { // Set the appropriate placeholder text if the editor buffer is empty. match self.state.as_ref(ctx).query_filter { Some(filter) => { - editor.set_placeholder_text(filter.placeholder_text(), ctx); + editor.set_placeholder_text(query_filter_placeholder_text(filter), ctx); } None => { - editor.set_placeholder_text(self.placeholder_text, ctx); + editor.set_placeholder_text(self.placeholder_text.clone(), ctx); } } } diff --git a/app/src/search/search_results_menu/view.rs b/app/src/search/search_results_menu/view.rs index 8e0513b49d..c2bc1be41b 100644 --- a/app/src/search/search_results_menu/view.rs +++ b/app/src/search/search_results_menu/view.rs @@ -191,7 +191,7 @@ impl SearchResultsMenuView { let theme = appearance.theme(); Container::new( Text::new( - "No results found", + i18n::t("search.no_results_found"), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -339,9 +339,9 @@ impl View for SearchResultsMenuView { } } -fn renderable_title_name(query_filter: QueryFilter) -> Option<&'static str> { +fn renderable_title_name(query_filter: QueryFilter) -> Option { if matches!(query_filter, QueryFilter::AgentModeWorkflows) { - return Some("Prompts"); + return Some(i18n::t("search.search_results_menu.prompts")); } None diff --git a/app/src/search/slash_command_menu/static_commands/bindings.rs b/app/src/search/slash_command_menu/static_commands/bindings.rs index 4574aed78d..d178298f3d 100644 --- a/app/src/search/slash_command_menu/static_commands/bindings.rs +++ b/app/src/search/slash_command_menu/static_commands/bindings.rs @@ -31,5 +31,9 @@ pub fn default_binding_for_command(name: &'static str) -> DefaultSlashCommandBin } pub fn binding_description(command: &StaticCommand) -> BindingDescription { - BindingDescription::new_preserve_case(format!("Slash command: {}", command.name)) + BindingDescription::new_preserve_case(format!( + "{}: {}", + i18n::t("search.slash_command.prefix"), + command.name + )) } diff --git a/app/src/search/slash_command_menu/static_commands/commands.rs b/app/src/search/slash_command_menu/static_commands/commands.rs index 35cf954cd6..dfadf7cfca 100644 --- a/app/src/search/slash_command_menu/static_commands/commands.rs +++ b/app/src/search/slash_command_menu/static_commands/commands.rs @@ -12,7 +12,7 @@ use crate::ui_components::color_dot; pub static AGENT: LazyLock = LazyLock::new(|| StaticCommand { name: "/agent", - description: "Start a new conversation", + description: "search.slash_command.description.agent", icon_path: "bundled/svg/oz.svg", availability: Availability::AI_ENABLED.union(Availability::NOT_CLOUD_AGENT), auto_enter_ai_mode: false, @@ -21,7 +21,7 @@ pub static AGENT: LazyLock = LazyLock::new(|| StaticCommand { pub static CLOUD_AGENT: LazyLock = LazyLock::new(|| StaticCommand { name: "/cloud-agent", - description: "Start a new cloud agent conversation", + description: "search.slash_command.description.cloud_agent", icon_path: "bundled/svg/oz-cloud.svg", availability: Availability::AI_ENABLED.union(Availability::NOT_CLOUD_AGENT), auto_enter_ai_mode: false, @@ -30,7 +30,7 @@ pub static CLOUD_AGENT: LazyLock = LazyLock::new(|| StaticCommand pub const ADD_MCP: StaticCommand = StaticCommand { name: "/add-mcp", - description: "Add a new MCP server via the MCP settings page", + description: "search.slash_command.description.add_mcp", icon_path: "bundled/svg/dataflow.svg", availability: Availability::AI_ENABLED, auto_enter_ai_mode: false, @@ -39,7 +39,7 @@ pub const ADD_MCP: StaticCommand = StaticCommand { pub const PR_COMMENTS: StaticCommand = StaticCommand { name: "/pr-comments", - description: "Pull GitHub PR review comments", + description: "search.slash_command.description.pr_comments", icon_path: "bundled/svg/github.svg", availability: Availability::REPOSITORY.union(Availability::AI_ENABLED), auto_enter_ai_mode: true, @@ -48,7 +48,7 @@ pub const PR_COMMENTS: StaticCommand = StaticCommand { pub static CREATE_ENVIRONMENT: LazyLock = LazyLock::new(|| StaticCommand { name: "/create-environment", - description: "Create an Oz environment (Docker image + repos) via guided setup", + description: "search.slash_command.description.create_environment", icon_path: "bundled/svg/dataflow.svg", availability: Availability::AI_ENABLED, auto_enter_ai_mode: false, @@ -61,7 +61,7 @@ pub static CREATE_ENVIRONMENT: LazyLock = LazyLock::new(|| Static pub const CREATE_DOCKER_SANDBOX: StaticCommand = StaticCommand { name: "/docker-sandbox", - description: "Create a new docker sandbox terminal session", + description: "search.slash_command.description.create_docker_sandbox", icon_path: "bundled/svg/docker.svg", availability: Availability::LOCAL.union(Availability::AI_ENABLED), auto_enter_ai_mode: false, @@ -70,7 +70,7 @@ pub const CREATE_DOCKER_SANDBOX: StaticCommand = StaticCommand { pub static CREATE_NEW_PROJECT: LazyLock = LazyLock::new(|| StaticCommand { name: "/create-new-project", - description: "Have Oz walk you through creating a new coding project", + description: "search.slash_command.description.create_new_project", icon_path: "bundled/svg/plus.svg", availability: Availability::LOCAL | Availability::AI_ENABLED, auto_enter_ai_mode: true, @@ -79,7 +79,7 @@ pub static CREATE_NEW_PROJECT: LazyLock = LazyLock::new(|| Static pub static EDIT_SKILL: LazyLock = LazyLock::new(|| StaticCommand { name: "/open-skill", - description: "Open a skill's markdown file in Warp's built-in editor", + description: "search.slash_command.description.edit_skill", icon_path: "bundled/svg/file-code-02.svg", availability: Availability::AI_ENABLED, auto_enter_ai_mode: false, @@ -88,7 +88,7 @@ pub static EDIT_SKILL: LazyLock = LazyLock::new(|| StaticCommand pub static INVOKE_SKILL: LazyLock = LazyLock::new(|| StaticCommand { name: "/skills", - description: "Invoke a skill", + description: "search.slash_command.description.invoke_skill", icon_path: "bundled/svg/stars-01.svg", availability: Availability::AI_ENABLED, auto_enter_ai_mode: false, @@ -97,7 +97,7 @@ pub static INVOKE_SKILL: LazyLock = LazyLock::new(|| StaticComman pub static ADD_PROMPT: LazyLock = LazyLock::new(|| StaticCommand { name: "/add-prompt", - description: "Add new Agent prompt", + description: "search.slash_command.description.add_prompt", icon_path: if FeatureFlag::AgentView.is_enabled() { "bundled/svg/prompt.svg" } else { @@ -110,7 +110,7 @@ pub static ADD_PROMPT: LazyLock = LazyLock::new(|| StaticCommand pub const ADD_RULE: StaticCommand = StaticCommand { name: "/add-rule", - description: "Add a new global rule for the agent", + description: "search.slash_command.description.add_rule", icon_path: "bundled/svg/book-open.svg", availability: Availability::AI_ENABLED, auto_enter_ai_mode: false, @@ -119,7 +119,7 @@ pub const ADD_RULE: StaticCommand = StaticCommand { pub static EDIT: LazyLock = LazyLock::new(|| StaticCommand { name: "/open-file", - description: "Open a file in Warp's code editor", + description: "search.slash_command.description.edit", icon_path: "bundled/svg/file-code-02.svg", availability: Availability::LOCAL, auto_enter_ai_mode: false, @@ -130,7 +130,7 @@ pub static EDIT: LazyLock = LazyLock::new(|| StaticCommand { pub static RENAME_TAB: LazyLock = LazyLock::new(|| StaticCommand { name: "/rename-tab", - description: "Rename the current tab", + description: "search.slash_command.description.rename_tab", icon_path: "bundled/svg/pencil-line.svg", availability: Availability::ALWAYS, auto_enter_ai_mode: false, @@ -149,7 +149,7 @@ static SET_TAB_COLOR_HINT: LazyLock = LazyLock::new(|| { pub static SET_TAB_COLOR: LazyLock = LazyLock::new(|| StaticCommand { name: "/set-tab-color", - description: "Set the color of the current tab", + description: "search.slash_command.description.set_tab_color", icon_path: "bundled/svg/ellipse.svg", availability: Availability::ALWAYS, auto_enter_ai_mode: false, @@ -160,7 +160,7 @@ pub static FORK: LazyLock = LazyLock::new(|| { let hint_text = ""; StaticCommand { name: "/fork", - description: "Fork the current conversation in a new pane or a new tab", + description: "search.slash_command.description.fork", icon_path: "bundled/svg/arrow-split.svg", availability: Availability::AGENT_VIEW | Availability::ACTIVE_CONVERSATION @@ -174,7 +174,7 @@ pub static FORK: LazyLock = LazyLock::new(|| { pub static MOVE_TO_CLOUD: LazyLock = LazyLock::new(|| StaticCommand { name: "/handoff", - description: "Hand off this conversation to a cloud agent", + description: "search.slash_command.description.move_to_cloud", icon_path: "bundled/svg/upload-cloud-01.svg", availability: Availability::AGENT_VIEW | Availability::ACTIVE_CONVERSATION @@ -190,7 +190,7 @@ pub static MOVE_TO_CLOUD: LazyLock = LazyLock::new(|| StaticComma pub const OPEN_CODE_REVIEW: StaticCommand = StaticCommand { name: "/open-code-review", - description: "Open code review", + description: "search.slash_command.description.open_code_review", icon_path: "bundled/svg/diff.svg", availability: Availability::REPOSITORY, auto_enter_ai_mode: false, @@ -199,7 +199,7 @@ pub const OPEN_CODE_REVIEW: StaticCommand = StaticCommand { pub const INDEX: StaticCommand = StaticCommand { name: "/index", - description: "Index this codebase", + description: "search.slash_command.description.index", icon_path: "bundled/svg/find-all.svg", availability: Availability::REPOSITORY .union(Availability::CODEBASE_CONTEXT) @@ -210,7 +210,7 @@ pub const INDEX: StaticCommand = StaticCommand { pub const INIT: StaticCommand = StaticCommand { name: "/init", - description: "Index this codebase and generate an AGENTS.md file", + description: "search.slash_command.description.init", icon_path: "bundled/svg/warp-2.svg", availability: Availability::REPOSITORY .union(Availability::AGENT_VIEW) @@ -221,7 +221,7 @@ pub const INIT: StaticCommand = StaticCommand { pub const OPEN_PROJECT_RULES: StaticCommand = StaticCommand { name: "/open-project-rules", - description: "Open the project rules file (AGENTS.md)", + description: "search.slash_command.description.open_project_rules", icon_path: "bundled/svg/file-code-02.svg", availability: Availability::REPOSITORY.union(Availability::AI_ENABLED), auto_enter_ai_mode: false, @@ -230,7 +230,7 @@ pub const OPEN_PROJECT_RULES: StaticCommand = StaticCommand { pub const OPEN_MCP_SERVERS: StaticCommand = StaticCommand { name: "/open-mcp-servers", - description: "Open MCP servers", + description: "search.slash_command.description.open_mcp_servers", icon_path: "bundled/svg/dataflow.svg", availability: Availability::AI_ENABLED, auto_enter_ai_mode: false, @@ -239,7 +239,7 @@ pub const OPEN_MCP_SERVERS: StaticCommand = StaticCommand { pub const OPEN_SETTINGS_FILE: StaticCommand = StaticCommand { name: "/open-settings-file", - description: "Open settings file (TOML)", + description: "search.slash_command.description.open_settings_file", icon_path: "bundled/svg/file-code-02.svg", availability: Availability::LOCAL, auto_enter_ai_mode: false, @@ -248,7 +248,7 @@ pub const OPEN_SETTINGS_FILE: StaticCommand = StaticCommand { pub const CHANGELOG: StaticCommand = StaticCommand { name: "/changelog", - description: "Open the latest changelog", + description: "search.slash_command.description.changelog", icon_path: "bundled/svg/book-open.svg", availability: Availability::ALWAYS, auto_enter_ai_mode: false, @@ -260,7 +260,7 @@ pub const CHANGELOG: StaticCommand = StaticCommand { // argument after `/feedback` would fall through and be treated as plain input. pub static FEEDBACK: LazyLock = LazyLock::new(|| StaticCommand { name: "/feedback", - description: "Send feedback", + description: "search.slash_command.description.feedback", icon_path: "bundled/svg/feedback.svg", availability: Availability::ALWAYS, auto_enter_ai_mode: false, @@ -269,7 +269,7 @@ pub static FEEDBACK: LazyLock = LazyLock::new(|| StaticCommand { pub const OPEN_REPO: StaticCommand = StaticCommand { name: "/open-repo", - description: "Switch to another indexed repository", + description: "search.slash_command.description.open_repo", icon_path: "bundled/svg/folder.svg", availability: Availability::LOCAL.union(Availability::AI_ENABLED), auto_enter_ai_mode: false, @@ -278,7 +278,7 @@ pub const OPEN_REPO: StaticCommand = StaticCommand { pub const OPEN_RULES: StaticCommand = StaticCommand { name: "/open-rules", - description: "View all of your global and project rules", + description: "search.slash_command.description.open_rules", icon_path: "bundled/svg/book-open.svg", availability: Availability::AI_ENABLED, auto_enter_ai_mode: false, @@ -287,7 +287,7 @@ pub const OPEN_RULES: StaticCommand = StaticCommand { pub static NEW: LazyLock = LazyLock::new(|| StaticCommand { name: "/new", - description: "Start a new conversation (alias for /agent)", + description: "search.slash_command.description.new", icon_path: "bundled/svg/new-conversation.svg", availability: Availability::NO_LRC_CONTROL | Availability::AI_ENABLED @@ -298,7 +298,7 @@ pub static NEW: LazyLock = LazyLock::new(|| StaticCommand { pub static MODEL: LazyLock = LazyLock::new(|| StaticCommand { name: "/model", - description: "Switch the base agent model", + description: "search.slash_command.description.model", icon_path: "bundled/svg/oz.svg", availability: Availability::AGENT_VIEW | Availability::AI_ENABLED, auto_enter_ai_mode: true, @@ -307,7 +307,7 @@ pub static MODEL: LazyLock = LazyLock::new(|| StaticCommand { pub static HOST: LazyLock = LazyLock::new(|| StaticCommand { name: "/host", - description: "Switch the cloud agent execution host", + description: "search.slash_command.description.host", icon_path: "bundled/svg/oz-cloud.svg", availability: Availability::AGENT_VIEW | Availability::AI_ENABLED @@ -318,7 +318,7 @@ pub static HOST: LazyLock = LazyLock::new(|| StaticCommand { pub static HARNESS: LazyLock = LazyLock::new(|| StaticCommand { name: "/harness", - description: "Switch the cloud agent harness", + description: "search.slash_command.description.harness", icon_path: "bundled/svg/oz.svg", availability: Availability::AGENT_VIEW | Availability::AI_ENABLED @@ -329,7 +329,7 @@ pub static HARNESS: LazyLock = LazyLock::new(|| StaticCommand { pub static ENVIRONMENT: LazyLock = LazyLock::new(|| StaticCommand { name: "/environment", - description: "Switch the cloud agent environment", + description: "search.slash_command.description.environment", icon_path: "bundled/svg/globe-04.svg", availability: Availability::AGENT_VIEW | Availability::AI_ENABLED @@ -340,7 +340,7 @@ pub static ENVIRONMENT: LazyLock = LazyLock::new(|| StaticCommand pub static PROFILE: LazyLock = LazyLock::new(|| StaticCommand { name: "/profile", - description: "Switch the active execution profile", + description: "search.slash_command.description.profile", icon_path: "bundled/svg/psychology.svg", availability: Availability::AGENT_VIEW | Availability::AI_ENABLED @@ -353,7 +353,7 @@ pub const PLAN_NAME: &str = "/plan"; pub static PLAN: LazyLock = LazyLock::new(|| StaticCommand { name: PLAN_NAME, - description: "Prompt the agent to do some research and create a plan for a task", + description: "search.slash_command.description.plan", icon_path: "bundled/svg/file-06.svg", availability: Availability::AI_ENABLED, auto_enter_ai_mode: true, @@ -364,7 +364,7 @@ pub const ORCHESTRATE_NAME: &str = "/orchestrate"; pub static ORCHESTRATE: LazyLock = LazyLock::new(|| StaticCommand { name: ORCHESTRATE_NAME, - description: "Break a task into subtasks and run them in parallel with multiple agents", + description: "search.slash_command.description.orchestrate", icon_path: "bundled/svg/oz.svg", availability: Availability::LOCAL | Availability::AI_ENABLED, auto_enter_ai_mode: true, @@ -382,7 +382,7 @@ pub fn strip_command_prefix(query: &str, name: &str) -> Option { pub static COMPACT: LazyLock = LazyLock::new(|| StaticCommand { name: "/compact", - description: "Free up context by summarizing convo history", + description: "search.slash_command.description.compact", icon_path: "bundled/svg/collapse_content.svg", availability: Availability::AGENT_VIEW | Availability::ACTIVE_CONVERSATION @@ -397,7 +397,7 @@ pub static COMPACT: LazyLock = LazyLock::new(|| StaticCommand { pub static COMPACT_AND: LazyLock = LazyLock::new(|| StaticCommand { name: "/compact-and", - description: "Compact conversation and then send a follow-up prompt", + description: "search.slash_command.description.compact_and", icon_path: "bundled/svg/collapse_content.svg", availability: Availability::AGENT_VIEW | Availability::ACTIVE_CONVERSATION @@ -410,7 +410,7 @@ pub static COMPACT_AND: LazyLock = LazyLock::new(|| StaticCommand pub static QUEUE: LazyLock = LazyLock::new(|| StaticCommand { name: "/queue", - description: "Queue a prompt to send after the agent finishes responding", + description: "search.slash_command.description.queue", icon_path: "bundled/svg/clock-plus.svg", availability: Availability::AGENT_VIEW | Availability::ACTIVE_CONVERSATION @@ -425,7 +425,7 @@ pub static FORK_AND_COMPACT: LazyLock = LazyLock::new(|| { let hint_text = ""; StaticCommand { name: "/fork-and-compact", - description: "Fork current conversation and compact it in the forked copy", + description: "search.slash_command.description.fork_and_compact", icon_path: "bundled/svg/fork_and_compact.svg", availability: Availability::AGENT_VIEW | Availability::ACTIVE_CONVERSATION @@ -439,7 +439,7 @@ pub static FORK_AND_COMPACT: LazyLock = LazyLock::new(|| { pub const FORK_FROM: StaticCommand = StaticCommand { name: "/fork-from", - description: "Fork conversation from a specific query", + description: "search.slash_command.description.fork_from", icon_path: "bundled/svg/arrow-split.svg", availability: Availability::AGENT_VIEW .union(Availability::NO_LRC_CONTROL) @@ -453,7 +453,7 @@ pub static CONTINUE_LOCALLY: LazyLock = LazyLock::new(|| { let hint_text = ""; StaticCommand { name: "/continue-locally", - description: "Continue this cloud conversation locally", + description: "search.slash_command.description.continue_locally", icon_path: "bundled/svg/arrow-split.svg", availability: Availability::AGENT_VIEW | Availability::ACTIVE_CONVERSATION @@ -465,7 +465,7 @@ pub static CONTINUE_LOCALLY: LazyLock = LazyLock::new(|| { pub const USAGE: StaticCommand = StaticCommand { name: "/usage", - description: "Open billing and usage settings", + description: "search.slash_command.description.usage", icon_path: "bundled/svg/bar-chart-04.svg", availability: Availability::AI_ENABLED, auto_enter_ai_mode: false, @@ -474,7 +474,7 @@ pub const USAGE: StaticCommand = StaticCommand { pub const REMOTE_CONTROL: StaticCommand = StaticCommand { name: "/remote-control", - description: "Start remote control for this session", + description: "search.slash_command.description.remote_control", icon_path: "bundled/svg/phone-01.svg", availability: Availability::AI_ENABLED.union(Availability::NOT_CLOUD_AGENT), auto_enter_ai_mode: false, @@ -483,7 +483,7 @@ pub const REMOTE_CONTROL: StaticCommand = StaticCommand { pub const COST: StaticCommand = StaticCommand { name: "/cost", - description: "Toggle credit usage details", + description: "search.slash_command.description.cost", icon_path: "bundled/svg/bar-chart-04.svg", availability: Availability::AGENT_VIEW .union(Availability::AI_ENABLED) @@ -494,7 +494,7 @@ pub const COST: StaticCommand = StaticCommand { pub const CONVERSATIONS: StaticCommand = StaticCommand { name: "/conversations", - description: "Open conversation history", + description: "search.slash_command.description.conversations", icon_path: "bundled/svg/conversation.svg", availability: Availability::AI_ENABLED, auto_enter_ai_mode: false, @@ -503,7 +503,7 @@ pub const CONVERSATIONS: StaticCommand = StaticCommand { pub static PROMPTS: LazyLock = LazyLock::new(|| StaticCommand { name: "/prompts", - description: "Search saved prompts", + description: "search.slash_command.description.prompts", icon_path: "bundled/svg/prompt.svg", availability: Availability::AI_ENABLED, auto_enter_ai_mode: false, @@ -512,7 +512,7 @@ pub static PROMPTS: LazyLock = LazyLock::new(|| StaticCommand { pub const REWIND: StaticCommand = StaticCommand { name: "/rewind", - description: "Rewind to a previous point in the conversation", + description: "search.slash_command.description.rewind", icon_path: "bundled/svg/clock-rewind.svg", availability: Availability::AGENT_VIEW .union(Availability::AI_ENABLED) @@ -523,7 +523,7 @@ pub const REWIND: StaticCommand = StaticCommand { pub const EXPORT_TO_CLIPBOARD: StaticCommand = StaticCommand { name: "/export-to-clipboard", - description: "Export current conversation to clipboard in markdown format", + description: "search.slash_command.description.export_to_clipboard", icon_path: "bundled/svg/copy.svg", availability: Availability::AGENT_VIEW .union(Availability::AI_ENABLED) @@ -534,7 +534,7 @@ pub const EXPORT_TO_CLIPBOARD: StaticCommand = StaticCommand { pub static EXPORT_TO_FILE: LazyLock = LazyLock::new(|| StaticCommand { name: "/export-to-file", - description: "Export current conversation to a markdown file", + description: "search.slash_command.description.export_to_file", icon_path: "bundled/svg/download-01.svg", availability: Availability::AGENT_VIEW | Availability::AI_ENABLED diff --git a/app/src/search/slash_command_menu/static_commands/mod.rs b/app/src/search/slash_command_menu/static_commands/mod.rs index 56649931ba..12c6363f45 100644 --- a/app/src/search/slash_command_menu/static_commands/mod.rs +++ b/app/src/search/slash_command_menu/static_commands/mod.rs @@ -98,6 +98,10 @@ pub struct StaticCommand { } impl StaticCommand { + pub fn description(&self) -> String { + i18n::t(self.description) + } + pub fn matches_filter(&self, filter_text: &str) -> bool { if filter_text.is_empty() { return true; diff --git a/app/src/search/welcome_palette/view.rs b/app/src/search/welcome_palette/view.rs index 94ce6de22b..3e0f03e213 100644 --- a/app/src/search/welcome_palette/view.rs +++ b/app/src/search/welcome_palette/view.rs @@ -269,7 +269,7 @@ impl WelcomePalette { SearchBar::new( mixer.clone(), search_bar_state.clone(), - "Code, build, or search for anything...", + i18n::t("search.welcome_palette.placeholder"), Self::create_query_result_renderer, ctx, ) @@ -286,7 +286,7 @@ impl WelcomePalette { }); let placeholder_element = QueryResultRenderer::new( - MatchedBinding::placeholder("No results found".into()).into(), + MatchedBinding::placeholder(i18n::t("search.welcome_palette.no_results")).into(), "welcome_palette:no_results".into(), |_, _, _| {}, *styles::QUERY_RESULT_RENDERER_STYLES, @@ -700,8 +700,13 @@ impl WelcomePalette { .with_text_and_icon_label(TextAndIcon::new( TextAndIconAlignment::IconFirst, match &self.open_project_keybinding { - Some(keystroke) => format!("Add repository {keystroke}"), - None => "Add repository".to_string(), + Some(keystroke) => { + format!( + "{} {keystroke}", + i18n::t("search.welcome_palette.add_repository") + ) + } + None => i18n::t("search.welcome_palette.add_repository"), }, Icon::Plus.to_warpui_icon(theme.foreground()), MainAxisSize::Max, @@ -723,8 +728,11 @@ impl WelcomePalette { .with_text_and_icon_label(TextAndIcon::new( TextAndIconAlignment::IconFirst, match &self.terminal_session_keybinding { - Some(keystroke) => format!("Terminal session {keystroke}"), - None => "Terminal session".to_string(), + Some(keystroke) => format!( + "{} {keystroke}", + i18n::t("search.welcome_palette.terminal_session") + ), + None => i18n::t("search.welcome_palette.terminal_session"), }, Icon::Terminal.to_warpui_icon(theme.foreground()), MainAxisSize::Max, diff --git a/app/src/server/iap.rs b/app/src/server/iap.rs index 6fe924d966..d45660379a 100644 --- a/app/src/server/iap.rs +++ b/app/src/server/iap.rs @@ -292,8 +292,9 @@ impl IapManager { let Some(window_id) = window_id else { return; }; - let toast: DismissibleToast = - DismissibleToast::error(format!("IAP credential refresh failed: {message}")); + let toast: DismissibleToast = DismissibleToast::error( + i18n::t("server.iap.credential_refresh_failed").replace("{message}", &message), + ); ToastStack::handle(ctx).update(ctx, |stack, ctx| { stack.add_ephemeral_toast(toast, window_id, ctx); }); diff --git a/app/src/server/network_log_view.rs b/app/src/server/network_log_view.rs index e0b3b6d537..a043bbcab9 100644 --- a/app/src/server/network_log_view.rs +++ b/app/src/server/network_log_view.rs @@ -30,12 +30,6 @@ use crate::server::network_logging::NetworkLogModel; use crate::ui_components::buttons::icon_button_with_color; use crate::ui_components::{blended_colors, icons}; -/// Header text for the network log pane. -pub const NETWORK_LOG_HEADER_TEXT: &str = "Network log"; - -/// Tooltip shown on hover over the refresh button in the pane header. -const REFRESH_TOOLTIP: &str = "Refresh"; - /// Event emitted by the [`NetworkLogView`]. #[derive(Debug, Clone, PartialEq, Eq)] pub enum NetworkLogViewEvent { @@ -65,7 +59,7 @@ pub struct NetworkLogView { impl NetworkLogView { pub fn new(ctx: &mut ViewContext) -> Self { let pane_configuration = - ctx.add_model(|_ctx| PaneConfiguration::new(NETWORK_LOG_HEADER_TEXT)); + ctx.add_model(|_ctx| PaneConfiguration::new(i18n::t("server.network_log.title"))); // Capture a one-shot snapshot of the model. We intentionally do not // subscribe to the model: new items that arrive after the pane is @@ -153,7 +147,7 @@ impl NetworkLogView { ) .with_tooltip(move || { ui_builder - .tool_tip(REFRESH_TOOLTIP.to_string()) + .tool_tip(i18n::t("server.network_log.refresh")) .build() .finish() }) @@ -229,7 +223,7 @@ impl BackingView for NetworkLogView { app: &AppContext, ) -> HeaderContent { HeaderContent::Standard(StandardHeader { - title: NETWORK_LOG_HEADER_TEXT.to_string(), + title: i18n::t("server.network_log.title"), title_secondary: None, title_style: None, title_clip_config: ClipConfig::start(), diff --git a/app/src/session_management.rs b/app/src/session_management.rs index ed480373bd..41a54ed38e 100644 --- a/app/src/session_management.rs +++ b/app/src/session_management.rs @@ -77,14 +77,21 @@ impl CommandContext { Self::None => None, Self::LastRunCommand { last_run_command, .. - } => Some(format!("Last run command {}", last_run_command.clone())), - Self::LastRunAIBlock { prompt } => Some(format!("Last AI interaction: {prompt}")), - Self::RunningCommand { running_command } => { - Some(format!("Currently running {running_command}")) - } - Self::RunningAIBlock { prompt } => { - Some(format!("Currently running AI interaction: {prompt}")) - } + } => Some( + i18n::t("session_management.a11y.last_run_command") + .replace("{command}", last_run_command), + ), + Self::LastRunAIBlock { prompt } => Some( + i18n::t("session_management.a11y.last_ai_interaction").replace("{prompt}", prompt), + ), + Self::RunningCommand { running_command } => Some( + i18n::t("session_management.a11y.currently_running_command") + .replace("{command}", running_command), + ), + Self::RunningAIBlock { prompt } => Some( + i18n::t("session_management.a11y.currently_running_ai_interaction") + .replace("{prompt}", prompt), + ), } } } diff --git a/app/src/settings/accessibility.rs b/app/src/settings/accessibility.rs index 24966b32b6..b824106591 100644 --- a/app/src/settings/accessibility.rs +++ b/app/src/settings/accessibility.rs @@ -11,6 +11,6 @@ define_settings_group!(AccessibilitySettings, settings: [ private: false, storage_key: "AccessibilityVerbosity", toml_path: "accessibility.accessibility_verbosity", - description: "The verbosity level for screen reader announcements.", + description_key: "settings.schema.accessibility.accessibility_verbosity.description", } ]); diff --git a/app/src/settings/ai.rs b/app/src/settings/ai.rs index f5b1513854..019a52d848 100644 --- a/app/src/settings/ai.rs +++ b/app/src/settings/ai.rs @@ -149,7 +149,7 @@ settings::macros::implement_setting_for_enum!( SyncToCloud::Never, private: false, toml_path: "agents.voice.voice_input_toggle_key", - description: "The key used to toggle voice input.", + description_key: "settings.schema.agents.voice.voice_input_toggle_key.description", ); impl VoiceInputToggleKey { @@ -255,20 +255,20 @@ impl VoiceInputToggleKey { VoiceInputToggleKey::AltLeft | VoiceInputToggleKey::ControlLeft | VoiceInputToggleKey::SuperLeft - | VoiceInputToggleKey::ShiftLeft => Some("Left"), + | VoiceInputToggleKey::ShiftLeft => Some(i18n::t("common.left")), VoiceInputToggleKey::AltRight | VoiceInputToggleKey::ControlRight | VoiceInputToggleKey::SuperRight - | VoiceInputToggleKey::ShiftRight => Some("Right"), + | VoiceInputToggleKey::ShiftRight => Some(i18n::t("common.right")), VoiceInputToggleKey::None | VoiceInputToggleKey::Fn => None, }; let key_name = match side { Some(side) => format!("{side} {symbol}"), None => symbol, }; - format!("Voice input (hold {key_name} key)") + i18n::t("settings.ai.voice_input.tooltip_with_key").replace("{key}", &key_name) } - None => "Voice input".to_string(), + None => i18n::t("settings.ai.voice_input.tooltip"), } } @@ -317,7 +317,7 @@ settings::macros::implement_setting_for_enum!( SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "general.default_session_mode", - description: "The default mode for new terminal sessions.", + description_key: "settings.schema.general.default_session_mode.description", ); impl DefaultSessionMode { @@ -367,7 +367,7 @@ settings::macros::implement_setting_for_enum!( SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.thinking_display_mode", - description: "Controls how agent thinking traces are displayed after streaming.", + description_key: "settings.schema.agents.warp_agent.other.thinking_display_mode.description", ); impl ThinkingDisplayMode { @@ -577,7 +577,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No), private: false, toml_path: "agents.warp_agent.is_any_ai_enabled", - description: "Controls whether all AI features are enabled.", + description_key: "settings.schema.agents.warp_agent.is_any_ai_enabled.description", }, // This field should not be referenced directly to lookup active AI enablement -- use the // `is_active_ai_enabled()` getter. @@ -588,7 +588,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No), private: false, toml_path: "agents.warp_agent.active_ai.enabled", - description: "Controls whether proactive AI features like suggestions are enabled.", + description_key: "settings.schema.agents.warp_agent.active_ai.enabled.description", }, // This field should not be referenced directly to lookup autodetection enablement -- use the // `is_ai_autodetection_enabled()` getter. @@ -599,7 +599,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.input.ai_auto_detection_enabled", - description: "Controls whether AI automatically detects natural language input.", + description_key: "settings.schema.agents.warp_agent.input.ai_auto_detection_enabled.description", }, // This field should not be referenced directly -- use the // `is_nld_in_terminal_enabled()` getter. @@ -613,7 +613,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.input.nld_in_terminal_enabled", - description: "Controls whether natural language detection is enabled in the terminal input.", + description_key: "settings.schema.agents.warp_agent.input.nld_in_terminal_enabled.description", }, autodetection_command_denylist: AICommandDenylist { type: String, @@ -622,7 +622,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.input.ai_command_denylist", - description: "Commands to exclude from AI natural language autodetection.", + description_key: "settings.schema.agents.warp_agent.input.ai_command_denylist.description", }, // This field should not be referenced directly to lookup intelligent autosuggestion enablement // -- use the `is_intelligent_autosuggestions_enabled()` getter. @@ -633,7 +633,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.active_ai.intelligent_autosuggestions_enabled", - description: "Controls whether AI-powered intelligent autosuggestions are enabled.", + description_key: "settings.schema.agents.warp_agent.active_ai.intelligent_autosuggestions_enabled.description", } // This field should not be referenced directly to lookup Prompt Suggestions // enablement -- use the `is_prompt_suggestions_enabled()` getter. @@ -647,7 +647,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.active_ai.agent_mode_query_suggestions_enabled", - description: "Controls whether prompt suggestions are shown in agent mode.", + description_key: "settings.schema.agents.warp_agent.active_ai.agent_mode_query_suggestions_enabled.description", } // This field should not be referenced directly to lookup Code Suggestions @@ -659,7 +659,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.active_ai.code_suggestions_enabled", - description: "Controls whether AI code suggestions are enabled.", + description_key: "settings.schema.agents.warp_agent.active_ai.code_suggestions_enabled.description", } // This field should not be referenced directly to lookup natural language autosuggestions // enablement -- use the `is_natural_language_autosuggestions_enabled()` getter. @@ -671,7 +671,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.active_ai.natural_language_autosuggestions_enabled", - description: "Controls whether ghosted text autosuggestions are shown for AI input queries.", + description_key: "settings.schema.agents.warp_agent.active_ai.natural_language_autosuggestions_enabled.description", feature_flag: FeatureFlag::PredictAMQueries, } // This field should not be referenced directly to lookup shared block title generations @@ -684,7 +684,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.active_ai.shared_block_title_generation_enabled", - description: "Controls whether titles are auto-generated when sharing blocks.", + description_key: "settings.schema.agents.warp_agent.active_ai.shared_block_title_generation_enabled.description", } // This field should not be referenced directly to lookup git operations AI autogen // enablement -- use the `is_git_operations_autogen_enabled()` getter. @@ -695,7 +695,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.active_ai.git_operations_autogen_enabled", - description: "Controls whether AI auto-generates commit messages and PR title/body in the code review dialogs.", + description_key: "settings.schema.agents.warp_agent.active_ai.git_operations_autogen_enabled.description", } // This field should not be referenced directly to lookup Rule Suggestions // enablement -- use the `is_rule_suggestions_enabled()` getter. @@ -706,7 +706,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.active_ai.rule_suggestions_enabled", - description: "Controls whether the agent suggests rules to save after responses.", + description_key: "settings.schema.agents.warp_agent.active_ai.rule_suggestions_enabled.description", feature_flag: FeatureFlag::SuggestedRules, } // This field should not be referenced directly to lookup Voice AI enablement -- use the @@ -718,7 +718,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.voice.voice_input_enabled", - description: "Controls whether voice input is enabled for AI interactions.", + description_key: "settings.schema.agents.voice.voice_input_enabled.description", }, // The number of times the user has entered Agent Mode. // Not a user-visible setting. We model it so we can show the voice input new feature popup @@ -764,7 +764,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.profiles.agent_mode_command_execution_allowlist", - description: "Commands that the agent can execute without explicit permission.", + description_key: "settings.schema.agents.profiles.agent_mode_command_execution_allowlist.description", }, // Predicates that Agent Mode can use to decide if a command must // be executed by the user. @@ -778,7 +778,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.profiles.agent_mode_command_execution_denylist", - description: "Commands that the agent must always ask before executing.", + description_key: "settings.schema.agents.profiles.agent_mode_command_execution_denylist.description", }, // Enabled iff Agent Mode can execute readonly commands without explicit user consent. // @@ -791,7 +791,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.profiles.agent_mode_execute_readonly_commands", - description: "Whether the agent can auto-execute read-only commands without asking.", + description_key: "settings.schema.agents.profiles.agent_mode_execute_readonly_commands.description", }, // Determines coding permissions that Agent Mode has. // Note that if Agent Mode has permissions to execute readonly commands, @@ -806,7 +806,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.profiles.agent_mode_coding_permissions", - description: "The file read permission level for the agent.", + description_key: "settings.schema.agents.profiles.agent_mode_coding_permissions.description", } // Specific filepaths that Agent Mode can read without asking for additional permissions. // These should be persisted as absolute filepaths to avoid ambiguity. @@ -822,7 +822,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.profiles.agent_mode_coding_file_read_allowlist", - description: "File paths the agent can read without asking for permission.", + description_key: "settings.schema.agents.profiles.agent_mode_coding_file_read_allowlist.description", } // Whether or not the profile-level command autoexecution speedbump has been shown. // @@ -889,7 +889,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "cloud_platform.third_party_api_keys.aws_bedrock_credentials_enabled", - description: "Whether Warp should use your local AWS credentials for Bedrock-enabled requests.", + description_key: "settings.schema.cloud_platform.third_party_api_keys.aws_bedrock_credentials_enabled.description", } // Whether to automatically run the AWS login command when Bedrock credentials are expired. // @@ -902,7 +902,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "cloud_platform.third_party_api_keys.aws_bedrock_auto_login", - description: "Whether to automatically run the AWS login command when Bedrock credentials expire.", + description_key: "settings.schema.cloud_platform.third_party_api_keys.aws_bedrock_auto_login.description", } // Command to run to refresh AWS credentials when using Bedrock auto-login. aws_bedrock_auth_refresh_command: AwsBedrockAuthRefreshCommand { @@ -912,7 +912,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "cloud_platform.third_party_api_keys.aws_bedrock_auth_refresh_command", - description: "The command to run to refresh AWS credentials for Bedrock.", + description_key: "settings.schema.cloud_platform.third_party_api_keys.aws_bedrock_auth_refresh_command.description", } // AWS profile name to use when loading credentials from the local AWS credential/config chain. aws_bedrock_profile: AwsBedrockProfile { @@ -922,7 +922,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "cloud_platform.third_party_api_keys.aws_bedrock_profile", - description: "The AWS profile name to use for Bedrock credentials.", + description_key: "settings.schema.cloud_platform.third_party_api_keys.aws_bedrock_profile.description", } // Whether the AWS Bedrock login banner has been permanently dismissed. // @@ -942,7 +942,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.knowledge.rules_enabled", - description: "Whether the agent uses your saved rules during requests.", + description_key: "settings.schema.agents.knowledge.rules_enabled.description", } // Whether warp drive context should be included in AI requests warp_drive_context_enabled: WarpDriveContextEnabled { @@ -952,7 +952,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.knowledge.warp_drive_context_enabled", - description: "Whether Warp Drive context is included in AI requests.", + description_key: "settings.schema.agents.knowledge.warp_drive_context_enabled.description", } // Whether the codebase speedbump banner has been permanently dismissed for a given repo path. @@ -1076,7 +1076,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.should_show_oz_updates_in_zero_state", - description: "Whether the \"What's new\" section is shown in the agent view.", + description_key: "settings.schema.agents.warp_agent.other.should_show_oz_updates_in_zero_state.description", } // Whether or not the user has enabled fallback to Warp credits for user-provided models. @@ -1088,7 +1088,7 @@ define_settings_group!(AISettings, settings: [ private: false, storage_key: "CanUseWarpCreditsWithByok", toml_path: "cloud_platform.third_party_api_keys.can_use_warp_credits_with_byok", - description: "Whether Warp credits can be used as a fallback for user-provided models.", + description_key: "settings.schema.cloud_platform.third_party_api_keys.can_use_warp_credits_with_byok.description", } should_render_use_agent_footer_for_user_commands: ShouldRenderUseAgentToolbarForUserCommands { @@ -1098,7 +1098,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.should_render_use_agent_toolbar_for_user_commands", - description: "Whether to show the \"Use Agent\" footer for terminal commands.", + description_key: "settings.schema.agents.warp_agent.other.should_render_use_agent_toolbar_for_user_commands.description", } // Whether to render the CLI agent footer for commands like Claude, Codex, Gemini, etc. @@ -1110,7 +1110,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.third_party.should_render_cli_agent_toolbar", - description: "Whether to show the CLI agent footer for coding agent commands.", + description_key: "settings.schema.agents.third_party.should_render_cli_agent_toolbar.description", } // When enabled and a CLI agent session has a plugin listener, rich input // auto-closes when the session enters a Blocked state (the agent requires @@ -1122,7 +1122,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.third_party.auto_toggle_composer", - description: "Whether CLI agent Rich Input automatically closes and reopens based on the agent's blocked state.", + description_key: "settings.schema.agents.third_party.auto_toggle_composer.description", } // When enabled and a CLI agent session has a plugin listener, rich input @@ -1134,7 +1134,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.third_party.auto_open_composer_on_cli_agent_start", - description: "Whether CLI agent Rich Input automatically opens when a CLI agent session starts.", + description_key: "settings.schema.agents.third_party.auto_open_composer_on_cli_agent_start.description", } // When enabled and a CLI agent session does NOT have a plugin listener, @@ -1148,7 +1148,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.third_party.auto_dismiss_composer_after_submit", - description: "Whether CLI agent Rich Input automatically closes after the user submits a prompt.", + description_key: "settings.schema.agents.third_party.auto_dismiss_composer_after_submit.description", } // Maps custom toolbar command regex patterns to specific CLI agents. @@ -1163,7 +1163,7 @@ define_settings_group!(AISettings, settings: [ private: false, toml_path: "agents.third_party.cli_agent_toolbar_enabled_commands", max_table_depth: 1, - description: "Maps custom toolbar command patterns to specific CLI agents.", + description_key: "settings.schema.agents.third_party.cli_agent_toolbar_enabled_commands.description", } // This is not a user-visible setting - it tracks whether a paid user has dismissed the @@ -1231,7 +1231,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.cloud_agent_computer_use_enabled", - description: "Whether computer use is enabled for cloud agent conversations.", + description_key: "settings.schema.agents.warp_agent.other.cloud_agent_computer_use_enabled.description", } @@ -1245,7 +1245,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.mcp_servers.file_based_mcp_enabled", - description: "Whether third-party file-based MCP servers are automatically detected.", + description_key: "settings.schema.agents.mcp_servers.file_based_mcp_enabled.description", } // Controls how agent thinking/reasoning traces are displayed. @@ -1261,7 +1261,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.input.include_agent_commands_in_history", - description: "Whether agent-executed commands are included in command history.", + description_key: "settings.schema.agents.warp_agent.input.include_agent_commands_in_history.description", } // Controls whether the conversation history view appears in the tools panel. @@ -1272,7 +1272,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.show_conversation_history", - description: "Whether conversation history appears in the tools panel.", + description_key: "settings.schema.agents.warp_agent.other.show_conversation_history.description", } @@ -1284,7 +1284,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.show_agent_notifications", - description: "Whether agent notifications are shown.", + description_key: "settings.schema.agents.warp_agent.other.show_agent_notifications.description", } // Per-agent, per-host tracking of whether the user dismissed the plugin install chip. @@ -1321,7 +1321,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No), private: false, toml_path: "agents.warp_agent.other.agent_attribution_enabled", - description: "Whether the Warp Agent adds an attribution co-author line to commit messages and pull requests it creates.", + description_key: "settings.schema.agents.warp_agent.other.agent_attribution_enabled.description", } should_force_disable_cloud_handoff: ShouldForceDisableCloudHandoff { @@ -1331,7 +1331,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.should_force_disable_cloud_handoff", - description: "Whether to force-disable local-to-cloud handoff.", + description_key: "settings.schema.agents.warp_agent.other.should_force_disable_cloud_handoff.description", } should_force_disable_ampersand_handoff: ShouldForceDisableAmpersandHandoff { @@ -1341,7 +1341,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.should_force_disable_ampersand_handoff", - description: "Whether to force-disable the & prefix for cloud handoff compose mode.", + description_key: "settings.schema.agents.warp_agent.other.should_force_disable_ampersand_handoff.description", } auto_handoff_on_sleep_enabled: AutoHandoffOnSleepEnabled { @@ -1351,7 +1351,7 @@ define_settings_group!(AISettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.auto_handoff_on_sleep_enabled", - description: "Whether Warp automatically hands off local agent conversations to cloud when the computer is about to sleep.", + description_key: "settings.schema.agents.warp_agent.other.auto_handoff_on_sleep_enabled.description", } ]); diff --git a/app/src/settings/alias_expansion.rs b/app/src/settings/alias_expansion.rs index fdfc8d972c..3526c66b36 100644 --- a/app/src/settings/alias_expansion.rs +++ b/app/src/settings/alias_expansion.rs @@ -9,6 +9,6 @@ define_settings_group!(AliasExpansionSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.input.alias_expansion_enabled", - description: "Whether shell alias expansion is enabled in the input.", + description_key: "settings.schema.terminal.input.alias_expansion_enabled.description", }, ]); diff --git a/app/src/settings/app_icon.rs b/app/src/settings/app_icon.rs index 5a80271e24..832f5a52c8 100644 --- a/app/src/settings/app_icon.rs +++ b/app/src/settings/app_icon.rs @@ -130,6 +130,6 @@ define_settings_group!(AppIconSettings, settings: [ private: false, storage_key: "AppIcon", toml_path: "appearance.icon.app_icon", - description: "The app icon displayed in the dock.", + description_key: "settings.schema.appearance.icon.app_icon.description", }, ]); diff --git a/app/src/settings/block_visibility.rs b/app/src/settings/block_visibility.rs index 938c4d48c9..96706d52fe 100644 --- a/app/src/settings/block_visibility.rs +++ b/app/src/settings/block_visibility.rs @@ -11,7 +11,7 @@ define_settings_group!(BlockVisibilitySettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "appearance.blocks.should_show_bootstrap_block", - description: "Whether the bootstrap block is visible in the terminal.", + description_key: "settings.schema.appearance.blocks.should_show_bootstrap_block.description", }, should_show_in_band_command_blocks: ShouldShowInBandCommandBlocks { type: bool, @@ -20,7 +20,7 @@ define_settings_group!(BlockVisibilitySettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "appearance.blocks.should_show_in_band_command_blocks", - description: "Whether in-band command blocks are visible in the terminal.", + description_key: "settings.schema.appearance.blocks.should_show_in_band_command_blocks.description", }, should_show_ssh_block: ShouldShowSSHBlock { type: bool, @@ -29,6 +29,6 @@ define_settings_group!(BlockVisibilitySettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "appearance.blocks.should_show_ssh_block", - description: "Whether the SSH connection block is visible in the terminal.", + description_key: "settings.schema.appearance.blocks.should_show_ssh_block.description", } ]); diff --git a/app/src/settings/changelog.rs b/app/src/settings/changelog.rs index 883293d89e..d47c2dd321 100644 --- a/app/src/settings/changelog.rs +++ b/app/src/settings/changelog.rs @@ -9,6 +9,6 @@ define_settings_group!(ChangelogSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "general.show_changelog_after_update", - description: "Whether the changelog is shown after an update.", + description_key: "settings.schema.general.show_changelog_after_update.description", }, ]); diff --git a/app/src/settings/cloud_preferences.rs b/app/src/settings/cloud_preferences.rs index a4e57b8888..082211511e 100644 --- a/app/src/settings/cloud_preferences.rs +++ b/app/src/settings/cloud_preferences.rs @@ -16,7 +16,7 @@ define_settings_group!(CloudPreferencesSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No), private: false, toml_path: "account.is_settings_sync_enabled", - description: "Whether settings are synced across devices via the cloud.", + description_key: "settings.schema.account.is_settings_sync_enabled.description", }, ]); diff --git a/app/src/settings/code.rs b/app/src/settings/code.rs index e2018d0d7d..74db1dcc58 100644 --- a/app/src/settings/code.rs +++ b/app/src/settings/code.rs @@ -9,7 +9,7 @@ define_settings_group!(CodeSettings, settings: [ sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "code.editor.use_warp_as_default_editor", - description: "Whether Warp is used as the default code editor.", + description_key: "settings.schema.code.editor.use_warp_as_default_editor.description", } codebase_context_enabled: CodebaseContextEnabled { type: bool, @@ -19,7 +19,7 @@ define_settings_group!(CodeSettings, settings: [ private: false, storage_key: "AgentModeCodebaseContext", toml_path: "code.indexing.agent_mode_codebase_context", - description: "Whether codebase context is provided to the AI agent.", + description_key: "settings.schema.code.indexing.agent_mode_codebase_context.description", }, auto_indexing_enabled: AutoIndexingEnabled { type: bool, @@ -29,7 +29,7 @@ define_settings_group!(CodeSettings, settings: [ private: false, storage_key: "AgentModeCodebaseContextAutoIndexing", toml_path: "code.indexing.agent_mode_codebase_context_auto_indexing", - description: "Whether automatic codebase indexing is enabled.", + description_key: "settings.schema.code.indexing.agent_mode_codebase_context_auto_indexing.description", }, // Whether or not the user has manually dismissed the code toolbelt new feature popup. dismissed_code_toolbelt_new_feature_popup: DismissedCodeToolbeltNewFeaturePopup { @@ -47,7 +47,7 @@ define_settings_group!(CodeSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "code.editor.show_project_explorer", - description: "Whether the project explorer is shown in the tools panel.", + description_key: "settings.schema.code.editor.show_project_explorer.description", }, // Controls whether global file search appears in the tools panel. show_global_search: ShowGlobalSearch { @@ -57,6 +57,6 @@ define_settings_group!(CodeSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "code.editor.show_global_search", - description: "Whether global file search is shown in the tools panel.", + description_key: "settings.schema.code.editor.show_global_search.description", }, ]); diff --git a/app/src/settings/editor.rs b/app/src/settings/editor.rs index 121ba53934..e1d928da6b 100644 --- a/app/src/settings/editor.rs +++ b/app/src/settings/editor.rs @@ -187,7 +187,7 @@ define_settings_group!(AppEditorSettings, settings: [ private: false, storage_key: "CursorBlink", toml_path: "appearance.cursor.cursor_blink", - description: "Whether the cursor blinks.", + description_key: "settings.schema.appearance.cursor.cursor_blink.description", }, cursor_display_type: CursorDisplayState { type: CursorDisplayType, @@ -197,7 +197,7 @@ define_settings_group!(AppEditorSettings, settings: [ private: false, storage_key: "CursorDisplayType", toml_path: "appearance.cursor.cursor_display_type", - description: "The visual style of the cursor.", + description_key: "settings.schema.appearance.cursor.cursor_display_type.description", }, vim_mode: VimModeEnabled { type: bool, @@ -206,7 +206,7 @@ define_settings_group!(AppEditorSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "text_editing.vim_mode_enabled", - description: "Whether Vim keybindings are enabled.", + description_key: "settings.schema.text_editing.vim_mode_enabled.description", }, vim_unnamed_system_clipboard: VimUnnamedSystemClipboard { type: bool, @@ -215,7 +215,7 @@ define_settings_group!(AppEditorSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "text_editing.vim_unnamed_system_clipboard", - description: "Whether the Vim unnamed register uses the system clipboard.", + description_key: "settings.schema.text_editing.vim_unnamed_system_clipboard.description", }, vim_status_bar: VimStatusBar { type: bool, @@ -224,7 +224,7 @@ define_settings_group!(AppEditorSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "text_editing.vim_status_bar", - description: "Whether the Vim status bar is displayed.", + description_key: "settings.schema.text_editing.vim_status_bar.description", }, code_editor_line_number_mode: CodeEditorLineNumberModeSetting { type: CodeEditorLineNumberMode, @@ -233,7 +233,7 @@ define_settings_group!(AppEditorSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "text_editing.code_editor_line_number_mode", - description: "How line numbers are displayed in code editors.", + description_key: "settings.schema.text_editing.code_editor_line_number_mode.description", }, autocomplete_symbols: AutocompleteSymbols { type: bool, @@ -242,7 +242,7 @@ define_settings_group!(AppEditorSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "text_editing.autocomplete_symbols", - description: "Whether matching symbols like brackets and quotes are auto-completed.", + description_key: "settings.schema.text_editing.autocomplete_symbols.description", }, enable_autosuggestions: EnableAutosuggestions { type: bool, @@ -252,7 +252,7 @@ define_settings_group!(AppEditorSettings, settings: [ private: false, storage_key: "Autosuggestions", toml_path: "terminal.input.autosuggestions.enabled", - description: "Whether command autosuggestions are shown.", + description_key: "settings.schema.terminal.input.autosuggestions.enabled.description", }, autosuggestion_keybinding_hint: AutosuggestionKeybindingHint { type: bool, @@ -261,7 +261,7 @@ define_settings_group!(AppEditorSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.input.autosuggestions.keybinding_hint", - description: "Whether autosuggestion keybinding hints are displayed.", + description_key: "settings.schema.terminal.input.autosuggestions.keybinding_hint.description", }, show_autosuggestion_ignore_button: ShowAutosuggestionIgnoreButton { type: bool, @@ -270,7 +270,7 @@ define_settings_group!(AppEditorSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.input.autosuggestions.show_ignore_button", - description: "Whether the ignore button is shown for autosuggestions.", + description_key: "settings.schema.terminal.input.autosuggestions.show_ignore_button.description", }, ]); diff --git a/app/src/settings/font.rs b/app/src/settings/font.rs index 56916e657c..e6515adb0b 100644 --- a/app/src/settings/font.rs +++ b/app/src/settings/font.rs @@ -22,7 +22,7 @@ define_settings_group!(FontSettings, private: false, storage_key: "FontName", toml_path: "appearance.text.font_name", - description: "The monospace font used in the terminal.", + description_key: "settings.schema.appearance.text.font_name.description", }, monospace_font_size: MonospaceFontSize { type: f32, @@ -32,7 +32,7 @@ define_settings_group!(FontSettings, private: false, storage_key: "FontSize", toml_path: "appearance.text.font_size", - description: "The size of the monospace font in the terminal.", + description_key: "settings.schema.appearance.text.font_size.description", }, monospace_font_weight: MonospaceFontWeight { type: Weight, @@ -42,7 +42,7 @@ define_settings_group!(FontSettings, private: false, storage_key: "FontWeight", toml_path: "appearance.text.font_weight", - description: "The weight of the monospace font in the terminal.", + description_key: "settings.schema.appearance.text.font_weight.description", }, line_height_ratio: LineHeightRatio { type: f32, @@ -51,7 +51,7 @@ define_settings_group!(FontSettings, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "appearance.text.line_height_ratio", - description: "The line height ratio for terminal text.", + description_key: "settings.schema.appearance.text.line_height_ratio.description", }, ai_font_name: AIFontName { type: String, @@ -61,7 +61,7 @@ define_settings_group!(FontSettings, private: false, storage_key: "AIFontName", toml_path: "appearance.text.ai_font_name", - description: "The font used for AI-generated content.", + description_key: "settings.schema.appearance.text.ai_font_name.description", }, match_ai_font_to_terminal_font: MatchAIFontToTerminalFont { type: bool, @@ -71,7 +71,7 @@ define_settings_group!(FontSettings, private: false, storage_key: "MatchAIFont", toml_path: "appearance.text.match_ai_font", - description: "Whether the AI font automatically matches the terminal font.", + description_key: "settings.schema.appearance.text.match_ai_font.description", }, notebook_font_size: NotebookFontSize { type: f32, @@ -80,7 +80,7 @@ define_settings_group!(FontSettings, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "appearance.text.notebook_font_size", - description: "The font size used in notebooks.", + description_key: "settings.schema.appearance.text.notebook_font_size.description", }, match_notebook_to_monospace_font_size: MatchNotebookToMonospaceFontSize { type: bool, @@ -89,7 +89,7 @@ define_settings_group!(FontSettings, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "appearance.text.match_notebook_to_monospace_font_size", - description: "Whether the notebook font size matches the terminal font size.", + description_key: "settings.schema.appearance.text.match_notebook_to_monospace_font_size.description", }, enforce_minimum_contrast: EnforceMinimumContrast { type: EnforceMinimumContrastEnum, @@ -98,7 +98,7 @@ define_settings_group!(FontSettings, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "appearance.text.enforce_minimum_contrast", - description: "Whether to enforce minimum contrast for text readability.", + description_key: "settings.schema.appearance.text.enforce_minimum_contrast.description", }, use_thin_strokes: UseThinStrokes { type: ThinStrokes, @@ -107,7 +107,7 @@ define_settings_group!(FontSettings, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "appearance.text.use_thin_strokes", - description: "Whether to use thin font strokes on macOS.", + description_key: "settings.schema.appearance.text.use_thin_strokes.description", }, ] ); diff --git a/app/src/settings/gpu.rs b/app/src/settings/gpu.rs index 6896c7b283..48ca622b81 100644 --- a/app/src/settings/gpu.rs +++ b/app/src/settings/gpu.rs @@ -12,7 +12,7 @@ define_settings_group!(GPUSettings, settings: [ sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "system.prefer_low_power_gpu", - description: "Whether to prefer the integrated (low-power) GPU.", + description_key: "settings.schema.system.prefer_low_power_gpu.description", }, preferred_backend: PreferredGraphicsBackend { type: Option, @@ -21,6 +21,6 @@ define_settings_group!(GPUSettings, settings: [ sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "system.preferred_graphics_backend", - description: "The preferred graphics backend on Windows.", + description_key: "settings.schema.system.preferred_graphics_backend.description", }, ]); diff --git a/app/src/settings/import/view.rs b/app/src/settings/import/view.rs index 39019f3d16..564a63526b 100644 --- a/app/src/settings/import/view.rs +++ b/app/src/settings/import/view.rs @@ -261,7 +261,7 @@ impl SettingsImportView { font_size: Some(FONT_SIZE), ..Default::default() }) - .with_centered_text_label("Import".to_owned()) + .with_centered_text_label(i18n::t("settings.import.import_button")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(SettingsImportAction::ImportButtonClicked); @@ -288,7 +288,7 @@ impl SettingsImportView { background: Some(appearance.theme().outline().into()), ..Default::default() }) - .with_centered_text_label("Reset to Warp defaults".to_owned()) + .with_centered_text_label(i18n::t("settings.import.reset_to_defaults")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(SettingsImportAction::ResetButtonClicked); @@ -412,20 +412,32 @@ impl SettingsImportView { .any(|setting| setting.setting_type == SettingType::Theme) { if num_prefs == 1 { - preference_text_elements.push(self.render_secondary_text(appearance, "Theme")); + preference_text_elements.push( + self.render_secondary_text(appearance, i18n::t("settings.import.theme")), + ); } else { - preference_text_elements.push(self.render_secondary_text(appearance, "Theme,")); + preference_text_elements.push( + self.render_secondary_text( + appearance, + i18n::t("settings.import.theme_comma"), + ), + ); } theme_subtraction = 1; } match num_prefs - theme_subtraction { - 1 => preference_text_elements - .push(self.render_secondary_text(appearance, "1 other setting")), - 0 => (), - _ => preference_text_elements.push(self.render_secondary_text( + 1 => preference_text_elements.push(self.render_secondary_text( appearance, - format!("{} other settings", num_prefs - theme_subtraction), + i18n::t("settings.import.one_other_setting"), )), + 0 => (), + _ => preference_text_elements.push( + self.render_secondary_text( + appearance, + i18n::t("settings.import.other_settings") + .replace("{count}", &(num_prefs - theme_subtraction).to_string()), + ), + ), } } @@ -985,7 +997,7 @@ impl View for SettingsImportView { if display_new_session_text { new_session_setting_text = Container::new( Text::new( - "Some settings will take effect when you open a new session.", + i18n::t("settings.import.new_session_notice"), font_family, font_size, ) diff --git a/app/src/settings/init.rs b/app/src/settings/init.rs index 33df98dca5..fc03bc09e5 100644 --- a/app/src/settings/init.rs +++ b/app/src/settings/init.rs @@ -11,6 +11,7 @@ use super::app_icon::AppIconSettings; use super::app_installation_detection::UserAppInstallDetectionSettings; use super::cloud_preferences::CloudPreferencesSettings; use super::initializer::SettingsInitializer; +use super::language::LanguageSettings; use super::native_preference::NativePreferenceSettings; use super::{ AISettings, AccessibilitySettings, AliasExpansionSettings, AppEditorSettings, @@ -83,6 +84,7 @@ pub fn register_all_settings(ctx: &mut AppContext) { WarpDrivePrivacySettings::register(ctx); UserAppInstallDetectionSettings::register(ctx); AppIconSettings::register(ctx); + LanguageSettings::register(ctx); AppEditorSettings::register(ctx); InputSettings::register(ctx); WarpifySettings::register(ctx); @@ -124,6 +126,11 @@ pub fn init( migrate_native_settings_to_settings_file(ctx); } + // Initialize the i18n layer from the saved language preference so the very + // first frame renders in the user's chosen language (Chinese by default). + let language = LanguageSettings::as_ref(ctx).language(); + i18n::set_locale(language.locale_tag()); + let use_thin_strokes = *FontSettings::as_ref(ctx).use_thin_strokes; let general_settings = GeneralSettings::as_ref(ctx); @@ -197,6 +204,18 @@ pub fn init( }, ); + // When the user switches languages, swap the active i18n catalog and + // repaint every view in every window so all labels update live — no + // restart required. + ctx.subscribe_to_model( + &LanguageSettings::handle(ctx), + |language_settings, _event, ctx| { + let language = language_settings.as_ref(ctx).language(); + i18n::set_locale(language.locale_tag()); + ctx.invalidate_all_views(); + }, + ); + appearance::register(ctx); // Set up hot-reload for the settings file. When the WarpConfig watcher diff --git a/app/src/settings/input.rs b/app/src/settings/input.rs index 049fc7a2be..29cfa260ee 100644 --- a/app/src/settings/input.rs +++ b/app/src/settings/input.rs @@ -42,7 +42,7 @@ define_settings_group!(InputSettings, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.input.show_hint_text", - description: "Whether hint text is shown in the terminal input.", + description_key: "settings.schema.terminal.input.show_hint_text.description", }, classic_completions_mode: ClassicCompletionsMode { type: bool, @@ -51,7 +51,7 @@ define_settings_group!(InputSettings, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.input.classic_completions_mode", - description: "Whether classic completions mode is enabled.", + description_key: "settings.schema.terminal.input.classic_completions_mode.description", }, completions_open_while_typing: CompletionsOpenWhileTyping { type: bool, @@ -60,7 +60,7 @@ define_settings_group!(InputSettings, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.input.completions_open_while_typing", - description: "Whether the completions menu opens automatically while typing.", + description_key: "settings.schema.terminal.input.completions_open_while_typing.description", }, error_underlining: ErrorUnderliningEnabled { type: bool, @@ -69,7 +69,7 @@ define_settings_group!(InputSettings, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.input.error_underlining_enabled", - description: "Whether command errors are underlined in the input.", + description_key: "settings.schema.terminal.input.error_underlining_enabled.description", }, syntax_highlighting: SyntaxHighlighting { type: bool, @@ -78,7 +78,7 @@ define_settings_group!(InputSettings, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.input.syntax_highlighting", - description: "Whether syntax highlighting is enabled in the terminal input.", + description_key: "settings.schema.terminal.input.syntax_highlighting.description", }, command_corrections: CommandCorrections { type: bool, @@ -87,7 +87,7 @@ define_settings_group!(InputSettings, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.input.command_corrections", - description: "Whether command corrections are suggested for mistyped commands.", + description_key: "settings.schema.terminal.input.command_corrections.description", }, workflows_box_expanded: WorkflowsBoxExpanded { type: bool, @@ -111,7 +111,7 @@ define_settings_group!(InputSettings, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.input.input_box_type_setting", - description: "The terminal input style.", + description_key: "settings.schema.terminal.input.input_box_type_setting.description", }, at_context_menu_in_terminal_mode: AtContextMenuInTerminalMode { type: bool, @@ -120,7 +120,7 @@ define_settings_group!(InputSettings, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.input.at_context_menu_in_terminal_mode", - description: "Whether the @ context menu is available in terminal mode.", + description_key: "settings.schema.terminal.input.at_context_menu_in_terminal_mode.description", }, enable_slash_commands_in_terminal: EnableSlashCommandsInTerminal { type: bool, @@ -129,7 +129,7 @@ define_settings_group!(InputSettings, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.input.enable_slash_commands_in_terminal", - description: "Whether slash commands are available in the terminal input.", + description_key: "settings.schema.terminal.input.enable_slash_commands_in_terminal.description", }, outline_codebase_symbols_for_at_context_menu: OutlineCodebaseSymbolsForAtContextMenu { type: bool, @@ -138,7 +138,7 @@ define_settings_group!(InputSettings, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.input.outline_codebase_symbols_for_at_context_menu", - description: "Whether codebase symbols appear in the @ context menu.", + description_key: "settings.schema.terminal.input.outline_codebase_symbols_for_at_context_menu.description", }, completions_menu_width: CompletionsMenuWidth { type: f32, @@ -161,7 +161,7 @@ define_settings_group!(InputSettings, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.input.show_agent_tips", - description: "Whether agent tips are displayed in the input.", + description_key: "settings.schema.agents.warp_agent.input.show_agent_tips.description", }, // Whether to show the terminal input message bar (contextual hints at the bottom of terminal input). // Only applicable when FeatureFlag::AgentView is enabled. @@ -172,7 +172,7 @@ define_settings_group!(InputSettings, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.input.show_terminal_input_message_bar", - description: "Whether the terminal input message bar is shown.", + description_key: "settings.schema.terminal.input.show_terminal_input_message_bar.description", }, // Per-menu custom content heights set by drag-to-resize. Not user-visible. inline_menu_custom_content_heights: InlineMenuCustomContentHeights { diff --git a/app/src/settings/input_mode.rs b/app/src/settings/input_mode.rs index 7602c8a95b..c4270cfda3 100644 --- a/app/src/settings/input_mode.rs +++ b/app/src/settings/input_mode.rs @@ -14,7 +14,7 @@ define_settings_group!(InputModeSettings, settings: [ private: false, storage_key: "InputMode", toml_path: "appearance.input.input_mode", - description: "The position of the terminal input.", + description_key: "settings.schema.appearance.input.input_mode.description", }, ]); diff --git a/app/src/settings/language.rs b/app/src/settings/language.rs new file mode 100644 index 0000000000..79ae4a16bc --- /dev/null +++ b/app/src/settings/language.rs @@ -0,0 +1,88 @@ +use enum_iterator::Sequence; +use serde::{Deserialize, Serialize}; +use warp_core::settings::macros::define_settings_group; +use warp_core::settings::{SupportedPlatforms, SyncToCloud}; + +/// The display language (i18n locale) used throughout the Warp UI. +/// +/// Serialized to `settings.toml` as the BCP-47 tag (`"zh-CN"` / `"en"`) so the +/// file is human-readable and matches the tags consumed by the `i18n` crate. +/// Defaults to Simplified Chinese. +#[derive( + Default, + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + Sequence, + schemars::JsonSchema, + settings_value::SettingsValue, +)] +#[schemars(description = "The display language for the Warp UI.")] +pub enum Language { + /// Simplified Chinese — the default UI language. + #[default] + #[serde(rename = "zh-CN")] + #[schemars(rename = "zh-CN", description = "简体中文 (Simplified Chinese)")] + ZhCn, + #[serde(rename = "en")] + #[schemars(rename = "en", description = "English")] + En, +} + +impl Language { + /// The BCP-47 locale tag passed to [`i18n::set_locale`], e.g. `"zh-CN"`. + pub fn locale_tag(self) -> &'static str { + match self { + Language::ZhCn => "zh-CN", + Language::En => "en", + } + } +} + +impl std::fmt::Display for Language { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.locale_tag()) + } +} + +define_settings_group!(LanguageSettings, settings: [ + language: LanguageState { + type: Language, + default: Language::ZhCn, + supported_platforms: SupportedPlatforms::ALL, + sync_to_cloud: SyncToCloud::Never, + private: false, + storage_key: "Language", + toml_path: "appearance.language", + description_key: "settings.schema.appearance.language.description", + }, +]); + +impl LanguageSettings { + /// Returns the currently selected UI language. + pub fn language(&self) -> Language { + *self.language + } +} + +#[cfg(test)] +mod tests { + use super::*; + use settings::Setting as _; + + #[test] + fn defaults_to_chinese() { + assert_eq!(*LanguageState::new(None).value(), Language::ZhCn); + } + + #[test] + fn locale_tags_match_i18n_catalogs() { + assert_eq!(Language::ZhCn.locale_tag(), "zh-CN"); + assert_eq!(Language::En.locale_tag(), "en"); + // The default tag must match the i18n crate's default locale. + assert_eq!(Language::default().locale_tag(), i18n::DEFAULT_LOCALE); + } +} diff --git a/app/src/settings/linux.rs b/app/src/settings/linux.rs index 53df13542a..3ff0c3d4d0 100644 --- a/app/src/settings/linux.rs +++ b/app/src/settings/linux.rs @@ -12,7 +12,7 @@ define_settings_group!(LinuxAppConfiguration, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "system.force_x11", - description: "Whether to force X11 instead of Wayland on Linux.", + description_key: "settings.schema.system.force_x11.description", }, ] ); diff --git a/app/src/settings/mod.rs b/app/src/settings/mod.rs index 1063ad8a9d..705bb828c0 100644 --- a/app/src/settings/mod.rs +++ b/app/src/settings/mod.rs @@ -18,6 +18,7 @@ mod init; pub mod initializer; mod input; mod input_mode; +pub mod language; #[cfg(any(target_os = "linux", target_os = "freebsd"))] mod linux; pub mod macros; @@ -52,6 +53,7 @@ pub use gpu::*; pub use init::*; pub use input::*; pub use input_mode::*; +pub use language::*; #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub use linux::*; pub use native_preference::*; @@ -98,17 +100,18 @@ impl SettingsFileError { pub fn heading_and_description(&self) -> (String, String) { match self { Self::FileParseFailed(_) => ( - "Your settings file contains an error.".to_owned(), - format!("{self}. Open the file to fix it."), + i18n::t("settings.file_error.heading"), + i18n::t("settings.file_error.parse_description"), ), - Self::InvalidSettings(keys) => match keys.len() { - 1 => ( - "Your settings file contains an error.".to_owned(), - format!("{self}. The default value is being used."), + Self::InvalidSettings(keys) => match keys.as_slice() { + [key] => ( + i18n::t("settings.file_error.heading"), + i18n::t("settings.file_error.invalid_single_description").replace("{key}", key), ), _ => ( - "Your settings file contains errors.".to_owned(), - format!("{self}. Default values are being used."), + i18n::t("settings.file_error.heading_plural"), + i18n::t("settings.file_error.invalid_multiple_description") + .replace("{keys}", &keys.join(", ")), ), }, } diff --git a/app/src/settings/native_preference.rs b/app/src/settings/native_preference.rs index 4570c747cd..b4951b5dff 100644 --- a/app/src/settings/native_preference.rs +++ b/app/src/settings/native_preference.rs @@ -35,7 +35,7 @@ define_settings_group!(NativePreferenceSettings, settings: [ private: false, storage_key: "UserNativePreference", toml_path: "general.user_native_preference", - description: "Whether to prefer the native desktop app or the web app.", + description_key: "settings.schema.general.user_native_preference.description", }, preference_dialog_dismissed: UserNativePreferenceDialogDismissed { type: bool, diff --git a/app/src/settings/pane.rs b/app/src/settings/pane.rs index ebf59cd4e2..898255e3a8 100644 --- a/app/src/settings/pane.rs +++ b/app/src/settings/pane.rs @@ -9,7 +9,7 @@ define_settings_group!(PaneSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "appearance.panes.should_dim_inactive_panes", - description: "Whether inactive panes are visually dimmed.", + description_key: "settings.schema.appearance.panes.should_dim_inactive_panes.description", }, focus_panes_on_hover: FocusPaneOnHover { type: bool, @@ -18,6 +18,6 @@ define_settings_group!(PaneSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "appearance.panes.focus_pane_on_hover", - description: "Whether panes are focused when hovered over.", + description_key: "settings.schema.appearance.panes.focus_pane_on_hover.description", } ]); diff --git a/app/src/settings/privacy.rs b/app/src/settings/privacy.rs index 5d97e3a7f1..fee9a9d374 100644 --- a/app/src/settings/privacy.rs +++ b/app/src/settings/privacy.rs @@ -96,7 +96,7 @@ define_settings_group!(WarpDrivePrivacySettings, settings: [ private: false, storage_key: "TelemetryEnabled", toml_path: "privacy.telemetry_enabled", - description: "Whether anonymous usage telemetry is collected.", + description_key: "settings.schema.privacy.telemetry_enabled.description", }, is_crash_reporting_enabled: IsCrashReportingEnabled { type: bool, @@ -106,7 +106,7 @@ define_settings_group!(WarpDrivePrivacySettings, settings: [ private: false, storage_key: "CrashReportingEnabled", toml_path: "privacy.crash_reporting_enabled", - description: "Whether crash reports are sent.", + description_key: "settings.schema.privacy.crash_reporting_enabled.description", }, is_cloud_conversation_storage_enabled: IsCloudConversationStorageEnabled { type: bool, @@ -116,7 +116,7 @@ define_settings_group!(WarpDrivePrivacySettings, settings: [ private: false, storage_key: "CloudConversationStorageEnabled", toml_path: "agents.cloud_conversation_storage_enabled", - description: "Whether conversations are stored in the cloud.", + description_key: "settings.schema.agents.cloud_conversation_storage_enabled.description", }, ]); @@ -127,7 +127,7 @@ maybe_define_setting!(CustomSecretRegexList, group: PrivacySettings, { sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No), private: false, toml_path: "privacy.custom_secret_regex_list", - description: "Custom regex patterns for detecting and redacting secrets.", + description_key: "settings.schema.privacy.custom_secret_regex_list.description", }); maybe_define_setting!(HasInitializedDefaultSecretRegexes, group: PrivacySettings, { diff --git a/app/src/settings/scroll.rs b/app/src/settings/scroll.rs index 1ca2ebda8d..d1a0505d4e 100644 --- a/app/src/settings/scroll.rs +++ b/app/src/settings/scroll.rs @@ -9,6 +9,6 @@ define_settings_group!(ScrollSettings, settings: [ sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "general.mouse_scroll_multiplier", - description: "The scroll speed multiplier for mouse scroll events.", + description_key: "settings.schema.general.mouse_scroll_multiplier.description", }, ]); diff --git a/app/src/settings/select.rs b/app/src/settings/select.rs index 2c8acd254a..15c6b1f591 100644 --- a/app/src/settings/select.rs +++ b/app/src/settings/select.rs @@ -13,7 +13,7 @@ define_settings_group!(SelectionSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.copy_on_select", - description: "Whether text is automatically copied to the clipboard when selected.", + description_key: "settings.schema.terminal.copy_on_select.description", }, linux_selection_clipboard: LinuxSelectionClipboard { type: bool, @@ -22,7 +22,7 @@ define_settings_group!(SelectionSettings, settings: [ sync_to_cloud: SyncToCloud::PerPlatform(RespectUserSyncSetting::Yes), private: false, toml_path: "system.linux_selection_clipboard", - description: "Whether the Linux primary selection clipboard is used.", + description_key: "settings.schema.system.linux_selection_clipboard.description", }, middle_click_paste_enabled: MiddleClickPasteEnabled { type: bool, @@ -34,7 +34,7 @@ define_settings_group!(SelectionSettings, settings: [ sync_to_cloud: SyncToCloud::PerPlatform(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.input.middle_click_paste_enabled", - description: "Whether middle-click pastes from the clipboard.", + description_key: "settings.schema.terminal.input.middle_click_paste_enabled.description", } ]); diff --git a/app/src/settings/ssh.rs b/app/src/settings/ssh.rs index 4819a06349..9b5e7feb87 100644 --- a/app/src/settings/ssh.rs +++ b/app/src/settings/ssh.rs @@ -11,7 +11,7 @@ define_settings_group!(SshSettings, private: false, storage_key: "EnableSSHWrapper", toml_path: "warpify.ssh.enable_legacy_ssh_wrapper", - description: "Whether the legacy SSH wrapper is enabled for SSH sessions.", + description_key: "settings.schema.warpify.ssh.enable_legacy_ssh_wrapper.description", }, ] ); diff --git a/app/src/settings/theme.rs b/app/src/settings/theme.rs index 0b57864287..5282d660b0 100644 --- a/app/src/settings/theme.rs +++ b/app/src/settings/theme.rs @@ -22,7 +22,7 @@ define_settings_group!(ThemeSettings, settings: [ private: false, toml_path: "appearance.themes.theme", max_table_depth: 0, - description: "The color theme.", + description_key: "settings.schema.appearance.themes.theme.description", }, use_system_theme: UseSystemTheme { type: bool, @@ -32,7 +32,7 @@ define_settings_group!(ThemeSettings, settings: [ private: false, storage_key: "SystemTheme", toml_path: "appearance.themes.system_theme", - description: "Whether to match the system light/dark theme.", + description_key: "settings.schema.appearance.themes.system_theme.description", }, selected_system_themes: SystemThemes { type: SelectedSystemThemes, @@ -43,7 +43,7 @@ define_settings_group!(ThemeSettings, settings: [ storage_key: "SelectedSystemThemes", toml_path: "appearance.themes.selected_system_themes", max_table_depth: 0, - description: "The themes to use for system light and dark modes.", + description_key: "settings.schema.appearance.themes.selected_system_themes.description", }, ]); diff --git a/app/src/settings_view/about_page.rs b/app/src/settings_view/about_page.rs index 8d4a233726..cff31872c0 100644 --- a/app/src/settings_view/about_page.rs +++ b/app/src/settings_view/about_page.rs @@ -115,7 +115,7 @@ impl SettingsWidget for AboutPageWidget { .with_child(version_row.finish()) .with_child( ui_builder - .span("Copyright 2026 Warp") + .span(i18n::t("settings.about.copyright")) .build() .with_margin_top(16.) .finish(), diff --git a/app/src/settings_view/agent_assisted_environment_modal.rs b/app/src/settings_view/agent_assisted_environment_modal.rs index a051f942b0..a5511b3156 100644 --- a/app/src/settings_view/agent_assisted_environment_modal.rs +++ b/app/src/settings_view/agent_assisted_environment_modal.rs @@ -93,23 +93,28 @@ pub struct AgentAssistedEnvironmentModal { impl AgentAssistedEnvironmentModal { pub fn new(ctx: &mut ViewContext) -> Self { let add_repo_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Add repo", SecondaryTheme) - .with_size(ButtonSize::Small) - .on_click(|ctx| { - ctx.dispatch_typed_action( - AgentAssistedEnvironmentModalAction::OpenDirectoryPicker, - ); - }) + ActionButton::new( + i18n::t("settings.environments.button.add_repo"), + SecondaryTheme, + ) + .with_size(ButtonSize::Small) + .on_click(|ctx| { + ctx.dispatch_typed_action(AgentAssistedEnvironmentModalAction::OpenDirectoryPicker); + }) }); let cancel_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Cancel", SecondaryTheme).on_click(|ctx| { + ActionButton::new(i18n::t("common.cancel"), SecondaryTheme).on_click(|ctx| { ctx.dispatch_typed_action(AgentAssistedEnvironmentModalAction::Cancel); }) }); let create_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Create environment", PrimaryTheme).on_click(|ctx| { + ActionButton::new( + i18n::t("settings.environments.button.create_environment"), + PrimaryTheme, + ) + .on_click(|ctx| { ctx.dispatch_typed_action(AgentAssistedEnvironmentModalAction::Confirm); }) }); @@ -325,12 +330,13 @@ impl AgentAssistedEnvironmentModal { .with_cross_axis_alignment(CrossAxisAlignment::Stretch) .with_spacing(8.); - col.add_child(self.render_section_title("Selected repos", appearance)); + let selected_repos_title = i18n::t("settings.environments.selected_repos"); + col.add_child(self.render_section_title(&selected_repos_title, appearance)); if self.selected_repo_paths.is_empty() { col.add_child( Text::new( - "No repos selected yet", + i18n::t("settings.environments.no_repos_selected"), appearance.ui_font_family(), appearance.ui_font_size() * 0.95, ) @@ -400,13 +406,14 @@ impl AgentAssistedEnvironmentModal { .with_cross_axis_alignment(CrossAxisAlignment::Stretch) .with_spacing(8.); + let available_repos_title = i18n::t("settings.environments.available_indexed_repos"); let header = Flex::row() .with_main_axis_size(MainAxisSize::Max) .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Expanded::new( 1., - self.render_section_title("Available indexed repos", appearance), + self.render_section_title(&available_repos_title, appearance), ) .finish(), ) @@ -426,12 +433,12 @@ impl AgentAssistedEnvironmentModal { if self.available_repos.is_empty() { let text = if cfg!(all(feature = "local_fs", not(target_family = "wasm"))) { if self.available_repos_loading { - "Loading locally indexed repos…" + i18n::t("settings.environments.loading_indexed_repos") } else { - "No locally indexed repos found yet. Index a repo, then try again." + i18n::t("settings.environments.no_indexed_repos_found") } } else { - "Local repo selection is unavailable in this build." + i18n::t("settings.environments.local_repo_selection_unavailable") }; col.add_child( @@ -501,7 +508,7 @@ impl AgentAssistedEnvironmentModal { if !has_any_available { col.add_child( Text::new( - "All locally indexed repos are already selected.", + i18n::t("settings.environments.all_indexed_repos_selected"), appearance.ui_font_family(), appearance.ui_font_size() * 0.95, ) @@ -543,9 +550,11 @@ impl AgentAssistedEnvironmentModal { let window_id = ctx.window_id(); let path = home_relative_path(selected_path); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = - DismissibleToast::error(format!("Selected folder is not a Git repository: {path}")) - .with_object_id("agent_assisted_env_add_repo_not_git_repo".to_string()); + let toast = DismissibleToast::error( + i18n::t("settings.environments.selected_folder_not_git_repository") + .replace("{path}", &path), + ) + .with_object_id("agent_assisted_env_add_repo_not_git_repo".to_string()); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); } @@ -588,7 +597,9 @@ impl AgentAssistedEnvironmentModal { move |paths_result, ctx| { let result = paths_result.and_then(|paths| { paths.into_iter().next().map(PathBuf::from).ok_or_else(|| { - FilePickerError::DialogFailed("No directory selected".to_string()) + FilePickerError::DialogFailed(i18n::t( + "settings.environments.no_directory_selected", + )) }) }); @@ -606,11 +617,10 @@ impl AgentAssistedEnvironmentModal { fn render_dialog(&self, appearance: &Appearance, app: &AppContext) -> Box { let description = if FeatureFlag::FullSourceCodeEmbedding.is_enabled() { - "Select locally indexed repos to provide context for the environment creation agent." + i18n::t("settings.environments.select_local_repos_description") } else { - "Select repos to provide context for the environment creation agent." - } - .to_string(); + i18n::t("settings.environments.select_repos_description") + }; let close_button = icon_button( appearance, @@ -632,7 +642,7 @@ impl AgentAssistedEnvironmentModal { .finish(); let dialog = Dialog::new( - "Select repos for your environment".to_string(), + i18n::t("settings.environments.select_repos_dialog_title"), Some(description), dialog_styles(appearance), ) diff --git a/app/src/settings_view/agent_assisted_environment_modal_tests.rs b/app/src/settings_view/agent_assisted_environment_modal_tests.rs index 8ca807b1b6..a46866a267 100644 --- a/app/src/settings_view/agent_assisted_environment_modal_tests.rs +++ b/app/src/settings_view/agent_assisted_environment_modal_tests.rs @@ -123,12 +123,12 @@ fn test_modal_show_renders_expected_copy_with_empty_repos_message() { let selected_section = modal.render_selected_section(appearance); let selected_text = selected_section.debug_text_content().unwrap_or_default(); assert!( - selected_text.contains("Selected repos"), + selected_text.contains(&i18n::t("settings.environments.selected_repos")), "Expected selected section title in rendered content: {}", selected_text ); assert!( - selected_text.contains("No repos selected yet"), + selected_text.contains(&i18n::t("settings.environments.no_repos_selected")), "Expected selected empty-state message in rendered content: {}", selected_text ); @@ -136,13 +136,12 @@ fn test_modal_show_renders_expected_copy_with_empty_repos_message() { let available_section = modal.render_available_section(appearance); let available_text = available_section.debug_text_content().unwrap_or_default(); assert!( - available_text.contains("Available indexed repos"), + available_text.contains(&i18n::t("settings.environments.available_indexed_repos")), "Expected available section title in rendered content: {}", available_text ); assert!( - available_text - .contains("No locally indexed repos found yet. Index a repo, then try again."), + available_text.contains(&i18n::t("settings.environments.no_indexed_repos_found")), "Expected available empty-state message in rendered content: {}", available_text ); diff --git a/app/src/settings_view/ai_page.rs b/app/src/settings_view/ai_page.rs index e94556548d..d1a039f82e 100644 --- a/app/src/settings_view/ai_page.rs +++ b/app/src/settings_view/ai_page.rs @@ -155,15 +155,6 @@ const AI_SETTINGS_DROPDOWN_MAX_HEIGHT: f32 = 250.; const CONTEXT_WINDOW_SLIDER_WIDTH: f32 = 220.; const CONTEXT_WINDOW_INPUT_BOX_WIDTH: f32 = 120.; -const NEXT_COMMAND_DESCRIPTION: &str = "Let AI suggest the next command to run based on your command history, outputs, and common workflows."; -const PROMPT_SUGGESTIONS_DESCRIPTION: &str = "Let AI suggest natural language prompts, as inline banners in the input, based on recent commands and their outputs."; -const SUGGESTED_CODE_BANNERS_DESCRIPTION: &str = "Let AI suggest code diffs and queries as inline banners in the blocklist, based on recent commands and their outputs."; -const NATURAL_LANGUAGE_AUTOSUGGESTIONS: &str = - "Let AI suggest natural language autosuggestions, based on recent commands and their outputs."; -const SHARED_BLOCK_TITLE_GENERATION_DESCRIPTION: &str = - "Let AI generate a title for your shared block based on the command and output."; -const GIT_OPERATIONS_AUTOGEN_DESCRIPTION: &str = - "Let AI generate commit messages and pull request titles and descriptions."; const WISPR_FLOW_URL: &str = "https://wisprflow.ai/"; const CUSTOM_INFERENCE_LEARN_MORE_URL: &str = "https://docs.warp.dev/support-and-community/plans-and-billing/bring-your-own-api-key/"; @@ -177,7 +168,7 @@ pub fn init_actions_from_parent_view( ) { ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::new( - "AI", + i18n::t("settings.nav.ai"), builder(SettingsAction::AI(AISettingsPageAction::ToggleGlobalAI)), context, flags::IS_ANY_AI_ENABLED, @@ -188,7 +179,7 @@ pub fn init_actions_from_parent_view( ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::new( - "Active AI", + i18n::t("settings.ai.active_ai.header"), builder(SettingsAction::AI(AISettingsPageAction::ToggleActiveAI)), &(context.clone() & id!(flags::IS_ANY_AI_ENABLED)), flags::IS_ACTIVE_AI_ENABLED, @@ -200,9 +191,9 @@ pub fn init_actions_from_parent_view( ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::new( if FeatureFlag::AgentView.is_enabled() { - "terminal command autodetection in agent input" + i18n::t("settings.ai.action.terminal_command_autodetection_in_agent_input") } else { - "natural language detection" + i18n::t("settings.ai.nld.label") }, builder(SettingsAction::AI( AISettingsPageAction::ToggleAIInputAutoDetection, @@ -216,7 +207,7 @@ pub fn init_actions_from_parent_view( ); ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::new( - "agent prompt autodetection in terminal input", + i18n::t("settings.ai.action.agent_prompt_autodetection_in_terminal_input"), builder(SettingsAction::AI( AISettingsPageAction::ToggleNLDInTerminal, )), @@ -229,7 +220,7 @@ pub fn init_actions_from_parent_view( ); ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::new( - "Next Command", + i18n::t("settings.ai.next_command.label"), builder(SettingsAction::AI( AISettingsPageAction::ToggleIntelligentAutosuggestions, )), @@ -241,7 +232,7 @@ pub fn init_actions_from_parent_view( ); ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::new( - "prompt suggestions", + i18n::t("settings.ai.prompt_suggestions.label"), builder(SettingsAction::AI( AISettingsPageAction::TogglePromptSuggestions, )), @@ -253,7 +244,7 @@ pub fn init_actions_from_parent_view( ); ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::new( - "code suggestions", + i18n::t("settings.ai.action.code_suggestions"), builder(SettingsAction::AI( AISettingsPageAction::ToggleCodeSuggestions, )), @@ -267,7 +258,10 @@ pub fn init_actions_from_parent_view( ); ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::custom( - SettingActionPairDescriptions::new("Show agent tips", "Hide agent tips"), + SettingActionPairDescriptions::new( + i18n::t("settings.ai.action.show_agent_tips"), + i18n::t("settings.ai.action.hide_agent_tips"), + ), builder(SettingsAction::AI( AISettingsPageAction::ToggleShowAgentTips, )), @@ -284,8 +278,8 @@ pub fn init_actions_from_parent_view( ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::custom( SettingActionPairDescriptions::new( - "Show Oz changelog in new agent conversation view", - "Hide Oz changelog in new agent conversation view", + i18n::t("settings.ai.action.show_oz_changelog"), + i18n::t("settings.ai.action.hide_oz_changelog"), ), builder(SettingsAction::AI( AISettingsPageAction::ToggleShowOzUpdatesInZeroState, @@ -333,7 +327,7 @@ pub fn init_actions_from_parent_view( } ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::new( - "natural language autosuggestions", + i18n::t("settings.ai.natural_language_autosuggestions.label"), builder(SettingsAction::AI( AISettingsPageAction::ToggleNaturalLanguageAutosuggestions, )), @@ -346,7 +340,7 @@ pub fn init_actions_from_parent_view( ); ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::new( - "shared block title generation", + i18n::t("settings.ai.shared_block_title.label"), builder(SettingsAction::AI( AISettingsPageAction::ToggleSharedTitleGeneration, )), @@ -359,7 +353,7 @@ pub fn init_actions_from_parent_view( ); ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::new( - "commit and pull request generation", + i18n::t("settings.ai.git_operations.label"), builder(SettingsAction::AI( AISettingsPageAction::ToggleGitOperationsAutogen, )), @@ -377,7 +371,7 @@ pub fn init_actions_from_parent_view( ); ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::new( - "voice input", + i18n::t("settings.ai.voice_input.label"), builder(SettingsAction::AI(AISettingsPageAction::ToggleVoiceInput)), &(context.clone() & id!(flags::IS_ANY_AI_ENABLED)), flags::IS_VOICE_INPUT_ENABLED, @@ -389,8 +383,8 @@ pub fn init_actions_from_parent_view( ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::custom( SettingActionPairDescriptions::new( - "Show \"Use Agent\" footer", - "Hide \"Use Agent\" footer", + i18n::t("settings.ai.action.show_use_agent_footer"), + i18n::t("settings.ai.action.hide_use_agent_footer"), ), builder(SettingsAction::AI( AISettingsPageAction::ToggleUseAgentToolbar, @@ -409,7 +403,7 @@ pub fn init_actions_from_parent_view( ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ ToggleSettingActionPair::new( - "include agent-executed commands in history", + i18n::t("settings.ai.action.agent_commands_in_history"), builder(SettingsAction::AI( AISettingsPageAction::ToggleIncludeAgentCommandsInHistory, )), @@ -418,7 +412,7 @@ pub fn init_actions_from_parent_view( ) .with_group(bindings::BindingGroup::WarpAi), ToggleSettingActionPair::new( - "conversation history in tools panel", + i18n::t("settings.ai.action.conversation_history_tools_panel"), builder(SettingsAction::AI( AISettingsPageAction::ToggleShowConversationHistory, )), @@ -427,7 +421,7 @@ pub fn init_actions_from_parent_view( ) .with_group(bindings::BindingGroup::WarpAi), ToggleSettingActionPair::new( - "model picker in prompt", + i18n::t("settings.ai.action.model_picker_in_prompt"), builder(SettingsAction::AI( AISettingsPageAction::ToggleShowBaseModelPickerInPrompt, )), @@ -436,7 +430,7 @@ pub fn init_actions_from_parent_view( ) .with_group(bindings::BindingGroup::WarpAi), ToggleSettingActionPair::new( - "coding agent toolbar", + i18n::t("settings.ai.action.coding_agent_toolbar"), builder(SettingsAction::AI( AISettingsPageAction::ToggleCLIAgentToolbar, )), @@ -450,7 +444,7 @@ pub fn init_actions_from_parent_view( ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ ToggleSettingActionPair::new( - "Rules", + i18n::t("settings.ai.rules.label"), builder(SettingsAction::AI(AISettingsPageAction::ToggleRules)), &(context.clone() & id!(flags::IS_ANY_AI_ENABLED)), flags::AI_RULES_FLAG, @@ -458,7 +452,7 @@ pub fn init_actions_from_parent_view( .with_group(bindings::BindingGroup::WarpAi) .with_enabled(|| FeatureFlag::AIRules.is_enabled()), ToggleSettingActionPair::new( - "Suggested Rules", + i18n::t("settings.ai.suggested_rules.label"), builder(SettingsAction::AI( AISettingsPageAction::ToggleRuleSuggestions, )), @@ -470,7 +464,7 @@ pub fn init_actions_from_parent_view( FeatureFlag::AIRules.is_enabled() && FeatureFlag::SuggestedRules.is_enabled() }), ToggleSettingActionPair::new( - "Warp Drive as agent context", + i18n::t("settings.ai.warp_drive_context.label"), builder(SettingsAction::AI( AISettingsPageAction::ToggleWarpDriveContext, )), @@ -480,7 +474,7 @@ pub fn init_actions_from_parent_view( .with_group(bindings::BindingGroup::WarpAi) .with_enabled(|| FeatureFlag::AIRules.is_enabled()), ToggleSettingActionPair::new( - "Auto-spawn servers from third-party agents", + i18n::t("settings.ai.auto_spawn_servers"), builder(SettingsAction::AI(AISettingsPageAction::ToggleFileBasedMcp)), &(context.clone() & id!(flags::IS_ANY_AI_ENABLED)), flags::FILE_BASED_MCP_FLAG, @@ -497,7 +491,7 @@ pub fn init_actions_from_parent_view( ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ ToggleSettingActionPair::new( - "Warp credit fallback", + i18n::t("settings.ai.warp_credit_fallback.label"), builder(SettingsAction::AI( AISettingsPageAction::ToggleCanUseWarpCreditsForFallback, )), @@ -511,7 +505,7 @@ pub fn init_actions_from_parent_view( && UserWorkspaces::as_ref(app).is_custom_inference_enabled(app)), ), ToggleSettingActionPair::new( - "auto show or hide Rich Input based on agent status", + i18n::t("settings.ai.action.rich_input_auto_toggle"), builder(SettingsAction::AI( AISettingsPageAction::ToggleAutoToggleRichInput, )), @@ -521,7 +515,7 @@ pub fn init_actions_from_parent_view( .with_group(bindings::BindingGroup::WarpAi) .with_enabled(|| FeatureFlag::CLIAgentRichInput.is_enabled()), ToggleSettingActionPair::new( - "auto open Rich Input when a coding agent session starts", + i18n::t("settings.ai.action.rich_input_auto_open_on_agent_start"), builder(SettingsAction::AI( AISettingsPageAction::ToggleAutoOpenRichInputOnCLIAgentStart, )), @@ -531,7 +525,7 @@ pub fn init_actions_from_parent_view( .with_group(bindings::BindingGroup::WarpAi) .with_enabled(|| FeatureFlag::CLIAgentRichInput.is_enabled()), ToggleSettingActionPair::new( - "auto dismiss Rich Input after prompt submission", + i18n::t("settings.ai.action.rich_input_auto_dismiss_after_submit"), builder(SettingsAction::AI( AISettingsPageAction::ToggleAutoDismissRichInputAfterSubmit, )), @@ -546,7 +540,7 @@ pub fn init_actions_from_parent_view( if !FeatureFlag::FullSourceCodeEmbedding.is_enabled() { ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::new( - "codebase index", + i18n::t("settings.code.action.codebase_index"), builder(SettingsAction::AI( AISettingsPageAction::ToggleCodebaseContext, )), @@ -782,7 +776,7 @@ impl AISettingsPageView { let expanded = host_native_absolute_path(s, &None, &None); Path::new(&expanded).is_dir() }); - input.set_placeholder_text("e.g. ~/code-repos/repo", ctx); + input.set_placeholder_text(i18n::t("settings.ai.directory_allowlist.placeholder"), ctx); input }); Self::update_editor_interaction_state( @@ -821,7 +815,7 @@ impl AISettingsPageView { }; let mut editor = EditorView::new(options, ctx); - editor.set_placeholder_text("Commands, comma separated", ctx); + editor.set_placeholder_text(i18n::t("settings.ai.command_denylist.placeholder"), ctx); let current_value = AISettings::as_ref(ctx) .autodetection_command_denylist @@ -843,7 +837,7 @@ impl AISettingsPageView { let command_execution_allowlist_editor = ctx.add_typed_action_view(|ctx| { let mut input = SubmittableTextInput::new(ctx).validate_on_edit(|s| Regex::new(s).is_ok()); - input.set_placeholder_text("e.g. ls .*", ctx); + input.set_placeholder_text(i18n::t("settings.ai.command_allowlist.placeholder"), ctx); input }); Self::update_editor_interaction_state( @@ -875,7 +869,10 @@ impl AISettingsPageView { let command_execution_denylist_editor = ctx.add_typed_action_view(|ctx| { let mut input = SubmittableTextInput::new(ctx).validate_on_edit(|s| Regex::new(s).is_ok()); - input.set_placeholder_text("e.g. rm .*", ctx); + input.set_placeholder_text( + i18n::t("settings.ai.command_denylist.regex_placeholder"), + ctx, + ); input }); Self::update_editor_interaction_state( @@ -907,7 +904,7 @@ impl AISettingsPageView { let cli_agent_footer_command_editor = ctx.add_typed_action_view(|ctx| { let mut input = SubmittableTextInput::new(ctx).validate_on_edit(|s| Regex::new(s).is_ok()); - input.set_placeholder_text("command (supports regex)", ctx); + input.set_placeholder_text(i18n::t("settings.ai.cli_agent.command_placeholder"), ctx); input }); // The coding agent footer command editor is always enabled, @@ -1188,15 +1185,15 @@ impl AISettingsPageView { dropdown.set_items( vec![ DropdownItem::new( - "Agent decides", + i18n::t("settings.ai.permission.agent_decides"), AISettingsPageAction::SetApplyCodeDiffs(ActionPermission::AgentDecides), ), DropdownItem::new( - "Always allow", + i18n::t("settings.ai.permission.always_allow"), AISettingsPageAction::SetApplyCodeDiffs(ActionPermission::AlwaysAllow), ), DropdownItem::new( - "Always ask", + i18n::t("settings.ai.permission.always_ask"), AISettingsPageAction::SetApplyCodeDiffs(ActionPermission::AlwaysAsk), ), ], @@ -1218,15 +1215,15 @@ impl AISettingsPageView { dropdown.set_items( vec![ DropdownItem::new( - "Agent decides", + i18n::t("settings.ai.permission.agent_decides"), AISettingsPageAction::SetReadFiles(ActionPermission::AgentDecides), ), DropdownItem::new( - "Always allow", + i18n::t("settings.ai.permission.always_allow"), AISettingsPageAction::SetReadFiles(ActionPermission::AlwaysAllow), ), DropdownItem::new( - "Always ask", + i18n::t("settings.ai.permission.always_ask"), AISettingsPageAction::SetReadFiles(ActionPermission::AlwaysAsk), ), ], @@ -1248,15 +1245,15 @@ impl AISettingsPageView { dropdown.set_items( vec![ DropdownItem::new( - "Agent decides", + i18n::t("settings.ai.permission.agent_decides"), AISettingsPageAction::SetExecuteCommands(ActionPermission::AgentDecides), ), DropdownItem::new( - "Always allow", + i18n::t("settings.ai.permission.always_allow"), AISettingsPageAction::SetExecuteCommands(ActionPermission::AlwaysAllow), ), DropdownItem::new( - "Always ask", + i18n::t("settings.ai.permission.always_ask"), AISettingsPageAction::SetExecuteCommands(ActionPermission::AlwaysAsk), ), ], @@ -1278,15 +1275,15 @@ impl AISettingsPageView { dropdown.set_items( vec![ DropdownItem::new( - "Always allow", + i18n::t("settings.ai.permission.always_allow"), AISettingsPageAction::SetWriteToPty(WriteToPtyPermission::AlwaysAllow), ), DropdownItem::new( - "Always ask", + i18n::t("settings.ai.permission.always_ask"), AISettingsPageAction::SetWriteToPty(WriteToPtyPermission::AlwaysAsk), ), DropdownItem::new( - "Ask on first write", + i18n::t("settings.ai.permission.ask_on_first_write"), AISettingsPageAction::SetWriteToPty(WriteToPtyPermission::AskOnFirstWrite), ), ], @@ -1308,15 +1305,15 @@ impl AISettingsPageView { dropdown.set_items( vec![ DropdownItem::new( - "Agent decides", + i18n::t("settings.ai.permission.agent_decides"), AISettingsPageAction::SetMCPPermissions(ActionPermission::AgentDecides), ), DropdownItem::new( - "Always allow", + i18n::t("settings.ai.permission.always_allow"), AISettingsPageAction::SetMCPPermissions(ActionPermission::AlwaysAllow), ), DropdownItem::new( - "Always ask", + i18n::t("settings.ai.permission.always_ask"), AISettingsPageAction::SetMCPPermissions(ActionPermission::AlwaysAsk), ), ], @@ -1335,7 +1332,9 @@ impl AISettingsPageView { let mut dropdown = FilterableDropdown::new(ctx); dropdown.set_top_bar_max_width(AI_SETTINGS_DROPDOWN_WIDTH); dropdown.set_menu_width(AI_SETTINGS_DROPDOWN_WIDTH, ctx); - dropdown.set_menu_header_to_static("Select MCP servers"); + dropdown.set_menu_header_to_static(Box::leak( + i18n::t("settings.ai.select_mcp_servers").into_boxed_str(), + )); dropdown }); Self::refresh_mcp_allowlist_dropdown(&mcp_allowlist_dropdown, ctx); @@ -1349,7 +1348,9 @@ impl AISettingsPageView { let mut dropdown = FilterableDropdown::new(ctx); dropdown.set_top_bar_max_width(AI_SETTINGS_DROPDOWN_WIDTH); dropdown.set_menu_width(AI_SETTINGS_DROPDOWN_WIDTH, ctx); - dropdown.set_menu_header_to_static("Select MCP servers"); + dropdown.set_menu_header_to_static(Box::leak( + i18n::t("settings.ai.select_mcp_servers").into_boxed_str(), + )); dropdown }); Self::refresh_mcp_denylist_dropdown(&mcp_denylist_dropdown, ctx); @@ -1397,7 +1398,7 @@ impl AISettingsPageView { let expanded = host_native_absolute_path(s, &None, &None); Path::new(&expanded).is_dir() }); - input.set_placeholder_text("e.g. ~/code-repos/repo", ctx); + input.set_placeholder_text(i18n::t("settings.ai.directory_allowlist.placeholder"), ctx); input }); @@ -1432,7 +1433,10 @@ impl AISettingsPageView { let command_denylist_editor = ctx.add_typed_action_view(|ctx| { let mut input = SubmittableTextInput::new(ctx).validate_on_edit(|s| Regex::new(s).is_ok()); - input.set_placeholder_text("e.g. rm .*", ctx); + input.set_placeholder_text( + i18n::t("settings.ai.command_denylist.regex_placeholder"), + ctx, + ); input }); Self::update_editor_interaction_state( @@ -1470,7 +1474,7 @@ impl AISettingsPageView { let command_allowlist_editor = ctx.add_typed_action_view(|ctx| { let mut input = SubmittableTextInput::new(ctx).validate_on_edit(|s| Regex::new(s).is_ok()); - input.set_placeholder_text("e.g. ls .*", ctx); + input.set_placeholder_text(i18n::t("settings.ai.command_allowlist.placeholder"), ctx); input }); Self::update_editor_interaction_state( @@ -1513,7 +1517,7 @@ impl AISettingsPageView { let profile_views = Self::create_profile_views(ctx); let add_profile_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Add Profile", SecondaryTheme) + ActionButton::new(i18n::t("settings.ai.add_profile"), SecondaryTheme) .with_icon(Icon::Plus) .with_size(ButtonSize::Small) .on_click(|ctx| { @@ -1529,7 +1533,7 @@ impl AISettingsPageView { let custom_inference_controls_enabled = is_any_ai_enabled && UserWorkspaces::as_ref(ctx).is_custom_inference_enabled(ctx); let custom_inference_add_button = ctx.add_typed_action_view(|_| { - ActionButton::new("+ Add custom model", SecondaryTheme) + ActionButton::new(i18n::t("settings.ai.add_custom_model"), SecondaryTheme) .with_size(ButtonSize::Small) .on_click(|ctx| { ctx.dispatch_typed_action(AISettingsPageAction::OpenAddCustomEndpointModal); @@ -1547,7 +1551,7 @@ impl AISettingsPageView { let custom_endpoint_modal_view = ctx.add_typed_action_view(|ctx| { Modal::new( - Some("Add custom endpoint".to_string()), + Some(i18n::t("settings.ai.add_custom_endpoint")), custom_endpoint_modal_body.clone(), ctx, ) @@ -1617,13 +1621,15 @@ impl AISettingsPageView { dropdown.set_top_bar_max_width(AI_SETTINGS_DROPDOWN_WIDTH); dropdown.set_menu_width(AI_SETTINGS_DROPDOWN_WIDTH, ctx); + let new_tab_label = i18n::t("settings.ai.conversation_layout.new_tab"); + let split_pane_label = i18n::t("settings.ai.conversation_layout.split_pane"); let items = vec![ DropdownItem::new( - "New Tab", + new_tab_label.clone(), AISettingsPageAction::SetConversationLayout(OpenConversationPreference::NewTab), ), DropdownItem::new( - "Split Pane", + split_pane_label.clone(), AISettingsPageAction::SetConversationLayout( OpenConversationPreference::SplitPane, ), @@ -1634,9 +1640,11 @@ impl AISettingsPageView { let current = *crate::util::file::external_editor::EditorSettings::as_ref(ctx) .open_conversation_layout_preference; match current { - OpenConversationPreference::NewTab => dropdown.set_selected_by_name("New Tab", ctx), + OpenConversationPreference::NewTab => { + dropdown.set_selected_by_name(&new_tab_label, ctx) + } OpenConversationPreference::SplitPane => { - dropdown.set_selected_by_name("Split Pane", ctx) + dropdown.set_selected_by_name(&split_pane_label, ctx) } }; dropdown @@ -1750,7 +1758,7 @@ impl AISettingsPageView { (0..count) .map(|index| { let button = ctx.add_typed_action_view(move |_| { - ActionButton::new("Edit", SecondaryTheme) + ActionButton::new(i18n::t("settings.ai.edit"), SecondaryTheme) .with_icon(Icon::Pencil) .with_size(ButtonSize::Small) .on_click(move |ctx| { @@ -1783,7 +1791,7 @@ impl AISettingsPageView { self.pending_remove_custom_endpoint_index = None; self.custom_endpoint_modal_state - .set_title(Some("Add custom endpoint".to_string()), ctx); + .set_title(Some(i18n::t("settings.ai.add_custom_endpoint")), ctx); self.custom_endpoint_modal_state.prefill(None, None, ctx); self.custom_endpoint_modal_state.open(ctx); ctx.emit(AISettingsPageEvent::ShowModal); @@ -1810,7 +1818,7 @@ impl AISettingsPageView { self.pending_remove_custom_endpoint_index = None; self.custom_endpoint_modal_state - .set_title(Some("Edit custom endpoint".to_string()), ctx); + .set_title(Some(i18n::t("settings.ai.edit_custom_endpoint")), ctx); self.custom_endpoint_modal_state .prefill(endpoint.as_ref(), Some(index), ctx); self.custom_endpoint_modal_state.open(ctx); @@ -1868,9 +1876,9 @@ impl AISettingsPageView { let window_id = ctx.window_id(); crate::ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = crate::view_components::DismissibleToast::success( - "Endpoint added".to_string(), - ); + let toast = crate::view_components::DismissibleToast::success(i18n::t( + "settings.ai.toast.endpoint_added", + )); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); ctx.notify(); @@ -1900,9 +1908,9 @@ impl AISettingsPageView { let window_id = ctx.window_id(); crate::ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = crate::view_components::DismissibleToast::success( - "Endpoint saved".to_string(), - ); + let toast = crate::view_components::DismissibleToast::success(i18n::t( + "settings.ai.toast.endpoint_saved", + )); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); ctx.notify(); @@ -1986,9 +1994,9 @@ impl AISettingsPageView { let window_id = ctx.window_id(); crate::ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = crate::view_components::DismissibleToast::success( - "Endpoint removed".to_string(), - ); + let toast = crate::view_components::DismissibleToast::success(i18n::t( + "settings.ai.toast.endpoint_removed", + )); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); ctx.notify(); @@ -2366,11 +2374,11 @@ impl AISettingsPageView { menu.set_items( vec![ DropdownItem::new( - "Read only", + i18n::t("settings.ai.autonomy.read_only"), AISettingsPageAction::SetAutonomyReadonlyCommandsSetting, ), DropdownItem::new( - "Supervised", + i18n::t("settings.ai.autonomy.supervised"), AISettingsPageAction::SetAutonomySupervisedSetting, ), ], @@ -2549,10 +2557,14 @@ impl AISettingsPageView { AgentModeCodingPermissionsType::iter() .map(|t| { let display = match t { - AgentModeCodingPermissionsType::AlwaysAskBeforeReading => "Always ask", - AgentModeCodingPermissionsType::AlwaysAllowReading => "Always allow", + AgentModeCodingPermissionsType::AlwaysAskBeforeReading => { + i18n::t("settings.ai.permission.always_ask") + } + AgentModeCodingPermissionsType::AlwaysAllowReading => { + i18n::t("settings.ai.permission.always_allow") + } AgentModeCodingPermissionsType::AllowReadingSpecificFiles => { - "Allow in specific directories" + i18n::t("settings.ai.permission.allow_specific_directories") } }; DropdownItem::new(display, AISettingsPageAction::SetCodingPermission(t)) @@ -2700,8 +2712,9 @@ impl AISettingsPageView { items.push(fields.into_item()); } + let other_label = i18n::t("settings.ai.cli_agent.other"); items.push( - MenuItemFields::new("Other") + MenuItemFields::new(other_label.clone()) .with_on_select_action(DropdownAction::select_action_and_close( AISettingsPageAction::SetCLIAgentForCommand { pattern: pattern_clone.clone(), @@ -2713,18 +2726,20 @@ impl AISettingsPageView { dropdown.set_rich_items(items, ctx); - dropdown.set_menu_header_text_override(|label| { - if label == "Other" { - "Select coding agent".to_string() + let other_label_for_header = other_label.clone(); + let select_agent_header = i18n::t("settings.ai.cli_agent.select_agent"); + dropdown.set_menu_header_text_override(move |label| { + if label == other_label_for_header.as_str() { + select_agent_header.clone() } else { label.to_string() } }); let selected_name = if matches!(current_agent, CLIAgent::Unknown) { - "Other" + other_label } else { - current_agent.display_name() + current_agent.display_name().to_string() }; dropdown.set_selected_by_name(selected_name, ctx); @@ -3760,7 +3775,7 @@ fn render_toolbar_layout_editor( let label = Container::new( appearance .ui_builder() - .span("Toolbar layout".to_string()) + .span(i18n::t("settings.ai.toolbar_layout")) .with_style(UiComponentStyles { font_size: Some(CONTENT_FONT_SIZE), ..Default::default() @@ -3880,16 +3895,20 @@ impl SettingsWidget for GlobalAIWidget { row.add_child( ConstrainedBox::new( Container::new( - Text::new("Your organization disallows AI when the active pane contains content from a remote session", appearance.ui_font_family(), 12.) - .with_color(appearance.theme().ui_warning_color()) - .finish() + Text::new( + i18n::t("settings.ai.remote_session_disallowed"), + appearance.ui_font_family(), + 12., + ) + .with_color(appearance.theme().ui_warning_color()) + .finish(), ) .with_padding_left(8.) .with_padding_right(8.) - .finish() + .finish(), ) .with_max_width(400.) - .finish() + .finish(), ); } @@ -3901,7 +3920,7 @@ impl SettingsWidget for GlobalAIWidget { .with_child( Container::new( Text::new_inline( - "To use AI features, please create an account.", + i18n::t("settings.ai.create_account_prompt"), appearance.ui_font_family(), 14., ) @@ -3932,7 +3951,7 @@ impl SettingsWidget for GlobalAIWidget { }), ..Default::default() }) - .with_text_label("Sign up".to_owned()) + .with_text_label(i18n::t("settings.ai.sign_up")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( @@ -3998,9 +4017,9 @@ impl UsageWidget { } let request_count_label = if workspace_is_delinquent_due_to_payment_issue { - "Restricted due to billing issue".to_string() + i18n::t("settings.ai.usage.restricted_billing") } else if is_unlimited { - "Unlimited".to_string() + i18n::t("settings.ai.usage.unlimited") } else { format!("{used}/{limit}") }; @@ -4147,7 +4166,7 @@ impl SettingsWidget for UsageWidget { .with_child( build_sub_header( appearance, - "Usage", + i18n::t("settings.ai.usage.header"), Some(styles::header_font_color(true, app)), ) .finish(), @@ -4155,7 +4174,10 @@ impl SettingsWidget for UsageWidget { .with_child( appearance .ui_builder() - .paragraph(format!("Resets {formatted_next_refresh_time}")) + .paragraph( + i18n::t("settings.ai.usage.resets") + .replace("{date}", &formatted_next_refresh_time), + ) .with_style(UiComponentStyles { font_color: Some(blended_colors::text_sub( appearance.theme(), @@ -4171,13 +4193,13 @@ impl SettingsWidget for UsageWidget { .with_padding_bottom(HEADER_PADDING) .finish(); - let request_limit_description = format!( - "This is the {} limit of AI credits for your account.", - ai_request_usage_model.refresh_duration_to_string() + let request_limit_description = i18n::t("settings.ai.usage.credits_limit_desc").replace( + "{period}", + &ai_request_usage_model.refresh_duration_to_string(), ); let request_usage_row = self.render_ai_usage_limit_row( - "Credits", + i18n::t("settings.ai.usage.credits"), request_limit_description, ai_request_usage_model.requests_used(), ai_request_usage_model.request_limit(), @@ -4196,28 +4218,43 @@ impl SettingsWidget for UsageWidget { let upgrade_url = UserWorkspaces::upgrade_link_for_team(team.uid); if has_admin_permissions { vec![ - FormattedTextFragment::hyperlink("Upgrade", upgrade_url), - FormattedTextFragment::plain_text(" to get more AI usage."), + FormattedTextFragment::hyperlink( + i18n::t("settings.ai.usage.upgrade"), + upgrade_url, + ), + FormattedTextFragment::plain_text(i18n::t( + "settings.ai.usage.upgrade_suffix", + )), ] } else { // The /upgrade page says to contact their administrator. vec![ - FormattedTextFragment::hyperlink("Compare plans", upgrade_url), - FormattedTextFragment::plain_text(" for more AI usage."), + FormattedTextFragment::hyperlink( + i18n::t("settings.ai.usage.compare_plans"), + upgrade_url, + ), + FormattedTextFragment::plain_text(i18n::t( + "settings.ai.usage.more_usage_suffix", + )), ] } } else { vec![ - FormattedTextFragment::hyperlink("Contact support", "mailto:support@warp.dev"), - FormattedTextFragment::plain_text(" for more AI usage."), + FormattedTextFragment::hyperlink( + i18n::t("settings.ai.usage.contact_support"), + "mailto:support@warp.dev", + ), + FormattedTextFragment::plain_text(i18n::t( + "settings.ai.usage.more_usage_suffix", + )), ] } } else { let user_id = auth_state.user_id().unwrap_or_default(); let upgrade_url = UserWorkspaces::upgrade_link(user_id); vec![ - FormattedTextFragment::hyperlink("Upgrade", upgrade_url), - FormattedTextFragment::plain_text(" to get more AI usage."), + FormattedTextFragment::hyperlink(i18n::t("settings.ai.usage.upgrade"), upgrade_url), + FormattedTextFragment::plain_text(i18n::t("settings.ai.usage.upgrade_suffix")), ] }; @@ -4333,7 +4370,7 @@ impl ActiveAIWidget { Flex::column() .with_child( render_ai_setting_toggle::( - "Next Command", + i18n::t("settings.ai.next_command.label"), AISettingsPageAction::ToggleIntelligentAutosuggestions, *ai_settings.intelligent_autosuggestions_enabled_internal, is_toggleable, @@ -4343,7 +4380,7 @@ impl ActiveAIWidget { ), ) .with_child(render_ai_setting_description( - NEXT_COMMAND_DESCRIPTION, + i18n::t("settings.ai.next_command.desc"), is_toggleable, app, )) @@ -4360,7 +4397,7 @@ impl ActiveAIWidget { Flex::column() .with_child( render_ai_setting_toggle::( - "Prompt Suggestions", + i18n::t("settings.ai.prompt_suggestions.label"), AISettingsPageAction::TogglePromptSuggestions, *ai_settings.prompt_suggestions_enabled_internal, is_toggleable, @@ -4370,7 +4407,7 @@ impl ActiveAIWidget { ), ) .with_child(render_ai_setting_description( - PROMPT_SUGGESTIONS_DESCRIPTION, + i18n::t("settings.ai.prompt_suggestions.desc"), is_toggleable, app, )) @@ -4387,7 +4424,7 @@ impl ActiveAIWidget { Flex::column() .with_child( render_ai_setting_toggle::( - "Suggested Code Banners", + i18n::t("settings.ai.suggested_code_banners.label"), AISettingsPageAction::ToggleCodeSuggestions, *ai_settings.code_suggestions_enabled_internal, is_toggleable, @@ -4397,7 +4434,7 @@ impl ActiveAIWidget { ), ) .with_child(render_ai_setting_description( - SUGGESTED_CODE_BANNERS_DESCRIPTION, + i18n::t("settings.ai.suggested_code_banners.desc"), is_toggleable, app, )) @@ -4415,7 +4452,7 @@ impl ActiveAIWidget { .with_child(render_ai_setting_toggle::< NaturalLanguageAutosuggestionsEnabled, >( - "Natural Language Autosuggestions", + i18n::t("settings.ai.natural_language_autosuggestions.label"), AISettingsPageAction::ToggleNaturalLanguageAutosuggestions, *ai_settings.natural_language_autosuggestions_enabled_internal, is_toggleable, @@ -4424,7 +4461,7 @@ impl ActiveAIWidget { app, )) .with_child(render_ai_setting_description( - NATURAL_LANGUAGE_AUTOSUGGESTIONS, + i18n::t("settings.ai.natural_language_autosuggestions.desc"), is_toggleable, app, )) @@ -4441,7 +4478,7 @@ impl ActiveAIWidget { Flex::column() .with_child( render_ai_setting_toggle::( - "Shared Block Title Generation", + i18n::t("settings.ai.shared_block_title.label"), AISettingsPageAction::ToggleSharedTitleGeneration, *ai_settings.shared_block_title_generation_enabled_internal, is_toggleable, @@ -4451,7 +4488,7 @@ impl ActiveAIWidget { ), ) .with_child(render_ai_setting_description( - SHARED_BLOCK_TITLE_GENERATION_DESCRIPTION, + i18n::t("settings.ai.shared_block_title.desc"), is_toggleable, app, )) @@ -4467,7 +4504,7 @@ impl ActiveAIWidget { let is_toggleable = ai_settings.is_active_ai_enabled(app); Flex::column() .with_child(render_ai_setting_toggle::( - "Commit & Pull Request Generation", + i18n::t("settings.ai.git_operations.label"), AISettingsPageAction::ToggleGitOperationsAutogen, *ai_settings.git_operations_autogen_enabled_internal, is_toggleable, @@ -4476,7 +4513,7 @@ impl ActiveAIWidget { app, )) .with_child(render_ai_setting_description( - GIT_OPERATIONS_AUTOGEN_DESCRIPTION, + i18n::t("settings.ai.git_operations.desc"), is_toggleable, app, )) @@ -4518,7 +4555,7 @@ impl SettingsWidget for ActiveAIWidget { .with_child( build_sub_header( appearance, - "Active AI", + i18n::t("settings.ai.active_ai.header"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .finish(), @@ -4609,14 +4646,14 @@ impl SettingsWidget for AgentsWidget { agents_header.add_child( build_sub_header( appearance, - "Agents", + i18n::t("settings.ai.agents.header"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) .finish(), ); agents_header.add_child(render_ai_setting_description( - "Set the boundaries for how your Agent operates. Choose what it can access, how much autonomy it has, and when it must ask for your approval. You can also fine-tune behavior around natural language input, codebase awareness, and more.", + i18n::t("settings.ai.agents.desc"), ai_settings.is_any_ai_enabled(app), app, )); @@ -4655,21 +4692,19 @@ impl AgentsWidget { .with_child( build_sub_header( appearance, - "Profiles", + i18n::t("settings.ai.profiles.header"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .finish(), ) .with_child( - Container::new( - render_ai_setting_description( - "Profiles let you define how your Agent operates — from the actions it can take and when it needs approval, to the models it uses for tasks like coding and planning. You can also scope them to individual projects.", - is_any_ai_enabled, - app, - ) - ) + Container::new(render_ai_setting_description( + i18n::t("settings.ai.profiles.desc"), + is_any_ai_enabled, + app, + )) .with_margin_top(12.) - .finish() + .finish(), ) .finish(); @@ -4713,7 +4748,7 @@ impl AgentsWidget { let is_any_ai_enabled = ai_settings.is_any_ai_enabled(app); let model_subheader = Container::new(render_custom_size_header( appearance, - "Models", + i18n::t("settings.ai.models.header"), 14.0, Some(styles::header_font_color(is_any_ai_enabled, app)), )) @@ -4761,7 +4796,7 @@ impl AgentsWidget { let max = cw.max; let label = Container::new(render_body_item_label::( - "Context window (tokens)".to_string(), + i18n::t("settings.ai.context_window.label"), None, None, LocalOnlyIconState::Hidden, @@ -4870,7 +4905,7 @@ impl AgentsWidget { let is_any_ai_enabled = ai_settings.is_any_ai_enabled(app); let permissions_subheader = Container::new(render_custom_size_header( appearance, - "Permissions", + i18n::t("settings.ai.permissions.header"), 14.0, Some(styles::header_font_color(is_any_ai_enabled, app)), )) @@ -4879,8 +4914,9 @@ impl AgentsWidget { let code_diff_setting = BlocklistAIPermissions::as_ref(app).get_apply_code_diffs_setting(app, None); + let apply_code_diffs_label = i18n::t("settings.ai.permissions.apply_code_diffs"); let code_diffs = self.render_execution_profile_dropdown( - "Apply code diffs", + &apply_code_diffs_label, Icon::Code2, code_diff_setting.description(), &view.apply_code_diffs_dropdown_menu, @@ -4892,8 +4928,9 @@ impl AgentsWidget { let read_files_setting = BlocklistAIPermissions::as_ref(app).get_read_files_setting(app, None); let mut read_files_flex = Flex::column().with_main_axis_size(MainAxisSize::Min); + let read_files_label = i18n::t("settings.ai.permissions.read_files"); read_files_flex.add_child(self.render_execution_profile_dropdown( - "Read files", + &read_files_label, Icon::Notebook, read_files_setting.description(), &view.read_files_dropdown_menu, @@ -4922,8 +4959,9 @@ impl AgentsWidget { let execute_commands_setting = BlocklistAIPermissions::as_ref(app).get_execute_commands_setting(app, None); let mut execute_commands_flex = Flex::column().with_main_axis_size(MainAxisSize::Min); + let execute_commands_label = i18n::t("settings.ai.permissions.execute_commands"); execute_commands_flex.add_child(self.render_execution_profile_dropdown( - "Execute commands", + &execute_commands_label, Icon::Terminal, execute_commands_setting.description(), &view.execute_commands_dropdown_menu, @@ -4972,7 +5010,7 @@ impl AgentsWidget { { widget_children.push( Container::new(render_settings_info_banner( - "Some of your permissions are managed by your workspace.", + &i18n::t("settings.ai.permissions.workspace_managed"), None, appearance, )) @@ -4985,8 +5023,10 @@ impl AgentsWidget { let write_to_pty_setting = BlocklistAIPermissions::as_ref(app).get_write_to_pty_setting(app, None); + let interact_running_commands_label = + i18n::t("settings.ai.permissions.interact_running_commands"); let write_to_pty = self.render_execution_profile_dropdown( - "Interact with running commands", + &interact_running_commands_label, Icon::Workflow, write_to_pty_setting.description(), &view.write_to_pty_autonomy_dropdown_menu, @@ -5122,8 +5162,8 @@ impl AgentsWidget { appearance, ); render_ai_list( - "Command denylist", - "Regular expressions to match commands that the Warp Agent should always ask permission to execute.", + &i18n::t("settings.ai.command_denylist.label"), + &i18n::t("settings.ai.command_denylist.desc"), list, view, ai_settings, @@ -5157,8 +5197,8 @@ impl AgentsWidget { ); render_ai_list( - "Command allowlist", - "Regular expressions to match commands that can be automatically executed by the Warp Agent.", + &i18n::t("settings.ai.command_allowlist.label"), + &i18n::t("settings.ai.command_allowlist.desc"), list, view, ai_settings, @@ -5195,8 +5235,8 @@ impl AgentsWidget { ); render_ai_list( - "Directory allowlist", - "Give the agent file access to certain directories.", + &i18n::t("settings.ai.directory_allowlist.label"), + &i18n::t("settings.ai.directory_allowlist.desc"), list, view, ai_settings, @@ -5238,7 +5278,7 @@ impl AgentsWidget { .finish(), appearance .ui_builder() - .span("Show model picker in prompt".to_string()) + .span(i18n::t("settings.ai.show_model_picker")) .with_style(UiComponentStyles { font_color: Some( theme.sub_text_color(theme.surface_2()).into_solid(), @@ -5256,12 +5296,12 @@ impl AgentsWidget { .finish() }; + let base_model_label = i18n::t("settings.ai.base_model.label"); + let base_model_desc = i18n::t("settings.ai.base_model.desc"); render_dropdown_item( appearance, - "Base model", - Some( - "This model serves as the primary engine behind the Warp Agent. It powers most interactions and invokes other models for tasks like planning or code generation when necessary. Warp may automatically switch to alternate models based on model availability or for auxiliary tasks such as conversation summarization.", - ), + &base_model_label, + Some(base_model_desc.as_str()), Some(show_in_prompt_checkbox), LocalOnlyIconState::Hidden, (!ai_settings.is_any_ai_enabled(app)) @@ -5280,7 +5320,7 @@ impl AgentsWidget { ) -> Box { let code_settings = CodeSettings::as_ref(app); let toggle = render_ai_setting_toggle::( - "Codebase Context", + i18n::t("settings.ai.codebase_context.label"), AISettingsPageAction::ToggleCodebaseContext, *code_settings.codebase_context_enabled, ai_settings.is_any_ai_enabled(app), @@ -5290,11 +5330,9 @@ impl AgentsWidget { ); let codebase_context_description = vec![ - FormattedTextFragment::plain_text( - "Allow the Warp Agent to generate an outline of your codebase that can be used for context. No code is ever stored on our servers. ", - ), + FormattedTextFragment::plain_text(i18n::t("settings.ai.codebase_context.desc")), FormattedTextFragment::hyperlink( - "Learn more", + i18n::t("settings.ai.learn_more"), "https://docs.warp.dev/agent-platform/capabilities/codebase-context", ), ]; @@ -5347,7 +5385,7 @@ impl AgentsWidget { app: &AppContext, ) -> Box { let header = Container::new(render_body_item_label_with_icon::( - "Call MCP servers".into(), + i18n::t("settings.ai.mcp.call_servers"), Icon::Dataflow, Some(styles::header_font_color( ai_settings.is_any_ai_enabled(app), @@ -5363,16 +5401,14 @@ impl AgentsWidget { let subtext = { let subtext_fragments = vec![ - FormattedTextFragment::plain_text( - "You haven't added any MCP servers yet. Once you do, you'll be able to control how much autonomy the Warp Agent has when interacting with them. ", - ), + FormattedTextFragment::plain_text(i18n::t("settings.ai.mcp.zero_state_intro")), FormattedTextFragment::hyperlink_action( - "Add a server", + i18n::t("settings.ai.mcp.add_server"), AISettingsPageAction::OpenMCPServerCollection, ), - FormattedTextFragment::plain_text(" or "), + FormattedTextFragment::plain_text(i18n::t("settings.ai.mcp.or")), FormattedTextFragment::hyperlink( - "learn more about MCPs.", + i18n::t("settings.ai.mcp.learn_more_link"), "https://docs.warp.dev/agent-platform/capabilities/mcp", ), ]; @@ -5429,8 +5465,9 @@ impl AgentsWidget { let current_mcp_setting = BlocklistAIPermissions::as_ref(app).get_mcp_permissions_setting(app, None); + let call_mcp_servers_label = i18n::t("settings.ai.mcp.call_servers"); let permission_setting = self.render_execution_profile_dropdown( - "Call MCP servers", + &call_mcp_servers_label, Icon::Dataflow, current_mcp_setting.description(), &view.mcp_permissions_dropdown_menu, @@ -5443,9 +5480,11 @@ impl AgentsWidget { if current_mcp_setting == ActionPermission::AlwaysAsk || current_mcp_setting == ActionPermission::AgentDecides { + let mcp_allowlist_label = i18n::t("settings.ai.mcp_allowlist.label"); + let mcp_allowlist_desc = i18n::t("settings.ai.mcp_allowlist.desc"); let allowlist = self.render_mcp_list( - "MCP allowlist", - "Allow the Warp Agent to call these MCP servers.", + &mcp_allowlist_label, + &mcp_allowlist_desc, &view.mcp_allowlist_dropdown, BlocklistAIPermissions::as_ref(app).get_mcp_allowlist(app, None), view.mcp_allowlist_mouse_state_handles.clone(), @@ -5460,9 +5499,11 @@ impl AgentsWidget { if current_mcp_setting == ActionPermission::AlwaysAllow || current_mcp_setting == ActionPermission::AgentDecides { + let mcp_denylist_label = i18n::t("settings.ai.mcp_denylist.label"); + let mcp_denylist_desc = i18n::t("settings.ai.mcp_denylist.desc"); let denylist = self.render_mcp_list( - "MCP denylist", - "The Warp Agent will always ask for permission before calling any MCP servers on this list.", + &mcp_denylist_label, + &mcp_denylist_desc, &view.mcp_denylist_dropdown, BlocklistAIPermissions::as_ref(app).get_mcp_denylist(app, None), view.mcp_denylist_mouse_state_handles.clone(), @@ -5572,7 +5613,7 @@ impl SettingsWidget for AIInputWidget { let input_header = build_sub_header( appearance, - "Input", + i18n::t("settings.ai.input.header"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) @@ -5589,7 +5630,7 @@ impl SettingsWidget for AIInputWidget { ); let show_input_hint_text = render_ai_setting_toggle::( - "Show input hint text", + i18n::t("settings.ai.show_input_hint"), AISettingsPageAction::ToggleShowInputHintText, *InputSettings::as_ref(app).show_hint_text, is_any_ai_enabled, @@ -5607,7 +5648,7 @@ impl SettingsWidget for AIInputWidget { if FeatureFlag::AgentTips.is_enabled() { let agent_tips_toggle = render_ai_setting_toggle::( - "Show agent tips", + i18n::t("settings.ai.show_agent_tips"), AISettingsPageAction::ToggleShowAgentTips, *InputSettings::as_ref(app).show_agent_tips, is_any_ai_enabled, @@ -5619,7 +5660,7 @@ impl SettingsWidget for AIInputWidget { } widget_children.push(render_ai_setting_toggle::( - "Include agent-executed commands in history", + i18n::t("settings.ai.include_agent_commands"), AISettingsPageAction::ToggleIncludeAgentCommandsInHistory, *ai_settings.include_agent_commands_in_history, is_any_ai_enabled, @@ -5668,9 +5709,11 @@ impl AIInputWidget { static AUTODETECTION_DESCRIPTION_FRAGMENTS: LazyLock> = LazyLock::new(|| { vec![ - FormattedTextFragment::plain_text("Encountered an incorrect detection? "), + FormattedTextFragment::plain_text(i18n::t( + "settings.ai.incorrect_detection_prompt", + )), FormattedTextFragment::hyperlink( - "Let us know", + i18n::t("settings.ai.let_us_know"), "https://warpdotdev.typeform.com/to/offrTIpq", ), ] @@ -5678,7 +5721,7 @@ impl AIInputWidget { section.add_children([ render_ai_setting_toggle::( - "Autodetect agent prompts in terminal input", + i18n::t("settings.ai.autodetect_agent_prompts"), AISettingsPageAction::ToggleNLDInTerminal, ai_settings.is_nld_in_terminal_enabled(app), is_toggleable, @@ -5687,7 +5730,7 @@ impl AIInputWidget { app, ), render_ai_setting_toggle::( - "Autodetect terminal commands in agent input", + i18n::t("settings.ai.autodetect_terminal_commands"), AISettingsPageAction::ToggleAIInputAutoDetection, is_nld_enabled, is_toggleable, @@ -5722,14 +5765,12 @@ impl AIInputWidget { Vec, > = LazyLock::new(|| { vec![ - FormattedTextFragment::plain_text( - "Enabling natural language detection will detect when natural language is written in the terminal input, and then automatically switch to Agent Mode for AI queries.", - ), - FormattedTextFragment::plain_text( - " Encountered an incorrect input detection? ", - ), + FormattedTextFragment::plain_text(i18n::t("settings.ai.nld.desc")), + FormattedTextFragment::plain_text(i18n::t( + "settings.ai.incorrect_input_detection_prompt", + )), FormattedTextFragment::hyperlink( - "Let us know", + i18n::t("settings.ai.let_us_know"), "https://warpdotdev.typeform.com/to/offrTIpq", ), ] @@ -5737,7 +5778,7 @@ impl AIInputWidget { section.add_children([ render_ai_setting_toggle::( - "Natural language detection", + i18n::t("settings.ai.nld.label"), AISettingsPageAction::ToggleAIInputAutoDetection, is_nld_enabled, is_toggleable, @@ -5771,13 +5812,13 @@ impl AIInputWidget { section .with_child(render_ai_setting_label::( - "Natural language denylist".to_owned(), + i18n::t("settings.ai.nld_denylist.label"), is_toggleable, &view.local_only_icon_tooltip_states, app, )) .with_child(render_ai_setting_description( - "Commands listed here will never trigger natural language detection.", + i18n::t("settings.ai.nld_denylist.desc"), is_toggleable, app, )) @@ -5826,19 +5867,16 @@ impl SettingsWidget for MCPServersWidget { let header = build_sub_header( appearance, - "MCP Servers", + i18n::t("settings.ai.mcp_servers.header"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) .finish(); let mcp_description = vec![ - FormattedTextFragment::plain_text( - "Add MCP servers to extend the Warp Agent's capabilities. \ - MCP servers expose data sources or tools to agents through a standardized interface, essentially acting like plugins. ", - ), + FormattedTextFragment::plain_text(i18n::t("settings.ai.mcp_servers.desc")), FormattedTextFragment::hyperlink( - "Learn more", + i18n::t("settings.ai.learn_more"), "https://docs.warp.dev/agent-platform/capabilities/mcp", ), ]; @@ -5867,7 +5905,7 @@ impl SettingsWidget for MCPServersWidget { Some( Flex::column() .with_child(render_ai_setting_toggle::( - "Auto-spawn servers from third-party agents", + i18n::t("settings.ai.auto_spawn_servers"), AISettingsPageAction::ToggleFileBasedMcp, *ai_settings.file_based_mcp_enabled, is_any_ai_enabled, @@ -5881,10 +5919,10 @@ impl SettingsWidget for MCPServersWidget { > = LazyLock::new(|| { vec![ FormattedTextFragment::plain_text( - "Automatically detect and spawn MCP servers from globally-scoped third-party AI agent configuration files (e.g. in your home directory). Servers detected inside a repository are never spawned automatically and must be enabled individually from the MCP settings page. ", + i18n::t("settings.ai.file_based_mcp.desc"), ), FormattedTextFragment::hyperlink( - "See supported providers.", + i18n::t("settings.ai.file_based_mcp.providers_link"), "https://docs.warp.dev/agent-platform/capabilities/mcp#file-based-mcp-servers", ), ] @@ -5918,7 +5956,7 @@ impl SettingsWidget for MCPServersWidget { }; let button = render_full_pane_width_ai_button( - "Manage MCP servers", + &i18n::t("settings.ai.manage_mcp_servers"), is_any_ai_enabled, self.manage_mcp_servers_button.clone(), AISettingsPageAction::OpenMCPServerCollection, @@ -5955,7 +5993,7 @@ impl AIFactWidget { app: &warpui::AppContext, ) -> Box { let toggle = render_ai_setting_toggle::( - "Rules", + i18n::t("settings.ai.rules.label"), AISettingsPageAction::ToggleRules, *ai_settings.memory_enabled, ai_settings.is_any_ai_enabled(app), @@ -5965,11 +6003,9 @@ impl AIFactWidget { ); let rules_description = vec![ - FormattedTextFragment::plain_text( - "Rules help the Warp Agent follow your conventions, whether for codebases or specific workflows. ", - ), + FormattedTextFragment::plain_text(i18n::t("settings.ai.rules.desc")), FormattedTextFragment::hyperlink( - "Learn more", + i18n::t("settings.ai.learn_more"), "https://docs.warp.dev/agent-platform/capabilities/rules", ), ]; @@ -6006,7 +6042,7 @@ impl AIFactWidget { app: &warpui::AppContext, ) -> Box { let toggle = render_ai_setting_toggle::( - "Suggested Rules", + i18n::t("settings.ai.suggested_rules.label"), AISettingsPageAction::ToggleRuleSuggestions, *ai_settings.rule_suggestions_enabled_internal, ai_settings.is_any_ai_enabled(app), @@ -6016,7 +6052,7 @@ impl AIFactWidget { ); let description = render_ai_setting_description( - "Let AI suggest rules to save based on your interactions.", + i18n::t("settings.ai.suggested_rules.desc"), ai_settings.is_any_ai_enabled(app), app, ); @@ -6034,7 +6070,7 @@ impl AIFactWidget { app: &warpui::AppContext, ) -> Box { let toggle = render_ai_setting_toggle::( - "Warp Drive as agent context", + i18n::t("settings.ai.warp_drive_context.label"), AISettingsPageAction::ToggleWarpDriveContext, *ai_settings.warp_drive_context_enabled, ai_settings.is_any_ai_enabled(app), @@ -6044,7 +6080,7 @@ impl AIFactWidget { ); let description = render_ai_setting_description( - "The Warp Agent can leverage your Warp Drive Contents to tailor responses to your personal and team developer workflows and environments. This includes any Workflows, Notebooks, and Environment Variables.", + i18n::t("settings.ai.warp_drive_context.desc"), ai_settings.is_any_ai_enabled(app), app, ); @@ -6078,14 +6114,14 @@ impl SettingsWidget for AIFactWidget { let header = build_sub_header( appearance, - "Knowledge", + i18n::t("settings.ai.knowledge.header"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_margin_bottom(HEADER_PADDING) .finish(); let button = render_full_pane_width_ai_button( - "Manage rules", + &i18n::t("settings.ai.manage_rules"), is_any_ai_enabled, self.manage_rules_button.clone(), AISettingsPageAction::OpenAIFactCollection, @@ -6123,7 +6159,7 @@ impl VoiceWidget { let ai_settings = AISettings::as_ref(app); let is_toggleable = ai_settings.is_any_ai_enabled(app); let mut column = Flex::column().with_child(render_ai_setting_toggle::( - "Voice Input", + i18n::t("settings.ai.voice_input.label"), AISettingsPageAction::ToggleVoiceInput, *ai_settings.voice_input_enabled_internal, is_toggleable, @@ -6133,11 +6169,9 @@ impl VoiceWidget { )); let voice_input_description_text_fragments = vec![ - FormattedTextFragment::plain_text( - "Voice input allows you to control Warp by speaking directly to your terminal (powered by ", - ), + FormattedTextFragment::plain_text(i18n::t("settings.ai.voice_input.desc_prefix")), FormattedTextFragment::hyperlink("Wispr Flow", WISPR_FLOW_URL), - FormattedTextFragment::plain_text(")."), + FormattedTextFragment::plain_text(i18n::t("settings.ai.voice_input.desc_suffix")), ]; let voice_input_description = FormattedTextElement::new( @@ -6164,10 +6198,12 @@ impl VoiceWidget { ); if ai_settings.is_voice_input_enabled(app) { + let voice_key_label = i18n::t("settings.ai.voice_input.key_label"); + let voice_key_desc = i18n::t("settings.ai.voice_input.key_desc"); column.add_child(render_dropdown_item( appearance, - "Key for Activating Voice Input", - Some("Press and hold to activate."), + &voice_key_label, + Some(voice_key_desc.as_str()), None, LocalOnlyIconState::for_setting( VoiceInputToggleKey::storage_key(), @@ -6208,7 +6244,7 @@ impl SettingsWidget for VoiceWidget { .with_child( build_sub_header( appearance, - "Voice", + i18n::t("settings.ai.voice.header"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) @@ -6271,7 +6307,7 @@ impl SettingsWidget for OtherAIWidget { .with_child( build_sub_header( appearance, - "Other", + i18n::t("settings.ai.other.header"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) @@ -6281,7 +6317,7 @@ impl SettingsWidget for OtherAIWidget { if FeatureFlag::AgentView.is_enabled() { let mut agent_view_column = Flex::column() .with_child(render_ai_setting_toggle::( - "Show Oz changelog in new conversation view", + i18n::t("settings.ai.show_oz_changelog"), AISettingsPageAction::ToggleShowOzUpdatesInZeroState, *ai_settings.should_show_oz_updates_in_zero_state, is_toggleable, @@ -6289,8 +6325,10 @@ impl SettingsWidget for OtherAIWidget { &view.local_only_icon_tooltip_states, app, )) - .with_child(render_ai_setting_toggle::( - "Show \"Use Agent\" footer", + .with_child(render_ai_setting_toggle::< + ShouldRenderUseAgentToolbarForUserCommands, + >( + i18n::t("settings.ai.use_agent_footer.label"), AISettingsPageAction::ToggleUseAgentToolbar, *ai_settings.should_render_use_agent_footer_for_user_commands, is_toggleable, @@ -6299,7 +6337,7 @@ impl SettingsWidget for OtherAIWidget { app, )) .with_child(render_ai_setting_description( - "Shows hint to use the \"Full Terminal Use\"-enabled agent in long running commands.", + i18n::t("settings.ai.use_agent_footer.desc"), is_toggleable, app, )); @@ -6315,7 +6353,7 @@ impl SettingsWidget for OtherAIWidget { } column.add_child(render_ai_setting_toggle::( - "Show conversation history in tools panel", + i18n::t("settings.ai.show_conversation_history"), AISettingsPageAction::ToggleShowConversationHistory, *ai_settings.show_conversation_history, is_toggleable, @@ -6324,10 +6362,12 @@ impl SettingsWidget for OtherAIWidget { app, )); + let thinking_display_label = i18n::t("settings.ai.thinking_display.label"); + let thinking_display_desc = i18n::t("settings.ai.thinking_display.desc"); column.add_child(render_dropdown_item( appearance, - "Agent thinking display", - Some("Controls how reasoning/thinking traces are displayed."), + &thinking_display_label, + Some(thinking_display_desc.as_str()), None, LocalOnlyIconState::for_setting( ThinkingDisplayMode::storage_key(), @@ -6345,9 +6385,10 @@ impl SettingsWidget for OtherAIWidget { if FeatureFlag::OpenWarpNewSettingsModes.is_enabled() { use crate::util::file::external_editor::settings::OpenConversationLayoutPreference; + let preferred_layout_label = i18n::t("settings.ai.preferred_conversation_layout"); column.add_child(render_dropdown_item( appearance, - "Preferred layout when opening existing agent conversations", + &preferred_layout_label, None, None, LocalOnlyIconState::for_setting( @@ -6398,7 +6439,7 @@ impl SettingsWidget for CLIAgentWidget { // global AI toggle, because these settings control third-party coding // agents (Claude Code, Codex, Gemini CLI) rather than Warp's own AI. let cli_agent_footer_toggle = render_ai_setting_toggle::( - "Show coding agent toolbar", + i18n::t("settings.ai.cli_agent.show_toolbar"), AISettingsPageAction::ToggleCLIAgentToolbar, *ai_settings.should_render_cli_agent_footer, true, @@ -6408,15 +6449,13 @@ impl SettingsWidget for CLIAgentWidget { ); let description_fragments = vec![ - FormattedTextFragment::plain_text( - "Show a toolbar with quick actions when running coding agents like ", - ), + FormattedTextFragment::plain_text(i18n::t("settings.ai.cli_agent.toolbar_desc_prefix")), FormattedTextFragment::inline_code("claude"), - FormattedTextFragment::plain_text(", "), + FormattedTextFragment::plain_text(i18n::t("settings.ai.cli_agent.toolbar_desc_sep1")), FormattedTextFragment::inline_code("codex"), - FormattedTextFragment::plain_text(", or "), + FormattedTextFragment::plain_text(i18n::t("settings.ai.cli_agent.toolbar_desc_sep2")), FormattedTextFragment::inline_code("gemini"), - FormattedTextFragment::plain_text("."), + FormattedTextFragment::plain_text(i18n::t("settings.ai.cli_agent.toolbar_desc_suffix")), ]; let description = FormattedTextElement::new( @@ -6434,7 +6473,7 @@ impl SettingsWidget for CLIAgentWidget { .with_child( build_sub_header( appearance, - "Third party CLI agents", + i18n::t("settings.ai.cli_agent.header"), Some(styles::header_font_color(true, app)), ) .with_padding_bottom(HEADER_PADDING) @@ -6459,15 +6498,15 @@ impl SettingsWidget for CLIAgentWidget { if FeatureFlag::CLIAgentRichInput.is_enabled() { // Setting 1: Auto show/hide rich input based on agent status let auto_show_toggle_label = render_body_item_label::( - "Auto show/hide Rich Input based on agent status".into(), + i18n::t("settings.ai.cli_agent.auto_toggle_rich_input.label"), Some(styles::header_font_color(true, app)), Some(AdditionalInfo { mouse_state: self.auto_toggle_rich_input_info_tooltip.clone(), on_click_action: None, secondary_text: None, - tooltip_override_text: Some( - "Requires the Warp plugin for your coding agent".to_owned(), - ), + tooltip_override_text: Some(i18n::t( + "settings.ai.cli_agent.requires_plugin_tooltip", + )), }), LocalOnlyIconState::for_setting( AutoToggleRichInput::storage_key(), @@ -6493,7 +6532,7 @@ impl SettingsWidget for CLIAgentWidget { column.add_child( render_ai_setting_toggle::( - "Auto open Rich Input when a coding agent session starts", + i18n::t("settings.ai.cli_agent.auto_open_rich_input"), AISettingsPageAction::ToggleAutoOpenRichInputOnCLIAgentStart, *ai_settings.auto_open_rich_input_on_cli_agent_start, true, @@ -6505,7 +6544,7 @@ impl SettingsWidget for CLIAgentWidget { // Setting 2: Auto dismiss rich input after prompt submission column.add_child(render_ai_setting_toggle::( - "Auto dismiss Rich Input after prompt submission", + i18n::t("settings.ai.cli_agent.auto_dismiss_rich_input"), AISettingsPageAction::ToggleAutoDismissRichInputAfterSubmit, *ai_settings.auto_dismiss_rich_input_after_submit, true, @@ -6521,7 +6560,7 @@ impl SettingsWidget for CLIAgentWidget { list_column.add_child( appearance .ui_builder() - .span("Commands that enable the toolbar".to_string()) + .span(i18n::t("settings.ai.cli_agent.commands_enable_toolbar")) .with_style(UiComponentStyles { font_size: Some(CONTENT_FONT_SIZE), ..Default::default() @@ -6612,9 +6651,7 @@ impl SettingsWidget for CLIAgentWidget { }; let command_list_description = appearance .ui_builder() - .paragraph( - "Add regex patterns to show the coding agent toolbar for matching commands.", - ) + .paragraph(i18n::t("settings.ai.cli_agent.commands_regex_desc")) .with_style(UiComponentStyles { font_size: Some(appearance.ui_font_size()), font_color: Some(styles::description_font_color(true, app).into()), @@ -6714,7 +6751,7 @@ impl SettingsWidget for AgentAttributionWidget { .switch(self.toggle.clone()) .check(state.is_enabled) .with_tooltip(TooltipConfig { - text: "This option is enforced by your organization's settings and cannot be customized.".to_string(), + text: i18n::t("settings.ai.org_enforced_tooltip"), styles: ui_builder.default_tool_tip_styles(), }) .disable() @@ -6740,7 +6777,7 @@ impl SettingsWidget for AgentAttributionWidget { let toggle_row = build_toggle_element( render_body_item_label::( - "Enable agent attribution".to_string(), + i18n::t("settings.ai.agent_attribution.label"), Some(styles::header_font_color(!state.is_disabled, app)), None, LocalOnlyIconState::Hidden, @@ -6757,7 +6794,7 @@ impl SettingsWidget for AgentAttributionWidget { .with_child( build_sub_header( appearance, - "Agent Attribution", + i18n::t("settings.ai.agent_attribution.header"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) @@ -6765,7 +6802,7 @@ impl SettingsWidget for AgentAttributionWidget { ) .with_child(toggle_row) .with_child(render_ai_setting_description( - "Oz can add attribution to commit messages and pull requests it creates", + i18n::t("settings.ai.agent_attribution.desc"), !state.is_disabled, app, )) @@ -6817,7 +6854,7 @@ impl SettingsWidget for CloudAgentComputerUseWidget { .switch(self.toggle.clone()) .check(is_checked) .with_tooltip(TooltipConfig { - text: "This option is enforced by your organization's settings and cannot be customized.".to_string(), + text: i18n::t("settings.ai.org_enforced_tooltip"), styles: ui_builder.default_tool_tip_styles(), }) .disable() @@ -6845,7 +6882,7 @@ impl SettingsWidget for CloudAgentComputerUseWidget { let toggle_row = build_toggle_element( render_body_item_label::( - "Computer use in Cloud Agents".to_string(), + i18n::t("settings.ai.computer_use.label"), Some(styles::header_font_color(!is_disabled, app)), None, LocalOnlyIconState::Hidden, @@ -6862,7 +6899,7 @@ impl SettingsWidget for CloudAgentComputerUseWidget { .with_child( build_sub_header( appearance, - "Experimental", + i18n::t("settings.ai.experimental.header"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) @@ -6870,7 +6907,7 @@ impl SettingsWidget for CloudAgentComputerUseWidget { ) .with_child(toggle_row) .with_child(render_ai_setting_description( - "Enable computer use in cloud agent conversations started from the Warp app.", + i18n::t("settings.ai.computer_use.desc"), !is_disabled, app, )) @@ -6916,9 +6953,9 @@ impl SettingsWidget for CloudHandoffWidget { let is_force_disabled = !is_any_ai_enabled || cloud_convos_off; let tooltip_text = if cloud_convos_off { - "Cloud handoff requires cloud conversations to be enabled." + i18n::t("settings.ai.cloud_handoff.requires_cloud_convos_tooltip") } else { - "" + String::new() }; let ui_builder = appearance.ui_builder(); @@ -6945,7 +6982,7 @@ impl SettingsWidget for CloudHandoffWidget { let handoff_row = build_toggle_element( render_body_item_label::( - "Cloud handoff".to_string(), + i18n::t("settings.ai.cloud_handoff.label"), Some(styles::header_font_color(!is_force_disabled, app)), None, LocalOnlyIconState::Hidden, @@ -6962,7 +6999,7 @@ impl SettingsWidget for CloudHandoffWidget { .with_child( build_sub_header( appearance, - "Cloud Handoff", + i18n::t("settings.ai.cloud_handoff.header"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) @@ -6970,7 +7007,7 @@ impl SettingsWidget for CloudHandoffWidget { ) .with_child(handoff_row) .with_child(render_ai_setting_description( - "Hand off local agent conversations to a cloud agent.", + i18n::t("settings.ai.cloud_handoff.desc"), !is_force_disabled, app, )); @@ -6990,7 +7027,7 @@ impl SettingsWidget for CloudHandoffWidget { .finish(); let auto_handoff_on_sleep_row = build_toggle_element( render_body_item_label::( - "Auto-handoff before sleep".to_string(), + i18n::t("settings.ai.cloud_handoff.auto_before_sleep.label"), Some(styles::header_font_color(true, app)), None, LocalOnlyIconState::Hidden, @@ -7003,7 +7040,7 @@ impl SettingsWidget for CloudHandoffWidget { ); column.add_child(auto_handoff_on_sleep_row); column.add_child(render_ai_setting_description( - "When macOS is about to sleep, automatically moves the most recently focused running local Warp Agent conversation to Cloud Mode so it can keep working.", + i18n::t("settings.ai.cloud_handoff.auto_before_sleep.desc"), true, app, )); @@ -7019,7 +7056,7 @@ impl SettingsWidget for CloudHandoffWidget { let ampersand_row = build_toggle_element( render_body_item_label::( - "Use & to trigger handoff".to_string(), + i18n::t("settings.ai.cloud_handoff.ampersand.label"), Some(styles::header_font_color(true, app)), None, LocalOnlyIconState::Hidden, @@ -7033,7 +7070,7 @@ impl SettingsWidget for CloudHandoffWidget { column.add_child(ampersand_row); column.add_child(render_ai_setting_description( - "Type & as the first character to enter cloud handoff compose mode.", + i18n::t("settings.ai.cloud_handoff.ampersand.desc"), true, app, )); @@ -7216,21 +7253,21 @@ impl ApiKeysWidget { let mut column = Flex::column().with_spacing(16.); column.add_child(self.render_api_key_input( appearance, - "OpenAI API key", + Box::leak(i18n::t("settings.ai.api_key.openai").into_boxed_str()), self.openai_api_key_editor.clone(), is_enabled, app, )); column.add_child(self.render_api_key_input( appearance, - "Anthropic API key", + Box::leak(i18n::t("settings.ai.api_key.anthropic").into_boxed_str()), self.anthropic_api_key_editor.clone(), is_enabled, app, )); column.add_child(self.render_api_key_input( appearance, - "Google API key", + Box::leak(i18n::t("settings.ai.api_key.google").into_boxed_str()), self.google_api_key_editor.clone(), is_enabled, app, @@ -7241,10 +7278,11 @@ impl ApiKeysWidget { fn render_custom_inference_description(&self, app: &AppContext) -> Box { let appearance = Appearance::as_ref(app); let text_fragments = vec![ - FormattedTextFragment::plain_text( - "Use your own API keys from model providers for Warp Agent. You can also add custom endpoints to use third-party models. Custom endpoints must support the OpenAI-compatible Chat Completions API. API keys are stored only on your device, never on Warp's servers. They're used to make requests to your chosen model provider. Using auto models or models from providers you have not provided API keys for will consume Warp credits. ", + FormattedTextFragment::plain_text(i18n::t("settings.ai.custom_inference.desc")), + FormattedTextFragment::hyperlink( + i18n::t("settings.ai.learn_more"), + CUSTOM_INFERENCE_LEARN_MORE_URL, ), - FormattedTextFragment::hyperlink("Learn more", CUSTOM_INFERENCE_LEARN_MORE_URL), ]; let description = FormattedTextElement::new( FormattedText::new([FormattedTextLine::Line(text_fragments)]), @@ -7279,13 +7317,12 @@ impl ApiKeysWidget { .finish(); let tooltip_text = FormattedText::new([FormattedTextLine::Line(vec![ - FormattedTextFragment::plain_text( - "By using BYOK or custom endpoints, you agree to use them only as permitted by ", - ), - FormattedTextFragment::hyperlink("Warp's Terms of Service", CUSTOM_INFERENCE_TERMS_URL), - FormattedTextFragment::plain_text( - ". BYOK and custom endpoints are intended for individual use and small teams. Companies or organizations with more than 10 employees should use Warp Business or Enterprise.", + FormattedTextFragment::plain_text(i18n::t("settings.ai.custom_inference.terms_prefix")), + FormattedTextFragment::hyperlink( + i18n::t("settings.ai.custom_inference.terms_link"), + CUSTOM_INFERENCE_TERMS_URL, ), + FormattedTextFragment::plain_text(i18n::t("settings.ai.custom_inference.terms_suffix")), ])]); let tooltip_background = appearance.theme().tooltip_background(); @@ -7419,7 +7456,7 @@ impl ApiKeysWidget { let ai_settings = AISettings::as_ref(app); let toggle = render_ai_setting_toggle::( - "Warp credit fallback", + i18n::t("settings.ai.warp_credit_fallback.label"), AISettingsPageAction::ToggleCanUseWarpCreditsForFallback, *ai_settings.can_use_warp_credits_for_fallback, ai_settings.is_any_ai_enabled(app), @@ -7429,7 +7466,7 @@ impl ApiKeysWidget { ); let description = render_ai_setting_description( - "When enabled, agent requests may be routed to one of Warp's provided models in the event of an error. Warp will prioritize using your API keys over your Warp credits.", + i18n::t("settings.ai.warp_credit_fallback.desc"), ai_settings.is_any_ai_enabled(app), app, ); @@ -7473,7 +7510,7 @@ impl SettingsWidget for ApiKeysWidget { .with_child( build_sub_header( appearance, - "Custom inference", + i18n::t("settings.ai.custom_inference.header"), Some(styles::header_font_color( custom_inference_controls_enabled, app, @@ -7506,7 +7543,7 @@ impl SettingsWidget for ApiKeysWidget { column.add_child( build_sub_header( appearance, - "API Keys", + i18n::t("settings.ai.api_keys.header"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) @@ -7524,7 +7561,7 @@ impl SettingsWidget for ApiKeysWidget { column.add_child( Container::new( Text::new_inline( - "Custom endpoints", + i18n::t("settings.ai.custom_endpoints.label"), appearance.ui_font_family(), CONTENT_FONT_SIZE, ) @@ -7565,10 +7602,13 @@ impl SettingsWidget for ApiKeysWidget { { if team.billing_metadata.customer_type == CustomerType::Enterprise { vec![ - FormattedTextFragment::hyperlink("Contact sales", "mailto:sales@warp.dev"), - FormattedTextFragment::plain_text( - " to enable bringing your own API keys on your Enterprise plan.", + FormattedTextFragment::hyperlink( + i18n::t("settings.ai.byok.contact_sales"), + "mailto:sales@warp.dev", ), + FormattedTextFragment::plain_text(i18n::t( + "settings.ai.byok.contact_sales_suffix", + )), ] } else { let current_user_email = auth_state.user_email().unwrap_or_default(); @@ -7577,15 +7617,17 @@ impl SettingsWidget for ApiKeysWidget { if has_admin_permissions { vec![ FormattedTextFragment::hyperlink( - "Upgrade to the Build plan", + i18n::t("settings.ai.byok.upgrade_build"), upgrade_url, ), - FormattedTextFragment::plain_text(" to use your own API keys."), + FormattedTextFragment::plain_text(i18n::t( + "settings.ai.byok.upgrade_suffix", + )), ] } else { - vec![FormattedTextFragment::plain_text( - "Ask your team's admin to upgrade to the Build plan to use your own API keys.", - )] + vec![FormattedTextFragment::plain_text(i18n::t( + "settings.ai.byok.ask_admin", + ))] } } } else if FeatureFlag::SoloUserByok.is_enabled() @@ -7593,17 +7635,20 @@ impl SettingsWidget for ApiKeysWidget { { vec![ FormattedTextFragment::hyperlink_action( - "Create an account", + i18n::t("settings.ai.byok.create_account"), AISettingsPageAction::SignupAnonymousUser, ), - FormattedTextFragment::plain_text(" to use your own API keys."), + FormattedTextFragment::plain_text(i18n::t("settings.ai.byok.upgrade_suffix")), ] } else { let user_id = auth_state.user_id().unwrap_or_default(); let upgrade_url = UserWorkspaces::upgrade_link(user_id); vec![ - FormattedTextFragment::hyperlink("Upgrade to the Build plan", upgrade_url), - FormattedTextFragment::plain_text(" to use your own API keys."), + FormattedTextFragment::hyperlink( + i18n::t("settings.ai.byok.upgrade_build"), + upgrade_url, + ), + FormattedTextFragment::plain_text(i18n::t("settings.ai.byok.upgrade_suffix")), ] }; @@ -7751,7 +7796,7 @@ impl AwsBedrockWidget { }); let refresh_credentials_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Refresh", SecondaryTheme) + ActionButton::new(i18n::t("settings.ai.aws_bedrock.refresh"), SecondaryTheme) .with_icon(Icon::RefreshCw04) .with_size(ButtonSize::Small) .on_click(|ctx| { @@ -7854,16 +7899,15 @@ impl AwsBedrockWidget { let are_credentials_enabled = user_workspaces.is_aws_bedrock_credentials_enabled(app); let is_usage_enabled = is_section_enabled && are_credentials_enabled; let toggle_description = if is_admin_enforced { - "Warp loads and sends local AWS CLI credentials for Bedrock-supported models. This setting is managed by your organization.".to_string() + i18n::t("settings.ai.aws_bedrock.desc_admin_enforced") } else { - "Warp loads and sends local AWS CLI credentials for Bedrock-supported models." - .to_string() + i18n::t("settings.ai.aws_bedrock.desc") }; let mut column = Flex::column().with_spacing(16.).with_child( Flex::column() .with_child(render_ai_setting_toggle::( - "Use AWS Bedrock credentials", + i18n::t("settings.ai.aws_bedrock.label"), AISettingsPageAction::ToggleAwsBedrockCredentialsEnabled, are_credentials_enabled, is_toggleable, @@ -7995,14 +8039,14 @@ impl AwsBedrockWidget { ); column.add_child(render_input( appearance, - "Login Command", + Box::leak(i18n::t("settings.ai.aws_bedrock.login_command").into_boxed_str()), self.aws_auth_refresh_command_editor.clone(), is_usage_enabled, app, )); column.add_child(render_input( appearance, - "AWS Profile", + Box::leak(i18n::t("settings.ai.aws_bedrock.profile").into_boxed_str()), self.aws_auth_refresh_profile_editor.clone(), is_usage_enabled, app, @@ -8011,7 +8055,7 @@ impl AwsBedrockWidget { let auto_login_enabled = *AISettings::as_ref(app).aws_bedrock_auto_login.value(); let toggle = render_ai_setting_toggle::( - "Automatically run login command", + i18n::t("settings.ai.aws_bedrock.auto_login.label"), AISettingsPageAction::ToggleAwsBedrockAutoLogin, auto_login_enabled, is_usage_enabled, @@ -8020,7 +8064,7 @@ impl AwsBedrockWidget { app, ); let description = render_ai_setting_description( - "When enabled, the login command will run automatically when AWS Bedrock credentials expire.", + i18n::t("settings.ai.aws_bedrock.auto_login.desc"), is_usage_enabled, app, ); diff --git a/app/src/settings_view/appearance_page.rs b/app/src/settings_view/appearance_page.rs index feefba1e9b..33a4e79425 100644 --- a/app/src/settings_view/appearance_page.rs +++ b/app/src/settings_view/appearance_page.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::cell::RefCell; use std::collections::HashMap; use std::path::PathBuf; @@ -56,6 +55,7 @@ use crate::gpu_state::{GPUState, GPUStateEvent}; use crate::prompt::editor_modal::OpenSource as PromptEditorOpenSource; use crate::server::telemetry::{InputUXChangeOrigin, TelemetryEvent}; use crate::settings::app_icon::{AppIcon, AppIconSettings}; +use crate::settings::language::{Language, LanguageSettings}; use crate::settings::{ active_theme_kind, respect_system_theme, AIFontName, AppEditorSettings, CursorBlink, CursorBlinkEnabled, CursorDisplayType, EnforceMinimumContrast, FocusPaneOnHover, FontSettings, @@ -116,11 +116,13 @@ const MIN_NEW_WINDOW_ROWS_OR_COLS: u16 = 5; const MAX_NEW_WINDOW_ROWS_OR_COLS: u16 = 2000; fn default_font_label(is_ai_font: bool) -> String { - if is_ai_font { - format!("{} (default)", AIFontName::default_value()) + let font = if is_ai_font { + AIFontName::default_value() } else { - format!("{} (default)", MonospaceFontName::default_value()) - } + MonospaceFontName::default_value() + }; + + i18n::t("settings.appearance.text.default_font_label").replace("{font}", &font) } pub fn init_actions_from_parent_view( @@ -131,7 +133,7 @@ pub fn init_actions_from_parent_view( // Add all the toggle settings from the Appearance Page that you want to show up on the Command Palette here. let mut toggle_binding_pairs = vec![ ToggleSettingActionPair::new( - "compact mode", + i18n::t("settings.appearance.blocks.compact_mode"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleCompactMode, )), @@ -139,7 +141,7 @@ pub fn init_actions_from_parent_view( flags::COMPACT_MODE_CONTEXT_FLAG, ), ToggleSettingActionPair::new( - "themes: sync with OS", + i18n::t("settings.appearance.action.theme_sync_with_os"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleRespectSystemTheme, )), @@ -150,7 +152,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "cursor blink", + i18n::t("settings.appearance.action.cursor_blink"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleCursorBlink, )), @@ -166,7 +168,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "jump to bottom of block button", + i18n::t("settings.appearance.action.jump_to_bottom_button"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleJumpToBottomOfBlockButton, )), @@ -182,7 +184,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "block dividers", + i18n::t("settings.appearance.action.block_dividers"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleShowBlockDividers, )), @@ -197,7 +199,7 @@ pub fn init_actions_from_parent_view( ); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "dim inactive panes", + i18n::t("settings.appearance.panes.dim_inactive"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleDimInactivePanes, )), @@ -206,7 +208,7 @@ pub fn init_actions_from_parent_view( )); app.register_fixed_bindings(vec![FixedBinding::empty( - "Start Input at the Top".to_string(), + i18n::t("settings.appearance.action.start_input_at_top"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::SetInputMode { new_mode: InputMode::Waterfall, @@ -218,7 +220,7 @@ pub fn init_actions_from_parent_view( .with_group(bindings::BindingGroup::Settings.as_str())]); app.register_fixed_bindings(vec![FixedBinding::empty( - "Pin Input to the Top".to_string(), + i18n::t("settings.appearance.action.pin_input_to_top"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::SetInputMode { new_mode: InputMode::PinnedToTop, @@ -230,7 +232,7 @@ pub fn init_actions_from_parent_view( .with_group(bindings::BindingGroup::Settings.as_str())]); app.register_fixed_bindings(vec![FixedBinding::empty( - "Pin Input to the Bottom".to_string(), + i18n::t("settings.appearance.action.pin_input_to_bottom"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::SetInputMode { new_mode: InputMode::PinnedToBottom, @@ -242,7 +244,7 @@ pub fn init_actions_from_parent_view( // Add command palette entry for toggling between Warp and Classic input modes app.register_fixed_bindings(vec![FixedBinding::empty( - "Toggle Input Mode (Warp/Classic)".to_string(), + i18n::t("settings.appearance.action.toggle_input_mode"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleInputMode, )), @@ -250,7 +252,7 @@ pub fn init_actions_from_parent_view( ) .with_group(bindings::BindingGroup::Settings.as_str())]); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "open new windows with custom size", + i18n::t("settings.appearance.action.open_windows_custom_size"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleOpenWindowsAtCustomSize, )), @@ -259,7 +261,7 @@ pub fn init_actions_from_parent_view( )); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "window blur acrylic texture", + i18n::t("settings.appearance.action.window_blur_acrylic_texture"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleBlurTexture, )), @@ -268,7 +270,7 @@ pub fn init_actions_from_parent_view( )); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "tools panel visibility across tabs", + i18n::t("settings.appearance.action.tools_panel_visibility_across_tabs"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleLeftPanelVisibility, )), @@ -277,7 +279,7 @@ pub fn init_actions_from_parent_view( )); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "agent font matching terminal font", + i18n::t("settings.appearance.action.agent_font_matching_terminal_font"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleMatchAIToTerminalFontFamily, )), @@ -286,7 +288,7 @@ pub fn init_actions_from_parent_view( )); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "notebook font size matching terminal font size", + i18n::t("settings.appearance.action.notebook_font_size_matching_terminal"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleMatchNotebookToMonospaceFontSize, )), @@ -296,7 +298,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "tab indicators", + i18n::t("settings.appearance.action.tab_indicators"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleTabIndicators, )), @@ -314,8 +316,8 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::custom( SettingActionPairDescriptions::new( - "Show code review button in tab bar", - "Hide code review button in tab bar", + i18n::t("settings.appearance.action.show_code_review_button"), + i18n::t("settings.appearance.action.hide_code_review_button"), ), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleShowCodeReviewButton, @@ -336,7 +338,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "focus follows mouse", + i18n::t("settings.appearance.panes.focus_follows_mouse"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleFocusPaneOnHover, )), @@ -354,7 +356,7 @@ pub fn init_actions_from_parent_view( // Add bindings for each visibility option. app.register_fixed_bindings([ FixedBinding::empty( - "Always show tab bar".to_string(), + i18n::t("settings.appearance.action.always_show_tab_bar"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::SetWorkspaceDecorationVisibility( WorkspaceDecorationVisibility::AlwaysShow, @@ -364,7 +366,7 @@ pub fn init_actions_from_parent_view( ) .with_group(bindings::BindingGroup::Settings.as_str()), FixedBinding::empty( - "Hide tab bar if fullscreen".to_string(), + i18n::t("settings.appearance.action.hide_tab_bar_if_fullscreen"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::SetWorkspaceDecorationVisibility( WorkspaceDecorationVisibility::HideFullscreen, @@ -374,7 +376,7 @@ pub fn init_actions_from_parent_view( ) .with_group(bindings::BindingGroup::Settings.as_str()), FixedBinding::empty( - "Only show tab bar on hover".to_string(), + i18n::t("settings.appearance.action.only_show_tab_bar_on_hover"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::SetWorkspaceDecorationVisibility( WorkspaceDecorationVisibility::OnHover, @@ -388,7 +390,7 @@ pub fn init_actions_from_parent_view( // Add a toggle alias for "Zen mode". toggle_binding_pairs.push( ToggleSettingActionPair::new( - "zen mode", + i18n::t("settings.appearance.action.zen_mode"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleWorkspaceDecorationVisibility, )), @@ -405,7 +407,7 @@ pub fn init_actions_from_parent_view( if FeatureFlag::VerticalTabs.is_enabled() { toggle_binding_pairs.push(ToggleSettingActionPair::new( - "vertical tab layout", + i18n::t("settings.appearance.action.vertical_tab_layout"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleVerticalTabs, )), @@ -413,7 +415,7 @@ pub fn init_actions_from_parent_view( flags::USE_VERTICAL_TABS_FLAG, )); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "show vertical tabs panel in restored windows", + i18n::t("settings.appearance.action.vertical_tabs_panel_restored_windows"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleShowVerticalTabPanelInRestoredWindows, )), @@ -421,7 +423,7 @@ pub fn init_actions_from_parent_view( flags::SHOW_VERTICAL_TAB_PANEL_IN_RESTORED_WINDOWS_FLAG, )); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "latest user prompt as conversation title in tab names", + i18n::t("settings.appearance.action.latest_prompt_as_tab_title"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleUseLatestUserPromptAsConversationTitleInTabNames, )), @@ -432,7 +434,7 @@ pub fn init_actions_from_parent_view( if FeatureFlag::Ligatures.is_enabled() { toggle_binding_pairs.push(ToggleSettingActionPair::new( - "ligature rendering", + i18n::t("settings.appearance.action.ligature_rendering"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleLigatureRendering, )), @@ -442,7 +444,7 @@ pub fn init_actions_from_parent_view( } toggle_binding_pairs.push(ToggleSettingActionPair::new( - "preserve active tab color for new tabs", + i18n::t("settings.appearance.action.preserve_active_tab_color"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::TogglePreserveActiveTabColor, )), @@ -451,7 +453,7 @@ pub fn init_actions_from_parent_view( )); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "custom padding in alt-screen", + i18n::t("settings.appearance.action.custom_padding_alt_screen"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleAltScreenPadding, )), @@ -500,6 +502,7 @@ pub enum AppearancePageAction { }, SetInputType(InputBoxType), SetAppIcon(AppIcon), + SetLanguage(Language), SetCursorType(CursorDisplayType), SetWorkspaceDecorationVisibility(WorkspaceDecorationVisibility), ToggleWorkspaceDecorationVisibility, @@ -562,6 +565,7 @@ pub struct AppearanceSettingsPageView { input_mode_dropdown: ViewHandle>, input_type_radio_state: RadioButtonStateHandle, app_icon_dropdown: ViewHandle>, + language_dropdown: ViewHandle>, workspace_decorations_dropdown: ViewHandle>, tab_close_button_position_dropdown: ViewHandle>, zoom_level_dropdown: ViewHandle>, @@ -647,6 +651,7 @@ impl TypedActionView for AppearanceSettingsPageView { } => self.set_input_mode(*new_mode, *from_binding, ctx), SetInputType(input_type) => self.set_input_type(*input_type, ctx), SetAppIcon(new_icon) => self.set_app_icon(*new_icon, ctx), + SetLanguage(new_language) => self.set_language(*new_language, ctx), SetCursorType(cursor_display_type) => self.set_cursor_type(*cursor_display_type, ctx), OpacitySliderDragged(val) => self.set_opacity(*val, false, ctx), BlurSliderDragged(val) => self.set_blur(*val, false, ctx), @@ -1212,6 +1217,38 @@ impl AppearanceSettingsPageView { dropdown }); + let language_dropdown = ctx.add_typed_action_view(|ctx| { + let mut dropdown = Dropdown::new(ctx); + dropdown.set_top_bar_max_width(INPUT_MODE_DROPDOWN_WIDTH); + dropdown.set_menu_width(INPUT_MODE_DROPDOWN_WIDTH, ctx); + + let values: Vec = all::().collect(); + let current_value = LanguageSettings::as_ref(ctx).language(); + let selected_index = values + .iter() + .position(|val| *val == current_value) + .unwrap_or_else(|| { + log::error!("Could not find current Language value in dropdown option list"); + 0 + }); + + dropdown.add_items( + values + .into_iter() + .map(|val| { + DropdownItem::new( + Self::language_dropdown_item_label(val), + AppearancePageAction::SetLanguage(val), + ) + }) + .collect(), + ctx, + ); + dropdown.set_selected_by_index(selected_index, ctx); + + dropdown + }); + let enforce_min_contrast_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = Dropdown::new(ctx); @@ -1291,6 +1328,7 @@ impl AppearanceSettingsPageView { input_mode_dropdown, input_type_radio_state, app_icon_dropdown, + language_dropdown, enforce_min_contrast_dropdown, workspace_decorations_dropdown: Self::build_workspace_decoration_visibility_dropdown( ctx, @@ -1316,17 +1354,28 @@ impl AppearanceSettingsPageView { } fn build_page(ctx: &mut ViewContext) -> PageType { + // Language switcher is placed first so it's the very top of the + // Appearance page and impossible to miss. The category header is + // intentionally bilingual and static: it labels a language switcher (so + // it should be legible in either language), and `Category` titles are + // fixed at construction rather than re-evaluated per render. The row + // label/subtext inside still translate live. let mut categories = vec![Category::new( - "Themes", + "语言 / Language", + vec![Box::new(LanguageWidget::default())], + )]; + + categories.push(Category::new( + Box::leak(i18n::t("settings.appearance.category.themes").into_boxed_str()), vec![ Box::new(CreateCustomThemeWidget::default()), Box::new(ThemeSelectWidget::default()), ], - )]; + )); if AppIconSettings::as_ref(ctx).is_supported_on_current_platform() { categories.push(Category::new( - "Icon", + Box::leak(i18n::t("settings.appearance.category.icon").into_boxed_str()), vec![Box::new(CustomAppIconWidget::default())], )); } @@ -1370,7 +1419,10 @@ impl AppearanceSettingsPageView { } if !window_settings_widgets.is_empty() { - categories.push(Category::new("Window", window_settings_widgets)); + categories.push(Category::new( + Box::leak(i18n::t("settings.appearance.category.window").into_boxed_str()), + window_settings_widgets, + )); } // Create the Input category with all widgets @@ -1382,10 +1434,13 @@ impl AppearanceSettingsPageView { Box::new(InputModeWidget::default()), ]; - categories.push(Category::new("Input", category_widgets)); + categories.push(Category::new( + Box::leak(i18n::t("settings.appearance.category.input").into_boxed_str()), + category_widgets, + )); categories.push(Category::new( - "Panes", + Box::leak(i18n::t("settings.appearance.category.panes").into_boxed_str()), vec![ Box::new(DimInactivePanesWidget::default()), Box::new(FocusFollowsMouseWidget::default()), @@ -1399,7 +1454,10 @@ impl AppearanceSettingsPageView { if FeatureFlag::MinimalistUI.is_enabled() { block_settings_widgets.push(Box::new(ShowBlockDividersWidget::default())); } - categories.push(Category::new("Blocks", block_settings_widgets)); + categories.push(Category::new( + Box::leak(i18n::t("settings.appearance.category.blocks").into_boxed_str()), + block_settings_widgets, + )); let font_settings = FontSettings::as_ref(ctx); let mut text_settings_widgets: Vec>> = vec![ @@ -1428,10 +1486,13 @@ impl AppearanceSettingsPageView { text_settings_widgets.push(Box::new(LigaturesWidget::default())); } - categories.push(Category::new("Text", text_settings_widgets)); + categories.push(Category::new( + Box::leak(i18n::t("settings.appearance.category.text").into_boxed_str()), + text_settings_widgets, + )); categories.push(Category::new( - "Cursor", + Box::leak(i18n::t("settings.appearance.category.cursor").into_boxed_str()), vec![ Box::new(CursorTypeWidget::default()), Box::new(BlinkingCursorWidget::default()), @@ -1477,10 +1538,13 @@ impl AppearanceSettingsPageView { tab_settings_widgets.push(Box::new(DirectoryTabColorsWidget { add_picker })); } - categories.push(Category::new("Tabs", tab_settings_widgets)); + categories.push(Category::new( + Box::leak(i18n::t("settings.appearance.category.tabs").into_boxed_str()), + tab_settings_widgets, + )); categories.push(Category::new( - "Full-screen Apps", + Box::leak(i18n::t("settings.appearance.category.full_screen_apps").into_boxed_str()), vec![Box::new(AltScreenPaddingWidget::default())], )); @@ -1587,69 +1651,96 @@ impl AppearanceSettingsPageView { initial_dropdown_item } - fn input_mode_dropdown_item_label(val: InputMode) -> &'static str { + fn input_mode_dropdown_item_label(val: InputMode) -> String { match val { - InputMode::PinnedToBottom => "Pin to the bottom (Warp mode)", - InputMode::PinnedToTop => "Pin to the top (Reverse mode)", - InputMode::Waterfall => "Start at the top (Classic mode)", + InputMode::PinnedToBottom => { + i18n::t("settings.appearance.input.position.pin_bottom_warp") + } + InputMode::PinnedToTop => i18n::t("settings.appearance.input.position.pin_top_reverse"), + InputMode::Waterfall => i18n::t("settings.appearance.input.position.start_top_classic"), } } - fn app_icon_dropdown_item_label(val: AppIcon) -> &'static str { + fn app_icon_dropdown_item_label(val: AppIcon) -> String { match val { - AppIcon::Aurora => "Aurora", - AppIcon::Default => "Default", - AppIcon::Classic1 => "Classic 1", - AppIcon::Classic2 => "Classic 2", - AppIcon::Classic3 => "Classic 3", - AppIcon::Comets => "Comets", - AppIcon::GlassSky => "Glass Sky", - AppIcon::Glitch => "Glitch", - AppIcon::Cow => "Cow", - AppIcon::Glow => "Glow", - AppIcon::Holographic => "Holographic", - AppIcon::Mono => "Mono", - AppIcon::Neon => "Neon", - AppIcon::Original => "Original", - AppIcon::Starburst => "Starburst", - AppIcon::Sticker => "Sticker", - AppIcon::WarpOne => "Warp 1", + AppIcon::Aurora => i18n::t("settings.appearance.app_icon.option.aurora"), + AppIcon::Default => i18n::t("settings.appearance.app_icon.option.default"), + AppIcon::Classic1 => i18n::t("settings.appearance.app_icon.option.classic_1"), + AppIcon::Classic2 => i18n::t("settings.appearance.app_icon.option.classic_2"), + AppIcon::Classic3 => i18n::t("settings.appearance.app_icon.option.classic_3"), + AppIcon::Comets => i18n::t("settings.appearance.app_icon.option.comets"), + AppIcon::GlassSky => i18n::t("settings.appearance.app_icon.option.glass_sky"), + AppIcon::Glitch => i18n::t("settings.appearance.app_icon.option.glitch"), + AppIcon::Cow => i18n::t("settings.appearance.app_icon.option.cow"), + AppIcon::Glow => i18n::t("settings.appearance.app_icon.option.glow"), + AppIcon::Holographic => i18n::t("settings.appearance.app_icon.option.holographic"), + AppIcon::Mono => i18n::t("settings.appearance.app_icon.option.mono"), + AppIcon::Neon => i18n::t("settings.appearance.app_icon.option.neon"), + AppIcon::Original => i18n::t("settings.appearance.app_icon.option.original"), + AppIcon::Starburst => i18n::t("settings.appearance.app_icon.option.starburst"), + AppIcon::Sticker => i18n::t("settings.appearance.app_icon.option.sticker"), + AppIcon::WarpOne => i18n::t("settings.appearance.app_icon.option.warp_1"), } } - fn thin_strokes_dropdown_item_label(val: ThinStrokes) -> &'static str { + /// Label for each language option. Shown in the language's own script so it + /// is recognizable regardless of the currently active UI language. + fn language_dropdown_item_label(val: Language) -> &'static str { match val { - ThinStrokes::Never => "Never", - ThinStrokes::OnLowDpiDisplays => "On low-DPI displays", - ThinStrokes::OnHighDpiDisplays => "On high-DPI displays", - ThinStrokes::Always => "Always", + Language::ZhCn => "简体中文", + Language::En => "English", } } - fn enforce_minimum_contrast_dropdown_item_label(val: EnforceMinimumContrast) -> &'static str { + fn thin_strokes_dropdown_item_label(val: ThinStrokes) -> String { match val { - EnforceMinimumContrast::Always => "Always", - EnforceMinimumContrast::OnlyNamedColors => "Only for named colors", - EnforceMinimumContrast::Never => "Never", + ThinStrokes::Never => i18n::t("settings.appearance.text.thin_strokes.never"), + ThinStrokes::OnLowDpiDisplays => { + i18n::t("settings.appearance.text.thin_strokes.low_dpi") + } + ThinStrokes::OnHighDpiDisplays => { + i18n::t("settings.appearance.text.thin_strokes.high_dpi") + } + ThinStrokes::Always => i18n::t("settings.appearance.text.thin_strokes.always"), + } + } + + fn enforce_minimum_contrast_dropdown_item_label(val: EnforceMinimumContrast) -> String { + match val { + EnforceMinimumContrast::Always => { + i18n::t("settings.appearance.text.min_contrast.always") + } + EnforceMinimumContrast::OnlyNamedColors => { + i18n::t("settings.appearance.text.min_contrast.named_colors") + } + EnforceMinimumContrast::Never => i18n::t("settings.appearance.text.min_contrast.never"), } } fn workspace_decoration_visibility_dropdown_item_label( value: WorkspaceDecorationVisibility, - ) -> &'static str { + ) -> String { match value { - WorkspaceDecorationVisibility::AlwaysShow => "Always", - WorkspaceDecorationVisibility::HideFullscreen => "When windowed", - WorkspaceDecorationVisibility::OnHover => "Only on hover", + WorkspaceDecorationVisibility::AlwaysShow => { + i18n::t("settings.appearance.tabs.visibility.always") + } + WorkspaceDecorationVisibility::HideFullscreen => { + i18n::t("settings.appearance.tabs.visibility.when_windowed") + } + WorkspaceDecorationVisibility::OnHover => { + i18n::t("settings.appearance.tabs.visibility.only_on_hover") + } } } - fn tab_close_button_position_dropdown_item_label( - value: TabCloseButtonPosition, - ) -> &'static str { + fn tab_close_button_position_dropdown_item_label(value: TabCloseButtonPosition) -> String { match value { - TabCloseButtonPosition::Right => "Right", - TabCloseButtonPosition::Left => "Left", + TabCloseButtonPosition::Right => { + i18n::t("settings.appearance.tabs.close_button_position.right") + } + TabCloseButtonPosition::Left => { + i18n::t("settings.appearance.tabs.close_button_position.left") + } } } @@ -2322,6 +2413,15 @@ impl AppearanceSettingsPageView { }); } + /// Persists the chosen UI language. The settings change event is observed in + /// `settings::init`, which swaps the active i18n catalog and repaints every + /// view, so the whole UI updates live without a restart. + fn set_language(&mut self, new_language: Language, ctx: &mut ViewContext) { + LanguageSettings::handle(ctx).update(ctx, |language_settings, ctx| { + report_if_error!(language_settings.language.set_value(new_language, ctx)); + }); + } + fn set_cursor_type(&mut self, new_cursor_type: CursorDisplayType, ctx: &mut ViewContext) { AppEditorSettings::handle(ctx).update(ctx, |app_editor_settings, ctx| { report_if_error!(app_editor_settings @@ -2674,7 +2774,7 @@ impl SettingsWidget for CreateCustomThemeWidget { appearance .ui_builder() .link( - "Create your own custom theme".to_string(), + i18n::t("settings.appearance.theme.create_custom"), Some("https://docs.warp.dev/terminal/appearance/custom-themes".to_string()), None, self.mouse_state.clone(), @@ -2709,9 +2809,9 @@ impl ThemeSelectWidget { ) -> Box { let theme: WarpTheme = WarpConfig::as_ref(app).theme_config().theme(&theme_kind); let mode_ui_label = match theme_chooser_mode { - ThemeChooserMode::SystemLight => "Light", - ThemeChooserMode::SystemDark => "Dark", - ThemeChooserMode::SystemAgnostic => "Current theme", + ThemeChooserMode::SystemLight => i18n::t("settings.appearance.theme.mode.light"), + ThemeChooserMode::SystemDark => i18n::t("settings.appearance.theme.mode.dark"), + ThemeChooserMode::SystemAgnostic => i18n::t("settings.appearance.theme.mode.current"), }; ConstrainedBox::new( @@ -2830,7 +2930,7 @@ impl SettingsWidget for ThemeSelectWidget { Flex::column() .with_cross_axis_alignment(CrossAxisAlignment::Stretch) .with_child(render_body_item::( - "Sync with OS".into(), + i18n::t("settings.appearance.theme.sync_with_os"), None, LocalOnlyIconState::for_setting( UseSystemTheme::storage_key(), @@ -2857,10 +2957,9 @@ impl SettingsWidget for ThemeSelectWidget { .with_child( appearance .ui_builder() - .span( - "Automatically switch between light and dark themes when your system does." - .to_string(), - ) + .span(i18n::t( + "settings.appearance.theme.sync_with_os.description", + )) .with_style( UiComponentStyles::default().set_margin(Coords::default().bottom(10.)), ) @@ -2907,10 +3006,12 @@ impl SettingsWidget for CustomAppIconWidget { } }; + let app_icon_label = i18n::t("settings.appearance.app_icon.label"); + let app_icon_bundle_warning = i18n::t("settings.appearance.app_icon.bundle_warning"); let dropdown = render_dropdown_item( appearance, - "Customize your app icon", - show_bundle_warning.then_some("Changing the app icon requires the app to be bundled."), + app_icon_label.as_str(), + show_bundle_warning.then_some(app_icon_bundle_warning.as_str()), None, LocalOnlyIconState::Hidden, None, @@ -2934,7 +3035,7 @@ impl SettingsWidget for CustomAppIconWidget { appearance .ui_builder() .wrappable_text( - "You may need to restart Warp for MacOS to apply the preferred icon style.", + i18n::t("settings.appearance.app_icon.restart_warning"), true, ) .with_style(UiComponentStyles { @@ -2979,7 +3080,7 @@ impl SettingsWidget for CustomWindowSizeWidget { let row_border_color: Option = (!view.valid_new_window_rows).then(|| themes::theme::Fill::error().into()); let mut column = Flex::column().with_child(render_body_item::( - "Open new windows with custom size".into(), + i18n::t("settings.appearance.window.custom_size"), None, LocalOnlyIconState::for_setting( OpenWindowsAtCustomSize::storage_key(), @@ -3003,7 +3104,7 @@ impl SettingsWidget for CustomWindowSizeWidget { if *window_settings.open_windows_at_custom_size.value() { column.add_child( Container::new(render_body_item::( - "Columns".into(), + i18n::t("settings.appearance.window.columns"), None, // We show the local-only icon for this with the toggle, not the individual inputs. LocalOnlyIconState::Hidden, @@ -3039,7 +3140,7 @@ impl SettingsWidget for CustomWindowSizeWidget { ); column.add_child( Container::new(render_body_item::( - "Rows".into(), + i18n::t("settings.appearance.window.rows"), None, // We show the local-only icon for this with the toggle, not the individual inputs. LocalOnlyIconState::Hidden, @@ -3104,7 +3205,7 @@ impl SettingsWidget for WindowOpacityWidget { return Flex::column() .with_child( Container::new(render_body_item_label::( - "Window Opacity:".to_owned(), + i18n::t("settings.appearance.window.opacity_label"), None, None, LocalOnlyIconState::Hidden, @@ -3116,7 +3217,7 @@ impl SettingsWidget for WindowOpacityWidget { .with_child( Container::new( FormattedTextElement::from_str( - "Transparency is not supported with your graphics drivers.", + i18n::t("settings.appearance.window.opacity_unsupported"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -3131,7 +3232,10 @@ impl SettingsWidget for WindowOpacityWidget { let opacity_value = *window_settings.background_opacity; let mut col = Flex::column().with_child(render_body_item::( - format!("Window Opacity: {opacity_value}"), + format!( + "{}: {opacity_value}", + i18n::t("settings.appearance.window.opacity") + ), // TODO(CORE-3384) add AdditionalInfo here. None, LocalOnlyIconState::for_setting( @@ -3167,9 +3271,7 @@ impl SettingsWidget for WindowOpacityWidget { // Skip showing the warning for OpenGL since WGPU often incorrectly reports it as not // supporting alpha. if !window.supports_transparency() && window.graphics_backend() != GraphicsBackend::Gl { - let mut message = Cow::Borrowed( - "The selected graphics settings may not support rendering transparent windows.", - ); + let mut message = i18n::t("settings.appearance.window.transparency_unsupported"); let gpu_settings = GPUSettings::as_ref(app); if (gpu_settings .prefer_low_power_gpu @@ -3179,10 +3281,10 @@ impl SettingsWidget for WindowOpacityWidget { .preferred_backend .is_supported_on_current_platform() { - message.to_mut().push_str( - " Try changing the settings for the graphics backend or integrated GPU in \ - Features > System.", - ); + message.push(' '); + message.push_str(&i18n::t( + "settings.appearance.window.transparency_unsupported_hint", + )); } col.add_child( @@ -3236,7 +3338,10 @@ impl SettingsWidget for WindowBlurWidget { Flex::column() .with_child(render_body_item::( - format!("Window Blur Radius: {blur_value}"), + format!( + "{}: {blur_value}", + i18n::t("settings.appearance.window.blur_radius") + ), Some(label_info), LocalOnlyIconState::for_setting( BackgroundBlurRadius::storage_key(), @@ -3293,7 +3398,7 @@ impl SettingsWidget for WindowBlurTextureWidget { let window_settings = WindowSettings::as_ref(app); let use_blur_texture = *window_settings.background_blur_texture; let mut col = Flex::column().with_child(render_body_item::( - "Use Window Blur (Acrylic texture)".to_string(), + i18n::t("settings.appearance.window.blur_texture"), None, LocalOnlyIconState::for_setting( BackgroundBlurTexture::storage_key(), @@ -3319,7 +3424,7 @@ impl SettingsWidget for WindowBlurTextureWidget { col.add_child( Container::new( FormattedTextElement::from_str( - "The selected hardware may not support rendering transparent windows.", + i18n::t("settings.appearance.window.blur_texture_unsupported"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -3357,7 +3462,7 @@ impl SettingsWidget for ToolsPanelStateScopeWidget { let is_enabled = *window_settings.left_panel_visibility_across_tabs; render_body_item::( - "Tools panel visibility is consistent across tabs".to_string(), + i18n::t("settings.appearance.window.tools_panel_consistent"), None, LocalOnlyIconState::for_setting( LeftPanelVisibilityAcrossTabs::storage_key(), @@ -3412,8 +3517,8 @@ impl SettingsWidget for InputTypeWidget { .radio_buttons( self.radio_buttons_states.clone(), vec![ - RadioButtonItem::text("Warp"), - RadioButtonItem::text("Shell (PS1)"), + RadioButtonItem::text(i18n::t("settings.appearance.input.type.warp")), + RadioButtonItem::text(i18n::t("settings.appearance.input.type.shell_ps1")), ], view.input_type_radio_state.clone(), Some(input_type as usize), @@ -3433,7 +3538,7 @@ impl SettingsWidget for InputTypeWidget { .finish(); render_body_item::( - "Input type".into(), + i18n::t("settings.appearance.input.type"), None, LocalOnlyIconState::Hidden, ToggleState::Enabled, @@ -3460,9 +3565,10 @@ impl SettingsWidget for InputModeWidget { appearance: &Appearance, app: &AppContext, ) -> Box { + let input_position_label = i18n::t("settings.appearance.input.position"); render_dropdown_item( appearance, - "Input position", + input_position_label.as_str(), None, None, LocalOnlyIconState::for_setting( @@ -3477,6 +3583,36 @@ impl SettingsWidget for InputModeWidget { } } +#[derive(Default)] +struct LanguageWidget {} + +impl SettingsWidget for LanguageWidget { + type View = AppearanceSettingsPageView; + + fn search_terms(&self) -> &str { + "language locale 语言 中文 chinese english i18n internationalization translation" + } + + fn render( + &self, + view: &Self::View, + appearance: &Appearance, + _app: &AppContext, + ) -> Box { + let label = i18n::t("settings.appearance.language.label"); + let subtext = i18n::t("settings.appearance.language.subtext"); + render_dropdown_item( + appearance, + label.as_str(), + Some(subtext.as_str()), + None, + LocalOnlyIconState::Hidden, + None, + &view.language_dropdown, + ) + } +} + #[derive(Default)] struct PromptWidget { button_mouse_state: MouseStateHandle, @@ -3584,7 +3720,7 @@ impl SettingsWidget for DimInactivePanesWidget { app: &AppContext, ) -> Box { render_body_item::( - "Dim inactive panes".into(), + i18n::t("settings.appearance.panes.dim_inactive"), None, LocalOnlyIconState::for_setting( ShouldDimInactivePanes::storage_key(), @@ -3627,7 +3763,7 @@ impl SettingsWidget for FocusFollowsMouseWidget { app: &AppContext, ) -> Box { render_body_item::( - "Focus follows mouse".into(), + i18n::t("settings.appearance.panes.focus_follows_mouse"), None, LocalOnlyIconState::for_setting( FocusPaneOnHover::storage_key(), @@ -3675,7 +3811,7 @@ impl SettingsWidget for CompactModeWidget { ); render_body_item::( - "Compact mode".into(), + i18n::t("settings.appearance.blocks.compact_mode"), None, LocalOnlyIconState::for_setting( Spacing::storage_key(), @@ -3722,7 +3858,7 @@ impl SettingsWidget for JumpToBottomOfBlockWidget { .show_jump_to_bottom_of_block_button .value(); render_body_item::( - "Show Jump to Bottom of Block button".into(), + i18n::t("settings.appearance.blocks.jump_to_bottom"), None, LocalOnlyIconState::for_setting( ShowJumpToBottomOfBlockButton::storage_key(), @@ -3769,7 +3905,7 @@ impl SettingsWidget for ShowBlockDividersWidget { let block_list_settings = BlockListSettings::as_ref(app); let enabled = block_list_settings.show_block_dividers.value(); render_body_item::( - "Show block dividers".into(), + i18n::t("settings.appearance.blocks.show_dividers"), None, LocalOnlyIconState::for_setting( ShowBlockDividers::storage_key(), @@ -3815,7 +3951,7 @@ impl SettingsWidget for AIFontWidget { let mut ai_font_row = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Center); let mut ai_font = Flex::column(); ai_font.add_child(render_body_item_label::( - "Agent font".to_string(), + i18n::t("settings.appearance.text.agent_font"), None, None, LocalOnlyIconState::for_setting( @@ -3851,7 +3987,7 @@ impl SettingsWidget for AIFontWidget { ai_font_row.add_child( appearance .ui_builder() - .span("Match terminal".to_string()) + .span(i18n::t("settings.appearance.text.match_terminal")) .build() .with_margin_left(2.) .with_margin_right(16.) @@ -3879,7 +4015,7 @@ impl TerminalFontWidget { line_height.add_child( appearance .ui_builder() - .label("Line height".to_string()) + .label(i18n::t("settings.appearance.text.line_height")) .with_style(UiComponentStyles { margin: Some(Coords { left: 12., @@ -3946,7 +4082,7 @@ impl TerminalFontWidget { font_size: Some(appearance.ui_font_size() * 0.8), ..Default::default() }) - .with_text_label("Reset to default".to_string()); + .with_text_label(i18n::t("settings.appearance.text.reset_to_default")); button .build() @@ -3977,7 +4113,7 @@ impl SettingsWidget for TerminalFontWidget { // Terminal Font let mut terminal_font = Flex::column(); terminal_font.add_child(render_body_item_label::( - "Terminal font".to_string(), + i18n::t("settings.appearance.text.terminal_font"), None, None, LocalOnlyIconState::for_setting( @@ -4020,7 +4156,7 @@ impl SettingsWidget for TerminalFontWidget { 1., appearance .ui_builder() - .span("View all available system fonts".to_string()) + .span(i18n::t("settings.appearance.text.view_all_fonts")) .build() .with_margin_left(2.) .finish(), @@ -4041,7 +4177,7 @@ impl SettingsWidget for TerminalFontWidget { font_weight.add_child( appearance .ui_builder() - .label("Font weight".to_string()) + .label(i18n::t("settings.appearance.text.font_weight")) .with_style(UiComponentStyles { font_size: Some(CONTENT_FONT_SIZE), ..Default::default() @@ -4064,7 +4200,7 @@ impl SettingsWidget for TerminalFontWidget { font_size.add_child( appearance .ui_builder() - .label("Font size (px)".to_string()) + .label(i18n::t("settings.appearance.text.font_size_px")) .with_style(UiComponentStyles { margin: Some(Coords { left: 2., @@ -4148,7 +4284,7 @@ impl SettingsWidget for NotebookFontSizeWidget { Align::new( appearance .ui_builder() - .span("Notebook font size".to_string()) + .span(i18n::t("settings.appearance.text.notebook_font_size")) .build() .with_margin_right(16.) .finish(), @@ -4174,7 +4310,7 @@ impl SettingsWidget for NotebookFontSizeWidget { .with_child( appearance .ui_builder() - .span("Match terminal".to_string()) + .span(i18n::t("settings.appearance.text.match_terminal")) .build() .with_margin_left(2.) .with_margin_right(16.) @@ -4230,9 +4366,10 @@ impl SettingsWidget for ThinStrokesWidget { appearance: &Appearance, app: &AppContext, ) -> Box { + let thin_strokes_label = i18n::t("settings.appearance.text.thin_strokes"); render_dropdown_item( appearance, - "Use thin strokes", + thin_strokes_label.as_str(), None, None, LocalOnlyIconState::for_setting( @@ -4263,9 +4400,10 @@ impl SettingsWidget for MinimumContrastWidget { appearance: &Appearance, app: &AppContext, ) -> Box { + let min_contrast_label = i18n::t("settings.appearance.text.min_contrast"); render_dropdown_item( appearance, - "Enforce minimum contrast", + min_contrast_label.as_str(), None, None, LocalOnlyIconState::for_setting( @@ -4303,12 +4441,12 @@ impl SettingsWidget for LigaturesWidget { let ligature_rendering_enabled = ligature_rendering.value(); render_body_item::( - "Show ligatures in terminal".into(), + i18n::t("settings.appearance.text.ligatures"), Some(AdditionalInfo { mouse_state: self.info_mouse_state.clone(), on_click_action: None, secondary_text: None, - tooltip_override_text: Some("Ligatures may reduce performance".to_string()), + tooltip_override_text: Some(i18n::t("settings.appearance.text.ligatures_tooltip")), }), LocalOnlyIconState::for_setting( LigatureRenderingEnabled::storage_key(), @@ -4368,7 +4506,7 @@ impl SettingsWidget for CursorTypeWidget { let cursor_display_types: Vec = all::().collect(); render_body_item::( - "Cursor type".into(), + i18n::t("settings.appearance.cursor.type"), None, LocalOnlyIconState::for_setting( CursorBlinkEnabled::storage_key(), @@ -4383,7 +4521,7 @@ impl SettingsWidget for CursorTypeWidget { .with_child( appearance .ui_builder() - .span("Cursor type is disabled in Vim mode".to_string()) + .span(i18n::t("settings.appearance.cursor.type_disabled_vim")) .build() .finish(), ) @@ -4437,7 +4575,7 @@ impl SettingsWidget for BlinkingCursorWidget { let settings = AppEditorSettings::as_ref(app); let cursor_blink = &settings.cursor_blink; render_body_item::( - "Blinking cursor".into(), + i18n::t("settings.appearance.cursor.blinking"), None, LocalOnlyIconState::for_setting( CursorBlinkEnabled::storage_key(), @@ -4477,9 +4615,10 @@ impl SettingsWidget for TabCloseButtonPositionWidget { appearance: &Appearance, app: &AppContext, ) -> Box { + let tab_close_position_label = i18n::t("settings.appearance.tabs.close_button_position"); render_dropdown_item( appearance, - "Tab close button position", + tab_close_position_label.as_str(), None, None, LocalOnlyIconState::for_setting( @@ -4515,7 +4654,7 @@ impl SettingsWidget for TabIndicatorWidget { let tab_settings = TabSettings::as_ref(app); render_body_item::( - "Show tab indicators".into(), + i18n::t("settings.appearance.tabs.show_indicators"), None, LocalOnlyIconState::for_setting( ShowIndicatorsButton::storage_key(), @@ -4560,7 +4699,7 @@ impl SettingsWidget for CodeReviewButtonWidget { let tab_settings = TabSettings::as_ref(app); render_body_item::( - "Show code review button".into(), + i18n::t("settings.appearance.tabs.show_code_review_button"), None, LocalOnlyIconState::for_setting( ShowCodeReviewButton::storage_key(), @@ -4605,7 +4744,7 @@ impl SettingsWidget for PreserveActiveTabColorWidget { let tab_settings = TabSettings::as_ref(app); render_body_item::( - "Preserve active tab color for new tabs".into(), + i18n::t("settings.appearance.tabs.preserve_active_color"), None, LocalOnlyIconState::for_setting( PreserveActiveTabColor::storage_key(), @@ -4650,7 +4789,7 @@ impl SettingsWidget for VerticalTabsWidget { let tab_settings = TabSettings::as_ref(app); render_body_item::( - "Use vertical tab layout".into(), + i18n::t("settings.appearance.tabs.vertical_layout"), None, LocalOnlyIconState::for_setting( UseVerticalTabs::storage_key(), @@ -4695,7 +4834,7 @@ impl SettingsWidget for ShowVerticalTabPanelInRestoredWindowsWidget { let tab_settings = TabSettings::as_ref(app); render_body_item::( - "Show vertical tabs panel in restored windows".into(), + i18n::t("settings.appearance.tabs.show_vertical_panel_restored"), None, LocalOnlyIconState::for_setting( ShowVerticalTabPanelInRestoredWindows::storage_key(), @@ -4716,10 +4855,9 @@ impl SettingsWidget for ShowVerticalTabPanelInRestoredWindowsWidget { ); }) .finish(), - Some( - "When enabled, reopening or restoring a window opens the vertical tabs panel even if it was closed when the window was last saved." - .to_string(), - ), + Some(i18n::t( + "settings.appearance.tabs.show_vertical_panel_restored.description", + )), ) } } @@ -4745,7 +4883,7 @@ impl SettingsWidget for UseLatestUserPromptAsConversationTitleInTabNamesWidget { let tab_settings = TabSettings::as_ref(app); render_body_item::( - "Use latest user prompt as conversation title in tab names".into(), + i18n::t("settings.appearance.tabs.latest_prompt_as_title"), None, LocalOnlyIconState::for_setting( UseLatestUserPromptAsConversationTitleInTabNames::storage_key(), @@ -4769,10 +4907,9 @@ impl SettingsWidget for UseLatestUserPromptAsConversationTitleInTabNamesWidget { ); }) .finish(), - Some( - "Show the latest user prompt instead of the generated conversation title for Oz and third-party agent sessions in vertical tabs." - .to_string(), - ), + Some(i18n::t( + "settings.appearance.tabs.latest_prompt_as_title.description", + )), ) } } @@ -4794,7 +4931,7 @@ impl SettingsWidget for EditToolbarWidget { _app: &AppContext, ) -> Box { let label = render_body_item_label::( - "Header toolbar layout".to_string(), + i18n::t("settings.appearance.tabs.header_toolbar_layout"), None, None, LocalOnlyIconState::Hidden, @@ -4901,7 +5038,7 @@ impl SettingsWidget for DirectoryTabColorsWidget { .with_spacing(4.) .with_child( Text::new( - "Directory tab colors", + i18n::t("settings.appearance.tabs.directory_colors"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -4911,7 +5048,7 @@ impl SettingsWidget for DirectoryTabColorsWidget { ) .with_child( Text::new( - "Automatically color tabs based on the directory or repo you're working in.", + i18n::t("settings.appearance.tabs.directory_colors.description"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -4967,7 +5104,7 @@ impl SettingsWidget for DirectoryTabColorsWidget { }; let is_selected = current_color == tab_color; let tooltip_text = match ansi_id { - None => "Default (no color)".to_string(), + None => i18n::t("settings.appearance.tabs.directory_colors.default_no_color"), Some(id) => id.to_string(), }; let dir_path_clone = PathBuf::from(&dir_path); @@ -5046,9 +5183,10 @@ impl SettingsWidget for ZenModeWidget { appearance: &Appearance, app: &AppContext, ) -> Box { + let show_tab_bar_label = i18n::t("settings.appearance.tabs.show_tab_bar"); render_dropdown_item( appearance, - "Show the tab bar", + show_tab_bar_label.as_str(), None, None, LocalOnlyIconState::for_setting( @@ -5085,7 +5223,7 @@ impl SettingsWidget for AltScreenPaddingWidget { let terminal_settings = &TerminalSettings::as_ref(app); let theme = appearance.theme(); let mut column = Flex::column().with_child(render_body_item::( - "Use custom padding in alt-screen".into(), + i18n::t("settings.appearance.full_screen_apps.custom_padding"), Some(AdditionalInfo { mouse_state: self.additional_info_mouse_state.clone(), on_click_action: Some(AppearancePageAction::OpenUrl( @@ -5146,7 +5284,7 @@ impl SettingsWidget for AltScreenPaddingWidget { Container::new( Align::new( Text::new( - "Uniform padding (px)", + i18n::t("settings.appearance.full_screen_apps.uniform_padding_px"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -5202,10 +5340,12 @@ impl SettingsWidget for ZoomLevelWidget { }) .finish(); + let zoom_label = i18n::t("settings.appearance.window.zoom"); + let zoom_desc = i18n::t("settings.appearance.window.zoom.description"); render_dropdown_item( appearance, - "Zoom", - Some("Adjusts the default zoom level across all windows"), + zoom_label.as_str(), + Some(zoom_desc.as_str()), Some(reset_button), LocalOnlyIconState::for_setting( crate::window_settings::ZoomLevel::storage_key(), diff --git a/app/src/settings_view/billing_and_usage/billing_cycle_usage_common.rs b/app/src/settings_view/billing_and_usage/billing_cycle_usage_common.rs index 34bcbf8917..d30aad7365 100644 --- a/app/src/settings_view/billing_and_usage/billing_cycle_usage_common.rs +++ b/app/src/settings_view/billing_and_usage/billing_cycle_usage_common.rs @@ -91,26 +91,30 @@ pub fn cost_type_color(cost_type: &AiCreditsUsageAndCostType) -> ColorU { } } -fn cost_type_label(cost_type: &AiCreditsUsageAndCostType) -> &'static str { +fn cost_type_label(cost_type: &AiCreditsUsageAndCostType) -> String { match cost_type { - AiCreditsUsageAndCostType::BaseLimit => "Base", - AiCreditsUsageAndCostType::BonusGrant => "Add-ons", - AiCreditsUsageAndCostType::Payg => "Pay-as-you-go", - AiCreditsUsageAndCostType::AmbientBonusGrant => "Cloud-only", - AiCreditsUsageAndCostType::Aggregate => "Combined", - AiCreditsUsageAndCostType::Other(_) => "Other", + AiCreditsUsageAndCostType::BaseLimit => i18n::t("settings.billing.legend_base"), + AiCreditsUsageAndCostType::BonusGrant => i18n::t("settings.billing.legend_addons"), + AiCreditsUsageAndCostType::Payg => i18n::t("settings.billing.legend_payg"), + AiCreditsUsageAndCostType::AmbientBonusGrant => { + i18n::t("settings.billing.legend_cloud_only") + } + AiCreditsUsageAndCostType::Aggregate => i18n::t("settings.billing.legend_combined"), + AiCreditsUsageAndCostType::Other(_) => i18n::t("settings.billing.usage.bucket.other"), } } -fn bucket_label(bucket: &AiCreditsUsageBucket) -> &'static str { +fn bucket_label(bucket: &AiCreditsUsageBucket) -> String { match bucket { - AiCreditsUsageBucket::Ai => "AI", - AiCreditsUsageBucket::Compute => "Compute", - AiCreditsUsageBucket::Platform => "Platform", - AiCreditsUsageBucket::SuggestedCodeDiffs => "Suggested code diffs", - AiCreditsUsageBucket::Voice => "Voice", - AiCreditsUsageBucket::Aggregate => "Total", - AiCreditsUsageBucket::Other(_) => "Other", + AiCreditsUsageBucket::Ai => i18n::t("settings.billing.usage.bucket.ai"), + AiCreditsUsageBucket::Compute => i18n::t("settings.billing.usage.bucket.compute"), + AiCreditsUsageBucket::Platform => i18n::t("settings.billing.usage.bucket.platform"), + AiCreditsUsageBucket::SuggestedCodeDiffs => { + i18n::t("settings.billing.usage.bucket.suggested_code_diffs") + } + AiCreditsUsageBucket::Voice => i18n::t("settings.billing.usage.bucket.voice"), + AiCreditsUsageBucket::Aggregate => i18n::t("settings.billing.usage.bucket.total"), + AiCreditsUsageBucket::Other(_) => i18n::t("settings.billing.usage.bucket.other"), } } @@ -286,7 +290,7 @@ pub fn render_breakdown_tooltip( column.add_child(render_tooltip_row( /* no swatch on the total row */ None, - "Total usage".to_string(), + i18n::t("settings.billing.usage.total"), total_credits, total_cost_cents, main, diff --git a/app/src/settings_view/billing_and_usage/billing_cycle_usage_rows.rs b/app/src/settings_view/billing_and_usage/billing_cycle_usage_rows.rs index d227a8631c..22238f077a 100644 --- a/app/src/settings_view/billing_and_usage/billing_cycle_usage_rows.rs +++ b/app/src/settings_view/billing_and_usage/billing_cycle_usage_rows.rs @@ -50,11 +50,11 @@ pub enum SourceFilter { } impl SourceFilter { - pub fn label(self) -> &'static str { + pub fn label(self) -> String { match self { - SourceFilter::All => "All", - SourceFilter::Local => "Local", - SourceFilter::Cloud => "Cloud", + SourceFilter::All => i18n::t("settings.billing.usage.source.all"), + SourceFilter::Local => i18n::t("settings.billing.usage.source.local"), + SourceFilter::Cloud => i18n::t("settings.billing.usage.source.cloud"), } } @@ -101,7 +101,7 @@ fn viewer_identity(app: &AppContext) -> (Option, String) { .display_name() .or_else(|| auth_state.username_for_display()) .or_else(|| auth_state.user_email()) - .unwrap_or_else(|| "Your usage".to_string()); + .unwrap_or_else(|| i18n::t("settings.billing.usage.your_usage")); (viewer_uid, display_name) } @@ -198,7 +198,7 @@ impl MemberUsageRow { subject_type: AiCreditsUsageAndCostSubjectType::Team, subject_key: OTHER_MEMBERS_KEY.to_string(), subject_uid: None, - display_name: "Other members".to_string(), + display_name: i18n::t("settings.billing.usage.other_members"), total_credits, total_cost_cents, base_limit: None, @@ -240,7 +240,7 @@ impl MemberUsageRow { display_name: entry .subject_display_name .clone() - .unwrap_or_else(|| "Unknown".to_string()), + .unwrap_or_else(|| i18n::t("settings.billing.usage.unknown_subject")), entries: Vec::new(), }); group.entries.push(entry.clone()); @@ -463,7 +463,7 @@ fn render_usage_tooltip_content(row: &MemberUsageRow, appearance: &Appearance) - fn render_service_account_info_tooltip(appearance: &Appearance) -> Box { let theme = appearance.theme(); let text = Text::new_inline( - "This is an automated agent on your team.".to_string(), + i18n::t("settings.billing.automated_agent_on_team"), appearance.ui_font_family(), 12., ) @@ -838,7 +838,8 @@ fn render_member_header( let show_toggle = visibility.granularity == UsageVisibilityGranularity::FullBreakdown && has_cloud_usage(entries); - let subheader = render_section_subheader("Members", appearance); + let subheader = + render_section_subheader(&i18n::t("settings.billing.members_header"), appearance); let header = if show_toggle { Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) diff --git a/app/src/settings_view/billing_and_usage/billing_cycle_usage_section.rs b/app/src/settings_view/billing_and_usage/billing_cycle_usage_section.rs index 43a4ebf39e..0ff99edbf0 100644 --- a/app/src/settings_view/billing_and_usage/billing_cycle_usage_section.rs +++ b/app/src/settings_view/billing_and_usage/billing_cycle_usage_section.rs @@ -384,10 +384,14 @@ impl BillingCycleUsageSectionView { .with_main_axis_size(MainAxisSize::Max); row.add_child( - Text::new_inline("Usage", appearance.ui_font_family(), HEADER_FONT_SIZE) - .with_style(Properties::default().weight(Weight::Bold)) - .with_color(theme.active_ui_text_color().into()) - .finish(), + Text::new_inline( + i18n::t("settings.billing.usage_header"), + appearance.ui_font_family(), + HEADER_FONT_SIZE, + ) + .with_style(Properties::default().weight(Weight::Bold)) + .with_color(theme.active_ui_text_color().into()) + .finish(), ); let mut right_side = Flex::row() @@ -449,7 +453,7 @@ impl BillingCycleUsageSectionView { let theme = appearance.theme(); let reset_str = AIRequestUsageModel::as_ref(app) .next_refresh_time_local() - .format("Resets %b %d, %-I:%M %p") + .format(&i18n::t("settings.billing.resets_at")) .to_string(); Some( Text::new_inline( @@ -737,28 +741,28 @@ fn visibility_cta_for( ) -> Option<(&'static str, &'static str, BillingCycleUsageAction, Icon)> { match granularity { UsageVisibilityGranularity::OwnOnly => Some(( - "Upgrade to Build", - "to see team-level credit usage.", + Box::leak(i18n::t("settings.billing.cta_upgrade_build_link").into_boxed_str()), + Box::leak(i18n::t("settings.billing.cta_upgrade_build_copy").into_boxed_str()), BillingCycleUsageAction::OpenUpgrade, Icon::ArrowCircleBrokenUp, )), UsageVisibilityGranularity::TeamAggregate => Some(( - "Upgrade to Business", - "to see per-user credit attribution.", + Box::leak(i18n::t("settings.billing.cta_upgrade_business_link").into_boxed_str()), + Box::leak(i18n::t("settings.billing.cta_upgrade_business_copy").into_boxed_str()), BillingCycleUsageAction::OpenUpgrade, Icon::ArrowCircleBrokenUp, )), UsageVisibilityGranularity::PerUserTotals => Some(( - "Upgrade to Enterprise", - "to see fine-grained credit attribution and set per-user spend limits.", + Box::leak(i18n::t("settings.billing.cta_upgrade_enterprise_link").into_boxed_str()), + Box::leak(i18n::t("settings.billing.cta_upgrade_enterprise_copy").into_boxed_str()), BillingCycleUsageAction::OpenUpgrade, Icon::ArrowCircleBrokenUp, )), // FullBreakdown viewers already have full visibility; nudge them to // the admin panel where per-user spend limits actually get configured. UsageVisibilityGranularity::FullBreakdown => Some(( - "Open the admin panel", - "to set per-user spend limits.", + Box::leak(i18n::t("settings.billing.cta_admin_panel_link").into_boxed_str()), + Box::leak(i18n::t("settings.billing.cta_admin_panel_copy").into_boxed_str()), BillingCycleUsageAction::OpenAdminPanel, Icon::Users, )), @@ -767,11 +771,26 @@ fn visibility_cta_for( fn legend_style_for(cost_type: AiCreditsUsageAndCostType) -> (ColorU, &'static str) { match cost_type { - AiCreditsUsageAndCostType::BaseLimit => (BASE_CREDITS_DOT_COLOR, "Base"), - AiCreditsUsageAndCostType::BonusGrant => (BONUS_CREDITS_DOT_COLOR, "Add-ons"), - AiCreditsUsageAndCostType::Payg => (PAYG_CREDITS_DOT_COLOR, "Pay-as-you-go"), - AiCreditsUsageAndCostType::AmbientBonusGrant => (AMBIENT_CREDITS_DOT_COLOR, "Cloud-only"), - AiCreditsUsageAndCostType::Aggregate => (AGGREGATE_CREDITS_DOT_COLOR, "Combined"), + AiCreditsUsageAndCostType::BaseLimit => ( + BASE_CREDITS_DOT_COLOR, + Box::leak(i18n::t("settings.billing.legend_base").into_boxed_str()), + ), + AiCreditsUsageAndCostType::BonusGrant => ( + BONUS_CREDITS_DOT_COLOR, + Box::leak(i18n::t("settings.billing.legend_addons").into_boxed_str()), + ), + AiCreditsUsageAndCostType::Payg => ( + PAYG_CREDITS_DOT_COLOR, + Box::leak(i18n::t("settings.billing.legend_payg").into_boxed_str()), + ), + AiCreditsUsageAndCostType::AmbientBonusGrant => ( + AMBIENT_CREDITS_DOT_COLOR, + Box::leak(i18n::t("settings.billing.legend_cloud_only").into_boxed_str()), + ), + AiCreditsUsageAndCostType::Aggregate => ( + AGGREGATE_CREDITS_DOT_COLOR, + Box::leak(i18n::t("settings.billing.legend_combined").into_boxed_str()), + ), AiCreditsUsageAndCostType::Other(_) => (BASE_CREDITS_DOT_COLOR, ""), } } @@ -779,8 +798,7 @@ fn legend_style_for(cost_type: AiCreditsUsageAndCostType) -> (ColorU, &'static s fn render_aggregate_legend_tooltip(appearance: &Appearance) -> Box { let theme = appearance.theme(); let text = Text::new_inline( - "Other team members' usage across add-on, pay-as-you-go, and cloud-only credits." - .to_string(), + i18n::t("settings.billing.aggregate_tooltip"), appearance.ui_font_family(), 12., ) diff --git a/app/src/settings_view/billing_and_usage/billing_cycle_usage_team_totals.rs b/app/src/settings_view/billing_and_usage/billing_cycle_usage_team_totals.rs index 6123e39628..47323a9c1d 100644 --- a/app/src/settings_view/billing_and_usage/billing_cycle_usage_team_totals.rs +++ b/app/src/settings_view/billing_and_usage/billing_cycle_usage_team_totals.rs @@ -59,7 +59,7 @@ pub fn build_team_total_card_summaries( ) -> Vec { let (overall_segments, overall_credits, overall_cost) = aggregate_segments(entries.iter()); let mut summaries = vec![TeamTotalCardSummary { - title: "Overall usage", + title: Box::leak(i18n::t("settings.billing.team_totals.overall_usage").into_boxed_str()), card_key: "__card_overall__", segments: overall_segments, total_credits: overall_credits, @@ -83,7 +83,9 @@ pub fn build_team_total_card_summaries( .filter(|e| e.usage_source == AiCreditsUsageSource::Cloud), ); summaries.push(TeamTotalCardSummary { - title: "Local agent usage", + title: Box::leak( + i18n::t("settings.billing.team_totals.local_agent_usage").into_boxed_str(), + ), card_key: "__card_local__", segments: local_segments, total_credits: local_credits, @@ -91,7 +93,9 @@ pub fn build_team_total_card_summaries( limit_cents: None, }); summaries.push(TeamTotalCardSummary { - title: "Cloud agent usage", + title: Box::leak( + i18n::t("settings.billing.team_totals.cloud_agent_usage").into_boxed_str(), + ), card_key: "__card_cloud__", segments: cloud_segments, total_credits: cloud_credits, @@ -221,7 +225,8 @@ fn build_team_total_card( .finish(); let credits_text = Text::new_inline( - format!("({} credits)", format_credits(summary.total_credits)), + i18n::t("settings.billing.team_totals.credits_count") + .replace("{credits}", &format_credits(summary.total_credits)), appearance.ui_font_family(), 13., ) @@ -237,7 +242,8 @@ fn build_team_total_card( let totals_row: Box = match summary.limit_cents { Some(limit) => { let limit_text = Text::new_inline( - format!("Limit: {}", format_cost_cents(limit)), + i18n::t("settings.billing.team_totals.limit") + .replace("{limit}", &format_cost_cents(limit)), appearance.ui_font_family(), 12., ) @@ -352,8 +358,9 @@ pub fn render_team_totals_block( appearance: &Appearance, ) -> Box { let mut column = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch); + let team_label = i18n::t("settings.billing.team_totals.team"); column.add_child( - Container::new(render_section_subheader("Team", appearance)) + Container::new(render_section_subheader(&team_label, appearance)) .with_margin_bottom(8.) .finish(), ); diff --git a/app/src/settings_view/billing_and_usage/overage_limit_modal.rs b/app/src/settings_view/billing_and_usage/overage_limit_modal.rs index dbe4d7bc4d..479e618a19 100644 --- a/app/src/settings_view/billing_and_usage/overage_limit_modal.rs +++ b/app/src/settings_view/billing_and_usage/overage_limit_modal.rs @@ -151,11 +151,11 @@ impl SpendingLimitModal { fn error_text(&self) -> Option { match self.input_error_state { - Some(SpendingLimitModalInputErrorState::InvalidNumberFormat) => { - Some("Please enter a valid currency amount".to_string()) - } + Some(SpendingLimitModalInputErrorState::InvalidNumberFormat) => Some(i18n::t( + "settings.billing.overage_modal.error_invalid_amount", + )), Some(SpendingLimitModalInputErrorState::NumberOutOfRange) => { - Some("Please enter a price between $0.01 and $10,000,000".to_string()) + Some(i18n::t("settings.billing.overage_modal.error_out_of_range")) } None => None, } @@ -195,7 +195,7 @@ impl View for SpendingLimitModal { let theme = appearance.theme(); let description_text = Text::new( - "Warp will prevent use of premium models when this dollar limit is reached. Resets on a monthly basis.", + i18n::t("settings.billing.overage_modal.description"), appearance.ui_font_family(), 14., ) @@ -203,7 +203,7 @@ impl View for SpendingLimitModal { .finish(); let additional_note_text = Text::new( - "Note that AI credits made near your chosen limit may exceed it by a few dollars.", + i18n::t("settings.billing.overage_modal.additional_note"), appearance.ui_font_family(), 12., ) @@ -263,7 +263,7 @@ impl View for SpendingLimitModal { ButtonVariant::Accent, self.update_button_mouse_state.clone(), ) - .with_text_label("Update".to_string()) + .with_text_label(i18n::t("settings.billing.overage_modal.update_button")) .with_style(button_style); if self.input_error_state.is_some() { @@ -278,7 +278,7 @@ impl View for SpendingLimitModal { ButtonVariant::Secondary, self.cancel_button_mouse_state.clone(), ) - .with_text_label("Cancel".to_string()) + .with_text_label(i18n::t("settings.billing.overage_modal.cancel_button")) .with_style(button_style) .build() .on_click(|ctx, _, _| { diff --git a/app/src/settings_view/billing_and_usage_dispatch.rs b/app/src/settings_view/billing_and_usage_dispatch.rs index d06f1f2aa8..a1c9b6b49e 100644 --- a/app/src/settings_view/billing_and_usage_dispatch.rs +++ b/app/src/settings_view/billing_and_usage_dispatch.rs @@ -47,7 +47,13 @@ impl BillingAndUsageDispatchView { ctx.notify(); }); - let page = PageType::new_monolith(BillingAndUsageWidget, Some("Billing and Usage"), true); + let page = PageType::new_monolith( + BillingAndUsageWidget, + Some(Box::leak( + i18n::t("settings.nav.billing_and_usage").into_boxed_str(), + )), + true, + ); Self { page, v1, v2 } } diff --git a/app/src/settings_view/billing_and_usage_page.rs b/app/src/settings_view/billing_and_usage_page.rs index 39b8cda394..e5c2779881 100644 --- a/app/src/settings_view/billing_and_usage_page.rs +++ b/app/src/settings_view/billing_and_usage_page.rs @@ -66,43 +66,6 @@ use crate::workspaces::workspace::{CustomerType, Workspace}; use crate::{send_telemetry_from_ctx, WorkspaceAction}; const HEADER_FONT_SIZE: f32 = 16.; -const OVERAGE_USAGE_LINK_TEXT: &str = "View details on overage usage"; -const OVERAGE_TOGGLE_ADMIN_HEADER: &str = "Enable premium model usage overages"; -const OVERAGE_TOGGLE_USER_HEADER_ENABLED: &str = "Premium model usage overages are enabled"; -const OVERAGE_TOGGLE_USER_HEADER_DISABLED: &str = "Premium model usage overages are not enabled"; -const OVERAGE_TOGGLE_DESCRIPTION: &str = "Continue using premium models beyond your plan's limits. Usage is charged in $20 increments up to your spending limit, with any remaining balance charged on your scheduled billing date."; -const OVERAGE_TOGGLE_USER_DESCRIPTION: &str = - "Ask a team admin to enable overages for more AI usage."; - -const SORT_MENU_ITEM_DISPLAY_NAME_A_Z_LABEL: &str = "A to Z"; -const SORT_MENU_ITEM_DISPLAY_NAME_Z_A_LABEL: &str = "Z to A"; -const SORT_MENU_ITEM_REQUEST_USAGE_ASCENDING_LABEL: &str = "Usage ascending"; -const SORT_MENU_ITEM_REQUEST_USAGE_DESCENDING_LABEL: &str = "Usage descending"; - -const AUTO_RELOAD_EXCEED_LIMIT_WARNING_STRING: &str = - "Auto reload is disabled, as the next reload would exceed your monthly spend limit. Increase your limit to use auto reload."; -const AUTO_RELOAD_DELINQUENT_WARNING_STRING: &str = - "Restricted due to billing issue. Update your payment method to purchase add-on credits."; -const RESTRICTED_BILLING_USAGE_WARNING_STRING: &str = - "Auto reload is disabled due to recent failed reload. Please update your payment method and try again."; - -const OVERVIEW_TAB_TEXT: &str = "Overview"; -const USAGE_HISTORY_TAB_TEXT: &str = "Usage History"; - -const ENTERPRISE_USAGE_CALLOUT_HEADER: &str = "Usage reporting is currently limited"; -const ENTERPRISE_USAGE_CALLOUT_BODY_ADMIN_PREFIX: &str = - "Enterprise credit usage isn't fully available in this view yet. For the most accurate spend tracking, "; -const ENTERPRISE_USAGE_CALLOUT_BODY_ADMIN_LINK: &str = "visit the admin panel"; -const ENTERPRISE_USAGE_CALLOUT_BODY_ADMIN_SUFFIX: &str = "."; -const ENTERPRISE_USAGE_CALLOUT_BODY_NON_ADMIN: &str = - "Enterprise credit usage isn't fully available in this view yet. Contact a team admin for detailed usage reporting."; - -const ADDON_CREDITS_DESCRIPTION: &str = "Add-on credits are purchased in prepaid packages that roll over each billing cycle and expire after one year. The more you purchase, the better the per-credit rate. Once your base plan credits are used, add-on credits will be consumed."; -const ADDITIONAL_ADDON_CREDITS_DESCRIPTION_FOR_TEAM: &str = - "Purchased add-on credits are shared across your team."; - -// Cloud agent trial widget constants. -const AMBIENT_AGENT_TRIAL_TITLE: &str = "Cloud agent trial"; /// The threshold below which we only show the "Buy more" button (not "New agent"). use crate::ai::request_usage_model::AMBIENT_AGENT_TRIAL_CREDIT_THRESHOLD; @@ -115,9 +78,13 @@ pub fn create_discount_badge(discount: u32, appearance: &Appearance) -> Box Self { - match label { - OVERVIEW_TAB_TEXT => BillingUsageTab::Overview, - USAGE_HISTORY_TAB_TEXT => BillingUsageTab::UsageHistory, - _ => BillingUsageTab::Overview, + if label == i18n::t("settings.billing.overview_tab") { + BillingUsageTab::Overview + } else if label == i18n::t("settings.billing.usage_history_tab") { + BillingUsageTab::UsageHistory + } else { + BillingUsageTab::Overview } } - pub fn label(&self) -> &str { + pub fn label(&self) -> String { match self { - BillingUsageTab::Overview => OVERVIEW_TAB_TEXT, - BillingUsageTab::UsageHistory => USAGE_HISTORY_TAB_TEXT, + BillingUsageTab::Overview => i18n::t("settings.billing.overview_tab"), + BillingUsageTab::UsageHistory => i18n::t("settings.billing.usage_history_tab"), } } } @@ -288,7 +257,7 @@ impl BillingAndUsagePageView { let overage_limit_modal_view = ctx.add_typed_action_view(|ctx| { Modal::new( - Some("Overage spending limit".to_string()), + Some(i18n::t("settings.billing.overage_modal.title")), overage_limit_modal, ctx, ) @@ -312,7 +281,7 @@ impl BillingAndUsagePageView { let addon_credit_modal_view = ctx.add_typed_action_view(|ctx| { Modal::new( - Some("Monthly spending limit".to_string()), + Some(i18n::t("settings.billing.spending_limit_modal.title")), addon_credit_modal, ctx, ) @@ -340,7 +309,11 @@ impl BillingAndUsagePageView { }); let load_more_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Load more", SecondaryTheme).on_click(|ctx| { + ActionButton::new( + i18n::t("settings.billing.usage_history.load_more"), + SecondaryTheme, + ) + .on_click(|ctx| { ctx.dispatch_typed_action(BillingAndUsagePageAction::RenderMoreUsageEntries); }) }); @@ -445,7 +418,7 @@ impl BillingAndUsagePageView { } UserWorkspacesEvent::UpdateWorkspaceSettingsRejected(_err) => { self.show_toast( - "Failed to update workspace settings", + &i18n::t("settings.billing.toast.update_settings_failed"), ToastFlavor::Error, ctx, ); @@ -458,7 +431,7 @@ impl BillingAndUsagePageView { UserWorkspacesEvent::PurchaseAddonCreditsSuccess => { self.purchase_addon_credits_loading = false; self.show_toast( - "Successfully purchased add-on credits", + &i18n::t("settings.billing.toast.purchase_success"), ToastFlavor::Success, ctx, ); @@ -828,42 +801,39 @@ impl TypedActionView for BillingAndUsagePageView { return; } // Build four menu items with checkmark for selected state - let sort_options = [ + let sort_options = vec![ ( - SORT_MENU_ITEM_DISPLAY_NAME_A_Z_LABEL, + i18n::t("settings.billing.sort.display_name_a_z"), SortKey::DisplayName, SortOrder::Asc, ), ( - SORT_MENU_ITEM_DISPLAY_NAME_Z_A_LABEL, + i18n::t("settings.billing.sort.display_name_z_a"), SortKey::DisplayName, SortOrder::Desc, ), ( - SORT_MENU_ITEM_REQUEST_USAGE_ASCENDING_LABEL, + i18n::t("settings.billing.sort.usage_ascending"), SortKey::Requests, SortOrder::Asc, ), ( - SORT_MENU_ITEM_REQUEST_USAGE_DESCENDING_LABEL, + i18n::t("settings.billing.sort.usage_descending"), SortKey::Requests, SortOrder::Desc, ), ]; let items: Vec> = sort_options - .iter() + .into_iter() .map(|(label, key, order)| { let is_selected = matches!( (self.current_sort_key, self.current_sort_order), - (Some(k), o) if k == *key && o == *order + (Some(k), o) if k == key && o == order ); - let mut menu_item = MenuItemFields::new(*label).with_on_select_action( - BillingAndUsagePageAction::ChangeUsageSort { - key: *key, - order: *order, - }, + let mut menu_item = MenuItemFields::new(label).with_on_select_action( + BillingAndUsagePageAction::ChangeUsageSort { key, order }, ); menu_item = if is_selected { @@ -1109,17 +1079,22 @@ impl BillingAndUsagePageView { let fg = theme.foreground().into_solid(); let bg = theme.background().into_solid(); - let title = Text::new_inline(AMBIENT_AGENT_TRIAL_TITLE, appearance.ui_font_family(), 14.) - .with_color(theme.active_ui_text_color().into()) - .with_style(Properties::default().weight(Weight::Semibold)) - .finish(); + let title = Text::new_inline( + i18n::t("settings.billing.ambient_trial.title"), + appearance.ui_font_family(), + 14., + ) + .with_color(theme.active_ui_text_color().into()) + .with_style(Properties::default().weight(Weight::Semibold)) + .finish(); let credits_text = if credits_remaining == 1 { - "1 credit remaining".to_string() + i18n::t("settings.billing.ambient_trial.credits_remaining_one") } else { format!( - "{} credits remaining", - credits_remaining.separate_with_commas() + "{} {}", + credits_remaining.separate_with_commas(), + i18n::t("settings.billing.ambient_trial.credits_remaining_suffix") ) }; let credits_label = Text::new_inline(credits_text, appearance.ui_font_family(), 12.) @@ -1141,7 +1116,7 @@ impl BillingAndUsagePageView { ButtonVariant::Secondary, self.ambient_trial_new_agent_button.clone(), ) - .with_text_label("New agent".to_string()) + .with_text_label(i18n::t("agent_management.new_agent")) .with_style(UiComponentStyles { font_color: Some(bg), background: Some(fg.into()), @@ -1178,7 +1153,7 @@ impl BillingAndUsagePageView { ButtonVariant::Secondary, self.ambient_trial_buy_more_button.clone(), ) - .with_text_label("Buy more".to_string()) + .with_text_label(i18n::t("settings.billing.buy_more")) .with_style(UiComponentStyles { background: Some(bg.into()), font_size: Some(14.), @@ -1264,16 +1239,19 @@ impl BillingAndUsagePageView { let enabled_and_not_delinquent = enabled && !is_delinquent; let (header_text, description_text) = if has_admin_permissions { - (OVERAGE_TOGGLE_ADMIN_HEADER, OVERAGE_TOGGLE_DESCRIPTION) + ( + i18n::t("settings.billing.overage.toggle_admin_header"), + i18n::t("settings.billing.overage.toggle_description"), + ) } else if enabled { ( - OVERAGE_TOGGLE_USER_HEADER_ENABLED, - OVERAGE_TOGGLE_DESCRIPTION, + i18n::t("settings.billing.overage.toggle_user_header_enabled"), + i18n::t("settings.billing.overage.toggle_description"), ) } else { ( - OVERAGE_TOGGLE_USER_HEADER_DISABLED, - OVERAGE_TOGGLE_USER_DESCRIPTION, + i18n::t("settings.billing.overage.toggle_user_header_disabled"), + i18n::t("settings.billing.overage.toggle_user_description"), ) }; @@ -1370,7 +1348,7 @@ impl BillingAndUsagePageView { let spend_limit_text = if let Some(cents) = usage_settings.max_monthly_spend_cents { format!("${:.2}", cents as f64 / 100.0) } else { - "Not set".to_string() + i18n::t("settings.billing.not_set") }; let info_icon = render_info_icon( @@ -1379,14 +1357,14 @@ impl BillingAndUsagePageView { mouse_state: self.ubp_info_icon_mouse_state.clone(), on_click_action: None, secondary_text: None, - tooltip_override_text: Some( - "Sets the monthly overage spending limit beyond the plan amount".to_string(), - ), + tooltip_override_text: Some(i18n::t( + "settings.billing.monthly_overage_spending_limit_tooltip", + )), }, ); let label = Text::new_inline( - "Monthly overage spending limit", + i18n::t("settings.billing.monthly_overage_spending_limit"), appearance.ui_font_family(), 12., ) @@ -1455,7 +1433,7 @@ impl BillingAndUsagePageView { appearance .ui_builder() .link( - OVERAGE_USAGE_LINK_TEXT.to_string(), + i18n::t("settings.billing.overage.usage_link"), None, Some(Box::new(move |ctx| { ctx.dispatch_typed_action( @@ -1615,10 +1593,14 @@ impl BillingAndUsagePageView { let ui_builder = appearance.ui_builder(); let theme = appearance.theme(); - let header = Text::new_inline("Add-on credits", appearance.ui_font_family(), 16.) - .with_color(fg.into()) - .with_style(Properties::default().weight(Weight::Bold)) - .finish(); + let header = Text::new_inline( + i18n::t("settings.billing.add_on_credits"), + appearance.ui_font_family(), + 16., + ) + .with_color(fg.into()) + .with_style(Properties::default().weight(Weight::Bold)) + .finish(); let credits_value = Text::new_inline( bonus_credit_balance.separate_with_commas(), @@ -1671,9 +1653,15 @@ impl BillingAndUsagePageView { .current_team() .is_some_and(|team| team.billing_metadata.is_on_legacy_paid_plan()); let (link_text, suffix) = if is_legacy_paid { - ("Switch to the Build plan", " to purchase add-on credits.") + ( + i18n::t("settings.billing.switch_to_build_plan"), + i18n::t("settings.billing.addon.upgrade_to_build_suffix"), + ) } else { - ("Upgrade to the Build plan", " to purchase add-on credits.") + ( + i18n::t("settings.billing.upgrade_to_build_plan"), + i18n::t("settings.billing.addon.upgrade_to_build_suffix"), + ) }; let text_fragments = vec![ @@ -1713,7 +1701,7 @@ impl BillingAndUsagePageView { // they're on an Enterprise-like plan. For admins, we show them a message to contact their // Account Executive. (false, false, true) => { - let paragraph_text = "Contact your Account Executive for more add-on credits."; + let paragraph_text = i18n::t("settings.billing.addon.contact_account_executive"); Some( ui_builder .paragraph(paragraph_text) @@ -1728,7 +1716,7 @@ impl BillingAndUsagePageView { // Every other case relates to not being a team admin. If you aren't an admin, we show // a generic message telling you to talk to them. (_, _, false) => { - let paragraph_text = "Contact a team admin to purchase add-on credits."; + let paragraph_text = i18n::t("settings.billing.addon.contact_team_admin_purchase"); Some( ui_builder .paragraph(paragraph_text) @@ -1765,9 +1753,13 @@ impl BillingAndUsagePageView { .unwrap_or(1); let paragraph_text = if team_member_count > 1 { - format!("{ADDON_CREDITS_DESCRIPTION} {ADDITIONAL_ADDON_CREDITS_DESCRIPTION_FOR_TEAM}") + format!( + "{} {}", + i18n::t("settings.billing.addon.description"), + i18n::t("settings.billing.addon.description_team_shared_suffix") + ) } else { - ADDON_CREDITS_DESCRIPTION.to_string() + i18n::t("settings.billing.addon.description") }; let paragraph = ui_builder .paragraph(paragraph_text) @@ -1784,9 +1776,9 @@ impl BillingAndUsagePageView { mouse_state: self.addon_info_icon_mouse_state.clone(), on_click_action: None, secondary_text: None, - tooltip_override_text: Some( - "Sets the monthly limit spent on add-on credits".to_string(), - ), + tooltip_override_text: Some(i18n::t( + "settings.billing.addon.monthly_spend_limit_tooltip", + )), }, ); @@ -1800,7 +1792,10 @@ impl BillingAndUsagePageView { let monthly_spend_row = Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_children([ - ui_builder.span("Monthly spend limit").build().finish(), + ui_builder + .span(i18n::t("settings.billing.addon.monthly_spend_limit_label")) + .build() + .finish(), Shrinkable::new(1., Align::new(info_icon).left().finish()).finish(), icon_button( appearance, @@ -1829,15 +1824,22 @@ impl BillingAndUsagePageView { let cost_cents = bonus_grants.cents_spent; let cost_dollars = cost_cents as f64 / 100.0; - let label = - Text::new_inline("Purchased this month", appearance.ui_font_family(), 12.) - .with_color(appearance.theme().active_ui_text_color().into()) - .finish(); + let label = Text::new_inline( + i18n::t("settings.billing.purchased_this_month"), + appearance.ui_font_family(), + 12., + ) + .with_color(appearance.theme().active_ui_text_color().into()) + .finish(); let credits_text = if credits_purchased == 1 { - "1 credit".to_string() + i18n::t("settings.billing.addon.credits_count_one") } else { - format!("{} credits", credits_purchased.separate_with_commas()) + format!( + "{} {}", + credits_purchased.separate_with_commas(), + i18n::t("settings.billing.addon.credits_unit") + ) }; let credits_component = Container::new( @@ -1894,9 +1896,15 @@ impl BillingAndUsagePageView { .auto_reload_enabled; let auto_reload_amount = selected_option - .map(|option| option.credits.to_string()) + .map(|option| { + format!( + "{} {}", + option.credits, + i18n::t("settings.billing.addon.credits_unit") + ) + }) .filter(|_| auto_reload_enabled) - .unwrap_or("your selected".to_string()); + .unwrap_or_else(|| i18n::t("settings.billing.addon.selected_credit_amount_fallback")); let auto_reload_switch = ui_builder .switch(self.auto_reload_switch.clone()) .check(auto_reload_enabled); @@ -1915,16 +1923,16 @@ impl BillingAndUsagePageView { }; let auto_reload_switch = Container::new(render_body_item::( - "Auto reload".into(), + i18n::t("settings.billing.addon.auto_reload_label"), None, Default::default(), Default::default(), appearance, auto_reload_switch, - Some(format!( - "When enabled, auto reload will automatically purchase {auto_reload_amount} \ - credits when your add-on credit balance reaches 100 credits remaining." - )), + Some( + i18n::t("settings.billing.addon.auto_reload_tooltip") + .replace("{amount}", &auto_reload_amount), + ), )) .with_padding_right(-TOGGLE_BUTTON_RIGHT_PADDING) .finish(); @@ -1983,9 +1991,9 @@ impl BillingAndUsagePageView { }; let button_text = if purchase_addon_credits_loading { - "Buying…".to_string() + i18n::t("settings.billing.addon.purchase_button_loading") } else { - "Buy".to_string() + i18n::t("settings.billing.addon.purchase_button") }; let would_exceed_limit = selected_option.is_some_and(|option| { @@ -2052,12 +2060,12 @@ impl BillingAndUsagePageView { if delinquent_due_to_payment_issue { card_content_upper.add_child(self.render_warning_row( appearance, - AUTO_RELOAD_DELINQUENT_WARNING_STRING.to_string(), + i18n::t("settings.billing.addon.warning_delinquent_admin"), )); } else if would_exceed_limit { card_content_upper.add_child(self.render_warning_row( appearance, - AUTO_RELOAD_EXCEED_LIMIT_WARNING_STRING.to_string(), + i18n::t("settings.billing.addon.warning_auto_reload_paused_admin"), )); } let card_upper = Container::new(card_content_upper.finish()) @@ -2076,14 +2084,17 @@ impl BillingAndUsagePageView { .finish(); let mut card_content_lower_children = vec![ - ui_builder.span("One-time purchase").build().finish(), + ui_builder + .span(i18n::t("settings.billing.addon.purchase_button")) + .build() + .finish(), buy_row.finish(), ]; if delinquent_due_to_payment_issue { card_content_lower_children.push(self.render_warning_row( appearance, - AUTO_RELOAD_DELINQUENT_WARNING_STRING.to_string(), + i18n::t("settings.billing.addon.warning_delinquent_admin"), )); } else if workspace .billing_metadata @@ -2091,18 +2102,18 @@ impl BillingAndUsagePageView { { card_content_lower_children.push(self.render_warning_row( appearance, - RESTRICTED_BILLING_USAGE_WARNING_STRING.to_string(), + i18n::t("settings.billing.addon.warning_failed_reload_admin"), )); } else if would_exceed_limit { let warning_fragments = vec![ - FormattedTextFragment::plain_text( - "Reloading would exceed your monthly limit. ", - ), + FormattedTextFragment::plain_text(i18n::t( + "settings.billing.reload_would_exceed_limit", + )), FormattedTextFragment::hyperlink_action( - "Increase your limit", + i18n::t("settings.billing.increase_your_limit"), BillingAndUsagePageAction::ShowAddOnCreditModal, ), - FormattedTextFragment::plain_text(" to continue."), + FormattedTextFragment::plain_text(i18n::t("settings.billing.to_continue")), ]; card_content_lower_children .push(self.render_warning_row_with_link(appearance, warning_fragments)); @@ -2159,24 +2170,35 @@ impl BillingAndUsagePageView { if let (Some(count), Some(cost)) = (total_overages_count, total_overages_cost) { if count == 1 { ( - "1 credit".to_string(), + i18n::t("settings.billing.addon.credits_count_one"), format!("${:.2}", cost as f64 / 100.0), ) } else { ( - format!("{} credits", count.separate_with_commas()), + format!( + "{} {}", + count.separate_with_commas(), + i18n::t("settings.billing.addon.credits_unit") + ), format!("${:.2}", cost as f64 / 100.0), ) } } else { - ("0 credits".to_string(), "$0.00".to_string()) + ( + format!("0 {}", i18n::t("settings.billing.addon.credits_unit")), + "$0.00".to_string(), + ) }; let mut left_side_component = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Center); - let label = Text::new_inline("Total overages", appearance.ui_font_family(), 12.) - .with_color(appearance.theme().active_ui_text_color().into()) - .finish(); + let label = Text::new_inline( + i18n::t("settings.billing.total_overages"), + appearance.ui_font_family(), + 12., + ) + .with_color(appearance.theme().active_ui_text_color().into()) + .finish(); left_side_component.add_child(Container::new(label).with_margin_right(8.).finish()); @@ -2201,7 +2223,8 @@ impl BillingAndUsagePageView { if let Some(period_end) = total_overages_period_end { let local_period_end = period_end.with_timezone(&Local); let formatted_date = local_period_end.format("%b %d at %-I:%M %p").to_string(); - let billing_date_text = format!("Usage resets on {formatted_date}"); + let billing_date_text = + i18n::t("settings.billing.usage_resets_on").replace("{date}", &formatted_date); left_side_component.add_child( Container::new( Text::new_inline(billing_date_text, appearance.ui_font_family(), 12.) @@ -2250,17 +2273,17 @@ impl BillingAndUsagePageView { if let Some(info) = prorated_request_limits_info { if info.is_request_limit_prorated { row.add_child(render_info_icon( - appearance, - AdditionalInfo:: { - mouse_state: info.mouse_state, - on_click_action: None, - secondary_text: None, - tooltip_override_text: match info.is_current_user { - true => Some("Your credit limit is prorated because you joined midway through the billing cycle.".to_string()), - false => Some("This credit limit is prorated because this user joined midway through the billing cycle.".to_string()), + appearance, + AdditionalInfo:: { + mouse_state: info.mouse_state, + on_click_action: None, + secondary_text: None, + tooltip_override_text: match info.is_current_user { + true => Some(i18n::t("settings.billing.prorated_limit_current_user")), + false => Some(i18n::t("settings.billing.prorated_limit_user")), + }, }, - }, - )) + )) } } @@ -2278,11 +2301,15 @@ impl BillingAndUsagePageView { } let request_count_label = if workspace_is_delinquent_due_to_payment_issue { - "Restricted due to billing issue".to_string() + i18n::t("settings.ai.usage.restricted_billing") } else { match divisor { Some(Divisor::Unlimited) => { - format!("{}/Unlimited", used.separate_with_commas()) + format!( + "{}/{}", + used.separate_with_commas(), + i18n::t("settings.ai.usage.unlimited") + ) } Some(Divisor::Limit(limit)) => format!( "{}/{}", @@ -2365,9 +2392,9 @@ impl BillingAndUsagePageView { ) .finish() } else { - let header = "Credits"; - let description = - format!("This is the {refresh_duration} limit of AI credits for your account."); + let header = i18n::t("settings.ai.usage.credits"); + let description = i18n::t("settings.ai.usage.credits_limit_desc") + .replace("{period}", &refresh_duration); let request_usage_description = FormattedTextElement::from_str( description, @@ -2457,7 +2484,7 @@ impl BillingAndUsagePageView { let tab_selector = tab_selector::render_tab_selector( tabs, - self.selected_tab.label(), + &self.selected_tab.label(), // On click, set clicked tab as selected |label, ctx| { ctx.dispatch_typed_action(BillingAndUsagePageAction::SelectTab( @@ -2508,12 +2535,16 @@ impl BillingAndUsagePageView { .with_main_axis_alignment(MainAxisAlignment::Center) .with_child( Container::new( - Text::new_inline("Last 30 days".to_string(), appearance.ui_font_family(), 14.) - .with_color(blended_colors::text_sub( - appearance.theme(), - appearance.theme().surface_1(), - )) - .finish(), + Text::new_inline( + i18n::t("settings.billing.last_30_days"), + appearance.ui_font_family(), + 14., + ) + .with_color(blended_colors::text_sub( + appearance.theme(), + appearance.theme().surface_1(), + )) + .finish(), ) .with_vertical_margin(12.) .finish(), @@ -2621,19 +2652,23 @@ impl BillingAndUsagePageView { ) .with_child( Container::new( - Text::new("No usage history", appearance.ui_font_family(), 14.) - .with_color(blended_colors::text_sub( - appearance.theme(), - appearance.theme().surface_1(), - )) - .finish(), + Text::new( + i18n::t("settings.billing.usage_history.empty_title"), + appearance.ui_font_family(), + 14., + ) + .with_color(blended_colors::text_sub( + appearance.theme(), + appearance.theme().surface_1(), + )) + .finish(), ) .with_margin_bottom(4.) .finish(), ) .with_child( Text::new( - "Kick off an agent task to view usage history here.", + i18n::t("settings.billing.usage_history_empty"), appearance.ui_font_family(), 14., ) @@ -2680,7 +2715,7 @@ impl BillingAndUsagePageView { .finish(); let header = Text::new_inline( - ENTERPRISE_USAGE_CALLOUT_HEADER, + i18n::t("settings.billing.enterprise_usage_callout.header"), appearance.ui_font_family(), 16., ) @@ -2698,12 +2733,14 @@ impl BillingAndUsagePageView { let body = if has_admin_permissions { let admin_panel_url = AdminActions::admin_panel_link_for_team(team_uid); let text_fragments = vec![ - FormattedTextFragment::plain_text(ENTERPRISE_USAGE_CALLOUT_BODY_ADMIN_PREFIX), + FormattedTextFragment::plain_text(i18n::t( + "settings.billing.enterprise_usage_callout.admin_prefix", + )), FormattedTextFragment::hyperlink( - ENTERPRISE_USAGE_CALLOUT_BODY_ADMIN_LINK, + i18n::t("settings.billing.enterprise_usage_callout.admin_link"), admin_panel_url, ), - FormattedTextFragment::plain_text(ENTERPRISE_USAGE_CALLOUT_BODY_ADMIN_SUFFIX), + FormattedTextFragment::plain_text("."), ]; FormattedTextElement::new( FormattedText::new([FormattedTextLine::Line(text_fragments)]), @@ -2721,7 +2758,9 @@ impl BillingAndUsagePageView { } else { appearance .ui_builder() - .paragraph(ENTERPRISE_USAGE_CALLOUT_BODY_NON_ADMIN) + .paragraph(i18n::t( + "settings.billing.enterprise_usage_callout.non_admin", + )) .with_style(UiComponentStyles { font_color: Some(theme.sub_text_color(bg).into()), font_size: Some(12.), @@ -2778,7 +2817,10 @@ impl BillingAndUsagePageView { .with_child( appearance .ui_builder() - .paragraph(format!("Resets {formatted_next_refresh_time}")) + .paragraph( + i18n::t("settings.ai.usage.resets") + .replace("{date}", formatted_next_refresh_time), + ) .with_style(UiComponentStyles { font_color: Some(blended_colors::text_sub( appearance.theme(), @@ -2828,8 +2870,9 @@ impl BillingAndUsagePageView { let hoverable = Hoverable::new(self.sort_icon_mouse_state.clone(), |mouse_state| { if mouse_state.is_hovered() { - let tooltip = - appearance.ui_builder().tool_tip("Sort by".to_string()); + let tooltip = appearance + .ui_builder() + .tool_tip(i18n::t("settings.billing.sort_by")); button.add_positioned_overlay_child( tooltip.build().finish(), @@ -2892,7 +2935,7 @@ impl BillingAndUsagePageView { .with_child( build_sub_header( appearance, - "Usage", + i18n::t("settings.billing.usage_header"), Some( appearance .theme() @@ -2944,7 +2987,7 @@ impl BillingAndUsagePageView { }; usage.add_child(self.render_ai_usage_limit_row( - "Team total".to_string(), + i18n::t("settings.billing.team_totals.team_total"), team_total_used, team_divisor, ai_request_usage_model.refresh_duration_to_string(), @@ -3071,18 +3114,20 @@ impl BillingAndUsagePageView { if has_admin_permissions { vec![ FormattedTextFragment::hyperlink_action( - "Manage billing", + i18n::t("settings.billing.manage_billing"), BillingAndUsagePageAction::GenerateStripeBillingPortalLink { team_uid: team.uid, }, ), - FormattedTextFragment::plain_text(" to regain access to AI features."), + FormattedTextFragment::plain_text(i18n::t( + "settings.billing.regain_access_suffix", + )), ] } else { // Non-admin team member - show message to contact admin - vec![FormattedTextFragment::plain_text( - "Contact your team admin to resolve billing issues.", - )] + vec![FormattedTextFragment::plain_text(i18n::t( + "settings.billing.contact_admin_resolve_issues", + ))] } } else if team.billing_metadata.can_upgrade_to_higher_tier_plan() { let upgrade_url = UserWorkspaces::upgrade_link_for_team(team.uid); @@ -3091,39 +3136,44 @@ impl BillingAndUsagePageView { if team.billing_metadata.is_on_legacy_paid_plan() { vec![ FormattedTextFragment::hyperlink( - "Switch to the Build plan", + i18n::t("settings.billing.switch_to_build_plan"), upgrade_url, ), - FormattedTextFragment::plain_text( - " for a more flexible pricing model.", - ), + FormattedTextFragment::plain_text(i18n::t( + "settings.billing.flexible_pricing_suffix", + )), ] } else { let mut fragments = vec![FormattedTextFragment::hyperlink( - "Upgrade to the Build plan", + i18n::t("settings.billing.upgrade_to_build_plan"), upgrade_url, )]; if team.billing_metadata.is_byo_api_key_enabled() { - fragments.push(FormattedTextFragment::plain_text(" or ")); + fragments + .push(FormattedTextFragment::plain_text(i18n::t("common.or"))); fragments.push(FormattedTextFragment::hyperlink_action( - "bring your own key", + i18n::t("settings.billing.bring_your_own_key"), BillingAndUsagePageAction::NavigateToByokSettings, )); } - fragments.push(FormattedTextFragment::plain_text( - " for increased access to AI features.", - )); + fragments.push(FormattedTextFragment::plain_text(i18n::t( + "settings.billing.increased_ai_access_suffix", + ))); fragments } } else { let upgrade_text = match team.billing_metadata.customer_type { - CustomerType::Prosumer => "Upgrade to Turbo plan", - CustomerType::Turbo => "Upgrade to Lightspeed plan", - _ => "Upgrade", + CustomerType::Prosumer => i18n::t("settings.account.upgrade_to_turbo"), + CustomerType::Turbo => { + i18n::t("settings.account.upgrade_to_lightspeed") + } + _ => i18n::t("common.upgrade"), }; vec![ FormattedTextFragment::hyperlink(upgrade_text, upgrade_url), - FormattedTextFragment::plain_text(" to get more AI usage."), + FormattedTextFragment::plain_text(i18n::t( + "settings.billing.more_ai_usage_suffix", + )), ] } } else { @@ -3132,35 +3182,44 @@ impl BillingAndUsagePageView { } else if team.billing_metadata.is_on_build_plan() { vec![ FormattedTextFragment::hyperlink( - "Upgrade to Max", + i18n::t("settings.billing.upgrade_to_max"), UserWorkspaces::upgrade_link_for_team(team.uid), ), - FormattedTextFragment::plain_text(" for more AI credits."), + FormattedTextFragment::plain_text(i18n::t( + "settings.billing.more_ai_credits_suffix", + )), ] } else if team.billing_metadata.is_on_build_max_plan() { vec![ FormattedTextFragment::hyperlink( - "Switch to Business", + i18n::t("settings.billing.switch_to_business"), UserWorkspaces::upgrade_link_for_team(team.uid), ), - FormattedTextFragment::plain_text( - " for security features like SSO and automatically applied zero data retention.", - ), + FormattedTextFragment::plain_text(i18n::t( + "settings.billing.business_security_suffix", + )), ] } else if team.billing_metadata.is_on_build_business_plan() || team.billing_metadata.is_on_legacy_business_plan() { vec![ FormattedTextFragment::hyperlink( - "Upgrade to Enterprise", + i18n::t("settings.billing.upgrade_to_enterprise"), "mailto:sales@warp.dev", ), - FormattedTextFragment::plain_text(" for custom limits and dedicated support."), + FormattedTextFragment::plain_text(i18n::t( + "settings.billing.enterprise_support_suffix", + )), ] } else if !team.billing_metadata.is_usage_based_pricing_toggleable() { vec![ - FormattedTextFragment::hyperlink("Contact support", "mailto:support@warp.dev"), - FormattedTextFragment::plain_text(" for more AI usage."), + FormattedTextFragment::hyperlink( + i18n::t("settings.billing.contact_support"), + "mailto:support@warp.dev", + ), + FormattedTextFragment::plain_text(i18n::t( + "settings.billing.more_ai_usage_suffix", + )), ] } else { vec![] @@ -3169,19 +3228,19 @@ impl BillingAndUsagePageView { let user_id = auth_state.user_id().unwrap_or_default(); let upgrade_url = UserWorkspaces::upgrade_link(user_id); let mut fragments = vec![FormattedTextFragment::hyperlink( - "Upgrade to the Build plan", + i18n::t("settings.billing.upgrade_to_build_plan"), upgrade_url, )]; if UserWorkspaces::as_ref(app).is_byo_api_key_enabled(app) { - fragments.push(FormattedTextFragment::plain_text(" or ")); + fragments.push(FormattedTextFragment::plain_text(i18n::t("common.or"))); fragments.push(FormattedTextFragment::hyperlink_action( - "bring your own key", + i18n::t("settings.billing.bring_your_own_key"), BillingAndUsagePageAction::NavigateToByokSettings, )); } - fragments.push(FormattedTextFragment::plain_text( - " for more credits and access to more models.", - )); + fragments.push(FormattedTextFragment::plain_text(i18n::t( + "settings.billing.more_credits_and_models_suffix", + ))); fragments }; @@ -3342,7 +3401,7 @@ impl BillingAndUsagePageView { self.anonymous_user_sign_up_button.clone(), ) .with_style(button_styles) - .with_text_label("Sign up".to_owned()) + .with_text_label(i18n::t("common.sign_up")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(BillingAndUsagePageAction::SignupAnonymousUser); @@ -3354,7 +3413,10 @@ impl BillingAndUsagePageView { .with_cross_axis_alignment(CrossAxisAlignment::End); let current_user_id = auth_state.user_id().unwrap_or_default(); - plan_info.add_child(render_customer_type_badge(appearance, "Free".into())); + plan_info.add_child(render_customer_type_badge( + appearance, + i18n::t("settings.billing.plan.free_badge"), + )); plan_info.add_child( Container::new( appearance @@ -3363,7 +3425,7 @@ impl BillingAndUsagePageView { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Compare plans", + i18n::t("settings.billing.plan.compare_plans"), Icon::CoinsStacked.to_warpui_icon(appearance.theme().accent()), MainAxisSize::Min, MainAxisAlignment::Center, @@ -3402,10 +3464,14 @@ impl BillingAndUsagePageView { } fn render_plan_header_text(&self, appearance: &Appearance) -> Box { - Text::new_inline("Plan", appearance.ui_font_family(), HEADER_FONT_SIZE) - .with_style(Properties::default().weight(Weight::Bold)) - .with_color(appearance.theme().active_ui_text_color().into()) - .finish() + Text::new_inline( + i18n::t("settings.billing.plan"), + appearance.ui_font_family(), + HEADER_FONT_SIZE, + ) + .with_style(Properties::default().weight(Weight::Bold)) + .with_color(appearance.theme().active_ui_text_color().into()) + .finish() } fn render_team_admin_actions( @@ -3428,7 +3494,7 @@ impl BillingAndUsagePageView { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Manage billing", + i18n::t("settings.billing.plan.manage_billing"), Icon::CoinsStacked.to_warpui_icon(appearance.theme().accent()), MainAxisSize::Min, MainAxisAlignment::Center, @@ -3486,7 +3552,7 @@ impl BillingAndUsagePageView { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Open admin panel", + i18n::t("settings.billing.plan.open_admin_panel"), Icon::Users.to_warpui_icon(appearance.theme().accent()), MainAxisSize::Min, MainAxisAlignment::Center, @@ -3513,7 +3579,8 @@ impl BillingAndUsagePageView { ) -> (Box, Box) { let current_user_id = auth_state.user_id().unwrap_or_default(); - let plan_badge = render_customer_type_badge(appearance, "Free".into()); + let plan_badge = + render_customer_type_badge(appearance, i18n::t("settings.billing.plan.free_badge")); let badge_element = Container::new(plan_badge).with_margin_right(16.).finish(); @@ -3524,7 +3591,7 @@ impl BillingAndUsagePageView { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Compare plans", + i18n::t("settings.billing.plan.compare_plans"), Icon::CoinsStacked.to_warpui_icon(appearance.theme().accent()), MainAxisSize::Min, MainAxisAlignment::Center, diff --git a/app/src/settings_view/billing_and_usage_page_v2.rs b/app/src/settings_view/billing_and_usage_page_v2.rs index c8e1a195c7..beb699dd6b 100644 --- a/app/src/settings_view/billing_and_usage_page_v2.rs +++ b/app/src/settings_view/billing_and_usage_page_v2.rs @@ -58,18 +58,6 @@ use crate::workspaces::user_workspaces::{UserWorkspaces, UserWorkspacesEvent}; use crate::workspaces::workspace::{CustomerType, Workspace, WorkspaceUid}; use crate::{send_telemetry_from_ctx, WorkspaceAction}; -const ADDON_CREDITS_DESCRIPTION: &str = "Add-on credits are purchased in prepaid packages that roll over each billing cycle and expire after one year. The more you purchase, the better the per-credit rate. Once your base plan credits are used, add-on credits will be consumed."; -const ADDITIONAL_ADDON_CREDITS_DESCRIPTION_FOR_TEAM: &str = - "Purchased add-on credits are added to your personal balance."; -const MANAGED_AUTO_RELOAD_HEADER: &str = "Auto-reload is enabled"; - -const ADDON_CREDITS_DELINQUENT_WARNING_STRING: &str = - "Restricted due to billing issue. Update your payment method to purchase add-on credits."; -const ADDON_CREDITS_NON_ADMIN_DELINQUENT_WARNING_STRING: &str = - "Restricted due to billing issue. Contact your team admin to update their payment method."; -const RESTRICTED_BILLING_USAGE_WARNING_STRING: &str = "Auto reload is disabled due to recent failed reload. Please update your payment method and try again."; -const RESTRICTED_BILLING_USAGE_NON_ADMIN_WARNING_STRING: &str = "Auto reload is disabled due to recent failed reload. Contact your team admin to update their payment method."; - const HEADER_FONT_SIZE: f32 = 16.; pub(super) const BASE_CREDITS_DOT_COLOR: ColorU = ColorU { @@ -103,7 +91,6 @@ pub(super) const AGGREGATE_CREDITS_DOT_COLOR: ColorU = ColorU { a: 255, }; const DEFAULT_MAX_MONTHLY_SPEND_CENTS: i32 = 20_000; -const AMBIENT_AGENT_TRIAL_TITLE: &str = "Cloud agent trial"; #[derive(Default)] struct PlanSectionMouseStates { @@ -144,16 +131,13 @@ enum AddonCreditsPanelState { IneligiblePlan(AddonCreditsRestriction), AutoreloadNonAdmin { description_text: String, - warning_text: Option<&'static str>, + warning_text: Option, }, Purchase(AddonCreditsPurchaseState), } enum AddonCreditsRestriction { - UpgradeToBuild { - link_text: &'static str, - url: String, - }, + UpgradeToBuild { link_text: String, url: String }, ContactAccountExecutive, ContactTeamAdmin, } @@ -166,7 +150,7 @@ struct AddonCreditsPurchaseState { auto_reload_switch_disabled: bool, price_label: String, auto_reload_tooltip_text: String, - warning_text: Option<&'static str>, + warning_text: Option, } struct UsageHistoryState { @@ -204,7 +188,11 @@ impl GrantBucket { .all(|e| e.date_naive() == first.date_naive()) { let local = first.with_timezone(&Local); - format!("Expires {}", local.format("%b %d, %Y")) + format!( + "{} {}", + i18n::t("settings.billing.balance.expires_prefix"), + local.format("%b %d, %Y") + ) } else { String::new() } @@ -311,7 +299,7 @@ impl BillingAndUsagePageV2View { let addon_credit_modal_view = ctx.add_typed_action_view(|ctx| { Modal::new( - Some("Monthly spending limit".to_string()), + Some(i18n::t("settings.billing.spending_limit_modal.title")), addon_credit_modal, ctx, ) @@ -329,7 +317,11 @@ impl BillingAndUsagePageV2View { }); let load_more_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Load more", SecondaryTheme).on_click(|ctx| { + ActionButton::new( + i18n::t("settings.billing.usage_history.load_more"), + SecondaryTheme, + ) + .on_click(|ctx| { ctx.dispatch_typed_action(BillingAndUsagePageAction::RenderMoreUsageEntries); }) }); @@ -416,22 +408,16 @@ impl BillingAndUsagePageV2View { } UserWorkspacesEvent::UpdateWorkspaceSettingsRejected(_err) => { self.pending_auto_reload_toast = None; - self.show_toast( - "Failed to update workspace settings", - ToastFlavor::Error, - ctx, - ); + let msg = i18n::t("settings.billing.toast.update_settings_failed"); + self.show_toast(&msg, ToastFlavor::Error, ctx); } UserWorkspacesEvent::AiOveragesUpdated => { ctx.notify(); } UserWorkspacesEvent::PurchaseAddonCreditsSuccess => { self.addon_credits.purchase_loading = false; - self.show_toast( - "Successfully purchased add-on credits", - ToastFlavor::Success, - ctx, - ); + let msg = i18n::t("settings.billing.toast.purchase_success"); + self.show_toast(&msg, ToastFlavor::Success, ctx); AIRequestUsageModel::handle(ctx) .update(ctx, |m, ctx| m.refresh_request_usage_async(ctx)); } @@ -567,10 +553,14 @@ impl BillingAndUsagePageV2View { .with_main_axis_size(MainAxisSize::Max); plan_header.add_child( - Text::new_inline("Plan", appearance.ui_font_family(), HEADER_FONT_SIZE) - .with_style(Properties::default().weight(Weight::Bold)) - .with_color(appearance.theme().active_ui_text_color().into()) - .finish(), + Text::new_inline( + i18n::t("settings.billing.plan.header"), + appearance.ui_font_family(), + HEADER_FONT_SIZE, + ) + .with_style(Properties::default().weight(Weight::Bold)) + .with_color(appearance.theme().active_ui_text_color().into()) + .finish(), ); let mut right_side = Flex::row() @@ -614,7 +604,7 @@ impl BillingAndUsagePageV2View { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Manage billing", + i18n::t("settings.billing.plan.manage_billing"), Icon::CoinsStacked.to_warpui_icon(fg_color), MainAxisSize::Min, MainAxisAlignment::Center, @@ -655,7 +645,7 @@ impl BillingAndUsagePageV2View { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Open admin panel", + i18n::t("settings.billing.plan.open_admin_panel"), Icon::Users.to_warpui_icon(fg_color), MainAxisSize::Min, MainAxisAlignment::Center, @@ -683,9 +673,12 @@ impl BillingAndUsagePageV2View { } else { let current_user_id = self.auth_state.user_id().unwrap_or_default(); right_side.add_child( - Container::new(render_customer_type_badge(appearance, "Free".into())) - .with_margin_right(8.) - .finish(), + Container::new(render_customer_type_badge( + appearance, + i18n::t("settings.billing.plan.free_badge"), + )) + .with_margin_right(8.) + .finish(), ); right_side.add_child( Container::new( @@ -698,7 +691,7 @@ impl BillingAndUsagePageV2View { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Compare plans", + i18n::t("settings.billing.plan.compare_plans"), Icon::CoinsStacked .to_warpui_icon(appearance.theme().active_ui_text_color()), MainAxisSize::Min, @@ -799,7 +792,7 @@ impl BillingAndUsagePageV2View { render_balance_card( appearance, BASE_CREDITS_DOT_COLOR, - "Base credits", + &i18n::t("settings.billing.balance.base_credits"), &reset_str, base_remaining, outline_color, @@ -816,7 +809,7 @@ impl BillingAndUsagePageV2View { render_balance_card( appearance, BONUS_CREDITS_DOT_COLOR, - "Personal credits", + &i18n::t("settings.billing.balance.personal_credits"), &classified.personal.expiry_label(), classified.personal.total_balance(), outline_color, @@ -833,7 +826,7 @@ impl BillingAndUsagePageV2View { render_balance_card( appearance, BONUS_CREDITS_DOT_COLOR, - "Team credits", + &i18n::t("settings.billing.balance.team_credits"), &classified.team.expiry_label(), classified.team.total_balance(), outline_color, @@ -847,10 +840,14 @@ impl BillingAndUsagePageV2View { Flex::column() .with_child( Container::new( - Text::new_inline("Balance", appearance.ui_font_family(), HEADER_FONT_SIZE) - .with_style(Properties::default().weight(Weight::Bold)) - .with_color(theme.active_ui_text_color().into()) - .finish(), + Text::new_inline( + i18n::t("settings.billing.balance.header"), + appearance.ui_font_family(), + HEADER_FONT_SIZE, + ) + .with_style(Properties::default().weight(Weight::Bold)) + .with_color(theme.active_ui_text_color().into()) + .finish(), ) .with_margin_bottom(12.) .finish(), @@ -885,17 +882,22 @@ impl BillingAndUsagePageV2View { let fg = theme.foreground().into_solid(); let bg = theme.background().into_solid(); - let title = Text::new_inline(AMBIENT_AGENT_TRIAL_TITLE, appearance.ui_font_family(), 14.) - .with_color(theme.active_ui_text_color().into()) - .with_style(Properties::default().weight(Weight::Semibold)) - .finish(); + let title = Text::new_inline( + i18n::t("settings.billing.ambient_trial.title"), + appearance.ui_font_family(), + 14., + ) + .with_color(theme.active_ui_text_color().into()) + .with_style(Properties::default().weight(Weight::Semibold)) + .finish(); let credits_text = if credits_remaining == 1 { - "1 credit remaining".to_string() + i18n::t("settings.billing.ambient_trial.credits_remaining_one") } else { format!( - "{} credits remaining", - credits_remaining.separate_with_commas() + "{} {}", + credits_remaining.separate_with_commas(), + i18n::t("settings.billing.ambient_trial.credits_remaining_suffix") ) }; let credits_label = Text::new_inline(credits_text, appearance.ui_font_family(), 12.) @@ -916,7 +918,7 @@ impl BillingAndUsagePageV2View { ButtonVariant::Secondary, self.ambient_trial_mouse_states.new_agent_button.clone(), ) - .with_text_label("New agent".to_string()) + .with_text_label(i18n::t("settings.billing.ambient_trial.new_agent_button")) .with_style(UiComponentStyles { font_color: Some(bg), background: Some(fg.into()), @@ -952,7 +954,7 @@ impl BillingAndUsagePageV2View { ButtonVariant::Secondary, self.ambient_trial_mouse_states.buy_more_button.clone(), ) - .with_text_label("Buy more".to_string()) + .with_text_label(i18n::t("settings.billing.ambient_trial.buy_more_button")) .with_style(UiComponentStyles { background: Some(bg.into()), font_size: Some(14.), @@ -1079,7 +1081,7 @@ impl BillingAndUsagePageV2View { } else if can_upgrade { return AddonCreditsPanelState::IneligiblePlan( AddonCreditsRestriction::UpgradeToBuild { - link_text: "Upgrade to Build", + link_text: i18n::t("settings.billing.addon.upgrade_to_build_link"), url: UserWorkspaces::upgrade_link_for_team(team_uid), }, ); @@ -1103,9 +1105,13 @@ impl BillingAndUsagePageV2View { .map(|t| t.members.len()) .unwrap_or(1); let description_text = if team_count > 1 { - format!("{ADDON_CREDITS_DESCRIPTION} {ADDITIONAL_ADDON_CREDITS_DESCRIPTION_FOR_TEAM}") + format!( + "{} {}", + i18n::t("settings.billing.addon.description"), + i18n::t("settings.billing.addon.description_team_suffix") + ) } else { - ADDON_CREDITS_DESCRIPTION.to_string() + i18n::t("settings.billing.addon.description") }; let would_exceed = selected_credit_option.is_some_and(|opt| { @@ -1127,42 +1133,46 @@ impl BillingAndUsagePageV2View { .map(|opt| { let credits = opt.credits.separate_with_commas(); let dollars = format!("${:.2}", opt.price_usd_cents as f64 / 100.0); - format!("{credits} credits / {dollars}") + i18n::t("settings.billing.addon.price_label") + .replace("{credits}", &credits) + .replace("{dollars}", &dollars) }) .unwrap_or_default(); let auto_reload_credit_amount = selected_credit_option - .map(|o| format!("{} credits", o.credits.separate_with_commas())) - .unwrap_or_else(|| "selected credit amount".to_string()); - let auto_reload_tooltip_text = format!( - "When any member on your team’s credit balance reaches 100 credits remaining, \ - automatically purchase {auto_reload_credit_amount}." - ); - let warning_text = if delinquent && has_admin_permissions { - Some(ADDON_CREDITS_DELINQUENT_WARNING_STRING) + .map(|o| { + format!( + "{} {}", + o.credits.separate_with_commas(), + i18n::t("settings.billing.addon.credits_unit") + ) + }) + .unwrap_or_else(|| i18n::t("settings.billing.addon.selected_credit_amount_fallback")); + let auto_reload_tooltip_text = i18n::t("settings.billing.addon.auto_reload_tooltip") + .replace("{amount}", &auto_reload_credit_amount); + let warning_text: Option = if delinquent && has_admin_permissions { + Some(i18n::t("settings.billing.addon.warning_delinquent_admin")) } else if delinquent { - Some(ADDON_CREDITS_NON_ADMIN_DELINQUENT_WARNING_STRING) + Some(i18n::t( + "settings.billing.addon.warning_delinquent_non_admin", + )) } else if workspace .billing_metadata .has_failed_addon_credit_auto_reload_status() { Some(if has_admin_permissions { - RESTRICTED_BILLING_USAGE_WARNING_STRING + i18n::t("settings.billing.addon.warning_failed_reload_admin") } else { - RESTRICTED_BILLING_USAGE_NON_ADMIN_WARNING_STRING + i18n::t("settings.billing.addon.warning_failed_reload_non_admin") }) } else if would_exceed { Some(match (auto_reload_enabled, has_admin_permissions) { - (true, true) => { - "Auto-reload is paused because the next reload would exceed your monthly spend limit. Increase your limit to continue using auto-reload." - } + (true, true) => i18n::t("settings.billing.addon.warning_auto_reload_paused_admin"), (true, false) => { - "Auto-reload is paused because the next reload would exceed your team’s monthly spend limit. Contact a team admin to increase it." - } - (false, true) => { - "This purchase would exceed your monthly limit. Increase your limit to continue." + i18n::t("settings.billing.addon.warning_auto_reload_paused_non_admin") } + (false, true) => i18n::t("settings.billing.addon.warning_purchase_exceeds_admin"), (false, false) => { - "This purchase would exceed your team’s monthly spend limit. Contact a team admin to increase it." + i18n::t("settings.billing.addon.warning_purchase_exceeds_non_admin") } }) } else { @@ -1185,13 +1195,11 @@ impl BillingAndUsagePageV2View { Some(option) => { let credits = option.credits.separate_with_commas(); let price = format!("${:.2}", option.price_usd_cents as f64 / 100.0); - format!( - "Your admin has enabled auto-reload for add-on credits. When your personal add-on credit balance runs low, Warp will automatically purchase {credits} credits for {price} and add them to your balance." - ) - } - None => { - "Your admin has enabled auto-reload for add-on credits. When your personal add-on credit balance runs low, Warp will automatically purchase add-on credits and add them to your balance.".to_string() + i18n::t("settings.billing.addon.non_admin_auto_reload_description_with_amount") + .replace("{credits}", &credits) + .replace("{price}", &price) } + None => i18n::t("settings.billing.addon.non_admin_auto_reload_description"), }; return AddonCreditsPanelState::AutoreloadNonAdmin { description_text, @@ -1223,7 +1231,9 @@ impl BillingAndUsagePageV2View { FormattedTextElement::new( FormattedText::new([FormattedTextLine::Line(vec![ FormattedTextFragment::hyperlink(link_text, url), - FormattedTextFragment::plain_text(" to purchase add-on credits."), + FormattedTextFragment::plain_text(i18n::t( + "settings.billing.addon.upgrade_to_build_suffix", + )), ])]), appearance.ui_font_size(), appearance.ui_font_family(), @@ -1248,7 +1258,7 @@ impl BillingAndUsagePageV2View { } AddonCreditsRestriction::ContactAccountExecutive => appearance .ui_builder() - .paragraph("Contact your Account Executive for more add-on credits.") + .paragraph(i18n::t("settings.billing.addon.contact_account_executive")) .with_style(UiComponentStyles { font_color: Some(theme.sub_text_color(bg).into()), ..Default::default() @@ -1257,7 +1267,7 @@ impl BillingAndUsagePageV2View { .finish(), AddonCreditsRestriction::ContactTeamAdmin => appearance .ui_builder() - .paragraph("Contact a team admin to enable add-on credits.") + .paragraph(i18n::t("settings.billing.addon.contact_team_admin")) .with_style(UiComponentStyles { font_color: Some(theme.sub_text_color(bg).into()), ..Default::default() @@ -1265,10 +1275,14 @@ impl BillingAndUsagePageV2View { .build() .finish(), }; - let header = Text::new_inline("Buy credits", appearance.ui_font_family(), HEADER_FONT_SIZE) - .with_color(theme.foreground().into()) - .with_style(Properties::default().weight(Weight::Medium)) - .finish(); + let header = Text::new_inline( + i18n::t("settings.billing.addon.buy_credits_header"), + appearance.ui_font_family(), + HEADER_FONT_SIZE, + ) + .with_color(theme.foreground().into()) + .with_style(Properties::default().weight(Weight::Medium)) + .finish(); let card = Flex::column() .with_cross_axis_alignment(CrossAxisAlignment::Stretch) .with_children([ @@ -1289,12 +1303,12 @@ impl BillingAndUsagePageV2View { &self, appearance: &Appearance, description_text: String, - warning_text: Option<&'static str>, + warning_text: Option, ) -> Box { let theme = appearance.theme(); let bg = theme.background(); let auto_reload_header = Text::new_inline( - MANAGED_AUTO_RELOAD_HEADER, + i18n::t("settings.billing.addon.auto_reload_enabled_header"), appearance.ui_font_family(), HEADER_FONT_SIZE, ) @@ -1358,10 +1372,14 @@ impl BillingAndUsagePageV2View { let theme = appearance.theme(); let bg = theme.background(); let ui_builder = appearance.ui_builder(); - let header = Text::new_inline("Buy credits", appearance.ui_font_family(), HEADER_FONT_SIZE) - .with_color(theme.foreground().into()) - .with_style(Properties::default().weight(Weight::Medium)) - .finish(); + let header = Text::new_inline( + i18n::t("settings.billing.addon.buy_credits_header"), + appearance.ui_font_family(), + HEADER_FONT_SIZE, + ) + .with_color(theme.foreground().into()) + .with_style(Properties::default().weight(Weight::Medium)) + .finish(); let paragraph = ui_builder .paragraph(state.description_text.clone()) .with_style(UiComponentStyles { @@ -1381,9 +1399,9 @@ impl BillingAndUsagePageV2View { mouse_state: self.buy_credits_mouse_states.addon_info_icon.clone(), on_click_action: None, secondary_text: None, - tooltip_override_text: Some( - "Sets the monthly limit spent on add-on credits".to_string(), - ), + tooltip_override_text: Some(i18n::t( + "settings.billing.addon.monthly_spend_limit_tooltip", + )), }, ); let spend_limit = workspace @@ -1395,7 +1413,10 @@ impl BillingAndUsagePageV2View { let spend_row = Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_children([ - ui_builder.span("Monthly spend limit").build().finish(), + ui_builder + .span(i18n::t("settings.billing.addon.monthly_spend_limit_label")) + .build() + .finish(), Shrinkable::new(1., Align::new(info_icon).left().finish()).finish(), icon_button( appearance, @@ -1458,14 +1479,22 @@ impl BillingAndUsagePageV2View { let cost_dollars = bonus_grants.cents_spent as f64 / 100.0; let theme = appearance.theme(); - let label = Text::new_inline("Purchased this month", appearance.ui_font_family(), 12.) - .with_color(theme.active_ui_text_color().into()) - .finish(); + let label = Text::new_inline( + i18n::t("settings.billing.addon.purchased_this_month"), + appearance.ui_font_family(), + 12., + ) + .with_color(theme.active_ui_text_color().into()) + .finish(); let credits_text = if credits_purchased == 1 { - "1 credit".to_string() + i18n::t("settings.billing.addon.credits_count_one") } else { - format!("{} credits", credits_purchased.separate_with_commas()) + format!( + "{} {}", + credits_purchased.separate_with_commas(), + i18n::t("settings.billing.addon.credits_unit") + ) }; let credits_component = Container::new( @@ -1515,9 +1544,9 @@ impl BillingAndUsagePageV2View { let fg = theme.foreground(); let auto_reload_enabled = state.auto_reload_enabled; let purchase_button_label = if self.addon_credits.purchase_loading { - "Buying\u{2026}" + i18n::t("settings.billing.addon.purchase_button_loading") } else { - "One-time purchase" + i18n::t("settings.billing.addon.purchase_button") }; let purchase_button_font_color = state .purchase_disabled @@ -1595,10 +1624,14 @@ impl BillingAndUsagePageV2View { ); right_group.add_children([ - Text::new_inline("Auto-reload", appearance.ui_font_family(), 14.) - .with_color(fg.into()) - .with_style(Properties::default().weight(Weight::Semibold)) - .finish(), + Text::new_inline( + i18n::t("settings.billing.addon.auto_reload_label"), + appearance.ui_font_family(), + 14., + ) + .with_color(fg.into()) + .with_style(Properties::default().weight(Weight::Semibold)) + .finish(), Container::new(auto_reload_info_icon) .with_margin_left(4.) .finish(), @@ -1620,7 +1653,7 @@ impl BillingAndUsagePageV2View { .with_child(right_group.finish()); let mut lower_children: Vec> = vec![lower_row.finish()]; - if let Some(warning_text) = state.warning_text { + if let Some(warning_text) = state.warning_text.as_ref() { lower_children.push(self.render_warning_row(appearance, warning_text.to_string())); } @@ -1732,12 +1765,16 @@ impl BillingAndUsagePageV2View { .with_main_axis_alignment(MainAxisAlignment::Center) .with_child( Container::new( - Text::new_inline("Last 30 days", appearance.ui_font_family(), 14.) - .with_color(blended_colors::text_sub( - appearance.theme(), - appearance.theme().surface_1(), - )) - .finish(), + Text::new_inline( + i18n::t("settings.billing.usage_history.last_30_days"), + appearance.ui_font_family(), + 14., + ) + .with_color(blended_colors::text_sub( + appearance.theme(), + appearance.theme().surface_1(), + )) + .finish(), ) .with_vertical_margin(12.) .finish(), @@ -1841,19 +1878,23 @@ impl BillingAndUsagePageV2View { ) .with_child( Container::new( - Text::new("No usage history", appearance.ui_font_family(), 14.) - .with_color(blended_colors::text_sub( - appearance.theme(), - appearance.theme().surface_1(), - )) - .finish(), + Text::new( + i18n::t("settings.billing.usage_history.empty_title"), + appearance.ui_font_family(), + 14., + ) + .with_color(blended_colors::text_sub( + appearance.theme(), + appearance.theme().surface_1(), + )) + .finish(), ) .with_margin_bottom(4.) .finish(), ) .with_child( Text::new( - "Kick off an agent task to view usage history here.", + i18n::t("settings.billing.usage_history.empty_description"), appearance.ui_font_family(), 14., ) @@ -1920,7 +1961,7 @@ impl View for BillingAndUsagePageV2View { page.add_child(tab_selector::render_tab_selector( tabs, - self.selected_tab.label(), + &self.selected_tab.label(), |label, ctx| { ctx.dispatch_typed_action(BillingAndUsagePageAction::SelectTab( BillingUsageTab::get_tab_from_label(label), @@ -2090,11 +2131,8 @@ impl TypedActionView for BillingAndUsagePageV2View { .options .get(self.addon_credits.selected_denomination) else { - self.show_toast( - "Unable to enable auto-reload until pricing options load.", - ToastFlavor::Error, - ctx, - ); + let msg = i18n::t("settings.billing.toast.auto_reload_pricing_loading"); + self.show_toast(&msg, ToastFlavor::Error, ctx); return; }; Some(option.credits) @@ -2114,12 +2152,13 @@ impl TypedActionView for BillingAndUsagePageV2View { self.pending_auto_reload_toast = Some(if *enabled { let credits = auto_reload_denomination_credits .map(|c| c.separate_with_commas()) - .unwrap_or_else(|| "your selected".to_string()); - format!( - "Auto-reload enabled. We'll refill with {credits} credits when your balance runs low." - ) + .unwrap_or_else(|| { + i18n::t("settings.billing.toast.your_selected_fallback") + }); + i18n::t("settings.billing.toast.auto_reload_enabled") + .replace("{credits}", &credits) } else { - "Auto-reload disabled.".to_string() + i18n::t("settings.billing.toast.auto_reload_disabled") }); UserWorkspaces::handle(ctx).update(ctx, |ws, ctx| { ws.update_addon_credits_settings( @@ -2204,9 +2243,13 @@ fn render_balance_card( .with_style(Properties::default().weight(Weight::Semibold)) .finish(); - let remaining_label = Text::new_inline("remaining", appearance.ui_font_family(), 14.) - .with_color(sub_color) - .finish(); + let remaining_label = Text::new_inline( + i18n::t("settings.billing.balance.remaining"), + appearance.ui_font_family(), + 14., + ) + .with_color(sub_color) + .finish(); let value_row = Flex::row() .with_child(credit_count) diff --git a/app/src/settings_view/code_page.rs b/app/src/settings_view/code_page.rs index 349dd12eee..4401858287 100644 --- a/app/src/settings_view/code_page.rs +++ b/app/src/settings_view/code_page.rs @@ -77,18 +77,6 @@ const SUB_SECTION_MARGIN: f32 = 8.; const STATUS_ICON_SIZE: f32 = 16.; const LSP_STATUS_INDICATOR_SIZE: f32 = 8.; -const CODE_FEATURE_NAME: &str = "Code"; -const INITIALIZATION_SETTINGS_HEADER: &str = "Initialization Settings"; -const CODEBASE_INDEXING_LABEL: &str = "Codebase indexing"; -const CODEBASE_INDEX_DESCRIPTION: &str = "Warp can automatically index code repositories as you navigate them, helping agents quickly understand context and provide solutions. Code is never stored on the server. If a codebase is unable to be indexed, Warp can still navigate your codebase and gain insights via grep and find tool calling."; -const WARP_INDEXING_IGNORE_DESCRIPTION: &str = "To exclude specific files or directories from indexing, add them to the .warpindexingignore file in your repository directory. These files will still be accessible to AI features, but they won't be included in codebase embeddings."; -const AUTO_INDEX_FEATURE_NAME: &str = "Index new folders by default"; -const AUTO_INDEX_DESCRIPTION: &str = "When set to true, Warp will automatically index code repositories as you navigate them - helping agents quickly understand context and provide targeted solutions."; -const INDEXING_DISABLED_ADMIN_TEXT: &str = "Team admins have disabled codebase indexing."; -const INDEXING_WORKSPACE_ENABLED_ADMIN_TEXT: &str = "Team admins have enabled codebase indexing."; -const INDEXING_DISABLED_GLOBAL_AI_TEXT: &str = - "AI Features must be enabled to use codebase indexing."; -const CODEBASE_INDEX_LIMIT_REACHED: &str = "You have reached the maximum number of codebase indices for your plan. Delete existing indices to auto-index new codebases."; #[cfg(not(target_family = "wasm"))] const REMOTE_CODEBASE_INDEX_LIMIT_REACHED_FAILURE: &str = "maximum number of codebase indexes has been reached"; @@ -113,8 +101,12 @@ impl CodeSubpage { pub fn title(&self) -> &'static str { match self { - Self::Indexing => "Codebase Indexing", - Self::EditorAndCodeReview => "Editor and Code Review", + Self::Indexing => { + Box::leak(i18n::t("settings.code.subpage.indexing.title").into_boxed_str()) + } + Self::EditorAndCodeReview => { + Box::leak(i18n::t("settings.code.subpage.editor_and_review.title").into_boxed_str()) + } } } } @@ -364,11 +356,14 @@ impl CodeSettingsPageView { }); let manual_add_directory_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Index new folder", SecondaryTheme) - .with_icon(Icon::FindAll) - .on_click(|ctx| { - ctx.dispatch_typed_action(CodeSettingsPageAction::ManualAddDirectory); - }) + ActionButton::new( + i18n::t("settings.code.index_new_folder.button"), + SecondaryTheme, + ) + .with_icon(Icon::FindAll) + .on_click(|ctx| { + ctx.dispatch_typed_action(CodeSettingsPageAction::ManualAddDirectory); + }) }); let code_page_widget = CodePageWidget { @@ -408,8 +403,16 @@ impl CodeSettingsPageView { Box::new(GlobalSearchToggleWidget::default()), ]); let categories = vec![ - Category::new("Codebase Indexing", codebase_indexing_widgets), - Category::new("Code Editor and Review", code_editor_review_widgets), + Category::new( + Box::leak(i18n::t("settings.code.category.indexing.title").into_boxed_str()), + codebase_indexing_widgets, + ), + Category::new( + Box::leak( + i18n::t("settings.code.category.editor_and_review.title").into_boxed_str(), + ), + code_editor_review_widgets, + ), ]; PageType::new_categorized(categories, None) } else { @@ -459,11 +462,14 @@ impl CodeSettingsPageView { // or the full categorized page when subpage is None. if let Some(subpage) = subpage { let manual_add_directory_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Index new folder", SecondaryTheme) - .with_icon(Icon::FindAll) - .on_click(|ctx| { - ctx.dispatch_typed_action(CodeSettingsPageAction::ManualAddDirectory); - }) + ActionButton::new( + i18n::t("settings.code.index_new_folder.button"), + SecondaryTheme, + ) + .with_icon(Icon::FindAll) + .on_click(|ctx| { + ctx.dispatch_typed_action(CodeSettingsPageAction::ManualAddDirectory); + }) }); let mut widgets: Vec>> = vec![Box::new(CodeSubpageHeaderWidget { @@ -508,11 +514,14 @@ impl CodeSettingsPageView { fn build_full_page(ctx: &mut ViewContext) -> PageType { if FeatureFlag::OpenWarpNewSettingsModes.is_enabled() { let manual_add_directory_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Index new folder", SecondaryTheme) - .with_icon(Icon::FindAll) - .on_click(|ctx| { - ctx.dispatch_typed_action(CodeSettingsPageAction::ManualAddDirectory); - }) + ActionButton::new( + i18n::t("settings.code.index_new_folder.button"), + SecondaryTheme, + ) + .with_icon(Icon::FindAll) + .on_click(|ctx| { + ctx.dispatch_typed_action(CodeSettingsPageAction::ManualAddDirectory); + }) }); let code_page_widget = CodePageWidget { switch_state: Default::default(), @@ -540,17 +549,28 @@ impl CodeSettingsPageView { Box::new(GlobalSearchToggleWidget::default()), ]); let categories = vec![ - Category::new("Codebase Indexing", codebase_indexing_widgets), - Category::new("Code Editor and Review", code_editor_review_widgets), + Category::new( + Box::leak(i18n::t("settings.code.category.indexing.title").into_boxed_str()), + codebase_indexing_widgets, + ), + Category::new( + Box::leak( + i18n::t("settings.code.category.editor_and_review.title").into_boxed_str(), + ), + code_editor_review_widgets, + ), ]; PageType::new_categorized(categories, None) } else { let manual_add_directory_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Index new folder", SecondaryTheme) - .with_icon(Icon::FindAll) - .on_click(|ctx| { - ctx.dispatch_typed_action(CodeSettingsPageAction::ManualAddDirectory); - }) + ActionButton::new( + i18n::t("settings.code.index_new_folder.button"), + SecondaryTheme, + ) + .with_icon(Icon::FindAll) + .on_click(|ctx| { + ctx.dispatch_typed_action(CodeSettingsPageAction::ManualAddDirectory); + }) }); let widgets: Vec>> = vec![Box::new(CodePageWidget { @@ -945,7 +965,7 @@ pub fn init_actions_from_parent_view( if FeatureFlag::FullSourceCodeEmbedding.is_enabled() { ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::new( - "codebase index", + i18n::t("settings.code.action.codebase_index"), builder(SettingsAction::Code( CodeSettingsPageAction::ToggleCodebaseContext, )), @@ -957,7 +977,7 @@ pub fn init_actions_from_parent_view( ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::new( - "auto-indexing", + i18n::t("settings.code.action.auto_indexing"), builder(SettingsAction::Code( CodeSettingsPageAction::ToggleAutoIndexing, )), @@ -972,7 +992,7 @@ pub fn init_actions_from_parent_view( ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ ToggleSettingActionPair::new( - "auto open code review panel", + i18n::t("settings.code.auto_open_review_panel.label"), builder(SettingsAction::Code( CodeSettingsPageAction::ToggleAutoOpenCodeReviewPane, )), @@ -980,7 +1000,7 @@ pub fn init_actions_from_parent_view( flags::AUTO_OPEN_CODE_REVIEW_PANE_FLAG, ), ToggleSettingActionPair::new( - "code review button", + i18n::t("settings.code.action.code_review_button"), builder(SettingsAction::Code( CodeSettingsPageAction::ToggleCodeReviewPanel, )), @@ -988,7 +1008,7 @@ pub fn init_actions_from_parent_view( flags::SHOW_CODE_REVIEW_BUTTON_FLAG, ), ToggleSettingActionPair::new( - "diff stats on code review button", + i18n::t("settings.code.action.diff_stats_on_code_review_button"), builder(SettingsAction::Code( CodeSettingsPageAction::ToggleShowCodeReviewDiffStats, )), @@ -996,7 +1016,7 @@ pub fn init_actions_from_parent_view( flags::SHOW_CODE_REVIEW_DIFF_STATS_FLAG, ), ToggleSettingActionPair::new( - "project explorer", + i18n::t("settings.code.project_explorer.label"), builder(SettingsAction::Code( CodeSettingsPageAction::ToggleProjectExplorer, )), @@ -1004,7 +1024,7 @@ pub fn init_actions_from_parent_view( flags::SHOW_PROJECT_EXPLORER, ), ToggleSettingActionPair::new( - "global file search", + i18n::t("settings.code.global_search.label"), builder(SettingsAction::Code( CodeSettingsPageAction::ToggleGlobalSearch, )), @@ -1053,12 +1073,12 @@ impl SettingsWidget for CodePageWidget { )); content.add_child(self.render_settings_subtext( global_ai_enabled, - CODEBASE_INDEX_DESCRIPTION, + Box::leak(i18n::t("settings.code.codebase_index.description").into_boxed_str()), appearance, )); content.add_child(self.render_settings_subtext( global_ai_enabled, - WARP_INDEXING_IGNORE_DESCRIPTION, + Box::leak(i18n::t("settings.code.indexing_ignore.description").into_boxed_str()), appearance, )); @@ -1105,7 +1125,7 @@ impl CodePageWidget { let mut rows = vec![ self.render_autoindex_row( - AUTO_INDEX_FEATURE_NAME, + Box::leak(i18n::t("settings.code.auto_index.label").into_boxed_str()), self.auto_index_switch_state.clone(), auto_indexing_enabled, CodeSettingsPageAction::ToggleAutoIndexing, @@ -1114,7 +1134,7 @@ impl CodePageWidget { // Use subtext styling for description (gray color per Figma) self.render_settings_subtext( codebase_indexing_enabled, - AUTO_INDEX_DESCRIPTION, + Box::leak(i18n::t("settings.code.auto_index.description").into_boxed_str()), appearance, ), ]; @@ -1123,7 +1143,7 @@ impl CodePageWidget { { rows.push(self.render_settings_subtext( false, - CODEBASE_INDEX_LIMIT_REACHED, + Box::leak(i18n::t("settings.code.index_limit_reached").into_boxed_str()), appearance, )); } @@ -1212,7 +1232,7 @@ impl CodePageWidget { Container::new( ui_builder - .span(CODE_FEATURE_NAME) + .span(i18n::t("settings.code.header")) .with_style(UiComponentStyles { font_size: Some(24.0), font_weight: Some(Weight::Bold), @@ -1233,7 +1253,7 @@ impl CodePageWidget { Container::new( ui_builder - .span(INITIALIZATION_SETTINGS_HEADER) + .span(i18n::t("settings.code.initialization_settings.header")) .with_style(UiComponentStyles { font_size: Some(18.0), font_weight: Some(Weight::Semibold), @@ -1260,7 +1280,7 @@ impl CodePageWidget { let admin_setting = UserWorkspaces::as_ref(app).team_allows_codebase_context(); let label = ui_builder - .span(CODEBASE_INDEXING_LABEL) + .span(i18n::t("settings.code.codebase_indexing.label")) .with_style(UiComponentStyles { font_size: Some(16.0), font_weight: Some(Weight::Semibold), @@ -1274,11 +1294,13 @@ impl CodePageWidget { .switch(self.switch_state.clone()) .check(UserWorkspaces::as_ref(app).is_codebase_context_enabled(app)); - let disabled_tooltip_text = match admin_setting { - AdminEnablementSetting::Enable => Some(INDEXING_WORKSPACE_ENABLED_ADMIN_TEXT), - AdminEnablementSetting::Disable => Some(INDEXING_DISABLED_ADMIN_TEXT), + let disabled_tooltip_text: Option = match admin_setting { + AdminEnablementSetting::Enable => Some(i18n::t("settings.code.tooltip.admin_enabled")), + AdminEnablementSetting::Disable => { + Some(i18n::t("settings.code.tooltip.admin_disabled")) + } AdminEnablementSetting::RespectUserSetting if !global_ai_enabled => { - Some(INDEXING_DISABLED_GLOBAL_AI_TEXT) + Some(i18n::t("settings.code.tooltip.ai_required")) } AdminEnablementSetting::RespectUserSetting => None, }; @@ -1351,7 +1373,7 @@ impl CodePageWidget { .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( ui_builder - .span("Initialized / indexed folders") + .span(i18n::t("settings.code.initialized_folders.header")) .with_style(UiComponentStyles { font_size: Some(16.0), font_weight: Some(Weight::Semibold), @@ -1475,7 +1497,7 @@ impl CodePageWidget { Container::new( appearance .ui_builder() - .paragraph("No folders have been initialized yet.") + .paragraph(i18n::t("settings.code.no_folders_initialized")) .build() .finish(), ) @@ -1542,7 +1564,7 @@ impl CodePageWidget { .with_text_and_icon_label( warpui::ui_components::button::TextAndIcon::new( warpui::ui_components::button::TextAndIconAlignment::IconFirst, - "Open project rules", + i18n::t("settings.code.open_project_rules.button"), warpui::elements::Icon::new( "bundled/svg/file-code-02.svg", theme.foreground(), @@ -1706,7 +1728,7 @@ impl CodePageWidget { let mut column = Flex::column().with_spacing(SUB_SECTION_MARGIN); column.add_child( ui_builder - .span("INDEXING") + .span(i18n::t("settings.code.section.indexing")) .with_style(UiComponentStyles { font_size: Some(11.0), font_weight: Some(Weight::Semibold), @@ -1745,7 +1767,7 @@ impl CodePageWidget { let theme = appearance.theme(); let Some(index_state) = index_state else { return IndexingStatusPresentation { - text: Cow::from("No index created"), + text: Cow::from(i18n::t("settings.code.status.no_index_created")), color: theme.disabled_ui_text_color().into_solid(), icon: Some(Icon::SlashCircle), refresh_action: None, @@ -1755,14 +1777,19 @@ impl CodePageWidget { if index_state.has_pending() { let text = match index_state.sync_progress() { - Some(SyncProgress::Discovering { total_nodes }) => { - Cow::from(format!("Discovered {total_nodes} chunks")) - } + Some(SyncProgress::Discovering { total_nodes }) => Cow::from(format!( + "{}{total_nodes}{}", + i18n::t("settings.code.status.discovered.prefix"), + i18n::t("settings.code.status.discovered.suffix") + )), Some(SyncProgress::Syncing { completed_nodes, total_nodes, - }) => Cow::from(format!("Syncing - {completed_nodes} / {total_nodes}")), - None => Cow::from("Syncing..."), + }) => Cow::from(format!( + "{}{completed_nodes} / {total_nodes}", + i18n::t("settings.code.status.syncing.prefix") + )), + None => Cow::from(i18n::t("settings.code.status.syncing")), }; return IndexingStatusPresentation { @@ -1776,25 +1803,33 @@ impl CodePageWidget { if let Some(completed_successfully) = index_state.last_sync_successful() { let (text, color, icon) = if completed_successfully { - ("Synced", theme.ansi_fg_green(), Icon::Check) + ( + i18n::t("settings.code.status.synced"), + theme.ansi_fg_green(), + Icon::Check, + ) } else if let Some(CodebaseIndexFinishedStatus::Failed( CodebaseIndexingError::ExceededMaxFileLimit | CodebaseIndexingError::MaxDepthExceeded, )) = index_state.last_sync_result() { ( - "Codebase too large", + i18n::t("settings.code.status.codebase_too_large"), theme.ui_warning_color(), Icon::AlertTriangle, ) } else if index_state.has_synced_version() { ( - "Stale", + i18n::t("settings.code.status.stale"), theme.nonactive_ui_detail().into_solid(), Icon::ClockRefresh, ) } else { - ("Failed", theme.ui_error_color(), Icon::AlertTriangle) + ( + i18n::t("settings.code.status.failed"), + theme.ui_error_color(), + Icon::AlertTriangle, + ) }; return IndexingStatusPresentation { @@ -1808,7 +1843,7 @@ impl CodePageWidget { log::warn!("No index state for codebase"); IndexingStatusPresentation { - text: Cow::from("No index built"), + text: Cow::from(i18n::t("settings.code.status.no_index_built")), color: theme.nonactive_ui_text_color().into_solid(), icon: None, refresh_action: None, @@ -1826,7 +1861,7 @@ impl CodePageWidget { match status.state { RemoteCodebaseIndexState::NotEnabled => IndexingStatusPresentation { - text: Cow::from("No index created"), + text: Cow::from(i18n::t("settings.code.status.no_index_created")), color: theme.disabled_ui_text_color().into_solid(), icon: Some(Icon::SlashCircle), refresh_action: Some(IndexingRefreshAction::RequestRemote), @@ -1836,9 +1871,9 @@ impl CodePageWidget { let limit_reached = remote_codebase_index_limit_reached(status); IndexingStatusPresentation { text: Cow::from(if limit_reached { - "Index limit reached" + i18n::t("settings.code.status.index_limit_reached") } else { - "Unavailable" + i18n::t("settings.code.status.unavailable") }), color: if limit_reached { theme.ui_warning_color() @@ -1855,27 +1890,28 @@ impl CodePageWidget { } } RemoteCodebaseIndexState::Disabled => IndexingStatusPresentation { - text: Cow::from("Disabled"), + text: Cow::from(i18n::t("settings.code.status.disabled")), color: theme.disabled_ui_text_color().into_solid(), icon: Some(Icon::SlashCircle), refresh_action: Some(IndexingRefreshAction::RequestRemote), show_delete: true, }, RemoteCodebaseIndexState::Queued => IndexingStatusPresentation { - text: Cow::from("Queued"), + text: Cow::from(i18n::t("settings.code.status.queued")), color: theme.disabled_ui_text_color().into_solid(), icon: None, refresh_action: None, show_delete: true, }, RemoteCodebaseIndexState::Indexing => { + let indexing_prefix = i18n::t("settings.code.status.indexing.prefix"); let text = match (status.progress_completed, status.progress_total) { (Some(completed), Some(total)) => { - Cow::from(format!("Indexing - {completed} / {total}")) + Cow::from(format!("{indexing_prefix}{completed} / {total}")) } - (Some(completed), None) => Cow::from(format!("Indexing - {completed}")), - (None, Some(total)) => Cow::from(format!("Indexing - 0 / {total}")), - (None, None) => Cow::from("Indexing..."), + (Some(completed), None) => Cow::from(format!("{indexing_prefix}{completed}")), + (None, Some(total)) => Cow::from(format!("{indexing_prefix}0 / {total}")), + (None, None) => Cow::from(i18n::t("settings.code.status.indexing")), }; IndexingStatusPresentation { @@ -1887,21 +1923,21 @@ impl CodePageWidget { } } RemoteCodebaseIndexState::Ready => IndexingStatusPresentation { - text: Cow::from("Synced"), + text: Cow::from(i18n::t("settings.code.status.synced")), color: theme.ansi_fg_green(), icon: Some(Icon::Check), refresh_action: Some(IndexingRefreshAction::Resync), show_delete: true, }, RemoteCodebaseIndexState::Stale => IndexingStatusPresentation { - text: Cow::from("Stale"), + text: Cow::from(i18n::t("settings.code.status.stale")), color: theme.nonactive_ui_detail().into_solid(), icon: Some(Icon::ClockRefresh), refresh_action: Some(IndexingRefreshAction::Resync), show_delete: true, }, RemoteCodebaseIndexState::Failed => IndexingStatusPresentation { - text: Cow::from("Failed"), + text: Cow::from(i18n::t("settings.code.status.failed")), color: theme.ui_error_color(), icon: Some(Icon::AlertTriangle), refresh_action: Some(IndexingRefreshAction::Resync), @@ -2056,7 +2092,7 @@ impl CodePageWidget { // "LSP SERVERS" label content.add_child( ui_builder - .span("LSP SERVERS") + .span(i18n::t("settings.code.section.lsp_servers")) .with_style(UiComponentStyles { font_size: Some(11.0), font_weight: Some(Weight::Semibold), @@ -2169,10 +2205,16 @@ impl CodePageWidget { ); let (description, is_installing) = match &repo_status { - Some(LspRepoStatus::DisabledAndInstalled { .. }) => ("Installed", false), - Some(LspRepoStatus::Installing { .. }) => ("Installing...", true), - Some(LspRepoStatus::CheckingForInstallation) => ("Checking...", true), - _ => ("Available for download", false), + Some(LspRepoStatus::DisabledAndInstalled { .. }) => { + (i18n::t("settings.code.lsp.installed"), false) + } + Some(LspRepoStatus::Installing { .. }) => { + (i18n::t("settings.code.lsp.installing"), true) + } + Some(LspRepoStatus::CheckingForInstallation) => { + (i18n::t("settings.code.lsp.checking"), true) + } + _ => (i18n::t("settings.code.lsp.available_for_download"), false), }; name_desc_column.add_child( @@ -2354,7 +2396,7 @@ impl CodePageWidget { background: Some(theme.surface_3().into()), ..Default::default() }) - .with_text_label("Restart server".to_owned()) + .with_text_label(i18n::t("settings.code.lsp.restart_server.button")) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { @@ -2385,7 +2427,7 @@ impl CodePageWidget { font_size: Some(12.), ..Default::default() }) - .with_text_label("View logs".to_owned()) + .with_text_label(i18n::t("settings.code.lsp.view_logs.button")) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { @@ -2441,26 +2483,30 @@ impl CodePageWidget { AnsiColorIdentifier::Green .to_ansi_color(&theme.terminal_colors().normal) .into(), - "Available", + Box::leak(i18n::t("settings.code.lsp.status.available").into_boxed_str()), ), LspState::Starting | LspState::Available { .. } => ( AnsiColorIdentifier::Yellow .to_ansi_color(&theme.terminal_colors().normal) .into(), - "Busy", + Box::leak(i18n::t("settings.code.lsp.status.busy").into_boxed_str()), ), LspState::Failed { .. } => ( AnsiColorIdentifier::Red .to_ansi_color(&theme.terminal_colors().normal) .into(), - "Failed", + Box::leak(i18n::t("settings.code.lsp.status.failed").into_boxed_str()), + ), + LspState::Stopped { .. } | LspState::Stopping { .. } => ( + theme.disabled_ui_text_color().into_solid(), + Box::leak(i18n::t("settings.code.lsp.status.stopped").into_boxed_str()), ), - LspState::Stopped { .. } | LspState::Stopping { .. } => { - (theme.disabled_ui_text_color().into_solid(), "Stopped") - } } } - None => (theme.disabled_ui_text_color().into_solid(), "Not running"), + None => ( + theme.disabled_ui_text_color().into_solid(), + Box::leak(i18n::t("settings.code.lsp.status.not_running").into_boxed_str()), + ), } } } @@ -2518,11 +2564,13 @@ impl SettingsWidget for CodebaseIndexingCategorizedWidget { .switch(self.inner.switch_state.clone()) .check(codebase_context_enabled); - let disabled_tooltip_text = match admin_setting { - AdminEnablementSetting::Enable => Some(INDEXING_WORKSPACE_ENABLED_ADMIN_TEXT), - AdminEnablementSetting::Disable => Some(INDEXING_DISABLED_ADMIN_TEXT), + let disabled_tooltip_text: Option = match admin_setting { + AdminEnablementSetting::Enable => Some(i18n::t("settings.code.tooltip.admin_enabled")), + AdminEnablementSetting::Disable => { + Some(i18n::t("settings.code.tooltip.admin_disabled")) + } AdminEnablementSetting::RespectUserSetting if !global_ai_enabled => { - Some(INDEXING_DISABLED_GLOBAL_AI_TEXT) + Some(i18n::t("settings.code.tooltip.ai_required")) } AdminEnablementSetting::RespectUserSetting => None, }; @@ -2546,13 +2594,13 @@ impl SettingsWidget for CodebaseIndexingCategorizedWidget { }; content.add_child(render_body_item::( - CODEBASE_INDEXING_LABEL.into(), + i18n::t("settings.code.codebase_indexing.label"), None, LocalOnlyIconState::Hidden, ToggleState::Enabled, appearance, toggle_element, - Some(CODEBASE_INDEX_DESCRIPTION.into()), + Some(i18n::t("settings.code.codebase_index.description")), )); // Auto-indexing toggle (only shown when codebase indexing is enabled) @@ -2560,7 +2608,7 @@ impl SettingsWidget for CodebaseIndexingCategorizedWidget { let auto_indexing_enabled = *CodeSettings::as_ref(app).auto_indexing_enabled; content.add_child(render_body_item::( - AUTO_INDEX_FEATURE_NAME.into(), + i18n::t("settings.code.auto_index.label"), None, LocalOnlyIconState::Hidden, ToggleState::Enabled, @@ -2573,13 +2621,13 @@ impl SettingsWidget for CodebaseIndexingCategorizedWidget { ctx.dispatch_typed_action(CodeSettingsPageAction::ToggleAutoIndexing); }) .finish(), - Some(AUTO_INDEX_DESCRIPTION.into()), + Some(i18n::t("settings.code.auto_index.description")), )); if !CodebaseIndexManager::as_ref(app).can_create_new_indices() { content.add_child( ui_builder - .paragraph(CODEBASE_INDEX_LIMIT_REACHED) + .paragraph(i18n::t("settings.code.index_limit_reached")) .with_style(UiComponentStyles { font_color: Some(appearance.theme().disabled_ui_text_color().into()), ..Default::default() @@ -2658,7 +2706,7 @@ impl SettingsWidget for AutoOpenCodeReviewPaneCodeWidget { ) -> Box { let general_settings = GeneralSettings::as_ref(app); render_body_item::( - "Auto open code review panel".into(), + i18n::t("settings.code.auto_open_review_panel.label"), None, LocalOnlyIconState::Hidden, ToggleState::Enabled, @@ -2672,7 +2720,7 @@ impl SettingsWidget for AutoOpenCodeReviewPaneCodeWidget { ctx.dispatch_typed_action(CodeSettingsPageAction::ToggleAutoOpenCodeReviewPane); }) .finish(), - Some("When this setting is on, the code review panel will open on the first accepted diff of a conversation".into()), + Some(i18n::t("settings.code.auto_open_review_panel.description")), ) } } @@ -2735,7 +2783,7 @@ impl SettingsWidget for CodeReviewPanelToggleWidget { let tab_settings = TabSettings::as_ref(app); render_body_item::( - "Show code review button".into(), + i18n::t("settings.code.show_review_button.label"), None, LocalOnlyIconState::Hidden, ToggleState::Enabled, @@ -2749,10 +2797,7 @@ impl SettingsWidget for CodeReviewPanelToggleWidget { ctx.dispatch_typed_action(CodeSettingsPageAction::ToggleCodeReviewPanel); }) .finish(), - Some( - "Show a button in the top right of the window to toggle the code review panel." - .into(), - ), + Some(i18n::t("settings.code.show_review_button.description")), ) } } @@ -2778,7 +2823,7 @@ impl SettingsWidget for CodeReviewDiffStatsToggleWidget { let tab_settings = TabSettings::as_ref(app); render_body_item::( - "Show diff stats on code review button".into(), + i18n::t("settings.code.show_diff_stats.label"), None, LocalOnlyIconState::Hidden, ToggleState::Enabled, @@ -2794,7 +2839,7 @@ impl SettingsWidget for CodeReviewDiffStatsToggleWidget { ); }) .finish(), - Some("Show lines added and removed counts on the code review button.".into()), + Some(i18n::t("settings.code.show_diff_stats.description")), ) } } @@ -2820,7 +2865,7 @@ impl SettingsWidget for ProjectExplorerToggleWidget { let code_settings = CodeSettings::as_ref(app); render_body_item::( - "Project explorer".into(), + i18n::t("settings.code.project_explorer.label"), None, LocalOnlyIconState::Hidden, ToggleState::Enabled, @@ -2834,10 +2879,7 @@ impl SettingsWidget for ProjectExplorerToggleWidget { ctx.dispatch_typed_action(CodeSettingsPageAction::ToggleProjectExplorer); }) .finish(), - Some( - "Adds an IDE-style project explorer / file tree to the left side tools panel." - .into(), - ), + Some(i18n::t("settings.code.project_explorer.description")), ) } } @@ -2863,7 +2905,7 @@ impl SettingsWidget for GlobalSearchToggleWidget { let code_settings = CodeSettings::as_ref(app); render_body_item::( - "Global file search".into(), + i18n::t("settings.code.global_search.label"), None, LocalOnlyIconState::Hidden, ToggleState::Enabled, @@ -2877,7 +2919,7 @@ impl SettingsWidget for GlobalSearchToggleWidget { ctx.dispatch_typed_action(CodeSettingsPageAction::ToggleGlobalSearch); }) .finish(), - Some("Adds global file search to the left side tools panel.".into()), + Some(i18n::t("settings.code.global_search.description")), ) } } diff --git a/app/src/settings_view/custom_inference_modal.rs b/app/src/settings_view/custom_inference_modal.rs index 97f98239c3..765d37bc8e 100644 --- a/app/src/settings_view/custom_inference_modal.rs +++ b/app/src/settings_view/custom_inference_modal.rs @@ -102,7 +102,10 @@ impl CustomEndpointModal { ..Default::default() }; let mut editor = EditorView::single_line(options, ctx); - editor.set_placeholder_text("e.g., Zach's external models", ctx); + editor.set_placeholder_text( + i18n::t("settings.custom_inference.endpoint_name_placeholder"), + ctx, + ); if let Some(ep) = endpoint { editor.set_buffer_text(&ep.name, ctx); } @@ -122,7 +125,8 @@ impl CustomEndpointModal { ..Default::default() }; let mut editor = EditorView::single_line(options, ctx); - editor.set_placeholder_text("Please include 'https://'", ctx); + editor + .set_placeholder_text(i18n::t("settings.ai.custom_endpoint.url_placeholder"), ctx); if let Some(ep) = endpoint { editor.set_buffer_text(&ep.url, ctx); } @@ -143,7 +147,10 @@ impl CustomEndpointModal { ..Default::default() }; let mut editor = EditorView::single_line(options, ctx); - editor.set_placeholder_text("e.g., sk-...", ctx); + editor.set_placeholder_text( + i18n::t("settings.custom_inference.api_key_placeholder"), + ctx, + ); if let Some(ep) = endpoint { editor.set_buffer_text(&ep.api_key, ctx); } @@ -197,7 +204,7 @@ impl CustomEndpointModal { }); } let remove_endpoint_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Remove", DangerSecondaryTheme) + ActionButton::new(i18n::t("common.remove"), DangerSecondaryTheme) .with_icon(Icon::Trash) .on_click(|ctx| { ctx.dispatch_typed_action(CustomEndpointModalAction::RemoveEndpoint); @@ -239,7 +246,10 @@ impl CustomEndpointModal { ..Default::default() }; let mut editor = EditorView::single_line(options, ctx); - editor.set_placeholder_text("e.g., GLM-5-FP8", ctx); + editor.set_placeholder_text( + i18n::t("settings.custom_inference.model_name_placeholder"), + ctx, + ); if let Some(n) = name { editor.set_buffer_text(n, ctx); } @@ -259,7 +269,10 @@ impl CustomEndpointModal { ..Default::default() }; let mut editor = EditorView::single_line(options, ctx); - editor.set_placeholder_text("e.g., GLM-5", ctx); + editor.set_placeholder_text( + i18n::t("settings.custom_inference.model_alias_placeholder"), + ctx, + ); if let Some(a) = alias { editor.set_buffer_text(a, ctx); } @@ -596,7 +609,7 @@ impl View for CustomEndpointModal { let label_font_family = appearance.ui_font_family(); let label_text_color = theme.active_ui_text_color().into(); - let label = move |text: &'static str| { + let label = move |text: String| { Text::new(text, label_font_family, LABEL_FONT_SIZE) .with_color(label_text_color) .finish() @@ -618,7 +631,7 @@ impl View for CustomEndpointModal { column.add_child( Container::new( Text::new( - "Provide your endpoint details below. You can add as many models from the endpoint as you'd like and can also provide aliases for the model picker in your input.", + i18n::t("settings.custom_inference.endpoint_details_help"), appearance.ui_font_family(), LABEL_FONT_SIZE, ) @@ -632,7 +645,7 @@ impl View for CustomEndpointModal { // Endpoint name column.add_child( - Container::new(label("Endpoint name")) + Container::new(label(i18n::t("settings.custom_inference.endpoint_name"))) .with_margin_bottom(4.) .finish(), ); @@ -651,7 +664,7 @@ impl View for CustomEndpointModal { // Endpoint URL column.add_child( - Container::new(label("Endpoint URL")) + Container::new(label(i18n::t("settings.custom_inference.endpoint_url"))) .with_margin_bottom(4.) .finish(), ); @@ -677,7 +690,7 @@ impl View for CustomEndpointModal { // API key column.add_child( - Container::new(label("API key")) + Container::new(label(i18n::t("settings.custom_inference.api_key"))) .with_margin_bottom(4.) .finish(), ); @@ -701,12 +714,12 @@ impl View for CustomEndpointModal { .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_spacing(MODEL_ROW_SPACING) .with_child( - ConstrainedBox::new(label("Model name")) + ConstrainedBox::new(label(i18n::t("settings.custom_inference.model_name"))) .with_width(MODEL_INPUT_WIDTH) .finish(), ) .with_child( - ConstrainedBox::new(label("Model alias (optional)")) + ConstrainedBox::new(label(i18n::t("settings.custom_inference.model_alias"))) .with_width(MODEL_INPUT_WIDTH) .finish(), ); @@ -785,7 +798,7 @@ impl View for CustomEndpointModal { ButtonVariant::Secondary, self.add_model_button_mouse_state.clone(), ) - .with_text_label("+ Add model".to_string()) + .with_text_label(i18n::t("settings.custom_inference.add_model")) .with_style(UiComponentStyles { font_size: Some(14.), padding: Some(Coords::uniform(6.).left(8.).right(8.)), @@ -820,7 +833,7 @@ impl View for CustomEndpointModal { ButtonVariant::Secondary, self.cancel_button_mouse_state.clone(), ) - .with_text_label("Cancel".to_string()) + .with_text_label(i18n::t("common.cancel")) .with_style(button_style) .build() .on_click(move |ctx, _, _| { @@ -833,9 +846,9 @@ impl View for CustomEndpointModal { .ui_builder() .button(ButtonVariant::Accent, self.save_button_mouse_state.clone()) .with_text_label(if is_editing { - "Save".to_string() + i18n::t("common.save") } else { - "Add endpoint".to_string() + i18n::t("settings.custom_inference.add_endpoint") }) .with_style(button_style); if !is_valid { @@ -861,19 +874,24 @@ impl View for CustomEndpointModal { } } -fn validate_url(url: &str) -> Result<(), &'static str> { +fn validate_url(url: &str) -> Result<(), String> { if url.trim().is_empty() { return Ok(()); } - let parsed = Url::parse(url).map_err(|_| "Invalid URL")?; + let parsed = + Url::parse(url).map_err(|_| i18n::t("settings.custom_inference.error.invalid_url"))?; if parsed.scheme() != "https" { - return Err("URL must use HTTPS"); + return Err(i18n::t( + "settings.custom_inference.error.url_https_required", + )); } let Some(host) = parsed.host_str().filter(|h| !h.is_empty()) else { - return Err("URL must include a host"); + return Err(i18n::t("settings.custom_inference.error.url_host_required")); }; if is_restricted_host(host) { - return Err("URL must not use a local or private host"); + return Err(i18n::t( + "settings.custom_inference.error.url_restricted_host", + )); } Ok(()) } diff --git a/app/src/settings_view/custom_inference_modal_tests.rs b/app/src/settings_view/custom_inference_modal_tests.rs index cad64c86a4..0374fb9150 100644 --- a/app/src/settings_view/custom_inference_modal_tests.rs +++ b/app/src/settings_view/custom_inference_modal_tests.rs @@ -1,5 +1,10 @@ use super::*; +fn assert_url_error(url: &str, key: &str) { + i18n::set_locale("en"); + assert_eq!(validate_url(url), Err(i18n::t(key))); +} + #[test] fn validate_url_accepts_https_with_host() { assert!(validate_url("https://api.example.com/v1").is_ok()); @@ -9,41 +14,44 @@ fn validate_url_accepts_https_with_host() { #[test] fn validate_url_rejects_http() { - assert_eq!( - validate_url("http://api.example.com/v1"), - Err("URL must use HTTPS") + assert_url_error( + "http://api.example.com/v1", + "settings.custom_inference.error.url_https_required", ); - assert_eq!( - validate_url("http://example.com"), - Err("URL must use HTTPS") + assert_url_error( + "http://example.com", + "settings.custom_inference.error.url_https_required", ); } #[test] fn validate_url_rejects_ftp_and_other_schemes() { - assert_eq!( - validate_url("ftp://files.example.com"), - Err("URL must use HTTPS") + assert_url_error( + "ftp://files.example.com", + "settings.custom_inference.error.url_https_required", ); - assert_eq!( - validate_url("file:///etc/passwd"), - Err("URL must use HTTPS") + assert_url_error( + "file:///etc/passwd", + "settings.custom_inference.error.url_https_required", ); - assert_eq!( - validate_url("ws://socket.example.com"), - Err("URL must use HTTPS") + assert_url_error( + "ws://socket.example.com", + "settings.custom_inference.error.url_https_required", ); } #[test] fn validate_url_rejects_malformed_strings() { - assert_eq!(validate_url("not a url"), Err("Invalid URL")); - assert_eq!(validate_url("https://"), Err("Invalid URL")); + assert_url_error("not a url", "settings.custom_inference.error.invalid_url"); + assert_url_error("https://", "settings.custom_inference.error.invalid_url"); } #[test] fn validate_url_rejects_empty_host() { - assert_eq!(validate_url("https://?query=1"), Err("Invalid URL")); + assert_url_error( + "https://?query=1", + "settings.custom_inference.error.invalid_url", + ); } #[test] @@ -58,19 +66,22 @@ fn validate_url_allows_whitespace_only() { #[test] fn validate_url_rejects_localhost_and_private_ips() { - let error = Err("URL must not use a local or private host"); - assert_eq!(validate_url("https://localhost:8080"), error); - assert_eq!(validate_url("https://127.0.0.1/v1"), error); - assert_eq!(validate_url("https://0.0.0.0/v1"), error); - assert_eq!(validate_url("https://10.0.0.1/v1"), error); - assert_eq!(validate_url("https://172.16.0.1/v1"), error); - assert_eq!(validate_url("https://192.168.0.1/v1"), error); - assert_eq!(validate_url("https://169.254.0.1/v1"), error); - assert_eq!(validate_url("https://[::1]/v1"), error); - assert_eq!(validate_url("https://[::]/v1"), error); - assert_eq!(validate_url("https://[fc00::1]/v1"), error); - assert_eq!(validate_url("https://[fe80::1]/v1"), error); - assert_eq!(validate_url("https://[::ffff:192.168.0.1]/v1"), error); + for url in [ + "https://localhost:8080", + "https://127.0.0.1/v1", + "https://0.0.0.0/v1", + "https://10.0.0.1/v1", + "https://172.16.0.1/v1", + "https://192.168.0.1/v1", + "https://169.254.0.1/v1", + "https://[::1]/v1", + "https://[::]/v1", + "https://[fc00::1]/v1", + "https://[fe80::1]/v1", + "https://[::ffff:192.168.0.1]/v1", + ] { + assert_url_error(url, "settings.custom_inference.error.url_restricted_host"); + } } #[test] diff --git a/app/src/settings_view/delete_environment_confirmation_dialog.rs b/app/src/settings_view/delete_environment_confirmation_dialog.rs index 5f543937e5..1b1a420e90 100644 --- a/app/src/settings_view/delete_environment_confirmation_dialog.rs +++ b/app/src/settings_view/delete_environment_confirmation_dialog.rs @@ -33,13 +33,17 @@ pub struct DeleteEnvironmentConfirmationDialog { impl DeleteEnvironmentConfirmationDialog { pub fn new(ctx: &mut ViewContext) -> Self { let cancel_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Cancel", NakedTheme).on_click(|ctx| { + ActionButton::new(i18n::t("common.cancel"), NakedTheme).on_click(|ctx| { ctx.dispatch_typed_action(DeleteEnvironmentConfirmationDialogAction::Cancel); }) }); let confirm_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Delete environment", DangerPrimaryTheme).on_click(|ctx| { + ActionButton::new( + i18n::t("settings.environments.button.delete_environment"), + DangerPrimaryTheme, + ) + .on_click(|ctx| { ctx.dispatch_typed_action(DeleteEnvironmentConfirmationDialogAction::Confirm); }) }); @@ -82,13 +86,11 @@ impl View for DeleteEnvironmentConfirmationDialog { let appearance = Appearance::as_ref(app); - let description = format!( - "Are you sure you want to remove the {} environment?", - self.env_name - ); + let description = i18n::t("settings.environments.delete_dialog_description") + .replace("{name}", &self.env_name); let dialog = Dialog::new( - "Delete environment?".to_string(), + i18n::t("settings.environments.delete_dialog_title"), Some(description), dialog_styles(appearance), ) diff --git a/app/src/settings_view/directory_color_add_picker.rs b/app/src/settings_view/directory_color_add_picker.rs index b9f4b6b3b2..31f632903b 100644 --- a/app/src/settings_view/directory_color_add_picker.rs +++ b/app/src/settings_view/directory_color_add_picker.rs @@ -24,8 +24,6 @@ use crate::workspace::tab_settings::{ DirectoryTabColor, DirectoryTabColors, TabSettings, TabSettingsChangedEvent, }; -const ADD_DIRECTORY_LABEL: &str = "+ Add directory…"; -const BUTTON_LABEL: &str = "Add directory color"; const MENU_WIDTH: f32 = 340.; /// A dropdown used by the Directory tab colors settings widget, with a button fallback @@ -108,18 +106,21 @@ impl DirectoryColorAddPicker { }); let button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new(BUTTON_LABEL, SecondaryTheme) - .with_icon(icons::Icon::Plus) - .on_click(|ctx| { - ctx.dispatch_typed_action(DirectoryColorAddPickerAction::AddNewDirectory); - }) + ActionButton::new( + i18n::t("settings.directory_color.add_button"), + SecondaryTheme, + ) + .with_icon(icons::Icon::Plus) + .on_click(|ctx| { + ctx.dispatch_typed_action(DirectoryColorAddPickerAction::AddNewDirectory); + }) }); let dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = FilterableDropdown::new(ctx); dropdown.set_top_bar_max_width(MENU_WIDTH); dropdown.set_menu_width(MENU_WIDTH, ctx); - dropdown.set_menu_header_to_static(BUTTON_LABEL); + dropdown.set_menu_header_to_static(i18n::t("settings.directory_color.add_button")); dropdown }); @@ -156,7 +157,7 @@ impl DirectoryColorAddPicker { .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Text::new_inline( - ADD_DIRECTORY_LABEL, + i18n::t("settings.directory_color.add_directory"), font_family, font_size, ) diff --git a/app/src/settings_view/environments_page.rs b/app/src/settings_view/environments_page.rs index 8426530366..99f67b6be9 100644 --- a/app/src/settings_view/environments_page.rs +++ b/app/src/settings_view/environments_page.rs @@ -79,8 +79,6 @@ use { warp_graphql::queries::user_github_info::UserGithubInfoResult, }; -const PAGE_TITLE_TEXT: &str = "Environments"; -const PAGE_DESCRIPTION_TEXT: &str = "Environments define where your ambient agents run. Set one up in minutes via GitHub (recommended), Warp-assisted setup, or manual configuration."; const CARD_BORDER_WIDTH: f32 = 1.; const CARD_PADDING: f32 = 16.; const CARD_SPACING: f32 = 12.; @@ -95,9 +93,9 @@ const TOOLBAR_SEARCH_MAX_WIDTH: f32 = 420.; struct EmptyStateRowConfig { icon: Icon, - title: &'static str, - badge: Option<&'static str>, - subtitle: &'static str, + title: String, + badge: Option, + subtitle: String, action_button: Box, compact_action_button: Box, icon_size: f32, @@ -188,16 +186,18 @@ impl EnvironmentDisplayData { fn format_timestamp_text(&self) -> String { let last_edited_part = self.last_edited_ts.map(|ts| { format!( - "Last edited: {}", + "{}{}", + i18n::t("settings.environments.card.last_edited_prefix"), format_approx_duration_from_now_utc(ts.utc()) ) }); let last_used_part = match self.last_used_ts { Some(ts) => format!( - "Last used: {}", + "{}{}", + i18n::t("settings.environments.card.last_used_prefix"), format_approx_duration_from_now_utc(ts.utc()) ), - None => "Last used: never".to_string(), + None => i18n::t("settings.environments.card.last_used_never"), }; match last_edited_part { Some(edited) => format!("{} · {}", edited, last_used_part), @@ -303,10 +303,11 @@ impl EnvironmentsPageView { } fn create_single_line_editor( - placeholder: &'static str, + placeholder: impl Into, ctx: &mut ViewContext, ) -> ViewHandle { - let editor = ctx.add_typed_action_view(|ctx| { + let placeholder = placeholder.into(); + let editor = ctx.add_typed_action_view(move |ctx| { let appearance = Appearance::as_ref(ctx); let options = SingleLineEditorOptions { text: TextOptions { @@ -370,7 +371,10 @@ impl EnvironmentsPageView { }); // Create search editor for list page - let search_editor = Self::create_single_line_editor("Search environments...", ctx); + let search_editor = Self::create_single_line_editor( + i18n::t("settings.environments.search.placeholder"), + ctx, + ); ctx.subscribe_to_view(&search_editor, |me, _, event, ctx| match event { crate::editor::Event::Edited(_) => { me.search_query = me.search_editor.as_ref(ctx).buffer_text(ctx); @@ -506,8 +510,11 @@ impl EnvironmentsPageView { } // Create pane configuration for BackingView support - let pane_configuration = - ctx.add_model(|_| crate::pane_group::pane::PaneConfiguration::new("Environments")); + let pane_configuration = ctx.add_model(|_| { + crate::pane_group::pane::PaneConfiguration::new(i18n::t( + "settings.environments.page.title", + )) + }); let mut view = Self { page: PageType::new_monolith( @@ -633,7 +640,7 @@ impl EnvironmentsPageView { if should_handle { self.pending_save_env_id = None; - self.show_success_toast("Successfully updated environment".to_string(), ctx); + self.show_success_toast(i18n::t("settings.environments.toast.updated"), ctx); // No need to force a global cloud-object refresh here: on update success the // sync pipeline updates this environment's `revision_ts` (used for "Last edited") @@ -651,7 +658,7 @@ impl EnvironmentsPageView { if let Some(result_client_id) = &result.client_id { if *result_client_id == pending_client_id { self.show_success_toast( - "Successfully created environment".to_string(), + i18n::t("settings.environments.toast.created"), ctx, ); } @@ -668,7 +675,7 @@ impl EnvironmentsPageView { if let Some(server_id) = &result.server_id { if server_id.uid() == pending_env_id.uid() { self.show_success_toast( - "Environment deleted successfully".to_string(), + i18n::t("settings.environments.toast.deleted"), ctx, ); } @@ -691,9 +698,9 @@ impl EnvironmentsPageView { self.pending_share_server_id = None; if matches!(result.success_type, OperationSuccessType::Success) { - self.show_success_toast("Successfully shared environment".to_string(), ctx); + self.show_success_toast(i18n::t("settings.environments.toast.shared"), ctx); } else { - self.show_error_toast("Failed to share environment with team".to_string(), ctx); + self.show_error_toast(i18n::t("settings.environments.toast.share_failed"), ctx); } ctx.notify(); @@ -763,7 +770,7 @@ impl EnvironmentsPageView { let Some(owner) = owner else { self.show_error_toast( - "Unable to create environment: not logged in.".to_string(), + i18n::t("settings.environments.toast.create_not_logged_in"), ctx, ); return; @@ -790,7 +797,7 @@ impl EnvironmentsPageView { let Some(existing_env) = CloudAmbientAgentEnvironment::get_by_id(env_id, ctx) else { self.show_error_toast( - "Unable to save: environment no longer exists.".to_string(), + i18n::t("settings.environments.toast.save_not_found"), ctx, ); return; @@ -959,7 +966,7 @@ impl TypedActionView for EnvironmentsPageView { EnvironmentsPageAction::ShareToTeam(env_id) => { let Some(team_uid) = UserWorkspaces::as_ref(ctx).current_team_uid() else { self.show_error_toast( - "Unable to share environment: you are not currently on a team.".to_string(), + i18n::t("settings.environments.toast.share_no_team"), ctx, ); return; @@ -967,7 +974,7 @@ impl TypedActionView for EnvironmentsPageView { let SyncId::ServerId(server_id) = *env_id else { self.show_error_toast( - "Unable to share environment: environment is not yet synced.".to_string(), + i18n::t("settings.environments.toast.share_not_synced"), ctx, ); return; @@ -1070,7 +1077,7 @@ impl EnvironmentsPageWidget { // Page title + description let title = Text::new( - PAGE_TITLE_TEXT, + i18n::t("settings.environments.page.title"), appearance.ui_font_family(), appearance.ui_font_size() * 1.5, ) @@ -1080,7 +1087,7 @@ impl EnvironmentsPageWidget { let description = appearance .ui_builder() - .paragraph(PAGE_DESCRIPTION_TEXT) + .paragraph(i18n::t("settings.environments.page.description")) .with_style(UiComponentStyles { font_color: Some(appearance.theme().nonactive_ui_text_color().into()), font_size: Some(CONTENT_FONT_SIZE), @@ -1284,7 +1291,7 @@ impl EnvironmentsPageWidget { let theme = appearance.theme(); Container::new( Text::new( - "No environments match your search.", + i18n::t("settings.environments.search.no_matches"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -1310,12 +1317,23 @@ impl EnvironmentsPageWidget { const HEADER_TO_LIST_SPACING: f32 = 8.; let header = match list_scope { - EnvironmentListScope::Personal => Self::render_overline_header("Personal", appearance), + EnvironmentListScope::Personal => { + let personal_label = i18n::t("settings.environments.section.personal"); + Self::render_overline_header(&personal_label, appearance) + } EnvironmentListScope::Team => { let shared_by_text = UserWorkspaces::as_ref(app) .current_team() - .map(|team| format!("Shared by Warp and {}", team.name)) - .unwrap_or_else(|| "Shared by Warp and your team".to_string()); + .map(|team| { + format!( + "{}{}", + i18n::t("settings.environments.section.shared_by_team_prefix"), + team.name + ) + }) + .unwrap_or_else(|| { + i18n::t("settings.environments.section.shared_by_your_team") + }); Self::render_overline_header(&shared_by_text, appearance) } }; @@ -1399,18 +1417,24 @@ impl EnvironmentsPageWidget { }; let (github_button_label, github_button_enabled) = if dropdown_state.is_loading { - ("Loading...", false) + (i18n::t("settings.environments.empty.button.loading"), false) } else if dropdown_state.load_error_message.is_some() { - ("Retry", true) + (i18n::t("settings.environments.empty.button.retry"), true) } else if dropdown_state.auth_url.is_some() { - ("Authorize", true) + ( + i18n::t("settings.environments.empty.button.authorize"), + true, + ) } else { - ("Get started", true) + ( + i18n::t("settings.environments.empty.button.get_started"), + true, + ) }; let github_button = Self::render_empty_state_button( appearance, - github_button_label, + &github_button_label, ButtonVariant::Accent, view.empty_state_github_repos_button_mouse_state.clone(), github_button_enabled, @@ -1418,16 +1442,17 @@ impl EnvironmentsPageWidget { ); let github_button_compact = Self::render_empty_state_button( appearance, - github_button_label, + &github_button_label, ButtonVariant::Accent, view.empty_state_github_repos_button_mouse_state.clone(), github_button_enabled, github_button_action, ); + let launch_agent_label = i18n::t("settings.environments.empty.button.launch_agent"); let local_repos_button = Self::render_empty_state_button( appearance, - "Launch agent", + &launch_agent_label, ButtonVariant::Secondary, view.empty_state_local_repos_button_mouse_state.clone(), true, @@ -1435,7 +1460,7 @@ impl EnvironmentsPageWidget { ); let local_repos_button_compact = Self::render_empty_state_button( appearance, - "Launch agent", + &launch_agent_label, ButtonVariant::Secondary, view.empty_state_local_repos_button_mouse_state.clone(), true, @@ -1446,10 +1471,9 @@ impl EnvironmentsPageWidget { appearance, EmptyStateRowConfig { icon: Icon::Github, - title: "Quick setup", - badge: Some("Suggested"), - subtitle: - "Select the GitHub repositories you’d like to work with and we’ll suggest a base image and config", + title: i18n::t("settings.environments.empty.github.title"), + badge: Some(i18n::t("settings.environments.empty.github.badge")), + subtitle: i18n::t("settings.environments.empty.github.subtitle"), action_button: github_button, compact_action_button: github_button_compact, icon_size, @@ -1460,10 +1484,9 @@ impl EnvironmentsPageWidget { appearance, EmptyStateRowConfig { icon: Icon::Terminal, - title: "Use the agent", + title: i18n::t("settings.environments.empty.agent.title"), badge: None, - subtitle: - "Choose a locally set up project and we’ll help you set up an environment based on it", + subtitle: i18n::t("settings.environments.empty.agent.subtitle"), action_button: local_repos_button, compact_action_button: local_repos_button_compact, icon_size, @@ -1482,7 +1505,7 @@ impl EnvironmentsPageWidget { .finish(); let header = Text::new( - "You haven’t set up any environments yet.", + i18n::t("settings.environments.empty.header"), appearance.ui_font_family(), appearance.ui_font_size() * 1.1, ) @@ -1491,7 +1514,7 @@ impl EnvironmentsPageWidget { .finish(); let subheader = Text::new( - "Choose how you’d like to set up your environment:", + i18n::t("settings.environments.empty.subheader"), appearance.ui_font_family(), appearance.ui_font_size() * 0.95, ) @@ -1558,7 +1581,7 @@ impl EnvironmentsPageWidget { .with_spacing(6.) .with_child( Text::new( - title, + title.clone(), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -1567,10 +1590,10 @@ impl EnvironmentsPageWidget { .finish(), ); - if let Some(badge) = badge { + if let Some(badge) = &badge { let badge = Container::new( Text::new( - badge, + badge.clone(), appearance.ui_font_family(), appearance.ui_font_size() * 0.85, ) @@ -1591,7 +1614,7 @@ impl EnvironmentsPageWidget { .with_child(title_row.finish()) .with_child( Text::new( - subtitle, + subtitle.clone(), appearance.ui_font_family(), appearance.ui_font_size() * 0.9, ) @@ -1756,13 +1779,17 @@ impl EnvironmentsPageWidget { // since it returns a Box that can only be consumed once let env_id_str_copy = env_id_str.clone(); let env_id_with_copy = render_copyable_text_field( - CopyableTextFieldConfig::new(format!("Env ID: {}", env_id_str.clone())) - .with_font_size(appearance.ui_font_size() * 0.9) - .with_text_color(blended_colors::text_sub(theme, theme.surface_1())) - .with_icon_size(12.) - .with_mouse_state(copy_button_mouse_state.clone()) - .with_last_copied_at(last_copied_at.as_ref()) - .with_copy_button_placement(CopyButtonPlacement::NextToText), + CopyableTextFieldConfig::new(format!( + "{}{}", + i18n::t("settings.environments.card.env_id_prefix"), + env_id_str.clone() + )) + .with_font_size(appearance.ui_font_size() * 0.9) + .with_text_color(blended_colors::text_sub(theme, theme.surface_1())) + .with_icon_size(12.) + .with_mouse_state(copy_button_mouse_state.clone()) + .with_last_copied_at(last_copied_at.as_ref()) + .with_copy_button_placement(CopyButtonPlacement::NextToText), move |ctx| { ctx.dispatch_typed_action(EnvironmentsPageAction::CopyEnvId( env_id, @@ -1811,7 +1838,11 @@ impl EnvironmentsPageWidget { } } - let mut details_parts = vec![format!("Image: {}", env_docker_image)]; + let mut details_parts = vec![format!( + "{}{}", + i18n::t("settings.environments.card.image_prefix"), + env_docker_image + )]; if !env_github_repos.is_empty() { let repos_text = env_github_repos @@ -1819,12 +1850,20 @@ impl EnvironmentsPageWidget { .map(|(owner, repo)| format!("{}/{}", owner, repo)) .collect::>() .join(", "); - details_parts.push(format!("Repos: {}", repos_text)); + details_parts.push(format!( + "{}{}", + i18n::t("settings.environments.card.repos_prefix"), + repos_text + )); } if !env_setup_commands.is_empty() { let commands_text = env_setup_commands.join(", "); - details_parts.push(format!("Setup commands: {}", commands_text)); + details_parts.push(format!( + "{}{}", + i18n::t("settings.environments.card.setup_commands_prefix"), + commands_text + )); } // Create details section with Env ID on first line and other details below @@ -1856,7 +1895,7 @@ impl EnvironmentsPageWidget { let view_runs_link = appearance .ui_builder() .link( - "View my runs".to_string(), + i18n::t("settings.environments.card.view_my_runs"), None, Some(Box::new(move |ctx| { ctx.dispatch_typed_action(WorkspaceAction::ViewAgentRunsForEnvironment { @@ -1936,7 +1975,7 @@ impl EnvironmentsPageWidget { ) .with_tooltip(move || { share_ui_builder - .tool_tip("Share".to_string()) + .tool_tip(i18n::t("settings.environments.card.share_tooltip")) .build() .finish() }) @@ -1972,7 +2011,7 @@ impl EnvironmentsPageWidget { if is_card_hovered { edit_button = edit_button.with_tooltip(move || { edit_ui_builder - .tool_tip("Edit".to_string()) + .tool_tip(i18n::t("settings.environments.card.edit_tooltip")) .build() .finish() }); @@ -2071,7 +2110,7 @@ impl BackingView for EnvironmentsPageView { _ctx: &HeaderRenderContext<'_>, _app: &AppContext, ) -> HeaderContent { - HeaderContent::simple("Environments") + HeaderContent::simple(i18n::t("settings.environments.page.title")) } fn set_focus_handle(&mut self, focus_handle: PaneFocusHandle, _ctx: &mut ViewContext) { diff --git a/app/src/settings_view/environments_page/new_environment_button.rs b/app/src/settings_view/environments_page/new_environment_button.rs index f378d0dc80..f156fc3b88 100644 --- a/app/src/settings_view/environments_page/new_environment_button.rs +++ b/app/src/settings_view/environments_page/new_environment_button.rs @@ -96,7 +96,7 @@ impl View for NewEnvironmentButtonView { .with_spacing(4.) .with_child( Text::new( - "New environment", + i18n::t("agent_input_footer.new_environment"), appearance.ui_font_family(), appearance.ui_font_size(), ) diff --git a/app/src/settings_view/execution_profile_view.rs b/app/src/settings_view/execution_profile_view.rs index e34ec58f4f..1624bc12ba 100644 --- a/app/src/settings_view/execution_profile_view.rs +++ b/app/src/settings_view/execution_profile_view.rs @@ -53,7 +53,7 @@ impl ExecutionProfileView { }); let edit_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Edit", SecondaryTheme) + ActionButton::new(i18n::t("common.edit"), SecondaryTheme) .with_icon(Icon::Pencil) .with_size(ButtonSize::Small) .on_click(|ctx| { @@ -117,14 +117,14 @@ impl View for ExecutionProfileView { .as_ref() .and_then(|id| llm_preferences.get_llm_info(id)) .map(|info| info.display_name.clone()) - .unwrap_or_else(|| "Auto".to_string()); + .unwrap_or_else(|| i18n::t("settings.execution_profile.auto")); let computer_use_model = profile .computer_use_model .as_ref() .and_then(|id| llm_preferences.get_llm_info(id)) .map(|info| info.display_name.clone()) - .unwrap_or_else(|| "Auto".to_string()); + .unwrap_or_else(|| i18n::t("settings.execution_profile.auto")); Container::new( Flex::column() @@ -150,9 +150,13 @@ impl View for ExecutionProfileView { let mut model_flex = Flex::column(); model_flex.add_child( Container::new( - Text::new("MODELS", appearance.ui_font_family(), 10.) - .with_color(appearance.theme().disabled_ui_text_color().into()) - .finish(), + Text::new( + i18n::t("settings.execution_profile.models"), + appearance.ui_font_family(), + 10., + ) + .with_color(appearance.theme().disabled_ui_text_color().into()) + .finish(), ) .with_margin_bottom(8.) .finish(), @@ -160,7 +164,7 @@ impl View for ExecutionProfileView { model_flex.add_child(with_standard_vertical_margin( render_model_line_with_icon( Icon::Lightning, - "Base model:", + label_with_separator("ai.execution_profiles.editor.base_model.label"), base_model, appearance, is_any_ai_enabled, @@ -169,7 +173,7 @@ impl View for ExecutionProfileView { model_flex.add_child(with_standard_vertical_margin( render_model_line_with_icon( Icon::Terminal, - "Full terminal use:", + label_with_separator("settings.execution_profile.full_terminal_use"), cli_agent_model, appearance, is_any_ai_enabled, @@ -179,7 +183,9 @@ impl View for ExecutionProfileView { model_flex.add_child(with_standard_vertical_margin( render_model_line_with_icon( Icon::Laptop, - "Computer use:", + label_with_separator( + "ai.execution_profiles.editor.permission.computer_use", + ), computer_use_model, appearance, is_any_ai_enabled, @@ -196,11 +202,13 @@ impl View for ExecutionProfileView { let mut permissions_column = Flex::column() .with_child( Container::new( - Text::new("PERMISSIONS", appearance.ui_font_family(), 10.) - .with_color( - appearance.theme().disabled_ui_text_color().into(), - ) - .finish(), + Text::new( + i18n::t("settings.execution_profile.permissions"), + appearance.ui_font_family(), + 10., + ) + .with_color(appearance.theme().disabled_ui_text_color().into()) + .finish(), ) .with_margin_bottom(8.) .finish(), @@ -208,7 +216,9 @@ impl View for ExecutionProfileView { .with_child(with_standard_vertical_margin( render_action_permission_line_with_icon( Icon::Code2, - "Apply code diffs:", + label_with_separator( + "ai.execution_profiles.editor.permission.apply_code_diffs", + ), &profile.apply_code_diffs, appearance, is_any_ai_enabled, @@ -217,7 +227,9 @@ impl View for ExecutionProfileView { .with_child(with_standard_vertical_margin( render_action_permission_line_with_icon( Icon::Notebook, - "Read files:", + label_with_separator( + "ai.execution_profiles.editor.permission.read_files", + ), &profile.read_files, appearance, is_any_ai_enabled, @@ -237,7 +249,9 @@ impl View for ExecutionProfileView { permissions_column.add_child(with_standard_vertical_margin( render_action_permission_line_with_icon( Icon::Terminal, - "Execute commands:", + label_with_separator( + "ai.execution_profiles.editor.permission.execute_commands", + ), &profile.execute_commands, appearance, is_any_ai_enabled, @@ -276,7 +290,9 @@ impl View for ExecutionProfileView { permissions_column.add_child(with_standard_vertical_margin( render_write_to_pty_permission_line_with_icon( Icon::Workflow, - "Interact with running commands:", + label_with_separator( + "ai.execution_profiles.editor.permission.interact_with_running_commands", + ), &profile.write_to_pty, appearance, is_any_ai_enabled, @@ -287,7 +303,9 @@ impl View for ExecutionProfileView { permissions_column.add_child(with_standard_vertical_margin( render_computer_use_permission_line_with_icon( Icon::Laptop, - "Computer use:", + label_with_separator( + "ai.execution_profiles.editor.permission.computer_use", + ), &profile.computer_use, appearance, is_any_ai_enabled, @@ -298,7 +316,9 @@ impl View for ExecutionProfileView { permissions_column.add_child(with_standard_vertical_margin( render_ask_user_question_permission_line_with_icon( Icon::MessageText, - "Ask questions:", + label_with_separator( + "ai.execution_profiles.editor.permission.ask_questions", + ), &profile.ask_user_question, appearance, is_any_ai_enabled, @@ -307,7 +327,7 @@ impl View for ExecutionProfileView { permissions_column.add_child(with_standard_vertical_margin( render_run_agents_permission_line_with_icon( Icon::Workflow, - "Run agents:", + label_with_separator("settings.execution_profile.run_agents"), &profile.run_agents, appearance, is_any_ai_enabled, @@ -317,7 +337,9 @@ impl View for ExecutionProfileView { permissions_column.add_child(with_standard_vertical_margin( render_action_permission_line_with_icon( Icon::Dataflow, - "Call MCP servers:", + label_with_separator( + "ai.execution_profiles.editor.permission.call_mcp_servers", + ), &profile.mcp_permissions, appearance, is_any_ai_enabled, @@ -361,7 +383,9 @@ impl View for ExecutionProfileView { permissions_column.add_child(with_standard_vertical_margin( render_bool_permission_line_with_icon( Icon::Globe, - "Call web tools:", + label_with_separator( + "ai.execution_profiles.editor.web_search.label", + ), profile.web_search_enabled, appearance, is_any_ai_enabled, @@ -372,7 +396,9 @@ impl View for ExecutionProfileView { permissions_column.add_child(with_standard_vertical_margin( render_bool_permission_line_with_icon( Icon::Compass, - "Auto-sync plans to Warp Drive:", + label_with_separator( + "settings.execution_profile.autosync_plans_to_warp_drive", + ), profile.autosync_plans_to_warp_drive, appearance, is_any_ai_enabled, @@ -424,7 +450,7 @@ where let items_vec: Vec = items.into_iter().map(|item| item.to_string()).collect(); if items_vec.is_empty() { return Container::new( - Text::new("None", appearance.ui_font_family(), 12.) + Text::new(i18n::t("common.none"), appearance.ui_font_family(), 12.) .with_color(appearance.theme().disabled_ui_text_color().into()) .finish(), ) @@ -566,6 +592,14 @@ fn with_standard_vertical_margin(element: Box) -> Box .finish() } +fn label_with_separator(key: &str) -> String { + format!( + "{}{}", + i18n::t(key), + i18n::t("settings.execution_profile.label_separator") + ) +} + fn render_model_line_with_icon( icon: Icon, label: impl Into, @@ -695,10 +729,16 @@ fn render_action_permission_line_with_icon( is_ai_enabled: bool, ) -> Box { let permission_text = match permission { - ActionPermission::AgentDecides => "Agent decides", - ActionPermission::AlwaysAllow => "Always allow", - ActionPermission::AlwaysAsk => "Always ask", - ActionPermission::Unknown => "Unknown", + ActionPermission::AgentDecides => { + i18n::t("ai.execution_profiles.editor.permission.agent_decides") + } + ActionPermission::AlwaysAllow => { + i18n::t("ai.execution_profiles.editor.permission.always_allow") + } + ActionPermission::AlwaysAsk => { + i18n::t("ai.execution_profiles.editor.permission.always_ask") + } + ActionPermission::Unknown => i18n::t("settings.execution_profile.permission.unknown"), }; render_permission_line_with_icon(icon, label, permission_text, appearance, is_ai_enabled) } @@ -711,10 +751,16 @@ fn render_write_to_pty_permission_line_with_icon( is_ai_enabled: bool, ) -> Box { let permission_text = match permission { - WriteToPtyPermission::AlwaysAllow => "Always allow", - WriteToPtyPermission::AlwaysAsk => "Always ask", - WriteToPtyPermission::AskOnFirstWrite => "Ask on first write", - WriteToPtyPermission::Unknown => "Unknown", + WriteToPtyPermission::AlwaysAllow => { + i18n::t("ai.execution_profiles.editor.permission.always_allow") + } + WriteToPtyPermission::AlwaysAsk => { + i18n::t("ai.execution_profiles.editor.permission.always_ask") + } + WriteToPtyPermission::AskOnFirstWrite => { + i18n::t("ai.execution_profiles.editor.permission.ask_on_first_write") + } + WriteToPtyPermission::Unknown => i18n::t("settings.execution_profile.permission.unknown"), }; render_permission_line_with_icon(icon, label, permission_text, appearance, is_ai_enabled) } @@ -728,9 +774,15 @@ fn render_computer_use_permission_line_with_icon( ) -> Box { let permission_text = match permission { crate::ai::execution_profiles::ComputerUsePermission::Never - | crate::ai::execution_profiles::ComputerUsePermission::Unknown => "Never", - crate::ai::execution_profiles::ComputerUsePermission::AlwaysAsk => "Always ask", - crate::ai::execution_profiles::ComputerUsePermission::AlwaysAllow => "Always allow", + | crate::ai::execution_profiles::ComputerUsePermission::Unknown => { + i18n::t("ai.execution_profiles.editor.permission.never") + } + crate::ai::execution_profiles::ComputerUsePermission::AlwaysAsk => { + i18n::t("ai.execution_profiles.editor.permission.always_ask") + } + crate::ai::execution_profiles::ComputerUsePermission::AlwaysAllow => { + i18n::t("ai.execution_profiles.editor.permission.always_allow") + } }; render_permission_line_with_icon(icon, label, permission_text, appearance, is_ai_enabled) } @@ -743,11 +795,15 @@ fn render_ask_user_question_permission_line_with_icon( is_ai_enabled: bool, ) -> Box { let permission_text = match permission { - AskUserQuestionPermission::Never => "Never ask", + AskUserQuestionPermission::Never => { + i18n::t("ai.execution_profiles.editor.permission.never_ask") + } AskUserQuestionPermission::AskExceptInAutoApprove | AskUserQuestionPermission::Unknown => { - "Ask unless auto-approve" + i18n::t("ai.execution_profiles.editor.permission.ask_unless_auto_approve") + } + AskUserQuestionPermission::AlwaysAsk => { + i18n::t("ai.execution_profiles.editor.permission.always_ask") } - AskUserQuestionPermission::AlwaysAsk => "Always ask", }; render_permission_line_with_icon(icon, label, permission_text, appearance, is_ai_enabled) } @@ -760,9 +816,15 @@ fn render_run_agents_permission_line_with_icon( is_ai_enabled: bool, ) -> Box { let permission_text = match permission { - RunAgentsPermission::NeverAllow | RunAgentsPermission::Unknown => "Never", - RunAgentsPermission::AlwaysAllow => "Always allow", - RunAgentsPermission::AlwaysAsk => "Always ask", + RunAgentsPermission::NeverAllow | RunAgentsPermission::Unknown => { + i18n::t("ai.execution_profiles.editor.permission.never") + } + RunAgentsPermission::AlwaysAllow => { + i18n::t("ai.execution_profiles.editor.permission.always_allow") + } + RunAgentsPermission::AlwaysAsk => { + i18n::t("ai.execution_profiles.editor.permission.always_ask") + } }; render_permission_line_with_icon(icon, label, permission_text, appearance, is_ai_enabled) } @@ -774,7 +836,11 @@ fn render_bool_permission_line_with_icon( appearance: &Appearance, is_ai_enabled: bool, ) -> Box { - let permission_text = if enabled { "On" } else { "Off" }; + let permission_text = if enabled { + i18n::t("settings.execution_profile.on") + } else { + i18n::t("settings.execution_profile.off") + }; render_permission_line_with_icon(icon, label, permission_text, appearance, is_ai_enabled) } @@ -785,7 +851,7 @@ fn render_directory_allowlist( ) -> Box { with_standard_vertical_margin(render_pathbuf_allowlist_row( Icon::Check, - "Directory allowlist:".to_string(), + label_with_separator("ai.execution_profiles.editor.directory_allowlist.label"), &profile.directory_allowlist, appearance, is_ai_enabled, @@ -799,7 +865,7 @@ fn render_command_allowlist( ) -> Box { with_standard_vertical_margin(render_command_predicate_row( Icon::Check, - "Command allowlist:".to_string(), + label_with_separator("ai.execution_profiles.editor.command_allowlist.label"), &profile.command_allowlist, appearance, is_ai_enabled, @@ -813,7 +879,7 @@ fn render_command_denylist( ) -> Box { with_standard_vertical_margin(render_command_predicate_row( Icon::SlashCircle, - "Command denylist:".to_string(), + label_with_separator("ai.execution_profiles.editor.command_denylist.label"), &profile.command_denylist, appearance, is_ai_enabled, @@ -828,7 +894,7 @@ fn render_mcp_allowlist( ) -> Box { with_standard_vertical_margin(render_mcp_uuid_row( Icon::Check, - "MCP allowlist:".to_string(), + label_with_separator("ai.execution_profiles.editor.mcp_allowlist.label"), &profile.mcp_allowlist, appearance, app, @@ -844,7 +910,7 @@ fn render_mcp_denylist( ) -> Box { with_standard_vertical_margin(render_mcp_uuid_row( Icon::SlashCircle, - "MCP denylist:".to_string(), + label_with_separator("ai.execution_profiles.editor.mcp_denylist.label"), &profile.mcp_denylist, appearance, app, diff --git a/app/src/settings_view/features/external_editor.rs b/app/src/settings_view/features/external_editor.rs index f962ef50c1..cb47968ee4 100644 --- a/app/src/settings_view/features/external_editor.rs +++ b/app/src/settings_view/features/external_editor.rs @@ -21,9 +21,6 @@ use crate::util::file::external_editor::{EditorSettings, SUPPORTED_EDITORS}; use crate::view_components::{Dropdown, DropdownItem}; use crate::{report_if_error, send_telemetry_from_ctx}; -const TABBED_FILE_VIEWER_TOGGLE_HEADER: &str = "Group files into single editor pane"; -const TABBED_FILE_VIEWER_TOGGLE_DESCRIPTION: &str = "When this setting is on, any files opened in the same tab will be automatically grouped into a single editor pane."; - #[derive(Debug, Clone, PartialEq)] pub enum ExternalEditorAction { SetEditor(EditorChoice), @@ -117,22 +114,24 @@ impl ExternalEditorView { dropdown: &mut Dropdown, ctx: &mut ViewContext>, ) { - let default_option_text = "Split Pane"; + let default_option_text = i18n::t("settings.external_editor.split_pane"); let default_app = DropdownItem::new( - default_option_text, + default_option_text.clone(), ExternalEditorAction::SetLayout(EditorLayout::SplitPane), ); let mut items = vec![default_app]; items.push(DropdownItem::new( - "New Tab", + i18n::t("settings.external_editor.new_tab"), ExternalEditorAction::SetLayout(EditorLayout::NewTab), )); dropdown.set_items(items, ctx); match layout_to_open_files { EditorLayout::SplitPane => dropdown.set_selected_by_name(default_option_text, ctx), - EditorLayout::NewTab => dropdown.set_selected_by_name("New Tab", ctx), + EditorLayout::NewTab => { + dropdown.set_selected_by_name(i18n::t("settings.external_editor.new_tab"), ctx) + } }; } @@ -142,15 +141,18 @@ impl ExternalEditorView { mut make_action: impl FnMut(EditorChoice) -> ExternalEditorAction, ctx: &mut ViewContext>, ) { - let default_option_text = "Default App"; + let default_option_text = i18n::t("settings.external_editor.default_app"); let default_app = DropdownItem::new( - default_option_text, + default_option_text.clone(), make_action(EditorChoice::SystemDefault), ); let mut items = vec![default_app]; - items.push(DropdownItem::new("Warp", make_action(EditorChoice::Warp))); + items.push(DropdownItem::new( + i18n::t("drive.title"), + make_action(EditorChoice::Warp), + )); if FeatureFlag::AllowOpeningFileLinksUsingEditorEnv.is_enabled() { items.push(DropdownItem::new( "$EDITOR", @@ -274,9 +276,10 @@ impl View for ExternalEditorView { fn render(&self, app: &warpui::AppContext) -> Box { let appearance = Appearance::as_ref(app); + let default_editor_label = i18n::t("settings.external_editor.open_file_links"); let default_editor = render_dropdown_item( appearance, - "Choose an editor to open file links", + &default_editor_label, None, None, LocalOnlyIconState::for_setting( @@ -289,9 +292,10 @@ impl View for ExternalEditorView { &self.editor_dropdown, ); + let code_panels_editor_label = i18n::t("settings.external_editor.open_code_panel_files"); let code_panels_editor = render_dropdown_item( appearance, - "Choose an editor to open files from the code review panel, project explorer, and global search", + &code_panels_editor_label, None, None, LocalOnlyIconState::for_setting( @@ -304,9 +308,10 @@ impl View for ExternalEditorView { &self.code_panels_editor_dropdown, ); + let default_layout_label = i18n::t("settings.external_editor.open_files_layout"); let default_layout = render_dropdown_item( appearance, - "Choose a layout to open files in Warp", + &default_layout_label, None, None, LocalOnlyIconState::for_setting( @@ -326,7 +331,7 @@ impl View for ExternalEditorView { if FeatureFlag::TabbedEditorView.is_enabled() { column.add_child(render_body_item::( - TABBED_FILE_VIEWER_TOGGLE_HEADER.into(), + i18n::t("settings.external_editor.tabbed_viewer.header"), None, LocalOnlyIconState::for_setting( PreferTabbedEditorView::storage_key(), @@ -349,12 +354,14 @@ impl View for ExternalEditorView { ctx.dispatch_typed_action(ExternalEditorAction::ToggleTabbedEditorView); }) .finish(), - Some(TABBED_FILE_VIEWER_TOGGLE_DESCRIPTION.into()), + Some(i18n::t( + "settings.external_editor.tabbed_viewer.description", + )), )); } column.add_child(render_body_item::( - "Open Markdown files in Warp's Markdown Viewer by default".to_string(), + i18n::t("settings.external_editor.markdown_viewer_default"), Some(AdditionalInfo { mouse_state: self.markdown_viewer_mouse_state.clone(), on_click_action: Some(ExternalEditorAction::OpenUrl( diff --git a/app/src/settings_view/features/startup_shell.rs b/app/src/settings_view/features/startup_shell.rs index 4f0ba26d03..31b66b51e2 100644 --- a/app/src/settings_view/features/startup_shell.rs +++ b/app/src/settings_view/features/startup_shell.rs @@ -93,7 +93,10 @@ impl StartupShellView { ..Default::default() }; let mut editor = EditorView::single_line(options, ctx); - editor.set_placeholder_text("Executable path", ctx); + editor.set_placeholder_text( + i18n::t("settings.features.startup_shell.executable_path_placeholder"), + ctx, + ); if let Some(shell) = custom_shell_text.as_ref() { editor.set_buffer_text(shell, ctx); @@ -132,7 +135,7 @@ impl StartupShellView { ) { dropdown.update(ctx, |dropdown, ctx| { let mut items = vec![DropdownItem::new( - "Default", + i18n::t("common.default"), NewSessionShellAction::Set(AvailableShell::default()), )]; let shell_to_index = AvailableShells::handle(ctx).read(ctx, |model, _| { @@ -150,7 +153,7 @@ impl StartupShellView { }); items.push(DropdownItem::new( - "Custom", + i18n::t("settings.startup_shell.custom"), NewSessionShellAction::ShowCustomPathInput, )); let custom_index = items.len() - 1; diff --git a/app/src/settings_view/features/undo_close.rs b/app/src/settings_view/features/undo_close.rs index 6d3e743afd..4efb6ce3cd 100644 --- a/app/src/settings_view/features/undo_close.rs +++ b/app/src/settings_view/features/undo_close.rs @@ -133,7 +133,7 @@ impl UndoCloseView { .with_child( Container::new( Text::new_inline( - "Grace period (seconds)", + i18n::t("settings.undo_close.grace_period_seconds"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -174,7 +174,7 @@ impl View for UndoCloseView { let mut column = Flex::column() .with_cross_axis_alignment(CrossAxisAlignment::Stretch) .with_child(render_body_item::( - "Enable reopening of closed sessions".into(), + i18n::t("settings.undo_close.enable_reopening"), None, LocalOnlyIconState::for_setting( UndoCloseEnabled::storage_key(), diff --git a/app/src/settings_view/features/working_directory.rs b/app/src/settings_view/features/working_directory.rs index 65873fe3b6..3d44afec9c 100644 --- a/app/src/settings_view/features/working_directory.rs +++ b/app/src/settings_view/features/working_directory.rs @@ -142,21 +142,30 @@ impl View for WorkingDirectoryView { let items = Flex::column() .with_cross_axis_alignment(CrossAxisAlignment::Stretch) .with_children([ - ui_builder.label("New window").build().finish(), + ui_builder + .label(i18n::t("settings.working_directory.new_window")) + .build() + .finish(), render_row( &self.new_window_working_directory_dropdown, &self.new_window_working_directory_editor, config.new_window.mode == WorkingDirectoryMode::CustomDir, appearance, ), - ui_builder.label("New tab").build().finish(), + ui_builder + .label(i18n::t("settings.working_directory.new_tab")) + .build() + .finish(), render_row( &self.new_tab_working_directory_dropdown, &self.new_tab_working_directory_editor, config.new_tab.mode == WorkingDirectoryMode::CustomDir, appearance, ), - ui_builder.label("Split pane").build().finish(), + ui_builder + .label(i18n::t("settings.working_directory.split_pane")) + .build() + .finish(), render_row( &self.split_pane_working_directory_dropdown, &self.split_pane_working_directory_editor, @@ -308,7 +317,7 @@ fn init_top_level_dropdown( }) .collect_vec(); items.push(DropdownItem::new( - "Advanced".to_string(), + i18n::t("settings.working_directory.advanced"), WorkingDirectoryAction::SetGlobalWorkingDirectoryMode(None), )); let advanced_item_index = items.len() - 1; @@ -367,7 +376,10 @@ fn create_editor( }; ctx.add_typed_action_view(|ctx| { let mut editor = EditorView::single_line(options, ctx); - editor.set_placeholder_text("Directory path", ctx); + editor.set_placeholder_text( + i18n::t("settings.features.working_directory.directory_path_placeholder"), + ctx, + ); editor }) }; diff --git a/app/src/settings_view/features_page.rs b/app/src/settings_view/features_page.rs index 9d770a37fe..5c3ca67f3b 100644 --- a/app/src/settings_view/features_page.rs +++ b/app/src/settings_view/features_page.rs @@ -110,16 +110,6 @@ use crate::workspace::tab_settings::{NewTabPlacement, TabSettings, TabSettingsCh use crate::workspace::WorkspaceAction; use crate::{report_if_error, send_telemetry_from_ctx, themes, GlobalResourceHandles}; -cfg_if::cfg_if! { - if #[cfg(target_os = "macos")] { - static EXTRA_META_KEYS_LEFT_TEXT: &str = "Left Option key is Meta"; - static EXTRA_META_KEYS_RIGHT_TEXT: &str = "Right Option key is Meta"; - } else { - static EXTRA_META_KEYS_LEFT_TEXT: &str = "Left Alt key is Meta"; - static EXTRA_META_KEYS_RIGHT_TEXT: &str = "Right Alt key is Meta"; - } -} - pub fn init_actions_from_parent_view( app: &mut AppContext, context: &ContextPredicate, @@ -128,9 +118,20 @@ pub fn init_actions_from_parent_view( use warpui::keymap::macros::*; // Add all of the toggle settings from the Features Page that you want to show up on the Command Palette here. + let extra_meta_keys_left_text = if cfg!(target_os = "macos") { + i18n::t("settings.features.extra_meta_keys.left_option") + } else { + i18n::t("settings.features.extra_meta_keys.left_alt") + }; + let extra_meta_keys_right_text = if cfg!(target_os = "macos") { + i18n::t("settings.features.extra_meta_keys.right_option") + } else { + i18n::t("settings.features.extra_meta_keys.right_alt") + }; + let mut toggle_binding_pairs = vec![ ToggleSettingActionPair::new( - "copy on select within the terminal", + i18n::t("settings.features.action.copy_on_select_terminal"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleCopyOnSelect, )), @@ -138,7 +139,7 @@ pub fn init_actions_from_parent_view( flags::COPY_ON_SELECT_CONTEXT_FLAG, ), ToggleSettingActionPair::new( - "linux selection clipboard", + i18n::t("settings.features.action.linux_selection_clipboard"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleLinuxClipboardSelection, )), @@ -151,7 +152,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - "autocomplete quotes, parentheses, and brackets", + i18n::t("settings.features.action.autocomplete_symbols"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAutocompleteSymbols, )), @@ -164,7 +165,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - "restore windows, tabs, and panes on startup", + i18n::t("settings.features.action.restore_session"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleRestoreSession, )), @@ -172,7 +173,7 @@ pub fn init_actions_from_parent_view( flags::RESTORE_SESSION_CONTEXT_FLAG, ), ToggleSettingActionPair::new( - EXTRA_META_KEYS_LEFT_TEXT, + extra_meta_keys_left_text, builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleLeftMetaKey, )), @@ -185,7 +186,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - EXTRA_META_KEYS_RIGHT_TEXT, + extra_meta_keys_right_text, builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleRightMetaKey, )), @@ -198,7 +199,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - "scroll reporting", + i18n::t("settings.features.action.scroll_reporting"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleScrollReporting, )), @@ -211,7 +212,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - "completions while typing", + i18n::t("settings.features.action.completions_while_typing"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleCompletionsOpenWhileTyping, )), @@ -224,7 +225,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - "command corrections", + i18n::t("settings.features.action.command_corrections"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleCommandCorrections, )), @@ -237,7 +238,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - "error underlining", + i18n::t("settings.features.action.error_underlining"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleErrorUnderlining, )), @@ -250,7 +251,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - "syntax highlighting", + i18n::t("settings.features.action.syntax_highlighting"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleSyntaxHighlighting, )), @@ -263,7 +264,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - "audible terminal bell", + i18n::t("settings.features.action.audible_terminal_bell"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleUseAudibleBell, )), @@ -276,7 +277,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - "autosuggestions", + i18n::t("settings.features.action.autosuggestions"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAutosuggestions, )), @@ -284,7 +285,7 @@ pub fn init_actions_from_parent_view( flags::AUTOSUGGESTIONS_ENABLED_FLAG, ), ToggleSettingActionPair::new( - "autosuggestion keybinding hint", + i18n::t("settings.features.action.autosuggestion_keybinding_hint"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAutosuggestionKeybindingHint, )), @@ -292,7 +293,7 @@ pub fn init_actions_from_parent_view( flags::AUTOSUGGESTION_KEYBINDING_HINT_FLAG, ), ToggleSettingActionPair::new( - "autosuggestion ignore button", + i18n::t("settings.features.action.autosuggestion_ignore_button"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleShowAutosuggestionIgnoreButton, )), @@ -304,7 +305,7 @@ pub fn init_actions_from_parent_view( if !FeatureFlag::SSHTmuxWrapper.is_enabled() { toggle_binding_pairs.push(ToggleSettingActionPair::new( - "Warp SSH wrapper", + i18n::t("settings.features.action.warp_ssh_wrapper"), builder(SettingsAction::FeaturesPageToggle( #[allow(deprecated)] FeaturesPageAction::ToggleSshWrapper, @@ -316,7 +317,7 @@ pub fn init_actions_from_parent_view( } toggle_binding_pairs.push(ToggleSettingActionPair::new( - "show tooltip on click on links", + i18n::t("settings.features.action.link_click_tooltip"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleLinkTooltip, )), @@ -325,7 +326,7 @@ pub fn init_actions_from_parent_view( )); toggle_binding_pairs.push( ToggleSettingActionPair::new( - "long-running command notifications", + i18n::t("settings.features.action.long_running_command_notifications"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleLongRunningNotifications, )), @@ -340,7 +341,7 @@ pub fn init_actions_from_parent_view( ); toggle_binding_pairs.push( ToggleSettingActionPair::new( - "agent task completion notifications", + i18n::t("settings.features.action.agent_task_completion_notifications"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAgentTaskCompletedNotifications, )), @@ -355,7 +356,7 @@ pub fn init_actions_from_parent_view( ); toggle_binding_pairs.push( ToggleSettingActionPair::new( - "needs-attention notifications", + i18n::t("settings.features.action.needs_attention_notifications"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleNeedsAttentionNotifications, )), @@ -371,7 +372,7 @@ pub fn init_actions_from_parent_view( #[cfg(target_os = "macos")] toggle_binding_pairs.push( ToggleSettingActionPair::new( - "notification sounds", + i18n::t("settings.features.action.notification_sounds"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleNotificationSound, )), @@ -386,7 +387,7 @@ pub fn init_actions_from_parent_view( ); toggle_binding_pairs.push( ToggleSettingActionPair::new( - "in-app agent notifications", + i18n::t("settings.features.action.in_app_agent_notifications"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAgentInAppNotifications, )), @@ -398,7 +399,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "quit warning modal", + i18n::t("settings.features.action.quit_warning_modal"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleShowWarningBeforeQuitting, )), @@ -413,7 +414,7 @@ pub fn init_actions_from_parent_view( ); toggle_binding_pairs.push( ToggleSettingActionPair::new( - "mouse reporting", + i18n::t("settings.features.action.mouse_reporting"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleMouseReporting, )), @@ -429,7 +430,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "alias expansion", + i18n::t("settings.features.action.alias_expansion"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAliasExpansion, )), @@ -445,7 +446,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "middle-click paste", + i18n::t("settings.features.action.middle_click_paste"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleMiddleClickPaste, )), @@ -461,7 +462,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "code as default editor", + i18n::t("settings.features.action.code_default_editor"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleCodeAsDefaultEditor, )), @@ -477,7 +478,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "input hint text", + i18n::t("settings.features.action.input_hint_text"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleShowInputHintText, )), @@ -493,7 +494,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "editing commands with Vim keybindings", + i18n::t("settings.features.action.vim_keybindings"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleVimMode, )), @@ -509,7 +510,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "Vim unnamed register as system clipboard", + i18n::t("settings.features.action.vim_unnamed_register_clipboard"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleVimUnnamedSystemClipboard, )), @@ -525,7 +526,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "Vim status bar", + i18n::t("settings.features.action.vim_status_bar"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleVimStatusBar, )), @@ -541,7 +542,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "focus reporting", + i18n::t("settings.features.action.focus_reporting"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleFocusReporting, )), @@ -556,7 +557,7 @@ pub fn init_actions_from_parent_view( ); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "smart select", + i18n::t("settings.features.action.smart_select"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleSmartSelection, )), @@ -566,7 +567,7 @@ pub fn init_actions_from_parent_view( if FeatureFlag::AgentView.is_enabled() && AISettings::as_ref(app).is_any_ai_enabled(app) { toggle_binding_pairs.push( ToggleSettingActionPair::new( - "help block in new sessions", + i18n::t("settings.features.action.help_block_new_sessions"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleShowTerminalZeroStateBlock, )), @@ -583,7 +584,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "terminal input message line", + i18n::t("settings.features.action.terminal_input_message_line"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleShowTerminalInputMessageLine, )), @@ -594,7 +595,7 @@ pub fn init_actions_from_parent_view( ); toggle_binding_pairs.push( ToggleSettingActionPair::new( - "'@' context menu in terminal mode", + i18n::t("settings.features.action.at_context_menu_terminal"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAtContextMenuInTerminalMode, )), @@ -611,7 +612,7 @@ pub fn init_actions_from_parent_view( if FeatureFlag::AgentView.is_enabled() && AISettings::as_ref(app).is_any_ai_enabled(app) { toggle_binding_pairs.push( ToggleSettingActionPair::new( - "slash commands in terminal mode", + i18n::t("settings.features.action.slash_commands_terminal"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleSlashCommandsInTerminalMode, )), @@ -628,7 +629,7 @@ pub fn init_actions_from_parent_view( if FeatureFlag::AIContextMenuCode.is_enabled() { toggle_binding_pairs.push( ToggleSettingActionPair::new( - "codebase symbols in the '@' context menu", + i18n::t("settings.features.action.codebase_symbols_at_context"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleOutlineCodebaseSymbolsForAtContextMenu, )), @@ -644,7 +645,7 @@ pub fn init_actions_from_parent_view( } toggle_binding_pairs.push( ToggleSettingActionPair::new( - "global workflows in Command Search", + i18n::t("settings.features.action.global_workflows_command_search"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleGlobalWorkflowsInUniversalSearch, )), @@ -661,7 +662,7 @@ pub fn init_actions_from_parent_view( if GPUState::as_ref(app).is_low_power_gpu_available() { toggle_binding_pairs.push( ToggleSettingActionPair::new( - "integrated GPU rendering (low power)", + i18n::t("settings.features.action.integrated_gpu_rendering_low_power"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::TogglePreferLowPowerGPU, )), @@ -681,7 +682,7 @@ pub fn init_actions_from_parent_view( if windowing_system_is_customizable(app) { toggle_binding_pairs.push( ToggleSettingActionPair::new( - "Wayland for window management", + i18n::t("settings.features.action.wayland_window_management"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleForceX11, )), @@ -699,7 +700,7 @@ pub fn init_actions_from_parent_view( ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings(toggle_binding_pairs, app); app.register_fixed_bindings([FixedBinding::empty( - "Configure Global Hotkey", + i18n::t("settings.features.action.configure_global_hotkey"), WorkspaceAction::ScrollToSettingsWidget { page: SettingsSection::Features, widget_id: GlobalHotkeyWidget::static_widget_id(), @@ -709,7 +710,7 @@ pub fn init_actions_from_parent_view( if DefaultTerminal::can_warp_become_default() { app.register_fixed_bindings([FixedBinding::empty( - "Make Warp the default terminal", + i18n::t("settings.features.default_terminal.make_default"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::MakeWarpDefaultTerminal, )), @@ -855,14 +856,12 @@ fn max_max_grid_size() -> usize { fn block_maximum_rows_description() -> String { let max_rows = if ChannelState::enable_debug_features() { - "10 million" + i18n::t("settings.features.block_limit.max_rows_10m") } else { - "1 million" + i18n::t("settings.features.block_limit.max_rows_1m") }; - format!( - "Setting the limit above 100k lines may impact performance. Maximum rows supported is {max_rows}." - ) + i18n::t("settings.features.block_limit.desc").replace("{max_rows}", &max_rows) } fn to_string(b: bool) -> String { @@ -2270,22 +2269,22 @@ impl FeaturesPageView { let mut dropdown = Dropdown::new(ctx); let top = DropdownItem::new( - "Pin to top", + i18n::t("settings.features.quake.pin_top"), FeaturesPageAction::QuakeEditorSetPinPosition(QuakeModePinPosition::Top), ); let bottom = DropdownItem::new( - "Pin to bottom", + i18n::t("settings.features.quake.pin_bottom"), FeaturesPageAction::QuakeEditorSetPinPosition(QuakeModePinPosition::Bottom), ); let left = DropdownItem::new( - "Pin to left", + i18n::t("settings.features.quake.pin_left"), FeaturesPageAction::QuakeEditorSetPinPosition(QuakeModePinPosition::Left), ); let right = DropdownItem::new( - "Pin to right", + i18n::t("settings.features.quake.pin_right"), FeaturesPageAction::QuakeEditorSetPinPosition(QuakeModePinPosition::Right), ); @@ -2958,18 +2957,42 @@ impl FeaturesPageView { } let categories = vec![ - Category::new("General", general_widgets), - Category::new("Session", session_widgets), - Category::new("Keys", keys_widgets), - Category::new("Text Editing", text_editing_widgets), - Category::new("Terminal Input", editor_widgets), - Category::new("Terminal", terminal_widgets), - Category::new("Notifications", notifications_widgets), Category::new( - "Workflows", + Box::leak(i18n::t("settings.features.category.general").into_boxed_str()), + general_widgets, + ), + Category::new( + Box::leak(i18n::t("settings.features.category.session").into_boxed_str()), + session_widgets, + ), + Category::new( + Box::leak(i18n::t("settings.features.category.keys").into_boxed_str()), + keys_widgets, + ), + Category::new( + Box::leak(i18n::t("settings.features.category.text_editing").into_boxed_str()), + text_editing_widgets, + ), + Category::new( + Box::leak(i18n::t("settings.features.category.terminal_input").into_boxed_str()), + editor_widgets, + ), + Category::new( + Box::leak(i18n::t("settings.features.category.terminal").into_boxed_str()), + terminal_widgets, + ), + Category::new( + Box::leak(i18n::t("settings.features.category.notifications").into_boxed_str()), + notifications_widgets, + ), + Category::new( + Box::leak(i18n::t("settings.features.category.workflows").into_boxed_str()), vec![Box::new(WorkflowsInCommandSearch::default())], ), - Category::new("System", system_widgets), + Category::new( + Box::leak(i18n::t("settings.features.category.system").into_boxed_str()), + system_widgets, + ), ]; PageType::new_categorized(categories, None) @@ -3426,7 +3449,7 @@ impl FeaturesPageView { self.graphics_backend_dropdown.update(ctx, |dropdown, ctx| { if let Some(window) = ctx.windows().platform_window(ctx.window_id()) { let mut items = vec![DropdownItem::new( - "Default", + i18n::t("common.default"), FeaturesPageAction::SetPreferredGraphicsBackend(None), )]; items.extend(window.supported_backends().into_iter().map(|backend| { @@ -3536,10 +3559,12 @@ impl FeaturesPageView { self.refresh_tab_behavior_state(ctx); } - fn new_tab_placement_dropdown_item_label(val: NewTabPlacement) -> &'static str { + fn new_tab_placement_dropdown_item_label(val: NewTabPlacement) -> String { match val { - NewTabPlacement::AfterAllTabs => "After all tabs", - NewTabPlacement::AfterCurrentTab => "After current tab", + NewTabPlacement::AfterAllTabs => i18n::t("settings.features.new_tab.after_all_tabs"), + NewTabPlacement::AfterCurrentTab => { + i18n::t("settings.features.new_tab.after_current_tab") + } } } @@ -3721,7 +3746,7 @@ impl FeaturesPageView { .with_child( Container::new( Text::new_inline( - "Width %", + i18n::t("settings.features.quake.width_percent"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -3759,7 +3784,7 @@ impl FeaturesPageView { .with_child( Container::new( Text::new_inline( - "Height %", + i18n::t("settings.features.quake.height_percent"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -3837,7 +3862,7 @@ impl FeaturesPageView { .with_child( appearance .ui_builder() - .span("Autohides on loss of keyboard focus") + .span(i18n::t("settings.features.quake.autohide_on_blur")) .build() .with_margin_left(5.) .finish(), @@ -3933,7 +3958,7 @@ impl FeaturesPageView { Container::new( Align::new( Text::new_inline( - "When a command takes longer than", + i18n::t("settings.features.notifications.long_running_prefix"), appearance.ui_font_family(), font_size, ) @@ -3969,7 +3994,7 @@ impl FeaturesPageView { Container::new( Align::new( Text::new_inline( - "seconds to complete", + i18n::t("settings.features.notifications.long_running_suffix"), appearance.ui_font_family(), font_size, ) @@ -4051,9 +4076,13 @@ impl FeaturesPageView { Shrinkable::new( 2., Align::new( - Text::new_inline("Keybinding", appearance.ui_font_family(), 13.) - .with_color(appearance.theme().active_ui_text_color().into()) - .finish(), + Text::new_inline( + i18n::t("settings.features.keybinding.label"), + appearance.ui_font_family(), + 13., + ) + .with_color(appearance.theme().active_ui_text_color().into()) + .finish(), ) .left() .finish(), @@ -4072,7 +4101,7 @@ impl FeaturesPageView { } else { appearance .ui_builder() - .paragraph("Click to set global hotkey".to_string()) + .paragraph(i18n::t("settings.features.keybinding.click_to_set")) .build() .finish() }) @@ -4127,7 +4156,7 @@ impl FeaturesPageView { padding: Some(Coords::default().right(10.)), ..Default::default() }) - .with_text_label("Cancel".to_string()) + .with_text_label(i18n::t("settings.features.button.cancel")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(cancel_action.clone()); @@ -4139,7 +4168,7 @@ impl FeaturesPageView { appearance .ui_builder() .button(ButtonVariant::Text, save_button_mouse_state) - .with_text_label("Save".to_string()) + .with_text_label(i18n::t("settings.features.button.save")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(save_action.clone()); @@ -4163,7 +4192,7 @@ impl FeaturesPageView { 2., Align::new( Text::new_inline( - "Press new keyboard shortcut", + i18n::t("settings.features.keybinding.press_new_shortcut"), appearance.ui_font_family(), 13., ) @@ -4214,9 +4243,13 @@ impl FeaturesPageView { } Container::new( - Text::new_inline("Change keybinding", appearance.ui_font_family(), 12.) - .with_color(button_color) - .finish(), + Text::new_inline( + i18n::t("settings.features.keybinding.change"), + appearance.ui_font_family(), + 12., + ) + .with_color(button_color) + .finish(), ) .with_border(border) .finish() @@ -4384,7 +4417,7 @@ fn init_display_count_dropdown( ctx: &mut ViewContext>, ) { let no_preference = DropdownItem::new( - "Active Screen", + i18n::t("settings.features.quake.active_screen"), //|| { FeaturesPageAction::QuakeEditorSetPinScreen(None), //} ); @@ -4408,7 +4441,7 @@ fn init_display_count_dropdown( Some(idx) if idx.is_valid_given_display_count(display_count) => { dropdown.set_selected_by_name(format!("{idx}"), ctx) } - _ => dropdown.set_selected_by_name("Active Screen", ctx), + _ => dropdown.set_selected_by_name(i18n::t("settings.features.quake.active_screen"), ctx), }; } @@ -4433,14 +4466,14 @@ impl SettingsWidget for NativeRedirectWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Open links in desktop app".into(), + i18n::t("settings.features.open_links_desktop.label"), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: None, secondary_text: None, - tooltip_override_text: Some( - "Automatically open links in desktop app whenever possible.".into(), - ), + tooltip_override_text: Some(i18n::t( + "settings.features.open_links_desktop.tooltip", + )), }), LocalOnlyIconState::for_setting( UserNativeRedirectPreference::storage_key(), @@ -4503,7 +4536,7 @@ impl SettingsWidget for SessionRestorationWidget { .finish(); let labeled_switch = render_body_item::( - "Restore windows, tabs, and panes on startup".into(), + i18n::t("settings.features.restore_session.label"), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl( @@ -4529,7 +4562,7 @@ impl SettingsWidget for SessionRestorationWidget { if app.is_wayland() { let message = Text::new_inline( - "Window positions won't be restored on Wayland. ", + i18n::t("settings.features.restore_session.wayland_warning"), appearance.ui_font_family(), CONTENT_FONT_SIZE, ) @@ -4538,7 +4571,7 @@ impl SettingsWidget for SessionRestorationWidget { let link = ui_builder .link( - "See docs.".to_owned(), + i18n::t("settings.features.see_docs"), Some("https://docs.warp.dev/terminal/sessions/session-restoration".to_owned()), None, self.docs_link.clone(), @@ -4588,7 +4621,7 @@ impl SettingsWidget for SnackbarHeaderWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Show sticky command header".into(), + i18n::t("settings.features.sticky_command_header.label"), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl( @@ -4641,7 +4674,7 @@ impl SettingsWidget for LinkTooltipWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Show tooltip on click on links".into(), + i18n::t("settings.features.link_tooltip.label"), None, LocalOnlyIconState::for_setting( LinkTooltip::storage_key(), @@ -4710,7 +4743,7 @@ impl SettingsWidget for QuitWarningModalWidget { let general_settings = GeneralSettings::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( - "Show warning before quitting/logging out".into(), + i18n::t("settings.features.quit_warning.label"), None, LocalOnlyIconState::for_setting( ShowWarningBeforeQuitting::storage_key(), @@ -4757,11 +4790,11 @@ impl SettingsWidget for LoginItemWidget { let general_settings = GeneralSettings::as_ref(app); let ui_builder = appearance.ui_builder(); #[cfg(target_os = "macos")] - let label = "Start Warp at login (requires macOS 13+)"; + let label = i18n::t("settings.features.login_item.label_macos"); #[cfg(not(target_os = "macos"))] - let label = "Start Warp at login"; + let label = i18n::t("settings.features.login_item.label"); render_body_item::( - label.into(), + label, None, LocalOnlyIconState::for_setting( LoginItem::storage_key(), @@ -4808,7 +4841,7 @@ impl SettingsWidget for QuitWhenAllWindowsClosedWidget { let general_settings = GeneralSettings::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( - "Quit when all windows are closed".into(), + i18n::t("settings.features.quit_all_windows.label"), None, LocalOnlyIconState::for_setting( QuitOnLastWindowClosed::storage_key(), @@ -4855,7 +4888,7 @@ impl SettingsWidget for ShowChangelogWidget { let changelog_settings = ChangelogSettings::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( - "Show changelog toast after updates".into(), + i18n::t("settings.features.show_changelog.label"), None, LocalOnlyIconState::for_setting( ShowChangelogAfterUpdate::storage_key(), @@ -4928,7 +4961,10 @@ impl SettingsWidget for MouseScrollMultiplierWidget { } else { appearance .ui_builder() - .wrappable_text("Allowed Values: 1-20", true) + .wrappable_text( + i18n::t("settings.features.mouse_scroll.allowed_values"), + true, + ) .with_style(UiComponentStyles { font_color: Some(themes::theme::Fill::error().into_solid()), ..Default::default() @@ -4941,14 +4977,12 @@ impl SettingsWidget for MouseScrollMultiplierWidget { .finish(); render_body_item::( - "Lines scrolled by mouse wheel interval".into(), + i18n::t("settings.features.mouse_scroll.label"), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: None, secondary_text: None, - tooltip_override_text: Some( - "Supports floating point values between 1 and 20.".to_string(), - ), + tooltip_override_text: Some(i18n::t("settings.features.mouse_scroll.tooltip")), }), LocalOnlyIconState::for_setting( MouseScrollMultiplier::storage_key(), @@ -4988,7 +5022,7 @@ impl SettingsWidget for AutoOpenCodeReviewPaneWidget { let general_settings = GeneralSettings::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( - "Auto open code review panel".into(), + i18n::t("settings.features.auto_open_code_review.label"), None, LocalOnlyIconState::for_setting( AutoOpenCodeReviewPaneOnFirstAgentChange::storage_key(), @@ -5009,7 +5043,7 @@ impl SettingsWidget for AutoOpenCodeReviewPaneWidget { ctx.dispatch_typed_action(FeaturesPageAction::ToggleAutoOpenCodeReviewPane); }) .finish(), - Some("When this setting is on, the code review panel will open on the first accepted diff of a conversation".into()), + Some(i18n::t("settings.features.auto_open_code_review.desc")), ) } } @@ -5036,7 +5070,10 @@ impl SettingsWidget for DefaultTerminalWidget { let default_terminal = DefaultTerminal::as_ref(app); if default_terminal.is_warp_default() { ui_builder - .wrappable_text("Warp is the default terminal", true) + .wrappable_text( + i18n::t("settings.features.default_terminal.is_default"), + true, + ) .with_style(UiComponentStyles { font_color: Some(appearance.theme().disabled_ui_text_color().into()), margin: Some(Coords::default().bottom(16.)), @@ -5047,7 +5084,7 @@ impl SettingsWidget for DefaultTerminalWidget { } else { ui_builder .link( - "Make Warp the default terminal".to_string(), + i18n::t("settings.features.default_terminal.make_default"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(FeaturesPageAction::MakeWarpDefaultTerminal); @@ -5100,7 +5137,7 @@ impl SettingsWidget for BlockLimitWidget { .finish(); render_body_item::( - "Maximum rows in a block".into(), + i18n::t("settings.features.block_limit.label"), None, LocalOnlyIconState::for_setting( MaximumGridSize::storage_key(), @@ -5140,14 +5177,16 @@ impl SettingsWidget for SSHWrapperWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Warp SSH Wrapper".into(), + i18n::t("settings.features.ssh_wrapper.label"), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl( "https://docs.warp.dev/terminal/warpify/ssh-legacy#implementation".into(), )), secondary_text: if view.ssh_wrapper_toggled { - Some("This change will take effect in new sessions".to_string()) + Some(i18n::t( + "settings.features.ssh_wrapper.takes_effect_new_sessions", + )) } else { None }, @@ -5201,7 +5240,7 @@ impl SettingsWidget for DesktopNotificationsWidget { let ui_builder = appearance.ui_builder(); let mut column = Flex::column(); column.add_child(render_body_item::( - "Receive desktop notifications from Warp".into(), + i18n::t("settings.features.notifications.receive_desktop"), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl(NOTIFICATIONS_DOCS_URL.into())), @@ -5237,12 +5276,15 @@ impl SettingsWidget for DesktopNotificationsWidget { session_settings.notifications.mode, NotificationsMode::Enabled ) { + let agent_task_completed_label = + i18n::t("settings.features.notifications.agent_task_completed"); + let needs_attention_label = i18n::t("settings.features.notifications.needs_attention"); let toggles = vec![ view.render_notification_toggle( session_settings .notifications .is_agent_task_completed_enabled, - "Notify when an agent completes a task", + &agent_task_completed_label, FeaturesPageAction::ToggleAgentTaskCompletedNotifications, view.button_mouse_states .agent_task_completed_notifications_checkbox @@ -5255,7 +5297,7 @@ impl SettingsWidget for DesktopNotificationsWidget { ), view.render_notification_toggle( session_settings.notifications.is_needs_attention_enabled, - "Notify when a command or agent needs your attention to continue", + &needs_attention_label, FeaturesPageAction::ToggleNeedsAttentionNotifications, view.button_mouse_states .agent_needs_attention_notifications_checkbox @@ -5265,9 +5307,10 @@ impl SettingsWidget for DesktopNotificationsWidget { // Add notification sound toggle only on macOS #[cfg(target_os = "macos")] { + let play_sounds_label = i18n::t("settings.features.notifications.play_sounds"); view.render_notification_toggle( session_settings.notifications.play_notification_sound, - "Play notification sounds", + &play_sounds_label, FeaturesPageAction::ToggleNotificationSound, view.button_mouse_states.notification_sound_checkbox.clone(), appearance, @@ -5282,7 +5325,7 @@ impl SettingsWidget for DesktopNotificationsWidget { let ai_settings = AISettings::as_ref(app); let show_agent_notifications = *ai_settings.show_agent_notifications; column.add_child(render_body_item::( - "Show in-app agent notifications".into(), + i18n::t("settings.features.notifications.in_app_agent"), None, LocalOnlyIconState::Hidden, ToggleState::Enabled, @@ -5321,7 +5364,7 @@ impl SettingsWidget for DesktopNotificationsWidget { .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Text::new_inline( - "Toast notifications stay visible for", + i18n::t("settings.features.notifications.toast_duration_prefix"), appearance.ui_font_family(), font_size, ) @@ -5350,9 +5393,13 @@ impl SettingsWidget for DesktopNotificationsWidget { .finish(), ) .with_child( - Text::new_inline("seconds", appearance.ui_font_family(), font_size) - .with_color(font_color.into()) - .finish(), + Text::new_inline( + i18n::t("settings.features.notifications.toast_duration_suffix"), + appearance.ui_font_family(), + font_size, + ) + .with_color(font_color.into()) + .finish(), ) .finish(); @@ -5386,7 +5433,7 @@ impl SettingsWidget for StartupShellWidget { .with_children([ render_sub_sub_header( appearance, - "Default shell for new sessions".to_string(), + i18n::t("settings.features.startup_shell.header"), Some(LocalOnlyIconState::for_setting( StartupShellOverride::storage_key(), StartupShellOverride::sync_to_cloud(), @@ -5425,7 +5472,7 @@ impl SettingsWidget for WorkingDirectoryWidget { .with_children([ render_sub_sub_header( appearance, - "Working directory for new sessions".to_string(), + i18n::t("settings.features.working_directory.header"), Some(LocalOnlyIconState::for_setting( WorkingDirectoryConfig::storage_key(), WorkingDirectoryConfig::sync_to_cloud(), @@ -5483,7 +5530,7 @@ impl SettingsWidget for ConfirmCloseSharedSessionWidget { let ui_builder = appearance.ui_builder(); let session_settings = SessionSettings::as_ref(app); render_body_item::( - "Confirm before closing shared session".into(), + i18n::t("settings.features.confirm_close_shared.label"), None, LocalOnlyIconState::for_setting( ShouldConfirmCloseSession::storage_key(), @@ -5536,7 +5583,11 @@ impl SettingsWidget for ExtraMetaKeysWidget { .borrow_mut(); Flex::column() .with_child(render_body_item::( - EXTRA_META_KEYS_LEFT_TEXT.into(), + if cfg!(target_os = "macos") { + i18n::t("settings.features.extra_meta_keys.left_option") + } else { + i18n::t("settings.features.extra_meta_keys.left_alt") + }, None, LocalOnlyIconState::for_setting( crate::terminal::keys_settings::ExtraMetaKeys::storage_key(), @@ -5557,7 +5608,11 @@ impl SettingsWidget for ExtraMetaKeysWidget { None, )) .with_child(render_body_item::( - EXTRA_META_KEYS_RIGHT_TEXT.into(), + if cfg!(target_os = "macos") { + i18n::t("settings.features.extra_meta_keys.right_option") + } else { + i18n::t("settings.features.extra_meta_keys.right_alt") + }, None, LocalOnlyIconState::for_setting( crate::terminal::keys_settings::ExtraMetaKeys::storage_key(), @@ -5601,7 +5656,7 @@ impl SettingsWidget for GlobalHotkeyWidget { let ui_builder = appearance.ui_builder(); if app.is_wayland() { column.add_child(render_body_item::( - "Global hotkey:".to_owned(), + i18n::t("settings.features.global_hotkey.label"), None, // Fine not to show local only icon state for this, as it's not a supported setting. LocalOnlyIconState::Hidden, @@ -5610,12 +5665,14 @@ impl SettingsWidget for GlobalHotkeyWidget { Flex::row() .with_children([ ui_builder - .span("Not supported on Wayland. ") + .span(i18n::t( + "settings.features.global_hotkey.not_supported_wayland", + )) .build() .finish(), ui_builder .link( - "See docs.".to_owned(), + i18n::t("settings.features.see_docs"), Some( "https://docs.warp.dev/terminal/windows/global-hotkey" .to_owned(), @@ -5635,9 +5692,10 @@ impl SettingsWidget for GlobalHotkeyWidget { &mut column, &KeysSettings::as_ref(app).activation_hotkey_enabled, || { + let global_hotkey_label = i18n::t("settings.features.global_hotkey.label"); render_dropdown_item( appearance, - "Global hotkey:", + &global_hotkey_label, None, None, LocalOnlyIconState::for_setting( @@ -5744,7 +5802,7 @@ impl SettingsWidget for AutocompleteSymbolsWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Autocomplete quotes, parentheses, and brackets".into(), + i18n::t("settings.features.autocomplete_symbols.label"), None, LocalOnlyIconState::for_setting( AutocompleteSymbols::storage_key(), @@ -5791,9 +5849,10 @@ impl SettingsWidget for CodeEditorLineNumberModeWidget { &mut column, &AppEditorSettings::as_ref(app).code_editor_line_number_mode, || { + let line_numbers_label = i18n::t("settings.features.code_line_numbers.label"); render_dropdown_item( appearance, - "Code editor line numbers:", + &line_numbers_label, None, None, LocalOnlyIconState::for_setting( @@ -5833,7 +5892,7 @@ impl SettingsWidget for ErrorUnderliningWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Error underlining for commands".into(), + i18n::t("settings.features.error_underlining.label"), None, LocalOnlyIconState::for_setting( ErrorUnderliningEnabled::storage_key(), @@ -5879,7 +5938,7 @@ impl SettingsWidget for SyntaxHighlightingWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Syntax highlighting for commands".into(), + i18n::t("settings.features.syntax_highlighting.label"), None, LocalOnlyIconState::for_setting( SyntaxHighlighting::storage_key(), @@ -5925,7 +5984,7 @@ impl SettingsWidget for CompletionsMenuWhileTypingWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Open completions menu as you type".into(), + i18n::t("settings.features.completions_while_typing.label"), None, LocalOnlyIconState::for_setting( CompletionsOpenWhileTyping::storage_key(), @@ -5975,7 +6034,7 @@ impl SettingsWidget for CommandCorrectionsWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Suggest corrected commands".into(), + i18n::t("settings.features.command_corrections.label"), None, LocalOnlyIconState::for_setting( CommandCorrections::storage_key(), @@ -6022,7 +6081,7 @@ impl SettingsWidget for AliasExpansionWidget { let alias_expansion_settings = AliasExpansionSettings::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( - "Expand aliases as you type".into(), + i18n::t("settings.features.alias_expansion.label"), None, LocalOnlyIconState::for_setting( AliasExpansionEnabled::storage_key(), @@ -6069,7 +6128,7 @@ impl SettingsWidget for MiddleClickPasteWidget { let ui_builder = appearance.ui_builder(); let selection_settings = SelectionSettings::as_ref(app); render_body_item::( - "Middle-click to paste".into(), + i18n::t("settings.features.middle_click_paste.label"), None, LocalOnlyIconState::for_setting( MiddleClickPasteEnabled::storage_key(), @@ -6121,7 +6180,7 @@ impl SettingsWidget for VimModeWidget { let app_editor_settings = AppEditorSettings::as_ref(app); let vim_mode_enabled = *app_editor_settings.vim_mode.value(); column.add_child(render_body_item::( - "Edit code and commands with Vim keybindings".into(), + i18n::t("settings.features.vim_mode.label"), None, LocalOnlyIconState::for_setting( VimModeEnabled::storage_key(), @@ -6169,7 +6228,7 @@ impl SettingsWidget for VimModeWidget { app, ), clipboard_switch, - "Set unnamed register as system clipboard".into(), + i18n::t("settings.features.vim_unnamed_clipboard.label"), ); let vim_status_bar = *app_editor_settings.vim_status_bar.value(); @@ -6193,7 +6252,7 @@ impl SettingsWidget for VimModeWidget { app, ), status_bar_switch, - "Show Vim status bar".into(), + i18n::t("settings.features.vim_status_bar.label"), ); column.add_child(render_group( @@ -6226,7 +6285,7 @@ impl SettingsWidget for AtContextMenuInTerminalModeWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Enable '@' context menu in terminal mode".into(), + i18n::t("settings.features.at_context_menu.label"), None, LocalOnlyIconState::for_setting( AtContextMenuInTerminalMode::storage_key(), @@ -6282,7 +6341,7 @@ impl SettingsWidget for SlashCommandsInTerminalModeWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Enable slash commands in terminal mode".into(), + i18n::t("settings.features.slash_commands.label"), None, LocalOnlyIconState::for_setting( EnableSlashCommandsInTerminal::storage_key(), @@ -6334,7 +6393,7 @@ impl SettingsWidget for OutlineCodebaseSymbolsForAtContextMenuWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Outline codebase symbols for '@' context menu".into(), + i18n::t("settings.features.outline_codebase_symbols.label"), None, LocalOnlyIconState::for_setting( OutlineCodebaseSymbolsForAtContextMenu::storage_key(), @@ -6386,7 +6445,7 @@ impl SettingsWidget for ShowTerminalInputMessageLineWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Show terminal input message line".into(), + i18n::t("settings.features.terminal_input_message_line.label"), None, LocalOnlyIconState::for_setting( ShowTerminalInputMessageBar::storage_key(), @@ -6439,7 +6498,7 @@ impl SettingsWidget for AutosuggestionKeybindingHintWidget { let autosuggestion_keybinding_hint = *app_editor_settings.autosuggestion_keybinding_hint.value(); column.add_child(render_body_item::( - "Show autosuggestion keybinding hint".into(), + i18n::t("settings.features.autosuggestion_hint.label"), None, LocalOnlyIconState::for_setting( AutosuggestionKeybindingHint::storage_key(), @@ -6495,7 +6554,7 @@ impl SettingsWidget for AutosuggestionIgnoreButtonWidget { .show_autosuggestion_ignore_button .value(); column.add_child(render_body_item::( - "Show autosuggestion ignore button".into(), + i18n::t("settings.features.autosuggestion_ignore_button.label"), None, LocalOnlyIconState::for_setting( ShowAutosuggestionIgnoreButton::storage_key(), @@ -6540,31 +6599,35 @@ impl TabKeyBehaviorWidget { TabBehavior::Completions if view.autosuggestions_keystroke.is_empty() => { // If the "Accept autosuggestions" keybinding is unbound, the // user can always still accept with right arrow. - Some("→ accepts autosuggestions.".into()) + Some(i18n::t( + "settings.features.tab_behavior.arrow_accepts_autosuggestions", + )) } - TabBehavior::Completions => Some(format!( - "{} accepts autosuggestions.", - *view.autosuggestions_keystroke - )), + TabBehavior::Completions => Some( + i18n::t("settings.features.tab_behavior.key_accepts_autosuggestions") + .replace("{key}", &view.autosuggestions_keystroke.to_string()), + ), TabBehavior::Autosuggestions if *input_settings.completions_open_while_typing.value() => { if view.completions_keystroke.is_empty() { - Some("Completions open as you type.".into()) - } else { - Some(format!( - "Completions open as you type (or {}).", - *view.completions_keystroke + Some(i18n::t( + "settings.features.tab_behavior.completions_open_typing", )) + } else { + Some( + i18n::t("settings.features.tab_behavior.completions_open_typing_or_key") + .replace("{key}", &view.completions_keystroke.to_string()), + ) } } - TabBehavior::Autosuggestions if view.completions_keystroke.is_empty() => { - Some("Opening the completion menu is unbound.".into()) - } - TabBehavior::Autosuggestions => Some(format!( - "{} opens completion menu.", - *view.completions_keystroke + TabBehavior::Autosuggestions if view.completions_keystroke.is_empty() => Some(i18n::t( + "settings.features.tab_behavior.completion_menu_unbound", )), + TabBehavior::Autosuggestions => Some( + i18n::t("settings.features.tab_behavior.key_opens_completion_menu") + .replace("{key}", &view.completions_keystroke.to_string()), + ), TabBehavior::UserDefined => None, }; let other_keybinding_name = match *view.tab_behavior { @@ -6622,7 +6685,7 @@ impl SettingsWidget for TabKeyBehaviorWidget { .with_child( appearance .ui_builder() - .span("Tab key behavior") + .span(i18n::t("settings.features.tab_behavior.header")) .with_style(UiComponentStyles { font_size: Some(CONTENT_FONT_SIZE + 1.), ..Default::default() @@ -6680,9 +6743,10 @@ impl SettingsWidget for CtrlTabBehaviorWidget { &mut column, &KeysSettings::as_ref(app).ctrl_tab_behavior, || { + let ctrl_tab_label = i18n::t("settings.features.ctrl_tab_behavior.label"); render_dropdown_item( appearance, - "Ctrl+Tab behavior:", + &ctrl_tab_label, None, None, LocalOnlyIconState::for_setting( @@ -6725,7 +6789,7 @@ impl SettingsWidget for MouseReportingWidget { let reporting_settings = AltScreenReporting::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( - "Enable Mouse Reporting".into(), + i18n::t("settings.features.mouse_reporting.label"), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl( @@ -6780,7 +6844,7 @@ impl SettingsWidget for ScrollReportingWidget { let reporting_settings = AltScreenReporting::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( - "Enable Scroll Reporting".into(), + i18n::t("settings.features.scroll_reporting.label"), None, LocalOnlyIconState::for_setting( ScrollReportingEnabled::storage_key(), @@ -6838,7 +6902,7 @@ impl SettingsWidget for FocusReportingWidget { let reporting_settings = AltScreenReporting::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( - "Enable Focus Reporting".into(), + i18n::t("settings.features.focus_reporting.label"), None, LocalOnlyIconState::for_setting( FocusReportingEnabled::storage_key(), @@ -6885,7 +6949,7 @@ impl SettingsWidget for AudibleBellWidget { let ui_builder = appearance.ui_builder(); let terminal_settings = TerminalSettings::as_ref(app); render_body_item::( - "Use Audible Bell".into(), + i18n::t("settings.features.audible_bell.label"), None, LocalOnlyIconState::for_setting( UseAudibleBell::storage_key(), @@ -6929,7 +6993,7 @@ impl SmartSelectWidget { Flex::column() .with_child( ui_builder - .label("Characters considered part of a word".to_string()) + .label(i18n::t("settings.features.smart_select.word_chars_label")) .with_style(UiComponentStyles { margin: Some(Coords { top: 10.0, @@ -6990,7 +7054,7 @@ impl SettingsWidget for SmartSelectWidget { let selection = SemanticSelection::as_ref(app); let mut column = Flex::column(); column.add_child(render_body_item::( - "Double-click smart selection".into(), + i18n::t("settings.features.smart_select.label"), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl( @@ -7066,7 +7130,7 @@ impl SettingsWidget for ShowTerminalZeroStateBlockWidget { let ui_builder = appearance.ui_builder(); let terminal_settings = TerminalSettings::as_ref(app); render_body_item::( - "Show help block in new sessions".into(), + i18n::t("settings.features.help_block.label"), None, LocalOnlyIconState::for_setting( ShowTerminalZeroStateBlock::storage_key(), @@ -7108,7 +7172,7 @@ impl SettingsWidget for CopyOnSelectWidget { let ui_builder = appearance.ui_builder(); let copy_on_select_enabled = SelectionSettings::as_ref(app).copy_on_select_enabled(); render_body_item::( - "Copy on select".into(), + i18n::t("settings.features.copy_on_select.label"), None, LocalOnlyIconState::for_setting( CopyOnSelect::storage_key(), @@ -7150,9 +7214,10 @@ impl SettingsWidget for NewTabPlacementWidget { appearance: &Appearance, app: &AppContext, ) -> Box { + let new_tab_placement_label = i18n::t("settings.features.new_tab.placement_label"); render_dropdown_item( appearance, - "New tab placement", + &new_tab_placement_label, None, None, LocalOnlyIconState::for_setting( @@ -7187,7 +7252,7 @@ impl SettingsWidget for DefaultSessionModeWidget { app: &AppContext, ) -> Box { let label = render_dropdown_item_label( - "Default mode for new sessions".to_string(), + i18n::t("settings.features.default_session_mode.label"), None, LocalOnlyIconState::for_setting( DefaultSessionMode::storage_key(), @@ -7241,7 +7306,7 @@ impl SettingsWidget for WorkflowsInCommandSearch { let ui_builder = appearance.ui_builder(); let workflow_settings = CommandSearchSettings::as_ref(app); render_body_item::( - "Show Global Workflows in Command Search (ctrl-r)".into(), + i18n::t("settings.features.global_workflows.label"), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl( @@ -7296,14 +7361,12 @@ impl SettingsWidget for LinuxSelectionClipboardWidget { app: &AppContext, ) -> Box { render_body_item::( - "Honor linux selection clipboard".into(), + i18n::t("settings.features.linux_clipboard.label"), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: None, secondary_text: None, - tooltip_override_text: Some( - "Whether the Linux primary clipboard should be supported.".into(), - ), + tooltip_override_text: Some(i18n::t("settings.features.linux_clipboard.tooltip")), }), LocalOnlyIconState::for_setting( LinuxSelectionClipboard::storage_key(), @@ -7350,7 +7413,7 @@ impl SettingsWidget for GPUWidget { ) -> Box { let gpu_settings = GPUSettings::as_ref(app); let mut col = Flex::column().with_child(render_body_item::( - "Prefer rendering new windows with integrated GPU (low power)".into(), + i18n::t("settings.features.gpu.prefer_low_power.label"), None, LocalOnlyIconState::for_setting( PreferLowPowerGPU::storage_key(), @@ -7380,7 +7443,10 @@ impl SettingsWidget for GPUWidget { Container::new( appearance .ui_builder() - .wrappable_text("Changes will apply to new windows.", true) + .wrappable_text( + i18n::t("settings.features.changes_apply_new_windows"), + true, + ) .with_style(UiComponentStyles { font_color: Some(theme.sub_text_color(theme.background()).into_solid()), ..Default::default() @@ -7420,12 +7486,12 @@ impl SettingsWidget for WindowSystemWidget { let mut children = Flex::column(); let force_x11 = *LinuxAppConfiguration::as_ref(app).force_x11.value(); children.add_child(render_body_item::( - "Use Wayland for window management".into(), + i18n::t("settings.features.wayland.label"), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: None, secondary_text: None, - tooltip_override_text: Some("Enables the use of Wayland".to_string()), + tooltip_override_text: Some(i18n::t("settings.features.wayland.tooltip")), }), LocalOnlyIconState::for_setting( ForceX11::storage_key(), @@ -7450,12 +7516,11 @@ impl SettingsWidget for WindowSystemWidget { None, )); - let mut secondary_text = - "Enabling this setting disables global hotkey support. When disabled, text \ - may be blurry if your Wayland compositor is using fraction scaling (ex: 125%)." - .to_string(); + let mut secondary_text = i18n::t("settings.features.wayland.secondary_text"); if view.force_x11_changed { - secondary_text.push_str("\n\nRestart Warp for changes to take effect."); + secondary_text.push('\n'); + secondary_text.push('\n'); + secondary_text.push_str(&i18n::t("settings.features.wayland.restart_required")); } let warp_theme = appearance.theme(); children.add_child( @@ -7494,9 +7559,10 @@ impl SettingsWidget for GraphicsBackendWidget { app: &AppContext, ) -> Box { let theme = appearance.theme(); + let graphics_backend_label = i18n::t("settings.features.graphics_backend.label"); let dropdown = render_dropdown_item( appearance, - "Preferred graphics backend", + &graphics_backend_label, None, None, LocalOnlyIconState::for_setting( @@ -7517,7 +7583,11 @@ impl SettingsWidget for GraphicsBackendWidget { col.add_child( appearance .ui_builder() - .wrappable_text(format!("Current backend: {}", backend.to_label()), true) + .wrappable_text( + i18n::t("settings.features.graphics_backend.current") + .replace("{backend}", backend.to_label()), + true, + ) .with_style(UiComponentStyles { font_color: Some(theme.sub_text_color(theme.background()).into_solid()), ..Default::default() @@ -7531,7 +7601,10 @@ impl SettingsWidget for GraphicsBackendWidget { Container::new( appearance .ui_builder() - .wrappable_text("Changes will apply to new windows.", true) + .wrappable_text( + i18n::t("settings.features.changes_apply_new_windows"), + true, + ) .with_style(UiComponentStyles { font_color: Some(theme.sub_text_color(theme.background()).into_solid()), ..Default::default() @@ -7574,7 +7647,7 @@ impl SettingsWidget for AsyncFindWidget { let ui_builder = appearance.ui_builder(); let label = render_body_item_label::( - "Asynchronous find".into(), + i18n::t("settings.features.async_find.label"), None, None, LocalOnlyIconState::for_setting( @@ -7609,10 +7682,7 @@ impl SettingsWidget for AsyncFindWidget { label_with_chip, switch, appearance, - Some( - "Use an improved implementation of find to keep the UI responsive while searching for matches on large outputs." - .into(), - ), + Some(i18n::t("settings.features.async_find.desc")), ) } } diff --git a/app/src/settings_view/handoff_environment_creation_modal.rs b/app/src/settings_view/handoff_environment_creation_modal.rs index 42d52981b7..eeae248b5b 100644 --- a/app/src/settings_view/handoff_environment_creation_modal.rs +++ b/app/src/settings_view/handoff_environment_creation_modal.rs @@ -115,7 +115,7 @@ impl HandoffEnvironmentCreationModal { let Some(owner) = owner else { log::error!("Unable to create environment: not logged in"); ctx.emit(HandoffEnvironmentCreationModalEvent::CreationFailed { - error_message: "Not logged in".to_string(), + error_message: i18n::t("settings.environments.error.not_logged_in"), }); return; }; @@ -194,7 +194,7 @@ impl HandoffEnvironmentCreationModal { .finish(); let dialog = Dialog::new( - "Create environment".to_string(), + i18n::t("settings.environments.button.create_environment"), None, dialog_styles(appearance), ) diff --git a/app/src/settings_view/keybindings.rs b/app/src/settings_view/keybindings.rs index 746a5da828..886c1621f8 100644 --- a/app/src/settings_view/keybindings.rs +++ b/app/src/settings_view/keybindings.rs @@ -45,12 +45,7 @@ const ROW_HEIGHT: f32 = 28.; const EDIT_BUTTONS_BORDER_RADIUS: f32 = 4.0; pub const SEARCH_PLACEHOLDER: &str = "Search by name or by keys (ex. \"cmd d\")"; -const SHORTCUT_CONFLICT_WARNING_TEXT: &str = "This shortcut conflicts with other keybinds"; const KEYBINDINGS_PAGE_SHORTCUT: &str = "workspace:toggle_keybindings_page"; -const RESET_BUTTON_TEXT: &str = "Default"; -const CANCEL_BUTTON_TEXT: &str = "Cancel"; -const CLEAR_BUTTON_TEXT: &str = "Clear"; -const SAVE_BUTTON_TEXT: &str = "Save"; /// Notifier for custom keybinding changed. Views could subscribe to this for /// KeybindingChangedEvent. @@ -311,8 +306,9 @@ impl KeybindingRow { appearance: &Appearance, ) -> Box { let conflict_warning = if has_conflicting_binding { + let conflict_text = i18n::t("settings.keybindings.shortcut_conflict_warning"); render_text( - SHORTCUT_CONFLICT_WARNING_TEXT, + &conflict_text, Some(UiComponentStyles { font_weight: Some(Weight::Bold), ..Default::default() @@ -323,7 +319,8 @@ impl KeybindingRow { Empty::new().finish() }; - let press_new_shortcut_text = render_text("Press new keyboard shortcut", None, appearance); + let press_new_shortcut_label = i18n::t("settings.keybindings.press_new_shortcut"); + let press_new_shortcut_text = render_text(&press_new_shortcut_label, None, appearance); let new_shortcut_element = Container::new(press_new_shortcut_text) .with_margin_left(ROW_LEFT_MARGIN) @@ -399,7 +396,7 @@ impl KeybindingRow { self.mouse_state_handles.remove_mouse_state.clone(), |state| { render_button( - CLEAR_BUTTON_TEXT, + Box::leak(i18n::t("settings.keybindings.clear_button").into_boxed_str()), appearance, self.get_button_text_color(appearance, state), ) @@ -420,7 +417,7 @@ impl KeybindingRow { .clone(), |state| { render_button( - RESET_BUTTON_TEXT, + Box::leak(i18n::t("settings.keybindings.reset_button").into_boxed_str()), appearance, self.get_button_text_color(appearance, state), ) @@ -442,12 +439,24 @@ impl KeybindingRow { let cancel_button_color = self.get_button_text_color(appearance, state); if index == 0 { SavePosition::new( - render_button(CANCEL_BUTTON_TEXT, appearance, cancel_button_color), + render_button( + Box::leak( + i18n::t("settings.keybindings.cancel_button").into_boxed_str(), + ), + appearance, + cancel_button_color, + ), "first_keybinding_cancel", ) .finish() } else { - render_button("Cancel", appearance, cancel_button_color) + render_button( + Box::leak( + i18n::t("settings.keybindings.cancel_button").into_boxed_str(), + ), + appearance, + cancel_button_color, + ) } }, ) @@ -464,7 +473,7 @@ impl KeybindingRow { let save = Container::new( Hoverable::new(self.mouse_state_handles.save_mouse_state.clone(), |state| { render_button( - SAVE_BUTTON_TEXT, + Box::leak(i18n::t("settings.keybindings.save_button").into_boxed_str()), appearance, self.get_button_text_color(appearance, state), ) @@ -505,7 +514,7 @@ impl KeybindingsView { search_editor.update(ctx, |editor, ctx| { editor.clear_buffer_and_reset_undo_stack(ctx); - editor.set_placeholder_text(SEARCH_PLACEHOLDER, ctx); + editor.set_placeholder_text(i18n::t("settings.keybindings.search_placeholder"), ctx); }); let search_bar = ctx.add_typed_action_view(|_| SearchBar::new(search_editor.clone())); @@ -801,7 +810,7 @@ impl SettingsPageMeta for KeybindingsView { self.search_editor.update(ctx, |editor, ctx| { editor.clear_buffer_and_reset_undo_stack(ctx); - editor.set_placeholder_text(SEARCH_PLACEHOLDER, ctx); + editor.set_placeholder_text(i18n::t("settings.keybindings.search_placeholder"), ctx); }); if allow_steal_focus { @@ -969,8 +978,9 @@ impl KeybindingsWidget { appearance: &Appearance, ) -> Box { let font_size = appearance.ui_font_size() + FONT_DELTA; + let intro_label = i18n::t("settings.keybindings.description_intro"); let mut description = Flex::column().with_child(render_text( - "Add your own custom keybindings to existing actions below.", + &intro_label, Some(UiComponentStyles { font_size: Some(font_size), font_color: Some( @@ -996,7 +1006,7 @@ impl KeybindingsWidget { Wrap::row() .with_child( Container::new(render_text( - "Use", + &i18n::t("settings.keybindings.shortcut_hint_prefix"), Some(UiComponentStyles { font_size: Some(font_size), font_color: Some( @@ -1025,7 +1035,7 @@ impl KeybindingsWidget { ) .with_child( Container::new(render_text( - "to reference these keybindings in a side pane at anytime.", + &i18n::t("settings.keybindings.shortcut_hint_suffix"), Some(UiComponentStyles { font_size: Some(font_size), font_color: Some( @@ -1105,7 +1115,7 @@ impl SettingsWidget for KeybindingsWidget { { Some(LocalOnlyIconState::Visible { mouse_state: self.local_only_icon_mouse_state.clone(), - custom_tooltip: Some("Keyboard shortcuts are not synced to the cloud".to_string()), + custom_tooltip: Some(i18n::t("settings.keybindings.not_synced_tooltip")), }) } else { None @@ -1113,7 +1123,7 @@ impl SettingsWidget for KeybindingsWidget { let subheader = render_sub_header( appearance, - "Configure keyboard shortcuts", + i18n::t("settings.keybindings.subheader"), local_only_icon_state, ); let description = self.render_description(view.bindings.as_ref(), appearance); @@ -1123,7 +1133,7 @@ impl SettingsWidget for KeybindingsWidget { .with_child(description) .with_child(render_columns( Container::new(render_text( - "Command", + &i18n::t("settings.keybindings.column_command"), Some(UiComponentStyles { font_size: Some(appearance.ui_font_size() + FONT_DELTA), ..Default::default() diff --git a/app/src/settings_view/main_page.rs b/app/src/settings_view/main_page.rs index 78ac5e2669..9a13f07379 100644 --- a/app/src/settings_view/main_page.rs +++ b/app/src/settings_view/main_page.rs @@ -48,10 +48,8 @@ use crate::workspaces::workspace::CustomerType; use crate::{report_if_error, send_telemetry_from_ctx, TelemetryEvent}; const PHOTO_SIZE: f32 = 40.; -const REFERRAL_CTA: &str = "Earn rewards by sharing Warp with friends & colleagues"; const REGULAR_TEXT_FONT_SIZE: f32 = 12.; const VERTICAL_MARGIN: f32 = 24.; -const LOG_OUT_TEXT: &str = "Log out"; lazy_static! { static ref SETTINGS_SYNC_BINDINGS_ADDED: Arc> = Default::default(); } @@ -82,7 +80,7 @@ fn maybe_add_settings_sync_toggle_binding( *lock = true; toggle_binding_pairs.push( ToggleSettingActionPair::new( - "settings sync", + i18n::t("settings.account.settings_sync"), builder(SettingsAction::MainPageToggle( MainPageAction::ToggleSettingsSync, )), @@ -297,7 +295,12 @@ impl MainSettingsPageView { widgets.push(Box::new(LogoutWidget::default())); - let page = PageType::new_uncategorized(widgets, Some("Account")); + let page = PageType::new_uncategorized( + widgets, + Some(Box::leak( + i18n::t("settings.account.page_title").into_boxed_str(), + )), + ); MainSettingsPageView { page, auth_state } } @@ -350,7 +353,7 @@ impl AccountWidget { self.ui_state_handles.anonymous_user_sign_up_button.clone(), ) .with_style(button_styles) - .with_text_label("Sign up".to_owned()) + .with_text_label(i18n::t("settings.account.sign_up")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(MainPageAction::SignupAnonymousUser); @@ -362,7 +365,10 @@ impl AccountWidget { .with_cross_axis_alignment(CrossAxisAlignment::End); let current_user_id = auth_state.user_id().unwrap_or_default(); - plan_info.add_child(render_customer_type_badge(appearance, "Free".into())); + plan_info.add_child(render_customer_type_badge( + appearance, + i18n::t("settings.account.plan.free"), + )); plan_info.add_child( Container::new( appearance @@ -374,7 +380,7 @@ impl AccountWidget { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Compare plans", + i18n::t("settings.account.compare_plans"), Icon::CoinsStacked.to_warpui_icon(appearance.theme().accent()), MainAxisSize::Min, MainAxisAlignment::Center, @@ -510,7 +516,7 @@ impl AccountWidget { appearance .ui_builder() .link( - "Contact support".into(), + i18n::t("settings.account.contact_support"), Some("mailto:support@warp.dev".into()), None, self.ui_state_handles.enterprise_contact_us_link.clone(), @@ -527,7 +533,7 @@ impl AccountWidget { appearance .ui_builder() .link( - "Manage billing".into(), + i18n::t("settings.account.manage_billing"), None, Some(Box::new(move |ctx| { ctx.dispatch_typed_action( @@ -548,16 +554,18 @@ impl AccountWidget { // If the team is upgradeable to self-serve tier, show them the upgrade link. if team.billing_metadata.can_upgrade_to_higher_tier_plan() { let description = match team.billing_metadata.customer_type { - CustomerType::Prosumer => "Upgrade to Turbo plan", - CustomerType::Turbo => "Upgrade to Lightspeed plan", - _ => "Compare plans", + CustomerType::Prosumer => i18n::t("settings.account.upgrade_to_turbo"), + CustomerType::Turbo => { + i18n::t("settings.account.upgrade_to_lightspeed") + } + _ => i18n::t("settings.account.compare_plans"), }; let team_uid = team.uid; plan_info.add_child( appearance .ui_builder() .link( - description.into(), + description, None, Some(Box::new(move |ctx| { ctx.dispatch_typed_action(MainPageAction::Upgrade { @@ -576,14 +584,15 @@ impl AccountWidget { } } } else { - let plan_badge_child = render_customer_type_badge(appearance, "Free".into()); + let plan_badge_child = + render_customer_type_badge(appearance, i18n::t("settings.account.plan.free")); plan_info.add_child(plan_badge_child); plan_info.add_child( appearance .ui_builder() .link( - "Compare plans".into(), + i18n::t("settings.account.compare_plans"), None, Some(Box::new(move |ctx| { ctx.dispatch_typed_action(MainPageAction::Upgrade { @@ -713,7 +722,7 @@ impl SettingsWidget for SettingsSyncWidget { }; Container::new(render_body_item::( - "Settings sync".to_string(), + i18n::t("settings.account.settings_sync"), Some(label_info), // Cloud prefs are always synced, so no need to show the local-only icon. LocalOnlyIconState::Hidden, @@ -790,14 +799,15 @@ impl SettingsWidget for EarnRewardsWidget { appearance: &Appearance, _app: &AppContext, ) -> Box { + let referral_cta = i18n::t("settings.account.referral_cta"); Container::new( self.render_row( appearance, - REFERRAL_CTA, + &referral_cta, appearance .ui_builder() .link( - "Refer a friend".into(), + i18n::t("settings.account.refer_a_friend"), None, Some(Box::new(move |ctx| { ctx.dispatch_typed_action(WorkspaceAction::ShowReferralSettingsPage); @@ -833,11 +843,11 @@ impl VersionInfoWidget { .with_opacity(60) .into(); struct StatusContent { - text: &'static str, + text: String, color: ColorU, } struct CallToActionContent { - text: &'static str, + text: String, action: MainPageAction, } @@ -847,73 +857,73 @@ impl VersionInfoWidget { match autoupdate::get_update_state(app) { AutoupdateStage::NoUpdateAvailable => ( Some(StatusContent { - text: "Up to date", + text: i18n::t("settings.account.version.up_to_date"), color: faded_text_color, }), Some(CallToActionContent { - text: "Check for updates", + text: i18n::t("settings.account.version.check_for_updates"), action: MainPageAction::CheckForUpdate, }), ), AutoupdateStage::CheckingForUpdate => ( Some(StatusContent { - text: "checking for update...", + text: i18n::t("settings.account.version.checking"), color: faded_text_color, }), None, ), AutoupdateStage::DownloadingUpdate => ( Some(StatusContent { - text: "downloading update...", + text: i18n::t("settings.account.version.downloading"), color: faded_text_color, }), None, ), AutoupdateStage::UpdateReady { .. } => ( Some(StatusContent { - text: "Update available", + text: i18n::t("settings.account.version.update_available"), color: ansi_red, }), Some(CallToActionContent { - text: "Relaunch Warp", + text: i18n::t("settings.account.version.relaunch_warp"), action: MainPageAction::Relaunch, }), ), AutoupdateStage::Updating { .. } => ( Some(StatusContent { - text: "Updating...", + text: i18n::t("settings.account.version.updating"), color: faded_text_color, }), None, ), AutoupdateStage::UpdatedPendingRestart { .. } => ( Some(StatusContent { - text: "Installed update", + text: i18n::t("settings.account.version.installed_update"), color: faded_text_color, }), Some(CallToActionContent { - text: "Relaunch Warp", + text: i18n::t("settings.account.version.relaunch_warp"), action: MainPageAction::Relaunch, }), ), AutoupdateStage::UnableToUpdateToNewVersion { .. } => ( Some(StatusContent { - text: "A new version of Warp is available but can't be installed", + text: i18n::t("settings.account.version.cannot_install"), color: ansi_red, }), Some(CallToActionContent { - text: "Update Warp manually", + text: i18n::t("settings.account.version.update_manually"), // note: the handler for this action is a no-op action: MainPageAction::DownloadUpdate, }), ), AutoupdateStage::UnableToLaunchNewVersion { .. } => ( Some(StatusContent { - text: "A new version of Warp is installed but can't be launched.", + text: i18n::t("settings.account.version.cannot_launch"), color: ansi_red, }), Some(CallToActionContent { - text: "Update Warp manually", + text: i18n::t("settings.account.version.update_manually"), // note: the handler for this action is a no-op action: MainPageAction::DownloadUpdate, }), @@ -930,7 +940,7 @@ impl VersionInfoWidget { 1.0, Align::new( Text::new_inline( - "Version".to_string(), + i18n::t("settings.account.version.label"), appearance.ui_font_family(), REGULAR_TEXT_FONT_SIZE, ) @@ -1058,7 +1068,7 @@ impl LogoutWidget { appearance .ui_builder() .button(ButtonVariant::Secondary, self.mouse_state.clone()) - .with_text_label(LOG_OUT_TEXT.into()) + .with_text_label(i18n::t("settings.account.log_out")) .with_style(UiComponentStyles { font_size: Some(14.), padding: Some(Coords::uniform(8.).left(32.).right(32.)), @@ -1103,18 +1113,27 @@ impl SettingsWidget for IapCredentialsWidget { let disabled: ColorU = appearance.theme().disabled_ui_text_color().into(); let active: ColorU = appearance.theme().active_ui_text_color().into(); let (status_text, status_color): (String, ColorU) = match &state { - IapCredentialsState::Missing => ("Not yet loaded".to_string(), disabled), - IapCredentialsState::Refreshing { .. } => ("Refreshing…".to_string(), active), + IapCredentialsState::Missing => (i18n::t("settings.account.iap.not_loaded"), disabled), + IapCredentialsState::Refreshing { .. } => { + (i18n::t("settings.account.iap.refreshing"), active) + } IapCredentialsState::Loaded(cached) => { let remaining = cached .expires_at .saturating_duration_since(instant::Instant::now()); let mins = remaining.as_secs() / 60; - (format!("Loaded (refreshes in ~{mins}m)"), active) + ( + i18n::t("settings.account.iap.loaded_refreshes") + .replace("{mins}", &mins.to_string()), + active, + ) } - IapCredentialsState::Failed { message, .. } => (format!("Failed: {message}"), ansi_red), + IapCredentialsState::Failed { message, .. } => ( + i18n::t("settings.account.iap.failed").replace("{message}", message), + ansi_red, + ), IapCredentialsState::EnvInjected { .. } => { - ("Using injected token (WARP_IAP_TOKEN)".to_string(), active) + (i18n::t("settings.account.iap.using_injected_token"), active) } }; @@ -1122,7 +1141,7 @@ impl SettingsWidget for IapCredentialsWidget { let label = Align::new( Text::new_inline( - "Staging IAP credentials".to_string(), + i18n::t("settings.account.iap.title"), appearance.ui_font_family(), REGULAR_TEXT_FONT_SIZE, ) diff --git a/app/src/settings_view/mcp_servers/destructive_mcp_confirmation_dialog.rs b/app/src/settings_view/mcp_servers/destructive_mcp_confirmation_dialog.rs index 6d2eda3bcf..638facb2cd 100644 --- a/app/src/settings_view/mcp_servers/destructive_mcp_confirmation_dialog.rs +++ b/app/src/settings_view/mcp_servers/destructive_mcp_confirmation_dialog.rs @@ -56,24 +56,30 @@ impl From<&DestructiveMCPConfirmationDialogVariant> { fn from(variant: &DestructiveMCPConfirmationDialogVariant) -> Self { match *variant { - DestructiveMCPConfirmationDialogVariant::DeleteLocal => DestructiveMCPConfirmationDialogDisplayOptions::new( - "Delete MCP server?".to_string(), - "This will uninstall and remove this MCP server from all your devices.".to_string(), - "Delete MCP".to_string(), - "Cancel".to_string(), - ), - DestructiveMCPConfirmationDialogVariant::DeleteShared => DestructiveMCPConfirmationDialogDisplayOptions::new( - "Delete shared MCP server?".to_string(), - "This will not only delete this MCP server for yourself, but also uninstall and remove this MCP server from Warp and across all of your teammates' devices.".to_string(), - "Delete MCP".to_string(), - "Cancel".to_string(), - ), - DestructiveMCPConfirmationDialogVariant::Unshare => DestructiveMCPConfirmationDialogDisplayOptions::new( - "Remove shared MCP server from team?".to_string(), - "This will uninstall and remove this MCP server from Warp and across all of your teammates' devices.".to_string(), - "Remove from team".to_string(), - "Cancel".to_string(), - ), + DestructiveMCPConfirmationDialogVariant::DeleteLocal => { + DestructiveMCPConfirmationDialogDisplayOptions::new( + i18n::t("settings.mcp.confirm.delete_local.title"), + i18n::t("settings.mcp.confirm.delete_local.description"), + i18n::t("settings.mcp.button.delete_mcp"), + i18n::t("common.cancel"), + ) + } + DestructiveMCPConfirmationDialogVariant::DeleteShared => { + DestructiveMCPConfirmationDialogDisplayOptions::new( + i18n::t("settings.mcp.confirm.delete_shared.title"), + i18n::t("settings.mcp.confirm.delete_shared.description"), + i18n::t("settings.mcp.button.delete_mcp"), + i18n::t("common.cancel"), + ) + } + DestructiveMCPConfirmationDialogVariant::Unshare => { + DestructiveMCPConfirmationDialogDisplayOptions::new( + i18n::t("settings.mcp.confirm.unshare.title"), + i18n::t("settings.mcp.confirm.unshare.description"), + i18n::t("settings.mcp.button.remove_from_team"), + i18n::t("common.cancel"), + ) + } } } } diff --git a/app/src/settings_view/mcp_servers/edit_page.rs b/app/src/settings_view/mcp_servers/edit_page.rs index 2739a45227..0209c46ed5 100644 --- a/app/src/settings_view/mcp_servers/edit_page.rs +++ b/app/src/settings_view/mcp_servers/edit_page.rs @@ -127,7 +127,7 @@ pub struct MCPServersEditPageView { impl MCPServersEditPageView { pub fn new(ctx: &mut ViewContext) -> Self { let save_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Save", PrimaryTheme) + ActionButton::new(i18n::t("settings.mcp.button.save"), PrimaryTheme) .with_icon(Icon::Check) .on_click(|ctx| { ctx.dispatch_typed_action(MCPServersEditPageViewAction::Save); @@ -135,25 +135,33 @@ impl MCPServersEditPageView { }); let reinstall_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Edit Variables", PrimaryTheme).on_click(|ctx| { - ctx.dispatch_typed_action(MCPServersEditPageViewAction::Reinstall); - }) + ActionButton::new(i18n::t("settings.mcp.button.edit_variables"), PrimaryTheme).on_click( + |ctx| { + ctx.dispatch_typed_action(MCPServersEditPageViewAction::Reinstall); + }, + ) }); let delete_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Delete MCP", DangerSecondaryTheme) - .with_icon(Icon::Trash) - .on_click(|ctx| { - ctx.dispatch_typed_action(MCPServersEditPageViewAction::Delete); - }) + ActionButton::new( + i18n::t("settings.mcp.button.delete_mcp"), + DangerSecondaryTheme, + ) + .with_icon(Icon::Trash) + .on_click(|ctx| { + ctx.dispatch_typed_action(MCPServersEditPageViewAction::Delete); + }) }); let unshare_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Remove from team", DangerNakedTheme) - .with_icon(Icon::MinusCircle) - .on_click(|ctx| { - ctx.dispatch_typed_action(MCPServersEditPageViewAction::Unshare); - }) + ActionButton::new( + i18n::t("settings.mcp.button.remove_from_team"), + DangerNakedTheme, + ) + .with_icon(Icon::MinusCircle) + .on_click(|ctx| { + ctx.dispatch_typed_action(MCPServersEditPageViewAction::Unshare); + }) }); let json_editor = ctx.add_typed_action_view(|ctx| { @@ -181,9 +189,9 @@ impl MCPServersEditPageView { }); let editing_disabled_banner = ctx.add_typed_action_view(|_| { - Banner::new_without_close(BannerTextContent::plain_text( - "Only team admins and the creator of the MCP server can edit the MCP server.", - )) + Banner::new_without_close(BannerTextContent::plain_text(i18n::t( + "settings.mcp.edit_disabled_banner", + ))) .with_icon(Icon::Warning) }); @@ -306,11 +314,11 @@ impl MCPServersEditPageView { fn render_header(&self, app: &AppContext) -> Box { let appearance = Appearance::as_ref(app); let title = if self.server_card_item_id.is_none() { - "Add New MCP Server".to_string() + i18n::t("settings.mcp.title.add_new") } else if let Some(name) = self.server_model.name() { - format!("Edit {name} MCP Server") + i18n::t("settings.mcp.title.edit_named").replace("{name}", &name) } else { - "Edit MCP Server".to_string() + i18n::t("settings.mcp.title.edit") }; let ui_builder = appearance.ui_builder().clone(); @@ -320,7 +328,12 @@ impl MCPServersEditPageView { false, self.log_out_icon_button_mouse_handle.clone(), ) - .with_tooltip(move || ui_builder.tool_tip("Log out".to_string()).build().finish()) + .with_tooltip(move || { + ui_builder + .tool_tip(i18n::t("settings.mcp.tooltip.log_out")) + .build() + .finish() + }) .build() .on_click(|ctx, _, _| ctx.dispatch_typed_action(MCPServersEditPageViewAction::LogOut)) .finish(); @@ -532,12 +545,12 @@ impl MCPServersEditPageView { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error("This MCP server contains secrets. Visit Settings > Privacy to modify your secret redaction settings.".to_string()), + DismissibleToast::error(i18n::t("settings.mcp.error.contains_secrets")), window_id, ctx, ); }); - return Err("This MCP server contains secrets. Visit Settings > Privacy to modify your secret redaction settings.".to_string()); + return Err(i18n::t("settings.mcp.error.contains_secrets")); } Ok(()) @@ -592,31 +605,26 @@ impl MCPServersEditPageView { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error("No MCP Server specified.".to_string()), + DismissibleToast::error(i18n::t("settings.mcp.error.no_server_specified")), window_id, ctx, ); }); - return Err("No MCP Server specified.".to_string()); + return Err(i18n::t("settings.mcp.error.no_server_specified")); } if parsed_templatable_mcp_servers.len() > 1 { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "Cannot add multiple MCP servers while editing a single server." - .to_string(), - ), + DismissibleToast::error(i18n::t("settings.mcp.error.cannot_add_multiple")), window_id, ctx, ); }); - return Err( - "Cannot add multiple MCP servers while editing a single server.".to_string(), - ); + return Err(i18n::t("settings.mcp.error.cannot_add_multiple")); } Ok(parsed_templatable_mcp_servers[0].clone()) @@ -889,7 +897,9 @@ impl TypedActionView for MCPServersEditPageView { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error("No MCP Server specified.".to_string()), + DismissibleToast::error(i18n::t( + "settings.mcp.error.no_server_specified", + )), window_id, ctx, ); diff --git a/app/src/settings_view/mcp_servers/installation_modal.rs b/app/src/settings_view/mcp_servers/installation_modal.rs index 681abeeb30..a23054713e 100644 --- a/app/src/settings_view/mcp_servers/installation_modal.rs +++ b/app/src/settings_view/mcp_servers/installation_modal.rs @@ -75,14 +75,14 @@ pub struct InstallationModalBody { impl InstallationModalBody { pub fn new(ctx: &mut ViewContext) -> Self { let cancel_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Cancel", NakedTheme).on_click(|ctx| { + ActionButton::new(i18n::t("common.cancel"), NakedTheme).on_click(|ctx| { ctx.dispatch_typed_action(InstallationModalBodyAction::Cancel); }) }); let enter_keystroke = Keystroke::parse("enter").expect("valid keystroke"); let install_button = ctx.add_typed_action_view(|ctx| { - ActionButton::new("Install", PrimaryTheme) + ActionButton::new(i18n::t("common.install"), PrimaryTheme) .with_keybinding(KeystrokeSource::Fixed(enter_keystroke), ctx) .on_click(|ctx| { ctx.dispatch_typed_action(InstallationModalBodyAction::Install); @@ -257,7 +257,7 @@ impl InstallationModalBody { // Renders MCP title text let title = Text::new( - format!("Install {name}"), + i18n::t("settings.mcp.modal.install_title").replace("{name}", &name), appearance.ui_font_family(), appearance.header_font_size(), ) @@ -349,7 +349,10 @@ impl InstallationModalBody { ) .with_margin_bottom(INSTALLATION_MODAL_TITLE_VERTICAL_SPACING) .finish()), - Err(e) => Err(format!("Failed to parse markdown: {e:?}")), + Err(e) => { + Err(i18n::t("settings.mcp.markdown_parse_failed") + .replace("{error}", &format!("{e:?}"))) + } } } @@ -423,13 +426,13 @@ impl InstallationModalBody { .finish(); let source_text = if is_shared { - "Shared from team" + i18n::t("settings.mcp.chip.shared_from_team") } else { - "From another device" + i18n::t("settings.mcp.chip.from_another_device") }; let label_text = Text::new_inline( - source_text.to_string(), + source_text, appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -548,7 +551,7 @@ impl View for InstallationModalBody { .finish() } else { Text::new( - "No MCP server selected", + i18n::t("settings.mcp.no_server_selected"), appearance.ui_font_family(), appearance.ui_font_size(), ) diff --git a/app/src/settings_view/mcp_servers/list_page.rs b/app/src/settings_view/mcp_servers/list_page.rs index 9a93275581..d8be29d216 100644 --- a/app/src/settings_view/mcp_servers/list_page.rs +++ b/app/src/settings_view/mcp_servers/list_page.rs @@ -69,8 +69,6 @@ use crate::workspace::Workspace; use crate::workspaces::user_workspaces::UserWorkspaces; use crate::ToastStack; -const DESCRIPTION_TEXT: &str = "Add MCP servers to extend the Warp Agent's capabilities. MCP servers expose data sources or tools to agents through a standardized interface, essentially acting like plugins. Add a custom server, or use the presets to get started with popular servers. You can also find team servers that have been shared with you here. "; - #[derive(Debug, Clone)] pub enum MCPServersListPageViewEvent { Add, @@ -95,9 +93,6 @@ pub enum MCPServersListPageViewAction { ToggleFileBasedMcp, } -const EMPTY_STATE_TEXT: &str = "Once you add a MCP server, it will be shown here."; -const NO_SEARCH_RESULTS_TEXT: &str = "No search results found"; - pub struct MCPServersListPageView { server_cards: HashMap>, gallery_server_cards: HashMap>, @@ -221,12 +216,12 @@ impl MCPServersListPageView { search_editor.update(ctx, |editor, ctx| { editor.clear_buffer_and_reset_undo_stack(ctx); - editor.set_placeholder_text("Search MCP Servers", ctx); + editor.set_placeholder_text(i18n::t("settings.mcp.search.placeholder"), ctx); }); let search_bar = ctx.add_typed_action_view(|_| SearchBar::new(search_editor.clone())); let add_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Add", NakedTheme) + ActionButton::new(i18n::t("settings.mcp.add_button"), NakedTheme) .with_icon(Icon::Plus) .on_click(|ctx| ctx.dispatch_typed_action(MCPServersListPageViewAction::Add)) }); @@ -367,7 +362,7 @@ impl MCPServersListPageView { template .description .clone() - .or_else(|| Some("Available to install".to_string())), + .or_else(|| Some(i18n::t("settings.mcp.status.available_to_install"))), None, // Templates can never have tools None, // Templates cannot have an error title_chip_text.into_iter().collect(), @@ -835,7 +830,8 @@ impl MCPServersListPageView { // Show the toast that the server updated, even though we don't update the cloud template in this case let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::success(String::from("MCP server updated")); + let toast = + DismissibleToast::success(i18n::t("settings.mcp.toast.server_updated")); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); } @@ -1103,7 +1099,7 @@ impl MCPServersListPageView { let is_any_ai_enabled = ai_settings.is_any_ai_enabled(app); let label = render_body_item_label::( - "Auto-spawn servers from third-party agents".to_string(), + i18n::t("settings.mcp.auto_spawn.label"), None, None, LocalOnlyIconState::Hidden, @@ -1136,11 +1132,9 @@ impl MCPServersListPageView { Vec, > = std::sync::LazyLock::new(|| { vec![ - FormattedTextFragment::plain_text( - "Automatically detect and spawn MCP servers from globally-scoped third-party AI agent configuration files (e.g. in your home directory). Servers detected inside a repository are never spawned automatically and must be enabled individually in the \"Detected from\" sections below. ", - ), + FormattedTextFragment::plain_text(i18n::t("settings.mcp.auto_spawn.description")), FormattedTextFragment::hyperlink( - "See supported providers.", + i18n::t("settings.mcp.auto_spawn.see_providers_link"), "https://docs.warp.dev/agent-platform/capabilities/mcp#file-based-mcp-servers", ), ] @@ -1176,9 +1170,9 @@ impl MCPServersListPageView { fn render_page_body(&self, appearance: &Appearance, app: &AppContext) -> Box { let description_fragments = vec![ - FormattedTextFragment::plain_text(DESCRIPTION_TEXT), + FormattedTextFragment::plain_text(i18n::t("settings.mcp.page.description")), FormattedTextFragment::hyperlink( - "Learn more.", + i18n::t("settings.mcp.page.learn_more_link"), "https://docs.warp.dev/agent-platform/capabilities/mcp", ), ]; @@ -1261,8 +1255,9 @@ impl MCPServersListPageView { Self::separate_server_cards_by_installed(&filtered_server_cards, app); if !owned_server_cards.is_empty() { + let my_mcps_header = i18n::t("settings.mcp.section.my_mcps"); page.add_child(self.render_server_cards_section( - "My MCPs", + &my_mcps_header, &owned_server_cards, appearance, app, @@ -1274,8 +1269,9 @@ impl MCPServersListPageView { .current_team() .map(|team| team.name.clone()); let shared_by_text = match team_name { - Some(name) => format!("Shared by Warp and {name}"), - None => "Shared by Warp and from other devices".to_string(), + Some(name) => i18n::t("settings.mcp.section.shared_by_warp_and_team") + .replace("{name}", &name), + None => i18n::t("settings.mcp.section.shared_by_warp_and_devices"), }; page.add_child(self.render_server_cards_section( @@ -1285,8 +1281,9 @@ impl MCPServersListPageView { app, )); } else if !filtered_gallery_cards.is_empty() { + let shared_from_warp_header = i18n::t("settings.mcp.section.shared_from_warp"); page.add_child(self.render_server_cards_section( - "Shared from Warp", + &shared_from_warp_header, &filtered_gallery_cards, appearance, app, @@ -1295,7 +1292,8 @@ impl MCPServersListPageView { // Render one section per provider (e.g. "Detected from Claude"). for (provider, cards) in &filtered_file_based_cards { - let section_title = format!("Detected from {}", provider.display_name()); + let section_title = i18n::t("settings.mcp.section.detected_from") + .replace("{provider}", provider.display_name()); page.add_child(self.render_server_cards_section( §ion_title, cards, @@ -1491,7 +1489,7 @@ impl MCPServersListPageView { .with_child( appearance .ui_builder() - .wrappable_text(EMPTY_STATE_TEXT, true) + .wrappable_text(i18n::t("settings.mcp.empty_state"), true) .with_style(style::description_text(appearance)) .build() .finish(), @@ -1522,7 +1520,7 @@ impl MCPServersListPageView { .with_child( appearance .ui_builder() - .wrappable_text(NO_SEARCH_RESULTS_TEXT, true) + .wrappable_text(i18n::t("settings.mcp.no_search_results"), true) .with_style(style::description_text(appearance)) .build() .finish(), @@ -1632,7 +1630,7 @@ impl MCPServersListPageView { .templatable_mcp_server() .description .clone() - .or_else(|| Some("Detected from config file".to_string())), + .or_else(|| Some(i18n::t("settings.mcp.status.detected_from_config"))), None, // tools only available when running None, // no error when not yet started title_chips, @@ -1766,11 +1764,17 @@ impl MCPServersListPageView { if is_shared { match creator { - Some(creator) => Some(TitleChip::text(format!("Shared by: {creator}"))), - None => Some(TitleChip::text("Shared by a team member")), + Some(creator) => Some(TitleChip::text( + i18n::t("settings.mcp.chip.shared_by").replace("{creator}", &creator), + )), + None => Some(TitleChip::text(i18n::t( + "settings.mcp.chip.shared_by_team_member", + ))), } } else if matches!(item_id, ServerCardItemId::TemplatableMCP(_)) { - Some(TitleChip::text("From another device")) + Some(TitleChip::text(i18n::t( + "settings.mcp.chip.from_another_device", + ))) } else { None } diff --git a/app/src/settings_view/mcp_servers/server_card.rs b/app/src/settings_view/mcp_servers/server_card.rs index f82b7719af..d9d922d75b 100644 --- a/app/src/settings_view/mcp_servers/server_card.rs +++ b/app/src/settings_view/mcp_servers/server_card.rs @@ -222,7 +222,7 @@ impl From for ServerCardOptions { indicator_type: StatusElementTypes::Circle, color: StatusColor::Neutral, }), - status_line: Some("Offline".to_string()), + status_line: Some(i18n::t("settings.mcp.status.offline")), background: Background::Filled, full_card_clickable: false, }, @@ -242,7 +242,7 @@ impl From for ServerCardOptions { indicator_type: StatusElementTypes::Circle, color: StatusColor::Yellow, }), - status_line: Some("Starting server...".to_string()), + status_line: Some(i18n::t("settings.mcp.status.starting_server")), background: Background::Filled, full_card_clickable: false, }, @@ -262,7 +262,7 @@ impl From for ServerCardOptions { indicator_type: StatusElementTypes::Circle, color: StatusColor::Yellow, }), - status_line: Some("Authenticating...".to_string()), + status_line: Some(i18n::t("settings.mcp.status.authenticating")), background: Background::Filled, full_card_clickable: false, }, @@ -302,7 +302,7 @@ impl From for ServerCardOptions { indicator_type: StatusElementTypes::Circle, color: StatusColor::Neutral, }), - status_line: Some("Shutting down...".to_string()), + status_line: Some(i18n::t("settings.mcp.status.shutting_down")), background: Background::Filled, full_card_clickable: false, }, @@ -479,7 +479,7 @@ impl ServerCardView { if tools.is_empty() { return Text::new( - "No tools available".to_string(), + i18n::t("settings.mcp.tools.none_available"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -501,7 +501,8 @@ impl ServerCardView { .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Text::new( - format!("{} tools available", tools.len()), + i18n::t("settings.mcp.tools.count_available") + .replace("{count}", &tools.len().to_string()), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -750,7 +751,7 @@ impl ServerCardView { self.build_icon_button( appearance, Icon::Code1, - "Show logs".to_string(), + i18n::t("settings.mcp.action.show_logs"), self.mouse_handles.show_logs_icon_button.clone(), ) .on_click(move |ctx, _, _| { @@ -765,7 +766,7 @@ impl ServerCardView { self.build_icon_button( appearance, Icon::LogOut, - "Log out".to_string(), + i18n::t("settings.mcp.action.log_out"), self.mouse_handles.logout_icon_button.clone(), ) .on_click(move |ctx, _, _| { @@ -780,7 +781,7 @@ impl ServerCardView { self.build_icon_button( appearance, Icon::Share, - "Share server".to_string(), + i18n::t("settings.mcp.action.share_server"), self.mouse_handles.share_icon_button.clone(), ) .on_click(move |ctx, _, _| { @@ -795,7 +796,7 @@ impl ServerCardView { self.build_icon_button( appearance, Icon::Pencil, - "Edit".to_string(), + i18n::t("settings.mcp.action.edit"), self.mouse_handles.edit_icon_button.clone(), ) .on_click(move |ctx, _, _| { @@ -817,7 +818,7 @@ impl ServerCardView { ButtonVariant::Secondary, self.mouse_handles.view_logs_button.clone(), ) - .with_centered_text_label("View logs".to_string()) + .with_centered_text_label(i18n::t("settings.mcp.action.view_logs")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(ServerCardAction::ViewLogs(item_id)) @@ -833,7 +834,7 @@ impl ServerCardView { ButtonVariant::Accent, self.mouse_handles.edit_config_button.clone(), ) - .with_centered_text_label("Edit config".to_string()) + .with_centered_text_label(i18n::t("settings.mcp.action.edit_config")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(ServerCardAction::Edit(item_id)); @@ -849,7 +850,7 @@ impl ServerCardView { ButtonVariant::Accent, self.mouse_handles.setup_button.clone(), ) - .with_centered_text_label("Set up".to_string()) + .with_centered_text_label(i18n::t("settings.mcp.action.set_up")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(ServerCardAction::Install(item_id)); @@ -897,7 +898,7 @@ impl ServerCardView { .build_icon_button( appearance, Icon::Refresh, - "Server update available".to_string(), + i18n::t("settings.mcp.action.server_update_available"), self.mouse_handles.update_icon_button.clone(), ) .on_click(move |ctx, _, _| { diff --git a/app/src/settings_view/mcp_servers/update_modal.rs b/app/src/settings_view/mcp_servers/update_modal.rs index 8652622c49..886273a1d3 100644 --- a/app/src/settings_view/mcp_servers/update_modal.rs +++ b/app/src/settings_view/mcp_servers/update_modal.rs @@ -57,14 +57,14 @@ pub struct UpdateModalBody { impl UpdateModalBody { pub fn new(ctx: &mut ViewContext) -> Self { let cancel_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Cancel", NakedTheme).on_click(|ctx| { + ActionButton::new(i18n::t("common.cancel"), NakedTheme).on_click(|ctx| { ctx.dispatch_typed_action(UpdateModalBodyAction::Cancel); }) }); let enter_keystroke = Keystroke::parse("enter").expect("valid keystroke"); let update_button = ctx.add_typed_action_view(|ctx| { - let mut button = ActionButton::new("Update", PrimaryTheme) + let mut button = ActionButton::new(i18n::t("common.update"), PrimaryTheme) .with_keybinding(KeystrokeSource::Fixed(enter_keystroke), ctx) .on_click(|ctx| { ctx.dispatch_typed_action(UpdateModalBodyAction::Update); @@ -123,13 +123,16 @@ impl UpdateModalBody { fn render_title(&self, appearance: &Appearance) -> Box { let theme = appearance.theme(); - let name = self.server_name.as_deref().unwrap_or("Server"); + let name = self + .server_name + .clone() + .unwrap_or_else(|| i18n::t("settings.mcp.server_fallback")); // Renders MCP avatar icon - let avatar_content = if let Some(icon) = ExternalProductIcon::from_string(name) { + let avatar_content = if let Some(icon) = ExternalProductIcon::from_string(&name) { AvatarContent::ExternalProductIcon(icon) } else { - AvatarContent::DisplayName(name.to_string()) + AvatarContent::DisplayName(name.clone()) }; let avatar = Avatar::new( avatar_content, @@ -153,7 +156,7 @@ impl UpdateModalBody { // Renders MCP title text let title = Text::new( - format!("Update {name}"), + i18n::t("settings.mcp.modal.update_title").replace("{name}", &name), appearance.ui_font_family(), appearance.header_font_size(), ) @@ -220,10 +223,8 @@ impl UpdateModalBody { fn render_description(&self, appearance: &Appearance) -> Box { // Modal appears only when multiple updates are available - let description = format!( - "This server has {} updates available, which would you like to proceed with?", - self.update_options.len() - ); + let description = i18n::t("settings.mcp.modal.multiple_updates_description") + .replace("{count}", &self.update_options.len().to_string()); Text::new( description, @@ -257,9 +258,9 @@ impl UpdateModalBody { .. } => { let publisher_string = match publisher { - Author::CurrentUser => "another device", - Author::OtherUser { name } => name, - Author::Unknown => "a team member", + Author::CurrentUser => i18n::t("settings.mcp.update_option.another_device"), + Author::OtherUser { name } => name.clone(), + Author::Unknown => i18n::t("settings.mcp.update_option.team_member"), }; let datetime = Local .timestamp_opt(*new_version_ts, 0) @@ -267,16 +268,21 @@ impl UpdateModalBody { .unwrap_or_else(Local::now); let formatted_time = format_approx_duration_from_now(datetime); ( - format!("Update from {publisher_string}"), + i18n::t("settings.mcp.update_option.from") + .replace("{source}", &publisher_string), formatted_time.to_string(), ) } MCPServerUpdate::Gallery { name, new_version, .. - } => ( - format!("Update from {name}"), - format!("Version {new_version}"), - ), + } => { + let new_version = new_version.to_string(); + ( + i18n::t("settings.mcp.update_option.from").replace("{source}", name), + i18n::t("settings.mcp.update_option.version") + .replace("{version}", &new_version), + ) + } }; let content = Flex::column() @@ -391,7 +397,7 @@ impl View for UpdateModalBody { // Add update options if self.update_options.is_empty() { let no_updates_text = Text::new( - "No updates available", + i18n::t("settings.mcp.no_updates_available"), appearance.ui_font_family(), appearance.ui_font_size(), ) diff --git a/app/src/settings_view/mcp_servers_page.rs b/app/src/settings_view/mcp_servers_page.rs index 23ee824983..7cf9c25799 100644 --- a/app/src/settings_view/mcp_servers_page.rs +++ b/app/src/settings_view/mcp_servers_page.rs @@ -50,7 +50,6 @@ pub enum InstallOrigin { Deeplink, } -const PAGE_TITLE_TEXT: &str = "MCP Servers"; #[derive(Debug, Default, Copy, Clone)] pub enum MCPServersSettingsPage { #[default] @@ -102,7 +101,9 @@ impl MCPServersSettingsPageView { Self { page: PageType::new_monolith( MCPServersSettingsWidget::default(), - Some(PAGE_TITLE_TEXT), + Some(Box::leak( + i18n::t("settings.mcp.page.title").into_boxed_str(), + )), true, ), current_page: MCPServersSettingsPage::default(), @@ -148,8 +149,8 @@ impl MCPServersSettingsPageView { ctx: &mut ViewContext, ) { let message = match server_name { - Some(name) => format!("Successfully logged out of {name} MCP server"), - None => "Successfully logged out of MCP server".to_string(), + Some(name) => i18n::t("settings.mcp.toast.logged_out_named").replace("{name}", &name), + None => i18n::t("settings.mcp.toast.logged_out"), }; match item_id { ServerCardItemId::TemplatableMCP(_) => { @@ -315,10 +316,7 @@ impl MCPServersSettingsPageView { log::warn!( "Ignoring MCP deeplink autoinstall for '{autoinstall_param}': installation modal already open" ); - self.add_error_toast( - "Finish the current MCP install before opening another install link.".to_string(), - ctx, - ); + self.add_error_toast(i18n::t("settings.mcp.error.finish_install_first"), ctx); return; } @@ -331,7 +329,10 @@ impl MCPServersSettingsPageView { log::warn!( "Unrecognized autoinstall value '{autoinstall_param}': no matching gallery item found" ); - self.add_error_toast(format!("Unknown MCP server '{autoinstall_param}'"), ctx); + self.add_error_toast( + i18n::t("settings.mcp.error.unknown_server").replace("{name}", autoinstall_param), + ctx, + ); return; }; @@ -359,7 +360,8 @@ impl MCPServersSettingsPageView { // gallery entry cannot be turned into a valid template. Surface the // failure to the user rather than silently returning. self.add_error_toast( - format!("MCP server '{gallery_title}' cannot be installed from this link."), + i18n::t("settings.mcp.error.cannot_install_from_link") + .replace("{name}", &gallery_title), ctx, ); return; diff --git a/app/src/settings_view/mod.rs b/app/src/settings_view/mod.rs index b64b35d34e..1287841f78 100644 --- a/app/src/settings_view/mod.rs +++ b/app/src/settings_view/mod.rs @@ -168,7 +168,7 @@ pub(super) fn render_beta_chip(appearance: &Appearance) -> Box { let theme = appearance.theme(); let chip_color = theme.sub_text_color(theme.surface_3()).into_solid(); Container::new( - Text::new_inline("BETA", appearance.ui_font_family(), 10.) + Text::new_inline(i18n::t("common.beta"), appearance.ui_font_family(), 10.) .with_color(chip_color) .finish(), ) @@ -301,6 +301,41 @@ impl Display for SettingsSection { } impl SettingsSection { + /// Returns the localized sidebar navigation label for this section. + /// + /// This mirrors the English text produced by `Display`, but routes it + /// through the i18n catalog so the sidebar can be translated. `Display` + /// itself must remain unchanged because it feeds crash-reporting and + /// `FromStr`. + pub fn nav_label(&self) -> String { + match self { + SettingsSection::About => i18n::t("settings.nav.about"), + SettingsSection::Account => i18n::t("settings.nav.account"), + SettingsSection::MCPServers => i18n::t("settings.nav.mcp_servers"), + SettingsSection::BillingAndUsage => i18n::t("settings.nav.billing_and_usage"), + SettingsSection::Appearance => i18n::t("settings.nav.appearance"), + SettingsSection::Features => i18n::t("settings.nav.features"), + SettingsSection::Keybindings => i18n::t("settings.nav.keybindings"), + SettingsSection::Privacy => i18n::t("settings.nav.privacy"), + SettingsSection::Referrals => i18n::t("settings.nav.referrals"), + SettingsSection::SharedBlocks => i18n::t("settings.nav.shared_blocks"), + SettingsSection::Teams => i18n::t("settings.nav.teams"), + SettingsSection::WarpDrive => i18n::t("settings.nav.warp_drive"), + SettingsSection::Warpify => i18n::t("settings.nav.warpify"), + SettingsSection::AI => i18n::t("settings.nav.ai"), + SettingsSection::WarpAgent => i18n::t("settings.nav.warp_agent"), + SettingsSection::AgentProfiles => i18n::t("settings.nav.agent_profiles"), + SettingsSection::AgentMCPServers => i18n::t("settings.nav.agent_mcp_servers"), + SettingsSection::Knowledge => i18n::t("settings.nav.knowledge"), + SettingsSection::ThirdPartyCLIAgents => i18n::t("settings.nav.third_party_cli_agents"), + SettingsSection::Code => i18n::t("settings.nav.code"), + SettingsSection::CodeIndexing => i18n::t("settings.nav.code_indexing"), + SettingsSection::EditorAndCodeReview => i18n::t("settings.nav.editor_and_code_review"), + SettingsSection::CloudEnvironments => i18n::t("settings.nav.cloud_environments"), + SettingsSection::OzCloudAPIKeys => i18n::t("settings.nav.oz_cloud_api_keys"), + } + } + /// Returns true if this section is a subpage under any umbrella. pub fn is_subpage(&self) -> bool { self.is_ai_subpage() || self.is_code_subpage() || self.is_cloud_platform_subpage() @@ -606,8 +641,8 @@ pub fn init_actions_from_parent_view( vec![ ToggleSettingActionPair::custom( SettingActionPairDescriptions::new( - "Show initialization block", - "Hide initialization block", + i18n::t("settings.action.show_initialization_block"), + i18n::t("settings.action.hide_initialization_block"), ), builder(SettingsAction::Debug( DebugSettingsAction::ToggleInitializationBlock, @@ -620,8 +655,8 @@ pub fn init_actions_from_parent_view( ), ToggleSettingActionPair::custom( SettingActionPairDescriptions::new( - "Show in-band command blocks", - "Hide in-band command blocks", + i18n::t("settings.action.show_in_band_command_blocks"), + i18n::t("settings.action.hide_in_band_command_blocks"), ), builder(SettingsAction::Debug( DebugSettingsAction::ToggleInBandCommandBlocks, @@ -641,25 +676,25 @@ pub fn init_actions_from_parent_view( ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ ToggleSettingActionPair::new( - "recording mode", + i18n::t("settings.action.recording_mode"), WorkspaceAction::ToggleRecordingMode, &id!("Workspace"), flags::RECORDING_MODE_FLAG, ), ToggleSettingActionPair::new( - "in-band generators for new sessions", + i18n::t("settings.action.in_band_generators_for_new_sessions"), WorkspaceAction::ToggleInBandGenerators, &id!("Workspace"), flags::IN_BAND_GENERATORS_FLAG, ), ToggleSettingActionPair::new( - "debug network status", + i18n::t("settings.action.debug_network_status"), WorkspaceAction::ToggleDebugNetworkStatus, &id!("Workspace"), flags::DEBUG_NETWORK_ONLINE_FLAG, ), ToggleSettingActionPair::new( - "memory statistics", + i18n::t("settings.action.memory_statistics"), WorkspaceAction::ToggleShowMemoryStats, &id!("Workspace"), flags::DEBUG_SHOW_MEMORY_STATS_FLAG, @@ -684,10 +719,10 @@ pub struct SettingActionPairDescriptions { } impl SettingActionPairDescriptions { - pub fn new(enable: &str, disable: &str) -> Self { + pub fn new(enable: impl Into, disable: impl Into) -> Self { Self { - enable: enable.to_owned(), - disable: disable.to_owned(), + enable: enable.into(), + disable: disable.into(), } } } @@ -750,17 +785,20 @@ impl ToggleSettingActionPair { /// is in the enabled state, /// and absent when the action is in the disabled state. pub fn new( - description_suffix: &str, + description_suffix: impl AsRef, toggle_action: T, context_prefix: &ContextPredicate, context_boolean_flag: &'static str, ) -> Self { use warpui::keymap::macros::id; + let description_suffix = description_suffix.as_ref(); ToggleSettingActionPair { descriptions: SettingActionPairDescriptions { - enable: format!("Enable {description_suffix}"), - disable: format!("Disable {description_suffix}"), + enable: i18n::t("settings.action.enable") + .replace("{description}", description_suffix), + disable: i18n::t("settings.action.disable") + .replace("{description}", description_suffix), }, contexts: SettingActionPairContexts { enable_predicate: context_prefix.to_owned() & !id!(context_boolean_flag), @@ -1100,7 +1138,8 @@ pub struct SettingsView { impl SettingsView { pub fn new(page: Option, ctx: &mut ViewContext) -> Self { - let pane_configuration = ctx.add_model(|_ctx| PaneConfiguration::new("Settings")); + let pane_configuration = + ctx.add_model(|_ctx| PaneConfiguration::new(i18n::t("settings.title"))); let global_resource_handles = GlobalResourceHandlesProvider::as_ref(ctx).get().clone(); // Main settings page with accounts info @@ -1234,7 +1273,7 @@ impl SettingsView { ..Default::default() }; let mut editor = EditorView::single_line(options, ctx); - editor.set_placeholder_text("Search", ctx); + editor.set_placeholder_text(i18n::t("common.search"), ctx); editor }); @@ -1593,28 +1632,28 @@ impl SettingsView { if ContextFlag::CreateNewSession.is_enabled() { items.extend(vec![ - MenuItemFields::new("Split pane right") + MenuItemFields::new(i18n::t("common.split_pane_right")) .with_on_select_action(SettingsAction::Split(Direction::Right)) .with_key_shortcut_label(keybinding_name_to_display_string( "pane_group:add_right", ctx, )) .into_item(), - MenuItemFields::new("Split pane left") + MenuItemFields::new(i18n::t("common.split_pane_left")) .with_on_select_action(SettingsAction::Split(Direction::Left)) .with_key_shortcut_label(keybinding_name_to_display_string( "pane_group:add_left", ctx, )) .into_item(), - MenuItemFields::new("Split pane down") + MenuItemFields::new(i18n::t("common.split_pane_down")) .with_on_select_action(SettingsAction::Split(Direction::Down)) .with_key_shortcut_label(keybinding_name_to_display_string( "pane_group:add_down", ctx, )) .into_item(), - MenuItemFields::new("Split pane up") + MenuItemFields::new(i18n::t("common.split_pane_up")) .with_on_select_action(SettingsAction::Split(Direction::Up)) .with_key_shortcut_label(keybinding_name_to_display_string( "pane_group:add_up", @@ -1643,7 +1682,7 @@ impl SettingsView { ); items.push( - MenuItemFields::new("Close pane") + MenuItemFields::new(i18n::t("common.close_pane")) .with_on_select_action(SettingsAction::Close) .with_key_shortcut_label( custom_tag_to_keystroke(CustomAction::CloseCurrentSession.into()) @@ -2323,10 +2362,10 @@ impl SettingsView { Container::new( Align::new( Flex::column() - .with_cross_axis_alignment(CrossAxisAlignment::Center) + .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_children([ Text::new( - "No settings match your search.", + i18n::t("settings.search.no_matches"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -2334,7 +2373,7 @@ impl SettingsView { .with_color(theme.sub_text_color(theme.background()).into_solid()) .finish(), Text::new( - "You may want to try using different keywords or checking for any possible typos.", + i18n::t("settings.search.no_matches_hint"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -2345,7 +2384,7 @@ impl SettingsView { ) .finish(), ) - .with_uniform_margin(16.) + .with_uniform_margin(16.) .with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.))) .with_background(internal_colors::fg_overlay_1(appearance.theme())) .finish() @@ -2753,7 +2792,7 @@ impl BackingView for SettingsView { _ctx: &view::HeaderRenderContext<'_>, _app: &AppContext, ) -> view::HeaderContent { - view::HeaderContent::simple("Settings") + view::HeaderContent::simple(i18n::t("settings.title")) } fn set_focus_handle(&mut self, focus_handle: PaneFocusHandle, _ctx: &mut ViewContext) { diff --git a/app/src/settings_view/nav.rs b/app/src/settings_view/nav.rs index 0c462c80be..0c7695988a 100644 --- a/app/src/settings_view/nav.rs +++ b/app/src/settings_view/nav.rs @@ -64,6 +64,14 @@ impl SettingsUmbrella { // rendered, so this just seeds a sensible default. let text_color = appearance.theme().nonactive_ui_text_color(); + // Map the umbrella's English label to its localized counterpart. + let localized_label = match self.label { + "Agents" => i18n::t("settings.nav.umbrella.agents"), + "Code" => i18n::t("settings.nav.umbrella.code"), + "Cloud platform" => i18n::t("settings.nav.umbrella.cloud_platform"), + other => other.to_string(), + }; + // Use a single full-width text button with a text+icon label so the // text label aligns with other top-level settings items and the // chevron sits flush-right — while the whole button area receives the @@ -73,7 +81,7 @@ impl SettingsUmbrella { .button(ButtonVariant::Text, self.button_state_handle.clone()) .with_text_and_icon_label(TextAndIcon::new( TextAndIconAlignment::TextFirst, - self.label.to_string(), + localized_label, chevron_icon.to_warpui_icon(text_color), MainAxisSize::Max, MainAxisAlignment::SpaceBetween, @@ -99,7 +107,7 @@ impl SettingsUmbrella { let section = self.subpages.get(index)?; let mouse_state = self.subpage_button_states.get(index)?.clone(); - let label = section.to_string() + &match_data.to_string(); + let label = section.nav_label() + &match_data.to_string(); let hoverable = appearance .ui_builder() diff --git a/app/src/settings_view/platform/create_api_key_modal.rs b/app/src/settings_view/platform/create_api_key_modal.rs index af75840fed..99f01f34f1 100644 --- a/app/src/settings_view/platform/create_api_key_modal.rs +++ b/app/src/settings_view/platform/create_api_key_modal.rs @@ -42,17 +42,11 @@ pub(crate) enum ApiKeyType { } impl ApiKeyType { - fn description(&self) -> &'static str { + fn description(&self) -> String { match self { - ApiKeyType::Personal => { - "This API key is tied to your user and can make requests against your Warp account." - } - ApiKeyType::Team => { - "This API key is tied to your team and can make requests on behalf of your team." - } - ApiKeyType::Agent => { - "This API key is tied to an agent and can make requests on behalf of the agent." - } + ApiKeyType::Personal => i18n::t("settings.platform.api_key.personal.description"), + ApiKeyType::Team => i18n::t("settings.platform.api_key.team.description"), + ApiKeyType::Agent => i18n::t("settings.platform.api_key.agent.description"), } } } @@ -85,12 +79,16 @@ pub(crate) enum ExpirationOption { } impl ExpirationOption { - fn display_text(&self) -> &'static str { + fn display_text(&self) -> String { match self { - ExpirationOption::OneDay => "1 day", - ExpirationOption::ThirtyDays => "30 days", - ExpirationOption::NinetyDays => "90 days", - ExpirationOption::Never => "Never", + ExpirationOption::OneDay => i18n::t("settings.platform.api_key.expiration.one_day"), + ExpirationOption::ThirtyDays => { + i18n::t("settings.platform.api_key.expiration.thirty_days") + } + ExpirationOption::NinetyDays => { + i18n::t("settings.platform.api_key.expiration.ninety_days") + } + ExpirationOption::Never => i18n::t("settings.platform.api_key.expiration.never"), } } @@ -159,7 +157,7 @@ impl CreateApiKeyModal { ..Default::default() }; let mut editor = EditorView::single_line(options, ctx); - editor.set_placeholder_text("Warp API Key", ctx); + editor.set_placeholder_text(i18n::t("settings.platform.api_key.name_placeholder"), ctx); editor }); @@ -192,9 +190,15 @@ impl CreateApiKeyModal { icon_color: theme.active_ui_text_color().into(), label: Some(LabelConfig { label: match key_type { - ApiKeyType::Personal => "Personal".into(), - ApiKeyType::Team => "Team".into(), - ApiKeyType::Agent => "Agent".into(), + ApiKeyType::Personal => { + i18n::t("settings.platform.api_key.type.personal").into() + } + ApiKeyType::Team => { + i18n::t("settings.platform.api_key.type.team").into() + } + ApiKeyType::Agent => { + i18n::t("settings.platform.api_key.type.agent").into() + } }, width_override: Some(55.0), color: if is_selected { @@ -290,8 +294,7 @@ impl CreateApiKeyModal { Err(err) => { log::error!("Failed to load agent identities: {err}"); ctx.emit(CreateApiKeyModalEvent::Error { - message: "Failed to load agents. Please close and try again." - .to_string(), + message: i18n::t("settings.platform.api_key.load_agents_failed"), }); } } @@ -324,7 +327,7 @@ impl CreateApiKeyModal { let name = self.name_editor.as_ref(ctx).buffer_text(ctx); let final_name = if name.trim().is_empty() { - "Warp API Key".to_string() + i18n::t("settings.platform.api_key.name_placeholder") } else { name.trim().to_string() }; @@ -348,7 +351,7 @@ impl CreateApiKeyModal { None => { self.request_state = RequestState::Idle; ctx.emit(CreateApiKeyModalEvent::Error { - message: "Please select an agent.".to_string(), + message: i18n::t("settings.platform.api_key.select_agent_error"), }); ctx.notify(); return; @@ -365,9 +368,7 @@ impl CreateApiKeyModal { None => { self.request_state = RequestState::Idle; ctx.emit(CreateApiKeyModalEvent::Error { - message: - "Unable to create a team API key because there is no current team." - .to_string(), + message: i18n::t("settings.platform.api_key.no_current_team_error"), }); ctx.notify(); return; @@ -397,7 +398,7 @@ impl CreateApiKeyModal { } Ok(warp_graphql::mutations::generate_api_key::GenerateApiKeyResult::Unknown) | Err(_) => { me.request_state = RequestState::Idle; - ctx.emit(CreateApiKeyModalEvent::Error { message: "Failed to create API key. Please try again.".to_string() }); + ctx.emit(CreateApiKeyModalEvent::Error { message: i18n::t("settings.platform.api_key.create_failed") }); ctx.notify(); } } @@ -473,7 +474,7 @@ impl CreateApiKeyModal { }; let info = Text::new( - "This secret key is shown only once. Copy and store it securely.", + i18n::t("settings.platform.api_key.secret_notice"), appearance.ui_font_family(), LABEL_FONT_SIZE, ) @@ -495,9 +496,9 @@ impl CreateApiKeyModal { .finish(); let copy_label = if self.raw_key_copied { - "Copied" + i18n::t("common.copied") } else { - "Copy" + i18n::t("common.copy") }; let copy_icon = if self.raw_key_copied { warp_core::ui::icons::Icon::Check.to_warpui_icon(appearance.theme().background()) @@ -546,7 +547,7 @@ impl CreateApiKeyModal { ButtonVariant::Accent, self.cancel_button_mouse_state.clone(), ) - .with_text_label("Done".to_string()) + .with_text_label(i18n::t("common.done")) .with_style(button_style) .build() .on_click(|ctx, _, _| ctx.dispatch_typed_action(CreateApiKeyModalAction::Cancel)) @@ -610,9 +611,13 @@ impl View for CreateApiKeyModal { .with_color(theme.nonactive_ui_text_color().into()) .finish(); - let name_label = Text::new("Name", appearance.ui_font_family(), LABEL_FONT_SIZE) - .with_color(theme.active_ui_text_color().into()) - .finish(); + let name_label = Text::new( + i18n::t("common.name"), + appearance.ui_font_family(), + LABEL_FONT_SIZE, + ) + .with_color(theme.active_ui_text_color().into()) + .finish(); let is_pending = self.request_state == RequestState::Pending; @@ -626,7 +631,7 @@ impl View for CreateApiKeyModal { ButtonVariant::Secondary, self.cancel_button_mouse_state.clone(), ) - .with_text_label("Cancel".to_string()) + .with_text_label(i18n::t("common.cancel")) .with_style(button_style) .build() .on_click(move |ctx, _, _| { @@ -644,9 +649,9 @@ impl View for CreateApiKeyModal { self.create_button_mouse_state.clone(), ) .with_text_label(if is_pending { - "Creating…".to_string() + i18n::t("settings.platform.api_key.creating") } else { - "Create key".to_string() + i18n::t("settings.platform.api_key.create_key") }) .with_style(button_style) .build() @@ -674,10 +679,13 @@ impl View for CreateApiKeyModal { let mut render_agent_dropdown = false; if self.has_team || self.has_named_agents { - let type_label = - Text::new("Type", appearance.ui_font_family(), LABEL_FONT_SIZE) - .with_color(theme.active_ui_text_color().into()) - .finish(); + let type_label = Text::new( + i18n::t("common.type"), + appearance.ui_font_family(), + LABEL_FONT_SIZE, + ) + .with_color(theme.active_ui_text_color().into()) + .finish(); col.add_child(Container::new(type_label).with_margin_bottom(4.).finish()); col.add_child( Container::new(ChildView::new(&self.api_key_type_control).finish()) @@ -693,10 +701,13 @@ impl View for CreateApiKeyModal { ); if selected_key_type == ApiKeyType::Agent { - let agent_label = - Text::new("Agent", appearance.ui_font_family(), LABEL_FONT_SIZE) - .with_color(theme.active_ui_text_color().into()) - .finish(); + let agent_label = Text::new( + i18n::t("common.agent"), + appearance.ui_font_family(), + LABEL_FONT_SIZE, + ) + .with_color(theme.active_ui_text_color().into()) + .finish(); col.add_child(Container::new(agent_label).with_margin_bottom(4.).finish()); let available_agents: Vec<&AgentIdentity> = @@ -704,7 +715,7 @@ impl View for CreateApiKeyModal { if !self.is_loading_agents && available_agents.is_empty() { let empty_text = Text::new( - "No agents available. Create one first.", + i18n::t("settings.platform.api_key.no_agents_available"), appearance.ui_font_family(), LABEL_FONT_SIZE, ) @@ -717,7 +728,7 @@ impl View for CreateApiKeyModal { ButtonVariant::Secondary, self.create_agent_button_mouse_state.clone(), ) - .with_text_label("Create agent".to_string()) + .with_text_label(i18n::t("settings.platform.api_key.create_agent")) .with_style(button_style) .build() .on_click(|ctx, _, _| { @@ -775,10 +786,13 @@ impl View for CreateApiKeyModal { .finish(), ); - let expiration_label = - Text::new("Expiration", appearance.ui_font_family(), LABEL_FONT_SIZE) - .with_color(theme.active_ui_text_color().into()) - .finish(); + let expiration_label = Text::new( + i18n::t("common.expiration"), + appearance.ui_font_family(), + LABEL_FONT_SIZE, + ) + .with_color(theme.active_ui_text_color().into()) + .finish(); col.add_child( Container::new(expiration_label) @@ -835,9 +849,9 @@ impl TypedActionView for CreateApiKeyModal { // Success toast let window_id = ctx.window_id(); crate::ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = crate::view_components::DismissibleToast::success( - "Secret key copied.".to_string(), - ); + let toast = crate::view_components::DismissibleToast::success(i18n::t( + "settings.platform.api_key.secret_copied", + )); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); ctx.notify(); diff --git a/app/src/settings_view/platform/expire_api_key_button.rs b/app/src/settings_view/platform/expire_api_key_button.rs index cdb6d9e714..86d36e3d50 100644 --- a/app/src/settings_view/platform/expire_api_key_button.rs +++ b/app/src/settings_view/platform/expire_api_key_button.rs @@ -74,7 +74,7 @@ impl ExpireApiKeyButton { | Err(_) => { me.request_state = RequestState::Idle; ctx.emit(ExpireApiKeyButtonEvent::ExpireApiKeyFailed { - message: "Failed to delete API key. Please try again.".to_string(), + message: i18n::t("settings.platform.api_key.delete_failed"), }); ctx.notify(); } diff --git a/app/src/settings_view/platform_page.rs b/app/src/settings_view/platform_page.rs index 491fa0d393..04efd4eef3 100644 --- a/app/src/settings_view/platform_page.rs +++ b/app/src/settings_view/platform_page.rs @@ -171,7 +171,10 @@ impl PlatformPageView { ..Default::default() }; let mut editor = EditorView::single_line(options, ctx); - editor.set_placeholder_text("Search API keys", ctx); + editor.set_placeholder_text( + i18n::t("settings.platform.search_api_keys_placeholder"), + ctx, + ); editor }); ctx.subscribe_to_view(&api_key_search_editor, |me, _, event, ctx| { @@ -186,34 +189,38 @@ impl PlatformPageView { }); let create_api_key_modal_view = ctx.add_typed_action_view(|ctx| { - Modal::new(Some("New API key".to_string()), create_api_key_body, ctx) - .with_modal_style(UiComponentStyles { - width: Some(MODAL_WIDTH), - height: Some(MODAL_HEIGHT), - ..Default::default() - }) - .with_header_style(UiComponentStyles { - padding: Some(Coords { - top: 24., - bottom: 0., - left: 24., - right: 24., - }), - font_size: Some(16.), - font_weight: Some(warpui::fonts::Weight::Bold), - ..Default::default() - }) - .with_body_style(UiComponentStyles { - padding: Some(Coords { - top: 0., - bottom: 24., - left: 24., - right: 24., - }), - ..Default::default() - }) - .with_background_opacity(100) - .with_dismiss_on_click() + Modal::new( + Some(i18n::t("settings.platform.new_api_key_title")), + create_api_key_body, + ctx, + ) + .with_modal_style(UiComponentStyles { + width: Some(MODAL_WIDTH), + height: Some(MODAL_HEIGHT), + ..Default::default() + }) + .with_header_style(UiComponentStyles { + padding: Some(Coords { + top: 24., + bottom: 0., + left: 24., + right: 24., + }), + font_size: Some(16.), + font_weight: Some(warpui::fonts::Weight::Bold), + ..Default::default() + }) + .with_body_style(UiComponentStyles { + padding: Some(Coords { + top: 0., + bottom: 24., + left: 24., + right: 24., + }), + ..Default::default() + }) + .with_background_opacity(100) + .with_dismiss_on_click() }); ctx.subscribe_to_view(&create_api_key_modal_view, |me, _, event, ctx| { me.handle_modal_event(event, ctx); @@ -237,7 +244,7 @@ impl PlatformPageView { fn show_create_api_key_modal(&mut self, ctx: &mut ViewContext) { self.create_api_key_modal_state - .set_title(Some("New API key".to_string()), ctx); + .set_title(Some(i18n::t("settings.platform.new_api_key_title")), ctx); self.create_api_key_modal_state.open(ctx); ctx.emit(PlatformPageViewEvent::ShowCreateApiKeyModal); } @@ -266,7 +273,7 @@ impl PlatformPageView { } CreateApiKeyModalEvent::Created { api_key } => { self.create_api_key_modal_state - .set_title(Some("Save your key".to_string()), ctx); + .set_title(Some(i18n::t("settings.platform.save_your_key_title")), ctx); let ui_key = APIKeyProperties::from(api_key); self.ensure_expire_button_for_key(ctx, ui_key.uid.clone()); self.api_keys.push(ui_key); @@ -319,9 +326,9 @@ impl PlatformPageView { me.expire_buttons.remove(uid); let window_id = ctx.window_id(); crate::ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = crate::view_components::DismissibleToast::success( - "API key deleted".to_string(), - ); + let toast = crate::view_components::DismissibleToast::success(i18n::t( + "settings.platform.api_key_deleted_toast", + )); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); ctx.notify(); @@ -489,8 +496,11 @@ impl PlatformPageWidget { appearance: &Appearance, ) -> Box { let text = vec![ - FormattedTextFragment::plain_text("Create and manage API keys to allow other Oz cloud agents to access your Warp account.\nFor more information, visit the "), - FormattedTextFragment::hyperlink("Documentation.", API_KEY_DOCS_URL), + FormattedTextFragment::plain_text(i18n::t("settings.platform.description")), + FormattedTextFragment::hyperlink( + i18n::t("settings.platform.documentation_link"), + API_KEY_DOCS_URL, + ), ]; let text_element = FormattedTextElement::new( @@ -524,11 +534,15 @@ impl PlatformPageWidget { Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( - Text::new_inline("Oz Cloud API Keys", appearance.ui_font_family(), 16.) - .with_style(Properties::default().weight(Weight::Bold)) - .with_color(appearance.theme().active_ui_text_color().into()) - .with_clip(ClipConfig::end()) - .finish(), + Text::new_inline( + i18n::t("settings.platform.section_header"), + appearance.ui_font_family(), + 16., + ) + .with_style(Properties::default().weight(Weight::Bold)) + .with_color(appearance.theme().active_ui_text_color().into()) + .with_clip(ClipConfig::end()) + .finish(), ) .with_child(Shrinkable::new(1.0, Empty::new().finish()).finish()) .with_child( @@ -537,7 +551,7 @@ impl PlatformPageWidget { ButtonVariant::Outlined, self.create_api_key_button_mouse_state.clone(), ) - .with_text_label("+ Create API Key".to_string()) + .with_text_label(i18n::t("settings.platform.create_api_key_button")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(PlatformPageAction::ShowCreateApiKeyModal); @@ -598,34 +612,41 @@ impl PlatformPageWidget { FeatureFlag::TeamApiKeys.is_enabled() || FeatureFlag::NamedAgents.is_enabled(); let min_non_resizable_columns_width = api_key_table_min_non_resizable_columns_width(show_scope_column); + let name_header = i18n::t("settings.platform.column_name"); + let key_header = i18n::t("settings.platform.column_key"); + let scope_header = i18n::t("settings.platform.column_scope"); + let created_header = i18n::t("settings.platform.column_created"); + let last_used_header = i18n::t("settings.platform.column_last_used"); + let expires_at_header = i18n::t("settings.platform.column_expires_at"); let mut header_row = Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_main_axis_size(MainAxisSize::Max); header_row.add_child(self.render_resizable_header_cell( appearance, - "Name", + &name_header, view.api_key_table_column_widths.name.clone(), API_KEY_NAME_COLUMN_MIN_WIDTH, min_non_resizable_columns_width, table_width_chrome, )); header_row.add_child( - ConstrainedBox::new(self.render_header_cell(appearance, "Key")) + ConstrainedBox::new(self.render_header_cell(appearance, &key_header)) .with_width(API_KEY_KEY_COLUMN_WIDTH) .finish(), ); if show_scope_column { header_row.add_child( - Expanded::new(1., self.render_header_cell(appearance, "Scope")).finish(), + Expanded::new(1., self.render_header_cell(appearance, &scope_header)).finish(), ); } - header_row - .add_child(Expanded::new(1., self.render_header_cell(appearance, "Created")).finish()); header_row.add_child( - Expanded::new(1., self.render_header_cell(appearance, "Last used")).finish(), + Expanded::new(1., self.render_header_cell(appearance, &created_header)).finish(), + ); + header_row.add_child( + Expanded::new(1., self.render_header_cell(appearance, &last_used_header)).finish(), ); header_row.add_child( - Expanded::new(1., self.render_header_cell(appearance, "Expires at")).finish(), + Expanded::new(1., self.render_header_cell(appearance, &expires_at_header)).finish(), ); header_row.add_child(Expanded::new(0.5, self.render_header_cell(appearance, "")).finish()); @@ -722,11 +743,11 @@ impl PlatformPageWidget { let last_used = key .last_used_at .map(format_approx_duration_from_now_utc) - .unwrap_or_else(|| "Never".to_owned()); + .unwrap_or_else(|| i18n::t("settings.platform.never")); let expires_at = key .expires_at .map(|dt| format!("{}", dt.format("%b %-d, %Y"))) - .unwrap_or_else(|| "Never".to_owned()); + .unwrap_or_else(|| i18n::t("settings.platform.never")); let name_column_width = view.api_key_table_column_widths.name_width(); let key_column_width = API_KEY_KEY_COLUMN_WIDTH; let mut row = Flex::row() @@ -767,9 +788,9 @@ impl PlatformPageWidget { ); if FeatureFlag::TeamApiKeys.is_enabled() || FeatureFlag::NamedAgents.is_enabled() { let scope_display = match key.scope { - ApiKeyScope::Personal => "Personal", - ApiKeyScope::Team => "Team", - ApiKeyScope::Agent => "Agent", + ApiKeyScope::Personal => i18n::t("settings.platform.scope_personal"), + ApiKeyScope::Team => i18n::t("settings.platform.scope_team"), + ApiKeyScope::Agent => i18n::t("settings.platform.scope_agent"), }; row.add_child( Expanded::new( @@ -857,7 +878,7 @@ impl PlatformPageWidget { .with_child( Container::new( Text::new( - "No API Keys", + i18n::t("settings.platform.zero_state_title"), appearance.ui_font_family(), SUBHEADER_FONT_SIZE, ) @@ -871,7 +892,7 @@ impl PlatformPageWidget { .with_child( Container::new( Text::new( - "Create a key to manage external access to Warp", + i18n::t("settings.platform.zero_state_description"), appearance.ui_font_family(), CONTENT_FONT_SIZE, ) @@ -892,7 +913,7 @@ impl PlatformPageWidget { fn render_no_search_results(&self, appearance: &Appearance) -> Box { Container::new( Text::new( - "No API keys match your search", + i18n::t("settings.platform.no_search_results"), appearance.ui_font_family(), CONTENT_FONT_SIZE, ) diff --git a/app/src/settings_view/privacy/add_regex_modal.rs b/app/src/settings_view/privacy/add_regex_modal.rs index 3f413be79a..841948fa9a 100644 --- a/app/src/settings_view/privacy/add_regex_modal.rs +++ b/app/src/settings_view/privacy/add_regex_modal.rs @@ -52,7 +52,7 @@ impl AddRegexModal { ..Default::default() }; let mut editor = EditorView::single_line(options, ctx); - editor.set_placeholder_text("e.g. \"Google API Key\"", ctx); + editor.set_placeholder_text(i18n::t("settings.privacy.regex.name_placeholder"), ctx); editor }); @@ -191,7 +191,7 @@ impl View for AddRegexModal { let is_submit_enabled = !pattern_text.trim().is_empty() && is_valid_regex; let name_label = Text::new( - "Name (optional)", + i18n::t("settings.privacy.regex.name_optional"), appearance.ui_font_family(), LABEL_FONT_SIZE, ) @@ -199,7 +199,7 @@ impl View for AddRegexModal { .finish(); let regex_label = Text::new( - "Regex pattern", + i18n::t("settings.privacy.regex.pattern"), appearance.ui_font_family(), LABEL_FONT_SIZE, ) @@ -218,7 +218,7 @@ impl View for AddRegexModal { ButtonVariant::Accent, self.submit_button_mouse_state.clone(), ) - .with_text_label("Add regex".to_string()) + .with_text_label(i18n::t("settings.privacy.regex.add")) .with_style(button_style); if !is_submit_enabled { @@ -233,7 +233,7 @@ impl View for AddRegexModal { 1., Container::new(if !is_valid_regex && !pattern_text.trim().is_empty() { Text::new( - "Invalid regex", + i18n::t("settings.privacy.regex.invalid"), appearance.ui_font_family(), LABEL_FONT_SIZE, ) @@ -259,7 +259,7 @@ impl View for AddRegexModal { ButtonVariant::Secondary, self.cancel_button_mouse_state.clone(), ) - .with_text_label("Cancel".to_string()) + .with_text_label(i18n::t("common.cancel")) .with_style(button_style) .build() .on_click(move |ctx, _, _| { diff --git a/app/src/settings_view/privacy_page.rs b/app/src/settings_view/privacy_page.rs index beb3492521..18e39588e0 100644 --- a/app/src/settings_view/privacy_page.rs +++ b/app/src/settings_view/privacy_page.rs @@ -1,7 +1,6 @@ use std::borrow::Cow; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; -use std::sync::LazyLock; use std::time::Duration; use pathfinder_geometry::vector::vec2f; @@ -61,39 +60,9 @@ use crate::{report_if_error, send_telemetry_from_ctx}; const FONT_SIZE: f32 = 12.; -const SAFE_MODE_TITLE: &str = "Secret redaction"; -static SAFE_MODE_DESCRIPTION: LazyLock<&'static str> = LazyLock::new(|| { - "When this setting is enabled, Warp will scan blocks, the contents of \ - Warp Drive objects, and Oz prompts for potential sensitive \ - information and prevent saving or sending this data to any \ - servers. You can customize this list via regexes." -}); -const USER_SECRET_REGEX_TITLE: &str = "Custom secret redaction"; -const USER_SECRET_REGEX_DESCRIPTION: &str = - "Use regex to define additional secrets or data you'd like to redact. This will take effect \ - when the next command runs. You can use the inline (?i) flag as a prefix to your regex \ - to make it case-insensitive."; -const TELEMETRY_DESCRIPTION_OLD: &str = - "App analytics help us make the product better for you. We only collect \ - app usage metadata, never console input or output."; -const TELEMETRY_TITLE: &str = "Help improve Warp"; -const TELEMETRY_DESCRIPTION: &str = - "App analytics help us make the product better for you. We may collect \ - certain console interactions to improve Warp's AI capabilities."; -const TELEMETRY_FREE_TIER_NOTE: &str = - "On the free tier, analytics must be enabled to use AI features."; const TELEMETRY_DOCS_URL: &str = "https://docs.warp.dev/support-and-community/privacy-and-security/privacy#what-telemetry-data-does-warp-collect-and-why"; -const DATA_MANAGEMENT_TITLE: &str = "Manage your data"; -const DATA_MANAGEMENT_DESCRIPTION: &str = - "At any time, you may choose to delete your Warp account permanently. \ - You will no longer be able to use Warp."; -const DATA_MANAGEMENT_LINK_TEXT: &str = "Visit the data management page"; - -const PRIVACY_POLICY_TITLE: &str = "Privacy policy"; -const PRIVACY_POLICY_LINK_TEXT: &str = "Read Warp's privacy policy"; - pub fn data_management_url(custom_token: Option<&str>) -> String { match custom_token { Some(token) => format!( @@ -156,34 +125,38 @@ impl PrivacyPageView { }); let add_regex_modal_view = ctx.add_typed_action_view(|ctx| { - Modal::new(Some("Add regex pattern".to_string()), add_regex_body, ctx) - .with_modal_style(UiComponentStyles { - width: Some(600.), - height: Some(400.), - ..Default::default() - }) - .with_header_style(UiComponentStyles { - padding: Some(Coords { - top: 24., - bottom: 0., - left: 24., - right: 24., - }), - font_size: Some(16.), - font_weight: Some(Weight::Bold), - ..Default::default() - }) - .with_body_style(UiComponentStyles { - padding: Some(Coords { - top: 0., - bottom: 24., - left: 24., - right: 24., - }), - ..Default::default() - }) - .with_background_opacity(100) - .with_dismiss_on_click() + Modal::new( + Some(i18n::t("settings.privacy.add_regex_modal.title")), + add_regex_body, + ctx, + ) + .with_modal_style(UiComponentStyles { + width: Some(600.), + height: Some(400.), + ..Default::default() + }) + .with_header_style(UiComponentStyles { + padding: Some(Coords { + top: 24., + bottom: 0., + left: 24., + right: 24., + }), + font_size: Some(16.), + font_weight: Some(Weight::Bold), + ..Default::default() + }) + .with_body_style(UiComponentStyles { + padding: Some(Coords { + top: 0., + bottom: 24., + left: 24., + right: 24., + }), + ..Default::default() + }) + .with_background_opacity(100) + .with_dismiss_on_click() }); ctx.subscribe_to_view(&add_regex_modal_view, |me, _, event, ctx| { me.handle_modal_event(event, ctx); @@ -236,7 +209,12 @@ impl PrivacyPageView { } widgets.push(Box::new(DataManagementWidget::default())); widgets.push(Box::new(PrivacyPolicyWidget::default())); - PageType::new_uncategorized(widgets, Some("Privacy")) + PageType::new_uncategorized( + widgets, + Some(Box::leak( + i18n::t("settings.privacy.page_title").into_boxed_str(), + )), + ) } fn update_button_states( @@ -760,7 +738,7 @@ impl SecretRedactionWidget { .count(); let personal_tab = self.render_tab( - "Personal".to_string(), + i18n::t("settings.privacy.tab.personal"), personal_count, SecretRedactionTab::Personal, active_tab == SecretRedactionTab::Personal, @@ -771,7 +749,7 @@ impl SecretRedactionWidget { let is_enterprise_tab_active = active_tab == SecretRedactionTab::Enterprise; let enterprise_tab = self.render_tab( - "Enterprise".to_string(), + i18n::t("settings.privacy.tab.enterprise"), enterprise_count, SecretRedactionTab::Enterprise, is_enterprise_tab_active, @@ -787,7 +765,7 @@ impl SecretRedactionWidget { if is_enterprise_tab_active { row.add_child(Shrinkable::new(1., Empty::new().finish()).finish()); row.add_child(self.render_info( - "Enterprise secret redaction cannot be modified.".to_string(), + i18n::t("settings.privacy.enterprise_redaction_readonly"), appearance, )); } @@ -906,7 +884,7 @@ impl SecretRedactionWidget { if enterprise_regex_list.is_empty() { return ui_builder - .paragraph("No enterprise regexes have been configured by your organization.") + .paragraph(i18n::t("settings.privacy.no_enterprise_regexes")) .with_style(UiComponentStyles { font_color: Some(description_text_color), ..Default::default() @@ -1005,9 +983,10 @@ impl SecretRedactionWidget { .with_main_axis_size(MainAxisSize::Max) .with_main_axis_alignment(MainAxisAlignment::SpaceBetween) .with_cross_axis_alignment(CrossAxisAlignment::Center) - .with_child( - self.render_section_title("Recommended".to_string(), appearance), - ) + .with_child(self.render_section_title( + i18n::t("settings.privacy.recommended_header"), + appearance, + )) .with_child( Container::new( ui_builder @@ -1016,7 +995,8 @@ impl SecretRedactionWidget { self.add_all_button_mouse_state.clone(), ) .with_text_and_icon_label(Self::add_button( - "Add all", appearance, + i18n::t("settings.privacy.add_all_button"), + appearance, )) .with_style(Self::add_button_style()) .build() @@ -1174,7 +1154,11 @@ impl SettingsWidget for SecretRedactionWidget { .with_child( Shrinkable::new( 1.0, - render_sub_header(appearance, SAFE_MODE_TITLE, Some(local_only_icon_state)), + render_sub_header( + appearance, + i18n::t("settings.privacy.safe_mode.title"), + Some(local_only_icon_state), + ), ) .finish(), ) @@ -1182,7 +1166,7 @@ impl SettingsWidget for SecretRedactionWidget { Container::new({ if is_enterprise_enabled { self.render_info( - "Enabled by your organization.".to_string(), + i18n::t("settings.privacy.enabled_by_organization"), appearance, ) } else { @@ -1209,7 +1193,7 @@ impl SettingsWidget for SecretRedactionWidget { .with_child(secret_redaction_title_row) .with_child( ui_builder - .paragraph((*SAFE_MODE_DESCRIPTION).to_owned()) + .paragraph(i18n::t("settings.privacy.safe_mode.description")) .with_style(UiComponentStyles { font_color: Some(description_text_color), font_size: Some(FONT_SIZE + 1.), // One size up from current 12px to 13px @@ -1235,7 +1219,7 @@ impl SettingsWidget for SecretRedactionWidget { // Create the label with local-only icon if needed let label_with_icon = super::settings_page::render_dropdown_item_label( - "Secret visual redaction mode".to_string(), + i18n::t("settings.privacy.secret_display_mode.label"), None, local_only_icon_state, None, @@ -1248,22 +1232,16 @@ impl SettingsWidget for SecretRedactionWidget { .with_child( Container::new( ui_builder - .paragraph( - "Choose how secrets are visually presented in the block list while keeping them searchable. This setting only affects what you see in the block list.", - ) + .paragraph(i18n::t("settings.privacy.secret_display_mode.description")) .with_style(UiComponentStyles { font_color: Some(description_text_color), - margin: Some( - Coords::default() - .top(4.) - .bottom(0.), - ), + margin: Some(Coords::default().top(4.).bottom(0.)), ..Default::default() }) .build() - .finish() + .finish(), ) - .finish() + .finish(), ) .finish(); @@ -1298,11 +1276,11 @@ impl SettingsWidget for SecretRedactionWidget { 1., Flex::column() .with_child(self.render_section_title( - USER_SECRET_REGEX_TITLE.to_string(), + i18n::t("settings.privacy.custom_secret_redaction.title"), appearance, )) .with_child(self.render_description( - USER_SECRET_REGEX_DESCRIPTION.to_owned(), + i18n::t("settings.privacy.custom_secret_redaction.description"), appearance, if privacy_settings.user_secret_regex_list.iter().count() > 0 { 10. @@ -1320,7 +1298,10 @@ impl SettingsWidget for SecretRedactionWidget { ButtonVariant::Secondary, self.add_regex_button_mouse_state.clone(), ) - .with_text_and_icon_label(Self::add_button("Add regex", appearance)) + .with_text_and_icon_label(Self::add_button( + i18n::t("settings.privacy.add_regex_button"), + appearance, + )) .with_style(Self::add_button_style()) .build() .on_click(move |ctx, _, _| { @@ -1397,10 +1378,7 @@ impl AppAnalyticsWidget { let mut stack = Stack::new().with_child(badge); if is_hovered { - let tooltip = ui_builder.tool_tip( - "Your administrator has enabled zero data retention for your team. User generated content will never be collected." - .to_string(), - ); + let tooltip = ui_builder.tool_tip(i18n::t("settings.privacy.zdr.tooltip")); stack.add_positioned_child( tooltip.build().finish(), OffsetPositioning::offset_from_parent( @@ -1455,9 +1433,9 @@ impl SettingsWidget for AppAnalyticsWidget { .is_some_and(|w| w.billing_metadata.customer_type == CustomerType::Enterprise); // Keep the old description for enterprise users because we do not collect block input/output for them. let description = if is_enterprise { - TELEMETRY_DESCRIPTION_OLD + i18n::t("settings.privacy.telemetry.description_enterprise") } else { - TELEMETRY_DESCRIPTION + i18n::t("settings.privacy.telemetry.description") }; let org_setting = UserWorkspaces::handle(app) @@ -1476,7 +1454,7 @@ impl SettingsWidget for AppAnalyticsWidget { Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child(render_body_item_label::( - TELEMETRY_TITLE.into(), + i18n::t("settings.privacy.telemetry.title"), None, None, LocalOnlyIconState::Hidden, @@ -1487,7 +1465,7 @@ impl SettingsWidget for AppAnalyticsWidget { .finish() } else { render_body_item_label::( - TELEMETRY_TITLE.into(), + i18n::t("settings.privacy.telemetry.title"), None, None, LocalOnlyIconState::Hidden, @@ -1509,7 +1487,7 @@ impl SettingsWidget for AppAnalyticsWidget { } else { switch .with_tooltip(TooltipConfig { - text: "This setting is managed by your organization.".to_string(), + text: i18n::t("settings.privacy.managed_by_organization"), styles: ui_builder.default_tool_tip_styles(), }) .disable() @@ -1551,7 +1529,7 @@ impl SettingsWidget for AppAnalyticsWidget { if !is_on_paid_plan { column.add_child( ui_builder - .paragraph(TELEMETRY_FREE_TIER_NOTE) + .paragraph(i18n::t("settings.privacy.telemetry.free_tier_note")) .with_style(UiComponentStyles { font_color: Some(description_text_color), margin: Some( @@ -1568,7 +1546,7 @@ impl SettingsWidget for AppAnalyticsWidget { Align::new( ui_builder .link( - "Read more about Warp's use of data".into(), + i18n::t("settings.privacy.telemetry.read_more_link"), Some(TELEMETRY_DOCS_URL.into()), None, self.docs_link_mouse_state.clone(), @@ -1618,7 +1596,7 @@ impl SettingsWidget for CrashReportsWidget { let privacy_settings = PrivacySettings::as_ref(app); Flex::column() .with_child(render_body_item::( - "Send crash reports".into(), + i18n::t("settings.privacy.crash_reports.title"), None, // Crash report state is always synced to cloud, so no need to show local only icon. LocalOnlyIconState::Hidden, @@ -1636,10 +1614,7 @@ impl SettingsWidget for CrashReportsWidget { )) .with_child( ui_builder - .paragraph( - "Crash reports assist with debugging and stability improvements." - .to_owned(), - ) + .paragraph(i18n::t("settings.privacy.crash_reports.description")) .with_style(UiComponentStyles { font_color: Some( appearance @@ -1722,7 +1697,7 @@ impl SettingsWidget for CloudConversationStorageWidget { } else { switch .with_tooltip(TooltipConfig { - text: "This setting is managed by your organization.".to_string(), + text: i18n::t("settings.privacy.managed_by_organization"), styles: ui_builder.default_tool_tip_styles(), }) .disable() @@ -1732,7 +1707,7 @@ impl SettingsWidget for CloudConversationStorageWidget { Flex::column() .with_child(render_body_item::( - "Store AI conversations in the cloud".into(), + i18n::t("settings.privacy.cloud_conversation_storage.title"), None, LocalOnlyIconState::Hidden, toggle_state, @@ -1742,18 +1717,11 @@ impl SettingsWidget for CloudConversationStorageWidget { )) .with_child( ui_builder - .paragraph( - if is_checked { - "Agent conversations can be shared with others and are retained \ - when you log in on different devices. This data is only stored \ - for product functionality, and Warp will not use it for analytics." - } else { - "Agent conversations are only stored locally on your machine, are \ - lost upon logout, and cannot be shared. Note: conversation data \ - for ambient agents are still stored in the cloud." - } - .to_owned(), - ) + .paragraph(if is_checked { + i18n::t("settings.privacy.cloud_conversation_storage.description_enabled") + } else { + i18n::t("settings.privacy.cloud_conversation_storage.description_disabled") + }) .with_style(UiComponentStyles { font_color: Some( appearance @@ -1796,7 +1764,7 @@ impl SettingsWidget for NetworkLogWidget { let ui_builder = appearance.ui_builder(); Flex::column() .with_child(render_body_item::( - "Network log console".into(), + i18n::t("settings.privacy.network_log.title"), None, // Not rendering a setting, so no need to show local only icon state. LocalOnlyIconState::Hidden, @@ -1807,12 +1775,7 @@ impl SettingsWidget for NetworkLogWidget { )) .with_child( ui_builder - .paragraph( - "We've built a native console that allows you to view all communications \ - from Warp to external servers to ensure you feel comfortable that your \ - work is always kept safe." - .to_owned(), - ) + .paragraph(i18n::t("settings.privacy.network_log.description")) .with_style(UiComponentStyles { font_color: Some( appearance @@ -1834,7 +1797,7 @@ impl SettingsWidget for NetworkLogWidget { Align::new( ui_builder .link( - "View network logging".to_owned(), + i18n::t("settings.privacy.network_log.link"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(PrivacyPageAction::LaunchNetworkLogging); @@ -1874,7 +1837,7 @@ impl SettingsWidget for DataManagementWidget { let ui_builder = appearance.ui_builder(); Flex::column() .with_child(render_body_item::( - DATA_MANAGEMENT_TITLE.into(), + i18n::t("settings.privacy.data_management.title"), None, // Not rendering a setting, so no need to show local only icon state. LocalOnlyIconState::Hidden, @@ -1885,7 +1848,7 @@ impl SettingsWidget for DataManagementWidget { )) .with_child( ui_builder - .paragraph(DATA_MANAGEMENT_DESCRIPTION) + .paragraph(i18n::t("settings.privacy.data_management.description")) .with_style(UiComponentStyles { font_color: Some( appearance @@ -1908,7 +1871,7 @@ impl SettingsWidget for DataManagementWidget { appearance .ui_builder() .link( - DATA_MANAGEMENT_LINK_TEXT.into(), + i18n::t("settings.privacy.data_management.link"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action( @@ -1949,7 +1912,7 @@ impl SettingsWidget for PrivacyPolicyWidget { ) -> Box { Flex::column() .with_child(render_body_item::( - PRIVACY_POLICY_TITLE.into(), + i18n::t("settings.privacy.privacy_policy.title"), None, // Not rendering a setting, so no need to show local only icon state. LocalOnlyIconState::Hidden, @@ -1963,7 +1926,7 @@ impl SettingsWidget for PrivacyPolicyWidget { appearance .ui_builder() .link( - PRIVACY_POLICY_LINK_TEXT.into(), + i18n::t("settings.privacy.privacy_policy.link"), Some(PRIVACY_POLICY_URL.into()), None, self.link_mouse_state.clone(), @@ -1987,7 +1950,7 @@ pub fn init_actions_from_parent_view( ) { let mut toggle_binding_pairs = vec![ ToggleSettingActionPair::new( - "app analytics", + i18n::t("settings.privacy.action.app_analytics"), builder(SettingsAction::PrivacyPageToggle( PrivacyPageAction::ToggleTelemetry, )), @@ -1995,7 +1958,7 @@ pub fn init_actions_from_parent_view( flags::TELEMETRY_FLAG, ), ToggleSettingActionPair::new( - "crash reporting", + i18n::t("settings.privacy.action.crash_reporting"), builder(SettingsAction::PrivacyPageToggle( PrivacyPageAction::ToggleCrashReporting, )), @@ -2005,7 +1968,7 @@ pub fn init_actions_from_parent_view( ]; toggle_binding_pairs.push(ToggleSettingActionPair::new( - "secret redaction", + i18n::t("settings.privacy.safe_mode.title"), builder(SettingsAction::PrivacyPageToggle( PrivacyPageAction::ToggleSafeMode, )), @@ -2015,7 +1978,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "cloud AI conversation storage", + i18n::t("settings.privacy.action.cloud_ai_conversation_storage"), builder(SettingsAction::PrivacyPageToggle( PrivacyPageAction::ToggleCloudConversationStorage, )), diff --git a/app/src/settings_view/referrals_page.rs b/app/src/settings_view/referrals_page.rs index 51e9557935..5396365da2 100644 --- a/app/src/settings_view/referrals_page.rs +++ b/app/src/settings_view/referrals_page.rs @@ -36,30 +36,18 @@ use crate::{safe_info, send_telemetry_from_ctx}; const HEADER_FONT_SIZE: f32 = 18.; const HEADER_MARGIN_BOTTOM: f32 = 32.; -const HEADER_TEXT: &str = "Invite a friend to Warp"; -const ANONYMOUS_USER_HEADER_TEXT: &str = "Sign up to participate in Warp's referral program"; const INVITE_FIELD_LABEL_BOTTOM_MARGIN: f32 = 8.; const LINK_BOTTOM_MARGIN: f32 = 12.; const LINK_TEXT_PADDING: f32 = 10.; const LINK_CORNER_RADIUS: Radius = Radius::Pixels(4.); -const LINK_ERROR_TEXT: &str = "Failed to load referral code."; const BUTTON_WIDTH: f32 = 98.; const BUTTON_HEIGHT: f32 = 36.; const BUTTON_LEFT_MARGIN: f32 = 8.; const BUTTON_FONT_SIZE: f32 = 12.; -const LINK_BUTTON_TEXT: &str = "Copy link"; -const EMAIL_BUTTON_TEXT: &str = "Send"; -const EMAIL_BUTTON_SENDING_TEXT: &str = "Sending..."; -const LOADING_TEXT: &str = "Loading..."; -const LINK_COPIED_TOAST: &str = "Link copied."; -const EMAIL_SUCCESS_TOAST: &str = "Successfully sent emails."; -const EMAIL_FAILURE_TOAST: &str = "Failed to send emails. Please try again."; - -const REWARD_INTRO: &str = "Get exclusive Warp goodies when you refer someone*"; const REWARD_INTRO_FONT_SIZE: f32 = 14.; const REWARD_SECTION_VERTICAL_SPACING: f32 = 24.; @@ -81,8 +69,6 @@ const METER_TOP_MARGIN: f32 = 16.; const METER_RIGHT_MARGIN: f32 = 12.; const CLAIMED_REFERRALS_LABEL_HORIZONTAL_SPACING: f32 = 4.; -const CLAIMED_REFERRALS_COUNT_LABEL_SINGULAR: &str = "Current referral"; -const CLAIMED_REFERRALS_COUNT_LABEL_PLURAL: &str = "Current referrals"; const CLAIMED_REFERRALS_LABEL_WIDTH: f32 = 52.; const CLAIMED_REFERRALS_LABEL_FONT_SIZE: f32 = 14.; const CLAIMED_REFERRALS_COUNT_FONT_SIZE: f32 = 48.; @@ -90,11 +76,8 @@ const CLAIMED_REFERRAL_COUNT_LEFT_MARGIN: f32 = 40.; const CLAIMED_REFERRAL_CLIP: usize = 999; -const TERMS_LINK_TEXT: &str = "Certain restrictions apply."; const TERMS_URL: &str = "https://docs.warp.dev/support-and-community/community/refer-a-friend#referral-program-terms-and-conditions"; -const TERMS_CONTACT_TEXT: &str = - " If you have any questions about the referral program, please contact referrals@warp.dev."; enum ApiState { Loading, @@ -149,56 +132,56 @@ lazy_static! { icon_path: "bundled/svg/referral-theme.svg", icon_width: 64., icon_height: 64., - label: "Exclusive theme".to_owned(), + label: i18n::t("settings.referrals.reward_theme"), }, Reward { required_referral_count: 5, icon_path: "bundled/svg/referral-keycaps.svg", icon_width: 56., icon_height: 56., - label: "Keycaps + stickers".to_owned(), + label: i18n::t("settings.referrals.reward_keycaps"), }, Reward { required_referral_count: 10, icon_path: "bundled/svg/referral-tshirt.svg", icon_width: 64., icon_height: 64., - label: "T-shirt".to_owned(), + label: i18n::t("settings.referrals.reward_tshirt"), }, Reward { required_referral_count: 20, icon_path: "bundled/svg/referral-notebook.svg", icon_width: 64., icon_height: 64., - label: "Notebook".to_owned(), + label: i18n::t("settings.referrals.reward_notebook"), }, Reward { required_referral_count: 35, icon_path: "bundled/svg/referral-hat.svg", icon_width: 64., icon_height: 64., - label: "Baseball cap".to_owned(), + label: i18n::t("settings.referrals.reward_cap"), }, Reward { required_referral_count: 50, icon_path: "bundled/svg/referral-hoodie.svg", icon_width: 64., icon_height: 64., - label: "Hoodie".to_owned(), + label: i18n::t("settings.referrals.reward_hoodie"), }, Reward { required_referral_count: 75, icon_path: "bundled/svg/referral-hydroflask.svg", icon_width: 48., icon_height: 48., - label: "Premium Hydro Flask".to_owned(), + label: i18n::t("settings.referrals.reward_hydroflask"), }, Reward { required_referral_count: 100, icon_path: "bundled/svg/referral-backpack.svg", icon_width: 50., icon_height: 50., - label: "Backpack".to_owned(), + label: i18n::t("settings.referrals.reward_backpack"), }, ]; } @@ -217,7 +200,12 @@ impl ReferralsPageView { me.handle_editor_event(event, ctx); }); - let page = PageType::new_monolith(ReferralsWidget::default(), Some(HEADER_TEXT), true); + let header_text = i18n::t("settings.referrals.header"); + let page = PageType::new_monolith( + ReferralsWidget::default(), + Some(Box::leak(header_text.into_boxed_str())), + true, + ); Self { page, referrals_client, @@ -281,7 +269,7 @@ impl ReferralsPageView { ctx.clipboard() .write(ClipboardContent::plain_text(referral_info.url.to_string())); ctx.emit(ReferralsPageEvent::ShowToast { - message: LINK_COPIED_TOAST.to_owned(), + message: i18n::t("settings.referrals.toast_link_copied"), flavor: ToastFlavor::Default, }); } @@ -334,14 +322,14 @@ impl ReferralsPageView { full: ("Successfully sent invites to: {:?}", successful) ); ctx.emit(ReferralsPageEvent::ShowToast { - message: EMAIL_SUCCESS_TOAST.to_owned(), + message: i18n::t("settings.referrals.toast_email_success"), flavor: ToastFlavor::Success, }); } Err(err) => { log::error!("Error sending referral emails: {err}"); ctx.emit(ReferralsPageEvent::ShowToast { - message: EMAIL_FAILURE_TOAST.to_owned(), + message: i18n::t("settings.referrals.toast_email_failure"), flavor: ToastFlavor::Error, }); } @@ -456,9 +444,12 @@ impl EmailValidationError { /// The user-readable error descriptions. fn ui_message(&self) -> String { match self { - EmailValidationError::Empty => "Please enter an email.".to_owned(), + EmailValidationError::Empty => i18n::t("settings.referrals.error_empty_email"), EmailValidationError::Invalid(invalid_email) => { - format!("Please ensure the following email is valid: {invalid_email}") + format!( + "{}{invalid_email}", + i18n::t("settings.referrals.error_invalid_email_prefix") + ) } } } @@ -520,10 +511,11 @@ impl ReferralsWidget { ) -> Box { let (link_text, button_enabled) = match &view.api_state { ApiState::Ready { referral_info, .. } => (referral_info.url.clone(), true), - ApiState::Loading => (LOADING_TEXT.into(), false), - ApiState::Failed => (LINK_ERROR_TEXT.into(), false), + ApiState::Loading => (i18n::t("settings.referrals.loading"), false), + ApiState::Failed => (i18n::t("settings.referrals.link_error"), false), }; let theme = appearance.theme(); + let copy_link_label = i18n::t("settings.referrals.copy_link"); Container::new( Flex::row() @@ -557,7 +549,7 @@ impl ReferralsWidget { ) .with_child(self.render_button( button_enabled, - LINK_BUTTON_TEXT, + ©_link_label, self.copy_link_mouse_state.clone(), |ctx, _, _| ctx.dispatch_typed_action(ReferralsPageAction::CopyLink), appearance, @@ -578,12 +570,12 @@ impl ReferralsWidget { ApiState::Ready { email_state: SendEmailState::Idle, .. - } => (EMAIL_BUTTON_TEXT, true), + } => (i18n::t("settings.referrals.send"), true), ApiState::Ready { email_state: SendEmailState::Sending, .. - } => (EMAIL_BUTTON_SENDING_TEXT, false), - _ => (EMAIL_BUTTON_TEXT, false), + } => (i18n::t("settings.referrals.sending"), false), + _ => (i18n::t("settings.referrals.send"), false), }; Flex::row() @@ -605,7 +597,7 @@ impl ReferralsWidget { ) .with_child(self.render_button( button_enabled, - button_text, + &button_text, self.send_email_mouse_state.clone(), |ctx, _, _| ctx.dispatch_typed_action(ReferralsPageAction::SendEmailInvite), appearance, @@ -620,12 +612,14 @@ impl ReferralsWidget { ) -> Box { Flex::column() .with_child( - Container::new(self.render_label("Link", appearance)) - .with_padding_top(PAGE_PADDING) - .finish(), + Container::new( + self.render_label(i18n::t("settings.referrals.link_label"), appearance), + ) + .with_padding_top(PAGE_PADDING) + .finish(), ) .with_child(self.render_link_row(view, appearance)) - .with_child(self.render_label("Email", appearance)) + .with_child(self.render_label(i18n::t("settings.referrals.email_label"), appearance)) .with_child(self.render_email_row(view, appearance)) .finish() } @@ -651,7 +645,7 @@ impl ReferralsWidget { self.sign_up_button_mouse_state.clone(), ) .with_style(button_styles) - .with_text_label("Sign up".to_owned()) + .with_text_label(i18n::t("settings.referrals.sign_up")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(ReferralsPageAction::SignupAnonymousUser); @@ -663,7 +657,7 @@ impl ReferralsWidget { Container::new( appearance .ui_builder() - .span(ANONYMOUS_USER_HEADER_TEXT) + .span(i18n::t("settings.referrals.anonymous_header")) .with_style(UiComponentStyles { font_size: Some(HEADER_FONT_SIZE), ..Default::default() @@ -735,7 +729,7 @@ impl ReferralsWidget { Container::new( appearance .ui_builder() - .span(REWARD_INTRO) + .span(i18n::t("settings.referrals.reward_intro")) .with_style(UiComponentStyles { font_size: Some(REWARD_INTRO_FONT_SIZE), ..Default::default() @@ -775,8 +769,13 @@ impl ReferralsWidget { FormattedTextElement::new( FormattedText::new([FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("*"), - FormattedTextFragment::hyperlink(TERMS_LINK_TEXT, TERMS_URL), - FormattedTextFragment::plain_text(TERMS_CONTACT_TEXT), + FormattedTextFragment::hyperlink( + i18n::t("settings.referrals.terms_link"), + TERMS_URL, + ), + FormattedTextFragment::plain_text(i18n::t( + "settings.referrals.terms_contact", + )), ])]), 12., appearance.ui_font_family(), @@ -1058,8 +1057,8 @@ impl ReferralsWidget { }; let current_referrals_label = match claimed_count { - 1 => CLAIMED_REFERRALS_COUNT_LABEL_SINGULAR, - _ => CLAIMED_REFERRALS_COUNT_LABEL_PLURAL, + 1 => i18n::t("settings.referrals.current_referral_singular"), + _ => i18n::t("settings.referrals.current_referral_plural"), }; Some( diff --git a/app/src/settings_view/remove_custom_endpoint_confirmation_dialog.rs b/app/src/settings_view/remove_custom_endpoint_confirmation_dialog.rs index 657e3dca52..9ccb61aafa 100644 --- a/app/src/settings_view/remove_custom_endpoint_confirmation_dialog.rs +++ b/app/src/settings_view/remove_custom_endpoint_confirmation_dialog.rs @@ -37,15 +37,19 @@ pub struct RemoveCustomEndpointConfirmationDialog { impl RemoveCustomEndpointConfirmationDialog { pub fn new(ctx: &mut ViewContext) -> Self { let cancel_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Cancel", NakedTheme).on_click(|ctx| { + ActionButton::new(i18n::t("common.cancel"), NakedTheme).on_click(|ctx| { ctx.dispatch_typed_action(RemoveCustomEndpointConfirmationDialogAction::Cancel); }) }); let confirm_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Remove endpoint", DangerPrimaryTheme).on_click(|ctx| { - ctx.dispatch_typed_action(RemoveCustomEndpointConfirmationDialogAction::Confirm); - }) + ActionButton::new(i18n::t("settings.ai.remove_endpoint"), DangerPrimaryTheme).on_click( + |ctx| { + ctx.dispatch_typed_action( + RemoveCustomEndpointConfirmationDialogAction::Confirm, + ); + }, + ) }); Self { @@ -99,7 +103,7 @@ impl View for RemoveCustomEndpointConfirmationDialog { let appearance = Appearance::as_ref(app); let theme = appearance.theme(); - let description = "Are you sure you want to remove this endpoint? You won't be able to use its models in your agent sessions moving forward.".to_string(); + let description = i18n::t("settings.ai.remove_endpoint_description"); let endpoint_title = Text::new_inline( self.endpoint_name.clone(), @@ -130,7 +134,7 @@ impl View for RemoveCustomEndpointConfirmationDialog { .finish(); let dialog = Dialog::new( - "Remove endpoint?".to_string(), + i18n::t("settings.ai.remove_endpoint_question"), Some(description), dialog_styles(appearance), ) diff --git a/app/src/settings_view/settings_file_footer.rs b/app/src/settings_view/settings_file_footer.rs index a2fbc2df57..7abca308a4 100644 --- a/app/src/settings_view/settings_file_footer.rs +++ b/app/src/settings_view/settings_file_footer.rs @@ -113,13 +113,17 @@ pub fn render_open_settings_file_button( .with_height(FOOTER_ICON_SIZE) .finish(); - let label = Text::new_inline("Open settings file", ui_font_family, FOOTER_FONT_SIZE) - .with_color(text_color) - .with_style(Properties { - weight: Weight::Semibold, - ..Default::default() - }) - .finish(); + let label = Text::new_inline( + i18n::t("settings.open_settings_file"), + ui_font_family, + FOOTER_FONT_SIZE, + ) + .with_color(text_color) + .with_style(Properties { + weight: Weight::Semibold, + ..Default::default() + }) + .finish(); // Use `MainAxisSize::Max` so the row (and its surrounding bordered // container) expands to fill the full sidebar width. The icon + text @@ -228,7 +232,7 @@ pub fn render_settings_error_alert( ui_font_family, text_color, mouse_states.alert_open_file_button.clone(), - "Open file", + i18n::t("common.open_file"), /*icon=*/ None, /*bordered=*/ true, WorkspaceAction::OpenSettingsFile, @@ -250,7 +254,7 @@ pub fn render_settings_error_alert( ui_font_family, text_color, mouse_states.alert_fix_with_oz_button.clone(), - "Fix with Oz", + i18n::t("workspace.banner.fix_with_oz"), Some(Icon::Oz), /*bordered=*/ false, WorkspaceAction::FixSettingsWithOz { error_description }, @@ -320,7 +324,7 @@ fn render_alert_action_button( ui_font_family: FamilyId, text_color: ColorU, mouse_state: MouseStateHandle, - text: &'static str, + text: String, icon: Option, bordered: bool, action: WorkspaceAction, @@ -343,7 +347,7 @@ fn render_alert_action_button( ); } row.add_child( - Text::new_inline(text.to_owned(), ui_font_family, FOOTER_FONT_SIZE) + Text::new_inline(text.clone(), ui_font_family, FOOTER_FONT_SIZE) .with_color(text_color) .with_style(Properties { weight: Weight::Semibold, diff --git a/app/src/settings_view/settings_page.rs b/app/src/settings_view/settings_page.rs index a235c872f8..dcb3150b24 100644 --- a/app/src/settings_view/settings_page.rs +++ b/app/src/settings_view/settings_page.rs @@ -178,7 +178,7 @@ impl SettingsPage { }, self.button_state_handle.clone(), ) - .with_text_label(self.section.to_string() + &match_data.to_string()) + .with_text_label(self.section.nav_label() + &match_data.to_string()) .with_style( UiComponentStyles::default() .set_border_width(0.) @@ -547,7 +547,7 @@ pub fn render_info_icon( ) -> Box { let tooltip_text = additional_info .tooltip_override_text - .unwrap_or("Click to learn more in docs".to_owned()); + .unwrap_or_else(|| i18n::t("settings.tooltip.learn_more_docs")); let icon = Container::new( ConstrainedBox::new( Icon::Info @@ -605,7 +605,7 @@ pub fn render_local_only_icon( .ui_builder() .local_only_icon_with_tooltip( 13., - custom_tooltip.unwrap_or("This setting is not synced to your other devices".to_owned()), + custom_tooltip.unwrap_or_else(|| i18n::t("settings.tooltip.not_synced")), mouse_state.clone(), ) .finish(); @@ -1020,9 +1020,6 @@ pub(crate) fn render_settings_info_banner( .finish() } -const WORKSPACE_OVERRIDE_TOOLTIP_TEXT: &str = - "This option is enforced by your organization's settings and cannot be customized."; - pub struct InputListItem { pub item: String, pub mouse_state_handle: MouseStateHandle, @@ -1130,7 +1127,7 @@ fn render_workspace_override_row_tooltip( if state.is_hovered() { let tooltip = appearance .ui_builder() - .tool_tip(WORKSPACE_OVERRIDE_TOOLTIP_TEXT.to_string()) + .tool_tip(i18n::t("settings.workspace_override_tooltip")) .build() .finish(); stack.add_positioned_child( @@ -1918,5 +1915,5 @@ pub(super) fn build_reset_button( font_size: Some(appearance.ui_font_size() * 0.8), ..Default::default() }) - .with_text_label("Reset to default".to_owned()) + .with_text_label(i18n::t("settings.reset_to_default")) } diff --git a/app/src/settings_view/show_blocks_view.rs b/app/src/settings_view/show_blocks_view.rs index ecbd76ab81..96a6246a64 100644 --- a/app/src/settings_view/show_blocks_view.rs +++ b/app/src/settings_view/show_blocks_view.rs @@ -35,10 +35,6 @@ use crate::view_components::ToastFlavor; const SCROLLBAR_WIDTH: ScrollbarWidth = ScrollbarWidth::Auto; -const UNSHARE_BLOCK_CONFIRMATION_DIALOG_TEXT: &str = - "Are you sure you want to unshare this block?\n\ -\nIt will no longer be accessible by link and will be permanently deleted from Warp servers."; - #[derive(Clone, Debug)] struct UserOwnedBlock { id: String, @@ -148,7 +144,7 @@ impl UserOwnedBlock { ButtonVariant::Basic, self.copy_button_mouse_state_handle.clone(), ) - .with_text_label("Copy link".into()); + .with_text_label(i18n::t("common.copy_link")); let button = if self.unshare_request_status == UnshareBlockRequestState::InFlight { button.disabled().build() @@ -165,7 +161,7 @@ impl UserOwnedBlock { if self.unshare_request_status == UnshareBlockRequestState::InFlight { appearance .ui_builder() - .label("Deleting...") + .label(i18n::t("settings.show_blocks.deleting")) .with_style( UiComponentStyles::default() .set_font_family_id(appearance.monospace_font_family()) @@ -238,15 +234,15 @@ impl UserOwnedBlock { .finish(), ) .finish(); + let executed_at = self + .time_started + .with_timezone(&Local) + .format("%a, %b %-d %Y at %-I:%M %p") + .to_string(); let timestamp_row = Container::new( appearance .ui_builder() - .label(format!( - "Executed on: {}", - self.time_started - .with_timezone(&Local) - .format("%a, %b %-d %Y at %-I:%M %p") - )) + .label(i18n::t("settings.show_blocks.executed_on").replace("{date}", &executed_at)) .with_style( UiComponentStyles::default() .set_font_color( @@ -303,14 +299,15 @@ impl GetBlocksForUserRequestState { let ui_builder = appearance.ui_builder(); match self { GetBlocksForUserRequestState::NotStarted => pad(ui_builder - .label("You don't have any shared blocks yet.") + .label(i18n::t("settings.show_blocks.empty")) + .build() + .finish()), + GetBlocksForUserRequestState::InFlight => pad(ui_builder + .label(i18n::t("settings.show_blocks.loading")) .build() .finish()), - GetBlocksForUserRequestState::InFlight => { - pad(ui_builder.label("Getting blocks...").build().finish()) - } GetBlocksForUserRequestState::Failed => pad(ui_builder - .label("Failed to load blocks. Please try again.") + .label(i18n::t("settings.show_blocks.load_failed")) .build() .finish()), GetBlocksForUserRequestState::Done(user_blocks) => { @@ -363,7 +360,7 @@ impl GetBlocksForUserRequestState { .finish() } else { pad(ui_builder - .label("You don't have any shared blocks yet.") + .label(i18n::t("settings.show_blocks.empty")) .build() .finish()) } @@ -424,7 +421,8 @@ impl ShowBlocksView { menu.set_items( vec![MenuItem::Item( - MenuItemFields::new("Unshare").with_on_select_action(ShowBlocksAction::Unshare), + MenuItemFields::new(i18n::t("settings.shared_blocks.unshare")) + .with_on_select_action(ShowBlocksAction::Unshare), )], ctx, ); @@ -491,7 +489,7 @@ impl ShowBlocksView { ctx.clipboard() .write(ClipboardContent::plain_text(block_url.to_string())); ctx.emit(ShowBlocksEvent::ShowToast { - message: "Link copied.".to_string(), + message: i18n::t("settings.show_blocks.link_copied"), flavor: ToastFlavor::Default, }) } @@ -550,14 +548,14 @@ impl ShowBlocksView { match request_result { Ok(_) => { ctx.emit(ShowBlocksEvent::ShowToast { - message: "Block was successfully unshared.".to_string(), + message: i18n::t("settings.show_blocks.unshare_success"), flavor: ToastFlavor::Success, }); user_block.unshare_request_status = UnshareBlockRequestState::Done; } Err(_) => { ctx.emit(ShowBlocksEvent::ShowToast { - message: "Failed to unshare block. Please try again.".to_string(), + message: i18n::t("settings.show_blocks.unshare_failed"), flavor: ToastFlavor::Error, }); user_block.unshare_request_status = UnshareBlockRequestState::Failed; @@ -657,7 +655,7 @@ impl ShowBlocksWidget { .with_child( Align::new( ui_builder - .label("Unshare block") + .label(i18n::t("settings.show_blocks.unshare_block")) .with_style(UiComponentStyles { font_size: Some(appearance.header_font_size()), ..Default::default() @@ -671,7 +669,7 @@ impl ShowBlocksWidget { .with_child( Container::new( ui_builder - .paragraph(UNSHARE_BLOCK_CONFIRMATION_DIALOG_TEXT) + .paragraph(i18n::t("settings.show_blocks.unshare_confirmation")) .with_style(UiComponentStyles { font_size: Some(appearance.ui_font_size() * 1.16), ..Default::default() @@ -692,7 +690,7 @@ impl ShowBlocksWidget { ButtonVariant::Basic, view.state_handles.cancel_dialog_handle.clone(), ) - .with_text_label("Cancel".into()) + .with_text_label(i18n::t("common.cancel")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action( @@ -710,7 +708,9 @@ impl ShowBlocksWidget { .confirm_dialog_handle .clone(), ) - .with_text_label("Unshare".into()) + .with_text_label(i18n::t( + "settings.shared_blocks.unshare", + )) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action( @@ -799,7 +799,11 @@ impl SettingsWidget for ShowBlocksWidget { ); } - let header = render_page_title("Shared blocks", HEADER_FONT_SIZE, appearance); + let header = render_page_title( + &i18n::t("settings.show_blocks.title"), + HEADER_FONT_SIZE, + appearance, + ); let col = Flex::column() .with_child(Container::new(header).with_margin_bottom(24.).finish()) .with_child(Expanded::new(1., stack.finish()).finish()); diff --git a/app/src/settings_view/teams_page.rs b/app/src/settings_view/teams_page.rs index 93e4538f23..ccc4afd61e 100644 --- a/app/src/settings_view/teams_page.rs +++ b/app/src/settings_view/teams_page.rs @@ -81,19 +81,7 @@ use crate::workspaces::workspace::{ }; const TEAM_MEMBERS_HEADER_POSITION_ID: &str = "team_settings:team_members_header"; -// Styling for team create page -const TEAM_NAME_EDITOR_PLACEHOLDER_TEXT: &str = "Team name"; const CREATE_TEAM_BUTTON_LEFT_PADDING: f32 = 10.; -const CREATE_TEAM_DESCRIPTION: &str = "When you create a team, you can collaborate on agent-driven development by sharing cloud agent runs, environments, automations, and artifacts. You can also create a shared knowledge store for teammates and agents alike."; - -// Styling for team management page -const LEAVE_TEAM_BUTTON_LABEL: &str = "Leave team"; -const DELETE_TEAM_BUTTON_LABEL: &str = "Delete team"; -const CREATE_TEAM_BUTTON_LABEL: &str = "Create"; -const APPROVE_DOMAINS_PLACEHOLDER: &str = "Domains, comma separated"; -const EMAILS_PLACEHOLDER: &str = "Emails, comma separated"; -const APPROVE_DOMAINS_BUTTON_LABEL: &str = "Set"; -const SEND_EMAIL_INVITES_BUTTON_LABEL: &str = "Invite"; const BUTTON_WIDTH: f32 = 82.; const BUTTON_HEIGHT: f32 = 40.; const COPY_LINK_LEFT_PADDING: f32 = 7.; @@ -108,18 +96,6 @@ const SUBSUBSECTION_HEADER_FONT_SIZE: f32 = 14.; const OWNER_STATE_CHIP_ACCENT_OPACITY: u8 = 30; const INVITE_LINK_PREFIX: &str = "/team/"; -const INVALID_DOMAINS_INSTRUCTIONS: &str = - "Some of the provided domains are invalid, or have already been added."; - -const INVITE_LINK_TOGGLE_INSTRUCTIONS: &str = "As an admin, you can choose whether to enable or disable the ability for team members to invite others by invitation link."; -const INVITE_LINK_DOMAIN_RESTRICTIONS_INSTRUCTIONS: &str = - "Restrict by domain — only allow users with emails at specific domains to join your team through the invite link."; - -const INVITE_BY_EMAIL_EXPIRY_INSTRUCTIONS: &str = "Email invitations are valid for 7 days."; -const INVALID_EMAILS_INSTRUCTIONS: &str = - "Some of the provided email addresses are invalid, already invited, or members of the team."; - -const OFFLINE_TEXT: &str = "You are offline."; const MAX_CHIP_WIDTH: f32 = 280.; @@ -717,17 +693,19 @@ impl TeamsPageView { let appearance = Appearance::as_ref(ctx); let font_size = appearance.ui_font_size(); + let team_name_placeholder = i18n::t("settings.teams.create.name_placeholder"); let create_team_editor = Self::editor( |me, event, ctx| me.handle_editor_event(event, ctx), - TEAM_NAME_EDITOR_PLACEHOLDER_TEXT, + &team_name_placeholder, font_size, ctx, ); + let approve_domains_placeholder = i18n::t("settings.teams.invite.domains_placeholder"); let approve_domains_block_editor = ctx.add_typed_action_view(|ctx| { WordBlockEditorView::new( ctx, - APPROVE_DOMAINS_PLACEHOLDER, + &approve_domains_placeholder, font_size, vec![',', ' '], MAX_CHIP_WIDTH, @@ -738,10 +716,11 @@ impl TeamsPageView { me.handle_approve_domains_block_editor_event(event, ctx); }); + let emails_placeholder = i18n::t("settings.teams.invite.emails_placeholder"); let email_invites_block_editor = ctx.add_typed_action_view(|ctx| { WordBlockEditorView::new( ctx, - EMAILS_PLACEHOLDER, + &emails_placeholder, font_size, vec![',', ' '], MAX_CHIP_WIDTH, @@ -775,9 +754,10 @@ impl TeamsPageView { let team_name = current_user_team .map_or_else(|| "", |team| &team.name) .to_string(); + let rename_team_placeholder = i18n::t("settings.teams.rename_placeholder"); let rename_team_editor = ctx.add_typed_action_view(|ctx| { let mut input = ClickableTextInput::new(team_name, ctx); - input.set_placeholder_text("Your new team name", ctx); + input.set_placeholder_text(&rename_team_placeholder, ctx); input }); ctx.subscribe_to_view(&rename_team_editor, |me, _, event, ctx| { @@ -801,7 +781,7 @@ impl TeamsPageView { }); let transfer_ownership_modal = ctx.add_typed_action_view(|ctx| { Modal::new( - Some("Transfer team ownership?".to_string()), + Some(i18n::t("settings.teams.transfer_ownership.modal_title")), transfer_ownership_modal_body, ctx, ) @@ -919,7 +899,11 @@ impl TeamsPageView { } UserWorkspacesEvent::EmailInviteRejected(err) => { self.update_team_members_state(ctx); - self.show_error("Failed to send invite", Some(err), ctx) + self.show_error( + i18n::t("settings.teams.toast.invite_send_failed"), + Some(err), + ctx, + ) } UserWorkspacesEvent::TeamsChanged => { self.update_team_members_state(ctx); @@ -932,25 +916,37 @@ impl TeamsPageView { ctx.emit(TeamsPageViewEvent::TeamsChanged); } UserWorkspacesEvent::ToggleInviteLinksSuccess => { - self.show_success("Toggled invite links", ctx); + self.show_success(i18n::t("settings.teams.toast.invite_links_toggled"), ctx); ctx.notify(); } UserWorkspacesEvent::ToggleInviteLinksRejected(err) => { - self.show_error("Failed to toggle invite links", Some(err), ctx); + self.show_error( + i18n::t("settings.teams.toast.invite_links_toggle_failed"), + Some(err), + ctx, + ); } UserWorkspacesEvent::ResetInviteLinks => { - self.show_success("Reset invite links", ctx); + self.show_success(i18n::t("settings.teams.toast.invite_links_reset"), ctx); ctx.notify(); } UserWorkspacesEvent::ResetInviteLinksRejected(err) => { - self.show_error("Failed to reset invite links", Some(err), ctx); + self.show_error( + i18n::t("settings.teams.toast.invite_links_reset_failed"), + Some(err), + ctx, + ); } UserWorkspacesEvent::DeleteTeamInvite => { self.update_team_members_state(ctx); - self.show_success("Deleted invite", ctx); + self.show_success(i18n::t("settings.teams.toast.invite_deleted"), ctx); } UserWorkspacesEvent::DeleteTeamInviteRejected(err) => { - self.show_error("Failed to delete invite", Some(err), ctx); + self.show_error( + i18n::t("settings.teams.toast.invite_delete_failed"), + Some(err), + ctx, + ); } UserWorkspacesEvent::AddDomainRestrictionsSuccess => { self.approve_domains_block_editor @@ -959,20 +955,24 @@ impl TeamsPageView { }); self.update_approved_domains_state(ctx); } - UserWorkspacesEvent::AddDomainRestrictionsRejected(err) => { - self.show_error("Failed to add domain restriction", Some(err), ctx) - } + UserWorkspacesEvent::AddDomainRestrictionsRejected(err) => self.show_error( + i18n::t("settings.teams.toast.domain_add_failed"), + Some(err), + ctx, + ), UserWorkspacesEvent::DeleteDomainRestrictionSuccess => { self.update_approved_domains_state(ctx); } - UserWorkspacesEvent::DeleteDomainRestrictionRejected(err) => { - self.show_error("Failed to delete domain restriction", Some(err), ctx) - } + UserWorkspacesEvent::DeleteDomainRestrictionRejected(err) => self.show_error( + i18n::t("settings.teams.toast.domain_delete_failed"), + Some(err), + ctx, + ), UserWorkspacesEvent::GenerateUpgradeLink(upgrade_link) => { ctx.open_url(upgrade_link); } UserWorkspacesEvent::GenerateUpgradeLinkRejected(err) => self.show_error( - "Failed to generate upgrade link. Please contact us at feedback@warp.dev", + i18n::t("settings.teams.toast.upgrade_link_failed"), Some(err), ctx, ), @@ -980,16 +980,20 @@ impl TeamsPageView { ctx.open_url(billing_session_link); } UserWorkspacesEvent::GenerateStripeBillingPortalLinkRejected(err) => self.show_error( - "Failed to generate billing link. Please contact us at feedback@warp.dev", + i18n::t("settings.teams.toast.billing_link_failed"), Some(err), ctx, ), UserWorkspacesEvent::ToggleTeamDiscoverabilitySuccess => { - self.show_success("Toggled team discoverability", ctx); + self.show_success(i18n::t("settings.teams.toast.discoverability_toggled"), ctx); ctx.notify(); } UserWorkspacesEvent::ToggleTeamDiscoverabilityRejected(err) => { - self.show_error("Failed to toggle team discoverability", Some(err), ctx); + self.show_error( + i18n::t("settings.teams.toast.discoverability_toggle_failed"), + Some(err), + ctx, + ); } UserWorkspacesEvent::JoinTeamWithTeamDiscoverySuccess => { // Force refresh of Warp Drive objects after joining a team @@ -997,18 +1001,18 @@ impl TeamsPageView { update_manager.refresh_updated_objects(ctx); }); - let message = self - .user_workspaces - .as_ref(ctx) - .current_team() - .map_or("Successfully joined team".to_string(), |team| { - format!("Successfully joined {}", team.name) - }); + let message = self.user_workspaces.as_ref(ctx).current_team().map_or( + i18n::t("settings.teams.toast.join_success_generic"), + |team| { + i18n::t("settings.teams.toast.join_success_named") + .replace("{name}", &team.name) + }, + ); self.show_success(message, ctx); ctx.notify(); } UserWorkspacesEvent::JoinTeamWithTeamDiscoveryRejected(err) => { - self.show_error("Failed to join team", Some(err), ctx); + self.show_error(i18n::t("settings.teams.toast.join_failed"), Some(err), ctx); } UserWorkspacesEvent::FetchDiscoverableTeamsSuccess(teams) => { self.discoverable_teams_states = teams @@ -1022,18 +1026,26 @@ impl TeamsPageView { log::error!("Failed to fetch discoverable teams: {e:?}"); } UserWorkspacesEvent::TransferTeamOwnershipSuccess => { - self.show_success("Successfully transferred team ownership", ctx); + self.show_success(i18n::t("settings.teams.toast.ownership_transferred"), ctx); ctx.notify(); } UserWorkspacesEvent::TransferTeamOwnershipRejected(err) => { - self.show_error("Failed to transfer team ownership", Some(err), ctx); + self.show_error( + i18n::t("settings.teams.toast.ownership_transfer_failed"), + Some(err), + ctx, + ); } UserWorkspacesEvent::SetTeamMemberRoleSuccess => { self.update_team_members_state(ctx); - self.show_success("Successfully updated team member role", ctx); + self.show_success(i18n::t("settings.teams.toast.role_updated"), ctx); } UserWorkspacesEvent::SetTeamMemberRoleRejected(err) => { - self.show_error("Failed to update team member role", Some(err), ctx); + self.show_error( + i18n::t("settings.teams.toast.role_update_failed"), + Some(err), + ctx, + ); } UserWorkspacesEvent::UpdateWorkspaceSettingsSuccess => { // as of right now, this is only emitted on the billing & usage page @@ -1150,18 +1162,18 @@ impl TeamsPageView { ) { match event { TeamUpdateManagerEvent::LeaveError => { - let error = "Error leaving team".to_string(); + let error = i18n::t("settings.teams.toast.leave_failed"); self.show_error(error, None, ctx); } TeamUpdateManagerEvent::LeaveSuccess => { - self.show_success("Successfully left team", ctx); + self.show_success(i18n::t("settings.teams.toast.leave_success"), ctx); ctx.notify(); } TeamUpdateManagerEvent::RenameTeamSuccess => { - self.show_success("Successfully renamed team", ctx) + self.show_success(i18n::t("settings.teams.toast.rename_success"), ctx) } TeamUpdateManagerEvent::RenameTeamError => { - self.show_error("Failed to rename team", None, ctx) + self.show_error(i18n::t("settings.teams.toast.rename_failed"), None, ctx) } } } @@ -1445,7 +1457,11 @@ impl TeamsPageView { fn copy_invite_link(&mut self, link: &str, ctx: &mut ViewContext) { ctx.clipboard() .write(ClipboardContent::plain_text(link.to_string())); - self.show_toast("Link copied to clipboard!", ToastFlavor::Default, ctx); + self.show_toast( + i18n::t("settings.teams.toast.link_copied"), + ToastFlavor::Default, + ctx, + ); } fn remove_user_from_team( @@ -1510,7 +1526,8 @@ impl TeamsPageView { // Verify no invalid domains before continuing let invalid_domains = editor.get_list_of_invalid_words(ctx); if !invalid_domains.is_empty() { - let error = format!("Invalid domains: {}", invalid_domains.len()); + let error = i18n::t("settings.teams.toast.invalid_domains_count") + .replace("{count}", &invalid_domains.len().to_string()); self.show_error(error, None, ctx); return; } @@ -1530,7 +1547,8 @@ impl TeamsPageView { .collect(); self.show_success( - format!("Domain restrictions added: {}", unique_domains.len()), + i18n::t("settings.teams.toast.domains_added_count") + .replace("{count}", &unique_domains.len().to_string()), ctx, ); self.user_workspaces @@ -1557,7 +1575,8 @@ impl TeamsPageView { // Verify no invalid emails before continuing let invalid_emails = editor.get_list_of_invalid_words(ctx); if !invalid_emails.is_empty() { - let error = format!("Invalid emails: {}", invalid_emails.len()); + let error = i18n::t("settings.teams.toast.invalid_emails_count") + .replace("{count}", &invalid_emails.len().to_string()); self.show_error(error, None, ctx); return; } @@ -1577,9 +1596,10 @@ impl TeamsPageView { .collect(); let message = if unique_emails.len() == 1 { - "Your invite is on the way!".to_string() + i18n::t("settings.teams.toast.invite_sent_single") } else { - format!("Your {} invites are on the way!", unique_emails.len()) + i18n::t("settings.teams.toast.invite_sent_multiple") + .replace("{count}", &unique_emails.len().to_string()) }; self.show_success(message, ctx); self.user_workspaces @@ -1718,7 +1738,7 @@ impl TeamsPageView { let actions = if current_user_has_admin_permissions { vec![ItemAction { icon: Icon::X, - label: "Cancel invite".to_string(), + label: i18n::t("settings.teams.action.cancel_invite"), action: TeamsPageAction::DeletePendingEmailInvitation { team_uid: team.uid, invitee_email: email_invite.invitee_email.clone(), @@ -1755,7 +1775,7 @@ impl TeamsPageView { if current_user_has_owner_permissions && !team_member_has_owner_permissions { actions.push(ItemAction { icon: Icon::Users, - label: "Transfer ownership".to_string(), + label: i18n::t("settings.teams.action.transfer_ownership"), action: TeamsPageAction::ShowTransferOwnershipModal { new_owner_email: member.email.clone(), new_owner_uid: member.uid, @@ -1772,7 +1792,7 @@ impl TeamsPageView { if team_member_has_admin_permissions { actions.push(ItemAction { icon: Icon::ArrowDown, - label: "Demote from admin".to_string(), + label: i18n::t("settings.teams.action.demote_from_admin"), action: TeamsPageAction::SetTeamMemberRole { team_uid: team.uid, user_uid: member.uid, @@ -1782,7 +1802,7 @@ impl TeamsPageView { } else { actions.push(ItemAction { icon: Icon::ArrowUp, - label: "Promote to admin".to_string(), + label: i18n::t("settings.teams.action.promote_to_admin"), action: TeamsPageAction::SetTeamMemberRole { team_uid: team.uid, user_uid: member.uid, @@ -1796,7 +1816,7 @@ impl TeamsPageView { if current_user_has_admin_permissions && !team_member_has_owner_permissions { actions.push(ItemAction { icon: Icon::X, - label: "Remove from team".to_string(), + label: i18n::t("settings.teams.action.remove_from_team"), action: TeamsPageAction::RemoveUserFromTeam { user_uid: member.uid, team_uid: team.uid, @@ -1995,12 +2015,18 @@ impl TeamsWidget { .finish(); let title = match warning { - GrowTeamWarning::SeatCapReached => "Your team is full", - GrowTeamWarning::SeatCapExceeded => "You've exceeded your member limit", - GrowTeamWarning::PaymentPastDue => "Payment past due", - GrowTeamWarning::PaymentUnpaid => "Subscription unpaid", + GrowTeamWarning::SeatCapReached => i18n::t("settings.teams.warning.team_full_title"), + GrowTeamWarning::SeatCapExceeded => { + i18n::t("settings.teams.warning.member_limit_exceeded_title") + } + GrowTeamWarning::PaymentPastDue => { + i18n::t("settings.teams.warning.payment_past_due_title") + } + GrowTeamWarning::PaymentUnpaid => { + i18n::t("settings.teams.warning.subscription_unpaid_title") + } }; - let title_element = self.render_subsection_header(title.to_owned(), appearance); + let title_element = self.render_subsection_header(title, appearance); let cta = Self::grow_team_warning_cta( warning, @@ -2010,16 +2036,16 @@ impl TeamsWidget { ); let body_prefix = match warning { - GrowTeamWarning::SeatCapReached => "You've reached your plan's member limit.", + GrowTeamWarning::SeatCapReached => { + i18n::t("settings.teams.warning.seat_cap_reached_body") + } GrowTeamWarning::SeatCapExceeded => { - "You've exceeded your plan's member limit. Existing team members keep their access, but you won't be able to add new members." + i18n::t("settings.teams.warning.seat_cap_exceeded_body") } GrowTeamWarning::PaymentPastDue => { - "Team invites have been restricted due to a past-due payment." - } - GrowTeamWarning::PaymentUnpaid => { - "Team invites have been restricted due to an unpaid subscription." + i18n::t("settings.teams.warning.payment_past_due_body") } + GrowTeamWarning::PaymentUnpaid => i18n::t("settings.teams.warning.payment_unpaid_body"), }; let is_delinquency = matches!( @@ -2028,22 +2054,24 @@ impl TeamsWidget { ); let cta_sentence = if !has_admin_permissions { if is_delinquency { - "Contact a team admin to restore access." + i18n::t("settings.teams.warning.cta_contact_admin_restore") } else { - "Contact a team admin to grow the team." + i18n::t("settings.teams.warning.cta_contact_admin_grow") } } else { match cta { - GrowTeamWarningCta::Upgrade => "Upgrade to grow your team.", + GrowTeamWarningCta::Upgrade => i18n::t("settings.teams.warning.cta_upgrade_grow"), GrowTeamWarningCta::UpdateBilling => { - "Update your payment information to restore access." + i18n::t("settings.teams.warning.cta_update_payment_restore") + } + GrowTeamWarningCta::ContactSupport => { + i18n::t("settings.teams.warning.cta_contact_support_restore") } - GrowTeamWarningCta::ContactSupport => "Contact support to restore access.", GrowTeamWarningCta::None => { if is_delinquency { - "Contact support to restore access." + i18n::t("settings.teams.warning.cta_contact_support_restore") } else { - "Contact sales to grow your team." + i18n::t("settings.teams.warning.cta_contact_sales_grow") } } } @@ -2073,16 +2101,17 @@ impl TeamsWidget { // mouse state handle is fine because at most one CTA shows at a time. if let Some((cta_label, cta_action)) = match cta { GrowTeamWarningCta::Upgrade => Some(( - "Upgrade", + i18n::t("settings.teams.warning.cta_button_upgrade"), TeamsPageAction::GenerateUpgradeLink { team_uid: team.uid }, )), GrowTeamWarningCta::UpdateBilling => Some(( - "Update billing", + i18n::t("settings.teams.warning.cta_button_update_billing"), TeamsPageAction::GenerateStripeBillingPortalLink { team_uid: team.uid }, )), - GrowTeamWarningCta::ContactSupport => { - Some(("Contact support", TeamsPageAction::ContactSupport)) - } + GrowTeamWarningCta::ContactSupport => Some(( + i18n::t("settings.teams.warning.cta_button_contact_support"), + TeamsPageAction::ContactSupport, + )), GrowTeamWarningCta::None => None, } { let cta_mouse_state = self @@ -2136,16 +2165,17 @@ impl TeamsWidget { .finish() } - fn outgrow_upgrade_line_copy( - billing_metadata: &BillingMetadata, - ) -> (&'static str, &'static str) { + fn outgrow_upgrade_line_copy(billing_metadata: &BillingMetadata) -> (String, String) { if billing_metadata.customer_type == CustomerType::Business { ( - "Upgrade to Enterprise", - " for an unlimited team member limit.", + i18n::t("settings.teams.outgrow.upgrade_enterprise_link"), + i18n::t("settings.teams.outgrow.upgrade_enterprise_suffix"), ) } else { - ("Upgrade to Business", " for a higher team member limit.") + ( + i18n::t("settings.teams.outgrow.upgrade_business_link"), + i18n::t("settings.teams.outgrow.upgrade_business_suffix"), + ) } } @@ -2157,21 +2187,21 @@ impl TeamsWidget { has_admin_permissions: bool, ) -> Box { let prorated_message = if has_admin_permissions { - "You'll be charged for a portion of the team member's usage of Warp." + i18n::t("settings.teams.cost_info.prorated_admin") } else { - "Your admin will be charged for a portion of the team member's usage of Warp." + i18n::t("settings.teams.cost_info.prorated_member") }; let additional_members_cost_money_msg = if let Some((monthly_cost, yearly_cost)) = self.get_per_seat_costs(team_metadata, pricing_info_model) { - format!( - "Additional members are billed at your plan's per-user rate: ${monthly_cost:.0}/month or ${yearly_cost:.0}/year, depending on your billing interval. {prorated_message}" - ) + i18n::t("settings.teams.cost_info.per_seat_with_price") + .replace("{monthly}", &format!("{monthly_cost:.0}")) + .replace("{yearly}", &format!("{yearly_cost:.0}")) + .replace("{prorated}", &prorated_message) } else { - format!( - "Additional members are billed at your plan's per-user rate. {prorated_message}" - ) + i18n::t("settings.teams.cost_info.per_seat_no_price") + .replace("{prorated}", &prorated_message) }; let horizontal_padding = 16.; @@ -2390,7 +2420,7 @@ impl TeamsWidget { left_side.add_child( Container::new(self.render_delinquency_badge( appearance, - "PAST DUE".into(), + i18n::t("settings.teams.badge.past_due"), themes::theme::Fill::from(*PAST_DUE_BADGE_COLOR).into(), )) .with_margin_left(8.) @@ -2401,7 +2431,7 @@ impl TeamsWidget { left_side.add_child( Container::new(self.render_delinquency_badge( appearance, - "UNPAID".into(), + i18n::t("settings.teams.badge.unpaid"), themes::theme::Fill::from(*UNPAID_BADGE_COLOR).into(), )) .with_margin_left(8.) @@ -2433,7 +2463,7 @@ impl TeamsWidget { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Contact support", + i18n::t("settings.teams.button.contact_support"), Icon::Phone.to_warpui_icon(appearance.theme().accent()), MainAxisSize::Min, MainAxisAlignment::Center, @@ -2462,7 +2492,7 @@ impl TeamsWidget { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Manage billing", + i18n::t("settings.teams.button.manage_billing"), Icon::CoinsStacked.to_warpui_icon(appearance.theme().accent()), MainAxisSize::Min, MainAxisAlignment::Center, @@ -2493,7 +2523,7 @@ impl TeamsWidget { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Open admin panel", + i18n::t("settings.teams.button.open_admin_panel"), Icon::Users.to_warpui_icon(appearance.theme().accent()), MainAxisSize::Min, MainAxisAlignment::Center, @@ -2527,17 +2557,17 @@ impl TeamsWidget { // If the team is upgradeable to self-serve tier, show them the upgrade link. if team.billing_metadata.can_upgrade_to_higher_tier_plan() { let description = if team.billing_metadata.can_upgrade_to_build_plan() { - "Upgrade to Build" + i18n::t("settings.teams.billing.upgrade_build") } else { match team.billing_metadata.customer_type { - CustomerType::Prosumer => "Upgrade to Turbo plan", - CustomerType::Turbo => "Upgrade to Lightspeed plan", - _ => "Compare plans", + CustomerType::Prosumer => i18n::t("settings.teams.billing.upgrade_turbo"), + CustomerType::Turbo => i18n::t("settings.teams.billing.upgrade_lightspeed"), + _ => i18n::t("settings.teams.billing.compare_plans"), } }; billing_links.add_child( Container::new(self.render_compare_plans_button( - description, + &description, self.mouse_state_handles.upgrade_link.clone(), team_uid, appearance, @@ -2573,10 +2603,10 @@ impl TeamsWidget { ) -> Box { let mut section = Flex::column(); let sub_header_text = match team.billing_metadata.customer_type { - CustomerType::Free => "Free plan usage limits", - _ => "Plan usage limits", + CustomerType::Free => i18n::t("settings.teams.plan_usage.free_limits_header"), + _ => i18n::t("settings.teams.plan_usage.limits_header"), }; - section.add_child(self.render_subsection_header(sub_header_text.into(), appearance)); + section.add_child(self.render_subsection_header(sub_header_text, appearance)); let mut shared_objects_usage_row = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Center); @@ -2584,9 +2614,10 @@ impl TeamsWidget { if let Some(policy) = team.billing_metadata.tier.shared_notebooks_policy { if !policy.is_unlimited { let mut shared_notebooks_column = Flex::column(); - shared_notebooks_column.add_child( - self.render_plan_usage_header("Shared Notebooks".into(), appearance), - ); + shared_notebooks_column.add_child(self.render_plan_usage_header( + i18n::t("settings.teams.plan_usage.shared_notebooks"), + appearance, + )); let num_shared_notebooks = cloud_model .active_notebooks_in_space(Space::Team { team_uid: team.uid }, app) .count(); @@ -2609,9 +2640,10 @@ impl TeamsWidget { if let Some(policy) = team.billing_metadata.tier.shared_workflows_policy { if !policy.is_unlimited { let mut shared_workflows_column = Flex::column(); - shared_workflows_column.add_child( - self.render_plan_usage_header("Shared Workflows".into(), appearance), - ); + shared_workflows_column.add_child(self.render_plan_usage_header( + i18n::t("settings.teams.plan_usage.shared_workflows"), + appearance, + )); let num_shared_workflows = cloud_model .active_workflows_in_space(Space::Team { team_uid: team.uid }, app) .count(); @@ -2663,7 +2695,7 @@ impl TeamsWidget { invitation_section.add_child( Container::new( - self.render_subsection_header("Invite team members".to_owned(), appearance), + self.render_subsection_header(i18n::t("settings.teams.invite.header"), appearance), ) .with_padding_bottom(16.) .finish(), @@ -2735,13 +2767,16 @@ impl TeamsWidget { // Header + admin-only subtext on the left, toggle on the right. The // text is stacked so the toggle centers against the whole block. - let header = self.render_subsubsection_header("By link".to_owned(), appearance); + let header = self.render_subsubsection_header( + i18n::t("settings.teams.invite.by_link_header"), + appearance, + ); let text_column = if has_admin_permissions { Flex::column() .with_child(header) .with_child( Container::new(self.render_sub_text( - INVITE_LINK_TOGGLE_INSTRUCTIONS.into(), + i18n::t("settings.teams.invite.link_toggle_instructions"), appearance, Some(Coords::uniform(0.).right(48.)), )) @@ -2793,7 +2828,7 @@ impl TeamsWidget { appearance .ui_builder() .link( - "Reset links".into(), + i18n::t("settings.teams.invite.reset_links"), None, Some(Box::new(move |ctx| { ctx.dispatch_typed_action(TeamsPageAction::ResetInviteLinks { @@ -2839,10 +2874,13 @@ impl TeamsWidget { // "By email" subsection header section.add_child( - Container::new(self.render_subsubsection_header("By email".to_owned(), appearance)) - .with_padding_top(CONTENT_SEPARATION_PADDING) - .with_padding_bottom(8.) - .finish(), + Container::new(self.render_subsubsection_header( + i18n::t("settings.teams.invite.by_email_header"), + appearance, + )) + .with_padding_top(CONTENT_SEPARATION_PADDING) + .with_padding_bottom(8.) + .finish(), ); // Form stays visually unchanged when blocked; the chip editor is @@ -2851,7 +2889,7 @@ impl TeamsWidget { // the invitation section owns the explanation + recovery CTA. section.add_child( Container::new(self.render_sub_text( - INVITE_BY_EMAIL_EXPIRY_INSTRUCTIONS.into(), + i18n::t("settings.teams.invite.email_expiry_instructions"), appearance, Some(Coords::uniform(0.).right(48.)), )) @@ -2887,9 +2925,10 @@ impl TeamsWidget { && view.email_invites_block_editor_state.num_chips > 0 { section.add_child( - Container::new( - self.render_error_sub_text(INVALID_EMAILS_INSTRUCTIONS.into(), appearance), - ) + Container::new(self.render_error_sub_text( + i18n::t("settings.teams.invite.invalid_emails_instructions"), + appearance, + )) .with_padding_top(8.) .finish(), ) @@ -2912,7 +2951,9 @@ impl TeamsWidget { .with_main_axis_size(MainAxisSize::Max) .with_main_axis_alignment(MainAxisAlignment::SpaceBetween) .with_cross_axis_alignment(CrossAxisAlignment::Center) - .with_child(self.render_subsection_header("Team members".to_owned(), appearance)) + .with_child( + self.render_subsection_header(i18n::t("settings.teams.members.header"), appearance), + ) .with_child(self.render_team_members_count(team, appearance)) .finish(); section.add_child( @@ -2939,9 +2980,9 @@ impl TeamsWidget { fn render_team_members_count(&self, team: &Team, appearance: &Appearance) -> Box { let count = team.members.len(); let count_label = if count == 1 { - "1 team member".to_string() + i18n::t("settings.teams.members.count_single") } else { - format!("{count} team members") + i18n::t("settings.teams.members.count_plural").replace("{count}", &count.to_string()) }; // No capacity tooltip when the plan is unlimited (or workspace size @@ -2984,8 +3025,9 @@ impl TeamsWidget { }; let plan_display = team.billing_metadata.customer_type.to_display_string(); - let tooltip_text = - format!("Your plan ({plan_display}) has a maximum capacity of {cap} members."); + let tooltip_text = i18n::t("settings.teams.members.capacity_tooltip") + .replace("{plan}", &plan_display) + .replace("{cap}", &cap.to_string()); let info_icon = Container::new( ConstrainedBox::new(Icon::Info.to_warpui_icon(muted_color).finish()) @@ -3038,7 +3080,11 @@ impl TeamsWidget { let team_uid = team.uid; let (link_text, suffix) = Self::outgrow_upgrade_line_copy(&team.billing_metadata); - let prefix = self.render_sub_text("Need more seats? ".to_string(), appearance, None); + let prefix = self.render_sub_text( + i18n::t("settings.teams.outgrow.need_more_seats"), + appearance, + None, + ); let link = appearance .ui_builder() .link( @@ -3079,7 +3125,7 @@ impl TeamsWidget { if has_admin_permissions { section.add_child( Container::new(self.render_sub_text( - INVITE_LINK_DOMAIN_RESTRICTIONS_INSTRUCTIONS.into(), + i18n::t("settings.teams.invite.domain_restrictions_instructions"), appearance, Some(Coords::uniform(0.).right(48.)), )) @@ -3115,9 +3161,10 @@ impl TeamsWidget { && view.approve_domains_block_editor_state.num_chips > 0 { section.add_child( - Container::new( - self.render_error_sub_text(INVALID_DOMAINS_INSTRUCTIONS.into(), appearance), - ) + Container::new(self.render_error_sub_text( + i18n::t("settings.teams.invite.invalid_domains_instructions"), + appearance, + )) .with_padding_top(8.) .finish(), ) @@ -3132,7 +3179,7 @@ impl TeamsWidget { let actions = if has_admin_permissions { vec![ItemAction { icon: Icon::X, - label: "Remove domain".to_string(), + label: i18n::t("settings.teams.action.remove_domain"), action: TeamsPageAction::DeleteDomainRestriction { domain_uid: domain_restriction.uid, team_uid: team.uid, @@ -3180,8 +3227,9 @@ impl TeamsWidget { } else { (None, ButtonVariant::Basic) }; + let approve_domains_label = i18n::t("settings.teams.button.set_domains"); Container::new(self.render_button( - APPROVE_DOMAINS_BUTTON_LABEL, + &approve_domains_label, variant, self.mouse_state_handles.approve_domains_button.clone(), action, @@ -3210,8 +3258,9 @@ impl TeamsWidget { } else { (None, ButtonVariant::Basic) }; + let send_invites_label = i18n::t("settings.teams.button.invite"); Container::new(self.render_button( - SEND_EMAIL_INVITES_BUTTON_LABEL, + &send_invites_label, variant, self.mouse_state_handles.send_email_invites_button.clone(), action, @@ -3246,11 +3295,14 @@ impl TeamsWidget { ) -> Box { // Same layout as the "By link" header row: text column on the left, // toggle on the right. - let header = self.render_subsubsection_header("By discovery".to_owned(), appearance); + let header = self.render_subsubsection_header( + i18n::t("settings.teams.invite.by_discovery_header"), + appearance, + ); let domain = current_user_email.split('@').nth(1).unwrap_or(""); let team_discoverability_instructions = - format!("Allow Warp users with an @{domain} email to find and join the team."); + i18n::t("settings.teams.invite.discovery_instructions").replace("{domain}", domain); let subtext = self.render_sub_text( team_discoverability_instructions, appearance, @@ -3303,12 +3355,12 @@ impl TeamsWidget { let (label, action) = if is_team_owner { ( - DELETE_TEAM_BUTTON_LABEL, + i18n::t("settings.teams.button.delete_team"), TeamsPageAction::ShowDeleteTeamConfirmationDialog, ) } else { ( - LEAVE_TEAM_BUTTON_LABEL, + i18n::t("settings.teams.button.leave_team"), TeamsPageAction::ShowLeaveTeamConfirmationDialog, ) }; @@ -3387,7 +3439,7 @@ impl TeamsWidget { let link = appearance .ui_builder() .link( - "Manage plan".into(), + i18n::t("settings.teams.button.manage_plan"), None, Some(Box::new(move |ctx| { ctx.dispatch_typed_action( @@ -3485,7 +3537,7 @@ impl TeamsWidget { pending_and_close_row.add_child( self.render_state_chip( appearance, - "EXPIRED".into(), + i18n::t("settings.teams.chip.expired"), appearance.theme().ui_error_color(), themes::theme::Fill::from(appearance.theme().ui_error_color()) .with_opacity(30) @@ -3499,7 +3551,7 @@ impl TeamsWidget { pending_and_close_row.add_child( self.render_state_chip( appearance, - "PENDING".into(), + i18n::t("settings.teams.chip.pending"), *EMAIL_INVITE_PENDING_COLOR, themes::theme::Fill::from(*EMAIL_INVITE_PENDING_COLOR) .with_opacity(30) @@ -3513,7 +3565,7 @@ impl TeamsWidget { pending_and_close_row.add_child( self.render_state_chip( appearance, - "OWNER".into(), + i18n::t("settings.teams.chip.owner"), owner_state_chip_text_color(appearance.theme()), appearance .theme() @@ -3529,7 +3581,7 @@ impl TeamsWidget { pending_and_close_row.add_child( self.render_state_chip( appearance, - "ADMIN".into(), + i18n::t("settings.teams.chip.admin"), appearance .theme() .background() @@ -3685,7 +3737,7 @@ impl TeamsWidget { ); (link, true) } - None => ("Failed to load invite link.".into(), false), + None => (i18n::t("settings.teams.invite.link_load_failed"), false), }; let theme = appearance.theme(); @@ -3980,13 +4032,18 @@ impl TeamsWidget { let mut page = Flex::column(); // Title, subtitle, and description - page.add_child(render_sub_header(appearance, "Teams".to_string(), None)); - page.add_child( - self.render_sub_header_with_subtext_color(appearance, "Create a team".to_string()), - ); + page.add_child(render_sub_header( + appearance, + i18n::t("settings.teams.create.title"), + None, + )); + page.add_child(self.render_sub_header_with_subtext_color( + appearance, + i18n::t("settings.teams.create.subtitle"), + )); page.add_child( Container::new( - self.render_description(CREATE_TEAM_DESCRIPTION.to_string(), appearance), + self.render_description(i18n::t("settings.teams.create.description"), appearance), ) .with_padding_top(6.) .finish(), @@ -4009,10 +4066,9 @@ impl TeamsWidget { .with_margin_left(-4.) .finish(); let checkbox_row_text = if let Some(domain) = view.auth_state.user_email_domain() { - format!("Allow Warp users with an @{domain} email to find and join the team.") + i18n::t("settings.teams.invite.discovery_instructions").replace("{domain}", &domain) } else { - "Allow Warp users with the same email domain as you to find and join the team." - .to_string() + i18n::t("settings.teams.create.discovery_same_domain") }; let checkbox_row = Container::new( Flex::row() @@ -4042,7 +4098,7 @@ impl TeamsWidget { page.add_child(render_separator(appearance)); page.add_child(self.render_sub_header_with_subtext_color( appearance, - "Or, join an existing team within your company".to_string(), + i18n::t("settings.teams.discovery.join_existing_subtitle"), )); // Team discovery @@ -4120,22 +4176,20 @@ impl TeamsWidget { // Number of teammates let teammate_string = if team_state.team.num_members == 1 { - "1 teammate".to_string() + i18n::t("settings.teams.discovery.teammate_count_single") } else { - format!("{} teammates", team_state.team.num_members) + i18n::t("settings.teams.discovery.teammate_count_plural") + .replace("{count}", &team_state.team.num_members.to_string()) }; single_team.add_child(self.render_sub_text(teammate_string, appearance, None)); // Call to action single_team.add_child( - Container::new( - self.render_sub_text( - "Join this team and start collaborating on workflows, notebooks, and more." - .to_string(), - appearance, - None, - ), - ) + Container::new(self.render_sub_text( + i18n::t("settings.teams.discovery.join_cta"), + appearance, + None, + )) .with_padding_top(12.) .with_padding_bottom(12.) .finish(), @@ -4262,7 +4316,7 @@ impl TeamsWidget { ButtonVariant::Accent, self.mouse_state_handles.create_team_button.clone(), ) - .with_centered_text_label(CREATE_TEAM_BUTTON_LABEL.to_owned()) + .with_centered_text_label(i18n::t("settings.teams.button.create")) .with_style(UiComponentStyles { font_color: Some( appearance @@ -4310,8 +4364,9 @@ impl TeamsWidget { appearance: &Appearance, ) -> Box { if team_state.team.team_accepting_invites { + let join_label = i18n::t("settings.teams.button.join"); self.render_button( - "Join", + &join_label, ButtonVariant::Accent, team_state.mouse_state_handle.clone(), Some(TeamsPageAction::JoinTeamWithTeamDiscovery { @@ -4341,7 +4396,7 @@ impl TeamsWidget { font_size: Some(14.), ..Default::default() }) - .with_centered_text_label("Contact Admin to request access".to_string()) + .with_centered_text_label(i18n::t("settings.teams.button.contact_admin_request")) .disabled() .build() .finish() @@ -4386,7 +4441,7 @@ impl SettingsWidget for TeamsWidget { } else { appearance .ui_builder() - .span(OFFLINE_TEXT.to_string()) + .span(i18n::t("settings.teams.offline_text")) .build() .finish() }; diff --git a/app/src/settings_view/transfer_ownership_confirmation_modal.rs b/app/src/settings_view/transfer_ownership_confirmation_modal.rs index c647df41ed..fced58a08b 100644 --- a/app/src/settings_view/transfer_ownership_confirmation_modal.rs +++ b/app/src/settings_view/transfer_ownership_confirmation_modal.rs @@ -51,10 +51,7 @@ impl View for TransferOwnershipConfirmationModal { let email = self.new_owner_email.as_deref().unwrap_or_default(); let description_text = Text::new( - format!( - "Are you sure you want to transfer team ownership to {}? You will no longer be the owner and will not be able to take any administrative actions for this team.", - email - ), + i18n::t("settings.teams.transfer_ownership.description").replace("{email}", email), appearance.ui_font_family(), 14., ) @@ -73,7 +70,7 @@ impl View for TransferOwnershipConfirmationModal { appearance .ui_builder() .button(ButtonVariant::Secondary, self.cancel_mouse_state.clone()) - .with_text_label("Cancel".to_string()) + .with_text_label(i18n::t("common.cancel")) .with_style(button_style) .build() .on_click(|ctx, _, _| { @@ -86,7 +83,7 @@ impl View for TransferOwnershipConfirmationModal { appearance .ui_builder() .button(ButtonVariant::Accent, self.confirm_mouse_state.clone()) - .with_text_label("Transfer".to_string()) + .with_text_label(i18n::t("settings.transfer_ownership.transfer")) .with_style(button_style) .build() .on_click(|ctx, _, _| { diff --git a/app/src/settings_view/update_environment_form.rs b/app/src/settings_view/update_environment_form.rs index f04dd0c281..df5ad47233 100644 --- a/app/src/settings_view/update_environment_form.rs +++ b/app/src/settings_view/update_environment_form.rs @@ -256,14 +256,30 @@ pub struct EnvironmentFormCopy { impl EnvironmentFormCopy { pub fn orchestration_modal() -> Self { Self { - name_placeholder: "e.g., dev-env", - repos_placeholder_authed: "Browse GitHub repos...", - repos_placeholder_unauthed: REPOS_PLACEHOLDER_UNAUTHED, - docker_image_label: "Docker image", - docker_image_placeholder: "e.g., node:20-alpine", - description_placeholder: DESCRIPTION_PLACEHOLDER, - setup_commands_placeholder: "e.g., node start", - setup_commands_helper: "Press Enter or click the submit button to add each command.", + name_placeholder: Box::leak( + i18n::t("settings.environments.name.placeholder_example").into_boxed_str(), + ), + repos_placeholder_authed: Box::leak( + i18n::t("settings.environments.repos.placeholder_browse").into_boxed_str(), + ), + repos_placeholder_unauthed: Box::leak( + i18n::t("settings.environments.repos.placeholder_unauthed").into_boxed_str(), + ), + docker_image_label: Box::leak( + i18n::t("settings.environments.docker_image.label_short").into_boxed_str(), + ), + docker_image_placeholder: Box::leak( + i18n::t("settings.environments.docker_image.placeholder_short").into_boxed_str(), + ), + description_placeholder: Box::leak( + i18n::t("settings.environments.description.placeholder").into_boxed_str(), + ), + setup_commands_placeholder: Box::leak( + i18n::t("settings.environments.setup_commands.placeholder_short").into_boxed_str(), + ), + setup_commands_helper: Box::leak( + i18n::t("settings.environments.setup_commands.helper_short").into_boxed_str(), + ), show_description_character_count: false, } } @@ -272,14 +288,30 @@ impl EnvironmentFormCopy { impl Default for EnvironmentFormCopy { fn default() -> Self { Self { - name_placeholder: "Environment name", - repos_placeholder_authed: REPOS_PLACEHOLDER_AUTHED, - repos_placeholder_unauthed: REPOS_PLACEHOLDER_UNAUTHED, - docker_image_label: "Docker image reference", - docker_image_placeholder: "e.g. python:3.11, node:20-alpine", - description_placeholder: DESCRIPTION_PLACEHOLDER, - setup_commands_placeholder: "e.g. cd my-repo && pip install -r requirements.txt", - setup_commands_helper: "Setup commands run independently. Each command runs from the workspace root (/workspace). If a command depends on the previous one, combine them with &&.", + name_placeholder: Box::leak( + i18n::t("settings.environments.name.placeholder").into_boxed_str(), + ), + repos_placeholder_authed: Box::leak( + i18n::t("settings.environments.repos.placeholder_authed").into_boxed_str(), + ), + repos_placeholder_unauthed: Box::leak( + i18n::t("settings.environments.repos.placeholder_unauthed").into_boxed_str(), + ), + docker_image_label: Box::leak( + i18n::t("settings.environments.docker_image.label").into_boxed_str(), + ), + docker_image_placeholder: Box::leak( + i18n::t("settings.environments.docker_image.placeholder").into_boxed_str(), + ), + description_placeholder: Box::leak( + i18n::t("settings.environments.description.placeholder").into_boxed_str(), + ), + setup_commands_placeholder: Box::leak( + i18n::t("settings.environments.setup_commands.placeholder").into_boxed_str(), + ), + setup_commands_helper: Box::leak( + i18n::t("settings.environments.setup_commands.helper").into_boxed_str(), + ), show_description_character_count: true, } } @@ -361,9 +393,6 @@ pub struct UpdateEnvironmentForm { } const DESCRIPTION_MAX_CHARS: usize = 240; -const DESCRIPTION_PLACEHOLDER: &str = "e.g., this environment is for all front end focused agents"; -const REPOS_PLACEHOLDER_AUTHED: &str = "Enter repos (owner/repo format)"; -const REPOS_PLACEHOLDER_UNAUTHED: &str = "Paste repo URL(s)"; const FORM_FIELD_SPACING: f32 = 20.; const FORM_LABEL_SPACING: f32 = 6.; const FORM_INPUT_HEIGHT: f32 = 36.; @@ -462,7 +491,7 @@ impl UpdateEnvironmentForm { // Create buttons let submit_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Create", PrimaryTheme) + ActionButton::new(i18n::t("settings.environments.button.create"), PrimaryTheme) .with_icon(Icon::Check) .on_click(|ctx| { ctx.dispatch_typed_action(UpdateEnvironmentFormAction::Submit); @@ -470,15 +499,22 @@ impl UpdateEnvironmentForm { }); let delete_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Delete environment", DangerSecondaryTheme) - .with_icon(Icon::Trash) - .on_click(|ctx| { - ctx.dispatch_typed_action(UpdateEnvironmentFormAction::Delete); - }) + ActionButton::new( + i18n::t("settings.environments.button.delete_environment"), + DangerSecondaryTheme, + ) + .with_icon(Icon::Trash) + .on_click(|ctx| { + ctx.dispatch_typed_action(UpdateEnvironmentFormAction::Delete); + }) }); let cancel_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Cancel", SecondaryTheme).on_click(|ctx| { + ActionButton::new( + i18n::t("settings.environments.button.cancel"), + SecondaryTheme, + ) + .on_click(|ctx| { ctx.dispatch_typed_action(UpdateEnvironmentFormAction::Cancel); }) }); @@ -831,10 +867,16 @@ impl UpdateEnvironmentForm { fn update_submit_button_label(&mut self, ctx: &mut ViewContext) { let button_text = match (&self.mode, self.show_header) { - (EnvironmentFormMode::Create, true) => "Create", - (EnvironmentFormMode::Create, false) => "Create environment", - (EnvironmentFormMode::Edit { .. }, true) => "Save", - (EnvironmentFormMode::Edit { .. }, false) => "Save environment", + (EnvironmentFormMode::Create, true) => i18n::t("settings.environments.button.create"), + (EnvironmentFormMode::Create, false) => { + i18n::t("settings.environments.button.create_environment") + } + (EnvironmentFormMode::Edit { .. }, true) => { + i18n::t("settings.environments.button.save") + } + (EnvironmentFormMode::Edit { .. }, false) => { + i18n::t("settings.environments.button.save_environment") + } }; self.submit_button.update(ctx, |button, ctx| { button.set_label(button_text, ctx); @@ -867,7 +909,7 @@ impl UpdateEnvironmentForm { self.remove_setup_command_mouse_states.clear(); // Update button text for Create mode self.submit_button.update(ctx, |button, ctx| { - button.set_label("Create", ctx); + button.set_label(i18n::t("settings.environments.button.create"), ctx); }); } EnvironmentFormInitArgs::Edit { @@ -908,7 +950,7 @@ impl UpdateEnvironmentForm { .collect(); // Update button text for Edit mode self.submit_button.update(ctx, |button, ctx| { - button.set_label("Save", ctx); + button.set_label(i18n::t("settings.environments.button.save"), ctx); }); } } @@ -1026,7 +1068,10 @@ impl UpdateEnvironmentForm { ..Default::default() }; let mut editor = EditorView::new(options, ctx); - editor.set_placeholder_text(DESCRIPTION_PLACEHOLDER, ctx); + editor.set_placeholder_text( + i18n::t("settings.environments.description.placeholder"), + ctx, + ); editor }) } @@ -1353,18 +1398,14 @@ impl UpdateEnvironmentForm { me.update_repos_input_placeholder(ctx); } Ok(UserGithubInfoResult::Unknown) => { - me.github_dropdown_state.load_error_message = Some( - "Couldn't load GitHub repos. You can paste repo URL(s), or retry." - .to_string(), - ); + me.github_dropdown_state.load_error_message = + Some(i18n::t("settings.environments.repos.load_error")); me.update_repos_input_placeholder(ctx); } Err(e) => { debug!("Failed to load GitHub repos: {e}"); - me.github_dropdown_state.load_error_message = Some( - "Couldn't load GitHub repos. You can paste repo URL(s), or retry." - .to_string(), - ); + me.github_dropdown_state.load_error_message = + Some(i18n::t("settings.environments.repos.load_error")); me.update_repos_input_placeholder(ctx); } } @@ -1554,7 +1595,8 @@ impl UpdateEnvironmentForm { }; } warp_graphql::queries::suggest_cloud_environment_image::SuggestCloudEnvironmentImageResult::UserFacingError(_) => { - let error_message = "Failed to suggest a Docker image".to_string(); + let error_message = + i18n::t("settings.environments.suggest_image.failed"); send_telemetry_from_ctx!( CloudAgentTelemetryEvent::ImageSuggestionFailed { error: error_message.clone(), @@ -1567,7 +1609,8 @@ impl UpdateEnvironmentForm { }; } warp_graphql::queries::suggest_cloud_environment_image::SuggestCloudEnvironmentImageResult::Unknown => { - let error_message = "Unknown response from suggestCloudEnvironmentImage".to_string(); + let error_message = + i18n::t("settings.environments.suggest_image.unknown_response"); send_telemetry_from_ctx!( CloudAgentTelemetryEvent::ImageSuggestionFailed { error: error_message.clone(), @@ -1581,7 +1624,10 @@ impl UpdateEnvironmentForm { } }, Err(e) => { - let error_message = format!("Failed to suggest a Docker image: {}", e); + let error_message = i18n::t( + "settings.environments.suggest_image.failed_with_error", + ) + .replace("{error}", &e.to_string()); send_telemetry_from_ctx!( CloudAgentTelemetryEvent::ImageSuggestionFailed { error: error_message.clone(), @@ -1649,9 +1695,13 @@ impl UpdateEnvironmentForm { theme.active_ui_text_color() }; - Text::new_inline("Share with team", font_family, font_size) - .with_color(color.into()) - .finish() + Text::new_inline( + i18n::t("settings.environments.share_with_team.label"), + font_family, + font_size, + ) + .with_color(color.into()) + .finish() }, ) .with_cursor(Cursor::PointingHand) @@ -1685,10 +1735,8 @@ impl UpdateEnvironmentForm { } Some(render_warning_box( - WarningBoxConfig::new( - "Personal environments cannot be used with external integrations or team API keys. For the best experience, use shared environments.", - ) - .with_width(self.field_max_width), + WarningBoxConfig::new(i18n::t("settings.environments.share_with_team.warning")) + .with_width(self.field_max_width), appearance, )) } @@ -1731,8 +1779,14 @@ impl UpdateEnvironmentForm { fn render_header(&self, appearance: &Appearance, app: &AppContext) -> Box { let (title, button_handle) = match &self.mode { - EnvironmentFormMode::Create => ("Create environment", &self.submit_button), - EnvironmentFormMode::Edit { .. } => ("Edit environment", &self.submit_button), + EnvironmentFormMode::Create => ( + i18n::t("settings.environments.title.create"), + &self.submit_button, + ), + EnvironmentFormMode::Edit { .. } => ( + i18n::t("settings.environments.title.edit"), + &self.submit_button, + ), }; let submit_actions = || self.render_submit_actions(appearance, app, button_handle); @@ -1741,14 +1795,14 @@ impl UpdateEnvironmentForm { .with_main_axis_size(MainAxisSize::Max) .with_main_axis_alignment(MainAxisAlignment::SpaceBetween) .with_cross_axis_alignment(CrossAxisAlignment::Center) - .with_child(self.render_back_button_and_title(title, appearance)) + .with_child(self.render_back_button_and_title(&title, appearance)) .with_child(submit_actions()) .finish(); let compact_header = Flex::column() .with_cross_axis_alignment(CrossAxisAlignment::Start) .with_spacing(8.) - .with_child(self.render_back_button_and_title(title, appearance)) + .with_child(self.render_back_button_and_title(&title, appearance)) .with_child(submit_actions()) .finish(); @@ -1858,7 +1912,7 @@ impl UpdateEnvironmentForm { .with_spacing(FORM_LABEL_SPACING); field.add_child(Self::render_form_label( - "Setup command(s)", + Box::leak(i18n::t("settings.environments.setup_commands.label").into_boxed_str()), false, appearance, )); @@ -1929,7 +1983,7 @@ impl UpdateEnvironmentForm { field.add_child( Text::new( - "Description", + i18n::t("settings.environments.description.label"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -1961,7 +2015,10 @@ impl UpdateEnvironmentForm { .buffer_text(app) .chars() .count(); - let count_text = format!("{char_count} / {DESCRIPTION_MAX_CHARS} characters"); + let count_text = format!( + "{char_count} / {DESCRIPTION_MAX_CHARS} {}", + i18n::t("settings.environments.description.character_count_suffix") + ); field.add_child( Text::new( count_text, @@ -1992,7 +2049,7 @@ impl UpdateEnvironmentForm { fn render_repos_field_label(&self, appearance: &Appearance) -> Box { let theme = appearance.theme(); Text::new( - "Repo(s)", + i18n::t("settings.environments.repos.label"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -2022,7 +2079,7 @@ impl UpdateEnvironmentForm { .with_child( Container::new( Text::new( - "Loading...", + i18n::t("settings.environments.repos.loading"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -2111,7 +2168,7 @@ impl UpdateEnvironmentForm { ) .with_child( Text::new( - "Auth with GitHub", + i18n::t("settings.environments.repos.auth_with_github"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -2159,7 +2216,7 @@ impl UpdateEnvironmentForm { .github_dropdown_state .load_error_message .clone() - .unwrap_or_else(|| "Failed to load GitHub repositories".to_string()); + .unwrap_or_else(|| i18n::t("settings.environments.repos.load_failed_fallback")); let mut field = Flex::column() .with_cross_axis_alignment(CrossAxisAlignment::Stretch) @@ -2225,7 +2282,7 @@ impl UpdateEnvironmentForm { ) .with_child( Text::new( - "Retry", + i18n::t("settings.environments.repos.retry"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -2468,7 +2525,7 @@ impl UpdateEnvironmentForm { fn render_repo_helper_text_row(&self, appearance: &Appearance) -> Box { let theme = appearance.theme(); let helper = Text::new( - "Type owner/repo and press Enter to add, or select from dropdown.", + i18n::t("settings.environments.repos.helper"), appearance.ui_font_family(), appearance.ui_font_size() * 0.85, ) @@ -2494,7 +2551,7 @@ impl UpdateEnvironmentForm { // Plain text part text_row.add_child( Text::new( - "Missing a repo?", + i18n::t("settings.environments.repos.missing_a_repo"), appearance.ui_font_family(), appearance.ui_font_size() * 0.85, ) @@ -2512,7 +2569,7 @@ impl UpdateEnvironmentForm { theme.accent() }; Text::new( - "Configure access on GitHub", + i18n::t("settings.environments.repos.configure_access"), appearance.ui_font_family(), appearance.ui_font_size() * 0.85, ) @@ -2681,7 +2738,7 @@ impl UpdateEnvironmentForm { content.add_child( Container::new( Text::new( - "No repositories found", + i18n::t("settings.environments.repos.none_found"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -2966,7 +3023,10 @@ impl UpdateEnvironmentForm { let ui_builder = appearance.ui_builder().clone(); move || { ui_builder - .tool_tip(format!("Open image at {docker_hub_url}")) + .tool_tip(format!( + "{} {docker_hub_url}", + i18n::t("settings.environments.docker_image.open_image_at") + )) .build() .finish() } @@ -3090,12 +3150,12 @@ impl UpdateEnvironmentForm { let is_disabled = !self.can_suggest_image_for_current_repos(); let button_text = if is_loading { - "Generating…" + i18n::t("settings.environments.docker_image.suggest_generating") } else { - "Suggest image" + i18n::t("settings.environments.docker_image.suggest_image") }; - let tooltip_text = "Warp will suggest a Docker image based on your selected repositories."; + let tooltip_text = i18n::t("settings.environments.docker_image.suggest_tooltip"); let button = Hoverable::new( self.suggest_image_button_mouse_state.clone(), @@ -3123,7 +3183,7 @@ impl UpdateEnvironmentForm { .finish(); let text = Text::new( - button_text, + button_text.clone(), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -3157,7 +3217,7 @@ impl UpdateEnvironmentForm { let tooltip = ConstrainedBox::new( appearance .ui_builder() - .tool_tip(tooltip_text.to_string()) + .tool_tip(tooltip_text.clone()) .build() .finish(), ) @@ -3214,16 +3274,16 @@ impl UpdateEnvironmentForm { let auth_url_with_next = self.auth_url_with_next(auth_url); let action = UpdateEnvironmentFormAction::OpenUrl(auth_url_with_next); let button = WarningBoxButtonConfig::new( - "Authenticate", + i18n::t("settings.environments.suggest_image.authenticate"), self.suggest_image_auth_button_mouse_state.clone(), move |ctx| { ctx.dispatch_typed_action(action.clone()); }, ); Some(render_warning_box( - WarningBoxConfig::new( - "You need to grant access to your GitHub repos to suggest a Docker image", - ) + WarningBoxConfig::new(i18n::t( + "settings.environments.suggest_image.auth_required", + )) .with_width(self.field_max_width) .with_button(button), appearance, @@ -3248,7 +3308,7 @@ impl UpdateEnvironmentForm { ) -> Box { let action = UpdateEnvironmentFormAction::LaunchAgentForSelectedRepos; let button = WarningBoxButtonConfig::new( - "Launch agent", + i18n::t("settings.environments.suggest_image.launch_agent"), self.suggest_image_launch_agent_button_mouse_state.clone(), move |ctx| { ctx.dispatch_typed_action(action.clone()); @@ -3256,13 +3316,11 @@ impl UpdateEnvironmentForm { ); render_warning_box( - WarningBoxConfig::new( - "We couldn't find a good match. We recommend using a custom Docker image for these repos.", - ) - .with_description(reason) - .with_icon(Icon::AlertTriangle) - .with_width(self.field_max_width) - .with_button(button), + WarningBoxConfig::new(i18n::t("settings.environments.suggest_image.no_match")) + .with_description(reason) + .with_icon(Icon::AlertTriangle) + .with_width(self.field_max_width) + .with_button(button), appearance, ) } @@ -3522,7 +3580,7 @@ impl View for UpdateEnvironmentForm { // Form fields page.add_child(Self::render_form_field( - "Name", + Box::leak(i18n::t("settings.environments.name.label").into_boxed_str()), true, None, &self.name_editor, diff --git a/app/src/settings_view/warp_drive_page.rs b/app/src/settings_view/warp_drive_page.rs index aca8427291..63bbc8d4c0 100644 --- a/app/src/settings_view/warp_drive_page.rs +++ b/app/src/settings_view/warp_drive_page.rs @@ -39,7 +39,10 @@ pub fn init_actions_from_parent_view( ) { ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::custom( - SettingActionPairDescriptions::new("Enable Warp Drive", "Disable Warp Drive"), + SettingActionPairDescriptions::new( + i18n::t("settings.warp_drive.action.enable"), + i18n::t("settings.warp_drive.action.disable"), + ), builder(SettingsAction::WarpDrive( WarpDriveSettingsPageAction::ToggleShowWarpDrive, )), @@ -168,7 +171,7 @@ impl SettingsWidget for WarpDriveHeaderWidget { let message = Container::new( Text::new_inline( - "To use Warp Drive, please create an account.".to_string(), + i18n::t("settings.warp_drive.create_account_required"), appearance.ui_font_family(), 14., ) @@ -200,7 +203,7 @@ impl SettingsWidget for WarpDriveHeaderWidget { }), ..Default::default() }) - .with_text_label("Sign up".to_owned()) + .with_text_label(i18n::t("common.sign_up")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(WarpDriveSettingsPageAction::SignUp); @@ -247,7 +250,7 @@ impl SettingsWidget for WarpDriveToggleWidget { .is_anonymous_or_logged_out(); render_body_item::( - "Warp Drive".into(), + i18n::t("settings.nav.warp_drive"), Some(AdditionalInfo { mouse_state: self.info_icon_mouse_state.clone(), on_click_action: Some(WarpDriveSettingsPageAction::OpenUrl( @@ -271,13 +274,11 @@ impl SettingsWidget for WarpDriveToggleWidget { .build() .on_click(move |ctx, _, _| { if !is_anonymous_or_logged_out { - ctx.dispatch_typed_action( - WarpDriveSettingsPageAction::ToggleShowWarpDrive, - ); + ctx.dispatch_typed_action(WarpDriveSettingsPageAction::ToggleShowWarpDrive); } }) .finish(), - Some("Warp Drive is a workspace in your terminal where you can save Workflows, Notebooks, Prompts, and Environment Variables for personal use or to share with a team.".into()), + Some(i18n::t("settings.warp_drive.description")), ) } } diff --git a/app/src/settings_view/warpify_page.rs b/app/src/settings_view/warpify_page.rs index 6d1e109475..4738bc6687 100644 --- a/app/src/settings_view/warpify_page.rs +++ b/app/src/settings_view/warpify_page.rs @@ -50,7 +50,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform() { toggle_binding_pairs.push(ToggleSettingActionPair::new( - "SSH Warpification", + i18n::t("settings.warpify.action.ssh_warpification"), builder(SettingsAction::WarpifyPageToggle( WarpifyPageAction::ToggleSshWarpification, )), @@ -65,7 +65,7 @@ pub fn init_actions_from_parent_view( .is_value_explicitly_set() { toggle_binding_pairs.push(ToggleSettingActionPair::new( - "SSH session detection for Warpification", + i18n::t("settings.warpify.action.ssh_session_detection"), builder(SettingsAction::WarpifyPageToggle( WarpifyPageAction::ToggleTmuxWarpification, )), @@ -83,11 +83,6 @@ const ITEM_VERTICAL_SPACING: f32 = 24.; const BUILT_IN_TEXT_INPUT_MARGIN: f32 = 10.; const SPACE_AFTER_TEXT_INPUT: f32 = ITEM_VERTICAL_SPACING - BUILT_IN_TEXT_INPUT_MARGIN; -const SSH_TMUX_WARPIFICATION_DESCRIPTION: &str = "The tmux ssh wrapper works in many situations where the default one does not, but may require you to hit a button to warpify. Takes effect in new tabs."; - -const SSH_EXTENSION_INSTALL_MODE_DESCRIPTION: &str = - "Controls the installation behavior for Warp's SSH extension when a remote host doesn't have it installed."; - /// This page lets users configure when they get asked to warpify a session. Some shell commands /// are recognized by default. Users can add new shell commands, or prevent the default ones from /// asking. Users can also enable the SSH wrapper, and add hosts to a denylist. @@ -129,7 +124,7 @@ impl WarpifyPageView { let add_added_commands_editor = ctx.add_typed_action_view(|ctx| { let mut input = SubmittableTextInput::new(ctx).validate_on_edit(|regex| Regex::new(regex).is_ok()); - input.set_placeholder_text("command (supports regex)", ctx); + input.set_placeholder_text(i18n::t("settings.warpify.command_placeholder"), ctx); input }); @@ -140,7 +135,7 @@ impl WarpifyPageView { let add_denylisted_commands_editor = ctx.add_typed_action_view(|ctx| { let mut input = SubmittableTextInput::new(ctx); - input.set_placeholder_text("command (supports regex)", ctx); + input.set_placeholder_text(i18n::t("settings.warpify.command_placeholder"), ctx); input }); @@ -151,7 +146,7 @@ impl WarpifyPageView { let add_denylisted_ssh_editor = ctx.add_typed_action_view(|ctx| { let mut input = SubmittableTextInput::new(ctx); - input.set_placeholder_text("host (supports regex)", ctx); + input.set_placeholder_text(i18n::t("settings.warpify.host_placeholder"), ctx); input }); @@ -181,8 +176,13 @@ impl WarpifyPageView { fn build_page(ctx: &mut ViewContext) -> PageType { let mut categories = vec![ Category::new("", vec![Box::new(TitleWidget::default())]), - Category::new("Subshells", vec![Box::new(SubshellsWidget::default())]) - .with_subtitle("Subshells supported: bash, zsh, and fish."), + Category::new( + Box::leak(i18n::t("settings.warpify.subshells.title").into_boxed_str()), + vec![Box::new(SubshellsWidget::default())], + ) + .with_subtitle(Box::leak( + i18n::t("settings.warpify.subshells.subtitle").into_boxed_str(), + )), ]; let warpify_settings = WarpifySettings::as_ref(ctx); @@ -192,8 +192,9 @@ impl WarpifyPageView { .is_supported_on_current_platform() { categories.push( - Category::new("SSH", vec![Box::new(SSHWidget::default())]) - .with_subtitle("Warpify your interactive SSH sessions."), + Category::new("SSH", vec![Box::new(SSHWidget::default())]).with_subtitle( + Box::leak(i18n::t("settings.warpify.ssh.subtitle").into_boxed_str()), + ), ); } PageType::new_categorized(categories, None) @@ -341,7 +342,7 @@ impl WarpifyPageView { let items: Vec> = SshExtensionInstallMode::iter() .map(|mode| { DropdownItem::new( - mode.display_name(), + mode.localized_display_name(), WarpifyPageAction::SetSshExtensionInstallMode(mode), ) }) @@ -539,12 +540,9 @@ struct TitleWidget { impl TitleWidget { fn render_top_of_page(&self, appearance: &Appearance, _app: &AppContext) -> Box { let warpify_description = vec![ - FormattedTextFragment::plain_text( - "Configure whether Warp attempts to “Warpify” (add support for blocks, \ - input modes, etc) certain shells. ", - ), + FormattedTextFragment::plain_text(i18n::t("settings.warpify.description")), FormattedTextFragment::hyperlink( - "Learn more", + i18n::t("settings.warpify.learn_more"), "https://docs.warp.dev/terminal/warpify/subshells", ), ]; @@ -603,9 +601,10 @@ impl SubshellsWidget { let warpify_settings = WarpifySettings::as_ref(app); + let added_commands_title = i18n::t("settings.warpify.added_commands.title"); column.add_child( view.build_input_list( - "Added commands", + &added_commands_title, &warpify_settings.added_subshell_commands, &view.remove_added_command_button_states, WarpifyPageAction::RemoveAddedCommand, @@ -615,9 +614,10 @@ impl SubshellsWidget { .finish(), ); + let denylisted_commands_title = i18n::t("settings.warpify.denylisted_commands.title"); column.add_child( view.build_input_list( - "Denylisted commands", + &denylisted_commands_title, &warpify_settings.subshell_command_denylist, &view.remove_denylisted_command_button_states, WarpifyPageAction::RemoveDenylistedCommand, @@ -690,7 +690,7 @@ impl SettingsWidget for SSHWidget { &WarpifySettings::as_ref(app).enable_ssh_warpification, move || { render_body_item::( - "Warpify SSH Sessions".into(), + i18n::t("settings.warpify.ssh_sessions.label"), None, LocalOnlyIconState::for_setting( EnableSshWarpification::storage_key(), @@ -723,10 +723,13 @@ impl SettingsWidget for SSHWidget { &mut column, &WarpifySettings::as_ref(app).ssh_extension_install_mode, move || { + let install_ext_label = i18n::t("settings.warpify.install_ssh_extension.label"); + let install_ext_desc = + i18n::t("settings.warpify.install_ssh_extension.description"); Container::new(render_dropdown_item( appearance, - "Install SSH extension", - Some(SSH_EXTENSION_INSTALL_MODE_DESCRIPTION), + &install_ext_label, + Some(install_ext_desc.as_str()), None, LocalOnlyIconState::for_setting( SshExtensionInstallModeSetting::storage_key(), @@ -760,7 +763,7 @@ impl SettingsWidget for SSHWidget { let mut column = Flex::column(); column.add_child(render_body_item::( - "Use Tmux Warpification".into(), + i18n::t("settings.warpify.use_tmux.label"), Some(AdditionalInfo { mouse_state: self.additional_info_mouse_state.clone(), on_click_action: Some(WarpifyPageAction::OpenUrl( @@ -795,7 +798,7 @@ impl SettingsWidget for SSHWidget { column.add_child( ui_builder - .paragraph(SSH_TMUX_WARPIFICATION_DESCRIPTION.to_owned()) + .paragraph(i18n::t("settings.warpify.tmux_description")) .with_style(UiComponentStyles { font_color: Some(description_text_color.into_solid()), margin: Some( @@ -811,9 +814,10 @@ impl SettingsWidget for SSHWidget { if enable_ssh_warpification && should_prompt_ssh_tmux_wrapper { let warpify_settings = WarpifySettings::as_ref(app); + let denylisted_hosts_title = i18n::t("settings.warpify.denylisted_hosts.title"); column.add_child( view.build_input_list( - "Denylisted hosts", + &denylisted_hosts_title, &warpify_settings.ssh_hosts_denylist, &view.remove_denylisted_ssh_button_states, WarpifyPageAction::RemoveDenylistedSshHost, diff --git a/app/src/tab.rs b/app/src/tab.rs index edff3e17e9..9289e3fb6b 100644 --- a/app/src/tab.rs +++ b/app/src/tab.rs @@ -241,7 +241,7 @@ impl TabData { .is_active_sharer() { menu_items.push( - MenuItemFields::new("Stop sharing") + MenuItemFields::new(i18n::t("terminal.shared_session.stop_sharing")) .with_on_select_action(WorkspaceAction::StopSharingSessionFromTabMenu { terminal_view_id: focused_session_view.id(), }) @@ -249,7 +249,7 @@ impl TabData { ); } else { menu_items.push( - MenuItemFields::new("Share session") + MenuItemFields::new(i18n::t("terminal.shared_session.share_session")) .with_on_select_action(WorkspaceAction::OpenShareSessionModal(index)) .into_item(), ); @@ -259,7 +259,7 @@ impl TabData { // Always show an option to stop sharing all when there's at least 1 shared session in the tab. if !shared_session_view_ids.is_empty() { menu_items.push( - MenuItemFields::new("Stop sharing all") + MenuItemFields::new(i18n::t("terminal.shared_session.stop_sharing_all")) .with_on_select_action(WorkspaceAction::StopSharingAllSessionsInTab { pane_group: self.pane_group.downgrade(), }) @@ -284,7 +284,7 @@ impl TabData { if is_shared_or_viewed { menu_items.push( - MenuItemFields::new("Copy link") + MenuItemFields::new(i18n::t("common.copy_link")) .with_on_select_action(WorkspaceAction::CopySharedSessionLinkFromTab { tab_index: index, }) @@ -319,7 +319,11 @@ impl TabData { let mut menu_items = vec![]; let tab_title = Self::copyable_metadata_value(Some(pane_group.display_title(ctx))); if !uses_vertical_tabs(ctx) { - Self::push_copy_metadata_menu_item(&mut menu_items, "Copy tab title", tab_title); + Self::push_copy_metadata_menu_item( + &mut menu_items, + i18n::t("tab.menu.copy_tab_title"), + tab_title, + ); return menu_items; } @@ -339,7 +343,7 @@ impl TabData { }) .unwrap_or_else(|| pane_group.focused_pane_id(ctx)); ( - "Copy pane title", + i18n::t("tab.menu.copy_pane_title"), Self::copyable_pane_title(pane_group, pane_id, ctx), pane_group.terminal_view_from_pane_id(pane_id, ctx), ) @@ -350,20 +354,20 @@ impl TabData { pane_group.terminal_view_from_pane_id(target.locator.pane_id, ctx) }) .or_else(|| pane_group.focused_session_view(ctx)); - ("Copy tab title", tab_title, terminal_view) + (i18n::t("tab.menu.copy_tab_title"), tab_title, terminal_view) }; if let Some(terminal_view) = terminal_view { let terminal_view = terminal_view.as_ref(ctx); Self::push_copy_metadata_menu_item( &mut menu_items, - "Copy branch", + i18n::t("tab.menu.copy_branch"), Self::copyable_metadata_value(terminal_view.current_git_branch(ctx)), ); Self::push_copy_metadata_menu_item(&mut menu_items, title_label, title); Self::push_copy_metadata_menu_item( &mut menu_items, - "Copy working directory", + i18n::t("tab.menu.copy_working_directory"), Self::copyable_metadata_value( terminal_view .pwd() @@ -372,7 +376,7 @@ impl TabData { ); Self::push_copy_metadata_menu_item( &mut menu_items, - "Copy pull request link", + i18n::t("tab.menu.copy_pull_request_link"), Self::copyable_metadata_value(terminal_view.current_pull_request_url(ctx)), ); } else { @@ -384,7 +388,7 @@ impl TabData { fn push_copy_metadata_menu_item( menu_items: &mut Vec>, - label: &'static str, + label: String, value: Option, ) { if let Some(value) = value { @@ -412,7 +416,7 @@ impl TabData { // TODO add option to show the keybinding once we figure out a nice API to retrieve // the actual keybinding (based on the user's preferences etc.) - menu_items.append(&mut vec![MenuItemFields::new("Rename tab") + menu_items.append(&mut vec![MenuItemFields::new(i18n::t("tab.rename_tab")) .with_on_select_action(WorkspaceAction::RenameTab(index)) .into_item()]); // Group together with rename option (note, resetting doesn't make @@ -420,7 +424,7 @@ impl TabData { let title = self.pane_group.as_ref(ctx).custom_title(ctx); if title.is_some() { menu_items.push( - MenuItemFields::new("Reset tab name") + MenuItemFields::new(i18n::t("tab.reset_tab_name")) .with_on_select_action(WorkspaceAction::ResetTabName(index)) .into_item(), ); @@ -498,14 +502,14 @@ impl TabData { if ContextFlag::CloseWindow.is_enabled() || tabs_len != 1 { menu_items.push( - MenuItemFields::new("Close tab") + MenuItemFields::new(i18n::t("tab.close_tab")) .with_on_select_action(WorkspaceAction::CloseTab(index)) .into_item(), ); } if tabs_len > 1 { menu_items.push( - MenuItemFields::new("Close other tabs") + MenuItemFields::new(i18n::t("tab.close_other_tabs")) .with_on_select_action(WorkspaceAction::CloseOtherTabs(index)) .into_item(), ); @@ -529,7 +533,7 @@ impl TabData { if !FeatureFlag::TabConfigs.is_enabled() { return vec![]; } - vec![MenuItemFields::new("Save as new config") + vec![MenuItemFields::new(i18n::t("tab.save_as_new_config")) .with_on_select_action(WorkspaceAction::SaveCurrentTabAsNewConfig(index)) .into_item()] } @@ -540,8 +544,8 @@ impl TabData { return vec![]; } vec![ - MenuItemFields::new("New group with tab").into_item(), - MenuItemFields::new_submenu("Move to group").into_item(), + MenuItemFields::new(i18n::t("tab.new_group_with_tab")).into_item(), + MenuItemFields::new_submenu(i18n::t("tab.move_to_group")).into_item(), ] } @@ -1291,7 +1295,7 @@ impl<'a> TabComponent<'a> { if state.is_hovered() { let tooltip = ui_builder - .tool_tip("Cloud agent run".to_string()) + .tool_tip(i18n::t("tab.cloud_agent_run")) .build() .finish(); stack.add_positioned_overlay_child( diff --git a/app/src/tab_configs/action_sidecar.rs b/app/src/tab_configs/action_sidecar.rs index 63a81005f4..4a76d034b2 100644 --- a/app/src/tab_configs/action_sidecar.rs +++ b/app/src/tab_configs/action_sidecar.rs @@ -130,13 +130,13 @@ pub(crate) fn render_action_sidecar( appearance .ui_builder() .button(ButtonVariant::Outlined, mouse_states.make_default.clone()) - .with_centered_text_label("Make default".into()) + .with_centered_text_label(i18n::t("tab_configs.make_default")) .with_style(disabled_style) .with_tooltip({ let ui_builder = appearance.ui_builder().clone(); move || { ui_builder - .tool_tip("Already the default".into()) + .tool_tip(i18n::t("tab_configs.already_default")) .build() .finish() } @@ -149,7 +149,7 @@ pub(crate) fn render_action_sidecar( appearance .ui_builder() .button(ButtonVariant::Outlined, mouse_states.make_default.clone()) - .with_centered_text_label("Make default".into()) + .with_centered_text_label(i18n::t("tab_configs.make_default")) .with_style(button_style) .build() .with_cursor(Cursor::PointingHand) @@ -171,7 +171,7 @@ pub(crate) fn render_action_sidecar( let edit_button = appearance .ui_builder() .button(ButtonVariant::Outlined, mouse_states.edit_config.clone()) - .with_centered_text_label("Edit config".into()) + .with_centered_text_label(i18n::t("tab_configs.edit_config")) .with_style(button_style) .build() .with_cursor(Cursor::PointingHand) @@ -202,7 +202,7 @@ pub(crate) fn render_action_sidecar( let remove_button = appearance .ui_builder() .button(ButtonVariant::Outlined, mouse_states.remove_config.clone()) - .with_centered_text_label("Remove".into()) + .with_centered_text_label(i18n::t("common.remove")) .with_style(remove_style) .with_hovered_styles(UiComponentStyles { border_color: Some(theme.accent().into()), diff --git a/app/src/tab_configs/branch_picker.rs b/app/src/tab_configs/branch_picker.rs index a022ff2d85..6bb8c51428 100644 --- a/app/src/tab_configs/branch_picker.rs +++ b/app/src/tab_configs/branch_picker.rs @@ -12,9 +12,6 @@ use crate::util::git::{ use crate::view_components::{DropdownItem, FilterableDropdown}; const DEFAULT_DROPDOWN_WIDTH: f32 = 380.; -/// Placeholder text shown in the dropdown top bar while branches are loading. -const LOADING_PLACEHOLDER: &str = "Fetching branches\u{2026}"; - /// A filterable dropdown that lists local git branches for the given repo path. /// /// Created with an optional `cwd` — if `None`, the picker starts with the @@ -123,9 +120,10 @@ impl BranchPicker { dropdown.set_disabled(ctx); // Show loading text in the dropdown top bar so the modal // doesn't shift layout while the fetch is in-flight. - let placeholder = DropdownItem::new(LOADING_PLACEHOLDER.to_string(), String::new()); + let loading_placeholder = i18n::t("tab_configs.fetching_branches"); + let placeholder = DropdownItem::new(loading_placeholder.clone(), String::new()); dropdown.set_items(vec![placeholder], ctx); - dropdown.set_selected_by_name(LOADING_PLACEHOLDER, ctx); + dropdown.set_selected_by_name(&loading_placeholder, ctx); }); self.fetch_epoch += 1; diff --git a/app/src/tab_configs/new_worktree_modal.rs b/app/src/tab_configs/new_worktree_modal.rs index cdb939ea0a..b7730c09b2 100644 --- a/app/src/tab_configs/new_worktree_modal.rs +++ b/app/src/tab_configs/new_worktree_modal.rs @@ -67,8 +67,7 @@ const CLOSE_ICON_SIZE: f32 = 14.; /// Font size for inline validation error messages. const ERROR_FONT_SIZE: f32 = 12.; /// Error shown when the user-entered worktree branch name contains invalid characters. -const INVALID_BRANCH_NAME_ERROR: &str = - "Name can only contain letters, numbers, hyphens, and underscores"; +const INVALID_BRANCH_NAME_ERROR_KEY: &str = "tab_configs.new_worktree.invalid_branch_name"; /// Returns `true` if `name` is a valid worktree branch name. /// @@ -324,7 +323,7 @@ impl View for NewWorktreeModal { // ── Header (custom — Modal wrapper has no title) ──────────────── let header = { let title = Text::new_inline( - "New worktree".to_string(), + i18n::t("tab_configs.new_worktree.title"), appearance.ui_font_family(), HEADER_TITLE_FONT_SIZE, ) @@ -406,14 +405,20 @@ impl View for NewWorktreeModal { .with_cross_axis_alignment(CrossAxisAlignment::Stretch); // Repo picker - body.add_child(Self::render_section_label("Select repository", appearance)); + body.add_child(Self::render_section_label( + &i18n::t("tab_configs.new_worktree.select_repository"), + appearance, + )); body.add_child(ChildView::new(&self.repo_picker).finish()); // Branch picker (with gap) body.add_child( - Container::new(Self::render_section_label("Select branch", appearance)) - .with_margin_top(SECTION_GAP) - .finish(), + Container::new(Self::render_section_label( + &i18n::t("tab_configs.new_worktree.select_branch"), + appearance, + )) + .with_margin_top(SECTION_GAP) + .finish(), ); body.add_child(ChildView::new(&self.branch_picker).finish()); @@ -463,7 +468,7 @@ impl View for NewWorktreeModal { .with_child(checkbox_element) .with_child( Text::new_inline( - "Autogenerate worktree branch name".to_string(), + i18n::t("tab_configs.new_worktree.autogenerate_branch_name"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -482,7 +487,7 @@ impl View for NewWorktreeModal { if !self.autogenerate_branch_name { body.add_child( Container::new(Self::render_section_label( - "Worktree branch name", + &i18n::t("tab_configs.new_worktree.branch_name"), appearance, )) .with_margin_top(SECTION_GAP) @@ -494,7 +499,7 @@ impl View for NewWorktreeModal { body.add_child( Container::new( Text::new_inline( - INVALID_BRANCH_NAME_ERROR.to_string(), + i18n::t(INVALID_BRANCH_NAME_ERROR_KEY), appearance.ui_font_family(), ERROR_FONT_SIZE, ) @@ -539,7 +544,7 @@ impl View for NewWorktreeModal { let cancel_button = appearance .ui_builder() .button(ButtonVariant::Text, self.cancel_button_mouse_state.clone()) - .with_text_label("Cancel".to_string()) + .with_text_label(i18n::t("common.cancel")) .with_style(text_button_base) .with_style(UiComponentStyles { font_color: Some(main_text.into()), @@ -561,7 +566,7 @@ impl View for NewWorktreeModal { let mut builder = appearance .ui_builder() .button(ButtonVariant::Text, self.open_button_mouse_state.clone()) - .with_text_label("Open".to_string()) + .with_text_label(i18n::t("common.open")) .with_style(text_button_base) .with_style(UiComponentStyles { font_color: Some(font_color.into()), diff --git a/app/src/tab_configs/params_modal.rs b/app/src/tab_configs/params_modal.rs index 197ad30da6..91f44f5edc 100644 --- a/app/src/tab_configs/params_modal.rs +++ b/app/src/tab_configs/params_modal.rs @@ -148,12 +148,12 @@ pub enum TabConfigParamsModalAction { impl TabConfigParamsModal { pub fn new(ctx: &mut ViewContext) -> Self { let cancel_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Cancel", NakedTheme).on_click(|ctx| { + ActionButton::new(i18n::t("common.cancel"), NakedTheme).on_click(|ctx| { ctx.dispatch_typed_action(TabConfigParamsModalAction::Cancel); }) }); let submit_button = ctx.add_typed_action_view(|ctx| { - ActionButton::new("Open Tab", PrimaryTheme) + ActionButton::new(i18n::t("tab_configs.open_tab"), PrimaryTheme) .with_keybinding( KeystrokeSource::Fixed(Keystroke::parse("enter").unwrap_or_default()), ctx, @@ -162,8 +162,9 @@ impl TabConfigParamsModal { ctx.dispatch_typed_action(TabConfigParamsModalAction::Submit); }) }); - let submit_button_disabled = - ctx.add_typed_action_view(|_| ActionButton::new("Open Tab", DisabledTheme)); + let submit_button_disabled = ctx.add_typed_action_view(|_| { + ActionButton::new(i18n::t("tab_configs.open_tab"), DisabledTheme) + }); Self { param_fields: Vec::new(), pending_config: None, @@ -281,7 +282,7 @@ impl TabConfigParamsModal { TabConfigParamType::Text => { let default_text = param.default.clone().unwrap_or_default(); let placeholder = if default_text.is_empty() { - format!("Enter {name}") + i18n::t("tab_configs.params.enter").replace("{name}", name) } else { default_text.clone() }; @@ -595,7 +596,8 @@ impl View for TabConfigParamsModal { form.add_child( Container::new( Text::new_inline( - format!("Default: {default_value}"), + i18n::t("tab_configs.params.default_value") + .replace("{default_value}", default_value), appearance.ui_font_family(), appearance.ui_font_size() - 1., ) diff --git a/app/src/tab_configs/remove_confirmation_dialog.rs b/app/src/tab_configs/remove_confirmation_dialog.rs index f262cdb670..1c3e49abd3 100644 --- a/app/src/tab_configs/remove_confirmation_dialog.rs +++ b/app/src/tab_configs/remove_confirmation_dialog.rs @@ -59,14 +59,14 @@ pub(crate) struct RemoveTabConfigConfirmationDialog { impl RemoveTabConfigConfirmationDialog { pub fn new(ctx: &mut ViewContext) -> Self { let cancel_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Cancel", NakedTheme).on_click(|ctx| { + ActionButton::new(i18n::t("common.cancel"), NakedTheme).on_click(|ctx| { ctx.dispatch_typed_action(RemoveTabConfigConfirmationAction::Cancel); }) }); let enter_keystroke = Keystroke::parse("enter").expect("Valid keystroke"); let confirm_button = ctx.add_typed_action_view(|ctx| { - ActionButton::new("Remove", DangerPrimaryTheme) + ActionButton::new(i18n::t("common.remove"), DangerPrimaryTheme) .with_keybinding(KeystrokeSource::Fixed(enter_keystroke), ctx) .on_click(|ctx| { ctx.dispatch_typed_action(RemoveTabConfigConfirmationAction::Confirm); @@ -107,13 +107,11 @@ impl View for RemoveTabConfigConfirmationDialog { .with_margin_right(12.) .finish(); - let title = format!("Remove '{}'?", self.config_name); + let title = i18n::t("tab_configs.remove.title").replace("{name}", &self.config_name); let dialog = Dialog::new( title, - Some( - "This tab config will be permanently deleted. This action cannot be undone.".into(), - ), + Some(i18n::t("tab_configs.remove.description")), UiComponentStyles { width: Some(DIALOG_WIDTH), ..dialog_styles(appearance) diff --git a/app/src/tab_configs/repo_picker.rs b/app/src/tab_configs/repo_picker.rs index 0a98ff7723..002d5b5db6 100644 --- a/app/src/tab_configs/repo_picker.rs +++ b/app/src/tab_configs/repo_picker.rs @@ -16,9 +16,6 @@ use crate::view_components::{DropdownItem, FilterableDropdown}; const DEFAULT_DROPDOWN_WIDTH: f32 = 380.; -/// Label for the sticky "Add new repo..." footer at the bottom of the picker. -const ADD_NEW_REPO_LABEL: &str = "+ Add new repo..."; - /// A filterable dropdown listing known repos (from `PersistedWorkspace`), with a /// sticky "+ Add new repo..." footer that is always visible even when scrolling. /// @@ -112,9 +109,13 @@ impl RepoPicker { let mouse_state_clone = mouse_state.clone(); Hoverable::new(mouse_state_clone, move |_| { Container::new( - Text::new_inline(ADD_NEW_REPO_LABEL, font_family, font_size) - .with_color(text_color.into()) - .finish(), + Text::new_inline( + i18n::t("tab_configs.add_new_repo"), + font_family, + font_size, + ) + .with_color(text_color.into()) + .finish(), ) .with_horizontal_padding(8.) .with_vertical_padding(6.) diff --git a/app/src/tab_configs/session_config.rs b/app/src/tab_configs/session_config.rs index 5413cd3683..9e8ca433ce 100644 --- a/app/src/tab_configs/session_config.rs +++ b/app/src/tab_configs/session_config.rs @@ -45,14 +45,14 @@ impl SessionType { } /// Short label for the session type pill in the modal. - pub(crate) fn pill_label(&self) -> &'static str { + pub(crate) fn pill_label(&self) -> String { match self { - SessionType::Terminal => "Terminal", - SessionType::Oz => "Built in agent", - SessionType::CliAgent(CLIAgent::Claude) => "Claude", - SessionType::CliAgent(CLIAgent::Codex) => "Codex", - SessionType::CliAgent(CLIAgent::Gemini) => "Gemini", - SessionType::CliAgent(agent) => agent.display_name(), + SessionType::Terminal => i18n::t("tab_configs.session_type.terminal"), + SessionType::Oz => i18n::t("tab_configs.session_type.built_in_agent"), + SessionType::CliAgent(CLIAgent::Claude) => "Claude".to_string(), + SessionType::CliAgent(CLIAgent::Codex) => "Codex".to_string(), + SessionType::CliAgent(CLIAgent::Gemini) => "Gemini".to_string(), + SessionType::CliAgent(agent) => agent.display_name().to_string(), } } } diff --git a/app/src/tab_configs/session_config_modal.rs b/app/src/tab_configs/session_config_modal.rs index 6f929afc29..cec2035faa 100644 --- a/app/src/tab_configs/session_config_modal.rs +++ b/app/src/tab_configs/session_config_modal.rs @@ -86,7 +86,7 @@ impl SessionConfigModal { }); let submit_button = ctx.add_view(|ctx| { - ActionButton::new("Get Warping", PrimaryTheme) + ActionButton::new(i18n::t("tab_configs.get_warping"), PrimaryTheme) .with_full_width(true) .with_keybinding( KeystrokeSource::Fixed(Keystroke::parse("enter").unwrap_or_default()), @@ -164,7 +164,7 @@ impl SessionConfigModal { let theme = appearance.theme(); let title = FormattedTextElement::from_str( - "Create your first tab config", + i18n::t("tab_configs.create_first_config"), appearance.ui_font_family(), 24., ) @@ -173,13 +173,9 @@ impl SessionConfigModal { .finish(); let subtitle_text = if self.show_session_type_row { - "Set up a reusable starting point for your tabs. \ - Pick a repo, choose a session type, and optionally attach a worktree. \ - Use it whenever you want to open a new tab with this setup." + i18n::t("tab_configs.create_first_config.subtitle_with_session_type") } else { - "Set up a reusable starting point for your tabs. \ - Pick a repo, optionally attach a worktree, and \ - use it whenever you want to open a new tab with this setup." + i18n::t("tab_configs.create_first_config.subtitle_without_session_type") }; let subtitle = FormattedTextElement::from_str(subtitle_text, appearance.ui_font_family(), 14.) diff --git a/app/src/tab_configs/session_config_rendering.rs b/app/src/tab_configs/session_config_rendering.rs index c894fa26f0..a87dc6f27a 100644 --- a/app/src/tab_configs/session_config_rendering.rs +++ b/app/src/tab_configs/session_config_rendering.rs @@ -87,13 +87,17 @@ where let on_accent_bg = bg.is_some(); let on_select = Arc::new(on_select); - let label = Text::new_inline("Session type".to_string(), appearance.ui_font_family(), 12.) - .with_color(if on_accent_bg { - callout_label_color(appearance) - } else { - blended_colors::text_disabled(theme, bg_fill) - }) - .finish(); + let label = Text::new_inline( + i18n::t("tab_configs.session_type"), + appearance.ui_font_family(), + 12., + ) + .with_color(if on_accent_bg { + callout_label_color(appearance) + } else { + blended_colors::text_disabled(theme, bg_fill) + }) + .finish(); let mut pills_row = Flex::row().with_spacing(PILL_GAP); @@ -113,13 +117,9 @@ where .with_height(14.) .finish(); - let name = Text::new_inline( - session_type.pill_label().to_string(), - appearance.ui_font_family(), - 14., - ) - .with_color(item_color) - .finish(); + let name = Text::new_inline(session_type.pill_label(), appearance.ui_font_family(), 14.) + .with_color(item_color) + .finish(); let pill_content = Flex::row() .with_main_axis_size(MainAxisSize::Max) @@ -224,7 +224,7 @@ where let on_accent_bg = bg.is_some(); let label = Text::new_inline( - "Select directory".to_string(), + i18n::t("tab_configs.select_directory"), appearance.ui_font_family(), 12., ) @@ -356,7 +356,7 @@ where if state.is_hovered() { let tooltip = Container::new( Text::new_inline( - "Select a git repository to enable worktree support".to_string(), + i18n::t("tab_configs.select_git_repo_for_worktree"), font_family, 12., ) @@ -400,7 +400,7 @@ where blended_colors::text_sub(theme, theme.background()) }; let label = Text::new( - "Automatically create a worktree when opening a new tab", + i18n::t("tab_configs.auto_create_worktree"), appearance.ui_font_family(), 12., ) @@ -487,9 +487,7 @@ where if state.is_hovered() { let tooltip = Container::new( Text::new_inline( - "You must select that you want to automatically create a \ - worktree in order to select this" - .to_string(), + i18n::t("tab_configs.auto_create_worktree_required"), font_family, 12., ) @@ -534,7 +532,7 @@ where }; let label = Text::new( - "Auto-generate worktree branch name", + i18n::t("tab_configs.auto_generate_worktree_branch_name"), appearance.ui_font_family(), 12., ) diff --git a/app/src/terminal/alt_screen_reporting.rs b/app/src/terminal/alt_screen_reporting.rs index d3d5718ee9..79830e557e 100644 --- a/app/src/terminal/alt_screen_reporting.rs +++ b/app/src/terminal/alt_screen_reporting.rs @@ -9,7 +9,7 @@ define_settings_group!(AltScreenReporting, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.mouse_reporting_enabled", - description: "Whether to forward mouse events to full-screen terminal applications.", + description_key: "settings.schema.terminal.mouse_reporting_enabled.description", }, scroll_reporting_enabled: ScrollReportingEnabled { type: bool, @@ -18,7 +18,7 @@ define_settings_group!(AltScreenReporting, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.scroll_reporting_enabled", - description: "Whether to forward scroll events to full-screen terminal applications.", + description_key: "settings.schema.terminal.scroll_reporting_enabled.description", }, focus_reporting_enabled: FocusReportingEnabled { type: bool, @@ -27,6 +27,6 @@ define_settings_group!(AltScreenReporting, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.focus_reporting_enabled", - description: "Whether to forward focus and blur events to full-screen terminal applications.", + description_key: "settings.schema.terminal.focus_reporting_enabled.description", }, ]); diff --git a/app/src/terminal/available_shells.rs b/app/src/terminal/available_shells.rs index dccad50ae1..fc0e73af19 100644 --- a/app/src/terminal/available_shells.rs +++ b/app/src/terminal/available_shells.rs @@ -111,7 +111,7 @@ impl AvailableShell { pub fn short_name(&self) -> Cow<'_, str> { match self.state.as_ref() { - Config::SystemDefault => Cow::from("Default"), + Config::SystemDefault => Cow::from(i18n::t("common.default")), Config::KnownLocal(LocalConfig { command, .. }) | Config::MSYS2(LocalConfig { command, .. }) => match command.as_str() { "bash" => Cow::from("Bash"), @@ -122,25 +122,36 @@ impl AvailableShell { _ => Cow::from(command), }, Config::Wsl { distro } => Cow::from(distro), - Config::Custom(_) => Cow::from("Custom"), - Config::DockerSandbox { .. } => Cow::from("Docker Sandbox"), + Config::Custom(_) => Cow::from(i18n::t("terminal.available_shells.custom")), + Config::DockerSandbox { .. } => { + Cow::from(i18n::t("terminal.available_shells.docker_sandbox")) + } } } pub fn details(&self) -> Cow<'_, str> { match self.state.as_ref() { - Config::SystemDefault => Cow::from("System default shell"), + Config::SystemDefault => { + Cow::from(i18n::t("terminal.available_shells.system_default_shell")) + } Config::KnownLocal(LocalConfig { executable_path, .. }) | Config::MSYS2(LocalConfig { executable_path, .. }) => Cow::from(format!("{}", executable_path.display())), - Config::Wsl { .. } => Cow::from("Windows Subsystem for Linux"), + Config::Wsl { .. } => Cow::from(i18n::t( + "terminal.available_shells.windows_subsystem_for_linux", + )), Config::Custom(LocalConfig { executable_path, .. - }) => Cow::from(format!("Custom: {}", executable_path.display())), - Config::DockerSandbox { .. } => Cow::from("Docker Sandbox"), + }) => Cow::from( + i18n::t("terminal.available_shells.custom_with_path") + .replace("{path}", &executable_path.display().to_string()), + ), + Config::DockerSandbox { .. } => { + Cow::from(i18n::t("terminal.available_shells.docker_sandbox")) + } } } @@ -174,16 +185,19 @@ impl AvailableShell { /// the executable. fn long_name(&self) -> String { match &self.state.as_ref() { - Config::SystemDefault => "Default".to_string(), + Config::SystemDefault => i18n::t("common.default"), Config::KnownLocal(LocalConfig { executable_path, .. }) => format!("{} ({})", self.short_name(), executable_path.display()), Config::Wsl { distro } => distro.to_string(), - Config::Custom(LocalConfig { command, .. }) => format!("Custom ({command})"), + Config::Custom(LocalConfig { command, .. }) => { + i18n::t("terminal.available_shells.custom_with_command") + .replace("{command}", command) + } Config::MSYS2(LocalConfig { executable_path, .. }) => format!("{} ({})", self.short_name(), executable_path.display()), - Config::DockerSandbox { .. } => "Docker Sandbox".to_string(), + Config::DockerSandbox { .. } => i18n::t("terminal.available_shells.docker_sandbox"), } } diff --git a/app/src/terminal/block_filter.rs b/app/src/terminal/block_filter.rs index 14607b90f5..146f4fb277 100644 --- a/app/src/terminal/block_filter.rs +++ b/app/src/terminal/block_filter.rs @@ -29,8 +29,6 @@ use crate::themes::theme::Fill; use crate::ui_components::blended_colors; use crate::ui_components::icons::Icon; -const FILTER_BLOCK_PLACEHOLDER_TEXT: &str = "Filter block output"; - const BLOCK_FILTER_BAR_WIDTH: f32 = 380.; const BLOCK_FILTER_BAR_PADDING: f32 = 4.; const BLOCK_FILTER_EDITOR_PADDING: f32 = 6.; @@ -47,10 +45,6 @@ const MAXIMUM_CONTEXT_LINES: u16 = 99; const MAXIMUM_CONTEXT_LINE_EDITOR_BUFFER_LENGTH: usize = 2; pub type ContextLines = u16; pub const DEFAULT_CONTEXT_LINES_VALUE: ContextLines = 0; -const CONTEXT_LINE_EDITOR_TOOLTIP_LABEL: &str = "Show context lines around matches"; -const REGEX_TOOLTIP_LABEL: &str = "Regex toggle"; -const CASE_SENSITIVITY_TOOLTIP_LABEL: &str = "Case sensitive search"; -const INVERT_FILTER_TOOLTIP_LABEL: &str = "Invert filter"; pub const BLOCK_FILTER_DOTTED_LINE_DASH: Dash = Dash { dash_length: 4., @@ -186,7 +180,7 @@ impl BlockFilterEditor { }, ctx, ); - editor.set_placeholder_text(FILTER_BLOCK_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text(i18n::t("terminal.block_filter.placeholder"), ctx); editor }); @@ -456,9 +450,9 @@ impl BlockFilterEditor { mouse_state_handle: MouseStateHandle, on_click_action: BlockFilterEditorAction, size: f32, - tooltip_text: Option<&str>, + tooltip_text: Option, ) -> Box { - Hoverable::new(mouse_state_handle, |state| { + Hoverable::new(mouse_state_handle, move |state| { let (border, background) = if is_selected { ( Border::all(1.).with_border_fill(appearance.theme().accent()), @@ -490,10 +484,10 @@ impl BlockFilterEditor { .finish(); let mut stack = Stack::new().with_child(icon); - if let (Some(tooltip_text), true) = (tooltip_text, state.is_hovered()) { + if let (Some(tooltip_text), true) = (tooltip_text.as_ref(), state.is_hovered()) { let tooltip = appearance .ui_builder() - .tool_tip(tooltip_text.to_string()) + .tool_tip(tooltip_text.clone()) .build() .finish(); @@ -535,7 +529,7 @@ impl View for BlockFilterEditor { self.mouse_state_handles.regex_mouse_state_handle.clone(), BlockFilterEditorAction::ToggleRegex, editor_height, - Some(REGEX_TOOLTIP_LABEL), + Some(i18n::t("terminal.block_filter.regex_tooltip")), ); let case_sensitive_icon = self.render_hoverable_icon( appearance, @@ -546,7 +540,7 @@ impl View for BlockFilterEditor { .clone(), BlockFilterEditorAction::ToggleCaseSensitivity, editor_height, - Some(CASE_SENSITIVITY_TOOLTIP_LABEL), + Some(i18n::t("terminal.block_filter.case_sensitive_tooltip")), ); let invert_filter_icon = self.render_hoverable_icon( appearance, @@ -557,7 +551,7 @@ impl View for BlockFilterEditor { .clone(), BlockFilterEditorAction::ToggleInvertFilter, editor_height, - Some(INVERT_FILTER_TOOLTIP_LABEL), + Some(i18n::t("terminal.block_filter.invert_tooltip")), ); let query_editor = Shrinkable::new( @@ -657,7 +651,7 @@ impl View for BlockFilterEditor { if state.is_hovered() { let tool_tip = appearance .ui_builder() - .tool_tip(CONTEXT_LINE_EDITOR_TOOLTIP_LABEL.to_string()) + .tool_tip(i18n::t("terminal.block_filter.context_lines_tooltip")) .build() .finish(); stack.add_positioned_child( @@ -751,8 +745,8 @@ impl View for BlockFilterEditor { fn accessibility_contents(&self, _: &AppContext) -> Option { Some(AccessibilityContent::new( - "Type searched phrase.", - "Press escape to quit", + i18n::t("terminal.block_filter.accessibility_title"), + i18n::t("terminal.block_filter.accessibility_help"), WarpA11yRole::TextareaRole, )) } diff --git a/app/src/terminal/block_list_element.rs b/app/src/terminal/block_list_element.rs index 9ef98a3cc7..866b61ed4d 100644 --- a/app/src/terminal/block_list_element.rs +++ b/app/src/terminal/block_list_element.rs @@ -61,8 +61,8 @@ use super::view::{ use super::warpify::render::{draw_flag_pole, render_subshell_flag}; use super::{heights_approx_eq, TerminalModel, HEIGHT_FUDGE_FACTOR_LINES}; use crate::ai::blocklist::agent_view::{agent_view_bg_fill, AgentViewState}; -use crate::ai::blocklist::{ai_brand_color, ATTACH_AS_AGENT_MODE_CONTEXT_TEXT}; -use crate::ai_assistant::{AI_ASSISTANT_SVG_PATH, ASK_AI_ASSISTANT_TEXT}; +use crate::ai::blocklist::{ai_brand_color, attach_as_agent_mode_context_text}; +use crate::ai_assistant::AI_ASSISTANT_SVG_PATH; use crate::appearance::Appearance; use crate::drive::settings::WarpDriveSettings; use crate::features::FeatureFlag; @@ -149,11 +149,6 @@ const LINEAR_SCROLLING: ScrollingAcceleration = ScrollingAcceleration::Polynomia /// have a height that extends down to the bottom of the window when there's a horizontal scroll bar, which messes with the on-hover behavior. const BLOCK_HOVER_BUTTON_HEIGHT: f32 = 28.; -const TAG_AGENT_FOR_ASSISTANCE_TEXT: &str = "Tag agent for assistance"; - -const SAVE_AS_WORKFLOW_TEXT: &str = "Save as Workflow"; -const SAVE_AS_WORKFLOW_SECRETS_TEXT: &str = "Blocks containing secrets cannot be saved."; - enum ScrollingAcceleration { Polynomial(f32), } @@ -1160,23 +1155,23 @@ impl BlockListElement { if has_active_long_running_command && active_block.index() == block_index { ( Some(TerminalAction::SetInputModeAgent), - TAG_AGENT_FOR_ASSISTANCE_TEXT, + i18n::t("terminal.block.tag_agent_for_assistance"), ) } else { ( Some(TerminalAction::AskAIAssistant { block_index }), - *ATTACH_AS_AGENT_MODE_CONTEXT_TEXT, + attach_as_agent_mode_context_text(), ) } } else { ( Some(TerminalAction::AskAIAssistant { block_index }), - ASK_AI_ASSISTANT_TEXT, + i18n::t("ai_assistant.ask_warp_ai"), ) }; let tooltip = ToolbeltButtonTooltip { - label: ai_button_tooltip.to_owned(), + label: ai_button_tooltip, tool_tip_below_button: should_render_tooltip_below_button, }; @@ -1221,7 +1216,7 @@ impl BlockListElement { render_hoverable_block_button( icon, Some(ToolbeltButtonTooltip { - label: SAVE_AS_WORKFLOW_SECRETS_TEXT.to_owned(), + label: i18n::t("terminal.block_list.save_as_workflow_secrets"), tool_tip_below_button: should_render_tooltip_below_button, }), false, @@ -1241,7 +1236,7 @@ impl BlockListElement { render_hoverable_block_button( icon, Some(ToolbeltButtonTooltip { - label: SAVE_AS_WORKFLOW_TEXT.to_owned(), + label: i18n::t("terminal.block_list.save_as_workflow"), tool_tip_below_button: should_render_tooltip_below_button, }), false, @@ -3407,17 +3402,16 @@ impl Element for BlockListElement { // we want to show different text in the separator if this is an individual conversation // restored from the command palette let banner_intro_text = if is_historical_conversation_restoration { - "Conversation restored".to_string() + i18n::t("terminal.block_list.conversation_restored") } else { - "Previous session".to_string() + i18n::t("terminal.block_list.previous_session") }; let separator_text = if let Some(ts) = (*model).block_list().restored_session_ts() { - format!( - "{banner_intro_text} from {}", - ts.format("%a %b %-d at %-I:%M %p") - ) + i18n::t("terminal.block_list.restored_from") + .replace("{label}", &banner_intro_text) + .replace("{date}", &ts.format("%a %b %-d at %-I:%M %p").to_string()) } else { banner_intro_text }; diff --git a/app/src/terminal/block_list_settings.rs b/app/src/terminal/block_list_settings.rs index 6c5c0e334b..7ccf9da8ae 100644 --- a/app/src/terminal/block_list_settings.rs +++ b/app/src/terminal/block_list_settings.rs @@ -10,7 +10,7 @@ define_settings_group!(BlockListSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "appearance.blocks.show_jump_to_bottom_of_block_button", - description: "Whether to show the jump-to-bottom button in long command output.", + description_key: "settings.schema.appearance.blocks.show_jump_to_bottom_of_block_button.description", }, snackbar_enabled: SnackbarEnabled { type: bool, @@ -19,7 +19,7 @@ define_settings_group!(BlockListSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "general.snackbar_enabled", - description: "Whether to show snackbar notifications.", + description_key: "settings.schema.general.snackbar_enabled.description", } show_block_dividers: ShowBlockDividers { type: bool, @@ -28,6 +28,6 @@ define_settings_group!(BlockListSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "appearance.blocks.show_block_dividers", - description: "Whether to show dividers between terminal blocks.", + description_key: "settings.schema.appearance.blocks.show_block_dividers.description", } ]); diff --git a/app/src/terminal/buy_credits_banner.rs b/app/src/terminal/buy_credits_banner.rs index 19802edb45..e8b0f62655 100644 --- a/app/src/terminal/buy_credits_banner.rs +++ b/app/src/terminal/buy_credits_banner.rs @@ -217,7 +217,7 @@ impl BuyCreditsBanner { if self.banner_auto_reload_update_in_flight { self.banner_auto_reload_update_in_flight = false; ctx.emit(BuyCreditsBannerEvent::ShowAutoReloadError { - error_message: "Failed to enable auto-reload for your team. Please try again in Settings > Billing and Usage.", + error_message: i18n::t("terminal.buy_credits.auto_reload_error"), }); ctx.notify(); } @@ -250,9 +250,13 @@ impl BuyCreditsBanner { let sub_text_color = theme.sub_text_color(theme.surface_1()); - let label = Text::new_inline("Auto reload", appearance.ui_font_family(), 12.) - .with_color(sub_text_color.into()) - .finish(); + let label = Text::new_inline( + i18n::t("settings.billing.addon.auto_reload_label"), + appearance.ui_font_family(), + 12., + ) + .with_color(sub_text_color.into()) + .finish(); // Get the selected amount for the tooltip let selected_credits = self @@ -261,10 +265,8 @@ impl BuyCreditsBanner { .map(|option| option.credits) .unwrap_or(0); - let tooltip_text = format!( - "When enabled, auto reload will purchase {} credits when your credit balance gets low", - selected_credits - ); + let tooltip_text = i18n::t("terminal.buy_credits.auto_reload_tooltip") + .replace("{credits}", &selected_credits.to_string()); // Create info icon with a custom sub_text_color & mouse cursor (i.e. as opposed to using IconWithTooltip) let ui_builder = appearance.ui_builder(); @@ -417,16 +419,16 @@ impl BuyCreditsBanner { // Banner text with title and description based on admin status let banner_description = if has_admin_permissions { - "Your monthly spend limit has been reached. Increase it to continue." + i18n::t("terminal.buy_credits.monthly_limit.admin_description") } else { - "Contact a team admin to increase monthly limit." + i18n::t("terminal.buy_credits.monthly_limit.non_admin_description") }; let banner_text = Flex::column() .with_children([ appearance .ui_builder() - .paragraph("Monthly limit reached") + .paragraph(i18n::t("terminal.buy_credits.monthly_limit.title")) .with_style(UiComponentStyles { font_size: Some(14.), ..Default::default() @@ -477,7 +479,7 @@ impl BuyCreditsBanner { }), ..Default::default() }) - .with_text_label("Manage billing".to_string()) + .with_text_label(i18n::t("settings.account.manage_billing")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(Action::ManageBilling); @@ -562,7 +564,7 @@ impl BuyCreditsBanner { let make_banner_text = || { let mut banner_text_children = vec![appearance .ui_builder() - .paragraph("Out of credits") + .paragraph(i18n::t("terminal.buy_credits.out_of_credits.title")) .with_style(UiComponentStyles { font_size: Some(14.), ..Default::default() @@ -574,11 +576,16 @@ impl BuyCreditsBanner { if is_at_monthly_limit || would_purchase_exceed_limit { // Create formatted text with clickable hyperlink let warning_text_fragments = vec![ - FormattedTextFragment::plain_text( - "Purchasing these credits would take you over your monthly spend limit. ", + FormattedTextFragment::plain_text(i18n::t( + "terminal.buy_credits.purchase_exceeds_limit_prefix", + )), + FormattedTextFragment::hyperlink_action( + i18n::t("terminal.buy_credits.increase_it"), + Action::ManageBilling, ), - FormattedTextFragment::hyperlink_action("Increase it", Action::ManageBilling), - FormattedTextFragment::plain_text(" to continue."), + FormattedTextFragment::plain_text(i18n::t( + "terminal.buy_credits.purchase_exceeds_limit_suffix", + )), ]; let formatted_warning = FormattedTextElement::new( @@ -606,9 +613,9 @@ impl BuyCreditsBanner { } else { // Default message when not at limit let banner_description = if has_admin_permissions { - "Add more credits to your account to continue using Oz agents." + i18n::t("terminal.buy_credits.out_of_credits.admin_description") } else { - "Contact a team admin to purchase more credits to continue." + i18n::t("terminal.buy_credits.out_of_credits.non_admin_description") }; banner_text_children.push( @@ -647,9 +654,9 @@ impl BuyCreditsBanner { || would_purchase_exceed_limit; let button_text = if self.purchase_addon_credits_loading { - "Buying…".to_string() + i18n::t("settings.billing.addon.purchase_button_loading") } else { - "Buy".to_string() + i18n::t("terminal.buy_credits.buy") }; let button_font_color = buy_button_disabled.then_some( @@ -806,7 +813,7 @@ pub enum BuyCreditsBannerEvent { OpenBillingAndUsage, RefocusInput, OpenAutoReloadModal { purchased_credits: i32 }, - ShowAutoReloadError { error_message: &'static str }, + ShowAutoReloadError { error_message: String }, } impl Entity for BuyCreditsBanner { diff --git a/app/src/terminal/cli_agent_sessions/mod.rs b/app/src/terminal/cli_agent_sessions/mod.rs index b1d89e2da6..3dafae877e 100644 --- a/app/src/terminal/cli_agent_sessions/mod.rs +++ b/app/src/terminal/cli_agent_sessions/mod.rs @@ -198,13 +198,13 @@ impl CLIAgentSession { message: event.payload.summary.clone(), } } - CLIAgentEventType::QuestionAsked => CLIAgentSessionStatus::Blocked { - message: event - .payload - .summary - .clone() - .or_else(|| Some("Waiting for your answer".to_owned())), - }, + CLIAgentEventType::QuestionAsked => { + CLIAgentSessionStatus::Blocked { + message: event.payload.summary.clone().or_else(|| { + Some(i18n::t("terminal.cli_agent_sessions.waiting_for_answer")) + }), + } + } CLIAgentEventType::PermissionReplied => { if !matches!(self.status, CLIAgentSessionStatus::Blocked { .. }) { return None; diff --git a/app/src/terminal/cli_agent_sessions/plugin_manager/claude.rs b/app/src/terminal/cli_agent_sessions/plugin_manager/claude.rs index 0ca6d1a9c4..b734d203a6 100644 --- a/app/src/terminal/cli_agent_sessions/plugin_manager/claude.rs +++ b/app/src/terminal/cli_agent_sessions/plugin_manager/claude.rs @@ -138,19 +138,19 @@ impl CliAgentPluginManager for ClaudeCodePluginManager { if still_outdated { log.push_str("Post-update version check: plugin is still outdated\n"); return Err(PluginInstallError { - message: "Plugin update did not take effect".to_owned(), + message: i18n::t("terminal.plugin_instructions.claude.error.update_no_effect"), log, }); } Ok(()) } - fn install_success_message(&self) -> &'static str { - "Warp plugin installed. Please run /reload-plugins to activate." + fn install_success_message(&self) -> String { + i18n::t("terminal.plugin_instructions.claude.success.installed_reload") } - fn update_success_message(&self) -> &'static str { - "Warp plugin updated. Please run /reload-plugins to activate." + fn update_success_message(&self) -> String { + i18n::t("terminal.plugin_instructions.claude.success.updated_reload") } fn install_instructions(&self) -> &'static PluginInstructions { @@ -195,7 +195,9 @@ impl CliAgentPluginManager for ClaudeCodePluginManager { if still_outdated { log.push_str("Post-install version check: platform plugin is still outdated\n"); return Err(PluginInstallError { - message: "Platform plugin installation did not take effect".to_owned(), + message: i18n::t( + "terminal.plugin_instructions.claude.error.platform_install_no_effect", + ), log, }); } @@ -218,7 +220,9 @@ impl CliAgentPluginManager for ClaudeCodePluginManager { if still_outdated { log.push_str("Post-update version check: platform plugin is still outdated\n"); return Err(PluginInstallError { - message: "Platform plugin update did not take effect".to_owned(), + message: i18n::t( + "terminal.plugin_instructions.claude.error.platform_update_no_effect", + ), log, }); } @@ -226,56 +230,53 @@ impl CliAgentPluginManager for ClaudeCodePluginManager { } } -static INSTALL_INSTRUCTIONS: LazyLock = LazyLock::new(|| { - PluginInstructions { - title: "Install Warp Plugin for Claude Code", - subtitle: "Ensure that jq is installed on your machine. Then, run these commands.", - steps: &[ - PluginInstructionStep { - description: "Add the Warp plugin marketplace repository", - command: "claude plugin marketplace add warpdotdev/claude-code-warp", - executable: true, - link: None, - }, - PluginInstructionStep { - description: "Install the Warp plugin", - command: "claude plugin install warp@claude-code-warp", - executable: true, - link: None, - }, - ], - post_install_notes: &[ - "Restart Claude Code to activate the plugin.", - "There are some known issues with Claude Code's plugin system. \ - If the plugin is not found after step 1, you can try manually adding an \"extraKnownMarketplaces\" entry to ~/.claude/settings.json.", - ], - } +static INSTALL_INSTRUCTIONS: LazyLock = LazyLock::new(|| PluginInstructions { + title: "terminal.plugin_instructions.claude.install.title", + subtitle: "terminal.plugin_instructions.claude.install.subtitle", + steps: &[ + PluginInstructionStep { + description: "terminal.plugin_instructions.claude.install.step.add_marketplace", + command: "claude plugin marketplace add warpdotdev/claude-code-warp", + executable: true, + link: None, + }, + PluginInstructionStep { + description: "terminal.plugin_instructions.claude.install.step.install_plugin", + command: "claude plugin install warp@claude-code-warp", + executable: true, + link: None, + }, + ], + post_install_notes: &[ + "terminal.plugin_instructions.claude.install.note.restart", + "terminal.plugin_instructions.claude.install.note.known_issues", + ], }); static UPDATE_INSTRUCTIONS: LazyLock = LazyLock::new(|| PluginInstructions { - title: "Update Warp Plugin for Claude Code", - subtitle: "Run the following commands.", + title: "terminal.plugin_instructions.claude.update.title", + subtitle: "terminal.plugin_instructions.run_commands", steps: &[ PluginInstructionStep { - description: "Remove the existing marketplace (if present)", + description: "terminal.plugin_instructions.claude.update.step.remove_marketplace", command: "claude plugin marketplace remove claude-code-warp", executable: true, link: None, }, PluginInstructionStep { - description: "Re-add the marketplace", + description: "terminal.plugin_instructions.claude.update.step.readd_marketplace", command: "claude plugin marketplace add warpdotdev/claude-code-warp", executable: true, link: None, }, PluginInstructionStep { - description: "Install the latest plugin version", + description: "terminal.plugin_instructions.claude.update.step.install_latest", command: "claude plugin install warp@claude-code-warp", executable: true, link: None, }, ], - post_install_notes: &["Restart Claude Code to activate the update."], + post_install_notes: &["terminal.plugin_instructions.claude.update.note.restart"], }); fn check_installed(claude_dir: &Path) -> bool { @@ -375,7 +376,7 @@ fn claude_home_dir() -> io::Result { .ok_or_else(|| { io::Error::new( io::ErrorKind::NotFound, - "could not determine home directory", + i18n::t("terminal.plugin_instructions.error.home_directory_not_found"), ) }) } diff --git a/app/src/terminal/cli_agent_sessions/plugin_manager/codex.rs b/app/src/terminal/cli_agent_sessions/plugin_manager/codex.rs index 57ecd6312d..0f2153e767 100644 --- a/app/src/terminal/cli_agent_sessions/plugin_manager/codex.rs +++ b/app/src/terminal/cli_agent_sessions/plugin_manager/codex.rs @@ -29,26 +29,24 @@ impl CliAgentPluginManager for CodexPluginManager { } } -static INSTALL_INSTRUCTIONS: LazyLock = LazyLock::new(|| { - PluginInstructions { - title: "Enable Warp Notifications for Codex", - subtitle: "Update Codex to the latest version, then enable in-focus notifications so Warp can display them while you work.", +static INSTALL_INSTRUCTIONS: LazyLock = LazyLock::new(|| PluginInstructions { + title: "terminal.plugin_instructions.codex.install.title", + subtitle: "terminal.plugin_instructions.codex.install.subtitle", steps: &[ PluginInstructionStep { - description: "Update Codex to the latest version.", + description: "terminal.plugin_instructions.codex.install.step.update", command: "", executable: false, link: Some("https://developers.openai.com/codex/cli#upgrade"), }, PluginInstructionStep { - description: "Set the notification condition to \"always\" in your Codex config. Open or create ~/.codex/config.toml and add:", + description: "terminal.plugin_instructions.codex.install.step.config", command: "[tui]\nnotification_condition = \"always\"", executable: false, link: None, }, ], - post_install_notes: &["Restart Codex to apply the changes."], -} + post_install_notes: &["terminal.plugin_instructions.codex.install.note.restart"], }); static EMPTY_INSTRUCTIONS: LazyLock = LazyLock::new(|| PluginInstructions { diff --git a/app/src/terminal/cli_agent_sessions/plugin_manager/gemini.rs b/app/src/terminal/cli_agent_sessions/plugin_manager/gemini.rs index 90e8de02b2..92ee0c35ff 100644 --- a/app/src/terminal/cli_agent_sessions/plugin_manager/gemini.rs +++ b/app/src/terminal/cli_agent_sessions/plugin_manager/gemini.rs @@ -98,19 +98,19 @@ impl CliAgentPluginManager for GeminiPluginManager { if still_outdated { log.push_str("Post-update version check: plugin is still outdated\n"); return Err(PluginInstallError { - message: "Plugin update did not take effect".to_owned(), + message: i18n::t("terminal.plugin_instructions.gemini.error.update_no_effect"), log, }); } Ok(()) } - fn install_success_message(&self) -> &'static str { - "Warp plugin installed. Please restart Gemini CLI to activate." + fn install_success_message(&self) -> String { + i18n::t("terminal.plugin_instructions.gemini.success.installed_restart") } - fn update_success_message(&self) -> &'static str { - "Warp plugin updated. Please restart Gemini CLI to activate." + fn update_success_message(&self) -> String { + i18n::t("terminal.plugin_instructions.gemini.success.updated_restart") } fn install_instructions(&self) -> &'static PluginInstructions { @@ -123,28 +123,28 @@ impl CliAgentPluginManager for GeminiPluginManager { } static INSTALL_INSTRUCTIONS: LazyLock = LazyLock::new(|| PluginInstructions { - title: "Install Warp Plugin for Gemini CLI", - subtitle: "Run the following command, then restart Gemini CLI.", + title: "terminal.plugin_instructions.gemini.install.title", + subtitle: "terminal.plugin_instructions.gemini.install.subtitle", steps: &[PluginInstructionStep { - description: "Install the Warp extension", + description: "terminal.plugin_instructions.gemini.install.step.install_extension", command: "gemini extensions install https://github.com/warpdotdev/gemini-cli-warp --consent", executable: true, link: None, }], - post_install_notes: &["Restart Gemini CLI to activate the plugin."], + post_install_notes: &["terminal.plugin_instructions.gemini.install.note.restart"], }); static UPDATE_INSTRUCTIONS: LazyLock = LazyLock::new(|| PluginInstructions { - title: "Update Warp Plugin for Gemini CLI", - subtitle: "Run the following command, then restart Gemini CLI.", + title: "terminal.plugin_instructions.gemini.update.title", + subtitle: "terminal.plugin_instructions.gemini.install.subtitle", steps: &[PluginInstructionStep { - description: "Update the Warp extension", + description: "terminal.plugin_instructions.gemini.update.step.update_extension", command: "gemini extensions update gemini-warp", executable: true, link: None, }], - post_install_notes: &["Restart Gemini CLI to activate the update."], + post_install_notes: &["terminal.plugin_instructions.gemini.update.note.restart"], }); fn check_installed(extensions_dir: &Path) -> bool { @@ -174,7 +174,7 @@ fn gemini_extensions_dir() -> io::Result { .ok_or_else(|| { io::Error::new( io::ErrorKind::NotFound, - "could not determine home directory", + i18n::t("terminal.plugin_instructions.error.home_directory_not_found"), ) }) } diff --git a/app/src/terminal/cli_agent_sessions/plugin_manager/mod.rs b/app/src/terminal/cli_agent_sessions/plugin_manager/mod.rs index fe5974a723..55595733f2 100644 --- a/app/src/terminal/cli_agent_sessions/plugin_manager/mod.rs +++ b/app/src/terminal/cli_agent_sessions/plugin_manager/mod.rs @@ -120,14 +120,16 @@ pub(crate) async fn run_cli_command_logged( return Ok(()); } Err(PluginInstallError { - message: format!("'{display_cmd}' failed"), + message: i18n::t("terminal.plugin_instructions.error.command_failed") + .replace("{command}", &display_cmd), log: log.to_owned(), }) } Err(err) => { log.push_str(&format!("error: {err}\n")); Err(PluginInstallError { - message: format!("failed to run '{display_cmd}'"), + message: i18n::t("terminal.plugin_instructions.error.command_run_failed") + .replace("{command}", &display_cmd), log: log.clone(), }) } @@ -182,7 +184,7 @@ pub(crate) trait CliAgentPluginManager: Send + Sync { /// Default returns an error — only agents with `can_auto_install() == true` should override. async fn install(&self) -> Result<(), PluginInstallError> { Err(PluginInstallError { - message: "Auto-install not supported for this agent".to_owned(), + message: i18n::t("terminal.plugin_instructions.error.auto_install_not_supported"), log: String::new(), }) } @@ -191,19 +193,19 @@ pub(crate) trait CliAgentPluginManager: Send + Sync { /// Default returns an error — only agents with `can_auto_install() == true` should override. async fn update(&self) -> Result<(), PluginInstallError> { Err(PluginInstallError { - message: "Auto-update not supported for this agent".to_owned(), + message: i18n::t("terminal.plugin_instructions.error.auto_update_not_supported"), log: String::new(), }) } /// Toast message shown after a successful auto-install. - fn install_success_message(&self) -> &'static str { - "Warp plugin installed. Please restart the session to activate." + fn install_success_message(&self) -> String { + i18n::t("terminal.plugin_instructions.success.installed_restart_session") } /// Toast message shown after a successful auto-update. - fn update_success_message(&self) -> &'static str { - "Warp plugin updated. Please restart the session to activate." + fn update_success_message(&self) -> String { + i18n::t("terminal.plugin_instructions.success.updated_restart_session") } /// Manual installation instructions for the modal UI. diff --git a/app/src/terminal/cli_agent_sessions/plugin_manager/opencode.rs b/app/src/terminal/cli_agent_sessions/plugin_manager/opencode.rs index 9e1a15de00..158b5c4392 100644 --- a/app/src/terminal/cli_agent_sessions/plugin_manager/opencode.rs +++ b/app/src/terminal/cli_agent_sessions/plugin_manager/opencode.rs @@ -31,49 +31,44 @@ impl CliAgentPluginManager for OpenCodePluginManager { } } -static INSTALL_INSTRUCTIONS: LazyLock = LazyLock::new(|| { - PluginInstructions { - title: "Install Warp Plugin for OpenCode", - subtitle: - "Add the Warp plugin to your OpenCode configuration, then restart OpenCode.", - steps: &[ - PluginInstructionStep { - description: "Open or create your opencode.json. This can be in your project root, or the global config path:", - command: "~/.config/opencode/opencode.json", - executable: false, - link: None, - }, - PluginInstructionStep { - description: "Add \"@warp-dot-dev/opencode-warp\" to the \"plugin\" array in the top-level JSON object:", - command: "\"plugin\": [\"@warp-dot-dev/opencode-warp\"]", - executable: false, - link: None, - }, - ], - post_install_notes: &["Restart OpenCode to activate the plugin."], - } +static INSTALL_INSTRUCTIONS: LazyLock = LazyLock::new(|| PluginInstructions { + title: "terminal.plugin_instructions.opencode.install.title", + subtitle: "terminal.plugin_instructions.opencode.install.subtitle", + steps: &[ + PluginInstructionStep { + description: "terminal.plugin_instructions.opencode.step.open_config", + command: "~/.config/opencode/opencode.json", + executable: false, + link: None, + }, + PluginInstructionStep { + description: "terminal.plugin_instructions.opencode.install.step.add_plugin", + command: "\"plugin\": [\"@warp-dot-dev/opencode-warp\"]", + executable: false, + link: None, + }, + ], + post_install_notes: &["terminal.plugin_instructions.opencode.install.note.restart"], }); -static UPDATE_INSTRUCTIONS: LazyLock = LazyLock::new(|| { - PluginInstructions { - title: "Update Warp Plugin for OpenCode", - subtitle: "Pin the plugin to the latest version in your opencode.json. OpenCode caches plugins per version spec, so changing the pin forces it to re-fetch on restart.", - steps: &[ - PluginInstructionStep { - description: "Open or create your opencode.json. This can be in your project root, or the global config path:", - command: "~/.config/opencode/opencode.json", - executable: false, - link: None, - }, - PluginInstructionStep { - description: "Replace the existing \"@warp-dot-dev/opencode-warp\" entry in the \"plugin\" array with the explicit version:", - command: "\"plugin\": [\"@warp-dot-dev/opencode-warp@0.1.5\"]", - executable: false, - link: None, - }, - ], - post_install_notes: &["Restart OpenCode to load the updated plugin."], - } +static UPDATE_INSTRUCTIONS: LazyLock = LazyLock::new(|| PluginInstructions { + title: "terminal.plugin_instructions.opencode.update.title", + subtitle: "terminal.plugin_instructions.opencode.update.subtitle", + steps: &[ + PluginInstructionStep { + description: "terminal.plugin_instructions.opencode.step.open_config", + command: "~/.config/opencode/opencode.json", + executable: false, + link: None, + }, + PluginInstructionStep { + description: "terminal.plugin_instructions.opencode.update.step.replace_plugin", + command: "\"plugin\": [\"@warp-dot-dev/opencode-warp@0.1.5\"]", + executable: false, + link: None, + }, + ], + post_install_notes: &["terminal.plugin_instructions.opencode.update.note.restart"], }); #[cfg(test)] diff --git a/app/src/terminal/enable_auto_reload_modal.rs b/app/src/terminal/enable_auto_reload_modal.rs index 848f167a02..c3239c73d2 100644 --- a/app/src/terminal/enable_auto_reload_modal.rs +++ b/app/src/terminal/enable_auto_reload_modal.rs @@ -73,52 +73,49 @@ impl EnableAutoReloadModalBody { }, ); - ctx.subscribe_to_model( - &UserWorkspaces::handle(ctx), - |me, _handle, event, ctx| { - match event { - UserWorkspacesEvent::UpdateWorkspaceSettingsSuccess => { - if me.update_workspace_settings_loading { - me.update_workspace_settings_loading = false; - - // Emit telemetry for successful auto-reload enablement - let selected_credits = me - .addon_credits_options - .get(me.selected_denomination_index) - .map(|option| option.credits); - send_telemetry_from_ctx!( - TelemetryEvent::AutoReloadModalClosed { - action: AutoReloadModalAction::EnabledAutoReload, - selected_credits, - banner_toggle_flag_enabled: - FeatureFlag::BuildPlanAutoReloadBannerToggle.is_enabled(), - post_purchase_modal_flag_enabled: - FeatureFlag::BuildPlanAutoReloadPostPurchaseModal.is_enabled(), - }, - ctx - ); - - ctx.emit(EnableAutoReloadModalBodyEvent::ShowToast { - message: "Auto-reload settings updated".to_string(), - flavor: ToastFlavor::Success, - }); - ctx.emit(EnableAutoReloadModalBodyEvent::Close); - } + ctx.subscribe_to_model(&UserWorkspaces::handle(ctx), |me, _handle, event, ctx| { + match event { + UserWorkspacesEvent::UpdateWorkspaceSettingsSuccess => { + if me.update_workspace_settings_loading { + me.update_workspace_settings_loading = false; + + // Emit telemetry for successful auto-reload enablement + let selected_credits = me + .addon_credits_options + .get(me.selected_denomination_index) + .map(|option| option.credits); + send_telemetry_from_ctx!( + TelemetryEvent::AutoReloadModalClosed { + action: AutoReloadModalAction::EnabledAutoReload, + selected_credits, + banner_toggle_flag_enabled: + FeatureFlag::BuildPlanAutoReloadBannerToggle.is_enabled(), + post_purchase_modal_flag_enabled: + FeatureFlag::BuildPlanAutoReloadPostPurchaseModal.is_enabled(), + }, + ctx + ); + + ctx.emit(EnableAutoReloadModalBodyEvent::ShowToast { + message: i18n::t("terminal.auto_reload_modal.toast.updated"), + flavor: ToastFlavor::Success, + }); + ctx.emit(EnableAutoReloadModalBodyEvent::Close); } - UserWorkspacesEvent::UpdateWorkspaceSettingsRejected(_err) => { - if me.update_workspace_settings_loading { - me.update_workspace_settings_loading = false; - ctx.emit(EnableAutoReloadModalBodyEvent::ShowToast { - message: "Failed to enable auto-reload. Please try updating your settings in Billing & usage.".to_string(), - flavor: ToastFlavor::Error, - }); - ctx.notify(); - } + } + UserWorkspacesEvent::UpdateWorkspaceSettingsRejected(_err) => { + if me.update_workspace_settings_loading { + me.update_workspace_settings_loading = false; + ctx.emit(EnableAutoReloadModalBodyEvent::ShowToast { + message: i18n::t("terminal.auto_reload_modal.toast.enable_failed"), + flavor: ToastFlavor::Error, + }); + ctx.notify(); } - _ => {} } - }, - ); + _ => {} + } + }); let denomination_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = Dropdown::new(ctx); @@ -216,13 +213,13 @@ impl EnableAutoReloadModalBody { fn render_content(&self, appearance: &Appearance) -> Box { let theme = appearance.theme(); let explanation_fragments = vec![ - FormattedTextFragment::plain_text("When enabled, "), - FormattedTextFragment::bold("auto-reload"), + FormattedTextFragment::plain_text(i18n::t("terminal.auto_reload.when_enabled_prefix")), + FormattedTextFragment::bold(i18n::t("terminal.auto_reload.name")), FormattedTextFragment::plain_text( - " will automatically purchase your selected package when you run out. ", + i18n::t("terminal.auto_reload.purchase_suffix"), ), FormattedTextFragment::hyperlink( - "Learn more", + i18n::t("common.learn_more"), "https://docs.warp.dev/support-and-community/plans-and-billing/add-on-credits#id-2.-enable-auto-reload", ), ]; @@ -274,7 +271,7 @@ impl EnableAutoReloadModalBody { }), ..Default::default() }) - .with_text_label("Cancel".to_string()) + .with_text_label(i18n::t("common.cancel")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(Action::Cancel); @@ -282,9 +279,9 @@ impl EnableAutoReloadModalBody { .finish(); let button_text = if self.update_workspace_settings_loading { - "Saving...".to_string() + i18n::t("common.saving") } else { - "Enable".to_string() + i18n::t("common.enable") }; let mut enable_button = appearance @@ -391,8 +388,7 @@ impl warpui::TypedActionView for EnableAutoReloadModalBody { let workspaces = UserWorkspaces::as_ref(ctx); let Some(team_uid) = workspaces.current_team_uid() else { ctx.emit(EnableAutoReloadModalBodyEvent::ShowToast { - message: "Oops, something went wrong; your team's data could not be found." - .to_string(), + message: i18n::t("terminal.auto_reload_modal.toast.team_not_found"), flavor: ToastFlavor::Error, }); return; @@ -423,14 +419,17 @@ impl EnableAutoReloadModal { let body = ctx.add_typed_action_view(EnableAutoReloadModalBody::new); let modal = ctx.add_typed_action_view(|ctx| { - Modal::new(Some("Enable auto reload?".to_string()), body.clone(), ctx).with_body_style( - UiComponentStyles { - // Padding of 0 here since we add a horizontal bar that needs to span the full width in the body - // So we handle padding in the body itself - padding: Some(Coords::uniform(0.)), - ..Default::default() - }, + Modal::new( + Some(i18n::t("terminal.auto_reload_modal.title")), + body.clone(), + ctx, ) + .with_body_style(UiComponentStyles { + // Padding of 0 here since we add a horizontal bar that needs to span the full width in the body + // So we handle padding in the body itself + padding: Some(Coords::uniform(0.)), + ..Default::default() + }) }); ctx.subscribe_to_view(&modal, |_, _, event, ctx| match event { diff --git a/app/src/terminal/general_settings.rs b/app/src/terminal/general_settings.rs index bd47567b2b..0a38d8a1b0 100644 --- a/app/src/terminal/general_settings.rs +++ b/app/src/terminal/general_settings.rs @@ -14,7 +14,7 @@ define_settings_group!(GeneralSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "general.show_warning_before_quitting", - description: "Whether to show a warning dialog before quitting Warp.", + description_key: "settings.schema.general.show_warning_before_quitting.description", }, quit_on_last_window_closed: QuitOnLastWindowClosed { type: bool, @@ -23,7 +23,7 @@ define_settings_group!(GeneralSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "general.quit_on_last_window_closed", - description: "Whether to quit Warp when the last window is closed.", + description_key: "settings.schema.general.quit_on_last_window_closed.description", }, restore_session: RestoreSession { type: bool, @@ -32,7 +32,7 @@ define_settings_group!(GeneralSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "general.restore_session", - description: "Whether to restore the previous session when Warp starts up.", + description_key: "settings.schema.general.restore_session.description", }, add_app_as_login_item: LoginItem { type: bool, @@ -44,7 +44,7 @@ define_settings_group!(GeneralSettings, settings: [ sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "general.login_item", - description: "Whether to launch Warp automatically when you log in.", + description_key: "settings.schema.general.login_item.description", }, // Records whether the app has been added as a login item. // If it has, we don't try to add it again unless the user explicitly @@ -68,7 +68,7 @@ define_settings_group!(GeneralSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "general.link_tooltip", - description: "Whether to show a tooltip when hovering over links.", + description_key: "settings.schema.general.link_tooltip.description", }, welcome_tips_features_used: WelcomeTipsFeaturesUsed { type: HashSet, @@ -164,7 +164,7 @@ define_settings_group!(GeneralSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "code.editor.auto_open_code_review_pane_on_first_agent_change", - description: "Whether to automatically open the code review pane when the agent makes its first change.", + description_key: "settings.schema.code.editor.auto_open_code_review_pane_on_first_agent_change.description", }, bonus_grants_shown: BonusGrantsShown { type: HashSet, diff --git a/app/src/terminal/input.rs b/app/src/terminal/input.rs index 183be67d78..3aa2f982cf 100644 --- a/app/src/terminal/input.rs +++ b/app/src/terminal/input.rs @@ -372,9 +372,7 @@ pub const DEBOUNCE_AI_QUERY_PREDICTION_PERIOD: Duration = Duration::from_millis( pub(super) const CLI_AGENT_RICH_INPUT_EDITOR_MAX_HEIGHT: f32 = 236.; pub(super) const CLI_AGENT_RICH_INPUT_EDITOR_TOP_PADDING: f32 = 10.; pub(super) const CLI_AGENT_RICH_INPUT_EDITOR_BOTTOM_PADDING: f32 = 8.; -pub(super) const CLI_AGENT_RICH_INPUT_HINT_TEXT: &str = "Tell the agent what to build..."; -const CLOUD_MODE_V2_HINT_TEXT: &str = "Kick off a cloud agent"; const SHORT_CIRCUIT_HIGHLIGHTING_ACTIONS: [Option; 7] = [ Some(PlainTextEditorViewAction::Space), Some(PlainTextEditorViewAction::NonExpandingSpace), @@ -396,61 +394,49 @@ pub fn get_input_box_top_border_width() -> f32 { pub const COMPLETIONS_MENU_WIDTH: f32 = 330.; pub const OPEN_COMPLETIONS_KEYBINDING_NAME: &str = "input:open_completion_suggestions"; -pub const INPUT_A11Y_LABEL: &str = "Command Input."; -pub const INPUT_A11Y_HELPER: &str = "Input your shell command, press enter to execute. Press cmd-up to navigate to output of previously executed commands. Press cmd-l to re-focus command input."; -pub const AI_COMMAND_SEARCH_HINT_TEXT: &str = "Type '#' for AI command suggestions"; - -const AGENT_MODE_AI_DISABLED_AUTODETECTION_DISABLED_HINT_TEXT: &str = "Run commands"; // Rotating hint text options for new Agent Mode conversations -const AGENT_MODE_HINT_OPTIONS: &[&str] = &[ - "Warp anything e.g. Deploy my React app to Vercel and set up environment variables", - "Warp anything e.g. Help me debug why my Python tests are failing in CI", - "Warp anything e.g. Set up a new microservice with Docker and create the deployment pipeline", - "Warp anything e.g. Find and fix the memory leak in my Node.js application", - "Warp anything e.g. Create a backup script for my PostgreSQL database and schedule it", - "Warp anything e.g. Help me migrate my data from MySQL to PostgreSQL", - "Warp anything e.g. Set up monitoring and alerts for my AWS infrastructure", - "Warp anything e.g. Build a REST API for my mobile app using FastAPI", - "Warp anything e.g. Help me optimize my SQL queries that are running slowly", - "Warp anything e.g. Create a GitHub Actions workflow to automatically deploy on merge", - "Warp anything e.g. Set up Redis caching for my web application", - "Warp anything e.g. Help me troubleshoot why my Kubernetes pods keep crashing", - "Warp anything e.g. Build a data pipeline to process CSV files and load them into BigQuery", - "Warp anything e.g. Set up SSL certificates and configure HTTPS for my domain", - "Warp anything e.g. Help me refactor this legacy code to use modern design patterns", - "Warp anything e.g. Create unit tests for my authentication service", - "Warp anything e.g. Set up log aggregation with ELK stack for my distributed system", - "Warp anything e.g. Help me implement OAuth2 authentication in my Express.js app", - "Warp anything e.g. Optimize my Docker images to reduce build times and size", - "Warp anything e.g. Set up A/B testing infrastructure for my web application", +const AGENT_MODE_HINT_KEYS: &[&str] = &[ + "terminal.input.agent_hint.deploy_react", + "terminal.input.agent_hint.debug_python_ci", + "terminal.input.agent_hint.setup_microservice", + "terminal.input.agent_hint.fix_memory_leak", + "terminal.input.agent_hint.create_backup_script", + "terminal.input.agent_hint.migrate_database", + "terminal.input.agent_hint.setup_monitoring", + "terminal.input.agent_hint.build_rest_api", + "terminal.input.agent_hint.optimize_sql", + "terminal.input.agent_hint.github_actions_deploy", + "terminal.input.agent_hint.setup_redis", + "terminal.input.agent_hint.troubleshoot_kubernetes", + "terminal.input.agent_hint.build_data_pipeline", + "terminal.input.agent_hint.setup_ssl", + "terminal.input.agent_hint.refactor_legacy", + "terminal.input.agent_hint.create_unit_tests", + "terminal.input.agent_hint.setup_log_aggregation", + "terminal.input.agent_hint.implement_oauth", + "terminal.input.agent_hint.optimize_docker", + "terminal.input.agent_hint.setup_ab_testing", ]; -fn get_agent_mode_new_conversation_hint_text() -> &'static str { +fn get_agent_mode_new_conversation_hint_text() -> String { use std::sync::atomic::{AtomicUsize, Ordering}; static HINT_INDEX: AtomicUsize = AtomicUsize::new(0); - let index = HINT_INDEX.fetch_add(1, Ordering::Relaxed) % AGENT_MODE_HINT_OPTIONS.len(); - AGENT_MODE_HINT_OPTIONS[index] + let index = HINT_INDEX.fetch_add(1, Ordering::Relaxed) % AGENT_MODE_HINT_KEYS.len(); + i18n::t(AGENT_MODE_HINT_KEYS[index]) } -fn get_stable_agent_mode_hint_text(cached_hint: &mut Option<&'static str>) -> &'static str { +fn get_stable_agent_mode_hint_text(cached_hint: &mut Option) -> String { if let Some(hint) = cached_hint { - hint + hint.clone() } else { let new_hint = get_agent_mode_new_conversation_hint_text(); - *cached_hint = Some(new_hint); + *cached_hint = Some(new_hint.clone()); new_hint } } -const AGENT_MODE_AI_ENABLED_STEER_HINT_TEXT_UDI: &str = "Steer the running agent"; -const AGENT_MODE_AI_ENABLED_STEER_HINT_TEXT_CLASSIC: &str = - "Steer the running agent, or backspace to exit"; -const AGENT_MODE_AI_ENABLED_FOLLOW_UP_HINT_TEXT_UDI: &str = "Ask a follow up"; -const AGENT_MODE_AI_ENABLED_FOLLOW_UP_HINT_TEXT_CLASSIC: &str = - "Ask a follow up, or backspace to exit"; - /// Action name for setting input mode to agent mode pub const SET_INPUT_MODE_AGENT_ACTION_NAME: &str = "input:set_mode_agent"; @@ -492,11 +478,6 @@ enum InputPrefixMode { const VIM_STATUS_BAR_BOTTOM_PADDING: f32 = 20.; -const DYNAMIC_ENUM_GENERATE_MESSAGE: &str = "Run the following command to generate variants:"; -const DYNAMIC_ENUM_RUN_MESSAGE: &str = "Run command"; -const DYNAMIC_ENUM_PENDING_MESSAGE: &str = "Command pending..."; -const DYNAMIC_ENUM_FAILURE_MESSAGE: &str = "Command failed"; -const DYNAMIC_ENUM_NO_RESULTS_MESSAGE: &str = "Command returned no results"; const DYNAMIC_ENUM_MENU_PADDING: f32 = 10.; const DYNAMIC_ENUM_MENU_HEIGHT_OFFSET: f32 = 25.; const DYNAMIC_ENUM_HORIZONTAL_TEXT_PADDING: f32 = 5.; @@ -765,26 +746,42 @@ impl InputSuggestionsMode { } /// Returns the placeholder text for this mode, if it has a custom one. - pub fn placeholder_text(&self) -> Option<&'static str> { + pub fn placeholder_text(&self) -> Option { match self { InputSuggestionsMode::UserQueryMenu { action: UserQueryMenuAction::ForkFrom, .. - } => Some("Search queries"), + } => Some(i18n::t("terminal.input.placeholder.search_queries")), InputSuggestionsMode::UserQueryMenu { action: UserQueryMenuAction::Rewind, .. - } => Some("Search queries to rewind to"), - InputSuggestionsMode::ConversationMenu => Some("Search conversations"), - InputSuggestionsMode::SkillMenu => Some("Search skills"), - InputSuggestionsMode::ModelSelector => Some("Search models"), - InputSuggestionsMode::ProfileSelector => Some("Search profiles"), + } => Some(i18n::t( + "terminal.input.placeholder.search_queries_to_rewind", + )), + InputSuggestionsMode::ConversationMenu => { + Some(i18n::t("terminal.input.placeholder.search_conversations")) + } + InputSuggestionsMode::SkillMenu => { + Some(i18n::t("terminal.input.placeholder.search_skills")) + } + InputSuggestionsMode::ModelSelector => { + Some(i18n::t("terminal.input.placeholder.search_models")) + } + InputSuggestionsMode::ProfileSelector => { + Some(i18n::t("terminal.input.placeholder.search_profiles")) + } InputSuggestionsMode::SlashCommands if FeatureFlag::AgentView.is_enabled() => { - Some("Search commands") + Some(i18n::t("terminal.input.placeholder.search_commands")) + } + InputSuggestionsMode::PromptsMenu => { + Some(i18n::t("terminal.input.placeholder.search_prompts")) + } + InputSuggestionsMode::IndexedReposMenu => { + Some(i18n::t("terminal.input.placeholder.search_indexed_repos")) + } + InputSuggestionsMode::PlanMenu { .. } => { + Some(i18n::t("terminal.input.placeholder.search_plans")) } - InputSuggestionsMode::PromptsMenu => Some("Search prompts"), - InputSuggestionsMode::IndexedReposMenu => Some("Search indexed repos"), - InputSuggestionsMode::PlanMenu { .. } => Some("Search plans"), _ => None, } } @@ -1608,7 +1605,7 @@ pub struct Input { conn: Option>>, /// Cached hint text to ensure it remains stable during shell initialization hooks - cached_agent_mode_hint_text: Option<&'static str>, + cached_agent_mode_hint_text: Option, predict_am_queries_future_handle: Option, @@ -2188,7 +2185,8 @@ impl Input { event { let window_id = ctx.window_id(); - let toast_message = format!("Failed to prepare cloud handoff: {error_message}"); + let toast_message = i18n::t("terminal.input.cloud_handoff_prepare_failed") + .replace("{error}", error_message); ToastStack::handle(ctx).update(ctx, |ts, ctx| { ts.add_ephemeral_toast( DismissibleToast::error(toast_message), @@ -3202,9 +3200,9 @@ impl Input { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |ts, ctx| { ts.add_ephemeral_toast( - DismissibleToast::error( - "Attached images were removed — the selected model does not support images.".to_string(), - ), + DismissibleToast::error(i18n::t( + "terminal.input.attached_images_removed_model_no_images", + )), window_id, ctx, ); @@ -3476,7 +3474,7 @@ impl Input { } BuyCreditsBannerEvent::ShowAutoReloadError { error_message } => { ctx.emit(Event::ShowToast { - message: error_message.to_string(), + message: error_message.clone(), flavor: ToastFlavor::Error, }); } @@ -4432,7 +4430,7 @@ impl Input { ctx.dispatch_typed_action_deferred(action); } else { ctx.emit(Event::ShowToast { - message: "Couldn't navigate to conversation.".to_string(), + message: i18n::t("ai.conversation.navigate_failed"), flavor: ToastFlavor::Error, }); } @@ -5260,7 +5258,10 @@ impl Input { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error(format!("Skill not found: {}", reference)), + DismissibleToast::error( + i18n::t("terminal.toast.skill_not_found") + .replace("{reference}", &reference.to_string()), + ), window_id, ctx, ); @@ -5323,8 +5324,9 @@ impl Input { else { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = - DismissibleToast::default(String::from("No active conversation to export")); + let toast = DismissibleToast::default(i18n::t( + "terminal.input.export.no_active_conversation", + )); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); return; @@ -5388,9 +5390,10 @@ impl Input { let window_id = ctx.window_id(); let display_path = file_path.display().to_string(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::default(format!( - "File {display_path} already exists and will be overwritten" - )); + let toast = DismissibleToast::default( + i18n::t("terminal.input.export.will_overwrite") + .replace("{path}", &display_path), + ); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); } @@ -5402,9 +5405,9 @@ impl Input { let window_id = ctx.window_id(); let display_path = file_path.display().to_string(); ToastStack::handle(ctx).update(ctx, move |toast_stack, ctx| { - let toast = DismissibleToast::default(format!( - "Conversation exported to {display_path}" - )); + let toast = DismissibleToast::default( + i18n::t("terminal.input.export.success").replace("{path}", &display_path), + ); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); } @@ -5412,26 +5415,24 @@ impl Input { // Show error toast with user-friendly message let user_message = match e.kind() { std::io::ErrorKind::PermissionDenied => { - format!( - "Permission denied writing to {}. Check file permissions.", - file_path.display() - ) + i18n::t("terminal.input.export.permission_denied") + .replace("{path}", &file_path.display().to_string()) } std::io::ErrorKind::NotFound => { - format!( - "Directory not found: {}", - file_path - .parent() - .map(|p| p.display().to_string()) - .unwrap_or_default() - ) + let path = file_path + .parent() + .map(|p| p.display().to_string()) + .unwrap_or_default(); + i18n::t("terminal.input.export.directory_not_found") + .replace("{path}", &path) } std::io::ErrorKind::AlreadyExists => { - format!("File {} already exists", file_path.display()) - } - _ => { - format!("Failed to export to {}: {}", file_path.display(), e) + i18n::t("terminal.input.export.file_already_exists") + .replace("{path}", &file_path.display().to_string()) } + _ => i18n::t("terminal.input.export.failed") + .replace("{path}", &file_path.display().to_string()) + .replace("{error}", &e.to_string()), }; log::error!( @@ -5482,9 +5483,12 @@ impl Input { let window_id = ctx.window_id(); let message = if images_removed == 1 { - "1 image was removed - limit is 20 per conversation.".into() + i18n::t("terminal.input.images_removed.one") + .replace("{limit}", &MAX_IMAGES_PER_CONVERSATION.to_string()) } else { - format!("{images_removed} images were removed - limit is 20 per conversation.") + i18n::t("terminal.input.images_removed.other") + .replace("{count}", &images_removed.to_string()) + .replace("{limit}", &MAX_IMAGES_PER_CONVERSATION.to_string()) }; ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { @@ -5884,7 +5888,7 @@ impl Input { // Returns the appropriate hint/placeholder text to render in an empty input when Agent Mode is // enabled (the feature flag, not the specific AI input mode). This method ensures that hint text // is cached when needed for new conversations. - fn agent_mode_hint_text(&mut self, app: &AppContext) -> &str { + fn agent_mode_hint_text(&mut self, app: &AppContext) -> String { let input_model = self.ai_input_model.as_ref(app); let is_udi_enabled = InputSettings::as_ref(app).is_universal_developer_input_enabled(app); @@ -5892,7 +5896,7 @@ impl Input { input_model.input_type(), input_model.should_run_input_autodetection(app), ) { - (InputType::Shell, false) => AGENT_MODE_AI_DISABLED_AUTODETECTION_DISABLED_HINT_TEXT, + (InputType::Shell, false) => i18n::t("terminal.input.hint.run_commands"), (InputType::Shell, true) => { // Ensure hint text is cached for new conversations get_stable_agent_mode_hint_text(&mut self.cached_agent_mode_hint_text) @@ -5909,16 +5913,16 @@ impl Input { { Some(status) if status.is_in_progress() => { if is_udi_enabled { - AGENT_MODE_AI_ENABLED_STEER_HINT_TEXT_UDI + i18n::t("terminal.input.hint.steer_running_agent") } else { - AGENT_MODE_AI_ENABLED_STEER_HINT_TEXT_CLASSIC + i18n::t("terminal.input.hint.steer_running_agent_classic") } } Some(_) => { if is_udi_enabled { - AGENT_MODE_AI_ENABLED_FOLLOW_UP_HINT_TEXT_UDI + i18n::t("terminal.input.hint.ask_follow_up") } else { - AGENT_MODE_AI_ENABLED_FOLLOW_UP_HINT_TEXT_CLASSIC + i18n::t("terminal.input.hint.ask_follow_up_classic") } } None => { @@ -6348,21 +6352,19 @@ impl Input { pub fn clear_cached_hint_text(&mut self) { self.cached_agent_mode_hint_text = None; } - fn cli_agent_rich_input_hint_text(&self, ctx: &ViewContext) -> Cow<'static, str> { + fn cli_agent_rich_input_hint_text(&self, ctx: &ViewContext) -> String { if self.is_locked_in_shell_mode(ctx) { - return Cow::Borrowed(AGENT_MODE_AI_DISABLED_AUTODETECTION_DISABLED_HINT_TEXT); + return i18n::t("terminal.input.hint.run_commands"); } CLIAgentSessionsModel::as_ref(ctx) .session(self.terminal_view_id) .map(|session| match session.agent { - CLIAgent::Unknown => Cow::Borrowed(CLI_AGENT_RICH_INPUT_HINT_TEXT), - _ => Cow::Owned(format!( - "Enter prompt for {}...", - session.agent.display_name() - )), + CLIAgent::Unknown => i18n::t("terminal.input.hint.cli_agent_rich_input"), + _ => i18n::t("terminal.input.hint.enter_prompt_for_agent") + .replace("{agent}", session.agent.display_name()), }) - .unwrap_or(Cow::Borrowed(CLI_AGENT_RICH_INPUT_HINT_TEXT)) + .unwrap_or_else(|| i18n::t("terminal.input.hint.cli_agent_rich_input")) } pub fn set_zero_state_hint_text(&mut self, ctx: &mut ViewContext) { @@ -6378,14 +6380,17 @@ impl Input { .active_conversation(self.terminal_view_id) .is_none_or(|c| c.is_empty()); let hint = if conversation_is_empty { - CLOUD_MODE_V2_HINT_TEXT.to_owned() + i18n::t("terminal.input.hint.cloud_agent") } else { self.handoff_compose_state .as_ref(ctx) .selected_environment_id() .and_then(|id| CloudAmbientAgentEnvironment::get_by_id(id, ctx)) - .map(|env| format!("Hand off to {}", env.model().string_model.display_name())) - .unwrap_or_else(|| "Handoff to cloud".to_owned()) + .map(|env| { + i18n::t("terminal.input.hint.hand_off_to") + .replace("{name}", &env.model().string_model.display_name()) + }) + .unwrap_or_else(|| i18n::t("terminal.input.hint.handoff_to_cloud")) }; self.editor.update(ctx, |editor, ctx| { editor.set_placeholder_text(&hint, ctx); @@ -6397,7 +6402,7 @@ impl Input { let show_hint = *InputSettings::as_ref(ctx).show_hint_text; self.editor.update(ctx, |editor, ctx| { if show_hint { - editor.set_placeholder_text(CLOUD_MODE_V2_HINT_TEXT, ctx); + editor.set_placeholder_text(i18n::t("terminal.input.hint.cloud_agent"), ctx); } else { editor.clear_placeholder_text(ctx); } @@ -6441,13 +6446,16 @@ impl Input { if toggled_on && AISettings::as_ref(ctx).is_any_ai_enabled(ctx) { if FeatureFlag::AgentMode.is_enabled() { // agent_mode_hint_text now handles caching internally - let hint_text = self.agent_mode_hint_text(ctx).to_string(); + let hint_text = self.agent_mode_hint_text(ctx); self.editor.update(ctx, |editor, ctx| { editor.set_placeholder_text(&hint_text, ctx); }); } else { self.editor.update(ctx, |editor, ctx| { - editor.set_placeholder_text(AI_COMMAND_SEARCH_HINT_TEXT, ctx); + editor.set_placeholder_text( + i18n::t("terminal.input.hint.ai_command_search"), + ctx, + ); }); } } else { @@ -6788,9 +6796,10 @@ impl Input { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error(format!( - "Cannot run `{truncated_command}` (command already running)." - )), + DismissibleToast::error( + i18n::t("terminal.input.cannot_run_command_already_running") + .replace("{command}", &truncated_command), + ), window_id, ctx, ); @@ -7496,13 +7505,14 @@ impl Input { // Emit the a11y content as the last step so that it overwrites any of the a11y content // emitted by the editor (if multiple `AccessibilityContent`s are emitted within the same // event loop, the last one wins). - let mut accessibility_text = format!("Workflow command {} inserted.", &command_to_insert); + let mut accessibility_text = i18n::t("terminal.input.workflow.command_inserted") + .replace("{command}", &command_to_insert); if let Some(a11y_content) = self.selected_workflow_a11y_text(ctx) { let _ = write!(accessibility_text, " {a11y_content}"); } ctx.emit_a11y_content(AccessibilityContent::new( accessibility_text, - "Press shift-tab to select the next workflow argument", + i18n::t("terminal.input.workflow.select_next_argument_helper"), WarpA11yRole::UserAction, )); @@ -7601,8 +7611,10 @@ impl Input { .as_ref() .and_then(|selected_workflow_state| { selected_workflow_state.more_info_view.read(ctx, |view, _| { - view.selected_argument() - .map(|argument| format!("Selected Workflow argument {}", argument.name())) + view.selected_argument().map(|argument| { + i18n::t("terminal.input.workflow.selected_argument") + .replace("{argument}", argument.name()) + }) }) }) } @@ -7854,7 +7866,7 @@ impl Input { self.try_execute_command(&command, ctx); ctx.emit_a11y_content(AccessibilityContent::new_without_help( - format!("Executed: {command}"), + i18n::t("terminal.input.executed_command").replace("{command}", &command), WarpA11yRole::UserAction, )); } @@ -10434,9 +10446,9 @@ impl Input { // Show voice status as placeholder when the buffer is empty. if self.editor.as_ref(ctx).is_empty(ctx) { let placeholder = if *is_listening { - "Listening..." + i18n::t("terminal.input.voice.listening") } else { - "Transcribing..." + i18n::t("terminal.input.voice.transcribing") }; self.editor.update(ctx, |editor, ctx| { editor.set_placeholder_text(placeholder, ctx); @@ -10913,17 +10925,26 @@ impl Input { // Show toast for excess images if any if excess_images > 0 { let (limit_name, limit_value) = if available_per_query < available_per_conversation { - ("per query", MAX_IMAGE_COUNT_FOR_QUERY) + ( + i18n::t("terminal.input.image_limit.per_query"), + MAX_IMAGE_COUNT_FOR_QUERY, + ) } else { - ("per conversation", MAX_IMAGES_PER_CONVERSATION) + ( + i18n::t("terminal.input.image_limit.per_conversation"), + MAX_IMAGES_PER_CONVERSATION, + ) }; let message = if excess_images == 1 { - format!("1 image wasn't attached - limit is {limit_value} images {limit_name}.") + i18n::t("terminal.input.images_not_attached.one") + .replace("{limit_value}", &limit_value.to_string()) + .replace("{limit_name}", &limit_name) } else { - format!( - "{excess_images} images weren't attached - limit is {limit_value} images {limit_name}." - ) + i18n::t("terminal.input.images_not_attached.other") + .replace("{count}", &excess_images.to_string()) + .replace("{limit_value}", &limit_value.to_string()) + .replace("{limit_name}", &limit_name) }; self.show_image_paste_error(ctx, message); } @@ -12756,10 +12777,9 @@ impl Input { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |ts, ctx| { ts.add_ephemeral_toast( - DismissibleToast::error( - "No agent harnesses are available. Contact your team admin." - .to_string(), - ), + DismissibleToast::error(i18n::t( + "terminal.input.no_agent_harnesses_available", + )), window_id, ctx, ); @@ -12810,9 +12830,9 @@ impl Input { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |ts, ctx| { ts.add_ephemeral_toast( - DismissibleToast::default( - "Preparing handoff — try again in a moment.".to_owned(), - ) + DismissibleToast::default(i18n::t( + "terminal.input.preparing_handoff", + )) .with_object_id("local-to-cloud-handoff-not-ready".to_owned()), window_id, ctx, @@ -13344,9 +13364,9 @@ impl Input { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "Cannot send queries as a read-only viewer.".to_string(), - ), + DismissibleToast::error(i18n::t( + "terminal.input.read_only_viewer_cannot_send", + )), window_id, ctx, ); @@ -13717,9 +13737,7 @@ impl Input { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "Too many attachments for this conversation.".to_string(), - ), + DismissibleToast::error(i18n::t("terminal.input.too_many_attachments")), window_id, ctx, ); @@ -14922,9 +14940,9 @@ impl TypedActionView for Input { match action { InputAction::FocusInputBox => { ActionAccessibilityContent::Custom(AccessibilityContent::new( - INPUT_A11Y_LABEL, + i18n::t("terminal.input.a11y_label"), // TODO (a11y) use bindings from user settings - INPUT_A11Y_HELPER, + i18n::t("terminal.input.a11y_helper"), WarpA11yRole::TextareaRole, )) } @@ -15057,9 +15075,9 @@ impl TypedActionView for Input { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "Cannot start a new conversation while agent is monitoring a command.".to_string() - ), + DismissibleToast::error(i18n::t( + "terminal.input.cannot_start_while_monitoring", + )), window_id, ctx, ); @@ -15136,9 +15154,9 @@ impl View for Input { fn accessibility_contents(&self, _: &AppContext) -> Option { Some(AccessibilityContent::new( - INPUT_A11Y_LABEL, + i18n::t("terminal.input.a11y_label"), // TODO (a11y) use bindings from user settings - INPUT_A11Y_HELPER, + i18n::t("terminal.input.a11y_helper"), WarpA11yRole::TextareaRole, )) } diff --git a/app/src/terminal/input/conversations/search_item.rs b/app/src/terminal/input/conversations/search_item.rs index bbc92018f4..0b9a605237 100644 --- a/app/src/terminal/input/conversations/search_item.rs +++ b/app/src/terminal/input/conversations/search_item.rs @@ -172,6 +172,6 @@ impl SearchItem for ConversationSearchItem { } fn accessibility_label(&self) -> String { - format!("Conversation: {}", self.entry.display.title) + i18n::t("terminal.input.a11y.conversation").replace("{title}", &self.entry.display.title) } } diff --git a/app/src/terminal/input/conversations/view.rs b/app/src/terminal/input/conversations/view.rs index 97b8919516..5e141ae9bb 100644 --- a/app/src/terminal/input/conversations/view.rs +++ b/app/src/terminal/input/conversations/view.rs @@ -36,13 +36,13 @@ static TAB_CONFIGS: LazyLock> LazyLock::new(|| { let mut configs = vec![InlineMenuTabConfig { id: InlineConversationMenuTab::All, - label: "All".to_string(), + label: i18n::t("terminal.input.conversations.tab.all"), filters: HashSet::new(), }]; if FeatureFlag::InlineMenuHeaders.is_enabled() { configs.push(InlineMenuTabConfig { id: InlineConversationMenuTab::CurrentDirectory, - label: "Current Directory".to_string(), + label: i18n::t("terminal.input.conversations.tab.current_directory"), filters: HashSet::from([QueryFilter::CurrentDirectoryConversations]), }); } diff --git a/app/src/terminal/input/inline_history/data_source.rs b/app/src/terminal/input/inline_history/data_source.rs index 77c6eeefd5..579091e658 100644 --- a/app/src/terminal/input/inline_history/data_source.rs +++ b/app/src/terminal/input/inline_history/data_source.rs @@ -166,7 +166,7 @@ impl InlineHistoryMenuDataSource { }; let title = conversation .title() - .unwrap_or_else(|| "Untitled conversation".to_string()); + .unwrap_or_else(|| i18n::t("terminal.input.untitled_conversation")); let match_result = if trimmed_query.is_empty() { None } else { diff --git a/app/src/terminal/input/inline_history/search_item.rs b/app/src/terminal/input/inline_history/search_item.rs index 8c4a4ffacb..6f59f2fbeb 100644 --- a/app/src/terminal/input/inline_history/search_item.rs +++ b/app/src/terminal/input/inline_history/search_item.rs @@ -278,9 +278,15 @@ impl SearchItem for InlineHistoryItem { fn accessibility_label(&self) -> String { match &self.item_type { - HistoryItemType::Conversation { title, .. } => format!("Conversation: {title}"), - HistoryItemType::Command { command, .. } => format!("Command: {command}"), - HistoryItemType::AIPrompt { query_text } => format!("AI prompt: {query_text}"), + HistoryItemType::Conversation { title, .. } => { + i18n::t("terminal.input.a11y.conversation").replace("{title}", title) + } + HistoryItemType::Command { command, .. } => { + i18n::t("terminal.input.a11y.command").replace("{command}", command) + } + HistoryItemType::AIPrompt { query_text } => { + i18n::t("terminal.input.a11y.ai_prompt").replace("{query}", query_text) + } } } } diff --git a/app/src/terminal/input/inline_history/view.rs b/app/src/terminal/input/inline_history/view.rs index ccfb79a477..bc59700b28 100644 --- a/app/src/terminal/input/inline_history/view.rs +++ b/app/src/terminal/input/inline_history/view.rs @@ -131,7 +131,7 @@ fn build_tab_configs(is_agent_view: bool) -> Vec if !is_agent_view { return vec![InlineMenuTabConfig { id: HistoryTab::All, - label: "All".to_string(), + label: i18n::t("terminal.inline_history.tab.all"), filters: HashSet::new(), }]; } @@ -139,17 +139,17 @@ fn build_tab_configs(is_agent_view: bool) -> Vec vec![ InlineMenuTabConfig { id: HistoryTab::All, - label: "All".to_string(), + label: i18n::t("terminal.inline_history.tab.all"), filters: HashSet::new(), }, InlineMenuTabConfig { id: HistoryTab::Commands, - label: "Commands".to_string(), + label: i18n::t("terminal.inline_history.tab.commands"), filters: HashSet::from([QueryFilter::Commands]), }, InlineMenuTabConfig { id: HistoryTab::Prompts, - label: "Prompts".to_string(), + label: i18n::t("terminal.inline_history.tab.prompts"), filters: HashSet::from([QueryFilter::PromptHistory]), }, ] @@ -262,7 +262,7 @@ impl InlineHistoryMenuView { let menu_view = if FeatureFlag::InlineMenuHeaders.is_enabled() { let configure_button = ctx.add_view(|_| { - ActionButton::new("Configure", ConfigureButtonTheme) + ActionButton::new(i18n::t("common.configure"), ConfigureButtonTheme) .with_icon(Icon::Settings) .with_size(ButtonSize::Small) .on_click(|ctx| { @@ -273,7 +273,7 @@ impl InlineHistoryMenuView { }) }); let header_config = InlineMenuHeaderConfig { - label: "History".to_string(), + label: i18n::t("terminal.inline_history.header"), trailing_element: Some(Box::new(move |_app: &AppContext| { ChildView::new(&configure_button).finish() })), diff --git a/app/src/terminal/input/inline_menu/message_provider.rs b/app/src/terminal/input/inline_menu/message_provider.rs index 935e9afb32..74b24bb165 100644 --- a/app/src/terminal/input/inline_menu/message_provider.rs +++ b/app/src/terminal/input/inline_menu/message_provider.rs @@ -41,7 +41,7 @@ pub fn default_navigation_message_items( let mut items = vec![ MessageItem::keystroke(navigation_keystrokes.0), MessageItem::keystroke(navigation_keystrokes.1), - MessageItem::text(" to navigate"), + MessageItem::text(i18n::t("terminal.message_bar.to_navigate")), ]; if args.inline_menu_model.tab_configs().len() > 1 { @@ -50,7 +50,9 @@ pub fn default_navigation_message_items( shift: true, ..Default::default() })); - items.push(MessageItem::text(" to cycle tabs")); + items.push(MessageItem::text(i18n::t( + "terminal.message_bar.to_cycle_tabs", + ))); } items.push(MessageItem::clickable( @@ -59,7 +61,7 @@ pub fn default_navigation_message_items( key: "escape".to_owned(), ..Default::default() }), - MessageItem::text(" to dismiss"), + MessageItem::text(i18n::t("terminal.message_bar.to_dismiss")), ], |ctx| { ctx.dispatch_typed_action(InlineMenuRowAction::::Dismiss); diff --git a/app/src/terminal/input/inline_menu/mod.rs b/app/src/terminal/input/inline_menu/mod.rs index 4d838e5e9f..42504f06f7 100644 --- a/app/src/terminal/input/inline_menu/mod.rs +++ b/app/src/terminal/input/inline_menu/mod.rs @@ -50,19 +50,19 @@ pub enum InlineMenuType { } impl InlineMenuType { - fn display_label(&self) -> &'static str { + fn display_label(&self) -> String { match self { - InlineMenuType::SlashCommands => "/Commands", - InlineMenuType::ModelSelector => "/Model", - InlineMenuType::ConversationMenu => "/Conversations", - InlineMenuType::ProfileSelector => "/Profiles", - InlineMenuType::PromptsMenu => "/Prompts", - InlineMenuType::SkillMenu => "/Skills", - InlineMenuType::UserQueryMenu => "/Fork", - InlineMenuType::RewindMenu => "/Rewind", - InlineMenuType::InlineHistoryMenu => "History", - InlineMenuType::IndexedReposMenu => "/Repos", - InlineMenuType::PlanMenu => "/Plans", + InlineMenuType::SlashCommands => "/Commands".to_owned(), + InlineMenuType::ModelSelector => "/Model".to_owned(), + InlineMenuType::ConversationMenu => "/Conversations".to_owned(), + InlineMenuType::ProfileSelector => "/Profiles".to_owned(), + InlineMenuType::PromptsMenu => "/Prompts".to_owned(), + InlineMenuType::SkillMenu => "/Skills".to_owned(), + InlineMenuType::UserQueryMenu => "/Fork".to_owned(), + InlineMenuType::RewindMenu => "/Rewind".to_owned(), + InlineMenuType::InlineHistoryMenu => i18n::t("terminal.input.inline_menu.history"), + InlineMenuType::IndexedReposMenu => "/Repos".to_owned(), + InlineMenuType::PlanMenu => "/Plans".to_owned(), } } diff --git a/app/src/terminal/input/inline_menu/view.rs b/app/src/terminal/input/inline_menu/view.rs index 1af1da99e0..0adfedfa2b 100644 --- a/app/src/terminal/input/inline_menu/view.rs +++ b/app/src/terminal/input/inline_menu/view.rs @@ -515,7 +515,7 @@ impl InlineMenuView { state_handles: Default::default(), weak_handle: ctx.handle(), header_config: InlineMenuHeaderConfig { - label: A::MENU_TYPE.display_label().to_string(), + label: A::MENU_TYPE.display_label(), trailing_element: None, }, banner_fn: None, @@ -1037,9 +1037,9 @@ impl View for InlineMenuView; if self.result_renderers.is_empty() { content = if self.mixer.as_ref(app).is_loading() { - self.render_no_results_state("Loading...".into(), app) + self.render_no_results_state(i18n::t("common.loading"), app) } else { - self.render_no_results_state("No results".into(), app) + self.render_no_results_state(i18n::t("terminal.input.no_results"), app) }; } else { let results_list = self.render_results_list(app); diff --git a/app/src/terminal/input/message_bar/attached_context.rs b/app/src/terminal/input/message_bar/attached_context.rs index bd358d957d..76e80d09a4 100644 --- a/app/src/terminal/input/message_bar/attached_context.rs +++ b/app/src/terminal/input/message_bar/attached_context.rs @@ -58,18 +58,18 @@ impl MessageProvider for AttachedBlocksM .map(|cmd| truncated_command_for_block(&cmd))?; let message_text = if context_block_ids.len() == 1 { - format!("`{}` attached as context", block_command) + i18n::t("terminal.input.message_bar.attached_context.single") + .replace("{command}", &block_command) } else if context_block_ids.len() == 2 { - format!( - "`{}` and 1 other command attached as context", - block_command - ) + i18n::t("terminal.input.message_bar.attached_context.one_other_command") + .replace("{command}", &block_command) } else { - format!( - "`{}` and {} other commands attached as context", - block_command, - context_block_ids.len().saturating_sub(1) - ) + i18n::t("terminal.input.message_bar.attached_context.other_commands") + .replace("{command}", &block_command) + .replace( + "{count}", + &context_block_ids.len().saturating_sub(1).to_string(), + ) }; let mut items = vec![MessageItem::text(message_text)]; @@ -83,7 +83,7 @@ impl MessageProvider for AttachedBlocksM key: "escape".to_owned(), ..Default::default() }), - MessageItem::text(" to remove"), + MessageItem::text(i18n::t("terminal.message_bar.to_remove")), ], |ctx| { ctx.dispatch_typed_action(InputAction::ClearAttachedContext); @@ -120,7 +120,9 @@ impl MessageProvider let _ = args.context_model().pending_context_selected_text()?; - let mut items = vec![MessageItem::text("selected text attached as context")]; + let mut items = vec![MessageItem::text(i18n::t( + "terminal.input.message_bar.attached_context.selected_text", + ))]; // Always show ESC hint in agent view, make it clickable if args.agent_view_controller().is_active() { @@ -131,7 +133,7 @@ impl MessageProvider key: "escape".to_owned(), ..Default::default() }), - MessageItem::text(" to remove"), + MessageItem::text(i18n::t("terminal.message_bar.to_remove")), ], |ctx| { ctx.dispatch_typed_action(InputAction::ClearAttachedContext); diff --git a/app/src/terminal/input/models/data_source.rs b/app/src/terminal/input/models/data_source.rs index 27ad12f23b..ff991bf015 100644 --- a/app/src/terminal/input/models/data_source.rs +++ b/app/src/terminal/input/models/data_source.rs @@ -20,8 +20,7 @@ use warpui::{AppContext, Element, Entity, EntityId, SingletonEntity as _}; use super::model_spec_scores::{ render_model_spec_header, render_model_spec_scores, CostRow, CostRowTooltip, - ModelSpecScoresLayout, MODEL_SPECS_DESCRIPTION, MODEL_SPECS_TITLE, REASONING_LEVEL_DESCRIPTION, - REASONING_LEVEL_TITLE, + ModelSpecScoresLayout, }; use crate::ai::execution_profiles::model_menu_items::is_auto; use crate::ai::llms::{ @@ -43,8 +42,6 @@ use crate::terminal::input::message_bar::{Message, MessageItem}; use crate::workspace::WorkspaceAction; use crate::workspaces::user_workspaces::UserWorkspaces; -const AUTO_BEDROCK_TOOLTIP: &str = "Warp uses Bedrock when the model Auto selects supports it; otherwise it may use Warp-hosted inference."; - #[derive(Clone, Debug)] pub struct AcceptModel { pub id: LLMId, @@ -63,7 +60,7 @@ impl InlineMenuAction for AcceptModel { key: "enter".to_owned(), ..Default::default() }), - MessageItem::text(" to select"), + MessageItem::text(i18n::t("terminal.message_bar.to_select")), MessageItem::keystroke(if OperatingSystem::get().is_mac() { Keystroke { key: "enter".to_owned(), @@ -78,7 +75,7 @@ impl InlineMenuAction for AcceptModel { ..Default::default() } }), - MessageItem::text(" select and save to profile"), + MessageItem::text(i18n::t("terminal.message_bar.select_and_save_to_profile")), ]; if args.inline_menu_model.tab_configs().len() > 1 { @@ -87,7 +84,9 @@ impl InlineMenuAction for AcceptModel { shift: true, ..Default::default() })); - items.push(MessageItem::text(" to cycle tabs")); + items.push(MessageItem::text(i18n::t( + "terminal.message_bar.to_cycle_tabs", + ))); } items.push(MessageItem::clickable( @@ -96,7 +95,7 @@ impl InlineMenuAction for AcceptModel { key: "escape".to_owned(), ..Default::default() }), - MessageItem::text(" to dismiss"), + MessageItem::text(i18n::t("terminal.message_bar.to_dismiss")), ], |ctx| { ctx.dispatch_typed_action( @@ -438,7 +437,10 @@ impl SearchItem for ModelSearchItem { let discount_percentage = self.discount_percentage.unwrap_or(0.); let chip = Container::new( Text::new_inline( - format!("{}% off!", discount_percentage.round() as u32), + i18n::t("terminal.input.models.discount_off").replace( + "{percent}", + &(discount_percentage.round() as u32).to_string(), + ), appearance.ui_font_family(), font_size, ) @@ -472,9 +474,15 @@ impl SearchItem for ModelSearchItem { let theme = appearance.theme(); let (title, description) = if self.reasoning_level.is_some() { - (REASONING_LEVEL_TITLE, REASONING_LEVEL_DESCRIPTION) + ( + i18n::t("terminal.model_specs.reasoning_level_title"), + i18n::t("terminal.model_specs.reasoning_level_description"), + ) } else { - (MODEL_SPECS_TITLE, MODEL_SPECS_DESCRIPTION) + ( + i18n::t("terminal.model_specs.title"), + i18n::t("terminal.model_specs.description"), + ) }; let header = render_model_spec_header(title, description, app); @@ -493,7 +501,7 @@ impl SearchItem for ModelSearchItem { ButtonVariant::Outlined, self.manage_api_key_mouse_state.clone(), ) - .with_text_label("Manage".to_string()) + .with_text_label(i18n::t("common.manage")) .with_style(UiComponentStyles { height: Some(24.), padding: Some(Coords { @@ -515,15 +523,15 @@ impl SearchItem for ModelSearchItem { .finish(); CostRow::BilledToProvider { label: if self.is_using_bedrock && self.is_auto { - "Inference may use Bedrock" + i18n::t("terminal.model_specs.inference_may_use_bedrock") } else if self.is_using_bedrock { - "Inference via Bedrock" + i18n::t("terminal.model_specs.inference_via_bedrock") } else { - "Inference via API key" + i18n::t("terminal.model_specs.inference_via_api_key") }, tooltip: if self.is_using_bedrock && self.is_auto { Some(CostRowTooltip { - text: AUTO_BEDROCK_TOOLTIP, + text: i18n::t("terminal.model_specs.auto_bedrock_tooltip"), mouse_state: self.cost_row_tooltip_mouse_state.clone(), }) } else { @@ -578,13 +586,13 @@ impl SearchItem for ModelSearchItem { FormattedTextFragment::plain_text(format!( "{display_name} is not available for free users. " )), - FormattedTextFragment::hyperlink("Upgrade", upgrade_url), + FormattedTextFragment::hyperlink(i18n::t("common.upgrade"), upgrade_url), ]; if byok_available { - text_fragments.push(FormattedTextFragment::plain_text(" or ".to_string())); + text_fragments.push(FormattedTextFragment::plain_text(i18n::t("common.or"))); text_fragments.push(FormattedTextFragment::hyperlink_action( - "bring your own key", + i18n::t("settings.billing.bring_your_own_key"), WorkspaceAction::ShowSettingsPageWithSearch { search_query: "api".to_string(), section: Some(SettingsSection::WarpAgent), @@ -659,12 +667,12 @@ impl SearchItem for ModelSearchItem { } fn accessibility_label(&self) -> String { - let mut label = format!("Model: {}", self.display_text); + let mut label = i18n::t("terminal.input.a11y.model").replace("{model}", &self.display_text); if self.is_selected { - label.push_str(" (selected)"); + label.push_str(&i18n::t("terminal.input.a11y.selected_suffix")); } if self.is_disabled() { - label.push_str(" (disabled)"); + label.push_str(&i18n::t("terminal.input.a11y.disabled_suffix")); } label } diff --git a/app/src/terminal/input/models/model_spec_scores.rs b/app/src/terminal/input/models/model_spec_scores.rs index e795fe0d6f..1a02012f80 100644 --- a/app/src/terminal/input/models/model_spec_scores.rs +++ b/app/src/terminal/input/models/model_spec_scores.rs @@ -18,24 +18,18 @@ use crate::terminal::input::inline_menu::styles as inline_styles; const CORNER_RADIUS: f32 = 4.0; const ROW_SPACING: f32 = 12.0; -pub const MODEL_SPECS_TITLE: &str = "Model Specs"; -pub const MODEL_SPECS_DESCRIPTION: &str = "Warp's benchmarks for how well a model performs in our harness, the rate at which it consumes credits, and task speed."; - -pub const REASONING_LEVEL_TITLE: &str = "Reasoning level"; -pub const REASONING_LEVEL_DESCRIPTION: &str = "Increased reasoning levels consume more credits and have higher latency, but higher performance for complicated tasks."; - pub enum CostRow { Bar { value: Option, }, BilledToProvider { - label: &'static str, + label: String, tooltip: Option, manage_button: Box, }, } pub struct CostRowTooltip { - pub text: &'static str, + pub text: String, pub mouse_state: MouseStateHandle, } @@ -50,7 +44,7 @@ pub fn render_model_spec_scores( app: &AppContext, ) -> Box { let mut rows = vec![render_score_row( - "Intelligence", + i18n::t("terminal.model_specs.intelligence"), ScoreRowKind::Bar { value: spec.as_ref().map(|spec| spec.quality), }, @@ -60,7 +54,7 @@ pub fn render_model_spec_scores( )]; rows.push(render_score_row( - "Speed", + i18n::t("terminal.model_specs.speed"), ScoreRowKind::Bar { value: spec.as_ref().map(|spec| spec.speed), }, @@ -72,7 +66,7 @@ pub fn render_model_spec_scores( match cost_row { CostRow::Bar { value } => { rows.push(render_score_row( - "Cost", + i18n::t("terminal.model_specs.cost"), ScoreRowKind::Bar { value }, None, layout.bg_bar_color, @@ -85,7 +79,7 @@ pub fn render_model_spec_scores( manage_button, } => { rows.push(render_score_row( - "Cost", + i18n::t("terminal.model_specs.cost"), ScoreRowKind::BilledToProvider { label, manage_button, @@ -108,13 +102,13 @@ enum ScoreRowKind { value: Option, }, BilledToProvider { - label: &'static str, + label: String, manage_button: Box, }, } fn render_score_row( - name: &str, + name: String, kind: ScoreRowKind, label_tooltip: Option, bg_bar_color: ColorU, @@ -131,7 +125,7 @@ fn render_score_row( appearance.ui_font_family(), appearance.monospace_font_size(), ) * 8.; - let label = ConstrainedBox::new(render_row_label(name, label_tooltip, appearance, app)) + let label = ConstrainedBox::new(render_row_label(&name, label_tooltip, appearance, app)) .with_width(label_width) .finish(); @@ -259,9 +253,9 @@ fn render_row_label( .finish() } -fn render_provider_label(label: &'static str, appearance: &Appearance) -> Box { +fn render_provider_label(label: String, appearance: &Appearance) -> Box { Container::new( - Text::new(label.to_string(), appearance.ui_font_family(), 14.) + Text::new(label, appearance.ui_font_family(), 14.) .with_color(appearance.theme().disabled_ui_text_color().into()) .finish(), ) @@ -271,7 +265,7 @@ fn render_provider_label(label: &'static str, appearance: &Appearance) -> Box Box { let icon_color = appearance.theme().disabled_ui_text_color(); let ui_builder = appearance.ui_builder(); - let tooltip_text = tooltip.text.to_string(); + let tooltip_text = tooltip.text; Hoverable::new(tooltip.mouse_state, move |state| { let info_icon = Container::new( ConstrainedBox::new(WarpUiIcon::new("bundled/svg/info.svg", icon_color).finish()) @@ -300,15 +294,15 @@ fn render_info_tooltip(tooltip: CostRowTooltip, appearance: &Appearance) -> Box< } pub fn render_model_spec_header( - title: &str, - description: &str, + title: String, + description: String, app: &AppContext, ) -> Box { let appearance = Appearance::as_ref(app); let theme = appearance.theme(); let title = Text::new( - title.to_string(), + title, appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -320,7 +314,7 @@ pub fn render_model_spec_header( .finish(); let description = Text::new( - description.to_string(), + description, appearance.ui_font_family(), inline_styles::font_size(appearance), ) diff --git a/app/src/terminal/input/models/view.rs b/app/src/terminal/input/models/view.rs index ab4569a5a5..042b649fef 100644 --- a/app/src/terminal/input/models/view.rs +++ b/app/src/terminal/input/models/view.rs @@ -1,8 +1,6 @@ -use std::collections::HashSet; -use std::sync::LazyLock; - use ai::api_keys::{ApiKeyManager, ApiKeyManagerEvent}; use pathfinder_color::ColorU; +use std::collections::HashSet; use warp_core::ui::appearance::Appearance; use warp_core::ui::theme::color::internal_colors; use warp_core::ui::theme::Fill; @@ -72,22 +70,21 @@ pub enum InlineModelSelectorEvent { Dismissed, } -static TAB_CONFIGS: LazyLock>> = - LazyLock::new(|| { - let mut configs = vec![InlineMenuTabConfig { - id: InlineModelSelectorTab::BaseAgent, - label: "Base".to_string(), - filters: HashSet::from([QueryFilter::BaseModels]), - }]; - if FeatureFlag::InlineMenuHeaders.is_enabled() { - configs.push(InlineMenuTabConfig { - id: InlineModelSelectorTab::FullTerminalUse, - label: "Full Terminal Use".to_string(), - filters: HashSet::from([QueryFilter::FullTerminalUseModels]), - }); - } - configs - }); +fn build_tab_configs() -> Vec> { + let mut configs = vec![InlineMenuTabConfig { + id: InlineModelSelectorTab::BaseAgent, + label: i18n::t("terminal.model_selector.tab.base"), + filters: HashSet::from([QueryFilter::BaseModels]), + }]; + if FeatureFlag::InlineMenuHeaders.is_enabled() { + configs.push(InlineMenuTabConfig { + id: InlineModelSelectorTab::FullTerminalUse, + label: i18n::t("terminal.model_selector.tab.full_terminal_use"), + filters: HashSet::from([QueryFilter::FullTerminalUseModels]), + }); + } + configs +} struct TabSwitchSelection { model_id: Option, @@ -119,7 +116,7 @@ impl InlineModelSelectorView { ) -> Self { let data_source = ctx.add_model(|_| ModelSelectorDataSource::new(terminal_view_id)); - let tab_configs = TAB_CONFIGS.clone(); + let tab_configs = build_tab_configs(); let initial_filters = tab_configs .first() .map(|config| config.filters.clone()) @@ -142,15 +139,18 @@ impl InlineModelSelectorView { let menu_view = if FeatureFlag::InlineMenuHeaders.is_enabled() { let manage_defaults_button = ctx.add_view(|_| { - ActionButton::new("Manage defaults", ManageDefaultsTheme) - .with_icon(Icon::Settings) - .with_size(ButtonSize::Small) - .on_click(|ctx| { - ctx.dispatch_typed_action(WorkspaceAction::ShowSettingsPageWithSearch { - search_query: String::new(), - section: Some(SettingsSection::WarpAgent), - }); - }) + ActionButton::new( + i18n::t("terminal.model_selector.manage_defaults"), + ManageDefaultsTheme, + ) + .with_icon(Icon::Settings) + .with_size(ButtonSize::Small) + .on_click(|ctx| { + ctx.dispatch_typed_action(WorkspaceAction::ShowSettingsPageWithSearch { + search_query: String::new(), + section: Some(SettingsSection::WarpAgent), + }); + }) }); let header_config = InlineMenuHeaderConfig { label: "/model".to_string(), @@ -186,11 +186,15 @@ impl InlineModelSelectorView { let is_cli_agent_in_control_or_tagged_in = cli_ctrl.as_ref(app).is_agent_in_control_or_tagged_in(); let message = match active_tab { - InlineModelSelectorTab::FullTerminalUse if main_agent_in_progress && !is_cli_agent_in_control_or_tagged_in => { - Some("You're using the base agent. Full terminal use models only apply to the full terminal use agent.") + InlineModelSelectorTab::FullTerminalUse + if main_agent_in_progress && !is_cli_agent_in_control_or_tagged_in => + { + Some(i18n::t("terminal.input.models.base_agent_warning")) } - InlineModelSelectorTab::BaseAgent if is_cli_agent_in_control_or_tagged_in => { - Some("You're using the full terminal use agent. Base models only apply to the base agent.") + InlineModelSelectorTab::BaseAgent + if is_cli_agent_in_control_or_tagged_in => + { + Some(i18n::t("terminal.input.models.full_terminal_use_warning")) } _ => None, }; diff --git a/app/src/terminal/input/plans/mod.rs b/app/src/terminal/input/plans/mod.rs index 0658ff2ff1..c0e327c86d 100644 --- a/app/src/terminal/input/plans/mod.rs +++ b/app/src/terminal/input/plans/mod.rs @@ -39,7 +39,7 @@ impl InlineMenuAction for AcceptPlan { key: "enter".to_owned(), ..Default::default() }), - MessageItem::text(" open plan"), + MessageItem::text(i18n::t("terminal.message_bar.open_plan")), ], move |ctx| { ctx.dispatch_typed_action(InlineMenuRowAction::Accept { diff --git a/app/src/terminal/input/plans/search_item.rs b/app/src/terminal/input/plans/search_item.rs index ded5ee12c0..abd2de845c 100644 --- a/app/src/terminal/input/plans/search_item.rs +++ b/app/src/terminal/input/plans/search_item.rs @@ -160,6 +160,6 @@ impl SearchItem for PlanSearchItem { } fn accessibility_label(&self) -> String { - format!("Plan: {}", self.title) + i18n::t("terminal.input.a11y.plan").replace("{title}", &self.title) } } diff --git a/app/src/terminal/input/profiles/search_item.rs b/app/src/terminal/input/profiles/search_item.rs index 2ce9768a5b..f2e1f8efff 100644 --- a/app/src/terminal/input/profiles/search_item.rs +++ b/app/src/terminal/input/profiles/search_item.rs @@ -14,8 +14,6 @@ use crate::search::{ItemHighlightState, SearchItem}; use crate::terminal::input::inline_menu::styles as inline_styles; use crate::terminal::input::profiles::data_source::SelectProfileMenuItem; -const MANAGE_PROFILES_LABEL: &str = "Manage profiles"; - #[derive(Debug, Clone)] enum ProfileSearchItemKind { Profile { @@ -108,7 +106,9 @@ impl SearchItem for ProfileSearchItem { is_selected, .. } => (profile_name.clone(), *is_selected), - ProfileSearchItemKind::ManageProfiles => (MANAGE_PROFILES_LABEL.to_owned(), false), + ProfileSearchItemKind::ManageProfiles => { + (i18n::t("terminal.profile_selector.manage_profiles"), false) + } }; let mut label = Text::new_inline(label_text, appearance.ui_font_family(), font_size) @@ -181,9 +181,11 @@ impl SearchItem for ProfileSearchItem { fn accessibility_label(&self) -> String { match &self.kind { ProfileSearchItemKind::Profile { profile_name, .. } => { - format!("Profile: {profile_name}") + i18n::t("terminal.input.a11y.profile").replace("{profile}", profile_name) + } + ProfileSearchItemKind::ManageProfiles => { + i18n::t("terminal.profile_selector.manage_profiles") } - ProfileSearchItemKind::ManageProfiles => MANAGE_PROFILES_LABEL.to_string(), } } } diff --git a/app/src/terminal/input/prompts/data_source.rs b/app/src/terminal/input/prompts/data_source.rs index 7c93bb9dc4..e1dfbb5a6f 100644 --- a/app/src/terminal/input/prompts/data_source.rs +++ b/app/src/terminal/input/prompts/data_source.rs @@ -224,6 +224,6 @@ impl SearchItem for PromptSearchItem { } fn accessibility_label(&self) -> String { - format!("Prompt: {}", self.name) + i18n::t("terminal.input.a11y.prompt").replace("{prompt}", &self.name) } } diff --git a/app/src/terminal/input/repos/search_item.rs b/app/src/terminal/input/repos/search_item.rs index 857d17a917..4f805e9b70 100644 --- a/app/src/terminal/input/repos/search_item.rs +++ b/app/src/terminal/input/repos/search_item.rs @@ -191,6 +191,7 @@ impl SearchItem for RepoSearchItem { } fn accessibility_label(&self) -> String { - format!("Indexed repository: {}", self.display_name) + i18n::t("terminal.input.a11y.indexed_repository") + .replace("{repository}", &self.display_name) } } diff --git a/app/src/terminal/input/rewind/mod.rs b/app/src/terminal/input/rewind/mod.rs index a278e79299..b6b78f2b43 100644 --- a/app/src/terminal/input/rewind/mod.rs +++ b/app/src/terminal/input/rewind/mod.rs @@ -23,7 +23,7 @@ impl InlineMenuAction for SelectRewindPoint { key: "enter".to_owned(), ..Default::default() }), - MessageItem::text("rewind"), + MessageItem::text(i18n::t("terminal.input.rewind.action")), ]; items.extend(default_navigation_message_items(&args)); diff --git a/app/src/terminal/input/rewind/search_item.rs b/app/src/terminal/input/rewind/search_item.rs index 6332241a16..4d9b0fa0bc 100644 --- a/app/src/terminal/input/rewind/search_item.rs +++ b/app/src/terminal/input/rewind/search_item.rs @@ -43,7 +43,7 @@ impl RewindSearchItem { pub fn new_current() -> Self { Self { exchange_id: None, - query_text: "Current".to_string(), + query_text: i18n::t("common.current"), file_changes: FileChangesInfo::default(), query_match_result: None, score: OrderedFloat(0.0), @@ -141,7 +141,7 @@ impl SearchItem for RewindSearchItem { let changes_element: Box = if self.is_current { // "Current" item shows "No code to be restored" Text::new_inline( - "No code to be restored".to_string(), + i18n::t("terminal.rewind.no_code_to_restore"), appearance.ui_font_family(), secondary_font_size, ) @@ -174,7 +174,7 @@ impl SearchItem for RewindSearchItem { row.finish() } else { Text::new_inline( - "No code to be restored".to_string(), + i18n::t("terminal.rewind.no_code_to_restore"), appearance.ui_font_family(), secondary_font_size, ) @@ -218,14 +218,14 @@ impl SearchItem for RewindSearchItem { fn accessibility_label(&self) -> String { if self.is_current { - "Current state (no rewind)".to_string() + i18n::t("terminal.input.rewind.current_state") } else if self.has_code_changes() { - format!( - "Rewind to: {} (+{} -{})", - self.query_text, self.file_changes.lines_added, self.file_changes.lines_removed - ) + i18n::t("terminal.input.rewind.with_changes") + .replace("{query}", &self.query_text) + .replace("{added}", &self.file_changes.lines_added.to_string()) + .replace("{removed}", &self.file_changes.lines_removed.to_string()) } else { - format!("Rewind to: {} (no code changes)", self.query_text) + i18n::t("terminal.input.rewind.no_code_changes").replace("{query}", &self.query_text) } } } diff --git a/app/src/terminal/input/skills/data_source.rs b/app/src/terminal/input/skills/data_source.rs index 04b19e66d3..d8b178a990 100644 --- a/app/src/terminal/input/skills/data_source.rs +++ b/app/src/terminal/input/skills/data_source.rs @@ -42,12 +42,12 @@ impl InlineMenuAction for AcceptSkill { // If no item is selected, show "No skills found" message with escape hint if args.inline_menu_model.selected_item().is_none() { return Some(Message::new(vec![ - MessageItem::text("No skills found"), + MessageItem::text(i18n::t("terminal.message_bar.no_skills_found")), MessageItem::keystroke(Keystroke { key: "escape".to_owned(), ..Default::default() }), - MessageItem::text(" to dismiss"), + MessageItem::text(i18n::t("terminal.message_bar.to_dismiss")), ])); } @@ -346,7 +346,7 @@ impl SearchItem for SkillSearchItem { let badge_text_color = inline_styles::disabled_text_color(theme, background_color.into()); let badge_text = Text::new_inline( - "Project Skill".to_string(), + i18n::t("terminal.skills.project_skill"), appearance.ui_font_family(), badge_font_size, ) @@ -399,6 +399,6 @@ impl SearchItem for SkillSearchItem { } fn accessibility_label(&self) -> String { - format!("Skill: {}", self.skill_name) + i18n::t("terminal.input.a11y.skill").replace("{skill}", &self.skill_name) } } diff --git a/app/src/terminal/input/slash_commands/cloud_mode_v2_view.rs b/app/src/terminal/input/slash_commands/cloud_mode_v2_view.rs index 03b0cf9923..63277b2c30 100644 --- a/app/src/terminal/input/slash_commands/cloud_mode_v2_view.rs +++ b/app/src/terminal/input/slash_commands/cloud_mode_v2_view.rs @@ -109,11 +109,11 @@ pub enum Section { impl Section { const RENDER_ORDER: [Self; 3] = [Self::Commands, Self::Skills, Self::Prompts]; - fn header(self) -> &'static str { + fn header(self) -> String { match self { - Self::Commands => "Commands", - Self::Skills => "Skills", - Self::Prompts => "Prompts", + Self::Commands => i18n::t("terminal.input.slash_commands.section.commands"), + Self::Skills => i18n::t("terminal.input.slash_commands.section.skills"), + Self::Prompts => i18n::t("terminal.input.slash_commands.section.prompts"), } } @@ -893,18 +893,14 @@ impl CloudModeV2SlashCommandView { let theme = appearance.theme(); let menu_bg = inline_styles::menu_background_color(app); let label = if self.mixer.as_ref(app).is_loading() { - "Loading..." + i18n::t("common.loading") } else { - "No results" + i18n::t("terminal.input.no_results") }; Container::new( - Text::new( - label.to_owned(), - appearance.ui_font_family(), - ITEM_FONT_SIZE, - ) - .with_color(theme.disabled_text_color(Fill::Solid(menu_bg)).into_solid()) - .finish(), + Text::new(label, appearance.ui_font_family(), ITEM_FONT_SIZE) + .with_color(theme.disabled_text_color(Fill::Solid(menu_bg)).into_solid()) + .finish(), ) .with_horizontal_padding(MENU_HORIZONTAL_PADDING) .with_vertical_padding(ROW_VERTICAL_PADDING) @@ -1159,7 +1155,7 @@ fn render_section_header(section: Section, app: &AppContext) -> Box Container::new( Text::new( - section.header().to_owned(), + section.header(), appearance.ui_font_family(), SECTION_HEADER_FONT_SIZE, ) @@ -1184,7 +1180,8 @@ fn render_show_more_row( let menu_bg = inline_styles::menu_background_color(app); let secondary_color = theme.sub_text_color(Fill::Solid(menu_bg)).into_solid(); - let label = format!("Show {hidden_count} more"); + let label = i18n::t("terminal.input.slash_commands.show_more") + .replace("{count}", &hidden_count.to_string()); let row = Hoverable::new(mouse_state, move |mouse_state| { let bg = if is_selected || mouse_state.is_hovered() { diff --git a/app/src/terminal/input/slash_commands/data_source/mod.rs b/app/src/terminal/input/slash_commands/data_source/mod.rs index f744b2751c..275ed6e78f 100644 --- a/app/src/terminal/input/slash_commands/data_source/mod.rs +++ b/app/src/terminal/input/slash_commands/data_source/mod.rs @@ -609,7 +609,7 @@ impl InlineItem { action: AcceptSlashCommandOrSavedPrompt::SlashCommand { id: *command_id }, icon_path: command.icon_path, name: command.name.to_owned(), - description: Some(command.description.to_owned()), + description: Some(command.description()), font_family: appearance.monospace_font_family(), name_match_result: None, description_match_result: None, diff --git a/app/src/terminal/input/slash_commands/mod.rs b/app/src/terminal/input/slash_commands/mod.rs index 1746388677..0a52177996 100644 --- a/app/src/terminal/input/slash_commands/mod.rs +++ b/app/src/terminal/input/slash_commands/mod.rs @@ -383,7 +383,11 @@ impl Input { if command.availability.contains(Availability::AI_ENABLED) && !AISettings::as_ref(ctx).is_any_ai_enabled(ctx) { - show_error_toast(format!("{} requires AI to be enabled", command.name), ctx); + show_error_toast( + i18n::t("terminal.slash_commands.requires_ai_enabled") + .replace("{command}", command.name), + ctx, + ); return true; } @@ -495,7 +499,7 @@ impl Input { .filter(|name| !name.is_empty()) else { show_error_toast( - "Please provide a tab name after /rename-tab".to_owned(), + i18n::t("terminal.slash_commands.rename_tab_missing_name"), ctx, ); return true; @@ -518,10 +522,8 @@ impl Input { .filter(|name| !name.is_empty()) else { show_error_toast( - format!( - "Please provide a color after /set-tab-color ({})", - supported_options() - ), + i18n::t("terminal.slash_commands.set_tab_color_missing_color") + .replace("{options}", &supported_options()), ctx, ); return true; @@ -538,10 +540,9 @@ impl Input { Some(c) => SelectedTabColor::Color(c), None => { show_error_toast( - format!( - "Unknown tab color '{arg}'. Use one of: {}.", - supported_options() - ), + i18n::t("terminal.slash_commands.set_tab_color_unknown") + .replace("{color}", arg) + .replace("{options}", &supported_options()), ctx, ); return true; @@ -567,8 +568,7 @@ impl Input { create_project if command.name == commands::CREATE_NEW_PROJECT.name => { if argument.is_none_or(|args| args.is_empty()) { show_error_toast( - "Please describe the project you want to create after /create-new-project" - .to_owned(), + i18n::t("terminal.slash_commands.create_project_missing_description"), ctx, ); return true; @@ -593,10 +593,9 @@ impl Input { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "The /open-file command is only available for local sessions" - .to_owned(), - ), + DismissibleToast::error(i18n::t( + "terminal.slash_commands.open_file_local_only", + )), window_id, ctx, ); @@ -629,15 +628,15 @@ impl Input { } Ok(_) => { show_error_toast( - "The /open-file command only works for files, not directories" - .to_owned(), + i18n::t("terminal.slash_commands.open_file_requires_file"), ctx, ); return true; } Err(_) => { show_error_toast( - format!("File not found: {}", file_path.display()), + i18n::t("terminal.slash_commands.file_not_found") + .replace("{path}", &file_path.display().to_string()), ctx, ); return true; @@ -655,7 +654,7 @@ impl Input { #[cfg(not(feature = "local_fs"))] { show_error_toast( - "The /open-file command is not supported in this build".to_owned(), + i18n::t("terminal.slash_commands.open_file_unsupported"), ctx, ); return true; @@ -667,7 +666,10 @@ impl Input { .as_ref(ctx) .active_conversation(self.terminal_view_id) else { - show_error_toast("No active conversation to export".to_owned(), ctx); + show_error_toast( + i18n::t("terminal.slash_commands.no_active_conversation_to_export"), + ctx, + ); return true; }; @@ -680,8 +682,8 @@ impl Input { // Show a toast to confirm the export let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::default(String::from( - "Conversation exported to clipboard", + let toast = DismissibleToast::default(i18n::t( + "terminal.slash_commands.conversation_exported_to_clipboard", )); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); @@ -697,7 +699,7 @@ impl Input { #[cfg(target_family = "wasm")] { show_error_toast( - "Export conversation to file unsupported in web".to_owned(), + i18n::t("terminal.slash_commands.export_to_file_unsupported_web"), ctx, ); return true; @@ -869,7 +871,10 @@ impl Input { .shared_session_status() .is_sharer_or_viewer() { - show_error_toast("Session is already being shared".to_owned(), ctx); + show_error_toast( + i18n::t("terminal.slash_commands.session_already_shared"), + ctx, + ); return true; } ctx.emit(Event::StartRemoteControl); @@ -881,17 +886,17 @@ impl Input { .active_conversation(self.terminal_view_id); if conversation.is_none() { show_error_toast( - "Cannot show conversation cost: no active conversation".to_owned(), + i18n::t("terminal.slash_commands.cost_no_active_conversation"), ctx, ); } else if conversation.is_some_and(|c| c.is_empty()) { show_error_toast( - "Cannot show conversation cost: conversation is empty".to_owned(), + i18n::t("terminal.slash_commands.cost_conversation_empty"), ctx, ); } else if conversation.is_some_and(|c| !c.status().is_done()) { show_error_toast( - "Cannot show conversation cost: conversation is in progress".to_owned(), + i18n::t("terminal.slash_commands.cost_conversation_in_progress"), ctx, ); } else { @@ -935,7 +940,11 @@ impl Input { .as_ref(ctx) .selected_conversation_id(ctx) else { - show_error_toast("/fork requires an active conversation".to_owned(), ctx); + show_error_toast( + i18n::t("terminal.slash_commands.active_conversation_required") + .replace("{command}", "/fork"), + ctx, + ); return true; }; @@ -966,7 +975,8 @@ impl Input { .selected_conversation_id(ctx) else { show_error_toast( - "/continue-locally requires an active conversation".to_owned(), + i18n::t("terminal.slash_commands.active_conversation_required") + .replace("{command}", "/continue-locally"), ctx, ); return true; @@ -974,7 +984,8 @@ impl Input { if !conversation_is_cloud_oz_for_slash_command(conversation_id, ctx) { show_error_toast( - "/continue-locally is only available for cloud Oz conversations".to_owned(), + i18n::t("terminal.slash_commands.cloud_oz_only") + .replace("{command}", "/continue-locally"), ctx, ); return true; @@ -1007,7 +1018,8 @@ impl Input { .selected_conversation_id(ctx) else { show_error_toast( - "/fork-and-compact requires an active conversation".to_owned(), + i18n::t("terminal.slash_commands.active_conversation_required") + .replace("{command}", "/fork-and-compact"), ctx, ); return true; @@ -1036,7 +1048,8 @@ impl Input { .is_none() { show_error_toast( - "/compact-and requires an active conversation".to_owned(), + i18n::t("terminal.slash_commands.active_conversation_required") + .replace("{command}", "/compact-and"), ctx, ); return true; @@ -1053,12 +1066,20 @@ impl Input { .as_ref(ctx) .selected_conversation_id(ctx) else { - show_error_toast("/queue requires an active conversation".to_owned(), ctx); + show_error_toast( + i18n::t("terminal.slash_commands.active_conversation_required") + .replace("{command}", "/queue"), + ctx, + ); return true; }; let Some(prompt) = argument.filter(|a| !a.is_empty()).cloned() else { - show_error_toast("/queue requires a prompt argument".to_owned(), ctx); + show_error_toast( + i18n::t("terminal.slash_commands.prompt_argument_required") + .replace("{command}", "/queue"), + ctx, + ); return true; }; diff --git a/app/src/terminal/input/slash_commands/search_item.rs b/app/src/terminal/input/slash_commands/search_item.rs index 8923e27048..4830b6032d 100644 --- a/app/src/terminal/input/slash_commands/search_item.rs +++ b/app/src/terminal/input/slash_commands/search_item.rs @@ -95,7 +95,7 @@ impl SearchItem for InlineItem { .with_child(name_text.finish()) .with_child( Text::new( - " or ", + i18n::t("common.or"), appearance.ui_font_family(), inline_styles::font_size(appearance), ) diff --git a/app/src/terminal/input/suggestions_mode_menu.rs b/app/src/terminal/input/suggestions_mode_menu.rs index 4db694d7e0..0a1451dbde 100644 --- a/app/src/terminal/input/suggestions_mode_menu.rs +++ b/app/src/terminal/input/suggestions_mode_menu.rs @@ -15,10 +15,9 @@ use warpui::presenter::ChildView; use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles}; use super::{ - DynamicEnumSuggestionStatus, Input, InputAction, MenuPositioning, DYNAMIC_ENUM_FAILURE_MESSAGE, - DYNAMIC_ENUM_GENERATE_MESSAGE, DYNAMIC_ENUM_HORIZONTAL_TEXT_PADDING, - DYNAMIC_ENUM_MENU_HEIGHT_OFFSET, DYNAMIC_ENUM_MENU_PADDING, DYNAMIC_ENUM_NO_RESULTS_MESSAGE, - DYNAMIC_ENUM_PENDING_MESSAGE, DYNAMIC_ENUM_RUN_MESSAGE, HISTORY_DETAILS_VIEW_WIDTH_REQUIREMENT, + DynamicEnumSuggestionStatus, Input, InputAction, MenuPositioning, + DYNAMIC_ENUM_HORIZONTAL_TEXT_PADDING, DYNAMIC_ENUM_MENU_HEIGHT_OFFSET, + DYNAMIC_ENUM_MENU_PADDING, HISTORY_DETAILS_VIEW_WIDTH_REQUIREMENT, RUN_DYNAMIC_ENUM_COMMAND_KEYSTROKE, TERMINAL_VIEW_PADDING_LEFT, }; use crate::appearance::Appearance; @@ -153,16 +152,22 @@ impl Input { SuggestionsResizeConfig::WidthOnly, ), DynamicEnumSuggestionStatus::Pending => ( - self.render_dynamic_enum_status_message(DYNAMIC_ENUM_PENDING_MESSAGE, appearance), + self.render_dynamic_enum_status_message( + i18n::t("terminal.input.dynamic_enum.pending"), + appearance, + ), SuggestionsResizeConfig::WidthAndHeight, ), DynamicEnumSuggestionStatus::Failure => ( - self.render_dynamic_enum_status_message(DYNAMIC_ENUM_FAILURE_MESSAGE, appearance), + self.render_dynamic_enum_status_message( + i18n::t("terminal.input.dynamic_enum.failure"), + appearance, + ), SuggestionsResizeConfig::WidthAndHeight, ), DynamicEnumSuggestionStatus::Success if suggestions.is_empty() => ( self.render_dynamic_enum_status_message( - DYNAMIC_ENUM_NO_RESULTS_MESSAGE, + i18n::t("terminal.input.dynamic_enum.no_results"), appearance, ), SuggestionsResizeConfig::WidthAndHeight, @@ -290,7 +295,7 @@ impl Input { ConstrainedBox::new( Container::new( Text::new_inline( - String::from(DYNAMIC_ENUM_GENERATE_MESSAGE), + i18n::t("terminal.input.dynamic_enum.generate"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -341,7 +346,7 @@ impl Input { 1., Container::new( Text::new_inline( - String::from(DYNAMIC_ENUM_RUN_MESSAGE), + i18n::t("terminal.input.dynamic_enum.run"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -365,7 +370,7 @@ impl Input { /// Renders a status message for dynamic enum suggestions. fn render_dynamic_enum_status_message( &self, - message: &str, + message: String, appearance: &Appearance, ) -> Box { let theme = appearance.theme(); @@ -373,7 +378,7 @@ impl Input { Align::new( Container::new( Text::new_inline( - String::from(message), + message, appearance.monospace_font_family(), appearance.monospace_font_size(), ) diff --git a/app/src/terminal/input/terminal_message_bar.rs b/app/src/terminal/input/terminal_message_bar.rs index dc5bfdeed2..f5fc4573a2 100644 --- a/app/src/terminal/input/terminal_message_bar.rs +++ b/app/src/terminal/input/terminal_message_bar.rs @@ -202,7 +202,7 @@ impl MessageProvider> for AgentMessageProducer { key: "enter".to_owned(), ..Default::default() }), - MessageItem::text(" new conversation"), + MessageItem::text(i18n::t("terminal.message_bar.new_conversation")), ]) .with_color(message_magenta(theme)), ) @@ -233,7 +233,7 @@ impl MessageProvider> for PlanMessageProducer { key: "enter".to_owned(), ..Default::default() }), - MessageItem::text(" plan with agent"), + MessageItem::text(i18n::t("terminal.message_bar.plan_with_agent")), ]) .with_color(message_magenta(theme)), ) @@ -255,7 +255,7 @@ impl MessageProvider> for ContinueConversationMessagePro let keystroke = keybinding_name_to_keystroke(commands::CONVERSATIONS.name, args.app)?; Some(Message::new(vec![ MessageItem::keystroke(keystroke), - MessageItem::text(" to continue conversation"), + MessageItem::text(i18n::t("terminal.message_bar.to_continue_conversation")), ])) } } @@ -342,12 +342,12 @@ impl MessageProvider> for DefaultMessageProducer { if let Some(keystroke) = keystroke { Some(Message::new(vec![ MessageItem::keystroke(keystroke), - MessageItem::text(" new /agent conversation"), + MessageItem::text(i18n::t("terminal.message_bar.new_agent_conversation")), ])) } else { - Some(Message::new(vec![MessageItem::text( - "/agent for new conversation", - )])) + Some(Message::new(vec![MessageItem::text(i18n::t( + "terminal.message_bar.agent_for_new_conversation", + ))])) } } } @@ -360,14 +360,21 @@ impl MessageProvider> for InlineHistoryMessageProduce ..Default::default() }); let items = match selected { - Some(AcceptHistoryItem::Command { .. }) => { - vec![enter, MessageItem::text(" to execute")] - } - Some(AcceptHistoryItem::AIPrompt { .. }) => { - vec![enter, MessageItem::text(" to send")] - } + Some(AcceptHistoryItem::Command { .. }) => vec![ + enter, + MessageItem::text(i18n::t("terminal.message_bar.to_execute")), + ], + Some(AcceptHistoryItem::AIPrompt { .. }) => vec![ + enter, + MessageItem::text(i18n::t("terminal.message_bar.to_send")), + ], Some(AcceptHistoryItem::Conversation { title, .. }) => { - vec![enter, MessageItem::text(format!(" to open '{title}'"))] + vec![ + enter, + MessageItem::text( + i18n::t("terminal.message_bar.to_open").replace("{title}", title), + ), + ] } None => { vec![MessageItem::text("")] @@ -400,9 +407,9 @@ impl MessageTransformer> for AutodetectedPromptMessageTr }); message.items.extend([ - MessageItem::text(" (autodetected) "), + MessageItem::text(i18n::t("terminal.message_bar.autodetected")), MessageItem::keystroke(set_terminal_mode_keystroke), - MessageItem::text(" to override"), + MessageItem::text(i18n::t("terminal.message_bar.to_override")), ]); } message.set_color(message_magenta(Appearance::as_ref(args.app).theme())); @@ -427,16 +434,22 @@ impl MessageTransformer> for AttachedBlocksMessageTransf }; if context_block_ids.len() == 1 { - message.append_text(format!(" with `{}` attached", block_command).as_str()); + message.append_text( + i18n::t("terminal.message_bar.block_attached") + .replace("{command}", &block_command) + .as_str(), + ); } else { let text = if context_block_ids.len() == 2 { - format!(" with `{}` and 1 other command attached", block_command) + i18n::t("terminal.message_bar.block_and_one_attached") + .replace("{command}", &block_command) } else { - format!( - " with `{}` and {} other commands attached", - block_command, - context_block_ids.len().saturating_sub(1) - ) + i18n::t("terminal.message_bar.block_and_many_attached") + .replace("{command}", &block_command) + .replace( + "{count}", + &context_block_ids.len().saturating_sub(1).to_string(), + ) }; message.append_text(text.as_str()); } @@ -453,7 +466,7 @@ impl MessageTransformer> for AttachedTextSelectionMessag { return false; } - message.append_text(" with text selection attached"); + message.append_text(i18n::t("terminal.message_bar.text_selection_attached").as_str()); true } } diff --git a/app/src/terminal/input/user_query/mod.rs b/app/src/terminal/input/user_query/mod.rs index a6b3d92167..d9c359653d 100644 --- a/app/src/terminal/input/user_query/mod.rs +++ b/app/src/terminal/input/user_query/mod.rs @@ -34,7 +34,7 @@ impl InlineMenuAction for SelectUserQuery { key: "enter".to_owned(), ..Default::default() }), - MessageItem::text(" current pane"), + MessageItem::text(i18n::t("terminal.message_bar.current_pane")), ], move |ctx| { ctx.dispatch_typed_action(InlineMenuRowAction::Accept { @@ -63,7 +63,7 @@ impl InlineMenuAction for SelectUserQuery { items.push(MessageItem::clickable( vec![ MessageItem::keystroke(modifier_keystroke), - MessageItem::text(" new pane"), + MessageItem::text(i18n::t("terminal.message_bar.new_pane")), ], move |ctx| { ctx.dispatch_typed_action(InlineMenuRowAction::Accept { diff --git a/app/src/terminal/input/user_query/search_item.rs b/app/src/terminal/input/user_query/search_item.rs index e191629892..6ed251c30d 100644 --- a/app/src/terminal/input/user_query/search_item.rs +++ b/app/src/terminal/input/user_query/search_item.rs @@ -132,6 +132,6 @@ impl SearchItem for UserQuerySearchItem { } fn accessibility_label(&self) -> String { - format!("Query: {}", self.query_text) + i18n::t("terminal.input.a11y.query").replace("{query}", &self.query_text) } } diff --git a/app/src/terminal/keys_settings.rs b/app/src/terminal/keys_settings.rs index fc0053b687..00c0eb67ec 100644 --- a/app/src/terminal/keys_settings.rs +++ b/app/src/terminal/keys_settings.rs @@ -19,7 +19,7 @@ define_settings_group!(KeysSettings, settings: [ private: false, toml_path: "global_hotkey.dedicated_window.settings", max_table_depth: 2, - description: "Configuration options for Quake Mode window behavior.", + description_key: "settings.schema.global_hotkey.dedicated_window.settings.description", }, quake_mode_enabled: QuakeModeEnabled { type: bool, @@ -28,7 +28,7 @@ define_settings_group!(KeysSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "global_hotkey.dedicated_window.enabled", - description: "Whether the dedicated hotkey window is enabled. Mutually exclusive with `global_hotkey.toggle_all_windows.enabled`; only one should be true at a time.", + description_key: "settings.schema.global_hotkey.dedicated_window.enabled.description", }, activation_hotkey_enabled: ActivationHotkeyEnabled { type: bool, @@ -37,7 +37,7 @@ define_settings_group!(KeysSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "global_hotkey.toggle_all_windows.enabled", - description: "Whether the hotkey that toggles visibility of all windows is enabled. Mutually exclusive with `global_hotkey.dedicated_window.enabled`; only one should be true at a time.", + description_key: "settings.schema.global_hotkey.toggle_all_windows.enabled.description", }, activation_hotkey_keybinding: ActivationHotkeyKeybinding { type: Option, @@ -46,7 +46,7 @@ define_settings_group!(KeysSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "global_hotkey.toggle_all_windows.keybinding", - description: "The keybinding used for the global activation hotkey. Format: modifiers (cmd, ctrl, alt, shift, meta) and a key joined by '-', e.g. \"cmd-shift-a\" or \"alt-enter\". Bindings are case-sensitive: when shift is present, the key must be its shifted form (e.g., \"ctrl-shift-E\", not \"ctrl-shift-e\").", + description_key: "settings.schema.global_hotkey.toggle_all_windows.keybinding.description", } extra_meta_keys: ExtraMetaKeys { type: ExtraMetaKeysEnum, @@ -55,7 +55,7 @@ define_settings_group!(KeysSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.input.extra_meta_keys", - description: "Controls which additional keys are treated as meta keys.", + description_key: "settings.schema.terminal.input.extra_meta_keys.description", } ctrl_tab_behavior: CtrlTabBehaviorSetting { type: CtrlTabBehavior, @@ -64,7 +64,7 @@ define_settings_group!(KeysSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "keys.ctrl_tab_behavior_setting", - description: "Controls the behavior of Ctrl+Tab.", + description_key: "settings.schema.keys.ctrl_tab_behavior_setting.description", } ]); diff --git a/app/src/terminal/ligature_settings.rs b/app/src/terminal/ligature_settings.rs index 6ae82b6e0b..57627319e5 100644 --- a/app/src/terminal/ligature_settings.rs +++ b/app/src/terminal/ligature_settings.rs @@ -12,7 +12,7 @@ define_settings_group!(LigatureSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "appearance.text.ligature_rendering_enabled", - description: "Whether to render font ligatures in the terminal.", + description_key: "settings.schema.appearance.text.ligature_rendering_enabled.description", }, ]); diff --git a/app/src/terminal/local_tty/terminal_manager.rs b/app/src/terminal/local_tty/terminal_manager.rs index 0191da6f59..9056172307 100644 --- a/app/src/terminal/local_tty/terminal_manager.rs +++ b/app/src/terminal/local_tty/terminal_manager.rs @@ -113,8 +113,6 @@ type PtyController = writeable_pty::PtyController>; type RemoteServerController = writeable_pty::remote_server_controller::RemoteServerController>; -const ACL_UPDATE_FAILURE_RESPONSE: &str = "Something went wrong. Please try again."; - /// Whether the given CRDT operation should be dropped when broadcasting /// sharer input to viewers. In ambient agent sessions the sharer is a /// headless worker — forwarding its selection ops would produce a phantom @@ -1671,7 +1669,7 @@ impl TerminalManager { terminal_view.update(ctx, |view, ctx| { view.show_persistent_toast( - "Something went wrong. Please try sharing again.".to_string(), + i18n::t("terminal.shared_session.error.internal"), ToastFlavor::Error, ctx, ); @@ -2050,7 +2048,7 @@ impl TerminalManager { } LinkAccessLevelUpdateResponse::Error => { let reason_string = - "Failed to update permissions for shared session".to_owned(); + i18n::t("terminal.shared_session.error.update_permissions_failed"); view.show_persistent_toast(reason_string, ToastFlavor::Error, ctx); } }); @@ -2074,7 +2072,7 @@ impl TerminalManager { } TeamAccessLevelUpdateResponse::Error(_) => { view.show_persistent_toast( - ACL_UPDATE_FAILURE_RESPONSE.to_owned(), + i18n::t("common.something_went_wrong_try_again"), crate::view_components::ToastFlavor::Error, ctx, ); @@ -2093,7 +2091,7 @@ impl TerminalManager { if let RemoveGuestResponse::Error(_) = response { terminal_view.update(ctx, |view, ctx| { view.show_persistent_toast( - ACL_UPDATE_FAILURE_RESPONSE.to_owned(), + i18n::t("common.something_went_wrong_try_again"), crate::view_components::ToastFlavor::Error, ctx, ); @@ -2104,7 +2102,7 @@ impl TerminalManager { if let UpdatePendingUserRoleResponse::Error(_) = response { terminal_view.update(ctx, |view, ctx| { view.show_persistent_toast( - ACL_UPDATE_FAILURE_RESPONSE.to_owned(), + i18n::t("common.something_went_wrong_try_again"), crate::view_components::ToastFlavor::Error, ctx, ); diff --git a/app/src/terminal/profile_model_selector.rs b/app/src/terminal/profile_model_selector.rs index f815db8f8d..9c112a766b 100644 --- a/app/src/terminal/profile_model_selector.rs +++ b/app/src/terminal/profile_model_selector.rs @@ -81,12 +81,6 @@ const MAX_PROFILE_NAME_WIDTH_SCALE_FACTOR: f32 = 10.0; const PROFILE_SELECTOR_POSITION_ID: &str = "profile_selector"; -const PROFILE_PICKER_TOOLTIP: &str = "Choose an AI execution profile"; -const MODEL_PICKER_TOOLTIP: &str = "Choose an agent model"; -const MODEL_LOCKED_FOR_FOLLOWUP_TOOLTIP: &str = "Follow-ups use the original run's model"; -const MODEL_REQUIRES_EDIT_ACCESS_TOOLTIP: &str = "Request edit access to change model"; -const HARNESS_DEFAULT_MODEL_LABEL: &str = "default"; - pub fn calculate_scaled_font_size(appearance: &warp_core::ui::appearance::Appearance) -> f32 { if FeatureFlag::AgentView.is_enabled() { udi_font_size(appearance) @@ -267,7 +261,7 @@ impl ProfileModelSelector { ), is_blurred: false, }) - .with_tooltip(PROFILE_PICKER_TOOLTIP) + .with_tooltip(i18n::t("terminal.profile_model_selector.profile_tooltip")) .with_size(ButtonSize::UDIButton) .with_icon(Icon::Psychology) }); @@ -295,14 +289,14 @@ impl ProfileModelSelector { ), is_blurred: false, }) - .with_tooltip(MODEL_PICKER_TOOLTIP) + .with_tooltip(i18n::t("terminal.profile_model_selector.model_tooltip")) .with_size(ButtonSize::UDIButton) }); let profile_compact_button = ctx.add_typed_action_view(|_| { ActionButton::new("", PromptIconButtonTheme::new(false)) .with_icon(Icon::Psychology) - .with_tooltip(PROFILE_PICKER_TOOLTIP) + .with_tooltip(i18n::t("terminal.profile_model_selector.profile_tooltip")) .with_size(ButtonSize::UDIButton) .on_click(|ctx| { ctx.dispatch_typed_action(ProfileModelSelectorAction::ToggleProfileMenu); @@ -312,7 +306,7 @@ impl ProfileModelSelector { let model_compact_button = ctx.add_typed_action_view(|_| { ActionButton::new("", PromptIconButtonTheme::new(false)) .with_icon(Icon::Neurology) - .with_tooltip(MODEL_PICKER_TOOLTIP) + .with_tooltip(i18n::t("terminal.profile_model_selector.model_tooltip")) .with_size(ButtonSize::UDIButton) .on_click(|ctx| { ctx.dispatch_typed_action(ProfileModelSelectorAction::ToggleModelMenu); @@ -349,9 +343,9 @@ impl ProfileModelSelector { .iter() .map(|name| { if *name == "auto" { - "auto-select the best model for the task" + i18n::t("terminal.profile_model_selector.auto_select_best_model") } else { - name + name.to_string() } }) .collect::>() @@ -361,7 +355,7 @@ impl ProfileModelSelector { } label } else { - "New models available".to_string() + i18n::t("terminal.profile_model_selector.new_models_available") } }))) }); @@ -540,8 +534,8 @@ impl ProfileModelSelector { ); let manage_api_key_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Manage", SecondaryTheme) - .with_tooltip("Manage API keys") + ActionButton::new(i18n::t("common.manage"), SecondaryTheme) + .with_tooltip(i18n::t("terminal.profile_model_selector.manage_api_keys")) .with_size(ButtonSize::XSmall) .on_click(|ctx| { ctx.dispatch_typed_action(WorkspaceAction::ShowSettingsPageWithSearch { @@ -706,18 +700,20 @@ impl ProfileModelSelector { // Non-Oz runs lock silently: the harness owns model selection, and the // user already knows that, so no tooltip is shown. - let model_tooltip: Option<&str> = if self.is_locked_for_cloud_followup(ctx) { - Some(MODEL_LOCKED_FOR_FOLLOWUP_TOOLTIP) + let model_tooltip: Option = if self.is_locked_for_cloud_followup(ctx) { + Some(i18n::t( + "terminal.profile_model_selector.model_locked_followup_tooltip", + )) } else if self.is_locked_for_non_oz_run(ctx) { None } else { - Some(MODEL_PICKER_TOOLTIP) + Some(i18n::t("terminal.profile_model_selector.model_tooltip")) }; let locked = self.is_model_locked(ctx); self.model_button.update(ctx, |button, ctx| { button.set_label(model_name, ctx); button.set_disabled(locked, ctx); - match model_tooltip { + match model_tooltip.clone() { Some(t) => button.set_tooltip(Some(t), ctx), None => button.clear_tooltip(ctx), } @@ -813,12 +809,13 @@ impl ProfileModelSelector { let appearance = Appearance::as_ref(ctx); let mut menu_items = vec![ MenuItem::Header { - fields: MenuItemFields::new("Profiles").with_override_text_color( - appearance - .theme() - .sub_text_color(appearance.theme().background()) - .into_solid(), - ), + fields: MenuItemFields::new(i18n::t("terminal.profile_selector.profiles")) + .with_override_text_color( + appearance + .theme() + .sub_text_color(appearance.theme().background()) + .into_solid(), + ), clickable: false, right_side_fields: None, }, @@ -844,7 +841,7 @@ impl ProfileModelSelector { menu_items.push(MenuItem::Separator); menu_items.push(MenuItem::Item( - MenuItemFields::new("Manage profiles") + MenuItemFields::new(i18n::t("terminal.profile_selector.manage_profiles")) .with_icon(Icon::Gear) .with_on_select_action(ProfileModelSelectorAction::ManageProfiles), )); @@ -872,7 +869,7 @@ impl ProfileModelSelector { fn harness_model_display_name(&self, app: &AppContext) -> String { self.active_harness_model_info(app) .map(|info| info.display_name.clone()) - .unwrap_or_else(|| HARNESS_DEFAULT_MODEL_LABEL.to_string()) + .unwrap_or_else(|| i18n::t("common.default")) } fn refresh_harness_model_menu(&mut self, ctx: &mut ViewContext) { @@ -900,7 +897,7 @@ impl ProfileModelSelector { reasoning_level: None, }; let mut default_fields = - MenuItemFields::new(HARNESS_DEFAULT_MODEL_LABEL).with_on_select_action(default_action); + MenuItemFields::new(i18n::t("common.default")).with_on_select_action(default_action); if default_selected { default_fields = default_fields.with_icon(Icon::Check); } else { @@ -1055,12 +1052,13 @@ impl ProfileModelSelector { items.push(MenuItem::Separator); } items.push(MenuItem::Header { - fields: MenuItemFields::new("Custom models").with_override_text_color( - appearance - .theme() - .sub_text_color(appearance.theme().background()) - .into_solid(), - ), + fields: MenuItemFields::new(i18n::t("terminal.profile_selector.custom_models")) + .with_override_text_color( + appearance + .theme() + .sub_text_color(appearance.theme().background()) + .into_solid(), + ), clickable: false, right_side_fields: None, }); @@ -1601,7 +1599,7 @@ impl ProfileModelSelector { let tooltip = appearance .ui_builder() - .tool_tip(PROFILE_PICKER_TOOLTIP.to_owned()); + .tool_tip(i18n::t("terminal.profile_model_selector.profile_tooltip")); let mut stack = Stack::new(); stack.add_child(button_with_hover); stack.add_positioned_overlay_child( @@ -1749,7 +1747,7 @@ impl ProfileModelSelector { let tooltip = appearance .ui_builder() - .tool_tip(MODEL_PICKER_TOOLTIP.to_owned()); + .tool_tip(i18n::t("terminal.profile_model_selector.model_tooltip")); let mut stack = Stack::new(); stack.add_child(button_with_hover); stack.add_positioned_overlay_child( @@ -1764,16 +1762,20 @@ impl ProfileModelSelector { stack.finish() } else if state.is_hovered() { // Non-Oz runs lock silently — skip the tooltip entirely. - let tooltip_text: Option<&str> = if is_locked_for_followup { - Some(MODEL_LOCKED_FOR_FOLLOWUP_TOOLTIP) + let tooltip_text: Option = if is_locked_for_followup { + Some(i18n::t( + "terminal.profile_model_selector.model_locked_followup_tooltip", + )) } else if is_locked_for_non_oz { None } else { - Some(MODEL_REQUIRES_EDIT_ACCESS_TOOLTIP) + Some(i18n::t( + "terminal.profile_model_selector.model_requires_edit_access_tooltip", + )) }; if let Some(text) = tooltip_text { - let tooltip = appearance.ui_builder().tool_tip(text.to_owned()); + let tooltip = appearance.ui_builder().tool_tip(text); let mut stack = Stack::new(); stack.add_child(button_with_save_position); stack.add_positioned_overlay_child( @@ -1933,7 +1935,9 @@ impl ProfileModelSelector { Flex::row() .with_main_axis_size(MainAxisSize::Max) .with_cross_axis_alignment(CrossAxisAlignment::Center) - .with_child(self.render_model_spec_value_label("Cost".to_string(), app)) + .with_child( + self.render_model_spec_value_label(i18n::t("terminal.model_specs.cost"), app), + ) .with_child( Expanded::new( 1., @@ -1944,7 +1948,7 @@ impl ProfileModelSelector { .with_child( Container::new( Text::new( - "Billed to API".to_string(), + i18n::t("terminal.profile_selector.billed_to_api"), appearance.ui_font_family(), 14., ) @@ -1974,18 +1978,23 @@ impl ProfileModelSelector { ) -> Box { let mut spec_values = vec![ self.render_model_spec_value( - "Intelligence".to_string(), + i18n::t("terminal.model_specs.intelligence"), spec.quality, bg_bar_color, app, ), - self.render_model_spec_value("Speed".to_string(), spec.speed, bg_bar_color, app), + self.render_model_spec_value( + i18n::t("terminal.model_specs.speed"), + spec.speed, + bg_bar_color, + app, + ), ]; if is_using_api_key { spec_values.push(self.render_model_spec_api_key(app)); } else { spec_values.push(self.render_model_spec_value( - "Cost".to_string(), + i18n::t("terminal.model_specs.cost"), spec.cost, bg_bar_color, app, @@ -2004,8 +2013,8 @@ impl ProfileModelSelector { let appearance = Appearance::as_ref(app); let theme = appearance.theme(); let header = self.render_model_spec_header( - "Model Specs".to_string(), - "Warp’s benchmarks for how well a model performs in our harness, the rate at which it consumes credits, and task speed.".to_string(), + i18n::t("terminal.model_specs.title"), + i18n::t("terminal.model_specs.description"), app, ); let spec = self.render_all_model_spec_values( @@ -2044,16 +2053,16 @@ impl ProfileModelSelector { let (title, description) = match kind { ModelSpecSidecarKind::Auto => ( - "Auto mode", - "Auto will select the best model for the task. Cost-efficiency optimizes for cost, Responsiveness optimizes for response speed.", + i18n::t("terminal.profile_model_selector.auto_mode_title"), + i18n::t("terminal.profile_model_selector.auto_mode_description"), ), ModelSpecSidecarKind::Reasoning => ( - "Reasoning level", - "Increased reasoning levels consume more credits and have higher latency, but higher performance for complicated tasks.", + i18n::t("terminal.profile_model_selector.reasoning_level_title"), + i18n::t("terminal.profile_model_selector.reasoning_level_description"), ), }; - let header = self.render_model_spec_header(title.to_string(), description.to_string(), app); + let header = self.render_model_spec_header(title, description, app); let sidecar_menu = ChildView::new(&self.model_spec_sidecar.dropdown).finish(); let spec_values = self.render_all_model_spec_values( &spec.clone().unwrap_or_default(), diff --git a/app/src/terminal/prompt_render_helper.rs b/app/src/terminal/prompt_render_helper.rs index 8514cb570e..54195afbc4 100644 --- a/app/src/terminal/prompt_render_helper.rs +++ b/app/src/terminal/prompt_render_helper.rs @@ -248,31 +248,43 @@ impl PromptRenderHelper { if let Some(pending_session_id) = model.pending_session_id() { if let Some(state) = sessions.remote_server_setup_state(pending_session_id) { return match state { - RemoteServerSetupState::Checking => "Starting shell...".to_string(), + RemoteServerSetupState::Checking => { + i18n::t("terminal.remote_server.loading.starting_shell") + } RemoteServerSetupState::Installing { progress_percent: Some(p), - } => format!("Installing Warp SSH Extension... ({p}%)"), + } => { + i18n::t("terminal.remote_server.loading.installing_ssh_extension_progress") + .replace("{percent}", &p.to_string()) + } RemoteServerSetupState::Installing { progress_percent: None, - } => "Installing Warp SSH Extension...".to_string(), + } => i18n::t("terminal.remote_server.loading.installing_ssh_extension"), RemoteServerSetupState::Updating => { - "Updating Warp SSH Extension...".to_string() + i18n::t("terminal.remote_server.loading.updating_ssh_extension") + } + RemoteServerSetupState::Initializing => { + i18n::t("terminal.remote_server.loading.initializing") + } + RemoteServerSetupState::Ready => { + i18n::t("terminal.remote_server.loading.starting_shell") } - RemoteServerSetupState::Initializing => "Initializing...".to_string(), - RemoteServerSetupState::Ready => "Starting shell...".to_string(), // Failed and Unsupported both fall back to the legacy SSH // flow, so we render the same generic prompt as a normal // SSH session that doesn't have the remote-server extension. RemoteServerSetupState::Failed { .. } - | RemoteServerSetupState::Unsupported { .. } => "Starting shell...".to_string(), + | RemoteServerSetupState::Unsupported { .. } => { + i18n::t("terminal.remote_server.loading.starting_shell") + } }; } } if !sessions.is_empty() { - "Starting shell...".to_string() + i18n::t("terminal.remote_server.loading.starting_shell") } else { - format!("Starting {}...", model.shell_launch_state().display_name()) + i18n::t("terminal.loading.starting_shell_named") + .replace("{shell}", model.shell_launch_state().display_name()) } } @@ -431,7 +443,7 @@ impl PromptRenderHelper { let prompt = PromptAndPadding { element: PromptAndPaddingElement::Text(Box::new( Text::new_inline( - "Loading prompt...", + i18n::t("terminal.loading_prompt"), appearance.monospace_font_family(), appearance.monospace_font_size(), ) diff --git a/app/src/terminal/recorder.rs b/app/src/terminal/recorder.rs index 2f571df1b6..f7e72efc08 100644 --- a/app/src/terminal/recorder.rs +++ b/app/src/terminal/recorder.rs @@ -103,7 +103,7 @@ impl PtyRecorder { let display_path = warp_core::paths::home_relative_path(path); let file_path = path.to_owned(); self.show_toast( - format!("PTY recording started: {display_path}"), + i18n::t("terminal.recorder.started").replace("{path}", display_path.as_ref()), Some(file_path), ctx, ); @@ -112,7 +112,7 @@ impl PtyRecorder { let display_path = warp_core::paths::home_relative_path(&self.path); self.stop_recording(); self.show_toast( - format!("PTY recording stopped: {display_path}"), + i18n::t("terminal.recorder.stopped").replace("{path}", display_path.as_ref()), Some(self.path.clone()), ctx, ); @@ -164,7 +164,7 @@ impl PtyRecorder { let path_str = path.to_string_lossy().into_owned(); toast = toast .with_link( - ToastLink::new("Open".to_string()) + ToastLink::new(i18n::t("common.open")) .with_onclick_action(WorkspaceAction::OpenInExplorer { path }), ) .with_on_body_click(move |ctx| { diff --git a/app/src/terminal/rich_history.rs b/app/src/terminal/rich_history.rs index 0db7141a11..b4e8e09d44 100644 --- a/app/src/terminal/rich_history.rs +++ b/app/src/terminal/rich_history.rs @@ -41,7 +41,8 @@ pub fn render_rich_history(entry: &HistoryEntry, ctx: &AppContext) -> Box Box Box &'static str { + pub fn display_name(self) -> String { match self { - SecretDisplayMode::Asterisks => "Asterisks", - SecretDisplayMode::Strikethrough => "Strikethrough", - SecretDisplayMode::AlwaysShow => "Always show secrets", + SecretDisplayMode::Asterisks => { + i18n::t("settings.privacy.secret_display_mode.asterisks") + } + SecretDisplayMode::Strikethrough => { + i18n::t("settings.privacy.secret_display_mode.strikethrough") + } + SecretDisplayMode::AlwaysShow => { + i18n::t("settings.privacy.secret_display_mode.always_show") + } } } @@ -78,7 +84,7 @@ define_settings_group!(SafeModeSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "privacy.secret_redaction.enabled", - description: "Whether secret redaction is enabled to detect and obscure secrets in terminal output.", + description_key: "settings.schema.privacy.secret_redaction.enabled.description", }, secret_display_mode: SecretDisplayModeSetting { type: SecretDisplayMode, @@ -87,7 +93,7 @@ define_settings_group!(SafeModeSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "privacy.secret_redaction.secret_display_mode_setting", - description: "Controls how detected secrets are visually displayed in the terminal.", + description_key: "settings.schema.privacy.secret_redaction.secret_display_mode_setting.description", }, // Keep legacy setting for backward compatibility during migration hide_secrets_in_block_list: HideSecretsInBlockList { @@ -97,7 +103,7 @@ define_settings_group!(SafeModeSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "privacy.secret_redaction.hide_secrets_in_block_list", - description: "Whether to hide detected secrets in the block list using asterisks.", + description_key: "settings.schema.privacy.secret_redaction.hide_secrets_in_block_list.description", }, ]); diff --git a/app/src/terminal/session_settings.rs b/app/src/terminal/session_settings.rs index 88f6a13204..3ab83416f9 100644 --- a/app/src/terminal/session_settings.rs +++ b/app/src/terminal/session_settings.rs @@ -283,7 +283,7 @@ define_settings_group!(SessionSettings, settings: [ sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "session.startup_shell_override", - description: "The shell to use when Warp starts up.", + description_key: "settings.schema.session.startup_shell_override.description", }, new_session_shell_override: NewSessionShellOverride { type: Option, @@ -292,7 +292,7 @@ define_settings_group!(SessionSettings, settings: [ sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "session.new_session_shell_override", - description: "The shell to use when opening a new session.", + description_key: "settings.schema.session.new_session_shell_override.description", } honor_ps1: HonorPS1 { type: bool, @@ -301,7 +301,7 @@ define_settings_group!(SessionSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.input.honor_ps1", - description: "Whether to use your shell's PS1 prompt instead of the Warp prompt.", + description_key: "settings.schema.terminal.input.honor_ps1.description", }, saved_prompt: SavedPrompt { type: PromptSelection, @@ -324,7 +324,7 @@ define_settings_group!(SessionSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "general.should_confirm_close_session", - description: "Whether to show a confirmation dialog when closing a session.", + description_key: "settings.schema.general.should_confirm_close_session.description", }, // Value is saved here but not shown in ui (can't be toggled in settings) should_confirm_shared_session_edit_access: ShouldConfirmSharedSessionEditAccess { @@ -342,7 +342,7 @@ define_settings_group!(SessionSettings, settings: [ private: false, toml_path: "notifications.preferences", max_table_depth: 1, - description: "Notification preferences for terminal events.", + description_key: "settings.schema.notifications.preferences.description", } // This is a legacy setting that we no longer allow users to toggle after // context chips were introduced. We keep it only to respect users who @@ -365,7 +365,7 @@ define_settings_group!(SessionSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.input.show_model_selectors_in_prompt", - description: "Whether to show AI model selectors in the input prompt.", + description_key: "settings.schema.agents.warp_agent.input.show_model_selectors_in_prompt.description", }, agent_footer_chip_selection: AgentToolbarChipSelectionSetting { type: AgentToolbarChipSelection, @@ -374,7 +374,7 @@ define_settings_group!(SessionSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.input.agent_toolbar_chip_selection_setting", - description: "Controls the layout of context chips in the Agent Mode toolbar.", + description_key: "settings.schema.agents.warp_agent.input.agent_toolbar_chip_selection_setting.description", }, cli_agent_footer_chip_selection: CLIAgentToolbarChipSelectionSetting { type: CLIAgentToolbarChipSelection, @@ -383,7 +383,7 @@ define_settings_group!(SessionSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.third_party.cli_agent_toolbar_chip_selection_setting", - description: "Controls the layout of context chips in the CLI Agent toolbar.", + description_key: "settings.schema.agents.third_party.cli_agent_toolbar_chip_selection_setting.description", }, notification_toast_duration_secs: NotificationToastDurationSecs { type: u64, @@ -392,7 +392,7 @@ define_settings_group!(SessionSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "notifications.toast_duration_secs", - description: "How long notification toasts are displayed, in seconds.", + description_key: "settings.schema.notifications.toast_duration_secs.description", }, // Tracks whether the `gh` CLI is installed and authenticated on this machine. // Not synced because `gh` CLI availability is machine-specific. @@ -424,5 +424,5 @@ settings::macros::implement_setting_for_enum!( private: false, toml_path: "session.working_directory_config", max_table_depth: 1, - description: "Controls the working directory used when opening new sessions.", + description_key: "settings.schema.session.working_directory_config.description", ); diff --git a/app/src/terminal/session_settings/working_directory_config.rs b/app/src/terminal/session_settings/working_directory_config.rs index 907d40dda4..30fa220c89 100644 --- a/app/src/terminal/session_settings/working_directory_config.rs +++ b/app/src/terminal/session_settings/working_directory_config.rs @@ -41,11 +41,17 @@ pub enum WorkingDirectoryMode { impl WorkingDirectoryMode { /// Returns the label that should be used for this mode when configuring /// values in the settings view. - pub fn dropdown_item_label(&self) -> &'static str { + pub fn dropdown_item_label(&self) -> String { match self { - WorkingDirectoryMode::HomeDir => "Home directory", - WorkingDirectoryMode::PreviousDir => "Previous session's directory", - WorkingDirectoryMode::CustomDir => "Custom directory", + WorkingDirectoryMode::HomeDir => { + i18n::t("terminal.session_settings.working_directory.home_directory") + } + WorkingDirectoryMode::PreviousDir => { + i18n::t("terminal.session_settings.working_directory.previous_session_directory") + } + WorkingDirectoryMode::CustomDir => { + i18n::t("terminal.session_settings.working_directory.custom_directory") + } } } } diff --git a/app/src/terminal/settings.rs b/app/src/terminal/settings.rs index e660a18e36..218d7b3c5b 100644 --- a/app/src/terminal/settings.rs +++ b/app/src/terminal/settings.rs @@ -93,7 +93,7 @@ define_settings_group!(TerminalSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.use_audible_bell", - description: "Whether to play an audible bell sound on terminal bell events.", + description_key: "settings.schema.terminal.use_audible_bell.description", }, spacing_mode: Spacing { type: SpacingMode, @@ -102,7 +102,7 @@ define_settings_group!(TerminalSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "appearance.spacing", - description: "Controls the spacing between terminal blocks.", + description_key: "settings.schema.appearance.spacing.description", } maximum_grid_size: MaximumGridSize { type: usize, @@ -111,7 +111,7 @@ define_settings_group!(TerminalSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.maximum_grid_size", - description: "The maximum number of rows in the terminal grid.", + description_key: "settings.schema.terminal.maximum_grid_size.description", }, alt_screen_padding: AltScreenPadding { type: AltScreenPaddingMode, @@ -121,7 +121,7 @@ define_settings_group!(TerminalSettings, settings: [ private: false, toml_path: "appearance.full_screen_apps.alt_screen_padding", max_table_depth: 0, - description: "Controls padding around full-screen terminal applications.", + description_key: "settings.schema.appearance.full_screen_apps.alt_screen_padding.description", }, // This field should not be referenced directly to check zero state block visibility -- use // the `should_show_zero_state_block()` getter, which also considers global AI enablement. @@ -132,7 +132,7 @@ define_settings_group!(TerminalSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "terminal.show_terminal_zero_state_block", - description: "Whether to show the AI zero-state block in new terminal sessions.", + description_key: "settings.schema.terminal.show_terminal_zero_state_block.description", }, // Opt-in toggle for running terminal find on a background thread. Only consulted on // channels where `FeatureFlag::AsyncFind` is off; channels with the flag on force the @@ -144,7 +144,7 @@ define_settings_group!(TerminalSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "experimental.async_find_enabled", - description: "Use an improved implementation of find to keep the UI responsive while searching for matches on large outputs.", + description_key: "settings.schema.experimental.async_find_enabled.description", }, ]); diff --git a/app/src/terminal/share_block_modal.rs b/app/src/terminal/share_block_modal.rs index e476e01737..205c934daa 100644 --- a/app/src/terminal/share_block_modal.rs +++ b/app/src/terminal/share_block_modal.rs @@ -65,8 +65,6 @@ const INNER_MARGIN: f32 = 20.; const MODAL_WIDTH: f32 = 862.; const BLOCK_TITLE_INPUT_WIDTH: f32 = 800.; -const BLOCK_TITLE_PLACEHOLDER: &str = "Title (optional)"; - // TODO(vorporeal): This is 12 in the specs, but I think our 14pt font is a bit // taller than 14pt? const VERTICAL_SEPARATOR_HEIGHT: f32 = 32.; @@ -76,15 +74,6 @@ const NEW_BUTTON_VERTICAL_PADDING: f32 = 10.; const NEW_BUTTON_HORIZONTAL_PADDING: f32 = 10.; const NEW_COPY_BUTTON_WIDTH: f32 = 80.; -const COMMAND_AND_OUTPUT_OPTION: (&str, DisplaySetting) = - ("Command and Output", DisplaySetting::CommandAndOutput); -const COMMAND_OPTION: (&str, DisplaySetting) = ("Command", DisplaySetting::Command); -const OUTPUT_OPTION: (&str, DisplaySetting) = ("Output", DisplaySetting::Output); - -/// This default title is helpful for screen readers. -const DEFAULT_EMBED_TITLE: &str = "embedded warp block"; -const BLOCK_CREATION_FAILED_MESSAGE: &str = "Something went wrong. Please try again."; - #[derive(PartialEq)] enum ShareRequestState { None, @@ -195,7 +184,7 @@ impl ShareBlockModal { }, ctx, ); - editor.set_placeholder_text(BLOCK_TITLE_PLACEHOLDER, ctx); + editor.set_placeholder_text(i18n::t("terminal.share_block.title_placeholder"), ctx); editor }); ctx.subscribe_to_view(&block_title_editor, move |me, _, event, ctx| { @@ -219,9 +208,21 @@ impl ShareBlockModal { ..Default::default() }; - let embed_display_options = [COMMAND_AND_OUTPUT_OPTION, COMMAND_OPTION, OUTPUT_OPTION] - .map(|(name, display_setting)| (name.to_string(), display_setting)) - .to_vec(); + let embed_display_options = [ + ( + i18n::t("terminal.share_block.display.command_and_output"), + DisplaySetting::CommandAndOutput, + ), + ( + i18n::t("terminal.share_block.display.command"), + DisplaySetting::Command, + ), + ( + i18n::t("terminal.share_block.display.output"), + DisplaySetting::Output, + ), + ] + .to_vec(); let ligature_handle = LigatureSettings::handle(ctx); ctx.subscribe_to_model(&ligature_handle, |_, _, _, ctx| ctx.notify()); @@ -382,7 +383,7 @@ impl ShareBlockModal { fn display_failure_toast(&mut self, ctx: &mut ViewContext) { ctx.emit(ShareBlockModalEvent::ShowToast { - message: BLOCK_CREATION_FAILED_MESSAGE.to_string(), + message: i18n::t("terminal.share_block.creation_failed"), flavor: ToastFlavor::Error, }); } @@ -499,7 +500,7 @@ impl ShareBlockModal { ); ctx.clipboard().write(ClipboardContent::plain_text(link)); ctx.emit(ShareBlockModalEvent::ShowToast { - message: "Link copied.".to_string(), + message: i18n::t("terminal.share_block.link_copied"), flavor: ToastFlavor::Default, }); } @@ -522,7 +523,7 @@ impl ShareBlockModal { let width = ServerBlock::embed_pixel_width(block); let mut title = self.block_title_editor.as_ref(app).buffer_text(app); if title.is_empty() { - title = DEFAULT_EMBED_TITLE.to_string(); + title = i18n::t("terminal.share_block.default_embed_title"); } Some(format!( @@ -543,7 +544,7 @@ impl ShareBlockModal { ctx.clipboard() .write(ClipboardContent::plain_text(embed_snippet)); ctx.emit(ShareBlockModalEvent::ShowToast { - message: "Embed code copied.".to_string(), + message: i18n::t("terminal.share_block.embed_copied"), flavor: ToastFlavor::Success, }); } @@ -623,7 +624,7 @@ impl ShareBlockModal { fn render_create_block_buttons_row(&self, appearance: &Appearance) -> Box { let create_link_button = self.render_create_block_button( appearance, - "Create link", + i18n::t("terminal.share_block.create_link"), Icon::Link, ButtonVariant::Accent, self.mouse_state_handles @@ -633,7 +634,7 @@ impl ShareBlockModal { ); let get_embed_button = self.render_create_block_button( appearance, - "Get embed", + i18n::t("terminal.share_block.get_embed"), Icon::Code1, ButtonVariant::Basic, self.mouse_state_handles @@ -650,7 +651,7 @@ impl ShareBlockModal { fn render_create_block_button( &self, appearance: &Appearance, - text_label: &str, + text_label: String, icon: Icon, button_variant: ButtonVariant, mouse_state_handle: MouseStateHandle, @@ -660,12 +661,12 @@ impl ShareBlockModal { TextAndIconAlignment::TextFirst, if let ShareRequestState::Pending(pending_share_type) = self.request_state { if pending_share_type == share_type { - "Creating block...".to_string() + i18n::t("terminal.share_block.creating_block") } else { - text_label.to_string() + text_label.clone() } } else { - text_label.to_string() + text_label.clone() }, icon.to_warpui_icon(appearance.theme().active_ui_text_color()), MainAxisSize::Max, @@ -735,7 +736,7 @@ impl ShareBlockModal { } else { let embed_snippet = self .generate_embed_snippet(app) - .unwrap_or("Error generating embed snippet".to_string()); + .unwrap_or_else(|| i18n::t("terminal.share_block.embed_snippet_error")); col.add_child(self.render_embed_label(appearance, embed_snippet)); col.add_child( Align::new( @@ -761,7 +762,7 @@ impl ShareBlockModal { .manage_permalinks_mouse_state .clone(), ) - .with_centered_text_label("Manage shared blocks".to_string()) + .with_centered_text_label(i18n::t("terminal.share_block.manage_shared_blocks")) .with_style( self.button_style_overrides(appearance) .set_font_size(12.) @@ -793,7 +794,7 @@ impl ShareBlockModal { ) -> Box { let text_and_icon = TextAndIcon::new( TextAndIconAlignment::TextFirst, - "Copy".to_string(), + i18n::t("common.copy"), Icon::Copy.to_warpui_icon(appearance.theme().active_ui_text_color()), MainAxisSize::Max, MainAxisAlignment::Center, @@ -867,7 +868,7 @@ impl ShareBlockModal { if link_generated { self.block_title_editor.as_ref(app).buffer_text(app) } else { - "Share block".to_string() + i18n::t("terminal.share_block.title") }, appearance.ui_font_family(), 24., @@ -955,7 +956,7 @@ impl ShareBlockModal { .finish(); let show_prompt_description = appearance .ui_builder() - .span("Show prompt".to_string()) + .span(i18n::t("terminal.share_block.show_prompt")) .build() .with_margin_left(2.) .finish(); @@ -1056,7 +1057,7 @@ impl ShareBlockModal { let redact_secrets_description = appearance .ui_builder() - .span("Redact secrets (API keys, passwords, IP addresses, PII etc.)".to_string()) + .span(i18n::t("terminal.share_block.redact_secrets")) .build() .with_margin_left(4.) .finish(); diff --git a/app/src/terminal/shared_session/mod.rs b/app/src/terminal/shared_session/mod.rs index 309d34c311..330a722880 100644 --- a/app/src/terminal/shared_session/mod.rs +++ b/app/src/terminal/shared_session/mod.rs @@ -32,9 +32,6 @@ pub mod viewer; #[cfg(test)] pub use tests::MAX_BYTES_SHAREABLE; -/// The toast copy when copying a shared session link. -pub const COPY_LINK_TEXT: &str = "Sharing link copied"; - /// Throttle period for selection updates. We throttle instead of debounce because we want /// to send selections even when it updates fast, so it appears live. /// Our throttle implementation throttles on the trailing edge (does not drop messages at the end, so the diff --git a/app/src/terminal/shared_session/participant_avatar_view.rs b/app/src/terminal/shared_session/participant_avatar_view.rs index 160e661e83..e69e0aeee5 100644 --- a/app/src/terminal/shared_session/participant_avatar_view.rs +++ b/app/src/terminal/shared_session/participant_avatar_view.rs @@ -163,18 +163,26 @@ impl ParticipantAvatarView { .into_item()]; match self.role { - Some(Role::Reader) => items.extend([MenuItemFields::new("Make editor") - .with_on_select_action(ParticipantAvatarAction::UpdateRole { - participant_id, - role: Role::Executor, - }) - .into_item()]), - Some(Role::Executor) => items.extend([MenuItemFields::new("Make viewer") - .with_on_select_action(ParticipantAvatarAction::UpdateRole { - participant_id, - role: Role::Reader, - }) - .into_item()]), + Some(Role::Reader) => { + items.extend([ + MenuItemFields::new(i18n::t("terminal.shared_session.make_editor")) + .with_on_select_action(ParticipantAvatarAction::UpdateRole { + participant_id, + role: Role::Executor, + }) + .into_item(), + ]) + } + Some(Role::Executor) => { + items.extend([ + MenuItemFields::new(i18n::t("terminal.shared_session.make_viewer")) + .with_on_select_action(ParticipantAvatarAction::UpdateRole { + participant_id, + role: Role::Reader, + }) + .into_item(), + ]) + } // Sharer does not have context menu _ => {} } @@ -541,7 +549,10 @@ pub fn render_revoke_all_button( ); stack.add_positioned_child( - render_tooltip("Revoke all edit permissions".to_string(), appearance), + render_tooltip( + i18n::t("terminal.shared_session.revoke_all_edit_permissions"), + appearance, + ), OffsetPositioning::offset_from_parent( vec2f(0., 3.), ParentOffsetBounds::Unbounded, @@ -583,7 +594,7 @@ pub fn render_viewer_role_button( let button = icon_button(appearance, icon, false, mouse_state_handle.clone()) .with_tooltip(move || { ui_builder - .tool_tip("Change role".to_string()) + .tool_tip(i18n::t("terminal.shared_session.change_role")) .build() .finish() }) diff --git a/app/src/terminal/shared_session/role_change_modal/sharer_grant_body.rs b/app/src/terminal/shared_session/role_change_modal/sharer_grant_body.rs index 4346a9492f..e5270b5d2a 100644 --- a/app/src/terminal/shared_session/role_change_modal/sharer_grant_body.rs +++ b/app/src/terminal/shared_session/role_change_modal/sharer_grant_body.rs @@ -59,7 +59,7 @@ impl SharerGrantBody { width: Some(BUTTON_WIDTH), ..Default::default() }) - .with_centered_text_label(String::from("Cancel")) + .with_centered_text_label(i18n::t("common.cancel")) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| ctx.dispatch_typed_action(SharerGrantBodyAction::Cancel)) @@ -82,7 +82,7 @@ impl SharerGrantBody { width: Some(BUTTON_WIDTH), ..Default::default() }) - .with_centered_text_label(String::from("Make Editor")) + .with_centered_text_label(i18n::t("terminal.shared_session.make_editor")) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { @@ -110,8 +110,8 @@ impl View for SharerGrantBody { let appearance = Appearance::as_ref(app); let button_row = self.render_button_row(appearance); - let text1 = "This grants the ability to execute commands on your"; - let text2 = "behalf. Use with caution."; + let text1 = i18n::t("terminal.shared_session.edit_permission_warning_line1"); + let text2 = i18n::t("terminal.shared_session.edit_permission_warning_line2"); let text_body = Container::new( Flex::column() .with_child( @@ -145,7 +145,10 @@ impl View for SharerGrantBody { self.dont_show_again_mouse_state.clone(), Some(TEXT_FONT_SIZE), ) - .with_label(Span::new("Don't show again.", Default::default())) + .with_label(Span::new( + i18n::t("common.do_not_show_again"), + Default::default(), + )) .check(self.dont_show_again) .build() .with_cursor(Cursor::PointingHand) diff --git a/app/src/terminal/shared_session/role_change_modal/sharer_response_body.rs b/app/src/terminal/shared_session/role_change_modal/sharer_response_body.rs index f217ad995d..2b25e987f0 100644 --- a/app/src/terminal/shared_session/role_change_modal/sharer_response_body.rs +++ b/app/src/terminal/shared_session/role_change_modal/sharer_response_body.rs @@ -143,7 +143,7 @@ impl SharerResponseBody { ButtonVariant::Outlined, role_request_params.button_mouse_states.deny_button, ) - .with_centered_text_label(String::from("Deny")) + .with_centered_text_label(i18n::t("terminal.shared_session.deny")) .with_style(UiComponentStyles { font_size: Some(BUTTON_FONT_SIZE), font_weight: Some(Weight::Bold), @@ -173,7 +173,7 @@ impl SharerResponseBody { ButtonVariant::Outlined, role_request_params.button_mouse_states.approve_button, ) - .with_centered_text_label(String::from("Approve")) + .with_centered_text_label(i18n::t("terminal.shared_session.approve")) .with_style(UiComponentStyles { font_size: Some(BUTTON_FONT_SIZE), font_weight: Some(Weight::Bold), @@ -264,9 +264,9 @@ impl View for SharerResponseBody { fn render(&self, app: &AppContext) -> Box { let appearance = Appearance::as_ref(app); - let header = "Edit Requests"; - let text1 = "This grants the ability to execute commands on your"; - let text2 = "behalf. Use with caution."; + let header = i18n::t("terminal.shared_session.edit_requests"); + let text1 = i18n::t("terminal.shared_session.edit_permission_warning_line1"); + let text2 = i18n::t("terminal.shared_session.edit_permission_warning_line2"); let text_body = Container::new( Flex::column() diff --git a/app/src/terminal/shared_session/role_change_modal/viewer_request_body.rs b/app/src/terminal/shared_session/role_change_modal/viewer_request_body.rs index 0939a16f98..58fefa8ee3 100644 --- a/app/src/terminal/shared_session/role_change_modal/viewer_request_body.rs +++ b/app/src/terminal/shared_session/role_change_modal/viewer_request_body.rs @@ -39,10 +39,10 @@ impl ViewerRequestBody { } } - fn role_label(&self) -> &str { + fn role_label(&self) -> String { match self.role { - Role::Executor => "edit", - _ => "view", + Role::Executor => i18n::t("terminal.shared_session.role.edit"), + _ => i18n::t("terminal.shared_session.role.view"), } } @@ -64,13 +64,15 @@ impl View for ViewerRequestBody { fn render(&self, app: &AppContext) -> Box { let appearance = Appearance::as_ref(app); - let header = format!("You have requested {} mode", self.role_label()); - let text = format!("Waiting for {}...", self.display_name); + let header = i18n::t("terminal.shared_session.viewer_request.header") + .replace("{role}", &self.role_label()); + let text = i18n::t("terminal.shared_session.viewer_request.waiting") + .replace("{name}", &self.display_name); let cancel_button = appearance .ui_builder() .button(ButtonVariant::Outlined, self.mouse_state_handle.clone()) - .with_centered_text_label(String::from("Cancel request")) + .with_centered_text_label(i18n::t("terminal.shared_session.viewer_request.cancel")) .with_style(UiComponentStyles { font_size: Some(TEXT_FONT_SIZE), font_weight: Some(Weight::Bold), diff --git a/app/src/terminal/shared_session/share_modal/body.rs b/app/src/terminal/shared_session/share_modal/body.rs index 96dc315b1a..1e9779ad55 100644 --- a/app/src/terminal/shared_session/share_modal/body.rs +++ b/app/src/terminal/shared_session/share_modal/body.rs @@ -39,7 +39,7 @@ struct RadioButtonGroupState { } struct ScrollbackOption { - label: &'static str, + label: String, scrollback_type: SharedSessionScrollbackType, mouse_state_handle: MouseStateHandle, is_disabled: bool, @@ -156,15 +156,15 @@ impl Body { > max_session_size.as_u64(); let scrollback_from_active_block_message = if model.is_alt_screen_active() { - "Share from current screen" + i18n::t("terminal.shared_session.scrollback.current_screen") } else if model .block_list() .active_block() .is_active_and_long_running() { - "Share from current block" + i18n::t("terminal.shared_session.scrollback.current_block") } else { - "Share without scrollback" + i18n::t("terminal.shared_session.scrollback.without_scrollback") }; let mut options = vec![ @@ -175,7 +175,7 @@ impl Body { is_disabled: is_scrollback_from_active_block_disabled, }, ScrollbackOption { - label: "Share from start of session", + label: i18n::t("terminal.shared_session.scrollback.start_of_session"), scrollback_type: SharedSessionScrollbackType::All, mouse_state_handle: Default::default(), is_disabled: is_all_scrollback_disabled, @@ -208,7 +208,7 @@ impl Body { options.insert( 0, ScrollbackOption { - label: "Share from selected block and onwards", + label: i18n::t("terminal.shared_session.scrollback.selected_block_onwards"), scrollback_type, mouse_state_handle: Default::default(), is_disabled, @@ -239,7 +239,7 @@ impl View for Body { ButtonVariant::Accent, self.button_mouse_states.start_sharing_button.clone(), ) - .with_centered_text_label(String::from("Start sharing")) + .with_centered_text_label(i18n::t("terminal.shared_session.start_sharing")) .with_style(style::button_styles()); // If none of the scrollback options are available, the start sharing @@ -265,7 +265,7 @@ impl View for Body { ButtonVariant::Outlined, self.button_mouse_states.cancel_button.clone(), ) - .with_centered_text_label(String::from("Cancel")) + .with_centered_text_label(i18n::t("common.cancel")) .with_style(style::button_styles()) .build() .with_cursor(Cursor::PointingHand) @@ -290,7 +290,7 @@ impl View for Body { self.radio_button_mouse_states .items .iter() - .map(|i| RadioButtonItem::text(i.label).with_disabled(i.is_disabled)) + .map(|i| RadioButtonItem::text(i.label.clone()).with_disabled(i.is_disabled)) .collect(), self.radio_button_mouse_states.group_state_handle.clone(), default_option, @@ -334,16 +334,20 @@ impl View for Body { } else if disabled_count > 1 { // Multiple options disabled - mention both reasons if agent conversations exist if self.has_agent_conversations { - Some("Some options are disabled due to sharing size limits and the presence of agent conversations in the session") + Some(i18n::t( + "terminal.shared_session.options_disabled_size_and_agents", + )) } else { - Some("Some options are disabled due to sharing size limits") + Some(i18n::t("terminal.shared_session.options_disabled_size")) } } else { // Only one option disabled - use specific message if it's due to agent conversations if self.has_agent_conversations { - Some("Sharing without scrollback is disabled because this session has agent conversations") + Some(i18n::t( + "terminal.shared_session.without_scrollback_disabled_agents", + )) } else { - Some("Some options are disabled due to sharing size limits") + Some(i18n::t("terminal.shared_session.options_disabled_size")) } }; diff --git a/app/src/terminal/shared_session/share_modal/denied_body.rs b/app/src/terminal/shared_session/share_modal/denied_body.rs index bddf55e157..c1933a3d86 100644 --- a/app/src/terminal/shared_session/share_modal/denied_body.rs +++ b/app/src/terminal/shared_session/share_modal/denied_body.rs @@ -7,9 +7,6 @@ use warpui::{AppContext, Element, Entity, SingletonEntity, TypedActionView, View use super::style::{self, MODAL_PADDING}; use crate::appearance::Appearance; -const SESSION_BUILD_FREE_PLAN_SUBHEADER: &str = "Warp's free and pro plans come with a limited number of shared sessions.\n\nFor increased access to session sharing upgrade to the Build plan."; -const VIEW_PLANS_TEXT: &str = "View plans"; - pub struct DeniedBody { button_mouse_state: MouseStateHandle, } @@ -44,11 +41,13 @@ impl View for DeniedBody { let appearance = Appearance::as_ref(app); let mut col = Flex::column(); - let subheader = SESSION_BUILD_FREE_PLAN_SUBHEADER; let text = appearance .ui_builder() - .wrappable_text(subheader, true) + .wrappable_text( + i18n::t("terminal.shared_session.limit_reached_subheader"), + true, + ) .with_style(style::subheader_styles(appearance)) .build() .finish(); @@ -56,7 +55,7 @@ impl View for DeniedBody { let button = appearance .ui_builder() .button(ButtonVariant::Accent, self.button_mouse_state.clone()) - .with_centered_text_label(VIEW_PLANS_TEXT.to_owned()) + .with_centered_text_label(i18n::t("common.view_plans")) .with_style(style::button_styles()) .build() .with_cursor(Cursor::PointingHand) diff --git a/app/src/terminal/shared_session/share_modal/mod.rs b/app/src/terminal/shared_session/share_modal/mod.rs index 570b007265..ff8731f105 100644 --- a/app/src/terminal/shared_session/share_modal/mod.rs +++ b/app/src/terminal/shared_session/share_modal/mod.rs @@ -27,9 +27,6 @@ use denied_body::{DeniedBody, DeniedBodyEvent}; use self::body::BodyEvent; use super::{SharedSessionActionSource, SharedSessionScrollbackType}; -const MODAL_HEADER: &str = "Share session"; -const SESSION_LIMIT_REACHED_HEADER: &str = "Shared session limit reached"; - pub struct ShareSessionModal { modal: ViewHandle>, denied_modal: ViewHandle>, @@ -72,17 +69,21 @@ impl ShareSessionModal { }); let modal = ctx.add_typed_action_view(|ctx| { - Modal::new(Some(MODAL_HEADER.to_string()), body, ctx) - .with_modal_style(UiComponentStyles { - width: Some(MODAL_WIDTH), - height: Some(MODAL_HEIGHT), - ..Default::default() - }) - .with_header_style(style::modal_header_styles()) - .with_body_style(style::modal_body_styles()) - .with_background_opacity(100) - .with_dismiss_on_click() - .close_modal_button_disabled() + Modal::new( + Some(i18n::t("terminal.shared_session.share_session")), + body, + ctx, + ) + .with_modal_style(UiComponentStyles { + width: Some(MODAL_WIDTH), + height: Some(MODAL_HEIGHT), + ..Default::default() + }) + .with_header_style(style::modal_header_styles()) + .with_body_style(style::modal_body_styles()) + .with_background_opacity(100) + .with_dismiss_on_click() + .close_modal_button_disabled() }); let denied_body = ctx.add_typed_action_view(DeniedBody::new); @@ -91,7 +92,7 @@ impl ShareSessionModal { }); let denied_modal = ctx.add_typed_action_view(|ctx| { let mut denied_modal = Modal::new( - Some(SESSION_LIMIT_REACHED_HEADER.to_string()), + Some(i18n::t("terminal.shared_session.limit_reached_header")), denied_body, ctx, ) diff --git a/app/src/terminal/shared_session/sharer/network.rs b/app/src/terminal/shared_session/sharer/network.rs index b253dca1a1..25471082e6 100644 --- a/app/src/terminal/shared_session/sharer/network.rs +++ b/app/src/terminal/shared_session/sharer/network.rs @@ -1304,8 +1304,6 @@ impl Network { } } -const NO_QUOTA_REMAINING_MESSAGE: &str = - "Session sharing usage exceeded for the day. Please try again later."; fn session_terminated_reason_diagnostic_label(reason: &SessionTerminatedReason) -> &'static str { match reason { SessionTerminatedReason::NoUserQuotaRemaining {} => "no_user_quota_remaining", @@ -1322,14 +1320,15 @@ pub fn session_terminated_reason_string( match reason { SessionTerminatedReason::NoUserQuotaRemaining {} => { // TODO: we should pass down the next refresh time to tell the user. - NO_QUOTA_REMAINING_MESSAGE.to_string() + i18n::t("terminal.shared_session.error.no_quota_remaining") } SessionTerminatedReason::ExceededSizeLimit => { let max_bytes = max_session_size.get_appropriate_unit(UnitType::Decimal); - format!("Session limit ({max_bytes}) exceeded. Please reshare to continue.") + i18n::t("terminal.shared_session.error.size_limit_exceeded") + .replace("{max_bytes}", &max_bytes.to_string()) } SessionTerminatedReason::InternalServerError { .. } => { - "Session ended due to an internal error. Please try sharing again.".to_string() + i18n::t("terminal.shared_session.error.internal_ended") } } } @@ -1338,31 +1337,31 @@ pub fn session_terminated_reason_string( pub fn failed_to_initialize_session_user_error(reason: &FailedToInitializeSessionReason) -> String { match reason { FailedToInitializeSessionReason::InternalServerError { .. } => { - "An internal error occurred. Please try sharing again." + i18n::t("terminal.shared_session.error.internal") } FailedToInitializeSessionReason::ScrollbackTooLarge {} => { - "Scrollback exceeds limit. Try sharing again without scrollback." + i18n::t("terminal.shared_session.error.scrollback_too_large") } FailedToInitializeSessionReason::NoUserQuotaRemaining { .. } => { // TODO: we should pass down the next refresh time to tell the user. - NO_QUOTA_REMAINING_MESSAGE + i18n::t("terminal.shared_session.error.no_quota_remaining") + } + FailedToInitializeSessionReason::UserNotFound => { + i18n::t("terminal.shared_session.error.login_required") } - FailedToInitializeSessionReason::UserNotFound => "You must be logged in to share sessions.", } - .to_string() } pub fn failed_to_add_guests_user_error(reason: &FailedToAddGuestsReason) -> String { match reason { - FailedToAddGuestsReason::Invalid => "Something went wrong. Please try again.", + FailedToAddGuestsReason::Invalid => i18n::t("common.something_went_wrong_try_again"), FailedToAddGuestsReason::NotWarpUsers => { - "One or more emails were not associated with Warp accounts." + i18n::t("terminal.shared_session.error.guests_not_warp_users") } FailedToAddGuestsReason::GuestAlreadyAdded => { - "One or more emails have already been added to the session." + i18n::t("terminal.shared_session.error.guests_already_added") } } - .to_string() } pub enum NetworkEvent { diff --git a/app/src/terminal/shared_session/viewer/network.rs b/app/src/terminal/shared_session/viewer/network.rs index d6c4d056ea..952e4e77f7 100644 --- a/app/src/terminal/shared_session/viewer/network.rs +++ b/app/src/terminal/shared_session/viewer/network.rs @@ -1016,18 +1016,24 @@ pub enum FailedToJoinReason { impl FailedToJoinReason { /// This error message will be displayed to the user. - pub fn user_facing_error_message(&self) -> &str { + pub fn user_facing_error_message(&self) -> String { match self { - FailedToJoinReason::Unknown => "Failed to join shared session.", + FailedToJoinReason::Unknown => i18n::t("terminal.shared_session.error.join_failed"), FailedToJoinReason::FailedToConnectToServer => { - "Failed to connect. Please try again later." + i18n::t("terminal.shared_session.error.connect_failed") + } + FailedToJoinReason::SessionNotFound => { + i18n::t("terminal.shared_session.error.session_not_found") + } + FailedToJoinReason::WrongPassword => { + i18n::t("terminal.shared_session.error.invalid_link") } - FailedToJoinReason::SessionNotFound => "Shared session not found.", - FailedToJoinReason::WrongPassword => "Invalid session sharing link.", FailedToJoinReason::MaxNumberOfParticipantsReached => { - "The maximum number of participants for this shared session has been reached." + i18n::t("terminal.shared_session.error.max_participants_reached") + } + FailedToJoinReason::SessionNotAccessible => { + i18n::t("terminal.shared_session.error.link_not_accessible") } - FailedToJoinReason::SessionNotAccessible => "You don't have access to this link.", } } } @@ -1049,20 +1055,19 @@ impl From for FailedToJoin pub fn session_ended_reason_string(reason: &SessionEndedReason) -> String { match reason { SessionEndedReason::InternalServerError => { - "Something went wrong. Please ask sharer to reshare to continue.".to_owned() + i18n::t("terminal.shared_session.error.reshare_to_continue") } SessionEndedReason::InactivityLimitReached => { - "Sharing ended due to sharer inactivity".to_owned() + i18n::t("terminal.shared_session.ended_due_to_sharer_inactivity") } - _ => "Session ended.".to_owned(), + _ => i18n::t("terminal.shared_session.session_ended"), } } pub fn viewer_removed_reason_string(reason: &ViewerRemovedReason) -> String { match reason { ViewerRemovedReason::LostAccess => { - "Your access to the session was removed. Please ask sharer to reshare to continue." - .to_owned() + i18n::t("terminal.shared_session.error.access_removed_reshare") } } } @@ -1071,9 +1076,9 @@ pub fn viewer_removed_reason_string(reason: &ViewerRemovedReason) -> String { pub fn command_execution_failure_reason_string(reason: &CommandExecutionFailureReason) -> String { match reason { CommandExecutionFailureReason::InsufficientPermissions => { - "Insufficient permissions. Please request edit access.".to_owned() + i18n::t("terminal.shared_session.error.insufficient_permissions_request_edit") } - _ => "Failed to execute command. Please try again.".to_owned(), + _ => i18n::t("terminal.shared_session.error.execute_command_failed"), } } @@ -1081,9 +1086,9 @@ pub fn command_execution_failure_reason_string(reason: &CommandExecutionFailureR pub fn write_to_pty_failure_reason_string(reason: &WriteToPtyFailureReason) -> String { match reason { WriteToPtyFailureReason::InsufficientPermissions => { - "Insufficient permissions. Please request edit access.".to_owned() + i18n::t("terminal.shared_session.error.insufficient_permissions_request_edit") } - _ => "Failed to make edit. Please try again.".to_owned(), + _ => i18n::t("terminal.shared_session.error.make_edit_failed"), } } @@ -1091,13 +1096,13 @@ pub fn write_to_pty_failure_reason_string(reason: &WriteToPtyFailureReason) -> S pub fn agent_prompt_failure_reason_string(reason: &AgentPromptFailureReason) -> String { match reason { AgentPromptFailureReason::InsufficientPermissions => { - "Insufficient permissions. Please request edit access.".to_owned() + i18n::t("terminal.shared_session.error.insufficient_permissions_request_edit") } AgentPromptFailureReason::InvalidConversation => { - "Invalid conversation. Please try again.".to_owned() + i18n::t("terminal.shared_session.error.invalid_conversation") } AgentPromptFailureReason::CommandInProgress => { - "A long running command is currently in progress. Please wait for it to complete before sending an agent prompt.".to_owned() + i18n::t("terminal.shared_session.error.command_in_progress") } } } @@ -1106,9 +1111,9 @@ pub fn agent_prompt_failure_reason_string(reason: &AgentPromptFailureReason) -> pub fn control_action_failure_reason_string(reason: &ControlActionFailureReason) -> String { match reason { ControlActionFailureReason::InsufficientPermissions => { - "Insufficient permissions. Please request edit access.".to_owned() + i18n::t("terminal.shared_session.error.insufficient_permissions_request_edit") } - _ => "Failed to perform action. Please try again.".to_owned(), + _ => i18n::t("terminal.shared_session.error.perform_action_failed"), } } diff --git a/app/src/terminal/shared_session/viewer/terminal_manager.rs b/app/src/terminal/shared_session/viewer/terminal_manager.rs index e9b12af238..a6b9696e6c 100644 --- a/app/src/terminal/shared_session/viewer/terminal_manager.rs +++ b/app/src/terminal/shared_session/viewer/terminal_manager.rs @@ -935,7 +935,7 @@ impl TerminalManager { Self::shared_session_ended(&view, model.clone(), ctx); view.update(ctx, |terminal_view, ctx| { terminal_view.show_persistent_toast( - "Failed to reconnect. Please try again later.".to_owned(), + i18n::t("terminal.shared_session.error.reconnect_failed"), ToastFlavor::Error, ctx, ); @@ -1246,7 +1246,7 @@ impl TerminalManager { } LinkAccessLevelUpdateResponse::Error => { terminal_view.show_persistent_toast( - "Failed to update permissions for shared session".to_owned(), + i18n::t("terminal.shared_session.error.update_permissions_failed"), ToastFlavor::Error, ctx, ); @@ -1275,7 +1275,7 @@ impl TerminalManager { } TeamAccessLevelUpdateResponse::Error(_) => { terminal_view.show_persistent_toast( - "Something went wrong. Please try again.".to_owned(), + i18n::t("common.something_went_wrong_try_again"), ToastFlavor::Error, ctx, ); @@ -1290,12 +1290,12 @@ impl TerminalManager { view.update(ctx, |terminal_view, ctx| { let reason_string = match reason { session_sharing_protocol::common::FailedToAddGuestsReason::NotWarpUsers => { - "One or more of the emails are not Warp users.".to_owned() + i18n::t("terminal.shared_session.error.guests_not_warp_users") } session_sharing_protocol::common::FailedToAddGuestsReason::GuestAlreadyAdded => { - "One or more of the guests has already been added.".to_owned() + i18n::t("terminal.shared_session.error.guests_already_added") } - _ => "Something went wrong. Please try again.".to_owned(), + _ => i18n::t("common.something_went_wrong_try_again"), }; terminal_view.show_persistent_toast(reason_string, ToastFlavor::Error, ctx); }); @@ -1308,7 +1308,7 @@ impl TerminalManager { }; view.update(ctx, |terminal_view, ctx| { terminal_view.show_persistent_toast( - "Something went wrong. Please try again.".to_owned(), + i18n::t("common.something_went_wrong_try_again"), ToastFlavor::Error, ctx, ); @@ -1322,7 +1322,7 @@ impl TerminalManager { }; view.update(ctx, |terminal_view, ctx| { terminal_view.show_persistent_toast( - "Something went wrong. Please try again.".to_owned(), + i18n::t("common.something_went_wrong_try_again"), ToastFlavor::Error, ctx, ); diff --git a/app/src/terminal/ssh/error.rs b/app/src/terminal/ssh/error.rs index 414286af5d..2aadaba9bc 100644 --- a/app/src/terminal/ssh/error.rs +++ b/app/src/terminal/ssh/error.rs @@ -21,18 +21,6 @@ use crate::terminal::warpify::render::{apply_spacing_styles, build_description_r use crate::terminal::warpify::settings::WarpifySettings; use crate::ui_components::icons::Icon as UiIcon; -const TMUX_NOT_INSTALLED_ERROR: &str = - "tmux is not installed on the remote machine. Please install tmux and try again."; -const UNSUPPORTED_TMUX_VERSION_ERROR: &str = - "The tmux version available on the remote machine is below 3.0. Please install tmux 3.0 or greater using a different method and try again."; -const TMUX_FAILED_ERROR: &str = - "tmux failed to execute on the remote machine. Please re-install tmux and try again."; -const WARPIFY_TIMEOUT_ERROR: &str = "Warpifying the session hit a timeout."; -const UNSUPPORTED_SHELL_ERROR: &str = - "Unsupported shell. Please set bash, zsh, or fish as your default shell and try again."; -const TMUX_INSTALL_FAILED_ERROR: &str = - "The tmux install hit an unexpected error. Please install tmux manually and try again."; - const SSH_GITHUB_ISSUE_URL: &str = "https://github.com/warpdotdev/Warp/issues/new?assignees=&labels=Bugs,SSH-tmux&projects=&template=03_ssh_tmux.yml"; fn get_ssh_github_issue_url(title: &str) -> String { @@ -48,16 +36,24 @@ fn get_ssh_github_issue_url(title: &str) -> String { } impl WarpificationUnavailableReason { - fn error_message(&self) -> &'static str { + fn error_message(&self) -> String { match self { - WarpificationUnavailableReason::TmuxNotInstalled { .. } => TMUX_NOT_INSTALLED_ERROR, + WarpificationUnavailableReason::TmuxNotInstalled { .. } => { + i18n::t("terminal.ssh.error.tmux_not_installed") + } WarpificationUnavailableReason::UnsupportedTmuxVersion { .. } => { - UNSUPPORTED_TMUX_VERSION_ERROR + i18n::t("terminal.ssh.error.unsupported_tmux_version") + } + WarpificationUnavailableReason::TmuxFailed => i18n::t("terminal.ssh.error.tmux_failed"), + WarpificationUnavailableReason::Timeout { .. } => { + i18n::t("terminal.ssh.error.warpify_timeout") + } + WarpificationUnavailableReason::UnsupportedShell { .. } => { + i18n::t("terminal.ssh.error.unsupported_shell") + } + WarpificationUnavailableReason::TmuxInstallFailed { .. } => { + i18n::t("terminal.ssh.error.tmux_install_failed") } - WarpificationUnavailableReason::TmuxFailed => TMUX_FAILED_ERROR, - WarpificationUnavailableReason::Timeout { .. } => WARPIFY_TIMEOUT_ERROR, - WarpificationUnavailableReason::UnsupportedShell { .. } => UNSUPPORTED_SHELL_ERROR, - WarpificationUnavailableReason::TmuxInstallFailed { .. } => TMUX_INSTALL_FAILED_ERROR, } } @@ -166,7 +162,7 @@ impl SshErrorBlock { appearance: &Appearance, ) -> Box { let header_contents = warpify::render::build_header_row( - "Error Warpifying session", + i18n::t("terminal.ssh.error.title"), Icon::new(UiIcon::AlertTriangle.into(), theme.ui_error_color()), theme, appearance, @@ -219,8 +215,9 @@ impl View for SshErrorBlock { content.add_child(self.render_title_ui(app, theme, appearance)); + let error_message = self.error_reason.error_message(); content.add_child(warpify::render::description_row( - self.error_reason.error_message(), + &error_message, theme, appearance, )); @@ -228,16 +225,28 @@ impl View for SshErrorBlock { let ui_builder = appearance.ui_builder(); if self.should_show_report_to_warp_button() { - let report_issue_text = build_description_row(FormattedText::new([FormattedTextLine::Line(vec![ - FormattedTextFragment::plain_text("We are actively working on improving the stability of SSH in Warp. Please consider "), - FormattedTextFragment::hyperlink("filing an issue", get_ssh_github_issue_url(self.error_reason.error_title())), - FormattedTextFragment::plain_text(" on GitHub so we can better identify the problem."), + let report_issue_text = build_description_row( + FormattedText::new([FormattedTextLine::Line(vec![ + FormattedTextFragment::plain_text(i18n::t( + "terminal.ssh.error.report_issue_prefix", + )), + FormattedTextFragment::hyperlink( + i18n::t("terminal.ssh.error.report_issue_link"), + get_ssh_github_issue_url(self.error_reason.error_title()), + ), + FormattedTextFragment::plain_text(i18n::t( + "terminal.ssh.error.report_issue_suffix", + )), ])]), - theme, appearance, self.report_link_highlight_index.clone()) - .with_hyperlink_font_color(theme.accent().into()) - .register_default_click_handlers(|link, ctx, _| { - ctx.dispatch_typed_action(SshErrorBlockAction::OpenUrl(link.url)); - }).finish(); + theme, + appearance, + self.report_link_highlight_index.clone(), + ) + .with_hyperlink_font_color(theme.accent().into()) + .register_default_click_handlers(|link, ctx, _| { + ctx.dispatch_typed_action(SshErrorBlockAction::OpenUrl(link.url)); + }) + .finish(); content.add_child(apply_spacing_styles(Container::new(report_issue_text)).finish()); } @@ -250,7 +259,9 @@ impl View for SshErrorBlock { ButtonVariant::Accent, self.warpify_without_tmux_button_mouse_state.clone(), ) - .with_centered_text_label("Warpify without TMUX".into()) + .with_centered_text_label(i18n::t( + "terminal.ssh.error.warpify_without_tmux", + )) .with_style(UiComponentStyles { font_size: Some(appearance.monospace_font_size()), ..Default::default() @@ -271,7 +282,9 @@ impl View for SshErrorBlock { ButtonVariant::Secondary, self.continue_button_mouse_state.clone(), ) - .with_centered_text_label("Continue without Warpification".into()) + .with_centered_text_label(i18n::t( + "terminal.ssh.error.continue_without_warpification", + )) .with_style(UiComponentStyles { font_size: Some(appearance.monospace_font_size()), ..Default::default() diff --git a/app/src/terminal/ssh/install_tmux.rs b/app/src/terminal/ssh/install_tmux.rs index 24ce00ded5..1d17284173 100644 --- a/app/src/terminal/ssh/install_tmux.rs +++ b/app/src/terminal/ssh/install_tmux.rs @@ -258,11 +258,12 @@ impl SshInstallTmuxBlock { let package_manager = &self.system_details.package_manager; Container::new(requested_script::render_requested_scripts( TitledScript { - title: format!("Install with {package_manager}"), + title: i18n::t("terminal.ssh.install_tmux.install_with") + .replace("{package_manager}", package_manager), content: tmux_system_install_script.to_string(), }, TitledScript { - title: "Install to ~/.warp".to_string(), + title: i18n::t("terminal.ssh.install_tmux.install_to_warp"), content: self.tmux_local_install_script.clone(), }, *is_first_script_active, @@ -290,12 +291,12 @@ impl SshInstallTmuxBlock { fn render_local_install_ui(&self, app: &AppContext) -> Box { let header = if self.is_focused { - "Run this script to install tmux?" + i18n::t("terminal.ssh.install_tmux.run_script_prompt") } else { - "" + String::new() }; Container::new(requested_script::render_requested_script( - header, + &header, &self.tmux_local_install_script, self.script_status.clone(), self.is_collapsed, @@ -320,7 +321,7 @@ impl SshInstallTmuxBlock { appearance: &Appearance, ) -> Box { let header_contents = render::build_header_row( - "Install tmux?", + i18n::t("terminal.ssh.install_tmux.title"), Icon::new(UiIcon::Warp.into(), theme.active_ui_detail()), theme, appearance, @@ -378,14 +379,17 @@ impl View for SshInstallTmuxBlock { ); let explanation = if self.outdated_version { - "In order to Warpify your SSH session, a more recent version of tmux (>=3.0) must be installed. " + i18n::t("terminal.ssh.install_tmux.outdated_version_explanation") } else { - "In order to Warpify your SSH session, tmux must be installed. " + i18n::t("terminal.ssh.install_tmux.missing_tmux_explanation") }; let warpify_description = vec![ FormattedTextFragment::plain_text(explanation), - FormattedTextFragment::hyperlink("Why do I need tmux?", WHY_INSTALL_TMUX_URL), + FormattedTextFragment::hyperlink( + i18n::t("terminal.ssh.why_tmux"), + WHY_INSTALL_TMUX_URL, + ), ]; let text_color = diff --git a/app/src/terminal/ssh/warpify.rs b/app/src/terminal/ssh/warpify.rs index a3c9a39522..90feee5c6e 100644 --- a/app/src/terminal/ssh/warpify.rs +++ b/app/src/terminal/ssh/warpify.rs @@ -66,7 +66,12 @@ impl Entity for SshWarpifyBlock { impl SshWarpifyBlock { fn render_title_ui(&self, theme: &WarpTheme, appearance: &Appearance) -> Box { let icon = Icon::new(UiIcon::Warp.into(), theme.active_ui_detail()); - warpify::render::header_row("Warpifying SSH Session...", icon, theme, appearance) + warpify::render::header_row( + i18n::t("terminal.ssh.warpify_title"), + icon, + theme, + appearance, + ) } } @@ -78,10 +83,8 @@ pub fn warpify_description( let theme = appearance.theme(); let description = FormattedText::new(vec![FormattedTextLine::Line(vec![ - FormattedTextFragment::plain_text( - "Bring Warp's features to your remote session. Blocks, full text editing, auto-complete, Oz, and more. " - ), - FormattedTextFragment::hyperlink("Learn more", SSH_DOCS_URL), + FormattedTextFragment::plain_text(i18n::t("terminal.ssh.warpify_description")), + FormattedTextFragment::hyperlink(i18n::t("common.learn_more"), SSH_DOCS_URL), ])]); warpify::render::build_description_row(description, theme, appearance, hyperlink_index.clone()) .with_hyperlink_font_color(appearance.theme().accent().into_solid()) diff --git a/app/src/terminal/universal_developer_input.rs b/app/src/terminal/universal_developer_input.rs index 6f8636704b..5a807b2394 100644 --- a/app/src/terminal/universal_developer_input.rs +++ b/app/src/terminal/universal_developer_input.rs @@ -84,19 +84,21 @@ impl AtContextMenuDisabledReason { match self { #[cfg(not(target_family = "wasm"))] AtContextMenuDisabledReason::NoObjectsAvailable => { - "No available objects in the current context.".to_string() + i18n::t("terminal.universal_input.no_context_objects") } #[cfg(not(target_family = "wasm"))] AtContextMenuDisabledReason::SshWithoutRemoteServer => { - "Not supported in SSH sessions without remote server".to_string() + i18n::t("terminal.universal_input.ssh_without_remote_server") } #[cfg(not(target_family = "wasm"))] - AtContextMenuDisabledReason::Subshell => "Not supported in subshells".to_string(), + AtContextMenuDisabledReason::Subshell => { + i18n::t("terminal.universal_input.subshell_not_supported") + } #[cfg(target_family = "wasm")] - AtContextMenuDisabledReason::Wasm => "Requires a filesystem".to_string(), + AtContextMenuDisabledReason::Wasm => i18n::t("terminal.universal_input.requires_fs"), #[cfg(not(target_family = "wasm"))] AtContextMenuDisabledReason::DisabledInTerminalMode => { - "Disabled in terminal mode, re-enable in settings".to_string() + i18n::t("terminal.universal_input.disabled_terminal_mode") } } } @@ -186,8 +188,6 @@ impl AtContextMenuDisabledReason { } } -const AT_CONTEXT_TOOLTIP: &str = "Attach context"; - const BLURRED_OPACITY: Opacity = 50; // Threshold calculation that estimates the width needed for the profile/model selector @@ -343,7 +343,7 @@ impl UniversalDeveloperInputButtonBar { #[cfg_attr(not(feature = "voice_input"), allow(unused_mut))] let mut button = ActionButton::new("", PromptIconButtonTheme::new(false)) .with_icon(Icon::Microphone) - .with_tooltip("Voice input") + .with_tooltip(i18n::t("agent_input_footer.voice_input")) .with_size(button_size) .with_tooltip_alignment(TooltipAlignment::Left); #[cfg(feature = "voice_input")] @@ -360,7 +360,7 @@ impl UniversalDeveloperInputButtonBar { let at_button_view = ctx.add_typed_action_view(|_ctx| { ActionButton::new("", PromptIconButtonTheme::new(false)) .with_icon(Icon::AtSign) - .with_tooltip(AT_CONTEXT_TOOLTIP) + .with_tooltip(i18n::t("terminal.universal_input.attach_context")) .with_size(button_size) .with_disabled_theme(UDIDisabledButtonTheme) .with_tooltip_alignment(TooltipAlignment::Left) @@ -374,7 +374,7 @@ impl UniversalDeveloperInputButtonBar { let file_button_view = ctx.add_typed_action_view(|_ctx| { ActionButton::new("", PromptIconButtonTheme::new(false)) .with_icon(Icon::Plus) - .with_tooltip("Attach file") + .with_tooltip(i18n::t("agent_input_footer.attach_file")) .with_size(button_size) .with_disabled_theme(UDIDisabledButtonTheme) .with_tooltip_alignment(TooltipAlignment::Left) @@ -386,7 +386,7 @@ impl UniversalDeveloperInputButtonBar { let slash_command_menu_view = ctx.add_typed_action_view(|_ctx| { ActionButton::new("", PromptIconButtonTheme::new(false)) .with_icon(Icon::SlashCommands) - .with_tooltip("Slash commands") + .with_tooltip(i18n::t("terminal.universal_input.slash_commands")) .with_size(button_size) .with_disabled_theme(UDIDisabledButtonTheme) .with_tooltip_alignment(TooltipAlignment::Left) @@ -677,9 +677,9 @@ impl UniversalDeveloperInputButtonBar { }; let tooltip = if is_reader { - Some("Request edit access to change input mode".to_string()) + Some(i18n::t("terminal.universal_input.request_edit_access")) } else if is_agent_in_control { - Some("Input mode locked while agent is monitoring a command".to_string()) + Some(i18n::t("terminal.universal_input.input_mode_locked")) } else { None }; @@ -701,7 +701,7 @@ impl UniversalDeveloperInputButtonBar { button.set_tooltip( disable_reason .map(|reason| reason.tooltip_text()) - .or(Some(AT_CONTEXT_TOOLTIP.to_string())), + .or_else(|| Some(i18n::t("terminal.universal_input.attach_context"))), ctx, ); ctx.notify(); @@ -985,7 +985,7 @@ fn build_renderable_option_config( icon_color: fg_color, label: None, tooltip: Some(tooltip_config( - "Terminal", + i18n::t("terminal.input_mode.terminal"), Some(terminal_mode_tooltip_subtext(terminal_keybindings)), app, )), @@ -1001,7 +1001,7 @@ fn build_renderable_option_config( icon_color: fg_color, label: None, tooltip: Some(tooltip_config( - "Agent Mode", + i18n::t("terminal.input_mode.agent_mode"), Some(agent_mode_tooltip_subtext(terminal_keybindings)), app, )), @@ -1048,7 +1048,9 @@ fn agent_mode_tooltip_subtext(terminal_keybindings: &TerminalKeybindings) -> Str return AGENT_MODE_TOOLTIP_PREFIX.into(); }; - format!("{keybinding} or {AGENT_MODE_TOOLTIP_PREFIX}") + i18n::t("terminal.input_mode.shortcut_or") + .replace("{keybinding}", &keybinding) + .replace("{prefix}", AGENT_MODE_TOOLTIP_PREFIX) } fn terminal_mode_tooltip_subtext(terminal_keybindings: &TerminalKeybindings) -> String { @@ -1057,7 +1059,9 @@ fn terminal_mode_tooltip_subtext(terminal_keybindings: &TerminalKeybindings) -> return TERMINAL_MODE_TOOLTIP_PREFIX.into(); }; - format!("{keybinding} or {TERMINAL_MODE_TOOLTIP_PREFIX}") + i18n::t("terminal.input_mode.shortcut_or") + .replace("{keybinding}", &keybinding) + .replace("{prefix}", TERMINAL_MODE_TOOLTIP_PREFIX) } fn tooltip_config( diff --git a/app/src/terminal/view.rs b/app/src/terminal/view.rs index a123768b43..7a006b0119 100644 --- a/app/src/terminal/view.rs +++ b/app/src/terminal/view.rs @@ -261,7 +261,7 @@ use crate::ai::blocklist::usage::conversation_usage_view::{ ConversationUsageInfo, ConversationUsageView, TimingInfo, }; use crate::ai::blocklist::{ - ai_brand_color, block_context_from_terminal_model, + ai_brand_color, attach_as_agent_mode_context_text, block_context_from_terminal_model, get_ai_block_overflow_menu_element_position_id, get_attached_blocks_chip_element_position_id, AIBlock, AIBlockEvent, AutofireAction, BlocklistAIActionEvent, BlocklistAIActionModel, BlocklistAIContextEvent, BlocklistAIContextModel, BlocklistAIController, @@ -272,7 +272,7 @@ use crate::ai::blocklist::{ PassiveSuggestionsModels, PendingAttachment, PendingQueryState, QueuedQueryModel, RequestFileEditsFormatKind, ShellCommandExecutor, ShellCommandExecutorEvent, SlashCommandRequest, StartAgentExecutor, StartAgentExecutorEvent, StartAgentRequest, - ATTACH_AS_AGENT_MODE_CONTEXT_TEXT, PRE_REWIND_PREFIX, + PRE_REWIND_PREFIX, }; use crate::ai::conversation_details_panel::ConversationDetailsPanelEvent; use crate::ai::conversation_utils; @@ -288,7 +288,7 @@ use crate::ai::predict::prompt_suggestions::{ is_accept_prompt_suggestion_bound_to_cmd_enter, is_accept_prompt_suggestion_bound_to_ctrl_enter, }; -use crate::ai_assistant::{AskAIType, ASK_AI_ASSISTANT_TEXT}; +use crate::ai_assistant::AskAIType; use crate::antivirus::AntivirusInfo; use crate::appearance::{Appearance, AppearanceEvent}; use crate::auth::auth_manager::AuthManager; @@ -505,7 +505,7 @@ use crate::terminal::waterfall_gap_element::WaterfallGapElement; use crate::terminal::{ block_list_element::BlockHoverAction, // find::{Event as FindEvent, Find, FindDirection}, - input::{Event as InputEvent, Input, INPUT_A11Y_HELPER, INPUT_A11Y_LABEL}, + input::{Event as InputEvent, Input}, model::block::SerializedBlock, shell::ShellType, terminal_size_element::TerminalSizeElement, @@ -688,8 +688,6 @@ const MOVE_LINE_END_BINDING_NAME: &str = "editor_view:move_to_line_end"; const DEFAULT_AI_BLOCK_HEIGHT: f32 = 96.; -pub const DEFAULT_ASK_AI_AUTOSUGGESTION_TEXT: &str = "What happened here?"; - const WARP_MD_PATH: &str = "WARP.md"; pub const LONG_RUNNING_AGENT_REQUESTED_COMMAND_CONTEXT_KEY: &str = "LongRunningRequestedCommand"; @@ -775,19 +773,19 @@ pub enum NotificationsTrigger { } impl NotificationsTrigger { - pub fn discovery_banner_copy(&self) -> &'static str { + pub fn discovery_banner_copy(&self) -> String { match self { NotificationsTrigger::LongRunningCommand(..) => { - "Warp can notify you when long-running commands finish." + i18n::t("terminal.inline_banner.notifications.long_running_command") } NotificationsTrigger::AgentTaskCompleted(..) => { - "Warp can notify you when an agent finishes responding." + i18n::t("terminal.inline_banner.notifications.agent_task_completed") } NotificationsTrigger::NeedsAttention => { - "Warp can notify you when a command or agent needs your attention." + i18n::t("terminal.inline_banner.notifications.needs_attention") } NotificationsTrigger::PasswordPrompt => { - "Warp can notify you when you're prompted to enter a password." + i18n::t("terminal.inline_banner.notifications.password_prompt") } } } @@ -815,35 +813,44 @@ impl NotificationsTrigger { let (title_suffix, body_prefix) = match self { LongRunningCommand(command_succeeded, block_duration) => { - let status = if *command_succeeded { - "finished" - } else { - "failed" - }; - let duration_seconds = block_duration.as_secs_f32(); let duration_seconds = if duration_seconds >= 1. { format!("{}", duration_seconds.round() as usize) } else { format!("{duration_seconds:.1}") }; + let duration_suffix = if *command_succeeded { + i18n::t("terminal.notification.long_running_finished_suffix") + } else { + i18n::t("terminal.notification.long_running_failed_suffix") + } + .replace("{duration}", &duration_seconds); ( - format!(" {status} after {duration_seconds}s"), - "Latest output: ".to_string(), + duration_suffix, + i18n::t("terminal.notification.latest_output_prefix"), ) } AgentTaskCompleted(command_succeeded) => { if *command_succeeded { - (" finished".to_string(), "Latest output: ".to_string()) + ( + i18n::t("terminal.notification.agent_finished_suffix"), + i18n::t("terminal.notification.latest_output_prefix"), + ) } else { - (" failed".to_string(), "Error: ".to_string()) + ( + i18n::t("terminal.notification.agent_failed_suffix"), + i18n::t("terminal.notification.error_prefix"), + ) } } - NotificationsTrigger::NeedsAttention => (" blocked".to_string(), "".to_string()), + NotificationsTrigger::NeedsAttention => ( + i18n::t("terminal.notification.needs_attention_suffix"), + "".to_string(), + ), PasswordPrompt => ( - " is waiting for a password".to_string(), - "Latest output: ".to_string(), + i18n::t("terminal.notification.password_prompt_suffix"), + i18n::t("terminal.notification.latest_output_prefix"), ), }; @@ -3816,13 +3823,14 @@ impl TerminalView { let slow_bootstrap_banner = ctx.add_typed_action_view(|_| { Banner::::new_with_buttons( BannerTextContent::formatted_text(vec![ - FormattedTextFragment::plain_text( - "Seems like your shell is taking a while to start... ", + FormattedTextFragment::plain_text(i18n::t("terminal.warning.shell_start_slow")), + FormattedTextFragment::hyperlink( + i18n::t("terminal.warning.more_info"), + KNOWN_ISSUES_URL, ), - FormattedTextFragment::hyperlink("More info", KNOWN_ISSUES_URL), ]), vec![BannerTextButton::new( - "Show initialization block".to_string(), + i18n::t("settings.action.show_initialization_block"), Rc::new(|event_ctx, _ctx, _position| { event_ctx.dispatch_typed_action(BannerAction::::Action( TerminalAction::ShowInitializationBlock, @@ -3842,14 +3850,21 @@ impl TerminalView { let control_master_error_banner = ctx.add_typed_action_view(|_| { Banner::new(BannerTextContent::formatted_text(vec![ - FormattedTextFragment::plain_text("Seems like your completions are not working ("), - FormattedTextFragment::hyperlink("more info", CONTROLMASTER_ISSUES_URL), - FormattedTextFragment::plain_text("). Enabling the SSH extension in "), + FormattedTextFragment::plain_text(i18n::t( + "terminal.warning.completions_not_working_prefix", + )), + FormattedTextFragment::hyperlink( + i18n::t("terminal.warning.more_info_lower"), + CONTROLMASTER_ISSUES_URL, + ), + FormattedTextFragment::plain_text(i18n::t( + "terminal.warning.enable_ssh_extension_prefix", + )), FormattedTextFragment::hyperlink_action( - "settings", + i18n::t("terminal.warning.settings"), TerminalAction::ShowWarpifySettings, ), - FormattedTextFragment::plain_text(" may resolve this issue."), + FormattedTextFragment::plain_text(i18n::t("terminal.warning.may_resolve_suffix")), ])) }); @@ -3859,10 +3874,13 @@ impl TerminalView { let incompatible_configuration_banner = ctx.add_typed_action_view(|_| { Banner::new(BannerTextContent::formatted_text(vec![ - FormattedTextFragment::plain_text( - "Your shell configuration is incompatible with Warp... ", + FormattedTextFragment::plain_text(i18n::t( + "terminal.warning.shell_config_incompatible", + )), + FormattedTextFragment::hyperlink( + i18n::t("terminal.warning.more_info"), + KNOWN_ISSUES_URL, ), - FormattedTextFragment::hyperlink("More info", KNOWN_ISSUES_URL), ])) }); @@ -3873,18 +3891,22 @@ impl TerminalView { let emacs_bindings_banner = ctx.add_typed_action_view(|_| { Banner::new_with_buttons( BannerTextContent::formatted_text(vec![ - FormattedTextFragment::plain_text("Did you intend "), + FormattedTextFragment::plain_text(i18n::t( + "terminal.warning.did_you_intend_prefix", + )), FormattedTextFragment::inline_code("ctrl-a"), FormattedTextFragment::plain_text("/"), FormattedTextFragment::inline_code("ctrl-e"), - FormattedTextFragment::plain_text(" to move the cursor?"), + FormattedTextFragment::plain_text(i18n::t( + "terminal.warning.move_cursor_suffix", + )), ]), // Here, we use DismissalType::Temporary and DismissalType::Permanent variants // as stand-ins for changing bindings vs. leaving them as-is. // TODO(Linear PLAT-512): update Banner to support generic event type. vec![ BannerTextButton::new( - String::from("Yes, use Emacs-style bindings"), + i18n::t("terminal.warning.use_emacs_style_bindings"), Rc::new(|event_ctx, _app_ctx, _| { event_ctx.dispatch_typed_action( BannerAction::::Dismiss(DismissalType::Temporary), @@ -3892,7 +3914,7 @@ impl TerminalView { }), ), BannerTextButton::new( - String::from("No, keep IDE bindings"), + i18n::t("terminal.warning.keep_ide_bindings"), Rc::new(|event_ctx, _app_ctx, _| { event_ctx.dispatch_typed_action( BannerAction::::Dismiss(DismissalType::Permanent), @@ -4115,25 +4137,28 @@ impl TerminalView { ctx.subscribe_to_view(&orchestration_pill_bar, |_, _, _, ctx| ctx.notify()); let agent_view_back_button = ctx.add_typed_action_view(|ctx| { - ActionButton::new("for terminal", AgentViewHeaderTheme) - .with_icon(icons::Icon::ArrowLeft) - .with_size(ButtonSize::Small) - .with_keybinding( - KeystrokeSource::Fixed(Keystroke { - key: "escape".to_string(), - ..Default::default() - }), - ctx, + ActionButton::new( + i18n::t("terminal.agent_view_header.for_terminal"), + AgentViewHeaderTheme, + ) + .with_icon(icons::Icon::ArrowLeft) + .with_size(ButtonSize::Small) + .with_keybinding( + KeystrokeSource::Fixed(Keystroke { + key: "escape".to_string(), + ..Default::default() + }), + ctx, + ) + .with_disabled_theme(AgentViewHeaderDisabledTheme) + .with_keybinding_before_label(true) + .on_click(|ctx| { + ctx.dispatch_typed_action( + PaneHeaderAction::::CustomAction( + TerminalAction::ExitAgentView, + ), ) - .with_disabled_theme(AgentViewHeaderDisabledTheme) - .with_keybinding_before_label(true) - .on_click(|ctx| { - ctx.dispatch_typed_action( - PaneHeaderAction::::CustomAction( - TerminalAction::ExitAgentView, - ), - ) - }) + }) }); // Conversation details panel (cloud Oz runs and any active local AI conversation). @@ -4408,7 +4433,7 @@ impl TerminalView { me.show_ssh_remote_server_failed_banner( *session_id, remote_server::transport::UserFacingError { - body: "Failed to start SSH extension".into(), + body: i18n::t("terminal.ssh.remote_server_failed.start_failed"), detail: if error.is_empty() { None } else { @@ -5167,7 +5192,7 @@ impl TerminalView { let block_id = BlockId::from(block.id().to_string()); let suggestion = AgentModePromptSuggestion::Success(PromptSuggestion { id: Uuid::new_v4().to_string(), - label: Some("Execute this plan".to_string()), + label: Some(i18n::t("terminal.prompt_suggestion.execute_plan")), prompt: "Execute this plan".to_string(), coding_query_context: None, static_prompt_suggestion_name: Some("EXECUTE_CREATED_PLAN".to_string()), @@ -7112,24 +7137,24 @@ impl TerminalView { .get_pending_action(app) .map(|action| match &action.action { AIAgentActionType::RequestCommandOutput { command, .. } => { - format!("Oz needs your permission to run `{command}`") + i18n::t("terminal.a11y.oz_permission_run_command") + .replace("{command}", command) } AIAgentActionType::ReadFiles(..) => { - "Oz needs your permission to read files".to_string() + i18n::t("terminal.a11y.oz_permission_read_files") } AIAgentActionType::SearchCodebase(..) => { - "Oz needs your permission to search your codebase".to_string() + i18n::t("terminal.a11y.oz_permission_search_codebase") } AIAgentActionType::RequestFileEdits { .. } => { - "Oz needs your permission to edit a file".to_string() + i18n::t("terminal.a11y.oz_permission_edit_file") } AIAgentActionType::WriteToLongRunningShellCommand { .. } => { - "Oz needs your permission to interact with a running shell command" - .to_string() + i18n::t("terminal.a11y.oz_permission_write_to_shell") } - _ => "Oz needs your confirmation to continue".to_string(), + _ => i18n::t("terminal.a11y.oz_confirmation_continue"), }) - .unwrap_or("Oz needs your confirmation to continue".to_string()); + .unwrap_or_else(|| i18n::t("terminal.a11y.oz_confirmation_continue")); return Some(AIBlockNotificationSummary { success: false, title, @@ -7178,7 +7203,7 @@ impl TerminalView { _ => Some(AIBlockNotificationSummary { success: false, title, - description: "An unknown error occurred".to_string(), + description: i18n::t("terminal.notification.unknown_error_occurred"), }), } } @@ -9557,12 +9582,12 @@ impl TerminalView { } let a11y_message = match &warpify_keybinding { - Some(keystroke) => format!( - "You can press {} to Warpify this {} for more Warp features.", - keystroke.displayed(), - lowercase_title - ), - None => format!("You can Warpify this {lowercase_title} for more Warp features."), + Some(keystroke) => i18n::t("terminal.a11y.warpify_with_key") + .replace("{key}", &keystroke.displayed()) + .replace("{title}", lowercase_title), + None => { + i18n::t("terminal.a11y.warpify_without_key").replace("{title}", lowercase_title) + } }; model @@ -9572,7 +9597,7 @@ impl TerminalView { ))); let a11y_content = AccessibilityContent::new( - format!("{title} recognized."), + i18n::t("terminal.a11y.recognized").replace("{title}", title), a11y_message, WarpA11yRole::TextRole, ); @@ -9686,7 +9711,7 @@ impl TerminalView { let a11y_content = AccessibilityContent::new( trigger.discovery_banner_copy(), - "You can enable notifications through the command palette.", + i18n::t("terminal.inline_banner.notifications.a11y_enable_through_command_palette"), WarpA11yRole::TextRole, ); ctx.emit_a11y_content(a11y_content); @@ -9720,12 +9745,12 @@ impl TerminalView { .notifications_error_banner .error .as_ref() - .map(|e| e.notifications_error_banner_title()) - .unwrap_or("Error sending notification"); + .map(|e| e.notifications_error_banner_title().to_string()) + .unwrap_or_else(|| i18n::t("terminal.notification.error_sending")); let a11y_content = AccessibilityContent::new( banner_title, - "Make sure you have enabled access for Warp notifications in System Preferences.", + i18n::t("terminal.notification.permissions_help"), WarpA11yRole::TextRole, ); ctx.emit_a11y_content(a11y_content); @@ -12489,8 +12514,9 @@ impl TerminalView { } if self.is_navigated_away_from_window(ctx) { - let notification_title = - title.clone().unwrap_or_else(|| "Notification".to_string()); + let notification_title = title + .clone() + .unwrap_or_else(|| i18n::t("terminal.notification.title")); let notification = BlockNotification { title: notification_title, body: body.clone(), @@ -12643,19 +12669,26 @@ impl TerminalView { .as_ref(app) .remote_server_setup_state(sid) .map(|state| match state { - RemoteServerSetupState::Checking => "Checking...".to_string(), + RemoteServerSetupState::Checking => { + i18n::t("terminal.remote_server.loading.checking") + } RemoteServerSetupState::Installing { progress_percent: Some(p), - } => format!("Installing... ({p}%)"), + } => i18n::t("terminal.remote_server.loading.installing_progress") + .replace("{percent}", &p.to_string()), RemoteServerSetupState::Installing { progress_percent: None, - } => "Installing...".to_string(), - RemoteServerSetupState::Updating => "Updating...".to_string(), - RemoteServerSetupState::Initializing => "Initializing...".to_string(), - _ => "Starting shell...".to_string(), + } => i18n::t("terminal.remote_server.loading.installing"), + RemoteServerSetupState::Updating => { + i18n::t("terminal.remote_server.loading.updating") + } + RemoteServerSetupState::Initializing => { + i18n::t("terminal.remote_server.loading.initializing") + } + _ => i18n::t("terminal.remote_server.loading.starting_shell"), }) }) - .unwrap_or_else(|| "Starting shell...".to_string()); + .unwrap_or_else(|| i18n::t("terminal.remote_server.loading.starting_shell")); let shimmer_element = shimmering_warp_loading_text( message, @@ -13690,7 +13723,7 @@ impl TerminalView { // Set fallback title since /init may have no initial query BlocklistAIHistoryModel::handle(ctx).update(ctx, |history, _ctx| { if let Some(conversation) = history.conversation_mut(&conversation_id) { - conversation.set_fallback_display_title("Project setup".to_string()); + conversation.set_fallback_display_title(i18n::t("terminal.project_setup.title")); } }); @@ -13939,10 +13972,8 @@ impl TerminalView { let repos = args; let (button_label, use_current_dir) = if !repos.is_empty() { ( - format!( - "Create environment using the supplied repos: {}", - repos.join(", ") - ), + i18n::t("terminal.init_environment.create_using_supplied_repos") + .replace("{repos}", &repos.join(", ")), false, ) } else { @@ -13965,11 +13996,14 @@ impl TerminalView { if is_repo { ( - "Create environment using the current working dir as repo".to_string(), + i18n::t("terminal.init_environment.create_using_current_dir"), true, ) } else { - ("Create environment without any repos".to_string(), false) + ( + i18n::t("terminal.init_environment.create_without_repos"), + false, + ) } }; @@ -14305,7 +14339,7 @@ fn build_onboarding_keybindings(ctx: &AppContext) -> OnboardingKeybindings { /// Builds the context-menu label for forking an AI conversation from a given query. fn fork_label_for_query(query: &str) -> String { if query.is_empty() { - "Fork from last query".to_string() + i18n::t("terminal.context_menu.fork_from_last_query") } else { let first_line = query.lines().next().unwrap_or(query).trim(); let chars: Vec = first_line.chars().take(21).collect(); @@ -14314,7 +14348,8 @@ fn fork_label_for_query(query: &str) -> String { } else { (chars.iter().collect::(), "") }; - format!("Fork from \"{truncated}{suffix}\"") + i18n::t("terminal.context_menu.fork_from_query") + .replace("{query}", &format!("{truncated}{suffix}")) } } @@ -14757,8 +14792,9 @@ impl TerminalView { }); let a11y_content = AccessibilityContent::new( - format!("Suggested corrected command: {}", correction.command), - "Press right arrow to insert or keep editing to ignore", + i18n::t("terminal.a11y.command_correction_suggested") + .replace("{command}", &correction.command), + i18n::t("terminal.a11y.command_correction_help"), WarpA11yRole::HelpRole, ); ctx.emit_a11y_content(a11y_content); @@ -16235,11 +16271,13 @@ impl TerminalView { Some(model.link_at_range(url, RespectObfuscatedSecrets::Yes)); url_content .map(|url_content| { - vec![MenuItemFields::new("Copy URL") - .with_on_select_action(TerminalAction::ContextMenu( - ContextMenuAction::CopyUrl { url_content }, - )) - .into_item()] + vec![ + MenuItemFields::new(i18n::t("terminal.context_menu.copy_url")) + .with_on_select_action(TerminalAction::ContextMenu( + ContextMenuAction::CopyUrl { url_content }, + )) + .into_item(), + ] }) .unwrap_or_default() } @@ -16247,13 +16285,13 @@ impl TerminalView { GridHighlightedLink::File(file_link) => { let path = file_link.get_inner().absolute_path(); let show_in_file_explorer_menu_item_label = if cfg!(target_os = "macos") { - "Show in Finder" + i18n::t("terminal.tooltips.show_in_finder") } else { - "Show containing folder" + i18n::t("terminal.tooltips.show_containing_folder") }; path.map(|path| { let mut items = vec![ - MenuItemFields::new("Copy path") + MenuItemFields::new(i18n::t("common.copy_path")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::CopyUrl { url_content: path.to_string_lossy().into(), @@ -16269,14 +16307,16 @@ impl TerminalView { if is_markdown_file(&path) { items.push( - MenuItemFields::new("Open in Warp") - .with_on_select_action(TerminalAction::OpenFileInWarp(path)) - .into_item(), + MenuItemFields::new(i18n::t( + "terminal.context_menu.open_in_warp", + )) + .with_on_select_action(TerminalAction::OpenFileInWarp(path)) + .into_item(), ); // Because the default for cmd-click is to open in Warp, we also // have an open-in-editor option. items.push( - MenuItemFields::new("Open in editor") + MenuItemFields::new(i18n::t("notebooks.file.open_in_editor")) .with_on_select_action(TerminalAction::OpenGridLink( highlighted_link.clone(), )) @@ -16297,7 +16337,7 @@ impl TerminalView { true, ) => { let mut fields = vec![ - MenuItemFields::new("Copy") + MenuItemFields::new(i18n::t("common.copy")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::CopySelectedText, )) @@ -16306,7 +16346,7 @@ impl TerminalView { ctx, )) .into_item(), - MenuItemFields::new("Insert into input") + MenuItemFields::new(i18n::t("terminal.context_menu.insert_into_input")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::InsertSelectedText, )) @@ -16316,9 +16356,9 @@ impl TerminalView { fields.extend([ MenuItem::Separator, MenuItemFields::new(if FeatureFlag::AgentMode.is_enabled() { - *ATTACH_AS_AGENT_MODE_CONTEXT_TEXT + attach_as_agent_mode_context_text() } else { - ASK_AI_ASSISTANT_TEXT + i18n::t("ai_assistant.ask_warp_ai") }) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::AskAI(if FeatureFlag::AgentMode.is_enabled() { @@ -16362,25 +16402,25 @@ impl TerminalView { .is_active_and_long_running(); let copy_commands_str = if is_single_selection { - "Copy command" + i18n::t("terminal.context_menu.copy_command") } else { - "Copy commands" + i18n::t("terminal.context_menu.copy_commands") }; - let copy_str = "Copy"; + let copy_str = i18n::t("common.copy"); let find_str = if is_single_selection { - "Find within block" + i18n::t("terminal.context_menu.find_within_block") } else { - "Find within blocks" + i18n::t("terminal.context_menu.find_within_blocks") }; let scroll_to_top_str = if is_single_selection { - "Scroll to top of block" + i18n::t("terminal.context_menu.scroll_to_top_of_block") } else { - "Scroll to top of blocks" + i18n::t("terminal.context_menu.scroll_to_top_of_blocks") }; let scroll_to_bottom_str = if is_single_selection { - "Scroll to bottom of block" + i18n::t("terminal.context_menu.scroll_to_bottom_of_block") } else { - "Scroll to bottom of blocks" + i18n::t("terminal.context_menu.scroll_to_bottom_of_blocks") }; // currently, we don't support share for multi selections @@ -16397,9 +16437,9 @@ impl TerminalView { let share_block_label = if FeatureFlag::CreatingSharedSessions.is_enabled() && ContextFlag::CreateSharedSession.is_enabled() { - "Share block..." + i18n::t("terminal.context_menu.share_block_ellipsis") } else { - "Share..." + i18n::t("terminal.context_menu.share_ellipsis") }; let mut items = vec![ @@ -16455,7 +16495,7 @@ impl TerminalView { if WarpDriveSettings::is_warp_drive_enabled(ctx) { items.push(MenuItem::Separator); items.push( - MenuItemFields::new("Save as workflow") + MenuItemFields::new(i18n::t("terminal.context_menu.save_as_workflow")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::OpenWorkflowModal, )) @@ -16473,7 +16513,7 @@ impl TerminalView { if self.is_input_box_visible(&model, ctx) { items.extend([ MenuItem::Separator, - MenuItemFields::new(*ATTACH_AS_AGENT_MODE_CONTEXT_TEXT) + MenuItemFields::new(attach_as_agent_mode_context_text()) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::AskAI(AskAISource::SelectedBlocks), )) @@ -16487,7 +16527,7 @@ impl TerminalView { } else { items.extend([ MenuItem::Separator, - MenuItemFields::new("Ask Warp AI") + MenuItemFields::new(i18n::t("ai_assistant.ask_warp_ai")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::AskAI(AskAISource::SelectedBlockOrText), )) @@ -16502,26 +16542,29 @@ impl TerminalView { } if is_single_selection { - let mut copy_output_menu_item = MenuItemFields::new("Copy output") - .with_on_select_action(TerminalAction::ContextMenu( - ContextMenuAction::CopyBlockOutputs, - )) - .with_disabled(tail_block.output_grid().is_empty()); + let mut copy_output_menu_item = + MenuItemFields::new(i18n::t("terminal.context_menu.copy_output")) + .with_on_select_action(TerminalAction::ContextMenu( + ContextMenuAction::CopyBlockOutputs, + )) + .with_disabled(tail_block.output_grid().is_empty()); // If there is an active filter on a block, then we want to display a // Copy filtered output option and assign the "terminal:copy_outputs" keybinding to it. if tail_block.has_active_filter() { items.insert( 1, - MenuItemFields::new("Copy filtered output") - .with_on_select_action(TerminalAction::ContextMenu( - ContextMenuAction::CopyBlockFilteredOutputs, - )) - .with_key_shortcut_label(keybinding_name_to_display_string( - "terminal:copy_outputs", - ctx, - )) - .into_item(), + MenuItemFields::new(i18n::t( + "terminal.context_menu.copy_filtered_output", + )) + .with_on_select_action(TerminalAction::ContextMenu( + ContextMenuAction::CopyBlockFilteredOutputs, + )) + .with_key_shortcut_label(keybinding_name_to_display_string( + "terminal:copy_outputs", + ctx, + )) + .into_item(), ); items.insert(2, copy_output_menu_item.into_item()); } else { @@ -16552,24 +16595,28 @@ impl TerminalView { )) .into_item(), ]); - items.append(&mut vec![MenuItemFields::new("Toggle block filter") - .with_on_select_action(TerminalAction::ToggleBlockFilterOnSelectedOrLastBlock( - ToggleBlockFilterSource::ContextMenu, - )) - .with_key_shortcut_label(keybinding_name_to_display_string( - TOGGLE_BLOCK_FILTER_KEYBINDING, - ctx, - )) - .into_item()]); - items.append(&mut vec![MenuItemFields::new("Toggle bookmark") - .with_on_select_action(TerminalAction::ContextMenu( - ContextMenuAction::ToggleBookmark, - )) - .with_key_shortcut_label(keybinding_name_to_display_string( - "terminal:bookmark_selected_block", - ctx, - )) - .into_item()]); + items.append(&mut vec![MenuItemFields::new(i18n::t( + "terminal.context_menu.toggle_block_filter", + )) + .with_on_select_action(TerminalAction::ToggleBlockFilterOnSelectedOrLastBlock( + ToggleBlockFilterSource::ContextMenu, + )) + .with_key_shortcut_label(keybinding_name_to_display_string( + TOGGLE_BLOCK_FILTER_KEYBINDING, + ctx, + )) + .into_item()]); + items.append(&mut vec![MenuItemFields::new(i18n::t( + "terminal.context_menu.toggle_bookmark", + )) + .with_on_select_action(TerminalAction::ContextMenu( + ContextMenuAction::ToggleBookmark, + )) + .with_key_shortcut_label(keybinding_name_to_display_string( + "terminal:bookmark_selected_block", + ctx, + )) + .into_item()]); items.append(&mut vec![ MenuItem::Separator, @@ -16702,15 +16749,17 @@ impl TerminalView { if ChannelState::channel().is_dogfood() { items.push( - MenuItemFields::new("Fork from here (dev only)") - .with_on_select_action(TerminalAction::ContextMenu( - ContextMenuAction::ForkAIConversationFromExactExchange { - ai_block_view_id: *rich_content_view_id, - exchange_id: ai_metadata.exchange_id, - conversation_id: ai_metadata.conversation_id, - }, - )) - .into_item(), + MenuItemFields::new(i18n::t( + "terminal.context_menu.fork_from_here_dev", + )) + .with_on_select_action(TerminalAction::ContextMenu( + ContextMenuAction::ForkAIConversationFromExactExchange { + ai_block_view_id: *rich_content_view_id, + exchange_id: ai_metadata.exchange_id, + conversation_id: ai_metadata.conversation_id, + }, + )) + .into_item(), ); } } @@ -16720,14 +16769,16 @@ impl TerminalView { && !ai_metadata.ai_block_handle.as_ref(ctx).is_restored() { items.push( - MenuItemFields::new("Rewind to before here") - .with_on_select_action(TerminalAction::RewindAIConversation { - ai_block_view_id: *rich_content_view_id, - exchange_id: ai_metadata.exchange_id, - conversation_id: ai_metadata.conversation_id, - entrypoint: AgentModeRewindEntrypoint::ContextMenu, - }) - .into_item(), + MenuItemFields::new(i18n::t( + "terminal.context_menu.rewind_to_before_here", + )) + .with_on_select_action(TerminalAction::RewindAIConversation { + ai_block_view_id: *rich_content_view_id, + exchange_id: ai_metadata.exchange_id, + conversation_id: ai_metadata.conversation_id, + entrypoint: AgentModeRewindEntrypoint::ContextMenu, + }) + .into_item(), ); } @@ -16810,7 +16861,7 @@ impl TerminalView { return None; } Some( - MenuItemFields::new("Clear Blocks") + MenuItemFields::new(i18n::t("terminal.context_menu.clear_blocks")) .with_on_select_action(TerminalAction::ClearBuffer) .with_key_shortcut_label(keybinding_name_to_display_string( "terminal:clear_blocks", @@ -16826,16 +16877,18 @@ impl TerminalView { is_rprompt_shown: bool, position: PromptPosition, ) -> Vec> { - let mut items = vec![MenuItemFields::new("Copy prompt") - .with_on_select_action(TerminalAction::ContextMenu(ContextMenuAction::CopyPrompt { - position, - part: PromptPart::EntirePrompt, - })) - .into_item()]; + let mut items = vec![ + MenuItemFields::new(i18n::t("terminal.context_menu.copy_prompt")) + .with_on_select_action(TerminalAction::ContextMenu(ContextMenuAction::CopyPrompt { + position, + part: PromptPart::EntirePrompt, + })) + .into_item(), + ]; if is_rprompt_shown { items.push( - MenuItemFields::new("Copy right prompt") + MenuItemFields::new(i18n::t("terminal.context_menu.copy_right_prompt")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::CopyRprompt, )) @@ -16844,7 +16897,7 @@ impl TerminalView { } items.push( - MenuItemFields::new("Copy working directory") + MenuItemFields::new(i18n::t("terminal.context_menu.copy_working_directory")) .with_on_select_action(TerminalAction::ContextMenu(ContextMenuAction::CopyPrompt { position, part: PromptPart::Pwd, @@ -16854,7 +16907,7 @@ impl TerminalView { if is_on_git_branch { items.push( - MenuItemFields::new("Copy git branch") + MenuItemFields::new(i18n::t("terminal.context_menu.copy_git_branch")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::CopyPrompt { position, @@ -16876,28 +16929,28 @@ impl TerminalView { if ContextFlag::CreateNewSession.is_enabled() { items.extend(vec![ - MenuItemFields::new("Split pane right") + MenuItemFields::new(i18n::t("common.split_pane_right")) .with_on_select_action(TerminalAction::SplitRight(shell.clone())) .with_key_shortcut_label(keybinding_name_to_display_string( "pane_group:add_right", ctx, )) .into_item(), - MenuItemFields::new("Split pane left") + MenuItemFields::new(i18n::t("common.split_pane_left")) .with_on_select_action(TerminalAction::SplitLeft(shell.clone())) .with_key_shortcut_label(keybinding_name_to_display_string( "pane_group:add_left", ctx, )) .into_item(), - MenuItemFields::new("Split pane down") + MenuItemFields::new(i18n::t("common.split_pane_down")) .with_on_select_action(TerminalAction::SplitDown(shell.clone())) .with_key_shortcut_label(keybinding_name_to_display_string( "pane_group:add_down", ctx, )) .into_item(), - MenuItemFields::new("Split pane up") + MenuItemFields::new(i18n::t("common.split_pane_up")) .with_on_select_action(TerminalAction::SplitUp(shell)) .with_key_shortcut_label(keybinding_name_to_display_string( "pane_group:add_up", @@ -16921,7 +16974,7 @@ impl TerminalView { ); items.push( - MenuItemFields::new("Close pane") + MenuItemFields::new(i18n::t("common.close_pane")) .with_on_select_action(TerminalAction::Close) .with_key_shortcut_label( custom_tag_to_keystroke(CustomAction::CloseCurrentSession.into()) @@ -16970,7 +17023,7 @@ impl TerminalView { } fn prompt_context_menu_items(&self, ctx: &AppContext) -> Vec> { - let copy_prompt = MenuItemFields::new("Copy prompt") + let copy_prompt = MenuItemFields::new(i18n::t("terminal.context_menu.copy_prompt")) .with_on_select_action(TerminalAction::ContextMenu(ContextMenuAction::CopyPrompt { position: PromptPosition::Input, part: PromptPart::EntirePrompt, @@ -16987,7 +17040,7 @@ impl TerminalView { .is_active(); let edit_menu_item = if has_cli_agent_session { FeatureFlag::AgentToolbarEditor.is_enabled().then(|| { - MenuItemFields::new("Edit CLI agent toolbelt") + MenuItemFields::new(i18n::t("terminal.context_menu.edit_cli_agent_toolbelt")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::EditCLIAgentToolbar, )) @@ -16995,7 +17048,7 @@ impl TerminalView { }) } else if is_agent_view_active { FeatureFlag::AgentToolbarEditor.is_enabled().then(|| { - MenuItemFields::new("Edit agent toolbelt") + MenuItemFields::new(i18n::t("terminal.context_menu.edit_agent_toolbelt")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::EditAgentToolbar, )) @@ -17003,7 +17056,7 @@ impl TerminalView { }) } else { Some( - MenuItemFields::new("Edit prompt") + MenuItemFields::new(i18n::t("terminal.context_menu.edit_prompt")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::EditPrompt, )) @@ -17016,7 +17069,7 @@ impl TerminalView { let mut items = vec![copy_prompt]; if self.is_rprompt_shown(&self.model.lock()) { items.push( - MenuItemFields::new("Copy right prompt") + MenuItemFields::new(i18n::t("terminal.context_menu.copy_right_prompt")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::CopyRprompt, )) @@ -17075,12 +17128,12 @@ impl TerminalView { if !selected_input_text.is_empty() { items.extend([ - MenuItemFields::new("Cut") + MenuItemFields::new(i18n::t("common.cut")) .with_on_select_action(TerminalAction::InputContextMenuItem( InputContextMenuAction::CutSelectedText, )) .into_item(), - MenuItemFields::new("Copy") + MenuItemFields::new(i18n::t("common.copy")) .with_on_select_action(TerminalAction::InputContextMenuItem( InputContextMenuAction::CopySelectedText, )) @@ -17094,7 +17147,7 @@ impl TerminalView { if !all_current_input_text.is_empty() & selected_input_text.is_empty() { items.push( - MenuItemFields::new("Select all") + MenuItemFields::new(i18n::t("common.select_all")) .with_on_select_action(TerminalAction::InputContextMenuItem( InputContextMenuAction::SelectAll, )) @@ -17108,7 +17161,7 @@ impl TerminalView { } items.push( - MenuItemFields::new("Paste") + MenuItemFields::new(i18n::t("common.paste")) .with_on_select_action(TerminalAction::InputContextMenuItem( InputContextMenuAction::Paste, )) @@ -17126,7 +17179,7 @@ impl TerminalView { // Section 2: AI Command Search, Ask Warp AI items.extend([ MenuItem::Separator, - MenuItemFields::new("Command search") + MenuItemFields::new(i18n::t("terminal.context_menu.command_search")) .with_on_select_action(TerminalAction::InputContextMenuItem( InputContextMenuAction::ShowCommandSearch, )) @@ -17140,7 +17193,7 @@ impl TerminalView { if AISettings::as_ref(ctx).is_any_ai_enabled(ctx) { items.push( - MenuItemFields::new("AI command search") + MenuItemFields::new(i18n::t("terminal.context_menu.ai_command_search")) .with_on_select_action(TerminalAction::InputContextMenuItem( InputContextMenuAction::ShowAICommandSearch, )) @@ -17154,7 +17207,7 @@ impl TerminalView { if !selected_input_text.is_empty() && !FeatureFlag::AgentMode.is_enabled() { items.push( - MenuItemFields::new("Ask Warp AI") + MenuItemFields::new(i18n::t("ai_assistant.ask_warp_ai")) .with_on_select_action(TerminalAction::InputContextMenuItem( InputContextMenuAction::AskWarpAI, )) @@ -17167,7 +17220,7 @@ impl TerminalView { if !all_current_input_text.is_empty() && WarpDriveSettings::is_warp_drive_enabled(ctx) { items.extend([ MenuItem::Separator, - MenuItemFields::new("Save as workflow") + MenuItemFields::new(i18n::t("terminal.context_menu.save_as_workflow")) .with_on_select_action(TerminalAction::InputContextMenuItem( InputContextMenuAction::SaveAsWorkflow, )) @@ -17179,13 +17232,13 @@ impl TerminalView { if !is_editor_disabled { let input_settings = InputSettings::as_ref(ctx); let inverse_action = if *input_settings.show_hint_text { - "Hide" + i18n::t("terminal.context_menu.hide_input_hint_text") } else { - "Show" + i18n::t("terminal.context_menu.show_input_hint_text") }; items.push(MenuItem::Separator); items.push( - MenuItemFields::new(format!("{inverse_action} input hint text")) + MenuItemFields::new(inverse_action) .with_on_select_action(TerminalAction::InputContextMenuItem( InputContextMenuAction::ToggleInputHintText, )) @@ -17331,7 +17384,7 @@ impl TerminalView { model.selection_to_string(semantic_selection, self.is_inverted_blocklist(ctx), ctx); if selection_string.is_some() { menu_items.push( - MenuItemFields::new("Copy") + MenuItemFields::new(i18n::t("common.copy")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::CopySelectedText, )) @@ -17342,9 +17395,9 @@ impl TerminalView { menu_items.extend([ MenuItem::Separator, MenuItemFields::new(if FeatureFlag::AgentMode.is_enabled() { - *ATTACH_AS_AGENT_MODE_CONTEXT_TEXT + attach_as_agent_mode_context_text() } else { - ASK_AI_ASSISTANT_TEXT + i18n::t("ai_assistant.ask_warp_ai") }) .with_on_select_action(TerminalAction::ContextMenu(ContextMenuAction::AskAI( AskAISource::SelectedTerminalText, @@ -19071,11 +19124,17 @@ impl TerminalView { AskAIType::FromBlock { block_index, .. } => { context_block_indices.insert(*block_index); - (None, Some(DEFAULT_ASK_AI_AUTOSUGGESTION_TEXT)) + ( + None, + Some(i18n::t("terminal.ask_ai.default_autosuggestion")), + ) } AskAIType::FromBlocks { block_indices } => { context_block_indices.extend(block_indices); - (None, Some(DEFAULT_ASK_AI_AUTOSUGGESTION_TEXT)) + ( + None, + Some(i18n::t("terminal.ask_ai.default_autosuggestion")), + ) } AskAIType::FromAICommandSearch { query } => { @@ -19121,7 +19180,7 @@ impl TerminalView { if input.buffer_text(ctx).is_empty() { if let Some(autosuggestion) = auto_suggestion { input.set_autosuggestion( - autosuggestion, + autosuggestion.as_str(), AutosuggestionType::AgentModeQuery { context_block_ids: selected_block_ids, was_intelligent_autosuggestion: false, @@ -20664,7 +20723,7 @@ impl TerminalView { let password_trigger = NotificationsTrigger::NeedsAttention; let notification_content = password_trigger.create_notification_content( active_block.command_to_string(), - "Command is waiting for a password".to_string(), + i18n::t("terminal.notification.command_waiting_for_password"), ); ctx.emit(Event::SendNotification(notification_content)); send_telemetry_from_ctx!( @@ -20745,13 +20804,13 @@ impl TerminalView { let Some(ambient_agent_view_model) = self.ambient_agent_view_model.clone() else { self.restore_followup_prompt_after_failed_submission(&prompt, ctx); - self.show_error_toast("Couldn't continue this cloud task.".to_string(), ctx); + self.show_error_toast(i18n::t("terminal.toast.couldnt_continue_cloud_task"), ctx); return true; }; if ambient_agent_view_model.as_ref(ctx).task_id() != Some(task_id) { self.restore_followup_prompt_after_failed_submission(&prompt, ctx); - self.show_error_toast("Couldn't continue this cloud task.".to_string(), ctx); + self.show_error_toast(i18n::t("terminal.toast.couldnt_continue_cloud_task"), ctx); return true; } @@ -20832,7 +20891,7 @@ impl TerminalView { { return; } - self.show_error_toast("Couldn't continue this cloud task.".to_string(), ctx); + self.show_error_toast(i18n::t("terminal.toast.couldnt_continue_cloud_task"), ctx); } InputEvent::CancelSharedSessionConversation { server_conversation_token, @@ -21678,23 +21737,29 @@ impl TerminalView { let show_banner = if honor_ps1 { let banner_content = if shell_plugins.contains("p10k_unsupported") { Some(BannerTextContent::formatted_text(vec![ - FormattedTextFragment::bold("Powerlevel10k now supports Warp! "), - FormattedTextFragment::plain_text( - "You seem to be running an older (unsupported) version, please follow ", - ), + FormattedTextFragment::bold(i18n::t( + "terminal.warning.powerlevel10k_supports_warp", + )), + FormattedTextFragment::plain_text(i18n::t( + "terminal.warning.old_prompt_version_prefix", + )), FormattedTextFragment::hyperlink( - "these instructions", + i18n::t("terminal.warning.these_instructions"), P10K_UPDATE_INSTRUCTIONS_URL, ), - FormattedTextFragment::plain_text(" to update to the latest version."), + FormattedTextFragment::plain_text(i18n::t( + "terminal.warning.update_latest_suffix", + )), ])) } else if shell_plugins.contains("pure") { Some(BannerTextContent::formatted_text(vec![ - FormattedTextFragment::plain_text( - "Pure is not yet supported in Warp. You might consider one of the \ - supported prompts as an alternative. ", + FormattedTextFragment::plain_text(i18n::t( + "terminal.warning.pure_not_supported", + )), + FormattedTextFragment::hyperlink( + i18n::t("common.learn_more"), + PROMPT_COMPATIBILITY_URL, ), - FormattedTextFragment::hyperlink("Learn more", PROMPT_COMPATIBILITY_URL), ])) } else { None @@ -22498,10 +22563,12 @@ impl TerminalView { }; let start = block.start_ts().map_or_else(String::new, |b| { - format!("Started at: {}", b.format("%a %b %-d at %-I:%M:%S %p")) + i18n::t("terminal.block.started_at") + .replace("{time}", &b.format("%a %b %-d at %-I:%M:%S %p").to_string()) }); let end = block.completed_ts().map_or_else(String::new, |b| { - format!("\nCompleted at: {}", b.format("%a %b %-d at %-I:%M:%S %p")) + i18n::t("terminal.block.completed_at") + .replace("{time}", &b.format("%a %b %-d at %-I:%M:%S %p").to_string()) }); format!("{start}{end}") } @@ -22675,7 +22742,7 @@ impl TerminalView { render_hoverable_block_button( icon, Some(ToolbeltButtonTooltip { - label: "Filter block output".to_string(), + label: i18n::t("terminal.block_filter.placeholder"), tool_tip_below_button, }), should_disable_filter_button, @@ -22723,7 +22790,7 @@ impl TerminalView { render_hoverable_block_button( icon, Some(ToolbeltButtonTooltip { - label: "Bookmark this block to quickly scroll to it".to_string(), + label: i18n::t("terminal.block.bookmark_tooltip"), tool_tip_below_button, }), false, @@ -22783,9 +22850,9 @@ impl TerminalView { && input_mode.is_inverted_blocklist() && is_long_running_command { - "Lock scrolling at bottom of block".to_string() + i18n::t("terminal.block.lock_scroll_at_bottom_tooltip") } else { - "Jump to the bottom of this block".to_string() + i18n::t("terminal.block.jump_to_bottom_tooltip") }; let tool_tip = appearance @@ -22984,13 +23051,13 @@ impl TerminalView { .notifications_error_banner .error .as_ref() - .map(|e| e.notifications_error_banner_title()) - .unwrap_or("Error sending notification"); + .map(|e| e.notifications_error_banner_title().to_string()) + .unwrap_or_else(|| i18n::t("terminal.notification.error_sending")); inline_banners.insert( state.banner_id, render_inline_notifications_error_banner( - banner_title, + banner_title.as_str(), state, &self.inline_banners_state.notifications_error_banner.error, appearance, @@ -23268,9 +23335,13 @@ impl TerminalView { .finish(), ) .with_child( - Text::new_inline("Loading session...", appearance.ui_font_family(), 14.) - .with_color(color.into()) - .finish(), + Text::new_inline( + i18n::t("terminal.loading_session"), + appearance.ui_font_family(), + 14., + ) + .with_color(color.into()) + .finish(), ) .with_cross_axis_alignment(CrossAxisAlignment::Center) .finish(), @@ -24327,19 +24398,23 @@ impl TerminalView { let model = self.model.lock(); model.block_list().block_at(index).map(|block| { let status = if block.has_failed() { - format!("failed, status code {}", block.exit_code().value()) + i18n::t("terminal.a11y.block_failed_status") + .replace("{code}", &block.exit_code().value().to_string()) } else if block.is_background() { - "background".to_string() + i18n::t("terminal.a11y.block_background_status") } else if block.is_done() { - "succeeded".to_string() + i18n::t("terminal.a11y.block_succeeded_status") } else { - "in progress".to_string() + i18n::t("terminal.a11y.block_in_progress_status") }; AccessibilityContent::new( - format!("Block {index}: {}, {}.\n", block.command_to_string(), status), + i18n::t("terminal.a11y.block_summary") + .replace("{index}", &index.to_string()) + .replace("{command}", &block.command_to_string()) + .replace("{status}", &status), // TODO (a11y) Keybindings should be taken from the actual user's // configuration - "Press cmd-C to read and copy both command and output, and cmd-option-shift-C to read and copy output only. Press cmd-B to bookmark the block: you could navigate between bookmarked blocks quickly using option-up and option-down.", + i18n::t("terminal.a11y.block_selection_help"), WarpA11yRole::TextRole, ) }) @@ -24727,10 +24802,7 @@ impl TerminalView { ) { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "Can not invoke environment variable subshell in a non-local session" - .to_owned(), - ), + DismissibleToast::error(i18n::t("terminal.toast.non_local_env_subshell")), window_id, ctx, ); @@ -24822,7 +24894,7 @@ impl TerminalView { env_var_collection .title .clone() - .unwrap_or("Untitled".to_owned()), + .unwrap_or_else(|| i18n::t("common.untitled")), env_var_collection .vars .iter() @@ -24865,8 +24937,9 @@ impl TerminalView { let (shell_path_string, shell_type) = shell_session_info; if shell_type == ShellType::PowerShell { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = - DismissibleToast::error("PowerShell subshells not supported".to_owned()); + let toast = DismissibleToast::error(i18n::t( + "terminal.toast.powershell_subshell_not_supported", + )); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); return; @@ -24876,7 +24949,9 @@ impl TerminalView { // subshell start self.env_vars = env_var_collection.vars; self.model.lock().set_env_var_collection_name(Some( - env_var_collection.title.unwrap_or("Untitled".to_owned()), + env_var_collection + .title + .unwrap_or_else(|| i18n::t("common.untitled")), )); self.set_and_execute_subshell_command(&shell_path_string, shell_type, ctx); @@ -25383,7 +25458,7 @@ impl TypedActionView for TerminalView { } BookmarkBlock(_) | BookmarkSelectedBlock => { Custom(AccessibilityContent::new_without_help( - "Toggle Bookmark block", + i18n::t("terminal.a11y.toggle_bookmark_block"), WarpA11yRole::TextRole, )) } @@ -25393,8 +25468,10 @@ impl TypedActionView for TerminalView { .tail() .and_then(|index| self.selected_block_accessibility_content(index)) { - let num_selected_text = - format!("Selected {} blocks.", self.num_non_hidden_selected_blocks()); + let num_selected_text = i18n::t("terminal.a11y.selected_blocks").replace( + "{count}", + &self.num_non_hidden_selected_blocks().to_string(), + ); content.value = format!("{}\n{}", num_selected_text, content.value); Custom(content) } else { @@ -25402,41 +25479,39 @@ impl TypedActionView for TerminalView { } } SelectAllBlocks => Custom(AccessibilityContent::new_without_help( - format!( - "Selected all {} blocks.", - self.num_non_hidden_selected_blocks() + i18n::t("terminal.a11y.selected_all_blocks").replace( + "{count}", + &self.num_non_hidden_selected_blocks().to_string(), ), WarpA11yRole::TextRole, )), ScrollToBottomOfSelectedBlocks => Custom(AccessibilityContent::new_without_help( - "Scrolled to bottom of selected block".to_string(), + i18n::t("terminal.a11y.scrolled_bottom_selected_block"), WarpA11yRole::TextRole, )), ScrollToTopOfSelectedBlocks => Custom(AccessibilityContent::new_without_help( - "Scrolled to top of selected block".to_string(), + i18n::t("terminal.a11y.scrolled_top_selected_block"), WarpA11yRole::TextRole, )), ScrollToBottomOfOverhangingBlock(_) => Custom(AccessibilityContent::new_without_help( - "Scrolled to bottom of bottommost visible block".to_string(), + i18n::t("terminal.a11y.scrolled_bottom_visible_block"), WarpA11yRole::TextRole, )), CopyOutputs => { let mut outputs = vec![]; self.with_non_hidden_selected_blocks( |block| { - outputs.push(format!( - "Block {}.\nOutput: {}", - block.index(), - block.output_to_string() - )); + outputs.push( + i18n::t("terminal.a11y.block_output") + .replace("{index}", &block.index().to_string()) + .replace("{output}", &block.output_to_string()), + ); }, ctx, ); - let text = format!( - "Copied {} block outputs.\n{}", - outputs.len(), - outputs.join("\n") - ); + let text = i18n::t("terminal.a11y.copied_block_outputs") + .replace("{count}", &outputs.len().to_string()) + .replace("{outputs}", &outputs.join("\n")); Custom(AccessibilityContent::new_without_help( text, WarpA11yRole::TextRole, @@ -25446,16 +25521,18 @@ impl TypedActionView for TerminalView { let mut blocks = vec![]; self.with_non_hidden_selected_blocks( |block| { - blocks.push(format!( - "Block {}: {}. Output: {}", - block.index(), - block.command_to_string(), - block.output_to_string() - )); + blocks.push( + i18n::t("terminal.a11y.block_command_output") + .replace("{index}", &block.index().to_string()) + .replace("{command}", &block.command_to_string()) + .replace("{output}", &block.output_to_string()), + ); }, ctx, ); - let text = format!("Copied {} blocks.\n{}", blocks.len(), blocks.join("\n")); + let text = i18n::t("terminal.a11y.copied_blocks") + .replace("{count}", &blocks.len().to_string()) + .replace("{blocks}", &blocks.join("\n")); Custom(AccessibilityContent::new_without_help( text, WarpA11yRole::TextRole, @@ -25463,17 +25540,17 @@ impl TypedActionView for TerminalView { } FocusInputAndClearSelection => { Custom(AccessibilityContent::new( - INPUT_A11Y_LABEL, + i18n::t("terminal.input.a11y_label"), // TODO (a11y) use bindings from user settings - INPUT_A11Y_HELPER, + i18n::t("terminal.input.a11y_helper"), WarpA11yRole::TextareaRole, )) } KeyDown(key) => { let label = if key.eq("\x1b") { - INPUT_A11Y_LABEL + i18n::t("terminal.input.a11y_label") } else { - key + key.to_string() }; Custom(AccessibilityContent::new_without_help( label, @@ -25481,19 +25558,20 @@ impl TypedActionView for TerminalView { )) } OpenBlockFilterEditor(block_index) => Custom(AccessibilityContent::new_without_help( - format!("Open block filter editor for block {block_index}"), + i18n::t("terminal.a11y.open_block_filter_editor") + .replace("{index}", &block_index.to_string()), WarpA11yRole::TextRole, )), ShowInitializationBlock => Custom(AccessibilityContent::new_without_help( - "Showed initialization block", + i18n::t("terminal.a11y.showed_initialization_block"), WarpA11yRole::TextareaRole, )), ShowWarpifySettings => Custom(AccessibilityContent::new_without_help( - "Opened Warpify Settings", + i18n::t("terminal.a11y.opened_warpify_settings"), WarpA11yRole::ButtonRole, )), OpenFilesPalette { .. } => Custom(AccessibilityContent::new_without_help( - "Opened file search palette", + i18n::t("terminal.a11y.opened_file_search_palette"), WarpA11yRole::ButtonRole, )), InsertCommandCorrection { .. } @@ -25562,28 +25640,27 @@ impl TypedActionView for TerminalView { OpenCodeInWarp { .. } => ActionAccessibilityContent::from_debug(), OpenInWarpBanner(action) => self.open_in_warp_banner_accessibility_content(*action), OpenAIBlockAttachedBlocksMenu { .. } => Custom(AccessibilityContent::new_without_help( - "Open list of blocks attached as context to this AI query.".to_owned(), + i18n::t("terminal.a11y.open_ai_attached_blocks_menu"), WarpA11yRole::PopoverRole, )), OpenAIBlockOverflowMenu { .. } => Custom(AccessibilityContent::new_without_help( - "Open overflow menu with copy options for this AI block.".to_owned(), + i18n::t("terminal.a11y.open_ai_block_overflow_menu"), WarpA11yRole::PopoverRole, )), RewindAIConversation { .. } => Custom(AccessibilityContent::new_without_help( - "Show confirmation dialog to rewind to before this point in the AI conversation." - .to_owned(), + i18n::t("terminal.a11y.rewind_ai_confirmation"), WarpA11yRole::ButtonRole, )), ExecuteRewindAIConversation { .. } => Custom(AccessibilityContent::new_without_help( - "Execute rewind to before this point in the AI conversation.".to_owned(), + i18n::t("terminal.a11y.execute_rewind_ai"), WarpA11yRole::ButtonRole, )), SelectAIAttachedBlock(_) => Custom(AccessibilityContent::new_without_help( - "Click on a block attached as context to this AI query.".to_owned(), + i18n::t("terminal.a11y.select_ai_attached_block"), WarpA11yRole::ButtonRole, )), PickRepoToOpen => Custom(AccessibilityContent::new_without_help( - "Use file picker to select a git repository".to_owned(), + i18n::t("terminal.a11y.pick_repo_to_open"), WarpA11yRole::PopoverRole, )), #[cfg(feature = "voice_input")] @@ -26032,10 +26109,12 @@ impl TypedActionView for TerminalView { let warpify_keybinding = keybinding_name_to_keystroke("terminal:warpify_subshell", ctx); + let title = i18n::t("terminal.warpify.subshell_title"); + let lowercase_title = i18n::t("terminal.warpify.subshell_lowercase_title"); self.show_warpify_banner( WarpificationMode::subshell(command.to_owned()), - "Subshell", - "subshell", + &title, + &lowercase_title, warpify_keybinding, TelemetryEvent::ShowSubshellBanner, ctx, @@ -26044,10 +26123,12 @@ impl TypedActionView for TerminalView { ShowWarpifySshBanner(command, host) => { let warpify_keybinding = keybinding_name_to_keystroke("terminal:warpify_ssh_session", ctx); + let title = i18n::t("terminal.warpify.ssh_session_title"); + let lowercase_title = i18n::t("terminal.warpify.ssh_session_lowercase_title"); self.show_warpify_banner( WarpificationMode::ssh(command.to_string(), host.to_owned()), - "SSH Session", - "SSH session", + &title, + &lowercase_title, warpify_keybinding, TelemetryEvent::SshTmuxWarpifyBannerDisplayed, ctx, @@ -26649,9 +26730,9 @@ impl TypedActionView for TerminalView { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "Bundled skills cannot be edited".to_string(), - ), + DismissibleToast::error(i18n::t( + "terminal.toast.bundled_skills_cannot_edit", + )), window_id, ctx, ); @@ -26666,9 +26747,9 @@ impl TypedActionView for TerminalView { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "Editing skills is not supported in this build".to_string(), - ), + DismissibleToast::error(i18n::t( + "terminal.toast.editing_skills_unsupported", + )), window_id, ctx, ); diff --git a/app/src/terminal/view/agent_view.rs b/app/src/terminal/view/agent_view.rs index 8e5e4c8f1e..9ccbd72027 100644 --- a/app/src/terminal/view/agent_view.rs +++ b/app/src/terminal/view/agent_view.rs @@ -63,10 +63,9 @@ impl TerminalView { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "Cannot start a new conversation while agent is monitoring a command." - .to_string(), - ), + DismissibleToast::error(i18n::t( + "terminal.input.cannot_start_while_monitoring", + )), window_id, ctx, ); @@ -312,7 +311,7 @@ impl TerminalView { key: "enter".to_owned(), ..Default::default() }), - MessageItem::text("again to send to agent"), + MessageItem::text(i18n::t("terminal.message_bar.again_to_send_to_agent")), ]) .with_text_color(appearance.theme().ansi_fg_magenta()); self.ephemeral_message_model.update(ctx, |model, ctx| { diff --git a/app/src/terminal/view/ambient_agent/auth_secret_ftux_dropdown.rs b/app/src/terminal/view/ambient_agent/auth_secret_ftux_dropdown.rs index 24b964c11d..20553bdf04 100644 --- a/app/src/terminal/view/ambient_agent/auth_secret_ftux_dropdown.rs +++ b/app/src/terminal/view/ambient_agent/auth_secret_ftux_dropdown.rs @@ -93,7 +93,7 @@ impl AuthSecretFtuxDropdown { }, ctx, ); - editor.set_placeholder_text("Search secrets or create a new one", ctx); + editor.set_placeholder_text(i18n::t("terminal.auth_secret.search_placeholder"), ctx); editor }); @@ -320,12 +320,15 @@ impl AuthSecretFtuxDropdown { // Compact (modal) mode: only render "+ New …" entries. for (index, info) in auth_secret_types_for_harness(harness).iter().enumerate() { items.push(MenuItem::Item( - MenuItemFields::new(format!("New {}", info.display_name)) - .with_font_size_override(FONT_SIZE) - .with_padding_override(MENU_ITEM_VERTICAL_PADDING, MENU_HORIZONTAL_PADDING) - .with_override_hover_background_color(hover_background) - .with_icon(Icon::Plus) - .with_on_select_action(FtuxDropdownAction::SelectNewType(index)), + MenuItemFields::new( + i18n::t("terminal.auth_secret.new_secret_type") + .replace("{name}", &info.display_name), + ) + .with_font_size_override(FONT_SIZE) + .with_padding_override(MENU_ITEM_VERTICAL_PADDING, MENU_HORIZONTAL_PADDING) + .with_override_hover_background_color(hover_background) + .with_icon(Icon::Plus) + .with_on_select_action(FtuxDropdownAction::SelectNewType(index)), )); } self.menu.update(ctx, |menu, ctx| { @@ -358,7 +361,7 @@ impl AuthSecretFtuxDropdown { } if !matched { items.push(MenuItem::Item( - MenuItemFields::new("No secrets found") + MenuItemFields::new(i18n::t("terminal.auth_secret.no_secrets_found")) .with_font_size_override(FONT_SIZE) .with_padding_override( MENU_ITEM_VERTICAL_PADDING, @@ -371,7 +374,7 @@ impl AuthSecretFtuxDropdown { } AuthSecretFetchState::NotFetched | AuthSecretFetchState::Loading => { items.push(MenuItem::Item( - MenuItemFields::new("Loading…") + MenuItemFields::new(i18n::t("terminal.auth_secret.loading")) .with_font_size_override(FONT_SIZE) .with_padding_override(MENU_ITEM_VERTICAL_PADDING, MENU_HORIZONTAL_PADDING) .with_disabled(true) @@ -380,7 +383,7 @@ impl AuthSecretFtuxDropdown { } AuthSecretFetchState::Failed(_) => { items.push(MenuItem::Item( - MenuItemFields::new("Unable to load secrets") + MenuItemFields::new(i18n::t("terminal.auth_secret.unable_to_load")) .with_font_size_override(FONT_SIZE) .with_padding_override(MENU_ITEM_VERTICAL_PADDING, MENU_HORIZONTAL_PADDING) .with_disabled(true) @@ -393,12 +396,15 @@ impl AuthSecretFtuxDropdown { for (index, info) in auth_secret_types_for_harness(harness).iter().enumerate() { items.push(MenuItem::Item( - MenuItemFields::new(format!("New {}", info.display_name)) - .with_font_size_override(FONT_SIZE) - .with_padding_override(MENU_ITEM_VERTICAL_PADDING, MENU_HORIZONTAL_PADDING) - .with_override_hover_background_color(hover_background) - .with_icon(Icon::Plus) - .with_on_select_action(FtuxDropdownAction::SelectNewType(index)), + MenuItemFields::new( + i18n::t("terminal.auth_secret.new_secret_type") + .replace("{name}", &info.display_name), + ) + .with_font_size_override(FONT_SIZE) + .with_padding_override(MENU_ITEM_VERTICAL_PADDING, MENU_HORIZONTAL_PADDING) + .with_override_hover_background_color(hover_background) + .with_icon(Icon::Plus) + .with_on_select_action(FtuxDropdownAction::SelectNewType(index)), )); } @@ -406,8 +412,8 @@ impl AuthSecretFtuxDropdown { items.push(MenuItem::Item( MenuItemFields::new_with_label( - "Skip (advanced)", - "Only if your key is already set in the environment (e.g. injected as a Kubernetes secret)", + i18n::t("terminal.auth_secret.skip_advanced"), + i18n::t("terminal.auth_secret.skip_advanced_label"), ) .with_font_size_override(FONT_SIZE) .with_padding_override(MENU_ITEM_VERTICAL_PADDING, MENU_HORIZONTAL_PADDING) @@ -499,8 +505,7 @@ impl AuthSecretFtuxDropdown { let theme = appearance.theme(); let color = internal_colors::text_sub(theme, theme.surface_1()); Text::new_inline( - "No secrets found. Save to use this value directly or click the key to add a secret." - .to_string(), + i18n::t("terminal.auth_secret.no_matches_helper"), appearance.ui_font_family(), HELPER_FONT_SIZE, ) diff --git a/app/src/terminal/view/ambient_agent/auth_secret_ftux_view.rs b/app/src/terminal/view/ambient_agent/auth_secret_ftux_view.rs index c65c76fc5b..9ee7062bb5 100644 --- a/app/src/terminal/view/ambient_agent/auth_secret_ftux_view.rs +++ b/app/src/terminal/view/ambient_agent/auth_secret_ftux_view.rs @@ -143,7 +143,13 @@ pub struct AuthSecretFtuxView { impl AuthSecretFtuxView { pub fn new(harness: Harness, ctx: &mut ViewContext) -> Self { - let name_editor = make_single_line_editor(Some("e.g. My API Key"), false, ctx); + let name_editor = make_single_line_editor( + Some(i18n::t( + "terminal.ambient_agent.auth_secret.name_placeholder", + )), + false, + ctx, + ); ctx.subscribe_to_view(&name_editor, |me, _, event, ctx| { me.handle_form_editor_event(0, event, ctx); @@ -218,7 +224,8 @@ impl AuthSecretFtuxView { state.is_saving = false; state.pending_name = None; let window_id = ctx.window_id(); - let message = format!("Failed to save API key: {error}"); + let message = i18n::t("terminal.ambient_agent.auth_secret.save_failed") + .replace("{error}", error); ToastStack::handle(ctx).update(ctx, |ts, ctx| { ts.add_ephemeral_toast( DismissibleToast::error(message), @@ -554,7 +561,8 @@ impl AuthSecretFtuxView { let mut editors = Vec::with_capacity(info.fields.len()); for (field_idx, field) in info.fields.iter().enumerate() { let placeholder = field.placeholder.unwrap_or(field.label); - let editor = make_single_line_editor(Some(placeholder), field.sensitive, ctx); + let editor = + make_single_line_editor(Some(placeholder.to_string()), field.sensitive, ctx); let editor_index = field_idx + 1; ctx.subscribe_to_view(&editor, move |me, _, event, ctx| { me.handle_form_editor_event(editor_index, event, ctx); @@ -723,7 +731,7 @@ impl AuthSecretFtuxView { ctx: &mut ViewContext, ) { let window_id = ctx.window_id(); - let message = format!("API key '{name}' saved."); + let message = i18n::t("terminal.ambient_agent.auth_secret.saved").replace("{name}", &name); ToastStack::handle(ctx).update(ctx, |ts, ctx| { ts.add_ephemeral_toast(DismissibleToast::default(message), window_id, ctx); }); @@ -741,10 +749,11 @@ impl AuthSecretFtuxView { let main_text = { let description = if self.current_type_info().is_some() { - "Enter your credentials below.".to_string() + i18n::t("terminal.ambient_agent.auth_secret.enter_credentials") } else { let display_name = harness_display::display_name(self.harness); - format!("Select an API key type to use {display_name} in the cloud with Oz.") + i18n::t("terminal.ambient_agent.auth_secret.select_type_description") + .replace("{display_name}", display_name) }; Text::new_inline(description, font_family, DESCRIPTION_FONT_SIZE) .with_color(theme.foreground().into()) @@ -753,7 +762,7 @@ impl AuthSecretFtuxView { }; let privacy_text = Text::new_inline( - "Your credentials are encrypted end-to-end. ".to_string(), + i18n::t("terminal.auth_secret.credentials_encrypted"), font_family, TYPE_DESCRIPTION_FONT_SIZE, ) @@ -766,8 +775,8 @@ impl AuthSecretFtuxView { .current_type_info() .map(|info| info.learn_more_url) .unwrap_or_else(|| learn_more_url_for_harness(self.harness)); - let learn_more_label = - format!("Learn more about authentication for {harness_name} in Warp."); + let learn_more_label = i18n::t("terminal.ambient_agent.auth_secret.learn_more") + .replace("{harness_name}", harness_name); let learn_more = Hoverable::new(self.learn_more_mouse_state.clone(), move |state| { let color = if state.is_hovered() { accent_color @@ -874,7 +883,7 @@ impl AuthSecretFtuxView { let theme = appearance.theme(); let label_color = internal_colors::text_sub(theme, theme.surface_1()); let label = Text::new_inline( - "Share with team".to_string(), + i18n::t("terminal.auth_secret.share_with_team"), appearance.ui_font_family(), TYPE_DESCRIPTION_FONT_SIZE, ) @@ -901,15 +910,19 @@ impl AuthSecretFtuxView { .with_spacing(FORM_FIELD_SPACING); column.add_child( - Container::new(self.render_field_label("NAME", app)) - .with_padding_top(CONTENT_SECTION_SPACING) - .finish(), + Container::new(self.render_field_label( + &i18n::t("terminal.ambient_agent.auth_secret.name_label"), + app, + )) + .with_padding_top(CONTENT_SECTION_SPACING) + .finish(), ); column.add_child(self.render_editor_container(&self.name_editor, app)); for (idx, field) in info.fields.iter().enumerate() { let label = if field.optional { - format!("{} (optional)", field.label) + i18n::t("terminal.ambient_agent.auth_secret.optional_label") + .replace("{label}", field.label) } else { field.label.to_string() }; @@ -926,7 +939,7 @@ impl AuthSecretFtuxView { fn render_button( &self, - label: &'static str, + label: String, mouse_state: MouseStateHandle, background: Option, action: AuthSecretFtuxAction, @@ -949,7 +962,7 @@ impl AuthSecretFtuxView { }; Hoverable::new(mouse_state, move |_| { let inner = Container::new( - Text::new_inline(label.to_string(), font_family, BUTTON_FONT_SIZE) + Text::new_inline(label.clone(), font_family, BUTTON_FONT_SIZE) .with_style(Properties::default().weight(Weight::Semibold)) .with_color(text_color) .finish(), @@ -986,9 +999,9 @@ impl AuthSecretFtuxView { row.add_child(Expanded::new(1., Empty::new().finish()).finish()); let (label, action) = if self.creation_state.is_some() { - ("Back", AuthSecretFtuxAction::Back) + (i18n::t("common.back"), AuthSecretFtuxAction::Back) } else { - ("Cancel", AuthSecretFtuxAction::Cancel) + (i18n::t("common.cancel"), AuthSecretFtuxAction::Cancel) }; row.add_child(self.render_button( label, @@ -1002,7 +1015,7 @@ impl AuthSecretFtuxView { let accent_fill = Appearance::as_ref(app).theme().accent(); let continue_disabled = !self.can_submit_creation_form(app); row.add_child(self.render_button( - "Continue", + i18n::t("common.continue"), self.continue_mouse_state.clone(), Some(accent_fill), AuthSecretFtuxAction::Continue, @@ -1015,11 +1028,10 @@ impl AuthSecretFtuxView { } fn make_single_line_editor( - placeholder: Option<&str>, + placeholder: Option, is_password: bool, ctx: &mut ViewContext, ) -> ViewHandle { - let placeholder = placeholder.map(str::to_owned); ctx.add_typed_action_view(move |ctx| { let appearance = Appearance::as_ref(ctx); let mut editor = EditorView::single_line( diff --git a/app/src/terminal/view/ambient_agent/auth_secret_selector.rs b/app/src/terminal/view/ambient_agent/auth_secret_selector.rs index bc2307d54e..da10550604 100644 --- a/app/src/terminal/view/ambient_agent/auth_secret_selector.rs +++ b/app/src/terminal/view/ambient_agent/auth_secret_selector.rs @@ -52,16 +52,6 @@ const SIDECAR_HORIZONTAL_GAP: f32 = 4.; const MENU_MAX_HEIGHT: f32 = 280.; -const BUTTON_TOOLTIP: &str = "API key"; - -const MENU_HEADER_LABEL: &str = "API key"; - -const SIDECAR_HEADER_LABEL: &str = "Choose a type"; - -const NO_SECRET_LABEL: &str = "Inherit key from environment"; - -const NEW_ITEM_LABEL: &str = "New"; - const MAIN_MENU_SAVE_POSITION_ID: &str = "auth_secret_selector_main_menu"; type PendingDeleteKey = (Harness, String, SecretOwner); @@ -103,14 +93,17 @@ impl AuthSecretSelector { ctx: &mut ViewContext, ) -> Self { let button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new(NO_SECRET_LABEL, NakedHeaderButtonTheme) - .with_size(ButtonSize::AgentInputButton) - .with_menu(true) - .with_icon(Icon::Key) - .with_tooltip(BUTTON_TOOLTIP) - .on_click(|ctx| { - ctx.dispatch_typed_action(AuthSecretSelectorAction::ToggleMenu); - }) + ActionButton::new( + i18n::t("terminal.ambient_agent.auth_secret.inherit_from_environment"), + NakedHeaderButtonTheme, + ) + .with_size(ButtonSize::AgentInputButton) + .with_menu(true) + .with_icon(Icon::Key) + .with_tooltip(i18n::t("terminal.ambient_agent.auth_secret.api_key")) + .on_click(|ctx| { + ctx.dispatch_typed_action(AuthSecretSelectorAction::ToggleMenu); + }) }); let menu = ctx.add_typed_action_view(|_ctx| { @@ -315,7 +308,9 @@ impl AuthSecretSelector { .get(hovered_index) .map(|item| { matches!(item, - MenuItem::Item(fields) if fields.label() == NEW_ITEM_LABEL) + MenuItem::Item(fields) + if fields.on_select_action() + == Some(&AuthSecretSelectorAction::OpenNewTypeSidecar)) }) .unwrap_or(false) }); @@ -335,7 +330,9 @@ impl AuthSecretSelector { .as_ref(ctx) .selected_harness_auth_secret_name() .map(|s| s.to_string()) - .unwrap_or_else(|| NO_SECRET_LABEL.to_string()); + .unwrap_or_else(|| { + i18n::t("terminal.ambient_agent.auth_secret.inherit_from_environment") + }); self.button.update(ctx, |button, ctx| { button.set_label(label, ctx); }); @@ -401,7 +398,8 @@ impl AuthSecretSelector { // window) shouldn't pop a duplicate confirmation here. if removed_pending { let window_id = ctx.window_id(); - let message = format!("API key '{name}' deleted."); + let message = + i18n::t("terminal.ambient_agent.auth_secret.deleted").replace("{name}", &name); ToastStack::handle(ctx).update(ctx, |ts, ctx| { ts.add_ephemeral_toast(DismissibleToast::success(message), window_id, ctx); }); @@ -430,7 +428,9 @@ impl AuthSecretSelector { // double-toasting if another surface also tried to delete. if removed_pending { let window_id = ctx.window_id(); - let message = format!("Failed to delete API key '{name}': {error}"); + let message = i18n::t("terminal.ambient_agent.auth_secret.delete_failed") + .replace("{name}", &name) + .replace("{error}", &error); ToastStack::handle(ctx).update(ctx, |ts, ctx| { ts.add_ephemeral_toast(DismissibleToast::error(message), window_id, ctx); }); @@ -567,7 +567,7 @@ fn build_main_menu_items( header_text_color: pathfinder_color::ColorU, ) -> Vec> { let header = MenuItem::Header { - fields: MenuItemFields::new(MENU_HEADER_LABEL) + fields: MenuItemFields::new(i18n::t("terminal.ambient_agent.auth_secret.api_key")) .with_font_size_override(HEADER_FONT_SIZE) .with_override_text_color(header_text_color) .with_padding_override(6., MENU_HORIZONTAL_PADDING) @@ -579,11 +579,13 @@ fn build_main_menu_items( let mut items = vec![header]; items.push(MenuItem::Item( - MenuItemFields::new(NO_SECRET_LABEL) - .with_font_size_override(ITEM_FONT_SIZE) - .with_padding_override(ITEM_VERTICAL_PADDING, MENU_HORIZONTAL_PADDING) - .with_override_hover_background_color(hover_background) - .with_on_select_action(AuthSecretSelectorAction::ClearSecret), + MenuItemFields::new(i18n::t( + "terminal.ambient_agent.auth_secret.inherit_from_environment", + )) + .with_font_size_override(ITEM_FONT_SIZE) + .with_padding_override(ITEM_VERTICAL_PADDING, MENU_HORIZONTAL_PADDING) + .with_override_hover_background_color(hover_background) + .with_on_select_action(AuthSecretSelectorAction::ClearSecret), )); match fetch_state { @@ -603,14 +605,17 @@ fn build_main_menu_items( name: secret.name.clone(), owner: secret.owner.clone(), }) - .with_right_side_icon_a11y_label(format!("Delete API key {}", secret.name)) + .with_right_side_icon_a11y_label( + i18n::t("terminal.ambient_agent.auth_secret.delete_a11y") + .replace("{name}", &secret.name), + ) .with_right_side_icon_disabled(is_pending_delete); items.push(MenuItem::Item(fields)); } } AuthSecretFetchState::NotFetched | AuthSecretFetchState::Loading => { items.push(MenuItem::Item( - MenuItemFields::new("Loading…") + MenuItemFields::new(i18n::t("common.loading")) .with_font_size_override(ITEM_FONT_SIZE) .with_padding_override(ITEM_VERTICAL_PADDING, MENU_HORIZONTAL_PADDING) .with_disabled(true) @@ -619,7 +624,7 @@ fn build_main_menu_items( } AuthSecretFetchState::Failed(_) => { items.push(MenuItem::Item( - MenuItemFields::new("Unable to load secrets") + MenuItemFields::new(i18n::t("ai.auth_secret.unable_to_load_secrets")) .with_font_size_override(ITEM_FONT_SIZE) .with_padding_override(ITEM_VERTICAL_PADDING, MENU_HORIZONTAL_PADDING) .with_disabled(true) @@ -629,7 +634,7 @@ fn build_main_menu_items( } items.push(MenuItem::Item( - MenuItemFields::new(NEW_ITEM_LABEL) + MenuItemFields::new(i18n::t("common.new")) .with_font_size_override(ITEM_FONT_SIZE) .with_padding_override(ITEM_VERTICAL_PADDING, MENU_HORIZONTAL_PADDING) .with_override_hover_background_color(hover_background) @@ -660,7 +665,7 @@ fn build_sidecar_items( header_text_color: pathfinder_color::ColorU, ) -> Vec> { let header = MenuItem::Header { - fields: MenuItemFields::new(SIDECAR_HEADER_LABEL) + fields: MenuItemFields::new(i18n::t("terminal.ambient_agent.auth_secret.choose_type")) .with_font_size_override(HEADER_FONT_SIZE) .with_override_text_color(header_text_color) .with_padding_override(6., MENU_HORIZONTAL_PADDING) diff --git a/app/src/terminal/view/ambient_agent/block/entry.rs b/app/src/terminal/view/ambient_agent/block/entry.rs index 4df59e6528..0a559067b8 100644 --- a/app/src/terminal/view/ambient_agent/block/entry.rs +++ b/app/src/terminal/view/ambient_agent/block/entry.rs @@ -27,7 +27,6 @@ use crate::terminal::{BlockListSettings, TerminalManager, TerminalView}; use crate::ui_components::agent_icon::terminal_view_agent_icon_variant; use crate::ui_components::blended_colors; use crate::ui_components::icon_with_status::{render_icon_with_status, IconWithStatusVariant}; -const DEFAULT_CLOUD_AGENT_TITLE: &str = "New cloud agent"; #[derive(Default)] struct StateHandles { @@ -135,8 +134,8 @@ impl AmbientAgentEntryBlock { fn meaningful_title(title: &str) -> Option { let title = title.trim(); - (!title.is_empty() && !title.eq_ignore_ascii_case(DEFAULT_CLOUD_AGENT_TITLE)) - .then(|| title.to_owned()) + let default_title = i18n::t("terminal.ambient_agent.default_cloud_agent_title"); + (!title.is_empty() && !title.eq_ignore_ascii_case(&default_title)).then(|| title.to_owned()) } fn title_from_task_data(&self, app: &AppContext) -> Option { @@ -164,7 +163,7 @@ impl AmbientAgentEntryBlock { .and_then(|title| Self::meaningful_title(&title)) .or_else(|| self.title_from_task_data(app)) .or_else(|| self.title_from_spawn_request(app)) - .unwrap_or_else(|| DEFAULT_CLOUD_AGENT_TITLE.to_owned()) + .unwrap_or_else(|| i18n::t("terminal.ambient_agent.default_cloud_agent_title")) } fn ambient_agent_view_model<'a>( @@ -178,14 +177,18 @@ impl AmbientAgentEntryBlock { } /// Gets the detail text to display based on the ambient agent status. - fn detail_text(&self, app: &AppContext) -> Option<&'static str> { + fn detail_text(&self, app: &AppContext) -> Option { match self.ambient_agent_view_model(app)?.status() { Status::Setup | Status::Composing => None, - Status::WaitingForSession { .. } => Some("Starting environment..."), - Status::AgentRunning => Some("Agent is working on task"), - Status::Failed { .. } => Some("Agent failed"), - Status::NeedsGithubAuth { .. } => Some("Authentication required"), - Status::Cancelled { .. } => Some("Cancelled"), + Status::WaitingForSession { .. } => Some(i18n::t( + "terminal.ambient_agent.status.starting_environment", + )), + Status::AgentRunning => Some(i18n::t("terminal.ambient_agent.status.agent_working")), + Status::Failed { .. } => Some(i18n::t("terminal.ambient_agent.status.agent_failed")), + Status::NeedsGithubAuth { .. } => { + Some(i18n::t("terminal.ambient_agent.status.auth_required")) + } + Status::Cancelled { .. } => Some(i18n::t("terminal.ambient_agent.status.cancelled")), } } @@ -197,7 +200,7 @@ impl AmbientAgentEntryBlock { } Status::Failed { .. } => Some(ConversationStatus::Error), Status::NeedsGithubAuth { .. } => Some(ConversationStatus::Blocked { - blocked_action: "GitHub authentication required".to_owned(), + blocked_action: i18n::t("terminal.ambient_agent.status.github_auth_required"), }), Status::Cancelled { .. } => Some(ConversationStatus::Cancelled), } diff --git a/app/src/terminal/view/ambient_agent/block/harness_session_header.rs b/app/src/terminal/view/ambient_agent/block/harness_session_header.rs index d47ce50d77..e88c3ed5d7 100644 --- a/app/src/terminal/view/ambient_agent/block/harness_session_header.rs +++ b/app/src/terminal/view/ambient_agent/block/harness_session_header.rs @@ -32,7 +32,7 @@ impl HarnessSessionHeader { pub fn new(block_id: BlockId, cli_agent: Option) -> Self { let cli_name = cli_agent .map(|agent| agent.display_name().to_owned()) - .unwrap_or_else(|| "Agent".to_owned()); + .unwrap_or_else(|| i18n::t("terminal.ambient_agent.generic_agent_name")); Self { block_id, @@ -63,7 +63,8 @@ impl View for HarnessSessionHeader { Icon::ChevronRight }; - let label = format!("Running {}...", self.cli_name); + let label = i18n::t("terminal.ambient_agent.harness_session.running") + .replace("{name}", &self.cli_name); let row = Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) diff --git a/app/src/terminal/view/ambient_agent/block/setup_command.rs b/app/src/terminal/view/ambient_agent/block/setup_command.rs index 4aa910b362..b0317d7ffb 100644 --- a/app/src/terminal/view/ambient_agent/block/setup_command.rs +++ b/app/src/terminal/view/ambient_agent/block/setup_command.rs @@ -14,7 +14,6 @@ use crate::ai::blocklist::inline_action::inline_action_header::{ ExpandedConfig, HeaderConfig, InteractionMode, }; use crate::ai::blocklist::inline_action::inline_action_icons::green_check_icon; -use crate::ai::blocklist::inline_action::requested_command::VIEWING_COMMAND_DETAIL_MESSAGE; use crate::terminal::event::BlockCompletedEvent; use crate::terminal::model_events::{ModelEvent, ModelEventDispatcher}; use crate::terminal::view::ambient_agent::{ @@ -134,7 +133,7 @@ impl View for CloudModeSetupCommandBlock { let appearance = Appearance::as_ref(app); let mut config = HeaderConfig::new( if self.is_expanded { - VIEWING_COMMAND_DETAIL_MESSAGE.to_owned() + i18n::t("ai.requested_command.viewing_command_detail") } else { self.command.clone() }, diff --git a/app/src/terminal/view/ambient_agent/block/setup_command_text.rs b/app/src/terminal/view/ambient_agent/block/setup_command_text.rs index 26162069d5..fe421f0639 100644 --- a/app/src/terminal/view/ambient_agent/block/setup_command_text.rs +++ b/app/src/terminal/view/ambient_agent/block/setup_command_text.rs @@ -184,9 +184,9 @@ impl View for CloudModeSetupTextBlock { .setup_command_state() .is_running(self.group_id) { - "Running setup commands..." + i18n::t("terminal.ambient_agent.setup_command.running") } else { - "Ran setup commands" + i18n::t("terminal.ambient_agent.setup_command.ran") }, appearance.ai_font_family(), appearance.monospace_font_size(), diff --git a/app/src/terminal/view/ambient_agent/delete_auth_secret_confirmation_dialog.rs b/app/src/terminal/view/ambient_agent/delete_auth_secret_confirmation_dialog.rs index 8a6fa57d0c..fe40f6cd7a 100644 --- a/app/src/terminal/view/ambient_agent/delete_auth_secret_confirmation_dialog.rs +++ b/app/src/terminal/view/ambient_agent/delete_auth_secret_confirmation_dialog.rs @@ -41,13 +41,13 @@ pub(super) struct DeleteAuthSecretConfirmationDialog { impl DeleteAuthSecretConfirmationDialog { pub(super) fn new(ctx: &mut ViewContext) -> Self { let cancel_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Cancel", NakedTheme).on_click(|ctx| { + ActionButton::new(i18n::t("common.cancel"), NakedTheme).on_click(|ctx| { ctx.dispatch_typed_action(DeleteAuthSecretConfirmationDialogAction::Cancel); }) }); let delete_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Delete", DangerPrimaryTheme).on_click(|ctx| { + ActionButton::new(i18n::t("common.delete"), DangerPrimaryTheme).on_click(|ctx| { ctx.dispatch_typed_action(DeleteAuthSecretConfirmationDialogAction::Confirm); }) }); @@ -93,13 +93,11 @@ impl View for DeleteAuthSecretConfirmationDialog { }; let appearance = Appearance::as_ref(app); - let description = format!( - "Are you sure you want to delete {}? This action cannot be undone. Any agents or environments referencing this secret will no longer have access to it.", - pending_deletion.name - ); + let description = i18n::t("terminal.ambient_agent.auth_secret.delete_confirmation") + .replace("{name}", &pending_deletion.name); let dialog = Dialog::new( - "Delete secret".to_string(), + i18n::t("terminal.auth_secret.delete_secret"), Some(description), dialog_styles(appearance), ) diff --git a/app/src/terminal/view/ambient_agent/first_time_setup.rs b/app/src/terminal/view/ambient_agent/first_time_setup.rs index 8858060f88..c8eeda5909 100644 --- a/app/src/terminal/view/ambient_agent/first_time_setup.rs +++ b/app/src/terminal/view/ambient_agent/first_time_setup.rs @@ -145,7 +145,7 @@ impl FirstTimeCloudAgentSetupView { // Title - 20px medium weight column.add_child( Text::new( - "Start a new Oz cloud agent", + i18n::t("terminal.cloud_agent_setup.title"), appearance.ui_font_family(), 20., ) @@ -156,11 +156,9 @@ impl FirstTimeCloudAgentSetupView { // Description with "Visit docs" link let description_fragments = vec![ - FormattedTextFragment::plain_text( - "Use Oz cloud agents to run parallel agents, build agents that run autonomously, and check in on your agents from anywhere. ", - ), + FormattedTextFragment::plain_text(i18n::t("terminal.cloud_agent_setup.desc_prefix")), FormattedTextFragment::hyperlink( - "Visit docs", + i18n::t("terminal.cloud_agent_setup.visit_docs"), "https://docs.warp.dev/agent-platform/cloud-agents/overview", ), ]; @@ -189,7 +187,7 @@ impl FirstTimeCloudAgentSetupView { // Bold/semibold text in foreground color (per Figma: font-semibold text-[#e3e2df]) Text::new( - "Cloud agents require an environment that they'll run in to get their task done. Create your first environment below. You'll be able to edit the environment later, or add new environments when you need them.", + i18n::t("terminal.cloud_agent_setup.subheading"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -209,10 +207,14 @@ impl FirstTimeCloudAgentSetupView { // Badge with blue border let badge = Container::new( - Text::new("Free credits", appearance.ui_font_family(), 12.) - .with_style(Properties::default().weight(Weight::Semibold)) - .with_color(theme.accent().into()) - .finish(), + Text::new( + i18n::t("terminal.cloud_agent_setup.free_credits"), + appearance.ui_font_family(), + 12., + ) + .with_style(Properties::default().weight(Weight::Semibold)) + .with_color(theme.accent().into()) + .finish(), ) .with_horizontal_padding(6.) .with_vertical_padding(4.) @@ -222,12 +224,10 @@ impl FirstTimeCloudAgentSetupView { // Banner text - dynamic based on credits let credits_text = if credits == 1 { - "You have 1 free credit to use on Oz cloud agents.".to_string() + i18n::t("terminal.cloud_agent_setup.free_credit_one") } else { - format!( - "You have {} free credits to use on Oz cloud agents.", - credits - ) + i18n::t("terminal.cloud_agent_setup.free_credits_other") + .replace("{credits}", &credits.to_string()) }; let text = Text::new(credits_text, appearance.ui_font_family(), 12.) .with_color(blended_colors::text_sub(theme, theme.surface_1())) diff --git a/app/src/terminal/view/ambient_agent/footer.rs b/app/src/terminal/view/ambient_agent/footer.rs index f7f85a648e..ca34bffa44 100644 --- a/app/src/terminal/view/ambient_agent/footer.rs +++ b/app/src/terminal/view/ambient_agent/footer.rs @@ -81,8 +81,8 @@ pub fn render_loading_footer(appearance: &Appearance) -> Box { let border_color = blended_colors::neutral_4(theme); build_centered_footer( - "Cloud agent starting up…".to_string(), - "You'll be able to interact with Oz soon".to_string(), + i18n::t("terminal.ambient_agent.footer.loading_header"), + i18n::t("terminal.ambient_agent.footer.loading_body"), header_color, body_color, background, @@ -104,7 +104,7 @@ pub fn render_error_footer(error_message: &str, appearance: &Appearance) -> Box< let border_color = theme.ui_error_color(); build_centered_footer( - "Agent failed".to_string(), + i18n::t("terminal.ambient_agent.footer.error_header"), error_message.to_string(), header_color, body_color, diff --git a/app/src/terminal/view/ambient_agent/harness_selector.rs b/app/src/terminal/view/ambient_agent/harness_selector.rs index 9e1fbc3612..1c59ecfba5 100644 --- a/app/src/terminal/view/ambient_agent/harness_selector.rs +++ b/app/src/terminal/view/ambient_agent/harness_selector.rs @@ -53,12 +53,6 @@ const MENU_WIDTH: f32 = 208.; /// than the default `ui_font_size()` to give the logos more visual presence. const ITEM_ICON_SIZE: f32 = 16.; -/// Tooltip string for the closed-state button. -const BUTTON_TOOLTIP: &str = "Agent harness"; - -/// Label rendered at the top of the dropdown. -const MENU_HEADER_LABEL: &str = "Agent harness"; - /// Actions dispatched by the [`HarnessSelector`]. #[derive(Clone, Debug, PartialEq)] pub enum HarnessSelectorAction { @@ -95,7 +89,7 @@ impl HarnessSelector { .with_size(ButtonSize::AgentInputButton) .with_menu(true) .with_disabled_theme(AgentInputButtonTheme) - .with_tooltip(BUTTON_TOOLTIP) + .with_tooltip(i18n::t("terminal.ambient_agent.harness_selector.label")) .on_click(|ctx| { ctx.dispatch_typed_action(HarnessSelectorAction::ToggleMenu); }) @@ -225,9 +219,9 @@ impl HarnessSelector { button.set_disabled(is_locked_to_oz, ctx); button.set_tooltip( Some(if is_locked_to_oz { - "This conversation is with the Warp Agent, so the cloud handoff will also use Warp" + i18n::t("terminal.ambient_agent.harness_selector.locked_to_warp") } else { - BUTTON_TOOLTIP + i18n::t("terminal.ambient_agent.harness_selector.label") }), ctx, ); @@ -283,7 +277,7 @@ fn build_menu_items( disabled_text_color: pathfinder_color::ColorU, ) -> Vec> { let header = MenuItem::Header { - fields: MenuItemFields::new(MENU_HEADER_LABEL) + fields: MenuItemFields::new(i18n::t("terminal.ambient_agent.harness_selector.label")) .with_font_size_override(HEADER_FONT_SIZE) .with_override_text_color(header_text_color) .with_padding_override(HEADER_VERTICAL_PADDING, MENU_HORIZONTAL_PADDING) @@ -312,7 +306,7 @@ fn build_menu_items( fields = fields .with_disabled(true) .with_override_text_color(disabled_text_color) - .with_tooltip("Disabled by your administrator"); + .with_tooltip(i18n::t("terminal.harness_selector.disabled_by_admin")); } items.push(MenuItem::Item(fields)); } diff --git a/app/src/terminal/view/ambient_agent/host_selector.rs b/app/src/terminal/view/ambient_agent/host_selector.rs index 93f40ebe28..f6b636ba5a 100644 --- a/app/src/terminal/view/ambient_agent/host_selector.rs +++ b/app/src/terminal/view/ambient_agent/host_selector.rs @@ -37,10 +37,6 @@ const HEADER_VERTICAL_PADDING: f32 = 6.; const MENU_WIDTH: f32 = 208.; -const BUTTON_TOOLTIP: &str = "Execution host"; - -const MENU_HEADER_LABEL: &str = "Execution host"; - #[derive(Clone, Debug, PartialEq, Eq)] pub enum Host { Warp, @@ -101,7 +97,7 @@ impl HostSelector { ActionButton::new(initial_label, NakedHeaderButtonTheme) .with_size(ButtonSize::AgentInputButton) .with_menu(true) - .with_tooltip(BUTTON_TOOLTIP) + .with_tooltip(i18n::t("terminal.ambient_agent.host_selector.label")) .with_tooltip_alignment(TooltipAlignment::Left) .on_click(|ctx| { ctx.dispatch_typed_action(HostSelectorAction::ToggleMenu); @@ -277,7 +273,7 @@ fn build_menu_items( ctx: &mut ViewContext, ) -> Vec> { let header = MenuItem::Header { - fields: MenuItemFields::new(MENU_HEADER_LABEL) + fields: MenuItemFields::new(i18n::t("terminal.ambient_agent.host_selector.label")) .with_font_size_override(HEADER_FONT_SIZE) .with_override_text_color(header_text_color) .with_padding_override(HEADER_VERTICAL_PADDING, MENU_HORIZONTAL_PADDING) diff --git a/app/src/terminal/view/ambient_agent/loading_screen.rs b/app/src/terminal/view/ambient_agent/loading_screen.rs index e6f4d782cf..39e80a5c06 100644 --- a/app/src/terminal/view/ambient_agent/loading_screen.rs +++ b/app/src/terminal/view/ambient_agent/loading_screen.rs @@ -49,7 +49,10 @@ pub fn render_cloud_mode_loading_screen( // Add link at the end if it exists if let Some(link_target) = tip.link() { fragments.push(FormattedTextFragment::plain_text(" ")); - fragments.push(FormattedTextFragment::hyperlink("Learn more", link_target)); + fragments.push(FormattedTextFragment::hyperlink( + i18n::t("common.learn_more"), + link_target, + )); } let formatted_text = FormattedText::new(vec![FormattedTextLine::Line(fragments)]); @@ -153,20 +156,22 @@ fn render_tier_limits_footer( return None; } - let mut fragments = vec![FormattedTextFragment::plain_text(format!( - "Your agent is currently running on a {} machine. ", - specs - ))]; + let mut fragments = vec![FormattedTextFragment::plain_text( + i18n::t("terminal.cloud_agent_loading.machine_prefix").replace("{specs}", &specs), + )]; // Get the upgrade URL for the current team let upgrade_url = UserWorkspaces::as_ref(app) .current_team() .map(|team| UserWorkspaces::upgrade_link_for_team(team.uid))?; - fragments.push(FormattedTextFragment::hyperlink("Upgrade", upgrade_url)); - fragments.push(FormattedTextFragment::plain_text( - " for more powerful cloud agents.", + fragments.push(FormattedTextFragment::hyperlink( + i18n::t("common.upgrade"), + upgrade_url, )); + fragments.push(FormattedTextFragment::plain_text(i18n::t( + "terminal.cloud_agent_loading.machine_suffix", + ))); let formatted_text = FormattedText::new(vec![FormattedTextLine::Line(fragments)]); @@ -233,7 +238,7 @@ pub fn render_cloud_mode_error_screen( // Error title text let title_text = Text::new( - "Failed to start environment", + i18n::t("terminal.cloud_agent_loading.failed_to_start_environment"), appearance.ui_font_family(), appearance.monospace_font_size() + 2., ) @@ -323,7 +328,7 @@ pub fn render_cloud_mode_github_auth_required_screen( // Title text - "GitHub Authentication Required" let title_text = Text::new( - "GitHub Authentication Required", + i18n::t("terminal.cloud_agent_loading.github_auth_required"), appearance.ui_font_family(), appearance.monospace_font_size() + 2., ) @@ -333,7 +338,7 @@ pub fn render_cloud_mode_github_auth_required_screen( // Message text - "Please authenticate with GitHub to continue" let message_text = Text::new( - "Please authenticate with GitHub to continue", + i18n::t("terminal.cloud_agent_loading.github_auth_message"), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -345,7 +350,7 @@ pub fn render_cloud_mode_github_auth_required_screen( let auth_button = appearance .ui_builder() .button(ButtonVariant::Accent, auth_button_mouse_state.clone()) - .with_centered_text_label("Authenticate with GitHub".to_string()) + .with_centered_text_label(i18n::t("terminal.cloud_agent_loading.github_auth_button")) .build() .on_click(move |_, app, _| { app.open_url(&auth_url_clone); @@ -410,7 +415,7 @@ pub fn render_cloud_mode_cancelled_screen(appearance: &Appearance) -> Box Box &'static str { + pub fn setup_status_text(&self) -> String { if self.harness_started_at.is_some() { - "Starting Environment (Step 3/3)" + i18n::t("terminal.ambient_agent.setup_status.starting_environment") } else if self.claimed_at.is_some() { - "Creating Environment (Step 2/3)" + i18n::t("terminal.ambient_agent.setup_status.creating_environment") } else { - "Connecting to Host (Step 1/3)" + i18n::t("terminal.ambient_agent.setup_status.connecting_to_host") } } } @@ -1316,9 +1316,10 @@ impl AmbientAgentViewModel { | AmbientAgentTaskState::Error | AmbientAgentTaskState::Blocked | AmbientAgentTaskState::Unknown => { - let error = status_message - .map(|msg| msg.message) - .unwrap_or_else(|| "Cloud agent failed".to_string()); + let error = + status_message.map(|msg| msg.message).unwrap_or_else(|| { + i18n::t("terminal.ambient_agent.error.cloud_agent_failed") + }); self.handle_spawn_error(error, ctx); } } diff --git a/app/src/terminal/view/ambient_agent/model_selector.rs b/app/src/terminal/view/ambient_agent/model_selector.rs index 5c75915f7e..319593f15a 100644 --- a/app/src/terminal/view/ambient_agent/model_selector.rs +++ b/app/src/terminal/view/ambient_agent/model_selector.rs @@ -55,12 +55,6 @@ const SEARCH_VERTICAL_PADDING: f32 = 4.; // of total breathing room above the divider line. const SEARCH_FOOTER_TOP_MARGIN: f32 = 4.; -const SEARCH_PLACEHOLDER_TEXT: &str = "Search models"; - -const BUTTON_TOOLTIP: &str = "Choose agent model"; - -const NO_RESULTS_LABEL: &str = "No results"; - #[derive(Clone, Debug, PartialEq, Eq)] pub enum ModelSelectorAction { ToggleMenu, @@ -117,7 +111,7 @@ impl ModelSelector { let button = ctx.add_typed_action_view(|_ctx| { ActionButton::new("", AgentInputButtonTheme) .with_size(ButtonSize::AgentInputButton) - .with_tooltip(BUTTON_TOOLTIP) + .with_tooltip(i18n::t("terminal.ambient_agent.model_selector.tooltip")) .on_click(|ctx| { ctx.dispatch_typed_action(ModelSelectorAction::ToggleMenu); }) @@ -137,7 +131,10 @@ impl ModelSelector { }, ctx, ); - editor.set_placeholder_text(SEARCH_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text( + i18n::t("terminal.ambient_agent.model_selector.search_placeholder"), + ctx, + ); editor }); ctx.subscribe_to_view(&search_editor, |me, _, event, ctx| { @@ -398,7 +395,7 @@ impl ModelSelector { .map(|info| info.display_name.clone()) }) }) - .unwrap_or_else(|| "default".to_string()), + .unwrap_or_else(|| i18n::t("terminal.ambient_agent.model_selector.default")), _ => LLMPreferences::as_ref(ctx) .get_active_base_model(ctx, Some(self.terminal_view_id)) .display_name @@ -432,7 +429,7 @@ impl ModelSelector { if items.is_empty() { let no_results_text_color = internal_colors::text_sub(theme, theme.surface_2()); items.push(MenuItem::Item( - MenuItemFields::new(NO_RESULTS_LABEL) + MenuItemFields::new(i18n::t("terminal.ambient_agent.model_selector.no_results")) .with_font_size_override(ITEM_FONT_SIZE) .with_padding_override(ITEM_VERTICAL_PADDING, MENU_HORIZONTAL_PADDING) .with_override_text_color(no_results_text_color) @@ -538,9 +535,13 @@ impl ModelSelector { reasoning_level: None, }; let mut items: Vec> = Vec::new(); - if query.is_empty() || "default".contains(query) { + let default_label = i18n::t("terminal.ambient_agent.model_selector.default"); + if query.is_empty() + || default_label.to_lowercase().contains(query) + || "default".contains(query) + { items.push(MenuItem::Item( - MenuItemFields::new("default") + MenuItemFields::new(default_label) .with_icon(icon) .with_icon_size_override(ITEM_ICON_SIZE) .with_font_size_override(ITEM_FONT_SIZE) diff --git a/app/src/terminal/view/ambient_agent/tips.rs b/app/src/terminal/view/ambient_agent/tips.rs index 9260c5f467..482470f48f 100644 --- a/app/src/terminal/view/ambient_agent/tips.rs +++ b/app/src/terminal/view/ambient_agent/tips.rs @@ -41,163 +41,163 @@ impl AITip for CloudModeTip { pub fn get_cloud_mode_tips() -> Vec { vec![ CloudModeTip::new( - "Install the Oz Slack integration to trigger agents from any channel or DM.", + i18n::t("terminal.ambient_agent.tips.slack_integration_trigger"), Some("https://docs.warp.dev/agent-platform/cloud-agents/integrations/slack"), ), CloudModeTip::new( - "Build programmatic agents using Oz's TypeScript and Python SDKs.", + i18n::t("terminal.ambient_agent.tips.programmatic_agents_sdk"), Some("https://docs.warp.dev/reference/api-and-sdk"), ), CloudModeTip::new( - "Set team or personal secrets for agents using the `oz secret` command.", + i18n::t("terminal.ambient_agent.tips.set_secrets"), Some("https://docs.warp.dev/agent-platform/cloud-agents/secrets"), ), CloudModeTip::new( - "View all your agent runs and their status in the Oz web app.", + i18n::t("terminal.ambient_agent.tips.view_runs_status"), Some("https://oz.warp.dev"), ), CloudModeTip::new( - "Join any Oz cloud agent run in real-time using Agent Session Sharing.", + i18n::t("terminal.ambient_agent.tips.join_run_realtime"), Some("https://docs.warp.dev/agent-platform/cloud-agents/viewing-cloud-agent-runs"), ), CloudModeTip::new( - "Set up recurring agents that run on cron schedules for automated maintenance.", + i18n::t("terminal.ambient_agent.tips.recurring_cron"), Some("https://docs.warp.dev/agent-platform/cloud-agents/triggers/scheduled-agents"), ), CloudModeTip::new( - "Create agents that automatically fix bugs when issues are filed in Linear.", + i18n::t("terminal.ambient_agent.tips.linear_fix_bugs"), Some("https://docs.warp.dev/agent-platform/cloud-agents/integrations/linear"), ), CloudModeTip::new( - "Build agents that respond to CI failures and attempt automatic fixes.", + i18n::t("terminal.ambient_agent.tips.ci_failures_fix"), Some("https://docs.warp.dev/agent-platform/cloud-agents/integrations/github-actions"), ), CloudModeTip::new( - "Run agents from GitHub Actions using the `oz-agent-action`.", + i18n::t("terminal.ambient_agent.tips.github_actions_agent_action"), Some("https://github.com/warpdotdev/oz-agent-action"), ), CloudModeTip::new( - "Call the Oz REST API to trigger agents from any backend service or internal tool.", + i18n::t("terminal.ambient_agent.tips.rest_api_trigger"), Some("https://docs.warp.dev/reference/api-and-sdk"), ), CloudModeTip::new( - "Create reusable environments with Docker images for consistent agent execution.", + i18n::t("terminal.ambient_agent.tips.reusable_environments"), Some("https://docs.warp.dev/agent-platform/cloud-agents/environments"), ), CloudModeTip::new( - "Share agent session links with your team for collaborative debugging.", + i18n::t("terminal.ambient_agent.tips.share_session_links"), Some("https://docs.warp.dev/agent-platform/cloud-agents/viewing-cloud-agent-runs"), ), CloudModeTip::new( - "Use the `--share` flag with the Oz CLI to enable session sharing from anywhere.", + i18n::t("terminal.ambient_agent.tips.share_flag"), Some("https://docs.warp.dev/agent-platform/cloud-agents/platform"), ), CloudModeTip::new( - "Fork a completed Oz cloud agent session into Warp to continue the work locally.", + i18n::t("terminal.ambient_agent.tips.fork_completed_session"), Some("https://docs.warp.dev/agent-platform/cloud-agents/viewing-cloud-agent-runs"), ), CloudModeTip::new( - "Build internal tools that use agents to answer questions from your databases.", + i18n::t("terminal.ambient_agent.tips.internal_tools_databases"), Some("https://docs.warp.dev/agent-platform/cloud-agents/integrations"), ), CloudModeTip::new( - "Create a scheduled agent to clean up stale feature flags every week.", + i18n::t("terminal.ambient_agent.tips.scheduled_feature_flags"), Some("https://docs.warp.dev/agent-platform/cloud-agents/triggers/scheduled-agents"), ), CloudModeTip::new( - "Tag @Oz in Linear issues to automatically investigate and propose fixes.", + i18n::t("terminal.ambient_agent.tips.linear_tag_oz"), Some("https://docs.warp.dev/agent-platform/cloud-agents/integrations/linear"), ), CloudModeTip::new( - "Run agents on remote dev boxes or CI runners using the Oz CLI.", + i18n::t("terminal.ambient_agent.tips.remote_dev_boxes"), Some("https://docs.warp.dev/agent-platform/cloud-agents/platform"), ), CloudModeTip::new( - "Configure MCP servers to give Oz cloud agents access to GitHub, Linear, and Sentry.", + i18n::t("terminal.ambient_agent.tips.mcp_servers_access"), Some("https://docs.warp.dev/agent-platform/capabilities/mcp"), ), CloudModeTip::new( - "Use `oz agent run` to kick off tasks without opening the Warp terminal.", + i18n::t("terminal.ambient_agent.tips.oz_agent_run"), Some("https://docs.warp.dev/agent-platform/cloud-agents/platform"), ), CloudModeTip::new( - "View your teammates' agent runs in the Oz web app for shared visibility.", + i18n::t("terminal.ambient_agent.tips.teammates_runs"), Some("https://oz.warp.dev"), ), CloudModeTip::new( - "Build agents that automatically triage and label incoming GitHub issues.", + i18n::t("terminal.ambient_agent.tips.triage_github_issues"), Some("https://docs.warp.dev/agent-platform/cloud-agents/integrations/github-actions"), ), CloudModeTip::new( - "Set up an agent to generate daily summaries of newly opened issues.", + i18n::t("terminal.ambient_agent.tips.daily_issue_summaries"), Some("https://docs.warp.dev/agent-platform/cloud-agents/integrations/github-actions"), ), CloudModeTip::new( - "Create an agent that automatically reviews PRs and suggests improvements.", + i18n::t("terminal.ambient_agent.tips.review_prs"), Some("https://docs.warp.dev/agent-platform/cloud-agents/integrations/github-actions"), ), CloudModeTip::new( - "Use `oz environment create` to define reproducible execution contexts.", + i18n::t("terminal.ambient_agent.tips.oz_environment_create"), Some("https://docs.warp.dev/agent-platform/cloud-agents/environments"), ), CloudModeTip::new( - "Trigger agents from webhooks to respond to production incidents.", + i18n::t("terminal.ambient_agent.tips.webhooks_incidents"), Some("https://docs.warp.dev/reference/api-and-sdk"), ), CloudModeTip::new( - "Build an agent that restarts services or scales deployments when alerts fire.", + i18n::t("terminal.ambient_agent.tips.restart_services_alerts"), Some("https://docs.warp.dev/agent-platform/cloud-agents/triggers"), ), CloudModeTip::new( - "Use personal secrets for credentials that should only be used by your agents.", + i18n::t("terminal.ambient_agent.tips.personal_secrets"), Some("https://docs.warp.dev/agent-platform/cloud-agents/secrets"), ), CloudModeTip::new( - "Use team secrets for shared infrastructure credentials across all agents.", + i18n::t("terminal.ambient_agent.tips.team_secrets"), Some("https://docs.warp.dev/agent-platform/cloud-agents/secrets"), ), CloudModeTip::new( - "Create an agent that runs nightly to check for dependency updates.", + i18n::t("terminal.ambient_agent.tips.nightly_dependency_updates"), Some("https://docs.warp.dev/agent-platform/cloud-agents/triggers/scheduled-agents"), ), CloudModeTip::new( - "Build an agent that automatically formats and lints code on a schedule.", + i18n::t("terminal.ambient_agent.tips.format_lint_schedule"), Some("https://docs.warp.dev/agent-platform/cloud-agents/triggers/scheduled-agents"), ), CloudModeTip::new( - "Use `oz schedule create` to set up cron-triggered agents.", + i18n::t("terminal.ambient_agent.tips.oz_schedule_create"), Some("https://docs.warp.dev/agent-platform/cloud-agents/triggers/scheduled-agents"), ), CloudModeTip::new( - "Pause and resume scheduled agents without deleting them using `oz schedule pause`.", + i18n::t("terminal.ambient_agent.tips.oz_schedule_pause"), Some("https://docs.warp.dev/agent-platform/cloud-agents/triggers/scheduled-agents"), ), CloudModeTip::new( - "Use `oz mcp list` to see which MCP servers are available to your agents.", + i18n::t("terminal.ambient_agent.tips.oz_mcp_list"), Some("https://docs.warp.dev/agent-platform/capabilities/mcp"), ), CloudModeTip::new( - "Build an internal Slack bot that delegates coding tasks to Oz agents.", + i18n::t("terminal.ambient_agent.tips.internal_slack_bot"), Some("https://docs.warp.dev/agent-platform/cloud-agents/integrations/slack"), ), CloudModeTip::new( - "Create an agent that responds to @mentions in Slack threads with full context.", + i18n::t("terminal.ambient_agent.tips.slack_mentions"), Some("https://docs.warp.dev/agent-platform/cloud-agents/integrations/slack"), ), CloudModeTip::new( - "Use the Oz TypeScript SDK to build custom automation pipelines.", + i18n::t("terminal.ambient_agent.tips.typescript_sdk"), Some("https://docs.warp.dev/reference/api-and-sdk"), ), CloudModeTip::new( - "Use the Oz Python SDK to integrate agents into your data pipelines.", + i18n::t("terminal.ambient_agent.tips.python_sdk"), Some("https://docs.warp.dev/reference/api-and-sdk"), ), CloudModeTip::new( - "Monitor agent success rates and runtimes using the Oz API.", + i18n::t("terminal.ambient_agent.tips.monitor_success_rates"), Some("https://docs.warp.dev/reference/api-and-sdk"), ), CloudModeTip::new( - "Build a dashboard that tracks all agent activity across your team.", + i18n::t("terminal.ambient_agent.tips.dashboard_agent_activity"), Some("https://docs.warp.dev/reference/api-and-sdk"), ), ] diff --git a/app/src/terminal/view/ambient_agent/view_impl.rs b/app/src/terminal/view/ambient_agent/view_impl.rs index 5a96f5d3e7..bfc48dd777 100644 --- a/app/src/terminal/view/ambient_agent/view_impl.rs +++ b/app/src/terminal/view/ambient_agent/view_impl.rs @@ -38,9 +38,6 @@ use crate::terminal::CLIAgent; use crate::workspace::view::cloud_agent_capacity_modal::CloudAgentCapacityModalVariant; use crate::workspaces::user_workspaces::UserWorkspaces; -const CHILD_AGENT_GITHUB_AUTH_REQUIRED_BLOCKED_ACTION: &str = - "GitHub authentication required before starting the child agent."; - impl TerminalView { fn active_ambient_agent_conversation_id(&self, ctx: &AppContext) -> Option { self.agent_view_controller @@ -290,8 +287,9 @@ impl TerminalView { if self.active_ambient_agent_conversation_is_child(ctx) { self.update_active_ambient_agent_conversation_status( ConversationStatus::Blocked { - blocked_action: CHILD_AGENT_GITHUB_AUTH_REQUIRED_BLOCKED_ACTION - .to_string(), + blocked_action: i18n::t( + "terminal.ambient_agent.status.child_github_auth_required", + ), }, None, ctx, @@ -899,7 +897,7 @@ impl TerminalView { let message = progress.setup_status_text(); render_cloud_mode_loading_screen( - message, + &message, appearance, &ui_state.loading_shimmer_handle, &ui_state.tip_model, diff --git a/app/src/terminal/view/block_banner/warpify.rs b/app/src/terminal/view/block_banner/warpify.rs index f5563468ac..4152278838 100644 --- a/app/src/terminal/view/block_banner/warpify.rs +++ b/app/src/terminal/view/block_banner/warpify.rs @@ -86,10 +86,14 @@ impl WarpifyBannerState { self.mode.is_ssh() } - pub fn title(&self) -> &str { + pub fn title(&self) -> String { match &self.mode { - WarpificationMode::Ssh { .. } => "Warpify SSH session", - WarpificationMode::Subshell { .. } => "Warpify subshell", + WarpificationMode::Ssh { .. } => { + i18n::t("terminal.use_agent_footer.warpify_ssh_session") + } + WarpificationMode::Subshell { .. } => { + i18n::t("terminal.use_agent_footer.warpify_subshell") + } } } @@ -151,7 +155,7 @@ pub fn render_warpification_banner( ButtonVariant::Text, state.dont_ask_button_mouse_state.clone(), ) - .with_text_label("Do not show again".to_owned()) + .with_text_label(i18n::t("common.do_not_show_again")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(TerminalAction::DismissWarpifyBanner( @@ -226,7 +230,7 @@ fn render_yes_button( let yes_button = match initialize_warpification_keybinding { Some(keystroke) => appearance .ui_builder() - .keyboard_shortcut_button(state.title().to_owned(), keystroke, mouse_state.clone()) + .keyboard_shortcut_button(state.title(), keystroke, mouse_state.clone()) .with_style(UiComponentStyles { height: Some(36.), padding: Some(Coords { @@ -240,7 +244,7 @@ fn render_yes_button( None => appearance .ui_builder() .button(ButtonVariant::Basic, mouse_state.clone()) - .with_text_label(state.title().to_owned()) + .with_text_label(state.title()) .with_style(UiComponentStyles { background: Some(Fill::Solid(ColorU::transparent_black()).into()), height: Some(36.), diff --git a/app/src/terminal/view/block_onboarding/onboarding_agentic_suggestions_block.rs b/app/src/terminal/view/block_onboarding/onboarding_agentic_suggestions_block.rs index 38d89f63ad..4e17eede3a 100644 --- a/app/src/terminal/view/block_onboarding/onboarding_agentic_suggestions_block.rs +++ b/app/src/terminal/view/block_onboarding/onboarding_agentic_suggestions_block.rs @@ -142,17 +142,22 @@ impl OnboardingAgenticSuggestionsBlock { git_repo_trimmed.clone(), ); - let matrix_save_directory = themes_dir() - .into_os_string() - .into_string() - .unwrap_or("the Warp themes directory.".to_string()); + let matrix_save_directory = + themes_dir() + .into_os_string() + .into_string() + .unwrap_or_else(|_| { + i18n::t("terminal.block_onboarding.agentic.prompt.matrix.default_directory") + }); let agent_suggestions = vec![ ( AgenticSuggestionsContent { - title: "Create a snake game in Python from scratch".to_string(), - description: "Have Agent Mode walk you through creating a snake game from end-to-end".to_string(), - prompt: "Make a snake game for playing in the terminal using python. Use the code tool and requested commands to do it for me. Before deciding on a solution, make sure I have all the prerequisites installed. At the end of our conversation, the app should run without any additional steps.".to_string(), + title: i18n::t("terminal.block_onboarding.agentic.suggestion.snake.title"), + description: i18n::t( + "terminal.block_onboarding.agentic.suggestion.snake.description", + ), + prompt: i18n::t("terminal.block_onboarding.agentic.prompt.snake"), chip_type: OnboardingChipType::PythonSnakeGame, icon: UIIcon::Icon::GamingPad, }, @@ -160,9 +165,15 @@ impl OnboardingAgenticSuggestionsBlock { ), ( AgenticSuggestionsContent { - title: format!("Explore git history in {git_repo_trimmed}"), - description: "Work with Agent Mode to understand recent changes to a git repository".to_string(), - prompt: format!("Explore my git history in {git_repo_path} and provide me a summary."), + title: i18n::t( + "terminal.block_onboarding.agentic.suggestion.git_history.title", + ) + .replace("{repo}", &git_repo_trimmed), + description: i18n::t( + "terminal.block_onboarding.agentic.suggestion.git_history.description", + ), + prompt: i18n::t("terminal.block_onboarding.agentic.prompt.git_history") + .replace("{repo_path}", &git_repo_path), chip_type: OnboardingChipType::ExploreGitHistory, icon: UIIcon::Icon::BookOpen, }, @@ -170,9 +181,12 @@ impl OnboardingAgenticSuggestionsBlock { ), ( AgenticSuggestionsContent { - title: "Create a Matrix-styled custom theme".to_string(), - description: "Make your terminal look like you entered the Matrix".to_string(), - prompt: format!("First check if {matrix_save_directory} exists, and create this path if it doesn't already exist. Then create a matrix theme for my Warp terminal without a background image field, following exact YAML structure on the warp website without any extra or missing fields. Call it matrix.yaml and save it in the directory we previously created. Once you've verified that the theme is correct and ready to be applied, let me know by only saying 'The matrix theme is now available at .'."), + title: i18n::t("terminal.block_onboarding.agentic.suggestion.matrix.title"), + description: i18n::t( + "terminal.block_onboarding.agentic.suggestion.matrix.description", + ), + prompt: i18n::t("terminal.block_onboarding.agentic.prompt.matrix") + .replace("{directory}", &matrix_save_directory), chip_type: OnboardingChipType::MatrixThemePicker, icon: UIIcon::Icon::PaintBrush, }, @@ -180,9 +194,11 @@ impl OnboardingAgenticSuggestionsBlock { ), ( AgenticSuggestionsContent { - title: "Something else?".to_string(), - description: "Pair with an Agent to accomplish another task".to_string(), - prompt: "What can you help with me on?".to_string(), + title: i18n::t("terminal.block_onboarding.agentic.suggestion.other.title"), + description: i18n::t( + "terminal.block_onboarding.agentic.suggestion.other.description", + ), + prompt: i18n::t("terminal.block_onboarding.agentic.prompt.other"), chip_type: OnboardingChipType::Other, icon: UIIcon::Icon::Stars, }, @@ -352,7 +368,9 @@ impl OnboardingAgenticSuggestionsBlock { fn get_git_repo_name(shell_type: ShellType, git_repo_path: Option) -> String { Self::split_path( - &git_repo_path.unwrap_or("my repository".to_string()), + &git_repo_path.unwrap_or_else(|| { + i18n::t("terminal.block_onboarding.agentic.suggestion.git_history.fallback_repo") + }), shell_type, ) .into_iter() @@ -581,25 +599,26 @@ impl OnboardingAgenticSuggestionsBlock { let font_size = appearance.monospace_font_size(); let font_color = current_theme.main_text_color(current_theme.background()); - const WELCOME_TEXT_LINE_ONE: &str = "Welcome to Warp!"; - const WELCOME_TEXT_LINE_TWO_PART_ONE: &str = - "Here are a few examples of how to leverage the power of AI in your terminal using"; - const WELCOME_TEXT_LINE_TWO_PART_TWO: &str = " Agent Mode"; - Flex::column() .with_children(vec![ Container::new( - Text::new(WELCOME_TEXT_LINE_ONE, font_family, font_size) - .with_color(font_color.into_solid()) - .finish(), + Text::new( + i18n::t("terminal.block_onboarding.agentic.welcome_title"), + font_family, + font_size, + ) + .with_color(font_color.into_solid()) + .finish(), ) .with_margin_bottom(10.) .finish(), FormattedTextElement::new( FormattedText::new([FormattedTextLine::Line(vec![ - FormattedTextFragment::plain_text(WELCOME_TEXT_LINE_TWO_PART_ONE), + FormattedTextFragment::plain_text(i18n::t( + "terminal.block_onboarding.agentic.welcome_body_prefix", + )), FormattedTextFragment::weighted( - WELCOME_TEXT_LINE_TWO_PART_TWO, + i18n::t("terminal.block_onboarding.agentic.welcome_body_agent_mode"), Some(CustomWeight::Bold), ), ])]), @@ -642,7 +661,7 @@ impl OnboardingAgenticSuggestionsBlock { ) .with_child( Text::new( - "Thinking...".to_owned(), + i18n::t("terminal.onboarding.thinking"), appearance.ui_font_family(), appearance.monospace_font_size(), ) diff --git a/app/src/terminal/view/block_onboarding/onboarding_drive_sharing_block.rs b/app/src/terminal/view/block_onboarding/onboarding_drive_sharing_block.rs index 738f74286d..eb9097c2ed 100644 --- a/app/src/terminal/view/block_onboarding/onboarding_drive_sharing_block.rs +++ b/app/src/terminal/view/block_onboarding/onboarding_drive_sharing_block.rs @@ -44,12 +44,6 @@ impl Entity for OnboardingDriveSharingBlock { type Event = (); } -const TITLE_TEXT: &str = "Sharing in Warp Drive"; -const BODY_TEXT: &[&str] = &[ - "You can now share drive objects, in Warp or on the web, with anyone - Warp user or not. Click Share in the Warp Drive menu or the pane header to share via link or email.", - "You’ll be able to modify the access permissions any time.", -]; - const BLOCK_PADDING: f32 = 16.; const BUTTON_WIDTH: f32 = 100.; const BUTTON_HEIGHT: f32 = 32.; @@ -66,21 +60,28 @@ impl View for OnboardingDriveSharingBlock { let font_size = appearance.monospace_font_size(); let header = Container::new( - Text::new(TITLE_TEXT, font_family, font_size) - .with_color(appearance.theme().accent().into_solid()) - .with_style(Properties::default().weight(Weight::Bold)) - .finish(), + Text::new( + i18n::t("terminal.block_onboarding.drive_sharing.title"), + font_family, + font_size, + ) + .with_color(appearance.theme().accent().into_solid()) + .with_style(Properties::default().weight(Weight::Bold)) + .finish(), ) .with_padding_bottom(BLOCK_PADDING) .finish(); let mut content = Flex::column().with_child(header); - for paragraph in BODY_TEXT.iter() { + for paragraph in [ + i18n::t("terminal.block_onboarding.drive_sharing.body"), + i18n::t("terminal.block_onboarding.drive_sharing.permissions"), + ] { content.add_child( appearance .ui_builder() - .paragraph(*paragraph) + .paragraph(paragraph) .with_style(UiComponentStyles { font_family_id: Some(font_family), font_size: Some(font_size), @@ -93,8 +94,10 @@ impl View for OnboardingDriveSharingBlock { } let button_label = match CloudModel::as_ref(app).get_by_uid(&self.object_id.uid()) { - Some(object) => format!("Share {}", object.display_name()), - None => format!("Share this {}", self.object_id.object_type()), + Some(object) => i18n::t("terminal.block_onboarding.drive_sharing.share_object") + .replace("{name}", &object.display_name()), + None => i18n::t("terminal.block_onboarding.drive_sharing.share_this_object") + .replace("{type}", &self.object_id.object_type().to_string()), }; let object_id = self.object_id; let button = appearance diff --git a/app/src/terminal/view/block_onboarding/onboarding_prompt_block.rs b/app/src/terminal/view/block_onboarding/onboarding_prompt_block.rs index 0cb69988f7..812e2baecc 100644 --- a/app/src/terminal/view/block_onboarding/onboarding_prompt_block.rs +++ b/app/src/terminal/view/block_onboarding/onboarding_prompt_block.rs @@ -61,27 +61,32 @@ impl OnboardingPromptBlock { let font_color = current_theme.main_text_color(current_theme.background()); // Copy - https://docs.google.com/document/d/1zttBLI5Mw07kUupvrMQoC5aTwTXSHIUOIFFnxZ8GQEU/edit - const LINE_ONE: &str = "Next, let’s set up your prompt. Warp has a custom prompt builder or you can select PS1 to honor your pre-existing prompt configuration."; - const LINE_TWO: &str = - "Warp works with many custom prompts like oh-my-zsh, Starship, Powerlevel10K. "; - const LINK_TEXT: &str = "Learn more"; const LINK_DESTINATION: &str = "https://docs.warp.dev/terminal/appearance/prompt#custom-prompt-compatibility-table"; Flex::column() .with_children([ Container::new( - Text::new(LINE_ONE, font_family, font_size) - .with_color(font_color.into_solid()) - .finish(), + Text::new( + i18n::t("terminal.block_onboarding.prompt.intro"), + font_family, + font_size, + ) + .with_color(font_color.into_solid()) + .finish(), ) .with_margin_top(14.) .finish(), Container::new( FormattedTextElement::new( FormattedText::new([FormattedTextLine::Line(vec![ - FormattedTextFragment::plain_text(LINE_TWO), - FormattedTextFragment::hyperlink(LINK_TEXT, LINK_DESTINATION), + FormattedTextFragment::plain_text(i18n::t( + "terminal.block_onboarding.prompt.compatibility_prefix", + )), + FormattedTextFragment::hyperlink( + i18n::t("common.learn_more"), + LINK_DESTINATION, + ), ])]), font_size, font_family, @@ -116,7 +121,7 @@ impl OnboardingPromptBlock { font_size: Some(14.), ..Default::default() }) - .with_centered_text_label("Confirm".to_owned()); + .with_centered_text_label(i18n::t("common.confirm")); if self.selected_prompt.is_none() { confirm_button = confirm_button.disabled(); } @@ -235,10 +240,6 @@ impl OnboardingPromptBlock { fn render_existing_prompt_button_interior(&self, appearance: &Appearance) -> Box { // Pixel values pulled from Figma mocks // https://www.figma.com/file/y888viqzWBoMpFTxQqkQEN/Activation?node-id=568:1595&mode=dev - const HEADER_TEXT: &str = "Shell prompt (PS1)"; - const NO_PS1_TEXT: &str = "No existing prompt."; - const CORRECTION_TEXT: &str = "Look incorrect? "; - const LINK_TEXT: &str = "Let us know."; const LINK_DESTINATION: &str = "https://github.com/warpdotdev/Warp/issues/new?assignees=&labels=Bug&projects=&template=01_bug_report.yml"; const HEADER_MARGIN_LEFT: f32 = 4.; @@ -263,9 +264,13 @@ impl OnboardingPromptBlock { .with_corner_radius(CornerRadius::with_all(Radius::Pixels(CORNER_RADIUS_PIXELS))) .finish() } else { - Text::new_inline(NO_PS1_TEXT, font_family, font_size) - .with_color(font_color.into_solid()) - .finish() + Text::new_inline( + i18n::t("terminal.block_onboarding.prompt.no_existing_prompt"), + font_family, + font_size, + ) + .with_color(font_color.into_solid()) + .finish() }; let link_style = UiComponentStyles { @@ -277,9 +282,13 @@ impl OnboardingPromptBlock { Flex::column() .with_child( Container::new( - Text::new_inline(HEADER_TEXT, font_family, font_size) - .with_color(font_color.into_solid()) - .finish(), + Text::new_inline( + i18n::t("terminal.block_onboarding.prompt.shell_prompt"), + font_family, + font_size, + ) + .with_color(font_color.into_solid()) + .finish(), ) .with_margin_left(HEADER_MARGIN_LEFT) .finish(), @@ -291,15 +300,19 @@ impl OnboardingPromptBlock { Align::new( Flex::row() .with_children([ - Text::new_inline(CORRECTION_TEXT, font_family, font_size) - .with_color( - font_color.with_opacity(CORRECTION_OPACITY).into_solid(), - ) - .finish(), + Text::new_inline( + i18n::t("terminal.block_onboarding.prompt.look_incorrect"), + font_family, + font_size, + ) + .with_color( + font_color.with_opacity(CORRECTION_OPACITY).into_solid(), + ) + .finish(), appearance .ui_builder() .link( - LINK_TEXT.to_string(), + i18n::t("terminal.block_onboarding.prompt.let_us_know"), Some(LINK_DESTINATION.to_string()), None, self.mouse_state_handle_look_incorrect.clone(), @@ -324,7 +337,6 @@ impl OnboardingPromptBlock { fn render_warp_prompt_button_interior(&self, appearance: &Appearance) -> Box { // Pixel values pulled from Figma mocks // https://www.figma.com/file/y888viqzWBoMpFTxQqkQEN/Activation?node-id=568:1595&mode=dev - const HEADER_TEXT: &str = "Warp prompt"; const HEADER_MARGIN_LEFT: f32 = 4.; const SECTION_MARGIN_TOP: f32 = 8.; const OUTER_CORNER_RADIUS: f32 = 4.; @@ -375,9 +387,13 @@ impl OnboardingPromptBlock { Flex::column() .with_child( Container::new( - Text::new_inline(HEADER_TEXT, font_family, appearance.ui_font_size()) - .with_color(font_color.into_solid()) - .finish(), + Text::new_inline( + i18n::t("terminal.block_onboarding.prompt.warp_prompt"), + font_family, + appearance.ui_font_size(), + ) + .with_color(font_color.into_solid()) + .finish(), ) .with_margin_left(HEADER_MARGIN_LEFT) .finish(), @@ -388,7 +404,7 @@ impl OnboardingPromptBlock { 1., Align::new( Text::new_inline( - "Customizable in appearance settings.", + i18n::t("terminal.block_onboarding.prompt.customizable"), font_family, ui_font_size, ) diff --git a/app/src/terminal/view/block_onboarding/util.rs b/app/src/terminal/view/block_onboarding/util.rs index 04349eb794..a666755d31 100644 --- a/app/src/terminal/view/block_onboarding/util.rs +++ b/app/src/terminal/view/block_onboarding/util.rs @@ -42,7 +42,7 @@ pub fn render_skip_button( background: Some(appearance.theme().outline().into()), ..Default::default() }) - .with_centered_text_label("Skip".to_owned()) + .with_centered_text_label(i18n::t("common.skip")) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| ctx.dispatch_typed_action(action.clone())) @@ -112,7 +112,7 @@ pub fn render_input_row( height: Some(SKIP_BUTTON_HEIGHT), ..Default::default() }) - .with_centered_text_label("Create team".to_owned()); + .with_centered_text_label(i18n::t("terminal.block_onboarding.create_team")); if name(ctx, team_name_editor).is_none() { create_team_button = create_team_button .with_style(UiComponentStyles { diff --git a/app/src/terminal/view/context_menu.rs b/app/src/terminal/view/context_menu.rs index 1af835bb9e..75108efe64 100644 --- a/app/src/terminal/view/context_menu.rs +++ b/app/src/terminal/view/context_menu.rs @@ -21,17 +21,17 @@ impl TerminalView { ctx: &mut ViewContext, ) -> Vec> { let mut items = vec![ - MenuItemFields::new("Copy") + MenuItemFields::new(i18n::t("common.copy")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::CopyAIBlock { ai_block_view_id }, )) .into_item(), - MenuItemFields::new("Copy prompt") + MenuItemFields::new(i18n::t("terminal.context_menu.copy_prompt")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::CopyAIBlockQuery { ai_block_view_id }, )) .into_item(), - MenuItemFields::new("Copy output as Markdown") + MenuItemFields::new(i18n::t("terminal.context_menu.copy_output_as_markdown")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::CopyAIBlockOutput { ai_block_view_id }, )) @@ -42,7 +42,7 @@ impl TerminalView { match link { RichContentLink::Url(url) => { items.push( - MenuItemFields::new("Copy URL") + MenuItemFields::new(i18n::t("terminal.context_menu.copy_url")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::CopyUrl { url_content: url }, )) @@ -52,7 +52,7 @@ impl TerminalView { #[cfg(feature = "local_fs")] RichContentLink::FilePath { absolute_path, .. } => { items.push( - MenuItemFields::new("Copy path") + MenuItemFields::new(i18n::t("common.copy_path")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::CopyUrl { url_content: absolute_path.to_string_lossy().into_owned(), @@ -78,7 +78,7 @@ impl TerminalView { if num_requested_commands > 0 { items.push( - MenuItemFields::new(String::from("Copy command")) + MenuItemFields::new(i18n::t("terminal.context_menu.copy_command")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::CopyAgentCommand { ai_block_view_id }, )) @@ -112,7 +112,7 @@ impl TerminalView { }); if has_git_branch { items.push( - MenuItemFields::new(String::from("Copy git branch")) + MenuItemFields::new(i18n::t("terminal.context_menu.copy_git_branch")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::CopyAgentGitBranch { ai_block_view_id }, )) @@ -121,7 +121,7 @@ impl TerminalView { } items.push(MenuItem::Separator); items.push( - MenuItemFields::new("Save as prompt") + MenuItemFields::new(i18n::t("terminal.context_menu.save_as_prompt")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::SavePromptAsAgentModeWorkflow { ai_block_view_id }, )) @@ -133,7 +133,7 @@ impl TerminalView { let history_model = BlocklistAIHistoryModel::as_ref(ctx); if history_model.can_conversation_be_shared(&ai_conversation_id) { items.push( - MenuItemFields::new("Copy share link") + MenuItemFields::new(i18n::t("terminal.context_menu.copy_share_link")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::CopyConversationShareLink { conversation_id: ai_conversation_id, @@ -142,7 +142,7 @@ impl TerminalView { .into_item(), ); items.push( - MenuItemFields::new("Share conversation") + MenuItemFields::new(i18n::t("terminal.context_menu.share_conversation")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::OpenConversationShareDialog { conversation_id: ai_conversation_id, @@ -154,7 +154,7 @@ impl TerminalView { } items.push( - MenuItemFields::new("Copy conversation text") + MenuItemFields::new(i18n::t("terminal.context_menu.copy_conversation_text")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::CopyAIBlockConversation { ai_block_view_id }, )) @@ -265,14 +265,14 @@ impl TerminalView { if ChannelState::channel().is_dogfood() { vec![ ( - "Copy debugging link".to_string(), + i18n::t("terminal.context_menu.copy_debugging_link"), ContextMenuAction::CopyAIDebuggingLink { conversation_token: conversation_token.clone(), request_id: server_output_id, }, ), ( - "Copy conversation ID".to_string(), + i18n::t("terminal.context_menu.copy_conversation_id"), ContextMenuAction::CopyConversationId { conversation_id: conversation_token, }, @@ -280,7 +280,7 @@ impl TerminalView { ] } else { vec![( - "Copy debugging ID".to_string(), + i18n::t("terminal.context_menu.copy_debugging_id"), ContextMenuAction::CopyExternalDebuggingId { request_id: server_output_id, conversation_id: conversation_token, @@ -326,7 +326,7 @@ impl TerminalView { .is_some() { items.push( - MenuItemFields::new("Copy share link") + MenuItemFields::new(i18n::t("terminal.context_menu.copy_share_link")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::CopyConversationShareLink { conversation_id }, )) @@ -335,7 +335,7 @@ impl TerminalView { } items.push( - MenuItemFields::new("Copy conversation text") + MenuItemFields::new(i18n::t("terminal.context_menu.copy_conversation_text")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::CopyConversationText { conversation_id }, )) @@ -343,7 +343,7 @@ impl TerminalView { ); items.push( - MenuItemFields::new("Fork") + MenuItemFields::new(i18n::t("terminal.context_menu.fork")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::ForkAIConversation { conversation_id }, )) @@ -438,7 +438,7 @@ impl TerminalView { if ChannelState::channel().is_dogfood() { menu_items.push( - MenuItemFields::new("Fork from here") + MenuItemFields::new(i18n::t("terminal.context_menu.fork_from_here")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::ForkAIConversationFromExactExchange { ai_block_view_id, @@ -454,7 +454,7 @@ impl TerminalView { // We can't revert restored blocks since we don't restore the full diff if FeatureFlag::RevertToCheckpoints.is_enabled() && !is_restored { menu_items.push( - MenuItemFields::new("Rewind to before here") + MenuItemFields::new(i18n::t("terminal.context_menu.rewind_to_before_here")) .with_on_select_action(TerminalAction::RewindAIConversation { ai_block_view_id, exchange_id: ai_exchange_id, diff --git a/app/src/terminal/view/init.rs b/app/src/terminal/view/init.rs index 46aff0e80e..636facf7ae 100644 --- a/app/src/terminal/view/init.rs +++ b/app/src/terminal/view/init.rs @@ -276,7 +276,7 @@ pub fn init(app: &mut AppContext) { #[cfg(windows)] app.register_editable_bindings([EditableBinding::new( "terminal:alternate_terminal_paste", - "Alternate terminal paste", + i18n::t("terminal.binding.alternate_terminal_paste"), TerminalAction::Paste, ) .with_key_binding("ctrl-v") @@ -314,7 +314,7 @@ pub fn init(app: &mut AppContext) { // of focus location or active-block state; fix for #9916) EditableBinding::new( OPEN_CLI_AGENT_RICH_INPUT_KEYBINDING, - "Toggle CLI Agent Rich Input", + i18n::t("terminal.binding.toggle_cli_agent_rich_input"), TerminalAction::ToggleCLIAgentRichInput, ) .with_key_binding("ctrl-g") @@ -334,7 +334,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "terminal:warpify_subshell", - "Warpify subshell", + i18n::t("terminal.use_agent_footer.warpify_subshell"), TerminalAction::TriggerSubshellBootstrap, ) .with_key_binding("ctrl-i") @@ -343,7 +343,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "terminal:warpify_ssh_session", - "Warpify ssh session", + i18n::t("terminal.use_agent_footer.warpify_ssh_session"), TerminalAction::WarpifySSHSession, ) .with_key_binding("ctrl-i") @@ -355,7 +355,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( ACCEPT_PROMPT_SUGGESTION_KEYBINDING, - "Accept Prompt Suggestion", + i18n::t("terminal.binding.accept_prompt_suggestion"), TerminalAction::ResolvePromptSuggestion(PromptSuggestionResolution::Accept { interaction_source: InteractionSource::Keybinding, }), @@ -376,9 +376,9 @@ pub fn init(app: &mut AppContext) { EditableBinding::new( CANCEL_COMMAND_KEYBINDING, if cfg!(windows) { - "Copy text or cancel active process" + i18n::t("terminal.binding.copy_text_or_cancel_process") } else { - "Cancel active process" + i18n::t("terminal.binding.cancel_process") }, TerminalAction::CtrlC, ) @@ -386,22 +386,30 @@ pub fn init(app: &mut AppContext) { .with_context_predicate(id!("Terminal") & !id!("IMEOpen")), EditableBinding::new( "terminal:focus_input", - "Focus terminal input", + i18n::t("terminal.binding.focus_input"), TerminalAction::FocusInputAndClearSelection, ) .with_custom_action(CustomAction::FocusInput) .with_context_predicate(id!("Terminal")), // Paste is not rebindable on the web. #[cfg(not(target_family = "wasm"))] - EditableBinding::new("terminal:paste", "Paste", TerminalAction::Paste) - .with_custom_action(CustomAction::Paste) - .with_context_predicate(id!("Terminal") & !id!("IMEOpen")), - EditableBinding::new("terminal:copy", "Copy", TerminalAction::Copy) - .with_custom_action(CustomAction::Copy) - .with_context_predicate(id!("Terminal") & !id!("IMEOpen")), + EditableBinding::new( + "terminal:paste", + i18n::t("common.paste"), + TerminalAction::Paste, + ) + .with_custom_action(CustomAction::Paste) + .with_context_predicate(id!("Terminal") & !id!("IMEOpen")), + EditableBinding::new( + "terminal:copy", + i18n::t("common.copy"), + TerminalAction::Copy, + ) + .with_custom_action(CustomAction::Copy) + .with_context_predicate(id!("Terminal") & !id!("IMEOpen")), EditableBinding::new( "terminal:reinput_commands", - "Reinput selected commands", + i18n::t("terminal.binding.reinput_commands"), TerminalAction::ReinputCommands, ) .with_context_predicate( @@ -409,7 +417,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "terminal:reinput_commands_with_sudo", - "Reinput selected commands as root", + i18n::t("terminal.binding.reinput_commands_with_sudo"), TerminalAction::ReinputCommandsWithSudo, ) .with_context_predicate( @@ -417,7 +425,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "terminal:find", - "Find in Terminal", + i18n::t("terminal.binding.find_in_terminal"), TerminalAction::ShowFindBar, ) .with_key_binding(cmd_or_ctrl_shift("f")) @@ -425,21 +433,21 @@ pub fn init(app: &mut AppContext) { .with_context_predicate(id!("Terminal")), EditableBinding::new( "terminal:select_bookmark_up", - "Select the closest bookmark up", + i18n::t("terminal.binding.select_bookmark_up"), TerminalAction::SelectBookmarkUp, ) .with_key_binding("alt-up") .with_context_predicate(id!("Terminal") & !id!("IMEOpen")), EditableBinding::new( "terminal:select_bookmark_down", - "Select the closest bookmark down", + i18n::t("terminal.binding.select_bookmark_down"), TerminalAction::SelectBookmarkDown, ) .with_key_binding("alt-down") .with_context_predicate(id!("Terminal") & !id!("IMEOpen")), EditableBinding::new( "terminal:open_block_list_context_menu_via_keybinding", - "Open block context menu", + i18n::t("terminal.binding.open_block_context_menu"), TerminalAction::OpenBlockListContextMenu, ) .with_mac_key_binding("ctrl-m") @@ -448,7 +456,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "terminal:toggle_teams_modal", - "Toggle team workflows modal", + i18n::t("terminal.binding.toggle_team_workflows_modal"), TerminalAction::OpenWorkflowModal, ) .with_key_binding(cmd_or_ctrl_shift("s")) @@ -459,7 +467,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "terminal:copy_git_branch", - "Copy git branch", + i18n::t("terminal.context_menu.copy_git_branch"), TerminalAction::CopyGitBranch, ) .with_context_predicate( @@ -469,7 +477,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "terminal:clear_blocks", - "Clear Blocks", + i18n::t("terminal.context_menu.clear_blocks"), TerminalAction::ClearBuffer, ) .with_custom_action(CustomAction::ClearBlocks) @@ -478,7 +486,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "terminal:executing_command_move_cursor_word_left", - "Move cursor one word to the left within an executing command", + i18n::t("terminal.binding.executing_command.move_cursor_word_left"), TerminalAction::ControlSequence(Vec::from(EscCodes::WORD_LEFT)), ) .with_mac_key_binding("alt-left") @@ -486,7 +494,7 @@ pub fn init(app: &mut AppContext) { .with_context_predicate(id!("Terminal") & !id!("IMEOpen") & id!("LongRunningCommand")), EditableBinding::new( "terminal:executing_command_move_cursor_word_right", - "Move cursor one word to the right within an executing command", + i18n::t("terminal.binding.executing_command.move_cursor_word_right"), TerminalAction::ControlSequence(Vec::from(EscCodes::WORD_RIGHT)), ) .with_mac_key_binding("alt-right") @@ -494,7 +502,7 @@ pub fn init(app: &mut AppContext) { .with_context_predicate(id!("Terminal") & !id!("IMEOpen") & id!("LongRunningCommand")), EditableBinding::new( "terminal:executing_command_move_cursor_home", - "Move cursor home within an executing command", + i18n::t("terminal.binding.executing_command.move_cursor_home"), TerminalAction::ControlSequence(vec![escape_sequences::C0::SOH]), ) // We already have bindings for home/end (the keybindings for this on Linux and Mac) that @@ -503,14 +511,14 @@ pub fn init(app: &mut AppContext) { .with_context_predicate(id!("Terminal") & !id!("IMEOpen") & id!("LongRunningCommand")), EditableBinding::new( "terminal:executing_command_move_cursor_end", - "Move cursor end within an executing command", + i18n::t("terminal.binding.executing_command.move_cursor_end"), TerminalAction::ControlSequence(vec![escape_sequences::C0::ENQ]), ) .with_mac_key_binding("cmd-right") .with_context_predicate(id!("Terminal") & !id!("IMEOpen") & id!("LongRunningCommand")), EditableBinding::new( "terminal:executing_command_delete_word_left", - "Delete word left within an executing command", + i18n::t("terminal.binding.executing_command.delete_word_left"), TerminalAction::ControlSequence(vec![escape_sequences::C0::ETB]), ) .with_mac_key_binding("alt-backspace") @@ -518,7 +526,7 @@ pub fn init(app: &mut AppContext) { .with_context_predicate(id!("Terminal") & !id!("IMEOpen") & id!("LongRunningCommand")), EditableBinding::new( "terminal:executing_command_delete_line_start", - "Delete to line start within an executing command", + i18n::t("terminal.binding.executing_command.delete_line_start"), TerminalAction::ControlSequence(vec![escape_sequences::C0::NAK]), ) .with_context_predicate(id!("Terminal") & !id!("IMEOpen") & id!("LongRunningCommand")) @@ -527,7 +535,7 @@ pub fn init(app: &mut AppContext) { .with_mac_key_binding("cmd-backspace"), EditableBinding::new( "terminal:executing_command_delete_line_end", - "Delete to line end within an executing command", + i18n::t("terminal.binding.executing_command.delete_line_end"), TerminalAction::ControlSequence(vec![escape_sequences::C0::VT]), ) .with_context_predicate(id!("Terminal") & !id!("IMEOpen") & id!("LongRunningCommand")) @@ -535,7 +543,7 @@ pub fn init(app: &mut AppContext) { .with_mac_key_binding("cmd-delete"), EditableBinding::new( "terminal:backward_tabulation", - "Backward tabulation within an executing command", + i18n::t("terminal.binding.executing_command.backward_tabulation"), TerminalAction::ControlSequence(EscCodes::build_escape_sequence_with_c1( escape_sequences::C1::CSI, EscCodes::BACKWARD_TABULATION, @@ -550,7 +558,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( SELECT_PREVIOUS_BLOCK_ACTION_NAME, - "Select previous block", + i18n::t("terminal.binding.select_previous_block"), TerminalAction::SelectPriorBlock, ) .with_custom_action(CustomAction::SelectBlockAbove) @@ -559,7 +567,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( SELECT_NEXT_BLOCK_ACTION_NAME, - "Select next block", + i18n::t("terminal.binding.select_next_block"), TerminalAction::SelectNextBlock, ) .with_custom_action(CustomAction::SelectBlockBelow) @@ -568,7 +576,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "terminal:open_share_block_modal", - "Share selected block", + i18n::t("terminal.binding.share_selected_block"), TerminalAction::OpenShareModal, ) .with_custom_action(CustomAction::CreateBlockPermalink) @@ -577,7 +585,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "terminal:bookmark_selected_block", - "Bookmark selected block", + i18n::t("terminal.binding.bookmark_selected_block"), TerminalAction::BookmarkSelectedBlock, ) .with_custom_action(CustomAction::ToggleBookmarkBlock) @@ -586,7 +594,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "terminal:find", - "Find within selected block", + i18n::t("terminal.binding.find_within_selected_block"), TerminalAction::ShowFindBar, ) .with_custom_action(CustomAction::FindWithinBlock) @@ -595,7 +603,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "terminal:copy", - "Copy command and output", + i18n::t("terminal.binding.copy_command_and_output"), TerminalAction::Copy, ) .with_custom_action(CustomAction::CopyBlock) @@ -604,7 +612,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "terminal:copy_outputs", - "Copy command output", + i18n::t("terminal.context_menu.copy_output"), TerminalAction::CopyOutputs, ) .with_custom_action(CustomAction::CopyBlockOutput) @@ -613,7 +621,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "terminal:copy_commands", - "Copy command", + i18n::t("terminal.context_menu.copy_command"), TerminalAction::CopyCommands, ) .with_custom_action(CustomAction::CopyBlockCommand) @@ -625,7 +633,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "terminal:scroll_up_one_line", - "Scroll terminal output up one line", + i18n::t("terminal.binding.scroll_up_one_line"), TerminalAction::Scroll { delta: 1.0.into_lines(), }, @@ -633,7 +641,7 @@ pub fn init(app: &mut AppContext) { .with_context_predicate(id!("Terminal") & id!("TerminalView_NonEmptyBlockList")), EditableBinding::new( "terminal:scroll_down_one_line", - "Scroll terminal output down one line", + i18n::t("terminal.binding.scroll_down_one_line"), TerminalAction::Scroll { delta: -(1.0.into_lines()), }, @@ -644,7 +652,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "terminal:scroll_up_one_page", - "Scroll terminal output up one page", + i18n::t("terminal.binding.scroll_up_one_page"), TerminalAction::PageUp, ) .with_key_binding("pageup") @@ -656,7 +664,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "terminal:scroll_down_one_page", - "Scroll terminal output down one page", + i18n::t("terminal.binding.scroll_down_one_page"), TerminalAction::PageDown, ) .with_key_binding("pagedown") @@ -670,7 +678,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( "terminal:scroll_to_top_of_selected_block", - "Scroll to top of selected block", + i18n::t("terminal.binding.scroll_to_top_of_selected_block"), TerminalAction::ScrollToTopOfSelectedBlocks, ) .with_custom_action(CustomAction::ScrollToTopOfSelectedBlocks) @@ -679,7 +687,7 @@ pub fn init(app: &mut AppContext) { )]); app.register_editable_bindings([EditableBinding::new( "terminal:scroll_to_bottom_of_selected_block", - "Scroll to bottom of selected block", + i18n::t("terminal.binding.scroll_to_bottom_of_selected_block"), TerminalAction::ScrollToBottomOfSelectedBlocks, ) .with_custom_action(CustomAction::ScrollToBottomOfSelectedBlocks) @@ -698,7 +706,7 @@ pub fn init(app: &mut AppContext) { // from the menus and doesn't conflict with cmd-A in the editor. EditableBinding::new( "terminal:select_all_blocks", - "Select all blocks", + i18n::t("terminal.binding.select_all_blocks"), TerminalAction::SelectAllBlocks, ) .with_context_predicate( @@ -707,7 +715,7 @@ pub fn init(app: &mut AppContext) { .with_custom_action(CustomAction::SelectAll), EditableBinding::new( "terminal:select_all_blocks", - "Select all blocks", + i18n::t("terminal.binding.select_all_blocks"), TerminalAction::SelectAllBlocks, ) .with_context_predicate( @@ -718,7 +726,7 @@ pub fn init(app: &mut AppContext) { } else { app.register_editable_bindings([EditableBinding::new( "terminal:select_all_blocks", - "Select all blocks", + i18n::t("terminal.binding.select_all_blocks"), TerminalAction::SelectAllBlocks, ) .with_context_predicate( @@ -729,7 +737,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "terminal:expand_block_selection_above", - "Expand selected blocks above", + i18n::t("terminal.binding.expand_selected_blocks_above"), TerminalAction::ExpandBlockSelectionAbove, ) .with_key_binding("shift-up") @@ -741,7 +749,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "terminal:expand_block_selection_below", - "Expand selected blocks below", + i18n::t("terminal.binding.expand_selected_blocks_below"), TerminalAction::ExpandBlockSelectionBelow, ) .with_key_binding("shift-down") @@ -756,11 +764,13 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "terminal:ask_ai_assistant", - BindingDescription::new("Attach Selected Block as Agent Context") - .with_custom_description( - bindings::MAC_MENUS_CONTEXT, - "Attach Selection as Agent Context", - ), + BindingDescription::new(i18n::t( + "terminal.binding.attach_selected_block_as_agent_context", + )) + .with_custom_description( + bindings::MAC_MENUS_CONTEXT, + i18n::t("terminal.binding.attach_selection_as_agent_context"), + ), TerminalAction::ContextMenu(ContextMenuAction::AskAI(AskAISource::SelectedBlocks)), ) .with_enabled(|| FeatureFlag::AgentMode.is_enabled()) @@ -777,11 +787,13 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "terminal:ask_ai_assistant", - BindingDescription::new("Attach Selected Text as Agent Context") - .with_custom_description( - bindings::MAC_MENUS_CONTEXT, - "Attach Selection as Agent Context", - ), + BindingDescription::new(i18n::t( + "terminal.binding.attach_selected_text_as_agent_context", + )) + .with_custom_description( + bindings::MAC_MENUS_CONTEXT, + i18n::t("terminal.binding.attach_selection_as_agent_context"), + ), TerminalAction::ContextMenu(ContextMenuAction::AskAI( AskAISource::SelectedTerminalText, )), @@ -800,7 +812,7 @@ pub fn init(app: &mut AppContext) { // this is a block selection or text selection later on. EditableBinding::new( "terminal:ask_ai_assistant", - "Ask Warp AI about Selection", + i18n::t("terminal.binding.ask_warp_ai_about_selection"), TerminalAction::ContextMenu(ContextMenuAction::AskAI(AskAISource::SelectedBlockOrText)), ) .with_enabled(|| !FeatureFlag::AgentMode.is_enabled()) @@ -818,7 +830,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "terminal:ask_ai_assistant_last_block", - "Ask Warp AI about last block", + i18n::t("terminal.binding.ask_warp_ai_about_last_block"), TerminalAction::ContextMenu(ContextMenuAction::AskAI(AskAISource::LastBlock)), ) .with_enabled(|| !FeatureFlag::AgentMode.is_enabled()) @@ -829,7 +841,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "terminal:ask_ai_assistant", - "Ask Warp AI", + i18n::t("terminal.binding.ask_warp_ai"), TerminalAction::ContextMenu(ContextMenuAction::AskAI(AskAISource::SelectedInputText)), ) .with_enabled(|| !FeatureFlag::AgentMode.is_enabled()) @@ -841,7 +853,7 @@ pub fn init(app: &mut AppContext) { if FeatureFlag::CommandCorrectionKey.is_enabled() { app.register_editable_bindings([EditableBinding::new( "input:insert_command_correction", - "Insert Command Correction", + i18n::t("terminal.binding.insert_command_correction"), TerminalAction::InsertMostRecentCommandCorrection, ) .with_context_predicate(id!("Terminal"))]); @@ -850,7 +862,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "terminal:onboarding_flow", - "Setup Guide", + i18n::t("terminal.binding.setup_guide"), TerminalAction::OnboardingFlow(OnboardingVersion::Legacy), ) .with_context_predicate( @@ -947,7 +959,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( "workspace:open_settings_import_page", - "Import External Settings", + i18n::t("terminal.binding.import_external_settings"), TerminalAction::ImportSettings, ) .with_context_predicate(id!("Terminal") & id!(flags::HAS_SETTINGS_TO_IMPORT_FLAG))]); @@ -955,7 +967,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "terminal:share_current_session", - "Share current session", + i18n::t("terminal.binding.share_current_session"), TerminalAction::OpenShareSessionModal { source: SharedSessionActionSource::CommandPalette, }, @@ -970,7 +982,7 @@ pub fn init(app: &mut AppContext) { }), EditableBinding::new( "terminal:stop_sharing_current_session", - "Stop sharing current session", + i18n::t("terminal.binding.stop_sharing_current_session"), TerminalAction::StopSharingCurrentSession { source: SharedSessionActionSource::CommandPalette, }, @@ -982,7 +994,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( TOGGLE_BLOCK_FILTER_KEYBINDING, - "Toggle block filter on selected or last block", + i18n::t("terminal.binding.toggle_block_filter_selected_or_last"), TerminalAction::ToggleBlockFilterOnSelectedOrLastBlock(ToggleBlockFilterSource::Binding), ) .with_mac_key_binding("shift-alt-F") @@ -990,7 +1002,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( "terminal:toggle_snackbar_in_active_pane", - "Toggle Sticky Command Header in Active Pane", + i18n::t("terminal.binding.toggle_sticky_command_header"), TerminalAction::ToggleSnackbarInActivePane, ) .with_context_predicate(id!("Terminal"))]); @@ -998,7 +1010,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( TOGGLE_AUTOEXECUTE_MODE_KEYBINDING, - "Toggle Auto-execute Mode", + i18n::t("terminal.binding.toggle_autoexecute_mode"), TerminalAction::ToggleAutoexecuteMode, ) .with_key_binding("cmdorctrl-shift-I") @@ -1007,7 +1019,7 @@ pub fn init(app: &mut AppContext) { .with_enabled(|| FeatureFlag::FastForwardAutoexecuteButton.is_enabled()), EditableBinding::new( TOGGLE_QUEUE_NEXT_PROMPT_KEYBINDING, - "Toggle Queue Next Prompt", + i18n::t("terminal.binding.toggle_queue_next_prompt"), TerminalAction::ToggleQueueNextPrompt, ) .with_key_binding("cmdorctrl-shift-J") @@ -1052,7 +1064,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( "workspace:write_codebase_index", - BindingDescription::new("Write current codebase index snapshot"), + BindingDescription::new(i18n::t("terminal.binding.write_codebase_index_snapshot")), TerminalAction::WriteCodebaseIndex, ) .with_enabled(|| FeatureFlag::CodebaseIndexPersistence.is_enabled()) @@ -1060,7 +1072,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( "terminal:load_agent_mode_conversation", - "Load agent mode conversation (from debug link in clipboard)", + i18n::t("terminal.binding.load_agent_mode_conversation"), TerminalAction::LoadAgentModeConversation, ) .with_enabled(ChannelState::enable_debug_features) @@ -1068,7 +1080,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( "terminal:toggle_session_recording", - "Toggle PTY Recording for Session", + i18n::t("terminal.binding.toggle_session_recording"), TerminalAction::ToggleSessionRecording, ) .with_enabled(|| cfg!(feature = "local_fs") && ChannelState::enable_debug_features()) @@ -1076,14 +1088,14 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( "workspace:init_project_rules", - BindingDescription::new("Initiate project for warp"), + BindingDescription::new(i18n::t("terminal.binding.initiate_project_for_warp")), TerminalAction::InitProject, ) .with_context_predicate(id!("Workspace") & id!(flags::IS_ANY_AI_ENABLED))]); app.register_editable_bindings([EditableBinding::new( "workspace:add_current_dir_as_project", - BindingDescription::new("Add current folder as project"), + BindingDescription::new(i18n::t("terminal.binding.add_current_folder_as_project")), TerminalAction::AddProjectAtCurrentDirectory, ) .with_enabled(|| FeatureFlag::Projects.is_enabled()) @@ -1092,7 +1104,7 @@ pub fn init(app: &mut AppContext) { #[cfg(not(target_arch = "wasm32"))] app.register_editable_bindings([EditableBinding::new( "terminal:toggle_conversation_details_panel", - "Toggle Conversation Details Panel", + i18n::t("terminal.binding.toggle_conversation_details_panel"), TerminalAction::ToggleConversationDetailsPanel, ) .with_group(bindings::BindingGroup::WarpAi.as_str()) @@ -1184,7 +1196,7 @@ fn register_input_mode_bindings(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( SET_INPUT_MODE_AGENT_ACTION_NAME, - "Set Input Mode to Agent Mode", + i18n::t("terminal.binding.set_input_mode_agent"), TerminalAction::SetInputModeAgent, ) .with_group(bindings::BindingGroup::WarpAi.as_str()) @@ -1193,7 +1205,7 @@ fn register_input_mode_bindings(app: &mut AppContext) { .with_linux_or_windows_key_binding("ctrl-i"), EditableBinding::new( SET_INPUT_MODE_TERMINAL_ACTION_NAME, - "Set Input Mode to Terminal Mode", + i18n::t("terminal.binding.set_input_mode_terminal"), TerminalAction::SetInputModeTerminal, ) .with_group(bindings::BindingGroup::WarpAi.as_str()) @@ -1202,7 +1214,7 @@ fn register_input_mode_bindings(app: &mut AppContext) { .with_linux_or_windows_key_binding("ctrl-i"), EditableBinding::new( TOGGLE_HIDE_CLI_RESPONSES_KEYBINDING, - "Toggle Hide CLI Responses", + i18n::t("terminal.binding.toggle_hide_cli_responses"), TerminalAction::ToggleHideCliResponses, ) .with_key_binding("cmdorctrl-g") diff --git a/app/src/terminal/view/init_environment/mod.rs b/app/src/terminal/view/init_environment/mod.rs index 2306ea78b6..5715f5dafc 100644 --- a/app/src/terminal/view/init_environment/mod.rs +++ b/app/src/terminal/view/init_environment/mod.rs @@ -19,9 +19,6 @@ use crate::ai::blocklist::inline_action::inline_action_icons::cancelled_icon; use crate::ai::blocklist::inline_action::requested_action::RenderableAction; use crate::appearance::Appearance; -const EXPLANATION_TEXT: &str = "Would you like to create an environment for this project so you can run cloud agents in it? The agent will guide you through choosing GitHub repos, configuring a Docker image, and specifying startup commands."; -const NO_REPOS_HELP_TEXT: &str = "If you want to create an environment with repos, rerun this command and pass in file paths or GitHub links as arguments, e.g. \"/create-environment \"."; - #[derive(Debug, Clone)] pub enum InitEnvironmentBlockAction { StartSetup, @@ -85,7 +82,7 @@ impl InitEnvironmentBlock { ), // Skip button simple_navigation_button( - "Cancel".to_string(), + i18n::t("common.cancel"), MouseStateHandle::default(), InitEnvironmentBlockAction::Skip, false, @@ -114,7 +111,7 @@ impl InitEnvironmentBlock { // Add help text if we don't have any repos to make it clearer if self.repos.is_empty() && !self.use_current_dir { let help_text = Text::new( - NO_REPOS_HELP_TEXT, + i18n::t("terminal.init_environment.no_repos_help"), appearance.ui_font_family(), appearance.monospace_font_size() - 2., ) @@ -131,7 +128,7 @@ impl InitEnvironmentBlock { RenderableAction::new_with_element(content.finish(), app) .with_header( - HeaderConfig::new(EXPLANATION_TEXT, app) + HeaderConfig::new(i18n::t("terminal.init_environment.explanation"), app) .with_icon(yellow_stop_icon(appearance)) .with_corner_radius_override(CornerRadius::with_top(Radius::Pixels(8.))) .with_soft_wrap_title(), @@ -156,11 +153,13 @@ impl View for InitEnvironmentBlock { let rendered_step = match &self.setup_state { SetupState::Pending { action_view } => self.render_pending_step(action_view, app), - SetupState::Skipped => RenderableAction::new("Environment setup cancelled", app) - .with_icon(cancelled_icon(appearance).finish()) - .with_content_item_spacing() - .render(app) - .finish(), + SetupState::Skipped => { + RenderableAction::new(&i18n::t("terminal.init_environment.cancelled"), app) + .with_icon(cancelled_icon(appearance).finish()) + .with_content_item_spacing() + .render(app) + .finish() + } }; Container::new(rendered_step).with_padding_top(16.).finish() } diff --git a/app/src/terminal/view/init_environment/mode_selector.rs b/app/src/terminal/view/init_environment/mode_selector.rs index 4456b66caa..a167b7294f 100644 --- a/app/src/terminal/view/init_environment/mode_selector.rs +++ b/app/src/terminal/view/init_environment/mode_selector.rs @@ -129,7 +129,7 @@ impl EnvironmentSetupModeSelector { let theme = appearance.theme(); let title = Text::new( - "Choose how you'd like to set up your environment".to_string(), + i18n::t("terminal.init_environment.mode.title"), appearance.ui_font_family(), TITLE_FONT_SIZE, ) @@ -188,8 +188,8 @@ impl EnvironmentSetupModeSelector { &self, index: usize, icon: Icon, - title: &'static str, - description: &'static str, + title: String, + description: String, is_suggested: bool, mouse_state: MouseStateHandle, action: EnvironmentSetupModeSelectorAction, @@ -251,7 +251,7 @@ impl EnvironmentSetupModeSelector { .with_border(Border::all(1.).with_border_color(avatar_border)) .finish(); - let title_text = Text::new(title.to_string(), font_family, OPTION_TITLE_FONT_SIZE) + let title_text = Text::new(title.clone(), font_family, OPTION_TITLE_FONT_SIZE) .with_style(Properties::default().weight(Weight::Semibold)) .with_color(active_text.into()) .finish(); @@ -262,11 +262,14 @@ impl EnvironmentSetupModeSelector { .with_child(title_text); if is_suggested { - let suggested_text = - Text::new("Suggested".to_string(), font_family, OPTION_DESC_FONT_SIZE) - .with_style(Properties::default().weight(Weight::Medium)) - .with_color(badge_text_color) - .finish(); + let suggested_text = Text::new( + i18n::t("common.suggested"), + font_family, + OPTION_DESC_FONT_SIZE, + ) + .with_style(Properties::default().weight(Weight::Medium)) + .with_color(badge_text_color) + .finish(); let suggested = Container::new(suggested_text) .with_horizontal_padding(8.) @@ -280,7 +283,7 @@ impl EnvironmentSetupModeSelector { } let description_text = - Text::new(description.to_string(), font_family, OPTION_DESC_FONT_SIZE) + Text::new(description.clone(), font_family, OPTION_DESC_FONT_SIZE) .with_style(Properties::default().weight(Weight::Normal)) .with_color(nonactive_text.into()) .soft_wrap(true) @@ -339,8 +342,8 @@ impl EnvironmentSetupModeSelector { let remote_github_option = self.render_option( 0, Icon::Github, - "Quick setup", - "Select the GitHub repositories you'd like to work with and we'll suggest a base image and config", + i18n::t("terminal.init_environment.mode.quick_setup.title"), + i18n::t("terminal.init_environment.mode.quick_setup.description"), true, self.remote_github_mouse_state.clone(), EnvironmentSetupModeSelectorAction::SelectRemoteGitHub, @@ -350,8 +353,8 @@ impl EnvironmentSetupModeSelector { let local_repos_option = self.render_option( 1, Icon::Terminal, - "Use the agent", - "Choose a locally set up project and we'll help you set up an environment based on it", + i18n::t("terminal.init_environment.mode.use_agent.title"), + i18n::t("terminal.init_environment.mode.use_agent.description"), false, self.local_repos_mouse_state.clone(), EnvironmentSetupModeSelectorAction::SelectLocalRepositories, diff --git a/app/src/terminal/view/init_project/lsp_server_selector.rs b/app/src/terminal/view/init_project/lsp_server_selector.rs index 7654dca147..d2efedc8c0 100644 --- a/app/src/terminal/view/init_project/lsp_server_selector.rs +++ b/app/src/terminal/view/init_project/lsp_server_selector.rs @@ -126,10 +126,13 @@ pub fn render_lsp_selector_block( ); let title_element = Span::new( - "Would you like to enable available language support for this codebase? This will give you smarter code navigation and inline error checking.", + i18n::t("terminal.init_project.lsp.multiple_prompt"), UiComponentStyles { font_family_id: Some(appearance.ui_font_family()), - font_color: Some(blended_colors::text_main(appearance.theme(), header_background)), + font_color: Some(blended_colors::text_main( + appearance.theme(), + header_background, + )), font_size: Some(appearance.monospace_font_size()), ..Default::default() }, @@ -159,7 +162,7 @@ pub fn render_lsp_selector_block( let skip_button = appearance .ui_builder() .button(ButtonVariant::Text, skip_mouse_state.clone()) - .with_text_label("Skip for now".to_string()) + .with_text_label(i18n::t("terminal.init_project.skip_for_now")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(InitProjectBlockAction::SkipLanguageServers); @@ -172,9 +175,9 @@ pub fn render_lsp_selector_block( let any_needs_download = selected_items.iter().any(|info| !info.is_installed); let enable_label = if any_needs_download { - "Install and enable" + i18n::t("terminal.init_project.lsp.install_and_enable") } else { - "Enable language support" + i18n::t("terminal.init_project.lsp.enable_language_support") }; // Create keyboard shortcut for Enter diff --git a/app/src/terminal/view/init_project/mod.rs b/app/src/terminal/view/init_project/mod.rs index 5a7e864aa0..d59c89c086 100644 --- a/app/src/terminal/view/init_project/mod.rs +++ b/app/src/terminal/view/init_project/mod.rs @@ -40,8 +40,6 @@ use crate::view_components::DismissibleToast; use crate::workspace::ToastStack; use crate::{send_telemetry_from_ctx, TelemetryEvent}; -const ONBOARDING_TEXT: &str = "Great - let's begin setting up this project! Would you like to give me permission to index this codebase? It allows me to quickly understand context and provide more targeted solutions when working in this codebase. No code is stored on Warp servers."; -const ALREADY_SETUP_TEXT: &str = "It looks like this project has already been initialized. You can re-generate the AGENTS.md for this codebase by clicking the button below."; // Native Warp rules file format. pub const FILES_TO_CHECK: [&str; 2] = ["AGENTS.md", "WARP.md"]; // File formats that can be linked to WARP.md. @@ -374,10 +372,16 @@ impl InitStepBlock { mouse_states: &LanguageServersMouseStateHandles, ) -> Vec { let button_text = if server_info.is_installed { - format!("Enable {} support", server_info.server_type.language_name()) + format!( + "{}{}{}", + i18n::t("terminal.init_project.lsp.enable_prefix"), + server_info.server_type.language_name(), + i18n::t("terminal.init_project.lsp.enable_suffix") + ) } else { format!( - "Install and enable {}", + "{}{}", + i18n::t("terminal.init_project.lsp.install_and_enable_prefix"), server_info.server_type.language_name() ) }; @@ -393,7 +397,7 @@ impl InitStepBlock { false, ), simple_navigation_button( - "Skip for now.".to_string(), + i18n::t("terminal.init_project.skip_for_now"), mouse_states.skip_button.clone(), InitProjectBlockAction::SkipLanguageServers, false, @@ -407,13 +411,13 @@ impl InitStepBlock { ) -> Vec { vec![ simple_navigation_button( - "Yes, index this codebase.".to_string(), + i18n::t("terminal.init_project.codebase.index_button"), mouse_states.index_button.clone(), InitProjectBlockAction::IndexCodebase(pwd_path.to_path_buf()), false, ), simple_navigation_button( - "Skip for now.".to_string(), + i18n::t("terminal.init_project.skip_for_now"), mouse_states.skip_button.clone(), InitProjectBlockAction::SkipIndex, false, @@ -430,7 +434,12 @@ impl InitStepBlock { for (i, linkable_file) in LINKABLE_FILES.iter().enumerate() { if let Some(path) = linkable_files.iter().find(|p| p.ends_with(linkable_file)) { buttons.push(simple_navigation_button( - format!("Link existing {linkable_file} to my AGENTS.md file"), + format!( + "{}{}{}", + i18n::t("terminal.init_project.project_rules.link_existing_prefix"), + linkable_file, + i18n::t("terminal.init_project.project_rules.link_existing_suffix") + ), mouse_states.link_buttons[i].clone(), InitProjectBlockAction::LinkFromExisting(path.clone()), false, @@ -439,13 +448,13 @@ impl InitStepBlock { } buttons.push(simple_navigation_button( - "Generate AGENTS.md file".to_string(), + i18n::t("terminal.init_project.project_rules.generate_button"), mouse_states.generate_button.clone(), InitProjectBlockAction::GenerateRules, false, )); buttons.push(simple_navigation_button( - "Skip AGENTS.md generation for now".to_string(), + i18n::t("terminal.init_project.project_rules.skip_generation_button"), mouse_states.skip_button.clone(), InitProjectBlockAction::SkipRules, false, @@ -459,13 +468,13 @@ impl InitStepBlock { ) -> Vec { vec![ simple_navigation_button( - "Create an environment".to_string(), + i18n::t("terminal.init_project.environment.create_button"), mouse_states.create_button.clone(), InitProjectBlockAction::StartCreateEnvironment, false, ), simple_navigation_button( - "Skip for now".to_string(), + i18n::t("terminal.init_project.skip_for_now"), mouse_states.skip_button.clone(), InitProjectBlockAction::SkipCreateEnvironment, false, @@ -519,7 +528,9 @@ impl InitStepBlock { let mut button = appearance .ui_builder() .button(ButtonVariant::Outlined, mouse_state.clone()) - .with_text_label("Re-generate AGENTS.md file".to_string()); + .with_text_label(i18n::t( + "terminal.init_project.project_rules.regenerate_button", + )); if disabled { button = button.disabled(); } @@ -575,9 +586,9 @@ impl InitStepBlock { let is_already_setup = self.model.as_ref(app).is_already_setup(); let display_text = if !is_already_setup { - ONBOARDING_TEXT + i18n::t("terminal.init_project.welcome.onboarding") } else { - ALREADY_SETUP_TEXT + i18n::t("terminal.init_project.welcome.already_setup") }; let text = Text::new( @@ -633,12 +644,9 @@ impl InitStepBlock { app, ) .with_header( - HeaderConfig::new( - "Would you like the Agent to index this codebase? This will lead to more efficient and tailored help.", - app, - ) - .with_icon(yellow_stop_icon(appearance)) - .with_soft_wrap_title(), + HeaderConfig::new(i18n::t("terminal.init_project.codebase.prompt"), app) + .with_icon(yellow_stop_icon(appearance)) + .with_soft_wrap_title(), ) .with_background_color(appearance.theme().surface_1().into_solid()) .with_content_item_spacing() @@ -670,7 +678,8 @@ impl InitStepBlock { match indexing_result { CodebaseIndexingResult::Accepted => { - RenderableAction::new("Codebase index started", app) + let label = i18n::t("terminal.init_project.codebase.started"); + RenderableAction::new(&label, app) .with_icon(Icon::Check.to_warpui_icon(Fill::success()).finish()) .with_action_button( Appearance::as_ref(app) @@ -679,7 +688,7 @@ impl InitStepBlock { ButtonVariant::Outlined, mouse_states.view_status_button.clone(), ) - .with_text_label("View index status".to_string()) + .with_text_label(i18n::t("terminal.init_project.codebase.view_status")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action( @@ -693,7 +702,8 @@ impl InitStepBlock { .finish() } CodebaseIndexingResult::Skipped => { - Self::render_skipped_completion("Codebase index cancelled", app) + let label = i18n::t("terminal.init_project.codebase.cancelled"); + Self::render_skipped_completion(&label, app) } } } @@ -743,8 +753,10 @@ impl InitStepBlock { Self::render_ready_with_buttons( action_view, format!( - "Enable {} support for this codebase? This will give you smarter code navigation, inline error checking, and more.", - server_info.server_type.language_name() + "{}{}{}", + i18n::t("terminal.init_project.lsp.single_prompt_prefix"), + server_info.server_type.language_name(), + i18n::t("terminal.init_project.lsp.single_prompt_suffix") ), app, ) @@ -788,19 +800,22 @@ impl InitStepBlock { servers_to_install, } => { let label = if !servers_to_install.is_empty() { - "Started installation for language support".to_string() + i18n::t("terminal.init_project.lsp.started_install") } else if enabled_servers.len() == 1 { format!( - "{} language support enabled", - enabled_servers[0].language_name() + "{}{}{}", + i18n::t("terminal.init_project.lsp.single_enabled_prefix"), + enabled_servers[0].language_name(), + i18n::t("terminal.init_project.lsp.single_enabled_suffix") ) } else { - "Language support enabled".to_string() + i18n::t("terminal.init_project.lsp.enabled") }; Self::render_success_completion(&label, app) } LanguageServersResult::Skipped => { - Self::render_skipped_completion("Language support skipped", app) + let label = i18n::t("terminal.init_project.lsp.skipped"); + Self::render_skipped_completion(&label, app) } } } @@ -827,14 +842,15 @@ impl InitStepBlock { }; Self::render_ready_with_buttons( action_view, - "Would you like to create an AGENTS.md file? Warp can create one for you with project specific rules, context, and conventions inferred from your codebase. The agent will use this context as it codes.", + i18n::t("terminal.init_project.project_rules.prompt"), app, ) } InitStepStatus::Running => { // AI is generating AGENTS.md - show in-progress state let appearance = Appearance::as_ref(app); - RenderableAction::new("Generating AGENTS.md...", app) + let label = i18n::t("terminal.init_project.project_rules.generating"); + RenderableAction::new(&label, app) .with_icon(in_progress_icon(appearance).finish()) .with_content_item_spacing() .render(app) @@ -866,13 +882,14 @@ impl InitStepBlock { }; Self::render_ready_with_buttons( action_view, - "Would you like to create an environment for this project so you can run cloud agents in it? The agent will guide you through choosing GitHub repos, configuring a Docker image, and specifying startup commands.", + i18n::t("terminal.init_project.environment.prompt"), app, ) } InitStepStatus::Running => { let appearance = Appearance::as_ref(app); - RenderableAction::new("Creating environment...", app) + let label = i18n::t("terminal.init_project.environment.creating"); + RenderableAction::new(&label, app) .with_icon(in_progress_icon(appearance).finish()) .with_content_item_spacing() .render(app) @@ -895,10 +912,12 @@ impl InitStepBlock { match env_result { CreateEnvironmentResult::Created => { - Self::render_success_completion("Environment created", app) + let label = i18n::t("terminal.init_project.environment.created"); + Self::render_success_completion(&label, app) } CreateEnvironmentResult::Skipped => { - Self::render_skipped_completion("Environment creation skipped", app) + let label = i18n::t("terminal.init_project.environment.skipped"); + Self::render_skipped_completion(&label, app) } } } @@ -921,12 +940,18 @@ impl InitStepBlock { let init_completed = self.model.as_ref(app).is_completed(); match rules_result { ProjectScopedRulesResult::LinkedFromExisting(path) => { - Self::render_success_completion(&format!("Project rules linked from {path}"), app) + let label = format!( + "{}{}", + i18n::t("terminal.init_project.project_rules.linked_from_prefix"), + path + ); + Self::render_success_completion(&label, app) } ProjectScopedRulesResult::GenerateNew { button_disabled, .. } => { - let mut action = RenderableAction::new("Project rules configured", app) + let label = i18n::t("terminal.init_project.project_rules.configured"); + let mut action = RenderableAction::new(&label, app) .with_icon(Icon::Check.to_warpui_icon(Fill::success()).finish()); if init_completed { action = action.with_action_button(Self::regenerate_button( @@ -938,7 +963,8 @@ impl InitStepBlock { action.with_content_item_spacing().render(app).finish() } ProjectScopedRulesResult::AlreadyExists { button_disabled } => { - let mut action = RenderableAction::new("Project rules already configured", app) + let label = i18n::t("terminal.init_project.project_rules.already_configured"); + let mut action = RenderableAction::new(&label, app) .with_icon(Icon::Check.to_warpui_icon(Fill::success()).finish()); if init_completed { action = action.with_action_button(Self::regenerate_button( @@ -950,7 +976,8 @@ impl InitStepBlock { action.with_content_item_spacing().render(app).finish() } ProjectScopedRulesResult::Skipped => { - Self::render_skipped_completion("Project rules skipped", app) + let label = i18n::t("terminal.init_project.project_rules.skipped"); + Self::render_skipped_completion(&label, app) } } } @@ -991,8 +1018,9 @@ impl InitStepBlock { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( DismissibleToast::success(format!( - "{} installed and enabled successfully.", - server_type.binary_name() + "{}{}", + server_type.binary_name(), + i18n::t("terminal.init_project.toast.lsp_install_success_suffix") )), window_id, ctx, @@ -1015,8 +1043,11 @@ impl InitStepBlock { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( DismissibleToast::error(format!( - "Failed to install {}: {e}", - server_type.binary_name() + "{}{}{}{}", + i18n::t("terminal.init_project.toast.lsp_install_failed_prefix"), + server_type.binary_name(), + i18n::t("terminal.init_project.toast.lsp_install_failed_suffix"), + e )), window_id, ctx, @@ -1139,8 +1170,10 @@ impl TypedActionView for InitStepBlock { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( DismissibleToast::default(format!( - "Installing {} in background...", - server_names.join(", ") + "{}{}{}", + i18n::t("terminal.init_project.toast.lsp_installing_prefix"), + server_names.join(", "), + i18n::t("terminal.init_project.toast.lsp_installing_suffix") )), window_id, ctx, diff --git a/app/src/terminal/view/inline_banner/agent_mode_setup.rs b/app/src/terminal/view/inline_banner/agent_mode_setup.rs index 20af0c3a39..d9ab308407 100644 --- a/app/src/terminal/view/inline_banner/agent_mode_setup.rs +++ b/app/src/terminal/view/inline_banner/agent_mode_setup.rs @@ -12,11 +12,6 @@ use crate::appearance::Appearance; use crate::terminal::view::inline_banner::InlineBannerIcon; use crate::terminal::view::{InlineBannerId, TerminalAction}; -const SPEEDBUMP_HEADER: &str = "Optimize Warp for this codebase?"; -const SPEEDBUMP_TEXT: &str = "Unlock smarter, more consistent responses by letting the Agent understand your codebase and generate rules for it. You can also do this at any point by running /init"; -/// Text for the button that allows execution -const ALLOW_BUTTON_TEXT: &str = "Optimize"; - #[derive(Clone, Copy, Debug)] pub enum AgentModeSetupSpeedbumpBannerAction { SetupAgentMode, @@ -51,7 +46,7 @@ pub fn render_agent_mode_setup_banner( appearance: &Appearance, ) -> Box { let open_button = InlineBannerTextButton { - text: ALLOW_BUTTON_TEXT.to_string(), + text: i18n::t("terminal.inline_banner.agent_mode_setup.optimize"), text_color: appearance.theme().active_ui_text_color().into_solid(), button_state: InlineBannerButtonState { on_click_event: TerminalAction::AgentModeSetupSpeedbumpBanner( @@ -75,7 +70,7 @@ pub fn render_agent_mode_setup_banner( InlineBannerStyle::Recommendation, appearance, InlineBannerContent { - title: SPEEDBUMP_HEADER.to_string(), + title: i18n::t("terminal.inline_banner.agent_mode_setup.title"), buttons: vec![open_button], close_button: Some(close_button), header_icon: Some(InlineBannerIcon { @@ -84,7 +79,7 @@ pub fn render_agent_mode_setup_banner( color_override: Some(appearance.theme().active_ui_text_color().into_solid()), }), content: Some(vec![Text::new( - SPEEDBUMP_TEXT, + i18n::t("terminal.inline_banner.agent_mode_setup.body"), appearance.ui_font_family(), appearance.monospace_font_size() - 2., ) diff --git a/app/src/terminal/view/inline_banner/alias_expansion.rs b/app/src/terminal/view/inline_banner/alias_expansion.rs index 1293e63251..b5393384f0 100644 --- a/app/src/terminal/view/inline_banner/alias_expansion.rs +++ b/app/src/terminal/view/inline_banner/alias_expansion.rs @@ -39,7 +39,7 @@ pub fn render_alias_expansion_banner( let accent_color = appearance.theme().accent().into_solid(); let buttons = vec![InlineBannerTextButton { - text: "Enable alias expansion".to_owned(), + text: i18n::t("terminal.inline_banner.alias_expansion.enable"), text_color: active_ui_text_color.into_solid(), button_state: InlineBannerButtonState { on_click_event: TerminalAction::AliasExpansionBanner( @@ -88,7 +88,7 @@ pub fn render_alias_expansion_banner( InlineBannerStyle::VeryLowPriority, appearance, InlineBannerContent { - title: "Warp can auto-expand aliases.".into(), + title: i18n::t("terminal.inline_banner.alias_expansion.title"), buttons, content: Some(content), close_button: Some(close_button), diff --git a/app/src/terminal/view/inline_banner/anonymous_user_ai_sign_up.rs b/app/src/terminal/view/inline_banner/anonymous_user_ai_sign_up.rs index 79bb2c1170..99c5ead1f2 100644 --- a/app/src/terminal/view/inline_banner/anonymous_user_ai_sign_up.rs +++ b/app/src/terminal/view/inline_banner/anonymous_user_ai_sign_up.rs @@ -15,11 +15,6 @@ use crate::terminal::view::{InlineBannerId, TerminalAction}; use crate::ui_components::buttons::icon_button; use crate::ui_components::icons::Icon as UiIcon; -const TITLE: &str = "Login for AI"; -const CONTENT: &str = - "AI features are unavailable for logged-out users. Create an account to use AI."; -const SIGN_UP_BUTTON_TEXT: &str = "Sign Up"; - // Layout constants for three-column banner const ICON_SIZE_OFFSET: f32 = 3.0; const TEXT_COLUMN_LEFT_PADDING: f32 = 8.0; @@ -51,9 +46,9 @@ impl AnonymousUserAISignUpBannerState { pub fn render(&self, appearance: &Appearance) -> Box { render_three_column_inline_banner( appearance, - TITLE, - CONTENT, - SIGN_UP_BUTTON_TEXT, + i18n::t("terminal.inline_banner.anonymous_user_ai_sign_up.title"), + i18n::t("terminal.inline_banner.anonymous_user_ai_sign_up.content"), + i18n::t("common.sign_up"), self.sign_up_button_mouse_state.clone(), self.close_button_mouse_state.clone(), ) @@ -66,9 +61,9 @@ impl AnonymousUserAISignUpBannerState { /// - Column 3: Buttons (Sign Up + Close) fn render_three_column_inline_banner( appearance: &Appearance, - title: &str, - content: &str, - button_text: &str, + title: String, + content: String, + button_text: String, button_mouse_state: MouseStateHandle, close_button_mouse_state: MouseStateHandle, ) -> Box { @@ -137,14 +132,10 @@ fn render_three_column_inline_banner( // Row 1: Title let title_row = Container::new( - Text::new( - title.to_owned(), - appearance.ui_font_family(), - title_font_size, - ) - .with_color(active_text_color) - .soft_wrap(true) - .finish(), + Text::new(title, appearance.ui_font_family(), title_font_size) + .with_color(active_text_color) + .soft_wrap(true) + .finish(), ) .with_padding_left(TEXT_COLUMN_LEFT_PADDING) .finish(); @@ -152,14 +143,10 @@ fn render_three_column_inline_banner( // Row 2: Content let content_row = Container::new( - Text::new( - content.to_owned(), - appearance.ui_font_family(), - content_font_size, - ) - .with_color(content_text_color) - .soft_wrap(true) - .finish(), + Text::new(content, appearance.ui_font_family(), content_font_size) + .with_color(content_text_color) + .soft_wrap(true) + .finish(), ) .with_padding_left(TEXT_COLUMN_LEFT_PADDING) .with_padding_top(CONTENT_TOP_PADDING) diff --git a/app/src/terminal/view/inline_banner/aws_bedrock_login.rs b/app/src/terminal/view/inline_banner/aws_bedrock_login.rs index c99a11e5dd..da93a89cea 100644 --- a/app/src/terminal/view/inline_banner/aws_bedrock_login.rs +++ b/app/src/terminal/view/inline_banner/aws_bedrock_login.rs @@ -30,7 +30,7 @@ pub fn render_aws_bedrock_login_banner( let active_ui_text_color = appearance.theme().active_ui_text_color().into_solid(); let buttons = vec![ InlineBannerTextButton { - text: "Don't show again".to_owned(), + text: i18n::t("common.do_not_show_again"), text_color: active_ui_text_color, button_state: InlineBannerButtonState { on_click_event: TerminalAction::AwsBedrockLoginBanner( @@ -43,7 +43,7 @@ pub fn render_aws_bedrock_login_banner( variant: InlineBannerTextButtonVariant::Secondary, }, InlineBannerTextButton { - text: "Log into AWS".to_owned(), + text: i18n::t("terminal.inline_banner.aws_bedrock.log_in"), text_color: active_ui_text_color, button_state: InlineBannerButtonState { on_click_event: TerminalAction::AwsBedrockLoginBanner( @@ -64,7 +64,7 @@ pub fn render_aws_bedrock_login_banner( // Use sub_text_color for description to differentiate from title let description_text = warpui::elements::Text::new( - "Your Warp admin has enabled AWS Bedrock for your team.", + i18n::t("terminal.inline_banner.aws_bedrock_enabled"), appearance.ui_font_family(), appearance.monospace_font_size() - 2., ) @@ -75,7 +75,7 @@ pub fn render_aws_bedrock_login_banner( InlineBannerStyle::Recommendation, appearance, InlineBannerContent { - title: "Use AWS Bedrock?".to_string(), + title: i18n::t("terminal.inline_banner.aws_bedrock.title"), content: Some(vec![description_text]), buttons, close_button: Some(close_button), diff --git a/app/src/terminal/view/inline_banner/aws_cli_not_installed.rs b/app/src/terminal/view/inline_banner/aws_cli_not_installed.rs index 074f5111c9..246bd39bd6 100644 --- a/app/src/terminal/view/inline_banner/aws_cli_not_installed.rs +++ b/app/src/terminal/view/inline_banner/aws_cli_not_installed.rs @@ -46,7 +46,7 @@ pub fn render_aws_cli_not_installed_banner( ) -> Box { let active_ui_text_color = appearance.theme().active_ui_text_color().into_solid(); let buttons = vec![InlineBannerTextButton { - text: "Learn More".to_owned(), + text: i18n::t("common.learn_more"), text_color: active_ui_text_color, button_state: InlineBannerButtonState { on_click_event: TerminalAction::AwsCliNotInstalledBanner( @@ -67,7 +67,7 @@ pub fn render_aws_cli_not_installed_banner( }); let description_text = warpui::elements::Text::new( - "The AWS CLI is required to authenticate with your organization's AWS Bedrock. Install it to continue.", + i18n::t("terminal.inline_banner.aws_cli_required"), appearance.ui_font_family(), appearance.monospace_font_size() - 2., ) @@ -78,7 +78,7 @@ pub fn render_aws_cli_not_installed_banner( InlineBannerStyle::Recommendation, appearance, InlineBannerContent { - title: "AWS CLI Not Installed".to_string(), + title: i18n::t("terminal.inline_banner.aws_cli_not_installed.title"), content: Some(vec![description_text]), buttons, close_button: Some(close_button), diff --git a/app/src/terminal/view/inline_banner/notifications_discovery.rs b/app/src/terminal/view/inline_banner/notifications_discovery.rs index 08b90f1cb0..65480fb946 100644 --- a/app/src/terminal/view/inline_banner/notifications_discovery.rs +++ b/app/src/terminal/view/inline_banner/notifications_discovery.rs @@ -45,7 +45,7 @@ pub fn render_inline_notifications_discovery_banner( let active_ui_text_color = appearance.theme().active_ui_text_color().into_solid(); let learn_more_button = InlineBannerTextButton { - text: "Learn more".to_string(), + text: i18n::t("common.learn_more"), text_color: active_ui_text_color, button_state: InlineBannerButtonState { on_click_event: TerminalAction::NotificationsDiscoveryBanner( @@ -58,7 +58,7 @@ pub fn render_inline_notifications_discovery_banner( variant: InlineBannerTextButtonVariant::Secondary, }; let troubleshoot_button = InlineBannerTextButton { - text: "Troubleshoot".to_string(), + text: i18n::t("common.troubleshoot"), text_color: active_ui_text_color, button_state: InlineBannerButtonState { on_click_event: TerminalAction::NotificationsDiscoveryBanner( @@ -73,11 +73,11 @@ pub fn render_inline_notifications_discovery_banner( let (title, buttons) = match notifications_mode { NotificationsMode::Dismissed => ( - "We won't show this banner again, but you can always go to Settings to enable notifications.", + i18n::t("terminal.inline_banner.notifications.dismissed"), vec![], ), NotificationsMode::Disabled => ( - "Notifications were turned off, but you can always go to Settings to enable notifications.", + i18n::t("terminal.inline_banner.notifications.disabled"), vec![], ), NotificationsMode::Unset => ( @@ -85,7 +85,7 @@ pub fn render_inline_notifications_discovery_banner( vec![ learn_more_button, InlineBannerTextButton { - text: "Enable".to_string(), + text: i18n::t("common.enable"), text_color: active_ui_text_color, button_state: InlineBannerButtonState { on_click_event: TerminalAction::NotificationsDiscoveryBanner( @@ -105,20 +105,20 @@ pub fn render_inline_notifications_discovery_banner( let (title, docs_button) = match request_outcome { Some(request_outcome) => match request_outcome { RequestPermissionsOutcome::Accepted => ( - "Success! You are now ready to receive desktop notifications.", + i18n::t("terminal.inline_banner.notifications.success"), learn_more_button, ), RequestPermissionsOutcome::PermissionsDenied => ( - "Warp was denied permissions to send you notifications.", + i18n::t("terminal.inline_banner.notifications.permissions_denied"), troubleshoot_button, ), RequestPermissionsOutcome::OtherError { .. } => ( - "Something went wrong while requesting permissions.", + i18n::t("terminal.inline_banner.notifications.permissions_error"), troubleshoot_button, ), }, None => ( - "Don't forget to 'Allow' the permissions request to finish setting up notifications.", + i18n::t("terminal.inline_banner.notifications.allow_permissions"), learn_more_button, ), }; @@ -128,7 +128,7 @@ pub fn render_inline_notifications_discovery_banner( vec![ docs_button, InlineBannerTextButton { - text: "Configure notifications".to_string(), + text: i18n::t("terminal.inline_banner.notifications.configure"), text_color: active_ui_text_color, button_state: InlineBannerButtonState { on_click_event: TerminalAction::NotificationsDiscoveryBanner( @@ -156,7 +156,7 @@ pub fn render_inline_notifications_discovery_banner( InlineBannerStyle::CallToAction, appearance, InlineBannerContent { - title: title.to_owned(), + title, buttons, close_button: Some(close_button), ..Default::default() diff --git a/app/src/terminal/view/inline_banner/notifications_error.rs b/app/src/terminal/view/inline_banner/notifications_error.rs index 888053984e..65979fd59a 100644 --- a/app/src/terminal/view/inline_banner/notifications_error.rs +++ b/app/src/terminal/view/inline_banner/notifications_error.rs @@ -43,7 +43,7 @@ pub fn render_inline_notifications_error_banner( // If permissions haven't been granted or denied, add a button to set the permissions. if matches!(error, Some(NotificationSendError::PermissionsNotYetGranted)) { buttons.push(InlineBannerTextButton { - text: "Set permissions".to_string(), + text: i18n::t("terminal.inline_banner.notifications.set_permissions"), text_color: active_ui_text_color, button_state: InlineBannerButtonState { on_click_event: TerminalAction::NotificationsErrorBanner( @@ -58,7 +58,7 @@ pub fn render_inline_notifications_error_banner( } buttons.push(InlineBannerTextButton { - text: "Troubleshoot".to_string(), + text: i18n::t("common.troubleshoot"), text_color: active_ui_text_color, button_state: InlineBannerButtonState { on_click_event: TerminalAction::NotificationsErrorBanner( diff --git a/app/src/terminal/view/inline_banner/open_in_warp.rs b/app/src/terminal/view/inline_banner/open_in_warp.rs index 8688a9c8cf..1237fc2e0d 100644 --- a/app/src/terminal/view/inline_banner/open_in_warp.rs +++ b/app/src/terminal/view/inline_banner/open_in_warp.rs @@ -47,9 +47,7 @@ impl OpenInWarpBannerState { /// Given an openable file, format a file-specific title for the Open in Warp banner. fn file_title_text(openable_path: &OpenablePath) -> String { match openable_path.file_type { - OpenableFileType::Markdown => { - "Did you know that Warp can directly display Markdown files?".to_string() - } + OpenableFileType::Markdown => i18n::t("terminal.open_in_warp.markdown_title"), OpenableFileType::Code | OpenableFileType::Text => { cfg_if::cfg_if! { if #[cfg(not(target_family = "wasm"))] { @@ -59,13 +57,14 @@ fn file_title_text(openable_path: &OpenablePath) -> String { match language.as_ref().map(|language| language.display_name()) { Some(display_name) => { - format!("Did you know that Warp can directly edit {display_name} files?") + i18n::t("terminal.open_in_warp.code_language_title") + .replace("{language}", display_name) } - None => "Did you know that Warp can directly edit code?".to_string(), + None => i18n::t("terminal.open_in_warp.code_title"), } } else { // The `languages` crate is not available on WASM, so use a fallback message. - "Did you know that Warp can directly edit code?".to_string() + i18n::t("terminal.open_in_warp.code_title") } } } @@ -78,12 +77,14 @@ pub fn render_open_in_warp_banner( appearance: &Appearance, ) -> Box { let button_text = match state.target.file_type { - OpenableFileType::Markdown => "View in Warp", - OpenableFileType::Code | OpenableFileType::Text => "Edit in Warp", + OpenableFileType::Markdown => i18n::t("terminal.open_in_warp.view_in_warp"), + OpenableFileType::Code | OpenableFileType::Text => { + i18n::t("terminal.open_in_warp.edit_in_warp") + } }; let open_button = InlineBannerTextButton { - text: button_text.to_string(), + text: button_text, text_color: appearance.theme().active_ui_text_color().into_solid(), button_state: InlineBannerButtonState { on_click_event: TerminalAction::OpenInWarpBanner(OpenInWarpBannerAction::OpenFile), @@ -98,7 +99,7 @@ pub fn render_open_in_warp_banner( }; let learn_more_button = InlineBannerTextButton { - text: "Learn more".to_string(), + text: i18n::t("common.learn_more"), text_color: appearance.theme().active_ui_text_color().into_solid(), button_state: InlineBannerButtonState { on_click_event: TerminalAction::OpenInWarpBanner(OpenInWarpBannerAction::LearnMore), diff --git a/app/src/terminal/view/inline_banner/prompt_suggestions.rs b/app/src/terminal/view/inline_banner/prompt_suggestions.rs index 11d94db6bb..b464bf3ea9 100644 --- a/app/src/terminal/view/inline_banner/prompt_suggestions.rs +++ b/app/src/terminal/view/inline_banner/prompt_suggestions.rs @@ -39,9 +39,6 @@ use crate::util::bindings::keybinding_name_to_keystroke; const INLINE_BANNER_SPACING: f32 = 8.; const INLINE_BANNER_BUTTON_PADDING: f32 = 8.; -const DELINQUENT_DUE_TO_PAYMENT_ISSUE_TOOLTIP_MESSAGE: &str = "Restricted due to payment issue"; -const OUT_OF_REQUESTS_TOOLTIP_MESSAGE: &str = "Out of credits"; - /// Types of zero-state prompt suggestions. #[derive(Debug, Copy, Clone, Serialize)] pub enum ZeroStatePromptSuggestionType { @@ -66,20 +63,16 @@ impl ZeroStatePromptSuggestionType { /// Constant for the number of zero-state prompt suggestion types. pub const COUNT: usize = 5; - pub fn query(&self) -> &'static str { + pub fn query(&self) -> String { match self { - Self::Explain => "Explain this to me.", - Self::Fix => "Help me fix this.", - Self::Install => { - "Help me install a binary/dependency. What information do I need to provide to you to do this?" - } - Self::Code => { - "Help me write some code. What information do I need to provide to you to do this?" - } - Self::Deploy => { - "Help me deploy my project. What information do I need to provide to you to do this?" + Self::Explain => i18n::t("terminal.inline_banner.prompt_suggestions.query.explain"), + Self::Fix => i18n::t("terminal.inline_banner.prompt_suggestions.query.fix"), + Self::Install => i18n::t("terminal.inline_banner.prompt_suggestions.query.install"), + Self::Code => i18n::t("terminal.inline_banner.prompt_suggestions.query.code"), + Self::Deploy => i18n::t("terminal.inline_banner.prompt_suggestions.query.deploy"), + Self::SomethingElse => { + i18n::t("terminal.inline_banner.prompt_suggestions.query.something_else") } - Self::SomethingElse => "Something else?", } } @@ -294,16 +287,16 @@ fn get_tooltip_text_for_alert_state(alert_state: &PromptAlertState) -> Option { - Some(DELINQUENT_DUE_TO_PAYMENT_ISSUE_TOOLTIP_MESSAGE.to_string()) - } + PromptAlertState::DelinquentDueToPaymentIssue => Some(i18n::t( + "terminal.inline_banner.prompt_suggestions.payment_issue_tooltip", + )), PromptAlertState::RequestLimitReached | PromptAlertState::AnonymousUserRequestLimitHardGate | PromptAlertState::AnonymousUserRequestLimitSoftGate | PromptAlertState::OveragesToggleableButNotEnabled - | PromptAlertState::MonthlyOveragesSpendLimitReached => { - Some(OUT_OF_REQUESTS_TOOLTIP_MESSAGE.to_string()) - } + | PromptAlertState::MonthlyOveragesSpendLimitReached => Some(i18n::t( + "terminal.inline_banner.prompt_suggestions.out_of_credits_tooltip", + )), _ => None, } } diff --git a/app/src/terminal/view/inline_banner/shared_sessions.rs b/app/src/terminal/view/inline_banner/shared_sessions.rs index 96694dacb3..bb4622364c 100644 --- a/app/src/terminal/view/inline_banner/shared_sessions.rs +++ b/app/src/terminal/view/inline_banner/shared_sessions.rs @@ -31,8 +31,11 @@ fn render_inline_shared_session_banner( let today = Local::now(); let is_today = datetime.year() == today.year() && datetime.ordinal() == today.ordinal(); + let is_zh = i18n::current_locale().starts_with("zh"); let day_str = if is_today { - String::from("Today") + i18n::t("terminal.inline_banner.shared_session.today") + } else if is_zh { + format!("{}月{}日", datetime.month(), datetime.day()) } else { // Formatted as "Month Day", e.g. "October 10". datetime.format("%B %e").to_string() @@ -40,8 +43,16 @@ fn render_inline_shared_session_banner( // TODO: look into using the OS's locale to format the time according // to user's preferences. - let time_str = datetime.format("%l:%M%P").to_string(); - let datetime_str = format!("{day_str}, {time_str}"); + let time_str = if is_zh { + datetime.format("%H:%M").to_string() + } else { + datetime.format("%l:%M%P").to_string() + }; + let datetime_str = if is_zh { + format!("{day_str} {time_str}") + } else { + format!("{day_str}, {time_str}") + }; let pill = Container::new( Flex::row() @@ -99,13 +110,13 @@ pub fn render_inline_shared_session_started_banner( appearance: &Appearance, ) -> Box { let label = if is_shared_ambient_agent_session { - "Environment started" + i18n::t("terminal.inline_banner.shared_session.environment_started") } else if is_remote_control { - "Remote control active" + i18n::t("terminal.inline_banner.shared_session.remote_control_active") } else { - "Sharing started" + i18n::t("terminal.inline_banner.shared_session.sharing_started") }; - render_inline_shared_session_banner(is_active, label.to_string(), started_at, appearance) + render_inline_shared_session_banner(is_active, label, started_at, appearance) } pub fn render_inline_shared_session_ended_banner( @@ -115,11 +126,11 @@ pub fn render_inline_shared_session_ended_banner( appearance: &Appearance, ) -> Box { let label = if is_shared_ambient_agent_session { - "Environment ended" + i18n::t("terminal.inline_banner.shared_session.environment_ended") } else if is_remote_control { - "Remote control stopped" + i18n::t("terminal.inline_banner.shared_session.remote_control_stopped") } else { - "Sharing ended" + i18n::t("terminal.inline_banner.shared_session.sharing_ended") }; - render_inline_shared_session_banner(false, label.to_string(), ended_at, appearance) + render_inline_shared_session_banner(false, label, ended_at, appearance) } diff --git a/app/src/terminal/view/inline_banner/shell_process_terminated.rs b/app/src/terminal/view/inline_banner/shell_process_terminated.rs index f6c15b1ed0..788e261d90 100644 --- a/app/src/terminal/view/inline_banner/shell_process_terminated.rs +++ b/app/src/terminal/view/inline_banner/shell_process_terminated.rs @@ -15,14 +15,14 @@ pub fn render_shell_process_terminated_banner( InlineBannerStyle::CallToAction, appearance, InlineBannerContent { - title: "Shell process exited prematurely!".to_string(), + title: i18n::t("terminal.inline_banner.shell_process_exited_prematurely"), header_icon: Some(InlineBannerIcon { asset_path: "bundled/svg/warning.svg", aspect_ratio: 1., color_override: Some(appearance.theme().foreground().into_solid()), }), content: Some(vec![Text::new( - "The output from Warp's initialization script is visible above to assist with debugging.", + i18n::t("terminal.inline_banner.init_script_output_visible"), appearance.ui_font_family(), appearance.ui_font_size(), )]), @@ -34,7 +34,7 @@ pub fn render_shell_process_terminated_banner( InlineBannerStyle::LowPriority, appearance, InlineBannerContent { - title: "Shell process exited".to_string(), + title: i18n::t("terminal.inline_banner.shell_process_exited"), header_icon: Some(InlineBannerIcon { asset_path: "bundled/svg/info.svg", aspect_ratio: 1., diff --git a/app/src/terminal/view/inline_banner/ssh.rs b/app/src/terminal/view/inline_banner/ssh.rs index e93af8a068..a084e79f80 100644 --- a/app/src/terminal/view/inline_banner/ssh.rs +++ b/app/src/terminal/view/inline_banner/ssh.rs @@ -39,17 +39,17 @@ pub fn render_inline_ssh_wrapper_banner( let (style, title) = if state.wrapper_enabled { ( InlineBannerStyle::LowPriority, - "Warp SSH wrapper enabled".to_string(), + i18n::t("terminal.inline_banner.ssh_wrapper.enabled"), ) } else { ( InlineBannerStyle::VeryLowPriority, - "Warp SSH wrapper disabled".to_string(), + i18n::t("terminal.inline_banner.ssh_wrapper.disabled"), ) }; let buttons = vec![ InlineBannerTextButton { - text: "Learn more".to_string(), + text: i18n::t("common.learn_more"), text_color: label_text_color, button_state: InlineBannerButtonState { on_click_event: TerminalAction::LegacySSHBanner(SSHBannerAction::LearnMore), @@ -60,7 +60,7 @@ pub fn render_inline_ssh_wrapper_banner( variant: InlineBannerTextButtonVariant::Secondary, }, InlineBannerTextButton { - text: "Settings".to_string(), + text: i18n::t("common.settings"), text_color: label_text_color, button_state: InlineBannerButtonState { on_click_event: TerminalAction::LegacySSHBanner(SSHBannerAction::Settings), diff --git a/app/src/terminal/view/inline_banner/vim_mode.rs b/app/src/terminal/view/inline_banner/vim_mode.rs index 172574dc08..b3d70729de 100644 --- a/app/src/terminal/view/inline_banner/vim_mode.rs +++ b/app/src/terminal/view/inline_banner/vim_mode.rs @@ -27,7 +27,7 @@ pub fn render_vim_mode_banner( let active_ui_text_color = appearance.theme().active_ui_text_color(); let buttons = vec![InlineBannerTextButton { - text: "Enable".to_owned(), + text: i18n::t("common.enable"), text_color: active_ui_text_color.into_solid(), button_state: InlineBannerButtonState { on_click_event: TerminalAction::VimModeBanner(VimModeBannerAction::Enable), @@ -47,7 +47,7 @@ pub fn render_vim_mode_banner( InlineBannerStyle::LowPriority, appearance, InlineBannerContent { - title: "Enable Warp's Vim keybindings?".to_string(), + title: i18n::t("terminal.inline_banner.vim_mode.title"), buttons, close_button: Some(close_button), ..Default::default() diff --git a/app/src/terminal/view/link_detection.rs b/app/src/terminal/view/link_detection.rs index 9d3f1401d7..aa2884189b 100644 --- a/app/src/terminal/view/link_detection.rs +++ b/app/src/terminal/view/link_detection.rs @@ -54,7 +54,7 @@ impl GridHighlightedLink { } } - pub fn tooltip_text(&self) -> &'static str { + pub fn tooltip_text(&self) -> String { match &self { #[cfg(feature = "local_fs")] GridHighlightedLink::File(file_link) @@ -64,11 +64,11 @@ impl GridHighlightedLink { .map(|path| path.is_dir()) .unwrap_or(false) => { - "Open folder" + i18n::t("terminal.link_detection.open_folder") } #[cfg(feature = "local_fs")] - GridHighlightedLink::File(_) => "Open file", - GridHighlightedLink::Url(_) => "Open link", + GridHighlightedLink::File(_) => i18n::t("terminal.link_detection.open_file"), + GridHighlightedLink::Url(_) => i18n::t("terminal.link_detection.open_link"), } } } @@ -147,15 +147,15 @@ pub enum RichContentLink { } impl RichContentLink { - pub fn tooltip_text(&self) -> &'static str { + pub fn tooltip_text(&self) -> String { match &self { #[cfg(feature = "local_fs")] RichContentLink::FilePath { absolute_path, .. } if absolute_path.is_dir() => { - "Open folder" + i18n::t("terminal.link_detection.open_folder") } #[cfg(feature = "local_fs")] - RichContentLink::FilePath { .. } => "Open file", - RichContentLink::Url(_) => "Open link", + RichContentLink::FilePath { .. } => i18n::t("terminal.link_detection.open_file"), + RichContentLink::Url(_) => i18n::t("terminal.link_detection.open_link"), } } } diff --git a/app/src/terminal/view/load_ai_conversation.rs b/app/src/terminal/view/load_ai_conversation.rs index 77c6079517..104fcad07c 100644 --- a/app/src/terminal/view/load_ai_conversation.rs +++ b/app/src/terminal/view/load_ai_conversation.rs @@ -21,7 +21,7 @@ use crate::ai::agent::{ AIAgentOutput, AIAgentOutputMessage, AIAgentOutputMessageType, CreateDocumentsRequest, CreateDocumentsResult, EditDocumentsResult, }; -use crate::ai::ai_document_view::DEFAULT_PLANNING_DOCUMENT_TITLE; +use crate::ai::ai_document_view::default_planning_document_title; use crate::ai::blocklist::agent_view::{ AgentViewEntryBlockParams, AgentViewEntryOrigin, DismissalStrategy, EphemeralMessage, }; @@ -370,9 +370,9 @@ impl TerminalView { let title = document_titles .get(index) .cloned() - .unwrap_or_else(|| { - DEFAULT_PLANNING_DOCUMENT_TITLE.to_string() - }); + .unwrap_or_else( + default_planning_document_title, + ); doc_model.restore_document( doc_context.document_id, diff --git a/app/src/terminal/view/open_in_warp.rs b/app/src/terminal/view/open_in_warp.rs index 94b3e28723..7d0b7949b6 100644 --- a/app/src/terminal/view/open_in_warp.rs +++ b/app/src/terminal/view/open_in_warp.rs @@ -224,7 +224,8 @@ impl TerminalView { match &self.inline_banners_state.open_in_warp_banner { Some(banner_state) => { ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( - format!("Open {} in Warp", banner_state.target.path.display()), + i18n::t("terminal.open_in_warp.a11y.open") + .replace("{path}", &banner_state.target.path.display().to_string()), WarpA11yRole::UserAction, )) } @@ -233,14 +234,14 @@ impl TerminalView { } OpenInWarpBannerAction::Close => { ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( - "Close View in Warp banner", + i18n::t("terminal.open_in_warp.a11y.close_banner"), WarpA11yRole::UserAction, )) } OpenInWarpBannerAction::LearnMore => { ActionAccessibilityContent::Custom(AccessibilityContent::new( - "Learn more", - "Learn more about opening Markdown files in Warp", + i18n::t("common.learn_more"), + i18n::t("terminal.open_in_warp.a11y.learn_more_help"), WarpA11yRole::UserAction, )) } diff --git a/app/src/terminal/view/pane_impl.rs b/app/src/terminal/view/pane_impl.rs index 0aa8ae4067..55ce54a711 100644 --- a/app/src/terminal/view/pane_impl.rs +++ b/app/src/terminal/view/pane_impl.rs @@ -676,7 +676,7 @@ impl BackingView for TerminalView { if shared_session_status.is_sharer_or_viewer() { if !is_ambient_agent { items.push( - MenuItemFields::new("Copy link") + MenuItemFields::new(i18n::t("common.copy_link")) .with_on_select_action(TerminalAction::CopySharedSessionLink { source }) .into_item(), ); @@ -684,7 +684,7 @@ impl BackingView for TerminalView { if shared_session_status.is_sharer() { items.push( - MenuItemFields::new("Stop sharing session") + MenuItemFields::new(i18n::t("terminal.shared_session.stop_sharing_session")) .with_on_select_action(TerminalAction::StopSharingCurrentSession { source }) .into_item(), ); @@ -696,7 +696,7 @@ impl BackingView for TerminalView { == UserAppInstallStatus::Detected { items.push( - MenuItemFields::new("Open on Desktop") + MenuItemFields::new(i18n::t("common.open_on_desktop")) .with_on_select_action(TerminalAction::OpenSharedSessionOnDesktop { source, }) @@ -707,7 +707,7 @@ impl BackingView for TerminalView { && ContextFlag::CreateSharedSession.is_enabled() { items.push( - MenuItemFields::new("Share session") + MenuItemFields::new(i18n::t("terminal.shared_session.share_session")) .with_on_select_action(TerminalAction::OpenShareSessionModal { source }) .into_item(), ); @@ -786,7 +786,12 @@ impl TerminalView { self.ambient_agent_cancel_mouse_state.clone(), blended_colors::text_sub(theme, theme.background()).into(), ) - .with_tooltip(move || ui_builder.tool_tip("Cancel".to_string()).build().finish()) + .with_tooltip(move || { + ui_builder + .tool_tip(i18n::t("common.cancel")) + .build() + .finish() + }) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action::>( @@ -830,14 +835,11 @@ impl TerminalView { button .with_tooltip(move || { let tooltip_text = if is_open { - "Hide details" + i18n::t("terminal.conversation_details.hide") } else { - "Show details" + i18n::t("terminal.conversation_details.show") }; - ui_builder - .tool_tip(tooltip_text.to_string()) - .build() - .finish() + ui_builder.tool_tip(tooltip_text).build().finish() }) .build() .on_click(|ctx, _, _| { @@ -1084,8 +1086,8 @@ impl TerminalView { fn default_agent_conversation_title(is_ambient_agent: bool) -> String { if is_ambient_agent { - "New cloud agent".to_owned() + i18n::t("terminal.ambient_agent.default_cloud_agent_title") } else { - "New agent conversation".to_owned() + i18n::t("terminal.agent_conversation.default_title") } } diff --git a/app/src/terminal/view/plugin_instructions_block.rs b/app/src/terminal/view/plugin_instructions_block.rs index 99df15bac0..b40d2e5db4 100644 --- a/app/src/terminal/view/plugin_instructions_block.rs +++ b/app/src/terminal/view/plugin_instructions_block.rs @@ -97,11 +97,15 @@ impl PluginInstructionsBlock { let theme = appearance.theme(); let step_number = render_step_number(index + 1, appearance); + let description = i18n::t(description); let desc_element: Box = if let Some(url) = link { let fragments = vec![ FormattedTextFragment::plain_text(format!("{description} ")), - FormattedTextFragment::hyperlink("Learn more", url), + FormattedTextFragment::hyperlink( + i18n::t("terminal.plugin_instructions.learn_more"), + url, + ), ]; let formatted = FormattedText::new(vec![FormattedTextLine::Line(fragments)]); FormattedTextElement::new( @@ -120,7 +124,7 @@ impl PluginInstructionsBlock { }) .finish() } else { - Text::new(description.to_owned(), appearance.ui_font_family(), 14.) + Text::new(description, appearance.ui_font_family(), 14.) .with_color(theme.nonactive_ui_text_color().into_solid()) .finish() }; @@ -187,7 +191,7 @@ impl View for PluginInstructionsBlock { let theme = appearance.theme(); let title = Text::new( - self.instructions.title.to_owned(), + i18n::t(self.instructions.title), appearance.ui_font_family(), 20., ) @@ -197,11 +201,12 @@ impl View for PluginInstructionsBlock { let subtitle_text = if self.is_remote_session { format!( - "{} Be sure to run these commands on your remote machine.", - self.instructions.subtitle + "{} {}", + i18n::t(self.instructions.subtitle), + i18n::t("terminal.plugin_instructions.remote_suffix") ) } else { - self.instructions.subtitle.to_owned() + i18n::t(self.instructions.subtitle) }; let subtitle = Text::new(subtitle_text, appearance.ui_font_family(), 14.) @@ -238,7 +243,7 @@ impl View for PluginInstructionsBlock { } for note in self.instructions.post_install_notes { - let post_note = Text::new((*note).to_owned(), appearance.ui_font_family(), 14.) + let post_note = Text::new(i18n::t(note), appearance.ui_font_family(), 14.) .with_color(theme.nonactive_ui_text_color().into_solid()) .finish(); content.add_child(post_note); @@ -293,7 +298,7 @@ impl TypedActionView for PluginInstructionsBlock { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::success("Copied to clipboard".to_owned()), + DismissibleToast::success(i18n::t("common.copied_to_clipboard")), window_id, ctx, ); diff --git a/app/src/terminal/view/queued_prompts_panel.rs b/app/src/terminal/view/queued_prompts_panel.rs index ce5c35aaec..b45299538b 100644 --- a/app/src/terminal/view/queued_prompts_panel.rs +++ b/app/src/terminal/view/queued_prompts_panel.rs @@ -57,7 +57,7 @@ fn build_row_state( let edit_button = ctx.add_typed_action_view(move |_| { ActionButton::new("", NakedTheme) .with_icon(TerminalIcon::Pencil) - .with_tooltip("Edit queued prompt") + .with_tooltip(i18n::t("terminal.queued_prompts.edit")) .with_size(ButtonSize::XSmall) .on_click(move |ctx| { ctx.dispatch_typed_action(QueuedPromptsPanelAction::StartEditingRow(query_id)); @@ -66,7 +66,7 @@ fn build_row_state( let delete_button = ctx.add_typed_action_view(move |_| { ActionButton::new("", NakedTheme) .with_icon(TerminalIcon::Trash) - .with_tooltip("Delete queued prompt") + .with_tooltip(i18n::t("terminal.queued_prompts.delete")) .with_size(ButtonSize::XSmall) .on_click(move |ctx| { ctx.dispatch_typed_action(QueuedPromptsPanelAction::DeleteRow(query_id)); @@ -847,5 +847,5 @@ fn render_row(props: RenderRowProps<'_>) -> Box { /// Returns the user-visible header label for `count` queued prompts. fn header_label_text(count: usize) -> String { - format!("{count} queued") + i18n::t("terminal.queued_prompts.header").replace("{count}", &count.to_string()) } diff --git a/app/src/terminal/view/shared_session/adapter.rs b/app/src/terminal/view/shared_session/adapter.rs index 3d1c830644..8756a8f9ed 100644 --- a/app/src/terminal/view/shared_session/adapter.rs +++ b/app/src/terminal/view/shared_session/adapter.rs @@ -107,7 +107,7 @@ impl Adapter { ) -> Self { let reconnecting_banner = ctx.add_typed_action_view(|_| { Banner::new_without_close(BannerTextContent::formatted_text(vec![ - FormattedTextFragment::plain_text("Offline, trying to reconnect..."), + FormattedTextFragment::plain_text(i18n::t("terminal.shared_session.reconnecting")), ])) .with_icon(Icon::CloudOffline) }); diff --git a/app/src/terminal/view/shared_session/conversation_ended_tombstone_view.rs b/app/src/terminal/view/shared_session/conversation_ended_tombstone_view.rs index 274219aa36..8be73068c2 100644 --- a/app/src/terminal/view/shared_session/conversation_ended_tombstone_view.rs +++ b/app/src/terminal/view/shared_session/conversation_ended_tombstone_view.rs @@ -190,7 +190,7 @@ impl ConversationEndedTombstoneView { }) .unwrap_or_default(); if display_data.is_error && task_id.is_none() && !display_data.conversation_is_transcript { - display_data.title = Some("Cloud agent failed to start".to_string()); + display_data.title = Some(i18n::t("terminal.shared_session.cloud_agent_failed")); display_data.credits = None; } @@ -199,8 +199,8 @@ impl ConversationEndedTombstoneView { let continue_in_cloud_button = match tombstone_cta { Some(TombstoneCta::ContinueInCloud { task_id }) => { Some(ctx.add_typed_action_view(move |_| { - ActionButton::new("Continue", PrimaryTheme) - .with_tooltip("Continue this cloud conversation") + ActionButton::new(i18n::t("common.continue"), PrimaryTheme) + .with_tooltip(i18n::t("terminal.shared_session.continue_cloud_tooltip")) .on_click(move |ctx| { ctx.dispatch_typed_action( ConversationEndedTombstoneAction::ContinueInCloud { task_id }, @@ -215,8 +215,8 @@ impl ConversationEndedTombstoneView { let continue_locally_button = match tombstone_cta { Some(TombstoneCta::ContinueLocally { conversation_id }) => { Some(ctx.add_typed_action_view(move |_| { - ActionButton::new("Continue locally", PrimaryTheme) - .with_tooltip("Fork this conversation locally") + ActionButton::new(i18n::t("ai.conversation.continue_locally"), PrimaryTheme) + .with_tooltip(i18n::t("ai.conversation.continue_locally_tooltip")) .on_click(move |ctx| { ctx.dispatch_typed_action( ConversationEndedTombstoneAction::ContinueLocally(conversation_id), @@ -236,13 +236,16 @@ impl ConversationEndedTombstoneView { } else { conversation_id.map(|conv_id| { ctx.add_typed_action_view(move |_| { - ActionButton::new("Open in Warp", PrimaryTheme) - .with_tooltip("Open this conversation in the Warp desktop app") - .on_click(move |ctx| { - ctx.dispatch_typed_action( - ConversationEndedTombstoneAction::OpenInWarp(conv_id), - ); - }) + ActionButton::new( + i18n::t("terminal.shared_session.open_in_warp"), + PrimaryTheme, + ) + .with_tooltip(i18n::t("terminal.shared_session.open_in_warp_tooltip")) + .on_click(move |ctx| { + ctx.dispatch_typed_action( + ConversationEndedTombstoneAction::OpenInWarp(conv_id), + ); + }) }) }) }; @@ -344,7 +347,7 @@ impl ConversationEndedTombstoneView { if is_transcript { return Text::new( - "You're viewing a snapshot", + i18n::t("terminal.shared_session.snapshot_title"), appearance.overline_font_family(), appearance.monospace_font_size(), ) @@ -376,7 +379,7 @@ impl ConversationEndedTombstoneView { .display_data .title .clone() - .unwrap_or_else(|| "Agent task".to_string()); + .unwrap_or_else(|| i18n::t("terminal.shared_session.agent_task")); Flex::row() .with_main_axis_size(MainAxisSize::Min) .with_cross_axis_alignment(CrossAxisAlignment::Center) @@ -403,8 +406,7 @@ impl ConversationEndedTombstoneView { let theme = appearance.theme(); Container::new( Text::new( - "This shared conversation shows the state when you opened it. \ - If the agent is still running, refresh to see the latest progress.", + i18n::t("terminal.shared_session.snapshot_subtitle"), appearance.overline_font_family(), appearance.monospace_font_size(), ) @@ -422,23 +424,32 @@ impl ConversationEndedTombstoneView { if let Some(dir) = &self.display_data.working_directory { let display_dir = home_relative_path(Path::new(dir)); - parts.push(format!("Directory: {display_dir}")); + parts.push( + i18n::t("terminal.shared_session.metadata.directory") + .replace("{value}", &display_dir), + ); } if let Some(source) = &self.display_data.source { - parts.push(format!("Source: {source}")); + parts.push( + i18n::t("terminal.shared_session.metadata.source").replace("{value}", source), + ); } if let Some(skill) = &self.display_data.skill_name { - parts.push(format!("Skill: {skill}")); + parts.push(i18n::t("terminal.shared_session.metadata.skill").replace("{value}", skill)); } if let Some(run_time) = &self.display_data.run_time { - parts.push(format!("Run time: {run_time}")); + parts.push( + i18n::t("terminal.shared_session.metadata.run_time").replace("{value}", run_time), + ); } if let Some(credits) = &self.display_data.credits { - parts.push(format!("Credits used: {credits}")); + parts.push( + i18n::t("terminal.shared_session.metadata.credits").replace("{value}", credits), + ); } if parts.is_empty() { diff --git a/app/src/terminal/view/shared_session/sharer/inactivity_modal.rs b/app/src/terminal/view/shared_session/sharer/inactivity_modal.rs index 5935bea526..ffc2b15def 100644 --- a/app/src/terminal/view/shared_session/sharer/inactivity_modal.rs +++ b/app/src/terminal/view/shared_session/sharer/inactivity_modal.rs @@ -148,11 +148,13 @@ impl InactivityModalBody { } fn render_countdown(&self, appearance: &Appearance) -> Box { - let text = format!( - "Sharing will end in {}:{:02} due to inactivity.", + let time = format!( + "{}:{:02}", self.duration.as_secs() / 60, - self.duration.as_secs() % 60, + self.duration.as_secs() % 60 ); + let text = i18n::t("terminal.shared_session.sharing_will_end_due_to_inactivity") + .replace("{time}", &time); Container::new( Text::new_inline(text, appearance.ui_font_family(), TEXT_FONT_SIZE) @@ -179,7 +181,7 @@ impl InactivityModalBody { font_weight: Some(Weight::Bold), ..Default::default() }) - .with_centered_text_label(String::from("Stop sharing")) + .with_centered_text_label(i18n::t("terminal.shared_session.stop_sharing")) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { @@ -205,7 +207,7 @@ impl InactivityModalBody { font_weight: Some(Weight::Bold), ..Default::default() }) - .with_centered_text_label(String::from("Continue sharing")) + .with_centered_text_label(i18n::t("terminal.shared_session.continue_sharing")) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { @@ -232,7 +234,7 @@ impl View for InactivityModalBody { let header = Container::new( Text::new_inline( - "Are you still there?", + i18n::t("terminal.shared_session.are_you_still_there"), appearance.ui_font_family(), HEADER_FONT_SIZE, ) diff --git a/app/src/terminal/view/shared_session/view_impl.rs b/app/src/terminal/view/shared_session/view_impl.rs index 672dc440df..4337330b57 100644 --- a/app/src/terminal/view/shared_session/view_impl.rs +++ b/app/src/terminal/view/shared_session/view_impl.rs @@ -58,7 +58,7 @@ use crate::terminal::shared_session::role_change_modal::{ use crate::terminal::shared_session::settings::SharedSessionSettings; use crate::terminal::shared_session::{ join_link, SharedSessionActionSource, SharedSessionScrollbackType, SharedSessionSource, - SharedSessionStatus, COPY_LINK_TEXT, + SharedSessionStatus, }; use crate::terminal::view::{ ContextMenuAction, Event, InlineBannerItem, InlineBannerType, PendingUserQueryKind, @@ -942,12 +942,12 @@ impl TerminalView { } let Some(ambient_agent_view_model) = self.ambient_agent_view_model.as_ref() else { - self.show_error_toast("Couldn't continue this cloud task.".to_string(), ctx); + self.show_error_toast(i18n::t("terminal.toast.couldnt_continue_cloud_task"), ctx); return; }; if ambient_agent_view_model.as_ref(ctx).task_id() != Some(task_id) { - self.show_error_toast("Couldn't continue this cloud task.".to_string(), ctx); + self.show_error_toast(i18n::t("terminal.toast.couldnt_continue_cloud_task"), ctx); return; } self.enable_cloud_followup_input_after_conversation_end(task_id, ctx); @@ -982,7 +982,7 @@ impl TerminalView { ctx, ); self.show_persistent_toast( - "Sharing ended due to inactivity".to_owned(), + i18n::t("terminal.shared_session.ended_due_to_inactivity"), ToastFlavor::Error, ctx, ); @@ -1033,7 +1033,7 @@ impl TerminalView { ctx, ); self.show_persistent_toast( - "Shared editing permissions were revoked due to inactivity".to_owned(), + i18n::t("terminal.shared_session.permissions_revoked_due_to_inactivity"), ToastFlavor::Error, ctx, ); @@ -1552,7 +1552,8 @@ impl TerminalView { let window_id = ctx.window_id(); crate::workspace::ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::default(COPY_LINK_TEXT.to_string()); + let toast = + DismissibleToast::default(i18n::t("terminal.shared_session.sharing_link_copied")); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); @@ -1656,7 +1657,7 @@ impl TerminalView { && matches!(reason, RoleUpdatedReason::InactivityLimitReached) { self.show_persistent_toast( - "Editing permissions were revoked because the sharer is idle".to_owned(), + i18n::t("terminal.shared_session.permissions_revoked_sharer_idle"), ToastFlavor::Error, ctx, ); @@ -1895,7 +1896,7 @@ impl TerminalView { if !model.shared_session_status().is_sharer_or_viewer() { items.push( - MenuItemFields::new("Share session...") + MenuItemFields::new(i18n::t("terminal.shared_session.share_session_ellipsis")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::OpenShareSessionModal, )) @@ -1904,7 +1905,7 @@ impl TerminalView { ); } else if model.shared_session_status().is_active_sharer() { items.push( - MenuItemFields::new("Stop sharing") + MenuItemFields::new(i18n::t("terminal.shared_session.stop_sharing")) .with_on_select_action(TerminalAction::ContextMenu( ContextMenuAction::StopSharing, )) @@ -1914,7 +1915,7 @@ impl TerminalView { if model.shared_session_status().is_sharer_or_viewer() { items.push( - MenuItemFields::new("Copy session sharing link") + MenuItemFields::new(i18n::t("terminal.shared_session.copy_session_sharing_link")) .with_on_select_action(TerminalAction::CopySharedSessionLink { source: SharedSessionActionSource::RightClickMenu, }) @@ -2028,7 +2029,7 @@ impl TerminalView { appearance .ui_builder() .button(ButtonVariant::Basic, button_handle) - .with_text_label("Request edit access".into()) + .with_text_label(i18n::t("terminal.shared_session.request_edit_access")) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(TerminalAction::RequestSharedSessionRole(Role::Executor)); diff --git a/app/src/terminal/view/shared_session/viewer.rs b/app/src/terminal/view/shared_session/viewer.rs index bc398dcf8d..6b0143b8b9 100644 --- a/app/src/terminal/view/shared_session/viewer.rs +++ b/app/src/terminal/view/shared_session/viewer.rs @@ -61,11 +61,11 @@ impl Viewer { match current_role { Role::Reader => items.extend([ // TODO: this should still dispatch an action that eventually no-ops - MenuItemFields::new("View") + MenuItemFields::new(i18n::t("common.view")) .with_icon(Icon::Check) .with_disabled(is_reconnecting) .into_item(), - MenuItemFields::new("Edit") + MenuItemFields::new(i18n::t("common.edit")) .with_indent() .with_disabled(is_reconnecting) .with_on_select_action( @@ -76,12 +76,12 @@ impl Viewer { .into_item(), ]), Role::Executor | Role::Full => items.extend([ - MenuItemFields::new("View") + MenuItemFields::new(i18n::t("common.view")) .with_indent() .with_disabled(true) .into_item(), // TODO: this should still dispatch an action that eventually no-ops - MenuItemFields::new("Edit") + MenuItemFields::new(i18n::t("common.edit")) .with_icon(Icon::Check) .with_disabled(is_reconnecting) .into_item(), diff --git a/app/src/terminal/view/shell_terminated_banner.rs b/app/src/terminal/view/shell_terminated_banner.rs index a8ee7fed7e..3941eb66c3 100644 --- a/app/src/terminal/view/shell_terminated_banner.rs +++ b/app/src/terminal/view/shell_terminated_banner.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::cell::RefCell; use warp_core::ui::appearance::Appearance; @@ -15,9 +14,6 @@ use warpui::{Entity, SingletonEntity as _, TypedActionView, View, ViewContext}; use crate::terminal::model::terminal_model::ExitReason; use crate::ui_components; -const FILE_ISSUE_TEXT: &str = "File issue"; -const MORE_INFO_TEXT: &str = "More info"; - /// A banner to display when the shell process terminates. /// /// This can be a simple informational banner or one giving information about @@ -165,9 +161,13 @@ impl TerminationType { fn text(&self, appearance: &Appearance) -> Box { let text = match self { - TerminationType::Normal => "Shell process exited", - TerminationType::PtySpawnFailure { .. } => "Shell process could not start!", - TerminationType::Premature { .. } => "Shell process exited prematurely!", + TerminationType::Normal => i18n::t("terminal.shell.process_exited"), + TerminationType::PtySpawnFailure { .. } => { + i18n::t("terminal.shell.process_could_not_start") + } + TerminationType::Premature { .. } => { + i18n::t("terminal.shell.process_exited_prematurely") + } }; Text::new(text, appearance.ui_font_family(), 14.) @@ -177,17 +177,15 @@ impl TerminationType { } fn subtext(&self, appearance: &Appearance) -> Option> { - let text: Cow = match self { + let text = match self { TerminationType::Normal => return None, TerminationType::PtySpawnFailure { pty_spawn_error } => { - format!("{pty_spawn_error:#}").into() + format!("{pty_spawn_error:#}") + } + TerminationType::Premature { shell_detail, .. } => { + i18n::t("terminal.shell.warpify_failed_subtext") + .replace("{shell_detail}", shell_detail) } - TerminationType::Premature { shell_detail, .. } => format!( - "Something went wrong while starting {shell_detail} and Warpifying it, causing the \ - process to terminate. Warpify script output is displayed here, which may point at \ - a cause." - ) - .into(), }; let text = Text::new(text, appearance.ui_font_family(), 12.) @@ -212,7 +210,7 @@ impl TerminationType { vec![ ui_builder .button(ButtonVariant::Text, handles[0].clone()) - .with_text_label(FILE_ISSUE_TEXT.to_string()) + .with_text_label(i18n::t("terminal.shell.file_issue")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(Action::OpenUrl( @@ -222,7 +220,7 @@ impl TerminationType { .finish(), ui_builder .button(ButtonVariant::Outlined, handles[1].clone()) - .with_text_label(MORE_INFO_TEXT.to_string()) + .with_text_label(i18n::t("terminal.shell.more_info")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(Action::OpenUrl( @@ -240,7 +238,7 @@ impl TerminationType { vec![ ui_builder .button(ButtonVariant::Text, handles[0].clone()) - .with_text_label("Copy error".to_string()) + .with_text_label(i18n::t("terminal.shell.copy_error")) .build() .on_click(move |evt_ctx, _ctx, _position| { evt_ctx.dispatch_typed_action(Action::CopyPtySpawnError( @@ -250,7 +248,7 @@ impl TerminationType { .finish(), ui_builder .button(ButtonVariant::Text, handles[1].clone()) - .with_text_label(FILE_ISSUE_TEXT.to_string()) + .with_text_label(i18n::t("terminal.shell.file_issue")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(Action::OpenUrl( @@ -260,7 +258,7 @@ impl TerminationType { .finish(), ui_builder .button(ButtonVariant::Outlined, handles[2].clone()) - .with_text_label(MORE_INFO_TEXT.to_string()) + .with_text_label(i18n::t("terminal.shell.more_info")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(Action::OpenUrl( diff --git a/app/src/terminal/view/ssh_file_upload.rs b/app/src/terminal/view/ssh_file_upload.rs index 51a4a90328..d2691bacc7 100644 --- a/app/src/terminal/view/ssh_file_upload.rs +++ b/app/src/terminal/view/ssh_file_upload.rs @@ -302,7 +302,7 @@ impl FileUpload { if let FileUploadStatus::AwaitingPassword = file.status { session_action_row.add_child( FormattedTextElement::from_str( - String::from("Waiting for password input"), + i18n::t("terminal.ssh.upload.waiting_for_password"), font_family, font_size, ) @@ -365,9 +365,15 @@ impl FileUpload { /// assembly. fn render_file_detail_text(&self, file: &FileUploadInfo) -> FormattedText { let status_string = match file.status { - FileUploadStatus::Started | FileUploadStatus::AwaitingPassword => "Uploading", - FileUploadStatus::Completed { successful: true } => "Uploaded", - FileUploadStatus::Completed { successful: false } => "Failed to upload", + FileUploadStatus::Started | FileUploadStatus::AwaitingPassword => { + i18n::t("terminal.ssh.upload.uploading") + } + FileUploadStatus::Completed { successful: true } => { + i18n::t("terminal.ssh.upload.uploaded") + } + FileUploadStatus::Completed { successful: false } => { + i18n::t("terminal.ssh.upload.failed") + } }; let mut file_iter = file.local_file_paths.iter().peekable(); @@ -392,7 +398,7 @@ impl FileUpload { } let mut dest_fragments = vec![ - FormattedTextFragment::plain_text(" to "), + FormattedTextFragment::plain_text(i18n::t("terminal.ssh.to_separator")), FormattedTextFragment::inline_code(&file.remote_host), ]; if let Some(remote_path) = &file.remote_dest_path { @@ -416,7 +422,12 @@ impl FileUpload { let ui_builder = appearance.ui_builder().clone(); Container::new( icon_button(appearance, Icon::X, true, file.clear_button.clone()) - .with_tooltip(move || ui_builder.tool_tip("Clear upload".into()).build().finish()) + .with_tooltip(move || { + ui_builder + .tool_tip(i18n::t("terminal.ssh.clear_upload")) + .build() + .finish() + }) .build() .on_click(move |event_ctx, _, _| { event_ctx @@ -460,7 +471,9 @@ impl FileUpload { FormattedTextElement::new( FormattedText::new(vec![FormattedTextLine::Heading(FormattedTextHeader { heading_size: 3, - text: vec![FormattedTextFragment::plain_text("File Uploads")], + text: vec![FormattedTextFragment::plain_text(i18n::t( + "terminal.ssh.file_uploads", + ))], })]), appearance.ui_font_size(), appearance.ui_font_family(), diff --git a/app/src/terminal/view/ssh_remote_server_choice_view.rs b/app/src/terminal/view/ssh_remote_server_choice_view.rs index 623564d205..e93000e448 100644 --- a/app/src/terminal/view/ssh_remote_server_choice_view.rs +++ b/app/src/terminal/view/ssh_remote_server_choice_view.rs @@ -74,23 +74,19 @@ impl SshRemoteServerChoiceView { let buttons = ctx.add_typed_action_view(|_| { KeyboardNavigableButtons::new(vec![ rich_navigation_button( - "Install Warp's SSH extension".to_string(), - Some( - "Install Warp's extension to enable agent features like file browsing, \ - code review, and intelligent command completions in this session." - .to_string(), - ), + i18n::t("terminal.ssh_remote_choice.install_extension.title"), + Some(i18n::t( + "terminal.ssh_remote_choice.install_extension.description", + )), /* recommended */ true, MouseStateHandle::default(), SshRemoteServerChoiceViewAction::Install, ), rich_navigation_button( - "Continue without installing".to_string(), - Some( - "You'll still get a Warpified experience just without the coding \ - features." - .to_string(), - ), + i18n::t("terminal.ssh_remote_choice.continue_without_installing.title"), + Some(i18n::t( + "terminal.ssh_remote_choice.continue_without_installing.description", + )), /* recommended */ false, MouseStateHandle::default(), SshRemoteServerChoiceViewAction::Skip, @@ -120,7 +116,7 @@ impl SshRemoteServerChoiceView { // Match the Figma design: a plain title row, no icon / chevron / // action buttons. `HeaderConfig` without an `interaction_mode` set // renders exactly that. - HeaderConfig::new("Choose your experience for this remote session:", app) + HeaderConfig::new(i18n::t("terminal.ssh_remote_choice.title"), app) .with_corner_radius_override(CornerRadius::with_top(Radius::Pixels( PROMPT_BORDER_RADIUS, ))) @@ -155,9 +151,13 @@ impl SshRemoteServerChoiceView { let checkbox_label = Hoverable::new(self.do_not_ask_again_label_mouse_state.clone(), move |_| { - Text::new("Don't ask me this again", ui_font_family, footer_font_size) - .with_color(muted_color) - .finish() + Text::new( + i18n::t("terminal.ssh_remote_choice.dont_ask_again"), + ui_font_family, + footer_font_size, + ) + .with_color(muted_color) + .finish() }) .with_cursor(Cursor::PointingHand) .on_click(|ctx, _, _| { @@ -175,7 +175,7 @@ impl SshRemoteServerChoiceView { let manage_settings_link = appearance .ui_builder() .link( - "Manage Warpify settings".into(), + i18n::t("terminal.ssh_remote_choice.manage_settings"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(SshRemoteServerChoiceViewAction::OpenWarpifySettings); diff --git a/app/src/terminal/view/ssh_remote_server_failed_banner.rs b/app/src/terminal/view/ssh_remote_server_failed_banner.rs index 53a7d06252..f957ac0f8b 100644 --- a/app/src/terminal/view/ssh_remote_server_failed_banner.rs +++ b/app/src/terminal/view/ssh_remote_server_failed_banner.rs @@ -15,12 +15,6 @@ use crate::terminal::model::session::SessionId; use crate::ui_components::icons::Icon; use crate::Appearance; -const BANNER_TITLE: &str = "Couldn't connect to the Warp SSH extension"; - -const BANNER_BODY: &str = - "While advanced features like file browsing and code review are currently \ - disabled, the rest of your Warpified experience is fully available."; - #[derive(Clone, Debug)] pub enum SshRemoteServerFailedBannerAction { Dismiss, @@ -79,7 +73,7 @@ impl View for SshRemoteServerFailedBanner { .finish(); let title = Text::new( - BANNER_TITLE.to_string(), + i18n::t("terminal.ssh.remote_server_failed.title"), appearance.ui_font_family(), font_size, ) @@ -87,7 +81,7 @@ impl View for SshRemoteServerFailedBanner { .finish(); let body = Text::new( - BANNER_BODY.to_string(), + i18n::t("terminal.ssh.remote_server_failed.body"), appearance.ui_font_family(), small_font_size, ) diff --git a/app/src/terminal/view/tooltips.rs b/app/src/terminal/view/tooltips.rs index ef21465f2d..07247a49ff 100644 --- a/app/src/terminal/view/tooltips.rs +++ b/app/src/terminal/view/tooltips.rs @@ -59,7 +59,7 @@ fn open_in_warp_tooltip( None }; Some(GridTooltipLink { - text: "Open in Warp".to_string(), + text: i18n::t("terminal.tooltips.open_in_warp"), action: TerminalAction::OpenCodeInWarp { path, layout: *EditorSettings::as_ref(app).open_file_layout.value(), @@ -78,11 +78,10 @@ fn show_in_file_explorer_tooltip( mouse_state: MouseStateHandle, ) -> GridTooltipLink { let text = if cfg!(target_os = "macos") { - "Show in Finder" + i18n::t("terminal.tooltips.show_in_finder") } else { - "Show containing folder" - } - .to_string(); + i18n::t("terminal.tooltips.show_containing_folder") + }; GridTooltipLink { text, action: TerminalAction::ShowInFileExplorer(path), @@ -130,7 +129,7 @@ impl TerminalView { if is_redacted { links.push(GridTooltipLink { - text: "Reveal secret".to_string(), + text: i18n::t("terminal.tooltips.reveal_secret"), action: TerminalAction::ToggleGridSecret { handle, show_secret: true, @@ -140,7 +139,7 @@ impl TerminalView { }); } else { links.push(GridTooltipLink { - text: "Hide secret".to_string(), + text: i18n::t("terminal.tooltips.hide_secret"), action: TerminalAction::ToggleGridSecret { handle, show_secret: false, @@ -152,7 +151,7 @@ impl TerminalView { } links.push(GridTooltipLink { - text: "Copy secret".to_string(), + text: i18n::t("terminal.tooltips.copy_secret"), action: TerminalAction::CopyGridSecret(handle), mouse_state: self.mouse_states.copy_secrets_tooltip.clone(), detail: None, @@ -172,7 +171,7 @@ impl TerminalView { if is_obfuscated { links.push(GridTooltipLink { - text: "Reveal secret".to_string(), + text: i18n::t("terminal.tooltips.reveal_secret"), action: TerminalAction::ToggleRichContentSecret { rich_content_tooltip_info: tooltip_info.clone(), show_secret: true, @@ -182,7 +181,7 @@ impl TerminalView { }); } else { links.push(GridTooltipLink { - text: "Hide secret".to_string(), + text: i18n::t("terminal.tooltips.hide_secret"), action: TerminalAction::ToggleRichContentSecret { rich_content_tooltip_info: tooltip_info.clone(), show_secret: false, @@ -194,7 +193,7 @@ impl TerminalView { } links.push(GridTooltipLink { - text: "Copy secret".to_string(), + text: i18n::t("terminal.tooltips.copy_secret"), action: TerminalAction::CopyRichContentSecret(tooltip_info.clone()), mouse_state: self.mouse_states.copy_secrets_tooltip.clone(), detail: None, diff --git a/app/src/terminal/view/use_agent_footer/mod.rs b/app/src/terminal/view/use_agent_footer/mod.rs index d302c851a0..ad32ba7686 100644 --- a/app/src/terminal/view/use_agent_footer/mod.rs +++ b/app/src/terminal/view/use_agent_footer/mod.rs @@ -1085,13 +1085,13 @@ impl UseAgentToolbar { let button = ctx.add_typed_action_view(|ctx| { ActionButton::new( - "Use agent", + i18n::t("terminal.use_agent_footer.use_agent"), AgentFooterButtonTheme::new(Some(terminal_model.clone())), ) .with_icon(Icon::Oz) .with_keybinding(KeystrokeSource::Fixed(USE_AGENT_KEYSTROKE.clone()), ctx) .with_size(button_size) - .with_tooltip("Ask the Warp agent to assist") + .with_tooltip(i18n::t("terminal.use_agent_footer.ask_assist")) .with_tooltip_alignment(TooltipAlignment::Left) .on_click(|ctx| { ctx.dispatch_typed_action(TerminalAction::SetInputModeAgent); @@ -1099,13 +1099,13 @@ impl UseAgentToolbar { }); let give_control_back_button = ctx.add_typed_action_view(|ctx| { ActionButton::new( - "Give control back to agent", + i18n::t("terminal.use_agent_footer.give_control_back"), AgentFooterButtonTheme::new(Some(terminal_model.clone())), ) .with_icon(Icon::Oz) .with_keybinding(KeystrokeSource::Fixed(USE_AGENT_KEYSTROKE.clone()), ctx) .with_size(button_size) - .with_tooltip("Ask the Warp agent to resume") + .with_tooltip(i18n::t("terminal.use_agent_footer.ask_resume")) .with_tooltip_alignment(TooltipAlignment::Left) .on_click(|ctx| { ctx.dispatch_typed_action(TerminalAction::SetInputModeAgent); @@ -1113,7 +1113,7 @@ impl UseAgentToolbar { }); let dismiss_button = ctx.add_typed_action_view(|_| { ActionButton::new( - "Dismiss", + i18n::t("common.dismiss"), AgentFooterButtonTheme::new(Some(terminal_model.clone())), ) .on_click(|ctx| { @@ -1123,7 +1123,7 @@ impl UseAgentToolbar { }); let dont_show_again_button = ctx.add_typed_action_view(|_| { ActionButton::new( - "Don't show again", + i18n::t("ai.block.dont_show_again"), AgentFooterButtonTheme::new(Some(terminal_model.clone())), ) .on_click(|ctx| { diff --git a/app/src/terminal/view/use_agent_footer/warpify_footer.rs b/app/src/terminal/view/use_agent_footer/warpify_footer.rs index 0e925b97f3..c654a1b9d0 100644 --- a/app/src/terminal/view/use_agent_footer/warpify_footer.rs +++ b/app/src/terminal/view/use_agent_footer/warpify_footer.rs @@ -30,30 +30,38 @@ impl WarpifyFooterView { let button_size = ButtonSize::XSmall; let warpify_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Warpify subshell", AgentFooterButtonTheme::new(None)) - .with_icon(Icon::Warp) - .with_size(button_size) - .with_tooltip("Enable Warp shell integration in this session") - .with_tooltip_alignment(TooltipAlignment::Left) - .on_click(|ctx| { - ctx.dispatch_typed_action(WarpifyFooterViewAction::Warpify); - }) + ActionButton::new( + i18n::t("terminal.use_agent_footer.warpify_subshell"), + AgentFooterButtonTheme::new(None), + ) + .with_icon(Icon::Warp) + .with_size(button_size) + .with_tooltip(i18n::t( + "terminal.use_agent_footer.enable_shell_integration", + )) + .with_tooltip_alignment(TooltipAlignment::Left) + .on_click(|ctx| { + ctx.dispatch_typed_action(WarpifyFooterViewAction::Warpify); + }) }); let use_agent_button = ctx.add_typed_action_view(|ctx| { - ActionButton::new("Use agent", AgentFooterButtonTheme::new(None)) - .with_icon(Icon::Oz) - .with_keybinding(KeystrokeSource::Fixed(USE_AGENT_KEYSTROKE.clone()), ctx) - .with_size(button_size) - .with_tooltip("Ask the Warp agent to assist") - .with_tooltip_alignment(TooltipAlignment::Left) - .on_click(|ctx| { - ctx.dispatch_typed_action(WarpifyFooterViewAction::UseAgent); - }) + ActionButton::new( + i18n::t("terminal.use_agent_footer.use_agent"), + AgentFooterButtonTheme::new(None), + ) + .with_icon(Icon::Oz) + .with_keybinding(KeystrokeSource::Fixed(USE_AGENT_KEYSTROKE.clone()), ctx) + .with_size(button_size) + .with_tooltip(i18n::t("terminal.use_agent_footer.ask_assist")) + .with_tooltip_alignment(TooltipAlignment::Left) + .on_click(|ctx| { + ctx.dispatch_typed_action(WarpifyFooterViewAction::UseAgent); + }) }); let dismiss_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Dismiss", AgentFooterButtonTheme::new(None)) + ActionButton::new(i18n::t("common.dismiss"), AgentFooterButtonTheme::new(None)) .with_size(button_size) .on_click(|ctx| { ctx.dispatch_typed_action(WarpifyFooterViewAction::Dismiss); @@ -72,10 +80,14 @@ impl WarpifyFooterView { /// Updates the warpify button label, keybinding, and stores the current warpification mode. pub fn set_mode(&mut self, mode: WarpificationMode, ctx: &mut ViewContext) { let (label, binding_name) = match mode { - WarpificationMode::Ssh { .. } => { - ("Warpify SSH session", "terminal:warpify_ssh_session") - } - WarpificationMode::Subshell { .. } => ("Warpify subshell", "terminal:warpify_subshell"), + WarpificationMode::Ssh { .. } => ( + i18n::t("terminal.use_agent_footer.warpify_ssh_session"), + "terminal:warpify_ssh_session", + ), + WarpificationMode::Subshell { .. } => ( + i18n::t("terminal.use_agent_footer.warpify_subshell"), + "terminal:warpify_subshell", + ), }; self.warpify_button.update(ctx, |button, ctx| { button.set_label(label, ctx); diff --git a/app/src/terminal/view/zero_state_block.rs b/app/src/terminal/view/zero_state_block.rs index b5d8de3566..713cce9adc 100644 --- a/app/src/terminal/view/zero_state_block.rs +++ b/app/src/terminal/view/zero_state_block.rs @@ -161,7 +161,7 @@ impl View for TerminalViewZeroStateBlock { ) .with_child( Text::new( - "New terminal session", + i18n::t("terminal.zero_state.new_terminal_session"), appearance.ui_font_family(), title_font_size, ) @@ -185,7 +185,7 @@ impl View for TerminalViewZeroStateBlock { Message::new(vec![MessageItem::clickable( vec![ MessageItem::keystroke(ENTER_AGENT_VIEW_NEW_CONVERSATION_KEYSTROKE.clone()), - MessageItem::text("start a new agent conversation"), + MessageItem::text(i18n::t("terminal.zero_state.start_agent_conversation")), ], |ctx| { ctx.dispatch_typed_action(TerminalAction::StartNewAgentConversation); @@ -200,7 +200,9 @@ impl View for TerminalViewZeroStateBlock { MessageItem::keystroke( ENTER_CLOUD_AGENT_VIEW_NEW_CONVERSATION_KEYSTROKE.clone(), ), - MessageItem::text("start a new cloud agent conversation"), + MessageItem::text(i18n::t( + "terminal.zero_state.start_cloud_agent_conversation", + )), ], |ctx| { ctx.dispatch_typed_action(TerminalAction::EnterCloudAgentView); @@ -216,7 +218,9 @@ impl View for TerminalViewZeroStateBlock { key: "up".to_owned(), ..Default::default() }), - MessageItem::text("cycle past commands and conversations"), + MessageItem::text(i18n::t( + "terminal.zero_state.cycle_past_commands_and_conversations", + )), ], |ctx| { ctx.dispatch_typed_action(TerminalAction::OpenInlineHistoryMenu); @@ -235,7 +239,7 @@ impl View for TerminalViewZeroStateBlock { Message::new(vec![MessageItem::clickable( vec![ MessageItem::keystroke(keystroke), - MessageItem::text("open code review"), + MessageItem::text(i18n::t("terminal.zero_state.open_code_review")), ], |ctx| { ctx.dispatch_typed_action(WorkspaceAction::ToggleRightPanel); @@ -274,7 +278,9 @@ impl View for TerminalViewZeroStateBlock { Shrinkable::new( 1., render_standard_message( - Message::from_text("autodetect agent prompts in terminal sessions"), + Message::from_text(i18n::t( + "terminal.zero_state.autodetect_agent_prompts", + )), app, ), ) @@ -291,7 +297,7 @@ impl View for TerminalViewZeroStateBlock { theme.disabled_text_color(theme.background()) }; Text::new( - "Don't show again", + i18n::t("common.do_not_show_again"), appearance.ui_font_family(), appearance.monospace_font_size() - 4., ) diff --git a/app/src/terminal/warpify/render.rs b/app/src/terminal/warpify/render.rs index 3e2ba6336f..338afd2e36 100644 --- a/app/src/terminal/warpify/render.rs +++ b/app/src/terminal/warpify/render.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine}; use pathfinder_color::ColorU; use pathfinder_geometry::rect::RectF; @@ -38,7 +40,7 @@ pub const SUBSHELL_DOCS_URL: &str = "https://docs.warp.dev/terminal/warpify/subs pub const LEFT_STRIPE_WIDTH: f32 = 5.; pub fn build_header_row( - text: &'static str, + text: impl Into>, icon: Icon, theme: &WarpTheme, appearance: &Appearance, @@ -77,7 +79,7 @@ pub fn apply_spacing_styles(header_row: Container) -> Container { /// UI helper to render the header of an SSH rich content block. pub fn header_row( - text: &'static str, + text: impl Into>, icon: Icon, theme: &WarpTheme, appearance: &Appearance, @@ -181,7 +183,7 @@ pub fn render_never_warpify_ssh_link( let link = appearance .ui_builder() .link( - "Never Warpify this host".into(), + i18n::t("terminal.warpify.never_warpify_this_host"), None, Some(Box::new({ let ssh_host = ssh_host.clone(); diff --git a/app/src/terminal/warpify/settings.rs b/app/src/terminal/warpify/settings.rs index 8b313ac370..eef5f089b1 100644 --- a/app/src/terminal/warpify/settings.rs +++ b/app/src/terminal/warpify/settings.rs @@ -19,7 +19,7 @@ maybe_define_setting!(AddedSubshellCommands, group: WarpifySettings, { sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "warpify.subshells.added_subshell_commands", - description: "Additional regex patterns for commands that should be recognized as subshells.", + description_key: "settings.schema.warpify.subshells.added_subshell_commands.description", }); maybe_define_setting!(SubshellCommandsDenylist, group: WarpifySettings, { @@ -29,7 +29,7 @@ maybe_define_setting!(SubshellCommandsDenylist, group: WarpifySettings, { sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "warpify.subshells.subshell_commands_denylist", - description: "Commands that should not trigger the subshell warpification prompt.", + description_key: "settings.schema.warpify.subshells.subshell_commands_denylist.description", }); maybe_define_setting!(SshHostsDenylist, group: WarpifySettings, { @@ -39,7 +39,7 @@ maybe_define_setting!(SshHostsDenylist, group: WarpifySettings, { sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "warpify.ssh.ssh_hosts_denylist", - description: "SSH hosts that should not trigger the warpification prompt.", + description_key: "settings.schema.warpify.ssh.ssh_hosts_denylist.description", }); maybe_define_setting!(EnableSshWarpification, group: WarpifySettings, { @@ -49,7 +49,7 @@ maybe_define_setting!(EnableSshWarpification, group: WarpifySettings, { sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "warpify.ssh.enable_ssh_warpification", - description: "Whether to enable Warp features in SSH sessions.", + description_key: "settings.schema.warpify.ssh.enable_ssh_warpification.description", }); maybe_define_setting!(UseSshTmuxWrapper, group: WarpifySettings, { @@ -59,7 +59,7 @@ maybe_define_setting!(UseSshTmuxWrapper, group: WarpifySettings, { sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "warpify.ssh.use_ssh_tmux_wrapper", - description: "Whether to use a tmux-based wrapper for SSH warpification.", + description_key: "settings.schema.warpify.ssh.use_ssh_tmux_wrapper.description", }); /// Controls how Warp handles the SSH extension (remote server binary) when connecting @@ -98,7 +98,7 @@ maybe_define_setting!(SshExtensionInstallModeSetting, group: WarpifySettings, { sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "warpify.ssh.ssh_extension_install_mode", - description: "Controls SSH extension installation behavior.", + description_key: "settings.schema.warpify.ssh.ssh_extension_install_mode.description", }); impl SshExtensionInstallMode { @@ -109,6 +109,20 @@ impl SshExtensionInstallMode { SshExtensionInstallMode::NeverInstall => "Never install", } } + + pub fn localized_display_name(&self) -> String { + match self { + SshExtensionInstallMode::AlwaysAsk => { + i18n::t("settings.warpify.install_mode.always_ask") + } + SshExtensionInstallMode::AlwaysInstall => { + i18n::t("settings.warpify.install_mode.always_install") + } + SshExtensionInstallMode::NeverInstall => { + i18n::t("settings.warpify.install_mode.never_install") + } + } + } } /// Normally we use the define_settings_group! macro for singleton models of settings like this. diff --git a/app/src/terminal/warpify/success_block.rs b/app/src/terminal/warpify/success_block.rs index a98b487023..c69b35b908 100644 --- a/app/src/terminal/warpify/success_block.rs +++ b/app/src/terminal/warpify/success_block.rs @@ -47,7 +47,7 @@ struct AutoWarpifySnippet { selected_text: Arc>>, shell_type: ShellType, - description: Cow<'static, str>, + description_key: &'static str, code_snippet_handles: CodeSnippetButtonHandles, can_write_to_rc: bool, } @@ -110,21 +110,20 @@ impl WarpifySuccessBlock { }) }) }; - let auto_warpify_snippet = auto_warpify_snippet.map(|(output_grid, can_write_to_rc)| { - AutoWarpifySnippet { - description: (if !output_grid.is_empty() { - "Run the following to automatically Warpify in the future:" + let auto_warpify_snippet = + auto_warpify_snippet.map(|(output_grid, can_write_to_rc)| AutoWarpifySnippet { + description_key: if !output_grid.is_empty() { + "terminal.warpify.auto_warpify_command.description" } else { - "In remote subshells, Warp runs commands in the background to power completions, syntax highlighting, and other features." - }).into(), + "terminal.warpify.remote_subshell.description" + }, output_grid: output_grid.into(), selection_handle: Default::default(), selected_text: Default::default(), code_snippet_handles: Default::default(), shell_type: shell.shell_type(), can_write_to_rc, - } - }); + }); Self { source, @@ -153,7 +152,7 @@ impl WarpifySuccessBlock { pub fn render_title_ui(&self, theme: &WarpTheme, appearance: &Appearance) -> Box { let header_contents = render::build_header_row( - "Session Warpified", + i18n::t("terminal.warpify.session_warpified"), Icon::new(UiIcon::Warp.into(), theme.active_ui_detail()), theme, appearance, @@ -191,7 +190,7 @@ impl WarpifySuccessBlock { appearance .ui_builder() .link( - "Learn more".into(), + i18n::t("common.learn_more"), None, Some(Box::new({ move |ctx| { @@ -283,7 +282,7 @@ impl WarpifySuccessBlock { .with_child( Container::new( Text::new( - auto_warpify_snippet.description.clone(), + i18n::t(auto_warpify_snippet.description_key), appearance.monospace_font_family(), appearance.monospace_font_size(), ) diff --git a/app/src/test_util/settings.rs b/app/src/test_util/settings.rs index 9e9abc3229..a9d18014ea 100644 --- a/app/src/test_util/settings.rs +++ b/app/src/test_util/settings.rs @@ -30,6 +30,7 @@ pub fn initialize_settings_for_tests_with_mode( use crate::drive::settings::WarpDriveSettings; use crate::search::command_search::settings::CommandSearchSettings; use crate::settings::app_icon::AppIconSettings; + use crate::settings::language::LanguageSettings; use crate::settings::manager::SettingsManager; use crate::settings::{ init_and_register_user_preferences, AISettings, AccessibilitySettings, @@ -70,6 +71,7 @@ pub fn initialize_settings_for_tests_with_mode( CommandSearchSettings::register(app); DebugSettings::register(app); AppIconSettings::register(app); + LanguageSettings::register(app); EmacsBindingsSettings::register(app); #[cfg(feature = "local_fs")] diff --git a/app/src/themes/theme_chooser.rs b/app/src/themes/theme_chooser.rs index 4aefd80835..1b97410553 100644 --- a/app/src/themes/theme_chooser.rs +++ b/app/src/themes/theme_chooser.rs @@ -45,7 +45,6 @@ use crate::workspace::PANEL_HEADER_HEIGHT; use crate::{report_if_error, send_telemetry_from_ctx}; // All units in px -const THEME_CHOOSER_TITLE: &str = "Themes"; const CLOSE_BUTTON_MARGIN_RIGHT: f32 = 6.; const TITLE_FONT_SIZE: f32 = 16.; const TITLE_MARGIN: f32 = 12.; @@ -115,13 +114,13 @@ impl ThemeChooserMode { let hint_text = match self { ThemeChooserMode::SystemAgnostic => appearance .ui_builder() - .paragraph("Change your current theme.".to_string()), + .paragraph(i18n::t("themes.chooser.hint.current")), ThemeChooserMode::SystemLight => appearance .ui_builder() - .paragraph("Pick a theme for when your system is in light mode.".to_string()), + .paragraph(i18n::t("themes.chooser.hint.light")), ThemeChooserMode::SystemDark => appearance .ui_builder() - .paragraph("Pick a theme for when your system is in dark mode.".to_string()), + .paragraph(i18n::t("themes.chooser.hint.dark")), }; hint_text .build() @@ -636,7 +635,7 @@ impl ThemeChooser { Align::new( appearance .ui_builder() - .span(THEME_CHOOSER_TITLE.to_string()) + .span(i18n::t("themes.chooser.title")) .with_style(UiComponentStyles { font_family_id: Some(appearance.ui_font_family()), font_size: Some(TITLE_FONT_SIZE), @@ -742,7 +741,7 @@ impl ThemeChooser { .with_child( appearance .ui_builder() - .span("No matching themes!".to_string()) + .span(i18n::t("themes.chooser.no_matching_themes")) .build() .finish(), ) @@ -839,9 +838,9 @@ impl View for ThemeChooser { fn accessibility_contents(&self, _: &AppContext) -> Option { Some(AccessibilityContent::new( - "Theme chooser. Unfortunately, theme chooser window isn't compatible with screen readers yet.", - "Press escape to close.", - WarpA11yRole::WindowRole, + i18n::t("themes.chooser.a11y.description"), + i18n::t("themes.chooser.a11y.close_hint"), + WarpA11yRole::WindowRole, )) } diff --git a/app/src/themes/theme_creator_body.rs b/app/src/themes/theme_creator_body.rs index 0d4ad28d5b..5b4dd6a193 100644 --- a/app/src/themes/theme_creator_body.rs +++ b/app/src/themes/theme_creator_body.rs @@ -36,14 +36,6 @@ const BUTTON_FONT_SIZE: f32 = 14.; const BUTTON_BORDER_RADIUS: f32 = 4.; const BORDER_WIDTH: f32 = 1.; -const MODAL_SUBHEADER: &str = - "Automatically generate a theme based on extracted colors from an image (.png, .jpg)."; -const IMAGE_PICKER_BUTTON_PRE_SELECT_TEXT: &str = "Select an image"; -const IMAGE_PICKER_BUTTON_SELECTING_TEXT: &str = "Selecting image..."; -const IMAGE_PICKER_BUTTON_POST_SELECT_TEXT: &str = "Select a new image"; -const CANCEL_BUTTON_TEXT: &str = "Cancel"; -const CREATE_BUTTON_TEXT: &str = "Create theme"; - #[derive(Default)] struct MouseStateHandles { image_picker_mouse_state: MouseStateHandle, @@ -85,12 +77,14 @@ pub enum ThemeCreatorImageState { impl fmt::Display for ThemeCreatorImageState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - ThemeCreatorImageState::Empty => write!(f, "{IMAGE_PICKER_BUTTON_PRE_SELECT_TEXT}"), + ThemeCreatorImageState::Empty => { + write!(f, "{}", i18n::t("themes.creator.select_image")) + } ThemeCreatorImageState::Uploading => { - write!(f, "{IMAGE_PICKER_BUTTON_SELECTING_TEXT}") + write!(f, "{}", i18n::t("themes.creator.selecting_image")) } ThemeCreatorImageState::Uploaded => { - write!(f, "{IMAGE_PICKER_BUTTON_POST_SELECT_TEXT}") + write!(f, "{}", i18n::t("themes.creator.select_new_image")) } } } @@ -175,11 +169,7 @@ impl ThemeCreatorBody { .and_then(|extension| extension.to_str()); let Some(image_extension) = image_extension else { - self.send_error_toast( - "Failed to process selected image. Please try again with a different image." - .to_string(), - ctx, - ); + self.send_error_toast(i18n::t("themes.creator.error_process_image"), ctx); return; }; @@ -212,7 +202,7 @@ impl ThemeCreatorBody { #[cfg(not(feature = "local_fs"))] log::warn!("Tried to save theme without a local filesystem."); if errored { - self.send_error_toast("Something went wrong".to_string(), ctx); + self.send_error_toast(i18n::t("common.something_went_wrong"), ctx); } } } @@ -260,25 +250,24 @@ impl ThemeCreatorBody { ctx.spawn( InMemoryThemeOptions::new(file_stem_string.clone(), path.clone()), - move |theme_creator_body, theme_options, ctx| { - match theme_options { - Ok(theme_options) => { - AppearanceManager::handle(ctx).update(ctx, |appearance_manager, ctx| { - appearance_manager.clear_transient_theme(ctx); - }); + move |theme_creator_body, theme_options, ctx| match theme_options { + Ok(theme_options) => { + AppearanceManager::handle(ctx).update(ctx, |appearance_manager, ctx| { + appearance_manager.clear_transient_theme(ctx); + }); - theme_creator_body.theme_options = Some(theme_options); - theme_creator_body.editor.update(ctx, |editor, ctx| { - editor.set_buffer_text(&file_stem_string, ctx); - }); - theme_creator_body.image_state = ThemeCreatorImageState::Uploaded; - }, - Err(e) => { - theme_creator_body.send_error_toast( - format!("Failed to process selected image due to error: {e}. Please try again with a different image."), - ctx, - ); - } + theme_creator_body.theme_options = Some(theme_options); + theme_creator_body.editor.update(ctx, |editor, ctx| { + editor.set_buffer_text(&file_stem_string, ctx); + }); + theme_creator_body.image_state = ThemeCreatorImageState::Uploaded; + } + Err(e) => { + theme_creator_body.send_error_toast( + i18n::t("themes.creator.error_process_image_with_error") + .replace("{error}", &e.to_string()), + ctx, + ); } }, ); @@ -418,7 +407,7 @@ impl View for ThemeCreatorBody { padding: Some(Coords::uniform(BUTTON_PADDING)), ..Default::default() }) - .with_centered_text_label(CANCEL_BUTTON_TEXT.into()); + .with_centered_text_label(i18n::t("common.cancel")); let mut create_button = appearance .ui_builder() @@ -430,15 +419,19 @@ impl View for ThemeCreatorBody { Some(create_hovered_styles), Some(disabled_styles), ) - .with_centered_text_label(CREATE_BUTTON_TEXT.into()); + .with_centered_text_label(i18n::t("themes.creator.create_theme")); let mut flex: Flex = Flex::column() .with_cross_axis_alignment(CrossAxisAlignment::Stretch) .with_child( Container::new( - Text::new_inline(MODAL_SUBHEADER, appearance.ui_font_family(), 14.) - .with_color(appearance.theme().active_ui_text_color().into()) - .finish(), + Text::new_inline( + i18n::t("themes.creator.modal_subheader"), + appearance.ui_font_family(), + 14., + ) + .with_color(appearance.theme().active_ui_text_color().into()) + .finish(), ) .finish(), ); @@ -446,9 +439,13 @@ impl View for ThemeCreatorBody { if let Some(theme_options) = &self.theme_options { flex.add_child( Container::new( - Text::new_inline("Theme name", appearance.ui_font_family(), 14.) - .with_color(appearance.theme().active_ui_text_color().into()) - .finish(), + Text::new_inline( + i18n::t("themes.creator.theme_name"), + appearance.ui_font_family(), + 14., + ) + .with_color(appearance.theme().active_ui_text_color().into()) + .finish(), ) .with_margin_top(12.) .finish(), @@ -475,9 +472,13 @@ impl View for ThemeCreatorBody { flex.add_child( Container::new( - Text::new_inline("Background color", appearance.ui_font_family(), 14.) - .with_color(appearance.theme().active_ui_text_color().into()) - .finish(), + Text::new_inline( + i18n::t("themes.creator.background_color"), + appearance.ui_font_family(), + 14., + ) + .with_color(appearance.theme().active_ui_text_color().into()) + .finish(), ) .with_margin_top(24.) .finish(), diff --git a/app/src/themes/theme_creator_modal.rs b/app/src/themes/theme_creator_modal.rs index 8f2bf927ed..5f6ac6a6ea 100644 --- a/app/src/themes/theme_creator_modal.rs +++ b/app/src/themes/theme_creator_modal.rs @@ -19,8 +19,6 @@ use crate::themes::theme_creator_body::{ use crate::view_components::DismissibleToast; use crate::workspace::ToastStack; -const THEME_CREATOR_MODAL_HEADER: &str = "Create new theme from image"; - pub struct ThemeCreatorModal { theme_creator_modal: ViewHandle>, } @@ -59,7 +57,7 @@ impl ThemeCreatorModal { let theme_creator_modal = ctx.add_typed_action_view(|ctx| { Modal::new( - Some(THEME_CREATOR_MODAL_HEADER.to_string()), + Some(i18n::t("themes.creator.modal_header")), theme_creator_body, ctx, ) diff --git a/app/src/themes/theme_deletion_body.rs b/app/src/themes/theme_deletion_body.rs index 46e5c2a1d5..e8c88203cf 100644 --- a/app/src/themes/theme_deletion_body.rs +++ b/app/src/themes/theme_deletion_body.rs @@ -25,10 +25,6 @@ const BUTTON_FONT_SIZE: f32 = 14.; const BUTTON_BORDER_RADIUS: f32 = 4.; const BORDER_WIDTH: f32 = 1.; -const MODAL_SUBHEADER: &str = "This will permanently delete the theme."; -const CANCEL_BUTTON_TEXT: &str = "Cancel"; -const DELETE_BUTTON_TEXT: &str = "Delete theme"; - #[derive(Default)] struct MouseStateHandles { cancel_mouse_state: MouseStateHandle, @@ -106,7 +102,7 @@ impl ThemeDeletionBody { } } if errored { - self.send_error_toast("Something went wrong", ctx); + self.send_error_toast(&i18n::t("common.something_went_wrong"), ctx); } } @@ -195,7 +191,7 @@ impl View for ThemeDeletionBody { Some(cancel_hovered_styles), Some(disabled_styles), ) - .with_centered_text_label(CANCEL_BUTTON_TEXT.into()); + .with_centered_text_label(i18n::t("common.cancel")); let create_button = appearance .ui_builder() @@ -207,15 +203,19 @@ impl View for ThemeDeletionBody { Some(create_hovered_styles), Some(disabled_styles), ) - .with_centered_text_label(DELETE_BUTTON_TEXT.into()); + .with_centered_text_label(i18n::t("themes.deletion.delete_theme")); Flex::column() .with_cross_axis_alignment(CrossAxisAlignment::Stretch) .with_child( Container::new( - Text::new_inline(MODAL_SUBHEADER, appearance.ui_font_family(), 14.) - .with_color(appearance.theme().active_ui_text_color().into()) - .finish(), + Text::new_inline( + i18n::t("themes.deletion.modal_subheader"), + appearance.ui_font_family(), + 14., + ) + .with_color(appearance.theme().active_ui_text_color().into()) + .finish(), ) .finish(), ) diff --git a/app/src/themes/theme_deletion_modal.rs b/app/src/themes/theme_deletion_modal.rs index b425080541..78fc70bbac 100644 --- a/app/src/themes/theme_deletion_modal.rs +++ b/app/src/themes/theme_deletion_modal.rs @@ -10,8 +10,6 @@ use crate::modal::Modal; use crate::themes::theme::ThemeKind; use crate::themes::theme_deletion_body::{ThemeDeletionBody, ThemeDeletionBodyEvent}; -const THEME_DELETION_MODAL_HEADER: &str = "Are you sure you want to delete this theme?"; - pub struct ThemeDeletionModal { theme_deletion_modal: ViewHandle>, } @@ -50,7 +48,7 @@ impl ThemeDeletionModal { let theme_deletion_modal = ctx.add_typed_action_view(|ctx| { Modal::new( - Some(THEME_DELETION_MODAL_HEADER.to_string()), + Some(i18n::t("themes.deletion.modal_header")), theme_deletion_body, ctx, ) diff --git a/app/src/tips/tip_view.rs b/app/src/tips/tip_view.rs index bcee5302ff..df05dcedd2 100644 --- a/app/src/tips/tip_view.rs +++ b/app/src/tips/tip_view.rs @@ -33,8 +33,8 @@ const MODAL_WIDTH: f32 = 250.; #[derive(Clone)] struct TipItem { - pub title: String, - pub description: String, + pub title_key: &'static str, + pub description_key: &'static str, pub editable_binding_name: String, pub shortcut: Option, pub tip_feature: Tip, @@ -42,8 +42,8 @@ struct TipItem { impl TipItem { pub fn new( - title: String, - description: String, + title_key: &'static str, + description_key: &'static str, feature: TipAction, ctx: &mut AppContext, ) -> Self { @@ -52,8 +52,8 @@ impl TipItem { let tip_feature = Tip::Action(feature); Self { - title, - description, + title_key, + description_key, editable_binding_name, shortcut, tip_feature, @@ -125,33 +125,32 @@ impl TipsView { let tip_items = vec![ TipItem::new( - "Command Palette".to_string(), - "Easily discover everything you can do in Warp without your hands leaving the keyboard.".to_string(), + "tips.command_palette.title", + "tips.command_palette.description", TipAction::CommandPalette, ctx, ), TipItem::new( - "Split Pane".to_string(), - "Split tabs into multiple panes to make your ideal layout." - .to_string(), + "tips.split_pane.title", + "tips.split_pane.description", TipAction::SplitPane, ctx, ), TipItem::new( - "History Search".to_string(), - "Find, edit and re-run previously executed commands.".to_string(), + "tips.history_search.title", + "tips.history_search.description", TipAction::HistorySearch, ctx, ), TipItem::new( - "AI Command Search".to_string(), - "Generate shell commands with natural language.".to_string(), + "tips.ai_command_search.title", + "tips.ai_command_search.description", TipAction::AiCommandSearch, ctx, ), TipItem::new( - "Theme Picker".to_string(), - "Make Warp your own by choosing a built-in theme. Or create your own.".to_string(), + "tips.theme_picker.title", + "tips.theme_picker.description", TipAction::ThemePicker, ctx, ), @@ -225,7 +224,7 @@ impl TipsView { content.add_child( Container::new( ui_builder - .wrappable_text(tip_item.title, false) + .wrappable_text(i18n::t(tip_item.title_key), false) .with_style(UiComponentStyles { font_family_id: Some(appearance.ui_font_family()), font_size: Some(appearance.monospace_font_size()), @@ -242,7 +241,7 @@ impl TipsView { content.add_child( Container::new( ui_builder - .wrappable_text(tip_item.description, true) + .wrappable_text(i18n::t(tip_item.description_key), true) .with_style(UiComponentStyles { font_family_id: Some(appearance.ui_font_family()), font_size: Some(appearance.monospace_font_size() * 0.8), @@ -260,7 +259,7 @@ impl TipsView { .with_child( Container::new( ui_builder - .wrappable_text("Shortcut".to_string(), false) + .wrappable_text(i18n::t("tips.shortcut"), false) .with_style(UiComponentStyles { font_family_id: Some(appearance.ui_font_family()), font_size: Some(appearance.monospace_font_size() * 0.8), @@ -395,7 +394,7 @@ impl TipsView { Align::new( appearance .ui_builder() - .paragraph("Skip Welcome Tips".to_string()) + .paragraph(i18n::t("tips.skip_welcome_tips")) .build() .finish(), ) @@ -448,7 +447,7 @@ impl TipsView { .finish(); let title = ui_builder - .span("Complete!") + .span(i18n::t("tips.complete")) .with_style(UiComponentStyles { font_weight: Some(Weight::Bold), // Set to white here as the background has 85% black overlay. @@ -460,7 +459,7 @@ impl TipsView { .finish(); let sub_text = ui_builder - .paragraph("Nice work on finishing the welcome tips!") + .paragraph(i18n::t("tips.finished_message")) .with_style(UiComponentStyles { font_size: Some(12.), font_color: Some(Fill::white().into()), @@ -480,7 +479,7 @@ impl TipsView { .set_width(152.) .set_height(34.), ) - .with_centered_text_label("Close Welcome Tips".to_string()) + .with_centered_text_label(i18n::t("tips.close_welcome_tips")) .build() .on_click(|ctx, _, _| ctx.dispatch_typed_action(TipsAction::DismissTips)) .finish(); diff --git a/app/src/undo_close/settings.rs b/app/src/undo_close/settings.rs index 25af7d7070..ab6b1b23d0 100644 --- a/app/src/undo_close/settings.rs +++ b/app/src/undo_close/settings.rs @@ -11,7 +11,7 @@ define_settings_group!(UndoCloseSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "general.undo_close.enabled", - description: "Whether the undo close feature is enabled.", + description_key: "settings.schema.general.undo_close.enabled.description", }, grace_period: UndoCloseGracePeriod { type: Duration, @@ -20,6 +20,6 @@ define_settings_group!(UndoCloseSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "general.undo_close.grace_period", - description: "How long after closing a tab you can still undo the close.", + description_key: "settings.schema.general.undo_close.grace_period.description", }, ]); diff --git a/app/src/uri/mod.rs b/app/src/uri/mod.rs index 0656355e7e..b88a5111b9 100644 --- a/app/src/uri/mod.rs +++ b/app/src/uri/mod.rs @@ -944,8 +944,7 @@ impl Action { if let Err(err) = open_docker_container(url, ctx) { if let Some(window_id) = primary_window_id { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = - DismissibleToast::error("Custom URI is invalid.".to_owned()); + let toast = DismissibleToast::error(i18n::t("uri.custom_uri_invalid")); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); } @@ -1145,8 +1144,8 @@ impl Action { | Self::FocusCloudMode | Self::AutoHandoffToCloud { .. } => W::default(), Self::NewTab => W::ShowPrimaryWindow(WindowActivationFallbackBehavior::Notify { - title: "New tab created".to_owned(), - description: "Go to Warp to see your new tab.".to_owned(), + title: i18n::t("uri.new_tab_created"), + description: i18n::t("uri.new_tab_created.description"), }), Self::NewWindow => W::Nothing, } @@ -1191,7 +1190,10 @@ pub fn handle_incoming_uri(url: &Url, ctx: &mut AppContext) { Err(e) => { if let Some(window_id) = primary_window_id { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::error(format!("Custom URI is invalid: {e:?}")); + let toast = DismissibleToast::error( + i18n::t("uri.custom_uri_invalid_with_error") + .replace("{error}", &format!("{e:?}")), + ); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); } diff --git a/app/src/util/bindings.rs b/app/src/util/bindings.rs index 9c4b89b3f6..52741fd799 100644 --- a/app/src/util/bindings.rs +++ b/app/src/util/bindings.rs @@ -726,7 +726,7 @@ impl CommandBinding { pub fn new(name: String, description: String, trigger: Option) -> Self { CommandBinding { name, - description: BindingDescription::new(description), + description: localize_binding_description(&BindingDescription::new(description)), trigger, action: None, group: None, @@ -783,11 +783,66 @@ impl CommandBinding { } fn materialize_description(desc: &BindingDescription, ctx: &AppContext) -> BindingDescription { - if desc.has_dynamic_override() { + let description = if desc.has_dynamic_override() { desc.materialized(ctx) } else { desc.clone() + }; + + localize_binding_description(&description) +} + +fn localize_binding_description(desc: &BindingDescription) -> BindingDescription { + let default = desc.in_context(DescriptionContext::Default); + let mac_menu = desc.in_context(MAC_MENUS_CONTEXT); + + let localized_default = localize_binding_description_text(default); + let localized_mac_menu = localize_binding_description_text(mac_menu); + + let mut localized = BindingDescription::new_preserve_case(localized_default.clone()); + if mac_menu != default || localized_mac_menu != localized_default { + localized = localized.with_custom_description(MAC_MENUS_CONTEXT, localized_mac_menu); } + + localized +} + +fn localize_binding_description_text(description: &str) -> String { + if i18n::current_locale() == i18n::FALLBACK_LOCALE { + return description.to_string(); + } + + let slug = binding_description_i18n_slug(description); + for prefix in ["keybinding.description.", "app_menu.action."] { + let key = format!("{prefix}{slug}"); + let localized = i18n::t(&key); + if localized != key { + return localized; + } + } + + description.to_string() +} + +fn binding_description_i18n_slug(description: &str) -> String { + let mut slug = String::new(); + let mut last_was_separator = true; + + for ch in description.chars() { + if ch.is_ascii_alphanumeric() { + slug.push(ch.to_ascii_lowercase()); + last_was_separator = false; + } else if !last_was_separator { + slug.push('_'); + last_was_separator = true; + } + } + + while slug.ends_with('_') { + slug.pop(); + } + + slug } /// Possible groups a Binding can be part of. The string representation (produced in diff --git a/app/src/util/file/external_editor/settings.rs b/app/src/util/file/external_editor/settings.rs index bb91ba9110..136fbf5597 100644 --- a/app/src/util/file/external_editor/settings.rs +++ b/app/src/util/file/external_editor/settings.rs @@ -76,7 +76,7 @@ define_settings_group!(EditorSettings, settings: [ private: false, toml_path: "code.editor.open_file_editor", max_table_depth: 0, - description: "The editor used to open files.", + description_key: "settings.schema.code.editor.open_file_editor.description", }, open_code_panels_file_editor: OpenCodePanelsFileEditor { type: EditorChoice, @@ -86,7 +86,7 @@ define_settings_group!(EditorSettings, settings: [ private: false, toml_path: "code.editor.open_code_panels_file_editor", max_table_depth: 0, - description: "The editor used to open files from code panels.", + description_key: "settings.schema.code.editor.open_code_panels_file_editor.description", }, open_file_layout: OpenFileLayout { type: EditorLayout, @@ -95,7 +95,7 @@ define_settings_group!(EditorSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "code.editor.open_file_layout", - description: "The layout used when opening files in the editor.", + description_key: "settings.schema.code.editor.open_file_layout.description", }, prefer_markdown_viewer: PreferMarkdownViewer { type: bool, @@ -104,7 +104,7 @@ define_settings_group!(EditorSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "code.editor.prefer_markdown_viewer", - description: "Whether to use the Markdown viewer when opening Markdown files.", + description_key: "settings.schema.code.editor.prefer_markdown_viewer.description", }, prefer_tabbed_editor_view: PreferTabbedEditorView { type: bool, @@ -113,7 +113,7 @@ define_settings_group!(EditorSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "code.editor.prefer_tabbed_editor_view", - description: "Whether to prefer opening files in a tabbed editor view.", + description_key: "settings.schema.code.editor.prefer_tabbed_editor_view.description", }, open_conversation_layout_preference: OpenConversationLayoutPreference { type: OpenConversationPreference, @@ -122,7 +122,7 @@ define_settings_group!(EditorSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.open_conversation_layout_preference", - description: "Whether to open agent conversations in a new tab or a split pane.", + description_key: "settings.schema.agents.warp_agent.other.open_conversation_layout_preference.description", }, ]); diff --git a/app/src/util/time_format.rs b/app/src/util/time_format.rs index 6e6d14aa6d..33a5e3ad24 100644 --- a/app/src/util/time_format.rs +++ b/app/src/util/time_format.rs @@ -35,25 +35,28 @@ pub fn human_readable_precise_duration(duration: Duration) -> String { let ms = duration.num_milliseconds() as f64; let weeks = ms / WEEK_TO_MS; if weeks >= 1. { - return String::from(">1 week"); + return i18n::t("util.time.precise.more_than_one_week"); } let days = ms / DAY_TO_MS; if days >= 1. { - return format!("{} days", format_sigfigs(days, 3)); + return t_count("util.time.precise.days", format_sigfigs(days, 3)); } let hours = ms / HOUR_TO_MS; if hours >= 1. { - return format!("{} hours", format_sigfigs(hours, 3)); + return t_count("util.time.precise.hours", format_sigfigs(hours, 3)); } let minutes = ms / MIN_TO_MS; if minutes >= 1. { - return format!("{} min", format_sigfigs(minutes, 3)); + return t_count("util.time.precise.minutes", format_sigfigs(minutes, 3)); } let seconds = ms / SEC_TO_MS; if seconds >= 1. { - return format!("{} sec", format_sigfigs(seconds, 3)); + return t_count("util.time.precise.seconds", format_sigfigs(seconds, 3)); } - format!("{} ms", duration.num_milliseconds()) + t_count( + "util.time.precise.milliseconds", + duration.num_milliseconds(), + ) } fn format_sigfigs(num: f64, sigfigs: usize) -> String { @@ -77,74 +80,98 @@ pub fn human_readable_approx_duration(duration: Duration, sentence_case: bool) - let ms = duration.num_milliseconds() as f64; let years = ms / YEAR_TO_MS; if years >= 1. { - return truncated_quantity_with_unit(years, "year"); + return truncated_quantity_with_unit( + years, + "util.time.approx.year_ago", + "util.time.approx.years_ago", + ); } let months = ms / MONTH_TO_MS; if months >= 1. { - return truncated_quantity_with_unit(months, "month"); + return truncated_quantity_with_unit( + months, + "util.time.approx.month_ago", + "util.time.approx.months_ago", + ); } let weeks = ms / WEEK_TO_MS; if weeks >= 1. { - return truncated_quantity_with_unit(weeks, "week"); + return truncated_quantity_with_unit( + weeks, + "util.time.approx.week_ago", + "util.time.approx.weeks_ago", + ); } let days = ms / DAY_TO_MS; if days >= 1. { - return truncated_quantity_with_unit(days, "day"); + return truncated_quantity_with_unit( + days, + "util.time.approx.day_ago", + "util.time.approx.days_ago", + ); } let hours = ms / HOUR_TO_MS; if hours >= 1. { - return truncated_quantity_with_unit(hours, "hour"); + return truncated_quantity_with_unit( + hours, + "util.time.approx.hour_ago", + "util.time.approx.hours_ago", + ); } // Minutes and seconds are both abbreviated, so skip pluralization. let minutes = ms / MIN_TO_MS; if minutes >= 1. { - return format!("{} min ago", minutes as i32); + return t_count("util.time.approx.minutes_ago_short", minutes as i32); } if sentence_case { - "Just now".to_owned() + i18n::t("util.time.approx.just_now_sentence") } else { - "just now".to_owned() + i18n::t("util.time.approx.just_now") } } /// Provided a value and a unit, this will format the quantity as an integer number with the /// unit pluralized if the value is not 1. -fn truncated_quantity_with_unit(num: f64, unit: &str) -> String { +fn truncated_quantity_with_unit(num: f64, singular_key: &str, plural_key: &str) -> String { let truncated_int = num as i32; if truncated_int == 1 { - format!("{truncated_int} {unit} ago") + t_count(singular_key, truncated_int) } else { - format!("{truncated_int} {unit}s ago") + t_count(plural_key, truncated_int) } } +fn t_count(key: &str, count: impl ToString) -> String { + i18n::t(key).replace("{count}", &count.to_string()) +} + /// Formats a monotonic `Instant` as a human-readable relative timestamp. /// (Uses `Instant` rather than wall-clock `DateTime` for elapsed-time display.) pub fn format_elapsed_since(created_at: instant::Instant) -> String { let secs = created_at.elapsed().as_secs(); if secs < 60 { - "Just now".to_string() + i18n::t("util.time.approx.just_now_sentence") } else if secs < 3600 { let mins = secs / 60; if mins == 1 { - "1 minute ago".to_string() + t_count("util.time.elapsed.minute_ago", mins) } else { - format!("{mins} minutes ago") + t_count("util.time.elapsed.minutes_ago", mins) } } else if secs < 86400 { let hours = secs / 3600; if hours == 1 { - "1 hour ago".to_string() + t_count("util.time.approx.hour_ago", hours) } else { - format!("{hours} hours ago") + t_count("util.time.approx.hours_ago", hours) } } else { let days = secs / 86400; if days == 1 { - "1 day ago".to_string() + t_count("util.time.approx.day_ago", days) } else { - format!("{days} days ago") + t_count("util.time.approx.days_ago", days) } } } diff --git a/app/src/util/time_format_tests.rs b/app/src/util/time_format_tests.rs index e1beb8ec54..0e16d5722e 100644 --- a/app/src/util/time_format_tests.rs +++ b/app/src/util/time_format_tests.rs @@ -14,35 +14,35 @@ fn test_format_sigfigs() { fn test_human_readable_precise_duration() { assert_eq!( human_readable_precise_duration(Duration::milliseconds(3)), - "3 ms".to_owned() + "3 毫秒".to_owned() ); assert_eq!( human_readable_precise_duration(Duration::milliseconds(10)), - "10 ms".to_owned() + "10 毫秒".to_owned() ); assert_eq!( human_readable_precise_duration(Duration::milliseconds(3141)), - "3.14 sec".to_owned() + "3.14 秒".to_owned() ); assert_eq!( human_readable_precise_duration(Duration::milliseconds(19961)), - "20.0 sec".to_owned() + "20.0 秒".to_owned() ); assert_eq!( human_readable_precise_duration(Duration::seconds(61)), - "1.02 min".to_owned() + "1.02 分钟".to_owned() ); assert_eq!( human_readable_precise_duration(Duration::minutes(930)), - "15.5 hours".to_owned() + "15.5 小时".to_owned() ); assert_eq!( human_readable_precise_duration(Duration::hours(46)), - "1.92 days".to_owned() + "1.92 天".to_owned() ); assert_eq!( human_readable_precise_duration(Duration::weeks(2)), - ">1 week".to_owned() + ">1 周".to_owned() ); } @@ -50,46 +50,46 @@ fn test_human_readable_precise_duration() { fn test_human_readable_approx_duration() { assert_eq!( human_readable_approx_duration(Duration::milliseconds(2), false), - "just now".to_owned() + "刚刚".to_owned() ); assert_eq!( human_readable_approx_duration(Duration::seconds(2), false), - "just now".to_owned() + "刚刚".to_owned() ); assert_eq!( human_readable_approx_duration(Duration::milliseconds(2), true), - "Just now".to_owned() + "刚刚".to_owned() ); assert_eq!( human_readable_approx_duration(Duration::seconds(2), true), - "Just now".to_owned() + "刚刚".to_owned() ); assert_eq!( human_readable_approx_duration(Duration::seconds(90), false), - "1 min ago".to_owned() + "1 分钟前".to_owned() ); assert_eq!( human_readable_approx_duration(Duration::minutes(100), false), - "1 hour ago".to_owned() + "1 小时前".to_owned() ); assert_eq!( human_readable_approx_duration(Duration::minutes(130), false), - "2 hours ago".to_owned() + "2 小时前".to_owned() ); assert_eq!( human_readable_approx_duration(Duration::days(4), false), - "4 days ago".to_owned() + "4 天前".to_owned() ); assert_eq!( human_readable_approx_duration(Duration::weeks(1), false), - "1 week ago".to_owned() + "1 周前".to_owned() ); assert_eq!( human_readable_approx_duration(Duration::weeks(15), false), - "3 months ago".to_owned() + "3 个月前".to_owned() ); assert_eq!( human_readable_approx_duration(Duration::weeks(520), false), - "9 years ago".to_owned() + "9 年前".to_owned() ); } diff --git a/app/src/util/tooltips.rs b/app/src/util/tooltips.rs index 4a48308d86..df8ef0dd3d 100644 --- a/app/src/util/tooltips.rs +++ b/app/src/util/tooltips.rs @@ -135,18 +135,16 @@ where redaction, TooltipRedaction::SecretNotSentToLLMMessaging { .. } ) { - "This wasn't included in the AI conversation." + i18n::t("util.tooltips.secret_not_included_ai_conversation") } else { - "This won't be included in any AI conversations or shared blocks." + i18n::t("util.tooltips.secret_not_included_ai_or_shared_blocks") }; // Generate the appropriate message based on secret level let secret_message = match secret_level { - Some(SecretLevel::Enterprise) => { - "Pattern matched your organization's secret redaction regex list." - } - Some(SecretLevel::User) => "Pattern matched your secret redaction regex list.", - None => "Pattern matched the secret redaction regex list.", + Some(SecretLevel::Enterprise) => i18n::t("util.tooltips.secret_pattern_enterprise"), + Some(SecretLevel::User) => i18n::t("util.tooltips.secret_pattern_user"), + None => i18n::t("util.tooltips.secret_pattern_generic"), }; tooltip.add_child( @@ -201,7 +199,7 @@ where .with_child( appearance .ui_builder() - .span("*Secrets are not sent to Warp's server.") + .span(i18n::t("util.tooltips.secrets_not_sent")) .with_style(UiComponentStyles { font_size: Some(12.), margin: Some(Coords::default().top(4.)), diff --git a/app/src/view_components/action_button.rs b/app/src/view_components/action_button.rs index f9a68b4c7d..670c0e11ad 100644 --- a/app/src/view_components/action_button.rs +++ b/app/src/view_components/action_button.rs @@ -532,7 +532,7 @@ impl ActionButton { Some( Container::new( Text::new_inline( - "Beta", + i18n::t("common.beta"), appearance.ui_font_family(), overall_height - padding.top() - padding.bottom(), ) diff --git a/app/src/view_components/feature_popup.rs b/app/src/view_components/feature_popup.rs index b04d18eac8..529ee4bffb 100644 --- a/app/src/view_components/feature_popup.rs +++ b/app/src/view_components/feature_popup.rs @@ -56,7 +56,7 @@ impl FeaturePopup { match self.badge { FeaturePopupBadge::New => Container::new( Text::new( - "NEW", + i18n::t("common.new_uppercase"), appearance.ui_font_family(), appearance.ui_font_size(), ) diff --git a/app/src/view_components/filterable_dropdown.rs b/app/src/view_components/filterable_dropdown.rs index c68ce6a4be..97f598347d 100644 --- a/app/src/view_components/filterable_dropdown.rs +++ b/app/src/view_components/filterable_dropdown.rs @@ -54,7 +54,7 @@ pub struct FilterableDropdown { selected_item: Option>, items: Vec>, orientation: FilterableDropdownOrientation, - static_menu_header: Option<&'static str>, + static_menu_header: Option, button_variant: ButtonVariant, style_override: Option, hovered_style_override: Option, @@ -106,7 +106,7 @@ where }, ctx, ); - editor.set_placeholder_text("Search", ctx); + editor.set_placeholder_text(i18n::t("common.search"), ctx); editor }); ctx.subscribe_to_view(&filter_editor, |me, _, event, ctx| { @@ -427,7 +427,7 @@ where } fn render_closed_top_bar(&self, appearance: &Appearance) -> Box { - let (selected_item_text, font_family_id) = match self.static_menu_header { + let (selected_item_text, font_family_id) = match self.static_menu_header.as_deref() { Some(header) => (header.to_string(), None), None => match self.selected_item.clone() { Some(MenuItem::Item(fields)) => { @@ -575,7 +575,7 @@ where let background_fill = appearance.theme().surface_2(); let empty_text = appearance .ui_builder() - .span("No matches found.") + .span(i18n::t("common.no_matches_found")) .with_style(UiComponentStyles { font_color: Some(appearance.theme().sub_text_color(background_fill).into()), ..Default::default() @@ -727,8 +727,8 @@ where }); } - pub fn set_menu_header_to_static(&mut self, header: &'static str) { - self.static_menu_header = Some(header); + pub fn set_menu_header_to_static(&mut self, header: impl Into) { + self.static_menu_header = Some(header.into()); } } diff --git a/app/src/view_components/find.rs b/app/src/view_components/find.rs index 56fd1f22df..344d58987f 100644 --- a/app/src/view_components/find.rs +++ b/app/src/view_components/find.rs @@ -40,13 +40,8 @@ pub(crate) const FIND_EDITOR_BORDER_WIDTH: f32 = 1.; const FIND_EDITOR_FONT_SIZE: f32 = 12.; pub const REGEX_TOGGLE_LABEL: &str = ". *"; -pub const REGEX_TOGGLE_TOOLTIP: &str = "Regex toggle"; pub const CASE_SENSITIVE_LABEL: &str = "Aa"; -pub const CASE_SENSITIVE_TOOLTIP: &str = "Case sensitive search"; - -pub const FIND_WITHIN_BLOCK_TOOLTIP: &str = "Find in selected block"; -pub const FIND_PLACEHOLDER_TEXT: &str = "Find"; // Moving FindEvent, FindModel implementations away from terminal/. pub enum FindEvent { @@ -134,7 +129,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "find:find_next_occurrence", - "Find the next occurrence of your search query", + i18n::t("find.binding.find_next_occurrence"), FindAction::CmdG, ) .with_context_predicate(id!("Find")) @@ -144,7 +139,7 @@ pub fn init(app: &mut AppContext) { .with_linux_or_windows_key_binding("f3"), EditableBinding::new( "find:find_prev_occurrence", - "Find the previous occurrence of your search query", + i18n::t("find.binding.find_previous_occurrence"), FindAction::CmdShiftG, ) .with_context_predicate(id!("Find")) @@ -168,7 +163,7 @@ impl + 'static> Find { }, ctx, ); - editor.set_placeholder_text(FIND_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text(&i18n::t("find.placeholder"), ctx); editor }); @@ -256,16 +251,17 @@ impl + 'static> Find { pub fn emit_result_a11y_content(&mut self, ctx: &mut ViewContext) { let content = if let Some(match_index) = self.model.as_ref(ctx).focused_match_index() { AccessibilityContent::new( - format!( - "Result {} of {}.", - match_index + 1, - self.model.as_ref(ctx).match_count() - ), - "Use enter and shift-enter to navigate between matches. Escape to quit.", + i18n::t("find.a11y.result_count") + .replace("{current}", &(match_index + 1).to_string()) + .replace("{total}", &self.model.as_ref(ctx).match_count().to_string()), + i18n::t("find.a11y.result_help"), WarpA11yRole::UserAction, ) } else { - AccessibilityContent::new_without_help("No results.", WarpA11yRole::UserAction) + AccessibilityContent::new_without_help( + i18n::t("find.a11y.no_results"), + WarpA11yRole::UserAction, + ) }; ctx.emit_a11y_content(content); } @@ -329,7 +325,7 @@ impl + 'static> Find { let label = if match_count > 0 { format!("{}+ ...", match_count) } else { - "Scanning...".to_string() + i18n::t("find.scanning") }; return Text::new_inline(label, appearance.ui_font_family(), FIND_EDITOR_FONT_SIZE) .with_color(blended_colors::text_sub( @@ -363,7 +359,7 @@ impl + 'static> Find { mouse_state_handle: MouseStateHandle, on_click_action: FindAction, size: f32, - tooltip_text: Option<&str>, + tooltip_text: Option, right_margin: f32, ) -> Box { Hoverable::new(mouse_state_handle, |state| { @@ -399,7 +395,7 @@ impl + 'static> Find { .finish(); let mut stack = Stack::new().with_child(icon); - if let (Some(tooltip_text), true) = (tooltip_text, state.is_hovered()) { + if let (Some(tooltip_text), true) = (tooltip_text.as_ref(), state.is_hovered()) { let tooltip = appearance .ui_builder() .tool_tip(tooltip_text.to_string()) @@ -520,8 +516,8 @@ impl + 'static> View for Find { fn accessibility_contents(&self, _: &AppContext) -> Option { Some(AccessibilityContent::new( - "Type searched phrase.", - "Press escape to quit, use enter and shift-enter to navigate between matches", + i18n::t("find.a11y.type_phrase"), + i18n::t("find.a11y.input_help"), WarpA11yRole::TextareaRole, )) } @@ -552,7 +548,7 @@ impl + 'static> View for Find { self.button_mouse_states.toggle_regex_search.clone(), FindAction::ToggleRegexSearch, editor_height, - Some(REGEX_TOGGLE_TOOLTIP), + Some(i18n::t("find.regex_toggle_tooltip")), ICON_PADDING, ); let case_sensitive_icon = Container::new( @@ -564,7 +560,7 @@ impl + 'static> View for Find { self.button_mouse_states.toggle_case_sensitivity.clone(), FindAction::ToggleCaseSensitivity, editor_height, - Some(CASE_SENSITIVE_TOOLTIP), + Some(i18n::t("find.case_sensitive_tooltip")), ICON_PADDING, ), "case_sensitive_button", @@ -581,7 +577,7 @@ impl + 'static> View for Find { self.button_mouse_states.toggle_find_in_block.clone(), FindAction::ToggleFindInBlock, editor_height, - Some(FIND_WITHIN_BLOCK_TOOLTIP), + Some(i18n::t("find.within_block_tooltip")), 0., ), "find_in_block_button", diff --git a/app/src/wasm_nux_dialog.rs b/app/src/wasm_nux_dialog.rs index 837ceb11cf..8bb167f98a 100644 --- a/app/src/wasm_nux_dialog.rs +++ b/app/src/wasm_nux_dialog.rs @@ -152,82 +152,84 @@ impl View for WasmNUXDialog { let dialog = if self.requested_download { Dialog::new( - "Open in Warp Desktop?".to_string(), - Some("Future links will automatically open on desktop.".to_string()), + i18n::t("wasm_nux.open_in_desktop_title"), + Some(i18n::t("wasm_nux.future_links_desktop")), dialog_styles, ) .with_bottom_row_child(Self::render_dialog_button( - "Open in Warp", + i18n::t("terminal.shared_session.open_in_warp"), WasmNUXDialogAction::OpenNativeAndClose, &self.confirm_mouse_state, appearance, )) } else if app_install_detected == &UserAppInstallStatus::NotDetected { - Dialog::new("Download Warp Desktop?".to_string(), None, dialog_styles) - .with_child( - Flex::column() - .with_cross_axis_alignment(CrossAxisAlignment::Stretch) - .with_main_axis_size(MainAxisSize::Min) - .with_child( + Dialog::new( + i18n::t("wasm_nux.download_desktop_title"), + None, + dialog_styles, + ) + .with_child( + Flex::column() + .with_cross_axis_alignment(CrossAxisAlignment::Stretch) + .with_main_axis_size(MainAxisSize::Min) + .with_child( + appearance + .ui_builder() + .span(i18n::t("wasm_nux.download_desktop_description")) + .with_style(UiComponentStyles { + font_weight: Some(Weight::Thin), + font_color: Some( + appearance + .theme() + .main_text_color(appearance.theme().surface_1()) + .into_solid(), + ), + ..Default::default() + }) + .with_soft_wrap() + .build() + .finish(), + ) + .with_child( + Align::new( appearance .ui_builder() - .span("Warp is the intelligent terminal with AI and your dev team's knowledge built-in.") - .with_style(UiComponentStyles { - font_weight: Some(Weight::Thin), - font_color: Some( - appearance - .theme() - .main_text_color(appearance.theme().surface_1()) - .into_solid(), - ), - ..Default::default() - }) - .with_soft_wrap() + .link( + i18n::t("common.learn_more"), + None, + Some(Box::new(|ctx| { + ctx.dispatch_typed_action(WasmNUXDialogAction::LearnMore) + })), + self.learn_more_mouse_state.clone(), + ) .build() .finish(), ) - .with_child( - Align::new( - appearance - .ui_builder() - .link( - "Learn more".to_string(), - None, - Some(Box::new(|ctx| { - ctx.dispatch_typed_action( - WasmNUXDialogAction::LearnMore, - ) - })), - self.learn_more_mouse_state.clone(), - ) - .build() - .finish(), - ) - .left() - .finish(), - ) + .left() .finish(), - ) - .with_bottom_row_child(Self::render_dialog_button( - "Download", - WasmNUXDialogAction::OpenDownloadDesktopAppLink, - &self.download_warp_mouse_state, - appearance, - )) + ) + .finish(), + ) + .with_bottom_row_child(Self::render_dialog_button( + i18n::t("common.download"), + WasmNUXDialogAction::OpenDownloadDesktopAppLink, + &self.download_warp_mouse_state, + appearance, + )) } else { let object_kind = match web_intent_parser::current_web_intent() { - Some(WebIntent::DriveObject(_)) => "Warp Drive objects", - Some(WebIntent::SessionView(_)) => "shared sessions", - _ => "Warp links", + Some(WebIntent::DriveObject(_)) => i18n::t("wasm_nux.object_kind.drive_objects"), + Some(WebIntent::SessionView(_)) => i18n::t("wasm_nux.object_kind.shared_sessions"), + _ => i18n::t("wasm_nux.object_kind.warp_links"), }; Dialog::new( - format!("Always open {object_kind} on the web?"), - Some("You can change this at any time in settings.".to_string()), + i18n::t("wasm_nux.always_open_on_web_title").replace("{object_kind}", &object_kind), + Some(i18n::t("wasm_nux.change_in_settings")), dialog_styles, ) .with_bottom_row_child(Self::render_dialog_button( - "Yes", + i18n::t("wasm_nux.yes"), WasmNUXDialogAction::SetWebAndClose, &self.confirm_mouse_state, appearance, diff --git a/app/src/window_settings.rs b/app/src/window_settings.rs index 2577de4f3c..bfad20095b 100644 --- a/app/src/window_settings.rs +++ b/app/src/window_settings.rs @@ -11,7 +11,7 @@ define_settings_group!(WindowSettings, settings: [ private: false, storage_key: "OverrideBlur", toml_path: "appearance.window.override_blur", - description: "The blur radius applied to the window background.", + description_key: "settings.schema.appearance.window.override_blur.description", }, background_blur_texture: BackgroundBlurTexture { type: bool, @@ -21,7 +21,7 @@ define_settings_group!(WindowSettings, settings: [ private: false, storage_key: "OverrideBlurTexture", toml_path: "appearance.window.override_blur_texture", - description: "Whether to apply a blur texture to the window background.", + description_key: "settings.schema.appearance.window.override_blur_texture.description", } background_opacity: BackgroundOpacity { type: u8, @@ -31,7 +31,7 @@ define_settings_group!(WindowSettings, settings: [ private: false, storage_key: "OverrideOpacity", toml_path: "appearance.window.override_opacity", - description: "The opacity of the window background, from 1 to 100 percent.", + description_key: "settings.schema.appearance.window.override_opacity.description", }, open_windows_at_custom_size: OpenWindowsAtCustomSize { type: bool, @@ -40,7 +40,7 @@ define_settings_group!(WindowSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "appearance.window.open_windows_at_custom_size", - description: "Whether to open new windows at a custom size instead of the default.", + description_key: "settings.schema.appearance.window.open_windows_at_custom_size.description", }, new_windows_num_columns: NewWindowsNumColumns { type: u16, @@ -49,7 +49,7 @@ define_settings_group!(WindowSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "appearance.window.new_windows_num_columns", - description: "The number of columns for new windows when using a custom size.", + description_key: "settings.schema.appearance.window.new_windows_num_columns.description", }, new_windows_num_rows: NewWindowsNumRows { type: u16, @@ -58,7 +58,7 @@ define_settings_group!(WindowSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "appearance.window.new_windows_num_rows", - description: "The number of rows for new windows when using a custom size.", + description_key: "settings.schema.appearance.window.new_windows_num_rows.description", }, left_panel_visibility_across_tabs: LeftPanelVisibilityAcrossTabs { type: bool, @@ -67,7 +67,7 @@ define_settings_group!(WindowSettings, settings: [ sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "appearance.window.left_panel_visibility_across_tabs", - description: "Whether the left panel visibility is shared across all tabs.", + description_key: "settings.schema.appearance.window.left_panel_visibility_across_tabs.description", }, zoom_level: ZoomLevel { type: u16, @@ -76,7 +76,7 @@ define_settings_group!(WindowSettings, settings: [ sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "appearance.window.zoom_level", - description: "The zoom level for the window, as a percentage.", + description_key: "settings.schema.appearance.window.zoom_level.description", }, ]); diff --git a/app/src/workflows/categories.rs b/app/src/workflows/categories.rs index ff4edb483f..99b869a675 100644 --- a/app/src/workflows/categories.rs +++ b/app/src/workflows/categories.rs @@ -119,7 +119,7 @@ impl WorkflowViewType { let mut container = Container::new( appearance .ui_builder() - .span(self.as_str(category_names).to_string()) + .span(self.display_name(category_names)) .with_style(UiComponentStyles { font_weight: Some(font_weight), font_color: Some(appearance.theme().main_text_color(bg_color).into_solid()), @@ -147,28 +147,28 @@ impl WorkflowViewType { .finish() } - fn as_str<'a>(&self, category_names: &'a [String]) -> &'a str { + fn display_name(&self, category_names: &[String]) -> String { match self { - WorkflowViewType::All => "All", - WorkflowViewType::LocalPersonal => "My Workflows", - WorkflowViewType::Project => "Repository Workflows", - WorkflowViewType::Team => "Team Workflows", - WorkflowViewType::Category { category_index, .. } => &category_names[*category_index], + WorkflowViewType::All => i18n::t("workflows.categories.all"), + WorkflowViewType::LocalPersonal => i18n::t("workflows.categories.my_workflows"), + WorkflowViewType::Project => i18n::t("workflows.categories.repository_workflows"), + WorkflowViewType::Team => i18n::t("workflows.categories.team_workflows"), + WorkflowViewType::Category { category_index, .. } => { + category_names[*category_index].clone() + } } } fn as_accessibility_contents(&self, category_names: &[String]) -> AccessibilityContent { let a11y_content = match self { WorkflowViewType::Category { .. } => { - format!( - "Showing workflows with category {}", - self.as_str(category_names) - ) + i18n::t("workflows.categories.a11y.showing_category") + .replace("{category}", &self.display_name(category_names)) } - WorkflowViewType::All => "Showing all workflows".into(), - WorkflowViewType::LocalPersonal => "Showing my workflows".into(), - WorkflowViewType::Project => "Showing project workflows".into(), - WorkflowViewType::Team => "Showing team workflows".into(), + WorkflowViewType::All => i18n::t("workflows.categories.a11y.showing_all"), + WorkflowViewType::LocalPersonal => i18n::t("workflows.categories.a11y.showing_mine"), + WorkflowViewType::Project => i18n::t("workflows.categories.a11y.showing_project"), + WorkflowViewType::Team => i18n::t("workflows.categories.a11y.showing_team"), }; AccessibilityContent::new_without_help(a11y_content, WarpA11yRole::UserAction) @@ -712,11 +712,15 @@ impl CategoriesView { if let Some(workflow_for_render) = self.filtered_workflows().nth(self.selected_workflow_index) { - let a11y_content_text = format!( - "Selected {} {}", - workflow_for_render.workflow_type.as_workflow().name(), - workflow_for_render.workflow_type.as_workflow().content() - ); + let a11y_content_text = i18n::t("workflows.categories.a11y.selected") + .replace( + "{name}", + workflow_for_render.workflow_type.as_workflow().name(), + ) + .replace( + "{content}", + workflow_for_render.workflow_type.as_workflow().content(), + ); ctx.emit_a11y_content(AccessibilityContent::new_without_help( a11y_content_text, WarpA11yRole::MenuItemRole, @@ -751,17 +755,20 @@ impl CategoriesView { } fn render_empty_list_placeholder(&self, appearance: &Appearance) -> Box { - let no_workflows_text = - CategoriesView::text_label("No matching workflows found.", appearance); + let no_workflows_text = CategoriesView::text_label( + i18n::t("workflows.categories.no_matching_workflows"), + appearance, + ); - let mut workflow_documentation_link_text = - Flex::row().with_child(CategoriesView::text_label("Try ", appearance)); + let mut workflow_documentation_link_text = Flex::row().with_child( + CategoriesView::text_label(i18n::t("common.try"), appearance), + ); workflow_documentation_link_text.add_child( appearance .ui_builder() .link( - "creating your own workflow".into(), + i18n::t("workflows.categories.create_your_own"), Some( "https://docs.warp.dev/knowledge-and-collaboration/warp-drive/workflows" .into(), @@ -940,7 +947,7 @@ impl CategoriesView { let theme = appearance.theme(); workflow_types_list.add_child( Container::new(Self::workflow_types_label( - "Categories", + i18n::t("workflows.categories.label"), Some(theme.sub_text_color(theme.surface_2()).into_solid()), appearance.ui_builder(), )) @@ -1207,8 +1214,8 @@ impl View for CategoriesView { fn accessibility_contents(&self, _: &AppContext) -> Option { Some(AccessibilityContent::new( - "Workflows", - "Search or use arrow up and arrow down keys to navigate and find a workflow. Use enter to confirm the workflow and esc to quit.", + i18n::t("workflows.categories.a11y.title"), + i18n::t("workflows.categories.a11y.help"), WarpA11yRole::MenuRole, )) } diff --git a/app/src/workflows/info_box.rs b/app/src/workflows/info_box.rs index f5275761ad..dfff2ae755 100644 --- a/app/src/workflows/info_box.rs +++ b/app/src/workflows/info_box.rs @@ -60,8 +60,6 @@ const ENV_VAR_HORIZONTAL_MARGIN: f32 = 20.; const ENV_VAR_RIGHT_ELEMENT_VERTICAL_MARGIN: f32 = 5.; const ENV_VAR_SPAN_VERTICAL_MARGIN: f32 = 15.; const ENV_VAR_BUTTON_HEIGHT: f32 = 30.; -const ENV_VAR_SPAN: &str = "Environment variables"; -const NEW_ENV_VAR_BUTTON_LABEL: &str = "New environment variables"; /// Scale factor the title should be from the user's current font size. const TITLE_FONT_SIZE_SCALE_FACTOR: f32 = 1.12; @@ -252,9 +250,9 @@ impl WorkflowsMoreInfoView { appearance: &Appearance, ) -> Box { let label = if cloud_workflow.model().data.is_agent_mode_workflow() { - "Edit prompt" + i18n::t("workflows.info.edit_prompt") } else { - "Edit workflow" + i18n::t("workflows.info.edit_workflow") }; let workflow = cloud_workflow.clone(); render_hoverable_card_button( @@ -438,7 +436,7 @@ impl WorkflowsMoreInfoView { .with_child( Container::new( Text::new_inline( - "Command edited.", + i18n::t("workflows.info.command_edited"), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -460,7 +458,7 @@ impl WorkflowsMoreInfoView { ButtonVariant::Text, self.button_mouse_states.reset_command.clone(), ) - .with_centered_text_label(String::from("Reset")) + .with_centered_text_label(i18n::t("common.reset")) .with_style(UiComponentStyles { font_family_id: Some(appearance.ui_font_family()), font_size: Some(appearance.monospace_font_size()), @@ -503,7 +501,7 @@ impl WorkflowsMoreInfoView { 1., Container::new( Text::new_inline( - "to cycle parameters", + i18n::t("workflows.info.cycle_parameters"), appearance.ui_font_family(), appearance.monospace_font_size(), ) @@ -537,7 +535,7 @@ impl WorkflowsMoreInfoView { let workflow = self.workflow.as_workflow().to_owned(); render_hoverable_card_button( icons::Icon::Workflow, - Some("Save as workflow".to_string()), + Some(i18n::t("workflows.info.save_as_workflow")), self.button_mouse_states.save_as_workflow.clone(), move |ctx, _, _| { ctx.dispatch_typed_action(TerminalAction::OpenWorkflowModalForAIWorkflow( @@ -569,7 +567,7 @@ impl WorkflowsMoreInfoView { Align::new( appearance .ui_builder() - .span(ENV_VAR_SPAN.to_string()) + .span(i18n::t("workflows.arguments.environment_variables")) .with_style(UiComponentStyles { font_size: Some(ENV_VAR_SPAN_FONT_SIZE), ..Default::default() @@ -596,7 +594,9 @@ impl WorkflowsMoreInfoView { ButtonVariant::Secondary, self.button_mouse_states.add_env_var_collection.clone(), ) - .with_centered_text_label(NEW_ENV_VAR_BUTTON_LABEL.to_owned()) + .with_centered_text_label(i18n::t( + "workflows.arguments.new_environment_variables", + )) .build() .on_click(|ctx, _, _| { // Create envvars in personal drive for max extensibility (can be moved @@ -1008,7 +1008,7 @@ impl WorkflowsMoreInfoView { appearance .ui_builder() .link( - "View Context".into(), + i18n::t("workflows.info.view_context"), Some(workflow_source), None, self.button_mouse_states.view_context.clone(), diff --git a/app/src/workflows/workflow_view.rs b/app/src/workflows/workflow_view.rs index b2b93c7015..a8f388a023 100644 --- a/app/src/workflows/workflow_view.rs +++ b/app/src/workflows/workflow_view.rs @@ -108,7 +108,7 @@ pub fn init(app: &mut AppContext) { use warpui::keymap::macros::id; app.register_editable_bindings([EditableBinding::new( "workflowview:save", - "Save workflow", + i18n::t("workflows.modal.save_workflow"), WorkflowAction::Save, ) .with_context_predicate(id!("WorkflowView")) @@ -116,7 +116,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( "Close Workflow", - "Close", + i18n::t("common.close"), WorkflowAction::Close, ) .with_custom_action(CustomAction::CloseCurrentSession) @@ -130,10 +130,6 @@ const WORKFLOW_PARAMETER_HIGHLIGHT_COLOR: u32 = 0x42C0FA4D; const MAX_ELEMENT_WIDTH: f32 = 800.; const SCROLLBAR_WIDTH: ScrollbarWidth = ScrollbarWidth::Auto; -const TITLE_PLACEHOLDER_TEXT: &str = "Add a title"; -const DESCRIPTION_PLACEHOLDER_TEXT: &str = "Add a description"; -const COMMAND_PLACEHOLDER_TEXT: &str = "echo \"Hello {{your_name}}\" # insert arguments with curly braces\n# enter a single-line command or an entire shell script"; -const AGENT_MODE_QUERY_PLACEHOLDER_TEXT: &str = "Enter your prompt here... (e.g., 'Create a function to sort an array of objects by date' or 'Help me debug this React component')."; const DESCRIPTION_MARGIN_TOP: f32 = 10.; const CORE_HORIZONATAL_MARGIN: f32 = 24.; @@ -155,26 +151,15 @@ const HORIZONTAL_TEXT_INPUT_PADDING: f32 = 10.; const EDITOR_FONT_SIZE: f32 = 14.; -const CREATE_BUTTON_TEXT: &str = "Create"; -const SAVE_BUTTON_TEXT: &str = "Update"; -const CANCEL_BUTTON_TEXT: &str = "Cancel"; const BUTTON_PADDING: f32 = 12.; const BUTTON_FONT_SIZE: f32 = 14.; const BUTTON_BORDER_RADIUS: f32 = 4.; const BUTTON_HEIGHT: f32 = 32.; const AI_ASSIST_BUTTON_SIZE: f32 = 92.; -const AI_ASSIST_BUTTON_TEXT: &str = "Autofill"; -const AI_ASSIST_LOADING_TEXT: &str = "Loading"; -const ALIAS_HELP_TEXT: &str = "Aliases allow you to create short strings to execute workflows. Each alias can have different argument values and environment variables, and aliases are personal to you."; - -const RUN_ON_DESKTOP_BUTTON_TEXT: &str = "Run in Warp"; const RUN_ON_DESKTOP_BUTTON_WIDTH: f32 = 108.; -const UNSAVED_CHANGES_TEXT: &str = "You have unsaved changes."; -const KEEP_EDITING_TEXT: &str = "Keep editing"; -const DISCARD_CHANGES_TEXT: &str = "Discard changes"; const DIALOG_WIDTH: f32 = 460.; const MODAL_HORIZONTAL_MARGIN: f32 = 28.; @@ -335,7 +320,8 @@ impl WorkflowView { impl WorkflowView { pub fn new_in_pane(ctx: &mut ViewContext) -> Self { - let pane_configuration = ctx.add_model(|_ctx| PaneConfiguration::new("Untitled")); + let pane_configuration = + ctx.add_model(|_ctx| PaneConfiguration::new(i18n::t("common.untitled"))); Self::new_internal(ctx, ContainerConfiguration::Pane(pane_configuration)) } @@ -352,12 +338,15 @@ impl WorkflowView { let header_font_size = appearance.header_font_size(); let ui_font_family = appearance.ui_font_family(); let monospace_font_family = appearance.monospace_font_family(); + let title_placeholder = i18n::t("workflows.editor.title_placeholder"); + let description_placeholder = i18n::t("workflows.editor.description_placeholder"); + let command_placeholder = i18n::t("workflows.editor.command_placeholder"); let name_editor = Self::create_editor_handle( ctx, Some(header_font_size), Some(ui_font_family), - Some(TITLE_PLACEHOLDER_TEXT), + Some(title_placeholder), false, true, true, @@ -367,7 +356,7 @@ impl WorkflowView { ctx, Some(EDITOR_FONT_SIZE), Some(ui_font_family), - Some(DESCRIPTION_PLACEHOLDER_TEXT), + Some(description_placeholder), false, false, true, @@ -377,7 +366,7 @@ impl WorkflowView { ctx, Some(EDITOR_FONT_SIZE), Some(monospace_font_family), - Some(COMMAND_PLACEHOLDER_TEXT), + Some(command_placeholder.clone()), true, false, true, @@ -387,7 +376,7 @@ impl WorkflowView { ctx, Some(EDITOR_FONT_SIZE), Some(monospace_font_family), - Some(COMMAND_PLACEHOLDER_TEXT), + Some(command_placeholder), true, false, true, @@ -487,7 +476,10 @@ impl WorkflowView { self.is_for_agent_mode = is_for_agent_mode; if is_for_agent_mode { self.content_editor.update(ctx, |editor, ctx| { - editor.set_placeholder_text(AGENT_MODE_QUERY_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text( + &i18n::t("workflows.editor.agent_prompt_placeholder"), + ctx, + ); editor.set_font_family(Appearance::as_ref(ctx).ui_font_family(), ctx); }); } @@ -510,7 +502,10 @@ impl WorkflowView { if is_for_agent_mode { self.content_editor.update(ctx, |editor, ctx| { - editor.set_placeholder_text(AGENT_MODE_QUERY_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text( + &i18n::t("workflows.editor.agent_prompt_placeholder"), + ctx, + ); }); } @@ -711,7 +706,10 @@ impl WorkflowView { self.is_for_agent_mode = workflow.model().data.is_agent_mode_workflow(); if self.is_for_agent_mode { self.content_editor.update(ctx, |editor, ctx| { - editor.set_placeholder_text(AGENT_MODE_QUERY_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text( + &i18n::t("workflows.editor.agent_prompt_placeholder"), + ctx, + ); editor.set_font_family(Appearance::as_ref(ctx).ui_font_family(), ctx); }); } @@ -803,7 +801,10 @@ impl WorkflowView { if self.is_for_agent_mode { self.content_editor.update(ctx, |editor, ctx| { - editor.set_placeholder_text(AGENT_MODE_QUERY_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text( + &i18n::t("workflows.editor.agent_prompt_placeholder"), + ctx, + ); }); } else { self.content_editor_highlight_model @@ -1578,7 +1579,7 @@ impl WorkflowView { fn save_aliases(&mut self, ctx: &mut ViewContext) { if let Err(e) = self.alias_bar.update(ctx, |bar, ctx| bar.save(ctx)) { log::error!("Error saving aliases: {e:?}"); - self.display_error_toast("Error saving aliases".to_string(), ctx); + self.display_error_toast(i18n::t("workflows.editor.error_saving_aliases"), ctx); } } @@ -1587,10 +1588,7 @@ impl WorkflowView { // Block saving if secrets are detected in the workflow when secret redaction is enabled. if self.workflow_contains_secrets(ctx) { - self.display_error_toast( - "This workflow cannot be saved because it contains secrets".to_string(), - ctx, - ); + self.display_error_toast(i18n::t("workflows.editor.cannot_save_with_secrets"), ctx); return; } @@ -1622,7 +1620,7 @@ impl WorkflowView { id } else { log::error!("No client_id obtained for creating workflow"); - self.display_error_toast(String::from("Could not create workflow"), ctx); + self.display_error_toast(i18n::t("workflows.editor.could_not_create"), ctx); return; }; @@ -1733,9 +1731,9 @@ impl WorkflowView { crate::workspace::ToastStack::handle(ctx).update(ctx, |stack, ctx| { stack.add_ephemeral_toast( DismissibleToast::success(if self.is_for_agent_mode { - "Prompt copied.".to_string() + i18n::t("workflows.editor.prompt_copied") } else { - "Command copied.".to_string() + i18n::t("workflows.editor.command_copied") }), window_id, ctx, @@ -1923,7 +1921,7 @@ impl WorkflowView { WorkflowViewMode::Edit => { let mode_text = appearance .ui_builder() - .span("Editing") + .span(i18n::t("common.editing")) .with_style(base_text_styles) .build(); let edit_button = accent_icon_button( @@ -1938,7 +1936,7 @@ impl WorkflowView { WorkflowViewMode::View => { let mode_text = appearance .ui_builder() - .span("Viewing") + .span(i18n::t("common.viewing")) .with_style(base_text_styles) .build(); let edit_button = icon_button( @@ -1958,7 +1956,7 @@ impl WorkflowView { let ui_builder = appearance.ui_builder().clone(); edit_button = edit_button.with_tooltip(move || { ui_builder - .tool_tip("Sign in to edit".to_string()) + .tool_tip(i18n::t("common.sign_in_to_edit")) .build() .finish() }); @@ -2173,11 +2171,7 @@ impl WorkflowView { .finish() } - fn render_section_header( - &self, - text: &'static str, - appearance: &Appearance, - ) -> Box { + fn render_section_header(&self, text: String, appearance: &Appearance) -> Box { Container::new( appearance .ui_builder() @@ -2219,7 +2213,7 @@ impl WorkflowView { if state.is_hovered() { let tooltip = ConstrainedBox::new( ui_builder - .tool_tip(ALIAS_HELP_TEXT.to_string()) + .tool_tip(i18n::t("workflows.editor.alias_help_tooltip")) .build() .finish(), ) @@ -2245,7 +2239,10 @@ impl WorkflowView { .with_children([ Flex::row() .with_children([ - self.render_section_header("Aliases", appearance), + self.render_section_header( + i18n::t("workflows.editor.aliases_section"), + appearance, + ), Container::new(help_icon).with_margin_left(4.).finish(), ]) .with_cross_axis_alignment(CrossAxisAlignment::Center) @@ -2272,7 +2269,7 @@ impl WorkflowView { padding: Some(Coords::uniform(BUTTON_PADDING)), ..Default::default() }) - .with_text_label(KEEP_EDITING_TEXT.into()) + .with_text_label(i18n::t("workflows.editor.keep_editing")) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { @@ -2292,7 +2289,7 @@ impl WorkflowView { padding: Some(Coords::uniform(BUTTON_PADDING)), ..Default::default() }) - .with_text_label(DISCARD_CHANGES_TEXT.into()) + .with_text_label(i18n::t("workflows.editor.discard_changes")) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { @@ -2302,7 +2299,7 @@ impl WorkflowView { Container::new( Dialog::new( - UNSAVED_CHANGES_TEXT.to_string(), + i18n::t("workflows.editor.unsaved_changes"), None, dialog_styles(appearance), ) @@ -2370,8 +2367,10 @@ impl WorkflowView { let mut save_button = self.build_footer_button( ButtonVariant::Accent, match self.workflow_view_mode { - WorkflowViewMode::Create => CREATE_BUTTON_TEXT.into(), - WorkflowViewMode::Edit | WorkflowViewMode::View => SAVE_BUTTON_TEXT.into(), + WorkflowViewMode::Create => i18n::t("workflows.editor.create"), + WorkflowViewMode::Edit | WorkflowViewMode::View => { + i18n::t("workflows.editor.update") + } }, None, self.ui_state_handles.save_workflow_state.clone(), @@ -2390,7 +2389,7 @@ impl WorkflowView { let mut cancel_button = self.build_footer_button( ButtonVariant::Secondary, - CANCEL_BUTTON_TEXT.into(), + i18n::t("common.cancel"), None, self.ui_state_handles.cancel_mouse_state.clone(), appearance, @@ -2415,8 +2414,12 @@ impl WorkflowView { let mut button_row = Flex::row(); let label_and_icon = match self.ai_metadata_assist_state { - AiAssistState::PreRequest => Some((AI_ASSIST_BUTTON_TEXT, Icon::AiAssistant)), - AiAssistState::RequestInFlight => Some((AI_ASSIST_LOADING_TEXT, Icon::Refresh)), + AiAssistState::PreRequest => { + Some((i18n::t("workflows.editor.autofill"), Icon::AiAssistant)) + } + AiAssistState::RequestInFlight => { + Some((i18n::t("workflows.editor.loading"), Icon::Refresh)) + } AiAssistState::Generated => None, }; @@ -2429,7 +2432,7 @@ impl WorkflowView { let mut button = self .build_footer_button( ButtonVariant::Secondary, - label.to_string(), + label, Some((icon, TextAndIconAlignment::TextFirst)), self.ui_state_handles.ai_assist_state.clone(), appearance, @@ -2450,7 +2453,7 @@ impl WorkflowView { .finish(); let button_with_tool_tip = appearance.ui_builder().tool_tip_on_element( - "Generate a title, descriptions, or parameters with Warp AI".to_string(), + i18n::t("workflows.editor.autofill_tooltip"), self.ui_state_handles.ai_assist_tool_tip.clone(), rendered_button, ParentAnchor::TopMiddle, @@ -2490,7 +2493,7 @@ impl WorkflowView { let run_on_desktop_button = self .build_footer_button( ButtonVariant::Accent, - RUN_ON_DESKTOP_BUTTON_TEXT.to_string(), + i18n::t("workflows.editor.run_in_warp"), Some((Icon::Laptop, TextAndIconAlignment::IconFirst)), // Reuse the execute button's handle since it's only shown if running workflows is // supported. @@ -2534,7 +2537,7 @@ impl WorkflowView { ctx: &mut ViewContext, font_size_override: Option, font_family_override: Option, - placeholder_text: Option<&str>, + placeholder_text: Option, supports_vim_mode: bool, single_line: bool, soft_wrap: bool, @@ -2585,7 +2588,7 @@ impl WorkflowView { editor.set_autogrow(soft_wrap); if let Some(text) = placeholder_text { - editor.set_placeholder_text(text, ctx); + editor.set_placeholder_text(&text, ctx); } editor @@ -2633,10 +2636,7 @@ impl WorkflowView { environment_variables: None, }; - send_telemetry_from_ctx!( - TelemetryEvent::AutoGenerateMetadataSuccess, - ctx - ); + send_telemetry_from_ctx!(TelemetryEvent::AutoGenerateMetadataSuccess, ctx); pane.populate_missing_field_with_suggestion(workflow, ctx); ctx.notify(); @@ -2648,30 +2648,31 @@ impl WorkflowView { if let Some(team) = UserWorkspaces::as_ref(ctx).current_team() { let current_user_email = pane.auth_state.user_email().unwrap_or_default(); - let has_admin_permissions = team.has_admin_permissions(¤t_user_email); + let has_admin_permissions = + team.has_admin_permissions(¤t_user_email); if team.billing_metadata.can_upgrade_to_higher_tier_plan() { if has_admin_permissions { - pane.display_upgrade_error(Some(team.uid), current_user_id, ctx); + pane.display_upgrade_error( + Some(team.uid), + current_user_id, + ctx, + ); } else { pane.display_error_toast( - "Looks like you're out of AI credits. Contact a team admin to upgrade for more credits.".to_string(), + i18n::t( + "workflow.toast.out_of_ai_credits_contact_admin", + ), ctx, ); } } else { - pane.display_error_toast( - message.clone(), - ctx, - ); + pane.display_error_toast(message.clone(), ctx); } } else { pane.display_upgrade_error(None, current_user_id, ctx); } } else { - pane.display_error_toast( - message.clone(), - ctx, - ); + pane.display_error_toast(message.clone(), ctx); } send_telemetry_from_ctx!( @@ -2689,7 +2690,7 @@ impl WorkflowView { AIRequestUsageModel::handle(ctx).update(ctx, |request_usage_model, ctx| { request_usage_model.refresh_request_usage_async(ctx); }); - } + }, ); self.ai_metadata_assist_state = AiAssistState::RequestInFlight; @@ -2709,15 +2710,16 @@ impl WorkflowView { let window_id = ctx.window_id(); let toast_link = if self.auth_state.is_anonymous_or_logged_out() { - ToastLink::new("Upgrade for more credits.".into()) + ToastLink::new(i18n::t("workflow.toast.upgrade_for_more_credits")) .with_onclick_action(WorkspaceAction::AttemptLoginGatedAIUpgrade) } else { - ToastLink::new("Upgrade for more credits.".into()).with_href(upgrade_link) + ToastLink::new(i18n::t("workflow.toast.upgrade_for_more_credits")) + .with_href(upgrade_link) }; crate::workspace::ToastStack::handle(ctx).update(ctx, |stack, ctx| { stack.add_ephemeral_toast( - DismissibleToast::error("Looks like you're out of AI credits.".into()) + DismissibleToast::error(i18n::t("workflow.toast.out_of_ai_credits")) .with_link(toast_link), window_id, ctx, @@ -2816,9 +2818,9 @@ impl WorkflowView { let appearance = Appearance::as_ref(app); let text = if deleted { - "You no longer have access to this workflow" + i18n::t("workflows.editor.access_removed") } else { - "Workflow moved to trash" + i18n::t("workflows.editor.moved_to_trash") }; let mut stack = Stack::new(); @@ -2872,11 +2874,11 @@ impl WorkflowView { ) .with_tooltip(move || { ui_builder - .tool_tip("Restore workflow from trash".to_string()) + .tool_tip(i18n::t("workflows.restore_from_trash")) .build() .finish() }) - .with_text_label("Restore".to_string()) + .with_text_label(i18n::t("common.restore")) .build() .on_click(|ctx, _, _| ctx.dispatch_typed_action(WorkflowAction::Untrash)) .finish(), @@ -3186,7 +3188,7 @@ impl BackingView for WorkflowView { // Add "Copy Link" to menu if let Some(link) = self.workflow_link(ctx) { menu_items.push( - MenuItemFields::new("Copy link") + MenuItemFields::new(i18n::t("common.copy_link")) .with_on_select_action(WorkflowAction::CopyLink(link)) .with_icon(Icon::Link) .into_item(), @@ -3197,7 +3199,7 @@ impl BackingView for WorkflowView { if let Some(link) = self.workflow_link(ctx) { if let Ok(url) = Url::parse(&link) { menu_items.push( - MenuItemFields::new("Open on Desktop") + MenuItemFields::new(i18n::t("common.open_on_desktop")) .with_on_select_action(WorkflowAction::OpenLinkOnDesktop(url)) .with_icon(Icon::Laptop) .into_item(), @@ -3211,7 +3213,7 @@ impl BackingView for WorkflowView { // Add "Duplicate" to menu if space != Some(Space::Shared) { menu_items.push( - MenuItemFields::new("Duplicate") + MenuItemFields::new(i18n::t("common.duplicate")) .with_on_select_action(WorkflowAction::Duplicate) .with_icon(Icon::Duplicate) .into_item(), @@ -3224,7 +3226,7 @@ impl BackingView for WorkflowView { && (!FeatureFlag::SharedWithMe.is_enabled() || access_level.can_trash()) { menu_items.push( - MenuItemFields::new("Trash") + MenuItemFields::new(i18n::t("common.trash")) .with_on_select_action(WorkflowAction::Trash) .with_icon(Icon::Trash) .into_item(), diff --git a/app/src/workflows/workflow_view/alias_bar.rs b/app/src/workflows/workflow_view/alias_bar.rs index 5a18b0a33f..6ff5491dd5 100644 --- a/app/src/workflows/workflow_view/alias_bar.rs +++ b/app/src/workflows/workflow_view/alias_bar.rs @@ -98,7 +98,7 @@ impl AliasBar { }, ctx, ); - view.set_placeholder_text("alias name", ctx); + view.set_placeholder_text(i18n::t("workflows.alias.name_placeholder"), ctx); view }); @@ -431,7 +431,7 @@ impl View for AliasBar { |_state, background| { appearance .ui_builder() - .span("Default") + .span(i18n::t("common.default")) .with_style(UiComponentStyles { font_color: Some( appearance.theme().main_text_color(background).into_solid(), @@ -467,7 +467,7 @@ impl View for AliasBar { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Add alias", + i18n::t("workflows.alias.add_alias"), Icon::Plus.to_warpui_icon( appearance .theme() diff --git a/app/src/workflows/workflow_view/argument_editor.rs b/app/src/workflows/workflow_view/argument_editor.rs index e815c496e7..d096dad6a8 100644 --- a/app/src/workflows/workflow_view/argument_editor.rs +++ b/app/src/workflows/workflow_view/argument_editor.rs @@ -35,12 +35,8 @@ use crate::workflows::workflow::Workflow; use crate::workspace::WorkspaceAction; const ARGUMENT_INPUT_HEIGHT: f32 = 30.; -const ARGUMENT_LABEL_TEXT: &str = "Arguments"; const ARGUMENT_LABEL_HEIGHT: f32 = 20.; const ARGUMENT_LABEL_MARGIN_BOTTOM: f32 = 5.; -const ARGUMENT_DESCRIPTION_PLACEHOLDER_TEXT: &str = "Description"; -const ARGUMENT_ALIAS_DESCRIPTION_PLACEHOLDER_TEXT: &str = "Value (optional)"; -const ARGUMENT_DEFAULT_VALUE_PLACEHOLDER_TEXT: &str = "Default value (optional)"; pub const DEFAULT_ARGUMENT_PREFIX: &str = "argument"; /// Width of the argument editor in alias mode. @@ -120,7 +116,7 @@ impl WorkflowView { ctx, Some(EDITOR_FONT_SIZE), Some(ui_font_family), - Some(ARGUMENT_DESCRIPTION_PLACEHOLDER_TEXT), + Some(i18n::t("workflows.modal.argument_description_placeholder")), false, /* vim_keybindings */ true, false, @@ -137,7 +133,9 @@ impl WorkflowView { ctx, Some(EDITOR_FONT_SIZE), Some(ui_font_family), - Some(ARGUMENT_DEFAULT_VALUE_PLACEHOLDER_TEXT), + Some(i18n::t( + "workflows.modal.argument_default_value_placeholder", + )), false, /* vim_keybindings */ true, false, @@ -563,7 +561,7 @@ impl WorkflowView { arguments_section_row.add_child( Shrinkable::new( 2., - self.render_section_header(ARGUMENT_LABEL_TEXT, appearance), + self.render_section_header(i18n::t("workflows.arguments.section"), appearance), ) .finish(), ); @@ -582,7 +580,7 @@ impl WorkflowView { ) .with_tooltip(move || { ui_builder - .tool_tip("Add a workflow argument".to_string()) + .tool_tip(i18n::t("workflows.argument.add_tooltip")) .build() .finish() }) @@ -591,26 +589,27 @@ impl WorkflowView { .finish(), ) } else { - arguments_section_row.add_child(Shrinkable::new( + arguments_section_row.add_child( + Shrinkable::new( 1., Container::new( appearance - .ui_builder() - .span("Fill out the arguments in this workflow and copy it to run in your terminal session") - .with_soft_wrap() - .with_style(UiComponentStyles { - font_size: Some(EDITOR_FONT_SIZE), - font_color: Some(sub_text_color), - ..Default::default() - }) - .build() - .finish(), + .ui_builder() + .span(i18n::t("workflows.arguments.description")) + .with_soft_wrap() + .with_style(UiComponentStyles { + font_size: Some(EDITOR_FONT_SIZE), + font_color: Some(sub_text_color), + ..Default::default() + }) + .build() + .finish(), ) .with_margin_left(40.) - .finish() + .finish(), ) - .finish() - ); + .finish(), + ); } arguments_section_row.finish() @@ -771,7 +770,8 @@ impl WorkflowView { // If the description is empty, show a placeholder text. if current_description.is_empty() { - current_description.push_str(ARGUMENT_ALIAS_DESCRIPTION_PLACEHOLDER_TEXT); + current_description + .push_str(&i18n::t("workflows.arguments.alias_value_placeholder")); styles.font_color = Some(theme.sub_text_color(theme.background()).into_solid()); } @@ -820,7 +820,7 @@ impl WorkflowView { .add_environment_variables_mouse_state .clone(), ) - .with_centered_text_label("Add environment variables".to_string()) + .with_centered_text_label(i18n::t("workflows.arguments.add_environment_variables")) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(WorkspaceAction::CreatePersonalEnvVarCollection); @@ -832,7 +832,7 @@ impl WorkflowView { .with_children([ appearance .ui_builder() - .span("Environment variables") + .span(i18n::t("workflows.arguments.environment_variables")) .with_style(UiComponentStyles { font_size: Some(13.), ..Default::default() diff --git a/app/src/workflows/workflow_view/env_var_selector.rs b/app/src/workflows/workflow_view/env_var_selector.rs index b752f4b2dc..3c10e4e2c2 100644 --- a/app/src/workflows/workflow_view/env_var_selector.rs +++ b/app/src/workflows/workflow_view/env_var_selector.rs @@ -85,7 +85,7 @@ impl EnvVarSelector { env_vars.sort_unstable_by(|a, b| a.0.cmp(&b.0)); let remove_item = std::iter::once(DropdownItem::new( - "None", + i18n::t("common.none"), EnvVarSelectorAction::Select(None), )); diff --git a/app/src/workspace/bonus_grant_notification_model.rs b/app/src/workspace/bonus_grant_notification_model.rs index e138957cda..21d1cd6f24 100644 --- a/app/src/workspace/bonus_grant_notification_model.rs +++ b/app/src/workspace/bonus_grant_notification_model.rs @@ -106,13 +106,12 @@ impl BonusGrantNotificationModel { fn format_generic_grant_message(grant: &BonusGrant) -> String { let scope_text = match grant.scope { - BonusGrantScope::User => "account", - BonusGrantScope::Workspace(_) => "team", + BonusGrantScope::User => i18n::t("workspace.bonus_grant.scope.account"), + BonusGrantScope::Workspace(_) => i18n::t("workspace.bonus_grant.scope.team"), }; - format!( - "{} Reload Credits have been added to your {}.", - grant.request_credits_granted, scope_text - ) + i18n::t("workspace.bonus_grant.reload_credits_added") + .replace("{credits}", &grant.request_credits_granted.to_string()) + .replace("{scope}", &scope_text) } fn create_grant_key(grant: &BonusGrant) -> String { diff --git a/app/src/workspace/close_session_confirmation_dialog.rs b/app/src/workspace/close_session_confirmation_dialog.rs index a24730cdad..69fcf40bb9 100644 --- a/app/src/workspace/close_session_confirmation_dialog.rs +++ b/app/src/workspace/close_session_confirmation_dialog.rs @@ -89,7 +89,10 @@ impl View for CloseSessionConfirmationDialog { let dont_show_again_checkbox = appearance .ui_builder() .checkbox(self.dont_show_again_mouse_state.clone(), Some(14.)) - .with_label(Span::new("Don't show again.", Default::default())) + .with_label(Span::new( + i18n::t("workspace.dialog.dont_show_again"), + Default::default(), + )) .check(self.dont_show_again) .build() .with_cursor(Cursor::PointingHand) @@ -102,7 +105,7 @@ impl View for CloseSessionConfirmationDialog { let close_session_button = appearance .ui_builder() .button(ButtonVariant::Accent, self.confirm_mouse_state.clone()) - .with_centered_text_label("Close session".into()) + .with_centered_text_label(i18n::t("workspace.dialog.close_session_button")) .with_style(button_style) .build() .with_cursor(Cursor::PointingHand) @@ -116,7 +119,7 @@ impl View for CloseSessionConfirmationDialog { let cancel_button = appearance .ui_builder() .button(ButtonVariant::Basic, self.cancel_mouse_state.clone()) - .with_centered_text_label("Cancel".into()) + .with_centered_text_label(i18n::t("workspace.dialog.cancel_button")) .with_style(button_style) .build() .with_cursor(Cursor::PointingHand) @@ -127,11 +130,8 @@ impl View for CloseSessionConfirmationDialog { let dialog = Container::new( Dialog::new( - "Close session?".into(), - Some( - "You are about to close a session that is currently being shared. Closing it will end sharing for everyone." - .into(), - ), + i18n::t("workspace.dialog.close_session_title").into(), + Some(i18n::t("workspace.dialog.close_session_shared_body").into()), UiComponentStyles { width: Some(460.), padding: Some(Coords::uniform(24.)), @@ -142,7 +142,7 @@ impl View for CloseSessionConfirmationDialog { .with_bottom_row_child(cancel_button) .with_bottom_row_child(close_session_button) .build() - .finish() + .finish(), ) .with_margin_top(35.) .finish(); diff --git a/app/src/workspace/delete_conversation_confirmation_dialog.rs b/app/src/workspace/delete_conversation_confirmation_dialog.rs index 44c3c7111d..b73a52915d 100644 --- a/app/src/workspace/delete_conversation_confirmation_dialog.rs +++ b/app/src/workspace/delete_conversation_confirmation_dialog.rs @@ -52,14 +52,14 @@ pub struct DeleteConversationConfirmationDialog { impl DeleteConversationConfirmationDialog { pub fn new(ctx: &mut ViewContext) -> Self { let cancel_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Cancel", NakedTheme).on_click(|ctx| { + ActionButton::new(i18n::t("common.cancel"), NakedTheme).on_click(|ctx| { ctx.dispatch_typed_action(DeleteConversationConfirmationAction::Cancel); }) }); let enter_keystroke = Keystroke::parse("enter").expect("Valid keystroke"); let delete_button = ctx.add_typed_action_view(|ctx| { - ActionButton::new("Delete", DangerPrimaryTheme) + ActionButton::new(i18n::t("common.delete"), DangerPrimaryTheme) .with_keybinding(KeystrokeSource::Fixed(enter_keystroke), ctx) .on_click(|ctx| { ctx.dispatch_typed_action(DeleteConversationConfirmationAction::Confirm); @@ -101,15 +101,15 @@ impl View for DeleteConversationConfirmationDialog { let title = self .source .as_ref() - .map(|s| format!("Delete '{}'?", s.conversation_title)) - .unwrap_or_else(|| "Delete conversation?".into()); + .map(|s| { + i18n::t("workspace.dialog.delete_conversation_title_named") + .replace("{title}", &s.conversation_title) + }) + .unwrap_or_else(|| i18n::t("workspace.dialog.delete_conversation_title")); let dialog = Dialog::new( title, - Some( - "This conversation will be permanently deleted. This action cannot be undone." - .into(), - ), + Some(i18n::t("workspace.dialog.delete_conversation_body").into()), UiComponentStyles { width: Some(DIALOG_WIDTH), ..dialog_styles(appearance) diff --git a/app/src/workspace/header_toolbar_editor.rs b/app/src/workspace/header_toolbar_editor.rs index b7c938ddde..503edb3d00 100644 --- a/app/src/workspace/header_toolbar_editor.rs +++ b/app/src/workspace/header_toolbar_editor.rs @@ -13,8 +13,6 @@ use crate::workspace::tab_settings::{ }; use crate::{report_if_error, Appearance}; -const MODAL_TITLE: &str = "Edit toolbar"; - pub fn init(app: &mut AppContext) { use warpui::keymap::macros::*; @@ -242,10 +240,11 @@ impl View for HeaderToolbarInlineEditor { fn render(&self, app: &AppContext) -> Box { let appearance = Appearance::as_ref(app); + let available_section_label = i18n::t("workspace.toolbar.available_items"); render_chip_editor_sections( &self.chip_configurator, ChipEditorSectionsConfig { - available_section_label: "Available items", + available_section_label: available_section_label.as_str(), is_at_defaults: is_toolbar_editor_at_defaults(&self.chip_configurator), reset_action: HeaderToolbarInlineEditorAction::ResetDefault, activate_action: HeaderToolbarInlineEditorAction::Activate, @@ -336,11 +335,13 @@ impl View for HeaderToolbarEditorModal { fn render(&self, app: &AppContext) -> Box { let appearance = Appearance::as_ref(app); + let title = i18n::t("workspace.toolbar.modal_title"); + let available_section_label = i18n::t("workspace.toolbar.available_items"); render_chip_editor_modal( &self.chip_configurator, ChipEditorModalConfig { - title: MODAL_TITLE, - available_section_label: "Available items", + title: title.as_str(), + available_section_label: available_section_label.as_str(), is_at_defaults: self.is_at_defaults(), is_dirty: self.is_dirty, cancel_action: HeaderToolbarEditorAction::Cancel, @@ -357,9 +358,8 @@ impl View for HeaderToolbarEditorModal { fn build_configurable_item(kind: &HeaderToolbarItemKind) -> ConfigurableItem { let id = serde_json::to_string(kind).expect("HeaderToolbarItemKind is serializable"); - let renderer = - ControlItemRenderer::new_with_label_and_icon(kind.display_label().to_string(), kind.icon()) - .with_identifier(id); + let renderer = ControlItemRenderer::new_with_label_and_icon(kind.display_label(), kind.icon()) + .with_identifier(id); let renderer = match kind { HeaderToolbarItemKind::TabsPanel => renderer.non_removable(), _ => renderer, diff --git a/app/src/workspace/header_toolbar_item.rs b/app/src/workspace/header_toolbar_item.rs index 961c2dc7c3..cfe1210e7a 100644 --- a/app/src/workspace/header_toolbar_item.rs +++ b/app/src/workspace/header_toolbar_item.rs @@ -34,13 +34,13 @@ pub enum HeaderToolbarItemKind { } impl HeaderToolbarItemKind { - pub fn display_label(&self) -> &'static str { + pub fn display_label(&self) -> String { match self { - Self::TabsPanel => "Tabs Panel", - Self::ToolsPanel => "Tools Panel", - Self::AgentManagement => "Agent Management", - Self::CodeReview => "Code Review", - Self::NotificationsMailbox => "Notifications", + Self::TabsPanel => i18n::t("workspace.toolbar.item.tabs_panel"), + Self::ToolsPanel => i18n::t("workspace.toolbar.item.tools_panel"), + Self::AgentManagement => i18n::t("workspace.toolbar.item.agent_management"), + Self::CodeReview => i18n::t("workspace.toolbar.item.code_review"), + Self::NotificationsMailbox => i18n::t("workspace.toolbar.item.notifications"), } } diff --git a/app/src/workspace/hoa_onboarding/hoa_onboarding_flow.rs b/app/src/workspace/hoa_onboarding/hoa_onboarding_flow.rs index 7f208ce849..020abbb40c 100644 --- a/app/src/workspace/hoa_onboarding/hoa_onboarding_flow.rs +++ b/app/src/workspace/hoa_onboarding/hoa_onboarding_flow.rs @@ -207,15 +207,18 @@ impl HoaOnboardingFlow { }); let cta_button = ctx.add_view(|_ctx| { - ActionButton::new("See what's new", HoaWelcomeModalButtonTheme) - .with_full_width(true) - .on_click(|ctx| ctx.dispatch_typed_action(HoaOnboardingAction::AdvanceFromWelcome)) + ActionButton::new( + i18n::t("workspace.hoa.see_whats_new"), + HoaWelcomeModalButtonTheme, + ) + .with_full_width(true) + .on_click(|ctx| ctx.dispatch_typed_action(HoaOnboardingAction::AdvanceFromWelcome)) }); let enter = Keystroke::parse("enter").unwrap_or_default(); let next_vtabs_button = ctx.add_view(|ctx| { - ActionButton::new("Next", HoaPrimaryButtonTheme) + ActionButton::new(i18n::t("common.next"), HoaPrimaryButtonTheme) .with_keybinding(KeystrokeSource::Fixed(enter.clone()), ctx) .on_click(|ctx| { ctx.dispatch_typed_action(HoaOnboardingAction::AdvanceFromVerticalTabs) @@ -223,19 +226,19 @@ impl HoaOnboardingFlow { }); let dismiss_vtabs_button = ctx.add_view(|ctx| { - ActionButton::new("Dismiss", HoaPrimaryButtonTheme) + ActionButton::new(i18n::t("common.dismiss"), HoaPrimaryButtonTheme) .with_keybinding(KeystrokeSource::Fixed(enter.clone()), ctx) .on_click(|ctx| ctx.dispatch_typed_action(HoaOnboardingAction::Dismiss)) }); let next_inbox_button = ctx.add_view(|ctx| { - ActionButton::new("Next", HoaPrimaryButtonTheme) + ActionButton::new(i18n::t("common.next"), HoaPrimaryButtonTheme) .with_keybinding(KeystrokeSource::Fixed(enter.clone()), ctx) .on_click(|ctx| ctx.dispatch_typed_action(HoaOnboardingAction::AdvanceFromInbox)) }); let finish_button = ctx.add_view(|ctx| { - ActionButton::new("Finish", HoaPrimaryButtonTheme) + ActionButton::new(i18n::t("common.finish"), HoaPrimaryButtonTheme) .with_keybinding(KeystrokeSource::Fixed(enter), ctx) .on_click(|ctx| ctx.dispatch_typed_action(HoaOnboardingAction::Finish)) }); @@ -373,8 +376,8 @@ impl HoaOnboardingFlow { fn render_callout_content( &self, - title: &'static str, - description: &'static str, + title: String, + description: String, extra_child: Option>, button: &ViewHandle, appearance: &Appearance, @@ -424,7 +427,7 @@ impl HoaOnboardingFlow { .finish(); let checkbox_label = Text::new_inline( - "Switch back to horizontal tabs".to_string(), + i18n::t("workspace.hoa.switch_back_horizontal_tabs"), appearance.ui_font_family(), 12., ) @@ -445,8 +448,8 @@ impl HoaOnboardingFlow { }; self.render_callout_content( - "Introducing vertical tabs - the new default", - "Vertical tabs show all open agent and terminal panes, grouped by tab. Customize what information you want to see to support your workflow.", + i18n::t("workspace.hoa.vertical_tabs_callout.title"), + i18n::t("workspace.hoa.vertical_tabs_callout.description"), Some(checkbox_row), button, appearance, @@ -455,7 +458,7 @@ impl HoaOnboardingFlow { fn render_inbox_callout(&self, appearance: &Appearance) -> Box { let title = Text::new( - "Meet your new agent inbox", + i18n::t("workspace.hoa.agent_inbox_title"), appearance.ui_font_family(), 16., ) @@ -465,7 +468,7 @@ impl HoaOnboardingFlow { // Build the description with an inline "Learn more" hyperlink. let learn_more_fragment = FormattedTextFragment { - text: "Learn more".into(), + text: i18n::t("common.learn_more"), styles: FormattedTextStyles { underline: true, hyperlink: Some(Hyperlink::Url( @@ -476,9 +479,7 @@ impl HoaOnboardingFlow { }; let formatted = FormattedText::new([FormattedTextLine::Line(vec![ - FormattedTextFragment::plain_text( - "Warp pipes through notifications from any CLI coding agent into a unified notification center that works across all coding agents and harnesses. ", - ), + FormattedTextFragment::plain_text(i18n::t("workspace.hoa.agent_inbox_description")), learn_more_fragment, ])]); diff --git a/app/src/workspace/hoa_onboarding/tab_config_step.rs b/app/src/workspace/hoa_onboarding/tab_config_step.rs index 10e5941953..2ca25489e9 100644 --- a/app/src/workspace/hoa_onboarding/tab_config_step.rs +++ b/app/src/workspace/hoa_onboarding/tab_config_step.rs @@ -55,7 +55,7 @@ where { let callout_bg = callout_background_fill(appearance).into_solid(); let title = Text::new( - "Create your first tab config", + i18n::t("tab_configs.create_first_config"), appearance.ui_font_family(), 16., ) @@ -64,7 +64,7 @@ where .finish(); let description = Text::new( - "Set up a reusable starting point for your tabs. Pick a repo, choose a session type, and optionally attach a worktree. Use it whenever you want to open a tab with this setup.", + i18n::t("workspace.hoa.tab_config_description"), appearance.ui_font_family(), 14., ) diff --git a/app/src/workspace/hoa_onboarding/welcome_banner.rs b/app/src/workspace/hoa_onboarding/welcome_banner.rs index 94aa5f687b..24cd6a451a 100644 --- a/app/src/workspace/hoa_onboarding/welcome_banner.rs +++ b/app/src/workspace/hoa_onboarding/welcome_banner.rs @@ -20,30 +20,30 @@ const HERO_IMAGE_PATH: &str = "async/png/onboarding/hoa_welcome_banner.png"; struct FeatureItem { icon: Icon, - title: &'static str, - description: &'static str, + title_key: &'static str, + description_key: &'static str, } const FEATURE_ITEMS: &[FeatureItem] = &[ FeatureItem { icon: Icon::LayoutAlt01, - title: "Vertical tabs", - description: "Rich tab titles and metadata like git branch, worktree, and PR. Fully customizable.", + title_key: "workspace.hoa.feature.vertical_tabs.title", + description_key: "workspace.hoa.feature.vertical_tabs.description", }, FeatureItem { icon: Icon::Sliders, - title: "Tab configs", - description: "Tab-level schema to set your directory, startup commands, theme, and worktree with one click", + title_key: "workspace.hoa.feature.tab_configs.title", + description_key: "workspace.hoa.feature.tab_configs.description", }, FeatureItem { icon: Icon::Inbox, - title: "Agent inbox", - description: "Notifications when any agent needs your attention, also accessible in a central inbox", + title_key: "workspace.hoa.feature.agent_inbox.title", + description_key: "workspace.hoa.feature.agent_inbox.description", }, FeatureItem { icon: Icon::MessageCheckSquare, - title: "Native code review", - description: "Send inline comments from Warp's code review directly to Claude Code, Codex, or OpenCode", + title_key: "workspace.hoa.feature.native_code_review.title", + description_key: "workspace.hoa.feature.native_code_review.description", }, ]; @@ -84,7 +84,7 @@ pub fn render_welcome_banner( ); // "New" badge - let text = Text::new_inline("New".to_string(), appearance.ui_font_family(), 14.) + let text = Text::new_inline(i18n::t("common.new"), appearance.ui_font_family(), 14.) .with_color(PhenomenonStyle::modal_badge_text()) .finish(); let badge = ConstrainedBox::new( @@ -105,7 +105,7 @@ pub fn render_welcome_banner( // Title let title = Text::new( - "Introducing universal agent support: level up any coding agent with Warp", + i18n::t("workspace.hoa.welcome_title"), appearance.ui_font_family(), 20., ) @@ -132,14 +132,18 @@ pub fn render_welcome_banner( .with_cross_axis_alignment(CrossAxisAlignment::Start) .with_spacing(2.) .with_child( - Text::new_inline(item.title.to_string(), appearance.ui_font_family(), 14.) + Text::new_inline(i18n::t(item.title_key), appearance.ui_font_family(), 14.) .with_color(PhenomenonStyle::modal_feature_title_text()) .finish(), ) .with_child( - Text::new(item.description, appearance.ui_font_family(), 14.) - .with_color(PhenomenonStyle::modal_feature_description_text()) - .finish(), + Text::new( + i18n::t(item.description_key), + appearance.ui_font_family(), + 14., + ) + .with_color(PhenomenonStyle::modal_feature_description_text()) + .finish(), ) .finish(); diff --git a/app/src/workspace/home.rs b/app/src/workspace/home.rs index 7dbd0e8c37..cd58a1d981 100644 --- a/app/src/workspace/home.rs +++ b/app/src/workspace/home.rs @@ -8,16 +8,6 @@ use warpui::ViewContext; use super::view::Workspace; use crate::pane_group::{AnyPaneContent, FilePane}; -const WARP_HOME_TITLE: &str = "Welcome to Warp on Web"; -const WARP_HOME_CONTENT: &str = r#" -Welcome to Warp on Web - your browser-based home for Warp! -Use Warp on Web to: -* Join Shared Sessions -* Create, View, and Edit Warp Drive Objects -* Manage your Warp Settings - -Warp on Web can also be used by your teammates and peers who don't have Warp downloaded yet to view your shared sessions, notebooks, and workflows."#; - /// Create a static "home page" pane. pub fn create_home_pane(ctx: &mut ViewContext) -> Box { let pane = FilePane::new( @@ -28,7 +18,8 @@ pub fn create_home_pane(ctx: &mut ViewContext) -> Box for DismissibleToast { fn from(value: ToastType) -> Self { match value { ToastType::CloudObjectNotFound => { - DismissibleToast::error(String::from("Resource not found or access denied")) + DismissibleToast::error(i18n::t("workspace.toast.resource_not_found")) } } } diff --git a/app/src/workspace/view.rs b/app/src/workspace/view.rs index c20d63c20d..a036c3e776 100644 --- a/app/src/workspace/view.rs +++ b/app/src/workspace/view.rs @@ -533,10 +533,6 @@ const TAB_BAR_ICON_PADDING: f32 = 4.; const TAB_BAR_PILL_WIDTH: f32 = 100.; const PILL_FONT_SIZE: f32 = 12.; -// We use the word "Warp" in the Update Ready button to make it obvious that the terminal is Warp. -// This can lead to free advertising when users screen-share Warp when an update is available. -const UPDATE_READY_TEXT: &str = "Update Warp"; - const TAB_BAR_OVERFLOW_MENU_WIDTH: f32 = 300.; #[cfg(not(target_family = "wasm"))] @@ -562,10 +558,6 @@ const ELLIPSE_SVG_PATH: &str = "bundled/svg/ellipse.svg"; const AI_ASSISTANT_BUTTON_ID: &str = "workspace_view:ai_assistant_button"; -const VERSION_DEPRECATION_BANNER_TEXT: &str = "Your app is out of date and some features may not work as expected. Please update immediately."; - -const VERSION_DEPRECATION_WITHOUT_PERMISSIONS_BANNER_TEXT: &str = "Some Warp features may not work as expected without updating immediately, but Warp is unable to perform the update."; - const ASK_AI_ASSISTANT_KEYBINDING_NAME: &str = "workspace:toggle_ai_assistant"; const TOGGLE_RESOURCE_CENTER_KEYBINDING_NAME: &str = "workspace:toggle_resource_center"; @@ -578,7 +570,6 @@ const NEW_SESSION_SIDECAR_SEARCH_BOX_HORIZONTAL_PADDING: f32 = 12.; const NEW_SESSION_SIDECAR_SEARCH_BOX_VERTICAL_PADDING: f32 = 6.; const NEW_SESSION_SIDECAR_FOOTER_HORIZONTAL_PADDING: f32 = 16.; const NEW_SESSION_SIDECAR_FOOTER_VERTICAL_PADDING: f32 = 8.; -const SESSION_CONFIG_TAB_CONFIG_CHIP_TEXT: &str = "Access your tab configs here."; const SESSION_CONFIG_TAB_CONFIG_CHIP_WIDTH: f32 = 206.; const SHOW_SETTINGS_KEYBINDING_NAME: &str = "workspace:show_settings"; pub const TOGGLE_COMMAND_PALETTE_KEYBINDING_NAME: &str = "workspace:toggle_command_palette"; @@ -640,9 +631,6 @@ const MAX_WINDOW_TITLE_LENGTH: usize = 80; const AUTO_CLOUD_HANDOFF_PROMPT: &str = "Continue this local Warp Agent task in the cloud from the current conversation state."; -/// The default display name used for the user if they have no associated display name. -pub const DEFAULT_USER_DISPLAY_NAME: &str = "User"; - lazy_static! { static ref OPENING_WARP_DRIVE_ON_START_UP: Arc> = Arc::new(Mutex::new(false)); static ref PANEL_CORNER_RADIUS: CornerRadius = CornerRadius::with_all(Radius::Pixels(8.)); @@ -1241,7 +1229,7 @@ impl Workspace { }, ctx, ); - editor.set_placeholder_text("Search repos", ctx); + editor.set_placeholder_text(i18n::t("workspace.repos.search_placeholder"), ctx); editor }); ctx.subscribe_to_view(&editor, |me, editor_view, event, ctx| match event { @@ -1277,7 +1265,7 @@ impl Workspace { EditorView::single_line(options, ctx) }); editor.update(ctx, |editor, ctx| { - editor.set_placeholder_text("Search tabs...", ctx); + editor.set_placeholder_text(i18n::t("workspace.tabs.search_placeholder"), ctx); }); ctx.subscribe_to_view(&editor, |me, editor_view, event, ctx| match event { EditorEvent::Edited(_) => { @@ -2004,7 +1992,10 @@ impl Workspace { log::warn!("Failed to remove tab config file: {e:?}"); self.toast_stack.update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error(format!("Failed to remove tab config: {e}")), + DismissibleToast::error( + i18n::t("workspace.toast.remove_tab_config_failed") + .replace("{error}", &e.to_string()), + ), ctx, ); }); @@ -2368,7 +2359,7 @@ impl Workspace { .finish(); let text = Text::new_inline( - SESSION_CONFIG_TAB_CONFIG_CHIP_TEXT.to_string(), + i18n::t("workspace.tab_config_chip.text"), appearance.ui_font_family(), 12., ) @@ -2425,9 +2416,9 @@ impl Workspace { if ChannelState::uses_staging_server() && me.shown_staging_banner_count < 5 { me.shown_staging_banner_count += 1; me.toast_stack.update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::error( - "Staging API call failed. Did your IP address change?".to_string(), - ) + let toast = DismissibleToast::error(i18n::t( + "workspace.toast.staging_api_call_failed", + )) .with_object_id("staging_access_blocked_toast".to_string()); toast_stack.add_ephemeral_toast(toast, ctx); }); @@ -2513,7 +2504,7 @@ impl Workspace { let toast = DismissibleToast::error(message) .with_object_id(object_id.clone()) .with_link( - ToastLink::new("Open file".to_string()).with_onclick_action( + ToastLink::new(i18n::t("common.open_file")).with_onclick_action( WorkspaceAction::OpenTabConfigErrorFile { path, toast_object_id: object_id, @@ -4097,9 +4088,9 @@ impl Workspace { let Some(cloud_conversation) = cloud_conversation else { log::error!("Failed to load conversation from server"); me.toast_stack.update(ctx, |view, ctx| { - let new_toast = DismissibleToast::error( - "Failed to load conversation data.".to_string(), - ); + let new_toast = DismissibleToast::error(i18n::t( + "workspace.toast.load_conversation_failed", + )); view.add_ephemeral_toast(new_toast, ctx); }); return; @@ -4264,7 +4255,8 @@ impl Workspace { )); self.toast_stack.update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::default("Remote control link copied.".to_string()); + let toast = + DismissibleToast::default(i18n::t("workspace.toast.remote_control_link_copied")); toast_stack.add_ephemeral_toast(toast, ctx); }); } @@ -5367,7 +5359,7 @@ impl Workspace { .unwrap_or_else(|| { let title = configuration.title().trim(); if title.is_empty() { - "Untitled pane".to_string() + i18n::t("workspace.pane.untitled") } else { title.to_string() } @@ -5713,9 +5705,11 @@ impl Workspace { if !FeatureFlag::ConfigurableToolbar.is_enabled() { return; } - let items = vec![MenuItemFields::new("Re-arrange toolbar items") - .with_on_select_action(WorkspaceAction::OpenHeaderToolbarEditor) - .into_item()]; + let items = vec![ + MenuItemFields::new(i18n::t("workspace.menu.rearrange_toolbar_items")) + .with_on_select_action(WorkspaceAction::OpenHeaderToolbarEditor) + .into_item(), + ]; self.header_toolbar_context_menu .update(ctx, |menu, ctx| menu.set_items(items, ctx)); self.show_header_toolbar_context_menu = Some(position); @@ -6127,7 +6121,8 @@ impl Workspace { ctx.open_file_path_in_explorer(&path); } Ok(Err(err)) => { - let error_message = format!("Failed to create log bundle: {err}"); + let error_message = i18n::t("workspace.toast.create_log_bundle_failed") + .replace("{error}", &err.to_string()); log::error!("{error_message}"); me.toast_stack.update(ctx, |toast_stack, ctx| { let toast = DismissibleToast::error(error_message); @@ -6135,7 +6130,8 @@ impl Workspace { }); } Err(err) => { - let error_message = format!("Failed to create log bundle: {err}"); + let error_message = i18n::t("workspace.toast.create_log_bundle_failed") + .replace("{error}", &err.to_string()); log::error!("{error_message}"); me.toast_stack.update(ctx, |toast_stack, ctx| { let toast = DismissibleToast::error(error_message); @@ -6178,7 +6174,7 @@ impl Workspace { // 1. Agent (if AI enabled) if is_any_ai_enabled { - let mut agent_item = MenuItemFields::new("Agent") + let mut agent_item = MenuItemFields::new(i18n::t("common.agent")) .with_on_select_action(WorkspaceAction::AddAgentTab) .with_icon(icons::Icon::LayoutAlt01); if effective_default == DefaultSessionMode::Agent { @@ -6194,11 +6190,12 @@ impl Workspace { #[cfg(target_os = "windows")] { let is_terminal_default = effective_default == DefaultSessionMode::Terminal; - let mut terminal_item = MenuItemFields::new("Terminal") - .with_on_select_action(WorkspaceAction::AddTerminalTab { - hide_homepage: false, - }) - .with_icon(icons::Icon::LayoutAlt01); + let mut terminal_item = + MenuItemFields::new(i18n::t("workspace.new_session.terminal")) + .with_on_select_action(WorkspaceAction::AddTerminalTab { + hide_homepage: false, + }) + .with_icon(icons::Icon::LayoutAlt01); if is_terminal_default { terminal_item = terminal_item.with_key_shortcut_label(shortcut_label.clone()); } @@ -6231,11 +6228,12 @@ impl Workspace { // On other platforms, Terminal is a regular item. #[cfg(not(target_os = "windows"))] { - let mut terminal_item = MenuItemFields::new("Terminal") - .with_on_select_action(WorkspaceAction::AddTerminalTab { - hide_homepage: false, - }) - .with_icon(icons::Icon::LayoutAlt01); + let mut terminal_item = + MenuItemFields::new(i18n::t("workspace.new_session.terminal")) + .with_on_select_action(WorkspaceAction::AddTerminalTab { + hide_homepage: false, + }) + .with_icon(icons::Icon::LayoutAlt01); if effective_default == DefaultSessionMode::Terminal { terminal_item = terminal_item.with_key_shortcut_label(shortcut_label.clone()); } @@ -6248,7 +6246,7 @@ impl Workspace { && FeatureFlag::AgentView.is_enabled() && FeatureFlag::CloudMode.is_enabled() { - let mut cloud_item = MenuItemFields::new("Cloud Agent") + let mut cloud_item = MenuItemFields::new(i18n::t("workspace.new_session.cloud_agent")) .with_on_select_action(WorkspaceAction::AddAmbientAgentTab) .with_icon(icons::Icon::LayoutAlt01); if effective_default == DefaultSessionMode::CloudAgent { @@ -6259,9 +6257,10 @@ impl Workspace { // 3b. Local Docker Sandbox if FeatureFlag::LocalDockerSandbox.is_enabled() { - let mut docker_item = MenuItemFields::new("Local Docker Sandbox") - .with_on_select_action(WorkspaceAction::AddDockerSandboxTab) - .with_icon(icons::Icon::Docker); + let mut docker_item = + MenuItemFields::new(i18n::t("workspace.new_session.local_docker_sandbox")) + .with_on_select_action(WorkspaceAction::AddDockerSandboxTab) + .with_icon(icons::Icon::Docker); if effective_default == DefaultSessionMode::DockerSandbox { docker_item = docker_item.with_key_shortcut_label(shortcut_label.clone()); } @@ -6319,14 +6318,14 @@ impl Workspace { if FeatureFlag::TabConfigs.is_enabled() { menu_items.push(MenuItem::Separator); menu_items.push( - MenuItemFields::new_submenu("New worktree config") + MenuItemFields::new_submenu(i18n::t("workspace.menu.new_worktree_config")) .with_icon(icons::Icon::Dataflow02) .into_item(), ); // 6. New tab config — V0: opens the TOML template. menu_items.push( - MenuItemFields::new("New tab config") + MenuItemFields::new(i18n::t("workspace.menu.new_tab_config")) .with_on_select_action(WorkspaceAction::SelectNewSessionMenuItem( NewSessionMenuItem::CreateNewTabConfig, )) @@ -6340,7 +6339,7 @@ impl Workspace { if FeatureFlag::GroupedTabs.is_enabled() { menu_items.push(MenuItem::Separator); menu_items.push( - MenuItemFields::new("New tab group") + MenuItemFields::new(i18n::t("workspace.menu.new_tab_group")) .with_on_select_action(WorkspaceAction::SelectNewSessionMenuItem( NewSessionMenuItem::CreateNewTabGroup, )) @@ -6351,7 +6350,7 @@ impl Workspace { menu_items.push(MenuItem::Separator); menu_items.push( - MenuItemFields::new("Reopen closed session") + MenuItemFields::new(i18n::t("workspace.menu.reopen_closed_session")) .with_on_select_action(WorkspaceAction::ReopenClosedSession) .with_key_shortcut_label(reopen_closed_session_shortcut_label) .with_disabled(UndoCloseStack::handle(ctx).as_ref(ctx).is_empty()) @@ -6526,7 +6525,8 @@ impl Workspace { .and_then(|view| view.as_ref(ctx).pwd()) .map(PathBuf::from); - let modal_title = format!("Open: {}", tab_config.name); + let modal_title = + i18n::t("workspace.modal.open_tab_config").replace("{name}", &tab_config.name); self.tab_config_params_modal.view.update(ctx, |modal, ctx| { modal.body().update(ctx, |body, ctx| { body.set_title(modal_title); @@ -6732,13 +6732,21 @@ impl Workspace { let pane_name_target = match target { VerticalTabsPaneContextMenuTarget::ClickedPane(locator) => PaneNameMenuTarget { locator, - rename_label: "Rename pane", - reset_label: "Reset pane name", + rename_label: Box::leak( + i18n::t("workspace.pane_menu.rename_pane").into_boxed_str(), + ), + reset_label: Box::leak( + i18n::t("workspace.pane_menu.reset_pane_name").into_boxed_str(), + ), }, VerticalTabsPaneContextMenuTarget::ActivePane(locator) => PaneNameMenuTarget { locator, - rename_label: "Rename active pane", - reset_label: "Reset active pane name", + rename_label: Box::leak( + i18n::t("workspace.pane_menu.rename_active_pane").into_boxed_str(), + ), + reset_label: Box::leak( + i18n::t("workspace.pane_menu.reset_active_pane_name").into_boxed_str(), + ), }, }; let menu_items = tab.menu_items_with_pane_name_target( @@ -6768,24 +6776,35 @@ impl Workspace { if FeatureFlag::Autoupdate.is_enabled() && ChannelState::show_autoupdate_menu_items() { if let Some(version) = ChannelState::app_version() { menu_items.push( - MenuItemFields::new(format!("Current version is {version}")) - .with_disabled(true) - .into_item(), + MenuItemFields::new(format!( + "{}{version}", + i18n::t("workspace.menu.current_version_prefix") + )) + .with_disabled(true) + .into_item(), ); match autoupdate::get_update_state(ctx) { AutoupdateStage::UpdateReady { new_version, .. } | AutoupdateStage::UpdatedPendingRestart { new_version } => menu_items.push( - MenuItemFields::new(format!("Install update ({})", new_version.version)) - .with_on_select_action(WorkspaceAction::ApplyUpdate) - .into_item(), + MenuItemFields::new(format!( + "{} ({})", + i18n::t("workspace.menu.install_update"), + new_version.version + )) + .with_on_select_action(WorkspaceAction::ApplyUpdate) + .into_item(), ), AutoupdateStage::Updating { new_version, .. } => menu_items.push( - MenuItemFields::new(format!("Updating to ({})", new_version.version)) - .with_disabled(true) - .into_item(), + MenuItemFields::new(format!( + "{} ({})", + i18n::t("workspace.menu.updating_to"), + new_version.version + )) + .with_disabled(true) + .into_item(), ), AutoupdateStage::UnableToUpdateToNewVersion { .. } => menu_items.push( - MenuItemFields::new("Update Warp manually") + MenuItemFields::new(i18n::t("workspace.menu.update_manually")) .with_on_select_action(WorkspaceAction::DownloadNewVersion) .into_item(), ), @@ -7400,7 +7419,7 @@ impl Workspace { self.add_tab_with_pane_layout( panes_layout, Arc::new(HashMap::new()), - Some("Settings".to_owned()), + Some(i18n::t("workspace.tab_title.settings")), ctx, ); } @@ -7848,31 +7867,35 @@ impl Workspace { /// Install the Warp CLI by creating a symlink in /usr/local/bin #[cfg(target_os = "macos")] fn install_cli(&mut self, ctx: &mut ViewContext) { - ctx.spawn(async { cli_install::install_cli() }, |view, result, ctx| { - match result { + ctx.spawn( + async { cli_install::install_cli() }, + |view, result, ctx| match result { Ok(_) => { let command_name = ChannelState::channel().cli_command_name(); - let message = format!("Successfully installed the Oz CLI! You can now run '{command_name}' from the command line."); + let message = format!( + "{}{command_name}{}", + i18n::t("workspace.toast.cli_installed_prefix"), + i18n::t("workspace.toast.cli_installed_suffix") + ); view.toast_stack.update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::success(message.to_string()) - .with_link( - ToastLink::new("Learn more".to_string()).with_href( - "https://docs.warp.dev/reference/cli".to_string(), - ), - ); + let toast = DismissibleToast::success(message.to_string()).with_link( + ToastLink::new(i18n::t("workspace.toast.learn_more")) + .with_href("https://docs.warp.dev/reference/cli".to_string()), + ); toast_stack.add_ephemeral_toast(toast, ctx); }); } Err(error) => { - let error_message = format!("Failed to install Oz command: {error}"); + let error_message = i18n::t("workspace.toast.oz_command_install_failed") + .replace("{error}", &error.to_string()); log::error!("{error_message}"); view.toast_stack.update(ctx, |toast_stack, ctx| { let toast = DismissibleToast::error(error_message); toast_stack.add_persistent_toast(toast, ctx); }); } - } - }); + }, + ); } /// Uninstall the Warp CLI by removing the symlink from /usr/local/bin @@ -7882,14 +7905,15 @@ impl Workspace { async { cli_install::uninstall_cli() }, |view, result, ctx| match result { Ok(_) => { - let message = "Successfully uninstalled the Oz command."; + let message = i18n::t("workspace.toast.oz_command_uninstalled"); view.toast_stack.update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::success(message.to_string()); + let toast = DismissibleToast::success(message); toast_stack.add_ephemeral_toast(toast, ctx); }); } Err(error) => { - let error_message = format!("Failed to uninstall Oz command: {error}"); + let error_message = i18n::t("workspace.toast.oz_command_uninstall_failed") + .replace("{error}", &error.to_string()); log::error!("{error_message}"); view.toast_stack.update(ctx, |toast_stack, ctx| { let toast = DismissibleToast::error(error_message); @@ -8485,7 +8509,7 @@ impl Workspace { ) => { items.push( - MenuItemFields::new("Update and relaunch Warp") + MenuItemFields::new(i18n::t("workspace.menu.update_and_relaunch")) .with_on_select_action(WorkspaceAction::ApplyUpdate) .with_override_text_color(appearance.theme().ansi_fg_red()) .into_item(), @@ -8497,9 +8521,13 @@ impl Workspace { ) => { items.push( - MenuItemFields::new(format!("Updating to ({})", new_version.version)) - .with_disabled(true) - .into_item(), + MenuItemFields::new(format!( + "{} ({})", + i18n::t("workspace.menu.updating_to"), + new_version.version + )) + .with_disabled(true) + .into_item(), ) } AutoupdateStage::UnableToUpdateToNewVersion { new_version } @@ -8508,7 +8536,7 @@ impl Workspace { ) => { items.push( - MenuItemFields::new("Update Warp manually") + MenuItemFields::new(i18n::t("workspace.menu.update_manually")) .with_on_select_action(WorkspaceAction::DownloadNewVersion) .with_override_text_color(appearance.theme().ansi_fg_red()) .into_item(), @@ -8519,27 +8547,27 @@ impl Workspace { } items.extend([ - MenuItemFields::new("What's new") + MenuItemFields::new(i18n::t("workspace.menu.whats_new")) .with_on_select_action(WorkspaceAction::ViewLatestChangelog) .into_item(), - MenuItemFields::new("Settings") + MenuItemFields::new(i18n::t("workspace.menu.settings")) .with_on_select_action(WorkspaceAction::ShowSettings) .into_item(), - MenuItemFields::new("Keyboard shortcuts") + MenuItemFields::new(i18n::t("workspace.menu.keyboard_shortcuts")) .with_on_select_action(WorkspaceAction::ToggleKeybindingsPage) .into_item(), MenuItem::Separator, - MenuItemFields::new("Documentation") + MenuItemFields::new(i18n::t("workspace.menu.documentation")) .with_on_select_action(WorkspaceAction::ViewUserDocs) .into_item(), - MenuItemFields::new("Feedback") + MenuItemFields::new(i18n::t("workspace.menu.feedback")) .with_on_select_action(WorkspaceAction::SendFeedback) .into_item(), ]); #[cfg(not(target_family = "wasm"))] items.push( - MenuItemFields::new("View Warp logs") + MenuItemFields::new(i18n::t("workspace.menu.view_logs")) .with_on_select_action(WorkspaceAction::ViewLogs) .into_item(), ); @@ -8553,7 +8581,7 @@ impl Workspace { if self.auth_state.is_anonymous_or_logged_out() { items.push( - MenuItemFields::new("Sign up") + MenuItemFields::new(i18n::t("workspace.menu.sign_up")) .with_on_select_action(WorkspaceAction::SignupAnonymousUser) .into_item(), ); @@ -8567,7 +8595,7 @@ impl Workspace { if is_on_paid_plan { items.push( - MenuItemFields::new("Billing and usage") + MenuItemFields::new(i18n::t("workspace.menu.billing_and_usage")) .with_on_select_action(WorkspaceAction::ShowSettingsPage( SettingsSection::BillingAndUsage, )) @@ -8575,21 +8603,21 @@ impl Workspace { ); } else { items.push( - MenuItemFields::new("Upgrade") + MenuItemFields::new(i18n::t("workspace.menu.upgrade")) .with_on_select_action(WorkspaceAction::ShowUpgrade) .into_item(), ); } items.push( - MenuItemFields::new("Invite a friend") + MenuItemFields::new(i18n::t("workspace.menu.invite_a_friend")) .with_on_select_action(WorkspaceAction::ShowReferralSettingsPage) .into_item(), ); if !self.auth_state.is_anonymous_or_logged_out() { items.push( - MenuItemFields::new("Log out") + MenuItemFields::new(i18n::t("workspace.menu.log_out")) .with_on_select_action(WorkspaceAction::LogOut) .into_item(), ); @@ -8801,7 +8829,7 @@ impl Workspace { .with_height(NEW_SESSION_SIDECAR_SEARCH_BOX_HEIGHT) .finish() }), - Some("Search repos".to_string()), + Some(i18n::t("workspace.repos.search_placeholder")), ) .with_no_interaction_on_hover() .no_highlight_on_hover() @@ -8880,9 +8908,13 @@ impl Workspace { .with_main_axis_size(MainAxisSize::Max) .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( - Text::new_inline(" + Add new repo", font_family, font_size) - .with_color(text_color.into()) - .finish(), + Text::new_inline( + format!(" + {}", i18n::t("workspace.repos.add_new")), + font_family, + font_size, + ) + .with_color(text_color.into()) + .finish(), ) .finish(), ) @@ -9055,16 +9087,20 @@ impl Workspace { return; }; - // Check what the hovered item is by reading its label. - let hovered_label = self.new_session_dropdown_menu.read(ctx, |menu, _| { + // Check what the hovered item is by reading its structure and action. + let hovered_item = self.new_session_dropdown_menu.read(ctx, |menu, _| { menu.items().get(hovered_index).and_then(|item| match item { - MenuItem::Item(fields) => Some(fields.label().to_string()), + MenuItem::Item(fields) => Some(( + fields.label().to_string(), + fields.on_select_action().cloned(), + fields.has_submenu(), + )), _ => None, }) }); // Separator or non-labeled item — hide sidecar. - let Some(label) = hovered_label else { + let Some((label, hovered_action, has_submenu)) = hovered_item else { if self.show_new_session_sidecar { self.show_new_session_sidecar = false; self.new_session_dropdown_menu.update(ctx, |menu, _| { @@ -9076,36 +9112,33 @@ impl Workspace { return; }; - match label.as_str() { - "New worktree config" => { - self.tab_config_action_sidecar_item = None; - let auto_select_first_repo = self.new_session_dropdown_menu.read(ctx, |menu, _| { - menu.last_selection_source() != Some(MenuSelectionSource::Pointer) - }); - self.configure_worktree_new_session_sidecar( - hovered_index, - auto_select_first_repo, - ctx, - ); - } + if has_submenu { + self.tab_config_action_sidecar_item = None; + let auto_select_first_repo = self.new_session_dropdown_menu.read(ctx, |menu, _| { + menu.last_selection_source() != Some(MenuSelectionSource::Pointer) + }); + self.configure_worktree_new_session_sidecar(hovered_index, auto_select_first_repo, ctx); + } else if matches!( + hovered_action, + Some(WorkspaceAction::SelectNewSessionMenuItem( + NewSessionMenuItem::CreateNewTabConfig + )) + ) { // Items that don't get any sidecar. - "New tab config" => { - self.tab_config_action_sidecar_item = None; - if self.show_new_session_sidecar { - self.show_new_session_sidecar = false; - self.worktree_sidecar_active = false; - self.new_session_dropdown_menu.update(ctx, |menu, _| { - menu.set_safe_zone_target(None); - menu.set_submenu_being_shown_for_item_index(None); - }); - } - } - // All other actionable items get the action sidecar. - _ => { + self.tab_config_action_sidecar_item = None; + if self.show_new_session_sidecar { self.show_new_session_sidecar = false; self.worktree_sidecar_active = false; - self.configure_action_sidecar_for_hovered_item(&label, hovered_index, ctx); + self.new_session_dropdown_menu.update(ctx, |menu, _| { + menu.set_safe_zone_target(None); + menu.set_submenu_being_shown_for_item_index(None); + }); } + } else { + // All other actionable items get the action sidecar. + self.show_new_session_sidecar = false; + self.worktree_sidecar_active = false; + self.configure_action_sidecar_for_hovered_item(&label, hovered_index, ctx); } ctx.notify(); @@ -9395,13 +9428,13 @@ impl Workspace { .map(|n| n.to_string_lossy().to_string()) .unwrap_or_else(|| repo.to_string()); let config_name = match worktree_branch_name { - Some(name) if !name.is_empty() => { - format!("New worktree: {repo_display_name}, {name}") - } - _ if !base_branch.is_empty() => { - format!("New worktree: {repo_display_name}, {base_branch}") - } - _ => format!("New worktree: {repo_display_name}"), + Some(name) if !name.is_empty() => i18n::t("workspace.worktree.new_with_name") + .replace("{repo}", &repo_display_name) + .replace("{name}", name), + _ if !base_branch.is_empty() => i18n::t("workspace.worktree.new_with_branch") + .replace("{repo}", &repo_display_name) + .replace("{branch}", base_branch), + _ => i18n::t("workspace.worktree.new").replace("{repo}", &repo_display_name), }; let filename_hint = if let Some(name) = worktree_branch_name { @@ -9521,7 +9554,8 @@ impl Workspace { .file_name() .map(|name| name.to_string_lossy().to_string()) .unwrap_or_else(|| repo_path.clone()); - let config_name = format!("Worktree: {repo_display_name}"); + let config_name = + i18n::t("workspace.worktree.config_name").replace("{repo}", &repo_display_name); // Use the user's default session mode to decide pane type. let pane_type = if AISettings::as_ref(ctx).is_any_ai_enabled(ctx) && AISettings::as_ref(ctx).default_session_mode(ctx) == DefaultSessionMode::Agent @@ -9667,9 +9701,9 @@ impl Workspace { self.toast_stack.update(ctx, |view, ctx| { let new_toast = - DismissibleToast::error("Looks like you're out of AI credits.".into()) + DismissibleToast::error(i18n::t("workspace.toast.out_of_credits")) .with_link( - ToastLink::new("Upgrade for more credits.".into()) + ToastLink::new(i18n::t("workspace.toast.upgrade_for_credits")) .with_href(upgrade_link), ); view.add_ephemeral_toast(new_toast, ctx); @@ -11262,7 +11296,7 @@ impl Workspace { self.add_tab_with_pane_layout( Default::default(), Arc::new(HashMap::new()), - Some("Install Update".to_owned()), + Some(i18n::t("workspace.tab_title.install_update")), ctx, ); @@ -11647,7 +11681,9 @@ impl Workspace { ctx.notify(); }); WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::error("Failed to load conversation.".to_owned()); + let toast = DismissibleToast::error(i18n::t( + "workspace.toast.load_conversation_failed", + )); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); return; @@ -11707,7 +11743,9 @@ impl Workspace { let Some(conversation) = conversation else { log::warn!("Failed to load conversation {conversation_id}"); WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::error("Failed to load conversation.".to_owned()); + let toast = DismissibleToast::error(i18n::t( + "workspace.toast.load_conversation_failed", + )); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); // Close the loading pane @@ -11776,7 +11814,9 @@ impl Workspace { let Some(conversation) = conversation else { log::warn!("Failed to load conversation {conversation_id}"); WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::error("Failed to load conversation.".to_owned()); + let toast = DismissibleToast::error(i18n::t( + "workspace.toast.load_conversation_failed", + )); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); // Close the loading tab @@ -11893,9 +11933,9 @@ impl Workspace { let Some(CloudConversationData::Oz(source_conversation)) = source_conversation else { log::error!("Failed to load Oz conversation {conversation_id} for forking."); WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::error( - "Failed to load conversation for forking.".to_owned(), - ); + let toast = DismissibleToast::error(i18n::t( + "workspace.toast.failed_to_load_conversation_for_forking", + )); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); return; @@ -12010,7 +12050,9 @@ impl Workspace { Err(e) => { log::error!("Conversation forking failed. {e}."); WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::error("Conversation forking failed.".to_owned()); + let toast = DismissibleToast::error(i18n::t( + "workspace.toast.conversation_fork_failed", + )); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); return; @@ -12243,7 +12285,7 @@ impl Workspace { .conversation(&conversation_id) .and_then(|c| c.title()) .map(|s| s.to_string()) - .unwrap_or_else(|| "Conversation".to_string()); + .unwrap_or_else(|| i18n::t("workspace.conversation.default_title")); let title = if source_title.chars().count() > MAX_FORK_TOAST_TITLE_LENGTH { let truncated: String = source_title @@ -12256,7 +12298,10 @@ impl Workspace { }; WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::default(format!("Forked \"{title}\"")); + let toast = DismissibleToast::default(format!( + "{}\"{title}\"", + i18n::t("workspace.toast.forked_prefix") + )); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); } @@ -12434,9 +12479,9 @@ impl Workspace { let url = NOTIFICATIONS_TROUBLESHOOT_URL.to_string(); view.toast_stack.update(ctx, |toast_stack, ctx| { let toast = DismissibleToast::error( - "Warp doesn't have permission to send desktop notifications.".to_string(), + i18n::t("workspace.toast.no_notification_permission"), ) - .with_link(ToastLink::new("Troubleshoot notifications".to_string()).with_href(url)); + .with_link(ToastLink::new(i18n::t("workspace.toast.troubleshoot_notifications")).with_href(url)); toast_stack.add_persistent_toast(toast, ctx); }); } @@ -13092,14 +13137,16 @@ impl Workspace { .find(|binding| binding.name == "workspace:view_changelog") .and_then(|binding| trigger_to_keystroke(binding.trigger)); - let mut link = ToastLink::new("View changelog".to_owned()) - .with_onclick_action(WorkspaceAction::ViewLatestChangelog); + let mut link = + ToastLink::new(i18n::t("workspace.toast.view_changelog")) + .with_onclick_action(WorkspaceAction::ViewLatestChangelog); if let Some(keystroke) = keystroke { link = link.with_keystroke(keystroke); } - let toast = DismissibleToast::default(String::from("Warp updated!")) - .with_link(link); + let toast = + DismissibleToast::default(i18n::t("workspace.toast.warp_updated")) + .with_link(link); stack.add_ephemeral_toast(toast, ctx); }); @@ -13406,9 +13453,10 @@ impl Workspace { me.handoff_environment_creation_modal = None; me.toast_stack.update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error(format!( - "Failed to create environment: {error_message}" - )), + DismissibleToast::error( + i18n::t("workspace.handoff.create_environment_failed") + .replace("{error}", error_message), + ), ctx, ); }); @@ -13450,7 +13498,7 @@ impl Workspace { AuthSecretFtuxViewEvent::Failed { .. } => {} }); - let title = "New API key".to_string(); + let title = i18n::t("workspace.modal.new_api_key"); let modal = ctx.add_typed_action_view(|ctx| { Modal::new(Some(title), body, ctx).with_modal_style(UiComponentStyles { width: Some(520.), @@ -13534,9 +13582,10 @@ impl Workspace { me.handoff_environment_creation_modal = None; me.toast_stack.update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error(format!( - "Failed to create environment: {error_message}" - )), + DismissibleToast::error( + i18n::t("workspace.handoff.create_environment_failed") + .replace("{error}", error_message), + ), ctx, ); }); @@ -13572,9 +13621,7 @@ impl Workspace { let window_id = ctx.window_id(); WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::default( - "Starting cloud environment for this session...".to_owned(), - ), + DismissibleToast::default(i18n::t("workspace.toast.starting_cloud_environment")), window_id, ctx, ); @@ -13656,8 +13703,7 @@ impl Workspace { WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( DismissibleToast::error( - "Couldn't open a cloud pane for handoff. Try again, or restart Warp if this keeps happening." - .to_owned(), + i18n::t("workspace.handoff.open_cloud_pane_failed").to_owned(), ), window_id, ctx, @@ -13725,8 +13771,7 @@ impl Workspace { WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( DismissibleToast::error( - "No active terminal session to hand off. Focus a pane and try again." - .to_owned(), + i18n::t("workspace.handoff.no_active_terminal").to_owned(), ), window_id, ctx, @@ -13815,8 +13860,7 @@ impl Workspace { WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( DismissibleToast::error( - "Cloud handoff isn't available for orchestrated agent conversations." - .to_owned(), + i18n::t("workspace.handoff.not_available_for_orchestrated").to_owned(), ), window_id, ctx, @@ -13872,8 +13916,7 @@ impl Workspace { WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( DismissibleToast::error( - "Can't hand off while a command is running. Cancel the command or wait for it to finish." - .to_owned(), + i18n::t("workspace.handoff.command_running").to_owned(), ), window_id, ctx, @@ -13909,8 +13952,7 @@ impl Workspace { WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( DismissibleToast::error( - "Your conversation hasn't synced to the cloud yet. Try sending another message, then hand off again." - .to_owned(), + i18n::t("workspace.handoff.not_synced_yet").to_owned(), ), window_id, ctx, @@ -13926,7 +13968,7 @@ impl Workspace { let source_conversation_id = source_token.as_str().to_string(); let title_for_fork = source_conversation .title() - .map(|t| format!("{t} (Moved to cloud)")); + .map(|t| i18n::t("workspace.handoff.moved_to_cloud_title").replace("{title}", &t)); ctx.spawn( async move { ai_client @@ -13962,8 +14004,7 @@ impl Workspace { WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( DismissibleToast::error( - "Couldn't start the handoff. Check your network connection and try again." - .to_owned(), + i18n::t("workspace.handoff.start_failed").to_owned(), ), window_id, ctx, @@ -13997,7 +14038,7 @@ impl Workspace { // Materialize the fork locally so the new pane can restore it. let title_override = source_conversation .title() - .map(|t| format!("{t} (Moved to cloud)")); + .map(|t| i18n::t("workspace.handoff.moved_to_cloud_title").replace("{title}", &t)); let local_fork = match history_model.update(ctx, |history_model, ctx| { history_model.fork_conversation( &source_conversation, @@ -14018,8 +14059,7 @@ impl Workspace { WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( DismissibleToast::error( - "Couldn't save your conversation locally. Try sending another message, then hand off again." - .to_owned(), + i18n::t("workspace.handoff.local_save_failed").to_owned(), ), window_id, ctx, @@ -14044,8 +14084,7 @@ impl Workspace { WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( DismissibleToast::error( - "Couldn't open a cloud pane for handoff. Try again, or restart Warp if this keeps happening." - .to_owned(), + i18n::t("workspace.handoff.open_cloud_pane_failed").to_owned(), ), window_id, ctx, @@ -14348,9 +14387,8 @@ impl Workspace { if !object_found { self.toast_stack.update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::error(String::from( - "Resource not found or access denied", - )); + let toast = + DismissibleToast::error(i18n::t("workspace.toast.resource_not_found")); toast_stack.add_ephemeral_toast(toast, ctx); }); ctx.notify(); @@ -15762,9 +15800,7 @@ impl Workspace { let Some(view) = self.active_session_view(ctx) else { let window_id = ctx.window_id(); WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::default( - "No terminal pane open. Open a new pane to attach as context.".to_owned(), - ); + let toast = DismissibleToast::default(i18n::t("workspace.toast.no_terminal_pane")); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); return; @@ -15782,8 +15818,9 @@ impl Workspace { { let window_id = ctx.window_id(); WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = - DismissibleToast::default("This plan is already in context.".to_owned()); + let toast = DismissibleToast::default(i18n::t( + "workspace.toast.plan_already_in_context", + )); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); return; @@ -15864,9 +15901,8 @@ impl Workspace { // The active terminal exists but is busy, and the fallback behavior is // RequireExisting or OpenIfNone. In those cases, show a toast and no-op. self.toast_stack.update(ctx, |toast_stack, ctx| { - let mut toast = DismissibleToast::error( - "A command in this session is still running.".to_string(), - ); + let mut toast = + DismissibleToast::error(i18n::t("workspace.toast.command_still_running")); if let Some(id) = object_id { toast = toast.with_object_id(id.uid()); } @@ -15888,7 +15924,7 @@ impl Workspace { if !ContextFlag::CreateNewSession.is_enabled() { self.toast_stack.update(ctx, |toast_stack, ctx| { let toast = - DismissibleToast::error("Cannot open a new terminal session".to_string()); + DismissibleToast::error(i18n::t("workspace.toast.cannot_open_terminal")); toast_stack.add_ephemeral_toast(toast, ctx); }); return None; @@ -16129,9 +16165,9 @@ impl Workspace { else { self.toast_stack.update(ctx, |view, ctx| { view.add_ephemeral_toast( - DismissibleToast::error( - "This workflow is no longer available.".to_string(), - ), + DismissibleToast::error(i18n::t( + "workspace.toast.workflow_unavailable", + )), ctx, ); }); @@ -16314,12 +16350,12 @@ impl Workspace { .contains_ai_document(&ai_doc_id, ctx) }, ) { - new_toast = DismissibleToast::success( - "Plan synced to your Warp Drive".to_string(), - ) + new_toast = DismissibleToast::success(i18n::t( + "workspace.toast.plan_synced", + )) .with_object_id(object_id_clone) .with_link( - ToastLink::new("View".to_string()) + ToastLink::new(i18n::t("workspace.toast.view")) .with_onclick_action( WorkspaceAction::ViewObjectInWarpDrive( WarpDriveItemId::Object( @@ -16341,20 +16377,23 @@ impl Workspace { || result.operation == ObjectOperation::Update { new_toast = new_toast.with_link( - ToastLink::new("View".to_string()).with_onclick_action( - WorkspaceAction::ViewObjectInWarpDrive( - WarpDriveItemId::Object( - CloudObjectTypeAndId::Workflow(workflow.id), + ToastLink::new(i18n::t("common.view")) + .with_onclick_action( + WorkspaceAction::ViewObjectInWarpDrive( + WarpDriveItemId::Object( + CloudObjectTypeAndId::Workflow( + workflow.id, + ), + ), ), ), - ), ) } } if result.operation == ObjectOperation::Trash { new_toast = new_toast.with_link( - ToastLink::new("Undo".to_string()).with_onclick_action( + ToastLink::new(i18n::t("common.undo")).with_onclick_action( WorkspaceAction::UndoTrash(cloud_object_type_and_id), ), ) @@ -16391,10 +16430,9 @@ impl Workspace { let new_toast = if let Some(workflow) = cloned_workflow { DismissibleToast::error(message) .with_link( - ToastLink::new( - "Check out the latest version and try again." - .to_string(), - ) + ToastLink::new(i18n::t( + "workspace.toast.checkout_latest_retry", + )) .with_onclick_action( WorkspaceAction::HandleConflictingWorkflow( workflow.id, @@ -16405,10 +16443,9 @@ impl Workspace { } else if let Some(env_var_collection) = cloned_env_var_collection { DismissibleToast::error(message) .with_link( - ToastLink::new( - "Check out the latest version and try again." - .to_string(), - ) + ToastLink::new(i18n::t( + "workspace.toast.checkout_latest_retry", + )) .with_onclick_action( WorkspaceAction::HandleConflictingEnvVarCollection( env_var_collection.id, @@ -16911,16 +16948,19 @@ impl Workspace { prev_mouse_reporting_enabled }); - let verb = if prev_mouse_reporting_enabled { - "disabled" + let mut message = if prev_mouse_reporting_enabled { + i18n::t("workspace.toast.mouse_reporting_disabled") } else { - "enabled" + i18n::t("workspace.toast.mouse_reporting_enabled") }; - let mut message = format!("You {verb} mouse reporting."); if let Some(keystroke) = keybinding_name_to_keystroke("workspace:toggle_mouse_reporting", ctx) { - let _ = write!(message, " Press {} to undo.", keystroke.displayed()); + let _ = write!( + message, + "{}", + i18n::t("workspace.toast.press_to_undo").replace("{key}", &keystroke.displayed()) + ); } self.toast_stack.update(ctx, |view, ctx| { @@ -17058,8 +17098,9 @@ impl Workspace { let command = code.trim().to_string(); let args_state = ArgumentsState::for_command_workflow(&Default::default(), command.clone()); - let workflow = Workflow::new("Command from Warp AI", command) - .with_arguments(args_state.arguments); + let workflow = + Workflow::new(i18n::t("workspace.workflow.command_from_warp_ai"), command) + .with_arguments(args_state.arguments); self.run_workflow_in_active_input( &WorkflowType::AIGenerated { workflow, @@ -17374,7 +17415,9 @@ impl Workspace { { BlocklistAIHistoryModel::handle(ctx).update(ctx, |history, _ctx| { if let Some(conversation) = history.conversation_mut(&conversation_id) { - conversation.set_fallback_display_title("Linear Issue".to_string()); + conversation.set_fallback_display_title(i18n::t( + "workspace.conversation.linear_issue_title", + )); } }); } @@ -17794,10 +17837,7 @@ impl Workspace { let body = appearance .ui_builder() - .wrappable_text( - "Ask Warp AI to explain errors, suggest commands or write scripts.".to_owned(), - true, - ) + .wrappable_text(i18n::t("workspace.ask_ai_description"), true) .with_style(UiComponentStyles { font_size: Some(12.), font_color: Some(sub_text_color), @@ -17931,7 +17971,7 @@ impl Workspace { icons::Icon::Grid, &self.mouse_states.agent_management_view_button, WorkspaceAction::ToggleAgentManagementView, - "Agent management panel".to_string(), + i18n::t("workspace.tooltip.agent_management_panel"), keybinding_name_to_display_string( "workspace:toggle_agent_management_view", ctx, @@ -17961,7 +18001,7 @@ impl Workspace { if vertical_tabs_active { ( self.vertical_tabs_panel_open, - "Tabs panel", + i18n::t("workspace.tooltip.tabs_panel"), WorkspaceAction::ToggleVerticalTabsPanel, "workspace:toggle_vertical_tabs_panel", "workspace:toggle_vertical_tabs_panel", @@ -17974,13 +18014,19 @@ impl Workspace { .copied() .unwrap_or(ToolPanelView::WarpDrive) { - ToolPanelView::ProjectExplorer => "Project explorer", - ToolPanelView::GlobalSearch { .. } => "Global search", - ToolPanelView::WarpDrive => "Warp Drive", - ToolPanelView::ConversationListView => "Agent conversations", + ToolPanelView::ProjectExplorer => { + i18n::t("workspace.tooltip.project_explorer") + } + ToolPanelView::GlobalSearch { .. } => { + i18n::t("workspace.tooltip.global_search") + } + ToolPanelView::WarpDrive => i18n::t("workspace.tooltip.warp_drive"), + ToolPanelView::ConversationListView => { + i18n::t("workspace.tooltip.agent_conversations") + } } } else { - "Tools panel" + i18n::t("workspace.tooltip.tools_panel") }; ( self.active_tab_pane_group().as_ref(ctx).left_panel_open, @@ -18028,13 +18074,15 @@ impl Workspace { .copied() .unwrap_or(ToolPanelView::WarpDrive) { - ToolPanelView::ProjectExplorer => "Project explorer", - ToolPanelView::GlobalSearch { .. } => "Global search", - ToolPanelView::WarpDrive => "Warp Drive", - ToolPanelView::ConversationListView => "Agent conversations", + ToolPanelView::ProjectExplorer => i18n::t("workspace.tooltip.project_explorer"), + ToolPanelView::GlobalSearch { .. } => i18n::t("workspace.tooltip.global_search"), + ToolPanelView::WarpDrive => i18n::t("workspace.tooltip.warp_drive"), + ToolPanelView::ConversationListView => { + i18n::t("workspace.tooltip.agent_conversations") + } } } else { - "Tools panel" + i18n::t("workspace.tooltip.tools_panel") }; SavePosition::new( @@ -18191,7 +18239,7 @@ impl Workspace { button .with_tooltip(self.render_tab_bar_icon_button_tooltip( appearance, - "Code review panel".to_string(), + i18n::t("workspace.tooltip.code_review_panel"), keybinding_name_to_display_string("workspace:toggle_right_panel", ctx), )) .build() @@ -18284,7 +18332,7 @@ impl Workspace { Shrinkable::new( 1., Text::new_inline( - "Search sessions, agents, files...", + i18n::t("workspace.search.title_bar_placeholder"), appearance.ui_font_family(), 14., ) @@ -18687,7 +18735,7 @@ impl Workspace { WorkspaceAction::ToggleNotificationMailbox { select_first: false, }, - "Notifications".to_string(), + i18n::t("workspace.tooltip.notifications"), keybinding_name_to_display_string(TOGGLE_NOTIFICATION_MAILBOX_BINDING_NAME, ctx), is_inbox_active, false, @@ -18956,10 +19004,10 @@ impl Workspace { const BUTTON_WIDTH: f32 = 24. + SIDE_MENU_WIDTH; const BUTTON_LEFT_MARGIN: f32 = 4.; - let new_tab_tool_tip_label_text = "New Tab".to_string(); + let new_tab_tool_tip_label_text = i18n::t("workspace.tooltip.new_tab"); let new_tab_tool_tip_sublabel_text = keybinding_name_to_display_string(NEW_TAB_BINDING_NAME, ctx); - let tab_configs_tool_tip_label_text = "Tab configs".to_string(); + let tab_configs_tool_tip_label_text = i18n::t("workspace.tooltip.tab_configs"); let tab_configs_tool_tip_sublabel_text = keybinding_name_to_display_string(TOGGLE_TAB_CONFIGS_MENU_BINDING_NAME, ctx); let appearance = Appearance::as_ref(ctx); @@ -19095,7 +19143,7 @@ impl Workspace { let display_name = self .auth_state .username_for_display() - .unwrap_or(DEFAULT_USER_DISPLAY_NAME.to_owned()); + .unwrap_or_else(|| i18n::t("workspace.user.default_display_name")); let avatar_content = if self.auth_state.is_anonymous_or_logged_out() { AvatarContent::Icon(icons::Icon::Gear) @@ -19198,7 +19246,7 @@ impl Workspace { icons::Icon::Lightbulb, &self.mouse_states.resource_center_icon, WorkspaceAction::ToggleResourceCenter, - "Warp Essentials".to_string(), + i18n::t("workspace.tooltip.warp_essentials"), self.cached_keybindings[TOGGLE_RESOURCE_CENTER_KEYBINDING_NAME].clone(), false, false, @@ -19240,7 +19288,7 @@ impl Workspace { icons::Icon::Gear, &self.mouse_states.settings_icon, WorkspaceAction::ShowSettings, - "Settings".to_string(), + i18n::t("workspace.tooltip.settings"), self.cached_keybindings[SHOW_SETTINGS_KEYBINDING_NAME].clone(), false, false, @@ -19281,7 +19329,7 @@ impl Workspace { Some(hovered_styles), None, ) - .with_centered_text_label(String::from("Sign up")); + .with_centered_text_label(i18n::t("workspace.menu.sign_up")); Align::new( button @@ -19323,7 +19371,7 @@ impl Workspace { Some(hovered_styles), None, ) - .with_centered_text_label(String::from("Sign up")); + .with_centered_text_label(i18n::t("workspace.menu.sign_up")); Align::new( button @@ -19339,7 +19387,7 @@ impl Workspace { fn render_offline_button(&self, appearance: &Appearance) -> Box { let ui_builder = appearance.ui_builder().clone(); - let tool_tip_label_text = "Some features may be unavailable offline".to_string(); + let tool_tip_label_text = i18n::t("workspace.tooltip.offline"); let icon = ConstrainedBox::new( Container::new( icons::Icon::CloudOffline @@ -19503,7 +19551,7 @@ impl Workspace { Flex::row() .with_child( Text::new_inline( - UPDATE_READY_TEXT, + i18n::t("workspace.update.update_warp"), appearance.ui_font_family(), PILL_FONT_SIZE, ) @@ -19696,7 +19744,7 @@ impl Workspace { AISettings::as_ref(app) .is_any_ai_enabled(app) .then(|| WorkspaceBannerButtonDetails { - text: "Fix with Oz".to_owned(), + text: i18n::t("workspace.banner.fix_with_oz"), action: WorkspaceAction::FixSettingsWithOz { error_description: error.to_string(), }, @@ -19711,7 +19759,7 @@ impl Workspace { description, secondary_button, button: Some(WorkspaceBannerButtonDetails { - text: "Open file".to_owned(), + text: i18n::t("workspace.banner.open_file"), action: WorkspaceAction::OpenSettingsFile, variant: BannerButtonVariant::Outlined, icon: None, @@ -19737,11 +19785,11 @@ impl Workspace { Some(WorkspaceBannerFields { banner_type: WorkspaceBanner::Reauth, severity: BannerSeverity::Warning, - heading: Some("Your login has expired.".into()), - description: "Please sign in again to restore access to cloud-based features.".into(), + heading: Some(i18n::t("workspace.banner.login_expired.heading")), + description: i18n::t("workspace.banner.login_expired.description"), secondary_button: None, button: Some(WorkspaceBannerButtonDetails { - text: "Sign in".into(), + text: i18n::t("workspace.banner.sign_in"), action: WorkspaceAction::Reauth, variant: BannerButtonVariant::Outlined, icon: None, @@ -19758,10 +19806,9 @@ impl Workspace { { let description = if is_incoming_version_past_current(new_version.soft_cutoff.as_deref()) { - VERSION_DEPRECATION_WITHOUT_PERMISSIONS_BANNER_TEXT.to_owned() + i18n::t("workspace.banner.version_deprecation_without_permissions") } else { - "A new version is available but Warp is unable to perform the update." - .to_owned() + i18n::t("workspace.banner.unable_to_update.description") }; Some(WorkspaceBannerFields { @@ -19771,7 +19818,7 @@ impl Workspace { description, secondary_button: None, button: Some(WorkspaceBannerButtonDetails { - text: "Update Warp manually".to_string(), + text: i18n::t("workspace.banner.update_manually"), action: WorkspaceAction::DownloadNewVersion, variant: BannerButtonVariant::Outlined, icon: None, @@ -19784,9 +19831,9 @@ impl Workspace { { let description = if is_incoming_version_past_current(new_version.soft_cutoff.as_deref()) { - VERSION_DEPRECATION_WITHOUT_PERMISSIONS_BANNER_TEXT.to_owned() + i18n::t("workspace.banner.version_deprecation_without_permissions") } else { - "Warp was unable to launch the new installed version.".to_owned() + i18n::t("workspace.banner.unable_to_launch.description") }; Some(WorkspaceBannerFields { @@ -19796,7 +19843,7 @@ impl Workspace { description, secondary_button: None, button: Some(WorkspaceBannerButtonDetails { - text: "Update Warp manually".to_string(), + text: i18n::t("workspace.banner.update_manually"), action: WorkspaceAction::DownloadNewVersion, variant: BannerButtonVariant::Outlined, icon: None, @@ -19811,10 +19858,10 @@ impl Workspace { banner_type: WorkspaceBanner::VersionDeprecated, severity: BannerSeverity::Error, heading: None, - description: VERSION_DEPRECATION_BANNER_TEXT.to_string(), + description: i18n::t("workspace.banner.version_deprecation"), secondary_button: None, button: Some(WorkspaceBannerButtonDetails { - text: "Update now".to_string(), + text: i18n::t("workspace.banner.update_now"), action: WorkspaceAction::ApplyUpdate, variant: BannerButtonVariant::Outlined, icon: None, @@ -19828,11 +19875,12 @@ impl Workspace { banner_type: WorkspaceBanner::VersionDeprecated, severity: BannerSeverity::Warning, heading: None, - description: "Your app is out of date and needs to update." - .to_string(), + description: i18n::t( + "workspace.banner.out_of_date.description", + ), secondary_button: None, button: Some(WorkspaceBannerButtonDetails { - text: "Restart app and update now".to_string(), + text: i18n::t("workspace.banner.restart_and_update"), action: WorkspaceAction::ApplyUpdate, variant: BannerButtonVariant::Outlined, icon: None, @@ -19940,7 +19988,7 @@ impl Workspace { if let Some(more_info_button_action) = more_info_button_action { let more_info_details = WorkspaceBannerButtonDetails { - text: "More info".to_owned(), + text: i18n::t("workspace.banner.more_info"), action: more_info_button_action, variant: BannerButtonVariant::Outlined, icon: None, @@ -21128,7 +21176,7 @@ impl Workspace { ..Default::default() })), Arc::new(HashMap::new()), - Some("Introducing Oz".to_string()), + Some(i18n::t("workspace.tab_title.introducing_oz")), ctx, ); self.oz_launch_modal.tab_pane_group_id = self @@ -21230,10 +21278,8 @@ impl Workspace { // the browser intentionally obscures the error root cause for privacy reasons. // Many users' browser settings will block Local Network Access so this will end up redirecting to download page, // even if they have the app installed. - let toast_message = format!( - "Have Warp installed but redirecting to download page?\nEnable Local Network Access for {} in your browser.", - ChannelState::server_root_url() - ); + let toast_message = i18n::t("workspace.wasm.local_network_access_required") + .replace("{server}", &ChannelState::server_root_url()); self.toast_stack.update(ctx, |toast_stack, ctx| { toast_stack.add_persistent_toast(DismissibleToast::default(toast_message), ctx) }); @@ -21260,7 +21306,8 @@ impl TypedActionView for Workspace { match action { WorkspaceAction::SetA11yVerbosityLevel(verbosity) => { ActionAccessibilityContent::Custom(AccessibilityContent::new_without_help( - format!("{verbosity:?} accessibility announcements set"), + i18n::t("workspace.a11y.announcements_set") + .replace("{verbosity}", &format!("{verbosity:?}")), WarpA11yRole::UserAction, )) } @@ -22343,13 +22390,21 @@ impl TypedActionView for Workspace { status.is_syncing_all_inputs(window_id) }); - let verb = if enabled { "enabled" } else { "disabled" }; - let mut message = format!("You {verb} synchronized inputs in all tabs."); + let mut message = if enabled { + i18n::t("workspace.toast.synced_inputs_all_tabs_enabled") + } else { + i18n::t("workspace.toast.synced_inputs_all_tabs_disabled") + }; if let Some(keystroke) = keybinding_name_to_keystroke( "workspace:toggle_sync_all_terminal_inputs_in_all_tabs", ctx, ) { - let _ = write!(message, " Press {} to undo.", keystroke.displayed()); + let _ = write!( + message, + "{}", + i18n::t("workspace.toast.press_to_undo") + .replace("{key}", &keystroke.displayed()) + ); } self.toast_stack.update(ctx, |view, ctx| { let new_toast = DismissibleToast::default(message); @@ -22376,13 +22431,21 @@ impl TypedActionView for Workspace { status.should_sync_this_pane_group(current_pane_group_id, window_id) }); - let verb = if enabled { "enabled" } else { "disabled" }; - let mut message = format!("You {verb} synchronized inputs in this tab."); + let mut message = if enabled { + i18n::t("workspace.toast.synced_inputs_tab_enabled") + } else { + i18n::t("workspace.toast.synced_inputs_tab_disabled") + }; if let Some(keystroke) = keybinding_name_to_keystroke( "workspace:toggle_sync_terminal_inputs_in_tab", ctx, ) { - let _ = write!(message, " Press {} to undo.", keystroke.displayed()); + let _ = write!( + message, + "{}", + i18n::t("workspace.toast.press_to_undo") + .replace("{key}", &keystroke.displayed()) + ); } self.toast_stack.update(ctx, |view, ctx| { let new_toast = DismissibleToast::default(message); @@ -22401,8 +22464,9 @@ impl TypedActionView for Workspace { self.process_updated_sync_state(ctx); self.toast_stack.update(ctx, |view, ctx| { - let new_toast = - DismissibleToast::success("Disabled all synchronized inputs.".to_string()); + let new_toast = DismissibleToast::success(i18n::t( + "workspace.toast.disabled_synced_inputs", + )); view.add_ephemeral_toast(new_toast, ctx); }); send_telemetry_from_ctx!(TelemetryEvent::DisableInputSync, ctx); @@ -22521,7 +22585,8 @@ impl TypedActionView for Workspace { } RunAISuggestedCommand(code) => { let command = code.trim().to_string(); - let workflow = Workflow::new("Command from Oz", command); + let workflow = + Workflow::new(i18n::t("workspace.workflow.command_from_oz"), command); self.run_workflow_in_active_input( &WorkflowType::AIGenerated { workflow, @@ -23066,7 +23131,7 @@ impl TypedActionView for Workspace { let entry = format!("file://{}", plugin_path.display()); set_opencode_warp_plugin(&entry) } - None => "Failed to determine home directory".to_string(), + None => i18n::t("workspace.opencode.failed_home_dir"), }; self.toast_stack.update(ctx, |view, ctx| { view.add_ephemeral_toast(DismissibleToast::default(message), ctx); @@ -23086,7 +23151,7 @@ impl TypedActionView for Workspace { self.toast_stack.update(ctx, |view, ctx| { view.add_ephemeral_toast( - DismissibleToast::default("Sampling process for 3 seconds...".to_string()), + DismissibleToast::default(i18n::t("workspace.toast.sampling_process")), ctx, ); }); @@ -23147,20 +23212,21 @@ impl TypedActionView for Workspace { } } - format!("Process sample saved to {output_path}") + i18n::t("workspace.toast.process_sample_saved") + .replace("{path}", &output_path) } Ok(Ok(output)) => { let stderr = String::from_utf8_lossy(&output.stderr); log::error!("sample command failed ({}): {stderr}", output.status); - "Failed to sample process (check logs)".to_string() + i18n::t("workspace.toast.process_sample_failed") } Ok(Err(io_err)) => { log::error!("Failed to run sample command: {io_err}"); - "Failed to sample process (check logs)".to_string() + i18n::t("workspace.toast.process_sample_failed") } Err(join_err) => { log::error!("Sample task panicked: {join_err}"); - "Failed to sample process (check logs)".to_string() + i18n::t("workspace.toast.process_sample_failed") } }; me.toast_stack.update(ctx, |view, ctx| { @@ -23308,9 +23374,9 @@ impl TypedActionView for Workspace { if !succesfully_exited_agent_view { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "Failed to delete conversation. Please exit the agent view and try again.".to_string(), - ), + DismissibleToast::error(i18n::t( + "workspace.toast.delete_conversation_failed", + )), window_id, ctx, ); @@ -23324,7 +23390,7 @@ impl TypedActionView for Workspace { send_telemetry_from_ctx!(TelemetryEvent::ConversationListItemDeleted, ctx); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::success("Conversation deleted".to_string()), + DismissibleToast::success(i18n::t("workspace.toast.conversation_deleted")), window_id, ctx, ); @@ -25594,7 +25660,7 @@ fn compute_default_panel_widths( #[cfg(debug_assertions)] fn set_opencode_warp_plugin(new_entry: &str) -> String { let Some(home) = dirs::home_dir() else { - return "Failed to determine home directory".to_string(); + return i18n::t("workspace.opencode.failed_home_dir"); }; let config_dir = home.join(".config/opencode"); @@ -25604,9 +25670,15 @@ fn set_opencode_warp_plugin(new_entry: &str) -> String { match std::fs::read_to_string(&config_path) { Ok(contents) => match serde_json::from_str(&contents) { Ok(val) => val, - Err(e) => return format!("Failed to parse opencode.json: {e}"), + Err(e) => { + return i18n::t("workspace.opencode.failed_parse") + .replace("{error}", &e.to_string()); + } }, - Err(e) => return format!("Failed to read opencode.json: {e}"), + Err(e) => { + return i18n::t("workspace.opencode.failed_read") + .replace("{error}", &e.to_string()); + } } } else { serde_json::json!({ @@ -25621,7 +25693,7 @@ fn set_opencode_warp_plugin(new_entry: &str) -> String { }); let Some(plugins) = plugins else { - return "opencode.json has unexpected structure (plugin is not an array)".to_string(); + return i18n::t("workspace.opencode.unexpected_structure"); }; // Remove any existing opencode-warp entries @@ -25633,14 +25705,15 @@ fn set_opencode_warp_plugin(new_entry: &str) -> String { plugins.push(serde_json::Value::String(new_entry.to_string())); if let Err(e) = std::fs::create_dir_all(&config_dir) { - return format!("Failed to create config directory: {e}"); + return i18n::t("workspace.opencode.failed_create_config_dir") + .replace("{error}", &e.to_string()); } match serde_json::to_string_pretty(&config) { Ok(json_str) => match std::fs::write(&config_path, format!("{json_str}\n")) { - Ok(()) => format!("OpenCode plugin set to: {new_entry}"), - Err(e) => format!("Failed to write opencode.json: {e}"), + Ok(()) => i18n::t("workspace.opencode.plugin_set").replace("{entry}", new_entry), + Err(e) => i18n::t("workspace.opencode.failed_write").replace("{error}", &e.to_string()), }, - Err(e) => format!("Failed to serialize opencode.json: {e}"), + Err(e) => i18n::t("workspace.opencode.failed_serialize").replace("{error}", &e.to_string()), } } diff --git a/app/src/workspace/view/build_plan_migration_modal.rs b/app/src/workspace/view/build_plan_migration_modal.rs index c3b6f81699..77bbd97924 100644 --- a/app/src/workspace/view/build_plan_migration_modal.rs +++ b/app/src/workspace/view/build_plan_migration_modal.rs @@ -193,7 +193,7 @@ impl BuildPlanMigrationModal { UserWorkspacesEvent::UpdateWorkspaceSettingsRejected(_err) => { self.is_updating = false; ctx.emit(BuildPlanMigrationModalEvent::ShowToast { - message: "Failed to enable auto-reload. Please try updating your settings in Billing & usage.".to_string(), + message: i18n::t("workspace.build_plan_migration.auto_reload_failed"), flavor: ToastFlavor::Error, }); ctx.notify(); @@ -266,12 +266,16 @@ impl BuildPlanMigrationModal { }) .finish(); - let label = FormattedTextElement::from_str("Auto-reload", appearance.ui_font_family(), 12.) - .with_color(blended_colors::text_sub( - theme, - blended_colors::neutral_4(theme), - )) - .finish(); + let label = FormattedTextElement::from_str( + i18n::t("settings.billing.addon.auto_reload_label"), + appearance.ui_font_family(), + 12., + ) + .with_color(blended_colors::text_sub( + theme, + blended_colors::neutral_4(theme), + )) + .finish(); let checkbox_row = Flex::row() .with_child(checkbox) @@ -304,9 +308,9 @@ impl BuildPlanMigrationModal { fn render_get_started_button(&self, appearance: &Appearance) -> Box { let button_text = if self.is_updating { - "Saving...".to_string() + i18n::t("workspace.build_plan_migration.saving") } else { - "Get Started".to_string() + i18n::t("workspace.build_plan_migration.get_started") }; let button_font_color = self.is_updating.then_some( @@ -354,7 +358,7 @@ impl BuildPlanMigrationModal { let theme = appearance.theme(); let title = Self::create_text( - "Use auto-reload to never miss a beat.".to_string(), + i18n::t("workspace.build_plan_migration.auto_reload_title"), appearance.ui_font_family(), 16., blended_colors::text_main(theme, blended_colors::neutral_2(theme)), @@ -362,7 +366,7 @@ impl BuildPlanMigrationModal { ); let description = Self::create_text( - "Auto-reload will automatically purchase credits at your selected rate when your account balance reaches 100 credits. Your monthly spend limit is set at your legacy plan's monthly cost and can be updated in Settings > Billing & usage.".to_string(), + i18n::t("workspace.build_plan_migration.auto_reload_description"), appearance.ui_font_family(), 14., blended_colors::text_sub(theme, blended_colors::neutral_4(theme)), @@ -514,13 +518,13 @@ impl BuildPlanMigrationModal { .unwrap_or((2000, 1800)); let title_text = if is_business { - "Welcome to the New Business Plan" + i18n::t("workspace.build_plan.welcome_business") } else { - "Welcome to Warp Build" + i18n::t("workspace.build_plan.welcome_build") }; let title = Self::create_text( - title_text.to_string(), + title_text, font_family, 24., blended_colors::text_main(theme, blended_colors::neutral_2(theme)), @@ -528,20 +532,19 @@ impl BuildPlanMigrationModal { ); let intro_text = if is_business { - "Your workspace has been updated to the new Warp Business Plan as the legacy Business plan is sunset." + i18n::t("workspace.build_plan.intro_business") } else { - "Your workspace has been updated to the Warp Build Plan as the legacy Pro, Turbo, and Lightspeed plans are sunset." + i18n::t("workspace.build_plan.intro_build") }; - let intro = Self::create_text(intro_text.to_string(), font_family, 14., text_color, None); + let intro = Self::create_text(intro_text, font_family, 14., text_color, None); let pricing_header = Self::create_text( if is_business { - "The new Business plan is a primarily usage-based plan, starting at:" + i18n::t("workspace.build_plan.pricing_header_business") } else { - "Warp Build is a primarily usage-based plan, starting at:" - } - .to_string(), + i18n::t("workspace.build_plan.pricing_header_build") + }, font_family, 14., text_color, @@ -549,17 +552,16 @@ impl BuildPlanMigrationModal { ); let price_monthly = Self::create_bullet_item( - format!("${} per user per month", base_plan_prices.0 / 100), + i18n::t("workspace.build_plan.price_per_user_month") + .replace("{price}", &(base_plan_prices.0 / 100).to_string()), font_family, 14., text_color, ); let price_annual = Self::create_bullet_item( - format!( - "${} per user per month for annual plans", - base_plan_prices.1 / 100 - ), + i18n::t("workspace.build_plan.price_per_user_month_annual") + .replace("{price}", &(base_plan_prices.1 / 100).to_string()), font_family, 14., text_color, @@ -567,11 +569,10 @@ impl BuildPlanMigrationModal { let features_header = Self::create_text( if is_business { - "The new Business plan comes with:" + i18n::t("workspace.build_plan.features_header_business") } else { - "Build comes with:" - } - .to_string(), + i18n::t("workspace.build_plan.features_header_build") + }, font_family, 14., text_color, @@ -579,24 +580,22 @@ impl BuildPlanMigrationModal { ); let base_credits = Self::create_bullet_item( - format!( - "{} base credits per month", - base_credits_limit.separate_with_commas() - ), + i18n::t("workspace.build_plan.base_credits_per_month") + .replace("{credits}", &base_credits_limit.separate_with_commas()), font_family, 14., text_color, ); let reload_credits = Self::create_bullet_item( - "Access to Reload credits and volume-based discounts".to_string(), + i18n::t("workspace.build_plan.reload_credits_discounts"), font_family, 14., text_color, ); let byok = Self::create_bullet_item( - "Bring your own API key".to_string(), + i18n::t("workspace.build_plan.bring_your_own_api_key"), font_family, 14., text_color, @@ -610,7 +609,7 @@ impl BuildPlanMigrationModal { if is_business { let sso = Self::create_bullet_item( - "SAML-based SSO".to_string(), + i18n::t("workspace.build_plan.saml_sso"), font_family, 14., text_color, @@ -618,7 +617,7 @@ impl BuildPlanMigrationModal { features_list.add_child(sso); let zdr = Self::create_bullet_item( - "Automatically enforced team-wide Zero Data Retention".to_string(), + i18n::t("workspace.build_plan.zero_data_retention"), font_family, 14., text_color, @@ -626,13 +625,20 @@ impl BuildPlanMigrationModal { features_list.add_child(zdr); } - let and_more = - Self::create_bullet_item("And more...".to_string(), font_family, 14., text_color); + let and_more = Self::create_bullet_item( + i18n::t("workspace.build_plan.and_more"), + font_family, + 14., + text_color, + ); features_list.add_child(and_more); let learn_more_fragments = vec![ - FormattedTextFragment::plain_text("Learn more on our "), - FormattedTextFragment::hyperlink("pricing page", "https://www.warp.dev/pricing"), + FormattedTextFragment::plain_text(i18n::t("workspace.build_plan.learn_more_prefix")), + FormattedTextFragment::hyperlink( + i18n::t("workspace.build_plan.pricing_page"), + "https://www.warp.dev/pricing", + ), FormattedTextFragment::plain_text("."), ]; let learn_more = Container::new( @@ -790,8 +796,7 @@ impl TypedActionView for BuildPlanMigrationModal { let workspaces = UserWorkspaces::as_ref(ctx); let Some(team_uid) = workspaces.current_team_uid() else { ctx.emit(BuildPlanMigrationModalEvent::ShowToast { - message: "Oops, something went wrong; your team data could not be found." - .to_string(), + message: i18n::t("workspace.build_plan_migration.team_data_not_found"), flavor: ToastFlavor::Error, }); return; diff --git a/app/src/workspace/view/cloud_agent_capacity_modal/mod.rs b/app/src/workspace/view/cloud_agent_capacity_modal/mod.rs index 13851802c4..b5a2e5d6e1 100644 --- a/app/src/workspace/view/cloud_agent_capacity_modal/mod.rs +++ b/app/src/workspace/view/cloud_agent_capacity_modal/mod.rs @@ -126,20 +126,28 @@ impl CloudAgentCapacityModal { let neutral_bg = blended_colors::neutral_1(theme); let (title_text, mut explanation_text) = match self.variant { CloudAgentCapacityModalVariant::ConcurrentLimit => ( - "Concurrent cloud agent limit reached", - "This cloud run is queued because your team has reached the maximum number of concurrent cloud agents. It will start automatically when another cloud run finishes.".to_string(), + i18n::t("workspace.cloud_capacity.concurrent_limit.title"), + i18n::t("workspace.cloud_capacity.concurrent_limit.description"), ), CloudAgentCapacityModalVariant::OutOfCredits => ( - "You're out of AI credits", - "This cloud run stopped because your team has used all available AI credits for the current billing period.".to_string(), + i18n::t("workspace.cloud_capacity.out_of_credits.title"), + i18n::t("workspace.cloud_capacity.out_of_credits.description"), ), }; // Title - let title = FormattedTextElement::from_str(title_text, appearance.ui_font_family(), 24.) - .with_color(blended_colors::text_main(theme, neutral_bg)) - .with_weight(Weight::Bold) - .finish(); + let title = FormattedTextElement::new( + FormattedText::new([FormattedTextLine::Line(vec![ + FormattedTextFragment::plain_text(title_text), + ])]), + 24., + appearance.ui_font_family(), + appearance.ui_font_family(), + blended_colors::text_main(theme, neutral_bg), + HighlightedHyperlink::default(), + ) + .with_weight(Weight::Bold) + .finish(); // Explanation. let can_upgrade = Self::can_upgrade(customer_type, self.variant); @@ -147,13 +155,13 @@ impl CloudAgentCapacityModal { if can_upgrade { let upgrade_suffix = match self.variant { CloudAgentCapacityModalVariant::ConcurrentLimit => { - " Upgrade your plan for more concurrent cloud agents." + i18n::t("workspace.cloud_capacity.concurrent_limit.upgrade_suffix") } CloudAgentCapacityModalVariant::OutOfCredits => { - " Upgrade your plan to continue running cloud agents." + i18n::t("workspace.cloud_capacity.out_of_credits.upgrade_suffix") } }; - explanation_text.push_str(upgrade_suffix); + explanation_text.push_str(&upgrade_suffix); } let subtitle = FormattedTextElement::from_str(explanation_text, appearance.ui_font_family(), 14.) @@ -182,19 +190,17 @@ impl CloudAgentCapacityModal { let pricing_text = if customer_type == CustomerType::Free { if let Some(pricing) = plan_pricing { let price = pricing.yearly_plan_price_per_month_usd_cents / 100; - format!( - "Paid plans start at ${price}/month and include everything in your free trial plus:" - ) + i18n::t("workspace.cloud_capacity.paid_plans_start") + .replace("{price}", &price.to_string()) } else { - "Paid plans include everything in your free trial plus:".to_string() + i18n::t("workspace.cloud_capacity.paid_plans_include") } } else if let Some(pricing) = plan_pricing { let price = pricing.yearly_plan_price_per_month_usd_cents / 100; - format!( - "The Business plan starts at ${price}/month and includes everything on your current plan plus:" - ) + i18n::t("workspace.cloud_capacity.business_plan_starts") + .replace("{price}", &price.to_string()) } else { - "The Business plan includes everything on your current plan plus:".to_string() + i18n::t("workspace.cloud_capacity.business_plan_includes") }; let pricing = FormattedTextElement::new( @@ -212,16 +218,18 @@ impl CloudAgentCapacityModal { // Credits text from plan pricing let credits_text = if let Some(limit) = plan_pricing.and_then(|plan| plan.request_limit) { - format!("{} AI credits per month", limit.separate_with_commas()) + i18n::t("workspace.cloud_capacity.ai_credits_per_month") + .replace("{credits}", &limit.separate_with_commas()) } else { - "Extended AI credits per month".to_string() + i18n::t("workspace.cloud_capacity.extended_ai_credits") }; // Benefits list based on plan type let mut benefits = vec![ - format!("{} the number of concurrent cloud agents", agent_multiplier), + i18n::t("workspace.cloud_capacity.concurrent_agents_multiplier") + .replace("{multiplier}", agent_multiplier), credits_text, - "Bring your own API key".to_string(), + i18n::t("workspace.build_plan.bring_your_own_api_key"), ]; for extra in extra_benefits { benefits.push(extra.to_string()); @@ -276,9 +284,9 @@ impl CloudAgentCapacityModal { let content = content.finish(); let cta_button = if show_cta { let cta_button_label = if can_upgrade { - "Upgrade plan" + i18n::t("workspace.cloud_capacity.upgrade_plan") } else { - "Open billing" + i18n::t("workspace.cloud_capacity.open_billing") }; Some( appearance diff --git a/app/src/workspace/view/codex_modal.rs b/app/src/workspace/view/codex_modal.rs index 42c7ac653e..84180ca6ac 100644 --- a/app/src/workspace/view/codex_modal.rs +++ b/app/src/workspace/view/codex_modal.rs @@ -71,12 +71,15 @@ pub struct CodexModal { impl CodexModal { pub fn new(ctx: &mut ViewContext) -> Self { let cta_button = ctx.add_view(|_| { - ActionButton::new("Use latest codex model", WhiteButtonTheme) - .with_icon(Icon::OpenAILogo) - .with_full_width(true) - .on_click(|ctx| { - ctx.dispatch_typed_action(CodexModalAction::UseCodex); - }) + ActionButton::new( + i18n::t("workspace.codex_modal.use_latest_model"), + WhiteButtonTheme, + ) + .with_icon(Icon::OpenAILogo) + .with_full_width(true) + .on_click(|ctx| { + ctx.dispatch_typed_action(CodexModalAction::UseCodex); + }) }); CodexModal { @@ -90,7 +93,7 @@ impl CodexModal { // Magenta/pink color for the badge let magenta: ColorU = theme.terminal_colors().normal.magenta.into(); Container::new( - Text::new("New", appearance.ui_font_family(), 12.) + Text::new(i18n::t("common.new"), appearance.ui_font_family(), 12.) .with_color(magenta) .finish(), ) @@ -110,7 +113,7 @@ impl CodexModal { // Title let title = FormattedTextElement::from_str( - "Use Codex models in Warp", + i18n::t("workspace.codex_modal.title"), appearance.ui_font_family(), 24., ) @@ -123,7 +126,7 @@ impl CodexModal { // Description - first paragraph let description_1 = FormattedTextElement::from_str( - "Codex is OpenAI's most advanced agentic coding model for real-world engineering.", + i18n::t("workspace.codex_modal.description_1"), appearance.ui_font_family(), 14., ) @@ -135,8 +138,7 @@ impl CodexModal { // Description - second paragraph let description_2 = FormattedTextElement::from_str( - "Use Codex directly in Oz and leverage \ - features like in-app code review, agent session sharing and file editing.", + i18n::t("workspace.codex_modal.description_2"), appearance.ui_font_family(), 14., ) diff --git a/app/src/workspace/view/conversation_list/item.rs b/app/src/workspace/view/conversation_list/item.rs index 8d26a0252b..d3ce16faf2 100644 --- a/app/src/workspace/view/conversation_list/item.rs +++ b/app/src/workspace/view/conversation_list/item.rs @@ -124,7 +124,7 @@ pub fn render_static_item(props: StaticItemProps<'_>, app: &AppContext) -> Box "ACTIVE", - ConversationSection::Past => "PAST", + ConversationSection::Active => i18n::t("workspace.conversations.section.active"), + ConversationSection::Past => i18n::t("workspace.conversations.section.past"), }, appearance.ui_font_family(), 11., @@ -878,9 +885,9 @@ impl TypedActionView for ConversationListView { if !conversation_is_done { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "Conversations cannot be deleted while in progress.".to_string(), - ), + DismissibleToast::error(i18n::t( + "workspace.conversations.delete.in_progress_error", + )), window_id, ctx, ); @@ -894,7 +901,7 @@ impl TypedActionView for ConversationListView { .as_ref(ctx) .get_item_by_id(&id, ctx) .map(|entry| entry.display.title) - .unwrap_or_else(|| "Conversation".to_string()); + .unwrap_or_else(|| i18n::t("workspace.conversations.fallback_title")); ctx.emit(Event::ShowDeleteConfirmationDialog { conversation_id: *conversation_id, conversation_title, @@ -925,21 +932,25 @@ impl TypedActionView for ConversationListView { return; }; - let mut delete_item = MenuItemFields::new("Delete") - .with_override_text_color(Appearance::as_ref(ctx).theme().ansi_fg_red()) - .with_on_select_action(ConversationListViewAction::DeleteFromOverflowMenu { - conversation_id, - }) - .with_disabled(!entry.capabilities.can_delete); + let mut delete_item = + MenuItemFields::new(i18n::t("workspace.conversations.menu.delete")) + .with_override_text_color(Appearance::as_ref(ctx).theme().ansi_fg_red()) + .with_on_select_action( + ConversationListViewAction::DeleteFromOverflowMenu { + conversation_id, + }, + ) + .with_disabled(!entry.capabilities.can_delete); if !entry.capabilities.can_delete { - delete_item = - delete_item.with_tooltip("This conversation cannot be deleted"); + delete_item = delete_item.with_tooltip(i18n::t( + "workspace.conversations.menu.delete_disabled_tooltip", + )); } // Only show share item if the conversation is shareable let share_item = if entry.capabilities.can_share { Some( - MenuItemFields::new("Share conversation") + MenuItemFields::new(i18n::t("workspace.conversations.menu.share")) .with_on_select_action( ConversationListViewAction::OpenShareDialog { conversation_id }, ) @@ -953,7 +964,7 @@ impl TypedActionView for ConversationListView { // Forking from a closed ambient agent conversation is not supported at this point. if entry.capabilities.can_fork_locally { Some([ - MenuItemFields::new("Fork in new pane") + MenuItemFields::new(i18n::t("workspace.conversations.menu.fork_new_pane")) .with_on_select_action( ConversationListViewAction::ForkConversation { conversation_id, @@ -961,7 +972,7 @@ impl TypedActionView for ConversationListView { }, ) .into_item(), - MenuItemFields::new("Fork in new tab") + MenuItemFields::new(i18n::t("workspace.conversations.menu.fork_new_tab")) .with_on_select_action( ConversationListViewAction::ForkConversation { conversation_id, @@ -1040,10 +1051,9 @@ impl TypedActionView for ConversationListView { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( - DismissibleToast::error( - "Conversations cannot be deleted while in progress." - .to_string(), - ), + DismissibleToast::error(i18n::t( + "workspace.conversations.delete.in_progress_error", + )), window_id, ctx, ); @@ -1109,9 +1119,9 @@ impl TypedActionView for ConversationListView { self.view_all = !self.view_all; let label = if self.view_all { - "Show less" + i18n::t("workspace.conversations.show_less") } else { - VIEW_ALL_LABEL + i18n::t("workspace.conversations.view_all") }; self.toggle_view_all_button .update(ctx, |button, ctx| button.set_label(label, ctx)); @@ -1178,7 +1188,7 @@ impl View for ConversationListView { } else if self.item_count() == 0 { Container::new( Text::new_inline( - "No matching conversations", + i18n::t("workspace.conversations.no_matches"), appearance.ui_font_family(), appearance.ui_font_size(), ) diff --git a/app/src/workspace/view/crash_recovery.rs b/app/src/workspace/view/crash_recovery.rs index d7dc76b63e..a0ee10e33d 100644 --- a/app/src/workspace/view/crash_recovery.rs +++ b/app/src/workspace/view/crash_recovery.rs @@ -15,13 +15,10 @@ pub fn banner_metadata(ctx: &AppContext) -> Option { banner_type: super::WorkspaceBanner::WaylandCrashRecovery, severity: super::BannerSeverity::Warning, heading: None, - description: "We detected a crash during application startup, and adjusted your \ - settings to use Xwayland for windowing. This can result in blurry text if you \ - are using fractional scaling." - .to_owned(), + description: i18n::t("workspace.crash_recovery.wayland.description"), secondary_button: None, button: Some(super::WorkspaceBannerButtonDetails { - text: "Learn More".to_owned(), + text: i18n::t("common.learn_more"), action: super::WorkspaceAction::DismissWaylandCrashRecoveryBannerAndOpenLink, variant: super::BannerButtonVariant::Outlined, icon: None, diff --git a/app/src/workspace/view/free_tier_limit_hit_modal.rs b/app/src/workspace/view/free_tier_limit_hit_modal.rs index 16f677fb28..0141f8241b 100644 --- a/app/src/workspace/view/free_tier_limit_hit_modal.rs +++ b/app/src/workspace/view/free_tier_limit_hit_modal.rs @@ -148,7 +148,7 @@ impl FreeTierLimitHitModal { .with_child( Container::new( FormattedTextElement::from_str( - "You’re out of credits", + i18n::t("workspace.free_tier.out_of_credits"), appearance.ui_font_family(), 24., ) @@ -165,7 +165,7 @@ impl FreeTierLimitHitModal { .with_child( Container::new( FormattedTextElement::from_str( - "To continue using AI, please upgrade your plan.", + i18n::t("workspace.free_tier.upgrade_to_continue"), appearance.ui_font_family(), 14., ) @@ -182,9 +182,10 @@ impl FreeTierLimitHitModal { Container::new({ let benefits_text = if let Some(plan) = Self::get_build_plan_details(app) { let price = plan.monthly_plan_price_per_month_usd_cents / 100; - format!("The Build plan is ${price}/month which includes everything in the free tier plus:") + i18n::t("workspace.free_tier.build_plan_price") + .replace("{price}", &price.to_string()) } else { - "The Build plan includes everything in the free tier plus:".to_string() + i18n::t("workspace.free_tier.build_plan_includes") }; let formatted_text = FormattedText::new([FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text(benefits_text), @@ -206,9 +207,10 @@ impl FreeTierLimitHitModal { Container::new({ let credits_text = if let Some(plan) = Self::get_build_plan_details(app) { let limit = plan.request_limit.unwrap_or(1500); - format!("{} Credits per month", limit.separate_with_commas()) + i18n::t("workspace.free_tier.credits_per_month") + .replace("{credits}", &limit.separate_with_commas()) } else { - "Extended Credits per month".to_string() + i18n::t("workspace.free_tier.extended_credits") }; Self::render_checklist_item_dynamic(credits_text, appearance, theme) }) @@ -218,7 +220,7 @@ impl FreeTierLimitHitModal { .with_child( Container::new( Self::render_checklist_item_dynamic( - "Access to frontier OpenAI, Anthropic, and Google models".to_string(), + i18n::t("workspace.free_tier.frontier_models"), appearance, theme, ) @@ -229,9 +231,9 @@ impl FreeTierLimitHitModal { .with_child( Container::new({ let formatted_text = FormattedText::new([FormattedTextLine::Line(vec![ - FormattedTextFragment::plain_text("Access to "), + FormattedTextFragment::plain_text(i18n::t("workspace.free_tier.access_to_prefix")), FormattedTextFragment::hyperlink( - "Reload Credits".to_string(), + i18n::t("workspace.free_tier.reload_credits"), "https://docs.warp.dev/support-and-community/plans-and-billing/add-on-credits".to_string(), ), ])]); @@ -274,7 +276,7 @@ impl FreeTierLimitHitModal { Container::new({ let formatted_text = FormattedText::new([FormattedTextLine::Line(vec![ FormattedTextFragment::hyperlink( - "Extended cloud agents access".to_string(), + i18n::t("workspace.free_tier.extended_cloud_agents_access"), "https://www.warp.dev/oz".to_string(), ), ])]); @@ -328,7 +330,7 @@ impl FreeTierLimitHitModal { width: Some(296.), ..Default::default() }) - .with_centered_text_label("Upgrade plan".to_string()) + .with_centered_text_label(i18n::t("workspace.free_tier.upgrade_plan")) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { diff --git a/app/src/workspace/view/global_search/model.rs b/app/src/workspace/view/global_search/model.rs index c0b110c483..2c4f7d75f8 100644 --- a/app/src/workspace/view/global_search/model.rs +++ b/app/src/workspace/view/global_search/model.rs @@ -108,7 +108,7 @@ impl GlobalSearch { log::error!("GlobalSearch: warp_ripgrep CLI search failed or aborted: {err}"); ctx.emit(GlobalSearchEvent::Failed { search_id, - error: "Global search failed.".to_string(), + error: i18n::t("workspace.search.failed"), }); } }, diff --git a/app/src/workspace/view/global_search/view.rs b/app/src/workspace/view/global_search/view.rs index 6e622d9e6c..8cf205723d 100644 --- a/app/src/workspace/view/global_search/view.rs +++ b/app/src/workspace/view/global_search/view.rs @@ -650,7 +650,7 @@ impl GlobalSearchView { }; let mut editor = EditorView::new(options, ctx); - editor.set_placeholder_text("Search in files", ctx); + editor.set_placeholder_text(i18n::t("workspace.search.placeholder"), ctx); editor }); @@ -664,7 +664,7 @@ impl GlobalSearchView { let case_sensitivity_button = ctx.add_typed_action_view(|_ctx| { ActionButton::new_with_boxed_theme(String::new(), Arc::new(NakedTheme)) .with_icon(UiIcon::CaseSensitivity) - .with_tooltip("Toggle Case Sensitivity") + .with_tooltip(i18n::t("workspace.search.toggle_case_sensitivity")) .with_size(ButtonSize::Small) .on_click(|ctx| { ctx.dispatch_typed_action(GlobalSearchAction::ToggleCaseSensitivity); @@ -674,7 +674,7 @@ impl GlobalSearchView { let regex_button = ctx.add_typed_action_view(|_ctx| { ActionButton::new_with_boxed_theme(String::new(), Arc::new(NakedTheme)) .with_icon(UiIcon::Regex) - .with_tooltip("Toggle Regex") + .with_tooltip(i18n::t("workspace.search.toggle_regex")) .with_size(ButtonSize::Small) .on_click(|ctx| { ctx.dispatch_typed_action(GlobalSearchAction::ToggleRegexSearch); @@ -2010,9 +2010,13 @@ impl View for GlobalSearchView { let appearance = Appearance::as_ref(app); let theme = appearance.theme(); - let search_label = Text::new_inline("Search", appearance.ui_font_family(), 14.) - .with_color(blended_colors::text_sub(theme, theme.background())) - .finish(); + let search_label = Text::new_inline( + i18n::t("workspace.search.label"), + appearance.ui_font_family(), + 14., + ) + .with_color(blended_colors::text_sub(theme, theme.background())) + .finish(); let editor_line_height = self .query_editor @@ -2064,16 +2068,18 @@ impl View for GlobalSearchView { .with_child(query_row); let files = self.unique_match_count(); - let file_word = if files == 1 { "file" } else { "files" }; let message = if self.is_search_in_progress && self.total_match_count == 0 { "".to_string() } else if !self.is_search_in_progress && self.total_match_count == 0 { - "No results found. Review your gitignore files.".to_string() + i18n::t("workspace.search.no_results") } else { match self.total_match_count { - 1 => format!("1 result in {files} {file_word}"), - n => format!("{n} results in {files} {file_word}"), + 1 => i18n::t("workspace.search.results_count.single") + .replace("{files}", &files.to_string()), + n => i18n::t("workspace.search.results_count.multiple") + .replace("{n}", &n.to_string()) + .replace("{files}", &files.to_string()), } }; @@ -2095,7 +2101,7 @@ impl View for GlobalSearchView { font_color: Some(blended_colors::text_sub(theme, theme.background())), ..Default::default() }; - let capped_message = "The result set only contains a subset of all matches. Be more specific in your search to narrow down results.".to_string(); + let capped_message = i18n::t("workspace.search.capped"); let capped_text = Span::new(capped_message, capped_text_styles) .with_soft_wrap() .build() @@ -2246,8 +2252,8 @@ impl GlobalSearchView { fn render_pre_search_state(&self, app: &AppContext) -> Box { self.render_zero_state( Icon::Search, - "Global search", - "Search in files across your current directories.", + i18n::t("workspace.search.zero_state.title"), + i18n::t("workspace.search.zero_state.body"), app, ) } @@ -2255,8 +2261,8 @@ impl GlobalSearchView { fn render_unavailable_state(&self, app: &AppContext) -> Box { self.render_zero_state( Icon::AlertTriangle, - "Global search unavailable", - "Global search requires access to your local workspace. Open a new session or navigate to an active session to view.", + i18n::t("workspace.search.unavailable.title"), + i18n::t("workspace.search.unavailable.body"), app, ) } @@ -2264,8 +2270,8 @@ impl GlobalSearchView { fn render_remote_state(&self, app: &AppContext) -> Box { self.render_zero_state( Icon::AlertTriangle, - "Global search unavailable", - "Global search requires access to your local workspace, which isn't supported in remote sessions", + i18n::t("workspace.search.unavailable.title"), + i18n::t("workspace.search.unavailable.remote_body"), app, ) } @@ -2273,8 +2279,8 @@ impl GlobalSearchView { fn render_unsupported_session_state(&self, app: &AppContext) -> Box { self.render_zero_state( Icon::AlertTriangle, - "Global search unavailable", - "Global search doesn't currently work in Git Bash or WSL.", + i18n::t("workspace.search.unavailable.title"), + i18n::t("workspace.search.unavailable.unsupported_body"), app, ) } diff --git a/app/src/workspace/view/launch_modal/mod.rs b/app/src/workspace/view/launch_modal/mod.rs index c630186680..69e4316531 100644 --- a/app/src/workspace/view/launch_modal/mod.rs +++ b/app/src/workspace/view/launch_modal/mod.rs @@ -78,11 +78,11 @@ where fn first() -> Self; fn next(&self) -> Option; fn prev(&self) -> Option; - fn display_text(&self) -> Option<&'static str>; - fn short_label(&self) -> &'static str; - fn title(&self) -> &'static str; + fn display_text(&self) -> Option; + fn short_label(&self) -> String; + fn title(&self) -> String; fn title_icon(&self) -> Option; - fn content(&self) -> &'static str; + fn content(&self) -> String; fn image(&self) -> AssetSource; fn all() -> Vec; fn cta_button(&self) -> CTAButton; @@ -354,8 +354,9 @@ impl LaunchModal { .with_main_axis_size(MainAxisSize::Max) .with_child( Container::new({ + let title = self.slide.title(); let text = FormattedTextElement::from_str( - self.slide.title(), + title, appearance.ui_font_family(), 16., ) @@ -400,10 +401,10 @@ impl LaunchModal { ) .with_child( Container::new( - Shrinkable::new( - 1., + Shrinkable::new(1., { + let content = self.slide.content(); FormattedTextElement::new( - parse_markdown(self.slide.content()).unwrap(), + parse_markdown(&content).unwrap(), 14., appearance.ui_font_family(), appearance.ui_font_family(), @@ -423,8 +424,8 @@ impl LaunchModal { } }, ) - .finish(), - ) + .finish() + }) .finish(), ) .with_margin_bottom(8.) diff --git a/app/src/workspace/view/launch_modal/oz_launch.rs b/app/src/workspace/view/launch_modal/oz_launch.rs index 8cf676e6c2..3b525f0c7f 100644 --- a/app/src/workspace/view/launch_modal/oz_launch.rs +++ b/app/src/workspace/view/launch_modal/oz_launch.rs @@ -23,14 +23,12 @@ pub enum OzLaunchSlide { impl Slide for OzLaunchSlide { fn modal_title(&self) -> String { - "Introducing Oz".to_string() + i18n::t("workspace.oz_launch.modal_title") } fn modal_subtext_paragraphs(&self) -> Vec { vec![FormattedTextLine::Line(vec![ - FormattedTextFragment::plain_text( - "Infinitely scalable coding agent — run in local sessions or in the cloud.", - ), + FormattedTextFragment::plain_text(i18n::t("workspace.oz_launch.description")), ])] } @@ -56,34 +54,36 @@ impl Slide for OzLaunchSlide { } } - fn display_text(&self) -> Option<&'static str> { + fn display_text(&self) -> Option { Some(match self { - OzLaunchSlide::CloudAgents => "Cloud agents", - OzLaunchSlide::AgentAutomations => "Agent automations", - OzLaunchSlide::AgentManagement => "Agent management", - OzLaunchSlide::LaunchCredits => "A little gift", + OzLaunchSlide::CloudAgents => i18n::t("workspace.oz_launch.slide.cloud_agents"), + OzLaunchSlide::AgentAutomations => { + i18n::t("workspace.oz_launch.slide.agent_automations") + } + OzLaunchSlide::AgentManagement => i18n::t("workspace.oz_launch.slide.agent_management"), + OzLaunchSlide::LaunchCredits => i18n::t("workspace.oz_launch.slide.gift"), }) } - fn short_label(&self) -> &'static str { + fn short_label(&self) -> String { match self { - OzLaunchSlide::CloudAgents => "Cloud agents", - OzLaunchSlide::AgentAutomations => "Agent automations", - OzLaunchSlide::AgentManagement => "Agent management", - OzLaunchSlide::LaunchCredits => "Launch credits", + OzLaunchSlide::CloudAgents => i18n::t("workspace.oz_launch.slide.cloud_agents"), + OzLaunchSlide::AgentAutomations => { + i18n::t("workspace.oz_launch.slide.agent_automations") + } + OzLaunchSlide::AgentManagement => i18n::t("workspace.oz_launch.slide.agent_management"), + OzLaunchSlide::LaunchCredits => i18n::t("workspace.oz_launch.slide.launch_credits"), } } - fn title(&self) -> &'static str { + fn title(&self) -> String { match self { - OzLaunchSlide::CloudAgents => "Break out of your laptop with cloud agents", + OzLaunchSlide::CloudAgents => i18n::t("workspace.oz_launch.cloud_agents.title"), OzLaunchSlide::AgentAutomations => { - "Orchestrate agents, turning Skills into automations" - } - OzLaunchSlide::AgentManagement => "Track local and cloud agents seamlessly", - OzLaunchSlide::LaunchCredits => { - "1,000 free cloud agent credits when you upgrade to Warp Build" + i18n::t("workspace.oz_launch.agent_automations.title") } + OzLaunchSlide::AgentManagement => i18n::t("workspace.oz_launch.agent_management.title"), + OzLaunchSlide::LaunchCredits => i18n::t("workspace.oz_launch.launch_credits.title"), } } @@ -91,20 +91,16 @@ impl Slide for OzLaunchSlide { None } - fn content(&self) -> &'static str { + fn content(&self) -> String { match self { - OzLaunchSlide::CloudAgents => { - "Use cloud agents to run many agents in parallel, keep agents working when you close your laptop, or start agents programmatically. Plus, you can check on their work through the web." - } + OzLaunchSlide::CloudAgents => i18n::t("workspace.oz_launch.cloud_agents.content"), OzLaunchSlide::AgentAutomations => { - "Oz agents can be defined using the standard Skills format. You can use the built in scheduler to setup agents to run autonomously at set intervals, or use the Oz SDK or API to programmatically start and manage Oz agents." + i18n::t("workspace.oz_launch.agent_automations.content") } OzLaunchSlide::AgentManagement => { - "View all of your agents across local and cloud sessions in the Warp app or at [oz.warp.dev](https://oz.warp.dev). Join live agent sessions, continue tasks locally, and steer agents with one click." - } - OzLaunchSlide::LaunchCredits => { - "Upgrade to Build this month and receive 1,000 extra credits to try using Oz. Credits are only eligible for Oz runs in Warp-hosted cloud environments." + i18n::t("workspace.oz_launch.agent_management.content") } + OzLaunchSlide::LaunchCredits => i18n::t("workspace.oz_launch.launch_credits.content"), } } @@ -141,29 +137,37 @@ impl Slide for OzLaunchSlide { | OzLaunchSlide::AgentAutomations | OzLaunchSlide::AgentManagement => { let next = self.next().expect("Non-final slides should have a next"); - CTAButton::next_slide(next, format!("Next: {}", next.short_label())) + CTAButton::next_slide( + next, + i18n::t("workspace.oz_launch.next_button") + .replace("{label}", &next.short_label()), + ) + } + OzLaunchSlide::LaunchCredits => { + CTAButton::custom(i18n::t("workspace.oz_launch.try_it_out"), |ctx| { + send_telemetry_from_ctx!( + CloudAgentTelemetryEvent::EnteredCloudMode { + entry_point: CloudModeEntryPoint::OzLaunchModal, + }, + ctx + ); + ctx.emit(LaunchModalEvent::Close); + ctx.dispatch_typed_action(&WorkspaceAction::StartAgentOnboardingTutorial( + OnboardingTutorial::NoProject { + intention: OnboardingIntention::AgentDrivenDevelopment, + }, + )); + ctx.dispatch_typed_action(&WorkspaceAction::AddAmbientAgentTab); + }) } - OzLaunchSlide::LaunchCredits => CTAButton::custom("Try it out", |ctx| { - send_telemetry_from_ctx!( - CloudAgentTelemetryEvent::EnteredCloudMode { - entry_point: CloudModeEntryPoint::OzLaunchModal, - }, - ctx - ); - ctx.emit(LaunchModalEvent::Close); - ctx.dispatch_typed_action(&WorkspaceAction::StartAgentOnboardingTutorial( - OnboardingTutorial::NoProject { - intention: OnboardingIntention::AgentDrivenDevelopment, - }, - )); - ctx.dispatch_typed_action(&WorkspaceAction::AddAmbientAgentTab); - }), } } fn secondary_cta_button(&self) -> Option> { match self { - OzLaunchSlide::LaunchCredits => Some(CTAButton::close("Skip for now")), + OzLaunchSlide::LaunchCredits => Some(CTAButton::close(i18n::t( + "workspace.launch_modal.skip_for_now", + ))), OzLaunchSlide::CloudAgents | OzLaunchSlide::AgentAutomations | OzLaunchSlide::AgentManagement => None, @@ -172,8 +176,13 @@ impl Slide for OzLaunchSlide { fn checkbox_config(&self) -> Option { Some(CheckboxConfig { - label: "Sync conversations to cloud", - description: "Agent conversations stored in the cloud can be shared with anyone with one click, and allow conversations to be continued across devices and on logout.", + label: Box::leak( + i18n::t("workspace.launch_modal.sync_conversations_to_cloud").into_boxed_str(), + ), + description: Box::leak( + i18n::t("workspace.launch_modal.sync_conversations_to_cloud.description") + .into_boxed_str(), + ), }) } diff --git a/app/src/workspace/view/left_panel.rs b/app/src/workspace/view/left_panel.rs index 9cd9881fae..0dea4eb6a5 100644 --- a/app/src/workspace/view/left_panel.rs +++ b/app/src/workspace/view/left_panel.rs @@ -402,7 +402,7 @@ impl LeftPanelView { ToolbeltButtonConfig { icon: Icon::FileCopy, active_icon: None, - tooltip_text: "Project explorer".to_string(), + tooltip_text: i18n::t("workspace.left_panel.project_explorer"), action: LeftPanelAction::ProjectExplorer, render_with_active_state: false, tooltip_keybinding: toolbelt_tooltip_keybinding(&tooltip_keybinding_names, ctx), @@ -418,7 +418,7 @@ impl LeftPanelView { ToolbeltButtonConfig { icon: Icon::Search, active_icon: None, - tooltip_text: "Global search".to_string(), + tooltip_text: i18n::t("workspace.left_panel.global_search"), action: LeftPanelAction::GlobalSearch { entry_focus: GlobalSearchEntryFocus::QueryEditor, }, @@ -436,7 +436,7 @@ impl LeftPanelView { ToolbeltButtonConfig { icon: Icon::WarpDrive, active_icon: None, - tooltip_text: "Warp Drive".to_string(), + tooltip_text: i18n::t("workspace.left_panel.warp_drive"), action: LeftPanelAction::WarpDrive, render_with_active_state: false, tooltip_keybinding: toolbelt_tooltip_keybinding(&tooltip_keybinding_names, ctx), @@ -452,7 +452,7 @@ impl LeftPanelView { ToolbeltButtonConfig { icon: Icon::Conversation, active_icon: Some(Icon::Conversation), - tooltip_text: "Agent conversations".to_string(), + tooltip_text: i18n::t("workspace.left_panel.agent_conversations"), action: LeftPanelAction::ConversationListView, render_with_active_state: false, tooltip_keybinding: toolbelt_tooltip_keybinding(&tooltip_keybinding_names, ctx), @@ -827,12 +827,12 @@ impl LeftPanelView { let tooltip = if let Some(keybinding) = tooltip_keybinding { ui_builder - .tool_tip_with_sublabel("Close panel".to_string(), keybinding) + .tool_tip_with_sublabel(i18n::t("workspace.close_panel"), keybinding) .build() .finish() } else { ui_builder - .tool_tip("Close panel".to_string()) + .tool_tip(i18n::t("workspace.close_panel")) .build() .finish() }; diff --git a/app/src/workspace/view/openwarp_launch_modal/view.rs b/app/src/workspace/view/openwarp_launch_modal/view.rs index 798aa3f3be..ce4500accd 100644 --- a/app/src/workspace/view/openwarp_launch_modal/view.rs +++ b/app/src/workspace/view/openwarp_launch_modal/view.rs @@ -45,26 +45,26 @@ struct FeatureItem { const FEATURE_ITEMS: &[FeatureItem] = &[ FeatureItem { icon: Icon::HeartHand, - title: "Contribute", - description: "Warp's client code is now open source. Get started by using the /feedback skill to open an issue, and follow the contribution guidelines here.", + title: "workspace.openwarp_launch.feature.contribute.title", + description: "workspace.openwarp_launch.feature.contribute.description", inline_link: Some(InlineLink { - text: "here", + text: "workspace.openwarp_launch.feature.contribute.link_text", url: CONTRIBUTING_URL, }), }, FeatureItem { icon: Icon::Oz, - title: "Open Automated Development", - description: "The Warp repo is managed by an agent-first workflow powered by Oz, our cloud agent orchestration platform.", + title: "workspace.openwarp_launch.feature.open_automated_development.title", + description: "workspace.openwarp_launch.feature.open_automated_development.description", inline_link: Some(InlineLink { - text: "Oz", + text: "workspace.openwarp_launch.feature.open_automated_development.link_text", url: OZ_URL, }), }, FeatureItem { icon: Icon::MessageChatSquare, - title: "Introducing 'auto (open-weights)'", - description: "We've added a new auto model that picks the best open weight model for a task, like Kimi or MiniMax.", + title: "workspace.openwarp_launch.feature.auto_open_weights.title", + description: "workspace.openwarp_launch.feature.auto_open_weights.description", inline_link: None, }, ]; @@ -143,7 +143,7 @@ impl OpenWarpLaunchModal { }); let cta_button = ctx.add_view(|_ctx| { - ActionButton::new("Visit the repo", CtaButtonTheme) + ActionButton::new(i18n::t("workspace.openwarp.visit_repo"), CtaButtonTheme) .with_full_width(true) .on_click(|ctx| ctx.dispatch_typed_action(OpenWarpLaunchModalAction::VisitRepo)) }); @@ -191,7 +191,7 @@ impl OpenWarpLaunchModal { } fn render_badge(appearance: &Appearance) -> Box { - let text = Text::new_inline("New".to_string(), appearance.ui_font_family(), 14.) + let text = Text::new_inline(i18n::t("common.new"), appearance.ui_font_family(), 14.) .with_color(PhenomenonStyle::modal_badge_text()) .finish(); ConstrainedBox::new( @@ -212,15 +212,19 @@ impl OpenWarpLaunchModal { } fn render_title(appearance: &Appearance) -> Box { - Text::new("Warp is now open-source", appearance.ui_font_family(), 20.) - .with_color(PhenomenonStyle::modal_title_text()) - .with_style(Properties::default().weight(Weight::Semibold)) - .finish() + Text::new( + i18n::t("workspace.openwarp.title"), + appearance.ui_font_family(), + 20., + ) + .with_color(PhenomenonStyle::modal_title_text()) + .with_style(Properties::default().weight(Weight::Semibold)) + .finish() } fn render_description(appearance: &Appearance) -> Box { Text::new( - "You, our community, can participate in building Warp using an agent-first workflow.", + i18n::t("workspace.openwarp_launch.description"), appearance.ui_font_family(), 14., ) @@ -254,20 +258,21 @@ impl OpenWarpLaunchModal { } fn render_feature_description(item: &FeatureItem, appearance: &Appearance) -> Box { + let description = i18n::t(item.description); let Some(link) = &item.inline_link else { - return Text::new(item.description, appearance.ui_font_family(), 14.) + return Text::new(description, appearance.ui_font_family(), 14.) .with_color(PhenomenonStyle::modal_feature_description_text()) .finish(); }; + let link_text = i18n::t(link.text); // Build a formatted description with an inline hyperlink and inline code. - let (before, after) = item - .description - .split_once(link.text) - .unwrap_or((item.description, "")); + let (before, after) = description + .split_once(&link_text) + .unwrap_or((description.as_str(), "")); let link_fragment = FormattedTextFragment { - text: link.text.into(), + text: link_text.into(), styles: FormattedTextStyles { underline: true, hyperlink: Some(Hyperlink::Url(link.url.into())), @@ -317,7 +322,7 @@ impl OpenWarpLaunchModal { .with_cross_axis_alignment(CrossAxisAlignment::Start) .with_spacing(2.) .with_child( - Text::new_inline(item.title.to_string(), appearance.ui_font_family(), 14.) + Text::new_inline(i18n::t(item.title), appearance.ui_font_family(), 14.) .with_color(PhenomenonStyle::modal_feature_title_text()) .finish(), ) diff --git a/app/src/workspace/view/orchestration_launch_modal/view.rs b/app/src/workspace/view/orchestration_launch_modal/view.rs index ab17358791..b9de59048f 100644 --- a/app/src/workspace/view/orchestration_launch_modal/view.rs +++ b/app/src/workspace/view/orchestration_launch_modal/view.rs @@ -66,21 +66,21 @@ struct FeatureItem { const FEATURE_ITEMS: &[FeatureItem] = &[ FeatureItem { icon: Icon::Cloud, - title: "Run any agent harness in the cloud", - description: "Use Oz to spin up Claude Code or Codex agents in the cloud; Oz will help you track and steer the agents.", + title: "workspace.orchestration_launch.feature.cloud_harness.title", + description: "workspace.orchestration_launch.feature.cloud_harness.description", badge: None, }, FeatureItem { icon: Icon::Atom, - title: "Multi-agent orchestration", - description: "Warp Agents will now orchestrate swarms of subagents, allowing you to parallelize tasks.", + title: "workspace.orchestration_launch.feature.multi_agent.title", + description: "workspace.orchestration_launch.feature.multi_agent.description", badge: None, }, FeatureItem { icon: Icon::Cognition, - title: "Agent Memory", - description: "Agents will now store and access long-term memories, enabling self-improvement over time.", - badge: Some("Research preview"), + title: "workspace.orchestration_launch.feature.agent_memory.title", + description: "workspace.orchestration_launch.feature.agent_memory.description", + badge: Some("workspace.orchestration_launch.feature.agent_memory.badge"), }, ]; @@ -184,7 +184,7 @@ impl OrchestrationLaunchModal { }); let learn_more_button = ctx.add_view(|_ctx| { - ActionButton::new("Learn more", LearnMoreButtonTheme) + ActionButton::new(i18n::t("common.learn_more"), LearnMoreButtonTheme) .with_icon(Icon::LinkExternal) .with_full_width(true) .on_click(|ctx| { @@ -193,7 +193,7 @@ impl OrchestrationLaunchModal { }); let go_to_warp_button = ctx.add_view(|_ctx| { - ActionButton::new("Close", CtaButtonTheme) + ActionButton::new(i18n::t("common.close"), CtaButtonTheme) .with_full_width(true) .on_click(|ctx| ctx.dispatch_typed_action(OrchestrationLaunchModalAction::Close)) }); @@ -247,7 +247,7 @@ impl OrchestrationLaunchModal { fn render_badge(appearance: &Appearance) -> Box { let text_color = modal_terminal_magenta(appearance); let background_color = modal_terminal_magenta_overlay_1(appearance); - let text = Text::new_inline("New".to_string(), appearance.ui_font_family(), 14.) + let text = Text::new_inline(i18n::t("common.new"), appearance.ui_font_family(), 14.) .with_color(text_color) .finish(); ConstrainedBox::new( @@ -269,7 +269,7 @@ impl OrchestrationLaunchModal { fn render_title(appearance: &Appearance) -> Box { Text::new( - "Orchestrate any agent, anywhere", + i18n::t("workspace.orchestration_launch.title"), appearance.ui_font_family(), 20., ) @@ -280,7 +280,7 @@ impl OrchestrationLaunchModal { fn render_description(appearance: &Appearance) -> Box { Text::new( - "We've made major improvements to Warp's cloud agent orchestration platform, Oz.", + i18n::t("workspace.orchestration_launch.description"), appearance.ui_font_family(), 14., ) @@ -292,7 +292,7 @@ impl OrchestrationLaunchModal { let font_family = appearance.ui_font_family(); let color = modal_text_sub(appearance); Container::new( - Text::new_inline(label.to_string(), font_family, 11.) + Text::new_inline(i18n::t(label), font_family, 11.) .with_color(color) .finish(), ) @@ -317,7 +317,7 @@ impl OrchestrationLaunchModal { .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_spacing(6.); title_row.add_child( - Text::new_inline(item.title.to_string(), appearance.ui_font_family(), 14.) + Text::new_inline(i18n::t(item.title), appearance.ui_font_family(), 14.) .with_color(modal_text_main(appearance)) .finish(), ); @@ -330,7 +330,7 @@ impl OrchestrationLaunchModal { .with_spacing(2.) .with_child(title_row.finish()) .with_child( - Text::new(item.description, appearance.ui_font_family(), 14.) + Text::new(i18n::t(item.description), appearance.ui_font_family(), 14.) .with_color(modal_text_sub(appearance)) .finish(), ) diff --git a/app/src/workspace/view/right_panel.rs b/app/src/workspace/view/right_panel.rs index 945ebdef25..4fe6bbfdf8 100644 --- a/app/src/workspace/view/right_panel.rs +++ b/app/src/workspace/view/right_panel.rs @@ -299,7 +299,7 @@ impl CodeReviewState { .map(|repo_path| { let display_name = self .get_repo_display_name(repo_path, ctx) - .unwrap_or_else(|| "Unknown".to_string()); + .unwrap_or_else(|| i18n::t("workspace.right_panel.repo.unknown")); DropdownItem::new( display_name, RightPanelAction::SelectRepo { @@ -435,7 +435,7 @@ impl RightPanelView { let maximize_button = ctx.add_typed_action_view(|ctx| { let mut button = ActionButton::new("", PaneHeaderTheme) .with_icon(Icon::Maximize) - .with_tooltip("Maximize") + .with_tooltip(i18n::t("workspace.right_panel.maximize.tooltip")) .with_tooltip_positioning_provider(Arc::new(MenuPositioning::BelowInputBox)) .on_click(|ctx| ctx.dispatch_typed_action(RightPanelAction::ToggleMaximize)); @@ -451,11 +451,14 @@ impl RightPanelView { #[cfg(feature = "local_fs")] let open_repository_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Open repository", NakedTheme) - .with_size(crate::view_components::action_button::ButtonSize::Small) - .with_tooltip("Navigate to a repo and initialize it for coding") - .with_tooltip_alignment(TooltipAlignment::Center) - .on_click(|ctx| ctx.dispatch_typed_action(RightPanelAction::OpenRepository)) + ActionButton::new( + i18n::t("workspace.right_panel.open_repository.label"), + NakedTheme, + ) + .with_size(crate::view_components::action_button::ButtonSize::Small) + .with_tooltip(i18n::t("workspace.right_panel.open_repository.tooltip")) + .with_tooltip_alignment(TooltipAlignment::Center) + .on_click(|ctx| ctx.dispatch_typed_action(RightPanelAction::OpenRepository)) }); Self { @@ -762,12 +765,15 @@ impl RightPanelView { let tooltip = if let Some(keybinding) = tooltip_keybinding { ui_builder - .tool_tip_with_sublabel("Close panel".to_string(), keybinding) + .tool_tip_with_sublabel( + i18n::t("workspace.right_panel.close_panel.tooltip"), + keybinding, + ) .build() .finish() } else { ui_builder - .tool_tip("Close panel".to_string()) + .tool_tip(i18n::t("workspace.right_panel.close_panel.tooltip")) .build() .finish() }; @@ -1035,10 +1041,14 @@ impl RightPanelView { let title = Shrinkable::new( 1.0, - Text::new_inline("Code review".to_string(), appearance.ui_font_family(), 12.) - .with_style(Properties::default().weight(Weight::Bold)) - .with_color(sub_text_color.into()) - .finish(), + Text::new_inline( + i18n::t("workspace.right_panel.code_review.title"), + appearance.ui_font_family(), + 12., + ) + .with_style(Properties::default().weight(Weight::Bold)) + .with_color(sub_text_color.into()) + .finish(), ) .finish(); @@ -1081,9 +1091,15 @@ impl RightPanelView { pub fn set_maximized(&mut self, is_maximized: bool, ctx: &mut ViewContext) { let (icon, tooltip) = if is_maximized { - (Icon::Minimize, "Minimize") + ( + Icon::Minimize, + i18n::t("workspace.right_panel.minimize.tooltip"), + ) } else { - (Icon::Maximize, "Maximize") + ( + Icon::Maximize, + i18n::t("workspace.right_panel.maximize.tooltip"), + ) }; self.maximize_button.update(ctx, |button, ctx| { diff --git a/app/src/workspace/view/vertical_tabs.rs b/app/src/workspace/view/vertical_tabs.rs index e31cece910..a5a20ccd43 100644 --- a/app/src/workspace/view/vertical_tabs.rs +++ b/app/src/workspace/view/vertical_tabs.rs @@ -1389,7 +1389,7 @@ fn render_settings_button( if hover_state.is_hovered() && !is_popup_open { let tooltip = ui_builder - .tool_tip("View options".to_string()) + .tool_tip(i18n::t("workspace.tabs.tooltip.view_options")) .build() .finish(); let mut stack = Stack::new().with_child(button_container); @@ -1469,12 +1469,12 @@ fn render_new_tab_button( let contents = if hover_state.is_hovered() { let tooltip = if let Some(sublabel) = tab_configs_keybinding.clone() { ui_builder - .tool_tip_with_sublabel("Tab configs".to_string(), sublabel) + .tool_tip_with_sublabel(i18n::t("workspace.tabs.tooltip.tab_configs"), sublabel) .build() .finish() } else { ui_builder - .tool_tip("Tab configs".to_string()) + .tool_tip(i18n::t("workspace.tabs.tooltip.tab_configs")) .build() .finish() }; @@ -1591,9 +1591,13 @@ fn render_groups( if workspace.tabs.is_empty() { return Container::new( - Text::new_inline("No tabs open", appearance.ui_font_family(), 12.) - .with_color(theme.sub_text_color(theme.background()).into()) - .finish(), + Text::new_inline( + i18n::t("workspace.tabs.empty.no_tabs_open"), + appearance.ui_font_family(), + 12., + ) + .with_color(theme.sub_text_color(theme.background()).into()) + .finish(), ) .with_padding(Padding::uniform(12.)) .finish(); @@ -1719,7 +1723,7 @@ fn render_groups( } else { return Container::new( Text::new_inline( - "No tabs match your search.", + i18n::t("workspace.tabs.empty.no_match"), appearance.ui_font_family(), 12., ) @@ -2476,15 +2480,18 @@ fn render_grouped_tabs_header( let title_text = group .name .clone() - .unwrap_or_else(|| "New Group".to_string()); + .unwrap_or_else(|| i18n::t("workspace.tabs.group.untitled")); let title_element: Box = Text::new_inline(title_text, font_family, 12.) .with_clip(ClipConfig::ellipsis()) .with_color(main_text_color.into()) .finish(); let subtitle_text = if member_count == 1 { - "1 tab".to_string() + i18n::t("workspace.tabs.group.tab_count_one") } else { - format!("{member_count} tabs") + format!( + "{member_count} {}", + i18n::t("workspace.tabs.group.tab_count_other") + ) }; let subtitle = Text::new_inline(subtitle_text, font_family, 10.) .with_clip(ClipConfig::ellipsis()) @@ -2702,7 +2709,7 @@ fn render_group_header(props: GroupHeaderProps<'_>, app: &AppContext) -> Box { matches!(self, TypedPane::Terminal(_) | TypedPane::Code(_)) || self.warp_drive_object_type().is_some() } - fn kind_label(&self) -> &'static str { + fn kind_label(&self) -> String { match self { - TypedPane::Terminal(_) => "Terminal", - TypedPane::Code(_) => "Code", - TypedPane::CodeDiff => "Code Diff", - TypedPane::File => "File", - TypedPane::Notebook { .. } => "Notebook", - TypedPane::Workflow { .. } => "Workflow", - TypedPane::Settings => "Settings", - TypedPane::EnvVarCollection => "Environment Variables", - TypedPane::EnvironmentManagement => "Environments", - TypedPane::AIFact => "Rules", - TypedPane::AIDocument => "Plan", - TypedPane::ExecutionProfileEditor => "Execution Profile", - TypedPane::Other => "Other", + TypedPane::Terminal(_) => i18n::t("workspace.tabs.kind.terminal"), + TypedPane::Code(_) => i18n::t("workspace.tabs.kind.code"), + TypedPane::CodeDiff => i18n::t("workspace.tabs.kind.code_diff"), + TypedPane::File => i18n::t("workspace.tabs.kind.file"), + TypedPane::Notebook { .. } => i18n::t("workspace.tabs.kind.notebook"), + TypedPane::Workflow { .. } => i18n::t("workspace.tabs.kind.workflow"), + TypedPane::Settings => i18n::t("workspace.tabs.kind.settings"), + TypedPane::EnvVarCollection => i18n::t("workspace.tabs.kind.env_var_collection"), + TypedPane::EnvironmentManagement => i18n::t("workspace.tabs.kind.environments"), + TypedPane::AIFact => i18n::t("workspace.tabs.kind.rules"), + TypedPane::AIDocument => i18n::t("workspace.tabs.kind.plan"), + TypedPane::ExecutionProfileEditor => i18n::t("workspace.tabs.kind.execution_profile"), + TypedPane::Other => i18n::t("workspace.tabs.kind.other"), } } @@ -3041,7 +3048,7 @@ impl TypedPane<'_> { .file_view(app) .as_ref(app) .contains_unsaved_changes(app) - .then(|| "Unsaved".to_string()), + .then(|| i18n::t("workspace.tabs.badge.unsaved")), TypedPane::Terminal(_) | TypedPane::CodeDiff | TypedPane::File @@ -3101,7 +3108,7 @@ fn pane_display_title_and_subtitle( } else { ( if title.is_empty() { - typed.kind_label().to_string() + typed.kind_label() } else { title.to_string() }, @@ -3504,7 +3511,7 @@ fn terminal_primary_line_data( } TerminalPrimaryLineData::Text { - text: "New session".to_string(), + text: i18n::t("workspace.tabs.terminal.new_session"), font: TerminalPrimaryLineFont::Ui, } } @@ -3515,7 +3522,7 @@ fn terminal_kind_badge_label(is_oz_agent: bool, cli_agent: Option) -> } else if is_oz_agent { "Oz".to_string() } else { - "Terminal".to_string() + i18n::t("workspace.tabs.kind.terminal") } } @@ -3697,9 +3704,9 @@ fn cloud_agent_working_directory_and_env( .and_then(|id| CloudAmbientAgentEnvironment::get_by_id(id, app)) .map(|env| env.model().string_model.display_name()); - let setup_status: Option<&str> = model_ref.agent_progress().map(|p| p.setup_status_text()); + let setup_status = model_ref.agent_progress().map(|p| p.setup_status_text()); - match (env_name, setup_status, working_directory) { + match (env_name, setup_status.as_deref(), working_directory) { (Some(env), Some(status), _) => Some(format!("{env} · {status}")), (Some(env), None, Some(wd)) => Some(format!("{env} · {wd}")), (Some(env), None, None) => Some(env), @@ -4252,7 +4259,10 @@ fn render_summary_overflow_line( appearance: &Appearance, ) -> Box { Text::new_inline( - format!("+ {hidden_count} more"), + format!( + "+ {hidden_count} {}", + i18n::t("workspace.tabs.summary.overflow_more_suffix") + ), appearance.ui_font_family(), 10., ) @@ -4999,30 +5009,36 @@ fn default_compact_subtitle(primary: VerticalTabsPrimaryInfo) -> VerticalTabsCom fn subtitle_options_for_primary( primary: VerticalTabsPrimaryInfo, -) -> [(VerticalTabsCompactSubtitle, &'static str); 2] { +) -> [(VerticalTabsCompactSubtitle, String); 2] { match primary { VerticalTabsPrimaryInfo::Command => [ - (VerticalTabsCompactSubtitle::Branch, "Branch"), + ( + VerticalTabsCompactSubtitle::Branch, + i18n::t("workspace.tabs.settings.info.branch"), + ), ( VerticalTabsCompactSubtitle::WorkingDirectory, - "Working Directory", + i18n::t("workspace.tabs.settings.info.working_directory"), ), ], VerticalTabsPrimaryInfo::WorkingDirectory => [ - (VerticalTabsCompactSubtitle::Branch, "Branch"), + ( + VerticalTabsCompactSubtitle::Branch, + i18n::t("workspace.tabs.settings.info.branch"), + ), ( VerticalTabsCompactSubtitle::Command, - "Command / Conversation", + i18n::t("workspace.tabs.settings.info.command"), ), ], VerticalTabsPrimaryInfo::Branch => [ ( VerticalTabsCompactSubtitle::Command, - "Command / Conversation", + i18n::t("workspace.tabs.settings.info.command"), ), ( VerticalTabsCompactSubtitle::WorkingDirectory, - "Working Directory", + i18n::t("workspace.tabs.settings.info.working_directory"), ), ], } @@ -5065,7 +5081,7 @@ pub(super) fn render_settings_popup( let sub_text = theme.sub_text_color(theme.background()); let view_as_header = Container::new( Text::new_inline( - "View as".to_string(), + i18n::t("workspace.tabs.settings.view_as"), appearance.ui_font_family(), SETTINGS_POPUP_MENU_ITEM_FONT_SIZE, ) @@ -5076,6 +5092,8 @@ pub(super) fn render_settings_popup( .with_margin_bottom(4.) .finish(); + let panes_segment_label = i18n::t("workspace.tabs.settings.granularity.panes"); + let tabs_segment_label = i18n::t("workspace.tabs.settings.granularity.tabs"); let view_as_segmented_control = Container::new( Flex::row() .with_main_axis_size(MainAxisSize::Max) @@ -5084,7 +5102,7 @@ pub(super) fn render_settings_popup( Expanded::new( 1., render_popup_text_segment( - "Panes", + &panes_segment_label, matches!(current_granularity, VerticalTabsDisplayGranularity::Panes), state.panes_segment_mouse_state.clone(), VerticalTabsDisplayGranularity::Panes, @@ -5098,7 +5116,7 @@ pub(super) fn render_settings_popup( Expanded::new( 1., render_popup_text_segment( - "Tabs", + &tabs_segment_label, matches!(current_granularity, VerticalTabsDisplayGranularity::Tabs), state.tabs_segment_mouse_state.clone(), VerticalTabsDisplayGranularity::Tabs, @@ -5124,7 +5142,7 @@ pub(super) fn render_settings_popup( let tab_item_header = Container::new( Text::new_inline( - "Tab item".to_string(), + i18n::t("workspace.tabs.settings.tab_item"), appearance.ui_font_family(), SETTINGS_POPUP_MENU_ITEM_FONT_SIZE, ) @@ -5135,8 +5153,9 @@ pub(super) fn render_settings_popup( .with_margin_bottom(4.) .finish(); + let focused_session_label = i18n::t("workspace.tabs.settings.tab_item.focused_session"); let focused_session_option = render_tab_item_mode_option( - "Focused session", + &focused_session_label, matches!( current_tab_item_mode, VerticalTabsTabItemMode::FocusedSession @@ -5147,9 +5166,10 @@ pub(super) fn render_settings_popup( theme, ); + let summary_label = i18n::t("workspace.tabs.settings.tab_item.summary"); let summary_option = if FeatureFlag::VerticalTabsSummaryMode.is_enabled() { Some(render_tab_item_mode_option( - "Summary", + &summary_label, matches!(current_tab_item_mode, VerticalTabsTabItemMode::Summary), state.summary_option_mouse_state.clone(), VerticalTabsTabItemMode::Summary, @@ -5162,7 +5182,7 @@ pub(super) fn render_settings_popup( let density_header = Container::new( Text::new_inline( - "Density".to_string(), + i18n::t("workspace.tabs.settings.density"), appearance.ui_font_family(), SETTINGS_POPUP_MENU_ITEM_FONT_SIZE, ) @@ -5239,7 +5259,7 @@ pub(super) fn render_settings_popup( let pane_title_header = Container::new( Text::new_inline( - "Pane title as".to_string(), + i18n::t("workspace.tabs.settings.pane_title_as"), appearance.ui_font_family(), SETTINGS_POPUP_MENU_ITEM_FONT_SIZE, ) @@ -5250,8 +5270,9 @@ pub(super) fn render_settings_popup( .with_margin_bottom(4.) .finish(); + let command_option_label = i18n::t("workspace.tabs.settings.info.command"); let command_option = render_primary_info_option( - "Command / Conversation", + &command_option_label, matches!(current_primary_info, VerticalTabsPrimaryInfo::Command), state.command_option_mouse_state.clone(), VerticalTabsPrimaryInfo::Command, @@ -5259,8 +5280,9 @@ pub(super) fn render_settings_popup( theme, ); + let directory_option_label = i18n::t("workspace.tabs.settings.info.working_directory"); let directory_option = render_primary_info_option( - "Working Directory", + &directory_option_label, matches!( current_primary_info, VerticalTabsPrimaryInfo::WorkingDirectory @@ -5271,8 +5293,9 @@ pub(super) fn render_settings_popup( theme, ); + let branch_option_label = i18n::t("workspace.tabs.settings.info.branch"); let branch_option = render_primary_info_option( - "Branch", + &branch_option_label, matches!(current_primary_info, VerticalTabsPrimaryInfo::Branch), state.branch_option_mouse_state.clone(), VerticalTabsPrimaryInfo::Branch, @@ -5310,7 +5333,7 @@ pub(super) fn render_settings_popup( let subtitle_header = Container::new( Text::new_inline( - "Additional metadata".to_string(), + i18n::t("workspace.tabs.settings.additional_metadata"), appearance.ui_font_family(), SETTINGS_POPUP_MENU_ITEM_FONT_SIZE, ) @@ -5344,7 +5367,7 @@ pub(super) fn render_settings_popup( let show_header = Container::new( Text::new_inline( - "Show".to_string(), + i18n::t("workspace.tabs.settings.show"), appearance.ui_font_family(), SETTINGS_POPUP_MENU_ITEM_FONT_SIZE, ) @@ -5361,14 +5384,17 @@ pub(super) fn render_settings_popup( let pr_link_info_tooltip = if show_pr_link && pr_validation_suppressed { Some(ShowToggleInfoTooltip { mouse_state: state.show_pr_link_info_tooltip_mouse_state.clone(), - tooltip_text: "Requires the GitHub CLI to be installed and authenticated", + tooltip_text: Box::leak( + i18n::t("workspace.tabs.settings.pr_link.requires_gh_cli").into_boxed_str(), + ), }) } else { None }; + let pr_link_label = i18n::t("workspace.tabs.settings.show.pr_link"); popup_col.add_child(render_show_toggle_option( - "PR link", + &pr_link_label, show_pr_link, state.show_pr_link_mouse_state.clone(), WorkspaceAction::ToggleVerticalTabsShowPrLink, @@ -5376,8 +5402,9 @@ pub(super) fn render_settings_popup( appearance, theme, )); + let diff_stats_label = i18n::t("workspace.tabs.settings.show.diff_stats"); popup_col.add_child(render_show_toggle_option( - "Diff stats", + &diff_stats_label, show_diff_stats, state.show_diff_stats_mouse_state.clone(), WorkspaceAction::ToggleVerticalTabsShowDiffStats, @@ -5389,8 +5416,9 @@ pub(super) fn render_settings_popup( } popup_col.add_child(make_divider(theme)); + let show_details_on_hover_label = i18n::t("workspace.tabs.settings.show.details_on_hover"); popup_col.add_child(render_show_toggle_option( - "Show details on hover", + &show_details_on_hover_label, show_details_on_hover, state.show_details_on_hover_mouse_state.clone(), WorkspaceAction::ToggleVerticalTabsShowDetailsOnHover, @@ -5967,7 +5995,7 @@ fn render_detail_status_pill( .finish(), ) .with_child( - Text::new_inline(status.to_string(), appearance.ui_font_family(), 10.) + Text::new_inline(status.localized_label(), appearance.ui_font_family(), 10.) .with_color(WarpThemeFill::Solid(color).into()) .finish(), ) @@ -6205,7 +6233,11 @@ fn render_code_detail_section( if extra_open_tabs > 0 { section.add_child(render_detail_wrapping_text( - format!("and {extra_open_tabs} more"), + format!( + "{} {extra_open_tabs} {}", + i18n::t("workspace.tabs.detail.more_open_tabs_prefix"), + i18n::t("workspace.tabs.detail.more_open_tabs_suffix") + ), 12., text_colors.sub, None, diff --git a/app/src/workspace/view/wasm_view.rs b/app/src/workspace/view/wasm_view.rs index 9d19e7d858..04de4f6e31 100644 --- a/app/src/workspace/view/wasm_view.rs +++ b/app/src/workspace/view/wasm_view.rs @@ -43,7 +43,11 @@ impl Workspace { ctx: &mut ViewContext, ) -> ViewHandle { ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Open in Warp", PrimaryTheme).on_click(move |ctx| { + ActionButton::new( + i18n::t("terminal.shared_session.open_in_warp"), + PrimaryTheme, + ) + .on_click(move |ctx| { // Get the current URL and dispatch action to open it on desktop if let Some(url) = parse_current_url() { ctx.dispatch_typed_action(WorkspaceAction::OpenLinkOnDesktop(url)); @@ -59,7 +63,11 @@ impl Workspace { ) -> ViewHandle { let url = build_oz_runs_url(); ctx.add_typed_action_view(|_ctx| { - ActionButton::new("View all cloud runs", SecondaryTheme).on_click(move |ctx| { + ActionButton::new( + i18n::t("workspace.wasm.view_all_cloud_runs"), + SecondaryTheme, + ) + .on_click(move |ctx| { ctx.dispatch_typed_action(WorkspaceAction::OpenLink(url.clone())); }) }) diff --git a/crates/i18n/Cargo.toml b/crates/i18n/Cargo.toml new file mode 100644 index 0000000000..bfa71c7435 --- /dev/null +++ b/crates/i18n/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "i18n" +edition = "2021" +authors = ["Warp Team "] +publish.workspace = true +license.workspace = true + +[dependencies] +serde_json.workspace = true +rust-embed.workspace = true +once_cell.workspace = true +log.workspace = true diff --git a/crates/i18n/README.md b/crates/i18n/README.md new file mode 100644 index 0000000000..7c5d698854 --- /dev/null +++ b/crates/i18n/README.md @@ -0,0 +1,72 @@ +# i18n — Warp UI internationalization + +Lightweight, dependency-free (within the workspace) internationalization for the +Warp client. Provides a runtime-switchable locale and a `t("key")` lookup that +drops into every Warp text constructor. Ships with **English** and **Simplified +Chinese**, defaulting to Chinese. + +## How it works + +- Translation catalogs live in [`locales/`](./locales) — one flat JSON + `key -> string` file per locale (`en.json`, `zh-CN.json`). They are embedded + into the binary at build time with `rust-embed` (same approach as the + `languages` crate). +- A single active locale is held in a global `RwLock`. `i18n::set_locale(tag)` + rebuilds the active catalog; `i18n::t(key)` reads it. +- Lookups degrade gracefully: `zh-CN` resolves through `en → zh → zh-CN` + (more specific overrides less specific, English is the base). A key missing + from every catalog returns the key itself, so the UI never blanks or panics. + +## API + +```rust +i18n::set_locale("zh-CN"); // set active locale (call at startup + on change) +let s: String = i18n::t("settings.x"); // translate; returns owned String +let s: String = i18n::t!("settings.x"); // macro form (identical) +let tag: String = i18n::current_locale(); +``` + +`t()` returns `String`, which satisfies `impl Into>` — the +type accepted by `Text::new(..)`, `Span::new`, `Paragraph::new`, dialog/tooltip +params, etc. For button labels (which take `String`) it drops in directly. + +## Wiring in the app (already done) + +- **Setting**: `app/src/settings/language.rs` defines `Language { ZhCn (default), + En }`, persisted to `settings.toml` as `appearance.language = "zh-CN"`. +- **Startup**: `app/src/settings/init.rs` reads the saved language and calls + `i18n::set_locale(..)` before the first frame. +- **Live switching**: `init.rs` subscribes to the language setting; on change it + swaps the catalog and calls `ctx.invalidate_all_views()` to repaint every view + — no restart. +- **Switcher UI**: a dropdown in the **Appearance** settings page + (`app/src/settings_view/appearance_page.rs`, the `LanguageWidget`). + +## Adding translations + +1. Add the English string to `locales/en.json` under a namespaced key, e.g. + `"settings.account.log_out": "Log out"`. +2. Add the Chinese string to `locales/zh-CN.json` under the same key. (Omit it to + fall back to English.) +3. At the **call site**, replace the literal with `i18n::t("settings.account.log_out")`. + +### Which call sites are safe to translate + +Prefer sites evaluated **on every render** so they update live when the language +changes: + +| Pattern | Safe? | Notes | +|---|---|---| +| `Text::new("X", ..)` inside a `render`/`view` method | ✅ | `Text::new(i18n::t("key"), ..)` — type-safe, updates live | +| `.with_text_label("X".into())` / button labels | ✅ | `.with_text_label(i18n::t("key"))` | +| dropdown item / inline labels built per render | ✅ | | +| `const FOO: &str = "X";` | ⚠️ | `t()` can't run in `const` context — translate at the **use site** instead, or convert the const to a `fn() -> String` | +| `Category::new("X", ..)`, `SettingsUmbrella::new("X", ..)`, struct fields typed `&'static str` | ⚠️ | These take `&'static str` and are set once at construction, so they neither accept a `String` nor update live. Add a dedicated render-time accessor (e.g. a `nav_label(&self) -> String`) and translate where it's rendered, **not** where it's stored | +| `impl Display for SettingsSection` | ⛔ | Do **not** localize `Display` — it feeds crash-reporting tags and `FromStr`. Add a separate `nav_label()` method for the UI | +| Native app menu bar (`app/src/app_menus.rs`) | ⚠️ | Menus are built once and registered with the OS; they render in the startup language correctly but won't switch live without a menu rebuild/restart | + +## Tests + +```sh +cargo test -p i18n +``` diff --git a/crates/i18n/locales/en.json b/crates/i18n/locales/en.json new file mode 100644 index 0000000000..35cbbc87cc --- /dev/null +++ b/crates/i18n/locales/en.json @@ -0,0 +1,6707 @@ +{ + "agent_input_footer.attach_file": "Attach file", + "agent_input_footer.auto_approve_locked": "Fast forward is always enabled for cloud agent conversations", + "agent_input_footer.auto_approve_off": "Auto-approve all agent actions for this task", + "agent_input_footer.auto_approve_on": "Turn off auto-approve all agent actions", + "agent_input_footer.choose_environment": "Choose an environment", + "agent_input_footer.context_remaining": "{percent}% context remaining", + "agent_input_footer.context_window_usage": "Context window usage", + "agent_input_footer.disable_command_autodetection": "Disable terminal command autodetection", + "agent_input_footer.editor.available_chips": "Available chips", + "agent_input_footer.editor.edit_agent_toolbelt": "Edit agent toolbelt", + "agent_input_footer.editor.edit_cli_agent_toolbelt": "Edit CLI agent toolbelt", + "agent_input_footer.enable_command_autodetection": "Enable terminal command autodetection", + "agent_input_footer.file_explorer": "File explorer", + "agent_input_footer.full_terminal_agent_default_model": "Now using Full Terminal Agent's default model.", + "agent_input_footer.hand_off_to_cloud": "Hand off to cloud (or type &)", + "agent_input_footer.hide_rich_input": "Hide Rich Input", + "agent_input_footer.new_environment": "New environment", + "agent_input_footer.open_coding_agent_settings": "Open coding agent settings", + "agent_input_footer.open_file_explorer": "Open file explorer", + "agent_input_footer.open_rich_input": "Open Rich Input", + "agent_input_footer.plugin_auto_install_failed": "Could not automatically install plugin. Please click the chip again for manual installation steps.", + "agent_input_footer.remote_control_login_required": "Log in to use /remote-control", + "agent_input_footer.rich_input": "Rich Input", + "agent_input_footer.start_remote_control": "Start remote control", + "agent_input_footer.stop_sharing": "Stop sharing", + "agent_input_footer.voice_first_time_enabled": "Voice input is enabled. You can also press and hold the `{key}` key to activate voice input (configure in Settings > AI > Voice)", + "agent_input_footer.voice_input": "Voice input", + "agent_input_footer.voice_limit_reached": "Voice input limit reached", + "agent_input_footer.voice_microphone_access_failed": "Failed to start voice input (you may need to enable Microphone access)", + "agent_input_footer.voice_transcribe_failed": "Failed to transcribe voice input", + "agent_management.agent_type.choose_agent": "Choose your agent", + "agent_management.agent_type.cloud_agent": "Cloud agent", + "agent_management.agent_type.cloud_agent.desc": "Runs autonomously in a cloud environment you choose. Best for parallel or long-running work.", + "agent_management.agent_type.local_agent": "Local agent", + "agent_management.agent_type.local_agent.desc": "Runs on your machine and requires supervision. Best for quick, interactive tasks.", + "agent_management.agent_type.suggested": "Suggested", + "agent_management.artifact.file": "File", + "agent_management.artifact.plan": "Plan", + "agent_management.artifact.pull_request": "Pull Request", + "agent_management.artifact.screenshot": "Screenshot", + "agent_management.clear_all": "Clear all", + "agent_management.clear_filters": "Clear filters", + "agent_management.cloud_setup.docs_link": "Oz documentation", + "agent_management.cloud_setup.docs_prefix": "Check out the ", + "agent_management.cloud_setup.docs_suffix": " to learn more.", + "agent_management.cloud_setup.manual_setup": "Manual setup: Create a Slack or Linear integration with the Oz CLI", + "agent_management.cloud_setup.quick_start": "Quick start: Visit oz.warp.dev for a UI-based setup experience.", + "agent_management.cloud_setup.step.create_environment": "Create an environment", + "agent_management.cloud_setup.step.create_environment.desc": "First, set up an environment to create an integration.", + "agent_management.cloud_setup.step.create_environment.docs_prefix": "Use Warp's environment setup command to have an agent help you through it. ", + "agent_management.cloud_setup.step.create_environment.or_text": "Or, supply your own existing docker image.", + "agent_management.cloud_setup.step.create_integration": "Create an integration", + "agent_management.cloud_setup.step.create_integration.docs_prefix": "Integrate Slack or Linear to assign Warp's Agent tasks with @Warp. ", + "agent_management.cloud_setup.subtitle": "Start Oz cloud agents directly in Warp from an integration (Linear, Slack), with an event (GitHub, built-in schedule), or programmatically with the Oz SDK or CLI.", + "agent_management.cloud_setup.title": "Getting started with Oz cloud agents", + "agent_management.cloud_setup.visit_docs": "Visit docs", + "agent_management.cloud_setup.visit_oz": "Visit Oz", + "agent_management.cloud_setup.workflow.arg.docker_image": "Docker image to use for the environment", + "agent_management.cloud_setup.workflow.arg.environment_id": "ID of the environment to integrate with", + "agent_management.cloud_setup.workflow.arg.environment_name": "Name for the environment", + "agent_management.cloud_setup.workflow.arg.github_link_or_path": "GitHub link or local filepath to the repository", + "agent_management.cloud_setup.workflow.create_environment": "Create Environment", + "agent_management.cloud_setup.workflow.create_environment_cli": "Create Environment (CLI)", + "agent_management.cloud_setup.workflow.create_linear_integration": "Create Linear Integration", + "agent_management.cloud_setup.workflow.create_slack_integration": "Create Slack Integration", + "agent_management.created_on.last_24_hours": "Last 24 hours", + "agent_management.created_on.last_week": "Last week", + "agent_management.created_on.past_3_days": "Past 3 days", + "agent_management.details.cancel_task": "Cancel task", + "agent_management.details.copy_link_to_run": "Copy link to run", + "agent_management.details.fork_conversation": "Fork conversation", + "agent_management.details.open_conversation": "Open conversation", + "agent_management.details.view_details": "View details", + "agent_management.filter.all": "All", + "agent_management.filter.created_by": "Created by", + "agent_management.filter.created_on": "Created on", + "agent_management.filter.environment": "Environment", + "agent_management.filter.harness": "Harness", + "agent_management.filter.has_artifact": "Has artifact", + "agent_management.filter.none": "None", + "agent_management.filter.source": "Source", + "agent_management.filter.status": "Status", + "agent_management.get_started": "Get started", + "agent_management.loading_agents": "Loading agents...", + "agent_management.loading_cloud_runs": "Loading cloud agent runs", + "agent_management.metadata.agent": "Agent", + "agent_management.metadata.credits_used": "Credits used", + "agent_management.metadata.executor": "Executor", + "agent_management.metadata.harness": "Harness", + "agent_management.metadata.run_time": "Run time", + "agent_management.metadata.source": "Source", + "agent_management.new_agent": "New agent", + "agent_management.no_filter_results": "No results matched your filters", + "agent_management.notifications.agent_completed_suffix": " completed", + "agent_management.notifications.agent_task": "Agent task", + "agent_management.notifications.close": "Close", + "agent_management.notifications.empty": "No notifications", + "agent_management.notifications.error": "Something went wrong.", + "agent_management.notifications.filter_with_count": "{label} ({count})", + "agent_management.notifications.filter.all_tabs": "All tabs", + "agent_management.notifications.filter.errors": "Errors", + "agent_management.notifications.filter.unread": "Unread", + "agent_management.notifications.from_codex": "Notification from Codex", + "agent_management.notifications.mark_all_as_read": "Mark all as read", + "agent_management.notifications.needs_attention_suffix": " needs attention", + "agent_management.notifications.open_conversation": "Open conversation", + "agent_management.notifications.task_cancelled": "Task was cancelled.", + "agent_management.notifications.task_completed": "Task completed.", + "agent_management.notifications.title": "Notifications", + "agent_management.notifications.waiting_for_input": "Waiting for input.", + "agent_management.owner_filter.all": "All", + "agent_management.owner_filter.all.tooltip": "View your agent tasks plus all shared team tasks", + "agent_management.owner_filter.personal": "Personal", + "agent_management.owner_filter.personal.tooltip": "View agent tasks you created", + "agent_management.runs_title": "Runs", + "agent_management.search_placeholder": "Search", + "agent_management.session.expired": "Session expired", + "agent_management.session.expired_tooltip": "Sessions expire after one week and cannot be opened.", + "agent_management.session.unavailable": "No session available", + "agent_management.source.github_action": "GitHub Action", + "agent_management.source.oz_web": "Oz Web", + "agent_management.source.scheduled": "Scheduled", + "agent_management.source.warp_app": "Warp App", + "agent_management.status.done": "Done", + "agent_management.status.failed": "Failed", + "agent_management.status.working": "Working", + "agent_management.toast.copied_branch_name": "Copied branch name", + "agent_management.unknown": "Unknown", + "agent_management.view_agents": "View Agents", + "ai.agent.figma.enable_mcp": "Enable Figma MCP", + "ai.agent.figma.enabling": "Enabling...", + "ai.agent.figma.get_mcp": "Get Figma MCP", + "ai.agent_mode.attach_as_context": "Attach as agent context", + "ai.artifact.button.copy_branch_name": "Copy branch name", + "ai.artifact.button.open_pull_request": "Open pull request", + "ai.model.disable_reason.admin_disabled": "This model has been disabled by your team admin.", + "ai.model.disable_reason.out_of_requests": "Please upgrade your plan to make more requests.", + "ai.model.disable_reason.provider_outage": "This model is temporarily unavailable due to a provider outage.", + "ai.model.disable_reason.requires_upgrade": "Please upgrade your plan to access this model.", + "ai.model.disable_reason.unavailable": "This model is unavailable.", + "ai_assistant.accuracy_notice": "AI responses can be inaccurate.", + "ai_assistant.ask_warp_ai": "Ask Warp AI", + "ai_assistant.character_limit_exceeded": "Character limit exceeded.", + "ai_assistant.copy_transcript_tooltip": "Copy transcript to clipboard", + "ai_assistant.credits.until_refresh": "{time} until refresh.", + "ai_assistant.credits.used": "Credits used: {used} / {limit}.", + "ai_assistant.generating_answer": "Generating answer...", + "ai_assistant.missing_context_notice": "Warp AI might forget earlier answers as conversations get long.", + "ai_assistant.placeholder.ask_question": "Ask a question...", + "ai_assistant.placeholder.followup": "Type a response or click one above...", + "ai_assistant.prompt.connect_aws_ec2": "Write a script to connect to an AWS EC2 instance.", + "ai_assistant.prompt.find_files_containing_text": "How do I find all files containing specific text?", + "ai_assistant.prompt.how_do_i_fix_this": "How do I fix this?", + "ai_assistant.prompt.show_examples": "Show examples.", + "ai_assistant.prompt.undo_recent_commits": "How do I undo the most recent commits in git?", + "ai_assistant.prompt.what_to_do_next": "What should I do next?", + "ai_assistant.requests.duration.day": "1 day", + "ai_assistant.requests.duration.days": "{count} days", + "ai_assistant.requests.duration.hour": "1 hour", + "ai_assistant.requests.duration.hours": "{count} hours", + "ai_assistant.requests.duration.minute": "1 minute", + "ai_assistant.requests.duration.minutes": "{count} minutes", + "ai_assistant.requests.out_of_credits": "It seems you're out of credits. Please try again {next_time}.", + "ai_assistant.requests.out_of_credits_contact_admin": "It seems you're out of credits. Please try again {next_time}.\n\nContact a team admin to upgrade for more credits.", + "ai_assistant.requests.out_of_credits_upgrade": "It seems you're out of credits. Please try again {next_time}.\n\n[Upgrade]({upgrade_url}) for more credits.", + "ai_assistant.requests.retry_after": "after {time}", + "ai_assistant.requests.retry_later": "later", + "ai_assistant.transcript.answer": "Warp AI: {text}\n\n", + "ai_assistant.transcript.copy_answer_tooltip": "Copy answer to clipboard", + "ai_assistant.transcript.copy_code_tooltip": "Copy code to clipboard [Cmd + C]", + "ai_assistant.transcript.insert_code_tooltip": "Insert code into terminal input [Cmd + Enter]", + "ai_assistant.transcript.prompt": "Prompt: {text}\n\n", + "ai_assistant.transcript.save_as_workflow_tooltip": "Save as workflow [Cmd + S]", + "ai_assistant.transcript.title": "## Warp AI Transcript ({time})\n\n", + "ai_assistant.zero_state_help": "Shift + ctrl + space a block or text selection to ask Warp AI.", + "ai_document.attach_to_active_session": "Attach to active session", + "ai_document.auto_save_synced_tooltip": "This plan is synced to your Warp Drive and will auto save any edits you make.", + "ai_document.copy_plan_id": "Copy plan ID", + "ai_document.default_planning_document_title": "Planning document", + "ai_document.save_and_auto_sync_tooltip": "Save and auto-sync this plan to your Warp Drive", + "ai_document.save_as_markdown_file": "Save as markdown file", + "ai_document.show_in_warp_drive": "Show in Warp Drive", + "ai_document.show_version_history": "Show version history", + "ai_document.toast.link_copied": "Link copied to clipboard", + "ai_document.toast.plan_id_copied": "Plan ID copied to clipboard", + "ai_document.update_agent": "Update Agent", + "ai_document.update_agent_tooltip": "This plan has changes the agent isn’t aware of. {save_action} to stop the agent’s current task and send the updated plan", + "ai_document.version_restored_from": "{version} (restored from {from_version})", + "ai.agent_conversations.task_blocked": "Task blocked", + "ai.agent_sdk.admin.already_logged_in": "You are already logged in.", + "ai.agent_sdk.admin.already_logged_in_as_name": "You are already logged in as {name}.", + "ai.agent_sdk.admin.already_logged_in_as_username_email": "You are already logged in as {username} ({email}).", + "ai.agent_sdk.admin.authentication_failed": "Authentication failed", + "ai.agent_sdk.admin.authentication_failed_with_error": "Authentication failed: {error}", + "ai.agent_sdk.admin.display_name": "Display Name: {name}", + "ai.agent_sdk.admin.email": "Email: {email}", + "ai.agent_sdk.admin.logged_in_successfully": "Logged in successfully", + "ai.agent_sdk.admin.logged_out_successfully": "Logged out successfully.", + "ai.agent_sdk.admin.login_open_url": "To log in, open this URL in your browser:\n{url}", + "ai.agent_sdk.admin.login_visit_and_enter_code": "To log in, visit {url} and enter this code: {code}", + "ai.agent_sdk.admin.not_logged_in": "You are not logged in.", + "ai.agent_sdk.admin.service_account_id": "Service account ID: {uid}", + "ai.agent_sdk.admin.team_id": "Team ID: {team_uid}", + "ai.agent_sdk.admin.team_name": "Team Name: {team_name}", + "ai.agent_sdk.admin.user_id": "User ID: {uid}", + "ai.agent_sdk.admin.user_id_missing": "Could not determine user ID. Are you logged in?", + "ai.agent_sdk.admin.whoami_ndjson_unsupported": "`whoami` does not support `--output-format ndjson`", + "ai.agent_sdk.agent_config.authorize_access_here": "Authorize access here: {url}", + "ai.agent_sdk.agent_config.cannot_access_private_repo": "Cannot access private repo {owner}/{repo}", + "ai.agent_sdk.agent_config.check_github_auth_status_failed": "Failed to check GitHub auth status", + "ai.agent_sdk.agent_config.fetching_from_environments": "Fetching agent skills from your Warp environments...", + "ai.agent_sdk.agent_config.fetching_from_repository": "Fetching agent skills from the specified repository...", + "ai.agent_sdk.agent_config.field.environments": "Environments: {environments}", + "ai.agent_sdk.agent_config.field.id": "ID: {id}", + "ai.agent_sdk.agent_config.field.source": "Source: {owner}/{name}", + "ai.agent_sdk.agent_config.github_authorization_expired": "GitHub authorization expired. Please try again.", + "ai.agent_sdk.agent_config.github_authorization_failed": "GitHub authorization failed. Please try again.", + "ai.agent_sdk.agent_config.heading.agent": "Agent:", + "ai.agent_sdk.agent_config.heading.agents": "Agents ({count}):", + "ai.agent_sdk.agent_config.invalid_repo_format": "Invalid repo format: '{repo}'. Expected 'owner/repo' or 'https://github.com/owner/repo'", + "ai.agent_sdk.agent_config.label.base_prompt": "Base Prompt", + "ai.agent_sdk.agent_config.label.description": "Description", + "ai.agent_sdk.agent_config.max_authorization_attempts_exceeded": "Exceeded maximum number of authorization attempts ({count}). Please try again later.", + "ai.agent_sdk.agent_config.no_auth_flow_provided": "Cannot list agents: authorization required but no auth flow provided", + "ai.agent_sdk.agent_config.no_skills_found": "No skills found.", + "ai.agent_sdk.agent_config.opening_github_authorization": "Opening browser for GitHub authorization: {url}", + "ai.agent_sdk.agent_config.poll_oauth_status_error": "Error polling OAuth status: {error}", + "ai.agent_sdk.agent_config.private_repo_authorization_required": "Authorization required for private repository access.", + "ai.agent_sdk.agent_config.rerun_after_authorizing": "After authorizing, please re-run this command.", + "ai.agent_sdk.agent_config.unexpected_oauth_status": "Unexpected OAuth status", + "ai.agent_sdk.agent_config.user_not_connected_to_github": "User not connected to GitHub", + "ai.agent_sdk.agent_management.deleted": "Deleted agent {uid}.", + "ai.agent_sdk.agent_management.disabled_agents_hidden": "{count} disabled agents hidden", + "ai.agent_sdk.agent_management.json_sort_unsupported": "--sort-by and --sort-order are not supported with JSON output", + "ai.agent_sdk.agent_management.no_agents_found": "No agents found.", + "ai.agent_sdk.agent_management.no_updates_requested": "No updates requested", + "ai.agent_sdk.agent_management.skills_hint": "Looking for your agent skills? Use `{binary_name} agent skills` instead.", + "ai.agent_sdk.agent_management.table.base_model": "Base model", + "ai.agent_sdk.agent_management.table.created": "Created", + "ai.agent_sdk.agent_management.table.description": "Description", + "ai.agent_sdk.agent_management.table.environment": "Environment", + "ai.agent_sdk.agent_management.table.name": "Name", + "ai.agent_sdk.agent_management.table.secrets": "Secrets", + "ai.agent_sdk.agent_management.table.skills": "Skills", + "ai.agent_sdk.agent_management.table.uid": "UID", + "ai.agent_sdk.ambient.agent_state": "Agent state: {state}", + "ai.agent_sdk.ambient.artifact.branch": "Branch: {branch}", + "ai.agent_sdk.ambient.artifact.description": "Description: {description}", + "ai.agent_sdk.ambient.artifact.file": "File: {label}", + "ai.agent_sdk.ambient.artifact.link": "Link: {url}", + "ai.agent_sdk.ambient.artifact.no_description": "No description", + "ai.agent_sdk.ambient.artifact.path": "Path: {path}", + "ai.agent_sdk.ambient.artifact.plan": "Plan: {title}", + "ai.agent_sdk.ambient.artifact.pr": "PR:", + "ai.agent_sdk.ambient.artifact.pr_with_repo": "PR: {repo} #{number}", + "ai.agent_sdk.ambient.artifact.screenshot": "Screenshot: {uid} ({description})", + "ai.agent_sdk.ambient.artifact.untitled_plan": "Untitled Plan", + "ai.agent_sdk.ambient.artifacts": "Artifacts:", + "ai.agent_sdk.ambient.attachment_upload_not_enabled": "Attachment upload is not enabled", + "ai.agent_sdk.ambient.concurrent_limit_reached": "Concurrent cloud agent limit reached. This agent run will begin when one of your current cloud runs completes.", + "ai.agent_sdk.ambient.config": "Config:\n{config}", + "ai.agent_sdk.ambient.created": "Created: {time}", + "ai.agent_sdk.ambient.env_not_unicode": "{env} is set but is not valid Unicode", + "ai.agent_sdk.ambient.error": "Error: {message}", + "ai.agent_sdk.ambient.executed_as": "Executed as: {executor}", + "ai.agent_sdk.ambient.failed_no_error_message": "Run failed with no error message", + "ai.agent_sdk.ambient.flush_stdout_failed": "unable to flush stdout", + "ai.agent_sdk.ambient.heading.run": "Agent Run:", + "ai.agent_sdk.ambient.heading.runs": "Agent Runs ({count}):", + "ai.agent_sdk.ambient.invalid_oz_run_id": "Invalid OZ_RUN_ID", + "ai.agent_sdk.ambient.label.status": "Status", + "ai.agent_sdk.ambient.label.title": "Title", + "ai.agent_sdk.ambient.message.body": "Body:", + "ai.agent_sdk.ambient.message.delivered_at": "Delivered At: {delivered_at}", + "ai.agent_sdk.ambient.message.from": "From: {from}", + "ai.agent_sdk.ambient.message.ids": "Message IDs:", + "ai.agent_sdk.ambient.message.marked_delivered": "Marked message delivered: {id}", + "ai.agent_sdk.ambient.message.message_id": "Message ID: {id}", + "ai.agent_sdk.ambient.message.read_at": "Read At: {read_at}", + "ai.agent_sdk.ambient.message.send_failed_context": "Failed to send agent message (sender_run_id={sender_run_id}, task_id={task_id}, target_agent_ids={target_agent_ids})", + "ai.agent_sdk.ambient.message.sent_at": "Sent At: {sent_at}", + "ai.agent_sdk.ambient.message.sent_count": "Sent {count} message(s).", + "ai.agent_sdk.ambient.message.subject": "Subject: {subject}", + "ai.agent_sdk.ambient.message.table.delivered_at": "DELIVERED AT", + "ai.agent_sdk.ambient.message.table.from": "FROM", + "ai.agent_sdk.ambient.message.table.message_id": "MESSAGE ID", + "ai.agent_sdk.ambient.message.table.read_at": "READ AT", + "ai.agent_sdk.ambient.message.table.sent_at": "SENT AT", + "ai.agent_sdk.ambient.message.table.subject": "SUBJECT", + "ai.agent_sdk.ambient.message.watch.closed": "Message watch stream closed. Reconnecting in {seconds}s.", + "ai.agent_sdk.ambient.message.watch.disconnected": "Message watch disconnected: {error}. Retrying in {seconds}s.", + "ai.agent_sdk.ambient.message.watch.hydrate_failed": "Failed to hydrate message {message_id}: {error}. Retrying in {seconds}s.", + "ai.agent_sdk.ambient.message.watch.malformed_event_payload": "Skipping malformed agent event payload: {error}", + "ai.agent_sdk.ambient.message.watch.missing_ref_id": "Skipping new_message event without ref_id at sequence {sequence}.", + "ai.agent_sdk.ambient.message.watch.open_failed": "Failed to open agent event stream", + "ai.agent_sdk.ambient.message.watch.reconnect_failed": "Message watch reconnect failed: {error}. Retrying in {seconds}s.", + "ai.agent_sdk.ambient.message.watch.reconnected": "Reconnected message watch for run {run_id} at sequence {sequence}.", + "ai.agent_sdk.ambient.missing_prompt_source": "Either --prompt, --skill, or --conversation must be provided", + "ai.agent_sdk.ambient.no_runs_found": "No runs found.", + "ai.agent_sdk.ambient.saved_prompt_not_found": "Saved prompt with ID '{id}' not found", + "ai.agent_sdk.ambient.saved_prompt_not_prompt": "'{id}' is not a saved prompt", + "ai.agent_sdk.ambient.saved_prompt_parse_failed": "Failed to parse saved prompt ID '{id}': {error}", + "ai.agent_sdk.ambient.session": "Session: {url}", + "ai.agent_sdk.ambient.session_not_ready": "Agent session with run ID {id} is not ready after {seconds}s. Check for a sharing link in the ambient agent management panel. See https://docs.warp.dev/agent-platform/cloud-agents/managing-cloud-agents for details.", + "ai.agent_sdk.ambient.spawned": "Spawned ambient agent with run ID: {id}", + "ai.agent_sdk.ambient.streaming_requires_ndjson": "Streaming commands require `--output-format ndjson`", + "ai.agent_sdk.ambient.too_many_attachments": "Too many attachments. Maximum {max} attachments allowed, but {count} were provided.", + "ai.agent_sdk.ambient.unexpected_skill_arg": "unexpected argument '--skill' found", + "ai.agent_sdk.ambient.unknown": "unknown", + "ai.agent_sdk.ambient.unsupported_feature": "Unsupported feature", + "ai.agent_sdk.ambient.upgrade_plan": "To increase your concurrent agent limit, upgrade your plan: {url}", + "ai.agent_sdk.ambient.view_run": "View run: {url}", + "ai.agent_sdk.ambient.view_session": "View agent session: {url}", + "ai.agent_sdk.ambient.without_environment": "Agent will run without an environment.", + "ai.agent_sdk.api_key.create_failed": "failed to create API key", + "ai.agent_sdk.api_key.created": "API key '{name}' created.", + "ai.agent_sdk.api_key.display": "{name} ({uid}, created {created_at})", + "ai.agent_sdk.api_key.expiration_behavior_required": "expiration behavior is required", + "ai.agent_sdk.api_key.expiration_cancelled": "Expiration cancelled", + "ai.agent_sdk.api_key.expiration_too_large": "expiration duration is too large", + "ai.agent_sdk.api_key.expire_confirm": "Expire API key '{key}'?", + "ai.agent_sdk.api_key.expire_confirm_help": "This action takes effect immediately", + "ai.agent_sdk.api_key.expire_failed": "failed to expire API key", + "ai.agent_sdk.api_key.expire_refusing_noninteractive": "Refusing to expire API key without confirmation in non-interactive mode (use --force to bypass)", + "ai.agent_sdk.api_key.expired": "API key '{uid}' expired.", + "ai.agent_sdk.api_key.multiple_matches": "Multiple API keys match '{key}':", + "ai.agent_sdk.api_key.multiple_select": "Multiple API keys match '{key}'. Select a key to expire:", + "ai.agent_sdk.api_key.multiple_specify_uid": "Multiple API keys match '{key}'; specify the key by UID", + "ai.agent_sdk.api_key.never": "Never", + "ai.agent_sdk.api_key.not_expired": "API key '{uid}' was not expired.", + "ai.agent_sdk.api_key.not_found": "API key '{key}' not found", + "ai.agent_sdk.api_key.raw_api_key": "Raw API key: {api_key}", + "ai.agent_sdk.api_key.store_securely": "This secret key is shown only once. Store it securely.", + "ai.agent_sdk.api_key.table.created": "Created", + "ai.agent_sdk.api_key.table.expires_at": "Expires At", + "ai.agent_sdk.api_key.table.key": "Key", + "ai.agent_sdk.api_key.table.last_used": "Last Used", + "ai.agent_sdk.api_key.table.name": "Name", + "ai.agent_sdk.api_key.table.scope": "Scope", + "ai.agent_sdk.api_key.table.uid": "UID", + "ai.agent_sdk.api_key.uid": "UID: {uid}", + "ai.agent_sdk.artifact_upload.confirm_failed": "Failed to confirm file artifact upload", + "ai.agent_sdk.artifact_upload.conversation_missing_cloud_task": "Conversation '{conversation_id}' is not backed by a cloud agent task", + "ai.agent_sdk.artifact_upload.conversation_not_found": "Conversation not found", + "ai.agent_sdk.artifact_upload.conversation_resolution_required": "Conversation resolution should be provided", + "ai.agent_sdk.artifact_upload.create_target_failed": "Failed to create file artifact upload target", + "ai.agent_sdk.artifact_upload.env_var_not_set": "{env_var} is not set", + "ai.agent_sdk.artifact_upload.env_var_not_unicode": "{env_var} is set but is not valid Unicode", + "ai.agent_sdk.artifact_upload.file_size_out_of_range": "Artifact file size exceeds supported range", + "ai.agent_sdk.artifact_upload.invalid_env_run_id": "Invalid {env_var}", + "ai.agent_sdk.artifact_upload.load_conversation_failed": "Failed to load conversation '{conversation_id}' to resolve artifact upload headers", + "ai.agent_sdk.artifact_upload.multiple_conversations_found": "Multiple conversations found for '{conversation_id}'", + "ai.agent_sdk.artifact_upload.open_file_failed": "Failed to open artifact file '{path}'", + "ai.agent_sdk.artifact_upload.read_file_failed": "Failed to read artifact file '{path}'", + "ai.agent_sdk.artifact_upload.resolve_association_failed": "Failed to resolve artifact upload association: no usable --run-id or --conversation-id was provided, and {env_var}: {env_err}", + "ai.agent_sdk.artifact_upload.resolve_association_for_conversation_failed": "Failed to resolve artifact upload association for conversation '{conversation_id}': {conversation_err}; also failed to use {env_var}: {env_err}", + "ai.agent_sdk.artifact_upload.stat_file_failed": "Failed to stat artifact file '{path}'", + "ai.agent_sdk.artifact.downloaded": "Artifact downloaded", + "ai.agent_sdk.artifact.field.artifact_type": "Artifact type: {type}", + "ai.agent_sdk.artifact.field.artifact_uid": "Artifact UID: {uid}", + "ai.agent_sdk.artifact.field.content_type": "Content type: {content_type}", + "ai.agent_sdk.artifact.field.created_at": "Created at: {created_at}", + "ai.agent_sdk.artifact.field.description": "Description: {description}", + "ai.agent_sdk.artifact.field.download_url": "Download URL: {url}", + "ai.agent_sdk.artifact.field.expires_at": "Expires at: {expires_at}", + "ai.agent_sdk.artifact.field.filename": "Filename: {filename}", + "ai.agent_sdk.artifact.field.filepath": "Filepath: {filepath}", + "ai.agent_sdk.artifact.field.mime_type": "MIME type: {mime_type}", + "ai.agent_sdk.artifact.field.path": "Path: {path}", + "ai.agent_sdk.artifact.field.size_bytes": "Size bytes: {size_bytes}", + "ai.agent_sdk.artifact.get_failed": "Failed to get artifact '{uid}'", + "ai.agent_sdk.artifact.header.artifact_type": "Artifact type", + "ai.agent_sdk.artifact.header.artifact_uid": "Artifact UID", + "ai.agent_sdk.artifact.header.content_type": "Content type", + "ai.agent_sdk.artifact.header.created_at": "Created at", + "ai.agent_sdk.artifact.header.description": "Description", + "ai.agent_sdk.artifact.header.download_url": "Download URL", + "ai.agent_sdk.artifact.header.expires_at": "Expires at", + "ai.agent_sdk.artifact.header.filename": "Filename", + "ai.agent_sdk.artifact.header.filepath": "Filepath", + "ai.agent_sdk.artifact.header.mime_type": "MIME type", + "ai.agent_sdk.artifact.header.path": "Path", + "ai.agent_sdk.artifact.header.size_bytes": "Size bytes", + "ai.agent_sdk.artifact.uploaded": "Artifact uploaded", + "ai.agent_sdk.authentication_failed_with_error": "Authentication failed: {error}", + "ai.agent_sdk.bedrock_role_region_required": "--bedrock-role-region is required when --bedrock-inference-role is set", + "ai.agent_sdk.check_warp_logs": "For more information, check Warp logs at {path}", + "ai.agent_sdk.claude_auth_secret_harness_only": "--claude-auth-secret is only valid with --harness claude.", + "ai.agent_sdk.common.conversation_not_found_or_inaccessible": "conversation {conversation_id} not found or not accessible", + "ai.agent_sdk.common.invalid_id": "{id} is not a valid {kind} identifier", + "ai.agent_sdk.common.invalid_run_id": "Invalid run ID", + "ai.agent_sdk.common.no_environment_choice": "No environment (agent will not be able to access private repositories or create pull requests)", + "ai.agent_sdk.common.no_environments_configured": "No environments are configured for this account.\nYou can create an environment with `{cli_name} environment create`.\nOr, re-run this command with `--no-environment` to not use an environment.\nWithout an environment, the agent will not be able to access private repositories or create pull requests.", + "ai.agent_sdk.common.object_kind.environment": "environment", + "ai.agent_sdk.common.object_not_found": "{kind} {id} not found", + "ai.agent_sdk.common.operation_cancelled": "Operation canceled", + "ai.agent_sdk.common.owner.personal": "Personal", + "ai.agent_sdk.common.owner.team": "Team", + "ai.agent_sdk.common.select_environment_error": "Error selecting environment: {error}", + "ai.agent_sdk.common.select_environment_prompt": "Select an environment to run the agent in (or 'No environment'):", + "ai.agent_sdk.common.team_metadata_timeout": "Timed out refreshing team metadata", + "ai.agent_sdk.common.unknown_model_id": "Unknown model id '{model_id}'. Try one of: {suggestions}", + "ai.agent_sdk.common.user_not_on_team": "User is not on a team", + "ai.agent_sdk.common.user_should_be_logged_in": "User should be logged in", + "ai.agent_sdk.common.warp_drive_sync_timeout": "Timed out waiting for Warp Drive to sync", + "ai.agent_sdk.config_file.invalid_json": "Invalid JSON in config file '{path}'", + "ai.agent_sdk.config_file.invalid_mcp_servers": "Invalid mcp_servers in '{path}'", + "ai.agent_sdk.config_file.invalid_yaml": "Invalid YAML in config file '{path}'", + "ai.agent_sdk.config_file.parse_json_or_yaml_failed": "Failed to parse config file '{path}' as JSON or YAML", + "ai.agent_sdk.config_file.read_failed": "Failed to read config file '{path}'", + "ai.agent_sdk.config_file.serialize_mcp_map_failed": "Failed to serialize MCP server map", + "ai.agent_sdk.config_file.supported_keys": "Supported keys: name, environment_id, model_id, base_prompt, mcp_servers, host, computer_use_enabled", + "ai.agent_sdk.config_file.wasm_unsupported": "Config files are not supported in WASM builds", + "ai.agent_sdk.conversation_conversion_failed": "Failed to convert conversation data to AIConversation", + "ai.agent_sdk.conversation_flag_unavailable": "The --conversation flag is not available in this build", + "ai.agent_sdk.conversation_subcommand_unavailable": "The 'conversation' subcommand is not available in this build", + "ai.agent_sdk.driver.attachments.create_dir_failed": "Failed to create attachments directory", + "ai.agent_sdk.driver.attachments.create_file_failed": "Failed to create file", + "ai.agent_sdk.driver.attachments.create_handoff_dir_failed": "Failed to create handoff attachments directory", + "ai.agent_sdk.driver.attachments.download_status_failed": "Download failed with status {status}: {body}", + "ai.agent_sdk.driver.attachments.fetch_handoff_failed": "Failed to fetch handoff snapshot attachments", + "ai.agent_sdk.driver.attachments.fetch_task_failed": "Failed to fetch task attachments", + "ai.agent_sdk.driver.attachments.file_too_large": "File is too large ({size_mb}MB). Maximum size is {max_mb}MB.", + "ai.agent_sdk.driver.attachments.invalid_filename": "Invalid filename for file_id={file_id}", + "ai.agent_sdk.driver.attachments.read_attachment_file_failed": "Failed to read attachment file '{path}': {error}", + "ai.agent_sdk.driver.attachments.send_download_request_failed": "Failed to send download request", + "ai.agent_sdk.driver.attachments.write_file_failed": "Failed to write file", + "ai.agent_sdk.driver.cleanup_cloud_provider_failed": "Unable to clean up cloud provider", + "ai.agent_sdk.driver.cleanup_harness_runtime_state_failed": "Failed to clean up harness runtime state", + "ai.agent_sdk.driver.cloud_provider.aws.create_token_file_failed": "Failed to create temporary AWS OIDC token file", + "ai.agent_sdk.driver.cloud_provider.aws.remove_token_file_failed": "Failed to remove AWS OIDC token file", + "ai.agent_sdk.driver.cloud_provider.aws.write_token_file_failed": "Failed to write AWS OIDC token file", + "ai.agent_sdk.driver.cloud_provider.gcp.prepare_credentials_failed": "Failed to prepare GCP federation credentials", + "ai.agent_sdk.driver.cloud_provider.gcp.remove_credentials_failed": "Failed to remove GCP credential files", + "ai.agent_sdk.driver.cloud_provider.setup_failed": "{provider_name} setup failed", + "ai.agent_sdk.driver.error_classification.aws_bedrock_failed": "Failed to initialize AWS Bedrock credentials: {message}", + "ai.agent_sdk.driver.error_classification.bootstrap_failed": "Terminal session failed to start. Please try running your task again.", + "ai.agent_sdk.driver.error_classification.cloud_access_failed": "Error configuring cloud access: {error}", + "ai.agent_sdk.driver.error_classification.config_build_failed": "Failed to build agent configuration: {error}", + "ai.agent_sdk.driver.error_classification.conversation_blocked": "The agent got stuck waiting for user confirmation on the action: {blocked_action}", + "ai.agent_sdk.driver.error_classification.conversation_harness_mismatch": "Conversation {conversation_id} was produced by the {expected} harness, but --harness {got} was requested. Re-run with --harness {expected} (or omit --harness) to continue this conversation.", + "ai.agent_sdk.driver.error_classification.conversation_load_failed": "Failed to load conversation: {message}", + "ai.agent_sdk.driver.error_classification.environment_not_found": "Environment '{id}' not found. Verify the environment ID and ensure it exists in your team settings.", + "ai.agent_sdk.driver.error_classification.environment_setup_failed": "Environment setup failed: {message}. Check your repository URLs and setup commands.", + "ai.agent_sdk.driver.error_classification.harness_auth_check_failed": "Harness '{harness}' authentication check failed: login credentials are invalid or expired. Verify that the authentication secret configured for this harness is correct.", + "ai.agent_sdk.driver.error_classification.harness_command_failed": "Harness command exited with code {exit_code}", + "ai.agent_sdk.driver.error_classification.harness_config_failed": "Harness '{harness}' config setup failed: {error}", + "ai.agent_sdk.driver.error_classification.harness_runtime_failure": "Harness '{harness}' could not make a successful API request. Matched failure pattern '{pattern}' in harness output: \"{excerpt}\". This usually means the API key is invalid, out of credits, or the account is misconfigured.", + "ai.agent_sdk.driver.error_classification.harness_setup_failed": "Harness '{harness}' validation failed: {reason}", + "ai.agent_sdk.driver.error_classification.internal_error": "An internal error occurred. Please try running your task again. If the issue persists, contact support.", + "ai.agent_sdk.driver.error_classification.invalid_working_directory": "Working directory '{path}' does not exist or is not a directory. Verify the path in your environment configuration.", + "ai.agent_sdk.driver.error_classification.mcp_json_parse_failed": "Failed to parse MCP server JSON configuration: {message}", + "ai.agent_sdk.driver.error_classification.mcp_missing_variables": "MCP server configuration is missing required variables. Provide all required environment variables or template values.", + "ai.agent_sdk.driver.error_classification.mcp_server_not_found": "MCP server {uuid} was not found. Verify the server exists in your Warp Drive and the UUID is correct.", + "ai.agent_sdk.driver.error_classification.mcp_startup_failed": "One or more MCP servers failed to start. Check that your MCP server configuration is valid and the server process is runnable.", + "ai.agent_sdk.driver.error_classification.not_logged_in": "Authentication required. Log in via '{bin} login', provide an API key via '--api-key', or set the WARP_API_KEY environment variable.", + "ai.agent_sdk.driver.error_classification.profile_not_found": "Agent profile \"{name}\" not found. Check the profile ID and ensure it exists in your team's Warp Drive.", + "ai.agent_sdk.driver.error_classification.prompt_resolution_failed": "Failed to resolve prompt for the run: {error}", + "ai.agent_sdk.driver.error_classification.resume_state_missing": "Conversation {conversation_id} has no stored transcript for the {harness} harness. The prior run may have crashed before saving any state.", + "ai.agent_sdk.driver.error_classification.saved_prompt_not_found": "Saved prompt not found for ID {id}. Verify the prompt exists in your Warp Drive.", + "ai.agent_sdk.driver.error_classification.secrets_fetch_failed": "Failed to fetch task secrets: {error}", + "ai.agent_sdk.driver.error_classification.share_session_disabled": "Session sharing is not enabled for your account. This is likely because an administrator has disabled session sharing for your team. Please verify that session sharing is enabled in your team settings, or try running without the --share flag.", + "ai.agent_sdk.driver.error_classification.share_session_failed": "Failed to share agent session: {reason}", + "ai.agent_sdk.driver.error_classification.share_session_internal": "Failed to share agent session due to an internal error. Please try running your task again.", + "ai.agent_sdk.driver.error_classification.share_session_interrupted": "Session sharing was interrupted before it could complete. Please try running your task again.", + "ai.agent_sdk.driver.error_classification.share_session_timeout": "Failed to share agent session: timed out waiting for the session sharing server to respond. Please check your network connection and try again.", + "ai.agent_sdk.driver.error_classification.skill_resolution_failed": "Skill resolution failed: {message}", + "ai.agent_sdk.driver.error_classification.task_cancelled": "Task cancelled.", + "ai.agent_sdk.driver.error_classification.task_harness_mismatch": "Task {task_id} was created with the {expected} harness, but --harness {got} was requested. Re-run with --harness {expected} (or omit --harness) to continue this task.", + "ai.agent_sdk.driver.error_classification.team_metadata_timeout": "Timed out refreshing team metadata. Please check your network connection and try again.", + "ai.agent_sdk.driver.error_classification.warp_drive_sync_failed": "Warp Drive failed to sync. Please check your network connection and try again.", + "ai.agent_sdk.driver.exit_harness_after_runtime_failure_failed": "Failed to exit harness after runtime failure detection", + "ai.agent_sdk.driver.exit_harness_failed": "Failed to exit harness", + "ai.agent_sdk.driver.git_credentials.create_dir_failed": "Failed to create {path}", + "ai.agent_sdk.driver.git_credentials.fetch_from_server_failed": "Failed to fetch git credentials from server", + "ai.agent_sdk.driver.git_credentials.home_dir_missing": "Could not determine home directory", + "ai.agent_sdk.driver.git_credentials.issue_workload_token_failed": "Failed to issue workload token for git credentials refresh", + "ai.agent_sdk.driver.git_credentials.open_for_writing_failed": "Failed to open {path} for writing", + "ai.agent_sdk.driver.git_credentials.rename_failed": "Failed to rename {from} to {to}", + "ai.agent_sdk.driver.git_credentials.set_permissions_failed": "Failed to set permissions on {path}", + "ai.agent_sdk.driver.git_credentials.write_file_failed": "Failed to write {path}", + "ai.agent_sdk.driver.harness.claude.open_jsonl_failed": "Failed to open {path}", + "ai.agent_sdk.driver.harness.claude.read_jsonl_line_failed": "Failed to read line from {path}", + "ai.agent_sdk.driver.harness.claude.read_subagents_dir_failed": "Failed to read subagents dir {path}", + "ai.agent_sdk.driver.harness.claude.read_todos_dir_failed": "Failed to read todos dir {path}", + "ai.agent_sdk.driver.harness.claude.read_transcript_for_session_failed": "Failed to read transcript for session {session_id}", + "ai.agent_sdk.driver.harness.claude.rehydrate_transcript_failed": "Failed to rehydrate Claude transcript", + "ai.agent_sdk.driver.harness.claude.resolve_config_dir_failed": "Failed to resolve Claude config dir", + "ai.agent_sdk.driver.harness.claude.serialize_config_failed": "Failed to serialize Claude config", + "ai.agent_sdk.driver.harness.claude.serialize_mcp_config_failed": "Failed to serialize Claude MCP config", + "ai.agent_sdk.driver.harness.claude.serialize_sessions_index_failed": "Failed to serialize sessions-index.json", + "ai.agent_sdk.driver.harness.claude.serialize_settings_failed": "Failed to serialize Claude settings", + "ai.agent_sdk.driver.harness.claude.serialize_transcript_envelope_failed": "Failed to serialize transcript envelope", + "ai.agent_sdk.driver.harness.claude.wake.deserialize_transcript_failed": "Failed to deserialize Claude transcript for wake task {task_id}", + "ai.agent_sdk.driver.harness.claude.wake.fetch_transcript_failed": "Failed to fetch Claude transcript for task {task_id}", + "ai.agent_sdk.driver.harness.claude.wake.prepare_environment_failed": "Failed to prepare Claude environment for wake", + "ai.agent_sdk.driver.harness.claude.wake.rehydrate_transcript_failed": "Failed to rehydrate Claude transcript for wake", + "ai.agent_sdk.driver.harness.claude.wake.resolve_prompt_failed": "Failed to resolve Claude wake prompt for task {task_id}", + "ai.agent_sdk.driver.harness.codex.create_config_dir_failed": "Failed to create Codex config dir at {path}", + "ai.agent_sdk.driver.harness.codex.open_auth_json_failed": "Failed to open {path} for writing", + "ai.agent_sdk.driver.harness.codex.rehydrate_transcript_failed": "Failed to rehydrate codex transcript", + "ai.agent_sdk.driver.harness.codex.resolve_sessions_root_failed": "Failed to resolve codex sessions root", + "ai.agent_sdk.driver.harness.codex.serialize_auth_json_failed": "Failed to serialize Codex auth.json", + "ai.agent_sdk.driver.harness.codex.serialize_transcript_failed": "Failed to serialize codex transcript", + "ai.agent_sdk.driver.harness.codex.set_auth_permissions_failed": "Failed to set permissions on {path}", + "ai.agent_sdk.driver.harness.codex.write_config_toml_failed": "Failed to write Codex config.toml at {path}", + "ai.agent_sdk.driver.harness.codex.write_system_prompt_failed": "Failed to write Codex system prompt to {path}", + "ai.agent_sdk.driver.harness.create_temp_file_failed": "Failed to create temp file '{prefix}': {error}", + "ai.agent_sdk.driver.harness.driver_dropped": "Agent driver dropped", + "ai.agent_sdk.driver.harness.driver_dropped_while_sending": "Agent driver dropped while sending {command}", + "ai.agent_sdk.driver.harness.gemini.serialize_settings_failed": "Failed to serialize Gemini settings", + "ai.agent_sdk.driver.harness.gemini.serialize_trusted_folders_failed": "Failed to serialize Gemini trusted folders", + "ai.agent_sdk.driver.harness.gemini.write_system_prompt_failed": "Failed to write Gemini system prompt to {path}", + "ai.agent_sdk.driver.harness.get_block_upload_slot_failed": "Unable to get block upload slot for conversation {conversation_id}", + "ai.agent_sdk.driver.harness.get_transcript_upload_target_failed": "Failed to get transcript upload target for {conversation_id}", + "ai.agent_sdk.driver.harness.home_dir_missing": "could not determine home directory", + "ai.agent_sdk.driver.harness.json.create_dir_failed": "Failed to create {path}", + "ai.agent_sdk.driver.harness.json.parse_failed": "Failed to parse {path}", + "ai.agent_sdk.driver.harness.json.read_failed": "Failed to read {path}", + "ai.agent_sdk.driver.harness.json.write_failed": "Failed to write {path}", + "ai.agent_sdk.driver.harness.parent_bridge.create_temp_file_failed": "Failed to create temp file for {path}", + "ai.agent_sdk.driver.harness.parent_bridge.flush_temp_file_failed": "Failed to flush temp file for {path}", + "ai.agent_sdk.driver.harness.parent_bridge.no_parent_dir": "{path} has no parent directory", + "ai.agent_sdk.driver.harness.parent_bridge.persist_temp_file_failed": "Failed to persist temporary {prefix} file", + "ai.agent_sdk.driver.harness.parent_bridge.read_lead_agent_message_failed": "Failed to read lead-agent message {message_id}", + "ai.agent_sdk.driver.harness.parent_bridge.remove_file_failed": "Failed to remove {path}", + "ai.agent_sdk.driver.harness.parent_bridge.write_temp_file_failed": "Failed to write temp file for {path}", + "ai.agent_sdk.driver.harness.read_envelope_task_panicked": "read_envelope task panicked", + "ai.agent_sdk.driver.harness.serialize_block_failed": "Unable to serialize block for conversation {conversation_id}", + "ai.agent_sdk.driver.harness.write_temp_file_failed": "Failed to write temp file '{prefix}': {error}", + "ai.agent_sdk.driver.output.action.computer_use": "Computer use action: {summary}", + "ai.agent_sdk.driver.output.action.editing_files": "Editing files:", + "ai.agent_sdk.driver.output.action.fetching_conversation": "Fetching conversation {conversation_id}", + "ai.agent_sdk.driver.output.action.finding_files": "Finding files matching {queries}", + "ai.agent_sdk.driver.output.action.finding_files_in_path": "Finding files matching {queries} in {path}", + "ai.agent_sdk.driver.output.action.grepping": "Grepping for {queries} in {path}", + "ai.agent_sdk.driver.output.action.mcp_tool_call": "MCP tool call {name}({input})", + "ai.agent_sdk.driver.output.action.reading": "Reading {files}", + "ai.agent_sdk.driver.output.action.reading_mcp_resource": "Reading MCP resource {resource}", + "ai.agent_sdk.driver.output.action.reading_skill": "Reading skill: {skill}", + "ai.agent_sdk.driver.output.action.request_computer_use": "Requesting computer use: {summary}", + "ai.agent_sdk.driver.output.action.running_command": "Running `{command}`", + "ai.agent_sdk.driver.output.action.searching_codebase": "Searching {codebase} for {query}", + "ai.agent_sdk.driver.output.action.sending_message": "Sending message to [{addresses}]: {subject}", + "ai.agent_sdk.driver.output.action.starting_agent": "Starting agent: {name}", + "ai.agent_sdk.driver.output.action.uploading_artifact": "Uploading artifact {file_path}", + "ai.agent_sdk.driver.output.action.write_bytes": "Write {bytes} bytes to command", + "ai.agent_sdk.driver.output.artifact.file_uploaded": "File artifact uploaded: {filepath} (artifact: {artifact_uid})", + "ai.agent_sdk.driver.output.artifact.pr_created": "Created PR: {url} (branch: {branch})", + "ai.agent_sdk.driver.output.artifact.screenshot_captured": "Screenshot captured (artifact: {artifact_uid})", + "ai.agent_sdk.driver.output.artifact.upload_failed": "Uploading artifact failed: {error}", + "ai.agent_sdk.driver.output.artifact.uploaded": "Uploaded artifact {artifact_uid}", + "ai.agent_sdk.driver.output.artifact.uploaded_from": "Uploaded artifact {artifact_uid} from {filepath}", + "ai.agent_sdk.driver.output.cancelled": "", + "ai.agent_sdk.driver.output.codebase.search_failed": "Searching codebase failed: {message}", + "ai.agent_sdk.driver.output.codebase.search_results": "Codebase search results:", + "ai.agent_sdk.driver.output.command.completed": "{output}\n\n (`{command}` exited with code {exit_code})", + "ai.agent_sdk.driver.output.command.denylisted": "Command was not allowed to run due to presence on denylist", + "ai.agent_sdk.driver.output.command.finished": "{output}\n\n (exited with code {exit_code})", + "ai.agent_sdk.driver.output.command.long_running": "`{command}` is still running...", + "ai.agent_sdk.driver.output.command.still_running": "Command is still running...", + "ai.agent_sdk.driver.output.command.write_failed": "Failed to write to command.", + "ai.agent_sdk.driver.output.comments.addressed": "Addressed {count} comments", + "ai.agent_sdk.driver.output.conversation_started": "New conversation started with debug ID: {conversation_id}\n", + "ai.agent_sdk.driver.output.events.received": "Received {count} agent events", + "ai.agent_sdk.driver.output.fetch_conversation.error": "Fetch conversation error: {error}", + "ai.agent_sdk.driver.output.fetch_conversation.success": "Fetched conversation to {directory_path}", + "ai.agent_sdk.driver.output.file_edits.failed": "Editing files failed: {error}", + "ai.agent_sdk.driver.output.file_edits.updated_deleted": "Updated {updated_count} files, deleted {deleted_count} files:\n```diff\n{diff}\n```", + "ai.agent_sdk.driver.output.find.failed": "find failed: {error}", + "ai.agent_sdk.driver.output.grep.failed": "grep failed: {error}", + "ai.agent_sdk.driver.output.mcp.audio": "{mime_type} audio", + "ai.agent_sdk.driver.output.mcp.call_tool_failed": "Calling MCP tool failed: {error}", + "ai.agent_sdk.driver.output.mcp.image": "{mime_type} image", + "ai.agent_sdk.driver.output.mcp.read_resource_failed": "Reading MCP resource failed: {error}", + "ai.agent_sdk.driver.output.messages.received": "Received {count} messages", + "ai.agent_sdk.driver.output.open_in_oz": "Open in Oz: {url}\n", + "ai.agent_sdk.driver.output.plan_created": "Created plan (title: {title}, id: {document_id}, notebook: {notebook_link})", + "ai.agent_sdk.driver.output.read_files.failed": "Reading files failed: {error}", + "ai.agent_sdk.driver.output.run_id": "Run ID: {run_id}", + "ai.agent_sdk.driver.output.sharing_session": "Sharing session at: {join_url}", + "ai.agent_sdk.driver.output.skill.invoked": "Skill Read: {name}", + "ai.agent_sdk.driver.output.skill.read_error": "Skill read error: {error}", + "ai.agent_sdk.driver.output.skill.read_success": "Skill read successfully: {file_name}", + "ai.agent_sdk.driver.output.todo.completed": "Completed TODOs:", + "ai.agent_sdk.driver.output.todo.updated": "Updated TODO list:", + "ai.agent_sdk.driver.output.use_computer.error": "Use computer error: {error}", + "ai.agent_sdk.driver.output.web.fetch_failed": "Web fetch failed", + "ai.agent_sdk.driver.output.web.fetched": "Fetched {count} web pages", + "ai.agent_sdk.driver.output.web.fetching": "Fetching {count} web pages...", + "ai.agent_sdk.driver.output.web.search_failed": "Web search failed for: {query}", + "ai.agent_sdk.driver.output.web.searched_results": "Searched web for: {query} ({count} results)", + "ai.agent_sdk.driver.output.web.searching": "Searching web", + "ai.agent_sdk.driver.output.web.searching_for": "Searching web for: {query}", + "ai.agent_sdk.driver.report_driver_error_failed": "Failed to report driver error for task {task_id}", + "ai.agent_sdk.driver.repository_indexing_failed": "Repository indexing failed: {error}", + "ai.agent_sdk.driver.repository_indexing_pending": "Repository indexing is still pending: {repo_path}", + "ai.agent_sdk.driver.repository_not_found": "Repository not found: {repo_path}", + "ai.agent_sdk.driver.save_harness_conversation_final_failed": "Failed to save harness conversation (final)", + "ai.agent_sdk.driver.save_harness_conversation_periodic_failed": "Failed to save harness conversation (periodic)", + "ai.agent_sdk.driver.save_harness_conversation_post_turn_failed": "Failed to save harness conversation (post-turn)", + "ai.agent_sdk.driver.set_ambient_agent_shared_session_id_failed": "Error setting ambient agent shared session ID", + "ai.agent_sdk.driver.slow_bootstrap_warning": "Warning: Terminal session is slow to bootstrap. See https://docs.warp.dev/support-and-community/troubleshooting-and-support/known-issues#shells to troubleshoot.", + "ai.agent_sdk.driver.snapshot.allocate_initial_snapshot_token_failed": "failed to allocate initial snapshot token", + "ai.agent_sdk.driver.snapshot.create_declarations_dir_failed": "Failed to create declarations directory {path}", + "ai.agent_sdk.driver.snapshot.flush_declarations_file_failed": "Failed to flush declarations file {path}", + "ai.agent_sdk.driver.snapshot.get_upload_targets_failed": "Failed to get snapshot upload targets; skipping upload", + "ai.agent_sdk.driver.snapshot.git_command_failed": "git {args} failed in {repo_dir}: {stderr}", + "ai.agent_sdk.driver.snapshot.git_command_timed_out": "git {args} timed out after {timeout} in {repo_dir}", + "ai.agent_sdk.driver.snapshot.open_declarations_file_failed": "Failed to open declarations file {path}", + "ai.agent_sdk.driver.snapshot.read_file_failed": "Failed to read file '{file_path}': {error}", + "ai.agent_sdk.driver.snapshot.serialize_file_declaration_failed": "Failed to serialize file declaration", + "ai.agent_sdk.driver.snapshot.serialize_manifest_failed": "Failed to serialize snapshot manifest; skipping upload", + "ai.agent_sdk.driver.snapshot.upload_manifest_failed": "Failed to upload manifest '{manifest_filename}'", + "ai.agent_sdk.driver.snapshot.write_declarations_file_failed": "Failed to write declarations file {path}", + "ai.agent_sdk.driver.update_harness_state_from_cli_session_event_failed": "Failed to update harness state from CLI session event", + "ai.agent_sdk.driver.write_artifact_created_failed": "Failed to write artifact_created", + "ai.agent_sdk.driver.write_conversation_id_failed": "Failed to write conversation ID", + "ai.agent_sdk.driver.write_exchange_inputs_failed": "Failed to write exchange inputs", + "ai.agent_sdk.driver.write_exchange_output_failed": "Failed to write exchange output", + "ai.agent_sdk.driver.write_run_id_failed": "Failed to write run ID", + "ai.agent_sdk.driver.write_shared_session_event_failed": "Failed to write shared session event", + "ai.agent_sdk.environment.action_cancelled": "Environment {action} canceled.", + "ai.agent_sdk.environment.action.create": "create", + "ai.agent_sdk.environment.action.delete": "delete", + "ai.agent_sdk.environment.action.update": "update", + "ai.agent_sdk.environment.authorize_access_here": "Authorize access here: {url}", + "ai.agent_sdk.environment.cannot_access_private_repo": "Cannot access private repo {owner}/{repo}", + "ai.agent_sdk.environment.check_github_auth_status_failed": "Failed to check GitHub auth status", + "ai.agent_sdk.environment.confirmation_prompt_error": "Error prompting for confirmation: {error}", + "ai.agent_sdk.environment.created_successfully": "Environment created successfully with ID: {id}", + "ai.agent_sdk.environment.creation_cancelled": "Environment creation canceled.", + "ai.agent_sdk.environment.custom_docker_image": "Custom Docker image", + "ai.agent_sdk.environment.delete_failed": "Failed to delete environment", + "ai.agent_sdk.environment.deleted_successfully": "Environment deleted successfully", + "ai.agent_sdk.environment.detail.description": "Description: {description}", + "ai.agent_sdk.environment.detail.docker_image": "Docker image: {image}", + "ai.agent_sdk.environment.detail.name": "Name: {name}", + "ai.agent_sdk.environment.detail.repositories": "Repositories:", + "ai.agent_sdk.environment.detail.repositories_none": "Repositories: None", + "ai.agent_sdk.environment.detail.setup_commands": "Setup commands:", + "ai.agent_sdk.environment.detail.setup_commands_none": "Setup commands: None", + "ai.agent_sdk.environment.enter_custom_docker_image": "Enter custom Docker image name:", + "ai.agent_sdk.environment.enter_custom_image_error": "Error entering custom image", + "ai.agent_sdk.environment.fetch_base_images_failed": "Failed to fetch list of base images", + "ai.agent_sdk.environment.fetch_images_failed": "Failed to fetch images", + "ai.agent_sdk.environment.fetch_images_failed_with_error": "Failed to fetch images: {error}", + "ai.agent_sdk.environment.github_authorization_expired": "GitHub authorization expired. Please try again.", + "ai.agent_sdk.environment.github_authorization_failed": "GitHub authorization failed. Please try again.", + "ai.agent_sdk.environment.image_list_info": "All Warp dev images contain Python and Node. For more information, see: {repo}", + "ai.agent_sdk.environment.image_table.image": "Image", + "ai.agent_sdk.environment.image_table.repository": "Repository", + "ai.agent_sdk.environment.image_table.tag": "Tag", + "ai.agent_sdk.environment.images_info": "All warpdotdev images contain Python and Node, in addition to language-specific tooling. For more info: {repo}", + "ai.agent_sdk.environment.integration_usage_confirm": "This environment is used in the following integration(s): {integrations}. Are you sure you want to {action} it?", + "ai.agent_sdk.environment.integration_usage_unknown_abort": "Aborting environment {action} because integration usage could not be determined. Re-run with --force to override.", + "ai.agent_sdk.environment.invalid_repo_format": "Invalid repo format: '{repo}'. Expected format: 'owner/repo'", + "ai.agent_sdk.environment.max_authorization_attempts_exceeded": "Exceeded maximum number of authorization attempts ({count}). Please try again later.", + "ai.agent_sdk.environment.missing_oauth_auth_url": "Server error: did not receive auth URL for OAuth flow", + "ai.agent_sdk.environment.no_auth_flow_provided": "Cannot {action} environment: authorization required but no auth flow provided by server", + "ai.agent_sdk.environment.no_docker_image": "No docker image provided, please select a base image.", + "ai.agent_sdk.environment.no_warp_dev_images_available": "No Warp dev images available.", + "ai.agent_sdk.environment.not_found": "Environment {id} not found", + "ai.agent_sdk.environment.opening_github_authorization": "Opening browser for GitHub authorization: {url}", + "ai.agent_sdk.environment.poll_oauth_status_error": "Error polling OAuth status: {error}", + "ai.agent_sdk.environment.private_repo_authorization_required": "Authorization required for private repository access.", + "ai.agent_sdk.environment.private_repos_multiple_owners": "All private repositories in an environment must belong to the same owner. Found multiple owners: {owners}.\nIf you need support for private repos from multiple owners, please submit a GitHub issue.", + "ai.agent_sdk.environment.public_repo_auth_warning": "Warning: using public repo {owner}/{repo} without authorization. Read-only access is available, but you need to authorize if you want full access.", + "ai.agent_sdk.environment.repository_removal_warning": "Warning: repository {owner}/{repo} not found in environment, skipping removal", + "ai.agent_sdk.environment.rerun_after_authorizing": "After authorizing, please re-run this command.", + "ai.agent_sdk.environment.select_base_image": "Select a base image:", + "ai.agent_sdk.environment.select_image_error": "Error selecting image", + "ai.agent_sdk.environment.setup_command_removal_warning": "Warning: setup command '{command}' not found in environment, skipping removal", + "ai.agent_sdk.environment.table.base_image": "Base image", + "ai.agent_sdk.environment.table.creator": "Creator", + "ai.agent_sdk.environment.table.description": "Description", + "ai.agent_sdk.environment.table.git_repos": "Git repos", + "ai.agent_sdk.environment.table.id": "ID", + "ai.agent_sdk.environment.table.last_edited": "Last edited", + "ai.agent_sdk.environment.table.name": "Name", + "ai.agent_sdk.environment.table.scope": "Scope", + "ai.agent_sdk.environment.table.setup_commands": "Setup commands", + "ai.agent_sdk.environment.timed_out_waiting_for_warp_drive": "Timed out waiting for Warp Drive to sync", + "ai.agent_sdk.environment.unexpected_non_terminal_oauth_status": "Unexpected non-terminal OAuth status returned", + "ai.agent_sdk.environment.update_failed": "Failed to update environment", + "ai.agent_sdk.environment.updated_successfully": "Environment updated successfully!", + "ai.agent_sdk.environment.user_not_connected_to_github": "User not connected to GitHub", + "ai.agent_sdk.federate.expires_at": "Expires at: {expires_at}", + "ai.agent_sdk.federate.feature_not_enabled": "This feature is not enabled", + "ai.agent_sdk.federate.issuer": "Issuer: {issuer}", + "ai.agent_sdk.federate.subject_template_required": "--subject-template requires at least one value", + "ai.agent_sdk.federate.token": "Token: {token}", + "ai.agent_sdk.federate.write_gcp_token_failed": "Error writing GCP token to {path}", + "ai.agent_sdk.harness_support.artifact_reported": "Artifact reported: {uid}", + "ai.agent_sdk.harness_support.feature_not_enabled": "This feature is not enabled", + "ai.agent_sdk.harness_support.notification_sent": "Notification sent.", + "ai.agent_sdk.harness_support.shutdown_error_args_required": "--error-category and --error-message must be provided together", + "ai.agent_sdk.harness_support.shutdown_reported": "Shutdown reported.", + "ai.agent_sdk.harness_support.task_finished": "Task finished.", + "ai.agent_sdk.harness.local_child_only": "The {harness} harness is only supported for local child agent launches.", + "ai.agent_sdk.integration.action.creation": "creation", + "ai.agent_sdk.integration.action.update": "update", + "ai.agent_sdk.integration.authorize_provider_here": "Authorize the provider here: {url}", + "ai.agent_sdk.integration.created": "Created: {time}", + "ai.agent_sdk.integration.creating_with_environment": "Creating integration with environment {id}.", + "ai.agent_sdk.integration.creating_without_environment": "Creating integration without an environment.", + "ai.agent_sdk.integration.creation_cancelled": "Integration creation canceled.", + "ai.agent_sdk.integration.heading.integration": "Integration:", + "ai.agent_sdk.integration.heading.integrations": "Integrations:", + "ai.agent_sdk.integration.label.base_prompt": "Base prompt", + "ai.agent_sdk.integration.label.environment": "Environment", + "ai.agent_sdk.integration.label.mcp_servers": "MCP servers", + "ai.agent_sdk.integration.label.model": "Model", + "ai.agent_sdk.integration.label.status": "Status", + "ai.agent_sdk.integration.max_attempts_exceeded": "Exceeded maximum number of integration {action} attempts ({count}). Retry.", + "ai.agent_sdk.integration.missing_auth_url": "Server did not return an authURL for the integration {action} process.", + "ai.agent_sdk.integration.no_integrations_found": "No integrations found.", + "ai.agent_sdk.integration.none": "(none)", + "ai.agent_sdk.integration.oauth_authorization_expired": "OAuth authorization expired.", + "ai.agent_sdk.integration.oauth_authorization_failed": "OAuth authorization failed.", + "ai.agent_sdk.integration.poll_oauth_status_error": "Error polling OAuth status: {error}", + "ai.agent_sdk.integration.reported_failure": "Integration {action} reported failure: {message}", + "ai.agent_sdk.integration.rerun_after_authorizing": "After authorizing, re-run the command to continue the integration {action} process.", + "ai.agent_sdk.integration.status.active": "Integration is connected and enabled.", + "ai.agent_sdk.integration.status.connection_error": "This provider is connected but there is an error.", + "ai.agent_sdk.integration.status.not_configured": "Connection is active, but the agent integration has not been configured yet.", + "ai.agent_sdk.integration.status.not_connected": "This integration is not connected.", + "ai.agent_sdk.integration.status.not_enabled": "Integration is configured but currently disabled.", + "ai.agent_sdk.integration.table.created": "Created", + "ai.agent_sdk.integration.table.description": "Description", + "ai.agent_sdk.integration.table.environment": "Environment", + "ai.agent_sdk.integration.table.provider": "Provider", + "ai.agent_sdk.integration.table.status": "Status", + "ai.agent_sdk.integration.table.updated": "Updated", + "ai.agent_sdk.integration.unexpected_non_terminal_oauth_status": "Unexpected non-terminal OAuth status returned", + "ai.agent_sdk.integration.updated": "Updated: {time}", + "ai.agent_sdk.invalid_api_key": "Your API key is invalid. Please provide a valid key via '--api-key' or the WARP_API_KEY environment variable.", + "ai.agent_sdk.invalid_credentials": "Your credentials are invalid. Please log in again with `{cli_name} login`.", + "ai.agent_sdk.invalid_value": "invalid value '{value}'", + "ai.agent_sdk.login_required": "You are not logged in - please log in with `{cli_name} login` to continue.", + "ai.agent_sdk.mcp_config.args_item_must_be_string": "MCP server '{server_name}' field 'args[{idx}]' must be a string", + "ai.agent_sdk.mcp_config.args_must_be_array": "MCP server '{server_name}' field 'args' must be an array", + "ai.agent_sdk.mcp_config.config_must_be_object": "MCP server '{server_name}' config must be a JSON object", + "ai.agent_sdk.mcp_config.duplicate_server_name": "Duplicate MCP server name '{name}' specified multiple times", + "ai.agent_sdk.mcp_config.exactly_one_transport": "MCP server '{server_name}' must have exactly one of: 'warp_id', 'command', or 'url'", + "ai.agent_sdk.mcp_config.field_must_be_non_empty": "MCP server '{server_name}' field '{field}' must be non-empty", + "ai.agent_sdk.mcp_config.field_must_be_object": "MCP server '{server_name}' field '{field}' must be an object", + "ai.agent_sdk.mcp_config.field_must_be_string": "MCP server '{server_name}' field '{field}' must be a string", + "ai.agent_sdk.mcp_config.invalid_json": "Invalid MCP JSON", + "ai.agent_sdk.mcp_config.nested_field_must_be_string": "MCP server '{server_name}' field '{field}.{key}' must be a string", + "ai.agent_sdk.mcp_config.normalize_json_failed": "Failed to normalize MCP JSON", + "ai.agent_sdk.mcp_config.parse_server_map_failed": "Failed to parse MCP server map", + "ai.agent_sdk.mcp_config.warp_id_must_be_uuid": "MCP server '{server_name}' field 'warp_id' must be a UUID", + "ai.agent_sdk.mcp.table.name": "Name", + "ai.agent_sdk.mcp.table.uuid": "UUID", + "ai.agent_sdk.message_subcommand_unavailable": "The 'message' subcommand is not available in this build", + "ai.agent_sdk.model.refresh_workspace_metadata_timeout": "Timed out refreshing workspace metadata", + "ai.agent_sdk.model.table.model_id": "MODEL ID", + "ai.agent_sdk.oauth.authorization_timeout": "Timed out waiting for OAuth authorization", + "ai.agent_sdk.oauth.waiting_for_authorization": "Waiting for authorization to complete... If this doesn't update after authorizing, please restart the command and try again.", + "ai.agent_sdk.output.invalid_data": "Invalid data: {error}", + "ai.agent_sdk.output.jq_filter_error": "jq filter error: {error}", + "ai.agent_sdk.output.write_jq_json_failed": "unable to write jq output as JSON", + "ai.agent_sdk.output.write_json_failed": "unable to write JSON output", + "ai.agent_sdk.profiles.table.id": "ID", + "ai.agent_sdk.profiles.table.name": "Name", + "ai.agent_sdk.profiles.unsynced": "Unsynced", + "ai.agent_sdk.provider.authenticate_open_url": "To authenticate {provider}, open this URL in your browser: {url}", + "ai.agent_sdk.provider.scope_required": "Provider '{provider}' must be setup for either a team or personal account", + "ai.agent_sdk.provider.status.not_connected": "❌ Not Connected", + "ai.agent_sdk.provider.table.allowed_for": "ALLOWED FOR", + "ai.agent_sdk.provider.table.name": "NAME", + "ai.agent_sdk.provider.table.slug": "SLUG", + "ai.agent_sdk.provider.table.status": "STATUS", + "ai.agent_sdk.provider.user_not_on_team": "User is not on a team", + "ai.agent_sdk.saved_prompt_source": "Saved prompt ({id})", + "ai.agent_sdk.schedule.deleted": "Schedule deleted", + "ai.agent_sdk.schedule.deleting_agent": "Deleting agent...", + "ai.agent_sdk.schedule.detail.agent_name": "Agent name: {name}", + "ai.agent_sdk.schedule.detail.cron_schedule": "Cron schedule: {schedule}", + "ai.agent_sdk.schedule.detail.environment_id": "Environment ID: {id}", + "ai.agent_sdk.schedule.detail.host": "Host: {host}", + "ai.agent_sdk.schedule.detail.last_error": "Last error: {error}", + "ai.agent_sdk.schedule.detail.last_ran": "Last ran: {last_ran}", + "ai.agent_sdk.schedule.detail.model_id": "Model ID: {id}", + "ai.agent_sdk.schedule.detail.name": "Name: {name}", + "ai.agent_sdk.schedule.detail.next_run": "Next run: {next_run}", + "ai.agent_sdk.schedule.detail.paused": "Paused: {paused}", + "ai.agent_sdk.schedule.detail.prompt": "Prompt: {prompt}", + "ai.agent_sdk.schedule.detail.skill": "Skill: {skill}", + "ai.agent_sdk.schedule.not_found": "Schedule not found", + "ai.agent_sdk.schedule.paused": "Schedule paused", + "ai.agent_sdk.schedule.pausing_agent": "Pausing agent...", + "ai.agent_sdk.schedule.resuming_agent": "Resuming agent...", + "ai.agent_sdk.schedule.scheduled_agent": "Scheduled agent: {id}", + "ai.agent_sdk.schedule.scheduling_agent": "Scheduling agent {name}...", + "ai.agent_sdk.schedule.table.agent_name": "Agent name", + "ai.agent_sdk.schedule.table.cron_schedule": "Cron schedule", + "ai.agent_sdk.schedule.table.environment_id": "Environment ID", + "ai.agent_sdk.schedule.table.host": "Host", + "ai.agent_sdk.schedule.table.id": "ID", + "ai.agent_sdk.schedule.table.last_error": "Last error", + "ai.agent_sdk.schedule.table.last_ran": "Last ran", + "ai.agent_sdk.schedule.table.model_id": "Model ID", + "ai.agent_sdk.schedule.table.name": "Name", + "ai.agent_sdk.schedule.table.next_run": "Next run", + "ai.agent_sdk.schedule.table.paused": "Paused", + "ai.agent_sdk.schedule.table.prompt": "Prompt", + "ai.agent_sdk.schedule.table.schedule": "Schedule", + "ai.agent_sdk.schedule.table.scope": "Scope", + "ai.agent_sdk.schedule.table.skill": "Skill", + "ai.agent_sdk.schedule.unpaused": "Schedule unpaused", + "ai.agent_sdk.schedule.updated": "Schedule updated", + "ai.agent_sdk.schedule.updating_agent": "Updating agent...", + "ai.agent_sdk.schedule.without_environment": "Scheduling agent to run without an environment.", + "ai.agent_sdk.secret.aws_access_key_id": "AWS Access Key ID:", + "ai.agent_sdk.secret.aws_region": "AWS Region:", + "ai.agent_sdk.secret.aws_secret_access_key": "AWS Secret Access Key:", + "ai.agent_sdk.secret.aws_session_token": "AWS Session Token (optional, press Enter to skip):", + "ai.agent_sdk.secret.bedrock_access_key_noninteractive_required": "Bedrock access key secrets require --access-key-id, --secret-access-key, and --region in non-interactive mode", + "ai.agent_sdk.secret.bedrock_access_key_update_not_supported": "Bedrock access key secrets cannot be updated via `--value`; re-create the secret instead", + "ai.agent_sdk.secret.bedrock_api_key": "Bedrock API key:", + "ai.agent_sdk.secret.bedrock_api_key_update_not_supported": "Bedrock API key secrets cannot be updated via `--value`; re-create the secret instead", + "ai.agent_sdk.secret.bedrock_noninteractive_required": "Bedrock secrets require --bedrock-api-key and --region in non-interactive mode", + "ai.agent_sdk.secret.created": "Secret '{name}' created", + "ai.agent_sdk.secret.delete_confirm": "Delete {scope} secret '{name}'?", + "ai.agent_sdk.secret.delete_confirm_help": "This action cannot be undone", + "ai.agent_sdk.secret.delete_refusing_noninteractive": "Refusing to delete secret without confirmation in non-interactive mode (use --force to bypass)", + "ai.agent_sdk.secret.deleted": "Secret '{name}' deleted", + "ai.agent_sdk.secret.deletion_cancelled": "Deletion cancelled", + "ai.agent_sdk.secret.feature_not_enabled": "This feature is not enabled", + "ai.agent_sdk.secret.name_required": "Secret name is required. Usage: oz secret create ", + "ai.agent_sdk.secret.not_found": "Secret '{name}' not found", + "ai.agent_sdk.secret.openai_base_url": "OpenAI base URL (optional, press Enter to skip):", + "ai.agent_sdk.secret.openai_base_url_help": "e.g. https://us.api.openai.com/v1 for a regional endpoint", + "ai.agent_sdk.secret.read_value_file_failed": "Failed to read secret value from: {path}", + "ai.agent_sdk.secret.scope.personal": "personal", + "ai.agent_sdk.secret.scope.team": "team", + "ai.agent_sdk.secret.secret_value": "Secret value:", + "ai.agent_sdk.secret.table.created": "Created", + "ai.agent_sdk.secret.table.name": "Name", + "ai.agent_sdk.secret.table.scope": "Scope", + "ai.agent_sdk.secret.table.type": "Type", + "ai.agent_sdk.secret.table.updated": "Updated", + "ai.agent_sdk.secret.type.anthropic_api_key": "Anthropic API Key", + "ai.agent_sdk.secret.type.anthropic_bedrock_access_key": "Anthropic Bedrock Access Key", + "ai.agent_sdk.secret.type.anthropic_bedrock_api_key": "Anthropic Bedrock API Key", + "ai.agent_sdk.secret.type.openai_api_key": "OpenAI API Key", + "ai.agent_sdk.secret.type.raw_value": "Raw Value", + "ai.agent_sdk.secret.updated": "Secret '{name}' updated", + "ai.agent_sdk.skill_resolution.ambiguous": "Skill '{skill}' is ambiguous; specify as repo:skill_name\n\nCandidates:\n", + "ai.agent_sdk.skill_resolution.clone_failed": "Failed to clone repository '{org}/{repo}': {message}", + "ai.agent_sdk.skill_resolution.org_mismatch": "Repository '{repo}' found but belongs to org '{found}', expected '{expected}'", + "ai.agent_sdk.skill_resolution.parse_failed": "Failed to parse skill file {path}: {message}", + "ai.agent_sdk.skill_resolution.repository_not_found": "Repository '{repo}' not found", + "ai.agent_sdk.skill_resolution.skill_not_found": "Skill '{skill}' not found", + "ai.agent_sdk.team_api_key_free_credits_warning": "Warning: Free cloud credits apply to personal runs only but this run uses a team API key. If you want to use free cloud credits, consider using a personal API key instead.", + "ai.agent_sdk.unable_to_resolve_path": "Unable to resolve {path}", + "ai.agent_sdk.unexpected_argument_found": "unexpected argument '{argument}' found", + "ai.agent_sdk.working_directory_unavailable": "Unable to determine working directory", + "ai.agent_shortcuts.file_paths_context": "for file paths and attaching other context", + "ai.agent_shortcuts.go_back_to_terminal": "go back to terminal", + "ai.agent_shortcuts.input_shell_command": "input shell command", + "ai.agent_shortcuts.open_code_review": "open code review", + "ai.agent_shortcuts.pause_agent": "pause agent", + "ai.agent_shortcuts.search_continue_conversations": "search and continue conversations", + "ai.agent_shortcuts.slash_commands": "for slash commands", + "ai.agent_shortcuts.start_new_conversation": "start a new conversation", + "ai.agent_shortcuts.toggle_auto_accept": "toggle auto-accept", + "ai.agent_shortcuts.toggle_conversation_list": "toggle conversation list", + "ai.agent_tips.action.open_palette": "Open palette", + "ai.agent_tips.action.show_diff_view": "Show diff view", + "ai.agent_tips.action.warp_drive": "Warp Drive.", + "ai.agent_tips.add_mcp": "`/add-mcp` to add an MCP server to your workspace.", + "ai.agent_tips.add_prompt": "`/add-prompt` to create a reusable prompt for repeatable workflows.", + "ai.agent_tips.add_rule": "`/add-rule` to create a global agent rule.", + "ai.agent_tips.agent_profiles": "Add agent profiles to customize permissions and models per session.", + "ai.agent_tips.at_add_context": "`@` to add context from files, blocks, or Warp Drive objects to your prompt.", + "ai.agent_tips.attach_prior_command_output": " to attach the prior command output as agent context.", + "ai.agent_tips.auto_approve_session": " to auto-approve the agent's commands and diffs for the rest of the session.", + "ai.agent_tips.cancel_agent_task": " to cancel the current agent task.", + "ai.agent_tips.compact_conversation": "`/compact` to summarize the current conversation and free up space in the context window.", + "ai.agent_tips.control_interactive_tools": "Prompt the agent to control interactive tools like node, python, postgres, gdb, or vim.", + "ai.agent_tips.create_environment": "`/create-environment` to turn a repo into a remote Docker environment an agent can run in.", + "ai.agent_tips.drag_image_context": "Drag an image into the pane to attach it as agent context.", + "ai.agent_tips.enable_desktop_notifications": "Enable desktop notifications to get an alert when an agent needs your attention.", + "ai.agent_tips.fork_conversation": "`/fork` to create a fresh copy of the current conversation, optionally with a new prompt.", + "ai.agent_tips.handoff_to_cloud": "Type `&` or use the handoff chip to move a local conversation to the cloud.", + "ai.agent_tips.init_generate_warp_md": "`/init` to generate a `WARP.md` file and define project rules for the agent.", + "ai.agent_tips.init_index_repo": "`/init` to index the repo so the agent can understand your codebase.", + "ai.agent_tips.new_conversation": "`/new` to start a new agent conversation with clean context.", + "ai.agent_tips.open_code_review_command": "`/open-code-review` to open the code review panel and inspect agent-generated diffs.", + "ai.agent_tips.open_code_review_panel": " to open the code review panel and review the agent's changes.", + "ai.agent_tips.open_command_palette": " to open the Command Palette and access Warp actions and shortcuts.", + "ai.agent_tips.open_mcp_servers": "`/open-mcp-servers` to view and share MCP servers with your team.", + "ai.agent_tips.oz_headless": "Use the `oz` command to run an Oz agent in headless mode, useful for remote machines.", + "ai.agent_tips.paste_url_context": "Paste a URL to attach that webpage as context for the agent.", + "ai.agent_tips.plan_prompt": "`/plan` to create a plan for the agent before executing.", + "ai.agent_tips.project_rules_files": "Use `AGENTS.md` or `CLAUDE.md` to apply project-scoped rules.", + "ai.agent_tips.redirect_running_agent": "Enter a new prompt to redirect the agent while it's running.", + "ai.agent_tips.right_click_copy_output": "Right-click a block to copy a conversation's output.", + "ai.agent_tips.right_click_fork": "Right-click a block to fork the conversation from that point.", + "ai.agent_tips.right_click_selected_text": "Right-click selected text to attach it as agent context.", + "ai.agent_tips.slash_command_menu": "`/` to open the slash-command menu and access quick agent actions.", + "ai.agent_tips.store_reusable_objects": "Store reusable workflows, notebooks, and prompts in your", + "ai.agent_tips.switch_agent_profiles": "Switch agent profiles to quickly change models and agent permissions.", + "ai.agent_tips.tip_prefix": "Tip: {description}", + "ai.agent_tips.toggle_natural_language_detection": " to toggle natural language detection and switch between agent and terminal input.", + "ai.agent_tips.usage": "`/usage` to show your current AI credits usage.", + "ai.agent_tips.voice_input": "Hold to speak your prompt directly to the agent.", + "ai.agent_tips.warpify_ssh": "Warpify a remote SSH session to enable Oz inside that environment.", + "ai.agent_view.continued": "Continued", + "ai.agent_view.deleted": "Deleted", + "ai.agent_view.deleted_conversation": "Deleted conversation", + "ai.agent_view.exit_confirmation.exit": "again to exit", + "ai.agent_view.exit_confirmation.start_new_conversation": "again to start new conversation", + "ai.agent_view.exit_confirmation.stop_and_exit": "again to stop and exit", + "ai.agent_view.open_in_different_pane": "Open in different pane", + "ai.agent_view.restored": "Restored", + "ai.agent_view.untitled_conversation": "Untitled conversation", + "ai.artifacts.download_prepare_failed": "Failed to prepare file download.", + "ai.artifacts.downloaded": "Downloaded {filename}.", + "ai.artifacts.failed_to_load": "Failed to load", + "ai.artifacts.file": "File", + "ai.artifacts.screenshots": "Screenshots", + "ai.artifacts.untitled_plan": "Untitled Plan", + "ai.ask_user_question.agent_questions": "Agent questions", + "ai.ask_user_question.allow_agent_to_ask_questions": "Allow the agent to ask questions:", + "ai.ask_user_question.answer_prefix": "A: {answer}", + "ai.ask_user_question.answered_all_questions": "Answered all {total} questions", + "ai.ask_user_question.answered_partial": "Answered {answered_count} of {total} questions", + "ai.ask_user_question.answered_question": "Answered question", + "ai.ask_user_question.other": "Other...", + "ai.ask_user_question.placeholder": "Type your answer and press Enter", + "ai.ask_user_question.question_prefix": "Q: {question}", + "ai.ask_user_question.questions_skipped": "Questions skipped", + "ai.ask_user_question.questions_skipped_auto_approve": "Questions skipped due to auto-approve", + "ai.ask_user_question.questions_unavailable": "Questions unavailable", + "ai.ask_user_question.select_all_suffix": " (select all that apply)", + "ai.ask_user_question.skip_all": "Skip all", + "ai.ask_user_question.skipped": "Skipped", + "ai.auth_secret.unable_to_load_secrets": "Unable to load secrets", + "ai.aws_bedrock.always_run_automatically": "Always run automatically", + "ai.aws_bedrock.credentials_expired_or_missing": "AWS credentials expired or missing", + "ai.aws_bedrock.refresh_credentials": "Refresh AWS Credentials", + "ai.block.always_allow": "Always allow", + "ai.block.dont_show_again": "Don’t show again", + "ai.block.mcp_tool": "MCP Tool: {name}", + "ai.block.mcp_tool_with_input": "MCP Tool: {name} ({input})", + "ai.block.navigate_to_repo_to_open_comments": "Navigate to {path} to open these comments", + "ai.block.open_all_in_code_review": "Open all in code review", + "ai.block.open_in_code_review": "Open in code review", + "ai.block.open_in_github": "Open in GitHub", + "ai.block.review_changes": "Review changes", + "ai.block.rewind": "Rewind", + "ai.block.rewind_tooltip": "Rewind to before this block", + "ai.block.thank_you_for_feedback": "Thank you for the feedback!", + "ai.block.view_screenshot": "View screenshot", + "ai.blocked.grep_or_file_glob": "OK if I search the files in this directory?", + "ai.blocked.reading_files": "Grant access to the following files?", + "ai.blocked.searching_codebase": "Grant access to the following repository?", + "ai.blocked.write_to_running_command": "Can I write the following to this running command?", + "ai.blocklist.local_agent_task_sync_model.agent_error": "Agent encountered an error", + "ai.blocklist.local_agent_task_sync_model.aws_bedrock_invalid": "AWS Bedrock credentials expired or invalid for {model_name}.", + "ai.blocklist.local_agent_task_sync_model.cancelled_by_user": "Cancelled by user", + "ai.blocklist.local_agent_task_sync_model.context_window_exceeded": "Context window exceeded: {message}", + "ai.blocklist.local_agent_task_sync_model.conversation_blocked": "The agent got stuck waiting for user confirmation on the action: {blocked_action}", + "ai.blocklist.local_agent_task_sync_model.internal_warp_error": "An internal error occurred during the conversation. Please try again.", + "ai.blocklist.local_agent_task_sync_model.invalid_api_key": "Invalid API key for {provider}. Update your API key in settings.", + "ai.blocklist.local_agent_task_sync_model.quota_limit": "Your team has run out of credits. Purchase more credits to continue.", + "ai.blocklist.local_agent_task_sync_model.server_overloaded": "Warp is temporarily overloaded. Please try again shortly.", + "ai.cli.agent_asking_take_control": "Agent is asking you to take control.", + "ai.code_block.add_as_context": "Add as Context", + "ai.code_block.run_in_terminal": "Run in terminal", + "ai.code_diff.accept_and_continue_with_agent": "Accept and continue with agent", + "ai.code_diff.edit_code_diff": "Edit Code Diff", + "ai.code_diff.failed_to_save_file": "Failed to save file {file_path}", + "ai.code_diff.file_name_deleted": "{file_name} (deleted)", + "ai.code_diff.file_name_new": "{file_name} (new)", + "ai.code_diff.file_renamed_without_changes": "File renamed without changes", + "ai.code_diff.hide_suggested_code_banners": "Don't show me suggested code banners again", + "ai.code_diff.iterate_with_agent": "Iterate with agent", + "ai.code_diff.manage_suggested_code_banner_settings": "Manage suggested code banner settings", + "ai.code_diff.no_file_name": "No file name", + "ai.code_diff.open_config": "Open config", + "ai.code_diff.requested_edit": "Requested Edit", + "ai.code_diff.revert_failed": "Failed to revert changes to {file_name}", + "ai.codebase_index.allow_automatic_indexing": "Allow automatic indexing", + "ai.codebase_index.index_codebase": "Index codebase", + "ai.codebase_index.indexing_header": "Indexing codebase", + "ai.codebase_index.speedbump_header": "Index Codebase?", + "ai.codebase_index.speedbump_text": "Indexing helps agents quickly understand context and provide targeted solutions. Code is never stored on the server.", + "ai.codebase_index.view_status": "View status", + "ai.command.accept": "Accept", + "ai.command.auto_approve": "Auto-approve", + "ai.command.manage_agent_permissions": "Manage Agent permissions", + "ai.command.take_control": "Take control", + "ai.command.take_over": "Take over", + "ai.common.agent_waiting_for_instructions": "Agent waiting for instructions...", + "ai.common.attempting_resume_conversation": "Attempting to resume conversation...", + "ai.common.auto_approve_actions_for_task": "Auto-approve all agent actions for this task", + "ai.common.auto_queue_next_prompt_tooltip": "Auto-queue next prompt while agent is responding", + "ai.common.auto_queue_on_tooltip": "Auto-queue is on: your next prompt will be queued", + "ai.common.aws_credentials_expired_or_missing": "AWS credentials expired or missing for {model_name}. Please refresh your AWS credentials.", + "ai.common.copy_debug_id": "Copy debug ID", + "ai.common.credit_limit_reset": "You've reached your credit limit. Your credit limit resets on {date}.", + "ai.common.debug_information": "Debug information: {debug_info}", + "ai.common.elapsed_second_one": "1 second", + "ai.common.elapsed_second_other": "{seconds} seconds", + "ai.common.error_apology": "I'm sorry, I couldn't complete that request.", + "ai.common.exit_agent_input_tooltip": "Exit agent input", + "ai.common.fast_forward_cloud_always_enabled": "Fast forward is always enabled for cloud agent conversations", + "ai.common.hide_agent_responses": "Hide agent responses", + "ai.common.hide_responses": "Hide responses", + "ai.common.internal_warp_error": "Internal Warp error.", + "ai.common.mermaid_diagram": "Mermaid diagram", + "ai.common.resume_when_network_restored": "Will resume conversation when network connectivity is restored...", + "ai.common.server_overloaded": "Warp is currently overloaded. Please try again later.", + "ai.common.show_agent_responses": "Show agent responses", + "ai.common.show_responses": "Show responses", + "ai.common.stop_agent_task_tooltip": "Stop agent task", + "ai.common.take_over_command_tooltip": "Take over control of the command", + "ai.common.turn_off_auto_approve_actions": "Turn off auto-approve all agent actions", + "ai.conversation_status.blocked": "Blocked", + "ai.conversation_status.cancelled": "Cancelled", + "ai.conversation_status.claimed": "Claimed", + "ai.conversation_status.done": "Done", + "ai.conversation_status.error": "Error", + "ai.conversation_status.failed": "Failed", + "ai.conversation_status.in_progress": "In progress", + "ai.conversation_status.pending": "Pending", + "ai.conversation_status.queued": "Queued", + "ai.conversation.artifacts": "Artifacts", + "ai.conversation.cloud_agent_run": "Cloud agent run", + "ai.conversation.continue_locally": "Continue locally", + "ai.conversation.continue_locally_tooltip": "Fork this conversation locally", + "ai.conversation.conversation_id": "Conversation ID", + "ai.conversation.created_by": "Created by {name} • {time}", + "ai.conversation.created_on": "Created on", + "ai.conversation.credits_used": "Credits used", + "ai.conversation.directory": "Directory", + "ai.conversation.environment_details": "Environment details", + "ai.conversation.environment_name": "Name: {name}", + "ai.conversation.environment_setup_commands": "Environment setup commands", + "ai.conversation.follow_up_existing_tooltip": "Follow up with existing conversation", + "ai.conversation.id": "ID", + "ai.conversation.image": "Image", + "ai.conversation.initial_query": "Initial query", + "ai.conversation.navigate_failed": "Couldn't navigate to conversation.", + "ai.conversation.open_in_github": "Open in GitHub", + "ai.conversation.open_in_oz": "Open in Oz", + "ai.conversation.run_id": "Run ID", + "ai.conversation.run_metadata_access_denied_description": "You can view this shared session, but run metadata is only visible to users with access to this run.", + "ai.conversation.run_metadata_access_denied_title": "Run metadata is not available", + "ai.conversation.run_time": "Run time", + "ai.conversation.view_in_oz": "View in Oz", + "ai.conversation.view_in_oz_tooltip": "View this run in the Oz web app", + "ai.execution_profiles.editor.ask_user_question_permission.label": "Ask questions", + "ai.execution_profiles.editor.base_model.desc": "This model serves as the primary engine behind the agent. It powers most interactions and invokes other models for tasks like planning or code generation when necessary. Warp may automatically switch to alternate models based on model availability or for auxiliary tasks such as conversation summarization.", + "ai.execution_profiles.editor.base_model.label": "Base model", + "ai.execution_profiles.editor.call_mcp_servers_permission.label": "Call MCP servers", + "ai.execution_profiles.editor.command_allowlist_placeholder": "e.g. ls .*", + "ai.execution_profiles.editor.command_allowlist.desc": "Regular expressions to match commands that can be automatically executed by Oz.", + "ai.execution_profiles.editor.command_allowlist.label": "Command allowlist", + "ai.execution_profiles.editor.command_denylist_placeholder": "e.g. rm .*", + "ai.execution_profiles.editor.command_denylist.desc": "Regular expressions to match commands that Oz should always ask permission to execute.", + "ai.execution_profiles.editor.command_denylist.label": "Command denylist", + "ai.execution_profiles.editor.computer_use_model.desc": "The model used when the agent takes control of your computer to interact with graphical applications through mouse movements, clicks, and keyboard input.", + "ai.execution_profiles.editor.computer_use_model.label": "Computer use model", + "ai.execution_profiles.editor.context_window.desc": "The base model's working memory - how many tokens of your conversation, code, and documents it can consider at once. Larger windows enable longer conversations and more coherent responses over bigger codebases, at the cost of higher latency and compute usage.", + "ai.execution_profiles.editor.context_window.label": "Context window", + "ai.execution_profiles.editor.default_profile": "Default", + "ai.execution_profiles.editor.default_profile_name_locked": "Default profile name cannot be changed.", + "ai.execution_profiles.editor.delete_profile": "Delete profile", + "ai.execution_profiles.editor.directory_allowlist_placeholder": "e.g. ~/code-repos/repo", + "ai.execution_profiles.editor.directory_allowlist.desc": "Give the agent file access to certain directories.", + "ai.execution_profiles.editor.directory_allowlist.label": "Directory allowlist", + "ai.execution_profiles.editor.edit_profile": "Edit Profile", + "ai.execution_profiles.editor.full_terminal_use_model.desc": "The model used when the agent operates inside interactive terminal applications like database shells, debuggers, REPLs, or dev servers - reading live output and writing commands to the PTY.", + "ai.execution_profiles.editor.full_terminal_use_model.label": "Full terminal use model", + "ai.execution_profiles.editor.header": "Profile Editor", + "ai.execution_profiles.editor.mcp_allowlist.desc": "MCP servers that are allowed to be called by Oz.", + "ai.execution_profiles.editor.mcp_allowlist.label": "MCP allowlist", + "ai.execution_profiles.editor.mcp_denylist.desc": "MCP servers that are not allowed to be called by Oz.", + "ai.execution_profiles.editor.mcp_denylist.label": "MCP denylist", + "ai.execution_profiles.editor.mcp_server_fallback": "MCP Server", + "ai.execution_profiles.editor.models_section": "MODELS", + "ai.execution_profiles.editor.name_label": "Name", + "ai.execution_profiles.editor.permission_desc.agent_decides": "The Agent chooses the safest path: acting on its own when confident, and asking for approval when uncertain.", + "ai.execution_profiles.editor.permission_desc.always_allow": "Give the Agent full autonomy - no manual approval ever required.", + "ai.execution_profiles.editor.permission_desc.always_ask": "Require explicit approval before the Agent takes any action.", + "ai.execution_profiles.editor.permission_desc.ask_user_question.always_ask": "The Agent may ask a question and will pause for your response even when auto-approve is on.", + "ai.execution_profiles.editor.permission_desc.ask_user_question.ask_unless_auto_approve": "The Agent may ask a question and pause for your response, but will continue automatically when auto-approve is on.", + "ai.execution_profiles.editor.permission_desc.ask_user_question.never": "The Agent will not ask questions and will continue with its best judgment.", + "ai.execution_profiles.editor.permission_desc.computer_use.always_allow": "Give the Agent full autonomy to use computer use tools without approval.", + "ai.execution_profiles.editor.permission_desc.computer_use.always_ask": "Require explicit approval before the Agent uses computer use tools.", + "ai.execution_profiles.editor.permission_desc.computer_use.never": "Computer use tools are disabled and will not be available to the Agent.", + "ai.execution_profiles.editor.permission_desc.run_agents.always_allow": "Give the Agent full autonomy to run child agents without approval.", + "ai.execution_profiles.editor.permission_desc.run_agents.always_ask": "Require explicit approval before the Agent runs child agents.", + "ai.execution_profiles.editor.permission_desc.run_agents.never": "The Agent cannot run child agents and the run_agents tool will not be available.", + "ai.execution_profiles.editor.permission_desc.unknown": "Unknown setting.", + "ai.execution_profiles.editor.permission_desc.write_to_pty.always_ask": "The agent will always ask for permission to interact with a running command.", + "ai.execution_profiles.editor.permission_desc.write_to_pty.ask_on_first_write": "The agent will ask for permission the first time it needs to interact with a running command. After that, it will continue automatically for the rest of that command.", + "ai.execution_profiles.editor.permission.agent_decides": "Agent decides", + "ai.execution_profiles.editor.permission.always_allow": "Always allow", + "ai.execution_profiles.editor.permission.always_ask": "Always ask", + "ai.execution_profiles.editor.permission.apply_code_diffs": "Apply code diffs", + "ai.execution_profiles.editor.permission.ask_on_first_write": "Ask on first write", + "ai.execution_profiles.editor.permission.ask_questions": "Ask questions", + "ai.execution_profiles.editor.permission.ask_unless_auto_approve": "Ask unless auto-approve", + "ai.execution_profiles.editor.permission.call_mcp_servers": "Call MCP servers", + "ai.execution_profiles.editor.permission.computer_use": "Computer use", + "ai.execution_profiles.editor.permission.execute_commands": "Execute commands", + "ai.execution_profiles.editor.permission.interact_with_running_commands": "Interact with running commands", + "ai.execution_profiles.editor.permission.never": "Never", + "ai.execution_profiles.editor.permission.never_ask": "Never ask", + "ai.execution_profiles.editor.permission.read_files": "Read files", + "ai.execution_profiles.editor.permission.run_orchestrated_agents": "Run orchestrated agents", + "ai.execution_profiles.editor.permissions_section": "PERMISSIONS", + "ai.execution_profiles.editor.plan_auto_sync.desc": "The plans this agent creates will be automatically added and synced to Warp Drive.", + "ai.execution_profiles.editor.plan_auto_sync.label": "Plan auto-sync", + "ai.execution_profiles.editor.profile_name_placeholder": "e.g. \"YOLO code\"", + "ai.execution_profiles.editor.select_mcp_servers": "Select MCP servers", + "ai.execution_profiles.editor.upgrade_footer.prefix": "Frontier models are unavailable on free plans. ", + "ai.execution_profiles.editor.upgrade_footer.upgrade": "Upgrade", + "ai.execution_profiles.editor.web_search.desc": "The agent may use web search when helpful for completing tasks.", + "ai.execution_profiles.editor.web_search.label": "Call web tools", + "ai.execution_profiles.editor.workspace_override_tooltip": "This option is enforced by your organization's settings and cannot be customized.", + "ai.host_picker.custom_host": "Custom host…", + "ai.inline_agent.agent_blocked": "Agent needs your permission to continue", + "ai.inline_agent.agent_in_control": "Agent is in control", + "ai.inline_agent.prompt_interact_with_command": "Prompt agent to interact with `{command}`", + "ai.inline_agent.prompt_interact_with_running_command": "Prompt agent to interact with the running command", + "ai.inline_agent.user_in_control": "User is in control", + "ai.inline_agent.waiting_for_command_exit": "Agent is waiting for command to exit", + "ai.inline_agent.waiting_on_instructions": "Agent is waiting on instructions", + "ai.llms.custom_endpoint": "Custom endpoint", + "ai.loading.adjusting_tasks": "Adjusting tasks...", + "ai.loading.calling_mcp_tool": "Calling \"{name}\" MCP tool...", + "ai.loading.creating_diff": "Creating diff...", + "ai.loading.executing_command": "Executing command...", + "ai.loading.fetching_pr_comments": "Fetching PR comments...", + "ai.loading.finding_files": "Finding files...", + "ai.loading.generating_fix": "Generating fix...", + "ai.loading.generating_plan": "Generating plan...", + "ai.loading.grepping": "Grepping...", + "ai.loading.next_check_in": "Next check in {time}", + "ai.loading.preparing_question": "Preparing question...", + "ai.loading.reading_files": "Reading files...", + "ai.loading.reading_mcp_resource": "Reading \"{name}\" MCP resource...", + "ai.loading.searching_codebase": "Searching codebase...", + "ai.loading.searching_web": "Searching the web...", + "ai.loading.searching_web_for": "Searching the web for \"{query}\"", + "ai.loading.summarizing_command_output": "Summarizing command output...", + "ai.loading.summarizing_conversation": "Summarizing conversation...", + "ai.loading.updating_plan": "Updating plan...", + "ai.loading.waiting_for_command_exit": "Waiting for command to exit...", + "ai.loading.warping": "Warping...", + "ai.loading.writing_command_input": "Writing command input...", + "ai.mcp.path_required_to_launch_server": "PATH required to launch MCP server. Please open a new terminal session to autopopulate PATH.", + "ai.mcp.templatable_manager.auth_success_toast": "Successfully authenticated {server_name} MCP server", + "ai.mcp.templatable_manager.error.auth_not_supported": "Server requires authentication, which is not yet supported.", + "ai.mcp.templatable_manager.error.cancelled": "Operation was cancelled with reason: {reason}", + "ai.mcp.templatable_manager.error.client_initialize": "Failed to initialize client: {error}", + "ai.mcp.templatable_manager.error.generic": "Error: {error}", + "ai.mcp.templatable_manager.error.installation_not_found": "Installation not found", + "ai.mcp.templatable_manager.error.parse_server_failed": "Failed to parse MCP server: {error}", + "ai.mcp.templatable_manager.error.path_not_available": "PATH not available", + "ai.mcp.templatable_manager.error.runtime": "Runtime error: {error}", + "ai.mcp.templatable_manager.error.server_initialize": "Failed to initialize server: {error}", + "ai.mcp.templatable_manager.error.server_returned_error": "Server returned an error. Please check server logs for details.", + "ai.mcp.templatable_manager.error.service": "Service error: {error}", + "ai.mcp.templatable_manager.error.template_contains_no_servers": "Template contains no servers", + "ai.mcp.templatable_manager.error.timeout": "Connection timed out after {seconds} seconds. The server may be unresponsive.", + "ai.mcp.templatable_manager.error.transport_closed": "Connection closed unexpectedly. The server may have crashed.", + "ai.mcp.templatable_manager.error.transport_creation": "Failed to establish connection: {error}", + "ai.mcp.templatable_manager.error.transport_send": "Failed to send data to server. Connection may have been lost.", + "ai.mcp.templatable_manager.error.unexpected_response": "Server sent an unexpected response. The server may be incompatible.", + "ai.mcp.templatable_manager.error.unexpected_status_code": "Unexpected status code: {status}", + "ai.mcp.templatable_manager.error.unknown_reason": "Unknown reason", + "ai.orchestration_config.base_model_helper": "The primary model all agents will use.", + "ai.orchestration_config.description": "Break this work into coordinated streams with multiple agents.", + "ai.orchestration_config.header": "Use orchestration", + "ai.orchestration.agent": "Agent", + "ai.orchestration.agent_cancelled_suffix": " cancelled.", + "ai.orchestration.agent_harness": "Agent harness", + "ai.orchestration.agent_location": "Agent location", + "ai.orchestration.api_key": "API key", + "ai.orchestration.back_to_parent_conversation": "Back to parent conversation", + "ai.orchestration.base_model": "Base model", + "ai.orchestration.cloud": "Cloud", + "ai.orchestration.default_model": "Default model", + "ai.orchestration.delete_agent": "Delete agent", + "ai.orchestration.disabled_by_admin": "Disabled by your administrator", + "ai.orchestration.empty_environment": "Empty environment", + "ai.orchestration.environment": "Environment", + "ai.orchestration.failed_to_start_agent": "Failed to start agent ", + "ai.orchestration.failed_to_start_remote_agent": "Failed to start remote agent ", + "ai.orchestration.focus_pane": "Focus pane", + "ai.orchestration.generating_title": "Generating title...", + "ai.orchestration.host": "Host", + "ai.orchestration.kill_agent": "Kill agent", + "ai.orchestration.local": "Local", + "ai.orchestration.new_api_key": "New API key…", + "ai.orchestration.open_in_new_pane": "Open in new pane", + "ai.orchestration.open_in_new_tab": "Open in new tab", + "ai.orchestration.orchestrator": "Orchestrator", + "ai.orchestration.parent_conversation": "Parent conversation", + "ai.orchestration.recommend_create_environment": "We recommend creating an environment for cloud agents.", + "ai.orchestration.recommend_select_environment": "We recommend selecting an environment for cloud agents.", + "ai.orchestration.select_api_key_for_harness": "Select an API key for this harness to continue.", + "ai.orchestration.send_message_cancelled": "Send message to {recipients} cancelled.", + "ai.orchestration.send_message_failed": "Failed to send message to {recipients}: {error}", + "ai.orchestration.sending_message_to": "Sending message to ", + "ai.orchestration.skip_advanced": "Skip (advanced)", + "ai.orchestration.start_agent": "Start agent ", + "ai.orchestration.start_remote_agent": "Start remote agent ", + "ai.orchestration.started_agent": "Started agent ", + "ai.orchestration.started_locally_suffix": " locally.", + "ai.orchestration.started_remotely_suffix": " remotely.", + "ai.orchestration.starting_agent": "Starting agent ", + "ai.orchestration.starting_remote_agent": "Starting remote agent ", + "ai.orchestration.stop_agent": "Stop agent", + "ai.orchestration.unknown_agent": "Unknown agent", + "ai.orchestration.view_in_oz": "View in Oz", + "ai.output.always_allow_file_access_for_coding_tasks": "Always allow file access for coding tasks", + "ai.output.always_allow_file_access_for_this_repo": "Always allow file access for this repo", + "ai.output.always_allow_oz_read_only_commands": "Always allow Oz to execute read-only commands (relies on model)", + "ai.output.artifact_description": "Description: {description}", + "ai.output.artifact_status_upload_failed": "Status: upload failed: {error}", + "ai.output.artifact_status_uploaded": "Status: uploaded artifact {artifact_uid}", + "ai.output.artifact_uploaded_file": "Uploaded file: {filepath}", + "ai.output.bad_response": "Bad response", + "ai.output.cancelled_file_search_patterns_in_path": "Cancelled search for files that match the following patterns in {path}", + "ai.output.cancelled_grep_patterns_in_path": "Cancelled grep for the following patterns in {path}", + "ai.output.check_now": " · Check now", + "ai.output.check_now_tooltip": "Ask the agent to check this command now, skipping its timer.", + "ai.output.comment_addressed": "Comment addressed: \"{content}\"", + "ai.output.continue_conversation": "Continue conversation", + "ai.output.continue_current_conversation": "Continue current conversation", + "ai.output.continuing_current_conversation": "Continuing current conversation", + "ai.output.conversation_search_grepping_patterns": "Grepping for patterns", + "ai.output.conversation_search_grepping_patterns_with_patterns": "Grepping for patterns: {patterns}", + "ai.output.conversation_search_listing_messages": "Listing messages", + "ai.output.conversation_search_reading_messages": "Reading {count} messages", + "ai.output.conversation_summarized": "Conversation summarized", + "ai.output.could_not_apply_changes_to_file": "Could not apply changes to file.", + "ai.output.current_directory": "the current directory", + "ai.output.debug_output": "Debug output", + "ai.output.failed_to_read_files": "Failed to read files", + "ai.output.find_file_patterns_in_path": "Find files that match the following patterns in {path}", + "ai.output.finding_file_patterns_in_path": "Finding files that match the following patterns in {path}", + "ai.output.finding_files_matching": "Finding files that match ", + "ai.output.fork_conversation": "Fork conversation", + "ai.output.good_response": "Good response", + "ai.output.grant_access_upload_artifact": "Grant access to upload this artifact?", + "ai.output.grep_for": "Grep for ", + "ai.output.grep_patterns_in_path": "Grep for the following patterns in {path}", + "ai.output.grepping_for": "Grepping for ", + "ai.output.grepping_patterns_in_path": "Grepping for the following patterns in {path}", + "ai.output.in_path": " in {path}", + "ai.output.in_path_cancelled": " in {path} cancelled", + "ai.output.manage_ai_autonomy_permissions": "Manage AI Autonomy permissions", + "ai.output.new_conversation_prompt": "It seems like the topic changed. Would you like to make a new conversation?", + "ai.output.new_conversation_started": "New conversation started", + "ai.output.new_conversation_suggestion_cancelled": "New conversation suggestion cancelled", + "ai.output.no_relevant_files_found": "No relevant files found.", + "ai.output.ok_read_mcp_resource": "OK if I read this MCP resource?", + "ai.output.ok_use_computer_control": "OK if I use computer control for this task?", + "ai.output.open_skill": "Open skill", + "ai.output.references": "References", + "ai.output.refunded_credits": "Sorry you had a bad experience with this interaction. We've refunded you {count} credits. We appreciate your feedback!", + "ai.output.refunded_one_credit": "Sorry you had a bad experience with this interaction. We've refunded you 1 credit. We appreciate your feedback!", + "ai.output.response_wont_count_usage": "This response won't count towards your usage.", + "ai.output.resume_conversation": "Resume conversation", + "ai.output.search_for_files_matching": "Search for files that match ", + "ai.output.search_in_path": "Search in {path}", + "ai.output.search_in_path_cancelled": "Search in {path} cancelled", + "ai.output.search_in_path_failed": "Search in {path} failed", + "ai.output.search_in_path_failed_not_indexed": "Search in {path} failed because the codebase isn't indexed", + "ai.output.searching_in_path": "Searching in {path}", + "ai.output.show_credit_usage_details": "Show credit usage details", + "ai.output.start_new_conversation": "Start a new conversation", + "ai.output.stopped_task": "Stopped task", + "ai.output.stopped_task_indexed": "Stopped task {current}/{total}: \"{title}\"", + "ai.output.stopped_task_named": "Stopped task: \"{title}\"", + "ai.output.suggestion_edited_in_another_tab": "This suggestion is being edited in another tab.", + "ai.output.suggestions": "Suggestions:", + "ai.output.thinking": "Thinking", + "ai.output.this_conversation": "this conversation", + "ai.output.thought_for": "Thought for {duration}", + "ai.output.upload_artifact": "Upload artifact: {file_path}", + "ai.pending_prompt.queued": "Queued", + "ai.pending_prompt.remove": "Remove queued prompt", + "ai.pending_prompt.send_now": "Send now", + "ai.prompt_alert.add_credits": "Add credits", + "ai.prompt_alert.anonymous_limit_hard_gate_primary": "At Limit -", + "ai.prompt_alert.ask_admin_enable_overages": ", ask a team admin to enable overages", + "ai.prompt_alert.ask_admin_increase_overages": ", ask a team admin to increase overages", + "ai.prompt_alert.compare_plans": "Compare plans", + "ai.prompt_alert.contact_support": "Contact support", + "ai.prompt_alert.contact_team_admin": ", contact a team admin", + "ai.prompt_alert.delinquent_primary": "Restricted due to payment issue", + "ai.prompt_alert.enable_analytics": "enable analytics", + "ai.prompt_alert.enable_premium_overages": "Enable premium overages", + "ai.prompt_alert.increase_monthly_spend_limit": "Increase monthly spend limit", + "ai.prompt_alert.manage_billing": "Manage billing", + "ai.prompt_alert.no_connection": "No internet connection", + "ai.prompt_alert.out_of_credits": "Out of credits", + "ai.prompt_alert.sign_up_for_more_credits": "Sign up for more AI credits", + "ai.prompt_alert.telemetry_disabled_primary": "To use AI features,", + "ai.prompt_alert.upgrade": "Upgrade", + "ai.prompt_alert.upgrade_to_build": "Upgrade to Build", + "ai.prompt_alert.use_own_api_keys": "use your own API keys", + "ai.requested_command.agent_error_take_over": "Agent ran into an issue. Take over control.", + "ai.requested_command.agent_monitoring_command": "Agent is monitoring command...", + "ai.requested_command.agent_needs_input": "Agent needs your input to continue", + "ai.requested_command.always_ask_permission": "Your profile is set to always ask for permission to execute commands.", + "ai.requested_command.copied_from": "Copied from", + "ai.requested_command.derived_from": "Derived from", + "ai.requested_command.edit_requested_command": "Edit requested command", + "ai.requested_command.error_formatting_json": "Error formatting JSON", + "ai.requested_command.error_prefix": "Error: {error}", + "ai.requested_command.generating_command": "Generating command...", + "ai.requested_command.manage_command_execution_setting": "Manage command execution setting", + "ai.requested_command.ok_call_mcp_tool": "OK if I call this MCP tool?", + "ai.requested_command.ok_run_command": "OK if I run this command and read the output?", + "ai.requested_command.paused_agent_user_in_control": "Paused agent. User is in control.", + "ai.requested_command.response_with_result": "{command}\n\nResponse: {result}", + "ai.requested_command.tool_call_cancelled": "Tool call was cancelled", + "ai.requested_command.user_in_control": "User is in control.", + "ai.requested_command.user_in_control_short": "User in control", + "ai.requested_command.viewing_command_detail": "Viewing command detail", + "ai.requested_command.viewing_mcp_tool_detail": "Viewing MCP tool call detail", + "ai.rules.add_rule": "Add rule", + "ai.rules.add_rule_title": "Add Rule", + "ai.rules.delete_rule": "Delete rule", + "ai.rules.description": "Rules enhance the agent by providing structured guidelines that help maintain consistency, enforce best practices, and adapt to specific workflows, including codebases or broader tasks.", + "ai.rules.description_placeholder": "e.g. Never use unwrap in Rust", + "ai.rules.disabled_banner_link": "turn it back on", + "ai.rules.disabled_banner_prefix": "Your rules are disabled and won’t be used as context in sessions. You can ", + "ai.rules.disabled_banner_suffix": " anytime.", + "ai.rules.edit_rule": "Edit rule", + "ai.rules.edit_rule_title": "Edit Rule", + "ai.rules.editing_disabled_offline": "Editing is disabled while offline.", + "ai.rules.header": "Rules", + "ai.rules.initialize_project": "Initialize Project", + "ai.rules.manage": "Manage rules", + "ai.rules.name_placeholder": "e.g. Rust rules", + "ai.rules.offline_banner": "You are offline. Some rules will be read only.", + "ai.rules.rule": "Rule", + "ai.rules.scope.global": "Global", + "ai.rules.scope.project_based": "Project based", + "ai.rules.search_placeholder": "Search rules", + "ai.rules.suggested_rule": "Suggested rule", + "ai.rules.zero_state.global": "Add a rule above, or drop one at ~/.agents/AGENTS.md to apply it across every project.", + "ai.rules.zero_state.project_based": "Once you generate a WARP.md rules file for a project, it will appear here.", + "ai.run_agents_card.title": "Can I start additional agents for this task?", + "ai.run_agents.accept_without_orchestration": "Accept w/o orchestration", + "ai.run_agents.agents_count": "Agents ({count})", + "ai.run_agents.cancelled": "Spawn agents cancelled", + "ai.run_agents.configuring": "Configuring agents...", + "ai.run_agents.failed_to_start_orchestration": "Failed to start orchestration", + "ai.run_agents.failed_to_start_orchestration_with_error": "Failed to start orchestration: {error}", + "ai.run_agents.orchestration_disabled": "Orchestration is currently disabled. Re-enable on the plan card to launch.", + "ai.run_agents.orchestration_disabled_reason": "Orchestration is currently disabled. Re-enable on the plan card to launch. ({reason})", + "ai.run_agents.spawned_one": "Spawned 1 agent", + "ai.run_agents.spawned_other": "Spawned {count} agents", + "ai.run_agents.spawned_partial": "Spawned {launched} of {total} agents", + "ai.run_agents.spawning_one": "Spawning 1 agent...", + "ai.run_agents.spawning_other": "Spawning {count} agents...", + "ai.search_codebase.cancelled": "Search for \"{query}\" cancelled", + "ai.search_codebase.cancelled_in_repo": "Search for \"{query}\" in {repo} cancelled", + "ai.search_codebase.searched_codebase_for": "Searched codebase for \"{query}\"", + "ai.search_codebase.searched_codebase_for_in_repo": "Searched codebase for \"{query}\" in {repo}", + "ai.search_codebase.searched_for": "Searched for \"{query}\"", + "ai.search_codebase.searched_for_in_repo": "Searched for \"{query}\" in {repo}", + "ai.search_codebase.searching_for": "Searching codebase for \"{query}\"", + "ai.search_codebase.searching_for_in_repo": "Searching for \"{query}\" in {repo}", + "ai.search_results.urls": "URLs", + "ai.settings.edit_api_keys": "Edit API Keys", + "ai.settings.invalid_api_key": "Provided API key is not valid", + "ai.status.authenticate_github": "Authenticate GitHub", + "ai.status.cloud_agent_run_cancelled": "Cloud agent run cancelled", + "ai.status.missing_github_authentication": "Missing GitHub authentication.", + "ai.status.primary_model_failed": "The primary model failed. Retrying with the fallback model.", + "ai.status.primary_model_failed_with_name": "The primary model ({model}) failed. Retrying with the fallback model.", + "ai.status.setting_up_environment": "Setting up environment", + "ai.status.warping_with": "Warping with {model}.", + "ai.status.warping_with_another_model": "Warping with another model.", + "ai.suggested_workflow.prompt": "Prompt", + "ai.suggestion.add_rule": "Add rule: {rule}", + "ai.suggestion.suggested_prompt": "Suggested prompt:\n{prompt}", + "ai.summarization.cancel": "Cancel summarization", + "ai.summarization.cancel_dialog.body": "Summarization is already running. If you cancel now, the request may still incur cost, any progress so far will be lost, and restarting will take longer.\n\nAre you sure you want to cancel?", + "ai.summarization.cancel_dialog.title": "Cancel summarization?", + "ai.summarization.continue": "Continue summarization", + "ai.telemetry.description": "We may collect certain console interactions to improve Warp's AI capabilities. You can opt out any time.", + "ai.telemetry.help_improve_warp": "Help improve Warp.", + "ai.telemetry.manage_privacy_settings": "Manage privacy settings", + "ai.telemetry.policy_updated": "We've updated our telemetry policy.", + "ai.todos.completed": "Completed {title}", + "ai.todos.completed_indexed": "Completed {title} ({current}/{total})", + "ai.todos.completed_item_separator": ", {title}", + "ai.todos.completed_item_separator_indexed": ", {title} ({current}/{total})", + "ai.todos.outdated": "Outdated", + "ai.todos.tasks": "Tasks", + "ai.usage.call_one": "{count} call", + "ai.usage.call_other": "{count} calls", + "ai.usage.command_one": "{count} command", + "ai.usage.command_other": "{count} commands", + "ai.usage.commands_executed": "Commands executed", + "ai.usage.context_window_used": "Context window used", + "ai.usage.credit_one": "{count} credit", + "ai.usage.credit_other": "{count} credits", + "ai.usage.credits_spent": "Credits spent", + "ai.usage.credits_spent_last_response": "Credits spent (last response)", + "ai.usage.credits_spent_total": "Credits spent (total)", + "ai.usage.diffs_applied": "Diffs applied", + "ai.usage.file_one": "{count} file", + "ai.usage.file_other": "{count} files", + "ai.usage.files_changed": "Files changed", + "ai.usage.full_terminal_model_tooltip": "You can change which model is used for full terminal use in the AI settings page", + "ai.usage.hide_details": "Hide details", + "ai.usage.last_response_time": "LAST RESPONSE TIME", + "ai.usage.models": "Models", + "ai.usage.models_with_category": "Models ({category})", + "ai.usage.seconds": "{seconds} seconds", + "ai.usage.show_more": "Show {count} more", + "ai.usage.summary": "USAGE SUMMARY", + "ai.usage.time_to_first_token": "Time to first token", + "ai.usage.tool_call_summary": "TOOL CALL SUMMARY", + "ai.usage.tool_calls": "Tool calls", + "ai.usage.total_agent_response_time": "Total agent response time", + "ai.usage.total_time_including_tool_calls": "Total time (including tool calls)", + "ai.usage.view_details": "View details", + "ai.web_fetch.fetched_all": "Fetched {count} web pages", + "ai.web_fetch.fetched_partial": "Fetched {successful} of {total} web pages", + "ai.web_fetch.fetching": "Fetching {count} web pages...", + "ai.web_fetch.no_urls_fetched": "No URLs fetched", + "ai.web_search.no_urls_found": "No URLs found", + "ai.web_search.searched": "Searched the web", + "ai.web_search.searched_for": "Searched the web for \"{query}\"", + "ai.web_search.searching": "Searching the web", + "ai.web_search.searching_for": "Searching the web for \"{query}\"", + "ai.zero_state.cloud_agent.title": "New Oz cloud agent conversation", + "ai.zero_state.cloud_mode.description": "Run your agent task in an isolated cloud environment.", + "ai.zero_state.cloud_mode.docs_prefix": "Use cloud agents to run parallel agents, build agents that run autonomously, and check in on your agents from anywhere. ", + "ai.zero_state.local_agent.title": "New Oz agent conversation", + "ai.zero_state.local_mode.description": "Send a prompt below to start a new conversation", + "ai.zero_state.local_mode.description_with_location": "Send a prompt below to start a new conversation in `{location}`", + "ai.zero_state.oz_updates_section_header": "What's new in Oz", + "ai.zero_state.recent_activity": "RECENT ACTIVITY", + "ai.zero_state.view_changelog": "View changelog", + "app_menu.action.activate_next_pane": "Activate Next Pane", + "app_menu.action.activate_previous_pane": "Activate Previous Pane", + "app_menu.action.add_cursor_above": "Add Cursor Above", + "app_menu.action.add_cursor_below": "Add Cursor Below", + "app_menu.action.add_next_occurrence": "Add Next Occurrence", + "app_menu.action.ai_search": "AI Search", + "app_menu.action.attach_selection_as_agent_context": "Attach Selection as Agent Context", + "app_menu.action.clear_blocks": "Clear Blocks", + "app_menu.action.clear_editor": "Clear Editor", + "app_menu.action.close_current_session": "Close Current Session", + "app_menu.action.close_other_tabs": "Close Other Tabs", + "app_menu.action.close_tab": "Close Tab", + "app_menu.action.close_tabs_right": "Close Tabs to the Right", + "app_menu.action.close_window": "Close Window", + "app_menu.action.command_palette": "Command Palette", + "app_menu.action.command_search": "Command Search", + "app_menu.action.configure_keybindings": "Configure Keybindings", + "app_menu.action.copy": "Copy", + "app_menu.action.copy_block": "Copy Block", + "app_menu.action.copy_block_command": "Copy Block Command", + "app_menu.action.copy_block_output": "Copy Block Output", + "app_menu.action.create_block_permalink": "Create Block Permalink", + "app_menu.action.cut": "Cut", + "app_menu.action.cycle_next_session": "Next Session", + "app_menu.action.cycle_previous_session": "Previous Session", + "app_menu.action.decrease_font_size": "Decrease Font Size", + "app_menu.action.decrease_zoom": "Decrease Zoom", + "app_menu.action.disable_sync_terminal_inputs": "Disable Input Synchronization", + "app_menu.action.files_palette": "Files Palette", + "app_menu.action.find": "Find", + "app_menu.action.find_within_block": "Find Within Block", + "app_menu.action.focus_input": "Focus Input", + "app_menu.action.go_to_line": "Go to Line", + "app_menu.action.history": "Command History", + "app_menu.action.increase_font_size": "Increase Font Size", + "app_menu.action.increase_zoom": "Increase Zoom", + "app_menu.action.launch_config_palette": "Launch Configurations Palette", + "app_menu.action.move_tab_left": "Move Tab Left", + "app_menu.action.move_tab_right": "Move Tab Right", + "app_menu.action.navigation_palette": "Navigation Palette", + "app_menu.action.new_agent_mode_pane": "New Agent Pane", + "app_menu.action.new_file": "New File", + "app_menu.action.new_personal_ai_prompt": "New Personal AI Prompt", + "app_menu.action.new_personal_env_vars": "New Personal Environment Variables", + "app_menu.action.new_personal_notebook": "New Personal Notebook", + "app_menu.action.new_personal_workflow": "New Personal Workflow", + "app_menu.action.new_team_ai_prompt": "New Team AI Prompt", + "app_menu.action.new_team_env_vars": "New Team Environment Variables", + "app_menu.action.new_team_notebook": "New Team Notebook", + "app_menu.action.new_team_workflow": "New Team Workflow", + "app_menu.action.open_ai_fact_collection": "Open AI Rules", + "app_menu.action.open_mcp_server_collection": "Open MCP Servers", + "app_menu.action.open_repository": "Open Repository", + "app_menu.action.open_team_settings": "Team Settings", + "app_menu.action.paste": "Paste", + "app_menu.action.redo": "Redo", + "app_menu.action.refer_a_friend": "Refer a Friend", + "app_menu.action.rename_tab": "Rename Tab", + "app_menu.action.reset_font_size": "Reset Font Size", + "app_menu.action.reset_zoom": "Reset Zoom", + "app_menu.action.save_current_config": "Save Current Configuration", + "app_menu.action.scroll_to_bottom_of_selected_blocks": "Scroll to Bottom of Selected Blocks", + "app_menu.action.scroll_to_top_of_selected_blocks": "Scroll to Top of Selected Blocks", + "app_menu.action.search_drive": "Search Drive", + "app_menu.action.select_all": "Select All", + "app_menu.action.select_all_blocks": "Select All Blocks", + "app_menu.action.select_block_above": "Select Block Above", + "app_menu.action.select_block_below": "Select Block Below", + "app_menu.action.share_current_session": "Share Current Session", + "app_menu.action.share_pane_contents": "Share Pane Contents", + "app_menu.action.show_about_warp": "About Warp", + "app_menu.action.show_appearance": "Appearance", + "app_menu.action.show_settings": "Settings", + "app_menu.action.split_pane_down": "Split Pane Down", + "app_menu.action.split_pane_left": "Split Pane Left", + "app_menu.action.split_pane_right": "Split Pane Right", + "app_menu.action.split_pane_up": "Split Pane Up", + "app_menu.action.toggle_bookmark_block": "Bookmark Block", + "app_menu.action.toggle_conversation_list_view": "Agent Conversations", + "app_menu.action.toggle_global_search": "Global Search", + "app_menu.action.toggle_keybindings_page": "Keyboard Shortcuts", + "app_menu.action.toggle_maximize_pane": "Toggle Maximize Pane", + "app_menu.action.toggle_project_explorer": "Project Explorer", + "app_menu.action.toggle_resource_center": "Resource Center", + "app_menu.action.toggle_sync_all_terminal_inputs": "Synchronize Inputs in All Tabs", + "app_menu.action.toggle_sync_current_tab_terminal_inputs": "Synchronize Inputs in Current Tab", + "app_menu.action.toggle_warp_drive": "Warp Drive", + "app_menu.action.undo": "Undo", + "app_menu.action.view_changelog": "View Changelog", + "app_menu.action.view_shared_blocks": "View Shared Blocks", + "app_menu.action.workflows": "Workflows", + "app_menu.app.debug": "Debug", + "app_menu.app.log_out": "Log out", + "app_menu.app.preferences": "Preferences", + "app_menu.app.privacy_policy": "Privacy Policy...", + "app_menu.app.set_default_terminal": "Set Warp as Default Terminal", + "app_menu.blocks.hide_in_band_command_blocks": "Hide In-band Command Blocks", + "app_menu.blocks.hide_initialization_block": "Hide Initialization Block", + "app_menu.blocks.hide_ssh_command_blocks": "Hide Warpified SSH Blocks", + "app_menu.blocks.show_in_band_command_blocks": "Show In-band Command Blocks", + "app_menu.blocks.show_initialization_block": "Show Initialization Block", + "app_menu.blocks.show_ssh_command_blocks": "Show Warpified SSH Blocks", + "app_menu.debug.create_anonymous_user": "Create anonymous user", + "app_menu.debug.disable_in_band_generators": "Disable in-band generators for new sessions", + "app_menu.debug.disable_pty_recording": "Disable PTY Recording Mode (warp.pty.recording)", + "app_menu.debug.disable_shell_debug_mode": "Disable Shell Debug Mode (-x) for New Sessions", + "app_menu.debug.enable_in_band_generators": "Enable In-band Generators for New Sessions", + "app_menu.debug.enable_pty_recording": "Enable PTY Recording Mode (warp.pty.recording)", + "app_menu.debug.enable_shell_debug_mode": "Enable Shell Debug Mode (-x) for New Sessions", + "app_menu.debug.export_default_settings_csv": "Export Default Settings as CSV to home dir", + "app_menu.debug.manually_toggle_network_status": "Manually Toggle Network Status", + "app_menu.dock.new_window": "New Window", + "app_menu.edit.copy_on_select": "Copy on Select within the Terminal", + "app_menu.edit.synchronize_inputs": "Synchronize Inputs", + "app_menu.edit.use_warp_prompt": "Use Warp's Prompt", + "app_menu.file.launch_configurations": "Launch Configurations", + "app_menu.file.new_agent_tab": "New Agent Tab", + "app_menu.file.new_terminal_tab": "New Terminal Tab", + "app_menu.file.new_window": "New Window", + "app_menu.file.open_recent": "Open Recent", + "app_menu.file.reopen_closed_session": "Reopen closed session", + "app_menu.help.github_issues": "GitHub Issues...", + "app_menu.help.send_feedback": "Send Feedback...", + "app_menu.help.slack_community": "Warp Slack Community...", + "app_menu.help.warp_documentation": "Warp Documentation...", + "app_menu.launch_config.save_new": "Save New...", + "app_menu.top.ai": "AI", + "app_menu.top.blocks": "Blocks", + "app_menu.top.drive": "Drive", + "app_menu.top.edit": "Edit", + "app_menu.top.file": "File", + "app_menu.top.help": "Help", + "app_menu.top.tab": "Tab", + "app_menu.top.view": "View", + "app_menu.top.window": "Window", + "app_menu.view.compact_mode": "Compact Mode", + "app_menu.view.toggle_focus_reporting": "Toggle Focus Reporting", + "app_menu.view.toggle_mouse_reporting": "Toggle Mouse Reporting", + "app_menu.view.toggle_scroll_reporting": "Toggle Scroll Reporting", + "auth.a11y_open_browser": "Press enter to open your browser to Sign Up or Sign In.", + "auth.adjust_your": "you can adjust your ", + "auth.already_have_account": "Already have an account? ", + "auth.and_open": " and open", + "auth.auth_token": "Auth Token", + "auth.browser_auth_token": "Browser auth token", + "auth.browser_not_launched_prefix": "If your browser hasn’t launched, ", + "auth.browser_sign_in_title": "Sign in on your browser to continue", + "auth.browser_sign_in_title_multiline": "Sign in on your browser \nto continue", + "auth.connect_account_ai": "Connect your account to enable AI-powered planning, coding, and automation.", + "auth.connect_account_drive": "Connect your account to save and share notebooks, workflows, and more across devices.", + "auth.copy_url": "copy the URL", + "auth.disable_ai_body": "Warp is better with AI. By continuing, you won’t have access to any of the following features:", + "auth.disable_ai_confirm": "Are you sure you want to disable AI features?", + "auth.disable_ai_features": "Disable AI features", + "auth.disable_warp_drive": "Disable Warp Drive", + "auth.disable_warp_drive_body": "Warp Drive lets you save workflows and knowledge across devices and share them with your team. By continuing, you won’t have access to the following features:", + "auth.disable_warp_drive_confirm": "Are you sure you want to disable Warp Drive?", + "auth.enable_ai_features": "Enable AI features", + "auth.enable_warp_drive": "Enable Warp Drive", + "auth.enter_auth_token": "Enter auth token", + "auth.errors.create_anonymous_user_failed": "Encountered an error trying to create anonymous users", + "auth.errors.redirect_url_missing_refresh_token": "Received URL without refresh token query param: {url}", + "auth.errors.redirect_url_unexpected_host": "Received URL with unexpected host: {url}", + "auth.errors.unknown_anonymous_user_type": "could not convert unknown anonymous user type", + "auth.errors.web_user_handoff_failed": "Web user handoff failed: {error}", + "auth.get_started_ai": "Get started with AI", + "auth.get_started_warp_drive": "Get started with Warp Drive", + "auth.link_sso": "Link SSO", + "auth.login_failure.copy_token_manually": "Failed to log in. Try manually copying the auth token from the authentication web page and pasting it into the modal.", + "auth.login_failure.invalid_auth_token": "An invalid auth token was entered into the modal.", + "auth.login_failure.invalid_redirect_url": "The redirect URL pasted did not originate from this app. Please click the button below to try again.", + "auth.login_failure.login_failed": "Request to log in failed.", + "auth.login_failure.signup_failed": "Request to sign up failed.", + "auth.login_failure.troubleshooting_docs": "troubleshooting docs", + "auth.login_failure.troubleshooting_prefix": " Not the first time? See our ", + "auth.logout.confirm": "Yes, log out", + "auth.logout.running_process": "You have {count} process running.", + "auth.logout.running_processes": "You have {count} processes running.", + "auth.logout.shared_session": "You have {count} shared session.", + "auth.logout.shared_sessions": "You have {count} shared sessions.", + "auth.logout.show_running_processes": "Show running processes", + "auth.logout.title": "Log out?", + "auth.logout.unsaved_file": "You have {count} unsaved file. Logging out will cause you to lose the file.", + "auth.logout.unsaved_files": "You have {count} unsaved files. Logging out will cause you to lose the files.", + "auth.logout.unsynced_object": "You have {count} unsynced Warp Drive object. Logging out will cause you to lose the object.", + "auth.logout.unsynced_objects": "You have {count} unsynced Warp Drive objects. Logging out will cause you to lose the objects.", + "auth.no_sign_in_now": "Don’t want to sign in right now? ", + "auth.offline_first_time": "You are currently offline. An internet connection is required to use Warp for the first time.", + "auth.offline_info.paragraph_1": "All of Warp’s non-cloud features work offline.", + "auth.offline_info.paragraph_2": "However, we require users to be online when using Warp for the first time in order to enable Warp’s AI and cloud features.", + "auth.offline_info.paragraph_3": "We offer cloud features to all users, and so we need an internet connection to meter AI usage, prevent abuse, and associate cloud objects with users. If you opt to use Warp logged-out, a unique ID will be attached to an anonymous user account in order to support these features.", + "auth.offline_info.title": "Using Warp Offline", + "auth.open_page_manually": "and open the page manually.", + "auth.opt_out_analytics_ai": "If you’d like to opt out of analytics and AI features,", + "auth.override.accessibility_description": "Warp has detected a new login from a web browser. Press escape to cancel and continue using Warp without login.", + "auth.override.confirm_header": "Delete personal Warp Drive objects and preferences?", + "auth.override.confirm_warning": "This cannot be undone.", + "auth.override.description": "It looks like you logged into a Warp account through a web browser. If you continue, any personal Warp Drive objects and preferences from this anonymous session will be permanently deleted.", + "auth.override.export_data": "Export your data", + "auth.override.export_suffix": " to import later.", + "auth.override.initial_header": "New login detected", + "auth.page_manually": "the page manually.", + "auth.paste_token_link": "Click here to paste your token from the browser", + "auth.paste_token_modal.subtitle": "Paste your auth token from the browser to complete login.", + "auth.paste_token_modal.title": "Paste your auth token below", + "auth.privacy_disclaimer_ai_prefix": "If you’d like to opt out of analytics and AI features, you can adjust your ", + "auth.privacy_disclaimer_prefix": "If you’d like to opt out of analytics, you can adjust your ", + "auth.privacy_settings": "Privacy Settings", + "auth.privacy.cloud_conversation_off_description": "Agent conversations are only stored locally on your machine, are lost upon logout, and cannot be shared. Note: conversation data for ambient agents are still stored in the cloud.", + "auth.privacy.cloud_conversation_on_description": "Agent conversations can be shared with others and are retained when you log in on different devices. This data is only stored for product functionality, and Warp will not use it for analytics.", + "auth.privacy.crash_reporting_description": "Crash reporting helps Warp’s engineering team understand stability and improve performance.", + "auth.privacy.help_improve_warp": "Help improve Warp", + "auth.privacy.send_crash_reports": "Send crash reports", + "auth.privacy.store_ai_conversations": "Store AI conversations in the cloud", + "auth.privacy.telemetry_description": "High-level feature usage data helps Warp’s product team prioritize the roadmap.", + "auth.require_login_ai": "In order to use Warp’s AI features or collaborate with others, please create an account.", + "auth.require_login_drive_limit": "In order to create more objects in Warp Drive, please create an account.", + "auth.require_login_share": "In order to share, please create an account.", + "auth.sign_in": "Sign in", + "auth.sign_up": "Sign up", + "auth.sign_up_for_warp": "Sign up for Warp", + "auth.skip_for_now": "Skip for now", + "auth.skip_login_confirm": "Are you sure you want to skip login?", + "auth.skip_login_details_1": "You can sign up later, but some features, such as AI,", + "auth.skip_login_details_2": "are only available to logged-in users. ", + "auth.sso_enabled_header": "Your organization has enabled SSO for your account", + "auth.sso_link_detail": "Click the button below to link your Warp account to your SSO provider.", + "auth.terms_of_service": "Terms of Service", + "auth.terms_prefix": "By continuing, you agree to Warp’s ", + "auth.web_handoff.failed": "Error authenticating - please refresh the page", + "auth.web_handoff.loading": "Loading...", + "auth.welcome_to_warp": "Welcome to Warp!", + "auth.yes_skip_login": "Yes, skip login", + "autoupdate.accessibility.install_relaunch_help": "Use the command palette to install and relaunch Warp", + "autoupdate.accessibility.no_updates_available": "No updates available", + "autoupdate.accessibility.update_available": "Update available.", + "autoupdate.linux.compatible_tool_suffix": " or a compatible tool, the pre-filled command will update Warp for you.", + "autoupdate.linux.ensure_repo_function_suffix": " function ensures the Warp package repository is enabled, as we've detected you recently upgraded your distribution.", + "autoupdate.linux.install_and_relaunch_suffix": " to install the update and re-launch Warp. ", + "autoupdate.linux.installed_using_prefix": "If you installed Warp using ", + "autoupdate.linux.one_time_repo_setup": "\nThe command below includes a one-time configuration of the Warp package repository and PGP signing key.", + "autoupdate.linux.press_enter": "press enter", + "autoupdate.linux.report_issues": "Please report any issues", + "autoupdate.linux.review_command_then": "\nReview the command below, then ", + "autoupdate.linux.run_package_manager_to_update": "Run {package_manager} to update", + "autoupdate.linux.the_prefix": "\nThe ", + "billing.shared_objects.compare_plans": "Compare plans", + "billing.shared_objects.default_delinquent_admin_enterprise": "Shared drive objects have been restricted due to a subscription payment issue.\n\nPlease contact support@warp.dev to restore access.", + "billing.shared_objects.default_delinquent_admin_stripe": "Shared drive objects have been restricted due to a subscription payment issue.\n\nPlease update your payment information to restore access.", + "billing.shared_objects.default_delinquent_non_admin": "Shared drive objects have been restricted due to a subscription payment issue.\n\nPlease contact a team admin to restore access.", + "billing.shared_objects.default_free_admin": "Warp's free plan comes with a limited number of shared drive objects.\n\nFor access to unlimited shared drive objects, upgrade to a paid plan.", + "billing.shared_objects.default_free_non_admin": "Warp's free plan comes with a limited number of shared drive objects.\n\nFor access to unlimited shared drive objects, contact a team admin to upgrade to a paid plan.", + "billing.shared_objects.default_limit_reached_header": "Shared object limit reached", + "billing.shared_objects.default_prosumer_admin": "Warp's Pro plan comes with a limited number of shared drive objects.\n\nFor access to unlimited shared drive objects, upgrade to the Turbo plan.", + "billing.shared_objects.default_prosumer_non_admin": "Warp's Pro plan comes with a limited number of shared drive objects.\n\nFor access to unlimited shared drive objects, contact a team admin to upgrade to the Turbo plan.", + "billing.shared_objects.delinquent_admin_enterprise": "Shared {object_type}s have been restricted due to a subscription payment issue.\n\nPlease contact support@warp.dev to restore access.", + "billing.shared_objects.delinquent_admin_stripe": "Shared {object_type}s have been restricted due to a subscription payment issue.\n\nPlease update your payment information to restore access.", + "billing.shared_objects.delinquent_non_admin": "Shared {object_type}s have been restricted due to a subscription payment issue.\n\nPlease contact a team admin to restore access.", + "billing.shared_objects.free_admin": "Warp's free plan comes with a limited number of shared {object_type}s.\n\nFor access to unlimited shared {object_type}s, upgrade to a paid plan.", + "billing.shared_objects.free_non_admin": "Warp's free plan comes with a limited number of shared {object_type}s.\n\nFor access to unlimited shared {object_type}s, contact a team admin to upgrade to a paid plan.", + "billing.shared_objects.limit_reached_title": "Shared {object_type}s limit reached", + "billing.shared_objects.manage_billing": "Manage billing", + "billing.shared_objects.prosumer_admin": "Warp's Pro plan comes with a limited number of shared {object_type}s.\n\nFor access to unlimited shared {object_type}s, upgrade to the Build plan.", + "billing.shared_objects.prosumer_non_admin": "Warp's Pro plan comes with a limited number of shared {object_type}s.\n\nFor access to unlimited shared {object_type}s, contact a team admin to upgrade to the Build plan.", + "billing.shared_objects.restricted_title": "Shared {object_type}s restricted", + "chip_configurator.left_side": "Left side", + "chip_configurator.restore_default": "Restore default", + "chip_configurator.right_side": "Right side", + "chip_configurator.unknown": "Unknown", + "cloud_object.action_history.last_day": "{count} {action} in the last day", + "cloud_object.action_history.last_month": "{count} {action} in the last month", + "cloud_object.action_history.last_week": "{count} {action} in the last week", + "cloud_object.action_history.last_year": "{count} {action} in the last year", + "cloud_object.action.run": "run", + "cloud_object.action.runs": "runs", + "cloud_object.grab_edit_access.description": "If you take edit controls, the current editor will be forced into view mode", + "cloud_object.grab_edit_access.edit_anyway": "Edit anyway", + "cloud_object.grab_edit_access.title": "This notebook is currently being edited", + "cloud_object.history.edited": "Edited {time_ago}", + "cloud_object.history.edited_by": "{name} edited {time_ago}", + "cloud_object.history.last_edited_by": "Last edited by {name}", + "cloud_object.permadeletion.days": "{days_left} days until permanent deletion", + "cloud_object.permadeletion.one_day": "1 day until permanent deletion", + "cloud_object.space.personal": "Personal", + "cloud_object.space.shared_with_me": "Shared with me", + "cloud_object.space.team": "Team", + "cloud_object.toast.deleted_forever": "{count_objects_message} deleted forever", + "cloud_object.toast.empty_trash_failed": "Failed to empty trash", + "cloud_object.toast.env_vars_conflict": "Environment variables could not be saved because changes were made while you were editing.", + "cloud_object.toast.failed_create": "Failed to create {object_name_lowercase}", + "cloud_object.toast.failed_delete": "Failed to delete {object_name_lowercase}", + "cloud_object.toast.failed_leave": "Failed to leave {object_name}", + "cloud_object.toast.failed_move": "Failed to move {object_name_lowercase}", + "cloud_object.toast.failed_restore": "Failed to restore {object_name_lowercase}", + "cloud_object.toast.failed_start_editing": "Failed to start editing {object_name_lowercase}", + "cloud_object.toast.failed_trash": "Failed to trash {object_name_lowercase}", + "cloud_object.toast.failed_update": "Failed to update {object_name_lowercase}", + "cloud_object.toast.left": "Left {object_name}", + "cloud_object.toast.moved_to": "{object_name} moved to {containing_object_name}", + "cloud_object.toast.no_objects_to_empty": "No objects in trash to empty", + "cloud_object.toast.object_count_many": "{count} objects", + "cloud_object.toast.object_count_one": "1 object", + "cloud_object.toast.permissions_update_failed": "Failed to update permissions for {object_name_lowercase}", + "cloud_object.toast.permissions_updated": "Successfully updated permissions for {object_name_lowercase}", + "cloud_object.toast.restored": "{object_name} restored", + "cloud_object.toast.rule_conflict": "Rule could not be saved because changes were made while you were editing.", + "cloud_object.toast.saved_to": "{object_name} saved to {containing_object_name}", + "cloud_object.toast.trash_emptied": "Trash emptied: {count_objects_message} deleted forever", + "cloud_object.toast.trashed": "{object_name} trashed", + "cloud_object.toast.updated": "{object_name} updated", + "cloud_object.toast.workflow_conflict": "This workflow could not be saved because changes were made while you were editing.", + "code.footer.use_oz_to_update_config": "Use Oz to update this config", + "code_review.add_context.input_unavailable": "Cannot attach diff while input is not available", + "code_review.add_diff_set_context": "Add diff set as context", + "code_review.add_file_diff_context": "Add file diff as context", + "code_review.binding.save_all_unsaved_files": "Save all unsaved files in code review", + "code_review.binding.show_find_bar": "Show find bar in code review", + "code_review.binding.toggle_file_navigation": "Toggle file navigation in code review", + "code_review.cannot_attach_context_terminal_running": "Cannot attach context when terminal is running", + "code_review.comments.add": "Add comment", + "code_review.comments.ai_must_be_enabled": "AI must be enabled to send comments to Agent", + "code_review.comments.all_terminals_busy": "All terminals are busy", + "code_review.comments.button.outdated_plural": "{count} Outdated Comments", + "code_review.comments.button.outdated_singular": "{count} Outdated Comment", + "code_review.comments.button.plural": "{count} Comments", + "code_review.comments.button.singular": "{count} Comment", + "code_review.comments.cli_agent": "CLI agent", + "code_review.comments.copy_text": "Copy text", + "code_review.comments.edit": "Edit", + "code_review.comments.file_level_cannot_edit": "File-level comments currently can't be edited.", + "code_review.comments.from_github": "From GitHub", + "code_review.comments.no_sendable_comments": "No non-outdated comments to send", + "code_review.comments.outdated_cannot_edit": "Outdated comments can't be edited.", + "code_review.comments.outdated_chip": "Outdated", + "code_review.comments.outdated_count": "{count} outdated", + "code_review.comments.outdated_omitted.plural": "{count} comments will be omitted because they are outdated.", + "code_review.comments.outdated_omitted.singular": "1 comment will be omitted because it is outdated.", + "code_review.comments.remove": "Remove", + "code_review.comments.requires_ai_credits": "Agent code review requires AI credits", + "code_review.comments.review_comment": "Review Comment", + "code_review.comments.send_to_agent.button": "Send to Agent", + "code_review.comments.send_to_agent.tooltip": "Send diff comments to Agent", + "code_review.comments.send_to_destination": "Send diff comments to {label}", + "code_review.comments.sent_to_agent": "Comments sent to agent", + "code_review.comments.show_saved": "Show saved comment", + "code_review.comments.submit_error": "Could not submit comments to the agent", + "code_review.comments.view_in_github": "View in GitHub", + "code_review.context.diffset_against": "diffset against {branch}", + "code_review.context.uncommitted_changes": "uncommitted changes", + "code_review.copy_file_path": "Copy file path", + "code_review.diff_menu.search_placeholder": "Search diff sets or branches to compare...", + "code_review.diff_removed": "Diff removed", + "code_review.diff_target.uncommitted_changes": "Uncommitted changes", + "code_review.discard.all": "Discard all", + "code_review.discard.confirm_button": "Discard changes", + "code_review.discard.description.all_changes": "You're about to discard all committed and uncommitted changes.", + "code_review.discard.description.all_uncommitted": "You're about to discard all local changes that haven't been committed.", + "code_review.discard.description.file_changes_branch": "This will reset this file to the {branch} branch version and discard all committed and uncommitted edits.", + "code_review.discard.description.file_changes_main": "This will restore this file to the main branch version and discard all committed and uncommitted edits.", + "code_review.discard.description.file_uncommitted": "This will restore this file to the last committed version and discard local edits.", + "code_review.discard.disabled_git_operation": "Cannot discard changes while a git operation (merge, rebase, etc.) is in progress", + "code_review.discard.no_changes": "No changes to discard", + "code_review.discard.no_file_selected": "No file selected", + "code_review.discard.no_files_to_discard": "No files to discard", + "code_review.discard.stash_changes": "Stash changes", + "code_review.discard.title.all_changes": "Discard all changes?", + "code_review.discard.title.all_uncommitted": "Discard uncommitted changes?", + "code_review.discard.title.file_changes": "Discard all changes to file?", + "code_review.discard.title.file_uncommitted": "Discard all uncommitted changes to file?", + "code_review.error_loading_diffs": "Error loading diffs", + "code_review.file_content.binary": "Binary file - no diff available", + "code_review.file_content.diff_too_large": "Diff is too large to render", + "code_review.file_content.new_empty_file": "New empty file", + "code_review.file_content.renamed_without_changes": "File renamed without changes", + "code_review.file_content.unable_to_load": "Unable to load file content", + "code_review.file_nav.hide": "Hide file navigation", + "code_review.file_nav.show": "Show file navigation", + "code_review.git.branch": "Branch", + "code_review.git.changes": "Changes", + "code_review.git.commit": "Commit", + "code_review.git.commit_and_create_pr": "Commit and create PR", + "code_review.git.commit_and_publish": "Commit and publish", + "code_review.git.commit_and_push": "Commit and push", + "code_review.git.commit_and_push.success": "Changes committed and pushed.", + "code_review.git.commit_message.enter_tooltip": "Enter a commit message", + "code_review.git.commit_message.generating_placeholder": "Generating commit message...", + "code_review.git.commit_message.label": "Commit message", + "code_review.git.commit_message.placeholder": "Type a commit message", + "code_review.git.commit.loading": "Committing...", + "code_review.git.commit.success": "Changes successfully committed.", + "code_review.git.confirm": "Confirm", + "code_review.git.create_pr": "Create PR", + "code_review.git.create_pr.loading": "Creating...", + "code_review.git.create_pr.success": "PR successfully created.", + "code_review.git.default_branch": "default branch", + "code_review.git.dialog.title.commit": "Commit your changes", + "code_review.git.dialog.title.create_pr": "Create pull request", + "code_review.git.dialog.title.publish": "Publish branch", + "code_review.git.dialog.title.push": "Push changes", + "code_review.git.error.auth_failed": "Authentication failed. Check your Git credentials.", + "code_review.git.error.generic": "Git operation failed.", + "code_review.git.error.gh_not_authenticated": "GitHub CLI not authenticated. Run `gh auth login`.", + "code_review.git.error.gh_not_installed": "GitHub CLI (gh) not installed. See https://cli.github.com/.", + "code_review.git.error.identity_not_configured": "Git identity not configured. Set user.name and user.email.", + "code_review.git.error.network": "Network error. Check your connection.", + "code_review.git.error.no_changes_to_commit": "No changes to commit.", + "code_review.git.error.no_remote": "No remote configured for this branch.", + "code_review.git.error.remote_has_new_changes": "Remote has new changes - pull before pushing.", + "code_review.git.error.remote_repo_not_found": "Remote repository not found.", + "code_review.git.files.plural": "{count} files", + "code_review.git.files.singular": "{count} file", + "code_review.git.include_unstaged": "Include unstaged", + "code_review.git.included_commits": "Included commits", + "code_review.git.loading": "Loading...", + "code_review.git.no_actions_available": "No git actions available", + "code_review.git.no_changes_to_commit": "No changes to commit", + "code_review.git.open_pr": "Open PR", + "code_review.git.publish": "Publish", + "code_review.git.publish.loading": "Publishing...", + "code_review.git.publish.success": "Branch successfully published.", + "code_review.git.push": "Push", + "code_review.git.push.loading": "Pushing...", + "code_review.git.push.success": "Changes successfully pushed.", + "code_review.git.refreshing_pr_info": "Refreshing PR info", + "code_review.header.reviewing_changes": "Reviewing code changes", + "code_review.header.reviewing_open_changes": "Reviewing open changes", + "code_review.init_codebase": "Initialize codebase", + "code_review.init_codebase.tooltip": "Enables codebase indexing and WARP.md", + "code_review.loading_open_changes": "Loading open changes...", + "code_review.maximize": "Maximize", + "code_review.no_changes.description": "As you or the Agent make changes, you'll be able to track them here.", + "code_review.no_changes.repo_initialized": "Repo is initialized with a {file_name} file.", + "code_review.no_changes.title": "No open changes", + "code_review.no_repo.disabled": "Diffs only work for git repositories.", + "code_review.no_repo.remote": "Diffs only work for local workspaces.", + "code_review.no_repo.title": "Cannot detect diffs for this folder", + "code_review.no_repo.wsl": "Diffs don't currently work in WSL.", + "code_review.open_file": "Open file", + "code_review.open_repository": "Open repository", + "code_review.open_repository.tooltip": "Navigate to a repo and initialize it for coding", + "code_review.remote_diff.empty_data": "Server reported loaded state but no diff data was available", + "code_review.restore": "Restore", + "code_review.retry": "Retry", + "code_review.undo": "Undo", + "code_review.unsaved_file.tooltip": "This file has unsaved changes. {shortcut} to save", + "code_review.view_changes": "View changes", + "code.accept_and_save": "Accept and save", + "code.close_saved": "Close saved", + "code.editor.add_as_context": "Add as context", + "code.editor.comment.comment": "Comment", + "code.editor.comment.imported_from_github": "Comment imported from GitHub", + "code.editor.discard_this_version": "Discard this version", + "code.editor.find_references": "Find references", + "code.editor.find.a11y.find_bar": "Find bar for searching text in the editor.", + "code.editor.find.a11y.find_bar_with_matches": "Find bar with {count} matches found. Currently on match {current} of {count}.", + "code.editor.find.a11y.find_focused": "Find field focused. Type to search text. Use Enter and Shift-Enter or up/down arrows to navigate between matches. Press Escape to close find bar.", + "code.editor.find.a11y.navigate_help": "Use enter and shift-enter to navigate between matches. Escape to quit.", + "code.editor.find.a11y.no_results": "No results.", + "code.editor.find.a11y.replace_focused": "Replace field focused. Type replacement text, press Enter to replace current match, Tab to return to find field. Use up/down arrows to navigate matches, Escape to close.", + "code.editor.find.a11y.replace_more_help": "Continue pressing Enter to replace more matches, or use up/down arrows to navigate.", + "code.editor.find.a11y.replaced_last_match": "Successfully replaced the last match.", + "code.editor.find.a11y.replaced_match": "Successfully replaced match. Selected match is {current} of {total}", + "code.editor.find.a11y.result": "Result {current} of {total}.", + "code.editor.find.case_sensitive_tooltip": "Case sensitive search", + "code.editor.find.placeholder": "Find", + "code.editor.find.preserve_case_tooltip": "Preserve case", + "code.editor.find.regex_tooltip": "Regex toggle", + "code.editor.find.replace_all": "Replace all", + "code.editor.find.replace_placeholder": "Replace", + "code.editor.find.select_all": "Select all", + "code.editor.go_to_definition": "Go to definition", + "code.editor.goto_line.enter_line": "Please enter a line number", + "code.editor.goto_line.placeholder": "Line number:Column", + "code.editor.goto_line.title": "Go to line", + "code.editor.goto_line.valid_column": "Please enter a valid column number", + "code.editor.goto_line.valid_line": "Please enter a valid line number", + "code.editor.gutter.add_comment_on_line": "Add comment on line", + "code.editor.gutter.add_diff_hunk_as_context": "Add diff hunk as context", + "code.editor.gutter.revert_diff_hunk": "Revert diff hunk", + "code.editor.gutter.save_changes_add_comment": "Save changes to add comment", + "code.editor.gutter.save_changes_attach_context": "Save changes to attach as context.", + "code.editor.gutter.save_changes_revert": "Save changes to revert", + "code.editor.gutter.show_saved_comment": "Show saved comment", + "code.editor.nav.hunk": "Hunk:", + "code.editor.nav.reject": "Reject", + "code.editor.overwrite": "Overwrite", + "code.editor.remote_host_disconnected": "Remote host disconnected. You will not be able to see updates and save changes.", + "code.editor.saved_changes_not_reflected": "This file has saved changes that are not reflected here.", + "code.file_tree.attach_as_context": "Attach as context", + "code.file_tree.cd_to_directory": "cd to directory", + "code.file_tree.disabled_unavailable": "The Project Explorer requires access to your local workspace. Open a new session or navigate to an active session to view.", + "code.file_tree.file": "File", + "code.file_tree.folder": "Folder", + "code.file_tree.new_file": "New file", + "code.file_tree.open_in_new_pane": "Open in new pane", + "code.file_tree.open_in_new_tab": "Open in new tab", + "code.file_tree.project_explorer_unavailable": "Project explorer unavailable", + "code.file_tree.remote_unavailable": "The Project Explorer requires access to your local workspace, which isn’t supported in remote sessions.", + "code.file_tree.reveal_in_explorer": "Reveal in Explorer", + "code.file_tree.reveal_in_file_manager": "Reveal in file manager", + "code.file_tree.reveal_in_finder": "Reveal in Finder", + "code.file_tree.too_many_files": "Folder has too many files to display in the file explorer.", + "code.file_tree.wsl_unavailable": "The Project Explorer doesn't currently work in WSL.", + "code.find_references.showing_plural": "Showing {count} references", + "code.find_references.showing_singular": "Showing 1 reference", + "code.footer.enable_server": "Enable {server_name}", + "code.footer.enable_servers": "Enable servers", + "code.footer.install_server": "Install {server_name}", + "code.footer.install_servers": "Install servers", + "code.footer.installing_server": "Installing {server_name}...", + "code.footer.language_server_unavailable_codebase": "Language server is unavailable for this codebase", + "code.footer.language_support_not_enabled": "Language support is not currently enabled for {root_name}", + "code.footer.language_support_unavailable": "Language support is unavailable for {root_name}", + "code.footer.language_support_unavailable_file_type": "Language support is unavailable for this file type", + "code.footer.manage_servers": "Manage servers", + "code.footer.open_logs": "Open logs", + "code.footer.remove_server": "Remove server", + "code.footer.restart_all_servers": "Restart all servers", + "code.footer.restart_server": "Restart server", + "code.footer.server_error": "{server_name}: error", + "code.footer.server_message": "{server_name}: {message}", + "code.footer.server_stopped": "{server_name}: stopped", + "code.footer.start_all_servers": "Start all servers", + "code.footer.start_all_stopped_servers": "Start all stopped servers", + "code.footer.start_server": "Start server", + "code.footer.stop_all_servers": "Stop all servers", + "code.footer.stop_server": "Stop server", + "code.footer.this_codebase": "this codebase", + "code.footer.this_workspace": "this workspace", + "code.footer.unknown_workspace": "unknown workspace", + "code.global_buffer_model.buffer_deallocated": "Buffer deallocated", + "code.global_buffer_model.buffer_not_found": "Buffer not found", + "code.global_buffer_model.no_remote_server_client": "No remote server client available", + "code.inline_diff.failed_delete_file": "Failed to delete file: {error}", + "code.inline_diff.failed_save_file": "Failed to save file: {error}", + "code.inline_diff.missing_base_content": "Missing base content", + "code.local_code_editor.failed_save_file": "Failed to save file: {error}", + "code.local_code_editor.missing_file_id": "Missing file_id", + "code.toast.file_saved": "File saved.", + "code.toast.load_file_failed": "Failed to load file.", + "code.toast.save_file_failed": "Failed to save file.", + "code.toast.save_file_remote_disconnected": "Cannot save — remote session disconnected.", + "code.view_markdown_preview": "View Markdown preview", + "code.view.new_suffix": " (new)", + "coding_entrypoints.clone_repo.placeholder": "Provide a repository URL e.g. \"git@github.com:username/project.git\"", + "coding_entrypoints.create_project.placeholder": "What do you want to build?", + "coding_entrypoints.create_project.suggestion.csv_to_json_cli": "Write a CSV to JSON converter CLI", + "coding_entrypoints.create_project.suggestion.game_of_life": "Make a Conway's Game of Life simulation", + "coding_entrypoints.create_project.suggestion.minesweeper": "Build a Minesweeper clone in React", + "coding_entrypoints.create_project.suggestion.random_quotes_server": "Code a Node.js server that returns random quotes from a JSON file", + "coding_entrypoints.create_project.suggestion.resume_template": "Create a starter template for a résumé web page", + "coding_entrypoints.project_buttons.clone_repository.label": "Clone repository", + "coding_entrypoints.project_buttons.clone_repository.tooltip": "Clone a repo from GitHub or another source", + "coding_entrypoints.project_buttons.create_new_project.label": "Create new project", + "coding_entrypoints.project_buttons.create_new_project.tooltip": "Create and initialize a brand new project", + "coding_entrypoints.project_buttons.open_repository.label": "Open repository", + "coding_entrypoints.project_buttons.open_repository.tooltip": "Open an existing local folder or repository", + "common.accept": "Accept", + "common.add": "Add", + "common.agent": "Agent", + "common.allow": "Allow", + "common.attach_to_active_session": "Attach to active session", + "common.back": "Back", + "common.beta": "Beta", + "common.cancel": "Cancel", + "common.click": "Click", + "common.close": "Close", + "common.close_pane": "Close pane", + "common.collapse": "Collapse", + "common.collapse_all": "Collapse all", + "common.configure": "Configure", + "common.confirm": "Confirm", + "common.continue": "Continue", + "common.copied": "Copied", + "common.copied_to_clipboard": "Copied to clipboard", + "common.copy": "Copy", + "common.copy_file_path": "Copy file path", + "common.copy_id": "Copy id", + "common.copy_link": "Copy link", + "common.copy_path": "Copy path", + "common.copy_relative_path": "Copy relative path", + "common.create": "Create", + "common.current": "Current", + "common.custom_ellipsis": "Custom...", + "common.cut": "Cut", + "common.default": "Default", + "common.delete": "Delete", + "common.delete_forever": "Delete forever", + "common.description": "Description", + "common.dismiss": "Dismiss", + "common.do_not_show_again": "Don't show again", + "common.done": "Done", + "common.download": "Download", + "common.duplicate": "Duplicate", + "common.edit": "Edit", + "common.editing": "Editing", + "common.enable": "Enable", + "common.error": "Error", + "common.exit": "Exit", + "common.expand": "Expand", + "common.expiration": "Expiration", + "common.export": "Export", + "common.finish": "Finish", + "common.install": "Install", + "common.installed": "Installed", + "common.learn_more": "Learn more", + "common.left": "Left", + "common.link_copied": "Link copied", + "common.link_copied_to_clipboard": "Link copied to clipboard", + "common.loading": "Loading...", + "common.manage": "Manage", + "common.name": "Name", + "common.new": "New", + "common.new_uppercase": "NEW", + "common.next": "Next", + "common.no": "No", + "common.no_matches": "No matches", + "common.no_matches_found": "No matches found.", + "common.none": "None", + "common.open": "Open", + "common.open_file": "Open file", + "common.open_folder": "Open folder", + "common.open_in_warp": "Open in Warp", + "common.open_on_desktop": "Open on Desktop", + "common.or": " or ", + "common.or_standalone": "Or", + "common.other_user": "Other user", + "common.paste": "Paste", + "common.previous": "Previous", + "common.recommended": "Recommended", + "common.redo": "Redo", + "common.refine": "Refine", + "common.refresh": "Refresh", + "common.reject": "Reject", + "common.remove": "Remove", + "common.rename": "Rename", + "common.reset": "Reset", + "common.restart": "Restart", + "common.restore": "Restore", + "common.restore_default": "Restore default", + "common.retry": "Retry", + "common.right": "Right", + "common.run": "Run", + "common.save": "Save", + "common.save_changes": "Save changes", + "common.saving": "Saving...", + "common.search": "Search", + "common.select_all": "Select all", + "common.send_feedback": "Send Feedback", + "common.settings": "Settings", + "common.share": "Share", + "common.sign_in_to_edit": "Sign in to edit", + "common.sign_up": "Sign up", + "common.skip": "Skip", + "common.something_went_wrong": "Something went wrong", + "common.something_went_wrong_try_again": "Something went wrong. Please try again.", + "common.split_pane_down": "Split pane down", + "common.split_pane_left": "Split pane left", + "common.split_pane_right": "Split pane right", + "common.split_pane_up": "Split pane up", + "common.status": "Status", + "common.suggested": "Suggested", + "common.text": "Text", + "common.to_toggle_selection": "to toggle selection", + "common.trash": "Trash", + "common.troubleshoot": "Troubleshoot", + "common.try": "Try ", + "common.try_again": "Try again", + "common.type": "Type", + "common.undo": "Undo", + "common.untitled": "Untitled", + "common.update": "Update", + "common.upgrade": "Upgrade", + "common.view": "View", + "common.view_details": "View details", + "common.view_plans": "View plans", + "common.viewing": "Viewing", + "common.yes": "Yes", + "context_chips.change_git_branch": "Change git branch", + "context_chips.change_working_directory": "Change working directory", + "context_chips.copy_chip": "Copy {title}", + "context_chips.create_new_branch": "Create new branch \"{branch}\"", + "context_chips.environment.id": "ID:", + "context_chips.environment.image": "Image:", + "context_chips.environment.name": "Name:", + "context_chips.environment.none": "(none)", + "context_chips.environment.repos": "Repos:", + "context_chips.menu.no_results": "No results", + "context_chips.menu.no_results_found": "No results found", + "context_chips.menu.search_branches": "Search branches...", + "context_chips.menu.search_directories": "Search directories...", + "context_chips.menu.search_environments": "Search environments...", + "context_chips.monthly_ai_credits_reset": "Monthly AI credits reset!", + "context_chips.node.install_nvm_description": "This menu helps you switch between Node.js versions — but it requires nvm to be installed.", + "context_chips.node.install_nvm_title": "Install nvm to enable version switching", + "context_chips.node.no_versions_installed": "No node versions installed", + "context_chips.node.try_installing_with_nvm": "Try installing versions with nvm", + "context_chips.parent_directory": ".. (Parent Directory)", + "context_chips.plan.agent_unaware_of_recent_edits": "Agent is unaware of recent plan edits", + "context_chips.plan.view_plan": "View plan", + "context_chips.requires_command": "Requires the `{command}` command", + "context_chips.requires_github_cli": "Requires the GitHub CLI", + "context_chips.requires_local_session": "Requires a local session", + "context_chips.title.agent_plan_and_todo_list": "Agent Plan and Todo List", + "context_chips.title.conda_environment": "Conda Environment", + "context_chips.title.date": "Date", + "context_chips.title.git_branch": "Git Branch", + "context_chips.title.git_diff_stats": "Git Diff Stats", + "context_chips.title.github_pull_request": "GitHub Pull Request", + "context_chips.title.host": "Host", + "context_chips.title.kubernetes_context": "Kubernetes Context", + "context_chips.title.node_js_version": "Node.js Version", + "context_chips.title.python_virtualenv": "Python Virtualenv", + "context_chips.title.remote_login": "Remote Login", + "context_chips.title.subshell": "subshell", + "context_chips.title.svn_branch": "Svn Branch", + "context_chips.title.svn_uncommitted_file_count": "Svn Uncommitted File Count", + "context_chips.title.time_12": "Time (12-hour format)", + "context_chips.title.time_24": "Time (24-hour format)", + "context_chips.title.user": "User", + "context_chips.title.working_directory": "Working Directory", + "context_chips.todo.view_todo_list": "View todo list", + "context_chips.view_pull_request": "View pull request", + "context_chips.working_directory": "Working directory", + "drive.banner.joined": "{first} {second}", + "drive.confirmation.delete_team.body": "Deleting this team will permanently delete it and all of its related content, including billing information or credits. You will not be able to restore them.", + "drive.confirmation.delete_team.confirm": "Yes, delete", + "drive.confirmation.delete_team.title": "Are you sure you want to delete this team?", + "drive.confirmation.leave_team_reload_credits.body": "If you leave this team, you’ll lose access to any remaining reload credits tied to it. You’ll regain access to any unused, non-expired credits if you rejoin the same team later.", + "drive.confirmation.leave_team_reload_credits.confirm": "Leave Team", + "drive.confirmation.leave_team.body": "You will need to be reinvited in order to rejoin.", + "drive.confirmation.leave_team.confirm": "Yes, leave", + "drive.confirmation.leave_team.title": "Are you sure you want to leave this team?", + "drive.confirmation.remove_member_reload_credits.body": "This member will lose access to any remaining reload credits tied to this team. If they rejoin later, they’ll regain access to any unused, non-expired credits.", + "drive.confirmation.remove_member_reload_credits.confirm": "Remove Member", + "drive.confirmation.remove_member.title": "Are you sure you want to remove this member?", + "drive.copy_variables": "Copy variables", + "drive.copy_workflow_text": "Copy workflow text", + "drive.empty_trash": "Empty trash", + "drive.export.exported_named": "Exported {name}", + "drive.export.exported_object": "Exported object", + "drive.export.failed": "Export failed", + "drive.export.failed_named": "Failed to export {name}", + "drive.export.finished": "Finished exporting objects", + "drive.export.open_in_finder": "Open in Finder", + "drive.export.open_in_folder": "Open in folder", + "drive.import.choose_files": "Choose files...", + "drive.import.failed_parse_file": "Failed to parse file: {error}", + "drive.import.failed_upload_file": "Failed to upload file to server", + "drive.import.failed_upload_folder": "Failed to upload folder to server", + "drive.import.learn_file_support": "Learn about file support and formatting", + "drive.import.preparing": "Preparing...", + "drive.import.title": "Import", + "drive.items.from_owner": "From {owner}", + "drive.items.mcp_servers": "MCP Servers", + "drive.items.rules": "Rules", + "drive.items.unknown_team": "unknown team", + "drive.items.unknown_user": "unknown user", + "drive.limit.agent_workflows": "Agent Workflows", + "drive.limit.ai_fact": "AI Fact", + "drive.limit.environment_variables": "Environment Variables", + "drive.limit.folders": "Folders", + "drive.limit.mcp_server": "MCP Server", + "drive.limit.mcp_servers": "MCP Servers", + "drive.limit.notebooks": "Notebooks", + "drive.limit.object.environment_variables": "environment variables", + "drive.limit.object.folders": "folders", + "drive.limit.object.notebooks": "notebooks", + "drive.limit.object.objects": "objects", + "drive.limit.object.rules": "rules", + "drive.limit.object.workflows": "workflows", + "drive.limit.personal_sign_up_description": "Sign up for free to increase your storage limit and unlock more features.", + "drive.limit.rules": "Rules", + "drive.limit.shared_limit_reached": "You've run out of {object_type} on your plan.", + "drive.limit.shared_limit_upgrade": "Upgrade for access to more notebooks, workflows, shared sessions, and AI credits.", + "drive.limit.workflows": "Workflows", + "drive.load_in_subshell": "Load in subshell", + "drive.menu.environment_variables": "Environment variables", + "drive.menu.folder": "Folder", + "drive.menu.move_to_space": "Move to {space}", + "drive.menu.new_environment_variables": "New environment variables", + "drive.menu.new_folder": "New folder", + "drive.menu.new_notebook": "New notebook", + "drive.menu.new_prompt": "New prompt", + "drive.menu.new_workflow": "New workflow", + "drive.menu.notebook": "Notebook", + "drive.menu.prompt": "Prompt", + "drive.menu.workflow": "Workflow", + "drive.naming.collection_name": "Collection name", + "drive.naming.folder_name": "Folder name", + "drive.naming.notebook_name": "Notebook name", + "drive.object.ai_fact": "AI fact", + "drive.object.ai_fact_collection": "AI fact collection", + "drive.object.env_var_collection": "env var collection", + "drive.object.folder": "folder", + "drive.object.mcp_server": "MCP server", + "drive.object.mcp_server_collection": "MCP server collection", + "drive.object.notebook": "notebook", + "drive.object.prompt": "prompt", + "drive.object.workflow": "workflow", + "drive.offline_banner": "You are offline. Some files will be read only.", + "drive.payment_issue.admin": "Please update your payment information to restore access.", + "drive.payment_issue.admin_enterprise": "Please contact support@warp.dev to restore access.", + "drive.payment_issue.non_admin": "Please contact a team admin to restore access.", + "drive.payment_issue.restricted": "Shared objects have been restricted due to a subscription payment issue.", + "drive.retry_sync": "Retry sync", + "drive.revert_to_server": "Revert to server", + "drive.sharing.access.can_edit": "Can edit", + "drive.sharing.access.can_view": "Can view", + "drive.sharing.access.edit_name": "edit", + "drive.sharing.access.full": "Full access", + "drive.sharing.access.full_name": "full", + "drive.sharing.access.no_access": "No access", + "drive.sharing.access.view_name": "view", + "drive.sharing.already_shared_with": "Already shared with {emails}", + "drive.sharing.anyone_with_link": "Anyone with the link", + "drive.sharing.cannot_edit_inherited_permissions_tooltip": "Cannot edit inherited permissions", + "drive.sharing.download_qr_code": "Download QR code", + "drive.sharing.edit_inherited_permissions_tooltip": "Edit inherited permissions on the parent folder", + "drive.sharing.emails_placeholder": "Emails", + "drive.sharing.inherited_from_prefix": "Inherited from ", + "drive.sharing.inherited_permission": "Inherited permission", + "drive.sharing.invalid_address": "Invalid address: {emails}", + "drive.sharing.link_copied": "Copied link to {object_name}.", + "drive.sharing.live_session_started": "Live session started at {time} on {date}", + "drive.sharing.only_invited_teammates": "Only invited teammates", + "drive.sharing.only_people_invited": "Only people invited", + "drive.sharing.owner_full_permissions_tooltip": "Owners always have full permissions on their objects", + "drive.sharing.qr_create_failed": "Unable to create QR code for this session link.", + "drive.sharing.qr_download_failed": "Unable to download QR code.", + "drive.sharing.qr_downloaded": "QR code downloaded.", + "drive.sharing.restricted_access_prefix": "You must have full access to manage permissions. You have ", + "drive.sharing.restricted_access_suffix": " access.", + "drive.sharing.share_session_qr_code": "Share session QR code", + "drive.sharing.show_qr_code": "Show QR code", + "drive.sharing.team_owner_full_permissions_tooltip": "Team objects automatically grant full permissions to team members", + "drive.sharing.teammates_with_link": "Teammates with the link", + "drive.sharing.unknown_subject": "Unknown", + "drive.sharing.who_has_access": "Who has access", + "drive.sort_by": "Sort by", + "drive.sort.a_to_z": "A to Z", + "drive.sort.last_trashed": "Last trashed", + "drive.sort.last_updated": "Last updated", + "drive.sort.type": "Type", + "drive.sort.z_to_a": "Z to A", + "drive.syncing": "Syncing Warp Drive", + "drive.team.create_hint": "Share commands & knowledge with your teammates.", + "drive.team.create_team": "Create team", + "drive.team.join_hint": "Collaborate with {count} of your teammates already on Warp.", + "drive.team.view_team_to_join": "View team to join", + "drive.team.view_teams_to_join": "View teams to join", + "drive.team.zero_state_hint": "Drag or move a personal workflow or notebook here to share it with your team.", + "drive.title": "Warp Drive", + "drive.trash.deleted_after_30_days": "Items in the trash will be deleted forever after 30 days.", + "drive.trash.empty_confirm": "Yes, empty trash", + "drive.trash.empty_confirmation_body": "This action cannot be undone.", + "drive.trash.empty_confirmation_title": "Are you sure you want to empty the trash?", + "drive.trash.title": "Trash", + "drive.trash.uppercase": "TRASH", + "drive.workflows.ai_assist.bad_command": "Failed to generate metadata. Please try again with a different command.", + "drive.workflows.ai_assist.rate_limited": "Looks like you're out of AI credits. Please try again later.", + "drive.workflows.ai_assist.rate_limited_contact_admin": "Looks like you're out of AI credits. Contact a team admin to upgrade for more credits.", + "drive.workflows.argument_default_value_placeholder": "Default value (optional)", + "editor.a11y.deleted": ", deleted", + "editor.a11y.pasting": "Pasting: {text}", + "editor.a11y.selected": "selected", + "editor.a11y.selection_action": ", {action}", + "editor.a11y.unselected": "unselected", + "editor.autosuggestion.change_keybinding": "Change keybinding", + "editor.autosuggestion.cycle_suggestions": "Cycle suggestions", + "editor.autosuggestion.ignore_this_suggestion": "Ignore this suggestion", + "editor.context_menu.search_files_and_directories": "Search files and directories", + "editor.image.attach_images": "Attach images", + "editor.image.attachment_disabled_conversation_limit": "Image attachment is disabled - limit is {count} per conversation", + "editor.image.attachment_disabled_query_limit": "Image attachment is disabled - limit is {count} per query", + "editor.image.attachment_unsupported_model": "Image attachment isn't supported by this model", + "editor.image.limit_per_conversation": "limit is {count} per conversation", + "editor.image.limit_per_query": "limit is {count} per query", + "editor.image.not_attached.limit_many": "{count} images weren't attached - {limit_reason}.", + "editor.image.not_attached.limit_one": "1 image wasn't attached - {limit_reason}.", + "editor.image.processing_error_many": "{count} images weren't attached - error processing.", + "editor.image.processing_error_one": "1 image wasn't attached - error processing.", + "editor.image.processing_error_single_only": "Image cannot be attached - error processing.", + "editor.image.read_failed_many": "{count} images weren't attached - failed to read files.", + "editor.image.read_failed_one": "1 image wasn't attached - failed to read file.", + "editor.image.read_failed_single_only": "Image cannot be attached - failed to read file.", + "editor.image.too_large_many": "{count} images weren't attached - files are too large.", + "editor.image.too_large_one": "1 image wasn't attached - file is too large.", + "editor.image.too_large_single_only": "Image cannot be attached - file is too large.", + "editor.image.unsupported_many": "{count} images weren't attached - supported types are PNG, JPG, GIF, WEBP.", + "editor.image.unsupported_one": "1 image wasn't attached - supported types are PNG, JPG, GIF, WEBP.", + "editor.image.unsupported_single_only": "Image cannot be attached - supported types are PNG, JPG, GIF, WEBP.", + "editor.model_no_image_context": "The selected model does not support images as context.", + "editor.model_no_image_context_no_period": "The selected model does not support images as context", + "editor.voice.enabled_with_key": "Voice input is enabled. You can also press and hold the `{key}` key to activate voice input (configure in Settings > AI > Voice)", + "editor.voice.error": "An error occurred while processing your voice input.", + "editor.voice.limit_hit": "You have hit the limit for Voice requests. Your limit will be refreshed as a part of your next cycle.", + "editor.voice.start_failed_microphone": "Failed to start voice input (you may need to enable Microphone access)", + "editor.voice.tooltip": "Voice transcription", + "editor.voice.tooltip_microphone_denied": "Voice transcription is disabled because Microphone access was not granted.", + "editor.voice.tooltip_with_key": "Voice transcription (hold `{key}` key)", + "editor.voice.try_voice_input": "Try Voice Input", + "env_vars.clear_secret": "Clear secret", + "env_vars.command": "Command", + "env_vars.command_placeholder": "Command", + "env_vars.command_waiting_for_user": "OK if I run this command and read the output?", + "env_vars.description_placeholder": "Add a description", + "env_vars.discard_changes": "Discard changes", + "env_vars.invoke_failed": "An error occurred while trying to invoke the env var", + "env_vars.keep_editing": "Keep editing", + "env_vars.load": "Load", + "env_vars.maximize_pane": "Maximize pane", + "env_vars.minimize_pane": "Minimize pane", + "env_vars.moved_to_trash": "Environment variables were moved to trash", + "env_vars.no_longer_access": "You no longer have access to these environment variables", + "env_vars.restore_from_trash": "Restore environment variables from trash", + "env_vars.secret_command": "Secret command", + "env_vars.secret_or_command_tooltip": "Add secret or command. Warp never stores external secrets", + "env_vars.secret_redaction_conflict_enterprise": "This environment variable cannot be created due to conflicts with your enterprise's secret redaction settings. Contact a team admin for details.", + "env_vars.secret_redaction_conflict_user": "This environment variable cannot be created due to conflicts with your secret redaction settings. Save the secret as an environment variable (in your shell config or a .env file), or update your secret redaction settings in Settings > Privacy.", + "env_vars.title": "Title", + "env_vars.title_placeholder": "Add a title", + "env_vars.unsaved_changes": "You have unsaved changes.", + "env_vars.value_placeholder": "Value", + "env_vars.variable_placeholder": "Variable", + "env_vars.variables": "Variables", + "external_secrets.error.cli_not_installed": "{manager} CLI is not installed", + "external_secrets.error.fetch_failed": "{manager} didn't return secrets (likely not configured or authenticated)", + "external_secrets.error.platform_not_supported": "Platform not supported", + "external_secrets.link.integrate_1password_cli": "Integrate 1Password app with CLI", + "external_secrets.link.view_cli_installation_docs": "View {manager} CLI installation documentation", + "find.a11y.close_find_bar": "Close find bar", + "find.a11y.disable_case_sensitive_search": "Disable case-sensitive search", + "find.a11y.disable_regex_search": "Disable regex search", + "find.a11y.enable_case_sensitive_search": "Enable case-sensitive search", + "find.a11y.enable_regex_search": "Enable regex search", + "find.a11y.focus_next_match": "Focus next match", + "find.a11y.focus_previous_match": "Focus previous match", + "find.a11y.input_help": "Press escape to quit, use enter and shift-enter to navigate between matches", + "find.a11y.no_results": "No results.", + "find.a11y.result_count": "Result {current} of {total}.", + "find.a11y.result_help": "Use enter and shift-enter to navigate between matches. Escape to quit.", + "find.a11y.type_phrase": "Type searched phrase.", + "find.binding.find_next_occurrence": "Find the next occurrence of your search query", + "find.binding.find_previous_occurrence": "Find the previous occurrence of your search query", + "find.case_sensitive_tooltip": "Case sensitive search", + "find.placeholder": "Find", + "find.regex_toggle_tooltip": "Regex toggle", + "find.scanning": "Scanning...", + "find.within_block_tooltip": "Find in selected block", + "input_suggestions.a11y.closed": "Closed suggestions.", + "input_suggestions.a11y.command_suggestions": "Command suggestions.", + "input_suggestions.a11y.command_suggestions_help": "Navigate with tab and shift-tab, and confirm with enter. Execute selected command with command + enter. Esc leaves the suggestions menu.", + "input_suggestions.a11y.last_ran": "Last ran {time}", + "input_suggestions.a11y.selected": "Selected: {text}", + "input_suggestions.a11y.suggestion": "Suggestion: {text}.", + "input_suggestions.no_suggestions": "No suggestions", + "keybinding.description.a11y_set_concise_accessibility_announcements": "[A11y] Set Concise Accessibility Announcements", + "keybinding.description.a11y_set_verbose_accessibility_announcements": "[A11y] Set Verbose Accessibility Announcements", + "keybinding.description.about_warp": "About Warp", + "keybinding.description.accept_autosuggestion": "Accept Autosuggestion", + "keybinding.description.activate_next_tab": "Activate Next Tab", + "keybinding.description.activate_previous_tab": "Activate Previous Tab", + "keybinding.description.add_current_folder_as_project": "Add current folder as project", + "keybinding.description.add_cursor_above": "Add Cursor Above", + "keybinding.description.add_cursor_below": "Add Cursor Below", + "keybinding.description.add_repository": "Add Repository", + "keybinding.description.add_selection_for_next_occurrence": "Add Selection For Next Occurrence", + "keybinding.description.agent_conversation_list_view": "Agent conversation list view", + "keybinding.description.alternate_terminal_paste": "Alternate Terminal Paste", + "keybinding.description.appearance": "Appearance...", + "keybinding.description.ask_warp_ai": "Ask Warp AI", + "keybinding.description.ask_warp_ai_about_last_block": "Ask Warp AI About Last Block", + "keybinding.description.ask_warp_ai_about_selection": "Ask Warp AI About Selection", + "keybinding.description.attach_selected_block_as_agent_context": "Attach Selected Block as Agent Context", + "keybinding.description.attach_selected_text_as_agent_context": "Attach Selected Text as Agent Context", + "keybinding.description.attach_selection_as_agent_context": "Attach Selection as Agent Context", + "keybinding.description.backward_tabulation_within_an_executing_command": "Backward Tabulation Within An Executing Command", + "keybinding.description.bookmark_selected_block": "Bookmark Selected Block", + "keybinding.description.check_for_updates": "Check for updates", + "keybinding.description.clear_and_reset_ai_context_menu_query": "Clear And Reset AI Context Menu Query", + "keybinding.description.clear_command_editor": "Clear Command Editor", + "keybinding.description.clear_screen": "Clear Screen", + "keybinding.description.clear_selected_lines": "Clear Selected Lines", + "keybinding.description.close": "Close", + "keybinding.description.close_all_tabs": "Close All Tabs", + "keybinding.description.close_current_session": "Close Current Session", + "keybinding.description.close_focused_panel": "Close focused panel", + "keybinding.description.close_saved_tabs": "Close Saved Tabs", + "keybinding.description.close_tabs_below": "close tabs below", + "keybinding.description.close_tabs_to_the_right": "Close tabs to the right", + "keybinding.description.close_the_current_tab": "Close The Current Tab", + "keybinding.description.close_warp_ai": "Close Warp AI", + "keybinding.description.close_window": "Close Window", + "keybinding.description.command_palette": "Command Palette", + "keybinding.description.configure_keyboard_shortcuts": "Configure Keyboard Shortcuts...", + "keybinding.description.configure_warpify": "Configure Warpify...", + "keybinding.description.copy_access_token_to_clipboard": "Copy Access Token To Clipboard", + "keybinding.description.copy_and_clear_selected_lines": "Copy And Clear Selected Lines", + "keybinding.description.copy_command": "Copy Command", + "keybinding.description.copy_command_and_output": "Copy Command And Output", + "keybinding.description.copy_command_output": "Copy Command Output", + "keybinding.description.copy_git_branch": "Copy Git Branch", + "keybinding.description.copy_rich_text_buffer": "Copy Rich-Text Buffer", + "keybinding.description.copy_rich_text_selection": "Copy Rich-Text Selection", + "keybinding.description.create_a_new_personal_folder": "Create a new personal folder", + "keybinding.description.create_a_new_personal_notebook": "Create a new personal notebook", + "keybinding.description.create_a_new_personal_prompt": "Create a new personal prompt", + "keybinding.description.create_a_new_personal_workflow": "Create a new personal workflow", + "keybinding.description.create_a_new_team_folder": "Create a new team folder", + "keybinding.description.create_a_new_team_notebook": "Create a new team notebook", + "keybinding.description.create_a_new_team_prompt": "Create a new team prompt", + "keybinding.description.create_a_new_team_workflow": "Create a new team workflow", + "keybinding.description.create_new_personal_environment_variables": "Create new personal environment variables", + "keybinding.description.create_new_project": "Create New Project", + "keybinding.description.create_new_tab": "Create new tab", + "keybinding.description.create_new_team_environment_variables": "Create new team environment variables", + "keybinding.description.create_or_edit_link": "Create Or Edit Link", + "keybinding.description.cursor_at_buffer_end": "Cursor At Buffer End", + "keybinding.description.cursor_at_buffer_start": "Cursor At Buffer Start", + "keybinding.description.cut_all_left": "Cut All Left", + "keybinding.description.cut_all_right": "Cut All Right", + "keybinding.description.cut_word_left": "Cut Word Left", + "keybinding.description.cut_word_right": "Cut Word Right", + "keybinding.description.de_select_shell_commands": "De-Select Shell Commands", + "keybinding.description.decrease_font_size": "Decrease Font Size", + "keybinding.description.decrease_notebook_font_size": "Decrease Notebook Font Size", + "keybinding.description.decrease_zoom_level": "Decrease Zoom Level", + "keybinding.description.delete": "Delete", + "keybinding.description.delete_all_left": "Delete All Left", + "keybinding.description.delete_all_right": "Delete All Right", + "keybinding.description.delete_to_line_end_within_an_executing_command": "Delete To Line End Within An Executing Command", + "keybinding.description.delete_to_line_start_within_an_executing_command": "Delete To Line Start Within An Executing Command", + "keybinding.description.delete_word_left": "Delete Word Left", + "keybinding.description.delete_word_left_within_an_executing_command": "Delete Word Left Within An Executing Command", + "keybinding.description.delete_word_right": "Delete Word Right", + "keybinding.description.dump_heap_profile_can_only_be_done_once": "Dump Heap Profile (Can Only Be Done Once)", + "keybinding.description.edit_prompt": "Edit Prompt", + "keybinding.description.end": "End", + "keybinding.description.exit_vim_insert_mode": "Exit Vim Insert Mode", + "keybinding.description.expand_selected_blocks_above": "Expand Selected Blocks Above", + "keybinding.description.expand_selected_blocks_below": "Expand Selected Blocks Below", + "keybinding.description.experimental_toggle_classic_completions_mode": "(Experimental) Toggle Classic Completions Mode", + "keybinding.description.export_all_warp_drive_objects": "Export All Warp Drive Objects", + "keybinding.description.find_in_code_editor": "Find In Code Editor", + "keybinding.description.find_in_notebook": "Find In Notebook", + "keybinding.description.find_in_terminal": "Find In Terminal", + "keybinding.description.find_the_next_occurrence_of_your_search_query": "Find The Next Occurrence Of Your Search Query", + "keybinding.description.find_the_previous_occurrence_of_your_search_query": "Find The Previous Occurrence Of Your Search Query", + "keybinding.description.find_within_selected_block": "Find Within Selected Block", + "keybinding.description.focus_next_match": "Focus Next Match", + "keybinding.description.focus_previous_match": "Focus Previous Match", + "keybinding.description.focus_terminal_input": "Focus Terminal Input", + "keybinding.description.focus_terminal_input_from_file": "Focus Terminal Input From File", + "keybinding.description.focus_terminal_input_from_notebook": "Focus Terminal Input From Notebook", + "keybinding.description.focus_terminal_input_from_warp_ai": "Focus Terminal Input From Warp AI", + "keybinding.description.fold": "Fold", + "keybinding.description.fold_selected_ranges": "Fold Selected Ranges", + "keybinding.description.global_search": "Global Search", + "keybinding.description.go_to_line": "Go To Line", + "keybinding.description.hide_all_windows": "Hide All Windows", + "keybinding.description.hide_dedicated_hotkey_window": "Hide Dedicated Hotkey Window", + "keybinding.description.history_search": "History Search", + "keybinding.description.home": "Home", + "keybinding.description.import_external_settings": "Import External Settings", + "keybinding.description.import_to_personal_drive": "Import To Personal Drive", + "keybinding.description.import_to_team_drive": "Import To Team Drive", + "keybinding.description.increase_font_size": "Increase Font Size", + "keybinding.description.increase_notebook_font_size": "Increase Notebook Font Size", + "keybinding.description.increase_zoom_level": "Increase Zoom Level", + "keybinding.description.initiate_project_for_warp": "Initiate project for warp", + "keybinding.description.insert_command_correction": "Insert Command Correction", + "keybinding.description.insert_last_word_of_previous_command": "Insert Last Word Of Previous Command", + "keybinding.description.insert_newline": "Insert Newline", + "keybinding.description.insert_non_expanding_space": "Insert Non-Expanding Space", + "keybinding.description.inspect_command": "Inspect Command", + "keybinding.description.install_oz_cli_command": "Install Oz CLI Command", + "keybinding.description.install_update_and_relaunch": "Install Update And Relaunch", + "keybinding.description.invite_people": "Invite People...", + "keybinding.description.join_our_slack_community_opens_external_link": "Join Our Slack Community (Opens External Link)", + "keybinding.description.jump_to_latest_agent_task": "Jump To Latest Agent Task", + "keybinding.description.launch_configuration_palette": "Launch Configuration Palette", + "keybinding.description.left_panel_agent_conversations": "Left Panel: Agent conversations", + "keybinding.description.left_panel_global_search": "Left Panel: Global search", + "keybinding.description.left_panel_project_explorer": "Left Panel: Project explorer", + "keybinding.description.left_panel_warp_drive": "Left Panel: Warp Drive", + "keybinding.description.load_agent_mode_conversation_from_debug_link_in_clipboard": "Load Agent Mode Conversation (From Debug Link In Clipboard)", + "keybinding.description.log_editor_state": "Log Editor State", + "keybinding.description.log_out": "Log out", + "keybinding.description.move_backward_one_subword": "Move Backward One Subword", + "keybinding.description.move_backward_one_word": "Move Backward One Word", + "keybinding.description.move_cursor_down": "Move Cursor Down", + "keybinding.description.move_cursor_end_within_an_executing_command": "Move Cursor End Within An Executing Command", + "keybinding.description.move_cursor_home_within_an_executing_command": "Move Cursor Home Within An Executing Command", + "keybinding.description.move_cursor_left": "Move Cursor Left", + "keybinding.description.move_cursor_one_word_to_the_left_within_an_executing_command": "Move Cursor One Word To The Left Within An Executing Command", + "keybinding.description.move_cursor_one_word_to_the_right_within_an_executing_command": "Move Cursor One Word To The Right Within An Executing Command", + "keybinding.description.move_cursor_right": "Move Cursor Right", + "keybinding.description.move_cursor_to_the_bottom": "Move Cursor To The Bottom", + "keybinding.description.move_cursor_to_the_top": "Move Cursor To The Top", + "keybinding.description.move_cursor_up": "Move Cursor Up", + "keybinding.description.move_forward_one_subword": "Move Forward One Subword", + "keybinding.description.move_forward_one_word": "Move Forward One Word", + "keybinding.description.move_tab_down": "move tab down", + "keybinding.description.move_tab_left": "Move tab left", + "keybinding.description.move_tab_right": "Move tab right", + "keybinding.description.move_tab_up": "move tab up", + "keybinding.description.move_to_end_of_line": "Move To End Of Line", + "keybinding.description.move_to_end_of_paragraph": "Move To End Of Paragraph", + "keybinding.description.move_to_line_end": "Move To Line End", + "keybinding.description.move_to_line_start": "Move To Line Start", + "keybinding.description.move_to_start_of_line": "Move To Start Of Line", + "keybinding.description.move_to_start_of_paragraph": "Move To Start Of Paragraph", + "keybinding.description.move_to_the_end_of_the_buffer": "Move To The End Of The Buffer", + "keybinding.description.move_to_the_end_of_the_paragraph": "Move To The End Of The Paragraph", + "keybinding.description.move_to_the_start_of_the_buffer": "Move To The Start Of The Buffer", + "keybinding.description.move_to_the_start_of_the_paragraph": "Move To The Start Of The Paragraph", + "keybinding.description.navigation_palette": "Navigation Palette", + "keybinding.description.new_agent_conversation": "New agent conversation", + "keybinding.description.new_agent_pane": "New Agent Pane", + "keybinding.description.new_agent_tab": "New Agent Tab", + "keybinding.description.new_cloud_agent_tab": "New Cloud Agent Tab", + "keybinding.description.new_personal_environment_variables": "New Personal Environment Variables", + "keybinding.description.new_personal_folder": "New Personal Folder", + "keybinding.description.new_personal_notebook": "New Personal Notebook", + "keybinding.description.new_personal_prompt": "New Personal Prompt", + "keybinding.description.new_personal_workflow": "New Personal Workflow", + "keybinding.description.new_team_environment_variables": "New Team Environment Variables", + "keybinding.description.new_team_folder": "New Team Folder", + "keybinding.description.new_team_notebook": "New Team Notebook", + "keybinding.description.new_team_prompt": "New Team Prompt", + "keybinding.description.new_team_workflow": "New Team Workflow", + "keybinding.description.new_terminal_tab": "New Terminal Tab", + "keybinding.description.open_ai_command_suggestions": "Open AI Command Suggestions", + "keybinding.description.open_ai_rules": "Open AI Rules", + "keybinding.description.open_block_context_menu": "Open Block Context Menu", + "keybinding.description.open_completions_menu": "Open completions menu", + "keybinding.description.open_global_search": "Open global search", + "keybinding.description.open_keybindings_editor": "Open Keybindings Editor", + "keybinding.description.open_left_panel": "Open Left Panel", + "keybinding.description.open_mcp_servers": "Open MCP Servers", + "keybinding.description.open_repository": "Open Repository", + "keybinding.description.open_settings": "Open Settings", + "keybinding.description.open_settings_about": "Open Settings: About", + "keybinding.description.open_settings_account": "Open Settings: Account", + "keybinding.description.open_settings_ai": "Open Settings: AI", + "keybinding.description.open_settings_appearance": "Open Settings: Appearance", + "keybinding.description.open_settings_billing_and_usage": "Open Settings: Billing and usage", + "keybinding.description.open_settings_code": "Open Settings: Code", + "keybinding.description.open_settings_environments": "Open Settings: Environments", + "keybinding.description.open_settings_features": "Open Settings: Features", + "keybinding.description.open_settings_file": "Open Settings File", + "keybinding.description.open_settings_keyboard_shortcuts": "Open Settings: Keyboard Shortcuts", + "keybinding.description.open_settings_mcp_servers": "Open Settings: MCP Servers", + "keybinding.description.open_settings_privacy": "Open Settings: Privacy", + "keybinding.description.open_settings_referrals": "Open Settings: Referrals", + "keybinding.description.open_settings_shared_blocks": "Open Settings: Shared Blocks", + "keybinding.description.open_settings_teams": "Open Settings: Teams", + "keybinding.description.open_settings_warpify": "Open Settings: Warpify", + "keybinding.description.open_tab_configs_menu": "Open tab configs menu", + "keybinding.description.open_team_settings": "Open Team Settings", + "keybinding.description.open_theme_picker": "Open Theme Picker", + "keybinding.description.open_view_tree_debugger": "Open View Tree Debugger", + "keybinding.description.project_explorer": "Project Explorer", + "keybinding.description.quit_warp": "Quit Warp", + "keybinding.description.reinput_selected_commands": "Reinput Selected Commands", + "keybinding.description.reinput_selected_commands_as_root": "Reinput Selected Commands As Root", + "keybinding.description.reload_file": "Reload File", + "keybinding.description.remove_the_previous_character": "Remove The Previous Character", + "keybinding.description.rename_the_current_pane": "Rename The Current Pane", + "keybinding.description.rename_the_current_tab": "Rename The Current Tab", + "keybinding.description.reopen_closed_session": "Reopen Closed Session", + "keybinding.description.reset_font_size_to_default": "Reset Font Size To Default", + "keybinding.description.reset_notebook_font_size": "Reset Notebook Font Size", + "keybinding.description.reset_zoom_level_to_default": "Reset Zoom Level To Default", + "keybinding.description.resize_pane_move_divider_down": "Resize Pane > Move Divider Down", + "keybinding.description.resize_pane_move_divider_left": "Resize Pane > Move Divider Left", + "keybinding.description.resize_pane_move_divider_right": "Resize Pane > Move Divider Right", + "keybinding.description.resize_pane_move_divider_up": "Resize Pane > Move Divider Up", + "keybinding.description.restart_warp_ai": "Restart Warp AI", + "keybinding.description.run_selected_commands": "Run Selected Commands", + "keybinding.description.sample_process": "Sample Process", + "keybinding.description.save_all_unsaved_files_in_code_review": "Save all unsaved files in code review", + "keybinding.description.save_file_as": "Save File As", + "keybinding.description.save_new_launch_configuration": "Save New Launch Configuration", + "keybinding.description.save_workflow": "Save Workflow", + "keybinding.description.scroll_down_half_a_page_vim": "Scroll Down Half A Page (Vim)", + "keybinding.description.scroll_terminal_output_down_one_line": "Scroll Terminal Output Down One Line", + "keybinding.description.scroll_terminal_output_down_one_page": "Scroll Terminal Output Down One Page", + "keybinding.description.scroll_terminal_output_up_one_line": "Scroll Terminal Output Up One Line", + "keybinding.description.scroll_terminal_output_up_one_page": "Scroll Terminal Output Up One Page", + "keybinding.description.scroll_to_bottom_of_selected_block": "Scroll To Bottom Of Selected Block", + "keybinding.description.scroll_to_top_of_selected_block": "Scroll To Top Of Selected Block", + "keybinding.description.scroll_up_half_a_page_vim": "Scroll Up Half A Page (Vim)", + "keybinding.description.select_all": "Select All", + "keybinding.description.select_and_move_to_the_bottom": "Select And Move To The Bottom", + "keybinding.description.select_and_move_to_the_top": "Select And Move To The Top", + "keybinding.description.select_down": "Select Down", + "keybinding.description.select_next_command": "Select Next Command", + "keybinding.description.select_one_character_to_the_left": "Select One Character To The Left", + "keybinding.description.select_one_character_to_the_right": "Select One Character To The Right", + "keybinding.description.select_one_subword_to_the_left": "Select One Subword To The Left", + "keybinding.description.select_one_subword_to_the_right": "Select One Subword To The Right", + "keybinding.description.select_one_word_to_the_left": "Select One Word To The Left", + "keybinding.description.select_one_word_to_the_right": "Select One Word To The Right", + "keybinding.description.select_previous_command": "Select Previous Command", + "keybinding.description.select_shell_command_at_cursor": "Select Shell Command At Cursor", + "keybinding.description.select_the_closest_bookmark_down": "Select The Closest Bookmark Down", + "keybinding.description.select_the_closest_bookmark_up": "Select The Closest Bookmark Up", + "keybinding.description.select_to_end_of_line": "Select To End Of Line", + "keybinding.description.select_to_end_of_paragraph": "Select To End Of Paragraph", + "keybinding.description.select_to_line_end": "Select To Line End", + "keybinding.description.select_to_line_start": "Select To Line Start", + "keybinding.description.select_to_start_of_line": "Select To Start Of Line", + "keybinding.description.select_to_start_of_paragraph": "Select To Start Of Paragraph", + "keybinding.description.select_up": "Select Up", + "keybinding.description.send_feedback_opens_external_link": "Send feedback (opens external link)", + "keybinding.description.settings": "Settings", + "keybinding.description.setup_guide": "Setup Guide", + "keybinding.description.share_current_session": "Share Current Session", + "keybinding.description.share_pane": "Share Pane", + "keybinding.description.share_selected_block": "Share Selected Block", + "keybinding.description.show_dedicated_hotkey_window": "Show Dedicated Hotkey Window", + "keybinding.description.show_find_bar_in_code_review": "Show find bar in code review", + "keybinding.description.show_warp_network_log": "Show Warp Network Log", + "keybinding.description.stop_sharing_current_session": "Stop Sharing Current Session", + "keybinding.description.stop_synchronizing_any_panes": "Stop Synchronizing Any Panes", + "keybinding.description.switch_focus_to_left_panel": "Switch Focus To Left Panel", + "keybinding.description.switch_focus_to_right_panel": "Switch Focus To Right Panel", + "keybinding.description.switch_panes_down": "Switch Panes Down", + "keybinding.description.switch_panes_left": "Switch Panes Left", + "keybinding.description.switch_panes_right": "Switch Panes Right", + "keybinding.description.switch_panes_up": "Switch Panes Up", + "keybinding.description.switch_to_1st_tab": "Switch To 1St Tab", + "keybinding.description.switch_to_2nd_tab": "Switch To 2Nd Tab", + "keybinding.description.switch_to_3rd_tab": "Switch To 3Rd Tab", + "keybinding.description.switch_to_4th_tab": "Switch To 4Th Tab", + "keybinding.description.switch_to_5th_tab": "Switch To 5Th Tab", + "keybinding.description.switch_to_6th_tab": "Switch To 6Th Tab", + "keybinding.description.switch_to_7th_tab": "Switch To 7Th Tab", + "keybinding.description.switch_to_8th_tab": "Switch To 8Th Tab", + "keybinding.description.switch_to_last_tab": "Switch To Last Tab", + "keybinding.description.take_control_of_running_command": "Take control of running command", + "keybinding.description.terminal_session": "Terminal Session", + "keybinding.description.toggle_agent_conversation_list_view": "Toggle Agent conversation list view", + "keybinding.description.toggle_case_sensitive_search": "Toggle Case-Sensitive Search", + "keybinding.description.toggle_code_review": "Toggle Code Review", + "keybinding.description.toggle_command_palette": "Toggle command palette", + "keybinding.description.toggle_comment": "Toggle Comment", + "keybinding.description.toggle_conversation_details_panel": "Toggle Conversation Details Panel", + "keybinding.description.toggle_file_navigation_in_code_review": "Toggle file navigation in code review", + "keybinding.description.toggle_files_palette": "Toggle Files Palette", + "keybinding.description.toggle_fullscreen": "Toggle Fullscreen", + "keybinding.description.toggle_inline_code_styling": "Toggle Inline Code Styling", + "keybinding.description.toggle_keyboard_shortcuts": "Toggle Keyboard Shortcuts", + "keybinding.description.toggle_maximize_active_pane": "Toggle Maximize Active Pane", + "keybinding.description.toggle_maximize_code_review_panel": "Toggle Maximize Code Review Panel", + "keybinding.description.toggle_mouse_reporting": "Toggle Mouse Reporting", + "keybinding.description.toggle_navigation_palette": "Toggle navigation palette", + "keybinding.description.toggle_notification_mailbox": "Toggle notification mailbox", + "keybinding.description.toggle_project_explorer": "Toggle project explorer", + "keybinding.description.toggle_pty_recording_for_session": "Toggle PTY Recording For Session", + "keybinding.description.toggle_regular_expression_search": "Toggle Regular Expression Search", + "keybinding.description.toggle_resource_center": "Toggle Resource Center", + "keybinding.description.toggle_rich_text_debug_mode": "Toggle Rich-Text Debug Mode", + "keybinding.description.toggle_sticky_command_header": "Toggle Sticky Command Header", + "keybinding.description.toggle_sticky_command_header_in_active_pane": "Toggle Sticky Command Header In Active Pane", + "keybinding.description.toggle_strikethrough_styling": "Toggle Strikethrough Styling", + "keybinding.description.toggle_synchronizing_all_panes_in_all_tabs": "Toggle Synchronizing All Panes In All Tabs", + "keybinding.description.toggle_synchronizing_all_panes_in_current_tab": "Toggle Synchronizing All Panes In Current Tab", + "keybinding.description.toggle_team_workflows_modal": "Toggle Team Workflows Modal", + "keybinding.description.toggle_the_agent_management_view": "Toggle The Agent Management View", + "keybinding.description.toggle_underline_styling": "Toggle Underline Styling", + "keybinding.description.toggle_vertical_tabs_panel": "Toggle Vertical Tabs Panel", + "keybinding.description.toggle_warp_ai": "Toggle Warp AI", + "keybinding.description.toggle_warp_drive": "Toggle Warp Drive", + "keybinding.description.trigger_auto_detection": "Trigger Auto Detection", + "keybinding.description.turn_notifications_off": "Turn Notifications Off", + "keybinding.description.turn_notifications_on": "Turn Notifications On", + "keybinding.description.unfold": "Unfold", + "keybinding.description.uninstall_oz_cli_command": "Uninstall Oz CLI Command", + "keybinding.description.view_latest_changelog": "View Latest Changelog", + "keybinding.description.view_privacy_policy_opens_external_link": "View Privacy Policy (Opens External Link)", + "keybinding.description.view_shared_blocks": "View Shared Blocks...", + "keybinding.description.view_user_docs_opens_external_link": "View User Docs (Opens External Link)", + "keybinding.description.view_warp_logs": "View Warp logs", + "keybinding.description.warp_drive": "Warp Drive", + "keybinding.description.warpify_ssh_session": "Warpify Ssh Session", + "keybinding.description.warpify_subshell": "Warpify Subshell", + "keybinding.description.workflows": "Workflows", + "keybinding.description.write_current_codebase_index_snapshot": "Write current codebase index snapshot", + "launch_configs.a11y.save_config_modal": "Save Config Modal", + "launch_configs.a11y.save_config_modal_help": "Type the name of the file to which you want to save your current configuration of windows, tabs, and panes. Use enter to save the launch configuration, esc to quit the save configuration modal.", + "launch_configs.description": "This will save your current configuration of windows, tabs and panes to a file so you can easily open it again.", + "launch_configs.description_with_shortcut": "This will save your current configuration of windows, tabs and panes to a file so you can easily open it again with {shortcut}.", + "launch_configs.failed_file_already_exists": "Failed to save. A launch configuration with the same name already exists.", + "launch_configs.failed_saving": "An issue was encountered while saving.", + "launch_configs.link_to_documentation": "Link to Documentation", + "launch_configs.open_yaml_file": "Open YAML File", + "launch_configs.save_configuration": "Save Configuration", + "launch_configs.save_current_configuration": "Save Current Configuration", + "launch_configs.saved_successfully_to": "Saved successfully to ", + "launch_configs.yaml_saved_to": "\nThe YAML file is saved to ", + "menu.a11y.action_selected": "Action Selected", + "menu.a11y.instructions.close_menu": "Press the escape key to close the menu", + "menu.a11y.instructions.close_submenu": "Removing focus from a submenu will close the submenu", + "menu.a11y.instructions.execute_action": "Press the enter key to execute the selected menu item action", + "menu.a11y.instructions.open_selected_submenu": "Press the right key to open the selected submenu", + "menu.a11y.instructions.select_item": "Press the up key or the down key to select a menu item", + "menu.a11y.instructions.select_item_open_submenu": "Press the up key or the down key to select a menu item. Press the right key to open the submenu", + "menu.a11y.item_expanded": "{item} Expanded", + "menu.a11y.item_selected": "{item} Selected", + "menu.a11y.menu_closed": "Menu Closed", + "menu.a11y.submenu_closed": "Submenu Closed", + "menu.a11y.submenu_expanded": "Submenu Expanded", + "menu.pane.maximize": "Maximize pane", + "menu.pane.minimize": "Minimize pane", + "node_version.install_nvm": "Install nvm", + "notebooks.a11y.change_code_block_language": "Change code block language to {language}", + "notebooks.a11y.convert_to": "Convert to {block_type}", + "notebooks.a11y.copy_code_block": "Copy code block", + "notebooks.a11y.copy_link": "Copy link", + "notebooks.a11y.cut_line_left": "Cut line left", + "notebooks.a11y.cut_line_right": "Cut line right", + "notebooks.a11y.cut_word_left": "Cut word left", + "notebooks.a11y.cut_word_right": "Cut word right", + "notebooks.a11y.delete_line_left": "Delete line left", + "notebooks.a11y.delete_line_right": "Delete line right", + "notebooks.a11y.delete_word_left": "Delete word left", + "notebooks.a11y.delete_word_right": "Delete word right", + "notebooks.a11y.deselect_command": "De-select command", + "notebooks.a11y.deselect_command_help": "Switch from selecting commands to selecting text", + "notebooks.a11y.edit_link": "Edit link", + "notebooks.a11y.insert_block": "Insert {block_type} block", + "notebooks.a11y.notebook_with_title": "{title} notebook", + "notebooks.a11y.open_block_insertion_menu": "Open block-insertion menu", + "notebooks.a11y.open_embedded_object_search_menu": "Open embedded object search menu", + "notebooks.a11y.open_link": "Open link: {link}", + "notebooks.a11y.pasting": "Pasting: {text}", + "notebooks.a11y.remove_link": "Remove link", + "notebooks.a11y.secondary_click_on": "Secondary click on {link}", + "notebooks.a11y.selected_workflow": "Selected workflow: {command}", + "notebooks.a11y.shift_tab": "Shift-tab", + "notebooks.a11y.show_character_palette": "Show character palette", + "notebooks.a11y.show_find_bar": "Show find bar", + "notebooks.a11y.style_toggle": "{style} {state}", + "notebooks.a11y.style.bold": "Bold", + "notebooks.a11y.style.inline_code": "Inline code", + "notebooks.a11y.style.italic": "Italic", + "notebooks.a11y.style.off": "off", + "notebooks.a11y.style.on": "on", + "notebooks.a11y.style.strikethrough": "Strikethrough", + "notebooks.a11y.style.underline": "Underline", + "notebooks.a11y.toggle_task_list": "Toggle task list", + "notebooks.binding.copy_rich_text_buffer": "Copy rich-text buffer", + "notebooks.binding.copy_rich_text_selection": "Copy rich-text selection", + "notebooks.binding.create_or_edit_link": "Create or edit link", + "notebooks.binding.cut_all_left": "Cut all left", + "notebooks.binding.cut_all_right": "Cut all right", + "notebooks.binding.cut_word_left": "Cut word left", + "notebooks.binding.cut_word_right": "Cut word right", + "notebooks.binding.decrease_font_size": "Decrease notebook font size", + "notebooks.binding.decrease_font_size_short": "Decrease font size", + "notebooks.binding.delete_all_left": "Delete all left", + "notebooks.binding.delete_all_right": "Delete all right", + "notebooks.binding.delete_word_left": "Delete word left", + "notebooks.binding.delete_word_right": "Delete word right", + "notebooks.binding.deselect_shell_commands": "De-select shell commands", + "notebooks.binding.end": "End", + "notebooks.binding.find_in_notebook": "Find in Notebook", + "notebooks.binding.focus_next_match": "Focus next match", + "notebooks.binding.focus_previous_match": "Focus previous match", + "notebooks.binding.focus_terminal_input_from_file": "Focus Terminal Input from File", + "notebooks.binding.focus_terminal_input_from_notebook": "Focus Terminal Input from Notebook", + "notebooks.binding.home": "Home", + "notebooks.binding.increase_font_size": "Increase notebook font size", + "notebooks.binding.increase_font_size_short": "Increase font size", + "notebooks.binding.log_editor_state": "Log editor state", + "notebooks.binding.move_backward_one_word": "Move backward one word", + "notebooks.binding.move_cursor_down": "Move cursor down", + "notebooks.binding.move_cursor_left": "Move cursor left", + "notebooks.binding.move_cursor_right": "Move cursor right", + "notebooks.binding.move_cursor_up": "Move cursor up", + "notebooks.binding.move_forward_one_word": "Move forward one word", + "notebooks.binding.move_to_paragraph_end": "Move to end of paragraph", + "notebooks.binding.move_to_paragraph_start": "Move to start of paragraph", + "notebooks.binding.reload_file": "Reload file", + "notebooks.binding.remove_previous_character": "Remove the previous character", + "notebooks.binding.reset_font_size": "Reset notebook font size", + "notebooks.binding.run_selected_commands": "Run selected commands", + "notebooks.binding.select_down": "Select down", + "notebooks.binding.select_next_command": "Select next command", + "notebooks.binding.select_one_character_left": "Select one character to the left", + "notebooks.binding.select_one_character_right": "Select one character to the right", + "notebooks.binding.select_one_word_left": "Select one word to the left", + "notebooks.binding.select_one_word_right": "Select one word to the right", + "notebooks.binding.select_previous_command": "Select previous command", + "notebooks.binding.select_shell_command_at_cursor": "Select shell command at cursor", + "notebooks.binding.select_to_line_end": "Select to line end", + "notebooks.binding.select_to_line_start": "Select to line start", + "notebooks.binding.select_to_paragraph_end": "Select to end of paragraph", + "notebooks.binding.select_to_paragraph_start": "Select to start of paragraph", + "notebooks.binding.select_up": "Select up", + "notebooks.binding.toggle_case_sensitive_search": "Toggle case-sensitive search", + "notebooks.binding.toggle_debug_mode": "Toggle rich-text debug mode", + "notebooks.binding.toggle_inline_code": "Toggle inline code styling", + "notebooks.binding.toggle_regex_search": "Toggle regular expression search", + "notebooks.binding.toggle_strikethrough": "Toggle strikethrough styling", + "notebooks.binding.toggle_underline": "Toggle underline styling", + "notebooks.block.bulleted_list": "Bulleted list", + "notebooks.block.code": "Code", + "notebooks.block.command": "Command", + "notebooks.block.heading": "Heading {level}", + "notebooks.block.numbered_list": "Numbered list", + "notebooks.block.text": "Text", + "notebooks.block.todo_list": "To-do list", + "notebooks.command.open_full_screen": "Open full screen", + "notebooks.command.raw": "Raw", + "notebooks.command.rendered": "Rendered", + "notebooks.command.run_in_terminal": "Run in terminal", + "notebooks.copy_all": "Copy All", + "notebooks.copy_all_tooltip": "Copy notebook contents to your clipboard", + "notebooks.copy_to_personal": "Copy to Personal", + "notebooks.copy_to_personal_tooltip": "Copy notebook contents into your personal workspace", + "notebooks.editor_is_editing": "{editor} is editing", + "notebooks.error.content_contains_secrets": "This notebook cannot be saved because its content contains secrets", + "notebooks.error.title_contains_secrets": "This notebook cannot be saved because its title contains secrets", + "notebooks.file.could_not_read": "Could not read {source}", + "notebooks.file.loading": "Loading {source}...", + "notebooks.file.missing_source_file": "Missing source file", + "notebooks.file.open_in_editor": "Open in editor", + "notebooks.file.refresh_file": "Refresh file", + "notebooks.insert.divider": "Divider", + "notebooks.insert.embed": "Embed", + "notebooks.insert.insert_block": "Insert block", + "notebooks.link_editor.apply_link": "Apply link", + "notebooks.link_editor.link_placeholder": "Link (web or file)", + "notebooks.link.edit_markdown_file": "Edit Markdown file", + "notebooks.link.error.broken_file_link": "Broken file link", + "notebooks.link.error.file_not_found": "File not found", + "notebooks.link.error.no_base_directory": "No base directory", + "notebooks.link.new_session": "New session", + "notebooks.link.new_session_tooltip": "Open a new terminal session in this directory", + "notebooks.link.open_in_terminal_session": "Open in terminal session", + "notebooks.move_to_space": "Move to {space}", + "notebooks.moved_to_trash": "Notebook was moved to trash", + "notebooks.no_longer_access": "You no longer have access to this notebook", + "notebooks.refresh": "Refresh notebook", + "notebooks.restore_from_trash": "Restore notebook from trash", + "notebooks.sync.conflict": "This notebook could not be saved because changes were made while you were editing. Please copy your work and refresh.", + "notebooks.sync.feature_not_available": "This notebook could not be saved to the server because the feature is temporarily unavailable. The changes are saved locally. Please retry later.", + "notebooks.workflow.command_from": "Command from {source}", + "onboarding.agent.autonomy": "Autonomy", + "onboarding.agent.autonomy_set_by_team_workspace": "Set by Team Workspace", + "onboarding.agent.autonomy_team_workspace_description": "Autonomy settings are configured as part of your team workspace.", + "onboarding.agent.autonomy.full": "Full", + "onboarding.agent.autonomy.full_description": "Runs commands, writes code, and reads files without asking.", + "onboarding.agent.autonomy.none": "None", + "onboarding.agent.autonomy.none_description": "Takes no actions without your approval.", + "onboarding.agent.autonomy.partial": "Partial", + "onboarding.agent.autonomy.partial_description": "Can plan, read files, and execute low-risk commands. Asks before making any changes or executing sensitive commands.", + "onboarding.agent.default_model": "Default model", + "onboarding.agent.disable_warp_agent": "Disable Warp Agent", + "onboarding.agent.plan_activated": "Plan successfully activated. All premium models are available.", + "onboarding.agent.premium": "Premium", + "onboarding.agent.recommended": "Recommended", + "onboarding.agent.subtitle": "Select your in-app agent's defaults.", + "onboarding.agent.title": "Customize your Warp Agent", + "onboarding.agent.upgrade_banner.subtitle": "State-of-the-art models require paid plans.", + "onboarding.agent.upgrade_banner.title": "Upgrade for access to premium models.", + "onboarding.agent.upgrade.browser_not_launched_prefix": "If your browser hasn't launched, ", + "onboarding.agent.upgrade.click_here": "Click here", + "onboarding.agent.upgrade.copy_url": "copy the URL", + "onboarding.agent.upgrade.open_page_manually": " and open the page manually. ", + "onboarding.agent.upgrade.paste_token_suffix": " to paste your token from the browser.", + "onboarding.callout.agent_mode.no_project_text": "Agent mode gives your questions and tasks their own conversation, so you can ask follow-ups without leaving your terminal workflow. Press {keybinding} to return to terminal mode at any point.", + "onboarding.callout.agent_mode.title": "You're in agent mode", + "onboarding.callout.agent_mode.with_project_text": "Agent mode gives your questions and tasks their own conversation, so you can ask follow-ups without leaving your terminal workflow.\n\nSubmit the query below to have the agent initialize this project, or ⊗ to clear the input and start your own!", + "onboarding.callout.back_to_terminal": "Back to terminal", + "onboarding.callout.enable_natural_language_detection": "Enable Natural Language Detection", + "onboarding.callout.initialize": "Initialize", + "onboarding.callout.meet_input.text": "Your terminal input accepts both terminal commands and agent prompts and automatically detects which you're using. Use {keybinding} to lock the input to Agent mode (natural language) or Terminal mode (commands).", + "onboarding.callout.meet_input.title": "Meet the Warp input", + "onboarding.callout.skip_initialization": "Skip initialization", + "onboarding.callout.talk_to_agent.text": "You can type in natural language to engage the agent. Submit the query below to start: What tests exist in this repo, how are they structured, and what do they cover?", + "onboarding.callout.talk_to_agent.title": "Talk to the agent", + "onboarding.callout.terminal_mode.in_terminal_title": "You’re in terminal mode", + "onboarding.callout.terminal_mode.text": "Run commands here, just like a regular terminal. If you type a question or task using natural language, Warp can suggest opening it in agent mode. You can always override using {keybinding}.", + "onboarding.callout.terminal_mode.welcome_title": "Welcome to terminal mode", + "onboarding.common.disabled": "Disabled", + "onboarding.common.enabled": "Enabled", + "onboarding.common.submit": "Submit", + "onboarding.customize.code_review": "Code review", + "onboarding.customize.conversation_history": "Conversation history", + "onboarding.customize.file_explorer": "File explorer", + "onboarding.customize.global_file_search": "Global file search", + "onboarding.customize.horizontal": "Horizontal", + "onboarding.customize.subtitle": "Tailor your features and UI to your working style.", + "onboarding.customize.tab_styling": "Tab styling", + "onboarding.customize.title": "Customize your Warp", + "onboarding.customize.tools_panel": "Tools panel", + "onboarding.customize.vertical": "Vertical", + "onboarding.features.agents_over_ssh": "Agents over SSH", + "onboarding.features.codebase_context": "Codebase context", + "onboarding.features.next_command_predictions": "Next command predictions", + "onboarding.features.oz_cloud_agents_platform": "Oz cloud agents platform", + "onboarding.features.prompt_suggestions": "Prompt suggestions", + "onboarding.features.remote_control": "Remote control with Claude Code, Codex, and other agents", + "onboarding.features.session_sharing": "Session Sharing", + "onboarding.features.warp_agents": "Warp agents", + "onboarding.features.warp_drive": "Warp Drive", + "onboarding.free_user.agent_option.description": "Iterate, plan, and build with Oz: Warp's built-in agent. Available locally or in the cloud.", + "onboarding.free_user.agent_option.title": "Agent driven development with Warp's built-in agent", + "onboarding.free_user.subscribe": "Subscribe", + "onboarding.free_user.subscribe_item.cloud_agents": "Extended cloud agents access", + "onboarding.free_user.subscribe_item.cloud_storage": "Unlimited cloud conversation storage", + "onboarding.free_user.subscribe_item.credits": "1,500 credits per month", + "onboarding.free_user.subscribe_item.email_support": "Private email support", + "onboarding.free_user.subscribe_item.frontier_models": "Access to frontier OpenAI, Anthropic, and Google models", + "onboarding.free_user.subscribe_item.indexing_limits": "Highest codebase indexing limits", + "onboarding.free_user.subscribe_item.reload_credits": "Access to Reload credits and volume-based discounts", + "onboarding.free_user.subscribe_item.warp_drive": "Unlimited Warp Drive objects and collaboration", + "onboarding.free_user.subscribe_title": "Subscribe to access agent driven development in Warp.", + "onboarding.free_user.terminal_option.description": "A modern terminal that supports third-party agents (Claude Code, Codex, Gemini CLI) and classic terminal workflows.", + "onboarding.free_user.terminal_option.title": "Classic terminal with third-party agents", + "onboarding.free_user.title": "Let's get started.", + "onboarding.get_warping": "Get Warping", + "onboarding.intention.agent.description": "An agent-first experience with best in class terminal support. Get terminal and agent driven development AI features like:", + "onboarding.intention.agent.title": "Build faster with AI agents", + "onboarding.intention.subtitle": "How do you want to work?", + "onboarding.intention.terminal.description": "A modern terminal optimized for speed, context, and control without AI.", + "onboarding.intention.terminal.no_ai_features": "No AI features", + "onboarding.intention.terminal.title": "Just use the terminal", + "onboarding.intention.title": "Welcome to Warp", + "onboarding.intro.already_have_account_prefix": "Already have an account? ", + "onboarding.intro.get_started": "Get started", + "onboarding.intro.log_in": "Log in", + "onboarding.intro.subtitle": "A modern terminal with state of the art agents built in.", + "onboarding.intro.welcome_to_warp": "Welcome to Warp", + "onboarding.project.initialize_automatically": "Initialize project automatically", + "onboarding.project.initialize_automatically_description": "Prepares the project environment, builds an index of your code, and generates project rules—giving the agent deeper understanding and better performance.", + "onboarding.project.open_local_folder": "Open local folder", + "onboarding.project.subtitle": "Set up a project to optimize it for coding in Warp.", + "onboarding.project.title": "Open a project", + "onboarding.theme.analytics_opt_out_prefix": "If you'd like to opt out of analytics, you can adjust your ", + "onboarding.theme.privacy_settings": "Privacy Settings", + "onboarding.theme.subtitle": "Click or use arrow keys to select, Enter to confirm.", + "onboarding.theme.terms_of_service": "Terms of Service", + "onboarding.theme.title": "Choose a theme", + "onboarding.theme.tos_prefix": "By continuing, you agree to Warp's ", + "onboarding.third_party.cli_agent_toolbar": "CLI agent toolbar", + "onboarding.third_party.notifications": "Notifications", + "onboarding.third_party.subtitle": "Select defaults for using agents like Claude Code, Codex, and Gemini.", + "onboarding.third_party.title": "Customize third party agents", + "pane_group.binding.add_repository": "Add repository", + "pane_group.binding.share_pane": "Share pane", + "pane_group.binding.terminal_session": "Terminal session", + "pane_group.default_shell_unsupported": "Warp doesn't currently support your default shell, falling back to zsh. ", + "pane_group.get_started.tagline": "The Agentic Development Environment", + "pane_group.get_started.title": "Get started", + "pane_group.get_started.welcome": "Welcome to Warp", + "pane_group.header.read_only": "Read-only", + "pane_group.header.toolbelt_feature_popup": "Open files and review code diffs", + "pane_group.header.unsharable_conversation_tooltip": "This conversation cannot be shared because it is not stored in the cloud.\nTo sync to cloud and share, enable the setting under Settings > Privacy, and then make another request.", + "pane_group.local_child.failed_create_hidden_agent_pane": "Failed to create a hidden pane for the local child agent.", + "pane_group.local_child.failed_create_hidden_harness_pane": "Failed to create a hidden pane for the local child harness.", + "pane_group.local_child.failed_create_task": "Failed to create local child task: {error}", + "pane_group.local_child.missing_harness_type": "Local child harness type is missing.", + "pane_group.local_child.missing_working_directory": "Could not resolve a working directory for the local {harness} child.", + "pane_group.local_child.no_supported_shell": "Local child harnesses currently require a detected bash, zsh, or fish session.", + "pane_group.local_child.powershell_unsupported": "Local child harnesses currently require bash, zsh, or fish; PowerShell is not supported.", + "pane_group.local_child.unsupported_harness": "Unsupported local child harness '{harness}'.", + "pane_group.welcome.title": "New tab", + "prompt.editor.same_line_prompt": "Same line prompt", + "prompt.editor.save_changes": "Save changes", + "prompt.editor.separator": "Separator", + "prompt.editor.shell_prompt": "Shell prompt (PS1)", + "prompt.editor.title": "Edit prompt", + "prompt.editor.warp_terminal_prompt": "Warp terminal prompt", + "quit_warning.button.cancel": "Cancel", + "quit_warning.button.dont_save": "Don't Save", + "quit_warning.button.save": "Save", + "quit_warning.button.show_running_processes": "Show running processes", + "quit_warning.button.yes_close": "Yes, close", + "quit_warning.button.yes_quit": "Yes, quit", + "quit_warning.file.this_file": "this file", + "quit_warning.process.in_tabs": " in {count} tabs", + "quit_warning.process.in_windows": " in {count} windows", + "quit_warning.process.running_many": "You have {count} processes running", + "quit_warning.process.running_one": "You have {count} process running", + "quit_warning.scope.default": ".", + "quit_warning.scope.this_pane": " in this pane.", + "quit_warning.scope.this_tab": " in this tab.", + "quit_warning.scope.this_window": " in this window.", + "quit_warning.shared_session.many": "You are sharing {count} sessions{scope}", + "quit_warning.shared_session.one": "You are sharing {count} session{scope}", + "quit_warning.title.close_pane": "Close pane?", + "quit_warning.title.close_tab": "Close tab?", + "quit_warning.title.close_tabs": "Close tabs?", + "quit_warning.title.close_window": "Close window?", + "quit_warning.title.quit_warp": "Quit Warp?", + "quit_warning.title.save_changes": "Save changes?", + "quit_warning.unsaved_file.changes": "You have unsaved file changes{scope}", + "quit_warning.unsaved_file.save_changes": "Do you want to save the changes you made to {file}? Your changes will be discarded if you don't save them.", + "remote_server.codebase_index.indexing_not_started": "Cannot index remote codebase because indexing did not start.", + "remote_server.codebase_index.max_indices_reached": "Cannot index remote codebase because the maximum number of codebase indexes has been reached.", + "remote_server.codebase_index.missing_root_hash": "The remote codebase index is missing its root hash.", + "remote_server.codebase_index.remote_host_disconnected": "The remote host is currently disconnected.", + "remote_server.codebase_index.resync_not_indexed": "Cannot resync remote codebase because it has not been indexed.", + "remote_server.codebase_index.search_unavailable": "Remote codebase search is not available.", + "resource_center.changelog.bug_fixes": "Bug fixes", + "resource_center.changelog.fetch_error": "Unable to fetch the latest changelog.", + "resource_center.changelog.improvements": "Improvements", + "resource_center.changelog.new_features": "New features", + "resource_center.changelog.read_all": "Read all changelogs", + "resource_center.content.custom_prompt.description": "Set up Warp to honor your PS1 setting", + "resource_center.content.custom_prompt.title": "Use your custom prompt", + "resource_center.content.how_warp_uses_warp.description": "Learn how Warp's engineering team uses their favorite features", + "resource_center.content.how_warp_uses_warp.title": "How Warp uses Warp", + "resource_center.content.ide_integration.description": "Configure Warp to launch from your most used development tools", + "resource_center.content.ide_integration.title": "Integrate Warp with your IDE", + "resource_center.content.read_article": "Read article", + "resource_center.content.view_documentation": "View documentation", + "resource_center.feature.ai_command_search.description": "Generate shell commands with natural language.", + "resource_center.feature.ai_command_search.title": "AI command search", + "resource_center.feature.block_action.description": "Right click on a block to copy/paste, share, more.", + "resource_center.feature.block_action.title": "Take an action on block", + "resource_center.feature.command_palette.description": "Access all of Warp via the keyboard.", + "resource_center.feature.command_palette.title": "Open command palette", + "resource_center.feature.command_search.description": "Find and run previously executed commands, workflows, and more.", + "resource_center.feature.command_search.title": "Command search", + "resource_center.feature.create_first_block.description": "Run a command to see your command and output grouped.", + "resource_center.feature.create_first_block.title": "Create your first block", + "resource_center.feature.launch_configuration.description": "Save your current configuration of windows, tabs, and panes.", + "resource_center.feature.launch_configuration.title": "Launch configuration", + "resource_center.feature.navigate_blocks.description": "Click to select a block and navigate with arrow keys.", + "resource_center.feature.navigate_blocks.title": "Navigate blocks", + "resource_center.feature.split_panes.description": "Split tabs into multiple panes to make your ideal layout.", + "resource_center.feature.split_panes.title": "Split panes", + "resource_center.feature.theme_picker.description": "Make Warp your own by choosing a theme.", + "resource_center.feature.theme_picker.title": "Set your theme", + "resource_center.footer.docs": "Docs", + "resource_center.footer.feedback": "Feedback", + "resource_center.footer.slack": "Slack", + "resource_center.header.keyboard_shortcuts": "Keyboard Shortcuts", + "resource_center.header.warp_essentials": "Warp Essentials", + "resource_center.invite_friend": "Invite a friend to Warp", + "resource_center.keybindings.additional.hide_others": "Hide Others", + "resource_center.keybindings.additional.hide_warp": "Hide Warp", + "resource_center.keybindings.additional.minimize": "Minimize", + "resource_center.keybindings.additional.open_new_window": "Open New Window", + "resource_center.keybindings.additional.quit_warp": "Quit Warp", + "resource_center.keybindings.blocks": "Blocks", + "resource_center.keybindings.essentials": "Essentials", + "resource_center.keybindings.fundamentals": "Fundamentals", + "resource_center.keybindings.input_editor": "Input Editor", + "resource_center.keybindings.settings_hint": "Go to settings > keyboard shortcuts to configure custom keybindings", + "resource_center.keybindings.settings_link": "here.", + "resource_center.keybindings.terminal": "Terminal", + "resource_center.keybindings.toggle_hint": "To toggle this panel", + "resource_center.mark_all_as_read": "Mark all as read", + "resource_center.section.advanced_setup": "Advanced Setup", + "resource_center.section.getting_started": "Getting Started", + "resource_center.section.maximize_warp": "Maximize Warp", + "resource_center.section.whats_new": "What's New?", + "root_view.binding.hide_all_windows": "Hide All Windows", + "root_view.binding.hide_dedicated_hotkey_window": "Hide Dedicated Hotkey Window", + "root_view.binding.show_dedicated_hotkey_window": "Show Dedicated Hotkey Window", + "root_view.binding.toggle_fullscreen": "Toggle fullscreen", + "root_view.create_environment": "Create Environment", + "root_view.resource_not_found": "Resource not found or access denied", + "search.a11y.error_finding_results": "Error finding results", + "search.a11y.loading_suggestions_prefix": "Loading suggestions for", + "search.a11y.press_enter_to_confirm": "Press enter to confirm.", + "search.a11y.press_enter_to_launch_session": "Press enter to launch this session.", + "search.a11y.press_enter_to_navigate_session": "Press enter to navigate to this session.", + "search.a11y.selected_prefix": "Selected", + "search.a11y.use_binding_prefix": "Use", + "search.a11y.use_binding_suffix": "binding to run this action in the future.", + "search.ai_context_menu.blocks.no_output": "No output", + "search.ai_context_menu.blocks.prefix": "Block", + "search.ai_context_menu.category.blocks": "Blocks", + "search.ai_context_menu.category.code": "Code", + "search.ai_context_menu.category.commands": "Commands", + "search.ai_context_menu.category.conversations": "Conversations", + "search.ai_context_menu.category.diff_set": "Diff sets", + "search.ai_context_menu.category.diffs": "Diffs", + "search.ai_context_menu.category.docs": "Docs", + "search.ai_context_menu.category.files_and_folders": "Files and folders", + "search.ai_context_menu.category.notebooks": "Notebooks", + "search.ai_context_menu.category.plans": "Plans", + "search.ai_context_menu.category.recent_block": "Most recent block", + "search.ai_context_menu.category.recent_diff": "Most recent diff", + "search.ai_context_menu.category.rules": "Rules", + "search.ai_context_menu.category.servers": "Servers and integrations", + "search.ai_context_menu.category.skills": "Skills", + "search.ai_context_menu.category.tasks": "Past tasks", + "search.ai_context_menu.category.terminal": "Terminal", + "search.ai_context_menu.category.web": "Web", + "search.ai_context_menu.category.workflows": "Workflows", + "search.ai_context_menu.code_search_failed": "Code search failed", + "search.ai_context_menu.code_symbol_in": "in", + "search.ai_context_menu.code_symbol_prefix": "Code symbol", + "search.ai_context_menu.code_symbols_indexing": "Code symbols indexing...", + "search.ai_context_menu.command_prefix": "Command", + "search.ai_context_menu.conversation_prefix": "Conversation", + "search.ai_context_menu.diffset.changes_vs_main": "Changes vs. main branch", + "search.ai_context_menu.diffset.changes_vs_prefix": "Changes vs. ", + "search.ai_context_menu.diffset.compared_to_prefix": "All changes compared to ", + "search.ai_context_menu.diffset.main_desc": "All changes compared to the main branch", + "search.ai_context_menu.diffset.uncommitted_changes": "Uncommitted changes", + "search.ai_context_menu.diffset.uncommitted_desc": "All uncommitted changes in the working directory", + "search.ai_context_menu.directory_prefix": "Directory", + "search.ai_context_menu.file_prefix": "File", + "search.ai_context_menu.loading_results": "Loading results...", + "search.ai_context_menu.no_results": "No results found", + "search.ai_context_menu.notebook_prefix": "Notebook", + "search.ai_context_menu.rule_prefix": "Rule", + "search.ai_context_menu.skill_prefix": "Skill", + "search.ai_context_menu.time.days_ago_suffix": " days ago", + "search.ai_context_menu.time.hours_ago_suffix": " hours ago", + "search.ai_context_menu.time.just_now": "Just now", + "search.ai_context_menu.time.minutes_ago_suffix": " minutes ago", + "search.ai_context_menu.workflow_prefix": "Workflow", + "search.command_palette.cannot_start_conversation": "Cannot start a new conversation while agent is monitoring a command.", + "search.command_palette.cannot_switch_conversations": "Cannot switch conversations while agent is monitoring a command.", + "search.command_palette.placeholder": "Search for a command", + "search.command_palette.tabs.title_with_index": "[Tab {index}] {title}", + "search.command_search.a11y.help": "Search your history, workflows, and more. Use the Up and Down arrows to browse search results after typing. Press Enter to accept a selected result, inserting it into the terminal input. Press Escape to close.", + "search.command_search.a11y.result_accepted": "Result accepted.", + "search.command_search.a11y.result_accepted_help": "You can edit the command here before pressing Enter to execute it.", + "search.command_search.a11y.result_executed": "Result executed", + "search.command_search.a11y.result_executed_help": "Press Cmd-Up to navigate to the command's output.", + "search.command_search.a11y.title": "Command Search", + "search.command_search.loading": "Loading...", + "search.command_search.no_results": "No results found.", + "search.command_search.out_of_credits_contact_admin": "Looks like you're out of credits. Contact a team admin to upgrade for more credits.", + "search.command_search.out_of_credits_prefix": "Looks like you're out of credits. ", + "search.command_search.out_of_credits_suffix": " for more credits.", + "search.command_search.placeholder": "Search your history, workflows, and more", + "search.command_search.result.ai_query_prefix": "AI query", + "search.command_search.result.environment_variables_prefix": "Environment Variables", + "search.command_search.result.history_item_prefix": "History item", + "search.command_search.result.notebook_prefix": "Notebook", + "search.command_search.result.project_prefix": "Project", + "search.command_search.result.ran_prefix": "Ran", + "search.command_search.result.untitled": "Untitled", + "search.command_search.result.workflow_prefix": "Workflow", + "search.command_search.upgrade": "Upgrade", + "search.command_search.warp_ai.error.bad_prompt": "No results found. Please try again with a more specific query.", + "search.command_search.warp_ai.error.provider_error": "Something went wrong. Please try again.", + "search.command_search.warp_ai.error.rate_limited": "Looks like you're out of AI credits. Please try again later.", + "search.command_search.warp_ai.open_body": "Ask Warp AI for command suggestions", + "search.command_search.warp_ai.prefix": "Warp AI", + "search.command_search.warp_ai.translate_body": "Translate into shell command using Warp AI", + "search.command_search.zero_state.example_queries": "Example queries", + "search.command_search.zero_state.looking_for": "I'm looking for...", + "search.command_search.zero_state.title": "Command Search", + "search.conversations.conversation": "Conversation", + "search.conversations.fork_conversation_tooltip": "Fork conversation", + "search.conversations.fork_current_conversation": "Fork current conversation", + "search.conversations.new_conversation": "New conversation", + "search.conversations.press_enter_to_create": "Press enter to create a new conversation.", + "search.conversations.press_enter_to_fork": "Press enter to fork the current conversation into a new conversation.", + "search.conversations.press_enter_to_navigate": "Press enter to navigate to conversation", + "search.conversations.section.active_pane": "Active pane conversations", + "search.conversations.section.other_active": "Other active conversations", + "search.conversations.section.past": "Past conversations", + "search.external_secrets.placeholder": "Search for a secret", + "search.external_secrets.secret_prefix": "Secret", + "search.files.create_file": "Create file", + "search.files.create_file_named": "Create a file named", + "search.files.directory": "Directory", + "search.files.file": "File", + "search.files.press_enter_to_create_prefix": "Press Enter to create", + "search.files.press_enter_to_create_suffix": "in the current directory", + "search.files.press_enter_to_navigate_directory": "Press Enter to navigate to this directory", + "search.files.press_enter_to_open_file": "Press Enter to open this file", + "search.filter.actions": "actions", + "search.filter.agent_mode_workflows": "prompts", + "search.filter.base_models": "base models", + "search.filter.blocks": "blocks", + "search.filter.code": "code", + "search.filter.commands": "commands", + "search.filter.conversations": "conversations", + "search.filter.current_directory_conversations": "current directory conversations", + "search.filter.diff_sets": "diff sets", + "search.filter.drive": "Warp Drive", + "search.filter.environment_variables": "environment variables", + "search.filter.files": "files", + "search.filter.full_terminal_use_models": "full terminal use models", + "search.filter.history": "history", + "search.filter.launch_configurations": "launch configurations", + "search.filter.natural_language": "AI command suggestions", + "search.filter.notebooks": "notebooks", + "search.filter.plans": "plans", + "search.filter.prompt_history": "prompt history", + "search.filter.repos": "repos", + "search.filter.rules": "rules", + "search.filter.sessions": "sessions", + "search.filter.skills": "skills", + "search.filter.static_slash_commands": "slash commands", + "search.filter.tabs": "tabs", + "search.filter.workflows": "workflows", + "search.launch_config.press_enter_to_use_launch_configuration": "Press enter to use this launch configuration.", + "search.navigation.completed": "Completed", + "search.navigation.completed_over_hour": "Completed over 1 hour ago", + "search.navigation.completed_prefix": "Completed", + "search.navigation.empty_session": "Empty Session", + "search.navigation.minute_ago": "minute ago", + "search.navigation.minutes_ago": "minutes ago", + "search.navigation.no_timestamp": "No timestamp found", + "search.navigation.running": "Running...", + "search.new_session.create_new_tab": "Create New Tab", + "search.new_session.create_new_window": "Create New Window", + "search.new_session.split_pane_down": "Split Pane Down", + "search.new_session.split_pane_left": "Split Pane Left", + "search.new_session.split_pane_right": "Split Pane Right", + "search.new_session.split_pane_up": "Split Pane Up", + "search.no_results": "No results found", + "search.no_results_found": "No results found", + "search.no_results_found_period": "No results found.", + "search.not_visible_to_other_users": "Not visible to other users", + "search.notebook_embedding.placeholder": "Search for a reference", + "search.placeholder.actions": "Search actions", + "search.placeholder.agent_mode_workflows": "Search prompts", + "search.placeholder.base_models": "Search base models", + "search.placeholder.blocks": "Search blocks", + "search.placeholder.code": "Search code symbols", + "search.placeholder.commands": "Search commands", + "search.placeholder.conversations": "Search conversations", + "search.placeholder.current_directory_conversations": "Search conversations in current directory", + "search.placeholder.diff_sets": "Search diff sets", + "search.placeholder.drive": "Search objects in drive", + "search.placeholder.environment_variables": "Search environment variables", + "search.placeholder.files": "Search files", + "search.placeholder.full_terminal_use_models": "Search full terminal use models", + "search.placeholder.history": "Search history", + "search.placeholder.launch_configurations": "Search launch configurations", + "search.placeholder.natural_language": "e.g. replace string in file", + "search.placeholder.notebooks": "Search notebooks", + "search.placeholder.plans": "Search plans", + "search.placeholder.prompt_history": "Search prompt history", + "search.placeholder.repos": "Search code repos", + "search.placeholder.rules": "Search AI rules", + "search.placeholder.sessions": "Search sessions", + "search.placeholder.skills": "Search skills", + "search.placeholder.static_slash_commands": "Search static slash commands", + "search.placeholder.tabs": "Search tabs", + "search.placeholder.workflows": "Search workflows", + "search.repos.repo": "Repo", + "search.search_results_menu.prompts": "Prompts", + "search.separator.section": "Section", + "search.slash_command.description.add_mcp": "Add a new MCP server via the MCP settings page", + "search.slash_command.description.add_prompt": "Add new Agent prompt", + "search.slash_command.description.add_rule": "Add a new global rule for the agent", + "search.slash_command.description.agent": "Start a new conversation", + "search.slash_command.description.changelog": "Open the latest changelog", + "search.slash_command.description.cloud_agent": "Start a new cloud agent conversation", + "search.slash_command.description.compact": "Free up context by summarizing convo history", + "search.slash_command.description.compact_and": "Compact conversation and then send a follow-up prompt", + "search.slash_command.description.continue_locally": "Continue this cloud conversation locally", + "search.slash_command.description.conversations": "Open conversation history", + "search.slash_command.description.cost": "Toggle credit usage details", + "search.slash_command.description.create_docker_sandbox": "Create a new Docker sandbox terminal session", + "search.slash_command.description.create_environment": "Create an Oz environment (Docker image + repos) via guided setup", + "search.slash_command.description.create_new_project": "Have Oz walk you through creating a new coding project", + "search.slash_command.description.edit": "Open a file in Warp's code editor", + "search.slash_command.description.edit_skill": "Open a skill's markdown file in Warp's built-in editor", + "search.slash_command.description.environment": "Switch the cloud agent environment", + "search.slash_command.description.export_to_clipboard": "Export current conversation to clipboard in markdown format", + "search.slash_command.description.export_to_file": "Export current conversation to a markdown file", + "search.slash_command.description.feedback": "Send feedback", + "search.slash_command.description.fork": "Fork the current conversation in a new pane or a new tab", + "search.slash_command.description.fork_and_compact": "Fork current conversation and compact it in the forked copy", + "search.slash_command.description.fork_from": "Fork conversation from a specific query", + "search.slash_command.description.harness": "Switch the cloud agent harness", + "search.slash_command.description.host": "Switch the cloud agent execution host", + "search.slash_command.description.index": "Index this codebase", + "search.slash_command.description.init": "Index this codebase and generate an AGENTS.md file", + "search.slash_command.description.invoke_skill": "Invoke a skill", + "search.slash_command.description.model": "Switch the base agent model", + "search.slash_command.description.move_to_cloud": "Hand off this conversation to a cloud agent", + "search.slash_command.description.new": "Start a new conversation (alias for /agent)", + "search.slash_command.description.open_code_review": "Open code review", + "search.slash_command.description.open_mcp_servers": "Open MCP servers", + "search.slash_command.description.open_project_rules": "Open the project rules file (AGENTS.md)", + "search.slash_command.description.open_repo": "Switch to another indexed repository", + "search.slash_command.description.open_rules": "View all of your global and project rules", + "search.slash_command.description.open_settings_file": "Open settings file (TOML)", + "search.slash_command.description.orchestrate": "Break a task into subtasks and run them in parallel with multiple agents", + "search.slash_command.description.plan": "Prompt the agent to do some research and create a plan for a task", + "search.slash_command.description.pr_comments": "Pull GitHub PR review comments", + "search.slash_command.description.profile": "Switch the active execution profile", + "search.slash_command.description.prompts": "Search saved prompts", + "search.slash_command.description.queue": "Queue a prompt to send after the agent finishes responding", + "search.slash_command.description.remote_control": "Start remote control for this session", + "search.slash_command.description.rename_tab": "Rename the current tab", + "search.slash_command.description.rewind": "Rewind to a previous point in the conversation", + "search.slash_command.description.set_tab_color": "Set the color of the current tab", + "search.slash_command.description.usage": "Open billing and usage settings", + "search.slash_command.prefix": "Slash command", + "search.tabs.press_enter_to_navigate": "Press enter to navigate to tab", + "search.tabs.tab": "tab", + "search.untitled": "Untitled", + "search.warp_drive.environment_variables": "Environment Variables", + "search.warp_drive.notebook": "Notebook", + "search.warp_drive.workflow": "Workflow", + "search.welcome_palette.add_repository": "Add repository", + "search.welcome_palette.no_results": "No results found", + "search.welcome_palette.placeholder": "Code, build, or search for anything...", + "search.welcome_palette.terminal_session": "Terminal session", + "search.zero_state.recent": "Recent", + "search.zero_state.suggested": "Suggested", + "server.iap.credential_refresh_failed": "IAP credential refresh failed: {message}", + "server.network_log.refresh": "Refresh", + "server.network_log.title": "Network log", + "session_management.a11y.currently_running_ai_interaction": "Currently running AI interaction: {prompt}", + "session_management.a11y.currently_running_command": "Currently running {command}", + "session_management.a11y.last_ai_interaction": "Last AI interaction: {prompt}", + "session_management.a11y.last_run_command": "Last run command {command}", + "settings.about.copyright": "Copyright 2026 Warp", + "settings.account.compare_plans": "Compare plans", + "settings.account.contact_support": "Contact support", + "settings.account.iap.failed": "Failed: {message}", + "settings.account.iap.loaded_refreshes": "Loaded (refreshes in ~{mins}m)", + "settings.account.iap.not_loaded": "Not yet loaded", + "settings.account.iap.refreshing": "Refreshing…", + "settings.account.iap.title": "Staging IAP credentials", + "settings.account.iap.using_injected_token": "Using injected token (WARP_IAP_TOKEN)", + "settings.account.log_out": "Log out", + "settings.account.manage_billing": "Manage billing", + "settings.account.page_title": "Account", + "settings.account.plan.free": "Free", + "settings.account.refer_a_friend": "Refer a friend", + "settings.account.referral_cta": "Earn rewards by sharing Warp with friends & colleagues", + "settings.account.settings_sync": "Settings sync", + "settings.account.sign_up": "Sign up", + "settings.account.upgrade_to_lightspeed": "Upgrade to Lightspeed plan", + "settings.account.upgrade_to_turbo": "Upgrade to Turbo plan", + "settings.account.version.cannot_install": "A new version of Warp is available but can't be installed", + "settings.account.version.cannot_launch": "A new version of Warp is installed but can't be launched.", + "settings.account.version.check_for_updates": "Check for updates", + "settings.account.version.checking": "checking for update...", + "settings.account.version.downloading": "downloading update...", + "settings.account.version.installed_update": "Installed update", + "settings.account.version.label": "Version", + "settings.account.version.relaunch_warp": "Relaunch Warp", + "settings.account.version.up_to_date": "Up to date", + "settings.account.version.update_available": "Update available", + "settings.account.version.update_manually": "Update Warp manually", + "settings.account.version.updating": "Updating...", + "settings.action.debug_network_status": "debug network status", + "settings.action.disable": "Disable {description}", + "settings.action.enable": "Enable {description}", + "settings.action.hide_in_band_command_blocks": "Hide in-band command blocks", + "settings.action.hide_initialization_block": "Hide initialization block", + "settings.action.in_band_generators_for_new_sessions": "in-band generators for new sessions", + "settings.action.memory_statistics": "memory statistics", + "settings.action.recording_mode": "recording mode", + "settings.action.show_in_band_command_blocks": "Show in-band command blocks", + "settings.action.show_initialization_block": "Show initialization block", + "settings.ai.action.agent_commands_in_history": "agent-executed commands in history", + "settings.ai.action.agent_prompt_autodetection_in_terminal_input": "agent prompt autodetection in terminal input", + "settings.ai.action.code_suggestions": "code suggestions", + "settings.ai.action.coding_agent_toolbar": "coding agent toolbar", + "settings.ai.action.conversation_history_tools_panel": "conversation history in tools panel", + "settings.ai.action.hide_agent_tips": "Hide agent tips", + "settings.ai.action.hide_oz_changelog": "Hide Oz changelog in new agent conversation view", + "settings.ai.action.hide_use_agent_footer": "Hide \"Use Agent\" footer", + "settings.ai.action.model_picker_in_prompt": "model picker in prompt", + "settings.ai.action.rich_input_auto_dismiss_after_submit": "automatic Rich Input dismissal after prompt submission", + "settings.ai.action.rich_input_auto_open_on_agent_start": "automatic Rich Input opening when a coding agent session starts", + "settings.ai.action.rich_input_auto_toggle": "automatic Rich Input show/hide based on agent status", + "settings.ai.action.show_agent_tips": "Show agent tips", + "settings.ai.action.show_oz_changelog": "Show Oz changelog in new agent conversation view", + "settings.ai.action.show_use_agent_footer": "Show \"Use Agent\" footer", + "settings.ai.action.terminal_command_autodetection_in_agent_input": "terminal command autodetection in agent input", + "settings.ai.active_ai.header": "Active AI", + "settings.ai.add_custom_endpoint": "Add custom endpoint", + "settings.ai.add_custom_model": "+ Add custom model", + "settings.ai.add_profile": "Add Profile", + "settings.ai.agent_attribution.desc": "Oz can add attribution to commit messages and pull requests it creates", + "settings.ai.agent_attribution.header": "Agent Attribution", + "settings.ai.agent_attribution.label": "Enable agent attribution", + "settings.ai.agents.desc": "Set the boundaries for how your Agent operates. Choose what it can access, how much autonomy it has, and when it must ask for your approval. You can also fine-tune behavior around natural language input, codebase awareness, and more.", + "settings.ai.agents.header": "Agents", + "settings.ai.api_key.anthropic": "Anthropic API key", + "settings.ai.api_key.google": "Google API key", + "settings.ai.api_key.openai": "OpenAI API key", + "settings.ai.api_keys.header": "API Keys", + "settings.ai.auto_spawn_servers": "Auto-spawn servers from third-party agents", + "settings.ai.autodetect_agent_prompts": "Autodetect agent prompts in terminal input", + "settings.ai.autodetect_terminal_commands": "Autodetect terminal commands in agent input", + "settings.ai.autonomy.read_only": "Read only", + "settings.ai.autonomy.supervised": "Supervised", + "settings.ai.aws_bedrock.auto_login.desc": "When enabled, the login command will run automatically when AWS Bedrock credentials expire.", + "settings.ai.aws_bedrock.auto_login.label": "Automatically run login command", + "settings.ai.aws_bedrock.desc": "Warp loads and sends local AWS CLI credentials for Bedrock-supported models.", + "settings.ai.aws_bedrock.desc_admin_enforced": "Warp loads and sends local AWS CLI credentials for Bedrock-supported models. This setting is managed by your organization.", + "settings.ai.aws_bedrock.label": "Use AWS Bedrock credentials", + "settings.ai.aws_bedrock.login_command": "Login Command", + "settings.ai.aws_bedrock.profile": "AWS Profile", + "settings.ai.aws_bedrock.refresh": "Refresh", + "settings.ai.base_model.desc": "This model serves as the primary engine behind the Warp Agent. It powers most interactions and invokes other models for tasks like planning or code generation when necessary. Warp may automatically switch to alternate models based on model availability or for auxiliary tasks such as conversation summarization.", + "settings.ai.base_model.label": "Base model", + "settings.ai.byok.ask_admin": "Ask your team's admin to upgrade to the Build plan to use your own API keys.", + "settings.ai.byok.contact_sales": "Contact sales", + "settings.ai.byok.contact_sales_suffix": " to enable bringing your own API keys on your Enterprise plan.", + "settings.ai.byok.create_account": "Create an account", + "settings.ai.byok.upgrade_build": "Upgrade to the Build plan", + "settings.ai.byok.upgrade_suffix": " to use your own API keys.", + "settings.ai.cli_agent.auto_dismiss_rich_input": "Auto dismiss Rich Input after prompt submission", + "settings.ai.cli_agent.auto_open_rich_input": "Auto open Rich Input when a coding agent session starts", + "settings.ai.cli_agent.auto_toggle_rich_input.label": "Auto show/hide Rich Input based on agent status", + "settings.ai.cli_agent.command_placeholder": "command (supports regex)", + "settings.ai.cli_agent.commands_enable_toolbar": "Commands that enable the toolbar", + "settings.ai.cli_agent.commands_regex_desc": "Add regex patterns to show the coding agent toolbar for matching commands.", + "settings.ai.cli_agent.header": "Third party CLI agents", + "settings.ai.cli_agent.other": "Other", + "settings.ai.cli_agent.requires_plugin_tooltip": "Requires the Warp plugin for your coding agent", + "settings.ai.cli_agent.select_agent": "Select coding agent", + "settings.ai.cli_agent.show_toolbar": "Show coding agent toolbar", + "settings.ai.cli_agent.toolbar_desc_prefix": "Show a toolbar with quick actions when running coding agents like ", + "settings.ai.cli_agent.toolbar_desc_sep1": ", ", + "settings.ai.cli_agent.toolbar_desc_sep2": ", or ", + "settings.ai.cli_agent.toolbar_desc_suffix": ".", + "settings.ai.cloud_handoff.ampersand.desc": "Type & as the first character to enter cloud handoff compose mode.", + "settings.ai.cloud_handoff.ampersand.label": "Use & to trigger handoff", + "settings.ai.cloud_handoff.auto_before_sleep.desc": "When macOS is about to sleep, automatically moves the most recently focused running local Warp Agent conversation to Cloud Mode so it can keep working.", + "settings.ai.cloud_handoff.auto_before_sleep.label": "Auto-handoff before sleep", + "settings.ai.cloud_handoff.desc": "Hand off local agent conversations to a cloud agent.", + "settings.ai.cloud_handoff.header": "Cloud Handoff", + "settings.ai.cloud_handoff.label": "Cloud handoff", + "settings.ai.cloud_handoff.requires_cloud_convos_tooltip": "Cloud handoff requires cloud conversations to be enabled.", + "settings.ai.codebase_context.desc": "Allow the Warp Agent to generate an outline of your codebase that can be used for context. No code is ever stored on our servers. ", + "settings.ai.codebase_context.label": "Codebase Context", + "settings.ai.command_allowlist.desc": "Regular expressions to match commands that can be automatically executed by the Warp Agent.", + "settings.ai.command_allowlist.label": "Command allowlist", + "settings.ai.command_allowlist.placeholder": "e.g. ls .*", + "settings.ai.command_denylist.desc": "Regular expressions to match commands that the Warp Agent should always ask permission to execute.", + "settings.ai.command_denylist.label": "Command denylist", + "settings.ai.command_denylist.placeholder": "Commands, comma separated", + "settings.ai.command_denylist.regex_placeholder": "e.g. rm .*", + "settings.ai.computer_use.desc": "Enable computer use in cloud agent conversations started from the Warp app.", + "settings.ai.computer_use.label": "Computer use in Cloud Agents", + "settings.ai.context_window.label": "Context window (tokens)", + "settings.ai.conversation_layout.new_tab": "New Tab", + "settings.ai.conversation_layout.split_pane": "Split Pane", + "settings.ai.create_account_prompt": "To use AI features, please create an account.", + "settings.ai.custom_endpoint.url_placeholder": "Please include 'https://'", + "settings.ai.custom_endpoints.label": "Custom endpoints", + "settings.ai.custom_inference.desc": "Use your own API keys from model providers for Warp Agent. You can also add custom endpoints to use third-party models. Custom endpoints must support the OpenAI-compatible Chat Completions API. API keys are stored only on your device, never on Warp's servers. They're used to make requests to your chosen model provider. Using auto models or models from providers you have not provided API keys for will consume Warp credits. ", + "settings.ai.custom_inference.header": "Custom inference", + "settings.ai.custom_inference.terms_link": "Warp's Terms of Service", + "settings.ai.custom_inference.terms_prefix": "By using BYOK or custom endpoints, you agree to use them only as permitted by ", + "settings.ai.custom_inference.terms_suffix": ". BYOK and custom endpoints are intended for individual use and small teams. Companies or organizations with more than 10 employees should use Warp Business or Enterprise.", + "settings.ai.directory_allowlist.desc": "Give the agent file access to certain directories.", + "settings.ai.directory_allowlist.label": "Directory allowlist", + "settings.ai.directory_allowlist.placeholder": "e.g. ~/code-repos/repo", + "settings.ai.edit": "Edit", + "settings.ai.edit_custom_endpoint": "Edit custom endpoint", + "settings.ai.experimental.header": "Experimental", + "settings.ai.file_based_mcp.desc": "Automatically detect and spawn MCP servers from globally-scoped third-party AI agent configuration files (e.g. in your home directory). Servers detected inside a repository are never spawned automatically and must be enabled individually from the MCP settings page. ", + "settings.ai.file_based_mcp.providers_link": "See supported providers.", + "settings.ai.git_operations.desc": "Let AI generate commit messages and pull request titles and descriptions.", + "settings.ai.git_operations.label": "Commit & Pull Request Generation", + "settings.ai.include_agent_commands": "Include agent-executed commands in history", + "settings.ai.incorrect_detection_prompt": "Encountered an incorrect detection? ", + "settings.ai.incorrect_input_detection_prompt": " Encountered an incorrect input detection? ", + "settings.ai.input.header": "Input", + "settings.ai.knowledge.header": "Knowledge", + "settings.ai.learn_more": "Learn more", + "settings.ai.let_us_know": "Let us know", + "settings.ai.manage_mcp_servers": "Manage MCP servers", + "settings.ai.manage_rules": "Manage rules", + "settings.ai.mcp_allowlist.desc": "Allow the Warp Agent to call these MCP servers.", + "settings.ai.mcp_allowlist.label": "MCP allowlist", + "settings.ai.mcp_denylist.desc": "The Warp Agent will always ask for permission before calling any MCP servers on this list.", + "settings.ai.mcp_denylist.label": "MCP denylist", + "settings.ai.mcp_servers.desc": "Add MCP servers to extend the Warp Agent's capabilities. MCP servers expose data sources or tools to agents through a standardized interface, essentially acting like plugins. ", + "settings.ai.mcp_servers.header": "MCP Servers", + "settings.ai.mcp.add_server": "Add a server", + "settings.ai.mcp.call_servers": "Call MCP servers", + "settings.ai.mcp.learn_more_link": "learn more about MCPs.", + "settings.ai.mcp.or": " or ", + "settings.ai.mcp.zero_state_intro": "You haven't added any MCP servers yet. Once you do, you'll be able to control how much autonomy the Warp Agent has when interacting with them. ", + "settings.ai.models.header": "Models", + "settings.ai.natural_language_autosuggestions.desc": "Let AI suggest natural language autosuggestions, based on recent commands and their outputs.", + "settings.ai.natural_language_autosuggestions.label": "Natural Language Autosuggestions", + "settings.ai.next_command.desc": "Let AI suggest the next command to run based on your command history, outputs, and common workflows.", + "settings.ai.next_command.label": "Next Command", + "settings.ai.nld_denylist.desc": "Commands listed here will never trigger natural language detection.", + "settings.ai.nld_denylist.label": "Natural language denylist", + "settings.ai.nld.desc": "Enabling natural language detection will detect when natural language is written in the terminal input, and then automatically switch to Agent Mode for AI queries.", + "settings.ai.nld.label": "Natural language detection", + "settings.ai.org_enforced_tooltip": "This option is enforced by your organization's settings and cannot be customized.", + "settings.ai.other.header": "Other", + "settings.ai.permission.agent_decides": "Agent decides", + "settings.ai.permission.allow_specific_directories": "Allow in specific directories", + "settings.ai.permission.always_allow": "Always allow", + "settings.ai.permission.always_ask": "Always ask", + "settings.ai.permission.ask_on_first_write": "Ask on first write", + "settings.ai.permissions.apply_code_diffs": "Apply code diffs", + "settings.ai.permissions.execute_commands": "Execute commands", + "settings.ai.permissions.header": "Permissions", + "settings.ai.permissions.interact_running_commands": "Interact with running commands", + "settings.ai.permissions.read_files": "Read files", + "settings.ai.permissions.workspace_managed": "Some of your permissions are managed by your workspace.", + "settings.ai.preferred_conversation_layout": "Preferred layout when opening existing agent conversations", + "settings.ai.profiles.desc": "Profiles let you define how your Agent operates — from the actions it can take and when it needs approval, to the models it uses for tasks like coding and planning. You can also scope them to individual projects.", + "settings.ai.profiles.header": "Profiles", + "settings.ai.prompt_suggestions.desc": "Let AI suggest natural language prompts, as inline banners in the input, based on recent commands and their outputs.", + "settings.ai.prompt_suggestions.label": "Prompt Suggestions", + "settings.ai.remote_session_disallowed": "Your organization disallows AI when the active pane contains content from a remote session", + "settings.ai.remove_endpoint": "Remove endpoint", + "settings.ai.remove_endpoint_description": "Are you sure you want to remove this endpoint? You won't be able to use its models in your agent sessions moving forward.", + "settings.ai.remove_endpoint_question": "Remove endpoint?", + "settings.ai.rules.desc": "Rules help the Warp Agent follow your conventions, whether for codebases or specific workflows. ", + "settings.ai.rules.label": "Rules", + "settings.ai.select_mcp_servers": "Select MCP servers", + "settings.ai.shared_block_title.desc": "Let AI generate a title for your shared block based on the command and output.", + "settings.ai.shared_block_title.label": "Shared Block Title Generation", + "settings.ai.show_agent_tips": "Show agent tips", + "settings.ai.show_conversation_history": "Show conversation history in tools panel", + "settings.ai.show_input_hint": "Show input hint text", + "settings.ai.show_model_picker": "Show model picker in prompt", + "settings.ai.show_oz_changelog": "Show Oz changelog in new conversation view", + "settings.ai.sign_up": "Sign up", + "settings.ai.suggested_code_banners.desc": "Let AI suggest code diffs and queries as inline banners in the blocklist, based on recent commands and their outputs.", + "settings.ai.suggested_code_banners.label": "Suggested Code Banners", + "settings.ai.suggested_rules.desc": "Let AI suggest rules to save based on your interactions.", + "settings.ai.suggested_rules.label": "Suggested Rules", + "settings.ai.thinking_display.desc": "Controls how reasoning/thinking traces are displayed.", + "settings.ai.thinking_display.label": "Agent thinking display", + "settings.ai.toast.endpoint_added": "Endpoint added", + "settings.ai.toast.endpoint_removed": "Endpoint removed", + "settings.ai.toast.endpoint_saved": "Endpoint saved", + "settings.ai.toolbar_layout": "Toolbar layout", + "settings.ai.usage.compare_plans": "Compare plans", + "settings.ai.usage.contact_support": "Contact support", + "settings.ai.usage.credits": "Credits", + "settings.ai.usage.credits_limit_desc": "This is the {period} limit of AI credits for your account.", + "settings.ai.usage.header": "Usage", + "settings.ai.usage.more_usage_suffix": " for more AI usage.", + "settings.ai.usage.resets": "Resets {date}", + "settings.ai.usage.restricted_billing": "Restricted due to billing issue", + "settings.ai.usage.unlimited": "Unlimited", + "settings.ai.usage.upgrade": "Upgrade", + "settings.ai.usage.upgrade_suffix": " to get more AI usage.", + "settings.ai.use_agent_footer.desc": "Shows hint to use the \"Full Terminal Use\"-enabled agent in long running commands.", + "settings.ai.use_agent_footer.label": "Show \"Use Agent\" footer", + "settings.ai.voice_input.desc_prefix": "Voice input allows you to control Warp by speaking directly to your terminal (powered by ", + "settings.ai.voice_input.desc_suffix": ").", + "settings.ai.voice_input.key_desc": "Press and hold to activate.", + "settings.ai.voice_input.key_label": "Key for Activating Voice Input", + "settings.ai.voice_input.label": "Voice Input", + "settings.ai.voice_input.tooltip": "Voice input", + "settings.ai.voice_input.tooltip_with_key": "Voice input (hold {key} key)", + "settings.ai.voice.header": "Voice", + "settings.ai.warp_credit_fallback.desc": "When enabled, agent requests may be routed to one of Warp's provided models in the event of an error. Warp will prioritize using your API keys over your Warp credits.", + "settings.ai.warp_credit_fallback.label": "Warp credit fallback", + "settings.ai.warp_drive_context.desc": "The Warp Agent can leverage your Warp Drive Contents to tailor responses to your personal and team developer workflows and environments. This includes any Workflows, Notebooks, and Environment Variables.", + "settings.ai.warp_drive_context.label": "Warp Drive as agent context", + "settings.appearance.action.agent_font_matching_terminal_font": "agent font matching terminal font", + "settings.appearance.action.always_show_tab_bar": "Always show tab bar", + "settings.appearance.action.block_dividers": "block dividers", + "settings.appearance.action.cursor_blink": "cursor blink", + "settings.appearance.action.custom_padding_alt_screen": "custom padding in alt-screen", + "settings.appearance.action.hide_code_review_button": "Hide code review button in tab bar", + "settings.appearance.action.hide_tab_bar_if_fullscreen": "Hide tab bar if fullscreen", + "settings.appearance.action.jump_to_bottom_button": "jump to bottom of block button", + "settings.appearance.action.latest_prompt_as_tab_title": "latest user prompt as conversation title in tab names", + "settings.appearance.action.ligature_rendering": "ligature rendering", + "settings.appearance.action.notebook_font_size_matching_terminal": "notebook font size matching terminal font size", + "settings.appearance.action.only_show_tab_bar_on_hover": "Only show tab bar on hover", + "settings.appearance.action.open_windows_custom_size": "opening new windows with custom size", + "settings.appearance.action.pin_input_to_bottom": "Pin Input to the Bottom", + "settings.appearance.action.pin_input_to_top": "Pin Input to the Top", + "settings.appearance.action.preserve_active_tab_color": "preserve active tab color for new tabs", + "settings.appearance.action.show_code_review_button": "Show code review button in tab bar", + "settings.appearance.action.start_input_at_top": "Start Input at the Top", + "settings.appearance.action.tab_indicators": "tab indicators", + "settings.appearance.action.theme_sync_with_os": "themes: sync with OS", + "settings.appearance.action.toggle_input_mode": "Toggle Input Mode (Warp/Classic)", + "settings.appearance.action.tools_panel_visibility_across_tabs": "tools panel visibility across tabs", + "settings.appearance.action.vertical_tab_layout": "vertical tab layout", + "settings.appearance.action.vertical_tabs_panel_restored_windows": "vertical tabs panel in restored windows", + "settings.appearance.action.window_blur_acrylic_texture": "window blur acrylic texture", + "settings.appearance.action.zen_mode": "zen mode", + "settings.appearance.app_icon.bundle_warning": "Changing the app icon requires the app to be bundled.", + "settings.appearance.app_icon.label": "Customize your app icon", + "settings.appearance.app_icon.option.aurora": "Aurora", + "settings.appearance.app_icon.option.classic_1": "Classic 1", + "settings.appearance.app_icon.option.classic_2": "Classic 2", + "settings.appearance.app_icon.option.classic_3": "Classic 3", + "settings.appearance.app_icon.option.comets": "Comets", + "settings.appearance.app_icon.option.cow": "Cow", + "settings.appearance.app_icon.option.default": "Default", + "settings.appearance.app_icon.option.glass_sky": "Glass Sky", + "settings.appearance.app_icon.option.glitch": "Glitch", + "settings.appearance.app_icon.option.glow": "Glow", + "settings.appearance.app_icon.option.holographic": "Holographic", + "settings.appearance.app_icon.option.mono": "Mono", + "settings.appearance.app_icon.option.neon": "Neon", + "settings.appearance.app_icon.option.original": "Original", + "settings.appearance.app_icon.option.starburst": "Starburst", + "settings.appearance.app_icon.option.sticker": "Sticker", + "settings.appearance.app_icon.option.warp_1": "Warp 1", + "settings.appearance.app_icon.restart_warning": "You may need to restart Warp for MacOS to apply the preferred icon style.", + "settings.appearance.blocks.compact_mode": "Compact mode", + "settings.appearance.blocks.jump_to_bottom": "Show Jump to Bottom of Block button", + "settings.appearance.blocks.show_dividers": "Show block dividers", + "settings.appearance.category.blocks": "Blocks", + "settings.appearance.category.cursor": "Cursor", + "settings.appearance.category.full_screen_apps": "Full-screen Apps", + "settings.appearance.category.icon": "Icon", + "settings.appearance.category.input": "Input", + "settings.appearance.category.panes": "Panes", + "settings.appearance.category.tabs": "Tabs", + "settings.appearance.category.text": "Text", + "settings.appearance.category.themes": "Themes", + "settings.appearance.category.window": "Window", + "settings.appearance.cursor.blinking": "Blinking cursor", + "settings.appearance.cursor.type": "Cursor type", + "settings.appearance.cursor.type_disabled_vim": "Cursor type is disabled in Vim mode", + "settings.appearance.full_screen_apps.custom_padding": "Use custom padding in alt-screen", + "settings.appearance.full_screen_apps.uniform_padding_px": "Uniform padding (px)", + "settings.appearance.input.position": "Input position", + "settings.appearance.input.position.pin_bottom_warp": "Pin to the bottom (Warp mode)", + "settings.appearance.input.position.pin_top_reverse": "Pin to the top (Reverse mode)", + "settings.appearance.input.position.start_top_classic": "Start at the top (Classic mode)", + "settings.appearance.input.type": "Input type", + "settings.appearance.input.type.shell_ps1": "Shell (PS1)", + "settings.appearance.input.type.warp": "Warp", + "settings.appearance.language.category": "Language", + "settings.appearance.language.label": "Display language", + "settings.appearance.language.subtext": "The language used for Warp's interface.", + "settings.appearance.panes.dim_inactive": "Dim inactive panes", + "settings.appearance.panes.focus_follows_mouse": "Focus follows mouse", + "settings.appearance.tabs.close_button_position": "Tab close button position", + "settings.appearance.tabs.close_button_position.left": "Left", + "settings.appearance.tabs.close_button_position.right": "Right", + "settings.appearance.tabs.directory_colors": "Directory tab colors", + "settings.appearance.tabs.directory_colors.default_no_color": "Default (no color)", + "settings.appearance.tabs.directory_colors.description": "Automatically color tabs based on the directory or repo you're working in.", + "settings.appearance.tabs.header_toolbar_layout": "Header toolbar layout", + "settings.appearance.tabs.latest_prompt_as_title": "Use latest user prompt as conversation title in tab names", + "settings.appearance.tabs.latest_prompt_as_title.description": "Show the latest user prompt instead of the generated conversation title for Oz and third-party agent sessions in vertical tabs.", + "settings.appearance.tabs.preserve_active_color": "Preserve active tab color for new tabs", + "settings.appearance.tabs.show_code_review_button": "Show code review button", + "settings.appearance.tabs.show_indicators": "Show tab indicators", + "settings.appearance.tabs.show_tab_bar": "Show the tab bar", + "settings.appearance.tabs.show_vertical_panel_restored": "Show vertical tabs panel in restored windows", + "settings.appearance.tabs.show_vertical_panel_restored.description": "When enabled, reopening or restoring a window opens the vertical tabs panel even if it was closed when the window was last saved.", + "settings.appearance.tabs.vertical_layout": "Use vertical tab layout", + "settings.appearance.tabs.visibility.always": "Always", + "settings.appearance.tabs.visibility.only_on_hover": "Only on hover", + "settings.appearance.tabs.visibility.when_windowed": "When windowed", + "settings.appearance.text.agent_font": "Agent font", + "settings.appearance.text.default_font_label": "{font} (default)", + "settings.appearance.text.font_size_px": "Font size (px)", + "settings.appearance.text.font_weight": "Font weight", + "settings.appearance.text.ligatures": "Show ligatures in terminal", + "settings.appearance.text.ligatures_tooltip": "Ligatures may reduce performance", + "settings.appearance.text.line_height": "Line height", + "settings.appearance.text.match_terminal": "Match terminal", + "settings.appearance.text.min_contrast": "Enforce minimum contrast", + "settings.appearance.text.min_contrast.always": "Always", + "settings.appearance.text.min_contrast.named_colors": "Only for named colors", + "settings.appearance.text.min_contrast.never": "Never", + "settings.appearance.text.notebook_font_size": "Notebook font size", + "settings.appearance.text.reset_to_default": "Reset to default", + "settings.appearance.text.terminal_font": "Terminal font", + "settings.appearance.text.thin_strokes": "Use thin strokes", + "settings.appearance.text.thin_strokes.always": "Always", + "settings.appearance.text.thin_strokes.high_dpi": "On high-DPI displays", + "settings.appearance.text.thin_strokes.low_dpi": "On low-DPI displays", + "settings.appearance.text.thin_strokes.never": "Never", + "settings.appearance.text.view_all_fonts": "View all available system fonts", + "settings.appearance.theme.create_custom": "Create your own custom theme", + "settings.appearance.theme.mode.current": "Current theme", + "settings.appearance.theme.mode.dark": "Dark", + "settings.appearance.theme.mode.light": "Light", + "settings.appearance.theme.sync_with_os": "Sync with OS", + "settings.appearance.theme.sync_with_os.description": "Automatically switch between light and dark themes when your system does.", + "settings.appearance.window.blur_radius": "Window Blur Radius", + "settings.appearance.window.blur_texture": "Use Window Blur (Acrylic texture)", + "settings.appearance.window.blur_texture_unsupported": "The selected hardware may not support rendering transparent windows.", + "settings.appearance.window.columns": "Columns", + "settings.appearance.window.custom_size": "Open new windows with custom size", + "settings.appearance.window.opacity": "Window Opacity", + "settings.appearance.window.opacity_label": "Window Opacity:", + "settings.appearance.window.opacity_unsupported": "Transparency is not supported with your graphics drivers.", + "settings.appearance.window.rows": "Rows", + "settings.appearance.window.tools_panel_consistent": "Tools panel visibility is consistent across tabs", + "settings.appearance.window.transparency_unsupported": "The selected graphics settings may not support rendering transparent windows.", + "settings.appearance.window.transparency_unsupported_hint": "Try changing the settings for the graphics backend or integrated GPU in Features > System.", + "settings.appearance.window.zoom": "Zoom", + "settings.appearance.window.zoom.description": "Adjusts the default zoom level across all windows", + "settings.billing.add_on_credits": "Add-on credits", + "settings.billing.addon.auto_reload_enabled_header": "Auto-reload is enabled", + "settings.billing.addon.auto_reload_label": "Auto-reload", + "settings.billing.addon.auto_reload_tooltip": "When any member on your team’s credit balance reaches 100 credits remaining, automatically purchase {amount}.", + "settings.billing.addon.buy_credits_header": "Buy credits", + "settings.billing.addon.contact_account_executive": "Contact your Account Executive for more add-on credits.", + "settings.billing.addon.contact_team_admin": "Contact a team admin to enable add-on credits.", + "settings.billing.addon.contact_team_admin_purchase": "Contact a team admin to purchase add-on credits.", + "settings.billing.addon.credits_count_one": "1 credit", + "settings.billing.addon.credits_unit": "credits", + "settings.billing.addon.description": "Add-on credits are purchased in prepaid packages that roll over each billing cycle and expire after one year. The more you purchase, the better the per-credit rate. Once your base plan credits are used, add-on credits will be consumed.", + "settings.billing.addon.description_team_shared_suffix": "Purchased add-on credits are shared across your team.", + "settings.billing.addon.description_team_suffix": "Purchased add-on credits are added to your personal balance.", + "settings.billing.addon.monthly_spend_limit_label": "Monthly spend limit", + "settings.billing.addon.monthly_spend_limit_tooltip": "Sets the monthly limit spent on add-on credits", + "settings.billing.addon.non_admin_auto_reload_description": "Your admin has enabled auto-reload for add-on credits. When your personal add-on credit balance runs low, Warp will automatically purchase add-on credits and add them to your balance.", + "settings.billing.addon.non_admin_auto_reload_description_with_amount": "Your admin has enabled auto-reload for add-on credits. When your personal add-on credit balance runs low, Warp will automatically purchase {credits} credits for {price} and add them to your balance.", + "settings.billing.addon.price_label": "{credits} credits / {dollars}", + "settings.billing.addon.purchase_button": "One-time purchase", + "settings.billing.addon.purchase_button_loading": "Buying…", + "settings.billing.addon.purchased_this_month": "Purchased this month", + "settings.billing.addon.selected_credit_amount_fallback": "selected credit amount", + "settings.billing.addon.upgrade_to_build_link": "Upgrade to Build", + "settings.billing.addon.upgrade_to_build_suffix": " to purchase add-on credits.", + "settings.billing.addon.warning_auto_reload_paused_admin": "Auto-reload is paused because the next reload would exceed your monthly spend limit. Increase your limit to continue using auto-reload.", + "settings.billing.addon.warning_auto_reload_paused_non_admin": "Auto-reload is paused because the next reload would exceed your team’s monthly spend limit. Contact a team admin to increase it.", + "settings.billing.addon.warning_delinquent_admin": "Restricted due to billing issue. Update your payment method to purchase add-on credits.", + "settings.billing.addon.warning_delinquent_non_admin": "Restricted due to billing issue. Contact your team admin to update their payment method.", + "settings.billing.addon.warning_failed_reload_admin": "Auto reload is disabled due to recent failed reload. Please update your payment method and try again.", + "settings.billing.addon.warning_failed_reload_non_admin": "Auto reload is disabled due to recent failed reload. Contact your team admin to update their payment method.", + "settings.billing.addon.warning_purchase_exceeds_admin": "This purchase would exceed your monthly limit. Increase your limit to continue.", + "settings.billing.addon.warning_purchase_exceeds_non_admin": "This purchase would exceed your team’s monthly spend limit. Contact a team admin to increase it.", + "settings.billing.aggregate_tooltip": "Other team members' usage across add-on, pay-as-you-go, and cloud-only credits.", + "settings.billing.ambient_trial.buy_more_button": "Buy more", + "settings.billing.ambient_trial.credits_remaining_one": "1 credit remaining", + "settings.billing.ambient_trial.credits_remaining_suffix": "credits remaining", + "settings.billing.ambient_trial.new_agent_button": "New agent", + "settings.billing.ambient_trial.title": "Cloud agent trial", + "settings.billing.automated_agent_on_team": "This is an automated agent on your team.", + "settings.billing.balance.base_credits": "Base credits", + "settings.billing.balance.expires_prefix": "Expires", + "settings.billing.balance.header": "Balance", + "settings.billing.balance.personal_credits": "Personal credits", + "settings.billing.balance.remaining": "remaining", + "settings.billing.balance.team_credits": "Team credits", + "settings.billing.bring_your_own_key": "bring your own key", + "settings.billing.business_security_suffix": " for security features like SSO and automatically applied zero data retention.", + "settings.billing.buy_more": "Buy more", + "settings.billing.contact_admin_resolve_issues": "Contact your team admin to resolve billing issues.", + "settings.billing.contact_support": "Contact support", + "settings.billing.cta_admin_panel_copy": "to set per-user spend limits.", + "settings.billing.cta_admin_panel_link": "Open the admin panel", + "settings.billing.cta_upgrade_build_copy": "to see team-level credit usage.", + "settings.billing.cta_upgrade_build_link": "Upgrade to Build", + "settings.billing.cta_upgrade_business_copy": "to see per-user credit attribution.", + "settings.billing.cta_upgrade_business_link": "Upgrade to Business", + "settings.billing.cta_upgrade_enterprise_copy": "to see fine-grained credit attribution and set per-user spend limits.", + "settings.billing.cta_upgrade_enterprise_link": "Upgrade to Enterprise", + "settings.billing.discount_badge": "{discount}% off", + "settings.billing.enterprise_support_suffix": " for custom limits and dedicated support.", + "settings.billing.enterprise_usage_callout.admin_link": "visit the admin panel", + "settings.billing.enterprise_usage_callout.admin_prefix": "Enterprise credit usage isn't fully available in this view yet. For the most accurate spend tracking, ", + "settings.billing.enterprise_usage_callout.header": "Usage reporting is currently limited", + "settings.billing.enterprise_usage_callout.non_admin": "Enterprise credit usage isn't fully available in this view yet. Contact a team admin for detailed usage reporting.", + "settings.billing.flexible_pricing_suffix": " for a more flexible pricing model.", + "settings.billing.increase_your_limit": "Increase your limit", + "settings.billing.increased_ai_access_suffix": " for increased access to AI features.", + "settings.billing.last_30_days": "Last 30 days", + "settings.billing.legend_addons": "Add-ons", + "settings.billing.legend_base": "Base", + "settings.billing.legend_cloud_only": "Cloud-only", + "settings.billing.legend_combined": "Combined", + "settings.billing.legend_payg": "Pay-as-you-go", + "settings.billing.manage_billing": "Manage billing", + "settings.billing.members_header": "Members", + "settings.billing.monthly_overage_spending_limit": "Monthly overage spending limit", + "settings.billing.monthly_overage_spending_limit_tooltip": "Sets the monthly overage spending limit beyond the plan amount", + "settings.billing.more_ai_credits_suffix": " for more AI credits.", + "settings.billing.more_ai_usage_suffix": " to get more AI usage.", + "settings.billing.more_credits_and_models_suffix": " for more credits and access to more models.", + "settings.billing.not_set": "Not set", + "settings.billing.overage_modal.additional_note": "Note that AI credits made near your chosen limit may exceed it by a few dollars.", + "settings.billing.overage_modal.cancel_button": "Cancel", + "settings.billing.overage_modal.description": "Warp will prevent use of premium models when this dollar limit is reached. Resets on a monthly basis.", + "settings.billing.overage_modal.error_invalid_amount": "Please enter a valid currency amount", + "settings.billing.overage_modal.error_out_of_range": "Please enter a price between $0.01 and $10,000,000", + "settings.billing.overage_modal.title": "Overage spending limit", + "settings.billing.overage_modal.update_button": "Update", + "settings.billing.overage.toggle_admin_header": "Enable premium model usage overages", + "settings.billing.overage.toggle_description": "Continue using premium models beyond your plan's limits. Usage is charged in $20 increments up to your spending limit, with any remaining balance charged on your scheduled billing date.", + "settings.billing.overage.toggle_user_description": "Ask a team admin to enable overages for more AI usage.", + "settings.billing.overage.toggle_user_header_disabled": "Premium model usage overages are not enabled", + "settings.billing.overage.toggle_user_header_enabled": "Premium model usage overages are enabled", + "settings.billing.overage.usage_link": "View details on overage usage", + "settings.billing.overview_tab": "Overview", + "settings.billing.plan": "Plan", + "settings.billing.plan.compare_plans": "Compare plans", + "settings.billing.plan.free_badge": "Free", + "settings.billing.plan.header": "Plan", + "settings.billing.plan.manage_billing": "Manage billing", + "settings.billing.plan.open_admin_panel": "Open admin panel", + "settings.billing.prorated_limit_current_user": "Your credit limit is prorated because you joined midway through the billing cycle.", + "settings.billing.prorated_limit_user": "This credit limit is prorated because this user joined midway through the billing cycle.", + "settings.billing.purchased_this_month": "Purchased this month", + "settings.billing.regain_access_suffix": " to regain access to AI features.", + "settings.billing.reload_would_exceed_limit": "Reloading would exceed your monthly limit. ", + "settings.billing.resets_at": "Resets %b %d, %-I:%M %p", + "settings.billing.sort_by": "Sort by", + "settings.billing.sort.display_name_a_z": "A to Z", + "settings.billing.sort.display_name_z_a": "Z to A", + "settings.billing.sort.usage_ascending": "Usage ascending", + "settings.billing.sort.usage_descending": "Usage descending", + "settings.billing.spending_limit_modal.title": "Monthly spending limit", + "settings.billing.switch_to_build_plan": "Switch to the Build plan", + "settings.billing.switch_to_business": "Switch to Business", + "settings.billing.team_totals.cloud_agent_usage": "Cloud agent usage", + "settings.billing.team_totals.credits_count": "({credits} credits)", + "settings.billing.team_totals.limit": "Limit: {limit}", + "settings.billing.team_totals.local_agent_usage": "Local agent usage", + "settings.billing.team_totals.overall_usage": "Overall usage", + "settings.billing.team_totals.team": "Team", + "settings.billing.team_totals.team_total": "Team total", + "settings.billing.to_continue": " to continue.", + "settings.billing.toast.auto_reload_disabled": "Auto-reload disabled.", + "settings.billing.toast.auto_reload_enabled": "Auto-reload enabled. We'll refill with {credits} credits when your balance runs low.", + "settings.billing.toast.auto_reload_pricing_loading": "Unable to enable auto-reload until pricing options load.", + "settings.billing.toast.purchase_success": "Successfully purchased add-on credits", + "settings.billing.toast.update_settings_failed": "Failed to update workspace settings", + "settings.billing.toast.your_selected_fallback": "your selected", + "settings.billing.total_overages": "Total overages", + "settings.billing.upgrade_to_build_plan": "Upgrade to the Build plan", + "settings.billing.upgrade_to_enterprise": "Upgrade to Enterprise", + "settings.billing.upgrade_to_max": "Upgrade to Max", + "settings.billing.usage_header": "Usage", + "settings.billing.usage_history_empty": "Kick off an agent task to view usage history here.", + "settings.billing.usage_history_tab": "Usage History", + "settings.billing.usage_history.empty_description": "Kick off an agent task to view usage history here.", + "settings.billing.usage_history.empty_title": "No usage history", + "settings.billing.usage_history.last_30_days": "Last 30 days", + "settings.billing.usage_history.load_more": "Load more", + "settings.billing.usage_resets_on": "Usage resets on {date}", + "settings.billing.usage.bucket.ai": "AI", + "settings.billing.usage.bucket.compute": "Compute", + "settings.billing.usage.bucket.other": "Other", + "settings.billing.usage.bucket.platform": "Platform", + "settings.billing.usage.bucket.suggested_code_diffs": "Suggested code diffs", + "settings.billing.usage.bucket.total": "Total", + "settings.billing.usage.bucket.voice": "Voice", + "settings.billing.usage.other_members": "Other members", + "settings.billing.usage.source.all": "All", + "settings.billing.usage.source.cloud": "Cloud", + "settings.billing.usage.source.local": "Local", + "settings.billing.usage.total": "Total usage", + "settings.billing.usage.unknown_subject": "Unknown", + "settings.billing.usage.your_usage": "Your usage", + "settings.code.action.auto_indexing": "auto-indexing", + "settings.code.action.code_review_button": "code review button", + "settings.code.action.codebase_index": "codebase index", + "settings.code.action.diff_stats_on_code_review_button": "diff stats on code review button", + "settings.code.auto_index.description": "When set to true, Warp will automatically index code repositories as you navigate them - helping agents quickly understand context and provide targeted solutions.", + "settings.code.auto_index.label": "Index new folders by default", + "settings.code.auto_open_review_panel.description": "When this setting is on, the code review panel will open on the first accepted diff of a conversation", + "settings.code.auto_open_review_panel.label": "Auto open code review panel", + "settings.code.category.editor_and_review.title": "Code Editor and Review", + "settings.code.category.indexing.title": "Codebase Indexing", + "settings.code.codebase_index.description": "Warp can automatically index code repositories as you navigate them, helping agents quickly understand context and provide solutions. Code is never stored on the server. If a codebase is unable to be indexed, Warp can still navigate your codebase and gain insights via grep and find tool calling.", + "settings.code.codebase_indexing.label": "Codebase indexing", + "settings.code.global_search.description": "Adds global file search to the left side tools panel.", + "settings.code.global_search.label": "Global file search", + "settings.code.header": "Code", + "settings.code.index_limit_reached": "You have reached the maximum number of codebase indices for your plan. Delete existing indices to auto-index new codebases.", + "settings.code.index_new_folder.button": "Index new folder", + "settings.code.indexing_ignore.description": "To exclude specific files or directories from indexing, add them to the .warpindexingignore file in your repository directory. These files will still be accessible to AI features, but they won't be included in codebase embeddings.", + "settings.code.initialization_settings.header": "Initialization Settings", + "settings.code.initialized_folders.header": "Initialized / indexed folders", + "settings.code.lsp.available_for_download": "Available for download", + "settings.code.lsp.checking": "Checking...", + "settings.code.lsp.installed": "Installed", + "settings.code.lsp.installing": "Installing...", + "settings.code.lsp.restart_server.button": "Restart server", + "settings.code.lsp.status.available": "Available", + "settings.code.lsp.status.busy": "Busy", + "settings.code.lsp.status.failed": "Failed", + "settings.code.lsp.status.not_running": "Not running", + "settings.code.lsp.status.stopped": "Stopped", + "settings.code.lsp.view_logs.button": "View logs", + "settings.code.no_folders_initialized": "No folders have been initialized yet.", + "settings.code.open_project_rules.button": "Open project rules", + "settings.code.project_explorer.description": "Adds an IDE-style project explorer / file tree to the left side tools panel.", + "settings.code.project_explorer.label": "Project explorer", + "settings.code.section.indexing": "INDEXING", + "settings.code.section.lsp_servers": "LSP SERVERS", + "settings.code.show_diff_stats.description": "Show lines added and removed counts on the code review button.", + "settings.code.show_diff_stats.label": "Show diff stats on code review button", + "settings.code.show_review_button.description": "Show a button in the top right of the window to toggle the code review panel.", + "settings.code.show_review_button.label": "Show code review button", + "settings.code.status.codebase_too_large": "Codebase too large", + "settings.code.status.disabled": "Disabled", + "settings.code.status.discovered.prefix": "Discovered ", + "settings.code.status.discovered.suffix": " chunks", + "settings.code.status.failed": "Failed", + "settings.code.status.index_limit_reached": "Index limit reached", + "settings.code.status.indexing": "Indexing...", + "settings.code.status.indexing.prefix": "Indexing - ", + "settings.code.status.no_index_built": "No index built", + "settings.code.status.no_index_created": "No index created", + "settings.code.status.queued": "Queued", + "settings.code.status.stale": "Stale", + "settings.code.status.synced": "Synced", + "settings.code.status.syncing": "Syncing...", + "settings.code.status.syncing.prefix": "Syncing - ", + "settings.code.status.unavailable": "Unavailable", + "settings.code.subpage.editor_and_review.title": "Editor and Code Review", + "settings.code.subpage.indexing.title": "Codebase Indexing", + "settings.code.tooltip.admin_disabled": "Team admins have disabled codebase indexing.", + "settings.code.tooltip.admin_enabled": "Team admins have enabled codebase indexing.", + "settings.code.tooltip.ai_required": "AI Features must be enabled to use codebase indexing.", + "settings.custom_inference.add_endpoint": "Add endpoint", + "settings.custom_inference.add_model": "+ Add model", + "settings.custom_inference.api_key": "API key", + "settings.custom_inference.api_key_placeholder": "e.g., sk-...", + "settings.custom_inference.endpoint_details_help": "Provide your endpoint details below. You can add as many models from the endpoint as you'd like and can also provide aliases for the model picker in your input.", + "settings.custom_inference.endpoint_name": "Endpoint name", + "settings.custom_inference.endpoint_name_placeholder": "e.g., Zach's external models", + "settings.custom_inference.endpoint_url": "Endpoint URL", + "settings.custom_inference.error.invalid_url": "Invalid URL", + "settings.custom_inference.error.url_host_required": "URL must include a host", + "settings.custom_inference.error.url_https_required": "URL must use HTTPS", + "settings.custom_inference.error.url_restricted_host": "URL must not use a local or private host", + "settings.custom_inference.model_alias": "Model alias (optional)", + "settings.custom_inference.model_alias_placeholder": "e.g., GLM-5", + "settings.custom_inference.model_name": "Model name", + "settings.custom_inference.model_name_placeholder": "e.g., GLM-5-FP8", + "settings.directory_color.add_button": "Add directory color", + "settings.directory_color.add_directory": "+ Add directory…", + "settings.environments.all_indexed_repos_selected": "All locally indexed repos are already selected.", + "settings.environments.available_indexed_repos": "Available indexed repos", + "settings.environments.button.add_repo": "Add repo", + "settings.environments.button.cancel": "Cancel", + "settings.environments.button.create": "Create", + "settings.environments.button.create_environment": "Create environment", + "settings.environments.button.delete_environment": "Delete environment", + "settings.environments.button.save": "Save", + "settings.environments.button.save_environment": "Save environment", + "settings.environments.card.edit_tooltip": "Edit", + "settings.environments.card.env_id_prefix": "Env ID: ", + "settings.environments.card.image_prefix": "Image: ", + "settings.environments.card.last_edited_prefix": "Last edited: ", + "settings.environments.card.last_used_never": "Last used: never", + "settings.environments.card.last_used_prefix": "Last used: ", + "settings.environments.card.repos_prefix": "Repos: ", + "settings.environments.card.setup_commands_prefix": "Setup commands: ", + "settings.environments.card.share_tooltip": "Share", + "settings.environments.card.view_my_runs": "View my runs", + "settings.environments.delete_dialog_description": "Are you sure you want to remove the {name} environment?", + "settings.environments.delete_dialog_title": "Delete environment?", + "settings.environments.description.character_count_suffix": "characters", + "settings.environments.description.label": "Description", + "settings.environments.description.placeholder": "e.g., this environment is for all front end focused agents", + "settings.environments.docker_image.label": "Docker image reference", + "settings.environments.docker_image.label_short": "Docker image", + "settings.environments.docker_image.open_image_at": "Open image at", + "settings.environments.docker_image.placeholder": "e.g. python:3.11, node:20-alpine", + "settings.environments.docker_image.placeholder_short": "e.g., node:20-alpine", + "settings.environments.docker_image.suggest_generating": "Generating…", + "settings.environments.docker_image.suggest_image": "Suggest image", + "settings.environments.docker_image.suggest_tooltip": "Warp will suggest a Docker image based on your selected repositories.", + "settings.environments.empty.agent.subtitle": "Choose a locally set up project and we’ll help you set up an environment based on it", + "settings.environments.empty.agent.title": "Use the agent", + "settings.environments.empty.button.authorize": "Authorize", + "settings.environments.empty.button.get_started": "Get started", + "settings.environments.empty.button.launch_agent": "Launch agent", + "settings.environments.empty.button.loading": "Loading...", + "settings.environments.empty.button.retry": "Retry", + "settings.environments.empty.github.badge": "Suggested", + "settings.environments.empty.github.subtitle": "Select the GitHub repositories you’d like to work with and we’ll suggest a base image and config", + "settings.environments.empty.github.title": "Quick setup", + "settings.environments.empty.header": "You haven’t set up any environments yet.", + "settings.environments.empty.subheader": "Choose how you’d like to set up your environment:", + "settings.environments.error.not_logged_in": "Not logged in", + "settings.environments.loading_indexed_repos": "Loading locally indexed repos…", + "settings.environments.local_repo_selection_unavailable": "Local repo selection is unavailable in this build.", + "settings.environments.name.label": "Name", + "settings.environments.name.placeholder": "Environment name", + "settings.environments.name.placeholder_example": "e.g., dev-env", + "settings.environments.no_directory_selected": "No directory selected", + "settings.environments.no_indexed_repos_found": "No locally indexed repos found yet. Index a repo, then try again.", + "settings.environments.no_repos_selected": "No repos selected yet", + "settings.environments.page.description": "Environments define where your ambient agents run. Set one up in minutes via GitHub (recommended), Warp-assisted setup, or manual configuration.", + "settings.environments.page.title": "Environments", + "settings.environments.repos.auth_with_github": "Auth with GitHub", + "settings.environments.repos.configure_access": "Configure access on GitHub", + "settings.environments.repos.helper": "Type owner/repo and press Enter to add, or select from dropdown.", + "settings.environments.repos.label": "Repo(s)", + "settings.environments.repos.load_error": "Couldn't load GitHub repos. You can paste repo URL(s), or retry.", + "settings.environments.repos.load_failed_fallback": "Failed to load GitHub repositories", + "settings.environments.repos.loading": "Loading...", + "settings.environments.repos.missing_a_repo": "Missing a repo?", + "settings.environments.repos.none_found": "No repositories found", + "settings.environments.repos.placeholder_authed": "Enter repos (owner/repo format)", + "settings.environments.repos.placeholder_browse": "Browse GitHub repos...", + "settings.environments.repos.placeholder_unauthed": "Paste repo URL(s)", + "settings.environments.repos.retry": "Retry", + "settings.environments.search.no_matches": "No environments match your search.", + "settings.environments.search.placeholder": "Search environments...", + "settings.environments.section.personal": "Personal", + "settings.environments.section.shared_by_team_prefix": "Shared by Warp and {team}", + "settings.environments.section.shared_by_your_team": "Shared by Warp and your team", + "settings.environments.select_local_repos_description": "Select locally indexed repos to provide context for the environment creation agent.", + "settings.environments.select_repos_description": "Select repos to provide context for the environment creation agent.", + "settings.environments.select_repos_dialog_title": "Select repos for your environment", + "settings.environments.selected_folder_not_git_repository": "Selected folder is not a Git repository: {path}", + "settings.environments.selected_repos": "Selected repos", + "settings.environments.setup_commands.helper": "Setup commands run independently. Each command runs from the workspace root (/workspace). If a command depends on the previous one, combine them with &&.", + "settings.environments.setup_commands.helper_short": "Press Enter or click the submit button to add each command.", + "settings.environments.setup_commands.label": "Setup command(s)", + "settings.environments.setup_commands.placeholder": "e.g. cd my-repo && pip install -r requirements.txt", + "settings.environments.setup_commands.placeholder_short": "e.g., node start", + "settings.environments.share_with_team.label": "Share with team", + "settings.environments.share_with_team.warning": "Personal environments cannot be used with external integrations or team API keys. For the best experience, use shared environments.", + "settings.environments.suggest_image.auth_required": "You need to grant access to your GitHub repos to suggest a Docker image", + "settings.environments.suggest_image.authenticate": "Authenticate", + "settings.environments.suggest_image.failed": "Failed to suggest a Docker image", + "settings.environments.suggest_image.failed_with_error": "Failed to suggest a Docker image: {error}", + "settings.environments.suggest_image.launch_agent": "Launch agent", + "settings.environments.suggest_image.no_match": "We couldn't find a good match. We recommend using a custom Docker image for these repos.", + "settings.environments.suggest_image.unknown_response": "Unknown response from suggestCloudEnvironmentImage", + "settings.environments.title.create": "Create environment", + "settings.environments.title.edit": "Edit environment", + "settings.environments.toast.create_not_logged_in": "Unable to create environment: not logged in.", + "settings.environments.toast.created": "Successfully created environment", + "settings.environments.toast.deleted": "Environment deleted successfully", + "settings.environments.toast.save_not_found": "Unable to save: environment no longer exists.", + "settings.environments.toast.share_failed": "Failed to share environment with team", + "settings.environments.toast.share_no_team": "Unable to share environment: you are not currently on a team.", + "settings.environments.toast.share_not_synced": "Unable to share environment: environment is not yet synced.", + "settings.environments.toast.shared": "Successfully shared environment", + "settings.environments.toast.updated": "Successfully updated environment", + "settings.execution_profile.auto": "Auto", + "settings.execution_profile.autosync_plans_to_warp_drive": "Auto-sync plans to Warp Drive", + "settings.execution_profile.full_terminal_use": "Full terminal use", + "settings.execution_profile.label_separator": ":", + "settings.execution_profile.models": "MODELS", + "settings.execution_profile.off": "Off", + "settings.execution_profile.on": "On", + "settings.execution_profile.permission.unknown": "Unknown", + "settings.execution_profile.permissions": "PERMISSIONS", + "settings.execution_profile.run_agents": "Run agents", + "settings.external_editor.default_app": "Default App", + "settings.external_editor.markdown_viewer_default": "Open Markdown files in Warp's Markdown Viewer by default", + "settings.external_editor.new_tab": "New Tab", + "settings.external_editor.open_code_panel_files": "Choose an editor to open files from the code review panel, project explorer, and global search", + "settings.external_editor.open_file_links": "Choose an editor to open file links", + "settings.external_editor.open_files_layout": "Choose a layout to open files in Warp", + "settings.external_editor.split_pane": "Split Pane", + "settings.external_editor.tabbed_viewer.description": "When this setting is on, any files opened in the same tab will be automatically grouped into a single editor pane.", + "settings.external_editor.tabbed_viewer.header": "Group files into single editor pane", + "settings.features.action.agent_task_completion_notifications": "agent task completion notifications", + "settings.features.action.alias_expansion": "alias expansion", + "settings.features.action.at_context_menu_terminal": "'@' context menu in terminal mode", + "settings.features.action.audible_terminal_bell": "audible terminal bell", + "settings.features.action.autocomplete_symbols": "autocomplete quotes, parentheses, and brackets", + "settings.features.action.autosuggestion_ignore_button": "autosuggestion ignore button", + "settings.features.action.autosuggestion_keybinding_hint": "autosuggestion keybinding hint", + "settings.features.action.autosuggestions": "autosuggestions", + "settings.features.action.code_default_editor": "Code as default editor", + "settings.features.action.codebase_symbols_at_context": "codebase symbols in the '@' context menu", + "settings.features.action.command_corrections": "command corrections", + "settings.features.action.completions_while_typing": "completions while typing", + "settings.features.action.configure_global_hotkey": "Configure Global Hotkey", + "settings.features.action.copy_on_select_terminal": "copy on select within the terminal", + "settings.features.action.error_underlining": "error underlining", + "settings.features.action.focus_reporting": "focus reporting", + "settings.features.action.global_workflows_command_search": "global workflows in Command Search", + "settings.features.action.help_block_new_sessions": "help block in new sessions", + "settings.features.action.in_app_agent_notifications": "in-app agent notifications", + "settings.features.action.input_hint_text": "input hint text", + "settings.features.action.integrated_gpu_rendering_low_power": "integrated GPU rendering (low power)", + "settings.features.action.link_click_tooltip": "link-click tooltip", + "settings.features.action.linux_selection_clipboard": "Linux selection clipboard", + "settings.features.action.long_running_command_notifications": "long-running command notifications", + "settings.features.action.middle_click_paste": "middle-click paste", + "settings.features.action.mouse_reporting": "mouse reporting", + "settings.features.action.needs_attention_notifications": "needs-attention notifications", + "settings.features.action.notification_sounds": "notification sounds", + "settings.features.action.quit_warning_modal": "quit warning modal", + "settings.features.action.restore_session": "restore windows, tabs, and panes on startup", + "settings.features.action.scroll_reporting": "scroll reporting", + "settings.features.action.slash_commands_terminal": "slash commands in terminal mode", + "settings.features.action.smart_select": "smart select", + "settings.features.action.syntax_highlighting": "syntax highlighting", + "settings.features.action.terminal_input_message_line": "terminal input message line", + "settings.features.action.vim_keybindings": "editing commands with Vim keybindings", + "settings.features.action.vim_status_bar": "Vim status bar", + "settings.features.action.vim_unnamed_register_clipboard": "Vim unnamed register as system clipboard", + "settings.features.action.warp_ssh_wrapper": "Warp SSH wrapper", + "settings.features.action.wayland_window_management": "Wayland for window management", + "settings.features.alias_expansion.label": "Expand aliases as you type", + "settings.features.async_find.desc": "Use an improved implementation of find to keep the UI responsive while searching for matches on large outputs.", + "settings.features.async_find.label": "Asynchronous find", + "settings.features.at_context_menu.label": "Enable '@' context menu in terminal mode", + "settings.features.audible_bell.label": "Use Audible Bell", + "settings.features.auto_open_code_review.desc": "When this setting is on, the code review panel will open on the first accepted diff of a conversation", + "settings.features.auto_open_code_review.label": "Auto open code review panel", + "settings.features.autocomplete_symbols.label": "Autocomplete quotes, parentheses, and brackets", + "settings.features.autosuggestion_hint.label": "Show autosuggestion keybinding hint", + "settings.features.autosuggestion_ignore_button.label": "Show autosuggestion ignore button", + "settings.features.block_limit.desc": "Setting the limit above 100k lines may impact performance. Maximum rows supported is {max_rows}.", + "settings.features.block_limit.label": "Maximum rows in a block", + "settings.features.block_limit.max_rows_10m": "10 million", + "settings.features.block_limit.max_rows_1m": "1 million", + "settings.features.button.cancel": "Cancel", + "settings.features.button.save": "Save", + "settings.features.category.general": "General", + "settings.features.category.keys": "Keys", + "settings.features.category.notifications": "Notifications", + "settings.features.category.session": "Session", + "settings.features.category.system": "System", + "settings.features.category.terminal": "Terminal", + "settings.features.category.terminal_input": "Terminal Input", + "settings.features.category.text_editing": "Text Editing", + "settings.features.category.workflows": "Workflows", + "settings.features.changes_apply_new_windows": "Changes will apply to new windows.", + "settings.features.code_line_numbers.label": "Code editor line numbers:", + "settings.features.command_corrections.label": "Suggest corrected commands", + "settings.features.completions_while_typing.label": "Open completions menu as you type", + "settings.features.confirm_close_shared.label": "Confirm before closing shared session", + "settings.features.copy_on_select.label": "Copy on select", + "settings.features.ctrl_tab_behavior.label": "Ctrl+Tab behavior:", + "settings.features.default_session_mode.label": "Default mode for new sessions", + "settings.features.default_terminal.is_default": "Warp is the default terminal", + "settings.features.default_terminal.make_default": "Make Warp the default terminal", + "settings.features.error_underlining.label": "Error underlining for commands", + "settings.features.extra_meta_keys.left_alt": "Left Alt key is Meta", + "settings.features.extra_meta_keys.left_option": "Left Option key is Meta", + "settings.features.extra_meta_keys.right_alt": "Right Alt key is Meta", + "settings.features.extra_meta_keys.right_option": "Right Option key is Meta", + "settings.features.focus_reporting.label": "Enable Focus Reporting", + "settings.features.global_hotkey.label": "Global hotkey:", + "settings.features.global_hotkey.not_supported_wayland": "Not supported on Wayland. ", + "settings.features.global_workflows.label": "Show Global Workflows in Command Search (ctrl-r)", + "settings.features.gpu.prefer_low_power.label": "Prefer rendering new windows with integrated GPU (low power)", + "settings.features.graphics_backend.current": "Current backend: {}", + "settings.features.graphics_backend.label": "Preferred graphics backend", + "settings.features.help_block.label": "Show help block in new sessions", + "settings.features.keybinding.change": "Change keybinding", + "settings.features.keybinding.click_to_set": "Click to set global hotkey", + "settings.features.keybinding.label": "Keybinding", + "settings.features.keybinding.press_new_shortcut": "Press new keyboard shortcut", + "settings.features.link_tooltip.label": "Show tooltip on click on links", + "settings.features.linux_clipboard.label": "Honor linux selection clipboard", + "settings.features.linux_clipboard.tooltip": "Whether the Linux primary clipboard should be supported.", + "settings.features.login_item.label": "Start Warp at login", + "settings.features.login_item.label_macos": "Start Warp at login (requires macOS 13+)", + "settings.features.middle_click_paste.label": "Middle-click to paste", + "settings.features.mouse_reporting.label": "Enable Mouse Reporting", + "settings.features.mouse_scroll.allowed_values": "Allowed Values: 1-20", + "settings.features.mouse_scroll.label": "Lines scrolled by mouse wheel interval", + "settings.features.mouse_scroll.tooltip": "Supports floating point values between 1 and 20.", + "settings.features.new_tab.after_all_tabs": "After all tabs", + "settings.features.new_tab.after_current_tab": "After current tab", + "settings.features.new_tab.placement_label": "New tab placement", + "settings.features.notifications.agent_task_completed": "Notify when an agent completes a task", + "settings.features.notifications.in_app_agent": "Show in-app agent notifications", + "settings.features.notifications.long_running_prefix": "When a command takes longer than", + "settings.features.notifications.long_running_suffix": "seconds to complete", + "settings.features.notifications.needs_attention": "Notify when a command or agent needs your attention to continue", + "settings.features.notifications.play_sounds": "Play notification sounds", + "settings.features.notifications.receive_desktop": "Receive desktop notifications from Warp", + "settings.features.notifications.toast_duration_prefix": "Toast notifications stay visible for", + "settings.features.notifications.toast_duration_suffix": "seconds", + "settings.features.open_links_desktop.label": "Open links in desktop app", + "settings.features.open_links_desktop.tooltip": "Automatically open links in desktop app whenever possible.", + "settings.features.outline_codebase_symbols.label": "Outline codebase symbols for '@' context menu", + "settings.features.quake.active_screen": "Active Screen", + "settings.features.quake.autohide_on_blur": "Autohides on loss of keyboard focus", + "settings.features.quake.height_percent": "Height %", + "settings.features.quake.pin_bottom": "Pin to bottom", + "settings.features.quake.pin_left": "Pin to left", + "settings.features.quake.pin_right": "Pin to right", + "settings.features.quake.pin_top": "Pin to top", + "settings.features.quake.width_percent": "Width %", + "settings.features.quit_all_windows.label": "Quit when all windows are closed", + "settings.features.quit_warning.label": "Show warning before quitting/logging out", + "settings.features.restore_session.label": "Restore windows, tabs, and panes on startup", + "settings.features.restore_session.wayland_warning": "Window positions won't be restored on Wayland. ", + "settings.features.scroll_reporting.label": "Enable Scroll Reporting", + "settings.features.see_docs": "See docs.", + "settings.features.show_changelog.label": "Show changelog toast after updates", + "settings.features.slash_commands.label": "Enable slash commands in terminal mode", + "settings.features.smart_select.label": "Double-click smart selection", + "settings.features.smart_select.word_chars_label": "Characters considered part of a word", + "settings.features.ssh_wrapper.label": "Warp SSH Wrapper", + "settings.features.ssh_wrapper.takes_effect_new_sessions": "This change will take effect in new sessions", + "settings.features.startup_shell.executable_path_placeholder": "Executable path", + "settings.features.startup_shell.header": "Default shell for new sessions", + "settings.features.sticky_command_header.label": "Show sticky command header", + "settings.features.syntax_highlighting.label": "Syntax highlighting for commands", + "settings.features.tab_behavior.arrow_accepts_autosuggestions": "→ accepts autosuggestions.", + "settings.features.tab_behavior.completion_menu_unbound": "Opening the completion menu is unbound.", + "settings.features.tab_behavior.completions_open_typing": "Completions open as you type.", + "settings.features.tab_behavior.completions_open_typing_or_key": "Completions open as you type (or {key}).", + "settings.features.tab_behavior.header": "Tab key behavior", + "settings.features.tab_behavior.key_accepts_autosuggestions": "{key} accepts autosuggestions.", + "settings.features.tab_behavior.key_opens_completion_menu": "{key} opens completion menu.", + "settings.features.terminal_input_message_line.label": "Show terminal input message line", + "settings.features.vim_mode.label": "Edit code and commands with Vim keybindings", + "settings.features.vim_status_bar.label": "Show Vim status bar", + "settings.features.vim_unnamed_clipboard.label": "Set unnamed register as system clipboard", + "settings.features.wayland.label": "Use Wayland for window management", + "settings.features.wayland.restart_required": "Restart Warp for changes to take effect.", + "settings.features.wayland.secondary_text": "Enabling this setting disables global hotkey support. When disabled, text may be blurry if your Wayland compositor is using fraction scaling (ex: 125%).", + "settings.features.wayland.tooltip": "Enables the use of Wayland", + "settings.features.working_directory.directory_path_placeholder": "Directory path", + "settings.features.working_directory.header": "Working directory for new sessions", + "settings.file_error.heading": "Your settings file contains an error.", + "settings.file_error.heading_plural": "Your settings file contains errors.", + "settings.file_error.invalid_multiple_description": "Invalid values for: {keys}. Default values are being used.", + "settings.file_error.invalid_single_description": "Invalid value for '{key}'. The default value is being used.", + "settings.file_error.parse_description": "Couldn't parse due to invalid syntax. Open the file to fix it.", + "settings.import.import_button": "Import", + "settings.import.new_session_notice": "Some settings will take effect when you open a new session.", + "settings.import.one_other_setting": "1 other setting", + "settings.import.other_settings": "{count} other settings", + "settings.import.reset_to_defaults": "Reset to Warp defaults", + "settings.import.theme": "Theme", + "settings.import.theme_comma": "Theme,", + "settings.keybindings.cancel_button": "Cancel", + "settings.keybindings.clear_button": "Clear", + "settings.keybindings.column_command": "Command", + "settings.keybindings.description_intro": "Add your own custom keybindings to existing actions below.", + "settings.keybindings.not_synced_tooltip": "Keyboard shortcuts are not synced to the cloud", + "settings.keybindings.press_new_shortcut": "Press new keyboard shortcut", + "settings.keybindings.reset_button": "Default", + "settings.keybindings.save_button": "Save", + "settings.keybindings.search_placeholder": "Search by name or by keys (ex. \"cmd d\")", + "settings.keybindings.shortcut_conflict_warning": "This shortcut conflicts with other keybinds", + "settings.keybindings.shortcut_hint_prefix": "Use", + "settings.keybindings.shortcut_hint_suffix": "to reference these keybindings in a side pane at anytime.", + "settings.keybindings.subheader": "Configure keyboard shortcuts", + "settings.mcp.action.edit": "Edit", + "settings.mcp.action.edit_config": "Edit config", + "settings.mcp.action.log_out": "Log out", + "settings.mcp.action.server_update_available": "Server update available", + "settings.mcp.action.set_up": "Set up", + "settings.mcp.action.share_server": "Share server", + "settings.mcp.action.show_logs": "Show logs", + "settings.mcp.action.view_logs": "View logs", + "settings.mcp.add_button": "Add", + "settings.mcp.auto_spawn.description": "Automatically detect and spawn MCP servers from globally-scoped third-party AI agent configuration files (e.g. in your home directory). Servers detected inside a repository are never spawned automatically and must be enabled individually in the \"Detected from\" sections below. ", + "settings.mcp.auto_spawn.label": "Auto-spawn servers from third-party agents", + "settings.mcp.auto_spawn.see_providers_link": "See supported providers.", + "settings.mcp.button.delete_mcp": "Delete MCP", + "settings.mcp.button.edit_variables": "Edit Variables", + "settings.mcp.button.remove_from_team": "Remove from team", + "settings.mcp.button.save": "Save", + "settings.mcp.chip.from_another_device": "From another device", + "settings.mcp.chip.shared_by": "Shared by: {creator}", + "settings.mcp.chip.shared_by_team_member": "Shared by a team member", + "settings.mcp.chip.shared_from_team": "Shared from team", + "settings.mcp.confirm.delete_local.description": "This will uninstall and remove this MCP server from all your devices.", + "settings.mcp.confirm.delete_local.title": "Delete MCP server?", + "settings.mcp.confirm.delete_shared.description": "This will not only delete this MCP server for yourself, but also uninstall and remove this MCP server from Warp and across all of your teammates' devices.", + "settings.mcp.confirm.delete_shared.title": "Delete shared MCP server?", + "settings.mcp.confirm.unshare.description": "This will uninstall and remove this MCP server from Warp and across all of your teammates' devices.", + "settings.mcp.confirm.unshare.title": "Remove shared MCP server from team?", + "settings.mcp.edit_disabled_banner": "Only team admins and the creator of the MCP server can edit the MCP server.", + "settings.mcp.empty_state": "Once you add a MCP server, it will be shown here.", + "settings.mcp.error.cannot_add_multiple": "Cannot add multiple MCP servers while editing a single server.", + "settings.mcp.error.cannot_install_from_link": "MCP server '{name}' cannot be installed from this link.", + "settings.mcp.error.contains_secrets": "This MCP server contains secrets. Visit Settings > Privacy to modify your secret redaction settings.", + "settings.mcp.error.finish_install_first": "Finish the current MCP install before opening another install link.", + "settings.mcp.error.no_server_specified": "No MCP Server specified.", + "settings.mcp.error.unknown_server": "Unknown MCP server '{name}'", + "settings.mcp.markdown_parse_failed": "Failed to parse markdown: {error}", + "settings.mcp.modal.install_title": "Install {name}", + "settings.mcp.modal.multiple_updates_description": "This server has {count} updates available, which would you like to proceed with?", + "settings.mcp.modal.update_title": "Update {name}", + "settings.mcp.no_search_results": "No search results found", + "settings.mcp.no_server_selected": "No MCP server selected", + "settings.mcp.no_updates_available": "No updates available", + "settings.mcp.page.description": "Add MCP servers to extend the Warp Agent's capabilities. MCP servers expose data sources or tools to agents through a standardized interface, essentially acting like plugins. Add a custom server, or use the presets to get started with popular servers. You can also find team servers that have been shared with you here. ", + "settings.mcp.page.learn_more_link": "Learn more.", + "settings.mcp.page.title": "MCP Servers", + "settings.mcp.search.placeholder": "Search MCP Servers", + "settings.mcp.section.detected_from": "Detected from {provider}", + "settings.mcp.section.my_mcps": "My MCPs", + "settings.mcp.section.shared_by_warp_and_devices": "Shared by Warp and from other devices", + "settings.mcp.section.shared_by_warp_and_team": "Shared by Warp and {name}", + "settings.mcp.section.shared_from_warp": "Shared from Warp", + "settings.mcp.server_fallback": "Server", + "settings.mcp.status.authenticating": "Authenticating...", + "settings.mcp.status.available_to_install": "Available to install", + "settings.mcp.status.detected_from_config": "Detected from config file", + "settings.mcp.status.offline": "Offline", + "settings.mcp.status.shutting_down": "Shutting down...", + "settings.mcp.status.starting_server": "Starting server...", + "settings.mcp.title.add_new": "Add New MCP Server", + "settings.mcp.title.edit": "Edit MCP Server", + "settings.mcp.title.edit_named": "Edit {name} MCP Server", + "settings.mcp.toast.logged_out": "Successfully logged out of MCP server", + "settings.mcp.toast.logged_out_named": "Successfully logged out of {name} MCP server", + "settings.mcp.toast.server_updated": "MCP server updated", + "settings.mcp.tools.count_available": "{count} tools available", + "settings.mcp.tools.none_available": "No tools available", + "settings.mcp.tooltip.log_out": "Log out", + "settings.mcp.update_option.another_device": "another device", + "settings.mcp.update_option.from": "Update from {source}", + "settings.mcp.update_option.team_member": "a team member", + "settings.mcp.update_option.version": "Version {version}", + "settings.nav.about": "About", + "settings.nav.account": "Account", + "settings.nav.agent_mcp_servers": "MCP servers", + "settings.nav.agent_profiles": "Profiles", + "settings.nav.ai": "AI", + "settings.nav.appearance": "Appearance", + "settings.nav.billing_and_usage": "Billing and usage", + "settings.nav.cloud_environments": "Environments", + "settings.nav.code": "Code", + "settings.nav.code_indexing": "Indexing and projects", + "settings.nav.editor_and_code_review": "Editor and Code Review", + "settings.nav.features": "Features", + "settings.nav.keybindings": "Keyboard shortcuts", + "settings.nav.knowledge": "Knowledge", + "settings.nav.mcp_servers": "MCP Servers", + "settings.nav.oz_cloud_api_keys": "Oz Cloud API Keys", + "settings.nav.privacy": "Privacy", + "settings.nav.referrals": "Referrals", + "settings.nav.shared_blocks": "Shared blocks", + "settings.nav.teams": "Teams", + "settings.nav.third_party_cli_agents": "Third party CLI agents", + "settings.nav.umbrella.agents": "Agents", + "settings.nav.umbrella.cloud_platform": "Cloud platform", + "settings.nav.umbrella.code": "Code", + "settings.nav.warp_agent": "Warp Agent", + "settings.nav.warp_drive": "Warp Drive", + "settings.nav.warpify": "Warpify", + "settings.open_settings_file": "Open settings file", + "settings.platform.api_key_deleted_toast": "API key deleted", + "settings.platform.api_key.agent.description": "This API key is tied to an agent and can make requests on behalf of the agent.", + "settings.platform.api_key.create_agent": "Create agent", + "settings.platform.api_key.create_failed": "Failed to create API key. Please try again.", + "settings.platform.api_key.create_key": "Create key", + "settings.platform.api_key.creating": "Creating…", + "settings.platform.api_key.delete_failed": "Failed to delete API key. Please try again.", + "settings.platform.api_key.expiration.never": "Never", + "settings.platform.api_key.expiration.ninety_days": "90 days", + "settings.platform.api_key.expiration.one_day": "1 day", + "settings.platform.api_key.expiration.thirty_days": "30 days", + "settings.platform.api_key.load_agents_failed": "Failed to load agents. Please close and try again.", + "settings.platform.api_key.name_placeholder": "Warp API Key", + "settings.platform.api_key.no_agents_available": "No agents available. Create one first.", + "settings.platform.api_key.no_current_team_error": "Unable to create a team API key because there is no current team.", + "settings.platform.api_key.personal.description": "This API key is tied to your user and can make requests against your Warp account.", + "settings.platform.api_key.secret_copied": "Secret key copied.", + "settings.platform.api_key.secret_notice": "This secret key is shown only once. Copy and store it securely.", + "settings.platform.api_key.select_agent_error": "Please select an agent.", + "settings.platform.api_key.team.description": "This API key is tied to your team and can make requests on behalf of your team.", + "settings.platform.api_key.type.agent": "Agent", + "settings.platform.api_key.type.personal": "Personal", + "settings.platform.api_key.type.team": "Team", + "settings.platform.column_created": "Created", + "settings.platform.column_expires_at": "Expires at", + "settings.platform.column_key": "Key", + "settings.platform.column_last_used": "Last used", + "settings.platform.column_name": "Name", + "settings.platform.column_scope": "Scope", + "settings.platform.create_api_key_button": "+ Create API Key", + "settings.platform.description": "Create and manage API keys to allow other Oz cloud agents to access your Warp account.\nFor more information, visit the ", + "settings.platform.documentation_link": "Documentation.", + "settings.platform.never": "Never", + "settings.platform.new_api_key_title": "New API key", + "settings.platform.no_search_results": "No API keys match your search", + "settings.platform.save_your_key_title": "Save your key", + "settings.platform.scope_agent": "Agent", + "settings.platform.scope_personal": "Personal", + "settings.platform.scope_team": "Team", + "settings.platform.search_api_keys_placeholder": "Search API keys", + "settings.platform.section_header": "Oz Cloud API Keys", + "settings.platform.zero_state_description": "Create a key to manage external access to Warp", + "settings.platform.zero_state_title": "No API Keys", + "settings.privacy.action.app_analytics": "app analytics", + "settings.privacy.action.cloud_ai_conversation_storage": "cloud AI conversation storage", + "settings.privacy.action.crash_reporting": "crash reporting", + "settings.privacy.add_all_button": "Add all", + "settings.privacy.add_regex_button": "Add regex", + "settings.privacy.add_regex_modal.title": "Add regex pattern", + "settings.privacy.cloud_conversation_storage.description_disabled": "Agent conversations are only stored locally on your machine, are lost upon logout, and cannot be shared. Note: conversation data for ambient agents are still stored in the cloud.", + "settings.privacy.cloud_conversation_storage.description_enabled": "Agent conversations can be shared with others and are retained when you log in on different devices. This data is only stored for product functionality, and Warp will not use it for analytics.", + "settings.privacy.cloud_conversation_storage.title": "Store AI conversations in the cloud", + "settings.privacy.crash_reports.description": "Crash reports assist with debugging and stability improvements.", + "settings.privacy.crash_reports.title": "Send crash reports", + "settings.privacy.custom_secret_redaction.description": "Use regex to define additional secrets or data you'd like to redact. This will take effect when the next command runs. You can use the inline (?i) flag as a prefix to your regex to make it case-insensitive.", + "settings.privacy.custom_secret_redaction.title": "Custom secret redaction", + "settings.privacy.data_management.description": "At any time, you may choose to delete your Warp account permanently. You will no longer be able to use Warp.", + "settings.privacy.data_management.link": "Visit the data management page", + "settings.privacy.data_management.title": "Manage your data", + "settings.privacy.enabled_by_organization": "Enabled by your organization.", + "settings.privacy.enterprise_redaction_readonly": "Enterprise secret redaction cannot be modified.", + "settings.privacy.managed_by_organization": "This setting is managed by your organization.", + "settings.privacy.network_log.description": "We've built a native console that allows you to view all communications from Warp to external servers to ensure you feel comfortable that your work is always kept safe.", + "settings.privacy.network_log.link": "View network logging", + "settings.privacy.network_log.title": "Network log console", + "settings.privacy.no_enterprise_regexes": "No enterprise regexes have been configured by your organization.", + "settings.privacy.page_title": "Privacy", + "settings.privacy.privacy_policy.link": "Read Warp's privacy policy", + "settings.privacy.privacy_policy.title": "Privacy policy", + "settings.privacy.recommended_header": "Recommended", + "settings.privacy.regex.add": "Add regex", + "settings.privacy.regex.invalid": "Invalid regex", + "settings.privacy.regex.name_optional": "Name (optional)", + "settings.privacy.regex.name_placeholder": "e.g. \"Google API Key\"", + "settings.privacy.regex.pattern": "Regex pattern", + "settings.privacy.safe_mode.description": "When this setting is enabled, Warp will scan blocks, the contents of Warp Drive objects, and Oz prompts for potential sensitive information and prevent saving or sending this data to any servers. You can customize this list via regexes.", + "settings.privacy.safe_mode.title": "Secret redaction", + "settings.privacy.secret_display_mode.always_show": "Always show secrets", + "settings.privacy.secret_display_mode.asterisks": "Asterisks", + "settings.privacy.secret_display_mode.description": "Choose how secrets are visually presented in the block list while keeping them searchable. This setting only affects what you see in the block list.", + "settings.privacy.secret_display_mode.label": "Secret visual redaction mode", + "settings.privacy.secret_display_mode.strikethrough": "Strikethrough", + "settings.privacy.tab.enterprise": "Enterprise", + "settings.privacy.tab.personal": "Personal", + "settings.privacy.telemetry.description": "App analytics help us make the product better for you. We may collect certain console interactions to improve Warp's AI capabilities.", + "settings.privacy.telemetry.description_enterprise": "App analytics help us make the product better for you. We only collect app usage metadata, never console input or output.", + "settings.privacy.telemetry.free_tier_note": "On the free tier, analytics must be enabled to use AI features.", + "settings.privacy.telemetry.read_more_link": "Read more about Warp's use of data", + "settings.privacy.telemetry.title": "Help improve Warp", + "settings.privacy.zdr.tooltip": "Your administrator has enabled zero data retention for your team. User generated content will never be collected.", + "settings.referrals.anonymous_header": "Sign up to participate in Warp's referral program", + "settings.referrals.copy_link": "Copy link", + "settings.referrals.current_referral_plural": "Current referrals", + "settings.referrals.current_referral_singular": "Current referral", + "settings.referrals.email_label": "Email", + "settings.referrals.error_empty_email": "Please enter an email.", + "settings.referrals.error_invalid_email_prefix": "Please ensure the following email is valid: ", + "settings.referrals.header": "Invite a friend to Warp", + "settings.referrals.link_error": "Failed to load referral code.", + "settings.referrals.link_label": "Link", + "settings.referrals.loading": "Loading...", + "settings.referrals.reward_backpack": "Backpack", + "settings.referrals.reward_cap": "Baseball cap", + "settings.referrals.reward_hoodie": "Hoodie", + "settings.referrals.reward_hydroflask": "Premium Hydro Flask", + "settings.referrals.reward_intro": "Get exclusive Warp goodies when you refer someone*", + "settings.referrals.reward_keycaps": "Keycaps + stickers", + "settings.referrals.reward_notebook": "Notebook", + "settings.referrals.reward_theme": "Exclusive theme", + "settings.referrals.reward_tshirt": "T-shirt", + "settings.referrals.send": "Send", + "settings.referrals.sending": "Sending...", + "settings.referrals.sign_up": "Sign up", + "settings.referrals.terms_contact": " If you have any questions about the referral program, please contact referrals@warp.dev.", + "settings.referrals.terms_link": "Certain restrictions apply.", + "settings.referrals.toast_email_failure": "Failed to send emails. Please try again.", + "settings.referrals.toast_email_success": "Successfully sent emails.", + "settings.referrals.toast_link_copied": "Link copied.", + "settings.reset_to_default": "Reset to default", + "settings.schema.accessibility.accessibility_verbosity.description": "The verbosity level for screen reader announcements.", + "settings.schema.account.is_settings_sync_enabled.description": "Whether settings are synced across devices via the cloud.", + "settings.schema.agents.cloud_conversation_storage_enabled.description": "Whether conversations are stored in the cloud.", + "settings.schema.agents.knowledge.rules_enabled.description": "Whether the agent uses your saved rules during requests.", + "settings.schema.agents.knowledge.warp_drive_context_enabled.description": "Whether Warp Drive context is included in AI requests.", + "settings.schema.agents.mcp_servers.file_based_mcp_enabled.description": "Whether third-party file-based MCP servers are automatically detected.", + "settings.schema.agents.profiles.agent_mode_coding_file_read_allowlist.description": "File paths the agent can read without asking for permission.", + "settings.schema.agents.profiles.agent_mode_coding_permissions.description": "The file read permission level for the agent.", + "settings.schema.agents.profiles.agent_mode_command_execution_allowlist.description": "Commands that the agent can execute without explicit permission.", + "settings.schema.agents.profiles.agent_mode_command_execution_denylist.description": "Commands that the agent must always ask before executing.", + "settings.schema.agents.profiles.agent_mode_execute_readonly_commands.description": "Whether the agent can auto-execute read-only commands without asking.", + "settings.schema.agents.third_party.auto_dismiss_composer_after_submit.description": "Whether CLI agent Rich Input automatically closes after the user submits a prompt.", + "settings.schema.agents.third_party.auto_open_composer_on_cli_agent_start.description": "Whether CLI agent Rich Input automatically opens when a CLI agent session starts.", + "settings.schema.agents.third_party.auto_toggle_composer.description": "Whether CLI agent Rich Input automatically closes and reopens based on the agent's blocked state.", + "settings.schema.agents.third_party.cli_agent_toolbar_chip_selection_setting.description": "Controls the layout of context chips in the CLI Agent toolbar.", + "settings.schema.agents.third_party.cli_agent_toolbar_enabled_commands.description": "Maps custom toolbar command patterns to specific CLI agents.", + "settings.schema.agents.third_party.should_render_cli_agent_toolbar.description": "Whether to show the CLI agent footer for coding agent commands.", + "settings.schema.agents.voice.voice_input_enabled.description": "Controls whether voice input is enabled for AI interactions.", + "settings.schema.agents.voice.voice_input_toggle_key.description": "The key used to toggle voice input.", + "settings.schema.agents.warp_agent.active_ai.agent_mode_query_suggestions_enabled.description": "Controls whether prompt suggestions are shown in agent mode.", + "settings.schema.agents.warp_agent.active_ai.code_suggestions_enabled.description": "Controls whether AI code suggestions are enabled.", + "settings.schema.agents.warp_agent.active_ai.enabled.description": "Controls whether proactive AI features like suggestions are enabled.", + "settings.schema.agents.warp_agent.active_ai.git_operations_autogen_enabled.description": "Controls whether AI auto-generates commit messages and PR title/body in the code review dialogs.", + "settings.schema.agents.warp_agent.active_ai.intelligent_autosuggestions_enabled.description": "Controls whether AI-powered intelligent autosuggestions are enabled.", + "settings.schema.agents.warp_agent.active_ai.natural_language_autosuggestions_enabled.description": "Controls whether ghosted text autosuggestions are shown for AI input queries.", + "settings.schema.agents.warp_agent.active_ai.rule_suggestions_enabled.description": "Controls whether the agent suggests rules to save after responses.", + "settings.schema.agents.warp_agent.active_ai.shared_block_title_generation_enabled.description": "Controls whether titles are auto-generated when sharing blocks.", + "settings.schema.agents.warp_agent.input.agent_toolbar_chip_selection_setting.description": "Controls the layout of context chips in the Agent Mode toolbar.", + "settings.schema.agents.warp_agent.input.ai_auto_detection_enabled.description": "Controls whether AI automatically detects natural language input.", + "settings.schema.agents.warp_agent.input.ai_command_denylist.description": "Commands to exclude from AI natural language autodetection.", + "settings.schema.agents.warp_agent.input.include_agent_commands_in_history.description": "Whether agent-executed commands are included in command history.", + "settings.schema.agents.warp_agent.input.nld_in_terminal_enabled.description": "Controls whether natural language detection is enabled in the terminal input.", + "settings.schema.agents.warp_agent.input.show_agent_tips.description": "Whether agent tips are displayed in the input.", + "settings.schema.agents.warp_agent.input.show_model_selectors_in_prompt.description": "Whether to show AI model selectors in the input prompt.", + "settings.schema.agents.warp_agent.is_any_ai_enabled.description": "Controls whether all AI features are enabled.", + "settings.schema.agents.warp_agent.other.agent_attribution_enabled.description": "Whether the Warp Agent adds an attribution co-author line to commit messages and pull requests it creates.", + "settings.schema.agents.warp_agent.other.auto_handoff_on_sleep_enabled.description": "Whether Warp automatically hands off local agent conversations to cloud when the computer is about to sleep.", + "settings.schema.agents.warp_agent.other.cloud_agent_computer_use_enabled.description": "Whether computer use is enabled for cloud agent conversations.", + "settings.schema.agents.warp_agent.other.open_conversation_layout_preference.description": "Whether to open agent conversations in a new tab or a split pane.", + "settings.schema.agents.warp_agent.other.should_force_disable_ampersand_handoff.description": "Whether to force-disable the & prefix for cloud handoff compose mode.", + "settings.schema.agents.warp_agent.other.should_force_disable_cloud_handoff.description": "Whether to force-disable local-to-cloud handoff.", + "settings.schema.agents.warp_agent.other.should_render_use_agent_toolbar_for_user_commands.description": "Whether to show the \"Use Agent\" footer for terminal commands.", + "settings.schema.agents.warp_agent.other.should_show_oz_updates_in_zero_state.description": "Whether the \"What's new\" section is shown in the agent view.", + "settings.schema.agents.warp_agent.other.show_agent_notifications.description": "Whether agent notifications are shown.", + "settings.schema.agents.warp_agent.other.show_conversation_history.description": "Whether conversation history appears in the tools panel.", + "settings.schema.agents.warp_agent.other.thinking_display_mode.description": "Controls how agent thinking traces are displayed after streaming.", + "settings.schema.appearance.blocks.should_show_bootstrap_block.description": "Whether the bootstrap block is visible in the terminal.", + "settings.schema.appearance.blocks.should_show_in_band_command_blocks.description": "Whether in-band command blocks are visible in the terminal.", + "settings.schema.appearance.blocks.should_show_ssh_block.description": "Whether the SSH connection block is visible in the terminal.", + "settings.schema.appearance.blocks.show_block_dividers.description": "Whether to show dividers between terminal blocks.", + "settings.schema.appearance.blocks.show_jump_to_bottom_of_block_button.description": "Whether to show the jump-to-bottom button in long command output.", + "settings.schema.appearance.cursor.cursor_blink.description": "Whether the cursor blinks.", + "settings.schema.appearance.cursor.cursor_display_type.description": "The visual style of the cursor.", + "settings.schema.appearance.full_screen_apps.alt_screen_padding.description": "Controls padding around full-screen terminal applications.", + "settings.schema.appearance.icon.app_icon.description": "The app icon displayed in the dock.", + "settings.schema.appearance.input.input_mode.description": "The position of the terminal input.", + "settings.schema.appearance.language.description": "The display language for the Warp UI.", + "settings.schema.appearance.panes.focus_pane_on_hover.description": "Whether panes are focused when hovered over.", + "settings.schema.appearance.panes.should_dim_inactive_panes.description": "Whether inactive panes are visually dimmed.", + "settings.schema.appearance.spacing.description": "Controls the spacing between terminal blocks.", + "settings.schema.appearance.tabs.directory_tab_colors.description": "Mapping of directory paths to their tab color assignments.", + "settings.schema.appearance.tabs.header_toolbar_chip_selection.description": "Configuration for the header toolbar chips in the vertical tab panel header.", + "settings.schema.appearance.tabs.preserve_active_tab_color.description": "Whether to preserve the active tab's color when switching tabs.", + "settings.schema.appearance.tabs.show_indicators_button.description": "Whether to show activity indicators on tabs.", + "settings.schema.appearance.tabs.tab_close_button_position.description": "Position of the close button on tabs.", + "settings.schema.appearance.tabs.workspace_decoration_visibility.description": "When workspace decorations such as the tab bar are visible.", + "settings.schema.appearance.text.ai_font_name.description": "The font used for AI-generated content.", + "settings.schema.appearance.text.enforce_minimum_contrast.description": "Whether to enforce minimum contrast for text readability.", + "settings.schema.appearance.text.font_name.description": "The monospace font used in the terminal.", + "settings.schema.appearance.text.font_size.description": "The size of the monospace font in the terminal.", + "settings.schema.appearance.text.font_weight.description": "The weight of the monospace font in the terminal.", + "settings.schema.appearance.text.ligature_rendering_enabled.description": "Whether to render font ligatures in the terminal.", + "settings.schema.appearance.text.line_height_ratio.description": "The line height ratio for terminal text.", + "settings.schema.appearance.text.match_ai_font.description": "Whether the AI font automatically matches the terminal font.", + "settings.schema.appearance.text.match_notebook_to_monospace_font_size.description": "Whether the notebook font size matches the terminal font size.", + "settings.schema.appearance.text.notebook_font_size.description": "The font size used in notebooks.", + "settings.schema.appearance.text.use_thin_strokes.description": "Whether to use thin font strokes on macOS.", + "settings.schema.appearance.themes.selected_system_themes.description": "The themes to use for system light and dark modes.", + "settings.schema.appearance.themes.system_theme.description": "Whether to match the system light/dark theme.", + "settings.schema.appearance.themes.theme.description": "The color theme.", + "settings.schema.appearance.vertical_tabs.compact_subtitle.description": "Subtitle shown on compact vertical tabs.", + "settings.schema.appearance.vertical_tabs.display_granularity.description": "Granularity of rows displayed in the vertical tabs panel.", + "settings.schema.appearance.vertical_tabs.enabled.description": "Whether to display tabs vertically instead of horizontally.", + "settings.schema.appearance.vertical_tabs.primary_info.description": "The primary information displayed on vertical tabs.", + "settings.schema.appearance.vertical_tabs.show_details_on_hover.description": "Whether to show a details sidecar when hovering over a vertical tab.", + "settings.schema.appearance.vertical_tabs.show_diff_stats.description": "Whether to show diff stats on vertical tabs.", + "settings.schema.appearance.vertical_tabs.show_panel_in_restored_windows.description": "When restoring a window, open the vertical tabs panel even if it was closed when the session was saved.", + "settings.schema.appearance.vertical_tabs.show_pr_link.description": "Whether to show PR links on vertical tabs.", + "settings.schema.appearance.vertical_tabs.tab_item_mode.description": "Tab item display mode in vertical tabs.", + "settings.schema.appearance.vertical_tabs.use_latest_prompt_as_title.description": "Whether vertical tab names for agent conversations use the latest user prompt.", + "settings.schema.appearance.vertical_tabs.view_mode.description": "Display mode for the vertical tab bar.", + "settings.schema.appearance.window.left_panel_visibility_across_tabs.description": "Whether the left panel visibility is shared across all tabs.", + "settings.schema.appearance.window.new_windows_num_columns.description": "The number of columns for new windows when using a custom size.", + "settings.schema.appearance.window.new_windows_num_rows.description": "The number of rows for new windows when using a custom size.", + "settings.schema.appearance.window.open_windows_at_custom_size.description": "Whether to open new windows at a custom size instead of the default.", + "settings.schema.appearance.window.override_blur_texture.description": "Whether to apply a blur texture to the window background.", + "settings.schema.appearance.window.override_blur.description": "The blur radius applied to the window background.", + "settings.schema.appearance.window.override_opacity.description": "The opacity of the window background, from 1 to 100 percent.", + "settings.schema.appearance.window.zoom_level.description": "The zoom level for the window, as a percentage.", + "settings.schema.cloud_platform.third_party_api_keys.aws_bedrock_auth_refresh_command.description": "The command to run to refresh AWS credentials for Bedrock.", + "settings.schema.cloud_platform.third_party_api_keys.aws_bedrock_auto_login.description": "Whether to automatically run the AWS login command when Bedrock credentials expire.", + "settings.schema.cloud_platform.third_party_api_keys.aws_bedrock_credentials_enabled.description": "Whether Warp should use your local AWS credentials for Bedrock-enabled requests.", + "settings.schema.cloud_platform.third_party_api_keys.aws_bedrock_profile.description": "The AWS profile name to use for Bedrock credentials.", + "settings.schema.cloud_platform.third_party_api_keys.can_use_warp_credits_with_byok.description": "Whether Warp credits can be used as a fallback for user-provided models.", + "settings.schema.code.editor.auto_open_code_review_pane_on_first_agent_change.description": "Whether to automatically open the code review pane when the agent makes its first change.", + "settings.schema.code.editor.open_code_panels_file_editor.description": "The editor used to open files from code panels.", + "settings.schema.code.editor.open_file_editor.description": "The editor used to open files.", + "settings.schema.code.editor.open_file_layout.description": "The layout used when opening files in the editor.", + "settings.schema.code.editor.prefer_markdown_viewer.description": "Whether to use the Markdown viewer when opening Markdown files.", + "settings.schema.code.editor.prefer_tabbed_editor_view.description": "Whether to prefer opening files in a tabbed editor view.", + "settings.schema.code.editor.show_code_review_button.description": "Whether to show the code review button on tabs.", + "settings.schema.code.editor.show_code_review_diff_stats.description": "Whether to show lines added/removed counts on the code review button.", + "settings.schema.code.editor.show_global_search.description": "Whether global file search is shown in the tools panel.", + "settings.schema.code.editor.show_project_explorer.description": "Whether the project explorer is shown in the tools panel.", + "settings.schema.code.editor.use_warp_as_default_editor.description": "Whether Warp is used as the default code editor.", + "settings.schema.code.indexing.agent_mode_codebase_context_auto_indexing.description": "Whether automatic codebase indexing is enabled.", + "settings.schema.code.indexing.agent_mode_codebase_context.description": "Whether codebase context is provided to the AI agent.", + "settings.schema.description": "JSON Schema for Warp settings ({channel} channel, {entry_count} settings)", + "settings.schema.experimental.async_find_enabled.description": "Use an improved implementation of find to keep the UI responsive while searching for matches on large outputs.", + "settings.schema.general.default_session_mode.description": "The default mode for new terminal sessions.", + "settings.schema.general.link_tooltip.description": "Whether to show a tooltip when hovering over links.", + "settings.schema.general.login_item.description": "Whether to launch Warp automatically when you log in.", + "settings.schema.general.mouse_scroll_multiplier.description": "The scroll speed multiplier for mouse scroll events.", + "settings.schema.general.new_tab_placement.description": "Where new tabs are placed in the tab bar.", + "settings.schema.general.quit_on_last_window_closed.description": "Whether to quit Warp when the last window is closed.", + "settings.schema.general.restore_session.description": "Whether to restore the previous session when Warp starts up.", + "settings.schema.general.should_confirm_close_session.description": "Whether to show a confirmation dialog when closing a session.", + "settings.schema.general.show_changelog_after_update.description": "Whether the changelog is shown after an update.", + "settings.schema.general.show_warning_before_quitting.description": "Whether to show a warning dialog before quitting Warp.", + "settings.schema.general.snackbar_enabled.description": "Whether to show snackbar notifications.", + "settings.schema.general.undo_close.enabled.description": "Whether the undo close feature is enabled.", + "settings.schema.general.undo_close.grace_period.description": "How long after closing a tab you can still undo the close.", + "settings.schema.general.user_native_preference.description": "Whether to prefer the native desktop app or the web app.", + "settings.schema.global_hotkey.dedicated_window.enabled.description": "Whether the dedicated hotkey window is enabled. Mutually exclusive with `global_hotkey.toggle_all_windows.enabled`; only one should be true at a time.", + "settings.schema.global_hotkey.dedicated_window.settings.description": "Configuration options for Quake Mode window behavior.", + "settings.schema.global_hotkey.toggle_all_windows.enabled.description": "Whether the hotkey that toggles visibility of all windows is enabled. Mutually exclusive with `global_hotkey.dedicated_window.enabled`; only one should be true at a time.", + "settings.schema.global_hotkey.toggle_all_windows.keybinding.description": "The keybinding used for the global activation hotkey. Format: modifiers (cmd, ctrl, alt, shift, meta) and a key joined by '-', e.g. \"cmd-shift-a\" or \"alt-enter\". Bindings are case-sensitive: when shift is present, the key must be its shifted form (e.g., \"ctrl-shift-E\", not \"ctrl-shift-e\").", + "settings.schema.keys.ctrl_tab_behavior_setting.description": "Controls the behavior of Ctrl+Tab.", + "settings.schema.notifications.preferences.description": "Notification preferences for terminal events.", + "settings.schema.notifications.toast_duration_secs.description": "How long notification toasts are displayed, in seconds.", + "settings.schema.privacy.crash_reporting_enabled.description": "Whether crash reports are sent.", + "settings.schema.privacy.custom_secret_regex_list.description": "Custom regex patterns for detecting and redacting secrets.", + "settings.schema.privacy.secret_redaction.enabled.description": "Whether secret redaction is enabled to detect and obscure secrets in terminal output.", + "settings.schema.privacy.secret_redaction.hide_secrets_in_block_list.description": "Whether to hide detected secrets in the block list using asterisks.", + "settings.schema.privacy.secret_redaction.secret_display_mode_setting.description": "Controls how detected secrets are visually displayed in the terminal.", + "settings.schema.privacy.telemetry_enabled.description": "Whether anonymous usage telemetry is collected.", + "settings.schema.session.new_session_shell_override.description": "The shell to use when opening a new session.", + "settings.schema.session.startup_shell_override.description": "The shell to use when Warp starts up.", + "settings.schema.session.working_directory_config.description": "Controls the working directory used when opening new sessions.", + "settings.schema.system.force_x11.description": "Whether to force X11 instead of Wayland on Linux.", + "settings.schema.system.linux_selection_clipboard.description": "Whether the Linux primary selection clipboard is used.", + "settings.schema.system.prefer_low_power_gpu.description": "Whether to prefer the integrated (low-power) GPU.", + "settings.schema.system.preferred_graphics_backend.description": "The preferred graphics backend on Windows.", + "settings.schema.terminal.copy_on_select.description": "Whether text is automatically copied to the clipboard when selected.", + "settings.schema.terminal.focus_reporting_enabled.description": "Whether to forward focus and blur events to full-screen terminal applications.", + "settings.schema.terminal.input.alias_expansion_enabled.description": "Whether shell alias expansion is enabled in the input.", + "settings.schema.terminal.input.at_context_menu_in_terminal_mode.description": "Whether the @ context menu is available in terminal mode.", + "settings.schema.terminal.input.autosuggestions.enabled.description": "Whether command autosuggestions are shown.", + "settings.schema.terminal.input.autosuggestions.keybinding_hint.description": "Whether autosuggestion keybinding hints are displayed.", + "settings.schema.terminal.input.autosuggestions.show_ignore_button.description": "Whether the ignore button is shown for autosuggestions.", + "settings.schema.terminal.input.classic_completions_mode.description": "Whether classic completions mode is enabled.", + "settings.schema.terminal.input.command_corrections.description": "Whether command corrections are suggested for mistyped commands.", + "settings.schema.terminal.input.completions_open_while_typing.description": "Whether the completions menu opens automatically while typing.", + "settings.schema.terminal.input.enable_slash_commands_in_terminal.description": "Whether slash commands are available in the terminal input.", + "settings.schema.terminal.input.error_underlining_enabled.description": "Whether command errors are underlined in the input.", + "settings.schema.terminal.input.extra_meta_keys.description": "Controls which additional keys are treated as meta keys.", + "settings.schema.terminal.input.honor_ps1.description": "Whether to use your shell's PS1 prompt instead of the Warp prompt.", + "settings.schema.terminal.input.input_box_type_setting.description": "The terminal input style.", + "settings.schema.terminal.input.middle_click_paste_enabled.description": "Whether middle-click pastes from the clipboard.", + "settings.schema.terminal.input.outline_codebase_symbols_for_at_context_menu.description": "Whether codebase symbols appear in the @ context menu.", + "settings.schema.terminal.input.show_hint_text.description": "Whether hint text is shown in the terminal input.", + "settings.schema.terminal.input.show_terminal_input_message_bar.description": "Whether the terminal input message bar is shown.", + "settings.schema.terminal.input.syntax_highlighting.description": "Whether syntax highlighting is enabled in the terminal input.", + "settings.schema.terminal.maximum_grid_size.description": "The maximum number of rows in the terminal grid.", + "settings.schema.terminal.mouse_reporting_enabled.description": "Whether to forward mouse events to full-screen terminal applications.", + "settings.schema.terminal.scroll_reporting_enabled.description": "Whether to forward scroll events to full-screen terminal applications.", + "settings.schema.terminal.show_terminal_zero_state_block.description": "Whether to show the AI zero-state block in new terminal sessions.", + "settings.schema.terminal.smart_select.enabled.description": "Whether double-click smart selection is enabled for URLs, emails, file paths, and identifiers.", + "settings.schema.terminal.smart_select.word_char_allowlist.description": "Characters that are considered part of a word for double-click selection when smart select is disabled.", + "settings.schema.terminal.use_audible_bell.description": "Whether to play an audible bell sound on terminal bell events.", + "settings.schema.text_editing.autocomplete_symbols.description": "Whether matching symbols like brackets and quotes are auto-completed.", + "settings.schema.text_editing.code_editor_line_number_mode.description": "How line numbers are displayed in code editors.", + "settings.schema.text_editing.vim_mode_enabled.description": "Whether Vim keybindings are enabled.", + "settings.schema.text_editing.vim_status_bar.description": "Whether the Vim status bar is displayed.", + "settings.schema.text_editing.vim_unnamed_system_clipboard.description": "Whether the Vim unnamed register uses the system clipboard.", + "settings.schema.title": "Warp Settings", + "settings.schema.warp_drive.enabled.description": "Whether Warp Drive is enabled.", + "settings.schema.warp_drive.sorting_choice.description": "The sort order for items in Warp Drive.", + "settings.schema.warpify.ssh.enable_legacy_ssh_wrapper.description": "Whether the legacy SSH wrapper is enabled for SSH sessions.", + "settings.schema.warpify.ssh.enable_ssh_warpification.description": "Whether to enable Warp features in SSH sessions.", + "settings.schema.warpify.ssh.ssh_extension_install_mode.description": "Controls SSH extension installation behavior.", + "settings.schema.warpify.ssh.ssh_hosts_denylist.description": "SSH hosts that should not trigger the warpification prompt.", + "settings.schema.warpify.ssh.use_ssh_tmux_wrapper.description": "Whether to use a tmux-based wrapper for SSH warpification.", + "settings.schema.warpify.subshells.added_subshell_commands.description": "Additional regex patterns for commands that should be recognized as subshells.", + "settings.schema.warpify.subshells.subshell_commands_denylist.description": "Commands that should not trigger the subshell warpification prompt.", + "settings.schema.workflows.show_global_workflows_in_universal_search.description": "Whether to show global workflows in universal search results.", + "settings.search.no_matches": "No settings match your search.", + "settings.search.no_matches_hint": "You may want to try using different keywords or checking for any possible typos.", + "settings.shared_blocks.unshare": "Unshare", + "settings.show_blocks.deleting": "Deleting...", + "settings.show_blocks.empty": "You don't have any shared blocks yet.", + "settings.show_blocks.executed_on": "Executed on: {date}", + "settings.show_blocks.link_copied": "Link copied.", + "settings.show_blocks.load_failed": "Failed to load blocks. Please try again.", + "settings.show_blocks.loading": "Getting blocks...", + "settings.show_blocks.title": "Shared blocks", + "settings.show_blocks.unshare_block": "Unshare block", + "settings.show_blocks.unshare_confirmation": "Are you sure you want to unshare this block?\n\nIt will no longer be accessible by link and will be permanently deleted from Warp servers.", + "settings.show_blocks.unshare_failed": "Failed to unshare block. Please try again.", + "settings.show_blocks.unshare_success": "Block was successfully unshared.", + "settings.startup_shell.custom": "Custom", + "settings.teams.action.cancel_invite": "Cancel invite", + "settings.teams.action.demote_from_admin": "Demote from admin", + "settings.teams.action.promote_to_admin": "Promote to admin", + "settings.teams.action.remove_domain": "Remove domain", + "settings.teams.action.remove_from_team": "Remove from team", + "settings.teams.action.transfer_ownership": "Transfer ownership", + "settings.teams.badge.past_due": "PAST DUE", + "settings.teams.badge.unpaid": "UNPAID", + "settings.teams.billing.compare_plans": "Compare plans", + "settings.teams.billing.upgrade_build": "Upgrade to Build", + "settings.teams.billing.upgrade_lightspeed": "Upgrade to Lightspeed plan", + "settings.teams.billing.upgrade_turbo": "Upgrade to Turbo plan", + "settings.teams.button.contact_admin_request": "Contact Admin to request access", + "settings.teams.button.contact_support": "Contact support", + "settings.teams.button.create": "Create", + "settings.teams.button.delete_team": "Delete team", + "settings.teams.button.invite": "Invite", + "settings.teams.button.join": "Join", + "settings.teams.button.leave_team": "Leave team", + "settings.teams.button.manage_billing": "Manage billing", + "settings.teams.button.manage_plan": "Manage plan", + "settings.teams.button.open_admin_panel": "Open admin panel", + "settings.teams.button.set_domains": "Set", + "settings.teams.chip.admin": "ADMIN", + "settings.teams.chip.expired": "EXPIRED", + "settings.teams.chip.owner": "OWNER", + "settings.teams.chip.pending": "PENDING", + "settings.teams.cost_info.per_seat_no_price": "Additional members are billed at your plan's per-user rate. {prorated}", + "settings.teams.cost_info.per_seat_with_price": "Additional members are billed at your plan's per-user rate: ${monthly}/month or ${yearly}/year, depending on your billing interval. {prorated}", + "settings.teams.cost_info.prorated_admin": "You'll be charged for a portion of the team member's usage of Warp.", + "settings.teams.cost_info.prorated_member": "Your admin will be charged for a portion of the team member's usage of Warp.", + "settings.teams.create.description": "When you create a team, you can collaborate on agent-driven development by sharing cloud agent runs, environments, automations, and artifacts. You can also create a shared knowledge store for teammates and agents alike.", + "settings.teams.create.discovery_same_domain": "Allow Warp users with the same email domain as you to find and join the team.", + "settings.teams.create.name_placeholder": "Team name", + "settings.teams.create.subtitle": "Create a team", + "settings.teams.create.title": "Teams", + "settings.teams.discovery.join_cta": "Join this team and start collaborating on workflows, notebooks, and more.", + "settings.teams.discovery.join_existing_subtitle": "Or, join an existing team within your company", + "settings.teams.discovery.teammate_count_plural": "{count} teammates", + "settings.teams.discovery.teammate_count_single": "1 teammate", + "settings.teams.invite.by_discovery_header": "By discovery", + "settings.teams.invite.by_email_header": "By email", + "settings.teams.invite.by_link_header": "By link", + "settings.teams.invite.discovery_instructions": "Allow Warp users with an @{domain} email to find and join the team.", + "settings.teams.invite.domain_restrictions_instructions": "Restrict by domain — only allow users with emails at specific domains to join your team through the invite link.", + "settings.teams.invite.domains_placeholder": "Domains, comma separated", + "settings.teams.invite.email_expiry_instructions": "Email invitations are valid for 7 days.", + "settings.teams.invite.emails_placeholder": "Emails, comma separated", + "settings.teams.invite.header": "Invite team members", + "settings.teams.invite.invalid_domains_instructions": "Some of the provided domains are invalid, or have already been added.", + "settings.teams.invite.invalid_emails_instructions": "Some of the provided email addresses are invalid, already invited, or members of the team.", + "settings.teams.invite.link_load_failed": "Failed to load invite link.", + "settings.teams.invite.link_toggle_instructions": "As an admin, you can choose whether to enable or disable the ability for team members to invite others by invitation link.", + "settings.teams.invite.reset_links": "Reset links", + "settings.teams.members.capacity_tooltip": "Your plan ({plan}) has a maximum capacity of {cap} members.", + "settings.teams.members.count_plural": "{count} team members", + "settings.teams.members.count_single": "1 team member", + "settings.teams.members.header": "Team members", + "settings.teams.offline_text": "You are offline.", + "settings.teams.outgrow.need_more_seats": "Need more seats? ", + "settings.teams.outgrow.upgrade_business_link": "Upgrade to Business", + "settings.teams.outgrow.upgrade_business_suffix": " for a higher team member limit.", + "settings.teams.outgrow.upgrade_enterprise_link": "Upgrade to Enterprise", + "settings.teams.outgrow.upgrade_enterprise_suffix": " for an unlimited team member limit.", + "settings.teams.plan_usage.free_limits_header": "Free plan usage limits", + "settings.teams.plan_usage.limits_header": "Plan usage limits", + "settings.teams.plan_usage.shared_notebooks": "Shared Notebooks", + "settings.teams.plan_usage.shared_workflows": "Shared Workflows", + "settings.teams.rename_placeholder": "Your new team name", + "settings.teams.toast.billing_link_failed": "Failed to generate billing link. Please contact us at feedback@warp.dev", + "settings.teams.toast.discoverability_toggle_failed": "Failed to toggle team discoverability", + "settings.teams.toast.discoverability_toggled": "Toggled team discoverability", + "settings.teams.toast.domain_add_failed": "Failed to add domain restriction", + "settings.teams.toast.domain_delete_failed": "Failed to delete domain restriction", + "settings.teams.toast.domains_added_count": "Domain restrictions added: {count}", + "settings.teams.toast.invalid_domains_count": "Invalid domains: {count}", + "settings.teams.toast.invalid_emails_count": "Invalid emails: {count}", + "settings.teams.toast.invite_delete_failed": "Failed to delete invite", + "settings.teams.toast.invite_deleted": "Deleted invite", + "settings.teams.toast.invite_links_reset": "Reset invite links", + "settings.teams.toast.invite_links_reset_failed": "Failed to reset invite links", + "settings.teams.toast.invite_links_toggle_failed": "Failed to toggle invite links", + "settings.teams.toast.invite_links_toggled": "Toggled invite links", + "settings.teams.toast.invite_send_failed": "Failed to send invite", + "settings.teams.toast.invite_sent_multiple": "Your {count} invites are on the way!", + "settings.teams.toast.invite_sent_single": "Your invite is on the way!", + "settings.teams.toast.join_failed": "Failed to join team", + "settings.teams.toast.join_success_generic": "Successfully joined team", + "settings.teams.toast.join_success_named": "Successfully joined {name}", + "settings.teams.toast.leave_failed": "Error leaving team", + "settings.teams.toast.leave_success": "Successfully left team", + "settings.teams.toast.link_copied": "Link copied to clipboard!", + "settings.teams.toast.ownership_transfer_failed": "Failed to transfer team ownership", + "settings.teams.toast.ownership_transferred": "Successfully transferred team ownership", + "settings.teams.toast.rename_failed": "Failed to rename team", + "settings.teams.toast.rename_success": "Successfully renamed team", + "settings.teams.toast.role_update_failed": "Failed to update team member role", + "settings.teams.toast.role_updated": "Successfully updated team member role", + "settings.teams.toast.upgrade_link_failed": "Failed to generate upgrade link. Please contact us at feedback@warp.dev", + "settings.teams.transfer_ownership.description": "Are you sure you want to transfer team ownership to {email}? You will no longer be the owner and will not be able to take any administrative actions for this team.", + "settings.teams.transfer_ownership.modal_title": "Transfer team ownership?", + "settings.teams.warning.cta_button_contact_support": "Contact support", + "settings.teams.warning.cta_button_update_billing": "Update billing", + "settings.teams.warning.cta_button_upgrade": "Upgrade", + "settings.teams.warning.cta_contact_admin_grow": "Contact a team admin to grow the team.", + "settings.teams.warning.cta_contact_admin_restore": "Contact a team admin to restore access.", + "settings.teams.warning.cta_contact_sales_grow": "Contact sales to grow your team.", + "settings.teams.warning.cta_contact_support_restore": "Contact support to restore access.", + "settings.teams.warning.cta_update_payment_restore": "Update your payment information to restore access.", + "settings.teams.warning.cta_upgrade_grow": "Upgrade to grow your team.", + "settings.teams.warning.member_limit_exceeded_title": "You've exceeded your member limit", + "settings.teams.warning.payment_past_due_body": "Team invites have been restricted due to a past-due payment.", + "settings.teams.warning.payment_past_due_title": "Payment past due", + "settings.teams.warning.payment_unpaid_body": "Team invites have been restricted due to an unpaid subscription.", + "settings.teams.warning.seat_cap_exceeded_body": "You've exceeded your plan's member limit. Existing team members keep their access, but you won't be able to add new members.", + "settings.teams.warning.seat_cap_reached_body": "You've reached your plan's member limit.", + "settings.teams.warning.subscription_unpaid_title": "Subscription unpaid", + "settings.teams.warning.team_full_title": "Your team is full", + "settings.title": "Settings", + "settings.tooltip.learn_more_docs": "Click to learn more in docs", + "settings.tooltip.not_synced": "This setting is not synced to your other devices", + "settings.transfer_ownership.transfer": "Transfer", + "settings.undo_close.enable_reopening": "Enable reopening of closed sessions", + "settings.undo_close.grace_period_seconds": "Grace period (seconds)", + "settings.warp_drive.action.disable": "Disable Warp Drive", + "settings.warp_drive.action.enable": "Enable Warp Drive", + "settings.warp_drive.create_account_required": "To use Warp Drive, please create an account.", + "settings.warp_drive.description": "Warp Drive is a workspace in your terminal where you can save Workflows, Notebooks, Prompts, and Environment Variables for personal use or to share with a team.", + "settings.warpify.action.ssh_session_detection": "SSH session detection for Warpification", + "settings.warpify.action.ssh_warpification": "SSH Warpification", + "settings.warpify.added_commands.title": "Added commands", + "settings.warpify.command_placeholder": "command (supports regex)", + "settings.warpify.denylisted_commands.title": "Denylisted commands", + "settings.warpify.denylisted_hosts.title": "Denylisted hosts", + "settings.warpify.description": "Configure whether Warp attempts to “Warpify” (add support for blocks, input modes, etc) certain shells. ", + "settings.warpify.host_placeholder": "host (supports regex)", + "settings.warpify.install_mode.always_ask": "Always ask", + "settings.warpify.install_mode.always_install": "Always install", + "settings.warpify.install_mode.never_install": "Never install", + "settings.warpify.install_ssh_extension.description": "Controls the installation behavior for Warp's SSH extension when a remote host doesn't have it installed.", + "settings.warpify.install_ssh_extension.label": "Install SSH extension", + "settings.warpify.learn_more": "Learn more", + "settings.warpify.ssh_sessions.label": "Warpify SSH Sessions", + "settings.warpify.ssh.subtitle": "Warpify your interactive SSH sessions.", + "settings.warpify.subshells.subtitle": "Subshells supported: bash, zsh, and fish.", + "settings.warpify.subshells.title": "Subshells", + "settings.warpify.tmux_description": "The tmux ssh wrapper works in many situations where the default one does not, but may require you to hit a button to warpify. Takes effect in new tabs.", + "settings.warpify.use_tmux.label": "Use Tmux Warpification", + "settings.working_directory.advanced": "Advanced", + "settings.working_directory.new_tab": "New tab", + "settings.working_directory.new_window": "New window", + "settings.working_directory.split_pane": "Split pane", + "settings.workspace_override_tooltip": "This option is enforced by your organization's settings and cannot be customized.", + "tab.menu.copy_branch": "Copy branch", + "tab.menu.copy_pane_title": "Copy pane title", + "tab.menu.copy_pull_request_link": "Copy pull request link", + "tab.menu.copy_tab_title": "Copy tab title", + "tab.menu.copy_working_directory": "Copy working directory", + "tab_configs.add_new_repo": "+ Add new repo...", + "tab_configs.already_default": "Already the default", + "tab_configs.auto_create_worktree": "Automatically create a worktree when opening a new tab", + "tab_configs.auto_create_worktree_required": "You must select that you want to automatically create a worktree in order to select this", + "tab_configs.auto_generate_worktree_branch_name": "Auto-generate worktree branch name", + "tab_configs.create_first_config": "Create your first tab config", + "tab_configs.create_first_config.subtitle_with_session_type": "Set up a reusable starting point for your tabs. Pick a repo, choose a session type, and optionally attach a worktree. Use it whenever you want to open a new tab with this setup.", + "tab_configs.create_first_config.subtitle_without_session_type": "Set up a reusable starting point for your tabs. Pick a repo, optionally attach a worktree, and use it whenever you want to open a new tab with this setup.", + "tab_configs.edit_config": "Edit config", + "tab_configs.fetching_branches": "Fetching branches...", + "tab_configs.get_warping": "Get Warping", + "tab_configs.make_default": "Make default", + "tab_configs.new_worktree.autogenerate_branch_name": "Autogenerate worktree branch name", + "tab_configs.new_worktree.branch_name": "Worktree branch name", + "tab_configs.new_worktree.invalid_branch_name": "Name can only contain letters, numbers, hyphens, and underscores", + "tab_configs.new_worktree.select_branch": "Select branch", + "tab_configs.new_worktree.select_repository": "Select repository", + "tab_configs.new_worktree.title": "New worktree", + "tab_configs.open_tab": "Open Tab", + "tab_configs.params.default_value": "Default: {default_value}", + "tab_configs.params.enter": "Enter {name}", + "tab_configs.remove.description": "This tab config will be permanently deleted. This action cannot be undone.", + "tab_configs.remove.title": "Remove '{name}'?", + "tab_configs.select_directory": "Select directory", + "tab_configs.select_git_repo_for_worktree": "Select a git repository to enable worktree support", + "tab_configs.session_type": "Session type", + "tab_configs.session_type.built_in_agent": "Built in agent", + "tab_configs.session_type.terminal": "Terminal", + "tab.close_other_tabs": "Close other tabs", + "tab.close_tab": "Close tab", + "tab.cloud_agent_run": "Cloud agent run", + "tab.move_to_group": "Move to group", + "tab.new_group_with_tab": "New group with tab", + "tab.rename_tab": "Rename tab", + "tab.reset_tab_name": "Reset tab name", + "tab.save_as_new_config": "Save as new config", + "terminal.a11y.block_background_status": "background", + "terminal.a11y.block_command_output": "Block {index}: {command}. Output: {output}", + "terminal.a11y.block_failed_status": "failed, status code {code}", + "terminal.a11y.block_in_progress_status": "in progress", + "terminal.a11y.block_output": "Block {index}.\nOutput: {output}", + "terminal.a11y.block_selection_help": "Press cmd-C to read and copy both command and output, and cmd-option-shift-C to read and copy output only. Press cmd-B to bookmark the block: you could navigate between bookmarked blocks quickly using option-up and option-down.", + "terminal.a11y.block_succeeded_status": "succeeded", + "terminal.a11y.block_summary": "Block {index}: {command}, {status}.\n", + "terminal.a11y.command_correction_help": "Press right arrow to insert or keep editing to ignore", + "terminal.a11y.command_correction_suggested": "Suggested corrected command: {command}", + "terminal.a11y.copied_block_outputs": "Copied {count} block outputs.\n{outputs}", + "terminal.a11y.copied_blocks": "Copied {count} blocks.\n{blocks}", + "terminal.a11y.execute_rewind_ai": "Execute rewind to before this point in the AI conversation.", + "terminal.a11y.open_ai_attached_blocks_menu": "Open list of blocks attached as context to this AI query.", + "terminal.a11y.open_ai_block_overflow_menu": "Open overflow menu with copy options for this AI block.", + "terminal.a11y.open_block_filter_editor": "Open block filter editor for block {index}", + "terminal.a11y.opened_file_search_palette": "Opened file search palette", + "terminal.a11y.opened_warpify_settings": "Opened Warpify Settings", + "terminal.a11y.oz_confirmation_continue": "Oz needs your confirmation to continue", + "terminal.a11y.oz_permission_edit_file": "Oz needs your permission to edit a file", + "terminal.a11y.oz_permission_read_files": "Oz needs your permission to read files", + "terminal.a11y.oz_permission_run_command": "Oz needs your permission to run `{command}`", + "terminal.a11y.oz_permission_search_codebase": "Oz needs your permission to search your codebase", + "terminal.a11y.oz_permission_write_to_shell": "Oz needs your permission to interact with a running shell command", + "terminal.a11y.pick_repo_to_open": "Use file picker to select a git repository", + "terminal.a11y.recognized": "{title} recognized.", + "terminal.a11y.rewind_ai_confirmation": "Show confirmation dialog to rewind to before this point in the AI conversation.", + "terminal.a11y.scrolled_bottom_selected_block": "Scrolled to bottom of selected block", + "terminal.a11y.scrolled_bottom_visible_block": "Scrolled to bottom of bottommost visible block", + "terminal.a11y.scrolled_top_selected_block": "Scrolled to top of selected block", + "terminal.a11y.select_ai_attached_block": "Click on a block attached as context to this AI query.", + "terminal.a11y.selected_all_blocks": "Selected all {count} blocks.", + "terminal.a11y.selected_blocks": "Selected {count} blocks.", + "terminal.a11y.showed_initialization_block": "Showed initialization block", + "terminal.a11y.toggle_bookmark_block": "Toggle Bookmark block", + "terminal.a11y.warpify_with_key": "You can press {key} to Warpify this {title} for more Warp features.", + "terminal.a11y.warpify_without_key": "You can Warpify this {title} for more Warp features.", + "terminal.agent_conversation.default_title": "New agent conversation", + "terminal.agent_message_bar.autodetected_shell_command": "autodetected shell command", + "terminal.agent_message_bar.autodetected_shell_command_prefix": "autodetected shell command, ", + "terminal.agent_message_bar.starting_shell": "Starting shell...", + "terminal.agent_message_bar.to_exit_shell_mode": "to exit shell mode", + "terminal.agent_view_header.for_terminal": "for terminal", + "terminal.ambient_agent.auth_secret.api_key": "API key", + "terminal.ambient_agent.auth_secret.choose_type": "Choose a type", + "terminal.ambient_agent.auth_secret.delete_a11y": "Delete API key {name}", + "terminal.ambient_agent.auth_secret.delete_confirmation": "Are you sure you want to delete {name}? This action cannot be undone. Any agents or environments referencing this secret will no longer have access to it.", + "terminal.ambient_agent.auth_secret.delete_failed": "Failed to delete API key '{name}': {error}", + "terminal.ambient_agent.auth_secret.deleted": "API key '{name}' deleted.", + "terminal.ambient_agent.auth_secret.enter_credentials": "Enter your credentials below.", + "terminal.ambient_agent.auth_secret.inherit_from_environment": "Inherit key from environment", + "terminal.ambient_agent.auth_secret.learn_more": "Learn more about authentication for {harness_name} in Warp.", + "terminal.ambient_agent.auth_secret.name_label": "NAME", + "terminal.ambient_agent.auth_secret.name_placeholder": "e.g. My API Key", + "terminal.ambient_agent.auth_secret.optional_label": "{label} (optional)", + "terminal.ambient_agent.auth_secret.save_failed": "Failed to save API key: {error}", + "terminal.ambient_agent.auth_secret.saved": "API key '{name}' saved.", + "terminal.ambient_agent.auth_secret.select_type_description": "Select an API key type to use {display_name} in the cloud with Oz.", + "terminal.ambient_agent.default_cloud_agent_title": "New cloud agent", + "terminal.ambient_agent.error.cloud_agent_failed": "Cloud agent failed", + "terminal.ambient_agent.footer.error_header": "Agent failed", + "terminal.ambient_agent.footer.loading_body": "You'll be able to interact with Oz soon", + "terminal.ambient_agent.footer.loading_header": "Cloud agent starting up…", + "terminal.ambient_agent.generic_agent_name": "Agent", + "terminal.ambient_agent.harness_selector.label": "Agent harness", + "terminal.ambient_agent.harness_selector.locked_to_warp": "This conversation is with the Warp Agent, so the cloud handoff will also use Warp", + "terminal.ambient_agent.harness_session.running": "Running {name}...", + "terminal.ambient_agent.host_selector.label": "Execution host", + "terminal.ambient_agent.model_selector.default": "default", + "terminal.ambient_agent.model_selector.no_results": "No results", + "terminal.ambient_agent.model_selector.search_placeholder": "Search models", + "terminal.ambient_agent.model_selector.tooltip": "Choose agent model", + "terminal.ambient_agent.setup_command.ran": "Ran setup commands", + "terminal.ambient_agent.setup_command.running": "Running setup commands...", + "terminal.ambient_agent.setup_status.connecting_to_host": "Connecting to Host (Step 1/3)", + "terminal.ambient_agent.setup_status.creating_environment": "Creating Environment (Step 2/3)", + "terminal.ambient_agent.setup_status.starting_environment": "Starting Environment (Step 3/3)", + "terminal.ambient_agent.status.agent_failed": "Agent failed", + "terminal.ambient_agent.status.agent_working": "Agent is working on task", + "terminal.ambient_agent.status.auth_required": "Authentication required", + "terminal.ambient_agent.status.cancelled": "Cancelled", + "terminal.ambient_agent.status.child_github_auth_required": "GitHub authentication required before starting the child agent.", + "terminal.ambient_agent.status.github_auth_required": "GitHub authentication required", + "terminal.ambient_agent.status.starting_environment": "Starting environment...", + "terminal.ambient_agent.tips.ci_failures_fix": "Build agents that respond to CI failures and attempt automatic fixes.", + "terminal.ambient_agent.tips.daily_issue_summaries": "Set up an agent to generate daily summaries of newly opened issues.", + "terminal.ambient_agent.tips.dashboard_agent_activity": "Build a dashboard that tracks all agent activity across your team.", + "terminal.ambient_agent.tips.fork_completed_session": "Fork a completed Oz cloud agent session into Warp to continue the work locally.", + "terminal.ambient_agent.tips.format_lint_schedule": "Build an agent that automatically formats and lints code on a schedule.", + "terminal.ambient_agent.tips.github_actions_agent_action": "Run agents from GitHub Actions using the `oz-agent-action`.", + "terminal.ambient_agent.tips.internal_slack_bot": "Build an internal Slack bot that delegates coding tasks to Oz agents.", + "terminal.ambient_agent.tips.internal_tools_databases": "Build internal tools that use agents to answer questions from your databases.", + "terminal.ambient_agent.tips.join_run_realtime": "Join any Oz cloud agent run in real-time using Agent Session Sharing.", + "terminal.ambient_agent.tips.linear_fix_bugs": "Create agents that automatically fix bugs when issues are filed in Linear.", + "terminal.ambient_agent.tips.linear_tag_oz": "Tag @Oz in Linear issues to automatically investigate and propose fixes.", + "terminal.ambient_agent.tips.mcp_servers_access": "Configure MCP servers to give Oz cloud agents access to GitHub, Linear, and Sentry.", + "terminal.ambient_agent.tips.monitor_success_rates": "Monitor agent success rates and runtimes using the Oz API.", + "terminal.ambient_agent.tips.nightly_dependency_updates": "Create an agent that runs nightly to check for dependency updates.", + "terminal.ambient_agent.tips.oz_agent_run": "Use `oz agent run` to kick off tasks without opening the Warp terminal.", + "terminal.ambient_agent.tips.oz_environment_create": "Use `oz environment create` to define reproducible execution contexts.", + "terminal.ambient_agent.tips.oz_mcp_list": "Use `oz mcp list` to see which MCP servers are available to your agents.", + "terminal.ambient_agent.tips.oz_schedule_create": "Use `oz schedule create` to set up cron-triggered agents.", + "terminal.ambient_agent.tips.oz_schedule_pause": "Pause and resume scheduled agents without deleting them using `oz schedule pause`.", + "terminal.ambient_agent.tips.personal_secrets": "Use personal secrets for credentials that should only be used by your agents.", + "terminal.ambient_agent.tips.programmatic_agents_sdk": "Build programmatic agents using Oz's TypeScript and Python SDKs.", + "terminal.ambient_agent.tips.python_sdk": "Use the Oz Python SDK to integrate agents into your data pipelines.", + "terminal.ambient_agent.tips.recurring_cron": "Set up recurring agents that run on cron schedules for automated maintenance.", + "terminal.ambient_agent.tips.remote_dev_boxes": "Run agents on remote dev boxes or CI runners using the Oz CLI.", + "terminal.ambient_agent.tips.rest_api_trigger": "Call the Oz REST API to trigger agents from any backend service or internal tool.", + "terminal.ambient_agent.tips.restart_services_alerts": "Build an agent that restarts services or scales deployments when alerts fire.", + "terminal.ambient_agent.tips.reusable_environments": "Create reusable environments with Docker images for consistent agent execution.", + "terminal.ambient_agent.tips.review_prs": "Create an agent that automatically reviews PRs and suggests improvements.", + "terminal.ambient_agent.tips.scheduled_feature_flags": "Create a scheduled agent to clean up stale feature flags every week.", + "terminal.ambient_agent.tips.set_secrets": "Set team or personal secrets for agents using the `oz secret` command.", + "terminal.ambient_agent.tips.share_flag": "Use the `--share` flag with the Oz CLI to enable session sharing from anywhere.", + "terminal.ambient_agent.tips.share_session_links": "Share agent session links with your team for collaborative debugging.", + "terminal.ambient_agent.tips.slack_integration_trigger": "Install the Oz Slack integration to trigger agents from any channel or DM.", + "terminal.ambient_agent.tips.slack_mentions": "Create an agent that responds to @mentions in Slack threads with full context.", + "terminal.ambient_agent.tips.team_secrets": "Use team secrets for shared infrastructure credentials across all agents.", + "terminal.ambient_agent.tips.teammates_runs": "View your teammates' agent runs in the Oz web app for shared visibility.", + "terminal.ambient_agent.tips.triage_github_issues": "Build agents that automatically triage and label incoming GitHub issues.", + "terminal.ambient_agent.tips.typescript_sdk": "Use the Oz TypeScript SDK to build custom automation pipelines.", + "terminal.ambient_agent.tips.view_runs_status": "View all your agent runs and their status in the Oz web app.", + "terminal.ambient_agent.tips.webhooks_incidents": "Trigger agents from webhooks to respond to production incidents.", + "terminal.ask_ai.default_autosuggestion": "What happened here?", + "terminal.auth_secret.credentials_encrypted": "Your credentials are encrypted end-to-end. ", + "terminal.auth_secret.delete_secret": "Delete secret", + "terminal.auth_secret.loading": "Loading…", + "terminal.auth_secret.new_secret_type": "New {name}", + "terminal.auth_secret.no_matches_helper": "No secrets found. Save to use this value directly or click the key to add a secret.", + "terminal.auth_secret.no_secrets_found": "No secrets found", + "terminal.auth_secret.search_placeholder": "Search secrets or create a new one", + "terminal.auth_secret.share_with_team": "Share with team", + "terminal.auth_secret.skip_advanced": "Skip (advanced)", + "terminal.auth_secret.skip_advanced_label": "Only if your key is already set in the environment (e.g. injected as a Kubernetes secret)", + "terminal.auth_secret.unable_to_load": "Unable to load secrets", + "terminal.auto_reload_modal.title": "Enable auto reload?", + "terminal.auto_reload_modal.toast.enable_failed": "Failed to enable auto-reload. Please try updating your settings in Billing & usage.", + "terminal.auto_reload_modal.toast.team_not_found": "Oops, something went wrong; your team's data could not be found.", + "terminal.auto_reload_modal.toast.updated": "Auto-reload settings updated", + "terminal.auto_reload.name": "auto-reload", + "terminal.auto_reload.purchase_suffix": " will automatically purchase your selected package when you run out. ", + "terminal.auto_reload.when_enabled_prefix": "When enabled, ", + "terminal.available_shells.custom": "Custom", + "terminal.available_shells.custom_with_command": "Custom ({command})", + "terminal.available_shells.custom_with_path": "Custom: {path}", + "terminal.available_shells.docker_sandbox": "Docker Sandbox", + "terminal.available_shells.system_default_shell": "System default shell", + "terminal.available_shells.windows_subsystem_for_linux": "Windows Subsystem for Linux", + "terminal.binding.accept_prompt_suggestion": "Accept Prompt Suggestion", + "terminal.binding.add_current_folder_as_project": "Add current folder as project", + "terminal.binding.alternate_terminal_paste": "Alternate terminal paste", + "terminal.binding.ask_warp_ai": "Ask Warp AI", + "terminal.binding.ask_warp_ai_about_last_block": "Ask Warp AI about last block", + "terminal.binding.ask_warp_ai_about_selection": "Ask Warp AI about Selection", + "terminal.binding.attach_selected_block_as_agent_context": "Attach Selected Block as Agent Context", + "terminal.binding.attach_selected_text_as_agent_context": "Attach Selected Text as Agent Context", + "terminal.binding.attach_selection_as_agent_context": "Attach Selection as Agent Context", + "terminal.binding.bookmark_selected_block": "Bookmark selected block", + "terminal.binding.cancel_process": "Cancel active process", + "terminal.binding.copy_command_and_output": "Copy command and output", + "terminal.binding.copy_text_or_cancel_process": "Copy text or cancel active process", + "terminal.binding.executing_command.backward_tabulation": "Backward tabulation within an executing command", + "terminal.binding.executing_command.delete_line_end": "Delete to line end within an executing command", + "terminal.binding.executing_command.delete_line_start": "Delete to line start within an executing command", + "terminal.binding.executing_command.delete_word_left": "Delete word left within an executing command", + "terminal.binding.executing_command.move_cursor_end": "Move cursor end within an executing command", + "terminal.binding.executing_command.move_cursor_home": "Move cursor home within an executing command", + "terminal.binding.executing_command.move_cursor_word_left": "Move cursor one word to the left within an executing command", + "terminal.binding.executing_command.move_cursor_word_right": "Move cursor one word to the right within an executing command", + "terminal.binding.expand_selected_blocks_above": "Expand selected blocks above", + "terminal.binding.expand_selected_blocks_below": "Expand selected blocks below", + "terminal.binding.find_in_terminal": "Find in Terminal", + "terminal.binding.find_within_selected_block": "Find within selected block", + "terminal.binding.focus_input": "Focus terminal input", + "terminal.binding.import_external_settings": "Import External Settings", + "terminal.binding.initiate_project_for_warp": "Initiate project for Warp", + "terminal.binding.insert_command_correction": "Insert Command Correction", + "terminal.binding.load_agent_mode_conversation": "Load agent mode conversation (from debug link in clipboard)", + "terminal.binding.open_block_context_menu": "Open block context menu", + "terminal.binding.reinput_commands": "Reinput selected commands", + "terminal.binding.reinput_commands_with_sudo": "Reinput selected commands as root", + "terminal.binding.scroll_down_one_line": "Scroll terminal output down one line", + "terminal.binding.scroll_down_one_page": "Scroll terminal output down one page", + "terminal.binding.scroll_to_bottom_of_selected_block": "Scroll to bottom of selected block", + "terminal.binding.scroll_to_top_of_selected_block": "Scroll to top of selected block", + "terminal.binding.scroll_up_one_line": "Scroll terminal output up one line", + "terminal.binding.scroll_up_one_page": "Scroll terminal output up one page", + "terminal.binding.select_all_blocks": "Select all blocks", + "terminal.binding.select_bookmark_down": "Select the closest bookmark down", + "terminal.binding.select_bookmark_up": "Select the closest bookmark up", + "terminal.binding.select_next_block": "Select next block", + "terminal.binding.select_previous_block": "Select previous block", + "terminal.binding.set_input_mode_agent": "Set Input Mode to Agent Mode", + "terminal.binding.set_input_mode_terminal": "Set Input Mode to Terminal Mode", + "terminal.binding.setup_guide": "Setup Guide", + "terminal.binding.share_current_session": "Share current session", + "terminal.binding.share_selected_block": "Share selected block", + "terminal.binding.stop_sharing_current_session": "Stop sharing current session", + "terminal.binding.toggle_autoexecute_mode": "Toggle Auto-execute Mode", + "terminal.binding.toggle_block_filter_selected_or_last": "Toggle block filter on selected or last block", + "terminal.binding.toggle_cli_agent_rich_input": "Toggle CLI Agent Rich Input", + "terminal.binding.toggle_conversation_details_panel": "Toggle Conversation Details Panel", + "terminal.binding.toggle_hide_cli_responses": "Toggle Hide CLI Responses", + "terminal.binding.toggle_queue_next_prompt": "Toggle Queue Next Prompt", + "terminal.binding.toggle_session_recording": "Toggle PTY Recording for Session", + "terminal.binding.toggle_sticky_command_header": "Toggle Sticky Command Header in Active Pane", + "terminal.binding.toggle_team_workflows_modal": "Toggle team workflows modal", + "terminal.binding.write_codebase_index_snapshot": "Write current codebase index snapshot", + "terminal.block_filter.accessibility_help": "Press Escape to quit", + "terminal.block_filter.accessibility_title": "Type searched phrase.", + "terminal.block_filter.case_sensitive_tooltip": "Case sensitive search", + "terminal.block_filter.context_lines_tooltip": "Show context lines around matches", + "terminal.block_filter.invert_tooltip": "Invert filter", + "terminal.block_filter.placeholder": "Filter block output", + "terminal.block_filter.regex_tooltip": "Regex toggle", + "terminal.block_list.conversation_restored": "Conversation restored", + "terminal.block_list.previous_session": "Previous session", + "terminal.block_list.restored_from": "{label} from {date}", + "terminal.block_list.save_as_workflow": "Save as Workflow", + "terminal.block_list.save_as_workflow_secrets": "Blocks containing secrets cannot be saved.", + "terminal.block_onboarding.agentic.prompt.git_history": "Explore my git history in {repo_path} and provide me a summary.", + "terminal.block_onboarding.agentic.prompt.matrix": "First check if {directory} exists, and create this path if it doesn't already exist. Then create a matrix theme for my Warp terminal without a background image field, following exact YAML structure on the warp website without any extra or missing fields. Call it matrix.yaml and save it in the directory we previously created. Once you've verified that the theme is correct and ready to be applied, let me know by only saying 'The matrix theme is now available at '.", + "terminal.block_onboarding.agentic.prompt.matrix.default_directory": "the Warp themes directory.", + "terminal.block_onboarding.agentic.prompt.other": "What can you help with me on?", + "terminal.block_onboarding.agentic.prompt.snake": "Make a snake game for playing in the terminal using python. Use the code tool and requested commands to do it for me. Before deciding on a solution, make sure I have all the prerequisites installed. At the end of our conversation, the app should run without any additional steps.", + "terminal.block_onboarding.agentic.suggestion.git_history.description": "Work with Agent Mode to understand recent changes to a git repository", + "terminal.block_onboarding.agentic.suggestion.git_history.fallback_repo": "my repository", + "terminal.block_onboarding.agentic.suggestion.git_history.title": "Explore git history in {repo}", + "terminal.block_onboarding.agentic.suggestion.matrix.description": "Make your terminal look like you entered the Matrix", + "terminal.block_onboarding.agentic.suggestion.matrix.title": "Create a Matrix-styled custom theme", + "terminal.block_onboarding.agentic.suggestion.other.description": "Pair with an Agent to accomplish another task", + "terminal.block_onboarding.agentic.suggestion.other.title": "Something else?", + "terminal.block_onboarding.agentic.suggestion.snake.description": "Have Agent Mode walk you through creating a snake game from end-to-end", + "terminal.block_onboarding.agentic.suggestion.snake.title": "Create a snake game in Python from scratch", + "terminal.block_onboarding.agentic.welcome_body_agent_mode": " Agent Mode", + "terminal.block_onboarding.agentic.welcome_body_prefix": "Here are a few examples of how to leverage the power of AI in your terminal using", + "terminal.block_onboarding.agentic.welcome_title": "Welcome to Warp!", + "terminal.block_onboarding.create_team": "Create team", + "terminal.block_onboarding.drive_sharing.body": "You can now share drive objects, in Warp or on the web, with anyone - Warp user or not. Click Share in the Warp Drive menu or the pane header to share via link or email.", + "terminal.block_onboarding.drive_sharing.permissions": "You’ll be able to modify the access permissions any time.", + "terminal.block_onboarding.drive_sharing.share_object": "Share {name}", + "terminal.block_onboarding.drive_sharing.share_this_object": "Share this {type}", + "terminal.block_onboarding.drive_sharing.title": "Sharing in Warp Drive", + "terminal.block_onboarding.prompt.compatibility_prefix": "Warp works with many custom prompts like oh-my-zsh, Starship, Powerlevel10K. ", + "terminal.block_onboarding.prompt.customizable": "Customizable in appearance settings.", + "terminal.block_onboarding.prompt.intro": "Next, let’s set up your prompt. Warp has a custom prompt builder or you can select PS1 to honor your pre-existing prompt configuration.", + "terminal.block_onboarding.prompt.let_us_know": "Let us know.", + "terminal.block_onboarding.prompt.look_incorrect": "Look incorrect? ", + "terminal.block_onboarding.prompt.no_existing_prompt": "No existing prompt.", + "terminal.block_onboarding.prompt.shell_prompt": "Shell prompt (PS1)", + "terminal.block_onboarding.prompt.warp_prompt": "Warp prompt", + "terminal.block.bookmark_tooltip": "Bookmark this block to quickly scroll to it", + "terminal.block.completed_at": "\nCompleted at: {time}", + "terminal.block.jump_to_bottom_tooltip": "Jump to the bottom of this block", + "terminal.block.lock_scroll_at_bottom_tooltip": "Lock scrolling at bottom of block", + "terminal.block.started_at": "Started at: {time}", + "terminal.block.tag_agent_for_assistance": "Tag agent for assistance", + "terminal.buy_credits.auto_reload_error": "Failed to enable auto-reload for your team. Please try again in Settings > Billing and Usage.", + "terminal.buy_credits.auto_reload_tooltip": "When enabled, auto reload will purchase {credits} credits when your credit balance gets low", + "terminal.buy_credits.buy": "Buy", + "terminal.buy_credits.increase_it": "Increase it", + "terminal.buy_credits.monthly_limit.admin_description": "Your monthly spend limit has been reached. Increase it to continue.", + "terminal.buy_credits.monthly_limit.non_admin_description": "Contact a team admin to increase monthly limit.", + "terminal.buy_credits.monthly_limit.title": "Monthly limit reached", + "terminal.buy_credits.out_of_credits.admin_description": "Add more credits to your account to continue using Oz agents.", + "terminal.buy_credits.out_of_credits.non_admin_description": "Contact a team admin to purchase more credits to continue.", + "terminal.buy_credits.out_of_credits.title": "Out of credits", + "terminal.buy_credits.purchase_exceeds_limit_prefix": "Purchasing these credits would take you over your monthly spend limit. ", + "terminal.buy_credits.purchase_exceeds_limit_suffix": " to continue.", + "terminal.cli_agent_sessions.waiting_for_answer": "Waiting for your answer", + "terminal.cloud_agent_loading.failed_to_start_environment": "Failed to start environment", + "terminal.cloud_agent_loading.github_auth_button": "Authenticate with GitHub", + "terminal.cloud_agent_loading.github_auth_message": "Please authenticate with GitHub to continue", + "terminal.cloud_agent_loading.github_auth_required": "GitHub Authentication Required", + "terminal.cloud_agent_loading.machine_prefix": "Your agent is currently running on a {specs} machine. ", + "terminal.cloud_agent_loading.machine_suffix": " for more powerful cloud agents.", + "terminal.cloud_agent_loading.no_environment_started": "No cloud environment was started", + "terminal.cloud_agent_loading.run_cancelled": "Cloud Agent Run Cancelled", + "terminal.cloud_agent_setup.desc_prefix": "Use Oz cloud agents to run parallel agents, build agents that run autonomously, and check in on your agents from anywhere. ", + "terminal.cloud_agent_setup.free_credit_one": "You have 1 free credit to use on Oz cloud agents.", + "terminal.cloud_agent_setup.free_credits": "Free credits", + "terminal.cloud_agent_setup.free_credits_other": "You have {credits} free credits to use on Oz cloud agents.", + "terminal.cloud_agent_setup.subheading": "Cloud agents require an environment that they'll run in to get their task done. Create your first environment below. You'll be able to edit the environment later, or add new environments when you need them.", + "terminal.cloud_agent_setup.title": "Start a new Oz cloud agent", + "terminal.cloud_agent_setup.visit_docs": "Visit docs", + "terminal.context_menu.ai_command_search": "AI command search", + "terminal.context_menu.clear_blocks": "Clear Blocks", + "terminal.context_menu.command_search": "Command search", + "terminal.context_menu.copy_command": "Copy command", + "terminal.context_menu.copy_commands": "Copy commands", + "terminal.context_menu.copy_conversation_id": "Copy conversation ID", + "terminal.context_menu.copy_conversation_text": "Copy conversation text", + "terminal.context_menu.copy_debugging_id": "Copy debugging ID", + "terminal.context_menu.copy_debugging_link": "Copy debugging link", + "terminal.context_menu.copy_filtered_output": "Copy filtered output", + "terminal.context_menu.copy_git_branch": "Copy git branch", + "terminal.context_menu.copy_output": "Copy output", + "terminal.context_menu.copy_output_as_markdown": "Copy output as Markdown", + "terminal.context_menu.copy_prompt": "Copy prompt", + "terminal.context_menu.copy_right_prompt": "Copy right prompt", + "terminal.context_menu.copy_share_link": "Copy share link", + "terminal.context_menu.copy_url": "Copy URL", + "terminal.context_menu.copy_working_directory": "Copy working directory", + "terminal.context_menu.edit_agent_toolbelt": "Edit agent toolbelt", + "terminal.context_menu.edit_cli_agent_toolbelt": "Edit CLI agent toolbelt", + "terminal.context_menu.edit_prompt": "Edit prompt", + "terminal.context_menu.find_within_block": "Find within block", + "terminal.context_menu.find_within_blocks": "Find within blocks", + "terminal.context_menu.fork": "Fork", + "terminal.context_menu.fork_from_here": "Fork from here", + "terminal.context_menu.fork_from_here_dev": "Fork from here (dev only)", + "terminal.context_menu.fork_from_last_query": "Fork from last query", + "terminal.context_menu.fork_from_query": "Fork from \"{query}\"", + "terminal.context_menu.hide_input_hint_text": "Hide input hint text", + "terminal.context_menu.insert_into_input": "Insert into input", + "terminal.context_menu.open_in_warp": "Open in Warp", + "terminal.context_menu.rewind_to_before_here": "Rewind to before here", + "terminal.context_menu.save_as_prompt": "Save as prompt", + "terminal.context_menu.save_as_workflow": "Save as workflow", + "terminal.context_menu.scroll_to_bottom_of_block": "Scroll to bottom of block", + "terminal.context_menu.scroll_to_bottom_of_blocks": "Scroll to bottom of blocks", + "terminal.context_menu.scroll_to_top_of_block": "Scroll to top of block", + "terminal.context_menu.scroll_to_top_of_blocks": "Scroll to top of blocks", + "terminal.context_menu.share_block_ellipsis": "Share block...", + "terminal.context_menu.share_conversation": "Share conversation", + "terminal.context_menu.share_ellipsis": "Share...", + "terminal.context_menu.show_input_hint_text": "Show input hint text", + "terminal.context_menu.toggle_block_filter": "Toggle block filter", + "terminal.context_menu.toggle_bookmark": "Toggle bookmark", + "terminal.conversation_details.hide": "Hide details", + "terminal.conversation_details.show": "Show details", + "terminal.harness_selector.disabled_by_admin": "Disabled by your administrator", + "terminal.init_environment.cancelled": "Environment setup cancelled", + "terminal.init_environment.create_using_current_dir": "Create environment using the current working dir as repo", + "terminal.init_environment.create_using_supplied_repos": "Create environment using the supplied repos: {repos}", + "terminal.init_environment.create_without_repos": "Create environment without any repos", + "terminal.init_environment.explanation": "Would you like to create an environment for this project so you can run cloud agents in it? The agent will guide you through choosing GitHub repos, configuring a Docker image, and specifying startup commands.", + "terminal.init_environment.mode.quick_setup.description": "Select the GitHub repositories you'd like to work with and we'll suggest a base image and config", + "terminal.init_environment.mode.quick_setup.title": "Quick setup", + "terminal.init_environment.mode.title": "Choose how you'd like to set up your environment", + "terminal.init_environment.mode.use_agent.description": "Choose a locally set up project and we'll help you set up an environment based on it", + "terminal.init_environment.mode.use_agent.title": "Use the agent", + "terminal.init_environment.no_repos_help": "If you want to create an environment with repos, rerun this command and pass in file paths or GitHub links as arguments, e.g. \"/create-environment \".", + "terminal.init_project.codebase.cancelled": "Codebase index cancelled", + "terminal.init_project.codebase.index_button": "Yes, index this codebase.", + "terminal.init_project.codebase.prompt": "Would you like the Agent to index this codebase? This will lead to more efficient and tailored help.", + "terminal.init_project.codebase.started": "Codebase index started", + "terminal.init_project.codebase.view_status": "View index status", + "terminal.init_project.environment.create_button": "Create an environment", + "terminal.init_project.environment.created": "Environment created", + "terminal.init_project.environment.creating": "Creating environment...", + "terminal.init_project.environment.prompt": "Would you like to create an environment for this project so you can run cloud agents in it? The agent will guide you through choosing GitHub repos, configuring a Docker image, and specifying startup commands.", + "terminal.init_project.environment.skipped": "Environment creation skipped", + "terminal.init_project.lsp.enable_language_support": "Enable language support", + "terminal.init_project.lsp.enable_prefix": "Enable ", + "terminal.init_project.lsp.enable_suffix": " support", + "terminal.init_project.lsp.enabled": "Language support enabled", + "terminal.init_project.lsp.install_and_enable": "Install and enable", + "terminal.init_project.lsp.install_and_enable_prefix": "Install and enable ", + "terminal.init_project.lsp.multiple_prompt": "Would you like to enable available language support for this codebase? This will give you smarter code navigation and inline error checking.", + "terminal.init_project.lsp.single_enabled_prefix": "", + "terminal.init_project.lsp.single_enabled_suffix": " language support enabled", + "terminal.init_project.lsp.single_prompt_prefix": "Enable ", + "terminal.init_project.lsp.single_prompt_suffix": " support for this codebase? This will give you smarter code navigation, inline error checking, and more.", + "terminal.init_project.lsp.skipped": "Language support skipped", + "terminal.init_project.lsp.started_install": "Started installation for language support", + "terminal.init_project.project_rules.already_configured": "Project rules already configured", + "terminal.init_project.project_rules.configured": "Project rules configured", + "terminal.init_project.project_rules.generate_button": "Generate AGENTS.md file", + "terminal.init_project.project_rules.generating": "Generating AGENTS.md...", + "terminal.init_project.project_rules.link_existing_prefix": "Link existing ", + "terminal.init_project.project_rules.link_existing_suffix": " to my AGENTS.md file", + "terminal.init_project.project_rules.linked_from_prefix": "Project rules linked from ", + "terminal.init_project.project_rules.prompt": "Would you like to create an AGENTS.md file? Warp can create one for you with project specific rules, context, and conventions inferred from your codebase. The agent will use this context as it codes.", + "terminal.init_project.project_rules.regenerate_button": "Re-generate AGENTS.md file", + "terminal.init_project.project_rules.skip_generation_button": "Skip AGENTS.md generation for now", + "terminal.init_project.project_rules.skipped": "Project rules skipped", + "terminal.init_project.skip_for_now": "Skip for now", + "terminal.init_project.toast.lsp_install_failed_prefix": "Failed to install ", + "terminal.init_project.toast.lsp_install_failed_suffix": ": ", + "terminal.init_project.toast.lsp_install_success_suffix": " installed and enabled successfully.", + "terminal.init_project.toast.lsp_installing_prefix": "Installing ", + "terminal.init_project.toast.lsp_installing_suffix": " in background...", + "terminal.init_project.welcome.already_setup": "It looks like this project has already been initialized. You can re-generate the AGENTS.md for this codebase by clicking the button below.", + "terminal.init_project.welcome.onboarding": "Great - let's begin setting up this project! Would you like to give me permission to index this codebase? It allows me to quickly understand context and provide more targeted solutions when working in this codebase. No code is stored on Warp servers.", + "terminal.inline_banner.agent_mode_setup.body": "Unlock smarter, more consistent responses by letting the Agent understand your codebase and generate rules for it. You can also do this at any point by running /init", + "terminal.inline_banner.agent_mode_setup.optimize": "Optimize", + "terminal.inline_banner.agent_mode_setup.title": "Optimize Warp for this codebase?", + "terminal.inline_banner.alias_expansion.enable": "Enable alias expansion", + "terminal.inline_banner.alias_expansion.title": "Warp can auto-expand aliases.", + "terminal.inline_banner.anonymous_user_ai_sign_up.content": "AI features are unavailable for logged-out users. Create an account to use AI.", + "terminal.inline_banner.anonymous_user_ai_sign_up.title": "Login for AI", + "terminal.inline_banner.aws_bedrock_enabled": "Your Warp admin has enabled AWS Bedrock for your team.", + "terminal.inline_banner.aws_bedrock.log_in": "Log into AWS", + "terminal.inline_banner.aws_bedrock.title": "Use AWS Bedrock?", + "terminal.inline_banner.aws_cli_not_installed.title": "AWS CLI Not Installed", + "terminal.inline_banner.aws_cli_required": "The AWS CLI is required to authenticate with your organization's AWS Bedrock. Install it to continue.", + "terminal.inline_banner.init_script_output_visible": "The output from Warp's initialization script is visible above to assist with debugging.", + "terminal.inline_banner.notifications.a11y_enable_through_command_palette": "You can enable notifications through the command palette.", + "terminal.inline_banner.notifications.agent_task_completed": "Warp can notify you when an agent finishes responding.", + "terminal.inline_banner.notifications.allow_permissions": "Don't forget to 'Allow' the permissions request to finish setting up notifications.", + "terminal.inline_banner.notifications.configure": "Configure notifications", + "terminal.inline_banner.notifications.disabled": "Notifications were turned off, but you can always go to Settings to enable notifications.", + "terminal.inline_banner.notifications.dismissed": "We won't show this banner again, but you can always go to Settings to enable notifications.", + "terminal.inline_banner.notifications.long_running_command": "Warp can notify you when long-running commands finish.", + "terminal.inline_banner.notifications.needs_attention": "Warp can notify you when a command or agent needs your attention.", + "terminal.inline_banner.notifications.password_prompt": "Warp can notify you when you're prompted to enter a password.", + "terminal.inline_banner.notifications.permissions_denied": "Warp was denied permissions to send you notifications.", + "terminal.inline_banner.notifications.permissions_error": "Something went wrong while requesting permissions.", + "terminal.inline_banner.notifications.set_permissions": "Set permissions", + "terminal.inline_banner.notifications.success": "Success! You are now ready to receive desktop notifications.", + "terminal.inline_banner.prompt_suggestions.out_of_credits_tooltip": "Out of credits", + "terminal.inline_banner.prompt_suggestions.payment_issue_tooltip": "Restricted due to payment issue", + "terminal.inline_banner.prompt_suggestions.query.code": "Help me write some code. What information do I need to provide to you to do this?", + "terminal.inline_banner.prompt_suggestions.query.deploy": "Help me deploy my project. What information do I need to provide to you to do this?", + "terminal.inline_banner.prompt_suggestions.query.explain": "Explain this to me.", + "terminal.inline_banner.prompt_suggestions.query.fix": "Help me fix this.", + "terminal.inline_banner.prompt_suggestions.query.install": "Help me install a binary/dependency. What information do I need to provide to you to do this?", + "terminal.inline_banner.prompt_suggestions.query.something_else": "Something else?", + "terminal.inline_banner.shared_session.environment_ended": "Environment ended", + "terminal.inline_banner.shared_session.environment_started": "Environment started", + "terminal.inline_banner.shared_session.remote_control_active": "Remote control active", + "terminal.inline_banner.shared_session.remote_control_stopped": "Remote control stopped", + "terminal.inline_banner.shared_session.sharing_ended": "Sharing ended", + "terminal.inline_banner.shared_session.sharing_started": "Sharing started", + "terminal.inline_banner.shared_session.today": "Today", + "terminal.inline_banner.shell_process_exited": "Shell process exited", + "terminal.inline_banner.shell_process_exited_prematurely": "Shell process exited prematurely!", + "terminal.inline_banner.ssh_wrapper.disabled": "Warp SSH wrapper disabled", + "terminal.inline_banner.ssh_wrapper.enabled": "Warp SSH wrapper enabled", + "terminal.inline_banner.vim_mode.title": "Enable Warp's Vim keybindings?", + "terminal.inline_history.header": "History", + "terminal.inline_history.tab.all": "All", + "terminal.inline_history.tab.commands": "Commands", + "terminal.inline_history.tab.prompts": "Prompts", + "terminal.input_mode.agent_mode": "Agent Mode", + "terminal.input_mode.shortcut_or": "{keybinding} or {prefix}", + "terminal.input_mode.terminal": "Terminal", + "terminal.input.a11y_helper": "Input your shell command, press enter to execute. Press cmd-up to navigate to output of previously executed commands. Press cmd-l to re-focus command input.", + "terminal.input.a11y_label": "Command Input.", + "terminal.input.a11y.ai_prompt": "AI prompt: {query}", + "terminal.input.a11y.command": "Command: {command}", + "terminal.input.a11y.conversation": "Conversation: {title}", + "terminal.input.a11y.disabled_suffix": " (disabled)", + "terminal.input.a11y.indexed_repository": "Indexed repository: {repository}", + "terminal.input.a11y.model": "Model: {model}", + "terminal.input.a11y.plan": "Plan: {title}", + "terminal.input.a11y.profile": "Profile: {profile}", + "terminal.input.a11y.prompt": "Prompt: {prompt}", + "terminal.input.a11y.query": "Query: {query}", + "terminal.input.a11y.selected_suffix": " (selected)", + "terminal.input.a11y.skill": "Skill: {skill}", + "terminal.input.agent_hint.build_data_pipeline": "Warp anything e.g. Build a data pipeline to process CSV files and load them into BigQuery", + "terminal.input.agent_hint.build_rest_api": "Warp anything e.g. Build a REST API for my mobile app using FastAPI", + "terminal.input.agent_hint.create_backup_script": "Warp anything e.g. Create a backup script for my PostgreSQL database and schedule it", + "terminal.input.agent_hint.create_unit_tests": "Warp anything e.g. Create unit tests for my authentication service", + "terminal.input.agent_hint.debug_python_ci": "Warp anything e.g. Help me debug why my Python tests are failing in CI", + "terminal.input.agent_hint.deploy_react": "Warp anything e.g. Deploy my React app to Vercel and set up environment variables", + "terminal.input.agent_hint.fix_memory_leak": "Warp anything e.g. Find and fix the memory leak in my Node.js application", + "terminal.input.agent_hint.github_actions_deploy": "Warp anything e.g. Create a GitHub Actions workflow to automatically deploy on merge", + "terminal.input.agent_hint.implement_oauth": "Warp anything e.g. Help me implement OAuth2 authentication in my Express.js app", + "terminal.input.agent_hint.migrate_database": "Warp anything e.g. Help me migrate my data from MySQL to PostgreSQL", + "terminal.input.agent_hint.optimize_docker": "Warp anything e.g. Optimize my Docker images to reduce build times and size", + "terminal.input.agent_hint.optimize_sql": "Warp anything e.g. Help me optimize my SQL queries that are running slowly", + "terminal.input.agent_hint.refactor_legacy": "Warp anything e.g. Help me refactor this legacy code to use modern design patterns", + "terminal.input.agent_hint.setup_ab_testing": "Warp anything e.g. Set up A/B testing infrastructure for my web application", + "terminal.input.agent_hint.setup_log_aggregation": "Warp anything e.g. Set up log aggregation with ELK stack for my distributed system", + "terminal.input.agent_hint.setup_microservice": "Warp anything e.g. Set up a new microservice with Docker and create the deployment pipeline", + "terminal.input.agent_hint.setup_monitoring": "Warp anything e.g. Set up monitoring and alerts for my AWS infrastructure", + "terminal.input.agent_hint.setup_redis": "Warp anything e.g. Set up Redis caching for my web application", + "terminal.input.agent_hint.setup_ssl": "Warp anything e.g. Set up SSL certificates and configure HTTPS for my domain", + "terminal.input.agent_hint.troubleshoot_kubernetes": "Warp anything e.g. Help me troubleshoot why my Kubernetes pods keep crashing", + "terminal.input.attached_images_removed_model_no_images": "Attached images were removed — the selected model does not support images.", + "terminal.input.cannot_run_command_already_running": "Cannot run `{command}` (command already running).", + "terminal.input.cannot_start_while_monitoring": "Cannot start a new conversation while agent is monitoring a command.", + "terminal.input.cloud_handoff_prepare_failed": "Failed to prepare cloud handoff: {error}", + "terminal.input.conversations.tab.all": "All", + "terminal.input.conversations.tab.current_directory": "Current Directory", + "terminal.input.dynamic_enum.failure": "Command failed", + "terminal.input.dynamic_enum.generate": "Run the following command to generate variants:", + "terminal.input.dynamic_enum.no_results": "Command returned no results", + "terminal.input.dynamic_enum.pending": "Command pending...", + "terminal.input.dynamic_enum.run": "Run command", + "terminal.input.executed_command": "Executed: {command}", + "terminal.input.export.directory_not_found": "Directory not found: {path}", + "terminal.input.export.failed": "Failed to export to {path}: {error}", + "terminal.input.export.file_already_exists": "File {path} already exists", + "terminal.input.export.no_active_conversation": "No active conversation to export", + "terminal.input.export.permission_denied": "Permission denied writing to {path}. Check file permissions.", + "terminal.input.export.success": "Conversation exported to {path}", + "terminal.input.export.will_overwrite": "File {path} already exists and will be overwritten", + "terminal.input.hint.ai_command_search": "Type '#' for AI command suggestions", + "terminal.input.hint.ask_follow_up": "Ask a follow up", + "terminal.input.hint.ask_follow_up_classic": "Ask a follow up, or backspace to exit", + "terminal.input.hint.cli_agent_rich_input": "Tell the agent what to build...", + "terminal.input.hint.cloud_agent": "Kick off a cloud agent", + "terminal.input.hint.enter_prompt_for_agent": "Enter prompt for {agent}...", + "terminal.input.hint.hand_off_to": "Hand off to {name}", + "terminal.input.hint.handoff_to_cloud": "Handoff to cloud", + "terminal.input.hint.run_commands": "Run commands", + "terminal.input.hint.steer_running_agent": "Steer the running agent", + "terminal.input.hint.steer_running_agent_classic": "Steer the running agent, or backspace to exit", + "terminal.input.image_limit.per_conversation": "per conversation", + "terminal.input.image_limit.per_query": "per query", + "terminal.input.images_not_attached.one": "1 image wasn't attached - limit is {limit_value} images {limit_name}.", + "terminal.input.images_not_attached.other": "{count} images weren't attached - limit is {limit_value} images {limit_name}.", + "terminal.input.images_removed.one": "1 image was removed - limit is {limit} per conversation.", + "terminal.input.images_removed.other": "{count} images were removed - limit is {limit} per conversation.", + "terminal.input.inline_menu.history": "History", + "terminal.input.message_bar.attached_context.one_other_command": "`{command}` and 1 other command attached as context", + "terminal.input.message_bar.attached_context.other_commands": "`{command}` and {count} other commands attached as context", + "terminal.input.message_bar.attached_context.selected_text": "selected text attached as context", + "terminal.input.message_bar.attached_context.single": "`{command}` attached as context", + "terminal.input.models.base_agent_warning": "You're using the base agent. Full terminal use models only apply to the full terminal use agent.", + "terminal.input.models.discount_off": "{percent}% off!", + "terminal.input.models.full_terminal_use_warning": "You're using the full terminal use agent. Base models only apply to the base agent.", + "terminal.input.no_agent_harnesses_available": "No agent harnesses are available. Contact your team admin.", + "terminal.input.no_results": "No results", + "terminal.input.placeholder.search_commands": "Search commands", + "terminal.input.placeholder.search_conversations": "Search conversations", + "terminal.input.placeholder.search_indexed_repos": "Search indexed repos", + "terminal.input.placeholder.search_models": "Search models", + "terminal.input.placeholder.search_plans": "Search plans", + "terminal.input.placeholder.search_profiles": "Search profiles", + "terminal.input.placeholder.search_prompts": "Search prompts", + "terminal.input.placeholder.search_queries": "Search queries", + "terminal.input.placeholder.search_queries_to_rewind": "Search queries to rewind to", + "terminal.input.placeholder.search_skills": "Search skills", + "terminal.input.preparing_handoff": "Preparing handoff — try again in a moment.", + "terminal.input.read_only_viewer_cannot_send": "Cannot send queries as a read-only viewer.", + "terminal.input.rewind.action": "rewind", + "terminal.input.rewind.current_state": "Current state (no rewind)", + "terminal.input.rewind.no_code_changes": "Rewind to: {query} (no code changes)", + "terminal.input.rewind.with_changes": "Rewind to: {query} (+{added} -{removed})", + "terminal.input.slash_commands.section.commands": "Commands", + "terminal.input.slash_commands.section.prompts": "Prompts", + "terminal.input.slash_commands.section.skills": "Skills", + "terminal.input.slash_commands.show_more": "Show {count} more", + "terminal.input.too_many_attachments": "Too many attachments for this conversation.", + "terminal.input.untitled_conversation": "Untitled conversation", + "terminal.input.voice.listening": "Listening...", + "terminal.input.voice.transcribing": "Transcribing...", + "terminal.input.workflow.command_inserted": "Workflow command {command} inserted.", + "terminal.input.workflow.select_next_argument_helper": "Press shift-tab to select the next workflow argument", + "terminal.input.workflow.selected_argument": "Selected Workflow argument {argument}", + "terminal.link_detection.open_file": "Open file", + "terminal.link_detection.open_folder": "Open folder", + "terminal.link_detection.open_link": "Open link", + "terminal.loading_prompt": "Loading prompt...", + "terminal.loading_session": "Loading session...", + "terminal.loading.starting_shell_named": "Starting {shell}...", + "terminal.message_bar.again_to_send_to_agent": "again to send to agent", + "terminal.message_bar.agent_for_new_conversation": "/agent for new conversation", + "terminal.message_bar.autodetected": " (autodetected) ", + "terminal.message_bar.block_and_many_attached": " with `{command}` and {count} other commands attached", + "terminal.message_bar.block_and_one_attached": " with `{command}` and 1 other command attached", + "terminal.message_bar.block_attached": " with `{command}` attached", + "terminal.message_bar.current_pane": " current pane", + "terminal.message_bar.for_code_review": "for code review", + "terminal.message_bar.new_agent_conversation": " new /agent conversation", + "terminal.message_bar.new_conversation": " new conversation", + "terminal.message_bar.new_pane": " new pane", + "terminal.message_bar.no_skills_found": "No skills found", + "terminal.message_bar.open_conversation": "open conversation", + "terminal.message_bar.open_plan": " open plan", + "terminal.message_bar.plan_with_agent": " plan with agent", + "terminal.message_bar.select_and_save_to_profile": " select and save to profile", + "terminal.message_bar.text_selection_attached": " with text selection attached", + "terminal.message_bar.to_continue_conversation": " to continue conversation", + "terminal.message_bar.to_cycle_tabs": " to cycle tabs", + "terminal.message_bar.to_dismiss": " to dismiss", + "terminal.message_bar.to_execute": " to execute", + "terminal.message_bar.to_fork_and_continue": "to fork and continue", + "terminal.message_bar.to_hide_help": "to hide help", + "terminal.message_bar.to_hide_plan": "to hide plan", + "terminal.message_bar.to_navigate": " to navigate", + "terminal.message_bar.to_open": " to open '{title}'", + "terminal.message_bar.to_override": " to override", + "terminal.message_bar.to_remove": " to remove", + "terminal.message_bar.to_resume_conversation": "to resume conversation", + "terminal.message_bar.to_select": " to select", + "terminal.message_bar.to_send": " to send", + "terminal.message_bar.to_view_plan": "to view plan", + "terminal.message_bar.to_view_plans": "to view plans", + "terminal.model_selector.manage_defaults": "Manage defaults", + "terminal.model_selector.tab.base": "Base", + "terminal.model_selector.tab.full_terminal_use": "Full Terminal Use", + "terminal.model_specs.auto_bedrock_tooltip": "Warp uses Bedrock when the model Auto selects supports it; otherwise it may use Warp-hosted inference.", + "terminal.model_specs.cost": "Cost", + "terminal.model_specs.description": "Warp's benchmarks for how well a model performs in our harness, the rate at which it consumes credits, and task speed.", + "terminal.model_specs.inference_may_use_bedrock": "Inference may use Bedrock", + "terminal.model_specs.inference_via_api_key": "Inference via API key", + "terminal.model_specs.inference_via_bedrock": "Inference via Bedrock", + "terminal.model_specs.intelligence": "Intelligence", + "terminal.model_specs.reasoning_level_description": "Increased reasoning levels consume more credits and have higher latency, but higher performance for complicated tasks.", + "terminal.model_specs.reasoning_level_title": "Reasoning level", + "terminal.model_specs.speed": "Speed", + "terminal.model_specs.title": "Model Specs", + "terminal.notification.agent_failed_suffix": " failed", + "terminal.notification.agent_finished_suffix": " finished", + "terminal.notification.command_waiting_for_password": "Command is waiting for a password", + "terminal.notification.error_prefix": "Error: ", + "terminal.notification.error_sending": "Error sending notification", + "terminal.notification.latest_output_prefix": "Latest output: ", + "terminal.notification.long_running_failed_suffix": " failed after {duration}s", + "terminal.notification.long_running_finished_suffix": " finished after {duration}s", + "terminal.notification.needs_attention_suffix": " blocked", + "terminal.notification.password_prompt_suffix": " is waiting for a password", + "terminal.notification.permissions_help": "Make sure you have enabled access for Warp notifications in System Preferences.", + "terminal.notification.title": "Notification", + "terminal.notification.unknown_error_occurred": "An unknown error occurred", + "terminal.onboarding.thinking": "Thinking...", + "terminal.open_in_warp.a11y.close_banner": "Close View in Warp banner", + "terminal.open_in_warp.a11y.learn_more_help": "Learn more about opening Markdown files in Warp", + "terminal.open_in_warp.a11y.open": "Open {path} in Warp", + "terminal.open_in_warp.code_language_title": "Did you know that Warp can directly edit {language} files?", + "terminal.open_in_warp.code_title": "Did you know that Warp can directly edit code?", + "terminal.open_in_warp.edit_in_warp": "Edit in Warp", + "terminal.open_in_warp.markdown_title": "Did you know that Warp can directly display Markdown files?", + "terminal.open_in_warp.view_in_warp": "View in Warp", + "terminal.plugin_instructions.claude.error.platform_install_no_effect": "Platform plugin installation did not take effect", + "terminal.plugin_instructions.claude.error.platform_update_no_effect": "Platform plugin update did not take effect", + "terminal.plugin_instructions.claude.error.update_no_effect": "Plugin update did not take effect", + "terminal.plugin_instructions.claude.install.note.known_issues": "There are some known issues with Claude Code's plugin system. If the plugin is not found after step 1, you can try manually adding an \"extraKnownMarketplaces\" entry to ~/.claude/settings.json.", + "terminal.plugin_instructions.claude.install.note.restart": "Restart Claude Code to activate the plugin.", + "terminal.plugin_instructions.claude.install.step.add_marketplace": "Add the Warp plugin marketplace repository", + "terminal.plugin_instructions.claude.install.step.install_plugin": "Install the Warp plugin", + "terminal.plugin_instructions.claude.install.subtitle": "Ensure that jq is installed on your machine. Then, run these commands.", + "terminal.plugin_instructions.claude.install.title": "Install Warp Plugin for Claude Code", + "terminal.plugin_instructions.claude.success.installed_reload": "Warp plugin installed. Please run /reload-plugins to activate.", + "terminal.plugin_instructions.claude.success.updated_reload": "Warp plugin updated. Please run /reload-plugins to activate.", + "terminal.plugin_instructions.claude.update.note.restart": "Restart Claude Code to activate the update.", + "terminal.plugin_instructions.claude.update.step.install_latest": "Install the latest plugin version", + "terminal.plugin_instructions.claude.update.step.readd_marketplace": "Re-add the marketplace", + "terminal.plugin_instructions.claude.update.step.remove_marketplace": "Remove the existing marketplace (if present)", + "terminal.plugin_instructions.claude.update.title": "Update Warp Plugin for Claude Code", + "terminal.plugin_instructions.codex.install.note.restart": "Restart Codex to apply the changes.", + "terminal.plugin_instructions.codex.install.step.config": "Set the notification condition to \"always\" in your Codex config. Open or create ~/.codex/config.toml and add:", + "terminal.plugin_instructions.codex.install.step.update": "Update Codex to the latest version.", + "terminal.plugin_instructions.codex.install.subtitle": "Update Codex to the latest version, then enable in-focus notifications so Warp can display them while you work.", + "terminal.plugin_instructions.codex.install.title": "Enable Warp Notifications for Codex", + "terminal.plugin_instructions.enable_agent_notifications": "Enable {agent} notifications", + "terminal.plugin_instructions.enable_notifications": "Enable notifications", + "terminal.plugin_instructions.enable_tooltip": "Install the Warp plugin to enable rich agent notifications within Warp", + "terminal.plugin_instructions.error.auto_install_not_supported": "Auto-install not supported for this agent", + "terminal.plugin_instructions.error.auto_update_not_supported": "Auto-update not supported for this agent", + "terminal.plugin_instructions.error.command_failed": "'{command}' failed", + "terminal.plugin_instructions.error.command_run_failed": "Failed to run '{command}'", + "terminal.plugin_instructions.error.home_directory_not_found": "Could not determine home directory", + "terminal.plugin_instructions.error.no_plugin_manager": "No plugin manager available", + "terminal.plugin_instructions.gemini.error.update_no_effect": "Plugin update did not take effect", + "terminal.plugin_instructions.gemini.install.note.restart": "Restart Gemini CLI to activate the plugin.", + "terminal.plugin_instructions.gemini.install.step.install_extension": "Install the Warp extension", + "terminal.plugin_instructions.gemini.install.subtitle": "Run the following command, then restart Gemini CLI.", + "terminal.plugin_instructions.gemini.install.title": "Install Warp Plugin for Gemini CLI", + "terminal.plugin_instructions.gemini.success.installed_restart": "Warp plugin installed. Please restart Gemini CLI to activate.", + "terminal.plugin_instructions.gemini.success.updated_restart": "Warp plugin updated. Please restart Gemini CLI to activate.", + "terminal.plugin_instructions.gemini.update.note.restart": "Restart Gemini CLI to activate the update.", + "terminal.plugin_instructions.gemini.update.step.update_extension": "Update the Warp extension", + "terminal.plugin_instructions.gemini.update.title": "Update Warp Plugin for Gemini CLI", + "terminal.plugin_instructions.install_failed": "Failed to install Warp plugin", + "terminal.plugin_instructions.install_instructions_button": "Notifications setup instructions", + "terminal.plugin_instructions.install_instructions_tooltip": "View instructions to install the Warp plugin", + "terminal.plugin_instructions.installing": "Installing Warp plugin...", + "terminal.plugin_instructions.learn_more": "Learn more", + "terminal.plugin_instructions.opencode.install.note.restart": "Restart OpenCode to activate the plugin.", + "terminal.plugin_instructions.opencode.install.step.add_plugin": "Add \"@warp-dot-dev/opencode-warp\" to the \"plugin\" array in the top-level JSON object:", + "terminal.plugin_instructions.opencode.install.subtitle": "Add the Warp plugin to your OpenCode configuration, then restart OpenCode.", + "terminal.plugin_instructions.opencode.install.title": "Install Warp Plugin for OpenCode", + "terminal.plugin_instructions.opencode.step.open_config": "Open or create your opencode.json. This can be in your project root, or the global config path:", + "terminal.plugin_instructions.opencode.update.note.restart": "Restart OpenCode to load the updated plugin.", + "terminal.plugin_instructions.opencode.update.step.replace_plugin": "Replace the existing \"@warp-dot-dev/opencode-warp\" entry in the \"plugin\" array with the explicit version:", + "terminal.plugin_instructions.opencode.update.subtitle": "Pin the plugin to the latest version in your opencode.json. OpenCode caches plugins per version spec, so changing the pin forces it to re-fetch on restart.", + "terminal.plugin_instructions.opencode.update.title": "Update Warp Plugin for OpenCode", + "terminal.plugin_instructions.remote_suffix": "Be sure to run these commands on your remote machine.", + "terminal.plugin_instructions.run_commands": "Run the following commands.", + "terminal.plugin_instructions.see_logs": "See logs for details", + "terminal.plugin_instructions.success.installed_restart_session": "Warp plugin installed. Please restart the session to activate.", + "terminal.plugin_instructions.success.updated_restart_session": "Warp plugin updated. Please restart the session to activate.", + "terminal.plugin_instructions.update_button": "Update Warp plugin", + "terminal.plugin_instructions.update_failed": "Failed to update Warp plugin", + "terminal.plugin_instructions.update_instructions_button": "Plugin update instructions", + "terminal.plugin_instructions.update_instructions_tooltip": "View instructions to update the Warp plugin", + "terminal.plugin_instructions.update_tooltip": "A new version of the Warp plugin is available", + "terminal.plugin_instructions.updating": "Updating Warp plugin...", + "terminal.profile_model_selector.auto_mode_description": "Auto will select the best model for the task. Cost-efficiency optimizes for cost, Responsiveness optimizes for response speed.", + "terminal.profile_model_selector.auto_mode_title": "Auto mode", + "terminal.profile_model_selector.auto_select_best_model": "auto-select the best model for the task", + "terminal.profile_model_selector.manage_api_keys": "Manage API keys", + "terminal.profile_model_selector.model_locked_followup_tooltip": "Follow-ups use the original run's model", + "terminal.profile_model_selector.model_requires_edit_access_tooltip": "Request edit access to change model", + "terminal.profile_model_selector.model_tooltip": "Choose an agent model", + "terminal.profile_model_selector.new_models_available": "New models available", + "terminal.profile_model_selector.profile_tooltip": "Choose an AI execution profile", + "terminal.profile_model_selector.reasoning_level_description": "Increased reasoning levels consume more credits and have higher latency, but higher performance for complicated tasks.", + "terminal.profile_model_selector.reasoning_level_title": "Reasoning level", + "terminal.profile_selector.billed_to_api": "Billed to API", + "terminal.profile_selector.custom_models": "Custom models", + "terminal.profile_selector.manage_profiles": "Manage profiles", + "terminal.profile_selector.profiles": "Profiles", + "terminal.project_setup.title": "Project setup", + "terminal.prompt_suggestion.execute_plan": "Execute this plan", + "terminal.queued_prompts.delete": "Delete queued prompt", + "terminal.queued_prompts.edit": "Edit queued prompt", + "terminal.queued_prompts.header": "{count} queued", + "terminal.recorder.started": "PTY recording started: {path}", + "terminal.recorder.stopped": "PTY recording stopped: {path}", + "terminal.remote_server.loading.checking": "Checking...", + "terminal.remote_server.loading.initializing": "Initializing...", + "terminal.remote_server.loading.installing": "Installing...", + "terminal.remote_server.loading.installing_progress": "Installing... ({percent}%)", + "terminal.remote_server.loading.installing_ssh_extension": "Installing Warp SSH Extension...", + "terminal.remote_server.loading.installing_ssh_extension_progress": "Installing Warp SSH Extension... ({percent}%)", + "terminal.remote_server.loading.starting_shell": "Starting shell...", + "terminal.remote_server.loading.updating": "Updating...", + "terminal.remote_server.loading.updating_ssh_extension": "Updating Warp SSH Extension...", + "terminal.rewind.no_code_to_restore": "No code to be restored", + "terminal.rich_history.exit_code": "Exit code {code}", + "terminal.rich_history.finished_in": "Finished in {duration}", + "terminal.rich_history.last_ran": "Last ran {time}", + "terminal.rich_history.ran": "Ran {time}", + "terminal.session_settings.working_directory.custom_directory": "Custom directory", + "terminal.session_settings.working_directory.home_directory": "Home directory", + "terminal.session_settings.working_directory.previous_session_directory": "Previous session's directory", + "terminal.share_block.create_link": "Create link", + "terminal.share_block.creating_block": "Creating block...", + "terminal.share_block.creation_failed": "Something went wrong. Please try again.", + "terminal.share_block.default_embed_title": "embedded warp block", + "terminal.share_block.display.command": "Command", + "terminal.share_block.display.command_and_output": "Command and Output", + "terminal.share_block.display.output": "Output", + "terminal.share_block.embed_copied": "Embed code copied.", + "terminal.share_block.embed_snippet_error": "Error generating embed snippet", + "terminal.share_block.get_embed": "Get embed", + "terminal.share_block.link_copied": "Link copied.", + "terminal.share_block.manage_shared_blocks": "Manage shared blocks", + "terminal.share_block.redact_secrets": "Redact secrets (API keys, passwords, IP addresses, PII etc.)", + "terminal.share_block.show_prompt": "Show prompt", + "terminal.share_block.title": "Share block", + "terminal.share_block.title_placeholder": "Title (optional)", + "terminal.shared_session.agent_task": "Agent task", + "terminal.shared_session.approve": "Approve", + "terminal.shared_session.are_you_still_there": "Are you still there?", + "terminal.shared_session.change_role": "Change role", + "terminal.shared_session.cloud_agent_failed": "Cloud agent failed to start", + "terminal.shared_session.continue_cloud_tooltip": "Continue this cloud conversation", + "terminal.shared_session.continue_sharing": "Continue sharing", + "terminal.shared_session.copy_session_sharing_link": "Copy session sharing link", + "terminal.shared_session.deny": "Deny", + "terminal.shared_session.edit_permission_warning_line1": "This grants the ability to execute commands on your", + "terminal.shared_session.edit_permission_warning_line2": "behalf. Use with caution.", + "terminal.shared_session.edit_requests": "Edit Requests", + "terminal.shared_session.ended_due_to_inactivity": "Sharing ended due to inactivity", + "terminal.shared_session.ended_due_to_sharer_inactivity": "Sharing ended due to sharer inactivity", + "terminal.shared_session.error.access_removed_reshare": "Your access to the session was removed. Please ask sharer to reshare to continue.", + "terminal.shared_session.error.command_in_progress": "A command is already in progress.", + "terminal.shared_session.error.connect_failed": "Failed to connect. Please try again later.", + "terminal.shared_session.error.execute_command_failed": "Failed to execute command.", + "terminal.shared_session.error.guests_already_added": "One or more emails have already been added to the session.", + "terminal.shared_session.error.guests_not_warp_users": "One or more emails were not associated with Warp accounts.", + "terminal.shared_session.error.insufficient_permissions_request_edit": "Insufficient permissions. Request edit access to continue.", + "terminal.shared_session.error.internal": "An internal error occurred. Please try sharing again.", + "terminal.shared_session.error.internal_ended": "Session ended due to an internal error. Please try sharing again.", + "terminal.shared_session.error.invalid_conversation": "Invalid conversation.", + "terminal.shared_session.error.invalid_link": "Invalid session sharing link.", + "terminal.shared_session.error.join_failed": "Failed to join shared session.", + "terminal.shared_session.error.link_not_accessible": "You don't have access to this link.", + "terminal.shared_session.error.login_required": "You must be logged in to share sessions.", + "terminal.shared_session.error.make_edit_failed": "Failed to make edit.", + "terminal.shared_session.error.max_participants_reached": "The maximum number of participants for this shared session has been reached.", + "terminal.shared_session.error.no_quota_remaining": "Session sharing usage exceeded for the day. Please try again later.", + "terminal.shared_session.error.perform_action_failed": "Failed to perform action.", + "terminal.shared_session.error.reconnect_failed": "Failed to reconnect.", + "terminal.shared_session.error.reshare_to_continue": "Something went wrong. Please ask sharer to reshare to continue.", + "terminal.shared_session.error.scrollback_too_large": "Scrollback exceeds limit. Try sharing again without scrollback.", + "terminal.shared_session.error.session_not_found": "Shared session not found.", + "terminal.shared_session.error.size_limit_exceeded": "Session limit ({max_bytes}) exceeded. Please reshare to continue.", + "terminal.shared_session.error.update_permissions_failed": "Failed to update permissions.", + "terminal.shared_session.limit_reached_header": "Shared session limit reached", + "terminal.shared_session.limit_reached_subheader": "Warp's free and pro plans come with a limited number of shared sessions.\n\nFor increased access to session sharing upgrade to the Build plan.", + "terminal.shared_session.make_editor": "Make editor", + "terminal.shared_session.make_viewer": "Make viewer", + "terminal.shared_session.metadata.credits": "Credits used: {value}", + "terminal.shared_session.metadata.directory": "Directory: {value}", + "terminal.shared_session.metadata.run_time": "Run time: {value}", + "terminal.shared_session.metadata.skill": "Skill: {value}", + "terminal.shared_session.metadata.source": "Source: {value}", + "terminal.shared_session.open_in_warp": "Open in Warp", + "terminal.shared_session.open_in_warp_tooltip": "Open this conversation in the Warp desktop app", + "terminal.shared_session.options_disabled_size": "Some options are disabled due to sharing size limits", + "terminal.shared_session.options_disabled_size_and_agents": "Some options are disabled due to sharing size limits and the presence of agent conversations in the session", + "terminal.shared_session.permissions_revoked_due_to_inactivity": "Shared editing permissions were revoked due to inactivity", + "terminal.shared_session.permissions_revoked_sharer_idle": "Editing permissions were revoked because the sharer is idle", + "terminal.shared_session.reconnecting": "Offline, trying to reconnect...", + "terminal.shared_session.request_edit_access": "Request edit access", + "terminal.shared_session.revoke_all_edit_permissions": "Revoke all edit permissions", + "terminal.shared_session.role.edit": "edit", + "terminal.shared_session.role.view": "view", + "terminal.shared_session.scrollback.current_block": "Share from current block", + "terminal.shared_session.scrollback.current_screen": "Share from current screen", + "terminal.shared_session.scrollback.selected_block_onwards": "Share from selected block and onwards", + "terminal.shared_session.scrollback.start_of_session": "Share from start of session", + "terminal.shared_session.scrollback.without_scrollback": "Share without scrollback", + "terminal.shared_session.session_ended": "Session ended.", + "terminal.shared_session.share_session": "Share session", + "terminal.shared_session.share_session_ellipsis": "Share session...", + "terminal.shared_session.sharing_link_copied": "Sharing link copied", + "terminal.shared_session.sharing_will_end_due_to_inactivity": "Sharing will end in {time} due to inactivity.", + "terminal.shared_session.snapshot_subtitle": "This shared conversation shows the state when you opened it. If the agent is still running, refresh to see the latest progress.", + "terminal.shared_session.snapshot_title": "You are viewing a snapshot", + "terminal.shared_session.start_sharing": "Start sharing", + "terminal.shared_session.stop_sharing": "Stop sharing", + "terminal.shared_session.stop_sharing_all": "Stop sharing all", + "terminal.shared_session.stop_sharing_session": "Stop sharing session", + "terminal.shared_session.viewer_request.cancel": "Cancel request", + "terminal.shared_session.viewer_request.header": "You have requested {role} mode", + "terminal.shared_session.viewer_request.waiting": "Waiting for {name}...", + "terminal.shared_session.without_scrollback_disabled_agents": "Sharing without scrollback is disabled because this session has agent conversations", + "terminal.shell.copy_error": "Copy error", + "terminal.shell.file_issue": "File issue", + "terminal.shell.more_info": "More info", + "terminal.shell.process_could_not_start": "Shell process could not start!", + "terminal.shell.process_exited": "Shell process exited", + "terminal.shell.process_exited_prematurely": "Shell process exited prematurely!", + "terminal.shell.warpify_failed_subtext": "Something went wrong while starting {shell_detail} and Warpifying it, causing the process to terminate. Warpify script output is displayed here, which may point at a cause.", + "terminal.skills.project_skill": "Project Skill", + "terminal.slash_commands.active_conversation_required": "{command} requires an active conversation", + "terminal.slash_commands.cloud_oz_only": "{command} is only available for cloud Oz conversations", + "terminal.slash_commands.conversation_exported_to_clipboard": "Conversation exported to clipboard", + "terminal.slash_commands.cost_conversation_empty": "Cannot show conversation cost: conversation is empty", + "terminal.slash_commands.cost_conversation_in_progress": "Cannot show conversation cost: conversation is in progress", + "terminal.slash_commands.cost_no_active_conversation": "Cannot show conversation cost: no active conversation", + "terminal.slash_commands.create_project_missing_description": "Please describe the project you want to create after /create-new-project", + "terminal.slash_commands.export_to_file_unsupported_web": "Export conversation to file unsupported in web", + "terminal.slash_commands.file_not_found": "File not found: {path}", + "terminal.slash_commands.no_active_conversation_to_export": "No active conversation to export", + "terminal.slash_commands.open_file_local_only": "The /open-file command is only available for local sessions", + "terminal.slash_commands.open_file_requires_file": "The /open-file command only works for files, not directories", + "terminal.slash_commands.open_file_unsupported": "The /open-file command is not supported in this build", + "terminal.slash_commands.prompt_argument_required": "{command} requires a prompt argument", + "terminal.slash_commands.rename_tab_missing_name": "Please provide a tab name after /rename-tab", + "terminal.slash_commands.requires_ai_enabled": "{command} requires AI to be enabled", + "terminal.slash_commands.session_already_shared": "Session is already being shared", + "terminal.slash_commands.set_tab_color_missing_color": "Please provide a color after /set-tab-color ({options})", + "terminal.slash_commands.set_tab_color_unknown": "Unknown tab color '{color}'. Use one of: {options}.", + "terminal.ssh_remote_choice.continue_without_installing.description": "You'll still get a Warpified experience just without the coding features.", + "terminal.ssh_remote_choice.continue_without_installing.title": "Continue without installing", + "terminal.ssh_remote_choice.dont_ask_again": "Don't ask me this again", + "terminal.ssh_remote_choice.install_extension.description": "Install Warp's extension to enable agent features like file browsing, code review, and intelligent command completions in this session.", + "terminal.ssh_remote_choice.install_extension.title": "Install Warp's SSH extension", + "terminal.ssh_remote_choice.manage_settings": "Manage Warpify settings", + "terminal.ssh_remote_choice.title": "Choose your experience for this remote session:", + "terminal.ssh.clear_upload": "Clear upload", + "terminal.ssh.error.continue_without_warpification": "Continue without Warpification", + "terminal.ssh.error.report_issue_link": "filing an issue", + "terminal.ssh.error.report_issue_prefix": "We are actively working on improving the stability of SSH in Warp. Please consider ", + "terminal.ssh.error.report_issue_suffix": " on GitHub so we can better identify the problem.", + "terminal.ssh.error.title": "Error Warpifying session", + "terminal.ssh.error.tmux_failed": "tmux failed to execute on the remote machine. Please re-install tmux and try again.", + "terminal.ssh.error.tmux_install_failed": "The tmux install hit an unexpected error. Please install tmux manually and try again.", + "terminal.ssh.error.tmux_not_installed": "tmux is not installed on the remote machine. Please install tmux and try again.", + "terminal.ssh.error.unsupported_shell": "Unsupported shell. Please set bash, zsh, or fish as your default shell and try again.", + "terminal.ssh.error.unsupported_tmux_version": "The tmux version available on the remote machine is below 3.0. Please install tmux 3.0 or greater using a different method and try again.", + "terminal.ssh.error.warpify_timeout": "Warpifying the session hit a timeout.", + "terminal.ssh.error.warpify_without_tmux": "Warpify without TMUX", + "terminal.ssh.file_uploads": "File Uploads", + "terminal.ssh.install_tmux.install_to_warp": "Install to ~/.warp", + "terminal.ssh.install_tmux.install_with": "Install with {package_manager}", + "terminal.ssh.install_tmux.missing_tmux_explanation": "In order to Warpify your SSH session, tmux must be installed. ", + "terminal.ssh.install_tmux.outdated_version_explanation": "In order to Warpify your SSH session, a more recent version of tmux (>=3.0) must be installed. ", + "terminal.ssh.install_tmux.run_script_prompt": "Run this script to install tmux?", + "terminal.ssh.install_tmux.title": "Install tmux?", + "terminal.ssh.remote_server_failed.body": "While advanced features like file browsing and code review are currently disabled, the rest of your Warpified experience is fully available.", + "terminal.ssh.remote_server_failed.start_failed": "Failed to start SSH extension", + "terminal.ssh.remote_server_failed.title": "Couldn't connect to the Warp SSH extension", + "terminal.ssh.to_separator": " to ", + "terminal.ssh.upload.failed": "Failed to upload", + "terminal.ssh.upload.uploaded": "Uploaded", + "terminal.ssh.upload.uploading": "Uploading", + "terminal.ssh.upload.waiting_for_password": "Waiting for password input", + "terminal.ssh.warpify_description": "Bring Warp's features to your remote session. Blocks, full text editing, auto-complete, Oz, and more. ", + "terminal.ssh.warpify_title": "Warpifying SSH Session...", + "terminal.ssh.why_tmux": "Why do I need tmux?", + "terminal.toast.bundled_skills_cannot_edit": "Bundled skills cannot be edited", + "terminal.toast.couldnt_continue_cloud_task": "Couldn't continue this cloud task.", + "terminal.toast.editing_skills_unsupported": "Editing skills is not supported in this build", + "terminal.toast.non_local_env_subshell": "Cannot invoke environment variable subshell in a non-local session", + "terminal.toast.powershell_subshell_not_supported": "PowerShell subshells not supported", + "terminal.toast.skill_not_found": "Skill not found: {reference}", + "terminal.tooltips.copy_secret": "Copy secret", + "terminal.tooltips.hide_secret": "Hide secret", + "terminal.tooltips.open_in_warp": "Open in Warp", + "terminal.tooltips.reveal_secret": "Reveal secret", + "terminal.tooltips.show_containing_folder": "Show containing folder", + "terminal.tooltips.show_in_finder": "Show in Finder", + "terminal.universal_input.attach_context": "Attach context", + "terminal.universal_input.disabled_terminal_mode": "Disabled in terminal mode, re-enable in settings", + "terminal.universal_input.input_mode_locked": "Input mode locked while agent is monitoring a command", + "terminal.universal_input.no_context_objects": "No available objects in the current context.", + "terminal.universal_input.request_edit_access": "Request edit access to change input mode", + "terminal.universal_input.requires_fs": "Requires a filesystem", + "terminal.universal_input.slash_commands": "Slash commands", + "terminal.universal_input.ssh_without_remote_server": "Not supported in SSH sessions without remote server", + "terminal.universal_input.subshell_not_supported": "Not supported in subshells", + "terminal.use_agent_footer.ask_assist": "Ask the Warp agent to assist", + "terminal.use_agent_footer.ask_resume": "Ask the Warp agent to resume", + "terminal.use_agent_footer.enable_shell_integration": "Enable Warp shell integration in this session", + "terminal.use_agent_footer.give_control_back": "Give control back to agent", + "terminal.use_agent_footer.use_agent": "Use agent", + "terminal.use_agent_footer.warpify_ssh_session": "Warpify SSH session", + "terminal.use_agent_footer.warpify_subshell": "Warpify subshell", + "terminal.warning.completions_not_working_prefix": "Seems like your completions are not working (", + "terminal.warning.did_you_intend_prefix": "Did you intend ", + "terminal.warning.enable_ssh_extension_prefix": "). Enabling the SSH extension in ", + "terminal.warning.keep_ide_bindings": "No, keep IDE bindings", + "terminal.warning.may_resolve_suffix": " may resolve this issue.", + "terminal.warning.more_info": "More info", + "terminal.warning.more_info_lower": "more info", + "terminal.warning.move_cursor_suffix": " to move the cursor?", + "terminal.warning.old_prompt_version_prefix": "You seem to be running an older (unsupported) version, please follow ", + "terminal.warning.powerlevel10k_supports_warp": "Powerlevel10k now supports Warp! ", + "terminal.warning.pure_not_supported": "Pure is not yet supported in Warp. You might consider one of the supported prompts as an alternative. ", + "terminal.warning.settings": "settings", + "terminal.warning.shell_config_incompatible": "Your shell configuration is incompatible with Warp... ", + "terminal.warning.shell_start_slow": "Seems like your shell is taking a while to start... ", + "terminal.warning.these_instructions": "these instructions", + "terminal.warning.update_latest_suffix": " to update to the latest version.", + "terminal.warning.use_emacs_style_bindings": "Yes, use Emacs-style bindings", + "terminal.warpify.auto_warpify_command.description": "Run the following to automatically Warpify in the future:", + "terminal.warpify.never_warpify_this_host": "Never Warpify this host", + "terminal.warpify.remote_subshell.description": "In remote subshells, Warp runs commands in the background to power completions, syntax highlighting, and other features.", + "terminal.warpify.session_warpified": "Session Warpified", + "terminal.warpify.ssh_session_lowercase_title": "SSH session", + "terminal.warpify.ssh_session_title": "SSH Session", + "terminal.warpify.subshell_lowercase_title": "subshell", + "terminal.warpify.subshell_title": "Subshell", + "terminal.zero_state.autodetect_agent_prompts": "autodetect agent prompts in terminal sessions", + "terminal.zero_state.cycle_past_commands_and_conversations": "cycle past commands and conversations", + "terminal.zero_state.go_back_to_terminal": "go back to terminal", + "terminal.zero_state.init_callout": "to index this codebase and generate an AGENTS.md for optimal performance", + "terminal.zero_state.new_terminal_session": "New terminal session", + "terminal.zero_state.open_code_review": "open code review", + "terminal.zero_state.start_agent_conversation": "start a new agent conversation", + "terminal.zero_state.start_cloud_agent_conversation": "start a new cloud agent conversation", + "terminal.zero_state.switch_model": "switch model", + "themes.chooser.a11y.close_hint": "Press escape to close.", + "themes.chooser.a11y.description": "Theme chooser. Unfortunately, the theme chooser window is not compatible with screen readers yet.", + "themes.chooser.hint.current": "Change your current theme.", + "themes.chooser.hint.dark": "Pick a theme for when your system is in dark mode.", + "themes.chooser.hint.light": "Pick a theme for when your system is in light mode.", + "themes.chooser.no_matching_themes": "No matching themes!", + "themes.chooser.title": "Themes", + "themes.creator.background_color": "Background color", + "themes.creator.create_theme": "Create theme", + "themes.creator.error_process_image": "Failed to process selected image. Please try again with a different image.", + "themes.creator.error_process_image_with_error": "Failed to process selected image due to error: {error}. Please try again with a different image.", + "themes.creator.modal_header": "Create new theme from image", + "themes.creator.modal_subheader": "Automatically generate a theme based on extracted colors from an image (.png, .jpg).", + "themes.creator.select_image": "Select an image", + "themes.creator.select_new_image": "Select a new image", + "themes.creator.selecting_image": "Selecting image...", + "themes.creator.theme_name": "Theme name", + "themes.deletion.delete_theme": "Delete theme", + "themes.deletion.modal_header": "Are you sure you want to delete this theme?", + "themes.deletion.modal_subheader": "This will permanently delete the theme.", + "tips.ai_command_search.description": "Generate shell commands with natural language.", + "tips.ai_command_search.title": "AI Command Search", + "tips.close_welcome_tips": "Close Welcome Tips", + "tips.command_palette.description": "Easily discover everything you can do in Warp without your hands leaving the keyboard.", + "tips.command_palette.title": "Command Palette", + "tips.complete": "Complete!", + "tips.finished_message": "Nice work on finishing the welcome tips!", + "tips.history_search.description": "Find, edit and re-run previously executed commands.", + "tips.history_search.title": "History Search", + "tips.shortcut": "Shortcut", + "tips.skip_welcome_tips": "Skip Welcome Tips", + "tips.split_pane.description": "Split tabs into multiple panes to make your ideal layout.", + "tips.split_pane.title": "Split Pane", + "tips.theme_picker.description": "Make Warp your own by choosing a built-in theme. Or create your own.", + "tips.theme_picker.title": "Theme Picker", + "uri.custom_uri_invalid": "Custom URI is invalid.", + "uri.custom_uri_invalid_with_error": "Custom URI is invalid: {error}", + "uri.new_tab_created": "New tab created", + "uri.new_tab_created.description": "Go to Warp to see your new tab.", + "util.time.approx.day_ago": "{count} day ago", + "util.time.approx.days_ago": "{count} days ago", + "util.time.approx.hour_ago": "{count} hour ago", + "util.time.approx.hours_ago": "{count} hours ago", + "util.time.approx.just_now": "just now", + "util.time.approx.just_now_sentence": "Just now", + "util.time.approx.minutes_ago_short": "{count} min ago", + "util.time.approx.month_ago": "{count} month ago", + "util.time.approx.months_ago": "{count} months ago", + "util.time.approx.week_ago": "{count} week ago", + "util.time.approx.weeks_ago": "{count} weeks ago", + "util.time.approx.year_ago": "{count} year ago", + "util.time.approx.years_ago": "{count} years ago", + "util.time.elapsed.minute_ago": "{count} minute ago", + "util.time.elapsed.minutes_ago": "{count} minutes ago", + "util.time.precise.days": "{count} days", + "util.time.precise.hours": "{count} hours", + "util.time.precise.milliseconds": "{count} ms", + "util.time.precise.minutes": "{count} min", + "util.time.precise.more_than_one_week": ">1 week", + "util.time.precise.seconds": "{count} sec", + "util.tooltips.secret_not_included_ai_conversation": "This wasn't included in the AI conversation.", + "util.tooltips.secret_not_included_ai_or_shared_blocks": "This won't be included in any AI conversations or shared blocks.", + "util.tooltips.secret_pattern_enterprise": "Pattern matched your organization's secret redaction regex list.", + "util.tooltips.secret_pattern_generic": "Pattern matched the secret redaction regex list.", + "util.tooltips.secret_pattern_user": "Pattern matched your secret redaction regex list.", + "util.tooltips.secrets_not_sent": "*Secrets are not sent to Warp's server.", + "warp_cli.about": "The orchestration platform for cloud agents\n\nThe Oz CLI is a tool for running, managing, and orchestrating coding agents at scale.\nUse the CLI to:\n* Launch and inspect cloud agents\n* Schedule cloud agents to run in the future\n* Manage the environments that cloud agents run in\n* Upload secrets to Oz's secure storage", + "warp_cli.after_help": "Examples:\n\n $ {bin_name} agent run --prompt \"Build anything\"\n\n $ {bin_name} mcp list\n\nLearn more:\n* Use {bin_name} help to learn more about each command\n* Read the documentation at https://docs.warp.dev/reference/cli\n", + "warp_cli.agent.arg.agent_uid.help": "UID of the agent to execute this run as", + "warp_cli.agent.arg.agent_uid.long_help": "UID of the agent to execute this run as.\n\nThis applies the agent's configuration, such as its skills and base model, and attributes credit usage back to the agent.", + "warp_cli.agent.arg.attachment_paths.help": "Path to a file to attach to the agent query", + "warp_cli.agent.arg.attachment_paths.long_help": "Path to a file to attach to the agent query.\n\nCan be specified multiple times to attach multiple files (maximum 5).\n\nExample: --attach file1.png --attach file2.txt", + "warp_cli.agent.arg.computer_use.help": "Enable computer use capabilities for this agent run", + "warp_cli.agent.arg.config_file.help": "Path to a YAML or JSON configuration file", + "warp_cli.agent.arg.conversation.help": "Continue an existing cloud conversation by ID", + "warp_cli.agent.arg.create.base_model.help": "Base model for runs of this agent", + "warp_cli.agent.arg.create.description.help": "Description of the agent", + "warp_cli.agent.arg.create.environment.help": "Default cloud environment for runs of this agent", + "warp_cli.agent.arg.create.name.help": "Name of the agent", + "warp_cli.agent.arg.create.secrets.help": "Attach a secret to the agent. Repeat the flag for multiple secrets.", + "warp_cli.agent.arg.create.skills.help": "Attach a skill to the agent. Repeat the flag for multiple skills.", + "warp_cli.agent.arg.cwd.help": "Working directory for the agent", + "warp_cli.agent.arg.environment.help": "Cloud environment to use, identified by ID", + "warp_cli.agent.arg.jq_filter.help": "Filter values from the response using jq syntax", + "warp_cli.agent.arg.jq_filter.long_help": "A filter to select values from the response using jq syntax.\n\nExample: `--jq '.runs[].creator'`\n\nWhen set, the result of the filter expression is printed instead of the full JSON output. Top-level scalar outputs are automatically unquoted.", + "warp_cli.agent.arg.model.help": "Override the base model used by this command. Use `warp model list` to see available models.", + "warp_cli.agent.arg.name.help": "Name for this agent task", + "warp_cli.agent.arg.no_computer_use.help": "Disable computer use capabilities for this agent run", + "warp_cli.agent.arg.no_environment.help": "Do not run the agent in an environment (not recommended)", + "warp_cli.agent.arg.no_snapshot.help": "Disable the end-of-run workspace snapshot upload", + "warp_cli.agent.arg.open.help": "Open the agent's session in Warp once it's available", + "warp_cli.agent.arg.profile.help": "Agent profile to configure the terminal session", + "warp_cli.agent.arg.prompt.help": "Prompt for the agent to carry out", + "warp_cli.agent.arg.saved_prompt.help": "The saved AI prompt to run, identified by ID", + "warp_cli.agent.arg.scope.personal.help": "Create as private to your account", + "warp_cli.agent.arg.scope.team.help": "Create at the team level", + "warp_cli.agent.arg.skill.help": "Use a skill as the base prompt for the agent", + "warp_cli.agent.arg.skill.long_help": "Use a skill as the base prompt for the agent.\n\nFormat: `skill_name`, `repo:skill_name`, or `org/repo:skill_name`\n\nSkills are searched in `.agents/skills/`, `.warp/skills/`, `.claude/skills/`, and `.codex/skills/` directories. If a repo is specified, searches only that repo. If org is also specified, validates the repo's git remote matches the expected org.\n\nWhen used with --prompt, the skill provides the base context and the prompt is the task.\n\nTo automate a skill on a schedule, use `oz schedule create --skill `.", + "warp_cli.agent.arg.skills.repo.help": "List skills from a specific GitHub repository", + "warp_cli.agent.arg.skills.repo.long_help": "List skills from a specific GitHub repository.\n\nFormat: `owner/repo` or `https://github.com/owner/repo`\n\nWhen provided, lists skills from this repo instead of from your environments. Any environments that include this repo will still be shown in the results.", + "warp_cli.agent.arg.snapshot_script_timeout.help": "Maximum time to wait for the declarations script before uploading the snapshot", + "warp_cli.agent.arg.snapshot_upload_timeout.help": "Maximum time to wait for the end-of-run snapshot upload", + "warp_cli.agent.arg.sort_by.help": "Sort field. Only supported for pretty, text, and ndjson output.", + "warp_cli.agent.arg.sort_order.help": "Sort direction. Only supported for pretty, text, and ndjson output.", + "warp_cli.agent.arg.uid.delete.help": "UID of the agent to delete", + "warp_cli.agent.arg.uid.get.help": "UID of the agent to get", + "warp_cli.agent.arg.uid.update.help": "UID of the agent to update", + "warp_cli.agent.arg.update.add_secrets.help": "Add a secret to the agent. Repeat the flag for multiple secrets.", + "warp_cli.agent.arg.update.add_skills.help": "Add a skill to the agent. Repeat the flag for multiple skills.", + "warp_cli.agent.arg.update.base_model.help": "Replacement base model for runs executed by this agent", + "warp_cli.agent.arg.update.description.help": "Replacement description for the agent", + "warp_cli.agent.arg.update.environment.help": "Replacement default cloud environment for runs executed by this agent", + "warp_cli.agent.arg.update.name.help": "New name for the agent", + "warp_cli.agent.arg.update.remove_all_secrets.help": "Remove all secrets from the agent", + "warp_cli.agent.arg.update.remove_all_skills.help": "Remove all skills from the agent", + "warp_cli.agent.arg.update.remove_base_model.help": "Remove the agent base model", + "warp_cli.agent.arg.update.remove_description.help": "Remove the agent description", + "warp_cli.agent.arg.update.remove_environment.help": "Remove the agent default environment", + "warp_cli.agent.arg.update.remove_secrets.help": "Remove a secret from the agent. Repeat the flag for multiple secrets.", + "warp_cli.agent.arg.update.remove_skills.help": "Remove a skill from the agent. Repeat the flag for multiple skills.", + "warp_cli.agent.arg.worker_host.help": "Where this job should be hosted", + "warp_cli.agent.arg.worker_host.long_help": "Where this job should be hosted.\n\nSetting \"warp\" runs it on Warp's infrastructure. Any other value is treated as a self-hosted job and the value will be matched with the self-hosted worker's name.", + "warp_cli.agent.prompt.plain_text": "Prompt: {text}", + "warp_cli.agent.prompt.saved_prompt_id": "Saved Prompt ID: {id}", + "warp_cli.api_key.arg.agent_uid.help": "UID of the agent to authenticate as", + "warp_cli.api_key.arg.expires_at.help": "Expire the API key at a specific time", + "warp_cli.api_key.arg.expires_in.help": "Expire the API key after this duration, such as \"30d\", \"12h\", or \"90m\"", + "warp_cli.api_key.arg.force.expire.help": "Expire without asking for confirmation", + "warp_cli.api_key.arg.key_uid.expire.help": "Name or UID of the API key to expire", + "warp_cli.api_key.arg.name.create.help": "Name of the API key to create", + "warp_cli.api_key.arg.no_expiration.help": "Create an API key with no expiration", + "warp_cli.api_key.arg.sort_by.help": "Sort field", + "warp_cli.api_key.arg.sort_order.help": "Sort direction", + "warp_cli.arg.api_key.help": "API key for server authentication", + "warp_cli.arg.debug.help": "Enable debug logging", + "warp_cli.arg.output_format.help": "Set the output format", + "warp_cli.artifact.arg.artifact_uid.download.help": "UID of the artifact to download", + "warp_cli.artifact.arg.artifact_uid.get.help": "UID of the artifact to get", + "warp_cli.artifact.arg.conversation_id.help": "Associate the uploaded artifact with a conversation", + "warp_cli.artifact.arg.description.help": "Description for the uploaded artifact", + "warp_cli.artifact.arg.out.help": "Write the downloaded artifact to a specific file path", + "warp_cli.artifact.arg.path.upload.help": "Path to the artifact file to upload", + "warp_cli.artifact.arg.run_id.help": "Associate the uploaded artifact with a run", + "warp_cli.command.agent.about": "Interact with Oz", + "warp_cli.command.agent.create.about": "Create a new agent", + "warp_cli.command.agent.delete.about": "Delete an agent", + "warp_cli.command.agent.get.about": "Get details of an agent", + "warp_cli.command.agent.list.about": "List all available agents", + "warp_cli.command.agent.profile.about": "Manage agent profiles", + "warp_cli.command.agent.profile.list.about": "List available agent profiles", + "warp_cli.command.agent.run_cloud.about": "Dispatch an Oz agent that runs remotely", + "warp_cli.command.agent.run.about": "Run a new Oz agent", + "warp_cli.command.agent.skills.about": "List available agent skills", + "warp_cli.command.agent.update.about": "Update an existing agent", + "warp_cli.command.api_key.about": "Manage API keys", + "warp_cli.command.api_key.create.about": "Create a new API key", + "warp_cli.command.api_key.expire.about": "Immediately expire an API key", + "warp_cli.command.api_key.list.about": "List active API keys", + "warp_cli.command.artifact.about": "Manage artifacts", + "warp_cli.command.artifact.download.about": "Download an artifact file", + "warp_cli.command.artifact.get.about": "Get artifact metadata", + "warp_cli.command.artifact.upload.about": "Upload an artifact file", + "warp_cli.command.completions.about": "Generate shell completions for your shell to stdout", + "warp_cli.command.completions.long_about": "Generate shell completions for your shell to stdout.\n\nFor bash, add the following to ~/.bashrc:\n source <(path/to/warp completions bash)\n\nFor zsh, add the following to ~/.zshrc:\n source <(path/to/warp completions zsh)\n\nFor fish, add the following to ~/.config/fish/config.fish:\n path/to/warp completions fish | source\n\nFor Powershell, add the following to $PROFILE:\n path\\to\\warp | Out-String | Invoke-Expression\n\nIf no shell is provided, this defaults to the shell that Warp was run from.", + "warp_cli.command.completions.shell.help": "Shell to generate completions for", + "warp_cli.command.dump_debug_info.about": "Print debugging information and exit", + "warp_cli.command.environment.about": "Manage cloud environments", + "warp_cli.command.environment.create.about": "Create a new cloud environment", + "warp_cli.command.environment.delete.about": "Delete a cloud environment", + "warp_cli.command.environment.get.about": "Get details of a cloud environment", + "warp_cli.command.environment.image.about": "Manage base images for cloud environments", + "warp_cli.command.environment.image.list.about": "List available Warp dev base images from Docker Hub", + "warp_cli.command.environment.list.about": "List cloud environments", + "warp_cli.command.environment.update.about": "Update an existing cloud environment", + "warp_cli.command.federate.about": "Issue and manage federated identity tokens", + "warp_cli.command.federate.issue_gcp_token.about": "Issue an identity token for Google Cloud executable-sourced credentials", + "warp_cli.command.federate.issue_gcp_token.long_about": "Issue an identity token for the current Oz agent, in the format expected by Google Cloud's executable-sourced credentials mechanism.\n\nSee https://docs.cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#executable-sourced-credentials", + "warp_cli.command.federate.issue_token.about": "Issue an identity token for the current Oz agent", + "warp_cli.command.federate.long_about": "Federated authentication between Oz and cloud providers.\n\nOz supports OIDC federation to allow agents to securely authenticate to other systems using short-lived credentials.", + "warp_cli.command.harness_support.about": "Support commands for agent harnesses to integrate with Oz", + "warp_cli.command.harness_support.finish_task.about": "Report task completion or failure", + "warp_cli.command.harness_support.notify_user.about": "Send a progress notification to the originating platform", + "warp_cli.command.harness_support.ping.about": "Verify connectivity to the current run", + "warp_cli.command.harness_support.report_artifact.about": "Report an artifact back to Oz", + "warp_cli.command.harness_support.report_artifact.pull_request.about": "Report a pull request artifact", + "warp_cli.command.harness_support.report_shutdown.about": "Report that the agent process is shutting down", + "warp_cli.command.integration.about": "Manage integrations", + "warp_cli.command.integration.create.about": "Create a new integration", + "warp_cli.command.integration.list.about": "List simple integrations and their connection status", + "warp_cli.command.integration.update.about": "Update an integration", + "warp_cli.command.login.about": "Log in to Warp", + "warp_cli.command.logout.about": "Log out of Warp", + "warp_cli.command.mcp.about": "Manage MCP servers", + "warp_cli.command.mcp.list.about": "List MCP servers", + "warp_cli.command.model.about": "Manage available models", + "warp_cli.command.model.list.about": "List available models", + "warp_cli.command.print_telemetry_events.about": "Print telemetry events in production and exit", + "warp_cli.command.provider.about": "Manage providers", + "warp_cli.command.provider.list.about": "List providers", + "warp_cli.command.provider.setup.about": "Set up a provider", + "warp_cli.command.run.about": "Manage runs", + "warp_cli.command.run.conversation.about": "Retrieve run conversations", + "warp_cli.command.run.conversation.get.about": "Get a conversation by conversation ID", + "warp_cli.command.run.get.about": "Get status of a specific run", + "warp_cli.command.run.list.about": "List ambient agent runs", + "warp_cli.command.run.message.about": "Messages sent to and from runs", + "warp_cli.command.run.message.list.about": "List inbox message headers for a run", + "warp_cli.command.run.message.mark_delivered.about": "Mark a message as delivered", + "warp_cli.command.run.message.read.about": "Read a full message body", + "warp_cli.command.run.message.send.about": "Send a message from one run to one or more recipient runs", + "warp_cli.command.run.message.watch.about": "Watch for new messages delivered to a run", + "warp_cli.command.schedule.about": "Create and manage scheduled Oz agents", + "warp_cli.command.schedule.create.about": "Create a scheduled Oz agent", + "warp_cli.command.schedule.delete.about": "Delete a scheduled Oz agent", + "warp_cli.command.schedule.get.about": "Get a scheduled Oz agent's configuration", + "warp_cli.command.schedule.list.about": "List scheduled Oz agents", + "warp_cli.command.schedule.long_about": "Create and manage scheduled Oz agents. Scheduled agents run a user-defined task periodically, according to a cron schedule.\n\nAs a shorthand, the `schedule` command behaves identically to `schedule create`.", + "warp_cli.command.schedule.pause.about": "Pause a scheduled Oz agent", + "warp_cli.command.schedule.pause.long_about": "Pause a scheduled Oz agent.\n\nA paused agent still exists, but will not run according to its schedule.", + "warp_cli.command.schedule.unpause.about": "Unpause a scheduled Oz agent", + "warp_cli.command.schedule.unpause.long_about": "Unpause a scheduled Oz agent.\n\nThe agent will resume executing on its previously-configured schedule.", + "warp_cli.command.schedule.update.about": "Update a scheduled Oz agent", + "warp_cli.command.secret.about": "Manage secrets", + "warp_cli.command.secret.create.about": "Create a new secret", + "warp_cli.command.secret.create.claude.about": "Create a Claude/Anthropic auth secret", + "warp_cli.command.secret.create.claude.api_key.about": "Direct Anthropic API key", + "warp_cli.command.secret.create.claude.bedrock_access_key.about": "Anthropic Bedrock authentication via AWS access keys", + "warp_cli.command.secret.create.claude.bedrock_api_key.about": "Anthropic API key via Amazon Bedrock", + "warp_cli.command.secret.create.codex.about": "Create a Codex/OpenAI auth secret", + "warp_cli.command.secret.create.codex.api_key.about": "Direct OpenAI API key", + "warp_cli.command.secret.create.long_about": "Create a new secret.\n\nUse `oz secret create claude api-key ` to create a Claude/Anthropic auth secret, or `oz secret create codex api-key ` to create a Codex/OpenAI auth secret.", + "warp_cli.command.secret.delete.about": "Delete a secret", + "warp_cli.command.secret.list.about": "List secrets", + "warp_cli.command.secret.update.about": "Update a secret", + "warp_cli.command.secret.update.long_about": "Update a secret.\n\nThis command supports changing the value (via the `--value` or `--value-file` flags) or the description. Moving or renaming secrets is not currently supported.", + "warp_cli.command.whoami.about": "Print information about the logged-in user", + "warp_cli.command.worker.minidump_server.about": "Run the minidump server", + "warp_cli.command.worker.plugin_host.about": "Run this process as the plugin host rather than the main app", + "warp_cli.command.worker.remote_server_daemon.about": "Run the long-lived remote development server daemon", + "warp_cli.command.worker.remote_server_proxy.about": "Run the remote development server proxy over SSH stdio", + "warp_cli.command.worker.ripgrep_search.about": "Run a headless ripgrep search worker", + "warp_cli.command.worker.terminal_server.about": "Run the terminal server", + "warp_cli.completions.error.shell_not_detected": "Could not determine shell from environment. Please provide a shell argument.", + "warp_cli.environment.arg.description.create.help": "Description of the environment (max 240 characters)", + "warp_cli.environment.arg.description.update.help": "Description of the environment (max 240 characters)", + "warp_cli.environment.arg.docker_image.create.help": "Docker image to use", + "warp_cli.environment.arg.docker_image.create.long_help": "Docker image to use. Run `warp environment image list` to list suggested dev images.\n\nIf not specified, you'll be prompted to select from available images.", + "warp_cli.environment.arg.docker_image.update.help": "Docker image to use (optional, updates if present)", + "warp_cli.environment.arg.force.delete.help": "Force delete without checking for integration usage", + "warp_cli.environment.arg.force.update.help": "Force update without checking for integration usage", + "warp_cli.environment.arg.id.delete.help": "ID of the environment to delete", + "warp_cli.environment.arg.id.get.help": "ID of the environment to get", + "warp_cli.environment.arg.id.update.help": "ID of the environment to update", + "warp_cli.environment.arg.name.create.help": "Name of the environment", + "warp_cli.environment.arg.name.update.help": "Name of the environment (optional, updates if present)", + "warp_cli.environment.arg.remove_description.help": "Remove the description from the environment", + "warp_cli.environment.arg.remove_repo.help": "Git repo in format \"owner/repo\" to remove (can be specified multiple times)", + "warp_cli.environment.arg.remove_setup_command.help": "Setup command to remove from the list (can be specified multiple times)", + "warp_cli.environment.arg.repo.create.help": "Git repo in format \"owner/repo\" (can be specified multiple times)", + "warp_cli.environment.arg.repo.update.help": "Git repo in format \"owner/repo\" to add (can be specified multiple times)", + "warp_cli.environment.arg.setup_command.create.help": "Accept multiple setup command args to be run after cloning", + "warp_cli.environment.arg.setup_command.update.help": "Setup command to add to the end of the list (can be specified multiple times)", + "warp_cli.environment.error.description_too_long": "Description must be at most {max} characters (got {len})", + "warp_cli.error.invalid_jq_filter": "invalid jq filter `{src}`:\n{detail}", + "warp_cli.error.invalid_parent_handle": "invalid parent handle: {error}", + "warp_cli.error.invalid_rfc3339": "invalid RFC 3339 timestamp '{value}': {error}", + "warp_cli.error.more_info_help": "For more information, try '--help'", + "warp_cli.error.unrecognized_subcommand": "error: unrecognized subcommand '{subcommand}'", + "warp_cli.federate.arg.audience.help": "The audience claim for the identity token", + "warp_cli.federate.arg.duration.help": "Requested token lifetime (e.g. \"1h\", \"30m\")", + "warp_cli.federate.arg.gcp_audience.help": "The audience for the token request", + "warp_cli.federate.arg.gcp_output_file.help": "Optional path to write the token output for caching", + "warp_cli.federate.arg.gcp_token_type.help": "The requested token type (e.g. \"urn:ietf:params:oauth:token-type:id_token\")", + "warp_cli.federate.arg.run_id.help": "The run ID to issue the token for", + "warp_cli.federate.arg.subject_template.help": "Controls how the OIDC token subject is formatted", + "warp_cli.federate.arg.subject_template.long_help": "Controls how the OIDC token subject is formatted.\n\nThe template consists of a list of claims, which are joined together to form the subject. The default subject template is the principal, such as `user:user-id`.\n\nSupported components are:\n- principal (`user:my-user-id`)\n- scoped_principal (`principal:my-team-id/user:my-user-id`)\n- email (`email:user@warp.dev`)\n- teams (`teams:my-team-id`)\n- environment (`environment:my-environment-id`)\n- agent_name (`agent_name:my-agent`)\n- skill_spec (`skill_spec:warpdotdev/repo_path_to_skill`)\n- run_id (`run_id:abc123`)\n- host (`host:my-worker-id`)", + "warp_cli.harness_support.arg.error_category.help": "Error category for abnormal shutdown", + "warp_cli.harness_support.arg.error_message.help": "Human-readable error message for abnormal shutdown", + "warp_cli.harness_support.arg.message.help": "Message to send as a progress update", + "warp_cli.harness_support.arg.pull_request.branch.help": "Branch name associated with the pull request", + "warp_cli.harness_support.arg.pull_request.url.help": "URL of the pull request", + "warp_cli.harness_support.arg.run_id.help": "Run ID to associate with harness-support API calls", + "warp_cli.harness_support.arg.status.help": "Whether the task succeeded or failed", + "warp_cli.harness_support.arg.summary.help": "Summary of the task outcome", + "warp_cli.integration.arg.environment.update.help": "Replacement cloud environment for this integration", + "warp_cli.integration.arg.mcp_specs.help": "MCP servers to configure for this integration", + "warp_cli.integration.arg.mcp_specs.long_help": "MCP servers to configure for this integration.\n\nCan be specified as:\n- A path to a JSON file containing MCP configuration\n- Inline JSON with MCP server configuration\n\nCan be specified multiple times to include multiple servers.", + "warp_cli.integration.arg.prompt.help": "Custom instructions for the integration", + "warp_cli.integration.arg.provider.create.help": "Provider to create the integration for", + "warp_cli.integration.arg.provider.update.help": "Provider to update the integration for", + "warp_cli.integration.arg.remove_environment.help": "Remove the cloud environment from this integration", + "warp_cli.integration.arg.remove_mcp.help": "Remove MCP servers from this integration by server name", + "warp_cli.integration.arg.remove_mcp.long_help": "Remove MCP servers from this integration by server name.\n\nThis removes the server entry whose key matches `SERVER_NAME`.", + "warp_cli.integration.arg.worker_host.help": "Worker host ID for self-hosted workers", + "warp_cli.integration.arg.worker_host.long_help": "Worker host ID for self-hosted workers.\n\nIf not specified or set to \"warp\", tasks will run on Warp-hosted workers.", + "warp_cli.mcp.arg.spec.help": "MCP servers to start before executing the agent", + "warp_cli.mcp.arg.spec.long_help": "MCP servers to start before executing the agent.\n\nCan be specified as:\n- A path to a JSON file containing MCP configuration\n- Inline JSON with MCP server configuration\n\nCan be specified multiple times to include multiple servers.", + "warp_cli.mcp.error.invalid_utf8": "Invalid UTF-8 in MCP spec", + "warp_cli.mcp.error.read_config_file_failed": "Failed to read MCP config file '{path}': {error}", + "warp_cli.mcp.possible.json": "Inline JSON MCP server configuration", + "warp_cli.mcp.possible.path": "Path to a JSON file containing MCP config", + "warp_cli.provider.arg.personal.help": "Set up provider for a personal account", + "warp_cli.provider.arg.provider_type.help": "The type of provider to set up", + "warp_cli.provider.arg.team.help": "Set up provider for a team", + "warp_cli.schedule.arg.cron.create.help": "Cron schedule expression (e.g., \"0 9 * * 1\" for 9 AM every Monday)", + "warp_cli.schedule.arg.cron.update.help": "Update the cron schedule on which the agent is executed", + "warp_cli.schedule.arg.environment.update.help": "Replacement cloud environment for this schedule", + "warp_cli.schedule.arg.mcp_specs.help": "MCP servers to configure for this schedule", + "warp_cli.schedule.arg.mcp_specs.long_help": "MCP servers to configure for this schedule.\n\nCan be specified as:\n- A path to a JSON file containing MCP configuration\n- Inline JSON with MCP server configuration\n\nCan be specified multiple times to include multiple servers.", + "warp_cli.schedule.arg.name.create.help": "Name of the scheduled agent", + "warp_cli.schedule.arg.name.update.help": "Update the scheduled agent name", + "warp_cli.schedule.arg.prompt.create.help": "Prompt for what the scheduled agent should do", + "warp_cli.schedule.arg.prompt.update.help": "Update the scheduled agent's prompt", + "warp_cli.schedule.arg.remove_environment.help": "Remove the cloud environment from this schedule", + "warp_cli.schedule.arg.remove_mcp.help": "Remove MCP servers from this schedule by server name", + "warp_cli.schedule.arg.remove_mcp.long_help": "Remove MCP servers from this schedule by server name.\n\nThis removes the server entry whose key matches `SERVER_NAME`.", + "warp_cli.schedule.arg.remove_skill.help": "Remove the skill from this scheduled agent", + "warp_cli.schedule.arg.schedule_id.delete.help": "ID of the schedule to delete", + "warp_cli.schedule.arg.schedule_id.get.help": "ID of the schedule to get", + "warp_cli.schedule.arg.schedule_id.pause.help": "ID of the schedule to pause", + "warp_cli.schedule.arg.schedule_id.unpause.help": "ID of the schedule to unpause", + "warp_cli.schedule.arg.schedule_id.update.help": "ID of the schedule to update", + "warp_cli.schedule.arg.skill.create.help": "Automate a skill to run on a schedule", + "warp_cli.schedule.arg.skill.create.long_help": "Automate a skill to run on a schedule.\n\nFormat: `repo:skill_name` or `org/repo:skill_name`\n\nSkills are searched in `.agents/skills/`, `.warp/skills/`, `.claude/skills/`, and `.codex/skills/` directories. The skill is resolved at runtime in the agent's cloud environment.\n\nWhen used with --prompt, the skill provides the base context and the prompt is the user task. This is useful for running recurring workflows like code reviews, dependency updates, or reports.", + "warp_cli.schedule.arg.skill.update.help": "Update the skill used as the base prompt for the scheduled agent", + "warp_cli.schedule.arg.skill.update.long_help": "Update the skill used as the base prompt for the scheduled agent.\n\nFormat: `skill_name`, `repo:skill_name`, or `org/repo:skill_name`\n\nSkills are searched in `.agents/skills/`, `.warp/skills/`, `.claude/skills/`, and `.codex/skills/` directories. The skill is resolved at runtime in the agent's cloud environment.", + "warp_cli.schedule.arg.worker_host.help": "Where this job should be hosted", + "warp_cli.schedule.arg.worker_host.long_help": "Where this job should be hosted.\n\nSetting \"warp\" runs it on Warp's infrastructure. Any other value is treated as a self-hosted job and the value will be matched with the self-hosted worker's name.", + "warp_cli.secret.arg.aws_access_key_id.help": "AWS access key ID. If not provided, prompts interactively.", + "warp_cli.secret.arg.aws_secret_access_key.help": "AWS secret access key. If not provided, prompts interactively.", + "warp_cli.secret.arg.aws_session_token.help": "AWS session token. If not provided, prompts interactively.", + "warp_cli.secret.arg.bedrock_api_key.help": "Bedrock API key. If not provided, prompts interactively.", + "warp_cli.secret.arg.bedrock_region.help": "AWS region for the Bedrock endpoint. If not provided, prompts interactively.", + "warp_cli.secret.arg.description.help": "Description of the secret", + "warp_cli.secret.arg.description.update.help": "New description for the secret. If omitted, the description is not changed.", + "warp_cli.secret.arg.force.delete.help": "Delete without asking for confirmation", + "warp_cli.secret.arg.name.create.help": "Name of the secret", + "warp_cli.secret.arg.name.delete.help": "Name of the secret to delete", + "warp_cli.secret.arg.name.update.help": "Name of the secret to update", + "warp_cli.secret.arg.openai_base_url.help": "Optional base URL for the OpenAI API", + "warp_cli.secret.arg.openai_base_url.long_help": "Optional base URL for the OpenAI API (e.g. a regional endpoint like `https://us.api.openai.com/v1`).\n\nWhen omitted in interactive mode the CLI prompts for it; pressing Enter at the prompt skips it. When omitted in non-interactive mode the harness uses the provider's default endpoint.", + "warp_cli.secret.arg.secret_type.help": "Type of secret to create", + "warp_cli.secret.arg.value_file.help": "File to read the secret value from", + "warp_cli.secret.arg.value_file.long_help": "File to read the secret value from.\n\nIf not provided, the secret value will be read from standard input.", + "warp_cli.secret.arg.value.update.help": "Prompt for a new value for the secret", + "warp_cli.share.arg.help": "Share the agent's session", + "warp_cli.share.arg.long_help": "Share the agent's session.\n\nLearn more at https://docs.warp.dev/knowledge-and-collaboration/session-sharing", + "warp_cli.share.error.invalid_recipient": "Invalid share recipient", + "warp_cli.share.error.invalid_subject": "Cannot share with '{subject}'. Expected 'team', 'public', or an email address", + "warp_cli.share.possible.public_edit": "Share with anyone who has the link, with edit access", + "warp_cli.share.possible.public_view": "Share with anyone who has the link, view-only", + "warp_cli.share.possible.team_edit": "Share with your team, with edit access", + "warp_cli.share.possible.team_view": "Share with your team, view-only", + "warp_cli.share.possible.user_edit": "Share with , with edit access", + "warp_cli.share.possible.user_view": "Share with , view-only", + "warp_cli.skill.error.empty_identifier": "Skill identifier cannot be empty", + "warp_cli.skill.error.empty_org": "Organization cannot be empty", + "warp_cli.skill.error.empty_qualifier": "Qualifier cannot be empty in 'repo:skill_identifier' format", + "warp_cli.skill.error.empty_repo": "Repository name cannot be empty", + "warp_cli.skill.error.empty_specifier": "Skill specifier cannot be empty", + "warp_cli.task.arg.ancestor_run.help": "Filter to descendants of a specific run", + "warp_cli.task.arg.artifact_type.help": "Filter by produced artifact type", + "warp_cli.task.arg.conversation_id.help": "The conversation ID to retrieve", + "warp_cli.task.arg.conversation.help": "Retrieve the conversation for this run instead of the run status", + "warp_cli.task.arg.created_after.help": "Only include runs created after the given timestamp", + "warp_cli.task.arg.created_before.help": "Only include runs created before the given timestamp", + "warp_cli.task.arg.creator.help": "Filter by creator ID", + "warp_cli.task.arg.cursor.help": "Opaque pagination cursor from a previous list response", + "warp_cli.task.arg.cursor.long_help": "Opaque pagination cursor from a previous list response.\n\nWhen using `--cursor`, `--sort-by` and `--sort-order` must match the values used to obtain the cursor.", + "warp_cli.task.arg.environment.help": "Filter by environment ID", + "warp_cli.task.arg.execution_location.help": "Filter by where the run executed", + "warp_cli.task.arg.limit.help": "Maximum number of runs to return (default: 10)", + "warp_cli.task.arg.message.body.help": "Message body", + "warp_cli.task.arg.message.limit.help": "Maximum number of messages to return (default: 50)", + "warp_cli.task.arg.message.message_id.mark_delivered.help": "The message ID to mark as delivered", + "warp_cli.task.arg.message.message_id.read.help": "The message ID to read", + "warp_cli.task.arg.message.run_id.list.help": "The run ID whose inbox should be listed", + "warp_cli.task.arg.message.run_id.watch.help": "The run ID whose inbox should be watched", + "warp_cli.task.arg.message.sender_run_id.help": "Sender run ID", + "warp_cli.task.arg.message.since_sequence.help": "Resume after this event sequence (inclusive cursor for reconnects)", + "warp_cli.task.arg.message.since.help": "Only return messages sent at or after this RFC3339 timestamp", + "warp_cli.task.arg.message.subject.help": "Message subject", + "warp_cli.task.arg.message.to.help": "Recipient run ID. Repeat the flag to send to multiple recipients", + "warp_cli.task.arg.message.unread.help": "Only return unread messages", + "warp_cli.task.arg.model.help": "Filter by model ID", + "warp_cli.task.arg.name.help": "Filter by agent config name", + "warp_cli.task.arg.query.help": "Fuzzy search across run title, prompt, and skill spec", + "warp_cli.task.arg.schedule.help": "Filter to runs created by a specific scheduled agent", + "warp_cli.task.arg.skill.help": "Filter by skill (e.g. `owner/repo:path/to/SKILL.md`)", + "warp_cli.task.arg.sort_by.help": "Sort field", + "warp_cli.task.arg.sort_order.help": "Sort direction", + "warp_cli.task.arg.source.help": "Filter by run source", + "warp_cli.task.arg.state.help": "Filter by run state. Repeat the flag to match any of multiple states", + "warp_cli.task.arg.task_id.help": "The run ID to get status for", + "warp_cli.task.arg.updated_after.help": "Only include runs updated after the given timestamp", + "warp_cli.worker.arg.minidump_socket_name.help": "Socket name for the minidump server", + "warp_cli.worker.arg.ripgrep_ignore_case.help": "Search case-insensitively", + "warp_cli.worker.arg.ripgrep_multiline.help": "Allow matches to span multiple lines", + "warp_cli.worker.arg.ripgrep_paths.help": "Paths to search", + "warp_cli.worker.arg.ripgrep_pattern.help": "Search pattern", + "wasm_nux.always_open_on_web_title": "Always open {object_kind} on the web?", + "wasm_nux.change_in_settings": "You can change this at any time in settings.", + "wasm_nux.download_desktop_description": "Warp is the intelligent terminal with AI and your dev team's knowledge built-in.", + "wasm_nux.download_desktop_title": "Download Warp Desktop?", + "wasm_nux.future_links_desktop": "Future links will automatically open on desktop.", + "wasm_nux.object_kind.drive_objects": "Warp Drive objects", + "wasm_nux.object_kind.shared_sessions": "shared sessions", + "wasm_nux.object_kind.warp_links": "Warp links", + "wasm_nux.open_in_desktop_title": "Open in Warp Desktop?", + "wasm_nux.yes": "Yes", + "workflow.toast.out_of_ai_credits": "Looks like you’re out of AI credits.", + "workflow.toast.out_of_ai_credits_contact_admin": "Looks like you’re out of AI credits. Contact a team admin to upgrade for more credits.", + "workflow.toast.upgrade_for_more_credits": "Upgrade for more credits.", + "workflows.alias.add_alias": "Add alias", + "workflows.alias.name_placeholder": "alias name", + "workflows.argument.add_tooltip": "Add a workflow argument", + "workflows.arguments.add_environment_variables": "Add environment variables", + "workflows.arguments.alias_value_placeholder": "Value (optional)", + "workflows.arguments.description": "Fill out the arguments in this workflow and copy it to run in your terminal session", + "workflows.arguments.environment_variables": "Environment variables", + "workflows.arguments.new_environment_variables": "New environment variables", + "workflows.arguments.section": "Arguments", + "workflows.categories.a11y.help": "Search or use arrow up and arrow down keys to navigate and find a workflow. Use enter to confirm the workflow and esc to quit.", + "workflows.categories.a11y.selected": "Selected {name} {content}", + "workflows.categories.a11y.showing_all": "Showing All workflows", + "workflows.categories.a11y.showing_category": "Showing {category} workflows", + "workflows.categories.a11y.showing_mine": "Showing My Workflows", + "workflows.categories.a11y.showing_project": "Showing Repository Workflows", + "workflows.categories.a11y.showing_team": "Showing Team Workflows", + "workflows.categories.a11y.title": "Workflows", + "workflows.categories.all": "All", + "workflows.categories.create_your_own": "creating your own workflow", + "workflows.categories.label": "Categories", + "workflows.categories.my_workflows": "My Workflows", + "workflows.categories.no_matching_workflows": "No matching workflows found.", + "workflows.categories.repository_workflows": "Repository Workflows", + "workflows.categories.team_workflows": "Team Workflows", + "workflows.editor.access_removed": "You no longer have access to this workflow", + "workflows.editor.agent_prompt_placeholder": "Enter your prompt here... (e.g., 'Create a function to sort an array of objects by date' or 'Help me debug this React component').", + "workflows.editor.alias_help_tooltip": "Aliases allow you to create short strings to execute workflows. Each alias can have different argument values and environment variables, and aliases are personal to you.", + "workflows.editor.aliases_section": "Aliases", + "workflows.editor.autofill": "Autofill", + "workflows.editor.autofill_tooltip": "Generate a title, descriptions, or parameters with Warp AI", + "workflows.editor.cannot_save_with_secrets": "This workflow cannot be saved because it contains secrets", + "workflows.editor.command_copied": "Command copied.", + "workflows.editor.command_placeholder": "echo \"Hello {{your_name}}\" # insert arguments with curly braces\n# enter a single-line command or an entire shell script", + "workflows.editor.could_not_create": "Could not create workflow", + "workflows.editor.create": "Create", + "workflows.editor.description_placeholder": "Add a description", + "workflows.editor.discard_changes": "Discard changes", + "workflows.editor.error_saving_aliases": "Error saving aliases", + "workflows.editor.keep_editing": "Keep editing", + "workflows.editor.loading": "Loading", + "workflows.editor.moved_to_trash": "Workflow moved to trash", + "workflows.editor.prompt_copied": "Prompt copied.", + "workflows.editor.run_in_warp": "Run in Warp", + "workflows.editor.title_placeholder": "Add a title", + "workflows.editor.unsaved_changes": "You have unsaved changes.", + "workflows.editor.update": "Update", + "workflows.enum.dynamic": "Dynamic", + "workflows.enum.dynamic_placeholder": "# Enter a shell command that generates variants, delimited by newlines.\n\ngit branch -a", + "workflows.enum.edit_title": "Edit enum", + "workflows.enum.name_placeholder": "Name", + "workflows.enum.new_title": "New enum", + "workflows.enum.static": "Static", + "workflows.enum.variant_placeholder": "Variant", + "workflows.enum.variants": "Variants", + "workflows.info.command_edited": "Command edited.", + "workflows.info.cycle_parameters": "to cycle parameters", + "workflows.info.edit_prompt": "Edit prompt", + "workflows.info.edit_workflow": "Edit workflow", + "workflows.info.save_as_workflow": "Save as workflow", + "workflows.info.view_context": "View Context", + "workflows.modal.argument_default_value_placeholder": "Default value (optional)", + "workflows.modal.argument_description_placeholder": "Description", + "workflows.modal.new_argument": "New argument", + "workflows.modal.save_workflow": "Save workflow", + "workflows.modal.title_placeholder": "Untitled workflow", + "workflows.restore_from_trash": "Restore workflow from trash", + "workspace.a11y.announcements_set": "{verbosity} accessibility announcements set", + "workspace.ask_ai_description": "Ask Warp AI to explain errors, suggest commands or write scripts.", + "workspace.banner.fix_with_oz": "Fix with Oz", + "workspace.banner.login_expired.description": "Please sign in again to restore access to cloud-based features.", + "workspace.banner.login_expired.heading": "Your login has expired.", + "workspace.banner.more_info": "More info", + "workspace.banner.open_file": "Open file", + "workspace.banner.out_of_date.description": "Your app is out of date and needs to update.", + "workspace.banner.restart_and_update": "Restart app and update now", + "workspace.banner.sign_in": "Sign in", + "workspace.banner.unable_to_launch.description": "Warp was unable to launch the new installed version.", + "workspace.banner.unable_to_update.description": "A new version is available but Warp is unable to perform the update.", + "workspace.banner.update_manually": "Update Warp manually", + "workspace.banner.update_now": "Update now", + "workspace.banner.version_deprecation": "Your app is out of date and some features may not work as expected. Please update immediately.", + "workspace.banner.version_deprecation_without_permissions": "Some Warp features may not work as expected without updating immediately, but Warp is unable to perform the update.", + "workspace.bonus_grant.reload_credits_added": "{credits} Reload Credits have been added to your {scope}.", + "workspace.bonus_grant.scope.account": "account", + "workspace.bonus_grant.scope.team": "team", + "workspace.build_plan_migration.auto_reload_description": "Auto-reload will automatically purchase credits at your selected rate when your account balance reaches 100 credits. Your monthly spend limit is set at your legacy plan's monthly cost and can be updated in Settings > Billing & usage.", + "workspace.build_plan_migration.auto_reload_failed": "Failed to enable auto-reload. Please try updating your settings in Billing & usage.", + "workspace.build_plan_migration.auto_reload_title": "Use auto-reload to never miss a beat.", + "workspace.build_plan_migration.get_started": "Get Started", + "workspace.build_plan_migration.saving": "Saving...", + "workspace.build_plan_migration.team_data_not_found": "Oops, something went wrong; your team data could not be found.", + "workspace.build_plan.and_more": "And more...", + "workspace.build_plan.base_credits_per_month": "{credits} base credits per month", + "workspace.build_plan.bring_your_own_api_key": "Bring your own API key", + "workspace.build_plan.features_header_build": "Build comes with:", + "workspace.build_plan.features_header_business": "The new Business plan comes with:", + "workspace.build_plan.intro_build": "Your workspace has been updated to the Warp Build Plan as the legacy Pro, Turbo, and Lightspeed plans are sunset.", + "workspace.build_plan.intro_business": "Your workspace has been updated to the new Warp Business Plan as the legacy Business plan is sunset.", + "workspace.build_plan.learn_more_prefix": "Learn more on our ", + "workspace.build_plan.price_per_user_month": "${price} per user per month", + "workspace.build_plan.price_per_user_month_annual": "${price} per user per month for annual plans", + "workspace.build_plan.pricing_header_build": "Warp Build is a primarily usage-based plan, starting at:", + "workspace.build_plan.pricing_header_business": "The new Business plan is a primarily usage-based plan, starting at:", + "workspace.build_plan.pricing_page": "pricing page", + "workspace.build_plan.reload_credits_discounts": "Access to Reload credits and volume-based discounts", + "workspace.build_plan.saml_sso": "SAML-based SSO", + "workspace.build_plan.welcome_build": "Welcome to Warp Build", + "workspace.build_plan.welcome_business": "Welcome to the New Business Plan", + "workspace.build_plan.zero_data_retention": "Automatically enforced team-wide Zero Data Retention", + "workspace.close_panel": "Close panel", + "workspace.cloud_capacity.ai_credits_per_month": "{credits} AI credits per month", + "workspace.cloud_capacity.business_plan_includes": "The Business plan includes everything on your current plan plus:", + "workspace.cloud_capacity.business_plan_starts": "The Business plan starts at ${price}/month and includes everything on your current plan plus:", + "workspace.cloud_capacity.concurrent_agents_multiplier": "{multiplier} the number of concurrent cloud agents", + "workspace.cloud_capacity.concurrent_limit.description": "This cloud run is queued because your team has reached the maximum number of concurrent cloud agents. It will start automatically when another cloud run finishes.", + "workspace.cloud_capacity.concurrent_limit.title": "Concurrent cloud agent limit reached", + "workspace.cloud_capacity.concurrent_limit.upgrade_suffix": " Upgrade your plan for more concurrent cloud agents.", + "workspace.cloud_capacity.extended_ai_credits": "Extended AI credits per month", + "workspace.cloud_capacity.open_billing": "Open billing", + "workspace.cloud_capacity.out_of_credits.description": "This cloud run stopped because your team has used all available AI credits for the current billing period.", + "workspace.cloud_capacity.out_of_credits.title": "You're out of AI credits", + "workspace.cloud_capacity.out_of_credits.upgrade_suffix": " Upgrade your plan to continue running cloud agents.", + "workspace.cloud_capacity.paid_plans_include": "Paid plans include everything in your free trial plus:", + "workspace.cloud_capacity.paid_plans_start": "Paid plans start at ${price}/month and include everything in your free trial plus:", + "workspace.cloud_capacity.upgrade_plan": "Upgrade plan", + "workspace.codex_modal.description_1": "Codex is OpenAI's most advanced agentic coding model for real-world engineering.", + "workspace.codex_modal.description_2": "Use Codex directly in Oz and leverage features like in-app code review, agent session sharing and file editing.", + "workspace.codex_modal.title": "Use Codex models in Warp", + "workspace.codex_modal.use_latest_model": "Use latest codex model", + "workspace.conversation.default_title": "Conversation", + "workspace.conversation.linear_issue_title": "Linear Issue", + "workspace.conversation.new_conversation": "New conversation", + "workspace.conversations.delete.in_progress_error": "Conversations cannot be deleted while in progress.", + "workspace.conversations.empty.subtitle": "Your active and past conversations with local and ambient agents will appear here.", + "workspace.conversations.empty.title": "No conversations yet", + "workspace.conversations.fallback_title": "Conversation", + "workspace.conversations.menu.delete": "Delete", + "workspace.conversations.menu.delete_disabled_tooltip": "This conversation cannot be deleted", + "workspace.conversations.menu.fork_new_pane": "Fork in new pane", + "workspace.conversations.menu.fork_new_tab": "Fork in new tab", + "workspace.conversations.menu.share": "Share conversation", + "workspace.conversations.new_conversation": "New conversation", + "workspace.conversations.no_matches": "No matching conversations", + "workspace.conversations.search_placeholder": "Search", + "workspace.conversations.section.active": "ACTIVE", + "workspace.conversations.section.past": "PAST", + "workspace.conversations.show_less": "Show less", + "workspace.conversations.view_all": "View all", + "workspace.crash_recovery.wayland.description": "We detected a crash during application startup, and adjusted your settings to use Xwayland for windowing. This can result in blurry text if you are using fractional scaling.", + "workspace.dialog.cancel_button": "Cancel", + "workspace.dialog.close_session_button": "Close session", + "workspace.dialog.close_session_shared_body": "You are about to close a session that is currently being shared. Closing it will end sharing for everyone.", + "workspace.dialog.close_session_title": "Close session?", + "workspace.dialog.delete_conversation_body": "This conversation will be permanently deleted. This action cannot be undone.", + "workspace.dialog.delete_conversation_title": "Delete conversation?", + "workspace.dialog.delete_conversation_title_named": "Delete '{title}'?", + "workspace.dialog.dont_show_again": "Don't show again.", + "workspace.dialog.rewind.body": "Are you sure you want to rewind? This will restore your code and conversation to before this point, and cancel any commands the agent is currently running. A copy of the original conversation will be saved in your conversation history.", + "workspace.dialog.rewind.button": "Rewind", + "workspace.dialog.rewind.cancel": "Cancel", + "workspace.dialog.rewind.info": "Rewinding does not affect files edited manually or via shell commands.", + "workspace.dialog.rewind.title": "Rewind", + "workspace.free_tier.access_to_prefix": "Access to ", + "workspace.free_tier.build_plan_includes": "The Build plan includes everything in the free tier plus:", + "workspace.free_tier.build_plan_price": "The Build plan is ${price}/month which includes everything in the free tier plus:", + "workspace.free_tier.credits_per_month": "{credits} Credits per month", + "workspace.free_tier.extended_cloud_agents_access": "Extended cloud agents access", + "workspace.free_tier.extended_credits": "Extended Credits per month", + "workspace.free_tier.frontier_models": "Access to frontier OpenAI, Anthropic, and Google models", + "workspace.free_tier.out_of_credits": "You’re out of credits", + "workspace.free_tier.reload_credits": "Reload Credits", + "workspace.free_tier.upgrade_plan": "Upgrade plan", + "workspace.free_tier.upgrade_to_continue": "To continue using AI, please upgrade your plan.", + "workspace.handoff.command_running": "Can't hand off while a command is running. Cancel the command or wait for it to finish.", + "workspace.handoff.create_environment_failed": "Failed to create environment: {error}", + "workspace.handoff.local_save_failed": "Couldn't save your conversation locally. Try sending another message, then hand off again.", + "workspace.handoff.moved_to_cloud_title": "{title} (Moved to cloud)", + "workspace.handoff.no_active_terminal": "No active terminal session to hand off. Focus a pane and try again.", + "workspace.handoff.not_available_for_orchestrated": "Cloud handoff isn't available for orchestrated agent conversations.", + "workspace.handoff.not_synced_yet": "Your conversation hasn't synced to the cloud yet. Try sending another message, then hand off again.", + "workspace.handoff.open_cloud_pane_failed": "Couldn't open a cloud pane for handoff. Try again, or restart Warp if this keeps happening.", + "workspace.handoff.start_failed": "Couldn't start the handoff. Check your network connection and try again.", + "workspace.hoa.agent_inbox_description": "Warp pipes through notifications from any CLI coding agent into a unified notification center that works across all coding agents and harnesses. ", + "workspace.hoa.agent_inbox_title": "Meet your new agent inbox", + "workspace.hoa.feature.agent_inbox.description": "Notifications when any agent needs your attention, also accessible in a central inbox", + "workspace.hoa.feature.agent_inbox.title": "Agent inbox", + "workspace.hoa.feature.native_code_review.description": "Send inline comments from Warp's code review directly to Claude Code, Codex, or OpenCode", + "workspace.hoa.feature.native_code_review.title": "Native code review", + "workspace.hoa.feature.tab_configs.description": "Tab-level schema to set your directory, startup commands, theme, and worktree with one click", + "workspace.hoa.feature.tab_configs.title": "Tab configs", + "workspace.hoa.feature.vertical_tabs.description": "Rich tab titles and metadata like git branch, worktree, and PR. Fully customizable.", + "workspace.hoa.feature.vertical_tabs.title": "Vertical tabs", + "workspace.hoa.see_whats_new": "See what’s new", + "workspace.hoa.switch_back_horizontal_tabs": "Switch back to horizontal tabs", + "workspace.hoa.tab_config_description": "Set up a reusable starting point for your tabs. Pick a repo, choose a session type, and optionally attach a worktree. Use it whenever you want to open a tab with this setup.", + "workspace.hoa.vertical_tabs_callout.description": "Vertical tabs show all open agent and terminal panes, grouped by tab. Customize what information you want to see to support your workflow.", + "workspace.hoa.vertical_tabs_callout.title": "Introducing vertical tabs - the new default", + "workspace.hoa.welcome_title": "Introducing universal agent support: level up any coding agent with Warp", + "workspace.home.content": "\nWelcome to Warp on Web - your browser-based home for Warp! \nUse Warp on Web to:\n* Join Shared Sessions\n* Create, View, and Edit Warp Drive Objects\n* Manage your Warp Settings\n\nWarp on Web can also be used by your teammates and peers who don't have Warp downloaded yet to view your shared sessions, notebooks, and workflows.", + "workspace.home.title": "Welcome to Warp on Web", + "workspace.launch_modal.skip_for_now": "Skip for now", + "workspace.launch_modal.sync_conversations_to_cloud": "Sync conversations to cloud", + "workspace.launch_modal.sync_conversations_to_cloud.description": "Agent conversations stored in the cloud can be shared with anyone with one click, and allow conversations to be continued across devices and on logout.", + "workspace.left_panel.agent_conversations": "Agent conversations", + "workspace.left_panel.global_search": "Global search", + "workspace.left_panel.project_explorer": "Project explorer", + "workspace.left_panel.warp_drive": "Warp Drive", + "workspace.menu.billing_and_usage": "Billing and usage", + "workspace.menu.current_version_prefix": "Current version is ", + "workspace.menu.documentation": "Documentation", + "workspace.menu.feedback": "Feedback", + "workspace.menu.install_update": "Install update", + "workspace.menu.invite_a_friend": "Invite a friend", + "workspace.menu.keyboard_shortcuts": "Keyboard shortcuts", + "workspace.menu.log_out": "Log out", + "workspace.menu.new_tab_config": "New tab config", + "workspace.menu.new_tab_group": "New tab group", + "workspace.menu.new_worktree_config": "New worktree config", + "workspace.menu.rearrange_toolbar_items": "Re-arrange toolbar items", + "workspace.menu.reopen_closed_session": "Reopen closed session", + "workspace.menu.settings": "Settings", + "workspace.menu.sign_up": "Sign up", + "workspace.menu.update_and_relaunch": "Update and relaunch Warp", + "workspace.menu.update_manually": "Update Warp manually", + "workspace.menu.updating_to": "Updating to", + "workspace.menu.upgrade": "Upgrade", + "workspace.menu.view_logs": "View Warp logs", + "workspace.menu.whats_new": "What's new", + "workspace.modal.new_api_key": "New API key", + "workspace.modal.open_tab_config": "Open: {name}", + "workspace.new_session.cloud_agent": "Cloud Agent", + "workspace.new_session.local_docker_sandbox": "Local Docker Sandbox", + "workspace.new_session.terminal": "Terminal", + "workspace.opencode.failed_create_config_dir": "Failed to create config directory: {error}", + "workspace.opencode.failed_home_dir": "Failed to determine home directory", + "workspace.opencode.failed_parse": "Failed to parse opencode.json: {error}", + "workspace.opencode.failed_read": "Failed to read opencode.json: {error}", + "workspace.opencode.failed_serialize": "Failed to serialize opencode.json: {error}", + "workspace.opencode.failed_write": "Failed to write opencode.json: {error}", + "workspace.opencode.plugin_set": "OpenCode plugin set to: {entry}", + "workspace.opencode.unexpected_structure": "opencode.json has unexpected structure (plugin is not an array)", + "workspace.openwarp_launch.description": "You, our community, can participate in building Warp using an agent-first workflow.", + "workspace.openwarp_launch.feature.auto_open_weights.description": "We've added a new auto model that picks the best open weight model for a task, like Kimi or MiniMax.", + "workspace.openwarp_launch.feature.auto_open_weights.title": "Introducing 'auto (open-weights)'", + "workspace.openwarp_launch.feature.contribute.description": "Warp's client code is now open source. Get started by using the /feedback skill to open an issue, and follow the contribution guidelines here.", + "workspace.openwarp_launch.feature.contribute.link_text": "here", + "workspace.openwarp_launch.feature.contribute.title": "Contribute", + "workspace.openwarp_launch.feature.open_automated_development.description": "The Warp repo is managed by an agent-first workflow powered by Oz, our cloud agent orchestration platform.", + "workspace.openwarp_launch.feature.open_automated_development.link_text": "Oz", + "workspace.openwarp_launch.feature.open_automated_development.title": "Open Automated Development", + "workspace.openwarp.title": "Warp is now open-source", + "workspace.openwarp.visit_repo": "Visit the repo", + "workspace.orchestration_launch.description": "We've made major improvements to Warp's cloud agent orchestration platform, Oz.", + "workspace.orchestration_launch.feature.agent_memory.badge": "Research preview", + "workspace.orchestration_launch.feature.agent_memory.description": "Agents will now store and access long-term memories, enabling self-improvement over time.", + "workspace.orchestration_launch.feature.agent_memory.title": "Agent Memory", + "workspace.orchestration_launch.feature.cloud_harness.description": "Use Oz to spin up Claude Code or Codex agents in the cloud; Oz will help you track and steer the agents.", + "workspace.orchestration_launch.feature.cloud_harness.title": "Run any agent harness in the cloud", + "workspace.orchestration_launch.feature.multi_agent.description": "Warp Agents will now orchestrate swarms of subagents, allowing you to parallelize tasks.", + "workspace.orchestration_launch.feature.multi_agent.title": "Multi-agent orchestration", + "workspace.orchestration_launch.title": "Orchestrate any agent, anywhere", + "workspace.oz_launch.agent_automations.content": "Oz agents can be defined using the standard Skills format. You can use the built in scheduler to setup agents to run autonomously at set intervals, or use the Oz SDK or API to programmatically start and manage Oz agents.", + "workspace.oz_launch.agent_automations.title": "Orchestrate agents, turning Skills into automations", + "workspace.oz_launch.agent_management.content": "View all of your agents across local and cloud sessions in the Warp app or at [oz.warp.dev](https://oz.warp.dev). Join live agent sessions, continue tasks locally, and steer agents with one click.", + "workspace.oz_launch.agent_management.title": "Track local and cloud agents seamlessly", + "workspace.oz_launch.cloud_agents.content": "Use cloud agents to run many agents in parallel, keep agents working when you close your laptop, or start agents programmatically. Plus, you can check on their work through the web.", + "workspace.oz_launch.cloud_agents.title": "Break out of your laptop with cloud agents", + "workspace.oz_launch.description": "Infinitely scalable coding agent — run in local sessions or in the cloud.", + "workspace.oz_launch.launch_credits.content": "Upgrade to Build this month and receive 1,000 extra credits to try using Oz. Credits are only eligible for Oz runs in Warp-hosted cloud environments.", + "workspace.oz_launch.launch_credits.title": "1,000 free cloud agent credits when you upgrade to Warp Build", + "workspace.oz_launch.modal_title": "Introducing Oz", + "workspace.oz_launch.next_button": "Next: {label}", + "workspace.oz_launch.slide.agent_automations": "Agent automations", + "workspace.oz_launch.slide.agent_management": "Agent management", + "workspace.oz_launch.slide.cloud_agents": "Cloud agents", + "workspace.oz_launch.slide.gift": "A little gift", + "workspace.oz_launch.slide.launch_credits": "Launch credits", + "workspace.oz_launch.try_it_out": "Try it out", + "workspace.pane_menu.rename_active_pane": "Rename active pane", + "workspace.pane_menu.rename_pane": "Rename pane", + "workspace.pane_menu.reset_active_pane_name": "Reset active pane name", + "workspace.pane_menu.reset_pane_name": "Reset pane name", + "workspace.pane.untitled": "Untitled pane", + "workspace.repos.add_new": "Add new repo", + "workspace.repos.search_placeholder": "Search repos", + "workspace.right_panel.close_panel.tooltip": "Close panel", + "workspace.right_panel.code_review.title": "Code review", + "workspace.right_panel.maximize.tooltip": "Maximize", + "workspace.right_panel.minimize.tooltip": "Minimize", + "workspace.right_panel.open_repository.label": "Open repository", + "workspace.right_panel.open_repository.tooltip": "Navigate to a repo and initialize it for coding", + "workspace.right_panel.repo.unknown": "Unknown", + "workspace.search.capped": "The result set only contains a subset of all matches. Be more specific in your search to narrow down results.", + "workspace.search.failed": "Global search failed.", + "workspace.search.label": "Search", + "workspace.search.no_results": "No results found. Review your gitignore files.", + "workspace.search.placeholder": "Search in files", + "workspace.search.results_count.multiple": "{n} results in {files} files", + "workspace.search.results_count.single": "1 result in {files} files", + "workspace.search.title_bar_placeholder": "Search sessions, agents, files...", + "workspace.search.toggle_case_sensitivity": "Toggle Case Sensitivity", + "workspace.search.toggle_regex": "Toggle Regex", + "workspace.search.unavailable.body": "Global search requires access to your local workspace. Open a new session or navigate to an active session to view.", + "workspace.search.unavailable.remote_body": "Global search requires access to your local workspace, which isn't supported in remote sessions", + "workspace.search.unavailable.title": "Global search unavailable", + "workspace.search.unavailable.unsupported_body": "Global search doesn't currently work in Git Bash or WSL.", + "workspace.search.zero_state.body": "Search in files across your current directories.", + "workspace.search.zero_state.title": "Global search", + "workspace.tab_config_chip.text": "Access your tab configs here.", + "workspace.tab_title.install_update": "Install Update", + "workspace.tab_title.introducing_oz": "Introducing Oz", + "workspace.tab_title.settings": "Settings", + "workspace.tabs.badge.unsaved": "Unsaved", + "workspace.tabs.detail.more_open_tabs_prefix": "and", + "workspace.tabs.detail.more_open_tabs_suffix": "more", + "workspace.tabs.empty.no_match": "No tabs match your search.", + "workspace.tabs.empty.no_tabs_open": "No tabs open", + "workspace.tabs.group.tab_count_one": "1 tab", + "workspace.tabs.group.tab_count_other": "tabs", + "workspace.tabs.group.untitled": "New Group", + "workspace.tabs.kind.code": "Code", + "workspace.tabs.kind.code_diff": "Code Diff", + "workspace.tabs.kind.env_var_collection": "Environment Variables", + "workspace.tabs.kind.environments": "Environments", + "workspace.tabs.kind.execution_profile": "Execution Profile", + "workspace.tabs.kind.file": "File", + "workspace.tabs.kind.notebook": "Notebook", + "workspace.tabs.kind.other": "Other", + "workspace.tabs.kind.plan": "Plan", + "workspace.tabs.kind.rules": "Rules", + "workspace.tabs.kind.settings": "Settings", + "workspace.tabs.kind.terminal": "Terminal", + "workspace.tabs.kind.workflow": "Workflow", + "workspace.tabs.search_placeholder": "Search tabs...", + "workspace.tabs.settings.additional_metadata": "Additional metadata", + "workspace.tabs.settings.density": "Density", + "workspace.tabs.settings.granularity.panes": "Panes", + "workspace.tabs.settings.granularity.tabs": "Tabs", + "workspace.tabs.settings.info.branch": "Branch", + "workspace.tabs.settings.info.command": "Command / Conversation", + "workspace.tabs.settings.info.working_directory": "Working Directory", + "workspace.tabs.settings.pane_title_as": "Pane title as", + "workspace.tabs.settings.pr_link.requires_gh_cli": "Requires the GitHub CLI to be installed and authenticated", + "workspace.tabs.settings.show": "Show", + "workspace.tabs.settings.show.details_on_hover": "Show details on hover", + "workspace.tabs.settings.show.diff_stats": "Diff stats", + "workspace.tabs.settings.show.pr_link": "PR link", + "workspace.tabs.settings.tab_item": "Tab item", + "workspace.tabs.settings.tab_item.focused_session": "Focused session", + "workspace.tabs.settings.tab_item.summary": "Summary", + "workspace.tabs.settings.view_as": "View as", + "workspace.tabs.summary.overflow_more_suffix": "more", + "workspace.tabs.terminal.new_session": "New session", + "workspace.tabs.tooltip.tab_configs": "Tab configs", + "workspace.tabs.tooltip.view_options": "View options", + "workspace.tabs.untitled_tab": "Untitled tab", + "workspace.toast.cannot_open_terminal": "Cannot open a new terminal session", + "workspace.toast.checkout_latest_retry": "Check out the latest version and try again.", + "workspace.toast.cli_installed_prefix": "Successfully installed the Oz CLI! You can now run '", + "workspace.toast.cli_installed_suffix": "' from the command line.", + "workspace.toast.command_still_running": "A command in this session is still running.", + "workspace.toast.conversation_deleted": "Conversation deleted", + "workspace.toast.conversation_fork_failed": "Conversation forking failed.", + "workspace.toast.create_log_bundle_failed": "Failed to create log bundle: {error}", + "workspace.toast.delete_conversation_failed": "Failed to delete conversation. Please exit the agent view and try again.", + "workspace.toast.disabled_synced_inputs": "Disabled all synchronized inputs.", + "workspace.toast.failed_to_load_conversation_for_forking": "Failed to load conversation for forking.", + "workspace.toast.forked_prefix": "Forked ", + "workspace.toast.learn_more": "Learn more", + "workspace.toast.load_conversation_failed": "Failed to load conversation.", + "workspace.toast.mouse_reporting_disabled": "You disabled mouse reporting.", + "workspace.toast.mouse_reporting_enabled": "You enabled mouse reporting.", + "workspace.toast.no_notification_permission": "Warp doesn't have permission to send desktop notifications.", + "workspace.toast.no_terminal_pane": "No terminal pane open. Open a new pane to attach as context.", + "workspace.toast.out_of_credits": "Looks like you're out of AI credits.", + "workspace.toast.oz_command_install_failed": "Failed to install Oz command: {error}", + "workspace.toast.oz_command_uninstall_failed": "Failed to uninstall Oz command: {error}", + "workspace.toast.oz_command_uninstalled": "Successfully uninstalled the Oz command.", + "workspace.toast.plan_already_in_context": "This plan is already in context.", + "workspace.toast.plan_synced": "Plan synced to your Warp Drive", + "workspace.toast.press_to_undo": " Press {key} to undo.", + "workspace.toast.process_sample_failed": "Failed to sample process (check logs)", + "workspace.toast.process_sample_saved": "Process sample saved to {path}", + "workspace.toast.remote_control_link_copied": "Remote control link copied.", + "workspace.toast.remove_tab_config_failed": "Failed to remove tab config: {error}", + "workspace.toast.resource_not_found": "Resource not found or access denied", + "workspace.toast.sampling_process": "Sampling process for 3 seconds...", + "workspace.toast.staging_api_call_failed": "Staging API call failed. Did your IP address change?", + "workspace.toast.starting_cloud_environment": "Starting cloud environment for this session...", + "workspace.toast.synced_inputs_all_tabs_disabled": "You disabled synchronized inputs in all tabs.", + "workspace.toast.synced_inputs_all_tabs_enabled": "You enabled synchronized inputs in all tabs.", + "workspace.toast.synced_inputs_tab_disabled": "You disabled synchronized inputs in this tab.", + "workspace.toast.synced_inputs_tab_enabled": "You enabled synchronized inputs in this tab.", + "workspace.toast.troubleshoot_notifications": "Troubleshoot notifications", + "workspace.toast.upgrade_for_credits": "Upgrade for more credits.", + "workspace.toast.view": "View", + "workspace.toast.view_changelog": "View changelog", + "workspace.toast.warp_updated": "Warp updated!", + "workspace.toast.workflow_unavailable": "This workflow is no longer available.", + "workspace.toolbar.available_items": "Available items", + "workspace.toolbar.item.agent_management": "Agent Management", + "workspace.toolbar.item.code_review": "Code Review", + "workspace.toolbar.item.notifications": "Notifications", + "workspace.toolbar.item.tabs_panel": "Tabs Panel", + "workspace.toolbar.item.tools_panel": "Tools Panel", + "workspace.toolbar.modal_title": "Edit toolbar", + "workspace.tooltip.agent_conversations": "Agent conversations", + "workspace.tooltip.agent_management_panel": "Agent management panel", + "workspace.tooltip.code_review_panel": "Code review panel", + "workspace.tooltip.global_search": "Global search", + "workspace.tooltip.new_tab": "New Tab", + "workspace.tooltip.notifications": "Notifications", + "workspace.tooltip.offline": "Some features may be unavailable offline", + "workspace.tooltip.project_explorer": "Project explorer", + "workspace.tooltip.settings": "Settings", + "workspace.tooltip.tab_configs": "Tab configs", + "workspace.tooltip.tabs_panel": "Tabs panel", + "workspace.tooltip.tools_panel": "Tools panel", + "workspace.tooltip.warp_drive": "Warp Drive", + "workspace.tooltip.warp_essentials": "Warp Essentials", + "workspace.update.update_warp": "Update Warp", + "workspace.user.default_display_name": "User", + "workspace.wasm.local_network_access_required": "Have Warp installed but redirecting to download page?\nEnable Local Network Access for {server} in your browser.", + "workspace.wasm.view_all_cloud_runs": "View all cloud runs", + "workspace.workflow.command_from_oz": "Command from Oz", + "workspace.workflow.command_from_warp_ai": "Command from Warp AI", + "workspace.worktree.config_name": "Worktree: {repo}", + "workspace.worktree.new": "New worktree: {repo}", + "workspace.worktree.new_with_branch": "New worktree: {repo}, {branch}", + "workspace.worktree.new_with_name": "New worktree: {repo}, {name}" +} diff --git a/crates/i18n/locales/zh-CN.json b/crates/i18n/locales/zh-CN.json new file mode 100644 index 0000000000..8758042c94 --- /dev/null +++ b/crates/i18n/locales/zh-CN.json @@ -0,0 +1,6707 @@ +{ + "agent_input_footer.attach_file": "附加文件", + "agent_input_footer.auto_approve_locked": "云端 Agent 对话始终启用快进", + "agent_input_footer.auto_approve_off": "自动批准此任务中的所有 Agent 操作", + "agent_input_footer.auto_approve_on": "关闭自动批准所有 Agent 操作", + "agent_input_footer.choose_environment": "选择环境", + "agent_input_footer.context_remaining": "上下文剩余 {percent}%", + "agent_input_footer.context_window_usage": "上下文窗口用量", + "agent_input_footer.disable_command_autodetection": "关闭终端命令自动检测", + "agent_input_footer.editor.available_chips": "可用 chip", + "agent_input_footer.editor.edit_agent_toolbelt": "编辑 Agent 工具栏", + "agent_input_footer.editor.edit_cli_agent_toolbelt": "编辑 CLI Agent 工具栏", + "agent_input_footer.enable_command_autodetection": "启用终端命令自动检测", + "agent_input_footer.file_explorer": "文件浏览器", + "agent_input_footer.full_terminal_agent_default_model": "现在使用 Full Terminal Agent 的默认模型。", + "agent_input_footer.hand_off_to_cloud": "移交到云端(或输入 &)", + "agent_input_footer.hide_rich_input": "隐藏富输入", + "agent_input_footer.new_environment": "新建环境", + "agent_input_footer.open_coding_agent_settings": "打开编码 Agent 设置", + "agent_input_footer.open_file_explorer": "打开文件浏览器", + "agent_input_footer.open_rich_input": "打开富输入", + "agent_input_footer.plugin_auto_install_failed": "无法自动安装插件。请再次点击 chip 查看手动安装步骤。", + "agent_input_footer.remote_control_login_required": "登录后才能使用 /remote-control", + "agent_input_footer.rich_input": "富输入", + "agent_input_footer.start_remote_control": "启动远程控制", + "agent_input_footer.stop_sharing": "停止共享", + "agent_input_footer.voice_first_time_enabled": "语音输入已启用。你也可以按住 `{key}` 键来启动语音输入(可在设置 > AI > 语音中配置)", + "agent_input_footer.voice_input": "语音输入", + "agent_input_footer.voice_limit_reached": "已达到语音输入上限", + "agent_input_footer.voice_microphone_access_failed": "无法启动语音输入(你可能需要启用麦克风访问权限)", + "agent_input_footer.voice_transcribe_failed": "语音输入转写失败", + "agent_management.agent_type.choose_agent": "选择 Agent", + "agent_management.agent_type.cloud_agent": "云端 Agent", + "agent_management.agent_type.cloud_agent.desc": "在你选择的云环境中自主运行。适合并行任务或长时间运行的工作。", + "agent_management.agent_type.local_agent": "本地 Agent", + "agent_management.agent_type.local_agent.desc": "在你的机器上运行,需要你监督。适合快速、交互式任务。", + "agent_management.agent_type.suggested": "建议", + "agent_management.artifact.file": "文件", + "agent_management.artifact.plan": "计划", + "agent_management.artifact.pull_request": "Pull Request", + "agent_management.artifact.screenshot": "截图", + "agent_management.clear_all": "全部清除", + "agent_management.clear_filters": "清除筛选", + "agent_management.cloud_setup.docs_link": "Oz 文档", + "agent_management.cloud_setup.docs_prefix": "查看 ", + "agent_management.cloud_setup.docs_suffix": " 了解更多。", + "agent_management.cloud_setup.manual_setup": "手动设置:使用 Oz CLI 创建 Slack 或 Linear 集成", + "agent_management.cloud_setup.quick_start": "快速开始:访问 oz.warp.dev,使用图形界面完成设置。", + "agent_management.cloud_setup.step.create_environment": "创建环境", + "agent_management.cloud_setup.step.create_environment.desc": "首先,设置一个环境以创建集成。", + "agent_management.cloud_setup.step.create_environment.docs_prefix": "使用 Warp 的环境设置命令,让 Agent 引导你完成。", + "agent_management.cloud_setup.step.create_environment.or_text": "或者,提供你已有的 Docker 镜像。", + "agent_management.cloud_setup.step.create_integration": "创建集成", + "agent_management.cloud_setup.step.create_integration.docs_prefix": "集成 Slack 或 Linear,即可通过 @Warp 分配 Warp Agent 任务。", + "agent_management.cloud_setup.subtitle": "直接在 Warp 中从集成(Linear、Slack)、事件(GitHub、内置计划任务)启动 Oz 云端 Agent,或通过 Oz SDK/CLI 以编程方式启动。", + "agent_management.cloud_setup.title": "开始使用 Oz 云端 Agent", + "agent_management.cloud_setup.visit_docs": "访问文档", + "agent_management.cloud_setup.visit_oz": "访问 Oz", + "agent_management.cloud_setup.workflow.arg.docker_image": "环境使用的 Docker 镜像", + "agent_management.cloud_setup.workflow.arg.environment_id": "要集成的环境 ID", + "agent_management.cloud_setup.workflow.arg.environment_name": "环境名称", + "agent_management.cloud_setup.workflow.arg.github_link_or_path": "仓库的 GitHub 链接或本地文件路径", + "agent_management.cloud_setup.workflow.create_environment": "创建环境", + "agent_management.cloud_setup.workflow.create_environment_cli": "创建环境(CLI)", + "agent_management.cloud_setup.workflow.create_linear_integration": "创建 Linear 集成", + "agent_management.cloud_setup.workflow.create_slack_integration": "创建 Slack 集成", + "agent_management.created_on.last_24_hours": "过去 24 小时", + "agent_management.created_on.last_week": "过去一周", + "agent_management.created_on.past_3_days": "过去 3 天", + "agent_management.details.cancel_task": "取消任务", + "agent_management.details.copy_link_to_run": "复制运行链接", + "agent_management.details.fork_conversation": "Fork 对话", + "agent_management.details.open_conversation": "打开对话", + "agent_management.details.view_details": "查看详情", + "agent_management.filter.all": "全部", + "agent_management.filter.created_by": "创建者", + "agent_management.filter.created_on": "创建时间", + "agent_management.filter.environment": "环境", + "agent_management.filter.harness": "Harness", + "agent_management.filter.has_artifact": "包含产物", + "agent_management.filter.none": "无", + "agent_management.filter.source": "来源", + "agent_management.filter.status": "状态", + "agent_management.get_started": "开始使用", + "agent_management.loading_agents": "正在加载 Agent...", + "agent_management.loading_cloud_runs": "正在加载云端 Agent 运行记录", + "agent_management.metadata.agent": "Agent", + "agent_management.metadata.credits_used": "已用额度", + "agent_management.metadata.executor": "执行者", + "agent_management.metadata.harness": "Harness", + "agent_management.metadata.run_time": "运行时长", + "agent_management.metadata.source": "来源", + "agent_management.new_agent": "新建 Agent", + "agent_management.no_filter_results": "没有符合筛选条件的结果", + "agent_management.notifications.agent_completed_suffix": " 已完成", + "agent_management.notifications.agent_task": "Agent 任务", + "agent_management.notifications.close": "关闭", + "agent_management.notifications.empty": "暂无通知", + "agent_management.notifications.error": "出了点问题。", + "agent_management.notifications.filter_with_count": "{label}({count})", + "agent_management.notifications.filter.all_tabs": "所有标签页", + "agent_management.notifications.filter.errors": "错误", + "agent_management.notifications.filter.unread": "未读", + "agent_management.notifications.from_codex": "来自 Codex 的通知", + "agent_management.notifications.mark_all_as_read": "全部标记为已读", + "agent_management.notifications.needs_attention_suffix": " 需要关注", + "agent_management.notifications.open_conversation": "打开对话", + "agent_management.notifications.task_cancelled": "任务已取消。", + "agent_management.notifications.task_completed": "任务已完成。", + "agent_management.notifications.title": "通知", + "agent_management.notifications.waiting_for_input": "正在等待输入。", + "agent_management.owner_filter.all": "全部", + "agent_management.owner_filter.all.tooltip": "查看你的 Agent 任务以及所有团队共享任务", + "agent_management.owner_filter.personal": "个人", + "agent_management.owner_filter.personal.tooltip": "查看你创建的 Agent 任务", + "agent_management.runs_title": "运行记录", + "agent_management.search_placeholder": "搜索", + "agent_management.session.expired": "会话已过期", + "agent_management.session.expired_tooltip": "会话会在一周后过期,且无法再打开。", + "agent_management.session.unavailable": "没有可用会话", + "agent_management.source.github_action": "GitHub Action", + "agent_management.source.oz_web": "Oz Web", + "agent_management.source.scheduled": "计划任务", + "agent_management.source.warp_app": "Warp App", + "agent_management.status.done": "已完成", + "agent_management.status.failed": "失败", + "agent_management.status.working": "进行中", + "agent_management.toast.copied_branch_name": "已复制分支名称", + "agent_management.unknown": "未知", + "agent_management.view_agents": "查看 Agent", + "ai.agent.figma.enable_mcp": "启用 Figma MCP", + "ai.agent.figma.enabling": "正在启用…", + "ai.agent.figma.get_mcp": "获取 Figma MCP", + "ai.agent_mode.attach_as_context": "附加为智能体上下文", + "ai.artifact.button.copy_branch_name": "复制分支名", + "ai.artifact.button.open_pull_request": "打开拉取请求", + "ai.model.disable_reason.admin_disabled": "此模型已被你的团队管理员禁用。", + "ai.model.disable_reason.out_of_requests": "请升级你的套餐以发起更多请求。", + "ai.model.disable_reason.provider_outage": "由于服务提供商故障,此模型暂时不可用。", + "ai.model.disable_reason.requires_upgrade": "请升级你的套餐以使用此模型。", + "ai.model.disable_reason.unavailable": "此模型不可用。", + "ai_assistant.accuracy_notice": "AI 回复可能不准确。", + "ai_assistant.ask_warp_ai": "询问 Warp AI", + "ai_assistant.character_limit_exceeded": "已超出字符限制。", + "ai_assistant.copy_transcript_tooltip": "复制会话记录到剪贴板", + "ai_assistant.credits.until_refresh": "{time} 后刷新。", + "ai_assistant.credits.used": "已用积分:{used} / {limit}。", + "ai_assistant.generating_answer": "正在生成回答...", + "ai_assistant.missing_context_notice": "随着对话变长,Warp AI 可能会忘记较早的回答。", + "ai_assistant.placeholder.ask_question": "提问...", + "ai_assistant.placeholder.followup": "输入回复或点击上方选项...", + "ai_assistant.prompt.connect_aws_ec2": "编写一个连接到 AWS EC2 实例的脚本。", + "ai_assistant.prompt.find_files_containing_text": "如何查找所有包含特定文本的文件?", + "ai_assistant.prompt.how_do_i_fix_this": "如何修复这个问题?", + "ai_assistant.prompt.show_examples": "展示示例。", + "ai_assistant.prompt.undo_recent_commits": "如何撤销 git 中最近的提交?", + "ai_assistant.prompt.what_to_do_next": "接下来我该怎么做?", + "ai_assistant.requests.duration.day": "1 天", + "ai_assistant.requests.duration.days": "{count} 天", + "ai_assistant.requests.duration.hour": "1 小时", + "ai_assistant.requests.duration.hours": "{count} 小时", + "ai_assistant.requests.duration.minute": "1 分钟", + "ai_assistant.requests.duration.minutes": "{count} 分钟", + "ai_assistant.requests.out_of_credits": "你的积分似乎已用完。请{next_time}重试。", + "ai_assistant.requests.out_of_credits_contact_admin": "你的积分似乎已用完。请{next_time}重试。\n\n请联系团队管理员升级以获得更多积分。", + "ai_assistant.requests.out_of_credits_upgrade": "你的积分似乎已用完。请{next_time}重试。\n\n[升级]({upgrade_url})以获得更多积分。", + "ai_assistant.requests.retry_after": "{time}后", + "ai_assistant.requests.retry_later": "稍后", + "ai_assistant.transcript.answer": "Warp AI:{text}\n\n", + "ai_assistant.transcript.copy_answer_tooltip": "复制回答到剪贴板", + "ai_assistant.transcript.copy_code_tooltip": "复制代码到剪贴板 [Cmd + C]", + "ai_assistant.transcript.insert_code_tooltip": "插入代码到终端输入框 [Cmd + Enter]", + "ai_assistant.transcript.prompt": "提示词:{text}\n\n", + "ai_assistant.transcript.save_as_workflow_tooltip": "另存为工作流 [Cmd + S]", + "ai_assistant.transcript.title": "## Warp AI 会话记录({time})\n\n", + "ai_assistant.zero_state_help": "按 Shift + Ctrl + Space 可对块或选中的文本询问 Warp AI。", + "ai_document.attach_to_active_session": "附加到活动会话", + "ai_document.auto_save_synced_tooltip": "此计划已同步到你的 Warp Drive,并会自动保存你进行的任何编辑。", + "ai_document.copy_plan_id": "复制计划 ID", + "ai_document.default_planning_document_title": "计划文档", + "ai_document.save_and_auto_sync_tooltip": "保存并自动同步此计划到 Warp Drive", + "ai_document.save_as_markdown_file": "另存为 Markdown 文件", + "ai_document.show_in_warp_drive": "在 Warp Drive 中显示", + "ai_document.show_version_history": "显示版本历史", + "ai_document.toast.link_copied": "链接已复制到剪贴板", + "ai_document.toast.plan_id_copied": "计划 ID 已复制到剪贴板", + "ai_document.update_agent": "更新 Agent", + "ai_document.update_agent_tooltip": "此计划有 Agent 尚未知晓的更改。按 {save_action} 可停止 Agent 当前任务并发送更新后的计划", + "ai_document.version_restored_from": "{version}(从 {from_version} 恢复)", + "ai.agent_conversations.task_blocked": "任务已阻塞", + "ai.agent_sdk.admin.already_logged_in": "你已登录。", + "ai.agent_sdk.admin.already_logged_in_as_name": "你已以 {name} 身份登录。", + "ai.agent_sdk.admin.already_logged_in_as_username_email": "你已以 {username}({email})身份登录。", + "ai.agent_sdk.admin.authentication_failed": "认证失败", + "ai.agent_sdk.admin.authentication_failed_with_error": "认证失败:{error}", + "ai.agent_sdk.admin.display_name": "显示名称:{name}", + "ai.agent_sdk.admin.email": "邮箱:{email}", + "ai.agent_sdk.admin.logged_in_successfully": "登录成功", + "ai.agent_sdk.admin.logged_out_successfully": "已成功登出。", + "ai.agent_sdk.admin.login_open_url": "要登录,请在浏览器中打开此 URL:\n{url}", + "ai.agent_sdk.admin.login_visit_and_enter_code": "要登录,请访问 {url} 并输入此代码:{code}", + "ai.agent_sdk.admin.not_logged_in": "你尚未登录。", + "ai.agent_sdk.admin.service_account_id": "服务账号 ID:{uid}", + "ai.agent_sdk.admin.team_id": "团队 ID:{team_uid}", + "ai.agent_sdk.admin.team_name": "团队名称:{team_name}", + "ai.agent_sdk.admin.user_id": "用户 ID:{uid}", + "ai.agent_sdk.admin.user_id_missing": "无法确定用户 ID。你是否已登录?", + "ai.agent_sdk.admin.whoami_ndjson_unsupported": "`whoami` 不支持 `--output-format ndjson`", + "ai.agent_sdk.agent_config.authorize_access_here": "在这里授权访问:{url}", + "ai.agent_sdk.agent_config.cannot_access_private_repo": "无法访问私有仓库 {owner}/{repo}", + "ai.agent_sdk.agent_config.check_github_auth_status_failed": "检查 GitHub 授权状态失败", + "ai.agent_sdk.agent_config.fetching_from_environments": "正在从你的 Warp 环境获取 Agent skills...", + "ai.agent_sdk.agent_config.fetching_from_repository": "正在从指定仓库获取 Agent skills...", + "ai.agent_sdk.agent_config.field.environments": "环境:{environments}", + "ai.agent_sdk.agent_config.field.id": "ID:{id}", + "ai.agent_sdk.agent_config.field.source": "来源:{owner}/{name}", + "ai.agent_sdk.agent_config.github_authorization_expired": "GitHub 授权已过期。请重试。", + "ai.agent_sdk.agent_config.github_authorization_failed": "GitHub 授权失败。请重试。", + "ai.agent_sdk.agent_config.heading.agent": "Agent:", + "ai.agent_sdk.agent_config.heading.agents": "Agent({count}):", + "ai.agent_sdk.agent_config.invalid_repo_format": "仓库格式无效:'{repo}'。预期为 'owner/repo' 或 'https://github.com/owner/repo'", + "ai.agent_sdk.agent_config.label.base_prompt": "基础提示词", + "ai.agent_sdk.agent_config.label.description": "描述", + "ai.agent_sdk.agent_config.max_authorization_attempts_exceeded": "已超过最大授权尝试次数({count})。请稍后重试。", + "ai.agent_sdk.agent_config.no_auth_flow_provided": "无法列出 Agent:需要授权,但未提供授权流程", + "ai.agent_sdk.agent_config.no_skills_found": "未找到 skill。", + "ai.agent_sdk.agent_config.opening_github_authorization": "正在打开浏览器进行 GitHub 授权:{url}", + "ai.agent_sdk.agent_config.poll_oauth_status_error": "轮询 OAuth 状态出错:{error}", + "ai.agent_sdk.agent_config.private_repo_authorization_required": "访问私有仓库需要授权。", + "ai.agent_sdk.agent_config.rerun_after_authorizing": "授权后,请重新运行此命令。", + "ai.agent_sdk.agent_config.unexpected_oauth_status": "意外的 OAuth 状态", + "ai.agent_sdk.agent_config.user_not_connected_to_github": "用户未连接到 GitHub", + "ai.agent_sdk.agent_management.deleted": "已删除 Agent {uid}。", + "ai.agent_sdk.agent_management.disabled_agents_hidden": "已隐藏 {count} 个被禁用的 Agent", + "ai.agent_sdk.agent_management.json_sort_unsupported": "--sort-by 和 --sort-order 不支持与 JSON 输出一起使用", + "ai.agent_sdk.agent_management.no_agents_found": "未找到 Agent。", + "ai.agent_sdk.agent_management.no_updates_requested": "未请求任何更新", + "ai.agent_sdk.agent_management.skills_hint": "在找你的 Agent skills?请改用 `{binary_name} agent skills`。", + "ai.agent_sdk.agent_management.table.base_model": "基础模型", + "ai.agent_sdk.agent_management.table.created": "创建时间", + "ai.agent_sdk.agent_management.table.description": "描述", + "ai.agent_sdk.agent_management.table.environment": "环境", + "ai.agent_sdk.agent_management.table.name": "名称", + "ai.agent_sdk.agent_management.table.secrets": "Secrets", + "ai.agent_sdk.agent_management.table.skills": "Skills", + "ai.agent_sdk.agent_management.table.uid": "UID", + "ai.agent_sdk.ambient.agent_state": "Agent 状态:{state}", + "ai.agent_sdk.ambient.artifact.branch": "分支:{branch}", + "ai.agent_sdk.ambient.artifact.description": "描述:{description}", + "ai.agent_sdk.ambient.artifact.file": "文件:{label}", + "ai.agent_sdk.ambient.artifact.link": "链接:{url}", + "ai.agent_sdk.ambient.artifact.no_description": "无描述", + "ai.agent_sdk.ambient.artifact.path": "路径:{path}", + "ai.agent_sdk.ambient.artifact.plan": "计划:{title}", + "ai.agent_sdk.ambient.artifact.pr": "PR:", + "ai.agent_sdk.ambient.artifact.pr_with_repo": "PR:{repo} #{number}", + "ai.agent_sdk.ambient.artifact.screenshot": "截图:{uid}({description})", + "ai.agent_sdk.ambient.artifact.untitled_plan": "未命名计划", + "ai.agent_sdk.ambient.artifacts": "产物:", + "ai.agent_sdk.ambient.attachment_upload_not_enabled": "附件上传未启用", + "ai.agent_sdk.ambient.concurrent_limit_reached": "已达到并发云端 Agent 限制。此 Agent 运行会在你当前某个云端运行完成后开始。", + "ai.agent_sdk.ambient.config": "配置:\n{config}", + "ai.agent_sdk.ambient.created": "创建时间:{time}", + "ai.agent_sdk.ambient.env_not_unicode": "{env} 已设置,但不是有效的 Unicode", + "ai.agent_sdk.ambient.error": "错误:{message}", + "ai.agent_sdk.ambient.executed_as": "执行身份:{executor}", + "ai.agent_sdk.ambient.failed_no_error_message": "运行失败,但没有错误消息", + "ai.agent_sdk.ambient.flush_stdout_failed": "无法刷新 stdout", + "ai.agent_sdk.ambient.heading.run": "Agent 运行:", + "ai.agent_sdk.ambient.heading.runs": "Agent 运行({count}):", + "ai.agent_sdk.ambient.invalid_oz_run_id": "无效的 OZ_RUN_ID", + "ai.agent_sdk.ambient.label.status": "状态", + "ai.agent_sdk.ambient.label.title": "标题", + "ai.agent_sdk.ambient.message.body": "正文:", + "ai.agent_sdk.ambient.message.delivered_at": "送达时间:{delivered_at}", + "ai.agent_sdk.ambient.message.from": "来自:{from}", + "ai.agent_sdk.ambient.message.ids": "消息 ID:", + "ai.agent_sdk.ambient.message.marked_delivered": "已标记消息送达:{id}", + "ai.agent_sdk.ambient.message.message_id": "消息 ID:{id}", + "ai.agent_sdk.ambient.message.read_at": "已读时间:{read_at}", + "ai.agent_sdk.ambient.message.send_failed_context": "发送 Agent 消息失败(sender_run_id={sender_run_id}, task_id={task_id}, target_agent_ids={target_agent_ids})", + "ai.agent_sdk.ambient.message.sent_at": "发送时间:{sent_at}", + "ai.agent_sdk.ambient.message.sent_count": "已发送 {count} 条消息。", + "ai.agent_sdk.ambient.message.subject": "主题:{subject}", + "ai.agent_sdk.ambient.message.table.delivered_at": "送达时间", + "ai.agent_sdk.ambient.message.table.from": "来自", + "ai.agent_sdk.ambient.message.table.message_id": "消息 ID", + "ai.agent_sdk.ambient.message.table.read_at": "已读时间", + "ai.agent_sdk.ambient.message.table.sent_at": "发送时间", + "ai.agent_sdk.ambient.message.table.subject": "主题", + "ai.agent_sdk.ambient.message.watch.closed": "消息监听流已关闭。将在 {seconds} 秒后重新连接。", + "ai.agent_sdk.ambient.message.watch.disconnected": "消息监听已断开:{error}。将在 {seconds} 秒后重试。", + "ai.agent_sdk.ambient.message.watch.hydrate_failed": "读取消息 {message_id} 详情失败:{error}。将在 {seconds} 秒后重试。", + "ai.agent_sdk.ambient.message.watch.malformed_event_payload": "跳过格式错误的 Agent 事件 payload:{error}", + "ai.agent_sdk.ambient.message.watch.missing_ref_id": "跳过 sequence {sequence} 中缺少 ref_id 的 new_message 事件。", + "ai.agent_sdk.ambient.message.watch.open_failed": "打开 Agent 事件流失败", + "ai.agent_sdk.ambient.message.watch.reconnect_failed": "消息监听重新连接失败:{error}。将在 {seconds} 秒后重试。", + "ai.agent_sdk.ambient.message.watch.reconnected": "已重新连接 run {run_id} 的消息监听,当前 sequence 为 {sequence}。", + "ai.agent_sdk.ambient.missing_prompt_source": "必须提供 --prompt、--skill 或 --conversation 之一", + "ai.agent_sdk.ambient.no_runs_found": "未找到运行记录。", + "ai.agent_sdk.ambient.saved_prompt_not_found": "未找到 ID 为 '{id}' 的已保存 prompt", + "ai.agent_sdk.ambient.saved_prompt_not_prompt": "'{id}' 不是已保存 prompt", + "ai.agent_sdk.ambient.saved_prompt_parse_failed": "解析已保存 prompt ID '{id}' 失败:{error}", + "ai.agent_sdk.ambient.session": "会话:{url}", + "ai.agent_sdk.ambient.session_not_ready": "运行 ID 为 {id} 的 Agent 会话在 {seconds} 秒后仍未就绪。请在 Ambient Agent 管理面板中查找共享链接。详情请见 https://docs.warp.dev/agent-platform/cloud-agents/managing-cloud-agents。", + "ai.agent_sdk.ambient.spawned": "已启动 Ambient Agent,运行 ID:{id}", + "ai.agent_sdk.ambient.streaming_requires_ndjson": "流式命令需要 `--output-format ndjson`", + "ai.agent_sdk.ambient.too_many_attachments": "附件数量过多。最多允许 {max} 个附件,但当前提供了 {count} 个。", + "ai.agent_sdk.ambient.unexpected_skill_arg": "发现意外参数 '--skill'", + "ai.agent_sdk.ambient.unknown": "未知", + "ai.agent_sdk.ambient.unsupported_feature": "不支持的功能", + "ai.agent_sdk.ambient.upgrade_plan": "如需提高并发 Agent 限制,请升级套餐:{url}", + "ai.agent_sdk.ambient.view_run": "查看运行:{url}", + "ai.agent_sdk.ambient.view_session": "查看 Agent 会话:{url}", + "ai.agent_sdk.ambient.without_environment": "Agent 将在不使用环境的情况下运行。", + "ai.agent_sdk.api_key.create_failed": "创建 API key 失败", + "ai.agent_sdk.api_key.created": "API key '{name}' 已创建。", + "ai.agent_sdk.api_key.display": "{name}({uid},创建于 {created_at})", + "ai.agent_sdk.api_key.expiration_behavior_required": "必须指定过期行为", + "ai.agent_sdk.api_key.expiration_cancelled": "已取消过期操作", + "ai.agent_sdk.api_key.expiration_too_large": "过期时长过大", + "ai.agent_sdk.api_key.expire_confirm": "要使 API key '{key}' 过期吗?", + "ai.agent_sdk.api_key.expire_confirm_help": "此操作会立即生效", + "ai.agent_sdk.api_key.expire_failed": "使 API key 过期失败", + "ai.agent_sdk.api_key.expire_refusing_noninteractive": "非交互模式下拒绝在未经确认的情况下使 API key 过期(使用 --force 可绕过)", + "ai.agent_sdk.api_key.expired": "API key '{uid}' 已过期。", + "ai.agent_sdk.api_key.multiple_matches": "多个 API key 匹配 '{key}':", + "ai.agent_sdk.api_key.multiple_select": "多个 API key 匹配 '{key}'。请选择要过期的 key:", + "ai.agent_sdk.api_key.multiple_specify_uid": "多个 API key 匹配 '{key}';请用 UID 指定 key", + "ai.agent_sdk.api_key.never": "从未", + "ai.agent_sdk.api_key.not_expired": "API key '{uid}' 未过期。", + "ai.agent_sdk.api_key.not_found": "未找到 API key '{key}'", + "ai.agent_sdk.api_key.raw_api_key": "原始 API key:{api_key}", + "ai.agent_sdk.api_key.store_securely": "此 secret key 只显示一次。请安全保存。", + "ai.agent_sdk.api_key.table.created": "创建时间", + "ai.agent_sdk.api_key.table.expires_at": "过期时间", + "ai.agent_sdk.api_key.table.key": "Key", + "ai.agent_sdk.api_key.table.last_used": "上次使用", + "ai.agent_sdk.api_key.table.name": "名称", + "ai.agent_sdk.api_key.table.scope": "范围", + "ai.agent_sdk.api_key.table.uid": "UID", + "ai.agent_sdk.api_key.uid": "UID:{uid}", + "ai.agent_sdk.artifact_upload.confirm_failed": "确认文件 artifact 上传失败", + "ai.agent_sdk.artifact_upload.conversation_missing_cloud_task": "对话 '{conversation_id}' 未关联云端 Agent 任务", + "ai.agent_sdk.artifact_upload.conversation_not_found": "未找到对话", + "ai.agent_sdk.artifact_upload.conversation_resolution_required": "应提供对话解析结果", + "ai.agent_sdk.artifact_upload.create_target_failed": "创建文件 artifact 上传目标失败", + "ai.agent_sdk.artifact_upload.env_var_not_set": "未设置 {env_var}", + "ai.agent_sdk.artifact_upload.env_var_not_unicode": "{env_var} 已设置,但不是有效的 Unicode", + "ai.agent_sdk.artifact_upload.file_size_out_of_range": "Artifact 文件大小超出支持范围", + "ai.agent_sdk.artifact_upload.invalid_env_run_id": "无效的 {env_var}", + "ai.agent_sdk.artifact_upload.load_conversation_failed": "加载对话 '{conversation_id}' 以解析 artifact 上传请求头失败", + "ai.agent_sdk.artifact_upload.multiple_conversations_found": "找到多个匹配 '{conversation_id}' 的对话", + "ai.agent_sdk.artifact_upload.open_file_failed": "打开 artifact 文件 '{path}' 失败", + "ai.agent_sdk.artifact_upload.read_file_failed": "读取 artifact 文件 '{path}' 失败", + "ai.agent_sdk.artifact_upload.resolve_association_failed": "解析 artifact 上传关联失败:未提供可用的 --run-id 或 --conversation-id,且 {env_var}:{env_err}", + "ai.agent_sdk.artifact_upload.resolve_association_for_conversation_failed": "解析对话 '{conversation_id}' 的 artifact 上传关联失败:{conversation_err};同时无法使用 {env_var}:{env_err}", + "ai.agent_sdk.artifact_upload.stat_file_failed": "读取 artifact 文件元数据 '{path}' 失败", + "ai.agent_sdk.artifact.downloaded": "Artifact 已下载", + "ai.agent_sdk.artifact.field.artifact_type": "Artifact 类型:{type}", + "ai.agent_sdk.artifact.field.artifact_uid": "Artifact UID:{uid}", + "ai.agent_sdk.artifact.field.content_type": "内容类型:{content_type}", + "ai.agent_sdk.artifact.field.created_at": "创建时间:{created_at}", + "ai.agent_sdk.artifact.field.description": "描述:{description}", + "ai.agent_sdk.artifact.field.download_url": "下载 URL:{url}", + "ai.agent_sdk.artifact.field.expires_at": "过期时间:{expires_at}", + "ai.agent_sdk.artifact.field.filename": "文件名:{filename}", + "ai.agent_sdk.artifact.field.filepath": "文件路径:{filepath}", + "ai.agent_sdk.artifact.field.mime_type": "MIME 类型:{mime_type}", + "ai.agent_sdk.artifact.field.path": "路径:{path}", + "ai.agent_sdk.artifact.field.size_bytes": "大小(字节):{size_bytes}", + "ai.agent_sdk.artifact.get_failed": "获取 artifact '{uid}' 失败", + "ai.agent_sdk.artifact.header.artifact_type": "Artifact 类型", + "ai.agent_sdk.artifact.header.artifact_uid": "Artifact UID", + "ai.agent_sdk.artifact.header.content_type": "内容类型", + "ai.agent_sdk.artifact.header.created_at": "创建时间", + "ai.agent_sdk.artifact.header.description": "描述", + "ai.agent_sdk.artifact.header.download_url": "下载 URL", + "ai.agent_sdk.artifact.header.expires_at": "过期时间", + "ai.agent_sdk.artifact.header.filename": "文件名", + "ai.agent_sdk.artifact.header.filepath": "文件路径", + "ai.agent_sdk.artifact.header.mime_type": "MIME 类型", + "ai.agent_sdk.artifact.header.path": "路径", + "ai.agent_sdk.artifact.header.size_bytes": "大小(字节)", + "ai.agent_sdk.artifact.uploaded": "Artifact 已上传", + "ai.agent_sdk.authentication_failed_with_error": "认证失败:{error}", + "ai.agent_sdk.bedrock_role_region_required": "设置 --bedrock-inference-role 时必须提供 --bedrock-role-region", + "ai.agent_sdk.check_warp_logs": "更多信息请查看 Warp 日志:{path}", + "ai.agent_sdk.claude_auth_secret_harness_only": "--claude-auth-secret 只能与 --harness claude 一起使用。", + "ai.agent_sdk.common.conversation_not_found_or_inaccessible": "conversation {conversation_id} 未找到或无法访问", + "ai.agent_sdk.common.invalid_id": "{id} 不是有效的{kind}标识符", + "ai.agent_sdk.common.invalid_run_id": "无效的 run ID", + "ai.agent_sdk.common.no_environment_choice": "不使用环境(Agent 将无法访问私有仓库或创建 Pull Request)", + "ai.agent_sdk.common.no_environments_configured": "此账号尚未配置环境。\n你可以用 `{cli_name} environment create` 创建环境。\n或者,使用 `--no-environment` 重新运行此命令以不使用环境。\n没有环境时,Agent 将无法访问私有仓库或创建 Pull Request。", + "ai.agent_sdk.common.object_kind.environment": "环境", + "ai.agent_sdk.common.object_not_found": "未找到{kind} {id}", + "ai.agent_sdk.common.operation_cancelled": "操作已取消", + "ai.agent_sdk.common.owner.personal": "个人", + "ai.agent_sdk.common.owner.team": "团队", + "ai.agent_sdk.common.select_environment_error": "选择环境出错:{error}", + "ai.agent_sdk.common.select_environment_prompt": "选择运行 Agent 的环境(或“不使用环境”):", + "ai.agent_sdk.common.team_metadata_timeout": "刷新团队 metadata 超时", + "ai.agent_sdk.common.unknown_model_id": "未知模型 ID '{model_id}'。可尝试:{suggestions}", + "ai.agent_sdk.common.user_not_on_team": "用户不在团队中", + "ai.agent_sdk.common.user_should_be_logged_in": "用户应已登录", + "ai.agent_sdk.common.warp_drive_sync_timeout": "等待 Warp Drive 同步超时", + "ai.agent_sdk.config_file.invalid_json": "配置文件 '{path}' 中的 JSON 无效", + "ai.agent_sdk.config_file.invalid_mcp_servers": "'{path}' 中的 mcp_servers 无效", + "ai.agent_sdk.config_file.invalid_yaml": "配置文件 '{path}' 中的 YAML 无效", + "ai.agent_sdk.config_file.parse_json_or_yaml_failed": "无法将配置文件 '{path}' 解析为 JSON 或 YAML", + "ai.agent_sdk.config_file.read_failed": "读取配置文件 '{path}' 失败", + "ai.agent_sdk.config_file.serialize_mcp_map_failed": "序列化 MCP 服务器映射失败", + "ai.agent_sdk.config_file.supported_keys": "支持的 key:name、environment_id、model_id、base_prompt、mcp_servers、host、computer_use_enabled", + "ai.agent_sdk.config_file.wasm_unsupported": "WASM 构建不支持配置文件", + "ai.agent_sdk.conversation_conversion_failed": "无法将 conversation 数据转换为 AIConversation", + "ai.agent_sdk.conversation_flag_unavailable": "此构建版本不支持 --conversation 标志", + "ai.agent_sdk.conversation_subcommand_unavailable": "此构建版本不支持 'conversation' 子命令", + "ai.agent_sdk.driver.attachments.create_dir_failed": "创建附件目录失败", + "ai.agent_sdk.driver.attachments.create_file_failed": "创建文件失败", + "ai.agent_sdk.driver.attachments.create_handoff_dir_failed": "创建 handoff 附件目录失败", + "ai.agent_sdk.driver.attachments.download_status_failed": "下载失败,状态码 {status}:{body}", + "ai.agent_sdk.driver.attachments.fetch_handoff_failed": "获取 handoff snapshot 附件失败", + "ai.agent_sdk.driver.attachments.fetch_task_failed": "获取任务附件失败", + "ai.agent_sdk.driver.attachments.file_too_large": "文件过大({size_mb}MB)。最大大小为 {max_mb}MB。", + "ai.agent_sdk.driver.attachments.invalid_filename": "file_id={file_id} 的文件名无效", + "ai.agent_sdk.driver.attachments.read_attachment_file_failed": "读取附件文件 '{path}' 失败:{error}", + "ai.agent_sdk.driver.attachments.send_download_request_failed": "发送下载请求失败", + "ai.agent_sdk.driver.attachments.write_file_failed": "写入文件失败", + "ai.agent_sdk.driver.cleanup_cloud_provider_failed": "清理云 provider 失败", + "ai.agent_sdk.driver.cleanup_harness_runtime_state_failed": "清理 harness 运行时状态失败", + "ai.agent_sdk.driver.cloud_provider.aws.create_token_file_failed": "创建临时 AWS OIDC token 文件失败", + "ai.agent_sdk.driver.cloud_provider.aws.remove_token_file_failed": "移除 AWS OIDC token 文件失败", + "ai.agent_sdk.driver.cloud_provider.aws.write_token_file_failed": "写入 AWS OIDC token 文件失败", + "ai.agent_sdk.driver.cloud_provider.gcp.prepare_credentials_failed": "准备 GCP federation credentials 失败", + "ai.agent_sdk.driver.cloud_provider.gcp.remove_credentials_failed": "移除 GCP credential 文件失败", + "ai.agent_sdk.driver.cloud_provider.setup_failed": "{provider_name} 设置失败", + "ai.agent_sdk.driver.error_classification.aws_bedrock_failed": "初始化 AWS Bedrock 凭证失败:{message}", + "ai.agent_sdk.driver.error_classification.bootstrap_failed": "终端会话启动失败。请尝试重新运行任务。", + "ai.agent_sdk.driver.error_classification.cloud_access_failed": "配置云端访问出错:{error}", + "ai.agent_sdk.driver.error_classification.config_build_failed": "构建 Agent 配置失败:{error}", + "ai.agent_sdk.driver.error_classification.conversation_blocked": "Agent 卡在等待用户确认以下操作:{blocked_action}", + "ai.agent_sdk.driver.error_classification.conversation_harness_mismatch": "Conversation {conversation_id} 由 {expected} harness 生成,但当前请求了 --harness {got}。请使用 --harness {expected} 重新运行(或省略 --harness)以继续此 conversation。", + "ai.agent_sdk.driver.error_classification.conversation_load_failed": "加载 conversation 失败:{message}", + "ai.agent_sdk.driver.error_classification.environment_not_found": "未找到环境 '{id}'。请确认环境 ID,并确保它存在于你的团队设置中。", + "ai.agent_sdk.driver.error_classification.environment_setup_failed": "环境设置失败:{message}。请检查仓库 URL 和设置命令。", + "ai.agent_sdk.driver.error_classification.harness_auth_check_failed": "Harness '{harness}' 认证检查失败:登录凭证无效或已过期。请确认为此 harness 配置的认证 secret 正确。", + "ai.agent_sdk.driver.error_classification.harness_command_failed": "Harness 命令退出,退出码 {exit_code}", + "ai.agent_sdk.driver.error_classification.harness_config_failed": "Harness '{harness}' 配置设置失败:{error}", + "ai.agent_sdk.driver.error_classification.harness_runtime_failure": "Harness '{harness}' 无法成功发起 API 请求。在 harness 输出中匹配到失败模式 '{pattern}':\"{excerpt}\"。这通常表示 API key 无效、额度不足,或账号配置有误。", + "ai.agent_sdk.driver.error_classification.harness_setup_failed": "Harness '{harness}' 验证失败:{reason}", + "ai.agent_sdk.driver.error_classification.internal_error": "发生内部错误。请尝试重新运行任务。如果问题仍然存在,请联系支持。", + "ai.agent_sdk.driver.error_classification.invalid_working_directory": "工作目录 '{path}' 不存在或不是目录。请检查环境配置中的路径。", + "ai.agent_sdk.driver.error_classification.mcp_json_parse_failed": "解析 MCP 服务器 JSON 配置失败:{message}", + "ai.agent_sdk.driver.error_classification.mcp_missing_variables": "MCP 服务器配置缺少必需变量。请提供所有必需的环境变量或模板值。", + "ai.agent_sdk.driver.error_classification.mcp_server_not_found": "未找到 MCP 服务器 {uuid}。请确认该服务器存在于你的 Warp Drive 中,且 UUID 正确。", + "ai.agent_sdk.driver.error_classification.mcp_startup_failed": "一个或多个 MCP 服务器启动失败。请检查 MCP 服务器配置是否有效,并确认服务器进程可以运行。", + "ai.agent_sdk.driver.error_classification.not_logged_in": "需要认证。请通过 '{bin} login' 登录、通过 '--api-key' 提供 API key,或设置 WARP_API_KEY 环境变量。", + "ai.agent_sdk.driver.error_classification.profile_not_found": "未找到 Agent profile \"{name}\"。请检查 profile ID,并确认它存在于你团队的 Warp Drive 中。", + "ai.agent_sdk.driver.error_classification.prompt_resolution_failed": "解析本次运行的 prompt 失败:{error}", + "ai.agent_sdk.driver.error_classification.resume_state_missing": "Conversation {conversation_id} 没有保存 {harness} harness 的 transcript。之前的运行可能在保存状态前崩溃了。", + "ai.agent_sdk.driver.error_classification.saved_prompt_not_found": "未找到 ID 为 {id} 的已保存 prompt。请确认该 prompt 存在于你的 Warp Drive 中。", + "ai.agent_sdk.driver.error_classification.secrets_fetch_failed": "获取任务 secrets 失败:{error}", + "ai.agent_sdk.driver.error_classification.share_session_disabled": "你的账号未启用会话共享。这通常是因为管理员为你的团队禁用了会话共享。请确认团队设置中已启用会话共享,或尝试不带 --share 参数运行。", + "ai.agent_sdk.driver.error_classification.share_session_failed": "共享 Agent 会话失败:{reason}", + "ai.agent_sdk.driver.error_classification.share_session_internal": "由于内部错误,无法共享 Agent 会话。请尝试重新运行任务。", + "ai.agent_sdk.driver.error_classification.share_session_interrupted": "会话共享在完成前被中断。请尝试重新运行任务。", + "ai.agent_sdk.driver.error_classification.share_session_timeout": "共享 Agent 会话失败:等待会话共享服务器响应超时。请检查网络连接后重试。", + "ai.agent_sdk.driver.error_classification.skill_resolution_failed": "Skill 解析失败:{message}", + "ai.agent_sdk.driver.error_classification.task_cancelled": "任务已取消。", + "ai.agent_sdk.driver.error_classification.task_harness_mismatch": "任务 {task_id} 使用 {expected} harness 创建,但当前请求了 --harness {got}。请使用 --harness {expected} 重新运行(或省略 --harness)以继续此任务。", + "ai.agent_sdk.driver.error_classification.team_metadata_timeout": "刷新团队 metadata 超时。请检查网络连接后重试。", + "ai.agent_sdk.driver.error_classification.warp_drive_sync_failed": "Warp Drive 同步失败。请检查网络连接后重试。", + "ai.agent_sdk.driver.exit_harness_after_runtime_failure_failed": "检测到运行时失败后退出 harness 失败", + "ai.agent_sdk.driver.exit_harness_failed": "退出 harness 失败", + "ai.agent_sdk.driver.git_credentials.create_dir_failed": "创建 {path} 失败", + "ai.agent_sdk.driver.git_credentials.fetch_from_server_failed": "从服务器获取 git credentials 失败", + "ai.agent_sdk.driver.git_credentials.home_dir_missing": "无法确定 home 目录", + "ai.agent_sdk.driver.git_credentials.issue_workload_token_failed": "为 git credentials refresh 签发 workload token 失败", + "ai.agent_sdk.driver.git_credentials.open_for_writing_failed": "打开 {path} 进行写入失败", + "ai.agent_sdk.driver.git_credentials.rename_failed": "将 {from} 重命名为 {to} 失败", + "ai.agent_sdk.driver.git_credentials.set_permissions_failed": "设置 {path} 的权限失败", + "ai.agent_sdk.driver.git_credentials.write_file_failed": "写入 {path} 失败", + "ai.agent_sdk.driver.harness.claude.open_jsonl_failed": "打开 {path} 失败", + "ai.agent_sdk.driver.harness.claude.read_jsonl_line_failed": "从 {path} 读取行失败", + "ai.agent_sdk.driver.harness.claude.read_subagents_dir_failed": "读取 subagents 目录 {path} 失败", + "ai.agent_sdk.driver.harness.claude.read_todos_dir_failed": "读取 todos 目录 {path} 失败", + "ai.agent_sdk.driver.harness.claude.read_transcript_for_session_failed": "读取 session {session_id} 的 transcript 失败", + "ai.agent_sdk.driver.harness.claude.rehydrate_transcript_failed": "恢复 Claude transcript 失败", + "ai.agent_sdk.driver.harness.claude.resolve_config_dir_failed": "解析 Claude config 目录失败", + "ai.agent_sdk.driver.harness.claude.serialize_config_failed": "序列化 Claude config 失败", + "ai.agent_sdk.driver.harness.claude.serialize_mcp_config_failed": "序列化 Claude MCP config 失败", + "ai.agent_sdk.driver.harness.claude.serialize_sessions_index_failed": "序列化 sessions-index.json 失败", + "ai.agent_sdk.driver.harness.claude.serialize_settings_failed": "序列化 Claude settings 失败", + "ai.agent_sdk.driver.harness.claude.serialize_transcript_envelope_failed": "序列化 transcript envelope 失败", + "ai.agent_sdk.driver.harness.claude.wake.deserialize_transcript_failed": "反序列化 wake 任务 {task_id} 的 Claude transcript 失败", + "ai.agent_sdk.driver.harness.claude.wake.fetch_transcript_failed": "获取任务 {task_id} 的 Claude transcript 失败", + "ai.agent_sdk.driver.harness.claude.wake.prepare_environment_failed": "为 wake 准备 Claude 环境失败", + "ai.agent_sdk.driver.harness.claude.wake.rehydrate_transcript_failed": "为 wake 恢复 Claude transcript 失败", + "ai.agent_sdk.driver.harness.claude.wake.resolve_prompt_failed": "解析任务 {task_id} 的 Claude wake prompt 失败", + "ai.agent_sdk.driver.harness.codex.create_config_dir_failed": "创建 Codex config 目录 {path} 失败", + "ai.agent_sdk.driver.harness.codex.open_auth_json_failed": "打开 {path} 进行写入失败", + "ai.agent_sdk.driver.harness.codex.rehydrate_transcript_failed": "恢复 codex transcript 失败", + "ai.agent_sdk.driver.harness.codex.resolve_sessions_root_failed": "解析 codex sessions root 失败", + "ai.agent_sdk.driver.harness.codex.serialize_auth_json_failed": "序列化 Codex auth.json 失败", + "ai.agent_sdk.driver.harness.codex.serialize_transcript_failed": "序列化 codex transcript 失败", + "ai.agent_sdk.driver.harness.codex.set_auth_permissions_failed": "设置 {path} 的权限失败", + "ai.agent_sdk.driver.harness.codex.write_config_toml_failed": "写入 Codex config.toml 到 {path} 失败", + "ai.agent_sdk.driver.harness.codex.write_system_prompt_failed": "写入 Codex system prompt 到 {path} 失败", + "ai.agent_sdk.driver.harness.create_temp_file_failed": "创建临时文件 '{prefix}' 失败:{error}", + "ai.agent_sdk.driver.harness.driver_dropped": "Agent driver 已被释放", + "ai.agent_sdk.driver.harness.driver_dropped_while_sending": "发送 {command} 时 Agent driver 已被释放", + "ai.agent_sdk.driver.harness.gemini.serialize_settings_failed": "序列化 Gemini settings 失败", + "ai.agent_sdk.driver.harness.gemini.serialize_trusted_folders_failed": "序列化 Gemini trusted folders 失败", + "ai.agent_sdk.driver.harness.gemini.write_system_prompt_failed": "写入 Gemini system prompt 到 {path} 失败", + "ai.agent_sdk.driver.harness.get_block_upload_slot_failed": "无法获取 conversation {conversation_id} 的 block 上传槽位", + "ai.agent_sdk.driver.harness.get_transcript_upload_target_failed": "获取 {conversation_id} 的 transcript 上传目标失败", + "ai.agent_sdk.driver.harness.home_dir_missing": "无法确定 home 目录", + "ai.agent_sdk.driver.harness.json.create_dir_failed": "创建 {path} 失败", + "ai.agent_sdk.driver.harness.json.parse_failed": "解析 {path} 失败", + "ai.agent_sdk.driver.harness.json.read_failed": "读取 {path} 失败", + "ai.agent_sdk.driver.harness.json.write_failed": "写入 {path} 失败", + "ai.agent_sdk.driver.harness.parent_bridge.create_temp_file_failed": "为 {path} 创建临时文件失败", + "ai.agent_sdk.driver.harness.parent_bridge.flush_temp_file_failed": "flush {path} 的临时文件失败", + "ai.agent_sdk.driver.harness.parent_bridge.no_parent_dir": "{path} 没有父目录", + "ai.agent_sdk.driver.harness.parent_bridge.persist_temp_file_failed": "持久化临时 {prefix} 文件失败", + "ai.agent_sdk.driver.harness.parent_bridge.read_lead_agent_message_failed": "读取 lead-agent message {message_id} 失败", + "ai.agent_sdk.driver.harness.parent_bridge.remove_file_failed": "移除 {path} 失败", + "ai.agent_sdk.driver.harness.parent_bridge.write_temp_file_failed": "写入 {path} 的临时文件失败", + "ai.agent_sdk.driver.harness.read_envelope_task_panicked": "read_envelope 任务 panic", + "ai.agent_sdk.driver.harness.serialize_block_failed": "无法序列化 conversation {conversation_id} 的 block", + "ai.agent_sdk.driver.harness.write_temp_file_failed": "写入临时文件 '{prefix}' 失败:{error}", + "ai.agent_sdk.driver.output.action.computer_use": "Computer use 操作:{summary}", + "ai.agent_sdk.driver.output.action.editing_files": "正在编辑文件:", + "ai.agent_sdk.driver.output.action.fetching_conversation": "正在获取 conversation {conversation_id}", + "ai.agent_sdk.driver.output.action.finding_files": "正在查找匹配 {queries} 的文件", + "ai.agent_sdk.driver.output.action.finding_files_in_path": "正在 {path} 中查找匹配 {queries} 的文件", + "ai.agent_sdk.driver.output.action.grepping": "正在 {path} 中 grep {queries}", + "ai.agent_sdk.driver.output.action.mcp_tool_call": "MCP tool 调用 {name}({input})", + "ai.agent_sdk.driver.output.action.reading": "正在读取 {files}", + "ai.agent_sdk.driver.output.action.reading_mcp_resource": "正在读取 MCP resource {resource}", + "ai.agent_sdk.driver.output.action.reading_skill": "正在读取 skill:{skill}", + "ai.agent_sdk.driver.output.action.request_computer_use": "正在请求 computer use:{summary}", + "ai.agent_sdk.driver.output.action.running_command": "正在运行 `{command}`", + "ai.agent_sdk.driver.output.action.searching_codebase": "正在 {codebase} 中搜索 {query}", + "ai.agent_sdk.driver.output.action.sending_message": "正在向 [{addresses}] 发送消息:{subject}", + "ai.agent_sdk.driver.output.action.starting_agent": "正在启动 Agent:{name}", + "ai.agent_sdk.driver.output.action.uploading_artifact": "正在上传 artifact {file_path}", + "ai.agent_sdk.driver.output.action.write_bytes": "向命令写入 {bytes} 字节", + "ai.agent_sdk.driver.output.artifact.file_uploaded": "文件 artifact 已上传:{filepath}(artifact:{artifact_uid})", + "ai.agent_sdk.driver.output.artifact.pr_created": "已创建 PR:{url}(分支:{branch})", + "ai.agent_sdk.driver.output.artifact.screenshot_captured": "截图已捕获(artifact:{artifact_uid})", + "ai.agent_sdk.driver.output.artifact.upload_failed": "上传 artifact 失败:{error}", + "ai.agent_sdk.driver.output.artifact.uploaded": "已上传 artifact {artifact_uid}", + "ai.agent_sdk.driver.output.artifact.uploaded_from": "已从 {filepath} 上传 artifact {artifact_uid}", + "ai.agent_sdk.driver.output.cancelled": "<已取消>", + "ai.agent_sdk.driver.output.codebase.search_failed": "搜索 codebase 失败:{message}", + "ai.agent_sdk.driver.output.codebase.search_results": "Codebase 搜索结果:", + "ai.agent_sdk.driver.output.command.completed": "{output}\n\n(`{command}` 已退出,退出码 {exit_code})", + "ai.agent_sdk.driver.output.command.denylisted": "由于命中 denylist,命令不允许运行", + "ai.agent_sdk.driver.output.command.finished": "{output}\n\n(已退出,退出码 {exit_code})", + "ai.agent_sdk.driver.output.command.long_running": "`{command}` 仍在运行...", + "ai.agent_sdk.driver.output.command.still_running": "命令仍在运行...", + "ai.agent_sdk.driver.output.command.write_failed": "写入命令失败。", + "ai.agent_sdk.driver.output.comments.addressed": "已处理 {count} 条评论", + "ai.agent_sdk.driver.output.conversation_started": "新 conversation 已启动,debug ID:{conversation_id}\n", + "ai.agent_sdk.driver.output.events.received": "已收到 {count} 个 Agent 事件", + "ai.agent_sdk.driver.output.fetch_conversation.error": "获取 conversation 出错:{error}", + "ai.agent_sdk.driver.output.fetch_conversation.success": "已将 conversation 获取到 {directory_path}", + "ai.agent_sdk.driver.output.file_edits.failed": "编辑文件失败:{error}", + "ai.agent_sdk.driver.output.file_edits.updated_deleted": "已更新 {updated_count} 个文件,删除 {deleted_count} 个文件:\n```diff\n{diff}\n```", + "ai.agent_sdk.driver.output.find.failed": "find 失败:{error}", + "ai.agent_sdk.driver.output.grep.failed": "grep 失败:{error}", + "ai.agent_sdk.driver.output.mcp.audio": "{mime_type} 音频", + "ai.agent_sdk.driver.output.mcp.call_tool_failed": "调用 MCP tool 失败:{error}", + "ai.agent_sdk.driver.output.mcp.image": "{mime_type} 图片", + "ai.agent_sdk.driver.output.mcp.read_resource_failed": "读取 MCP resource 失败:{error}", + "ai.agent_sdk.driver.output.messages.received": "已收到 {count} 条消息", + "ai.agent_sdk.driver.output.open_in_oz": "在 Oz 中打开:{url}\n", + "ai.agent_sdk.driver.output.plan_created": "已创建计划(标题:{title},id:{document_id},notebook:{notebook_link})", + "ai.agent_sdk.driver.output.read_files.failed": "读取文件失败:{error}", + "ai.agent_sdk.driver.output.run_id": "Run ID:{run_id}", + "ai.agent_sdk.driver.output.sharing_session": "共享会话地址:{join_url}", + "ai.agent_sdk.driver.output.skill.invoked": "Skill 已读取:{name}", + "ai.agent_sdk.driver.output.skill.read_error": "读取 skill 出错:{error}", + "ai.agent_sdk.driver.output.skill.read_success": "Skill 读取成功:{file_name}", + "ai.agent_sdk.driver.output.todo.completed": "已完成 TODO:", + "ai.agent_sdk.driver.output.todo.updated": "TODO 列表已更新:", + "ai.agent_sdk.driver.output.use_computer.error": "Use computer 出错:{error}", + "ai.agent_sdk.driver.output.web.fetch_failed": "Web fetch 失败", + "ai.agent_sdk.driver.output.web.fetched": "已获取 {count} 个网页", + "ai.agent_sdk.driver.output.web.fetching": "正在获取 {count} 个网页...", + "ai.agent_sdk.driver.output.web.search_failed": "Web search 失败:{query}", + "ai.agent_sdk.driver.output.web.searched_results": "已搜索网页:{query}({count} 个结果)", + "ai.agent_sdk.driver.output.web.searching": "正在搜索网页", + "ai.agent_sdk.driver.output.web.searching_for": "正在搜索网页:{query}", + "ai.agent_sdk.driver.report_driver_error_failed": "上报任务 {task_id} 的 driver 错误失败", + "ai.agent_sdk.driver.repository_indexing_failed": "仓库索引失败:{error}", + "ai.agent_sdk.driver.repository_indexing_pending": "仓库仍在索引中:{repo_path}", + "ai.agent_sdk.driver.repository_not_found": "未找到仓库:{repo_path}", + "ai.agent_sdk.driver.save_harness_conversation_final_failed": "保存 harness conversation 失败(最终)", + "ai.agent_sdk.driver.save_harness_conversation_periodic_failed": "保存 harness conversation 失败(定时)", + "ai.agent_sdk.driver.save_harness_conversation_post_turn_failed": "保存 harness conversation 失败(回合后)", + "ai.agent_sdk.driver.set_ambient_agent_shared_session_id_failed": "设置 ambient agent shared session ID 出错", + "ai.agent_sdk.driver.slow_bootstrap_warning": "警告:终端会话启动较慢。请查看 https://docs.warp.dev/support-and-community/troubleshooting-and-support/known-issues#shells 排查。", + "ai.agent_sdk.driver.snapshot.allocate_initial_snapshot_token_failed": "分配初始 snapshot token 失败", + "ai.agent_sdk.driver.snapshot.create_declarations_dir_failed": "创建 declarations 目录 {path} 失败", + "ai.agent_sdk.driver.snapshot.flush_declarations_file_failed": "flush declarations 文件 {path} 失败", + "ai.agent_sdk.driver.snapshot.get_upload_targets_failed": "获取 snapshot 上传目标失败,跳过上传", + "ai.agent_sdk.driver.snapshot.git_command_failed": "git {args} 在 {repo_dir} 中执行失败:{stderr}", + "ai.agent_sdk.driver.snapshot.git_command_timed_out": "git {args} 在 {repo_dir} 中执行超过 {timeout} 后超时", + "ai.agent_sdk.driver.snapshot.open_declarations_file_failed": "打开 declarations 文件 {path} 失败", + "ai.agent_sdk.driver.snapshot.read_file_failed": "读取文件 '{file_path}' 失败:{error}", + "ai.agent_sdk.driver.snapshot.serialize_file_declaration_failed": "序列化 file declaration 失败", + "ai.agent_sdk.driver.snapshot.serialize_manifest_failed": "序列化 snapshot manifest 失败,跳过上传", + "ai.agent_sdk.driver.snapshot.upload_manifest_failed": "上传 manifest '{manifest_filename}' 失败", + "ai.agent_sdk.driver.snapshot.write_declarations_file_failed": "写入 declarations 文件 {path} 失败", + "ai.agent_sdk.driver.update_harness_state_from_cli_session_event_failed": "根据 CLI session 事件更新 harness 状态失败", + "ai.agent_sdk.driver.write_artifact_created_failed": "写入 artifact_created 失败", + "ai.agent_sdk.driver.write_conversation_id_failed": "写入 conversation ID 失败", + "ai.agent_sdk.driver.write_exchange_inputs_failed": "写入 exchange 输入失败", + "ai.agent_sdk.driver.write_exchange_output_failed": "写入 exchange 输出失败", + "ai.agent_sdk.driver.write_run_id_failed": "写入 run ID 失败", + "ai.agent_sdk.driver.write_shared_session_event_failed": "写入 shared session 事件失败", + "ai.agent_sdk.environment.action_cancelled": "已取消{action}环境。", + "ai.agent_sdk.environment.action.create": "创建", + "ai.agent_sdk.environment.action.delete": "删除", + "ai.agent_sdk.environment.action.update": "更新", + "ai.agent_sdk.environment.authorize_access_here": "在这里授权访问:{url}", + "ai.agent_sdk.environment.cannot_access_private_repo": "无法访问私有仓库 {owner}/{repo}", + "ai.agent_sdk.environment.check_github_auth_status_failed": "检查 GitHub 授权状态失败", + "ai.agent_sdk.environment.confirmation_prompt_error": "确认提示出错:{error}", + "ai.agent_sdk.environment.created_successfully": "环境创建成功,ID:{id}", + "ai.agent_sdk.environment.creation_cancelled": "已取消创建环境。", + "ai.agent_sdk.environment.custom_docker_image": "自定义 Docker 镜像", + "ai.agent_sdk.environment.delete_failed": "删除环境失败", + "ai.agent_sdk.environment.deleted_successfully": "环境删除成功", + "ai.agent_sdk.environment.detail.description": "描述:{description}", + "ai.agent_sdk.environment.detail.docker_image": "Docker 镜像:{image}", + "ai.agent_sdk.environment.detail.name": "名称:{name}", + "ai.agent_sdk.environment.detail.repositories": "仓库:", + "ai.agent_sdk.environment.detail.repositories_none": "仓库:无", + "ai.agent_sdk.environment.detail.setup_commands": "设置命令:", + "ai.agent_sdk.environment.detail.setup_commands_none": "设置命令:无", + "ai.agent_sdk.environment.enter_custom_docker_image": "输入自定义 Docker 镜像名称:", + "ai.agent_sdk.environment.enter_custom_image_error": "输入自定义镜像出错", + "ai.agent_sdk.environment.fetch_base_images_failed": "获取基础镜像列表失败", + "ai.agent_sdk.environment.fetch_images_failed": "获取镜像失败", + "ai.agent_sdk.environment.fetch_images_failed_with_error": "获取镜像失败:{error}", + "ai.agent_sdk.environment.github_authorization_expired": "GitHub 授权已过期。请重试。", + "ai.agent_sdk.environment.github_authorization_failed": "GitHub 授权失败。请重试。", + "ai.agent_sdk.environment.image_list_info": "所有 Warp dev 镜像都包含 Python 和 Node。更多信息请见:{repo}", + "ai.agent_sdk.environment.image_table.image": "镜像", + "ai.agent_sdk.environment.image_table.repository": "仓库", + "ai.agent_sdk.environment.image_table.tag": "Tag", + "ai.agent_sdk.environment.images_info": "所有 warpdotdev 镜像都包含 Python 和 Node,以及特定语言工具。更多信息:{repo}", + "ai.agent_sdk.environment.integration_usage_confirm": "此环境正被以下集成使用:{integrations}。确定要{action}它吗?", + "ai.agent_sdk.environment.integration_usage_unknown_abort": "由于无法确定集成使用情况,正在中止{action}环境。可使用 --force 重新运行以覆盖。", + "ai.agent_sdk.environment.invalid_repo_format": "仓库格式无效:'{repo}'。预期格式:'owner/repo'", + "ai.agent_sdk.environment.max_authorization_attempts_exceeded": "已超过最大授权尝试次数({count})。请稍后重试。", + "ai.agent_sdk.environment.missing_oauth_auth_url": "服务器错误:未收到 OAuth 流程的授权 URL", + "ai.agent_sdk.environment.no_auth_flow_provided": "无法{action}环境:需要授权,但服务器未提供授权流程", + "ai.agent_sdk.environment.no_docker_image": "未提供 Docker 镜像,请选择一个基础镜像。", + "ai.agent_sdk.environment.no_warp_dev_images_available": "没有可用的 Warp dev 镜像。", + "ai.agent_sdk.environment.not_found": "未找到环境 {id}", + "ai.agent_sdk.environment.opening_github_authorization": "正在打开浏览器进行 GitHub 授权:{url}", + "ai.agent_sdk.environment.poll_oauth_status_error": "轮询 OAuth 状态出错:{error}", + "ai.agent_sdk.environment.private_repo_authorization_required": "访问私有仓库需要授权。", + "ai.agent_sdk.environment.private_repos_multiple_owners": "一个环境中的所有私有仓库必须属于同一 owner。发现多个 owner:{owners}。\n如果你需要支持来自多个 owner 的私有仓库,请提交 GitHub issue。", + "ai.agent_sdk.environment.public_repo_auth_warning": "警告:正在未授权的情况下使用公开仓库 {owner}/{repo}。当前可使用只读访问;如果需要完整访问,请完成授权。", + "ai.agent_sdk.environment.repository_removal_warning": "警告:环境中未找到仓库 {owner}/{repo},已跳过移除", + "ai.agent_sdk.environment.rerun_after_authorizing": "授权后,请重新运行此命令。", + "ai.agent_sdk.environment.select_base_image": "选择基础镜像:", + "ai.agent_sdk.environment.select_image_error": "选择镜像出错", + "ai.agent_sdk.environment.setup_command_removal_warning": "警告:环境中未找到设置命令 '{command}',已跳过移除", + "ai.agent_sdk.environment.table.base_image": "基础镜像", + "ai.agent_sdk.environment.table.creator": "创建者", + "ai.agent_sdk.environment.table.description": "描述", + "ai.agent_sdk.environment.table.git_repos": "Git 仓库", + "ai.agent_sdk.environment.table.id": "ID", + "ai.agent_sdk.environment.table.last_edited": "上次编辑", + "ai.agent_sdk.environment.table.name": "名称", + "ai.agent_sdk.environment.table.scope": "范围", + "ai.agent_sdk.environment.table.setup_commands": "设置命令", + "ai.agent_sdk.environment.timed_out_waiting_for_warp_drive": "等待 Warp Drive 同步超时", + "ai.agent_sdk.environment.unexpected_non_terminal_oauth_status": "返回了意外的非终态 OAuth 状态", + "ai.agent_sdk.environment.update_failed": "更新环境失败", + "ai.agent_sdk.environment.updated_successfully": "环境更新成功!", + "ai.agent_sdk.environment.user_not_connected_to_github": "用户未连接到 GitHub", + "ai.agent_sdk.federate.expires_at": "过期时间:{expires_at}", + "ai.agent_sdk.federate.feature_not_enabled": "此功能未启用", + "ai.agent_sdk.federate.issuer": "签发方:{issuer}", + "ai.agent_sdk.federate.subject_template_required": "--subject-template 至少需要一个值", + "ai.agent_sdk.federate.token": "Token:{token}", + "ai.agent_sdk.federate.write_gcp_token_failed": "写入 GCP token 到 {path} 时出错", + "ai.agent_sdk.harness_support.artifact_reported": "Artifact 已上报:{uid}", + "ai.agent_sdk.harness_support.feature_not_enabled": "此功能未启用", + "ai.agent_sdk.harness_support.notification_sent": "通知已发送。", + "ai.agent_sdk.harness_support.shutdown_error_args_required": "--error-category 和 --error-message 必须一起提供", + "ai.agent_sdk.harness_support.shutdown_reported": "Shutdown 已上报。", + "ai.agent_sdk.harness_support.task_finished": "任务已完成。", + "ai.agent_sdk.harness.local_child_only": "{harness} harness 仅支持本地子 Agent 启动。", + "ai.agent_sdk.integration.action.creation": "创建", + "ai.agent_sdk.integration.action.update": "更新", + "ai.agent_sdk.integration.authorize_provider_here": "在这里授权 provider:{url}", + "ai.agent_sdk.integration.created": "创建时间:{time}", + "ai.agent_sdk.integration.creating_with_environment": "正在用环境 {id} 创建集成。", + "ai.agent_sdk.integration.creating_without_environment": "正在创建不使用环境的集成。", + "ai.agent_sdk.integration.creation_cancelled": "已取消创建集成。", + "ai.agent_sdk.integration.heading.integration": "集成:", + "ai.agent_sdk.integration.heading.integrations": "集成:", + "ai.agent_sdk.integration.label.base_prompt": "基础提示词", + "ai.agent_sdk.integration.label.environment": "环境", + "ai.agent_sdk.integration.label.mcp_servers": "MCP 服务器", + "ai.agent_sdk.integration.label.model": "模型", + "ai.agent_sdk.integration.label.status": "状态", + "ai.agent_sdk.integration.max_attempts_exceeded": "已超过最大集成{action}尝试次数({count})。请重试。", + "ai.agent_sdk.integration.missing_auth_url": "服务器没有返回集成{action}流程所需的 authURL。", + "ai.agent_sdk.integration.no_integrations_found": "未找到集成。", + "ai.agent_sdk.integration.none": "(无)", + "ai.agent_sdk.integration.oauth_authorization_expired": "OAuth 授权已过期。", + "ai.agent_sdk.integration.oauth_authorization_failed": "OAuth 授权失败。", + "ai.agent_sdk.integration.poll_oauth_status_error": "轮询 OAuth 状态出错:{error}", + "ai.agent_sdk.integration.reported_failure": "集成{action}报告失败:{message}", + "ai.agent_sdk.integration.rerun_after_authorizing": "授权后,请重新运行此命令以继续集成{action}流程。", + "ai.agent_sdk.integration.status.active": "集成已连接并启用。", + "ai.agent_sdk.integration.status.connection_error": "此 provider 已连接,但存在错误。", + "ai.agent_sdk.integration.status.not_configured": "连接已激活,但尚未配置 Agent 集成。", + "ai.agent_sdk.integration.status.not_connected": "此集成未连接。", + "ai.agent_sdk.integration.status.not_enabled": "集成已配置,但当前已禁用。", + "ai.agent_sdk.integration.table.created": "创建时间", + "ai.agent_sdk.integration.table.description": "描述", + "ai.agent_sdk.integration.table.environment": "环境", + "ai.agent_sdk.integration.table.provider": "Provider", + "ai.agent_sdk.integration.table.status": "状态", + "ai.agent_sdk.integration.table.updated": "更新时间", + "ai.agent_sdk.integration.unexpected_non_terminal_oauth_status": "返回了意外的非终态 OAuth 状态", + "ai.agent_sdk.integration.updated": "更新时间:{time}", + "ai.agent_sdk.invalid_api_key": "你的 API key 无效。请通过 '--api-key' 或 WARP_API_KEY 环境变量提供有效 key。", + "ai.agent_sdk.invalid_credentials": "你的凭证无效。请用 `{cli_name} login` 重新登录。", + "ai.agent_sdk.invalid_value": "无效的值 '{value}'", + "ai.agent_sdk.login_required": "你尚未登录,请先用 `{cli_name} login` 登录后再继续。", + "ai.agent_sdk.mcp_config.args_item_must_be_string": "MCP 服务器 '{server_name}' 的 'args[{idx}]' 字段必须是字符串", + "ai.agent_sdk.mcp_config.args_must_be_array": "MCP 服务器 '{server_name}' 的 'args' 字段必须是数组", + "ai.agent_sdk.mcp_config.config_must_be_object": "MCP 服务器 '{server_name}' 的配置必须是 JSON 对象", + "ai.agent_sdk.mcp_config.duplicate_server_name": "MCP 服务器名称 '{name}' 被指定了多次", + "ai.agent_sdk.mcp_config.exactly_one_transport": "MCP 服务器 '{server_name}' 必须且只能包含 'warp_id'、'command' 或 'url' 之一", + "ai.agent_sdk.mcp_config.field_must_be_non_empty": "MCP 服务器 '{server_name}' 的 '{field}' 字段不能为空", + "ai.agent_sdk.mcp_config.field_must_be_object": "MCP 服务器 '{server_name}' 的 '{field}' 字段必须是对象", + "ai.agent_sdk.mcp_config.field_must_be_string": "MCP 服务器 '{server_name}' 的 '{field}' 字段必须是字符串", + "ai.agent_sdk.mcp_config.invalid_json": "无效的 MCP JSON", + "ai.agent_sdk.mcp_config.nested_field_must_be_string": "MCP 服务器 '{server_name}' 的 '{field}.{key}' 字段必须是字符串", + "ai.agent_sdk.mcp_config.normalize_json_failed": "标准化 MCP JSON 失败", + "ai.agent_sdk.mcp_config.parse_server_map_failed": "解析 MCP 服务器映射失败", + "ai.agent_sdk.mcp_config.warp_id_must_be_uuid": "MCP 服务器 '{server_name}' 的 'warp_id' 字段必须是 UUID", + "ai.agent_sdk.mcp.table.name": "名称", + "ai.agent_sdk.mcp.table.uuid": "UUID", + "ai.agent_sdk.message_subcommand_unavailable": "此构建版本不支持 'message' 子命令", + "ai.agent_sdk.model.refresh_workspace_metadata_timeout": "刷新 workspace metadata 超时", + "ai.agent_sdk.model.table.model_id": "模型 ID", + "ai.agent_sdk.oauth.authorization_timeout": "等待 OAuth 授权超时", + "ai.agent_sdk.oauth.waiting_for_authorization": "正在等待授权完成... 如果授权后这里没有更新,请重启命令后重试。", + "ai.agent_sdk.output.invalid_data": "数据无效:{error}", + "ai.agent_sdk.output.jq_filter_error": "jq filter 错误:{error}", + "ai.agent_sdk.output.write_jq_json_failed": "无法将 jq 输出写为 JSON", + "ai.agent_sdk.output.write_json_failed": "无法写入 JSON 输出", + "ai.agent_sdk.profiles.table.id": "ID", + "ai.agent_sdk.profiles.table.name": "名称", + "ai.agent_sdk.profiles.unsynced": "未同步", + "ai.agent_sdk.provider.authenticate_open_url": "要认证 {provider},请在浏览器中打开此 URL:{url}", + "ai.agent_sdk.provider.scope_required": "Provider '{provider}' 必须设置为团队账号或个人账号", + "ai.agent_sdk.provider.status.not_connected": "❌ 未连接", + "ai.agent_sdk.provider.table.allowed_for": "允许范围", + "ai.agent_sdk.provider.table.name": "名称", + "ai.agent_sdk.provider.table.slug": "SLUG", + "ai.agent_sdk.provider.table.status": "状态", + "ai.agent_sdk.provider.user_not_on_team": "用户不在团队中", + "ai.agent_sdk.saved_prompt_source": "已保存 prompt({id})", + "ai.agent_sdk.schedule.deleted": "计划任务已删除", + "ai.agent_sdk.schedule.deleting_agent": "正在删除 Agent...", + "ai.agent_sdk.schedule.detail.agent_name": "Agent 名称:{name}", + "ai.agent_sdk.schedule.detail.cron_schedule": "Cron 计划:{schedule}", + "ai.agent_sdk.schedule.detail.environment_id": "环境 ID:{id}", + "ai.agent_sdk.schedule.detail.host": "主机:{host}", + "ai.agent_sdk.schedule.detail.last_error": "上次错误:{error}", + "ai.agent_sdk.schedule.detail.last_ran": "上次运行:{last_ran}", + "ai.agent_sdk.schedule.detail.model_id": "模型 ID:{id}", + "ai.agent_sdk.schedule.detail.name": "名称:{name}", + "ai.agent_sdk.schedule.detail.next_run": "下次运行:{next_run}", + "ai.agent_sdk.schedule.detail.paused": "已暂停:{paused}", + "ai.agent_sdk.schedule.detail.prompt": "提示词:{prompt}", + "ai.agent_sdk.schedule.detail.skill": "Skill:{skill}", + "ai.agent_sdk.schedule.not_found": "未找到计划任务", + "ai.agent_sdk.schedule.paused": "计划任务已暂停", + "ai.agent_sdk.schedule.pausing_agent": "正在暂停 Agent...", + "ai.agent_sdk.schedule.resuming_agent": "正在恢复 Agent...", + "ai.agent_sdk.schedule.scheduled_agent": "已计划 Agent:{id}", + "ai.agent_sdk.schedule.scheduling_agent": "正在计划 Agent {name}...", + "ai.agent_sdk.schedule.table.agent_name": "Agent 名称", + "ai.agent_sdk.schedule.table.cron_schedule": "Cron 计划", + "ai.agent_sdk.schedule.table.environment_id": "环境 ID", + "ai.agent_sdk.schedule.table.host": "主机", + "ai.agent_sdk.schedule.table.id": "ID", + "ai.agent_sdk.schedule.table.last_error": "上次错误", + "ai.agent_sdk.schedule.table.last_ran": "上次运行", + "ai.agent_sdk.schedule.table.model_id": "模型 ID", + "ai.agent_sdk.schedule.table.name": "名称", + "ai.agent_sdk.schedule.table.next_run": "下次运行", + "ai.agent_sdk.schedule.table.paused": "已暂停", + "ai.agent_sdk.schedule.table.prompt": "提示词", + "ai.agent_sdk.schedule.table.schedule": "计划", + "ai.agent_sdk.schedule.table.scope": "范围", + "ai.agent_sdk.schedule.table.skill": "Skill", + "ai.agent_sdk.schedule.unpaused": "计划任务已取消暂停", + "ai.agent_sdk.schedule.updated": "计划任务已更新", + "ai.agent_sdk.schedule.updating_agent": "正在更新 Agent...", + "ai.agent_sdk.schedule.without_environment": "正在计划不使用环境运行的 Agent。", + "ai.agent_sdk.secret.aws_access_key_id": "AWS Access Key ID:", + "ai.agent_sdk.secret.aws_region": "AWS 区域:", + "ai.agent_sdk.secret.aws_secret_access_key": "AWS Secret Access Key:", + "ai.agent_sdk.secret.aws_session_token": "AWS Session Token(可选,按 Enter 跳过):", + "ai.agent_sdk.secret.bedrock_access_key_noninteractive_required": "非交互模式下,Bedrock access key secret 需要 --access-key-id、--secret-access-key 和 --region", + "ai.agent_sdk.secret.bedrock_access_key_update_not_supported": "Bedrock access key secret 不能通过 `--value` 更新;请重新创建 secret", + "ai.agent_sdk.secret.bedrock_api_key": "Bedrock API key:", + "ai.agent_sdk.secret.bedrock_api_key_update_not_supported": "Bedrock API key secret 不能通过 `--value` 更新;请重新创建 secret", + "ai.agent_sdk.secret.bedrock_noninteractive_required": "非交互模式下,Bedrock secret 需要 --bedrock-api-key 和 --region", + "ai.agent_sdk.secret.created": "Secret '{name}' 已创建", + "ai.agent_sdk.secret.delete_confirm": "要删除{scope} secret '{name}' 吗?", + "ai.agent_sdk.secret.delete_confirm_help": "此操作无法撤销", + "ai.agent_sdk.secret.delete_refusing_noninteractive": "非交互模式下拒绝在未经确认的情况下删除 secret(使用 --force 可绕过)", + "ai.agent_sdk.secret.deleted": "Secret '{name}' 已删除", + "ai.agent_sdk.secret.deletion_cancelled": "已取消删除", + "ai.agent_sdk.secret.feature_not_enabled": "此功能未启用", + "ai.agent_sdk.secret.name_required": "Secret 名称是必需的。用法:oz secret create ", + "ai.agent_sdk.secret.not_found": "未找到 secret '{name}'", + "ai.agent_sdk.secret.openai_base_url": "OpenAI base URL(可选,按 Enter 跳过):", + "ai.agent_sdk.secret.openai_base_url_help": "例如:https://us.api.openai.com/v1,用于区域端点", + "ai.agent_sdk.secret.read_value_file_failed": "无法从以下位置读取 secret 值:{path}", + "ai.agent_sdk.secret.scope.personal": "个人", + "ai.agent_sdk.secret.scope.team": "团队", + "ai.agent_sdk.secret.secret_value": "密钥值:", + "ai.agent_sdk.secret.table.created": "创建时间", + "ai.agent_sdk.secret.table.name": "名称", + "ai.agent_sdk.secret.table.scope": "范围", + "ai.agent_sdk.secret.table.type": "类型", + "ai.agent_sdk.secret.table.updated": "更新时间", + "ai.agent_sdk.secret.type.anthropic_api_key": "Anthropic API Key", + "ai.agent_sdk.secret.type.anthropic_bedrock_access_key": "Anthropic Bedrock Access Key", + "ai.agent_sdk.secret.type.anthropic_bedrock_api_key": "Anthropic Bedrock API Key", + "ai.agent_sdk.secret.type.openai_api_key": "OpenAI API Key", + "ai.agent_sdk.secret.type.raw_value": "原始值", + "ai.agent_sdk.secret.updated": "Secret '{name}' 已更新", + "ai.agent_sdk.skill_resolution.ambiguous": "Skill '{skill}' 不明确;请用 repo:skill_name 指定\n\n候选项:\n", + "ai.agent_sdk.skill_resolution.clone_failed": "克隆仓库 '{org}/{repo}' 失败:{message}", + "ai.agent_sdk.skill_resolution.org_mismatch": "已找到仓库 '{repo}',但它属于 org '{found}',预期为 '{expected}'", + "ai.agent_sdk.skill_resolution.parse_failed": "解析 skill 文件 {path} 失败:{message}", + "ai.agent_sdk.skill_resolution.repository_not_found": "未找到仓库 '{repo}'", + "ai.agent_sdk.skill_resolution.skill_not_found": "未找到 Skill '{skill}'", + "ai.agent_sdk.team_api_key_free_credits_warning": "警告:免费云端额度仅适用于个人运行,但此运行使用了团队 API key。如需使用免费云端额度,请考虑改用个人 API key。", + "ai.agent_sdk.unable_to_resolve_path": "无法解析路径 {path}", + "ai.agent_sdk.unexpected_argument_found": "发现非预期参数 '{argument}'", + "ai.agent_sdk.working_directory_unavailable": "无法确定工作目录", + "ai.agent_shortcuts.file_paths_context": "用于文件路径和附加其他上下文", + "ai.agent_shortcuts.go_back_to_terminal": "返回终端", + "ai.agent_shortcuts.input_shell_command": "输入 shell 命令", + "ai.agent_shortcuts.open_code_review": "打开 code review", + "ai.agent_shortcuts.pause_agent": "暂停 Agent", + "ai.agent_shortcuts.search_continue_conversations": "搜索并继续对话", + "ai.agent_shortcuts.slash_commands": "用于 slash command", + "ai.agent_shortcuts.start_new_conversation": "开始新对话", + "ai.agent_shortcuts.toggle_auto_accept": "切换自动接受", + "ai.agent_shortcuts.toggle_conversation_list": "切换对话列表", + "ai.agent_tips.action.open_palette": "打开命令面板", + "ai.agent_tips.action.show_diff_view": "显示 diff 视图", + "ai.agent_tips.action.warp_drive": "Warp Drive。", + "ai.agent_tips.add_mcp": "使用 `/add-mcp` 将 MCP 服务器添加到工作区。", + "ai.agent_tips.add_prompt": "使用 `/add-prompt` 创建可复用 prompt,用于可重复的工作流。", + "ai.agent_tips.add_rule": "使用 `/add-rule` 创建全局 Agent 规则。", + "ai.agent_tips.agent_profiles": "添加 Agent profile,为每个会话自定义权限和模型。", + "ai.agent_tips.at_add_context": "使用 `@` 将文件、Block 或 Warp Drive 对象添加为 prompt 上下文。", + "ai.agent_tips.attach_prior_command_output": "使用 将上一条命令输出附加为 Agent 上下文。", + "ai.agent_tips.auto_approve_session": "使用 在本次会话剩余时间内自动批准 Agent 的命令和 diff。", + "ai.agent_tips.cancel_agent_task": "使用 取消当前 Agent 任务。", + "ai.agent_tips.compact_conversation": "使用 `/compact` 总结当前对话,并释放上下文窗口空间。", + "ai.agent_tips.control_interactive_tools": "提示 Agent 控制 node、python、postgres、gdb 或 vim 等交互式工具。", + "ai.agent_tips.create_environment": "使用 `/create-environment` 将 repo 转换为 Agent 可运行的远程 Docker 环境。", + "ai.agent_tips.drag_image_context": "将图片拖入窗格,即可附加为 Agent 上下文。", + "ai.agent_tips.enable_desktop_notifications": "启用桌面通知,在 Agent 需要你关注时收到提醒。", + "ai.agent_tips.fork_conversation": "使用 `/fork` 创建当前对话的新副本,也可以附带新的 prompt。", + "ai.agent_tips.handoff_to_cloud": "输入 `&` 或使用 handoff chip,将本地对话移动到云端。", + "ai.agent_tips.init_generate_warp_md": "使用 `/init` 生成 `WARP.md` 文件,并为 Agent 定义项目规则。", + "ai.agent_tips.init_index_repo": "使用 `/init` 索引 repo,帮助 Agent 理解你的代码库。", + "ai.agent_tips.new_conversation": "使用 `/new` 以干净上下文开始新的 Agent 对话。", + "ai.agent_tips.open_code_review_command": "使用 `/open-code-review` 打开 code review 面板,检查 Agent 生成的 diff。", + "ai.agent_tips.open_code_review_panel": "使用 打开 code review 面板,审查 Agent 的更改。", + "ai.agent_tips.open_command_palette": "使用 打开命令面板,访问 Warp 操作和快捷键。", + "ai.agent_tips.open_mcp_servers": "使用 `/open-mcp-servers` 查看 MCP 服务器,并与团队共享。", + "ai.agent_tips.oz_headless": "使用 `oz` 命令以 headless 模式运行 Oz Agent,适合远程机器。", + "ai.agent_tips.paste_url_context": "粘贴 URL,将该网页作为 Agent 上下文附加。", + "ai.agent_tips.plan_prompt": "使用 `/plan` 让 Agent 在执行前先创建计划。", + "ai.agent_tips.project_rules_files": "使用 `AGENTS.md` 或 `CLAUDE.md` 应用项目级规则。", + "ai.agent_tips.redirect_running_agent": "输入新的 prompt,在 Agent 运行时重新引导它。", + "ai.agent_tips.right_click_copy_output": "右键点击 Block 以复制对话输出。", + "ai.agent_tips.right_click_fork": "右键点击 Block,从该位置 fork 对话。", + "ai.agent_tips.right_click_selected_text": "右键点击选中文本,将其附加为 Agent 上下文。", + "ai.agent_tips.slash_command_menu": "使用 `/` 打开 slash-command 菜单,快速访问 Agent 操作。", + "ai.agent_tips.store_reusable_objects": "将可复用的 workflow、notebook 和 prompt 存放到你的", + "ai.agent_tips.switch_agent_profiles": "切换 Agent profile,快速更改模型和 Agent 权限。", + "ai.agent_tips.tip_prefix": "提示:{description}", + "ai.agent_tips.toggle_natural_language_detection": "使用 切换自然语言检测,并在 Agent 输入和终端输入之间切换。", + "ai.agent_tips.usage": "使用 `/usage` 查看当前 AI 额度用量。", + "ai.agent_tips.voice_input": "按住 ,直接向 Agent 说出你的 prompt。", + "ai.agent_tips.warpify_ssh": "Warpify 远程 SSH 会话,以便在该环境中启用 Oz。", + "ai.agent_view.continued": "已继续", + "ai.agent_view.deleted": "已删除", + "ai.agent_view.deleted_conversation": "已删除的对话", + "ai.agent_view.exit_confirmation.exit": "再次按下以退出", + "ai.agent_view.exit_confirmation.start_new_conversation": "再次按下以开始新对话", + "ai.agent_view.exit_confirmation.stop_and_exit": "再次按下以停止并退出", + "ai.agent_view.open_in_different_pane": "已在其他窗格中打开", + "ai.agent_view.restored": "已恢复", + "ai.agent_view.untitled_conversation": "未命名对话", + "ai.artifacts.download_prepare_failed": "准备文件下载失败。", + "ai.artifacts.downloaded": "已下载 {filename}。", + "ai.artifacts.failed_to_load": "加载失败", + "ai.artifacts.file": "文件", + "ai.artifacts.screenshots": "截图", + "ai.artifacts.untitled_plan": "未命名计划", + "ai.ask_user_question.agent_questions": "Agent 问题", + "ai.ask_user_question.allow_agent_to_ask_questions": "允许 Agent 提问:", + "ai.ask_user_question.answer_prefix": "答:{answer}", + "ai.ask_user_question.answered_all_questions": "已回答全部 {total} 个问题", + "ai.ask_user_question.answered_partial": "已回答 {answered_count}/{total} 个问题", + "ai.ask_user_question.answered_question": "已回答问题", + "ai.ask_user_question.other": "其他...", + "ai.ask_user_question.placeholder": "输入你的回答并按 Enter", + "ai.ask_user_question.question_prefix": "问:{question}", + "ai.ask_user_question.questions_skipped": "问题已跳过", + "ai.ask_user_question.questions_skipped_auto_approve": "因自动批准已跳过问题", + "ai.ask_user_question.questions_unavailable": "问题不可用", + "ai.ask_user_question.select_all_suffix": "(可多选)", + "ai.ask_user_question.skip_all": "全部跳过", + "ai.ask_user_question.skipped": "已跳过", + "ai.auth_secret.unable_to_load_secrets": "无法加载密钥", + "ai.aws_bedrock.always_run_automatically": "始终自动运行", + "ai.aws_bedrock.credentials_expired_or_missing": "AWS 凭证已过期或缺失", + "ai.aws_bedrock.refresh_credentials": "刷新 AWS 凭证", + "ai.block.always_allow": "始终允许", + "ai.block.dont_show_again": "不再显示", + "ai.block.mcp_tool": "MCP 工具:{name}", + "ai.block.mcp_tool_with_input": "MCP 工具:{name}({input})", + "ai.block.navigate_to_repo_to_open_comments": "前往 {path} 以打开这些评论", + "ai.block.open_all_in_code_review": "在代码审查中全部打开", + "ai.block.open_in_code_review": "在代码审查中打开", + "ai.block.open_in_github": "在 GitHub 中打开", + "ai.block.review_changes": "审查更改", + "ai.block.rewind": "回退", + "ai.block.rewind_tooltip": "回退到此块之前", + "ai.block.thank_you_for_feedback": "感谢你的反馈!", + "ai.block.view_screenshot": "查看截图", + "ai.blocked.grep_or_file_glob": "可以搜索此目录中的文件吗?", + "ai.blocked.reading_files": "授予以下文件的访问权限?", + "ai.blocked.searching_codebase": "授予以下仓库的访问权限?", + "ai.blocked.write_to_running_command": "可以向这个正在运行的命令写入以下内容吗?", + "ai.blocklist.local_agent_task_sync_model.agent_error": "Agent 遇到错误", + "ai.blocklist.local_agent_task_sync_model.aws_bedrock_invalid": "{model_name} 的 AWS Bedrock 凭证已过期或无效。", + "ai.blocklist.local_agent_task_sync_model.cancelled_by_user": "用户已取消", + "ai.blocklist.local_agent_task_sync_model.context_window_exceeded": "已超过上下文窗口:{message}", + "ai.blocklist.local_agent_task_sync_model.conversation_blocked": "Agent 卡在等待用户确认以下操作:{blocked_action}", + "ai.blocklist.local_agent_task_sync_model.internal_warp_error": "Conversation 期间发生内部错误。请重试。", + "ai.blocklist.local_agent_task_sync_model.invalid_api_key": "{provider} 的 API key 无效。请在设置中更新 API key。", + "ai.blocklist.local_agent_task_sync_model.quota_limit": "你的团队额度已用完。请购买更多额度后继续。", + "ai.blocklist.local_agent_task_sync_model.server_overloaded": "Warp 暂时负载过高。请稍后重试。", + "ai.cli.agent_asking_take_control": "Agent 正在请求你接管控制。", + "ai.code_block.add_as_context": "添加为上下文", + "ai.code_block.run_in_terminal": "在终端中运行", + "ai.code_diff.accept_and_continue_with_agent": "接受并让 Agent 继续", + "ai.code_diff.edit_code_diff": "编辑代码 Diff", + "ai.code_diff.failed_to_save_file": "保存文件失败:{file_path}", + "ai.code_diff.file_name_deleted": "{file_name}(已删除)", + "ai.code_diff.file_name_new": "{file_name}(新建)", + "ai.code_diff.file_renamed_without_changes": "文件已重命名,无内容更改", + "ai.code_diff.hide_suggested_code_banners": "不再显示建议代码横幅", + "ai.code_diff.iterate_with_agent": "与 Agent 迭代", + "ai.code_diff.manage_suggested_code_banner_settings": "管理建议代码横幅设置", + "ai.code_diff.no_file_name": "无文件名", + "ai.code_diff.open_config": "打开配置", + "ai.code_diff.requested_edit": "请求的编辑", + "ai.code_diff.revert_failed": "无法还原对 {file_name} 的更改", + "ai.codebase_index.allow_automatic_indexing": "允许自动索引", + "ai.codebase_index.index_codebase": "索引代码库", + "ai.codebase_index.indexing_header": "正在索引代码库", + "ai.codebase_index.speedbump_header": "索引代码库?", + "ai.codebase_index.speedbump_text": "索引可帮助 Agent 快速理解上下文并提供更有针对性的解决方案。代码永远不会存储在服务器上。", + "ai.codebase_index.view_status": "查看状态", + "ai.command.accept": "接受", + "ai.command.auto_approve": "自动批准", + "ai.command.manage_agent_permissions": "管理 Agent 权限", + "ai.command.take_control": "接管控制", + "ai.command.take_over": "接手", + "ai.common.agent_waiting_for_instructions": "Agent 正在等待指令...", + "ai.common.attempting_resume_conversation": "正在尝试恢复对话...", + "ai.common.auto_approve_actions_for_task": "自动批准此任务的所有 Agent 操作", + "ai.common.auto_queue_next_prompt_tooltip": "Agent 响应时自动排队下一条提示", + "ai.common.auto_queue_on_tooltip": "自动排队已开启:你的下一条提示会被加入队列", + "ai.common.aws_credentials_expired_or_missing": "AWS 凭证已过期或缺失,模型:{model_name}。请刷新 AWS 凭证。", + "ai.common.copy_debug_id": "复制调试 ID", + "ai.common.credit_limit_reset": "你已达到额度上限。额度上限将在 {date} 重置。", + "ai.common.debug_information": "调试信息:{debug_info}", + "ai.common.elapsed_second_one": "1 秒", + "ai.common.elapsed_second_other": "{seconds} 秒", + "ai.common.error_apology": "抱歉,我无法完成该请求。", + "ai.common.exit_agent_input_tooltip": "退出 Agent 输入", + "ai.common.fast_forward_cloud_always_enabled": "云端 Agent 对话始终启用快进", + "ai.common.hide_agent_responses": "隐藏 Agent 响应", + "ai.common.hide_responses": "隐藏响应", + "ai.common.internal_warp_error": "Warp 内部错误。", + "ai.common.mermaid_diagram": "Mermaid 图表", + "ai.common.resume_when_network_restored": "网络连接恢复后将恢复对话...", + "ai.common.server_overloaded": "Warp 当前负载过高。请稍后重试。", + "ai.common.show_agent_responses": "显示 Agent 响应", + "ai.common.show_responses": "显示响应", + "ai.common.stop_agent_task_tooltip": "停止 Agent 任务", + "ai.common.take_over_command_tooltip": "接管命令控制", + "ai.common.turn_off_auto_approve_actions": "关闭所有 Agent 操作的自动批准", + "ai.conversation_status.blocked": "已阻塞", + "ai.conversation_status.cancelled": "已取消", + "ai.conversation_status.claimed": "已领取", + "ai.conversation_status.done": "已完成", + "ai.conversation_status.error": "错误", + "ai.conversation_status.failed": "失败", + "ai.conversation_status.in_progress": "进行中", + "ai.conversation_status.pending": "待处理", + "ai.conversation_status.queued": "已排队", + "ai.conversation.artifacts": "产物", + "ai.conversation.cloud_agent_run": "云端 Agent 运行", + "ai.conversation.continue_locally": "在本地继续", + "ai.conversation.continue_locally_tooltip": "在本地 fork 此对话", + "ai.conversation.conversation_id": "对话 ID", + "ai.conversation.created_by": "由 {name} 创建 • {time}", + "ai.conversation.created_on": "创建时间", + "ai.conversation.credits_used": "已用额度", + "ai.conversation.directory": "目录", + "ai.conversation.environment_details": "环境详情", + "ai.conversation.environment_name": "名称:{name}", + "ai.conversation.environment_setup_commands": "环境设置命令", + "ai.conversation.follow_up_existing_tooltip": "继续跟进现有对话", + "ai.conversation.id": "ID", + "ai.conversation.image": "镜像", + "ai.conversation.initial_query": "初始查询", + "ai.conversation.navigate_failed": "无法跳转到对话。", + "ai.conversation.open_in_github": "在 GitHub 中打开", + "ai.conversation.open_in_oz": "在 Oz 中打开", + "ai.conversation.run_id": "运行 ID", + "ai.conversation.run_metadata_access_denied_description": "你可以查看此共享会话,但运行元数据仅对有权访问此运行的用户可见。", + "ai.conversation.run_metadata_access_denied_title": "运行元数据不可用", + "ai.conversation.run_time": "运行时长", + "ai.conversation.view_in_oz": "在 Oz 中查看", + "ai.conversation.view_in_oz_tooltip": "在 Oz Web App 中查看此次运行", + "ai.execution_profiles.editor.ask_user_question_permission.label": "询问问题", + "ai.execution_profiles.editor.base_model.desc": "此模型是 Agent 的主要引擎。它负责大多数交互,并在需要规划或代码生成等任务时调用其他模型。Warp 可能会根据模型可用性,或为了对话总结等辅助任务自动切换到备用模型。", + "ai.execution_profiles.editor.base_model.label": "基础模型", + "ai.execution_profiles.editor.call_mcp_servers_permission.label": "调用 MCP 服务器", + "ai.execution_profiles.editor.command_allowlist_placeholder": "例如 ls .*", + "ai.execution_profiles.editor.command_allowlist.desc": "用于匹配可由 Oz 自动执行的命令的正则表达式。", + "ai.execution_profiles.editor.command_allowlist.label": "命令允许列表", + "ai.execution_profiles.editor.command_denylist_placeholder": "例如 rm .*", + "ai.execution_profiles.editor.command_denylist.desc": "用于匹配 Oz 始终需要请求权限才能执行的命令的正则表达式。", + "ai.execution_profiles.editor.command_denylist.label": "命令拒绝列表", + "ai.execution_profiles.editor.computer_use_model.desc": "Agent 控制你的电脑并通过鼠标移动、点击和键盘输入与图形应用交互时使用的模型。", + "ai.execution_profiles.editor.computer_use_model.label": "电脑控制模型", + "ai.execution_profiles.editor.context_window.desc": "基础模型的工作记忆,也就是它一次可以考虑的对话、代码和文档 token 数。更大的窗口支持更长的对话,并能在更大的代码库中给出更连贯的回复,但会增加延迟和计算成本。", + "ai.execution_profiles.editor.context_window.label": "上下文窗口", + "ai.execution_profiles.editor.default_profile": "默认", + "ai.execution_profiles.editor.default_profile_name_locked": "默认配置名称无法更改。", + "ai.execution_profiles.editor.delete_profile": "删除配置", + "ai.execution_profiles.editor.directory_allowlist_placeholder": "例如 ~/code-repos/repo", + "ai.execution_profiles.editor.directory_allowlist.desc": "授予 Agent 对指定目录的文件访问权限。", + "ai.execution_profiles.editor.directory_allowlist.label": "目录允许列表", + "ai.execution_profiles.editor.edit_profile": "编辑配置", + "ai.execution_profiles.editor.full_terminal_use_model.desc": "Agent 在数据库 shell、调试器、REPL 或开发服务器等交互式终端应用中运行时使用的模型,用于读取实时输出并向 PTY 写入命令。", + "ai.execution_profiles.editor.full_terminal_use_model.label": "完整终端使用模型", + "ai.execution_profiles.editor.header": "配置编辑器", + "ai.execution_profiles.editor.mcp_allowlist.desc": "允许 Oz 调用的 MCP 服务器。", + "ai.execution_profiles.editor.mcp_allowlist.label": "MCP 允许列表", + "ai.execution_profiles.editor.mcp_denylist.desc": "不允许 Oz 调用的 MCP 服务器。", + "ai.execution_profiles.editor.mcp_denylist.label": "MCP 拒绝列表", + "ai.execution_profiles.editor.mcp_server_fallback": "MCP 服务器", + "ai.execution_profiles.editor.models_section": "模型", + "ai.execution_profiles.editor.name_label": "名称", + "ai.execution_profiles.editor.permission_desc.agent_decides": "Agent 会选择最安全的方式:有把握时自行操作,不确定时请求批准。", + "ai.execution_profiles.editor.permission_desc.always_allow": "授予 Agent 完全自主权限,无需手动批准。", + "ai.execution_profiles.editor.permission_desc.always_ask": "Agent 执行任何操作前都必须获得明确批准。", + "ai.execution_profiles.editor.permission_desc.ask_user_question.always_ask": "Agent 可以提问,并且即使自动批准已开启,也会暂停等待你的回复。", + "ai.execution_profiles.editor.permission_desc.ask_user_question.ask_unless_auto_approve": "Agent 可以提问并暂停等待你的回复,但在自动批准开启时会自动继续。", + "ai.execution_profiles.editor.permission_desc.ask_user_question.never": "Agent 不会提问,会根据自己的最佳判断继续。", + "ai.execution_profiles.editor.permission_desc.computer_use.always_allow": "允许 Agent 无需批准即可自主使用电脑控制工具。", + "ai.execution_profiles.editor.permission_desc.computer_use.always_ask": "Agent 使用电脑控制工具前必须获得明确批准。", + "ai.execution_profiles.editor.permission_desc.computer_use.never": "电脑控制工具已禁用,Agent 无法使用。", + "ai.execution_profiles.editor.permission_desc.run_agents.always_allow": "允许 Agent 无需批准即可自主运行子 Agent。", + "ai.execution_profiles.editor.permission_desc.run_agents.always_ask": "Agent 运行子 Agent 前必须获得明确批准。", + "ai.execution_profiles.editor.permission_desc.run_agents.never": "Agent 不能运行子 Agent,run_agents 工具将不可用。", + "ai.execution_profiles.editor.permission_desc.unknown": "未知设置。", + "ai.execution_profiles.editor.permission_desc.write_to_pty.always_ask": "Agent 与运行中的命令交互时始终需要请求权限。", + "ai.execution_profiles.editor.permission_desc.write_to_pty.ask_on_first_write": "Agent 首次需要与运行中的命令交互时会请求权限。之后,在该命令的剩余生命周期内会自动继续。", + "ai.execution_profiles.editor.permission.agent_decides": "由 Agent 决定", + "ai.execution_profiles.editor.permission.always_allow": "始终允许", + "ai.execution_profiles.editor.permission.always_ask": "始终询问", + "ai.execution_profiles.editor.permission.apply_code_diffs": "应用代码 diff", + "ai.execution_profiles.editor.permission.ask_on_first_write": "首次写入时询问", + "ai.execution_profiles.editor.permission.ask_questions": "询问问题", + "ai.execution_profiles.editor.permission.ask_unless_auto_approve": "除自动批准外均询问", + "ai.execution_profiles.editor.permission.call_mcp_servers": "调用 MCP 服务器", + "ai.execution_profiles.editor.permission.computer_use": "电脑控制", + "ai.execution_profiles.editor.permission.execute_commands": "执行命令", + "ai.execution_profiles.editor.permission.interact_with_running_commands": "与运行中的命令交互", + "ai.execution_profiles.editor.permission.never": "从不", + "ai.execution_profiles.editor.permission.never_ask": "从不询问", + "ai.execution_profiles.editor.permission.read_files": "读取文件", + "ai.execution_profiles.editor.permission.run_orchestrated_agents": "运行编排 Agent", + "ai.execution_profiles.editor.permissions_section": "权限", + "ai.execution_profiles.editor.plan_auto_sync.desc": "此 Agent 创建的计划会自动添加并同步到 Warp Drive。", + "ai.execution_profiles.editor.plan_auto_sync.label": "计划自动同步", + "ai.execution_profiles.editor.profile_name_placeholder": "例如 \"YOLO code\"", + "ai.execution_profiles.editor.select_mcp_servers": "选择 MCP 服务器", + "ai.execution_profiles.editor.upgrade_footer.prefix": "免费计划无法使用前沿模型。", + "ai.execution_profiles.editor.upgrade_footer.upgrade": "升级", + "ai.execution_profiles.editor.web_search.desc": "Agent 可在有助于完成任务时使用网页搜索。", + "ai.execution_profiles.editor.web_search.label": "调用网页工具", + "ai.execution_profiles.editor.workspace_override_tooltip": "此选项由组织设置强制执行,无法自定义。", + "ai.host_picker.custom_host": "自定义主机…", + "ai.inline_agent.agent_blocked": "Agent 需要你的许可才能继续", + "ai.inline_agent.agent_in_control": "Agent 正在控制", + "ai.inline_agent.prompt_interact_with_command": "提示 Agent 与 `{command}` 交互", + "ai.inline_agent.prompt_interact_with_running_command": "提示 Agent 与正在运行的命令交互", + "ai.inline_agent.user_in_control": "用户正在控制", + "ai.inline_agent.waiting_for_command_exit": "Agent 正在等待命令退出", + "ai.inline_agent.waiting_on_instructions": "Agent 正在等待指令", + "ai.llms.custom_endpoint": "自定义端点", + "ai.loading.adjusting_tasks": "正在调整任务...", + "ai.loading.calling_mcp_tool": "正在调用 \"{name}\" MCP 工具...", + "ai.loading.creating_diff": "正在创建 diff...", + "ai.loading.executing_command": "正在执行命令...", + "ai.loading.fetching_pr_comments": "正在获取 PR 评论...", + "ai.loading.finding_files": "正在查找文件...", + "ai.loading.generating_fix": "正在生成修复...", + "ai.loading.generating_plan": "正在生成计划...", + "ai.loading.grepping": "正在 grep 搜索...", + "ai.loading.next_check_in": "下次检查还有 {time}", + "ai.loading.preparing_question": "正在准备问题...", + "ai.loading.reading_files": "正在读取文件...", + "ai.loading.reading_mcp_resource": "正在读取 \"{name}\" MCP 资源...", + "ai.loading.searching_codebase": "正在搜索代码库...", + "ai.loading.searching_web": "正在搜索网页...", + "ai.loading.searching_web_for": "正在搜索网页:\"{query}\"", + "ai.loading.summarizing_command_output": "正在总结命令输出...", + "ai.loading.summarizing_conversation": "正在总结对话...", + "ai.loading.updating_plan": "正在更新计划...", + "ai.loading.waiting_for_command_exit": "正在等待命令退出...", + "ai.loading.warping": "Warping...", + "ai.loading.writing_command_input": "正在写入命令输入...", + "ai.mcp.path_required_to_launch_server": "启动 MCP 服务器需要 PATH。请打开一个新的终端会话以自动填充 PATH。", + "ai.mcp.templatable_manager.auth_success_toast": "{server_name} MCP 服务器认证成功", + "ai.mcp.templatable_manager.error.auth_not_supported": "服务器需要认证,但目前尚不支持。", + "ai.mcp.templatable_manager.error.cancelled": "操作已取消,原因:{reason}", + "ai.mcp.templatable_manager.error.client_initialize": "初始化客户端失败:{error}", + "ai.mcp.templatable_manager.error.generic": "错误:{error}", + "ai.mcp.templatable_manager.error.installation_not_found": "未找到安装项", + "ai.mcp.templatable_manager.error.parse_server_failed": "解析 MCP 服务器失败:{error}", + "ai.mcp.templatable_manager.error.path_not_available": "PATH 不可用", + "ai.mcp.templatable_manager.error.runtime": "运行时错误:{error}", + "ai.mcp.templatable_manager.error.server_initialize": "初始化服务器失败:{error}", + "ai.mcp.templatable_manager.error.server_returned_error": "服务器返回错误。请查看服务器日志了解详情。", + "ai.mcp.templatable_manager.error.service": "服务错误:{error}", + "ai.mcp.templatable_manager.error.template_contains_no_servers": "模板不包含任何服务器", + "ai.mcp.templatable_manager.error.timeout": "连接在 {seconds} 秒后超时。服务器可能无响应。", + "ai.mcp.templatable_manager.error.transport_closed": "连接意外关闭。服务器可能已崩溃。", + "ai.mcp.templatable_manager.error.transport_creation": "建立连接失败:{error}", + "ai.mcp.templatable_manager.error.transport_send": "向服务器发送数据失败。连接可能已丢失。", + "ai.mcp.templatable_manager.error.unexpected_response": "服务器返回了意外响应。服务器可能不兼容。", + "ai.mcp.templatable_manager.error.unexpected_status_code": "意外的状态码:{status}", + "ai.mcp.templatable_manager.error.unknown_reason": "未知原因", + "ai.orchestration_config.base_model_helper": "所有 Agent 将使用的主要模型。", + "ai.orchestration_config.description": "将这项工作拆分成由多个 Agent 协调的任务流。", + "ai.orchestration_config.header": "使用编排", + "ai.orchestration.agent": "Agent", + "ai.orchestration.agent_cancelled_suffix": " 已取消。", + "ai.orchestration.agent_harness": "Agent Harness", + "ai.orchestration.agent_location": "Agent 位置", + "ai.orchestration.api_key": "API 密钥", + "ai.orchestration.back_to_parent_conversation": "返回父对话", + "ai.orchestration.base_model": "基础模型", + "ai.orchestration.cloud": "云端", + "ai.orchestration.default_model": "默认模型", + "ai.orchestration.delete_agent": "删除 Agent", + "ai.orchestration.disabled_by_admin": "已被管理员禁用", + "ai.orchestration.empty_environment": "空环境", + "ai.orchestration.environment": "环境", + "ai.orchestration.failed_to_start_agent": "启动 agent 失败 ", + "ai.orchestration.failed_to_start_remote_agent": "启动远程 agent 失败 ", + "ai.orchestration.focus_pane": "聚焦窗格", + "ai.orchestration.generating_title": "正在生成标题...", + "ai.orchestration.host": "主机", + "ai.orchestration.kill_agent": "终止 Agent", + "ai.orchestration.local": "本地", + "ai.orchestration.new_api_key": "新建 API 密钥…", + "ai.orchestration.open_in_new_pane": "在新窗格中打开", + "ai.orchestration.open_in_new_tab": "在新标签页中打开", + "ai.orchestration.orchestrator": "编排器", + "ai.orchestration.parent_conversation": "父对话", + "ai.orchestration.recommend_create_environment": "建议为云端 Agent 创建一个环境。", + "ai.orchestration.recommend_select_environment": "建议为云端 Agent 选择一个环境。", + "ai.orchestration.select_api_key_for_harness": "请选择此 harness 的 API 密钥以继续。", + "ai.orchestration.send_message_cancelled": "发送给 {recipients} 的消息已取消。", + "ai.orchestration.send_message_failed": "发送消息给 {recipients} 失败:{error}", + "ai.orchestration.sending_message_to": "正在发送消息给 ", + "ai.orchestration.skip_advanced": "跳过(高级)", + "ai.orchestration.start_agent": "启动 agent ", + "ai.orchestration.start_remote_agent": "启动远程 agent ", + "ai.orchestration.started_agent": "已启动 agent ", + "ai.orchestration.started_locally_suffix": "(本地)。", + "ai.orchestration.started_remotely_suffix": "(远程)。", + "ai.orchestration.starting_agent": "正在启动 agent ", + "ai.orchestration.starting_remote_agent": "正在启动远程 agent ", + "ai.orchestration.stop_agent": "停止 Agent", + "ai.orchestration.unknown_agent": "未知 agent", + "ai.orchestration.view_in_oz": "在 Oz 中查看", + "ai.output.always_allow_file_access_for_coding_tasks": "始终允许编码任务访问文件", + "ai.output.always_allow_file_access_for_this_repo": "始终允许此仓库的文件访问", + "ai.output.always_allow_oz_read_only_commands": "始终允许 Oz 执行只读命令(依赖模型判断)", + "ai.output.artifact_description": "描述:{description}", + "ai.output.artifact_status_upload_failed": "状态:上传失败:{error}", + "ai.output.artifact_status_uploaded": "状态:已上传产物 {artifact_uid}", + "ai.output.artifact_uploaded_file": "已上传文件:{filepath}", + "ai.output.bad_response": "回答不好", + "ai.output.cancelled_file_search_patterns_in_path": "已取消在 {path} 中搜索匹配以下模式的文件", + "ai.output.cancelled_grep_patterns_in_path": "已取消在 {path} 中 grep 搜索以下模式", + "ai.output.check_now": " · 立即检查", + "ai.output.check_now_tooltip": "让 agent 立即检查此命令,跳过等待计时。", + "ai.output.comment_addressed": "评论已处理:\"{content}\"", + "ai.output.continue_conversation": "继续对话", + "ai.output.continue_current_conversation": "继续当前对话", + "ai.output.continuing_current_conversation": "继续当前对话", + "ai.output.conversation_search_grepping_patterns": "正在 grep 搜索模式", + "ai.output.conversation_search_grepping_patterns_with_patterns": "正在 grep 搜索模式:{patterns}", + "ai.output.conversation_search_listing_messages": "正在列出消息", + "ai.output.conversation_search_reading_messages": "正在读取 {count} 条消息", + "ai.output.conversation_summarized": "对话已总结", + "ai.output.could_not_apply_changes_to_file": "无法将更改应用到文件。", + "ai.output.current_directory": "当前目录", + "ai.output.debug_output": "调试输出", + "ai.output.failed_to_read_files": "读取文件失败", + "ai.output.find_file_patterns_in_path": "在 {path} 中查找匹配以下模式的文件", + "ai.output.finding_file_patterns_in_path": "正在 {path} 中查找匹配以下模式的文件", + "ai.output.finding_files_matching": "正在查找匹配的文件 ", + "ai.output.fork_conversation": "派生对话", + "ai.output.good_response": "回答不错", + "ai.output.grant_access_upload_artifact": "授予上传此产物的访问权限?", + "ai.output.grep_for": "Grep 搜索 ", + "ai.output.grep_patterns_in_path": "在 {path} 中 grep 搜索以下模式", + "ai.output.grepping_for": "正在 grep 搜索 ", + "ai.output.grepping_patterns_in_path": "正在 {path} 中 grep 搜索以下模式", + "ai.output.in_path": ",位置:{path}", + "ai.output.in_path_cancelled": ",位置:{path},已取消", + "ai.output.manage_ai_autonomy_permissions": "管理 AI 自主权限", + "ai.output.new_conversation_prompt": "话题似乎变了。要开始新对话吗?", + "ai.output.new_conversation_started": "已开始新对话", + "ai.output.new_conversation_suggestion_cancelled": "新对话建议已取消", + "ai.output.no_relevant_files_found": "未找到相关文件。", + "ai.output.ok_read_mcp_resource": "可以读取此 MCP 资源吗?", + "ai.output.ok_use_computer_control": "可以为此任务使用电脑控制吗?", + "ai.output.open_skill": "打开技能", + "ai.output.references": "引用", + "ai.output.refunded_credits": "很抱歉这次互动体验不佳。我们已退还你 {count} 个额度。感谢你的反馈!", + "ai.output.refunded_one_credit": "很抱歉这次互动体验不佳。我们已退还你 1 个额度。感谢你的反馈!", + "ai.output.response_wont_count_usage": "此回复不会计入你的用量。", + "ai.output.resume_conversation": "恢复对话", + "ai.output.search_for_files_matching": "搜索匹配的文件 ", + "ai.output.search_in_path": "在 {path} 中搜索", + "ai.output.search_in_path_cancelled": "已取消在 {path} 中搜索", + "ai.output.search_in_path_failed": "在 {path} 中搜索失败", + "ai.output.search_in_path_failed_not_indexed": "在 {path} 中搜索失败,因为代码库尚未建立索引", + "ai.output.searching_in_path": "正在 {path} 中搜索", + "ai.output.show_credit_usage_details": "显示额度用量详情", + "ai.output.start_new_conversation": "开始新对话", + "ai.output.stopped_task": "任务已停止", + "ai.output.stopped_task_indexed": "已停止任务 {current}/{total}: \"{title}\"", + "ai.output.stopped_task_named": "已停止任务:\"{title}\"", + "ai.output.suggestion_edited_in_another_tab": "此建议正在另一个标签页中编辑。", + "ai.output.suggestions": "建议:", + "ai.output.thinking": "正在思考", + "ai.output.this_conversation": "此对话", + "ai.output.thought_for": "思考了 {duration}", + "ai.output.upload_artifact": "上传产物:{file_path}", + "ai.pending_prompt.queued": "已排队", + "ai.pending_prompt.remove": "移除排队提示词", + "ai.pending_prompt.send_now": "立即发送", + "ai.prompt_alert.add_credits": "添加额度", + "ai.prompt_alert.anonymous_limit_hard_gate_primary": "已达到上限 -", + "ai.prompt_alert.ask_admin_enable_overages": ",请让团队管理员启用超额使用", + "ai.prompt_alert.ask_admin_increase_overages": ",请让团队管理员提高超额使用上限", + "ai.prompt_alert.compare_plans": "比较套餐", + "ai.prompt_alert.contact_support": "联系支持", + "ai.prompt_alert.contact_team_admin": ",请联系团队管理员", + "ai.prompt_alert.delinquent_primary": "因付款问题已受限", + "ai.prompt_alert.enable_analytics": "启用分析", + "ai.prompt_alert.enable_premium_overages": "启用高级超额使用", + "ai.prompt_alert.increase_monthly_spend_limit": "提高每月消费上限", + "ai.prompt_alert.manage_billing": "管理账单", + "ai.prompt_alert.no_connection": "没有互联网连接", + "ai.prompt_alert.out_of_credits": "额度已用完", + "ai.prompt_alert.sign_up_for_more_credits": "注册以获取更多 AI 额度", + "ai.prompt_alert.telemetry_disabled_primary": "如需使用 AI 功能,", + "ai.prompt_alert.upgrade": "升级", + "ai.prompt_alert.upgrade_to_build": "升级到 Build", + "ai.prompt_alert.use_own_api_keys": "使用你自己的 API 密钥", + "ai.requested_command.agent_error_take_over": "Agent 遇到问题。请接管控制。", + "ai.requested_command.agent_monitoring_command": "Agent 正在监控命令...", + "ai.requested_command.agent_needs_input": "Agent 需要你的输入才能继续", + "ai.requested_command.always_ask_permission": "你的配置已设置为执行命令前始终请求权限。", + "ai.requested_command.copied_from": "复制自", + "ai.requested_command.derived_from": "派生自", + "ai.requested_command.edit_requested_command": "编辑请求的命令", + "ai.requested_command.error_formatting_json": "格式化 JSON 时出错", + "ai.requested_command.error_prefix": "错误:{error}", + "ai.requested_command.generating_command": "正在生成命令...", + "ai.requested_command.manage_command_execution_setting": "管理命令执行设置", + "ai.requested_command.ok_call_mcp_tool": "可以调用这个 MCP 工具吗?", + "ai.requested_command.ok_run_command": "可以运行此命令并读取输出吗?", + "ai.requested_command.paused_agent_user_in_control": "已暂停 Agent。用户正在控制。", + "ai.requested_command.response_with_result": "{command}\n\n响应:{result}", + "ai.requested_command.tool_call_cancelled": "工具调用已取消", + "ai.requested_command.user_in_control": "用户正在控制。", + "ai.requested_command.user_in_control_short": "用户正在控制", + "ai.requested_command.viewing_command_detail": "正在查看命令详情", + "ai.requested_command.viewing_mcp_tool_detail": "正在查看 MCP 工具调用详情", + "ai.rules.add_rule": "添加规则", + "ai.rules.add_rule_title": "添加规则", + "ai.rules.delete_rule": "删除规则", + "ai.rules.description": "规则通过提供结构化指南来增强 Agent,帮助保持一致性、执行最佳实践,并适配特定工作流,包括代码库或更广泛的任务。", + "ai.rules.description_placeholder": "例如:在 Rust 中不要使用 unwrap", + "ai.rules.disabled_banner_link": "重新开启", + "ai.rules.disabled_banner_prefix": "你的规则已禁用,不会作为会话上下文使用。你可以", + "ai.rules.disabled_banner_suffix": "。", + "ai.rules.edit_rule": "编辑规则", + "ai.rules.edit_rule_title": "编辑规则", + "ai.rules.editing_disabled_offline": "离线时无法编辑。", + "ai.rules.header": "规则", + "ai.rules.initialize_project": "初始化项目", + "ai.rules.manage": "管理规则", + "ai.rules.name_placeholder": "例如:Rust 规则", + "ai.rules.offline_banner": "你处于离线状态。部分规则将为只读。", + "ai.rules.rule": "规则", + "ai.rules.scope.global": "全局", + "ai.rules.scope.project_based": "基于项目", + "ai.rules.search_placeholder": "搜索规则", + "ai.rules.suggested_rule": "建议规则", + "ai.rules.zero_state.global": "在上方添加规则,或将规则放到 ~/.agents/AGENTS.md 以应用到所有项目。", + "ai.rules.zero_state.project_based": "为项目生成 WARP.md 规则文件后,它会显示在这里。", + "ai.run_agents_card.title": "可以为此任务启动更多 Agent 吗?", + "ai.run_agents.accept_without_orchestration": "不使用编排接受", + "ai.run_agents.agents_count": "Agent({count})", + "ai.run_agents.cancelled": "启动 Agent 已取消", + "ai.run_agents.configuring": "正在配置 Agent...", + "ai.run_agents.failed_to_start_orchestration": "启动编排失败", + "ai.run_agents.failed_to_start_orchestration_with_error": "启动编排失败:{error}", + "ai.run_agents.orchestration_disabled": "编排当前已禁用。请在计划卡片中重新启用后再启动。", + "ai.run_agents.orchestration_disabled_reason": "编排当前已禁用。请在计划卡片中重新启用后再启动。({reason})", + "ai.run_agents.spawned_one": "已启动 1 个 Agent", + "ai.run_agents.spawned_other": "已启动 {count} 个 Agent", + "ai.run_agents.spawned_partial": "已启动 {launched}/{total} 个 Agent", + "ai.run_agents.spawning_one": "正在启动 1 个 Agent...", + "ai.run_agents.spawning_other": "正在启动 {count} 个 Agent...", + "ai.search_codebase.cancelled": "已取消搜索“{query}”", + "ai.search_codebase.cancelled_in_repo": "已取消在 {repo} 中搜索“{query}”", + "ai.search_codebase.searched_codebase_for": "已在代码库中搜索“{query}”", + "ai.search_codebase.searched_codebase_for_in_repo": "已在 {repo} 的代码库中搜索“{query}”", + "ai.search_codebase.searched_for": "已搜索“{query}”", + "ai.search_codebase.searched_for_in_repo": "已在 {repo} 中搜索“{query}”", + "ai.search_codebase.searching_for": "正在代码库中搜索“{query}”", + "ai.search_codebase.searching_for_in_repo": "正在 {repo} 中搜索“{query}”", + "ai.search_results.urls": "URL", + "ai.settings.edit_api_keys": "编辑 API 密钥", + "ai.settings.invalid_api_key": "提供的 API 密钥无效", + "ai.status.authenticate_github": "验证 GitHub", + "ai.status.cloud_agent_run_cancelled": "云端 agent 运行已取消", + "ai.status.missing_github_authentication": "缺少 GitHub 认证。", + "ai.status.primary_model_failed": "主模型失败。正在使用备用模型重试。", + "ai.status.primary_model_failed_with_name": "主模型 ({model}) 失败。正在使用备用模型重试。", + "ai.status.setting_up_environment": "正在设置环境", + "ai.status.warping_with": "正在使用 {model} 进行 Warp。", + "ai.status.warping_with_another_model": "正在使用另一个模型进行 Warp。", + "ai.suggested_workflow.prompt": "提示词", + "ai.suggestion.add_rule": "添加规则:{rule}", + "ai.suggestion.suggested_prompt": "建议提示词:\n{prompt}", + "ai.summarization.cancel": "取消总结", + "ai.summarization.cancel_dialog.body": "摘要生成正在运行。如果现在取消,请求可能仍会产生费用,当前进度会丢失,重新开始也会花费更长时间。\n\n确定要取消吗?", + "ai.summarization.cancel_dialog.title": "取消摘要生成?", + "ai.summarization.continue": "继续总结", + "ai.telemetry.description": "我们可能会收集某些控制台交互,以改进 Warp 的 AI 能力。你可以随时选择退出。", + "ai.telemetry.help_improve_warp": "帮助改进 Warp。", + "ai.telemetry.manage_privacy_settings": "管理隐私设置", + "ai.telemetry.policy_updated": "我们已更新遥测政策。", + "ai.todos.completed": "已完成 {title}", + "ai.todos.completed_indexed": "已完成 {title} ({current}/{total})", + "ai.todos.completed_item_separator": "、{title}", + "ai.todos.completed_item_separator_indexed": "、{title} ({current}/{total})", + "ai.todos.outdated": "已过期", + "ai.todos.tasks": "任务", + "ai.usage.call_one": "{count} 次调用", + "ai.usage.call_other": "{count} 次调用", + "ai.usage.command_one": "{count} 条命令", + "ai.usage.command_other": "{count} 条命令", + "ai.usage.commands_executed": "已执行命令", + "ai.usage.context_window_used": "已用上下文窗口", + "ai.usage.credit_one": "{count} 个额度", + "ai.usage.credit_other": "{count} 个额度", + "ai.usage.credits_spent": "已用额度", + "ai.usage.credits_spent_last_response": "已用额度(上一条回复)", + "ai.usage.credits_spent_total": "已用额度(总计)", + "ai.usage.diffs_applied": "已应用 diff", + "ai.usage.file_one": "{count} 个文件", + "ai.usage.file_other": "{count} 个文件", + "ai.usage.files_changed": "已更改文件", + "ai.usage.full_terminal_model_tooltip": "你可以在 AI 设置页面更改完整终端使用的模型", + "ai.usage.hide_details": "隐藏详情", + "ai.usage.last_response_time": "上一条回复耗时", + "ai.usage.models": "模型", + "ai.usage.models_with_category": "模型({category})", + "ai.usage.seconds": "{seconds} 秒", + "ai.usage.show_more": "再显示 {count} 个", + "ai.usage.summary": "用量摘要", + "ai.usage.time_to_first_token": "首个 token 耗时", + "ai.usage.tool_call_summary": "工具调用摘要", + "ai.usage.tool_calls": "工具调用", + "ai.usage.total_agent_response_time": "Agent 总回复耗时", + "ai.usage.total_time_including_tool_calls": "总耗时(包括工具调用)", + "ai.usage.view_details": "查看详情", + "ai.web_fetch.fetched_all": "已获取 {count} 个网页", + "ai.web_fetch.fetched_partial": "已获取 {successful}/{total} 个网页", + "ai.web_fetch.fetching": "正在获取 {count} 个网页...", + "ai.web_fetch.no_urls_fetched": "没有获取到 URL", + "ai.web_search.no_urls_found": "没有找到 URL", + "ai.web_search.searched": "已搜索网页", + "ai.web_search.searched_for": "已在网页中搜索“{query}”", + "ai.web_search.searching": "正在搜索网页", + "ai.web_search.searching_for": "正在网页中搜索“{query}”", + "ai.zero_state.cloud_agent.title": "新的 Oz 云端 Agent 对话", + "ai.zero_state.cloud_mode.description": "在隔离的云环境中运行你的 agent 任务。", + "ai.zero_state.cloud_mode.docs_prefix": "使用云端 agent 并行运行任务、构建可自主运行的 agent,并随时随地查看 agent 进展。", + "ai.zero_state.local_agent.title": "新的 Oz Agent 对话", + "ai.zero_state.local_mode.description": "在下方发送 prompt 以开始新对话", + "ai.zero_state.local_mode.description_with_location": "在下方发送 prompt,以在 `{location}` 中开始新对话", + "ai.zero_state.oz_updates_section_header": "Oz 新功能", + "ai.zero_state.recent_activity": "最近活动", + "ai.zero_state.view_changelog": "查看更新日志", + "app_menu.action.activate_next_pane": "激活下一个窗格", + "app_menu.action.activate_previous_pane": "激活上一个窗格", + "app_menu.action.add_cursor_above": "在上方添加光标", + "app_menu.action.add_cursor_below": "在下方添加光标", + "app_menu.action.add_next_occurrence": "添加下一个匹配项", + "app_menu.action.ai_search": "AI 搜索", + "app_menu.action.attach_selection_as_agent_context": "将所选内容附加为 Agent 上下文", + "app_menu.action.clear_blocks": "清除块", + "app_menu.action.clear_editor": "清空编辑器", + "app_menu.action.close_current_session": "关闭当前会话", + "app_menu.action.close_other_tabs": "关闭其他标签页", + "app_menu.action.close_tab": "关闭标签页", + "app_menu.action.close_tabs_right": "关闭右侧标签页", + "app_menu.action.close_window": "关闭窗口", + "app_menu.action.command_palette": "命令面板", + "app_menu.action.command_search": "命令搜索", + "app_menu.action.configure_keybindings": "配置快捷键", + "app_menu.action.copy": "复制", + "app_menu.action.copy_block": "复制块", + "app_menu.action.copy_block_command": "复制块命令", + "app_menu.action.copy_block_output": "复制块输出", + "app_menu.action.create_block_permalink": "创建块永久链接", + "app_menu.action.cut": "剪切", + "app_menu.action.cycle_next_session": "下一个会话", + "app_menu.action.cycle_previous_session": "上一个会话", + "app_menu.action.decrease_font_size": "减小字体大小", + "app_menu.action.decrease_zoom": "缩小", + "app_menu.action.disable_sync_terminal_inputs": "停用输入同步", + "app_menu.action.files_palette": "文件面板", + "app_menu.action.find": "查找", + "app_menu.action.find_within_block": "在块内查找", + "app_menu.action.focus_input": "聚焦输入框", + "app_menu.action.go_to_line": "跳转到行", + "app_menu.action.history": "命令历史", + "app_menu.action.increase_font_size": "增大字体大小", + "app_menu.action.increase_zoom": "放大", + "app_menu.action.launch_config_palette": "启动配置面板", + "app_menu.action.move_tab_left": "向左移动标签页", + "app_menu.action.move_tab_right": "向右移动标签页", + "app_menu.action.navigation_palette": "导航面板", + "app_menu.action.new_agent_mode_pane": "新建 Agent 窗格", + "app_menu.action.new_file": "新建文件", + "app_menu.action.new_personal_ai_prompt": "新建个人 AI Prompt", + "app_menu.action.new_personal_env_vars": "新建个人环境变量", + "app_menu.action.new_personal_notebook": "新建个人笔记本", + "app_menu.action.new_personal_workflow": "新建个人工作流", + "app_menu.action.new_team_ai_prompt": "新建团队 AI Prompt", + "app_menu.action.new_team_env_vars": "新建团队环境变量", + "app_menu.action.new_team_notebook": "新建团队笔记本", + "app_menu.action.new_team_workflow": "新建团队工作流", + "app_menu.action.open_ai_fact_collection": "打开 AI 规则", + "app_menu.action.open_mcp_server_collection": "打开 MCP 服务器", + "app_menu.action.open_repository": "打开仓库", + "app_menu.action.open_team_settings": "团队设置", + "app_menu.action.paste": "粘贴", + "app_menu.action.redo": "重做", + "app_menu.action.refer_a_friend": "推荐好友", + "app_menu.action.rename_tab": "重命名标签页", + "app_menu.action.reset_font_size": "重置字体大小", + "app_menu.action.reset_zoom": "重置缩放", + "app_menu.action.save_current_config": "保存当前配置", + "app_menu.action.scroll_to_bottom_of_selected_blocks": "滚动到所选块底部", + "app_menu.action.scroll_to_top_of_selected_blocks": "滚动到所选块顶部", + "app_menu.action.search_drive": "搜索 Drive", + "app_menu.action.select_all": "全选", + "app_menu.action.select_all_blocks": "选择所有块", + "app_menu.action.select_block_above": "选择上方块", + "app_menu.action.select_block_below": "选择下方块", + "app_menu.action.share_current_session": "共享当前会话", + "app_menu.action.share_pane_contents": "共享窗格内容", + "app_menu.action.show_about_warp": "关于 Warp", + "app_menu.action.show_appearance": "外观", + "app_menu.action.show_settings": "设置", + "app_menu.action.split_pane_down": "向下拆分窗格", + "app_menu.action.split_pane_left": "向左拆分窗格", + "app_menu.action.split_pane_right": "向右拆分窗格", + "app_menu.action.split_pane_up": "向上拆分窗格", + "app_menu.action.toggle_bookmark_block": "收藏块", + "app_menu.action.toggle_conversation_list_view": "Agent 会话", + "app_menu.action.toggle_global_search": "全局搜索", + "app_menu.action.toggle_keybindings_page": "键盘快捷键", + "app_menu.action.toggle_maximize_pane": "切换最大化窗格", + "app_menu.action.toggle_project_explorer": "项目浏览器", + "app_menu.action.toggle_resource_center": "资源中心", + "app_menu.action.toggle_sync_all_terminal_inputs": "同步所有标签页中的输入", + "app_menu.action.toggle_sync_current_tab_terminal_inputs": "同步当前标签页中的输入", + "app_menu.action.toggle_warp_drive": "Warp Drive", + "app_menu.action.undo": "撤销", + "app_menu.action.view_changelog": "查看更新日志", + "app_menu.action.view_shared_blocks": "查看共享块", + "app_menu.action.workflows": "工作流", + "app_menu.app.debug": "调试", + "app_menu.app.log_out": "退出登录", + "app_menu.app.preferences": "偏好设置", + "app_menu.app.privacy_policy": "隐私政策...", + "app_menu.app.set_default_terminal": "将 Warp 设为默认终端", + "app_menu.blocks.hide_in_band_command_blocks": "隐藏带内命令块", + "app_menu.blocks.hide_initialization_block": "隐藏初始化块", + "app_menu.blocks.hide_ssh_command_blocks": "隐藏 Warp 化 SSH 块", + "app_menu.blocks.show_in_band_command_blocks": "显示带内命令块", + "app_menu.blocks.show_initialization_block": "显示初始化块", + "app_menu.blocks.show_ssh_command_blocks": "显示 Warp 化 SSH 块", + "app_menu.debug.create_anonymous_user": "创建匿名用户", + "app_menu.debug.disable_in_band_generators": "为新会话停用带内生成器", + "app_menu.debug.disable_pty_recording": "停用 PTY 录制模式 (warp.pty.recording)", + "app_menu.debug.disable_shell_debug_mode": "为新会话停用 Shell 调试模式 (-x)", + "app_menu.debug.enable_in_band_generators": "为新会话启用带内生成器", + "app_menu.debug.enable_pty_recording": "启用 PTY 录制模式 (warp.pty.recording)", + "app_menu.debug.enable_shell_debug_mode": "为新会话启用 Shell 调试模式 (-x)", + "app_menu.debug.export_default_settings_csv": "将默认设置导出为 CSV 到主目录", + "app_menu.debug.manually_toggle_network_status": "手动切换网络状态", + "app_menu.dock.new_window": "新建窗口", + "app_menu.edit.copy_on_select": "在终端中选中即复制", + "app_menu.edit.synchronize_inputs": "同步输入", + "app_menu.edit.use_warp_prompt": "使用 Warp 提示符", + "app_menu.file.launch_configurations": "启动配置", + "app_menu.file.new_agent_tab": "新建 Agent 标签页", + "app_menu.file.new_terminal_tab": "新建终端标签页", + "app_menu.file.new_window": "新建窗口", + "app_menu.file.open_recent": "打开最近项目", + "app_menu.file.reopen_closed_session": "重新打开已关闭会话", + "app_menu.help.github_issues": "GitHub Issues...", + "app_menu.help.send_feedback": "发送反馈...", + "app_menu.help.slack_community": "Warp Slack 社区...", + "app_menu.help.warp_documentation": "Warp 文档...", + "app_menu.launch_config.save_new": "保存新的...", + "app_menu.top.ai": "AI", + "app_menu.top.blocks": "块", + "app_menu.top.drive": "Drive", + "app_menu.top.edit": "编辑", + "app_menu.top.file": "文件", + "app_menu.top.help": "帮助", + "app_menu.top.tab": "标签页", + "app_menu.top.view": "视图", + "app_menu.top.window": "窗口", + "app_menu.view.compact_mode": "紧凑模式", + "app_menu.view.toggle_focus_reporting": "切换焦点报告", + "app_menu.view.toggle_mouse_reporting": "切换鼠标报告", + "app_menu.view.toggle_scroll_reporting": "切换滚动报告", + "auth.a11y_open_browser": "按 Enter 打开浏览器以注册或登录。", + "auth.adjust_your": "你可以调整你的", + "auth.already_have_account": "已有账号?", + "auth.and_open": " 并打开", + "auth.auth_token": "身份验证令牌", + "auth.browser_auth_token": "浏览器身份验证令牌", + "auth.browser_not_launched_prefix": "如果浏览器没有打开,", + "auth.browser_sign_in_title": "在浏览器中登录以继续", + "auth.browser_sign_in_title_multiline": "在浏览器中登录\n以继续", + "auth.connect_account_ai": "连接你的账号以启用 AI 驱动的规划、编码和自动化。", + "auth.connect_account_drive": "连接你的账号,以在不同设备间保存和共享 notebook、workflow 等内容。", + "auth.copy_url": "复制 URL", + "auth.disable_ai_body": "有了 AI,Warp 会更好用。继续后,你将无法使用以下功能:", + "auth.disable_ai_confirm": "确定要禁用 AI 功能吗?", + "auth.disable_ai_features": "禁用 AI 功能", + "auth.disable_warp_drive": "禁用 Warp Drive", + "auth.disable_warp_drive_body": "Warp Drive 可帮助你跨设备保存 workflow 和知识,并与团队共享。继续后,你将无法使用以下功能:", + "auth.disable_warp_drive_confirm": "确定要禁用 Warp Drive 吗?", + "auth.enable_ai_features": "启用 AI 功能", + "auth.enable_warp_drive": "启用 Warp Drive", + "auth.enter_auth_token": "输入身份验证令牌", + "auth.errors.create_anonymous_user_failed": "创建匿名用户时遇到错误", + "auth.errors.redirect_url_missing_refresh_token": "收到的 URL 缺少 refresh token 查询参数:{url}", + "auth.errors.redirect_url_unexpected_host": "收到的 URL host 异常:{url}", + "auth.errors.unknown_anonymous_user_type": "无法转换未知匿名用户类型", + "auth.errors.web_user_handoff_failed": "Web 用户 handoff 失败:{error}", + "auth.get_started_ai": "开始使用 AI", + "auth.get_started_warp_drive": "开始使用 Warp Drive", + "auth.link_sso": "关联 SSO", + "auth.login_failure.copy_token_manually": "登录失败。请尝试从认证网页手动复制身份验证令牌,并将其粘贴到弹窗中。", + "auth.login_failure.invalid_auth_token": "输入到弹窗中的身份验证令牌无效。", + "auth.login_failure.invalid_redirect_url": "粘贴的重定向 URL 不是来自此应用。请点击下方按钮重试。", + "auth.login_failure.login_failed": "登录请求失败。", + "auth.login_failure.signup_failed": "注册请求失败。", + "auth.login_failure.troubleshooting_docs": "故障排查文档", + "auth.login_failure.troubleshooting_prefix": " 不是第一次遇到?请查看我们的", + "auth.logout.confirm": "是,退出登录", + "auth.logout.running_process": "你有 {count} 个进程正在运行。", + "auth.logout.running_processes": "你有 {count} 个进程正在运行。", + "auth.logout.shared_session": "你有 {count} 个共享会话。", + "auth.logout.shared_sessions": "你有 {count} 个共享会话。", + "auth.logout.show_running_processes": "显示正在运行的进程", + "auth.logout.title": "退出登录?", + "auth.logout.unsaved_file": "你有 {count} 个未保存文件。退出登录会导致该文件丢失。", + "auth.logout.unsaved_files": "你有 {count} 个未保存文件。退出登录会导致这些文件丢失。", + "auth.logout.unsynced_object": "你有 {count} 个未同步的 Warp Drive 对象。退出登录会导致该对象丢失。", + "auth.logout.unsynced_objects": "你有 {count} 个未同步的 Warp Drive 对象。退出登录会导致这些对象丢失。", + "auth.no_sign_in_now": "现在不想登录?", + "auth.offline_first_time": "你当前处于离线状态。首次使用 Warp 需要互联网连接。", + "auth.offline_info.paragraph_1": "Warp 的所有非云端功能都可以离线使用。", + "auth.offline_info.paragraph_2": "不过,首次使用 Warp 时需要在线,以启用 Warp 的 AI 和云端功能。", + "auth.offline_info.paragraph_3": "我们向所有用户提供云端功能,因此需要互联网连接来计量 AI 使用量、防止滥用,并将云对象关联到用户。如果你选择未登录使用 Warp,一个唯一 ID 会附加到匿名用户账号,以支持这些功能。", + "auth.offline_info.title": "离线使用 Warp", + "auth.open_page_manually": "并手动打开页面。", + "auth.opt_out_analytics_ai": "如果你想退出分析和 AI 功能,", + "auth.override.accessibility_description": "Warp 检测到来自网页浏览器的新登录。按 Escape 可取消,并继续以未登录状态使用 Warp。", + "auth.override.confirm_header": "删除个人 Warp Drive 对象和偏好设置?", + "auth.override.confirm_warning": "此操作无法撤销。", + "auth.override.description": "看起来你已通过网页浏览器登录了一个 Warp 账号。如果继续,此匿名会话中的所有个人 Warp Drive 对象和偏好设置都将被永久删除。", + "auth.override.export_data": "导出你的数据", + "auth.override.export_suffix": ",以便稍后导入。", + "auth.override.initial_header": "检测到新登录", + "auth.page_manually": "手动打开页面。", + "auth.paste_token_link": "点击此处粘贴浏览器中的令牌", + "auth.paste_token_modal.subtitle": "粘贴浏览器中的身份验证令牌以完成登录。", + "auth.paste_token_modal.title": "在下方粘贴你的身份验证令牌", + "auth.privacy_disclaimer_ai_prefix": "如果你想退出分析和 AI 功能,可以调整你的", + "auth.privacy_disclaimer_prefix": "如果你想退出分析,可以调整你的", + "auth.privacy_settings": "隐私设置", + "auth.privacy.cloud_conversation_off_description": "Agent 对话只会本地存储在你的机器上,登出后会丢失,且无法分享。注意:ambient agent 的对话数据仍会存储在云端。", + "auth.privacy.cloud_conversation_on_description": "Agent 对话可以与他人共享,并且在你登录不同设备时保留。此数据仅用于产品功能,Warp 不会将其用于分析。", + "auth.privacy.crash_reporting_description": "崩溃报告帮助 Warp 工程团队了解稳定性并提升性能。", + "auth.privacy.help_improve_warp": "帮助改进 Warp", + "auth.privacy.send_crash_reports": "发送崩溃报告", + "auth.privacy.store_ai_conversations": "在云端存储 AI 对话", + "auth.privacy.telemetry_description": "高层级功能使用数据帮助 Warp 产品团队确定路线图优先级。", + "auth.require_login_ai": "如需使用 Warp 的 AI 功能或与他人协作,请创建账号。", + "auth.require_login_drive_limit": "如需在 Warp Drive 中创建更多对象,请创建账号。", + "auth.require_login_share": "如需共享,请创建账号。", + "auth.sign_in": "登录", + "auth.sign_up": "注册", + "auth.sign_up_for_warp": "注册 Warp", + "auth.skip_for_now": "暂时跳过", + "auth.skip_login_confirm": "确定要跳过登录吗?", + "auth.skip_login_details_1": "你之后仍可注册,但部分功能(例如 AI)", + "auth.skip_login_details_2": "仅登录用户可用。", + "auth.sso_enabled_header": "你的组织已为你的账号启用 SSO", + "auth.sso_link_detail": "点击下方按钮,将你的 Warp 账号关联到 SSO provider。", + "auth.terms_of_service": "服务条款", + "auth.terms_prefix": "继续即表示你同意 Warp 的", + "auth.web_handoff.failed": "认证出错,请刷新页面", + "auth.web_handoff.loading": "正在加载...", + "auth.welcome_to_warp": "欢迎使用 Warp!", + "auth.yes_skip_login": "是,跳过登录", + "autoupdate.accessibility.install_relaunch_help": "使用命令面板安装并重新启动 Warp", + "autoupdate.accessibility.no_updates_available": "没有可用更新", + "autoupdate.accessibility.update_available": "有可用更新。", + "autoupdate.linux.compatible_tool_suffix": "或兼容工具安装,预填命令会为你更新 Warp。", + "autoupdate.linux.ensure_repo_function_suffix": "函数会确保 Warp 软件包仓库已启用,因为我们检测到你最近升级了发行版。", + "autoupdate.linux.install_and_relaunch_suffix": "以安装更新并重新启动 Warp。", + "autoupdate.linux.installed_using_prefix": "如果你使用", + "autoupdate.linux.one_time_repo_setup": "\n下面的命令包含一次性的 Warp 软件包仓库和 PGP 签名密钥配置。", + "autoupdate.linux.press_enter": "按 Enter", + "autoupdate.linux.report_issues": "请报告任何问题", + "autoupdate.linux.review_command_then": "\n请检查下面的命令,然后", + "autoupdate.linux.run_package_manager_to_update": "运行 {package_manager} 以更新", + "autoupdate.linux.the_prefix": "\n", + "billing.shared_objects.compare_plans": "比较套餐", + "billing.shared_objects.default_delinquent_admin_enterprise": "共享 Drive 对象因订阅付款问题已受限。\n\n请联系 support@warp.dev 以恢复访问权限。", + "billing.shared_objects.default_delinquent_admin_stripe": "共享 Drive 对象因订阅付款问题已受限。\n\n请更新你的付款信息以恢复访问权限。", + "billing.shared_objects.default_delinquent_non_admin": "共享 Drive 对象因订阅付款问题已受限。\n\n请联系团队管理员以恢复访问权限。", + "billing.shared_objects.default_free_admin": "Warp 免费计划包含的共享 Drive 对象数量有限。\n\n如需无限数量的共享 Drive 对象,请升级到付费计划。", + "billing.shared_objects.default_free_non_admin": "Warp 免费计划包含的共享 Drive 对象数量有限。\n\n如需无限数量的共享 Drive 对象,请联系团队管理员升级到付费计划。", + "billing.shared_objects.default_limit_reached_header": "已达到共享对象数量上限", + "billing.shared_objects.default_prosumer_admin": "Warp Pro 计划包含的共享 Drive 对象数量有限。\n\n如需无限数量的共享 Drive 对象,请升级到 Turbo 计划。", + "billing.shared_objects.default_prosumer_non_admin": "Warp Pro 计划包含的共享 Drive 对象数量有限。\n\n如需无限数量的共享 Drive 对象,请联系团队管理员升级到 Turbo 计划。", + "billing.shared_objects.delinquent_admin_enterprise": "共享{object_type}因订阅付款问题已受限。\n\n请联系 support@warp.dev 以恢复访问权限。", + "billing.shared_objects.delinquent_admin_stripe": "共享{object_type}因订阅付款问题已受限。\n\n请更新你的付款信息以恢复访问权限。", + "billing.shared_objects.delinquent_non_admin": "共享{object_type}因订阅付款问题已受限。\n\n请联系团队管理员以恢复访问权限。", + "billing.shared_objects.free_admin": "Warp 免费计划包含的共享{object_type}数量有限。\n\n如需无限数量的共享{object_type},请升级到付费计划。", + "billing.shared_objects.free_non_admin": "Warp 免费计划包含的共享{object_type}数量有限。\n\n如需无限数量的共享{object_type},请联系团队管理员升级到付费计划。", + "billing.shared_objects.limit_reached_title": "已达到共享{object_type}数量上限", + "billing.shared_objects.manage_billing": "管理账单", + "billing.shared_objects.prosumer_admin": "Warp Pro 计划包含的共享{object_type}数量有限。\n\n如需无限数量的共享{object_type},请升级到 Build 计划。", + "billing.shared_objects.prosumer_non_admin": "Warp Pro 计划包含的共享{object_type}数量有限。\n\n如需无限数量的共享{object_type},请联系团队管理员升级到 Build 计划。", + "billing.shared_objects.restricted_title": "共享{object_type}已受限", + "chip_configurator.left_side": "左侧", + "chip_configurator.restore_default": "恢复默认", + "chip_configurator.right_side": "右侧", + "chip_configurator.unknown": "未知", + "cloud_object.action_history.last_day": "过去一天内 {count} {action}", + "cloud_object.action_history.last_month": "过去一个月内 {count} {action}", + "cloud_object.action_history.last_week": "过去一周内 {count} {action}", + "cloud_object.action_history.last_year": "过去一年内 {count} {action}", + "cloud_object.action.run": "次运行", + "cloud_object.action.runs": "次运行", + "cloud_object.grab_edit_access.description": "如果你接管编辑控制权,当前编辑者将被强制切换到查看模式", + "cloud_object.grab_edit_access.edit_anyway": "仍然编辑", + "cloud_object.grab_edit_access.title": "此 Notebook 当前正在被编辑", + "cloud_object.history.edited": "编辑于 {time_ago}", + "cloud_object.history.edited_by": "{name} 编辑于 {time_ago}", + "cloud_object.history.last_edited_by": "最后由 {name} 编辑", + "cloud_object.permadeletion.days": "{days_left} 天后永久删除", + "cloud_object.permadeletion.one_day": "1 天后永久删除", + "cloud_object.space.personal": "个人", + "cloud_object.space.shared_with_me": "与我共享", + "cloud_object.space.team": "团队", + "cloud_object.toast.deleted_forever": "{count_objects_message}已永久删除", + "cloud_object.toast.empty_trash_failed": "清空废纸篓失败", + "cloud_object.toast.env_vars_conflict": "环境变量未能保存,因为你编辑期间已有其他更改。", + "cloud_object.toast.failed_create": "创建{object_name_lowercase}失败", + "cloud_object.toast.failed_delete": "删除{object_name_lowercase}失败", + "cloud_object.toast.failed_leave": "离开 {object_name} 失败", + "cloud_object.toast.failed_move": "移动{object_name_lowercase}失败", + "cloud_object.toast.failed_restore": "恢复{object_name_lowercase}失败", + "cloud_object.toast.failed_start_editing": "开始编辑{object_name_lowercase}失败", + "cloud_object.toast.failed_trash": "将{object_name_lowercase}移至废纸篓失败", + "cloud_object.toast.failed_update": "更新{object_name_lowercase}失败", + "cloud_object.toast.left": "已离开 {object_name}", + "cloud_object.toast.moved_to": "{object_name} 已移动到 {containing_object_name}", + "cloud_object.toast.no_objects_to_empty": "废纸篓中没有可清空的对象", + "cloud_object.toast.object_count_many": "{count} 个对象", + "cloud_object.toast.object_count_one": "1 个对象", + "cloud_object.toast.permissions_update_failed": "更新{object_name_lowercase}权限失败", + "cloud_object.toast.permissions_updated": "已成功更新{object_name_lowercase}权限", + "cloud_object.toast.restored": "{object_name} 已恢复", + "cloud_object.toast.rule_conflict": "规则未能保存,因为你编辑期间已有其他更改。", + "cloud_object.toast.saved_to": "{object_name} 已保存到 {containing_object_name}", + "cloud_object.toast.trash_emptied": "废纸篓已清空:{count_objects_message}已永久删除", + "cloud_object.toast.trashed": "{object_name} 已移至废纸篓", + "cloud_object.toast.updated": "{object_name} 已更新", + "cloud_object.toast.workflow_conflict": "此工作流未能保存,因为你编辑期间已有其他更改。", + "code.footer.use_oz_to_update_config": "使用 Oz 更新此配置", + "code_review.add_context.input_unavailable": "输入框不可用,无法附加差异", + "code_review.add_diff_set_context": "将差异集添加为上下文", + "code_review.add_file_diff_context": "将文件差异添加为上下文", + "code_review.binding.save_all_unsaved_files": "保存代码审查中的所有未保存文件", + "code_review.binding.show_find_bar": "在代码审查中显示查找栏", + "code_review.binding.toggle_file_navigation": "切换代码审查中的文件导航", + "code_review.cannot_attach_context_terminal_running": "终端正在运行时无法附加上下文", + "code_review.comments.add": "添加评论", + "code_review.comments.ai_must_be_enabled": "需要启用 AI 才能将评论发送给 Agent", + "code_review.comments.all_terminals_busy": "所有终端都在忙", + "code_review.comments.button.outdated_plural": "{count} 条过时评论", + "code_review.comments.button.outdated_singular": "{count} 条过时评论", + "code_review.comments.button.plural": "{count} 条评论", + "code_review.comments.button.singular": "{count} 条评论", + "code_review.comments.cli_agent": "CLI Agent", + "code_review.comments.copy_text": "复制文本", + "code_review.comments.edit": "编辑", + "code_review.comments.file_level_cannot_edit": "暂不支持编辑文件级评论。", + "code_review.comments.from_github": "来自 GitHub", + "code_review.comments.no_sendable_comments": "没有可发送的非过时评论", + "code_review.comments.outdated_cannot_edit": "无法编辑过时评论。", + "code_review.comments.outdated_chip": "已过时", + "code_review.comments.outdated_count": "{count} 条已过时", + "code_review.comments.outdated_omitted.plural": "{count} 条评论因已过时将被省略。", + "code_review.comments.outdated_omitted.singular": "1 条评论因已过时将被省略。", + "code_review.comments.remove": "移除", + "code_review.comments.requires_ai_credits": "Agent 代码审查需要 AI 额度", + "code_review.comments.review_comment": "审查评论", + "code_review.comments.send_to_agent.button": "发送给 Agent", + "code_review.comments.send_to_agent.tooltip": "将差异评论发送给 Agent", + "code_review.comments.send_to_destination": "将差异评论发送给 {label}", + "code_review.comments.sent_to_agent": "评论已发送给 Agent", + "code_review.comments.show_saved": "显示已保存评论", + "code_review.comments.submit_error": "无法将评论提交给 Agent", + "code_review.comments.view_in_github": "在 GitHub 中查看", + "code_review.context.diffset_against": "与 {branch} 比较的差异集", + "code_review.context.uncommitted_changes": "未提交的更改", + "code_review.copy_file_path": "复制文件路径", + "code_review.diff_menu.search_placeholder": "搜索要比较的差异集或分支...", + "code_review.diff_removed": "Diff 已移除", + "code_review.diff_target.uncommitted_changes": "未提交变更", + "code_review.discard.all": "全部丢弃", + "code_review.discard.confirm_button": "丢弃变更", + "code_review.discard.description.all_changes": "你将丢弃所有已提交和未提交的变更。", + "code_review.discard.description.all_uncommitted": "你将丢弃所有尚未提交的本地变更。", + "code_review.discard.description.file_changes_branch": "这会将此文件重置为 {branch} 分支版本,并丢弃所有已提交和未提交的编辑。", + "code_review.discard.description.file_changes_main": "这会将此文件恢复到 main 分支版本,并丢弃所有已提交和未提交的编辑。", + "code_review.discard.description.file_uncommitted": "这会将此文件恢复到最后一次提交的版本,并丢弃本地编辑。", + "code_review.discard.disabled_git_operation": "Git 操作(merge、rebase 等)进行中,无法丢弃变更", + "code_review.discard.no_changes": "没有可丢弃的变更", + "code_review.discard.no_file_selected": "未选择文件", + "code_review.discard.no_files_to_discard": "没有可丢弃的文件", + "code_review.discard.stash_changes": "储藏变更", + "code_review.discard.title.all_changes": "丢弃所有变更?", + "code_review.discard.title.all_uncommitted": "丢弃未提交的变更?", + "code_review.discard.title.file_changes": "丢弃此文件的所有变更?", + "code_review.discard.title.file_uncommitted": "丢弃此文件的所有未提交变更?", + "code_review.error_loading_diffs": "加载差异失败", + "code_review.file_content.binary": "二进制文件 - 无可用差异", + "code_review.file_content.diff_too_large": "差异过大,无法渲染", + "code_review.file_content.new_empty_file": "新建空文件", + "code_review.file_content.renamed_without_changes": "文件已重命名,无内容变更", + "code_review.file_content.unable_to_load": "无法加载文件内容", + "code_review.file_nav.hide": "隐藏文件导航", + "code_review.file_nav.show": "显示文件导航", + "code_review.git.branch": "分支", + "code_review.git.changes": "变更", + "code_review.git.commit": "提交", + "code_review.git.commit_and_create_pr": "提交并创建 PR", + "code_review.git.commit_and_publish": "提交并发布", + "code_review.git.commit_and_push": "提交并推送", + "code_review.git.commit_and_push.success": "变更已提交并推送。", + "code_review.git.commit_message.enter_tooltip": "输入提交信息", + "code_review.git.commit_message.generating_placeholder": "正在生成提交信息...", + "code_review.git.commit_message.label": "提交信息", + "code_review.git.commit_message.placeholder": "输入提交信息", + "code_review.git.commit.loading": "正在提交...", + "code_review.git.commit.success": "变更已成功提交。", + "code_review.git.confirm": "确认", + "code_review.git.create_pr": "创建 PR", + "code_review.git.create_pr.loading": "正在创建...", + "code_review.git.create_pr.success": "PR 已成功创建。", + "code_review.git.default_branch": "默认分支", + "code_review.git.dialog.title.commit": "提交你的变更", + "code_review.git.dialog.title.create_pr": "创建拉取请求", + "code_review.git.dialog.title.publish": "发布分支", + "code_review.git.dialog.title.push": "推送变更", + "code_review.git.error.auth_failed": "认证失败。请检查你的 Git 凭据。", + "code_review.git.error.generic": "Git 操作失败。", + "code_review.git.error.gh_not_authenticated": "GitHub CLI 未认证。请运行 `gh auth login`。", + "code_review.git.error.gh_not_installed": "未安装 GitHub CLI (gh)。请参阅 https://cli.github.com/。", + "code_review.git.error.identity_not_configured": "未配置 Git 身份。请设置 user.name 和 user.email。", + "code_review.git.error.network": "网络错误。请检查连接。", + "code_review.git.error.no_changes_to_commit": "没有可提交的变更。", + "code_review.git.error.no_remote": "此分支未配置远程仓库。", + "code_review.git.error.remote_has_new_changes": "远程有新的变更,请先拉取再推送。", + "code_review.git.error.remote_repo_not_found": "未找到远程仓库。", + "code_review.git.files.plural": "{count} 个文件", + "code_review.git.files.singular": "{count} 个文件", + "code_review.git.include_unstaged": "包含未暂存变更", + "code_review.git.included_commits": "包含的提交", + "code_review.git.loading": "正在加载...", + "code_review.git.no_actions_available": "没有可用的 Git 操作", + "code_review.git.no_changes_to_commit": "没有可提交的变更", + "code_review.git.open_pr": "打开 PR", + "code_review.git.publish": "发布", + "code_review.git.publish.loading": "正在发布...", + "code_review.git.publish.success": "分支已成功发布。", + "code_review.git.push": "推送", + "code_review.git.push.loading": "正在推送...", + "code_review.git.push.success": "变更已成功推送。", + "code_review.git.refreshing_pr_info": "正在刷新 PR 信息", + "code_review.header.reviewing_changes": "正在审查代码变更", + "code_review.header.reviewing_open_changes": "正在审查打开的变更", + "code_review.init_codebase": "初始化代码库", + "code_review.init_codebase.tooltip": "启用代码库索引和 WARP.md", + "code_review.loading_open_changes": "正在加载打开的变更...", + "code_review.maximize": "最大化", + "code_review.no_changes.description": "当你或 Agent 做出变更后,可以在这里跟踪。", + "code_review.no_changes.repo_initialized": "仓库已使用 {file_name} 文件初始化。", + "code_review.no_changes.title": "没有打开的变更", + "code_review.no_repo.disabled": "差异比较仅适用于 Git 仓库。", + "code_review.no_repo.remote": "差异比较仅适用于本地工作区。", + "code_review.no_repo.title": "无法检测此文件夹的差异", + "code_review.no_repo.wsl": "差异比较暂不支持 WSL。", + "code_review.open_file": "打开文件", + "code_review.open_repository": "打开仓库", + "code_review.open_repository.tooltip": "前往仓库并初始化以进行编码", + "code_review.remote_diff.empty_data": "服务器报告已加载状态,但没有可用的差异数据", + "code_review.restore": "恢复", + "code_review.retry": "重试", + "code_review.undo": "撤销", + "code_review.unsaved_file.tooltip": "此文件有未保存的变更。按 {shortcut} 保存", + "code_review.view_changes": "查看变更", + "code.accept_and_save": "接受并保存", + "code.close_saved": "关闭已保存项", + "code.editor.add_as_context": "添加为上下文", + "code.editor.comment.comment": "评论", + "code.editor.comment.imported_from_github": "从 GitHub 导入的评论", + "code.editor.discard_this_version": "丢弃此版本", + "code.editor.find_references": "查找引用", + "code.editor.find.a11y.find_bar": "用于在编辑器中搜索文本的查找栏。", + "code.editor.find.a11y.find_bar_with_matches": "查找栏找到 {count} 个匹配项。当前位于第 {current} 个,共 {count} 个。", + "code.editor.find.a11y.find_focused": "查找字段已聚焦。输入以搜索文本。使用 Enter 和 Shift-Enter 或上下箭头在匹配项之间导航。按 Escape 关闭查找栏。", + "code.editor.find.a11y.navigate_help": "使用 Enter 和 Shift-Enter 在匹配项之间导航。按 Escape 退出。", + "code.editor.find.a11y.no_results": "没有结果。", + "code.editor.find.a11y.replace_focused": "替换字段已聚焦。输入替换文本,按 Enter 替换当前匹配项,按 Tab 返回查找字段。使用上下箭头导航匹配项,按 Escape 关闭。", + "code.editor.find.a11y.replace_more_help": "继续按 Enter 替换更多匹配项,或使用上下箭头导航。", + "code.editor.find.a11y.replaced_last_match": "已成功替换最后一个匹配项。", + "code.editor.find.a11y.replaced_match": "已成功替换匹配项。选中的匹配项为第 {current} 个,共 {total} 个", + "code.editor.find.a11y.result": "第 {current} 个结果,共 {total} 个。", + "code.editor.find.case_sensitive_tooltip": "区分大小写搜索", + "code.editor.find.placeholder": "查找", + "code.editor.find.preserve_case_tooltip": "保留大小写", + "code.editor.find.regex_tooltip": "正则表达式开关", + "code.editor.find.replace_all": "全部替换", + "code.editor.find.replace_placeholder": "替换", + "code.editor.find.select_all": "全选", + "code.editor.go_to_definition": "转到定义", + "code.editor.goto_line.enter_line": "请输入行号", + "code.editor.goto_line.placeholder": "行号:列号", + "code.editor.goto_line.title": "转到行", + "code.editor.goto_line.valid_column": "请输入有效的列号", + "code.editor.goto_line.valid_line": "请输入有效的行号", + "code.editor.gutter.add_comment_on_line": "在此行添加评论", + "code.editor.gutter.add_diff_hunk_as_context": "将差异块添加为上下文", + "code.editor.gutter.revert_diff_hunk": "还原差异块", + "code.editor.gutter.save_changes_add_comment": "保存更改后添加评论", + "code.editor.gutter.save_changes_attach_context": "保存更改后附加为上下文。", + "code.editor.gutter.save_changes_revert": "保存更改后还原", + "code.editor.gutter.show_saved_comment": "显示已保存的评论", + "code.editor.nav.hunk": "代码块:", + "code.editor.nav.reject": "拒绝", + "code.editor.overwrite": "覆盖", + "code.editor.remote_host_disconnected": "远程主机已断开连接。你将无法看到更新或保存更改。", + "code.editor.saved_changes_not_reflected": "此文件已有保存的更改,但这里尚未反映。", + "code.file_tree.attach_as_context": "作为上下文附加", + "code.file_tree.cd_to_directory": "cd 到目录", + "code.file_tree.disabled_unavailable": "项目浏览器需要访问你的本地工作区。打开新会话或切换到活动会话后即可查看。", + "code.file_tree.file": "文件", + "code.file_tree.folder": "文件夹", + "code.file_tree.new_file": "新建文件", + "code.file_tree.open_in_new_pane": "在新窗格中打开", + "code.file_tree.open_in_new_tab": "在新标签页中打开", + "code.file_tree.project_explorer_unavailable": "项目浏览器不可用", + "code.file_tree.remote_unavailable": "项目浏览器需要访问你的本地工作区,远程会话暂不支持。", + "code.file_tree.reveal_in_explorer": "在资源管理器中显示", + "code.file_tree.reveal_in_file_manager": "在文件管理器中显示", + "code.file_tree.reveal_in_finder": "在 Finder 中显示", + "code.file_tree.too_many_files": "文件夹中的文件过多,无法在文件浏览器中显示。", + "code.file_tree.wsl_unavailable": "项目浏览器目前不支持 WSL。", + "code.find_references.showing_plural": "正在显示 {count} 个引用", + "code.find_references.showing_singular": "正在显示 1 个引用", + "code.footer.enable_server": "启用 {server_name}", + "code.footer.enable_servers": "启用服务器", + "code.footer.install_server": "安装 {server_name}", + "code.footer.install_servers": "安装服务器", + "code.footer.installing_server": "正在安装 {server_name}...", + "code.footer.language_server_unavailable_codebase": "此代码库的语言服务器不可用", + "code.footer.language_support_not_enabled": "{root_name} 当前未启用语言支持", + "code.footer.language_support_unavailable": "{root_name} 的语言支持不可用", + "code.footer.language_support_unavailable_file_type": "此文件类型不支持语言支持", + "code.footer.manage_servers": "管理服务器", + "code.footer.open_logs": "打开日志", + "code.footer.remove_server": "移除服务器", + "code.footer.restart_all_servers": "重启所有服务器", + "code.footer.restart_server": "重启服务器", + "code.footer.server_error": "{server_name}:错误", + "code.footer.server_message": "{server_name}:{message}", + "code.footer.server_stopped": "{server_name}:已停止", + "code.footer.start_all_servers": "启动所有服务器", + "code.footer.start_all_stopped_servers": "启动所有已停止的服务器", + "code.footer.start_server": "启动服务器", + "code.footer.stop_all_servers": "停止所有服务器", + "code.footer.stop_server": "停止服务器", + "code.footer.this_codebase": "此代码库", + "code.footer.this_workspace": "此工作区", + "code.footer.unknown_workspace": "未知工作区", + "code.global_buffer_model.buffer_deallocated": "缓冲区已释放", + "code.global_buffer_model.buffer_not_found": "未找到缓冲区", + "code.global_buffer_model.no_remote_server_client": "没有可用的远程服务器客户端", + "code.inline_diff.failed_delete_file": "删除文件失败:{error}", + "code.inline_diff.failed_save_file": "保存文件失败:{error}", + "code.inline_diff.missing_base_content": "缺少基准内容", + "code.local_code_editor.failed_save_file": "保存文件失败:{error}", + "code.local_code_editor.missing_file_id": "缺少 file_id", + "code.toast.file_saved": "文件已保存。", + "code.toast.load_file_failed": "加载文件失败。", + "code.toast.save_file_failed": "保存文件失败。", + "code.toast.save_file_remote_disconnected": "无法保存 — 远程会话已断开连接。", + "code.view_markdown_preview": "查看 Markdown 预览", + "code.view.new_suffix": "(新)", + "coding_entrypoints.clone_repo.placeholder": "提供仓库 URL,例如 \"git@github.com:username/project.git\"", + "coding_entrypoints.create_project.placeholder": "你想构建什么?", + "coding_entrypoints.create_project.suggestion.csv_to_json_cli": "编写一个 CSV 转 JSON 的 CLI 转换器", + "coding_entrypoints.create_project.suggestion.game_of_life": "制作一个 Conway 生命游戏模拟", + "coding_entrypoints.create_project.suggestion.minesweeper": "用 React 构建一个扫雷游戏克隆", + "coding_entrypoints.create_project.suggestion.random_quotes_server": "编写一个 Node.js 服务器,从 JSON 文件返回随机语录", + "coding_entrypoints.create_project.suggestion.resume_template": "创建一个简历网页的起始模板", + "coding_entrypoints.project_buttons.clone_repository.label": "克隆仓库", + "coding_entrypoints.project_buttons.clone_repository.tooltip": "从 GitHub 或其他来源克隆 repo", + "coding_entrypoints.project_buttons.create_new_project.label": "创建新项目", + "coding_entrypoints.project_buttons.create_new_project.tooltip": "创建并初始化一个全新的项目", + "coding_entrypoints.project_buttons.open_repository.label": "打开仓库", + "coding_entrypoints.project_buttons.open_repository.tooltip": "打开现有本地文件夹或仓库", + "common.accept": "接受", + "common.add": "添加", + "common.agent": "Agent", + "common.allow": "允许", + "common.attach_to_active_session": "附加到活动会话", + "common.back": "返回", + "common.beta": "Beta", + "common.cancel": "取消", + "common.click": "点击", + "common.close": "关闭", + "common.close_pane": "关闭窗格", + "common.collapse": "折叠", + "common.collapse_all": "全部折叠", + "common.configure": "配置", + "common.confirm": "确认", + "common.continue": "继续", + "common.copied": "已复制", + "common.copied_to_clipboard": "已复制到剪贴板", + "common.copy": "复制", + "common.copy_file_path": "复制文件路径", + "common.copy_id": "复制 ID", + "common.copy_link": "复制链接", + "common.copy_path": "复制路径", + "common.copy_relative_path": "复制相对路径", + "common.create": "创建", + "common.current": "当前", + "common.custom_ellipsis": "自定义...", + "common.cut": "剪切", + "common.default": "默认", + "common.delete": "删除", + "common.delete_forever": "永久删除", + "common.description": "描述", + "common.dismiss": "忽略", + "common.do_not_show_again": "不再显示", + "common.done": "完成", + "common.download": "下载", + "common.duplicate": "复制副本", + "common.edit": "编辑", + "common.editing": "正在编辑", + "common.enable": "启用", + "common.error": "错误", + "common.exit": "退出", + "common.expand": "展开", + "common.expiration": "过期时间", + "common.export": "导出", + "common.finish": "完成", + "common.install": "安装", + "common.installed": "已安装", + "common.learn_more": "了解更多", + "common.left": "左侧", + "common.link_copied": "链接已复制", + "common.link_copied_to_clipboard": "链接已复制到剪贴板", + "common.loading": "加载中...", + "common.manage": "管理", + "common.name": "名称", + "common.new": "新", + "common.new_uppercase": "新功能", + "common.next": "下一个", + "common.no": "否", + "common.no_matches": "没有匹配项", + "common.no_matches_found": "未找到匹配项。", + "common.none": "无", + "common.open": "打开", + "common.open_file": "打开文件", + "common.open_folder": "打开文件夹", + "common.open_in_warp": "在 Warp 中打开", + "common.open_on_desktop": "在桌面端打开", + "common.or": " 或 ", + "common.or_standalone": "或", + "common.other_user": "其他用户", + "common.paste": "粘贴", + "common.previous": "上一个", + "common.recommended": "推荐", + "common.redo": "重做", + "common.refine": "优化", + "common.refresh": "刷新", + "common.reject": "拒绝", + "common.remove": "移除", + "common.rename": "重命名", + "common.reset": "重置", + "common.restart": "重新开始", + "common.restore": "恢复", + "common.restore_default": "恢复默认", + "common.retry": "重试", + "common.right": "右侧", + "common.run": "运行", + "common.save": "保存", + "common.save_changes": "保存更改", + "common.saving": "正在保存...", + "common.search": "搜索", + "common.select_all": "全选", + "common.send_feedback": "发送反馈", + "common.settings": "设置", + "common.share": "分享", + "common.sign_in_to_edit": "登录后编辑", + "common.sign_up": "注册", + "common.skip": "跳过", + "common.something_went_wrong": "出了点问题", + "common.something_went_wrong_try_again": "出了点问题。请重试。", + "common.split_pane_down": "向下拆分窗格", + "common.split_pane_left": "向左拆分窗格", + "common.split_pane_right": "向右拆分窗格", + "common.split_pane_up": "向上拆分窗格", + "common.status": "状态", + "common.suggested": "建议", + "common.text": "文本", + "common.to_toggle_selection": "切换选择", + "common.trash": "移到废纸篓", + "common.troubleshoot": "故障排查", + "common.try": "试试", + "common.try_again": "重试", + "common.type": "类型", + "common.undo": "撤销", + "common.untitled": "未命名", + "common.update": "更新", + "common.upgrade": "升级", + "common.view": "查看", + "common.view_details": "查看详情", + "common.view_plans": "查看套餐", + "common.viewing": "正在查看", + "common.yes": "是", + "context_chips.change_git_branch": "切换 Git 分支", + "context_chips.change_working_directory": "更改工作目录", + "context_chips.copy_chip": "复制{title}", + "context_chips.create_new_branch": "创建新分支“{branch}”", + "context_chips.environment.id": "ID:", + "context_chips.environment.image": "镜像:", + "context_chips.environment.name": "名称:", + "context_chips.environment.none": "(无)", + "context_chips.environment.repos": "仓库:", + "context_chips.menu.no_results": "无结果", + "context_chips.menu.no_results_found": "未找到结果", + "context_chips.menu.search_branches": "搜索分支...", + "context_chips.menu.search_directories": "搜索目录...", + "context_chips.menu.search_environments": "搜索环境...", + "context_chips.monthly_ai_credits_reset": "每月 AI 额度已重置!", + "context_chips.node.install_nvm_description": "此菜单可帮助你切换 Node.js 版本,但需要先安装 nvm。", + "context_chips.node.install_nvm_title": "安装 nvm 以启用版本切换", + "context_chips.node.no_versions_installed": "未安装 Node 版本", + "context_chips.node.try_installing_with_nvm": "请尝试使用 nvm 安装版本", + "context_chips.parent_directory": "..(父目录)", + "context_chips.plan.agent_unaware_of_recent_edits": "Agent 不知道最近的计划编辑", + "context_chips.plan.view_plan": "查看计划", + "context_chips.requires_command": "需要 `{command}` 命令", + "context_chips.requires_github_cli": "需要 GitHub CLI", + "context_chips.requires_local_session": "需要本地会话", + "context_chips.title.agent_plan_and_todo_list": "Agent 计划和待办列表", + "context_chips.title.conda_environment": "Conda 环境", + "context_chips.title.date": "日期", + "context_chips.title.git_branch": "Git 分支", + "context_chips.title.git_diff_stats": "Git Diff 统计", + "context_chips.title.github_pull_request": "GitHub Pull Request", + "context_chips.title.host": "主机", + "context_chips.title.kubernetes_context": "Kubernetes 上下文", + "context_chips.title.node_js_version": "Node.js 版本", + "context_chips.title.python_virtualenv": "Python Virtualenv", + "context_chips.title.remote_login": "远程登录", + "context_chips.title.subshell": "子 Shell", + "context_chips.title.svn_branch": "Svn 分支", + "context_chips.title.svn_uncommitted_file_count": "Svn 未提交文件数", + "context_chips.title.time_12": "时间(12 小时制)", + "context_chips.title.time_24": "时间(24 小时制)", + "context_chips.title.user": "用户", + "context_chips.title.working_directory": "工作目录", + "context_chips.todo.view_todo_list": "查看待办列表", + "context_chips.view_pull_request": "查看 Pull Request", + "context_chips.working_directory": "工作目录", + "drive.banner.joined": "{first}{second}", + "drive.confirmation.delete_team.body": "删除此团队会永久删除该团队及其所有相关内容,包括账单信息或额度。你将无法恢复它们。", + "drive.confirmation.delete_team.confirm": "是,删除", + "drive.confirmation.delete_team.title": "确定要删除此团队吗?", + "drive.confirmation.leave_team_reload_credits.body": "如果离开此团队,你将无法使用与该团队关联的剩余充值额度。如果之后重新加入同一团队,你可以重新获得尚未使用且未过期的额度。", + "drive.confirmation.leave_team_reload_credits.confirm": "离开团队", + "drive.confirmation.leave_team.body": "你需要重新受邀才能再次加入。", + "drive.confirmation.leave_team.confirm": "是,离开", + "drive.confirmation.leave_team.title": "确定要离开此团队吗?", + "drive.confirmation.remove_member_reload_credits.body": "此成员将无法使用与该团队关联的剩余充值额度。如果之后重新加入,他们可以重新获得尚未使用且未过期的额度。", + "drive.confirmation.remove_member_reload_credits.confirm": "移除成员", + "drive.confirmation.remove_member.title": "确定要移除此成员吗?", + "drive.copy_variables": "复制变量", + "drive.copy_workflow_text": "复制 workflow 文本", + "drive.empty_trash": "清空废纸篓", + "drive.export.exported_named": "已导出 {name}", + "drive.export.exported_object": "已导出对象", + "drive.export.failed": "导出失败", + "drive.export.failed_named": "导出 {name} 失败", + "drive.export.finished": "对象导出完成", + "drive.export.open_in_finder": "在 Finder 中打开", + "drive.export.open_in_folder": "在文件夹中打开", + "drive.import.choose_files": "选择文件...", + "drive.import.failed_parse_file": "解析文件失败:{error}", + "drive.import.failed_upload_file": "上传文件到服务器失败", + "drive.import.failed_upload_folder": "上传文件夹到服务器失败", + "drive.import.learn_file_support": "了解文件支持和格式要求", + "drive.import.preparing": "正在准备...", + "drive.import.title": "导入", + "drive.items.from_owner": "来自 {owner}", + "drive.items.mcp_servers": "MCP Servers", + "drive.items.rules": "规则", + "drive.items.unknown_team": "未知团队", + "drive.items.unknown_user": "未知用户", + "drive.limit.agent_workflows": "Agent 工作流", + "drive.limit.ai_fact": "AI Fact", + "drive.limit.environment_variables": "环境变量", + "drive.limit.folders": "文件夹", + "drive.limit.mcp_server": "MCP Server", + "drive.limit.mcp_servers": "MCP Servers", + "drive.limit.notebooks": "Notebook", + "drive.limit.object.environment_variables": "环境变量", + "drive.limit.object.folders": "文件夹", + "drive.limit.object.notebooks": "Notebook", + "drive.limit.object.objects": "对象", + "drive.limit.object.rules": "规则", + "drive.limit.object.workflows": "工作流", + "drive.limit.personal_sign_up_description": "免费注册即可提高存储上限并解锁更多功能。", + "drive.limit.rules": "规则", + "drive.limit.shared_limit_reached": "你的套餐中的{object_type}已用完。", + "drive.limit.shared_limit_upgrade": "升级即可获得更多 Notebook、工作流、共享会话和 AI 额度。", + "drive.limit.workflows": "工作流", + "drive.load_in_subshell": "在子 shell 中加载", + "drive.menu.environment_variables": "环境变量", + "drive.menu.folder": "文件夹", + "drive.menu.move_to_space": "移动到 {space}", + "drive.menu.new_environment_variables": "新建环境变量", + "drive.menu.new_folder": "新建文件夹", + "drive.menu.new_notebook": "新建 Notebook", + "drive.menu.new_prompt": "新建 Prompt", + "drive.menu.new_workflow": "新建工作流", + "drive.menu.notebook": "Notebook", + "drive.menu.prompt": "Prompt", + "drive.menu.workflow": "工作流", + "drive.naming.collection_name": "集合名称", + "drive.naming.folder_name": "文件夹名称", + "drive.naming.notebook_name": "Notebook 名称", + "drive.object.ai_fact": "AI fact", + "drive.object.ai_fact_collection": "AI fact 集合", + "drive.object.env_var_collection": "环境变量集合", + "drive.object.folder": "文件夹", + "drive.object.mcp_server": "MCP server", + "drive.object.mcp_server_collection": "MCP server 集合", + "drive.object.notebook": "Notebook", + "drive.object.prompt": "Prompt", + "drive.object.workflow": "工作流", + "drive.offline_banner": "你当前处于离线状态。部分文件将为只读。", + "drive.payment_issue.admin": "请更新你的付款信息以恢复访问权限。", + "drive.payment_issue.admin_enterprise": "请联系 support@warp.dev 以恢复访问权限。", + "drive.payment_issue.non_admin": "请联系团队管理员以恢复访问权限。", + "drive.payment_issue.restricted": "共享对象因订阅付款问题已受限。", + "drive.retry_sync": "重试同步", + "drive.revert_to_server": "恢复为服务器版本", + "drive.sharing.access.can_edit": "可编辑", + "drive.sharing.access.can_view": "可查看", + "drive.sharing.access.edit_name": "编辑", + "drive.sharing.access.full": "完整访问权限", + "drive.sharing.access.full_name": "完整访问", + "drive.sharing.access.no_access": "无访问权限", + "drive.sharing.access.view_name": "查看", + "drive.sharing.already_shared_with": "已与 {emails} 共享", + "drive.sharing.anyone_with_link": "拥有链接的任何人", + "drive.sharing.cannot_edit_inherited_permissions_tooltip": "无法编辑继承的权限", + "drive.sharing.download_qr_code": "下载二维码", + "drive.sharing.edit_inherited_permissions_tooltip": "在父文件夹上编辑继承的权限", + "drive.sharing.emails_placeholder": "邮箱", + "drive.sharing.inherited_from_prefix": "继承自 ", + "drive.sharing.inherited_permission": "继承的权限", + "drive.sharing.invalid_address": "地址无效:{emails}", + "drive.sharing.link_copied": "已复制 {object_name} 的链接。", + "drive.sharing.live_session_started": "实时会话开始于 {date} {time}", + "drive.sharing.only_invited_teammates": "仅受邀团队成员", + "drive.sharing.only_people_invited": "仅受邀人员", + "drive.sharing.owner_full_permissions_tooltip": "所有者始终拥有其对象的完整权限", + "drive.sharing.qr_create_failed": "无法为此会话链接创建二维码。", + "drive.sharing.qr_download_failed": "无法下载二维码。", + "drive.sharing.qr_downloaded": "二维码已下载。", + "drive.sharing.restricted_access_prefix": "你需要拥有完整访问权限才能管理权限。你当前拥有", + "drive.sharing.restricted_access_suffix": "权限。", + "drive.sharing.share_session_qr_code": "分享会话二维码", + "drive.sharing.show_qr_code": "显示二维码", + "drive.sharing.team_owner_full_permissions_tooltip": "团队对象会自动向团队成员授予完整权限", + "drive.sharing.teammates_with_link": "拥有链接的团队成员", + "drive.sharing.unknown_subject": "未知", + "drive.sharing.who_has_access": "谁有访问权限", + "drive.sort_by": "排序方式", + "drive.sort.a_to_z": "A 到 Z", + "drive.sort.last_trashed": "最近移入废纸篓", + "drive.sort.last_updated": "最近更新", + "drive.sort.type": "类型", + "drive.sort.z_to_a": "Z 到 A", + "drive.syncing": "正在同步 Warp Drive", + "drive.team.create_hint": "与队友共享命令和知识。", + "drive.team.create_team": "创建团队", + "drive.team.join_hint": "与你在 Warp 上的 {count} 位队友协作。", + "drive.team.view_team_to_join": "查看可加入的团队", + "drive.team.view_teams_to_join": "查看可加入的团队", + "drive.team.zero_state_hint": "将个人工作流或 Notebook 拖到这里或移动到这里,即可与团队共享。", + "drive.title": "Warp Drive", + "drive.trash.deleted_after_30_days": "废纸篓中的项目将在 30 天后永久删除。", + "drive.trash.empty_confirm": "是,清空废纸篓", + "drive.trash.empty_confirmation_body": "此操作无法撤销。", + "drive.trash.empty_confirmation_title": "确定要清空废纸篓吗?", + "drive.trash.title": "废纸篓", + "drive.trash.uppercase": "废纸篓", + "drive.workflows.ai_assist.bad_command": "无法生成元数据。请换一个命令后重试。", + "drive.workflows.ai_assist.rate_limited": "你的 AI 额度似乎已用完。请稍后重试。", + "drive.workflows.ai_assist.rate_limited_contact_admin": "你的 AI 额度似乎已用完。请联系团队管理员升级以获得更多额度。", + "drive.workflows.argument_default_value_placeholder": "默认值(可选)", + "editor.a11y.deleted": ",已删除", + "editor.a11y.pasting": "正在粘贴:{text}", + "editor.a11y.selected": "已选择", + "editor.a11y.selection_action": ",{action}", + "editor.a11y.unselected": "已取消选择", + "editor.autosuggestion.change_keybinding": "更改快捷键", + "editor.autosuggestion.cycle_suggestions": "切换建议", + "editor.autosuggestion.ignore_this_suggestion": "忽略此建议", + "editor.context_menu.search_files_and_directories": "搜索文件和目录", + "editor.image.attach_images": "附加图片", + "editor.image.attachment_disabled_conversation_limit": "图片附加已禁用 - 每个对话最多 {count} 张", + "editor.image.attachment_disabled_query_limit": "图片附加已禁用 - 每次查询最多 {count} 张", + "editor.image.attachment_unsupported_model": "此模型不支持附加图片", + "editor.image.limit_per_conversation": "每个对话最多 {count} 张", + "editor.image.limit_per_query": "每次查询最多 {count} 张", + "editor.image.not_attached.limit_many": "有 {count} 张图片未附加 - {limit_reason}。", + "editor.image.not_attached.limit_one": "有 1 张图片未附加 - {limit_reason}。", + "editor.image.processing_error_many": "有 {count} 张图片未附加 - 处理出错。", + "editor.image.processing_error_one": "有 1 张图片未附加 - 处理出错。", + "editor.image.processing_error_single_only": "无法附加图片 - 处理出错。", + "editor.image.read_failed_many": "有 {count} 张图片未附加 - 文件读取失败。", + "editor.image.read_failed_one": "有 1 张图片未附加 - 文件读取失败。", + "editor.image.read_failed_single_only": "无法附加图片 - 文件读取失败。", + "editor.image.too_large_many": "有 {count} 张图片未附加 - 文件过大。", + "editor.image.too_large_one": "有 1 张图片未附加 - 文件过大。", + "editor.image.too_large_single_only": "无法附加图片 - 文件过大。", + "editor.image.unsupported_many": "有 {count} 张图片未附加 - 支持的类型为 PNG、JPG、GIF、WEBP。", + "editor.image.unsupported_one": "有 1 张图片未附加 - 支持的类型为 PNG、JPG、GIF、WEBP。", + "editor.image.unsupported_single_only": "无法附加图片 - 支持的类型为 PNG、JPG、GIF、WEBP。", + "editor.model_no_image_context": "所选模型不支持将图片作为上下文。", + "editor.model_no_image_context_no_period": "所选模型不支持将图片作为上下文", + "editor.voice.enabled_with_key": "语音输入已启用。你也可以按住 `{key}` 键来启用语音输入(可在设置 > AI > 语音中配置)", + "editor.voice.error": "处理语音输入时发生错误。", + "editor.voice.limit_hit": "你已达到语音请求上限。你的额度会在下一个周期刷新。", + "editor.voice.start_failed_microphone": "无法启动语音输入(你可能需要启用麦克风访问权限)", + "editor.voice.tooltip": "语音转录", + "editor.voice.tooltip_microphone_denied": "由于未授予麦克风访问权限,语音转录已停用。", + "editor.voice.tooltip_with_key": "语音转录(按住 `{key}` 键)", + "editor.voice.try_voice_input": "试试语音输入", + "env_vars.clear_secret": "清除 secret", + "env_vars.command": "命令", + "env_vars.command_placeholder": "命令", + "env_vars.command_waiting_for_user": "可以运行此命令并读取输出吗?", + "env_vars.description_placeholder": "添加描述", + "env_vars.discard_changes": "放弃更改", + "env_vars.invoke_failed": "调用环境变量时发生错误", + "env_vars.keep_editing": "继续编辑", + "env_vars.load": "加载", + "env_vars.maximize_pane": "最大化窗格", + "env_vars.minimize_pane": "最小化窗格", + "env_vars.moved_to_trash": "环境变量已移到废纸篓", + "env_vars.no_longer_access": "你无法再访问这些环境变量", + "env_vars.restore_from_trash": "从废纸篓恢复环境变量", + "env_vars.secret_command": "Secret 命令", + "env_vars.secret_or_command_tooltip": "添加 secret 或命令。Warp 绝不会存储外部 secret", + "env_vars.secret_redaction_conflict_enterprise": "由于与你所在企业的 secret 脱敏设置冲突,无法创建此环境变量。请联系团队管理员了解详情。", + "env_vars.secret_redaction_conflict_user": "由于与你的 secret 脱敏设置冲突,无法创建此环境变量。请将该 secret 保存为环境变量(在 shell 配置或 .env 文件中),或在“设置 > 隐私”中更新 secret 脱敏设置。", + "env_vars.title": "标题", + "env_vars.title_placeholder": "添加标题", + "env_vars.unsaved_changes": "你有未保存的更改。", + "env_vars.value_placeholder": "值", + "env_vars.variable_placeholder": "变量", + "env_vars.variables": "变量", + "external_secrets.error.cli_not_installed": "未安装 {manager} CLI", + "external_secrets.error.fetch_failed": "{manager} 未返回密钥(可能未配置或未完成身份验证)", + "external_secrets.error.platform_not_supported": "平台不受支持", + "external_secrets.link.integrate_1password_cli": "将 1Password 应用与 CLI 集成", + "external_secrets.link.view_cli_installation_docs": "查看 {manager} CLI 安装文档", + "find.a11y.close_find_bar": "关闭查找栏", + "find.a11y.disable_case_sensitive_search": "停用区分大小写搜索", + "find.a11y.disable_regex_search": "停用正则表达式搜索", + "find.a11y.enable_case_sensitive_search": "启用区分大小写搜索", + "find.a11y.enable_regex_search": "启用正则表达式搜索", + "find.a11y.focus_next_match": "聚焦下一个匹配项", + "find.a11y.focus_previous_match": "聚焦上一个匹配项", + "find.a11y.input_help": "按 Escape 退出,使用 Enter 和 Shift-Enter 在匹配项之间导航", + "find.a11y.no_results": "没有结果。", + "find.a11y.result_count": "第 {current} 个结果,共 {total} 个。", + "find.a11y.result_help": "使用 Enter 和 Shift-Enter 在匹配项之间导航。按 Escape 退出。", + "find.a11y.type_phrase": "输入搜索词。", + "find.binding.find_next_occurrence": "查找搜索内容的下一个匹配项", + "find.binding.find_previous_occurrence": "查找搜索内容的上一个匹配项", + "find.case_sensitive_tooltip": "区分大小写搜索", + "find.placeholder": "查找", + "find.regex_toggle_tooltip": "正则表达式开关", + "find.scanning": "正在扫描...", + "find.within_block_tooltip": "在所选块中查找", + "input_suggestions.a11y.closed": "已关闭建议。", + "input_suggestions.a11y.command_suggestions": "命令建议。", + "input_suggestions.a11y.command_suggestions_help": "使用 Tab 和 Shift-Tab 导航,按 Enter 确认。按 Command + Enter 执行选中的命令。Esc 退出建议菜单。", + "input_suggestions.a11y.last_ran": "上次运行于 {time}", + "input_suggestions.a11y.selected": "已选择:{text}", + "input_suggestions.a11y.suggestion": "建议:{text}。", + "input_suggestions.no_suggestions": "没有建议", + "keybinding.description.a11y_set_concise_accessibility_announcements": "[无障碍] 设置简洁播报", + "keybinding.description.a11y_set_verbose_accessibility_announcements": "[无障碍] 设置详细播报", + "keybinding.description.about_warp": "关于 Warp", + "keybinding.description.accept_autosuggestion": "接受自动建议", + "keybinding.description.activate_next_tab": "激活下一个标签页", + "keybinding.description.activate_previous_tab": "激活上一个标签页", + "keybinding.description.add_current_folder_as_project": "将当前文件夹添加为项目", + "keybinding.description.add_cursor_above": "在上方添加光标", + "keybinding.description.add_cursor_below": "在下方添加光标", + "keybinding.description.add_repository": "添加仓库", + "keybinding.description.add_selection_for_next_occurrence": "选择下一个匹配项", + "keybinding.description.agent_conversation_list_view": "Agent 对话列表视图", + "keybinding.description.alternate_terminal_paste": "备用终端粘贴", + "keybinding.description.appearance": "外观...", + "keybinding.description.ask_warp_ai": "询问 Warp AI", + "keybinding.description.ask_warp_ai_about_last_block": "向 Warp AI 询问最后一个块", + "keybinding.description.ask_warp_ai_about_selection": "向 Warp AI 询问所选内容", + "keybinding.description.attach_selected_block_as_agent_context": "将选中块附加为 Agent 上下文", + "keybinding.description.attach_selected_text_as_agent_context": "将选中文本附加为 Agent 上下文", + "keybinding.description.attach_selection_as_agent_context": "将选区附加为 Agent 上下文", + "keybinding.description.backward_tabulation_within_an_executing_command": "在执行中的命令内反向制表", + "keybinding.description.bookmark_selected_block": "收藏所选块", + "keybinding.description.check_for_updates": "检查更新", + "keybinding.description.clear_and_reset_ai_context_menu_query": "清空并重置 AI 上下文菜单查询", + "keybinding.description.clear_command_editor": "清空命令编辑器", + "keybinding.description.clear_screen": "清屏", + "keybinding.description.clear_selected_lines": "清除所选行", + "keybinding.description.close": "关闭", + "keybinding.description.close_all_tabs": "关闭所有标签页", + "keybinding.description.close_current_session": "关闭当前会话", + "keybinding.description.close_focused_panel": "关闭聚焦面板", + "keybinding.description.close_saved_tabs": "关闭已保存的标签页", + "keybinding.description.close_tabs_below": "关闭下方标签页", + "keybinding.description.close_tabs_to_the_right": "关闭右侧标签页", + "keybinding.description.close_the_current_tab": "关闭当前标签页", + "keybinding.description.close_warp_ai": "关闭 Warp AI", + "keybinding.description.close_window": "关闭窗口", + "keybinding.description.command_palette": "命令面板", + "keybinding.description.configure_keyboard_shortcuts": "配置键盘快捷键...", + "keybinding.description.configure_warpify": "配置 Warpify...", + "keybinding.description.copy_access_token_to_clipboard": "将访问令牌复制到剪贴板", + "keybinding.description.copy_and_clear_selected_lines": "复制并清除所选行", + "keybinding.description.copy_command": "复制命令", + "keybinding.description.copy_command_and_output": "复制命令和输出", + "keybinding.description.copy_command_output": "复制命令输出", + "keybinding.description.copy_git_branch": "复制 Git 分支", + "keybinding.description.copy_rich_text_buffer": "复制富文本缓冲区", + "keybinding.description.copy_rich_text_selection": "复制富文本所选内容", + "keybinding.description.create_a_new_personal_folder": "新建个人文件夹", + "keybinding.description.create_a_new_personal_notebook": "新建个人 notebook", + "keybinding.description.create_a_new_personal_prompt": "新建个人提示词", + "keybinding.description.create_a_new_personal_workflow": "新建个人 workflow", + "keybinding.description.create_a_new_team_folder": "新建团队文件夹", + "keybinding.description.create_a_new_team_notebook": "新建团队 notebook", + "keybinding.description.create_a_new_team_prompt": "新建团队提示词", + "keybinding.description.create_a_new_team_workflow": "新建团队 workflow", + "keybinding.description.create_new_personal_environment_variables": "新建个人环境变量", + "keybinding.description.create_new_project": "新建项目", + "keybinding.description.create_new_tab": "新建标签页", + "keybinding.description.create_new_team_environment_variables": "新建团队环境变量", + "keybinding.description.create_or_edit_link": "创建或编辑链接", + "keybinding.description.cursor_at_buffer_end": "光标移动到缓冲区末尾", + "keybinding.description.cursor_at_buffer_start": "光标移动到缓冲区开头", + "keybinding.description.cut_all_left": "剪切左侧全部内容", + "keybinding.description.cut_all_right": "剪切右侧全部内容", + "keybinding.description.cut_word_left": "剪切左侧单词", + "keybinding.description.cut_word_right": "剪切右侧单词", + "keybinding.description.de_select_shell_commands": "取消选择 Shell 命令", + "keybinding.description.decrease_font_size": "减小字体大小", + "keybinding.description.decrease_notebook_font_size": "减小笔记本字体大小", + "keybinding.description.decrease_zoom_level": "缩小", + "keybinding.description.delete": "删除", + "keybinding.description.delete_all_left": "删除左侧全部内容", + "keybinding.description.delete_all_right": "删除右侧全部内容", + "keybinding.description.delete_to_line_end_within_an_executing_command": "删除到执行中命令的行尾", + "keybinding.description.delete_to_line_start_within_an_executing_command": "删除到执行中命令的行首", + "keybinding.description.delete_word_left": "删除左侧单词", + "keybinding.description.delete_word_left_within_an_executing_command": "删除执行中命令左侧单词", + "keybinding.description.delete_word_right": "删除右侧单词", + "keybinding.description.dump_heap_profile_can_only_be_done_once": "转储堆配置文件(只能执行一次)", + "keybinding.description.edit_prompt": "编辑提示词", + "keybinding.description.end": "行尾", + "keybinding.description.exit_vim_insert_mode": "退出 Vim 插入模式", + "keybinding.description.expand_selected_blocks_above": "向上扩展所选块", + "keybinding.description.expand_selected_blocks_below": "向下扩展所选块", + "keybinding.description.experimental_toggle_classic_completions_mode": "(实验) 切换经典补全模式", + "keybinding.description.export_all_warp_drive_objects": "导出所有 Warp Drive 对象", + "keybinding.description.find_in_code_editor": "在代码编辑器中查找", + "keybinding.description.find_in_notebook": "在笔记本中查找", + "keybinding.description.find_in_terminal": "在终端中查找", + "keybinding.description.find_the_next_occurrence_of_your_search_query": "查找下一个搜索匹配项", + "keybinding.description.find_the_previous_occurrence_of_your_search_query": "查找上一个搜索匹配项", + "keybinding.description.find_within_selected_block": "在所选块内查找", + "keybinding.description.focus_next_match": "聚焦下一个匹配项", + "keybinding.description.focus_previous_match": "聚焦上一个匹配项", + "keybinding.description.focus_terminal_input": "聚焦终端输入", + "keybinding.description.focus_terminal_input_from_file": "从文件聚焦终端输入", + "keybinding.description.focus_terminal_input_from_notebook": "从笔记本聚焦终端输入", + "keybinding.description.focus_terminal_input_from_warp_ai": "从 Warp AI 聚焦终端输入", + "keybinding.description.fold": "折叠", + "keybinding.description.fold_selected_ranges": "折叠所选范围", + "keybinding.description.global_search": "全局搜索", + "keybinding.description.go_to_line": "跳转到行", + "keybinding.description.hide_all_windows": "隐藏所有窗口", + "keybinding.description.hide_dedicated_hotkey_window": "隐藏专用热键窗口", + "keybinding.description.history_search": "历史搜索", + "keybinding.description.home": "行首", + "keybinding.description.import_external_settings": "导入外部设置", + "keybinding.description.import_to_personal_drive": "导入到个人 Drive", + "keybinding.description.import_to_team_drive": "导入到团队 Drive", + "keybinding.description.increase_font_size": "增大字体大小", + "keybinding.description.increase_notebook_font_size": "增大笔记本字体大小", + "keybinding.description.increase_zoom_level": "放大", + "keybinding.description.initiate_project_for_warp": "为 Warp 初始化项目", + "keybinding.description.insert_command_correction": "插入命令修正", + "keybinding.description.insert_last_word_of_previous_command": "插入上一条命令的最后一个单词", + "keybinding.description.insert_newline": "插入换行", + "keybinding.description.insert_non_expanding_space": "插入非扩展空格", + "keybinding.description.inspect_command": "检查命令", + "keybinding.description.install_oz_cli_command": "安装 Oz CLI 命令", + "keybinding.description.install_update_and_relaunch": "安装更新并重新启动", + "keybinding.description.invite_people": "邀请成员...", + "keybinding.description.join_our_slack_community_opens_external_link": "加入我们的 Slack 社区(打开外部链接)", + "keybinding.description.jump_to_latest_agent_task": "跳转到最新 Agent 任务", + "keybinding.description.launch_configuration_palette": "启动配置面板", + "keybinding.description.left_panel_agent_conversations": "左侧面板:Agent 对话", + "keybinding.description.left_panel_global_search": "左侧面板:全局搜索", + "keybinding.description.left_panel_project_explorer": "左侧面板:项目浏览器", + "keybinding.description.left_panel_warp_drive": "左侧面板:Warp Drive", + "keybinding.description.load_agent_mode_conversation_from_debug_link_in_clipboard": "从剪贴板中的调试链接加载 Agent 模式会话", + "keybinding.description.log_editor_state": "记录编辑器状态", + "keybinding.description.log_out": "退出登录", + "keybinding.description.move_backward_one_subword": "向后移动一个子词", + "keybinding.description.move_backward_one_word": "向后移动一个单词", + "keybinding.description.move_cursor_down": "向下移动光标", + "keybinding.description.move_cursor_end_within_an_executing_command": "移动到执行中命令的末尾", + "keybinding.description.move_cursor_home_within_an_executing_command": "移动到执行中命令的开头", + "keybinding.description.move_cursor_left": "向左移动光标", + "keybinding.description.move_cursor_one_word_to_the_left_within_an_executing_command": "在执行中的命令内向左移动一个单词", + "keybinding.description.move_cursor_one_word_to_the_right_within_an_executing_command": "在执行中的命令内向右移动一个单词", + "keybinding.description.move_cursor_right": "向右移动光标", + "keybinding.description.move_cursor_to_the_bottom": "将光标移动到底部", + "keybinding.description.move_cursor_to_the_top": "将光标移动到顶部", + "keybinding.description.move_cursor_up": "向上移动光标", + "keybinding.description.move_forward_one_subword": "向前移动一个子词", + "keybinding.description.move_forward_one_word": "向前移动一个单词", + "keybinding.description.move_tab_down": "向下移动标签页", + "keybinding.description.move_tab_left": "向左移动标签页", + "keybinding.description.move_tab_right": "向右移动标签页", + "keybinding.description.move_tab_up": "向上移动标签页", + "keybinding.description.move_to_end_of_line": "移动到行尾", + "keybinding.description.move_to_end_of_paragraph": "移动到段落末尾", + "keybinding.description.move_to_line_end": "移动到行尾", + "keybinding.description.move_to_line_start": "移动到行首", + "keybinding.description.move_to_start_of_line": "移动到行首", + "keybinding.description.move_to_start_of_paragraph": "移动到段落开头", + "keybinding.description.move_to_the_end_of_the_buffer": "移动到缓冲区末尾", + "keybinding.description.move_to_the_end_of_the_paragraph": "移动到段落末尾", + "keybinding.description.move_to_the_start_of_the_buffer": "移动到缓冲区开头", + "keybinding.description.move_to_the_start_of_the_paragraph": "移动到段落开头", + "keybinding.description.navigation_palette": "导航面板", + "keybinding.description.new_agent_conversation": "新建智能体会话", + "keybinding.description.new_agent_pane": "新建智能体窗格", + "keybinding.description.new_agent_tab": "新建 Agent 标签页", + "keybinding.description.new_cloud_agent_tab": "新建云端 Agent 标签页", + "keybinding.description.new_personal_environment_variables": "新建个人环境变量", + "keybinding.description.new_personal_folder": "新建个人文件夹", + "keybinding.description.new_personal_notebook": "新建个人 Notebook", + "keybinding.description.new_personal_prompt": "新建个人提示词", + "keybinding.description.new_personal_workflow": "新建个人 Workflow", + "keybinding.description.new_team_environment_variables": "新建团队环境变量", + "keybinding.description.new_team_folder": "新建团队文件夹", + "keybinding.description.new_team_notebook": "新建团队 Notebook", + "keybinding.description.new_team_prompt": "新建团队提示词", + "keybinding.description.new_team_workflow": "新建团队 Workflow", + "keybinding.description.new_terminal_tab": "新建终端标签页", + "keybinding.description.open_ai_command_suggestions": "打开 AI 命令建议", + "keybinding.description.open_ai_rules": "打开 AI 规则", + "keybinding.description.open_block_context_menu": "打开块上下文菜单", + "keybinding.description.open_completions_menu": "打开补全菜单", + "keybinding.description.open_global_search": "打开全局搜索", + "keybinding.description.open_keybindings_editor": "打开快捷键编辑器", + "keybinding.description.open_left_panel": "打开左侧面板", + "keybinding.description.open_mcp_servers": "打开 MCP 服务器", + "keybinding.description.open_repository": "打开仓库", + "keybinding.description.open_settings": "打开设置", + "keybinding.description.open_settings_about": "打开设置:关于", + "keybinding.description.open_settings_account": "打开设置: 账户", + "keybinding.description.open_settings_ai": "打开设置:AI", + "keybinding.description.open_settings_appearance": "打开设置:外观", + "keybinding.description.open_settings_billing_and_usage": "打开设置:账单和用量", + "keybinding.description.open_settings_code": "打开设置:代码", + "keybinding.description.open_settings_environments": "打开设置:环境", + "keybinding.description.open_settings_features": "打开设置: 功能", + "keybinding.description.open_settings_file": "打开设置文件", + "keybinding.description.open_settings_keyboard_shortcuts": "打开设置:键盘快捷键", + "keybinding.description.open_settings_mcp_servers": "打开设置:MCP 服务器", + "keybinding.description.open_settings_privacy": "打开设置:隐私", + "keybinding.description.open_settings_referrals": "打开设置:推荐", + "keybinding.description.open_settings_shared_blocks": "打开设置:共享块", + "keybinding.description.open_settings_teams": "打开设置:团队", + "keybinding.description.open_settings_warpify": "打开设置:Warpify", + "keybinding.description.open_tab_configs_menu": "打开标签页配置菜单", + "keybinding.description.open_team_settings": "打开团队设置", + "keybinding.description.open_theme_picker": "打开主题选择器", + "keybinding.description.open_view_tree_debugger": "打开视图树调试器", + "keybinding.description.project_explorer": "项目浏览器", + "keybinding.description.quit_warp": "退出 Warp", + "keybinding.description.reinput_selected_commands": "重新输入所选命令", + "keybinding.description.reinput_selected_commands_as_root": "以 root 重新输入所选命令", + "keybinding.description.reload_file": "重新加载文件", + "keybinding.description.remove_the_previous_character": "删除前一个字符", + "keybinding.description.rename_the_current_pane": "重命名当前窗格", + "keybinding.description.rename_the_current_tab": "重命名当前标签页", + "keybinding.description.reopen_closed_session": "重新打开已关闭会话", + "keybinding.description.reset_font_size_to_default": "将字体大小重置为默认值", + "keybinding.description.reset_notebook_font_size": "重置笔记本字体大小", + "keybinding.description.reset_zoom_level_to_default": "将缩放级别重置为默认值", + "keybinding.description.resize_pane_move_divider_down": "调整窗格大小 > 向下移动分隔线", + "keybinding.description.resize_pane_move_divider_left": "调整窗格大小 > 向左移动分隔线", + "keybinding.description.resize_pane_move_divider_right": "调整窗格大小 > 向右移动分隔线", + "keybinding.description.resize_pane_move_divider_up": "调整窗格大小 > 向上移动分隔线", + "keybinding.description.restart_warp_ai": "重启 Warp AI", + "keybinding.description.run_selected_commands": "运行所选命令", + "keybinding.description.sample_process": "采样进程", + "keybinding.description.save_all_unsaved_files_in_code_review": "保存代码审查中的所有未保存文件", + "keybinding.description.save_file_as": "文件另存为", + "keybinding.description.save_new_launch_configuration": "保存新的启动配置", + "keybinding.description.save_workflow": "保存工作流", + "keybinding.description.scroll_down_half_a_page_vim": "向下滚动半页 (Vim)", + "keybinding.description.scroll_terminal_output_down_one_line": "终端输出向下滚动一行", + "keybinding.description.scroll_terminal_output_down_one_page": "终端输出向下滚动一页", + "keybinding.description.scroll_terminal_output_up_one_line": "终端输出向上滚动一行", + "keybinding.description.scroll_terminal_output_up_one_page": "终端输出向上滚动一页", + "keybinding.description.scroll_to_bottom_of_selected_block": "滚动到所选块底部", + "keybinding.description.scroll_to_top_of_selected_block": "滚动到所选块顶部", + "keybinding.description.scroll_up_half_a_page_vim": "向上滚动半页 (Vim)", + "keybinding.description.select_all": "全选", + "keybinding.description.select_and_move_to_the_bottom": "选择并移动到底部", + "keybinding.description.select_and_move_to_the_top": "选择并移动到顶部", + "keybinding.description.select_down": "向下选择", + "keybinding.description.select_next_command": "选择下一条命令", + "keybinding.description.select_one_character_to_the_left": "向左选择一个字符", + "keybinding.description.select_one_character_to_the_right": "向右选择一个字符", + "keybinding.description.select_one_subword_to_the_left": "向左选择一个子词", + "keybinding.description.select_one_subword_to_the_right": "向右选择一个子词", + "keybinding.description.select_one_word_to_the_left": "向左选择一个单词", + "keybinding.description.select_one_word_to_the_right": "向右选择一个单词", + "keybinding.description.select_previous_command": "选择上一条命令", + "keybinding.description.select_shell_command_at_cursor": "选择光标处的 Shell 命令", + "keybinding.description.select_the_closest_bookmark_down": "选择下方最近的书签", + "keybinding.description.select_the_closest_bookmark_up": "选择上方最近的书签", + "keybinding.description.select_to_end_of_line": "选择到行尾", + "keybinding.description.select_to_end_of_paragraph": "选择到段落末尾", + "keybinding.description.select_to_line_end": "选择到行尾", + "keybinding.description.select_to_line_start": "选择到行首", + "keybinding.description.select_to_start_of_line": "选择到行首", + "keybinding.description.select_to_start_of_paragraph": "选择到段落开头", + "keybinding.description.select_up": "向上选择", + "keybinding.description.send_feedback_opens_external_link": "发送反馈(打开外部链接)", + "keybinding.description.settings": "设置", + "keybinding.description.setup_guide": "设置指南", + "keybinding.description.share_current_session": "共享当前会话", + "keybinding.description.share_pane": "共享窗格", + "keybinding.description.share_selected_block": "共享所选块", + "keybinding.description.show_dedicated_hotkey_window": "显示专用热键窗口", + "keybinding.description.show_find_bar_in_code_review": "在代码审查中显示查找栏", + "keybinding.description.show_warp_network_log": "显示 Warp 网络日志", + "keybinding.description.stop_sharing_current_session": "停止共享当前会话", + "keybinding.description.stop_synchronizing_any_panes": "停止同步所有窗格", + "keybinding.description.switch_focus_to_left_panel": "切换焦点到左侧面板", + "keybinding.description.switch_focus_to_right_panel": "切换焦点到右侧面板", + "keybinding.description.switch_panes_down": "切换到下方窗格", + "keybinding.description.switch_panes_left": "切换到左侧窗格", + "keybinding.description.switch_panes_right": "切换到右侧窗格", + "keybinding.description.switch_panes_up": "切换到上方窗格", + "keybinding.description.switch_to_1st_tab": "切换到第 1 个标签页", + "keybinding.description.switch_to_2nd_tab": "切换到第 2 个标签页", + "keybinding.description.switch_to_3rd_tab": "切换到第 3 个标签页", + "keybinding.description.switch_to_4th_tab": "切换到第 4 个标签页", + "keybinding.description.switch_to_5th_tab": "切换到第 5 个标签页", + "keybinding.description.switch_to_6th_tab": "切换到第 6 个标签页", + "keybinding.description.switch_to_7th_tab": "切换到第 7 个标签页", + "keybinding.description.switch_to_8th_tab": "切换到第 8 个标签页", + "keybinding.description.switch_to_last_tab": "切换到最后一个标签页", + "keybinding.description.take_control_of_running_command": "接管正在运行的命令", + "keybinding.description.terminal_session": "终端会话", + "keybinding.description.toggle_agent_conversation_list_view": "切换 Agent 对话列表视图", + "keybinding.description.toggle_case_sensitive_search": "切换区分大小写搜索", + "keybinding.description.toggle_code_review": "切换代码审查", + "keybinding.description.toggle_command_palette": "切换命令面板", + "keybinding.description.toggle_comment": "切换注释", + "keybinding.description.toggle_conversation_details_panel": "切换会话详情面板", + "keybinding.description.toggle_file_navigation_in_code_review": "切换代码审查中的文件导航", + "keybinding.description.toggle_files_palette": "切换文件面板", + "keybinding.description.toggle_fullscreen": "切换全屏", + "keybinding.description.toggle_inline_code_styling": "切换行内代码样式", + "keybinding.description.toggle_keyboard_shortcuts": "切换键盘快捷键", + "keybinding.description.toggle_maximize_active_pane": "切换最大化活动窗格", + "keybinding.description.toggle_maximize_code_review_panel": "切换最大化 Code Review 面板", + "keybinding.description.toggle_mouse_reporting": "切换鼠标上报", + "keybinding.description.toggle_navigation_palette": "切换导航面板", + "keybinding.description.toggle_notification_mailbox": "切换通知收件箱", + "keybinding.description.toggle_project_explorer": "切换项目浏览器", + "keybinding.description.toggle_pty_recording_for_session": "切换会话 PTY 录制", + "keybinding.description.toggle_regular_expression_search": "切换正则表达式搜索", + "keybinding.description.toggle_resource_center": "切换资源中心", + "keybinding.description.toggle_rich_text_debug_mode": "切换富文本调试模式", + "keybinding.description.toggle_sticky_command_header": "切换固定命令标题", + "keybinding.description.toggle_sticky_command_header_in_active_pane": "切换活动窗格中的固定命令标题", + "keybinding.description.toggle_strikethrough_styling": "切换删除线样式", + "keybinding.description.toggle_synchronizing_all_panes_in_all_tabs": "切换同步所有标签页中的所有窗格", + "keybinding.description.toggle_synchronizing_all_panes_in_current_tab": "切换同步当前标签页中的所有窗格", + "keybinding.description.toggle_team_workflows_modal": "切换团队工作流弹窗", + "keybinding.description.toggle_the_agent_management_view": "切换 Agent 管理视图", + "keybinding.description.toggle_underline_styling": "切换下划线样式", + "keybinding.description.toggle_vertical_tabs_panel": "切换垂直标签页面板", + "keybinding.description.toggle_warp_ai": "切换 Warp AI", + "keybinding.description.toggle_warp_drive": "切换 Warp Drive", + "keybinding.description.trigger_auto_detection": "触发自动检测", + "keybinding.description.turn_notifications_off": "关闭通知", + "keybinding.description.turn_notifications_on": "开启通知", + "keybinding.description.unfold": "展开", + "keybinding.description.uninstall_oz_cli_command": "卸载 Oz CLI 命令", + "keybinding.description.view_latest_changelog": "查看最新更新日志", + "keybinding.description.view_privacy_policy_opens_external_link": "查看隐私政策(打开外部链接)", + "keybinding.description.view_shared_blocks": "查看共享块...", + "keybinding.description.view_user_docs_opens_external_link": "查看用户文档(打开外部链接)", + "keybinding.description.view_warp_logs": "查看 Warp 日志", + "keybinding.description.warp_drive": "Warp Drive", + "keybinding.description.warpify_ssh_session": "Warpify SSH 会话", + "keybinding.description.warpify_subshell": "Warpify 子 Shell", + "keybinding.description.workflows": "工作流", + "keybinding.description.write_current_codebase_index_snapshot": "写入当前代码库索引快照", + "launch_configs.a11y.save_config_modal": "保存配置弹窗", + "launch_configs.a11y.save_config_modal_help": "输入用于保存当前窗口、标签页和窗格配置的文件名。按 Enter 保存启动配置,按 Esc 退出保存配置弹窗。", + "launch_configs.description": "这会将当前窗口、标签页和窗格配置保存到文件,方便之后再次打开。", + "launch_configs.description_with_shortcut": "这会将当前窗口、标签页和窗格配置保存到文件,方便之后通过 {shortcut} 再次打开。", + "launch_configs.failed_file_already_exists": "保存失败。已存在同名启动配置。", + "launch_configs.failed_saving": "保存时遇到问题。", + "launch_configs.link_to_documentation": "文档链接", + "launch_configs.open_yaml_file": "打开 YAML 文件", + "launch_configs.save_configuration": "保存配置", + "launch_configs.save_current_configuration": "保存当前配置", + "launch_configs.saved_successfully_to": "已成功保存到", + "launch_configs.yaml_saved_to": "\nYAML 文件已保存到", + "menu.a11y.action_selected": "已选择操作", + "menu.a11y.instructions.close_menu": "按 Escape 键关闭菜单", + "menu.a11y.instructions.close_submenu": "将焦点移出子菜单会关闭该子菜单", + "menu.a11y.instructions.execute_action": "按 Enter 键执行所选菜单项操作", + "menu.a11y.instructions.open_selected_submenu": "按右方向键打开所选子菜单", + "menu.a11y.instructions.select_item": "按上方向键或下方向键选择菜单项", + "menu.a11y.instructions.select_item_open_submenu": "按上方向键或下方向键选择菜单项。按右方向键打开子菜单", + "menu.a11y.item_expanded": "已展开:{item}", + "menu.a11y.item_selected": "已选择:{item}", + "menu.a11y.menu_closed": "菜单已关闭", + "menu.a11y.submenu_closed": "子菜单已关闭", + "menu.a11y.submenu_expanded": "子菜单已展开", + "menu.pane.maximize": "最大化窗格", + "menu.pane.minimize": "最小化窗格", + "node_version.install_nvm": "安装 nvm", + "notebooks.a11y.change_code_block_language": "将代码块语言改为 {language}", + "notebooks.a11y.convert_to": "转换为{block_type}", + "notebooks.a11y.copy_code_block": "复制代码块", + "notebooks.a11y.copy_link": "复制链接", + "notebooks.a11y.cut_line_left": "剪切左侧行内容", + "notebooks.a11y.cut_line_right": "剪切右侧行内容", + "notebooks.a11y.cut_word_left": "剪切左侧单词", + "notebooks.a11y.cut_word_right": "剪切右侧单词", + "notebooks.a11y.delete_line_left": "删除左侧行内容", + "notebooks.a11y.delete_line_right": "删除右侧行内容", + "notebooks.a11y.delete_word_left": "删除左侧单词", + "notebooks.a11y.delete_word_right": "删除右侧单词", + "notebooks.a11y.deselect_command": "取消选择命令", + "notebooks.a11y.deselect_command_help": "从选择命令切换为选择文本", + "notebooks.a11y.edit_link": "编辑链接", + "notebooks.a11y.insert_block": "插入{block_type}块", + "notebooks.a11y.notebook_with_title": "{title} notebook", + "notebooks.a11y.open_block_insertion_menu": "打开块插入菜单", + "notebooks.a11y.open_embedded_object_search_menu": "打开嵌入对象搜索菜单", + "notebooks.a11y.open_link": "打开链接:{link}", + "notebooks.a11y.pasting": "正在粘贴:{text}", + "notebooks.a11y.remove_link": "移除链接", + "notebooks.a11y.secondary_click_on": "对 {link} 执行二级点击", + "notebooks.a11y.selected_workflow": "已选择工作流:{command}", + "notebooks.a11y.shift_tab": "Shift-tab", + "notebooks.a11y.show_character_palette": "显示字符面板", + "notebooks.a11y.show_find_bar": "显示查找栏", + "notebooks.a11y.style_toggle": "{style}{state}", + "notebooks.a11y.style.bold": "粗体", + "notebooks.a11y.style.inline_code": "行内代码", + "notebooks.a11y.style.italic": "斜体", + "notebooks.a11y.style.off": "关闭", + "notebooks.a11y.style.on": "开启", + "notebooks.a11y.style.strikethrough": "删除线", + "notebooks.a11y.style.underline": "下划线", + "notebooks.a11y.toggle_task_list": "切换任务列表", + "notebooks.binding.copy_rich_text_buffer": "复制富文本缓冲区", + "notebooks.binding.copy_rich_text_selection": "复制富文本选区", + "notebooks.binding.create_or_edit_link": "创建或编辑链接", + "notebooks.binding.cut_all_left": "剪切左侧所有内容", + "notebooks.binding.cut_all_right": "剪切右侧所有内容", + "notebooks.binding.cut_word_left": "剪切左侧单词", + "notebooks.binding.cut_word_right": "剪切右侧单词", + "notebooks.binding.decrease_font_size": "减小 notebook 字号", + "notebooks.binding.decrease_font_size_short": "减小字号", + "notebooks.binding.delete_all_left": "删除左侧所有内容", + "notebooks.binding.delete_all_right": "删除右侧所有内容", + "notebooks.binding.delete_word_left": "删除左侧单词", + "notebooks.binding.delete_word_right": "删除右侧单词", + "notebooks.binding.deselect_shell_commands": "取消选择 shell 命令", + "notebooks.binding.end": "行尾", + "notebooks.binding.find_in_notebook": "在 Notebook 中查找", + "notebooks.binding.focus_next_match": "聚焦下一个匹配项", + "notebooks.binding.focus_previous_match": "聚焦上一个匹配项", + "notebooks.binding.focus_terminal_input_from_file": "从文件聚焦终端输入框", + "notebooks.binding.focus_terminal_input_from_notebook": "从 notebook 聚焦终端输入框", + "notebooks.binding.home": "行首", + "notebooks.binding.increase_font_size": "增大 notebook 字号", + "notebooks.binding.increase_font_size_short": "增大字号", + "notebooks.binding.log_editor_state": "记录编辑器状态", + "notebooks.binding.move_backward_one_word": "向后移动一个单词", + "notebooks.binding.move_cursor_down": "向下移动光标", + "notebooks.binding.move_cursor_left": "向左移动光标", + "notebooks.binding.move_cursor_right": "向右移动光标", + "notebooks.binding.move_cursor_up": "向上移动光标", + "notebooks.binding.move_forward_one_word": "向前移动一个单词", + "notebooks.binding.move_to_paragraph_end": "移动到段落末尾", + "notebooks.binding.move_to_paragraph_start": "移动到段落开头", + "notebooks.binding.reload_file": "重新加载文件", + "notebooks.binding.remove_previous_character": "删除前一个字符", + "notebooks.binding.reset_font_size": "重置 notebook 字号", + "notebooks.binding.run_selected_commands": "运行选中的命令", + "notebooks.binding.select_down": "向下选择", + "notebooks.binding.select_next_command": "选择下一个命令", + "notebooks.binding.select_one_character_left": "向左选择一个字符", + "notebooks.binding.select_one_character_right": "向右选择一个字符", + "notebooks.binding.select_one_word_left": "向左选择一个单词", + "notebooks.binding.select_one_word_right": "向右选择一个单词", + "notebooks.binding.select_previous_command": "选择上一个命令", + "notebooks.binding.select_shell_command_at_cursor": "选择光标处的 shell 命令", + "notebooks.binding.select_to_line_end": "选择到行尾", + "notebooks.binding.select_to_line_start": "选择到行首", + "notebooks.binding.select_to_paragraph_end": "选择到段落末尾", + "notebooks.binding.select_to_paragraph_start": "选择到段落开头", + "notebooks.binding.select_up": "向上选择", + "notebooks.binding.toggle_case_sensitive_search": "切换区分大小写搜索", + "notebooks.binding.toggle_debug_mode": "切换富文本调试模式", + "notebooks.binding.toggle_inline_code": "切换行内代码样式", + "notebooks.binding.toggle_regex_search": "切换正则表达式搜索", + "notebooks.binding.toggle_strikethrough": "切换删除线样式", + "notebooks.binding.toggle_underline": "切换下划线样式", + "notebooks.block.bulleted_list": "项目符号列表", + "notebooks.block.code": "代码", + "notebooks.block.command": "命令", + "notebooks.block.heading": "标题 {level}", + "notebooks.block.numbered_list": "编号列表", + "notebooks.block.text": "文本", + "notebooks.block.todo_list": "待办列表", + "notebooks.command.open_full_screen": "全屏打开", + "notebooks.command.raw": "原始", + "notebooks.command.rendered": "渲染后", + "notebooks.command.run_in_terminal": "在终端中运行", + "notebooks.copy_all": "全部复制", + "notebooks.copy_all_tooltip": "将 notebook 内容复制到剪贴板", + "notebooks.copy_to_personal": "复制到个人空间", + "notebooks.copy_to_personal_tooltip": "将 notebook 内容复制到你的个人工作区", + "notebooks.editor_is_editing": "{editor} 正在编辑", + "notebooks.error.content_contains_secrets": "此 notebook 的内容包含密钥,无法保存", + "notebooks.error.title_contains_secrets": "此 notebook 的标题包含密钥,无法保存", + "notebooks.file.could_not_read": "无法读取 {source}", + "notebooks.file.loading": "正在加载 {source}...", + "notebooks.file.missing_source_file": "缺少源文件", + "notebooks.file.open_in_editor": "在编辑器中打开", + "notebooks.file.refresh_file": "刷新文件", + "notebooks.insert.divider": "分割线", + "notebooks.insert.embed": "嵌入", + "notebooks.insert.insert_block": "插入块", + "notebooks.link_editor.apply_link": "应用链接", + "notebooks.link_editor.link_placeholder": "链接(网页或文件)", + "notebooks.link.edit_markdown_file": "编辑 Markdown 文件", + "notebooks.link.error.broken_file_link": "文件链接已损坏", + "notebooks.link.error.file_not_found": "未找到文件", + "notebooks.link.error.no_base_directory": "没有基础目录", + "notebooks.link.new_session": "新会话", + "notebooks.link.new_session_tooltip": "在此目录中打开新的终端会话", + "notebooks.link.open_in_terminal_session": "在终端会话中打开", + "notebooks.move_to_space": "移动到 {space}", + "notebooks.moved_to_trash": "此 notebook 已移至废纸篓", + "notebooks.no_longer_access": "你已无法访问此 notebook", + "notebooks.refresh": "刷新 notebook", + "notebooks.restore_from_trash": "从废纸篓恢复 notebook", + "notebooks.sync.conflict": "此 notebook 无法保存,因为你编辑时已有其他更改。请复制你的内容并刷新。", + "notebooks.sync.feature_not_available": "此 notebook 无法保存到服务器,因为该功能暂时不可用。更改已保存到本地,请稍后重试。", + "notebooks.workflow.command_from": "来自 {source} 的命令", + "onboarding.agent.autonomy": "自主程度", + "onboarding.agent.autonomy_set_by_team_workspace": "由团队工作区设置", + "onboarding.agent.autonomy_team_workspace_description": "自主程度设置由你的团队工作区统一配置。", + "onboarding.agent.autonomy.full": "完全", + "onboarding.agent.autonomy.full_description": "无需询问即可运行命令、编写代码和读取文件。", + "onboarding.agent.autonomy.none": "无", + "onboarding.agent.autonomy.none_description": "未经你批准不会执行任何操作。", + "onboarding.agent.autonomy.partial": "部分", + "onboarding.agent.autonomy.partial_description": "可以规划、读取文件并执行低风险命令。修改内容或执行敏感命令前会先询问。", + "onboarding.agent.default_model": "默认模型", + "onboarding.agent.disable_warp_agent": "停用 Warp Agent", + "onboarding.agent.plan_activated": "计划已成功激活。所有高级模型均可使用。", + "onboarding.agent.premium": "高级", + "onboarding.agent.recommended": "推荐", + "onboarding.agent.subtitle": "选择内置 agent 的默认设置。", + "onboarding.agent.title": "自定义 Warp Agent", + "onboarding.agent.upgrade_banner.subtitle": "前沿模型需要付费计划。", + "onboarding.agent.upgrade_banner.title": "升级以使用高级模型。", + "onboarding.agent.upgrade.browser_not_launched_prefix": "如果浏览器没有打开,", + "onboarding.agent.upgrade.click_here": "点击这里", + "onboarding.agent.upgrade.copy_url": "复制 URL", + "onboarding.agent.upgrade.open_page_manually": "并手动打开页面。", + "onboarding.agent.upgrade.paste_token_suffix": "以粘贴浏览器中的令牌。", + "onboarding.callout.agent_mode.no_project_text": "Agent mode 会为你的问题和任务创建独立对话,因此你可以在不中断终端工作流的情况下继续追问。随时按 {keybinding} 返回终端模式。", + "onboarding.callout.agent_mode.title": "你正在使用 agent mode", + "onboarding.callout.agent_mode.with_project_text": "Agent mode 会为你的问题和任务创建独立对话,因此你可以在不中断终端工作流的情况下继续追问。\n\n提交下方查询,让 agent 初始化此项目;也可以点击 ⊗ 清空输入并开始自己的任务。", + "onboarding.callout.back_to_terminal": "返回终端", + "onboarding.callout.enable_natural_language_detection": "启用自然语言检测", + "onboarding.callout.initialize": "初始化", + "onboarding.callout.meet_input.text": "终端输入框既接受终端命令,也接受 agent 提示词,并会自动检测你正在使用哪一种。使用 {keybinding} 可将输入锁定为 Agent 模式(自然语言)或终端模式(命令)。", + "onboarding.callout.meet_input.title": "认识 Warp 输入框", + "onboarding.callout.skip_initialization": "跳过初始化", + "onboarding.callout.talk_to_agent.text": "你可以输入自然语言来与 agent 交互。提交下方查询开始:What tests exist in this repo, how are they structured, and what do they cover?", + "onboarding.callout.talk_to_agent.title": "与 agent 对话", + "onboarding.callout.terminal_mode.in_terminal_title": "你正在使用终端模式", + "onboarding.callout.terminal_mode.text": "在这里像普通终端一样运行命令。如果你输入自然语言问题或任务,Warp 可以建议在 agent mode 中打开。你始终可以使用 {keybinding} 手动切换。", + "onboarding.callout.terminal_mode.welcome_title": "欢迎使用终端模式", + "onboarding.common.disabled": "已停用", + "onboarding.common.enabled": "已启用", + "onboarding.common.submit": "提交", + "onboarding.customize.code_review": "代码审查", + "onboarding.customize.conversation_history": "对话历史", + "onboarding.customize.file_explorer": "文件资源管理器", + "onboarding.customize.global_file_search": "全局文件搜索", + "onboarding.customize.horizontal": "水平", + "onboarding.customize.subtitle": "根据你的工作方式调整功能和界面。", + "onboarding.customize.tab_styling": "标签页样式", + "onboarding.customize.title": "自定义 Warp", + "onboarding.customize.tools_panel": "工具面板", + "onboarding.customize.vertical": "垂直", + "onboarding.features.agents_over_ssh": "通过 SSH 使用 Agents", + "onboarding.features.codebase_context": "代码库上下文", + "onboarding.features.next_command_predictions": "下一条命令预测", + "onboarding.features.oz_cloud_agents_platform": "Oz 云端 agents 平台", + "onboarding.features.prompt_suggestions": "提示词建议", + "onboarding.features.remote_control": "通过 Claude Code、Codex 和其他 agents 进行远程控制", + "onboarding.features.session_sharing": "会话共享", + "onboarding.features.warp_agents": "Warp agents", + "onboarding.features.warp_drive": "Warp Drive", + "onboarding.free_user.agent_option.description": "使用 Oz(Warp 内置 agent)迭代、规划和构建。可在本地或云端使用。", + "onboarding.free_user.agent_option.title": "使用 Warp 内置 agent 进行 agent 驱动开发", + "onboarding.free_user.subscribe": "订阅", + "onboarding.free_user.subscribe_item.cloud_agents": "扩展云端 agents 访问权限", + "onboarding.free_user.subscribe_item.cloud_storage": "无限云端对话存储", + "onboarding.free_user.subscribe_item.credits": "每月 1,500 点额度", + "onboarding.free_user.subscribe_item.email_support": "私人邮件支持", + "onboarding.free_user.subscribe_item.frontier_models": "访问 OpenAI、Anthropic 和 Google 的前沿模型", + "onboarding.free_user.subscribe_item.indexing_limits": "最高代码库索引额度", + "onboarding.free_user.subscribe_item.reload_credits": "访问 Reload 额度和基于用量的折扣", + "onboarding.free_user.subscribe_item.warp_drive": "无限 Warp Drive 对象与协作", + "onboarding.free_user.subscribe_title": "订阅以在 Warp 中使用 agent 驱动开发。", + "onboarding.free_user.terminal_option.description": "一个现代终端,支持第三方 agents(Claude Code、Codex、Gemini CLI)和经典终端工作流。", + "onboarding.free_user.terminal_option.title": "使用第三方 agents 的经典终端", + "onboarding.free_user.title": "让我们开始吧。", + "onboarding.get_warping": "开始使用 Warp", + "onboarding.intention.agent.description": "以 agent 为先、同时具备一流终端支持的体验。获得终端和 agent 驱动开发 AI 功能,例如:", + "onboarding.intention.agent.title": "使用 AI agents 更快构建", + "onboarding.intention.subtitle": "你想如何工作?", + "onboarding.intention.terminal.description": "一个针对速度、上下文和控制能力优化的现代终端,不包含 AI。", + "onboarding.intention.terminal.no_ai_features": "无 AI 功能", + "onboarding.intention.terminal.title": "只使用终端", + "onboarding.intention.title": "欢迎使用 Warp", + "onboarding.intro.already_have_account_prefix": "已有账号?", + "onboarding.intro.get_started": "开始使用", + "onboarding.intro.log_in": "登录", + "onboarding.intro.subtitle": "内置前沿 agents 的现代终端。", + "onboarding.intro.welcome_to_warp": "欢迎使用 Warp", + "onboarding.project.initialize_automatically": "自动初始化项目", + "onboarding.project.initialize_automatically_description": "准备项目环境、构建代码索引并生成项目规则,让 agent 获得更深入的理解和更好的表现。", + "onboarding.project.open_local_folder": "打开本地文件夹", + "onboarding.project.subtitle": "设置项目,以便在 Warp 中优化编码体验。", + "onboarding.project.title": "打开项目", + "onboarding.theme.analytics_opt_out_prefix": "如果你想退出分析统计,可以调整你的", + "onboarding.theme.privacy_settings": "隐私设置", + "onboarding.theme.subtitle": "点击或使用方向键选择,按 Enter 确认。", + "onboarding.theme.terms_of_service": "服务条款", + "onboarding.theme.title": "选择主题", + "onboarding.theme.tos_prefix": "继续即表示你同意 Warp 的", + "onboarding.third_party.cli_agent_toolbar": "CLI agent 工具栏", + "onboarding.third_party.notifications": "通知", + "onboarding.third_party.subtitle": "选择使用 Claude Code、Codex 和 Gemini 等 agents 的默认设置。", + "onboarding.third_party.title": "自定义第三方 agents", + "pane_group.binding.add_repository": "添加仓库", + "pane_group.binding.share_pane": "共享窗格", + "pane_group.binding.terminal_session": "终端会话", + "pane_group.default_shell_unsupported": "Warp 当前不支持你的默认 shell,已回退到 zsh。", + "pane_group.get_started.tagline": "Agentic 开发环境", + "pane_group.get_started.title": "开始使用", + "pane_group.get_started.welcome": "欢迎使用 Warp", + "pane_group.header.read_only": "只读", + "pane_group.header.toolbelt_feature_popup": "打开文件并查看代码 diff", + "pane_group.header.unsharable_conversation_tooltip": "此对话无法共享,因为它未存储在云端。\n要同步到云端并共享,请在设置 > 隐私中启用相应设置,然后再发起一次请求。", + "pane_group.local_child.failed_create_hidden_agent_pane": "无法为本地子 Agent 创建隐藏窗格。", + "pane_group.local_child.failed_create_hidden_harness_pane": "无法为本地子 harness 创建隐藏窗格。", + "pane_group.local_child.failed_create_task": "创建本地子任务失败:{error}", + "pane_group.local_child.missing_harness_type": "缺少本地子 harness 类型。", + "pane_group.local_child.missing_working_directory": "无法为本地 {harness} 子任务解析工作目录。", + "pane_group.local_child.no_supported_shell": "本地子 harness 当前需要检测到 bash、zsh 或 fish 会话。", + "pane_group.local_child.powershell_unsupported": "本地子 harness 当前需要 bash、zsh 或 fish;暂不支持 PowerShell。", + "pane_group.local_child.unsupported_harness": "不支持的本地子 harness:“{harness}”。", + "pane_group.welcome.title": "新标签页", + "prompt.editor.same_line_prompt": "同一行提示符", + "prompt.editor.save_changes": "保存更改", + "prompt.editor.separator": "分隔符", + "prompt.editor.shell_prompt": "Shell 提示符 (PS1)", + "prompt.editor.title": "编辑提示符", + "prompt.editor.warp_terminal_prompt": "Warp 终端提示符", + "quit_warning.button.cancel": "取消", + "quit_warning.button.dont_save": "不保存", + "quit_warning.button.save": "保存", + "quit_warning.button.show_running_processes": "显示正在运行的进程", + "quit_warning.button.yes_close": "是,关闭", + "quit_warning.button.yes_quit": "是,退出", + "quit_warning.file.this_file": "此文件", + "quit_warning.process.in_tabs": ",分布在 {count} 个标签页中", + "quit_warning.process.in_windows": ",分布在 {count} 个窗口中", + "quit_warning.process.running_many": "你有 {count} 个进程正在运行", + "quit_warning.process.running_one": "你有 {count} 个进程正在运行", + "quit_warning.scope.default": "。", + "quit_warning.scope.this_pane": ",位于此窗格中。", + "quit_warning.scope.this_tab": ",位于此标签页中。", + "quit_warning.scope.this_window": ",位于此窗口中。", + "quit_warning.shared_session.many": "你正在共享 {count} 个会话{scope}", + "quit_warning.shared_session.one": "你正在共享 {count} 个会话{scope}", + "quit_warning.title.close_pane": "关闭窗格?", + "quit_warning.title.close_tab": "关闭标签页?", + "quit_warning.title.close_tabs": "关闭标签页?", + "quit_warning.title.close_window": "关闭窗口?", + "quit_warning.title.quit_warp": "退出 Warp?", + "quit_warning.title.save_changes": "保存更改?", + "quit_warning.unsaved_file.changes": "你有未保存的文件更改{scope}", + "quit_warning.unsaved_file.save_changes": "是否要保存你对 {file} 所做的更改?如果不保存,这些更改将被丢弃。", + "remote_server.codebase_index.indexing_not_started": "无法索引远程代码库,因为索引未开始。", + "remote_server.codebase_index.max_indices_reached": "无法索引远程代码库,因为已达到代码库索引数量上限。", + "remote_server.codebase_index.missing_root_hash": "远程代码库索引缺少根哈希。", + "remote_server.codebase_index.remote_host_disconnected": "远程主机当前已断开连接。", + "remote_server.codebase_index.resync_not_indexed": "无法重新同步远程代码库,因为它尚未被索引。", + "remote_server.codebase_index.search_unavailable": "远程代码库搜索不可用。", + "resource_center.changelog.bug_fixes": "错误修复", + "resource_center.changelog.fetch_error": "无法获取最新更新日志。", + "resource_center.changelog.improvements": "改进", + "resource_center.changelog.new_features": "新功能", + "resource_center.changelog.read_all": "阅读所有更新日志", + "resource_center.content.custom_prompt.description": "设置 Warp 以使用你的 PS1 prompt", + "resource_center.content.custom_prompt.title": "使用自定义 prompt", + "resource_center.content.how_warp_uses_warp.description": "了解 Warp 工程团队如何使用他们最喜欢的功能", + "resource_center.content.how_warp_uses_warp.title": "Warp 如何使用 Warp", + "resource_center.content.ide_integration.description": "配置 Warp,使其可从你最常用的开发工具中启动", + "resource_center.content.ide_integration.title": "将 Warp 与 IDE 集成", + "resource_center.content.read_article": "阅读文章", + "resource_center.content.view_documentation": "查看文档", + "resource_center.feature.ai_command_search.description": "用自然语言生成 shell 命令。", + "resource_center.feature.ai_command_search.title": "AI 命令搜索", + "resource_center.feature.block_action.description": "右键点击块即可复制/粘贴、分享等。", + "resource_center.feature.block_action.title": "对块执行操作", + "resource_center.feature.command_palette.description": "通过键盘访问 Warp 的所有功能。", + "resource_center.feature.command_palette.title": "打开命令面板", + "resource_center.feature.command_search.description": "查找并运行之前执行过的命令、工作流等。", + "resource_center.feature.command_search.title": "命令搜索", + "resource_center.feature.create_first_block.description": "运行命令即可看到命令和输出被组合在一起。", + "resource_center.feature.create_first_block.title": "创建第一个块", + "resource_center.feature.launch_configuration.description": "保存当前窗口、标签页和窗格配置。", + "resource_center.feature.launch_configuration.title": "启动配置", + "resource_center.feature.navigate_blocks.description": "点击选择块,并使用方向键导航。", + "resource_center.feature.navigate_blocks.title": "导航块", + "resource_center.feature.split_panes.description": "将标签页拆分成多个窗格,创建理想布局。", + "resource_center.feature.split_panes.title": "拆分窗格", + "resource_center.feature.theme_picker.description": "选择主题,让 Warp 更符合你的偏好。", + "resource_center.feature.theme_picker.title": "设置主题", + "resource_center.footer.docs": "文档", + "resource_center.footer.feedback": "反馈", + "resource_center.footer.slack": "Slack", + "resource_center.header.keyboard_shortcuts": "键盘快捷键", + "resource_center.header.warp_essentials": "Warp 入门", + "resource_center.invite_friend": "邀请朋友使用 Warp", + "resource_center.keybindings.additional.hide_others": "隐藏其他应用", + "resource_center.keybindings.additional.hide_warp": "隐藏 Warp", + "resource_center.keybindings.additional.minimize": "最小化", + "resource_center.keybindings.additional.open_new_window": "打开新窗口", + "resource_center.keybindings.additional.quit_warp": "退出 Warp", + "resource_center.keybindings.blocks": "块", + "resource_center.keybindings.essentials": "基础", + "resource_center.keybindings.fundamentals": "常用", + "resource_center.keybindings.input_editor": "输入编辑器", + "resource_center.keybindings.settings_hint": "前往设置 > 键盘快捷键以配置自定义快捷键", + "resource_center.keybindings.settings_link": "这里。", + "resource_center.keybindings.terminal": "终端", + "resource_center.keybindings.toggle_hint": "用于切换此面板", + "resource_center.mark_all_as_read": "全部标记为已读", + "resource_center.section.advanced_setup": "高级设置", + "resource_center.section.getting_started": "开始使用", + "resource_center.section.maximize_warp": "最大化利用 Warp", + "resource_center.section.whats_new": "新功能", + "root_view.binding.hide_all_windows": "隐藏所有窗口", + "root_view.binding.hide_dedicated_hotkey_window": "隐藏专用热键窗口", + "root_view.binding.show_dedicated_hotkey_window": "显示专用热键窗口", + "root_view.binding.toggle_fullscreen": "切换全屏", + "root_view.create_environment": "创建环境", + "root_view.resource_not_found": "资源不存在或访问被拒绝", + "search.a11y.error_finding_results": "查找结果时出错", + "search.a11y.loading_suggestions_prefix": "正在加载建议:", + "search.a11y.press_enter_to_confirm": "按 Enter 确认。", + "search.a11y.press_enter_to_launch_session": "按 Enter 启动此会话。", + "search.a11y.press_enter_to_navigate_session": "按 Enter 导航到此会话。", + "search.a11y.selected_prefix": "已选择", + "search.a11y.use_binding_prefix": "以后可使用快捷键", + "search.a11y.use_binding_suffix": "运行此操作。", + "search.ai_context_menu.blocks.no_output": "无输出", + "search.ai_context_menu.blocks.prefix": "块", + "search.ai_context_menu.category.blocks": "块", + "search.ai_context_menu.category.code": "代码", + "search.ai_context_menu.category.commands": "命令", + "search.ai_context_menu.category.conversations": "会话", + "search.ai_context_menu.category.diff_set": "Diff 集", + "search.ai_context_menu.category.diffs": "Diff", + "search.ai_context_menu.category.docs": "文档", + "search.ai_context_menu.category.files_and_folders": "文件和文件夹", + "search.ai_context_menu.category.notebooks": "笔记本", + "search.ai_context_menu.category.plans": "计划", + "search.ai_context_menu.category.recent_block": "最近的块", + "search.ai_context_menu.category.recent_diff": "最近的 Diff", + "search.ai_context_menu.category.rules": "规则", + "search.ai_context_menu.category.servers": "服务器和集成", + "search.ai_context_menu.category.skills": "技能", + "search.ai_context_menu.category.tasks": "历史任务", + "search.ai_context_menu.category.terminal": "终端", + "search.ai_context_menu.category.web": "Web", + "search.ai_context_menu.category.workflows": "工作流", + "search.ai_context_menu.code_search_failed": "代码搜索失败", + "search.ai_context_menu.code_symbol_in": "位于", + "search.ai_context_menu.code_symbol_prefix": "代码符号", + "search.ai_context_menu.code_symbols_indexing": "正在索引代码符号...", + "search.ai_context_menu.command_prefix": "命令", + "search.ai_context_menu.conversation_prefix": "对话", + "search.ai_context_menu.diffset.changes_vs_main": "与 main 分支相比的变更", + "search.ai_context_menu.diffset.changes_vs_prefix": "相较于 ", + "search.ai_context_menu.diffset.compared_to_prefix": "所有变更,相较于 ", + "search.ai_context_menu.diffset.main_desc": "与 main 分支相比的所有变更", + "search.ai_context_menu.diffset.uncommitted_changes": "未提交的变更", + "search.ai_context_menu.diffset.uncommitted_desc": "工作目录中的所有未提交变更", + "search.ai_context_menu.directory_prefix": "目录", + "search.ai_context_menu.file_prefix": "文件", + "search.ai_context_menu.loading_results": "正在加载结果...", + "search.ai_context_menu.no_results": "未找到结果", + "search.ai_context_menu.notebook_prefix": "笔记本", + "search.ai_context_menu.rule_prefix": "规则", + "search.ai_context_menu.skill_prefix": "技能", + "search.ai_context_menu.time.days_ago_suffix": " 天前", + "search.ai_context_menu.time.hours_ago_suffix": " 小时前", + "search.ai_context_menu.time.just_now": "刚刚", + "search.ai_context_menu.time.minutes_ago_suffix": " 分钟前", + "search.ai_context_menu.workflow_prefix": "工作流", + "search.command_palette.cannot_start_conversation": "Agent 正在监控命令时,无法开始新会话。", + "search.command_palette.cannot_switch_conversations": "Agent 正在监控命令时,无法切换会话。", + "search.command_palette.placeholder": "搜索命令", + "search.command_palette.tabs.title_with_index": "[标签页 {index}] {title}", + "search.command_search.a11y.help": "搜索你的历史记录、工作流等内容。输入后使用上下箭头浏览搜索结果。按 Enter 接受所选结果并插入到终端输入中。按 Escape 关闭。", + "search.command_search.a11y.result_accepted": "已接受结果。", + "search.command_search.a11y.result_accepted_help": "执行前,你可以在这里编辑命令,然后按 Enter。", + "search.command_search.a11y.result_executed": "已执行结果", + "search.command_search.a11y.result_executed_help": "按 Cmd-Up 可跳转到命令输出。", + "search.command_search.a11y.title": "命令搜索", + "search.command_search.loading": "正在加载...", + "search.command_search.no_results": "未找到结果。", + "search.command_search.out_of_credits_contact_admin": "你的 AI 额度似乎已用完。请联系团队管理员升级以获取更多额度。", + "search.command_search.out_of_credits_prefix": "你的 AI 额度似乎已用完。", + "search.command_search.out_of_credits_suffix": "以获取更多额度。", + "search.command_search.placeholder": "搜索历史记录、工作流等内容", + "search.command_search.result.ai_query_prefix": "AI 查询", + "search.command_search.result.environment_variables_prefix": "环境变量", + "search.command_search.result.history_item_prefix": "历史项", + "search.command_search.result.notebook_prefix": "笔记本", + "search.command_search.result.project_prefix": "项目", + "search.command_search.result.ran_prefix": "运行于", + "search.command_search.result.untitled": "未命名", + "search.command_search.result.workflow_prefix": "工作流", + "search.command_search.upgrade": "升级", + "search.command_search.warp_ai.error.bad_prompt": "未找到结果。请尝试更具体的查询。", + "search.command_search.warp_ai.error.provider_error": "出了点问题。请重试。", + "search.command_search.warp_ai.error.rate_limited": "你的 AI 额度似乎已用完。请稍后再试。", + "search.command_search.warp_ai.open_body": "向 Warp AI 请求命令建议", + "search.command_search.warp_ai.prefix": "Warp AI", + "search.command_search.warp_ai.translate_body": "使用 Warp AI 转换为 shell 命令", + "search.command_search.zero_state.example_queries": "示例查询", + "search.command_search.zero_state.looking_for": "我想查找...", + "search.command_search.zero_state.title": "命令搜索", + "search.conversations.conversation": "会话", + "search.conversations.fork_conversation_tooltip": "Fork 会话", + "search.conversations.fork_current_conversation": "Fork 当前会话", + "search.conversations.new_conversation": "新建会话", + "search.conversations.press_enter_to_create": "按 Enter 创建新会话。", + "search.conversations.press_enter_to_fork": "按 Enter 将当前会话 fork 为新会话。", + "search.conversations.press_enter_to_navigate": "按 Enter 导航到会话", + "search.conversations.section.active_pane": "当前窗格会话", + "search.conversations.section.other_active": "其他活动会话", + "search.conversations.section.past": "历史会话", + "search.external_secrets.placeholder": "搜索密钥", + "search.external_secrets.secret_prefix": "密钥", + "search.files.create_file": "创建文件", + "search.files.create_file_named": "创建名为", + "search.files.directory": "目录", + "search.files.file": "文件", + "search.files.press_enter_to_create_prefix": "按 Enter 在当前目录创建", + "search.files.press_enter_to_create_suffix": "", + "search.files.press_enter_to_navigate_directory": "按 Enter 导航到此目录", + "search.files.press_enter_to_open_file": "按 Enter 打开此文件", + "search.filter.actions": "操作", + "search.filter.agent_mode_workflows": "prompts", + "search.filter.base_models": "基础模型", + "search.filter.blocks": "块", + "search.filter.code": "代码", + "search.filter.commands": "命令", + "search.filter.conversations": "会话", + "search.filter.current_directory_conversations": "当前目录会话", + "search.filter.diff_sets": "diff 集", + "search.filter.drive": "Warp Drive", + "search.filter.environment_variables": "环境变量", + "search.filter.files": "文件", + "search.filter.full_terminal_use_models": "完整终端模型", + "search.filter.history": "历史", + "search.filter.launch_configurations": "启动配置", + "search.filter.natural_language": "AI 命令建议", + "search.filter.notebooks": "笔记本", + "search.filter.plans": "计划", + "search.filter.prompt_history": "prompt 历史", + "search.filter.repos": "仓库", + "search.filter.rules": "规则", + "search.filter.sessions": "会话", + "search.filter.skills": "技能", + "search.filter.static_slash_commands": "斜杠命令", + "search.filter.tabs": "标签页", + "search.filter.workflows": "工作流", + "search.launch_config.press_enter_to_use_launch_configuration": "按 Enter 使用此启动配置。", + "search.navigation.completed": "已完成", + "search.navigation.completed_over_hour": "1 小时前完成", + "search.navigation.completed_prefix": "已完成", + "search.navigation.empty_session": "空会话", + "search.navigation.minute_ago": "分钟前", + "search.navigation.minutes_ago": "分钟前", + "search.navigation.no_timestamp": "未找到时间戳", + "search.navigation.running": "正在运行...", + "search.new_session.create_new_tab": "新建标签页", + "search.new_session.create_new_window": "新建窗口", + "search.new_session.split_pane_down": "向下拆分窗格", + "search.new_session.split_pane_left": "向左拆分窗格", + "search.new_session.split_pane_right": "向右拆分窗格", + "search.new_session.split_pane_up": "向上拆分窗格", + "search.no_results": "未找到结果", + "search.no_results_found": "未找到结果", + "search.no_results_found_period": "未找到结果。", + "search.not_visible_to_other_users": "其他用户不可见", + "search.notebook_embedding.placeholder": "搜索引用", + "search.placeholder.actions": "搜索操作", + "search.placeholder.agent_mode_workflows": "搜索 prompts", + "search.placeholder.base_models": "搜索基础模型", + "search.placeholder.blocks": "搜索块", + "search.placeholder.code": "搜索代码符号", + "search.placeholder.commands": "搜索命令", + "search.placeholder.conversations": "搜索会话", + "search.placeholder.current_directory_conversations": "搜索当前目录中的会话", + "search.placeholder.diff_sets": "搜索 diff 集", + "search.placeholder.drive": "搜索 Drive 中的对象", + "search.placeholder.environment_variables": "搜索环境变量", + "search.placeholder.files": "搜索文件", + "search.placeholder.full_terminal_use_models": "搜索完整终端模型", + "search.placeholder.history": "搜索历史", + "search.placeholder.launch_configurations": "搜索启动配置", + "search.placeholder.natural_language": "例如:替换文件中的字符串", + "search.placeholder.notebooks": "搜索笔记本", + "search.placeholder.plans": "搜索计划", + "search.placeholder.prompt_history": "搜索 prompt 历史", + "search.placeholder.repos": "搜索代码仓库", + "search.placeholder.rules": "搜索 AI 规则", + "search.placeholder.sessions": "搜索会话", + "search.placeholder.skills": "搜索技能", + "search.placeholder.static_slash_commands": "搜索静态斜杠命令", + "search.placeholder.tabs": "搜索标签页", + "search.placeholder.workflows": "搜索工作流", + "search.repos.repo": "仓库", + "search.search_results_menu.prompts": "Prompts", + "search.separator.section": "分区", + "search.slash_command.description.add_mcp": "通过 MCP 设置页面添加新的 MCP 服务器", + "search.slash_command.description.add_prompt": "添加新的 Agent prompt", + "search.slash_command.description.add_rule": "为 Agent 添加新的全局规则", + "search.slash_command.description.agent": "开始新对话", + "search.slash_command.description.changelog": "打开最新 changelog", + "search.slash_command.description.cloud_agent": "开始新的云端 Agent 对话", + "search.slash_command.description.compact": "总结对话历史以释放上下文空间", + "search.slash_command.description.compact_and": "压缩对话,然后发送 follow-up prompt", + "search.slash_command.description.continue_locally": "在本地继续此云端对话", + "search.slash_command.description.conversations": "打开对话历史", + "search.slash_command.description.cost": "切换额度用量详情", + "search.slash_command.description.create_docker_sandbox": "创建新的 Docker sandbox 终端会话", + "search.slash_command.description.create_environment": "通过引导式设置创建 Oz 环境(Docker 镜像 + repo)", + "search.slash_command.description.create_new_project": "让 Oz 引导你创建新的 coding project", + "search.slash_command.description.edit": "在 Warp 内置代码编辑器中打开文件", + "search.slash_command.description.edit_skill": "在 Warp 内置编辑器中打开 skill 的 markdown 文件", + "search.slash_command.description.environment": "切换云端 Agent 环境", + "search.slash_command.description.export_to_clipboard": "将当前对话以 markdown 格式导出到剪贴板", + "search.slash_command.description.export_to_file": "将当前对话导出为 markdown 文件", + "search.slash_command.description.feedback": "发送反馈", + "search.slash_command.description.fork": "在新窗格或新标签页中 fork 当前对话", + "search.slash_command.description.fork_and_compact": "Fork 当前对话,并在 fork 后的副本中压缩它", + "search.slash_command.description.fork_from": "从指定 query fork 对话", + "search.slash_command.description.harness": "切换云端 Agent harness", + "search.slash_command.description.host": "切换云端 Agent 执行主机", + "search.slash_command.description.index": "索引此代码库", + "search.slash_command.description.init": "索引此代码库并生成 AGENTS.md 文件", + "search.slash_command.description.invoke_skill": "调用 skill", + "search.slash_command.description.model": "切换基础 Agent 模型", + "search.slash_command.description.move_to_cloud": "将此对话移交给云端 Agent", + "search.slash_command.description.new": "开始新对话(/agent 的别名)", + "search.slash_command.description.open_code_review": "打开 code review", + "search.slash_command.description.open_mcp_servers": "打开 MCP 服务器", + "search.slash_command.description.open_project_rules": "打开项目规则文件(AGENTS.md)", + "search.slash_command.description.open_repo": "切换到另一个已索引仓库", + "search.slash_command.description.open_rules": "查看你的所有全局和项目规则", + "search.slash_command.description.open_settings_file": "打开设置文件(TOML)", + "search.slash_command.description.orchestrate": "将任务拆分为子任务,并用多个 Agent 并行运行", + "search.slash_command.description.plan": "让 Agent 先做一些研究,并为任务创建计划", + "search.slash_command.description.pr_comments": "拉取 GitHub PR review 评论", + "search.slash_command.description.profile": "切换当前执行 profile", + "search.slash_command.description.prompts": "搜索已保存 prompt", + "search.slash_command.description.queue": "排队一个 prompt,在 Agent 响应结束后发送", + "search.slash_command.description.remote_control": "为此会话启动远程控制", + "search.slash_command.description.rename_tab": "重命名当前标签页", + "search.slash_command.description.rewind": "回溯到对话中的上一个位置", + "search.slash_command.description.set_tab_color": "设置当前标签页颜色", + "search.slash_command.description.usage": "打开账单和用量设置", + "search.slash_command.prefix": "Slash 命令", + "search.tabs.press_enter_to_navigate": "按 Enter 导航到标签页", + "search.tabs.tab": "标签页", + "search.untitled": "未命名", + "search.warp_drive.environment_variables": "环境变量", + "search.warp_drive.notebook": "笔记本", + "search.warp_drive.workflow": "工作流", + "search.welcome_palette.add_repository": "添加仓库", + "search.welcome_palette.no_results": "未找到结果", + "search.welcome_palette.placeholder": "编写代码、构建项目或搜索任何内容...", + "search.welcome_palette.terminal_session": "终端会话", + "search.zero_state.recent": "最近使用", + "search.zero_state.suggested": "建议", + "server.iap.credential_refresh_failed": "IAP 凭证刷新失败:{message}", + "server.network_log.refresh": "刷新", + "server.network_log.title": "网络日志", + "session_management.a11y.currently_running_ai_interaction": "当前正在运行 AI 交互:{prompt}", + "session_management.a11y.currently_running_command": "当前正在运行 {command}", + "session_management.a11y.last_ai_interaction": "上次 AI 交互:{prompt}", + "session_management.a11y.last_run_command": "上次运行命令 {command}", + "settings.about.copyright": "版权所有 2026 Warp", + "settings.account.compare_plans": "比较套餐", + "settings.account.contact_support": "联系支持", + "settings.account.iap.failed": "失败:{message}", + "settings.account.iap.loaded_refreshes": "已加载(约 {mins} 分钟后刷新)", + "settings.account.iap.not_loaded": "尚未加载", + "settings.account.iap.refreshing": "正在刷新…", + "settings.account.iap.title": "Staging IAP 凭证", + "settings.account.iap.using_injected_token": "正在使用注入的 token(WARP_IAP_TOKEN)", + "settings.account.log_out": "退出登录", + "settings.account.manage_billing": "管理账单", + "settings.account.page_title": "账户", + "settings.account.plan.free": "免费版", + "settings.account.refer_a_friend": "推荐好友", + "settings.account.referral_cta": "与好友和同事分享 Warp,赢取奖励", + "settings.account.settings_sync": "设置同步", + "settings.account.sign_up": "注册", + "settings.account.upgrade_to_lightspeed": "升级到 Lightspeed 套餐", + "settings.account.upgrade_to_turbo": "升级到 Turbo 套餐", + "settings.account.version.cannot_install": "有新版本的 Warp 可用,但无法安装", + "settings.account.version.cannot_launch": "已安装新版本的 Warp,但无法启动。", + "settings.account.version.check_for_updates": "检查更新", + "settings.account.version.checking": "正在检查更新…", + "settings.account.version.downloading": "正在下载更新…", + "settings.account.version.installed_update": "已安装更新", + "settings.account.version.label": "版本", + "settings.account.version.relaunch_warp": "重新启动 Warp", + "settings.account.version.up_to_date": "已是最新版本", + "settings.account.version.update_available": "有可用更新", + "settings.account.version.update_manually": "手动更新 Warp", + "settings.account.version.updating": "正在更新…", + "settings.action.debug_network_status": "调试网络状态", + "settings.action.disable": "停用{description}", + "settings.action.enable": "启用{description}", + "settings.action.hide_in_band_command_blocks": "隐藏带内命令区块", + "settings.action.hide_initialization_block": "隐藏初始化区块", + "settings.action.in_band_generators_for_new_sessions": "新会话的带内生成器", + "settings.action.memory_statistics": "内存统计", + "settings.action.recording_mode": "录制模式", + "settings.action.show_in_band_command_blocks": "显示带内命令区块", + "settings.action.show_initialization_block": "显示初始化区块", + "settings.ai.action.agent_commands_in_history": "历史记录中的 Agent 执行命令", + "settings.ai.action.agent_prompt_autodetection_in_terminal_input": "终端输入中的 Agent 提示词自动检测", + "settings.ai.action.code_suggestions": "代码建议", + "settings.ai.action.coding_agent_toolbar": "编码 Agent 工具栏", + "settings.ai.action.conversation_history_tools_panel": "工具面板中的对话历史", + "settings.ai.action.hide_agent_tips": "隐藏 Agent 提示", + "settings.ai.action.hide_oz_changelog": "在新 Agent 对话视图中隐藏 Oz 更新日志", + "settings.ai.action.hide_use_agent_footer": "隐藏「使用 Agent」页脚", + "settings.ai.action.model_picker_in_prompt": "提示词中的模型选择器", + "settings.ai.action.rich_input_auto_dismiss_after_submit": "提交提示词后自动关闭 Rich Input", + "settings.ai.action.rich_input_auto_open_on_agent_start": "编码 Agent 会话启动时自动打开 Rich Input", + "settings.ai.action.rich_input_auto_toggle": "根据 Agent 状态自动显示或隐藏 Rich Input", + "settings.ai.action.show_agent_tips": "显示 Agent 提示", + "settings.ai.action.show_oz_changelog": "在新 Agent 对话视图中显示 Oz 更新日志", + "settings.ai.action.show_use_agent_footer": "显示「使用 Agent」页脚", + "settings.ai.action.terminal_command_autodetection_in_agent_input": "Agent 输入中的终端命令自动检测", + "settings.ai.active_ai.header": "主动 AI", + "settings.ai.add_custom_endpoint": "添加自定义端点", + "settings.ai.add_custom_model": "+ 添加自定义模型", + "settings.ai.add_profile": "添加配置文件", + "settings.ai.agent_attribution.desc": "Oz 可以为它创建的提交信息和拉取请求添加署名", + "settings.ai.agent_attribution.header": "Agent 署名", + "settings.ai.agent_attribution.label": "启用 Agent 署名", + "settings.ai.agents.desc": "设定 Agent 的运行边界。选择它可以访问哪些内容、拥有多大的自主权,以及何时必须征求您的批准。您还可以细化自然语言输入、代码库感知等方面的行为。", + "settings.ai.agents.header": "Agent", + "settings.ai.api_key.anthropic": "Anthropic API 密钥", + "settings.ai.api_key.google": "Google API 密钥", + "settings.ai.api_key.openai": "OpenAI API 密钥", + "settings.ai.api_keys.header": "API 密钥", + "settings.ai.auto_spawn_servers": "自动从第三方 Agent 启动服务器", + "settings.ai.autodetect_agent_prompts": "自动检测终端输入中的 Agent 提示词", + "settings.ai.autodetect_terminal_commands": "自动检测 Agent 输入中的终端命令", + "settings.ai.autonomy.read_only": "只读", + "settings.ai.autonomy.supervised": "受监督", + "settings.ai.aws_bedrock.auto_login.desc": "启用后,当 AWS Bedrock 凭证过期时,登录命令将自动运行。", + "settings.ai.aws_bedrock.auto_login.label": "自动运行登录命令", + "settings.ai.aws_bedrock.desc": "Warp 会为 Bedrock 支持的模型加载并发送本地 AWS CLI 凭证。", + "settings.ai.aws_bedrock.desc_admin_enforced": "Warp 会为 Bedrock 支持的模型加载并发送本地 AWS CLI 凭证。此设置由您的组织管理。", + "settings.ai.aws_bedrock.label": "使用 AWS Bedrock 凭证", + "settings.ai.aws_bedrock.login_command": "登录命令", + "settings.ai.aws_bedrock.profile": "AWS 配置文件", + "settings.ai.aws_bedrock.refresh": "刷新", + "settings.ai.base_model.desc": "该模型是 Warp Agent 背后的主引擎。它驱动大多数交互,并在必要时调用其他模型来完成规划或代码生成等任务。Warp 可能会根据模型可用性,或为对话摘要等辅助任务自动切换到备用模型。", + "settings.ai.base_model.label": "基础模型", + "settings.ai.byok.ask_admin": "请联系您团队的管理员升级到 Build 套餐,以使用您自己的 API 密钥。", + "settings.ai.byok.contact_sales": "联系销售", + "settings.ai.byok.contact_sales_suffix": ",以在您的 Enterprise 套餐上启用自带 API 密钥功能。", + "settings.ai.byok.create_account": "创建账户", + "settings.ai.byok.upgrade_build": "升级到 Build 套餐", + "settings.ai.byok.upgrade_suffix": ",以使用您自己的 API 密钥。", + "settings.ai.cli_agent.auto_dismiss_rich_input": "提交提示后自动关闭 Rich Input", + "settings.ai.cli_agent.auto_open_rich_input": "编码 Agent 会话启动时自动打开 Rich Input", + "settings.ai.cli_agent.auto_toggle_rich_input.label": "根据 Agent 状态自动显示/隐藏 Rich Input", + "settings.ai.cli_agent.command_placeholder": "命令(支持正则表达式)", + "settings.ai.cli_agent.commands_enable_toolbar": "启用工具栏的命令", + "settings.ai.cli_agent.commands_regex_desc": "添加正则表达式模式,为匹配的命令显示编码 Agent 工具栏。", + "settings.ai.cli_agent.header": "第三方 CLI Agent", + "settings.ai.cli_agent.other": "其他", + "settings.ai.cli_agent.requires_plugin_tooltip": "需要为您的编码 Agent 安装 Warp 插件", + "settings.ai.cli_agent.select_agent": "选择编码 Agent", + "settings.ai.cli_agent.show_toolbar": "显示编码 Agent 工具栏", + "settings.ai.cli_agent.toolbar_desc_prefix": "在运行 ", + "settings.ai.cli_agent.toolbar_desc_sep1": "、", + "settings.ai.cli_agent.toolbar_desc_sep2": " 或 ", + "settings.ai.cli_agent.toolbar_desc_suffix": " 等编码 Agent 时,显示包含快捷操作的工具栏。", + "settings.ai.cloud_handoff.ampersand.desc": "将 & 作为首个字符输入,即可进入云端交接撰写模式。", + "settings.ai.cloud_handoff.ampersand.label": "使用 & 触发交接", + "settings.ai.cloud_handoff.auto_before_sleep.desc": "当 macOS 即将进入睡眠时,自动将最近聚焦的正在运行的本地 Warp Agent 对话移至云端模式,使其能够继续工作。", + "settings.ai.cloud_handoff.auto_before_sleep.label": "睡眠前自动交接", + "settings.ai.cloud_handoff.desc": "将本地 Agent 对话交接给云端 Agent。", + "settings.ai.cloud_handoff.header": "云端交接", + "settings.ai.cloud_handoff.label": "云端交接", + "settings.ai.cloud_handoff.requires_cloud_convos_tooltip": "云端交接需要启用云端对话。", + "settings.ai.codebase_context.desc": "允许 Warp Agent 生成代码库的概要,用作上下文。我们的服务器绝不会存储任何代码。", + "settings.ai.codebase_context.label": "代码库上下文", + "settings.ai.command_allowlist.desc": "用于匹配命令的正则表达式,Warp Agent 可自动执行这些命令。", + "settings.ai.command_allowlist.label": "命令允许列表", + "settings.ai.command_allowlist.placeholder": "例如 ls .*", + "settings.ai.command_denylist.desc": "用于匹配命令的正则表达式,Warp Agent 在执行这些命令前应始终征求许可。", + "settings.ai.command_denylist.label": "命令拒绝列表", + "settings.ai.command_denylist.placeholder": "命令,用逗号分隔", + "settings.ai.command_denylist.regex_placeholder": "例如 rm .*", + "settings.ai.computer_use.desc": "在从 Warp 应用启动的云端 Agent 对话中启用计算机操作。", + "settings.ai.computer_use.label": "云端 Agent 中的计算机操作", + "settings.ai.context_window.label": "上下文窗口(token)", + "settings.ai.conversation_layout.new_tab": "新标签页", + "settings.ai.conversation_layout.split_pane": "分屏窗格", + "settings.ai.create_account_prompt": "要使用 AI 功能,请先创建账户。", + "settings.ai.custom_endpoint.url_placeholder": "请包含 'https://'", + "settings.ai.custom_endpoints.label": "自定义端点", + "settings.ai.custom_inference.desc": "在 Warp Agent 中使用您自己的模型提供方 API 密钥。您还可以添加自定义端点以使用第三方模型。自定义端点必须支持兼容 OpenAI 的 Chat Completions API。API 密钥仅存储在您的设备上,绝不会存储在 Warp 的服务器上,仅用于向您选择的模型提供方发起请求。使用自动模型或来自您未提供 API 密钥的提供方的模型将消耗 Warp 额度。", + "settings.ai.custom_inference.header": "自定义推理", + "settings.ai.custom_inference.terms_link": "Warp 服务条款", + "settings.ai.custom_inference.terms_prefix": "使用 BYOK 或自定义端点即表示您同意仅在 ", + "settings.ai.custom_inference.terms_suffix": " 允许的范围内使用它们。BYOK 和自定义端点面向个人用户和小型团队。员工超过 10 人的公司或组织应使用 Warp Business 或 Enterprise。", + "settings.ai.directory_allowlist.desc": "授予 Agent 对特定目录的文件访问权限。", + "settings.ai.directory_allowlist.label": "目录允许列表", + "settings.ai.directory_allowlist.placeholder": "例如 ~/code-repos/repo", + "settings.ai.edit": "编辑", + "settings.ai.edit_custom_endpoint": "编辑自定义端点", + "settings.ai.experimental.header": "实验性功能", + "settings.ai.file_based_mcp.desc": "自动检测并从全局范围的第三方 AI Agent 配置文件(例如位于主目录中的文件)启动 MCP 服务器。在代码仓库内检测到的服务器绝不会自动启动,必须在 MCP 设置页面中单独启用。", + "settings.ai.file_based_mcp.providers_link": "查看支持的提供方。", + "settings.ai.git_operations.desc": "让 AI 生成提交信息以及拉取请求的标题和描述。", + "settings.ai.git_operations.label": "提交与拉取请求生成", + "settings.ai.include_agent_commands": "在历史记录中包含 Agent 执行的命令", + "settings.ai.incorrect_detection_prompt": "遇到错误的检测结果?", + "settings.ai.incorrect_input_detection_prompt": " 遇到错误的输入检测?", + "settings.ai.input.header": "输入", + "settings.ai.knowledge.header": "知识", + "settings.ai.learn_more": "了解更多", + "settings.ai.let_us_know": "告诉我们", + "settings.ai.manage_mcp_servers": "管理 MCP 服务器", + "settings.ai.manage_rules": "管理规则", + "settings.ai.mcp_allowlist.desc": "允许 Warp Agent 调用这些 MCP 服务器。", + "settings.ai.mcp_allowlist.label": "MCP 允许列表", + "settings.ai.mcp_denylist.desc": "在调用此列表中的任何 MCP 服务器之前,Warp Agent 都会先征求许可。", + "settings.ai.mcp_denylist.label": "MCP 拒绝列表", + "settings.ai.mcp_servers.desc": "添加 MCP 服务器以扩展 Warp Agent 的能力。MCP 服务器通过标准化接口向 Agent 暴露数据源或工具,本质上就像插件一样。", + "settings.ai.mcp_servers.header": "MCP 服务器", + "settings.ai.mcp.add_server": "添加服务器", + "settings.ai.mcp.call_servers": "调用 MCP 服务器", + "settings.ai.mcp.learn_more_link": "了解有关 MCP 的更多信息。", + "settings.ai.mcp.or": ",或", + "settings.ai.mcp.zero_state_intro": "您尚未添加任何 MCP 服务器。添加后,您将能够控制 Warp Agent 与它们交互时的自主权。", + "settings.ai.models.header": "模型", + "settings.ai.natural_language_autosuggestions.desc": "让 AI 根据近期命令及其输出,提供自然语言自动建议。", + "settings.ai.natural_language_autosuggestions.label": "自然语言自动建议", + "settings.ai.next_command.desc": "让 AI 根据您的命令历史、输出和常见工作流,建议下一条要运行的命令。", + "settings.ai.next_command.label": "下一条命令", + "settings.ai.nld_denylist.desc": "此处列出的命令将永远不会触发自然语言检测。", + "settings.ai.nld_denylist.label": "自然语言拒绝列表", + "settings.ai.nld.desc": "启用自然语言检测后,系统会识别终端输入中的自然语言,并自动切换到 Agent 模式进行 AI 查询。", + "settings.ai.nld.label": "自然语言检测", + "settings.ai.org_enforced_tooltip": "此选项由您组织的设置强制指定,无法自定义。", + "settings.ai.other.header": "其他", + "settings.ai.permission.agent_decides": "由 Agent 决定", + "settings.ai.permission.allow_specific_directories": "在特定目录中允许", + "settings.ai.permission.always_allow": "始终允许", + "settings.ai.permission.always_ask": "始终询问", + "settings.ai.permission.ask_on_first_write": "首次写入时询问", + "settings.ai.permissions.apply_code_diffs": "应用代码差异", + "settings.ai.permissions.execute_commands": "执行命令", + "settings.ai.permissions.header": "权限", + "settings.ai.permissions.interact_running_commands": "与运行中的命令交互", + "settings.ai.permissions.read_files": "读取文件", + "settings.ai.permissions.workspace_managed": "您的部分权限由工作区管理。", + "settings.ai.preferred_conversation_layout": "打开现有 Agent 对话时的首选布局", + "settings.ai.profiles.desc": "配置文件让您可以定义 Agent 的运行方式——从它能执行哪些操作、何时需要批准,到它在编码和规划等任务中使用的模型。您还可以将其限定到具体项目。", + "settings.ai.profiles.header": "配置文件", + "settings.ai.prompt_suggestions.desc": "让 AI 根据近期命令及其输出,在输入区以内嵌横幅的形式建议自然语言提示。", + "settings.ai.prompt_suggestions.label": "提示建议", + "settings.ai.remote_session_disallowed": "当活动窗格包含来自远程会话的内容时,您的组织禁止使用 AI 功能", + "settings.ai.remove_endpoint": "移除端点", + "settings.ai.remove_endpoint_description": "确定要移除此端点吗?之后你将无法在 Agent 会话中使用它的模型。", + "settings.ai.remove_endpoint_question": "移除端点?", + "settings.ai.rules.desc": "规则可帮助 Warp Agent 遵循您的约定,无论是针对代码库还是特定工作流。", + "settings.ai.rules.label": "规则", + "settings.ai.select_mcp_servers": "选择 MCP 服务器", + "settings.ai.shared_block_title.desc": "让 AI 根据命令和输出,为您共享的命令块生成标题。", + "settings.ai.shared_block_title.label": "共享命令块标题生成", + "settings.ai.show_agent_tips": "显示 Agent 提示", + "settings.ai.show_conversation_history": "在工具面板中显示对话历史", + "settings.ai.show_input_hint": "显示输入提示文本", + "settings.ai.show_model_picker": "在输入框中显示模型选择器", + "settings.ai.show_oz_changelog": "在新对话视图中显示 Oz 更新日志", + "settings.ai.sign_up": "注册", + "settings.ai.suggested_code_banners.desc": "让 AI 根据近期命令及其输出,在命令块列表中以内嵌横幅的形式建议代码差异和查询。", + "settings.ai.suggested_code_banners.label": "代码建议横幅", + "settings.ai.suggested_rules.desc": "让 AI 根据您的交互建议可保存的规则。", + "settings.ai.suggested_rules.label": "建议规则", + "settings.ai.thinking_display.desc": "控制推理/思考过程的显示方式。", + "settings.ai.thinking_display.label": "Agent 思考过程显示", + "settings.ai.toast.endpoint_added": "已添加端点", + "settings.ai.toast.endpoint_removed": "已移除端点", + "settings.ai.toast.endpoint_saved": "已保存端点", + "settings.ai.toolbar_layout": "工具栏布局", + "settings.ai.usage.compare_plans": "比较套餐", + "settings.ai.usage.contact_support": "联系支持团队", + "settings.ai.usage.credits": "额度", + "settings.ai.usage.credits_limit_desc": "这是您账户的 AI 额度{period}上限。", + "settings.ai.usage.header": "用量", + "settings.ai.usage.more_usage_suffix": "以获得更多 AI 用量。", + "settings.ai.usage.resets": "{date} 重置", + "settings.ai.usage.restricted_billing": "因账单问题受限", + "settings.ai.usage.unlimited": "无限制", + "settings.ai.usage.upgrade": "升级", + "settings.ai.usage.upgrade_suffix": "以获得更多 AI 用量。", + "settings.ai.use_agent_footer.desc": "在长时间运行的命令中显示提示,引导使用已启用「完整终端使用」的 Agent。", + "settings.ai.use_agent_footer.label": "显示「使用 Agent」页脚", + "settings.ai.voice_input.desc_prefix": "语音输入让您可以通过直接对终端说话来控制 Warp(由 ", + "settings.ai.voice_input.desc_suffix": " 提供支持)。", + "settings.ai.voice_input.key_desc": "按住以激活。", + "settings.ai.voice_input.key_label": "激活语音输入的按键", + "settings.ai.voice_input.label": "语音输入", + "settings.ai.voice_input.tooltip": "语音输入", + "settings.ai.voice_input.tooltip_with_key": "语音输入(按住 {key} 键)", + "settings.ai.voice.header": "语音", + "settings.ai.warp_credit_fallback.desc": "启用后,发生错误时 Agent 请求可能会被路由到 Warp 提供的某个模型。Warp 会优先使用您的 API 密钥,而非您的 Warp 额度。", + "settings.ai.warp_credit_fallback.label": "Warp 额度回退", + "settings.ai.warp_drive_context.desc": "Warp Agent 可以利用您的 Warp Drive 内容,为您个人和团队的开发工作流及环境量身定制响应。这包括所有工作流、笔记本和环境变量。", + "settings.ai.warp_drive_context.label": "将 Warp Drive 用作 Agent 上下文", + "settings.appearance.action.agent_font_matching_terminal_font": "Agent 字体匹配终端字体", + "settings.appearance.action.always_show_tab_bar": "始终显示标签栏", + "settings.appearance.action.block_dividers": "区块分隔线", + "settings.appearance.action.cursor_blink": "光标闪烁", + "settings.appearance.action.custom_padding_alt_screen": "alt-screen 自定义内边距", + "settings.appearance.action.hide_code_review_button": "隐藏标签栏中的代码审查按钮", + "settings.appearance.action.hide_tab_bar_if_fullscreen": "全屏时隐藏标签栏", + "settings.appearance.action.jump_to_bottom_button": "跳转到区块底部按钮", + "settings.appearance.action.latest_prompt_as_tab_title": "将最新用户提示词用作标签页对话标题", + "settings.appearance.action.ligature_rendering": "连字渲染", + "settings.appearance.action.notebook_font_size_matching_terminal": "Notebook 字号匹配终端字号", + "settings.appearance.action.only_show_tab_bar_on_hover": "仅悬停时显示标签栏", + "settings.appearance.action.open_windows_custom_size": "使用自定义大小打开新窗口", + "settings.appearance.action.pin_input_to_bottom": "将输入固定到底部", + "settings.appearance.action.pin_input_to_top": "将输入固定到顶部", + "settings.appearance.action.preserve_active_tab_color": "为新标签页保留当前活动标签颜色", + "settings.appearance.action.show_code_review_button": "显示标签栏中的代码审查按钮", + "settings.appearance.action.start_input_at_top": "从顶部开始输入", + "settings.appearance.action.tab_indicators": "标签页指示器", + "settings.appearance.action.theme_sync_with_os": "主题随系统同步", + "settings.appearance.action.toggle_input_mode": "切换输入模式(Warp/Classic)", + "settings.appearance.action.tools_panel_visibility_across_tabs": "工具面板可见性跨标签页保持一致", + "settings.appearance.action.vertical_tab_layout": "垂直标签页布局", + "settings.appearance.action.vertical_tabs_panel_restored_windows": "恢复窗口时显示垂直标签页面板", + "settings.appearance.action.window_blur_acrylic_texture": "窗口模糊亚克力纹理", + "settings.appearance.action.zen_mode": "Zen 模式", + "settings.appearance.app_icon.bundle_warning": "更改应用图标需要应用以打包形式运行。", + "settings.appearance.app_icon.label": "自定义应用图标", + "settings.appearance.app_icon.option.aurora": "极光", + "settings.appearance.app_icon.option.classic_1": "经典 1", + "settings.appearance.app_icon.option.classic_2": "经典 2", + "settings.appearance.app_icon.option.classic_3": "经典 3", + "settings.appearance.app_icon.option.comets": "彗星", + "settings.appearance.app_icon.option.cow": "奶牛", + "settings.appearance.app_icon.option.default": "默认", + "settings.appearance.app_icon.option.glass_sky": "玻璃天空", + "settings.appearance.app_icon.option.glitch": "故障风", + "settings.appearance.app_icon.option.glow": "辉光", + "settings.appearance.app_icon.option.holographic": "全息", + "settings.appearance.app_icon.option.mono": "单色", + "settings.appearance.app_icon.option.neon": "霓虹", + "settings.appearance.app_icon.option.original": "原版", + "settings.appearance.app_icon.option.starburst": "星芒", + "settings.appearance.app_icon.option.sticker": "贴纸", + "settings.appearance.app_icon.option.warp_1": "Warp 1", + "settings.appearance.app_icon.restart_warning": "你可能需要重启 Warp,macOS 才能应用所选的图标样式。", + "settings.appearance.blocks.compact_mode": "紧凑模式", + "settings.appearance.blocks.jump_to_bottom": "显示“跳转到区块底部”按钮", + "settings.appearance.blocks.show_dividers": "显示区块分隔线", + "settings.appearance.category.blocks": "区块", + "settings.appearance.category.cursor": "光标", + "settings.appearance.category.full_screen_apps": "全屏应用", + "settings.appearance.category.icon": "图标", + "settings.appearance.category.input": "输入", + "settings.appearance.category.panes": "窗格", + "settings.appearance.category.tabs": "标签页", + "settings.appearance.category.text": "文本", + "settings.appearance.category.themes": "主题", + "settings.appearance.category.window": "窗口", + "settings.appearance.cursor.blinking": "光标闪烁", + "settings.appearance.cursor.type": "光标类型", + "settings.appearance.cursor.type_disabled_vim": "Vim 模式下无法设置光标类型", + "settings.appearance.full_screen_apps.custom_padding": "在备用屏幕中使用自定义边距", + "settings.appearance.full_screen_apps.uniform_padding_px": "统一边距(px)", + "settings.appearance.input.position": "输入框位置", + "settings.appearance.input.position.pin_bottom_warp": "固定到底部(Warp 模式)", + "settings.appearance.input.position.pin_top_reverse": "固定到顶部(反向模式)", + "settings.appearance.input.position.start_top_classic": "从顶部开始(经典模式)", + "settings.appearance.input.type": "输入类型", + "settings.appearance.input.type.shell_ps1": "Shell (PS1)", + "settings.appearance.input.type.warp": "Warp", + "settings.appearance.language.category": "语言", + "settings.appearance.language.label": "显示语言", + "settings.appearance.language.subtext": "Warp 界面所使用的语言。", + "settings.appearance.panes.dim_inactive": "调暗非活动窗格", + "settings.appearance.panes.focus_follows_mouse": "焦点跟随鼠标", + "settings.appearance.tabs.close_button_position": "标签页关闭按钮位置", + "settings.appearance.tabs.close_button_position.left": "左侧", + "settings.appearance.tabs.close_button_position.right": "右侧", + "settings.appearance.tabs.directory_colors": "目录标签页颜色", + "settings.appearance.tabs.directory_colors.default_no_color": "默认(无颜色)", + "settings.appearance.tabs.directory_colors.description": "根据你当前所在的目录或仓库自动为标签页着色。", + "settings.appearance.tabs.header_toolbar_layout": "标题栏工具栏布局", + "settings.appearance.tabs.latest_prompt_as_title": "在标签名中使用最新的用户提示作为会话标题", + "settings.appearance.tabs.latest_prompt_as_title.description": "在纵向标签页中,为 Oz 和第三方 Agent 会话显示最新的用户提示,而非自动生成的会话标题。", + "settings.appearance.tabs.preserve_active_color": "为新标签页保留当前标签页的颜色", + "settings.appearance.tabs.show_code_review_button": "显示代码评审按钮", + "settings.appearance.tabs.show_indicators": "显示标签页指示器", + "settings.appearance.tabs.show_tab_bar": "显示标签栏", + "settings.appearance.tabs.show_vertical_panel_restored": "在恢复的窗口中显示纵向标签页面板", + "settings.appearance.tabs.show_vertical_panel_restored.description": "启用后,重新打开或恢复窗口时会打开纵向标签页面板,即使上次保存窗口时该面板处于关闭状态。", + "settings.appearance.tabs.vertical_layout": "使用纵向标签页布局", + "settings.appearance.tabs.visibility.always": "始终", + "settings.appearance.tabs.visibility.only_on_hover": "仅悬停时", + "settings.appearance.tabs.visibility.when_windowed": "窗口模式时", + "settings.appearance.text.agent_font": "Agent 字体", + "settings.appearance.text.default_font_label": "{font}(默认)", + "settings.appearance.text.font_size_px": "字号(px)", + "settings.appearance.text.font_weight": "字重", + "settings.appearance.text.ligatures": "在终端中显示连字", + "settings.appearance.text.ligatures_tooltip": "连字可能会降低性能", + "settings.appearance.text.line_height": "行高", + "settings.appearance.text.match_terminal": "与终端一致", + "settings.appearance.text.min_contrast": "强制最低对比度", + "settings.appearance.text.min_contrast.always": "始终", + "settings.appearance.text.min_contrast.named_colors": "仅命名颜色", + "settings.appearance.text.min_contrast.never": "永不", + "settings.appearance.text.notebook_font_size": "Notebook 字号", + "settings.appearance.text.reset_to_default": "恢复默认", + "settings.appearance.text.terminal_font": "终端字体", + "settings.appearance.text.thin_strokes": "使用细笔画", + "settings.appearance.text.thin_strokes.always": "始终", + "settings.appearance.text.thin_strokes.high_dpi": "在高 DPI 显示器上", + "settings.appearance.text.thin_strokes.low_dpi": "在低 DPI 显示器上", + "settings.appearance.text.thin_strokes.never": "永不", + "settings.appearance.text.view_all_fonts": "查看所有可用的系统字体", + "settings.appearance.theme.create_custom": "创建自定义主题", + "settings.appearance.theme.mode.current": "当前主题", + "settings.appearance.theme.mode.dark": "深色", + "settings.appearance.theme.mode.light": "浅色", + "settings.appearance.theme.sync_with_os": "跟随系统", + "settings.appearance.theme.sync_with_os.description": "在系统切换浅色和深色主题时自动同步切换。", + "settings.appearance.window.blur_radius": "窗口模糊半径", + "settings.appearance.window.blur_texture": "启用窗口模糊(亚克力质感)", + "settings.appearance.window.blur_texture_unsupported": "所选硬件可能不支持渲染透明窗口。", + "settings.appearance.window.columns": "列数", + "settings.appearance.window.custom_size": "以自定义尺寸打开新窗口", + "settings.appearance.window.opacity": "窗口不透明度", + "settings.appearance.window.opacity_label": "窗口不透明度:", + "settings.appearance.window.opacity_unsupported": "你的显卡驱动不支持透明效果。", + "settings.appearance.window.rows": "行数", + "settings.appearance.window.tools_panel_consistent": "工具面板在各标签页间保持一致的显示状态", + "settings.appearance.window.transparency_unsupported": "所选的图形设置可能不支持渲染透明窗口。", + "settings.appearance.window.transparency_unsupported_hint": "请尝试在“功能 > 系统”中更改图形后端或集成显卡的设置。", + "settings.appearance.window.zoom": "缩放", + "settings.appearance.window.zoom.description": "调整所有窗口的默认缩放级别", + "settings.billing.add_on_credits": "附加额度", + "settings.billing.addon.auto_reload_enabled_header": "已启用自动充值", + "settings.billing.addon.auto_reload_label": "自动充值", + "settings.billing.addon.auto_reload_tooltip": "当团队中任意成员的积分余额降至剩余 100 个时,自动购买 {amount}。", + "settings.billing.addon.buy_credits_header": "购买积分", + "settings.billing.addon.contact_account_executive": "如需更多附加积分,请联系您的客户经理。", + "settings.billing.addon.contact_team_admin": "请联系团队管理员以启用附加积分。", + "settings.billing.addon.contact_team_admin_purchase": "请联系团队管理员购买附加积分。", + "settings.billing.addon.credits_count_one": "1 个积分", + "settings.billing.addon.credits_unit": "个积分", + "settings.billing.addon.description": "附加积分以预付费套餐形式购买,可跨账单周期结转,并在一年后过期。购买越多,单价越优惠。当您的基础套餐积分用完后,将开始消耗附加积分。", + "settings.billing.addon.description_team_shared_suffix": "购买的附加积分会在团队内共享。", + "settings.billing.addon.description_team_suffix": "购买的附加积分将计入您的个人余额。", + "settings.billing.addon.monthly_spend_limit_label": "每月消费上限", + "settings.billing.addon.monthly_spend_limit_tooltip": "设置每月用于购买附加积分的消费上限", + "settings.billing.addon.non_admin_auto_reload_description": "您的管理员已为附加积分启用自动充值。当您的个人附加积分余额不足时,Warp 将自动购买附加积分并计入您的余额。", + "settings.billing.addon.non_admin_auto_reload_description_with_amount": "您的管理员已为附加积分启用自动充值。当您的个人附加积分余额不足时,Warp 将自动以 {price} 购买 {credits} 个积分并计入您的余额。", + "settings.billing.addon.price_label": "{credits} 个积分 / {dollars}", + "settings.billing.addon.purchase_button": "一次性购买", + "settings.billing.addon.purchase_button_loading": "购买中…", + "settings.billing.addon.purchased_this_month": "本月已购买", + "settings.billing.addon.selected_credit_amount_fallback": "所选积分数量", + "settings.billing.addon.upgrade_to_build_link": "升级到 Build 套餐", + "settings.billing.addon.upgrade_to_build_suffix": " 即可购买附加积分。", + "settings.billing.addon.warning_auto_reload_paused_admin": "由于下一次充值将超出您的每月消费上限,自动充值已暂停。请调高上限以继续使用自动充值。", + "settings.billing.addon.warning_auto_reload_paused_non_admin": "由于下一次充值将超出团队的每月消费上限,自动充值已暂停。请联系团队管理员调高上限。", + "settings.billing.addon.warning_delinquent_admin": "因账单问题受限。请更新您的付款方式以购买附加积分。", + "settings.billing.addon.warning_delinquent_non_admin": "因账单问题受限。请联系团队管理员更新其付款方式。", + "settings.billing.addon.warning_failed_reload_admin": "由于最近一次充值失败,自动充值已被禁用。请更新您的付款方式后重试。", + "settings.billing.addon.warning_failed_reload_non_admin": "由于最近一次充值失败,自动充值已被禁用。请联系团队管理员更新其付款方式。", + "settings.billing.addon.warning_purchase_exceeds_admin": "此次购买将超出您的每月上限。请调高上限以继续。", + "settings.billing.addon.warning_purchase_exceeds_non_admin": "此次购买将超出团队的每月消费上限。请联系团队管理员调高上限。", + "settings.billing.aggregate_tooltip": "其他团队成员在附加项、按量付费和仅云端额度上的用量。", + "settings.billing.ambient_trial.buy_more_button": "购买更多", + "settings.billing.ambient_trial.credits_remaining_one": "剩余 1 个积分", + "settings.billing.ambient_trial.credits_remaining_suffix": "个积分剩余", + "settings.billing.ambient_trial.new_agent_button": "新建 Agent", + "settings.billing.ambient_trial.title": "云端 Agent 试用", + "settings.billing.automated_agent_on_team": "这是你团队中的自动 agent。", + "settings.billing.balance.base_credits": "基础积分", + "settings.billing.balance.expires_prefix": "到期时间", + "settings.billing.balance.header": "余额", + "settings.billing.balance.personal_credits": "个人积分", + "settings.billing.balance.remaining": "剩余", + "settings.billing.balance.team_credits": "团队积分", + "settings.billing.bring_your_own_key": "自带密钥", + "settings.billing.business_security_suffix": "以获得 SSO 和自动应用零数据保留等安全功能。", + "settings.billing.buy_more": "购买更多", + "settings.billing.contact_admin_resolve_issues": "请联系团队管理员解决账单问题。", + "settings.billing.contact_support": "联系支持", + "settings.billing.cta_admin_panel_copy": "以设置按用户的支出上限。", + "settings.billing.cta_admin_panel_link": "打开管理面板", + "settings.billing.cta_upgrade_build_copy": "以查看团队级别的额度用量。", + "settings.billing.cta_upgrade_build_link": "升级到 Build", + "settings.billing.cta_upgrade_business_copy": "以查看按用户细分的额度归属。", + "settings.billing.cta_upgrade_business_link": "升级到 Business", + "settings.billing.cta_upgrade_enterprise_copy": "以查看精细的额度归属并设置按用户的支出上限。", + "settings.billing.cta_upgrade_enterprise_link": "升级到 Enterprise", + "settings.billing.discount_badge": "{discount}% 折扣", + "settings.billing.enterprise_support_suffix": "以获得自定义额度和专属支持。", + "settings.billing.enterprise_usage_callout.admin_link": "访问管理后台", + "settings.billing.enterprise_usage_callout.admin_prefix": "企业积分用量暂未在此视图中完整显示。要获得最准确的支出跟踪,请", + "settings.billing.enterprise_usage_callout.header": "用量报告目前受限", + "settings.billing.enterprise_usage_callout.non_admin": "企业积分用量暂未在此视图中完整显示。请联系团队管理员获取详细用量报告。", + "settings.billing.flexible_pricing_suffix": "以使用更灵活的定价模式。", + "settings.billing.increase_your_limit": "提高你的限额", + "settings.billing.increased_ai_access_suffix": "以增加 AI 功能访问额度。", + "settings.billing.last_30_days": "过去 30 天", + "settings.billing.legend_addons": "附加项", + "settings.billing.legend_base": "基础额度", + "settings.billing.legend_cloud_only": "仅云端", + "settings.billing.legend_combined": "合计", + "settings.billing.legend_payg": "按量付费", + "settings.billing.manage_billing": "管理账单", + "settings.billing.members_header": "成员", + "settings.billing.monthly_overage_spending_limit": "每月超额消费限额", + "settings.billing.monthly_overage_spending_limit_tooltip": "设置超出计划金额后的每月超额消费限额", + "settings.billing.more_ai_credits_suffix": "以获得更多 AI 额度。", + "settings.billing.more_ai_usage_suffix": "以获得更多 AI 用量。", + "settings.billing.more_credits_and_models_suffix": "以获得更多额度并访问更多模型。", + "settings.billing.not_set": "未设置", + "settings.billing.overage_modal.additional_note": "请注意,临近所设限额时产生的 AI 额度消耗可能会超出限额几美元。", + "settings.billing.overage_modal.cancel_button": "取消", + "settings.billing.overage_modal.description": "达到此美元限额后,Warp 将禁止使用高级模型。该限额每月重置一次。", + "settings.billing.overage_modal.error_invalid_amount": "请输入有效的金额", + "settings.billing.overage_modal.error_out_of_range": "请输入介于 $0.01 与 $10,000,000 之间的金额", + "settings.billing.overage_modal.title": "超额消费限额", + "settings.billing.overage_modal.update_button": "更新", + "settings.billing.overage.toggle_admin_header": "启用高级模型用量超额计费", + "settings.billing.overage.toggle_description": "超出计划限制后继续使用高级模型。用量将按 20 美元增量计费,最高不超过你的支出限额,剩余余额会在预定账单日收取。", + "settings.billing.overage.toggle_user_description": "请让团队管理员启用超额计费以获得更多 AI 用量。", + "settings.billing.overage.toggle_user_header_disabled": "高级模型用量超额计费未启用", + "settings.billing.overage.toggle_user_header_enabled": "高级模型用量超额计费已启用", + "settings.billing.overage.usage_link": "查看超额用量详情", + "settings.billing.overview_tab": "概览", + "settings.billing.plan": "套餐", + "settings.billing.plan.compare_plans": "比较套餐", + "settings.billing.plan.free_badge": "免费版", + "settings.billing.plan.header": "套餐", + "settings.billing.plan.manage_billing": "管理账单", + "settings.billing.plan.open_admin_panel": "打开管理面板", + "settings.billing.prorated_limit_current_user": "你的额度上限会按比例计算,因为你是在账单周期中途加入的。", + "settings.billing.prorated_limit_user": "此用户是在账单周期中途加入的,因此该额度上限会按比例计算。", + "settings.billing.purchased_this_month": "本月已购买", + "settings.billing.regain_access_suffix": "以恢复 AI 功能访问。", + "settings.billing.reload_would_exceed_limit": "重新加载会超出你的每月限额。", + "settings.billing.resets_at": "将于 %b %d %-I:%M %p 重置", + "settings.billing.sort_by": "排序方式", + "settings.billing.sort.display_name_a_z": "A 到 Z", + "settings.billing.sort.display_name_z_a": "Z 到 A", + "settings.billing.sort.usage_ascending": "用量升序", + "settings.billing.sort.usage_descending": "用量降序", + "settings.billing.spending_limit_modal.title": "每月消费上限", + "settings.billing.switch_to_build_plan": "切换到 Build 套餐", + "settings.billing.switch_to_business": "切换到 Business", + "settings.billing.team_totals.cloud_agent_usage": "云端 Agent 用量", + "settings.billing.team_totals.credits_count": "({credits} 个积分)", + "settings.billing.team_totals.limit": "上限:{limit}", + "settings.billing.team_totals.local_agent_usage": "本地 Agent 用量", + "settings.billing.team_totals.overall_usage": "总体用量", + "settings.billing.team_totals.team": "团队", + "settings.billing.team_totals.team_total": "团队总计", + "settings.billing.to_continue": "以继续。", + "settings.billing.toast.auto_reload_disabled": "已禁用自动充值。", + "settings.billing.toast.auto_reload_enabled": "已启用自动充值。当您的余额不足时,我们将为您补充 {credits} 个积分。", + "settings.billing.toast.auto_reload_pricing_loading": "价格选项加载完成前无法启用自动充值。", + "settings.billing.toast.purchase_success": "附加积分购买成功", + "settings.billing.toast.update_settings_failed": "更新工作区设置失败", + "settings.billing.toast.your_selected_fallback": "您所选数量的", + "settings.billing.total_overages": "超额总量", + "settings.billing.upgrade_to_build_plan": "升级到 Build 套餐", + "settings.billing.upgrade_to_enterprise": "升级到 Enterprise", + "settings.billing.upgrade_to_max": "升级到 Max", + "settings.billing.usage_header": "用量", + "settings.billing.usage_history_empty": "启动一个 agent 任务后即可在此查看用量历史。", + "settings.billing.usage_history_tab": "用量历史", + "settings.billing.usage_history.empty_description": "启动一个 Agent 任务,即可在此查看使用记录。", + "settings.billing.usage_history.empty_title": "暂无使用记录", + "settings.billing.usage_history.last_30_days": "最近 30 天", + "settings.billing.usage_history.load_more": "加载更多", + "settings.billing.usage_resets_on": "用量将于 {date} 重置", + "settings.billing.usage.bucket.ai": "AI", + "settings.billing.usage.bucket.compute": "计算", + "settings.billing.usage.bucket.other": "其他", + "settings.billing.usage.bucket.platform": "平台", + "settings.billing.usage.bucket.suggested_code_diffs": "建议代码 diff", + "settings.billing.usage.bucket.total": "总计", + "settings.billing.usage.bucket.voice": "语音", + "settings.billing.usage.other_members": "其他成员", + "settings.billing.usage.source.all": "全部", + "settings.billing.usage.source.cloud": "云端", + "settings.billing.usage.source.local": "本地", + "settings.billing.usage.total": "总用量", + "settings.billing.usage.unknown_subject": "未知", + "settings.billing.usage.your_usage": "你的用量", + "settings.code.action.auto_indexing": "自动索引", + "settings.code.action.code_review_button": "代码审查按钮", + "settings.code.action.codebase_index": "代码库索引", + "settings.code.action.diff_stats_on_code_review_button": "代码审查按钮上的 diff 统计", + "settings.code.auto_index.description": "开启后,在你浏览代码仓库时,Warp 会自动为其建立索引,帮助 Agent 快速理解上下文并给出有针对性的解决方案。", + "settings.code.auto_index.label": "默认索引新文件夹", + "settings.code.auto_open_review_panel.description": "开启后,在一次对话中首次接受 diff 时会自动打开代码评审面板", + "settings.code.auto_open_review_panel.label": "自动打开代码评审面板", + "settings.code.category.editor_and_review.title": "代码编辑器与评审", + "settings.code.category.indexing.title": "代码库索引", + "settings.code.codebase_index.description": "在你浏览代码仓库时,Warp 可以自动为其建立索引,帮助 Agent 快速理解上下文并给出解决方案。代码绝不会存储在服务器上。即使某个代码库无法建立索引,Warp 仍可通过 grep 和 find 工具调用来浏览代码库并获取洞察。", + "settings.code.codebase_indexing.label": "代码库索引", + "settings.code.global_search.description": "在左侧工具面板中添加全局文件搜索。", + "settings.code.global_search.label": "全局文件搜索", + "settings.code.header": "代码", + "settings.code.index_limit_reached": "你已达到当前套餐允许的代码库索引数量上限。请删除现有索引,以便自动索引新的代码库。", + "settings.code.index_new_folder.button": "索引新文件夹", + "settings.code.indexing_ignore.description": "如需将特定文件或目录排除在索引之外,请将它们添加到仓库目录下的 .warpindexingignore 文件中。这些文件仍可被 AI 功能访问,但不会包含在代码库 embedding 中。", + "settings.code.initialization_settings.header": "初始化设置", + "settings.code.initialized_folders.header": "已初始化 / 已索引的文件夹", + "settings.code.lsp.available_for_download": "可供下载", + "settings.code.lsp.checking": "检查中…", + "settings.code.lsp.installed": "已安装", + "settings.code.lsp.installing": "安装中…", + "settings.code.lsp.restart_server.button": "重启服务器", + "settings.code.lsp.status.available": "可用", + "settings.code.lsp.status.busy": "繁忙", + "settings.code.lsp.status.failed": "失败", + "settings.code.lsp.status.not_running": "未运行", + "settings.code.lsp.status.stopped": "已停止", + "settings.code.lsp.view_logs.button": "查看日志", + "settings.code.no_folders_initialized": "尚未初始化任何文件夹。", + "settings.code.open_project_rules.button": "打开项目规则", + "settings.code.project_explorer.description": "在左侧工具面板中添加 IDE 风格的项目浏览器 / 文件树。", + "settings.code.project_explorer.label": "项目浏览器", + "settings.code.section.indexing": "索引", + "settings.code.section.lsp_servers": "LSP 服务器", + "settings.code.show_diff_stats.description": "在代码评审按钮上显示新增和删除的行数。", + "settings.code.show_diff_stats.label": "在代码评审按钮上显示 diff 统计", + "settings.code.show_review_button.description": "在窗口右上角显示一个按钮,用于切换代码评审面板。", + "settings.code.show_review_button.label": "显示代码评审按钮", + "settings.code.status.codebase_too_large": "代码库过大", + "settings.code.status.disabled": "已禁用", + "settings.code.status.discovered.prefix": "已发现 ", + "settings.code.status.discovered.suffix": " 个分块", + "settings.code.status.failed": "失败", + "settings.code.status.index_limit_reached": "已达索引数量上限", + "settings.code.status.indexing": "索引中…", + "settings.code.status.indexing.prefix": "索引中 - ", + "settings.code.status.no_index_built": "尚未构建索引", + "settings.code.status.no_index_created": "尚未创建索引", + "settings.code.status.queued": "排队中", + "settings.code.status.stale": "已过期", + "settings.code.status.synced": "已同步", + "settings.code.status.syncing": "同步中…", + "settings.code.status.syncing.prefix": "同步中 - ", + "settings.code.status.unavailable": "不可用", + "settings.code.subpage.editor_and_review.title": "编辑器与代码评审", + "settings.code.subpage.indexing.title": "代码库索引", + "settings.code.tooltip.admin_disabled": "团队管理员已禁用代码库索引。", + "settings.code.tooltip.admin_enabled": "团队管理员已启用代码库索引。", + "settings.code.tooltip.ai_required": "需先启用 AI 功能才能使用代码库索引。", + "settings.custom_inference.add_endpoint": "添加端点", + "settings.custom_inference.add_model": "+ 添加模型", + "settings.custom_inference.api_key": "API key", + "settings.custom_inference.api_key_placeholder": "例如 sk-...", + "settings.custom_inference.endpoint_details_help": "在下方提供端点详情。你可以从该端点添加任意数量的模型,也可以为输入框中的模型选择器提供别名。", + "settings.custom_inference.endpoint_name": "端点名称", + "settings.custom_inference.endpoint_name_placeholder": "例如 Zach 的外部模型", + "settings.custom_inference.endpoint_url": "端点 URL", + "settings.custom_inference.error.invalid_url": "无效的 URL", + "settings.custom_inference.error.url_host_required": "URL 必须包含 host", + "settings.custom_inference.error.url_https_required": "URL 必须使用 HTTPS", + "settings.custom_inference.error.url_restricted_host": "URL 不能使用本地或私有 host", + "settings.custom_inference.model_alias": "模型别名(可选)", + "settings.custom_inference.model_alias_placeholder": "例如 GLM-5", + "settings.custom_inference.model_name": "模型名称", + "settings.custom_inference.model_name_placeholder": "例如 GLM-5-FP8", + "settings.directory_color.add_button": "添加目录颜色", + "settings.directory_color.add_directory": "+ 添加目录…", + "settings.environments.all_indexed_repos_selected": "所有本地已索引仓库都已选择。", + "settings.environments.available_indexed_repos": "可用的已索引仓库", + "settings.environments.button.add_repo": "添加仓库", + "settings.environments.button.cancel": "取消", + "settings.environments.button.create": "创建", + "settings.environments.button.create_environment": "创建环境", + "settings.environments.button.delete_environment": "删除环境", + "settings.environments.button.save": "保存", + "settings.environments.button.save_environment": "保存环境", + "settings.environments.card.edit_tooltip": "编辑", + "settings.environments.card.env_id_prefix": "环境 ID:", + "settings.environments.card.image_prefix": "镜像:", + "settings.environments.card.last_edited_prefix": "上次编辑:", + "settings.environments.card.last_used_never": "上次使用:从未使用", + "settings.environments.card.last_used_prefix": "上次使用:", + "settings.environments.card.repos_prefix": "仓库:", + "settings.environments.card.setup_commands_prefix": "配置命令:", + "settings.environments.card.share_tooltip": "共享", + "settings.environments.card.view_my_runs": "查看我的运行记录", + "settings.environments.delete_dialog_description": "确定要移除 {name} 环境吗?", + "settings.environments.delete_dialog_title": "删除环境?", + "settings.environments.description.character_count_suffix": "个字符", + "settings.environments.description.label": "描述", + "settings.environments.description.placeholder": "例如:此环境用于所有专注前端的 Agent", + "settings.environments.docker_image.label": "Docker 镜像引用", + "settings.environments.docker_image.label_short": "Docker 镜像", + "settings.environments.docker_image.open_image_at": "在以下位置打开镜像:", + "settings.environments.docker_image.placeholder": "例如:python:3.11, node:20-alpine", + "settings.environments.docker_image.placeholder_short": "例如:node:20-alpine", + "settings.environments.docker_image.suggest_generating": "生成中……", + "settings.environments.docker_image.suggest_image": "推荐镜像", + "settings.environments.docker_image.suggest_tooltip": "Warp 将根据你所选的仓库推荐一个 Docker 镜像。", + "settings.environments.empty.agent.subtitle": "选择一个本地已配置好的项目,我们会基于它帮你创建一个环境", + "settings.environments.empty.agent.title": "使用 Agent", + "settings.environments.empty.button.authorize": "授权", + "settings.environments.empty.button.get_started": "开始使用", + "settings.environments.empty.button.launch_agent": "启动 Agent", + "settings.environments.empty.button.loading": "加载中……", + "settings.environments.empty.button.retry": "重试", + "settings.environments.empty.github.badge": "推荐", + "settings.environments.empty.github.subtitle": "选择你想使用的 GitHub 仓库,我们会为你推荐基础镜像和配置", + "settings.environments.empty.github.title": "快速配置", + "settings.environments.empty.header": "你还没有配置任何环境。", + "settings.environments.empty.subheader": "选择你希望如何配置环境:", + "settings.environments.error.not_logged_in": "尚未登录", + "settings.environments.loading_indexed_repos": "正在加载本地已索引仓库……", + "settings.environments.local_repo_selection_unavailable": "此构建版本不支持选择本地仓库。", + "settings.environments.name.label": "名称", + "settings.environments.name.placeholder": "环境名称", + "settings.environments.name.placeholder_example": "例如:dev-env", + "settings.environments.no_directory_selected": "未选择目录", + "settings.environments.no_indexed_repos_found": "尚未找到本地已索引仓库。请先索引一个仓库,然后重试。", + "settings.environments.no_repos_selected": "尚未选择仓库", + "settings.environments.page.description": "环境定义了你的后台 Agent 的运行位置。你可以通过 GitHub(推荐)、Warp 辅助配置或手动配置,在几分钟内创建一个环境。", + "settings.environments.page.title": "环境", + "settings.environments.repos.auth_with_github": "通过 GitHub 授权", + "settings.environments.repos.configure_access": "在 GitHub 上配置访问权限", + "settings.environments.repos.helper": "输入 owner/repo 并按回车键添加,或从下拉列表中选择。", + "settings.environments.repos.label": "代码仓库", + "settings.environments.repos.load_error": "无法加载 GitHub 仓库。你可以粘贴仓库 URL,或重试。", + "settings.environments.repos.load_failed_fallback": "加载 GitHub 仓库失败", + "settings.environments.repos.loading": "加载中……", + "settings.environments.repos.missing_a_repo": "缺少某个仓库?", + "settings.environments.repos.none_found": "未找到仓库", + "settings.environments.repos.placeholder_authed": "输入仓库(owner/repo 格式)", + "settings.environments.repos.placeholder_browse": "浏览 GitHub 仓库……", + "settings.environments.repos.placeholder_unauthed": "粘贴仓库 URL", + "settings.environments.repos.retry": "重试", + "settings.environments.search.no_matches": "没有与搜索条件匹配的环境。", + "settings.environments.search.placeholder": "搜索环境……", + "settings.environments.section.personal": "个人", + "settings.environments.section.shared_by_team_prefix": "由 Warp 与 {team} 共享", + "settings.environments.section.shared_by_your_team": "由 Warp 与你的团队共享", + "settings.environments.select_local_repos_description": "选择本地已索引仓库,为环境创建 Agent 提供上下文。", + "settings.environments.select_repos_description": "选择仓库,为环境创建 Agent 提供上下文。", + "settings.environments.select_repos_dialog_title": "为你的环境选择仓库", + "settings.environments.selected_folder_not_git_repository": "所选文件夹不是 Git 仓库:{path}", + "settings.environments.selected_repos": "已选择仓库", + "settings.environments.setup_commands.helper": "安装命令各自独立运行。每条命令都从工作区根目录(/workspace)执行。如果某条命令依赖前一条,请用 && 将它们组合起来。", + "settings.environments.setup_commands.helper_short": "按回车键或点击提交按钮以添加每条命令。", + "settings.environments.setup_commands.label": "安装命令", + "settings.environments.setup_commands.placeholder": "例如:cd my-repo && pip install -r requirements.txt", + "settings.environments.setup_commands.placeholder_short": "例如:node start", + "settings.environments.share_with_team.label": "与团队共享", + "settings.environments.share_with_team.warning": "个人环境无法与外部集成或团队 API 密钥配合使用。为获得最佳体验,请使用共享环境。", + "settings.environments.suggest_image.auth_required": "你需要授予对 GitHub 仓库的访问权限,才能推荐 Docker 镜像", + "settings.environments.suggest_image.authenticate": "授权", + "settings.environments.suggest_image.failed": "无法建议 Docker 镜像", + "settings.environments.suggest_image.failed_with_error": "无法建议 Docker 镜像:{error}", + "settings.environments.suggest_image.launch_agent": "启动 Agent", + "settings.environments.suggest_image.no_match": "未能找到合适的匹配项。建议为这些仓库使用自定义 Docker 镜像。", + "settings.environments.suggest_image.unknown_response": "suggestCloudEnvironmentImage 返回了未知响应", + "settings.environments.title.create": "创建环境", + "settings.environments.title.edit": "编辑环境", + "settings.environments.toast.create_not_logged_in": "无法创建环境:尚未登录。", + "settings.environments.toast.created": "环境创建成功", + "settings.environments.toast.deleted": "环境删除成功", + "settings.environments.toast.save_not_found": "无法保存:环境已不存在。", + "settings.environments.toast.share_failed": "向团队共享环境失败", + "settings.environments.toast.share_no_team": "无法共享环境:你当前不在任何团队中。", + "settings.environments.toast.share_not_synced": "无法共享环境:环境尚未同步。", + "settings.environments.toast.shared": "环境共享成功", + "settings.environments.toast.updated": "环境更新成功", + "settings.execution_profile.auto": "自动", + "settings.execution_profile.autosync_plans_to_warp_drive": "自动同步计划到 Warp Drive", + "settings.execution_profile.full_terminal_use": "完整终端使用", + "settings.execution_profile.label_separator": ":", + "settings.execution_profile.models": "模型", + "settings.execution_profile.off": "关闭", + "settings.execution_profile.on": "开启", + "settings.execution_profile.permission.unknown": "未知", + "settings.execution_profile.permissions": "权限", + "settings.execution_profile.run_agents": "运行 Agent", + "settings.external_editor.default_app": "默认应用", + "settings.external_editor.markdown_viewer_default": "默认使用 Warp 的 Markdown Viewer 打开 Markdown 文件", + "settings.external_editor.new_tab": "新标签页", + "settings.external_editor.open_code_panel_files": "选择用于打开代码评审面板、项目浏览器和全局搜索中文件的编辑器", + "settings.external_editor.open_file_links": "选择用于打开文件链接的编辑器", + "settings.external_editor.open_files_layout": "选择在 Warp 中打开文件的布局", + "settings.external_editor.split_pane": "拆分窗格", + "settings.external_editor.tabbed_viewer.description": "开启后,在同一标签页中打开的所有文件都会自动分组到单个编辑器窗格中。", + "settings.external_editor.tabbed_viewer.header": "将文件分组到单个编辑器窗格", + "settings.features.action.agent_task_completion_notifications": "Agent 任务完成通知", + "settings.features.action.alias_expansion": "别名展开", + "settings.features.action.at_context_menu_terminal": "终端模式中的「@」上下文菜单", + "settings.features.action.audible_terminal_bell": "终端提示音", + "settings.features.action.autocomplete_symbols": "引号、圆括号和方括号自动补全", + "settings.features.action.autosuggestion_ignore_button": "自动建议忽略按钮", + "settings.features.action.autosuggestion_keybinding_hint": "自动建议快捷键提示", + "settings.features.action.autosuggestions": "自动建议", + "settings.features.action.code_default_editor": "Code 默认编辑器", + "settings.features.action.codebase_symbols_at_context": "「@」上下文菜单中的代码库符号", + "settings.features.action.command_corrections": "命令纠错", + "settings.features.action.completions_while_typing": "输入时显示补全", + "settings.features.action.configure_global_hotkey": "配置全局热键", + "settings.features.action.copy_on_select_terminal": "终端中选中即复制", + "settings.features.action.error_underlining": "错误下划线", + "settings.features.action.focus_reporting": "焦点上报", + "settings.features.action.global_workflows_command_search": "Command Search 中的全局 Workflow", + "settings.features.action.help_block_new_sessions": "新会话中的帮助区块", + "settings.features.action.in_app_agent_notifications": "应用内 Agent 通知", + "settings.features.action.input_hint_text": "输入提示文本", + "settings.features.action.integrated_gpu_rendering_low_power": "集成 GPU 渲染(低功耗)", + "settings.features.action.link_click_tooltip": "链接点击工具提示", + "settings.features.action.linux_selection_clipboard": "Linux 选择剪贴板", + "settings.features.action.long_running_command_notifications": "长时间运行命令通知", + "settings.features.action.middle_click_paste": "中键点击粘贴", + "settings.features.action.mouse_reporting": "鼠标上报", + "settings.features.action.needs_attention_notifications": "需要关注通知", + "settings.features.action.notification_sounds": "通知声音", + "settings.features.action.quit_warning_modal": "退出警告弹窗", + "settings.features.action.restore_session": "启动时恢复窗口、标签页和窗格", + "settings.features.action.scroll_reporting": "滚动上报", + "settings.features.action.slash_commands_terminal": "终端模式中的 slash command", + "settings.features.action.smart_select": "智能选择", + "settings.features.action.syntax_highlighting": "语法高亮", + "settings.features.action.terminal_input_message_line": "终端输入消息行", + "settings.features.action.vim_keybindings": "使用 Vim 键位编辑命令", + "settings.features.action.vim_status_bar": "Vim 状态栏", + "settings.features.action.vim_unnamed_register_clipboard": "将 Vim 未命名寄存器用作系统剪贴板", + "settings.features.action.warp_ssh_wrapper": "Warp SSH 封装器", + "settings.features.action.wayland_window_management": "用于窗口管理的 Wayland", + "settings.features.alias_expansion.label": "输入时展开别名", + "settings.features.async_find.desc": "使用改进的查找实现,在大量输出中搜索匹配项时保持界面响应流畅。", + "settings.features.async_find.label": "异步查找", + "settings.features.at_context_menu.label": "在终端模式下启用「@」上下文菜单", + "settings.features.audible_bell.label": "使用提示音", + "settings.features.auto_open_code_review.desc": "开启此设置后,代码审查面板会在对话中第一次接受 diff 时自动打开", + "settings.features.auto_open_code_review.label": "自动打开代码审查面板", + "settings.features.autocomplete_symbols.label": "自动补全引号、圆括号和方括号", + "settings.features.autosuggestion_hint.label": "显示自动建议快捷键提示", + "settings.features.autosuggestion_ignore_button.label": "显示自动建议忽略按钮", + "settings.features.block_limit.desc": "将上限设置为超过 10 万行可能会影响性能。支持的最大行数为 {max_rows}。", + "settings.features.block_limit.label": "单个区块的最大行数", + "settings.features.block_limit.max_rows_10m": "1000 万", + "settings.features.block_limit.max_rows_1m": "100 万", + "settings.features.button.cancel": "取消", + "settings.features.button.save": "保存", + "settings.features.category.general": "通用", + "settings.features.category.keys": "按键", + "settings.features.category.notifications": "通知", + "settings.features.category.session": "会话", + "settings.features.category.system": "系统", + "settings.features.category.terminal": "终端", + "settings.features.category.terminal_input": "终端输入", + "settings.features.category.text_editing": "文本编辑", + "settings.features.category.workflows": "工作流", + "settings.features.changes_apply_new_windows": "更改将应用于新窗口。", + "settings.features.code_line_numbers.label": "代码编辑器行号:", + "settings.features.command_corrections.label": "建议纠正后的命令", + "settings.features.completions_while_typing.label": "输入时自动打开补全菜单", + "settings.features.confirm_close_shared.label": "关闭共享会话前确认", + "settings.features.copy_on_select.label": "选中即复制", + "settings.features.ctrl_tab_behavior.label": "Ctrl+Tab 行为:", + "settings.features.default_session_mode.label": "新会话的默认模式", + "settings.features.default_terminal.is_default": "Warp 已是默认终端", + "settings.features.default_terminal.make_default": "将 Warp 设为默认终端", + "settings.features.error_underlining.label": "为命令显示错误下划线", + "settings.features.extra_meta_keys.left_alt": "左 Alt 键作为 Meta 键", + "settings.features.extra_meta_keys.left_option": "左 Option 键作为 Meta 键", + "settings.features.extra_meta_keys.right_alt": "右 Alt 键作为 Meta 键", + "settings.features.extra_meta_keys.right_option": "右 Option 键作为 Meta 键", + "settings.features.focus_reporting.label": "启用焦点上报", + "settings.features.global_hotkey.label": "全局快捷键:", + "settings.features.global_hotkey.not_supported_wayland": "在 Wayland 上不受支持。", + "settings.features.global_workflows.label": "在命令搜索中显示全局工作流(ctrl-r)", + "settings.features.gpu.prefer_low_power.label": "优先使用集成显卡渲染新窗口(低功耗)", + "settings.features.graphics_backend.current": "当前后端:{backend}", + "settings.features.graphics_backend.label": "首选图形后端", + "settings.features.help_block.label": "在新会话中显示帮助区块", + "settings.features.keybinding.change": "更改快捷键", + "settings.features.keybinding.click_to_set": "点击以设置全局快捷键", + "settings.features.keybinding.label": "快捷键", + "settings.features.keybinding.press_new_shortcut": "按下新的快捷键", + "settings.features.link_tooltip.label": "点击链接时显示提示", + "settings.features.linux_clipboard.label": "启用 Linux 选区剪贴板", + "settings.features.linux_clipboard.tooltip": "是否支持 Linux 主剪贴板。", + "settings.features.login_item.label": "登录时启动 Warp", + "settings.features.login_item.label_macos": "登录时启动 Warp(需要 macOS 13 及以上版本)", + "settings.features.middle_click_paste.label": "鼠标中键粘贴", + "settings.features.mouse_reporting.label": "启用鼠标上报", + "settings.features.mouse_scroll.allowed_values": "允许的取值:1-20", + "settings.features.mouse_scroll.label": "鼠标滚轮每次滚动的行数", + "settings.features.mouse_scroll.tooltip": "支持 1 到 20 之间的浮点数值。", + "settings.features.new_tab.after_all_tabs": "在所有标签页之后", + "settings.features.new_tab.after_current_tab": "在当前标签页之后", + "settings.features.new_tab.placement_label": "新建标签页位置", + "settings.features.notifications.agent_task_completed": "Agent 完成任务时通知", + "settings.features.notifications.in_app_agent": "显示应用内 Agent 通知", + "settings.features.notifications.long_running_prefix": "当命令运行超过", + "settings.features.notifications.long_running_suffix": "秒时", + "settings.features.notifications.needs_attention": "命令或 Agent 需要你介入才能继续时通知", + "settings.features.notifications.play_sounds": "播放通知声音", + "settings.features.notifications.receive_desktop": "接收来自 Warp 的桌面通知", + "settings.features.notifications.toast_duration_prefix": "提示通知保持显示", + "settings.features.notifications.toast_duration_suffix": "秒", + "settings.features.open_links_desktop.label": "在桌面应用中打开链接", + "settings.features.open_links_desktop.tooltip": "尽可能自动在桌面应用中打开链接。", + "settings.features.outline_codebase_symbols.label": "为「@」上下文菜单提取代码库符号", + "settings.features.quake.active_screen": "当前屏幕", + "settings.features.quake.autohide_on_blur": "失去键盘焦点时自动隐藏", + "settings.features.quake.height_percent": "高度 %", + "settings.features.quake.pin_bottom": "固定到底部", + "settings.features.quake.pin_left": "固定到左侧", + "settings.features.quake.pin_right": "固定到右侧", + "settings.features.quake.pin_top": "固定到顶部", + "settings.features.quake.width_percent": "宽度 %", + "settings.features.quit_all_windows.label": "关闭所有窗口时退出", + "settings.features.quit_warning.label": "退出或登出前显示警告", + "settings.features.restore_session.label": "启动时恢复窗口、标签页和窗格", + "settings.features.restore_session.wayland_warning": "在 Wayland 上不会恢复窗口位置。", + "settings.features.scroll_reporting.label": "启用滚动上报", + "settings.features.see_docs": "查看文档。", + "settings.features.show_changelog.label": "更新后显示更新日志提示", + "settings.features.slash_commands.label": "在终端模式下启用斜杠命令", + "settings.features.smart_select.label": "双击智能选择", + "settings.features.smart_select.word_chars_label": "视为单词组成部分的字符", + "settings.features.ssh_wrapper.label": "Warp SSH 包装器", + "settings.features.ssh_wrapper.takes_effect_new_sessions": "此更改将在新会话中生效", + "settings.features.startup_shell.executable_path_placeholder": "可执行文件路径", + "settings.features.startup_shell.header": "新会话的默认 Shell", + "settings.features.sticky_command_header.label": "显示粘性命令标题栏", + "settings.features.syntax_highlighting.label": "为命令启用语法高亮", + "settings.features.tab_behavior.arrow_accepts_autosuggestions": "→ 接受自动建议。", + "settings.features.tab_behavior.completion_menu_unbound": "打开补全菜单尚未绑定快捷键。", + "settings.features.tab_behavior.completions_open_typing": "输入时自动打开补全。", + "settings.features.tab_behavior.completions_open_typing_or_key": "输入时自动打开补全(或按 {key})。", + "settings.features.tab_behavior.header": "Tab 键行为", + "settings.features.tab_behavior.key_accepts_autosuggestions": "{key} 接受自动建议。", + "settings.features.tab_behavior.key_opens_completion_menu": "{key} 打开补全菜单。", + "settings.features.terminal_input_message_line.label": "显示终端输入提示行", + "settings.features.vim_mode.label": "使用 Vim 快捷键编辑代码和命令", + "settings.features.vim_status_bar.label": "显示 Vim 状态栏", + "settings.features.vim_unnamed_clipboard.label": "将无名寄存器设为系统剪贴板", + "settings.features.wayland.label": "使用 Wayland 进行窗口管理", + "settings.features.wayland.restart_required": "重启 Warp 以使更改生效。", + "settings.features.wayland.secondary_text": "启用此设置会禁用全局快捷键支持。禁用时,如果你的 Wayland 合成器使用了非整数缩放(例如 125%),文本可能会显示模糊。", + "settings.features.wayland.tooltip": "启用 Wayland", + "settings.features.working_directory.directory_path_placeholder": "目录路径", + "settings.features.working_directory.header": "新会话的工作目录", + "settings.file_error.heading": "你的设置文件包含错误。", + "settings.file_error.heading_plural": "你的设置文件包含多个错误。", + "settings.file_error.invalid_multiple_description": "以下设置的值无效:{keys}。将使用默认值。", + "settings.file_error.invalid_single_description": "“{key}” 的值无效。将使用默认值。", + "settings.file_error.parse_description": "由于语法无效,无法解析。请打开文件进行修复。", + "settings.import.import_button": "导入", + "settings.import.new_session_notice": "部分设置会在打开新会话后生效。", + "settings.import.one_other_setting": "1 个其他设置", + "settings.import.other_settings": "{count} 个其他设置", + "settings.import.reset_to_defaults": "重置为 Warp 默认值", + "settings.import.theme": "主题", + "settings.import.theme_comma": "主题,", + "settings.keybindings.cancel_button": "取消", + "settings.keybindings.clear_button": "清除", + "settings.keybindings.column_command": "命令", + "settings.keybindings.description_intro": "在下方为现有操作添加你自己的自定义快捷键。", + "settings.keybindings.not_synced_tooltip": "键盘快捷键不会同步到云端", + "settings.keybindings.press_new_shortcut": "请按下新的键盘快捷键", + "settings.keybindings.reset_button": "默认", + "settings.keybindings.save_button": "保存", + "settings.keybindings.search_placeholder": "按名称或按键搜索(例如「cmd d」)", + "settings.keybindings.shortcut_conflict_warning": "此快捷键与其他快捷键冲突", + "settings.keybindings.shortcut_hint_prefix": "使用", + "settings.keybindings.shortcut_hint_suffix": "即可随时在侧边栏中查看这些快捷键。", + "settings.keybindings.subheader": "配置键盘快捷键", + "settings.mcp.action.edit": "编辑", + "settings.mcp.action.edit_config": "编辑配置", + "settings.mcp.action.log_out": "退出登录", + "settings.mcp.action.server_update_available": "服务器有可用更新", + "settings.mcp.action.set_up": "设置", + "settings.mcp.action.share_server": "分享服务器", + "settings.mcp.action.show_logs": "查看日志", + "settings.mcp.action.view_logs": "查看日志", + "settings.mcp.add_button": "添加", + "settings.mcp.auto_spawn.description": "自动检测并启动来自全局范围第三方 AI Agent 配置文件(例如位于主目录中)的 MCP 服务器。在仓库内检测到的服务器永远不会自动启动,必须在下方的「检测来源」部分中逐个启用。", + "settings.mcp.auto_spawn.label": "自动启动来自第三方 Agent 的服务器", + "settings.mcp.auto_spawn.see_providers_link": "查看支持的提供方。", + "settings.mcp.button.delete_mcp": "删除 MCP", + "settings.mcp.button.edit_variables": "编辑变量", + "settings.mcp.button.remove_from_team": "从团队中移除", + "settings.mcp.button.save": "保存", + "settings.mcp.chip.from_another_device": "来自其他设备", + "settings.mcp.chip.shared_by": "共享者:{creator}", + "settings.mcp.chip.shared_by_team_member": "由某位团队成员共享", + "settings.mcp.chip.shared_from_team": "来自团队共享", + "settings.mcp.confirm.delete_local.description": "这会从你的所有设备中卸载并移除此 MCP 服务器。", + "settings.mcp.confirm.delete_local.title": "删除 MCP 服务器?", + "settings.mcp.confirm.delete_shared.description": "这不仅会为你删除此 MCP 服务器,还会从 Warp 以及所有队友的设备中卸载并移除此 MCP 服务器。", + "settings.mcp.confirm.delete_shared.title": "删除共享的 MCP 服务器?", + "settings.mcp.confirm.unshare.description": "这会从 Warp 以及所有队友的设备中卸载并移除此 MCP 服务器。", + "settings.mcp.confirm.unshare.title": "从团队中移除共享的 MCP 服务器?", + "settings.mcp.edit_disabled_banner": "只有团队管理员和该 MCP 服务器的创建者才能编辑此 MCP 服务器。", + "settings.mcp.empty_state": "添加 MCP 服务器后,它将显示在这里。", + "settings.mcp.error.cannot_add_multiple": "编辑单个服务器时无法同时添加多个 MCP 服务器。", + "settings.mcp.error.cannot_install_from_link": "无法通过此链接安装 MCP 服务器“{name}”。", + "settings.mcp.error.contains_secrets": "此 MCP 服务器包含密钥。请前往「设置 > 隐私」修改密钥脱敏设置。", + "settings.mcp.error.finish_install_first": "请先完成当前 MCP 安装,再打开其他安装链接。", + "settings.mcp.error.no_server_specified": "未指定 MCP 服务器。", + "settings.mcp.error.unknown_server": "未知 MCP 服务器“{name}”", + "settings.mcp.markdown_parse_failed": "解析 Markdown 失败:{error}", + "settings.mcp.modal.install_title": "安装 {name}", + "settings.mcp.modal.multiple_updates_description": "此服务器有 {count} 个可用更新,你想继续哪一个?", + "settings.mcp.modal.update_title": "更新 {name}", + "settings.mcp.no_search_results": "未找到搜索结果", + "settings.mcp.no_server_selected": "未选择 MCP 服务器", + "settings.mcp.no_updates_available": "没有可用更新", + "settings.mcp.page.description": "添加 MCP 服务器以扩展 Warp Agent 的能力。MCP 服务器通过标准化接口向 Agent 暴露数据源或工具,本质上类似于插件。你可以添加自定义服务器,也可以使用预设快速接入常用服务器。你还可以在此处找到团队成员与你共享的服务器。", + "settings.mcp.page.learn_more_link": "了解更多。", + "settings.mcp.page.title": "MCP 服务器", + "settings.mcp.search.placeholder": "搜索 MCP 服务器", + "settings.mcp.section.detected_from": "检测来源:{provider}", + "settings.mcp.section.my_mcps": "我的 MCP", + "settings.mcp.section.shared_by_warp_and_devices": "由 Warp 及其他设备共享", + "settings.mcp.section.shared_by_warp_and_team": "由 Warp 和 {name} 共享", + "settings.mcp.section.shared_from_warp": "来自 Warp 的共享", + "settings.mcp.server_fallback": "服务器", + "settings.mcp.status.authenticating": "正在进行身份验证……", + "settings.mcp.status.available_to_install": "可安装", + "settings.mcp.status.detected_from_config": "从配置文件中检测到", + "settings.mcp.status.offline": "离线", + "settings.mcp.status.shutting_down": "正在关闭……", + "settings.mcp.status.starting_server": "正在启动服务器……", + "settings.mcp.title.add_new": "添加新的 MCP 服务器", + "settings.mcp.title.edit": "编辑 MCP 服务器", + "settings.mcp.title.edit_named": "编辑 {name} MCP 服务器", + "settings.mcp.toast.logged_out": "已成功退出 MCP 服务器登录", + "settings.mcp.toast.logged_out_named": "已成功退出 {name} MCP 服务器登录", + "settings.mcp.toast.server_updated": "MCP 服务器已更新", + "settings.mcp.tools.count_available": "{count} 个可用工具", + "settings.mcp.tools.none_available": "暂无可用工具", + "settings.mcp.tooltip.log_out": "退出登录", + "settings.mcp.update_option.another_device": "另一台设备", + "settings.mcp.update_option.from": "来自 {source} 的更新", + "settings.mcp.update_option.team_member": "一位团队成员", + "settings.mcp.update_option.version": "版本 {version}", + "settings.nav.about": "关于", + "settings.nav.account": "账户", + "settings.nav.agent_mcp_servers": "MCP 服务器", + "settings.nav.agent_profiles": "配置文件", + "settings.nav.ai": "AI", + "settings.nav.appearance": "外观", + "settings.nav.billing_and_usage": "账单与用量", + "settings.nav.cloud_environments": "环境", + "settings.nav.code": "代码", + "settings.nav.code_indexing": "索引与项目", + "settings.nav.editor_and_code_review": "编辑器与代码审查", + "settings.nav.features": "功能", + "settings.nav.keybindings": "键盘快捷键", + "settings.nav.knowledge": "知识库", + "settings.nav.mcp_servers": "MCP 服务器", + "settings.nav.oz_cloud_api_keys": "Oz Cloud API 密钥", + "settings.nav.privacy": "隐私", + "settings.nav.referrals": "推荐", + "settings.nav.shared_blocks": "共享块", + "settings.nav.teams": "团队", + "settings.nav.third_party_cli_agents": "第三方 CLI 代理", + "settings.nav.umbrella.agents": "代理", + "settings.nav.umbrella.cloud_platform": "云平台", + "settings.nav.umbrella.code": "代码", + "settings.nav.warp_agent": "Warp Agent", + "settings.nav.warp_drive": "Warp Drive", + "settings.nav.warpify": "Warpify", + "settings.open_settings_file": "打开设置文件", + "settings.platform.api_key_deleted_toast": "API 密钥已删除", + "settings.platform.api_key.agent.description": "此 API key 绑定到 Agent,可代表该 Agent 发起请求。", + "settings.platform.api_key.create_agent": "创建 Agent", + "settings.platform.api_key.create_failed": "创建 API key 失败。请重试。", + "settings.platform.api_key.create_key": "创建 key", + "settings.platform.api_key.creating": "正在创建…", + "settings.platform.api_key.delete_failed": "删除 API key 失败。请重试。", + "settings.platform.api_key.expiration.never": "永不过期", + "settings.platform.api_key.expiration.ninety_days": "90 天", + "settings.platform.api_key.expiration.one_day": "1 天", + "settings.platform.api_key.expiration.thirty_days": "30 天", + "settings.platform.api_key.load_agents_failed": "加载 Agent 失败。请关闭后重试。", + "settings.platform.api_key.name_placeholder": "Warp API Key", + "settings.platform.api_key.no_agents_available": "没有可用 Agent。请先创建一个。", + "settings.platform.api_key.no_current_team_error": "没有当前团队,无法创建团队 API key。", + "settings.platform.api_key.personal.description": "此 API key 绑定到你的用户,可向你的 Warp 账户发起请求。", + "settings.platform.api_key.secret_copied": "Secret key 已复制。", + "settings.platform.api_key.secret_notice": "此 secret key 只显示一次。请复制并安全保存。", + "settings.platform.api_key.select_agent_error": "请选择一个 Agent。", + "settings.platform.api_key.team.description": "此 API key 绑定到你的团队,可代表你的团队发起请求。", + "settings.platform.api_key.type.agent": "Agent", + "settings.platform.api_key.type.personal": "个人", + "settings.platform.api_key.type.team": "团队", + "settings.platform.column_created": "创建时间", + "settings.platform.column_expires_at": "过期时间", + "settings.platform.column_key": "密钥", + "settings.platform.column_last_used": "上次使用", + "settings.platform.column_name": "名称", + "settings.platform.column_scope": "范围", + "settings.platform.create_api_key_button": "+ 创建 API 密钥", + "settings.platform.description": "创建并管理 API 密钥,以允许其他 Oz 云端 Agent 访问你的 Warp 账户。\n了解更多信息,请访问", + "settings.platform.documentation_link": "文档。", + "settings.platform.never": "从不", + "settings.platform.new_api_key_title": "新建 API 密钥", + "settings.platform.no_search_results": "没有与搜索匹配的 API 密钥", + "settings.platform.save_your_key_title": "保存你的密钥", + "settings.platform.scope_agent": "Agent", + "settings.platform.scope_personal": "个人", + "settings.platform.scope_team": "团队", + "settings.platform.search_api_keys_placeholder": "搜索 API 密钥", + "settings.platform.section_header": "Oz 云端 API 密钥", + "settings.platform.zero_state_description": "创建密钥以管理对 Warp 的外部访问", + "settings.platform.zero_state_title": "暂无 API 密钥", + "settings.privacy.action.app_analytics": "应用分析", + "settings.privacy.action.cloud_ai_conversation_storage": "云端 AI 对话存储", + "settings.privacy.action.crash_reporting": "崩溃报告", + "settings.privacy.add_all_button": "全部添加", + "settings.privacy.add_regex_button": "添加正则表达式", + "settings.privacy.add_regex_modal.title": "添加正则表达式", + "settings.privacy.cloud_conversation_storage.description_disabled": "智能体对话仅存储在您本地的机器上,退出登录后将丢失,且无法共享。注意:环境智能体的对话数据仍存储在云端。", + "settings.privacy.cloud_conversation_storage.description_enabled": "智能体对话可与他人共享,并在您于不同设备登录时保留。此数据仅用于产品功能存储,Warp 不会将其用于分析。", + "settings.privacy.cloud_conversation_storage.title": "在云端存储 AI 对话", + "settings.privacy.crash_reports.description": "崩溃报告有助于调试和提升稳定性。", + "settings.privacy.crash_reports.title": "发送崩溃报告", + "settings.privacy.custom_secret_redaction.description": "使用正则表达式定义您想要脱敏的其他密钥或数据。将在下次运行命令时生效。您可以在正则表达式前添加内联 (?i) 标志使其不区分大小写。", + "settings.privacy.custom_secret_redaction.title": "自定义密钥脱敏", + "settings.privacy.data_management.description": "您可以随时选择永久删除您的 Warp 账户。届时您将无法再使用 Warp。", + "settings.privacy.data_management.link": "访问数据管理页面", + "settings.privacy.data_management.title": "管理您的数据", + "settings.privacy.enabled_by_organization": "已由您的组织启用。", + "settings.privacy.enterprise_redaction_readonly": "企业密钥脱敏规则无法修改。", + "settings.privacy.managed_by_organization": "此设置由您的组织管理。", + "settings.privacy.network_log.description": "我们打造了一个原生控制台,让您可以查看 Warp 与外部服务器之间的所有通信,确保您安心,工作始终受到保护。", + "settings.privacy.network_log.link": "查看网络日志", + "settings.privacy.network_log.title": "网络日志控制台", + "settings.privacy.no_enterprise_regexes": "您的组织尚未配置任何企业正则表达式。", + "settings.privacy.page_title": "隐私", + "settings.privacy.privacy_policy.link": "阅读 Warp 的隐私政策", + "settings.privacy.privacy_policy.title": "隐私政策", + "settings.privacy.recommended_header": "推荐", + "settings.privacy.regex.add": "添加正则表达式", + "settings.privacy.regex.invalid": "无效正则表达式", + "settings.privacy.regex.name_optional": "名称(可选)", + "settings.privacy.regex.name_placeholder": "例如 \"Google API Key\"", + "settings.privacy.regex.pattern": "正则表达式模式", + "settings.privacy.safe_mode.description": "启用此设置后,Warp 会扫描块、Warp Drive 对象内容以及 Oz 提示词中潜在的敏感信息,并阻止将这些数据保存或发送到任何服务器。您可以通过正则表达式自定义此列表。", + "settings.privacy.safe_mode.title": "密钥脱敏", + "settings.privacy.secret_display_mode.always_show": "始终显示密钥", + "settings.privacy.secret_display_mode.asterisks": "星号", + "settings.privacy.secret_display_mode.description": "选择密钥在块列表中的视觉呈现方式,同时保持其可搜索。此设置仅影响您在块列表中看到的内容。", + "settings.privacy.secret_display_mode.label": "密钥视觉脱敏模式", + "settings.privacy.secret_display_mode.strikethrough": "删除线", + "settings.privacy.tab.enterprise": "企业", + "settings.privacy.tab.personal": "个人", + "settings.privacy.telemetry.description": "应用分析有助于我们为您打造更好的产品。我们可能会收集某些控制台交互,以改进 Warp 的 AI 能力。", + "settings.privacy.telemetry.description_enterprise": "应用分析有助于我们为您打造更好的产品。我们仅收集应用使用元数据,绝不收集控制台的输入或输出。", + "settings.privacy.telemetry.free_tier_note": "在免费套餐中,必须启用分析才能使用 AI 功能。", + "settings.privacy.telemetry.read_more_link": "详细了解 Warp 如何使用数据", + "settings.privacy.telemetry.title": "帮助改进 Warp", + "settings.privacy.zdr.tooltip": "您的管理员已为您的团队启用零数据保留。系统将永不收集用户生成的内容。", + "settings.referrals.anonymous_header": "注册以参与 Warp 推荐计划", + "settings.referrals.copy_link": "复制链接", + "settings.referrals.current_referral_plural": "当前推荐数", + "settings.referrals.current_referral_singular": "当前推荐数", + "settings.referrals.email_label": "邮箱", + "settings.referrals.error_empty_email": "请输入邮箱地址。", + "settings.referrals.error_invalid_email_prefix": "请确认以下邮箱地址有效:", + "settings.referrals.header": "邀请好友使用 Warp", + "settings.referrals.link_error": "加载推荐码失败。", + "settings.referrals.link_label": "链接", + "settings.referrals.loading": "加载中…", + "settings.referrals.reward_backpack": "背包", + "settings.referrals.reward_cap": "棒球帽", + "settings.referrals.reward_hoodie": "连帽衫", + "settings.referrals.reward_hydroflask": "高级款 Hydro Flask 水壶", + "settings.referrals.reward_intro": "成功推荐好友即可获得 Warp 专属周边*", + "settings.referrals.reward_keycaps": "键帽 + 贴纸", + "settings.referrals.reward_notebook": "笔记本", + "settings.referrals.reward_theme": "专属主题", + "settings.referrals.reward_tshirt": "T 恤", + "settings.referrals.send": "发送", + "settings.referrals.sending": "发送中…", + "settings.referrals.sign_up": "注册", + "settings.referrals.terms_contact": " 如果你对推荐计划有任何疑问,请联系 referrals@warp.dev。", + "settings.referrals.terms_link": "部分条款适用。", + "settings.referrals.toast_email_failure": "邮件发送失败,请重试。", + "settings.referrals.toast_email_success": "邮件已成功发送。", + "settings.referrals.toast_link_copied": "链接已复制。", + "settings.reset_to_default": "重置为默认值", + "settings.schema.accessibility.accessibility_verbosity.description": "屏幕阅读器播报的详细程度。", + "settings.schema.account.is_settings_sync_enabled.description": "是否通过云端在设备之间同步设置。", + "settings.schema.agents.cloud_conversation_storage_enabled.description": "是否将对话存储在云端。", + "settings.schema.agents.knowledge.rules_enabled.description": "Agent 在请求中是否使用你保存的规则。", + "settings.schema.agents.knowledge.warp_drive_context_enabled.description": "AI 请求中是否包含 Warp Drive 上下文。", + "settings.schema.agents.mcp_servers.file_based_mcp_enabled.description": "是否自动检测第三方基于文件的 MCP 服务器。", + "settings.schema.agents.profiles.agent_mode_coding_file_read_allowlist.description": "Agent 无需请求权限即可读取的文件路径。", + "settings.schema.agents.profiles.agent_mode_coding_permissions.description": "Agent 的文件读取权限级别。", + "settings.schema.agents.profiles.agent_mode_command_execution_allowlist.description": "Agent 无需明确许可即可执行的命令。", + "settings.schema.agents.profiles.agent_mode_command_execution_denylist.description": "Agent 执行前必须始终询问的命令。", + "settings.schema.agents.profiles.agent_mode_execute_readonly_commands.description": "Agent 是否可以无需询问自动执行只读命令。", + "settings.schema.agents.third_party.auto_dismiss_composer_after_submit.description": "用户提交提示后,CLI Agent 富输入是否自动关闭。", + "settings.schema.agents.third_party.auto_open_composer_on_cli_agent_start.description": "CLI Agent 会话启动时,CLI Agent 富输入是否自动打开。", + "settings.schema.agents.third_party.auto_toggle_composer.description": "CLI Agent 富输入是否根据 Agent 的阻塞状态自动关闭并重新打开。", + "settings.schema.agents.third_party.cli_agent_toolbar_chip_selection_setting.description": "控制 CLI Agent 工具栏中上下文 chip 的布局。", + "settings.schema.agents.third_party.cli_agent_toolbar_enabled_commands.description": "将自定义工具栏命令模式映射到指定的 CLI Agent。", + "settings.schema.agents.third_party.should_render_cli_agent_toolbar.description": "是否为编码 Agent 命令显示 CLI Agent 底部栏。", + "settings.schema.agents.voice.voice_input_enabled.description": "控制是否为 AI 交互启用语音输入。", + "settings.schema.agents.voice.voice_input_toggle_key.description": "用于切换语音输入的按键。", + "settings.schema.agents.warp_agent.active_ai.agent_mode_query_suggestions_enabled.description": "控制是否在 Agent 模式中显示提示建议。", + "settings.schema.agents.warp_agent.active_ai.code_suggestions_enabled.description": "控制是否启用 AI 代码建议。", + "settings.schema.agents.warp_agent.active_ai.enabled.description": "控制是否启用建议等主动 AI 功能。", + "settings.schema.agents.warp_agent.active_ai.git_operations_autogen_enabled.description": "控制是否在代码审查对话框中由 AI 自动生成 commit message 和 PR 标题/正文。", + "settings.schema.agents.warp_agent.active_ai.intelligent_autosuggestions_enabled.description": "控制是否启用 AI 驱动的智能自动建议。", + "settings.schema.agents.warp_agent.active_ai.natural_language_autosuggestions_enabled.description": "控制是否为 AI 输入查询显示灰色自动建议文本。", + "settings.schema.agents.warp_agent.active_ai.rule_suggestions_enabled.description": "控制 Agent 是否在回复后建议保存规则。", + "settings.schema.agents.warp_agent.active_ai.shared_block_title_generation_enabled.description": "控制分享 block 时是否自动生成标题。", + "settings.schema.agents.warp_agent.input.agent_toolbar_chip_selection_setting.description": "控制 Agent 模式工具栏中上下文 chip 的布局。", + "settings.schema.agents.warp_agent.input.ai_auto_detection_enabled.description": "控制 AI 是否自动检测自然语言输入。", + "settings.schema.agents.warp_agent.input.ai_command_denylist.description": "从 AI 自然语言自动检测中排除的命令。", + "settings.schema.agents.warp_agent.input.include_agent_commands_in_history.description": "Agent 执行的命令是否包含在命令历史中。", + "settings.schema.agents.warp_agent.input.nld_in_terminal_enabled.description": "控制终端输入中是否启用自然语言检测。", + "settings.schema.agents.warp_agent.input.show_agent_tips.description": "是否在输入中显示 Agent 提示。", + "settings.schema.agents.warp_agent.input.show_model_selectors_in_prompt.description": "是否在输入提示中显示 AI 模型选择器。", + "settings.schema.agents.warp_agent.is_any_ai_enabled.description": "控制是否启用所有 AI 功能。", + "settings.schema.agents.warp_agent.other.agent_attribution_enabled.description": "Warp Agent 创建 commit message 和 pull request 时,是否添加 attribution co-author 行。", + "settings.schema.agents.warp_agent.other.auto_handoff_on_sleep_enabled.description": "电脑即将休眠时,Warp 是否自动将本地 Agent 对话移交到云端。", + "settings.schema.agents.warp_agent.other.cloud_agent_computer_use_enabled.description": "云端 Agent 对话是否启用 computer use。", + "settings.schema.agents.warp_agent.other.open_conversation_layout_preference.description": "打开 Agent 对话时使用新标签页还是拆分面板。", + "settings.schema.agents.warp_agent.other.should_force_disable_ampersand_handoff.description": "是否强制禁用用于云端移交 compose 模式的 & 前缀。", + "settings.schema.agents.warp_agent.other.should_force_disable_cloud_handoff.description": "是否强制禁用本地到云端的移交。", + "settings.schema.agents.warp_agent.other.should_render_use_agent_toolbar_for_user_commands.description": "是否为终端命令显示“Use Agent”底部栏。", + "settings.schema.agents.warp_agent.other.should_show_oz_updates_in_zero_state.description": "是否在 Agent 视图中显示“What's new”区域。", + "settings.schema.agents.warp_agent.other.show_agent_notifications.description": "是否显示 Agent 通知。", + "settings.schema.agents.warp_agent.other.show_conversation_history.description": "对话历史是否显示在工具面板中。", + "settings.schema.agents.warp_agent.other.thinking_display_mode.description": "控制 Agent thinking trace 在流式输出结束后的显示方式。", + "settings.schema.appearance.blocks.should_show_bootstrap_block.description": "是否在终端中显示 bootstrap block。", + "settings.schema.appearance.blocks.should_show_in_band_command_blocks.description": "是否在终端中显示 in-band command block。", + "settings.schema.appearance.blocks.should_show_ssh_block.description": "是否在终端中显示 SSH 连接 block。", + "settings.schema.appearance.blocks.show_block_dividers.description": "是否显示终端 block 之间的分隔线。", + "settings.schema.appearance.blocks.show_jump_to_bottom_of_block_button.description": "是否在较长命令输出中显示跳转到底部按钮。", + "settings.schema.appearance.cursor.cursor_blink.description": "光标是否闪烁。", + "settings.schema.appearance.cursor.cursor_display_type.description": "光标的视觉样式。", + "settings.schema.appearance.full_screen_apps.alt_screen_padding.description": "控制全屏终端应用周围的内边距。", + "settings.schema.appearance.icon.app_icon.description": "Dock 中显示的应用图标。", + "settings.schema.appearance.input.input_mode.description": "终端输入的位置。", + "settings.schema.appearance.language.description": "Warp UI 的显示语言。", + "settings.schema.appearance.panes.focus_pane_on_hover.description": "鼠标悬停时是否聚焦 pane。", + "settings.schema.appearance.panes.should_dim_inactive_panes.description": "是否在视觉上淡化非活动 pane。", + "settings.schema.appearance.spacing.description": "控制终端 block 之间的间距。", + "settings.schema.appearance.tabs.directory_tab_colors.description": "目录路径到标签页颜色分配的映射。", + "settings.schema.appearance.tabs.header_toolbar_chip_selection.description": "垂直标签页面板 header 中 toolbar chip 的配置。", + "settings.schema.appearance.tabs.preserve_active_tab_color.description": "切换标签页时是否保留活动标签页的颜色。", + "settings.schema.appearance.tabs.show_indicators_button.description": "是否在标签页上显示活动指示器。", + "settings.schema.appearance.tabs.tab_close_button_position.description": "标签页关闭按钮的位置。", + "settings.schema.appearance.tabs.workspace_decoration_visibility.description": "标签栏等 workspace 装饰的显示时机。", + "settings.schema.appearance.text.ai_font_name.description": "AI 生成内容使用的字体。", + "settings.schema.appearance.text.enforce_minimum_contrast.description": "是否强制执行最低对比度以提升文本可读性。", + "settings.schema.appearance.text.font_name.description": "终端中使用的等宽字体。", + "settings.schema.appearance.text.font_size.description": "终端中等宽字体的大小。", + "settings.schema.appearance.text.font_weight.description": "终端中等宽字体的字重。", + "settings.schema.appearance.text.ligature_rendering_enabled.description": "是否在终端中渲染字体连字。", + "settings.schema.appearance.text.line_height_ratio.description": "终端文本的行高比例。", + "settings.schema.appearance.text.match_ai_font.description": "AI 字体是否自动匹配终端字体。", + "settings.schema.appearance.text.match_notebook_to_monospace_font_size.description": "notebook 字体大小是否匹配终端字体大小。", + "settings.schema.appearance.text.notebook_font_size.description": "notebook 中使用的字体大小。", + "settings.schema.appearance.text.use_thin_strokes.description": "是否在 macOS 上使用细字体笔画。", + "settings.schema.appearance.themes.selected_system_themes.description": "系统浅色和深色模式使用的主题。", + "settings.schema.appearance.themes.system_theme.description": "是否匹配系统浅色/深色主题。", + "settings.schema.appearance.themes.theme.description": "颜色主题。", + "settings.schema.appearance.vertical_tabs.compact_subtitle.description": "紧凑垂直标签页上显示的副标题。", + "settings.schema.appearance.vertical_tabs.display_granularity.description": "垂直标签页面板中显示行的粒度。", + "settings.schema.appearance.vertical_tabs.enabled.description": "是否使用垂直方式显示标签页。", + "settings.schema.appearance.vertical_tabs.primary_info.description": "垂直标签页上显示的主要信息。", + "settings.schema.appearance.vertical_tabs.show_details_on_hover.description": "悬停在垂直标签页上时是否显示详情 sidecar。", + "settings.schema.appearance.vertical_tabs.show_diff_stats.description": "是否在垂直标签页上显示 diff 统计。", + "settings.schema.appearance.vertical_tabs.show_panel_in_restored_windows.description": "恢复窗口时,即使保存会话时垂直标签页面板已关闭,也打开该面板。", + "settings.schema.appearance.vertical_tabs.show_pr_link.description": "是否在垂直标签页上显示 PR 链接。", + "settings.schema.appearance.vertical_tabs.tab_item_mode.description": "垂直标签页中的标签项显示模式。", + "settings.schema.appearance.vertical_tabs.use_latest_prompt_as_title.description": "Agent 对话的垂直标签页名称是否使用最新的用户提示。", + "settings.schema.appearance.vertical_tabs.view_mode.description": "垂直标签栏的显示模式。", + "settings.schema.appearance.window.left_panel_visibility_across_tabs.description": "左侧面板可见性是否在所有标签页之间共享。", + "settings.schema.appearance.window.new_windows_num_columns.description": "使用自定义大小时新窗口的列数。", + "settings.schema.appearance.window.new_windows_num_rows.description": "使用自定义大小时新窗口的行数。", + "settings.schema.appearance.window.open_windows_at_custom_size.description": "是否以自定义大小而非默认大小打开新窗口。", + "settings.schema.appearance.window.override_blur_texture.description": "是否对窗口背景应用模糊纹理。", + "settings.schema.appearance.window.override_blur.description": "应用到窗口背景的模糊半径。", + "settings.schema.appearance.window.override_opacity.description": "窗口背景的不透明度,范围为 1 到 100%。", + "settings.schema.appearance.window.zoom_level.description": "窗口缩放级别,以百分比表示。", + "settings.schema.cloud_platform.third_party_api_keys.aws_bedrock_auth_refresh_command.description": "用于刷新 Bedrock AWS 凭证的命令。", + "settings.schema.cloud_platform.third_party_api_keys.aws_bedrock_auto_login.description": "Bedrock 凭证过期时,是否自动运行 AWS 登录命令。", + "settings.schema.cloud_platform.third_party_api_keys.aws_bedrock_credentials_enabled.description": "Warp 是否应将你的本地 AWS 凭证用于启用 Bedrock 的请求。", + "settings.schema.cloud_platform.third_party_api_keys.aws_bedrock_profile.description": "用于 Bedrock 凭证的 AWS profile 名称。", + "settings.schema.cloud_platform.third_party_api_keys.can_use_warp_credits_with_byok.description": "用户提供的模型不可用时,是否可以使用 Warp credits 作为 fallback。", + "settings.schema.code.editor.auto_open_code_review_pane_on_first_agent_change.description": "Agent 第一次做出更改时,是否自动打开代码审查 pane。", + "settings.schema.code.editor.open_code_panels_file_editor.description": "从代码面板打开文件时使用的编辑器。", + "settings.schema.code.editor.open_file_editor.description": "用于打开文件的编辑器。", + "settings.schema.code.editor.open_file_layout.description": "在编辑器中打开文件时使用的布局。", + "settings.schema.code.editor.prefer_markdown_viewer.description": "打开 Markdown 文件时是否使用 Markdown viewer。", + "settings.schema.code.editor.prefer_tabbed_editor_view.description": "是否优先在 tabbed editor view 中打开文件。", + "settings.schema.code.editor.show_code_review_button.description": "是否在标签页上显示代码审查按钮。", + "settings.schema.code.editor.show_code_review_diff_stats.description": "是否在代码审查按钮上显示新增/删除行数。", + "settings.schema.code.editor.show_global_search.description": "是否在工具面板中显示全局文件搜索。", + "settings.schema.code.editor.show_project_explorer.description": "是否在工具面板中显示项目资源管理器。", + "settings.schema.code.editor.use_warp_as_default_editor.description": "是否将 Warp 用作默认代码编辑器。", + "settings.schema.code.indexing.agent_mode_codebase_context_auto_indexing.description": "是否启用自动代码库索引。", + "settings.schema.code.indexing.agent_mode_codebase_context.description": "是否向 AI Agent 提供代码库上下文。", + "settings.schema.description": "Warp 设置的 JSON Schema({channel} 频道,{entry_count} 项设置)", + "settings.schema.experimental.async_find_enabled.description": "使用改进的查找实现,以便在大型输出中搜索匹配项时保持 UI 响应。", + "settings.schema.general.default_session_mode.description": "新终端会话的默认模式。", + "settings.schema.general.link_tooltip.description": "悬停在链接上时是否显示 tooltip。", + "settings.schema.general.login_item.description": "登录时是否自动启动 Warp。", + "settings.schema.general.mouse_scroll_multiplier.description": "鼠标滚动事件的滚动速度倍率。", + "settings.schema.general.new_tab_placement.description": "新标签页在标签栏中的放置位置。", + "settings.schema.general.quit_on_last_window_closed.description": "关闭最后一个窗口时是否退出 Warp。", + "settings.schema.general.restore_session.description": "Warp 启动时是否恢复上一个会话。", + "settings.schema.general.should_confirm_close_session.description": "关闭会话时是否显示确认对话框。", + "settings.schema.general.show_changelog_after_update.description": "更新后是否显示 changelog。", + "settings.schema.general.show_warning_before_quitting.description": "退出 Warp 前是否显示警告对话框。", + "settings.schema.general.snackbar_enabled.description": "是否显示 snackbar 通知。", + "settings.schema.general.undo_close.enabled.description": "是否启用撤销关闭功能。", + "settings.schema.general.undo_close.grace_period.description": "关闭标签页后仍可撤销关闭的时长。", + "settings.schema.general.user_native_preference.description": "是否优先使用原生桌面应用或 Web 应用。", + "settings.schema.global_hotkey.dedicated_window.enabled.description": "是否启用专用热键窗口。与 `global_hotkey.toggle_all_windows.enabled` 互斥;同一时间只能有一个为 true。", + "settings.schema.global_hotkey.dedicated_window.settings.description": "Quake Mode 窗口行为的配置选项。", + "settings.schema.global_hotkey.toggle_all_windows.enabled.description": "是否启用切换所有窗口可见性的热键。与 `global_hotkey.dedicated_window.enabled` 互斥;同一时间只能有一个为 true。", + "settings.schema.global_hotkey.toggle_all_windows.keybinding.description": "全局激活热键使用的 keybinding。格式:修饰键(cmd、ctrl、alt、shift、meta)和按键用 `-` 连接,例如 \"cmd-shift-a\" 或 \"alt-enter\"。绑定区分大小写:存在 shift 时,按键必须是其 shift 后的形式(例如 \"ctrl-shift-E\",而不是 \"ctrl-shift-e\")。", + "settings.schema.keys.ctrl_tab_behavior_setting.description": "控制 Ctrl+Tab 的行为。", + "settings.schema.notifications.preferences.description": "终端事件的通知偏好设置。", + "settings.schema.notifications.toast_duration_secs.description": "通知 toast 显示的时长,以秒为单位。", + "settings.schema.privacy.crash_reporting_enabled.description": "是否发送崩溃报告。", + "settings.schema.privacy.custom_secret_regex_list.description": "用于检测和遮盖 secret 的自定义 regex pattern。", + "settings.schema.privacy.secret_redaction.enabled.description": "是否启用 secret redaction,以检测并遮盖终端输出中的 secret。", + "settings.schema.privacy.secret_redaction.hide_secrets_in_block_list.description": "是否在 block list 中用星号隐藏检测到的 secret。", + "settings.schema.privacy.secret_redaction.secret_display_mode_setting.description": "控制检测到的 secret 在终端中的视觉显示方式。", + "settings.schema.privacy.telemetry_enabled.description": "是否收集匿名使用情况遥测。", + "settings.schema.session.new_session_shell_override.description": "打开新会话时使用的 shell。", + "settings.schema.session.startup_shell_override.description": "Warp 启动时使用的 shell。", + "settings.schema.session.working_directory_config.description": "控制打开新会话时使用的工作目录。", + "settings.schema.system.force_x11.description": "是否在 Linux 上强制使用 X11 而不是 Wayland。", + "settings.schema.system.linux_selection_clipboard.description": "是否使用 Linux primary selection clipboard。", + "settings.schema.system.prefer_low_power_gpu.description": "是否优先使用集成(低功耗)GPU。", + "settings.schema.system.preferred_graphics_backend.description": "Windows 上首选的图形后端。", + "settings.schema.terminal.copy_on_select.description": "选中文本时是否自动复制到剪贴板。", + "settings.schema.terminal.focus_reporting_enabled.description": "是否将焦点和失焦事件转发给全屏终端应用。", + "settings.schema.terminal.input.alias_expansion_enabled.description": "是否在输入中启用 shell alias expansion。", + "settings.schema.terminal.input.at_context_menu_in_terminal_mode.description": "终端模式中是否可使用 @ 上下文菜单。", + "settings.schema.terminal.input.autosuggestions.enabled.description": "是否显示命令自动建议。", + "settings.schema.terminal.input.autosuggestions.keybinding_hint.description": "是否显示自动建议 keybinding 提示。", + "settings.schema.terminal.input.autosuggestions.show_ignore_button.description": "是否为自动建议显示忽略按钮。", + "settings.schema.terminal.input.classic_completions_mode.description": "是否启用经典补全模式。", + "settings.schema.terminal.input.command_corrections.description": "是否为输错的命令建议修正。", + "settings.schema.terminal.input.completions_open_while_typing.description": "键入时是否自动打开补全菜单。", + "settings.schema.terminal.input.enable_slash_commands_in_terminal.description": "终端输入中是否可使用 slash command。", + "settings.schema.terminal.input.error_underlining_enabled.description": "是否在输入中为命令错误添加下划线。", + "settings.schema.terminal.input.extra_meta_keys.description": "控制哪些额外按键被视为 meta 键。", + "settings.schema.terminal.input.honor_ps1.description": "是否使用你的 shell 的 PS1 prompt,而不是 Warp prompt。", + "settings.schema.terminal.input.input_box_type_setting.description": "终端输入样式。", + "settings.schema.terminal.input.middle_click_paste_enabled.description": "是否通过中键点击从剪贴板粘贴。", + "settings.schema.terminal.input.outline_codebase_symbols_for_at_context_menu.description": "@ 上下文菜单中是否显示代码库符号。", + "settings.schema.terminal.input.show_hint_text.description": "是否在终端输入中显示提示文本。", + "settings.schema.terminal.input.show_terminal_input_message_bar.description": "是否显示终端输入消息栏。", + "settings.schema.terminal.input.syntax_highlighting.description": "是否在终端输入中启用语法高亮。", + "settings.schema.terminal.maximum_grid_size.description": "终端网格的最大行数。", + "settings.schema.terminal.mouse_reporting_enabled.description": "是否将鼠标事件转发给全屏终端应用。", + "settings.schema.terminal.scroll_reporting_enabled.description": "是否将滚动事件转发给全屏终端应用。", + "settings.schema.terminal.show_terminal_zero_state_block.description": "是否在新终端会话中显示 AI zero-state block。", + "settings.schema.terminal.smart_select.enabled.description": "是否为 URL、邮箱、文件路径和标识符启用双击智能选择。", + "settings.schema.terminal.smart_select.word_char_allowlist.description": "禁用智能选择时,双击选择中被视为单词一部分的字符。", + "settings.schema.terminal.use_audible_bell.description": "终端 bell 事件发生时是否播放可听提示音。", + "settings.schema.text_editing.autocomplete_symbols.description": "是否自动补全括号和引号等匹配符号。", + "settings.schema.text_editing.code_editor_line_number_mode.description": "代码编辑器中行号的显示方式。", + "settings.schema.text_editing.vim_mode_enabled.description": "是否启用 Vim keybindings。", + "settings.schema.text_editing.vim_status_bar.description": "是否显示 Vim 状态栏。", + "settings.schema.text_editing.vim_unnamed_system_clipboard.description": "Vim unnamed register 是否使用系统剪贴板。", + "settings.schema.title": "Warp 设置", + "settings.schema.warp_drive.enabled.description": "是否启用 Warp Drive。", + "settings.schema.warp_drive.sorting_choice.description": "Warp Drive 中项目的排序方式。", + "settings.schema.warpify.ssh.enable_legacy_ssh_wrapper.description": "是否为 SSH 会话启用旧版 SSH wrapper。", + "settings.schema.warpify.ssh.enable_ssh_warpification.description": "是否在 SSH 会话中启用 Warp 功能。", + "settings.schema.warpify.ssh.ssh_extension_install_mode.description": "控制 SSH extension 安装行为。", + "settings.schema.warpify.ssh.ssh_hosts_denylist.description": "不应触发 warpification 提示的 SSH host。", + "settings.schema.warpify.ssh.use_ssh_tmux_wrapper.description": "SSH warpification 是否使用基于 tmux 的 wrapper。", + "settings.schema.warpify.subshells.added_subshell_commands.description": "应识别为 subshell 的命令的额外 regex pattern。", + "settings.schema.warpify.subshells.subshell_commands_denylist.description": "不应触发 subshell warpification 提示的命令。", + "settings.schema.workflows.show_global_workflows_in_universal_search.description": "是否在通用搜索结果中显示全局 workflow。", + "settings.search.no_matches": "没有设置与你的搜索匹配。", + "settings.search.no_matches_hint": "你可以尝试使用不同关键词,或检查是否有拼写错误。", + "settings.shared_blocks.unshare": "取消共享", + "settings.show_blocks.deleting": "正在删除...", + "settings.show_blocks.empty": "你还没有任何共享块。", + "settings.show_blocks.executed_on": "执行时间:{date}", + "settings.show_blocks.link_copied": "链接已复制。", + "settings.show_blocks.load_failed": "加载块失败。请重试。", + "settings.show_blocks.loading": "正在获取块...", + "settings.show_blocks.title": "共享块", + "settings.show_blocks.unshare_block": "取消共享块", + "settings.show_blocks.unshare_confirmation": "确定要取消共享此块吗?\n\n它将无法再通过链接访问,并会从 Warp 服务器永久删除。", + "settings.show_blocks.unshare_failed": "取消共享块失败。请重试。", + "settings.show_blocks.unshare_success": "块已成功取消共享。", + "settings.startup_shell.custom": "自定义", + "settings.teams.action.cancel_invite": "取消邀请", + "settings.teams.action.demote_from_admin": "取消管理员", + "settings.teams.action.promote_to_admin": "设为管理员", + "settings.teams.action.remove_domain": "移除域名", + "settings.teams.action.remove_from_team": "移出团队", + "settings.teams.action.transfer_ownership": "转移所有权", + "settings.teams.badge.past_due": "已逾期", + "settings.teams.badge.unpaid": "未付款", + "settings.teams.billing.compare_plans": "对比套餐", + "settings.teams.billing.upgrade_build": "升级到 Build", + "settings.teams.billing.upgrade_lightspeed": "升级到 Lightspeed 套餐", + "settings.teams.billing.upgrade_turbo": "升级到 Turbo 套餐", + "settings.teams.button.contact_admin_request": "联系管理员申请加入", + "settings.teams.button.contact_support": "联系客服", + "settings.teams.button.create": "创建", + "settings.teams.button.delete_team": "删除团队", + "settings.teams.button.invite": "邀请", + "settings.teams.button.join": "加入", + "settings.teams.button.leave_team": "退出团队", + "settings.teams.button.manage_billing": "管理账单", + "settings.teams.button.manage_plan": "管理套餐", + "settings.teams.button.open_admin_panel": "打开管理面板", + "settings.teams.button.set_domains": "设置", + "settings.teams.chip.admin": "管理员", + "settings.teams.chip.expired": "已过期", + "settings.teams.chip.owner": "所有者", + "settings.teams.chip.pending": "待处理", + "settings.teams.cost_info.per_seat_no_price": "新增成员将按你套餐的单用户价格计费。{prorated}", + "settings.teams.cost_info.per_seat_with_price": "新增成员将按你套餐的单用户价格计费:${monthly}/月 或 ${yearly}/年,具体取决于你的计费周期。{prorated}", + "settings.teams.cost_info.prorated_admin": "你将按比例为该成员使用 Warp 的部分用量付费。", + "settings.teams.cost_info.prorated_member": "你的管理员将按比例为该成员使用 Warp 的部分用量付费。", + "settings.teams.create.description": "创建团队后,你可以通过共享云端 Agent 运行记录、环境、自动化流程与产物,开展由 Agent 驱动的协作开发。你还可以为团队成员和 Agent 创建共享的知识库。", + "settings.teams.create.discovery_same_domain": "允许与你邮箱域名相同的 Warp 用户发现并加入该团队。", + "settings.teams.create.name_placeholder": "团队名称", + "settings.teams.create.subtitle": "创建团队", + "settings.teams.create.title": "团队", + "settings.teams.discovery.join_cta": "加入该团队,开始协作共享 Workflow、Notebook 等内容。", + "settings.teams.discovery.join_existing_subtitle": "或者,加入公司内已有的团队", + "settings.teams.discovery.teammate_count_plural": "{count} 名成员", + "settings.teams.discovery.teammate_count_single": "1 名成员", + "settings.teams.invite.by_discovery_header": "通过发现邀请", + "settings.teams.invite.by_email_header": "通过邮箱邀请", + "settings.teams.invite.by_link_header": "通过链接邀请", + "settings.teams.invite.discovery_instructions": "允许使用 @{domain} 邮箱的 Warp 用户发现并加入该团队。", + "settings.teams.invite.domain_restrictions_instructions": "按域名限制——仅允许使用特定邮箱域名的用户通过邀请链接加入你的团队。", + "settings.teams.invite.domains_placeholder": "域名,以逗号分隔", + "settings.teams.invite.email_expiry_instructions": "邮箱邀请有效期为 7 天。", + "settings.teams.invite.emails_placeholder": "邮箱地址,以逗号分隔", + "settings.teams.invite.header": "邀请团队成员", + "settings.teams.invite.invalid_domains_instructions": "部分填写的域名无效,或已被添加。", + "settings.teams.invite.invalid_emails_instructions": "部分填写的邮箱地址无效、已被邀请,或已是团队成员。", + "settings.teams.invite.link_load_failed": "加载邀请链接失败。", + "settings.teams.invite.link_toggle_instructions": "作为管理员,你可以选择是否允许团队成员通过邀请链接邀请他人加入。", + "settings.teams.invite.reset_links": "重置链接", + "settings.teams.members.capacity_tooltip": "你的套餐({plan})最多可容纳 {cap} 名成员。", + "settings.teams.members.count_plural": "{count} 名团队成员", + "settings.teams.members.count_single": "1 名团队成员", + "settings.teams.members.header": "团队成员", + "settings.teams.offline_text": "你当前处于离线状态。", + "settings.teams.outgrow.need_more_seats": "需要更多席位?", + "settings.teams.outgrow.upgrade_business_link": "升级到 Business", + "settings.teams.outgrow.upgrade_business_suffix": ",即可提高团队成员上限。", + "settings.teams.outgrow.upgrade_enterprise_link": "升级到 Enterprise", + "settings.teams.outgrow.upgrade_enterprise_suffix": ",即可不限团队成员数量。", + "settings.teams.plan_usage.free_limits_header": "免费套餐用量限制", + "settings.teams.plan_usage.limits_header": "套餐用量限制", + "settings.teams.plan_usage.shared_notebooks": "共享 Notebook", + "settings.teams.plan_usage.shared_workflows": "共享 Workflow", + "settings.teams.rename_placeholder": "新的团队名称", + "settings.teams.toast.billing_link_failed": "生成账单链接失败。请通过 feedback@warp.dev 联系我们。", + "settings.teams.toast.discoverability_toggle_failed": "切换团队可发现性失败", + "settings.teams.toast.discoverability_toggled": "已切换团队可发现性", + "settings.teams.toast.domain_add_failed": "添加域名限制失败", + "settings.teams.toast.domain_delete_failed": "删除域名限制失败", + "settings.teams.toast.domains_added_count": "已添加域名限制:{count} 个", + "settings.teams.toast.invalid_domains_count": "无效域名:{count} 个", + "settings.teams.toast.invalid_emails_count": "无效邮箱地址:{count} 个", + "settings.teams.toast.invite_delete_failed": "删除邀请失败", + "settings.teams.toast.invite_deleted": "已删除邀请", + "settings.teams.toast.invite_links_reset": "已重置邀请链接", + "settings.teams.toast.invite_links_reset_failed": "重置邀请链接失败", + "settings.teams.toast.invite_links_toggle_failed": "切换邀请链接状态失败", + "settings.teams.toast.invite_links_toggled": "已切换邀请链接状态", + "settings.teams.toast.invite_send_failed": "发送邀请失败", + "settings.teams.toast.invite_sent_multiple": "{count} 封邀请已发出!", + "settings.teams.toast.invite_sent_single": "邀请已发出!", + "settings.teams.toast.join_failed": "加入团队失败", + "settings.teams.toast.join_success_generic": "已成功加入团队", + "settings.teams.toast.join_success_named": "已成功加入「{name}」", + "settings.teams.toast.leave_failed": "退出团队失败", + "settings.teams.toast.leave_success": "已成功退出团队", + "settings.teams.toast.link_copied": "链接已复制到剪贴板!", + "settings.teams.toast.ownership_transfer_failed": "转移团队所有权失败", + "settings.teams.toast.ownership_transferred": "已成功转移团队所有权", + "settings.teams.toast.rename_failed": "重命名团队失败", + "settings.teams.toast.rename_success": "已成功重命名团队", + "settings.teams.toast.role_update_failed": "更新团队成员角色失败", + "settings.teams.toast.role_updated": "已成功更新团队成员角色", + "settings.teams.toast.upgrade_link_failed": "生成升级链接失败。请通过 feedback@warp.dev 联系我们。", + "settings.teams.transfer_ownership.description": "确定要将团队所有权转移给 {email} 吗?你将不再是所有者,也无法再对此团队执行任何管理操作。", + "settings.teams.transfer_ownership.modal_title": "转移团队所有权?", + "settings.teams.warning.cta_button_contact_support": "联系客服", + "settings.teams.warning.cta_button_update_billing": "更新账单", + "settings.teams.warning.cta_button_upgrade": "升级", + "settings.teams.warning.cta_contact_admin_grow": "请联系团队管理员以扩充团队规模。", + "settings.teams.warning.cta_contact_admin_restore": "请联系团队管理员以恢复访问权限。", + "settings.teams.warning.cta_contact_sales_grow": "请联系销售团队以扩充团队规模。", + "settings.teams.warning.cta_contact_support_restore": "请联系客服以恢复访问权限。", + "settings.teams.warning.cta_update_payment_restore": "更新付款信息以恢复访问权限。", + "settings.teams.warning.cta_upgrade_grow": "升级套餐以扩充团队规模。", + "settings.teams.warning.member_limit_exceeded_title": "已超出成员上限", + "settings.teams.warning.payment_past_due_body": "由于付款逾期,团队邀请已被限制。", + "settings.teams.warning.payment_past_due_title": "付款已逾期", + "settings.teams.warning.payment_unpaid_body": "由于订阅未付款,团队邀请已被限制。", + "settings.teams.warning.seat_cap_exceeded_body": "你已超出当前套餐的成员上限。现有团队成员仍可正常使用,但你将无法再添加新成员。", + "settings.teams.warning.seat_cap_reached_body": "你已达到当前套餐的成员上限。", + "settings.teams.warning.subscription_unpaid_title": "订阅未付款", + "settings.teams.warning.team_full_title": "团队席位已满", + "settings.title": "设置", + "settings.tooltip.learn_more_docs": "点击可在文档中了解更多", + "settings.tooltip.not_synced": "此设置不会同步到你的其他设备", + "settings.transfer_ownership.transfer": "转让", + "settings.undo_close.enable_reopening": "启用重新打开已关闭会话", + "settings.undo_close.grace_period_seconds": "宽限时间(秒)", + "settings.warp_drive.action.disable": "停用 Warp Drive", + "settings.warp_drive.action.enable": "启用 Warp Drive", + "settings.warp_drive.create_account_required": "要使用 Warp Drive,请先创建账号。", + "settings.warp_drive.description": "Warp Drive 是终端中的工作区,可用于保存 Workflow、Notebook、Prompt 和环境变量,供个人使用或与团队共享。", + "settings.warpify.action.ssh_session_detection": "用于 Warpify 的 SSH 会话检测", + "settings.warpify.action.ssh_warpification": "SSH Warpify", + "settings.warpify.added_commands.title": "已添加的命令", + "settings.warpify.command_placeholder": "命令(支持正则表达式)", + "settings.warpify.denylisted_commands.title": "已加入拒绝列表的命令", + "settings.warpify.denylisted_hosts.title": "已加入拒绝列表的主机", + "settings.warpify.description": "配置 Warp 是否对特定 Shell 启用「Warpify」(添加对块、输入模式等的支持)。", + "settings.warpify.host_placeholder": "主机(支持正则表达式)", + "settings.warpify.install_mode.always_ask": "始终询问", + "settings.warpify.install_mode.always_install": "始终安装", + "settings.warpify.install_mode.never_install": "永不安装", + "settings.warpify.install_ssh_extension.description": "控制当远程主机未安装 Warp SSH 扩展时的安装行为。", + "settings.warpify.install_ssh_extension.label": "安装 SSH 扩展", + "settings.warpify.learn_more": "了解更多", + "settings.warpify.ssh_sessions.label": "对 SSH 会话启用 Warpify", + "settings.warpify.ssh.subtitle": "为交互式 SSH 会话启用 Warpify。", + "settings.warpify.subshells.subtitle": "支持的子 Shell:bash、zsh 和 fish。", + "settings.warpify.subshells.title": "子 Shell", + "settings.warpify.tmux_description": "tmux SSH 封装器在许多默认封装器无法工作的场景中仍然有效,但可能需要你点击按钮才能启用 Warpify。该设置在新标签页中生效。", + "settings.warpify.use_tmux.label": "使用 Tmux Warpify", + "settings.working_directory.advanced": "高级", + "settings.working_directory.new_tab": "新标签页", + "settings.working_directory.new_window": "新窗口", + "settings.working_directory.split_pane": "拆分窗格", + "settings.workspace_override_tooltip": "此选项由你组织的设置强制执行,无法自定义。", + "tab.menu.copy_branch": "复制分支", + "tab.menu.copy_pane_title": "复制窗格标题", + "tab.menu.copy_pull_request_link": "复制拉取请求链接", + "tab.menu.copy_tab_title": "复制标签页标题", + "tab.menu.copy_working_directory": "复制工作目录", + "tab_configs.add_new_repo": "+ 添加新仓库...", + "tab_configs.already_default": "已是默认", + "tab_configs.auto_create_worktree": "打开新标签页时自动创建 worktree", + "tab_configs.auto_create_worktree_required": "必须先选择自动创建 worktree,才能选择此项", + "tab_configs.auto_generate_worktree_branch_name": "自动生成 worktree 分支名称", + "tab_configs.create_first_config": "创建你的第一个标签页配置", + "tab_configs.create_first_config.subtitle_with_session_type": "为标签页设置一个可复用的起点。选择仓库、会话类型,并可选择附加 worktree。以后需要用这套设置打开新标签页时即可复用。", + "tab_configs.create_first_config.subtitle_without_session_type": "为标签页设置一个可复用的起点。选择仓库,并可选择附加 worktree。以后需要用这套设置打开新标签页时即可复用。", + "tab_configs.edit_config": "编辑配置", + "tab_configs.fetching_branches": "正在获取分支...", + "tab_configs.get_warping": "开始使用 Warp", + "tab_configs.make_default": "设为默认", + "tab_configs.new_worktree.autogenerate_branch_name": "自动生成 worktree 分支名称", + "tab_configs.new_worktree.branch_name": "Worktree 分支名称", + "tab_configs.new_worktree.invalid_branch_name": "名称只能包含字母、数字、连字符和下划线", + "tab_configs.new_worktree.select_branch": "选择分支", + "tab_configs.new_worktree.select_repository": "选择仓库", + "tab_configs.new_worktree.title": "新建 worktree", + "tab_configs.open_tab": "打开标签页", + "tab_configs.params.default_value": "默认值:{default_value}", + "tab_configs.params.enter": "输入{name}", + "tab_configs.remove.description": "此标签页配置将被永久删除。此操作无法撤销。", + "tab_configs.remove.title": "移除“{name}”?", + "tab_configs.select_directory": "选择目录", + "tab_configs.select_git_repo_for_worktree": "选择一个 Git 仓库以启用 worktree 支持", + "tab_configs.session_type": "会话类型", + "tab_configs.session_type.built_in_agent": "内置 Agent", + "tab_configs.session_type.terminal": "终端", + "tab.close_other_tabs": "关闭其他标签页", + "tab.close_tab": "关闭标签页", + "tab.cloud_agent_run": "云端 agent 运行", + "tab.move_to_group": "移动到分组", + "tab.new_group_with_tab": "用标签页新建分组", + "tab.rename_tab": "重命名标签页", + "tab.reset_tab_name": "重置标签页名称", + "tab.save_as_new_config": "另存为新配置", + "terminal.a11y.block_background_status": "后台运行", + "terminal.a11y.block_command_output": "块 {index}:{command}。输出:{output}", + "terminal.a11y.block_failed_status": "失败,状态码 {code}", + "terminal.a11y.block_in_progress_status": "进行中", + "terminal.a11y.block_output": "块 {index}。\n输出:{output}", + "terminal.a11y.block_selection_help": "按 cmd-C 读取并复制命令和输出,按 cmd-option-shift-C 只读取并复制输出。按 cmd-B 为此块添加书签;可以用 option-up 和 option-down 在已加书签的块之间快速导航。", + "terminal.a11y.block_succeeded_status": "成功", + "terminal.a11y.block_summary": "块 {index}:{command},{status}。\n", + "terminal.a11y.command_correction_help": "按向右箭头插入,或继续编辑以忽略", + "terminal.a11y.command_correction_suggested": "建议修正命令:{command}", + "terminal.a11y.copied_block_outputs": "已复制 {count} 个块输出。\n{outputs}", + "terminal.a11y.copied_blocks": "已复制 {count} 个块。\n{blocks}", + "terminal.a11y.execute_rewind_ai": "执行倒回到 AI 对话中此位置之前。", + "terminal.a11y.open_ai_attached_blocks_menu": "打开附加为此 AI 查询上下文的块列表。", + "terminal.a11y.open_ai_block_overflow_menu": "打开此 AI 块的复制选项菜单。", + "terminal.a11y.open_block_filter_editor": "打开块 {index} 的块过滤器编辑器", + "terminal.a11y.opened_file_search_palette": "已打开文件搜索面板", + "terminal.a11y.opened_warpify_settings": "已打开 Warpify 设置", + "terminal.a11y.oz_confirmation_continue": "Oz 需要你的确认才能继续", + "terminal.a11y.oz_permission_edit_file": "Oz 需要你的权限才能编辑文件", + "terminal.a11y.oz_permission_read_files": "Oz 需要你的权限才能读取文件", + "terminal.a11y.oz_permission_run_command": "Oz 需要你的权限才能运行 `{command}`", + "terminal.a11y.oz_permission_search_codebase": "Oz 需要你的权限才能搜索你的代码库", + "terminal.a11y.oz_permission_write_to_shell": "Oz 需要你的权限才能与正在运行的 shell 命令交互", + "terminal.a11y.pick_repo_to_open": "使用文件选择器选择一个 Git 仓库", + "terminal.a11y.recognized": "已识别 {title}。", + "terminal.a11y.rewind_ai_confirmation": "显示确认对话框,倒回到 AI 对话中此位置之前。", + "terminal.a11y.scrolled_bottom_selected_block": "已滚动到所选块底部", + "terminal.a11y.scrolled_bottom_visible_block": "已滚动到最底部可见块的底部", + "terminal.a11y.scrolled_top_selected_block": "已滚动到所选块顶部", + "terminal.a11y.select_ai_attached_block": "点击一个作为此 AI 查询上下文附加的块。", + "terminal.a11y.selected_all_blocks": "已选择全部 {count} 个块。", + "terminal.a11y.selected_blocks": "已选择 {count} 个块。", + "terminal.a11y.showed_initialization_block": "已显示初始化块", + "terminal.a11y.toggle_bookmark_block": "切换块书签", + "terminal.a11y.warpify_with_key": "你可以按 {key} 对这个 {title} 进行 Warpify,以获得更多 Warp 功能。", + "terminal.a11y.warpify_without_key": "你可以对这个 {title} 进行 Warpify,以获得更多 Warp 功能。", + "terminal.agent_conversation.default_title": "新 Agent 对话", + "terminal.agent_message_bar.autodetected_shell_command": "已自动检测到 shell 命令", + "terminal.agent_message_bar.autodetected_shell_command_prefix": "已自动检测到 shell 命令,", + "terminal.agent_message_bar.starting_shell": "正在启动 shell...", + "terminal.agent_message_bar.to_exit_shell_mode": "退出 shell 模式", + "terminal.agent_view_header.for_terminal": "返回终端", + "terminal.ambient_agent.auth_secret.api_key": "API 密钥", + "terminal.ambient_agent.auth_secret.choose_type": "选择类型", + "terminal.ambient_agent.auth_secret.delete_a11y": "删除 API 密钥 {name}", + "terminal.ambient_agent.auth_secret.delete_confirmation": "确定要删除 {name} 吗?此操作无法撤销。任何引用此密钥的 Agent 或环境都将无法再访问它。", + "terminal.ambient_agent.auth_secret.delete_failed": "删除 API 密钥“{name}”失败:{error}", + "terminal.ambient_agent.auth_secret.deleted": "API 密钥“{name}”已删除。", + "terminal.ambient_agent.auth_secret.enter_credentials": "在下方输入你的凭证。", + "terminal.ambient_agent.auth_secret.inherit_from_environment": "从环境继承密钥", + "terminal.ambient_agent.auth_secret.learn_more": "了解 Warp 中 {harness_name} 认证的更多信息。", + "terminal.ambient_agent.auth_secret.name_label": "名称", + "terminal.ambient_agent.auth_secret.name_placeholder": "例如 My API Key", + "terminal.ambient_agent.auth_secret.optional_label": "{label}(可选)", + "terminal.ambient_agent.auth_secret.save_failed": "保存 API 密钥失败:{error}", + "terminal.ambient_agent.auth_secret.saved": "API 密钥“{name}”已保存。", + "terminal.ambient_agent.auth_secret.select_type_description": "选择一个 API 密钥类型,以便在云端通过 Oz 使用 {display_name}。", + "terminal.ambient_agent.default_cloud_agent_title": "新的云端 Agent", + "terminal.ambient_agent.error.cloud_agent_failed": "云端 Agent 失败", + "terminal.ambient_agent.footer.error_header": "Agent 失败", + "terminal.ambient_agent.footer.loading_body": "很快即可与 Oz 交互", + "terminal.ambient_agent.footer.loading_header": "云端 Agent 正在启动…", + "terminal.ambient_agent.generic_agent_name": "Agent", + "terminal.ambient_agent.harness_selector.label": "Agent 运行框架", + "terminal.ambient_agent.harness_selector.locked_to_warp": "此对话正在使用 Warp Agent,因此云端交接也会使用 Warp", + "terminal.ambient_agent.harness_session.running": "正在运行 {name}...", + "terminal.ambient_agent.host_selector.label": "执行主机", + "terminal.ambient_agent.model_selector.default": "默认", + "terminal.ambient_agent.model_selector.no_results": "无结果", + "terminal.ambient_agent.model_selector.search_placeholder": "搜索模型", + "terminal.ambient_agent.model_selector.tooltip": "选择 Agent 模型", + "terminal.ambient_agent.setup_command.ran": "已运行设置命令", + "terminal.ambient_agent.setup_command.running": "正在运行设置命令...", + "terminal.ambient_agent.setup_status.connecting_to_host": "正在连接主机(第 1/3 步)", + "terminal.ambient_agent.setup_status.creating_environment": "正在创建环境(第 2/3 步)", + "terminal.ambient_agent.setup_status.starting_environment": "正在启动环境(第 3/3 步)", + "terminal.ambient_agent.status.agent_failed": "Agent 失败", + "terminal.ambient_agent.status.agent_working": "Agent 正在处理任务", + "terminal.ambient_agent.status.auth_required": "需要认证", + "terminal.ambient_agent.status.cancelled": "已取消", + "terminal.ambient_agent.status.child_github_auth_required": "启动子 Agent 前需要 GitHub 认证。", + "terminal.ambient_agent.status.github_auth_required": "需要 GitHub 认证", + "terminal.ambient_agent.status.starting_environment": "正在启动环境...", + "terminal.ambient_agent.tips.ci_failures_fix": "构建响应 CI 失败并尝试自动修复的 Agent。", + "terminal.ambient_agent.tips.daily_issue_summaries": "设置 Agent,为新打开的 issue 生成每日摘要。", + "terminal.ambient_agent.tips.dashboard_agent_activity": "构建仪表盘,跟踪团队中的所有 Agent 活动。", + "terminal.ambient_agent.tips.fork_completed_session": "将已完成的 Oz 云端 Agent 会话 fork 到 Warp 中,继续本地工作。", + "terminal.ambient_agent.tips.format_lint_schedule": "构建按计划自动格式化并 lint 代码的 Agent。", + "terminal.ambient_agent.tips.github_actions_agent_action": "通过 GitHub Actions 使用 `oz-agent-action` 运行 Agent。", + "terminal.ambient_agent.tips.internal_slack_bot": "构建内部 Slack bot,将编码任务委托给 Oz Agent。", + "terminal.ambient_agent.tips.internal_tools_databases": "构建内部工具,用 Agent 回答来自数据库的问题。", + "terminal.ambient_agent.tips.join_run_realtime": "使用 Agent Session Sharing 实时加入任何 Oz 云端 Agent 运行。", + "terminal.ambient_agent.tips.linear_fix_bugs": "创建在 Linear 提交 issue 时自动修复 bug 的 Agent。", + "terminal.ambient_agent.tips.linear_tag_oz": "在 Linear issue 中标记 @Oz,以自动调查并提出修复方案。", + "terminal.ambient_agent.tips.mcp_servers_access": "配置 MCP 服务器,让 Oz 云端 Agent 访问 GitHub、Linear 和 Sentry。", + "terminal.ambient_agent.tips.monitor_success_rates": "使用 Oz API 监控 Agent 成功率和运行时长。", + "terminal.ambient_agent.tips.nightly_dependency_updates": "创建每晚运行的 Agent 来检查依赖更新。", + "terminal.ambient_agent.tips.oz_agent_run": "使用 `oz agent run` 启动任务,无需打开 Warp 终端。", + "terminal.ambient_agent.tips.oz_environment_create": "使用 `oz environment create` 定义可复现的执行上下文。", + "terminal.ambient_agent.tips.oz_mcp_list": "使用 `oz mcp list` 查看你的 Agent 可用的 MCP 服务器。", + "terminal.ambient_agent.tips.oz_schedule_create": "使用 `oz schedule create` 设置由 cron 触发的 Agent。", + "terminal.ambient_agent.tips.oz_schedule_pause": "使用 `oz schedule pause` 暂停和恢复计划 Agent,而无需删除它们。", + "terminal.ambient_agent.tips.personal_secrets": "使用个人密钥保存仅应由你的 Agent 使用的凭证。", + "terminal.ambient_agent.tips.programmatic_agents_sdk": "使用 Oz 的 TypeScript 和 Python SDK 构建可编程 Agent。", + "terminal.ambient_agent.tips.python_sdk": "使用 Oz Python SDK 将 Agent 集成到你的数据管道中。", + "terminal.ambient_agent.tips.recurring_cron": "设置按 cron 定期运行的 Agent,用于自动维护。", + "terminal.ambient_agent.tips.remote_dev_boxes": "在远程开发机或 CI runner 上使用 Oz CLI 运行 Agent。", + "terminal.ambient_agent.tips.rest_api_trigger": "调用 Oz REST API,从任何后端服务或内部工具触发 Agent。", + "terminal.ambient_agent.tips.restart_services_alerts": "构建在告警触发时重启服务或扩缩容部署的 Agent。", + "terminal.ambient_agent.tips.reusable_environments": "使用 Docker 镜像创建可复用环境,确保 Agent 执行一致。", + "terminal.ambient_agent.tips.review_prs": "创建自动 review PR 并提出改进建议的 Agent。", + "terminal.ambient_agent.tips.scheduled_feature_flags": "创建一个计划 Agent,每周清理过期 feature flag。", + "terminal.ambient_agent.tips.set_secrets": "使用 `oz secret` 命令为 Agent 设置团队或个人密钥。", + "terminal.ambient_agent.tips.share_flag": "使用 Oz CLI 的 `--share` 标志,从任何地方启用会话共享。", + "terminal.ambient_agent.tips.share_session_links": "与团队共享 Agent 会话链接,用于协同调试。", + "terminal.ambient_agent.tips.slack_integration_trigger": "安装 Oz Slack 集成,以便从任何频道或私信触发 Agent。", + "terminal.ambient_agent.tips.slack_mentions": "创建能在 Slack thread 中响应 @mentions 并使用完整上下文的 Agent。", + "terminal.ambient_agent.tips.team_secrets": "使用团队密钥为所有 Agent 共享基础设施凭证。", + "terminal.ambient_agent.tips.teammates_runs": "在 Oz Web 应用中查看队友的 Agent 运行,获得共享可见性。", + "terminal.ambient_agent.tips.triage_github_issues": "构建可自动分流并标记 GitHub issue 的 Agent。", + "terminal.ambient_agent.tips.typescript_sdk": "使用 Oz TypeScript SDK 构建自定义自动化流水线。", + "terminal.ambient_agent.tips.view_runs_status": "在 Oz Web 应用中查看所有 Agent 运行及其状态。", + "terminal.ambient_agent.tips.webhooks_incidents": "通过 webhook 触发 Agent 以响应生产事故。", + "terminal.ask_ai.default_autosuggestion": "这里发生了什么?", + "terminal.auth_secret.credentials_encrypted": "你的凭证已端到端加密。", + "terminal.auth_secret.delete_secret": "删除密钥", + "terminal.auth_secret.loading": "加载中…", + "terminal.auth_secret.new_secret_type": "新建 {name}", + "terminal.auth_secret.no_matches_helper": "未找到密钥。保存即可直接使用此值,或点击钥匙图标添加密钥。", + "terminal.auth_secret.no_secrets_found": "未找到密钥", + "terminal.auth_secret.search_placeholder": "搜索密钥或创建一个新密钥", + "terminal.auth_secret.share_with_team": "与团队共享", + "terminal.auth_secret.skip_advanced": "跳过(高级)", + "terminal.auth_secret.skip_advanced_label": "仅当你的 key 已在环境中设置时使用(例如作为 Kubernetes secret 注入)", + "terminal.auth_secret.unable_to_load": "无法加载密钥", + "terminal.auto_reload_modal.title": "启用自动充值?", + "terminal.auto_reload_modal.toast.enable_failed": "启用自动充值失败。请尝试在“账单和用量”中更新设置。", + "terminal.auto_reload_modal.toast.team_not_found": "出了点问题,找不到你的团队数据。", + "terminal.auto_reload_modal.toast.updated": "自动充值设置已更新", + "terminal.auto_reload.name": "自动补充", + "terminal.auto_reload.purchase_suffix": "会在额度用完时自动购买你选择的套餐。", + "terminal.auto_reload.when_enabled_prefix": "启用后,", + "terminal.available_shells.custom": "自定义", + "terminal.available_shells.custom_with_command": "自定义({command})", + "terminal.available_shells.custom_with_path": "自定义:{path}", + "terminal.available_shells.docker_sandbox": "Docker 沙盒", + "terminal.available_shells.system_default_shell": "系统默认 shell", + "terminal.available_shells.windows_subsystem_for_linux": "Windows Linux 子系统", + "terminal.binding.accept_prompt_suggestion": "接受提示建议", + "terminal.binding.add_current_folder_as_project": "将当前文件夹添加为项目", + "terminal.binding.alternate_terminal_paste": "备用终端粘贴", + "terminal.binding.ask_warp_ai": "询问 Warp AI", + "terminal.binding.ask_warp_ai_about_last_block": "询问 Warp AI 上一个块", + "terminal.binding.ask_warp_ai_about_selection": "询问 Warp AI 所选内容", + "terminal.binding.attach_selected_block_as_agent_context": "将所选块附加为 Agent 上下文", + "terminal.binding.attach_selected_text_as_agent_context": "将所选文本附加为 Agent 上下文", + "terminal.binding.attach_selection_as_agent_context": "将所选内容附加为 Agent 上下文", + "terminal.binding.bookmark_selected_block": "为所选块添加书签", + "terminal.binding.cancel_process": "取消活动进程", + "terminal.binding.copy_command_and_output": "复制命令和输出", + "terminal.binding.copy_text_or_cancel_process": "复制文本或取消活动进程", + "terminal.binding.executing_command.backward_tabulation": "在执行中的命令里反向制表", + "terminal.binding.executing_command.delete_line_end": "在执行中的命令里删除到行尾", + "terminal.binding.executing_command.delete_line_start": "在执行中的命令里删除到行首", + "terminal.binding.executing_command.delete_word_left": "在执行中的命令里向左删除一个词", + "terminal.binding.executing_command.move_cursor_end": "在执行中的命令里将光标移到行尾", + "terminal.binding.executing_command.move_cursor_home": "在执行中的命令里将光标移到行首", + "terminal.binding.executing_command.move_cursor_word_left": "在执行中的命令里将光标向左移动一个词", + "terminal.binding.executing_command.move_cursor_word_right": "在执行中的命令里将光标向右移动一个词", + "terminal.binding.expand_selected_blocks_above": "向上扩展所选块", + "terminal.binding.expand_selected_blocks_below": "向下扩展所选块", + "terminal.binding.find_in_terminal": "在终端中查找", + "terminal.binding.find_within_selected_block": "在所选块中查找", + "terminal.binding.focus_input": "聚焦终端输入", + "terminal.binding.import_external_settings": "导入外部设置", + "terminal.binding.initiate_project_for_warp": "为 Warp 初始化项目", + "terminal.binding.insert_command_correction": "插入命令修正", + "terminal.binding.load_agent_mode_conversation": "加载 Agent Mode 对话(来自剪贴板中的调试链接)", + "terminal.binding.open_block_context_menu": "打开块上下文菜单", + "terminal.binding.reinput_commands": "重新输入所选命令", + "terminal.binding.reinput_commands_with_sudo": "以 root 身份重新输入所选命令", + "terminal.binding.scroll_down_one_line": "将终端输出向下滚动一行", + "terminal.binding.scroll_down_one_page": "将终端输出向下滚动一页", + "terminal.binding.scroll_to_bottom_of_selected_block": "滚动到所选块底部", + "terminal.binding.scroll_to_top_of_selected_block": "滚动到所选块顶部", + "terminal.binding.scroll_up_one_line": "将终端输出向上滚动一行", + "terminal.binding.scroll_up_one_page": "将终端输出向上滚动一页", + "terminal.binding.select_all_blocks": "选择所有块", + "terminal.binding.select_bookmark_down": "选择下方最近的书签", + "terminal.binding.select_bookmark_up": "选择上方最近的书签", + "terminal.binding.select_next_block": "选择下一个块", + "terminal.binding.select_previous_block": "选择上一个块", + "terminal.binding.set_input_mode_agent": "将输入模式设为 Agent Mode", + "terminal.binding.set_input_mode_terminal": "将输入模式设为终端模式", + "terminal.binding.setup_guide": "设置指南", + "terminal.binding.share_current_session": "共享当前会话", + "terminal.binding.share_selected_block": "共享所选块", + "terminal.binding.stop_sharing_current_session": "停止共享当前会话", + "terminal.binding.toggle_autoexecute_mode": "切换自动执行模式", + "terminal.binding.toggle_block_filter_selected_or_last": "切换所选块或上一个块的过滤器", + "terminal.binding.toggle_cli_agent_rich_input": "切换 CLI Agent 富输入", + "terminal.binding.toggle_conversation_details_panel": "切换对话详情面板", + "terminal.binding.toggle_hide_cli_responses": "切换隐藏 CLI 响应", + "terminal.binding.toggle_queue_next_prompt": "切换队列中的下一个提示", + "terminal.binding.toggle_session_recording": "切换会话 PTY 录制", + "terminal.binding.toggle_sticky_command_header": "切换活动窗格中的粘性命令标题", + "terminal.binding.toggle_team_workflows_modal": "切换团队 workflows 弹窗", + "terminal.binding.write_codebase_index_snapshot": "写入当前代码库索引快照", + "terminal.block_filter.accessibility_help": "按 Escape 退出", + "terminal.block_filter.accessibility_title": "输入搜索短语。", + "terminal.block_filter.case_sensitive_tooltip": "区分大小写搜索", + "terminal.block_filter.context_lines_tooltip": "显示匹配项周围的上下文行", + "terminal.block_filter.invert_tooltip": "反向过滤", + "terminal.block_filter.placeholder": "过滤块输出", + "terminal.block_filter.regex_tooltip": "正则表达式开关", + "terminal.block_list.conversation_restored": "对话已恢复", + "terminal.block_list.previous_session": "上一个会话", + "terminal.block_list.restored_from": "{label}:{date}", + "terminal.block_list.save_as_workflow": "另存为工作流", + "terminal.block_list.save_as_workflow_secrets": "包含密钥的块无法保存。", + "terminal.block_onboarding.agentic.prompt.git_history": "探索 {repo_path} 中的 Git 历史,并给我一份摘要。", + "terminal.block_onboarding.agentic.prompt.matrix": "先检查 {directory} 是否存在,如果不存在就创建这个路径。然后为我的 Warp 终端创建一个 matrix 主题,不要包含 background image 字段,并严格遵循 Warp 网站上的 YAML 结构,不要多字段或少字段。将它命名为 matrix.yaml 并保存到刚才创建的目录中。确认主题正确且可以应用后,只回复:'The matrix theme is now available at '。", + "terminal.block_onboarding.agentic.prompt.matrix.default_directory": "Warp 主题目录。", + "terminal.block_onboarding.agentic.prompt.other": "你能帮我做什么?", + "terminal.block_onboarding.agentic.prompt.snake": "用 python 做一个可以在终端里玩的贪吃蛇游戏。请使用代码工具和请求命令帮我完成。在决定方案前,先确认我已经安装了所有必要依赖。对话结束时,这个应用应该无需额外步骤即可运行。", + "terminal.block_onboarding.agentic.suggestion.git_history.description": "与 Agent Mode 协作,了解 Git 仓库中的近期更改", + "terminal.block_onboarding.agentic.suggestion.git_history.fallback_repo": "我的仓库", + "terminal.block_onboarding.agentic.suggestion.git_history.title": "探索 {repo} 的 Git 历史", + "terminal.block_onboarding.agentic.suggestion.matrix.description": "让你的终端看起来像进入了 Matrix", + "terminal.block_onboarding.agentic.suggestion.matrix.title": "创建 Matrix 风格的自定义主题", + "terminal.block_onboarding.agentic.suggestion.other.description": "与 Agent 协作完成其他任务", + "terminal.block_onboarding.agentic.suggestion.other.title": "想做点别的?", + "terminal.block_onboarding.agentic.suggestion.snake.description": "让 Agent Mode 引导你从头到尾创建一个贪吃蛇游戏", + "terminal.block_onboarding.agentic.suggestion.snake.title": "从零开始用 Python 创建贪吃蛇游戏", + "terminal.block_onboarding.agentic.welcome_body_agent_mode": " Agent Mode 示例", + "terminal.block_onboarding.agentic.welcome_body_prefix": "下面是几个在终端中使用 AI 能力的", + "terminal.block_onboarding.agentic.welcome_title": "欢迎使用 Warp!", + "terminal.block_onboarding.create_team": "创建团队", + "terminal.block_onboarding.drive_sharing.body": "现在你可以在 Warp 或网页上与任何人共享 Drive 对象,无论对方是否是 Warp 用户。点击 Warp Drive 菜单或窗格标题栏中的共享,即可通过链接或电子邮件共享。", + "terminal.block_onboarding.drive_sharing.permissions": "你可以随时修改访问权限。", + "terminal.block_onboarding.drive_sharing.share_object": "共享 {name}", + "terminal.block_onboarding.drive_sharing.share_this_object": "共享此 {type}", + "terminal.block_onboarding.drive_sharing.title": "在 Warp Drive 中共享", + "terminal.block_onboarding.prompt.compatibility_prefix": "Warp 支持许多自定义提示符,例如 oh-my-zsh、Starship、Powerlevel10K。", + "terminal.block_onboarding.prompt.customizable": "可在外观设置中自定义。", + "terminal.block_onboarding.prompt.intro": "接下来,设置你的提示符。Warp 提供自定义提示符构建器,你也可以选择 PS1 以沿用现有的提示符配置。", + "terminal.block_onboarding.prompt.let_us_know": "告诉我们。", + "terminal.block_onboarding.prompt.look_incorrect": "看起来不对?", + "terminal.block_onboarding.prompt.no_existing_prompt": "没有现有提示符。", + "terminal.block_onboarding.prompt.shell_prompt": "Shell 提示符 (PS1)", + "terminal.block_onboarding.prompt.warp_prompt": "Warp 提示符", + "terminal.block.bookmark_tooltip": "为此块添加书签,以便快速滚动到这里", + "terminal.block.completed_at": "\n完成于:{time}", + "terminal.block.jump_to_bottom_tooltip": "跳转到此块底部", + "terminal.block.lock_scroll_at_bottom_tooltip": "将滚动锁定在块底部", + "terminal.block.started_at": "开始于:{time}", + "terminal.block.tag_agent_for_assistance": "标记 Agent 寻求帮助", + "terminal.buy_credits.auto_reload_error": "无法为你的团队启用自动充值。请在“设置 > 账单和使用量”中重试。", + "terminal.buy_credits.auto_reload_tooltip": "启用后,当你的额度余额偏低时,自动充值会购买 {credits} 个额度", + "terminal.buy_credits.buy": "购买", + "terminal.buy_credits.increase_it": "提高限额", + "terminal.buy_credits.monthly_limit.admin_description": "你的月度支出限额已达到。提高限额以继续。", + "terminal.buy_credits.monthly_limit.non_admin_description": "请联系团队管理员提高月度限额。", + "terminal.buy_credits.monthly_limit.title": "已达到月度限额", + "terminal.buy_credits.out_of_credits.admin_description": "向你的账户添加更多额度以继续使用 Oz Agent。", + "terminal.buy_credits.out_of_credits.non_admin_description": "请联系团队管理员购买更多额度以继续。", + "terminal.buy_credits.out_of_credits.title": "额度已用尽", + "terminal.buy_credits.purchase_exceeds_limit_prefix": "购买这些额度会使你超出月度支出限额。", + "terminal.buy_credits.purchase_exceeds_limit_suffix": "以继续。", + "terminal.cli_agent_sessions.waiting_for_answer": "正在等待你的回答", + "terminal.cloud_agent_loading.failed_to_start_environment": "启动环境失败", + "terminal.cloud_agent_loading.github_auth_button": "使用 GitHub 认证", + "terminal.cloud_agent_loading.github_auth_message": "请通过 GitHub 认证以继续", + "terminal.cloud_agent_loading.github_auth_required": "需要 GitHub 认证", + "terminal.cloud_agent_loading.machine_prefix": "你的 Agent 当前正在 {specs} 机器上运行。", + "terminal.cloud_agent_loading.machine_suffix": "以获得更强大的云端 Agent。", + "terminal.cloud_agent_loading.no_environment_started": "未启动云环境", + "terminal.cloud_agent_loading.run_cancelled": "云端 Agent 运行已取消", + "terminal.cloud_agent_setup.desc_prefix": "使用 Oz 云端 Agent 来并行运行 Agent、构建可自主运行的 Agent,并从任何地方查看 Agent 状态。", + "terminal.cloud_agent_setup.free_credit_one": "你有 1 个免费额度可用于 Oz 云端 Agent。", + "terminal.cloud_agent_setup.free_credits": "免费额度", + "terminal.cloud_agent_setup.free_credits_other": "你有 {credits} 个免费额度可用于 Oz 云端 Agent。", + "terminal.cloud_agent_setup.subheading": "云端 Agent 需要在环境中运行以完成任务。在下方创建你的第一个环境。之后你可以编辑该环境,也可以在需要时添加新环境。", + "terminal.cloud_agent_setup.title": "启动新的 Oz 云端 Agent", + "terminal.cloud_agent_setup.visit_docs": "访问文档", + "terminal.context_menu.ai_command_search": "AI 命令搜索", + "terminal.context_menu.clear_blocks": "清除块", + "terminal.context_menu.command_search": "命令搜索", + "terminal.context_menu.copy_command": "复制命令", + "terminal.context_menu.copy_commands": "复制命令", + "terminal.context_menu.copy_conversation_id": "复制对话 ID", + "terminal.context_menu.copy_conversation_text": "复制对话文本", + "terminal.context_menu.copy_debugging_id": "复制调试 ID", + "terminal.context_menu.copy_debugging_link": "复制调试链接", + "terminal.context_menu.copy_filtered_output": "复制过滤后的输出", + "terminal.context_menu.copy_git_branch": "复制 Git 分支", + "terminal.context_menu.copy_output": "复制输出", + "terminal.context_menu.copy_output_as_markdown": "复制输出为 Markdown", + "terminal.context_menu.copy_prompt": "复制提示词", + "terminal.context_menu.copy_right_prompt": "复制右侧提示符", + "terminal.context_menu.copy_share_link": "复制共享链接", + "terminal.context_menu.copy_url": "复制 URL", + "terminal.context_menu.copy_working_directory": "复制工作目录", + "terminal.context_menu.edit_agent_toolbelt": "编辑 agent 工具带", + "terminal.context_menu.edit_cli_agent_toolbelt": "编辑 CLI agent 工具带", + "terminal.context_menu.edit_prompt": "编辑提示词", + "terminal.context_menu.find_within_block": "在块内查找", + "terminal.context_menu.find_within_blocks": "在多个块内查找", + "terminal.context_menu.fork": "派生", + "terminal.context_menu.fork_from_here": "从这里派生", + "terminal.context_menu.fork_from_here_dev": "从这里派生(仅开发)", + "terminal.context_menu.fork_from_last_query": "从上一个查询派生", + "terminal.context_menu.fork_from_query": "从“{query}”派生", + "terminal.context_menu.hide_input_hint_text": "隐藏输入提示文本", + "terminal.context_menu.insert_into_input": "插入到输入框", + "terminal.context_menu.open_in_warp": "在 Warp 中打开", + "terminal.context_menu.rewind_to_before_here": "倒回到这里之前", + "terminal.context_menu.save_as_prompt": "另存为提示词", + "terminal.context_menu.save_as_workflow": "另存为 workflow", + "terminal.context_menu.scroll_to_bottom_of_block": "滚动到块底部", + "terminal.context_menu.scroll_to_bottom_of_blocks": "滚动到多个块底部", + "terminal.context_menu.scroll_to_top_of_block": "滚动到块顶部", + "terminal.context_menu.scroll_to_top_of_blocks": "滚动到多个块顶部", + "terminal.context_menu.share_block_ellipsis": "分享块...", + "terminal.context_menu.share_conversation": "分享对话", + "terminal.context_menu.share_ellipsis": "分享...", + "terminal.context_menu.show_input_hint_text": "显示输入提示文本", + "terminal.context_menu.toggle_block_filter": "切换块过滤器", + "terminal.context_menu.toggle_bookmark": "切换书签", + "terminal.conversation_details.hide": "隐藏详情", + "terminal.conversation_details.show": "显示详情", + "terminal.harness_selector.disabled_by_admin": "已被管理员禁用", + "terminal.init_environment.cancelled": "环境设置已取消", + "terminal.init_environment.create_using_current_dir": "使用当前工作目录作为仓库创建环境", + "terminal.init_environment.create_using_supplied_repos": "使用提供的仓库创建环境:{repos}", + "terminal.init_environment.create_without_repos": "不使用任何仓库创建环境", + "terminal.init_environment.explanation": "是否要为此项目创建一个环境,以便在其中运行云端 Agent?Agent 会引导你选择 GitHub 仓库、配置 Docker 镜像并指定启动命令。", + "terminal.init_environment.mode.quick_setup.description": "选择你想使用的 GitHub 仓库,我们会建议基础镜像和配置", + "terminal.init_environment.mode.quick_setup.title": "快速设置", + "terminal.init_environment.mode.title": "选择环境设置方式", + "terminal.init_environment.mode.use_agent.description": "选择一个本地已设置的项目,我们会帮助你基于它设置环境", + "terminal.init_environment.mode.use_agent.title": "使用 Agent", + "terminal.init_environment.no_repos_help": "如果你想创建包含仓库的环境,请重新运行此命令,并将文件路径或 GitHub 链接作为参数传入,例如:\"/create-environment \"。", + "terminal.init_project.codebase.cancelled": "代码库索引已取消", + "terminal.init_project.codebase.index_button": "是的,索引此代码库。", + "terminal.init_project.codebase.prompt": "是否允许 Agent 索引此代码库?这会让帮助更高效、更贴合当前项目。", + "terminal.init_project.codebase.started": "代码库索引已开始", + "terminal.init_project.codebase.view_status": "查看索引状态", + "terminal.init_project.environment.create_button": "创建环境", + "terminal.init_project.environment.created": "环境已创建", + "terminal.init_project.environment.creating": "正在创建环境...", + "terminal.init_project.environment.prompt": "是否要为此项目创建一个环境,以便在其中运行云端 Agent?Agent 会引导你选择 GitHub 仓库、配置 Docker 镜像并指定启动命令。", + "terminal.init_project.environment.skipped": "已跳过环境创建", + "terminal.init_project.lsp.enable_language_support": "启用语言支持", + "terminal.init_project.lsp.enable_prefix": "启用 ", + "terminal.init_project.lsp.enable_suffix": " 支持", + "terminal.init_project.lsp.enabled": "语言支持已启用", + "terminal.init_project.lsp.install_and_enable": "安装并启用", + "terminal.init_project.lsp.install_and_enable_prefix": "安装并启用 ", + "terminal.init_project.lsp.multiple_prompt": "是否为此代码库启用可用的语言支持?这会提供更智能的代码导航和行内错误检查。", + "terminal.init_project.lsp.single_enabled_prefix": "", + "terminal.init_project.lsp.single_enabled_suffix": " 语言支持已启用", + "terminal.init_project.lsp.single_prompt_prefix": "是否为此代码库启用 ", + "terminal.init_project.lsp.single_prompt_suffix": " 支持?这会提供更智能的代码导航、行内错误检查等能力。", + "terminal.init_project.lsp.skipped": "已跳过语言支持", + "terminal.init_project.lsp.started_install": "已开始安装语言支持", + "terminal.init_project.project_rules.already_configured": "项目规则已配置", + "terminal.init_project.project_rules.configured": "项目规则已配置", + "terminal.init_project.project_rules.generate_button": "生成 AGENTS.md 文件", + "terminal.init_project.project_rules.generating": "正在生成 AGENTS.md...", + "terminal.init_project.project_rules.link_existing_prefix": "将现有 ", + "terminal.init_project.project_rules.link_existing_suffix": " 链接到我的 AGENTS.md 文件", + "terminal.init_project.project_rules.linked_from_prefix": "项目规则已链接自 ", + "terminal.init_project.project_rules.prompt": "是否要创建 AGENTS.md 文件?Warp 可以根据你的代码库推断项目规则、上下文和约定,并为你生成该文件。Agent 编码时会使用这些上下文。", + "terminal.init_project.project_rules.regenerate_button": "重新生成 AGENTS.md 文件", + "terminal.init_project.project_rules.skip_generation_button": "暂不生成 AGENTS.md", + "terminal.init_project.project_rules.skipped": "已跳过项目规则", + "terminal.init_project.skip_for_now": "暂时跳过", + "terminal.init_project.toast.lsp_install_failed_prefix": "安装 ", + "terminal.init_project.toast.lsp_install_failed_suffix": " 失败:", + "terminal.init_project.toast.lsp_install_success_suffix": " 已安装并成功启用。", + "terminal.init_project.toast.lsp_installing_prefix": "正在后台安装 ", + "terminal.init_project.toast.lsp_installing_suffix": "...", + "terminal.init_project.welcome.already_setup": "看起来此项目已经初始化。你可以点击下方按钮,为此代码库重新生成 AGENTS.md。", + "terminal.init_project.welcome.onboarding": "很好,我们开始设置这个项目!是否允许我索引此代码库?这样我可以快速理解上下文,并在处理此代码库时提供更有针对性的解决方案。代码不会存储在 Warp 服务器上。", + "terminal.inline_banner.agent_mode_setup.body": "让 Agent 理解你的代码库并为其生成规则,以获得更智能、更一致的回复。你也可以随时运行 /init 来完成此操作", + "terminal.inline_banner.agent_mode_setup.optimize": "优化", + "terminal.inline_banner.agent_mode_setup.title": "要为此代码库优化 Warp 吗?", + "terminal.inline_banner.alias_expansion.enable": "启用 alias 展开", + "terminal.inline_banner.alias_expansion.title": "Warp 可以自动展开 aliases。", + "terminal.inline_banner.anonymous_user_ai_sign_up.content": "未登录用户无法使用 AI 功能。创建账号即可使用 AI。", + "terminal.inline_banner.anonymous_user_ai_sign_up.title": "登录以使用 AI", + "terminal.inline_banner.aws_bedrock_enabled": "你的 Warp 管理员已为团队启用 AWS Bedrock。", + "terminal.inline_banner.aws_bedrock.log_in": "登录 AWS", + "terminal.inline_banner.aws_bedrock.title": "使用 AWS Bedrock?", + "terminal.inline_banner.aws_cli_not_installed.title": "未安装 AWS CLI", + "terminal.inline_banner.aws_cli_required": "需要 AWS CLI 才能与你组织的 AWS Bedrock 认证。请安装后继续。", + "terminal.inline_banner.init_script_output_visible": "上方显示了 Warp 初始化脚本的输出,以便辅助调试。", + "terminal.inline_banner.notifications.a11y_enable_through_command_palette": "你可以通过命令面板启用通知。", + "terminal.inline_banner.notifications.agent_task_completed": "Agent 完成响应时,Warp 可以通知你。", + "terminal.inline_banner.notifications.allow_permissions": "别忘了点击“允许”,以完成通知设置。", + "terminal.inline_banner.notifications.configure": "配置通知", + "terminal.inline_banner.notifications.disabled": "通知已关闭,但你随时可以前往设置启用通知。", + "terminal.inline_banner.notifications.dismissed": "我们不会再次显示此横幅,但你随时可以前往设置启用通知。", + "terminal.inline_banner.notifications.long_running_command": "长时间运行的命令完成时,Warp 可以通知你。", + "terminal.inline_banner.notifications.needs_attention": "命令或 Agent 需要你关注时,Warp 可以通知你。", + "terminal.inline_banner.notifications.password_prompt": "系统提示你输入密码时,Warp 可以通知你。", + "terminal.inline_banner.notifications.permissions_denied": "Warp 的通知发送权限被拒绝。", + "terminal.inline_banner.notifications.permissions_error": "请求权限时出了点问题。", + "terminal.inline_banner.notifications.set_permissions": "设置权限", + "terminal.inline_banner.notifications.success": "成功!你现在可以接收桌面通知了。", + "terminal.inline_banner.prompt_suggestions.out_of_credits_tooltip": "额度已用完", + "terminal.inline_banner.prompt_suggestions.payment_issue_tooltip": "因付款问题受限", + "terminal.inline_banner.prompt_suggestions.query.code": "帮我写一些代码。我需要向你提供哪些信息?", + "terminal.inline_banner.prompt_suggestions.query.deploy": "帮我部署我的项目。我需要向你提供哪些信息?", + "terminal.inline_banner.prompt_suggestions.query.explain": "帮我解释一下这段内容。", + "terminal.inline_banner.prompt_suggestions.query.fix": "帮我修复这个问题。", + "terminal.inline_banner.prompt_suggestions.query.install": "帮我安装一个二进制文件或依赖。我需要向你提供哪些信息?", + "terminal.inline_banner.prompt_suggestions.query.something_else": "想做点别的?", + "terminal.inline_banner.shared_session.environment_ended": "环境已结束", + "terminal.inline_banner.shared_session.environment_started": "环境已启动", + "terminal.inline_banner.shared_session.remote_control_active": "远程控制进行中", + "terminal.inline_banner.shared_session.remote_control_stopped": "远程控制已停止", + "terminal.inline_banner.shared_session.sharing_ended": "共享已结束", + "terminal.inline_banner.shared_session.sharing_started": "共享已开始", + "terminal.inline_banner.shared_session.today": "今天", + "terminal.inline_banner.shell_process_exited": "Shell 进程已退出", + "terminal.inline_banner.shell_process_exited_prematurely": "Shell 进程提前退出!", + "terminal.inline_banner.ssh_wrapper.disabled": "Warp SSH wrapper 已禁用", + "terminal.inline_banner.ssh_wrapper.enabled": "Warp SSH wrapper 已启用", + "terminal.inline_banner.vim_mode.title": "启用 Warp 的 Vim 快捷键?", + "terminal.inline_history.header": "历史记录", + "terminal.inline_history.tab.all": "全部", + "terminal.inline_history.tab.commands": "命令", + "terminal.inline_history.tab.prompts": "提示词", + "terminal.input_mode.agent_mode": "Agent 模式", + "terminal.input_mode.shortcut_or": "{keybinding} 或 {prefix}", + "terminal.input_mode.terminal": "终端", + "terminal.input.a11y_helper": "输入 shell 命令,按 Enter 执行。按 Cmd-Up 导航到之前执行命令的输出。按 Cmd-L 重新聚焦命令输入框。", + "terminal.input.a11y_label": "命令输入。", + "terminal.input.a11y.ai_prompt": "AI 提示词:{query}", + "terminal.input.a11y.command": "命令:{command}", + "terminal.input.a11y.conversation": "对话:{title}", + "terminal.input.a11y.disabled_suffix": "(已禁用)", + "terminal.input.a11y.indexed_repository": "已索引仓库:{repository}", + "terminal.input.a11y.model": "模型:{model}", + "terminal.input.a11y.plan": "计划:{title}", + "terminal.input.a11y.profile": "配置:{profile}", + "terminal.input.a11y.prompt": "提示词:{prompt}", + "terminal.input.a11y.query": "查询:{query}", + "terminal.input.a11y.selected_suffix": "(已选择)", + "terminal.input.a11y.skill": "技能:{skill}", + "terminal.input.agent_hint.build_data_pipeline": "让 Warp 处理任何任务,例如构建数据管道,处理 CSV 文件并加载到 BigQuery", + "terminal.input.agent_hint.build_rest_api": "让 Warp 处理任何任务,例如使用 FastAPI 为我的移动应用构建 REST API", + "terminal.input.agent_hint.create_backup_script": "让 Warp 处理任何任务,例如为我的 PostgreSQL 数据库创建备份脚本并设置计划任务", + "terminal.input.agent_hint.create_unit_tests": "让 Warp 处理任何任务,例如为我的认证服务创建单元测试", + "terminal.input.agent_hint.debug_python_ci": "让 Warp 处理任何任务,例如帮我调试 Python 测试在 CI 中失败的原因", + "terminal.input.agent_hint.deploy_react": "让 Warp 处理任何任务,例如将我的 React 应用部署到 Vercel 并设置环境变量", + "terminal.input.agent_hint.fix_memory_leak": "让 Warp 处理任何任务,例如查找并修复我的 Node.js 应用中的内存泄漏", + "terminal.input.agent_hint.github_actions_deploy": "让 Warp 处理任何任务,例如创建 GitHub Actions 工作流,在合并时自动部署", + "terminal.input.agent_hint.implement_oauth": "让 Warp 处理任何任务,例如帮我在 Express.js 应用中实现 OAuth2 认证", + "terminal.input.agent_hint.migrate_database": "让 Warp 处理任何任务,例如帮我将数据从 MySQL 迁移到 PostgreSQL", + "terminal.input.agent_hint.optimize_docker": "让 Warp 处理任何任务,例如优化 Docker 镜像以减少构建时间和体积", + "terminal.input.agent_hint.optimize_sql": "让 Warp 处理任何任务,例如帮我优化运行缓慢的 SQL 查询", + "terminal.input.agent_hint.refactor_legacy": "让 Warp 处理任何任务,例如帮我将旧代码重构为现代设计模式", + "terminal.input.agent_hint.setup_ab_testing": "让 Warp 处理任何任务,例如为我的 Web 应用设置 A/B 测试基础设施", + "terminal.input.agent_hint.setup_log_aggregation": "让 Warp 处理任何任务,例如为我的分布式系统设置 ELK 日志聚合", + "terminal.input.agent_hint.setup_microservice": "让 Warp 处理任何任务,例如用 Docker 设置新的微服务并创建部署流水线", + "terminal.input.agent_hint.setup_monitoring": "让 Warp 处理任何任务,例如为我的 AWS 基础设施设置监控和告警", + "terminal.input.agent_hint.setup_redis": "让 Warp 处理任何任务,例如为我的 Web 应用设置 Redis 缓存", + "terminal.input.agent_hint.setup_ssl": "让 Warp 处理任何任务,例如为我的域名设置 SSL 证书并配置 HTTPS", + "terminal.input.agent_hint.troubleshoot_kubernetes": "让 Warp 处理任何任务,例如帮我排查 Kubernetes pod 持续崩溃的原因", + "terminal.input.attached_images_removed_model_no_images": "已移除附加图片,所选模型不支持图片。", + "terminal.input.cannot_run_command_already_running": "无法运行 `{command}`(已有命令正在运行)。", + "terminal.input.cannot_start_while_monitoring": "agent 正在监控命令时无法开始新对话。", + "terminal.input.cloud_handoff_prepare_failed": "准备云端交接失败:{error}", + "terminal.input.conversations.tab.all": "全部", + "terminal.input.conversations.tab.current_directory": "当前目录", + "terminal.input.dynamic_enum.failure": "命令失败", + "terminal.input.dynamic_enum.generate": "运行以下命令以生成选项:", + "terminal.input.dynamic_enum.no_results": "命令未返回结果", + "terminal.input.dynamic_enum.pending": "命令等待中...", + "terminal.input.dynamic_enum.run": "运行命令", + "terminal.input.executed_command": "已执行:{command}", + "terminal.input.export.directory_not_found": "未找到目录:{path}", + "terminal.input.export.failed": "导出到 {path} 失败:{error}", + "terminal.input.export.file_already_exists": "文件 {path} 已存在", + "terminal.input.export.no_active_conversation": "没有可导出的活动对话", + "terminal.input.export.permission_denied": "没有写入 {path} 的权限。请检查文件权限。", + "terminal.input.export.success": "对话已导出到 {path}", + "terminal.input.export.will_overwrite": "文件 {path} 已存在,将被覆盖", + "terminal.input.hint.ai_command_search": "输入 '#' 获取 AI 命令建议", + "terminal.input.hint.ask_follow_up": "追问", + "terminal.input.hint.ask_follow_up_classic": "追问,或按 Backspace 退出", + "terminal.input.hint.cli_agent_rich_input": "告诉 agent 要构建什么...", + "terminal.input.hint.cloud_agent": "启动云端 agent", + "terminal.input.hint.enter_prompt_for_agent": "输入给 {agent} 的提示词...", + "terminal.input.hint.hand_off_to": "交接给 {name}", + "terminal.input.hint.handoff_to_cloud": "交接到云端", + "terminal.input.hint.run_commands": "运行命令", + "terminal.input.hint.steer_running_agent": "引导正在运行的 agent", + "terminal.input.hint.steer_running_agent_classic": "引导正在运行的 agent,或按 Backspace 退出", + "terminal.input.image_limit.per_conversation": "每个对话", + "terminal.input.image_limit.per_query": "每次查询", + "terminal.input.images_not_attached.one": "有 1 张图片未附加,限制为{limit_name} {limit_value} 张图片。", + "terminal.input.images_not_attached.other": "有 {count} 张图片未附加,限制为{limit_name} {limit_value} 张图片。", + "terminal.input.images_removed.one": "已移除 1 张图片,每个对话限制为 {limit} 张。", + "terminal.input.images_removed.other": "已移除 {count} 张图片,每个对话限制为 {limit} 张。", + "terminal.input.inline_menu.history": "历史", + "terminal.input.message_bar.attached_context.one_other_command": "已将 `{command}` 和另外 1 条命令附加为上下文", + "terminal.input.message_bar.attached_context.other_commands": "已将 `{command}` 和另外 {count} 条命令附加为上下文", + "terminal.input.message_bar.attached_context.selected_text": "已将所选文本附加为上下文", + "terminal.input.message_bar.attached_context.single": "已将 `{command}` 附加为上下文", + "terminal.input.models.base_agent_warning": "你正在使用基础 Agent。完整终端使用模型只适用于完整终端使用 Agent。", + "terminal.input.models.discount_off": "{percent}% 折扣!", + "terminal.input.models.full_terminal_use_warning": "你正在使用完整终端使用 Agent。基础模型只适用于基础 Agent。", + "terminal.input.no_agent_harnesses_available": "没有可用的 agent harness。请联系团队管理员。", + "terminal.input.no_results": "无结果", + "terminal.input.placeholder.search_commands": "搜索命令", + "terminal.input.placeholder.search_conversations": "搜索对话", + "terminal.input.placeholder.search_indexed_repos": "搜索已索引仓库", + "terminal.input.placeholder.search_models": "搜索模型", + "terminal.input.placeholder.search_plans": "搜索计划", + "terminal.input.placeholder.search_profiles": "搜索配置", + "terminal.input.placeholder.search_prompts": "搜索提示词", + "terminal.input.placeholder.search_queries": "搜索查询", + "terminal.input.placeholder.search_queries_to_rewind": "搜索要倒回到的查询", + "terminal.input.placeholder.search_skills": "搜索技能", + "terminal.input.preparing_handoff": "正在准备交接,请稍后重试。", + "terminal.input.read_only_viewer_cannot_send": "只读查看者无法发送查询。", + "terminal.input.rewind.action": "倒回", + "terminal.input.rewind.current_state": "当前状态(不倒回)", + "terminal.input.rewind.no_code_changes": "倒回到:{query}(无代码更改)", + "terminal.input.rewind.with_changes": "倒回到:{query}(+{added} -{removed})", + "terminal.input.slash_commands.section.commands": "命令", + "terminal.input.slash_commands.section.prompts": "提示词", + "terminal.input.slash_commands.section.skills": "技能", + "terminal.input.slash_commands.show_more": "再显示 {count} 个", + "terminal.input.too_many_attachments": "此对话的附件过多。", + "terminal.input.untitled_conversation": "未命名对话", + "terminal.input.voice.listening": "正在聆听...", + "terminal.input.voice.transcribing": "正在转写...", + "terminal.input.workflow.command_inserted": "已插入 workflow 命令 {command}。", + "terminal.input.workflow.select_next_argument_helper": "按 Shift-Tab 选择下一个 workflow 参数", + "terminal.input.workflow.selected_argument": "已选择 workflow 参数 {argument}", + "terminal.link_detection.open_file": "打开文件", + "terminal.link_detection.open_folder": "打开文件夹", + "terminal.link_detection.open_link": "打开链接", + "terminal.loading_prompt": "正在加载提示符...", + "terminal.loading_session": "正在加载会话...", + "terminal.loading.starting_shell_named": "正在启动 {shell}...", + "terminal.message_bar.again_to_send_to_agent": "再次发送给 agent", + "terminal.message_bar.agent_for_new_conversation": "/agent 开始新对话", + "terminal.message_bar.autodetected": "(自动检测)", + "terminal.message_bar.block_and_many_attached": ",已附加 `{command}` 和另外 {count} 个命令", + "terminal.message_bar.block_and_one_attached": ",已附加 `{command}` 和另外 1 个命令", + "terminal.message_bar.block_attached": ",已附加 `{command}`", + "terminal.message_bar.current_pane": " 当前窗格", + "terminal.message_bar.for_code_review": "用于代码审查", + "terminal.message_bar.new_agent_conversation": " 新建 /agent 对话", + "terminal.message_bar.new_conversation": " 新建对话", + "terminal.message_bar.new_pane": " 新窗格", + "terminal.message_bar.no_skills_found": "未找到技能", + "terminal.message_bar.open_conversation": "打开对话", + "terminal.message_bar.open_plan": " 打开计划", + "terminal.message_bar.plan_with_agent": " 让 agent 规划", + "terminal.message_bar.select_and_save_to_profile": " 选择并保存到配置", + "terminal.message_bar.text_selection_attached": ",已附加文本选择", + "terminal.message_bar.to_continue_conversation": " 继续对话", + "terminal.message_bar.to_cycle_tabs": " 切换标签页", + "terminal.message_bar.to_dismiss": " 关闭", + "terminal.message_bar.to_execute": " 执行", + "terminal.message_bar.to_fork_and_continue": "fork 并继续", + "terminal.message_bar.to_hide_help": "隐藏帮助", + "terminal.message_bar.to_hide_plan": "隐藏计划", + "terminal.message_bar.to_navigate": " 导航", + "terminal.message_bar.to_open": " 打开“{title}”", + "terminal.message_bar.to_override": " 覆盖", + "terminal.message_bar.to_remove": " 移除", + "terminal.message_bar.to_resume_conversation": "恢复对话", + "terminal.message_bar.to_select": " 选择", + "terminal.message_bar.to_send": " 发送", + "terminal.message_bar.to_view_plan": "查看计划", + "terminal.message_bar.to_view_plans": "查看计划", + "terminal.model_selector.manage_defaults": "管理默认值", + "terminal.model_selector.tab.base": "基础", + "terminal.model_selector.tab.full_terminal_use": "完整终端使用", + "terminal.model_specs.auto_bedrock_tooltip": "当 Auto 选择的模型支持 Bedrock 时,Warp 会使用 Bedrock;否则可能使用 Warp 托管推理。", + "terminal.model_specs.cost": "成本", + "terminal.model_specs.description": "Warp 对模型在 harness 中表现、积分消耗速率和任务速度的基准评估。", + "terminal.model_specs.inference_may_use_bedrock": "推理可能使用 Bedrock", + "terminal.model_specs.inference_via_api_key": "通过 API key 推理", + "terminal.model_specs.inference_via_bedrock": "通过 Bedrock 推理", + "terminal.model_specs.intelligence": "智能", + "terminal.model_specs.reasoning_level_description": "更高的 reasoning level 会消耗更多积分并带来更高延迟,但在复杂任务上表现更好。", + "terminal.model_specs.reasoning_level_title": "Reasoning level", + "terminal.model_specs.speed": "速度", + "terminal.model_specs.title": "模型规格", + "terminal.notification.agent_failed_suffix": " 已失败", + "terminal.notification.agent_finished_suffix": " 已完成", + "terminal.notification.command_waiting_for_password": "命令正在等待密码", + "terminal.notification.error_prefix": "错误:", + "terminal.notification.error_sending": "发送通知时出错", + "terminal.notification.latest_output_prefix": "最新输出:", + "terminal.notification.long_running_failed_suffix": " 在 {duration}s 后失败", + "terminal.notification.long_running_finished_suffix": " 在 {duration}s 后完成", + "terminal.notification.needs_attention_suffix": " 被阻止", + "terminal.notification.password_prompt_suffix": " 正在等待密码", + "terminal.notification.permissions_help": "请确认已在系统偏好设置中允许 Warp 发送通知。", + "terminal.notification.title": "通知", + "terminal.notification.unknown_error_occurred": "发生未知错误", + "terminal.onboarding.thinking": "思考中...", + "terminal.open_in_warp.a11y.close_banner": "关闭“在 Warp 中查看”横幅", + "terminal.open_in_warp.a11y.learn_more_help": "了解更多关于在 Warp 中打开 Markdown 文件的信息", + "terminal.open_in_warp.a11y.open": "在 Warp 中打开 {path}", + "terminal.open_in_warp.code_language_title": "你知道 Warp 可以直接编辑 {language} 文件吗?", + "terminal.open_in_warp.code_title": "你知道 Warp 可以直接编辑代码吗?", + "terminal.open_in_warp.edit_in_warp": "在 Warp 中编辑", + "terminal.open_in_warp.markdown_title": "你知道 Warp 可以直接显示 Markdown 文件吗?", + "terminal.open_in_warp.view_in_warp": "在 Warp 中查看", + "terminal.plugin_instructions.claude.error.platform_install_no_effect": "平台插件安装未生效", + "terminal.plugin_instructions.claude.error.platform_update_no_effect": "平台插件更新未生效", + "terminal.plugin_instructions.claude.error.update_no_effect": "插件更新未生效", + "terminal.plugin_instructions.claude.install.note.known_issues": "Claude Code 的插件系统存在一些已知问题。如果第 1 步后找不到插件,可以尝试手动向 ~/.claude/settings.json 添加 \"extraKnownMarketplaces\" 条目。", + "terminal.plugin_instructions.claude.install.note.restart": "重启 Claude Code 以激活插件。", + "terminal.plugin_instructions.claude.install.step.add_marketplace": "添加 Warp 插件市场仓库", + "terminal.plugin_instructions.claude.install.step.install_plugin": "安装 Warp 插件", + "terminal.plugin_instructions.claude.install.subtitle": "确认你的机器已安装 jq,然后运行以下命令。", + "terminal.plugin_instructions.claude.install.title": "为 Claude Code 安装 Warp 插件", + "terminal.plugin_instructions.claude.success.installed_reload": "Warp 插件已安装。请运行 /reload-plugins 以激活。", + "terminal.plugin_instructions.claude.success.updated_reload": "Warp 插件已更新。请运行 /reload-plugins 以激活。", + "terminal.plugin_instructions.claude.update.note.restart": "重启 Claude Code 以激活更新。", + "terminal.plugin_instructions.claude.update.step.install_latest": "安装最新插件版本", + "terminal.plugin_instructions.claude.update.step.readd_marketplace": "重新添加插件市场", + "terminal.plugin_instructions.claude.update.step.remove_marketplace": "移除现有插件市场(如果存在)", + "terminal.plugin_instructions.claude.update.title": "更新 Claude Code 的 Warp 插件", + "terminal.plugin_instructions.codex.install.note.restart": "重启 Codex 以应用更改。", + "terminal.plugin_instructions.codex.install.step.config": "在 Codex 配置中将通知条件设置为 \"always\"。打开或创建 ~/.codex/config.toml 并添加:", + "terminal.plugin_instructions.codex.install.step.update": "将 Codex 更新到最新版本。", + "terminal.plugin_instructions.codex.install.subtitle": "将 Codex 更新到最新版本,然后启用焦点内通知,让 Warp 可以在你工作时显示通知。", + "terminal.plugin_instructions.codex.install.title": "为 Codex 启用 Warp 通知", + "terminal.plugin_instructions.enable_agent_notifications": "启用 {agent} 通知", + "terminal.plugin_instructions.enable_notifications": "启用通知", + "terminal.plugin_instructions.enable_tooltip": "安装 Warp 插件,在 Warp 中启用富 Agent 通知", + "terminal.plugin_instructions.error.auto_install_not_supported": "此 Agent 不支持自动安装", + "terminal.plugin_instructions.error.auto_update_not_supported": "此 Agent 不支持自动更新", + "terminal.plugin_instructions.error.command_failed": "'{command}' 执行失败", + "terminal.plugin_instructions.error.command_run_failed": "运行 '{command}' 失败", + "terminal.plugin_instructions.error.home_directory_not_found": "无法确定主目录", + "terminal.plugin_instructions.error.no_plugin_manager": "没有可用的插件管理器", + "terminal.plugin_instructions.gemini.error.update_no_effect": "插件更新未生效", + "terminal.plugin_instructions.gemini.install.note.restart": "重启 Gemini CLI 以激活插件。", + "terminal.plugin_instructions.gemini.install.step.install_extension": "安装 Warp 扩展", + "terminal.plugin_instructions.gemini.install.subtitle": "运行以下命令,然后重启 Gemini CLI。", + "terminal.plugin_instructions.gemini.install.title": "为 Gemini CLI 安装 Warp 插件", + "terminal.plugin_instructions.gemini.success.installed_restart": "Warp 插件已安装。请重启 Gemini CLI 以激活。", + "terminal.plugin_instructions.gemini.success.updated_restart": "Warp 插件已更新。请重启 Gemini CLI 以激活。", + "terminal.plugin_instructions.gemini.update.note.restart": "重启 Gemini CLI 以激活更新。", + "terminal.plugin_instructions.gemini.update.step.update_extension": "更新 Warp 扩展", + "terminal.plugin_instructions.gemini.update.title": "更新 Gemini CLI 的 Warp 插件", + "terminal.plugin_instructions.install_failed": "安装 Warp 插件失败", + "terminal.plugin_instructions.install_instructions_button": "通知设置说明", + "terminal.plugin_instructions.install_instructions_tooltip": "查看 Warp 插件安装说明", + "terminal.plugin_instructions.installing": "正在安装 Warp 插件...", + "terminal.plugin_instructions.learn_more": "了解更多", + "terminal.plugin_instructions.opencode.install.note.restart": "重启 OpenCode 以激活插件。", + "terminal.plugin_instructions.opencode.install.step.add_plugin": "将 \"@warp-dot-dev/opencode-warp\" 添加到顶层 JSON 对象的 \"plugin\" 数组中:", + "terminal.plugin_instructions.opencode.install.subtitle": "将 Warp 插件添加到 OpenCode 配置,然后重启 OpenCode。", + "terminal.plugin_instructions.opencode.install.title": "为 OpenCode 安装 Warp 插件", + "terminal.plugin_instructions.opencode.step.open_config": "打开或创建 opencode.json。它可以位于项目根目录,也可以位于全局配置路径:", + "terminal.plugin_instructions.opencode.update.note.restart": "重启 OpenCode 以加载更新后的插件。", + "terminal.plugin_instructions.opencode.update.step.replace_plugin": "将 \"plugin\" 数组中现有的 \"@warp-dot-dev/opencode-warp\" 条目替换为显式版本:", + "terminal.plugin_instructions.opencode.update.subtitle": "在 opencode.json 中将插件固定到最新版本。OpenCode 会按版本规格缓存插件,因此更改固定版本会强制它在重启后重新获取。", + "terminal.plugin_instructions.opencode.update.title": "更新 OpenCode 的 Warp 插件", + "terminal.plugin_instructions.remote_suffix": "请确保在远程机器上运行这些命令。", + "terminal.plugin_instructions.run_commands": "运行以下命令。", + "terminal.plugin_instructions.see_logs": "查看日志详情", + "terminal.plugin_instructions.success.installed_restart_session": "Warp 插件已安装。请重启会话以激活。", + "terminal.plugin_instructions.success.updated_restart_session": "Warp 插件已更新。请重启会话以激活。", + "terminal.plugin_instructions.update_button": "更新 Warp 插件", + "terminal.plugin_instructions.update_failed": "更新 Warp 插件失败", + "terminal.plugin_instructions.update_instructions_button": "插件更新说明", + "terminal.plugin_instructions.update_instructions_tooltip": "查看 Warp 插件更新说明", + "terminal.plugin_instructions.update_tooltip": "有新的 Warp 插件版本可用", + "terminal.plugin_instructions.updating": "正在更新 Warp 插件...", + "terminal.profile_model_selector.auto_mode_description": "自动模式会为任务选择最佳模型。Cost-efficiency 会优化成本,Responsiveness 会优化响应速度。", + "terminal.profile_model_selector.auto_mode_title": "自动模式", + "terminal.profile_model_selector.auto_select_best_model": "自动选择最适合任务的模型", + "terminal.profile_model_selector.manage_api_keys": "管理 API 密钥", + "terminal.profile_model_selector.model_locked_followup_tooltip": "后续追问会使用原始运行的模型", + "terminal.profile_model_selector.model_requires_edit_access_tooltip": "请求编辑权限以更改模型", + "terminal.profile_model_selector.model_tooltip": "选择 Agent 模型", + "terminal.profile_model_selector.new_models_available": "有新模型可用", + "terminal.profile_model_selector.profile_tooltip": "选择 AI 执行配置", + "terminal.profile_model_selector.reasoning_level_description": "更高的推理等级会消耗更多额度、延迟更高,但在复杂任务上表现更好。", + "terminal.profile_model_selector.reasoning_level_title": "推理等级", + "terminal.profile_selector.billed_to_api": "按 API 计费", + "terminal.profile_selector.custom_models": "自定义模型", + "terminal.profile_selector.manage_profiles": "管理配置", + "terminal.profile_selector.profiles": "配置", + "terminal.project_setup.title": "项目设置", + "terminal.prompt_suggestion.execute_plan": "执行此计划", + "terminal.queued_prompts.delete": "删除排队提示词", + "terminal.queued_prompts.edit": "编辑排队提示词", + "terminal.queued_prompts.header": "{count} 个排队中", + "terminal.recorder.started": "PTY 录制已开始:{path}", + "terminal.recorder.stopped": "PTY 录制已停止:{path}", + "terminal.remote_server.loading.checking": "检查中...", + "terminal.remote_server.loading.initializing": "初始化中...", + "terminal.remote_server.loading.installing": "安装中...", + "terminal.remote_server.loading.installing_progress": "安装中... ({percent}%)", + "terminal.remote_server.loading.installing_ssh_extension": "正在安装 Warp SSH Extension...", + "terminal.remote_server.loading.installing_ssh_extension_progress": "正在安装 Warp SSH Extension... ({percent}%)", + "terminal.remote_server.loading.starting_shell": "正在启动 shell...", + "terminal.remote_server.loading.updating": "更新中...", + "terminal.remote_server.loading.updating_ssh_extension": "正在更新 Warp SSH Extension...", + "terminal.rewind.no_code_to_restore": "没有可恢复的代码", + "terminal.rich_history.exit_code": "退出码 {code}", + "terminal.rich_history.finished_in": "耗时 {duration}", + "terminal.rich_history.last_ran": "上次运行于 {time}", + "terminal.rich_history.ran": "运行于 {time}", + "terminal.session_settings.working_directory.custom_directory": "自定义目录", + "terminal.session_settings.working_directory.home_directory": "主目录", + "terminal.session_settings.working_directory.previous_session_directory": "上一个会话的目录", + "terminal.share_block.create_link": "创建链接", + "terminal.share_block.creating_block": "正在创建块...", + "terminal.share_block.creation_failed": "出了点问题。请重试。", + "terminal.share_block.default_embed_title": "嵌入式 Warp 区块", + "terminal.share_block.display.command": "命令", + "terminal.share_block.display.command_and_output": "命令和输出", + "terminal.share_block.display.output": "输出", + "terminal.share_block.embed_copied": "嵌入代码已复制。", + "terminal.share_block.embed_snippet_error": "生成嵌入代码片段出错", + "terminal.share_block.get_embed": "获取嵌入代码", + "terminal.share_block.link_copied": "链接已复制。", + "terminal.share_block.manage_shared_blocks": "管理共享块", + "terminal.share_block.redact_secrets": "隐藏敏感信息(API key、密码、IP 地址、PII 等)", + "terminal.share_block.show_prompt": "显示提示符", + "terminal.share_block.title": "共享块", + "terminal.share_block.title_placeholder": "标题(可选)", + "terminal.shared_session.agent_task": "Agent 任务", + "terminal.shared_session.approve": "同意", + "terminal.shared_session.are_you_still_there": "你还在吗?", + "terminal.shared_session.change_role": "更改角色", + "terminal.shared_session.cloud_agent_failed": "云端 Agent 启动失败", + "terminal.shared_session.continue_cloud_tooltip": "继续此云端对话", + "terminal.shared_session.continue_sharing": "继续共享", + "terminal.shared_session.copy_session_sharing_link": "复制会话共享链接", + "terminal.shared_session.deny": "拒绝", + "terminal.shared_session.edit_permission_warning_line1": "授予后,对方将能够代表你执行命令", + "terminal.shared_session.edit_permission_warning_line2": "请谨慎操作。", + "terminal.shared_session.edit_requests": "编辑请求", + "terminal.shared_session.ended_due_to_inactivity": "共享因无活动而结束", + "terminal.shared_session.ended_due_to_sharer_inactivity": "共享因共享者无活动而结束", + "terminal.shared_session.error.access_removed_reshare": "你对此会话的访问权限已被移除。请让共享者重新共享以继续。", + "terminal.shared_session.error.command_in_progress": "已有命令正在进行。", + "terminal.shared_session.error.connect_failed": "连接失败。请稍后重试。", + "terminal.shared_session.error.execute_command_failed": "执行命令失败。", + "terminal.shared_session.error.guests_already_added": "一个或多个邮箱已添加到此会话。", + "terminal.shared_session.error.guests_not_warp_users": "一个或多个邮箱未关联 Warp 账号。", + "terminal.shared_session.error.insufficient_permissions_request_edit": "权限不足。请请求编辑权限以继续。", + "terminal.shared_session.error.internal": "发生内部错误。请尝试重新共享。", + "terminal.shared_session.error.internal_ended": "会话因内部错误而结束。请尝试重新共享。", + "terminal.shared_session.error.invalid_conversation": "无效的对话。", + "terminal.shared_session.error.invalid_link": "无效的会话共享链接。", + "terminal.shared_session.error.join_failed": "加入共享会话失败。", + "terminal.shared_session.error.link_not_accessible": "你无权访问此链接。", + "terminal.shared_session.error.login_required": "你必须登录后才能共享会话。", + "terminal.shared_session.error.make_edit_failed": "编辑失败。", + "terminal.shared_session.error.max_participants_reached": "此共享会话的参与者数量已达到上限。", + "terminal.shared_session.error.no_quota_remaining": "今日会话共享用量已超出。请稍后重试。", + "terminal.shared_session.error.perform_action_failed": "执行操作失败。", + "terminal.shared_session.error.reconnect_failed": "重新连接失败。", + "terminal.shared_session.error.reshare_to_continue": "出了点问题。请让共享者重新共享以继续。", + "terminal.shared_session.error.scrollback_too_large": "回滚内容超出限制。请不带回滚内容重新共享。", + "terminal.shared_session.error.session_not_found": "未找到共享会话。", + "terminal.shared_session.error.size_limit_exceeded": "会话限制 ({max_bytes}) 已超出。请重新共享以继续。", + "terminal.shared_session.error.update_permissions_failed": "更新权限失败。", + "terminal.shared_session.limit_reached_header": "已达到共享会话限制", + "terminal.shared_session.limit_reached_subheader": "Warp 的免费和 Pro 套餐包含的共享会话数量有限。\n\n如需更多会话共享额度,请升级到 Build 套餐。", + "terminal.shared_session.make_editor": "设为编辑者", + "terminal.shared_session.make_viewer": "设为查看者", + "terminal.shared_session.metadata.credits": "已用额度:{value}", + "terminal.shared_session.metadata.directory": "目录:{value}", + "terminal.shared_session.metadata.run_time": "运行时长:{value}", + "terminal.shared_session.metadata.skill": "技能:{value}", + "terminal.shared_session.metadata.source": "来源:{value}", + "terminal.shared_session.open_in_warp": "在 Warp 中打开", + "terminal.shared_session.open_in_warp_tooltip": "在 Warp 桌面应用中打开此对话", + "terminal.shared_session.options_disabled_size": "部分选项因共享大小限制而不可用", + "terminal.shared_session.options_disabled_size_and_agents": "部分选项因共享大小限制以及会话中存在 Agent 对话而不可用", + "terminal.shared_session.permissions_revoked_due_to_inactivity": "共享编辑权限已因无活动而撤销", + "terminal.shared_session.permissions_revoked_sharer_idle": "由于共享者处于空闲状态,编辑权限已撤销", + "terminal.shared_session.reconnecting": "已离线,正在尝试重新连接...", + "terminal.shared_session.request_edit_access": "请求编辑权限", + "terminal.shared_session.revoke_all_edit_permissions": "撤销所有编辑权限", + "terminal.shared_session.role.edit": "编辑", + "terminal.shared_session.role.view": "查看", + "terminal.shared_session.scrollback.current_block": "从当前块开始分享", + "terminal.shared_session.scrollback.current_screen": "从当前屏幕分享", + "terminal.shared_session.scrollback.selected_block_onwards": "从选中的块及之后开始分享", + "terminal.shared_session.scrollback.start_of_session": "从会话开始处分享", + "terminal.shared_session.scrollback.without_scrollback": "不带回滚内容分享", + "terminal.shared_session.session_ended": "会话已结束。", + "terminal.shared_session.share_session": "分享会话", + "terminal.shared_session.share_session_ellipsis": "分享会话...", + "terminal.shared_session.sharing_link_copied": "共享链接已复制", + "terminal.shared_session.sharing_will_end_due_to_inactivity": "由于无活动,共享将在 {time} 后结束。", + "terminal.shared_session.snapshot_subtitle": "此共享对话显示的是你打开时的状态。如果 Agent 仍在运行,请刷新以查看最新进度。", + "terminal.shared_session.snapshot_title": "你正在查看快照", + "terminal.shared_session.start_sharing": "开始共享", + "terminal.shared_session.stop_sharing": "停止共享", + "terminal.shared_session.stop_sharing_all": "全部停止共享", + "terminal.shared_session.stop_sharing_session": "停止共享会话", + "terminal.shared_session.viewer_request.cancel": "取消请求", + "terminal.shared_session.viewer_request.header": "你已请求{role}模式", + "terminal.shared_session.viewer_request.waiting": "正在等待 {name}...", + "terminal.shared_session.without_scrollback_disabled_agents": "此会话包含 Agent 对话,因此无法不带回滚内容共享", + "terminal.shell.copy_error": "复制错误", + "terminal.shell.file_issue": "提交 issue", + "terminal.shell.more_info": "更多信息", + "terminal.shell.process_could_not_start": "Shell 进程无法启动!", + "terminal.shell.process_exited": "Shell 进程已退出", + "terminal.shell.process_exited_prematurely": "Shell 进程意外退出!", + "terminal.shell.warpify_failed_subtext": "启动 {shell_detail} 并对其进行 Warpify 时出现问题,导致进程终止。此处显示了 Warpify 脚本输出,可能指出原因。", + "terminal.skills.project_skill": "项目技能", + "terminal.slash_commands.active_conversation_required": "{command} 需要一个活动对话", + "terminal.slash_commands.cloud_oz_only": "{command} 仅适用于云端 Oz 对话", + "terminal.slash_commands.conversation_exported_to_clipboard": "对话已导出到剪贴板", + "terminal.slash_commands.cost_conversation_empty": "无法显示对话费用:对话为空", + "terminal.slash_commands.cost_conversation_in_progress": "无法显示对话费用:对话正在进行中", + "terminal.slash_commands.cost_no_active_conversation": "无法显示对话费用:没有活动对话", + "terminal.slash_commands.create_project_missing_description": "请在 /create-new-project 后描述你想创建的项目", + "terminal.slash_commands.export_to_file_unsupported_web": "网页端不支持将对话导出到文件", + "terminal.slash_commands.file_not_found": "未找到文件:{path}", + "terminal.slash_commands.no_active_conversation_to_export": "没有可导出的活动对话", + "terminal.slash_commands.open_file_local_only": "/open-file 命令仅适用于本地会话", + "terminal.slash_commands.open_file_requires_file": "/open-file 命令仅适用于文件,不适用于目录", + "terminal.slash_commands.open_file_unsupported": "此构建不支持 /open-file 命令", + "terminal.slash_commands.prompt_argument_required": "{command} 需要一个 prompt 参数", + "terminal.slash_commands.rename_tab_missing_name": "请在 /rename-tab 后提供标签页名称", + "terminal.slash_commands.requires_ai_enabled": "{command} 需要启用 AI", + "terminal.slash_commands.session_already_shared": "会话已在共享中", + "terminal.slash_commands.set_tab_color_missing_color": "请在 /set-tab-color 后提供颜色({options})", + "terminal.slash_commands.set_tab_color_unknown": "未知标签页颜色“{color}”。请使用以下之一:{options}。", + "terminal.ssh_remote_choice.continue_without_installing.description": "你仍会获得 Warpified 体验,只是不包含编码相关功能。", + "terminal.ssh_remote_choice.continue_without_installing.title": "不安装并继续", + "terminal.ssh_remote_choice.dont_ask_again": "不再询问", + "terminal.ssh_remote_choice.install_extension.description": "安装 Warp 扩展,以在此会话中启用文件浏览、代码审查和智能命令补全等 Agent 功能。", + "terminal.ssh_remote_choice.install_extension.title": "安装 Warp 的 SSH 扩展", + "terminal.ssh_remote_choice.manage_settings": "管理 Warpify 设置", + "terminal.ssh_remote_choice.title": "为此远程会话选择体验:", + "terminal.ssh.clear_upload": "清除上传", + "terminal.ssh.error.continue_without_warpification": "不使用 Warpification 继续", + "terminal.ssh.error.report_issue_link": "提交 issue", + "terminal.ssh.error.report_issue_prefix": "我们正在积极提升 Warp 中 SSH 的稳定性。请考虑在 GitHub 上", + "terminal.ssh.error.report_issue_suffix": ",帮助我们更好地定位问题。", + "terminal.ssh.error.title": "Warpify 会话出错", + "terminal.ssh.error.tmux_failed": "tmux 无法在远程机器上执行。请重新安装 tmux 后重试。", + "terminal.ssh.error.tmux_install_failed": "tmux 安装遇到意外错误。请手动安装 tmux 后重试。", + "terminal.ssh.error.tmux_not_installed": "远程机器上未安装 tmux。请安装 tmux 后重试。", + "terminal.ssh.error.unsupported_shell": "不支持的 shell。请将 bash、zsh 或 fish 设为默认 shell 后重试。", + "terminal.ssh.error.unsupported_tmux_version": "远程机器上的 tmux 版本低于 3.0。请用其他方式安装 tmux 3.0 或更高版本后重试。", + "terminal.ssh.error.warpify_timeout": "Warpify 会话超时。", + "terminal.ssh.error.warpify_without_tmux": "不使用 TMUX 进行 Warpify", + "terminal.ssh.file_uploads": "文件上传", + "terminal.ssh.install_tmux.install_to_warp": "安装到 ~/.warp", + "terminal.ssh.install_tmux.install_with": "使用 {package_manager} 安装", + "terminal.ssh.install_tmux.missing_tmux_explanation": "要 Warpify 你的 SSH 会话,必须安装 tmux。", + "terminal.ssh.install_tmux.outdated_version_explanation": "要 Warpify 你的 SSH 会话,必须安装更新版本的 tmux(>=3.0)。", + "terminal.ssh.install_tmux.run_script_prompt": "运行此脚本来安装 tmux?", + "terminal.ssh.install_tmux.title": "安装 tmux?", + "terminal.ssh.remote_server_failed.body": "虽然文件浏览和代码审查等高级功能当前已禁用,但其余 Warpified 体验仍可完整使用。", + "terminal.ssh.remote_server_failed.start_failed": "SSH 扩展启动失败", + "terminal.ssh.remote_server_failed.title": "无法连接到 Warp SSH 扩展", + "terminal.ssh.to_separator": " 到 ", + "terminal.ssh.upload.failed": "上传失败", + "terminal.ssh.upload.uploaded": "已上传", + "terminal.ssh.upload.uploading": "正在上传", + "terminal.ssh.upload.waiting_for_password": "正在等待密码输入", + "terminal.ssh.warpify_description": "将 Warp 功能带到你的远程会话:块、全文本编辑、自动补全、Oz 等。", + "terminal.ssh.warpify_title": "正在 Warpify SSH 会话...", + "terminal.ssh.why_tmux": "为什么需要 tmux?", + "terminal.toast.bundled_skills_cannot_edit": "无法编辑内置技能", + "terminal.toast.couldnt_continue_cloud_task": "无法继续此云端任务。", + "terminal.toast.editing_skills_unsupported": "此构建不支持编辑技能", + "terminal.toast.non_local_env_subshell": "无法在非本地会话中调用环境变量子 shell", + "terminal.toast.powershell_subshell_not_supported": "不支持 PowerShell 子 shell", + "terminal.toast.skill_not_found": "未找到技能:{reference}", + "terminal.tooltips.copy_secret": "复制密钥", + "terminal.tooltips.hide_secret": "隐藏密钥", + "terminal.tooltips.open_in_warp": "在 Warp 中打开", + "terminal.tooltips.reveal_secret": "显示密钥", + "terminal.tooltips.show_containing_folder": "显示所在文件夹", + "terminal.tooltips.show_in_finder": "在 Finder 中显示", + "terminal.universal_input.attach_context": "附加上下文", + "terminal.universal_input.disabled_terminal_mode": "终端模式下已禁用,请在设置中重新启用", + "terminal.universal_input.input_mode_locked": "Agent 正在监控命令,输入模式已锁定", + "terminal.universal_input.no_context_objects": "当前上下文中没有可用对象。", + "terminal.universal_input.request_edit_access": "请求编辑权限以更改输入模式", + "terminal.universal_input.requires_fs": "需要文件系统", + "terminal.universal_input.slash_commands": "斜杠命令", + "terminal.universal_input.ssh_without_remote_server": "不支持没有远程服务器的 SSH 会话", + "terminal.universal_input.subshell_not_supported": "不支持子 shell", + "terminal.use_agent_footer.ask_assist": "让 Warp Agent 协助", + "terminal.use_agent_footer.ask_resume": "让 Warp Agent 继续", + "terminal.use_agent_footer.enable_shell_integration": "在此会话中启用 Warp shell 集成", + "terminal.use_agent_footer.give_control_back": "将控制权交还给 Agent", + "terminal.use_agent_footer.use_agent": "使用 Agent", + "terminal.use_agent_footer.warpify_ssh_session": "Warpify SSH 会话", + "terminal.use_agent_footer.warpify_subshell": "Warpify 子 shell", + "terminal.warning.completions_not_working_prefix": "你的补全似乎无法正常工作(", + "terminal.warning.did_you_intend_prefix": "你是否想使用", + "terminal.warning.enable_ssh_extension_prefix": ")。在", + "terminal.warning.keep_ide_bindings": "否,保留 IDE 风格绑定", + "terminal.warning.may_resolve_suffix": "中启用 SSH 扩展可能会解决此问题。", + "terminal.warning.more_info": "更多信息", + "terminal.warning.more_info_lower": "更多信息", + "terminal.warning.move_cursor_suffix": "移动光标?", + "terminal.warning.old_prompt_version_prefix": "你似乎正在运行较旧且不受支持的版本,请按照", + "terminal.warning.powerlevel10k_supports_warp": "Powerlevel10k 现在支持 Warp!", + "terminal.warning.pure_not_supported": "Warp 暂不支持 Pure。你可以考虑使用受支持的提示符作为替代。", + "terminal.warning.settings": "设置", + "terminal.warning.shell_config_incompatible": "你的 shell 配置与 Warp 不兼容...", + "terminal.warning.shell_start_slow": "你的 shell 启动似乎需要较长时间...", + "terminal.warning.these_instructions": "这些说明", + "terminal.warning.update_latest_suffix": "更新到最新版本。", + "terminal.warning.use_emacs_style_bindings": "是,使用 Emacs 风格绑定", + "terminal.warpify.auto_warpify_command.description": "运行以下内容,以后自动 Warpify:", + "terminal.warpify.never_warpify_this_host": "永不 Warpify 此主机", + "terminal.warpify.remote_subshell.description": "在远程子 shell 中,Warp 会在后台运行命令,以支持补全、语法高亮和其他功能。", + "terminal.warpify.session_warpified": "会话已 Warpify", + "terminal.warpify.ssh_session_lowercase_title": "SSH 会话", + "terminal.warpify.ssh_session_title": "SSH 会话", + "terminal.warpify.subshell_lowercase_title": "子 shell", + "terminal.warpify.subshell_title": "子 shell", + "terminal.zero_state.autodetect_agent_prompts": "自动检测终端会话中的 agent 提示词", + "terminal.zero_state.cycle_past_commands_and_conversations": "切换历史命令和对话", + "terminal.zero_state.go_back_to_terminal": "返回终端", + "terminal.zero_state.init_callout": "索引此代码库并生成 AGENTS.md,以获得最佳性能", + "terminal.zero_state.new_terminal_session": "新建终端会话", + "terminal.zero_state.open_code_review": "打开代码审查", + "terminal.zero_state.start_agent_conversation": "开始新的 agent 对话", + "terminal.zero_state.start_cloud_agent_conversation": "开始新的云端 agent 对话", + "terminal.zero_state.switch_model": "切换模型", + "themes.chooser.a11y.close_hint": "按 Escape 关闭。", + "themes.chooser.a11y.description": "主题选择器。很遗憾,主题选择器窗口暂时还不兼容屏幕阅读器。", + "themes.chooser.hint.current": "更改当前主题。", + "themes.chooser.hint.dark": "选择系统处于深色模式时使用的主题。", + "themes.chooser.hint.light": "选择系统处于浅色模式时使用的主题。", + "themes.chooser.no_matching_themes": "没有匹配的主题!", + "themes.chooser.title": "主题", + "themes.creator.background_color": "背景颜色", + "themes.creator.create_theme": "创建主题", + "themes.creator.error_process_image": "处理所选图片失败。请换一张图片重试。", + "themes.creator.error_process_image_with_error": "处理所选图片失败,错误:{error}。请换一张图片重试。", + "themes.creator.modal_header": "从图片创建新主题", + "themes.creator.modal_subheader": "根据图片(.png、.jpg)中提取的颜色自动生成主题。", + "themes.creator.select_image": "选择图片", + "themes.creator.select_new_image": "选择新图片", + "themes.creator.selecting_image": "正在选择图片...", + "themes.creator.theme_name": "主题名称", + "themes.deletion.delete_theme": "删除主题", + "themes.deletion.modal_header": "确定要删除此主题吗?", + "themes.deletion.modal_subheader": "这将永久删除该主题。", + "tips.ai_command_search.description": "用自然语言生成 shell 命令。", + "tips.ai_command_search.title": "AI 命令搜索", + "tips.close_welcome_tips": "关闭欢迎提示", + "tips.command_palette.description": "不用离开键盘,就能轻松发现 Warp 中的所有操作。", + "tips.command_palette.title": "命令面板", + "tips.complete": "完成!", + "tips.finished_message": "欢迎提示已完成,做得不错!", + "tips.history_search.description": "查找、编辑并重新运行之前执行过的命令。", + "tips.history_search.title": "历史搜索", + "tips.shortcut": "快捷键", + "tips.skip_welcome_tips": "跳过欢迎提示", + "tips.split_pane.description": "把标签页拆分成多个窗格,搭出适合你的布局。", + "tips.split_pane.title": "拆分窗格", + "tips.theme_picker.description": "选择内置主题,或创建自己的主题,让 Warp 更符合你的偏好。", + "tips.theme_picker.title": "主题选择器", + "uri.custom_uri_invalid": "自定义 URI 无效。", + "uri.custom_uri_invalid_with_error": "自定义 URI 无效:{error}", + "uri.new_tab_created": "已创建新标签页", + "uri.new_tab_created.description": "前往 Warp 查看你的新标签页。", + "util.time.approx.day_ago": "{count} 天前", + "util.time.approx.days_ago": "{count} 天前", + "util.time.approx.hour_ago": "{count} 小时前", + "util.time.approx.hours_ago": "{count} 小时前", + "util.time.approx.just_now": "刚刚", + "util.time.approx.just_now_sentence": "刚刚", + "util.time.approx.minutes_ago_short": "{count} 分钟前", + "util.time.approx.month_ago": "{count} 个月前", + "util.time.approx.months_ago": "{count} 个月前", + "util.time.approx.week_ago": "{count} 周前", + "util.time.approx.weeks_ago": "{count} 周前", + "util.time.approx.year_ago": "{count} 年前", + "util.time.approx.years_ago": "{count} 年前", + "util.time.elapsed.minute_ago": "{count} 分钟前", + "util.time.elapsed.minutes_ago": "{count} 分钟前", + "util.time.precise.days": "{count} 天", + "util.time.precise.hours": "{count} 小时", + "util.time.precise.milliseconds": "{count} 毫秒", + "util.time.precise.minutes": "{count} 分钟", + "util.time.precise.more_than_one_week": ">1 周", + "util.time.precise.seconds": "{count} 秒", + "util.tooltips.secret_not_included_ai_conversation": "此内容未包含在 AI 对话中。", + "util.tooltips.secret_not_included_ai_or_shared_blocks": "此内容不会包含在任何 AI 对话或共享 Block 中。", + "util.tooltips.secret_pattern_enterprise": "已匹配你组织的密钥脱敏正则列表。", + "util.tooltips.secret_pattern_generic": "已匹配密钥脱敏正则列表。", + "util.tooltips.secret_pattern_user": "已匹配你的密钥脱敏正则列表。", + "util.tooltips.secrets_not_sent": "*密钥不会发送到 Warp 服务器。", + "warp_cli.about": "云端 Agent 编排平台\n\nOz CLI 可用于大规模运行、管理和编排 coding agents。\n你可以用 CLI:\n* 启动并查看云端 Agent\n* 安排云端 Agent 在未来运行\n* 管理云端 Agent 运行所用的环境\n* 将 secrets 上传到 Oz 的安全存储", + "warp_cli.after_help": "示例:\n\n $ {bin_name} agent run --prompt \"Build anything\"\n\n $ {bin_name} mcp list\n\n了解更多:\n* 使用 {bin_name} help 查看每个命令的说明\n* 阅读文档:https://docs.warp.dev/reference/cli\n", + "warp_cli.agent.arg.agent_uid.help": "执行此次运行时使用的 Agent UID", + "warp_cli.agent.arg.agent_uid.long_help": "执行此次运行时使用的 Agent UID。\n\n这会应用该 Agent 的配置,例如 skills 和基础模型,并将额度使用归因到该 Agent。", + "warp_cli.agent.arg.attachment_paths.help": "要附加到 Agent query 的文件路径", + "warp_cli.agent.arg.attachment_paths.long_help": "要附加到 Agent query 的文件路径。\n\n可多次指定以附加多个文件(最多 5 个)。\n\n示例:--attach file1.png --attach file2.txt", + "warp_cli.agent.arg.computer_use.help": "为此次 Agent 运行启用 computer use 能力", + "warp_cli.agent.arg.config_file.help": "YAML 或 JSON 配置文件路径", + "warp_cli.agent.arg.conversation.help": "通过 ID 继续现有云端 conversation", + "warp_cli.agent.arg.create.base_model.help": "此 Agent 运行时使用的基础模型", + "warp_cli.agent.arg.create.description.help": "Agent 描述", + "warp_cli.agent.arg.create.environment.help": "此 Agent 运行时默认使用的云端环境", + "warp_cli.agent.arg.create.name.help": "Agent 名称", + "warp_cli.agent.arg.create.secrets.help": "给 Agent 附加 secret。可重复此 flag 附加多个 secrets。", + "warp_cli.agent.arg.create.skills.help": "给 Agent 附加 skill。可重复此 flag 附加多个 skills。", + "warp_cli.agent.arg.cwd.help": "Agent 的工作目录", + "warp_cli.agent.arg.environment.help": "要使用的云端环境,以 ID 指定", + "warp_cli.agent.arg.jq_filter.help": "使用 jq 语法从响应中筛选值", + "warp_cli.agent.arg.jq_filter.long_help": "使用 jq 语法从响应中选择值。\n\n示例:`--jq '.runs[].creator'`\n\n设置后会打印 filter 表达式的结果,而不是完整 JSON 输出。顶层标量输出会自动去除引号。", + "warp_cli.agent.arg.model.help": "覆盖此命令使用的基础模型。使用 `warp model list` 查看可用模型。", + "warp_cli.agent.arg.name.help": "此次 Agent task 的名称", + "warp_cli.agent.arg.no_computer_use.help": "为此次 Agent 运行禁用 computer use 能力", + "warp_cli.agent.arg.no_environment.help": "不在环境中运行 Agent(不推荐)", + "warp_cli.agent.arg.no_snapshot.help": "禁用运行结束时的 workspace snapshot 上传", + "warp_cli.agent.arg.open.help": "Agent 会话可用后在 Warp 中打开", + "warp_cli.agent.arg.profile.help": "用于配置终端会话的 Agent profile", + "warp_cli.agent.arg.prompt.help": "让 Agent 执行的 prompt", + "warp_cli.agent.arg.saved_prompt.help": "要运行的已保存 AI prompt,以 ID 指定", + "warp_cli.agent.arg.scope.personal.help": "作为你账号下的私有对象创建", + "warp_cli.agent.arg.scope.team.help": "在团队级别创建", + "warp_cli.agent.arg.skill.help": "使用 skill 作为 Agent 的基础 prompt", + "warp_cli.agent.arg.skill.long_help": "使用 skill 作为 Agent 的基础 prompt。\n\n格式:`skill_name`、`repo:skill_name` 或 `org/repo:skill_name`\n\n会在 `.agents/skills/`、`.warp/skills/`、`.claude/skills/` 和 `.codex/skills/` 目录中查找 skills。如果指定 repo,则只搜索该 repo。如果同时指定 org,会校验 repo 的 git remote 是否匹配预期 org。\n\n与 --prompt 一起使用时,skill 提供基础上下文,prompt 是任务。\n\n如需定时自动运行 skill,请使用 `oz schedule create --skill `。", + "warp_cli.agent.arg.skills.repo.help": "列出指定 GitHub 仓库中的 skills", + "warp_cli.agent.arg.skills.repo.long_help": "列出指定 GitHub 仓库中的 skills。\n\n格式:`owner/repo` 或 `https://github.com/owner/repo`\n\n提供后会列出此 repo 中的 skills,而不是从你的环境中列出。包含此 repo 的任何环境仍会显示在结果中。", + "warp_cli.agent.arg.snapshot_script_timeout.help": "上传 snapshot 前等待 declarations script 的最长时间", + "warp_cli.agent.arg.snapshot_upload_timeout.help": "等待运行结束 snapshot 上传的最长时间", + "warp_cli.agent.arg.sort_by.help": "排序字段。仅支持 pretty、text 和 ndjson 输出。", + "warp_cli.agent.arg.sort_order.help": "排序方向。仅支持 pretty、text 和 ndjson 输出。", + "warp_cli.agent.arg.uid.delete.help": "要删除的 Agent UID", + "warp_cli.agent.arg.uid.get.help": "要获取的 Agent UID", + "warp_cli.agent.arg.uid.update.help": "要更新的 Agent UID", + "warp_cli.agent.arg.update.add_secrets.help": "给 Agent 添加 secret。可重复此 flag 添加多个 secrets。", + "warp_cli.agent.arg.update.add_skills.help": "给 Agent 添加 skill。可重复此 flag 添加多个 skills。", + "warp_cli.agent.arg.update.base_model.help": "替换由此 Agent 执行的运行所用的基础模型", + "warp_cli.agent.arg.update.description.help": "替换 Agent 描述", + "warp_cli.agent.arg.update.environment.help": "替换由此 Agent 执行的运行所用的默认云端环境", + "warp_cli.agent.arg.update.name.help": "Agent 的新名称", + "warp_cli.agent.arg.update.remove_all_secrets.help": "移除此 Agent 的所有 secrets", + "warp_cli.agent.arg.update.remove_all_skills.help": "移除此 Agent 的所有 skills", + "warp_cli.agent.arg.update.remove_base_model.help": "移除此 Agent 的基础模型", + "warp_cli.agent.arg.update.remove_description.help": "移除此 Agent 的描述", + "warp_cli.agent.arg.update.remove_environment.help": "移除此 Agent 的默认环境", + "warp_cli.agent.arg.update.remove_secrets.help": "从 Agent 移除 secret。可重复此 flag 移除多个 secrets。", + "warp_cli.agent.arg.update.remove_skills.help": "从 Agent 移除 skill。可重复此 flag 移除多个 skills。", + "warp_cli.agent.arg.worker_host.help": "此 job 的托管位置", + "warp_cli.agent.arg.worker_host.long_help": "此 job 的托管位置。\n\n设置为 \"warp\" 时会在 Warp 基础设施上运行。其他值会被视为 self-hosted job,并与 self-hosted worker 的名称匹配。", + "warp_cli.agent.prompt.plain_text": "Prompt:{text}", + "warp_cli.agent.prompt.saved_prompt_id": "已保存 Prompt ID:{id}", + "warp_cli.api_key.arg.agent_uid.help": "作为其进行认证的 Agent UID", + "warp_cli.api_key.arg.expires_at.help": "让 API key 在指定时间过期", + "warp_cli.api_key.arg.expires_in.help": "让 API key 在此时长后过期,例如 \"30d\"、\"12h\" 或 \"90m\"", + "warp_cli.api_key.arg.force.expire.help": "过期前不要求确认", + "warp_cli.api_key.arg.key_uid.expire.help": "要过期的 API key 名称或 UID", + "warp_cli.api_key.arg.name.create.help": "要创建的 API key 名称", + "warp_cli.api_key.arg.no_expiration.help": "创建永不过期的 API key", + "warp_cli.api_key.arg.sort_by.help": "排序字段", + "warp_cli.api_key.arg.sort_order.help": "排序方向", + "warp_cli.arg.api_key.help": "用于服务端认证的 API key", + "warp_cli.arg.debug.help": "启用 debug 日志", + "warp_cli.arg.output_format.help": "设置输出格式", + "warp_cli.artifact.arg.artifact_uid.download.help": "要下载的 artifact UID", + "warp_cli.artifact.arg.artifact_uid.get.help": "要获取的 artifact UID", + "warp_cli.artifact.arg.conversation_id.help": "将上传的 artifact 关联到 conversation", + "warp_cli.artifact.arg.description.help": "上传 artifact 的描述", + "warp_cli.artifact.arg.out.help": "将下载的 artifact 写入指定文件路径", + "warp_cli.artifact.arg.path.upload.help": "要上传的 artifact 文件路径", + "warp_cli.artifact.arg.run_id.help": "将上传的 artifact 关联到 run", + "warp_cli.command.agent.about": "与 Oz 交互", + "warp_cli.command.agent.create.about": "创建新的 Agent", + "warp_cli.command.agent.delete.about": "删除 Agent", + "warp_cli.command.agent.get.about": "获取 Agent 详情", + "warp_cli.command.agent.list.about": "列出所有可用 Agent", + "warp_cli.command.agent.profile.about": "管理 Agent profiles", + "warp_cli.command.agent.profile.list.about": "列出可用的 Agent profiles", + "warp_cli.command.agent.run_cloud.about": "派发远程运行的 Oz Agent", + "warp_cli.command.agent.run.about": "运行新的 Oz Agent", + "warp_cli.command.agent.skills.about": "列出可用的 Agent skills", + "warp_cli.command.agent.update.about": "更新现有 Agent", + "warp_cli.command.api_key.about": "管理 API keys", + "warp_cli.command.api_key.create.about": "创建新的 API key", + "warp_cli.command.api_key.expire.about": "立即使 API key 过期", + "warp_cli.command.api_key.list.about": "列出活跃 API keys", + "warp_cli.command.artifact.about": "管理 artifacts", + "warp_cli.command.artifact.download.about": "下载 artifact 文件", + "warp_cli.command.artifact.get.about": "获取 artifact metadata", + "warp_cli.command.artifact.upload.about": "上传 artifact 文件", + "warp_cli.command.completions.about": "将 shell completions 输出到 stdout", + "warp_cli.command.completions.long_about": "将 shell completions 输出到 stdout。\n\n对于 bash,请将以下内容添加到 ~/.bashrc:\n source <(path/to/warp completions bash)\n\n对于 zsh,请将以下内容添加到 ~/.zshrc:\n source <(path/to/warp completions zsh)\n\n对于 fish,请将以下内容添加到 ~/.config/fish/config.fish:\n path/to/warp completions fish | source\n\n对于 Powershell,请将以下内容添加到 $PROFILE:\n path\\to\\warp | Out-String | Invoke-Expression\n\n如果未提供 shell,则默认使用运行 Warp 时所在的 shell。", + "warp_cli.command.completions.shell.help": "要生成 completions 的 shell", + "warp_cli.command.dump_debug_info.about": "打印调试信息并退出", + "warp_cli.command.environment.about": "管理云端环境", + "warp_cli.command.environment.create.about": "创建新的云环境", + "warp_cli.command.environment.delete.about": "删除云环境", + "warp_cli.command.environment.get.about": "获取云环境详情", + "warp_cli.command.environment.image.about": "管理云环境 base images", + "warp_cli.command.environment.image.list.about": "列出 Docker Hub 上可用的 Warp dev base images", + "warp_cli.command.environment.list.about": "列出云环境", + "warp_cli.command.environment.update.about": "更新现有云环境", + "warp_cli.command.federate.about": "签发并管理 federated identity tokens", + "warp_cli.command.federate.issue_gcp_token.about": "为 Google Cloud executable-sourced credentials 签发 identity token", + "warp_cli.command.federate.issue_gcp_token.long_about": "为当前 Oz Agent 签发 identity token,输出格式符合 Google Cloud executable-sourced credentials 机制要求。\n\n参见 https://docs.cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#executable-sourced-credentials", + "warp_cli.command.federate.issue_token.about": "为当前 Oz Agent 签发 identity token", + "warp_cli.command.federate.long_about": "Oz 与云 providers 之间的 federated authentication。\n\nOz 支持 OIDC federation,让 agents 可使用短期 credentials 安全认证到其他系统。", + "warp_cli.command.harness_support.about": "供 Agent harness 与 Oz 集成的支持命令", + "warp_cli.command.harness_support.finish_task.about": "上报任务完成或失败", + "warp_cli.command.harness_support.notify_user.about": "向任务来源平台发送进度通知", + "warp_cli.command.harness_support.ping.about": "验证当前 run 的连接状态", + "warp_cli.command.harness_support.report_artifact.about": "向 Oz 上报 artifact", + "warp_cli.command.harness_support.report_artifact.pull_request.about": "上报 pull request artifact", + "warp_cli.command.harness_support.report_shutdown.about": "上报 Agent 进程正在关闭", + "warp_cli.command.integration.about": "管理 integrations", + "warp_cli.command.integration.create.about": "创建新的 integration", + "warp_cli.command.integration.list.about": "列出 simple integrations 及其连接状态", + "warp_cli.command.integration.update.about": "更新 integration", + "warp_cli.command.login.about": "登录 Warp", + "warp_cli.command.logout.about": "退出 Warp 登录", + "warp_cli.command.mcp.about": "管理 MCP servers", + "warp_cli.command.mcp.list.about": "列出 MCP servers", + "warp_cli.command.model.about": "管理可用模型", + "warp_cli.command.model.list.about": "列出可用模型", + "warp_cli.command.print_telemetry_events.about": "打印生产环境 telemetry events 并退出", + "warp_cli.command.provider.about": "管理 providers", + "warp_cli.command.provider.list.about": "列出 providers", + "warp_cli.command.provider.setup.about": "设置 provider", + "warp_cli.command.run.about": "管理 runs", + "warp_cli.command.run.conversation.about": "获取 run conversations", + "warp_cli.command.run.conversation.get.about": "通过 conversation ID 获取 conversation", + "warp_cli.command.run.get.about": "获取指定 run 的状态", + "warp_cli.command.run.list.about": "列出 Ambient Agent runs", + "warp_cli.command.run.message.about": "管理 run 之间收发的消息", + "warp_cli.command.run.message.list.about": "列出 run 收件箱中的消息头", + "warp_cli.command.run.message.mark_delivered.about": "将消息标记为已送达", + "warp_cli.command.run.message.read.about": "读取完整消息正文", + "warp_cli.command.run.message.send.about": "从一个 run 向一个或多个目标 run 发送消息", + "warp_cli.command.run.message.watch.about": "监听送达指定 run 的新消息", + "warp_cli.command.schedule.about": "创建并管理定时 Oz agents", + "warp_cli.command.schedule.create.about": "创建定时 Oz Agent", + "warp_cli.command.schedule.delete.about": "删除定时 Oz Agent", + "warp_cli.command.schedule.get.about": "获取定时 Oz Agent 的配置", + "warp_cli.command.schedule.list.about": "列出定时 Oz Agents", + "warp_cli.command.schedule.long_about": "创建并管理定时 Oz agents。定时 agents 会根据 cron schedule 定期运行用户定义的任务。\n\n作为简写,`schedule` 命令的行为与 `schedule create` 相同。", + "warp_cli.command.schedule.pause.about": "暂停定时 Oz Agent", + "warp_cli.command.schedule.pause.long_about": "暂停定时 Oz Agent。\n\n暂停后的 Agent 仍然存在,但不会按其 schedule 运行。", + "warp_cli.command.schedule.unpause.about": "恢复定时 Oz Agent", + "warp_cli.command.schedule.unpause.long_about": "恢复定时 Oz Agent。\n\nAgent 将按之前配置的 schedule 继续执行。", + "warp_cli.command.schedule.update.about": "更新定时 Oz Agent", + "warp_cli.command.secret.about": "管理 secrets", + "warp_cli.command.secret.create.about": "创建新 secret", + "warp_cli.command.secret.create.claude.about": "创建 Claude/Anthropic auth secret", + "warp_cli.command.secret.create.claude.api_key.about": "直接使用 Anthropic API key", + "warp_cli.command.secret.create.claude.bedrock_access_key.about": "通过 AWS access keys 使用 Anthropic Bedrock 认证", + "warp_cli.command.secret.create.claude.bedrock_api_key.about": "通过 Amazon Bedrock 使用 Anthropic API key", + "warp_cli.command.secret.create.codex.about": "创建 Codex/OpenAI auth secret", + "warp_cli.command.secret.create.codex.api_key.about": "直接使用 OpenAI API key", + "warp_cli.command.secret.create.long_about": "创建新 secret。\n\n使用 `oz secret create claude api-key ` 创建 Claude/Anthropic auth secret,或使用 `oz secret create codex api-key ` 创建 Codex/OpenAI auth secret。", + "warp_cli.command.secret.delete.about": "删除 secret", + "warp_cli.command.secret.list.about": "列出 secrets", + "warp_cli.command.secret.update.about": "更新 secret", + "warp_cli.command.secret.update.long_about": "更新 secret。\n\n此命令支持更改值(通过 `--value` 或 `--value-file` flags)或描述。当前不支持移动或重命名 secrets。", + "warp_cli.command.whoami.about": "打印当前登录用户信息", + "warp_cli.command.worker.minidump_server.about": "运行 minidump server", + "warp_cli.command.worker.plugin_host.about": "将此进程作为 plugin host 运行,而不是主应用", + "warp_cli.command.worker.remote_server_daemon.about": "运行长期 remote development server daemon", + "warp_cli.command.worker.remote_server_proxy.about": "通过 SSH stdio 运行 remote development server proxy", + "warp_cli.command.worker.ripgrep_search.about": "运行 headless ripgrep search worker", + "warp_cli.command.worker.terminal_server.about": "运行 terminal server", + "warp_cli.completions.error.shell_not_detected": "无法从环境中确定 shell。请提供 shell 参数。", + "warp_cli.environment.arg.description.create.help": "环境描述(最多 240 个字符)", + "warp_cli.environment.arg.description.update.help": "环境描述(最多 240 个字符)", + "warp_cli.environment.arg.docker_image.create.help": "要使用的 Docker image", + "warp_cli.environment.arg.docker_image.create.long_help": "要使用的 Docker image。运行 `warp environment image list` 可列出建议的 dev images。\n\n如果未指定,系统会提示你从可用 images 中选择。", + "warp_cli.environment.arg.docker_image.update.help": "要使用的 Docker image(可选;提供后会更新)", + "warp_cli.environment.arg.force.delete.help": "强制删除,不检查 integration 使用情况", + "warp_cli.environment.arg.force.update.help": "强制更新,不检查 integration 使用情况", + "warp_cli.environment.arg.id.delete.help": "要删除的环境 ID", + "warp_cli.environment.arg.id.get.help": "要获取的环境 ID", + "warp_cli.environment.arg.id.update.help": "要更新的环境 ID", + "warp_cli.environment.arg.name.create.help": "环境名称", + "warp_cli.environment.arg.name.update.help": "环境名称(可选;提供后会更新)", + "warp_cli.environment.arg.remove_description.help": "移除此环境的描述", + "warp_cli.environment.arg.remove_repo.help": "要移除的 Git repo,格式为 \"owner/repo\"(可多次指定)", + "warp_cli.environment.arg.remove_setup_command.help": "要从列表中移除的 setup command(可多次指定)", + "warp_cli.environment.arg.repo.create.help": "Git repo,格式为 \"owner/repo\"(可多次指定)", + "warp_cli.environment.arg.repo.update.help": "要添加的 Git repo,格式为 \"owner/repo\"(可多次指定)", + "warp_cli.environment.arg.setup_command.create.help": "接受多个 clone 后运行的 setup command 参数", + "warp_cli.environment.arg.setup_command.update.help": "添加到列表末尾的 setup command(可多次指定)", + "warp_cli.environment.error.description_too_long": "描述最多 {max} 个字符(当前 {len} 个)", + "warp_cli.error.invalid_jq_filter": "无效的 jq filter `{src}`:\n{detail}", + "warp_cli.error.invalid_parent_handle": "无效的 parent handle:{error}", + "warp_cli.error.invalid_rfc3339": "无效的 RFC 3339 时间戳 '{value}':{error}", + "warp_cli.error.more_info_help": "更多信息请尝试 '--help'", + "warp_cli.error.unrecognized_subcommand": "错误:无法识别的子命令 '{subcommand}'", + "warp_cli.federate.arg.audience.help": "identity token 的 audience claim", + "warp_cli.federate.arg.duration.help": "请求的 token 生命周期(例如 \"1h\"、\"30m\")", + "warp_cli.federate.arg.gcp_audience.help": "token 请求的 audience", + "warp_cli.federate.arg.gcp_output_file.help": "用于缓存 token 输出的可选写入路径", + "warp_cli.federate.arg.gcp_token_type.help": "请求的 token 类型(例如 \"urn:ietf:params:oauth:token-type:id_token\")", + "warp_cli.federate.arg.run_id.help": "要为其签发 token 的 run ID", + "warp_cli.federate.arg.subject_template.help": "控制 OIDC token subject 的格式", + "warp_cli.federate.arg.subject_template.long_help": "控制 OIDC token subject 的格式。\n\n模板由一组 claims 组成,这些 claims 会拼接成 subject。默认 subject template 是 principal,例如 `user:user-id`。\n\n支持的 components:\n- principal (`user:my-user-id`)\n- scoped_principal (`principal:my-team-id/user:my-user-id`)\n- email (`email:user@warp.dev`)\n- teams (`teams:my-team-id`)\n- environment (`environment:my-environment-id`)\n- agent_name (`agent_name:my-agent`)\n- skill_spec (`skill_spec:warpdotdev/repo_path_to_skill`)\n- run_id (`run_id:abc123`)\n- host (`host:my-worker-id`)", + "warp_cli.harness_support.arg.error_category.help": "异常 shutdown 的错误类别", + "warp_cli.harness_support.arg.error_message.help": "异常 shutdown 的可读错误消息", + "warp_cli.harness_support.arg.message.help": "作为进度更新发送的消息", + "warp_cli.harness_support.arg.pull_request.branch.help": "与 pull request 关联的 branch 名称", + "warp_cli.harness_support.arg.pull_request.url.help": "pull request 的 URL", + "warp_cli.harness_support.arg.run_id.help": "与 harness-support API 调用关联的 run ID", + "warp_cli.harness_support.arg.status.help": "任务是成功还是失败", + "warp_cli.harness_support.arg.summary.help": "任务结果 summary", + "warp_cli.integration.arg.environment.update.help": "此 integration 的替换云环境", + "warp_cli.integration.arg.mcp_specs.help": "为此 integration 配置的 MCP servers", + "warp_cli.integration.arg.mcp_specs.long_help": "为此 integration 配置的 MCP servers。\n\n可指定为:\n- 包含 MCP 配置的 JSON 文件路径\n- 内联 JSON MCP server 配置\n\n可多次指定以包含多个 servers。", + "warp_cli.integration.arg.prompt.help": "integration 的自定义指令", + "warp_cli.integration.arg.provider.create.help": "要为其创建 integration 的 provider", + "warp_cli.integration.arg.provider.update.help": "要更新其 integration 的 provider", + "warp_cli.integration.arg.remove_environment.help": "移除此 integration 的云环境", + "warp_cli.integration.arg.remove_mcp.help": "按 server 名称从此 integration 中移除 MCP servers", + "warp_cli.integration.arg.remove_mcp.long_help": "按 server 名称从此 integration 中移除 MCP servers。\n\n这会移除 key 与 `SERVER_NAME` 匹配的 server 条目。", + "warp_cli.integration.arg.worker_host.help": "self-hosted workers 的 worker host ID", + "warp_cli.integration.arg.worker_host.long_help": "self-hosted workers 的 worker host ID。\n\n如果未指定或设置为 \"warp\",任务会在 Warp-hosted workers 上运行。", + "warp_cli.mcp.arg.spec.help": "执行 Agent 前要启动的 MCP servers", + "warp_cli.mcp.arg.spec.long_help": "执行 Agent 前要启动的 MCP servers。\n\n可指定为:\n- 包含 MCP 配置的 JSON 文件路径\n- 内联 JSON MCP server 配置\n\n可多次指定以包含多个 servers。", + "warp_cli.mcp.error.invalid_utf8": "MCP spec 中包含无效 UTF-8", + "warp_cli.mcp.error.read_config_file_failed": "读取 MCP 配置文件 '{path}' 失败:{error}", + "warp_cli.mcp.possible.json": "内联 JSON MCP server 配置", + "warp_cli.mcp.possible.path": "包含 MCP config 的 JSON 文件路径", + "warp_cli.provider.arg.personal.help": "为个人账号设置 provider", + "warp_cli.provider.arg.provider_type.help": "要设置的 provider 类型", + "warp_cli.provider.arg.team.help": "为团队设置 provider", + "warp_cli.schedule.arg.cron.create.help": "Cron schedule 表达式(例如 \"0 9 * * 1\" 表示每周一上午 9 点)", + "warp_cli.schedule.arg.cron.update.help": "更新 Agent 执行所依据的 cron schedule", + "warp_cli.schedule.arg.environment.update.help": "此 schedule 的替换云环境", + "warp_cli.schedule.arg.mcp_specs.help": "为此 schedule 配置的 MCP servers", + "warp_cli.schedule.arg.mcp_specs.long_help": "为此 schedule 配置的 MCP servers。\n\n可指定为:\n- 包含 MCP 配置的 JSON 文件路径\n- 内联 JSON MCP server 配置\n\n可多次指定以包含多个 servers。", + "warp_cli.schedule.arg.name.create.help": "定时 Agent 名称", + "warp_cli.schedule.arg.name.update.help": "更新定时 Agent 名称", + "warp_cli.schedule.arg.prompt.create.help": "定时 Agent 要执行的 prompt", + "warp_cli.schedule.arg.prompt.update.help": "更新定时 Agent 的 prompt", + "warp_cli.schedule.arg.remove_environment.help": "移除此 schedule 的云环境", + "warp_cli.schedule.arg.remove_mcp.help": "按 server 名称从此 schedule 中移除 MCP servers", + "warp_cli.schedule.arg.remove_mcp.long_help": "按 server 名称从此 schedule 中移除 MCP servers。\n\n这会移除 key 与 `SERVER_NAME` 匹配的 server 条目。", + "warp_cli.schedule.arg.remove_skill.help": "从此定时 Agent 中移除 skill", + "warp_cli.schedule.arg.schedule_id.delete.help": "要删除的 schedule ID", + "warp_cli.schedule.arg.schedule_id.get.help": "要获取的 schedule ID", + "warp_cli.schedule.arg.schedule_id.pause.help": "要暂停的 schedule ID", + "warp_cli.schedule.arg.schedule_id.unpause.help": "要恢复的 schedule ID", + "warp_cli.schedule.arg.schedule_id.update.help": "要更新的 schedule ID", + "warp_cli.schedule.arg.skill.create.help": "按 schedule 自动运行 skill", + "warp_cli.schedule.arg.skill.create.long_help": "按 schedule 自动运行 skill。\n\n格式:`repo:skill_name` 或 `org/repo:skill_name`\n\nSkills 会在 `.agents/skills/`、`.warp/skills/`、`.claude/skills/` 和 `.codex/skills/` 目录中查找。该 skill 会在 Agent 的云环境中于运行时解析。\n\n与 --prompt 一起使用时,skill 会提供基础上下文,prompt 是用户任务。这适合运行代码审查、依赖更新、报告等周期性 workflow。", + "warp_cli.schedule.arg.skill.update.help": "更新定时 Agent 用作基础 prompt 的 skill", + "warp_cli.schedule.arg.skill.update.long_help": "更新定时 Agent 用作基础 prompt 的 skill。\n\n格式:`skill_name`、`repo:skill_name` 或 `org/repo:skill_name`\n\nSkills 会在 `.agents/skills/`、`.warp/skills/`、`.claude/skills/` 和 `.codex/skills/` 目录中查找。该 skill 会在 Agent 的云环境中于运行时解析。", + "warp_cli.schedule.arg.worker_host.help": "此 job 的托管位置", + "warp_cli.schedule.arg.worker_host.long_help": "此 job 的托管位置。\n\n设置为 \"warp\" 时会在 Warp 基础设施上运行。其他值会被视为 self-hosted job,并与 self-hosted worker 的名称匹配。", + "warp_cli.secret.arg.aws_access_key_id.help": "AWS access key ID。如果未提供,会进入交互式提示。", + "warp_cli.secret.arg.aws_secret_access_key.help": "AWS secret access key。如果未提供,会进入交互式提示。", + "warp_cli.secret.arg.aws_session_token.help": "AWS session token。如果未提供,会进入交互式提示。", + "warp_cli.secret.arg.bedrock_api_key.help": "Bedrock API key。如果未提供,会进入交互式提示。", + "warp_cli.secret.arg.bedrock_region.help": "Bedrock endpoint 的 AWS region。如果未提供,会进入交互式提示。", + "warp_cli.secret.arg.description.help": "secret 描述", + "warp_cli.secret.arg.description.update.help": "secret 的新描述。如果省略,则不更改描述。", + "warp_cli.secret.arg.force.delete.help": "删除前不要求确认", + "warp_cli.secret.arg.name.create.help": "secret 名称", + "warp_cli.secret.arg.name.delete.help": "要删除的 secret 名称", + "warp_cli.secret.arg.name.update.help": "要更新的 secret 名称", + "warp_cli.secret.arg.openai_base_url.help": "OpenAI API 的可选 base URL", + "warp_cli.secret.arg.openai_base_url.long_help": "OpenAI API 的可选 base URL(例如区域端点 `https://us.api.openai.com/v1`)。\n\n在交互模式下省略时,CLI 会提示输入;在提示处按 Enter 会跳过。在非交互模式下省略时,harness 会使用 provider 的默认 endpoint。", + "warp_cli.secret.arg.secret_type.help": "要创建的 secret 类型", + "warp_cli.secret.arg.value_file.help": "读取 secret 值的文件", + "warp_cli.secret.arg.value_file.long_help": "读取 secret 值的文件。\n\n如果未提供,secret 值会从标准输入读取。", + "warp_cli.secret.arg.value.update.help": "提示输入 secret 的新值", + "warp_cli.share.arg.help": "共享 Agent 会话", + "warp_cli.share.arg.long_help": "共享 Agent 会话。\n\n了解更多:https://docs.warp.dev/knowledge-and-collaboration/session-sharing", + "warp_cli.share.error.invalid_recipient": "无效的共享对象", + "warp_cli.share.error.invalid_subject": "无法共享给 '{subject}'。应为 'team'、'public' 或电子邮件地址", + "warp_cli.share.possible.public_edit": "共享给拥有链接的任何人,可编辑", + "warp_cli.share.possible.public_view": "共享给拥有链接的任何人,仅查看", + "warp_cli.share.possible.team_edit": "共享给你的团队,可编辑", + "warp_cli.share.possible.team_view": "共享给你的团队,仅查看", + "warp_cli.share.possible.user_edit": "共享给 ,可编辑", + "warp_cli.share.possible.user_view": "共享给 ,仅查看", + "warp_cli.skill.error.empty_identifier": "Skill identifier 不能为空", + "warp_cli.skill.error.empty_org": "Organization 不能为空", + "warp_cli.skill.error.empty_qualifier": "'repo:skill_identifier' 格式中的 qualifier 不能为空", + "warp_cli.skill.error.empty_repo": "Repository name 不能为空", + "warp_cli.skill.error.empty_specifier": "Skill specifier 不能为空", + "warp_cli.task.arg.ancestor_run.help": "筛选指定 run 的后代 runs", + "warp_cli.task.arg.artifact_type.help": "按生成的 artifact 类型筛选", + "warp_cli.task.arg.conversation_id.help": "要获取的 conversation ID", + "warp_cli.task.arg.conversation.help": "获取此 run 的 conversation,而不是 run 状态", + "warp_cli.task.arg.created_after.help": "仅包含在给定时间戳之后创建的 runs", + "warp_cli.task.arg.created_before.help": "仅包含在给定时间戳之前创建的 runs", + "warp_cli.task.arg.creator.help": "按创建者 ID 筛选", + "warp_cli.task.arg.cursor.help": "上一次列表响应返回的不透明分页 cursor", + "warp_cli.task.arg.cursor.long_help": "上一次列表响应返回的不透明分页 cursor。\n\n使用 `--cursor` 时,`--sort-by` 和 `--sort-order` 必须与获取该 cursor 时使用的值一致。", + "warp_cli.task.arg.environment.help": "按环境 ID 筛选", + "warp_cli.task.arg.execution_location.help": "按 run 执行位置筛选", + "warp_cli.task.arg.limit.help": "最多返回的 run 数量(默认:10)", + "warp_cli.task.arg.message.body.help": "消息正文", + "warp_cli.task.arg.message.limit.help": "最多返回的消息数量(默认:50)", + "warp_cli.task.arg.message.message_id.mark_delivered.help": "要标记为已送达的 message ID", + "warp_cli.task.arg.message.message_id.read.help": "要读取的 message ID", + "warp_cli.task.arg.message.run_id.list.help": "要列出收件箱的 run ID", + "warp_cli.task.arg.message.run_id.watch.help": "要监听收件箱的 run ID", + "warp_cli.task.arg.message.sender_run_id.help": "发送方 run ID", + "warp_cli.task.arg.message.since_sequence.help": "从此事件序列之后恢复(用于重连的包含式 cursor)", + "warp_cli.task.arg.message.since.help": "仅返回在此 RFC3339 时间戳或之后发送的消息", + "warp_cli.task.arg.message.subject.help": "消息主题", + "warp_cli.task.arg.message.to.help": "目标 run ID。重复此 flag 可发送给多个目标。", + "warp_cli.task.arg.message.unread.help": "仅返回未读消息", + "warp_cli.task.arg.model.help": "按模型 ID 筛选", + "warp_cli.task.arg.name.help": "按 Agent config 名称筛选", + "warp_cli.task.arg.query.help": "在 run 标题、prompt 和 skill spec 中进行模糊搜索", + "warp_cli.task.arg.schedule.help": "筛选由指定 scheduled agent 创建的 runs", + "warp_cli.task.arg.skill.help": "按 skill 筛选(例如 `owner/repo:path/to/SKILL.md`)", + "warp_cli.task.arg.sort_by.help": "排序字段", + "warp_cli.task.arg.sort_order.help": "排序方向", + "warp_cli.task.arg.source.help": "按 run 来源筛选", + "warp_cli.task.arg.state.help": "按 run 状态筛选。重复此 flag 可匹配多个状态中的任意一个。", + "warp_cli.task.arg.task_id.help": "要获取状态的 run ID", + "warp_cli.task.arg.updated_after.help": "仅包含在给定时间戳之后更新的 runs", + "warp_cli.worker.arg.minidump_socket_name.help": "minidump server 的 socket 名称", + "warp_cli.worker.arg.ripgrep_ignore_case.help": "忽略大小写搜索", + "warp_cli.worker.arg.ripgrep_multiline.help": "允许匹配跨越多行", + "warp_cli.worker.arg.ripgrep_paths.help": "要搜索的路径", + "warp_cli.worker.arg.ripgrep_pattern.help": "搜索 pattern", + "wasm_nux.always_open_on_web_title": "始终在网页端打开{object_kind}?", + "wasm_nux.change_in_settings": "你可以随时在设置中更改此选项。", + "wasm_nux.download_desktop_description": "Warp 是一款智能终端,内置 AI 和开发团队知识。", + "wasm_nux.download_desktop_title": "下载 Warp Desktop?", + "wasm_nux.future_links_desktop": "之后的链接将自动在桌面端打开。", + "wasm_nux.object_kind.drive_objects": "Warp Drive 对象", + "wasm_nux.object_kind.shared_sessions": "共享会话", + "wasm_nux.object_kind.warp_links": "Warp 链接", + "wasm_nux.open_in_desktop_title": "在 Warp Desktop 中打开?", + "wasm_nux.yes": "是", + "workflow.toast.out_of_ai_credits": "你的 AI 额度似乎已用完。", + "workflow.toast.out_of_ai_credits_contact_admin": "你的 AI 额度似乎已用完。请联系团队管理员升级以获取更多额度。", + "workflow.toast.upgrade_for_more_credits": "升级以获取更多额度。", + "workflows.alias.add_alias": "添加别名", + "workflows.alias.name_placeholder": "别名名称", + "workflows.argument.add_tooltip": "添加 workflow 参数", + "workflows.arguments.add_environment_variables": "添加环境变量", + "workflows.arguments.alias_value_placeholder": "值(可选)", + "workflows.arguments.description": "填写此工作流中的参数,并复制到终端会话中运行", + "workflows.arguments.environment_variables": "环境变量", + "workflows.arguments.new_environment_variables": "新建环境变量", + "workflows.arguments.section": "参数", + "workflows.categories.a11y.help": "搜索,或使用上下箭头导航并查找 workflow。按 Enter 确认 workflow,按 Esc 退出。", + "workflows.categories.a11y.selected": "已选择 {name} {content}", + "workflows.categories.a11y.showing_all": "正在显示全部 workflows", + "workflows.categories.a11y.showing_category": "正在显示 {category} workflows", + "workflows.categories.a11y.showing_mine": "正在显示我的 Workflows", + "workflows.categories.a11y.showing_project": "正在显示仓库 Workflows", + "workflows.categories.a11y.showing_team": "正在显示团队 Workflows", + "workflows.categories.a11y.title": "Workflows", + "workflows.categories.all": "全部", + "workflows.categories.create_your_own": "创建自己的 workflow", + "workflows.categories.label": "分类", + "workflows.categories.my_workflows": "我的 Workflows", + "workflows.categories.no_matching_workflows": "未找到匹配的 workflows。", + "workflows.categories.repository_workflows": "仓库 Workflows", + "workflows.categories.team_workflows": "团队 Workflows", + "workflows.editor.access_removed": "你不再有权访问此 workflow", + "workflows.editor.agent_prompt_placeholder": "在这里输入你的提示...(例如:“Create a function to sort an array of objects by date” 或 “Help me debug this React component”)。", + "workflows.editor.alias_help_tooltip": "别名可让你创建短字符串来执行工作流。每个别名可以使用不同的参数值和环境变量,且只对你个人生效。", + "workflows.editor.aliases_section": "别名", + "workflows.editor.autofill": "自动填充", + "workflows.editor.autofill_tooltip": "使用 Warp AI 生成标题、描述或参数", + "workflows.editor.cannot_save_with_secrets": "此工作流包含密钥,无法保存", + "workflows.editor.command_copied": "命令已复制。", + "workflows.editor.command_placeholder": "echo \"Hello {{your_name}}\" # 使用花括号插入参数\n# 请输入单行命令或完整的 shell 脚本", + "workflows.editor.could_not_create": "无法创建工作流", + "workflows.editor.create": "创建", + "workflows.editor.description_placeholder": "添加描述", + "workflows.editor.discard_changes": "放弃更改", + "workflows.editor.error_saving_aliases": "保存别名出错", + "workflows.editor.keep_editing": "继续编辑", + "workflows.editor.loading": "加载中", + "workflows.editor.moved_to_trash": "Workflow 已移至废纸篓", + "workflows.editor.prompt_copied": "提示词已复制。", + "workflows.editor.run_in_warp": "在 Warp 中运行", + "workflows.editor.title_placeholder": "添加标题", + "workflows.editor.unsaved_changes": "你有未保存的更改。", + "workflows.editor.update": "更新", + "workflows.enum.dynamic": "动态", + "workflows.enum.dynamic_placeholder": "# 输入一个生成变体的 shell 命令,每行输出一个变体。\n\ngit branch -a", + "workflows.enum.edit_title": "编辑 enum", + "workflows.enum.name_placeholder": "名称", + "workflows.enum.new_title": "新建 enum", + "workflows.enum.static": "静态", + "workflows.enum.variant_placeholder": "变体", + "workflows.enum.variants": "变体", + "workflows.info.command_edited": "命令已编辑。", + "workflows.info.cycle_parameters": "切换参数", + "workflows.info.edit_prompt": "编辑提示词", + "workflows.info.edit_workflow": "编辑 workflow", + "workflows.info.save_as_workflow": "另存为工作流", + "workflows.info.view_context": "查看上下文", + "workflows.modal.argument_default_value_placeholder": "默认值(可选)", + "workflows.modal.argument_description_placeholder": "描述", + "workflows.modal.new_argument": "新建参数", + "workflows.modal.save_workflow": "保存工作流", + "workflows.modal.title_placeholder": "未命名工作流", + "workflows.restore_from_trash": "从废纸篓恢复 workflow", + "workspace.a11y.announcements_set": "已设置 {verbosity} 辅助功能播报", + "workspace.ask_ai_description": "让 Warp AI 解释错误、建议命令或编写脚本。", + "workspace.banner.fix_with_oz": "用 Oz 修复", + "workspace.banner.login_expired.description": "请重新登录以恢复对云端功能的访问。", + "workspace.banner.login_expired.heading": "你的登录状态已过期。", + "workspace.banner.more_info": "更多信息", + "workspace.banner.open_file": "打开文件", + "workspace.banner.out_of_date.description": "你的应用版本过旧,需要更新。", + "workspace.banner.restart_and_update": "重启应用并立即更新", + "workspace.banner.sign_in": "登录", + "workspace.banner.unable_to_launch.description": "Warp 无法启动新安装的版本。", + "workspace.banner.unable_to_update.description": "有新版本可用,但 Warp 无法执行更新。", + "workspace.banner.update_manually": "手动更新 Warp", + "workspace.banner.update_now": "立即更新", + "workspace.banner.version_deprecation": "你的应用版本过旧,部分功能可能无法正常工作。请立即更新。", + "workspace.banner.version_deprecation_without_permissions": "如果不立即更新,部分 Warp 功能可能无法正常工作,但 Warp 无法执行此次更新。", + "workspace.bonus_grant.reload_credits_added": "已向你的{scope}添加 {credits} 个 Reload Credits。", + "workspace.bonus_grant.scope.account": "账户", + "workspace.bonus_grant.scope.team": "团队", + "workspace.build_plan_migration.auto_reload_description": "自动充值会在账户余额降至 100 个积分时,按你选择的档位自动购买积分。每月消费上限将设为你旧套餐的月费,并且可在“设置 > 账单和用量”中更新。", + "workspace.build_plan_migration.auto_reload_failed": "启用自动充值失败。请尝试在“账单和用量”中更新设置。", + "workspace.build_plan_migration.auto_reload_title": "使用自动充值,避免中断。", + "workspace.build_plan_migration.get_started": "开始使用", + "workspace.build_plan_migration.saving": "正在保存...", + "workspace.build_plan_migration.team_data_not_found": "出了点问题,找不到你的团队数据。", + "workspace.build_plan.and_more": "更多内容...", + "workspace.build_plan.base_credits_per_month": "每月 {credits} 基础额度", + "workspace.build_plan.bring_your_own_api_key": "自带 API key", + "workspace.build_plan.features_header_build": "Build 包含:", + "workspace.build_plan.features_header_business": "新的 Business 套餐包含:", + "workspace.build_plan.intro_build": "你的工作区已更新到 Warp Build 套餐,旧版 Pro、Turbo 和 Lightspeed 套餐即将下线。", + "workspace.build_plan.intro_business": "你的工作区已更新到新的 Warp Business 套餐,旧版 Business 套餐即将下线。", + "workspace.build_plan.learn_more_prefix": "在我们的", + "workspace.build_plan.price_per_user_month": "每位用户每月 ${price}", + "workspace.build_plan.price_per_user_month_annual": "年度套餐每位用户每月 ${price}", + "workspace.build_plan.pricing_header_build": "Warp Build 是以用量为主的套餐,起价为:", + "workspace.build_plan.pricing_header_business": "新的 Business 套餐是以用量为主的套餐,起价为:", + "workspace.build_plan.pricing_page": "价格页面了解更多", + "workspace.build_plan.reload_credits_discounts": "可使用 Reload 额度和阶梯折扣", + "workspace.build_plan.saml_sso": "基于 SAML 的 SSO", + "workspace.build_plan.welcome_build": "欢迎使用 Warp Build", + "workspace.build_plan.welcome_business": "欢迎使用新的 Business 套餐", + "workspace.build_plan.zero_data_retention": "自动强制团队范围 Zero Data Retention", + "workspace.close_panel": "关闭面板", + "workspace.cloud_capacity.ai_credits_per_month": "每月 {credits} AI 额度", + "workspace.cloud_capacity.business_plan_includes": "Business 套餐包含你当前套餐的所有内容,并额外包含:", + "workspace.cloud_capacity.business_plan_starts": "Business 套餐起价为 ${price}/月,包含你当前套餐的所有内容,并额外包含:", + "workspace.cloud_capacity.concurrent_agents_multiplier": "并发云端 Agent 数量提升至 {multiplier}", + "workspace.cloud_capacity.concurrent_limit.description": "此云端运行已排队,因为你的团队已达到云端 Agent 并发数量上限。其他云端运行结束后,它会自动开始。", + "workspace.cloud_capacity.concurrent_limit.title": "已达到云端 Agent 并发上限", + "workspace.cloud_capacity.concurrent_limit.upgrade_suffix": "升级套餐以获得更多并发云端 Agent。", + "workspace.cloud_capacity.extended_ai_credits": "扩展 AI 月度额度", + "workspace.cloud_capacity.open_billing": "打开账单", + "workspace.cloud_capacity.out_of_credits.description": "此云端运行已停止,因为你的团队已用完当前账单周期的所有可用 AI 额度。", + "workspace.cloud_capacity.out_of_credits.title": "AI 额度已用完", + "workspace.cloud_capacity.out_of_credits.upgrade_suffix": "升级套餐以继续运行云端 Agent。", + "workspace.cloud_capacity.paid_plans_include": "付费套餐包含免费试用的所有内容,并额外包含:", + "workspace.cloud_capacity.paid_plans_start": "付费套餐起价为 ${price}/月,包含免费试用的所有内容,并额外包含:", + "workspace.cloud_capacity.upgrade_plan": "升级套餐", + "workspace.codex_modal.description_1": "Codex 是 OpenAI 面向真实工程的先进 agentic coding 模型。", + "workspace.codex_modal.description_2": "直接在 Oz 中使用 Codex,并利用应用内代码审查、agent 会话共享和文件编辑等功能。", + "workspace.codex_modal.title": "在 Warp 中使用 Codex 模型", + "workspace.codex_modal.use_latest_model": "使用最新 Codex 模型", + "workspace.conversation.default_title": "会话", + "workspace.conversation.linear_issue_title": "Linear 问题", + "workspace.conversation.new_conversation": "新对话", + "workspace.conversations.delete.in_progress_error": "会话进行中时无法删除。", + "workspace.conversations.empty.subtitle": "你与本地 Agent 和云端 Agent 的进行中会话和历史会话将显示在这里。", + "workspace.conversations.empty.title": "暂无会话", + "workspace.conversations.fallback_title": "会话", + "workspace.conversations.menu.delete": "删除", + "workspace.conversations.menu.delete_disabled_tooltip": "该会话无法删除", + "workspace.conversations.menu.fork_new_pane": "在新窗格中复刻", + "workspace.conversations.menu.fork_new_tab": "在新标签页中复刻", + "workspace.conversations.menu.share": "分享会话", + "workspace.conversations.new_conversation": "新建会话", + "workspace.conversations.no_matches": "没有匹配的会话", + "workspace.conversations.search_placeholder": "搜索", + "workspace.conversations.section.active": "进行中", + "workspace.conversations.section.past": "历史", + "workspace.conversations.show_less": "收起", + "workspace.conversations.view_all": "查看全部", + "workspace.crash_recovery.wayland.description": "我们检测到应用启动期间发生崩溃,并已将你的设置调整为使用 Xwayland 进行窗口显示。如果你使用了分数缩放,这可能导致文字变模糊。", + "workspace.dialog.cancel_button": "取消", + "workspace.dialog.close_session_button": "关闭会话", + "workspace.dialog.close_session_shared_body": "您即将关闭一个正在共享的会话。关闭后将结束所有人的共享。", + "workspace.dialog.close_session_title": "关闭会话?", + "workspace.dialog.delete_conversation_body": "此对话将被永久删除。此操作无法撤销。", + "workspace.dialog.delete_conversation_title": "删除对话?", + "workspace.dialog.delete_conversation_title_named": "删除“{title}”?", + "workspace.dialog.dont_show_again": "不再显示。", + "workspace.dialog.rewind.body": "确定要回溯吗?此操作会将你的代码和对话恢复到此处之前的状态,并取消 Agent 当前正在运行的所有命令。原对话的副本将保存在你的对话历史中。", + "workspace.dialog.rewind.button": "回溯", + "workspace.dialog.rewind.cancel": "取消", + "workspace.dialog.rewind.info": "回溯不会影响手动编辑或通过 shell 命令修改的文件。", + "workspace.dialog.rewind.title": "回溯", + "workspace.free_tier.access_to_prefix": "访问", + "workspace.free_tier.build_plan_includes": "Build 套餐包含免费套餐中的所有内容,并额外包含:", + "workspace.free_tier.build_plan_price": "Build 套餐价格为 ${price}/月,包含免费套餐中的所有内容,并额外包含:", + "workspace.free_tier.credits_per_month": "每月 {credits} 额度", + "workspace.free_tier.extended_cloud_agents_access": "扩展云端 agent 访问", + "workspace.free_tier.extended_credits": "扩展月度额度", + "workspace.free_tier.frontier_models": "可访问 OpenAI、Anthropic 和 Google 的前沿模型", + "workspace.free_tier.out_of_credits": "你的额度已用完", + "workspace.free_tier.reload_credits": "充值额度", + "workspace.free_tier.upgrade_plan": "升级计划", + "workspace.free_tier.upgrade_to_continue": "要继续使用 AI,请升级你的套餐。", + "workspace.handoff.command_running": "命令运行时无法交接。请取消命令或等待其完成。", + "workspace.handoff.create_environment_failed": "创建环境失败:{error}", + "workspace.handoff.local_save_failed": "无法在本地保存你的对话。请尝试再发送一条消息,然后重新交接。", + "workspace.handoff.moved_to_cloud_title": "{title}(已移至云端)", + "workspace.handoff.no_active_terminal": "没有可交接的活动终端会话。请聚焦一个窗格后重试。", + "workspace.handoff.not_available_for_orchestrated": "编排式 agent 对话不支持云端交接。", + "workspace.handoff.not_synced_yet": "你的对话尚未同步到云端。请尝试再发送一条消息,然后重新交接。", + "workspace.handoff.open_cloud_pane_failed": "无法打开用于交接的云端窗格。请重试;如果持续发生,请重启 Warp。", + "workspace.handoff.start_failed": "无法开始交接。请检查网络连接后重试。", + "workspace.hoa.agent_inbox_description": "Warp 会把任何 CLI coding agent 的通知汇入统一通知中心,跨所有 coding agent 和 harness 工作。", + "workspace.hoa.agent_inbox_title": "认识你的新 agent 收件箱", + "workspace.hoa.feature.agent_inbox.description": "当任何 Agent 需要你关注时发送通知,也可在统一收件箱中查看", + "workspace.hoa.feature.agent_inbox.title": "Agent 收件箱", + "workspace.hoa.feature.native_code_review.description": "直接从 Warp 的 code review 向 Claude Code、Codex 或 OpenCode 发送行内评论", + "workspace.hoa.feature.native_code_review.title": "原生 code review", + "workspace.hoa.feature.tab_configs.description": "用一键式标签页配置设置目录、启动命令、主题和 worktree", + "workspace.hoa.feature.tab_configs.title": "标签页配置", + "workspace.hoa.feature.vertical_tabs.description": "丰富的标签页标题和元数据,如 Git 分支、worktree 和 PR,并且完全可自定义。", + "workspace.hoa.feature.vertical_tabs.title": "垂直标签页", + "workspace.hoa.see_whats_new": "查看新功能", + "workspace.hoa.switch_back_horizontal_tabs": "切回水平标签页", + "workspace.hoa.tab_config_description": "为标签页设置一个可复用的起点。选择仓库、会话类型,并可选择附加 worktree。以后需要用这套设置打开标签页时即可复用。", + "workspace.hoa.vertical_tabs_callout.description": "垂直标签页会按标签页分组显示所有打开的 Agent 和终端窗格。你可以自定义要展示的信息,以贴合自己的工作流。", + "workspace.hoa.vertical_tabs_callout.title": "介绍新默认体验:垂直标签页", + "workspace.hoa.welcome_title": "介绍通用 agent 支持:用 Warp 强化任何 coding agent", + "workspace.home.content": "\n欢迎使用网页版 Warp,你在浏览器中的 Warp 主页!\n你可以在网页版 Warp 中:\n* 加入共享会话\n* 创建、查看和编辑 Warp Drive 对象\n* 管理你的 Warp 设置\n\n尚未下载 Warp 的团队成员和同伴也可以使用网页版 Warp 查看你共享的会话、Notebook 和工作流。", + "workspace.home.title": "欢迎使用网页版 Warp", + "workspace.launch_modal.skip_for_now": "暂时跳过", + "workspace.launch_modal.sync_conversations_to_cloud": "将对话同步到云端", + "workspace.launch_modal.sync_conversations_to_cloud.description": "存储在云端的 Agent 对话可以一键分享给任何人,也可以跨设备或退出登录后继续。", + "workspace.left_panel.agent_conversations": "Agent 对话", + "workspace.left_panel.global_search": "全局搜索", + "workspace.left_panel.project_explorer": "项目浏览器", + "workspace.left_panel.warp_drive": "Warp Drive", + "workspace.menu.billing_and_usage": "账单与用量", + "workspace.menu.current_version_prefix": "当前版本:", + "workspace.menu.documentation": "文档", + "workspace.menu.feedback": "反馈", + "workspace.menu.install_update": "安装更新", + "workspace.menu.invite_a_friend": "邀请好友", + "workspace.menu.keyboard_shortcuts": "键盘快捷键", + "workspace.menu.log_out": "退出登录", + "workspace.menu.new_tab_config": "新建标签页配置", + "workspace.menu.new_tab_group": "新建标签页分组", + "workspace.menu.new_worktree_config": "新建工作树配置", + "workspace.menu.rearrange_toolbar_items": "重新排列工具栏项目", + "workspace.menu.reopen_closed_session": "重新打开已关闭的会话", + "workspace.menu.settings": "设置", + "workspace.menu.sign_up": "注册", + "workspace.menu.update_and_relaunch": "更新并重新启动 Warp", + "workspace.menu.update_manually": "手动更新 Warp", + "workspace.menu.updating_to": "正在更新到", + "workspace.menu.upgrade": "升级", + "workspace.menu.view_logs": "查看 Warp 日志", + "workspace.menu.whats_new": "新增内容", + "workspace.modal.new_api_key": "新建 API 密钥", + "workspace.modal.open_tab_config": "打开:{name}", + "workspace.new_session.cloud_agent": "云端 Agent", + "workspace.new_session.local_docker_sandbox": "本地 Docker 沙盒", + "workspace.new_session.terminal": "终端", + "workspace.opencode.failed_create_config_dir": "创建配置目录失败:{error}", + "workspace.opencode.failed_home_dir": "无法确定 home 目录", + "workspace.opencode.failed_parse": "解析 opencode.json 失败:{error}", + "workspace.opencode.failed_read": "读取 opencode.json 失败:{error}", + "workspace.opencode.failed_serialize": "序列化 opencode.json 失败:{error}", + "workspace.opencode.failed_write": "写入 opencode.json 失败:{error}", + "workspace.opencode.plugin_set": "OpenCode 插件已设置为:{entry}", + "workspace.opencode.unexpected_structure": "opencode.json 结构异常(plugin 不是数组)", + "workspace.openwarp_launch.description": "你和我们的社区可以用 agent-first 工作流参与构建 Warp。", + "workspace.openwarp_launch.feature.auto_open_weights.description": "我们新增了一个 auto 模型,它会为任务选择最佳 open-weight 模型,例如 Kimi 或 MiniMax。", + "workspace.openwarp_launch.feature.auto_open_weights.title": "介绍 'auto (open-weights)'", + "workspace.openwarp_launch.feature.contribute.description": "Warp 客户端代码现已开源。你可以使用 /feedback skill 提交 issue,并在这里查看贡献指南。", + "workspace.openwarp_launch.feature.contribute.link_text": "这里", + "workspace.openwarp_launch.feature.contribute.title": "参与贡献", + "workspace.openwarp_launch.feature.open_automated_development.description": "Warp repo 由 agent-first 工作流管理,并由我们的云端 Agent 编排平台 Oz 提供支持。", + "workspace.openwarp_launch.feature.open_automated_development.link_text": "Oz", + "workspace.openwarp_launch.feature.open_automated_development.title": "开放自动化开发", + "workspace.openwarp.title": "Warp 现已开源", + "workspace.openwarp.visit_repo": "访问仓库", + "workspace.orchestration_launch.description": "我们对 Warp 的云端 agent 编排平台 Oz 做了重大改进。", + "workspace.orchestration_launch.feature.agent_memory.badge": "研究预览", + "workspace.orchestration_launch.feature.agent_memory.description": "Agent 现在可以存储和访问长期记忆,从而随时间自我改进。", + "workspace.orchestration_launch.feature.agent_memory.title": "Agent 记忆", + "workspace.orchestration_launch.feature.cloud_harness.description": "使用 Oz 在云端启动 Claude Code 或 Codex Agent;Oz 会帮你跟踪和引导这些 Agent。", + "workspace.orchestration_launch.feature.cloud_harness.title": "在云端运行任何 Agent harness", + "workspace.orchestration_launch.feature.multi_agent.description": "Warp Agent 现在会编排多个 subagent,让你可以并行处理任务。", + "workspace.orchestration_launch.feature.multi_agent.title": "多 Agent 编排", + "workspace.orchestration_launch.title": "随时随地编排任何 agent", + "workspace.oz_launch.agent_automations.content": "Oz Agent 可以使用标准 Skills 格式定义。你可以使用内置调度器让 Agent 按固定间隔自主运行,也可以使用 Oz SDK 或 API 以编程方式启动和管理 Oz Agent。", + "workspace.oz_launch.agent_automations.title": "编排 Agent,将 Skills 变成自动化任务", + "workspace.oz_launch.agent_management.content": "在 Warp 应用或 [oz.warp.dev](https://oz.warp.dev) 查看本地和云端会话中的所有 Agent。加入实时 Agent 会话,在本地继续任务,并一键引导 Agent。", + "workspace.oz_launch.agent_management.title": "无缝跟踪本地和云端 Agent", + "workspace.oz_launch.cloud_agents.content": "使用云端 Agent 并行运行多个 Agent,在合上笔记本后保持 Agent 继续工作,或以编程方式启动 Agent。此外,你还可以通过网页查看它们的工作进展。", + "workspace.oz_launch.cloud_agents.title": "用云端 Agent 突破本机限制", + "workspace.oz_launch.description": "可无限扩展的 coding agent,可在本地会话或云端运行。", + "workspace.oz_launch.launch_credits.content": "本月升级到 Build 即可获得 1,000 个额外额度来试用 Oz。额度仅适用于 Warp 托管云端环境中的 Oz 运行。", + "workspace.oz_launch.launch_credits.title": "升级到 Warp Build 即可获得 1,000 个免费云端 Agent 额度", + "workspace.oz_launch.modal_title": "介绍 Oz", + "workspace.oz_launch.next_button": "下一步:{label}", + "workspace.oz_launch.slide.agent_automations": "Agent 自动化", + "workspace.oz_launch.slide.agent_management": "Agent 管理", + "workspace.oz_launch.slide.cloud_agents": "云端 Agent", + "workspace.oz_launch.slide.gift": "一份小礼物", + "workspace.oz_launch.slide.launch_credits": "Launch 额度", + "workspace.oz_launch.try_it_out": "试试看", + "workspace.pane_menu.rename_active_pane": "重命名活动窗格", + "workspace.pane_menu.rename_pane": "重命名窗格", + "workspace.pane_menu.reset_active_pane_name": "重置活动窗格名称", + "workspace.pane_menu.reset_pane_name": "重置窗格名称", + "workspace.pane.untitled": "未命名窗格", + "workspace.repos.add_new": "添加新仓库", + "workspace.repos.search_placeholder": "搜索仓库", + "workspace.right_panel.close_panel.tooltip": "关闭面板", + "workspace.right_panel.code_review.title": "代码审查", + "workspace.right_panel.maximize.tooltip": "最大化", + "workspace.right_panel.minimize.tooltip": "还原", + "workspace.right_panel.open_repository.label": "打开仓库", + "workspace.right_panel.open_repository.tooltip": "进入某个仓库并初始化以开始编码", + "workspace.right_panel.repo.unknown": "未知", + "workspace.search.capped": "结果集仅包含部分匹配项。请让搜索更具体,以缩小结果范围。", + "workspace.search.failed": "全局搜索失败。", + "workspace.search.label": "搜索", + "workspace.search.no_results": "未找到任何结果。请检查你的 gitignore 文件。", + "workspace.search.placeholder": "在文件中搜索", + "workspace.search.results_count.multiple": "在 {files} 个文件中找到 {n} 条结果", + "workspace.search.results_count.single": "在 {files} 个文件中找到 1 条结果", + "workspace.search.title_bar_placeholder": "搜索会话、Agent、文件…", + "workspace.search.toggle_case_sensitivity": "切换区分大小写", + "workspace.search.toggle_regex": "切换正则表达式", + "workspace.search.unavailable.body": "全局搜索需要访问你的本地工作区。请打开一个新会话,或切换到一个活动会话以查看。", + "workspace.search.unavailable.remote_body": "全局搜索需要访问你的本地工作区,而远程会话不支持此功能", + "workspace.search.unavailable.title": "全局搜索不可用", + "workspace.search.unavailable.unsupported_body": "全局搜索目前无法在 Git Bash 或 WSL 中使用。", + "workspace.search.zero_state.body": "在当前目录下的文件中进行搜索。", + "workspace.search.zero_state.title": "全局搜索", + "workspace.tab_config_chip.text": "在这里访问你的标签页配置。", + "workspace.tab_title.install_update": "安装更新", + "workspace.tab_title.introducing_oz": "认识 Oz", + "workspace.tab_title.settings": "设置", + "workspace.tabs.badge.unsaved": "未保存", + "workspace.tabs.detail.more_open_tabs_prefix": "另外", + "workspace.tabs.detail.more_open_tabs_suffix": "个标签页", + "workspace.tabs.empty.no_match": "没有标签页符合你的搜索。", + "workspace.tabs.empty.no_tabs_open": "没有打开的标签页", + "workspace.tabs.group.tab_count_one": "1 个标签页", + "workspace.tabs.group.tab_count_other": "个标签页", + "workspace.tabs.group.untitled": "新建分组", + "workspace.tabs.kind.code": "代码", + "workspace.tabs.kind.code_diff": "代码差异", + "workspace.tabs.kind.env_var_collection": "环境变量", + "workspace.tabs.kind.environments": "环境", + "workspace.tabs.kind.execution_profile": "执行配置", + "workspace.tabs.kind.file": "文件", + "workspace.tabs.kind.notebook": "笔记本", + "workspace.tabs.kind.other": "其他", + "workspace.tabs.kind.plan": "计划", + "workspace.tabs.kind.rules": "规则", + "workspace.tabs.kind.settings": "设置", + "workspace.tabs.kind.terminal": "终端", + "workspace.tabs.kind.workflow": "工作流", + "workspace.tabs.search_placeholder": "搜索标签页…", + "workspace.tabs.settings.additional_metadata": "附加信息", + "workspace.tabs.settings.density": "密度", + "workspace.tabs.settings.granularity.panes": "窗格", + "workspace.tabs.settings.granularity.tabs": "标签页", + "workspace.tabs.settings.info.branch": "分支", + "workspace.tabs.settings.info.command": "命令 / 对话", + "workspace.tabs.settings.info.working_directory": "工作目录", + "workspace.tabs.settings.pane_title_as": "窗格标题显示为", + "workspace.tabs.settings.pr_link.requires_gh_cli": "需要已安装并完成认证的 GitHub CLI", + "workspace.tabs.settings.show": "显示", + "workspace.tabs.settings.show.details_on_hover": "悬停时显示详情", + "workspace.tabs.settings.show.diff_stats": "差异统计", + "workspace.tabs.settings.show.pr_link": "PR 链接", + "workspace.tabs.settings.tab_item": "标签项", + "workspace.tabs.settings.tab_item.focused_session": "聚焦的会话", + "workspace.tabs.settings.tab_item.summary": "摘要", + "workspace.tabs.settings.view_as": "显示方式", + "workspace.tabs.summary.overflow_more_suffix": "项", + "workspace.tabs.terminal.new_session": "新建会话", + "workspace.tabs.tooltip.tab_configs": "标签页配置", + "workspace.tabs.tooltip.view_options": "视图选项", + "workspace.tabs.untitled_tab": "未命名标签页", + "workspace.toast.cannot_open_terminal": "无法打开新的终端会话", + "workspace.toast.checkout_latest_retry": "请检出最新版本后重试。", + "workspace.toast.cli_installed_prefix": "Oz CLI 已成功安装!现在可以在命令行运行「", + "workspace.toast.cli_installed_suffix": "」。", + "workspace.toast.command_still_running": "此会话中仍有命令正在运行。", + "workspace.toast.conversation_deleted": "会话已删除", + "workspace.toast.conversation_fork_failed": "对话 fork 失败。", + "workspace.toast.create_log_bundle_failed": "创建日志包失败:{error}", + "workspace.toast.delete_conversation_failed": "删除会话失败。请退出 Agent 视图后重试。", + "workspace.toast.disabled_synced_inputs": "已停用所有同步输入。", + "workspace.toast.failed_to_load_conversation_for_forking": "加载用于派生的对话失败。", + "workspace.toast.forked_prefix": "已复刻 ", + "workspace.toast.learn_more": "了解更多", + "workspace.toast.load_conversation_failed": "加载对话失败。", + "workspace.toast.mouse_reporting_disabled": "已停用鼠标上报。", + "workspace.toast.mouse_reporting_enabled": "已启用鼠标上报。", + "workspace.toast.no_notification_permission": "Warp 没有发送桌面通知的权限。", + "workspace.toast.no_terminal_pane": "没有打开的终端窗格。请打开新窗格以作为上下文附加。", + "workspace.toast.out_of_credits": "你的 AI 积分似乎已用完。", + "workspace.toast.oz_command_install_failed": "安装 Oz 命令失败:{error}", + "workspace.toast.oz_command_uninstall_failed": "卸载 Oz 命令失败:{error}", + "workspace.toast.oz_command_uninstalled": "已成功卸载 Oz 命令。", + "workspace.toast.plan_already_in_context": "此计划已在上下文中。", + "workspace.toast.plan_synced": "计划已同步到你的 Warp Drive", + "workspace.toast.press_to_undo": " 按 {key} 撤销。", + "workspace.toast.process_sample_failed": "进程采样失败(请查看日志)", + "workspace.toast.process_sample_saved": "进程采样已保存到 {path}", + "workspace.toast.remote_control_link_copied": "远程控制链接已复制。", + "workspace.toast.remove_tab_config_failed": "移除标签页配置失败:{error}", + "workspace.toast.resource_not_found": "未找到资源或访问被拒绝", + "workspace.toast.sampling_process": "正在采样进程 3 秒...", + "workspace.toast.staging_api_call_failed": "Staging API 调用失败。你的 IP 地址是否发生了变化?", + "workspace.toast.starting_cloud_environment": "正在为此会话启动云端环境…", + "workspace.toast.synced_inputs_all_tabs_disabled": "已停用所有标签页中的同步输入。", + "workspace.toast.synced_inputs_all_tabs_enabled": "已启用所有标签页中的同步输入。", + "workspace.toast.synced_inputs_tab_disabled": "已停用此标签页中的同步输入。", + "workspace.toast.synced_inputs_tab_enabled": "已启用此标签页中的同步输入。", + "workspace.toast.troubleshoot_notifications": "通知故障排查", + "workspace.toast.upgrade_for_credits": "升级以获得更多积分。", + "workspace.toast.view": "查看", + "workspace.toast.view_changelog": "查看更新日志", + "workspace.toast.warp_updated": "Warp 已更新!", + "workspace.toast.workflow_unavailable": "此工作流不再可用。", + "workspace.toolbar.available_items": "可用项", + "workspace.toolbar.item.agent_management": "Agent 管理", + "workspace.toolbar.item.code_review": "Code Review", + "workspace.toolbar.item.notifications": "通知", + "workspace.toolbar.item.tabs_panel": "标签页面板", + "workspace.toolbar.item.tools_panel": "工具面板", + "workspace.toolbar.modal_title": "编辑工具栏", + "workspace.tooltip.agent_conversations": "Agent 会话", + "workspace.tooltip.agent_management_panel": "Agent 管理面板", + "workspace.tooltip.code_review_panel": "代码审查面板", + "workspace.tooltip.global_search": "全局搜索", + "workspace.tooltip.new_tab": "新建标签页", + "workspace.tooltip.notifications": "通知", + "workspace.tooltip.offline": "离线时部分功能可能不可用", + "workspace.tooltip.project_explorer": "项目资源管理器", + "workspace.tooltip.settings": "设置", + "workspace.tooltip.tab_configs": "标签页配置", + "workspace.tooltip.tabs_panel": "标签页面板", + "workspace.tooltip.tools_panel": "工具面板", + "workspace.tooltip.warp_drive": "Warp Drive", + "workspace.tooltip.warp_essentials": "Warp 入门", + "workspace.update.update_warp": "更新 Warp", + "workspace.user.default_display_name": "用户", + "workspace.wasm.local_network_access_required": "已安装 Warp 但被重定向到下载页面?\n请在浏览器中为 {server} 启用本地网络访问权限。", + "workspace.wasm.view_all_cloud_runs": "查看所有云端运行记录", + "workspace.workflow.command_from_oz": "来自 Oz 的命令", + "workspace.workflow.command_from_warp_ai": "来自 Warp AI 的命令", + "workspace.worktree.config_name": "Worktree:{repo}", + "workspace.worktree.new": "新建 worktree:{repo}", + "workspace.worktree.new_with_branch": "新建 worktree:{repo},{branch}", + "workspace.worktree.new_with_name": "新建 worktree:{repo},{name}" +} diff --git a/crates/i18n/src/lib.rs b/crates/i18n/src/lib.rs new file mode 100644 index 0000000000..3c6a015b6e --- /dev/null +++ b/crates/i18n/src/lib.rs @@ -0,0 +1,193 @@ +//! Lightweight internationalization (i18n) for the Warp UI. +//! +//! Translation catalogs are embedded from the `locales/` directory at build +//! time (one flat JSON `key -> string` file per locale, e.g. `en.json`, +//! `zh-CN.json`) using `rust-embed`, mirroring the catalog approach already +//! used by the `languages` crate. A single active locale is held in a global +//! [`RwLock`] so any UI code can translate a key with the free function +//! [`t`] without threading a context through every call site. +//! +//! # Usage +//! +//! ```ignore +//! // At startup, after reading the saved language preference: +//! i18n::set_locale("zh-CN"); +//! +//! // Anywhere in the UI — returns an owned `String`, which satisfies +//! // `impl Into>` accepted by `Text::new(..)` and friends: +//! let label = i18n::t("settings.appearance.language.label"); +//! // or via the macro: +//! let label = i18n::t!("settings.appearance.language.label"); +//! ``` +//! +//! # Fallback +//! +//! Lookups degrade gracefully: the active locale's catalog is built by layering +//! the more specific file on top of less specific ones and the English base, +//! e.g. `zh-CN` resolves through `en -> zh -> zh-CN`. A key missing from every +//! catalog returns the key itself, so the UI never panics or shows a blank. + +use std::collections::HashMap; +use std::sync::RwLock; + +use once_cell::sync::Lazy; +use rust_embed::RustEmbed; + +/// The catalog whose translations are considered the source of truth and the +/// final fallback for any key missing from the active locale. +pub const FALLBACK_LOCALE: &str = "en"; + +/// The locale selected on first run, before the user picks one. Chinese per +/// product requirement; keep in sync with `Language`'s `#[default]` in the app. +pub const DEFAULT_LOCALE: &str = "zh-CN"; + +#[derive(RustEmbed)] +#[folder = "locales"] +struct Locales; + +/// A flat `key -> translated string` catalog for a single locale. +type Catalog = HashMap; + +struct State { + /// BCP-47-ish tag of the active locale, e.g. `"zh-CN"`. + active_tag: String, + /// Active catalog: the English base with the active locale layered on top, + /// so every key present in `en.json` resolves even if untranslated. + active: Catalog, +} + +static STATE: Lazy> = Lazy::new(|| { + RwLock::new(State { + active_tag: DEFAULT_LOCALE.to_string(), + active: build_active(DEFAULT_LOCALE), + }) +}); + +/// Loads and parses the embedded catalog for `tag` (without the `.json` +/// suffix). Returns an empty catalog if the file is absent or malformed — +/// missing/invalid catalogs must never crash the UI. +fn load_catalog(tag: &str) -> Catalog { + let path = format!("{tag}.json"); + match Locales::get(&path) { + Some(file) => serde_json::from_slice(&file.data).unwrap_or_else(|err| { + log::error!("i18n: failed to parse locale catalog `{path}`: {err}"); + Catalog::new() + }), + None => Catalog::new(), + } +} + +/// Returns the catalog file stems to layer for `tag`, least specific first, so +/// later entries override earlier ones. +/// +/// `"zh-CN"` -> `["en", "zh", "zh-CN"]`; `"en"` -> `["en"]`. +fn merge_order(tag: &str) -> Vec { + let mut order = vec![FALLBACK_LOCALE.to_string()]; + if let Some((base, _region)) = tag.split_once('-') { + if base != FALLBACK_LOCALE { + order.push(base.to_string()); + } + } + if tag != FALLBACK_LOCALE && !order.iter().any(|stem| stem == tag) { + order.push(tag.to_string()); + } + order +} + +/// Builds the merged active catalog for `tag` (English base + locale overrides). +fn build_active(tag: &str) -> Catalog { + let mut merged = Catalog::new(); + for stem in merge_order(tag) { + for (key, value) in load_catalog(&stem) { + merged.insert(key, value); + } + } + merged +} + +/// Sets the active UI locale, rebuilding the active catalog. Idempotent for the +/// already-active tag. Call this at startup and whenever the language +/// preference changes; follow it with a full UI repaint so visible labels +/// update. +pub fn set_locale(tag: &str) { + let mut state = STATE + .write() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if state.active_tag == tag { + return; + } + state.active = build_active(tag); + state.active_tag = tag.to_string(); + log::info!("i18n: active locale set to `{tag}`"); +} + +/// Returns the tag of the currently active locale, e.g. `"zh-CN"`. +pub fn current_locale() -> String { + STATE + .read() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .active_tag + .clone() +} + +/// Translates `key` into the active locale. Returns the English fallback when +/// the active locale lacks the key, or the key itself when no catalog defines +/// it (so untranslated UI still renders something meaningful). +pub fn t(key: &str) -> String { + let state = STATE + .read() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + state + .active + .get(key) + .cloned() + .unwrap_or_else(|| key.to_string()) +} + +/// Ergonomic shorthand for [`t`]: `t!("some.key")`. +#[macro_export] +macro_rules! t { + ($key:expr $(,)?) => { + $crate::t($key) + }; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn merge_order_layers_region_over_base_over_english() { + assert_eq!(merge_order("zh-CN"), vec!["en", "zh", "zh-CN"]); + assert_eq!(merge_order("en"), vec!["en"]); + assert_eq!(merge_order("fr"), vec!["en", "fr"]); + } + + #[test] + fn missing_key_returns_key() { + set_locale("en"); + assert_eq!(t("this.key.does.not.exist"), "this.key.does.not.exist"); + } + + #[test] + fn default_locale_is_chinese() { + // The lazily-initialized state starts on the default locale. + assert_eq!(DEFAULT_LOCALE, "zh-CN"); + } + + #[test] + fn english_and_chinese_catalogs_have_the_same_keys() { + let en = load_catalog(FALLBACK_LOCALE); + let zh = load_catalog(DEFAULT_LOCALE); + + let mut missing_from_zh: Vec<_> = en.keys().filter(|key| !zh.contains_key(*key)).collect(); + let mut missing_from_en: Vec<_> = zh.keys().filter(|key| !en.contains_key(*key)).collect(); + missing_from_zh.sort(); + missing_from_en.sort(); + + assert!( + missing_from_zh.is_empty() && missing_from_en.is_empty(), + "locale catalogs are out of sync; missing from zh-CN: {missing_from_zh:?}; missing from en: {missing_from_en:?}" + ); + } +} diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 1fd0e90ce0..d61fbe9779 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -18,6 +18,7 @@ bin = ["warpui/log_named_telemetry_events"] ai.workspace = true anyhow.workspace = true cfg-if.workspace = true +i18n.workspace = true instant.workspace = true log.workspace = true pathfinder_color.workspace = true diff --git a/crates/onboarding/src/agent_onboarding_view.rs b/crates/onboarding/src/agent_onboarding_view.rs index 1b894d875b..dcedd0a04f 100644 --- a/crates/onboarding/src/agent_onboarding_view.rs +++ b/crates/onboarding/src/agent_onboarding_view.rs @@ -457,7 +457,7 @@ impl View for AgentOnboardingView { let close_button = self.close_button.render( appearance, button::Params { - content: button::Content::Label("Skip".into()), + content: button::Content::Label(i18n::t("common.skip").into()), theme: &button::themes::Naked, options: button::Options { size: button::Size::Small, diff --git a/crates/onboarding/src/callout/view.rs b/crates/onboarding/src/callout/view.rs index 0a633ae083..c17ca91748 100644 --- a/crates/onboarding/src/callout/view.rs +++ b/crates/onboarding/src/callout/view.rs @@ -30,7 +30,7 @@ use crate::OnboardingIntention; /// Options for rendering a callout. struct CalloutOptions { - title: &'static str, + title: String, /// Pre-built text with keybindings already embedded text: String, step: StepStatus, @@ -42,13 +42,13 @@ struct CalloutOptions { } struct ButtonOptions { - text: &'static str, + text: String, action: OnboardingCalloutViewAction, keystroke: Option, } struct CheckboxOptions { - label: &'static str, + label: String, checked: bool, } @@ -59,27 +59,25 @@ fn get_universal_input_callout_options( ) -> Option { match state { UniversalInputCalloutState::MeetInput => Some(CalloutOptions { - title: "Meet the Warp input", - text: format!( - "Your terminal input accepts both terminal commands and agent prompts and automatically detects which you're using. Use {} to lock the input to Agent mode (natural language) or Terminal mode (commands).", - keybindings.toggle_input_mode - ), + title: i18n::t("onboarding.callout.meet_input.title"), + text: i18n::t("onboarding.callout.meet_input.text") + .replace("{keybinding}", &keybindings.toggle_input_mode), step: StepStatus::new(0, 2), left_button: None, right_button: ButtonOptions { - text: "Next", + text: i18n::t("common.next"), action: OnboardingCalloutViewAction::NextClicked, keystroke: Some(Keystroke::parse("enter").unwrap_or_default()), }, checkbox: None, }), UniversalInputCalloutState::TalkToAgent => Some(CalloutOptions { - title: "Talk to the agent", - text: "You can type in natural language to engage the agent. Submit the query below to start: What tests exist in this repo, how are they structured, and what do they cover?".to_string(), + title: i18n::t("onboarding.callout.talk_to_agent.title"), + text: i18n::t("onboarding.callout.talk_to_agent.text"), step: StepStatus::new(1, 2), left_button: if has_project { Some(ButtonOptions { - text: "Skip", + text: i18n::t("common.skip"), action: OnboardingCalloutViewAction::SkipClicked, keystroke: Some(Keystroke::parse("delete").unwrap_or_default()), }) @@ -87,7 +85,11 @@ fn get_universal_input_callout_options( None }, right_button: ButtonOptions { - text: if has_project { "Submit" } else { "Finish" }, + text: if has_project { + i18n::t("onboarding.common.submit") + } else { + i18n::t("common.finish") + }, action: OnboardingCalloutViewAction::NextClicked, keystroke: Some(Keystroke::parse("enter").unwrap_or_default()), }, @@ -117,15 +119,17 @@ fn get_agent_modality_callout_options( if initial_natural_language_detection_enabled { // NL detection was already enabled - show simpler "overrides" callout without checkbox Some(CalloutOptions { - title: "Welcome to terminal mode", - text: format!( - "Run commands here, just like a regular terminal. If you type a question or task using natural language, Warp can suggest opening it in agent mode. You can always override using {}.", - keybindings.toggle_input_mode - ), + title: i18n::t("onboarding.callout.terminal_mode.welcome_title"), + text: i18n::t("onboarding.callout.terminal_mode.text") + .replace("{keybinding}", &keybindings.toggle_input_mode), step: StepStatus::new(0, total_steps), left_button: None, right_button: ButtonOptions { - text: if is_final_step { "Finish" } else { "Next" }, + text: if is_final_step { + i18n::t("common.finish") + } else { + i18n::t("common.next") + }, action: OnboardingCalloutViewAction::NextClicked, keystroke: Some(Keystroke::parse("enter").unwrap_or_default()), }, @@ -134,20 +138,22 @@ fn get_agent_modality_callout_options( } else { // NL detection was disabled - show full explanation with checkbox to enable Some(CalloutOptions { - title: "You’re in terminal mode", - text: format!( - "Run commands here, just like a regular terminal. If you type a question or task using natural language, Warp can suggest opening it in agent mode. You can always override using {}.", - keybindings.toggle_input_mode - ), + title: i18n::t("onboarding.callout.terminal_mode.in_terminal_title"), + text: i18n::t("onboarding.callout.terminal_mode.text") + .replace("{keybinding}", &keybindings.toggle_input_mode), step: StepStatus::new(0, total_steps), left_button: None, right_button: ButtonOptions { - text: if is_final_step { "Finish" } else { "Next" }, + text: if is_final_step { + i18n::t("common.finish") + } else { + i18n::t("common.next") + }, action: OnboardingCalloutViewAction::NextClicked, keystroke: Some(Keystroke::parse("enter").unwrap_or_default()), }, checkbox: Some(CheckboxOptions { - label: "Enable Natural Language Detection", + label: i18n::t("onboarding.callout.enable_natural_language_detection"), checked: natural_language_detection_enabled, }), }) @@ -156,16 +162,16 @@ fn get_agent_modality_callout_options( AgentModalityCalloutState::AgentMode => { if has_project { Some(CalloutOptions { - title: "You're in agent mode", - text: "Agent mode gives your questions and tasks their own conversation, so you can ask follow-ups without leaving your terminal workflow.\n\nSubmit the query below to have the agent initialize this project, or ⊗ to clear the input and start your own!".to_string(), + title: i18n::t("onboarding.callout.agent_mode.title"), + text: i18n::t("onboarding.callout.agent_mode.with_project_text"), step: StepStatus::new(1, total_steps), left_button: Some(ButtonOptions { - text: "Skip initialization", + text: i18n::t("onboarding.callout.skip_initialization"), action: OnboardingCalloutViewAction::SkipClicked, keystroke: Some(Keystroke::parse("delete").unwrap_or_default()), }), right_button: ButtonOptions { - text: "Initialize", + text: i18n::t("onboarding.callout.initialize"), action: OnboardingCalloutViewAction::NextClicked, keystroke: Some(Keystroke::parse("enter").unwrap_or_default()), }, @@ -173,19 +179,17 @@ fn get_agent_modality_callout_options( }) } else { Some(CalloutOptions { - title: "You're in agent mode", - text: format!( - "Agent mode gives your questions and tasks their own conversation, so you can ask follow-ups without leaving your terminal workflow. Press {} to return to terminal mode at any point.", - keybindings.return_to_terminal_mode - ), + title: i18n::t("onboarding.callout.agent_mode.title"), + text: i18n::t("onboarding.callout.agent_mode.no_project_text") + .replace("{keybinding}", &keybindings.return_to_terminal_mode), step: StepStatus::new(1, total_steps), left_button: Some(ButtonOptions { - text: "Back to terminal", + text: i18n::t("onboarding.callout.back_to_terminal"), action: OnboardingCalloutViewAction::BackToTerminalClicked, keystroke: Some(Keystroke::parse("escape").unwrap_or_default()), }), right_button: ButtonOptions { - text: "Finish", + text: i18n::t("common.finish"), action: OnboardingCalloutViewAction::NextClicked, keystroke: Some(Keystroke::parse("enter").unwrap_or_default()), }, @@ -429,7 +433,7 @@ impl View for OnboardingCalloutView { self.callout_component.render( appearance, onboarding_callout::Params { - title: options.title.to_string().into(), + title: options.title.into(), text: options.text.into(), step: options.step, right_button, diff --git a/crates/onboarding/src/components/onboarding_callout.rs b/crates/onboarding/src/components/onboarding_callout.rs index 0cc14caee9..88d9e839ea 100644 --- a/crates/onboarding/src/components/onboarding_callout.rs +++ b/crates/onboarding/src/components/onboarding_callout.rs @@ -92,7 +92,7 @@ pub struct Button { impl Button { pub fn next(handler: MouseEventHandler) -> Self { Self { - text: Cow::Borrowed("Next"), + text: Cow::Owned(i18n::t("common.next")), keystroke: Some(Keystroke { key: "enter".into(), ..Default::default() diff --git a/crates/onboarding/src/lib.rs b/crates/onboarding/src/lib.rs index 00016d98c6..5f952036c8 100644 --- a/crates/onboarding/src/lib.rs +++ b/crates/onboarding/src/lib.rs @@ -27,21 +27,28 @@ pub use callout::{OnboardingCalloutView, OnboardingKeybindings}; /// User-facing names of the AI features enabled when the agent intention is selected. /// Shared by the intention slide's agent card checklist and the login slide's /// skip-login confirmation dialog so the two always stay in sync. -pub const AI_FEATURES: &[&str] = &[ - "Warp agents", - "Oz cloud agents platform", - "Next command predictions", - "Prompt suggestions", - "Codebase context", - "Remote control with Claude Code, Codex, and other agents", - "Agents over SSH", -]; +pub fn ai_features() -> Vec { + vec![ + i18n::t("onboarding.features.warp_agents"), + i18n::t("onboarding.features.oz_cloud_agents_platform"), + i18n::t("onboarding.features.next_command_predictions"), + i18n::t("onboarding.features.prompt_suggestions"), + i18n::t("onboarding.features.codebase_context"), + i18n::t("onboarding.features.remote_control"), + i18n::t("onboarding.features.agents_over_ssh"), + ] +} /// User-facing names of the Warp Drive features enabled when the terminal /// intention is selected with Warp Drive turned on. Shared by the login slide's /// skip-login confirmation dialog so the list stays in sync with any future /// surfaces that need it. -pub const WARP_DRIVE_FEATURES: &[&str] = &["Warp Drive", "Session Sharing"]; +pub fn warp_drive_features() -> Vec { + vec![ + i18n::t("onboarding.features.warp_drive"), + i18n::t("onboarding.features.session_sharing"), + ] +} cfg_if::cfg_if! { if #[cfg(feature = "bin")] { diff --git a/crates/onboarding/src/slides/agent_slide.rs b/crates/onboarding/src/slides/agent_slide.rs index 41e1c0e379..c9dbc3ecff 100644 --- a/crates/onboarding/src/slides/agent_slide.rs +++ b/crates/onboarding/src/slides/agent_slide.rs @@ -319,7 +319,7 @@ impl AgentSlide { fn render_header(&self, appearance: &Appearance) -> Box { let title = appearance .ui_builder() - .paragraph("Customize your Warp Agent") + .paragraph(i18n::t("onboarding.agent.title")) .with_style(UiComponentStyles { font_size: Some(36.), font_weight: Some(Weight::Medium), @@ -329,7 +329,7 @@ impl AgentSlide { .finish(); let subtitle = FormattedTextElement::from_str( - "Select your in-app agent's defaults.", + i18n::t("onboarding.agent.subtitle"), appearance.ui_font_family(), 16., ) @@ -401,11 +401,7 @@ impl AgentSlide { Container::new(col.finish()).with_margin_top(40.).finish() } - fn render_section_header( - &self, - title: &'static str, - appearance: &Appearance, - ) -> Box { + fn render_section_header(&self, title: String, appearance: &Appearance) -> Box { appearance .ui_builder() .paragraph(title) @@ -424,7 +420,8 @@ impl AgentSlide { settings: &AgentDevelopmentSettings, app: &AppContext, ) -> Box { - let header = self.render_section_header("Default model", appearance); + let header = + self.render_section_header(i18n::t("onboarding.agent.default_model"), appearance); let expanded = self.is_model_list_expanded; let chip = self.render_collapsed_model_chip(appearance, settings, app, expanded); @@ -730,7 +727,7 @@ impl AgentSlide { // model, "Premium" on paywalled rows. In practice a single row is // at most one of these, but both can be shown side-by-side if the // default is also premium for any reason. - let make_pill = |label: &'static str| -> Box { + let make_pill = |label: String| -> Box { let badge = Text::new(label.to_string(), ui_font_family, 11.0) .with_color(internal_colors::text_sub(theme, background_for_text)) .with_style(Properties { @@ -750,9 +747,9 @@ impl AgentSlide { }; let trailing: Box = if is_default { - make_pill("Recommended") + make_pill(i18n::t("onboarding.agent.recommended")) } else if requires_upgrade { - make_pill("Premium") + make_pill(i18n::t("onboarding.agent.premium")) } else { Empty::new().finish() }; @@ -806,7 +803,7 @@ impl AgentSlide { } fn render_autonomy_workspace_enforced(&self, appearance: &Appearance) -> Box { - let header = self.render_section_header("Autonomy", appearance); + let header = self.render_section_header(i18n::t("onboarding.agent.autonomy"), appearance); let theme = appearance.theme(); let background_for_text = theme.background().into_solid(); @@ -815,17 +812,21 @@ impl AgentSlide { let title_color = internal_colors::text_main(theme, background_for_text); let subtitle_color = internal_colors::text_sub(theme, background_for_text); - let title_el = Text::new("Set by Team Workspace", ui_font_family, 14.0) - .with_color(title_color) - .with_style(Properties { - weight: Weight::Normal, - ..Default::default() - }) - .with_line_height_ratio(1.0) - .finish(); + let title_el = Text::new( + i18n::t("onboarding.agent.autonomy_set_by_team_workspace"), + ui_font_family, + 14.0, + ) + .with_color(title_color) + .with_style(Properties { + weight: Weight::Normal, + ..Default::default() + }) + .with_line_height_ratio(1.0) + .finish(); let subtitle_el = Text::new( - "Autonomy settings are configured as part of your team workspace.", + i18n::t("onboarding.agent.autonomy_team_workspace_description"), ui_font_family, 12.0, ) @@ -861,7 +862,7 @@ impl AgentSlide { appearance: &Appearance, settings: &AgentDevelopmentSettings, ) -> Box { - let header = self.render_section_header("Autonomy", appearance); + let header = self.render_section_header(i18n::t("onboarding.agent.autonomy"), appearance); // The rows now take the full column width (vs. the previous three-across layout), // so they no longer need the extra height that came from cramped subtitle wrapping. @@ -873,23 +874,23 @@ impl AgentSlide { let text_main = internal_colors::text_main(theme, background_for_text); let text_sub = internal_colors::text_sub(theme, background_for_text); - let autonomy_options: [(AgentAutonomy, &str, &str, MouseStateHandle); 3] = [ + let autonomy_options: [(AgentAutonomy, String, String, MouseStateHandle); 3] = [ ( AgentAutonomy::Full, - "Full", - "Runs commands, writes code, and reads files without asking.", + i18n::t("onboarding.agent.autonomy.full"), + i18n::t("onboarding.agent.autonomy.full_description"), self.autonomy_full_mouse_state.clone(), ), ( AgentAutonomy::Partial, - "Partial", - "Can plan, read files, and execute low-risk commands. Asks before making any changes or executing sensitive commands.", + i18n::t("onboarding.agent.autonomy.partial"), + i18n::t("onboarding.agent.autonomy.partial_description"), self.autonomy_partial_mouse_state.clone(), ), ( AgentAutonomy::None, - "None", - "Takes no actions without your approval.", + i18n::t("onboarding.agent.autonomy.none"), + i18n::t("onboarding.agent.autonomy.none_description"), self.autonomy_none_mouse_state.clone(), ), ]; @@ -910,8 +911,8 @@ impl AgentSlide { appearance, TwoLineButtonSpec { is_selected, - title: title.to_string(), - subtitle: subtitle.to_string(), + title, + subtitle, height: OPTION_HEIGHT, mouse_state, click_action: AgentSlideAction::SelectAutonomy(autonomy), @@ -951,14 +952,18 @@ impl AgentSlide { .on_click(|ctx, _, _| ctx.dispatch_typed_action(AgentSlideAction::ToggleDisableOz)) .finish(); - let label = Text::new("Disable Warp Agent", appearance.ui_font_family(), 14.0) - .with_color(internal_colors::text_sub(theme, background_for_text)) - .with_style(Properties { - weight: Weight::Normal, - ..Default::default() - }) - .with_line_height_ratio(1.0) - .finish(); + let label = Text::new( + i18n::t("onboarding.agent.disable_warp_agent"), + appearance.ui_font_family(), + 14.0, + ) + .with_color(internal_colors::text_sub(theme, background_for_text)) + .with_style(Properties { + weight: Weight::Normal, + ..Default::default() + }) + .with_line_height_ratio(1.0) + .finish(); Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) @@ -971,7 +976,7 @@ impl AgentSlide { let back_button = self.back_button.render( appearance, button::Params { - content: button::Content::Label("Back".into()), + content: button::Content::Label(i18n::t("common.back").into()), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -986,7 +991,7 @@ impl AgentSlide { let next_button = self.next_button.render( appearance, button::Params { - content: button::Content::Label("Next".into()), + content: button::Content::Label(i18n::t("common.next").into()), theme: &button::themes::Primary, options: button::Options { keystroke: Some(enter), @@ -1036,7 +1041,7 @@ impl AgentSlide { // Primary "heading" line: bolder, full-contrast. let title = Text::new( - "Upgrade for access to premium models.", + i18n::t("onboarding.agent.upgrade_banner.title"), ui_font_family, 13.0, ) @@ -1050,7 +1055,7 @@ impl AgentSlide { // Secondary subtext: muted, normal weight. let subtitle = Text::new( - "State-of-the-art models require paid plans.", + i18n::t("onboarding.agent.upgrade_banner.subtitle"), ui_font_family, 12.0, ) @@ -1072,7 +1077,7 @@ impl AgentSlide { let upgrade_button = self.upgrade_button.render( appearance, button::Params { - content: button::Content::Label("Upgrade".into()), + content: button::Content::Label(i18n::t("common.upgrade").into()), theme: &UpgradeButtonTheme, options: button::Options { size: button::Size::Small, @@ -1169,7 +1174,7 @@ impl AgentSlide { let copy_url_link = ui_builder .link( - "copy the URL".into(), + i18n::t("onboarding.agent.upgrade.copy_url"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(AgentSlideAction::CopyUpgradeUrlClicked); @@ -1183,7 +1188,7 @@ impl AgentSlide { let paste_token_link = ui_builder .link( - "Click here".into(), + i18n::t("onboarding.agent.upgrade.click_here"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(AgentSlideAction::PasteAuthTokenFromClipboardClicked); @@ -1201,7 +1206,9 @@ impl AgentSlide { .with_child( Container::new( ui_builder - .span("If your browser hasn't launched, ") + .span(i18n::t( + "onboarding.agent.upgrade.browser_not_launched_prefix", + )) .with_style(text_styles) .build() .finish(), @@ -1212,7 +1219,7 @@ impl AgentSlide { .with_child(copy_url_link) .with_child( ui_builder - .span(" and open the page manually. ") + .span(i18n::t("onboarding.agent.upgrade.open_page_manually")) .with_style(text_styles) .build() .finish(), @@ -1220,7 +1227,7 @@ impl AgentSlide { .with_child(paste_token_link) .with_child( ui_builder - .span(" to paste your token from the browser.") + .span(i18n::t("onboarding.agent.upgrade.paste_token_suffix")) .with_style(text_styles) .build() .finish(), @@ -1266,7 +1273,7 @@ impl AgentSlide { .finish(); let text = ui_builder - .span("Plan successfully activated. All premium models are available.") + .span(i18n::t("onboarding.agent.plan_activated")) .with_style(UiComponentStyles { font_color: Some(text_color), font_size: Some(FONT_SIZE), diff --git a/crates/onboarding/src/slides/customize_slide.rs b/crates/onboarding/src/slides/customize_slide.rs index eb68d9df27..e506322b64 100644 --- a/crates/onboarding/src/slides/customize_slide.rs +++ b/crates/onboarding/src/slides/customize_slide.rs @@ -146,7 +146,7 @@ impl CustomizeUISlide { fn render_header(&self, appearance: &Appearance) -> Box { let title = appearance .ui_builder() - .paragraph("Customize your Warp") + .paragraph(i18n::t("onboarding.customize.title")) .with_style(UiComponentStyles { font_size: Some(36.), font_weight: Some(Weight::Medium), @@ -156,7 +156,7 @@ impl CustomizeUISlide { .finish(); let subtitle = FormattedTextElement::from_str( - "Tailor your features and UI to your working style.", + i18n::t("onboarding.customize.subtitle"), appearance.ui_font_family(), 16., ) @@ -218,11 +218,11 @@ impl CustomizeUISlide { render_toggle_card( appearance, ToggleCardSpec { - title: "Tab styling", + title: i18n::t("onboarding.customize.tab_styling"), is_expanded: is_selected, is_left_selected: ui.use_vertical_tabs, - left_label: "Vertical", - right_label: "Horizontal", + left_label: i18n::t("onboarding.customize.vertical"), + right_label: i18n::t("onboarding.customize.horizontal"), card_mouse_state: self.tab_styling_mouse_state.clone(), on_expand: Box::new(|ctx, _, _| { ctx.dispatch_typed_action(CustomizeSlideAction::SelectSettingCard { @@ -258,7 +258,7 @@ impl CustomizeUISlide { if ui.tools_panel_enabled(&intention) { chips.push(ChipSpec { - label: "File explorer", + label: i18n::t("onboarding.customize.file_explorer"), is_enabled: ui.show_project_explorer, mouse_state: self.chip_file_explorer_mouse.clone(), on_click: Box::new(|ctx, _, _| { @@ -278,7 +278,7 @@ impl CustomizeUISlide { // Conversation history chip is only shown for the agent intention. if is_agent { chips.push(ChipSpec { - label: "Conversation history", + label: i18n::t("onboarding.customize.conversation_history"), is_enabled: ui.show_conversation_history, mouse_state: self.chip_conversation_mouse.clone(), on_click: Box::new(|ctx, _, _| { @@ -297,7 +297,7 @@ impl CustomizeUISlide { } chips.push(ChipSpec { - label: "Global file search", + label: i18n::t("onboarding.customize.global_file_search"), is_enabled: ui.show_global_search, mouse_state: self.chip_global_search_mouse.clone(), on_click: Box::new(|ctx, _, _| { @@ -315,7 +315,7 @@ impl CustomizeUISlide { }); chips.push(ChipSpec { - label: "Warp Drive", + label: i18n::t("onboarding.features.warp_drive"), is_enabled: ui.show_warp_drive, mouse_state: self.chip_warp_drive_mouse.clone(), on_click: Box::new(|ctx, _, _| { @@ -336,11 +336,11 @@ impl CustomizeUISlide { render_toggle_card( appearance, ToggleCardSpec { - title: "Tools panel", + title: i18n::t("onboarding.customize.tools_panel"), is_expanded: is_selected, is_left_selected: ui.tools_panel_enabled(&intention), - left_label: "Enabled", - right_label: "Disabled", + left_label: i18n::t("onboarding.common.enabled"), + right_label: i18n::t("onboarding.common.disabled"), card_mouse_state: self.tools_panel_mouse_state.clone(), on_expand: Box::new(|ctx, _, _| { ctx.dispatch_typed_action(CustomizeSlideAction::SelectSettingCard { @@ -374,11 +374,11 @@ impl CustomizeUISlide { render_toggle_card( appearance, ToggleCardSpec { - title: "Code review", + title: i18n::t("onboarding.customize.code_review"), is_expanded: is_selected, is_left_selected: ui.show_code_review_button, - left_label: "Enabled", - right_label: "Disabled", + left_label: i18n::t("onboarding.common.enabled"), + right_label: i18n::t("onboarding.common.disabled"), card_mouse_state: self.code_review_mouse_state.clone(), on_expand: Box::new(|ctx, _, _| { ctx.dispatch_typed_action(CustomizeSlideAction::SelectSettingCard { @@ -412,7 +412,7 @@ impl CustomizeUISlide { let back_button = self.back_button.render( appearance, button::Params { - content: button::Content::Label("Back".into()), + content: button::Content::Label(i18n::t("common.back").into()), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -427,7 +427,7 @@ impl CustomizeUISlide { let next_button = self.next_button.render( appearance, button::Params { - content: button::Content::Label("Next".into()), + content: button::Content::Label(i18n::t("common.next").into()), theme: &button::themes::Primary, options: button::Options { keystroke: Some(enter), diff --git a/crates/onboarding/src/slides/free_user_no_ai_slide.rs b/crates/onboarding/src/slides/free_user_no_ai_slide.rs index 61c6942312..e27ca35aff 100644 --- a/crates/onboarding/src/slides/free_user_no_ai_slide.rs +++ b/crates/onboarding/src/slides/free_user_no_ai_slide.rs @@ -26,16 +26,18 @@ use crate::slides::{bottom_nav, layout, slide_content}; use crate::telemetry::OnboardingEvent; use crate::OnboardingIntention; -const SUBSCRIBE_ITEMS: &[&str] = &[ - "1,500 credits per month", - "Access to frontier OpenAI, Anthropic, and Google models", - "Access to Reload credits and volume-based discounts", - "Extended cloud agents access", - "Highest codebase indexing limits", - "Unlimited Warp Drive objects and collaboration", - "Private email support", - "Unlimited cloud conversation storage", -]; +fn subscribe_items() -> Vec { + vec![ + i18n::t("onboarding.free_user.subscribe_item.credits"), + i18n::t("onboarding.free_user.subscribe_item.frontier_models"), + i18n::t("onboarding.free_user.subscribe_item.reload_credits"), + i18n::t("onboarding.free_user.subscribe_item.cloud_agents"), + i18n::t("onboarding.free_user.subscribe_item.indexing_limits"), + i18n::t("onboarding.free_user.subscribe_item.warp_drive"), + i18n::t("onboarding.free_user.subscribe_item.email_support"), + i18n::t("onboarding.free_user.subscribe_item.cloud_storage"), + ] +} #[derive(Debug, Clone)] pub enum FreeUserNoAiSlideAction { @@ -103,7 +105,7 @@ impl FreeUserNoAiSlide { fn render_header(&self, appearance: &Appearance) -> Box { appearance .ui_builder() - .paragraph("Let's get started.") + .paragraph(i18n::t("onboarding.free_user.title")) .with_style(UiComponentStyles { font_size: Some(36.), font_weight: Some(Weight::Medium), @@ -123,8 +125,8 @@ impl FreeUserNoAiSlide { appearance, 0, Icon::Code2, - "Agent driven development with Warp's built-in agent", - "Iterate, plan, and build with Oz: Warp's built-in agent. Available locally or in the cloud.", + i18n::t("onboarding.free_user.agent_option.title"), + i18n::t("onboarding.free_user.agent_option.description"), agent_price_badge.to_string(), true, // badge is green self.agent_mouse_state.clone(), @@ -135,9 +137,9 @@ impl FreeUserNoAiSlide { appearance, 1, Icon::Terminal, - "Classic terminal with third-party agents", - "A modern terminal that supports third-party agents (Claude Code, Codex, Gemini CLI) and classic terminal workflows.", - "Free".to_string(), + i18n::t("onboarding.free_user.terminal_option.title"), + i18n::t("onboarding.free_user.terminal_option.description"), + i18n::t("settings.account.plan.free"), false, // badge is gray self.classic_terminal_mouse_state.clone(), selected_index, @@ -192,8 +194,8 @@ impl FreeUserNoAiSlide { appearance: &Appearance, index: usize, icon: Icon, - label: &'static str, - description: &'static str, + label: String, + description: String, badge_text: String, badge_green: bool, mouse_state: MouseStateHandle, @@ -259,12 +261,13 @@ impl FreeUserNoAiSlide { .build() .finish(); - let description_el = FormattedTextElement::from_str(description, ui_font_family, 12.) - .with_color(text_color) - .with_weight(Weight::Normal) - .with_alignment(TextAlignment::Left) - .with_line_height_ratio(1.4) - .finish(); + let description_el = + FormattedTextElement::from_str(description.clone(), ui_font_family, 12.) + .with_color(text_color) + .with_weight(Weight::Normal) + .with_alignment(TextAlignment::Left) + .with_line_height_ratio(1.4) + .finish(); let content = Flex::column() .with_main_axis_size(MainAxisSize::Min) @@ -302,7 +305,7 @@ impl FreeUserNoAiSlide { let back_button = self.back_button.render( appearance, button::Params { - content: button::Content::Label("Back".into()), + content: button::Content::Label(i18n::t("common.back").into()), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -318,7 +321,7 @@ impl FreeUserNoAiSlide { self.next_button.render( appearance, button::Params { - content: button::Content::Label("Get Warping".into()), + content: button::Content::Label(i18n::t("onboarding.get_warping").into()), theme: &button::themes::Primary, options: button::Options { keystroke: Some(enter), @@ -333,7 +336,9 @@ impl FreeUserNoAiSlide { self.subscribe_nav_button.render( appearance, button::Params { - content: button::Content::Label("Subscribe".into()), + content: button::Content::Label( + i18n::t("onboarding.free_user.subscribe").into(), + ), theme: &button::themes::Primary, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -347,7 +352,7 @@ impl FreeUserNoAiSlide { self.next_button.render( appearance, button::Params { - content: button::Content::Label("Next".into()), + content: button::Content::Label(i18n::t("common.next").into()), theme: &button::themes::Primary, options: button::Options { disabled: true, @@ -370,7 +375,7 @@ impl FreeUserNoAiSlide { let text_sub = internal_colors::text_sub(theme, internal_colors::neutral_2(theme)); let title = FormattedTextElement::from_str( - "Subscribe to access agent driven development in Warp.", + i18n::t("onboarding.free_user.subscribe_title"), ui_font_family, 24., ) @@ -385,7 +390,7 @@ impl FreeUserNoAiSlide { .with_cross_axis_alignment(CrossAxisAlignment::Start) .with_spacing(8.); - for item_text in SUBSCRIBE_ITEMS { + for item_text in subscribe_items() { let bullet = appearance .ui_builder() .paragraph(format!("\u{2022} {item_text}")) @@ -418,7 +423,7 @@ impl FreeUserNoAiSlide { .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( warpui::elements::Text::new_inline( - "Subscribe", + i18n::t("onboarding.free_user.subscribe"), appearance.ui_font_family(), 14., ) diff --git a/crates/onboarding/src/slides/intention_slide.rs b/crates/onboarding/src/slides/intention_slide.rs index 38125b89c5..188980e63e 100644 --- a/crates/onboarding/src/slides/intention_slide.rs +++ b/crates/onboarding/src/slides/intention_slide.rs @@ -24,7 +24,7 @@ use super::OnboardingSlide; use crate::model::OnboardingStateModel; use crate::slides::{bottom_nav, layout, slide_content}; use crate::visuals::{intention_terminal_visual, intention_visual}; -use crate::{OnboardingIntention, AI_FEATURES}; +use crate::{ai_features, OnboardingIntention}; #[derive(Debug, Clone)] pub enum IntentionSlideAction { @@ -83,7 +83,7 @@ impl IntentionSlide { let title = appearance .ui_builder() - .paragraph("Welcome to Warp") + .paragraph(i18n::t("onboarding.intention.title")) .with_style(UiComponentStyles { font_size: Some(36.), font_weight: Some(Weight::Medium), @@ -93,7 +93,7 @@ impl IntentionSlide { .finish(); let subtitle = FormattedTextElement::from_str( - "How do you want to work?", + i18n::t("onboarding.intention.subtitle"), appearance.ui_font_family(), 16., ) @@ -202,7 +202,7 @@ impl IntentionSlide { let header_row = { let label = appearance .ui_builder() - .paragraph("Build faster with AI agents") + .paragraph(i18n::t("onboarding.intention.agent.title")) .with_style(UiComponentStyles { font_size: Some(16.), font_weight: Some(Weight::Semibold), @@ -240,7 +240,7 @@ impl IntentionSlide { }; let description = FormattedTextElement::from_str( - "An agent-first experience with best in class terminal support. Get terminal and agent driven development AI features like:", + i18n::t("onboarding.intention.agent.description"), appearance.ui_font_family(), 14., ) @@ -251,7 +251,7 @@ impl IntentionSlide { .finish(); let checklist = { - let items = AI_FEATURES; + let items = ai_features(); // When the agent card is selected, use the theme's green to match the // "Blended ANSI/green_fg" token in the design. let check_fill = if is_selected { @@ -262,14 +262,14 @@ impl IntentionSlide { let mut col = Flex::column() .with_main_axis_size(MainAxisSize::Min) .with_cross_axis_alignment(CrossAxisAlignment::Start); - for &item in items { + for item in items { let icon_el = ConstrainedBox::new(Icon::Check.to_warpui_icon(check_fill).finish()) .with_width(16.) .with_height(16.) .finish(); let text_el = appearance .ui_builder() - .paragraph(item.to_string()) + .paragraph(item) .with_style(UiComponentStyles { font_size: Some(14.), font_weight: Some(Weight::Normal), @@ -321,7 +321,7 @@ impl IntentionSlide { let label = appearance .ui_builder() - .paragraph("Just use the terminal") + .paragraph(i18n::t("onboarding.intention.terminal.title")) .with_style(UiComponentStyles { font_size: Some(16.), font_weight: Some(Weight::Semibold), @@ -334,7 +334,7 @@ impl IntentionSlide { let badge = { let badge_text = appearance .ui_builder() - .paragraph("No AI features") + .paragraph(i18n::t("onboarding.intention.terminal.no_ai_features")) .with_style(UiComponentStyles { font_size: Some(12.), font_weight: Some(Weight::Semibold), @@ -360,7 +360,7 @@ impl IntentionSlide { .finish(); let description = FormattedTextElement::from_str( - "A modern terminal optimized for speed, context, and control without AI.", + i18n::t("onboarding.intention.terminal.description"), appearance.ui_font_family(), 14., ) @@ -388,7 +388,7 @@ impl IntentionSlide { let back_button = self.back_button.render( appearance, button::Params { - content: button::Content::Label("Back".into()), + content: button::Content::Label(i18n::t("common.back").into()), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -401,9 +401,9 @@ impl IntentionSlide { let new_settings_modes = FeatureFlag::OpenWarpNewSettingsModes.is_enabled(); let next_text = if !new_settings_modes && selected_index == 1 { - "Get Warping" + i18n::t("onboarding.get_warping") } else { - "Next" + i18n::t("common.next") }; let enter = Keystroke::parse("enter").unwrap_or_default(); let next_button = self.next_button.render( diff --git a/crates/onboarding/src/slides/intro_slide.rs b/crates/onboarding/src/slides/intro_slide.rs index 3838f615b1..e6e27bdc4d 100644 --- a/crates/onboarding/src/slides/intro_slide.rs +++ b/crates/onboarding/src/slides/intro_slide.rs @@ -82,7 +82,7 @@ impl View for IntroSlide { let login_row = Flex::row() .with_child( ui_builder - .span("Already have an account? ") + .span(i18n::t("onboarding.intro.already_have_account_prefix")) .with_style(disclaimer_styles) .build() .finish(), @@ -90,7 +90,7 @@ impl View for IntroSlide { .with_child( ui_builder .link( - "Log in".into(), + i18n::t("onboarding.intro.log_in"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(IntroSlideAction::LoginClicked); @@ -151,7 +151,7 @@ impl IntroSlide { let base_color: ColorU = internal_colors::fg_overlay_4(theme).into(); let shimmer_color: ColorU = theme.foreground().into(); let title = ShimmeringTextElement::new( - "Welcome to Warp", + i18n::t("onboarding.intro.welcome_to_warp"), appearance.ui_font_family(), 32., base_color, @@ -163,7 +163,7 @@ impl IntroSlide { let subtitle_color = internal_colors::text_sub(theme, theme.background().into_solid()); let subtitle = FormattedTextElement::from_str( - "A modern terminal with state of the art agents built in.", + i18n::t("onboarding.intro.subtitle"), appearance.ui_font_family(), 16., ) @@ -176,7 +176,7 @@ impl IntroSlide { let get_started_button = self.get_started_button.render( appearance, button::Params { - content: button::Content::Label("Get started".into()), + content: button::Content::Label(i18n::t("onboarding.intro.get_started").into()), theme: &button::themes::Primary, options: button::Options { keystroke: Some(enter), diff --git a/crates/onboarding/src/slides/project_slide.rs b/crates/onboarding/src/slides/project_slide.rs index 7ff6113689..26ccb6e3d8 100644 --- a/crates/onboarding/src/slides/project_slide.rs +++ b/crates/onboarding/src/slides/project_slide.rs @@ -124,7 +124,7 @@ impl ProjectSlide { fn render_header(&self, appearance: &Appearance) -> Box { let title = appearance .ui_builder() - .paragraph("Open a project") + .paragraph(i18n::t("onboarding.project.title")) .with_style(UiComponentStyles { font_size: Some(36.), font_weight: Some(Weight::Medium), @@ -135,7 +135,7 @@ impl ProjectSlide { let subtitle = appearance .ui_builder() - .paragraph("Set up a project to optimize it for coding in Warp.") + .paragraph(i18n::t("onboarding.project.subtitle")) .with_style(UiComponentStyles { font_size: Some(20.), font_weight: Some(Weight::Normal), @@ -213,7 +213,7 @@ impl ProjectSlide { let folder_text = Container::new( appearance .ui_builder() - .paragraph("Open local folder") + .paragraph(i18n::t("onboarding.project.open_local_folder")) .with_style(UiComponentStyles { font_color: Some(text_color), ..Default::default() @@ -280,7 +280,7 @@ impl ProjectSlide { let back_button = self.back_button.render( appearance, button::Params { - content: button::Content::Label("Back".into()), + content: button::Content::Label(i18n::t("common.back").into()), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -297,15 +297,15 @@ impl ProjectSlide { let (label, keystroke, action) = match settings { ProjectOnboardingSettings::Project { .. } => ( if theme_picker_last { - "Next" + i18n::t("common.next") } else { - "Get Warping" + i18n::t("onboarding.get_warping") }, Keystroke::parse("enter").unwrap_or_default(), ProjectSlideAction::NextClicked, ), ProjectOnboardingSettings::NoProject => ( - "Skip", + i18n::t("common.skip"), Keystroke::parse("cmdorctrl-enter").unwrap_or_default(), ProjectSlideAction::SkipClicked, ), @@ -343,8 +343,8 @@ impl ProjectSlide { appearance: &Appearance, mouse_state: MouseStateHandle, checked: bool, - title: &'static str, - description: &'static str, + title: String, + description: String, action: ProjectSlideAction, ) -> Box { let theme = appearance.theme(); @@ -408,8 +408,8 @@ impl ProjectSlide { appearance, self.initialize_projects_automatically_mouse_state.clone(), initialize_projects_automatically, - "Initialize project automatically", - "Prepares the project environment, builds an index of your code, and generates project rules—giving the agent deeper understanding and better performance.", + i18n::t("onboarding.project.initialize_automatically"), + i18n::t("onboarding.project.initialize_automatically_description"), ProjectSlideAction::ToggleInitializeProjectsAutomatically, ); diff --git a/crates/onboarding/src/slides/theme_picker_slide.rs b/crates/onboarding/src/slides/theme_picker_slide.rs index aad8b36ee3..c8010698e6 100644 --- a/crates/onboarding/src/slides/theme_picker_slide.rs +++ b/crates/onboarding/src/slides/theme_picker_slide.rs @@ -194,7 +194,7 @@ impl ThemePickerSlide { fn render_header_text(&self, appearance: &Appearance) -> Box { let title = appearance .ui_builder() - .paragraph("Choose a theme") + .paragraph(i18n::t("onboarding.theme.title")) .with_style(UiComponentStyles { font_size: Some(36.), font_weight: Some(Weight::Medium), @@ -204,7 +204,7 @@ impl ThemePickerSlide { .finish(); let subtitle = FormattedTextElement::from_str( - "Click or use arrow keys to select, Enter to confirm.", + i18n::t("onboarding.theme.subtitle"), appearance.ui_font_family(), 16., ) @@ -261,7 +261,7 @@ impl ThemePickerSlide { let back_button = self.back_button.render( appearance, button::Params { - content: button::Content::Label("Back".into()), + content: button::Content::Label(i18n::t("common.back").into()), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -274,9 +274,9 @@ impl ThemePickerSlide { let theme_picker_last = FeatureFlag::OpenWarpNewSettingsModes.is_enabled(); let next_label = if theme_picker_last { - "Get Warping" + i18n::t("onboarding.get_warping") } else { - "Next" + i18n::t("common.next") }; let enter = Keystroke::parse("enter").unwrap_or_default(); @@ -576,7 +576,7 @@ impl ThemePickerSlide { let privacy_line = Flex::row() .with_child( ui_builder - .span("If you'd like to opt out of analytics, you can adjust your ") + .span(i18n::t("onboarding.theme.analytics_opt_out_prefix")) .with_style(disclaimer_styles) .build() .finish(), @@ -584,7 +584,7 @@ impl ThemePickerSlide { .with_child( ui_builder .link( - "Privacy Settings".into(), + i18n::t("onboarding.theme.privacy_settings"), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action( @@ -603,7 +603,7 @@ impl ThemePickerSlide { let tos_line = Flex::row() .with_child( ui_builder - .span("By continuing, you agree to Warp's ") + .span(i18n::t("onboarding.theme.tos_prefix")) .with_style(disclaimer_styles) .build() .finish(), @@ -611,7 +611,7 @@ impl ThemePickerSlide { .with_child( ui_builder .link( - "Terms of Service".into(), + i18n::t("onboarding.theme.terms_of_service"), Some(TOS_URL.into()), None, self.tos_mouse_state.clone(), diff --git a/crates/onboarding/src/slides/third_party_slide.rs b/crates/onboarding/src/slides/third_party_slide.rs index b89b0ad2ac..f87c667058 100644 --- a/crates/onboarding/src/slides/third_party_slide.rs +++ b/crates/onboarding/src/slides/third_party_slide.rs @@ -136,7 +136,7 @@ impl ThirdPartySlide { fn render_header(&self, appearance: &Appearance) -> Box { let title = appearance .ui_builder() - .paragraph("Customize third party agents") + .paragraph(i18n::t("onboarding.third_party.title")) .with_style(UiComponentStyles { font_size: Some(36.), font_weight: Some(Weight::Medium), @@ -146,7 +146,7 @@ impl ThirdPartySlide { .finish(); let subtitle = FormattedTextElement::from_str( - "Select defaults for using agents like Claude Code, Codex, and Gemini.", + i18n::t("onboarding.third_party.subtitle"), appearance.ui_font_family(), 16., ) @@ -177,11 +177,11 @@ impl ThirdPartySlide { let card = render_toggle_card( appearance, ToggleCardSpec { - title: "CLI agent toolbar", + title: i18n::t("onboarding.third_party.cli_agent_toolbar"), is_expanded: is_selected, is_left_selected: cli_toolbar_enabled, - left_label: "Enabled", - right_label: "Disabled", + left_label: i18n::t("onboarding.common.enabled"), + right_label: i18n::t("onboarding.common.disabled"), card_mouse_state: self.cli_toolbar_card_mouse_state.clone(), on_expand: Box::new(|ctx, _, _| { ctx.dispatch_typed_action(ThirdPartySlideAction::SelectSettingCard { @@ -225,11 +225,11 @@ impl ThirdPartySlide { let card = render_toggle_card( appearance, ToggleCardSpec { - title: "Notifications", + title: i18n::t("onboarding.third_party.notifications"), is_expanded: is_selected, is_left_selected: show_agent_notifications, - left_label: "Enabled", - right_label: "Disabled", + left_label: i18n::t("onboarding.common.enabled"), + right_label: i18n::t("onboarding.common.disabled"), card_mouse_state: self.notifications_card_mouse_state.clone(), on_expand: Box::new(|ctx, _, _| { ctx.dispatch_typed_action(ThirdPartySlideAction::SelectSettingCard { @@ -271,7 +271,7 @@ impl ThirdPartySlide { let back_button = self.back_button.render( appearance, button::Params { - content: button::Content::Label("Back".into()), + content: button::Content::Label(i18n::t("common.back").into()), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -286,7 +286,7 @@ impl ThirdPartySlide { let next_button = self.next_button.render( appearance, button::Params { - content: button::Content::Label("Next".into()), + content: button::Content::Label(i18n::t("common.next").into()), theme: &button::themes::Primary, options: button::Options { keystroke: Some(enter), diff --git a/crates/onboarding/src/slides/toggle_card.rs b/crates/onboarding/src/slides/toggle_card.rs index 9d55c3ca42..a4496d9960 100644 --- a/crates/onboarding/src/slides/toggle_card.rs +++ b/crates/onboarding/src/slides/toggle_card.rs @@ -19,7 +19,7 @@ pub(super) type HoverCallback = Box; pub(super) struct ChipSpec { - pub label: &'static str, + pub label: String, pub is_enabled: bool, pub mouse_state: MouseStateHandle, pub on_click: ClickCallback, @@ -27,11 +27,11 @@ pub(super) struct ChipSpec { } pub(super) struct ToggleCardSpec { - pub title: &'static str, + pub title: String, pub is_expanded: bool, pub is_left_selected: bool, - pub left_label: &'static str, - pub right_label: &'static str, + pub left_label: String, + pub right_label: String, pub card_mouse_state: MouseStateHandle, pub on_expand: ClickCallback, pub left_mouse: MouseStateHandle, @@ -67,7 +67,7 @@ fn collapsed_subtitle( let enabled_labels: Vec<&str> = chips .iter() .filter(|c| c.is_enabled) - .map(|c| c.label) + .map(|c| c.label.as_str()) .collect(); if enabled_labels.is_empty() { return left_label.to_string(); @@ -87,14 +87,15 @@ fn render_collapsed(appearance: &Appearance, spec: ToggleCardSpec) -> Box Box Box { - $crate::macros::define_setting!(@base $name: $type, default: $default, supported_platforms: $supported_platforms, group: $group, sync_to_cloud: $sync_to_cloud, private: $private, storage_key: $storage_key, toml_path_value: Some($toml_path), max_table_depth_value: $mtd $(, description: $desc)? $(, feature_flag: $flag)?); + ($name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, storage_key: $storage_key:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr, max_table_depth: $mtd:literal $(, description: $desc:literal)? $(, description_key: $desc_key:literal)? $(, feature_flag: $flag:path)?) => { + $crate::macros::define_setting!(@base $name: $type, default: $default, supported_platforms: $supported_platforms, group: $group, sync_to_cloud: $sync_to_cloud, private: $private, storage_key: $storage_key, toml_path_value: Some($toml_path), max_table_depth_value: $mtd $(, description: $desc)? $(, description_key: $desc_key)? $(, feature_flag: $flag)?); }; // Convenience arm: with toml_path + max_table_depth (no explicit storage_key) - ($name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr, max_table_depth: $mtd:literal $(, description: $desc:literal)? $(, feature_flag: $flag:path)?) => { - $crate::macros::define_setting!(@base $name: $type, default: $default, supported_platforms: $supported_platforms, group: $group, sync_to_cloud: $sync_to_cloud, private: $private, storage_key: stringify!($name), toml_path_value: Some($toml_path), max_table_depth_value: $mtd $(, description: $desc)? $(, feature_flag: $flag)?); + ($name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr, max_table_depth: $mtd:literal $(, description: $desc:literal)? $(, description_key: $desc_key:literal)? $(, feature_flag: $flag:path)?) => { + $crate::macros::define_setting!(@base $name: $type, default: $default, supported_platforms: $supported_platforms, group: $group, sync_to_cloud: $sync_to_cloud, private: $private, storage_key: stringify!($name), toml_path_value: Some($toml_path), max_table_depth_value: $mtd $(, description: $desc)? $(, description_key: $desc_key)? $(, feature_flag: $flag)?); }; // Convenience arm: with storage_key + toml_path - ($name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, storage_key: $storage_key:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)?) => { - $crate::macros::define_setting!(@base $name: $type, default: $default, supported_platforms: $supported_platforms, group: $group, sync_to_cloud: $sync_to_cloud, private: $private, storage_key: $storage_key, toml_path_value: Some($toml_path) $(, description: $desc)? $(, feature_flag: $flag)?); + ($name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, storage_key: $storage_key:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr $(, description: $desc:literal)? $(, description_key: $desc_key:literal)? $(, feature_flag: $flag:path)?) => { + $crate::macros::define_setting!(@base $name: $type, default: $default, supported_platforms: $supported_platforms, group: $group, sync_to_cloud: $sync_to_cloud, private: $private, storage_key: $storage_key, toml_path_value: Some($toml_path) $(, description: $desc)? $(, description_key: $desc_key)? $(, feature_flag: $flag)?); }; // Convenience arm: with toml_path (no explicit storage_key) - ($name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)?) => { - $crate::macros::define_setting!(@base $name: $type, default: $default, supported_platforms: $supported_platforms, group: $group, sync_to_cloud: $sync_to_cloud, private: $private, storage_key: stringify!($name), toml_path_value: Some($toml_path) $(, description: $desc)? $(, feature_flag: $flag)?); + ($name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr $(, description: $desc:literal)? $(, description_key: $desc_key:literal)? $(, feature_flag: $flag:path)?) => { + $crate::macros::define_setting!(@base $name: $type, default: $default, supported_platforms: $supported_platforms, group: $group, sync_to_cloud: $sync_to_cloud, private: $private, storage_key: stringify!($name), toml_path_value: Some($toml_path) $(, description: $desc)? $(, description_key: $desc_key)? $(, feature_flag: $flag)?); }; // Convenience arm: without toml_path (private settings with explicit storage_key) - ($name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, storage_key: $storage_key:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)?) => { - $crate::macros::define_setting!(@base $name: $type, default: $default, supported_platforms: $supported_platforms, group: $group, sync_to_cloud: $sync_to_cloud, private: $private, storage_key: $storage_key, toml_path_value: None::<&str> $(, description: $desc)? $(, feature_flag: $flag)?); + ($name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, storage_key: $storage_key:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr $(, description: $desc:literal)? $(, description_key: $desc_key:literal)? $(, feature_flag: $flag:path)?) => { + $crate::macros::define_setting!(@base $name: $type, default: $default, supported_platforms: $supported_platforms, group: $group, sync_to_cloud: $sync_to_cloud, private: $private, storage_key: $storage_key, toml_path_value: None::<&str> $(, description: $desc)? $(, description_key: $desc_key)? $(, feature_flag: $flag)?); }; // Convenience arm: without toml_path (private settings with default storage_key) - ($name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)?) => { - $crate::macros::define_setting!(@base $name: $type, default: $default, supported_platforms: $supported_platforms, group: $group, sync_to_cloud: $sync_to_cloud, private: $private, storage_key: stringify!($name), toml_path_value: None::<&str> $(, description: $desc)? $(, feature_flag: $flag)?); + ($name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr $(, description: $desc:literal)? $(, description_key: $desc_key:literal)? $(, feature_flag: $flag:path)?) => { + $crate::macros::define_setting!(@base $name: $type, default: $default, supported_platforms: $supported_platforms, group: $group, sync_to_cloud: $sync_to_cloud, private: $private, storage_key: stringify!($name), toml_path_value: None::<&str> $(, description: $desc)? $(, description_key: $desc_key)? $(, feature_flag: $flag)?); }; // Base arm: generates the struct and Setting impl - (@base $name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, storage_key: $storage_key:expr, toml_path_value: $toml_path_value:expr $(, max_table_depth_value: $mtd:literal)? $(, description: $desc:literal)? $(, feature_flag: $flag:path)?) => { + (@base $name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, storage_key: $storage_key:expr, toml_path_value: $toml_path_value:expr $(, max_table_depth_value: $mtd:literal)? $(, description: $desc:literal)? $(, description_key: $desc_key:literal)? $(, feature_flag: $flag:path)?) => { pub struct $name { inner: $type, is_explicitly_set: bool, @@ -409,6 +409,7 @@ macro_rules! define_setting { $crate::submit_schema_entry!( private: $private, description: $crate::_schema_default_description!($($desc)?), + description_key: $crate::_schema_default_description_key!($($desc_key)?), toml_path_value: $toml_path_value, fallback_storage_key: $storage_key, supported_platforms: $supported_platforms, @@ -424,7 +425,7 @@ pub use define_setting; #[macro_export] macro_rules! maybe_define_setting { // storage_key + toml_path + max_table_depth - ($setting:ident, group: $group:path, { type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, storage_key: $key:expr, toml_path: $toml_path:expr, max_table_depth: $mtd:literal $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)? }) => { + ($setting:ident, group: $group:path, { type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, storage_key: $key:expr, toml_path: $toml_path:expr, max_table_depth: $mtd:literal $(, description: $desc:literal)? $(, description_key: $desc_key:literal)? $(, feature_flag: $flag:path)? $(,)? }) => { $crate::macros::define_setting!( $setting: $value_type, default: $default, @@ -436,11 +437,12 @@ macro_rules! maybe_define_setting { toml_path: $toml_path, max_table_depth: $mtd $(, description: $desc)? + $(, description_key: $desc_key)? $(, feature_flag: $flag)? ); }; // toml_path + max_table_depth (no explicit storage_key) - ($setting:ident, group: $group:path, { type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr, max_table_depth: $mtd:literal $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)? }) => { + ($setting:ident, group: $group:path, { type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr, max_table_depth: $mtd:literal $(, description: $desc:literal)? $(, description_key: $desc_key:literal)? $(, feature_flag: $flag:path)? $(,)? }) => { $crate::macros::define_setting!( $setting: $value_type, default: $default, @@ -451,11 +453,12 @@ macro_rules! maybe_define_setting { toml_path: $toml_path, max_table_depth: $mtd $(, description: $desc)? + $(, description_key: $desc_key)? $(, feature_flag: $flag)? ); }; // storage_key + toml_path - ($setting:ident, group: $group:path, { type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, storage_key: $key:expr, toml_path: $toml_path:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)? }) => { + ($setting:ident, group: $group:path, { type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, storage_key: $key:expr, toml_path: $toml_path:expr $(, description: $desc:literal)? $(, description_key: $desc_key:literal)? $(, feature_flag: $flag:path)? $(,)? }) => { $crate::macros::define_setting!( $setting: $value_type, default: $default, @@ -466,11 +469,12 @@ macro_rules! maybe_define_setting { private: $private, toml_path: $toml_path $(, description: $desc)? + $(, description_key: $desc_key)? $(, feature_flag: $flag)? ); }; // toml_path only (no explicit storage_key) - ($setting:ident, group: $group:path, { type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)? }) => { + ($setting:ident, group: $group:path, { type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr $(, description: $desc:literal)? $(, description_key: $desc_key:literal)? $(, feature_flag: $flag:path)? $(,)? }) => { $crate::macros::define_setting!( $setting: $value_type, default: $default, @@ -480,11 +484,12 @@ macro_rules! maybe_define_setting { private: $private, toml_path: $toml_path $(, description: $desc)? + $(, description_key: $desc_key)? $(, feature_flag: $flag)? ); }; // storage_key only, no toml_path (private settings with custom key) - ($setting:ident, group: $group:path, { type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, storage_key: $key:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)? }) => { + ($setting:ident, group: $group:path, { type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, storage_key: $key:expr $(, description: $desc:literal)? $(, description_key: $desc_key:literal)? $(, feature_flag: $flag:path)? $(,)? }) => { $crate::macros::define_setting!( $setting: $value_type, default: $default, @@ -494,11 +499,12 @@ macro_rules! maybe_define_setting { sync_to_cloud: $sync_to_cloud, private: $private $(, description: $desc)? + $(, description_key: $desc_key)? $(, feature_flag: $flag)? ); }; // neither toml_path nor storage_key (private settings with default key) - ($setting:ident, group: $group:path, { type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)? }) => { + ($setting:ident, group: $group:path, { type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr $(, description: $desc:literal)? $(, description_key: $desc_key:literal)? $(, feature_flag: $flag:path)? $(,)? }) => { $crate::macros::define_setting!( $setting: $value_type, default: $default, @@ -507,6 +513,7 @@ macro_rules! maybe_define_setting { sync_to_cloud: $sync_to_cloud, private: $private $(, description: $desc)? + $(, description_key: $desc_key)? $(, feature_flag: $flag)? ); }; @@ -517,7 +524,7 @@ pub use maybe_define_setting; #[macro_export] macro_rules! implement_setting_for_enum { // Base arm with all parameters - (@base $name:ident, $group:path, $supported_platforms:expr, $sync_to_cloud:expr, private: $private:expr, storage_key: $storage_key:expr, toml_path_value: $toml_path_value:expr $(, max_table_depth_value: $mtd:literal)? $(, description: $desc:literal)? $(, feature_flag: $flag:path)?) => { + (@base $name:ident, $group:path, $supported_platforms:expr, $sync_to_cloud:expr, private: $private:expr, storage_key: $storage_key:expr, toml_path_value: $toml_path_value:expr $(, max_table_depth_value: $mtd:literal)? $(, description: $desc:literal)? $(, description_key: $desc_key:literal)? $(, feature_flag: $flag:path)?) => { const _: () = { let toml_path: Option<&str> = $toml_path_value; if !$private && toml_path.is_none() { @@ -668,6 +675,7 @@ macro_rules! implement_setting_for_enum { $crate::submit_schema_entry!( private: $private, description: $crate::_schema_default_description!($($desc)?), + description_key: $crate::_schema_default_description_key!($($desc_key)?), toml_path_value: $toml_path_value, fallback_storage_key: $storage_key, supported_platforms: $supported_platforms, @@ -678,16 +686,16 @@ macro_rules! implement_setting_for_enum { ); }; // toml_path + max_table_depth - ($name:ident, $group:path, $supported_platforms:expr, $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr, max_table_depth: $mtd:literal $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)?) => { - $crate::macros::implement_setting_for_enum!(@base $name, $group, $supported_platforms, $sync_to_cloud, private: $private, storage_key: stringify!($name), toml_path_value: Some($toml_path), max_table_depth_value: $mtd $(, description: $desc)? $(, feature_flag: $flag)?); + ($name:ident, $group:path, $supported_platforms:expr, $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr, max_table_depth: $mtd:literal $(, description: $desc:literal)? $(, description_key: $desc_key:literal)? $(, feature_flag: $flag:path)? $(,)?) => { + $crate::macros::implement_setting_for_enum!(@base $name, $group, $supported_platforms, $sync_to_cloud, private: $private, storage_key: stringify!($name), toml_path_value: Some($toml_path), max_table_depth_value: $mtd $(, description: $desc)? $(, description_key: $desc_key)? $(, feature_flag: $flag)?); }; // toml_path only - ($name:ident, $group:path, $supported_platforms:expr, $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)?) => { - $crate::macros::implement_setting_for_enum!(@base $name, $group, $supported_platforms, $sync_to_cloud, private: $private, storage_key: stringify!($name), toml_path_value: Some($toml_path) $(, description: $desc)? $(, feature_flag: $flag)?); + ($name:ident, $group:path, $supported_platforms:expr, $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr $(, description: $desc:literal)? $(, description_key: $desc_key:literal)? $(, feature_flag: $flag:path)? $(,)?) => { + $crate::macros::implement_setting_for_enum!(@base $name, $group, $supported_platforms, $sync_to_cloud, private: $private, storage_key: stringify!($name), toml_path_value: Some($toml_path) $(, description: $desc)? $(, description_key: $desc_key)? $(, feature_flag: $flag)?); }; // neither (private settings) - ($name:ident, $group:path, $supported_platforms:expr, $sync_to_cloud:expr, private: $private:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)?) => { - $crate::macros::implement_setting_for_enum!(@base $name, $group, $supported_platforms, $sync_to_cloud, private: $private, storage_key: stringify!($name), toml_path_value: None::<&str> $(, description: $desc)? $(, feature_flag: $flag)?); + ($name:ident, $group:path, $supported_platforms:expr, $sync_to_cloud:expr, private: $private:expr $(, description: $desc:literal)? $(, description_key: $desc_key:literal)? $(, feature_flag: $flag:path)? $(,)?) => { + $crate::macros::implement_setting_for_enum!(@base $name, $group, $supported_platforms, $sync_to_cloud, private: $private, storage_key: stringify!($name), toml_path_value: None::<&str> $(, description: $desc)? $(, description_key: $desc_key)? $(, feature_flag: $flag)?); }; } pub use implement_setting_for_enum; @@ -701,9 +709,9 @@ pub trait SettingSection { #[macro_export] macro_rules! define_settings_group { - ($group:ident, settings: [$($var:ident: $setting:ident $({ type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr $(, storage_key: $storage_key:literal)? $(, toml_path: $toml_path:literal)? $(, max_table_depth: $mtd:literal)? $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)? })? $(,)? )*]) => { + ($group:ident, settings: [$($var:ident: $setting:ident $({ type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr $(, storage_key: $storage_key:literal)? $(, toml_path: $toml_path:literal)? $(, max_table_depth: $mtd:literal)? $(, description: $desc:literal)? $(, description_key: $desc_key:literal)? $(, feature_flag: $flag:path)? $(,)? })? $(,)? )*]) => { $( - $crate::macros::maybe_define_setting!($setting, group: $group $(, { type: $value_type, default: $default, supported_platforms: $supported_platforms, sync_to_cloud: $sync_to_cloud, private: $private $(, storage_key: $storage_key)? $(, toml_path: $toml_path)? $(, max_table_depth: $mtd)? $(, description: $desc)? $(, feature_flag: $flag)? })?); + $crate::macros::maybe_define_setting!($setting, group: $group $(, { type: $value_type, default: $default, supported_platforms: $supported_platforms, sync_to_cloud: $sync_to_cloud, private: $private $(, storage_key: $storage_key)? $(, toml_path: $toml_path)? $(, max_table_depth: $mtd)? $(, description: $desc)? $(, description_key: $desc_key)? $(, feature_flag: $flag)? })?); )* pub struct $group { diff --git a/crates/settings/src/schema.rs b/crates/settings/src/schema.rs index e96e7f0b43..056370473e 100644 --- a/crates/settings/src/schema.rs +++ b/crates/settings/src/schema.rs @@ -16,6 +16,10 @@ pub struct SettingSchemaEntry { /// User-facing description of what this setting does. pub description: &'static str, + /// i18n key for the user-facing description, when the description is + /// provided by a locale catalog rather than an inline fallback string. + pub description_key: Option<&'static str>, + /// The TOML section path (everything before the last segment of toml_path). pub hierarchy: Option<&'static str>, @@ -53,6 +57,7 @@ macro_rules! submit_schema_entry { ( private: $private:expr, description: $desc:expr, + description_key: $desc_key:expr, toml_path_value: $toml_path:expr, fallback_storage_key: $fallback_key:expr, supported_platforms: $plat:expr, @@ -71,6 +76,7 @@ macro_rules! submit_schema_entry { KEY }, description: $desc, + description_key: $desc_key, hierarchy: { const HIER: Option<&str> = match $toml_path { Some(path) => $crate::toml_path_hierarchy(path), @@ -109,6 +115,17 @@ macro_rules! _schema_default_description { }; } +/// Helper: produces an optional schema description i18n key. +#[macro_export] +macro_rules! _schema_default_description_key { + () => { + None + }; + ($desc_key:literal) => { + Some($desc_key) + }; +} + /// Helper: produces `Option` for a feature flag, defaulting to `None`. #[macro_export] macro_rules! _schema_default_flag { diff --git a/crates/warp_cli/Cargo.toml b/crates/warp_cli/Cargo.toml index ca404baadd..ab2dcd0567 100644 --- a/crates/warp_cli/Cargo.toml +++ b/crates/warp_cli/Cargo.toml @@ -11,6 +11,7 @@ chrono.workspace = true clap = { workspace = true, features = ["derive", "env"] } cfg-if = { workspace = true } humantime.workspace = true +i18n.workspace = true jaq-all.workspace = true serde = { workspace = true, features = ["derive"] } url = { workspace = true, features = ["serde"] } diff --git a/crates/warp_cli/src/agent.rs b/crates/warp_cli/src/agent.rs index 375d29e744..c9a39d4a62 100644 --- a/crates/warp_cli/src/agent.rs +++ b/crates/warp_cli/src/agent.rs @@ -48,8 +48,16 @@ pub enum Prompt { impl fmt::Display for Prompt { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Prompt::PlainText(text) => write!(f, "Prompt: {text}"), - Prompt::SavedPrompt(id) => write!(f, "Saved Prompt ID: {id}"), + Prompt::PlainText(text) => write!( + f, + "{}", + i18n::t("warp_cli.agent.prompt.plain_text").replace("{text}", text) + ), + Prompt::SavedPrompt(id) => write!( + f, + "{}", + i18n::t("warp_cli.agent.prompt.saved_prompt_id").replace("{id}", id) + ), } } } diff --git a/crates/warp_cli/src/completions.rs b/crates/warp_cli/src/completions.rs index 9ab09d5946..49dfcb33e1 100644 --- a/crates/warp_cli/src/completions.rs +++ b/crates/warp_cli/src/completions.rs @@ -9,9 +9,7 @@ use crate::{Args, binary_name}; pub fn generate_to_stdout(shell: Option) -> anyhow::Result<()> { let shell = match shell.or_else(Shell::from_env) { Some(s) => s, - None => anyhow::bail!( - "Could not determine shell from environment. Please provide a shell argument." - ), + None => anyhow::bail!(i18n::t("warp_cli.completions.error.shell_not_detected")), }; let mut cmd = Args::clap_command(); diff --git a/crates/warp_cli/src/date_time.rs b/crates/warp_cli/src/date_time.rs index 2d24494a06..cbb00ae0ce 100644 --- a/crates/warp_cli/src/date_time.rs +++ b/crates/warp_cli/src/date_time.rs @@ -4,5 +4,9 @@ use chrono::{DateTime, Utc}; pub(crate) fn parse_rfc3339(s: &str) -> Result, String> { DateTime::parse_from_rfc3339(s) .map(|dt| dt.with_timezone(&Utc)) - .map_err(|e| format!("invalid RFC 3339 timestamp '{s}': {e}")) + .map_err(|e| { + i18n::t("warp_cli.error.invalid_rfc3339") + .replace("{value}", s) + .replace("{error}", &e.to_string()) + }) } diff --git a/crates/warp_cli/src/environment.rs b/crates/warp_cli/src/environment.rs index d426e47ff7..dcb1517255 100644 --- a/crates/warp_cli/src/environment.rs +++ b/crates/warp_cli/src/environment.rs @@ -9,10 +9,9 @@ const MAX_DESCRIPTION_LENGTH: usize = 240; fn validate_description(s: &str) -> Result { let len = s.chars().count(); if len > MAX_DESCRIPTION_LENGTH { - Err(format!( - "Description must be at most {} characters (got {})", - MAX_DESCRIPTION_LENGTH, len - )) + Err(i18n::t("warp_cli.environment.error.description_too_long") + .replace("{max}", &MAX_DESCRIPTION_LENGTH.to_string()) + .replace("{len}", &len.to_string())) } else { Ok(s.to_string()) } diff --git a/crates/warp_cli/src/json_filter.rs b/crates/warp_cli/src/json_filter.rs index da5600ca8d..d961c81e7f 100644 --- a/crates/warp_cli/src/json_filter.rs +++ b/crates/warp_cli/src/json_filter.rs @@ -71,7 +71,9 @@ pub fn parse_jq_filter(src: &str) -> Result { .iter() .map(|report| FileReportsDisp::new(report).to_string()) .collect::(); - format!("invalid jq filter `{src}`:\n{detail}") + i18n::t("warp_cli.error.invalid_jq_filter") + .replace("{src}", src) + .replace("{detail}", &detail) })?; Ok(JqFilter(Arc::new(compiled))) } diff --git a/crates/warp_cli/src/lib.rs b/crates/warp_cli/src/lib.rs index c2149a6a91..f52ed7c964 100644 --- a/crates/warp_cli/src/lib.rs +++ b/crates/warp_cli/src/lib.rs @@ -187,72 +187,56 @@ impl Args { if !FeatureFlag::CloudEnvironments.is_enabled() { let args: Vec = env::args().collect(); if args.len() > 1 && args[1] == "environment" { - eprintln!("error: unrecognized subcommand 'environment'\n"); - eprintln!("For more information, try '--help'"); - std::process::exit(2); + exit_unrecognized_subcommand("environment"); } } if !FeatureFlag::ProviderCommand.is_enabled() { let args: Vec = env::args().collect(); if args.len() > 1 && args[1] == "provider" { - eprintln!("error: unrecognized subcommand 'provider'\n"); - eprintln!("For more information, try '--help'"); - std::process::exit(2); + exit_unrecognized_subcommand("provider"); } } if !FeatureFlag::IntegrationCommand.is_enabled() { let args: Vec = env::args().collect(); if args.len() > 1 && args[1] == "integration" { - eprintln!("error: unrecognized subcommand 'integration'\n"); - eprintln!("For more information, try '--help'"); - std::process::exit(2); + exit_unrecognized_subcommand("integration"); } } if !FeatureFlag::ScheduledAmbientAgents.is_enabled() { let args: Vec = env::args().collect(); if args.len() > 1 && args[1] == "schedule" { - eprintln!("error: unrecognized subcommand 'schedule'\n"); - eprintln!("For more information, try '--help'"); - std::process::exit(2); + exit_unrecognized_subcommand("schedule"); } } if !FeatureFlag::WarpManagedSecrets.is_enabled() { let args: Vec = env::args().collect(); if args.len() > 1 && args[1] == "secret" { - eprintln!("error: unrecognized subcommand 'secret'\n"); - eprintln!("For more information, try '--help'"); - std::process::exit(2); + exit_unrecognized_subcommand("secret"); } } if !FeatureFlag::OzIdentityFederation.is_enabled() { let args: Vec = env::args().collect(); if args.len() > 1 && args[1] == "federate" { - eprintln!("error: unrecognized subcommand 'federate'\n"); - eprintln!("For more information, try '--help'"); - std::process::exit(2); + exit_unrecognized_subcommand("federate"); } } if !FeatureFlag::ArtifactCommand.is_enabled() { let args: Vec = env::args().collect(); if args.len() > 1 && args[1] == "artifact" { - eprintln!("error: unrecognized subcommand 'artifact'\n"); - eprintln!("For more information, try '--help'"); - std::process::exit(2); + exit_unrecognized_subcommand("artifact"); } } if !FeatureFlag::APIKeyManagement.is_enabled() { let args: Vec = env::args().collect(); if args.len() > 1 && args[1] == "api-key" { - eprintln!("error: unrecognized subcommand 'api-key'\n"); - eprintln!("For more information, try '--help'"); - std::process::exit(2); + exit_unrecognized_subcommand("api-key"); } } @@ -276,6 +260,7 @@ impl Args { /// IMPORTANT: use this instead of [`CommandFactory::command`], since we customize the command at runtime. pub fn clap_command() -> clap::Command { let mut command = ::command(); + command = localize_clap_command(command); // Hide the environment subcommands and --environment flags from help text if !FeatureFlag::CloudEnvironments.is_enabled() { @@ -374,18 +359,8 @@ impl Args { // Substitute the actual binary name into help output. Ideally clap would do this for us. let bin_name = binary_name().unwrap_or_else(|| ChannelState::channel().cli_command_name().to_string()); - command = command.after_help(color_print::cformat!( - r#"Examples: - - $ {bin_name} agent run --prompt "Build anything" - - $ {bin_name} mcp list - -Learn more: -* Use {bin_name} help to learn more about each command -* Read the documentation at https://docs.warp.dev/reference/cli -"# - )); + command = + command.after_help(i18n::t("warp_cli.after_help").replace("{bin_name}", &bin_name)); command } @@ -438,6 +413,1323 @@ impl Args { } } +fn localize_clap_command(mut command: clap::Command) -> clap::Command { + command = command + .about(i18n::t("warp_cli.about")) + .mut_arg("api_key", |arg| { + arg.help(i18n::t("warp_cli.arg.api_key.help")) + }) + .mut_arg("output_format", |arg| { + arg.help(i18n::t("warp_cli.arg.output_format.help")) + }) + .mut_arg("debug", |arg| arg.help(i18n::t("warp_cli.arg.debug.help"))); + + command = command + .mut_subcommand("agent", |cmd| { + localize_agent_command(cmd.about(i18n::t("warp_cli.command.agent.about"))) + }) + .mut_subcommand("environment", |cmd| { + localize_environment_cli_command( + cmd.about(i18n::t("warp_cli.command.environment.about")), + ) + }) + .mut_subcommand("mcp", |cmd| { + cmd.about(i18n::t("warp_cli.command.mcp.about")) + .mut_subcommand("list", |cmd| { + cmd.about(i18n::t("warp_cli.command.mcp.list.about")) + }) + }) + .mut_subcommand("run", |cmd| { + localize_task_command(cmd.about(i18n::t("warp_cli.command.run.about"))) + }) + .mut_subcommand("model", |cmd| { + localize_model_command(cmd.about(i18n::t("warp_cli.command.model.about"))) + }) + .mut_subcommand("login", |cmd| { + cmd.about(i18n::t("warp_cli.command.login.about")) + }) + .mut_subcommand("logout", |cmd| { + cmd.about(i18n::t("warp_cli.command.logout.about")) + }) + .mut_subcommand("whoami", |cmd| { + cmd.about(i18n::t("warp_cli.command.whoami.about")) + }) + .mut_subcommand("provider", |cmd| { + localize_provider_command(cmd.about(i18n::t("warp_cli.command.provider.about"))) + }) + .mut_subcommand("integration", |cmd| { + localize_integration_command(cmd.about(i18n::t("warp_cli.command.integration.about"))) + }) + .mut_subcommand("schedule", |cmd| { + localize_schedule_command( + cmd.about(i18n::t("warp_cli.command.schedule.about")) + .long_about(i18n::t("warp_cli.command.schedule.long_about")), + ) + }) + .mut_subcommand("secret", |cmd| { + localize_secret_command(cmd.about(i18n::t("warp_cli.command.secret.about"))) + }) + .mut_subcommand("federate", |cmd| { + localize_federate_command( + cmd.about(i18n::t("warp_cli.command.federate.about")) + .long_about(i18n::t("warp_cli.command.federate.long_about")), + ) + }) + .mut_subcommand("artifact", |cmd| { + localize_artifact_command(cmd.about(i18n::t("warp_cli.command.artifact.about"))) + }) + .mut_subcommand("api-key", |cmd| { + localize_api_key_command(cmd.about(i18n::t("warp_cli.command.api_key.about"))) + }) + .mut_subcommand("completions", |cmd| { + cmd.about(i18n::t("warp_cli.command.completions.about")) + .long_about(i18n::t("warp_cli.command.completions.long_about")) + .mut_arg("shell", |arg| { + arg.help(i18n::t("warp_cli.command.completions.shell.help")) + }) + }) + .mut_subcommand("dump-debug-info", |cmd| { + cmd.about(i18n::t("warp_cli.command.dump_debug_info.about")) + }) + .mut_subcommand("harness-support", |cmd| { + localize_harness_support_command( + cmd.about(i18n::t("warp_cli.command.harness_support.about")), + ) + }) + .mut_subcommand("minidump-server", |cmd| { + cmd.about(i18n::t("warp_cli.command.worker.minidump_server.about")) + .mut_arg("socket_name", |arg| { + arg.help(i18n::t("warp_cli.worker.arg.minidump_socket_name.help")) + }) + }); + + #[cfg(unix)] + { + command = command.mut_subcommand("terminal-server", |cmd| { + cmd.about(i18n::t("warp_cli.command.worker.terminal_server.about")) + }); + } + + #[cfg(feature = "plugin_host")] + { + command = command.mut_subcommand("plugin-host", |cmd| { + cmd.about(i18n::t("warp_cli.command.worker.plugin_host.about")) + }); + } + + #[cfg(not(target_family = "wasm"))] + { + command = command + .mut_subcommand("remote-server-proxy", |cmd| { + cmd.about(i18n::t("warp_cli.command.worker.remote_server_proxy.about")) + }) + .mut_subcommand("remote-server-daemon", |cmd| { + cmd.about(i18n::t( + "warp_cli.command.worker.remote_server_daemon.about", + )) + }) + .mut_subcommand("ripgrep-search", |cmd| { + cmd.about(i18n::t("warp_cli.command.worker.ripgrep_search.about")) + .mut_arg("ignore_case", |arg| { + arg.help(i18n::t("warp_cli.worker.arg.ripgrep_ignore_case.help")) + }) + .mut_arg("multiline", |arg| { + arg.help(i18n::t("warp_cli.worker.arg.ripgrep_multiline.help")) + }) + .mut_arg("pattern", |arg| { + arg.help(i18n::t("warp_cli.worker.arg.ripgrep_pattern.help")) + }) + .mut_arg("paths", |arg| { + arg.help(i18n::t("warp_cli.worker.arg.ripgrep_paths.help")) + }) + }) + .mut_subcommand("print-telemetry-events", |cmd| { + cmd.about(i18n::t("warp_cli.command.print_telemetry_events.about")) + }); + } + + command +} + +fn localize_agent_command(command: clap::Command) -> clap::Command { + command + .mut_subcommand("run", |cmd| { + localize_agent_run_args(cmd.about(i18n::t("warp_cli.command.agent.run.about"))) + }) + .mut_subcommand("run-cloud", |cmd| { + localize_agent_run_cloud_args( + cmd.about(i18n::t("warp_cli.command.agent.run_cloud.about")), + ) + }) + .mut_subcommand("profile", |cmd| { + cmd.about(i18n::t("warp_cli.command.agent.profile.about")) + .mut_subcommand("list", |cmd| { + cmd.about(i18n::t("warp_cli.command.agent.profile.list.about")) + }) + }) + .mut_subcommand("list", |cmd| { + localize_agent_list_args(cmd.about(i18n::t("warp_cli.command.agent.list.about"))) + }) + .mut_subcommand("get", |cmd| { + localize_agent_get_args(cmd.about(i18n::t("warp_cli.command.agent.get.about"))) + }) + .mut_subcommand("create", |cmd| { + localize_agent_create_args(cmd.about(i18n::t("warp_cli.command.agent.create.about"))) + }) + .mut_subcommand("update", |cmd| { + localize_agent_update_args(cmd.about(i18n::t("warp_cli.command.agent.update.about"))) + }) + .mut_subcommand("delete", |cmd| { + localize_agent_delete_args(cmd.about(i18n::t("warp_cli.command.agent.delete.about"))) + }) + .mut_subcommand("skills", |cmd| { + localize_agent_skills_args(cmd.about(i18n::t("warp_cli.command.agent.skills.about"))) + }) +} + +fn localize_agent_run_args(command: clap::Command) -> clap::Command { + localize_snapshot_args(localize_config_file_args(localize_model_args( + localize_prompt_args(command), + ))) + .mut_arg("skill", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.skill.help")) + .long_help(i18n::t("warp_cli.agent.arg.skill.long_help")) + }) + .mut_arg("name", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.name.help")) + }) + .mut_arg("cwd", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.cwd.help")) + }) + .mut_arg("share", |arg| { + arg.help(i18n::t("warp_cli.share.arg.help")) + .long_help(i18n::t("warp_cli.share.arg.long_help")) + }) + .mut_arg("mcp_specs", |arg| { + arg.help(i18n::t("warp_cli.mcp.arg.spec.help")) + .long_help(i18n::t("warp_cli.mcp.arg.spec.long_help")) + }) + .mut_arg("environment", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.environment.help")) + }) + .mut_arg("conversation", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.conversation.help")) + }) + .mut_arg("profile", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.profile.help")) + }) +} + +fn localize_agent_run_cloud_args(command: clap::Command) -> clap::Command { + localize_snapshot_args(localize_computer_use_args(localize_scope_args( + localize_environment_create_args(localize_config_file_args(localize_model_args( + localize_prompt_args(command), + ))), + ))) + .mut_arg("skill", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.skill.help")) + .long_help(i18n::t("warp_cli.agent.arg.skill.long_help")) + }) + .mut_arg("name", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.name.help")) + }) + .mut_arg("mcp_specs", |arg| { + arg.help(i18n::t("warp_cli.mcp.arg.spec.help")) + .long_help(i18n::t("warp_cli.mcp.arg.spec.long_help")) + }) + .mut_arg("open", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.open.help")) + }) + .mut_arg("conversation", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.conversation.help")) + }) + .mut_arg("agent_uid", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.agent_uid.help")) + .long_help(i18n::t("warp_cli.agent.arg.agent_uid.long_help")) + }) + .mut_arg("worker_host", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.worker_host.help")) + .long_help(i18n::t("warp_cli.agent.arg.worker_host.long_help")) + }) + .mut_arg("attachment_paths", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.attachment_paths.help")) + .long_help(i18n::t("warp_cli.agent.arg.attachment_paths.long_help")) + }) +} + +fn localize_agent_list_args(command: clap::Command) -> clap::Command { + localize_json_output_args(command) + .mut_arg("sort_by", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.sort_by.help")) + }) + .mut_arg("sort_order", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.sort_order.help")) + }) +} + +fn localize_agent_get_args(command: clap::Command) -> clap::Command { + localize_json_output_args(command).mut_arg("uid", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.uid.get.help")) + }) +} + +fn localize_agent_create_args(command: clap::Command) -> clap::Command { + localize_json_output_args(command) + .mut_arg("name", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.create.name.help")) + }) + .mut_arg("description", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.create.description.help")) + }) + .mut_arg("secrets", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.create.secrets.help")) + }) + .mut_arg("skills", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.create.skills.help")) + }) + .mut_arg("base_model", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.create.base_model.help")) + }) + .mut_arg("environment", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.create.environment.help")) + }) +} + +fn localize_agent_update_args(command: clap::Command) -> clap::Command { + localize_json_output_args(command) + .mut_arg("uid", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.uid.update.help")) + }) + .mut_arg("name", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.update.name.help")) + }) + .mut_arg("description", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.update.description.help")) + }) + .mut_arg("remove_description", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.update.remove_description.help")) + }) + .mut_arg("add_secrets", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.update.add_secrets.help")) + }) + .mut_arg("remove_secrets", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.update.remove_secrets.help")) + }) + .mut_arg("remove_all_secrets", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.update.remove_all_secrets.help")) + }) + .mut_arg("add_skills", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.update.add_skills.help")) + }) + .mut_arg("remove_skills", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.update.remove_skills.help")) + }) + .mut_arg("remove_all_skills", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.update.remove_all_skills.help")) + }) + .mut_arg("base_model", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.update.base_model.help")) + }) + .mut_arg("remove_base_model", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.update.remove_base_model.help")) + }) + .mut_arg("environment", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.update.environment.help")) + }) + .mut_arg("remove_environment", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.update.remove_environment.help")) + }) +} + +fn localize_agent_delete_args(command: clap::Command) -> clap::Command { + command.mut_arg("uid", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.uid.delete.help")) + }) +} + +fn localize_agent_skills_args(command: clap::Command) -> clap::Command { + command.mut_arg("repo", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.skills.repo.help")) + .long_help(i18n::t("warp_cli.agent.arg.skills.repo.long_help")) + }) +} + +fn localize_task_command(command: clap::Command) -> clap::Command { + command + .mut_subcommand("list", |cmd| { + localize_task_list_args(cmd.about(i18n::t("warp_cli.command.run.list.about"))) + }) + .mut_subcommand("get", |cmd| { + localize_task_get_args(cmd.about(i18n::t("warp_cli.command.run.get.about"))) + }) + .mut_subcommand("conversation", |cmd| { + localize_conversation_command( + cmd.about(i18n::t("warp_cli.command.run.conversation.about")), + ) + }) + .mut_subcommand("message", |cmd| { + localize_message_command(cmd.about(i18n::t("warp_cli.command.run.message.about"))) + }) +} + +fn localize_task_list_args(command: clap::Command) -> clap::Command { + localize_json_output_args(command) + .mut_arg("limit", |arg| { + arg.help(i18n::t("warp_cli.task.arg.limit.help")) + }) + .mut_arg("state", |arg| { + arg.help(i18n::t("warp_cli.task.arg.state.help")) + }) + .mut_arg("source", |arg| { + arg.help(i18n::t("warp_cli.task.arg.source.help")) + }) + .mut_arg("execution_location", |arg| { + arg.help(i18n::t("warp_cli.task.arg.execution_location.help")) + }) + .mut_arg("creator", |arg| { + arg.help(i18n::t("warp_cli.task.arg.creator.help")) + }) + .mut_arg("environment", |arg| { + arg.help(i18n::t("warp_cli.task.arg.environment.help")) + }) + .mut_arg("skill", |arg| { + arg.help(i18n::t("warp_cli.task.arg.skill.help")) + }) + .mut_arg("schedule", |arg| { + arg.help(i18n::t("warp_cli.task.arg.schedule.help")) + }) + .mut_arg("ancestor_run", |arg| { + arg.help(i18n::t("warp_cli.task.arg.ancestor_run.help")) + }) + .mut_arg("name", |arg| { + arg.help(i18n::t("warp_cli.task.arg.name.help")) + }) + .mut_arg("model", |arg| { + arg.help(i18n::t("warp_cli.task.arg.model.help")) + }) + .mut_arg("artifact_type", |arg| { + arg.help(i18n::t("warp_cli.task.arg.artifact_type.help")) + }) + .mut_arg("created_after", |arg| { + arg.help(i18n::t("warp_cli.task.arg.created_after.help")) + }) + .mut_arg("created_before", |arg| { + arg.help(i18n::t("warp_cli.task.arg.created_before.help")) + }) + .mut_arg("updated_after", |arg| { + arg.help(i18n::t("warp_cli.task.arg.updated_after.help")) + }) + .mut_arg("query", |arg| { + arg.help(i18n::t("warp_cli.task.arg.query.help")) + }) + .mut_arg("sort_by", |arg| { + arg.help(i18n::t("warp_cli.task.arg.sort_by.help")) + }) + .mut_arg("sort_order", |arg| { + arg.help(i18n::t("warp_cli.task.arg.sort_order.help")) + }) + .mut_arg("cursor", |arg| { + arg.help(i18n::t("warp_cli.task.arg.cursor.help")) + .long_help(i18n::t("warp_cli.task.arg.cursor.long_help")) + }) +} + +fn localize_task_get_args(command: clap::Command) -> clap::Command { + localize_json_output_args(command) + .mut_arg("task_id", |arg| { + arg.help(i18n::t("warp_cli.task.arg.task_id.help")) + }) + .mut_arg("conversation", |arg| { + arg.help(i18n::t("warp_cli.task.arg.conversation.help")) + }) +} + +fn localize_conversation_command(command: clap::Command) -> clap::Command { + command.mut_subcommand("get", |cmd| { + cmd.about(i18n::t("warp_cli.command.run.conversation.get.about")) + .mut_arg("conversation_id", |arg| { + arg.help(i18n::t("warp_cli.task.arg.conversation_id.help")) + }) + }) +} + +fn localize_message_command(command: clap::Command) -> clap::Command { + command + .mut_subcommand("watch", |cmd| { + localize_message_watch_args( + cmd.about(i18n::t("warp_cli.command.run.message.watch.about")), + ) + }) + .mut_subcommand("send", |cmd| { + localize_message_send_args( + cmd.about(i18n::t("warp_cli.command.run.message.send.about")), + ) + }) + .mut_subcommand("list", |cmd| { + localize_message_list_args( + cmd.about(i18n::t("warp_cli.command.run.message.list.about")), + ) + }) + .mut_subcommand("read", |cmd| { + localize_message_read_args( + cmd.about(i18n::t("warp_cli.command.run.message.read.about")), + ) + }) + .mut_subcommand("mark-delivered", |cmd| { + localize_message_delivered_args( + cmd.about(i18n::t("warp_cli.command.run.message.mark_delivered.about")), + ) + }) +} + +fn localize_message_send_args(command: clap::Command) -> clap::Command { + command + .mut_arg("to", |arg| { + arg.help(i18n::t("warp_cli.task.arg.message.to.help")) + }) + .mut_arg("subject", |arg| { + arg.help(i18n::t("warp_cli.task.arg.message.subject.help")) + }) + .mut_arg("body", |arg| { + arg.help(i18n::t("warp_cli.task.arg.message.body.help")) + }) + .mut_arg("sender_run_id", |arg| { + arg.help(i18n::t("warp_cli.task.arg.message.sender_run_id.help")) + }) +} + +fn localize_message_list_args(command: clap::Command) -> clap::Command { + command + .mut_arg("run_id", |arg| { + arg.help(i18n::t("warp_cli.task.arg.message.run_id.list.help")) + }) + .mut_arg("unread", |arg| { + arg.help(i18n::t("warp_cli.task.arg.message.unread.help")) + }) + .mut_arg("since", |arg| { + arg.help(i18n::t("warp_cli.task.arg.message.since.help")) + }) + .mut_arg("limit", |arg| { + arg.help(i18n::t("warp_cli.task.arg.message.limit.help")) + }) +} + +fn localize_message_watch_args(command: clap::Command) -> clap::Command { + command + .mut_arg("run_id", |arg| { + arg.help(i18n::t("warp_cli.task.arg.message.run_id.watch.help")) + }) + .mut_arg("since_sequence", |arg| { + arg.help(i18n::t("warp_cli.task.arg.message.since_sequence.help")) + }) +} + +fn localize_message_read_args(command: clap::Command) -> clap::Command { + command.mut_arg("message_id", |arg| { + arg.help(i18n::t("warp_cli.task.arg.message.message_id.read.help")) + }) +} + +fn localize_message_delivered_args(command: clap::Command) -> clap::Command { + command.mut_arg("message_id", |arg| { + arg.help(i18n::t( + "warp_cli.task.arg.message.message_id.mark_delivered.help", + )) + }) +} + +fn localize_schedule_command(command: clap::Command) -> clap::Command { + localize_schedule_create_args(command) + .mut_subcommand("create", |cmd| { + localize_schedule_create_args( + cmd.about(i18n::t("warp_cli.command.schedule.create.about")), + ) + }) + .mut_subcommand("list", |cmd| { + cmd.about(i18n::t("warp_cli.command.schedule.list.about")) + }) + .mut_subcommand("get", |cmd| { + localize_schedule_get_args(cmd.about(i18n::t("warp_cli.command.schedule.get.about"))) + }) + .mut_subcommand("update", |cmd| { + localize_schedule_update_args( + cmd.about(i18n::t("warp_cli.command.schedule.update.about")), + ) + }) + .mut_subcommand("pause", |cmd| { + localize_schedule_pause_args( + cmd.about(i18n::t("warp_cli.command.schedule.pause.about")) + .long_about(i18n::t("warp_cli.command.schedule.pause.long_about")), + ) + }) + .mut_subcommand("unpause", |cmd| { + localize_schedule_unpause_args( + cmd.about(i18n::t("warp_cli.command.schedule.unpause.about")) + .long_about(i18n::t("warp_cli.command.schedule.unpause.long_about")), + ) + }) + .mut_subcommand("delete", |cmd| { + localize_schedule_delete_args( + cmd.about(i18n::t("warp_cli.command.schedule.delete.about")), + ) + }) +} + +fn localize_schedule_create_args(command: clap::Command) -> clap::Command { + localize_scope_args(localize_environment_create_args(localize_config_file_args( + localize_model_args(command), + ))) + .mut_arg("name", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.name.create.help")) + }) + .mut_arg("cron", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.cron.create.help")) + }) + .mut_arg("mcp_specs", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.mcp_specs.help")) + .long_help(i18n::t("warp_cli.schedule.arg.mcp_specs.long_help")) + }) + .mut_arg("prompt", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.prompt.create.help")) + }) + .mut_arg("skill", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.skill.create.help")) + .long_help(i18n::t("warp_cli.schedule.arg.skill.create.long_help")) + }) + .mut_arg("worker_host", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.worker_host.help")) + .long_help(i18n::t("warp_cli.schedule.arg.worker_host.long_help")) + }) +} + +fn localize_schedule_get_args(command: clap::Command) -> clap::Command { + command.mut_arg("schedule_id", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.schedule_id.get.help")) + }) +} + +fn localize_schedule_update_args(command: clap::Command) -> clap::Command { + localize_schedule_environment_update_args(localize_config_file_args(localize_model_args( + command, + ))) + .mut_arg("schedule_id", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.schedule_id.update.help")) + }) + .mut_arg("name", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.name.update.help")) + }) + .mut_arg("cron", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.cron.update.help")) + }) + .mut_arg("mcp_specs", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.mcp_specs.help")) + .long_help(i18n::t("warp_cli.schedule.arg.mcp_specs.long_help")) + }) + .mut_arg("remove_mcp", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.remove_mcp.help")) + .long_help(i18n::t("warp_cli.schedule.arg.remove_mcp.long_help")) + }) + .mut_arg("prompt", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.prompt.update.help")) + }) + .mut_arg("skill", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.skill.update.help")) + .long_help(i18n::t("warp_cli.schedule.arg.skill.update.long_help")) + }) + .mut_arg("remove_skill", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.remove_skill.help")) + }) + .mut_arg("worker_host", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.worker_host.help")) + .long_help(i18n::t("warp_cli.schedule.arg.worker_host.long_help")) + }) +} + +fn localize_schedule_environment_update_args(command: clap::Command) -> clap::Command { + command + .mut_arg("environment", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.environment.update.help")) + }) + .mut_arg("remove_environment", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.remove_environment.help")) + }) +} + +fn localize_schedule_pause_args(command: clap::Command) -> clap::Command { + command.mut_arg("schedule_id", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.schedule_id.pause.help")) + }) +} + +fn localize_schedule_unpause_args(command: clap::Command) -> clap::Command { + command.mut_arg("schedule_id", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.schedule_id.unpause.help")) + }) +} + +fn localize_schedule_delete_args(command: clap::Command) -> clap::Command { + command.mut_arg("schedule_id", |arg| { + arg.help(i18n::t("warp_cli.schedule.arg.schedule_id.delete.help")) + }) +} + +fn localize_environment_cli_command(command: clap::Command) -> clap::Command { + command + .mut_subcommand("list", |cmd| { + cmd.about(i18n::t("warp_cli.command.environment.list.about")) + }) + .mut_subcommand("image", |cmd| { + cmd.about(i18n::t("warp_cli.command.environment.image.about")) + .mut_subcommand("list", |cmd| { + cmd.about(i18n::t("warp_cli.command.environment.image.list.about")) + }) + }) + .mut_subcommand("create", |cmd| { + localize_environment_create_command_args( + cmd.about(i18n::t("warp_cli.command.environment.create.about")), + ) + }) + .mut_subcommand("delete", |cmd| { + localize_environment_delete_command_args( + cmd.about(i18n::t("warp_cli.command.environment.delete.about")), + ) + }) + .mut_subcommand("get", |cmd| { + localize_environment_get_command_args( + cmd.about(i18n::t("warp_cli.command.environment.get.about")), + ) + }) + .mut_subcommand("update", |cmd| { + localize_environment_update_command_args( + cmd.about(i18n::t("warp_cli.command.environment.update.about")), + ) + }) +} + +fn localize_environment_create_command_args(command: clap::Command) -> clap::Command { + localize_scope_args(command) + .mut_arg("name", |arg| { + arg.help(i18n::t("warp_cli.environment.arg.name.create.help")) + }) + .mut_arg("description", |arg| { + arg.help(i18n::t("warp_cli.environment.arg.description.create.help")) + }) + .mut_arg("docker_image", |arg| { + arg.help(i18n::t("warp_cli.environment.arg.docker_image.create.help")) + .long_help(i18n::t( + "warp_cli.environment.arg.docker_image.create.long_help", + )) + }) + .mut_arg("repo", |arg| { + arg.help(i18n::t("warp_cli.environment.arg.repo.create.help")) + }) + .mut_arg("setup_command", |arg| { + arg.help(i18n::t( + "warp_cli.environment.arg.setup_command.create.help", + )) + }) +} + +fn localize_environment_delete_command_args(command: clap::Command) -> clap::Command { + command + .mut_arg("id", |arg| { + arg.help(i18n::t("warp_cli.environment.arg.id.delete.help")) + }) + .mut_arg("force", |arg| { + arg.help(i18n::t("warp_cli.environment.arg.force.delete.help")) + }) +} + +fn localize_environment_get_command_args(command: clap::Command) -> clap::Command { + command.mut_arg("id", |arg| { + arg.help(i18n::t("warp_cli.environment.arg.id.get.help")) + }) +} + +fn localize_environment_update_command_args(command: clap::Command) -> clap::Command { + command + .mut_arg("id", |arg| { + arg.help(i18n::t("warp_cli.environment.arg.id.update.help")) + }) + .mut_arg("name", |arg| { + arg.help(i18n::t("warp_cli.environment.arg.name.update.help")) + }) + .mut_arg("description", |arg| { + arg.help(i18n::t("warp_cli.environment.arg.description.update.help")) + }) + .mut_arg("remove_description", |arg| { + arg.help(i18n::t("warp_cli.environment.arg.remove_description.help")) + }) + .mut_arg("docker_image", |arg| { + arg.help(i18n::t("warp_cli.environment.arg.docker_image.update.help")) + }) + .mut_arg("repo", |arg| { + arg.help(i18n::t("warp_cli.environment.arg.repo.update.help")) + }) + .mut_arg("setup_command", |arg| { + arg.help(i18n::t( + "warp_cli.environment.arg.setup_command.update.help", + )) + }) + .mut_arg("remove_repo", |arg| { + arg.help(i18n::t("warp_cli.environment.arg.remove_repo.help")) + }) + .mut_arg("remove_setup_command", |arg| { + arg.help(i18n::t( + "warp_cli.environment.arg.remove_setup_command.help", + )) + }) + .mut_arg("force", |arg| { + arg.help(i18n::t("warp_cli.environment.arg.force.update.help")) + }) +} + +fn localize_secret_command(command: clap::Command) -> clap::Command { + command + .mut_subcommand("create", |cmd| { + localize_secret_create_args( + cmd.about(i18n::t("warp_cli.command.secret.create.about")) + .long_about(i18n::t("warp_cli.command.secret.create.long_about")), + ) + }) + .mut_subcommand("delete", |cmd| { + localize_secret_delete_args(cmd.about(i18n::t("warp_cli.command.secret.delete.about"))) + }) + .mut_subcommand("update", |cmd| { + localize_secret_update_args( + cmd.about(i18n::t("warp_cli.command.secret.update.about")) + .long_about(i18n::t("warp_cli.command.secret.update.long_about")), + ) + }) + .mut_subcommand("list", |cmd| { + cmd.about(i18n::t("warp_cli.command.secret.list.about")) + }) +} + +fn localize_secret_create_args(command: clap::Command) -> clap::Command { + localize_secret_value_args(localize_common_secret_create_args(command)) + .mut_arg("secret_type", |arg| { + arg.help(i18n::t("warp_cli.secret.arg.secret_type.help")) + }) + .mut_subcommand("claude", |cmd| { + localize_secret_claude_create_command( + cmd.about(i18n::t("warp_cli.command.secret.create.claude.about")), + ) + }) + .mut_subcommand("codex", |cmd| { + localize_secret_codex_create_command( + cmd.about(i18n::t("warp_cli.command.secret.create.codex.about")), + ) + }) +} + +fn localize_secret_claude_create_command(command: clap::Command) -> clap::Command { + command + .mut_subcommand("api-key", |cmd| { + localize_anthropic_api_key_args(cmd.about(i18n::t( + "warp_cli.command.secret.create.claude.api_key.about", + ))) + }) + .mut_subcommand("bedrock-api-key", |cmd| { + localize_bedrock_api_key_args(cmd.about(i18n::t( + "warp_cli.command.secret.create.claude.bedrock_api_key.about", + ))) + }) + .mut_subcommand("bedrock-access-key", |cmd| { + localize_bedrock_access_key_args(cmd.about(i18n::t( + "warp_cli.command.secret.create.claude.bedrock_access_key.about", + ))) + }) +} + +fn localize_secret_codex_create_command(command: clap::Command) -> clap::Command { + command.mut_subcommand("api-key", |cmd| { + localize_openai_api_key_args(cmd.about(i18n::t( + "warp_cli.command.secret.create.codex.api_key.about", + ))) + }) +} + +fn localize_common_secret_create_args(command: clap::Command) -> clap::Command { + localize_scope_args(command) + .mut_arg("name", |arg| { + arg.help(i18n::t("warp_cli.secret.arg.name.create.help")) + }) + .mut_arg("description", |arg| { + arg.help(i18n::t("warp_cli.secret.arg.description.help")) + }) +} + +fn localize_secret_value_args(command: clap::Command) -> clap::Command { + command.mut_arg("value_file", |arg| { + arg.help(i18n::t("warp_cli.secret.arg.value_file.help")) + .long_help(i18n::t("warp_cli.secret.arg.value_file.long_help")) + }) +} + +fn localize_anthropic_api_key_args(command: clap::Command) -> clap::Command { + localize_secret_value_args(localize_common_secret_create_args(command)) +} + +fn localize_bedrock_api_key_args(command: clap::Command) -> clap::Command { + localize_common_secret_create_args(command) + .mut_arg("bedrock_api_key", |arg| { + arg.help(i18n::t("warp_cli.secret.arg.bedrock_api_key.help")) + }) + .mut_arg("region", |arg| { + arg.help(i18n::t("warp_cli.secret.arg.bedrock_region.help")) + }) +} + +fn localize_bedrock_access_key_args(command: clap::Command) -> clap::Command { + localize_common_secret_create_args(command) + .mut_arg("access_key_id", |arg| { + arg.help(i18n::t("warp_cli.secret.arg.aws_access_key_id.help")) + }) + .mut_arg("secret_access_key", |arg| { + arg.help(i18n::t("warp_cli.secret.arg.aws_secret_access_key.help")) + }) + .mut_arg("session_token", |arg| { + arg.help(i18n::t("warp_cli.secret.arg.aws_session_token.help")) + }) + .mut_arg("region", |arg| { + arg.help(i18n::t("warp_cli.secret.arg.bedrock_region.help")) + }) +} + +fn localize_openai_api_key_args(command: clap::Command) -> clap::Command { + localize_secret_value_args(localize_common_secret_create_args(command)).mut_arg( + "base_url", + |arg| { + arg.help(i18n::t("warp_cli.secret.arg.openai_base_url.help")) + .long_help(i18n::t("warp_cli.secret.arg.openai_base_url.long_help")) + }, + ) +} + +fn localize_secret_delete_args(command: clap::Command) -> clap::Command { + localize_scope_args(command) + .mut_arg("name", |arg| { + arg.help(i18n::t("warp_cli.secret.arg.name.delete.help")) + }) + .mut_arg("force", |arg| { + arg.help(i18n::t("warp_cli.secret.arg.force.delete.help")) + }) +} + +fn localize_secret_update_args(command: clap::Command) -> clap::Command { + localize_secret_value_args(localize_scope_args(command)) + .mut_arg("name", |arg| { + arg.help(i18n::t("warp_cli.secret.arg.name.update.help")) + }) + .mut_arg("value", |arg| { + arg.help(i18n::t("warp_cli.secret.arg.value.update.help")) + }) + .mut_arg("description", |arg| { + arg.help(i18n::t("warp_cli.secret.arg.description.update.help")) + }) +} + +fn localize_integration_command(command: clap::Command) -> clap::Command { + command + .mut_subcommand("create", |cmd| { + localize_integration_create_args( + cmd.about(i18n::t("warp_cli.command.integration.create.about")), + ) + }) + .mut_subcommand("update", |cmd| { + localize_integration_update_args( + cmd.about(i18n::t("warp_cli.command.integration.update.about")), + ) + }) + .mut_subcommand("list", |cmd| { + cmd.about(i18n::t("warp_cli.command.integration.list.about")) + }) +} + +fn localize_integration_create_args(command: clap::Command) -> clap::Command { + localize_environment_create_args(localize_config_file_args(localize_model_args(command))) + .mut_arg("provider", |arg| { + arg.help(i18n::t("warp_cli.integration.arg.provider.create.help")) + }) + .mut_arg("mcp_specs", |arg| { + arg.help(i18n::t("warp_cli.integration.arg.mcp_specs.help")) + .long_help(i18n::t("warp_cli.integration.arg.mcp_specs.long_help")) + }) + .mut_arg("prompt", |arg| { + arg.help(i18n::t("warp_cli.integration.arg.prompt.help")) + }) + .mut_arg("worker_host", |arg| { + arg.help(i18n::t("warp_cli.integration.arg.worker_host.help")) + .long_help(i18n::t("warp_cli.integration.arg.worker_host.long_help")) + }) +} + +fn localize_integration_update_args(command: clap::Command) -> clap::Command { + localize_integration_environment_update_args(localize_config_file_args(localize_model_args( + command, + ))) + .mut_arg("provider", |arg| { + arg.help(i18n::t("warp_cli.integration.arg.provider.update.help")) + }) + .mut_arg("mcp_specs", |arg| { + arg.help(i18n::t("warp_cli.integration.arg.mcp_specs.help")) + .long_help(i18n::t("warp_cli.integration.arg.mcp_specs.long_help")) + }) + .mut_arg("remove_mcp", |arg| { + arg.help(i18n::t("warp_cli.integration.arg.remove_mcp.help")) + .long_help(i18n::t("warp_cli.integration.arg.remove_mcp.long_help")) + }) + .mut_arg("prompt", |arg| { + arg.help(i18n::t("warp_cli.integration.arg.prompt.help")) + }) + .mut_arg("worker_host", |arg| { + arg.help(i18n::t("warp_cli.integration.arg.worker_host.help")) + .long_help(i18n::t("warp_cli.integration.arg.worker_host.long_help")) + }) +} + +fn localize_integration_environment_update_args(command: clap::Command) -> clap::Command { + command + .mut_arg("environment", |arg| { + arg.help(i18n::t("warp_cli.integration.arg.environment.update.help")) + }) + .mut_arg("remove_environment", |arg| { + arg.help(i18n::t("warp_cli.integration.arg.remove_environment.help")) + }) +} + +fn localize_api_key_command(command: clap::Command) -> clap::Command { + command + .mut_subcommand("list", |cmd| { + localize_api_key_list_args(cmd.about(i18n::t("warp_cli.command.api_key.list.about"))) + }) + .mut_subcommand("create", |cmd| { + localize_api_key_create_args( + cmd.about(i18n::t("warp_cli.command.api_key.create.about")), + ) + }) + .mut_subcommand("expire", |cmd| { + localize_api_key_expire_args( + cmd.about(i18n::t("warp_cli.command.api_key.expire.about")), + ) + }) +} + +fn localize_api_key_list_args(command: clap::Command) -> clap::Command { + localize_json_output_args(command) + .mut_arg("sort_by", |arg| { + arg.help(i18n::t("warp_cli.api_key.arg.sort_by.help")) + }) + .mut_arg("sort_order", |arg| { + arg.help(i18n::t("warp_cli.api_key.arg.sort_order.help")) + }) +} + +fn localize_api_key_create_args(command: clap::Command) -> clap::Command { + localize_json_output_args(command) + .mut_arg("name", |arg| { + arg.help(i18n::t("warp_cli.api_key.arg.name.create.help")) + }) + .mut_arg("agent_uid", |arg| { + arg.help(i18n::t("warp_cli.api_key.arg.agent_uid.help")) + }) + .mut_arg("expires_in", |arg| { + arg.help(i18n::t("warp_cli.api_key.arg.expires_in.help")) + }) + .mut_arg("expires_at", |arg| { + arg.help(i18n::t("warp_cli.api_key.arg.expires_at.help")) + }) + .mut_arg("no_expiration", |arg| { + arg.help(i18n::t("warp_cli.api_key.arg.no_expiration.help")) + }) +} + +fn localize_api_key_expire_args(command: clap::Command) -> clap::Command { + localize_json_output_args(command) + .mut_arg("key_uid", |arg| { + arg.help(i18n::t("warp_cli.api_key.arg.key_uid.expire.help")) + }) + .mut_arg("force", |arg| { + arg.help(i18n::t("warp_cli.api_key.arg.force.expire.help")) + }) +} + +fn localize_artifact_command(command: clap::Command) -> clap::Command { + command + .mut_subcommand("upload", |cmd| { + localize_artifact_upload_args( + cmd.about(i18n::t("warp_cli.command.artifact.upload.about")), + ) + }) + .mut_subcommand("get", |cmd| { + localize_artifact_get_args(cmd.about(i18n::t("warp_cli.command.artifact.get.about"))) + }) + .mut_subcommand("download", |cmd| { + localize_artifact_download_args( + cmd.about(i18n::t("warp_cli.command.artifact.download.about")), + ) + }) +} + +fn localize_artifact_upload_args(command: clap::Command) -> clap::Command { + command + .mut_arg("path", |arg| { + arg.help(i18n::t("warp_cli.artifact.arg.path.upload.help")) + }) + .mut_arg("run_id", |arg| { + arg.help(i18n::t("warp_cli.artifact.arg.run_id.help")) + }) + .mut_arg("conversation_id", |arg| { + arg.help(i18n::t("warp_cli.artifact.arg.conversation_id.help")) + }) + .mut_arg("description", |arg| { + arg.help(i18n::t("warp_cli.artifact.arg.description.help")) + }) +} + +fn localize_artifact_get_args(command: clap::Command) -> clap::Command { + command.mut_arg("artifact_uid", |arg| { + arg.help(i18n::t("warp_cli.artifact.arg.artifact_uid.get.help")) + }) +} + +fn localize_artifact_download_args(command: clap::Command) -> clap::Command { + command + .mut_arg("artifact_uid", |arg| { + arg.help(i18n::t("warp_cli.artifact.arg.artifact_uid.download.help")) + }) + .mut_arg("out", |arg| { + arg.help(i18n::t("warp_cli.artifact.arg.out.help")) + }) +} + +fn localize_federate_command(command: clap::Command) -> clap::Command { + command + .mut_subcommand("issue-token", |cmd| { + localize_federate_issue_token_args( + cmd.about(i18n::t("warp_cli.command.federate.issue_token.about")), + ) + }) + .mut_subcommand("issue-gcp-token", |cmd| { + localize_federate_issue_gcp_token_args( + cmd.about(i18n::t("warp_cli.command.federate.issue_gcp_token.about")) + .long_about(i18n::t( + "warp_cli.command.federate.issue_gcp_token.long_about", + )), + ) + }) +} + +fn localize_federate_issue_token_args(command: clap::Command) -> clap::Command { + command + .mut_arg("run_id", |arg| { + arg.help(i18n::t("warp_cli.federate.arg.run_id.help")) + }) + .mut_arg("audience", |arg| { + arg.help(i18n::t("warp_cli.federate.arg.audience.help")) + }) + .mut_arg("duration", |arg| { + arg.help(i18n::t("warp_cli.federate.arg.duration.help")) + }) + .mut_arg("subject_template", |arg| { + arg.help(i18n::t("warp_cli.federate.arg.subject_template.help")) + .long_help(i18n::t("warp_cli.federate.arg.subject_template.long_help")) + }) +} + +fn localize_federate_issue_gcp_token_args(command: clap::Command) -> clap::Command { + command + .mut_arg("run_id", |arg| { + arg.help(i18n::t("warp_cli.federate.arg.run_id.help")) + }) + .mut_arg("duration", |arg| { + arg.help(i18n::t("warp_cli.federate.arg.duration.help")) + }) + .mut_arg("audience", |arg| { + arg.help(i18n::t("warp_cli.federate.arg.gcp_audience.help")) + }) + .mut_arg("token_type", |arg| { + arg.help(i18n::t("warp_cli.federate.arg.gcp_token_type.help")) + }) + .mut_arg("output_file", |arg| { + arg.help(i18n::t("warp_cli.federate.arg.gcp_output_file.help")) + }) +} + +fn localize_provider_command(command: clap::Command) -> clap::Command { + command + .mut_subcommand("setup", |cmd| { + localize_provider_setup_args( + cmd.about(i18n::t("warp_cli.command.provider.setup.about")), + ) + }) + .mut_subcommand("list", |cmd| { + cmd.about(i18n::t("warp_cli.command.provider.list.about")) + }) +} + +fn localize_provider_setup_args(command: clap::Command) -> clap::Command { + command + .mut_arg("provider_type", |arg| { + arg.help(i18n::t("warp_cli.provider.arg.provider_type.help")) + }) + .mut_arg("team", |arg| { + arg.help(i18n::t("warp_cli.provider.arg.team.help")) + }) + .mut_arg("personal", |arg| { + arg.help(i18n::t("warp_cli.provider.arg.personal.help")) + }) +} + +fn localize_model_command(command: clap::Command) -> clap::Command { + command.mut_subcommand("list", |cmd| { + cmd.about(i18n::t("warp_cli.command.model.list.about")) + }) +} + +fn localize_harness_support_command(command: clap::Command) -> clap::Command { + command + .mut_arg("run_id", |arg| { + arg.help(i18n::t("warp_cli.harness_support.arg.run_id.help")) + }) + .mut_subcommand("ping", |cmd| { + cmd.about(i18n::t("warp_cli.command.harness_support.ping.about")) + }) + .mut_subcommand("report-artifact", |cmd| { + cmd.about(i18n::t( + "warp_cli.command.harness_support.report_artifact.about", + )) + .mut_subcommand("pull-request", |cmd| { + cmd.about(i18n::t( + "warp_cli.command.harness_support.report_artifact.pull_request.about", + )) + .mut_arg("url", |arg| { + arg.help(i18n::t( + "warp_cli.harness_support.arg.pull_request.url.help", + )) + }) + .mut_arg("branch", |arg| { + arg.help(i18n::t( + "warp_cli.harness_support.arg.pull_request.branch.help", + )) + }) + }) + }) + .mut_subcommand("notify-user", |cmd| { + cmd.about(i18n::t( + "warp_cli.command.harness_support.notify_user.about", + )) + .mut_arg("message", |arg| { + arg.help(i18n::t("warp_cli.harness_support.arg.message.help")) + }) + }) + .mut_subcommand("finish-task", |cmd| { + cmd.about(i18n::t( + "warp_cli.command.harness_support.finish_task.about", + )) + .mut_arg("status", |arg| { + arg.help(i18n::t("warp_cli.harness_support.arg.status.help")) + }) + .mut_arg("summary", |arg| { + arg.help(i18n::t("warp_cli.harness_support.arg.summary.help")) + }) + }) + .mut_subcommand("report-shutdown", |cmd| { + cmd.about(i18n::t( + "warp_cli.command.harness_support.report_shutdown.about", + )) + .mut_arg("error_category", |arg| { + arg.help(i18n::t("warp_cli.harness_support.arg.error_category.help")) + }) + .mut_arg("error_message", |arg| { + arg.help(i18n::t("warp_cli.harness_support.arg.error_message.help")) + }) + }) +} + +fn localize_prompt_args(command: clap::Command) -> clap::Command { + command + .mut_arg("prompt", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.prompt.help")) + }) + .mut_arg("saved_prompt", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.saved_prompt.help")) + }) +} + +fn localize_model_args(command: clap::Command) -> clap::Command { + command.mut_arg("model", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.model.help")) + }) +} + +fn localize_config_file_args(command: clap::Command) -> clap::Command { + command.mut_arg("file", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.config_file.help")) + }) +} + +fn localize_json_output_args(command: clap::Command) -> clap::Command { + command.mut_arg("filter", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.jq_filter.help")) + .long_help(i18n::t("warp_cli.agent.arg.jq_filter.long_help")) + }) +} + +fn localize_snapshot_args(command: clap::Command) -> clap::Command { + command + .mut_arg("no_snapshot", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.no_snapshot.help")) + }) + .mut_arg("snapshot_upload_timeout", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.snapshot_upload_timeout.help")) + }) + .mut_arg("snapshot_script_timeout", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.snapshot_script_timeout.help")) + }) +} + +fn localize_scope_args(command: clap::Command) -> clap::Command { + command + .mut_arg("team", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.scope.team.help")) + }) + .mut_arg("personal", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.scope.personal.help")) + }) +} + +fn localize_environment_create_args(command: clap::Command) -> clap::Command { + command + .mut_arg("environment", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.environment.help")) + }) + .mut_arg("no_environment", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.no_environment.help")) + }) +} + +fn localize_computer_use_args(command: clap::Command) -> clap::Command { + command + .mut_arg("computer_use", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.computer_use.help")) + }) + .mut_arg("no_computer_use", |arg| { + arg.help(i18n::t("warp_cli.agent.arg.no_computer_use.help")) + }) +} + +fn exit_unrecognized_subcommand(subcommand: &str) -> ! { + eprintln!( + "{}", + i18n::t("warp_cli.error.unrecognized_subcommand").replace("{subcommand}", subcommand) + ); + eprintln!(); + eprintln!("{}", i18n::t("warp_cli.error.more_info_help")); + std::process::exit(2); +} + /// Warp may spawn several worker processes - mostly servers that support the main application. /// /// These subcommands run those worker processes, which are bundled into the Warp binary. diff --git a/crates/warp_cli/src/lib_tests.rs b/crates/warp_cli/src/lib_tests.rs index 0979fcba73..35c20348e1 100644 --- a/crates/warp_cli/src/lib_tests.rs +++ b/crates/warp_cli/src/lib_tests.rs @@ -1,6 +1,7 @@ use std::ffi::OsString; use clap::Parser; +use clap::error::ErrorKind; use super::*; use crate::agent::{AgentCommand, Harness, OutputFormat}; @@ -31,6 +32,141 @@ fn restore_env_var(name: &str, previous: Option) { } } +#[test] +fn localized_clap_command_help_paths_do_not_panic() { + warp_core::features::mark_initialized(); + + let mut help_paths: Vec> = vec![ + vec!["oz", "--help"], + vec!["oz", "agent", "--help"], + vec!["oz", "agent", "run", "--help"], + vec!["oz", "agent", "run-cloud", "--help"], + vec!["oz", "agent", "profile", "--help"], + vec!["oz", "agent", "profile", "list", "--help"], + vec!["oz", "agent", "list", "--help"], + vec!["oz", "agent", "get", "--help"], + vec!["oz", "agent", "create", "--help"], + vec!["oz", "agent", "update", "--help"], + vec!["oz", "agent", "delete", "--help"], + vec!["oz", "agent", "skills", "--help"], + vec!["oz", "mcp", "--help"], + vec!["oz", "mcp", "list", "--help"], + vec!["oz", "run", "--help"], + vec!["oz", "run", "list", "--help"], + vec!["oz", "run", "get", "--help"], + vec!["oz", "run", "conversation", "--help"], + vec!["oz", "run", "conversation", "get", "--help"], + vec!["oz", "run", "message", "--help"], + vec!["oz", "run", "message", "send", "--help"], + vec!["oz", "run", "message", "list", "--help"], + vec!["oz", "run", "message", "watch", "--help"], + vec!["oz", "run", "message", "read", "--help"], + vec!["oz", "run", "message", "mark-delivered", "--help"], + vec!["oz", "schedule", "--help"], + vec!["oz", "schedule", "create", "--help"], + vec!["oz", "schedule", "list", "--help"], + vec!["oz", "schedule", "get", "--help"], + vec!["oz", "schedule", "update", "--help"], + vec!["oz", "schedule", "pause", "--help"], + vec!["oz", "schedule", "unpause", "--help"], + vec!["oz", "schedule", "delete", "--help"], + vec!["oz", "environment", "--help"], + vec!["oz", "environment", "list", "--help"], + vec!["oz", "environment", "image", "--help"], + vec!["oz", "environment", "image", "list", "--help"], + vec!["oz", "environment", "create", "--help"], + vec!["oz", "environment", "delete", "--help"], + vec!["oz", "environment", "get", "--help"], + vec!["oz", "environment", "update", "--help"], + vec!["oz", "secret", "--help"], + vec!["oz", "secret", "create", "--help"], + vec!["oz", "secret", "create", "claude", "--help"], + vec!["oz", "secret", "create", "claude", "api-key", "--help"], + vec![ + "oz", + "secret", + "create", + "claude", + "bedrock-api-key", + "--help", + ], + vec![ + "oz", + "secret", + "create", + "claude", + "bedrock-access-key", + "--help", + ], + vec!["oz", "secret", "create", "codex", "--help"], + vec!["oz", "secret", "create", "codex", "api-key", "--help"], + vec!["oz", "secret", "delete", "--help"], + vec!["oz", "secret", "update", "--help"], + vec!["oz", "secret", "list", "--help"], + vec!["oz", "integration", "--help"], + vec!["oz", "integration", "create", "--help"], + vec!["oz", "integration", "update", "--help"], + vec!["oz", "integration", "list", "--help"], + vec!["oz", "api-key", "--help"], + vec!["oz", "api-key", "list", "--help"], + vec!["oz", "api-key", "create", "--help"], + vec!["oz", "api-key", "expire", "--help"], + vec!["oz", "artifact", "--help"], + vec!["oz", "artifact", "upload", "--help"], + vec!["oz", "artifact", "get", "--help"], + vec!["oz", "artifact", "download", "--help"], + vec!["oz", "federate", "--help"], + vec!["oz", "federate", "issue-token", "--help"], + vec!["oz", "federate", "issue-gcp-token", "--help"], + vec!["oz", "provider", "--help"], + vec!["oz", "provider", "setup", "--help"], + vec!["oz", "provider", "list", "--help"], + vec!["oz", "model", "--help"], + vec!["oz", "model", "list", "--help"], + vec!["oz", "login", "--help"], + vec!["oz", "logout", "--help"], + vec!["oz", "whoami", "--help"], + vec!["oz", "completions", "--help"], + vec!["oz", "--dump-debug-info", "--help"], + vec!["oz", "harness-support", "--help"], + vec!["oz", "harness-support", "ping", "--help"], + vec!["oz", "harness-support", "report-artifact", "--help"], + vec![ + "oz", + "harness-support", + "report-artifact", + "pull-request", + "--help", + ], + vec!["oz", "harness-support", "notify-user", "--help"], + vec!["oz", "harness-support", "finish-task", "--help"], + vec!["oz", "harness-support", "report-shutdown", "--help"], + vec!["oz", "minidump-server", "--help"], + ]; + + #[cfg(unix)] + help_paths.push(vec!["oz", "terminal-server", "--help"]); + + #[cfg(feature = "plugin_host")] + help_paths.push(vec!["oz", "--plugin-host", "--help"]); + + #[cfg(not(target_family = "wasm"))] + { + help_paths.push(vec!["oz", "remote-server-proxy", "--help"]); + help_paths.push(vec!["oz", "remote-server-daemon", "--help"]); + help_paths.push(vec!["oz", "ripgrep-search", "--help"]); + help_paths.push(vec!["oz", "--print-telemetry-events", "--help"]); + } + + for args in &help_paths { + let mut command = Args::clap_command(); + let err = command + .try_get_matches_from_mut(args.iter().copied()) + .expect_err("help paths should exit with DisplayHelp"); + assert_eq!(err.kind(), ErrorKind::DisplayHelp, "args: {args:?}"); + } +} + #[test] fn agent_run_accepts_model() { let args = Args::try_parse_from([ diff --git a/crates/warp_cli/src/mcp.rs b/crates/warp_cli/src/mcp.rs index f0ec5ab767..b1c93d9143 100644 --- a/crates/warp_cli/src/mcp.rs +++ b/crates/warp_cli/src/mcp.rs @@ -44,9 +44,12 @@ impl clap::builder::TypedValueParser for MCPSpecParser { _arg: Option<&Arg>, value: &OsStr, ) -> Result { - let s = value - .to_str() - .ok_or_else(|| clap::Error::raw(ErrorKind::InvalidUtf8, "Invalid UTF-8 in MCP spec"))?; + let s = value.to_str().ok_or_else(|| { + clap::Error::raw( + ErrorKind::InvalidUtf8, + i18n::t("warp_cli.mcp.error.invalid_utf8"), + ) + })?; // Try UUID first if let Ok(uuid) = uuid::Uuid::parse_str(s) { @@ -59,7 +62,9 @@ impl clap::builder::TypedValueParser for MCPSpecParser { std::fs::read_to_string(path).map_err(|e| { clap::Error::raw( ErrorKind::Io, - format!("Failed to read MCP config file '{}': {e}", path.display()), + i18n::t("warp_cli.mcp.error.read_config_file_failed") + .replace("{path}", &path.display().to_string()) + .replace("{error}", &e.to_string()), ) })? } else { @@ -73,8 +78,8 @@ impl clap::builder::TypedValueParser for MCPSpecParser { fn possible_values(&self) -> Option + '_>> { Some(Box::new( [ - PossibleValue::new("").help("Path to a JSON file containing MCP config"), - PossibleValue::new("").help("Inline JSON MCP server configuration"), + PossibleValue::new("").help(i18n::t("warp_cli.mcp.possible.path")), + PossibleValue::new("").help(i18n::t("warp_cli.mcp.possible.json")), ] .into_iter(), )) diff --git a/crates/warp_cli/src/process_handle.rs b/crates/warp_cli/src/process_handle.rs index 92a917213f..c0077db901 100644 --- a/crates/warp_cli/src/process_handle.rs +++ b/crates/warp_cli/src/process_handle.rs @@ -17,9 +17,9 @@ impl FromStr for ProcessHandle { type Err = String; fn from_str(raw: &str) -> Result { - let pid = raw - .parse::() - .map_err(|e| format!("invalid parent handle: {e}"))?; + let pid = raw.parse::().map_err(|e| { + i18n::t("warp_cli.error.invalid_parent_handle").replace("{error}", &e.to_string()) + })?; Ok(Self(pid)) } } diff --git a/crates/warp_cli/src/share.rs b/crates/warp_cli/src/share.rs index 96dd29d496..a7fa489e79 100644 --- a/crates/warp_cli/src/share.rs +++ b/crates/warp_cli/src/share.rs @@ -63,9 +63,12 @@ impl clap::builder::TypedValueParser for ShareRequestParser { arg: Option<&Arg>, value: &OsStr, ) -> Result { - let value_str = value - .to_str() - .ok_or_else(|| clap::Error::raw(ErrorKind::InvalidUtf8, "Invalid share recipient"))?; + let value_str = value.to_str().ok_or_else(|| { + clap::Error::raw( + ErrorKind::InvalidUtf8, + i18n::t("warp_cli.share.error.invalid_recipient"), + ) + })?; // If there's a `:`, treat the first part as the subject and the second as the access level. Otherwise, default to `view` access. let (subject_str, level_str) = match value_str.split_once(':') { @@ -89,19 +92,19 @@ impl clap::builder::TypedValueParser for ShareRequestParser { Some(Box::new( [ PossibleValue::new("team:view") - .help("Share with your team, view-only") + .help(i18n::t("warp_cli.share.possible.team_view")) .alias("team"), - PossibleValue::new("team:edit").help("Share with your team, with edit access"), + PossibleValue::new("team:edit").help(i18n::t("warp_cli.share.possible.team_edit")), PossibleValue::new("public:view") - .help("Share with anyone who has the link, view-only") + .help(i18n::t("warp_cli.share.possible.public_view")) .alias("public"), PossibleValue::new("public:edit") - .help("Share with anyone who has the link, with edit access"), + .help(i18n::t("warp_cli.share.possible.public_edit")), PossibleValue::new(":view") - .help("Share with , view-only") + .help(i18n::t("warp_cli.share.possible.user_view")) .alias(""), PossibleValue::new(":edit") - .help("Share with , with edit access"), + .help(i18n::t("warp_cli.share.possible.user_edit")), ] .into_iter(), )) @@ -147,9 +150,7 @@ impl FromStr for ShareSubject { }), other => Err(clap::Error::raw( ErrorKind::InvalidValue, - format!( - "Cannot share with '{other}'. Expected 'team', 'public', or an email address" - ), + i18n::t("warp_cli.share.error.invalid_subject").replace("{subject}", other), )), } } diff --git a/crates/warp_cli/src/skill.rs b/crates/warp_cli/src/skill.rs index 83beba8636..f2cfcdc636 100644 --- a/crates/warp_cli/src/skill.rs +++ b/crates/warp_cli/src/skill.rs @@ -132,7 +132,7 @@ impl FromStr for SkillSpec { fn from_str(s: &str) -> Result { let s = s.trim(); if s.is_empty() { - return Err("Skill specifier cannot be empty".to_string()); + return Err(i18n::t("warp_cli.skill.error.empty_specifier")); } // Check for [qualifier:]skill_identifier format @@ -141,12 +141,10 @@ impl FromStr for SkillSpec { let skill_identifier = skill_identifier.trim(); if qualifier.is_empty() { - return Err( - "Qualifier cannot be empty in 'repo:skill_identifier' format".to_string(), - ); + return Err(i18n::t("warp_cli.skill.error.empty_qualifier")); } if skill_identifier.is_empty() { - return Err("Skill identifier cannot be empty".to_string()); + return Err(i18n::t("warp_cli.skill.error.empty_identifier")); } // Check for org/repo format in qualifier @@ -155,10 +153,10 @@ impl FromStr for SkillSpec { let repo = repo.trim(); if org.is_empty() { - return Err("Organization cannot be empty".to_string()); + return Err(i18n::t("warp_cli.skill.error.empty_org")); } if repo.is_empty() { - return Err("Repository name cannot be empty".to_string()); + return Err(i18n::t("warp_cli.skill.error.empty_repo")); } Ok(Self::with_org_and_repo( diff --git a/crates/warp_cli/src/skill_tests.rs b/crates/warp_cli/src/skill_tests.rs index 22a67a9d75..25a729db68 100644 --- a/crates/warp_cli/src/skill_tests.rs +++ b/crates/warp_cli/src/skill_tests.rs @@ -119,6 +119,28 @@ fn test_parse_empty_path_fails() { assert!(result.is_err()); } +#[test] +fn parse_errors_use_i18n_messages() { + for (input, key) in [ + ("", "warp_cli.skill.error.empty_specifier"), + (":code-review", "warp_cli.skill.error.empty_qualifier"), + ("warp-internal:", "warp_cli.skill.error.empty_identifier"), + ( + "/warp-internal:code-review", + "warp_cli.skill.error.empty_org", + ), + ("warpdotdev/:code-review", "warp_cli.skill.error.empty_repo"), + ] { + let err = input + .parse::() + .expect_err("invalid skill spec should fail"); + let expected = i18n::t(key); + + assert_ne!(expected, key); + assert_eq!(err, expected); + } +} + #[test] fn test_skill_name_simple_name() { let spec: SkillSpec = "feedback-triage-bot".parse().unwrap(); diff --git a/crates/warp_core/src/semantic_selection/mod.rs b/crates/warp_core/src/semantic_selection/mod.rs index a3da90d8cf..a6b739b06a 100644 --- a/crates/warp_core/src/semantic_selection/mod.rs +++ b/crates/warp_core/src/semantic_selection/mod.rs @@ -106,7 +106,7 @@ define_settings_group!(SemanticSelection, settings: [ private: false, storage_key: "SmartSelect", toml_path: "terminal.smart_select.enabled", - description: "Whether double-click smart selection is enabled for URLs, emails, file paths, and identifiers.", + description_key: "settings.schema.terminal.smart_select.enabled.description", }, word_char_allowlist: WordCharAllowlist { type: String, @@ -116,7 +116,7 @@ define_settings_group!(SemanticSelection, settings: [ private: false, storage_key: "WordCharAllowlist", toml_path: "terminal.smart_select.word_char_allowlist", - description: "Characters that are considered part of a word for double-click selection when smart select is disabled.", + description_key: "settings.schema.terminal.smart_select.word_char_allowlist.description", }, ]); diff --git a/crates/warpui_core/src/platform/menu.rs b/crates/warpui_core/src/platform/menu.rs index 65b395b97d..9d7980ed8a 100644 --- a/crates/warpui_core/src/platform/menu.rs +++ b/crates/warpui_core/src/platform/menu.rs @@ -18,6 +18,7 @@ pub enum MenuItem { pub struct Menu { pub title: String, pub menu_items: Vec, + is_window_menu: bool, } impl Menu { @@ -25,11 +26,20 @@ impl Menu { Menu { title: title.into(), menu_items, + is_window_menu: false, + } + } + + pub fn new_window>(title: S, menu_items: Vec) -> Self { + Menu { + title: title.into(), + menu_items, + is_window_menu: true, } } pub fn is_window_menu(&self) -> bool { - &self.title == "Window" + self.is_window_menu } }