use clap::Command;
use horae_core::i18n::Lang;
pub fn detect_lang() -> Lang {
if let Ok(v) = std::env::var("HORAE_LANG") {
if is_zh(&v) {
return Lang::Zh;
}
if is_en(&v) {
return Lang::En;
}
}
let mut args_iter = std::env::args();
while let Some(a) = args_iter.next() {
if a == "--lang" || a == "-L" {
if let Some(v) = args_iter.next() {
if is_zh(&v) {
return Lang::Zh;
}
if is_en(&v) {
return Lang::En;
}
}
} else if let Some(v) = a.strip_prefix("--lang=") {
if is_zh(v) {
return Lang::Zh;
}
if is_en(v) {
return Lang::En;
}
}
}
Lang::En
}
fn is_zh(v: &str) -> bool {
matches!(
v.to_ascii_lowercase().as_str(),
"zh" | "zh-cn" | "zh_cn" | "chinese" | "中文"
)
}
fn is_en(v: &str) -> bool {
matches!(v.to_ascii_lowercase().as_str(), "en" | "english" | "英文")
}
pub fn localize(cmd: &mut Command, lang: Lang) {
if !lang.is_zh() {
return;
}
localize_recursive(cmd, "horae");
}
fn localize_recursive(cmd: &mut Command, path: &str) {
if let Some(zh) = zh_about(path) {
*cmd = std::mem::take(cmd).about(zh);
}
if let Some(zh) = zh_long_about(path) {
*cmd = std::mem::take(cmd).long_about(zh);
}
if let Some(zh) = zh_after_help(path) {
*cmd = std::mem::take(cmd).after_help(zh);
}
let ids: Vec<String> = cmd
.get_arguments()
.map(|a| a.get_id().to_string())
.collect();
for id in ids {
if let Some(zh) = zh_arg_help(path, &id) {
*cmd = std::mem::take(cmd).mut_arg(id.as_str(), |a| a.help(zh));
}
}
let sub_names: Vec<String> = cmd
.get_subcommands()
.map(|s| s.get_name().to_string())
.collect();
for name in sub_names {
let sub_path = if path.is_empty() || path == "horae" {
name.clone()
} else {
format!("{path} {name}")
};
if let Some(sub) = cmd.get_subcommands_mut().find(|s| s.get_name() == name) {
localize_recursive(sub, &sub_path);
}
}
}
fn zh_about(path: &str) -> Option<&'static str> {
Some(match path {
"horae" => "GTD 终端任务管理器",
"capture" => "将新条目捕获到收件箱",
"list" => "列出任务(支持过滤)",
"show" => "查看任务及其完整事件时间线",
"modify" => "修改已有任务(标题、标签、截止、排程、状态、备注)",
"next" => "标记为可行动(next)",
"wait" => "标记为等待中",
"schedule" => "排程(设定计划开始时间,可选 --rrule)",
"someday" => "移至「将来/也许」",
"done" => "标记为完成(周期性任务会重排到下一次)",
"archive" => "归档(软删除)任务",
"restore" => "恢复已归档(软删除)的任务",
"purge" => "永久删除已归档任务(不可撤销)",
"tag" => "给任务添加标签(预设或自定义)",
"untag" => "移除任务的标签",
"review" => "周回顾助手",
"tags" => "按分类列出所有标签",
"focus" => "计算并输出当前最重要的一件事",
"log" => "记录带时间戳的事件/日志(不创建任务)",
"pomo" => "番茄钟命令(start、stop、daemon、waybar)",
"alarm" => "临近任务的闹钟提醒(waybar、next)",
"tui" => "启动交互式 TUI",
"flash" => "以闪念录入模式启动(开启即录入,回车确认录入即关闭)",
"ntfy" => "通过 ntfy 推送手机提醒",
"watch" => "监视与手机同步的文件夹(手机 <-> 电脑 桥接)",
"export" => "导出完整备份(任务、事件、标签、设置、番茄钟)到 JSON",
"import" => "导入备份,合并(或替换)数据库",
"stats" => "显示终端仪表盘摘要(MOTD 风格)",
"completions" => "生成 shell 补全脚本(bash、elvish、fish、powershell、zsh)",
"profile" => "管理配置集(列出、创建、删除、重命名、设置默认)",
"profile list" => "列出所有配置集并标记默认项",
"profile new" => "创建新配置集(数据集)",
"profile rename" => "重命名配置集",
"profile rm" => "从配置中删除配置集(其数据库文件保留)",
"profile set-default" => "设置未指定 --profile 时使用的默认配置集",
_ => return None,
})
}
fn zh_long_about(path: &str) -> Option<&'static str> {
Some(match path {
"horae" => "用 Rust 编写的 GTD 终端任务管理器:单一二进制内置 SQLite 数据层 + CLI + ratatui TUI。\n每次任务状态变更都会打上 UTC 毫秒时间戳,并追加到只追加的 task_events 时间线中。",
"capture" => "将新条目捕获到收件箱。标签首次使用时自动创建;标题中可用 quick-add 语法(@tag ~time *rrule !priority)。",
"list" => "列出任务,支持可选过滤条件。排序使用有效截止时间:对周期性任务而言,即当前时间之后下一次发生的时间。",
"show" => "查看任务详情及其完整的只追加事件时间线。",
"modify" => "修改已有任务。支持在一句话标题中使用 quick-add 语法(@tag ~time *rrule !priority),也支持通过显式参数修改特定字段。",
"schedule" => "为任务排程,设定计划开始/结束时间,以及可选的重复规则(RRULE)。",
"focus" => "计算并输出当前最重要的一件事,终结选择困难。综合优先级(high/medium/low)、有效截止时间与上下文。",
"log" => "向时间线记录一条带时间戳的事件/日志,而不创建待办任务。",
"pomo" => "番茄钟专注模式。`start` 会启动一个后台守护进程,每秒计时,写入 pomo.json 并发送桌面通知。",
"alarm" => "临近任务的闹钟提醒:`waybar` 为 waybar 模块输出 JSON,`next` 打印最近的待响闹钟。",
"ntfy" => "通过 ntfy(https://ntfy.sh)把任务提醒推送到手机。需在配置中设置 `ntfy` 块(url、topic、可选的 token_env、priority、lead_minutes)。`watch` 守护进程会在定时任务到期时推送原生通知;`ntfy test` 会发送一条示例推送,便于确认手机能收到。",
"watch" => "监视与手机同步的文件夹(例如通过 Syncthing),并与本地数据库对账。每隔几秒它会:把 capture.txt 中的新行摄入收件箱(quick-add 语法),执行 actions.txt 中的动作行,用活动任务列表重写 today.md,并在任务到期时向 reminders/ 写入提醒文件。\n请在后台运行(systemd / tmux / 自启动);传入 --once 则只执行一轮。",
"export" => "把所有任务、事件、标签、设置与番茄钟状态导出到单个 JSON 文件——数据库的完整还原点。",
"import" => "导入由 `horae export` 创建的备份。默认采用合并方式:已存在的 id 对应任务保持不变,其余任务加入。传入 --replace 则清空当前任务数据并精确还原备份。",
"stats" => "以终端仪表盘(MOTD 风格)展示今日完成的番茄钟、燃尽情况与待办任务。",
"flash" => "直接进入快速录入模式。输入任务并回车确认后,自动保存至收件箱并立即退出。",
"profile" => "配置集让你可以维护多套独立数据(例如 work / personal / prod1),各自存放在独立的 SQLite 文件中,通过 `horae --profile <name>` 或 TUI 设置视图切换。本命令只编辑配置(~/.config/horae/config.json),不触碰任何数据。",
_ => return None,
})
}
fn zh_after_help(path: &str) -> Option<&'static str> {
Some(match path {
"horae" => "示例:\n horae 正常模式启动(主页面)\n horae -f 闪念录入模式启动\n horae capture \"买牛奶\" --tag home --high\n horae list --status next\n horae show <id>\n horae completions bash\n\n时间语法: now、+2h、+30m、+1d、today、tomorrow、2026-07-24 14:30\n日期搜索: 四位数字 MMDD,例如 0829\n任务引用: 完整 id、唯一 id 前缀,或精确标题",
"flash" => "示例:\n horae flash\n horae f\n horae -f",
"capture" => "示例:\n horae capture \"买牛奶\" --tag home\n horae capture \"给妈妈打电话\" --high --due tomorrow\n horae capture \"提交报告\" --status scheduled --due +1d\n horae capture \"给老板发邮件 ~today @work !high\"",
"modify" => "示例:\n horae modify <id> \"买有机牛奶 @groceries\"\n horae modify <id> --due tomorrow\n horae modify <id> --tag home --untag work\n horae modify <id> --clear-due\n horae modify <id> --clear-schedule\n horae modify <id> --notes \"下午3点电话\"\n horae modify <id> --edit-notes\n horae modify <id> --status next",
"list" => "示例:\n horae list\n horae list --status next\n horae list --status scheduled --tag work\n horae list --date 0829 --json\n horae list --due-before +1d --json",
"schedule" => "示例:\n horae schedule <id> --start tomorrow\n horae schedule <id> --start +1d --end +1d 14:00\n horae schedule <id> --start +1w --rrule 'FREQ=WEEKLY;BYDAY=MO,WE'",
"log" => "示例:\n horae log \"喝了 3 杯水\"\n horae log",
"pomo" => "示例:\n horae pomo start <task-id>\n horae pomo stop\n horae pomo daemon\n horae pomo waybar",
"ntfy" => "示例:\n horae ntfy test\n horae --profile work ntfy test",
"watch" => "文件夹协议(手机写入,电脑消费):\n capture.txt 每行一条 quick-add 捕获:标题 @tag ~time *rrule !high\n actions.txt done <id|标题> | set <id|标题> status next | set <id|标题> due <时间>\n电脑回写:\n today.md 活动任务快照(Next / Scheduled / Waiting / 逾期)\n reminders/*.md 到期/逾期任务提醒(Syncthing 通过文件变更通知手机)\n *.done 已消费行的回执",
"export" => "示例:\n horae export\n horae export --file ~/backups/horae.json",
"import" => "示例:\n horae import horae-backup-2026-08-15.json\n horae import --replace ~/backups/horae.json",
"completions" => "用法:\n horae completions bash\n horae completions fish\n\n安装到 ~/.bashrc 或 ~/.config/fish/completions/",
"profile" => "示例:\n horae profile list\n horae profile new work\n horae profile new prod1 --db prod1.db\n horae profile rename work work2\n horae profile rm prod1\n horae profile set-default work\n horae --profile work capture \"买牛奶\"",
_ => return None,
})
}
fn zh_arg_help(path: &str, arg: &str) -> Option<&'static str> {
if arg == "help" {
return Some("打印帮助信息 (--help)");
}
if arg == "version" {
return Some("打印版本号 (--version)");
}
Some(match (path, arg) {
("horae", "profile") => "要使用的配置集(数据集);默认为配置中的默认配置集。",
("horae", "lang") => {
"帮助文本的输出语言:`en`(默认)或 `zh`(中文)。也可通过环境变量 `HORAE_LANG` 设置。"
}
(_, "flash") => "以闪念录入模式启动(开启即录入,回车确认录入即关闭)",
(_, "normal") => "以正常模式启动(开启后正常进入主页面)",
("capture", "title") => "任务标题(可省略引号)",
("capture", "clip") => "从系统剪贴板读取内容作为任务标题/语法",
("capture", "tag") => "要添加的标签(可重复)",
("capture", "high") => "设为高优先级",
("capture", "medium") => "设为中优先级",
("capture", "low") => "设为低优先级",
("capture", "due") => "截止时间(now、+2h、today、2026-07-24 14:30)",
("capture", "status") => "初始状态(inbox、next、waiting、scheduled、someday、reference)",
("capture", "notes") => "任务备注/详细描述(剪贴板长文本会自动沉淀在此)",
("capture", "json") => "以 JSON 格式输出新建的任务",
("modify", "text") => "新标题或 quick-add 语法更新内容",
("modify", "title") => "显式设置任务标题(覆盖 quick-add 标题)",
("modify", "tag") => "要添加的标签(可重复)",
("modify", "untag") => "要移除的标签(可重复)",
("modify", "clear_tags") => "清空所有标签",
("modify", "high") => "设为高优先级",
("modify", "medium") => "设为中优先级",
("modify", "low") => "设为低优先级",
("modify", "clear_priority") => "清除优先级",
("modify", "due") => "截止时间(now、+2h、today、2026-07-24 14:30 或 'none' 清除)",
("modify", "clear_due") => "清除截止时间",
("modify", "start") => "计划开始时间(或 'none' 清除)",
("modify", "end") => "计划结束时间(或 'none' 清除)",
("modify", "rrule") => "重复规则(FREQ=DAILY|WEEKLY|MONTHLY|YEARLY 或 'none' 清除)",
("modify", "clear_schedule") => "清空排程(开始、结束与重复规则)",
("modify", "status") => {
"更改状态(inbox、next、waiting、scheduled、someday、reference、done)"
}
("modify", "notes") => "任务备注/详细描述",
("modify", "edit_notes") => "打开 $EDITOR 交互式编辑备注",
("modify", "json") => "以 JSON 格式输出修改后的任务",
("list", "status") => "按状态过滤",
("list", "tag") => "按标签过滤(可重复)",
("list", "due_before") => "仅显示此时间之前截止的任务",
("list", "date") => "仅显示该日期截止的任务,例如 0829",
("list", "json") => "以 JSON 格式输出各行",
("show", "id") => "任务 id(完整 id、唯一前缀或精确标题)",
("show", "json") => "以 JSON 格式输出任务",
("schedule", "start") => "计划开始时间",
("schedule", "end") => "计划结束时间",
("schedule", "rrule") => {
"重复规则(FREQ=DAILY|WEEKLY|MONTHLY|YEARLY;INTERVAL=..;BYDAY=..;BYMONTH=..;COUNT=..|UNTIL=..)"
}
("tag", "name") | ("untag", "name") => "标签名称",
("focus", "start") => "立即为此任务开启番茄钟",
("log", "message") => "要记录的日志内容(省略则列出最近的日志)",
("pomo", "action") => "start、stop、daemon 或 waybar",
("pomo", "task_id") => "要专注的任务(start 时必填)",
("alarm", "action") => "waybar 或 next",
("alarm", "slot") => "位置 1/2:用于排列多个 waybar 闹钟模块;`next` 会跳过相应数量",
("alarm", "limit") => "窗口显示的闹钟任务数量(默认 2)",
("alarm", "all") => "waybar:将整个窗口作为 JSON 数组输出,而非单个位置",
("ntfy", "action") => "test(发送一条示例推送)",
("watch", "dir") => "要监视的同步文件夹(默认 ~/.config/horae/sync)",
("watch", "interval") => "轮询间隔(秒,默认 5)",
("watch", "once") => "只执行一轮后退出,而非一直运行",
("export", "file") => "输出路径(默认 horae-backup-<date>.json)",
("import", "file") => "备份 JSON 文件路径",
("import", "replace") => "清空当前数据并精确还原备份",
("completions", "shell") => "目标 shell(bash、elvish、fish、powershell、zsh)",
("profile new", "name") => "新配置集名称",
("profile new", "db") => "数据库文件(默认 profiles/<name>.db)",
("profile rename", "from") => "原名称",
("profile rename", "to") => "新名称",
("profile rm", "name") => "要删除的配置集名称",
("profile set-default", "name") => "要设为默认的配置集名称",
(_, "lang") => "帮助文本的输出语言:en(默认)或 zh(中文)。",
(_, "profile") => "要使用的配置集(数据集);默认配置中的默认配置集。",
(_, "id") => "任务 id(完整 id、唯一前缀或精确标题)",
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::Cli;
use clap::CommandFactory;
use std::sync::Mutex;
static LANG_ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn default_language_is_english() {
let _guard = LANG_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::remove_var("HORAE_LANG");
assert_eq!(detect_lang(), Lang::En);
}
#[test]
fn env_var_selects_chinese() {
let _guard = LANG_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var("HORAE_LANG", "zh");
assert_eq!(detect_lang(), Lang::Zh);
std::env::remove_var("HORAE_LANG");
std::env::set_var("HORAE_LANG", "中文");
assert_eq!(detect_lang(), Lang::Zh);
std::env::remove_var("HORAE_LANG");
}
#[test]
fn flag_pre_scan_selects_chinese() {
let _guard = LANG_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var("HORAE_LANG", "en");
assert_eq!(detect_lang(), Lang::En);
std::env::remove_var("HORAE_LANG");
}
#[test]
fn localize_overrides_top_level_and_subcommand_to_chinese() {
let mut cmd = Cli::command();
localize(&mut cmd, Lang::Zh);
assert_eq!(
cmd.get_about().map(|s| s.to_string()),
Some("GTD 终端任务管理器".to_string())
);
let capture = cmd
.get_subcommands()
.find(|s| s.get_name() == "capture")
.expect("capture 子命令存在");
assert_eq!(
capture.get_about().map(|s| s.to_string()),
Some("将新条目捕获到收件箱".to_string())
);
}
#[test]
fn localize_is_noop_for_english() {
let mut cmd = Cli::command();
localize(&mut cmd, Lang::En);
assert_eq!(
cmd.get_about().map(|s| s.to_string()),
Some("GTD terminal task manager".to_string())
);
}
}