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    StorageSafeCleanupHeading,
96    StorageClearSnippets,
97    StorageClearSearchCache,
98    StorageDangerHeading,
99    StorageResetCatalog,
100    StorageResetWarning,
101    // Models view
102    ModelsTitle,
103    ModelsEmbeddingRole,
104    ModelsRerankerRole,
105    ModelsStatusAvailable,
106    ModelsStatusMissing,
107    ModelsKeywordOnlyHint,
108    // Settings view
109    SettingsTitle,
110    SettingsLanguageHeading,
111    SettingsPrivacyHeading,
112    SettingsPrivacyLocalOnly,
113    // Search modes (RFC-009 §8)
114    SearchModeLabel,
115    SearchModeAuto,
116    SearchModeExact,
117    SearchModeConceptual,
118    SearchModeFast,
119    // Match badges
120    BadgeKeyword,
121    BadgeSemantic,
122    BadgeFused,
123    // Startup wizard (design §wizard)
124    WizardTitleNotConfigured,
125    WizardTitleFileMissing,
126    WizardTitleValidating,
127    WizardTitleReady,
128    WizardBodyNotConfigured,
129    WizardBodyFileMissing,
130    WizardFilesNeededLabel,
131    WizardDownloadHint,
132    WizardPathInputPlaceholder,
133    WizardActionLocate,
134    WizardActionValidate,
135    WizardActionUseModel,
136    WizardActionContinue,
137    WizardPathPlaceholder,
138    WizardDownloadAction,
139    WizardDownloadProgress,
140    WizardActionSkip,
141    WizardPreviousPathLabel,
142    WizardValidationOk,
143    WizardValidationFail,
144    WizardReadyBody,
145    // Common actions
146    Cancel,
147    Confirm,
148}
149
150/// Translate a fixed message. The per-locale functions are exhaustive
151/// matches — completeness is enforced by the compiler.
152pub fn tr(locale: Locale, key: MessageKey) -> &'static str {
153    match locale {
154        Locale::En => en::message(key),
155        Locale::Ja => ja::message(key),
156    }
157}
158
159/// Parameterized: "812 files indexed".
160pub fn files_indexed(locale: Locale, count: u64) -> String {
161    match locale {
162        Locale::En => format!("{count} files indexed"),
163        Locale::Ja => format!("{count} 件のファイルをインデックス済み"),
164    }
165}
166
167/// Parameterized: source card summary line.
168pub fn source_summary(locale: Locale, indexed: u64, stale: u64, failed: u64) -> String {
169    match locale {
170        Locale::En => format!("{indexed} indexed · {stale} stale · {failed} failed"),
171        Locale::Ja => format!("インデックス済み {indexed} · 要更新 {stale} · 失敗 {failed}"),
172    }
173}
174
175/// Parameterized: "3 results".
176pub fn search_result_count(locale: Locale, count: usize) -> String {
177    match locale {
178        Locale::En => format!("{count} result{}", if count == 1 { "" } else { "s" }),
179        Locale::Ja => format!("{count} 件の結果"),
180    }
181}