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    // Common actions
113    Cancel,
114    Confirm,
115}
116
117/// Translate a fixed message. The per-locale functions are exhaustive
118/// matches — completeness is enforced by the compiler.
119pub fn tr(locale: Locale, key: MessageKey) -> &'static str {
120    match locale {
121        Locale::En => en::message(key),
122        Locale::Ja => ja::message(key),
123    }
124}
125
126/// Parameterized: "812 files indexed".
127pub fn files_indexed(locale: Locale, count: u64) -> String {
128    match locale {
129        Locale::En => format!("{count} files indexed"),
130        Locale::Ja => format!("{count} 件のファイルをインデックス済み"),
131    }
132}
133
134/// Parameterized: source card summary line.
135pub fn source_summary(locale: Locale, indexed: u64, stale: u64, failed: u64) -> String {
136    match locale {
137        Locale::En => format!("{indexed} indexed · {stale} stale · {failed} failed"),
138        Locale::Ja => format!("インデックス済み {indexed} · 要更新 {stale} · 失敗 {failed}"),
139    }
140}