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    NoticeDownloadFailTitle,
154    NoticeDownloadFailBody,
155    NoticeFolderFailTitle,
156    NoticeFolderFailBody,
157    NoticeSearchFailTitle,
158    NoticeSearchFailBody,
159    NoticeFilesMissingTitle,
160    NoticeFilesMissingBody,
161    NoticeFolderAddedTitle,
162    NoticeFolderAddedBody,
163    NoticeSearchReadyTitle,
164    NoticeSearchReadyBody,
165    NoticePreviewsClearedTitle,
166    NoticePreviewsClearedBody,
167    NoticeActionTryAgain,
168    NoticeActionChooseFolder,
169    NoticeDismiss,
170    Cancel,
171    Confirm,
172}
173
174/// Translate a fixed message. The per-locale functions are exhaustive
175/// matches — completeness is enforced by the compiler.
176pub fn tr(locale: Locale, key: MessageKey) -> &'static str {
177    match locale {
178        Locale::En => en::message(key),
179        Locale::Ja => ja::message(key),
180    }
181}
182
183/// Parameterized: "812 files indexed".
184pub fn files_indexed(locale: Locale, count: u64) -> String {
185    match locale {
186        Locale::En => format!("{count} files indexed"),
187        Locale::Ja => format!("{count} 件のファイルをインデックス済み"),
188    }
189}
190
191/// Parameterized: source card summary line.
192pub fn source_summary(locale: Locale, indexed: u64, stale: u64, failed: u64) -> String {
193    match locale {
194        Locale::En => format!("{indexed} indexed · {stale} stale · {failed} failed"),
195        Locale::Ja => format!("インデックス済み {indexed} · 要更新 {stale} · 失敗 {failed}"),
196    }
197}
198
199/// Parameterized: "3 results".
200pub fn search_result_count(locale: Locale, count: usize) -> String {
201    match locale {
202        Locale::En => format!("{count} result{}", if count == 1 { "" } else { "s" }),
203        Locale::Ja => format!("{count} 件の結果"),
204    }
205}