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    WizardActionSkip,
138    WizardPreviousPathLabel,
139    WizardValidationOk,
140    WizardValidationFail,
141    WizardReadyBody,
142    // Common actions
143    Cancel,
144    Confirm,
145}
146
147/// Translate a fixed message. The per-locale functions are exhaustive
148/// matches — completeness is enforced by the compiler.
149pub fn tr(locale: Locale, key: MessageKey) -> &'static str {
150    match locale {
151        Locale::En => en::message(key),
152        Locale::Ja => ja::message(key),
153    }
154}
155
156/// Parameterized: "812 files indexed".
157pub fn files_indexed(locale: Locale, count: u64) -> String {
158    match locale {
159        Locale::En => format!("{count} files indexed"),
160        Locale::Ja => format!("{count} 件のファイルをインデックス済み"),
161    }
162}
163
164/// Parameterized: source card summary line.
165pub fn source_summary(locale: Locale, indexed: u64, stale: u64, failed: u64) -> String {
166    match locale {
167        Locale::En => format!("{indexed} indexed · {stale} stale · {failed} failed"),
168        Locale::Ja => format!("インデックス済み {indexed} · 要更新 {stale} · 失敗 {failed}"),
169    }
170}
171
172/// Parameterized: "3 results".
173pub fn search_result_count(locale: Locale, count: usize) -> String {
174    match locale {
175        Locale::En => format!("{count} result{}", if count == 1 { "" } else { "s" }),
176        Locale::Ja => format!("{count} 件の結果"),
177    }
178}