Skip to main content

orbok_ui/
i18n.rs

1//! Typed i18n message catalog (RFC-031).
2//!
3//! Compile-time completeness: each locale module implements one
4//! exhaustive `match` over [`MessageKey`]. Adding a key without adding
5//! every translation fails the build — there is no runtime fallback
6//! path to hide a missing string.
7//!
8//! Parameterized messages are plain functions (RFC-031 §5.3) so the
9//! compiler also checks their arguments.
10
11pub mod en;
12pub mod ja;
13
14use serde::{Deserialize, Serialize};
15
16/// Supported UI locales. Default English; persisted in the catalog
17/// under the `ui.locale` setting (read/written by `orbok-app`).
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum Locale {
21    #[default]
22    En,
23    Ja,
24}
25
26impl Locale {
27    pub const ALL: &'static [Locale] = &[Locale::En, Locale::Ja];
28
29    /// Setting string stored in `app_settings` (`"en"` / `"ja"`).
30    pub fn as_str(&self) -> &'static str {
31        match self {
32            Locale::En => "en",
33            Locale::Ja => "ja",
34        }
35    }
36
37    pub fn parse(s: &str) -> Option<Locale> {
38        match s {
39            "en" => Some(Locale::En),
40            "ja" => Some(Locale::Ja),
41            _ => None,
42        }
43    }
44
45    /// Self-described language name, shown in the language picker.
46    pub fn display_name(&self) -> &'static str {
47        match self {
48            Locale::En => "English",
49            Locale::Ja => "日本語",
50        }
51    }
52}
53
54/// Every fixed UI string. One variant per string; views never embed
55/// literals (RFC-031 §6 rule 1).
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum MessageKey {
58    // Application chrome
59    AppTitle,
60    LocalOnlyBadge,
61    // Navigation
62    NavSearch,
63    NavSources,
64    NavIndexing,
65    NavStorage,
66    NavModels,
67    NavSettings,
68    // Search view
69    SearchPlaceholder,
70    SearchButton,
71    SearchNoSourcesTitle,
72    SearchNoSourcesBody,
73    SearchAddSource,
74    SearchNoResults,
75    SearchKeywordOnlyNotice,
76    // Sources view
77    SourcesTitle,
78    SourcesEmptyTitle,
79    SourcesEmptyBody,
80    SourcesAddFolder,
81    SourcesStatusActive,
82    SourcesStatusPaused,
83    SourcesStatusMissing,
84    // Indexing view
85    IndexingTitle,
86    IndexingIdle,
87    IndexingHealthIndexed,
88    IndexingHealthStale,
89    IndexingHealthFailed,
90    IndexingHealthQueued,
91    // Storage view
92    StorageTitle,
93    StorageIntro,
94    StorageSafeCleanupHeading,
95    StorageClearSnippets,
96    StorageClearSearchCache,
97    StorageDangerHeading,
98    StorageResetCatalog,
99    StorageResetWarning,
100    // Models view
101    ModelsTitle,
102    ModelsEmbeddingRole,
103    ModelsRerankerRole,
104    ModelsStatusAvailable,
105    ModelsStatusMissing,
106    ModelsKeywordOnlyHint,
107    // Settings view
108    SettingsTitle,
109    SettingsLanguageHeading,
110    SettingsPrivacyHeading,
111    SettingsPrivacyLocalOnly,
112    // Search modes (RFC-009 §8)
113    SearchModeLabel,
114    SearchModeAuto,
115    SearchModeExact,
116    SearchModeConceptual,
117    SearchModeFast,
118    // Match badges
119    BadgeKeyword,
120    BadgeSemantic,
121    BadgeFused,
122    // Common actions
123    Cancel,
124    Confirm,
125}
126
127/// Translate a fixed message. The per-locale functions are exhaustive
128/// matches — completeness is enforced by the compiler.
129pub fn tr(locale: Locale, key: MessageKey) -> &'static str {
130    match locale {
131        Locale::En => en::message(key),
132        Locale::Ja => ja::message(key),
133    }
134}
135
136/// Parameterized: "812 files indexed".
137pub fn files_indexed(locale: Locale, count: u64) -> String {
138    match locale {
139        Locale::En => format!("{count} files indexed"),
140        Locale::Ja => format!("{count} 件のファイルをインデックス済み"),
141    }
142}
143
144/// Parameterized: source card summary line.
145pub fn source_summary(locale: Locale, indexed: u64, stale: u64, failed: u64) -> String {
146    match locale {
147        Locale::En => format!("{indexed} indexed · {stale} stale · {failed} failed"),
148        Locale::Ja => format!("インデックス済み {indexed} · 要更新 {stale} · 失敗 {failed}"),
149    }
150}
151
152/// Parameterized: "3 results".
153pub fn search_result_count(locale: Locale, count: usize) -> String {
154    match locale {
155        Locale::En => format!("{count} result{}", if count == 1 { "" } else { "s" }),
156        Locale::Ja => format!("{count} 件の結果"),
157    }
158}