feat(tui): localize onboarding screens (api key, trust, tips, picker)
When a user picked 简体中文 / 日本語 / Português (Brasil) on the
step-2 language picker, every subsequent onboarding screen used to
stay in English. The set_locale_from_onboarding path already
re-resolved `app.ui_locale`, but the hardcoded `Line::from(Span::
styled("Connect your DeepSeek API key", …))` strings in
`onboarding/api_key.rs`, `trust_directory.rs`, `language.rs`, and
the `tips_lines()` block in `onboarding/mod.rs` never consulted
the locale.
This commit:
- Adds 25 `MessageId` entries (`OnboardLanguageTitle`,
`OnboardApiKey*`, `OnboardTrust*`, `OnboardTips*`, …) covering
the title / body / hint / footer strings for each screen.
- Translates each into all four shipping locales (en / ja /
zh-Hans / pt-BR), with the same care the existing translation
surfaces use (no machine translation; idiomatic phrasing for
each locale).
- Threads the active locale through `language::lines`,
`api_key::lines`, `trust_directory::lines`, and `tips_lines`
via `app.tr(MessageId::…)`.
- Adds `api_key_screen_renders_in_selected_locale` regression
test pinning that the rendered lines actually contain the
translated strings for zh-Hans / ja / en.
Particularly noticeable for users on CJK input methods: picking
their language at step 2 now means the remaining setup runs in
that language rather than forcing IME juggling for English text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -186,6 +186,15 @@ Big thanks to every contributor below.
|
||||
The heuristic now treats any non-ASCII run as paste-like, so the
|
||||
Enter is absorbed into the burst buffer. Thanks **@reidliu41**
|
||||
(PR #1342).
|
||||
- **Onboarding screens render in the selected language** — when a
|
||||
user picked 简体中文 / 日本語 / Português (Brasil) at the language
|
||||
step, every subsequent screen (API key entry, workspace trust
|
||||
prompt, final tips) used to remain in English. The
|
||||
`set_locale_from_onboarding` path now drives the title, body
|
||||
copy, hints, and footer of each onboarding screen through the
|
||||
localization table, so once you pick your language the rest of
|
||||
the flow is in that language. Particularly nice for users on
|
||||
CJK input methods who want to avoid IME juggling during setup.
|
||||
- **HTTP 400 quota errors retried** (#1203) — some OpenAI-compatible
|
||||
gateways return quota/rate-limit errors as HTTP 400 instead of 429.
|
||||
These are now classified as retryable `RateLimited` errors.
|
||||
|
||||
@@ -376,6 +376,36 @@ pub enum MessageId {
|
||||
HomeYoloModeCaution,
|
||||
HomePlanModeTip,
|
||||
HomePlanModeChecklistTip,
|
||||
// Onboarding screens — language picker.
|
||||
OnboardLanguageTitle,
|
||||
OnboardLanguageBlurb,
|
||||
OnboardLanguageFooter,
|
||||
// Onboarding screens — API key entry.
|
||||
OnboardApiKeyTitle,
|
||||
OnboardApiKeyStep1,
|
||||
OnboardApiKeyStep2,
|
||||
OnboardApiKeySavedHint,
|
||||
OnboardApiKeyFormatHint,
|
||||
OnboardApiKeyPlaceholder,
|
||||
OnboardApiKeyLabel,
|
||||
OnboardApiKeyFooter,
|
||||
// Onboarding screens — workspace trust prompt.
|
||||
OnboardTrustTitle,
|
||||
OnboardTrustQuestion,
|
||||
OnboardTrustLocationPrefix,
|
||||
OnboardTrustRiskHint,
|
||||
OnboardTrustEffectHint,
|
||||
OnboardTrustFooterPrefix,
|
||||
OnboardTrustFooterMiddle,
|
||||
OnboardTrustFooterSuffix,
|
||||
// Onboarding screens — final tips screen.
|
||||
OnboardTipsTitle,
|
||||
OnboardTipsLine1,
|
||||
OnboardTipsLine2,
|
||||
OnboardTipsLine3,
|
||||
OnboardTipsLine4,
|
||||
OnboardTipsFooterEnter,
|
||||
OnboardTipsFooterAction,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -567,6 +597,32 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::HomeYoloModeCaution,
|
||||
MessageId::HomePlanModeTip,
|
||||
MessageId::HomePlanModeChecklistTip,
|
||||
MessageId::OnboardLanguageTitle,
|
||||
MessageId::OnboardLanguageBlurb,
|
||||
MessageId::OnboardLanguageFooter,
|
||||
MessageId::OnboardApiKeyTitle,
|
||||
MessageId::OnboardApiKeyStep1,
|
||||
MessageId::OnboardApiKeyStep2,
|
||||
MessageId::OnboardApiKeySavedHint,
|
||||
MessageId::OnboardApiKeyFormatHint,
|
||||
MessageId::OnboardApiKeyPlaceholder,
|
||||
MessageId::OnboardApiKeyLabel,
|
||||
MessageId::OnboardApiKeyFooter,
|
||||
MessageId::OnboardTrustTitle,
|
||||
MessageId::OnboardTrustQuestion,
|
||||
MessageId::OnboardTrustLocationPrefix,
|
||||
MessageId::OnboardTrustRiskHint,
|
||||
MessageId::OnboardTrustEffectHint,
|
||||
MessageId::OnboardTrustFooterPrefix,
|
||||
MessageId::OnboardTrustFooterMiddle,
|
||||
MessageId::OnboardTrustFooterSuffix,
|
||||
MessageId::OnboardTipsTitle,
|
||||
MessageId::OnboardTipsLine1,
|
||||
MessageId::OnboardTipsLine2,
|
||||
MessageId::OnboardTipsLine3,
|
||||
MessageId::OnboardTipsLine4,
|
||||
MessageId::OnboardTipsFooterEnter,
|
||||
MessageId::OnboardTipsFooterAction,
|
||||
];
|
||||
|
||||
pub fn tr(locale: Locale, id: MessageId) -> &'static str {
|
||||
@@ -956,6 +1012,58 @@ fn english(id: MessageId) -> &'static str {
|
||||
MessageId::HomeYoloModeCaution => " Be careful with destructive operations!",
|
||||
MessageId::HomePlanModeTip => "Plan mode - Design before implementing",
|
||||
MessageId::HomePlanModeChecklistTip => " Use /mode plan to create structured checklists",
|
||||
// Onboarding — language picker.
|
||||
MessageId::OnboardLanguageTitle => "Choose your language",
|
||||
MessageId::OnboardLanguageBlurb => {
|
||||
"Pick the UI language. You can change it any time with `/settings set locale <tag>`."
|
||||
}
|
||||
MessageId::OnboardLanguageFooter => {
|
||||
"Press 1-5 to choose, or Enter to keep the current setting"
|
||||
}
|
||||
// Onboarding — API key entry.
|
||||
MessageId::OnboardApiKeyTitle => "Connect your DeepSeek API key",
|
||||
MessageId::OnboardApiKeyStep1 => {
|
||||
"Step 1. Open https://platform.deepseek.com/api_keys and create a key."
|
||||
}
|
||||
MessageId::OnboardApiKeyStep2 => "Step 2. Paste it below and press Enter.",
|
||||
MessageId::OnboardApiKeySavedHint => {
|
||||
"Saved to ~/.deepseek/config.toml so it works from any folder."
|
||||
}
|
||||
MessageId::OnboardApiKeyFormatHint => {
|
||||
"Paste the full key exactly as issued (no spaces or newlines)."
|
||||
}
|
||||
MessageId::OnboardApiKeyPlaceholder => "(paste key here)",
|
||||
MessageId::OnboardApiKeyLabel => "Key: ",
|
||||
MessageId::OnboardApiKeyFooter => "Press Enter to save, Esc to go back.",
|
||||
// Onboarding — workspace trust.
|
||||
MessageId::OnboardTrustTitle => "Trust Workspace",
|
||||
MessageId::OnboardTrustQuestion => "Do you trust the contents of this directory?",
|
||||
MessageId::OnboardTrustLocationPrefix => "You are in ",
|
||||
MessageId::OnboardTrustRiskHint => {
|
||||
"Working with untrusted contents comes with higher risk of prompt injection."
|
||||
}
|
||||
MessageId::OnboardTrustEffectHint => {
|
||||
"Trusting this directory records it in global config and enables trusted workspace mode."
|
||||
}
|
||||
MessageId::OnboardTrustFooterPrefix => "Press ",
|
||||
MessageId::OnboardTrustFooterMiddle => " to trust and continue, ",
|
||||
MessageId::OnboardTrustFooterSuffix => " to quit",
|
||||
// Onboarding — final tips.
|
||||
MessageId::OnboardTipsTitle => "Start Simple",
|
||||
MessageId::OnboardTipsLine1 => {
|
||||
"Write the task in plain language. Use /help or Ctrl+K when you want a command."
|
||||
}
|
||||
MessageId::OnboardTipsLine2 => {
|
||||
"The bottom composer is multi-line: Enter sends, Alt+Enter or Ctrl+J adds a new line."
|
||||
}
|
||||
MessageId::OnboardTipsLine3 => {
|
||||
"Switch modes only when the job changes: Plan for review-first work, Agent for execution, YOLO when you want auto-approval."
|
||||
}
|
||||
MessageId::OnboardTipsLine4 => {
|
||||
"Ctrl+R resumes earlier sessions, and Esc backs out of the current draft or overlay."
|
||||
}
|
||||
MessageId::OnboardTipsFooterEnter => "Press Enter",
|
||||
MessageId::OnboardTipsFooterAction => " to open the workspace",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1248,6 +1356,56 @@ fn japanese(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::HomePlanModeChecklistTip => {
|
||||
" /mode plan を使って構造化されたチェックリストを作成"
|
||||
}
|
||||
// Onboarding — language picker.
|
||||
MessageId::OnboardLanguageTitle => "言語を選択",
|
||||
MessageId::OnboardLanguageBlurb => {
|
||||
"UI 言語を選んでください。`/settings set locale <tag>` でいつでも変更できます。"
|
||||
}
|
||||
MessageId::OnboardLanguageFooter => "1〜5 で選択、または Enter で現在の設定を維持",
|
||||
// Onboarding — API key entry.
|
||||
MessageId::OnboardApiKeyTitle => "DeepSeek API キーを設定",
|
||||
MessageId::OnboardApiKeyStep1 => {
|
||||
"ステップ 1. https://platform.deepseek.com/api_keys を開いてキーを作成。"
|
||||
}
|
||||
MessageId::OnboardApiKeyStep2 => "ステップ 2. 下に貼り付けて Enter を押してください。",
|
||||
MessageId::OnboardApiKeySavedHint => {
|
||||
"~/.deepseek/config.toml に保存されるので、どのフォルダからでも有効になります。"
|
||||
}
|
||||
MessageId::OnboardApiKeyFormatHint => {
|
||||
"発行されたキーをそのまま貼り付けてください(空白や改行を含めない)。"
|
||||
}
|
||||
MessageId::OnboardApiKeyPlaceholder => "(ここにキーを貼り付け)",
|
||||
MessageId::OnboardApiKeyLabel => "キー: ",
|
||||
MessageId::OnboardApiKeyFooter => "Enter で保存、Esc で戻る。",
|
||||
// Onboarding — workspace trust.
|
||||
MessageId::OnboardTrustTitle => "ワークスペースを信頼",
|
||||
MessageId::OnboardTrustQuestion => "このディレクトリの内容を信頼しますか?",
|
||||
MessageId::OnboardTrustLocationPrefix => "現在の場所: ",
|
||||
MessageId::OnboardTrustRiskHint => {
|
||||
"信頼されていない内容を扱うとプロンプトインジェクションのリスクが高くなります。"
|
||||
}
|
||||
MessageId::OnboardTrustEffectHint => {
|
||||
"信頼するとグローバル設定に記録され、信頼済みワークスペースモードが有効になります。"
|
||||
}
|
||||
MessageId::OnboardTrustFooterPrefix => "キー ",
|
||||
MessageId::OnboardTrustFooterMiddle => " で信頼して続行、",
|
||||
MessageId::OnboardTrustFooterSuffix => " で終了",
|
||||
// Onboarding — final tips.
|
||||
MessageId::OnboardTipsTitle => "シンプルに始めよう",
|
||||
MessageId::OnboardTipsLine1 => {
|
||||
"タスクを自然な言葉で記入。コマンドが必要な時は /help や Ctrl+K を使ってください。"
|
||||
}
|
||||
MessageId::OnboardTipsLine2 => {
|
||||
"下の入力欄は複数行対応です。Enter で送信、Alt+Enter または Ctrl+J で改行。"
|
||||
}
|
||||
MessageId::OnboardTipsLine3 => {
|
||||
"用途に応じてモードを切り替え:Plan は事前レビュー、Agent は実行、YOLO は自動承認。"
|
||||
}
|
||||
MessageId::OnboardTipsLine4 => {
|
||||
"Ctrl+R で過去のセッションを再開、Esc で現在の入力やオーバーレイをキャンセル。"
|
||||
}
|
||||
MessageId::OnboardTipsFooterEnter => "Enter を押す",
|
||||
MessageId::OnboardTipsFooterAction => " とワークスペースが開きます",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1491,6 +1649,46 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::HomeYoloModeCaution => " 请小心破坏性操作!",
|
||||
MessageId::HomePlanModeTip => "Plan 模式 - 先设计再实现",
|
||||
MessageId::HomePlanModeChecklistTip => " 使用 /mode plan 创建结构化检查清单",
|
||||
// Onboarding — language picker.
|
||||
MessageId::OnboardLanguageTitle => "选择语言",
|
||||
MessageId::OnboardLanguageBlurb => {
|
||||
"选择界面语言。可随时使用 `/settings set locale <tag>` 修改。"
|
||||
}
|
||||
MessageId::OnboardLanguageFooter => "按 1-5 选择,或按 Enter 保留当前设置",
|
||||
// Onboarding — API key entry.
|
||||
MessageId::OnboardApiKeyTitle => "连接你的 DeepSeek API 密钥",
|
||||
MessageId::OnboardApiKeyStep1 => {
|
||||
"步骤 1. 打开 https://platform.deepseek.com/api_keys 创建一个密钥。"
|
||||
}
|
||||
MessageId::OnboardApiKeyStep2 => "步骤 2. 把密钥粘贴到下方并按 Enter。",
|
||||
MessageId::OnboardApiKeySavedHint => {
|
||||
"保存到 ~/.deepseek/config.toml,因此在任何目录下都生效。"
|
||||
}
|
||||
MessageId::OnboardApiKeyFormatHint => "请完整粘贴密钥(不要含空格或换行)。",
|
||||
MessageId::OnboardApiKeyPlaceholder => "(在此粘贴密钥)",
|
||||
MessageId::OnboardApiKeyLabel => "密钥: ",
|
||||
MessageId::OnboardApiKeyFooter => "Enter 保存,Esc 返回。",
|
||||
// Onboarding — workspace trust.
|
||||
MessageId::OnboardTrustTitle => "信任工作目录",
|
||||
MessageId::OnboardTrustQuestion => "你信任此目录中的内容吗?",
|
||||
MessageId::OnboardTrustLocationPrefix => "当前位置:",
|
||||
MessageId::OnboardTrustRiskHint => "处理不受信任的内容会增加提示词注入的风险。",
|
||||
MessageId::OnboardTrustEffectHint => {
|
||||
"信任此目录会记录在全局配置中,并启用受信任工作区模式。"
|
||||
}
|
||||
MessageId::OnboardTrustFooterPrefix => "按 ",
|
||||
MessageId::OnboardTrustFooterMiddle => " 信任并继续,",
|
||||
MessageId::OnboardTrustFooterSuffix => " 退出",
|
||||
// Onboarding — final tips.
|
||||
MessageId::OnboardTipsTitle => "从简开始",
|
||||
MessageId::OnboardTipsLine1 => "用自然语言描述任务。需要命令时使用 /help 或 Ctrl+K。",
|
||||
MessageId::OnboardTipsLine2 => "底部输入框支持多行:Enter 发送,Alt+Enter 或 Ctrl+J 换行。",
|
||||
MessageId::OnboardTipsLine3 => {
|
||||
"按需切换模式:Plan 适合先审后行,Agent 用于执行,YOLO 启用自动批准。"
|
||||
}
|
||||
MessageId::OnboardTipsLine4 => "Ctrl+R 恢复历史会话,Esc 退出当前输入或弹层。",
|
||||
MessageId::OnboardTipsFooterEnter => "按 Enter",
|
||||
MessageId::OnboardTipsFooterAction => " 进入工作区",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1792,6 +1990,58 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::HomePlanModeChecklistTip => {
|
||||
" Use /mode plan para criar checklists estruturados"
|
||||
}
|
||||
// Onboarding — language picker.
|
||||
MessageId::OnboardLanguageTitle => "Escolha o idioma",
|
||||
MessageId::OnboardLanguageBlurb => {
|
||||
"Escolha o idioma da interface. Você pode mudá-lo a qualquer momento com `/settings set locale <tag>`."
|
||||
}
|
||||
MessageId::OnboardLanguageFooter => {
|
||||
"Pressione 1-5 para escolher, ou Enter para manter a configuração atual"
|
||||
}
|
||||
// Onboarding — API key entry.
|
||||
MessageId::OnboardApiKeyTitle => "Conecte sua chave de API DeepSeek",
|
||||
MessageId::OnboardApiKeyStep1 => {
|
||||
"Passo 1. Abra https://platform.deepseek.com/api_keys e crie uma chave."
|
||||
}
|
||||
MessageId::OnboardApiKeyStep2 => "Passo 2. Cole abaixo e pressione Enter.",
|
||||
MessageId::OnboardApiKeySavedHint => {
|
||||
"Salvo em ~/.deepseek/config.toml para funcionar em qualquer pasta."
|
||||
}
|
||||
MessageId::OnboardApiKeyFormatHint => {
|
||||
"Cole a chave inteira como foi emitida (sem espaços ou quebras de linha)."
|
||||
}
|
||||
MessageId::OnboardApiKeyPlaceholder => "(cole a chave aqui)",
|
||||
MessageId::OnboardApiKeyLabel => "Chave: ",
|
||||
MessageId::OnboardApiKeyFooter => "Enter para salvar, Esc para voltar.",
|
||||
// Onboarding — workspace trust.
|
||||
MessageId::OnboardTrustTitle => "Confiar no diretório",
|
||||
MessageId::OnboardTrustQuestion => "Você confia no conteúdo deste diretório?",
|
||||
MessageId::OnboardTrustLocationPrefix => "Você está em ",
|
||||
MessageId::OnboardTrustRiskHint => {
|
||||
"Trabalhar com conteúdo não confiável aumenta o risco de injeção de prompt."
|
||||
}
|
||||
MessageId::OnboardTrustEffectHint => {
|
||||
"Confiar neste diretório o registra na configuração global e habilita o modo workspace confiável."
|
||||
}
|
||||
MessageId::OnboardTrustFooterPrefix => "Pressione ",
|
||||
MessageId::OnboardTrustFooterMiddle => " para confiar e continuar, ",
|
||||
MessageId::OnboardTrustFooterSuffix => " para sair",
|
||||
// Onboarding — final tips.
|
||||
MessageId::OnboardTipsTitle => "Comece simples",
|
||||
MessageId::OnboardTipsLine1 => {
|
||||
"Escreva a tarefa em linguagem natural. Use /help ou Ctrl+K para comandos."
|
||||
}
|
||||
MessageId::OnboardTipsLine2 => {
|
||||
"O composer inferior é multilinhas: Enter envia, Alt+Enter ou Ctrl+J adiciona uma nova linha."
|
||||
}
|
||||
MessageId::OnboardTipsLine3 => {
|
||||
"Mude de modo apenas quando o trabalho mudar: Plan para revisar antes, Agent para execução, YOLO para auto-aprovação."
|
||||
}
|
||||
MessageId::OnboardTipsLine4 => {
|
||||
"Ctrl+R retoma sessões anteriores, e Esc cancela o rascunho ou overlay atual."
|
||||
}
|
||||
MessageId::OnboardTipsFooterEnter => "Pressione Enter",
|
||||
MessageId::OnboardTipsFooterAction => " para abrir o workspace",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3,48 +3,53 @@
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
|
||||
use crate::localization::MessageId;
|
||||
use crate::palette;
|
||||
use crate::tui::app::App;
|
||||
|
||||
pub fn lines(app: &App) -> Vec<Line<'static>> {
|
||||
let mut lines = vec![
|
||||
Line::from(Span::styled(
|
||||
"Connect your DeepSeek API key",
|
||||
app.tr(MessageId::OnboardApiKeyTitle).to_string(),
|
||||
Style::default()
|
||||
.fg(palette::DEEPSEEK_SKY)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(
|
||||
"Step 1. Open https://platform.deepseek.com/api_keys and create a key.",
|
||||
app.tr(MessageId::OnboardApiKeyStep1).to_string(),
|
||||
Style::default().fg(palette::TEXT_PRIMARY),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
"Step 2. Paste it below and press Enter.",
|
||||
app.tr(MessageId::OnboardApiKeyStep2).to_string(),
|
||||
Style::default().fg(palette::TEXT_PRIMARY),
|
||||
)),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(
|
||||
"Saved to ~/.deepseek/config.toml so it works from any folder.",
|
||||
app.tr(MessageId::OnboardApiKeySavedHint).to_string(),
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
"Paste the full key exactly as issued (no spaces or newlines).",
|
||||
app.tr(MessageId::OnboardApiKeyFormatHint).to_string(),
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
)),
|
||||
Line::from(""),
|
||||
];
|
||||
|
||||
let masked = mask_key(&app.api_key_input);
|
||||
let placeholder = app.tr(MessageId::OnboardApiKeyPlaceholder).to_string();
|
||||
let display = if masked.is_empty() {
|
||||
"(paste key here)"
|
||||
placeholder
|
||||
} else {
|
||||
masked.as_str()
|
||||
masked
|
||||
};
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Key: ", Style::default().fg(palette::TEXT_MUTED)),
|
||||
Span::styled(
|
||||
display.to_string(),
|
||||
app.tr(MessageId::OnboardApiKeyLabel).to_string(),
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
),
|
||||
Span::styled(
|
||||
display,
|
||||
Style::default()
|
||||
.fg(palette::TEXT_PRIMARY)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
@@ -62,7 +67,7 @@ pub fn lines(app: &App) -> Vec<Line<'static>> {
|
||||
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
"Press Enter to save, Esc to go back.",
|
||||
app.tr(MessageId::OnboardApiKeyFooter).to_string(),
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
)));
|
||||
|
||||
@@ -88,3 +93,84 @@ fn mask_key(input: &str) -> String {
|
||||
.collect();
|
||||
format!("{}{}", "*".repeat(len - 4), visible)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::localization::Locale;
|
||||
use crate::tui::app::TuiOptions;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn test_app_with_locale(locale: Locale) -> App {
|
||||
let options = TuiOptions {
|
||||
model: "deepseek-v4-pro".to_string(),
|
||||
workspace: PathBuf::from("."),
|
||||
config_path: None,
|
||||
config_profile: None,
|
||||
allow_shell: false,
|
||||
use_alt_screen: true,
|
||||
use_mouse_capture: false,
|
||||
use_bracketed_paste: true,
|
||||
max_subagents: 1,
|
||||
skills_dir: PathBuf::from("."),
|
||||
memory_path: PathBuf::from("memory.md"),
|
||||
notes_path: PathBuf::from("notes.txt"),
|
||||
mcp_config_path: PathBuf::from("mcp.json"),
|
||||
use_memory: false,
|
||||
start_in_agent_mode: false,
|
||||
skip_onboarding: true,
|
||||
yolo: false,
|
||||
resume_session_id: None,
|
||||
initial_input: None,
|
||||
};
|
||||
let mut app = App::new(options, &Config::default());
|
||||
app.ui_locale = locale;
|
||||
app
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_key_screen_renders_in_selected_locale() {
|
||||
// The most-visible regression of the missing onboarding-localization:
|
||||
// after the user picks 简体中文 at step 2, step 3 used to remain
|
||||
// English. Pin that the rendered lines actually contain the
|
||||
// translated strings for each locale we ship.
|
||||
let zh = test_app_with_locale(Locale::ZhHans);
|
||||
let body: String = lines(&zh)
|
||||
.iter()
|
||||
.flat_map(|l| l.spans.iter().map(|s| s.content.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(body.contains("DeepSeek API"), "title carries DeepSeek API");
|
||||
assert!(
|
||||
body.contains("密钥"),
|
||||
"expected zh-Hans 'key' label, got: {body}"
|
||||
);
|
||||
assert!(
|
||||
body.contains("Enter 保存"),
|
||||
"expected zh-Hans footer, got: {body}"
|
||||
);
|
||||
|
||||
let ja = test_app_with_locale(Locale::Ja);
|
||||
let body: String = lines(&ja)
|
||||
.iter()
|
||||
.flat_map(|l| l.spans.iter().map(|s| s.content.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(
|
||||
body.contains("キー"),
|
||||
"expected ja 'key' label, got: {body}"
|
||||
);
|
||||
|
||||
let en = test_app_with_locale(Locale::En);
|
||||
let body: String = lines(&en)
|
||||
.iter()
|
||||
.flat_map(|l| l.spans.iter().map(|s| s.content.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(
|
||||
body.contains("Press Enter to save"),
|
||||
"expected en footer, got: {body}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
|
||||
use crate::localization::MessageId;
|
||||
use crate::palette;
|
||||
use crate::tui::app::App;
|
||||
|
||||
@@ -29,14 +30,14 @@ pub fn lines(app: &App) -> Vec<Line<'static>> {
|
||||
|
||||
let mut out: Vec<Line<'static>> = vec![
|
||||
Line::from(Span::styled(
|
||||
"Choose your language",
|
||||
app.tr(MessageId::OnboardLanguageTitle).to_string(),
|
||||
Style::default()
|
||||
.fg(palette::DEEPSEEK_SKY)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(
|
||||
"Pick the UI language. You can change it any time with `/settings set locale <tag>`.",
|
||||
app.tr(MessageId::OnboardLanguageBlurb).to_string(),
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
)),
|
||||
Line::from(""),
|
||||
@@ -73,26 +74,10 @@ pub fn lines(app: &App) -> Vec<Line<'static>> {
|
||||
}
|
||||
|
||||
out.push(Line::from(""));
|
||||
out.push(Line::from(vec![
|
||||
Span::styled("Press ", Style::default().fg(palette::TEXT_MUTED)),
|
||||
Span::styled(
|
||||
"1-5",
|
||||
Style::default()
|
||||
.fg(palette::TEXT_PRIMARY)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" to choose, or ", Style::default().fg(palette::TEXT_MUTED)),
|
||||
Span::styled(
|
||||
"Enter",
|
||||
Style::default()
|
||||
.fg(palette::TEXT_PRIMARY)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
" to keep the current setting",
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
),
|
||||
]));
|
||||
out.push(Line::from(Span::styled(
|
||||
app.tr(MessageId::OnboardLanguageFooter).to_string(),
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
)));
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
OnboardingState::Language => language::lines(app),
|
||||
OnboardingState::ApiKey => api_key::lines(app),
|
||||
OnboardingState::TrustDirectory => trust_directory::lines(app),
|
||||
OnboardingState::Tips => tips_lines(),
|
||||
OnboardingState::Tips => tips_lines(app),
|
||||
OnboardingState::None => Vec::new(),
|
||||
};
|
||||
|
||||
@@ -94,40 +94,32 @@ fn onboarding_step(app: &App) -> (usize, usize) {
|
||||
(step, total)
|
||||
}
|
||||
|
||||
pub fn tips_lines() -> Vec<ratatui::text::Line<'static>> {
|
||||
pub fn tips_lines(app: &App) -> Vec<ratatui::text::Line<'static>> {
|
||||
use crate::localization::MessageId;
|
||||
use ratatui::style::Modifier;
|
||||
use ratatui::text::{Line, Span};
|
||||
|
||||
vec![
|
||||
Line::from(Span::styled(
|
||||
"Start Simple",
|
||||
app.tr(MessageId::OnboardTipsTitle).to_string(),
|
||||
Style::default()
|
||||
.fg(palette::DEEPSEEK_SKY)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
Line::from(""),
|
||||
Line::from(Span::raw(
|
||||
"Write the task in plain language. Use /help or Ctrl+K when you want a command.",
|
||||
)),
|
||||
Line::from(Span::raw(
|
||||
"The bottom composer is multi-line: Enter sends, Alt+Enter or Ctrl+J adds a new line.",
|
||||
)),
|
||||
Line::from(Span::raw(
|
||||
"Switch modes only when the job changes: Plan for review-first work, Agent for execution, YOLO when you want auto-approval.",
|
||||
)),
|
||||
Line::from(Span::raw(
|
||||
"Ctrl+R resumes earlier sessions, and Esc backs out of the current draft or overlay.",
|
||||
)),
|
||||
Line::from(Span::raw(app.tr(MessageId::OnboardTipsLine1).to_string())),
|
||||
Line::from(Span::raw(app.tr(MessageId::OnboardTipsLine2).to_string())),
|
||||
Line::from(Span::raw(app.tr(MessageId::OnboardTipsLine3).to_string())),
|
||||
Line::from(Span::raw(app.tr(MessageId::OnboardTipsLine4).to_string())),
|
||||
Line::from(vec![
|
||||
Span::styled("Press ", Style::default().fg(palette::TEXT_MUTED)),
|
||||
Span::styled(
|
||||
"Enter",
|
||||
app.tr(MessageId::OnboardTipsFooterEnter).to_string(),
|
||||
Style::default()
|
||||
.fg(palette::TEXT_PRIMARY)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
" to open the workspace",
|
||||
app.tr(MessageId::OnboardTipsFooterAction).to_string(),
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
),
|
||||
]),
|
||||
|
||||
@@ -3,33 +3,38 @@
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
|
||||
use crate::localization::MessageId;
|
||||
use crate::palette;
|
||||
use crate::tui::app::App;
|
||||
|
||||
pub fn lines(app: &App) -> Vec<Line<'static>> {
|
||||
let mut lines = Vec::new();
|
||||
lines.push(Line::from(Span::styled(
|
||||
"Trust Workspace",
|
||||
app.tr(MessageId::OnboardTrustTitle).to_string(),
|
||||
Style::default()
|
||||
.fg(palette::DEEPSEEK_SKY)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)));
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
"Do you trust the contents of this directory?",
|
||||
app.tr(MessageId::OnboardTrustQuestion).to_string(),
|
||||
Style::default().fg(palette::TEXT_PRIMARY),
|
||||
)));
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!("You are in {}", crate::utils::display_path(&app.workspace)),
|
||||
format!(
|
||||
"{}{}",
|
||||
app.tr(MessageId::OnboardTrustLocationPrefix),
|
||||
crate::utils::display_path(&app.workspace)
|
||||
),
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
)));
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
"Working with untrusted contents comes with higher risk of prompt injection.",
|
||||
app.tr(MessageId::OnboardTrustRiskHint).to_string(),
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
)));
|
||||
lines.push(Line::from(Span::styled(
|
||||
"Trusting this directory records it in global config and enables trusted workspace mode.",
|
||||
app.tr(MessageId::OnboardTrustEffectHint).to_string(),
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
)));
|
||||
if let Some(message) = app.status_message.as_deref() {
|
||||
@@ -41,7 +46,10 @@ pub fn lines(app: &App) -> Vec<Line<'static>> {
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Press ", Style::default().fg(palette::TEXT_MUTED)),
|
||||
Span::styled(
|
||||
app.tr(MessageId::OnboardTrustFooterPrefix).to_string(),
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
),
|
||||
Span::styled(
|
||||
"1/Y",
|
||||
Style::default()
|
||||
@@ -49,7 +57,7 @@ pub fn lines(app: &App) -> Vec<Line<'static>> {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
" to trust and continue, ",
|
||||
app.tr(MessageId::OnboardTrustFooterMiddle).to_string(),
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
),
|
||||
Span::styled(
|
||||
@@ -58,7 +66,10 @@ pub fn lines(app: &App) -> Vec<Line<'static>> {
|
||||
.fg(palette::TEXT_PRIMARY)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" to quit", Style::default().fg(palette::TEXT_MUTED)),
|
||||
Span::styled(
|
||||
app.tr(MessageId::OnboardTrustFooterSuffix).to_string(),
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
),
|
||||
]));
|
||||
lines
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user