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    NavAi,
68    NavSettings,
69    // Search view
70    SearchPlaceholder,
71    SearchButton,
72    SearchNoSourcesTitle,
73    SearchNoSourcesBody,
74    SearchAddSource,
75    SearchNoResults,
76    SearchKeywordOnlyNotice,
77    // Sources view
78    SourcesTitle,
79    SourcesEmptyTitle,
80    SourcesEmptyBody,
81    SourcesAddFolder,
82    SourcesStatusActive,
83    SourcesStatusPaused,
84    SourcesStatusMissing,
85    // Indexing view
86    IndexingTitle,
87    IndexingIdle,
88    IndexingHealthIndexed,
89    IndexingHealthStale,
90    IndexingHealthFailed,
91    IndexingHealthQueued,
92    // Storage view
93    StorageTitle,
94    StorageIntro,
95    StorageGroupSearchIndex,
96    StorageGroupModels,
97    StorageGroupCaches,
98    StorageSafeCleanupHeading,
99    StorageClearSnippets,
100    StorageClearSearchCache,
101    StorageDangerHeading,
102    StorageResetCatalog,
103    StorageResetWarning,
104    // Models view
105    ModelsTitle,
106    ModelsEmbeddingRole,
107    ModelsRerankerRole,
108    ModelsStatusAvailable,
109    ModelsStatusMissing,
110    ModelsKeywordOnlyHint,
111    // Settings view
112    SettingsTitle,
113    SettingsLanguageHeading,
114    SettingsPrivacyHeading,
115    SettingsAdvancedHeading,
116    SettingsAdvancedOn,
117    SettingsAdvancedOff,
118    SettingsAdvancedHint,
119    SettingsPrivacyLocalOnly,
120    // Search modes (RFC-009 §8)
121    SearchModeLabel,
122    SearchModeAuto,
123    SearchModeExact,
124    SearchModeConceptual,
125    SearchModeFast,
126    // Match badges
127    BadgeKeyword,
128    BadgeSemantic,
129    BadgeFused,
130    // Startup wizard (design §wizard)
131    WizardTitleNotConfigured,
132    WizardTitleFileMissing,
133    WizardTitleValidating,
134    WizardTitleReady,
135    WizardBodyNotConfigured,
136    WizardBodyFileMissing,
137    WizardFilesNeededLabel,
138    WizardDownloadHint,
139    WizardPathInputPlaceholder,
140    WizardActionLocate,
141    WizardActionValidate,
142    WizardActionUseModel,
143    WizardActionContinue,
144    WizardPathPlaceholder,
145    WizardDownloadAction,
146    WizardDownloadProgress,
147    WizardActionSkip,
148    WizardPreviousPathLabel,
149    WizardValidationOk,
150    WizardValidationFail,
151    WizardReadyBody,
152    // Common actions
153    Cancel,
154    Confirm,
155}
156
157/// Translate a fixed message. The per-locale functions are exhaustive
158/// matches — completeness is enforced by the compiler.
159pub fn tr(locale: Locale, key: MessageKey) -> &'static str {
160    match locale {
161        Locale::En => en::message(key),
162        Locale::Ja => ja::message(key),
163    }
164}
165
166/// Parameterized: "812 files indexed".
167pub fn files_indexed(locale: Locale, count: u64) -> String {
168    match locale {
169        Locale::En => format!("{count} files indexed"),
170        Locale::Ja => format!("{count} 件のファイルをインデックス済み"),
171    }
172}
173
174/// Parameterized: source card summary line.
175pub fn source_summary(locale: Locale, indexed: u64, stale: u64, failed: u64) -> String {
176    match locale {
177        Locale::En => format!("{indexed} indexed · {stale} stale · {failed} failed"),
178        Locale::Ja => format!("インデックス済み {indexed} · 要更新 {stale} · 失敗 {failed}"),
179    }
180}
181
182/// Parameterized: "3 results".
183pub fn search_result_count(locale: Locale, count: usize) -> String {
184    match locale {
185        Locale::En => format!("{count} result{}", if count == 1 { "" } else { "s" }),
186        Locale::Ja => format!("{count} 件の結果"),
187    }
188}