claude-dashboard 0.2.2

A terminal TUI dashboard that renders Claude Code usage reports with token stats, project breakdowns, and language distribution.
Documentation
use std::sync::OnceLock;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Locale {
    En,
    Zh,
}

pub struct I18n {
    pub locale: Locale,
    // --- tab bar ---
    pub tab_overview: &'static str,
    pub tab_monthly: &'static str,
    pub tab_yearly: &'static str,
    pub tab_llm: &'static str,
    // --- title bar ---
    pub title_dashboard: &'static str,
    pub title_all_time: &'static str,
    pub title_llm_summary: &'static str,
    // --- status bar ---
    pub status_scroll: &'static str,
    pub status_tab_focus: &'static str,
    pub status_navigate: &'static str,
    pub status_copy: &'static str,
    pub status_refresh: &'static str,
    pub status_quit: &'static str,
    pub status_loading: &'static str,
    pub status_parse_errors: &'static str,
    pub status_llm_hint: &'static str,
    // --- summary cards ---
    pub card_projects: &'static str,
    pub card_sessions: &'static str,
    pub card_messages: &'static str,
    pub card_total_tokens: &'static str,
    pub card_cache_hit_rate: &'static str,
    pub card_tool_calls: &'static str,
    pub card_longest_session: &'static str,
    pub card_avg_session: &'static str,
    pub card_peak_period: &'static str,
    // --- chart titles ---
    pub chart_model_usage: &'static str,
    pub chart_language_dist: &'static str,
    pub chart_daily_activity: &'static str,
    pub chart_tool_calls_model: &'static str,
    pub chart_mcp_usage: &'static str,
    pub chart_project_breakdown: &'static str,
    pub chart_model_usage_month: &'static str,
    pub chart_model_usage_year: &'static str,
    pub chart_project_month: &'static str,
    pub chart_project_year: &'static str,
    // --- widget messages ---
    pub msg_no_data: &'static str,
    pub msg_scanning: &'static str,
    pub msg_no_mcp: &'static str,
    pub msg_no_tool_calls: &'static str,
    pub heatmap_less: &'static str,
    pub heatmap_more: &'static str,
    // --- LLM section ---
    pub llm_btn_all: &'static str,
    pub llm_btn_monthly: &'static str,
    pub llm_btn_yearly: &'static str,
    pub llm_thinking_on: &'static str,
    pub llm_thinking_off: &'static str,
    pub llm_report_title: &'static str,
    pub llm_thinking_header: &'static str,
    pub llm_empty_hint: &'static str,
    pub llm_generating: &'static str,
    pub llm_generating_thinking: &'static str,
    pub llm_cached: &'static str,
    pub llm_disconnected: &'static str,
    // --- loading ---
    pub loading_generating: &'static str,
    // --- focus section ---
    pub focus_model_bars: &'static str,
    pub focus_language_bars: &'static str,
    pub focus_tool_calls: &'static str,
    pub focus_project_table: &'static str,
    // --- table headers ---
    pub th_model: &'static str,
    pub th_tool_calls: &'static str,
    pub th_web_search: &'static str,
    pub th_web_fetch: &'static str,
    pub th_total: &'static str,
    pub th_project: &'static str,
    pub th_tokens: &'static str,
    pub th_percent: &'static str,
    // --- misc ---
    pub more_models_fmt: &'static str,
    pub project_unknown: &'static str,
    pub legend_input: &'static str,
    pub legend_output: &'static str,
    pub legend_cache: &'static str,
}

static I18N: OnceLock<I18n> = OnceLock::new();

fn detect_locale() -> Locale {
    // Unix: check standard POSIX locale variables
    for var in &["LC_ALL", "LC_MESSAGES", "LANG"] {
        if let Ok(val) = std::env::var(var) {
            let lower = val.to_lowercase();
            if lower.starts_with("zh") {
                return Locale::Zh;
            }
        }
    }

    // Windows: use GetUserDefaultUILanguage() from kernel32
    #[cfg(windows)]
    {
        let lang_id = unsafe { win32_get_user_default_ui_language() };
        // PRIMARYLANGID(lang_id): extract bits 0–9
        // 0x04 = LANG_CHINESE (zh-CN, zh-TW, zh-HK, zh-SG all share this)
        if (lang_id & 0x03FF) == 0x04 {
            return Locale::Zh;
        }
    }

    Locale::En
}

#[cfg(windows)]
extern "system" {
    /// <https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getuserdefaultuilanguage>
    fn GetUserDefaultUILanguage() -> u16;
}

#[cfg(windows)]
unsafe fn win32_get_user_default_ui_language() -> u16 {
    GetUserDefaultUILanguage()
}

pub fn init() {
    let locale = detect_locale();
    let i18n = match locale {
        Locale::Zh => I18n {
            locale,
            tab_overview: "[1] 概览",
            tab_monthly: "[2] 月报",
            tab_yearly: "[3] 年报",
            tab_llm: "[4] LLM 总结",
            title_dashboard: "Claude Code 仪表盘",
            title_all_time: "全部统计",
            title_llm_summary: "LLM 总结",
            status_scroll: "↑↓ 滚动",
            status_tab_focus: "Tab 聚焦",
            status_navigate: "←→ 导航",
            status_copy: "y 复制",
            status_refresh: "r 刷新",
            status_quit: "q 退出",
            status_loading: "加载中...",
            status_parse_errors: "解析错误",
            status_llm_hint: "a/m/Y=报告 | ←→ 切换月/年",
            card_projects: "项目数",
            card_sessions: "会话数",
            card_messages: "消息数",
            card_total_tokens: "总 Token",
            card_cache_hit_rate: "缓存命中率",
            card_tool_calls: "工具调用",
            card_longest_session: "最长会话",
            card_avg_session: "平均会话",
            card_peak_period: "高峰时段",
            chart_model_usage: "模型 Token 使用 (堆叠)",
            chart_language_dist: "语言分布",
            chart_daily_activity: "每日活动",
            chart_tool_calls_model: "各模型工具调用",
            chart_mcp_usage: "MCP 工具使用",
            chart_project_breakdown: "项目 Token 分布 (全部)",
            chart_model_usage_month: "模型 Token 使用 (月)",
            chart_model_usage_year: "模型 Token 使用 (年)",
            chart_project_month: "项目 Token 分布 (月)",
            chart_project_year: "项目 Token 分布 (年)",
            msg_no_data: "暂无数据",
            msg_scanning: "扫描中...",
            msg_no_mcp: "暂无 MCP 工具使用记录",
            msg_no_tool_calls: "暂无工具调用记录",
            heatmap_less: "",
            heatmap_more: "",
            llm_btn_all: "全部 (A)ll",
            llm_btn_monthly: "月报 (M)onthly",
            llm_btn_yearly: "年报 (Y)early",
            llm_thinking_on: "思考 [T]: ✓",
            llm_thinking_off: "思考 [T]: ✗",
            llm_report_title: "LLM 分析报告",
            llm_thinking_header: "━━━━━━━━━━ 💭 思考过程 ━━━━━━━━━━",
            llm_empty_hint: "按 a/m/Y 或 Enter 生成报告  |  [T] 切换思考模式",
            llm_generating: "正在生成报告...",
            llm_generating_thinking: "正在生成报告 (思考模式)...",
            llm_cached: "已从缓存加载",
            llm_disconnected: "LLM 线程异常断开",
            loading_generating: "正在生成 LLM 报告...",
            focus_model_bars: "模型柱图",
            focus_language_bars: "语言柱图",
            focus_tool_calls: "工具调用",
            focus_project_table: "项目表格",
            th_model: "模型",
            th_tool_calls: "工具调用",
            th_web_search: "Web 搜索",
            th_web_fetch: "Web 获取",
            th_total: "总计",
            th_project: "项目",
            th_tokens: "Tokens",
            th_percent: "%",
            more_models_fmt: "... 还有 {} 个模型",
            project_unknown: "[未知]",
            legend_input: "▊Input ",
            legend_output: "▊Output ",
            legend_cache: "▊Cache",
        },
        Locale::En => I18n {
            locale,
            tab_overview: "[1] Overview",
            tab_monthly: "[2] Monthly Report",
            tab_yearly: "[3] Yearly Report",
            tab_llm: "[4] LLM Summary",
            title_dashboard: "Claude Code Dashboard",
            title_all_time: "All-Time Summary",
            title_llm_summary: "LLM Summary",
            status_scroll: "↑↓ scroll",
            status_tab_focus: "Tab focus",
            status_navigate: "←→ navigate",
            status_copy: "y copy",
            status_refresh: "r refresh",
            status_quit: "q quit",
            status_loading: "Loading...",
            status_parse_errors: "Parse errors",
            status_llm_hint: "a/m/Y=report | ←→ switch month/year",
            card_projects: "Projects",
            card_sessions: "Sessions",
            card_messages: "Messages",
            card_total_tokens: "Total Tokens",
            card_cache_hit_rate: "Cache Hit Rate",
            card_tool_calls: "Tool Calls",
            card_longest_session: "Longest Session",
            card_avg_session: "Avg Session",
            card_peak_period: "Peak Period",
            chart_model_usage: "Model Token Usage (Stacked)",
            chart_language_dist: "Language Distribution",
            chart_daily_activity: "Daily Activity",
            chart_tool_calls_model: "Tool Calls by Model",
            chart_mcp_usage: "MCP Tool Usage",
            chart_project_breakdown: "Project Token Breakdown (All-Time)",
            chart_model_usage_month: "Model Token Usage (Month)",
            chart_model_usage_year: "Model Token Usage (Year)",
            chart_project_month: "Project Token Breakdown (Month)",
            chart_project_year: "Project Token Breakdown (Year)",
            msg_no_data: "No data",
            msg_scanning: "Scanning...",
            msg_no_mcp: "No MCP tool usage recorded",
            msg_no_tool_calls: "No tool calls recorded",
            heatmap_less: "Less",
            heatmap_more: "More",
            llm_btn_all: "All (A)ll",
            llm_btn_monthly: "Monthly (M)onthly",
            llm_btn_yearly: "Yearly (Y)early",
            llm_thinking_on: "Think [T]: ✓",
            llm_thinking_off: "Think [T]: ✗",
            llm_report_title: "LLM Analysis Report",
            llm_thinking_header: "━━━━━━━━━━ 💭 Thinking Process ━━━━━━━━━━",
            llm_empty_hint: "Press a/m/Y or Enter to generate report  |  [T] Toggle thinking mode",
            llm_generating: "Generating report...",
            llm_generating_thinking: "Generating report (thinking mode)...",
            llm_cached: "Loaded from cache",
            llm_disconnected: "LLM thread disconnected unexpectedly",
            loading_generating: "Generating LLM report...",
            focus_model_bars: "Model Bars",
            focus_language_bars: "Language Bars",
            focus_tool_calls: "Tool Calls",
            focus_project_table: "Project Table",
            th_model: "Model",
            th_tool_calls: "Tool Calls",
            th_web_search: "Web Search",
            th_web_fetch: "Web Fetch",
            th_total: "Total",
            th_project: "Project",
            th_tokens: "Tokens",
            th_percent: "%",
            more_models_fmt: "... and {} more models",
            project_unknown: "[unknown]",
            legend_input: "▊Input ",
            legend_output: "▊Output ",
            legend_cache: "▊Cache",
        },
    };
    let _ = I18N.set(i18n);
}

pub fn t() -> &'static I18n {
    I18N.get().expect("i18n::init() not called")
}

pub fn current_locale() -> Locale {
    t().locale
}